Do more sophisticated clicking for hints with QtWebEngine

We now use click() or focus() in JS if possible, or manually follow links in a
href attribute.

While this probably introduces some new corner cases, it fixes a handful of
older ones:

- window.open() in JS can now be handled correctly as we don't need hacks in
  createWindow anymore.
- Focusing input fields with images now works - fixes #1613, #1879
- Hinting now works better on QtWebEngine with Qt 5.8 - fixes #2273

Also see #70.
This commit is contained in:
Florian Bruhin 2017-02-03 22:20:15 +01:00
parent ba2f4fb1b9
commit 545539f28d
14 changed files with 148 additions and 74 deletions

View File

@ -1036,7 +1036,7 @@ Clear the currently entered key chain.
[[click-element]] [[click-element]]
=== click-element === click-element
Syntax: +:click-element [*--target* 'target'] 'filter' 'value'+ Syntax: +:click-element [*--target* 'target'] [*--force-event*] 'filter' 'value'+
Click the element matching the given filter. Click the element matching the given filter.
@ -1049,6 +1049,7 @@ The given filter needs to result in exactly one element, otherwise, an error is
==== optional arguments ==== optional arguments
* +*-t*+, +*--target*+: How to open the clicked element (normal/tab/tab-bg/window). * +*-t*+, +*--target*+: How to open the clicked element (normal/tab/tab-bg/window).
* +*-f*+, +*--force-event*+: Force generating a fake click event.
[[command-accept]] [[command-accept]]
=== command-accept === command-accept

View File

@ -1545,7 +1545,8 @@ class CommandDispatcher:
@cmdutils.argument('filter_', choices=['id']) @cmdutils.argument('filter_', choices=['id'])
def click_element(self, filter_: str, value, *, def click_element(self, filter_: str, value, *,
target: usertypes.ClickTarget= target: usertypes.ClickTarget=
usertypes.ClickTarget.normal): usertypes.ClickTarget.normal,
force_event=False):
"""Click the element matching the given filter. """Click the element matching the given filter.
The given filter needs to result in exactly one element, otherwise, an The given filter needs to result in exactly one element, otherwise, an
@ -1556,6 +1557,7 @@ class CommandDispatcher:
id: Get an element based on its ID. id: Get an element based on its ID.
value: The value to filter for. value: The value to filter for.
target: How to open the clicked element (normal/tab/tab-bg/window). target: How to open the clicked element (normal/tab/tab-bg/window).
force_event: Force generating a fake click event.
""" """
tab = self._current_widget() tab = self._current_widget()
@ -1565,7 +1567,7 @@ class CommandDispatcher:
message.error("No element found with id {}!".format(value)) message.error("No element found with id {}!".format(value))
return return
try: try:
elem.click(target) elem.click(target, force_event=force_event)
except webelem.Error as e: except webelem.Error as e:
message.error(str(e)) message.error(str(e))
return return

View File

@ -93,7 +93,10 @@ class MouseEventFilter(QObject):
return True return True
self._ignore_wheel_event = True self._ignore_wheel_event = True
self._tab.elements.find_at_pos(e.pos(), self._mousepress_insertmode_cb)
if e.button() != Qt.NoButton:
self._tab.elements.find_at_pos(e.pos(),
self._mousepress_insertmode_cb)
return False return False

View File

@ -33,7 +33,8 @@ from PyQt5.QtCore import QUrl, Qt, QEvent, QTimer
from PyQt5.QtGui import QMouseEvent from PyQt5.QtGui import QMouseEvent
from qutebrowser.config import config from qutebrowser.config import config
from qutebrowser.utils import log, usertypes, utils, qtutils from qutebrowser.keyinput import modeman
from qutebrowser.utils import log, usertypes, utils, qtutils, objreg
Group = usertypes.enum('Group', ['all', 'links', 'images', 'url', 'prevnext', Group = usertypes.enum('Group', ['all', 'links', 'images', 'url', 'prevnext',
@ -322,14 +323,8 @@ class AbstractWebElement(collections.abc.MutableMapping):
raise Error("Element position is out of view!") raise Error("Element position is out of view!")
return pos return pos
def click(self, click_target): def _click_fake_event(self, click_target):
"""Simulate a click on the element.""" """Send a fake click event to the element."""
# FIXME:qtwebengine do we need this?
# self._widget.setFocus()
# For QtWebKit
self._tab.data.override_target = click_target
pos = self._mouse_pos() pos = self._mouse_pos()
log.webelem.debug("Sending fake click to {!r} at position {} with " log.webelem.debug("Sending fake click to {!r} at position {} with "
@ -364,6 +359,70 @@ class AbstractWebElement(collections.abc.MutableMapping):
self._tab.caret.move_to_end_of_document() self._tab.caret.move_to_end_of_document()
QTimer.singleShot(0, after_click) QTimer.singleShot(0, after_click)
def _click_editable(self):
"""Fake a click on an editable input field."""
raise NotImplementedError
def _click_js(self, click_target):
"""Fake a click by using the JS .click() method."""
raise NotImplementedError
def _click_href(self, click_target):
"""Fake a click on an element with a href by opening the link."""
baseurl = self._tab.url()
url = self.resolve_url(baseurl)
if url is None:
self._click_fake_event(click_target)
return
if click_target in [usertypes.ClickTarget.tab,
usertypes.ClickTarget.tab_bg]:
background = click_target == usertypes.ClickTarget.tab_bg
tabbed_browser = objreg.get('tabbed-browser', scope='window',
window=self._tab.win_id)
tabbed_browser.tabopen(url, background=background)
elif click_target == usertypes.ClickTarget.window:
from qutebrowser.mainwindow import mainwindow
window = mainwindow.MainWindow()
window.show()
window.tabbed_browser.tabopen(url)
else:
raise ValueError("Unknown ClickTarget {}".format(click_target))
def click(self, click_target, *, force_event=False):
"""Simulate a click on the element.
Args:
click_target: An usertypes.ClickTarget member, what kind of click
to simulate.
force_event: Force generating a fake mouse event.
"""
if force_event:
self._click_fake_event(click_target)
return
href_tags = ['a', 'area', 'link']
if click_target == usertypes.ClickTarget.normal:
if self.tag_name() in href_tags:
log.webelem.debug("Clicking via JS click()")
self._click_js(click_target)
elif self.is_editable(strict=True):
log.webelem.debug("Clicking via JS focus()")
self._click_editable()
modeman.enter(self._tab.win_id, usertypes.KeyMode.insert,
'clicking input')
else:
self._click_fake_event(click_target)
elif click_target in [usertypes.ClickTarget.tab,
usertypes.ClickTarget.tab_bg,
usertypes.ClickTarget.window]:
if self.tag_name() in href_tags:
self._click_href(click_target)
else:
self._click_fake_event(click_target)
else:
raise ValueError("Unknown ClickTarget {}".format(click_target))
def hover(self): def hover(self):
"""Simulate a mouse hover over the element.""" """Simulate a mouse hover over the element."""
pos = self._mouse_pos() pos = self._mouse_pos()

View File

@ -22,9 +22,10 @@
"""QtWebEngine specific part of the web element API.""" """QtWebEngine specific part of the web element API."""
from PyQt5.QtCore import QRect from PyQt5.QtCore import QRect, Qt, QPoint
from PyQt5.QtGui import QMouseEvent
from qutebrowser.utils import log, javascript from qutebrowser.utils import log, javascript, usertypes
from qutebrowser.browser import webelem from qutebrowser.browser import webelem
@ -149,3 +150,17 @@ class WebEngineElement(webelem.AbstractWebElement):
js_code = javascript.assemble('webelem', 'remove_blank_target', js_code = javascript.assemble('webelem', 'remove_blank_target',
self._id) self._id)
self._tab.run_js_async(js_code) self._tab.run_js_async(js_code)
def _click_editable(self):
# WORKAROUND for https://bugreports.qt.io/browse/QTBUG-58515
ev = QMouseEvent(QMouseEvent.MouseButtonPress, QPoint(0, 0),
QPoint(0, 0), QPoint(0, 0), Qt.NoButton, Qt.NoButton,
Qt.NoModifier, Qt.MouseEventSynthesizedBySystem)
self._tab.send_event(ev)
# This actually "clicks" the element by calling focus() on it in JS.
js_code = javascript.assemble('webelem', 'focus', self._id)
self._tab.run_js_async(js_code)
def _click_js(self, _click_target):
js_code = javascript.assemble('webelem', 'click', self._id)
self._tab.run_js_async(js_code)

View File

@ -73,10 +73,8 @@ class WebEngineView(QWebEngineView):
The new QWebEngineView object. The new QWebEngineView object.
""" """
debug_type = debug.qenum_key(QWebEnginePage, wintype) debug_type = debug.qenum_key(QWebEnginePage, wintype)
background_tabs = config.get('tabs', 'background-tabs')
log.webview.debug("createWindow with type {}, background_tabs " log.webview.debug("createWindow with type {}".format(debug_type))
"{}".format(debug_type, background_tabs))
try: try:
background_tab_wintype = QWebEnginePage.WebBrowserBackgroundTab background_tab_wintype = QWebEnginePage.WebBrowserBackgroundTab
@ -85,30 +83,22 @@ class WebEngineView(QWebEngineView):
# this with a newer Qt... # this with a newer Qt...
background_tab_wintype = 0x0003 background_tab_wintype = 0x0003
if wintype == QWebEnginePage.WebBrowserWindow: click_target = {
# Shift-Alt-Click # Shift-Alt-Click
target = usertypes.ClickTarget.window QWebEnginePage.WebBrowserWindow: usertypes.ClickTarget.window,
elif wintype == QWebEnginePage.WebDialog: # ?
QWebEnginePage.WebDialog: usertypes.ClickTarget.tab,
# Middle-click / Ctrl-Click with Shift
QWebEnginePage.WebBrowserTab: usertypes.ClickTarget.tab_bg,
# Middle-click / Ctrl-Click
background_tab_wintype: usertypes.ClickTarget.tab,
}
if wintype == QWebEnginePage.WebDialog:
log.webview.warning("{} requested, but we don't support " log.webview.warning("{} requested, but we don't support "
"that!".format(debug_type)) "that!".format(debug_type))
target = usertypes.ClickTarget.tab
elif wintype == QWebEnginePage.WebBrowserTab:
# Middle-click / Ctrl-Click with Shift
# FIXME:qtwebengine this also affects target=_blank links...
if background_tabs:
target = usertypes.ClickTarget.tab
else:
target = usertypes.ClickTarget.tab_bg
elif wintype == background_tab_wintype:
# Middle-click / Ctrl-Click
if background_tabs:
target = usertypes.ClickTarget.tab_bg
else:
target = usertypes.ClickTarget.tab
else:
raise ValueError("Invalid wintype {}".format(debug_type))
tab = shared.get_tab(self._win_id, target) tab = shared.get_tab(self._win_id, click_target[wintype])
# WORKAROUND for https://bugreports.qt.io/browse/QTBUG-54419 # WORKAROUND for https://bugreports.qt.io/browse/QTBUG-54419
vercheck = qtutils.version_check vercheck = qtutils.version_check

View File

@ -297,6 +297,20 @@ class WebKitElement(webelem.AbstractWebElement):
break break
elem = elem._parent() # pylint: disable=protected-access elem = elem._parent() # pylint: disable=protected-access
def _click_editable(self):
self._elem.evaluateJavaScript('this.focus();')
def _click_js(self, click_target):
if self.get('target') == '_blank':
# QtWebKit does nothing in this case for some reason...
self._click_fake_event(click_target)
else:
self._elem.evaluateJavaScript('this.click();')
def _click_fake_event(self, click_target):
self._tab.data.override_target = click_target
super()._click_fake_event(click_target)
def get_child_frames(startframe): def get_child_frames(startframe):
"""Get all children recursively of a given QWebFrame. """Get all children recursively of a given QWebFrame.

View File

@ -191,5 +191,15 @@ window._qutebrowser.webelem = (function() {
} }
}; };
funcs.click = function(id) {
var elem = elements[id];
elem.click();
};
funcs.focus = function(id) {
var elem = elements[id];
elem.focus();
};
return funcs; return funcs;
})(); })();

