This commit is contained in:
Jan Verbeek 2016-08-09 14:36:01 +02:00
commit 8e4733f483
38 changed files with 1105 additions and 591 deletions

View File

@ -1 +0,0 @@
qutebrowser/3rdparty/pdfjs/*

View File

@ -1,49 +0,0 @@
# vim: ft=yaml
env:
browser: true
rules:
block-scoped-var: 2
dot-location: 2
default-case: 2
guard-for-in: 2
no-div-regex: 2
no-param-reassign: 2
no-eq-null: 2
no-floating-decimal: 2
no-self-compare: 2
no-throw-literal: 2
no-void: 2
radix: 2
wrap-iife: [2, "inside"]
brace-style: [2, "1tbs", {"allowSingleLine": true}]
comma-style: [2, "last"]
consistent-this: [2, "self"]
func-style: [2, "declaration"]
indent: [2, 4, {"SwitchCase": 1}]
linebreak-style: [2, "unix"]
max-nested-callbacks: [2, 3]
no-lonely-if: 2
no-multiple-empty-lines: [2, {"max": 2}]
no-nested-ternary: 2
no-unneeded-ternary: 2
operator-assignment: [2, "always"]
operator-linebreak: [2, "after"]
keyword-spacing: 2
space-before-blocks: [2, "always"]
space-before-function-paren: [2, {"anonymous": "never", "named": "never"}]
object-curly-spacing: [2, "never"]
array-bracket-spacing: [2, "never"]
computed-property-spacing: [2, "never"]
space-in-parens: [2, "never"]
space-unary-ops: [2, {"words": true, "nonwords": false}]
spaced-comment: [2, "always"]
max-depth: [2, 5]
max-len: [2, 79, 4]
max-params: [2, 5]
max-statements: [2, 30]
no-bitwise: 2
quote-props: [2, "always"]
global-strict: 0
quotes: 0

View File

@ -49,12 +49,16 @@ Changed
- Replacements like `{url}` can now also be used in the middle of an argument.
Consequently, commands taking another command (`:later`, `:repeat` and
`:bind`) now don't immediately evaluate variables.
- Tab titles in the `:buffer` completion now update correctly when a page's
title is changed via javascript.
Removed
~~~~~~~
- The `:yank-selected` command got merged into `:yank` as `:yank selection`
and thus removed.
- The `:completion-item-prev` and `:completion-item-next` commands got merged
into a new `:completion-focus {prev,next}` command and thus removed.
v0.8.3 (unreleased)
-------------------
@ -64,6 +68,7 @@ Fixed
- Fixed crash when doing `:<space><enter>`, another corner-case introduced in v0.8.0
- Fixed `:open-editor` (`<Ctrl-e>`) on Windows
- Fixed crash when setting `general -> auto-save-interval` to a too big value.
v0.8.2
------

View File

@ -35,8 +35,8 @@ exclude pytest.ini
exclude qutebrowser.rcc
exclude .coveragerc
exclude .pylintrc
exclude .eslintrc
exclude .eslintignore
exclude qutebrowser/javascript/.eslintrc.yaml
exclude qutebrowser/javascript/.eslintignore
exclude doc/help
exclude .appveyor.yml
exclude .travis.yml

View File

@ -212,6 +212,7 @@ Contributors, sorted by the number of commits in descending order:
* adam
* Samir Benmendil
* Regina Hug
* Niklas Haas
* Mathias Fussenegger
* Marcelo Santos
* Jean-Louis Fuchs
@ -225,6 +226,7 @@ Contributors, sorted by the number of commits in descending order:
* haxwithaxe
* evan
* dylan araps
* addictedtoflames
* Xitian9
* Tomas Orsava
* Tom Janson

View File

@ -1,3 +1,7 @@
// DO NOT EDIT THIS FILE DIRECTLY!
// It is autogenerated from docstrings by running:
// $ python3 scripts/dev/src2asciidoc.py
= Commands
== Normal commands
@ -901,8 +905,7 @@ How many steps to zoom out.
|<<command-history-next,command-history-next>>|Go forward in the commandline history.
|<<command-history-prev,command-history-prev>>|Go back in the commandline history.
|<<completion-item-del,completion-item-del>>|Delete the current completion item.
|<<completion-item-next,completion-item-next>>|Select the next completion item.
|<<completion-item-prev,completion-item-prev>>|Select the previous completion item.
|<<completion-item-focus,completion-item-focus>>|Shift the focus of the completion menu to another item.
|<<drop-selection,drop-selection>>|Drop selection and keep selection mode enabled.
|<<enter-mode,enter-mode>>|Enter a key mode.
|<<follow-hint,follow-hint>>|Follow a hint.
@ -978,13 +981,14 @@ Go back in the commandline history.
=== completion-item-del
Delete the current completion item.
[[completion-item-next]]
=== completion-item-next
Select the next completion item.
[[completion-item-focus]]
=== completion-item-focus
Syntax: +:completion-item-focus 'which'+
[[completion-item-prev]]
=== completion-item-prev
Select the previous completion item.
Shift the focus of the completion menu to another item.
==== positional arguments
* +'which'+: 'next' or 'prev'
[[drop-selection]]
=== drop-selection

View File

@ -1,3 +1,7 @@
// DO NOT EDIT THIS FILE DIRECTLY!
// It is autogenerated from docstrings by running:
// $ python3 scripts/dev/src2asciidoc.py
= Settings
.Quick reference for section ``general''
@ -87,8 +91,8 @@
[options="header",width="75%",cols="25%,75%"]
|==============
|Setting|Description
|<<input-timeout,timeout>>|Timeout for ambiguous key bindings.
|<<input-partial-timeout,partial-timeout>>|Timeout for partially typed key bindings.
|<<input-timeout,timeout>>|Timeout (in milliseconds) for ambiguous key bindings.
|<<input-partial-timeout,partial-timeout>>|Timeout (in milliseconds) for partially typed key bindings.
|<<input-insert-mode-on-plugins,insert-mode-on-plugins>>|Whether to switch to insert mode when clicking flash and other plugins.
|<<input-auto-leave-insert-mode,auto-leave-insert-mode>>|Whether to leave insert mode if a non-editable element is clicked.
|<<input-auto-insert-mode,auto-insert-mode>>|Whether to automatically enter insert mode if an editable element is focused after page load.
@ -182,7 +186,7 @@
|<<hints-uppercase,uppercase>>|Make chars in hint strings uppercase.
|<<hints-dictionary,dictionary>>|The dictionary file to be used by the word hints.
|<<hints-auto-follow,auto-follow>>|Follow a hint immediately when the hint text is completely matched.
|<<hints-auto-follow-timeout,auto-follow-timeout>>|A timeout to inhibit normal-mode key bindings after a successfulauto-follow.
|<<hints-auto-follow-timeout,auto-follow-timeout>>|A timeout (in milliseconds) to inhibit normal-mode key bindings after a successful auto-follow.
|<<hints-next-regexes,next-regexes>>|A comma-separated list of regexes to use for 'next' links.
|<<hints-prev-regexes,prev-regexes>>|A comma-separated list of regexes to use for 'prev' links.
|<<hints-find-implementation,find-implementation>>|Which implementation to use to find elements to hint.
@ -900,7 +904,7 @@ Options related to input modes.
[[input-timeout]]
=== timeout
Timeout for ambiguous key bindings.
Timeout (in milliseconds) for ambiguous key bindings.
If the current input forms both a complete match and a partial match, the complete match will be executed after this time.
@ -908,7 +912,7 @@ Default: +pass:[500]+
[[input-partial-timeout]]
=== partial-timeout
Timeout for partially typed key bindings.
Timeout (in milliseconds) for partially typed key bindings.
If the current input forms only partial matches, the keystring will be cleared after this time.
@ -1667,7 +1671,7 @@ Default: +pass:[true]+
[[hints-auto-follow-timeout]]
=== auto-follow-timeout
A timeout to inhibit normal-mode key bindings after a successfulauto-follow.
A timeout (in milliseconds) to inhibit normal-mode key bindings after a successful auto-follow.
Default: +pass:[0]+

View File

@ -2,4 +2,4 @@
codecov==2.0.5
coverage==4.2
requests==2.10.0
requests==2.11.0

View File

@ -21,5 +21,5 @@ pep8-naming==0.4.1
pycodestyle==2.0.0
pydocstyle==1.0.0
pyflakes==1.2.3
pyparsing==2.1.5
pyparsing==2.1.6
six==1.10.0

View File

@ -6,6 +6,6 @@ lazy-object-proxy==1.2.2
mccabe==0.5.2
-e git+https://github.com/PyCQA/pylint.git#egg=pylint
./scripts/dev/pylint_checkers
requests==2.10.0
requests==2.11.0
six==1.10.0
wrapt==1.10.8

View File

@ -7,7 +7,7 @@ lazy-object-proxy==1.2.2
mccabe==0.5.2
pylint==1.6.4
./scripts/dev/pylint_checkers
requests==2.10.0
requests==2.11.0
six==1.10.0
uritemplate.py==0.3.0
wrapt==1.10.8

View File

@ -18,7 +18,7 @@ py==1.4.31
pytest==2.9.2
pytest-bdd==2.17.0
pytest-catchlog==1.2.2
pytest-cov==2.3.0
pytest-cov==2.3.1
pytest-faulthandler==1.3.0
pytest-instafail==0.3.0
pytest-mock==1.2

View File

@ -613,6 +613,15 @@ class AbstractTab(QWidget):
"""
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 __repr__(self):
try:
url = utils.elide(self.url().toDisplayString(QUrl.EncodeUnicode),

View File

@ -40,7 +40,7 @@ import pygments.formatters
from qutebrowser.commands import userscripts, cmdexc, cmdutils, runners
from qutebrowser.config import config, configexc
from qutebrowser.browser import urlmarks, browsertab, inspector, navigate
from qutebrowser.browser.webkit import webelem, downloads, mhtml
from qutebrowser.browser.webkit import webkitelem, downloads, mhtml
from qutebrowser.keyinput import modeman
from qutebrowser.utils import (message, usertypes, log, qtutils, urlutils,
objreg, utils, typing, javascript)
@ -464,8 +464,7 @@ class CommandDispatcher:
"""
self._back_forward(tab, bg, window, count, forward=True)
@cmdutils.register(instance='command-dispatcher', scope='window',
backend=usertypes.Backend.QtWebKit)
@cmdutils.register(instance='command-dispatcher', scope='window')
@cmdutils.argument('where', choices=['prev', 'next', 'up', 'increment',
'decrement'])
def navigate(self, where: str, tab=False, bg=False, window=False):
@ -1432,6 +1431,21 @@ class CommandDispatcher:
url = QUrl('qute://log?level={}'.format(level))
self._open(url, tab, bg, window)
def _open_editor_cb(self, elem):
"""Open editor after the focus elem was found in open_editor."""
if elem is None:
message.error(self._win_id, "No element focused!")
return
if not elem.is_editable(strict=True):
message.error(self._win_id, "Focused element is not editable!")
return
text = elem.text(use_js=True)
ed = editor.ExternalEditor(self._win_id, self._tabbed_browser)
ed.editing_finished.connect(functools.partial(
self.on_editing_finished, elem))
ed.edit(text)
@cmdutils.register(instance='command-dispatcher',
modes=[KeyMode.insert], hide=True, scope='window',
backend=usertypes.Backend.QtWebKit)
@ -1441,20 +1455,8 @@ class CommandDispatcher:
The editor which should be launched can be configured via the
`general -> editor` config option.
"""
# FIXME:qtwebengine have a proper API for this
tab = self._current_widget()
page = tab._widget.page() # pylint: disable=protected-access
try:
elem = webelem.focus_elem(page.currentFrame())
except webelem.IsNullError:
raise cmdexc.CommandError("No element focused!")
if not elem.is_editable(strict=True):
raise cmdexc.CommandError("Focused element is not editable!")
text = elem.text(use_js=True)
ed = editor.ExternalEditor(self._win_id, self._tabbed_browser)
ed.editing_finished.connect(functools.partial(
self.on_editing_finished, elem))
ed.edit(text)
tab.find_focus_element(self._open_editor_cb)
def on_editing_finished(self, elem, text):
"""Write the editor text into the form field and clean up tempfile.
@ -1467,7 +1469,7 @@ class CommandDispatcher:
"""
try:
elem.set_text(text, use_js=True)
except webelem.IsNullError:
except webkitelem.IsNullError:
raise cmdexc.CommandError("Element vanished while editing!")
@cmdutils.register(instance='command-dispatcher',
@ -1479,8 +1481,8 @@ class CommandDispatcher:
tab = self._current_widget()
page = tab._widget.page() # pylint: disable=protected-access
try:
elem = webelem.focus_elem(page.currentFrame())
except webelem.IsNullError:
elem = webkitelem.focus_elem(page.currentFrame())
except webkitelem.IsNullError:
raise cmdexc.CommandError("No element focused!")
if not elem.is_editable(strict=True):
raise cmdexc.CommandError("Focused element is not editable!")

View File

@ -28,12 +28,11 @@ from string import ascii_lowercase
from PyQt5.QtCore import (pyqtSignal, pyqtSlot, QObject, QEvent, Qt, QUrl,
QTimer)
from PyQt5.QtGui import QMouseEvent
from PyQt5.QtWebKit import QWebElement
from PyQt5.QtWebKitWidgets import QWebPage
from qutebrowser.config import config
from qutebrowser.keyinput import modeman, modeparsers
from qutebrowser.browser.webkit import webelem
from qutebrowser.browser import webelem
from qutebrowser.commands import userscripts, cmdexc, cmdutils, runners
from qutebrowser.utils import usertypes, log, qtutils, message, objreg, utils
@ -374,7 +373,7 @@ class HintManager(QObject):
for elem in self._context.all_elems:
try:
elem.label.remove_from_document()
except webelem.IsNullError:
except webelem.Error:
pass
text = self._get_text()
message_bridge = objreg.get('message-bridge', scope='window',
@ -516,7 +515,7 @@ class HintManager(QObject):
def _is_hidden(self, elem):
"""Check if the element is hidden via display=none."""
display = elem.style_property('display', QWebElement.InlineStyle)
display = elem.style_property('display', strategy='inline')
return display == 'none'
def _show_elem(self, elem):
@ -767,7 +766,7 @@ class HintManager(QObject):
else:
# element doesn't match anymore -> hide it
self._hide_elem(elem.label)
except webelem.IsNullError:
except webelem.Error:
pass
def _filter_number_hints(self):
@ -782,7 +781,7 @@ class HintManager(QObject):
try:
if not self._is_hidden(e.label):
elems.append(e)
except webelem.IsNullError:
except webelem.Error:
pass
if not elems:
# Whoops, filtered all hints
@ -813,7 +812,7 @@ class HintManager(QObject):
try:
if not self._is_hidden(elem.label):
visible[string] = elem
except webelem.IsNullError:
except webelem.Error:
pass
if not visible:
# Whoops, filtered all hints
@ -844,7 +843,7 @@ class HintManager(QObject):
else:
# element doesn't match anymore -> hide it
self._hide_elem(elem.label)
except webelem.IsNullError:
except webelem.Error:
pass
if config.get('hints', 'mode') == 'number':
@ -961,7 +960,7 @@ class HintManager(QObject):
e.label.remove_from_document()
continue
self._set_style_position(e.elem, e.label)
except webelem.IsNullError:
except webelem.Error:
pass
@pyqtSlot(usertypes.KeyMode)
@ -1022,15 +1021,17 @@ class WordHinter:
"alt": lambda elem: elem["alt"],
"name": lambda elem: elem["name"],
"title": lambda elem: elem["title"],
"placeholder": lambda elem: elem["placeholder"],
"src": lambda elem: elem["src"].split('/')[-1],
"href": lambda elem: elem["href"].split('/')[-1],
"text": str,
}
extractable_attrs = collections.defaultdict(list, {
"IMG": ["alt", "title", "src"],
"A": ["title", "href", "text"],
"INPUT": ["name"]
"img": ["alt", "title", "src"],
"a": ["title", "href", "text"],
"input": ["name", "placeholder"],
"button": ["text"]
})
return (attr_extractors[attr](elem)

View File

@ -21,10 +21,9 @@
import posixpath
from qutebrowser.browser.webkit import webelem
from qutebrowser.browser import webelem
from qutebrowser.config import config
from qutebrowser.utils import (usertypes, objreg, urlutils, log, message,
qtutils)
from qutebrowser.utils import objreg, urlutils, log, message, qtutils
class Error(Exception):
@ -109,11 +108,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'

View File

@ -0,0 +1,369 @@
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
# Copyright 2014-2016 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
#
# 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 <http://www.gnu.org/licenses/>.
"""Generic web element related code.
Module attributes:
Group: Enum for different kinds of groups.
SELECTORS: CSS selectors for different groups of elements.
FILTERS: A dictionary of filter functions for the modes.
The filter for "links" filters javascript:-links and a-tags
without "href".
"""
import collections.abc
from PyQt5.QtCore import QUrl
from qutebrowser.config import config
from qutebrowser.utils import log, usertypes, utils, qtutils
Group = usertypes.enum('Group', ['all', 'links', 'images', 'url', 'prevnext',
'inputs'])
SELECTORS = {
Group.all: ('a, area, textarea, select, input:not([type=hidden]), button, '
'frame, iframe, link, [onclick], [onmousedown], [role=link], '
'[role=option], [role=button], img'),
Group.links: 'a, area, link, [role=link]',
Group.images: 'img',
Group.url: '[src], [href]',
Group.prevnext: 'a, area, button, link, [role=button]',
Group.inputs: ('input[type=text], input[type=email], input[type=url], '
'input[type=tel], input[type=number], '
'input[type=password], input[type=search], '
'input:not([type]), textarea'),
}
def filter_links(elem):
return 'href' in elem and QUrl(elem['href']).scheme() != 'javascript'
FILTERS = {
Group.links: filter_links,
Group.prevnext: filter_links,
}
class Error(Exception):
"""Base class for WebElement errors."""
pass
class AbstractWebElement(collections.abc.MutableMapping):
"""A wrapper around QtWebKit/QtWebEngine web element."""
def __eq__(self, other):
raise NotImplementedError
def __str__(self):
return self.text()
def __getitem__(self, key):
raise NotImplementedError
def __setitem__(self, key, val):
raise NotImplementedError
def __delitem__(self, key):
raise NotImplementedError
def __iter__(self):
raise NotImplementedError
def __len__(self):
raise NotImplementedError
def __repr__(self):
try:
html = self.debug_text()
except Error:
html = None
return utils.get_repr(self, html=html)
def frame(self):
"""Get the main frame of this element."""
# FIXME:qtwebengine get rid of this?
raise NotImplementedError
def geometry(self):
"""Get the geometry for this element."""
raise NotImplementedError
def document_element(self):
"""Get the document element of this element."""
# FIXME:qtwebengine get rid of this?
raise NotImplementedError
def create_inside(self, tagname):
"""Append the given element inside the current one."""
# FIXME:qtwebengine get rid of this?
raise NotImplementedError
def find_first(self, selector):
"""Find the first child based on the given CSS selector."""
# FIXME:qtwebengine get rid of this?
raise NotImplementedError
def style_property(self, name, *, strategy):
"""Get the element style resolved with the given strategy."""
raise NotImplementedError
def classes(self):
"""Get a list of classes assigned to this element."""
raise NotImplementedError
def tag_name(self):
"""Get the tag name of this element.
The returned name will always be lower-case.
"""
raise NotImplementedError
def outer_xml(self):
"""Get the full HTML representation of this element."""
raise NotImplementedError
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.
"""
# FIXME:qtwebengine what to do about use_js with WebEngine?
raise NotImplementedError
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?
raise NotImplementedError
def set_inner_xml(self, xml):
"""Set the given inner XML."""
# FIXME:qtwebengine get rid of this?
raise NotImplementedError
def remove_from_document(self):
"""Remove the node from the document."""
# FIXME:qtwebengine get rid of this?
raise NotImplementedError
def set_style_property(self, name, value):
"""Set the element style."""
# FIXME:qtwebengine get rid of this?
raise NotImplementedError
def run_js_async(self, code, callback=None):
"""Run the given JS snippet async on the element."""
# FIXME:qtwebengine get rid of this?
raise NotImplementedError
def parent(self):
"""Get the parent element of this element."""
# FIXME:qtwebengine get rid of this?
raise NotImplementedError
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 <a> 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
"""
raise NotImplementedError
def is_visible(self, mainframe):
"""Check if the given element is visible in the given frame."""
# FIXME:qtwebengine get rid of this?
raise NotImplementedError
def is_writable(self):
"""Check whether an element is writable."""
return not ('disabled' in self or 'readonly' in self)
def is_content_editable(self):
"""Check if an element has a contenteditable attribute.
Args:
elem: The QWebElement to check.
Return:
True if the element has a contenteditable attribute,
False otherwise.
"""
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("<object> 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("<object type='{}'> 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.classes():
if any([klass.startswith(e) for e in div_classes]):
return True
return False
def is_editable(self, strict=False):
"""Check whether we should switch to insert mode for this element.
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.
"""
roles = ('combobox', 'textbox')
log.misc.debug("Checking if element is editable: {}".format(
repr(self)))
tag = self.tag_name()
if self.is_content_editable() and self.is_writable():
return True
elif self.get('role', None) in roles and self.is_writable():
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 is_text_input(self):
"""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):
"""Remove target from link."""
elem = self
for _ in range(5):
if elem is None:
break
tag = elem.tag_name()
if tag == 'a' or tag == 'area':
if elem.get('target', None) == '_blank':
elem['target'] = '_top'
break
elem = elem.parent()
def debug_text(self):
"""Get a text based on an element suitable for debug output."""
return utils.compact_text(self.outer_xml(), 500)
def resolve_url(self, baseurl):
"""Resolve the URL in the element's src/href attribute.
Args:
baseurl: The URL to base relative URLs on as QUrl.
Return:
A QUrl with the absolute URL, or None.
"""
if baseurl.isRelative():
raise ValueError("Need an absolute base URL!")
for attr in ['href', 'src']:
if attr in self:
text = self[attr].strip()
break
else:
return None
url = QUrl(text)
if not url.isValid():
return None
if url.isRelative():
url = baseurl.resolved(url)
qtutils.ensure_valid(url)
return url

View File

@ -0,0 +1,176 @@
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
# Copyright 2016 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
#
# 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 <http://www.gnu.org/licenses/>.
# 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 <a> 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

View File

@ -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,8 +32,8 @@ 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.utils import usertypes, qtutils, log, javascript
from qutebrowser.browser.webengine import webview, webengineelem
from qutebrowser.utils import usertypes, qtutils, log, javascript, utils
class WebEnginePrinting(browsertab.AbstractPrinting):
@ -95,7 +97,7 @@ class WebEngineSearch(browsertab.AbstractSearch):
flags &= ~QWebEnginePage.FindBackward
else:
flags |= QWebEnginePage.FindBackward
self._find(self.text, self._flags, result_cb)
self._find(self.text, flags, result_cb)
def next_result(self, *, result_cb=None):
self._find(self.text, self._flags, result_cb)
@ -182,18 +184,17 @@ class WebEngineScroller(browsertab.AbstractScroller):
def __init__(self, tab, parent=None):
super().__init__(tab, parent)
self._pos_perc = (None, None)
self._pos_perc = (0, 0)
self._pos_px = QPoint()
def _init_widget(self, widget):
super()._init_widget(widget)
page = widget.page()
try:
page.scrollPositionChanged.connect(
self._on_scroll_pos_changed)
page.scrollPositionChanged.connect(self._update_pos)
except AttributeError:
log.stub('scrollPositionChanged, on Qt < 5.7')
self._on_scroll_pos_changed()
self._pos_perc = (None, None)
def _key_press(self, key, count=1):
# FIXME:qtwebengine Abort scrolling if the minimum/maximum was reached.
@ -207,9 +208,9 @@ class WebEngineScroller(browsertab.AbstractScroller):
QApplication.postEvent(recipient, release_evt)
@pyqtSlot()
def _on_scroll_pos_changed(self):
def _update_pos(self):
"""Update the scroll position attributes when it changed."""
def update_scroll_pos(jsret):
def update_pos_cb(jsret):
"""Callback after getting scroll position via JS."""
if jsret is None:
# This can happen when the callback would get called after
@ -220,8 +221,8 @@ class WebEngineScroller(browsertab.AbstractScroller):
self._pos_px = QPoint(jsret['px']['x'], jsret['px']['y'])
self.perc_changed.emit(*self._pos_perc)
js_code = javascript.assemble('scroll', 'scroll_pos')
self._tab.run_js_async(js_code, update_scroll_pos)
js_code = javascript.assemble('scroll', 'pos')
self._tab.run_js_async(js_code, update_pos_cb)
def pos_px(self):
return self._pos_px
@ -230,7 +231,7 @@ class WebEngineScroller(browsertab.AbstractScroller):
return self._pos_perc
def to_perc(self, x=None, y=None):
js_code = javascript.assemble('scroll', 'scroll_to_perc', x, y)
js_code = javascript.assemble('scroll', 'to_perc', x, y)
self._tab.run_js_async(js_code)
def to_point(self, point):
@ -241,7 +242,7 @@ class WebEngineScroller(browsertab.AbstractScroller):
self._tab.run_js_async("window.scrollBy({x}, {y});".format(x=x, y=y))
def delta_page(self, x=0, y=0):
js_code = javascript.assemble('scroll', 'scroll_delta_page', x, y)
js_code = javascript.assemble('scroll', 'delta_page', x, y)
self._tab.run_js_async(js_code)
def up(self, count=1):
@ -332,6 +333,31 @@ class WebEngineTab(browsertab.AbstractTab):
self._set_widget(widget)
self._connect_signals()
self.backend = usertypes.Backend.QtWebEngine
# init js stuff
self._init_js()
def _init_js(self):
js_code = '\n'.join([
'"use strict";',
'window._qutebrowser = {};',
utils.read_file('javascript/scroll.js'),
utils.read_file('javascript/webelem.js'),
])
script = QWebEngineScript()
script.setInjectionPoint(QWebEngineScript.DocumentCreation)
page = self._widget.page()
script.setSourceCode(js_code)
try:
page.runJavaScript("", QWebEngineScript.ApplicationWorld)
except TypeError:
# We're unable to pass a world to runJavaScript
script.setWorldId(QWebEngineScript.MainWorld)
else:
script.setWorldId(QWebEngineScript.ApplicationWorld)
# FIXME:qtwebengine what about runsOnSubFrames?
page.scripts().insert(script)
def openurl(self, url):
self._openurl_prepare(url)
@ -411,9 +437,42 @@ 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', selector)
js_cb = functools.partial(self._find_all_elements_js_cb, callback)
self.run_js_async(js_code, js_cb)
def _find_focus_element_js_cb(self, callback, js_elem):
"""Handle a found focus elem coming from JS and call the real callback.
Args:
callback: The callback originally passed to find_focus_element.
Called with a WebEngineElement or None.
js_elem: The element serialized from javascript.
"""
log.webview.debug("Got focus element from JS: {!r}".format(js_elem))
if js_elem is None:
callback(None)
else:
callback(webengineelem.WebEngineElement(js_elem))
def find_focus_element(self, callback):
js_code = javascript.assemble('webelem', 'focus_element')
js_cb = functools.partial(self._find_focus_element_js_cb, callback)
self.run_js_async(js_code, js_cb)
def _connect_signals(self):
view = self._widget

View File

@ -34,7 +34,7 @@ import email.message
from PyQt5.QtCore import QUrl
from qutebrowser.browser.webkit import webelem, downloads
from qutebrowser.browser.webkit import webkitelem, downloads
from qutebrowser.utils import log, objreg, message, usertypes, utils, urlutils
try:
@ -271,7 +271,7 @@ class _Downloader:
elements = web_frame.findAllElements('link, script, img')
for element in elements:
element = webelem.WebElementWrapper(element)
element = webkitelem.WebKitElement(element)
# Websites are free to set whatever rel=... attribute they want.
# We just care about stylesheets and icons.
if not _check_rel(element):
@ -288,7 +288,7 @@ class _Downloader:
styles = web_frame.findAllElements('style')
for style in styles:
style = webelem.WebElementWrapper(style)
style = webkitelem.WebKitElement(style)
# The Mozilla Developer Network says:
# type: This attribute defines the styling language as a MIME type
# (charset should not be specified). This attribute is optional and
@ -301,7 +301,7 @@ class _Downloader:
# Search for references in inline styles
for element in web_frame.findAllElements('[style]'):
element = webelem.WebElementWrapper(element)
element = webkitelem.WebKitElement(element)
style = element['style']
for element_url in _get_css_imports(style, inline=True):
self._fetch_url(web_url.resolved(QUrl(element_url)))

View File

@ -17,65 +17,26 @@
# You should have received a copy of the GNU General Public License
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
"""Utilities related to QWebElements.
"""QtWebKit specific part of the web element API."""
Module attributes:
Group: Enum for different kinds of groups.
SELECTORS: CSS selectors for different groups of elements.
FILTERS: A dictionary of filter functions for the modes.
The filter for "links" filters javascript:-links and a-tags
without "href".
"""
import collections.abc
from PyQt5.QtCore import QRect, QUrl
from PyQt5.QtCore import QRect
from PyQt5.QtWebKit import QWebElement
from qutebrowser.config import config
from qutebrowser.utils import log, usertypes, utils, javascript, qtutils
from qutebrowser.utils import log, utils, javascript
from qutebrowser.browser import webelem
Group = usertypes.enum('Group', ['all', 'links', 'images', 'url', 'prevnext',
'focus', 'inputs'])
class IsNullError(webelem.Error):
SELECTORS = {
Group.all: ('a, area, textarea, select, input:not([type=hidden]), button, '
'frame, iframe, link, [onclick], [onmousedown], [role=link], '
'[role=option], [role=button], img'),
Group.links: 'a, area, link, [role=link]',
Group.images: 'img',
Group.url: '[src], [href]',
Group.prevnext: 'a, area, button, link, [role=button]',
Group.focus: '*:focus',
Group.inputs: ('input[type=text], input[type=email], input[type=url], '
'input[type=tel], input[type=number], '
'input[type=password], input[type=search], '
'input:not([type]), textarea'),
}
def filter_links(elem):
return 'href' in elem and QUrl(elem['href']).scheme() != 'javascript'
FILTERS = {
Group.links: filter_links,
Group.prevnext: filter_links,
}
class IsNullError(Exception):
"""Gets raised by WebElementWrapper if an element is null."""
"""Gets raised by WebKitElement if an element is null."""
pass
class WebElementWrapper(collections.abc.MutableMapping):
class WebKitElement(webelem.AbstractWebElement):
"""A wrapper around QWebElement to make it more intelligent."""
"""A wrapper around a QWebElement."""
def __init__(self, elem):
if isinstance(elem, self.__class__):
@ -85,21 +46,10 @@ class WebElementWrapper(collections.abc.MutableMapping):
self._elem = elem
def __eq__(self, other):
if not isinstance(other, WebElementWrapper):
if not isinstance(other, WebKitElement):
return NotImplemented
return self._elem == other._elem # pylint: disable=protected-access
def __str__(self):
self._check_vanished()
return self._elem.toPlainText()
def __repr__(self):
try:
html = self.debug_text()
except IsNullError:
html = None
return utils.get_repr(self, html=html)
def __getitem__(self, key):
self._check_vanished()
if key not in self:
@ -134,24 +84,19 @@ class WebElementWrapper(collections.abc.MutableMapping):
raise IsNullError('Element {} vanished!'.format(self._elem))
def frame(self):
"""Get the main frame of this element."""
# FIXME:qtwebengine how to get rid of this?
self._check_vanished()
return self._elem.webFrame()
def geometry(self):
"""Get the geometry for this element."""
self._check_vanished()
return self._elem.geometry()
def document_element(self):
"""Get the document element of this element."""
self._check_vanished()
elem = self._elem.webFrame().documentElement()
return WebElementWrapper(elem)
return WebKitElement(elem)
def create_inside(self, tagname):
"""Append the given element inside the current one."""
# It seems impossible to create an empty QWebElement for which isNull()
# is false so we can work with it.
# As a workaround, we use appendInside() with markup as argument, and
@ -159,28 +104,40 @@ class WebElementWrapper(collections.abc.MutableMapping):
# See: http://stackoverflow.com/q/7364852/2085149
self._check_vanished()
self._elem.appendInside('<{}></{}>'.format(tagname, tagname))
return WebElementWrapper(self._elem.lastChild())
return WebKitElement(self._elem.lastChild())
def find_first(self, selector):
"""Find the first child based on the given CSS selector."""
self._check_vanished()
elem = self._elem.findFirst(selector)
if elem.isNull():
return None
return WebElementWrapper(elem)
return WebKitElement(elem)
def style_property(self, name, strategy):
"""Get the element style resolved with the given strategy."""
def style_property(self, name, *, strategy):
self._check_vanished()
return self._elem.styleProperty(name, strategy)
strategies = {
# FIXME:qtwebengine which ones do we actually need?
'inline': QWebElement.InlineStyle,
'computed': QWebElement.ComputedStyle,
}
qt_strategy = strategies[strategy]
return self._elem.styleProperty(name, qt_strategy)
def classes(self):
self._check_vanished()
return self._elem.classes()
def tag_name(self):
"""Get the tag name for the current element."""
self._check_vanished()
return self._elem.tagName().lower()
def outer_xml(self):
"""Get the full HTML representation of this element."""
self._check_vanished()
return self._elem.toOuterXml()
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.
"""
self._check_vanished()
if self.is_content_editable() or not use_js:
return self._elem.toPlainText()
@ -188,12 +145,6 @@ class WebElementWrapper(collections.abc.MutableMapping):
return self._elem.evaluateJavaScript('this.value')
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.
"""
self._check_vanished()
if self.is_content_editable() or not use_js:
log.misc.debug("Filling element {} via set_text.".format(
@ -206,158 +157,17 @@ class WebElementWrapper(collections.abc.MutableMapping):
self._elem.evaluateJavaScript("this.value='{}'".format(text))
def set_inner_xml(self, xml):
"""Set the given inner XML."""
self._check_vanished()
self._elem.setInnerXml(xml)
def remove_from_document(self):
"""Remove the node from the document."""
self._check_vanished()
self._elem.removeFromDocument()
def set_style_property(self, name, value):
"""Set the element style."""
self._check_vanished()
return self._elem.setStyleProperty(name, value)
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.
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("<object> 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("<object type='{}'> 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
return False
def is_editable(self, strict=False):
"""Check whether we should switch to insert mode for this element.
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.
"""
self._check_vanished()
roles = ('combobox', 'textbox')
log.misc.debug("Checking if element is editable: {}".format(
repr(self)))
tag = self._elem.tagName().lower()
if self.is_content_editable() and self.is_writable():
return True
elif self.get('role', None) in roles and self.is_writable():
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 is_text_input(self):
"""Check if this element is some kind of text box."""
self._check_vanished()
roles = ('combobox', 'textbox')
tag = self._elem.tagName().lower()
return self.get('role', None) in roles or tag in ['input', 'textarea']
def remove_blank_target(self):
"""Remove target from link."""
self._check_vanished()
elem = self._elem
for _ in range(5):
if elem is None:
break
tag = elem.tagName().lower()
if tag == 'a' or tag == 'area':
if elem.attribute('target') == '_blank':
elem.setAttribute('target', '_top')
break
elem = elem.parent()
def debug_text(self):
"""Get a text based on an element suitable for debug output."""
self._check_vanished()
return utils.compact_text(self._elem.toOuterXml(), 500)
def outer_xml(self):
"""Get the full HTML representation of this element."""
self._check_vanished()
return self._elem.toOuterXml()
def tag_name(self):
"""Get the tag name for the current element."""
self._check_vanished()
return self._elem.tagName()
def run_js_async(self, code, callback=None):
"""Run the given JS snippet async on the element."""
self._check_vanished()
@ -365,8 +175,16 @@ class WebElementWrapper(collections.abc.MutableMapping):
if callback is not None:
callback(result)
def parent(self):
self._check_vanished()
elem = self._elem.parent()
if elem is None:
return None
return WebKitElement(elem)
def _rect_on_view_js(self, adjust_zoom):
"""Javascript implementation for rect_on_view."""
# FIXME:qtwebengine maybe we can reuse this?
rects = self._elem.evaluateJavaScript("this.getClientRects()")
if rects is None: # pragma: no cover
# Depending on unknown circumstances, this might not work with JS
@ -444,6 +262,8 @@ class WebElementWrapper(collections.abc.MutableMapping):
current zoom level.
no_js: Fall back to the Python implementation
"""
# FIXME:qtwebengine can we get rid of this with
# find_all_elements(only_visible=True)?
self._check_vanished()
# First try getting the element rect via JS, as that's usually more
@ -500,33 +320,6 @@ class WebElementWrapper(collections.abc.MutableMapping):
visible_in_frame = visible_on_screen
return all([visible_on_screen, visible_in_frame])
def resolve_url(self, baseurl):
"""Resolve the URL in the element's src/href attribute.
Args:
baseurl: The URL to base relative URLs on as QUrl.
Return:
A QUrl with the absolute URL, or None.
"""
if baseurl.isRelative():
raise ValueError("Need an absolute base URL!")
for attr in ['href', 'src']:
if attr in self:
text = self[attr].strip()
break
else:
return None
url = QUrl(text)
if not url.isValid():
return None
if url.isRelative():
url = baseurl.resolved(url)
qtutils.ensure_valid(url)
return url
def get_child_frames(startframe):
"""Get all children recursively of a given QWebFrame.
@ -556,5 +349,5 @@ def focus_elem(frame):
Args:
frame: The QWebFrame to search in.
"""
elem = frame.findFirstElement(SELECTORS[Group.focus])
return WebElementWrapper(elem)
elem = frame.findFirstElement('*:focus')
return WebKitElement(elem)

View File

@ -31,7 +31,7 @@ from PyQt5.QtWebKit import QWebSettings
from PyQt5.QtPrintSupport import QPrinter
from qutebrowser.browser import browsertab
from qutebrowser.browser.webkit import webview, tabhistory, webelem
from qutebrowser.browser.webkit import webview, tabhistory, webkitelem
from qutebrowser.utils import qtutils, objreg, usertypes, utils
@ -564,16 +564,28 @@ class WebKitTab(browsertab.AbstractTab):
raise browsertab.WebTabError("No frame focused!")
elems = []
frames = webelem.get_child_frames(mainframe)
frames = webkitelem.get_child_frames(mainframe)
for f in frames:
for elem in f.findAllElements(selector):
elems.append(webelem.WebElementWrapper(elem))
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))
@pyqtSlot()
def _on_frame_load_finished(self):
"""Make sure we emit an appropriate status when loading finished.

View File

@ -31,7 +31,7 @@ from qutebrowser.config import config
from qutebrowser.keyinput import modeman
from qutebrowser.utils import message, log, usertypes, utils, qtutils, objreg
from qutebrowser.browser import hints
from qutebrowser.browser.webkit import webpage, webelem
from qutebrowser.browser.webkit import webpage, webkitelem
class WebView(QWebView):
@ -196,13 +196,13 @@ class WebView(QWebView):
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 webelem.focus_elem.
# later (in mouseReleaseEvent) which uses webkitelem.focus_elem.
log.mouse.debug("Hitresult is null!")
self._check_insertmode = True
return
try:
elem = webelem.WebElementWrapper(hitresult.element())
except webelem.IsNullError:
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
@ -223,12 +223,13 @@ class WebView(QWebView):
def mouserelease_insertmode(self):
"""If we have an insertmode check scheduled, handle it."""
# FIXME:qtwebengine Use tab.find_focus_element here
if not self._check_insertmode:
return
self._check_insertmode = False
try:
elem = webelem.focus_elem(self.page().currentFrame())
except (webelem.IsNullError, RuntimeError):
elem = webkitelem.focus_elem(self.page().currentFrame())
except (webkitelem.IsNullError, RuntimeError):
log.mouse.debug("Element/page vanished!")
return
if elem.is_editable():
@ -325,8 +326,8 @@ class WebView(QWebView):
return
frame = self.page().currentFrame()
try:
elem = webelem.WebElementWrapper(frame.findFirstElement(':focus'))
except webelem.IsNullError:
elem = webkitelem.focus_elem(frame)
except webkitelem.IsNullError:
log.webview.debug("Focused element is null!")
return
log.modes.debug("focus element: {}".format(repr(elem)))

View File

@ -181,16 +181,14 @@ class CompletionView(QTreeView):
# Item is a real item, not a category header -> success
return idx
def _next_prev_item(self, prev):
"""Handle a tab press for the CompletionView.
Select the previous/next item and write the new text to the
statusbar.
Helper for completion_item_next and completion_item_prev.
@cmdutils.register(instance='completion', hide=True,
modes=[usertypes.KeyMode.command], scope='window')
@cmdutils.argument('which', choices=['next', 'prev'])
def completion_item_focus(self, which):
"""Shift the focus of the completion menu to another item.
Args:
prev: True for prev item, False for next one.
which: 'next' or 'prev'
"""
# selmodel can be None if 'show' and 'auto-open' are set to False
# https://github.com/The-Compiler/qutebrowser/issues/1731
@ -198,7 +196,7 @@ class CompletionView(QTreeView):
if selmodel is None:
return
idx = self._next_idx(prev)
idx = self._next_idx(which == 'prev')
if not idx.isValid():
return
@ -278,18 +276,6 @@ class CompletionView(QTreeView):
scrollbar.setValue(scrollbar.minimum())
super().showEvent(e)
@cmdutils.register(instance='completion', hide=True,
modes=[usertypes.KeyMode.command], scope='window')
def completion_item_prev(self):
"""Select the previous completion item."""
self._next_prev_item(True)
@cmdutils.register(instance='completion', hide=True,
modes=[usertypes.KeyMode.command], scope='window')
def completion_item_next(self):
"""Select the next completion item."""
self._next_prev_item(False)
@cmdutils.register(instance='completion', hide=True,
modes=[usertypes.KeyMode.command], scope='window')
def completion_item_del(self):

View File

@ -174,6 +174,7 @@ class TabCompletionModel(base.BaseCompletionModel):
for i in range(tabbed_browser.count()):
tab = tabbed_browser.widget(i)
tab.url_changed.connect(self.rebuild)
tab.title_changed.connect(self.rebuild)
tab.shutting_down.connect(self.delayed_rebuild)
tabbed_browser.new_tab.connect(self.on_new_tab)
objreg.get("app").new_window.connect(self.on_new_window)
@ -187,6 +188,7 @@ class TabCompletionModel(base.BaseCompletionModel):
def on_new_tab(self, tab):
"""Add hooks to new tabs."""
tab.url_changed.connect(self.rebuild)
tab.title_changed.connect(self.rebuild)
tab.shutting_down.connect(self.delayed_rebuild)
self.rebuild()

View File

@ -154,7 +154,7 @@ def data(readonly=False):
"Whether to save the config automatically on quit."),
('auto-save-interval',
SettingValue(typ.Int(minval=0), '15000'),
SettingValue(typ.Int(minval=0, maxval=MAXVALS['int']), '15000'),
"How often (in milliseconds) to auto-save config/cookies/etc."),
('editor',
@ -488,13 +488,13 @@ def data(readonly=False):
('input', sect.KeyValue(
('timeout',
SettingValue(typ.Int(minval=0, maxval=MAXVALS['int']), '500'),
"Timeout for ambiguous key bindings.\n\n"
"Timeout (in milliseconds) for ambiguous key bindings.\n\n"
"If the current input forms both a complete match and a partial "
"match, the complete match will be executed after this time."),
('partial-timeout',
SettingValue(typ.Int(minval=0, maxval=MAXVALS['int']), '5000'),
"Timeout for partially typed key bindings.\n\n"
"Timeout (in milliseconds) for partially typed key bindings.\n\n"
"If the current input forms only partial matches, the keystring "
"will be cleared after this time."),
@ -933,8 +933,8 @@ def data(readonly=False):
('auto-follow-timeout',
SettingValue(typ.Int(), '0'),
"A timeout to inhibit normal-mode key bindings after a successful"
"auto-follow."),
"A timeout (in milliseconds) to inhibit normal-mode key bindings "
"after a successful auto-follow."),
('next-regexes',
SettingValue(typ.List(typ.Regex(flags=re.IGNORECASE)),
@ -1415,8 +1415,7 @@ KEY_SECTION_DESC = {
"Useful hidden commands to map in this section:\n\n"
" * `command-history-prev`: Switch to previous command in history.\n"
" * `command-history-next`: Switch to next command in history.\n"
" * `completion-item-prev`: Select previous item in completion.\n"
" * `completion-item-next`: Select next item in completion.\n"
" * `completion-item-focus`: Select another item in completion.\n"
" * `command-accept`: Execute the command currently in the "
"commandline."),
'prompt': (
@ -1589,8 +1588,8 @@ KEY_DATA = collections.OrderedDict([
('command', collections.OrderedDict([
('command-history-prev', ['<Ctrl-P>']),
('command-history-next', ['<Ctrl-N>']),
('completion-item-prev', ['<Shift-Tab>', '<Up>']),
('completion-item-next', ['<Tab>', '<Down>']),
('completion-item-focus prev', ['<Shift-Tab>', '<Up>']),
('completion-item-focus next', ['<Tab>', '<Down>']),
('completion-item-del', ['<Ctrl-D>']),
('command-accept', RETURN_KEYS),
])),
@ -1691,4 +1690,7 @@ CHANGED_KEY_COMMANDS = [
(re.compile(r'^paste -([twb])$'), r'open -\1 {clipboard}'),
(re.compile(r'^paste -([twb])s$'), r'open -\1 {primary}'),
(re.compile(r'^paste -s([twb])$'), r'open -\1 {primary}'),
(re.compile(r'^completion-item-next'), r'completion-item-focus next'),
(re.compile(r'^completion-item-prev'), r'completion-item-focus prev'),
]

View File

@ -0,0 +1,35 @@
env:
browser: true
parserOptions:
ecmaVersion: 3
extends:
"eslint:all"
rules:
strict: ["error", "global"]
one-var: "off"
padded-blocks: ["error", "never"]
space-before-function-paren: ["error", "never"]
no-underscore-dangle: "off"
no-var: "off"
vars-on-top: "off"
newline-after-var: "off"
camelcase: "off"
require-jsdoc: "off"
func-style: ["error", "declaration"]
newline-before-return: "off"
init-declarations: "off"
no-plusplus: "off"
no-extra-parens: off
id-length: ["error", {"exceptions": ["i", "x", "y"]}]
object-shorthand: "off"
max-statements: ["error", {"max": 30}]
quotes: ["error", "double", {"avoidEscape": true}]
object-property-newline: ["error", {"allowMultiplePropertiesPerLine": true}]
comma-dangle: ["error", "always-multiline"]
no-magic-numbers: "off"
no-undefined: "off"
wrap-iife: ["error", "inside"]
func-names: "off"

View File

@ -1,6 +1,6 @@
/**
* Copyright 2015 Artur Shaik <ashaihullin@gmail.com>
* Copyright 2015 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
* Copyright 2015-2016 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
*
* This file is part of qutebrowser.
*
@ -32,79 +32,83 @@
"use strict";
function isElementInViewport(node) {
var i;
var boundingRect = (node.getClientRects()[0] ||
node.getBoundingClientRect());
if (boundingRect.width <= 1 && boundingRect.height <= 1) {
var rects = node.getClientRects();
for (i = 0; i < rects.length; i++) {
if (rects[i].width > rects[0].height &&
rects[i].height > rects[0].height) {
boundingRect = rects[i];
(function() {
function isElementInViewport(node) { // eslint-disable-line complexity
var i;
var boundingRect = (node.getClientRects()[0] ||
node.getBoundingClientRect());
if (boundingRect.width <= 1 && boundingRect.height <= 1) {
var rects = node.getClientRects();
for (i = 0; i < rects.length; i++) {
if (rects[i].width > rects[0].height &&
rects[i].height > rects[0].height) {
boundingRect = rects[i];
}
}
}
if (boundingRect === undefined) {
return null;
}
if (boundingRect.top > innerHeight || boundingRect.left > innerWidth) {
return null;
}
if (boundingRect.width <= 1 || boundingRect.height <= 1) {
var children = node.children;
var visibleChildNode = false;
for (i = 0; i < children.length; ++i) {
boundingRect = (children[i].getClientRects()[0] ||
children[i].getBoundingClientRect());
if (boundingRect.width > 1 && boundingRect.height > 1) {
visibleChildNode = true;
break;
}
}
if (visibleChildNode === false) {
return null;
}
}
if (boundingRect.top + boundingRect.height < 10 ||
boundingRect.left + boundingRect.width < -10) {
return null;
}
var computedStyle = window.getComputedStyle(node, null);
if (computedStyle.visibility !== "visible" ||
computedStyle.display === "none" ||
node.hasAttribute("disabled") ||
parseInt(computedStyle.width, 10) === 0 ||
parseInt(computedStyle.height, 10) === 0) {
return null;
}
return boundingRect.top >= -20;
}
if (boundingRect === undefined) {
return null;
}
if (boundingRect.top > innerHeight || boundingRect.left > innerWidth) {
return null;
}
if (boundingRect.width <= 1 || boundingRect.height <= 1) {
var children = node.children;
var visibleChildNode = false;
var l = children.length;
for (i = 0; i < l; ++i) {
boundingRect = (children[i].getClientRects()[0] ||
children[i].getBoundingClientRect());
if (boundingRect.width > 1 && boundingRect.height > 1) {
visibleChildNode = true;
function positionCaret() {
var walker = document.createTreeWalker(document.body, 4, null);
var node;
var textNodes = [];
var el;
while ((node = walker.nextNode())) {
if (node.nodeType === 3 && node.data.trim() !== "") {
textNodes.push(node);
}
}
for (var i = 0; i < textNodes.length; i++) {
var element = textNodes[i].parentElement;
if (isElementInViewport(element.parentElement)) {
el = element;
break;
}
}
if (visibleChildNode === false) {
return null;
if (el !== undefined) {
var range = document.createRange();
range.setStart(el, 0);
range.setEnd(el, 0);
var sel = window.getSelection();
sel.removeAllRanges();
sel.addRange(range);
}
}
if (boundingRect.top + boundingRect.height < 10 ||
boundingRect.left + boundingRect.width < -10) {
return null;
}
var computedStyle = window.getComputedStyle(node, null);
if (computedStyle.visibility !== 'visible' ||
computedStyle.display === 'none' ||
node.hasAttribute('disabled') ||
parseInt(computedStyle.width, 10) === 0 ||
parseInt(computedStyle.height, 10) === 0) {
return null;
}
return boundingRect.top >= -20;
}
(function() {
var walker = document.createTreeWalker(document.body, 4, null);
var node;
var textNodes = [];
var el;
while ((node = walker.nextNode())) {
if (node.nodeType === 3 && node.data.trim() !== '') {
textNodes.push(node);
}
}
for (var i = 0; i < textNodes.length; i++) {
var element = textNodes[i].parentElement;
if (isElementInViewport(element.parentElement)) {
el = element;
break;
}
}
if (el !== undefined) {
var range = document.createRange();
range.setStart(el, 0);
range.setEnd(el, 0);
var sel = window.getSelection();
sel.removeAllRanges();
sel.addRange(range);
}
positionCaret();
})();

View File

@ -17,51 +17,59 @@
* along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
*/
function _qutebrowser_scroll_to_perc(x, y) {
var elem = document.documentElement;
var x_px = window.scrollX;
var y_px = window.scrollY;
"use strict";
if (x !== undefined) {
x_px = (elem.scrollWidth - elem.clientWidth) / 100 * x;
}
window._qutebrowser.scroll = (function() {
var funcs = {};
if (y !== undefined) {
y_px = (elem.scrollHeight - elem.clientHeight) / 100 * y;
}
funcs.to_perc = function(x, y) {
var elem = document.documentElement;
var x_px = window.scrollX;
var y_px = window.scrollY;
window.scroll(x_px, y_px);
}
if (x !== undefined) {
x_px = (elem.scrollWidth - elem.clientWidth) / 100 * x;
}
function _qutebrowser_scroll_delta_page(x, y) {
var dx = document.documentElement.clientWidth * x;
var dy = document.documentElement.clientHeight * y;
window.scrollBy(dx, dy);
}
if (y !== undefined) {
y_px = (elem.scrollHeight - elem.clientHeight) / 100 * y;
}
function _qutebrowser_scroll_pos() {
var elem = document.documentElement;
var dx = (elem.scrollWidth - elem.clientWidth);
var dy = (elem.scrollHeight - elem.clientHeight);
window.scroll(x_px, y_px);
};
var perc_x, perc_y;
funcs.delta_page = function(x, y) {
var dx = document.documentElement.clientWidth * x;
var dy = document.documentElement.clientHeight * y;
window.scrollBy(dx, dy);
};
if (dx === 0) {
perc_x = 0;
} else {
perc_x = 100 / dx * window.scrollX;
}
funcs.pos = function() {
var elem = document.documentElement;
var dx = elem.scrollWidth - elem.clientWidth;
var dy = elem.scrollHeight - elem.clientHeight;
var perc_x, perc_y;
if (dy === 0) {
perc_y = 0;
} else {
perc_y = 100 / dy * window.scrollY;
}
if (dx === 0) {
perc_x = 0;
} else {
perc_x = 100 / dx * window.scrollX;
}
var pos_perc = {'x': perc_x, 'y': perc_y};
var pos_px = {'x': window.scrollX, 'y': window.scrollY};
var pos = {'perc': pos_perc, 'px': pos_px};
if (dy === 0) {
perc_y = 0;
} else {
perc_y = 100 / dy * window.scrollY;
}
// console.log(JSON.stringify(pos));
return pos;
}
var pos = {
"perc": {"x": perc_x, "y": perc_y},
"px": {"x": window.scrollX, "y": window.scrollY},
};
// console.log(JSON.stringify(pos));
return pos;
};
return funcs;
})();

