Add webelem.click() and webelem.hover()

This commit is contained in:
Florian Bruhin 2016-08-18 14:32:19 +02:00
parent 5ac9fe9c32
commit 63c66945a4
9 changed files with 98 additions and 89 deletions

View File

@ -70,7 +70,7 @@ class TabData:
inspector: The QWebInspector used for this webview. inspector: The QWebInspector used for this webview.
viewing_source: Set if we're currently showing a source view. viewing_source: Set if we're currently showing a source view.
open_target: How the next clicked link should be opened. open_target: How the next clicked link should be opened.
hint_target: Override for open_target for hints. override_target: Override for open_target for fake clicks (like hints).
""" """
def __init__(self): def __init__(self):
@ -78,11 +78,11 @@ class TabData:
self.viewing_source = False self.viewing_source = False
self.inspector = None self.inspector = None
self.open_target = usertypes.ClickTarget.normal self.open_target = usertypes.ClickTarget.normal
self.hint_target = None self.override_target = None
def combined_target(self): def combined_target(self):
if self.hint_target is not None: if self.override_target is not None:
return self.hint_target return self.override_target
else: else:
return self.open_target return self.open_target
@ -535,7 +535,6 @@ class AbstractTab(QWidget):
# FIXME:qtwebengine Should this be public api via self.hints? # FIXME:qtwebengine Should this be public api via self.hints?
# Also, should we get it out of objreg? # Also, should we get it out of objreg?
hintmanager = hints.HintManager(win_id, self.tab_id, parent=self) hintmanager = hints.HintManager(win_id, self.tab_id, parent=self)
hintmanager.hint_events.connect(self._on_hint_events)
objreg.register('hintmanager', hintmanager, scope='tab', objreg.register('hintmanager', hintmanager, scope='tab',
window=self.win_id, tab=self.tab_id) window=self.win_id, tab=self.tab_id)
@ -567,27 +566,12 @@ class AbstractTab(QWidget):
"""Send the given event to the underlying widget.""" """Send the given event to the underlying widget."""
raise NotImplementedError raise NotImplementedError
@pyqtSlot(usertypes.ClickTarget, list)
def _on_hint_events(self, target, events):
"""Post a new mouse event from a hintmanager."""
log.modes.debug("Sending hint events to {!r} with target {}".format(
self, target))
self._widget.setFocus()
self.data.hint_target = target
for evt in events:
self.post_event(evt)
def reset_target():
self.data.hint_target = None
QTimer.singleShot(0, reset_target)
@pyqtSlot(QUrl) @pyqtSlot(QUrl)
def _on_link_clicked(self, url): def _on_link_clicked(self, url):
log.webview.debug("link clicked: url {}, hint target {}, " log.webview.debug("link clicked: url {}, override target {}, "
"open_target {}".format( "open_target {}".format(
url.toDisplayString(), url.toDisplayString(),
self.data.hint_target, self.data.open_target)) self.data.override_target, self.data.open_target))
if not url.isValid(): if not url.isValid():
msg = urlutils.get_errstring(url, "Invalid link clicked") msg = urlutils.get_errstring(url, "Invalid link clicked")

View File