View File

@ -6,6 +6,7 @@
</head> </head>
<body> <body>
<a href="/data/hello.txt" id="link">Just a link</a> <a href="/data/hello.txt" id="link">Just a link</a>
<button>blub</button>
<pre> <pre>
0 0
1 1

View File

@ -62,7 +62,7 @@ Feature: Opening external editors
When I set up a fake editor returning "foobar" When I set up a fake editor returning "foobar"
And I open data/editor.html And I open data/editor.html
And I run :click-element id qute-textarea And I run :click-element id qute-textarea
And I wait for "Clicked editable element!" in the log And I wait for "Entering mode KeyMode.insert (reason: clicking input)" in the log
And I run :open-editor And I run :open-editor
And I wait for "Read back: foobar" in the log And I wait for "Read back: foobar" in the log
And I run :click-element id qute-button And I run :click-element id qute-button
@ -72,7 +72,7 @@ Feature: Opening external editors
When I set up a fake editor returning "foobar" When I set up a fake editor returning "foobar"
And I open data/editor.html And I open data/editor.html
And I run :click-element id qute-textarea And I run :click-element id qute-textarea
And I wait for "Clicked editable element!" in the log And I wait for "Entering mode KeyMode.insert (reason: clicking input)" in the log
And I run :leave-mode And I run :leave-mode
And I wait for "Leaving mode KeyMode.insert (reason: leave current)" in the log And I wait for "Leaving mode KeyMode.insert (reason: leave current)" in the log
And I run :open-editor And I run :open-editor
@ -84,7 +84,7 @@ Feature: Opening external editors
When I set up a fake editor returning "foobar" When I set up a fake editor returning "foobar"
And I open data/editor.html And I open data/editor.html
And I run :click-element id qute-textarea And I run :click-element id qute-textarea
And I wait for "Clicked editable element!" in the log And I wait for "Entering mode KeyMode.insert (reason: clicking input)" in the log
And I run :leave-mode And I run :leave-mode
And I wait for "Leaving mode KeyMode.insert (reason: leave current)" in the log And I wait for "Leaving mode KeyMode.insert (reason: leave current)" in the log
And I run :enter-mode caret And I run :enter-mode caret
@ -99,7 +99,7 @@ Feature: Opening external editors
When I set up a fake editor replacing "foo" by "bar" When I set up a fake editor replacing "foo" by "bar"
And I open data/editor.html And I open data/editor.html
And I run :click-element id qute-textarea And I run :click-element id qute-textarea
And I wait for "Clicked editable element!" in the log And I wait for "Entering mode KeyMode.insert (reason: clicking input)" in the log
And I run :insert-text foo And I run :insert-text foo
And I wait for "Inserting text into element *" in the log And I wait for "Inserting text into element *" in the log
And I run :open-editor And I run :open-editor

