Add a separate tab.elements object

This commit is contained in:
Florian Bruhin 2016-08-18 14:01:27 +02:00
parent da73a7123c
commit d3084dd690
7 changed files with 173 additions and 149 deletions

View File

@ -415,6 +415,46 @@ class AbstractHistory:
raise NotImplementedError
class AbstractElements:
"""Finding and handling of elements on the page."""
def __init__(self, tab):
self._widget = None
self._tab = tab
def find_css(self, selector, callback, *, only_visible=False):
"""Find all HTML elements matching a given selector async.
Args:
callback: The callback to be called when the search finished.
selector: The CSS selector to search for.
only_visible: Only show elements which are visible on screen.
"""
raise NotImplementedError
def find_focused(self, callback):
"""Find the focused element on the page async.
Args:
callback: The callback to be called when the search finished.
Called with a WebEngineElement or None.
"""
raise NotImplementedError
def find_at_pos(self, pos, callback):
"""Find the element at the given position async.
This is also called "hit test" elsewhere.
Args:
pos: The QPoint to get the element for.
callback: The callback to be called when the search finished.
Called with a WebEngineElement or None.
"""
raise NotImplementedError
class AbstractTab(QWidget):
"""A wrapper over the given widget to hide its API and expose another one.
@ -472,6 +512,7 @@ class AbstractTab(QWidget):
# self.zoom = AbstractZoom(win_id=win_id)
# self.search = AbstractSearch(parent=self)
# self.printing = AbstractPrinting()
# self.elements = AbstractElements(self)
self.data = TabData()
self._layout = miscwidgets.WrapperLayout(self)
@ -499,6 +540,7 @@ class AbstractTab(QWidget):
self.zoom._widget = widget
self.search._widget = widget
self.printing._widget = widget
self.elements._widget = widget
self._install_event_filter()
def _install_event_filter(self):
@ -676,37 +718,6 @@ class AbstractTab(QWidget):
def set_html(self, html, base_url):
raise NotImplementedError
def find_all_elements(self, selector, callback, *, only_visible=False):
"""Find all HTML elements matching a given selector async.
Args:
callback: The callback to be called when the search finished.
selector: The CSS selector to search for.
only_visible: Only show elements which are visible on screen.
"""
raise NotImplementedError
def find_focus_element(self, callback):
"""Find the focused element on the page async.
Args:
callback: The callback to be called when the search finished.
Called with a WebEngineElement or None.
"""
raise NotImplementedError
def find_element_at_pos(self, pos, callback):
"""Find the element at the given position async.
This is also called "hit test" elsewhere.
Args:
pos: The QPoint to get the element for.
callback: The callback to be called when the search finished.
Called with a WebEngineElement or None.
"""
raise NotImplementedError
def __repr__(self):
try:
url = utils.elide(self.url().toDisplayString(QUrl.EncodeUnicode),

View File

@ -1474,7 +1474,7 @@ class CommandDispatcher:
`general -> editor` config option.
"""
tab = self._current_widget()
tab.find_focus_element(self._open_editor_cb)
tab.elements.find_focused(self._open_editor_cb)
def on_editing_finished(self, elem, text):
"""Write the editor text into the form field and clean up tempfile.

View File

@ -739,7 +739,7 @@ class HintManager(QObject):
self._context.args = args
self._context.group = group
selector = webelem.SELECTORS[self._context.group]
self._context.tab.find_all_elements(selector, self._start_cb,
self._context.tab.elements.find_css(selector, self._start_cb,
only_visible=True)
def current_mode(self):

View File

@ -94,7 +94,7 @@ class MouseEventFilter(QObject):
self._ignore_wheel_event = True
self._mousepress_opentarget(e)
self._tab.find_element_at_pos(e.pos(), self._mousepress_insertmode_cb)
self._tab.elements.find_at_pos(e.pos(), self._mousepress_insertmode_cb)
return False
@ -175,7 +175,7 @@ class MouseEventFilter(QObject):
usertypes.KeyMode.insert,
'click-delayed')
self._tab.find_focus_element(mouserelease_insertmode_cb)
self._tab.elements.find_focused(mouserelease_insertmode_cb)
def _mousepress_backforward(self, e):
"""Handle back/forward mouse button presses.

View File

