Support ambiguous keybindings

This commit is contained in:
Florian Bruhin 2014-04-22 16:45:13 +02:00
parent f38871c9c9
commit aedf1889dd
2 changed files with 81 additions and 21 deletions

View File

@ -162,6 +162,10 @@ DATA = OrderedDict([
('background_tabs', ('background_tabs',
SettingValue(types.Bool(), "false"), SettingValue(types.Bool(), "false"),
"Whether to open new tabs (middleclick/ctrl+click) in background"), "Whether to open new tabs (middleclick/ctrl+click) in background"),
('cmd_timeout',
SettingValue(types.Int(minval=0), "500"),
"Timeout for ambiguous keybindings."),
)), )),
('tabbar', sect.KeyValue( ('tabbar', sect.KeyValue(

View File

@ -19,10 +19,13 @@
import re import re
import logging import logging
from functools import partial
from PyQt5.QtCore import pyqtSignal, Qt, QObject from PyQt5.QtCore import pyqtSignal, Qt, QObject, QTimer
from PyQt5.QtGui import QKeySequence from PyQt5.QtGui import QKeySequence
import qutebrowser.config.config as config
class KeyParser(QObject): class KeyParser(QObject):
@ -35,11 +38,13 @@ class KeyParser(QObject):
MATCH_PARTIAL: Constant for a partial match (no keychain matched yet, MATCH_PARTIAL: Constant for a partial match (no keychain matched yet,
but it's still possible in the future. but it's still possible in the future.
MATCH_DEFINITIVE: Constant for a full match (keychain matches exactly). MATCH_DEFINITIVE: Constant for a full match (keychain matches exactly).
MATCH_BOTH: There are both a partial and a definitive match.
MATCH_NONE: Constant for no match (no more matches possible). MATCH_NONE: Constant for no match (no more matches possible).
supports_count: If the keyparser should support counts. supports_count: If the keyparser should support counts.
Attributes: Attributes:
_keystring: The currently entered key sequence _keystring: The currently entered key sequence
_timer: QTimer for delayed execution.
bindings: Bound keybindings bindings: Bound keybindings
modifier_bindings: Bound modifier bindings. modifier_bindings: Bound modifier bindings.
@ -52,12 +57,14 @@ class KeyParser(QObject):
MATCH_PARTIAL = 0 MATCH_PARTIAL = 0
MATCH_DEFINITIVE = 1 MATCH_DEFINITIVE = 1
MATCH_NONE = 2 MATCH_BOTH = 2
MATCH_NONE = 3
supports_count = False supports_count = False
def __init__(self, parent=None, bindings=None, modifier_bindings=None): def __init__(self, parent=None, bindings=None, modifier_bindings=None):
super().__init__(parent) super().__init__(parent)
self._timer = None
self._keystring = '' self._keystring = ''
self.bindings = {} if bindings is None else bindings self.bindings = {} if bindings is None else bindings
self.modifier_bindings = ({} if modifier_bindings is None self.modifier_bindings = ({} if modifier_bindings is None
@ -116,6 +123,11 @@ class KeyParser(QObject):
logging.debug('Ignoring, no text') logging.debug('Ignoring, no text')
return return
if self._timer is not None:
logging.debug("Stopping delayed execution.")
self._timer.stop()
self._timer = None
self._keystring += txt self._keystring += txt
if self.supports_count: if self.supports_count:
@ -137,21 +149,35 @@ class KeyParser(QObject):
(match, cmdstr_hay) = self._match_key(cmdstr_needle) (match, cmdstr_hay) = self._match_key(cmdstr_needle)
if match == self.MATCH_DEFINITIVE: if match == self.MATCH_DEFINITIVE:
pass self._keystring = ''
count = int(countstr) if countstr else None
self.execute(cmdstr_hay, count=count)
elif match == self.MATCH_BOTH:
logging.debug("Partial and definitive match for \"{}\"".format(
self._keystring))
time = config.get('general', 'cmd_timeout')
count = int(countstr) if countstr else None
if time == 0:
# execute immediately
self._keystring = ''
self.execute(cmdstr_hay, count=count)
else:
# execute in `time' ms
logging.debug("Scheduling execution of {} in {}ms".format(
cmdstr_hay, time))
self._timer = QTimer(self)
self._timer.setSingleShot(True)
self._timer.setInterval(time)
self._timer.timeout.connect(
partial(self.delayed_exec, cmdstr_hay, count))
self._timer.start()
elif match == self.MATCH_PARTIAL: elif match == self.MATCH_PARTIAL:
logging.debug('No match for "{}" (added {})'.format( logging.debug('No match for "{}" (added {})'.format(
self._keystring, txt)) self._keystring, txt))
return
elif match == self.MATCH_NONE: elif match == self.MATCH_NONE:
logging.debug('Giving up with "{}", no matches'.format( logging.debug('Giving up with "{}", no matches'.format(
self._keystring)) self._keystring))
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): def _match_key(self, cmdstr_needle):
"""Try to match a given keystring with any bound keychain. """Try to match a given keystring with any bound keychain.
@ -164,20 +190,35 @@ class KeyParser(QObject):
MATCH_PARTIAL or MATCH_NONE and hay is the long keystring where the MATCH_PARTIAL or MATCH_NONE and hay is the long keystring where the
part was found in. part was found in.
""" """
# Check definitive match
definitive_match = None
partial_match = False
try: try:
cmdstr_hay = self.bindings[cmdstr_needle] cmdstr_hay = self.bindings[cmdstr_needle]
return (self.MATCH_DEFINITIVE, cmdstr_hay)
except KeyError: except KeyError:
# No definitive match, check if there's a chance of a partial match pass
for hay in self.bindings: else:
try: definitive_match = (cmdstr_needle, cmdstr_hay)
if cmdstr_needle[-1] == hay[len(cmdstr_needle) - 1]: # Check partial match
return (self.MATCH_PARTIAL, None) for hay in self.bindings:
except IndexError: if definitive_match is not None and hay == definitive_match[0]:
# current cmd is shorter than our cmdstr_needle, so it # We already matched that one
# won't match continue
continue try:
# no definitive and no partial matches if we arrived here if cmdstr_needle[-1] == hay[len(cmdstr_needle) - 1]:
partial_match = True
break
except IndexError:
# current cmd is shorter than our cmdstr_needle, so it
# won't match
continue
if definitive_match is not None and partial_match:
return (self.MATCH_BOTH, definitive_match[1])
elif definitive_match is not None:
return (self.MATCH_DEFINITIVE, definitive_match[1])
elif partial_match:
return (self.MATCH_PARTIAL, None)
else:
return (self.MATCH_NONE, None) return (self.MATCH_NONE, None)
def _normalize_keystr(self, keystr): def _normalize_keystr(self, keystr):
@ -202,6 +243,21 @@ class KeyParser(QObject):
keystr = QKeySequence(keystr).toString() keystr = QKeySequence(keystr).toString()
return keystr return keystr
def delayed_exec(self, command, count):
"""Execute a delayed command.
Args:
command/count: As if passed to self.execute()
Emit:
keystring_updated to do a delayed update.
"""
logging.debug("Executing delayed command now!")
self._timer = None
self._keystring = ''
self.keystring_updated.emit(self._keystring)
self.execute(command, count)
def execute(self, cmdstr, count=None): def execute(self, cmdstr, count=None):
"""Execute an action when a binding is triggered.""" """Execute an action when a binding is triggered."""
raise NotImplementedError raise NotImplementedError