View File

@ -9,17 +9,6 @@ Feature: Using hints
And I hint with args "links normal" and follow xyz And I hint with args "links normal" and follow xyz
Then the error "No hint xyz!" should be shown Then the error "No hint xyz!" should be shown
# https://travis-ci.org/The-Compiler/qutebrowser/jobs/159412291
@qtwebengine_flaky
Scenario: Following a link after scrolling down
When I open data/scroll/simple.html
And I run :hint links normal
And I wait for "hints: *" in the log
And I run :scroll-page 0 1
And I wait until the scroll position changed
And I run :follow-hint a
Then the error "Element position is out of view!" should be shown
### Opening in current or new tab ### Opening in current or new tab
Scenario: Following a hint and force to open in current tab. Scenario: Following a hint and force to open in current tab.
@ -159,7 +148,7 @@ Feature: Using hints
Scenario: Hinting inputs without type Scenario: Hinting inputs without type
When I open data/hints/input.html When I open data/hints/input.html
And I hint with args "inputs" and follow a And I hint with args "inputs" and follow a
And I wait for "Entering mode KeyMode.insert (reason: click)" in the log And I wait for "Entering mode KeyMode.insert (reason: clicking input)" in the log
And I run :leave-mode And I run :leave-mode
# The actual check is already done above # The actual check is already done above
Then no crash should happen Then no crash should happen
@ -167,7 +156,7 @@ Feature: Using hints
Scenario: Hinting with ACE editor Scenario: Hinting with ACE editor
When I open data/hints/ace/ace.html When I open data/hints/ace/ace.html
And I hint with args "inputs" and follow a And I hint with args "inputs" and follow a
And I wait for "Entering mode KeyMode.insert (reason: click)" in the log And I wait for "Entering mode KeyMode.insert (reason: clicking input)" in the log
And I run :leave-mode And I run :leave-mode
# The actual check is already done above # The actual check is already done above
Then no crash should happen Then no crash should happen

View File

@ -574,7 +574,7 @@ Feature: Various utility commands.
Scenario: Clicking an element by ID Scenario: Clicking an element by ID
When I open data/click_element.html When I open data/click_element.html
And I run :click-element id qute-input And I run :click-element id qute-input
Then "Clicked editable element!" should be logged Then "Entering mode KeyMode.insert (reason: clicking input)" should be logged
Scenario: Clicking an element with tab target Scenario: Clicking an element with tab target
When I open data/click_element.html When I open data/click_element.html
@ -585,14 +585,6 @@ Feature: Various utility commands.
- data/click_element.html - data/click_element.html
- data/hello.txt (active) - data/hello.txt (active)
@qtwebengine_flaky
Scenario: Clicking an element which is out of view
When I open data/scroll/simple.html
And I run :scroll-page 0 1
And I wait until the scroll position changed
And I run :click-element id link
Then the error "Element position is out of view!" should be shown
## :command-history-{prev,next} ## :command-history-{prev,next}
Scenario: Calling previous command Scenario: Calling previous command

