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

This also implements the feature for QtWebEngine.
This commit is contained in:
Florian Bruhin 2016-08-16 16:10:10 +02:00
parent eef76dde86
commit 1138d068e6
6 changed files with 147 additions and 107 deletions

View File

@ -706,6 +706,18 @@ class AbstractTab(QWidget):
""" """
raise NotImplementedError 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): def __repr__(self):
try: try:
url = utils.elide(self.url().toDisplayString(QUrl.EncodeUnicode), url = utils.elide(self.url().toDisplayString(QUrl.EncodeUnicode),

View File

@ -22,9 +22,10 @@
from qutebrowser.config import config from qutebrowser.config import config
from qutebrowser.utils import message, log, usertypes 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): class ChildEventFilter(QObject):
@ -66,6 +67,8 @@ class MouseEventFilter(QObject):
_tab: The browsertab object this filter is installed on. _tab: The browsertab object this filter is installed on.
_handlers: A dict of handler functions for the handled events. _handlers: A dict of handler functions for the handled events.
_ignore_wheel_event: Whether to ignore the next wheelEvent. _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): def __init__(self, tab, parent=None):
@ -73,10 +76,12 @@ class MouseEventFilter(QObject):
self._tab = tab self._tab = tab
self._handlers = { self._handlers = {
QEvent.MouseButtonPress: self._handle_mouse_press, QEvent.MouseButtonPress: self._handle_mouse_press,
QEvent.MouseButtonRelease: self._handle_mouse_release,
QEvent.Wheel: self._handle_wheel, QEvent.Wheel: self._handle_wheel,
QEvent.ContextMenu: self._handle_context_menu, QEvent.ContextMenu: self._handle_context_menu,
} }
self._ignore_wheel_event = False self._ignore_wheel_event = False
self._check_insertmode_on_release = False
def _handle_mouse_press(self, e): def _handle_mouse_press(self, e):
"""Handle pressing of a mouse button.""" """Handle pressing of a mouse button."""
@ -89,9 +94,17 @@ class MouseEventFilter(QObject):
self._ignore_wheel_event = True self._ignore_wheel_event = True
self._mousepress_opentarget(e) self._mousepress_opentarget(e)
self._tab.find_element_at_pos(e.pos(), self._mousepress_insertmode_cb)
return False 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): def _handle_wheel(self, e):
"""Zoom on Ctrl-Mousewheel. """Zoom on Ctrl-Mousewheel.
@ -118,6 +131,52 @@ class MouseEventFilter(QObject):
"""Suppress context menus if rocker gestures are turned on.""" """Suppress context menus if rocker gestures are turned on."""
return config.get('input', 'rocker-gestures') 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): def _mousepress_backforward(self, e):
"""Handle back/forward mouse button presses. """Handle back/forward mouse button presses.

View File

@ -449,11 +449,11 @@ class WebEngineTab(browsertab.AbstractTab):
def clear_ssl_errors(self): def clear_ssl_errors(self):
log.stub() 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. """Handle found elements coming from JS and call the real callback.
Args: 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. js_elems: The elements serialized from javascript.
""" """
elems = [] elems = []
@ -462,29 +462,37 @@ class WebEngineTab(browsertab.AbstractTab):
elems.append(elem) elems.append(elem)
callback(elems) callback(elems)
def find_all_elements(self, selector, callback, *, only_visible=False): def _js_element_cb_single(self, callback, js_elem):
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):
"""Handle a found focus elem coming from JS and call the real callback. """Handle a found focus elem coming from JS and call the real callback.
Args: 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. Called with a WebEngineElement or None.
js_elem: The element serialized from javascript. 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: if js_elem is None:
callback(None) callback(None)
else: else:
elem = webengineelem.WebEngineElement(js_elem, self.run_js_async) elem = webengineelem.WebEngineElement(js_elem, self.run_js_async)
callback(elem) 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): def find_focus_element(self, callback):
js_code = javascript.assemble('webelem', 'focus_element') 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) self.run_js_async(js_code, js_cb)
def _connect_signals(self): def _connect_signals(self):

View File

@ -32,7 +32,7 @@ from PyQt5.QtPrintSupport import QPrinter
from qutebrowser.browser import browsertab from qutebrowser.browser import browsertab
from qutebrowser.browser.webkit import webview, tabhistory, webkitelem 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): class WebKitPrinting(browsertab.AbstractPrinting):
@ -593,6 +593,44 @@ class WebKitTab(browsertab.AbstractTab):
else: else:
callback(webkitelem.WebKitElement(elem)) 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() @pyqtSlot()
def _on_frame_load_finished(self): def _on_frame_load_finished(self):
"""Make sure we emit an appropriate status when loading finished. """Make sure we emit an appropriate status when loading finished.

View File

@ -21,7 +21,7 @@
import sys 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.QtGui import QPalette
from PyQt5.QtWidgets import QStyleFactory from PyQt5.QtWidgets import QStyleFactory
from PyQt5.QtWebKit import QWebSettings from PyQt5.QtWebKit import QWebSettings
@ -118,74 +118,6 @@ class WebView(QWebView):
palette.setColor(QPalette.Base, col) palette.setColor(QPalette.Base, col)
self.setPalette(palette) 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): def shutdown(self):
"""Shut down the webview.""" """Shut down the webview."""
self.shutting_down.emit() self.shutting_down.emit()
@ -324,31 +256,6 @@ class WebView(QWebView):
# Let superclass handle the event # Let superclass handle the event
super().paintEvent(e) 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): def contextMenuEvent(self, e):
"""Save a reference to the context menu so we can close it.""" """Save a reference to the context menu so we can close it."""
menu = self.page().createStandardContextMenu() menu = self.page().createStandardContextMenu()

View File

@ -76,5 +76,21 @@ window._qutebrowser.webelem = (function() {
elements[id].value = text; 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; return funcs;
})(); })();