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:
parent
ba2f4fb1b9
commit
545539f28d
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
||||
|
@ -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()
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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.
|
||||
|
@ -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;
|
||||
})();
|
||||
|
@ -6,6 +6,7 @@
|
||||
</head>
|
||||
<body>
|
||||
<a href="/data/hello.txt" id="link">Just a link</a>
|
||||
<button>blub</button>
|
||||
<pre>
|
||||
0
|
||||
1
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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"
|
||||
|
@ -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')
|
||||
|
Loading…
Reference in New Issue
Block a user