From b856bf3a47077711a14929ec4f41ca2276e3209a Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 4 Sep 2014 08:00:05 +0200 Subject: [PATCH] Improve webelement API --- .pylintrc | 3 + doc/TODO | 2 - qutebrowser/browser/commands.py | 34 +- qutebrowser/browser/hints.py | 60 ++-- qutebrowser/test/stubs.py | 79 ----- qutebrowser/test/utils/test_webelem.py | 324 +++++++++++-------- qutebrowser/utils/webelem.py | 429 +++++++++++++++---------- qutebrowser/widgets/webview.py | 27 +- 8 files changed, 520 insertions(+), 438 deletions(-) diff --git a/.pylintrc b/.pylintrc index 5c1717e92..83ffc5fc1 100644 --- a/.pylintrc +++ b/.pylintrc @@ -51,3 +51,6 @@ defining-attr-methods=__init__,__new__,setUp [DESIGN] max-args=10 + +[TYPECHECK] +ignored-classes=WebElementWrapper diff --git a/doc/TODO b/doc/TODO index 9215849f3..f829ac6cd 100644 --- a/doc/TODO +++ b/doc/TODO @@ -127,8 +127,6 @@ style - Always use a list as argument for namedtuple? - Refactor enum() so it uses a list as second argument (like python enum/namedtuple). -- utils.webelem could be a wrapper class over QWebElement instead of a - collection of functions. dwb keybindings to possibly implement ===================================== diff --git a/qutebrowser/browser/commands.py b/qutebrowser/browser/commands.py index 9a9835cdc..13173c674 100644 --- a/qutebrowser/browser/commands.py +++ b/qutebrowser/browser/commands.py @@ -763,13 +763,14 @@ class CommandDispatcher: and do everything async. """ frame = self._current_widget().page().currentFrame() - elem = webelem.focus_elem(frame) - if elem.isNull(): + try: + elem = webelem.focus_elem(frame) + except webelem.IsNullError: raise cmdexc.CommandError("No element focused!") - if not webelem.is_editable(elem, strict=True): + if not elem.is_editable(strict=True): raise cmdexc.CommandError("Focused element is not editable!") - if webelem.is_content_editable(elem): - text = elem.toPlainText() + if elem.is_content_editable(): + text = str(elem) else: text = elem.evaluateJavaScript('this.value') self._editor = editor.ExternalEditor(self._tabs) @@ -783,17 +784,18 @@ class CommandDispatcher: Callback for QProcess when the editor was closed. Args: - elem: The QWebElement which was modified. + elem: The WebElementWrapper which was modified. text: The new text to insert. """ - if elem.isNull(): + 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)) + except webelem.IsNullError: raise cmdexc.CommandError("Element vanished while editing!") - if webelem.is_content_editable(elem): - log.misc.debug("Filling element {} via setPlainText.".format( - webelem.debug_text(elem))) - elem.setPlainText(text) - else: - log.misc.debug("Filling element {} via javascript.".format( - webelem.debug_text(elem))) - text = webelem.javascript_escape(text) - elem.evaluateJavaScript("this.value='{}'".format(text)) diff --git a/qutebrowser/browser/hints.py b/qutebrowser/browser/hints.py index 0ea1977d7..1583fa331 100644 --- a/qutebrowser/browser/hints.py +++ b/qutebrowser/browser/hints.py @@ -155,8 +155,10 @@ class HintManager(QObject): def _cleanup(self): """Clean up after hinting.""" for elem in self._context.elems.values(): - if not elem.label.isNull(): + try: elem.label.removeFromDocument() + except webelem.IsNullError: + pass text = self.HINT_TEXTS[self._context.target] message.instance().maybe_reset_text(text) self._context = None @@ -263,7 +265,7 @@ class HintManager(QObject): Return: The CSS to set as a string. """ - if label is None or label.attribute('hidden') != 'true': + if label is None or label['hidden'] != 'true': display = 'inline' else: display = 'none' @@ -290,7 +292,9 @@ class HintManager(QObject): # See: http://stackoverflow.com/q/7364852/2085149 doc.appendInside('{}'.format( css, string)) - return doc.lastChild() + elem = webelem.WebElementWrapper(doc.lastChild()) + elem['hidden'] = 'false' + return elem def _click(self, elem): """Click an element. @@ -306,9 +310,9 @@ class HintManager(QObject): # FIXME Instead of clicking the center, we could have nicer heuristics. # e.g. parse (-webkit-)border-radius correctly and click text fields at # the bottom right, and everything else on the top left or so. - pos = webelem.rect_on_view(elem).center() - log.hints.debug("Clicking on '{}' at {}/{}".format(elem.toPlainText(), - pos.x(), pos.y())) + pos = elem.rect_on_view().center() + log.hints.debug("Clicking on '{}' at {}/{}".format( + elem, pos.x(), pos.y())) events = ( QMouseEvent(QEvent.MouseMove, pos, Qt.NoButton, Qt.NoButton, Qt.NoModifier), @@ -384,7 +388,7 @@ class HintManager(QObject): Return: A QUrl with the absolute URL, or None. """ - text = elem.attribute('href') + text = elem['href'] if not text: return None if baseurl is None: @@ -402,12 +406,14 @@ class HintManager(QObject): webelem.SELECTORS[webelem.Group.links]) rel_values = ('prev', 'previous') if prev else ('next') for e in elems: - if not e.hasAttribute('rel'): + e = webelem.WebElementWrapper(e) + try: + rel_attr = e['rel'] + except KeyError: continue - rel_attr = e.attribute('rel') if rel_attr in rel_values: log.hints.debug("Found '{}' with rel={}".format( - webelem.debug_text(e), rel_attr)) + e.debug_text(), rel_attr)) return e # Then check for regular links/buttons. elems = frame.findAllElements( @@ -418,7 +424,8 @@ class HintManager(QObject): for regex in config.get('hints', option): log.hints.vdebug("== Checking regex '{}'.".format(regex.pattern)) for e in elems: - text = e.toPlainText() + e = webelem.WebElementWrapper(e) + text = str(e) if not text: continue if regex.search(text): @@ -498,10 +505,11 @@ class HintManager(QObject): ctx = HintContext() ctx.frames = webelem.get_child_frames(mainframe) for f in ctx.frames: - elems += f.findAllElements(webelem.SELECTORS[group]) + for e in f.findAllElements(webelem.SELECTORS[group]): + elems.append(webelem.WebElementWrapper(e)) filterfunc = webelem.FILTERS.get(group, lambda e: True) visible_elems = [e for e in elems if filterfunc(e) and - webelem.is_visible(e, mainframe)] + e.is_visible(mainframe)] if not visible_elems: raise cmdexc.CommandError("No elements found.") ctx.target = target @@ -529,34 +537,34 @@ class HintManager(QObject): rest = string[len(keystr):] elems.label.setInnerXml('{}{}'.format( config.get('colors', 'hints.fg.match'), matched, rest)) - if elems.label.attribute('hidden') == 'true': + if elems.label['hidden'] == 'true': # hidden element which matches again -> unhide it - elems.label.setAttribute('hidden', 'false') + elems.label['hidden'] = 'false' css = self._get_hint_css(elems.elem, elems.label) - elems.label.setAttribute('style', css) + elems.label['style'] = css else: # element doesn't match anymore -> hide it - elems.label.setAttribute('hidden', 'true') + elems.label['hidden'] = 'true' css = self._get_hint_css(elems.elem, elems.label) - elems.label.setAttribute('style', css) + elems.label['style'] = css def filter_hints(self, filterstr): """Filter displayed hints according to a text.""" for elems in self._context.elems.values(): - if elems.elem.toPlainText().lower().startswith(filterstr): - if elems.label.attribute('hidden') == 'true': + if str(elems.elem).lower().startswith(filterstr): + if elems.label['hidden'] == 'true': # hidden element which matches again -> unhide it - elems.label.setAttribute('hidden', 'false') + elems.label['hidden'] = 'false' css = self._get_hint_css(elems.elem, elems.label) - elems.label.setAttribute('style', css) + elems.label['style'] = css else: # element doesn't match anymore -> hide it - elems.label.setAttribute('hidden', 'true') + elems.label['hidden'] = 'true' css = self._get_hint_css(elems.elem, elems.label) - elems.label.setAttribute('style', css) + elems.label['style'] = css visible = {} for k, e in self._context.elems.items(): - if e.label.attribute('hidden') != 'true': + if e.label['hidden'] != 'true': visible[k] = e if not visible: # Whoops, filtered all hints @@ -625,7 +633,7 @@ class HintManager(QObject): log.hints.debug("Contents size changed...!") for elems in self._context.elems.values(): css = self._get_hint_css(elems.elem, elems.label) - elems.label.setAttribute('style', css) + elems.label['style'] = css @pyqtSlot(usertypes.KeyMode) def on_mode_entered(self, mode): diff --git a/qutebrowser/test/stubs.py b/qutebrowser/test/stubs.py index 2625ba909..a45cd4e12 100644 --- a/qutebrowser/test/stubs.py +++ b/qutebrowser/test/stubs.py @@ -24,7 +24,6 @@ from unittest import mock from PyQt5.QtCore import QPoint, QProcess -from PyQt5.QtWebKit import QWebElement from PyQt5.QtNetwork import QNetworkRequest @@ -78,84 +77,6 @@ class FakeKeyEvent: self.modifiers = mock.Mock(return_value=modifiers) -class FakeWebElement: - - """A stub for QWebElement.""" - - def __init__(self, geometry=None, frame=None, null=False, visibility='', - display='', attributes=None, tagname=None, classes=None): - """Constructor. - - Args: - geometry: The geometry of the QWebElement as QRect. - frame: The QWebFrame the element is in. - null: Whether the element is null or not. - visibility: The CSS visibility style property calue. - display: The CSS display style property calue. - attributes: Boolean HTML attributes to be added. - tagname: The tag name. - classes: HTML classes to be added. - - Raise: - ValueError if element is not null and geometry/frame are not given. - """ - self.geometry = mock.Mock(return_value=geometry) - self.webFrame = mock.Mock(return_value=frame) - self.isNull = mock.Mock(return_value=null) - self.tagName = mock.Mock(return_value=tagname) - self._visibility = visibility - self._display = display - self._attributes = attributes - self._classes = classes - - def toOuterXml(self): - """Imitate toOuterXml.""" - return '' - - def styleProperty(self, name, strategy): - """Return the CSS style property named name. - - Only display/visibility and ComputedStyle are simulated. - - Raise: - ValueError if strategy is not ComputedStyle or name is not - visibility/display. - """ - if strategy != QWebElement.ComputedStyle: - raise ValueError("styleProperty called with strategy != " - "ComputedStyle ({})!".format(strategy)) - if name == 'visibility': - return self._visibility - elif name == 'display': - return self._display - else: - raise ValueError("styleProperty called with unknown name " - "'{}'".format(name)) - - def hasAttribute(self, name): - """Check if the element has an attribute named name.""" - if self._attributes is None: - return False - else: - return name in self._attributes - - def attribute(self, name): - """Get the attribute named name.""" - if self._attributes is None: - return '' - try: - return self._attributes[name] - except KeyError: - return '' - - def classes(self): - """Get the classes of the object.""" - if self._classes is not None: - return self._classes.split(' ') - else: - return [] - - class FakeWebFrame: """A stub for QWebFrame.""" diff --git a/qutebrowser/test/utils/test_webelem.py b/qutebrowser/test/utils/test_webelem.py index 978076b6a..c9d422b60 100644 --- a/qutebrowser/test/utils/test_webelem.py +++ b/qutebrowser/test/utils/test_webelem.py @@ -17,16 +17,86 @@ # You should have received a copy of the GNU General Public License # along with qutebrowser. If not, see . +# pylint: disable=protected-access + """Tests for the webelement utils.""" import unittest +import unittest.mock +import collections.abc from PyQt5.QtCore import QRect, QPoint +from PyQt5.QtWebKit import QWebElement from qutebrowser.utils import webelem from qutebrowser.test import stubs +def get_webelem(geometry=None, frame=None, null=False, visibility='', + display='', attributes=None, tagname=None, classes=None): + """Factory for WebElementWrapper objects based on a mock. + + Args: + geometry: The geometry of the QWebElement as QRect. + frame: The QWebFrame the element is in. + null: Whether the element is null or not. + visibility: The CSS visibility style property calue. + display: The CSS display style property calue. + attributes: Boolean HTML attributes to be added. + tagname: The tag name. + classes: HTML classes to be added. + """ + elem = unittest.mock.Mock() + elem.isNull.return_value = null + elem.geometry.return_value = geometry + elem.webFrame.return_value = frame + elem.tagName.return_value = tagname + elem.toOuterXml.return_value = '' + if attributes is not None: + if not isinstance(attributes, collections.abc.Mapping): + attributes = {e: None for e in attributes} + elem.hasAttribute.side_effect = lambda k: k in attributes + elem.attribute.side_effect = lambda k: attributes.get(k, '') + elem.attributeNames.return_value = list(attributes) + else: + elem.hasAttribute.return_value = False + elem.attribute.return_value = '' + elem.attributeNames.return_value = [] + if classes is not None: + elem.classes.return_value = classes.split(' ') + else: + elem.classes.return_value = [] + + def _style_property(name, strategy): + """Helper function to act as styleProperty method.""" + if strategy != QWebElement.ComputedStyle: + raise ValueError("styleProperty called with strategy != " + "ComputedStyle ({})!".format(strategy)) + if name == 'visibility': + return visibility + elif name == 'display': + return display + else: + raise ValueError("styleProperty called with unknown name " + "'{}'".format(name)) + + elem.styleProperty.side_effect = _style_property + wrapped = webelem.WebElementWrapper(elem) + if attributes is not None: + wrapped.update(attributes) + return wrapped + + +class WebElementWrapperTests(unittest.TestCase): + + """Test WebElementWrapper.""" + + def test_nullelem(self): + """Test __init__ with a null element.""" + with self.assertRaises(webelem.IsNullError): + get_webelem(null=True) + + class IsVisibleInvalidTests(unittest.TestCase): """Tests for is_visible with invalid elements. @@ -44,19 +114,17 @@ class IsVisibleInvalidTests(unittest.TestCase): geometry() and webFrame() should not be called, and ValueError should be raised. """ - elem = stubs.FakeWebElement(null=True) - with self.assertRaises(ValueError): - webelem.is_visible(elem, self.frame) - elem.isNull.assert_called_once_with() - self.assertFalse(elem.geometry.called) - self.assertFalse(elem.webFrame.called) + elem = get_webelem() + elem._elem.isNull.return_value = True + with self.assertRaises(webelem.IsNullError): + elem.is_visible(self.frame) def test_invalid_invisible(self): """Test elements with an invalid geometry which are invisible.""" - elem = stubs.FakeWebElement(QRect(0, 0, 0, 0), self.frame) + elem = get_webelem(QRect(0, 0, 0, 0), self.frame) self.assertFalse(elem.geometry().isValid()) self.assertEqual(elem.geometry().x(), 0) - self.assertFalse(webelem.is_visible(elem, self.frame)) + self.assertFalse(elem.is_visible(self.frame)) def test_invalid_visible(self): """Test elements with an invalid geometry which are visible. @@ -64,9 +132,9 @@ class IsVisibleInvalidTests(unittest.TestCase): This seems to happen sometimes in the real world, with real elements which *are* visible, but don't have a valid geometry. """ - elem = stubs.FakeWebElement(QRect(10, 10, 0, 0), self.frame) + elem = get_webelem(QRect(10, 10, 0, 0), self.frame) self.assertFalse(elem.geometry().isValid()) - self.assertTrue(webelem.is_visible(elem, self.frame)) + self.assertTrue(elem.is_visible(self.frame)) class IsVisibleScrollTests(unittest.TestCase): @@ -83,13 +151,13 @@ class IsVisibleScrollTests(unittest.TestCase): def test_invisible(self): """Test elements which should be invisible due to scrolling.""" - elem = stubs.FakeWebElement(QRect(5, 5, 4, 4), self.frame) - self.assertFalse(webelem.is_visible(elem, self.frame)) + elem = get_webelem(QRect(5, 5, 4, 4), self.frame) + self.assertFalse(elem.is_visible(self.frame)) def test_visible(self): """Test elements which still should be visible after scrolling.""" - elem = stubs.FakeWebElement(QRect(10, 10, 1, 1), self.frame) - self.assertTrue(webelem.is_visible(elem, self.frame)) + elem = get_webelem(QRect(10, 10, 1, 1), self.frame) + self.assertTrue(elem.is_visible(self.frame)) class IsVisibleCssTests(unittest.TestCase): @@ -105,27 +173,25 @@ class IsVisibleCssTests(unittest.TestCase): def test_visibility_visible(self): """Check that elements with "visibility = visible" are visible.""" - elem = stubs.FakeWebElement(QRect(0, 0, 10, 10), self.frame, - visibility='visible') - self.assertTrue(webelem.is_visible(elem, self.frame)) + elem = get_webelem(QRect(0, 0, 10, 10), self.frame, + visibility='visible') + self.assertTrue(elem.is_visible(self.frame)) def test_visibility_hidden(self): """Check that elements with "visibility = hidden" are not visible.""" - elem = stubs.FakeWebElement(QRect(0, 0, 10, 10), self.frame, - visibility='hidden') - self.assertFalse(webelem.is_visible(elem, self.frame)) + elem = get_webelem(QRect(0, 0, 10, 10), self.frame, + visibility='hidden') + self.assertFalse(elem.is_visible(self.frame)) def test_display_inline(self): """Check that elements with "display = inline" are visible.""" - elem = stubs.FakeWebElement(QRect(0, 0, 10, 10), self.frame, - display='inline') - self.assertTrue(webelem.is_visible(elem, self.frame)) + elem = get_webelem(QRect(0, 0, 10, 10), self.frame, display='inline') + self.assertTrue(elem.is_visible(self.frame)) def test_display_none(self): """Check that elements with "display = none" are not visible.""" - elem = stubs.FakeWebElement(QRect(0, 0, 10, 10), self.frame, - display='none') - self.assertFalse(webelem.is_visible(elem, self.frame)) + elem = get_webelem(QRect(0, 0, 10, 10), self.frame, display='none') + self.assertFalse(elem.is_visible(self.frame)) class IsVisibleIframeTests(unittest.TestCase): @@ -162,26 +228,26 @@ class IsVisibleIframeTests(unittest.TestCase): self.frame = stubs.FakeWebFrame(QRect(0, 0, 300, 300)) self.iframe = stubs.FakeWebFrame(QRect(0, 10, 100, 100), parent=self.frame) - self.elem1 = stubs.FakeWebElement(QRect(0, 0, 10, 10), self.iframe) - self.elem2 = stubs.FakeWebElement(QRect(20, 90, 10, 10), self.iframe) - self.elem3 = stubs.FakeWebElement(QRect(20, 150, 10, 10), self.iframe) - self.elem4 = stubs.FakeWebElement(QRect(30, 180, 10, 10), self.frame) + self.elem1 = get_webelem(QRect(0, 0, 10, 10), self.iframe) + self.elem2 = get_webelem(QRect(20, 90, 10, 10), self.iframe) + self.elem3 = get_webelem(QRect(20, 150, 10, 10), self.iframe) + self.elem4 = get_webelem(QRect(30, 180, 10, 10), self.frame) def test_not_scrolled(self): """Test base situation.""" self.assertTrue(self.frame.geometry().contains(self.iframe.geometry())) - self.assertTrue(webelem.is_visible(self.elem1, self.frame)) - self.assertTrue(webelem.is_visible(self.elem2, self.frame)) - self.assertFalse(webelem.is_visible(self.elem3, self.frame)) - self.assertTrue(webelem.is_visible(self.elem4, self.frame)) + self.assertTrue(self.elem1.is_visible(self.frame)) + self.assertTrue(self.elem2.is_visible(self.frame)) + self.assertFalse(self.elem3.is_visible(self.frame)) + self.assertTrue(self.elem4.is_visible(self.frame)) def test_iframe_scrolled(self): """Scroll iframe down so elem3 gets visible and elem1/elem2 not.""" self.iframe.scrollPosition.return_value = QPoint(0, 100) - self.assertFalse(webelem.is_visible(self.elem1, self.frame)) - self.assertFalse(webelem.is_visible(self.elem2, self.frame)) - self.assertTrue(webelem.is_visible(self.elem3, self.frame)) - self.assertTrue(webelem.is_visible(self.elem4, self.frame)) + self.assertFalse(self.elem1.is_visible(self.frame)) + self.assertFalse(self.elem2.is_visible(self.frame)) + self.assertTrue(self.elem3.is_visible(self.frame)) + self.assertTrue(self.elem4.is_visible(self.frame)) def test_mainframe_scrolled_iframe_visible(self): """Scroll mainframe down so iframe is partly visible but elem1 not.""" @@ -189,10 +255,10 @@ class IsVisibleIframeTests(unittest.TestCase): geom = self.frame.geometry().translated(self.frame.scrollPosition()) self.assertFalse(geom.contains(self.iframe.geometry())) self.assertTrue(geom.intersects(self.iframe.geometry())) - self.assertFalse(webelem.is_visible(self.elem1, self.frame)) - self.assertTrue(webelem.is_visible(self.elem2, self.frame)) - self.assertFalse(webelem.is_visible(self.elem3, self.frame)) - self.assertTrue(webelem.is_visible(self.elem4, self.frame)) + self.assertFalse(self.elem1.is_visible(self.frame)) + self.assertTrue(self.elem2.is_visible(self.frame)) + self.assertFalse(self.elem3.is_visible(self.frame)) + self.assertTrue(self.elem4.is_visible(self.frame)) def test_mainframe_scrolled_iframe_invisible(self): """Scroll mainframe down so iframe is invisible.""" @@ -200,10 +266,10 @@ class IsVisibleIframeTests(unittest.TestCase): geom = self.frame.geometry().translated(self.frame.scrollPosition()) self.assertFalse(geom.contains(self.iframe.geometry())) self.assertFalse(geom.intersects(self.iframe.geometry())) - self.assertFalse(webelem.is_visible(self.elem1, self.frame)) - self.assertFalse(webelem.is_visible(self.elem2, self.frame)) - self.assertFalse(webelem.is_visible(self.elem3, self.frame)) - self.assertTrue(webelem.is_visible(self.elem4, self.frame)) + self.assertFalse(self.elem1.is_visible(self.frame)) + self.assertFalse(self.elem2.is_visible(self.frame)) + self.assertFalse(self.elem3.is_visible(self.frame)) + self.assertTrue(self.elem4.is_visible(self.frame)) class IsWritableTests(unittest.TestCase): @@ -212,18 +278,18 @@ class IsWritableTests(unittest.TestCase): def test_writable(self): """Test a normal element.""" - elem = stubs.FakeWebElement() - self.assertTrue(webelem.is_writable(elem)) + elem = get_webelem() + self.assertTrue(elem.is_writable()) def test_disabled(self): """Test a disabled element.""" - elem = stubs.FakeWebElement(attributes=['disabled']) - self.assertFalse(webelem.is_writable(elem)) + elem = get_webelem(attributes=['disabled']) + self.assertFalse(elem.is_writable()) def test_readonly(self): """Test a readonly element.""" - elem = stubs.FakeWebElement(attributes=['readonly']) - self.assertFalse(webelem.is_writable(elem)) + elem = get_webelem(attributes=['readonly']) + self.assertFalse(elem.is_writable()) class JavascriptEscapeTests(unittest.TestCase): @@ -309,204 +375,186 @@ class IsEditableTests(unittest.TestCase): def test_input_plain(self): """Test with plain input element.""" - elem = stubs.FakeWebElement(tagname='input') - self.assertTrue(webelem.is_editable(elem)) + elem = get_webelem(tagname='input') + self.assertTrue(elem.is_editable()) def test_input_text(self): """Test with text input element.""" - elem = stubs.FakeWebElement(tagname='input', - attributes={'type': 'text'}) - self.assertTrue(webelem.is_editable(elem)) + elem = get_webelem(tagname='input', attributes={'type': 'text'}) + self.assertTrue(elem.is_editable()) def test_input_text_caps(self): """Test with text input element with caps attributes.""" - elem = stubs.FakeWebElement(tagname='INPUT', - attributes={'TYPE': 'TEXT'}) - self.assertTrue(webelem.is_editable(elem)) + elem = get_webelem(tagname='INPUT', attributes={'TYPE': 'TEXT'}) + self.assertTrue(elem.is_editable()) def test_input_email(self): """Test with email input element.""" - elem = stubs.FakeWebElement(tagname='input', - attributes={'type': 'email'}) - self.assertTrue(webelem.is_editable(elem)) + elem = get_webelem(tagname='input', attributes={'type': 'email'}) + self.assertTrue(elem.is_editable()) def test_input_url(self): """Test with url input element.""" - elem = stubs.FakeWebElement(tagname='input', - attributes={'type': 'url'}) - self.assertTrue(webelem.is_editable(elem)) + elem = get_webelem(tagname='input', attributes={'type': 'url'}) + self.assertTrue(elem.is_editable()) def test_input_tel(self): """Test with tel input element.""" - elem = stubs.FakeWebElement(tagname='input', - attributes={'type': 'tel'}) - self.assertTrue(webelem.is_editable(elem)) + elem = get_webelem(tagname='input', attributes={'type': 'tel'}) + self.assertTrue(elem.is_editable()) def test_input_number(self): """Test with number input element.""" - elem = stubs.FakeWebElement(tagname='input', - attributes={'type': 'number'}) - self.assertTrue(webelem.is_editable(elem)) + elem = get_webelem(tagname='input', attributes={'type': 'number'}) + self.assertTrue(elem.is_editable()) def test_input_password(self): """Test with password input element.""" - elem = stubs.FakeWebElement(tagname='input', - attributes={'type': 'password'}) - self.assertTrue(webelem.is_editable(elem)) + elem = get_webelem(tagname='input', attributes={'type': 'password'}) + self.assertTrue(elem.is_editable()) def test_input_search(self): """Test with search input element.""" - elem = stubs.FakeWebElement(tagname='input', - attributes={'type': 'search'}) - self.assertTrue(webelem.is_editable(elem)) + elem = get_webelem(tagname='input', attributes={'type': 'search'}) + self.assertTrue(elem.is_editable()) def test_input_button(self): """Button should not be editable.""" - elem = stubs.FakeWebElement(tagname='input', - attributes={'type': 'button'}) - self.assertFalse(webelem.is_editable(elem)) + elem = get_webelem(tagname='input', attributes={'type': 'button'}) + self.assertFalse(elem.is_editable()) def test_input_checkbox(self): """Checkbox should not be editable.""" - elem = stubs.FakeWebElement(tagname='input', - attributes={'type': 'checkbox'}) - self.assertFalse(webelem.is_editable(elem)) + elem = get_webelem(tagname='input', attributes={'type': 'checkbox'}) + self.assertFalse(elem.is_editable()) def test_textarea(self): """Test textarea element.""" - elem = stubs.FakeWebElement(tagname='textarea') - self.assertTrue(webelem.is_editable(elem)) + elem = get_webelem(tagname='textarea') + self.assertTrue(elem.is_editable()) def test_select(self): """Test selectbox.""" - elem = stubs.FakeWebElement(tagname='select') - self.assertFalse(webelem.is_editable(elem)) + elem = get_webelem(tagname='select') + self.assertFalse(elem.is_editable()) def test_input_disabled(self): """Test disabled input element.""" - elem = stubs.FakeWebElement(tagname='input', - attributes={'disabled': None}) - self.assertFalse(webelem.is_editable(elem)) + elem = get_webelem(tagname='input', attributes={'disabled': None}) + self.assertFalse(elem.is_editable()) def test_input_readonly(self): """Test readonly input element.""" - elem = stubs.FakeWebElement(tagname='input', - attributes={'readonly': None}) - self.assertFalse(webelem.is_editable(elem)) + elem = get_webelem(tagname='input', attributes={'readonly': None}) + self.assertFalse(elem.is_editable()) def test_textarea_disabled(self): """Test disabled textarea element.""" - elem = stubs.FakeWebElement(tagname='textarea', - attributes={'disabled': None}) - self.assertFalse(webelem.is_editable(elem)) + elem = get_webelem(tagname='textarea', attributes={'disabled': None}) + self.assertFalse(elem.is_editable()) def test_textarea_readonly(self): """Test readonly textarea element.""" - elem = stubs.FakeWebElement(tagname='textarea', - attributes={'readonly': None}) - self.assertFalse(webelem.is_editable(elem)) + elem = get_webelem(tagname='textarea', attributes={'readonly': None}) + self.assertFalse(elem.is_editable()) def test_embed_true(self): """Test embed-element with insert-mode-on-plugins true.""" webelem.config = stubs.ConfigStub({'input': {'insert-mode-on-plugins': True}}) - elem = stubs.FakeWebElement(tagname='embed') - self.assertTrue(webelem.is_editable(elem)) + elem = get_webelem(tagname='embed') + self.assertTrue(elem.is_editable()) def test_applet_true(self): """Test applet-element with insert-mode-on-plugins true.""" webelem.config = stubs.ConfigStub({'input': {'insert-mode-on-plugins': True}}) - elem = stubs.FakeWebElement(tagname='applet') - self.assertTrue(webelem.is_editable(elem)) + elem = get_webelem(tagname='applet') + self.assertTrue(elem.is_editable()) def test_embed_false(self): """Test embed-element with insert-mode-on-plugins false.""" webelem.config = stubs.ConfigStub({'input': {'insert-mode-on-plugins': False}}) - elem = stubs.FakeWebElement(tagname='embed') - self.assertFalse(webelem.is_editable(elem)) + elem = get_webelem(tagname='embed') + self.assertFalse(elem.is_editable()) def test_applet_false(self): """Test applet-element with insert-mode-on-plugins false.""" webelem.config = stubs.ConfigStub({'input': {'insert-mode-on-plugins': False}}) - elem = stubs.FakeWebElement(tagname='applet') - self.assertFalse(webelem.is_editable(elem)) + elem = get_webelem(tagname='applet') + self.assertFalse(elem.is_editable()) def test_object_no_type(self): """Test object-element without type.""" - elem = stubs.FakeWebElement(tagname='object') - self.assertFalse(webelem.is_editable(elem)) + elem = get_webelem(tagname='object') + self.assertFalse(elem.is_editable()) def test_object_image(self): """Test object-element with image type.""" - elem = stubs.FakeWebElement(tagname='object', - attributes={'type': 'image/gif'}) - self.assertFalse(webelem.is_editable(elem)) + elem = get_webelem(tagname='object', attributes={'type': 'image/gif'}) + self.assertFalse(elem.is_editable()) def test_object_application(self): """Test object-element with application type.""" webelem.config = stubs.ConfigStub({'input': {'insert-mode-on-plugins': True}}) - elem = stubs.FakeWebElement(tagname='object', - attributes={'type': 'application/foo'}) - self.assertTrue(webelem.is_editable(elem)) + elem = get_webelem(tagname='object', + attributes={'type': 'application/foo'}) + self.assertTrue(elem.is_editable()) def test_object_application_false(self): """Test object-element with application type but not ...-on-plugins.""" webelem.config = stubs.ConfigStub({'input': {'insert-mode-on-plugins': False}}) - elem = stubs.FakeWebElement(tagname='object', - attributes={'type': 'application/foo'}) - self.assertFalse(webelem.is_editable(elem)) + elem = get_webelem(tagname='object', + attributes={'type': 'application/foo'}) + self.assertFalse(elem.is_editable()) def test_object_classid(self): """Test object-element with classid.""" webelem.config = stubs.ConfigStub({'input': {'insert-mode-on-plugins': True}}) - elem = stubs.FakeWebElement(tagname='object', - attributes={'type': 'foo', - 'classid': 'foo'}) - self.assertTrue(webelem.is_editable(elem)) + elem = get_webelem(tagname='object', + attributes={'type': 'foo', 'classid': 'foo'}) + self.assertTrue(elem.is_editable()) def test_object_classid_false(self): """Test object-element with classid but not insert-mode-on-plugins.""" webelem.config = stubs.ConfigStub({'input': {'insert-mode-on-plugins': False}}) - elem = stubs.FakeWebElement(tagname='object', - attributes={'type': 'foo', - 'classid': 'foo'}) - self.assertFalse(webelem.is_editable(elem)) + elem = get_webelem(tagname='object', + attributes={'type': 'foo', 'classid': 'foo'}) + self.assertFalse(elem.is_editable()) def test_div_empty(self): """Test div-element without class.""" - elem = stubs.FakeWebElement(tagname='div') - self.assertFalse(webelem.is_editable(elem)) + elem = get_webelem(tagname='div') + self.assertFalse(elem.is_editable()) def test_div_noneditable(self): """Test div-element with non-editableclass.""" - elem = stubs.FakeWebElement(tagname='div', classes='foo-kix-bar') - self.assertFalse(webelem.is_editable(elem)) + elem = get_webelem(tagname='div', classes='foo-kix-bar') + self.assertFalse(elem.is_editable()) def test_div_xik(self): """Test div-element with xik class.""" - elem = stubs.FakeWebElement(tagname='div', classes='foo kix-foo') - self.assertTrue(webelem.is_editable(elem)) + elem = get_webelem(tagname='div', classes='foo kix-foo') + self.assertTrue(elem.is_editable()) def test_div_xik_caps(self): """Test div-element with xik class in caps. This tests if classes are case sensitive as they should. """ - elem = stubs.FakeWebElement(tagname='div', classes='KIX-FOO') - self.assertFalse(webelem.is_editable(elem)) + elem = get_webelem(tagname='div', classes='KIX-FOO') + self.assertFalse(elem.is_editable()) def test_div_codemirror(self): """Test div-element with codemirror class.""" - elem = stubs.FakeWebElement(tagname='div', - classes='foo CodeMirror-foo') - self.assertTrue(webelem.is_editable(elem)) + elem = get_webelem(tagname='div', classes='foo CodeMirror-foo') + self.assertTrue(elem.is_editable()) if __name__ == '__main__': diff --git a/qutebrowser/utils/webelem.py b/qutebrowser/utils/webelem.py index dcc2b7276..b0f637909 100644 --- a/qutebrowser/utils/webelem.py +++ b/qutebrowser/utils/webelem.py @@ -27,6 +27,9 @@ Module attributes: without "href". """ +import collections.abc +import functools + from PyQt5.QtCore import QRect, QUrl from PyQt5.QtWebKit import QWebElement @@ -50,80 +53,278 @@ SELECTORS = { } FILTERS = { - Group.links: (lambda e: e.hasAttribute('href') and - QUrl(e.attribute('href')).scheme() != 'javascript'), + Group.links: (lambda e: 'href' in e and + QUrl(e['href']).scheme() != 'javascript'), } -def is_visible(elem, mainframe): - """Check whether the element is currently visible on the screen. +class IsNullError(Exception): - Args: - elem: The QWebElement to check. - mainframe: The main QWebFrame. + """Gets raised by WebElementWrapper if an element is null.""" - Return: - True if the element is visible, False otherwise. - """ - # CSS attributes which hide an element - hidden_attributes = { - 'visibility': 'hidden', - 'display': 'none', - } - if elem.isNull(): - raise ValueError("Element is a null-element!") - for k, v in hidden_attributes.items(): - if elem.styleProperty(k, QWebElement.ComputedStyle) == v: + pass + + +class WebElementWrapper(collections.abc.MutableMapping): + + """A wrapper around QWebElement to make it more intelligent.""" + + def __init__(self, elem): + 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) + + @functools.wraps(method) + def _wrapper(meth, *args, **kwargs): + # pylint: disable=missing-docstring + self._check_vanished() + return meth(*args, **kwargs) + + wrapper = functools.partial(_wrapper, method) + functools.update_wrapper(wrapper, method) + + setattr(self, name, wrapper) + + def __str__(self): + self._check_vanished() + return self._elem.toPlainText() + + def __repr__(self): + try: + return "".format(self.debug_text()) + except IsNullError: + return "" + + def __getitem__(self, key): + self._check_vanished() + if key not in self: + raise KeyError(key) + return self._elem.attribute(key) + + def __setitem__(self, key, val): + self._check_vanished() + self._elem.setAttribute(key, val) + + def __delitem__(self, key): + self._check_vanished() + if key not in self: + raise KeyError(key) + self.removeAttribute(key) + + def __contains__(self, key): + self._check_vanished() + return self._elem.hasAttribute(key) + + def __iter__(self): + self._check_vanished() + yield from self._elem.attributeNames() + + def __len__(self): + self._check_vanished() + return len(self._elem.attributeNames()) + + def _check_vanished(self): + """Raise an exception if the element vanished (is null).""" + 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. + + Args: + mainframe: The main QWebFrame. + + Return: + True if the element is visible, False otherwise. + """ + 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 + geometry = self._elem.geometry() + if not geometry.isValid() and geometry.x() == 0: + # Most likely an invisible link return False - 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) - 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() - elem_rect = elem.geometry() - 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()) + # First check if the element is visible on screen + elem_rect = self.rect_on_view() if elem_rect.isValid(): - visible_in_frame = framegeom.intersects(elem_rect) + 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_in_frame = framegeom.contains(elem_rect.topLeft()) - else: - visible_in_frame = visible_on_screen - return all([visible_on_screen, visible_in_frame]) + 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() + elem_rect = self._elem.geometry() + 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_rect.isValid(): + visible_in_frame = framegeom.intersects(elem_rect) + 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_rect.topLeft()) + else: + visible_in_frame = visible_on_screen + return all([visible_on_screen, visible_in_frame]) + def rect_on_view(self): + """Get the geometry of the element relative to the webview.""" + self._check_vanished() + frame = self._elem.webFrame() + rect = QRect(self._elem.geometry()) + while frame is not None: + rect.translate(frame.geometry().topLeft()) + rect.translate(frame.scrollPosition() * -1) + frame = frame.parentFrame() + return rect -def rect_on_view(elem): - """Get the geometry of the element relative to the webview.""" - frame = elem.webFrame() - rect = QRect(elem.geometry()) - while frame is not None: - rect.translate(frame.geometry().topLeft()) - rect.translate(frame.scrollPosition() * -1) - frame = frame.parentFrame() - return rect + def is_writable(self): + """Check whether an element is writable.""" + self._check_vanished() + return not ('disabled' in self or 'readonly' in self) + def is_content_editable(self): + """Check if an element has a contenteditable attribute. -def is_writable(elem): - """Check wheter an element is writable. + FIXME: Add tests. - Args: - elem: The QWebElement to check. - """ - return not (elem.hasAttribute('disabled') or elem.hasAttribute('readonly')) + Args: + elem: The QWebElement to check. + + Return: + True if the element has a contenteditable attribute, + False otherwise. + """ + self._check_vanished() + try: + return self['contenteditable'].lower() not in ('false', 'inherit') + except KeyError: + return False + + def _is_editable_object(self): + """Check if an object-element is editable.""" + if 'type' not in self: + log.webview.debug(" without type clicked...") + return False + objtype = self['type'].lower() + if objtype.startswith('application/') or 'classid' in self: + # Let's hope flash/java stuff has an application/* mimetype OR + # at least a classid attribute. Oh, and let's hope images/... + # DON'T have a classid attribute. HTML sucks. + log.webview.debug(" clicked.".format(objtype)) + return config.get('input', 'insert-mode-on-plugins') + else: + # Image/Audio/... + return False + + def _is_editable_input(self): + """Check if an input-element is editable. + + Return: + True if the element is editable, False otherwise. + """ + try: + objtype = self['type'].lower() + except KeyError: + return self.is_writable() + else: + if objtype in ['text', 'email', 'url', 'tel', 'number', 'password', + 'search']: + return self.is_writable() + else: + return False + + def _is_editable_div(self): + """Check if a div-element is editable. + + Return: + True if the element is editable, False otherwise. + """ + # Beginnings of div-classes which are actually some kind of editor. + div_classes = ('CodeMirror', # Javascript editor over a textarea + 'kix-', # Google Docs editor + 'ace_') # http://ace.c9.io/ + for klass in self._elem.classes(): + if any([klass.startswith(e) for e in div_classes]): + return True + + def is_editable(self, strict=False): + """Check whether we should switch to insert mode for this element. + + FIXME: add tests + + Args: + strict: Whether to do stricter checking so only fields where we can + get the value match, for use with the :editor command. + + Return: + True if we should switch to insert mode, False otherwise. + """ + # pylint: disable=too-many-return-statements + self._check_vanished() + roles = ('combobox', 'textbox') + log.misc.debug("Checking if element is editable: {}".format( + self.debug_text())) + tag = self._elem.tagName().lower() + if self.is_content_editable() and self.is_writable(): + return True + elif self.get('role', None) in roles: + return True + elif tag == 'input': + return self._is_editable_input() + elif tag == 'textarea': + return self.is_writable() + elif tag in ('embed', 'applet'): + # Flash/Java/... + return config.get('input', 'insert-mode-on-plugins') and not strict + elif tag == 'object': + return self._is_editable_object() and not strict + elif tag == 'div': + return self._is_editable_div() and not strict + else: + return False + + def debug_text(self): + """Get a text based on an element suitable for debug output.""" + self._check_vanished() + return utils.compact_text(repr(self._elem), 500) def javascript_escape(text): @@ -168,108 +369,6 @@ def get_child_frames(startframe): return results -def is_content_editable(elem): - """Check if an element hsa a contenteditable attribute. - - FIXME: Add tests. - - Args: - elem: The QWebElement to check. - - Return: - True if the element has a contenteditable attribute, False otherwise. - """ - return (elem.hasAttribute('contenteditable') and - elem.attribute('contenteditable') not in ('false', 'inherit')) - - -def _is_editable_object(elem): - """Check if an object-element is editable.""" - if not elem.hasAttribute('type'): - log.webview.debug(" without type clicked...") - return False - objtype = elem.attribute('type').lower() - if objtype.startswith('application/') or elem.hasAttribute('classid'): - # Let's hope flash/java stuff has an application/* mimetype OR - # at least a classid attribute. Oh, and let's hope images/... - # DON'T have a classid attribute. HTML sucks. - log.webview.debug(" clicked.".format(objtype)) - return config.get('input', 'insert-mode-on-plugins') - else: - # Image/Audio/... - return False - - -def _is_editable_input(elem): - """Check if an input-element is editable. - - Args: - elem: The QWebElement to check. - - Return: - True if the element is editable, False otherwise. - """ - objtype = elem.attribute('type').lower() - if objtype in ['text', 'email', 'url', 'tel', 'number', 'password', - 'search', '']: - return is_writable(elem) - - -def _is_editable_div(elem): - """Check if a div-element is editable. - - Args: - elem: The QWebElement to check. - - Return: - True if the element is editable, False otherwise. - """ - # Beginnings of div-classes which are actually some kind of editor. - div_classes = ('CodeMirror', # Javascript editor over a textarea - 'kix-', # Google Docs editor - 'ace_') # http://ace.c9.io/ - for klass in elem.classes(): - if any([klass.startswith(e) for e in div_classes]): - return True - - -def is_editable(elem, strict=False): - """Check whether we should switch to insert mode for this element. - - FIXME: add tests - - Args: - elem: The QWebElement to check. - strict: Whether to do stricter checking so only fields where we can get - the value match, for use with the :editor command. - - Return: - True if we should switch to insert mode, False otherwise. - """ - # pylint: disable=too-many-return-statements - roles = ('combobox', 'textbox') - log.misc.debug("Checking if element is editable: {}".format( - debug_text(elem))) - tag = elem.tagName().lower() - if is_content_editable(elem) and is_writable(elem): - return True - elif elem.hasAttribute('role') and elem.attribute('role') in roles: - return True - elif tag == 'input': - return _is_editable_input(elem) - elif tag == 'textarea': - return is_writable(elem) - elif tag in ('embed', 'applet'): - # Flash/Java/... - return config.get('input', 'insert-mode-on-plugins') and not strict - elif tag == 'object': - return _is_editable_object(elem) and not strict - elif tag == 'div': - return _is_editable_div(elem) and not strict - else: - return False - - def focus_elem(frame): """Get the focused element in a webframe. @@ -278,9 +377,5 @@ def focus_elem(frame): Args: frame: The QWebFrame to search in. """ - return frame.findFirstElement(SELECTORS[Group.focus]) - - -def debug_text(elem): - """Get a text based on an element suitable for debug output.""" - return utils.compact_text(elem.toOuterXml(), 500) + elem = frame.findFirstElement(SELECTORS[Group.focus]) + return WebElementWrapper(elem) diff --git a/qutebrowser/widgets/webview.py b/qutebrowser/widgets/webview.py index a469ff822..34b8f3016 100644 --- a/qutebrowser/widgets/webview.py +++ b/qutebrowser/widgets/webview.py @@ -177,8 +177,9 @@ class WebView(QWebView): log.mouse.debug("Hitresult is null!") self._check_insertmode = True return - elem = hitresult.element() - if elem.isNull(): + try: + elem = webelem.WebElementWrapper(hitresult.element()) + except webelem.IsNullError: # For some reason, the hitresult 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 @@ -186,8 +187,8 @@ class WebView(QWebView): log.mouse.debug("Hitresult element is null!") self._check_insertmode = True return - elif ((hitresult.isContentEditable() and webelem.is_writable(elem)) or - webelem.is_editable(elem)): + if ((hitresult.isContentEditable() and elem.is_writable()) or + elem.is_editable(elem)): log.mouse.debug("Clicked editable element!") modeman.maybe_enter(usertypes.KeyMode.insert, 'click') else: @@ -200,8 +201,12 @@ class WebView(QWebView): if not self._check_insertmode: return self._check_insertmode = False - elem = webelem.focus_elem(self.page().currentFrame()) - if webelem.is_editable(elem): + try: + elem = webelem.focus_elem(self.page().currentFrame()) + except webelem.IsNullError: + log.mouse.warning("Element vanished!") + return + if elem.is_editable(): log.mouse.debug("Clicked editable element (delayed)!") modeman.maybe_enter(usertypes.KeyMode.insert, 'click-delayed') else: @@ -353,11 +358,13 @@ class WebView(QWebView): if modeman.instance().mode() == usertypes.KeyMode.insert or not ok: return frame = self.page().currentFrame() - elem = frame.findFirstElement(':focus') - log.modes.debug("focus element: {}".format(elem.toOuterXml())) - if elem.isNull(): + try: + elem = webelem.WebElementWrapper(frame.findFirstElement(':focus')) + except webelem.IsNullError: log.webview.debug("Focused element is null!") - elif webelem.is_editable(elem): + return + log.modes.debug("focus element: {}".format(repr(elem))) + if elem.is_editable(): modeman.maybe_enter(usertypes.KeyMode.insert, 'load finished') @pyqtSlot(str)