Merge branch 'fix_hints_autofollow' of https://github.com/lahwaacz/qutebrowser into lahwaacz-fix_hints_autofollow

This commit is contained in:
Florian Bruhin 2016-08-16 12:58:56 +02:00
commit 8e6d784fd7
6 changed files with 267 additions and 89 deletions

View File

@ -62,7 +62,6 @@ class HintContext:
"""Context namespace used for hinting.
Attributes:
frames: The QWebFrames to use.
all_elems: A list of all (elem, label) namedtuples ever created.
elems: A mapping from key strings to (elem, label) namedtuples.
May contain less elements than `all_elems` due to filtering.
@ -79,6 +78,7 @@ class HintContext:
to_follow: The link to follow when enter is pressed.
args: Custom arguments for userscript/spawn
rapid: Whether to do rapid hinting.
filterstr: Used to save the filter string for restoring in rapid mode.
tab: The WebTab object we started hinting in.
group: The group of web elements to hint.
"""
@ -90,7 +90,7 @@ class HintContext:
self.baseurl = None
self.to_follow = None
self.rapid = False
self.frames = []
self.filterstr = None
self.args = []
self.tab = None
self.group = None
@ -309,7 +309,6 @@ class HintManager(QObject):
_context: The HintContext for the current invocation.
_win_id: The window 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:
See HintActions
@ -342,7 +341,6 @@ class HintManager(QObject):
self._win_id = win_id
self._tab_id = tab_id
self._context = None
self._filterstr = None
self._word_hinter = WordHinter()
self._actions = HintActions(win_id)
@ -381,7 +379,6 @@ class HintManager(QObject):
window=self._win_id)
message_bridge.maybe_reset_text(text)
self._context = None
self._filterstr = None
def _hint_strings(self, elems):
"""Calculate the hint strings for elems.
@ -394,6 +391,8 @@ class HintManager(QObject):
Return:
A list of hint strings, in the same order as the elements.
"""
if not elems:
return []
hint_mode = self._context.hint_mode
if hint_mode == 'word':
try:
@ -630,6 +629,15 @@ class HintManager(QObject):
# Do multi-word matching
return all(word in elemstr for word in filterstr.split())
def _filter_matches_exactly(self, filterstr, elemstr):
"""Return True if `filterstr` exactly matches `elemstr`."""
# Empty string and None never match
if not filterstr:
return False
filterstr = filterstr.casefold()
elemstr = elemstr.casefold()
return filterstr == elemstr
def _start_cb(self, elems):
"""Initialize the elements and labels based on the context set."""
filterfunc = webelem.FILTERS.get(self._context.group, lambda e: True)
@ -656,6 +664,9 @@ class HintManager(QObject):
modeman.enter(self._win_id, usertypes.KeyMode.hint,
'HintManager.start')
# to make auto-follow == 'always' work
self._handle_auto_follow()
@cmdutils.register(instance='hintmanager', scope='tab', name='hint',
star_args_optional=True, maxsplit=2,
backend=usertypes.Backend.QtWebKit)
@ -753,7 +764,6 @@ class HintManager(QObject):
self._context.baseurl = tabbed_browser.current_url()
except qtutils.QtValueError:
raise cmdexc.CommandError("No URL set for this page yet!")
self._context.tab = tab
self._context.args = args
self._context.group = group
selector = webelem.SELECTORS[self._context.group]
@ -767,6 +777,51 @@ class HintManager(QObject):
return self._context.hint_mode
def _get_visible_hints(self):
"""Get elements which are currently visible."""
visible = {}
for string, elem in self._context.elems.items():
try:
if not self._is_hidden(elem.label):
visible[string] = elem
except webelem.Error:
pass
return visible
def _handle_auto_follow(self, keystr="", filterstr="", visible=None):
"""Handle the auto-follow option."""
if visible is None:
visible = self._get_visible_hints()
if len(visible) != 1:
return
auto_follow = config.get('hints', 'auto-follow')
if auto_follow == "always":
follow = True
elif auto_follow == "unique-match":
follow = keystr or filterstr
elif auto_follow == "full-match":
elemstr = str(list(visible.values())[0].elem)
filter_match = self._filter_matches_exactly(filterstr, elemstr)
follow = (keystr in visible) or filter_match
else:
follow = False
# save the keystr of the only one visible hint to be picked up
# later by self.follow_hint
self._context.to_follow = list(visible.keys())[0]
if follow:
# 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.
self._fire(*visible)
def handle_partial_key(self, keystr):
"""Handle a new partial keypress."""
log.hints.debug("Handling new keystring: '{}'".format(keystr))
@ -790,57 +845,7 @@ class HintManager(QObject):
self._hide_elem(elem.label)
except webelem.Error:
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.Error:
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.set_inner_xml(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.Error:
pass
if not visible:
# Whoops, filtered all hints
modeman.leave(self._win_id, usertypes.KeyMode.hint,
'all filtered')
return visible
self._handle_auto_follow(keystr=keystr)
def filter_hints(self, filterstr):
"""Filter displayed hints according to a text.
@ -852,13 +857,15 @@ class HintManager(QObject):
and `self._filterstr` are None, all hints are shown.
"""
if filterstr is None:
filterstr = self._filterstr
filterstr = self._context.filterstr
else:
self._filterstr = filterstr
self._context.filterstr = filterstr
visible = []
for elem in self._context.all_elems:
try:
if self._filter_matches(filterstr, str(elem.elem)):
visible.append(elem)
if self._is_hidden(elem.label):
# hidden element which matches again -> show it
self._show_elem(elem.label)
@ -868,35 +875,37 @@ class HintManager(QObject):
except webelem.Error:
pass
if self._context.hint_mode == 'number':
visible = self._filter_number_hints()
else:
visible = self._filter_non_number_hints()
if not visible:
# Whoops, filtered all hints
modeman.leave(self._win_id, usertypes.KeyMode.hint,
'all filtered')
return
if (len(visible) == 1 and
config.get('hints', 'auto-follow') and
filterstr is not None):
# apply auto-follow-timeout
timeout = config.get('hints', 'auto-follow-timeout')
if self._context.hint_mode == 'number':
# renumber filtered hints
strings = self._hint_strings(visible)
self._context.elems = {}
for elem, string in zip(visible, strings):
elem.label.set_inner_xml(string)
self._context.elems[string] = elem
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.
self.fire(*visible)
keyparser = keyparsers[usertypes.KeyMode.hint]
keyparser.update_bindings(strings, preserve_filter=True)
def fire(self, keystr, force=False):
# Note: filter_hints can be called with non-None filterstr only
# when number mode is active
if filterstr is not None:
# pass self._context.elems as the dict of visible hints
self._handle_auto_follow(filterstr=filterstr,
visible=self._context.elems)
def _fire(self, keystr):
"""Fire a completed hint.
Args:
keystr: The keychain string to follow.
force: When True, follow even when auto-follow is false.
"""
if not (force or config.get('hints', 'auto-follow')):
self.handle_partial_key(keystr)
self._context.to_follow = keystr
return
# Handlers which take a QWebElement
elem_handlers = {
Target.normal: self._actions.click,
@ -969,7 +978,7 @@ class HintManager(QObject):
keystring = self._context.to_follow
elif keystring not in self._context.elems:
raise cmdexc.CommandError("No hint {}!".format(keystring))
self.fire(keystring, force=True)
self._fire(keystring)
@pyqtSlot()
def on_contents_size_changed(self):

View File

@ -365,6 +365,8 @@ class ConfigManager(QObject):
_get_value_transformer({'false': 'none', 'true': 'debug'}),
('ui', 'keyhint-blacklist'):
_get_value_transformer({'false': '*', 'true': ''}),
('hints', 'auto-follow'):
_get_value_transformer({'false': 'never', 'true': 'unique-match'}),
}
changed = pyqtSignal(str, str)

View File

@ -936,9 +936,21 @@ def data(readonly=False):
"The dictionary file to be used by the word hints."),
('auto-follow',
SettingValue(typ.Bool(), 'true'),
"Follow a hint immediately when the hint text is completely "
"matched."),
SettingValue(typ.String(
valid_values=typ.ValidValues(
('always', "Auto-follow whenever there is only a single "
"hint on a page."),
('unique-match', "Auto-follow whenever there is a unique "
"non-empty match in either the hint string (word mode) "
"or filter (number mode)."),
('full-match', "Follow the hint when the user typed the "
"whole hint (letter, word or number mode) or the "
"element's text (only in number mode)."),
('never', "The user will always need to press Enter to "
"follow a hint."),
)), 'unique-match'),
"Controls when a hint can be automatically followed without the "
"user pressing Enter."),
('auto-follow-timeout',
SettingValue(typ.Int(), '0'),

View File

@ -231,7 +231,7 @@ class HintKeyParser(keyparser.CommandKeyParser):
if keytype == self.Type.chain:
hintmanager = objreg.get('hintmanager', scope='tab',
window=self._win_id, tab='current')
hintmanager.fire(cmdstr)
hintmanager.handle_partial_key(cmdstr)
else:
# execute as command
super().execute(cmdstr, keytype, count)

View File

@ -39,10 +39,10 @@ Feature: Using hints
- data/hello.txt (active)
Scenario: Entering and leaving hinting mode (issue 1464)
When I open data/hints/html/simple.html
And I run :hint
And I run :fake-key -g <Esc>
Then no crash should happen
When I open data/hints/html/simple.html
And I run :hint
And I run :fake-key -g <Esc>
Then no crash should happen
Scenario: Using :hint spawn with flags and -- (issue 797)
When I open data/hints/html/simple.html
@ -225,7 +225,7 @@ Feature: Using hints
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 to unique-match
And I set hints -> auto-follow-timeout to 0
And I run :hint all
And I press the keys "ten pos"
@ -265,3 +265,133 @@ Feature: Using hints
And I press the key "s"
And I run :follow-hint 1
Then data/numbers/7.txt should be loaded
### auto-follow option
Scenario: Using hints -> auto-follow == 'always' in letter mode
When I open data/hints/html/simple.html
And I set hints -> mode to letter
And I set hints -> auto-follow to always
And I run :hint
Then data/hello.txt should be loaded
# unique-match is actually the same as full-match in letter mode
Scenario: Using hints -> auto-follow == 'unique-match' in letter mode
When I open data/hints/html/simple.html
And I set hints -> mode to letter
And I set hints -> auto-follow to unique-match
And I run :hint
And I press the key "a"
Then data/hello.txt should be loaded
Scenario: Using hints -> auto-follow == 'full-match' in letter mode
When I open data/hints/html/simple.html
And I set hints -> mode to letter
And I set hints -> auto-follow to full-match
And I run :hint
And I press the key "a"
Then data/hello.txt should be loaded
Scenario: Using hints -> auto-follow == 'never' without Enter in letter mode
When I open data/hints/html/simple.html
And I set hints -> mode to letter
And I set hints -> auto-follow to never
And I run :hint
And I press the key "a"
Then "Leaving mode KeyMode.hint (reason: followed)" should not be logged
Scenario: Using hints -> auto-follow == 'never' in letter mode
When I open data/hints/html/simple.html
And I set hints -> mode to letter
And I set hints -> auto-follow to never
And I run :hint
And I press the key "a"
And I press the key "<Enter>"
Then data/hello.txt should be loaded
Scenario: Using hints -> auto-follow == 'always' in number mode
When I open data/hints/html/simple.html
And I set hints -> mode to number
And I set hints -> auto-follow to always
And I run :hint
Then data/hello.txt should be loaded
Scenario: Using hints -> auto-follow == 'unique-match' in number mode
When I open data/hints/html/simple.html
And I set hints -> mode to number
And I set hints -> auto-follow to unique-match
And I run :hint
And I press the key "f"
Then data/hello.txt should be loaded
Scenario: Using hints -> auto-follow == 'full-match' in number mode
When I open data/hints/html/simple.html
And I set hints -> mode to number
And I set hints -> auto-follow to full-match
And I run :hint
# this actually presses the keys one by one
And I press the key "follow me!"
Then data/hello.txt should be loaded
Scenario: Using hints -> auto-follow == 'never' without Enter in number mode
When I open data/hints/html/simple.html
And I set hints -> mode to number
And I set hints -> auto-follow to never
And I run :hint
# this actually presses the keys one by one
And I press the key "follow me!"
Then "Leaving mode KeyMode.hint (reason: followed)" should not be logged
Scenario: Using hints -> auto-follow == 'never' in number mode
When I open data/hints/html/simple.html
And I set hints -> mode to number
And I set hints -> auto-follow to never
And I run :hint
# this actually presses the keys one by one
And I press the key "follow me!"
And I press the key "<Enter>"
Then data/hello.txt should be loaded
Scenario: Using hints -> auto-follow == 'always' in word mode
When I open data/hints/html/simple.html
And I set hints -> mode to word
And I set hints -> auto-follow to always
And I run :hint
Then data/hello.txt should be loaded
Scenario: Using hints -> auto-follow == 'unique-match' in word mode
When I open data/hints/html/simple.html
And I set hints -> mode to word
And I set hints -> auto-follow to unique-match
And I run :hint
# the link gets "hello" as the hint
And I press the key "h"
Then data/hello.txt should be loaded
Scenario: Using hints -> auto-follow == 'full-match' in word mode
When I open data/hints/html/simple.html
And I set hints -> mode to word
And I set hints -> auto-follow to full-match
And I run :hint
# this actually presses the keys one by one
And I press the key "hello"
Then data/hello.txt should be loaded
Scenario: Using hints -> auto-follow == 'never' without Enter in word mode
When I open data/hints/html/simple.html
And I set hints -> mode to word
And I set hints -> auto-follow to never
And I run :hint
# this actually presses the keys one by one
And I press the key "hello"
Then "Leaving mode KeyMode.hint (reason: followed)" should not be logged
Scenario: Using hints -> auto-follow == 'never' in word mode
When I open data/hints/html/simple.html
And I set hints -> mode to word
And I set hints -> auto-follow to never
And I run :hint
# this actually presses the keys one by one
And I press the key "hello"
And I press the key "<Enter>"
Then data/hello.txt should be loaded

View File

@ -17,5 +17,30 @@
# You should have received a copy of the GNU General Public License
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
import textwrap
import pytest
import pytest_bdd as bdd
bdd.scenarios('hints.feature')
@pytest.fixture(autouse=True)
def set_up_word_hints(tmpdir, quteproc):
dict_file = tmpdir / 'dict'
dict_file.write(textwrap.dedent("""
one
two
three
four
five
six
seven
eight
nine
ten
eleven
twelve
thirteen
"""))
quteproc.set_setting('hints', 'dictionary', str(dict_file))