Move key related utils to sequence.py

This commit is contained in:
Florian Bruhin 2017-12-29 01:02:28 +01:00
parent 600919a23a
commit dcf0d21121
3 changed files with 293 additions and 287 deletions

View File

@ -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)

View File

@ -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:

View File

@ -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."""