From 0f7a7fc35d8d977becc8b7f2a903a59c054deed7 Mon Sep 17 00:00:00 2001 From: Greg Lucas Date: Sat, 30 Oct 2021 19:32:24 +0100 Subject: [PATCH 1/5] MNT: Add modifier key press handling to macosx backend The flagsChanged event handles single presses of modifier keys, so we need to send the corresponding string to our key press handlers from within that routine. Modifier + second key is handled within the KeyUp/KeyDown routines already, so leave those alone. --- src/_macosx.m | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/src/_macosx.m b/src/_macosx.m index 9bae57ea0447..cbf80827e38c 100755 --- a/src/_macosx.m +++ b/src/_macosx.m @@ -247,7 +247,7 @@ - (void)keyDown:(NSEvent*)event; - (void)keyUp:(NSEvent*)event; - (void)scrollWheel:(NSEvent *)event; - (BOOL)acceptsFirstResponder; -//- (void)flagsChanged:(NSEvent*)event; +- (void)flagsChanged:(NSEvent*)event; @end /* ---------------------------- Python classes ---------------------------- */ @@ -1711,21 +1711,24 @@ - (BOOL)acceptsFirstResponder return YES; } -/* This is all wrong. Address of pointer is being passed instead of pointer, keynames don't - match up with what the front-end and does the front-end even handle modifier keys by themselves? - -- (void)flagsChanged:(NSEvent*)event +// flagsChanged gets called on single modifier keypresses +// The modifier + second key gets handled in convertKeyEvent in KeyUp/KeyDown +- (void)flagsChanged:(NSEvent *)event { const char *s = NULL; - if (([event modifierFlags] & NSControlKeyMask) == NSControlKeyMask) + if ([event modifierFlags] & NSEventModifierFlagControl) s = "control"; - else if (([event modifierFlags] & NSShiftKeyMask) == NSShiftKeyMask) + else if ([event modifierFlags] & NSEventModifierFlagShift) s = "shift"; - else if (([event modifierFlags] & NSAlternateKeyMask) == NSAlternateKeyMask) + else if ([event modifierFlags] & NSEventModifierFlagOption) s = "alt"; + else if ([event modifierFlags] & NSEventModifierFlagCommand) + s = "cmd"; + else if ([event modifierFlags] & NSEventModifierFlagCapsLock) + s = "caps_lock"; else return; PyGILState_STATE gstate = PyGILState_Ensure(); - PyObject* result = PyObject_CallMethod(canvas, "key_press_event", "s", &s); + PyObject* result = PyObject_CallMethod(canvas, "key_press_event", "s", s); if (result) Py_DECREF(result); else @@ -1733,7 +1736,6 @@ - (void)flagsChanged:(NSEvent*)event PyGILState_Release(gstate); } - */ @end static PyObject* From 97c99c2cc9554ecc9d7d25f6b2715f7b9f709280 Mon Sep 17 00:00:00 2001 From: Greg Lucas Date: Sat, 20 Nov 2021 14:25:46 -0700 Subject: [PATCH 2/5] FIX: macosx key-press key-release events Handle the key-press and release events on macosx. --- src/_macosx.m | 116 +++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 92 insertions(+), 24 deletions(-) diff --git a/src/_macosx.m b/src/_macosx.m index cbf80827e38c..a642a1c5a618 100755 --- a/src/_macosx.m +++ b/src/_macosx.m @@ -62,6 +62,20 @@ Needed to know when to stop the NSApp */ static long FigureWindowCount = 0; +/* Keep track of modifier key states for flagsChanged + to keep track of press vs release */ +static bool lastCommand = false; +static bool lastControl = false; +static bool lastShift = false; +static bool lastOption = false; +static bool lastCapsLock = false; +/* Keep track of whether this specific key modifier was pressed or not */ +static bool keyChangeCommand = false; +static bool keyChangeControl = false; +static bool keyChangeShift = false; +static bool keyChangeOption = false; +static bool keyChangeCapsLock = false; + /* -------------------------- Helper function ---------------------------- */ static void @@ -1641,8 +1655,6 @@ - (const char*)convertKeyEvent:(NSEvent*)event } [returnkey appendString:specialchar]; } - else - [returnkey appendString:[event charactersIgnoringModifiers]]; return [returnkey UTF8String]; } @@ -1711,30 +1723,86 @@ - (BOOL)acceptsFirstResponder return YES; } -// flagsChanged gets called on single modifier keypresses -// The modifier + second key gets handled in convertKeyEvent in KeyUp/KeyDown +// flagsChanged gets called whenever a modifier key is pressed OR released +// so we need to handle both cases here - (void)flagsChanged:(NSEvent *)event { - const char *s = NULL; - if ([event modifierFlags] & NSEventModifierFlagControl) - s = "control"; - else if ([event modifierFlags] & NSEventModifierFlagShift) - s = "shift"; - else if ([event modifierFlags] & NSEventModifierFlagOption) - s = "alt"; - else if ([event modifierFlags] & NSEventModifierFlagCommand) - s = "cmd"; - else if ([event modifierFlags] & NSEventModifierFlagCapsLock) - s = "caps_lock"; - else return; - PyGILState_STATE gstate = PyGILState_Ensure(); - PyObject* result = PyObject_CallMethod(canvas, "key_press_event", "s", s); - if (result) - Py_DECREF(result); - else - PyErr_Print(); - - PyGILState_Release(gstate); + bool keypress = false; + bool keyrelease = false; + if (([event modifierFlags] & NSEventModifierFlagCommand) && !lastCommand) { + // Command pressed + lastCommand = true; + keyChangeCommand = true; + keypress = true; + } + else if (!([event modifierFlags] & NSEventModifierFlagCommand) && lastCommand) { + // Command released + lastCommand = false; + keyChangeCommand = true; + keyrelease = true; + } + else if (([event modifierFlags] & NSEventModifierFlagControl) && !lastControl) { + // Control pressed + lastControl = true; + keyChangeControl = true; + keypress = true; + } + else if (!([event modifierFlags] & NSEventModifierFlagControl) && lastControl) { + // Control released + lastControl = false; + keyChangeControl = true; + keyrelease = true; + } + else if (([event modifierFlags] & NSEventModifierFlagShift) && !lastShift) { + // Shift pressed + lastShift = true; + keyChangeShift = true; + keypress = true; + } + else if (!([event modifierFlags] & NSEventModifierFlagShift) && lastShift) { + // Shift released + lastShift = false; + keyChangeShift = true; + keyrelease = true; + } + else if (([event modifierFlags] & NSEventModifierFlagOption) && !lastOption) { + // Option pressed + lastOption = true; + keyChangeOption = true; + keypress = true; + } + else if (!([event modifierFlags] & NSEventModifierFlagOption) && lastOption) { + // Option released + lastOption = false; + keyChangeOption = true; + keyrelease = true; + } + else if (([event modifierFlags] & NSEventModifierFlagCapsLock) && !lastCapsLock) { + // Capslock pressed + lastCapsLock = true; + keyChangeCapsLock = true; + keypress = true; + } + else if (!([event modifierFlags] & NSEventModifierFlagCapsLock) && lastCapsLock) { + // Capslock released + lastCapsLock = false; + keyChangeCapsLock = true; + keyrelease = true; + } + + if (keypress) { + [self keyDown: event]; + } + else if (keyrelease) { + [self keyUp: event]; + } + + // Reset the state for the key changes after handling the event + keyChangeCommand = false; + keyChangeControl = false; + keyChangeShift = false; + keyChangeOption = false; + keyChangeCapsLock = false; } @end From f00a204779d2a94fc72fb8fd727ddc7bf4d6ee07 Mon Sep 17 00:00:00 2001 From: Greg Lucas Date: Sun, 21 Nov 2021 08:20:48 -0700 Subject: [PATCH 3/5] MNT: Cleanup the logic in macosx keypress handling This removes half of the else-if blocks and combines the keypress/keyrelease for each key into a single statement with negation for whether it was a press or release being handled. --- src/_macosx.m | 84 +++++++++++++++++++-------------------------------- 1 file changed, 31 insertions(+), 53 deletions(-) diff --git a/src/_macosx.m b/src/_macosx.m index a642a1c5a618..d5ceba5aae39 100755 --- a/src/_macosx.m +++ b/src/_macosx.m @@ -1727,73 +1727,51 @@ - (BOOL)acceptsFirstResponder // so we need to handle both cases here - (void)flagsChanged:(NSEvent *)event { - bool keypress = false; - bool keyrelease = false; - if (([event modifierFlags] & NSEventModifierFlagCommand) && !lastCommand) { - // Command pressed - lastCommand = true; + bool isPress = false; // true if key is pressed, false is released + if ((([event modifierFlags] & NSEventModifierFlagCommand) && !lastCommand) || + (!([event modifierFlags] & NSEventModifierFlagCommand) && lastCommand)) { + // Command pressed/released + lastCommand = !lastCommand; keyChangeCommand = true; - keypress = true; + isPress = lastCommand; } - else if (!([event modifierFlags] & NSEventModifierFlagCommand) && lastCommand) { - // Command released - lastCommand = false; - keyChangeCommand = true; - keyrelease = true; - } - else if (([event modifierFlags] & NSEventModifierFlagControl) && !lastControl) { - // Control pressed - lastControl = true; + else if ((([event modifierFlags] & NSEventModifierFlagControl) && !lastControl) || + (!([event modifierFlags] & NSEventModifierFlagControl) && lastControl)) { + // Control pressed/released + lastControl = !lastControl; keyChangeControl = true; - keypress = true; - } - else if (!([event modifierFlags] & NSEventModifierFlagControl) && lastControl) { - // Control released - lastControl = false; - keyChangeControl = true; - keyrelease = true; - } - else if (([event modifierFlags] & NSEventModifierFlagShift) && !lastShift) { - // Shift pressed - lastShift = true; - keyChangeShift = true; - keypress = true; + isPress = lastControl; } - else if (!([event modifierFlags] & NSEventModifierFlagShift) && lastShift) { - // Shift released - lastShift = false; + else if ((([event modifierFlags] & NSEventModifierFlagShift) && !lastShift) || + (!([event modifierFlags] & NSEventModifierFlagShift) && lastShift)) { + // Shift pressed/released + lastShift = !lastShift; keyChangeShift = true; - keyrelease = true; - } - else if (([event modifierFlags] & NSEventModifierFlagOption) && !lastOption) { - // Option pressed - lastOption = true; - keyChangeOption = true; - keypress = true; + isPress = lastShift; } - else if (!([event modifierFlags] & NSEventModifierFlagOption) && lastOption) { - // Option released - lastOption = false; + else if ((([event modifierFlags] & NSEventModifierFlagOption) && !lastOption) || + (!([event modifierFlags] & NSEventModifierFlagOption) && lastOption)) { + // Option pressed/released + lastOption = !lastOption; keyChangeOption = true; - keyrelease = true; + isPress = lastOption; } - else if (([event modifierFlags] & NSEventModifierFlagCapsLock) && !lastCapsLock) { - // Capslock pressed - lastCapsLock = true; + else if ((([event modifierFlags] & NSEventModifierFlagCapsLock) && !lastCapsLock) || + (!([event modifierFlags] & NSEventModifierFlagCapsLock) && lastCapsLock)) { + // Capslock pressed/released + lastCapsLock = !lastCapsLock; keyChangeCapsLock = true; - keypress = true; + isPress = lastCapsLock; } - else if (!([event modifierFlags] & NSEventModifierFlagCapsLock) && lastCapsLock) { - // Capslock released - lastCapsLock = false; - keyChangeCapsLock = true; - keyrelease = true; + else{ + // flag we don't handle + return; } - if (keypress) { + if (isPress) { [self keyDown: event]; } - else if (keyrelease) { + else { [self keyUp: event]; } From a88e2786eaa710b067630db46bbe6ea845931c83 Mon Sep 17 00:00:00 2001 From: Greg Lucas Date: Mon, 22 Nov 2021 11:42:20 -0700 Subject: [PATCH 4/5] MNT: Change if blocks from negated OR to XOR and handle shift key - This updates the if conditional to use XOR logic instead of the two negated and/or conditions. - It also handles the shift modifier when combined with other modifier keys, as before (ctrl + shift + cmd) would have not printed out the middle shift. --- src/_macosx.m | 80 ++++++++++++++++++++++++++++++--------------------- 1 file changed, 48 insertions(+), 32 deletions(-) diff --git a/src/_macosx.m b/src/_macosx.m index d5ceba5aae39..5088cb865605 100755 --- a/src/_macosx.m +++ b/src/_macosx.m @@ -1637,23 +1637,44 @@ - (const char*)convertKeyEvent:(NSEvent*)event ]; NSMutableString* returnkey = [NSMutableString string]; - if ([event modifierFlags] & NSEventModifierFlagControl) { - [returnkey appendString:@"ctrl+" ]; - } - if ([event modifierFlags] & NSEventModifierFlagOption) { + if (keyChangeControl) { + // When control is the key that was pressed, return the full word + [returnkey appendString:@"control+"]; + } else if (([event modifierFlags] & NSEventModifierFlagControl)) { + // If control is already pressed, return the shortened version + [returnkey appendString:@"ctrl+"]; + } + if (([event modifierFlags] & NSEventModifierFlagOption) || keyChangeOption) { [returnkey appendString:@"alt+" ]; } - if ([event modifierFlags] & NSEventModifierFlagCommand) { + if (([event modifierFlags] & NSEventModifierFlagCommand) || keyChangeCommand) { [returnkey appendString:@"cmd+" ]; } + // Don't print caps_lock unless it was the key that got pressed + if (keyChangeCapsLock) { + [returnkey appendString:@"caps_lock+" ]; + } - unichar uc = [[event charactersIgnoringModifiers] characterAtIndex:0]; - NSString* specialchar = [specialkeymappings objectForKey:[NSNumber numberWithUnsignedLong:uc]]; - if (specialchar) { - if ([event modifierFlags] & NSEventModifierFlagShift) { - [returnkey appendString:@"shift+" ]; + // flagsChanged event can't handle charactersIgnoringModifiers + // because it was a modifier key that was pressed/released + if (event.type != NSEventTypeFlagsChanged) { + unichar uc = [[event charactersIgnoringModifiers] characterAtIndex:0]; + NSString *specialchar = [specialkeymappings objectForKey:[NSNumber numberWithUnsignedLong:uc]]; + if (specialchar) { + if (([event modifierFlags] & NSEventModifierFlagShift) || keyChangeShift) { + [returnkey appendString:@"shift+"]; + } + [returnkey appendString:specialchar]; + } else { + [returnkey appendString:[event charactersIgnoringModifiers]]; } - [returnkey appendString:specialchar]; + } else { + if (([event modifierFlags] & NSEventModifierFlagShift) || keyChangeShift) { + [returnkey appendString:@"shift+"]; + } + // Since it was a modifier event trim the final character of the string + // because we added in "+" earlier + [returnkey setString: [returnkey substringToIndex:[returnkey length] - 1]]; } return [returnkey UTF8String]; @@ -1727,52 +1748,47 @@ - (BOOL)acceptsFirstResponder // so we need to handle both cases here - (void)flagsChanged:(NSEvent *)event { - bool isPress = false; // true if key is pressed, false is released - if ((([event modifierFlags] & NSEventModifierFlagCommand) && !lastCommand) || - (!([event modifierFlags] & NSEventModifierFlagCommand) && lastCommand)) { + bool isPress = false; // true if key is pressed, false if key was released + + // Each if clause tests the two cases for each of the keys we can handle + // 1. If the modifier flag "command key" is pressed and it was not previously + // 2. If the modifier flag "command key" is not pressed and it was previously + // !! converts the result of the bitwise & operator to a logical boolean, + // which allows us to then bitwise xor (^) the result with a boolean (lastCommand). + if (!!([event modifierFlags] & NSEventModifierFlagCommand) ^ lastCommand) { // Command pressed/released lastCommand = !lastCommand; keyChangeCommand = true; isPress = lastCommand; - } - else if ((([event modifierFlags] & NSEventModifierFlagControl) && !lastControl) || - (!([event modifierFlags] & NSEventModifierFlagControl) && lastControl)) { + } else if (!!([event modifierFlags] & NSEventModifierFlagControl) ^ lastControl) { // Control pressed/released lastControl = !lastControl; keyChangeControl = true; isPress = lastControl; - } - else if ((([event modifierFlags] & NSEventModifierFlagShift) && !lastShift) || - (!([event modifierFlags] & NSEventModifierFlagShift) && lastShift)) { + } else if (!!([event modifierFlags] & NSEventModifierFlagShift) ^ lastShift) { // Shift pressed/released lastShift = !lastShift; keyChangeShift = true; isPress = lastShift; - } - else if ((([event modifierFlags] & NSEventModifierFlagOption) && !lastOption) || - (!([event modifierFlags] & NSEventModifierFlagOption) && lastOption)) { + } else if (!!([event modifierFlags] & NSEventModifierFlagOption) ^ lastOption) { // Option pressed/released lastOption = !lastOption; keyChangeOption = true; isPress = lastOption; - } - else if ((([event modifierFlags] & NSEventModifierFlagCapsLock) && !lastCapsLock) || - (!([event modifierFlags] & NSEventModifierFlagCapsLock) && lastCapsLock)) { + } else if (!!([event modifierFlags] & NSEventModifierFlagCapsLock) ^ lastCapsLock) { // Capslock pressed/released lastCapsLock = !lastCapsLock; keyChangeCapsLock = true; isPress = lastCapsLock; - } - else{ + } else { // flag we don't handle return; } if (isPress) { - [self keyDown: event]; - } - else { - [self keyUp: event]; + [self keyDown:event]; + } else { + [self keyUp:event]; } // Reset the state for the key changes after handling the event From 7819b0e0eb63a23fe0f9e670a972995edd814bd0 Mon Sep 17 00:00:00 2001 From: Greg Lucas Date: Thu, 25 Nov 2021 20:22:27 -0700 Subject: [PATCH 5/5] DOC: Add macosx keypress events to the documentation --- doc/users/explain/event_handling.rst | 45 ++++++++++++++-------------- 1 file changed, 22 insertions(+), 23 deletions(-) diff --git a/doc/users/explain/event_handling.rst b/doc/users/explain/event_handling.rst index 239c32a194bb..909e9f4b9ff8 100644 --- a/doc/users/explain/event_handling.rst +++ b/doc/users/explain/event_handling.rst @@ -82,29 +82,28 @@ Event name Class Description you may encounter inconsistencies between the different user interface toolkits that Matplotlib works with. This is due to inconsistencies/limitations of the user interface toolkit. The following table shows some basic examples of - what you may expect to receive as key(s) from the different user interface toolkits, - where a comma separates different keys: - - ============== ============================= ============================== ============================== ============================== ============================== - Key(s) Pressed WxPython Qt WebAgg Gtk Tkinter - ============== ============================= ============================== ============================== ============================== ============================== - Shift+2 shift, shift+2 shift, " shift, " shift, " shift, " - Shift+F1 shift, shift+f1 shift, shift+f1 shift, shift+f1 shift, shift+f1 shift, shift+f1 - Shift shift shift shift shift shift - Control control control control control control - Alt alt alt alt alt alt - AltGr Nothing Nothing alt iso_level3_shift iso_level3_shift - CapsLock caps_lock caps_lock caps_lock caps_lock caps_lock - A a a A A A - a a a a a a - Shift+a shift, A shift, A shift, A shift, A shift, A - Shift+A shift, A shift, A shift, a shift, a shift, a - Ctrl+Shift+Alt control, ctrl+shift, ctrl+alt control, ctrl+shift, ctrl+meta control, ctrl+shift, ctrl+meta control, ctrl+shift, ctrl+meta control, ctrl+shift, ctrl+meta - Ctrl+Shift+a control, ctrl+shift, ctrl+A control, ctrl+shift, ctrl+A control, ctrl+shift, ctrl+A control, ctrl+shift, ctrl+A control, ctrl+shift, ctrl+a - Ctrl+Shift+A control, ctrl+shift, ctrl+A control, ctrl+shift, ctrl+A control, ctrl+shift, ctrl+a control, ctrl+shift, ctrl+a control, ctrl+shift, ctrl+a - F1 f1 f1 f1 f1 f1 - Ctrl+F1 control, ctrl+f1 control, ctrl+f1 control, ctrl+f1 control, ctrl+f1 control, ctrl+f1 - ============== ============================= ============================== ============================== ============================== ============================== + what you may expect to receive as key(s) (using a QWERTY keyboard layout) + from the different user interface toolkits, where a comma separates different keys: + + ================ ============================= ============================== ============================== ============================== ============================== =================================== + Key(s) Pressed WxPython Qt WebAgg Gtk Tkinter macosx + ================ ============================= ============================== ============================== ============================== ============================== =================================== + Shift+2 shift, shift+2 shift, @ shift, @ shift, @ shift, @ shift, @ + Shift+F1 shift, shift+f1 shift, shift+f1 shift, shift+f1 shift, shift+f1 shift, shift+f1 shift, shift+f1 + Shift shift shift shift shift shift shift + Control control control control control control control + Alt alt alt alt alt alt alt + AltGr Nothing Nothing alt iso_level3_shift iso_level3_shift + CapsLock caps_lock caps_lock caps_lock caps_lock caps_lock caps_lock + CapsLock+a caps_lock, a caps_lock, a caps_lock, A caps_lock, A caps_lock, A caps_lock, a + a a a a a a a + Shift+a shift, A shift, A shift, A shift, A shift, A shift, A + CapsLock+Shift+a caps_lock, shift, A caps_lock, shift, A caps_lock, shift, a caps_lock, shift, a caps_lock, shift, a caps_lock, shift, A + Ctrl+Shift+Alt control, ctrl+shift, ctrl+alt control, ctrl+shift, ctrl+meta control, ctrl+shift, ctrl+meta control, ctrl+shift, ctrl+meta control, ctrl+shift, ctrl+meta control, ctrl+shift, ctrl+alt+shift + Ctrl+Shift+a control, ctrl+shift, ctrl+A control, ctrl+shift, ctrl+A control, ctrl+shift, ctrl+A control, ctrl+shift, ctrl+A control, ctrl+shift, ctrl+a control, ctrl+shift, ctrl+A + F1 f1 f1 f1 f1 f1 f1 + Ctrl+F1 control, ctrl+f1 control, ctrl+f1 control, ctrl+f1 control, ctrl+f1 control, ctrl+f1 control, Nothing + ================ ============================= ============================== ============================== ============================== ============================== =================================== Matplotlib attaches some keypress callbacks by default for interactivity; they are documented in the :ref:`key-event-handling` section.