Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Send broken keychains in passthrough modes to browser #3683

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
31 changes: 30 additions & 1 deletion qutebrowser/keyinput/basekeyparser.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@

import string

from PyQt5.QtCore import pyqtSignal, QObject
from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject
from PyQt5.QtGui import QKeySequence

from qutebrowser.config import config
Expand Down Expand Up @@ -53,6 +53,7 @@ class BaseKeyParser(QObject):
_sequence: The currently entered key sequence
_modename: The name of the input mode associated with this keyparser.
_supports_count: Whether count is supported
_partial_timer: Timer to clear partial keypresses.

Signals:
keystring_updated: Emitted when the keystring is updated.
Expand All @@ -75,6 +76,8 @@ def __init__(self, win_id, parent=None, supports_count=True):
self._sequence = keyutils.KeySequence()
self._count = ''
self._supports_count = supports_count
self._partial_timer = usertypes.Timer(self, 'partial-match')
self._partial_timer.setSingleShot(True)
self.bindings = {}
config.instance.changed.connect(self._on_config_changed)

Expand Down Expand Up @@ -188,6 +191,7 @@ def handle(self, e, *, dry_run=False):
if match == QKeySequence.NoMatch:
was_count = self._match_count(sequence, dry_run)
if was_count:
self._set_partial_timeout()
return QKeySequence.ExactMatch

if dry_run:
Expand All @@ -205,6 +209,7 @@ def handle(self, e, *, dry_run=False):
self._debug_log("No match for '{}' (added {})".format(
sequence, txt))
self.keystring_updated.emit(self._count + str(sequence))
self._set_partial_timeout()
elif match == QKeySequence.NoMatch:
self._debug_log("Giving up with '{}', no matches".format(
sequence))
Expand Down Expand Up @@ -250,6 +255,24 @@ def execute(self, cmdstr, count=None):
"""
raise NotImplementedError

def _set_partial_timeout(self):
"""Set a timeout to clear a partial keystring."""
timeout = config.val.input.partial_timeout
if timeout != 0:
self._partial_timer.setInterval(timeout)
self._partial_timer.timeout.connect(self.clear_partial_match)
self._partial_timer.start()

@pyqtSlot()
def clear_partial_match(self):
"""Clear a partial keystring after a timeout."""
self._debug_log("Clearing partial keystring {}".format(
self._sequence))
if self._count:
self._count = ''
self._sequence = keyutils.KeySequence()
self.keystring_updated.emit(str(self._sequence))

def clear_keystring(self):
"""Clear the currently entered key sequence."""
if self._sequence:
Expand All @@ -258,3 +281,9 @@ def clear_keystring(self):
self._sequence = keyutils.KeySequence()
self._count = ''
self.keystring_updated.emit('')
self._partial_timer.stop()
try:
self._partial_timer.timeout.disconnect(self.clear_partial_match)
except TypeError:
# no connections
pass
107 changes: 65 additions & 42 deletions qutebrowser/keyinput/modeparsers.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,11 @@
import traceback
import enum

from PyQt5.QtCore import pyqtSlot, Qt
from PyQt5.QtWidgets import QApplication
from PyQt5.QtCore import pyqtSlot, Qt, QEvent
from PyQt5.QtGui import QKeySequence

from qutebrowser.commands import runners, cmdexc
from qutebrowser.config import config
from qutebrowser.keyinput import basekeyparser, keyutils
from qutebrowser.utils import usertypes, log, message, objreg, utils

Expand Down Expand Up @@ -60,17 +60,11 @@ def execute(self, cmdstr, count=None):

class NormalKeyParser(CommandKeyParser):

"""KeyParser for normal mode with added STARTCHARS detection and more.

