From b8e2d5f8f6a3547fede59e1dbc8e65f5e3c7358f Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 8 Aug 2016 15:01:41 +0200 Subject: [PATCH] Add initial WebEngineElement implementation This allows :navigate prev/next to work correctly via the javascript bridge. --- qutebrowser/browser/navigate.py | 5 - qutebrowser/browser/webelem.py | 5 +- .../browser/webengine/webengineelem.py | 176 ++++++++++++++++++ qutebrowser/browser/webengine/webenginetab.py | 22 ++- qutebrowser/browser/webkit/webkitelem.py | 4 - qutebrowser/javascript/webelem.js | 63 +++++++ 6 files changed, 259 insertions(+), 16 deletions(-) create mode 100644 qutebrowser/browser/webengine/webengineelem.py create mode 100644 qutebrowser/javascript/webelem.js diff --git a/qutebrowser/browser/navigate.py b/qutebrowser/browser/navigate.py index 04d4c6be9..a76bec05d 100644 --- a/qutebrowser/browser/navigate.py +++ b/qutebrowser/browser/navigate.py @@ -109,11 +109,6 @@ def prevnext(*, browsertab, win_id, baseurl, prev=False, background: True to open in a background tab. window: True to open in a new window, False for the current one. """ - # FIXME:qtwebengine have a proper API for this - if browsertab.backend == usertypes.Backend.QtWebEngine: - raise Error(":navigate prev/next is not supported yet with " - "QtWebEngine") - def _prevnext_cb(elems): elem = _find_prevnext(prev, elems) word = 'prev' if prev else 'forward' diff --git a/qutebrowser/browser/webelem.py b/qutebrowser/browser/webelem.py index 8bb60d4ea..749cb151f 100644 --- a/qutebrowser/browser/webelem.py +++ b/qutebrowser/browser/webelem.py @@ -79,7 +79,7 @@ class AbstractWebElement(collections.abc.MutableMapping): raise NotImplementedError def __str__(self): - raise NotImplementedError + return self.text() def __getitem__(self, key): raise NotImplementedError @@ -90,9 +90,6 @@ class AbstractWebElement(collections.abc.MutableMapping): def __delitem__(self, key): raise NotImplementedError - def __contains__(self, key): - raise NotImplementedError - def __iter__(self): raise NotImplementedError diff --git a/qutebrowser/browser/webengine/webengineelem.py b/qutebrowser/browser/webengine/webengineelem.py new file mode 100644 index 000000000..69de7c906 --- /dev/null +++ b/qutebrowser/browser/webengine/webengineelem.py @@ -0,0 +1,176 @@ +# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: + +# Copyright 2016 Florian Bruhin (The Compiler) +# +# This file is part of qutebrowser. +# +# qutebrowser is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# qutebrowser is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with qutebrowser. If not, see . + +# FIXME:qtwebengine remove this once the stubs are gone +# pylint: disable=unused-variable + +"""QtWebEngine specific part of the web element API.""" + +from PyQt5.QtCore import QRect + +from qutebrowser.utils import log +from qutebrowser.browser import webelem + + +class WebEngineElement(webelem.AbstractWebElement): + + """A web element for QtWebEngine, using JS under the hood.""" + + def __init__(self, js_dict): + self._id = js_dict['id'] + self._js_dict = js_dict + + def __eq__(self, other): + if not isinstance(other, WebEngineElement): + return NotImplemented + return self._id == other._id # pylint: disable=protected-access + + def __getitem__(self, key): + attrs = self._js_dict['attributes'] + return attrs[key] + + def __setitem__(self, key, val): + log.stub() + + def __delitem__(self, key): + log.stub() + + def __iter__(self): + return iter(self._js_dict['attributes']) + + def __len__(self): + return len(self._js_dict['attributes']) + + def frame(self): + log.stub() + return None + + def geometry(self): + log.stub() + return QRect() + + def document_element(self): + log.stub() + return None + + def create_inside(self, tagname): + log.stub() + return None + + def find_first(self, selector): + log.stub() + return None + + def style_property(self, name, *, strategy): + log.stub() + return '' + + def classes(self): + """Get a list of classes assigned to this element.""" + log.stub() + return [] + + def tag_name(self): + """Get the tag name of this element. + + The returned name will always be lower-case. + """ + return self._js_dict['tag_name'] + + def outer_xml(self): + """Get the full HTML representation of this element.""" + return self._js_dict['outer_xml'] + + def text(self, *, use_js=False): + """Get the plain text content for this element. + + Args: + use_js: Whether to use javascript if the element isn't + content-editable. + """ + if use_js: + # FIXME:qtwebengine what to do about use_js with WebEngine? + log.stub('with use_js=True') + return self._js_dict.get('text', '') + + 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. + """ + # FIXME:qtwebengine what to do about use_js with WebEngine? + log.stub() + + def set_inner_xml(self, xml): + """Set the given inner XML.""" + # FIXME:qtwebengine get rid of this? + log.stub() + + def remove_from_document(self): + """Remove the node from the document.""" + # FIXME:qtwebengine get rid of this? + log.stub() + + def set_style_property(self, name, value): + """Set the element style.""" + # FIXME:qtwebengine get rid of this? + log.stub() + + def run_js_async(self, code, callback=None): + """Run the given JS snippet async on the element.""" + # FIXME:qtwebengine get rid of this? + log.stub() + + def parent(self): + """Get the parent element of this element.""" + # FIXME:qtwebengine get rid of this? + log.stub() + return None + + 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 + """ + log.stub() + return QRect() + + def is_visible(self, mainframe): + """Check if the given element is visible in the given frame.""" + # FIXME:qtwebengine get rid of this? + log.stub() + return True diff --git a/qutebrowser/browser/webengine/webenginetab.py b/qutebrowser/browser/webengine/webenginetab.py index 39c6ed47e..3ac460a87 100644 --- a/qutebrowser/browser/webengine/webenginetab.py +++ b/qutebrowser/browser/webengine/webenginetab.py @@ -22,6 +22,8 @@ """Wrapper over a QWebEngineView.""" +import functools + from PyQt5.QtCore import pyqtSlot, Qt, QEvent, QPoint from PyQt5.QtGui import QKeyEvent, QIcon from PyQt5.QtWidgets import QApplication @@ -30,7 +32,7 @@ from PyQt5.QtWebEngineWidgets import QWebEnginePage, QWebEngineScript # pylint: enable=no-name-in-module,import-error,useless-suppression from qutebrowser.browser import browsertab -from qutebrowser.browser.webengine import webview +from qutebrowser.browser.webengine import webview, webengineelem from qutebrowser.utils import usertypes, qtutils, log, javascript @@ -411,9 +413,23 @@ class WebEngineTab(browsertab.AbstractTab): def clear_ssl_errors(self): log.stub() + def _find_all_elements_js_cb(self, callback, js_elems): + """Handle found elements coming from JS and call the real callback. + + Args: + callback: The callback originally passed to find_all_elements. + js_elems: The elements serialized from javascript. + """ + elems = [] + for js_elem in js_elems: + elem = webengineelem.WebEngineElement(js_elem) + elems.append(elem) + callback(elems) + def find_all_elements(self, selector, callback, *, only_visible=False): - log.stub() - callback([]) + js_code = javascript.assemble('webelem', 'find_all_elements', selector) + js_cb = functools.partial(self._find_all_elements_js_cb, callback) + self.run_js_async(js_code, js_cb) def _connect_signals(self): view = self._widget diff --git a/qutebrowser/browser/webkit/webkitelem.py b/qutebrowser/browser/webkit/webkitelem.py index 46f4fb8e9..895c48e59 100644 --- a/qutebrowser/browser/webkit/webkitelem.py +++ b/qutebrowser/browser/webkit/webkitelem.py @@ -50,10 +50,6 @@ class WebKitElement(webelem.AbstractWebElement): return NotImplemented return self._elem == other._elem # pylint: disable=protected-access - def __str__(self): - self._check_vanished() - return self._elem.toPlainText() - def __getitem__(self, key): self._check_vanished() if key not in self: diff --git a/qutebrowser/javascript/webelem.js b/qutebrowser/javascript/webelem.js new file mode 100644 index 000000000..9aa132e41 --- /dev/null +++ b/qutebrowser/javascript/webelem.js @@ -0,0 +1,63 @@ +/** + * Copyright 2016 Florian Bruhin (The Compiler) + * + * This file is part of qutebrowser. + * + * qutebrowser is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * qutebrowser is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with qutebrowser. If not, see . + */ + + +document._qutebrowser_elements = []; + + +function _qutebrowser_serialize_elem(elem, id) { + var out = {}; + + var attributes = {}; + for (var i = 0; i < elem.attributes.length; ++i) { + attr = elem.attributes[i]; + attributes[attr.name] = attr.value; + } + out["attributes"] = attributes; + + out["text"] = elem.text; + out["tag_name"] = elem.tagName; + out["outer_xml"] = elem.outerHTML; + out["id"] = id; + + // console.log(JSON.stringify(out)); + + return out; +} + + +function _qutebrowser_find_all_elements(selector) { + var elems = document.querySelectorAll(selector); + var out = []; + var id = document._qutebrowser_elements.length; + + for (var i = 0; i < elems.length; ++i) { + var elem = elems[i]; + out.push(_qutebrowser_serialize_elem(elem, id)); + document._qutebrowser_elements[id] = elem; + id++; + } + + return out; +} + + +function _qutebrowser_get_element(id) { + return document._qutebrowser_elements[id]; +}