Refined the widget forwarding mechanism for key forwarding. Added documentation, formatting, etc.

This commit is contained in:
brightonanc 2022-05-04 23:33:13 -04:00
parent f114aacad2
commit 2956c5f2c3
8 changed files with 244 additions and 105 deletions

View File

@ -1101,7 +1101,7 @@ class AbstractTab(QWidget):
self._load_status = 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.
The event will be sent via QApplication.postEvent.
@ -1122,6 +1122,28 @@ class AbstractTab(QWidget):
evt.posted = True # type: ignore[attr-defined]
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:
"""Test if navigation is allowed on the current tab."""
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)
release_evt = QKeyEvent(QEvent.Type.KeyRelease, key, modifier,
0, 0, 0)
self.send_event(press_evt)
self.send_event(release_evt)
self.post_event(press_evt)
self.post_event(release_evt)
def dump_async(self,
callback: Callable[[str], None], *,

View File

@ -1781,8 +1781,8 @@ class CommandDispatcher:
QApplication.postEvent(window, release_event)
else:
tab = self._current_widget()
tab.send_event(press_event)
tab.send_event(release_event)
tab.post_event(press_event)
tab.post_event(release_event)
@cmdutils.register(instance='command-dispatcher', scope='window',
debug=True, backend=usertypes.Backend.QtWebKit)

View File

@ -443,7 +443,7 @@ class AbstractWebElement(collections.abc.MutableMapping): # type: ignore[type-a
pos = self._mouse_pos()
event = QMouseEvent(QEvent.Type.MouseMove, pos, Qt.MouseButton.NoButton, Qt.MouseButton.NoButton,
Qt.KeyboardModifier.NoModifier)
self._tab.send_event(event)
self._tab.post_event(event)
def right_click(self) -> None:
"""Simulate a right-click on the element."""

View File

@ -148,12 +148,24 @@ class BaseKeyParser(QObject):
bindings: Bound key bindings
_mode: The usertypes.KeyMode associated with this keyparser.
_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
_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.
passthrough: Whether unbound keys should be passed through with this
handler.
_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:
keystring_updated: Emitted when the keystring is updated.
@ -162,8 +174,11 @@ class BaseKeyParser(QObject):
arg 0: Mode to leave.
arg 1: Reason for leaving.
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)
request_leave = pyqtSignal(usertypes.KeyMode, str, bool)
@ -176,11 +191,13 @@ class BaseKeyParser(QObject):
do_log: bool = True,
passthrough: bool = False,
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)
self._win_id = win_id
self._sequence = keyutils.KeySequence()
self._pure_sequence = keyutils.KeySequence()
self._sequence = keyutils.KeySequence()
self._count = ''
self._count_keyposs = []
self._mode = mode
@ -188,6 +205,8 @@ class BaseKeyParser(QObject):
self.passthrough = passthrough
self._supports_count = supports_count
self.allow_partial_timeout = allow_partial_timeout
self.allow_forward = allow_forward
self.forward_widget_name = forward_widget_name
self.bindings = BindingTrie()
self._read_config()
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})")
if info.is_modifier_key():
self._debug_log("Ignoring, only modifier")
return QKeySequence.SequenceMatch.NoMatch
# Modifier keys handled in modeman
assert not keyutils.is_modifier_key(key)
had_empty_queue = (not self._pure_sequence) and (not self._count)
@ -304,8 +322,9 @@ class BaseKeyParser(QObject):
self.clear_keystring()
return QKeySequence.SequenceMatch.NoMatch
flag0 = True
# Have these shadow variables to have replicable behavior when doing a
# dry_run
# dry run
count = self._count
count_keyposs = self._count_keyposs.copy()
while pure_sequence:
@ -320,7 +339,8 @@ class BaseKeyParser(QObject):
"mappings.".format(result.sequence))
seq_len = len(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
# _match_key_mapping call that directly convert a single key to
# a numeral character are allowed to be recognized as counts.
@ -342,45 +362,48 @@ class BaseKeyParser(QObject):
"matching will be attempted.".format(
result.sequence))
if not dry_run:
# Update state variables
self._sequence = result.sequence
self._pure_sequence = pure_sequence
# TODO: forwarding debug log message
if result.match_type:
break
else:
# TODO ensure all actual values are shadowed properly for dry_run
if not had_empty_queue:
self._debug_log("No match for '{}'. Will forward first "
"key in the sequence and retry.".format(
result.sequence))
# TODO: empty handling (and find all others and fix)
while count_keyposs and (0 == count_keyposs[0]):
self._debug_log("Hit a queued count key ('{}'). "
"Forwarding.".format(count[0]))
count = count[1:]
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]
assert pure_sequence
if not had_empty_queue:
self._debug_log("No match for '{}'. Will forward first "
"key in the sequence and retry.".format(
result.sequence))
# Forward all the leading count keys
while count_keyposs and (0 == count_keyposs[0]):
self._debug_log("Hit a queued count key ('{}'). "
"Forwarding.".format(count[0]))
count = count[1:]
count_keyposs.pop(0)
if not dry_run:
self._count_keyposs = [x - 1 for x in self._count_keyposs]
# TODO: TODO ensure there's always a 0th element here
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:]
# TODO: TODO check if on next loop a count could've slipped in somehow
self.forward_partial_key.emit(self._count[0])
self._count = self._count[1:]
self._count_keyposs.pop(0)
self._debug_log("Forwarding first key in sequence "
"('{}').".format(str(pure_sequence[0])))
# Update the count_keyposs to reflect the shortened
# 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:
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:
assert result.command is not None
self._debug_log("Definitive match for '{}'.".format(
@ -437,11 +460,11 @@ class BaseKeyParser(QObject):
self._count))
self._count = ''
self._count_keyposs = []
# TODO: better handling of clearing managing _pure_sequence length
if self._pure_sequence:
self._pure_sequence = keyutils.KeySequence()
if self._sequence:
# self._pure_sequence should non-empty if and only if self._sequence is
# non-empty, but to be safe both conditions are included below
if self._pure_sequence or self._sequence:
self._debug_log("Clearing keystring (was: {}).".format(
self._sequence))
self._pure_sequence = keyutils.KeySequence()
self._sequence = keyutils.KeySequence()
self.keystring_updated.emit('')
self.keystring_updated.emit('')

View File

@ -530,7 +530,6 @@ class KeyEvent:
@dataclasses.dataclass(frozen=False)
class QueuedKeyEventPair:
# TODO: docs
"""A wrapper over a QKeyEvent capable of recreating the event.
This is needed to recreate any queued events when either a timeout occurs
@ -538,9 +537,10 @@ class QueuedKeyEventPair:
Attributes:
key_event: A KeyEvent member for comparison.
key_info: A keyutils.KeyInfo member for complete event reconstruction
(e.g. with modifiers).
typ: QEvent.KeyPress or QEvent.KeyRelease.
key_info_press: A KeyInfo member for complete event reconstruction
(e.g. with modifiers) corresponding to the press event.
key_info_release: A KeyInfo member for complete event reconstruction
(e.g. with modifiers) corresponding to the release event.
"""
key_event: KeyEvent
@ -553,12 +553,13 @@ class QueuedKeyEventPair:
return cls(KeyEvent.from_event(event), KeyInfo.from_event(event), None)
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):
self.key_info_release = KeyInfo.from_event(event)
return True
return False
def is_released(self):
def is_released(self) -> bool:
return self.key_info_release is not None
def to_events(self) -> Tuple[QKeyEvent]:

View File

@ -102,7 +102,9 @@ def init(win_id: int, parent: QObject) -> 'ModeManager':
passthrough=True,
do_log=log_sensitive_keys,
supports_count=False,
allow_partial_timeout=True),
allow_partial_timeout=True,
allow_forward=True,
forward_widget_name='status-command'),
usertypes.KeyMode.prompt:
modeparsers.CommandKeyParser(
@ -113,7 +115,12 @@ def init(win_id: int, parent: QObject) -> 'ModeManager':
passthrough=True,
do_log=log_sensitive_keys,
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:
modeparsers.CommandKeyParser(
@ -122,7 +129,10 @@ def init(win_id: int, parent: QObject) -> 'ModeManager':
commandrunner=commandrunner,
parent=modeman,
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:
modeparsers.CommandKeyParser(
@ -211,6 +221,8 @@ class ModeManager(QObject):
_releaseevents_to_pass: A set of KeyEvents where the keyPressEvent was
passed through, so the release event should as
well.
_partial_timer: The timer which forwards partial keys after no key has
been pressed for a timeout period.
Signals:
entered: Emitted when a mode is entered.
@ -220,10 +232,15 @@ class ModeManager(QObject):
arg1: The mode which has been left.
arg2: The new current mode.
arg3: The window ID of this mode manager.
keystring_updated: Emitted when the keystring was updated in any mode.
arg 1: The mode in which the keystring has been
updated.
arg 2: The new key string.
keystring_updated: Emitted when the keystring was updated in any mode.
arg1: The mode in which the keystring has been
updated.
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)
@ -240,8 +257,7 @@ class ModeManager(QObject):
self._releaseevents_to_pass: set[keyutils.KeyEvent] = set()
# Set after __init__
self.hintmanager = cast(hints.HintManager, None)
# TODO: type hints
self._partial_match_events = []
self._partial_match_events: Sequence[keyutils.QueuedKeyEventPair] = []
self.forward_partial_key.connect(self.forward_partial_match_event)
self._partial_timer = usertypes.Timer(self, 'partial-match')
self._partial_timer.setSingleShot(True)
@ -267,13 +283,24 @@ class ModeManager(QObject):
"{}".format(curmode, utils.qualname(parser)))
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(
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:
filter_this = True
if not dry_run:
@ -281,19 +308,21 @@ class ModeManager(QObject):
self.clear_partial_match_events()
elif match == QKeySequence.SequenceMatch.PartialMatch:
filter_this = True
if not dry_run:
if parser.allow_forward and (not dry_run):
if had_empty_queue:
# Begin recording partial match events
self._partial_match_events.append(
keyutils.QueuedKeyEventPair.from_event_press(event))
keyutils.QueuedKeyEventPair.from_event_press(event))
self._start_partial_timer()
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
if not dry_run:
self._stop_partial_timer()
# TODO: spacing and tabbing and formatting
else:
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:
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()
forward_unbound_keys = config.cache['input.forward_unbound_keys']
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()
log.modes.debug("match: {}, forward_unbound_keys: {}, "
"passthrough: {}, is_non_alnum: {}, "
@ -321,7 +350,7 @@ class ModeManager(QObject):
"--> filter: {} (focused: {!r})".format(
match, forward_unbound_keys,
parser.passthrough, is_non_alnum,
should_forward_event, dry_run, filter_this,
should_filter_event, dry_run, filter_this,
qtutils.qobj_repr(focus_widget)))
return filter_this
@ -340,6 +369,8 @@ class ModeManager(QObject):
self._releaseevents_to_pass.remove(keyevent)
filter_this = False
else:
# Record the releases for partial matches to later forward along
# with the presses
for match_event in self._partial_match_events[::-1]:
if match_event.add_event_release(event):
break
@ -349,7 +380,9 @@ class ModeManager(QObject):
return filter_this
@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
ignored_modifiers = [
cast(Qt.KeyboardModifiers, Qt.KeyboardModifier.NoModifier),
@ -363,42 +396,79 @@ class ModeManager(QObject):
has_modifier = key_info.modifiers not in ignored_modifiers
is_non_alnum = has_modifier or not key_info.text().strip()
forward_unbound_keys = config.cache['input.forward_unbound_keys']
return (parser.passthrough or forward_unbound_keys == 'all' or
(forward_unbound_keys == 'auto' and is_non_alnum)) and (
not isinstance(parser, modeparsers.HintKeyParser))
return not (parser.passthrough or forward_unbound_keys == 'all' or
(forward_unbound_keys == 'auto' and is_non_alnum))
@pyqtSlot(usertypes.KeyMode, str)
def forward_partial_match_event(self, mode: usertypes.KeyMode, text: str = None) -> None:
# TODO: add debug messages
#self._debug_log("Clearing partial keystring {}".format(
# self._sequence))
# TODO: Check for transient self.whatever statements (e.g. self.mode) in slots and remove, might not be thread-safe
def forward_partial_match_event(self, mode: usertypes.KeyMode,
text: str = None) -> None:
"""Forward the oldest partial match event for a given mode
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:
raise ValueError("Can't forward partial key: No keyparser for "
"mode {}".format(mode))
parser = self.parsers[mode]
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
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 self._should_forward_event(match_event.key_info_press, parser):
# TODO: review alternatives
tabbed_browser = objreg.get('tabbed-browser', scope='window', window=QApplication.activeWindow().win_id)
tab = tabbed_browser.widget.currentWidget()
if parser.allow_forward and (not
self._should_filter_event(match_event.key_info_press, parser)):
if mode != usertypes.KeyMode.insert:
log.modes.debug("Forwarding partial match event in mode "
"{}.".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():
tab.send_event(event_)
send_event(event_)
if not match_event.is_released():
self._releaseevents_to_pass.add(match_event.key_event)
@pyqtSlot(usertypes.KeyMode)
def forward_all_partial_match_events(self, mode: usertypes.KeyMode, *,
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:
self._stop_partial_timer()
if mode in self.parsers:
parser = self.parsers[mode]
if isinstance(parser, modeparsers.HintKeyParser):
# Call the subparsers analogous function, propagating the timer
# stopping.
parser.forward_all_partial_match_events(stop_timer=True)
if self._partial_match_events:
while self._partial_match_events:
@ -424,7 +494,7 @@ class ModeManager(QObject):
except TypeError:
pass
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()
def _stop_partial_timer(self) -> None:
@ -437,7 +507,6 @@ class ModeManager(QObject):
assert parser is not None
self.parsers[mode] = parser
parser.request_leave.connect(self.leave)
# TODO: maybe make keystring_updated a domino of forward_partial_key?
parser.keystring_updated.connect(
functools.partial(self.keystring_updated.emit, mode))
parser.forward_partial_key.connect(

View File

@ -52,11 +52,15 @@ class CommandKeyParser(basekeyparser.BaseKeyParser):
do_log: bool = True,
passthrough: bool = False,
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,
do_log=do_log, passthrough=passthrough,
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
def execute(self, cmdstr: str, count: int = None) -> None:
@ -120,6 +124,8 @@ class HintKeyParser(basekeyparser.BaseKeyParser):
_filtertext: The text to filter with.
_hintmanager: The HintManager to use.
_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
@ -130,7 +136,7 @@ class HintKeyParser(basekeyparser.BaseKeyParser):
parent: QObject = None) -> None:
super().__init__(mode=usertypes.KeyMode.hint, win_id=win_id,
parent=parent, supports_count=False,
allow_partial_timeout=False)
allow_partial_timeout=False, allow_forward=False)
self._command_parser = CommandKeyParser(mode=usertypes.KeyMode.hint,
win_id=win_id,
commandrunner=commandrunner,
@ -140,16 +146,16 @@ class HintKeyParser(basekeyparser.BaseKeyParser):
self._hintmanager = hintmanager
self._filtertext = ''
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._command_parser.forward_partial_key.connect(
self.forward_partial_match_event)
self.forward_partial_match_event)
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.setSingleShot(True)
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:
"""Handle keys for string filtering."""
@ -198,8 +204,10 @@ class HintKeyParser(basekeyparser.BaseKeyParser):
had_empty_queue = not self._partial_match_events
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(
keyutils.QueuedKeyEventPair.from_event_press(e))
keyutils.QueuedKeyEventPair.from_event_press(e))
result = self._command_parser.handle(e)
if result == QKeySequence.SequenceMatch.ExactMatch:
@ -211,8 +219,9 @@ class HintKeyParser(basekeyparser.BaseKeyParser):
elif result == QKeySequence.SequenceMatch.PartialMatch:
log.keyboard.debug("Handling key via command parser")
if had_empty_queue:
# Begin recording partial match events
self._partial_match_events.append(
keyutils.QueuedKeyEventPair.from_event_press(e))
keyutils.QueuedKeyEventPair.from_event_press(e))
self._start_partial_timer()
return result
elif not had_empty_queue:
@ -244,12 +253,22 @@ class HintKeyParser(basekeyparser.BaseKeyParser):
@pyqtSlot(str)
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:
# TODO: debug message
self._debug_log("Attempting to forward (expected text = {}) but "
"there are no events to forward.".format(text))
return
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()
assert 1 == len(e)
e = e[0]
@ -258,6 +277,12 @@ class HintKeyParser(basekeyparser.BaseKeyParser):
@pyqtSlot()
def forward_all_partial_match_events(self, *,
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:
self._stop_partial_timer()
if self._partial_match_events:

View File

@ -30,7 +30,6 @@ MAPPINGS = {
'e': 'd',
}
# TODO: ensure multiple-length mappings are safe
@pytest.fixture
def keyinput_bindings(config_stub, key_config_stub):