Split command-related stuff off from KeyParser

This commit is contained in:
Florian Bruhin 2014-04-21 10:45:57 +02:00
parent 406e115a07
commit 52c7376402
3 changed files with 252 additions and 189 deletions

View File

@ -55,7 +55,7 @@ import qutebrowser.network.qutescheme as qutescheme
import qutebrowser.utils.message as message import qutebrowser.utils.message as message
from qutebrowser.widgets.mainwindow import MainWindow from qutebrowser.widgets.mainwindow import MainWindow
from qutebrowser.widgets.crash import CrashDialog from qutebrowser.widgets.crash import CrashDialog
from qutebrowser.commands.keys import KeyParser from qutebrowser.commands.keys import CommandKeyParser
from qutebrowser.commands.parsers import CommandParser, SearchParser from qutebrowser.commands.parsers import CommandParser, SearchParser
from qutebrowser.utils.appdirs import AppDirs from qutebrowser.utils.appdirs import AppDirs
from qutebrowser.utils.misc import dotted_getattr from qutebrowser.utils.misc import dotted_getattr
@ -74,7 +74,7 @@ class QuteBrowser(QApplication):
Attributes: Attributes:
mainwindow: The MainWindow QWidget. mainwindow: The MainWindow QWidget.
commandparser: The main CommandParser instance. commandparser: The main CommandParser instance.
keyparser: The main KeyParser instance. keyparser: The main CommandKeyParser instance.
searchparser: The main SearchParser instance. searchparser: The main SearchParser instance.
_dirs: AppDirs instance for config/cache directories. _dirs: AppDirs instance for config/cache directories.
_args: ArgumentParser instance. _args: ArgumentParser instance.
@ -108,7 +108,7 @@ class QuteBrowser(QApplication):
self.commandparser = CommandParser() self.commandparser = CommandParser()
self.searchparser = SearchParser() self.searchparser = SearchParser()
self.keyparser = KeyParser(self) self.keyparser = CommandKeyParser(self)
self._init_cmds() self._init_cmds()
self.mainwindow = MainWindow() self.mainwindow = MainWindow()

View File

@ -21,202 +21,39 @@ Module attributes:
STARTCHARS: Possible chars for starting a commandline input. STARTCHARS: Possible chars for starting a commandline input.
""" """
import re
import logging import logging
from PyQt5.QtCore import pyqtSignal, pyqtSlot, Qt, QObject from PyQt5.QtCore import pyqtSignal, pyqtSlot
from PyQt5.QtGui import QKeySequence
import qutebrowser.config.config as config import qutebrowser.config.config as config
from qutebrowser.utils.keyparser import KeyParser
from qutebrowser.commands.parsers import (CommandParser, ArgumentCountError, from qutebrowser.commands.parsers import (CommandParser, ArgumentCountError,
NoSuchCommandError) NoSuchCommandError)
STARTCHARS = ":/?" STARTCHARS = ":/?"
class KeyParser(QObject): class CommandKeyParser(KeyParser):
"""Parser for vim-like key sequences. """Keyparser for command bindings.
Class Attributes:
MATCH_PARTIAL: Constant for a partial match (no keychain matched yet,
but it's still possible in the future.
MATCH_DEFINITIVE: Constant for a full match (keychain matches exactly).
MATCH_NONE: Constant for no match (no more matches possible).
Attributes: Attributes:
commandparser: Commandparser instance. commandparser: Commandparser instance.
_keystring: The currently entered key sequence supports_count: If the keyparser should support counts.
_bindings: Bound keybindings
_modifier_bindings: Bound modifier bindings.
Signals: Signals:
set_cmd_text: Emitted when the statusbar should set a partial command. set_cmd_text: Emitted when the statusbar should set a partial command.
arg: Text to set. arg: Text to set.
keystring_updated: Emitted when the keystring is updated.
arg: New keystring.
""" """
set_cmd_text = pyqtSignal(str) set_cmd_text = pyqtSignal(str)
keystring_updated = pyqtSignal(str) supports_count = True
MATCH_PARTIAL = 0 def __init__(self, parent=None):
MATCH_DEFINITIVE = 1 super().__init__(parent)
MATCH_NONE = 2
def __init__(self, mainwindow):
super().__init__(mainwindow)
self.commandparser = CommandParser() self.commandparser = CommandParser()
self._keystring = ''
self._bindings = {}
self._modifier_bindings = {}
self.read_config() self.read_config()
def _handle_modifier_key(self, e):
"""Handle a new keypress with modifiers.
Return True if the keypress has been handled, and False if not.
Args:
e: the KeyPressEvent from Qt.
Return:
True if event has been handled, False otherwise.
"""
modmask2str = {
Qt.ControlModifier: 'Ctrl',
Qt.AltModifier: 'Alt',
Qt.MetaModifier: 'Meta',
Qt.ShiftModifier: 'Shift'
}
if e.key() in [Qt.Key_Control, Qt.Key_Alt, Qt.Key_Shift, Qt.Key_Meta]:
# Only modifier pressed
return False
mod = e.modifiers()
modstr = ''
if not mod & (Qt.ControlModifier | Qt.AltModifier | Qt.MetaModifier):
# won't be a shortcut with modifiers
return False
for (mask, s) in modmask2str.items():
if mod & mask:
modstr += s + '+'
keystr = QKeySequence(e.key()).toString()
try:
cmdstr = self._modifier_bindings[modstr + keystr]
except KeyError:
logging.debug('No binding found for {}.'.format(modstr + keystr))
return True
self._run_or_fill(cmdstr, ignore_exc=False)
return True
def _handle_single_key(self, e):
"""Handle a new keypress with a single key (no modifiers).
Separate the keypress into count/command, then check if it matches
any possible command, and either run the command, ignore it, or
display an error.
Args:
e: the KeyPressEvent from Qt.
Emit:
set_cmd_text: If the keystring should be shown in the statusbar.
"""
# FIXME maybe we can do this in an easier way by using QKeySeqyence
# which has a matches method.
logging.debug('Got key: {} / text: "{}"'.format(e.key(), e.text()))
txt = e.text().strip()
if not txt:
logging.debug('Ignoring, no text')
return
self._keystring += txt
if any(self._keystring == c for c in STARTCHARS):
self.set_cmd_text.emit(self._keystring)
self._keystring = ''
return
(countstr, cmdstr_needle) = re.match(r'^(\d*)(.*)',
self._keystring).groups()
if not cmdstr_needle:
return
# FIXME this doesn't handle ambigious keys correctly.
#
# If a keychain is ambigious, we probably should set up a QTimer with a
# configurable timeout, which triggers cmd.run() when expiring. Then
# when we enter _handle() again in time we stop the timer.
(match, cmdstr_hay) = self._match_key(cmdstr_needle)
if match == self.MATCH_DEFINITIVE:
pass
elif match == self.MATCH_PARTIAL:
logging.debug('No match for "{}" (added {})'.format(
self._keystring, txt))
return
elif match == self.MATCH_NONE:
logging.debug('Giving up with "{}", no matches'.format(
self._keystring))
self._keystring = ''
return
self._keystring = ''
count = int(countstr) if countstr else None
self._run_or_fill(cmdstr_hay, count=count, ignore_exc=False)
return
def _match_key(self, cmdstr_needle):
"""Try to match a given keystring with any bound keychain.
Args:
cmdstr_needle: The command string to find.
Return:
A tuple (matchtype, hay) where matchtype is MATCH_DEFINITIVE,
MATCH_PARTIAL or MATCH_NONE and hay is the long keystring where the
part was found in.
"""
try:
cmdstr_hay = self._bindings[cmdstr_needle]
return (self.MATCH_DEFINITIVE, cmdstr_hay)
except KeyError:
# No definitive match, check if there's a chance of a partial match
for hay in self._bindings:
try:
if cmdstr_needle[-1] == hay[len(cmdstr_needle) - 1]:
return (self.MATCH_PARTIAL, None)
except IndexError:
# current cmd is shorter than our cmdstr_needle, so it
# won't match
continue
# no definitive and no partial matches if we arrived here
return (self.MATCH_NONE, None)
def _normalize_keystr(self, 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.
"""
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 + '+')
keystr = QKeySequence(keystr).toString()
return keystr
def _run_or_fill(self, cmdstr, count=None, ignore_exc=True): def _run_or_fill(self, cmdstr, count=None, ignore_exc=True):
"""Run the command in cmdstr or fill the statusbar if args missing. """Run the command in cmdstr or fill the statusbar if args missing.
@ -237,7 +74,25 @@ class KeyParser(QObject):
logging.debug('Filling statusbar with partial command {}'.format( logging.debug('Filling statusbar with partial command {}'.format(
cmdstr)) cmdstr))
self.set_cmd_text.emit(':{} '.format(cmdstr)) self.set_cmd_text.emit(':{} '.format(cmdstr))
return
def _handle_single_key(self, e):
"""Override _handle_single_key to abort if the key is a startchar.
Args:
e: the KeyPressEvent from Qt.
Emit:
set_cmd_text: If the keystring should be shown in the statusbar.
"""
txt = e.text().strip()
if not self._keystring and any(txt == c for c in STARTCHARS):
self.set_cmd_text.emit(txt)
return
super()._handle_single_key(e)
def execute(self, cmdstr, count=None):
"""Handle a completed keychain."""
self._run_or_fill(cmdstr, count, ignore_exc=False)
# pylint: disable=unused-argument # pylint: disable=unused-argument
@pyqtSlot(str, str) @pyqtSlot(str, str)
@ -265,17 +120,3 @@ class KeyParser(QObject):
else: else:
logging.debug('registered key: {} -> {}'.format(key, cmd)) logging.debug('registered key: {} -> {}'.format(key, cmd))
self._bindings[key] = cmd self._bindings[key] = cmd
def handle(self, e):
"""Handle a new keypress and call the respective handlers.
Args:
e: the KeyPressEvent from Qt
Emit:
keystring_updated: If a new keystring should be set.
"""
handled = self._handle_modifier_key(e)
if not handled:
self._handle_single_key(e)
self.keystring_updated.emit(self._keystring)

View File

@ -0,0 +1,222 @@
# Copyright 2014 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/>.
"""Base class for vim-like keysequence parser."""
import re
import logging
from PyQt5.QtCore import pyqtSignal, Qt, QObject
from PyQt5.QtGui import QKeySequence
class KeyParser(QObject):
"""Parser for vim-like key sequences.
Not intended to be instantiated directly. Subclasses have to override
execute() to do whatever they want to.
Class Attributes:
MATCH_PARTIAL: Constant for a partial match (no keychain matched yet,
but it's still possible in the future.
MATCH_DEFINITIVE: Constant for a full match (keychain matches exactly).
MATCH_NONE: Constant for no match (no more matches possible).
supports_count: If the keyparser should support counts.
Attributes:
_keystring: The currently entered key sequence
_bindings: Bound keybindings
_modifier_bindings: Bound modifier bindings.
Signals:
keystring_updated: Emitted when the keystring is updated.
arg: New keystring.
"""
keystring_updated = pyqtSignal(str)
MATCH_PARTIAL = 0
MATCH_DEFINITIVE = 1
MATCH_NONE = 2
supports_count = False
def __init__(self, parent=None):
super().__init__(parent)
self._keystring = ''
self._bindings = {}
self._modifier_bindings = {}
def _handle_modifier_key(self, e):
"""Handle a new keypress with modifiers.
Return True if the keypress has been handled, and False if not.
Args:
e: the KeyPressEvent from Qt.
Return:
True if event has been handled, False otherwise.
"""
modmask2str = {
Qt.ControlModifier: 'Ctrl',
Qt.AltModifier: 'Alt',
Qt.MetaModifier: 'Meta',
Qt.ShiftModifier: 'Shift'
}
if e.key() in [Qt.Key_Control, Qt.Key_Alt, Qt.Key_Shift, Qt.Key_Meta]:
# Only modifier pressed
return False
mod = e.modifiers()
modstr = ''
if not mod & (Qt.ControlModifier | Qt.AltModifier | Qt.MetaModifier):
# won't be a shortcut with modifiers
return False
for (mask, s) in modmask2str.items():
if mod & mask:
modstr += s + '+'
keystr = QKeySequence(e.key()).toString()
try:
cmdstr = self._modifier_bindings[modstr + keystr]
except KeyError:
logging.debug('No binding found for {}.'.format(modstr + keystr))
return True
self.execute(cmdstr)
return True
def _handle_single_key(self, e):
"""Handle a new keypress with a single key (no modifiers).
Separate the keypress into count/command, then check if it matches
any possible command, and either run the command, ignore it, or
display an error.
Args:
e: the KeyPressEvent from Qt.
"""
# FIXME maybe we can do this in an easier way by using QKeySeqyence
# which has a matches method.
logging.debug('Got key: {} / text: "{}"'.format(e.key(), e.text()))
txt = e.text().strip()
if not txt:
logging.debug('Ignoring, no text')
return
self._keystring += txt
if self.supports_count:
(countstr, cmdstr_needle) = re.match(r'^(\d*)(.*)',
self._keystring).groups()
else:
countstr = None
cmdstr_needle = self._keystring
if not cmdstr_needle:
return
# FIXME this doesn't handle ambigious keys correctly.
#
# If a keychain is ambigious, we probably should set up a QTimer with a
# configurable timeout, which triggers cmd.run() when expiring. Then
# when we enter _handle() again in time we stop the timer.
(match, cmdstr_hay) = self._match_key(cmdstr_needle)
if match == self.MATCH_DEFINITIVE:
pass
elif match == self.MATCH_PARTIAL:
logging.debug('No match for "{}" (added {})'.format(
self._keystring, txt))
return
elif match == self.MATCH_NONE:
logging.debug('Giving up with "{}", no matches'.format(
self._keystring))
self._keystring = ''
return
self._keystring = ''
count = int(countstr) if countstr else None
self.execute(cmdstr_hay, count=count)
return
def _match_key(self, cmdstr_needle):
"""Try to match a given keystring with any bound keychain.
Args:
cmdstr_needle: The command string to find.
Return:
A tuple (matchtype, hay) where matchtype is MATCH_DEFINITIVE,
MATCH_PARTIAL or MATCH_NONE and hay is the long keystring where the
part was found in.
"""
try:
cmdstr_hay = self._bindings[cmdstr_needle]
return (self.MATCH_DEFINITIVE, cmdstr_hay)
except KeyError:
# No definitive match, check if there's a chance of a partial match
for hay in self._bindings:
try:
if cmdstr_needle[-1] == hay[len(cmdstr_needle) - 1]:
return (self.MATCH_PARTIAL, None)
except IndexError:
# current cmd is shorter than our cmdstr_needle, so it
# won't match
continue
# no definitive and no partial matches if we arrived here
return (self.MATCH_NONE, None)
def _normalize_keystr(self, 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.
"""
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 + '+')
keystr = QKeySequence(keystr).toString()
return keystr
def execute(self, cmdstr, count=None):
"""Execute an action when a binding is triggered."""
raise NotImplementedError
def handle(self, e):
"""Handle a new keypress and call the respective handlers.
Args:
e: the KeyPressEvent from Qt
Emit:
keystring_updated: If a new keystring should be set.
"""
handled = self._handle_modifier_key(e)
if not handled:
self._handle_single_key(e)
self.keystring_updated.emit(self._keystring)