@ -141,4 +141,4 @@ def prevnext(*, browsertab, win_id, baseurl, prev=False,
selector = ', '.join([webelem.SELECTORS[webelem.Group.links],
webelem.SELECTORS[webelem.Group.prevnext]])
browsertab.find_all_elements(selector, _prevnext_cb)
browsertab.elements.find_css(selector, _prevnext_cb)

View File

@ -313,6 +313,58 @@ class WebEngineZoom(browsertab.AbstractZoom):
return self._widget.zoomFactor()
class WebEngineElements(browsertab.AbstractElements):
"""QtWebEngine implemementations related to elements on the page."""
def _js_cb_multiple(self, callback, js_elems):
"""Handle found elements coming from JS and call the real callback.
Args:
callback: The callback to call with the found elements.
js_elems: The elements serialized from javascript.
"""
elems = []
for js_elem in js_elems:
elem = webengineelem.WebEngineElement(js_elem, tab=self)
elems.append(elem)
callback(elems)
def _js_cb_single(self, callback, js_elem):
"""Handle a found focus elem coming from JS and call the real callback.
Args:
callback: The callback to call with the found element.
Called with a WebEngineElement or None.
js_elem: The element serialized from javascript.
"""
log.webview.debug("Got element from JS: {!r}".format(js_elem))
if js_elem is None:
callback(None)
else:
elem = webengineelem.WebEngineElement(js_elem, tab=self)
callback(elem)
def find_css(self, selector, callback, *, only_visible=False):
js_code = javascript.assemble('webelem', 'find_all', selector,
only_visible)
js_cb = functools.partial(self._js_cb_multiple, callback)
self._tab.run_js_async(js_code, js_cb)
def find_focused(self, callback):
js_code = javascript.assemble('webelem', 'focus_element')
js_cb = functools.partial(self._js_cb_single, callback)
self._tab.run_js_async(js_code, js_cb)
def find_at_pos(self, pos, callback):
assert pos.x() >= 0
assert pos.y() >= 0
js_code = javascript.assemble('webelem', 'element_at_pos',
pos.x(), pos.y())
js_cb = functools.partial(self._js_cb_single, callback)
self._tab.run_js_async(js_code, js_cb)
class WebEngineTab(browsertab.AbstractTab):
"""A QtWebEngine tab in the browser."""
@ -327,6 +379,7 @@ class WebEngineTab(browsertab.AbstractTab):
self.zoom = WebEngineZoom(win_id=win_id, parent=self)
self.search = WebEngineSearch(parent=self)
self.printing = WebEnginePrinting()
self.elements = WebEngineElements(self)
self._set_widget(widget)
self._connect_signals()
self.backend = usertypes.Backend.QtWebEngine
@ -446,53 +499,6 @@ class WebEngineTab(browsertab.AbstractTab):
def clear_ssl_errors(self):
log.stub()
def _js_element_cb_multiple(self, callback, js_elems):
"""Handle found elements coming from JS and call the real callback.
Args:
callback: The callback to call with the found elements.
js_elems: The elements serialized from javascript.
"""
elems = []
for js_elem in js_elems:
elem = webengineelem.WebEngineElement(js_elem, tab=self)
elems.append(elem)
callback(elems)
def _js_element_cb_single(self, callback, js_elem):
"""Handle a found focus elem coming from JS and call the real callback.
Args:
callback: The callback to call with the found element.
Called with a WebEngineElement or None.
js_elem: The element serialized from javascript.
"""
log.webview.debug("Got element from JS: {!r}".format(js_elem))
if js_elem is None:
callback(None)
else:
elem = webengineelem.WebEngineElement(js_elem, tab=self)
callback(elem)
def find_all_elements(self, selector, callback, *, only_visible=False):
js_code = javascript.assemble('webelem', 'find_all', selector,
only_visible)
js_cb = functools.partial(self._js_element_cb_multiple, callback)
self.run_js_async(js_code, js_cb)
def find_focus_element(self, callback):
js_code = javascript.assemble('webelem', 'focus_element')
js_cb = functools.partial(self._js_element_cb_single, callback)
self.run_js_async(js_code, js_cb)
def find_element_at_pos(self, pos, callback):
assert pos.x() >= 0
assert pos.y() >= 0
js_code = javascript.assemble('webelem', 'element_at_pos',
pos.x(), pos.y())
js_cb = functools.partial(self._js_element_cb_single, callback)
self.run_js_async(js_code, js_cb)
def _connect_signals(self):
view = self._widget
page = view.page()

View File

@ -492,6 +492,78 @@ class WebKitHistory(browsertab.AbstractHistory):
self._tab.scroller.to_point, cur_data['scroll-pos']))
class WebKitElements(browsertab.AbstractElements):
"""QtWebKit implemementations related to elements on the page."""
def find_css(self, selector, callback, *, only_visible=False):
mainframe = self._widget.page().mainFrame()
if mainframe is None:
raise browsertab.WebTabError("No frame focused!")
elems = []
frames = webkitelem.get_child_frames(mainframe)
for f in frames:
for elem in f.findAllElements(selector):
elems.append(webkitelem.WebKitElement(elem))
if only_visible:
elems = [e for e in elems if e.is_visible(mainframe)]
callback(elems)
def find_focused(self, callback):
frame = self._widget.page().currentFrame()
if frame is None:
callback(None)
return
elem = frame.findFirstElement('*:focus')
if elem.isNull():
callback(None)
else:
callback(webkitelem.WebKitElement(elem))
def find_at_pos(self, pos, callback):
assert pos.x() >= 0
assert pos.y() >= 0
frame = self._widget.page().frameAt(pos)
if frame is None:
# This happens when we click inside the webview, but not actually
# on the QWebPage - for example when clicking the scrollbar
# sometimes.
log.webview.debug("Hit test at {} but frame is None!".format(pos))
callback(None)
return
# You'd think we have to subtract frame.geometry().topLeft() from the
# position, but it seems QWebFrame::hitTestContent wants a position
# relative to the QWebView, not to the frame. This makes no sense to
# me, but it works this way.
hitresult = frame.hitTestContent(pos)
if hitresult.isNull():
# For some reason, the whole hit result can be null sometimes (e.g.
# on doodle menu links). If this is the case, we schedule a check
# later (in mouseReleaseEvent) which uses webkitelem.focus_elem.
log.webview.debug("Hit test result is null!")
callback(None)
return
try:
elem = webkitelem.WebKitElement(hitresult.element())
except webkitelem.IsNullError:
# For some reason, the hit result element can be a null element
# sometimes (e.g. when clicking the timetable fields on
# http://www.sbb.ch/ ). If this is the case, we schedule a check
# later (in mouseReleaseEvent) which uses webelem.focus_elem.
log.webview.debug("Hit test result element is null!")
callback(None)
return
callback(elem)
class WebKitTab(browsertab.AbstractTab):
"""A QtWebKit tab in the browser."""
@ -506,6 +578,7 @@ class WebKitTab(browsertab.AbstractTab):
self.zoom = WebKitZoom(win_id=win_id, parent=self)
self.search = WebKitSearch(parent=self)
self.printing = WebKitPrinting()
self.elements = WebKitElements(self)
self._set_widget(widget)
self._connect_signals()
self.zoom.set_default()
@ -566,72 +639,6 @@ class WebKitTab(browsertab.AbstractTab):
def set_html(self, html, base_url):
self._widget.setHtml(html, base_url)
def find_all_elements(self, selector, callback, *, only_visible=False):
mainframe = self._widget.page().mainFrame()
if mainframe is None:
raise browsertab.WebTabError("No frame focused!")
elems = []
frames = webkitelem.get_child_frames(mainframe)
for f in frames:
for elem in f.findAllElements(selector):
elems.append(webkitelem.WebKitElement(elem))
if only_visible:
elems = [e for e in elems if e.is_visible(mainframe)]
callback(elems)
def find_focus_element(self, callback):
frame = self._widget.page().currentFrame()
if frame is None:
callback(None)
return
elem = frame.findFirstElement('*:focus')
if elem.isNull():
callback(None)
else:
callback(webkitelem.WebKitElement(elem))
def find_element_at_pos(self, pos, callback):
assert pos.x() >= 0
assert pos.y() >= 0
frame = self._widget.page().frameAt(pos)
if frame is None:
# This happens when we click inside the webview, but not actually
# on the QWebPage - for example when clicking the scrollbar
# sometimes.
log.webview.debug("Hit test at {} but frame is None!".format(pos))
callback(None)
return
# You'd think we have to subtract frame.geometry().topLeft() from the
# position, but it seems QWebFrame::hitTestContent wants a position
# relative to the QWebView, not to the frame. This makes no sense to
# me, but it works this way.
hitresult = frame.hitTestContent(pos)
if hitresult.isNull():
# For some reason, the whole hit result can be null sometimes (e.g.
# on doodle menu links). If this is the case, we schedule a check
# later (in mouseReleaseEvent) which uses webkitelem.focus_elem.
log.webview.debug("Hit test result is null!")
callback(None)
return
try:
elem = webkitelem.WebKitElement(hitresult.element())
except webkitelem.IsNullError:
# For some reason, the hit result element can be a null element
# sometimes (e.g. when clicking the timetable fields on
# http://www.sbb.ch/ ). If this is the case, we schedule a check
# later (in mouseReleaseEvent) which uses webelem.focus_elem.
log.webview.debug("Hit test result element is null!")
callback(None)
return
callback(elem)
@pyqtSlot()
def _on_frame_load_finished(self):
"""Make sure we emit an appropriate status when loading finished.