Add type annotations for webelem/webkitelem/webengineelem
This commit is contained in:
parent
7c486a76f8
commit
81375b3029
12
mypy.ini
12
mypy.ini
@ -73,3 +73,15 @@ disallow_incomplete_defs = True
|
||||
[mypy-qutebrowser.extensions.*]
|
||||
disallow_untyped_defs = True
|
||||
disallow_incomplete_defs = True
|
||||
|
||||
[mypy-qutebrowser.browser.webelem]
|
||||
disallow_untyped_defs = True
|
||||
disallow_incomplete_defs = True
|
||||
|
||||
[mypy-qutebrowser.browser.webkit.webkitelem]
|
||||
disallow_untyped_defs = True
|
||||
disallow_incomplete_defs = True
|
||||
|
||||
[mypy-qutebrowser.browser.webengine.webengineelem]
|
||||
disallow_untyped_defs = True
|
||||
disallow_incomplete_defs = True
|
||||
|
@ -19,15 +19,23 @@
|
||||
|
||||
"""Generic web element related code."""
|
||||
|
||||
import typing
|
||||
import collections.abc
|
||||
|
||||
from PyQt5.QtCore import QUrl, Qt, QEvent, QTimer
|
||||
from PyQt5.QtCore import QUrl, Qt, QEvent, QTimer, QRect, QPoint
|
||||
from PyQt5.QtGui import QMouseEvent
|
||||
|
||||
from qutebrowser.config import config
|
||||
from qutebrowser.keyinput import modeman
|
||||
from qutebrowser.mainwindow import mainwindow
|
||||
from qutebrowser.utils import log, usertypes, utils, qtutils, objreg
|
||||
MYPY = False
|
||||
if MYPY:
|
||||
# pylint: disable=unused-import,useless-suppression
|
||||
from qutebrowser.browser import browsertab
|
||||
|
||||
|
||||
JsValueType = typing.Union[int, float, str, None]
|
||||
|
||||
|
||||
class Error(Exception):
|
||||
@ -40,7 +48,7 @@ class OrphanedError(Error):
|
||||
"""Raised when a webelement's parent has vanished."""
|
||||
|
||||
|
||||
def css_selector(group, url):
|
||||
def css_selector(group: str, url: QUrl) -> str:
|
||||
"""Get a CSS selector for the given group/URL."""
|
||||
selectors = config.instance.get('hints.selectors', url)
|
||||
if group not in selectors:
|
||||
@ -60,70 +68,72 @@ class AbstractWebElement(collections.abc.MutableMapping):
|
||||
tab: The tab associated with this element.
|
||||
"""
|
||||
|
||||
def __init__(self, tab):
|
||||
def __init__(self, tab: 'browsertab.AbstractTab') -> None:
|
||||
self._tab = tab
|
||||
|
||||
def __eq__(self, other):
|
||||
def __eq__(self, other: object) -> bool:
|
||||
raise NotImplementedError
|
||||
|
||||
def __str__(self):
|
||||
def __str__(self) -> str:
|
||||
raise NotImplementedError
|
||||
|
||||
def __getitem__(self, key):
|
||||
def __getitem__(self, key: str) -> str:
|
||||
raise NotImplementedError
|
||||
|
||||
def __setitem__(self, key, val):
|
||||
def __setitem__(self, key: str, val: str) -> None:
|
||||
raise NotImplementedError
|
||||
|
||||
def __delitem__(self, key):
|
||||
def __delitem__(self, key: str) -> None:
|
||||
raise NotImplementedError
|
||||
|
||||
def __iter__(self):
|
||||
def __iter__(self) -> typing.Iterator[str]:
|
||||
raise NotImplementedError
|
||||
|
||||
def __len__(self):
|
||||
def __len__(self) -> int:
|
||||
raise NotImplementedError
|
||||
|
||||
def __repr__(self):
|
||||
def __repr__(self) -> str:
|
||||
try:
|
||||
html = utils.compact_text(self.outer_xml(), 500)
|
||||
except Error:
|
||||
html = None
|
||||
return utils.get_repr(self, html=html)
|
||||
|
||||
def has_frame(self):
|
||||
def has_frame(self) -> bool:
|
||||
"""Check if this element has a valid frame attached."""
|
||||
raise NotImplementedError
|
||||
|
||||
def geometry(self):
|
||||
def geometry(self) -> QRect:
|
||||
"""Get the geometry for this element."""
|
||||
raise NotImplementedError
|
||||
|
||||
def classes(self):
|
||||
def classes(self) -> typing.List[str]:
|
||||
"""Get a list of classes assigned to this element."""
|
||||
raise NotImplementedError
|
||||
|
||||
def tag_name(self):
|
||||
def tag_name(self) -> str:
|
||||
"""Get the tag name of this element.
|
||||
|
||||
The returned name will always be lower-case.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def outer_xml(self):
|
||||
def outer_xml(self) -> str:
|
||||
"""Get the full HTML representation of this element."""
|
||||
raise NotImplementedError
|
||||
|
||||
def value(self):
|
||||
def value(self) -> JsValueType:
|
||||
"""Get the value attribute for this element, or None."""
|
||||
raise NotImplementedError
|
||||
|
||||
def set_value(self, value):
|
||||
def set_value(self, value: JsValueType) -> None:
|
||||
"""Set the element value."""
|
||||
raise NotImplementedError
|
||||
|
||||
def dispatch_event(self, event, bubbles=False,
|
||||
cancelable=False, composed=False):
|
||||
def dispatch_event(self, event: str,
|
||||
bubbles: bool = False,
|
||||
cancelable: bool = False,
|
||||
composed: bool = False) -> None:
|
||||
"""Dispatch an event to the element.
|
||||
|
||||
Args:
|
||||
@ -134,11 +144,12 @@ class AbstractWebElement(collections.abc.MutableMapping):
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def insert_text(self, text):
|
||||
def insert_text(self, text: str) -> None:
|
||||
"""Insert the given text into the element."""
|
||||
raise NotImplementedError
|
||||
|
||||
def rect_on_view(self, *, elem_geometry=None, no_js=False):
|
||||
def rect_on_view(self, *, elem_geometry: QRect = None,
|
||||
no_js: bool = False) -> QRect:
|
||||
"""Get the geometry of the element relative to the webview.
|
||||
|
||||
Args:
|
||||
@ -147,11 +158,11 @@ class AbstractWebElement(collections.abc.MutableMapping):
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def is_writable(self):
|
||||
def is_writable(self) -> bool:
|
||||
"""Check whether an element is writable."""
|
||||
return not ('disabled' in self or 'readonly' in self)
|
||||
|
||||
def is_content_editable(self):
|
||||
def is_content_editable(self) -> bool:
|
||||
"""Check if an element has a contenteditable attribute.
|
||||
|
||||
Args:
|
||||
@ -166,7 +177,7 @@ class AbstractWebElement(collections.abc.MutableMapping):
|
||||
except KeyError:
|
||||
return False
|
||||
|
||||
def _is_editable_object(self):
|
||||
def _is_editable_object(self) -> bool:
|
||||
"""Check if an object-element is editable."""
|
||||
if 'type' not in self:
|
||||
log.webelem.debug("<object> without type clicked...")
|
||||
@ -182,7 +193,7 @@ class AbstractWebElement(collections.abc.MutableMapping):
|
||||
# Image/Audio/...
|
||||
return False
|
||||
|
||||
def _is_editable_input(self):
|
||||
def _is_editable_input(self) -> bool:
|
||||
"""Check if an input-element is editable.
|
||||
|
||||
Return:
|
||||
@ -199,7 +210,7 @@ class AbstractWebElement(collections.abc.MutableMapping):
|
||||
else:
|
||||
return False
|
||||
|
||||
def _is_editable_classes(self):
|
||||
def _is_editable_classes(self) -> bool:
|
||||
"""Check if an element is editable based on its classes.
|
||||
|
||||
Return:
|
||||
@ -218,7 +229,7 @@ class AbstractWebElement(collections.abc.MutableMapping):
|
||||
return True
|
||||
return False
|
||||
|
||||
def is_editable(self, strict=False):
|
||||
def is_editable(self, strict: bool = False) -> bool:
|
||||
"""Check whether we should switch to insert mode for this element.
|
||||
|
||||
Args:
|
||||
@ -249,17 +260,17 @@ class AbstractWebElement(collections.abc.MutableMapping):
|
||||
return self._is_editable_classes() and not strict
|
||||
return False
|
||||
|
||||
def is_text_input(self):
|
||||
def is_text_input(self) -> bool:
|
||||
"""Check if this element is some kind of text box."""
|
||||
roles = ('combobox', 'textbox')
|
||||
tag = self.tag_name()
|
||||
return self.get('role', None) in roles or tag in ['input', 'textarea']
|
||||
|
||||
def remove_blank_target(self):
|
||||
def remove_blank_target(self) -> None:
|
||||
"""Remove target from link."""
|
||||
raise NotImplementedError
|
||||
|
||||
def resolve_url(self, baseurl):
|
||||
def resolve_url(self, baseurl: QUrl) -> typing.Optional[QUrl]:
|
||||
"""Resolve the URL in the element's src/href attribute.
|
||||
|
||||
Args:
|
||||
@ -286,16 +297,16 @@ class AbstractWebElement(collections.abc.MutableMapping):
|
||||
qtutils.ensure_valid(url)
|
||||
return url
|
||||
|
||||
def is_link(self):
|
||||
def is_link(self) -> bool:
|
||||
"""Return True if this AbstractWebElement is a link."""
|
||||
href_tags = ['a', 'area', 'link']
|
||||
return self.tag_name() in href_tags and 'href' in self
|
||||
|
||||
def _requires_user_interaction(self):
|
||||
def _requires_user_interaction(self) -> bool:
|
||||
"""Return True if clicking this element needs user interaction."""
|
||||
raise NotImplementedError
|
||||
|
||||
def _mouse_pos(self):
|
||||
def _mouse_pos(self) -> QPoint:
|
||||
"""Get the position to click/hover."""
|
||||
# Click the center of the largest square fitting into the top/left
|
||||
# corner of the rectangle, this will help if part of the <a> element
|
||||
@ -311,35 +322,38 @@ class AbstractWebElement(collections.abc.MutableMapping):
|
||||
raise Error("Element position is out of view!")
|
||||
return pos
|
||||
|
||||
def _move_text_cursor(self):
|
||||
def _move_text_cursor(self) -> None:
|
||||
"""Move cursor to end after clicking."""
|
||||
raise NotImplementedError
|
||||
|
||||
def _click_fake_event(self, click_target):
|
||||
def _click_fake_event(self, click_target: usertypes.ClickTarget) -> None:
|
||||
"""Send a fake click event to the element."""
|
||||
pos = self._mouse_pos()
|
||||
|
||||
log.webelem.debug("Sending fake click to {!r} at position {} with "
|
||||
"target {}".format(self, pos, click_target))
|
||||
|
||||
modifiers = {
|
||||
target_modifiers = {
|
||||
usertypes.ClickTarget.normal: Qt.NoModifier,
|
||||
usertypes.ClickTarget.window: Qt.AltModifier | Qt.ShiftModifier,
|
||||
usertypes.ClickTarget.tab: Qt.ControlModifier,
|
||||
usertypes.ClickTarget.tab_bg: Qt.ControlModifier,
|
||||
}
|
||||
if config.val.tabs.background:
|
||||
modifiers[usertypes.ClickTarget.tab] |= Qt.ShiftModifier
|
||||
target_modifiers[usertypes.ClickTarget.tab] |= Qt.ShiftModifier
|
||||
else:
|
||||
modifiers[usertypes.ClickTarget.tab_bg] |= Qt.ShiftModifier
|
||||
target_modifiers[usertypes.ClickTarget.tab_bg] |= Qt.ShiftModifier
|
||||
|
||||
modifiers = typing.cast(Qt.KeyboardModifiers,
|
||||
target_modifiers[click_target])
|
||||
|
||||
events = [
|
||||
QMouseEvent(QEvent.MouseMove, pos, Qt.NoButton, Qt.NoButton,
|
||||
Qt.NoModifier),
|
||||
QMouseEvent(QEvent.MouseButtonPress, pos, Qt.LeftButton,
|
||||
Qt.LeftButton, modifiers[click_target]),
|
||||
Qt.LeftButton, modifiers),
|
||||
QMouseEvent(QEvent.MouseButtonRelease, pos, Qt.LeftButton,
|
||||
Qt.NoButton, modifiers[click_target]),
|
||||
Qt.NoButton, modifiers),
|
||||
]
|
||||
|
||||
for evt in events:
|
||||
@ -347,15 +361,15 @@ class AbstractWebElement(collections.abc.MutableMapping):
|
||||
|
||||
QTimer.singleShot(0, self._move_text_cursor)
|
||||
|
||||
def _click_editable(self, click_target):
|
||||
def _click_editable(self, click_target: usertypes.ClickTarget) -> None:
|
||||
"""Fake a click on an editable input field."""
|
||||
raise NotImplementedError
|
||||
|
||||
def _click_js(self, click_target):
|
||||
def _click_js(self, click_target: usertypes.ClickTarget) -> None:
|
||||
"""Fake a click by using the JS .click() method."""
|
||||
raise NotImplementedError
|
||||
|
||||
def _click_href(self, click_target):
|
||||
def _click_href(self, click_target: usertypes.ClickTarget) -> None:
|
||||
"""Fake a click on an element with a href by opening the link."""
|
||||
baseurl = self._tab.url()
|
||||
url = self.resolve_url(baseurl)
|
||||
@ -377,7 +391,8 @@ class AbstractWebElement(collections.abc.MutableMapping):
|
||||
else:
|
||||
raise ValueError("Unknown ClickTarget {}".format(click_target))
|
||||
|
||||
def click(self, click_target, *, force_event=False):
|
||||
def click(self, click_target: usertypes.ClickTarget, *,
|
||||
force_event: bool = False) -> None:
|
||||
"""Simulate a click on the element.
|
||||
|
||||
Args:
|
||||
@ -414,7 +429,7 @@ class AbstractWebElement(collections.abc.MutableMapping):
|
||||
else:
|
||||
raise ValueError("Unknown ClickTarget {}".format(click_target))
|
||||
|
||||
def hover(self):
|
||||
def hover(self) -> None:
|
||||
"""Simulate a mouse hover over the element."""
|
||||
pos = self._mouse_pos()
|
||||
event = QMouseEvent(QEvent.MouseMove, pos, Qt.NoButton, Qt.NoButton,
|
||||
|
@ -22,20 +22,27 @@
|
||||
|
||||
"""QtWebEngine specific part of the web element API."""
|
||||
|
||||
import typing
|
||||
|
||||
from PyQt5.QtCore import QRect, Qt, QPoint, QEventLoop
|
||||
from PyQt5.QtGui import QMouseEvent
|
||||
from PyQt5.QtWidgets import QApplication
|
||||
from PyQt5.QtWebEngineWidgets import QWebEngineSettings
|
||||
|
||||
from qutebrowser.utils import log, javascript, urlutils
|
||||
from qutebrowser.utils import log, javascript, urlutils, usertypes
|
||||
from qutebrowser.browser import webelem
|
||||
MYPY = False
|
||||
if MYPY:
|
||||
# pylint: disable=unused-import,useless-suppression
|
||||
from qutebrowser.browser.webengine import webenginetab
|
||||
|
||||
|
||||
class WebEngineElement(webelem.AbstractWebElement):
|
||||
|
||||
"""A web element for QtWebEngine, using JS under the hood."""
|
||||
|
||||
def __init__(self, js_dict, tab):
|
||||
def __init__(self, js_dict: typing.Dict[str, typing.Any],
|
||||
tab: 'webenginetab.WebEngineTab') -> None:
|
||||
super().__init__(tab)
|
||||
# Do some sanity checks on the data we get from JS
|
||||
js_dict_types = {
|
||||
@ -48,7 +55,7 @@ class WebEngineElement(webelem.AbstractWebElement):
|
||||
'rects': list,
|
||||
'attributes': dict,
|
||||
'caret_position': (int, type(None)),
|
||||
}
|
||||
} # type: typing.Dict[str, typing.Union[type, typing.Tuple[type,...]]]
|
||||
assert set(js_dict.keys()).issubset(js_dict_types.keys())
|
||||
for name, typ in js_dict_types.items():
|
||||
if name in js_dict and not isinstance(js_dict[name], typ):
|
||||
@ -73,50 +80,51 @@ class WebEngineElement(webelem.AbstractWebElement):
|
||||
self._id = js_dict['id']
|
||||
self._js_dict = js_dict
|
||||
|
||||
def __str__(self):
|
||||
def __str__(self) -> str:
|
||||
return self._js_dict.get('text', '')
|
||||
|
||||
def __eq__(self, other):
|
||||
def __eq__(self, other: object) -> bool:
|
||||
if not isinstance(other, WebEngineElement):
|
||||
return NotImplemented
|
||||
return self._id == other._id # pylint: disable=protected-access
|
||||
|
||||
def __getitem__(self, key):
|
||||
def __getitem__(self, key: str) -> str:
|
||||
attrs = self._js_dict['attributes']
|
||||
return attrs[key]
|
||||
|
||||
def __setitem__(self, key, val):
|
||||
def __setitem__(self, key: str, val: str) -> None:
|
||||
self._js_dict['attributes'][key] = val
|
||||
self._js_call('set_attribute', key, val)
|
||||
|
||||
def __delitem__(self, key):
|
||||
def __delitem__(self, key: str) -> None:
|
||||
log.stub()
|
||||
|
||||
def __iter__(self):
|
||||
def __iter__(self) -> typing.Iterator[str]:
|
||||
return iter(self._js_dict['attributes'])
|
||||
|
||||
def __len__(self):
|
||||
def __len__(self) -> int:
|
||||
return len(self._js_dict['attributes'])
|
||||
|
||||
def _js_call(self, name, *args, callback=None):
|
||||
def _js_call(self, name: str, *args: webelem.JsValueType,
|
||||
callback: typing.Callable[[typing.Any], None] = None) -> None:
|
||||
"""Wrapper to run stuff from webelem.js."""
|
||||
if self._tab.is_deleted():
|
||||
raise webelem.OrphanedError("Tab containing element vanished")
|
||||
js_code = javascript.assemble('webelem', name, self._id, *args)
|
||||
self._tab.run_js_async(js_code, callback=callback)
|
||||
|
||||
def has_frame(self):
|
||||
def has_frame(self) -> bool:
|
||||
return True
|
||||
|
||||
def geometry(self):
|
||||
def geometry(self) -> QRect:
|
||||
log.stub()
|
||||
return QRect()
|
||||
|
||||
def classes(self):
|
||||
def classes(self) -> typing.List[str]:
|
||||
"""Get a list of classes assigned to this element."""
|
||||
return self._js_dict['class_name'].split()
|
||||
|
||||
def tag_name(self):
|
||||
def tag_name(self) -> str:
|
||||
"""Get the tag name of this element.
|
||||
|
||||
The returned name will always be lower-case.
|
||||
@ -125,34 +133,37 @@ class WebEngineElement(webelem.AbstractWebElement):
|
||||
assert isinstance(tag, str), tag
|
||||
return tag.lower()
|
||||
|
||||
def outer_xml(self):
|
||||
def outer_xml(self) -> str:
|
||||
"""Get the full HTML representation of this element."""
|
||||
return self._js_dict['outer_xml']
|
||||
|
||||
def value(self):
|
||||
def value(self) -> webelem.JsValueType:
|
||||
return self._js_dict.get('value', None)
|
||||
|
||||
def set_value(self, value):
|
||||
def set_value(self, value: webelem.JsValueType) -> None:
|
||||
self._js_call('set_value', value)
|
||||
|
||||
def dispatch_event(self, event, bubbles=False,
|
||||
cancelable=False, composed=False):
|
||||
def dispatch_event(self, event: str,
|
||||
bubbles: bool = False,
|
||||
cancelable: bool = False,
|
||||
composed: bool = False) -> None:
|
||||
self._js_call('dispatch_event', event, bubbles, cancelable, composed)
|
||||
|
||||
def caret_position(self):
|
||||
def caret_position(self) -> typing.Optional[int]:
|
||||
"""Get the text caret position for the current element.
|
||||
|
||||
If the element is not a text element, None is returned.
|
||||
"""
|
||||
return self._js_dict.get('caret_position', None)
|
||||
|
||||
def insert_text(self, text):
|
||||
def insert_text(self, text: str) -> None:
|
||||
if not self.is_editable(strict=True):
|
||||
raise webelem.Error("Element is not editable!")
|
||||
log.webelem.debug("Inserting text into element {!r}".format(self))
|
||||
self._js_call('insert_text', text)
|
||||
|
||||
def rect_on_view(self, *, elem_geometry=None, no_js=False):
|
||||
def rect_on_view(self, *, elem_geometry: QRect = None,
|
||||
no_js: bool = False) -> QRect:
|
||||
"""Get the geometry of the element relative to the webview.
|
||||
|
||||
Skipping of small rectangles is due to <a> elements containing other
|
||||
@ -193,16 +204,16 @@ class WebEngineElement(webelem.AbstractWebElement):
|
||||
self, rects))
|
||||
return QRect()
|
||||
|
||||
def remove_blank_target(self):
|
||||
def remove_blank_target(self) -> None:
|
||||
if self._js_dict['attributes'].get('target') == '_blank':
|
||||
self._js_dict['attributes']['target'] = '_top'
|
||||
self._js_call('remove_blank_target')
|
||||
|
||||
def _move_text_cursor(self):
|
||||
def _move_text_cursor(self) -> None:
|
||||
if self.is_text_input() and self.is_editable():
|
||||
self._js_call('move_cursor_to_end')
|
||||
|
||||
def _requires_user_interaction(self):
|
||||
def _requires_user_interaction(self) -> bool:
|
||||
baseurl = self._tab.url()
|
||||
url = self.resolve_url(baseurl)
|
||||
if url is None:
|
||||
@ -211,7 +222,7 @@ class WebEngineElement(webelem.AbstractWebElement):
|
||||
return False
|
||||
return url.scheme() not in urlutils.WEBENGINE_SCHEMES
|
||||
|
||||
def _click_editable(self, click_target):
|
||||
def _click_editable(self, click_target: usertypes.ClickTarget) -> None:
|
||||
# WORKAROUND for https://bugreports.qt.io/browse/QTBUG-58515
|
||||
ev = QMouseEvent(QMouseEvent.MouseButtonPress, QPoint(0, 0),
|
||||
QPoint(0, 0), QPoint(0, 0), Qt.NoButton, Qt.NoButton,
|
||||
@ -221,10 +232,11 @@ class WebEngineElement(webelem.AbstractWebElement):
|
||||
self._js_call('focus')
|
||||
self._move_text_cursor()
|
||||
|
||||
def _click_js(self, _click_target):
|
||||
def _click_js(self, _click_target: usertypes.ClickTarget) -> None:
|
||||
# FIXME:qtwebengine Have a proper API for this
|
||||
# pylint: disable=protected-access
|
||||
view = self._tab._widget
|
||||
assert view is not None
|
||||
# pylint: enable=protected-access
|
||||
attribute = QWebEngineSettings.JavascriptCanOpenWindows
|
||||
could_open_windows = view.settings().testAttribute(attribute)
|
||||
@ -238,8 +250,9 @@ class WebEngineElement(webelem.AbstractWebElement):
|
||||
qapp.processEvents(QEventLoop.ExcludeSocketNotifiers |
|
||||
QEventLoop.ExcludeUserInputEvents)
|
||||
|
||||
def reset_setting(_arg):
|
||||
def reset_setting(_arg: typing.Any) -> None:
|
||||
"""Set the JavascriptCanOpenWindows setting to its old value."""
|
||||
assert view is not None
|
||||
try:
|
||||
view.settings().setAttribute(attribute, could_open_windows)
|
||||
except RuntimeError:
|
||||
|
@ -19,12 +19,18 @@
|
||||
|
||||
"""QtWebKit specific part of the web element API."""
|
||||
|
||||
import typing
|
||||
|
||||
from PyQt5.QtCore import QRect
|
||||
from PyQt5.QtWebKit import QWebElement, QWebSettings
|
||||
from PyQt5.QtWebKitWidgets import QWebFrame
|
||||
|
||||
from qutebrowser.config import config
|
||||
from qutebrowser.utils import log, utils, javascript
|
||||
from qutebrowser.utils import log, utils, javascript, usertypes
|
||||
from qutebrowser.browser import webelem
|
||||
MYPY = False
|
||||
if MYPY:
|
||||
from qutebrowser.browser.webkit import webkittab
|
||||
|
||||
|
||||
class IsNullError(webelem.Error):
|
||||
@ -36,7 +42,7 @@ class WebKitElement(webelem.AbstractWebElement):
|
||||
|
||||
"""A wrapper around a QWebElement."""
|
||||
|
||||
def __init__(self, elem, tab):
|
||||
def __init__(self, elem: QWebElement, tab: webkittab.WebKitTab) -> None:
|
||||
super().__init__(tab)
|
||||
if isinstance(elem, self.__class__):
|
||||
raise TypeError("Trying to wrap a wrapper!")
|
||||
@ -44,90 +50,94 @@ class WebKitElement(webelem.AbstractWebElement):
|
||||
raise IsNullError('{} is a null element!'.format(elem))
|
||||
self._elem = elem
|
||||
|
||||
def __str__(self):
|
||||
def __str__(self) -> str:
|
||||
self._check_vanished()
|
||||
return self._elem.toPlainText()
|
||||
|
||||
def __eq__(self, other):
|
||||
def __eq__(self, other: object) -> bool:
|
||||
if not isinstance(other, WebKitElement):
|
||||
return NotImplemented
|
||||
return self._elem == other._elem # pylint: disable=protected-access
|
||||
|
||||
def __getitem__(self, key):
|
||||
def __getitem__(self, key: str) -> str:
|
||||
self._check_vanished()
|
||||
if key not in self:
|
||||
raise KeyError(key)
|
||||
return self._elem.attribute(key)
|
||||
|
||||
def __setitem__(self, key, val):
|
||||
def __setitem__(self, key: str, val: str) -> None:
|
||||
self._check_vanished()
|
||||
self._elem.setAttribute(key, val)
|
||||
|
||||
def __delitem__(self, key):
|
||||
def __delitem__(self, key: str) -> None:
|
||||
self._check_vanished()
|
||||
if key not in self:
|
||||
raise KeyError(key)
|
||||
self._elem.removeAttribute(key)
|
||||
|
||||
def __contains__(self, key):
|
||||
def __contains__(self, key: object) -> bool:
|
||||
assert isinstance(key, str)
|
||||
self._check_vanished()
|
||||
return self._elem.hasAttribute(key)
|
||||
|
||||
def __iter__(self):
|
||||
def __iter__(self) -> typing.Iterator[str]:
|
||||
self._check_vanished()
|
||||
yield from self._elem.attributeNames()
|
||||
|
||||
def __len__(self):
|
||||
def __len__(self) -> int:
|
||||
self._check_vanished()
|
||||
return len(self._elem.attributeNames())
|
||||
|
||||
def _check_vanished(self):
|
||||
def _check_vanished(self) -> None:
|
||||
"""Raise an exception if the element vanished (is null)."""
|
||||
if self._elem.isNull():
|
||||
raise IsNullError('Element {} vanished!'.format(self._elem))
|
||||
|
||||
def has_frame(self):
|
||||
def has_frame(self) -> bool:
|
||||
self._check_vanished()
|
||||
return self._elem.webFrame() is not None
|
||||
|
||||
def geometry(self):
|
||||
def geometry(self) -> QRect:
|
||||
self._check_vanished()
|
||||
return self._elem.geometry()
|
||||
|
||||
def classes(self):
|
||||
def classes(self) -> typing.List[str]:
|
||||
self._check_vanished()
|
||||
return self._elem.classes()
|
||||
|
||||
def tag_name(self):
|
||||
def tag_name(self) -> str:
|
||||
"""Get the tag name for the current element."""
|
||||
self._check_vanished()
|
||||
return self._elem.tagName().lower()
|
||||
|
||||
def outer_xml(self):
|
||||
def outer_xml(self) -> str:
|
||||
"""Get the full HTML representation of this element."""
|
||||
self._check_vanished()
|
||||
return self._elem.toOuterXml()
|
||||
|
||||
def value(self):
|
||||
def value(self) -> webelem.JsValueType:
|
||||
self._check_vanished()
|
||||
val = self._elem.evaluateJavaScript('this.value')
|
||||
assert isinstance(val, (int, float, str, type(None))), val
|
||||
return val
|
||||
|
||||
def set_value(self, value):
|
||||
def set_value(self, value: webelem.JsValueType) -> None:
|
||||
self._check_vanished()
|
||||
if self._tab.is_deleted():
|
||||
raise webelem.OrphanedError("Tab containing element vanished")
|
||||
if self.is_content_editable():
|
||||
log.webelem.debug("Filling {!r} via set_text.".format(self))
|
||||
assert isinstance(value, str)
|
||||
self._elem.setPlainText(value)
|
||||
else:
|
||||
log.webelem.debug("Filling {!r} via javascript.".format(self))
|
||||
value = javascript.to_js(value)
|
||||
self._elem.evaluateJavaScript("this.value={}".format(value))
|
||||
|
||||
def dispatch_event(self, event, bubbles=False,
|
||||
cancelable=False, composed=False):
|
||||
def dispatch_event(self, event: str,
|
||||
bubbles: bool = False,
|
||||
cancelable: bool = False,
|
||||
composed: bool = False) -> None:
|
||||
self._check_vanished()
|
||||
log.webelem.debug("Firing event on {!r} via javascript.".format(self))
|
||||
self._elem.evaluateJavaScript(
|
||||
@ -138,7 +148,7 @@ class WebKitElement(webelem.AbstractWebElement):
|
||||
javascript.to_js(cancelable),
|
||||
javascript.to_js(composed)))
|
||||
|
||||
def caret_position(self):
|
||||
def caret_position(self) -> int:
|
||||
"""Get the text caret position for the current element."""
|
||||
self._check_vanished()
|
||||
pos = self._elem.evaluateJavaScript('this.selectionStart')
|
||||
@ -146,7 +156,7 @@ class WebKitElement(webelem.AbstractWebElement):
|
||||
return 0
|
||||
return int(pos)
|
||||
|
||||
def insert_text(self, text):
|
||||
def insert_text(self, text: str) -> None:
|
||||
self._check_vanished()
|
||||
if not self.is_editable(strict=True):
|
||||
raise webelem.Error("Element is not editable!")
|
||||
@ -158,7 +168,7 @@ class WebKitElement(webelem.AbstractWebElement):
|
||||
this.dispatchEvent(event);
|
||||
""".format(javascript.to_js(text)))
|
||||
|
||||
def _parent(self):
|
||||
def _parent(self) -> typing.Optional['WebKitElement']:
|
||||
"""Get the parent element of this element."""
|
||||
self._check_vanished()
|
||||
elem = self._elem.parent()
|
||||
@ -166,7 +176,7 @@ class WebKitElement(webelem.AbstractWebElement):
|
||||
return None
|
||||
return WebKitElement(elem, tab=self._tab)
|
||||
|
||||
def _rect_on_view_js(self):
|
||||
def _rect_on_view_js(self) -> typing.Optional[QRect]:
|
||||
"""Javascript implementation for rect_on_view."""
|
||||
# FIXME:qtwebengine maybe we can reuse this?
|
||||
rects = self._elem.evaluateJavaScript("this.getClientRects()")
|
||||
@ -178,8 +188,8 @@ class WebKitElement(webelem.AbstractWebElement):
|
||||
return None
|
||||
|
||||
text = utils.compact_text(self._elem.toOuterXml(), 500)
|
||||
log.webelem.vdebug("Client rectangles of element '{}': {}".format(
|
||||
text, rects))
|
||||
log.webelem.vdebug( # type: ignore
|
||||
"Client rectangles of element '{}': {}".format(text, rects))
|
||||
|
||||
for i in range(int(rects.get("length", 0))):
|
||||
rect = rects[str(i)]
|
||||
@ -204,7 +214,8 @@ class WebKitElement(webelem.AbstractWebElement):
|
||||
|
||||
return None
|
||||
|
||||
def _rect_on_view_python(self, elem_geometry):
|
||||
def _rect_on_view_python(self,
|
||||
elem_geometry: typing.Optional[QRect]) -> QRect:
|
||||
"""Python implementation for rect_on_view."""
|
||||
if elem_geometry is None:
|
||||
geometry = self._elem.geometry()
|
||||
@ -218,7 +229,8 @@ class WebKitElement(webelem.AbstractWebElement):
|
||||
frame = frame.parentFrame()
|
||||
return rect
|
||||
|
||||
def rect_on_view(self, *, elem_geometry=None, no_js=False):
|
||||
def rect_on_view(self, *, elem_geometry: QRect = None,
|
||||
no_js: bool = False) -> QRect:
|
||||
"""Get the geometry of the element relative to the webview.
|
||||
|
||||
Uses the getClientRects() JavaScript method to obtain the collection of
|
||||
@ -248,7 +260,7 @@ class WebKitElement(webelem.AbstractWebElement):
|
||||
# No suitable rects found via JS, try via the QWebElement API
|
||||
return self._rect_on_view_python(elem_geometry)
|
||||
|
||||
def _is_visible(self, mainframe):
|
||||
def _is_visible(self, mainframe: QWebFrame) -> bool:
|
||||
"""Check if the given element is visible in the given frame.
|
||||
|
||||
This is not public API because it can't be implemented easily here with
|
||||
@ -300,8 +312,8 @@ class WebKitElement(webelem.AbstractWebElement):
|
||||
visible_in_frame = visible_on_screen
|
||||
return all([visible_on_screen, visible_in_frame])
|
||||
|
||||
def remove_blank_target(self):
|
||||
elem = self
|
||||
def remove_blank_target(self) -> None:
|
||||
elem = self # type: typing.Optional[WebKitElement]
|
||||
for _ in range(5):
|
||||
if elem is None:
|
||||
break
|
||||
@ -311,14 +323,14 @@ class WebKitElement(webelem.AbstractWebElement):
|
||||
break
|
||||
elem = elem._parent() # pylint: disable=protected-access
|
||||
|
||||
def _move_text_cursor(self):
|
||||
def _move_text_cursor(self) -> None:
|
||||
if self.is_text_input() and self.is_editable():
|
||||
self._tab.caret.move_to_end_of_document()
|
||||
|
||||
def _requires_user_interaction(self):
|
||||
def _requires_user_interaction(self) -> bool:
|
||||
return False
|
||||
|
||||
def _click_editable(self, click_target):
|
||||
def _click_editable(self, click_target: usertypes.ClickTarget) -> None:
|
||||
ok = self._elem.evaluateJavaScript('this.focus(); true;')
|
||||
if ok:
|
||||
self._move_text_cursor()
|
||||
@ -326,7 +338,7 @@ class WebKitElement(webelem.AbstractWebElement):
|
||||
log.webelem.debug("Failed to focus via JS, falling back to event")
|
||||
self._click_fake_event(click_target)
|
||||
|
||||
def _click_js(self, click_target):
|
||||
def _click_js(self, click_target: usertypes.ClickTarget) -> None:
|
||||
settings = QWebSettings.globalSettings()
|
||||
attribute = QWebSettings.JavascriptCanOpenWindows
|
||||
could_open_windows = settings.testAttribute(attribute)
|
||||
@ -337,12 +349,12 @@ class WebKitElement(webelem.AbstractWebElement):
|
||||
log.webelem.debug("Failed to click via JS, falling back to event")
|
||||
self._click_fake_event(click_target)
|
||||
|
||||
def _click_fake_event(self, click_target):
|
||||
def _click_fake_event(self, click_target: usertypes.ClickTarget) -> None:
|
||||
self._tab.data.override_target = click_target
|
||||
super()._click_fake_event(click_target)
|
||||
|
||||
|
||||
def get_child_frames(startframe):
|
||||
def get_child_frames(startframe: QWebFrame) -> typing.List[QWebFrame]:
|
||||
"""Get all children recursively of a given QWebFrame.
|
||||
|
||||
Loosely based on http://blog.nextgenetics.net/?e=64
|
||||
@ -356,7 +368,7 @@ def get_child_frames(startframe):
|
||||
results = []
|
||||
frames = [startframe]
|
||||
while frames:
|
||||
new_frames = []
|
||||
new_frames = [] # type: typing.List[QWebFrame]
|
||||
for frame in frames:
|
||||
results.append(frame)
|
||||
new_frames += frame.childFrames()
|
||||
|
Loading…
Reference in New Issue
Block a user