/* * tkMacOSXKeyEvent.c -- * * This file implements functions that decode & handle keyboard events on * MacOS X. * * Copyright 2001-2009, Apple Inc. * Copyright (c) 2006-2009 Daniel A. Steffen * Copyright (c) 2012 Adrian Robert. * Copyright 2015-2020 Marc Culler. * * See the file "license.terms" for information on usage and redistribution of * this file, and for a DISCLAIMER OF ALL WARRANTIES. */ #include "tkMacOSXPrivate.h" #include "tkMacOSXInt.h" #include "tkMacOSXConstants.h" #include "tkMacOSXWm.h" /* * See tkMacOSXPrivate.h for macros related to key event processing. */ /* #ifdef TK_MAC_DEBUG #define TK_MAC_DEBUG_KEYBOARD #endif */ #define NS_KEYLOG 0 #define XEVENT_MOD_MASK (ControlMask | Mod1Mask | Mod3Mask | Mod4Mask) static Tk_Window keyboardGrabWinPtr = NULL; /* Current keyboard grab window. */ static NSWindow *keyboardGrabNSWindow = nil; /* Its underlying NSWindow.*/ static NSModalSession modalSession = nil; static BOOL processingCompose = NO; static Tk_Window composeWin = NULL; static int caret_x = 0, caret_y = 0, caret_height = 0; static void setupXEvent(XEvent *xEvent, Tk_Window tkwin, NSUInteger modifiers); static void setXEventPoint(XEvent *xEvent, Tk_Window tkwin, NSWindow *w); static NSUInteger textInputModifiers; #pragma mark TKApplication(TKKeyEvent) @implementation TKApplication(TKKeyEvent) - (NSEvent *) tkProcessKeyEvent: (NSEvent *) theEvent { #ifdef TK_MAC_DEBUG_EVENTS TKLog(@"-[%@(%p) %s] %@", [self class], self, _cmd, theEvent); #endif NSWindow *w = [theEvent window]; TkWindow *winPtr = TkMacOSXGetTkWindow(w), *grabWinPtr, *focusWinPtr; Tk_Window tkwin = (Tk_Window) winPtr; NSEventType type = [theEvent type]; NSUInteger virtual = [theEvent keyCode]; NSUInteger modifiers = ([theEvent modifierFlags] & NSDeviceIndependentModifierFlagsMask); XEvent xEvent; MacKeycode macKC; UniChar keychar = 0; Bool can_input_text, has_modifiers = NO, use_text_input = NO; static NSUInteger savedModifiers = 0; static NSMutableArray *nsEvArray = nil; if (nsEvArray == nil) { nsEvArray = [[NSMutableArray alloc] initWithCapacity: 1]; processingCompose = NO; } if (!winPtr) { return theEvent; } /* * If a local grab is in effect, key events for windows in the * grabber's application are redirected to the grabber. Key events * for other applications are delivered normally. If a global * grab is in effect all key events are redirected to the grabber. */ grabWinPtr = winPtr->dispPtr->grabWinPtr; if (grabWinPtr) { if (winPtr->dispPtr->grabFlags || /* global grab */ grabWinPtr->mainPtr == winPtr->mainPtr){ /* same application */ winPtr =winPtr->dispPtr->focusPtr; tkwin = (Tk_Window) winPtr; } } /* * Extract the unicode character from KeyUp and KeyDown events. */ if (type == NSKeyUp || type == NSKeyDown) { if ([[theEvent characters] length] > 0) { keychar = [[theEvent characters] characterAtIndex:0]; /* * Currently, real keys always send BMP characters, but who knows? */ if (CFStringIsSurrogateHighCharacter(keychar)) { UniChar lowChar = [[theEvent characters] characterAtIndex:1]; keychar = CFStringGetLongCharacterForSurrogatePair( keychar, lowChar); } } else { /* * This is a dead key, such as Option-e, so it should go to the * TextInputClient. */ use_text_input = YES; } /* * Apple uses 0x10 for unrecognized keys. */ if (keychar == 0x10) { keychar = UNKNOWN_KEYCHAR; } #if defined(TK_MAC_DEBUG_EVENTS) || NS_KEYLOG == 1 TKLog(@"-[%@(%p) %s] repeat=%d mods=%x char=%x code=%lu c=%d type=%d", [self class], self, _cmd, (type == NSKeyDown) && [theEvent isARepeat], modifiers, keychar, virtual, w, type); #endif } /* * Build a skeleton XEvent. We need to build it here, even if we will not * send it, so we can pass it to TkFocusKeyEvent to determine whether the * target widget can input text. */ setupXEvent(&xEvent, tkwin, modifiers); has_modifiers = xEvent.xkey.state & XEVENT_MOD_MASK; focusWinPtr = TkFocusKeyEvent(winPtr, &xEvent); if (focusWinPtr == NULL) { TKContentView *contentView = [w contentView]; /* * This NSEvent is being sent to a window which does not have focus. * This could mean, for example, that the user deactivated the Tk app * while the NSTextInputClient's popup character selection window was * still open. We attempt to abandon any ongoing composition operation * and discard the event. */ [contentView cancelComposingText]; return theEvent; } can_input_text = ((focusWinPtr->flags & TK_CAN_INPUT_TEXT) != 0); #if (NS_KEYLOG) TKLog(@"keyDown: %s compose sequence.\n", processingCompose == YES ? "Continue" : "Begin"); #endif /* * Decide whether this event should be processed with the NSTextInputClient * protocol. */ if (processingCompose || (type == NSKeyDown && can_input_text && !has_modifiers && IS_PRINTABLE(keychar)) ) { use_text_input = YES; } /* * If we are processing this KeyDown event as an NSTextInputClient we do * not queue an XEvent. We pass the NSEvent to our interpretKeyEvents * method. When the composition sequence is complete, the callback method * insertText: replacementRange will be called. That method generates a * keyPress XEvent with the selected character. */ if (use_text_input) { textInputModifiers = modifiers; /* * In IME the Enter key is used to terminate a composition sequence. * When there are multiple choices of input text available, and the * user's selected choice is not the default, it may be necessary to * hit the Enter key multiple times before the text is accepted and * rendered (See ticket 39de9677aa]). So when sending an Enter key * during composition, we continue sending Enter keys until the * inputText method has cleared the processingCompose flag. */ if (processingCompose && [theEvent keyCode] == 36) { [nsEvArray addObject: theEvent]; while(processingCompose) { [[w contentView] interpretKeyEvents: nsEvArray]; } [nsEvArray removeObject: theEvent]; } else { [nsEvArray addObject: theEvent]; [[w contentView] interpretKeyEvents: nsEvArray]; [nsEvArray removeObject: theEvent]; } return theEvent; } /* * We are not handling this event as an NSTextInputClient, so we need to * finish constructing the XEvent and queue it. */ macKC.v.o_s = ((modifiers & NSShiftKeyMask ? INDEX_SHIFT : 0) | (modifiers & NSAlternateKeyMask ? INDEX_OPTION : 0)); macKC.v.virtual = virtual; switch (type) { case NSFlagsChanged: /* * This XEvent is a simulated KeyPress or KeyRelease event for a * modifier key. To determine the type, note that the highest bit * where the flags differ is 1 if and only if it is a KeyPress. The * modifiers are saved so we can detect the next flag change. */ xEvent.xany.type = modifiers > savedModifiers ? KeyPress : KeyRelease; savedModifiers = modifiers; /* * Set the keychar to MOD_KEYCHAR as a signal to TkpGetKeySym (see * tkMacOSXKeyboard.c) that this is a modifier key event. */ keychar = MOD_KEYCHAR; break; case NSKeyUp: xEvent.xany.type = KeyRelease; break; case NSKeyDown: xEvent.xany.type = KeyPress; break; default: return theEvent; /* Unrecognized key event. */ } macKC.v.keychar = keychar; xEvent.xkey.keycode = macKC.uint; setXEventPoint(&xEvent, tkwin, w); /* * Finally we can queue the XEvent, inserting a KeyRelease before a * repeated KeyPress. */ if (type == NSKeyDown && [theEvent isARepeat]) { xEvent.xany.type = KeyRelease; Tk_QueueWindowEvent(&xEvent, TCL_QUEUE_TAIL); xEvent.xany.type = KeyPress; } Tk_QueueWindowEvent(&xEvent, TCL_QUEUE_TAIL); return theEvent; } @end @implementation TKContentView /* * Implementation of the NSTextInputClient protocol. */ /* [NSTextInputClient inputText: replacementRange:] is called by * interpretKeyEvents when a composition sequence is complete. It is also * called when we delete working text. In that case the call is followed * immediately by doCommandBySelector: deleteBackward: */ - (void)insertText: (id)aString replacementRange: (NSRange)repRange { int i, len, state; XEvent xEvent; NSString *str, *keystr, *lower; TkWindow *winPtr = TkMacOSXGetTkWindow([self window]); Tk_Window tkwin = (Tk_Window) winPtr; Bool sendingIMEText = NO; str = ([aString isKindOfClass: [NSAttributedString class]]) ? [aString string] : aString; len = [str length]; if (NS_KEYLOG) { TKLog(@"insertText '%@'\tlen = %d", aString, len); } /* * Clear any working text. */ if (privateWorkingText != nil) { sendingIMEText = YES; [self deleteWorkingText]; } /* * Insert the string as a sequence of keystrokes. */ setupXEvent(&xEvent, tkwin, textInputModifiers); setXEventPoint(&xEvent, tkwin, [self window]); xEvent.xany.type = KeyPress; /* * Apple evidently sets location to 0 to signal that an accented letter has * been selected from the accent menu. An unaccented letter has already * been displayed and we need to erase it before displaying the accented * letter. */ if (repRange.location == 0) { Tk_Window focusWin = (Tk_Window) winPtr->dispPtr->focusPtr; TkSendVirtualEvent(focusWin, "TkAccentBackspace", NULL); } /* * Next we generate an XEvent for each unicode character in our string. * This string could contain non-BMP characters, for example if the * emoji palette was used. * * NSString uses UTF-16 internally, which means that a non-BMP character is * represented by a sequence of two 16-bit "surrogates". We record this in * the XEvent by setting the low order 21-bits of the keycode to the UCS-32 * value value of the character and the virtual keycode in the high order * byte to the special value NON_BMP. */ state = xEvent.xkey.state; for (i = 0; i < len; i++) { UniChar keychar; MacKeycode macKC = {0}; keychar = [str characterAtIndex:i]; macKC.v.keychar = keychar; if (CFStringIsSurrogateHighCharacter(keychar)) { UniChar lowChar = [str characterAtIndex:++i]; macKC.v.keychar = CFStringGetLongCharacterForSurrogatePair( (UniChar)keychar, lowChar); macKC.v.virtual = NON_BMP_VIRTUAL; } else if (repRange.location == 0 || sendingIMEText) { macKC.v.virtual = REPLACEMENT_VIRTUAL; } else { macKC.uint = TkMacOSXAddVirtual(macKC.uint); xEvent.xkey.state |= INDEX2STATE(macKC.x.xvirtual); } keystr = [[NSString alloc] initWithCharacters:&keychar length:1]; lower = [keystr lowercaseString]; if (![keystr isEqual: lower]) { macKC.v.o_s |= INDEX_SHIFT; xEvent.xkey.state |= ShiftMask; } if (xEvent.xkey.state & Mod2Mask) { macKC.v.o_s |= INDEX_OPTION; } xEvent.xkey.keycode = macKC.uint; xEvent.xany.type = KeyPress; Tk_QueueWindowEvent(&xEvent, TCL_QUEUE_TAIL); xEvent.xkey.state = state; } } /* * This required method is allowed to return nil. */ - (NSAttributedString *)attributedSubstringForProposedRange:(NSRange)theRange actualRange:(NSRangePointer)thePointer { return nil; } /* * This method is supposed to insert (or replace selected text with) the string * argument. If the argument is an NSString, it should be displayed with a * distinguishing appearance, e.g underlined. */ - (void)setMarkedText: (id)aString selectedRange: (NSRange)selRange replacementRange: (NSRange)repRange { TkWindow *winPtr = TkMacOSXGetTkWindow([self window]); Tk_Window focusWin = (Tk_Window) winPtr->dispPtr->focusPtr; NSString *temp; NSString *str; str = ([aString isKindOfClass: [NSAttributedString class]]) ? [aString string] : aString; if (focusWin) { /* * Remember the widget where the composition is happening, in case it * gets defocussed during the composition. */ composeWin = focusWin; } else { return; } if (NS_KEYLOG) { TKLog(@"setMarkedText '%@' len =%lu range %lu from %lu", str, (unsigned long) [str length], (unsigned long) selRange.length, (unsigned long) selRange.location); } if (privateWorkingText != nil) { [self deleteWorkingText]; } if ([str length] == 0) { return; } /* * Use our insertText method to display the marked text. */ TkSendVirtualEvent(focusWin, "TkStartIMEMarkedText", NULL); processingCompose = YES; temp = [str copy]; [self insertText: temp replacementRange:repRange]; privateWorkingText = temp; TkSendVirtualEvent(focusWin, "TkEndIMEMarkedText", NULL); } - (BOOL)hasMarkedText { return privateWorkingText != nil; } - (NSRange)markedRange { NSRange rng = privateWorkingText != nil ? NSMakeRange(0, [privateWorkingText length]) : NSMakeRange(NSNotFound, 0); if (NS_KEYLOG) { TKLog(@"markedRange request"); } return rng; } - (void)unmarkText { if (NS_KEYLOG) { TKLog(@"unmarkText"); } [self deleteWorkingText]; processingCompose = NO; } /* * Called by the system to get a position for popup character selection windows * such as a Character Palette, or a selection menu for IME. */ - (NSRect)firstRectForCharacterRange: (NSRange)theRange actualRange: (NSRangePointer)thePointer { NSRect rect; NSPoint pt; pt.x = caret_x; pt.y = caret_y; pt = [self convertPoint: pt toView: nil]; pt = [[self window] tkConvertPointToScreen: pt]; pt.y -= caret_height; rect.origin = pt; rect.size.width = 0; rect.size.height = caret_height; return rect; } - (NSInteger)conversationIdentifier { return (NSInteger) self; } - (void)doCommandBySelector: (SEL)aSelector { if (NS_KEYLOG) { TKLog(@"doCommandBySelector: %@", NSStringFromSelector(aSelector)); } processingCompose = NO; if (aSelector == @selector (deleteBackward:)) { TkWindow *winPtr = TkMacOSXGetTkWindow([self window]); Tk_Window focusWin = (Tk_Window) winPtr->dispPtr->focusPtr; TkSendVirtualEvent(focusWin, "TkAccentBackspace", NULL); } } - (NSArray *)validAttributesForMarkedText { static NSArray *arr = nil; if (arr == nil) { arr = [[NSArray alloc] initWithObjects: NSUnderlineStyleAttributeName, NSUnderlineColorAttributeName, nil]; [arr retain]; } return arr; } - (NSRange)selectedRange { if (NS_KEYLOG) { TKLog(@"selectedRange request"); } return NSMakeRange(0, 0); } - (NSUInteger)characterIndexForPoint: (NSPoint)thePoint { if (NS_KEYLOG) { TKLog(@"characterIndexForPoint request"); } return NSNotFound; } - (NSAttributedString *)attributedSubstringFromRange: (NSRange)theRange { static NSAttributedString *str = nil; if (str == nil) { str = [NSAttributedString new]; } if (NS_KEYLOG) { TKLog(@"attributedSubstringFromRange request"); } return str; } /* End of NSTextInputClient implementation. */ @end @implementation TKContentView(TKKeyEvent) /* * Tell the widget to erase the displayed composing characters. This * is not part of the NSTextInputClient protocol. */ - (void)deleteWorkingText { if (privateWorkingText == nil) { return; } else { if (NS_KEYLOG) { TKLog(@"deleteWorkingText len = %lu\n", (unsigned long)[privateWorkingText length]); } [privateWorkingText release]; privateWorkingText = nil; processingCompose = NO; if (composeWin) { TkSendVirtualEvent(composeWin, "TkClearIMEMarkedText", NULL); } } } - (void)cancelComposingText { if (NS_KEYLOG) { TKLog(@"cancelComposingText"); } [self deleteWorkingText]; processingCompose = NO; } @end /* * Set up basic fields in xevent for keyboard input. */ static void setupXEvent(XEvent *xEvent, Tk_Window tkwin, NSUInteger modifiers) { unsigned int state = 0; Display *display = Tk_Display(tkwin); if (tkwin == NULL) { return; } if (modifiers) { state = (modifiers & NSAlphaShiftKeyMask ? LockMask : 0) | (modifiers & NSShiftKeyMask ? ShiftMask : 0) | (modifiers & NSControlKeyMask ? ControlMask : 0) | (modifiers & NSCommandKeyMask ? Mod1Mask : 0) | (modifiers & NSAlternateKeyMask ? Mod2Mask : 0) | (modifiers & NSNumericPadKeyMask ? Mod3Mask : 0) | (modifiers & NSFunctionKeyMask ? Mod4Mask : 0) ; } memset(xEvent, 0, sizeof(XEvent)); xEvent->xany.serial = LastKnownRequestProcessed(display); xEvent->xany.display = Tk_Display(tkwin); xEvent->xany.window = Tk_WindowId(tkwin); xEvent->xkey.root = XRootWindow(display, 0); xEvent->xkey.time = TkpGetMS(); xEvent->xkey.state = state; xEvent->xkey.same_screen = true; /* No need to initialize other fields implicitly here, * because of the memset() above. */ } static void setXEventPoint( XEvent *xEvent, Tk_Window tkwin, NSWindow *w) { TkWindow *winPtr = (TkWindow *) tkwin; NSPoint local = [w mouseLocationOutsideOfEventStream]; NSPoint global = [w tkConvertPointToScreen: local]; int win_x, win_y; if (Tk_IsEmbedded(winPtr)) { TkWindow *contPtr = TkpGetOtherWindow(winPtr); if (Tk_IsTopLevel(contPtr)) { local.x -= contPtr->wmInfoPtr->xInParent; local.y -= contPtr->wmInfoPtr->yInParent; } else { TkWindow *topPtr = TkMacOSXGetHostToplevel(winPtr)->winPtr; local.x -= (topPtr->wmInfoPtr->xInParent + contPtr->changes.x); local.y -= (topPtr->wmInfoPtr->yInParent + contPtr->changes.y); } } else if (winPtr->wmInfoPtr != NULL) { local.x -= winPtr->wmInfoPtr->xInParent; local.y -= winPtr->wmInfoPtr->yInParent; } tkwin = Tk_TopCoordsToWindow(tkwin, local.x, local.y, &win_x, &win_y); local.x = win_x; local.y = win_y; global.y = TkMacOSXZeroScreenHeight() - global.y; xEvent->xbutton.x = local.x; xEvent->xbutton.y = local.y; xEvent->xbutton.x_root = global.x; xEvent->xbutton.y_root = global.y; } #pragma mark - /* *---------------------------------------------------------------------- * * XGrabKeyboard -- * * Simulates a keyboard grab by setting the focus. * * Results: * Always returns GrabSuccess. * * Side effects: * Sets the keyboard focus to the specified window. * *---------------------------------------------------------------------- */ int XGrabKeyboard( Display* display, Window grab_window, Bool owner_events, int pointer_mode, int keyboard_mode, Time time) { keyboardGrabWinPtr = Tk_IdToWindow(display, grab_window); TkWindow *captureWinPtr = (TkWindow *) TkpGetCapture(); if (keyboardGrabWinPtr && captureWinPtr) { NSWindow *w = TkMacOSXDrawableWindow(grab_window); MacDrawable *macWin = (MacDrawable *) grab_window; if (w && macWin->toplevel->winPtr == (TkWindow *) captureWinPtr) { if (modalSession) { Tcl_Panic("XGrabKeyboard: already grabbed"); } keyboardGrabNSWindow = w; [w retain]; modalSession = [NSApp beginModalSessionForWindow:w]; } } return GrabSuccess; } /* *---------------------------------------------------------------------- * * XUngrabKeyboard -- * * Releases the simulated keyboard grab. * * Results: * None. * * Side effects: * Sets the keyboard focus back to the value before the grab. * *---------------------------------------------------------------------- */ int XUngrabKeyboard( Display* display, Time time) { if (modalSession) { [NSApp endModalSession:modalSession]; modalSession = nil; } if (keyboardGrabNSWindow) { [keyboardGrabNSWindow release]; keyboardGrabNSWindow = nil; } keyboardGrabWinPtr = NULL; return Success; } /* *---------------------------------------------------------------------- * * TkMacOSXGetModalSession -- * * Results: * Returns the current modal session * * Side effects: * None. * *---------------------------------------------------------------------- */ MODULE_SCOPE NSModalSession TkMacOSXGetModalSession(void) { return modalSession; } /* *---------------------------------------------------------------------- * * Tk_SetCaretPos -- * * This enables correct placement of the popups used for character * selection by the NSTextInputClient. It gets called by text entry * widgets whenever the cursor is drawn. It does nothing if the widget's * NSWindow is not the current KeyWindow. Otherwise it updates the * display's caret structure and records the caret geometry in static * variables for use by the NSTextInputClient implementation. Any * widget passed to this function will be marked as being able to input * text by setting the TK_CAN_INPUT_TEXT flag. * * Results: * None * * Side effects: * Sets the CAN_INPUT_TEXT flag on the widget passed as tkwin. May update * the display's caret structure as well as the static variables caret_x, * caret_y and caret_height. * *---------------------------------------------------------------------- */ void Tk_SetCaretPos( Tk_Window tkwin, int x, int y, int height) { TkWindow *winPtr = (TkWindow *) tkwin; TkCaret *caretPtr = &(winPtr->dispPtr->caret); NSWindow *w = TkMacOSXDrawableWindow(Tk_WindowId(tkwin)); /* * Register this widget as being capable of text input, so we know we * should process (appropriate) key events for this window with the * NSTextInputClient protocol. */ winPtr->flags |= TK_CAN_INPUT_TEXT; if (w && ![w isKeyWindow]) { return; } if ((caretPtr->winPtr == winPtr && caretPtr->x == x) && (caretPtr->y == y)) { return; } /* * Update the display's caret information. */ caretPtr->winPtr = winPtr; caretPtr->x = x; caretPtr->y = y; caretPtr->height = height; /* * Record the caret geometry in static variables for use when processing * key events. We use the TKContextView coordinate system for this. */ caret_height = height; while (!Tk_IsTopLevel(tkwin)) { x += Tk_X(tkwin); y += Tk_Y(tkwin); tkwin = Tk_Parent(tkwin); if (tkwin == NULL) { return; } } caret_x = x; caret_y = Tk_Height(tkwin) - y; } /* * Local Variables: * mode: objc * c-basic-offset: 4 * fill-column: 79 * coding: utf-8 * End: */