app: [macos] don't relay key events handled by the IME

Widgets such as Editor use certain key events such as the backspace key
to implement text editing. On macOS, such key events are sometimes used
by an input method, and in those cases the key effect would be applied
twice: first by the IME and then the Editor.

Report such key events through the doCommandBySelector callback, which
receives key events not handled by the IME.

References: https://todo.sr.ht/~eliasnaur/gio/616
Signed-off-by: Elias Naur <mail@eliasnaur.com>
This commit is contained in:
Elias Naur
2024-10-25 19:31:44 +02:00
parent d4c5e54375
commit d7a1ec7461
2 changed files with 119 additions and 14 deletions
+69 -10
View File
@@ -39,6 +39,12 @@ import (
#define MOUSE_DOWN 3 #define MOUSE_DOWN 3
#define MOUSE_SCROLL 4 #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"))) void gio_main(void);
__attribute__ ((visibility ("hidden"))) CFTypeRef gio_createView(int presentWithTrans); __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); __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 scale float32
config Config config Config
cmdKeysDown map[key.Name]struct{}
} }
// launched is closed when applicationDidFinishLaunching is called. // 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) w := windowFor(h)
for _, k := range str { 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{ w.ProcessEvent(key.Event{
Name: n, Name: n,
Modifiers: kmods, 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 //export gio_onText
func gio_onText(h C.uintptr_t, cstr C.CFTypeRef) { func gio_onText(h C.uintptr_t, cstr C.CFTypeRef) {
str := nsstringToString(cstr) str := nsstringToString(cstr)
@@ -749,8 +789,19 @@ func gio_substringForProposedRange(h C.uintptr_t, crng C.NSRange, actual C.NSRan
} }
//export gio_insertText //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) 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() state := w.w.EditorState()
rng := state.compose rng := state.compose
if rng.Start == -1 { 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)), End: state.RunesIndex(int(crng.location + crng.length)),
} }
} }
str := nsstringToString(cstr)
w.w.EditorReplace(rng, str) w.w.EditorReplace(rng, str)
w.w.SetComposingRegion(key.Range{Start: -1, End: -1}) w.w.SetComposingRegion(key.Range{Start: -1, End: -1})
start := rng.Start start := rng.Start
@@ -957,10 +1007,10 @@ func osMain() {
C.gio_main() C.gio_main()
} }
func convertKey(k rune) (key.Name, bool) { func convertCommandKey(k rune) (key.Name, bool) {
var n key.Name var n key.Name
switch k { switch k {
case 0x1b: case C.KEY_ESCAPE:
n = key.NameEscape n = key.NameEscape
case C.NSLeftArrowFunctionKey: case C.NSLeftArrowFunctionKey:
n = key.NameLeftArrow n = key.NameLeftArrow
@@ -970,22 +1020,33 @@ func convertKey(k rune) (key.Name, bool) {
n = key.NameUpArrow n = key.NameUpArrow
case C.NSDownArrowFunctionKey: case C.NSDownArrowFunctionKey:
n = key.NameDownArrow n = key.NameDownArrow
case 0xd: case C.KEY_RETURN:
n = key.NameReturn n = key.NameReturn
case 0x3: case C.KEY_ENTER:
n = key.NameEnter n = key.NameEnter
case C.NSHomeFunctionKey: case C.NSHomeFunctionKey:
n = key.NameHome n = key.NameHome
case C.NSEndFunctionKey: case C.NSEndFunctionKey:
n = key.NameEnd n = key.NameEnd
case 0x7f: case C.KEY_DELETE_BACKWARD, 0x08:
n = key.NameDeleteBackward n = key.NameDeleteBackward
case C.NSDeleteFunctionKey: case C.NSDeleteFunctionKey:
n = key.NameDeleteForward n = key.NameDeleteForward
case C.KEY_TAB, 0x19:
n = key.NameTab
case C.NSPageUpFunctionKey: case C.NSPageUpFunctionKey:
n = key.NamePageUp n = key.NamePageUp
case C.NSPageDownFunctionKey: case C.NSPageDownFunctionKey:
n = key.NamePageDown 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: case C.NSF1FunctionKey:
n = key.NameF1 n = key.NameF1
case C.NSF2FunctionKey: case C.NSF2FunctionKey:
@@ -1010,8 +1071,6 @@ func convertKey(k rune) (key.Name, bool) {
n = key.NameF11 n = key.NameF11
case C.NSF12FunctionKey: case C.NSF12FunctionKey:
n = key.NameF12 n = key.NameF12
case 0x09, 0x19:
n = key.NameTab
case 0x20: case 0x20:
n = key.NameSpace n = key.NameSpace
default: default:
+50 -4
View File
@@ -15,6 +15,7 @@ __attribute__ ((visibility ("hidden"))) CALayer *gio_layerFactory(BOOL presentWi
@end @end
@interface GioView : NSView <CALayerDelegate,NSTextInputClient> @interface GioView : NSView <CALayerDelegate,NSTextInputClient>
@property NSEvent *lastKeyDown;
@property uintptr_t handle; @property uintptr_t handle;
@property BOOL presentWithTrans; @property BOOL presentWithTrans;
@end @end
@@ -132,7 +133,10 @@ static void handleMouse(GioView *view, NSEvent *event, int typ, CGFloat dx, CGFl
handleMouse(self, event, MOUSE_SCROLL, dx, dy); handleMouse(self, event, MOUSE_SCROLL, dx, dy);
} }
- (void)keyDown:(NSEvent *)event { - (void)keyDown:(NSEvent *)event {
// Stash the event for use by doCommandBySelector.
self.lastKeyDown = event;
[self interpretKeyEvents:[NSArray arrayWithObject:event]]; [self interpretKeyEvents:[NSArray arrayWithObject:event]];
self.lastKeyDown = nil;
NSString *keys = [event charactersIgnoringModifiers]; NSString *keys = [event charactersIgnoringModifiers];
gio_onKeys(self.handle, (__bridge CFTypeRef)keys, [event timestamp], [event modifierFlags], true); 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 { - (void)insertText:(id)string {
gio_onText(self.handle, (__bridge CFTypeRef)string); gio_onText(self.handle, (__bridge CFTypeRef)string);
} }
- (void)doCommandBySelector:(SEL)sel { - (void)doCommandBySelector:(SEL)action {
// Don't pass commands up the responder chain. NSEvent *event = self.lastKeyDown;
// They will end up in a beep. 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 { - (BOOL)hasMarkedText {
@@ -183,6 +225,10 @@ static void handleMouse(GioView *view, NSEvent *event, int typ, CGFloat dx, CGFl
} }
- (void)insertText:(id)string - (void)insertText:(id)string
replacementRange:(NSRange)replaceRange { replacementRange:(NSRange)replaceRange {
NSEvent *event = self.lastKeyDown;
if (event == nil) {
return;
}
NSString *str; NSString *str;
// string is either an NSAttributedString or an NSString. // string is either an NSAttributedString or an NSString.
if ([string isKindOfClass:[NSAttributedString class]]) { if ([string isKindOfClass:[NSAttributedString class]]) {
@@ -190,7 +236,7 @@ static void handleMouse(GioView *view, NSEvent *event, int typ, CGFloat dx, CGFl
} else { } else {
str = string; 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 { - (NSUInteger)characterIndexForPoint:(NSPoint)p {
return gio_characterIndexForPoint(self.handle, p); return gio_characterIndexForPoint(self.handle, p);