Support ambiguous keybindings
This commit is contained in:
parent
f38871c9c9
commit
aedf1889dd
@ -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(
|
||||||
|
@ -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
|
||||||
|
Loading…
Reference in New Issue
Block a user