View File

@ -252,7 +252,7 @@ Feature: Yanking and pasting.
When I set general -> log-javascript-console to info When I set general -> log-javascript-console to info
And I open data/paste_primary.html And I open data/paste_primary.html
And I run :click-element id qute-textarea And I run :click-element id qute-textarea
And I wait for "Clicked editable element!" in the log And I wait for "Entering mode KeyMode.insert (reason: clicking input)" in the log
And I run :insert-text Hello world And I run :insert-text Hello world
# Compare # Compare
Then the javascript message "textarea contents: Hello world" should be logged Then the javascript message "textarea contents: Hello world" should be logged
@ -263,7 +263,7 @@ Feature: Yanking and pasting.
And I set content -> allow-javascript to false And I set content -> allow-javascript to false
And I open data/paste_primary.html And I open data/paste_primary.html
And I run :click-element id qute-textarea And I run :click-element id qute-textarea
And I wait for "Clicked editable element!" in the log And I wait for "Entering mode KeyMode.insert (reason: clicking input)" in the log
And I run :insert-text Hello world And I run :insert-text Hello world
And I wait for "Inserting text into element *" in the log And I wait for "Inserting text into element *" in the log
And I run :jseval console.log("textarea contents: " + document.getElementById('qute-textarea').value); And I run :jseval console.log("textarea contents: " + document.getElementById('qute-textarea').value);
@ -278,7 +278,7 @@ Feature: Yanking and pasting.
And I open data/paste_primary.html And I open data/paste_primary.html
And I set the text field to "one two three four" And I set the text field to "one two three four"
And I run :click-element id qute-textarea And I run :click-element id qute-textarea
And I wait for "Clicked editable element!" in the log And I wait for "Entering mode KeyMode.insert (reason: clicking input)" in the log
# Move to the beginning and two characters to the right # Move to the beginning and two characters to the right
And I press the keys "<Home>" And I press the keys "<Home>"
And I press the key "<Right>" And I press the key "<Right>"
@ -292,7 +292,7 @@ Feature: Yanking and pasting.
When I set general -> log-javascript-console to info When I set general -> log-javascript-console to info
And I open data/paste_primary.html And I open data/paste_primary.html
And I run :click-element id qute-textarea And I run :click-element id qute-textarea
And I wait for "Clicked editable element!" in the log And I wait for "Entering mode KeyMode.insert (reason: clicking input)" in the log
# Paste and undo # Paste and undo
And I run :insert-text This text should be undone And I run :insert-text This text should be undone
And I wait for the javascript message "textarea contents: This text should be undone" And I wait for the javascript message "textarea contents: This text should be undone"

View File

@ -41,8 +41,8 @@ def test_insert_mode(file_name, elem_id, source, input_text, auto_insert,
quteproc.open_path(url_path) quteproc.open_path(url_path)
quteproc.set_setting('input', 'auto-insert-mode', auto_insert) quteproc.set_setting('input', 'auto-insert-mode', auto_insert)
quteproc.send_cmd(':click-element id {}'.format(elem_id)) quteproc.send_cmd(':click-element --force-event id {}'.format(elem_id))
quteproc.wait_for(message='Clicked editable element!') quteproc.wait_for(message='Entering mode KeyMode.insert (reason: *)')
quteproc.send_cmd(':debug-set-fake-clipboard') quteproc.send_cmd(':debug-set-fake-clipboard')
if source == 'keypress': if source == 'keypress':
@ -62,10 +62,8 @@ def test_insert_mode(file_name, elem_id, source, input_text, auto_insert,
quteproc.wait_for_js('contents: {}'.format(input_text)) quteproc.wait_for_js('contents: {}'.format(input_text))
quteproc.send_cmd(':leave-mode') quteproc.send_cmd(':leave-mode')
quteproc.send_cmd(':hint all') quteproc.send_cmd(':click-element --force-event id {}'.format(elem_id))
quteproc.wait_for(message='hints: *') quteproc.wait_for(message='Entering mode KeyMode.insert (reason: *)')
quteproc.send_cmd(':follow-hint a')
quteproc.wait_for(message='Clicked editable element!')
quteproc.send_cmd(':enter-mode caret') quteproc.send_cmd(':enter-mode caret')
quteproc.send_cmd(':toggle-selection') quteproc.send_cmd(':toggle-selection')
quteproc.send_cmd(':move-to-prev-word') quteproc.send_cmd(':move-to-prev-word')