Implemented the nuances for the hint mode.
This commit is contained in:
parent
595cda4896
commit
f114aacad2
|
|
@ -18,7 +18,7 @@ handle what we actually think we do.
|
|||
|
||||
import itertools
|
||||
import dataclasses
|
||||
from typing import Optional, Union, overload, cast
|
||||
from typing import Optional, Union, overload, cast, Tuple
|
||||
from collections.abc import Iterator, Iterable, Mapping
|
||||
|
||||
from qutebrowser.qt import machinery
|
||||
|
|
@ -504,6 +504,72 @@ class KeyInfo:
|
|||
return self.key in _MODIFIER_MAP
|
||||
|
||||
|
||||
@dataclasses.dataclass(frozen=True)
|
||||
class KeyEvent:
|
||||
|
||||
"""A small wrapper over a QKeyEvent storing its data.
|
||||
|
||||
This is needed because Qt apparently mutates existing events with new data.
|
||||
It doesn't store the modifiers because they can be different for a key
|
||||
press/release.
|
||||
|
||||
Attributes:
|
||||
key: A Qt.Key member (QKeyEvent::key).
|
||||
text: A string (QKeyEvent::text).
|
||||
"""
|
||||
|
||||
key: Qt.Key
|
||||
text: str
|
||||
|
||||
@classmethod
|
||||
def from_event(cls, event: QKeyEvent) -> 'KeyEvent':
|
||||
"""Initialize a KeyEvent from a QKeyEvent."""
|
||||
return cls(Qt.Key(event.key()), event.text())
|
||||
|
||||
|
||||
@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
|
||||
or a match is not completed.
|
||||
|
||||
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_event: KeyEvent
|
||||
key_info_press: KeyInfo
|
||||
key_info_release: KeyInfo
|
||||
|
||||
@classmethod
|
||||
def from_event_press(cls, event: QKeyEvent) -> 'QueuedKeyEventPair':
|
||||
"""Initialize a QueuedKeyEventPair from a QKeyEvent and QKeyEvent."""
|
||||
return cls(KeyEvent.from_event(event), KeyInfo.from_event(event), None)
|
||||
|
||||
def add_event_release(self, event: QKeyEvent) -> bool:
|
||||
if self.key_event == KeyEvent.from_event(event):
|
||||
self.key_info_release = KeyInfo.from_event(event)
|
||||
return True
|
||||
return False
|
||||
|
||||
def is_released(self):
|
||||
return self.key_info_release is not None
|
||||
|
||||
def to_events(self) -> Tuple[QKeyEvent]:
|
||||
"""Get a QKeyEvent from this QueuedEvent."""
|
||||
if self.key_info_release is None:
|
||||
return (self.key_info_press.to_event(QEvent.KeyPress),)
|
||||
else:
|
||||
return (self.key_info_press.to_event(QEvent.KeyPress),
|
||||
self.key_info_release.to_event(QEvent.KeyRelease))
|
||||
|
||||
|
||||
class KeySequence:
|
||||
|
||||
"""A sequence of key presses.
|
||||
|
|
|
|||
|
|
@ -30,74 +30,6 @@ PROMPT_MODES = [usertypes.KeyMode.prompt, usertypes.KeyMode.yesno]
|
|||
ParserDictType = MutableMapping[usertypes.KeyMode, basekeyparser.BaseKeyParser]
|
||||
|
||||
|
||||
@dataclasses.dataclass(frozen=True)
|
||||
class KeyEvent:
|
||||
|
||||
"""A small wrapper over a QKeyEvent storing its data.
|
||||
|
||||
This is needed because Qt apparently mutates existing events with new data.
|
||||
It doesn't store the modifiers because they can be different for a key
|
||||
press/release.
|
||||
|
||||
Attributes:
|
||||
key: Usually a Qt.Key member, but could be other ints (QKeyEvent::key).
|
||||
text: A string (QKeyEvent::text).
|
||||
"""
|
||||
|
||||
# int instead of Qt.Key:
|
||||
# WORKAROUND for https://www.riverbankcomputing.com/pipermail/pyqt/2022-April/044607.html
|
||||
key: int
|
||||
text: str
|
||||
|
||||
@classmethod
|
||||
def from_event(cls, event: QKeyEvent) -> 'KeyEvent':
|
||||
"""Initialize a KeyEvent from a QKeyEvent."""
|
||||
return cls(event.key(), event.text())
|
||||
|
||||
@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
|
||||
or a match is not completed.
|
||||
|
||||
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_event: KeyEvent
|
||||
key_info_press: keyutils.KeyInfo
|
||||
key_info_release: keyutils.KeyInfo
|
||||
|
||||
@classmethod
|
||||
def from_event_press(cls, event: QKeyEvent) -> 'QueuedKeyEventPair':
|
||||
"""Initialize a QueuedKeyEventPair from a QKeyEvent and QKeyEvent."""
|
||||
return cls(KeyEvent.from_event(event),
|
||||
keyutils.KeyInfo.from_event(event), None)
|
||||
|
||||
def add_event_release(self, event: QKeyEvent) -> bool:
|
||||
if self.key_event == KeyEvent.from_event(event):
|
||||
self.key_info_release = keyutils.KeyInfo.from_event(event)
|
||||
return True
|
||||
return False
|
||||
|
||||
def is_released(self):
|
||||
return self.key_info_release is not None
|
||||
|
||||
def to_events(self) -> Tuple[QKeyEvent]:
|
||||
"""Get a QKeyEvent from this QueuedEvent."""
|
||||
if self.key_info_release is None:
|
||||
return (self.key_info_press.to_event(QEvent.KeyPress),)
|
||||
else:
|
||||
return (self.key_info_press.to_event(QEvent.KeyPress),
|
||||
self.key_info_release.to_event(QEvent.KeyRelease))
|
||||
|
||||
|
||||
class NotInModeError(Exception):
|
||||
|
||||
"""Exception raised when we want to leave a mode we're not in."""
|
||||
|
|
@ -305,7 +237,7 @@ class ModeManager(QObject):
|
|||
self.parsers: ParserDictType = {}
|
||||
self._prev_mode = usertypes.KeyMode.normal
|
||||
self.mode = usertypes.KeyMode.normal
|
||||
self._releaseevents_to_pass: set[KeyEvent] = set()
|
||||
self._releaseevents_to_pass: set[keyutils.KeyEvent] = set()
|
||||
# Set after __init__
|
||||
self.hintmanager = cast(hints.HintManager, None)
|
||||
# TODO: type hints
|
||||
|
|
@ -337,7 +269,7 @@ class ModeManager(QObject):
|
|||
had_empty_queue = not self._partial_match_events
|
||||
if (not dry_run) and (not had_empty_queue):
|
||||
self._partial_match_events.append(
|
||||
QueuedKeyEventPair.from_event_press(event))
|
||||
keyutils.QueuedKeyEventPair.from_event_press(event))
|
||||
|
||||
match = parser.handle(event, dry_run=dry_run)
|
||||
|
||||
|
|
@ -352,7 +284,7 @@ class ModeManager(QObject):
|
|||
if not dry_run:
|
||||
if had_empty_queue:
|
||||
self._partial_match_events.append(
|
||||
QueuedKeyEventPair.from_event_press(event))
|
||||
keyutils.QueuedKeyEventPair.from_event_press(event))
|
||||
self._start_partial_timer()
|
||||
elif not had_empty_queue:
|
||||
filter_this = True
|
||||
|
|
@ -364,7 +296,7 @@ class ModeManager(QObject):
|
|||
filter_this = not self._should_forward_event(key_info, parser)
|
||||
|
||||
if not filter_this and not dry_run:
|
||||
self._releaseevents_to_pass.add(KeyEvent.from_event(event))
|
||||
self._releaseevents_to_pass.add(keyutils.KeyEvent.from_event(event))
|
||||
|
||||
if curmode != usertypes.KeyMode.insert:
|
||||
if machinery.IS_QT5: # FIXME:v4 needed for Qt 5 typing
|
||||
|
|
@ -403,7 +335,7 @@ class ModeManager(QObject):
|
|||
True if event should be filtered, False otherwise.
|
||||
"""
|
||||
# handle like matching KeyPress
|
||||
keyevent = KeyEvent.from_event(event)
|
||||
keyevent = keyutils.KeyEvent.from_event(event)
|
||||
if keyevent in self._releaseevents_to_pass:
|
||||
self._releaseevents_to_pass.remove(keyevent)
|
||||
filter_this = False
|
||||
|
|
@ -418,14 +350,22 @@ class ModeManager(QObject):
|
|||
|
||||
@staticmethod
|
||||
def _should_forward_event(key_info, parser):
|
||||
has_modifier = key_info.modifiers not in [
|
||||
Qt.NoModifier,
|
||||
Qt.ShiftModifier,
|
||||
] # type: ignore[comparison-overlap]
|
||||
if machinery.IS_QT5: # FIXME:v4 needed for Qt 5 typing
|
||||
ignored_modifiers = [
|
||||
cast(Qt.KeyboardModifiers, Qt.KeyboardModifier.NoModifier),
|
||||
cast(Qt.KeyboardModifiers, Qt.KeyboardModifier.ShiftModifier),
|
||||
]
|
||||
else:
|
||||
ignored_modifiers = [
|
||||
Qt.KeyboardModifier.NoModifier,
|
||||
Qt.KeyboardModifier.ShiftModifier,
|
||||
]
|
||||
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))
|
||||
(forward_unbound_keys == 'auto' and is_non_alnum)) and (
|
||||
not isinstance(parser, modeparsers.HintKeyParser))
|
||||
|
||||
@pyqtSlot(usertypes.KeyMode, str)
|
||||
def forward_partial_match_event(self, mode: usertypes.KeyMode, text: str = None) -> None:
|
||||
|
|
@ -452,7 +392,14 @@ class ModeManager(QObject):
|
|||
self._releaseevents_to_pass.add(match_event.key_event)
|
||||
|
||||
@pyqtSlot(usertypes.KeyMode)
|
||||
def forward_all_partial_match_events(self, mode: usertypes.KeyMode) -> None:
|
||||
def forward_all_partial_match_events(self, mode: usertypes.KeyMode, *,
|
||||
stop_timer: bool = False) -> None:
|
||||
if stop_timer:
|
||||
self._stop_partial_timer()
|
||||
if mode in self.parsers:
|
||||
parser = self.parsers[mode]
|
||||
if isinstance(parser, modeparsers.HintKeyParser):
|
||||
parser.forward_all_partial_match_events(stop_timer=True)
|
||||
if self._partial_match_events:
|
||||
while self._partial_match_events:
|
||||
self.forward_partial_match_event(mode)
|
||||
|
|
@ -476,15 +423,13 @@ class ModeManager(QObject):
|
|||
self._partial_timer.timeout.disconnect()
|
||||
except TypeError:
|
||||
pass
|
||||
self._partial_timer.timeout.connect(
|
||||
functools.partial(self.forward_all_partial_match_events, self.mode))
|
||||
self._partial_timer.timeout.connect(functools.partial(
|
||||
self.forward_all_partial_match_events, self.mode))
|
||||
self._partial_timer.start()
|
||||
|
||||
def _stop_partial_timer(self) -> None:
|
||||
"""Prematurely stop the the partial keystring timer."""
|
||||
timeout = config.val.input.partial_timeout
|
||||
if self.parsers[self.mode].allow_partial_timeout and (timeout != 0):
|
||||
self._partial_timer.stop()
|
||||
self._partial_timer.stop()
|
||||
|
||||
def register(self, mode: usertypes.KeyMode,
|
||||
parser: basekeyparser.BaseKeyParser) -> None:
|
||||
|
|
@ -601,9 +546,8 @@ class ModeManager(QObject):
|
|||
self.leave(self.mode, 'leave current')
|
||||
|
||||
def change_mode(self, mode: usertypes.KeyMode) -> None:
|
||||
self._stop_partial_timer()
|
||||
# catches the case where change of mode is not keys, e.g. mouse click
|
||||
self.forward_all_partial_match_events(self.mode)
|
||||
self.forward_all_partial_match_events(self.mode, stop_timer=True)
|
||||
self.mode = mode
|
||||
|
||||
def handle_event(self, event: QEvent) -> bool:
|
||||
|
|
|
|||
|
|
@ -136,11 +136,20 @@ class HintKeyParser(basekeyparser.BaseKeyParser):
|
|||
commandrunner=commandrunner,
|
||||
parent=self,
|
||||
supports_count=False,
|
||||
allow_partial_timeout=False)
|
||||
allow_partial_timeout=True)
|
||||
self._hintmanager = hintmanager
|
||||
self._filtertext = ''
|
||||
self._last_press = LastPress.none
|
||||
self._partial_match_events = []
|
||||
self.keystring_updated.connect(self._hintmanager.handle_partial_key)
|
||||
self._command_parser.forward_partial_key.connect(
|
||||
self.forward_partial_match_event)
|
||||
self._command_parser.clear_partial_keys.connect(
|
||||
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)
|
||||
|
||||
def _handle_filter_key(self, e: QKeyEvent) -> QKeySequence.SequenceMatch:
|
||||
"""Handle keys for string filtering."""
|
||||
|
|
@ -180,16 +189,45 @@ class HintKeyParser(basekeyparser.BaseKeyParser):
|
|||
dry_run: bool = False) -> QKeySequence.SequenceMatch:
|
||||
"""Handle a new keypress and call the respective handlers."""
|
||||
if dry_run:
|
||||
result = self._command_parser.handle(e, dry_run=True)
|
||||
if result != QKeySequence.SequenceMatch.NoMatch:
|
||||
return result
|
||||
return super().handle(e, dry_run=True)
|
||||
|
||||
assert not dry_run
|
||||
|
||||
if (self._command_parser.handle(e, dry_run=True) !=
|
||||
QKeySequence.SequenceMatch.NoMatch):
|
||||
had_empty_queue = not self._partial_match_events
|
||||
if not had_empty_queue:
|
||||
self._partial_match_events.append(
|
||||
keyutils.QueuedKeyEventPair.from_event_press(e))
|
||||
|
||||
result = self._command_parser.handle(e)
|
||||
if result == QKeySequence.SequenceMatch.ExactMatch:
|
||||
self._stop_partial_timer()
|
||||
self.clear_partial_match_events()
|
||||
log.keyboard.debug("Handling key via command parser")
|
||||
self.clear_keystring()
|
||||
return self._command_parser.handle(e)
|
||||
return result
|
||||
elif result == QKeySequence.SequenceMatch.PartialMatch:
|
||||
log.keyboard.debug("Handling key via command parser")
|
||||
if had_empty_queue:
|
||||
self._partial_match_events.append(
|
||||
keyutils.QueuedKeyEventPair.from_event_press(e))
|
||||
self._start_partial_timer()
|
||||
return result
|
||||
elif not had_empty_queue:
|
||||
self._stop_partial_timer()
|
||||
# It's unclear exactly what the return here should be. The safest
|
||||
# bet seems to be PartialMatch as it won't clear the unused
|
||||
# modeman._partial_match_events buffer, which if done could lead to
|
||||
# an issue if forward_partial were called with an empty buffer. At
|
||||
# the time of writing this, the behaviors of returning
|
||||
# ExactMatch/PartialMatch are identical, practically speaking.
|
||||
return QKeySequence.SequenceMatch.PartialMatch
|
||||
else:
|
||||
return self._handle_hint(e)
|
||||
|
||||
def _handle_hint(self, e: QKeyEvent) -> QKeySequence.SequenceMatch:
|
||||
match = super().handle(e)
|
||||
|
||||
if match == QKeySequence.SequenceMatch.PartialMatch:
|
||||
|
|
@ -204,6 +242,44 @@ class HintKeyParser(basekeyparser.BaseKeyParser):
|
|||
|
||||
return match
|
||||
|
||||
@pyqtSlot(str)
|
||||
def forward_partial_match_event(self, text: str = None) -> None:
|
||||
# TODO: add debug messages
|
||||
if not self._partial_match_events:
|
||||
# TODO: debug message
|
||||
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
|
||||
e = match_event.to_events()
|
||||
assert 1 == len(e)
|
||||
e = e[0]
|
||||
self._handle_hint(e)
|
||||
|
||||
@pyqtSlot()
|
||||
def forward_all_partial_match_events(self, *,
|
||||
stop_timer: bool = False) -> None:
|
||||
if stop_timer:
|
||||
self._stop_partial_timer()
|
||||
if self._partial_match_events:
|
||||
while self._partial_match_events:
|
||||
self.forward_partial_match_event()
|
||||
self._command_parser.clear_keystring()
|
||||
|
||||
@pyqtSlot()
|
||||
def clear_partial_match_events(self) -> None:
|
||||
self._partial_match_events = []
|
||||
|
||||
def _start_partial_timer(self) -> None:
|
||||
"""Set a timeout to clear a partial keystring."""
|
||||
timeout = config.val.input.partial_timeout
|
||||
if self._command_parser.allow_partial_timeout and (timeout != 0):
|
||||
self._partial_timer.setInterval(timeout)
|
||||
self._partial_timer.start()
|
||||
|
||||
def _stop_partial_timer(self) -> None:
|
||||
"""Prematurely stop the the partial keystring timer."""
|
||||
self._partial_timer.stop()
|
||||
|
||||
def update_bindings(self, strings: Sequence[str],
|
||||
preserve_filter: bool = False) -> None:
|
||||
"""Update bindings when the hint strings changed.
|
||||
|
|
|
|||
|
|
@ -337,8 +337,8 @@ class TestHandle:
|
|||
(Qt.Key.Key_2, Qt.Key.Key_F, Qt.Key.Key_G),
|
||||
(Qt.Key.Key_2, Qt.Key.Key_F, Qt.Key.Key_G, Qt.Key.Key_H),
|
||||
])
|
||||
def test_forward_keys(self, config_stub, handle_text, keyparser, seq,
|
||||
qtbot):
|
||||
def test_forward_keys(self, config_stub, handle_text, keyparser, qtbot,
|
||||
seq):
|
||||
config_stub.val.bindings.commands = {
|
||||
'normal': {
|
||||
'fy': 'message-info fy',
|
||||
|
|
@ -400,6 +400,7 @@ class TestHandle:
|
|||
assert keyparser._count == ''.join(
|
||||
str(keyutils.KeyInfo(key, Qt.KeyboardModifier.NoModifier)) for key in count_seq
|
||||
)
|
||||
assert keyparser._sequence == keyseq('f')
|
||||
|
||||
|
||||
class TestCount:
|
||||
|
|
|
|||
|
|
@ -4,7 +4,9 @@
|
|||
|
||||
"""Tests for mode parsers."""
|
||||
|
||||
from qutebrowser.qt.core import Qt
|
||||
from unittest import mock
|
||||
|
||||
from qutebrowser.qt.core import Qt, QTimer
|
||||
from qutebrowser.qt.gui import QKeySequence
|
||||
|
||||
import pytest
|
||||
|
|
@ -18,6 +20,16 @@ def commandrunner(stubs):
|
|||
return stubs.FakeCommandRunner()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def handle_text():
|
||||
"""Helper function to handle multiple fake keypresses."""
|
||||
def func(kp, *args):
|
||||
for key in args:
|
||||
info = keyutils.KeyInfo(key, Qt.KeyboardModifier.NoModifier)
|
||||
kp.handle(info.to_event())
|
||||
return func
|
||||
|
||||
|
||||
class TestsNormalKeyParser:
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
|
|
@ -128,8 +140,8 @@ class TestHintKeyParser:
|
|||
|
||||
steps = [
|
||||
(Qt.Key.Key_X, QKeySequence.SequenceMatch.PartialMatch, 'x'),
|
||||
(Qt.Key.Key_A, QKeySequence.SequenceMatch.PartialMatch, ''),
|
||||
(Qt.Key.Key_B, QKeySequence.SequenceMatch.PartialMatch, ''),
|
||||
(Qt.Key.Key_A, QKeySequence.SequenceMatch.PartialMatch, 'x'),
|
||||
(Qt.Key.Key_B, QKeySequence.SequenceMatch.PartialMatch, 'x'),
|
||||
(Qt.Key.Key_C, QKeySequence.SequenceMatch.ExactMatch, ''),
|
||||
]
|
||||
for key, expected_match, keystr in steps:
|
||||
|
|
@ -141,3 +153,173 @@ class TestHintKeyParser:
|
|||
assert not commandrunner.commands
|
||||
|
||||
assert commandrunner.commands == [('message-info abc', None)]
|
||||
|
||||
@pytest.mark.parametrize('seq, hint_seq', [
|
||||
((Qt.Key.Key_F,), None),
|
||||
((Qt.Key.Key_F,), 'f'),
|
||||
((Qt.Key.Key_F,), 'fz'),
|
||||
((Qt.Key.Key_F,), 'fzz'),
|
||||
((Qt.Key.Key_F,), 'fza'),
|
||||
((Qt.Key.Key_F, Qt.Key.Key_G), None),
|
||||
((Qt.Key.Key_F, Qt.Key.Key_G), 'f'),
|
||||
((Qt.Key.Key_F, Qt.Key.Key_G), 'fg'),
|
||||
((Qt.Key.Key_F, Qt.Key.Key_G), 'fgz'),
|
||||
((Qt.Key.Key_F, Qt.Key.Key_G), 'fgzz'),
|
||||
((Qt.Key.Key_F, Qt.Key.Key_G), 'fgza'),
|
||||
((Qt.Key.Key_F, Qt.Key.Key_G, Qt.Key.Key_H), None),
|
||||
((Qt.Key.Key_F, Qt.Key.Key_G, Qt.Key.Key_H), 'f'),
|
||||
((Qt.Key.Key_F, Qt.Key.Key_G, Qt.Key.Key_H), 'fg'),
|
||||
((Qt.Key.Key_F, Qt.Key.Key_G, Qt.Key.Key_H), 'fgh'),
|
||||
((Qt.Key.Key_F, Qt.Key.Key_G, Qt.Key.Key_H), 'fghz'),
|
||||
((Qt.Key.Key_F, Qt.Key.Key_G, Qt.Key.Key_H), 'fghzz'),
|
||||
((Qt.Key.Key_F, Qt.Key.Key_G, Qt.Key.Key_H), 'fghza'),
|
||||
])
|
||||
def test_forward_keys(self, config_stub, handle_text, keyparser, qtbot,
|
||||
hintmanager, commandrunner, seq, hint_seq):
|
||||
command_parser = keyparser._command_parser
|
||||
config_stub.val.bindings.commands = {
|
||||
'hint': {
|
||||
'fy': 'message-info fy',
|
||||
'fgy': 'message-info fgy',
|
||||
'fghy': 'message-info fghy',
|
||||
}
|
||||
}
|
||||
if hint_seq is not None:
|
||||
keyparser.update_bindings([hint_seq, 'zz'])
|
||||
forward_partial_key = mock.Mock()
|
||||
command_parser.forward_partial_key.connect(forward_partial_key)
|
||||
handle_text(keyparser, *seq)
|
||||
assert not commandrunner.commands
|
||||
seq = list(seq) + [Qt.Key.Key_Z]
|
||||
signals = [command_parser.forward_partial_key] * len(seq)
|
||||
with qtbot.wait_signals(signals) as blocker:
|
||||
handle_text(keyparser, seq[-1])
|
||||
assert forward_partial_key.call_args_list == [
|
||||
((str(keyutils.KeyInfo(key, Qt.KeyboardModifier.NoModifier)),),) for key in seq
|
||||
]
|
||||
if hint_seq is not None:
|
||||
if len(seq) > len(hint_seq):
|
||||
assert hintmanager.keystr == 'z'
|
||||
else:
|
||||
assert hintmanager.keystr == hint_seq[:len(seq)]
|
||||
else:
|
||||
assert hintmanager.keystr == ''
|
||||
|
||||
@pytest.mark.parametrize('seq, hint_seq, keystr', [
|
||||
((Qt.Key.Key_F,), None, None),
|
||||
((Qt.Key.Key_F,), 'f', None),
|
||||
((Qt.Key.Key_F,), 'fz', None),
|
||||
((Qt.Key.Key_F,), 'fzz', None),
|
||||
((Qt.Key.Key_F,), 'fza', None),
|
||||
((Qt.Key.Key_F, Qt.Key.Key_G), None, None),
|
||||
((Qt.Key.Key_F, Qt.Key.Key_G), 'f', 'g'),
|
||||
((Qt.Key.Key_F, Qt.Key.Key_G), 'fg', None),
|
||||
((Qt.Key.Key_F, Qt.Key.Key_G), 'fgz', None),
|
||||
((Qt.Key.Key_F, Qt.Key.Key_G), 'fgzz', None),
|
||||
((Qt.Key.Key_F, Qt.Key.Key_G), 'fgza', None),
|
||||
((Qt.Key.Key_F, Qt.Key.Key_G, Qt.Key.Key_H), None, None),
|
||||
((Qt.Key.Key_F, Qt.Key.Key_G, Qt.Key.Key_H), 'f', 'gh'),
|
||||
((Qt.Key.Key_F, Qt.Key.Key_G, Qt.Key.Key_H), 'fg', 'h'),
|
||||
((Qt.Key.Key_F, Qt.Key.Key_G, Qt.Key.Key_H), 'fgh', None),
|
||||
((Qt.Key.Key_F, Qt.Key.Key_G, Qt.Key.Key_H), 'fghz', None),
|
||||
((Qt.Key.Key_F, Qt.Key.Key_G, Qt.Key.Key_H), 'fghzz', None),
|
||||
((Qt.Key.Key_F, Qt.Key.Key_G, Qt.Key.Key_H), 'fghza', None),
|
||||
])
|
||||
def test_forward_keys_partial(self, config_stub, handle_text, keyparser,
|
||||
qtbot, hintmanager, commandrunner, seq,
|
||||
hint_seq, keystr):
|
||||
command_parser = keyparser._command_parser
|
||||
config_stub.val.bindings.commands = {
|
||||
'hint': {
|
||||
'fy': 'message-info fy',
|
||||
'fgy': 'message-info fgy',
|
||||
'fghy': 'message-info fghy',
|
||||
}
|
||||
}
|
||||
if hint_seq is not None:
|
||||
keyparser.update_bindings([hint_seq, 'gh', 'h'])
|
||||
forward_partial_key = mock.Mock()
|
||||
command_parser.forward_partial_key.connect(forward_partial_key)
|
||||
handle_text(keyparser, *seq)
|
||||
assert not commandrunner.commands
|
||||
signals = [command_parser.forward_partial_key] * len(seq)
|
||||
with qtbot.wait_signals(signals) as blocker:
|
||||
handle_text(keyparser, Qt.Key.Key_F)
|
||||
assert forward_partial_key.call_args_list == [
|
||||
((str(keyutils.KeyInfo(key, Qt.KeyboardModifier.NoModifier)),),) for key in seq
|
||||
]
|
||||
assert command_parser._sequence == keyutils.KeySequence.parse('f')
|
||||
if hint_seq is not None:
|
||||
if keystr is not None:
|
||||
assert len(seq) > len(hint_seq)
|
||||
assert hintmanager.keystr == keystr
|
||||
else:
|
||||
assert hintmanager.keystr == hint_seq[:len(seq)]
|
||||
else:
|
||||
assert hintmanager.keystr == ''.join(
|
||||
str(keyutils.KeyInfo(key, Qt.KeyboardModifier.NoModifier)) for key in seq)
|
||||
|
||||
@pytest.mark.parametrize('data_sequence', [
|
||||
((Qt.Key.Key_A, 'timer_inactive'),),
|
||||
((Qt.Key.Key_B, 'timer_active'),),
|
||||
((Qt.Key.Key_C, 'timer_inactive'),),
|
||||
((Qt.Key.Key_B, 'timer_active'), (Qt.Key.Key_A, 'timer_inactive'),),
|
||||
((Qt.Key.Key_B, 'timer_active'), (Qt.Key.Key_B, 'timer_reset'),),
|
||||
((Qt.Key.Key_B, 'timer_active'), (Qt.Key.Key_C, 'timer_inactive'),),
|
||||
((Qt.Key.Key_B, 'timer_active'), (Qt.Key.Key_B, 'timer_reset'), (Qt.Key.Key_A, 'timer_inactive'),),
|
||||
((Qt.Key.Key_B, 'timer_active'), (Qt.Key.Key_B, 'timer_reset'), (Qt.Key.Key_B, 'timer_reset'),),
|
||||
((Qt.Key.Key_B, 'timer_active'), (Qt.Key.Key_B, 'timer_reset'), (Qt.Key.Key_C, 'timer_inactive'),),
|
||||
])
|
||||
def test_partial_keychain_timeout(self, keyparser, config_stub, qtbot,
|
||||
hintmanager, commandrunner,
|
||||
data_sequence):
|
||||
"""Test partial keychain timeout behavior."""
|
||||
command_parser = keyparser._command_parser
|
||||
config_stub.val.bindings.commands = {
|
||||
'hint': {
|
||||
'a': 'message-info a',
|
||||
'ba': 'message-info ba',
|
||||
'bba': 'message-info bba',
|
||||
'bbba': 'message-info bbba',
|
||||
}
|
||||
}
|
||||
keyparser.update_bindings(['bbb'])
|
||||
|
||||
timeout = 100
|
||||
config_stub.val.input.partial_timeout = timeout
|
||||
timer = keyparser._partial_timer
|
||||
assert not timer.isActive()
|
||||
|
||||
for key, behavior in data_sequence:
|
||||
keyinfo = keyutils.KeyInfo(key, Qt.KeyboardModifier.NoModifier)
|
||||
if behavior == 'timer_active':
|
||||
# Timer should be active
|
||||
keyparser.handle(keyinfo.to_event())
|
||||
assert timer.isSingleShot()
|
||||
assert timer.interval() == timeout
|
||||
assert timer.isActive()
|
||||
elif behavior == 'timer_inactive':
|
||||
# Timer should be inactive
|
||||
keyparser.handle(keyinfo.to_event())
|
||||
assert not timer.isActive()
|
||||
elif behavior == 'timer_reset':
|
||||
# Timer should be reset after handling the key
|
||||
half_timer = QTimer()
|
||||
half_timer.setSingleShot(True)
|
||||
half_timer.setInterval(timeout//2)
|
||||
half_timer.start()
|
||||
# Simulate a half timeout to check for reset
|
||||
qtbot.wait_signal(half_timer.timeout).wait()
|
||||
assert (timeout - (timeout//4)) > timer.remainingTime()
|
||||
keyparser.handle(keyinfo.to_event())
|
||||
assert (timeout - (timeout//4)) < timer.remainingTime()
|
||||
assert timer.isActive()
|
||||
else:
|
||||
# Unreachable
|
||||
assert False
|
||||
if behavior in ['timer_active', 'timer_reset']:
|
||||
# Now simulate a timeout and check the keystring has been forwarded.
|
||||
with qtbot.wait_signal(command_parser.keystring_updated) as blocker:
|
||||
timer.timeout.emit()
|
||||
assert blocker.args == ['']
|
||||
assert hintmanager.keystr == ('b' * len(data_sequence))
|
||||
|
|
|
|||
Loading…
Reference in New Issue