苹果如何在打开Airport菜单时更新它?(当NSMenu已经打开时如何更改)

37

我有一个状态栏项目,可以弹出一个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个回答

18

菜单鼠标跟踪是在特殊的运行循环模式(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];
}

这似乎不起作用。我通过NSTask请求数据,等待通知,收到通知后,我填充一个可供整个类访问的数据对象,并调用您的NSRunLoop行,该行调用updateTheMenu方法。然而,菜单不会实时更新,我必须先单击它,然后重新打开它,才能显示更新的信息。 - Aaron
1
如果您使用NSRunLoopCommonModes而不是NSEventTrackingRunLoopMode,那么它是否能正常工作? - Rob Keniger
当我这样做时,updateTheMenu根本没有被调用,这让我感到非常困惑。 - Aaron

14

(如果你想改变菜单的布局,类似于当你选项点击它时Airport菜单显示更多信息的方式,那么请继续阅读。如果你想做完全不同的事情,则本答案可能与你想要的不太相关。)

关键是使用-[NSMenuItem setAlternate:]。举个例子,假设我们要建立一个包含Do something...操作的NSMenu,则代码可能如下:

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的项)会显示。然后,当您打开菜单时,如果按下表示第二个菜单项(即选项键)的修饰符掩码,则菜单项将实时从第一个菜单项转换为第二个菜单项。

例如,这就是苹果菜单的工作方式。如果您单击它一次,您将看到一些选项后面带有省略号,例如“重新启动…”和“关闭…”。HIG指定,如果有省略号,这意味着系统将在执行操作之前提示用户确认。但是,如果您按下选项键(菜单仍然打开),您会注意到它们变成了“重新启动”和“关闭”。省略号消失了,这意味着如果您在按下选项键的同时选择它们,它们将立即执行而无需提示用户确认。

状态栏中的菜单也适用于相同的一般功能。您可以将扩展信息作为常规信息的“备用”项,这些常规信息仅在按下选项键时显示。一旦您理解了基本原理,实际上很容易实现,而无需进行太多的诡计。


5
非常有用的信息,尽管不是我想要的。不过我肯定会找到它的用处。我真正想了解的是菜单在打开时注入它发现的网络的方式。谢谢。 - Aaron

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。

2
尽管这个问题已经存在很长时间了,但我仍然对此感到好奇,现在看到为什么我的方法不起作用,我感到非常高兴。谢谢! - Aaron

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接