Skip to content
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.