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')