Permalink
Browse files

Move insert-mode-on-click to tab API / mouse.py

This also implements the feature for QtWebEngine.
  • Loading branch information...
The-Compiler committed Aug 16, 2016
1 parent eef76dd commit 1138d068e6dbd5a15a6e74a7323c53b451ba3e40
@@ -706,6 +706,18 @@ def find_focus_element(self, callback):
"""
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),
@@ -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,17 +67,21 @@ 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):
super().__init__(parent)
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 @@ def _handle_mouse_press(self, e):
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 @@ def _handle_context_menu(self, _e):
"""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.
@@ -449,11 +449,11 @@ def set_html(self, html, base_url):
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 @@ def _find_all_elements_js_cb(self, callback, js_elems):
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):
@@ -32,7 +32,7 @@
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 @@ def find_focus_element(self, callback):
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.
@@ -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 @@ def _set_bg_color(self):
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 @@ def paintEvent(self, e):
# 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()
@@ -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;
})();

0 comments on commit 1138d06

Please sign in to comment.