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
Syntax: +:click-element [*--target* 'target'] 'filter' 'value'+
Syntax: +:click-element [*--target* 'target'] [*--force-event*] 'filter' 'value'+
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
* +*-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

View File

@ -1545,7 +1545,8 @@ class CommandDispatcher:
@cmdutils.argument('filter_', choices=['id'])
def click_element(self, filter_: str, value, *,
target: usertypes.ClickTarget=
usertypes.ClickTarget.normal):
usertypes.ClickTarget.normal,
force_event=False):
"""Click the element matching the given filter.
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.
value: The value to filter for.
target: How to open the clicked element (normal/tab/tab-bg/window).
force_event: Force generating a fake click event.
"""
tab = self._current_widget()
@ -1565,7 +1567,7 @@ class CommandDispatcher:
message.error("No element found with id {}!".format(value))
return
try:
elem.click(target)
elem.click(target, force_event=force_event)
except webelem.Error as e:
message.error(str(e))
return

View File

@ -93,7 +93,10 @@ class MouseEventFilter(QObject):
return 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

View File

@ -33,7 +33,8 @@ from PyQt5.QtCore import QUrl, Qt, QEvent, QTimer
from PyQt5.QtGui import QMouseEvent
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',
@ -322,14 +323,8 @@ class AbstractWebElement(collections.abc.MutableMapping):
raise Error("Element position is out of view!")
return pos
def click(self, click_target):
"""Simulate a click on the element."""
# FIXME:qtwebengine do we need this?
# self._widget.setFocus()
# For QtWebKit
self._tab.data.override_target = click_target
def _click_fake_event(self, click_target):
"""Send a fake click event to the element."""
pos = self._mouse_pos()
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()
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):
"""Simulate a mouse hover over the element."""
pos = self._mouse_pos()

View File

@ -22,9 +22,10 @@
"""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
@ -149,3 +150,17 @@ class WebEngineElement(webelem.AbstractWebElement):
js_code = javascript.assemble('webelem', 'remove_blank_target',
self._id)
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.
"""
debug_type = debug.qenum_key(QWebEnginePage, wintype)
background_tabs = config.get('tabs', 'background-tabs')
log.webview.debug("createWindow with type {}, background_tabs "
"{}".format(debug_type, background_tabs))
log.webview.debug("createWindow with type {}".format(debug_type))
try:
background_tab_wintype = QWebEnginePage.WebBrowserBackgroundTab
@ -85,30 +83,22 @@ class WebEngineView(QWebEngineView):
# this with a newer Qt...
background_tab_wintype = 0x0003
if wintype == QWebEnginePage.WebBrowserWindow:
click_target = {
# Shift-Alt-Click
target = usertypes.ClickTarget.window
elif wintype == QWebEnginePage.WebDialog:
QWebEnginePage.WebBrowserWindow: usertypes.ClickTarget.window,
# ?
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 "
"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
vercheck = qtutils.version_check

View File

@ -297,6 +297,20 @@ class WebKitElement(webelem.AbstractWebElement):
break
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):
"""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;
})();

View File

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

View File

@ -62,7 +62,7 @@ Feature: Opening external editors
When I set up a fake editor returning "foobar"
And I open data/editor.html
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 wait for "Read back: foobar" in the log
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"
And I open data/editor.html
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 wait for "Leaving mode KeyMode.insert (reason: leave current)" in the log
And I run :open-editor
@ -84,7 +84,7 @@ Feature: Opening external editors
When I set up a fake editor returning "foobar"
And I open data/editor.html
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 wait for "Leaving mode KeyMode.insert (reason: leave current)" in the log
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"
And I open data/editor.html
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 wait for "Inserting text into element *" in the log
And I run :open-editor

View File

@ -9,17 +9,6 @@ Feature: Using hints
And I hint with args "links normal" and follow xyz
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
Scenario: Following a hint and force to open in current tab.
@ -159,7 +148,7 @@ Feature: Using hints
Scenario: Hinting inputs without type
When I open data/hints/input.html
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
# The actual check is already done above
Then no crash should happen
@ -167,7 +156,7 @@ Feature: Using hints
Scenario: Hinting with ACE editor
When I open data/hints/ace/ace.html
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
# The actual check is already done above
Then no crash should happen

View File

@ -574,7 +574,7 @@ Feature: Various utility commands.
Scenario: Clicking an element by ID
When I open data/click_element.html
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
When I open data/click_element.html
@ -585,14 +585,6 @@ Feature: Various utility commands.
- data/click_element.html
- 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}
Scenario: Calling previous command

View File

@ -252,7 +252,7 @@ Feature: Yanking and pasting.
When I set general -> log-javascript-console to info
And I open data/paste_primary.html
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
# Compare
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 open data/paste_primary.html
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 wait for "Inserting text into element *" in the log
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 set the text field to "one two three four"
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
And I press the keys "<Home>"
And I press the key "<Right>"
@ -292,7 +292,7 @@ Feature: Yanking and pasting.
When I set general -> log-javascript-console to info
And I open data/paste_primary.html
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
And I run :insert-text 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.set_setting('input', 'auto-insert-mode', auto_insert)
quteproc.send_cmd(':click-element id {}'.format(elem_id))
quteproc.wait_for(message='Clicked editable element!')
quteproc.send_cmd(':click-element --force-event id {}'.format(elem_id))
quteproc.wait_for(message='Entering mode KeyMode.insert (reason: *)')
quteproc.send_cmd(':debug-set-fake-clipboard')
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.send_cmd(':leave-mode')
quteproc.send_cmd(':hint all')
quteproc.wait_for(message='hints: *')
quteproc.send_cmd(':follow-hint a')
quteproc.wait_for(message='Clicked editable element!')
quteproc.send_cmd(':click-element --force-event id {}'.format(elem_id))
quteproc.wait_for(message='Entering mode KeyMode.insert (reason: *)')
quteproc.send_cmd(':enter-mode caret')
quteproc.send_cmd(':toggle-selection')
quteproc.send_cmd(':move-to-prev-word')