@ -26,9 +26,7 @@ import re
import html import html
from string import ascii_lowercase from string import ascii_lowercase
from PyQt5.QtCore import (pyqtSignal, pyqtSlot, QObject, QEvent, Qt, QUrl, from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject, Qt, QUrl
QTimer)
from PyQt5.QtGui import QMouseEvent
from PyQt5.QtWidgets import QLabel from PyQt5.QtWidgets import QLabel
from qutebrowser.config import config, style from qutebrowser.config import config, style
@ -182,13 +180,7 @@ class HintContext:
class HintActions(QObject): class HintActions(QObject):
"""Actions which can be done after selecting a hint. """Actions which can be done after selecting a hint."""
Signals:
hint_events: Emitted with a ClickTarget and a list of hint event.s
"""
hint_events = pyqtSignal(usertypes.ClickTarget, list) # QMouseEvent list
def __init__(self, win_id, parent=None): def __init__(self, win_id, parent=None):
super().__init__(parent) super().__init__(parent)
@ -214,50 +206,19 @@ class HintActions(QObject):
else: else:
target_mapping[Target.tab] = usertypes.ClickTarget.tab target_mapping[Target.tab] = usertypes.ClickTarget.tab
# Click the center of the largest square fitting into the top/left
# corner of the rectangle, this will help if part of the <a> element
# is hidden behind other elements
# https://github.com/The-Compiler/qutebrowser/issues/1005
rect = elem.rect_on_view()
if rect.width() > rect.height():
rect.setWidth(rect.height())
else:
rect.setHeight(rect.width())
pos = rect.center()
action = "Hovering" if context.target == Target.hover else "Clicking"
log.hints.debug("{} on '{}' at position {}".format(
action, elem.debug_text(), pos))
if context.target in [Target.tab, Target.tab_fg, Target.tab_bg,
Target.window]:
modifiers = Qt.ControlModifier
else:
modifiers = Qt.NoModifier
events = [
QMouseEvent(QEvent.MouseMove, pos, Qt.NoButton, Qt.NoButton,
Qt.NoModifier),
]
if context.target != Target.hover:
events += [
QMouseEvent(QEvent.MouseButtonPress, pos, Qt.LeftButton,
Qt.LeftButton, modifiers),
QMouseEvent(QEvent.MouseButtonRelease, pos, Qt.LeftButton,
Qt.NoButton, modifiers),
]
if context.target in [Target.normal, Target.current]: if context.target in [Target.normal, Target.current]:
# Set the pre-jump mark ', so we can jump back here after following # Set the pre-jump mark ', so we can jump back here after following
tabbed_browser = objreg.get('tabbed-browser', scope='window', tabbed_browser = objreg.get('tabbed-browser', scope='window',
window=self._win_id) window=self._win_id)
tabbed_browser.set_mark("'") tabbed_browser.set_mark("'")
if context.target == Target.current: if context.target == Target.hover:
elem.hover()
elif context.target == Target.current:
elem.remove_blank_target() elem.remove_blank_target()
elem.click(target_mapping[context.target])
self.hint_events.emit(target_mapping[context.target], events) else:
if elem.is_text_input() and elem.is_editable(): elem.click(target_mapping[context.target])
QTimer.singleShot(0, context.tab.caret.move_to_end_of_document)
def yank(self, url, context): def yank(self, url, context):
"""Yank an element to the clipboard or primary selection. """Yank an element to the clipboard or primary selection.
@ -397,8 +358,6 @@ class HintManager(QObject):
Target.spawn: "Spawn command via hint", Target.spawn: "Spawn command via hint",
} }
hint_events = pyqtSignal(usertypes.ClickTarget, list) # QMouseEvent list
def __init__(self, win_id, tab_id, parent=None): def __init__(self, win_id, tab_id, parent=None):
"""Constructor.""" """Constructor."""
super().__init__(parent) super().__init__(parent)
@ -408,7 +367,6 @@ class HintManager(QObject):
self._word_hinter = WordHinter() self._word_hinter = WordHinter()
self._actions = HintActions(win_id) self._actions = HintActions(win_id)
self._actions.hint_events.connect(self.hint_events)
mode_manager = objreg.get('mode-manager', scope='window', mode_manager = objreg.get('mode-manager', scope='window',
window=win_id) window=win_id)

View File

@ -29,7 +29,8 @@ Module attributes:
import collections.abc import collections.abc
from PyQt5.QtCore import QUrl from PyQt5.QtCore import QUrl, Qt, QEvent, QTimer
from PyQt5.QtGui import QMouseEvent
from qutebrowser.config import config from qutebrowser.config import config
from qutebrowser.utils import log, usertypes, utils, qtutils from qutebrowser.utils import log, usertypes, utils, qtutils
@ -73,7 +74,14 @@ class Error(Exception):
class AbstractWebElement(collections.abc.MutableMapping): class AbstractWebElement(collections.abc.MutableMapping):
"""A wrapper around QtWebKit/QtWebEngine web element.""" """A wrapper around QtWebKit/QtWebEngine web element.
Attributes:
tab: The tab associated with this element.
"""
def __init__(self, tab):
self._tab = tab
def __eq__(self, other): def __eq__(self, other):
raise NotImplementedError raise NotImplementedError
@ -333,3 +341,60 @@ class AbstractWebElement(collections.abc.MutableMapping):
url = baseurl.resolved(url) url = baseurl.resolved(url)
qtutils.ensure_valid(url) qtutils.ensure_valid(url)
return url return url
def _mouse_pos(self):
"""Get the position to click/hover."""
# Click the center of the largest square fitting into the top/left
# corner of the rectangle, this will help if part of the <a> element
# is hidden behind other elements
# https://github.com/The-Compiler/qutebrowser/issues/1005
rect = self.rect_on_view()
if rect.width() > rect.height():
rect.setWidth(rect.height())
else:
rect.setHeight(rect.width())
return rect.center()
def click(self, click_target):
"""Simulate a click on the element."""
# FIXME:qtwebengine do we need this?
# self._widget.setFocus()
self._tab.data.override_target = click_target
pos = self._mouse_pos()
log.hints.debug("Sending fake click to '{}' at position {} with "
"target {}".format(self.debug_text(), pos,
click_target))
if click_target in [usertypes.ClickTarget.tab,
usertypes.ClickTarget.tab_bg,
usertypes.ClickTarget.window]:
modifiers = Qt.ControlModifier
else:
modifiers = Qt.NoModifier
events = [
QMouseEvent(QEvent.MouseMove, pos, Qt.NoButton, Qt.NoButton,
Qt.NoModifier),
QMouseEvent(QEvent.MouseButtonPress, pos, Qt.LeftButton,
Qt.LeftButton, modifiers),
QMouseEvent(QEvent.MouseButtonRelease, pos, Qt.LeftButton,
Qt.NoButton, modifiers),
]
for evt in events:
self._tab.post_event(evt)
def after_click():
if self.is_text_input() and self.is_editable():
self._tab.caret.move_to_end_of_document()
self._tab.data.override_target = None
QTimer.singleShot(0, after_click)
def hover(self):
"""Simulate a mouse hover over the element."""
pos = self._mouse_pos()
event = QMouseEvent(QEvent.MouseMove, pos, Qt.NoButton, Qt.NoButton,
Qt.NoModifier)
self._tab.post_event(event)

