diff --git a/doc/help/commands.asciidoc b/doc/help/commands.asciidoc index 283acdf9f..eb7a25dee 100644 --- a/doc/help/commands.asciidoc +++ b/doc/help/commands.asciidoc @@ -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 diff --git a/qutebrowser/browser/commands.py b/qutebrowser/browser/commands.py index e8f15720d..63080af4f 100644 --- a/qutebrowser/browser/commands.py +++ b/qutebrowser/browser/commands.py @@ -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 diff --git a/qutebrowser/browser/mouse.py b/qutebrowser/browser/mouse.py index 1346a7693..23ddce6ee 100644 --- a/qutebrowser/browser/mouse.py +++ b/qutebrowser/browser/mouse.py @@ -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 diff --git a/qutebrowser/browser/webelem.py b/qutebrowser/browser/webelem.py index 0ddd4d265..560674e98 100644 --- a/qutebrowser/browser/webelem.py +++ b/qutebrowser/browser/webelem.py @@ -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() diff --git a/qutebrowser/browser/webengine/webengineelem.py b/qutebrowser/browser/webengine/webengineelem.py index bf8d06474..fe42d355d 100644 --- a/qutebrowser/browser/webengine/webengineelem.py +++ b/qutebrowser/browser/webengine/webengineelem.py @@ -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) diff --git a/qutebrowser/browser/webengine/webview.py b/qutebrowser/browser/webengine/webview.py index f3e9d6a94..585c184d9 100644 --- a/qutebrowser/browser/webengine/webview.py +++ b/qutebrowser/browser/webengine/webview.py @@ -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 diff --git a/qutebrowser/browser/webkit/webkitelem.py b/qutebrowser/browser/webkit/webkitelem.py index 6de1842bc..aab3d237c 100644 --- a/qutebrowser/browser/webkit/webkitelem.py +++ b/qutebrowser/browser/webkit/webkitelem.py @@ -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. diff --git a/qutebrowser/javascript/webelem.js b/qutebrowser/javascript/webelem.js index 99c8edf80..6e7209914 100644 --- a/qutebrowser/javascript/webelem.js +++ b/qutebrowser/javascript/webelem.js @@ -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; })(); diff --git a/tests/end2end/data/scroll/simple.html b/tests/end2end/data/scroll/simple.html index 2b1412ea4..7da9df101 100644 --- a/tests/end2end/data/scroll/simple.html +++ b/tests/end2end/data/scroll/simple.html @@ -6,6 +6,7 @@
Just a link +0 1 diff --git a/tests/end2end/features/editor.feature b/tests/end2end/features/editor.feature index 84ce47412..c7a5b9726 100644 --- a/tests/end2end/features/editor.feature +++ b/tests/end2end/features/editor.feature @@ -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 diff --git a/tests/end2end/features/hints.feature b/tests/end2end/features/hints.feature index 8d23d0199..24effc777 100644 --- a/tests/end2end/features/hints.feature +++ b/tests/end2end/features/hints.feature @@ -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 diff --git a/tests/end2end/features/misc.feature b/tests/end2end/features/misc.feature index 143988c64..4cd0b5602 100644 --- a/tests/end2end/features/misc.feature +++ b/tests/end2end/features/misc.feature @@ -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 diff --git a/tests/end2end/features/yankpaste.feature b/tests/end2end/features/yankpaste.feature index 1c65c63b4..67e9e2e9c 100644 --- a/tests/end2end/features/yankpaste.feature +++ b/tests/end2end/features/yankpaste.feature @@ -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 "" And I press the key " " @@ -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" diff --git a/tests/end2end/test_insert_mode.py b/tests/end2end/test_insert_mode.py index 6939dae0e..bd6aeda5a 100644 --- a/tests/end2end/test_insert_mode.py +++ b/tests/end2end/test_insert_mode.py @@ -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')