diff --git a/qutebrowser/browser/browsertab.py b/qutebrowser/browser/browsertab.py index 473969460..a7f4116e4 100644 --- a/qutebrowser/browser/browsertab.py +++ b/qutebrowser/browser/browsertab.py @@ -706,6 +706,18 @@ class AbstractTab(QWidget): """ raise NotImplementedError + def find_element_at_pos(self, pos, callback): + """Find the element at the given position async. + + This is also called "hit test" elsewhere. + + Args: + pos: The QPoint to get the element for. + callback: The callback to be called when the search finished. + Called with a WebEngineElement or None. + """ + raise NotImplementedError + def __repr__(self): try: url = utils.elide(self.url().toDisplayString(QUrl.EncodeUnicode), diff --git a/qutebrowser/browser/mouse.py b/qutebrowser/browser/mouse.py index 1a035c612..c3c8ac879 100644 --- a/qutebrowser/browser/mouse.py +++ b/qutebrowser/browser/mouse.py @@ -22,9 +22,10 @@ from qutebrowser.config import config from qutebrowser.utils import message, log, usertypes +from qutebrowser.keyinput import modeman -from PyQt5.QtCore import QObject, QEvent, Qt +from PyQt5.QtCore import QObject, QEvent, Qt, QTimer class ChildEventFilter(QObject): @@ -66,6 +67,8 @@ class MouseEventFilter(QObject): _tab: The browsertab object this filter is installed on. _handlers: A dict of handler functions for the handled events. _ignore_wheel_event: Whether to ignore the next wheelEvent. + _check_insertmode_on_release: Whether an insertmode check should be + done when the mouse is released. """ def __init__(self, tab, parent=None): @@ -73,10 +76,12 @@ class MouseEventFilter(QObject): self._tab = tab self._handlers = { QEvent.MouseButtonPress: self._handle_mouse_press, + QEvent.MouseButtonRelease: self._handle_mouse_release, QEvent.Wheel: self._handle_wheel, QEvent.ContextMenu: self._handle_context_menu, } self._ignore_wheel_event = False + self._check_insertmode_on_release = False def _handle_mouse_press(self, e): """Handle pressing of a mouse button.""" @@ -89,9 +94,17 @@ class MouseEventFilter(QObject): self._ignore_wheel_event = True self._mousepress_opentarget(e) + self._tab.find_element_at_pos(e.pos(), self._mousepress_insertmode_cb) return False + def _handle_mouse_release(self, _e): + """Handle releasing of a mouse button.""" + # We want to make sure we check the focus element after the WebView is + # updated completely. + QTimer.singleShot(0, self._mouserelease_insertmode) + return False + def _handle_wheel(self, e): """Zoom on Ctrl-Mousewheel. @@ -118,6 +131,52 @@ class MouseEventFilter(QObject): """Suppress context menus if rocker gestures are turned on.""" return config.get('input', 'rocker-gestures') + def _mousepress_insertmode_cb(self, elem): + """Check if the clicked element is editable.""" + if elem is None: + # Something didn't work out, let's find the focus element after + # a mouse release. + log.mouse.debug("Got None element, scheduling check on " + "mouse release") + self._check_insertmode_on_release = True + return + + if elem.is_editable(): + log.mouse.debug("Clicked editable element!") + modeman.enter(self._tab.win_id, usertypes.KeyMode.insert, + 'click', only_if_normal=True) + else: + log.mouse.debug("Clicked non-editable element!") + if config.get('input', 'auto-leave-insert-mode'): + modeman.maybe_leave(self._tab.win_id, + usertypes.KeyMode.insert, + 'click') + + def _mouserelease_insertmode(self): + """If we have an insertmode check scheduled, handle it.""" + if not self._check_insertmode_on_release: + return + self._check_insertmode_on_release = False + + def mouserelease_insertmode_cb(elem): + """Callback which gets called from JS.""" + if elem is None: + log.mouse.debug("Element vanished!") + return + + if elem.is_editable(): + log.mouse.debug("Clicked editable element (delayed)!") + modeman.enter(self._tab.win_id, usertypes.KeyMode.insert, + 'click-delayed', only_if_normal=True) + else: + log.mouse.debug("Clicked non-editable element (delayed)!") + if config.get('input', 'auto-leave-insert-mode'): + modeman.maybe_leave(self._tab.win_id, + usertypes.KeyMode.insert, + 'click-delayed') + + self._tab.find_focus_element(mouserelease_insertmode_cb) + def _mousepress_backforward(self, e): """Handle back/forward mouse button presses. diff --git a/qutebrowser/browser/webengine/webenginetab.py b/qutebrowser/browser/webengine/webenginetab.py index d366d127b..0e57d10aa 100644 --- a/qutebrowser/browser/webengine/webenginetab.py +++ b/qutebrowser/browser/webengine/webenginetab.py @@ -449,11 +449,11 @@ class WebEngineTab(browsertab.AbstractTab): def clear_ssl_errors(self): log.stub() - def _find_all_elements_js_cb(self, callback, js_elems): + def _js_element_cb_multiple(self, callback, js_elems): """Handle found elements coming from JS and call the real callback. Args: - callback: The callback originally passed to find_all_elements. + callback: The callback to call with the found elements. js_elems: The elements serialized from javascript. """ elems = [] @@ -462,29 +462,37 @@ class WebEngineTab(browsertab.AbstractTab): elems.append(elem) callback(elems) - def find_all_elements(self, selector, callback, *, only_visible=False): - js_code = javascript.assemble('webelem', 'find_all', selector) - js_cb = functools.partial(self._find_all_elements_js_cb, callback) - self.run_js_async(js_code, js_cb) - - def _find_focus_element_js_cb(self, callback, js_elem): + def _js_element_cb_single(self, callback, js_elem): """Handle a found focus elem coming from JS and call the real callback. Args: - callback: The callback originally passed to find_focus_element. + callback: The callback to call with the found element. Called with a WebEngineElement or None. js_elem: The element serialized from javascript. """ - log.webview.debug("Got focus element from JS: {!r}".format(js_elem)) + log.webview.debug("Got element from JS: {!r}".format(js_elem)) if js_elem is None: callback(None) else: elem = webengineelem.WebEngineElement(js_elem, self.run_js_async) callback(elem) + def find_all_elements(self, selector, callback, *, only_visible=False): + js_code = javascript.assemble('webelem', 'find_all', selector) + js_cb = functools.partial(self._js_element_cb_multiple, callback) + self.run_js_async(js_code, js_cb) + def find_focus_element(self, callback): js_code = javascript.assemble('webelem', 'focus_element') - js_cb = functools.partial(self._find_focus_element_js_cb, callback) + js_cb = functools.partial(self._js_element_cb_single, callback) + self.run_js_async(js_code, js_cb) + + def find_element_at_pos(self, pos, callback): + assert pos.x() >= 0 + assert pos.y() >= 0 + js_code = javascript.assemble('webelem', 'element_at_pos', + pos.x(), pos.y()) + js_cb = functools.partial(self._js_element_cb_single, callback) self.run_js_async(js_code, js_cb) def _connect_signals(self): diff --git a/qutebrowser/browser/webkit/webkittab.py b/qutebrowser/browser/webkit/webkittab.py index 19a6dc0fa..030819571 100644 --- a/qutebrowser/browser/webkit/webkittab.py +++ b/qutebrowser/browser/webkit/webkittab.py @@ -32,7 +32,7 @@ from PyQt5.QtPrintSupport import QPrinter from qutebrowser.browser import browsertab from qutebrowser.browser.webkit import webview, tabhistory, webkitelem -from qutebrowser.utils import qtutils, objreg, usertypes, utils +from qutebrowser.utils import qtutils, objreg, usertypes, utils, log class WebKitPrinting(browsertab.AbstractPrinting): @@ -593,6 +593,44 @@ class WebKitTab(browsertab.AbstractTab): else: callback(webkitelem.WebKitElement(elem)) + def find_element_at_pos(self, pos, callback): + assert pos.x() >= 0 + assert pos.y() >= 0 + frame = self._widget.page().frameAt(pos) + if frame is None: + # This happens when we click inside the webview, but not actually + # on the QWebPage - for example when clicking the scrollbar + # sometimes. + log.webview.debug("Hit test at {} but frame is None!".format(pos)) + callback(None) + return + + # You'd think we have to subtract frame.geometry().topLeft() from the + # position, but it seems QWebFrame::hitTestContent wants a position + # relative to the QWebView, not to the frame. This makes no sense to + # me, but it works this way. + hitresult = frame.hitTestContent(pos) + if hitresult.isNull(): + # For some reason, the whole hit result can be null sometimes (e.g. + # on doodle menu links). If this is the case, we schedule a check + # later (in mouseReleaseEvent) which uses webkitelem.focus_elem. + log.webview.debug("Hit test result is null!") + callback(None) + return + + try: + elem = webkitelem.WebKitElement(hitresult.element()) + except webkitelem.IsNullError: + # For some reason, the hit result element can be a null element + # sometimes (e.g. when clicking the timetable fields on + # http://www.sbb.ch/ ). If this is the case, we schedule a check + # later (in mouseReleaseEvent) which uses webelem.focus_elem. + log.webview.debug("Hit test result element is null!") + callback(None) + return + + callback(elem) + @pyqtSlot() def _on_frame_load_finished(self): """Make sure we emit an appropriate status when loading finished. diff --git a/qutebrowser/browser/webkit/webview.py b/qutebrowser/browser/webkit/webview.py index f69247f18..b6c03710c 100644 --- a/qutebrowser/browser/webkit/webview.py +++ b/qutebrowser/browser/webkit/webview.py @@ -21,7 +21,7 @@ import sys -from PyQt5.QtCore import pyqtSignal, pyqtSlot, Qt, QTimer, QUrl +from PyQt5.QtCore import pyqtSignal, pyqtSlot, Qt, QUrl from PyQt5.QtGui import QPalette from PyQt5.QtWidgets import QStyleFactory from PyQt5.QtWebKit import QWebSettings @@ -118,74 +118,6 @@ class WebView(QWebView): palette.setColor(QPalette.Base, col) self.setPalette(palette) - def _mousepress_insertmode(self, e): - """Switch to insert mode when an editable element was clicked. - - Args: - e: The QMouseEvent. - """ - pos = e.pos() - frame = self.page().frameAt(pos) - if frame is None: - # This happens when we click inside the webview, but not actually - # on the QWebPage - for example when clicking the scrollbar - # sometimes. - log.mouse.debug("Clicked at {} but frame is None!".format(pos)) - return - # You'd think we have to subtract frame.geometry().topLeft() from the - # position, but it seems QWebFrame::hitTestContent wants a position - # relative to the QWebView, not to the frame. This makes no sense to - # me, but it works this way. - hitresult = frame.hitTestContent(pos) - if hitresult.isNull(): - # For some reason, the whole hit result can be null sometimes (e.g. - # on doodle menu links). If this is the case, we schedule a check - # later (in mouseReleaseEvent) which uses webkitelem.focus_elem. - log.mouse.debug("Hitresult is null!") - self._check_insertmode = True - return - try: - elem = webkitelem.WebKitElement(hitresult.element()) - except webkitelem.IsNullError: - # For some reason, the hit result element can be a null element - # sometimes (e.g. when clicking the timetable fields on - # http://www.sbb.ch/ ). If this is the case, we schedule a check - # later (in mouseReleaseEvent) which uses webelem.focus_elem. - log.mouse.debug("Hitresult element is null!") - self._check_insertmode = True - return - if ((hitresult.isContentEditable() and elem.is_writable()) or - elem.is_editable()): - log.mouse.debug("Clicked editable element!") - modeman.enter(self.win_id, usertypes.KeyMode.insert, 'click', - only_if_normal=True) - else: - log.mouse.debug("Clicked non-editable element!") - if config.get('input', 'auto-leave-insert-mode'): - modeman.maybe_leave(self.win_id, usertypes.KeyMode.insert, - 'click') - - def mouserelease_insertmode(self): - """If we have an insertmode check scheduled, handle it.""" - # FIXME:qtwebengine Use tab.find_focus_element here - if not self._check_insertmode: - return - self._check_insertmode = False - try: - elem = webkitelem.focus_elem(self.page().currentFrame()) - except (webkitelem.IsNullError, RuntimeError): - log.mouse.debug("Element/page vanished!") - return - if elem.is_editable(): - log.mouse.debug("Clicked editable element (delayed)!") - modeman.enter(self.win_id, usertypes.KeyMode.insert, - 'click-delayed', only_if_normal=True) - else: - log.mouse.debug("Clicked non-editable element (delayed)!") - if config.get('input', 'auto-leave-insert-mode'): - modeman.maybe_leave(self.win_id, usertypes.KeyMode.insert, - 'click-delayed') - def shutdown(self): """Shut down the webview.""" self.shutting_down.emit() @@ -324,31 +256,6 @@ class WebView(QWebView): # Let superclass handle the event super().paintEvent(e) - def mousePressEvent(self, e): - """Extend QWidget::mousePressEvent(). - - This does the following things: - - Check if a link was clicked with the middle button or Ctrl and - set the page's open_target attribute accordingly. - - Emit the editable_elem_selected signal if an editable element was - clicked. - - Args: - e: The arrived event. - - Return: - The superclass return value. - """ - self._mousepress_insertmode(e) - super().mousePressEvent(e) - - def mouseReleaseEvent(self, e): - """Extend mouseReleaseEvent to enter insert mode if needed.""" - super().mouseReleaseEvent(e) - # We want to make sure we check the focus element after the WebView is - # updated completely. - QTimer.singleShot(0, self.mouserelease_insertmode) - def contextMenuEvent(self, e): """Save a reference to the context menu so we can close it.""" menu = self.page().createStandardContextMenu() diff --git a/qutebrowser/javascript/webelem.js b/qutebrowser/javascript/webelem.js index d35974e0d..193dbfc87 100644 --- a/qutebrowser/javascript/webelem.js +++ b/qutebrowser/javascript/webelem.js @@ -76,5 +76,21 @@ window._qutebrowser.webelem = (function() { elements[id].value = text; }; + funcs.element_at_pos = function(x, y) { + // FIXME:qtwebengine + // If the element at the specified point belongs to another document + // (for example, an iframe's subdocument), the subdocument's parent + // element is returned (the iframe itself). + + var elem = document.elementFromPoint(x, y); + if (!elem) { + return null; + } + + var id = elements.length; + elements[id] = elem; + return serialize_elem(elem, id); + }; + return funcs; })();