View File

@ -33,9 +33,9 @@ class WebEngineElement(webelem.AbstractWebElement):
"""A web element for QtWebEngine, using JS under the hood.""" """A web element for QtWebEngine, using JS under the hood."""
def __init__(self, js_dict, tab): def __init__(self, js_dict, tab):
super().__init__(tab)
self._id = js_dict['id'] self._id = js_dict['id']
self._js_dict = js_dict self._js_dict = js_dict
self._tab = tab
def __eq__(self, other): def __eq__(self, other):
if not isinstance(other, WebEngineElement): if not isinstance(other, WebEngineElement):

View File

@ -271,7 +271,7 @@ class _Downloader:
elements = web_frame.findAllElements('link, script, img') elements = web_frame.findAllElements('link, script, img')
for element in elements: for element in elements:
element = webkitelem.WebKitElement(element) element = webkitelem.WebKitElement(element, tab=self.tab)
# Websites are free to set whatever rel=... attribute they want. # Websites are free to set whatever rel=... attribute they want.
# We just care about stylesheets and icons. # We just care about stylesheets and icons.
if not _check_rel(element): if not _check_rel(element):
@ -288,7 +288,7 @@ class _Downloader:
styles = web_frame.findAllElements('style') styles = web_frame.findAllElements('style')
for style in styles: for style in styles:
style = webkitelem.WebKitElement(style) style = webkitelem.WebKitElement(style, tab=self.tab)
# The Mozilla Developer Network says: # The Mozilla Developer Network says:
# type: This attribute defines the styling language as a MIME type # type: This attribute defines the styling language as a MIME type
# (charset should not be specified). This attribute is optional and # (charset should not be specified). This attribute is optional and
@ -301,7 +301,7 @@ class _Downloader:
# Search for references in inline styles # Search for references in inline styles
for element in web_frame.findAllElements('[style]'): for element in web_frame.findAllElements('[style]'):
element = webkitelem.WebKitElement(element) element = webkitelem.WebKitElement(element, tab=self.tab)
style = element['style'] style = element['style']
for element_url in _get_css_imports(style, inline=True): for element_url in _get_css_imports(style, inline=True):
self._fetch_url(web_url.resolved(QUrl(element_url))) self._fetch_url(web_url.resolved(QUrl(element_url)))

View File

@ -38,7 +38,8 @@ class WebKitElement(webelem.AbstractWebElement):
"""A wrapper around a QWebElement.""" """A wrapper around a QWebElement."""
def __init__(self, elem): def __init__(self, elem, tab):
super().__init__(tab)
if isinstance(elem, self.__class__): if isinstance(elem, self.__class__):
raise TypeError("Trying to wrap a wrapper!") raise TypeError("Trying to wrap a wrapper!")
if elem.isNull(): if elem.isNull():
@ -146,7 +147,7 @@ class WebKitElement(webelem.AbstractWebElement):
elem = self._elem.parent() elem = self._elem.parent()
if elem is None: if elem is None:
return None return None
return WebKitElement(elem) return WebKitElement(elem, tab=self._tab)
def _rect_on_view_js(self): def _rect_on_view_js(self):
"""Javascript implementation for rect_on_view.""" """Javascript implementation for rect_on_view."""
@ -303,4 +304,4 @@ def focus_elem(frame):
frame: The QWebFrame to search in. frame: The QWebFrame to search in.
""" """
elem = frame.findFirstElement('*:focus') elem = frame.findFirstElement('*:focus')
return WebKitElement(elem) return WebKitElement(elem, tab=None)

View File

@ -505,7 +505,7 @@ class WebKitElements(browsertab.AbstractElements):
frames = webkitelem.get_child_frames(mainframe) frames = webkitelem.get_child_frames(mainframe)
for f in frames: for f in frames:
for elem in f.findAllElements(selector): for elem in f.findAllElements(selector):
elems.append(webkitelem.WebKitElement(elem)) elems.append(webkitelem.WebKitElement(elem, tab=self._tab))
if only_visible: if only_visible:
elems = [e for e in elems if e.is_visible(mainframe)] elems = [e for e in elems if e.is_visible(mainframe)]
@ -525,7 +525,7 @@ class WebKitElements(browsertab.AbstractElements):
if elem.isNull(): if elem.isNull():
callback(None) callback(None)
else: else:
callback(webkitelem.WebKitElement(elem)) callback(webkitelem.WebKitElement(elem, tab=self._tab))
def find_at_pos(self, pos, callback): def find_at_pos(self, pos, callback):
assert pos.x() >= 0 assert pos.x() >= 0
@ -553,7 +553,7 @@ class WebKitElements(browsertab.AbstractElements):
return return
try: try:
elem = webkitelem.WebKitElement(hitresult.element()) elem = webkitelem.WebKitElement(hitresult.element(), tab=self._tab)
except webkitelem.IsNullError: except webkitelem.IsNullError:
# For some reason, the hit result element can be a null element # For some reason, the hit result element can be a null element
# sometimes (e.g. when clicking the timetable fields on # sometimes (e.g. when clicking the timetable fields on

View File

@ -226,7 +226,8 @@ PromptMode = enum('PromptMode', ['yesno', 'text', 'user_pwd', 'alert',
# Where to open a clicked link. # Where to open a clicked link.
ClickTarget = enum('ClickTarget', ['normal', 'tab', 'tab_bg', 'window']) ClickTarget = enum('ClickTarget', ['normal', 'tab', 'tab_bg', 'window',
'hover'])
# Key input modes # Key input modes

View File

@ -120,7 +120,7 @@ def get_webelem(geometry=None, frame=None, *, null=False, style=None,
return style_dict[name] return style_dict[name]
elem.styleProperty.side_effect = _style_property elem.styleProperty.side_effect = _style_property
wrapped = webkitelem.WebKitElement(elem) wrapped = webkitelem.WebKitElement(elem, tab=None)
return wrapped return wrapped
@ -218,7 +218,7 @@ class TestSelectorsAndFilters:
# Make sure setting HTML succeeded and there's a new element # Make sure setting HTML succeeded and there's a new element
assert len(webframe.findAllElements('*')) == 3 assert len(webframe.findAllElements('*')) == 3
elems = webframe.findAllElements(webelem.SELECTORS[group]) elems = webframe.findAllElements(webelem.SELECTORS[group])
elems = [webkitelem.WebKitElement(e) for e in elems] elems = [webkitelem.WebKitElement(e, tab=None) for e in elems]
filterfunc = webelem.FILTERS.get(group, lambda e: True) filterfunc = webelem.FILTERS.get(group, lambda e: True)
elems = [e for e in elems if filterfunc(e)] elems = [e for e in elems if filterfunc(e)]
assert bool(elems) == matching assert bool(elems) == matching
@ -244,7 +244,7 @@ class TestWebKitElement:
def test_double_wrap(self, elem): def test_double_wrap(self, elem):
"""Test wrapping a WebKitElement.""" """Test wrapping a WebKitElement."""
with pytest.raises(TypeError) as excinfo: with pytest.raises(TypeError) as excinfo:
webkitelem.WebKitElement(elem) webkitelem.WebKitElement(elem, tab=None)
assert str(excinfo.value) == "Trying to wrap a wrapper!" assert str(excinfo.value) == "Trying to wrap a wrapper!"
@pytest.mark.parametrize('code', [ @pytest.mark.parametrize('code', [
@ -329,7 +329,7 @@ class TestWebKitElement:
def test_eq(self): def test_eq(self):
one = get_webelem() one = get_webelem()
two = webkitelem.WebKitElement(one._elem) two = webkitelem.WebKitElement(one._elem, tab=None)
assert one == two assert one == two
def test_eq_other_type(self): def test_eq_other_type(self):