views:

183

answers:

2

I have multiple superposed controls which can handle a mouse click under certain conditions. What I want to be able to do is:

  1. The top control receives the mouseDown: event.
  2. The top control decides if it handles the mouseDown: event.
  3. If it does, do something and prevent other controls from receiving the mouseDown: event.
  4. If it does not, send the event to the control that's underneath.
  5. This control decides if it handles the event.
  6. etc.

In essence I'm trying to send the event to the control whose "Z-Order" is just below the top control, without the top control needing to know about the other controls or needing some special setup at instantiation.

The first thing that came to my mind was to send the event to [topControl nextResponder] but it seems the nextResponder for all controls on the window is the window itself and not a chain of controls based on their Z-Order as I previously thought.

Is there a way to do this without resorting to setting the next responder manually? The goal is to get a control which is independent from the other controls and can just be dropped on a window and work as expected.

Thanks in advance!

+1  A: 

It's hard to know exactly the best approach because I don't know what your application does, but here's a thought. It sounds like you want to pass the messages up through the view hierarchy... somehow.

Regardless, a view would do one of two things:

  • handle the message
  • pass it to the "next view" (how you define "next view" depends on your application)

So. How would you do this? The default behavior for a view should be to pass the message to the next view. A good way of implementing this kind of thing is through an informal protocol.

@interface NSView (MessagePassing)

- (void)handleMouseDown:(NSEvent *)event;
- (NSView *)nextViewForEvent:(NSEvent *)event;

@end

@implementation NSView (MessagePassing)

- (void)handleMouseDown:(NSEvent *)event {
    [[self nextView] handleMouseDown:event];
}

- (NSView *)nextViewForEvent:(NSEvent *)event {
    // Implementation dependent, but here's a simple one:
    return [self superview];
}

@end

Now, in the views that should have that behavior, you'd do this:

- (void)mouseDown:(NSEvent *)event {
    [self handleMouseDown:event];
}

- (void)handleMouseDown:(NSEvent *)event {
    if (/* Do I not want to handle this event? */) {
        // Let superclass decide what to do.
        // If no superclass handles the event, it will be punted to the next view
        [super handleMouseDown:event];
        return;
    }

    // Handle the event
}

You would probably want to create an NSView subclass to override mouseDown: that you would then base your other custom view classes on.

If you wanted to determine the "next view" based on actual z-order, keep in mind that z-order is determined by the order within the subviews collection, with later views appearing first. So, you could do something like this:

- (void)nextViewForEvent:(NSEvent *)event {
    NSPoint pointInSuperview = [[self superview] convertPoint:[event locationInWindow] fromView:nil];
    NSInteger locationInSubviews = [[[self superview] subviews] indexOfObject:self];
    for (NSInteger index = locationInSubviews - 1; index >= 0; index--) {
        NSView *subview = [[[self superview] subviews] objectAtIndex:index];
        if (NSPointInRect(pointInSuperview, [subview frame]))
            return subview;
    }
    return [self superview];
}

This might be way more than you wanted, but I hope it helps.

Alex
That's a very interesting answer, I'll most probably use the second part of your answer, it seems to be a good way to get the views based on their z-order. Thanks!
Form
A: 

All you have to do is call [super mouseDown:event]. Since Mac OS X 10.5 (this did not work the same way before) NSView knows how to handle overlapping views and will take care of event handling for you.


If you need to target releases before 10.5: This is a really bad idea. Not only does the event handling mechanism not know how to deal with overlapping subviews, neither does the drawing system and you can see some very strange artefacts. That said, if you're determined:

Override -[NSView hitTest:] in your custom control/view. AppKit uses this method to determine which view in the hierarchy to deliver mouse events to. If you return nil that point in your custom view is ignored and the event should get delivered to the next view covering that point.

Mind though, this is still a bad idea because of the reasons I outlined above. It just wasn't something formally supported by AppKit at the time. The more generally accepted workaround on 10.4 and earlier is to use a child window.

Mike Abdullah
That's nice to know, thanks! However, I must support at least 10.4 so I'll have to do this another way. Currently I'm setting the next responder manually and it works, but it's not optimal. Surely there is a cleaner way to do this on 10.4 and older...
Form
Could you elaborate on the child window please?
Form
Create a borderless window that is the size of your custom control and attach it to the main window as a child window. Thus you gain the effect of overlapping views without having actually done so. It's somewhat of a pain to do, but like I said overlapping views are not supported on 10.4- and trying to force them to work is dangerous.
Mike Abdullah
The hitTest override you mentioned is great, it works as intended on 10.5, so I'll be using this for my 10.5+ projects if I have to support this kind of behaviour again. I'm not sure about the child windows though… it looks like a pretty far fetched hack to me. Personally I think manually setting the next responder (or another variable to use for passing events) is a *little* better (still not very "clean" but well…). Thanks for taking the time to answer my question!
Form
Well like I said, 10.4 doesn't support overlapping views. It's not a hack to get the desired event handling behaviour, but a hack to get the effect of an overlapping view in the first place.
Mike Abdullah