Refined the widget forwarding mechanism for key forwarding. Added documentation, formatting, etc.
This commit is contained in:
parent
f114aacad2
commit
2956c5f2c3
|
|
@ -1101,7 +1101,7 @@ class AbstractTab(QWidget):
|
||||||
self._load_status = val
|
self._load_status = val
|
||||||
self.load_status_changed.emit(val)
|
self.load_status_changed.emit(val)
|
||||||
|
|
||||||
def send_event(self, evt: QEvent) -> None:
|
def post_event(self, evt: QEvent) -> None:
|
||||||
"""Send the given event to the underlying widget.
|
"""Send the given event to the underlying widget.
|
||||||
|
|
||||||
The event will be sent via QApplication.postEvent.
|
The event will be sent via QApplication.postEvent.
|
||||||
|
|
@ -1122,6 +1122,28 @@ class AbstractTab(QWidget):
|
||||||
evt.posted = True # type: ignore[attr-defined]
|
evt.posted = True # type: ignore[attr-defined]
|
||||||
QApplication.postEvent(recipient, evt)
|
QApplication.postEvent(recipient, evt)
|
||||||
|
|
||||||
|
def send_event(self, evt: QEvent) -> bool:
|
||||||
|
"""Send the given event to the underlying widget.
|
||||||
|
|
||||||
|
The event will be sent via QApplication.sendEvent.
|
||||||
|
Note that a sent event is not deleted after return.
|
||||||
|
Note that a posted event must not be re-used in any way!
|
||||||
|
"""
|
||||||
|
# This only gives us some mild protection against re-using events, but
|
||||||
|
# it's certainly better than a segfault.
|
||||||
|
if getattr(evt, 'posted', False):
|
||||||
|
raise utils.Unreachable("Can't re-use an event which was already "
|
||||||
|
"posted!")
|
||||||
|
|
||||||
|
recipient = self.private_api.event_target()
|
||||||
|
if recipient is None:
|
||||||
|
# https://github.com/qutebrowser/qutebrowser/issues/3888
|
||||||
|
log.webview.warning("Unable to find event target!")
|
||||||
|
return
|
||||||
|
|
||||||
|
evt.posted = True # type: ignore[attr-defined]
|
||||||
|
return QApplication.sendEvent(recipient, evt)
|
||||||
|
|
||||||
def navigation_blocked(self) -> bool:
|
def navigation_blocked(self) -> bool:
|
||||||
"""Test if navigation is allowed on the current tab."""
|
"""Test if navigation is allowed on the current tab."""
|
||||||
return self.data.pinned and config.val.tabs.pinned.frozen
|
return self.data.pinned and config.val.tabs.pinned.frozen
|
||||||
|
|
@ -1253,8 +1275,8 @@ class AbstractTab(QWidget):
|
||||||
press_evt = QKeyEvent(QEvent.Type.KeyPress, key, modifier, 0, 0, 0)
|
press_evt = QKeyEvent(QEvent.Type.KeyPress, key, modifier, 0, 0, 0)
|
||||||
release_evt = QKeyEvent(QEvent.Type.KeyRelease, key, modifier,
|
release_evt = QKeyEvent(QEvent.Type.KeyRelease, key, modifier,
|
||||||
0, 0, 0)
|
0, 0, 0)
|
||||||
self.send_event(press_evt)
|
self.post_event(press_evt)
|
||||||
self.send_event(release_evt)
|
self.post_event(release_evt)
|
||||||
|
|
||||||
def dump_async(self,
|
def dump_async(self,
|
||||||
callback: Callable[[str], None], *,
|
callback: Callable[[str], None], *,
|
||||||
|
|
|
||||||
|
|
@ -1781,8 +1781,8 @@ class CommandDispatcher:
|
||||||
QApplication.postEvent(window, release_event)
|
QApplication.postEvent(window, release_event)
|
||||||
else:
|
else:
|
||||||
tab = self._current_widget()
|
tab = self._current_widget()
|
||||||
tab.send_event(press_event)
|
tab.post_event(press_event)
|
||||||
tab.send_event(release_event)
|
tab.post_event(release_event)
|
||||||
|
|
||||||
@cmdutils.register(instance='command-dispatcher', scope='window',
|
@cmdutils.register(instance='command-dispatcher', scope='window',
|
||||||
debug=True, backend=usertypes.Backend.QtWebKit)
|
debug=True, backend=usertypes.Backend.QtWebKit)
|
||||||
|
|
|
||||||
|
|
@ -443,7 +443,7 @@ class AbstractWebElement(collections.abc.MutableMapping): # type: ignore[type-a
|
||||||
pos = self._mouse_pos()
|
pos = self._mouse_pos()
|
||||||
event = QMouseEvent(QEvent.Type.MouseMove, pos, Qt.MouseButton.NoButton, Qt.MouseButton.NoButton,
|
event = QMouseEvent(QEvent.Type.MouseMove, pos, Qt.MouseButton.NoButton, Qt.MouseButton.NoButton,
|
||||||
Qt.KeyboardModifier.NoModifier)
|
Qt.KeyboardModifier.NoModifier)
|
||||||
self._tab.send_event(event)
|
self._tab.post_event(event)
|
||||||
|
|
||||||
def right_click(self) -> None:
|
def right_click(self) -> None:
|
||||||
"""Simulate a right-click on the element."""
|
"""Simulate a right-click on the element."""
|
||||||
|
|
|
||||||
|
|
@ -148,12 +148,24 @@ class BaseKeyParser(QObject):
|
||||||
bindings: Bound key bindings
|
bindings: Bound key bindings
|
||||||
_mode: The usertypes.KeyMode associated with this keyparser.
|
_mode: The usertypes.KeyMode associated with this keyparser.
|
||||||
_win_id: The window ID this keyparser is associated with.
|
_win_id: The window ID this keyparser is associated with.
|
||||||
|
_pure_sequence: The currently entered key sequence (exactly as typed,
|
||||||
|
no substitutions performed)
|
||||||
_sequence: The currently entered key sequence
|
_sequence: The currently entered key sequence
|
||||||
|
_count: The currently entered count
|
||||||
|
_count_keyposs: Locations of count characters in the typed sequence
|
||||||
|
(self._count[i] was typed before
|
||||||
|
self._pure_sequence[self._count_keyposs[i]])
|
||||||
_do_log: Whether to log keypresses or not.
|
_do_log: Whether to log keypresses or not.
|
||||||
passthrough: Whether unbound keys should be passed through with this
|
passthrough: Whether unbound keys should be passed through with this
|
||||||
handler.
|
handler.
|
||||||
_supports_count: Whether count is supported.
|
_supports_count: Whether count is supported.
|
||||||
_partial_timer: Timer to clear partial keypresses.
|
allow_partial_timeout: Whether this key parser allows for partial keys
|
||||||
|
to be forwarded after a timeout.
|
||||||
|
allow_forward: Whether this key parser allows for unmatched partial
|
||||||
|
keys to be forwarded to underlying widgets.
|
||||||
|
forward_widget_name: Name of the widget to which partial keys are
|
||||||
|
forwarded. If None, the browser's current widget
|
||||||
|
is used.
|
||||||
|
|
||||||
Signals:
|
Signals:
|
||||||
keystring_updated: Emitted when the keystring is updated.
|
keystring_updated: Emitted when the keystring is updated.
|
||||||
|
|
@ -162,8 +174,11 @@ class BaseKeyParser(QObject):
|
||||||
arg 0: Mode to leave.
|
arg 0: Mode to leave.
|
||||||
arg 1: Reason for leaving.
|
arg 1: Reason for leaving.
|
||||||
arg 2: Ignore the request if we're not in that mode
|
arg 2: Ignore the request if we're not in that mode
|
||||||
|
forward_partial_key: Emitted when a partial key should be forwarded.
|
||||||
|
arg: Text expected to be forwarded (used solely
|
||||||
|
for debug info, default is None).
|
||||||
|
clear_partial_keys: Emitted to clear recorded partial keys.
|
||||||
"""
|
"""
|
||||||
#TODO: partial docs
|
|
||||||
|
|
||||||
keystring_updated = pyqtSignal(str)
|
keystring_updated = pyqtSignal(str)
|
||||||
request_leave = pyqtSignal(usertypes.KeyMode, str, bool)
|
request_leave = pyqtSignal(usertypes.KeyMode, str, bool)
|
||||||
|
|
@ -176,11 +191,13 @@ class BaseKeyParser(QObject):
|
||||||
do_log: bool = True,
|
do_log: bool = True,
|
||||||
passthrough: bool = False,
|
passthrough: bool = False,
|
||||||
supports_count: bool = True,
|
supports_count: bool = True,
|
||||||
allow_partial_timeout: bool = False) -> None:
|
allow_partial_timeout: bool = False,
|
||||||
|
allow_forward: bool = True,
|
||||||
|
forward_widget_name: str = None) -> None:
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
self._win_id = win_id
|
self._win_id = win_id
|
||||||
self._sequence = keyutils.KeySequence()
|
|
||||||
self._pure_sequence = keyutils.KeySequence()
|
self._pure_sequence = keyutils.KeySequence()
|
||||||
|
self._sequence = keyutils.KeySequence()
|
||||||
self._count = ''
|
self._count = ''
|
||||||
self._count_keyposs = []
|
self._count_keyposs = []
|
||||||
self._mode = mode
|
self._mode = mode
|
||||||
|
|
@ -188,6 +205,8 @@ class BaseKeyParser(QObject):
|
||||||
self.passthrough = passthrough
|
self.passthrough = passthrough
|
||||||
self._supports_count = supports_count
|
self._supports_count = supports_count
|
||||||
self.allow_partial_timeout = allow_partial_timeout
|
self.allow_partial_timeout = allow_partial_timeout
|
||||||
|
self.allow_forward = allow_forward
|
||||||
|
self.forward_widget_name = forward_widget_name
|
||||||
self.bindings = BindingTrie()
|
self.bindings = BindingTrie()
|
||||||
self._read_config()
|
self._read_config()
|
||||||
config.instance.changed.connect(self._on_config_changed)
|
config.instance.changed.connect(self._on_config_changed)
|
||||||
|
|
@ -291,9 +310,8 @@ class BaseKeyParser(QObject):
|
||||||
|
|
||||||
self._debug_log(f"Got key: {info!r} (dry_run {dry_run})")
|
self._debug_log(f"Got key: {info!r} (dry_run {dry_run})")
|
||||||
|
|
||||||
if info.is_modifier_key():
|
# Modifier keys handled in modeman
|
||||||
self._debug_log("Ignoring, only modifier")
|
assert not keyutils.is_modifier_key(key)
|
||||||
return QKeySequence.SequenceMatch.NoMatch
|
|
||||||
|
|
||||||
had_empty_queue = (not self._pure_sequence) and (not self._count)
|
had_empty_queue = (not self._pure_sequence) and (not self._count)
|
||||||
|
|
||||||
|
|
@ -304,8 +322,9 @@ class BaseKeyParser(QObject):
|
||||||
self.clear_keystring()
|
self.clear_keystring()
|
||||||
return QKeySequence.SequenceMatch.NoMatch
|
return QKeySequence.SequenceMatch.NoMatch
|
||||||
|
|
||||||
|
flag0 = True
|
||||||
# Have these shadow variables to have replicable behavior when doing a
|
# Have these shadow variables to have replicable behavior when doing a
|
||||||
# dry_run
|
# dry run
|
||||||
count = self._count
|
count = self._count
|
||||||
count_keyposs = self._count_keyposs.copy()
|
count_keyposs = self._count_keyposs.copy()
|
||||||
while pure_sequence:
|
while pure_sequence:
|
||||||
|
|
@ -320,7 +339,8 @@ class BaseKeyParser(QObject):
|
||||||
"mappings.".format(result.sequence))
|
"mappings.".format(result.sequence))
|
||||||
seq_len = len(result.sequence)
|
seq_len = len(result.sequence)
|
||||||
result = self._match_key_mapping(result.sequence)
|
result = self._match_key_mapping(result.sequence)
|
||||||
if result.match_type == QKeySequence.SequenceMatch.NoMatch:
|
if (result.match_type == QKeySequence.SequenceMatch.NoMatch) and flag0:
|
||||||
|
flag0 = False
|
||||||
# this length check is to ensure that key mappings from the
|
# this length check is to ensure that key mappings from the
|
||||||
# _match_key_mapping call that directly convert a single key to
|
# _match_key_mapping call that directly convert a single key to
|
||||||
# a numeral character are allowed to be recognized as counts.
|
# a numeral character are allowed to be recognized as counts.
|
||||||
|
|
@ -342,45 +362,48 @@ class BaseKeyParser(QObject):
|
||||||
"matching will be attempted.".format(
|
"matching will be attempted.".format(
|
||||||
result.sequence))
|
result.sequence))
|
||||||
if not dry_run:
|
if not dry_run:
|
||||||
|
# Update state variables
|
||||||
self._sequence = result.sequence
|
self._sequence = result.sequence
|
||||||
self._pure_sequence = pure_sequence
|
self._pure_sequence = pure_sequence
|
||||||
# TODO: forwarding debug log message
|
|
||||||
if result.match_type:
|
if result.match_type:
|
||||||
break
|
break
|
||||||
else:
|
assert pure_sequence
|
||||||
# TODO ensure all actual values are shadowed properly for dry_run
|
if not had_empty_queue:
|
||||||
if not had_empty_queue:
|
self._debug_log("No match for '{}'. Will forward first "
|
||||||
self._debug_log("No match for '{}'. Will forward first "
|
"key in the sequence and retry.".format(
|
||||||
"key in the sequence and retry.".format(
|
result.sequence))
|
||||||
result.sequence))
|
# Forward all the leading count keys
|
||||||
# TODO: empty handling (and find all others and fix)
|
while count_keyposs and (0 == count_keyposs[0]):
|
||||||
while count_keyposs and (0 == count_keyposs[0]):
|
self._debug_log("Hit a queued count key ('{}'). "
|
||||||
self._debug_log("Hit a queued count key ('{}'). "
|
"Forwarding.".format(count[0]))
|
||||||
"Forwarding.".format(count[0]))
|
count = count[1:]
|
||||||
count = count[1:]
|
count_keyposs.pop(0)
|
||||||
count_keyposs.pop(0)
|
|
||||||
if not dry_run:
|
|
||||||
self.forward_partial_key.emit(self._count[0])
|
|
||||||
# TODO: remove all = [1:] and so on with pops instead for non-strings
|
|
||||||
# TODO: check that matching is unaffected by count changing, e.g. for dry_runs
|
|
||||||
self._count = self._count[1:]
|
|
||||||
self._count_keyposs.pop(0)
|
|
||||||
# TODO: TODO ensure the keystring is updated after this
|
|
||||||
self._debug_log("Forwarding first key in sequence "
|
|
||||||
"('{}').".format(str(pure_sequence[0])))
|
|
||||||
count_keyposs = [x - 1 for x in count_keyposs]
|
|
||||||
if not dry_run:
|
if not dry_run:
|
||||||
self._count_keyposs = [x - 1 for x in self._count_keyposs]
|
self.forward_partial_key.emit(self._count[0])
|
||||||
# TODO: TODO ensure there's always a 0th element here
|
self._count = self._count[1:]
|
||||||
self.forward_partial_key.emit(str(self._pure_sequence[0]))
|
self._count_keyposs.pop(0)
|
||||||
else:
|
self._debug_log("Forwarding first key in sequence "
|
||||||
self._debug_log("No partial keys in queue. Continuing.")
|
"('{}').".format(str(pure_sequence[0])))
|
||||||
pure_sequence = pure_sequence[1:]
|
# Update the count_keyposs to reflect the shortened
|
||||||
# TODO: TODO check if on next loop a count could've slipped in somehow
|
# pure_sequence
|
||||||
|
count_keyposs = [x - 1 for x in count_keyposs]
|
||||||
|
if not dry_run:
|
||||||
|
self._count_keyposs = [x - 1 for x in self._count_keyposs]
|
||||||
|
self.forward_partial_key.emit(str(self._pure_sequence[0]))
|
||||||
|
else:
|
||||||
|
self._debug_log("No partial keys in queue. Continuing.")
|
||||||
|
pure_sequence = pure_sequence[1:]
|
||||||
|
# self._pure_sequence is updated either on next loop in the 'Update
|
||||||
|
# state variables' block or (if pure_sequence is empty and there is
|
||||||
|
# no next loop) in the self.clear_keystring call in the NoMatch
|
||||||
|
# block below
|
||||||
|
|
||||||
if dry_run:
|
if dry_run:
|
||||||
return result.match_type
|
return result.match_type
|
||||||
|
|
||||||
|
# Each of the three following blocks need to emit
|
||||||
|
# self.keystring_updated, either directly (as PartialMatch does) or
|
||||||
|
# indirectly (as ExactMatch and NoMatch do via self.clear_keystring)
|
||||||
if result.match_type == QKeySequence.SequenceMatch.ExactMatch:
|
if result.match_type == QKeySequence.SequenceMatch.ExactMatch:
|
||||||
assert result.command is not None
|
assert result.command is not None
|
||||||
self._debug_log("Definitive match for '{}'.".format(
|
self._debug_log("Definitive match for '{}'.".format(
|
||||||
|
|
@ -437,11 +460,11 @@ class BaseKeyParser(QObject):
|
||||||
self._count))
|
self._count))
|
||||||
self._count = ''
|
self._count = ''
|
||||||
self._count_keyposs = []
|
self._count_keyposs = []
|
||||||
# TODO: better handling of clearing managing _pure_sequence length
|
# self._pure_sequence should non-empty if and only if self._sequence is
|
||||||
if self._pure_sequence:
|
# non-empty, but to be safe both conditions are included below
|
||||||
self._pure_sequence = keyutils.KeySequence()
|
if self._pure_sequence or self._sequence:
|
||||||
if self._sequence:
|
|
||||||
self._debug_log("Clearing keystring (was: {}).".format(
|
self._debug_log("Clearing keystring (was: {}).".format(
|
||||||
self._sequence))
|
self._sequence))
|
||||||
|
self._pure_sequence = keyutils.KeySequence()
|
||||||
self._sequence = keyutils.KeySequence()
|
self._sequence = keyutils.KeySequence()
|
||||||
self.keystring_updated.emit('')
|
self.keystring_updated.emit('')
|
||||||
|
|
|
||||||
|
|
@ -530,7 +530,6 @@ class KeyEvent:
|
||||||
@dataclasses.dataclass(frozen=False)
|
@dataclasses.dataclass(frozen=False)
|
||||||
class QueuedKeyEventPair:
|
class QueuedKeyEventPair:
|
||||||
|
|
||||||
# TODO: docs
|
|
||||||
"""A wrapper over a QKeyEvent capable of recreating the event.
|
"""A wrapper over a QKeyEvent capable of recreating the event.
|
||||||
|
|
||||||
This is needed to recreate any queued events when either a timeout occurs
|
This is needed to recreate any queued events when either a timeout occurs
|
||||||
|
|
@ -538,9 +537,10 @@ class QueuedKeyEventPair:
|
||||||
|
|
||||||
Attributes:
|
Attributes:
|
||||||
key_event: A KeyEvent member for comparison.
|
key_event: A KeyEvent member for comparison.
|
||||||
key_info: A keyutils.KeyInfo member for complete event reconstruction
|
key_info_press: A KeyInfo member for complete event reconstruction
|
||||||
(e.g. with modifiers).
|
(e.g. with modifiers) corresponding to the press event.
|
||||||
typ: QEvent.KeyPress or QEvent.KeyRelease.
|
key_info_release: A KeyInfo member for complete event reconstruction
|
||||||
|
(e.g. with modifiers) corresponding to the release event.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
key_event: KeyEvent
|
key_event: KeyEvent
|
||||||
|
|
@ -553,12 +553,13 @@ class QueuedKeyEventPair:
|
||||||
return cls(KeyEvent.from_event(event), KeyInfo.from_event(event), None)
|
return cls(KeyEvent.from_event(event), KeyInfo.from_event(event), None)
|
||||||
|
|
||||||
def add_event_release(self, event: QKeyEvent) -> bool:
|
def add_event_release(self, event: QKeyEvent) -> bool:
|
||||||
|
"""Attempt to add a release event. Returns True if successful."""
|
||||||
if self.key_event == KeyEvent.from_event(event):
|
if self.key_event == KeyEvent.from_event(event):
|
||||||
self.key_info_release = KeyInfo.from_event(event)
|
self.key_info_release = KeyInfo.from_event(event)
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def is_released(self):
|
def is_released(self) -> bool:
|
||||||
return self.key_info_release is not None
|
return self.key_info_release is not None
|
||||||
|
|
||||||
def to_events(self) -> Tuple[QKeyEvent]:
|
def to_events(self) -> Tuple[QKeyEvent]:
|
||||||
|
|
|
||||||
|
|
@ -102,7 +102,9 @@ def init(win_id: int, parent: QObject) -> 'ModeManager':
|
||||||
passthrough=True,
|
passthrough=True,
|
||||||
do_log=log_sensitive_keys,
|
do_log=log_sensitive_keys,
|
||||||
supports_count=False,
|
supports_count=False,
|
||||||
allow_partial_timeout=True),
|
allow_partial_timeout=True,
|
||||||
|
allow_forward=True,
|
||||||
|
forward_widget_name='status-command'),
|
||||||
|
|
||||||
usertypes.KeyMode.prompt:
|
usertypes.KeyMode.prompt:
|
||||||
modeparsers.CommandKeyParser(
|
modeparsers.CommandKeyParser(
|
||||||
|
|
@ -113,7 +115,12 @@ def init(win_id: int, parent: QObject) -> 'ModeManager':
|
||||||
passthrough=True,
|
passthrough=True,
|
||||||
do_log=log_sensitive_keys,
|
do_log=log_sensitive_keys,
|
||||||
supports_count=False,
|
supports_count=False,
|
||||||
allow_partial_timeout=True),
|
# Maybe in the future implement this, but for the time being its
|
||||||
|
# infeasible as 'prompt-container' is registered as command-only.
|
||||||
|
# Plus, I imagine the use case for such a thing is quite rare.
|
||||||
|
allow_forward=False,
|
||||||
|
forward_widget_name=None, #'prompt-container'
|
||||||
|
allow_partial_timeout=False),
|
||||||
|
|
||||||
usertypes.KeyMode.yesno:
|
usertypes.KeyMode.yesno:
|
||||||
modeparsers.CommandKeyParser(
|
modeparsers.CommandKeyParser(
|
||||||
|
|
@ -122,7 +129,10 @@ def init(win_id: int, parent: QObject) -> 'ModeManager':
|
||||||
commandrunner=commandrunner,
|
commandrunner=commandrunner,
|
||||||
parent=modeman,
|
parent=modeman,
|
||||||
supports_count=False,
|
supports_count=False,
|
||||||
allow_partial_timeout=True),
|
# Similar story to prompt mode
|
||||||
|
allow_forward=False,
|
||||||
|
forward_widget_name=None,
|
||||||
|
allow_partial_timeout=False),
|
||||||
|
|
||||||
usertypes.KeyMode.caret:
|
usertypes.KeyMode.caret:
|
||||||
modeparsers.CommandKeyParser(
|
modeparsers.CommandKeyParser(
|
||||||
|
|
@ -211,6 +221,8 @@ class ModeManager(QObject):
|
||||||
_releaseevents_to_pass: A set of KeyEvents where the keyPressEvent was
|
_releaseevents_to_pass: A set of KeyEvents where the keyPressEvent was
|
||||||
passed through, so the release event should as
|
passed through, so the release event should as
|
||||||
well.
|
well.
|
||||||
|
_partial_timer: The timer which forwards partial keys after no key has
|
||||||
|
been pressed for a timeout period.
|
||||||
|
|
||||||
Signals:
|
Signals:
|
||||||
entered: Emitted when a mode is entered.
|
entered: Emitted when a mode is entered.
|
||||||
|
|
@ -220,10 +232,15 @@ class ModeManager(QObject):
|
||||||
arg1: The mode which has been left.
|
arg1: The mode which has been left.
|
||||||
arg2: The new current mode.
|
arg2: The new current mode.
|
||||||
arg3: The window ID of this mode manager.
|
arg3: The window ID of this mode manager.
|
||||||
keystring_updated: Emitted when the keystring was updated in any mode.
|
keystring_updated: Emitted when the keystring was updated in any mode.
|
||||||
arg 1: The mode in which the keystring has been
|
arg1: The mode in which the keystring has been
|
||||||
updated.
|
updated.
|
||||||
arg 2: The new key string.
|
arg2: The new key string.
|
||||||
|
forward_partial_key: Emitted when a partial key should be forwarded.
|
||||||
|
arg1: The mode in which the partial key was
|
||||||
|
pressed.
|
||||||
|
arg2: Text expected to be forwarded (used solely
|
||||||
|
for debug info, default is None).
|
||||||
"""
|
"""
|
||||||
|
|
||||||
entered = pyqtSignal(usertypes.KeyMode, int)
|
entered = pyqtSignal(usertypes.KeyMode, int)
|
||||||
|
|
@ -240,8 +257,7 @@ class ModeManager(QObject):
|
||||||
self._releaseevents_to_pass: set[keyutils.KeyEvent] = set()
|
self._releaseevents_to_pass: set[keyutils.KeyEvent] = set()
|
||||||
# Set after __init__
|
# Set after __init__
|
||||||
self.hintmanager = cast(hints.HintManager, None)
|
self.hintmanager = cast(hints.HintManager, None)
|
||||||
# TODO: type hints
|
self._partial_match_events: Sequence[keyutils.QueuedKeyEventPair] = []
|
||||||
self._partial_match_events = []
|
|
||||||
self.forward_partial_key.connect(self.forward_partial_match_event)
|
self.forward_partial_key.connect(self.forward_partial_match_event)
|
||||||
self._partial_timer = usertypes.Timer(self, 'partial-match')
|
self._partial_timer = usertypes.Timer(self, 'partial-match')
|
||||||
self._partial_timer.setSingleShot(True)
|
self._partial_timer.setSingleShot(True)
|
||||||
|
|
@ -267,13 +283,24 @@ class ModeManager(QObject):
|
||||||
"{}".format(curmode, utils.qualname(parser)))
|
"{}".format(curmode, utils.qualname(parser)))
|
||||||
|
|
||||||
had_empty_queue = not self._partial_match_events
|
had_empty_queue = not self._partial_match_events
|
||||||
if (not dry_run) and (not had_empty_queue):
|
if parser.allow_forward and (not dry_run) and (not had_empty_queue):
|
||||||
|
# Immediately record the event so that parser.handle may forward if
|
||||||
|
# appropriate from its logic.
|
||||||
self._partial_match_events.append(
|
self._partial_match_events.append(
|
||||||
keyutils.QueuedKeyEventPair.from_event_press(event))
|
keyutils.QueuedKeyEventPair.from_event_press(event))
|
||||||
|
|
||||||
match = parser.handle(event, dry_run=dry_run)
|
if keyutils.is_modifier_key(Qt.Key(event.key())):
|
||||||
|
if curmode != usertypes.KeyMode.insert:
|
||||||
|
log.modes.debug("Ignoring, only modifier")
|
||||||
|
if not dry_run:
|
||||||
|
# Since this is a NoMatch without a call to parser.handle, we
|
||||||
|
# must manually forward the events
|
||||||
|
self.forward_all_partial_match_events(self.mode,
|
||||||
|
stop_timer=True)
|
||||||
|
match = QKeySequence.NoMatch
|
||||||
|
else:
|
||||||
|
match = parser.handle(event, dry_run=dry_run)
|
||||||
|
|
||||||
# TODO: Check dry_run conditions are everywhere
|
|
||||||
if match == QKeySequence.SequenceMatch.ExactMatch:
|
if match == QKeySequence.SequenceMatch.ExactMatch:
|
||||||
filter_this = True
|
filter_this = True
|
||||||
if not dry_run:
|
if not dry_run:
|
||||||
|
|
@ -281,19 +308,21 @@ class ModeManager(QObject):
|
||||||
self.clear_partial_match_events()
|
self.clear_partial_match_events()
|
||||||
elif match == QKeySequence.SequenceMatch.PartialMatch:
|
elif match == QKeySequence.SequenceMatch.PartialMatch:
|
||||||
filter_this = True
|
filter_this = True
|
||||||
if not dry_run:
|
if parser.allow_forward and (not dry_run):
|
||||||
if had_empty_queue:
|
if had_empty_queue:
|
||||||
|
# Begin recording partial match events
|
||||||
self._partial_match_events.append(
|
self._partial_match_events.append(
|
||||||
keyutils.QueuedKeyEventPair.from_event_press(event))
|
keyutils.QueuedKeyEventPair.from_event_press(event))
|
||||||
self._start_partial_timer()
|
self._start_partial_timer()
|
||||||
elif not had_empty_queue:
|
elif not had_empty_queue:
|
||||||
|
# Since partial events were recorded, this event must be filtered.
|
||||||
|
# Since a NoMatch was found, this event has already been forwarded
|
||||||
filter_this = True
|
filter_this = True
|
||||||
if not dry_run:
|
if not dry_run:
|
||||||
self._stop_partial_timer()
|
self._stop_partial_timer()
|
||||||
# TODO: spacing and tabbing and formatting
|
|
||||||
else:
|
else:
|
||||||
key_info = keyutils.KeyInfo.from_event(event)
|
key_info = keyutils.KeyInfo.from_event(event)
|
||||||
filter_this = not self._should_forward_event(key_info, parser)
|
filter_this = self._should_filter_event(key_info, parser)
|
||||||
|
|
||||||
if not filter_this and not dry_run:
|
if not filter_this and not dry_run:
|
||||||
self._releaseevents_to_pass.add(keyutils.KeyEvent.from_event(event))
|
self._releaseevents_to_pass.add(keyutils.KeyEvent.from_event(event))
|
||||||
|
|
@ -313,7 +342,7 @@ class ModeManager(QObject):
|
||||||
is_non_alnum = has_modifier or not event.text().strip()
|
is_non_alnum = has_modifier or not event.text().strip()
|
||||||
forward_unbound_keys = config.cache['input.forward_unbound_keys']
|
forward_unbound_keys = config.cache['input.forward_unbound_keys']
|
||||||
key_info = keyutils.KeyInfo.from_event(event)
|
key_info = keyutils.KeyInfo.from_event(event)
|
||||||
should_forward_event = self._should_forward_event(key_info, parser)
|
should_filter_event = self._should_filter_event(key_info, parser)
|
||||||
focus_widget = objects.qapp.focusWidget()
|
focus_widget = objects.qapp.focusWidget()
|
||||||
log.modes.debug("match: {}, forward_unbound_keys: {}, "
|
log.modes.debug("match: {}, forward_unbound_keys: {}, "
|
||||||
"passthrough: {}, is_non_alnum: {}, "
|
"passthrough: {}, is_non_alnum: {}, "
|
||||||
|
|
@ -321,7 +350,7 @@ class ModeManager(QObject):
|
||||||
"--> filter: {} (focused: {!r})".format(
|
"--> filter: {} (focused: {!r})".format(
|
||||||
match, forward_unbound_keys,
|
match, forward_unbound_keys,
|
||||||
parser.passthrough, is_non_alnum,
|
parser.passthrough, is_non_alnum,
|
||||||
should_forward_event, dry_run, filter_this,
|
should_filter_event, dry_run, filter_this,
|
||||||
qtutils.qobj_repr(focus_widget)))
|
qtutils.qobj_repr(focus_widget)))
|
||||||
return filter_this
|
return filter_this
|
||||||
|
|
||||||
|
|
@ -340,6 +369,8 @@ class ModeManager(QObject):
|
||||||
self._releaseevents_to_pass.remove(keyevent)
|
self._releaseevents_to_pass.remove(keyevent)
|
||||||
filter_this = False
|
filter_this = False
|
||||||
else:
|
else:
|
||||||
|
# Record the releases for partial matches to later forward along
|
||||||
|
# with the presses
|
||||||
for match_event in self._partial_match_events[::-1]:
|
for match_event in self._partial_match_events[::-1]:
|
||||||
if match_event.add_event_release(event):
|
if match_event.add_event_release(event):
|
||||||
break
|
break
|
||||||
|
|
@ -349,7 +380,9 @@ class ModeManager(QObject):
|
||||||
return filter_this
|
return filter_this
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _should_forward_event(key_info, parser):
|
def _should_filter_event(key_info: keyutils.KeyInfo,
|
||||||
|
parser: basekeyparser.BaseKeyParser) -> bool:
|
||||||
|
"""Returns True if the event should be filtered, False otherwise."""
|
||||||
if machinery.IS_QT5: # FIXME:v4 needed for Qt 5 typing
|
if machinery.IS_QT5: # FIXME:v4 needed for Qt 5 typing
|
||||||
ignored_modifiers = [
|
ignored_modifiers = [
|
||||||
cast(Qt.KeyboardModifiers, Qt.KeyboardModifier.NoModifier),
|
cast(Qt.KeyboardModifiers, Qt.KeyboardModifier.NoModifier),
|
||||||
|
|
@ -363,42 +396,79 @@ class ModeManager(QObject):
|
||||||
has_modifier = key_info.modifiers not in ignored_modifiers
|
has_modifier = key_info.modifiers not in ignored_modifiers
|
||||||
is_non_alnum = has_modifier or not key_info.text().strip()
|
is_non_alnum = has_modifier or not key_info.text().strip()
|
||||||
forward_unbound_keys = config.cache['input.forward_unbound_keys']
|
forward_unbound_keys = config.cache['input.forward_unbound_keys']
|
||||||
return (parser.passthrough or forward_unbound_keys == 'all' or
|
return not (parser.passthrough or forward_unbound_keys == 'all' or
|
||||||
(forward_unbound_keys == 'auto' and is_non_alnum)) and (
|
(forward_unbound_keys == 'auto' and is_non_alnum))
|
||||||
not isinstance(parser, modeparsers.HintKeyParser))
|
|
||||||
|
|
||||||
@pyqtSlot(usertypes.KeyMode, str)
|
@pyqtSlot(usertypes.KeyMode, str)
|
||||||
def forward_partial_match_event(self, mode: usertypes.KeyMode, text: str = None) -> None:
|
def forward_partial_match_event(self, mode: usertypes.KeyMode,
|
||||||
# TODO: add debug messages
|
text: str = None) -> None:
|
||||||
#self._debug_log("Clearing partial keystring {}".format(
|
"""Forward the oldest partial match event for a given mode
|
||||||
# self._sequence))
|
|
||||||
# TODO: Check for transient self.whatever statements (e.g. self.mode) in slots and remove, might not be thread-safe
|
Args:
|
||||||
|
mode: The mode from which the forwarded match is.
|
||||||
|
text: The expected text to be forwarded. Only used for debug
|
||||||
|
purposes. Default is None.
|
||||||
|
"""
|
||||||
if mode not in self.parsers:
|
if mode not in self.parsers:
|
||||||
raise ValueError("Can't forward partial key: No keyparser for "
|
raise ValueError("Can't forward partial key: No keyparser for "
|
||||||
"mode {}".format(mode))
|
"mode {}".format(mode))
|
||||||
parser = self.parsers[mode]
|
parser = self.parsers[mode]
|
||||||
if not self._partial_match_events:
|
if not self._partial_match_events:
|
||||||
# TODO: debug message
|
if parser.allow_forward:
|
||||||
|
log.modes.warning("Attempting to forward for mode {} "
|
||||||
|
"(expected text = {}), which should allow "
|
||||||
|
"forwarding, but there are no events to "
|
||||||
|
"forward.".format(mode, text))
|
||||||
return
|
return
|
||||||
match_event = self._partial_match_events.pop(0)
|
match_event = self._partial_match_events.pop(0)
|
||||||
# TODO: debug message when text and event.text don't match up, minding text may be None
|
if parser.allow_forward and (not
|
||||||
if self._should_forward_event(match_event.key_info_press, parser):
|
self._should_filter_event(match_event.key_info_press, parser)):
|
||||||
# TODO: review alternatives
|
if mode != usertypes.KeyMode.insert:
|
||||||
tabbed_browser = objreg.get('tabbed-browser', scope='window', window=QApplication.activeWindow().win_id)
|
log.modes.debug("Forwarding partial match event in mode "
|
||||||
tab = tabbed_browser.widget.currentWidget()
|
"{}.".format(mode))
|
||||||
|
text_actual = str(match_event.key_info_press)
|
||||||
|
if (text is not None) and (text_actual != text):
|
||||||
|
log.modes.debug("Text mismatch (this is likely benign): "
|
||||||
|
"'{}' != '{}'".format(text_actual, text))
|
||||||
|
# Get the widget to which the event will be forwarded
|
||||||
|
widget_name = parser.forward_widget_name
|
||||||
|
if widget_name is None:
|
||||||
|
# By default, the widget is the current widget of the browser
|
||||||
|
tabbed_browser = objreg.get('tabbed-browser', scope='window',
|
||||||
|
window=self._win_id)
|
||||||
|
tab = tabbed_browser.widget.currentWidget()
|
||||||
|
if tab is None:
|
||||||
|
raise cmdutils.CommandError("No WebView available yet!")
|
||||||
|
send_event = tab.send_event
|
||||||
|
else:
|
||||||
|
# When a specific widget is specified, QApplication.sendEvent
|
||||||
|
# is used
|
||||||
|
widget = objreg.get(widget_name, scope='window',
|
||||||
|
window=self._win_id)
|
||||||
|
send_event = functools.partial(QApplication.sendEvent, widget)
|
||||||
for event_ in match_event.to_events():
|
for event_ in match_event.to_events():
|
||||||
tab.send_event(event_)
|
send_event(event_)
|
||||||
if not match_event.is_released():
|
if not match_event.is_released():
|
||||||
self._releaseevents_to_pass.add(match_event.key_event)
|
self._releaseevents_to_pass.add(match_event.key_event)
|
||||||
|
|
||||||
@pyqtSlot(usertypes.KeyMode)
|
@pyqtSlot(usertypes.KeyMode)
|
||||||
def forward_all_partial_match_events(self, mode: usertypes.KeyMode, *,
|
def forward_all_partial_match_events(self, mode: usertypes.KeyMode, *,
|
||||||
stop_timer: bool = False) -> None:
|
stop_timer: bool = False) -> None:
|
||||||
|
"""Forward all partial match events for a given mode
|
||||||
|
|
||||||
|
Args:
|
||||||
|
mode: The mode from which the forwarded match is.
|
||||||
|
stop_timer: If true, stop the partial timer (and any nested timers)
|
||||||
|
as well. Default is False.
|
||||||
|
"""
|
||||||
|
log.modes.debug("Forwarding all partial matches.")
|
||||||
if stop_timer:
|
if stop_timer:
|
||||||
self._stop_partial_timer()
|
self._stop_partial_timer()
|
||||||
if mode in self.parsers:
|
if mode in self.parsers:
|
||||||
parser = self.parsers[mode]
|
parser = self.parsers[mode]
|
||||||
if isinstance(parser, modeparsers.HintKeyParser):
|
if isinstance(parser, modeparsers.HintKeyParser):
|
||||||
|
# Call the subparsers analogous function, propagating the timer
|
||||||
|
# stopping.
|
||||||
parser.forward_all_partial_match_events(stop_timer=True)
|
parser.forward_all_partial_match_events(stop_timer=True)
|
||||||
if self._partial_match_events:
|
if self._partial_match_events:
|
||||||
while self._partial_match_events:
|
while self._partial_match_events:
|
||||||
|
|
@ -424,7 +494,7 @@ class ModeManager(QObject):
|
||||||
except TypeError:
|
except TypeError:
|
||||||
pass
|
pass
|
||||||
self._partial_timer.timeout.connect(functools.partial(
|
self._partial_timer.timeout.connect(functools.partial(
|
||||||
self.forward_all_partial_match_events, self.mode))
|
self.forward_all_partial_match_events, self.mode))
|
||||||
self._partial_timer.start()
|
self._partial_timer.start()
|
||||||
|
|
||||||
def _stop_partial_timer(self) -> None:
|
def _stop_partial_timer(self) -> None:
|
||||||
|
|
@ -437,7 +507,6 @@ class ModeManager(QObject):
|
||||||
assert parser is not None
|
assert parser is not None
|
||||||
self.parsers[mode] = parser
|
self.parsers[mode] = parser
|
||||||
parser.request_leave.connect(self.leave)
|
parser.request_leave.connect(self.leave)
|
||||||
# TODO: maybe make keystring_updated a domino of forward_partial_key?
|
|
||||||
parser.keystring_updated.connect(
|
parser.keystring_updated.connect(
|
||||||
functools.partial(self.keystring_updated.emit, mode))
|
functools.partial(self.keystring_updated.emit, mode))
|
||||||
parser.forward_partial_key.connect(
|
parser.forward_partial_key.connect(
|
||||||
|
|
|
||||||
|
|
@ -52,11 +52,15 @@ class CommandKeyParser(basekeyparser.BaseKeyParser):
|
||||||
do_log: bool = True,
|
do_log: bool = True,
|
||||||
passthrough: bool = False,
|
passthrough: bool = False,
|
||||||
supports_count: bool = True,
|
supports_count: bool = True,
|
||||||
allow_partial_timeout: bool = True) -> None:
|
allow_partial_timeout: bool = True,
|
||||||
|
allow_forward: bool = True,
|
||||||
|
forward_widget_name: str = None) -> None:
|
||||||
super().__init__(mode=mode, win_id=win_id, parent=parent,
|
super().__init__(mode=mode, win_id=win_id, parent=parent,
|
||||||
do_log=do_log, passthrough=passthrough,
|
do_log=do_log, passthrough=passthrough,
|
||||||
supports_count=supports_count,
|
supports_count=supports_count,
|
||||||
allow_partial_timeout=allow_partial_timeout)
|
allow_partial_timeout=allow_partial_timeout,
|
||||||
|
allow_forward=allow_forward,
|
||||||
|
forward_widget_name=forward_widget_name)
|
||||||
self._commandrunner = commandrunner
|
self._commandrunner = commandrunner
|
||||||
|
|
||||||
def execute(self, cmdstr: str, count: int = None) -> None:
|
def execute(self, cmdstr: str, count: int = None) -> None:
|
||||||
|
|
@ -120,6 +124,8 @@ class HintKeyParser(basekeyparser.BaseKeyParser):
|
||||||
_filtertext: The text to filter with.
|
_filtertext: The text to filter with.
|
||||||
_hintmanager: The HintManager to use.
|
_hintmanager: The HintManager to use.
|
||||||
_last_press: The nature of the last keypress, a LastPress member.
|
_last_press: The nature of the last keypress, a LastPress member.
|
||||||
|
_partial_timer: The timer which forwards partial keys after no key has
|
||||||
|
been pressed for a timeout period.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
_sequence: keyutils.KeySequence
|
_sequence: keyutils.KeySequence
|
||||||
|
|
@ -130,7 +136,7 @@ class HintKeyParser(basekeyparser.BaseKeyParser):
|
||||||
parent: QObject = None) -> None:
|
parent: QObject = None) -> None:
|
||||||
super().__init__(mode=usertypes.KeyMode.hint, win_id=win_id,
|
super().__init__(mode=usertypes.KeyMode.hint, win_id=win_id,
|
||||||
parent=parent, supports_count=False,
|
parent=parent, supports_count=False,
|
||||||
allow_partial_timeout=False)
|
allow_partial_timeout=False, allow_forward=False)
|
||||||
self._command_parser = CommandKeyParser(mode=usertypes.KeyMode.hint,
|
self._command_parser = CommandKeyParser(mode=usertypes.KeyMode.hint,
|
||||||
win_id=win_id,
|
win_id=win_id,
|
||||||
commandrunner=commandrunner,
|
commandrunner=commandrunner,
|
||||||
|
|
@ -140,16 +146,16 @@ class HintKeyParser(basekeyparser.BaseKeyParser):
|
||||||
self._hintmanager = hintmanager
|
self._hintmanager = hintmanager
|
||||||
self._filtertext = ''
|
self._filtertext = ''
|
||||||
self._last_press = LastPress.none
|
self._last_press = LastPress.none
|
||||||
self._partial_match_events = []
|
self._partial_match_events: Sequence[keyutils.QueuedKeyEventPair] = []
|
||||||
self.keystring_updated.connect(self._hintmanager.handle_partial_key)
|
self.keystring_updated.connect(self._hintmanager.handle_partial_key)
|
||||||
self._command_parser.forward_partial_key.connect(
|
self._command_parser.forward_partial_key.connect(
|
||||||
self.forward_partial_match_event)
|
self.forward_partial_match_event)
|
||||||
self._command_parser.clear_partial_keys.connect(
|
self._command_parser.clear_partial_keys.connect(
|
||||||
self.clear_partial_match_events)
|
self.clear_partial_match_events)
|
||||||
self._partial_timer = usertypes.Timer(self, 'partial-match')
|
self._partial_timer = usertypes.Timer(self, 'partial-match')
|
||||||
self._partial_timer.setSingleShot(True)
|
self._partial_timer.setSingleShot(True)
|
||||||
self._partial_timer.timeout.connect(
|
self._partial_timer.timeout.connect(
|
||||||
self.forward_all_partial_match_events)
|
self.forward_all_partial_match_events)
|
||||||
|
|
||||||
def _handle_filter_key(self, e: QKeyEvent) -> QKeySequence.SequenceMatch:
|
def _handle_filter_key(self, e: QKeyEvent) -> QKeySequence.SequenceMatch:
|
||||||
"""Handle keys for string filtering."""
|
"""Handle keys for string filtering."""
|
||||||
|
|
@ -198,8 +204,10 @@ class HintKeyParser(basekeyparser.BaseKeyParser):
|
||||||
|
|
||||||
had_empty_queue = not self._partial_match_events
|
had_empty_queue = not self._partial_match_events
|
||||||
if not had_empty_queue:
|
if not had_empty_queue:
|
||||||
|
# Immediately record the event so that parser.handle may forward if
|
||||||
|
# appropriate from its logic.
|
||||||
self._partial_match_events.append(
|
self._partial_match_events.append(
|
||||||
keyutils.QueuedKeyEventPair.from_event_press(e))
|
keyutils.QueuedKeyEventPair.from_event_press(e))
|
||||||
|
|
||||||
result = self._command_parser.handle(e)
|
result = self._command_parser.handle(e)
|
||||||
if result == QKeySequence.SequenceMatch.ExactMatch:
|
if result == QKeySequence.SequenceMatch.ExactMatch:
|
||||||
|
|
@ -211,8 +219,9 @@ class HintKeyParser(basekeyparser.BaseKeyParser):
|
||||||
elif result == QKeySequence.SequenceMatch.PartialMatch:
|
elif result == QKeySequence.SequenceMatch.PartialMatch:
|
||||||
log.keyboard.debug("Handling key via command parser")
|
log.keyboard.debug("Handling key via command parser")
|
||||||
if had_empty_queue:
|
if had_empty_queue:
|
||||||
|
# Begin recording partial match events
|
||||||
self._partial_match_events.append(
|
self._partial_match_events.append(
|
||||||
keyutils.QueuedKeyEventPair.from_event_press(e))
|
keyutils.QueuedKeyEventPair.from_event_press(e))
|
||||||
self._start_partial_timer()
|
self._start_partial_timer()
|
||||||
return result
|
return result
|
||||||
elif not had_empty_queue:
|
elif not had_empty_queue:
|
||||||
|
|
@ -244,12 +253,22 @@ class HintKeyParser(basekeyparser.BaseKeyParser):
|
||||||
|
|
||||||
@pyqtSlot(str)
|
@pyqtSlot(str)
|
||||||
def forward_partial_match_event(self, text: str = None) -> None:
|
def forward_partial_match_event(self, text: str = None) -> None:
|
||||||
# TODO: add debug messages
|
"""Forward the oldest partial match event
|
||||||
|
|
||||||
|
Args:
|
||||||
|
text: The expected text to be forwarded. Only used for debug
|
||||||
|
purposes. Default is None.
|
||||||
|
"""
|
||||||
if not self._partial_match_events:
|
if not self._partial_match_events:
|
||||||
# TODO: debug message
|
self._debug_log("Attempting to forward (expected text = {}) but "
|
||||||
|
"there are no events to forward.".format(text))
|
||||||
return
|
return
|
||||||
match_event = self._partial_match_events.pop(0)
|
match_event = self._partial_match_events.pop(0)
|
||||||
# TODO: debug message when text and event.text don't match up, minding text may be None
|
self._debug_log("Forwarding partial match event.")
|
||||||
|
text_actual = str(match_event.key_info_press)
|
||||||
|
if (text is not None) and (text_actual != text):
|
||||||
|
self._debug_log("Text mismatch (this is likely benign): '{}' != "
|
||||||
|
"'{}'".format(text_actual, text))
|
||||||
e = match_event.to_events()
|
e = match_event.to_events()
|
||||||
assert 1 == len(e)
|
assert 1 == len(e)
|
||||||
e = e[0]
|
e = e[0]
|
||||||
|
|
@ -258,6 +277,12 @@ class HintKeyParser(basekeyparser.BaseKeyParser):
|
||||||
@pyqtSlot()
|
@pyqtSlot()
|
||||||
def forward_all_partial_match_events(self, *,
|
def forward_all_partial_match_events(self, *,
|
||||||
stop_timer: bool = False) -> None:
|
stop_timer: bool = False) -> None:
|
||||||
|
"""Forward all partial match events
|
||||||
|
|
||||||
|
Args:
|
||||||
|
stop_timer: If true, stop the partial timer as well. Default is False.
|
||||||
|
"""
|
||||||
|
self._debug_log("Forwarding all partial matches.")
|
||||||
if stop_timer:
|
if stop_timer:
|
||||||
self._stop_partial_timer()
|
self._stop_partial_timer()
|
||||||
if self._partial_match_events:
|
if self._partial_match_events:
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,6 @@ MAPPINGS = {
|
||||||
'e': 'd',
|
'e': 'd',
|
||||||
}
|
}
|
||||||
|
|
||||||
# TODO: ensure multiple-length mappings are safe
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def keyinput_bindings(config_stub, key_config_stub):
|
def keyinput_bindings(config_stub, key_config_stub):
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue