541 lines
16 KiB
Python
541 lines
16 KiB
Python
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
|
||
|
||
# Copyright 2014-2018 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
|
||
#
|
||
# This file is part of qutebrowser.
|
||
#
|
||
# qutebrowser is free software: you can redistribute it and/or modify
|
||
# it under the terms of the GNU General Public License as published by
|
||
# the Free Software Foundation, either version 3 of the License, or
|
||
# (at your option) any later version.
|
||
#
|
||
# qutebrowser is distributed in the hope that it will be useful,
|
||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||
# GNU General Public License for more details.
|
||
#
|
||
# You should have received a copy of the GNU General Public License
|
||
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
|
||
|
||
"""Our own QKeySequence-like class and related utilities."""
|
||
|
||
import itertools
|
||
|
||
import attr
|
||
from PyQt5.QtCore import Qt, QEvent
|
||
from PyQt5.QtGui import QKeySequence, QKeyEvent
|
||
|
||
from qutebrowser.utils import utils
|
||
|
||
|
||
# Map Qt::Key values to their Qt::KeyboardModifier value.
|
||
_MODIFIER_MAP = {
|
||
Qt.Key_Shift: Qt.ShiftModifier,
|
||
Qt.Key_Control: Qt.ControlModifier,
|
||
Qt.Key_Alt: Qt.AltModifier,
|
||
Qt.Key_Meta: Qt.MetaModifier,
|
||
Qt.Key_Mode_switch: Qt.GroupSwitchModifier,
|
||
}
|
||
|
||
|
||
def _assert_plain_key(key):
|
||
"""Make sure this is a key without KeyboardModifiers mixed in."""
|
||
assert not key & Qt.KeyboardModifierMask, hex(key)
|
||
|
||
|
||
def _assert_plain_modifier(key):
|
||
"""Make sure this is a modifier without a key mixed in."""
|
||
assert not key & ~Qt.KeyboardModifierMask, hex(key)
|
||
|
||
|
||
def _is_printable(key):
|
||
_assert_plain_key(key)
|
||
return key <= 0xff and key not in [Qt.Key_Space, 0x0]
|
||
|
||
|
||
def is_special(key, modifiers):
|
||
"""Check whether this key requires special key syntax."""
|
||
_assert_plain_key(key)
|
||
_assert_plain_modifier(modifiers)
|
||
return not (_is_printable(key) and
|
||
modifiers in [Qt.ShiftModifier, Qt.NoModifier])
|
||
|
||
|
||
def is_modifier_key(key):
|
||
"""Test whether the given key is a modifier.
|
||
|
||
This only considers keys which are part of Qt::KeyboardModifiers, i.e.
|
||
which would interrupt a key chain like "yY" when handled.
|
||
"""
|
||
_assert_plain_key(key)
|
||
return key in _MODIFIER_MAP
|
||
|
||
|
||
def _check_valid_utf8(s, data):
|
||
"""Make sure the given string is valid UTF-8.
|
||
|
||
Makes sure there are no chars where Qt did fall back to weird UTF-16
|
||
surrogates.
|
||
"""
|
||
try:
|
||
s.encode('utf-8')
|
||
except UnicodeEncodeError as e: # pragma: no cover
|
||
raise ValueError("Invalid encoding in 0x{:x} -> {}: {}"
|
||
.format(data, s, e))
|
||
|
||
|
||
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.
|
||
"""
|
||
_assert_plain_key(key)
|
||
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.
|
||
|
||
'Super_L': 'Super L',
|
||
'Super_R': 'Super R',
|
||
'Hyper_L': 'Hyper L',
|
||
'Hyper_R': 'Hyper R',
|
||
'Direction_L': 'Direction L',
|
||
'Direction_R': 'Direction R',
|
||
|
||
'Shift': 'Shift',
|
||
'Control': 'Control',
|
||
'Meta': 'Meta',
|
||
'Alt': 'Alt',
|
||
|
||
'AltGr': 'AltGr',
|
||
'Multi_key': 'Multi key',
|
||
'SingleCandidate': 'Single Candidate',
|
||
'Mode_switch': 'Mode switch',
|
||
'Dead_Grave': '`',
|
||
'Dead_Acute': '´',
|
||
'Dead_Circumflex': '^',
|
||
'Dead_Tilde': '~',
|
||
'Dead_Macron': '¯',
|
||
'Dead_Breve': '˘',
|
||
'Dead_Abovedot': '˙',
|
||
'Dead_Diaeresis': '¨',
|
||
'Dead_Abovering': '˚',
|
||
'Dead_Doubleacute': '˝',
|
||
'Dead_Caron': 'ˇ',
|
||
'Dead_Cedilla': '¸',
|
||
'Dead_Ogonek': '˛',
|
||
'Dead_Iota': 'Iota',
|
||
'Dead_Voiced_Sound': 'Voiced Sound',
|
||
'Dead_Semivoiced_Sound': 'Semivoiced Sound',
|
||
'Dead_Belowdot': 'Belowdot',
|
||
'Dead_Hook': 'Hook',
|
||
'Dead_Horn': 'Horn',
|
||
|
||
'Memo': 'Memo',
|
||
'ToDoList': 'To Do List',
|
||
'Calendar': 'Calendar',
|
||
'ContrastAdjust': 'Contrast Adjust',
|
||
'LaunchG': 'Launch (G)',
|
||
'LaunchH': 'Launch (H)',
|
||
|
||
'MediaLast': 'Media Last',
|
||
|
||
'unknown': 'Unknown',
|
||
|
||
# For some keys, we just want a different name
|
||
'Escape': 'Escape',
|
||
}
|
||
# 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, 'Key_' + k)] = v
|
||
except AttributeError:
|
||
pass
|
||
special_names[0x0] = 'nil'
|
||
|
||
if key in special_names:
|
||
return special_names[key]
|
||
|
||
result = QKeySequence(key).toString()
|
||
_check_valid_utf8(result, key)
|
||
return result
|
||
|
||
|
||
def _modifiers_to_string(modifiers):
|
||
"""Convert the given Qt::KeyboardModifiers to a string.
|
||
|
||
Handles Qt.GroupSwitchModifier because Qt doesn't handle that as a
|
||
modifier.
|
||
"""
|
||
_assert_plain_modifier(modifiers)
|
||
if modifiers & Qt.GroupSwitchModifier:
|
||
modifiers &= ~Qt.GroupSwitchModifier
|
||
result = 'AltGr+'
|
||
else:
|
||
result = ''
|
||
|
||
result += QKeySequence(modifiers).toString()
|
||
|
||
_check_valid_utf8(result, modifiers)
|
||
return result
|
||
|
||
|
||
class KeyParseError(Exception):
|
||
|
||
"""Raised by _parse_single_key/parse_keystring on parse errors."""
|
||
|
||
def __init__(self, keystr, error):
|
||
if keystr is None:
|
||
msg = "Could not parse keystring: {}".format(error)
|
||
else:
|
||
msg = "Could not parse {!r}: {}".format(keystr, error)
|
||
super().__init__(msg)
|
||
|
||
|
||
def _parse_keystring(keystr):
|
||
key = ''
|
||
special = False
|
||
for c in keystr:
|
||
if c == '>':
|
||
if special:
|
||
yield _parse_special_key(key)
|
||
key = ''
|
||
special = False
|
||
else:
|
||
yield '>'
|
||
assert not key, key
|
||
elif c == '<':
|
||
special = True
|
||
elif special:
|
||
key += c
|
||
else:
|
||
yield _parse_single_key(c)
|
||
if special:
|
||
yield '<'
|
||
for c in key:
|
||
yield _parse_single_key(c)
|
||
|
||
|
||
def _parse_special_key(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'),
|
||
('less', '<'),
|
||
('greater', '>'),
|
||
)
|
||
for (orig, repl) in replacements:
|
||
keystr = keystr.replace(orig, repl)
|
||
|
||
for mod in ['ctrl', 'meta', 'alt', 'shift', 'num']:
|
||
keystr = keystr.replace(mod + '-', mod + '+')
|
||
return keystr
|
||
|
||
|
||
def _parse_single_key(keystr):
|
||
"""Get a keystring for QKeySequence for a single key."""
|
||
return 'Shift+' + keystr if keystr.isupper() else keystr
|
||
|
||
|
||
@attr.s
|
||
class KeyInfo:
|
||
|
||
"""A key with optional modifiers.
|
||
|
||
Attributes:
|
||
key: A Qt::Key member.
|
||
modifiers: A Qt::KeyboardModifiers enum value.
|
||
"""
|
||
|
||
key = attr.ib()
|
||
modifiers = attr.ib()
|
||
|
||
@classmethod
|
||
def from_event(cls, e):
|
||
return cls(e.key(), e.modifiers())
|
||
|
||
def __str__(self):
|
||
"""Convert this KeyInfo to a meaningful name.
|
||
|
||
Return:
|
||
A name of the key (combination) as a string.
|
||
"""
|
||
key_string = _key_to_string(self.key)
|
||
modifiers = int(self.modifiers)
|
||
|
||
if self.key in _MODIFIER_MAP:
|
||
# Don't return e.g. <Shift+Shift>
|
||
modifiers &= ~_MODIFIER_MAP[self.key]
|
||
elif _is_printable(self.key):
|
||
# "normal" binding
|
||
if not key_string: # pragma: no cover
|
||
raise ValueError("Got empty string for key 0x{:x}!"
|
||
.format(self.key))
|
||
|
||
assert len(key_string) == 1, key_string
|
||
if self.modifiers == Qt.ShiftModifier:
|
||
assert not is_special(self.key, self.modifiers)
|
||
return key_string.upper()
|
||
elif self.modifiers == Qt.NoModifier:
|
||
assert not is_special(self.key, self.modifiers)
|
||
return key_string.lower()
|
||
else:
|
||
# Use special binding syntax, but <Ctrl-a> instead of <Ctrl-A>
|
||
key_string = key_string.lower()
|
||
|
||
# "special" binding
|
||
assert is_special(self.key, self.modifiers)
|
||
modifier_string = _modifiers_to_string(modifiers)
|
||
return '<{}{}>'.format(modifier_string, key_string)
|
||
|
||
def text(self):
|
||
"""Get the text which would be displayed when pressing this key."""
|
||
control = {
|
||
Qt.Key_Space: ' ',
|
||
Qt.Key_Tab: '\t',
|
||
Qt.Key_Backspace: '\b',
|
||
Qt.Key_Return: '\r',
|
||
Qt.Key_Enter: '\r',
|
||
Qt.Key_Escape: '\x1b',
|
||
}
|
||
|
||
if self.key in control:
|
||
return control[self.key]
|
||
elif not _is_printable(self.key):
|
||
return ''
|
||
|
||
text = QKeySequence(self.key).toString()
|
||
if not self.modifiers & Qt.ShiftModifier:
|
||
text = text.lower()
|
||
return text
|
||
|
||
def to_event(self, typ=QEvent.KeyPress):
|
||
"""Get a QKeyEvent from this KeyInfo."""
|
||
return QKeyEvent(typ, self.key, self.modifiers, self.text())
|
||
|
||
def to_int(self):
|
||
"""Get the key as an integer (with key/modifiers)."""
|
||
return int(self.key) | int(self.modifiers)
|
||
|
||
|
||
class KeySequence:
|
||
|
||
"""A sequence of key presses.
|
||
|
||
This internally uses chained QKeySequence objects and exposes a nicer
|
||
interface over it.
|
||
|
||
NOTE: While private members of this class are in theory mutable, they must
|
||
not be mutated in order to ensure consistent hashing.
|
||
|
||
Attributes:
|
||
_sequences: A list of QKeySequence
|
||
|
||
Class attributes:
|
||
_MAX_LEN: The maximum amount of keys in a QKeySequence.
|
||
"""
|
||
|
||
_MAX_LEN = 4
|
||
|
||
def __init__(self, *keys):
|
||
self._sequences = []
|
||
for sub in utils.chunk(keys, self._MAX_LEN):
|
||
sequence = QKeySequence(*sub)
|
||
self._sequences.append(sequence)
|
||
if keys:
|
||
assert self
|
||
self._validate()
|
||
|
||
def __str__(self):
|
||
parts = []
|
||
for info in self:
|
||
parts.append(str(info))
|
||
return ''.join(parts)
|
||
|
||
def __iter__(self):
|
||
"""Iterate over KeyInfo objects."""
|
||
for key_and_modifiers in self._iter_keys():
|
||
key = int(key_and_modifiers) & ~Qt.KeyboardModifierMask
|
||
modifiers = Qt.KeyboardModifiers(int(key_and_modifiers) &
|
||
Qt.KeyboardModifierMask)
|
||
yield KeyInfo(key=key, modifiers=modifiers)
|
||
|
||
def __repr__(self):
|
||
return utils.get_repr(self, keys=str(self))
|
||
|
||
def __lt__(self, other):
|
||
# pylint: disable=protected-access
|
||
return self._sequences < other._sequences
|
||
|
||
def __gt__(self, other):
|
||
# pylint: disable=protected-access
|
||
return self._sequences > other._sequences
|
||
|
||
def __le__(self, other):
|
||
# pylint: disable=protected-access
|
||
return self._sequences <= other._sequences
|
||
|
||
def __ge__(self, other):
|
||
# pylint: disable=protected-access
|
||
return self._sequences >= other._sequences
|
||
|
||
def __eq__(self, other):
|
||
# pylint: disable=protected-access
|
||
return self._sequences == other._sequences
|
||
|
||
def __ne__(self, other):
|
||
# pylint: disable=protected-access
|
||
return self._sequences != other._sequences
|
||
|
||
def __hash__(self):
|
||
return hash(tuple(self._sequences))
|
||
|
||
def __len__(self):
|
||
return sum(len(seq) for seq in self._sequences)
|
||
|
||
def __bool__(self):
|
||
return bool(self._sequences)
|
||
|
||
def __getitem__(self, item):
|
||
if isinstance(item, slice):
|
||
keys = list(self._iter_keys())
|
||
return self.__class__(*keys[item])
|
||
else:
|
||
infos = list(self)
|
||
return infos[item]
|
||
|
||
def _iter_keys(self):
|
||
return itertools.chain.from_iterable(self._sequences)
|
||
|
||
def _validate(self, keystr=None):
|
||
for info in self:
|
||
if info.key < Qt.Key_Space or info.key >= Qt.Key_unknown:
|
||
raise KeyParseError(keystr, "Got invalid key!")
|
||
|
||
for seq in self._sequences:
|
||
if not seq:
|
||
raise KeyParseError(keystr, "Got invalid key!")
|
||
|
||
def matches(self, other):
|
||
"""Check whether the given KeySequence matches with this one.
|
||
|
||
We store multiple QKeySequences with <= 4 keys each, so we need to
|
||
match those pair-wise, and account for an unequal amount of sequences
|
||
as well.
|
||
"""
|
||
# pylint: disable=protected-access
|
||
|
||
if len(self._sequences) > len(other._sequences):
|
||
# If we entered more sequences than there are in the config,
|
||
# there's no way there can be a match.
|
||
return QKeySequence.NoMatch
|
||
|
||
for entered, configured in zip(self._sequences, other._sequences):
|
||
# If we get NoMatch/PartialMatch in a sequence, we can abort there.
|
||
match = entered.matches(configured)
|
||
if match != QKeySequence.ExactMatch:
|
||
return match
|
||
|
||
# We checked all common sequences and they had an ExactMatch.
|
||
#
|
||
# If there's still more sequences configured than entered, that's a
|
||
# PartialMatch, as more keypresses can still follow and new sequences
|
||
# will appear which we didn't check above.
|
||
#
|
||
# If there's the same amount of sequences configured and entered,
|
||
# that's an EqualMatch.
|
||
if len(self._sequences) == len(other._sequences):
|
||
return QKeySequence.ExactMatch
|
||
elif len(self._sequences) < len(other._sequences):
|
||
return QKeySequence.PartialMatch
|
||
else:
|
||
raise utils.Unreachable("self={!r} other={!r}".format(self, other))
|
||
|
||
def append_event(self, ev):
|
||
"""Create a new KeySequence object with the given QKeyEvent added."""
|
||
key = ev.key()
|
||
modifiers = ev.modifiers()
|
||
|
||
_assert_plain_key(key)
|
||
_assert_plain_modifier(modifiers)
|
||
|
||
if key == 0x0:
|
||
raise KeyParseError(None, "Got nil key!")
|
||
|
||
# We always remove Qt.GroupSwitchModifier because QKeySequence has no
|
||
# way to mention that in a binding anyways...
|
||
modifiers &= ~Qt.GroupSwitchModifier
|
||
|
||
# We change Qt.Key_Backtab to Key_Tab here because nobody would
|
||
# configure "Shift-Backtab" in their config.
|
||
if modifiers & Qt.ShiftModifier and key == Qt.Key_Backtab:
|
||
key = Qt.Key_Tab
|
||
|
||
# We don't care about a shift modifier with symbols (Shift-: should
|
||
# match a : binding even though we typed it with a shift on an
|
||
# US-keyboard)
|
||
#
|
||
# However, we *do* care about Shift being involved if we got an
|
||
# upper-case letter, as Shift-A should match a Shift-A binding, but not
|
||
# an "a" binding.
|
||
#
|
||
# In addition, Shift also *is* relevant when other modifiers are
|
||
# involved. Shift-Ctrl-X should not be equivalent to Ctrl-X.
|
||
if (modifiers == Qt.ShiftModifier and
|
||
_is_printable(ev.key()) and
|
||
not ev.text().isupper()):
|
||
modifiers = Qt.KeyboardModifiers()
|
||
|
||
keys = list(self._iter_keys())
|
||
keys.append(key | int(modifiers))
|
||
|
||
return self.__class__(*keys)
|
||
|
||
def with_mappings(self, mappings):
|
||
"""Get a new KeySequence with the given mappings applied."""
|
||
keys = []
|
||
for key in self._iter_keys():
|
||
key_seq = KeySequence(key)
|
||
if key_seq in mappings:
|
||
new_seq = mappings[key_seq]
|
||
assert len(new_seq) == 1
|
||
key = new_seq[0].to_int()
|
||
keys.append(key)
|
||
return self.__class__(*keys)
|
||
|
||
@classmethod
|
||
def parse(cls, keystr):
|
||
"""Parse a keystring like <Ctrl-x> or xyz and return a KeySequence."""
|
||
# pylint: disable=protected-access
|
||
new = cls()
|
||
strings = list(_parse_keystring(keystr))
|
||
for sub in utils.chunk(strings, cls._MAX_LEN):
|
||
sequence = QKeySequence(', '.join(sub))
|
||
new._sequences.append(sequence)
|
||
|
||
if keystr:
|
||
assert new, keystr
|
||
|
||
# pylint: disable=protected-access
|
||
new._validate(keystr)
|
||
return new
|