hints: Integrate _get_first_rectangle into webelem
This commit is contained in:
parent
4d04d0a511
commit
10630e30ab
@ -26,7 +26,7 @@ import re
|
|||||||
import string
|
import string
|
||||||
|
|
||||||
from PyQt5.QtCore import (pyqtSignal, pyqtSlot, QObject, QEvent, Qt, QUrl,
|
from PyQt5.QtCore import (pyqtSignal, pyqtSlot, QObject, QEvent, Qt, QUrl,
|
||||||
QTimer, QRect)
|
QTimer)
|
||||||
from PyQt5.QtGui import QMouseEvent
|
from PyQt5.QtGui import QMouseEvent
|
||||||
from PyQt5.QtWebKit import QWebElement
|
from PyQt5.QtWebKit import QWebElement
|
||||||
from PyQt5.QtWebKitWidgets import QWebPage
|
from PyQt5.QtWebKitWidgets import QWebPage
|
||||||
@ -376,7 +376,7 @@ class HintManager(QObject):
|
|||||||
elem: The QWebElement to set the style attributes for.
|
elem: The QWebElement to set the style attributes for.
|
||||||
label: The label QWebElement.
|
label: The label QWebElement.
|
||||||
"""
|
"""
|
||||||
rect = self._get_first_rectangle(elem, adjust_zoom=False)
|
rect = elem.rect_on_view(adjust_zoom=False)
|
||||||
left = rect.x()
|
left = rect.x()
|
||||||
top = rect.y()
|
top = rect.y()
|
||||||
log.hints.vdebug("Drawing label '{!r}' at {}/{} for element '{!r}'"
|
log.hints.vdebug("Drawing label '{!r}' at {}/{} for element '{!r}'"
|
||||||
@ -417,55 +417,6 @@ class HintManager(QObject):
|
|||||||
message.error(self._win_id, "No suitable link found for this element.",
|
message.error(self._win_id, "No suitable link found for this element.",
|
||||||
immediately=True)
|
immediately=True)
|
||||||
|
|
||||||
def _get_first_rectangle(self, elem, *, adjust_zoom=True):
|
|
||||||
"""Return the element's first client rectangle with positive size.
|
|
||||||
|
|
||||||
Uses the getClientRects() JavaScript method to obtain the collection of
|
|
||||||
rectangles containing the element and returns the first rectangle which
|
|
||||||
is large enough (larger than 1px times 1px). If all rectangles returned
|
|
||||||
by getClientRects() are too small, falls back to elem.rect_on_view().
|
|
||||||
|
|
||||||
Skipping of small rectangles is due to <a> elements containing other
|
|
||||||
elements with "display:block" style, see
|
|
||||||
https://github.com/The-Compiler/qutebrowser/issues/1298
|
|
||||||
|
|
||||||
Args:
|
|
||||||
elem: The QWebElement of interest.
|
|
||||||
adjust_zoom: Whether to adjust the element position based on the
|
|
||||||
current zoom level.
|
|
||||||
"""
|
|
||||||
rects = elem.evaluateJavaScript("this.getClientRects()")
|
|
||||||
log.hints.vdebug("Client rectangles of element '{}': {}"
|
|
||||||
.format(elem.debug_text(), rects))
|
|
||||||
for i in range(int(rects.get("length", 0))):
|
|
||||||
rect = rects[str(i)]
|
|
||||||
width = rect.get("width", 0)
|
|
||||||
height = rect.get("height", 0)
|
|
||||||
if width > 1 and height > 1:
|
|
||||||
# fix coordinates according to zoom level
|
|
||||||
zoom = elem.webFrame().zoomFactor()
|
|
||||||
if not config.get('ui', 'zoom-text-only') and adjust_zoom:
|
|
||||||
rect["left"] *= zoom
|
|
||||||
rect["top"] *= zoom
|
|
||||||
width *= zoom
|
|
||||||
height *= zoom
|
|
||||||
rect = QRect(rect["left"], rect["top"], width, height)
|
|
||||||
frame = elem.webFrame()
|
|
||||||
while frame is not None:
|
|
||||||
# Translate to parent frames' position
|
|
||||||
# (scroll position is taken care of inside getClientRects)
|
|
||||||
rect.translate(frame.geometry().topLeft())
|
|
||||||
frame = frame.parentFrame()
|
|
||||||
return rect
|
|
||||||
|
|
||||||
# No suitable rects found via JS, try via the QWebElement API
|
|
||||||
rect = elem.rect_on_view()
|
|
||||||
zoom = elem.webFrame().zoomFactor()
|
|
||||||
if not config.get('ui', 'zoom-text-only'):
|
|
||||||
rect.setLeft(rect.left() / zoom)
|
|
||||||
rect.setTop(rect.top() / zoom)
|
|
||||||
return rect
|
|
||||||
|
|
||||||
def _click(self, elem, context):
|
def _click(self, elem, context):
|
||||||
"""Click an element.
|
"""Click an element.
|
||||||
|
|
||||||
@ -490,7 +441,7 @@ class HintManager(QObject):
|
|||||||
# corner of the rectangle, this will help if part of the <a> element
|
# corner of the rectangle, this will help if part of the <a> element
|
||||||
# is hidden behind other elements
|
# is hidden behind other elements
|
||||||
# https://github.com/The-Compiler/qutebrowser/issues/1005
|
# https://github.com/The-Compiler/qutebrowser/issues/1005
|
||||||
rect = self._get_first_rectangle(elem)
|
rect = elem.rect_on_view()
|
||||||
if rect.width() > rect.height():
|
if rect.width() > rect.height():
|
||||||
rect.setWidth(rect.height())
|
rect.setWidth(rect.height())
|
||||||
else:
|
else:
|
||||||
|
@ -173,9 +173,14 @@ class WebElementWrapper(collections.abc.MutableMapping):
|
|||||||
"""
|
"""
|
||||||
return is_visible(self._elem, mainframe)
|
return is_visible(self._elem, mainframe)
|
||||||
|
|
||||||
def rect_on_view(self):
|
def rect_on_view(self, *, adjust_zoom=True):
|
||||||
"""Get the geometry of the element relative to the webview."""
|
"""Get the geometry of the element relative to the webview.
|
||||||
return rect_on_view(self._elem)
|
|
||||||
|
Args:
|
||||||
|
adjust_zoom: Whether to adjust the element position based on the
|
||||||
|
current zoom level.
|
||||||
|
"""
|
||||||
|
return rect_on_view(self._elem, adjust_zoom=adjust_zoom)
|
||||||
|
|
||||||
def is_writable(self):
|
def is_writable(self):
|
||||||
"""Check whether an element is writable."""
|
"""Check whether an element is writable."""
|
||||||
@ -363,21 +368,62 @@ def focus_elem(frame):
|
|||||||
return WebElementWrapper(elem)
|
return WebElementWrapper(elem)
|
||||||
|
|
||||||
|
|
||||||
def rect_on_view(elem, elem_geometry=None):
|
def rect_on_view(elem, *, elem_geometry=None, adjust_zoom=True):
|
||||||
"""Get the geometry of the element relative to the webview.
|
"""Get the geometry of the element relative to the webview.
|
||||||
|
|
||||||
We need this as a standalone function (as opposed to a WebElementWrapper
|
We need this as a standalone function (as opposed to a WebElementWrapper
|
||||||
method) because we want to run is_visible before wrapping when hinting for
|
method) because we want to run is_visible before wrapping when hinting for
|
||||||
performance reasons.
|
performance reasons.
|
||||||
|
|
||||||
|
Uses the getClientRects() JavaScript method to obtain the collection of
|
||||||
|
rectangles containing the element and returns the first rectangle which is
|
||||||
|
large enough (larger than 1px times 1px). If all rectangles returned by
|
||||||
|
getClientRects() are too small, falls back to elem.rect_on_view().
|
||||||
|
|
||||||
|
Skipping of small rectangles is due to <a> elements containing other
|
||||||
|
elements with "display:block" style, see
|
||||||
|
https://github.com/The-Compiler/qutebrowser/issues/1298
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
elem: The QWebElement to get the rect for.
|
elem: The QWebElement to get the rect for.
|
||||||
elem_geometry: The geometry of the element, or None.
|
elem_geometry: The geometry of the element, or None.
|
||||||
Calling QWebElement::geometry is rather expensive so we
|
Calling QWebElement::geometry is rather expensive so we
|
||||||
want to avoid doing it twice.
|
want to avoid doing it twice.
|
||||||
|
adjust_zoom: Whether to adjust the element position based on the
|
||||||
|
current zoom level.
|
||||||
"""
|
"""
|
||||||
if elem.isNull():
|
if elem.isNull():
|
||||||
raise IsNullError("Got called on a null element!")
|
raise IsNullError("Got called on a null element!")
|
||||||
|
|
||||||
|
# First try getting the element rect via JS, as that's usually more
|
||||||
|
# accurate
|
||||||
|
if elem_geometry is None:
|
||||||
|
rects = elem.evaluateJavaScript("this.getClientRects()")
|
||||||
|
text = utils.compact_text(elem.toOuterXml(), 500)
|
||||||
|
log.hints.vdebug("Client rectangles of element '{}': {}".format(text,
|
||||||
|
rects))
|
||||||
|
for i in range(int(rects.get("length", 0))):
|
||||||
|
rect = rects[str(i)]
|
||||||
|
width = rect.get("width", 0)
|
||||||
|
height = rect.get("height", 0)
|
||||||
|
if width > 1 and height > 1:
|
||||||
|
# fix coordinates according to zoom level
|
||||||
|
zoom = elem.webFrame().zoomFactor()
|
||||||
|
if not config.get('ui', 'zoom-text-only') and adjust_zoom:
|
||||||
|
rect["left"] *= zoom
|
||||||
|
rect["top"] *= zoom
|
||||||
|
width *= zoom
|
||||||
|
height *= zoom
|
||||||
|
rect = QRect(rect["left"], rect["top"], width, height)
|
||||||
|
frame = elem.webFrame()
|
||||||
|
while frame is not None:
|
||||||
|
# Translate to parent frames' position
|
||||||
|
# (scroll position is taken care of inside getClientRects)
|
||||||
|
rect.translate(frame.geometry().topLeft())
|
||||||
|
frame = frame.parentFrame()
|
||||||
|
return rect
|
||||||
|
|
||||||
|
# No suitable rects found via JS, try via the QWebElement API
|
||||||
if elem_geometry is None:
|
if elem_geometry is None:
|
||||||
elem_geometry = elem.geometry()
|
elem_geometry = elem.geometry()
|
||||||
frame = elem.webFrame()
|
frame = elem.webFrame()
|
||||||
@ -386,6 +432,11 @@ def rect_on_view(elem, elem_geometry=None):
|
|||||||
rect.translate(frame.geometry().topLeft())
|
rect.translate(frame.geometry().topLeft())
|
||||||
rect.translate(frame.scrollPosition() * -1)
|
rect.translate(frame.scrollPosition() * -1)
|
||||||
frame = frame.parentFrame()
|
frame = frame.parentFrame()
|
||||||
|
# We deliberately always adjust the zoom here, even with adjust_zoom=False
|
||||||
|
zoom = elem.webFrame().zoomFactor()
|
||||||
|
if not config.get('ui', 'zoom-text-only'):
|
||||||
|
rect.setLeft(rect.left() / zoom)
|
||||||
|
rect.setTop(rect.top() / zoom)
|
||||||
return rect
|
return rect
|
||||||
|
|
||||||
|
|
||||||
|
@ -77,7 +77,7 @@ class FakeWebFrame:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, geometry=None, *, scroll=None, plaintext=None,
|
def __init__(self, geometry=None, *, scroll=None, plaintext=None,
|
||||||
html=None, parent=None):
|
html=None, parent=None, zoom=1.0):
|
||||||
"""Constructor.
|
"""Constructor.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@ -85,6 +85,7 @@ class FakeWebFrame:
|
|||||||
scroll: The scroll position as QPoint.
|
scroll: The scroll position as QPoint.
|
||||||
plaintext: Return value of toPlainText
|
plaintext: Return value of toPlainText
|
||||||
html: Return value of tohtml.
|
html: Return value of tohtml.
|
||||||
|
zoom: The zoom factor.
|
||||||
parent: The parent frame.
|
parent: The parent frame.
|
||||||
"""
|
"""
|
||||||
if scroll is None:
|
if scroll is None:
|
||||||
@ -95,6 +96,7 @@ class FakeWebFrame:
|
|||||||
self.focus_elem = None
|
self.focus_elem = None
|
||||||
self.toPlainText = mock.Mock(return_value=plaintext)
|
self.toPlainText = mock.Mock(return_value=plaintext)
|
||||||
self.toHtml = mock.Mock(return_value=html)
|
self.toHtml = mock.Mock(return_value=html)
|
||||||
|
self.zoomFactor = mock.Mock(return_value=zoom)
|
||||||
|
|
||||||
def findFirstElement(self, selector):
|
def findFirstElement(self, selector):
|
||||||
if selector == '*:focus':
|
if selector == '*:focus':
|
||||||
|
@ -49,6 +49,7 @@ def get_webelem(geometry=None, frame=None, null=False, style=None,
|
|||||||
tagname: The tag name.
|
tagname: The tag name.
|
||||||
classes: HTML classes to be added.
|
classes: HTML classes to be added.
|
||||||
"""
|
"""
|
||||||
|
# pylint: disable=too-many-locals
|
||||||
elem = mock.Mock()
|
elem = mock.Mock()
|
||||||
elem.isNull.return_value = null
|
elem.isNull.return_value = null
|
||||||
elem.geometry.return_value = geometry
|
elem.geometry.return_value = geometry
|
||||||
@ -58,6 +59,25 @@ def get_webelem(geometry=None, frame=None, null=False, style=None,
|
|||||||
elem.toPlainText.return_value = 'text'
|
elem.toPlainText.return_value = 'text'
|
||||||
elem.parent.return_value = parent
|
elem.parent.return_value = parent
|
||||||
|
|
||||||
|
if geometry is not None:
|
||||||
|
if frame is None:
|
||||||
|
scroll_x = 0
|
||||||
|
scroll_y = 0
|
||||||
|
else:
|
||||||
|
scroll_x = frame.scrollPosition().x()
|
||||||
|
scroll_y = frame.scrollPosition().y()
|
||||||
|
elem.evaluateJavaScript.return_value = {
|
||||||
|
"length": 1,
|
||||||
|
"0": {
|
||||||
|
"left": geometry.left() - scroll_x,
|
||||||
|
"top": geometry.top() - scroll_y,
|
||||||
|
"right": geometry.right() - scroll_x,
|
||||||
|
"bottom": geometry.bottom() - scroll_y,
|
||||||
|
"width": geometry.width(),
|
||||||
|
"height": geometry.height(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
attribute_dict = {}
|
attribute_dict = {}
|
||||||
if attributes is None:
|
if attributes is None:
|
||||||
pass
|
pass
|
||||||
@ -94,6 +114,17 @@ def get_webelem(geometry=None, frame=None, null=False, style=None,
|
|||||||
return wrapped
|
return wrapped
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(autouse=True)
|
||||||
|
def stubbed_config(config_stub, monkeypatch):
|
||||||
|
"""Add a zoom-text-only fake config value.
|
||||||
|
|
||||||
|
This is needed for all the tests calling rect_on_view or is_visible.
|
||||||
|
"""
|
||||||
|
config_stub.data = {'ui': {'zoom-text-only': 'true'}}
|
||||||
|
monkeypatch.setattr('qutebrowser.browser.webelem.config', config_stub)
|
||||||
|
return config_stub
|
||||||
|
|
||||||
|
|
||||||
class SelectionAndFilterTests:
|
class SelectionAndFilterTests:
|
||||||
|
|
||||||
"""Generator for tests for TestSelectionsAndFilters."""
|
"""Generator for tests for TestSelectionsAndFilters."""
|
||||||
@ -618,9 +649,10 @@ class TestRectOnView:
|
|||||||
|
|
||||||
def test_passed_geometry(self, stubs):
|
def test_passed_geometry(self, stubs):
|
||||||
"""Make sure geometry isn't called when a geometry is passed."""
|
"""Make sure geometry isn't called when a geometry is passed."""
|
||||||
raw_elem = get_webelem()._elem
|
frame = stubs.FakeWebFrame(QRect(0, 0, 200, 200))
|
||||||
|
raw_elem = get_webelem(frame=frame)._elem
|
||||||
rect = QRect(10, 20, 30, 40)
|
rect = QRect(10, 20, 30, 40)
|
||||||
assert webelem.rect_on_view(raw_elem, rect) == rect
|
assert webelem.rect_on_view(raw_elem, elem_geometry=rect) == rect
|
||||||
assert not raw_elem.geometry.called
|
assert not raw_elem.geometry.called
|
||||||
|
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user