From d3084dd69063b43e19b22247c87754c867ef81e9 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 18 Aug 2016 14:01:27 +0200 Subject: [PATCH] Add a separate tab.elements object --- qutebrowser/browser/browsertab.py | 73 +++++---- qutebrowser/browser/commands.py | 2 +- qutebrowser/browser/hints.py | 2 +- qutebrowser/browser/mouse.py | 4 +- qutebrowser/browser/navigate.py | 2 +- qutebrowser/browser/webengine/webenginetab.py | 100 +++++++------ qutebrowser/browser/webkit/webkittab.py | 139 +++++++++--------- 7 files changed, 173 insertions(+), 149 deletions(-) diff --git a/qutebrowser/browser/browsertab.py b/qutebrowser/browser/browsertab.py index f470cf313..f43ff43f8 100644 --- a/qutebrowser/browser/browsertab.py +++ b/qutebrowser/browser/browsertab.py @@ -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), diff --git a/qutebrowser/browser/commands.py b/qutebrowser/browser/commands.py index 276b0e352..b3f190951 100644 --- a/qutebrowser/browser/commands.py +++ b/qutebrowser/browser/commands.py @@ -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. diff --git a/qutebrowser/browser/hints.py b/qutebrowser/browser/hints.py index 07727b27f..1c2c1bf4b 100644 --- a/qutebrowser/browser/hints.py +++ b/qutebrowser/browser/hints.py @@ -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): diff --git a/qutebrowser/browser/mouse.py b/qutebrowser/browser/mouse.py index c3c8ac879..a08b693eb 100644 --- a/qutebrowser/browser/mouse.py +++ b/qutebrowser/browser/mouse.py @@ -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. diff --git a/qutebrowser/browser/navigate.py b/qutebrowser/browser/navigate.py index 12492e673..c78714315 100644 --- a/qutebrowser/browser/navigate.py +++ b/qutebrowser/browser/navigate.py @@ -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) diff --git a/qutebrowser/browser/webengine/webenginetab.py b/qutebrowser/browser/webengine/webenginetab.py index df13f2285..3b593d6a8 100644 --- a/qutebrowser/browser/webengine/webenginetab.py +++ b/qutebrowser/browser/webengine/webenginetab.py @@ -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() diff --git a/qutebrowser/browser/webkit/webkittab.py b/qutebrowser/browser/webkit/webkittab.py index 196a1215e..933a3b25e 100644 --- a/qutebrowser/browser/webkit/webkittab.py +++ b/qutebrowser/browser/webkit/webkittab.py @@ -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.