diff --git a/qutebrowser/browser/browsertab.py b/qutebrowser/browser/browsertab.py index 2c94aa671..e825ea6cd 100644 --- a/qutebrowser/browser/browsertab.py +++ b/qutebrowser/browser/browsertab.py @@ -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") diff --git a/qutebrowser/browser/hints.py b/qutebrowser/browser/hints.py index 1c2c1bf4b..7c59af8ee 100644 --- a/qutebrowser/browser/hints.py +++ b/qutebrowser/browser/hints.py @@ -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 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) diff --git a/qutebrowser/browser/webelem.py b/qutebrowser/browser/webelem.py index d20863174..462328827 100644 --- a/qutebrowser/browser/webelem.py +++ b/qutebrowser/browser/webelem.py @@ -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 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) diff --git a/qutebrowser/browser/webengine/webengineelem.py b/qutebrowser/browser/webengine/webengineelem.py index 7b17372e4..9610d3647 100644 --- a/qutebrowser/browser/webengine/webengineelem.py +++ b/qutebrowser/browser/webengine/webengineelem.py @@ -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): diff --git a/qutebrowser/browser/webkit/mhtml.py b/qutebrowser/browser/webkit/mhtml.py index 2928c5a3f..c615fcad4 100644 --- a/qutebrowser/browser/webkit/mhtml.py +++ b/qutebrowser/browser/webkit/mhtml.py @@ -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))) diff --git a/qutebrowser/browser/webkit/webkitelem.py b/qutebrowser/browser/webkit/webkitelem.py index ce5e9caea..7b0810372 100644 --- a/qutebrowser/browser/webkit/webkitelem.py +++ b/qutebrowser/browser/webkit/webkitelem.py @@ -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) diff --git a/qutebrowser/browser/webkit/webkittab.py b/qutebrowser/browser/webkit/webkittab.py index 4e5eb10ee..e2b33ba2c 100644 --- a/qutebrowser/browser/webkit/webkittab.py +++ b/qutebrowser/browser/webkit/webkittab.py @@ -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 diff --git a/qutebrowser/utils/usertypes.py b/qutebrowser/utils/usertypes.py index ea7662e41..26f9202ff 100644 --- a/qutebrowser/utils/usertypes.py +++ b/qutebrowser/utils/usertypes.py @@ -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 diff --git a/tests/unit/browser/webkit/test_webkitelem.py b/tests/unit/browser/webkit/test_webkitelem.py index 329c00ae6..efe676c5a 100644 --- a/tests/unit/browser/webkit/test_webkitelem.py +++ b/tests/unit/browser/webkit/test_webkitelem.py @@ -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):