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_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], *,
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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."""
|
||||
|
|
|
|||
|
|
@ -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('')
|
||||
|
|
|
|||
|
|
@ -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]:
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -30,7 +30,6 @@ MAPPINGS = {
|
|||
'e': 'd',
|
||||
}
|
||||
|
||||
# TODO: ensure multiple-length mappings are safe
|
||||
|
||||
@pytest.fixture
|
||||
def keyinput_bindings(config_stub, key_config_stub):
|
||||
|
|
|
|||
Loading…
Reference in New Issue