Merge https://github.com/The-Compiler/qutebrowser into clip
This commit is contained in:
commit
8e4733f483
@ -1 +0,0 @@
|
||||
qutebrowser/3rdparty/pdfjs/*
|
49
.eslintrc
49
.eslintrc
@ -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
|
@ -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
|
||||
------
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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]+
|
||||
|
||||
|
@ -2,4 +2,4 @@
|
||||
|
||||
codecov==2.0.5
|
||||
coverage==4.2
|
||||
requests==2.10.0
|
||||
requests==2.11.0
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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),
|
||||
|
@ -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!")
|
||||
|
@ -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)
|
||||
|
@ -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'
|
||||
|
369
qutebrowser/browser/webelem.py
Normal file
369
qutebrowser/browser/webelem.py
Normal 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
|
176
qutebrowser/browser/webengine/webengineelem.py
Normal file
176
qutebrowser/browser/webengine/webengineelem.py
Normal 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
|
@ -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
|
||||
|
@ -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)))
|
||||
|
@ -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)
|
@ -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.
|
||||
|
@ -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)))
|
||||
|
@ -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):
|
||||
|
@ -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()
|
||||
|
||||
|
@ -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'),
|
||||
]
|
||||
|
35
qutebrowser/javascript/.eslintrc.yaml
Normal file
35
qutebrowser/javascript/.eslintrc.yaml
Normal 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"
|
@ -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();
|
||||
})();
|
||||
|
@ -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;
|
||||
})();
|
||||
|
80
qutebrowser/javascript/webelem.js
Normal file
80
qutebrowser/javascript/webelem.js
Normal 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;
|
||||
})();
|
@ -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
|
||||
|
@ -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',
|
||||
|
@ -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():
|
||||
|
11
tests/manual/completion/changing_title.html
Normal file
11
tests/manual/completion/changing_title.html
Normal 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>
|
@ -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
|
||||
|
@ -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')
|
||||
|
@ -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
24
tox.ini
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user