4

I want to design a panel acting a little like a popover: when mouse down outside it, it may dismiss or hide itself.

But I have no idea about how to achieve this. What I know is that a view could handle -mouseDown, -mouseUp, etc. But when mouse down at other place? I don't know how to catch this event.


Further discussion with Bavarious:

I am actually doing a study on status bar. There is a question I followed. And the sample code I studied.

As the sample code did, I overwrote the view described previously and set it with the status bar item's -setView: method. Most of my codes in my work are quite the same as the sample code. Here are some parts of codes those I think are related to my confusion (BTW, ARC is used):

...
@property (nonatomic) SEL disclickAction;     // Called when "dismissed"
@property (nonatomic) SEL action;             // Called when selected
@property (nonatomic, assign) id target;  
...


- (void)dealloc
{
    NSLog(@"%@ dealloc", self);
    [self invalidate];
    //[super dealloc];
}

- (void)mouseDown:(NSEvent *)theEvent
{
    [self setHighlighted:![self isHighlighted]];
    if (_target && _action &&
        [_target respondsToSelector:_action])
    {
        [NSApp sendAction:_action to:_target from:self];
    }
}

// Here is the code that Bavarious taught me:
- (void)setDisclickAction:(SEL)disclickAction
{
    _disclickAction = disclickAction;

    if (!_mouseEventMonitor)
    {
        if (_disclickAction)
        {
            self.mouseEventMonitor = [NSEvent
                           addLocalMonitorForEventsMatchingMask:(NSLeftMouseDownMask | NSRightMouseDownMask | NSOtherMouseDownMask) 
                           handler:^NSEvent *(NSEvent *event) {
                if (event.window != self.window)
                {
                    [self actionDisclick:nil];
                }
                return event;
            }];
        
            [[NSNotificationCenter defaultCenter]
             addObserver:self
             selector:@selector(actionDisclick:)
             name:NSApplicationDidResignActiveNotification
             object:nil];
        }
    }
    else if (!_disclickAction)  // cancel operation
    {
        [NSEvent removeMonitor:_mouseEventMonitor];
        _mouseEventMonitor = nil;
    }
}

Here is my screen, for example (the yellow cartoon face is my status bar item):

When left mouse down at positions above:
A: The view's mouseDown called, and then the local observer of mouse down event notified.
B: local observer of mouse down event notified.
C: Resign application event notified.
D: No event. Neither the local observer of mouse down event. And this is the problem.

Glorfindel
  • 21,988
  • 13
  • 81
  • 109
Andrew Chang
  • 1,289
  • 1
  • 18
  • 38

2 Answers2

11

AppKit takes care of forwarding mouse events to the window/view where those events took place. If you wish to catch mouse events in other locations, you can use a local event monitor.

In the class that’s supposed to implement this behaviour, which is probably the class that owns the panel, define an instance variable or declared property to hold an event monitor instance:

@property (nonatomic, strong) id mouseEventMonitor;

When you show your panel, add a mouse event monitor:

self.mouseEventMonitor = [NSEvent addLocalMonitorForEventsMatchingMask:(NSLeftMouseDownMask | NSRightMouseDownMask | NSOtherMouseDownMask) handler:^NSEvent *(NSEvent *event) {
    if (event.window != self.panelWindow)
        [self dismissPanel];

    return event;
}];

When you dismiss the panel, remove the mouse event monitor:

- (void)dismissPanel {
    if (self.mouseEventMonitor != nil) {
        [NSEvent removeMonitor:self.mouseEventMonitor];
        self.mouseEventMonitor = nil;
    }

    // …
}

If you need to test a particular view instead of the entire window containing the panel, use -[NSView hitTest:] to check if the mouse location (event.locationInWindow) belongs to that view or one of its subviews.


Edit: In order to dismiss the panel if the user clicks outside of the application windows, observe NSApplicationDidResignActiveNotification, which is posted whenever the application resigns its active status, meaning that some other application has become active. When you show the panel:

[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(dismissPanel) name:NSApplicationDidResignActiveNotification object:nil];

And when you dismiss the panel:

[[NSNotificationCenter defaultCenter] removeObserver:self name:NSApplicationDidResignActiveNotification object:nil];

Edit: To handle the case whether the user has clicked the menu bar (which does not post NSApplicationDidResignActiveNotification since the application is still active), you must also observe NSMenuDidBeginTrackingNotification posted by the main menu, [NSApp mainMenu]. When you show the panel:

[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(dismissPanel) name:NSMenuDidBeginTrackingNotification object:[NSApp mainMenu]];

And when you dismiss the panel:

[[NSNotificationCenter defaultCenter] removeObserver:self name:NSMenuDidBeginTrackingNotification object:[NSApp mainMenu]];
-3

You can create a transparent or semi-transparent background view that is a UIButton. You can place any view elements on top of it. Add a handler to the button to handle closing or hiding the view. Something like this:

- (id)init
{
self = [super init];
    if(self)
    {
        [self createBackground];
        [self createUIElements]; 
    }
    return self;
}

- (void)createBackground
{
    UIButton *backgroundButton = [[UIButton alloc] initWithFrame:CGRectMake(0, 0, self.frame.size.width, self.frame.size.height)];
    [backgroundButton addTarget:self forSelector:@selector(closeOrHide) forControlEvent:UIControlEventTouchUpInside];
    [self addSubview:backgroundButton];

    //If no-arc    [backgroundButton release]; backgroundButton = nil
}

- (void)closeOrHide
{
    //Do stuff
}
jfuellert
  • 550
  • 4
  • 10