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
|
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),
|
||||||
|
@ -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.
|
||||||
|
|
||||||
|
@ -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):
|
||||||
|
@ -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.
|
||||||
|
@ -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()
|
||||||
|
@ -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;
|
||||||
})();
|
})();
|
||||||
|
Loading…
Reference in New Issue
Block a user