Refactor WebElement API, part 2

Now we don't get a crash, but not any hints either...
This commit is contained in:
Florian Bruhin 2016-07-27 16:27:38 +02:00
parent c5a91004f5
commit 193c755637
5 changed files with 43 additions and 54 deletions

View File

@ -633,6 +633,10 @@ class AbstractTab(QWidget):
def set_html(self, html, base_url): def set_html(self, html, base_url):
raise NotImplementedError raise NotImplementedError
def find_all_elements(self, selector):
"""Find all HTML elements matching a given selector."""
raise NotImplementedError
def __repr__(self): def __repr__(self):
try: try:
url = utils.elide(self.url().toDisplayString(QUrl.EncodeUnicode), url = utils.elide(self.url().toDisplayString(QUrl.EncodeUnicode),

View File

@ -503,21 +503,13 @@ class CommandDispatcher:
if widget.backend == usertypes.Backend.QtWebEngine: if widget.backend == usertypes.Backend.QtWebEngine:
raise cmdexc.CommandError(":navigate prev/next is not " raise cmdexc.CommandError(":navigate prev/next is not "
"supported yet with QtWebEngine") "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') hintmanager = objreg.get('hintmanager', scope='tab', tab='current')
if where == 'prev': if where == 'prev':
assert frame is not None hintmanager.follow_prevnext(widget, url, prev=True, tab=tab,
hintmanager.follow_prevnext(frame, url, prev=True, tab=tab,
background=bg, window=window) background=bg, window=window)
elif where == 'next': elif where == 'next':
assert frame is not None hintmanager.follow_prevnext(widget, url, prev=False, tab=tab,
hintmanager.follow_prevnext(frame, url, prev=False, tab=tab,
background=bg, window=window) background=bg, window=window)
elif where == 'up': elif where == 'up':
self._navigate_up(url, tab, bg, window) self._navigate_up(url, tab, bg, window)

View File

@ -80,7 +80,6 @@ class HintContext:
to_follow: The link to follow when enter is pressed. to_follow: The link to follow when enter is pressed.
args: Custom arguments for userscript/spawn args: Custom arguments for userscript/spawn
rapid: Whether to do rapid hinting. rapid: Whether to do rapid hinting.
mainframe: The main QWebFrame where we started hinting in.
tab: The WebTab object we started hinting in. tab: The WebTab object we started hinting in.
group: The group of web elements to hint. group: The group of web elements to hint.
""" """
@ -94,7 +93,6 @@ class HintContext:
self.rapid = False self.rapid = False
self.frames = [] self.frames = []
self.args = [] self.args = []
self.mainframe = None
self.tab = None self.tab = None
self.group = None self.group = None
@ -173,7 +171,7 @@ class HintManager(QObject):
"""Clean up after hinting.""" """Clean up after hinting."""
for elem in self._context.all_elems: for elem in self._context.all_elems:
try: try:
elem.label.removeFromDocument() elem.label.remove_from_document()
except webelem.IsNullError: except webelem.IsNullError:
pass pass
text = self._get_text() text = self._get_text()
@ -384,7 +382,7 @@ class HintManager(QObject):
The newly created label element The newly created label element
""" """
doc = elem.document_element() doc = elem.document_element()
body = doc.findFirst('body') body = doc.find_first('body')
if body is None: if body is None:
parent = doc parent = doc
else: else:
@ -598,13 +596,12 @@ class HintManager(QObject):
qtutils.ensure_valid(url) qtutils.ensure_valid(url)
return url return url
def _find_prevnext(self, frame, prev=False): def _find_prevnext(self, tab, prev=False):
"""Find a prev/next element in frame.""" """Find a prev/next element in frame."""
# First check for <link rel="prev(ious)|next"> # 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') rel_values = ('prev', 'previous') if prev else ('next')
for e in elems: for e in elems:
e = webelem.WebElementWrapper(e)
try: try:
rel_attr = e['rel'] rel_attr = e['rel']
except KeyError: except KeyError:
@ -614,9 +611,8 @@ class HintManager(QObject):
e.debug_text(), rel_attr)) e.debug_text(), rel_attr))
return e return e
# Then check for regular links/buttons. # Then check for regular links/buttons.
elems = frame.findAllElements( elems = tab.find_all_elements(
webelem.SELECTORS[webelem.Group.prevnext]) webelem.SELECTORS[webelem.Group.prevnext])
elems = [webelem.WebElementWrapper(e) for e in elems]
filterfunc = webelem.FILTERS[webelem.Group.prevnext] filterfunc = webelem.FILTERS[webelem.Group.prevnext]
elems = [e for e in elems if filterfunc(e)] elems = [e for e in elems if filterfunc(e)]
@ -659,14 +655,9 @@ class HintManager(QObject):
def _init_elements(self): def _init_elements(self):
"""Initialize the elements and labels based on the context set.""" """Initialize the elements and labels based on the context set."""
elems = [] selector = webelem.SELECTORS[self._context.group]
for f in self._context.frames: elems = self._context.tab.find_all_elements(selector)
elems += f.findAllElements(webelem.SELECTORS[self._context.group]) elems = [e for e in elems if e.is_visible()]
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]
filterfunc = webelem.FILTERS.get(self._context.group, lambda e: True) filterfunc = webelem.FILTERS.get(self._context.group, lambda e: True)
elems = [e for e in elems if filterfunc(e)] elems = [e for e in elems if filterfunc(e)]
if not elems: if not elems:
@ -693,12 +684,12 @@ class HintManager(QObject):
# Do multi-word matching # Do multi-word matching
return all(word in elemstr for word in filterstr.split()) 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): background=False, window=False):
"""Click a "previous"/"next" element on the page. """Click a "previous"/"next" element on the page.
Args: Args:
frame: The frame where the element is in. browsertab: The WebKitTab/WebEngineTab of the page.
baseurl: The base URL of the current tab. baseurl: The base URL of the current tab.
prev: True to open a "previous" link, False to open a "next" link. 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. tab: True to open in a new tab, False for the current tab.
@ -706,7 +697,7 @@ class HintManager(QObject):
window: True to open in a new window, False for the current one. window: True to open in a new window, False for the current one.
""" """
from qutebrowser.mainwindow import mainwindow from qutebrowser.mainwindow import mainwindow
elem = self._find_prevnext(frame, prev) elem = self._find_prevnext(browsertab, prev)
if elem is None: if elem is None:
raise cmdexc.CommandError("No {} links found!".format( raise cmdexc.CommandError("No {} links found!".format(
"prev" if prev else "forward")) "prev" if prev else "forward"))
@ -789,11 +780,6 @@ class HintManager(QObject):
tab = tabbed_browser.currentWidget() tab = tabbed_browser.currentWidget()
if tab is None: if tab is None:
raise cmdexc.CommandError("No WebView available yet!") 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', mode_manager = objreg.get('mode-manager', scope='window',
window=self._win_id) window=self._win_id)
if mode_manager.mode == usertypes.KeyMode.hint: if mode_manager.mode == usertypes.KeyMode.hint:
@ -821,15 +807,13 @@ class HintManager(QObject):
self._context.baseurl = tabbed_browser.current_url() self._context.baseurl = tabbed_browser.current_url()
except qtutils.QtValueError: except qtutils.QtValueError:
raise cmdexc.CommandError("No URL set for this page yet!") raise cmdexc.CommandError("No URL set for this page yet!")
self._context.frames = webelem.get_child_frames(mainframe) self._context.tab = tab
self._context.args = args self._context.args = args
self._context.mainframe = mainframe
self._context.group = group self._context.group = group
self._init_elements() self._init_elements()
message_bridge = objreg.get('message-bridge', scope='window', message_bridge = objreg.get('message-bridge', scope='window',
window=self._win_id) window=self._win_id)
message_bridge.set_text(self._get_text()) message_bridge.set_text(self._get_text())
self._connect_frame_signals()
modeman.enter(self._win_id, usertypes.KeyMode.hint, modeman.enter(self._win_id, usertypes.KeyMode.hint,
'HintManager.start') 'HintManager.start')
@ -1034,7 +1018,7 @@ class HintManager(QObject):
try: try:
if e.elem.frame() is None: if e.elem.frame() is None:
# This sometimes happens for some reason... # This sometimes happens for some reason...
e.label.removeFromDocument() e.label.remove_from_document()
continue continue
self._set_style_position(e.elem, e.label) self._set_style_position(e.elem, e.label)
except webelem.IsNullError: except webelem.IsNullError:

