Dismissing menu from menu item that uses custom view

  • I have an NSView subclass that's used to draw contents of a menu item. When I get a mouseUp in my view, I need to flash the hilite (I'm guessing just draw it a couple times with a short delay between draws), dismiss the menu and perform the action associated with the item. These seem like the right methods to use, but the menu is not closing before the action executes. What's a better way to do this? The MenuItemView sample app also has this problem when you click the Button.

    - (void)mouseUp:(NSEvent*)event
    {
    UNUSED_VAR(event);

    NSMenuItem*  item = [self enclosingMenuItem];
    NSMenu*  menu = [item menu];

    // On mouseUp, we want to dismiss the menu being tracked:
    [menu cancelTracking];

    // Then send the action to the target:
    SEL    act = [item action];
    id    targ = [item target];

    if(act != nil && targ != nil)
    //        [NSApp sendAction:act to:targ from:item];
    // Temporarily sending the About menu item so it'll fire the About box:
      [NSApp sendAction:act to:targ from:[menu itemAtIndex:0]];

    // Copied from MenuItemView sample project. I don't think this is necessary:
    //    [self setNeedsDisplay:YES];
    }

    --
    Steve Mills
    office: 952-818-3871
    home: 952-401-6255
    cell: 612-803-6157
  • On 30/05/2013, at 8:41 AM, Steve Mills <smills...> wrote:

    > but the menu is not closing before the action executes

    This is normal. I think the idea is that the user gets a cue that whatever action is being executed "came from" a menu choice, so the menu remains visible while the action is carried out.

    Further, if you don't want this, calling -cancelTracking probably only schedules the closure, and requires some iterations of the runloop to take effect. The easiest solution might be not to directly invoke your action, but schedule that to be performed after a short delay (maybe 0), which will let the menu close first before invoking the action.

    --Graham
  • Use performSelector:withObject:afterDelay:0 to push your response to the end of the run loop, giving the UI a chance to update first. It's what I do when a button (etc) is going to trigger something that won't be instantaneous, where the button shouldn't keep showing in the pressed state while it happens.

    On May 29, 2013, at 3:41 PM, Steve Mills wrote:

    > I have an NSView subclass that's used to draw contents of a menu item. When I get a mouseUp in my view, I need to flash the hilite (I'm guessing just draw it a couple times with a short delay between draws), dismiss the menu and perform the action associated with the item. These seem like the right methods to use, but the menu is not closing before the action executes. What's a better way to do this? The MenuItemView sample app also has this problem when you click the Button.
    >
    > - (void)mouseUp:(NSEvent*)event
    > {
    > UNUSED_VAR(event);
    >
    > NSMenuItem*        item = [self enclosingMenuItem];
    > NSMenu*            menu = [item menu];
    >
    > // On mouseUp, we want to dismiss the menu being tracked:
    > [menu cancelTracking];
    >
    > // Then send the action to the target:
    > SEL                act = [item action];
    > id                targ = [item target];
    >
    > if(act != nil && targ != nil)
    > //        [NSApp sendAction:act to:targ from:item];
    > // Temporarily sending the About menu item so it'll fire the About box:
    > [NSApp sendAction:act to:targ from:[menu itemAtIndex:0]];
    >
    > // Copied from MenuItemView sample project. I don't think this is necessary:
    > //    [self setNeedsDisplay:YES];
    > }
    >
    > --
    > Steve Mills
    > office: 952-818-3871
    > home: 952-401-6255
    > cell: 612-803-6157
  • On May 29, 2013, at 18:06:45, Lee Ann Rucker <lrucker...>
    wrote:

    > Use performSelector:withObject:afterDelay:0 to push your response to the end of the run loop, giving the UI a chance to update first. It's what I do when a button (etc) is going to trigger something that won't be instantaneous, where the button shouldn't keep showing in the pressed state while it happens.

    Good idea, but I need to send the 3rd parameter; the selector, the target, and the sender. I can't do that with performSelector:withObject:afterDelay:.

    --
    Steve Mills
    office: 952-818-3871
    home: 952-401-6255
    cell: 612-803-6157
  • On May 29, 2013, at 17:59:58, Graham Cox <graham.cox...>
    wrote:

    > This is normal. I think the idea is that the user gets a cue that whatever action is being executed "came from" a menu choice, so the menu remains visible while the action is carried out.

    That's totally untrue. When an item is chosen, the menu goes away, but the menu title in the menubar remains hilited until control is returned to the event loop that spawned the menu. Leaving the menu visible would be very ugly and confusing.

    --
    Steve Mills
    office: 952-818-3871
    home: 952-401-6255
    cell: 612-803-6157
  • Don't delay the action/target part, write a wrapper function and delay that:

    - (void)mouseUp:(NSEvent*)event
    {
    UNUSED_VAR(event);

    NSMenuItem*  item = [self enclosingMenuItem];
    NSMenu*  menu = [item menu];
    // On mouseUp, we want to dismiss the menu being tracked:
    [menu cancelTracking];
    [self performSelector:@selector(delayedSendAction:) withObject:item afterDelay:0];
    }

    - (void)delayedSendAction: (NSMenuItem *)item
    {
    // Then send the action to the target:
    SEL    act = [item action];
    id    targ = [item target];

    if(act != nil && targ != nil)
    //        [NSApp sendAction:act to:targ from:item];
    // Temporarily sending the About menu item so it'll fire the About box:
      [NSApp sendAction:act to:targ from:[menu itemAtIndex:0]];

    // Copied from MenuItemView sample project. I don't think this is necessary:
    //    [self setNeedsDisplay:YES];
    }
    On May 29, 2013, at 4:34 PM, Steve Mills wrote:

    > On May 29, 2013, at 18:06:45, Lee Ann Rucker <lrucker...>
    > wrote:
    >
    >> Use performSelector:withObject:afterDelay:0 to push your response to the end of the run loop, giving the UI a chance to update first. It's what I do when a button (etc) is going to trigger something that won't be instantaneous, where the button shouldn't keep showing in the pressed state while it happens.
    >
    > Good idea, but I need to send the 3rd parameter; the selector, the target, and the sender. I can't do that with performSelector:withObject:afterDelay:.
    >
    > --
    > Steve Mills
    > office: 952-818-3871
    > home: 952-401-6255
    > cell: 612-803-6157
  • On 30/05/2013, at 9:37 AM, Steve Mills <smills...> wrote:

    > On May 29, 2013, at 17:59:58, Graham Cox <graham.cox...>
    > wrote:
    >
    >> This is normal. I think the idea is that the user gets a cue that whatever action is being executed "came from" a menu choice, so the menu remains visible while the action is carried out.
    >
    > That's totally untrue. When an item is chosen, the menu goes away, but the menu title in the menubar remains hilited until control is returned to the event loop that spawned the menu. Leaving the menu visible would be very ugly and confusing.

    You're right. Shouldn't comment before coffee.

    --Graham
  • On May 29, 2013, at 18:46:18, Lee Ann Rucker <lrucker...>
    wrote:

    > Don't delay the action/target part, write a wrapper function and delay that:

    Heh, so simple. Thanks!

    --
    Steve Mills
    office: 952-818-3871
    home: 952-401-6255
    cell: 612-803-6157
  • I previously assumed I could simply draw my view a couple times with a delay between, to simulate the quick flash that a menu item does when you choose it. Of course, so many things have to be difficult in the world of Cocoa. I assume drawing can't get flushed to the screen until control is returned to the tracking event loop? What I came up with works, but seems ridiculously inelegant. Is there some better way to give the current event loop time so I can have the drawing and canceling done right in my mouseUp handler?

    -(void) mouseUp:(NSEvent*)event
    {
    UNUSED_VAR(event);

    NSMenuItem*  item = [self enclosingMenuItem];
    NSMenu*  menu = [item menu];

    // Flash the menu item off and back on just like real ones:
    [self performSelector:@selector(flashOff:) withObject:menu afterDelay:0.05 inModes:[NSArray arrayWithObject:NSEventTrackingRunLoopMode]];
    }

    -(void) flashOff:(NSMenu*)menu
    {
    isHilited = NO;
    [self display];
    [self performSelector:@selector(flashOn:) withObject:menu afterDelay:0.05 inModes:[NSArray arrayWithObject:NSEventTrackingRunLoopMode]];
    }

    -(void) flashOn:(NSMenu*)menu
    {
    isHilited = YES;
    [self display];
    [self performSelector:@selector(cancelMenu:) withObject:menu afterDelay:0.05 inModes:[NSArray arrayWithObject:NSEventTrackingRunLoopMode]];
    }

    -(void) cancelMenu:(NSMenu*)menu
    {
    [menu cancelTracking];
    [self performSelector:@selector(delayedSendAction:) withObject:nil afterDelay:0 inModes:[NSArray arrayWithObjects:NSEventTrackingRunLoopMode, NSDefaultRunLoopMode, nil]];
    }

    -(void) delayedSendAction:(id)obj
    {
    UNUSED_VAR(obj);

    // Then send the action to the target:
    NSMenuItem*  item = [self enclosingMenuItem];
    SEL    act = [item action];
    id    targ = [item target];

    if(act != nil && targ != nil)
      [NSApp sendAction:act to:targ from:item];
    }

    --
    Steve Mills
    office: 952-818-3871
    home: 952-401-6255
    cell: 612-803-6157
  • On 30/05/2013, at 11:49 PM, Steve Mills <smills...> wrote:

    > Is there some better way to give the current event loop time so I can have the drawing and canceling done right in my mouseUp handler?

    Have you looked at -nextEventMatchingMask:untilDate:inMode:dequeue:? As in:

    NSEvent *event;
    while ((event = [NSApp nextEventMatchingMask:NSAnyEventMask untilDate:nil inMode:NSDefaultRunLoopMode dequeue:YES]))
    {
      [NSApp sendEvent: event];
      event = nil;
    }

    --
    Shane Stanley <sstanley...>
    'AppleScriptObjC Explored' <www.macosxautomation.com/applescript/apps/>
  • On Thu, May 30, 2013, at 06:49 AM, Steve Mills wrote:
    > I assume drawing can't get flushed to the screen until control is
    > returned to the tracking event loop? What I came up with works, but seems
    > ridiculously inelegant. Is there some better way to give the current
    > event loop time so I can have the drawing and canceling done right in my
    > mouseUp handler?

    I would argue that attempting to flush the screen yourself is the far
    more inelegant approach. Screen flushing and compositing is handled by
    an external process; the system drives repainting, not your app. Yes,
    -performSelector:afterDelay: is slightly smelly, but much less so that
    trying to force the compositing engine to fake drawing menu selection.

    --Kyle Sluder
  • On May 30, 2013, at 18:44:46, Shane Stanley <sstanley...> wrote:

    > Have you looked at -nextEventMatchingMask:untilDate:inMode:dequeue:? As in:
    >
    > NSEvent *event;
    > while ((event = [NSApp nextEventMatchingMask:NSAnyEventMask untilDate:nil inMode:NSDefaultRunLoopMode dequeue:YES]))
    > {
    > [NSApp sendEvent: event];
    > event = nil;
    > }

    Just tried it. Didn't work. I also used NSEventTrackingRunLoopMode, since that's the current run loop when tracking a menu. I wish Apple had more people on these lists that can give definitive answers to our questions. It sure would help to know how Apple does it, or even better, be able to call something like [menuItem flashAndCancelTracking].

    --
    Steve Mills
    office: 952-818-3871
    home: 952-401-6255
    cell: 612-803-6157
  • On Thu, May 30, 2013, at 08:30 PM, Steve Mills wrote:
    > Just tried it. Didn't work. I also used NSEventTrackingRunLoopMode, since
    > that's the current run loop when tracking a menu. I wish Apple had more
    > people on these lists that can give definitive answers to our questions.
    > It sure would help to know how Apple does it, or even better, be able to
    > call something like [menuItem flashAndCancelTracking].

    It's done in a private 64-bit reimplementation of the Carbon Menu
    Manager that spins its own nested event loop (which you can see in a
    backtrace) and then sends the action.

    Seriously. Just use -performSelector:afterDelay:.

    --Kyle Sluder
previous month may 2013 next month
MTWTFSS
    1 2 3 4 5
6 7 8 9 10 11 12
13 14 15 16 17 18 19
20 21 22 23 24 25 26
27 28 29 30 31    
Go to today