diff --git a/app/os_macos.go b/app/os_macos.go index d62bb645..7aa086cf 100644 --- a/app/os_macos.go +++ b/app/os_macos.go @@ -39,6 +39,12 @@ import ( #define MOUSE_DOWN 3 #define MOUSE_SCROLL 4 +#define KEY_DELETE_BACKWARD 0x7f +#define KEY_RETURN 0xd +#define KEY_ENTER 0x3 +#define KEY_ESCAPE 0x1b +#define KEY_TAB 0x09 + __attribute__ ((visibility ("hidden"))) void gio_main(void); __attribute__ ((visibility ("hidden"))) CFTypeRef gio_createView(int presentWithTrans); __attribute__ ((visibility ("hidden"))) CFTypeRef gio_createWindow(CFTypeRef viewRef, CGFloat width, CGFloat height, CGFloat minWidth, CGFloat minHeight, CGFloat maxWidth, CGFloat maxHeight); @@ -343,6 +349,8 @@ type window struct { scale float32 config Config + + cmdKeysDown map[key.Name]struct{} } // launched is closed when applicationDidFinishLaunching is called. @@ -551,7 +559,16 @@ func gio_onKeys(h C.uintptr_t, cstr C.CFTypeRef, ti C.double, mods C.NSUInteger, } w := windowFor(h) for _, k := range str { - if n, ok := convertKey(k); ok { + n, ok := convertKey(k) + if !ok && ks == key.Release { + // Key release events are not reported by onCommandBySelector. + n, ok = convertCommandKey(k) + if ok { + _, ok = w.cmdKeysDown[n] + delete(w.cmdKeysDown, n) + } + } + if ok { w.ProcessEvent(key.Event{ Name: n, Modifiers: kmods, @@ -561,6 +578,29 @@ func gio_onKeys(h C.uintptr_t, cstr C.CFTypeRef, ti C.double, mods C.NSUInteger, } } +//export gio_onCommandBySelector +func gio_onCommandBySelector(h C.uintptr_t, k C.int, ti C.double, mods C.NSUInteger) { + w := windowFor(h) + w.commandKey(rune(k), ti, mods) +} + +func (w *window) commandKey(k rune, ti C.double, mods C.NSUInteger) bool { + n, ok := convertCommandKey(k) + if !ok { + return false + } + if w.cmdKeysDown == nil { + w.cmdKeysDown = make(map[key.Name]struct{}) + } + w.cmdKeysDown[n] = struct{}{} + w.ProcessEvent(key.Event{ + Modifiers: convertMods(mods), + Name: n, + State: key.Press, + }) + return true +} + //export gio_onText func gio_onText(h C.uintptr_t, cstr C.CFTypeRef) { str := nsstringToString(cstr) @@ -749,8 +789,19 @@ func gio_substringForProposedRange(h C.uintptr_t, crng C.NSRange, actual C.NSRan } //export gio_insertText -func gio_insertText(h C.uintptr_t, cstr C.CFTypeRef, crng C.NSRange) { +func gio_insertText(h C.uintptr_t, cstr C.CFTypeRef, crng C.NSRange, ti C.double, mods C.NSUInteger) { w := windowFor(h) + str := nsstringToString(cstr) + // macOS IME in some cases calls insertText for command keys such as backspace + // instead of doCommandBySelector. + hadCommands := false + for _, r := range str { + hadCommands = w.commandKey(r, ti, mods) || hadCommands + } + if hadCommands { + w.w.SetComposingRegion(key.Range{Start: -1, End: -1}) + return + } state := w.w.EditorState() rng := state.compose if rng.Start == -1 { @@ -762,7 +813,6 @@ func gio_insertText(h C.uintptr_t, cstr C.CFTypeRef, crng C.NSRange) { End: state.RunesIndex(int(crng.location + crng.length)), } } - str := nsstringToString(cstr) w.w.EditorReplace(rng, str) w.w.SetComposingRegion(key.Range{Start: -1, End: -1}) start := rng.Start @@ -957,10 +1007,10 @@ func osMain() { C.gio_main() } -func convertKey(k rune) (key.Name, bool) { +func convertCommandKey(k rune) (key.Name, bool) { var n key.Name switch k { - case 0x1b: + case C.KEY_ESCAPE: n = key.NameEscape case C.NSLeftArrowFunctionKey: n = key.NameLeftArrow @@ -970,22 +1020,33 @@ func convertKey(k rune) (key.Name, bool) { n = key.NameUpArrow case C.NSDownArrowFunctionKey: n = key.NameDownArrow - case 0xd: + case C.KEY_RETURN: n = key.NameReturn - case 0x3: + case C.KEY_ENTER: n = key.NameEnter case C.NSHomeFunctionKey: n = key.NameHome case C.NSEndFunctionKey: n = key.NameEnd - case 0x7f: + case C.KEY_DELETE_BACKWARD, 0x08: n = key.NameDeleteBackward case C.NSDeleteFunctionKey: n = key.NameDeleteForward + case C.KEY_TAB, 0x19: + n = key.NameTab case C.NSPageUpFunctionKey: n = key.NamePageUp case C.NSPageDownFunctionKey: n = key.NamePageDown + default: + return "", false + } + return n, true +} + +func convertKey(k rune) (key.Name, bool) { + var n key.Name + switch k { case C.NSF1FunctionKey: n = key.NameF1 case C.NSF2FunctionKey: @@ -1010,8 +1071,6 @@ func convertKey(k rune) (key.Name, bool) { n = key.NameF11 case C.NSF12FunctionKey: n = key.NameF12 - case 0x09, 0x19: - n = key.NameTab case 0x20: n = key.NameSpace default: diff --git a/app/os_macos.m b/app/os_macos.m index 9c895148..9a3858b7 100644 --- a/app/os_macos.m +++ b/app/os_macos.m @@ -15,6 +15,7 @@ __attribute__ ((visibility ("hidden"))) CALayer *gio_layerFactory(BOOL presentWi @end @interface GioView : NSView +@property NSEvent *lastKeyDown; @property uintptr_t handle; @property BOOL presentWithTrans; @end @@ -132,7 +133,10 @@ static void handleMouse(GioView *view, NSEvent *event, int typ, CGFloat dx, CGFl handleMouse(self, event, MOUSE_SCROLL, dx, dy); } - (void)keyDown:(NSEvent *)event { + // Stash the event for use by doCommandBySelector. + self.lastKeyDown = event; [self interpretKeyEvents:[NSArray arrayWithObject:event]]; + self.lastKeyDown = nil; NSString *keys = [event charactersIgnoringModifiers]; gio_onKeys(self.handle, (__bridge CFTypeRef)keys, [event timestamp], [event modifierFlags], true); } @@ -143,9 +147,47 @@ static void handleMouse(GioView *view, NSEvent *event, int typ, CGFloat dx, CGFl - (void)insertText:(id)string { gio_onText(self.handle, (__bridge CFTypeRef)string); } -- (void)doCommandBySelector:(SEL)sel { - // Don't pass commands up the responder chain. - // They will end up in a beep. +- (void)doCommandBySelector:(SEL)action { + NSEvent *event = self.lastKeyDown; + if (event == nil) { + return; + } + int key = 0; + if (action == @selector(deleteBackward:)) { + key = KEY_DELETE_BACKWARD; + } else if (action == @selector(deleteForward:)) { + key = NSDeleteFunctionKey; + } else if (action == @selector(insertNewline:)) { + NSString *keys = [event charactersIgnoringModifiers]; + if ([keys isEqualToString:@"\r"]) { + key = KEY_RETURN; + } else { + key = KEY_ENTER; + } + } else if (action == @selector(moveUp:)) { + key = NSUpArrowFunctionKey; + } else if (action == @selector(moveDown:)) { + key = NSDownArrowFunctionKey; + } else if (action == @selector(moveLeft:)) { + key = NSLeftArrowFunctionKey; + } else if (action == @selector(moveRight:)) { + key = NSRightArrowFunctionKey; + } else if (action == @selector(cancelOperation:)) { + key = KEY_ESCAPE; + } else if (action == @selector(insertTab:)) { + key = KEY_TAB; + } else if (action == @selector(scrollToBeginningOfDocument:)) { + key = NSHomeFunctionKey; + } else if (action == @selector(scrollToEndOfDocument:)) { + key = NSEndFunctionKey; + } else if (action == @selector(scrollPageUp:)) { + key = NSPageUpFunctionKey; + } else if (action == @selector(scrollPageDown:)) { + key = NSPageDownFunctionKey; + } else { + return; + } + gio_onCommandBySelector(self.handle, key, [event timestamp], [event modifierFlags]); } - (BOOL)hasMarkedText { @@ -183,6 +225,10 @@ static void handleMouse(GioView *view, NSEvent *event, int typ, CGFloat dx, CGFl } - (void)insertText:(id)string replacementRange:(NSRange)replaceRange { + NSEvent *event = self.lastKeyDown; + if (event == nil) { + return; + } NSString *str; // string is either an NSAttributedString or an NSString. if ([string isKindOfClass:[NSAttributedString class]]) { @@ -190,7 +236,7 @@ static void handleMouse(GioView *view, NSEvent *event, int typ, CGFloat dx, CGFl } else { str = string; } - gio_insertText(self.handle, (__bridge CFTypeRef)str, replaceRange); + gio_insertText(self.handle, (__bridge CFTypeRef)str, replaceRange, [event timestamp], [event modifierFlags]); } - (NSUInteger)characterIndexForPoint:(NSPoint)p { return gio_characterIndexForPoint(self.handle, p);