diff --git a/qutebrowser/browser/browsertab.py b/qutebrowser/browser/browsertab.py
index 5886d851e..996edc842 100644
--- a/qutebrowser/browser/browsertab.py
+++ b/qutebrowser/browser/browsertab.py
@@ -21,7 +21,7 @@
import itertools
-from PyQt5.QtCore import pyqtSignal, pyqtSlot, QUrl, QObject, QPoint
+from PyQt5.QtCore import pyqtSignal, pyqtSlot, QUrl, QObject, QPoint, QSizeF
from PyQt5.QtGui import QIcon
from PyQt5.QtWidgets import QWidget, QLayout
@@ -482,6 +482,7 @@ class AbstractTab(QWidget):
new_tab_requested = pyqtSignal(QUrl)
url_changed = pyqtSignal(QUrl)
shutting_down = pyqtSignal()
+ contents_size_changed = pyqtSignal(QSizeF)
def __init__(self, win_id, parent=None):
self.win_id = win_id
@@ -632,6 +633,15 @@ class AbstractTab(QWidget):
def set_html(self, html, base_url):
raise NotImplementedError
+ def find_all_elements(self, selector, *, only_visible=False):
+ """Find all HTML elements matching a given selector.
+
+ Args:
+ selector: The CSS selector to search for.
+ only_visible: Only show elements which are visible on screen.
+ """
+ raise NotImplementedError
+
def __repr__(self):
try:
url = utils.elide(self.url().toDisplayString(QUrl.EncodeUnicode),
diff --git a/qutebrowser/browser/commands.py b/qutebrowser/browser/commands.py
index c91cfca2b..3e5581ff3 100644
--- a/qutebrowser/browser/commands.py
+++ b/qutebrowser/browser/commands.py
@@ -503,21 +503,13 @@ class CommandDispatcher:
if widget.backend == usertypes.Backend.QtWebEngine:
raise cmdexc.CommandError(":navigate prev/next is not "
"supported yet with QtWebEngine")
- page = widget._widget.page() # pylint: disable=protected-access
- frame = page.currentFrame()
- if frame is None:
- raise cmdexc.CommandError("No frame focused!")
- else:
- frame = None
hintmanager = objreg.get('hintmanager', scope='tab', tab='current')
if where == 'prev':
- assert frame is not None
- hintmanager.follow_prevnext(frame, url, prev=True, tab=tab,
+ hintmanager.follow_prevnext(widget, url, prev=True, tab=tab,
background=bg, window=window)
elif where == 'next':
- assert frame is not None
- hintmanager.follow_prevnext(frame, url, prev=False, tab=tab,
+ hintmanager.follow_prevnext(widget, url, prev=False, tab=tab,
background=bg, window=window)
elif where == 'up':
self._navigate_up(url, tab, bg, window)
@@ -1441,10 +1433,7 @@ class CommandDispatcher:
raise cmdexc.CommandError("No element focused!")
if not elem.is_editable(strict=True):
raise cmdexc.CommandError("Focused element is not editable!")
- if elem.is_content_editable():
- text = str(elem)
- else:
- text = elem.evaluateJavaScript('this.value')
+ text = elem.text(use_js=True)
ed = editor.ExternalEditor(self._win_id, self._tabbed_browser)
ed.editing_finished.connect(functools.partial(
self.on_editing_finished, elem))
@@ -1460,15 +1449,7 @@ class CommandDispatcher:
text: The new text to insert.
"""
try:
- if elem.is_content_editable():
- log.misc.debug("Filling element {} via setPlainText.".format(
- elem.debug_text()))
- elem.setPlainText(text)
- else:
- log.misc.debug("Filling element {} via javascript.".format(
- elem.debug_text()))
- text = webelem.javascript_escape(text)
- elem.evaluateJavaScript("this.value='{}'".format(text))
+ elem.set_text(text, use_js=True)
except webelem.IsNullError:
raise cmdexc.CommandError("Element vanished while editing!")
@@ -1494,7 +1475,7 @@ class CommandDispatcher:
log.misc.debug("Pasting primary selection into element {}".format(
elem.debug_text()))
- elem.evaluateJavaScript("""
+ elem.run_js_async("""
var sel = '{}';
var event = document.createEvent('TextEvent');
event.initTextEvent('textInput', true, true, null, sel);
diff --git a/qutebrowser/browser/hints.py b/qutebrowser/browser/hints.py
index be2b1bcef..d59267c92 100644
--- a/qutebrowser/browser/hints.py
+++ b/qutebrowser/browser/hints.py
@@ -64,8 +64,6 @@ class HintContext:
Attributes:
frames: The QWebFrames to use.
- destroyed_frames: id()'s of QWebFrames which have been destroyed.
- (Workaround for https://github.com/The-Compiler/qutebrowser/issues/152)
all_elems: A list of all (elem, label) namedtuples ever created.
elems: A mapping from key strings to (elem, label) namedtuples.
May contain less elements than `all_elems` due to filtering.
@@ -82,7 +80,6 @@ class HintContext:
to_follow: The link to follow when enter is pressed.
args: Custom arguments for userscript/spawn
rapid: Whether to do rapid hinting.
- mainframe: The main QWebFrame where we started hinting in.
tab: The WebTab object we started hinting in.
group: The group of web elements to hint.
"""
@@ -95,9 +92,7 @@ class HintContext:
self.to_follow = None
self.rapid = False
self.frames = []
- self.destroyed_frames = []
self.args = []
- self.mainframe = None
self.tab = None
self.group = None
@@ -176,25 +171,9 @@ class HintManager(QObject):
"""Clean up after hinting."""
for elem in self._context.all_elems:
try:
- elem.label.removeFromDocument()
+ elem.label.remove_from_document()
except webelem.IsNullError:
pass
- for f in self._context.frames:
- log.hints.debug("Disconnecting frame {}".format(f))
- if id(f) in self._context.destroyed_frames:
- # WORKAROUND for
- # https://github.com/The-Compiler/qutebrowser/issues/152
- log.hints.debug("Frame has been destroyed, ignoring.")
- continue
- try:
- f.contentsSizeChanged.disconnect(self.on_contents_size_changed)
- except TypeError:
- # It seems we can get this here:
- # TypeError: disconnect() failed between
- # 'contentsSizeChanged' and 'on_contents_size_changed'
- # See # https://github.com/The-Compiler/qutebrowser/issues/263
- pass
- log.hints.debug("Disconnected.")
text = self._get_text()
message_bridge = objreg.get('message-bridge', scope='window',
window=self._win_id)
@@ -335,16 +314,16 @@ class HintManager(QObject):
def _is_hidden(self, elem):
"""Check if the element is hidden via display=none."""
- display = elem.styleProperty('display', QWebElement.InlineStyle)
+ display = elem.style_property('display', QWebElement.InlineStyle)
return display == 'none'
def _show_elem(self, elem):
"""Show a given element."""
- elem.setStyleProperty('display', 'inline !important')
+ elem.set_style_property('display', 'inline !important')
def _hide_elem(self, elem):
"""Hide a given element."""
- elem.setStyleProperty('display', 'none !important')
+ elem.set_style_property('display', 'none !important')
def _set_style_properties(self, elem, label):
"""Set the hint CSS on the element given.
@@ -373,7 +352,7 @@ class HintManager(QObject):
attrs.append(('text-transform', 'none !important'))
for k, v in attrs:
- label.setStyleProperty(k, v)
+ label.set_style_property(k, v)
self._set_style_position(elem, label)
def _set_style_position(self, elem, label):
@@ -389,8 +368,8 @@ class HintManager(QObject):
top = rect.y()
log.hints.vdebug("Drawing label '{!r}' at {}/{} for element '{!r}' "
"(no_js: {})".format(label, left, top, elem, no_js))
- label.setStyleProperty('left', '{}px !important'.format(left))
- label.setStyleProperty('top', '{}px !important'.format(top))
+ label.set_style_property('left', '{}px !important'.format(left))
+ label.set_style_property('top', '{}px !important'.format(top))
def _draw_label(self, elem, string):
"""Draw a hint label over an element.
@@ -402,22 +381,16 @@ class HintManager(QObject):
Return:
The newly created label element
"""
- doc = elem.webFrame().documentElement()
- # It seems impossible to create an empty QWebElement for which isNull()
- # is false so we can work with it.
- # As a workaround, we use appendInside() with markup as argument, and
- # then use lastChild() to get a reference to it.
- # See: http://stackoverflow.com/q/7364852/2085149
- body = doc.findFirst('body')
- if not body.isNull():
- parent = body
- else:
+ doc = elem.document_element()
+ body = doc.find_first('body')
+ if body is None:
parent = doc
- parent.appendInside('')
- label = webelem.WebElementWrapper(parent.lastChild())
+ else:
+ parent = body
+ label = parent.create_inside('span')
label['class'] = 'qutehint'
self._set_style_properties(elem, label)
- label.setPlainText(string)
+ label.set_text(string)
return label
def _show_url_error(self):
@@ -490,7 +463,7 @@ class HintManager(QObject):
self.mouse_event.emit(evt)
if elem.is_text_input() and elem.is_editable():
QTimer.singleShot(0, functools.partial(
- elem.webFrame().page().triggerAction,
+ elem.frame().page().triggerAction,
QWebPage.MoveToEndOfDocument))
QTimer.singleShot(0, self.stop_hinting.emit)
@@ -559,7 +532,7 @@ class HintManager(QObject):
download_manager = objreg.get('download-manager', scope='window',
window=self._win_id)
- download_manager.get(url, page=elem.webFrame().page(),
+ download_manager.get(url, page=elem.frame().page(),
prompt_download_directory=prompt)
def _call_userscript(self, elem, context):
@@ -574,7 +547,7 @@ class HintManager(QObject):
env = {
'QUTE_MODE': 'hints',
'QUTE_SELECTED_TEXT': str(elem),
- 'QUTE_SELECTED_HTML': elem.toOuterXml(),
+ 'QUTE_SELECTED_HTML': elem.outer_xml(),
}
url = self._resolve_url(elem, context.baseurl)
if url is not None:
@@ -623,13 +596,12 @@ class HintManager(QObject):
qtutils.ensure_valid(url)
return url
- def _find_prevnext(self, frame, prev=False):
+ def _find_prevnext(self, tab, prev=False):
"""Find a prev/next element in frame."""
# First check for
- elems = frame.findAllElements(webelem.SELECTORS[webelem.Group.links])
+ elems = tab.find_all_elements(webelem.SELECTORS[webelem.Group.links])
rel_values = ('prev', 'previous') if prev else ('next')
for e in elems:
- e = webelem.WebElementWrapper(e)
try:
rel_attr = e['rel']
except KeyError:
@@ -639,9 +611,8 @@ class HintManager(QObject):
e.debug_text(), rel_attr))
return e
# Then check for regular links/buttons.
- elems = frame.findAllElements(
+ elems = tab.find_all_elements(
webelem.SELECTORS[webelem.Group.prevnext])
- elems = [webelem.WebElementWrapper(e) for e in elems]
filterfunc = webelem.FILTERS[webelem.Group.prevnext]
elems = [e for e in elems if filterfunc(e)]
@@ -662,12 +633,6 @@ class HintManager(QObject):
log.hints.vdebug("No match on '{}'!".format(text))
return None
- def _connect_frame_signals(self):
- """Connect the contentsSizeChanged signals to all frames."""
- for f in self._context.frames:
- log.hints.debug("Connecting frame {}".format(f))
- f.contentsSizeChanged.connect(self.on_contents_size_changed)
-
def _check_args(self, target, *args):
"""Check the arguments passed to start() and raise if they're wrong.
@@ -690,14 +655,9 @@ class HintManager(QObject):
def _init_elements(self):
"""Initialize the elements and labels based on the context set."""
- elems = []
- for f in self._context.frames:
- elems += f.findAllElements(webelem.SELECTORS[self._context.group])
- elems = [e for e in elems
- if webelem.is_visible(e, self._context.mainframe)]
- # We wrap the elements late for performance reasons, as wrapping 1000s
- # of elements (with ~50 methods each) just takes too much time...
- elems = [webelem.WebElementWrapper(e) for e in elems]
+ selector = webelem.SELECTORS[self._context.group]
+ elems = self._context.tab.find_all_elements(selector,
+ only_visible=True)
filterfunc = webelem.FILTERS.get(self._context.group, lambda e: True)
elems = [e for e in elems if filterfunc(e)]
if not elems:
@@ -724,12 +684,12 @@ class HintManager(QObject):
# Do multi-word matching
return all(word in elemstr for word in filterstr.split())
- def follow_prevnext(self, frame, baseurl, prev=False, tab=False,
+ def follow_prevnext(self, browsertab, baseurl, prev=False, tab=False,
background=False, window=False):
"""Click a "previous"/"next" element on the page.
Args:
- frame: The frame where the element is in.
+ browsertab: The WebKitTab/WebEngineTab of the page.
baseurl: The base URL of the current tab.
prev: True to open a "previous" link, False to open a "next" link.
tab: True to open in a new tab, False for the current tab.
@@ -737,7 +697,7 @@ class HintManager(QObject):
window: True to open in a new window, False for the current one.
"""
from qutebrowser.mainwindow import mainwindow
- elem = self._find_prevnext(frame, prev)
+ elem = self._find_prevnext(browsertab, prev)
if elem is None:
raise cmdexc.CommandError("No {} links found!".format(
"prev" if prev else "forward"))
@@ -820,11 +780,6 @@ class HintManager(QObject):
tab = tabbed_browser.currentWidget()
if tab is None:
raise cmdexc.CommandError("No WebView available yet!")
- # FIXME:qtwebengine have a proper API for this
- page = tab._widget.page() # pylint: disable=protected-access
- mainframe = page.mainFrame()
- if mainframe is None:
- raise cmdexc.CommandError("No frame focused!")
mode_manager = objreg.get('mode-manager', scope='window',
window=self._win_id)
if mode_manager.mode == usertypes.KeyMode.hint:
@@ -852,20 +807,13 @@ class HintManager(QObject):
self._context.baseurl = tabbed_browser.current_url()
except qtutils.QtValueError:
raise cmdexc.CommandError("No URL set for this page yet!")
- self._context.frames = webelem.get_child_frames(mainframe)
- for frame in self._context.frames:
- # WORKAROUND for
- # https://github.com/The-Compiler/qutebrowser/issues/152
- frame.destroyed.connect(functools.partial(
- self._context.destroyed_frames.append, id(frame)))
+ self._context.tab = tab
self._context.args = args
- self._context.mainframe = mainframe
self._context.group = group
self._init_elements()
message_bridge = objreg.get('message-bridge', scope='window',
window=self._win_id)
message_bridge.set_text(self._get_text())
- self._connect_frame_signals()
modeman.enter(self._win_id, usertypes.KeyMode.hint,
'HintManager.start')
@@ -878,7 +826,7 @@ class HintManager(QObject):
matched = string[:len(keystr)]
rest = string[len(keystr):]
match_color = config.get('colors', 'hints.fg.match')
- elem.label.setInnerXml(
+ elem.label.set_inner_xml(
'{}{}'.format(
match_color, matched, rest))
if self._is_hidden(elem.label):
@@ -913,7 +861,7 @@ class HintManager(QObject):
strings = self._hint_strings(elems)
self._context.elems = {}
for elem, string in zip(elems, strings):
- elem.label.setInnerXml(string)
+ elem.label.set_inner_xml(string)
self._context.elems[string] = elem
keyparsers = objreg.get('keyparsers', scope='window',
window=self._win_id)
@@ -1017,7 +965,7 @@ class HintManager(QObject):
Target.spawn: self._spawn,
}
elem = self._context.elems[keystr].elem
- if elem.webFrame() is None:
+ if elem.frame() is None:
message.error(self._win_id,
"This element has no webframe.",
immediately=True)
@@ -1042,7 +990,7 @@ class HintManager(QObject):
self.filter_hints(None)
# Undo keystring highlighting
for string, elem in self._context.elems.items():
- elem.label.setInnerXml(string)
+ elem.label.set_inner_xml(string)
handler()
@cmdutils.register(instance='hintmanager', scope='tab', hide=True,
@@ -1068,9 +1016,9 @@ class HintManager(QObject):
log.hints.debug("Contents size changed...!")
for e in self._context.all_elems:
try:
- if e.elem.webFrame() is None:
+ if e.elem.frame() is None:
# This sometimes happens for some reason...
- e.label.removeFromDocument()
+ e.label.remove_from_document()
continue
self._set_style_position(e.elem, e.label)
except webelem.IsNullError:
@@ -1146,7 +1094,7 @@ class WordHinter:
})
return (attr_extractors[attr](elem)
- for attr in extractable_attrs[elem.tagName()]
+ for attr in extractable_attrs[elem.tag_name()]
if attr in elem or attr == "text")
def tag_words_to_hints(self, words):
diff --git a/qutebrowser/browser/webengine/webenginetab.py b/qutebrowser/browser/webengine/webenginetab.py
index 96c7e562e..4e0a82066 100644
--- a/qutebrowser/browser/webengine/webenginetab.py
+++ b/qutebrowser/browser/webengine/webenginetab.py
@@ -410,6 +410,10 @@ class WebEngineTab(browsertab.AbstractTab):
def clear_ssl_errors(self):
log.stub()
+ def find_all_elements(self, selector, *, only_visible=False):
+ log.stub()
+ return []
+
def _connect_signals(self):
view = self._widget
page = view.page()
@@ -425,3 +429,7 @@ class WebEngineTab(browsertab.AbstractTab):
view.iconChanged.connect(self.icon_changed)
except AttributeError:
log.stub('iconChanged, on Qt < 5.7')
+ try:
+ page.contentsSizeChanged.connect(self.contents_size_changed)
+ except AttributeError:
+ log.stub('contentsSizeChanged, on Qt < 5.7')
diff --git a/qutebrowser/browser/webkit/webelem.py b/qutebrowser/browser/webkit/webelem.py
index 97210bebd..5de299f77 100644
--- a/qutebrowser/browser/webkit/webelem.py
+++ b/qutebrowser/browser/webkit/webelem.py
@@ -28,7 +28,6 @@ Module attributes:
"""
import collections.abc
-import functools
from PyQt5.QtCore import QRect, QUrl
from PyQt5.QtWebKit import QWebElement
@@ -83,40 +82,6 @@ class WebElementWrapper(collections.abc.MutableMapping):
if elem.isNull():
raise IsNullError('{} is a null element!'.format(elem))
self._elem = elem
- for name in ['addClass', 'appendInside', 'appendOutside',
- 'attributeNS', 'classes', 'clone', 'document',
- 'encloseContentsWith', 'encloseWith',
- 'evaluateJavaScript', 'findAll', 'findFirst',
- 'firstChild', 'geometry', 'hasAttributeNS',
- 'hasAttributes', 'hasClass', 'hasFocus', 'lastChild',
- 'localName', 'namespaceUri', 'nextSibling', 'parent',
- 'prefix', 'prependInside', 'prependOutside',
- 'previousSibling', 'removeAllChildren',
- 'removeAttributeNS', 'removeClass', 'removeFromDocument',
- 'render', 'replace', 'setAttributeNS', 'setFocus',
- 'setInnerXml', 'setOuterXml', 'setPlainText',
- 'setStyleProperty', 'styleProperty', 'tagName',
- 'takeFromDocument', 'toInnerXml', 'toOuterXml',
- 'toggleClass', 'webFrame', '__eq__', '__ne__']:
- # We don't wrap some methods for which we have better alternatives:
- # - Mapping access for attributeNames/hasAttribute/setAttribute/
- # attribute/removeAttribute.
- # - isNull is checked automagically.
- # - str(...) instead of toPlainText
- # For the rest, we create a wrapper which checks if the element is
- # null.
-
- method = getattr(self._elem, name)
-
- def _wrapper(meth, *args, **kwargs):
- self._check_vanished()
- return meth(*args, **kwargs)
-
- wrapper = functools.partial(_wrapper, method)
- # We used to do functools.update_wrapper here, but for some reason
- # when using hints with many links, this accounted for nearly 50%
- # of the time when profiling, which is unacceptable.
- setattr(self, name, wrapper)
def __str__(self):
self._check_vanished()
@@ -162,20 +127,92 @@ class WebElementWrapper(collections.abc.MutableMapping):
if self._elem.isNull():
raise IsNullError('Element {} vanished!'.format(self._elem))
- def is_visible(self, mainframe):
- """Check whether the element is currently visible on the screen.
+ def frame(self):
+ """Get the main frame of this element."""
+ # FIXME:qtwebengine how to get rid of this?
+ self._check_vanished()
+ return self._elem.webFrame()
+
+ def geometry(self):
+ """Get the geometry for this element."""
+ self._check_vanished()
+ return self._elem.geometry()
+
+ def document_element(self):
+ """Get the document element of this element."""
+ self._check_vanished()
+ elem = self._elem.webFrame().documentElement()
+ return WebElementWrapper(elem)
+
+ def create_inside(self, tagname):
+ """Append the given element inside the current one."""
+ # It seems impossible to create an empty QWebElement for which isNull()
+ # is false so we can work with it.
+ # As a workaround, we use appendInside() with markup as argument, and
+ # then use lastChild() to get a reference to it.
+ # See: http://stackoverflow.com/q/7364852/2085149
+ self._check_vanished()
+ self._elem.appendInside('<{}>{}>'.format(tagname, tagname))
+ return WebElementWrapper(self._elem.lastChild())
+
+ def find_first(self, selector):
+ """Find the first child based on the given CSS selector."""
+ self._check_vanished()
+ elem = self._elem.findFirst(selector)
+ if elem.isNull():
+ return None
+ return WebElementWrapper(elem)
+
+ def style_property(self, name, strategy):
+ """Get the element style resolved with the given strategy."""
+ self._check_vanished()
+ return self._elem.styleProperty(name, strategy)
+
+ def text(self, *, use_js=False):
+ """Get the plain text content for this element.
Args:
- mainframe: The main QWebFrame.
-
- Return:
- True if the element is visible, False otherwise.
+ use_js: Whether to use javascript if the element isn't
+ content-editable.
"""
- return is_visible(self._elem, mainframe)
+ self._check_vanished()
+ if self.is_content_editable() or not use_js:
+ return self._elem.toPlainText()
+ else:
+ return self._elem.evaluateJavaScript('this.value')
- def rect_on_view(self, **kwargs):
- """Get the geometry of the element relative to the webview."""
- return rect_on_view(self._elem, **kwargs)
+ def set_text(self, text, *, use_js=False):
+ """Set the given plain text.
+
+ Args:
+ use_js: Whether to use javascript if the element isn't
+ content-editable.
+ """
+ self._check_vanished()
+ if self.is_content_editable() or not use_js:
+ log.misc.debug("Filling element {} via set_text.".format(
+ self.debug_text()))
+ self._elem.setPlainText(text)
+ else:
+ log.misc.debug("Filling element {} via javascript.".format(
+ self.debug_text()))
+ text = javascript_escape(text)
+ self._elem.evaluateJavaScript("this.value='{}'".format(text))
+
+ def set_inner_xml(self, xml):
+ """Set the given inner XML."""
+ self._check_vanished()
+ self._elem.setInnerXml(xml)
+
+ def remove_from_document(self):
+ """Remove the node from the document."""
+ self._check_vanished()
+ self._elem.removeFromDocument()
+
+ def set_style_property(self, name, value):
+ """Set the element style."""
+ self._check_vanished()
+ return self._elem.setStyleProperty(name, value)
def is_writable(self):
"""Check whether an element is writable."""
@@ -304,6 +341,141 @@ class WebElementWrapper(collections.abc.MutableMapping):
self._check_vanished()
return utils.compact_text(self._elem.toOuterXml(), 500)
+ def outer_xml(self):
+ """Get the full HTML representation of this element."""
+ self._check_vanished()
+ return self._elem.toOuterXml()
+
+ def tag_name(self):
+ """Get the tag name for the current element."""
+ self._check_vanished()
+ return self._elem.tagName()
+
+ def run_js_async(self, code, callback=None):
+ """Run the given JS snippet async on the element."""
+ self._check_vanished()
+ result = self._elem.evaluateJavaScript(code)
+ if callback is not None:
+ callback(result)
+
+ def rect_on_view(self, *, elem_geometry=None, adjust_zoom=True,
+ no_js=False):
+ """Get the geometry of the element relative to the webview.
+
+ 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 elements containing other
+ elements with "display:block" style, see
+ https://github.com/The-Compiler/qutebrowser/issues/1298
+
+ Args:
+ elem_geometry: The geometry of the element, or None.
+ Calling QWebElement::geometry is rather expensive so
+ we want to avoid doing it twice.
+ adjust_zoom: Whether to adjust the element position based on the
+ current zoom level.
+ no_js: Fall back to the Python implementation
+ """
+ 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:
+ rects = self._elem.evaluateJavaScript("this.getClientRects()")
+ text = utils.compact_text(self._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 = self._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 = self._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:
+ geometry = self._elem.geometry()
+ else:
+ geometry = elem_geometry
+ frame = self._elem.webFrame()
+ rect = QRect(geometry)
+ while frame is not None:
+ rect.translate(frame.geometry().topLeft())
+ rect.translate(frame.scrollPosition() * -1)
+ frame = frame.parentFrame()
+ # We deliberately always adjust the zoom here, even with
+ # adjust_zoom=False
+ if elem_geometry is None:
+ zoom = self._elem.webFrame().zoomFactor()
+ if not config.get('ui', 'zoom-text-only'):
+ rect.moveTo(rect.left() / zoom, rect.top() / zoom)
+ rect.setWidth(rect.width() / zoom)
+ rect.setHeight(rect.height() / zoom)
+ return rect
+
+ def is_visible(self, mainframe):
+ """Check if the given element is visible in the given frame."""
+ self._check_vanished()
+ # CSS attributes which hide an element
+ hidden_attributes = {
+ 'visibility': 'hidden',
+ 'display': 'none',
+ }
+ for k, v in hidden_attributes.items():
+ if self._elem.styleProperty(k, QWebElement.ComputedStyle) == v:
+ return False
+ elem_geometry = self._elem.geometry()
+ if not elem_geometry.isValid() and elem_geometry.x() == 0:
+ # 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)
+ mainframe_geometry = mainframe.geometry()
+ if elem_rect.isValid():
+ visible_on_screen = mainframe_geometry.intersects(elem_rect)
+ else:
+ # We got an invalid rectangle (width/height 0/0 probably), but this
+ # can still be a valid link.
+ visible_on_screen = mainframe_geometry.contains(
+ elem_rect.topLeft())
+ # Then check if it's visible in its frame if it's not in the main
+ # frame.
+ elem_frame = self._elem.webFrame()
+ framegeom = QRect(elem_frame.geometry())
+ if not framegeom.isValid():
+ visible_in_frame = False
+ elif elem_frame.parentFrame() is not None:
+ framegeom.moveTo(0, 0)
+ framegeom.translate(elem_frame.scrollPosition())
+ if elem_geometry.isValid():
+ visible_in_frame = framegeom.intersects(elem_geometry)
+ else:
+ # We got an invalid rectangle (width/height 0/0 probably), but
+ # this can still be a valid link.
+ visible_in_frame = framegeom.contains(elem_geometry.topLeft())
+ else:
+ visible_in_frame = visible_on_screen
+ return all([visible_on_screen, visible_in_frame])
+
+
+
def javascript_escape(text):
"""Escape values special to javascript in strings.
@@ -361,134 +533,3 @@ def focus_elem(frame):
"""
elem = frame.findFirstElement(SELECTORS[Group.focus])
return WebElementWrapper(elem)
-
-
-def rect_on_view(elem, *, elem_geometry=None, adjust_zoom=True, no_js=False):
- """Get the geometry of the element relative to the webview.
-
- 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
- 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 elements containing other
- elements with "display:block" style, see
- https://github.com/The-Compiler/qutebrowser/issues/1298
-
- Args:
- elem: The QWebElement to get the rect for.
- elem_geometry: The geometry of the element, or None.
- Calling QWebElement::geometry is rather expensive so we
- want to avoid doing it twice.
- adjust_zoom: Whether to adjust the element position based on the
- current zoom level.
- no_js: Fall back to the Python implementation
- """
- if elem.isNull():
- 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 and not no_js:
- 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:
- geometry = elem.geometry()
- else:
- geometry = elem_geometry
- frame = elem.webFrame()
- rect = QRect(geometry)
- while frame is not None:
- rect.translate(frame.geometry().topLeft())
- rect.translate(frame.scrollPosition() * -1)
- frame = frame.parentFrame()
- # We deliberately always adjust the zoom here, even with adjust_zoom=False
- if elem_geometry is None:
- zoom = elem.webFrame().zoomFactor()
- if not config.get('ui', 'zoom-text-only'):
- rect.moveTo(rect.left() / zoom, rect.top() / zoom)
- rect.setWidth(rect.width() / zoom)
- rect.setHeight(rect.height() / zoom)
- return rect
-
-
-def is_visible(elem, mainframe):
- """Check if the given element is visible in the frame.
-
- We need this as a standalone function (as opposed to a WebElementWrapper
- method) because we want to check this before wrapping when hinting for
- performance reasons.
-
- Args:
- elem: The QWebElement to check.
- mainframe: The QWebFrame in which the element should be visible.
- """
- if elem.isNull():
- raise IsNullError("Got called on a null element!")
- # CSS attributes which hide an element
- hidden_attributes = {
- 'visibility': 'hidden',
- 'display': 'none',
- }
- for k, v in hidden_attributes.items():
- if elem.styleProperty(k, QWebElement.ComputedStyle) == v:
- return False
- elem_geometry = elem.geometry()
- if not elem_geometry.isValid() and elem_geometry.x() == 0:
- # Most likely an invisible link
- return False
- # First check if the element is visible on screen
- elem_rect = rect_on_view(elem, elem_geometry=elem_geometry)
- mainframe_geometry = mainframe.geometry()
- if elem_rect.isValid():
- visible_on_screen = mainframe_geometry.intersects(elem_rect)
- else:
- # We got an invalid rectangle (width/height 0/0 probably), but this
- # can still be a valid link.
- visible_on_screen = mainframe_geometry.contains(elem_rect.topLeft())
- # Then check if it's visible in its frame if it's not in the main
- # frame.
- elem_frame = elem.webFrame()
- framegeom = QRect(elem_frame.geometry())
- if not framegeom.isValid():
- visible_in_frame = False
- elif elem_frame.parentFrame() is not None:
- framegeom.moveTo(0, 0)
- framegeom.translate(elem_frame.scrollPosition())
- if elem_geometry.isValid():
- visible_in_frame = framegeom.intersects(elem_geometry)
- else:
- # We got an invalid rectangle (width/height 0/0 probably), but
- # this can still be a valid link.
- visible_in_frame = framegeom.contains(elem_geometry.topLeft())
- else:
- visible_in_frame = visible_on_screen
- return all([visible_on_screen, visible_in_frame])
diff --git a/qutebrowser/browser/webkit/webkittab.py b/qutebrowser/browser/webkit/webkittab.py
index 899a46573..15c670567 100644
--- a/qutebrowser/browser/webkit/webkittab.py
+++ b/qutebrowser/browser/webkit/webkittab.py
@@ -25,12 +25,12 @@ import xml.etree.ElementTree
from PyQt5.QtCore import pyqtSlot, Qt, QEvent, QUrl, QPoint, QTimer
from PyQt5.QtGui import QKeyEvent
-from PyQt5.QtWebKitWidgets import QWebPage
+from PyQt5.QtWebKitWidgets import QWebPage, QWebFrame
from PyQt5.QtWebKit import QWebSettings
from PyQt5.QtPrintSupport import QPrinter
from qutebrowser.browser import browsertab
-from qutebrowser.browser.webkit import webview, tabhistory
+from qutebrowser.browser.webkit import webview, tabhistory, webelem
from qutebrowser.utils import qtutils, objreg, usertypes, utils
@@ -557,6 +557,22 @@ class WebKitTab(browsertab.AbstractTab):
def set_html(self, html, base_url):
self._widget.setHtml(html, base_url)
+ def find_all_elements(self, selector, *, only_visible=False):
+ mainframe = self._widget.page().mainFrame()
+ if mainframe is None:
+ raise browsertab.WebTabError("No frame focused!")
+
+ elems = []
+ frames = webelem.get_child_frames(mainframe)
+ for f in frames:
+ for elem in f.findAllElements(selector):
+ elems.append(webelem.WebElementWrapper(elem))
+
+ if only_visible:
+ elems = [e for e in elems if e.is_visible(mainframe)]
+
+ return elems
+
@pyqtSlot()
def _on_frame_load_finished(self):
"""Make sure we emit an appropriate status when loading finished.
@@ -572,6 +588,14 @@ class WebKitTab(browsertab.AbstractTab):
"""Emit iconChanged with a QIcon like QWebEngineView does."""
self.icon_changed.emit(self._widget.icon())
+ @pyqtSlot(QWebFrame)
+ def _on_frame_created(self, frame):
+ """Connect the contentsSizeChanged signal of each frame."""
+ # FIXME:qtwebengine those could theoretically regress:
+ # https://github.com/The-Compiler/qutebrowser/issues/152
+ # https://github.com/The-Compiler/qutebrowser/issues/263
+ frame.contentsSizeChanged.connect(self.contents_size_changed)
+
def _connect_signals(self):
view = self._widget
page = view.page()
diff --git a/tests/unit/browser/webkit/test_webelem.py b/tests/unit/browser/webkit/test_webelem.py
index b87a2f529..aaeb3c42e 100644
--- a/tests/unit/browser/webkit/test_webelem.py
+++ b/tests/unit/browser/webkit/test_webelem.py
@@ -402,7 +402,7 @@ class TestRemoveBlankTarget:
elem = [None] * depth
elem[0] = get_webelem(tagname='div')
for i in range(1, depth):
- elem[i] = get_webelem(tagname='div', parent=elem[i-1])
+ elem[i] = get_webelem(tagname='div', parent=elem[i-1]._elem)
elem[i]._elem.encloseWith(elem[i-1]._elem)
elem[-1].remove_blank_target()
for i in range(depth):
@@ -672,10 +672,10 @@ class TestRectOnView:
def test_passed_geometry(self, stubs, js_rect):
"""Make sure geometry isn't called when a geometry is passed."""
frame = stubs.FakeWebFrame(QRect(0, 0, 200, 200))
- raw_elem = get_webelem(frame=frame, js_rect_return=js_rect)._elem
+ elem = get_webelem(frame=frame, js_rect_return=js_rect)
rect = QRect(10, 20, 30, 40)
- assert webelem.rect_on_view(raw_elem, elem_geometry=rect) == rect
- assert not raw_elem.geometry.called
+ assert elem.rect_on_view(elem_geometry=rect) == rect
+ assert not elem._elem.geometry.called
@pytest.mark.parametrize('js_rect', [None, {}])
@pytest.mark.parametrize('zoom_text_only', [True, False])