Add webelem.click() and webelem.hover()
This commit is contained in:
parent
5ac9fe9c32
commit
63c66945a4
@ -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")
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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 @@ 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)))
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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):
|
||||
|
Loading…
Reference in New Issue
Block a user