Permalink
Browse files

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

  • Loading branch information...
The-Compiler committed Aug 18, 2016
1 parent 5ac9fe9 commit 63c66945a439cc71db3b2166fd1174978f34dc79
@@ -70,19 +70,19 @@ 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):
self.keep_icon = False
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 @@ def __init__(self, win_id, parent=None):
# 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 @@ def post_event(self, evt):
"""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")
@@ -26,9 +26,7 @@
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 @@ def get_args(self, urlstr):
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 @@ def click(self, elem, context):
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 @@ def __init__(self, win_id, tab_id, parent=None):
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)
@@ -29,7 +29,8 @@
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 @@ def resolve_url(self, baseurl):
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)
@@ -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):
@@ -271,7 +271,7 @@ def run(self):
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 @@ def run(self):
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 @@ def run(self):
# 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)))
@@ -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 @@ def parent(self):
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)
@@ -505,7 +505,7 @@ def find_css(self, selector, callback, *, only_visible=False):
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 @@ def find_focused(self, callback):
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 @@ def find_at_pos(self, pos, callback):
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
@@ -226,7 +226,8 @@ def reset(self):
# 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
@@ -120,7 +120,7 @@ def _style_property(name, strategy):
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 @@ def test_selectors(self, webframe, group, val, matching):
# 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 @@ def test_nullelem(self):
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 @@ def test_not_eq(self):
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):

0 comments on commit 63c6694

Please sign in to comment.