2
votes

(Targeting OS X 10.10, latest version of Xcode)

I have an NSButton subclass.

The button needs to respond to mouse rollovers/hovers, ala a web button.

The button requires an appearance in the shape of a circle. Accordingly, -setImage: and -setAlternateImage: are called with circular-shaped graphics.

Problem 1: the default behaviour of NSButton results in a positive hit-test when the mouse is clicked anywhere within the rect of the NSButton. This includes mouse clicks that are outside the circular region of the button.

Proposed solution: override -hitTest: to calculate distance of click from centre of the button. If distance > button's radius, we don't have a valid click.

OK, fine. That provides us with accurate collision detection on mouse clicks on circular buttons.

Problem 2: how to handle mouse hover/rollover, and changing the appearance of the button accordingly.

Proposed solution: add an NSTrackingArea to the button, and then override -mouseEntered: and -mouseExited: so we can change the button image and alternateImage as the mouse moves over the button.

OK, fine. That works for rectangles.

But... NSTrackingArea works on rectangles, not arbitrary regions such a circle. Worse, it does NOT honour -hitTest:

Proposed solution: add the circular collision detection code in to -mouseEntered: and -mouseExited:

But...

-mouseEntered: and -mouseExited: are only called ONCE per button entry/exit, not continuously. This means that if the mouse is moved just in to the button's rectangular region, but not far enough that it enters the button's circular region, -mouseEntered: will be called. But it will not be called again, until after -mouseExited: is called. Further movement of the mouse in to the circular region will have no effect. (No rollover will occur.)

i.e. it is not possible to continuously update the button's state with accurate mouse position information using these two methods.

QUESTION: does anyone know how to implement hover and pressing on a non-rectangular button?

[edit - SOLVED thanks to cacau.]

Solution steps:

  1. Add an NSTrackingArea to the button, with (at a minimum) the NSTrackingMouseEnteredAndExited and NSTrackingMouseMoved options.

  2. Implement -hitTest: in order to do accurate (e.g. circular) hit testing for button presses. We do not change the appearance of the button here.

  3. Implement -mouseEntered: and -mouseExited:

  4. Do (circular) collision detection within -mouseEntered, changing the appearance of the button accordingly via -setImage: (or otherwise flagging changes to your drawing strategy.)

  5. On -mouseExited: set the button's appearance back to its default.

  6. But, -mouseEntered: and -mouseExited: are not called continuously. Therefore we also need to implement -mouseMoved:

  7. But, we must call -setAcceptsMouseMovedEvents:YES on the window, so that the -mouseMoved: method is called on the button.

  8. Add the (circular) collision detection and appearance changing to -mouseMoved as well.

2
You've not actually targeting 10.0, are you? :-)cacau

2 Answers

0
votes

Using NSTrackingArea is the way to go, for non-rectangular shapes you'll have to add custom code to perform hit testing for your custom geometries.

Not clear why you'd want to continuously set the same image when the mouse is hovering above your shape?

Anyway, there's mouseMoved: to get notified when the mouse moves within you tracking area. Check the docs, though - you might need to set some extra flags on the view or window as the default behaviour might not send moved messages to not clog the framework with huge amount of messages in case nobody listens..

0
votes

So you do want to use tracking areas and mouse events. These help you know if you want to hittest a point or not.

You probably actually want a slightly larger area than the image in most cases. It appears more natural.

If your button is image based, use this NSImage method for hit testing

hitTestRect:withImageDestinationRect:context:hints:flipped:

Remember a rect can be the size of a point.

https://developer.apple.com/library/mac/documentation/Cocoa/Reference/ApplicationKit/Classes/NSImage_Class/index.html#//apple_ref/occ/instm/NSImage/hitTestRect:withImageDestinationRect:context:hints:flipped:

If your button is path based, NSBezierPath and CGPath have their own contains point kind of solutions.

Spend your time here. It's one of the experiences people have most in a UI so if it's not just right it will feel clunky.