View File

@ -0,0 +1,80 @@
/**
* Copyright 2016 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
*
* 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 <http://www.gnu.org/licenses/>.
*/
"use strict";
window._qutebrowser.webelem = (function() {
var funcs = {};
var elements = [];
function serialize_elem(elem, id) {
var out = {
"id": id,
"text": elem.text,
"tag_name": elem.tagName,
"outer_xml": elem.outerHTML,
};
var attributes = {};
for (var i = 0; i < elem.attributes.length; ++i) {
var attr = elem.attributes[i];
attributes[attr.name] = attr.value;
}
out.attributes = attributes;
// console.log(JSON.stringify(out));
return out;
}
funcs.find_all = function(selector) {
var elems = document.querySelectorAll(selector);
var out = [];
var id = elements.length;
for (var i = 0; i < elems.length; ++i) {
var elem = elems[i];
out.push(serialize_elem(elem, id));
elements[id] = elem;
id++;
}
return out;
};
funcs.focus_element = function() {
var elem = document.activeElement;
if (!elem || elem === document.body) {
// "When there is no selection, the active element is the page's
// <body> or null."
return null;
}
var id = elements.length;
return serialize_elem(elem, id);
};
funcs.get_element = function(id) {
return elements[id];
};
return funcs;
})();

View File

