/* * 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 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 "tkMacOSXEvent.h" #include "tkMacOSXConstants.h" /* #ifdef TK_MAC_DEBUG #define TK_MAC_DEBUG_KEYBOARD #endif */ #define NS_KEYLOG 0 static Tk_Window keyboardGrabWinPtr = NULL; /* Current keyboard grab window. */ static NSWindow *keyboardGrabNSWindow = nil; /* NSWindow for the current keyboard grab * window. */ static NSModalSession modalSession = nil; static BOOL processingCompose = NO; static BOOL finishedCompose = NO; static int caret_x = 0, caret_y = 0, caret_height = 0; static void setupXEvent(XEvent *xEvent, NSWindow *w, unsigned int state); static unsigned isFunctionKey(unsigned int code); unsigned short releaseCode; #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; NSEventType type = [theEvent type]; NSUInteger modifiers = ([theEvent modifierFlags] & NSDeviceIndependentModifierFlagsMask); NSUInteger len = 0; BOOL repeat = NO; unsigned short keyCode = [theEvent keyCode]; NSString *characters = nil, *charactersIgnoringModifiers = nil; static NSUInteger savedModifiers = 0; static NSMutableArray *nsEvArray; if (nsEvArray == nil) { nsEvArray = [[NSMutableArray alloc] initWithCapacity: 1]; processingCompose = NO; } w = [theEvent window]; TkWindow *winPtr = TkMacOSXGetTkWindow(w); Tk_Window tkwin = (Tk_Window) winPtr; XEvent xEvent; if (!winPtr) { return theEvent; } /* * Control-Tab and Control-Shift-Tab are used to switch tabs in a tabbed * window. We do not want to generate an Xevent for these since that might * cause the deselected tab to be reactivated. */ if (keyCode == 48 && (modifiers & NSControlKeyMask) == NSControlKeyMask) { return theEvent; } switch (type) { case NSKeyUp: /*Fix for bug #1ba71a86bb: key release firing on key press.*/ setupXEvent(&xEvent, w, 0); xEvent.xany.type = KeyRelease; xEvent.xkey.keycode = releaseCode; xEvent.xany.serial = LastKnownRequestProcessed(Tk_Display(tkwin)); case NSKeyDown: repeat = [theEvent isARepeat]; characters = [theEvent characters]; charactersIgnoringModifiers = [theEvent charactersIgnoringModifiers]; len = [charactersIgnoringModifiers length]; case NSFlagsChanged: #if defined(TK_MAC_DEBUG_EVENTS) || NS_KEYLOG == 1 TKLog(@"-[%@(%p) %s] r=%d mods=%u '%@' '%@' code=%u c=%d %@ %d", [self class], self, _cmd, repeat, modifiers, characters, charactersIgnoringModifiers, keyCode,([charactersIgnoringModifiers length] == 0) ? 0 : [charactersIgnoringModifiers characterAtIndex: 0], w, type); #endif break; default: return theEvent; /* Unrecognized key event. */ } /* * Create an Xevent to add to the Tk queue. */ if (!processingCompose) { unsigned int state = 0; if (modifiers & NSAlphaShiftKeyMask) { state |= LockMask; } if (modifiers & NSShiftKeyMask) { state |= ShiftMask; } if (modifiers & NSControlKeyMask) { state |= ControlMask; } if (modifiers & NSCommandKeyMask) { state |= Mod1Mask; /* command key */ } if (modifiers & NSAlternateKeyMask) { state |= Mod2Mask; /* option key */ } if (modifiers & NSNumericPadKeyMask) { state |= Mod3Mask; } if (modifiers & NSFunctionKeyMask) { state |= Mod4Mask; } /* * Events are only received for the front Window on the Macintosh. So * to build an XEvent we look up the Tk window associated to the Front * window. If a different window has a local grab we ignore the event. */ TkWindow *winPtr = TkMacOSXGetTkWindow(w); Tk_Window tkwin = (Tk_Window) winPtr; if (tkwin) { TkWindow *grabWinPtr = winPtr->dispPtr->grabWinPtr; if (grabWinPtr && grabWinPtr != winPtr && !winPtr->dispPtr->grabFlags /* this means the grab is local. */ && grabWinPtr->mainPtr == winPtr->mainPtr) { return theEvent; } } else { tkwin = (Tk_Window) winPtr->dispPtr->focusPtr; } if (!tkwin) { TkMacOSXDbgMsg("tkwin == NULL"); return theEvent; /* Give up. No window for this event. */ } /* * If it's a function key, or we have modifiers other than Shift or * Alt, pass it straight to Tk. Otherwise we'll send for input * processing. */ int code = (len == 0) ? 0 : [charactersIgnoringModifiers characterAtIndex: 0]; if (type != NSKeyDown || isFunctionKey(code) || (len > 0 && state & (ControlMask | Mod1Mask | Mod3Mask | Mod4Mask))) { XEvent xEvent; setupXEvent(&xEvent, w, state); if (type == NSFlagsChanged) { if (savedModifiers > modifiers) { xEvent.xany.type = KeyRelease; } else { xEvent.xany.type = KeyPress; } /* * Use special '-1' to signify a special keycode to our * platform specific code in tkMacOSXKeyboard.c. This is rather * like what happens on Windows. */ xEvent.xany.send_event = -1; /* * Set keycode (which was zero) to the changed modifier */ xEvent.xkey.keycode = (modifiers ^ savedModifiers); } else { if (type == NSKeyUp || repeat) { xEvent.xany.type = KeyRelease; } else { xEvent.xany.type = KeyPress; } /* * For command key, take input manager's word so things like * dvorak / qwerty layout work. */ if ((modifiers & NSCommandKeyMask) == NSCommandKeyMask && (modifiers & NSAlternateKeyMask) != NSAlternateKeyMask && len > 0 && !isFunctionKey(code)) { // head off keycode-based translation in tkMacOSXKeyboard.c xEvent.xkey.nbytes = [characters length]; //len } if ([characters length] > 0) { xEvent.xkey.keycode = (keyCode << 16) | (UInt16) [characters characterAtIndex:0]; if (![characters getCString:xEvent.xkey.trans_chars maxLength:XMaxTransChars encoding:NSUTF8StringEncoding]) { /* prevent SF bug 2907388 (crash on some composite chars) */ //PENDING: we might not need this anymore TkMacOSXDbgMsg("characters too long"); return theEvent; } } if (repeat) { Tk_QueueWindowEvent(&xEvent, TCL_QUEUE_TAIL); xEvent.xany.type = KeyPress; xEvent.xany.serial = LastKnownRequestProcessed(Tk_Display(tkwin)); } } Tk_QueueWindowEvent(&xEvent, TCL_QUEUE_TAIL); savedModifiers = modifiers; return theEvent; } /* if send straight to TK */ } /* if not processing compose */ if (type == NSKeyDown) { if (NS_KEYLOG) { TKLog(@"keyDown: %s compose sequence.\n", processingCompose == YES ? "Continue" : "Begin"); } processingCompose = YES; [nsEvArray addObject: theEvent]; [[w contentView] interpretKeyEvents: nsEvArray]; [nsEvArray removeObject: theEvent]; } savedModifiers = modifiers; return theEvent; } @end @implementation TKContentView -(id)init { self = [super init]; if (self) { _needsRedisplay = NO; } return self; } /* implementation (called through interpretKeyEvents:]). */ /* : called when done composing; NOTE: also called when we delete over working text, followed immed. by doCommandBySelector: deleteBackward: */ - (void)insertText: (id)aString { int i, len = [(NSString *) aString length]; XEvent xEvent; if (NS_KEYLOG) { TKLog(@"insertText '%@'\tlen = %d", aString, len); } processingCompose = NO; finishedCompose = YES; /* * First, clear any working text. */ if (privateWorkingText != nil) { [self deleteWorkingText]; } /* * Now insert the string as keystrokes. */ setupXEvent(&xEvent, [self window], 0); xEvent.xany.type = KeyPress; for (i =0; i: inserts display of composing characters */ - (void)setMarkedText: (id)aString selectedRange: (NSRange)selRange { NSString *str = [aString respondsToSelector: @selector (string)] ? [aString string] : aString; 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; } processingCompose = YES; privateWorkingText = [str copy]; //PENDING: insert workingText underlined } - (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(@"unmark (accept) text"); } [self deleteWorkingText]; processingCompose = NO; } /* used to position char selection windows, etc. */ - (NSRect)firstRectForCharacterRange: (NSRange)theRange { 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 = caret_height; 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:)) { /* * Happens when user backspaces over an ongoing composition: * throw a 'delete' into the event queue. */ XEvent xEvent; setupXEvent(&xEvent, [self window], 0); xEvent.xany.type = KeyPress; xEvent.xkey.nbytes = 1; xEvent.xkey.keycode = (0x33 << 16) | 0x7F; xEvent.xkey.trans_chars[0] = 0x7F; xEvent.xkey.trans_chars[1] = 0x0; Tk_QueueWindowEvent(&xEvent, TCL_QUEUE_TAIL); } } - (NSArray *)validAttributesForMarkedText { static NSArray *arr = nil; if (arr == nil) { arr = [NSArray new]; } /* [[NSArray arrayWithObject: NSUnderlineStyleAttributeName] retain]; */ return arr; } - (NSRange)selectedRange { if (NS_KEYLOG) { TKLog(@"selectedRange request"); } return NSMakeRange(NSNotFound, 0); } - (NSUInteger)characterIndexForPoint: (NSPoint)thePoint { if (NS_KEYLOG) { TKLog(@"characterIndexForPoint request"); } return 0; } - (NSAttributedString *)attributedSubstringFromRange: (NSRange)theRange { static NSAttributedString *str = nil; if (str == nil) { str = [NSAttributedString new]; } if (NS_KEYLOG) { TKLog(@"attributedSubstringFromRange request"); } return str; } /* End impl. */ @synthesize needsRedisplay = _needsRedisplay; @end @implementation TKContentView(TKKeyEvent) /* delete display of composing characters [not in ] */ - (void)deleteWorkingText { if (privateWorkingText == nil) { return; } if (NS_KEYLOG) { TKLog(@"deleteWorkingText len = %lu\n", (unsigned long)[privateWorkingText length]); } [privateWorkingText release]; privateWorkingText = nil; processingCompose = NO; //PENDING: delete working text } @end /* * Set up basic fields in xevent for keyboard input. */ static void setupXEvent(XEvent *xEvent, NSWindow *w, unsigned int state) { TkWindow *winPtr = TkMacOSXGetTkWindow(w); Tk_Window tkwin = (Tk_Window) winPtr; if (!winPtr) { return; } memset(xEvent, 0, sizeof(XEvent)); xEvent->xany.serial = LastKnownRequestProcessed(Tk_Display(tkwin)); xEvent->xany.send_event = false; xEvent->xany.display = Tk_Display(tkwin); xEvent->xany.window = Tk_WindowId(tkwin); xEvent->xkey.root = XRootWindow(Tk_Display(tkwin), 0); xEvent->xkey.subwindow = None; xEvent->xkey.time = TkpGetMS(); xEvent->xkey.state = state; xEvent->xkey.same_screen = true; xEvent->xkey.trans_chars[0] = 0; xEvent->xkey.nbytes = 0; } #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 *) TkMacOSXGetCapture(); 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 XIM caret. This is called by * widgets to indicate their cursor placement, and the caret location is * used by TkpGetString to place the XIM caret. * * Results: * None * * Side effects: * None * *---------------------------------------------------------------------- */ void Tk_SetCaretPos( Tk_Window tkwin, int x, int y, int height) { TkCaret *caretPtr = &(((TkWindow *) tkwin)->dispPtr->caret); /* * Prevent processing anything if the values haven't changed. Windows only * has one display, so we can do this with statics. */ if ((caretPtr->winPtr == ((TkWindow *) tkwin)) && (caretPtr->x == x) && (caretPtr->y == y)) { return; } caretPtr->winPtr = ((TkWindow *) tkwin); caretPtr->x = x; caretPtr->y = y; caretPtr->height = height; /* * As in Windows, adjust to the toplevel to get the coords right. */ while (!Tk_IsTopLevel(tkwin)) { x += Tk_X(tkwin); y += Tk_Y(tkwin); tkwin = Tk_Parent(tkwin); if (tkwin == NULL) { return; } } /* * But adjust for fact that NS uses flipped view. */ y = Tk_Height(tkwin) - y; caret_x = x; caret_y = y; caret_height = height; } static unsigned convert_ns_to_X_keysym[] = { NSHomeFunctionKey, 0x50, NSLeftArrowFunctionKey, 0x51, NSUpArrowFunctionKey, 0x52, NSRightArrowFunctionKey, 0x53, NSDownArrowFunctionKey, 0x54, NSPageUpFunctionKey, 0x55, NSPageDownFunctionKey, 0x56, NSEndFunctionKey, 0x57, NSBeginFunctionKey, 0x58, NSSelectFunctionKey, 0x60, NSPrintFunctionKey, 0x61, NSExecuteFunctionKey, 0x62, NSInsertFunctionKey, 0x63, NSUndoFunctionKey, 0x65, NSRedoFunctionKey, 0x66, NSMenuFunctionKey, 0x67, NSFindFunctionKey, 0x68, NSHelpFunctionKey, 0x6A, NSBreakFunctionKey, 0x6B, NSF1FunctionKey, 0xBE, NSF2FunctionKey, 0xBF, NSF3FunctionKey, 0xC0, NSF4FunctionKey, 0xC1, NSF5FunctionKey, 0xC2, NSF6FunctionKey, 0xC3, NSF7FunctionKey, 0xC4, NSF8FunctionKey, 0xC5, NSF9FunctionKey, 0xC6, NSF10FunctionKey, 0xC7, NSF11FunctionKey, 0xC8, NSF12FunctionKey, 0xC9, NSF13FunctionKey, 0xCA, NSF14FunctionKey, 0xCB, NSF15FunctionKey, 0xCC, NSF16FunctionKey, 0xCD, NSF17FunctionKey, 0xCE, NSF18FunctionKey, 0xCF, NSF19FunctionKey, 0xD0, NSF20FunctionKey, 0xD1, NSF21FunctionKey, 0xD2, NSF22FunctionKey, 0xD3, NSF23FunctionKey, 0xD4, NSF24FunctionKey, 0xD5, NSBackspaceCharacter, 0x08, /* 8: Not on some KBs. */ NSDeleteCharacter, 0xFF, /* 127: Big 'delete' key upper right. */ NSDeleteFunctionKey, 0x9F, /* 63272: Del forw key off main array. */ NSTabCharacter, 0x09, 0x19, 0x09, /* left tab->regular since pass shift */ NSCarriageReturnCharacter, 0x0D, NSNewlineCharacter, 0x0D, NSEnterCharacter, 0x8D, 0x1B, 0x1B /* escape */ }; static unsigned isFunctionKey( unsigned code) { const unsigned last_keysym = (sizeof(convert_ns_to_X_keysym) / sizeof(convert_ns_to_X_keysym[0])); unsigned keysym; for (keysym = 0; keysym < last_keysym; keysym += 2) { if (code == convert_ns_to_X_keysym[keysym]) { return 0xFF00 | convert_ns_to_X_keysym[keysym + 1]; } } return 0; } /* * Local Variables: * mode: objc * c-basic-offset: 4 * fill-column: 79 * coding: utf-8 * End: */