Skip to content

Commit

Permalink
Move insert-mode-on-click to tab API / mouse.py
Browse files Browse the repository at this point in the history
This also implements the feature for QtWebEngine.
  • Loading branch information
The-Compiler committed Aug 16, 2016
1 parent eef76dd commit 1138d06
Show file tree
Hide file tree
Showing 6 changed files with 147 additions and 107 deletions.
12 changes: 12 additions & 0 deletions qutebrowser/browser/browsertab.py
Expand Up @@ -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),
Expand Down
61 changes: 60 additions & 1 deletion qutebrowser/browser/mouse.py
Expand Up @@ -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):
Expand Down Expand Up @@ -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."""
Expand All @@ -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.
Expand All @@ -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.
Expand Down
30 changes: 19 additions & 11 deletions qutebrowser/browser/webengine/webenginetab.py
Expand Up @@ -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 = []
Expand All @@ -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):
Expand Down
40 changes: 39 additions & 1 deletion qutebrowser/browser/webkit/webkittab.py
Expand Up @@ -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):
Expand Down Expand Up @@ -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.
Expand Down
95 changes: 1 addition & 94 deletions qutebrowser/browser/webkit/webview.py
Expand Up @@ -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
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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()
Expand Down
16 changes: 16 additions & 0 deletions qutebrowser/javascript/webelem.js
Expand Up @@ -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.