@ -20,9 +20,6 @@
"""Utilities related to javascript interaction."""
from qutebrowser.utils import utils
def string_escape(text):
"""Escape values special to javascript in strings.
@ -55,18 +52,16 @@ def _convert_js_arg(arg):
return 'undefined'
elif isinstance(arg, str):
return '"{}"'.format(string_escape(arg))
elif isinstance(arg, int):
elif isinstance(arg, (int, float)):
return str(arg)
else:
raise TypeError("Don't know how to handle {!r} of type {}!".format(
arg, type(arg).__name__))
def assemble(name, function, *args):
def assemble(module, function, *args):
"""Assemble a javascript file and a function call."""
code = "{code}\n_qutebrowser_{function}({args});".format(
code=utils.read_file('javascript/{}.js'.format(name)),
function=function,
args=', '.join(_convert_js_arg(arg) for arg in args),
)
js_args = ', '.join(_convert_js_arg(arg) for arg in args)
code = '"use strict";\nwindow._qutebrowser.{}.{}({});'.format(
module, function, js_args)
return code

View File

@ -60,7 +60,9 @@ PERFECT_FILES = [
('tests/unit/browser/webkit/http/test_content_disposition.py',
'qutebrowser/browser/webkit/rfc6266.py'),
('tests/unit/browser/webkit/test_webelem.py',
'qutebrowser/browser/webkit/webelem.py'),
'qutebrowser/browser/webkit/webkitelem.py'),
('tests/unit/browser/webkit/test_webelem.py',
'qutebrowser/browser/webelem.py'),
('tests/unit/browser/webkit/network/test_schemehandler.py',
'qutebrowser/browser/webkit/network/schemehandler.py'),
('tests/unit/browser/webkit/network/test_filescheme.py',

View File

@ -42,6 +42,13 @@ from qutebrowser.commands import cmdutils, argparser
from qutebrowser.config import configdata
from qutebrowser.utils import docutils, usertypes
FILE_HEADER = """
// DO NOT EDIT THIS FILE DIRECTLY!
// It is autogenerated from docstrings by running:
// $ python3 scripts/dev/src2asciidoc.py
""".lstrip()
class UsageFormatter(argparse.HelpFormatter):
@ -312,6 +319,7 @@ def _format_action(action):
def generate_commands(filename):
"""Generate the complete commands section."""
with _open_file(filename) as f:
f.write(FILE_HEADER)
f.write("= Commands\n")
normal_cmds = []
hidden_cmds = []
@ -395,6 +403,7 @@ def _generate_setting_section(f, sectname, sect):
def generate_settings(filename):
"""Generate the complete settings section."""
with _open_file(filename) as f:
f.write(FILE_HEADER)
f.write("= Settings\n")
f.write(_get_setting_quickref() + "\n")
for sectname, sect in configdata.DATA.items():

View File

@ -0,0 +1,11 @@
<head>
<title>Old title</title>
<script type="text/javascript">
setTimeout(function(){ document.title = "New title"; }, 3000);
</script>
</head>
<body>
<p>This page should change its title after 3s.</p>
<p>When opening the :buffer completion ("gt"), the title should update while it's open.</p>
</body>

View File

@ -28,13 +28,14 @@ from PyQt5.QtCore import QRect, QPoint, QUrl
from PyQt5.QtWebKit import QWebElement
import pytest
from qutebrowser.browser.webkit import webelem
from qutebrowser.browser import webelem
from qutebrowser.browser.webkit import webkitelem
def get_webelem(geometry=None, frame=None, *, null=False, style=None,
attributes=None, tagname=None, classes=None,
parent=None, js_rect_return=None, zoom_text_only=False):
"""Factory for WebElementWrapper objects based on a mock.
"""Factory for WebKitElement objects based on a mock.
Args:
geometry: The geometry of the QWebElement as QRect.
@ -117,7 +118,7 @@ def get_webelem(geometry=None, frame=None, *, null=False, style=None,
return style_dict[name]
elem.styleProperty.side_effect = _style_property
wrapped = webelem.WebElementWrapper(elem)
wrapped = webkitelem.WebKitElement(elem)
return wrapped
@ -187,7 +188,7 @@ class SelectionAndFilterTests:
webelem.Group.url]),
]
GROUPS = [e for e in webelem.Group if e != webelem.Group.focus]
GROUPS = list(webelem.Group)
COMBINATIONS = list(itertools.product(TESTS, GROUPS))
@ -215,15 +216,14 @@ class TestSelectorsAndFilters:
# Make sure setting HTML succeeded and there's a new element
assert len(webframe.findAllElements('*')) == 3
elems = webframe.findAllElements(webelem.SELECTORS[group])
elems = [webelem.WebElementWrapper(e) for e in elems]
elems = [webkitelem.WebKitElement(e) for e in elems]
filterfunc = webelem.FILTERS.get(group, lambda e: True)
elems = [e for e in elems if filterfunc(e)]
assert bool(elems) == matching
class TestWebKitElement:
class TestWebElementWrapper:
"""Generic tests for WebElementWrapper.
"""Generic tests for WebKitElement.
Note: For some methods, there's a dedicated test class with more involved
tests.
@ -235,13 +235,13 @@ class TestWebElementWrapper:
def test_nullelem(self):
"""Test __init__ with a null element."""
with pytest.raises(webelem.IsNullError):
with pytest.raises(webkitelem.IsNullError):
get_webelem(null=True)
def test_double_wrap(self, elem):
"""Test wrapping a WebElementWrapper."""
"""Test wrapping a WebKitElement."""
with pytest.raises(TypeError) as excinfo:
webelem.WebElementWrapper(elem)
webkitelem.WebKitElement(elem)
assert str(excinfo.value) == "Trying to wrap a wrapper!"
@pytest.mark.parametrize('code', [
@ -257,7 +257,7 @@ class TestWebElementWrapper:
lambda e: e.document_element(),
lambda e: e.create_inside('span'),
lambda e: e.find_first('span'),
lambda e: e.style_property('visibility', QWebElement.ComputedStyle),
lambda e: e.style_property('visibility', strategy='computed'),
lambda e: e.text(),
lambda e: e.set_text('foo'),
lambda e: e.set_inner_xml(''),
@ -285,16 +285,16 @@ class TestWebElementWrapper:
"""Make sure methods check if the element is vanished."""
elem._elem.isNull.return_value = True
elem._elem.tagName.return_value = 'span'
with pytest.raises(webelem.IsNullError):
with pytest.raises(webkitelem.IsNullError):
code(elem)
def test_str(self, elem):
assert str(elem) == 'text'
@pytest.mark.parametrize('is_null, expected', [
(False, "<qutebrowser.browser.webkit.webelem.WebElementWrapper "
(False, "<qutebrowser.browser.webkit.webkitelem.WebKitElement "
"html='<fakeelem/>'>"),
(True, '<qutebrowser.browser.webkit.webelem.WebElementWrapper '
(True, '<qutebrowser.browser.webkit.webkitelem.WebKitElement '
'html=None>'),
])
def test_repr(self, elem, is_null, expected):
@ -334,7 +334,7 @@ class TestWebElementWrapper:
def test_eq(self):
one = get_webelem()
two = webelem.WebElementWrapper(one._elem)
two = webkitelem.WebKitElement(one._elem)
assert one == two
def test_eq_other_type(self):
@ -402,7 +402,6 @@ class TestWebElementWrapper:
('webFrame', lambda e: e.frame()),
('geometry', lambda e: e.geometry()),
('toOuterXml', lambda e: e.outer_xml()),
('tagName', lambda e: e.tag_name()),
])
def test_simple_getters(self, elem, attribute, code):
sentinel = object()
@ -421,8 +420,12 @@ class TestWebElementWrapper:
mock = getattr(elem._elem, method)
mock.assert_called_with(*args)
def test_tag_name(self, elem):
elem._elem.tagName.return_value = 'SPAN'
assert elem.tag_name() == 'span'
def test_style_property(self, elem):
assert elem.style_property('foo', QWebElement.ComputedStyle) == 'bar'
assert elem.style_property('foo', strategy='computed') == 'bar'
def test_document_element(self, stubs):
doc_elem = get_webelem()
@ -430,14 +433,14 @@ class TestWebElementWrapper:
elem = get_webelem(frame=frame)
doc_elem_ret = elem.document_element()
assert isinstance(doc_elem_ret, webelem.WebElementWrapper)
assert isinstance(doc_elem_ret, webkitelem.WebKitElement)
assert doc_elem_ret == doc_elem
def test_find_first(self, elem):
result = get_webelem()
elem._elem.findFirst.return_value = result._elem
find_result = elem.find_first('')
assert isinstance(find_result, webelem.WebElementWrapper)
assert isinstance(find_result, webkitelem.WebKitElement)
assert find_result == result
def test_create_inside(self, elem):
@ -727,7 +730,7 @@ def test_focus_element(stubs):
frame = stubs.FakeWebFrame(QRect(0, 0, 100, 100))
elem = get_webelem()
frame.focus_elem = elem._elem
assert webelem.focus_elem(frame)._elem is elem._elem
assert webkitelem.focus_elem(frame)._elem is elem._elem
class TestRectOnView:
@ -739,7 +742,7 @@ class TestRectOnView:
This is needed for all the tests calling rect_on_view or is_visible.
"""
config_stub.data = {'ui': {'zoom-text-only': 'true'}}
monkeypatch.setattr('qutebrowser.browser.webkit.webelem.config',
monkeypatch.setattr('qutebrowser.browser.webkit.webkitelem.config',
config_stub)
return config_stub
@ -821,7 +824,7 @@ class TestGetChildFrames:
def test_single_frame(self, stubs):
"""Test get_child_frames with a single frame without children."""
frame = stubs.FakeChildrenFrame()
children = webelem.get_child_frames(frame)
children = webkitelem.get_child_frames(frame)
assert len(children) == 1
assert children[0] is frame
frame.childFrames.assert_called_once_with()
@ -836,7 +839,7 @@ class TestGetChildFrames:
child1 = stubs.FakeChildrenFrame()
child2 = stubs.FakeChildrenFrame()
parent = stubs.FakeChildrenFrame([child1, child2])
children = webelem.get_child_frames(parent)
children = webkitelem.get_child_frames(parent)
assert len(children) == 3
assert children[0] is parent
assert children[1] is child1
@ -858,7 +861,7 @@ class TestGetChildFrames:
first = [stubs.FakeChildrenFrame(second[0:2]),
stubs.FakeChildrenFrame(second[2:4])]
root = stubs.FakeChildrenFrame(first)
children = webelem.get_child_frames(root)
children = webkitelem.get_child_frames(root)
assert len(children) == 7
assert children[0] is root
for frame in [root] + first + second:
@ -873,7 +876,7 @@ class TestIsEditable:
def stubbed_config(self, config_stub, monkeypatch):
"""Fixture to create a config stub with an input section."""
config_stub.data = {'input': {}}
monkeypatch.setattr('qutebrowser.browser.webkit.webelem.config',
monkeypatch.setattr('qutebrowser.browser.webkit.webkitelem.config',
config_stub)
return config_stub

View File

@ -122,7 +122,7 @@ def test_maybe_resize_completion(completionview, config_stub, qtbot):
([[]], 1, None),
([[]], -1, None),
])
def test_completion_item_next_prev(tree, count, expected, completionview):
def test_completion_item_focus(tree, count, expected, completionview):
"""Test that on_next_prev_item moves the selection properly.
Args:
@ -140,21 +140,18 @@ def test_completion_item_next_prev(tree, count, expected, completionview):
filtermodel = sortfilter.CompletionFilterModel(model,
parent=completionview)
completionview.set_model(filtermodel)
if count < 0:
for _ in range(-count):
completionview.completion_item_prev()
else:
for _ in range(count):
completionview.completion_item_next()
direction = 'prev' if count < 0 else 'next'
for _ in range(abs(count)):
completionview.completion_item_focus(direction)
idx = completionview.selectionModel().currentIndex()
assert filtermodel.data(idx) == expected
def test_completion_item_next_prev_no_model(completionview):
def test_completion_item_focus_no_model(completionview):
"""Test that next/prev won't crash with no model set.
This can happen if completion.show and completion.auto-open are False.
Regression test for issue #1722.
"""
completionview.completion_item_prev()
completionview.completion_item_next()
completionview.completion_item_focus('prev')
completionview.completion_item_focus('next')

View File

@ -126,6 +126,7 @@ class TestStringEscape:
('foobar', '"foobar"'),
('foo\\bar', r'"foo\\bar"'),
(42, '42'),
(23.42, '23.42'),
(None, 'undefined'),
(object(), TypeError),
])
@ -137,8 +138,6 @@ def test_convert_js_arg(arg, expected):
assert javascript._convert_js_arg(arg) == expected
def test_assemble(monkeypatch):
monkeypatch.setattr(javascript.utils, 'read_file',
'<code from {}>'.format)
expected = '<code from javascript/foo.js>\n_qutebrowser_func(23);'
def test_assemble():
expected = '"use strict";\nwindow._qutebrowser.foo.func(23);'
assert javascript.assemble('foo', 'func', 23) == expected

24
tox.ini
View File

@ -6,6 +6,7 @@
[tox]
envlist = py34,py35-cov,misc,vulture,flake8,pylint,pyroma,check-manifest
distshare = {toxworkdir}
skipsdist = true
[testenv]
# https://bitbucket.org/hpk42/tox/issue/246/ - only needed for Windows though
@ -61,7 +62,6 @@ deps = {[testenv:mkvenv]deps}
# cx_Freeze doesn't support Python 3.5 yet
basepython = python3.4
passenv = {[testenv]passenv}
skip_install = true
deps =
{[testenv]deps}
-r{toxinidir}/misc/requirements/requirements-cxfreeze.txt
@ -75,7 +75,7 @@ ignore_errors = true
basepython = python3
# For global .gitignore files
passenv = HOME
deps = -r{toxinidir}/misc/requirements/requirements-pip.txt
deps =
commands =
{envpython} scripts/dev/misc_checks.py git
{envpython} scripts/dev/misc_checks.py vcs
@ -85,7 +85,9 @@ commands =
basepython = python3
deps =
-r{toxinidir}/misc/requirements/requirements-pip.txt
-r{toxinidir}/requirements.txt
-r{toxinidir}/misc/requirements/requirements-vulture.txt
setenv = PYTHONPATH={toxinidir}
commands =
{envpython} scripts/link_pyqt.py --tox {envdir}
{envpython} scripts/dev/run_vulture.py
@ -124,23 +126,24 @@ commands =
[testenv:pyroma]
basepython = python3
skip_install = true
passenv =
deps = -r{toxinidir}/misc/requirements/requirements-pyroma.txt
deps =
-r{toxinidir}/misc/requirements/requirements-pip.txt
-r{toxinidir}/misc/requirements/requirements-pyroma.txt
commands =
{envdir}/bin/pyroma .
[testenv:check-manifest]
basepython = python3
skip_install = true
passenv =
deps = -r{toxinidir}/misc/requirements/requirements-check-manifest.txt
deps =
-r{toxinidir}/misc/requirements/requirements-pip.txt
-r{toxinidir}/misc/requirements/requirements-check-manifest.txt
commands =
{envdir}/bin/check-manifest --ignore 'qutebrowser/git-commit-id,qutebrowser/html/doc,qutebrowser/html/doc/*,*/__pycache__'
[testenv:docs]
basepython = python3
skip_install = true
whitelist_externals = git
passenv = TRAVIS_PULL_REQUEST
deps =
@ -156,7 +159,6 @@ commands =
# PYTHON is actually required when using this env, but the entire tox.ini would
# fail if we didn't have a fallback defined.
basepython = {env:PYTHON:}/python.exe
skip_install = true
deps =
-r{toxinidir}/misc/requirements/requirements-pip.txt
-r{toxinidir}/requirements.txt
@ -168,7 +170,6 @@ commands =
[testenv:pyinstaller]
basepython = python3
skip_install = true
deps =
-r{toxinidir}/misc/requirements/requirements-pip.txt
-r{toxinidir}/requirements.txt
@ -178,7 +179,6 @@ commands =
{envbindir}/pyinstaller --noconfirm misc/qutebrowser.spec
[testenv:eslint]
skip_install = True
deps = -r{toxinidir}/misc/requirements/requirements-pip.txt
deps =
whitelist_externals = eslint
commands = eslint qutebrowser
commands = eslint --color qutebrowser/javascript