qutebrowser/qutebrowser/keyinput/keyutils.py
2017-12-29 14:43:04 +01:00

310 lines
9.8 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
# Copyright 2014-2017 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 unicodedata
import collections
import attr
from PyQt5.QtCore import Qt
from PyQt5.QtGui import QKeySequence
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."""
return key_with_modifiers_to_string(e.key(), e.modifiers())
def key_with_modifiers_to_string(key, modifiers):
"""Convert a Qt.Key with modifiers to a meaningful name.
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'),
])
modifier_keys = (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 key in modifier_keys:
# Only modifier pressed
return None
parts = []
for (mask, s) in modmask2str.items():
if modifiers & mask and s not in parts:
parts.append(s)
key_string = key_to_string(key)
# FIXME needed?
if len(key_string) == 1:
category = unicodedata.category(key_string)
is_control_char = (category == 'Cc')
else:
is_control_char = False
if modifiers == Qt.ShiftModifier and not is_control_char:
parts = []
parts.append(key_string)
normalized = normalize_keystr('+'.join(parts))
if len(normalized) > 1:
# "special" binding
return '<{}>'.format(normalized)
else:
# "normal" binding
return normalized
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 _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 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:
def __init__(self, *args):
self._sequence = QKeySequence(*args)
# FIXME handle more than 4 keys
def __str__(self):
modifier_mask = int(Qt.ShiftModifier | Qt.ControlModifier |
Qt.AltModifier | Qt.MetaModifier |
Qt.KeypadModifier | Qt.GroupSwitchModifier)
parts = []
for key in self._sequence:
part = key_with_modifiers_to_string(
key=int(key) & ~modifier_mask,
modifiers=int(key) & modifier_mask)
parts.append(part)
return ''.join(parts)
def __repr__(self):
return utils.get_repr(self, keys=str(self))
def __lt__(self, other):
return self._sequence < other._sequence
def __gt__(self, other):
return self._sequence > other._sequence
def __eq__(self, other):
return self._sequence == other._sequence
def __ne__(self, other):
return self._sequence != other._sequence
def __hash__(self):
return hash(self._sequence)
def matches(self, other):
# pylint: disable=protected-access
return self._sequence.matches(other._sequence)
def append_event(self, ev):
"""Create a new KeySequence object with the given QKeyEvent added.
We need to do some sophisticated checking of modifiers here:
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.
FIXME: create test cases!
"""
modifiers = ev.modifiers()
if (modifiers == Qt.ShiftModifier and
unicodedata.category(ev.text()) != 'Lu'):
modifiers = Qt.KeyboardModifiers()
return self.__class__(*self._sequence, modifiers | ev.key())
@classmethod
def parse(cls, keystr):
"""Parse a keystring like <Ctrl-x> or xyz and return a KeySequence."""
s = ', '.join(_parse_keystring(keystr))
return cls(s)