-
-
Notifications
You must be signed in to change notification settings - Fork 7.9k
Qt4 keys #2273
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Qt4 keys #2273
Changes from all commits
26e8b9f
62f564e
db86c40
1226094
b454f32
b80a8e7
41fdfae
b22193b
16ed6e8
60407dc
bf4e43d
32ddf1e
c9721fe
5cfbeb4
6770646
2bea38e
8f68252
ec40639
989ce39
2359f09
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,22 +2,13 @@ | |
|
||
import six | ||
|
||
import math # might not ever be used | ||
import os | ||
import re | ||
import signal | ||
import sys | ||
|
||
import matplotlib | ||
|
||
####### might not ever be used | ||
from matplotlib import verbose | ||
from matplotlib.cbook import onetrue | ||
from matplotlib.backend_bases import GraphicsContextBase | ||
from matplotlib.backend_bases import RendererBase | ||
from matplotlib.backend_bases import IdleEvent | ||
from matplotlib.mathtext import MathTextParser | ||
####### | ||
from matplotlib.cbook import is_string_like | ||
from matplotlib.backend_bases import FigureManagerBase | ||
from matplotlib.backend_bases import FigureCanvasBase | ||
|
@@ -41,6 +32,64 @@ | |
|
||
backend_version = __version__ | ||
|
||
# SPECIAL_KEYS are keys that do *not* return their unicode name | ||
# instead they have manually specified names | ||
SPECIAL_KEYS = {QtCore.Qt.Key_Control: 'control', | ||
QtCore.Qt.Key_Shift: 'shift', | ||
QtCore.Qt.Key_Alt: 'alt', | ||
QtCore.Qt.Key_Meta: 'super', | ||
QtCore.Qt.Key_Return: 'enter', | ||
QtCore.Qt.Key_Left: 'left', | ||
QtCore.Qt.Key_Up: 'up', | ||
QtCore.Qt.Key_Right: 'right', | ||
QtCore.Qt.Key_Down: 'down', | ||
QtCore.Qt.Key_Escape: 'escape', | ||
QtCore.Qt.Key_F1: 'f1', | ||
QtCore.Qt.Key_F2: 'f2', | ||
QtCore.Qt.Key_F3: 'f3', | ||
QtCore.Qt.Key_F4: 'f4', | ||
QtCore.Qt.Key_F5: 'f5', | ||
QtCore.Qt.Key_F6: 'f6', | ||
QtCore.Qt.Key_F7: 'f7', | ||
QtCore.Qt.Key_F8: 'f8', | ||
QtCore.Qt.Key_F9: 'f9', | ||
QtCore.Qt.Key_F10: 'f10', | ||
QtCore.Qt.Key_F11: 'f11', | ||
QtCore.Qt.Key_F12: 'f12', | ||
QtCore.Qt.Key_Home: 'home', | ||
QtCore.Qt.Key_End: 'end', | ||
QtCore.Qt.Key_PageUp: 'pageup', | ||
QtCore.Qt.Key_PageDown: 'pagedown', | ||
QtCore.Qt.Key_Tab: 'tab', | ||
QtCore.Qt.Key_Backspace: 'backspace', | ||
QtCore.Qt.Key_Enter: 'enter', | ||
QtCore.Qt.Key_Insert: 'insert', | ||
QtCore.Qt.Key_Delete: 'delete', | ||
QtCore.Qt.Key_Pause: 'pause', | ||
QtCore.Qt.Key_SysReq: 'sysreq', | ||
QtCore.Qt.Key_Clear: 'clear', } | ||
|
||
# define which modifier keys are collected on keyboard events. | ||
# elements are (mpl names, Modifier Flag, Qt Key) tuples | ||
SUPER = 0 | ||
ALT = 1 | ||
CTRL = 2 | ||
SHIFT = 3 | ||
MODIFIER_KEYS = [('super', QtCore.Qt.MetaModifier, QtCore.Qt.Key_Meta), | ||
('alt', QtCore.Qt.AltModifier, QtCore.Qt.Key_Alt), | ||
('ctrl', QtCore.Qt.ControlModifier, QtCore.Qt.Key_Control), | ||
('shift', QtCore.Qt.ShiftModifier, QtCore.Qt.Key_Shift), | ||
] | ||
|
||
if sys.platform == 'darwin': | ||
# in OSX, the control and super (aka cmd/apple) keys are switched, so | ||
# switch them back. | ||
SPECIAL_KEYS.update({QtCore.Qt.Key_Control: 'super', # cmd/apple key | ||
QtCore.Qt.Key_Meta: 'control', | ||
}) | ||
MODIFIER_KEYS[0] = ('super', QtCore.Qt.ControlModifier, QtCore.Qt.Key_Control) | ||
MODIFIER_KEYS[2] = ('ctrl', QtCore.Qt.MetaModifier, QtCore.Qt.Key_Meta) | ||
|
||
|
||
def fn_name(): | ||
return sys._getframe(1).f_code.co_name | ||
|
@@ -160,63 +209,6 @@ def _timer_stop(self): | |
|
||
|
||
class FigureCanvasQT(QtGui.QWidget, FigureCanvasBase): | ||
keyvald = {QtCore.Qt.Key_Control: 'control', | ||
QtCore.Qt.Key_Shift: 'shift', | ||
QtCore.Qt.Key_Alt: 'alt', | ||
QtCore.Qt.Key_Meta: 'super', | ||
QtCore.Qt.Key_Return: 'enter', | ||
QtCore.Qt.Key_Left: 'left', | ||
QtCore.Qt.Key_Up: 'up', | ||
QtCore.Qt.Key_Right: 'right', | ||
QtCore.Qt.Key_Down: 'down', | ||
QtCore.Qt.Key_Escape: 'escape', | ||
QtCore.Qt.Key_F1: 'f1', | ||
QtCore.Qt.Key_F2: 'f2', | ||
QtCore.Qt.Key_F3: 'f3', | ||
QtCore.Qt.Key_F4: 'f4', | ||
QtCore.Qt.Key_F5: 'f5', | ||
QtCore.Qt.Key_F6: 'f6', | ||
QtCore.Qt.Key_F7: 'f7', | ||
QtCore.Qt.Key_F8: 'f8', | ||
QtCore.Qt.Key_F9: 'f9', | ||
QtCore.Qt.Key_F10: 'f10', | ||
QtCore.Qt.Key_F11: 'f11', | ||
QtCore.Qt.Key_F12: 'f12', | ||
QtCore.Qt.Key_Home: 'home', | ||
QtCore.Qt.Key_End: 'end', | ||
QtCore.Qt.Key_PageUp: 'pageup', | ||
QtCore.Qt.Key_PageDown: 'pagedown', | ||
} | ||
|
||
# define the modifier keys which are to be collected on keyboard events. | ||
# format is: [(modifier_flag, modifier_name, equivalent_key) | ||
_modifier_keys = [ | ||
(QtCore.Qt.MetaModifier, 'super', QtCore.Qt.Key_Meta), | ||
(QtCore.Qt.AltModifier, 'alt', QtCore.Qt.Key_Alt), | ||
(QtCore.Qt.ControlModifier, 'ctrl', | ||
QtCore.Qt.Key_Control) | ||
] | ||
|
||
_ctrl_modifier = QtCore.Qt.ControlModifier | ||
|
||
if sys.platform == 'darwin': | ||
# in OSX, the control and super (aka cmd/apple) keys are switched, so | ||
# switch them back. | ||
keyvald.update({ | ||
QtCore.Qt.Key_Control: 'super', # cmd/apple key | ||
QtCore.Qt.Key_Meta: 'control', | ||
}) | ||
|
||
_modifier_keys = [ | ||
(QtCore.Qt.ControlModifier, 'super', | ||
QtCore.Qt.Key_Control), | ||
(QtCore.Qt.AltModifier, 'alt', | ||
QtCore.Qt.Key_Alt), | ||
(QtCore.Qt.MetaModifier, 'ctrl', | ||
QtCore.Qt.Key_Meta), | ||
] | ||
|
||
_ctrl_modifier = QtCore.Qt.MetaModifier | ||
|
||
# map Qt button codes to MouseEvent's ones: | ||
buttond = {QtCore.Qt.LeftButton: 1, | ||
|
@@ -344,35 +336,38 @@ def _get_key(self, event): | |
if event.isAutoRepeat(): | ||
return None | ||
|
||
if event.key() < 256: | ||
key = six.text_type(event.text()) | ||
# if the control key is being pressed, we don't get the correct | ||
# characters, so interpret them directly from the event.key(). | ||
# Unfortunately, this means that we cannot handle key's case | ||
# since event.key() is not case sensitive, whereas event.text() is, | ||
# Finally, since it is not possible to get the CapsLock state | ||
# we cannot accurately compute the case of a pressed key when | ||
# ctrl+shift+p is pressed. | ||
if int(event.modifiers()) & self._ctrl_modifier: | ||
# we always get an uppercase character | ||
key = chr(event.key()) | ||
# if shift is not being pressed, lowercase it (as mentioned, | ||
# this does not take into account the CapsLock state) | ||
if not int(event.modifiers()) & QtCore.Qt.ShiftModifier: | ||
key = key.lower() | ||
event_key = event.key() | ||
event_mods = int(event.modifiers()) # actually a bitmask | ||
|
||
else: | ||
key = self.keyvald.get(event.key()) | ||
|
||
if key is not None: | ||
# prepend the ctrl, alt, super keys if appropriate (sorted | ||
# in that order) | ||
for modifier, prefix, Qt_key in self._modifier_keys: | ||
if (event.key() != Qt_key and | ||
int(event.modifiers()) & modifier == modifier): | ||
key = '{0}+{1}'.format(prefix, key) | ||
# get names of the pressed modifier keys | ||
# bit twiddling to pick out modifier keys from event_mods bitmask, | ||
# if event_key is a MODIFIER, it should not be duplicated in mods | ||
mods = [name for name, mod_key, qt_key in MODIFIER_KEYS | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could you throw in a comment here to explain what this list comprehension is for so one can grok it at a glance? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. certainly. |
||
if event_key != qt_key and (event_mods & mod_key) == mod_key] | ||
try: | ||
# for certain keys (enter, left, backspace, etc) use a word for the | ||
# key, rather than unicode | ||
key = SPECIAL_KEYS[event_key] | ||
except KeyError: | ||
# unicode defines code points up to 0x0010ffff | ||
# QT will use Key_Codes larger than that for keyboard keys that are | ||
# are not unicode characters (like multimedia keys) | ||
# skip these | ||
# if you really want them, you should add them to SPECIAL_KEYS | ||
MAX_UNICODE = 0x10ffff | ||
if event_key > MAX_UNICODE: | ||
return None | ||
|
||
key = unichr(event_key) | ||
# qt delivers capitalized letters. fix capitalization | ||
# note that capslock is ignored | ||
if 'shift' in mods: | ||
mods.remove('shift') | ||
else: | ||
key = key.lower() | ||
|
||
return key | ||
mods.reverse() | ||
return u'+'.join(mods + [key]) | ||
|
||
def new_timer(self, *args, **kwargs): | ||
""" | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -9,7 +9,20 @@ | |
import copy | ||
|
||
try: | ||
import matplotlib.backends.qt4_compat | ||
# mock in python 3.3+ | ||
from unittest import mock | ||
except ImportError: | ||
import mock | ||
|
||
try: | ||
from matplotlib.backends.qt4_compat import QtCore | ||
from matplotlib.backends.backend_qt4 import (MODIFIER_KEYS, | ||
SUPER, ALT, CTRL, SHIFT) | ||
|
||
_, ControlModifier, ControlKey = MODIFIER_KEYS[CTRL] | ||
_, AltModifier, AltKey = MODIFIER_KEYS[ALT] | ||
_, SuperModifier, SuperKey = MODIFIER_KEYS[SUPER] | ||
_, ShiftModifier, ShiftKey = MODIFIER_KEYS[SHIFT] | ||
HAS_QT = True | ||
except ImportError: | ||
HAS_QT = False | ||
|
@@ -34,3 +47,113 @@ def test_fig_close(): | |
# assert that we have removed the reference to the FigureManager | ||
# that got added by plt.figure() | ||
assert(init_figs == Gcf.figs) | ||
|
||
|
||
def assert_correct_key(qt_key, qt_mods, answer): | ||
""" | ||
Make a figure | ||
Send a key_press_event event (using non-public, qt4 backend specific api) | ||
Catch the event | ||
Assert sent and caught keys are the same | ||
""" | ||
plt.switch_backend('Qt4Agg') | ||
qt_canvas = plt.figure().canvas | ||
|
||
event = mock.Mock() | ||
event.isAutoRepeat.return_value = False | ||
event.key.return_value = qt_key | ||
event.modifiers.return_value = qt_mods | ||
|
||
def receive(event): | ||
assert event.key == answer | ||
|
||
qt_canvas.mpl_connect('key_press_event', receive) | ||
qt_canvas.keyPressEvent(event) | ||
|
||
|
||
@cleanup | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think the cleanup decorator is only for when we are plotting There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ah, I missed that. Yes, that does count as "plotting". There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Would it be easier to go with a test class that has a setup and teardown so we don't have to do cleanup for each test function here? Don't know if it would make a difference or not. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think this way is more robust. A test class would presumably have a single, shared figure that all the callback test use. I don't fully trust key_press_events cough_macos_cough to actually get fired. In the event that one doesn't fire, the test will eventually time out. I don't know what cascade gets launched if I'm using a class based test with a shared figure and one of the tests times out. That and I like functions : ) |
||
@knownfailureif(not HAS_QT) | ||
def test_shift(): | ||
assert_correct_key(QtCore.Qt.Key_A, | ||
ShiftModifier, | ||
u'A') | ||
|
||
|
||
@cleanup | ||
@knownfailureif(not HAS_QT) | ||
def test_lower(): | ||
assert_correct_key(QtCore.Qt.Key_A, | ||
QtCore.Qt.NoModifier, | ||
u'a') | ||
|
||
|
||
@cleanup | ||
@knownfailureif(not HAS_QT) | ||
def test_control(): | ||
assert_correct_key(QtCore.Qt.Key_A, | ||
ControlModifier, | ||
u'ctrl+a') | ||
|
||
|
||
@cleanup | ||
@knownfailureif(not HAS_QT) | ||
def test_unicode_upper(): | ||
assert_correct_key(QtCore.Qt.Key_Aacute, | ||
ShiftModifier, | ||
unichr(193)) | ||
|
||
|
||
@cleanup | ||
@knownfailureif(not HAS_QT) | ||
def test_unicode_lower(): | ||
assert_correct_key(QtCore.Qt.Key_Aacute, | ||
QtCore.Qt.NoModifier, | ||
unichr(225)) | ||
|
||
|
||
@cleanup | ||
@knownfailureif(not HAS_QT) | ||
def test_alt_control(): | ||
assert_correct_key(ControlKey, | ||
AltModifier, | ||
u'alt+control') | ||
|
||
|
||
@cleanup | ||
@knownfailureif(not HAS_QT) | ||
def test_control_alt(): | ||
assert_correct_key(AltKey, | ||
ControlModifier, | ||
u'ctrl+alt') | ||
|
||
|
||
@cleanup | ||
@knownfailureif(not HAS_QT) | ||
def test_modifier_order(): | ||
assert_correct_key(QtCore.Qt.Key_Aacute, | ||
(ControlModifier | AltModifier | SuperModifier), | ||
u'ctrl+alt+super+' + unichr(225)) | ||
|
||
|
||
@cleanup | ||
@knownfailureif(not HAS_QT) | ||
def test_backspace(): | ||
assert_correct_key(QtCore.Qt.Key_Backspace, | ||
QtCore.Qt.NoModifier, | ||
u'backspace') | ||
|
||
|
||
@cleanup | ||
@knownfailureif(not HAS_QT) | ||
def test_backspace_mod(): | ||
assert_correct_key(QtCore.Qt.Key_Backspace, | ||
ControlModifier, | ||
u'ctrl+backspace') | ||
|
||
|
||
@cleanup | ||
@knownfailureif(not HAS_QT) | ||
def test_non_unicode_key(): | ||
assert_correct_key(QtCore.Qt.Key_Play, | ||
QtCore.Qt.NoModifier, | ||
None) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It claims there is a pep8 issuer around here.