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):