5
votes

I have a problem that I think is solvable with some hackery, but I'm very curious if there is an easier way to get the job done without having to do all of that.

I have a stack of NSViews (layer-backed, if that somehow helps provides some better solution), as shown below:

The view stack/layout

The thing here is that this is essentially a menu, but is hover-sensitive. If the user hovers over one of the exposed parts of the lower-level views, I need to perform an action depending on what that view is. It is a dynamic system so the number of stacked menu items like this may change, making static calculations more difficult. As you can see, they are basically all a copy (shape-wise) of the first item, but then rotated a bit the further you go down the stack via simple transform rotation.

My question to the SO community is what do you all think the best approach to getting mouseEntered: and mouseExited: events for just the literally visible portions of these views?

What I have attempted to do is use an NSTrackingArea on the visibleRect portion of these views, which sounds much more handy than it really is in this situation. In reality, the visibleRect seems to be "visible" for all of them, all the time. Nothing is explicitly blocked or hidden by anything more than just a partially overlapping NSView. All that happens is I get a spammed console from all of the views screaming out at once that a mouse entered their rect.

Something I am considering is making sub-NSView's of each menu item and having each of those be responsible for the tracking area... each menu item having a "strip" view along the right and bottom sides that could report, but that's still a bit of a hack and is icky.

Does anyone have a better idea? Perhaps one from experience?

Thanks!

4

4 Answers

3
votes

I know you already have a solution, but I thought I would try a different approach, that didn't require getting tons of mouseMoved events. I created 3 custom views in code, added tracking rects for them and sent all mouseEntered and mouseExited messages to the same method that does a hitTest to determine which view is top most. This is the code for the content view of the window.

@implementation MainView
@synthesize oldView;

-(void)awakeFromNib {
    oldView = nil;
    Card *card1 = [[Card alloc]initWithFrame:NSMakeRect(150, 150, 200, 150) color:[NSColor redColor] name:@"Red Box"];
    NSTrackingArea *area1 = [[NSTrackingArea alloc]initWithRect:card1.frame options:NSTrackingMouseEnteredAndExited|NSTrackingActiveInActiveApp owner:self userInfo:nil];
    [self addTrackingArea:area1];
    [self addSubview:card1];

    Card *card2 = [[Card alloc]initWithFrame:NSMakeRect(180, 120, 200, 150) color:[NSColor yellowColor] name:@"Yellow Box"];
    NSTrackingArea *area2 = [[NSTrackingArea alloc]initWithRect:card2.frame options:NSTrackingMouseEnteredAndExited|NSTrackingActiveInActiveApp owner:self userInfo:nil];
    [self addTrackingArea:area2];
    [self addSubview:card2];

    Card *card3 = [[Card alloc]initWithFrame:NSMakeRect(210, 90, 200, 150) color:[NSColor greenColor] name:@"Green Box"];
    NSTrackingArea *area3 = [[NSTrackingArea alloc]initWithRect:card3.frame options:NSTrackingMouseEnteredAndExited|NSTrackingActiveInActiveApp owner:self userInfo:nil];
    [self addTrackingArea:area3];
    [self addSubview:card3];
}

-(void)mouseEntered:(NSEvent *)theEvent {
    [self reportTopView:theEvent];
}

-(void)mouseExited:(NSEvent *)theEvent {
    [self reportTopView:theEvent];
}

-(void)reportTopView:(NSEvent *)theEvent {
    id topView = [self hitTest:[theEvent locationInWindow]];
    if (![topView isEqual:oldView]) {
        oldView = topView;
        ([topView isKindOfClass:[Card class]])? NSLog(@"%@",[(Card *)topView name]):NULL;
    }
}

This is the code for what I called cards (colored rectangles):

@implementation Card
@synthesize name,fillColor;

- (id)initWithFrame:(NSRect)frame color:(NSColor *)color name:(NSString *)aName{
    self = [super initWithFrame:frame];
    if (self) {
        self.fillColor = color;
        self.name = aName;
    }
    return self;
}

- (void)drawRect:(NSRect)rect {
    [self.fillColor drawSwatchInRect:rect];

}
1
votes

I finally came to a solution on Twitter via Steven Troughton-Smith. Here's how it works:

In each menu item, I am disregarding anything related to NSTrackingArea or direct mouse position interpretation. Instead, the parent controller view is handling all of the tracking and receiving mouse movement events.

Each menu item has an overridden hitTest: method that does the point conversion and returns whether or not the point being tested is within the background image (there are shadows and stuff in there, making it more difficult than the vanilla implementation).

I then setup a sort of "hover menu item changed" callback in the controller so that I can handle hover menu changes.

This was a pretty straightforward solution. Very glad I decided to stop and ask, rather than hack something together with my previous idea.

Thanks Steven!

1
votes

Overlapping tracking-areas:

All you have to do is hitTest from view you are in. if this is true:

 window.view.hitTest(window.mousePos) === self/*sudo code*/

What this code does is that it returns the view under the mouse position. Now all you have to do is setup a few "if" and "else" clauses to verify that your mouse is off or on the view.

Full code example:
https://gist.github.com/eonist/537ae53b86d5fc332fd3

Full description of the concept here: (perma link)
http://stylekit.org/blog/2015/12/20/Overlapping-tracking-areas/

Example

VS the default enter and exit behaviour:

enter image description here

0
votes

Example

I had to add another answer to this question as this is another approach to solve the problem. This approach now also includes path assertion (think rects with round edges or other custom paths)

The answer is long winded but it works:

http://stylekit.org/blog/2016/01/28/Hit-testing-sub-views/

it involves using the apple provided method: CGPathContainsPoint(path,transform,point)

If you follow the link to that blog post and then from there check the styleKit repo on github. You will find the code need to achieve the gif animation example given above. Im providing this as a pointer to the answer as it may take you significantly less time than trying to research this on your own. I use this technique in all my UI elements and it works flawlessly.

Example