hitTest() and LookAndFeel


#1

The other day I found myself implementing a nice round knob using PNG images as pre-rendered frames.

I implemented that using a standard Slider for which I wrote a custom LookAndFeel that took care of painting the appropriate frame for the current Slider's value.

The problem came when I went to try it and realized that (obviously) the whole recangle that encloses the frames is sensible to mouse click and drag, not just the round shape of the knob. Since it is a fairly large knob, that leaves four big corners around it where the user may happen to click triggering an unwanted adjustment of the Slider's value.

The obvious solution here is: create a subclass of Slider and implement a correct hitTest() method for it, that knows about the knob's centre and radius and can tell if the click has happened inside the knob or not.

That's true, but since the "Look" of my knob is determined by the LookAndFeel class I assigned to it (because it basically works as a delegate for the Slider::paint() calls), what do you think about making also the "Feel" of the Slider depend on the LookAndFeel that's assigned to it, by forwarding the Slider::hitTest() calls to appropriate methods inside LookAndFeel? (and that applies not only to Sliders but also Buttons, ScrollBars, etc.)

My feeling is that the two aspects are tightly related and thus should go together in the LookAndFeel class.

After all, what's the point of using a standard widget with a custom LookAndFeel, if I have to subclass the widget class anyway, to implement the proper hitTest() that reflects what's being drawn by the LookAndFeel?


#2

If you're not going to photo realistic why not just use svg's and load them directly into your lookAndFeel methods? You can then perform transforms on them directly. In this way you need only produce one svg of the slider knob, and in the look and feel method you simply rotate it. In the long run it's much handier. In this rather ugly screenshot you can see the svg image open in Inkscape on the left.

On the right is the slider with this svg drawn on it. You can't see from the screenshot, but I can drag and rotate the svg simply by drawing it directly in the slider's look and feel methods. The one limitation with this is that you must keep you svg's pretty simple.


#3

I'm not sure you got the point... the problem is not painting the round thing, the problem is that, after the circular knob is being correctly painted, the Slider object has a rectangular shape and clicks inside it, even if they don't hit the circular knob, will be detected as clicks on the slider and will allow the user to drag it to adjust the value.

In your example image, imagine the user clicks in the very top left corner of the square that encloses the knob. There isn't an actual "knob" there, but the Slider class takes it as a valid click and allows you to drag the knob.

This kind of thing is usually dealt with by writing your own hitTest() method, but it seems to me that it is overkill to create your own subclass of Slider for a single method, and I think it is more appropriate if the LookAndFeel could take care of this subject as well as drawing


#4

Ah yes, I know what you mean now. 


#5

jules, any word about my suggestion above?


#6

I'd be reluctant to add that to the l+f.. That class is already far more bloated than I'd like it to be, and if I added a hit-test for sliders, then it'd imply that there should also be similar methods for all the buttons, etc..


#7

Yes exactly.. that's why the LookAndFeel class has been refactored to have multiple inheritance from the LookAndFeel-lets that are now nested classes in the various widget classes, right? So you can easily add a method for one widget and have it in the LookAndFeel class.

And yes, the final result would be that all of these hitTest methods would be included in the LookAndFeel class, one per each widget, but I don't think that's poor design. Or, better, I think the current situation is worse than that, because now we can delegate the LookAndFeel for the painting, but not for the hitTest, so we now must subclass the LookAndFeel to get our custom implementation of the painting AND also have to subclass the widget's class to implement the custom hitTest.

If you want my opinion, I think LookAndFeels should include methods for painting AND hitTest, and there shouldn't be a single LookAndFeel class that handles those for all the widgets, but every widget that has the feature to delegate painting and hitTest to a look and feel should have a nested class with only the relevant methods for that, _exactly_ like Listeners.

Until then, integrating hitTest methods in the LookAndFeel still feels like a better solution than having to subclass two classes instead of one


#8

Yes, I do understand, and your arguments are perfectly good.. I just don't like the way that it feels like functionality is creeping inexorably into the l+f classes! I wish I had a better alternative suggestion, but I don't really want to add that right now - let me mull it over!


#9

Sure, take your time to think about it!

Just to throw some wild ideas about the subject into your mulling engine, by now I have considered (and implemented, to some degree) these classes that more or less are pertinent to the subject:

/** Classes derived from this can delegate the painting of some components
 to a Delegate instance.

 This makes it possible to concentrate the painting for different components
 into a single method in the delegate, thus avoiding the need to subclass every
 Component just to properly implement its paint() method.
 */
class PaintableWithDelegate
{
public:

    /** Classes that receive paint requests from PaintableWithDelegate objects
     should derive from this class */
    class Delegate
    {
    public:
        virtual ~Delegate () { }

        /** This method is called when the delegate should paint the given
         component in place of the component's own paint() method.
         _source_ is the Component to be painted, _g_ is the Graphics context
         into which it should be painted */
        virtual void paint (Component* /*source*/, Graphics& /*g*/) { }
    };

    explicit PaintableWithDelegate () : m_delegate (nullptr) { }
    virtual ~PaintableWithDelegate () { }

    /** Sets _delegate_ as the Delegate to be called when delegatePaint() is invoked. */
    void setPaintDelegate (Delegate* delegate) { m_delegate = delegate; }

    /** This method is called when a PaintableWithDelegate object asked the
     delegate to paint the given _component_ inside the Graphics _g_*/
    bool delegatePaint (Component* component, Graphics& g)
    {
        if (m_delegate == nullptr)
            return false;

        m_delegate->paint (component, g);
        return true;
    }

private:
    Delegate* m_delegate;
};

 

This "PaintableWithDelegate" approach could be translated to the hitTest thing creating a similar "HittableWithDelegate" base class.

 

/** A base class for those widgets that need to have a custom hit region */
class SettableHitArea
{
public:
    SettableHitArea () : m_areaToTest (lookAndFeelWhenAvailable) { }
    virtual ~SettableHitArea () { }

    void resetHitArea (bool queryLookAndFeelWhenAvailable = true);
    void setRectangularHitArea (Rectangle <int> area);
    void setRoundHitArea (Point <float> centre, float radius);

    bool checkHit (int x, int y) const;

private:
    enum AreaToTest
    {
        all,
        lookAndFeelWhenAvailable,
        rectangular,
        round,
    };

    AreaToTest m_areaToTest;

    Rectangle <int> m_rectangularHitArea;

    Point <float> m_roundHitAreaCentre;
    float m_roundHitAreaRadiusSquared;
};


/** Resets the hit region. If _queryLookAndFeelWhenAvailable_ is true and this
 is a Component whose LookAndFeel also derives from SettableHitArea, it queries
 that LookAndFeel for hit tests. If false, every test sent to checkHit()
 will return true. */
void SettableHitArea::resetHitArea (bool queryLookAndFeelWhenAvailable)
{
    m_areaToTest = (queryLookAndFeelWhenAvailable ? lookAndFeelWhenAvailable : all);
}

/** Sets the hit region to be the specified rectangular _area_. */
void SettableHitArea::setRectangularHitArea (Rectangle <int> area)
{
    m_rectangularHitArea = area;
    m_areaToTest = rectangular;
}

/** Sets the hit region to be a circle with given _centre_ and _radius_ */
void SettableHitArea::setRoundHitArea (Point <float> centre, float radius)
{
    m_roundHitAreaCentre = centre;
    m_roundHitAreaRadiusSquared = radius * radius;
    m_areaToTest = round;
}

/** Checks whether _x_ and _y_ fall within the current hit region */
bool SettableHitArea::checkHit (int x, int y) const
{
    switch (m_areaToTest)
    {
        case all:
            return true;

        case lookAndFeelWhenAvailable:
        {
            const Component* c = dynamic_cast <const Component*> (this);
            if (c == nullptr)
                return true;

            const SettableHitArea* lookAndFeelHitArea = dynamic_cast <const SettableHitArea*> (&c->getLookAndFeel());
            if (lookAndFeelHitArea == nullptr)
                return true;

            return lookAndFeelHitArea->checkHit (x, y);
        }

        case rectangular:
            return m_rectangularHitArea.contains (x, y);

        case round:
        {
            const float deltaX = x - m_roundHitAreaCentre.getX ();
            const float deltaY = y - m_roundHitAreaCentre.getY ();
            return ((deltaX * deltaX) + (deltaY * deltaY) <= m_roundHitAreaRadiusSquared);
        }

        default:
            jassertfalse;
            return true;
    }
}

This other one is a base class meant to be a base class for either Components or LookAndFeel derived classes.

Feel free to steal ideas or piece of codes from those classes at your will