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

View File

@ -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.

View File

@ -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):

View File

@ -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.

View File

@ -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()

View File

@ -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;
})();