From 1d0fcb50f591bedd3646c0169036686acae552f1 Mon Sep 17 00:00:00 2001 From: Antony Lee Date: Fri, 22 Mar 2019 15:53:33 +0100 Subject: [PATCH] More accurate handling of unicode/numpad input in gtk3 backends. See changelog. --- .../next_api_changes/behavior/17791-AL.rst | 13 ++++ lib/matplotlib/backends/backend_gtk3.py | 71 +++---------------- lib/matplotlib/tests/test_backend_gtk3.py | 51 +++++++++++++ lib/matplotlib/tests/test_backend_qt.py | 4 +- 4 files changed, 77 insertions(+), 62 deletions(-) create mode 100644 doc/api/next_api_changes/behavior/17791-AL.rst create mode 100644 lib/matplotlib/tests/test_backend_gtk3.py diff --git a/doc/api/next_api_changes/behavior/17791-AL.rst b/doc/api/next_api_changes/behavior/17791-AL.rst new file mode 100644 index 000000000000..4c2142ea8de6 --- /dev/null +++ b/doc/api/next_api_changes/behavior/17791-AL.rst @@ -0,0 +1,13 @@ +GTK key name changes +~~~~~~~~~~~~~~~~~~~~ + +The handling of non-ASCII keypresses (as reported in the KeyEvent passed to +``key_press_event``-handlers) in the GTK backends now correctly reports Unicode +characters (e.g., €), and respects NumLock on the numpad. + +The following key names have changed; the new names are consistent with those +reported by the Qt backends: + +- The "Break/Pause" key (keysym 0xff13) is now reported as "pause" instead of + "break" (this is also consistent with the X key name). +- The numpad "delete" key is now reported as "delete" instead of "dec". diff --git a/lib/matplotlib/backends/backend_gtk3.py b/lib/matplotlib/backends/backend_gtk3.py index ae445d19ade1..f03ae0b1c0d1 100644 --- a/lib/matplotlib/backends/backend_gtk3.py +++ b/lib/matplotlib/backends/backend_gtk3.py @@ -88,58 +88,6 @@ def _on_timer(self): class FigureCanvasGTK3(Gtk.DrawingArea, FigureCanvasBase): required_interactive_framework = "gtk3" _timer_cls = TimerGTK3 - - keyvald = {65507: 'control', - 65505: 'shift', - 65513: 'alt', - 65508: 'control', - 65506: 'shift', - 65514: 'alt', - 65361: 'left', - 65362: 'up', - 65363: 'right', - 65364: 'down', - 65307: 'escape', - 65470: 'f1', - 65471: 'f2', - 65472: 'f3', - 65473: 'f4', - 65474: 'f5', - 65475: 'f6', - 65476: 'f7', - 65477: 'f8', - 65478: 'f9', - 65479: 'f10', - 65480: 'f11', - 65481: 'f12', - 65300: 'scroll_lock', - 65299: 'break', - 65288: 'backspace', - 65293: 'enter', - 65379: 'insert', - 65535: 'delete', - 65360: 'home', - 65367: 'end', - 65365: 'pageup', - 65366: 'pagedown', - 65438: '0', - 65436: '1', - 65433: '2', - 65435: '3', - 65430: '4', - 65437: '5', - 65432: '6', - 65429: '7', - 65431: '8', - 65434: '9', - 65451: '+', - 65453: '-', - 65450: '*', - 65455: '/', - 65439: 'dec', - 65421: 'enter', - } - # Setting this as a static constant prevents # this resulting expression from leaking event_mask = (Gdk.EventMask.BUTTON_PRESS_MASK @@ -259,13 +207,17 @@ def size_allocate(self, widget, allocation): self.draw_idle() def _get_key(self, event): - if event.keyval in self.keyvald: - key = self.keyvald[event.keyval] - elif event.keyval < 256: - key = chr(event.keyval) - else: - key = None - + key = chr(Gdk.keyval_to_unicode(event.keyval)) + if not key.isprintable(): + key = Gdk.keyval_name(event.keyval).lower() + if key.startswith("kp_"): # keypad_x (including kp_enter). + key = key[3:] + if key.startswith("page_"): # page_{up,down} + key = key.replace("page_", "page") + if key.endswith(("_l", "_r")): # alt_l, ctrl_l, shift_l. + key = key[:-2] + if key == "enter": + key = "return" modifiers = [ (Gdk.ModifierType.MOD4_MASK, 'super'), (Gdk.ModifierType.MOD1_MASK, 'alt'), @@ -274,7 +226,6 @@ def _get_key(self, event): for key_mask, prefix in modifiers: if event.state & key_mask: key = '{0}+{1}'.format(prefix, key) - return key def configure_event(self, widget, event): diff --git a/lib/matplotlib/tests/test_backend_gtk3.py b/lib/matplotlib/tests/test_backend_gtk3.py new file mode 100644 index 000000000000..5442930d117f --- /dev/null +++ b/lib/matplotlib/tests/test_backend_gtk3.py @@ -0,0 +1,51 @@ +from matplotlib import pyplot as plt + +import pytest + + +pytest.importorskip("matplotlib.backends.backend_gtk3agg") + + +@pytest.mark.backend("gtk3agg") +def test_correct_key(): + pytest.xfail("test_widget_send_event is not triggering key_press_event") + + from gi.repository import Gdk, Gtk + fig = plt.figure() + buf = [] + + def send(event): + for key, mod in [ + (Gdk.KEY_a, Gdk.ModifierType.SHIFT_MASK), + (Gdk.KEY_a, 0), + (Gdk.KEY_a, Gdk.ModifierType.CONTROL_MASK), + (Gdk.KEY_agrave, 0), + (Gdk.KEY_Control_L, Gdk.ModifierType.MOD1_MASK), + (Gdk.KEY_Alt_L, Gdk.ModifierType.CONTROL_MASK), + (Gdk.KEY_agrave, + Gdk.ModifierType.CONTROL_MASK + | Gdk.ModifierType.MOD1_MASK + | Gdk.ModifierType.MOD4_MASK), + (0xfd16, 0), # KEY_3270_Play. + (Gdk.KEY_BackSpace, 0), + (Gdk.KEY_BackSpace, Gdk.ModifierType.CONTROL_MASK), + ]: + # This is not actually really the right API: it depends on the + # actual keymap (e.g. on Azerty, shift+agrave -> 0). + Gtk.test_widget_send_key(fig.canvas, key, mod) + + def receive(event): + buf.append(event.key) + if buf == [ + "A", "a", "ctrl+a", + "\N{LATIN SMALL LETTER A WITH GRAVE}", + "alt+control", "ctrl+alt", + "ctrl+alt+super+\N{LATIN SMALL LETTER A WITH GRAVE}", + # (No entry for KEY_3270_Play.) + "backspace", "ctrl+backspace", + ]: + plt.close(fig) + + fig.canvas.mpl_connect("draw_event", send) + fig.canvas.mpl_connect("key_press_event", receive) + plt.show() diff --git a/lib/matplotlib/tests/test_backend_qt.py b/lib/matplotlib/tests/test_backend_qt.py index 3aee9331ce45..ab90da74d51e 100644 --- a/lib/matplotlib/tests/test_backend_qt.py +++ b/lib/matplotlib/tests/test_backend_qt.py @@ -110,9 +110,9 @@ def CustomHandler(signum, frame): ('Key_Alt', ['ControlModifier'], 'ctrl+alt'), ('Key_Aacute', ['ControlModifier', 'AltModifier', 'MetaModifier'], 'ctrl+alt+super+\N{LATIN SMALL LETTER A WITH ACUTE}'), + ('Key_Play', [], None), ('Key_Backspace', [], 'backspace'), ('Key_Backspace', ['ControlModifier'], 'ctrl+backspace'), - ('Key_Play', [], None), ], ids=[ 'shift', @@ -123,9 +123,9 @@ def CustomHandler(signum, frame): 'alt_control', 'control_alt', 'modifier_order', + 'non_unicode_key', 'backspace', 'backspace_mod', - 'non_unicode_key', ] ) @pytest.mark.parametrize('backend', [