Move key related utils to sequence.py
This commit is contained in:
parent
600919a23a
commit
dcf0d21121
@ -62,6 +62,7 @@ from PyQt5.QtWidgets import QTabWidget, QTabBar
|
|||||||
from qutebrowser.commands import cmdutils
|
from qutebrowser.commands import cmdutils
|
||||||
from qutebrowser.config import configexc
|
from qutebrowser.config import configexc
|
||||||
from qutebrowser.utils import standarddir, utils, qtutils, urlutils
|
from qutebrowser.utils import standarddir, utils, qtutils, urlutils
|
||||||
|
from qutebrowser.keyinput import sequence
|
||||||
|
|
||||||
|
|
||||||
SYSTEM_PROXY = object() # Return value for Proxy type
|
SYSTEM_PROXY = object() # Return value for Proxy type
|
||||||
@ -1654,4 +1655,4 @@ class Key(BaseType):
|
|||||||
#if utils.is_special_key(value):
|
#if utils.is_special_key(value):
|
||||||
# value = '<{}>'.format(utils.normalize_keystr(value[1:-1]))
|
# value = '<{}>'.format(utils.normalize_keystr(value[1:-1]))
|
||||||
#return value
|
#return value
|
||||||
return utils.parse_keystring(value)
|
return sequence.parse_keystring(value)
|
||||||
|
@ -19,9 +19,299 @@
|
|||||||
|
|
||||||
"""Our own QKeySequence-like class and related utilities."""
|
"""Our own QKeySequence-like class and related utilities."""
|
||||||
|
|
||||||
|
import unicodedata
|
||||||
|
import collections
|
||||||
|
|
||||||
|
import attr
|
||||||
|
from PyQt5.QtCore import Qt
|
||||||
from PyQt5.QtGui import QKeySequence
|
from PyQt5.QtGui import QKeySequence
|
||||||
|
|
||||||
from qutebrowser.utils import utils
|
from qutebrowser.utils import utils, debug
|
||||||
|
|
||||||
|
|
||||||
|
def key_to_string(key):
|
||||||
|
"""Convert a Qt::Key member to a meaningful name.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
key: A Qt::Key member.
|
||||||
|
|
||||||
|
Return:
|
||||||
|
A name of the key as a string.
|
||||||
|
"""
|
||||||
|
special_names_str = {
|
||||||
|
# Some keys handled in a weird way by QKeySequence::toString.
|
||||||
|
# See https://bugreports.qt.io/browse/QTBUG-40030
|
||||||
|
# Most are unlikely to be ever needed, but you never know ;)
|
||||||
|
# For dead/combining keys, we return the corresponding non-combining
|
||||||
|
# key, as that's easier to add to the config.
|
||||||
|
'Key_Blue': 'Blue',
|
||||||
|
'Key_Calendar': 'Calendar',
|
||||||
|
'Key_ChannelDown': 'Channel Down',
|
||||||
|
'Key_ChannelUp': 'Channel Up',
|
||||||
|
'Key_ContrastAdjust': 'Contrast Adjust',
|
||||||
|
'Key_Dead_Abovedot': '˙',
|
||||||
|
'Key_Dead_Abovering': '˚',
|
||||||
|
'Key_Dead_Acute': '´',
|
||||||
|
'Key_Dead_Belowdot': 'Belowdot',
|
||||||
|
'Key_Dead_Breve': '˘',
|
||||||
|
'Key_Dead_Caron': 'ˇ',
|
||||||
|
'Key_Dead_Cedilla': '¸',
|
||||||
|
'Key_Dead_Circumflex': '^',
|
||||||
|
'Key_Dead_Diaeresis': '¨',
|
||||||
|
'Key_Dead_Doubleacute': '˝',
|
||||||
|
'Key_Dead_Grave': '`',
|
||||||
|
'Key_Dead_Hook': 'Hook',
|
||||||
|
'Key_Dead_Horn': 'Horn',
|
||||||
|
'Key_Dead_Iota': 'Iota',
|
||||||
|
'Key_Dead_Macron': '¯',
|
||||||
|
'Key_Dead_Ogonek': '˛',
|
||||||
|
'Key_Dead_Semivoiced_Sound': 'Semivoiced Sound',
|
||||||
|
'Key_Dead_Tilde': '~',
|
||||||
|
'Key_Dead_Voiced_Sound': 'Voiced Sound',
|
||||||
|
'Key_Exit': 'Exit',
|
||||||
|
'Key_Green': 'Green',
|
||||||
|
'Key_Guide': 'Guide',
|
||||||
|
'Key_Info': 'Info',
|
||||||
|
'Key_LaunchG': 'LaunchG',
|
||||||
|
'Key_LaunchH': 'LaunchH',
|
||||||
|
'Key_MediaLast': 'MediaLast',
|
||||||
|
'Key_Memo': 'Memo',
|
||||||
|
'Key_MicMute': 'Mic Mute',
|
||||||
|
'Key_Mode_switch': 'Mode switch',
|
||||||
|
'Key_Multi_key': 'Multi key',
|
||||||
|
'Key_PowerDown': 'Power Down',
|
||||||
|
'Key_Red': 'Red',
|
||||||
|
'Key_Settings': 'Settings',
|
||||||
|
'Key_SingleCandidate': 'Single Candidate',
|
||||||
|
'Key_ToDoList': 'Todo List',
|
||||||
|
'Key_TouchpadOff': 'Touchpad Off',
|
||||||
|
'Key_TouchpadOn': 'Touchpad On',
|
||||||
|
'Key_TouchpadToggle': 'Touchpad toggle',
|
||||||
|
'Key_Yellow': 'Yellow',
|
||||||
|
'Key_Alt': 'Alt',
|
||||||
|
'Key_AltGr': 'AltGr',
|
||||||
|
'Key_Control': 'Control',
|
||||||
|
'Key_Direction_L': 'Direction L',
|
||||||
|
'Key_Direction_R': 'Direction R',
|
||||||
|
'Key_Hyper_L': 'Hyper L',
|
||||||
|
'Key_Hyper_R': 'Hyper R',
|
||||||
|
'Key_Meta': 'Meta',
|
||||||
|
'Key_Shift': 'Shift',
|
||||||
|
'Key_Super_L': 'Super L',
|
||||||
|
'Key_Super_R': 'Super R',
|
||||||
|
'Key_unknown': 'Unknown',
|
||||||
|
}
|
||||||
|
# We now build our real special_names dict from the string mapping above.
|
||||||
|
# The reason we don't do this directly is that certain Qt versions don't
|
||||||
|
# have all the keys, so we want to ignore AttributeErrors.
|
||||||
|
special_names = {}
|
||||||
|
for k, v in special_names_str.items():
|
||||||
|
try:
|
||||||
|
special_names[getattr(Qt, k)] = v
|
||||||
|
except AttributeError:
|
||||||
|
pass
|
||||||
|
# Now we check if the key is any special one - if not, we use
|
||||||
|
# QKeySequence::toString.
|
||||||
|
try:
|
||||||
|
return special_names[key]
|
||||||
|
except KeyError:
|
||||||
|
name = QKeySequence(key).toString()
|
||||||
|
morphings = {
|
||||||
|
'Backtab': 'Tab',
|
||||||
|
'Esc': 'Escape',
|
||||||
|
}
|
||||||
|
if name in morphings:
|
||||||
|
return morphings[name]
|
||||||
|
else:
|
||||||
|
return name
|
||||||
|
|
||||||
|
|
||||||
|
def keyevent_to_string(e):
|
||||||
|
"""Convert a QKeyEvent to a meaningful name.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
e: A QKeyEvent.
|
||||||
|
|
||||||
|
Return:
|
||||||
|
A name of the key (combination) as a string or
|
||||||
|
None if only modifiers are pressed..
|
||||||
|
"""
|
||||||
|
if utils.is_mac:
|
||||||
|
# Qt swaps Ctrl/Meta on macOS, so we switch it back here so the user
|
||||||
|
# can use it in the config as expected. See:
|
||||||
|
# https://github.com/qutebrowser/qutebrowser/issues/110
|
||||||
|
# http://doc.qt.io/qt-5.4/osx-issues.html#special-keys
|
||||||
|
modmask2str = collections.OrderedDict([
|
||||||
|
(Qt.MetaModifier, 'Ctrl'),
|
||||||
|
(Qt.AltModifier, 'Alt'),
|
||||||
|
(Qt.ControlModifier, 'Meta'),
|
||||||
|
(Qt.ShiftModifier, 'Shift'),
|
||||||
|
])
|
||||||
|
else:
|
||||||
|
modmask2str = collections.OrderedDict([
|
||||||
|
(Qt.ControlModifier, 'Ctrl'),
|
||||||
|
(Qt.AltModifier, 'Alt'),
|
||||||
|
(Qt.MetaModifier, 'Meta'),
|
||||||
|
(Qt.ShiftModifier, 'Shift'),
|
||||||
|
])
|
||||||
|
modifiers = (Qt.Key_Control, Qt.Key_Alt, Qt.Key_Shift, Qt.Key_Meta,
|
||||||
|
Qt.Key_AltGr, Qt.Key_Super_L, Qt.Key_Super_R, Qt.Key_Hyper_L,
|
||||||
|
Qt.Key_Hyper_R, Qt.Key_Direction_L, Qt.Key_Direction_R)
|
||||||
|
if e.key() in modifiers:
|
||||||
|
# Only modifier pressed
|
||||||
|
return None
|
||||||
|
mod = e.modifiers()
|
||||||
|
parts = []
|
||||||
|
|
||||||
|
for (mask, s) in modmask2str.items():
|
||||||
|
if mod & mask and s not in parts:
|
||||||
|
parts.append(s)
|
||||||
|
|
||||||
|
key_string = key_to_string(e.key())
|
||||||
|
if len(key_string) == 1:
|
||||||
|
category = unicodedata.category(key_string)
|
||||||
|
is_control_char = (category == 'Cc')
|
||||||
|
else:
|
||||||
|
is_control_char = False
|
||||||
|
|
||||||
|
if e.modifiers() == Qt.ShiftModifier and not is_control_char:
|
||||||
|
parts = []
|
||||||
|
|
||||||
|
parts.append(key_string)
|
||||||
|
return normalize_keystr('+'.join(parts))
|
||||||
|
|
||||||
|
|
||||||
|
@attr.s(repr=False)
|
||||||
|
class KeyInfo:
|
||||||
|
|
||||||
|
"""Stores information about a key, like used in a QKeyEvent.
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
key: Qt::Key
|
||||||
|
modifiers: Qt::KeyboardModifiers
|
||||||
|
text: str
|
||||||
|
"""
|
||||||
|
|
||||||
|
key = attr.ib()
|
||||||
|
modifiers = attr.ib()
|
||||||
|
text = attr.ib()
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
if self.modifiers is None:
|
||||||
|
modifiers = None
|
||||||
|
else:
|
||||||
|
#modifiers = qflags_key(Qt, self.modifiers)
|
||||||
|
modifiers = hex(int(self.modifiers))
|
||||||
|
return utils.get_repr(self, constructor=True,
|
||||||
|
key=debug.qenum_key(Qt, self.key),
|
||||||
|
modifiers=modifiers, text=self.text)
|
||||||
|
|
||||||
|
|
||||||
|
class KeyParseError(Exception):
|
||||||
|
|
||||||
|
"""Raised by _parse_single_key/parse_keystring on parse errors."""
|
||||||
|
|
||||||
|
def __init__(self, keystr, error):
|
||||||
|
super().__init__("Could not parse {!r}: {}".format(keystr, error))
|
||||||
|
|
||||||
|
|
||||||
|
def is_special_key(keystr):
|
||||||
|
"""True if keystr is a 'special' keystring (e.g. <ctrl-x> or <space>)."""
|
||||||
|
return keystr.startswith('<') and keystr.endswith('>')
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_single_key(keystr):
|
||||||
|
"""Convert a single key string to a (Qt.Key, Qt.Modifiers, text) tuple."""
|
||||||
|
# FIXME remove
|
||||||
|
|
||||||
|
if is_special_key(keystr):
|
||||||
|
# Special key
|
||||||
|
keystr = keystr[1:-1]
|
||||||
|
elif len(keystr) == 1:
|
||||||
|
# vim-like key
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
raise KeyParseError(keystr, "Expecting either a single key or a "
|
||||||
|
"<Ctrl-x> like keybinding.")
|
||||||
|
|
||||||
|
seq = KeySequence(normalize_keystr(keystr), QKeySequence.PortableText)
|
||||||
|
if len(seq) != 1:
|
||||||
|
raise KeyParseError(keystr, "Got {} keys instead of 1.".format(
|
||||||
|
len(seq)))
|
||||||
|
result = seq[0]
|
||||||
|
|
||||||
|
if result == Qt.Key_unknown:
|
||||||
|
raise KeyParseError(keystr, "Got unknown key.")
|
||||||
|
|
||||||
|
modifier_mask = int(Qt.ShiftModifier | Qt.ControlModifier |
|
||||||
|
Qt.AltModifier | Qt.MetaModifier | Qt.KeypadModifier |
|
||||||
|
Qt.GroupSwitchModifier)
|
||||||
|
assert Qt.Key_unknown & ~modifier_mask == Qt.Key_unknown
|
||||||
|
|
||||||
|
modifiers = result & modifier_mask
|
||||||
|
key = result & ~modifier_mask
|
||||||
|
|
||||||
|
if len(keystr) == 1 and keystr.isupper():
|
||||||
|
modifiers |= Qt.ShiftModifier
|
||||||
|
|
||||||
|
assert key != 0, key
|
||||||
|
key = Qt.Key(key)
|
||||||
|
modifiers = Qt.KeyboardModifiers(modifiers)
|
||||||
|
|
||||||
|
# Let's hope this is accurate...
|
||||||
|
if len(keystr) == 1 and not modifiers:
|
||||||
|
text = keystr
|
||||||
|
elif len(keystr) == 1 and modifiers == Qt.ShiftModifier:
|
||||||
|
text = keystr.upper()
|
||||||
|
else:
|
||||||
|
text = ''
|
||||||
|
|
||||||
|
return KeyInfo(key, modifiers, text)
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_keystring(keystr):
|
||||||
|
key = ''
|
||||||
|
special = False
|
||||||
|
for c in keystr:
|
||||||
|
if c == '>':
|
||||||
|
yield normalize_keystr(key)
|
||||||
|
key = ''
|
||||||
|
special = False
|
||||||
|
elif c == '<':
|
||||||
|
special = True
|
||||||
|
elif special:
|
||||||
|
key += c
|
||||||
|
else:
|
||||||
|
yield 'Shift+' + c if c.isupper() else c
|
||||||
|
|
||||||
|
|
||||||
|
def parse_keystring(keystr):
|
||||||
|
"""Parse a keystring like <Ctrl-x> or xyz and return a KeyInfo list."""
|
||||||
|
s = ', '.join(_parse_keystring(keystr))
|
||||||
|
return KeySequence(s)
|
||||||
|
|
||||||
|
|
||||||
|
def normalize_keystr(keystr):
|
||||||
|
"""Normalize a keystring like Ctrl-Q to a keystring like Ctrl+Q.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
keystr: The key combination as a string.
|
||||||
|
|
||||||
|
Return:
|
||||||
|
The normalized keystring.
|
||||||
|
"""
|
||||||
|
keystr = keystr.lower()
|
||||||
|
replacements = (
|
||||||
|
('control', 'ctrl'),
|
||||||
|
('windows', 'meta'),
|
||||||
|
('mod1', 'alt'),
|
||||||
|
('mod4', 'meta'),
|
||||||
|
)
|
||||||
|
for (orig, repl) in replacements:
|
||||||
|
keystr = keystr.replace(orig, repl)
|
||||||
|
for mod in ['ctrl', 'meta', 'alt', 'shift']:
|
||||||
|
keystr = keystr.replace(mod + '-', mod + '+')
|
||||||
|
return keystr
|
||||||
|
|
||||||
|
|
||||||
class KeySequence:
|
class KeySequence:
|
||||||
|
@ -287,291 +287,6 @@ def format_size(size, base=1024, suffix=''):
|
|||||||
return '{:.02f}{}{}'.format(size, prefixes[-1], suffix)
|
return '{:.02f}{}{}'.format(size, prefixes[-1], suffix)
|
||||||
|
|
||||||
|
|
||||||
def key_to_string(key):
|
|
||||||
"""Convert a Qt::Key member to a meaningful name.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
key: A Qt::Key member.
|
|
||||||
|
|
||||||
Return:
|
|
||||||
A name of the key as a string.
|
|
||||||
"""
|
|
||||||
special_names_str = {
|
|
||||||
# Some keys handled in a weird way by QKeySequence::toString.
|
|
||||||
# See https://bugreports.qt.io/browse/QTBUG-40030
|
|
||||||
# Most are unlikely to be ever needed, but you never know ;)
|
|
||||||
# For dead/combining keys, we return the corresponding non-combining
|
|
||||||
# key, as that's easier to add to the config.
|
|
||||||
'Key_Blue': 'Blue',
|
|
||||||
'Key_Calendar': 'Calendar',
|
|
||||||
'Key_ChannelDown': 'Channel Down',
|
|
||||||
'Key_ChannelUp': 'Channel Up',
|
|
||||||
'Key_ContrastAdjust': 'Contrast Adjust',
|
|
||||||
'Key_Dead_Abovedot': '˙',
|
|
||||||
'Key_Dead_Abovering': '˚',
|
|
||||||
'Key_Dead_Acute': '´',
|
|
||||||
'Key_Dead_Belowdot': 'Belowdot',
|
|
||||||
'Key_Dead_Breve': '˘',
|
|
||||||
'Key_Dead_Caron': 'ˇ',
|
|
||||||
'Key_Dead_Cedilla': '¸',
|
|
||||||
'Key_Dead_Circumflex': '^',
|
|
||||||
'Key_Dead_Diaeresis': '¨',
|
|
||||||
'Key_Dead_Doubleacute': '˝',
|
|
||||||
'Key_Dead_Grave': '`',
|
|
||||||
'Key_Dead_Hook': 'Hook',
|
|
||||||
'Key_Dead_Horn': 'Horn',
|
|
||||||
'Key_Dead_Iota': 'Iota',
|
|
||||||
'Key_Dead_Macron': '¯',
|
|
||||||
'Key_Dead_Ogonek': '˛',
|
|
||||||
'Key_Dead_Semivoiced_Sound': 'Semivoiced Sound',
|
|
||||||
'Key_Dead_Tilde': '~',
|
|
||||||
'Key_Dead_Voiced_Sound': 'Voiced Sound',
|
|
||||||
'Key_Exit': 'Exit',
|
|
||||||
'Key_Green': 'Green',
|
|
||||||
'Key_Guide': 'Guide',
|
|
||||||
'Key_Info': 'Info',
|
|
||||||
'Key_LaunchG': 'LaunchG',
|
|
||||||
'Key_LaunchH': 'LaunchH',
|
|
||||||
'Key_MediaLast': 'MediaLast',
|
|
||||||
'Key_Memo': 'Memo',
|
|
||||||
'Key_MicMute': 'Mic Mute',
|
|
||||||
'Key_Mode_switch': 'Mode switch',
|
|
||||||
'Key_Multi_key': 'Multi key',
|
|
||||||
'Key_PowerDown': 'Power Down',
|
|
||||||
'Key_Red': 'Red',
|
|
||||||
'Key_Settings': 'Settings',
|
|
||||||
'Key_SingleCandidate': 'Single Candidate',
|
|
||||||
'Key_ToDoList': 'Todo List',
|
|
||||||
'Key_TouchpadOff': 'Touchpad Off',
|
|
||||||
'Key_TouchpadOn': 'Touchpad On',
|
|
||||||
'Key_TouchpadToggle': 'Touchpad toggle',
|
|
||||||
'Key_Yellow': 'Yellow',
|
|
||||||
'Key_Alt': 'Alt',
|
|
||||||
'Key_AltGr': 'AltGr',
|
|
||||||
'Key_Control': 'Control',
|
|
||||||
'Key_Direction_L': 'Direction L',
|
|
||||||
'Key_Direction_R': 'Direction R',
|
|
||||||
'Key_Hyper_L': 'Hyper L',
|
|
||||||
'Key_Hyper_R': 'Hyper R',
|
|
||||||
'Key_Meta': 'Meta',
|
|
||||||
'Key_Shift': 'Shift',
|
|
||||||
'Key_Super_L': 'Super L',
|
|
||||||
'Key_Super_R': 'Super R',
|
|
||||||
'Key_unknown': 'Unknown',
|
|
||||||
}
|
|
||||||
# We now build our real special_names dict from the string mapping above.
|
|
||||||
# The reason we don't do this directly is that certain Qt versions don't
|
|
||||||
# have all the keys, so we want to ignore AttributeErrors.
|
|
||||||
special_names = {}
|
|
||||||
for k, v in special_names_str.items():
|
|
||||||
try:
|
|
||||||
special_names[getattr(Qt, k)] = v
|
|
||||||
except AttributeError:
|
|
||||||
pass
|
|
||||||
# Now we check if the key is any special one - if not, we use
|
|
||||||
# QKeySequence::toString.
|
|
||||||
try:
|
|
||||||
return special_names[key]
|
|
||||||
except KeyError:
|
|
||||||
name = QKeySequence(key).toString()
|
|
||||||
morphings = {
|
|
||||||
'Backtab': 'Tab',
|
|
||||||
'Esc': 'Escape',
|
|
||||||
}
|
|
||||||
if name in morphings:
|
|
||||||
return morphings[name]
|
|
||||||
else:
|
|
||||||
return name
|
|
||||||
|
|
||||||
|
|
||||||
def keyevent_to_string(e):
|
|
||||||
"""Convert a QKeyEvent to a meaningful name.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
e: A QKeyEvent.
|
|
||||||
|
|
||||||
Return:
|
|
||||||
A name of the key (combination) as a string or
|
|
||||||
None if only modifiers are pressed..
|
|
||||||
"""
|
|
||||||
if is_mac:
|
|
||||||
# Qt swaps Ctrl/Meta on macOS, so we switch it back here so the user
|
|
||||||
# can use it in the config as expected. See:
|
|
||||||
# https://github.com/qutebrowser/qutebrowser/issues/110
|
|
||||||
# http://doc.qt.io/qt-5.4/osx-issues.html#special-keys
|
|
||||||
modmask2str = collections.OrderedDict([
|
|
||||||
(Qt.MetaModifier, 'Ctrl'),
|
|
||||||
(Qt.AltModifier, 'Alt'),
|
|
||||||
(Qt.ControlModifier, 'Meta'),
|
|
||||||
(Qt.ShiftModifier, 'Shift'),
|
|
||||||
])
|
|
||||||
else:
|
|
||||||
modmask2str = collections.OrderedDict([
|
|
||||||
(Qt.ControlModifier, 'Ctrl'),
|
|
||||||
(Qt.AltModifier, 'Alt'),
|
|
||||||
(Qt.MetaModifier, 'Meta'),
|
|
||||||
(Qt.ShiftModifier, 'Shift'),
|
|
||||||
])
|
|
||||||
modifiers = (Qt.Key_Control, Qt.Key_Alt, Qt.Key_Shift, Qt.Key_Meta,
|
|
||||||
Qt.Key_AltGr, Qt.Key_Super_L, Qt.Key_Super_R, Qt.Key_Hyper_L,
|
|
||||||
Qt.Key_Hyper_R, Qt.Key_Direction_L, Qt.Key_Direction_R)
|
|
||||||
if e.key() in modifiers:
|
|
||||||
# Only modifier pressed
|
|
||||||
return None
|
|
||||||
mod = e.modifiers()
|
|
||||||
parts = []
|
|
||||||
|
|
||||||
for (mask, s) in modmask2str.items():
|
|
||||||
if mod & mask and s not in parts:
|
|
||||||
parts.append(s)
|
|
||||||
|
|
||||||
key_string = key_to_string(e.key())
|
|
||||||
if len(key_string) == 1:
|
|
||||||
category = unicodedata.category(key_string)
|
|
||||||
is_control_char = (category == 'Cc')
|
|
||||||
else:
|
|
||||||
is_control_char = False
|
|
||||||
|
|
||||||
if e.modifiers() == Qt.ShiftModifier and not is_control_char:
|
|
||||||
parts = []
|
|
||||||
|
|
||||||
parts.append(key_string)
|
|
||||||
return normalize_keystr('+'.join(parts))
|
|
||||||
|
|
||||||
|
|
||||||
@attr.s(repr=False)
|
|
||||||
class KeyInfo:
|
|
||||||
|
|
||||||
"""Stores information about a key, like used in a QKeyEvent.
|
|
||||||
|
|
||||||
Attributes:
|
|
||||||
key: Qt::Key
|
|
||||||
modifiers: Qt::KeyboardModifiers
|
|
||||||
text: str
|
|
||||||
"""
|
|
||||||
|
|
||||||
key = attr.ib()
|
|
||||||
modifiers = attr.ib()
|
|
||||||
text = attr.ib()
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
if self.modifiers is None:
|
|
||||||
modifiers = None
|
|
||||||
else:
|
|
||||||
#modifiers = qflags_key(Qt, self.modifiers)
|
|
||||||
modifiers = hex(int(self.modifiers))
|
|
||||||
return get_repr(self, constructor=True,
|
|
||||||
key=debug.qenum_key(Qt, self.key),
|
|
||||||
modifiers=modifiers, text=self.text)
|
|
||||||
|
|
||||||
|
|
||||||
class KeyParseError(Exception):
|
|
||||||
|
|
||||||
"""Raised by _parse_single_key/parse_keystring on parse errors."""
|
|
||||||
|
|
||||||
def __init__(self, keystr, error):
|
|
||||||
super().__init__("Could not parse {!r}: {}".format(keystr, error))
|
|
||||||
|
|
||||||
|
|
||||||
def is_special_key(keystr):
|
|
||||||
"""True if keystr is a 'special' keystring (e.g. <ctrl-x> or <space>)."""
|
|
||||||
return keystr.startswith('<') and keystr.endswith('>')
|
|
||||||
|
|
||||||
|
|
||||||
def _parse_single_key(keystr):
|
|
||||||
"""Convert a single key string to a (Qt.Key, Qt.Modifiers, text) tuple."""
|
|
||||||
# FIXME remove
|
|
||||||
|
|
||||||
if is_special_key(keystr):
|
|
||||||
# Special key
|
|
||||||
keystr = keystr[1:-1]
|
|
||||||
elif len(keystr) == 1:
|
|
||||||
# vim-like key
|
|
||||||
pass
|
|
||||||
else:
|
|
||||||
raise KeyParseError(keystr, "Expecting either a single key or a "
|
|
||||||
"<Ctrl-x> like keybinding.")
|
|
||||||
|
|
||||||
seq = sequence.KeySequence(normalize_keystr(keystr), QKeySequence.PortableText)
|
|
||||||
if len(seq) != 1:
|
|
||||||
raise KeyParseError(keystr, "Got {} keys instead of 1.".format(
|
|
||||||
len(seq)))
|
|
||||||
result = seq[0]
|
|
||||||
|
|
||||||
if result == Qt.Key_unknown:
|
|
||||||
raise KeyParseError(keystr, "Got unknown key.")
|
|
||||||
|
|
||||||
modifier_mask = int(Qt.ShiftModifier | Qt.ControlModifier |
|
|
||||||
Qt.AltModifier | Qt.MetaModifier | Qt.KeypadModifier |
|
|
||||||
Qt.GroupSwitchModifier)
|
|
||||||
assert Qt.Key_unknown & ~modifier_mask == Qt.Key_unknown
|
|
||||||
|
|
||||||
modifiers = result & modifier_mask
|
|
||||||
key = result & ~modifier_mask
|
|
||||||
|
|
||||||
if len(keystr) == 1 and keystr.isupper():
|
|
||||||
modifiers |= Qt.ShiftModifier
|
|
||||||
|
|
||||||
assert key != 0, key
|
|
||||||
key = Qt.Key(key)
|
|
||||||
modifiers = Qt.KeyboardModifiers(modifiers)
|
|
||||||
|
|
||||||
# Let's hope this is accurate...
|
|
||||||
if len(keystr) == 1 and not modifiers:
|
|
||||||
text = keystr
|
|
||||||
elif len(keystr) == 1 and modifiers == Qt.ShiftModifier:
|
|
||||||
text = keystr.upper()
|
|
||||||
else:
|
|
||||||
text = ''
|
|
||||||
|
|
||||||
return KeyInfo(key, modifiers, text)
|
|
||||||
|
|
||||||
|
|
||||||
def _parse_keystring(keystr):
|
|
||||||
key = ''
|
|
||||||
special = False
|
|
||||||
for c in keystr:
|
|
||||||
if c == '>':
|
|
||||||
yield normalize_keystr(key)
|
|
||||||
key = ''
|
|
||||||
special = False
|
|
||||||
elif c == '<':
|
|
||||||
special = True
|
|
||||||
elif special:
|
|
||||||
key += c
|
|
||||||
else:
|
|
||||||
yield 'Shift+' + c if c.isupper() else c
|
|
||||||
|
|
||||||
|
|
||||||
def parse_keystring(keystr):
|
|
||||||
"""Parse a keystring like <Ctrl-x> or xyz and return a KeyInfo list."""
|
|
||||||
s = ', '.join(_parse_keystring(keystr))
|
|
||||||
return sequence.KeySequence(s)
|
|
||||||
|
|
||||||
|
|
||||||
def normalize_keystr(keystr):
|
|
||||||
"""Normalize a keystring like Ctrl-Q to a keystring like Ctrl+Q.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
keystr: The key combination as a string.
|
|
||||||
|
|
||||||
Return:
|
|
||||||
The normalized keystring.
|
|
||||||
"""
|
|
||||||
keystr = keystr.lower()
|
|
||||||
replacements = (
|
|
||||||
('control', 'ctrl'),
|
|
||||||
('windows', 'meta'),
|
|
||||||
('mod1', 'alt'),
|
|
||||||
('mod4', 'meta'),
|
|
||||||
)
|
|
||||||
for (orig, repl) in replacements:
|
|
||||||
keystr = keystr.replace(orig, repl)
|
|
||||||
for mod in ['ctrl', 'meta', 'alt', 'shift']:
|
|
||||||
keystr = keystr.replace(mod + '-', mod + '+')
|
|
||||||
return keystr
|
|
||||||
|
|
||||||
|
|
||||||
class FakeIOStream(io.TextIOBase):
|
class FakeIOStream(io.TextIOBase):
|
||||||
|
|
||||||
"""A fake file-like stream which calls a function for write-calls."""
|
"""A fake file-like stream which calls a function for write-calls."""
|
||||||
|
Loading…
Reference in New Issue
Block a user