View File

@ -184,9 +184,9 @@ class WebElementWrapper(collections.abc.MutableMapping):
def set_inner_xml(self, xml): def set_inner_xml(self, xml):
"""Set the given inner XML.""" """Set the given inner XML."""
self._check_vanished() self._check_vanished()
self._elem.setInnerXml(text) self._elem.setInnerXml(xml)
def remove(self): def remove_from_document(self):
"""Remove the node from the document.""" """Remove the node from the document."""
self._check_vanished() self._check_vanished()
self._elem.removeFromDocument() self._elem.removeFromDocument()
@ -196,16 +196,13 @@ class WebElementWrapper(collections.abc.MutableMapping):
self._check_vanished() self._check_vanished()
return self._elem.setStyleProperty(name, value) return self._elem.setStyleProperty(name, value)
def is_visible(self, mainframe): def is_visible(self):
"""Check whether the element is currently visible on the screen. """Check whether the element is currently visible on the screen.
Args:
mainframe: The main QWebFrame.
Return: Return:
True if the element is visible, False otherwise. True if the element is visible, False otherwise.
""" """
return is_visible(self._elem, mainframe) return is_visible(self._elem)
def rect_on_view(self, **kwargs): def rect_on_view(self, **kwargs):
"""Get the geometry of the element relative to the webview.""" """Get the geometry of the element relative to the webview."""
@ -438,14 +435,14 @@ def rect_on_view(elem, *, elem_geometry=None, adjust_zoom=True, no_js=False):
height = rect.get("height", 0) height = rect.get("height", 0)
if width > 1 and height > 1: if width > 1 and height > 1:
# fix coordinates according to zoom level # fix coordinates according to zoom level
zoom = elem.frame().zoomFactor() zoom = elem.webFrame().zoomFactor()
if not config.get('ui', 'zoom-text-only') and adjust_zoom: if not config.get('ui', 'zoom-text-only') and adjust_zoom:
rect["left"] *= zoom rect["left"] *= zoom
rect["top"] *= zoom rect["top"] *= zoom
width *= zoom width *= zoom
height *= zoom height *= zoom
rect = QRect(rect["left"], rect["top"], width, height) rect = QRect(rect["left"], rect["top"], width, height)
frame = elem.frame() frame = elem.webFrame()
while frame is not None: while frame is not None:
# Translate to parent frames' position # Translate to parent frames' position
# (scroll position is taken care of inside getClientRects) # (scroll position is taken care of inside getClientRects)
@ -458,7 +455,7 @@ def rect_on_view(elem, *, elem_geometry=None, adjust_zoom=True, no_js=False):
geometry = elem.geometry() geometry = elem.geometry()
else: else:
geometry = elem_geometry geometry = elem_geometry
frame = elem.frame() frame = elem.webFrame()
rect = QRect(geometry) rect = QRect(geometry)
while frame is not None: while frame is not None:
rect.translate(frame.geometry().topLeft()) rect.translate(frame.geometry().topLeft())
@ -466,7 +463,7 @@ def rect_on_view(elem, *, elem_geometry=None, adjust_zoom=True, no_js=False):
frame = frame.parentFrame() frame = frame.parentFrame()
# We deliberately always adjust the zoom here, even with adjust_zoom=False # We deliberately always adjust the zoom here, even with adjust_zoom=False
if elem_geometry is None: if elem_geometry is None:
zoom = elem.frame().zoomFactor() zoom = elem.webFrame().zoomFactor()
if not config.get('ui', 'zoom-text-only'): if not config.get('ui', 'zoom-text-only'):
rect.moveTo(rect.left() / zoom, rect.top() / zoom) rect.moveTo(rect.left() / zoom, rect.top() / zoom)
rect.setWidth(rect.width() / zoom) rect.setWidth(rect.width() / zoom)
@ -474,7 +471,7 @@ def rect_on_view(elem, *, elem_geometry=None, adjust_zoom=True, no_js=False):
return rect return rect
def is_visible(elem, mainframe): def is_visible(elem):
"""Check if the given element is visible in the frame. """Check if the given element is visible in the frame.
We need this as a standalone function (as opposed to a WebElementWrapper We need this as a standalone function (as opposed to a WebElementWrapper
@ -483,10 +480,10 @@ def is_visible(elem, mainframe):
Args: Args:
elem: The QWebElement to check. elem: The QWebElement to check.
mainframe: The QWebFrame in which the element should be visible.
""" """
if elem.isNull(): if elem.isNull():
raise IsNullError("Got called on a null element!") raise IsNullError("Got called on a null element!")
mainframe = elem.webFrame()
# CSS attributes which hide an element # CSS attributes which hide an element
hidden_attributes = { hidden_attributes = {
'visibility': 'hidden', 'visibility': 'hidden',
@ -510,7 +507,7 @@ def is_visible(elem, mainframe):
visible_on_screen = mainframe_geometry.contains(elem_rect.topLeft()) 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 # Then check if it's visible in its frame if it's not in the main
# frame. # frame.
elem_frame = elem.frame() elem_frame = elem.webFrame()
framegeom = QRect(elem_frame.geometry()) framegeom = QRect(elem_frame.geometry())
if not framegeom.isValid(): if not framegeom.isValid():
visible_in_frame = False visible_in_frame = False

View File

@ -30,7 +30,7 @@ from PyQt5.QtWebKit import QWebSettings
from PyQt5.QtPrintSupport import QPrinter from PyQt5.QtPrintSupport import QPrinter
from qutebrowser.browser import browsertab 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 from qutebrowser.utils import qtutils, objreg, usertypes, utils
@ -557,6 +557,18 @@ class WebKitTab(browsertab.AbstractTab):
def set_html(self, html, base_url): def set_html(self, html, base_url):
self._widget.setHtml(html, base_url) self._widget.setHtml(html, base_url)
def find_all_elements(self, selector):
mainframe = self._widget.page().mainFrame()
if mainframe is None:
raise 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))
return elems
@pyqtSlot() @pyqtSlot()
def _on_frame_load_finished(self): def _on_frame_load_finished(self):
"""Make sure we emit an appropriate status when loading finished. """Make sure we emit an appropriate status when loading finished.