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.
viewing_source: Set if we're currently showing a source view.
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):
@ -78,11 +78,11 @@ class TabData:
self.viewing_source = False
self.inspector = None
self.open_target = usertypes.ClickTarget.normal
self.hint_target = None
self.override_target = None
def combined_target(self):
if self.hint_target is not None:
return self.hint_target
if self.override_target is not None:
return self.override_target
else:
return self.open_target
@ -535,7 +535,6 @@ class AbstractTab(QWidget):
# FIXME:qtwebengine Should this be public api via self.hints?
# Also, should we get it out of objreg?
hintmanager = hints.HintManager(win_id, self.tab_id, parent=self)
hintmanager.hint_events.connect(self._on_hint_events)
objreg.register('hintmanager', hintmanager, scope='tab',
window=self.win_id, tab=self.tab_id)
@ -567,27 +566,12 @@ class AbstractTab(QWidget):
"""Send the given event to the underlying widget."""
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)
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(
url.toDisplayString(),
self.data.hint_target, self.data.open_target))
self.data.override_target, self.data.open_target))
if not url.isValid():
msg = urlutils.get_errstring(url, "Invalid link clicked")

View File

@ -26,9 +26,7 @@ import re
import html
from string import ascii_lowercase
from PyQt5.QtCore import (pyqtSignal, pyqtSlot, QObject, QEvent, Qt, QUrl,
QTimer)
from PyQt5.QtGui import QMouseEvent
from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject, Qt, QUrl
from PyQt5.QtWidgets import QLabel
from qutebrowser.config import config, style
@ -182,13 +180,7 @@ class HintContext:
class HintActions(QObject):
"""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
"""Actions which can be done after selecting a hint."""
def __init__(self, win_id, parent=None):
super().__init__(parent)
@ -214,50 +206,19 @@ class HintActions(QObject):
else:
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]:
# Set the pre-jump mark ', so we can jump back here after following
tabbed_browser = objreg.get('tabbed-browser', scope='window',
window=self._win_id)
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()
self.hint_events.emit(target_mapping[context.target], events)
if elem.is_text_input() and elem.is_editable():
QTimer.singleShot(0, context.tab.caret.move_to_end_of_document)
elem.click(target_mapping[context.target])
else:
elem.click(target_mapping[context.target])
def yank(self, url, context):
"""Yank an element to the clipboard or primary selection.
@ -397,8 +358,6 @@ class HintManager(QObject):
Target.spawn: "Spawn command via hint",
}
hint_events = pyqtSignal(usertypes.ClickTarget, list) # QMouseEvent list
def __init__(self, win_id, tab_id, parent=None):
"""Constructor."""
super().__init__(parent)
@ -408,7 +367,6 @@ class HintManager(QObject):
self._word_hinter = WordHinter()
self._actions = HintActions(win_id)
self._actions.hint_events.connect(self.hint_events)
mode_manager = objreg.get('mode-manager', scope='window',
window=win_id)

View File

@ -29,7 +29,8 @@ Module attributes:
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.utils import log, usertypes, utils, qtutils
@ -73,7 +74,14 @@ class Error(Exception):
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):
raise NotImplementedError
@ -333,3 +341,60 @@ class AbstractWebElement(collections.abc.MutableMapping):
url = baseurl.resolved(url)
qtutils.ensure_valid(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."""
def __init__(self, js_dict, tab):
super().__init__(tab)
self._id = js_dict['id']
self._js_dict = js_dict
self._tab = tab
def __eq__(self, other):
if not isinstance(other, WebEngineElement):

View File

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

View File

@ -38,7 +38,8 @@ class WebKitElement(webelem.AbstractWebElement):
"""A wrapper around a QWebElement."""
def __init__(self, elem):
def __init__(self, elem, tab):
super().__init__(tab)
if isinstance(elem, self.__class__):
raise TypeError("Trying to wrap a wrapper!")
if elem.isNull():
@ -146,7 +147,7 @@ class WebKitElement(webelem.AbstractWebElement):
elem = self._elem.parent()
if elem is None:
return None
return WebKitElement(elem)
return WebKitElement(elem, tab=self._tab)
def _rect_on_view_js(self):
"""Javascript implementation for rect_on_view."""
@ -303,4 +304,4 @@ def focus_elem(frame):
frame: The QWebFrame to search in.
"""
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)
for f in frames:
for elem in f.findAllElements(selector):
elems.append(webkitelem.WebKitElement(elem))
elems.append(webkitelem.WebKitElement(elem, tab=self._tab))
if only_visible:
elems = [e for e in elems if e.is_visible(mainframe)]
@ -525,7 +525,7 @@ class WebKitElements(browsertab.AbstractElements):
if elem.isNull():
callback(None)
else:
callback(webkitelem.WebKitElement(elem))
callback(webkitelem.WebKitElement(elem, tab=self._tab))
def find_at_pos(self, pos, callback):
assert pos.x() >= 0
@ -553,7 +553,7 @@ class WebKitElements(browsertab.AbstractElements):
return
try:
elem = webkitelem.WebKitElement(hitresult.element())
elem = webkitelem.WebKitElement(hitresult.element(), tab=self._tab)
except webkitelem.IsNullError:
# For some reason, the hit result element can be a null element
# 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.
ClickTarget = enum('ClickTarget', ['normal', 'tab', 'tab_bg', 'window'])
ClickTarget = enum('ClickTarget', ['normal', 'tab', 'tab_bg', 'window',
'hover'])
# Key input modes

View File

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