Move insert-mode-on-click to tab API / mouse.py
This also implements the feature for QtWebEngine.
This commit is contained in:
parent
eef76dde86
commit
1138d068e6
@ -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),
|
||||
|
@ -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.
|
||||
|
||||
|
@ -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):
|
||||
|
@ -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.
|
||||
|
@ -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()
|
||||
|
@ -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;
|
||||
})();
|
||||
|
Loading…
Reference in New Issue
Block a user