Merge branch 'webelem'
This commit is contained in:
commit
a1fd161a4a
@ -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),
|
||||
|
@ -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);
|
||||
|
@ -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('<span></span>')
|
||||
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 <link rel="prev(ious)|next">
|
||||
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(
|
||||
'<font color="{}">{}</font>{}'.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):
|
||||
|
@ -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')
|
||||
|
@ -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 <a> 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 <a> 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])
|
||||
|
@ -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()
|
||||
|
@ -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])
|
||||
|
Loading…
Reference in New Issue
Block a user