Attributes:
_partial_timer: Timer to clear partial keypresses.
"""
"""KeyParser for normal mode with added STARTCHARS detection and more."""

def __init__(self, win_id, parent=None):
super().__init__(win_id, parent, supports_count=True)
self._read_config('normal')
self._partial_timer = usertypes.Timer(self, 'partial-match')
self._partial_timer.setSingleShot(True)
self._inhibited = False
self._inhibited_timer = usertypes.Timer(self, 'normal-inhibited')
self._inhibited_timer.setSingleShot(True)
Expand All @@ -95,15 +89,7 @@ def handle(self, e, *, dry_run=False):
"currently inhibited.".format(txt))
return QKeySequence.NoMatch

match = super().handle(e, dry_run=dry_run)

if match == QKeySequence.PartialMatch and not dry_run:
timeout = config.val.input.partial_timeout
if timeout != 0:
self._partial_timer.setInterval(timeout)
self._partial_timer.timeout.connect(self._clear_partial_match)
self._partial_timer.start()
return match
return super().handle(e, dry_run=dry_run)

def set_inhibited_timeout(self, timeout):
"""Ignore keypresses for the given duration."""
Expand All @@ -115,36 +101,12 @@ def set_inhibited_timeout(self, timeout):
self._inhibited_timer.timeout.connect(self._clear_inhibited)
self._inhibited_timer.start()

@pyqtSlot()
def _clear_partial_match(self):
"""Clear a partial keystring after a timeout."""
self._debug_log("Clearing partial keystring {}".format(
self._sequence))
self._sequence = keyutils.KeySequence()
self.keystring_updated.emit(str(self._sequence))

@pyqtSlot()
def _clear_inhibited(self):
"""Reset inhibition state after a timeout."""
self._debug_log("Releasing inhibition state of normal mode.")
self._inhibited = False

@pyqtSlot()
def _stop_timers(self):
super()._stop_timers()
self._partial_timer.stop()
try:
self._partial_timer.timeout.disconnect(self._clear_partial_match)
except TypeError:
# no connections
pass
self._inhibited_timer.stop()
try:
self._inhibited_timer.timeout.disconnect(self._clear_inhibited)
except TypeError:
# no connections
pass


class PassthroughKeyParser(CommandKeyParser):

Expand All @@ -154,6 +116,7 @@ class PassthroughKeyParser(CommandKeyParser):

Attributes:
_mode: The mode this keyparser is for.
_orig_sequence: Current sequence with no key_mappings applied.
"""

do_log = False
Expand All @@ -169,11 +132,65 @@ def __init__(self, win_id, mode, parent=None):
"""
super().__init__(win_id, parent)
self._read_config(mode)
self._orig_sequence = keyutils.KeySequence()
self._mode = mode

def __repr__(self):
return utils.get_repr(self, mode=self._mode)

def handle(self, e, *, dry_run=False):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems to duplicate a lot of the logic in BaseKeyParser.match, so I'm wondering whether there's a better way. Maybe KeySequence should have an original attribute where the original is stored when using with_mappings instead? Not sure how much easier this would make the code without seeing it though - what do you think?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here's a patch that adds a without_mappings method to KeySequence, to see how the code looks would look like. It's kind of clunky and incomplete though (even has pylint errors), because KeySequence is used pretty much as an immutable object.
If you want, I can rewrite the KeySequence class to be more mutable, but that would probably belong in it's own PR.
https://gist.github.com/jakanaka-evan/4973c4a4e46e8e3bdae57b17dba0e26c

"""Override to pass the chain through on NoMatch.

Args:
e: the KeyPressEvent from Qt.
dry_run: Don't actually execute anything, only check whether there
would be a match.

Return:
A self.Match member.
"""
if (keyutils.is_modifier_key(e.key()) or
getattr(e, "ignore_event", False)):
return QKeySequence.NoMatch

orig_sequence = self._orig_sequence.append_event(e)
match = super().handle(e, dry_run=dry_run)

if not dry_run and match == QKeySequence.PartialMatch:
self._orig_sequence = orig_sequence

if dry_run or len(orig_sequence) == 1 or match != QKeySequence.NoMatch:
return match

self._forward_keystring(orig_sequence)
return QKeySequence.ExactMatch

def _forward_keystring(self, orig_sequence):
window = QApplication.focusWindow()
if window is None:
return

first = True
for keyinfo in orig_sequence:
press_event = keyinfo.to_event(QEvent.KeyPress)
if first:
press_event.ignore_event = True
first = False
release_event = keyinfo.to_event(QEvent.KeyRelease)
QApplication.postEvent(window, press_event)
QApplication.postEvent(window, release_event)

@pyqtSlot()
def clear_partial_match(self):
"""Override to forward the original sequence to browser."""
self._forward_keystring(self._orig_sequence)
self.clear_keystring()

def clear_keystring(self):
"""Override to also clear the original sequence."""
super().clear_keystring()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Usually additional things are done after the super() call, not before it.

self._orig_sequence = keyutils.KeySequence()


class PromptKeyParser(CommandKeyParser):

Expand Down Expand Up @@ -296,6 +313,12 @@ def update_bindings(self, strings, preserve_filter=False):
if not preserve_filter:
self._filtertext = ''

@pyqtSlot()
def clear_partial_match(self):
"""Override to avoid clearing filter text after a timeout."""
if self._last_press != LastPress.filtertext:
super().clear_partial_match()

@pyqtSlot(str)
def on_keystring_updated(self, keystr):
"""Update hintmanager when the keystring was updated."""
Expand Down