macOS App:处理绑定到全局键盘快捷键的组合键
macOS App: handling key combinations bound to global keyboard shortcuts
在某些应用中,应用直接处理键盘快捷键是有意义的,否则这些快捷键将绑定到系统范围的组合。例如,⌘-Space(通常为 Spotlight)或 ⌘-Tab(通常为应用程序切换器)。这适用于各种 Mac 应用程序,例如 VMWare Fusion、Apple 自己的屏幕共享和远程桌面客户端(分别将事件转发到 VM 或服务器,而不是在本地处理),以及一些类似的第三方App Store 中的应用程序。
我们想在我们正在开发的应用程序中实现这种模式,但很难弄清楚如何实现。我应该指出,有问题的应用程序是一个常规的前台应用程序,是沙盒的,任何解决方案都必须符合 App Store 规则。商店中的其他应用程序可以做到这一点意味着这一定是可能的。
明确地说,我们想要:
- 检测并处理所有按键,包括绑定到全局快捷方式的按键。
- 防止全局快捷方式触发其全局绑定效果。
Apple 的 Event Architecture document suggests that the foreground application should already be receiving these events. (It only talks about earlier levels handling things such as the power and eject buttons, which is fine.) It goes on to suggest, and the key events document also implies NSApplication
的 sendEvent:
方法是根据修饰符标志检测潜在的快捷方式,将它们分派到 windows,如果失败,到菜单栏。它没有明确说明全局绑定的快捷方式会发生什么。
我尝试继承 NSApplication
并覆盖 sendEvent:
。无论我是否将所有事件传递给超类实现,或者如果我说过滤修饰键事件,当我按下 ⌘-Space 时,我都会收到按下和释放命令 (⌘) 键的事件,但是不是空格键。 Spotlight UI 始终弹出。
我没有从 Apple 或其他地方找到很多关于子类化 NSApplication 及其早期事件处理的信息。我似乎无法找出在哪个级别检测和处理全局快捷方式。
有人能给我指出正确的方向吗?
可能无效的解决方案:
我在其他 Stack Overflow 帖子中看到但不适用于我看到的其他应用程序的建议(并且会违反 App Store 规则):
- 可访问性 API(需要特殊许可)
- 事件taps/hooks(需要运行作为root)
无论如何,这两个都太过分了,因为它们让你在 all 次拦截 all 事件,而不仅仅是在你的应用程序运行时前台应用程序。
NSevent
的 addGlobalMonitorForEventsMatchingMask:handler:
同时不会阻止全局快捷方式处理程序为这些事件触发,所以我什至懒得尝试。
好的,所以 Cocoa 事件方法和 Quartz 事件抽头已经过时了,因为它们要么需要 root 或可访问性访问权限,要么不能在 dock 之前捕获事件。
Carbon 的 PushSymbolicHotKeyMode
已经过时了,因为根据文档,它需要访问权限。
Carbon 的 RegisterEventHotKey
可能已经过时了,因为 Apple 似乎不允许这样做(请参阅我对问题的评论 link)。但是,即便如此,我测试过,你不能用它来捕捉Command+Tab。
我快速验证了这个可以如何工作,但是 YMMV:
- 从这个 answer 中实现
KeyboardWatcher
示例 class。您将需要 link IOKit。
- 添加硬件 - USB (com.apple.security.device.usb) 沙盒授权。这是必要的,因为 KeyboardWatcher 使用 HID 来捕捉按键
-
Handle_DeviceEventCallback
会给你按下的键。您显然可以根据自己的需要进行修改
- 使用
SetSystemUIMode
来阻止任务切换器和聚光灯。您将需要 link Carbon。
SetSystemUIMode(kUIModeContentSuppressed, kUIOptionDisableProcessSwitch);
请注意,这仅在您的应用程序位于前台(可能是您想要的)时有效。我使用跟踪矩形在我的视图上设置了它,因此只有当鼠标悬停在我的视图上时它才会生效(就像在 Remotix 中一样):
- (void)viewDidLoad {
[super viewDidLoad];
NSTrackingArea* trackingArea = [[NSTrackingArea alloc] initWithRect:[self.view bounds] options: (NSTrackingMouseEnteredAndExited |
NSTrackingActiveAlways) owner:self userInfo:nil];
[self.view addTrackingArea:trackingArea];
}
- (void) mouseEntered:(NSEvent*)theEvent {
SetSystemUIMode(kUIModeContentSuppressed, kUIOptionDisableProcessSwitch);
}
- (void) mouseExited:(NSEvent*)theEvent {
SetSystemUIMode(kUIModeNormal, 0);
}
Remotix 似乎 link Carbon 和 IOKit,但我看不到它们是否有 USB 授权(我试过演示,不是 App Store 版本)。他们可能正在做这样的事情。
实现此目的的正常方法是安装 Quartz 事件分流器。但是,要接收针对其他应用程序的事件,您需要(如您所说)是 root 用户,或者为您的应用程序启用辅助访问权限。
目前的沙盒规则似乎无法使用事件点击。 developer forum 中证实了这一点。 link 仅用于登录,但引用线程:
Is there are any chance to handle events that comming from media keys by prevents launch iTunes. Before sandbox it was possible by create CGEventTap but now sandbox deny using hid-controll.
No, this is not currently possible within App Sandbox.
我不确定还有其他方法可以做到这一点;我很想知道 App Store 中的哪些应用程序可以?
VMWare Fusion 显然没有沙盒化,Apple 自己的应用程序不受规则约束。请记住,沙盒仅适用于 2012 年推出后添加的新应用程序。在此日期之前添加的应用程序不会强制执行沙盒。看到这个 .
我很久以前就解决了这个问题,但我只是注意到我从来没有把它贴在这里。答案最终涉及 CGSSetGlobalHotKeyOperatingMode()
。这不是 public API,但有许多 Mac App Store 应用程序通过混淆函数名称并动态查找来使用它。苹果似乎并不介意。 API 使用起来非常简单,并且有大量公开的示例源代码。
对于正在寻找 full-screen 应用程序解决方案的其他人,或者如果您愿意接管全屏,您可以使用:CGDisplayCapture
它会导致您的应用程序捕获所有键盘输入,甚至阻止使用键盘调用 Spotlight 和应用程序切换。
import Quartz
// disable keyboard events for all apps, except yours
CGDisplayCapture(CGMainDisplayID())
// reenable keyboard events for other apps
CGDisplayRelease(CGMainDisplayID())
注意:在显示被释放之前,应用程序不会收到 window/application active/resign 事件。因此,也许您可以在应用处于活动状态时使用鼠标跟踪来释放显示。此外,甚至 screensaver/lock 屏幕也会受到影响。确保根据需要停用捕获。
参考文献:
- https://lists.apple.com/archives/cocoa-dev/2012/Sep/msg00476.html
- Monitor screensaver events
- Display Capture
在某些应用中,应用直接处理键盘快捷键是有意义的,否则这些快捷键将绑定到系统范围的组合。例如,⌘-Space(通常为 Spotlight)或 ⌘-Tab(通常为应用程序切换器)。这适用于各种 Mac 应用程序,例如 VMWare Fusion、Apple 自己的屏幕共享和远程桌面客户端(分别将事件转发到 VM 或服务器,而不是在本地处理),以及一些类似的第三方App Store 中的应用程序。
我们想在我们正在开发的应用程序中实现这种模式,但很难弄清楚如何实现。我应该指出,有问题的应用程序是一个常规的前台应用程序,是沙盒的,任何解决方案都必须符合 App Store 规则。商店中的其他应用程序可以做到这一点意味着这一定是可能的。
明确地说,我们想要:
- 检测并处理所有按键,包括绑定到全局快捷方式的按键。
- 防止全局快捷方式触发其全局绑定效果。
Apple 的 Event Architecture document suggests that the foreground application should already be receiving these events. (It only talks about earlier levels handling things such as the power and eject buttons, which is fine.) It goes on to suggest, and the key events document also implies NSApplication
的 sendEvent:
方法是根据修饰符标志检测潜在的快捷方式,将它们分派到 windows,如果失败,到菜单栏。它没有明确说明全局绑定的快捷方式会发生什么。
我尝试继承 NSApplication
并覆盖 sendEvent:
。无论我是否将所有事件传递给超类实现,或者如果我说过滤修饰键事件,当我按下 ⌘-Space 时,我都会收到按下和释放命令 (⌘) 键的事件,但是不是空格键。 Spotlight UI 始终弹出。
我没有从 Apple 或其他地方找到很多关于子类化 NSApplication 及其早期事件处理的信息。我似乎无法找出在哪个级别检测和处理全局快捷方式。
有人能给我指出正确的方向吗?
可能无效的解决方案:
我在其他 Stack Overflow 帖子中看到但不适用于我看到的其他应用程序的建议(并且会违反 App Store 规则):
- 可访问性 API(需要特殊许可)
- 事件taps/hooks(需要运行作为root)
无论如何,这两个都太过分了,因为它们让你在 all 次拦截 all 事件,而不仅仅是在你的应用程序运行时前台应用程序。
NSevent
的 addGlobalMonitorForEventsMatchingMask:handler:
同时不会阻止全局快捷方式处理程序为这些事件触发,所以我什至懒得尝试。
好的,所以 Cocoa 事件方法和 Quartz 事件抽头已经过时了,因为它们要么需要 root 或可访问性访问权限,要么不能在 dock 之前捕获事件。
Carbon 的 PushSymbolicHotKeyMode
已经过时了,因为根据文档,它需要访问权限。
Carbon 的 RegisterEventHotKey
可能已经过时了,因为 Apple 似乎不允许这样做(请参阅我对问题的评论 link)。但是,即便如此,我测试过,你不能用它来捕捉Command+Tab。
我快速验证了这个可以如何工作,但是 YMMV:
- 从这个 answer 中实现
KeyboardWatcher
示例 class。您将需要 link IOKit。 - 添加硬件 - USB (com.apple.security.device.usb) 沙盒授权。这是必要的,因为 KeyboardWatcher 使用 HID 来捕捉按键
-
Handle_DeviceEventCallback
会给你按下的键。您显然可以根据自己的需要进行修改 - 使用
SetSystemUIMode
来阻止任务切换器和聚光灯。您将需要 link Carbon。
SetSystemUIMode(kUIModeContentSuppressed, kUIOptionDisableProcessSwitch);
请注意,这仅在您的应用程序位于前台(可能是您想要的)时有效。我使用跟踪矩形在我的视图上设置了它,因此只有当鼠标悬停在我的视图上时它才会生效(就像在 Remotix 中一样):
- (void)viewDidLoad {
[super viewDidLoad];
NSTrackingArea* trackingArea = [[NSTrackingArea alloc] initWithRect:[self.view bounds] options: (NSTrackingMouseEnteredAndExited |
NSTrackingActiveAlways) owner:self userInfo:nil];
[self.view addTrackingArea:trackingArea];
}
- (void) mouseEntered:(NSEvent*)theEvent {
SetSystemUIMode(kUIModeContentSuppressed, kUIOptionDisableProcessSwitch);
}
- (void) mouseExited:(NSEvent*)theEvent {
SetSystemUIMode(kUIModeNormal, 0);
}
Remotix 似乎 link Carbon 和 IOKit,但我看不到它们是否有 USB 授权(我试过演示,不是 App Store 版本)。他们可能正在做这样的事情。
实现此目的的正常方法是安装 Quartz 事件分流器。但是,要接收针对其他应用程序的事件,您需要(如您所说)是 root 用户,或者为您的应用程序启用辅助访问权限。
目前的沙盒规则似乎无法使用事件点击。 developer forum 中证实了这一点。 link 仅用于登录,但引用线程:
Is there are any chance to handle events that comming from media keys by prevents launch iTunes. Before sandbox it was possible by create CGEventTap but now sandbox deny using hid-controll.
No, this is not currently possible within App Sandbox.
我不确定还有其他方法可以做到这一点;我很想知道 App Store 中的哪些应用程序可以?
VMWare Fusion 显然没有沙盒化,Apple 自己的应用程序不受规则约束。请记住,沙盒仅适用于 2012 年推出后添加的新应用程序。在此日期之前添加的应用程序不会强制执行沙盒。看到这个
我很久以前就解决了这个问题,但我只是注意到我从来没有把它贴在这里。答案最终涉及 CGSSetGlobalHotKeyOperatingMode()
。这不是 public API,但有许多 Mac App Store 应用程序通过混淆函数名称并动态查找来使用它。苹果似乎并不介意。 API 使用起来非常简单,并且有大量公开的示例源代码。
对于正在寻找 full-screen 应用程序解决方案的其他人,或者如果您愿意接管全屏,您可以使用:CGDisplayCapture
它会导致您的应用程序捕获所有键盘输入,甚至阻止使用键盘调用 Spotlight 和应用程序切换。
import Quartz
// disable keyboard events for all apps, except yours
CGDisplayCapture(CGMainDisplayID())
// reenable keyboard events for other apps
CGDisplayRelease(CGMainDisplayID())
注意:在显示被释放之前,应用程序不会收到 window/application active/resign 事件。因此,也许您可以在应用处于活动状态时使用鼠标跟踪来释放显示。此外,甚至 screensaver/lock 屏幕也会受到影响。确保根据需要停用捕获。
参考文献:
- https://lists.apple.com/archives/cocoa-dev/2012/Sep/msg00476.html
- Monitor screensaver events
- Display Capture