Merge branch 'hints'
This commit is contained in:
commit
b972acf20c
@ -32,6 +32,8 @@ Added
|
|||||||
- New `:messages` command to show error messages
|
- New `:messages` command to show error messages
|
||||||
- New pop-up showing possible keybinding when the first key of a keychain is
|
- New pop-up showing possible keybinding when the first key of a keychain is
|
||||||
pressed. This can be turned off using `:set ui keyhint-blacklist *`.
|
pressed. This can be turned off using `:set ui keyhint-blacklist *`.
|
||||||
|
- New `hints -> auto-follow-timeout` setting to ignore keypresses after
|
||||||
|
following a hint when filtering in number mode.
|
||||||
|
|
||||||
Changed
|
Changed
|
||||||
~~~~~~~
|
~~~~~~~
|
||||||
@ -60,6 +62,10 @@ Changed
|
|||||||
- `:zoom-in` or `:zoom-out` (`+`/`-`) with a too large count now zooms to the
|
- `:zoom-in` or `:zoom-out` (`+`/`-`) with a too large count now zooms to the
|
||||||
smallest/largest zoom instead of doing nothing.
|
smallest/largest zoom instead of doing nothing.
|
||||||
- The commandline now accepts partially typed commands if they're unique.
|
- The commandline now accepts partially typed commands if they're unique.
|
||||||
|
- Number hints are now kept filtered after following a hint in rapid mode.
|
||||||
|
- Number hints are now renumbered after filtering
|
||||||
|
- Number hints can now be filtered with multiple space-separated search terms
|
||||||
|
- `hints -> scatter` is now ignored for number hints
|
||||||
|
|
||||||
Fixed
|
Fixed
|
||||||
-----
|
-----
|
||||||
@ -80,6 +86,7 @@ Fixed
|
|||||||
- Fixed crash when cancelling a download which belongs to a MHTML download
|
- Fixed crash when cancelling a download which belongs to a MHTML download
|
||||||
- Fixed rebinding of keybindings being case-sensitive
|
- Fixed rebinding of keybindings being case-sensitive
|
||||||
- Fix for tab indicators getting lost when moving tabs
|
- Fix for tab indicators getting lost when moving tabs
|
||||||
|
- Fixed handling of backspace in number hinting mode
|
||||||
|
|
||||||
v0.6.2
|
v0.6.2
|
||||||
------
|
------
|
||||||
|
@ -148,9 +148,9 @@ Contributors, sorted by the number of commits in descending order:
|
|||||||
* Alexander Cogneau
|
* Alexander Cogneau
|
||||||
* Felix Van der Jeugt
|
* Felix Van der Jeugt
|
||||||
* Martin Tournoij
|
* Martin Tournoij
|
||||||
|
* Jakub Klinkovský
|
||||||
* Raphael Pierzina
|
* Raphael Pierzina
|
||||||
* Joel Torstensson
|
* Joel Torstensson
|
||||||
* Jakub Klinkovský
|
|
||||||
* Tarcisio Fedrizzi
|
* Tarcisio Fedrizzi
|
||||||
* Patric Schmitz
|
* Patric Schmitz
|
||||||
* Claude
|
* Claude
|
||||||
|
@ -179,10 +179,11 @@
|
|||||||
|<<hints-mode,mode>>|Mode to use for hints.
|
|<<hints-mode,mode>>|Mode to use for hints.
|
||||||
|<<hints-chars,chars>>|Chars used for hint strings.
|
|<<hints-chars,chars>>|Chars used for hint strings.
|
||||||
|<<hints-min-chars,min-chars>>|Minimum number of chars used for hint strings.
|
|<<hints-min-chars,min-chars>>|Minimum number of chars used for hint strings.
|
||||||
|<<hints-scatter,scatter>>|Whether to scatter hint key chains (like Vimium) or not (like dwb).
|
|<<hints-scatter,scatter>>|Whether to scatter hint key chains (like Vimium) or not (like dwb). Ignored for number hints.
|
||||||
|<<hints-uppercase,uppercase>>|Make chars in hint strings uppercase.
|
|<<hints-uppercase,uppercase>>|Make chars in hint strings uppercase.
|
||||||
|<<hints-dictionary,dictionary>>|The dictionary file to be used by the word hints.
|
|<<hints-dictionary,dictionary>>|The dictionary file to be used by the word hints.
|
||||||
|<<hints-auto-follow,auto-follow>>|Follow a hint immediately when the hint text is completely matched.
|
|<<hints-auto-follow,auto-follow>>|Follow a hint immediately when the hint text is completely matched.
|
||||||
|
|<<hints-auto-follow-timeout,auto-follow-timeout>>|A timeout to inhibit normal-mode key bindings after a successfulauto-follow.
|
||||||
|<<hints-next-regexes,next-regexes>>|A comma-separated list of regexes to use for 'next' links.
|
|<<hints-next-regexes,next-regexes>>|A comma-separated list of regexes to use for 'next' links.
|
||||||
|<<hints-prev-regexes,prev-regexes>>|A comma-separated list of regexes to use for 'prev' links.
|
|<<hints-prev-regexes,prev-regexes>>|A comma-separated list of regexes to use for 'prev' links.
|
||||||
|==============
|
|==============
|
||||||
@ -1581,7 +1582,7 @@ Default: +pass:[1]+
|
|||||||
|
|
||||||
[[hints-scatter]]
|
[[hints-scatter]]
|
||||||
=== scatter
|
=== scatter
|
||||||
Whether to scatter hint key chains (like Vimium) or not (like dwb).
|
Whether to scatter hint key chains (like Vimium) or not (like dwb). Ignored for number hints.
|
||||||
|
|
||||||
Valid values:
|
Valid values:
|
||||||
|
|
||||||
@ -1618,6 +1619,12 @@ Valid values:
|
|||||||
|
|
||||||
Default: +pass:[true]+
|
Default: +pass:[true]+
|
||||||
|
|
||||||
|
[[hints-auto-follow-timeout]]
|
||||||
|
=== auto-follow-timeout
|
||||||
|
A timeout to inhibit normal-mode key bindings after a successfulauto-follow.
|
||||||
|
|
||||||
|
Default: +pass:[0]+
|
||||||
|
|
||||||
[[hints-next-regexes]]
|
[[hints-next-regexes]]
|
||||||
=== next-regexes
|
=== next-regexes
|
||||||
A comma-separated list of regexes to use for 'next' links.
|
A comma-separated list of regexes to use for 'next' links.
|
||||||
|
@ -23,7 +23,7 @@ import collections
|
|||||||
import functools
|
import functools
|
||||||
import math
|
import math
|
||||||
import re
|
import re
|
||||||
import string
|
from string import ascii_lowercase
|
||||||
|
|
||||||
from PyQt5.QtCore import (pyqtSignal, pyqtSlot, QObject, QEvent, Qt, QUrl,
|
from PyQt5.QtCore import (pyqtSignal, pyqtSlot, QObject, QEvent, Qt, QUrl,
|
||||||
QTimer)
|
QTimer)
|
||||||
@ -67,7 +67,9 @@ class HintContext:
|
|||||||
frames: The QWebFrames to use.
|
frames: The QWebFrames to use.
|
||||||
destroyed_frames: id()'s of QWebFrames which have been destroyed.
|
destroyed_frames: id()'s of QWebFrames which have been destroyed.
|
||||||
(Workaround for https://github.com/The-Compiler/qutebrowser/issues/152)
|
(Workaround for https://github.com/The-Compiler/qutebrowser/issues/152)
|
||||||
|
all_elems: A list of all (elem, label) namedtuples ever created.
|
||||||
elems: A mapping from key strings to (elem, label) namedtuples.
|
elems: A mapping from key strings to (elem, label) namedtuples.
|
||||||
|
May contain less elements than `all_elems` due to filtering.
|
||||||
baseurl: The URL of the current page.
|
baseurl: The URL of the current page.
|
||||||
target: What to do with the opened links.
|
target: What to do with the opened links.
|
||||||
normal/current/tab/tab_fg/tab_bg/window: Get passed to
|
normal/current/tab/tab_fg/tab_bg/window: Get passed to
|
||||||
@ -86,6 +88,7 @@ class HintContext:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
|
self.all_elems = []
|
||||||
self.elems = {}
|
self.elems = {}
|
||||||
self.target = None
|
self.target = None
|
||||||
self.baseurl = None
|
self.baseurl = None
|
||||||
@ -117,6 +120,7 @@ class HintManager(QObject):
|
|||||||
_context: The HintContext for the current invocation.
|
_context: The HintContext for the current invocation.
|
||||||
_win_id: The window ID this HintManager is associated with.
|
_win_id: The window ID this HintManager is associated with.
|
||||||
_tab_id: The tab ID this HintManager is associated with.
|
_tab_id: The tab ID this HintManager is associated with.
|
||||||
|
_filterstr: Used to save the filter string for restoring in rapid mode.
|
||||||
|
|
||||||
Signals:
|
Signals:
|
||||||
mouse_event: Mouse event to be posted in the web view.
|
mouse_event: Mouse event to be posted in the web view.
|
||||||
@ -153,6 +157,7 @@ class HintManager(QObject):
|
|||||||
self._win_id = win_id
|
self._win_id = win_id
|
||||||
self._tab_id = tab_id
|
self._tab_id = tab_id
|
||||||
self._context = None
|
self._context = None
|
||||||
|
self._filterstr = None
|
||||||
self._word_hinter = WordHinter()
|
self._word_hinter = WordHinter()
|
||||||
mode_manager = objreg.get('mode-manager', scope='window',
|
mode_manager = objreg.get('mode-manager', scope='window',
|
||||||
window=win_id)
|
window=win_id)
|
||||||
@ -168,7 +173,7 @@ class HintManager(QObject):
|
|||||||
|
|
||||||
def _cleanup(self):
|
def _cleanup(self):
|
||||||
"""Clean up after hinting."""
|
"""Clean up after hinting."""
|
||||||
for elem in self._context.elems.values():
|
for elem in self._context.all_elems:
|
||||||
try:
|
try:
|
||||||
elem.label.removeFromDocument()
|
elem.label.removeFromDocument()
|
||||||
except webelem.IsNullError:
|
except webelem.IsNullError:
|
||||||
@ -218,7 +223,7 @@ class HintManager(QObject):
|
|||||||
else:
|
else:
|
||||||
chars = config.get('hints', 'chars')
|
chars = config.get('hints', 'chars')
|
||||||
min_chars = config.get('hints', 'min-chars')
|
min_chars = config.get('hints', 'min-chars')
|
||||||
if config.get('hints', 'scatter'):
|
if config.get('hints', 'scatter') and hint_mode != 'number':
|
||||||
return self._hint_scattered(min_chars, chars, elems)
|
return self._hint_scattered(min_chars, chars, elems)
|
||||||
else:
|
else:
|
||||||
return self._hint_linear(min_chars, chars, elems)
|
return self._hint_linear(min_chars, chars, elems)
|
||||||
@ -384,7 +389,7 @@ class HintManager(QObject):
|
|||||||
label.setStyleProperty('left', '{}px !important'.format(left))
|
label.setStyleProperty('left', '{}px !important'.format(left))
|
||||||
label.setStyleProperty('top', '{}px !important'.format(top))
|
label.setStyleProperty('top', '{}px !important'.format(top))
|
||||||
|
|
||||||
def _draw_label(self, elem, text):
|
def _draw_label(self, elem, string):
|
||||||
"""Draw a hint label over an element.
|
"""Draw a hint label over an element.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@ -409,7 +414,7 @@ class HintManager(QObject):
|
|||||||
label = webelem.WebElementWrapper(parent.lastChild())
|
label = webelem.WebElementWrapper(parent.lastChild())
|
||||||
label['class'] = 'qutehint'
|
label['class'] = 'qutehint'
|
||||||
self._set_style_properties(elem, label)
|
self._set_style_properties(elem, label)
|
||||||
label.setPlainText(text)
|
label.setPlainText(string)
|
||||||
return label
|
return label
|
||||||
|
|
||||||
def _show_url_error(self):
|
def _show_url_error(self):
|
||||||
@ -691,15 +696,27 @@ class HintManager(QObject):
|
|||||||
elems = [e for e in elems if filterfunc(e)]
|
elems = [e for e in elems if filterfunc(e)]
|
||||||
if not elems:
|
if not elems:
|
||||||
raise cmdexc.CommandError("No elements found.")
|
raise cmdexc.CommandError("No elements found.")
|
||||||
hints = self._hint_strings(elems)
|
strings = self._hint_strings(elems)
|
||||||
log.hints.debug("hints: {}".format(', '.join(hints)))
|
log.hints.debug("hints: {}".format(', '.join(strings)))
|
||||||
for e, hint in zip(elems, hints):
|
for e, string in zip(elems, strings):
|
||||||
label = self._draw_label(e, hint)
|
label = self._draw_label(e, string)
|
||||||
self._context.elems[hint] = ElemTuple(e, label)
|
elem = ElemTuple(e, label)
|
||||||
|
self._context.all_elems.append(elem)
|
||||||
|
self._context.elems[string] = elem
|
||||||
keyparsers = objreg.get('keyparsers', scope='window',
|
keyparsers = objreg.get('keyparsers', scope='window',
|
||||||
window=self._win_id)
|
window=self._win_id)
|
||||||
keyparser = keyparsers[usertypes.KeyMode.hint]
|
keyparser = keyparsers[usertypes.KeyMode.hint]
|
||||||
keyparser.update_bindings(hints)
|
keyparser.update_bindings(strings)
|
||||||
|
|
||||||
|
def _filter_matches(self, filterstr, elemstr):
|
||||||
|
"""Return True if `filterstr` matches `elemstr`."""
|
||||||
|
# Empty string and None always match
|
||||||
|
if not filterstr:
|
||||||
|
return True
|
||||||
|
filterstr = filterstr.casefold()
|
||||||
|
elemstr = elemstr.casefold()
|
||||||
|
# Do multi-word matching
|
||||||
|
return all(word in elemstr for word in filterstr.split())
|
||||||
|
|
||||||
def follow_prevnext(self, frame, baseurl, prev=False, tab=False,
|
def follow_prevnext(self, frame, baseurl, prev=False, tab=False,
|
||||||
background=False, window=False):
|
background=False, window=False):
|
||||||
@ -844,55 +861,115 @@ class HintManager(QObject):
|
|||||||
def handle_partial_key(self, keystr):
|
def handle_partial_key(self, keystr):
|
||||||
"""Handle a new partial keypress."""
|
"""Handle a new partial keypress."""
|
||||||
log.hints.debug("Handling new keystring: '{}'".format(keystr))
|
log.hints.debug("Handling new keystring: '{}'".format(keystr))
|
||||||
for (text, elems) in self._context.elems.items():
|
for string, elem in self._context.elems.items():
|
||||||
try:
|
try:
|
||||||
if text.startswith(keystr):
|
if string.startswith(keystr):
|
||||||
matched = text[:len(keystr)]
|
matched = string[:len(keystr)]
|
||||||
rest = text[len(keystr):]
|
rest = string[len(keystr):]
|
||||||
match_color = config.get('colors', 'hints.fg.match')
|
match_color = config.get('colors', 'hints.fg.match')
|
||||||
elems.label.setInnerXml(
|
elem.label.setInnerXml(
|
||||||
'<font color="{}">{}</font>{}'.format(
|
'<font color="{}">{}</font>{}'.format(
|
||||||
match_color, matched, rest))
|
match_color, matched, rest))
|
||||||
if self._is_hidden(elems.label):
|
if self._is_hidden(elem.label):
|
||||||
# hidden element which matches again -> show it
|
# hidden element which matches again -> show it
|
||||||
self._show_elem(elems.label)
|
self._show_elem(elem.label)
|
||||||
else:
|
else:
|
||||||
# element doesn't match anymore -> hide it
|
# element doesn't match anymore -> hide it
|
||||||
self._hide_elem(elems.label)
|
self._hide_elem(elem.label)
|
||||||
except webelem.IsNullError:
|
except webelem.IsNullError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
def _filter_number_hints(self):
|
||||||
|
"""Apply filters for numbered hints and renumber them.
|
||||||
|
|
||||||
|
Return:
|
||||||
|
Elements which are still visible
|
||||||
|
"""
|
||||||
|
# renumber filtered hints
|
||||||
|
elems = []
|
||||||
|
for e in self._context.all_elems:
|
||||||
|
try:
|
||||||
|
if not self._is_hidden(e.label):
|
||||||
|
elems.append(e)
|
||||||
|
except webelem.IsNullError:
|
||||||
|
pass
|
||||||
|
if not elems:
|
||||||
|
# Whoops, filtered all hints
|
||||||
|
modeman.leave(self._win_id, usertypes.KeyMode.hint,
|
||||||
|
'all filtered')
|
||||||
|
return
|
||||||
|
|
||||||
|
strings = self._hint_strings(elems)
|
||||||
|
self._context.elems = {}
|
||||||
|
for elem, string in zip(elems, strings):
|
||||||
|
elem.label.setInnerXml(string)
|
||||||
|
self._context.elems[string] = elem
|
||||||
|
keyparsers = objreg.get('keyparsers', scope='window',
|
||||||
|
window=self._win_id)
|
||||||
|
keyparser = keyparsers[usertypes.KeyMode.hint]
|
||||||
|
keyparser.update_bindings(strings, preserve_filter=True)
|
||||||
|
|
||||||
|
return self._context.elems
|
||||||
|
|
||||||
|
def _filter_non_number_hints(self):
|
||||||
|
"""Apply filters for letter/word hints.
|
||||||
|
|
||||||
|
Return:
|
||||||
|
Elements which are still visible
|
||||||
|
"""
|
||||||
|
visible = {}
|
||||||
|
for string, elem in self._context.elems.items():
|
||||||
|
try:
|
||||||
|
if not self._is_hidden(elem.label):
|
||||||
|
visible[string] = elem
|
||||||
|
except webelem.IsNullError:
|
||||||
|
pass
|
||||||
|
if not visible:
|
||||||
|
# Whoops, filtered all hints
|
||||||
|
modeman.leave(self._win_id, usertypes.KeyMode.hint,
|
||||||
|
'all filtered')
|
||||||
|
return visible
|
||||||
|
|
||||||
def filter_hints(self, filterstr):
|
def filter_hints(self, filterstr):
|
||||||
"""Filter displayed hints according to a text.
|
"""Filter displayed hints according to a text.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
filterstr: The string to filter with, or None to show all.
|
filterstr: The string to filter with, or None to use the filter
|
||||||
|
from previous call (saved in `self._filterstr`). If
|
||||||
|
`filterstr` is an empty string or if both `filterstr`
|
||||||
|
and `self._filterstr` are None, all hints are shown.
|
||||||
"""
|
"""
|
||||||
for elems in self._context.elems.values():
|
if filterstr is None:
|
||||||
|
filterstr = self._filterstr
|
||||||
|
else:
|
||||||
|
self._filterstr = filterstr
|
||||||
|
|
||||||
|
for elem in self._context.all_elems:
|
||||||
try:
|
try:
|
||||||
if (filterstr is None or
|
if self._filter_matches(filterstr, str(elem.elem)):
|
||||||
filterstr.casefold() in str(elems.elem).casefold()):
|
if self._is_hidden(elem.label):
|
||||||
if self._is_hidden(elems.label):
|
|
||||||
# hidden element which matches again -> show it
|
# hidden element which matches again -> show it
|
||||||
self._show_elem(elems.label)
|
self._show_elem(elem.label)
|
||||||
else:
|
else:
|
||||||
# element doesn't match anymore -> hide it
|
# element doesn't match anymore -> hide it
|
||||||
self._hide_elem(elems.label)
|
self._hide_elem(elem.label)
|
||||||
except webelem.IsNullError:
|
except webelem.IsNullError:
|
||||||
pass
|
pass
|
||||||
visible = {}
|
|
||||||
for k, e in self._context.elems.items():
|
if config.get('hints', 'mode') == 'number':
|
||||||
try:
|
visible = self._filter_number_hints()
|
||||||
if not self._is_hidden(e.label):
|
else:
|
||||||
visible[k] = e
|
visible = self._filter_non_number_hints()
|
||||||
except webelem.IsNullError:
|
|
||||||
pass
|
if (len(visible) == 1 and
|
||||||
if not visible:
|
|
||||||
# Whoops, filtered all hints
|
|
||||||
modeman.leave(self._win_id, usertypes.KeyMode.hint, 'all filtered')
|
|
||||||
elif (len(visible) == 1 and
|
|
||||||
config.get('hints', 'auto-follow') and
|
config.get('hints', 'auto-follow') and
|
||||||
filterstr is not None):
|
filterstr is not None):
|
||||||
|
# apply auto-follow-timeout
|
||||||
|
timeout = config.get('hints', 'auto-follow-timeout')
|
||||||
|
keyparsers = objreg.get('keyparsers', scope='window',
|
||||||
|
window=self._win_id)
|
||||||
|
normal_parser = keyparsers[usertypes.KeyMode.normal]
|
||||||
|
normal_parser.set_inhibited_timeout(timeout)
|
||||||
# unpacking gets us the first (and only) key in the dict.
|
# unpacking gets us the first (and only) key in the dict.
|
||||||
self.fire(*visible)
|
self.fire(*visible)
|
||||||
|
|
||||||
@ -950,11 +1027,11 @@ class HintManager(QObject):
|
|||||||
modeman.maybe_leave(self._win_id, usertypes.KeyMode.hint,
|
modeman.maybe_leave(self._win_id, usertypes.KeyMode.hint,
|
||||||
'followed')
|
'followed')
|
||||||
else:
|
else:
|
||||||
# Show all hints again
|
# Reset filtering
|
||||||
self.filter_hints(None)
|
self.filter_hints(None)
|
||||||
# Undo keystring highlighting
|
# Undo keystring highlighting
|
||||||
for (text, elems) in self._context.elems.items():
|
for string, elem in self._context.elems.items():
|
||||||
elems.label.setInnerXml(text)
|
elem.label.setInnerXml(string)
|
||||||
handler()
|
handler()
|
||||||
|
|
||||||
@cmdutils.register(instance='hintmanager', scope='tab', hide=True,
|
@cmdutils.register(instance='hintmanager', scope='tab', hide=True,
|
||||||
@ -978,13 +1055,13 @@ class HintManager(QObject):
|
|||||||
def on_contents_size_changed(self, _size):
|
def on_contents_size_changed(self, _size):
|
||||||
"""Reposition hints if contents size changed."""
|
"""Reposition hints if contents size changed."""
|
||||||
log.hints.debug("Contents size changed...!")
|
log.hints.debug("Contents size changed...!")
|
||||||
for elems in self._context.elems.values():
|
for e in self._context.all_elems:
|
||||||
try:
|
try:
|
||||||
if elems.elem.webFrame() is None:
|
if e.elem.webFrame() is None:
|
||||||
# This sometimes happens for some reason...
|
# This sometimes happens for some reason...
|
||||||
elems.label.removeFromDocument()
|
e.label.removeFromDocument()
|
||||||
continue
|
continue
|
||||||
self._set_style_position(elems.elem, elems.label)
|
self._set_style_position(e.elem, e.label)
|
||||||
except webelem.IsNullError:
|
except webelem.IsNullError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@ -1021,7 +1098,7 @@ class WordHinter:
|
|||||||
self.dictionary = dictionary
|
self.dictionary = dictionary
|
||||||
try:
|
try:
|
||||||
with open(dictionary, encoding="UTF-8") as wordfile:
|
with open(dictionary, encoding="UTF-8") as wordfile:
|
||||||
alphabet = set(string.ascii_lowercase)
|
alphabet = set(ascii_lowercase)
|
||||||
hints = set()
|
hints = set()
|
||||||
lines = (line.rstrip().lower() for line in wordfile)
|
lines = (line.rstrip().lower() for line in wordfile)
|
||||||
for word in lines:
|
for word in lines:
|
||||||
|
@ -898,7 +898,7 @@ def data(readonly=False):
|
|||||||
('scatter',
|
('scatter',
|
||||||
SettingValue(typ.Bool(), 'true'),
|
SettingValue(typ.Bool(), 'true'),
|
||||||
"Whether to scatter hint key chains (like Vimium) or not (like "
|
"Whether to scatter hint key chains (like Vimium) or not (like "
|
||||||
"dwb)."),
|
"dwb). Ignored for number hints."),
|
||||||
|
|
||||||
('uppercase',
|
('uppercase',
|
||||||
SettingValue(typ.Bool(), 'false'),
|
SettingValue(typ.Bool(), 'false'),
|
||||||
@ -913,6 +913,11 @@ def data(readonly=False):
|
|||||||
"Follow a hint immediately when the hint text is completely "
|
"Follow a hint immediately when the hint text is completely "
|
||||||
"matched."),
|
"matched."),
|
||||||
|
|
||||||
|
('auto-follow-timeout',
|
||||||
|
SettingValue(typ.Int(), '0'),
|
||||||
|
"A timeout to inhibit normal-mode key bindings after a successful"
|
||||||
|
"auto-follow."),
|
||||||
|
|
||||||
('next-regexes',
|
('next-regexes',
|
||||||
SettingValue(typ.RegexList(flags=re.IGNORECASE),
|
SettingValue(typ.RegexList(flags=re.IGNORECASE),
|
||||||
r'\bnext\b,\bmore\b,\bnewer\b,\b[>→≫]\b,\b(>>|»)\b,'
|
r'\bnext\b,\bmore\b,\bnewer\b,\b[>→≫]\b,\b(>>|»)\b,'
|
||||||
|
@ -49,6 +49,9 @@ class NormalKeyParser(keyparser.CommandKeyParser):
|
|||||||
self.read_config('normal')
|
self.read_config('normal')
|
||||||
self._partial_timer = usertypes.Timer(self, 'partial-match')
|
self._partial_timer = usertypes.Timer(self, 'partial-match')
|
||||||
self._partial_timer.setSingleShot(True)
|
self._partial_timer.setSingleShot(True)
|
||||||
|
self._inhibited = False
|
||||||
|
self._inhibited_timer = usertypes.Timer(self, 'normal-inhibited')
|
||||||
|
self._inhibited_timer.setSingleShot(True)
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return utils.get_repr(self)
|
return utils.get_repr(self)
|
||||||
@ -63,6 +66,10 @@ class NormalKeyParser(keyparser.CommandKeyParser):
|
|||||||
A self.Match member.
|
A self.Match member.
|
||||||
"""
|
"""
|
||||||
txt = e.text().strip()
|
txt = e.text().strip()
|
||||||
|
if self._inhibited:
|
||||||
|
self._debug_log("Ignoring key '{}', because the normal mode is "
|
||||||
|
"currently inhibited.".format(txt))
|
||||||
|
return self.Match.none
|
||||||
if not self._keystring and any(txt == c for c in STARTCHARS):
|
if not self._keystring and any(txt == c for c in STARTCHARS):
|
||||||
message.set_cmd_text(self._win_id, txt)
|
message.set_cmd_text(self._win_id, txt)
|
||||||
return self.Match.definitive
|
return self.Match.definitive
|
||||||
@ -75,6 +82,15 @@ class NormalKeyParser(keyparser.CommandKeyParser):
|
|||||||
self._partial_timer.start()
|
self._partial_timer.start()
|
||||||
return match
|
return match
|
||||||
|
|
||||||
|
def set_inhibited_timeout(self, timeout):
|
||||||
|
if timeout != 0:
|
||||||
|
self._debug_log("Inhibiting the normal mode for {}ms.".format(
|
||||||
|
timeout))
|
||||||
|
self._inhibited = True
|
||||||
|
self._inhibited_timer.setInterval(timeout)
|
||||||
|
self._inhibited_timer.timeout.connect(self._clear_inhibited)
|
||||||
|
self._inhibited_timer.start()
|
||||||
|
|
||||||
@pyqtSlot()
|
@pyqtSlot()
|
||||||
def _clear_partial_match(self):
|
def _clear_partial_match(self):
|
||||||
"""Clear a partial keystring after a timeout."""
|
"""Clear a partial keystring after a timeout."""
|
||||||
@ -83,6 +99,12 @@ class NormalKeyParser(keyparser.CommandKeyParser):
|
|||||||
self._keystring = ''
|
self._keystring = ''
|
||||||
self.keystring_updated.emit(self._keystring)
|
self.keystring_updated.emit(self._keystring)
|
||||||
|
|
||||||
|
@pyqtSlot()
|
||||||
|
def _clear_inhibited(self):
|
||||||
|
"""Reset inhibition state after a timeout."""
|
||||||
|
self._debug_log("Releasing inhibition state of normal mode.")
|
||||||
|
self._inhibited = False
|
||||||
|
|
||||||
@pyqtSlot()
|
@pyqtSlot()
|
||||||
def _stop_timers(self):
|
def _stop_timers(self):
|
||||||
super()._stop_timers()
|
super()._stop_timers()
|
||||||
@ -92,6 +114,12 @@ class NormalKeyParser(keyparser.CommandKeyParser):
|
|||||||
except TypeError:
|
except TypeError:
|
||||||
# no connections
|
# no connections
|
||||||
pass
|
pass
|
||||||
|
self._inhibited_timer.stop()
|
||||||
|
try:
|
||||||
|
self._inhibited_timer.timeout.disconnect(self._clear_inhibited)
|
||||||
|
except TypeError:
|
||||||
|
# no connections
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class PromptKeyParser(keyparser.CommandKeyParser):
|
class PromptKeyParser(keyparser.CommandKeyParser):
|
||||||
@ -153,6 +181,11 @@ class HintKeyParser(keyparser.CommandKeyParser):
|
|||||||
elif self._last_press == LastPress.keystring and self._keystring:
|
elif self._last_press == LastPress.keystring and self._keystring:
|
||||||
self._keystring = self._keystring[:-1]
|
self._keystring = self._keystring[:-1]
|
||||||
self.keystring_updated.emit(self._keystring)
|
self.keystring_updated.emit(self._keystring)
|
||||||
|
if not self._keystring and self._filtertext:
|
||||||
|
# Switch back to hint filtering mode (this can happen only
|
||||||
|
# in numeric mode after the number has been deleted).
|
||||||
|
hintmanager.filter_hints(self._filtertext)
|
||||||
|
self._last_press = LastPress.filtertext
|
||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
return super()._handle_special_key(e)
|
return super()._handle_special_key(e)
|
||||||
@ -203,13 +236,16 @@ class HintKeyParser(keyparser.CommandKeyParser):
|
|||||||
# execute as command
|
# execute as command
|
||||||
super().execute(cmdstr, keytype, count)
|
super().execute(cmdstr, keytype, count)
|
||||||
|
|
||||||
def update_bindings(self, strings):
|
def update_bindings(self, strings, preserve_filter=False):
|
||||||
"""Update bindings when the hint strings changed.
|
"""Update bindings when the hint strings changed.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
strings: A list of hint strings.
|
strings: A list of hint strings.
|
||||||
|
preserve_filter: Whether to keep the current value of
|
||||||
|
`self._filtertext`.
|
||||||
"""
|
"""
|
||||||
self.bindings = {s: s for s in strings}
|
self.bindings = {s: s for s in strings}
|
||||||
|
if not preserve_filter:
|
||||||
self._filtertext = ''
|
self._filtertext = ''
|
||||||
|
|
||||||
@pyqtSlot(str)
|
@pyqtSlot(str)
|
||||||
|
35
tests/end2end/data/hints/issue1186.html
Normal file
35
tests/end2end/data/hints/issue1186.html
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>Issue 1186</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<p>
|
||||||
|
This page contains 10 hints to test backspace handling, see #1186.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
This requires setting hints -> mode to number.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
When pressing f, x, 0, Backspace, only hints starting with x should be
|
||||||
|
shown.
|
||||||
|
</p>
|
||||||
|
<ul>
|
||||||
|
<li><a href="/data/numbers/1.txt">x one</a></li>
|
||||||
|
<li><a href="/data/numbers/2.txt">x two</a></li>
|
||||||
|
<li><a href="/data/numbers/3.txt">x three</a></li>
|
||||||
|
<li><a href="/data/numbers/4.txt">x four</a></li>
|
||||||
|
<li><a href="/data/numbers/5.txt">x five</a></li>
|
||||||
|
<li><a href="/data/numbers/6.txt">x six</a></li>
|
||||||
|
<li><a href="/data/numbers/7.txt">x seven</a></li>
|
||||||
|
<li><a href="/data/numbers/8.txt">x eight</a></li>
|
||||||
|
<li><a href="/data/numbers/9.txt">x nine</a></li>
|
||||||
|
<li><a href="/data/numbers/10.txt">x ten</a></li>
|
||||||
|
<li><a href="/data/numbers/11.txt">x eleven</a></li>
|
||||||
|
<li><a href="/data/numbers/12.txt">twelve</a></li>
|
||||||
|
</ul>
|
||||||
|
</body>
|
||||||
|
</html>
|
35
tests/end2end/data/hints/number.html
Normal file
35
tests/end2end/data/hints/number.html
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>Numbered hints</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<p>
|
||||||
|
This page contains various links to test numbered hints. This requires setting hints -> mode to number.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Related issues:
|
||||||
|
<ul>
|
||||||
|
<li>#308 - Renumber hints when filtering them in number mode.</li>
|
||||||
|
<li>#576 - Keep hint filtering when rapid hinting in number mode</li>
|
||||||
|
<li>#674 (comment) - Multi-word matching for hints </li>
|
||||||
|
</ul>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<ul>
|
||||||
|
<li><a href="/data/numbers/1.txt">one</a></li>
|
||||||
|
<li><a href="/data/numbers/2.txt">two</a></li>
|
||||||
|
<li><a href="/data/numbers/3.txt">three</a></li>
|
||||||
|
<li><a href="/data/numbers/4.txt">four</a></li>
|
||||||
|
<li><a href="/data/numbers/5.txt">five</a></li>
|
||||||
|
<li><a href="/data/numbers/6.txt">six</a></li>
|
||||||
|
<li><a href="/data/numbers/7.txt">seven</a></li>
|
||||||
|
<li><a href="/data/numbers/8.txt">eight</a></li>
|
||||||
|
<li><a href="/data/numbers/9.txt">nine</a></li>
|
||||||
|
<li><a href="/data/numbers/10.txt">ten</a></li>
|
||||||
|
<li><a href="/data/numbers/11.txt">tenorhorn posaune (eleven)</a></li>
|
||||||
|
</ul>
|
||||||
|
</body>
|
||||||
|
</html>
|
@ -140,3 +140,80 @@ Feature: Using hints
|
|||||||
Then the following tabs should be open:
|
Then the following tabs should be open:
|
||||||
- data/hints/iframe_target.html
|
- data/hints/iframe_target.html
|
||||||
- data/hello.txt (active)
|
- data/hello.txt (active)
|
||||||
|
|
||||||
|
### hints -> auto-follow-timeout
|
||||||
|
|
||||||
|
Scenario: Ignoring key presses after auto-following hints
|
||||||
|
When I set hints -> auto-follow-timeout to 200
|
||||||
|
And I set hints -> mode to number
|
||||||
|
And I run :bind --force , message-error "This should not happen"
|
||||||
|
And I open data/hints/html/simple.html
|
||||||
|
And I run :hint all
|
||||||
|
And I press the key "f"
|
||||||
|
And I wait until data/hello.txt is loaded
|
||||||
|
And I press the key ","
|
||||||
|
# Waiting here so we don't affect the next test
|
||||||
|
And I wait for "Releasing inhibition state of normal mode." in the log
|
||||||
|
Then "Ignoring key ',', because the normal mode is currently inhibited." should be logged
|
||||||
|
|
||||||
|
Scenario: Turning off auto-follow-timeout
|
||||||
|
When I set hints -> auto-follow-timeout to 0
|
||||||
|
And I set hints -> mode to number
|
||||||
|
And I run :bind --force , message-info "Keypress worked!"
|
||||||
|
And I open data/hints/html/simple.html
|
||||||
|
And I run :hint all
|
||||||
|
And I press the key "f"
|
||||||
|
And I wait until data/hello.txt is loaded
|
||||||
|
And I press the key ","
|
||||||
|
Then the message "Keypress worked!" should be shown
|
||||||
|
|
||||||
|
### Number hint mode
|
||||||
|
|
||||||
|
# https://github.com/The-Compiler/qutebrowser/issues/308
|
||||||
|
Scenario: Renumbering hints when filtering
|
||||||
|
When I open data/hints/number.html
|
||||||
|
And I set hints -> mode to number
|
||||||
|
And I run :hint all
|
||||||
|
And I press the key "s"
|
||||||
|
And I run :follow-hint 1
|
||||||
|
Then data/numbers/7.txt should be loaded
|
||||||
|
|
||||||
|
# https://github.com/The-Compiler/qutebrowser/issues/576
|
||||||
|
Scenario: Keeping hint filter in rapid mode
|
||||||
|
When I open data/hints/number.html
|
||||||
|
And I set hints -> mode to number
|
||||||
|
And I run :hint all tab-bg --rapid
|
||||||
|
And I press the key "t"
|
||||||
|
And I run :follow-hint 0
|
||||||
|
And I run :follow-hint 1
|
||||||
|
Then data/numbers/2.txt should be loaded
|
||||||
|
And data/numbers/3.txt should be loaded
|
||||||
|
|
||||||
|
# https://github.com/The-Compiler/qutebrowser/issues/1186
|
||||||
|
Scenario: Keeping hints filter when using backspace
|
||||||
|
When I open data/hints/issue1186.html
|
||||||
|
And I set hints -> mode to number
|
||||||
|
And I run :hint all
|
||||||
|
And I press the key "x"
|
||||||
|
And I press the key "0"
|
||||||
|
And I press the key "<Backspace>"
|
||||||
|
And I run :follow-hint 11
|
||||||
|
Then the error "No hint 11!" should be shown
|
||||||
|
|
||||||
|
# https://github.com/The-Compiler/qutebrowser/issues/674#issuecomment-165096744
|
||||||
|
Scenario: Multi-word matching
|
||||||
|
When I open data/hints/number.html
|
||||||
|
And I set hints -> mode to number
|
||||||
|
And I set hints -> auto-follow to true
|
||||||
|
And I set hints -> auto-follow-timeout to 0
|
||||||
|
And I run :hint all
|
||||||
|
And I press the keys "ten pos"
|
||||||
|
Then data/numbers/11.txt should be loaded
|
||||||
|
|
||||||
|
Scenario: Scattering is ignored with number hints
|
||||||
|
When I open data/hints/number.html
|
||||||
|
And I set hints -> mode to number
|
||||||
|
And I set hints -> scatter to true
|
||||||
|
And I run :hint all
|
||||||
|
And I run :follow-hint 00
|
||||||
|
Then data/numbers/1.txt should be loaded
|
||||||
|
Loading…
Reference in New Issue
Block a user