Apple如何在打开时更新机场菜单? (如何在NSMenu已经打开时更改)

时间:2010-05-11 04:08:52

标签: objective-c cocoa statusbar nsmenu nsmenuitem

我有一个弹出打开NSMenu的状态栏项,我有一个委托集并且它已正确连接(-(void)menuNeedsUpdate:(NSMenu *)menu正常工作)。也就是说,该方法设置为在显示菜单之前调用,我需要监听并触发异步请求,稍后在菜单打开时更新菜单,我无法弄清楚应该如何完成

谢谢:)

修改

好的,我现在在这里:

当您单击菜单项(在状态栏中)时,会调用一个运行NSTask的选择器。我使用通知中心来监听该任务何时完成,并写下:

[[NSRunLoop currentRunLoop] performSelector:@selector(updateTheMenu:) target:self argument:statusBarMenu order:0 modes:[NSArray arrayWithObject:NSEventTrackingRunLoopMode]];

并且:

- (void)updateTheMenu:(NSMenu*)menu {
    NSMenuItem *mitm = [[NSMenuItem alloc] init];
    [mitm setEnabled:NO];
    [mitm setTitle:@"Bananas"];
    [mitm setIndentationLevel:2];
    [menu insertItem:mitm atIndex:2];
    [mitm release];
}

这个方法肯定被调用,因为如果我单击菜单并立即返回到它,我会得到一个包含此信息的更新菜单。问题是它没有更新 - 当菜单打开时 - 。

3 个答案:

答案 0 :(得分:16)

菜单鼠标跟踪以特殊的运行循环模式(NSEventTrackingRunLoopMode)完成。要修改菜单,您需要发送一条消息,以便在事件跟踪模式下处理它。最简单的方法是使用NSRunLoop的这种方法:

[[NSRunLoop currentRunLoop] performSelector:@selector(updateTheMenu:) target:self argument:yourMenu order:0 modes:[NSArray arrayWithObject:NSEventTrackingRunLoopMode]]

您还可以将模式指定为NSRunLoopCommonModes,并且在任何常见的运行循环模式期间都会发送消息,包括NSEventTrackingRunLoopMode

您的更新方法将执行以下操作:

- (void)updateTheMenu:(NSMenu*)menu
{
    [menu addItemWithTitle:@"Foobar" action:NULL keyEquivalent:@""];
    [menu update];
}

答案 1 :(得分:14)

(如果你想改变菜单的布局,类似于机场菜单显示更多信息,当你选择点击它,然后继续阅读。如果你想做一些完全不同的事情,那么这个答案可能不是根据你的意愿相关。)

关键是-[NSMenuItem setAlternate:]。举个例子,假设我们要构建一个NSMenu,其中包含Do something...个动作。你可以将其编码为:

NSMenu * m = [[NSMenu alloc] init];

NSMenuItem * doSomethingPrompt = [m addItemWithTitle:@"Do something..." action:@selector(doSomethingPrompt:) keyEquivalent:@"d"];
[doSomethingPrompt setTarget:self];
[doSomethingPrompt setKeyEquivalentModifierMask:NSShiftKeyMask];

NSMenuItem * doSomething = [m addItemWithTitle:@"Do something" action:@selector(doSomething:) keyEquivalent:@"d"];
[doSomething setTarget:self];
[doSomething setKeyEquivalentModifierMask:(NSShiftKeyMask | NSAlternateKeyMask)];
[doSomething setAlternate:YES];

//do something with m

现在,您认为这会创建一个包含两个项目的菜单:“做点什么......”和“做点什么”,你就部分正确了。因为我们将第二个菜单项设置为备用项,并且因为两个菜单项具有相同的等效键(但不同的修饰符掩码),所以只有第一个菜单项(即默认为setAlternate:NO的那个)将节目。然后,当您打开菜单时,如果按下代表第二个菜单的修改器掩码(即选项键),则菜单项将实时从第一个菜单项转换为第二个菜单项。

例如,这就是Apple菜单的工作原理。如果单击一次,您将看到一些带有省略号的选项,例如“重新启动...”和“关闭...”。 HIG指定如果存在省略号,则表示系统将在执行操作之前提示用户进行确认。但是,如果按下选项键(菜单仍然打开),您会注意到它们会变为“重新启动”和“关机”。省略号消失,这意味着如果您在按下选项键时选择它们,它们将立即执行而不会提示用户进行确认。

相同的一般功能适用于状态项中的菜单。您可以将扩展信息作为常规信息的“备用”项目,仅显示按下选项键。一旦你理解了基本原理,它实际上很容易实现,没有太多的诡计。

答案 2 :(得分:13)

这里的问题是,即使在菜单跟踪模式下,您也需要触发回调。

例如, - [NSTask waitUntilExit]“使用NSDefaultRunLoopMode轮询当前运行循环,直到任务完成”。这意味着它将在菜单关闭后才会运行。此时,调度updateTheMenu在NSCommonRunLoopMode上运行没有帮助 - 毕竟它无法及时返回。我相信NSNotificationCenter观察员也只在NSDefaultRunLoopMode中触发。

如果你能找到某种方法来安排即使在菜单跟踪模式下运行的回调,你也可以设置;您可以直接从该回调调用updateTheMenu。

- (void)updateTheMenu {
  static BOOL flip = NO;
  NSMenu *filemenu = [[[NSApp mainMenu] itemAtIndex:1] submenu];
  if (flip) {
    [filemenu removeItemAtIndex:[filemenu numberOfItems] - 1];
  } else {
    [filemenu addItemWithTitle:@"Now you see me" action:nil keyEquivalent:@""];
  }
  flip = !flip;
}

- (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
  NSTimer *timer = [NSTimer timerWithTimeInterval:0.5
                                           target:self
                                         selector:@selector(updateTheMenu)
                                         userInfo:nil
                                          repeats:YES];
  [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
}

运行此按钮并按住文件菜单,您将看到额外的菜单项出现并每半秒消失。显然“每半秒钟”不是你想要的,而且NSTimer不理解“当我的后台任务完成时”。但是你可以使用一些同样简单的机制。

如果没有,你可以自己从一个NSPort子类中构建它 - 例如,创建一个NSMessagePort并在完成后让你的NSTask写入。

你真正需要明确安排updateTheMenu的唯一情况就像Rob Keniger上面描述的那样,如果你试图从运行循环之外调用它。例如,您可以生成一个触发子进程并调用waitpid的线程(在进程完成之前阻塞),然后该线程必须调用performSelector:target:argument:order:modes:而不是直接调用updateTheMenu。