mirror of
https://github.com/kovidgoyal/kitty.git
synced 2026-05-13 08:26:56 +00:00
Merge branch 'dictation_fix' of https://github.com/alexeyshurygin/kitty
This commit is contained in:
commit
d244a697bf
3 changed files with 150 additions and 0 deletions
|
|
@ -839,6 +839,24 @@ is_apple_jis_layout_function_key(NSEvent *event) {
|
|||
return [event keyCode] == 0x66 /* kVK_JIS_Eisu */ || [event keyCode] == 0x68 /* kVK_JIS_Kana */;
|
||||
}
|
||||
|
||||
static bool
|
||||
has_apple_fn_global_shortcut(void) {
|
||||
NSDictionary *hitoolbox_settings = [[NSUserDefaults standardUserDefaults] persistentDomainForName:@"com.apple.HIToolbox"];
|
||||
id obj = [hitoolbox_settings objectForKey:@"AppleFnUsageType"];
|
||||
if (![obj isKindOfClass:[NSNumber class]]) return false;
|
||||
// Non-zero AppleFnUsageType means macOS has reserved Fn/Globe for a
|
||||
// system action such as input source switching, emoji picker, or dictation.
|
||||
return [obj integerValue] != 0;
|
||||
}
|
||||
|
||||
static bool
|
||||
is_apple_fn_global_shortcut(NSEvent *event) {
|
||||
if ([event keyCode] != 0x3f /* kVK_Function */) return false;
|
||||
NSEventModifierFlags mods = USEFUL_MODS([event modifierFlags]);
|
||||
if (mods != 0 && mods != NSEventModifierFlagFunction) return false;
|
||||
return has_apple_fn_global_shortcut();
|
||||
}
|
||||
|
||||
GLFWAPI GLFWapplicationshouldhandlereopenfun glfwSetApplicationShouldHandleReopen(GLFWapplicationshouldhandlereopenfun callback) {
|
||||
GLFWapplicationshouldhandlereopenfun previous = handle_reopen_callback;
|
||||
handle_reopen_callback = callback;
|
||||
|
|
@ -949,6 +967,10 @@ int _glfwPlatformInit(bool *supports_window_occlusion)
|
|||
debug_key("-------------- flags changed -----------------\n");
|
||||
debug_key("%s\n", [[event description] UTF8String]);
|
||||
last_keydown_shortcut_event.virtual_key_code = 0xffff;
|
||||
if (!_glfw.ignoreOSKeyboardProcessing && !_glfw.keyboard_grabbed && is_apple_fn_global_shortcut(event)) {
|
||||
debug_key("flagsChanged triggered global fn shortcut ignoring\n");
|
||||
return event;
|
||||
}
|
||||
// switching to the next input source is only confirmed when all modifier keys are released
|
||||
if (last_keydown_shortcut_event.input_source_switch_modifiers) {
|
||||
if (!([event modifierFlags] & last_keydown_shortcut_event.input_source_switch_modifiers))
|
||||
|
|
|
|||
|
|
@ -99,6 +99,22 @@ polymorphic_string_as_utf8(id string) {
|
|||
return [characters UTF8String];
|
||||
}
|
||||
|
||||
static bool
|
||||
forward_dictation_selector_to_app(SEL selector, id sender) {
|
||||
static SEL start_dictation_selector = NULL, stop_dictation_selector = NULL;
|
||||
if (start_dictation_selector == NULL) {
|
||||
start_dictation_selector = NSSelectorFromString(@"startDictation:");
|
||||
stop_dictation_selector = NSSelectorFromString(@"stopDictation:");
|
||||
}
|
||||
if (selector != start_dictation_selector && selector != stop_dictation_selector) return false;
|
||||
if ([NSApp respondsToSelector:selector]) {
|
||||
debug_key("Forwarding %s to NSApp\n", [NSStringFromSelector(selector) UTF8String]);
|
||||
[NSApp performSelector:selector withObject:sender];
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static uint32_t
|
||||
vk_code_to_functional_key_code(uint8_t key_code) { // {{{
|
||||
switch(key_code) {
|
||||
|
|
@ -842,6 +858,7 @@ static void update_titlebar_button_visibility_after_fullscreen_transition(_GLFWw
|
|||
// With the default macOS keybindings, pressing certain key combinations
|
||||
// (e.g. Ctrl+/, Ctrl+Cmd+Down/Left/Right) will produce a beep sound.
|
||||
debug_key("\n\tTextInputCtx: doCommandBySelector: (%s)\n", [NSStringFromSelector(selector) UTF8String]);
|
||||
if (forward_dictation_selector_to_app(selector, nil)) return;
|
||||
}
|
||||
@end // }}}
|
||||
|
||||
|
|
@ -1930,6 +1947,17 @@ void _glfwPlatformUpdateIMEState(_GLFWwindow *w, const GLFWIMEUpdateEvent *ev) {
|
|||
- (void)doCommandBySelector:(SEL)selector
|
||||
{
|
||||
debug_key("\n\tdoCommandBySelector: (%s)\n", [NSStringFromSelector(selector) UTF8String]);
|
||||
if (forward_dictation_selector_to_app(selector, self)) return;
|
||||
}
|
||||
|
||||
- (void)startDictation:(id)sender
|
||||
{
|
||||
forward_dictation_selector_to_app(_cmd, sender);
|
||||
}
|
||||
|
||||
- (void)stopDictation:(id)sender
|
||||
{
|
||||
forward_dictation_selector_to_app(_cmd, sender);
|
||||
}
|
||||
|
||||
- (BOOL)isAccessibilityElement
|
||||
|
|
|
|||
|
|
@ -2,10 +2,13 @@
|
|||
# License: GPLv3 Copyright: 2021, Kovid Goyal <kovid at kovidgoyal.net>
|
||||
|
||||
|
||||
import json
|
||||
import os
|
||||
import stat
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import textwrap
|
||||
import unittest
|
||||
from functools import partial
|
||||
|
||||
|
|
@ -35,6 +38,103 @@ class TestBuild(BaseTest):
|
|||
for name in 'cell border bgimage tint graphics'.split():
|
||||
Program(name)
|
||||
|
||||
def test_macos_dictation_forwarding(self) -> None:
|
||||
from kitty.constants import glfw_path, is_macos
|
||||
if not is_macos:
|
||||
self.skipTest('Dictation smoke test is macOS only')
|
||||
cocoa_module = glfw_path('cocoa')
|
||||
probe = textwrap.dedent('''\
|
||||
#import <AppKit/AppKit.h>
|
||||
#import <dlfcn.h>
|
||||
#import <objc/runtime.h>
|
||||
#import <objc/message.h>
|
||||
|
||||
static int start_calls = 0;
|
||||
static int stop_calls = 0;
|
||||
static id last_sender = nil;
|
||||
|
||||
static void fake_start_dictation(id self, SEL _cmd, id sender) {
|
||||
(void)self; (void)_cmd;
|
||||
start_calls++;
|
||||
last_sender = sender;
|
||||
}
|
||||
|
||||
static void fake_stop_dictation(id self, SEL _cmd, id sender) {
|
||||
(void)self; (void)_cmd;
|
||||
stop_calls++;
|
||||
last_sender = sender;
|
||||
}
|
||||
|
||||
static void require_true(BOOL condition, const char *message) {
|
||||
if (!condition) {
|
||||
fprintf(stderr, "FAIL: %s\\n", message);
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
@autoreleasepool {
|
||||
[NSApplication sharedApplication];
|
||||
void *handle = dlopen(@@COCOA_MODULE@@, RTLD_NOW | RTLD_GLOBAL);
|
||||
require_true(handle != NULL, dlerror());
|
||||
|
||||
SEL start = NSSelectorFromString(@"startDictation:");
|
||||
SEL stop = NSSelectorFromString(@"stopDictation:");
|
||||
Method start_method = class_getInstanceMethod([NSApplication class], start);
|
||||
Method stop_method = class_getInstanceMethod([NSApplication class], stop);
|
||||
require_true(start_method != NULL, "NSApplication startDictation: missing");
|
||||
require_true(stop_method != NULL, "NSApplication stopDictation: missing");
|
||||
method_setImplementation(start_method, (IMP)fake_start_dictation);
|
||||
method_setImplementation(stop_method, (IMP)fake_stop_dictation);
|
||||
|
||||
Class view_cls = NSClassFromString(@"GLFWContentView");
|
||||
Class context_cls = NSClassFromString(@"GLFWTextInputContext");
|
||||
require_true(view_cls != Nil, "GLFWContentView class not loaded");
|
||||
require_true(context_cls != Nil, "GLFWTextInputContext class not loaded");
|
||||
|
||||
SEL init_with_glfw_window = NSSelectorFromString(@"initWithGlfwWindow:");
|
||||
id view = ((id (*)(id, SEL, void *)) objc_msgSend)([view_cls alloc], init_with_glfw_window, NULL);
|
||||
require_true(view != nil, "GLFWContentView initWithGlfwWindow: failed");
|
||||
require_true([view respondsToSelector:start], "GLFWContentView does not expose startDictation:");
|
||||
require_true([view respondsToSelector:stop], "GLFWContentView does not expose stopDictation:");
|
||||
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
|
||||
[view performSelector:start withObject:@"menu sender"];
|
||||
#pragma clang diagnostic pop
|
||||
require_true(start_calls == 1, "startDictation: action was not forwarded to NSApplication");
|
||||
require_true([(id)last_sender isEqual:@"menu sender"], "startDictation: forwarded wrong sender");
|
||||
|
||||
[view doCommandBySelector:start];
|
||||
require_true(start_calls == 2, "doCommandBySelector:startDictation: was swallowed");
|
||||
require_true(last_sender == view, "doCommandBySelector:startDictation: should forward self as sender");
|
||||
|
||||
id context = [view inputContext];
|
||||
require_true(context != nil, "GLFWContentView inputContext missing");
|
||||
require_true([context isKindOfClass:context_cls], "GLFWContentView inputContext has wrong class");
|
||||
[context doCommandBySelector:stop];
|
||||
require_true(stop_calls == 1, "GLFWTextInputContext did not forward stopDictation:");
|
||||
require_true(last_sender == nil, "GLFWTextInputContext should forward nil sender");
|
||||
|
||||
printf("dictation forwarding probe passed\\n");
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
''').replace('@@COCOA_MODULE@@', json.dumps(cocoa_module))
|
||||
with tempfile.TemporaryDirectory() as tdir:
|
||||
src = os.path.join(tdir, 'dictation_probe.m')
|
||||
exe = os.path.join(tdir, 'dictation_probe')
|
||||
with open(src, 'w') as f:
|
||||
f.write(probe)
|
||||
cp = subprocess.run(
|
||||
['clang', '-framework', 'AppKit', src, '-o', exe],
|
||||
stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True
|
||||
)
|
||||
self.assertEqual(cp.returncode, 0, cp.stdout)
|
||||
cp = subprocess.run([exe], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)
|
||||
self.assertEqual(cp.returncode, 0, cp.stdout)
|
||||
self.assertIn('dictation forwarding probe passed', cp.stdout)
|
||||
|
||||
def test_glfw_modules(self) -> None:
|
||||
from kitty.constants import glfw_path, is_macos
|
||||
linux_backends = ['x11']
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue