diff --git a/qutebrowser/browser/hints.py b/qutebrowser/browser/hints.py index 7659897c0..3069ed71e 100644 --- a/qutebrowser/browser/hints.py +++ b/qutebrowser/browser/hints.py @@ -124,8 +124,8 @@ class HintLabel(QLabel): self.hide() return no_js = config.get('hints', 'find-implementation') != 'javascript' - rect = self.elem.rect_on_view(no_js=no_js) - self.move(rect.x(), rect.y()) + self.elem.rect_on_view(no_js=no_js, + callback=lambda r: self.move(r.x(), r.y())) def cleanup(self): """Clean up this element and hide it.""" @@ -221,54 +221,59 @@ 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() + def click_cb(rect): + """Actually click the element. - action = "Hovering" if context.target == Target.hover else "Clicking" - log.hints.debug("{} on '{}' at position {}".format( - action, elem.debug_text(), pos)) + 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 + """ + if rect.width() > rect.height(): + rect.setWidth(rect.height()) + else: + rect.setHeight(rect.width()) + pos = rect.center() - self.start_hinting.emit(target_mapping[context.target]) - 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), + action = "Hovering" if context.target == Target.hover else "Clicking" + log.hints.debug("{} on '{}' at position {}".format( + action, elem.debug_text(), pos)) + + self.start_hinting.emit(target_mapping[context.target]) + 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 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: - elem.remove_blank_target() - for evt in events: - self.mouse_event.emit(evt) - if elem.is_text_input() and elem.is_editable(): - QTimer.singleShot(0, functools.partial( - elem.frame().page().triggerAction, - QWebPage.MoveToEndOfDocument)) - QTimer.singleShot(0, self.stop_hinting.emit) + if context.target == Target.current: + elem.remove_blank_target() + for evt in events: + self.mouse_event.emit(evt) + if elem.is_text_input() and elem.is_editable(): + QTimer.singleShot(0, functools.partial( + elem.frame().page().triggerAction, + QWebPage.MoveToEndOfDocument)) + QTimer.singleShot(0, self.stop_hinting.emit) + + elem.rect_on_view(callback=click_cb) def yank(self, url, context): """Yank an element to the clipboard or primary selection. diff --git a/qutebrowser/browser/webkit/webkitelem.py b/qutebrowser/browser/webkit/webkitelem.py index 75b6559b3..90d4812d9 100644 --- a/qutebrowser/browser/webkit/webkitelem.py +++ b/qutebrowser/browser/webkit/webkitelem.py @@ -199,8 +199,22 @@ class WebKitElement(webelem.AbstractWebElement): frame = frame.parentFrame() return rect - def rect_on_view(self, *, elem_geometry=None, no_js=False): - """Get the geometry of the element relative to the webview. + def _rect_on_view_sync(self, *, elem_geometry=None, no_js=False): + """Synchronous part of rect_on_view.""" + self._check_vanished() + + # First try getting the element rect via JS, as that's usually more + # accurate + if elem_geometry is None and not no_js: + rect = self._rect_on_view_js() + if rect is not None: + return rect + + # No suitable rects found via JS, try via the QWebElement API + return self._rect_on_view_python(elem_geometry) + + def rect_on_view(self, *, callback, **kwargs): + """Get the geometry of the element relative to the webview (async). Uses the getClientRects() JavaScript method to obtain the collection of rectangles containing the element and returns the first rectangle which @@ -216,21 +230,15 @@ class WebKitElement(webelem.AbstractWebElement): Calling QWebElement::geometry is rather expensive so we want to avoid doing it twice. no_js: Fall back to the Python implementation + callback: Gets called with the found QRect. """ self._check_vanished() - - # First try getting the element rect via JS, as that's usually more - # accurate - if elem_geometry is None and not no_js: - rect = self._rect_on_view_js() - if rect is not None: - return rect - - # No suitable rects found via JS, try via the QWebElement API - return self._rect_on_view_python(elem_geometry) + callback(self._rect_on_view_sync(**kwargs)) def is_visible(self, mainframe): """Check if the given element is visible in the given frame.""" + # FIXME:qtwebengine can we get rid of this with + # find_all_elements(only_visible=True)? self._check_vanished() # CSS attributes which hide an element hidden_attributes = { @@ -245,7 +253,7 @@ class WebKitElement(webelem.AbstractWebElement): # Most likely an invisible link return False # First check if the element is visible on screen - elem_rect = self.rect_on_view(elem_geometry=elem_geometry) + elem_rect = self._rect_on_view_sync(elem_geometry=elem_geometry) mainframe_geometry = mainframe.geometry() if elem_rect.isValid(): visible_on_screen = mainframe_geometry.intersects(elem_rect) diff --git a/tests/unit/browser/webkit/test_webkitelem.py b/tests/unit/browser/webkit/test_webkitelem.py index fb179c590..443913b3e 100644 --- a/tests/unit/browser/webkit/test_webkitelem.py +++ b/tests/unit/browser/webkit/test_webkitelem.py @@ -269,7 +269,7 @@ class TestWebKitElement: lambda e: e.outer_xml(), lambda e: e.tag_name(), lambda e: e.run_js_async(''), - lambda e: e.rect_on_view(), + lambda e: e.rect_on_view(callback=None), lambda e: e.is_visible(None), ], ids=['str', 'getitem', 'setitem', 'delitem', 'contains', 'iter', 'len', 'frame', 'geometry', 'style_property', 'text', 'set_text', @@ -708,22 +708,24 @@ class TestRectOnView: # unusable geometry via getElementRects {'length': '1', '0': {'width': 0, 'height': 0, 'x': 0, 'y': 0}}, ]) - def test_simple(self, stubs, js_rect): + def test_simple(self, callback_checker, stubs, js_rect): geometry = QRect(5, 5, 4, 4) frame = stubs.FakeWebFrame(QRect(0, 0, 100, 100)) elem = get_webelem(geometry, frame, js_rect_return=js_rect) - assert elem.rect_on_view() == QRect(5, 5, 4, 4) + elem.rect_on_view(callback=callback_checker.callback) + callback_checker.check(QRect(5, 5, 4, 4)) @pytest.mark.parametrize('js_rect', [None, {}]) - def test_scrolled(self, stubs, js_rect): + def test_scrolled(self, callback_checker, stubs, js_rect): geometry = QRect(20, 20, 4, 4) frame = stubs.FakeWebFrame(QRect(0, 0, 100, 100), scroll=QPoint(10, 10)) elem = get_webelem(geometry, frame, js_rect_return=js_rect) - assert elem.rect_on_view() == QRect(20 - 10, 20 - 10, 4, 4) + elem.rect_on_view(callback=callback_checker.callback) + callback_checker.check(QRect(20 - 10, 20 - 10, 4, 4)) @pytest.mark.parametrize('js_rect', [None, {}]) - def test_iframe(self, stubs, js_rect): + def test_iframe(self, callback_checker, stubs, js_rect): """Test an element in an iframe. 0, 0 200, 0 @@ -744,27 +746,32 @@ class TestRectOnView: assert frame.geometry().contains(iframe.geometry()) elem = get_webelem(QRect(20, 90, 10, 10), iframe, js_rect_return=js_rect) - assert elem.rect_on_view() == QRect(20, 10 + 90, 10, 10) + elem.rect_on_view(callback=callback_checker.callback) + callback_checker.check(QRect(20, 10 + 90, 10, 10)) @pytest.mark.parametrize('js_rect', [None, {}]) - def test_passed_geometry(self, stubs, js_rect): + def test_passed_geometry(self, callback_checker, stubs, js_rect): """Make sure geometry isn't called when a geometry is passed.""" frame = stubs.FakeWebFrame(QRect(0, 0, 200, 200)) elem = get_webelem(frame=frame, js_rect_return=js_rect) rect = QRect(10, 20, 30, 40) - assert elem.rect_on_view(elem_geometry=rect) == rect + elem.rect_on_view(elem_geometry=rect, + callback=callback_checker.callback) + callback_checker.check(rect) assert not elem._elem.geometry.called @pytest.mark.parametrize('js_rect', [None, {}]) @pytest.mark.parametrize('zoom_text_only', [True, False]) - def test_zoomed(self, stubs, config_stub, js_rect, zoom_text_only): + def test_zoomed(self, callback_checker, stubs, config_stub, js_rect, + zoom_text_only): """Make sure the coordinates are adjusted when zoomed.""" config_stub.data = {'ui': {'zoom-text-only': zoom_text_only}} geometry = QRect(10, 10, 4, 4) frame = stubs.FakeWebFrame(QRect(0, 0, 100, 100), zoom=0.5) elem = get_webelem(geometry, frame, js_rect_return=js_rect, zoom_text_only=zoom_text_only) - assert elem.rect_on_view() == QRect(10, 10, 4, 4) + elem.rect_on_view(callback=callback_checker.callback) + callback_checker.check(QRect(10, 10, 4, 4)) class TestGetChildFrames: