Files
cpython-source-deps/macosx/tkMacOSXKeyEvent.c
2020-09-24 22:55:34 +01:00

856 lines
22 KiB
C
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
* 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 <das@users.sourceforge.net>
* Copyright (c) 2012 Adrian Robert.
* Copyright 2015-2019 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"
/*
#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 Tk_Window composeWin = NULL;
static int caret_x = 0, caret_y = 0, caret_height = 0;
static unsigned short releaseCode;
static void setupXEvent(XEvent *xEvent, NSWindow *w, unsigned int state);
static unsigned isFunctionKey(unsigned int code);
#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;
}
/*
* Key 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.
*/
TkWindow *winPtr = TkMacOSXGetTkWindow(w);
Tk_Window tkwin = (Tk_Window) winPtr;
if (tkwin) {
TkWindow *grabWinPtr = winPtr->dispPtr->grabWinPtr;
/*
* 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.
*/
if (grabWinPtr) {
if (winPtr->dispPtr->grabFlags || /* global grab */
grabWinPtr->mainPtr == winPtr->mainPtr){ /* same appl. */
tkwin = (Tk_Window) winPtr->dispPtr->focusPtr;
}
}
} 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;
}
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 this is a function key or has modifiers */
} /* if not processing compose */
if (type == NSKeyDown) {
if (NS_KEYLOG) {
TKLog(@"keyDown: %s compose sequence.\n",
processingCompose == YES ? "Continue" : "Begin");
}
/*
* Call the interpretKeyEvents method to interpret composition key
* strokes. When it detects a complete composition sequence it will
* call our implementation of insertText: replacementRange, which
* generates a key down XEvent with the appropriate character. In IME
* when multiple characters have the same composition sequence and the
* chosen character is not the default it may be necessary to hit the
* enter key multiple times before the character is accepted and
* rendered. We send enter key events until inputText has cleared
* the processingCompose flag.
*/
processingCompose = YES;
while(processingCompose) {
[nsEvArray addObject: theEvent];
[[w contentView] interpretKeyEvents: nsEvArray];
[nsEvArray removeObject: theEvent];
if ([theEvent keyCode] != 36) {
break;
}
}
}
savedModifiers = modifiers;
return theEvent;
}
@end
@implementation TKContentView
-(id)init {
self = [super init];
if (self) {
_needsRedisplay = NO;
}
return self;
}
/*
* 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 over working text. In that case the call is followed
* immediately by doCommandBySelector: deleteBackward:
*/
- (void)insertText: (id)aString
replacementRange: (NSRange)repRange
{
int i, len;
XEvent xEvent;
NSString *str;
str = ([aString isKindOfClass: [NSAttributedString class]]) ?
[aString string] : aString;
len = [str length];
if (NS_KEYLOG) {
TKLog(@"insertText '%@'\tlen = %d", aString, len);
}
processingCompose = NO;
/*
* Clear any working text.
*/
if (privateWorkingText != nil) {
[self deleteWorkingText];
}
/*
* Insert the string as a sequence of keystrokes.
*/
setupXEvent(&xEvent, [self window], 0);
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) {
TkWindow *winPtr = TkMacOSXGetTkWindow([self window]);
Tk_Window focusWin = (Tk_Window) winPtr->dispPtr->focusPtr;
TkSendVirtualEvent(focusWin, "TkAccentBackspace", NULL);
}
/*
* Next we generate an XEvent for each unicode character in our string.
*
* NSString uses UTF-16 internally, which means that a non-BMP character is
* represented by a sequence of two 16-bit "surrogates". In principle we
* could record this in the XEvent by setting the keycode to the 32-bit
* unicode code point and setting the trans_chars string to the 4-byte
* UTF-8 string for the non-BMP character. However, that will not work
* when TCL_UTF_MAX is set to 3, as is the case for Tcl 8.6. A workaround
* used internally by Tcl 8.6 is to encode each surrogate as a 3-byte
* sequence using the UTF-8 algorithm (ignoring the fact that the UTF-8
* encoding specification does not allow encoding UTF-16 surrogates).
* This gives a 6-byte encoding of the non-BMP character which we write into
* the trans_chars field of the XEvent.
*/
for (i = 0; i < len; i++) {
xEvent.xkey.nbytes = TclUniAtIndex(str, i, xEvent.xkey.trans_chars,
&xEvent.xkey.keycode);
if (xEvent.xkey.keycode > 0xffff){
i++;
}
xEvent.xany.type = KeyPress;
Tk_QueueWindowEvent(&xEvent, TCL_QUEUE_TAIL);
}
releaseCode = (UInt16) [str characterAtIndex: 0];
}
/*
* 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);
temp = [str copy];
[self insertText: temp replacementRange:repRange];
privateWorkingText = temp;
processingCompose = YES;
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)cancelComposingText
{
if (NS_KEYLOG) {
TKLog(@"cancelComposingText");
}
[self deleteWorkingText];
processingCompose = NO;
}
- (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. */
@synthesize needsRedisplay = _needsRedisplay;
@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);
}
}
}
@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.display = Tk_Display(tkwin);
xEvent->xany.window = Tk_WindowId(tkwin);
xEvent->xkey.root = XRootWindow(Tk_Display(tkwin), 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. */
}
#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 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:
*/