This commit is contained in:
Jan Verbeek 2016-08-10 20:54:54 +02:00
commit db0f8fffcd
95 changed files with 2633 additions and 1245 deletions

View File

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

View File

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

View File

@ -28,6 +28,10 @@ Added
completion category headers.
- New `:debug-log-capacity` command to adjust how many lines are logged into RAM
(to report bugs which are difficult to reproduce).
- New `hide-unmatched-rapid-hints` option to not hide hint unmatched hint labels
in rapid mode.
- New `{clipboard}` and `{primary}` replacements for the commandline which
replace the `:paste` command.
Changed
~~~~~~~
@ -44,6 +48,47 @@ Changed
(i.e. to open it at the position it would be opened if it was a clicked link)
- `:download-open` and `:prompt-open-download` now have an optional `cmdline`
argument to pass a commandline to open the download with.
- `:yank` now has a position argument to select what to yank instead of using
flags.
- 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.
- `:hint` now has a `--mode <mode>` flag to override the hint mode configured
using the `hints -> mode` setting.
- With `new-instance-open-target` set to a tab option, the tab is now opened in
the most recently focused (instead of the last opened) window. This can be
configured with the new `new-instance-open-target.window` setting.
It can also be set to `last-visible` to show the pages in the most recently
visible window.
- Word hints now are more clever about getting the element text from some elements.
- Completions for `:help` and `:bind` now also show hidden commands
- The `:buffer` completion now also filters using the first column (id).
- `:undo` has been improved to reopen tabs at the position they were closed.
Deprecated
~~~~~~~~~~
- The `:paste` command got deprecated as `:open` with `{clipboard}` and
`{primary}` can be used instead.
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.
- The `ui -> hide-mouse-cursor` setting since it was completely broken and
nobody seemed to care.
Fixed
~~~~~
- `:bind` can now be used to bind to an alias (binding by editing `keys.conf`
already worked before)
- The command completion now updates correctly when changing aliases
v0.8.3 (unreleased)
-------------------
@ -52,6 +97,8 @@ Fixed
~~~~~
- Fixed crash when doing `:<space><enter>`, another corner-case introduced in v0.8.0
- Fixed `:open-editor` (`<Ctrl-e>`) on Windows
- Fixed crash when setting `general -> auto-save-interval` to a too big value.
v0.8.2
------

View File

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

View File

@ -145,14 +145,14 @@ Contributors, sorted by the number of commits in descending order:
* Antoni Boucher
* Lamar Pavel
* Bruno Oliveira
* Jan Verbeek
* Alexander Cogneau
* Felix Van der Jeugt
* Jakub Klinkovský
* Martin Tournoij
* Marshall Lochbaum
* Jakub Klinkovský
* Felix Van der Jeugt
* Martin Tournoij
* Raphael Pierzina
* Joel Torstensson
* Jan Verbeek
* Patric Schmitz
* Tarcisio Fedrizzi
* Claude
@ -175,6 +175,7 @@ Contributors, sorted by the number of commits in descending order:
* Clayton Craft
* nanjekyejoannah
* Oliver Caldwell
* Niklas Haas
* Jonas Schürmann
* error800
* Liam BEGUIN
@ -212,6 +213,7 @@ Contributors, sorted by the number of commits in descending order:
* adam
* Samir Benmendil
* Regina Hug
* Michael Hoang
* Mathias Fussenegger
* Marcelo Santos
* Jean-Louis Fuchs
@ -225,6 +227,7 @@ Contributors, sorted by the number of commits in descending order:
* haxwithaxe
* evan
* dylan araps
* addictedtoflames
* Xitian9
* Tomas Orsava
* Tom Janson
@ -233,6 +236,7 @@ Contributors, sorted by the number of commits in descending order:
* Thiago Barroso Perrotta
* Sorokin Alexei
* Noah Huesser
* Moez Bouhlel
* Matthias Lisin
* Marcel Schilling
* Julie Engel

View File

@ -1,3 +1,7 @@
// DO NOT EDIT THIS FILE DIRECTLY!
// It is autogenerated from docstrings by running:
// $ python3 scripts/dev/src2asciidoc.py
= Commands
== Normal commands
@ -34,7 +38,6 @@
|<<messages,messages>>|Show a log of past messages.
|<<navigate,navigate>>|Open typical prev/next links or navigate using the URL path.
|<<open,open>>|Open a URL in the current/[count]th tab.
|<<paste,paste>>|Open a page from the clipboard.
|<<print,print>>|Print the current/[count]th tab.
|<<quickmark-add,quickmark-add>>|Add a new quickmark.
|<<quickmark-del,quickmark-del>>|Delete a quickmark.
@ -66,8 +69,7 @@
|<<undo,undo>>|Re-open a closed tab (optionally skipping [count] closed tabs).
|<<view-source,view-source>>|Show the source of the current page.
|<<wq,wq>>|Save open pages and quit.
|<<yank,yank>>|Yank the current URL/title to the clipboard or primary selection.
|<<yank-selected,yank-selected>>|Yank the selected text to the clipboard or primary selection.
|<<yank,yank>>|Yank something to the clipboard or primary selection.
|<<zoom,zoom>>|Set the zoom level for the current tab.
|<<zoom-in,zoom-in>>|Increase the zoom level for the current tab.
|<<zoom-out,zoom-out>>|Decrease the zoom level for the current tab.
@ -111,6 +113,7 @@ Bind a key to a command.
==== note
* This command does not split arguments after the last argument and handles quotes literally.
* With this command, +;;+ is interpreted literally instead of splitting off a second command.
* This command does not replace variables like +\{url\}+.
[[bookmark-add]]
=== bookmark-add
@ -320,12 +323,12 @@ Show help about a command or setting.
[[hint]]
=== hint
Syntax: +:hint [*--rapid*] ['group'] ['target'] ['args' ['args' ...]]+
Syntax: +:hint [*--rapid*] [*--mode* 'mode'] ['group'] ['target'] ['args' ['args' ...]]+
Start hinting.
==== positional arguments
* +'group'+: The hinting mode to use.
* +'group'+: The element types to hint.
- `all`: All clickable elements.
- `links`: Only links.
@ -376,6 +379,15 @@ Start hinting.
* +*-r*+, +*--rapid*+: Whether to do rapid hinting. This is only possible with targets `tab` (with background-tabs=true), `tab-bg`,
`window`, `run`, `hover`, `userscript` and `spawn`.
* +*-m*+, +*--mode*+: The hinting mode to use.
- `number`: Use numeric hints.
- `letter`: Use the chars in the hints->chars settings.
- `word`: Use hint words based on the html elements and the
extra words.
==== note
* This command does not split arguments after the last argument and handles quotes literally.
@ -425,6 +437,7 @@ Execute a command after some time.
==== note
* This command does not split arguments after the last argument and handles quotes literally.
* With this command, +;;+ is interpreted literally instead of splitting off a second command.
* This command does not replace variables like +\{url\}+.
[[messages]]
=== messages
@ -473,6 +486,8 @@ Syntax: +:open [*--implicit*] [*--bg*] [*--tab*] [*--window*] ['url']+
Open a URL in the current/[count]th tab.
If the URL contains newlines, each line gets opened in its own tab.
==== positional arguments
* +'url'+: The URL to open.
@ -489,20 +504,6 @@ The tab index to open the URL in.
==== note
* This command does not split arguments after the last argument and handles quotes literally.
[[paste]]
=== paste
Syntax: +:paste [*--sel*] [*--tab*] [*--bg*] [*--window*]+
Open a page from the clipboard.
If the pasted text contains newlines, each line gets opened in its own tab.
==== optional arguments
* +*-s*+, +*--sel*+: Use the primary selection instead of the clipboard.
* +*-t*+, +*--tab*+: Open in a new tab.
* +*-b*+, +*--bg*+: Open in a background tab.
* +*-w*+, +*--window*+: Open in new window.
[[print]]
=== print
Syntax: +:print [*--preview*] [*--pdf* 'file']+
@ -592,6 +593,7 @@ Repeat a given command.
==== note
* This command does not split arguments after the last argument and handles quotes literally.
* With this command, +;;+ is interpreted literally instead of splitting off a second command.
* This command does not replace variables like +\{url\}+.
[[report]]
=== report
@ -853,25 +855,25 @@ Save open pages and quit.
[[yank]]
=== yank
Syntax: +:yank [*--title*] [*--sel*] [*--domain*] [*--pretty*]+
Syntax: +:yank [*--sel*] [*--keep*] ['what']+
Yank the current URL/title to the clipboard or primary selection.
Yank something to the clipboard or primary selection.
==== optional arguments
* +*-t*+, +*--title*+: Yank the title instead of the URL.
* +*-s*+, +*--sel*+: Use the primary selection instead of the clipboard.
* +*-d*+, +*--domain*+: Yank only the scheme, domain, and port number.
* +*-p*+, +*--pretty*+: Yank the URL in pretty decoded form.
==== positional arguments
* +'what'+: What to yank.
- `url`: The current URL.
- `pretty-url`: The URL in pretty decoded form.
- `title`: The current page's title.
- `domain`: The current scheme, domain, and port number.
- `selection`: The selection under the cursor.
[[yank-selected]]
=== yank-selected
Syntax: +:yank-selected [*--sel*] [*--keep*]+
Yank the selected text to the clipboard or primary selection.
==== optional arguments
* +*-s*+, +*--sel*+: Use the primary selection instead of the clipboard.
* +*-k*+, +*--keep*+: If given, stay in visual mode after yanking.
* +*-k*+, +*--keep*+: Stay in visual mode after yanking the selection.
[[zoom]]
=== zoom
@ -912,8 +914,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.
@ -990,13 +991,14 @@ Go back in the commandline history.
=== completion-item-del
Delete the current completion item.
[[completion-item-next]]
=== completion-item-next
Select the next completion item.
[[completion-item-focus]]
=== completion-item-focus
Syntax: +:completion-item-focus 'which'+
[[completion-item-prev]]
=== completion-item-prev
Select the previous completion item.
Shift the focus of the completion menu to another item.
==== positional arguments
* +'which'+: 'next' or 'prev'
[[drop-selection]]
=== drop-selection

View File

@ -1,3 +1,7 @@
// DO NOT EDIT THIS FILE DIRECTLY!
// It is autogenerated from docstrings by running:
// $ python3 scripts/dev/src2asciidoc.py
= Settings
.Quick reference for section ``general''
@ -19,6 +23,7 @@
|<<general-site-specific-quirks,site-specific-quirks>>|Enable workarounds for broken sites.
|<<general-default-encoding,default-encoding>>|Default encoding to use for websites.
|<<general-new-instance-open-target,new-instance-open-target>>|How to open links in an existing instance if a new one is launched.
|<<general-new-instance-open-target.window,new-instance-open-target.window>>|Which window to choose when opening links as new tabs.
|<<general-log-javascript-console,log-javascript-console>>|How to log javascript console messages.
|<<general-save-session,save-session>>|Whether to always save the open pages.
|<<general-session-default-name,session-default-name>>|The name of the session to save by default, or empty for the last loaded session.
@ -45,7 +50,6 @@
|<<ui-hide-statusbar,hide-statusbar>>|Whether to hide the statusbar unless a message is shown.
|<<ui-statusbar-padding,statusbar-padding>>|Padding for statusbar (top, bottom, left, right).
|<<ui-window-title-format,window-title-format>>|The format to use for the window title. The following placeholders are defined:
|<<ui-hide-mouse-cursor,hide-mouse-cursor>>|Whether to hide the mouse cursor.
|<<ui-modal-js-dialog,modal-js-dialog>>|Use standard JavaScript modal dialog for alert() and confirm()
|<<ui-hide-wayland-decoration,hide-wayland-decoration>>|Hide the window decoration when using wayland (requires restart)
|<<ui-keyhint-blacklist,keyhint-blacklist>>|Keychains that shouldn't be shown in the keyhint dialog
@ -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,10 +186,11 @@
|<<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.
|<<hints-hide-unmatched-rapid-hints,hide-unmatched-rapid-hints>>|Controls hiding unmatched hints in rapid mode.
|==============
.Quick reference for section ``colors''
@ -443,6 +448,18 @@ Valid values:
Default: +pass:[tab]+
[[general-new-instance-open-target.window]]
=== new-instance-open-target.window
Which window to choose when opening links as new tabs.
Valid values:
* +last-opened+: Open new tabs in the last opened window.
* +last-focused+: Open new tabs in the most recently focused window.
* +last-visible+: Open new tabs in the most recently visible window.
Default: +pass:[last-focused]+
[[general-log-javascript-console]]
=== log-javascript-console
How to log javascript console messages.
@ -455,8 +472,6 @@ Valid values:
Default: +pass:[debug]+
This setting is only available with the QtWebKit backend.
[[general-save-session]]
=== save-session
Whether to always save the open pages.
@ -646,17 +661,6 @@ The format to use for the window title. The following placeholders are defined:
Default: +pass:[{perc}{title}{title_sep}qutebrowser]+
[[ui-hide-mouse-cursor]]
=== hide-mouse-cursor
Whether to hide the mouse cursor.
Valid values:
* +true+
* +false+
Default: +pass:[false]+
[[ui-modal-js-dialog]]
=== modal-js-dialog
Use standard JavaScript modal dialog for alert() and confirm()
@ -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]+
@ -1694,6 +1698,17 @@ Valid values:
Default: +pass:[python]+
[[hints-hide-unmatched-rapid-hints]]
=== hide-unmatched-rapid-hints
Controls hiding unmatched hints in rapid mode.
Valid values:
* +true+
* +false+
Default: +pass:[true]+
== searchengines
Definitions of search engines which can be used via the address bar.
The searchengine named `DEFAULT` is used when `general -> auto-search` is true and something else than a URL was entered to be opened. Other search engines can be used by prepending the search engine name to the search term, e.g. `:open google qutebrowser`. The string `{}` will be replaced by the search term, use `{{` and `}}` for literal `{`/`}` signs.

View File

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

View File

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

View File

@ -1,2 +1,2 @@
pip==8.1.2
setuptools==25.1.4
setuptools==25.1.6

View File

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

View File

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

View File

@ -2,7 +2,12 @@ bzr+lp:beautifulsoup
git+https://github.com/cherrypy/cherrypy.git
hg+https://bitbucket.org/ned/coveragepy
git+https://github.com/micheles/decorator.git
git+https://github.com/pallets/flask.git
# We need to use flask < 0.11 because of
# https://github.com/Runscope/httpbin/issues/290
# git+https://github.com/pallets/flask.git
Flask==0.10.1 # rq.filter: < 0.11.0
git+https://github.com/miracle2k/python-glob2.git
git+https://github.com/Runscope/httpbin.git
git+https://github.com/HypothesisWorks/hypothesis-python.git

View File

@ -18,16 +18,16 @@ 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
pytest-qt==2.0.0
pytest-repeat==0.3.0
pytest-rerunfailures==2.0.0
pytest-repeat==0.4.0
pytest-rerunfailures==2.0.1
pytest-travis-fold==1.2.0
pytest-warnings==0.1.0
pytest-xvfb==0.2.0
pytest-xvfb==0.2.1
six==1.10.0
vulture==0.10
Werkzeug==0.11.10

View File

@ -3,4 +3,4 @@
pluggy==0.3.1
py==1.4.31
tox==2.3.1
virtualenv==15.0.2
virtualenv==15.0.3

View File

@ -33,9 +33,9 @@ import tokenize
from PyQt5.QtWidgets import QApplication, QWidget
from PyQt5.QtWebKit import QWebSettings
from PyQt5.QtGui import QDesktopServices, QPixmap, QIcon, QCursor, QWindow
from PyQt5.QtGui import QDesktopServices, QPixmap, QIcon, QWindow
from PyQt5.QtCore import (pyqtSlot, qInstallMessageHandler, QTimer, QUrl,
QObject, Qt, QEvent, pyqtSignal)
QObject, QEvent, pyqtSignal)
try:
import hunter
except ImportError:
@ -46,8 +46,8 @@ import qutebrowser.resources
from qutebrowser.completion.models import instances as completionmodels
from qutebrowser.commands import cmdutils, runners, cmdexc
from qutebrowser.config import style, config, websettings, configexc
from qutebrowser.browser import urlmarks, adblock
from qutebrowser.browser.webkit import cookies, cache, history, downloads
from qutebrowser.browser import urlmarks, adblock, history
from qutebrowser.browser.webkit import cookies, cache, downloads
from qutebrowser.browser.webkit.network import (qutescheme, proxy,
networkmanager)
from qutebrowser.mainwindow import mainwindow
@ -339,19 +339,20 @@ def _save_version():
def on_focus_changed(_old, new):
"""Register currently focused main window in the object registry."""
if not isinstance(new, QWidget) and new is not None:
if new is None:
return
if not isinstance(new, QWidget):
log.misc.debug("on_focus_changed called with non-QWidget {!r}".format(
new))
return
if new is None or not isinstance(new, mainwindow.MainWindow):
try:
objreg.delete('last-focused-main-window')
except KeyError:
pass
qApp.restoreOverrideCursor()
else:
objreg.register('last-focused-main-window', new.window(), update=True)
_maybe_hide_mouse_cursor()
window = new.window()
if isinstance(window, mainwindow.MainWindow):
objreg.register('last-focused-main-window', window, update=True)
# A focused window must also be visible, and in this case we should
# consider it as the most recently looked-at window
objreg.register('last-visible-main-window', window, update=True)
def open_desktopservices_url(url):
@ -362,17 +363,6 @@ def open_desktopservices_url(url):
tabbed_browser.tabopen(url)
@config.change_filter('ui', 'hide-mouse-cursor', function=True)
def _maybe_hide_mouse_cursor():
"""Hide the mouse cursor if it isn't yet and it's configured."""
if config.get('ui', 'hide-mouse-cursor'):
if qApp.overrideCursor() is not None:
return
qApp.setOverrideCursor(QCursor(Qt.BlankCursor))
else:
qApp.restoreOverrideCursor()
def _init_modules(args, crash_handler):
"""Initialize all 'modules' which need to be initialized.
@ -434,8 +424,6 @@ def _init_modules(args, crash_handler):
os.environ['QT_WAYLAND_DISABLE_WINDOWDECORATION'] = '1'
else:
os.environ.pop('QT_WAYLAND_DISABLE_WINDOWDECORATION', None)
_maybe_hide_mouse_cursor()
objreg.get('config').changed.connect(_maybe_hide_mouse_cursor)
temp_downloads = downloads.TempDownloadManager(qApp)
objreg.register('temporary-downloads', temp_downloads)

View File

@ -29,6 +29,7 @@ from qutebrowser.keyinput import modeman
from qutebrowser.config import config
from qutebrowser.utils import utils, objreg, usertypes, message, log, qtutils
from qutebrowser.misc import miscwidgets
from qutebrowser.browser import mouse
tab_id_gen = itertools.count(0)
@ -455,6 +456,7 @@ class AbstractTab(QWidget):
url_changed = pyqtSignal(QUrl)
shutting_down = pyqtSignal()
contents_size_changed = pyqtSignal(QSizeF)
add_history_item = pyqtSignal(QUrl, QUrl, str) # url, requested url, title
def __init__(self, win_id, parent=None):
self.win_id = win_id
@ -480,6 +482,7 @@ class AbstractTab(QWidget):
self._progress = 0
self._has_ssl_errors = False
self._load_status = usertypes.LoadStatus.none
self._mouse_event_filter = mouse.MouseEventFilter(self, parent=self)
self.backend = None
def _set_widget(self, widget):
@ -493,6 +496,10 @@ class AbstractTab(QWidget):
self.search._widget = widget
self.printing._widget = widget
widget.mouse_wheel_zoom.connect(self.zoom._on_mouse_wheel_zoom)
self._install_event_filter()
def _install_event_filter(self):
raise NotImplementedError
def _set_load_status(self, val):
"""Setter for load_status."""
@ -533,6 +540,13 @@ class AbstractTab(QWidget):
if not self.title():
self.title_changed.emit(self.url().toDisplayString())
@pyqtSlot()
def _on_history_trigger(self):
"""Emit add_history_item when triggered by backend-specific signal."""
url = self.url()
requested_url = self.url(requested=True)
self.add_history_item.emit(url, requested_url, self.title())
@pyqtSlot(int)
def _on_load_progress(self, perc):
self._progress = perc
@ -542,7 +556,7 @@ class AbstractTab(QWidget):
def _on_ssl_errors(self):
self._has_ssl_errors = True
def url(self):
def url(self, requested=False):
raise NotImplementedError
def progress(self):
@ -613,6 +627,15 @@ class AbstractTab(QWidget):
"""
raise NotImplementedError
def find_focus_element(self, callback):
"""Find the focused element on the page async.
Args:
callback: The callback to be called when the search finished.
Called with a WebEngineElement or None.
"""
raise NotImplementedError
def __repr__(self):
try:
url = utils.elide(self.url().toDisplayString(QUrl.EncodeUnicode),

View File

@ -40,7 +40,7 @@ import pygments.formatters
from qutebrowser.commands import userscripts, cmdexc, cmdutils, runners
from qutebrowser.config import config, configexc
from qutebrowser.browser import urlmarks, browsertab, inspector, navigate
from qutebrowser.browser.webkit import webelem, downloads, mhtml
from qutebrowser.browser.webkit import webkitelem, downloads, mhtml
from qutebrowser.keyinput import modeman
from qutebrowser.utils import (message, usertypes, log, qtutils, urlutils,
objreg, utils, typing, javascript)
@ -236,6 +236,8 @@ class CommandDispatcher:
bg=False, tab=False, window=False, count=None):
"""Open a URL in the current/[count]th tab.
If the URL contains newlines, each line gets opened in its own tab.
Args:
url: The URL to open.
bg: Open in a new background tab.
@ -247,35 +249,73 @@ class CommandDispatcher:
"""
if url is None:
if tab or bg or window:
url = config.get('general', 'default-page')
urls = [config.get('general', 'default-page')]
else:
raise cmdexc.CommandError("No URL given, but -t/-b/-w is not "
"set!")
else:
try:
url = objreg.get('quickmark-manager').get(url)
except urlmarks.Error:
try:
url = urlutils.fuzzy_url(url)
except urlutils.InvalidUrlError as e:
# We don't use cmdexc.CommandError here as this can be
# called async from edit_url
message.error(self._win_id, str(e))
return
if tab or bg or window:
self._open(url, tab, bg, window, not implicit)
else:
curtab = self._cntwidget(count)
if curtab is None:
if count is None:
# We want to open a URL in the current tab, but none exists
# yet.
self._tabbed_browser.tabopen(url)
else:
# Explicit count with a tab that doesn't exist.
return
urls = self._parse_url_input(url)
for i, cur_url in enumerate(urls):
if not window and i > 0:
tab = False
bg = True
if tab or bg or window:
self._open(cur_url, tab, bg, window, not implicit)
else:
curtab.openurl(url)
curtab = self._cntwidget(count)
if curtab is None:
if count is None:
# We want to open a URL in the current tab, but none
# exists yet.
self._tabbed_browser.tabopen(cur_url)
else:
# Explicit count with a tab that doesn't exist.
return
else:
curtab.openurl(cur_url)
def _parse_url(self, url, *, force_search=False):
"""Parse a URL or quickmark or search query.
Args:
url: The URL to parse.
force_search: Whether to force a search even if the content can be
interpreted as a URL or a path.
Return:
A URL that can be opened.
"""
try:
return objreg.get('quickmark-manager').get(url)
except urlmarks.Error:
try:
return urlutils.fuzzy_url(url, force_search=force_search)
except urlutils.InvalidUrlError as e:
# We don't use cmdexc.CommandError here as this can be
# called async from edit_url
message.error(self._win_id, str(e))
return None
def _parse_url_input(self, url):
"""Parse a URL or newline-separated list of URLs.
Args:
url: The URL or list to parse.
Return:
A list of URLs that can be opened.
"""
force_search = False
urllist = [u for u in url.split('\n') if u.strip()]
if (len(urllist) > 1 and not urlutils.is_url(urllist[0]) and
urlutils.get_path_if_valid(urllist[0], check_exists=True)
is None):
urllist = [url]
force_search = True
for cur_url in urllist:
parsed = self._parse_url(cur_url, force_search=force_search)
if parsed is not None:
yield parsed
@cmdutils.register(instance='command-dispatcher', name='reload',
scope='window')
@ -439,8 +479,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):
@ -620,30 +659,44 @@ class CommandDispatcher:
"representation.")
@cmdutils.register(instance='command-dispatcher', scope='window')
def yank(self, title=False, sel=False, domain=False, pretty=False):
"""Yank the current URL/title to the clipboard or primary selection.
@cmdutils.argument('what', choices=['selection', 'url', 'pretty-url',
'title', 'domain'])
def yank(self, what='url', sel=False, keep=False):
"""Yank something to the clipboard or primary selection.
Args:
what: What to yank.
- `url`: The current URL.
- `pretty-url`: The URL in pretty decoded form.
- `title`: The current page's title.
- `domain`: The current scheme, domain, and port number.
- `selection`: The selection under the cursor.
sel: Use the primary selection instead of the clipboard.
title: Yank the title instead of the URL.
domain: Yank only the scheme, domain, and port number.
pretty: Yank the URL in pretty decoded form.
keep: Stay in visual mode after yanking the selection.
"""
if title:
if what == 'title':
s = self._tabbed_browser.page_title(self._current_index())
what = 'title'
elif domain:
elif what == 'domain':
port = self._current_url().port()
s = '{}://{}{}'.format(self._current_url().scheme(),
self._current_url().host(),
':' + str(port) if port > -1 else '')
what = 'domain'
else:
elif what in ['url', 'pretty-url']:
flags = QUrl.RemovePassword
if not pretty:
if what != 'pretty-url':
flags |= QUrl.FullyEncoded
s = self._current_url().toString(flags)
what = 'URL'
what = 'URL' # For printing
elif what == 'selection':
caret = self._current_widget().caret
s = caret.selection()
if not caret.has_selection() or not s:
message.info(self._win_id, "Nothing to yank")
return
else: # pragma: no cover
raise ValueError("Invalid value {!r} for `what'.".format(what))
if sel and utils.supports_selection():
target = "primary selection"
@ -652,8 +705,15 @@ class CommandDispatcher:
target = "clipboard"
utils.set_clipboard(s, selection=sel)
message.info(self._win_id, "Yanked {} to {}: {}".format(
what, target, s))
if what != 'selection':
message.info(self._win_id, "Yanked {} to {}: {}".format(
what, target, s))
else:
message.info(self._win_id, "{} {} yanked to {}".format(
len(s), "char" if len(s) == 1 else "chars", target))
if not keep:
modeman.maybe_leave(self._win_id, KeyMode.caret,
"yank selected")
@cmdutils.register(instance='command-dispatcher', scope='window')
@cmdutils.argument('count', count=True)
@ -776,7 +836,8 @@ class CommandDispatcher:
else:
raise cmdexc.CommandError("Last tab")
@cmdutils.register(instance='command-dispatcher', scope='window')
@cmdutils.register(instance='command-dispatcher', scope='window',
deprecated="Use :open {clipboard}")
def paste(self, sel=False, tab=False, bg=False, window=False):
"""Open a page from the clipboard.
@ -790,15 +851,12 @@ class CommandDispatcher:
window: Open in new window.
"""
force_search = False
if sel and utils.supports_selection():
target = "Primary selection"
else:
if not utils.supports_selection():
sel = False
target = "Clipboard"
text = utils.get_clipboard(selection=sel)
if not text.strip():
raise cmdexc.CommandError("{} is empty.".format(target))
log.misc.debug("{} contained: {!r}".format(target, text))
try:
text = utils.get_clipboard(selection=sel)
except utils.ClipboardError as e:
raise cmdexc.CommandError(e)
text_urls = [u for u in text.split('\n') if u.strip()]
if (len(text_urls) > 1 and not urlutils.is_url(text_urls[0]) and
urlutils.get_path_if_valid(
@ -959,7 +1017,7 @@ class CommandDispatcher:
self._tabbed_browser.setUpdatesEnabled(True)
@cmdutils.register(instance='command-dispatcher', scope='window',
maxsplit=0)
maxsplit=0, no_replace_variables=True)
def spawn(self, cmdline, userscript=False, verbose=False, detach=False):
"""Spawn a command in a shell.
@ -1165,14 +1223,13 @@ class CommandDispatcher:
current page's url.
"""
if url is None:
url = self._current_url().toString(QUrl.RemovePassword
| QUrl.FullyEncoded)
url = self._current_url().toString(QUrl.RemovePassword |
QUrl.FullyEncoded)
try:
objreg.get('bookmark-manager').delete(url)
except KeyError:
raise cmdexc.CommandError("Bookmark '{}' not found!".format(url))
@cmdutils.register(instance='command-dispatcher', hide=True,
scope='window')
def follow_selected(self, *, tab=False):
@ -1388,29 +1445,31 @@ 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)
modes=[KeyMode.insert], hide=True, scope='window')
def open_editor(self):
"""Open an external editor with the currently selected form field.
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.
@ -1423,7 +1482,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',
@ -1448,8 +1507,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!")
@ -1736,31 +1795,6 @@ class CommandDispatcher:
"""Move the cursor or selection to the end of the document."""
self._current_widget().caret.move_to_end_of_document()
@cmdutils.register(instance='command-dispatcher', scope='window')
def yank_selected(self, sel=False, keep=False):
"""Yank the selected text to the clipboard or primary selection.
Args:
sel: Use the primary selection instead of the clipboard.
keep: If given, stay in visual mode after yanking.
"""
caret = self._current_widget().caret
s = caret.selection()
if not caret.has_selection() or len(s) == 0:
message.info(self._win_id, "Nothing to yank")
return
if sel and utils.supports_selection():
target = "primary selection"
else:
sel = False
target = "clipboard"
utils.set_clipboard(s, sel)
message.info(self._win_id, "{} {} yanked to {}".format(
len(s), "char" if len(s) == 1 else "chars", target))
if not keep:
modeman.maybe_leave(self._win_id, KeyMode.caret, "yank selected")
@cmdutils.register(instance='command-dispatcher', hide=True,
modes=[KeyMode.caret], scope='window')
def toggle_selection(self):

View File

@ -28,12 +28,11 @@ from string import ascii_lowercase
from PyQt5.QtCore import (pyqtSignal, pyqtSlot, QObject, QEvent, Qt, QUrl,
QTimer)
from PyQt5.QtGui import QMouseEvent
from PyQt5.QtWebKit import QWebElement
from PyQt5.QtWebKitWidgets import QWebPage
from qutebrowser.config import config
from qutebrowser.keyinput import modeman, modeparsers
from qutebrowser.browser.webkit import webelem
from qutebrowser.browser import webelem
from qutebrowser.commands import userscripts, cmdexc, cmdutils, runners
from qutebrowser.utils import usertypes, log, qtutils, message, objreg, utils
@ -95,6 +94,7 @@ class HintContext:
self.args = []
self.tab = None
self.group = None
self.hint_mode = None
def get_args(self, urlstr):
"""Get the arguments, with {hint-url} replaced by the given URL."""
@ -374,7 +374,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',
@ -394,7 +394,7 @@ class HintManager(QObject):
Return:
A list of hint strings, in the same order as the elements.
"""
hint_mode = config.get('hints', 'mode')
hint_mode = self._context.hint_mode
if hint_mode == 'word':
try:
return self._word_hinter.hint(elems)
@ -516,7 +516,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):
@ -548,7 +548,7 @@ class HintManager(QObject):
# Make text uppercase if set in config
if (config.get('hints', 'uppercase') and
config.get('hints', 'mode') == 'letter'):
self._context.hint_mode == 'letter'):
attrs.append(('text-transform', 'uppercase !important'))
else:
attrs.append(('text-transform', 'none !important'))
@ -661,14 +661,14 @@ class HintManager(QObject):
backend=usertypes.Backend.QtWebKit)
@cmdutils.argument('win_id', win_id=True)
def start(self, rapid=False, group=webelem.Group.all, target=Target.normal,
*args, win_id):
*args, win_id, mode=None):
"""Start hinting.
Args:
rapid: Whether to do rapid hinting. This is only possible with
targets `tab` (with background-tabs=true), `tab-bg`,
`window`, `run`, `hover`, `userscript` and `spawn`.
group: The hinting mode to use.
group: The element types to hint.
- `all`: All clickable elements.
- `links`: Only links.
@ -695,6 +695,13 @@ class HintManager(QObject):
link.
- `spawn`: Spawn a command.
mode: The hinting mode to use.
- `number`: Use numeric hints.
- `letter`: Use the chars in the hints->chars settings.
- `word`: Use hint words based on the html elements and the
extra words.
*args: Arguments for spawn/userscript/run/fill.
- With `spawn`: The executable and arguments to spawn.
@ -733,11 +740,15 @@ class HintManager(QObject):
raise cmdexc.CommandError("Rapid hinting makes no sense with "
"target {}!".format(name))
if mode is None:
mode = config.get('hints', 'mode')
self._check_args(target, *args)
self._context = HintContext()
self._context.tab = tab
self._context.target = target
self._context.rapid = rapid
self._context.hint_mode = mode
try:
self._context.baseurl = tabbed_browser.current_url()
except qtutils.QtValueError:
@ -749,6 +760,13 @@ class HintManager(QObject):
self._context.tab.find_all_elements(selector, self._start_cb,
only_visible=True)
def current_mode(self):
"""Return the currently active hinting mode (or None otherwise)."""
if self._context is None:
return None
return self._context.hint_mode
def handle_partial_key(self, keystr):
"""Handle a new partial keypress."""
log.hints.debug("Handling new keystring: '{}'".format(keystr))
@ -765,9 +783,12 @@ class HintManager(QObject):
# hidden element which matches again -> show it
self._show_elem(elem.label)
else:
# element doesn't match anymore -> hide it
self._hide_elem(elem.label)
except webelem.IsNullError:
# element doesn't match anymore -> hide it, unless in rapid
# mode and hide-unmatched-rapid-hints is false (see #1799)
if (not self._context.rapid or
config.get('hints', 'hide-unmatched-rapid-hints')):
self._hide_elem(elem.label)
except webelem.Error:
pass
def _filter_number_hints(self):
@ -782,7 +803,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 +834,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,10 +865,10 @@ 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':
if self._context.hint_mode == 'number':
visible = self._filter_number_hints()
else:
visible = self._filter_non_number_hints()
@ -961,7 +982,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 +1043,18 @@ 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"],
"textarea": ["name", "placeholder"],
"button": ["text"]
})
return (attr_extractors[attr](elem)

View File

@ -22,11 +22,11 @@
import time
import collections
from PyQt5.QtCore import pyqtSignal, QUrl, QObject
from PyQt5.QtWebKit import QWebHistoryInterface
from PyQt5.QtCore import pyqtSignal, pyqtSlot, QUrl, QObject
from qutebrowser.commands import cmdutils
from qutebrowser.utils import utils, objreg, standarddir, log, qtutils
from qutebrowser.utils import (utils, objreg, standarddir, log, qtutils,
usertypes)
from qutebrowser.config import config
from qutebrowser.misc import lineparser
@ -108,34 +108,6 @@ class Entry:
return cls(atime, url, title, redirect=redirect)
class WebHistoryInterface(QWebHistoryInterface):
"""Glue code between WebHistory and Qt's QWebHistoryInterface.
Attributes:
_history: The WebHistory object.
"""
def __init__(self, webhistory, parent=None):
super().__init__(parent)
self._history = webhistory
def addHistoryEntry(self, url_string):
"""Required for a QWebHistoryInterface impl, obsoleted by add_url."""
pass
def historyContains(self, url_string):
"""Called by WebKit to determine if a URL is contained in the history.
Args:
url_string: The URL (as string) to check for.
Return:
True if the url is in the history, False otherwise.
"""
return url_string in self._history.history_dict
class WebHistory(QObject):
"""The global history of visited pages.
@ -284,8 +256,19 @@ class WebHistory(QObject):
self._saved_count = 0
self.cleared.emit()
@pyqtSlot(QUrl, QUrl, str)
def add_from_tab(self, url, requested_url, title):
"""Add a new history entry as slot, called from a BrowserTab."""
no_formatting = QUrl.UrlFormattingOption(0)
if (requested_url.isValid() and
not requested_url.matches(url, no_formatting)):
# If the url of the page is different than the url of the link
# originally clicked, save them both.
self.add_url(requested_url, title, redirect=True)
self.add_url(url, title)
def add_url(self, url, title="", *, redirect=False, atime=None):
"""Called by WebKit when a URL should be added to the history.
"""Called via add_from_tab when a URL should be added to the history.
Args:
url: A url (as QUrl) to add to the history.
@ -322,5 +305,7 @@ def init(parent=None):
parent=parent)
objreg.register('web-history', history)
interface = WebHistoryInterface(history, parent=history)
QWebHistoryInterface.setDefaultInterface(interface)
used_backend = usertypes.arg2backend[objreg.get('args').backend]
if used_backend == usertypes.Backend.QtWebKit:
from qutebrowser.browser.webkit import webkithistory
webkithistory.init(history)

View File

@ -22,7 +22,7 @@
import base64
import binascii
from PyQt5.QtWebKitWidgets import QWebInspector
from PyQt5.QtWidgets import QWidget
from qutebrowser.utils import log, objreg
from qutebrowser.misc import miscwidgets
@ -52,7 +52,7 @@ class WebInspectorError(Exception):
pass
class AbstractWebInspector(QWebInspector):
class AbstractWebInspector(QWidget):
"""A customized WebInspector which stores its geometry."""

View File

@ -0,0 +1,104 @@
# 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/>.
"""Mouse handling for a browser tab."""
from qutebrowser.config import config
from qutebrowser.utils import message, log
from PyQt5.QtCore import QObject, QEvent, Qt
class ChildEventFilter(QObject):
"""An event filter re-adding MouseEventFilter on ChildEvent.
This is needed because QtWebEngine likes to randomly change its
focusProxy...
FIXME:qtwebengine Add a test for this happening
"""
def __init__(self, eventfilter, widget, parent=None):
super().__init__(parent)
self._filter = eventfilter
assert widget is not None
self._widget = widget
def eventFilter(self, obj, event):
"""Act on ChildAdded events."""
if event.type() == QEvent.ChildAdded:
child = event.child()
log.mouse.debug("{} got new child {}, installing filter".format(
obj, child))
assert obj is self._widget
child.installEventFilter(self._filter)
return False
class MouseEventFilter(QObject):
"""Handle mouse events on a tab."""
def __init__(self, tab, parent=None):
super().__init__(parent)
self._tab = tab
self._handlers = {
QEvent.MouseButtonPress: self._handle_mouse_press,
}
def _handle_mouse_press(self, _obj, e):
"""Handle pressing of a mouse button."""
is_rocker_gesture = (config.get('input', 'rocker-gestures') and
e.buttons() == Qt.LeftButton | Qt.RightButton)
if e.button() in [Qt.XButton1, Qt.XButton2] or is_rocker_gesture:
self._mousepress_backforward(e)
return True
return False
def _mousepress_backforward(self, e):
"""Handle back/forward mouse button presses.
Args:
e: The QMouseEvent.
"""
if e.button() in [Qt.XButton1, Qt.LeftButton]:
# Back button on mice which have it, or rocker gesture
if self._tab.history.can_go_back():
self._tab.history.back()
else:
message.error(self._tab.win_id, "At beginning of history.",
immediately=True)
elif e.button() in [Qt.XButton2, Qt.RightButton]:
# Forward button on mice which have it, or rocker gesture
if self._tab.history.can_go_forward():
self._tab.history.forward()
else:
message.error(self._tab.win_id, "At end of history.",
immediately=True)
def eventFilter(self, obj, event):
"""Filter events going to a QWeb(Engine)View."""
evtype = event.type()
if evtype not in self._handlers:
return False
return self._handlers[evtype](obj, event)

View File

@ -17,14 +17,13 @@
# You should have received a copy of the GNU General Public License
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
"""Implementation of :navigate"""
"""Implementation of :navigate."""
import posixpath
from qutebrowser.browser.webkit import webelem
from qutebrowser.browser import webelem
from qutebrowser.config import config
from qutebrowser.utils import (usertypes, objreg, urlutils, log, message,
qtutils)
from qutebrowser.utils import objreg, urlutils, log, message, qtutils
class Error(Exception):
@ -109,11 +108,6 @@ def prevnext(*, browsertab, win_id, baseurl, prev=False,
background: True to open in a background tab.
window: True to open in a new window, False for the current one.
"""
# FIXME:qtwebengine have a proper API for this
if browsertab.backend == usertypes.Backend.QtWebEngine:
raise Error(":navigate prev/next is not supported yet with "
"QtWebEngine")
def _prevnext_cb(elems):
elem = _find_prevnext(prev, elems)
word = 'prev' if prev else 'forward'

View File

@ -0,0 +1,369 @@
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
# Copyright 2014-2016 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
#
# This file is part of qutebrowser.
#
# qutebrowser is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# qutebrowser is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
"""Generic web element related code.
Module attributes:
Group: Enum for different kinds of groups.
SELECTORS: CSS selectors for different groups of elements.
FILTERS: A dictionary of filter functions for the modes.
The filter for "links" filters javascript:-links and a-tags
without "href".
"""
import collections.abc
from PyQt5.QtCore import QUrl
from qutebrowser.config import config
from qutebrowser.utils import log, usertypes, utils, qtutils
Group = usertypes.enum('Group', ['all', 'links', 'images', 'url', 'prevnext',
'inputs'])
SELECTORS = {
Group.all: ('a, area, textarea, select, input:not([type=hidden]), button, '
'frame, iframe, link, [onclick], [onmousedown], [role=link], '
'[role=option], [role=button], img'),
Group.links: 'a, area, link, [role=link]',
Group.images: 'img',
Group.url: '[src], [href]',
Group.prevnext: 'a, area, button, link, [role=button]',
Group.inputs: ('input[type=text], input[type=email], input[type=url], '
'input[type=tel], input[type=number], '
'input[type=password], input[type=search], '
'input:not([type]), textarea'),
}
def filter_links(elem):
return 'href' in elem and QUrl(elem['href']).scheme() != 'javascript'
FILTERS = {
Group.links: filter_links,
Group.prevnext: filter_links,
}
class Error(Exception):
"""Base class for WebElement errors."""
pass
class AbstractWebElement(collections.abc.MutableMapping):
"""A wrapper around QtWebKit/QtWebEngine web element."""
def __eq__(self, other):
raise NotImplementedError
def __str__(self):
return self.text()
def __getitem__(self, key):
raise NotImplementedError
def __setitem__(self, key, val):
raise NotImplementedError
def __delitem__(self, key):
raise NotImplementedError
def __iter__(self):
raise NotImplementedError
def __len__(self):
raise NotImplementedError
def __repr__(self):
try:
html = self.debug_text()
except Error:
html = None
return utils.get_repr(self, html=html)
def frame(self):
"""Get the main frame of this element."""
# FIXME:qtwebengine get rid of this?
raise NotImplementedError
def geometry(self):
"""Get the geometry for this element."""
raise NotImplementedError
def document_element(self):
"""Get the document element of this element."""
# FIXME:qtwebengine get rid of this?
raise NotImplementedError
def create_inside(self, tagname):
"""Append the given element inside the current one."""
# FIXME:qtwebengine get rid of this?
raise NotImplementedError
def find_first(self, selector):
"""Find the first child based on the given CSS selector."""
# FIXME:qtwebengine get rid of this?
raise NotImplementedError
def style_property(self, name, *, strategy):
"""Get the element style resolved with the given strategy."""
raise NotImplementedError
def classes(self):
"""Get a list of classes assigned to this element."""
raise NotImplementedError
def tag_name(self):
"""Get the tag name of this element.
The returned name will always be lower-case.
"""
raise NotImplementedError
def outer_xml(self):
"""Get the full HTML representation of this element."""
raise NotImplementedError
def text(self, *, use_js=False):
"""Get the plain text content for this element.
Args:
use_js: Whether to use javascript if the element isn't
content-editable.
"""
# FIXME:qtwebengine what to do about use_js with WebEngine?
raise NotImplementedError
def set_text(self, text, *, use_js=False):
"""Set the given plain text.
Args:
use_js: Whether to use javascript if the element isn't
content-editable.
"""
# FIXME:qtwebengine what to do about use_js with WebEngine?
raise NotImplementedError
def set_inner_xml(self, xml):
"""Set the given inner XML."""
# FIXME:qtwebengine get rid of this?
raise NotImplementedError
def remove_from_document(self):
"""Remove the node from the document."""
# FIXME:qtwebengine get rid of this?
raise NotImplementedError
def set_style_property(self, name, value):
"""Set the element style."""
# FIXME:qtwebengine get rid of this?
raise NotImplementedError
def run_js_async(self, code, callback=None):
"""Run the given JS snippet async on the element."""
# FIXME:qtwebengine get rid of this?
raise NotImplementedError
def parent(self):
"""Get the parent element of this element."""
# FIXME:qtwebengine get rid of this?
raise NotImplementedError
def rect_on_view(self, *, elem_geometry=None, adjust_zoom=True,
no_js=False):
"""Get the geometry of the element relative to the webview.
Uses the getClientRects() JavaScript method to obtain the collection of
rectangles containing the element and returns the first rectangle which
is large enough (larger than 1px times 1px). If all rectangles returned
by getClientRects() are too small, falls back to elem.rect_on_view().
Skipping of small rectangles is due to <a> elements containing other
elements with "display:block" style, see
https://github.com/The-Compiler/qutebrowser/issues/1298
Args:
elem_geometry: The geometry of the element, or None.
Calling QWebElement::geometry is rather expensive so
we want to avoid doing it twice.
adjust_zoom: Whether to adjust the element position based on the
current zoom level.
no_js: Fall back to the Python implementation
"""
raise NotImplementedError
def is_visible(self, mainframe):
"""Check if the given element is visible in the given frame."""
# FIXME:qtwebengine get rid of this?
raise NotImplementedError
def is_writable(self):
"""Check whether an element is writable."""
return not ('disabled' in self or 'readonly' in self)
def is_content_editable(self):
"""Check if an element has a contenteditable attribute.
Args:
elem: The QWebElement to check.
Return:
True if the element has a contenteditable attribute,
False otherwise.
"""
try:
return self['contenteditable'].lower() not in ['false', 'inherit']
except KeyError:
return False
def _is_editable_object(self):
"""Check if an object-element is editable."""
if 'type' not in self:
log.webview.debug("<object> without type clicked...")
return False
objtype = self['type'].lower()
if objtype.startswith('application/') or 'classid' in self:
# Let's hope flash/java stuff has an application/* mimetype OR
# at least a classid attribute. Oh, and let's hope images/...
# DON'T have a classid attribute. HTML sucks.
log.webview.debug("<object type='{}'> clicked.".format(objtype))
return config.get('input', 'insert-mode-on-plugins')
else:
# Image/Audio/...
return False
def _is_editable_input(self):
"""Check if an input-element is editable.
Return:
True if the element is editable, False otherwise.
"""
try:
objtype = self['type'].lower()
except KeyError:
return self.is_writable()
else:
if objtype in ['text', 'email', 'url', 'tel', 'number', 'password',
'search']:
return self.is_writable()
else:
return False
def _is_editable_div(self):
"""Check if a div-element is editable.
Return:
True if the element is editable, False otherwise.
"""
# Beginnings of div-classes which are actually some kind of editor.
div_classes = ('CodeMirror', # Javascript editor over a textarea
'kix-', # Google Docs editor
'ace_') # http://ace.c9.io/
for klass in self.classes():
if any([klass.startswith(e) for e in div_classes]):
return True
return False
def is_editable(self, strict=False):
"""Check whether we should switch to insert mode for this element.
Args:
strict: Whether to do stricter checking so only fields where we can
get the value match, for use with the :editor command.
Return:
True if we should switch to insert mode, False otherwise.
"""
roles = ('combobox', 'textbox')
log.misc.debug("Checking if element is editable: {}".format(
repr(self)))
tag = self.tag_name()
if self.is_content_editable() and self.is_writable():
return True
elif self.get('role', None) in roles and self.is_writable():
return True
elif tag == 'input':
return self._is_editable_input()
elif tag == 'textarea':
return self.is_writable()
elif tag in ['embed', 'applet']:
# Flash/Java/...
return config.get('input', 'insert-mode-on-plugins') and not strict
elif tag == 'object':
return self._is_editable_object() and not strict
elif tag == 'div':
return self._is_editable_div() and not strict
else:
return False
def is_text_input(self):
"""Check if this element is some kind of text box."""
roles = ('combobox', 'textbox')
tag = self.tag_name()
return self.get('role', None) in roles or tag in ['input', 'textarea']
def remove_blank_target(self):
"""Remove target from link."""
elem = self
for _ in range(5):
if elem is None:
break
tag = elem.tag_name()
if tag == 'a' or tag == 'area':
if elem.get('target', None) == '_blank':
elem['target'] = '_top'
break
elem = elem.parent()
def debug_text(self):
"""Get a text based on an element suitable for debug output."""
return utils.compact_text(self.outer_xml(), 500)
def resolve_url(self, baseurl):
"""Resolve the URL in the element's src/href attribute.
Args:
baseurl: The URL to base relative URLs on as QUrl.
Return:
A QUrl with the absolute URL, or None.
"""
if baseurl.isRelative():
raise ValueError("Need an absolute base URL!")
for attr in ['href', 'src']:
if attr in self:
text = self[attr].strip()
break
else:
return None
url = QUrl(text)
if not url.isValid():
return None
if url.isRelative():
url = baseurl.resolved(url)
qtutils.ensure_valid(url)
return url

View File

@ -0,0 +1,178 @@
# 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, javascript
from qutebrowser.browser import webelem
class WebEngineElement(webelem.AbstractWebElement):
"""A web element for QtWebEngine, using JS under the hood."""
def __init__(self, js_dict, run_js_callable):
self._id = js_dict['id']
self._js_dict = js_dict
self._run_js = run_js_callable
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'].lower()
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?
js_code = javascript.assemble('webelem', 'set_text', self._id, text)
self._run_js(js_code)
def set_inner_xml(self, xml):
"""Set the given inner XML."""
# FIXME:qtwebengine get rid of this?
log.stub()
def remove_from_document(self):
"""Remove the node from the document."""
# FIXME:qtwebengine get rid of this?
log.stub()
def set_style_property(self, name, value):
"""Set the element style."""
# FIXME:qtwebengine get rid of this?
log.stub()
def run_js_async(self, code, callback=None):
"""Run the given JS snippet async on the element."""
# FIXME:qtwebengine get rid of this?
log.stub()
def parent(self):
"""Get the parent element of this element."""
# FIXME:qtwebengine get rid of this?
log.stub()
return None
def rect_on_view(self, *, elem_geometry=None, adjust_zoom=True,
no_js=False):
"""Get the geometry of the element relative to the webview.
Uses the getClientRects() JavaScript method to obtain the collection of
rectangles containing the element and returns the first rectangle which
is large enough (larger than 1px times 1px). If all rectangles returned
by getClientRects() are too small, falls back to elem.rect_on_view().
Skipping of small rectangles is due to <a> elements containing other
elements with "display:block" style, see
https://github.com/The-Compiler/qutebrowser/issues/1298
Args:
elem_geometry: The geometry of the element, or None.
Calling QWebElement::geometry is rather expensive so
we want to avoid doing it twice.
adjust_zoom: Whether to adjust the element position based on the
current zoom level.
no_js: Fall back to the Python implementation
"""
log.stub()
return QRect()
def is_visible(self, mainframe):
"""Check if the given element is visible in the given frame."""
# FIXME:qtwebengine get rid of this?
log.stub()
return True

View File

@ -36,22 +36,30 @@ from qutebrowser.utils import objreg, utils
class Attribute(websettings.Attribute):
"""A setting set via QWebEngineSettings::setAttribute."""
GLOBAL_SETTINGS = QWebEngineSettings.globalSettings
ENUM_BASE = QWebEngineSettings
class Setter(websettings.Setter):
"""A setting set via QWebEngineSettings getter/setter methods."""
GLOBAL_SETTINGS = QWebEngineSettings.globalSettings
class NullStringSetter(websettings.NullStringSetter):
"""A setter for settings requiring a null QString as default."""
GLOBAL_SETTINGS = QWebEngineSettings.globalSettings
class StaticSetter(websettings.StaticSetter):
"""A setting set via static QWebEngineSettings getter/setter methods."""
GLOBAL_SETTINGS = QWebEngineSettings.globalSettings

View File

@ -22,6 +22,8 @@
"""Wrapper over a QWebEngineView."""
import functools
from PyQt5.QtCore import pyqtSlot, Qt, QEvent, QPoint
from PyQt5.QtGui import QKeyEvent, QIcon
from PyQt5.QtWidgets import QApplication
@ -29,9 +31,9 @@ from PyQt5.QtWidgets import QApplication
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 import browsertab, mouse
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,18 +231,18 @@ 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):
self._tab.run_js_async("window.scroll({x}, {y});".format(
x=point.x(), y=point.y()))
js_code = javascript.assemble('window', 'scroll', point.x(), point.y())
self._tab.run_js_async(js_code)
def delta(self, x=0, y=0):
self._tab.run_js_async("window.scrollBy({x}, {y});".format(x=x, y=y))
self._tab.run_js_async(javascript.assemble('window', 'scrollBy', x, 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,13 +333,50 @@ class WebEngineTab(browsertab.AbstractTab):
self._set_widget(widget)
self._connect_signals()
self.backend = usertypes.Backend.QtWebEngine
# init js stuff
self._init_js()
self._child_event_filter = None
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 _install_event_filter(self):
self._widget.focusProxy().installEventFilter(self._mouse_event_filter)
self._child_event_filter = mouse.ChildEventFilter(
eventfilter=self._mouse_event_filter, widget=self._widget,
parent=self)
self._widget.installEventFilter(self._child_event_filter)
def openurl(self, url):
self._openurl_prepare(url)
self._widget.load(url)
def url(self):
return self._widget.url()
def url(self, requested=False):
page = self._widget.page()
if requested:
return page.requestedUrl()
else:
return page.url()
def dump_async(self, callback, *, plain=False):
if plain:
@ -411,9 +449,43 @@ 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, self.run_js_async)
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:
elem = webengineelem.WebEngineElement(js_elem, self.run_js_async)
callback(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
@ -422,6 +494,7 @@ class WebEngineTab(browsertab.AbstractTab):
page.linkHovered.connect(self.link_hovered)
page.loadProgress.connect(self._on_load_progress)
page.loadStarted.connect(self._on_load_started)
page.loadStarted.connect(self._on_history_trigger)
view.titleChanged.connect(self.title_changed)
view.urlChanged.connect(self._on_url_changed)
page.loadFinished.connect(self._on_load_finished)

View File

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

View File

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

View File

@ -0,0 +1,61 @@
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
# Copyright 2015-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/>.
"""QtWebKit specific part of history."""
from PyQt5.QtWebKit import QWebHistoryInterface
class WebHistoryInterface(QWebHistoryInterface):
"""Glue code between WebHistory and Qt's QWebHistoryInterface.
Attributes:
_history: The WebHistory object.
"""
def __init__(self, webhistory, parent=None):
super().__init__(parent)
self._history = webhistory
def addHistoryEntry(self, url_string):
"""Required for a QWebHistoryInterface impl, obsoleted by add_url."""
pass
def historyContains(self, url_string):
"""Called by WebKit to determine if a URL is contained in the history.
Args:
url_string: The URL (as string) to check for.
Return:
True if the url is in the history, False otherwise.
"""
return url_string in self._history.history_dict
def init(history):
"""Initialize the QWebHistoryInterface.
Args:
history: The WebHistory object.
"""
interface = WebHistoryInterface(history, parent=history)
QWebHistoryInterface.setDefaultInterface(interface)

View File

@ -34,22 +34,30 @@ from qutebrowser.utils import standarddir, objreg
class Attribute(websettings.Attribute):
"""A setting set via QWebSettings::setAttribute."""
GLOBAL_SETTINGS = QWebSettings.globalSettings
ENUM_BASE = QWebSettings
class Setter(websettings.Setter):
"""A setting set via QWebSettings getter/setter methods."""
GLOBAL_SETTINGS = QWebSettings.globalSettings
class NullStringSetter(websettings.NullStringSetter):
"""A setter for settings requiring a null QString as default."""
GLOBAL_SETTINGS = QWebSettings.globalSettings
class StaticSetter(websettings.StaticSetter):
"""A setting set via static QWebSettings getter/setter methods."""
GLOBAL_SETTINGS = QWebSettings.globalSettings

View File

@ -31,7 +31,7 @@ from PyQt5.QtWebKit import QWebSettings
from PyQt5.QtPrintSupport import QPrinter
from qutebrowser.browser import browsertab
from qutebrowser.browser.webkit import webview, tabhistory, webelem
from qutebrowser.browser.webkit import webview, tabhistory, webkitelem
from qutebrowser.utils import qtutils, objreg, usertypes, utils
@ -510,12 +510,19 @@ class WebKitTab(browsertab.AbstractTab):
self.zoom.set_default()
self.backend = usertypes.Backend.QtWebKit
def _install_event_filter(self):
self._widget.installEventFilter(self._mouse_event_filter)
def openurl(self, url):
self._openurl_prepare(url)
self._widget.openurl(url)
def url(self):
return self._widget.url()
def url(self, requested=False):
frame = self._widget.page().mainFrame()
if requested:
return frame.requestedUrl()
else:
return frame.url()
def dump_async(self, callback, *, plain=False):
frame = self._widget.page().mainFrame()
@ -564,16 +571,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.
@ -618,3 +637,4 @@ class WebKitTab(browsertab.AbstractTab):
view.iconChanged.connect(self._on_webkit_icon_changed)
page.frameCreated.connect(self._on_frame_created)
frame.contentsSizeChanged.connect(self._on_contents_size_changed)
frame.initialLayoutCompleted.connect(self._on_history_trigger)

View File

@ -29,9 +29,9 @@ from PyQt5.QtWebKitWidgets import QWebView, QWebPage, QWebFrame
from qutebrowser.config import config
from qutebrowser.keyinput import modeman
from qutebrowser.utils import message, log, usertypes, utils, qtutils, objreg
from qutebrowser.utils import 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):
@ -94,26 +94,11 @@ class WebView(QWebView):
self.setContextMenuPolicy(Qt.PreventContextMenu)
objreg.get('config').changed.connect(self.on_config_changed)
@pyqtSlot()
def on_initial_layout_completed(self):
"""Add url to history now that we have displayed something."""
history = objreg.get('web-history')
no_formatting = QUrl.UrlFormattingOption(0)
orig_url = self.page().mainFrame().requestedUrl()
if (orig_url.isValid() and
not orig_url.matches(self.url(), no_formatting)):
# If the url of the page is different than the url of the link
# originally clicked, save them both.
history.add_url(orig_url, self.title(), redirect=True)
history.add_url(self.url(), self.title())
def _init_page(self):
"""Initialize the QWebPage used by this view."""
page = webpage.BrowserPage(self.win_id, self._tab_id, self)
self.setPage(page)
page.mainFrame().loadFinished.connect(self.on_load_finished)
page.mainFrame().initialLayoutCompleted.connect(
self.on_initial_layout_completed)
return page
def __repr__(self):
@ -153,27 +138,6 @@ class WebView(QWebView):
elif section == 'colors' and option == 'webpage.bg':
self._set_bg_color()
def _mousepress_backforward(self, e):
"""Handle back/forward mouse button presses.
Args:
e: The QMouseEvent.
"""
if e.button() in [Qt.XButton1, Qt.LeftButton]:
# Back button on mice which have it, or rocker gesture
if self.page().history().canGoBack():
self.back()
else:
message.error(self.win_id, "At beginning of history.",
immediately=True)
elif e.button() in [Qt.XButton2, Qt.RightButton]:
# Forward button on mice which have it, or rocker gesture
if self.page().history().canGoForward():
self.forward()
else:
message.error(self.win_id, "At end of history.",
immediately=True)
def _mousepress_insertmode(self, e):
"""Switch to insert mode when an editable element was clicked.
@ -196,13 +160,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 +187,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 +290,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)))
@ -421,13 +386,6 @@ class WebView(QWebView):
Return:
The superclass return value.
"""
is_rocker_gesture = (config.get('input', 'rocker-gestures') and
e.buttons() == Qt.LeftButton | Qt.RightButton)
if e.button() in [Qt.XButton1, Qt.XButton2] or is_rocker_gesture:
self._mousepress_backforward(e)
super().mousePressEvent(e)
return
self._mousepress_insertmode(e)
self._mousepress_opentarget(e)
self._ignore_wheel_event = True

View File

@ -75,13 +75,13 @@ class Command:
deprecated: False, or a string to describe why a command is deprecated.
desc: The description of the command.
handler: The handler function to call.
completion: Completions to use for arguments, as a list of strings.
debug: Whether this is a debugging command (only shown with --debug).
parser: The ArgumentParser to use to parse this command.
flags_with_args: A list of flags which take an argument.
no_cmd_split: If true, ';;' to split sub-commands is ignored.
backend: Which backend the command works with (or None if it works with
both)
no_replace_variables: Don't replace variables like {url}
_qute_args: The saved data from @cmdutils.argument
_needs_js: Whether the command needs javascript enabled
_modes: The modes the command can be executed in.
@ -95,7 +95,7 @@ class Command:
hide=False, modes=None, not_modes=None, needs_js=False,
debug=False, ignore_args=False, deprecated=False,
no_cmd_split=False, star_args_optional=False, scope='global',
backend=None):
backend=None, no_replace_variables=False):
# I really don't know how to solve this in a better way, I tried.
# pylint: disable=too-many-locals
if modes is not None and not_modes is not None:
@ -127,6 +127,7 @@ class Command:
self.handler = handler
self.no_cmd_split = no_cmd_split
self.backend = backend
self.no_replace_variables = no_replace_variables
self.docparser = docutils.DocstringParser(handler)
self.parser = argparser.ArgumentParser(
@ -148,13 +149,7 @@ class Command:
self._qute_args = getattr(self.handler, 'qute_args', {})
self.handler.qute_args = None
args = self._inspect_func()
self.completion = []
for arg in args:
arg_completion = self.get_arg_info(arg).completion
if arg_completion is not None:
self.completion.append(arg_completion)
self._inspect_func()
def _check_prerequisites(self, win_id):
"""Check if the command is permitted to run currently.
@ -208,6 +203,11 @@ class Command:
"""Get an ArgInfo tuple for the given inspect.Parameter."""
return self._qute_args.get(param.name, ArgInfo())
def get_pos_arg_info(self, pos):
"""Get an ArgInfo tuple for the given positional parameter."""
name = self.pos_args[pos][0]
return self._qute_args.get(name, ArgInfo())
def _inspect_special_param(self, param):
"""Check if the given parameter is a special one.

View File

@ -26,7 +26,7 @@ from PyQt5.QtCore import pyqtSlot, QUrl, QObject
from qutebrowser.config import config, configexc
from qutebrowser.commands import cmdexc, cmdutils
from qutebrowser.utils import message, objreg, qtutils
from qutebrowser.utils import message, objreg, qtutils, utils
from qutebrowser.misc import split
@ -49,21 +49,29 @@ def _current_url(tabbed_browser):
def replace_variables(win_id, arglist):
"""Utility function to replace variables like {url} in a list of args."""
variables = {
'{url}': lambda: _current_url(tabbed_browser).toString(
QUrl.FullyEncoded | QUrl.RemovePassword),
'{url:pretty}': lambda: _current_url(tabbed_browser).toString(
QUrl.RemovePassword),
'{clipboard}': utils.get_clipboard,
'{primary}': lambda: utils.get_clipboard(selection=True),
}
values = {}
args = []
tabbed_browser = objreg.get('tabbed-browser', scope='window',
window=win_id)
if '{url}' in arglist:
url = _current_url(tabbed_browser).toString(QUrl.FullyEncoded |
QUrl.RemovePassword)
if '{url:pretty}' in arglist:
pretty_url = _current_url(tabbed_browser).toString(QUrl.RemovePassword)
for arg in arglist:
if arg == '{url}':
args.append(url)
elif arg == '{url:pretty}':
args.append(pretty_url)
else:
try:
for arg in arglist:
for var, func in variables.items():
if var in arg:
if var not in values:
values[var] = func()
arg = arg.replace(var, values[var])
args.append(arg)
except utils.ClipboardError as e:
raise cmdexc.CommandError(e)
return args
@ -279,7 +287,10 @@ class CommandRunner(QObject):
window=self._win_id)
cur_mode = mode_manager.mode
args = replace_variables(self._win_id, result.args)
if result.cmd.no_replace_variables:
args = result.args
else:
args = replace_variables(self._win_id, result.args)
if count is not None:
if result.count is not None:
raise cmdexc.CommandMetaError("Got count via command and "

View File

@ -204,25 +204,18 @@ class Completer(QObject):
return sortfilter.CompletionFilterModel(source=model, parent=self)
# delegate completion to command
try:
completions = cmdutils.cmd_dict[parts[0]].completion
cmd = cmdutils.cmd_dict[parts[0]]
except KeyError:
# entering an unknown command
return None
if completions is None:
# command without any available completions
return None
dbg_completions = [c.name for c in completions]
try:
idx = cursor_part - 1
completion = completions[idx]
completion = cmd.get_pos_arg_info(idx).completion
except IndexError:
# More arguments than completions
log.completion.debug("completions: {}".format(
', '.join(dbg_completions)))
# user provided more positional arguments than the command takes
return None
if completion is None:
return None
dbg_completions[idx] = '*' + dbg_completions[idx] + '*'
log.completion.debug("completions: {}".format(
', '.join(dbg_completions)))
model = self._get_completion_model(completion, parts, cursor_part)
return model

View File

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

View File

@ -27,10 +27,9 @@ Module attributes:
import functools
from qutebrowser.completion.models import (miscmodels, urlmodel, configmodel,
base)
from qutebrowser.completion.models import miscmodels, urlmodel, configmodel
from qutebrowser.utils import objreg, usertypes, log, debug
from qutebrowser.config import configdata
from qutebrowser.config import configdata, config
_instances = {}
@ -115,11 +114,11 @@ def init_session_completion():
_instances[usertypes.Completion.sessions] = model
def _init_empty_completion():
"""Initialize empty completion model."""
log.completion.debug("Initializing empty completion.")
if usertypes.Completion.empty not in _instances:
_instances[usertypes.Completion.empty] = base.BaseCompletionModel()
def _init_bind_completion():
"""Initialize the command completion model."""
log.completion.debug("Initializing bind completion.")
model = miscmodels.BindCompletionModel()
_instances[usertypes.Completion.bind] = model
INITIALIZERS = {
@ -133,7 +132,7 @@ INITIALIZERS = {
usertypes.Completion.quickmark_by_name: init_quickmark_completions,
usertypes.Completion.bookmark_by_url: init_bookmark_completions,
usertypes.Completion.sessions: init_session_completion,
usertypes.Completion.empty: _init_empty_completion,
usertypes.Completion.bind: _init_bind_completion,
}
@ -164,6 +163,12 @@ def update(completions):
did_run.append(func)
@config.change_filter('aliases', function=True)
def _update_aliases():
"""Update completions that include command aliases."""
update([usertypes.Completion.command])
def init():
"""Initialize completions. Note this only connects signals."""
quickmark_manager = objreg.get('quickmark-manager')
@ -185,3 +190,7 @@ def init():
keyconf = objreg.get('key-config')
keyconf.changed.connect(
functools.partial(update, [usertypes.Completion.command]))
keyconf.changed.connect(
functools.partial(update, [usertypes.Completion.bind]))
objreg.get('config').changed.connect(_update_aliases)

View File

@ -30,7 +30,7 @@ from qutebrowser.completion.models import base
class CommandCompletionModel(base.BaseCompletionModel):
"""A CompletionModel filled with all commands and descriptions."""
"""A CompletionModel filled with non-hidden commands and descriptions."""
# https://github.com/The-Compiler/qutebrowser/issues/545
# pylint: disable=abstract-method
@ -39,23 +39,11 @@ class CommandCompletionModel(base.BaseCompletionModel):
def __init__(self, parent=None):
super().__init__(parent)
assert cmdutils.cmd_dict
cmdlist = []
for obj in set(cmdutils.cmd_dict.values()):
if (obj.hide or (obj.debug and not objreg.get('args').debug) or
obj.deprecated):
pass
else:
cmdlist.append((obj.name, obj.desc))
for name, cmd in config.section('aliases').items():
cmdlist.append((name, "Alias for '{}'".format(cmd)))
cmdlist = _get_cmd_completions(include_aliases=True,
include_hidden=False)
cat = self.new_category("Commands")
# map each command to its bound keys and show these in the misc column
key_config = objreg.get('key-config')
cmd_to_keys = key_config.get_reverse_bindings_for('normal')
for (name, desc) in sorted(cmdlist):
self.new_item(cat, name, desc, ', '.join(cmd_to_keys[name]))
for (name, desc, misc) in cmdlist:
self.new_item(cat, name, desc, misc)
class HelpCompletionModel(base.BaseCompletionModel):
@ -72,17 +60,11 @@ class HelpCompletionModel(base.BaseCompletionModel):
def _init_commands(self):
"""Fill completion with :command entries."""
assert cmdutils.cmd_dict
cmdlist = []
for obj in set(cmdutils.cmd_dict.values()):
if (obj.hide or (obj.debug and not objreg.get('args').debug) or
obj.deprecated):
pass
else:
cmdlist.append((':' + obj.name, obj.desc))
cmdlist = _get_cmd_completions(include_aliases=False,
include_hidden=True, prefix=':')
cat = self.new_category("Commands")
for (name, desc) in sorted(cmdlist):
self.new_item(cat, name, desc)
for (name, desc, misc) in cmdlist:
self.new_item(cat, name, desc, misc)
def _init_settings(self):
"""Fill completion with section->option entries."""
@ -166,7 +148,8 @@ class TabCompletionModel(base.BaseCompletionModel):
def __init__(self, parent=None):
super().__init__(parent)
self.columns_to_filter = [self.URL_COLUMN, self.TEXT_COLUMN]
self.columns_to_filter = [self.IDX_COLUMN, self.URL_COLUMN,
self.TEXT_COLUMN]
for win_id in objreg.window_registry:
tabbed_browser = objreg.get('tabbed-browser', scope='window',
@ -174,6 +157,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 +171,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()
@ -257,3 +242,49 @@ class TabCompletionModel(base.BaseCompletionModel):
tabbed_browser = objreg.get('tabbed-browser', scope='window',
window=int(win_id))
tabbed_browser.on_tab_close_requested(int(tab_index) - 1)
class BindCompletionModel(base.BaseCompletionModel):
"""A CompletionModel filled with all bindable commands and descriptions."""
# https://github.com/The-Compiler/qutebrowser/issues/545
# pylint: disable=abstract-method
COLUMN_WIDTHS = (20, 60, 20)
def __init__(self, parent=None):
super().__init__(parent)
cmdlist = _get_cmd_completions(include_hidden=True,
include_aliases=True)
cat = self.new_category("Commands")
for (name, desc, misc) in cmdlist:
self.new_item(cat, name, desc, misc)
def _get_cmd_completions(include_hidden, include_aliases, prefix=''):
"""Get a list of completions info for commands, sorted by name.
Args:
include_hidden: True to include commands annotated with hide=True.
include_aliases: True to include command aliases.
prefix: String to append to the command name.
Return: A list of tuples of form (name, description, bindings).
"""
assert cmdutils.cmd_dict
cmdlist = []
cmd_to_keys = objreg.get('key-config').get_reverse_bindings_for('normal')
for obj in set(cmdutils.cmd_dict.values()):
hide_debug = obj.debug and not objreg.get('args').debug
hide_hidden = obj.hide and not include_hidden
if not (hide_debug or hide_hidden or obj.deprecated):
bindings = ', '.join(cmd_to_keys.get(obj.name, []))
cmdlist.append((prefix + obj.name, obj.desc, bindings))
if include_aliases:
for name, cmd in config.section('aliases').items():
bindings = ', '.join(cmd_to_keys.get(name, []))
cmdlist.append((name, "Alias for '{}'".format(cmd), bindings))
return cmdlist

View File

@ -135,8 +135,8 @@ class CompletionFilterModel(QSortFilterProxyModel):
for col in self.srcmodel.columns_to_filter:
idx = self.srcmodel.index(row, col, parent)
if not idx.isValid():
# No entries in parent model
if not idx.isValid(): # pragma: no cover
# this is a sanity check not hit by any test case
continue
data = self.srcmodel.data(idx)
if not data:

View File

@ -36,7 +36,8 @@ import collections.abc
from PyQt5.QtCore import pyqtSignal, QObject, QUrl, QSettings
from qutebrowser.config import configdata, configexc, textwrapper
from qutebrowser.config.parsers import ini, keyconf
from qutebrowser.config.parsers import keyconf
from qutebrowser.config.parsers import ini
from qutebrowser.commands import cmdexc, cmdutils
from qutebrowser.utils import (message, objreg, utils, standarddir, log,
qtutils, error, usertypes)
@ -350,6 +351,7 @@ class ConfigManager(QObject):
('tabs', 'auto-hide'),
('tabs', 'hide-always'),
('ui', 'display-statusbar-messages'),
('ui', 'hide-mouse-cursor'),
('general', 'wrap-search'),
]
CHANGED_OPTIONS = {

View File

@ -154,7 +154,7 @@ def data(readonly=False):
"Whether to save the config automatically on quit."),
('auto-save-interval',
SettingValue(typ.Int(minval=0), '15000'),
SettingValue(typ.Int(minval=0, maxval=MAXVALS['int']), '15000'),
"How often (in milliseconds) to auto-save config/cookies/etc."),
('editor',
@ -227,13 +227,25 @@ def data(readonly=False):
"How to open links in an existing instance if a new one is "
"launched."),
('new-instance-open-target.window',
SettingValue(typ.String(
valid_values=typ.ValidValues(
('last-opened', "Open new tabs in the last opened "
"window."),
('last-focused', "Open new tabs in the most recently "
"focused window."),
('last-visible', "Open new tabs in the most recently "
"visible window.")
)), 'last-focused'),
"Which window to choose when opening links as new tabs."),
('log-javascript-console',
SettingValue(typ.String(
valid_values=typ.ValidValues(
('none', "Don't log messages."),
('debug', "Log messages with debug level."),
('info', "Log messages with info level.")
)), 'debug', backends=[usertypes.Backend.QtWebKit]),
)), 'debug'),
"How to log javascript console messages."),
('save-session',
@ -346,10 +358,6 @@ def data(readonly=False):
"* `{scroll_pos}`: The page scroll position.\n"
"* `{host}`: The host of the current web page."),
('hide-mouse-cursor',
SettingValue(typ.Bool(), 'false'),
"Whether to hide the mouse cursor."),
('modal-js-dialog',
SettingValue(typ.Bool(), 'false'),
"Use standard JavaScript modal dialog for alert() and confirm()"),
@ -488,13 +496,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 +941,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)),
@ -956,6 +964,10 @@ def data(readonly=False):
)), 'python'),
"Which implementation to use to find elements to hint."),
('hide-unmatched-rapid-hints',
SettingValue(typ.Bool(), 'true'),
"Controls hiding unmatched hints in rapid mode."),
readonly=readonly
)),
@ -1266,7 +1278,7 @@ def data(readonly=False):
"Font used in the completion widget."),
('completion.category',
SettingValue(typ.Font(), 'bold ${completion}'),
SettingValue(typ.Font(), 'bold ${completion}'),
"Font used in the completion categories."),
('tabbar',
@ -1415,8 +1427,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': (
@ -1506,18 +1517,18 @@ KEY_DATA = collections.OrderedDict([
('enter-mode jump_mark', ["'"]),
('yank', ['yy']),
('yank -s', ['yY']),
('yank -t', ['yt']),
('yank -ts', ['yT']),
('yank -d', ['yd']),
('yank -ds', ['yD']),
('yank -p', ['yp']),
('yank -ps', ['yP']),
('paste', ['pp']),
('paste -s', ['pP']),
('paste -t', ['Pp']),
('paste -ts', ['PP']),
('paste -w', ['wp']),
('paste -ws', ['wP']),
('yank title', ['yt']),
('yank title -s', ['yT']),
('yank domain', ['yd']),
('yank domain -s', ['yD']),
('yank pretty-url', ['yp']),
('yank pretty-url -s', ['yP']),
('open {clipboard}', ['pp']),
('open {primary}', ['pP']),
('open -t {clipboard}', ['Pp']),
('open -t {primary}', ['PP']),
('open -w {clipboard}', ['wp']),
('open -w {primary}', ['wP']),
('quickmark-save', ['m']),
('set-cmd-text -s :quickmark-load', ['b']),
('set-cmd-text -s :quickmark-load -t', ['B']),
@ -1589,8 +1600,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),
])),
@ -1638,8 +1649,8 @@ KEY_DATA = collections.OrderedDict([
('move-to-end-of-line', ['$']),
('move-to-start-of-document', ['gg']),
('move-to-end-of-document', ['G']),
('yank-selected -p', ['Y']),
('yank-selected', ['y'] + RETURN_KEYS),
('yank selection -s', ['Y']),
('yank selection', ['y'] + RETURN_KEYS),
('scroll left', ['H']),
('scroll down', ['J']),
('scroll up', ['K']),
@ -1677,4 +1688,21 @@ CHANGED_KEY_COMMANDS = [
(re.compile(r'^download-remove --all$'), r'download-clear'),
(re.compile(r'^hint links fill "([^"]*)"$'), r'hint links fill \1'),
(re.compile(r'^yank -t(\S+)'), r'yank title -\1'),
(re.compile(r'^yank -t'), r'yank title'),
(re.compile(r'^yank -d(\S+)'), r'yank domain -\1'),
(re.compile(r'^yank -d'), r'yank domain'),
(re.compile(r'^yank -p(\S+)'), r'yank pretty-url -\1'),
(re.compile(r'^yank -p'), r'yank pretty-url'),
(re.compile(r'^yank-selected -p'), r'yank selection -s'),
(re.compile(r'^yank-selected'), r'yank selection'),
(re.compile(r'^paste$'), r'open {clipboard}'),
(re.compile(r'^paste -([twb])$'), r'open -\1 {clipboard}'),
(re.compile(r'^paste -([twb])s$'), r'open -\1 {primary}'),
(re.compile(r'^paste -s([twb])$'), r'open -\1 {primary}'),
(re.compile(r'^completion-item-next'), r'completion-item-focus next'),
(re.compile(r'^completion-item-prev'), r'completion-item-focus prev'),
]

View File

@ -125,11 +125,11 @@ class BaseType:
self.valid_values = None
def get_name(self):
"""Get a name for the type for documentation"""
"""Get a name for the type for documentation."""
return self.__class__.__name__
def get_valid_values(self):
"""Get the type's valid values for documentation"""
"""Get the type's valid values for documentation."""
return self.valid_values
def _basic_validation(self, value):

View File

@ -150,10 +150,10 @@ class KeyConfigParser(QObject):
data = str(self)
f.write(data)
@cmdutils.register(instance='key-config', maxsplit=1, no_cmd_split=True)
@cmdutils.register(instance='key-config', maxsplit=1, no_cmd_split=True,
no_replace_variables=True)
@cmdutils.argument('win_id', win_id=True)
@cmdutils.argument('key', completion=usertypes.Completion.empty)
@cmdutils.argument('command', completion=usertypes.Completion.command)
@cmdutils.argument('command', completion=usertypes.Completion.bind)
def bind(self, key, win_id, command=None, *, mode='normal', force=False):
"""Bind a key to a command.
@ -335,6 +335,7 @@ class KeyConfigParser(QObject):
def _validate_command(self, line):
"""Check if a given command is valid."""
from qutebrowser.config import config
if line == self.UNBOUND_COMMAND:
return
commands = line.split(';;')
@ -352,7 +353,8 @@ class KeyConfigParser(QObject):
line))
commands = [c.split(maxsplit=1)[0].strip() for c in commands]
for cmd in commands:
if cmd not in cmdutils.cmd_dict:
aliases = config.section('aliases')
if cmd not in cmdutils.cmd_dict and cmd not in aliases:
raise KeyConfigError("Invalid command '{}'!".format(cmd))
def _read_command(self, line):
@ -361,12 +363,12 @@ class KeyConfigParser(QObject):
raise KeyConfigError("Got command '{}' without getting a "
"section!".format(line))
else:
self._validate_command(line)
for rgx, repl in configdata.CHANGED_KEY_COMMANDS:
if rgx.match(line):
line = rgx.sub(repl, line)
self._mark_config_dirty()
break
self._validate_command(line)
self._cur_command = line
def _read_keybinding(self, line):
@ -422,8 +424,9 @@ class KeyConfigParser(QObject):
def get_reverse_bindings_for(self, section):
"""Get a dict of commands to a list of bindings for the section."""
cmd_to_keys = collections.defaultdict(list)
cmd_to_keys = {}
for key, cmd in self.get_bindings_for(section).items():
cmd_to_keys.setdefault(cmd, [])
# put special bindings last
if utils.is_special_key(key):
cmd_to_keys[cmd].append(key)

View File

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

View File

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

View File

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

View File

@ -0,0 +1,80 @@
/**
* Copyright 2016 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
*
* This file is part of qutebrowser.
*
* qutebrowser is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* qutebrowser is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
*/
"use strict";
window._qutebrowser.webelem = (function() {
var funcs = {};
var elements = [];
function serialize_elem(elem, id) {
var out = {
"id": id,
"text": elem.text,
"tag_name": elem.tagName,
"outer_xml": elem.outerHTML,
};
var attributes = {};
for (var i = 0; i < elem.attributes.length; ++i) {
var attr = elem.attributes[i];
attributes[attr.name] = attr.value;
}
out.attributes = attributes;
// console.log(JSON.stringify(out));
return out;
}
funcs.find_all = function(selector) {
var elems = document.querySelectorAll(selector);
var out = [];
var id = elements.length;
for (var i = 0; i < elems.length; ++i) {
var elem = elems[i];
out.push(serialize_elem(elem, id));
elements[id] = elem;
id++;
}
return out;
};
funcs.focus_element = function() {
var elem = document.activeElement;
if (!elem || elem === document.body) {
// "When there is no selection, the active element is the page's
// <body> or null."
return null;
}
var id = elements.length;
elements[id] = elem;
return serialize_elem(elem, id);
};
funcs.set_text = function(id, text) {
elements[id].value = text;
};
return funcs;
})();

View File

@ -189,7 +189,7 @@ class HintKeyParser(keyparser.CommandKeyParser):
return True
else:
return super()._handle_special_key(e)
elif config.get('hints', 'mode') != 'number':
elif hintmanager.current_mode() != 'number':
return super()._handle_special_key(e)
elif not e.text():
return super()._handle_special_key(e)

View File

@ -69,7 +69,13 @@ def get_window(via_ipc, force_window=False, force_tab=False,
window_to_raise = window
else:
try:
window = objreg.last_window()
win_mode = config.get('general', 'new-instance-open-target.window')
if win_mode == 'last-focused':
window = objreg.last_focused_window()
elif win_mode == 'last-opened':
window = objreg.last_window()
elif win_mode == 'last-visible':
window = objreg.last_visible_window()
except objreg.NoWindow:
# There is no window left, so we open a new one
window = MainWindow()
@ -175,9 +181,6 @@ class MainWindow(QWidget):
QTimer.singleShot(0, self._connect_resize_keyhint)
objreg.get('config').changed.connect(self.on_config_changed)
if config.get('ui', 'hide-mouse-cursor'):
self.setCursor(Qt.BlankCursor)
objreg.get("app").new_window.emit(self)
def _init_downloadmanager(self):
@ -457,8 +460,23 @@ class MainWindow(QWidget):
self._downloadview.updateGeometry()
self.tabbed_browser.tabBar().refresh()
def showEvent(self, e):
"""Extend showEvent to register us as the last-visible-main-window.
Args:
e: The QShowEvent
"""
super().showEvent(e)
objreg.register('last-visible-main-window', self, update=True)
def _do_close(self):
"""Helper function for closeEvent."""
last_visible = objreg.get('last-visible-main-window')
if self is last_visible:
try:
objreg.delete('last-visible-main-window')
except KeyError:
pass
objreg.get('session-manager').save_last_window_session()
self._save_geometry()
log.destroy.debug("Closing window {}".format(self.win_id))

View File

@ -23,8 +23,8 @@ from PyQt5.QtCore import pyqtSignal, pyqtSlot, Qt, QSize
from PyQt5.QtWidgets import QSizePolicy
from qutebrowser.keyinput import modeman, modeparsers
from qutebrowser.commands import cmdexc, cmdutils, runners
from qutebrowser.misc import cmdhistory, split
from qutebrowser.commands import cmdexc, cmdutils
from qutebrowser.misc import cmdhistory
from qutebrowser.misc import miscwidgets as misc
from qutebrowser.utils import usertypes, log, objreg
@ -108,10 +108,6 @@ class Command(misc.MinimalLineEditMixin, misc.CommandLineEdit):
space: If given, a space is added to the end.
append: If given, the text is appended to the current text.
"""
args = split.simple_split(text)
args = runners.replace_variables(self._win_id, args)
text = ' '.join(args)
if space:
text += ' '
if append:

View File

@ -34,7 +34,7 @@ from qutebrowser.utils import (log, usertypes, utils, qtutils, objreg,
urlutils, message)
UndoEntry = collections.namedtuple('UndoEntry', ['url', 'history'])
UndoEntry = collections.namedtuple('UndoEntry', ['url', 'history', 'index'])
class TabDeletedError(Exception):
@ -198,6 +198,7 @@ class TabbedBrowser(tabwidget.TabWidget):
tab.window_close_requested.connect(
functools.partial(self.on_window_close_requested, tab))
tab.new_tab_requested.connect(self.tabopen)
tab.add_history_item.connect(objreg.get('web-history').add_from_tab)
def current_url(self):
"""Get the URL of the current tab.
@ -260,7 +261,7 @@ class TabbedBrowser(tabwidget.TabWidget):
window=self._win_id)
if tab.url().isValid():
history_data = tab.history.serialize()
entry = UndoEntry(tab.url(), history_data)
entry = UndoEntry(tab.url(), history_data, idx)
self._undo_stack.append(entry)
elif tab.url().isEmpty():
# There are some good reasons why a URL could be empty
@ -297,13 +298,13 @@ class TabbedBrowser(tabwidget.TabWidget):
use_current_tab = (only_one_tab_open and no_history and
last_close_url_used)
url, history_data = self._undo_stack.pop()
url, history_data, idx = self._undo_stack.pop()
if use_current_tab:
self.openurl(url, newtab=False)
newtab = self.widget(0)
else:
newtab = self.tabopen(url, background=False)
newtab = self.tabopen(url, background=False, idx=idx)
newtab.history.deserialize(history_data)
@ -342,7 +343,7 @@ class TabbedBrowser(tabwidget.TabWidget):
@pyqtSlot('QUrl')
@pyqtSlot('QUrl', bool)
def tabopen(self, url=None, background=None, explicit=False):
def tabopen(self, url=None, background=None, explicit=False, idx=None):
"""Open a new tab with a given URL.
Inner logic for open-tab and open-tab-bg.
@ -358,6 +359,7 @@ class TabbedBrowser(tabwidget.TabWidget):
- Tabs from clicked links etc. are to the right of
the current.
- Explicitly opened tabs are at the very right.
idx: The index where the new tab should be opened.
Return:
The opened WebView instance.
@ -376,7 +378,8 @@ class TabbedBrowser(tabwidget.TabWidget):
tab = browsertab.create(win_id=self._win_id, parent=self)
self._connect_tab_signals(tab)
idx = self._get_new_tab_idx(explicit)
if idx is None:
idx = self._get_new_tab_idx(explicit)
self.insertTab(idx, tab, "")
if url is not None:

View File

@ -60,11 +60,11 @@ def _missing_str(name, *, windows=None, pip=None, webengine=False):
blocks.append('<br />'.join(lines))
if webengine:
lines = [
'Note QtWebEngine is not available for some distributions '
('Note QtWebEngine is not available for some distributions '
'(like Debian/Ubuntu), so you need to start without '
'--backend webengine there.',
'QtWebEngine is currently unsupported with the OS X .app, see '
'https://github.com/The-Compiler/qutebrowser/issues/1692',
'--backend webengine there.'),
('QtWebEngine is currently unsupported with the OS X .app, see '
'https://github.com/The-Compiler/qutebrowser/issues/1692'),
]
else:
lines = ['<b>If you installed a qutebrowser package for your '
@ -301,6 +301,13 @@ def init_log(args):
log.init.debug("Log initialized.")
def check_optimize_flag():
from qutebrowser.utils import log
if sys.flags.optimize >= 2:
log.init.warning("Running on optimize level higher than 1, "
"unexpected behavior may occur.")
def earlyinit(args):
"""Do all needed early initialization.
@ -327,3 +334,4 @@ def earlyinit(args):
remove_inputhook()
check_libraries(args)
check_ssl_support()
check_optimize_flag()

View File

@ -35,8 +35,8 @@ class ExternalEditor(QObject):
Attributes:
_text: The current text before the editor is opened.
_oshandle: The OS level handle to the tmpfile.
_filehandle: The file handle to the tmpfile.
_file: The file handle as tempfile.NamedTemporaryFile. Note that this
handle will be closed after the initial file has been created.
_proc: The GUIProcess of the editor.
_win_id: The window ID the ExternalEditor is associated with.
"""
@ -46,20 +46,18 @@ class ExternalEditor(QObject):
def __init__(self, win_id, parent=None):
super().__init__(parent)
self._text = None
self._oshandle = None
self._filename = None
self._file = None
self._proc = None
self._win_id = win_id
def _cleanup(self):
"""Clean up temporary files after the editor closed."""
if self._oshandle is None or self._filename is None:
if self._file is None:
# Could not create initial file.
return
try:
os.close(self._oshandle)
if self._proc.exit_status() != QProcess.CrashExit:
os.remove(self._filename)
os.remove(self._file.name)
except OSError as e:
# NOTE: Do not replace this with "raise CommandError" as it's
# executed async.
@ -82,7 +80,7 @@ class ExternalEditor(QObject):
return
encoding = config.get('general', 'editor-encoding')
try:
with open(self._filename, 'r', encoding=encoding) as f:
with open(self._file.name, 'r', encoding=encoding) as f:
text = f.read()
except OSError as e:
# NOTE: Do not replace this with "raise CommandError" as it's
@ -108,13 +106,18 @@ class ExternalEditor(QObject):
if self._text is not None:
raise ValueError("Already editing a file!")
self._text = text
encoding = config.get('general', 'editor-encoding')
try:
self._oshandle, self._filename = tempfile.mkstemp(
text=True, prefix='qutebrowser-editor-')
if text:
encoding = config.get('general', 'editor-encoding')
with open(self._filename, 'w', encoding=encoding) as f:
f.write(text)
# Close while the external process is running, as otherwise systems
# with exclusive write access (e.g. Windows) may fail to update
# the file from the external editor, see
# https://github.com/The-Compiler/qutebrowser/issues/1767
with tempfile.NamedTemporaryFile(
mode='w', prefix='qutebrowser-editor-', encoding=encoding,
delete=False) as fobj:
if text:
fobj.write(text)
self._file = fobj
except OSError as e:
message.error(self._win_id, "Failed to create initial file: "
"{}".format(e))
@ -125,6 +128,6 @@ class ExternalEditor(QObject):
self._proc.error.connect(self.on_proc_error)
editor = config.get('general', 'editor')
executable = editor[0]
args = [arg.replace('{}', self._filename) for arg in editor[1:]]
args = [arg.replace('{}', self._file.name) for arg in editor[1:]]
log.procs.debug("Calling \"{}\" with args {}".format(executable, args))
self._proc.start(executable, args)

View File

@ -47,7 +47,7 @@ class MinimalLineEditMixin:
if e.key() == Qt.Key_Insert and e.modifiers() == Qt.ShiftModifier:
try:
text = utils.get_clipboard(selection=True)
except utils.SelectionUnsupportedError:
except utils.ClipboardError:
pass
else:
e.accept()

View File

@ -39,7 +39,7 @@ from PyQt5.QtCore import QUrl
from PyQt5.QtWidgets import QApplication # pylint: disable=unused-import
@cmdutils.register(maxsplit=1, no_cmd_split=True)
@cmdutils.register(maxsplit=1, no_cmd_split=True, no_replace_variables=True)
@cmdutils.argument('win_id', win_id=True)
def later(ms: int, command, win_id):
"""Execute a command after some time.
@ -69,7 +69,7 @@ def later(ms: int, command, win_id):
raise
@cmdutils.register(maxsplit=1, no_cmd_split=True)
@cmdutils.register(maxsplit=1, no_cmd_split=True, no_replace_variables=True)
@cmdutils.argument('win_id', win_id=True)
def repeat(times: int, command, win_id):
"""Repeat a given command.

View File

@ -26,7 +26,7 @@ import os.path
import collections
import qutebrowser
from qutebrowser.utils import usertypes
from qutebrowser.utils import usertypes, log, utils
def is_git_repo():
@ -98,6 +98,15 @@ class DocstringParser:
self.State.arg_inside: self._parse_arg_inside,
self.State.misc: self._skip,
}
if doc is None:
if sys.flags.optimize < 2:
log.commands.warning(
"Function {}() from {} has no docstring".format(
utils.qualname(func),
inspect.getsourcefile(func)))
self.long_desc = ""
self.short_desc = ""
return
for line in doc.splitlines():
handler = handlers[self._state]
stop = handler(line)

View File

@ -20,9 +20,6 @@
"""Utilities related to javascript interaction."""
from qutebrowser.utils import utils
def string_escape(text):
"""Escape values special to javascript in strings.
@ -55,18 +52,19 @@ 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)
if module == 'window':
parts = ['window', function]
else:
parts = ['window', '_qutebrowser', module, function]
code = '"use strict";\n{}({});'.format('.'.join(parts), js_args)
return code

View File

@ -94,8 +94,15 @@ class ObjectRegistry(collections.UserDict):
def _disconnect_destroyed(self, name):
"""Disconnect the destroyed slot if it was connected."""
if name in self._partial_objs:
func = self._partial_objs[name]
try:
partial_objs = self._partial_objs
except AttributeError:
# This sometimes seems to happen on Travis during
# test_history.test_adding_item_during_async_read
# and I have no idea why...
return
if name in partial_objs:
func = partial_objs[name]
try:
self[name].destroyed.disconnect(func)
except (RuntimeError, TypeError):
@ -106,7 +113,7 @@ class ObjectRegistry(collections.UserDict):
# pyqtSignal must be bound to a QObject" instead:
# https://github.com/The-Compiler/qutebrowser/issues/257
pass
del self._partial_objs[name]
del partial_objs[name]
def on_destroyed(self, name):
"""Schedule removing of a destroyed QObject.
@ -121,6 +128,11 @@ class ObjectRegistry(collections.UserDict):
def _on_destroyed(self, name):
"""Remove a destroyed QObject."""
log.destroy.debug("removed: {}".format(name))
if not hasattr(self, 'data'):
# This sometimes seems to happen on Travis during
# test_history.test_adding_item_during_async_read
# and I have no idea why...
return
try:
del self[name]
del self._partial_objs[name]
@ -178,10 +190,7 @@ def _get_window_registry(window):
app = get('app')
win = app.activeWindow()
elif window == 'last-focused':
try:
win = get('last-focused-main-window')
except KeyError:
win = last_window()
win = last_focused_window()
else:
win = window_registry[window]
except (KeyError, NoWindow):
@ -276,6 +285,22 @@ def dump_objects():
return lines
def last_visible_window():
"""Get the last visible window, or the last focused window if none."""
try:
return get('last-visible-main-window')
except KeyError:
return last_focused_window()
def last_focused_window():
"""Get the last focused window, or the last window if none."""
try:
return get('last-focused-main-window')
except KeyError:
return last_window()
def last_window():
"""Get the last opened window object."""
if not window_registry:

View File

@ -239,7 +239,7 @@ KeyMode = enum('KeyMode', ['normal', 'hint', 'command', 'yesno', 'prompt',
Completion = enum('Completion', ['command', 'section', 'option', 'value',
'helptopic', 'quickmark_by_name',
'bookmark_by_url', 'url', 'tab', 'sessions',
'empty'])
'bind'])
# Exit statuses for errors. Needs to be an int for sys.exit.

View File

@ -43,11 +43,21 @@ fake_clipboard = None
log_clipboard = False
class SelectionUnsupportedError(Exception):
class ClipboardError(Exception):
"""Raised if the clipboard contents are unavailable for some reason."""
class SelectionUnsupportedError(ClipboardError):
"""Raised if [gs]et_clipboard is used and selection=True is unsupported."""
class ClipboardEmptyError(ClipboardError):
"""Raised if get_clipboard is used and the clipboard is empty."""
def elide(text, length):
"""Elide text so it uses a maximum of length chars."""
if length < 1:
@ -810,6 +820,11 @@ def get_clipboard(selection=False):
mode = QClipboard.Selection if selection else QClipboard.Clipboard
data = QApplication.clipboard().text(mode=mode)
target = "Primary selection" if selection else "Clipboard"
if not data.strip():
raise ClipboardEmptyError("{} is empty.".format(target))
log.misc.debug("{} contained: {!r}".format(target, data))
return data

View File

@ -29,10 +29,14 @@ import importlib
import collections
from PyQt5.QtCore import QT_VERSION_STR, PYQT_VERSION_STR, qVersion
from PyQt5.QtWebKit import qWebKitVersion
from PyQt5.QtNetwork import QSslSocket
from PyQt5.QtWidgets import QApplication
try:
from PyQt5.QtWebKit import qWebKitVersion
except ImportError: # pragma: no cover
qWebKitVersion = None
import qutebrowser
from qutebrowser.utils import log, utils
from qutebrowser.browser import pdfjs
@ -225,9 +229,14 @@ def version():
lines += _module_versions()
lines += ['pdf.js: {}'.format(_pdfjs_version())]
if qWebKitVersion is None:
lines.append('Webkit: no')
else:
lines.append('Webkit: {}'.format(qWebKitVersion()))
lines += [
'pdf.js: {}'.format(_pdfjs_version()),
'Webkit: {}'.format(qWebKitVersion()),
'Harfbuzz: {}'.format(os.environ.get('QT_HARFBUZZ', 'system')),
'SSL: {}'.format(QSslSocket.sslLibraryVersionString()),
'',

View File

@ -52,7 +52,9 @@ PERFECT_FILES = [
('tests/unit/browser/webkit/test_cookies.py',
'qutebrowser/browser/webkit/cookies.py'),
('tests/unit/browser/webkit/test_history.py',
'qutebrowser/browser/webkit/history.py'),
'qutebrowser/browser/history.py'),
('tests/unit/browser/webkit/test_history.py',
'qutebrowser/browser/webkit/webkithistory.py'),
('tests/unit/browser/webkit/test_tabhistory.py',
'qutebrowser/browser/webkit/tabhistory.py'),
('tests/unit/browser/webkit/http/test_http.py',
@ -60,7 +62,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',
@ -150,12 +154,14 @@ PERFECT_FILES = [
('tests/unit/completion/test_models.py',
'qutebrowser/completion/models/base.py'),
('tests/unit/completion/test_sortfilter.py',
'qutebrowser/completion/models/sortfilter.py'),
]
# 100% coverage because of end2end tests, but no perfect unit tests yet.
WHITELISTED_FILES = []
WHITELISTED_FILES = ['qutebrowser/browser/webkit/webkitinspector.py']
class Skipped(Exception):

View File

@ -42,6 +42,13 @@ from qutebrowser.commands import cmdutils, argparser
from qutebrowser.config import configdata
from qutebrowser.utils import docutils, usertypes
FILE_HEADER = """
// DO NOT EDIT THIS FILE DIRECTLY!
// It is autogenerated from docstrings by running:
// $ python3 scripts/dev/src2asciidoc.py
""".lstrip()
class UsageFormatter(argparse.HelpFormatter):
@ -239,7 +246,8 @@ def _get_command_doc_notes(cmd):
Yield:
Strings which should be added to the docs.
"""
if cmd.maxsplit is not None or cmd.no_cmd_split:
if (cmd.maxsplit is not None or cmd.no_cmd_split or
cmd.no_replace_variables and cmd.name != "spawn"):
yield ""
yield "==== note"
if cmd.maxsplit is not None:
@ -248,6 +256,8 @@ def _get_command_doc_notes(cmd):
if cmd.no_cmd_split:
yield ("* With this command, +;;+ is interpreted literally "
"instead of splitting off a second command.")
if cmd.no_replace_variables and cmd.name != "spawn":
yield r"* This command does not replace variables like +\{url\}+."
def _get_action_metavar(action, nargs=1):
@ -309,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 = []
@ -392,6 +403,7 @@ def _generate_setting_section(f, sectname, sect):
def generate_settings(filename):
"""Generate the complete settings section."""
with _open_file(filename) as f:
f.write(FILE_HEADER)
f.write("= Settings\n")
f.write(_get_setting_quickref() + "\n")
for sectname, sect in configdata.DATA.items():

View File

@ -10,7 +10,7 @@ Feature: Caret mode
Scenario: Selecting the entire document
When I run :toggle-selection
And I run :move-to-end-of-document
And I run :yank-selected
And I run :yank selection
Then the clipboard should contain:
one two three
eins zwei drei
@ -23,14 +23,14 @@ Feature: Caret mode
And I run :move-to-start-of-document
And I run :toggle-selection
And I run :move-to-end-of-word
And I run :yank-selected
And I run :yank selection
Then the clipboard should contain "one"
Scenario: Moving to end and to start of document (with selection)
When I run :move-to-end-of-document
And I run :toggle-selection
And I run :move-to-start-of-document
And I run :yank-selected
And I run :yank selection
Then the clipboard should contain:
one two three
eins zwei drei
@ -43,7 +43,7 @@ Feature: Caret mode
Scenario: Selecting a block
When I run :toggle-selection
And I run :move-to-end-of-next-block
And I run :yank-selected
And I run :yank selection
Then the clipboard should contain:
one two three
eins zwei drei
@ -53,7 +53,7 @@ Feature: Caret mode
And I run :toggle-selection
And I run :move-to-end-of-prev-block
And I run :move-to-prev-word
And I run :yank-selected
And I run :yank selection
Then the clipboard should contain:
drei
@ -64,14 +64,14 @@ Feature: Caret mode
And I run :move-to-end-of-prev-block
And I run :toggle-selection
And I run :move-to-prev-word
And I run :yank-selected
And I run :yank selection
Then the clipboard should contain "drei"
Scenario: Moving back to the start of previous block (with selection)
When I run :move-to-end-of-next-block with count 2
And I run :toggle-selection
And I run :move-to-start-of-prev-block
And I run :yank-selected
And I run :yank selection
Then the clipboard should contain:
eins zwei drei
@ -82,20 +82,20 @@ Feature: Caret mode
And I run :move-to-start-of-prev-block
And I run :toggle-selection
And I run :move-to-next-word
And I run :yank-selected
And I run :yank selection
Then the clipboard should contain "eins "
Scenario: Moving to the start of next block (with selection)
When I run :toggle-selection
And I run :move-to-start-of-next-block
And I run :yank-selected
And I run :yank selection
Then the clipboard should contain "one two three\n"
Scenario: Moving to the start of next block
When I run :move-to-start-of-next-block
And I run :toggle-selection
And I run :move-to-end-of-word
And I run :yank-selected
And I run :yank selection
Then the clipboard should contain "eins"
# line
@ -103,20 +103,20 @@ Feature: Caret mode
Scenario: Selecting a line
When I run :toggle-selection
And I run :move-to-end-of-line
And I run :yank-selected
And I run :yank selection
Then the clipboard should contain "one two three"
Scenario: Moving and selecting a line
When I run :move-to-next-line
And I run :toggle-selection
And I run :move-to-end-of-line
And I run :yank-selected
And I run :yank selection
Then the clipboard should contain "eins zwei drei"
Scenario: Selecting next line
When I run :toggle-selection
And I run :move-to-next-line
And I run :yank-selected
And I run :yank selection
Then the clipboard should contain "one two three\n"
Scenario: Moving to end and to start of line
@ -124,21 +124,21 @@ Feature: Caret mode
And I run :move-to-start-of-line
And I run :toggle-selection
And I run :move-to-end-of-word
And I run :yank-selected
And I run :yank selection
Then the clipboard should contain "one"
Scenario: Selecting a line (backwards)
When I run :move-to-end-of-line
And I run :toggle-selection
When I run :move-to-start-of-line
And I run :yank-selected
And I run :yank selection
Then the clipboard should contain "one two three"
Scenario: Selecting previous line
When I run :move-to-next-line
And I run :toggle-selection
When I run :move-to-prev-line
And I run :yank-selected
And I run :yank selection
Then the clipboard should contain "one two three\n"
Scenario: Moving to previous line
@ -146,7 +146,7 @@ Feature: Caret mode
When I run :move-to-prev-line
And I run :toggle-selection
When I run :move-to-next-line
And I run :yank-selected
And I run :yank selection
Then the clipboard should contain "one two three\n"
# word
@ -154,35 +154,35 @@ Feature: Caret mode
Scenario: Selecting a word
When I run :toggle-selection
And I run :move-to-end-of-word
And I run :yank-selected
And I run :yank selection
Then the clipboard should contain "one"
Scenario: Moving to end and selecting a word
When I run :move-to-end-of-word
And I run :toggle-selection
And I run :move-to-end-of-word
And I run :yank-selected
And I run :yank selection
Then the clipboard should contain " two"
Scenario: Moving to next word and selecting a word
When I run :move-to-next-word
And I run :toggle-selection
And I run :move-to-end-of-word
And I run :yank-selected
And I run :yank selection
Then the clipboard should contain "two"
Scenario: Moving to next word and selecting until next word
When I run :move-to-next-word
And I run :toggle-selection
And I run :move-to-next-word
And I run :yank-selected
And I run :yank selection
Then the clipboard should contain "two "
Scenario: Moving to previous word and selecting a word
When I run :move-to-end-of-word
And I run :toggle-selection
And I run :move-to-prev-word
And I run :yank-selected
And I run :yank selection
Then the clipboard should contain "one"
Scenario: Moving to previous word
@ -190,7 +190,7 @@ Feature: Caret mode
And I run :move-to-prev-word
And I run :toggle-selection
And I run :move-to-end-of-word
And I run :yank-selected
And I run :yank selection
Then the clipboard should contain "one"
# char
@ -198,21 +198,21 @@ Feature: Caret mode
Scenario: Selecting a char
When I run :toggle-selection
And I run :move-to-next-char
And I run :yank-selected
And I run :yank selection
Then the clipboard should contain "o"
Scenario: Moving and selecting a char
When I run :move-to-next-char
And I run :toggle-selection
And I run :move-to-next-char
And I run :yank-selected
And I run :yank selection
Then the clipboard should contain "n"
Scenario: Selecting previous char
When I run :move-to-end-of-word
And I run :toggle-selection
And I run :move-to-prev-char
And I run :yank-selected
And I run :yank selection
Then the clipboard should contain "e"
Scenario: Moving to previous char
@ -220,41 +220,41 @@ Feature: Caret mode
And I run :move-to-prev-char
And I run :toggle-selection
And I run :move-to-end-of-word
And I run :yank-selected
And I run :yank selection
Then the clipboard should contain "e"
# :yank-selected
# :yank selection
Scenario: :yank-selected without selection
When I run :yank-selected
Scenario: :yank selection without selection
When I run :yank selection
Then the message "Nothing to yank" should be shown.
Scenario: :yank-selected message
Scenario: :yank selection message
When I run :toggle-selection
And I run :move-to-end-of-word
And I run :yank-selected
And I run :yank selection
Then the message "3 chars yanked to clipboard" should be shown.
Scenario: :yank-selected message with one char
Scenario: :yank selection message with one char
When I run :toggle-selection
And I run :move-to-next-char
And I run :yank-selected
And I run :yank selection
Then the message "1 char yanked to clipboard" should be shown.
Scenario: :yank-selected with primary selection
Scenario: :yank selection with primary selection
When selection is supported
And I run :toggle-selection
And I run :move-to-end-of-word
And I run :yank-selected --sel
And I run :yank selection --sel
Then the message "3 chars yanked to primary selection" should be shown.
And the primary selection should contain "one"
Scenario: :yank-selected with --keep
Scenario: :yank selection with --keep
When I run :toggle-selection
And I run :move-to-end-of-word
And I run :yank-selected --keep
And I run :yank selection --keep
And I run :move-to-end-of-word
And I run :yank-selected --keep
And I run :yank selection --keep
Then the message "3 chars yanked to clipboard" should be shown.
And the message "7 chars yanked to clipboard" should be shown.
And the clipboard should contain "one two"
@ -265,7 +265,7 @@ Feature: Caret mode
When I run :toggle-selection
And I run :move-to-end-of-word
And I run :drop-selection
And I run :yank-selected
And I run :yank selection
Then the message "Nothing to yank" should be shown.
# :follow-selected

View File

@ -257,3 +257,11 @@ Feature: Using hints
And I run :hint --rapid
And I run :follow-hint 00
Then data/numbers/1.txt should be loaded
Scenario: Using a specific hints mode
When I open data/hints/number.html
And I set hints -> mode to letter
And I run :hint --mode number all
And I press the key "s"
And I run :follow-hint 1
Then data/numbers/7.txt should be loaded

View File

@ -61,6 +61,18 @@ Feature: Keyboard input
And I run :bind <ctrl-test23>
Then the message "<ctrl-test23> is bound to 'message-info bar' in normal mode" should be shown
Scenario: Binding to an alias
When I run :set aliases 'mib' 'message-info baz'
And I run :bind test25 mib
And I press the keys "test25"
Then the message "baz" should be shown
Scenario: Printing a bound alias
When I run :set aliases 'mib' 'message-info baz'
And I run :bind <test26> mib
And I run :bind <test26>
Then the message "<test26> is bound to 'mib' in normal mode" should be shown
# :unbind
Scenario: Binding and unbinding a keychain

View File

@ -524,3 +524,16 @@ Feature: Various utility commands.
Then the following tabs should be open:
- data/hints/link_blank.html
- data/hello.txt (active)
## Variables
Scenario: {url} as part of an argument
When I open data/hello.txt
And I run :message-info foo{url}
Then the message "foohttp://localhost:*/hello.txt" should be shown
Scenario: Multiple variables in an argument
When I open data/hello.txt
And I put "foo" into the clipboard
And I run :message-info {clipboard}bar{url}
Then the message "foobarhttp://localhost:*/hello.txt" should be shown

View File

@ -9,19 +9,19 @@ Feature: Searching on a page
Scenario: Searching text
When I run :search foo
And I run :yank-selected
And I run :yank selection
Then the clipboard should contain "foo"
Scenario: Searching twice
When I run :search foo
And I run :search bar
And I run :yank-selected
And I run :yank selection
Then the clipboard should contain "Bar"
Scenario: Searching with --reverse
When I set general -> ignore-case to true
And I run :search -r foo
And I run :yank-selected
And I run :yank selection
Then the clipboard should contain "Foo"
Scenario: Searching without matches
@ -32,13 +32,13 @@ Feature: Searching on a page
Scenario: Searching with / and spaces at the end (issue 874)
When I run :set-cmd-text -s /space
And I run :command-accept
And I run :yank-selected
And I run :yank selection
Then the clipboard should contain "space "
Scenario: Searching with / and slash in search term (issue 507)
When I run :set-cmd-text -s //slash
And I run :command-accept
And I run :yank-selected
And I run :yank selection
Then the clipboard should contain "/slash"
# This doesn't work because this is QtWebKit behavior.
@ -52,25 +52,25 @@ Feature: Searching on a page
Scenario: Searching text with ignore-case = true
When I set general -> ignore-case to true
And I run :search bar
And I run :yank-selected
And I run :yank selection
Then the clipboard should contain "Bar"
Scenario: Searching text with ignore-case = false
When I set general -> ignore-case to false
And I run :search bar
And I run :yank-selected
And I run :yank selection
Then the clipboard should contain "bar"
Scenario: Searching text with ignore-case = smart (lower-case)
When I set general -> ignore-case to smart
And I run :search bar
And I run :yank-selected
And I run :yank selection
Then the clipboard should contain "Bar"
Scenario: Searching text with ignore-case = smart (upper-case)
When I set general -> ignore-case to smart
And I run :search Foo
And I run :yank-selected
And I run :yank selection
Then the clipboard should contain "Foo" # even though foo was first
## :search-next
@ -79,21 +79,21 @@ Feature: Searching on a page
When I set general -> ignore-case to true
And I run :search foo
And I run :search-next
And I run :yank-selected
And I run :yank selection
Then the clipboard should contain "Foo"
Scenario: Jumping to next match with count
When I set general -> ignore-case to true
And I run :search baz
And I run :search-next with count 2
And I run :yank-selected
And I run :yank selection
Then the clipboard should contain "BAZ"
Scenario: Jumping to next match with --reverse
When I set general -> ignore-case to true
And I run :search --reverse foo
And I run :search-next
And I run :yank-selected
And I run :yank selection
Then the clipboard should contain "foo"
Scenario: Jumping to next match without search
@ -107,7 +107,7 @@ Feature: Searching on a page
And I run :search foo
And I run :tab-prev
And I run :search-next
And I run :yank-selected
And I run :yank selection
Then the clipboard should contain "foo"
## :search-prev
@ -117,7 +117,7 @@ Feature: Searching on a page
And I run :search foo
And I run :search-next
And I run :search-prev
And I run :yank-selected
And I run :yank selection
Then the clipboard should contain "foo"
Scenario: Jumping to previous match with count
@ -126,7 +126,7 @@ Feature: Searching on a page
And I run :search-next
And I run :search-next
And I run :search-prev with count 2
And I run :yank-selected
And I run :yank selection
Then the clipboard should contain "baz"
Scenario: Jumping to previous match with --reverse
@ -134,7 +134,7 @@ Feature: Searching on a page
And I run :search --reverse foo
And I run :search-next
And I run :search-prev
And I run :yank-selected
And I run :yank selection
Then the clipboard should contain "Foo"
Scenario: Jumping to previous match without search
@ -149,14 +149,14 @@ Feature: Searching on a page
When I run :search foo
And I run :search-next
And I run :search-next
And I run :yank-selected
And I run :yank selection
Then the clipboard should contain "foo"
Scenario: Wrapping around page with --reverse
When I run :search --reverse foo
And I run :search-next
And I run :search-next
And I run :yank-selected
And I run :yank selection
Then the clipboard should contain "Foo"
# TODO: wrapping message with scrolling

View File

@ -703,6 +703,53 @@ Feature: Tab management
Then the error "Nothing to undo!" should be shown
And the error "Nothing to undo!" should be shown
Scenario: Undo a tab closed by index
When I open data/numbers/1.txt
And I open data/numbers/2.txt in a new tab
And I open data/numbers/3.txt in a new tab
And I run :tab-close with count 1
And I run :undo
Then the following tabs should be open:
- data/numbers/1.txt (active)
- data/numbers/2.txt
- data/numbers/3.txt
Scenario: Undo a tab closed after switching tabs
When I open data/numbers/1.txt
And I open data/numbers/2.txt in a new tab
And I open data/numbers/3.txt in a new tab
And I run :tab-close with count 1
And I run :tab-focus 2
And I run :undo
Then the following tabs should be open:
- data/numbers/1.txt (active)
- data/numbers/2.txt
- data/numbers/3.txt
Scenario: Undo a tab closed after rearranging tabs
When I open data/numbers/1.txt
And I open data/numbers/2.txt in a new tab
And I open data/numbers/3.txt in a new tab
And I run :tab-close with count 1
And I run :tab-focus 2
And I run :tab-move with count 1
And I run :undo
Then the following tabs should be open:
- data/numbers/1.txt (active)
- data/numbers/3.txt
- data/numbers/2.txt
Scenario: Undo a tab closed after new tab opened
When I open data/numbers/1.txt
And I open data/numbers/2.txt in a new tab
And I run :tab-close with count 1
And I open data/numbers/3.txt in a new tab
And I run :undo
Then the following tabs should be open:
- data/numbers/1.txt (active)
- data/numbers/2.txt
- data/numbers/3.txt
# last-close
Scenario: last-close = blank

View File

@ -77,6 +77,7 @@ def download_open(quteproc):
cmd = '{} -c pass'.format(shlex.quote(sys.executable))
quteproc.send_cmd(':download-open {}'.format(cmd))
@bdd.when("I directly open the download")
def download_open_with_prompt(quteproc):
cmd = '{} -c pass'.format(shlex.quote(sys.executable))

View File

@ -1,6 +1,6 @@
Feature: Yanking and pasting.
:yank and :paste can be used to copy/paste the URL or title from/to the
clipboard and primary selection.
:yank, {clipboard} and {primary} can be used to copy/paste the URL or title
from/to the clipboard and primary selection.
Background:
Given I run :tab-only
@ -23,13 +23,13 @@ Feature: Yanking and pasting.
Scenario: Yanking title to clipboard
When I open data/title.html
And I wait for regex "Changing title for idx \d to 'Test title'" in the log
And I run :yank --title
And I run :yank title
Then the message "Yanked title to clipboard: Test title" should be shown
And the clipboard should contain "Test title"
Scenario: Yanking domain to clipboard
When I open data/title.html
And I run :yank --domain
And I run :yank domain
Then the message "Yanked domain to clipboard: http://localhost:(port)" should be shown
And the clipboard should contain "http://localhost:(port)"
@ -41,15 +41,15 @@ Feature: Yanking and pasting.
Scenario: Yanking pretty decoded URL
When I open data/title with spaces.html
And I run :yank --pretty
And I run :yank pretty-url
Then the message "Yanked URL to clipboard: http://localhost:(port)/data/title with spaces.html" should be shown
And the clipboard should contain "http://localhost:(port)/data/title with spaces.html"
#### :paste
#### {clipboard} and {primary}
Scenario: Pasting a URL
When I put "http://localhost:(port)/data/hello.txt" into the clipboard
And I run :paste
And I run :open {clipboard}
And I wait until data/hello.txt is loaded
Then the requests should be:
data/hello.txt
@ -57,32 +57,32 @@ Feature: Yanking and pasting.
Scenario: Pasting a URL from primary selection
When selection is supported
And I put "http://localhost:(port)/data/hello2.txt" into the primary selection
And I run :paste --sel
And I run :open {primary}
And I wait until data/hello2.txt is loaded
Then the requests should be:
data/hello2.txt
Scenario: Pasting with empty clipboard
When I put "" into the clipboard
And I run :paste
And I run :open {clipboard} (invalid command)
Then the error "Clipboard is empty." should be shown
Scenario: Pasting with empty selection
When selection is supported
And I put "" into the primary selection
And I run :paste --sel
And I run :open {primary} (invalid command)
Then the error "Primary selection is empty." should be shown
Scenario: Pasting with a space in clipboard
When I put " " into the clipboard
And I run :paste
And I run :open {clipboard} (invalid command)
Then the error "Clipboard is empty." should be shown
Scenario: Pasting in a new tab
Given I open about:blank
When I run :tab-only
And I put "http://localhost:(port)/data/hello.txt" into the clipboard
And I run :paste -t
And I run :open -t {clipboard}
And I wait until data/hello.txt is loaded
Then the following tabs should be open:
- about:blank
@ -92,7 +92,7 @@ Feature: Yanking and pasting.
Given I open about:blank
When I run :tab-only
And I put "http://localhost:(port)/data/hello.txt" into the clipboard
And I run :paste -b
And I run :open -b {clipboard}
And I wait until data/hello.txt is loaded
Then the following tabs should be open:
- about:blank (active)
@ -101,7 +101,7 @@ Feature: Yanking and pasting.
Scenario: Pasting in a new window
Given I have a fresh instance
When I put "http://localhost:(port)/data/hello.txt" into the clipboard
And I run :paste -w
And I run :open -w {clipboard}
And I wait until data/hello.txt is loaded
Then the session should look like:
windows:
@ -119,7 +119,7 @@ Feature: Yanking and pasting.
Scenario: Pasting an invalid URL
When I set general -> auto-search to false
And I put "foo bar" into the clipboard
And I run :paste
And I run :open {clipboard}
Then the error "Invalid URL" should be shown
Scenario: Pasting multiple urls in a new tab
@ -128,7 +128,7 @@ Feature: Yanking and pasting.
http://localhost:(port)/data/hello.txt
http://localhost:(port)/data/hello2.txt
http://localhost:(port)/data/hello3.txt
And I run :paste -t
And I run :open -t {clipboard}
And I wait until data/hello.txt is loaded
And I wait until data/hello2.txt is loaded
And I wait until data/hello3.txt is loaded
@ -145,7 +145,7 @@ Feature: Yanking and pasting.
this url:
http://qutebrowser.org
should not open
And I run :paste -t
And I run :open -t {clipboard}
And I wait until data/hello.txt?q=this%20url%3A%0Ahttp%3A//qutebrowser.org%0Ashould%20not%20open is loaded
Then the following tabs should be open:
- about:blank
@ -159,7 +159,7 @@ Feature: Yanking and pasting.
text:
should open
as search
And I run :paste -t
And I run :open -t {clipboard}
And I wait until data/hello.txt?q=text%3A%0Ashould%20open%0Aas%20search is loaded
Then the following tabs should be open:
- about:blank
@ -172,7 +172,7 @@ Feature: Yanking and pasting.
http://localhost:(port)/data/hello.txt
http://localhost:(port)/data/hello2.txt
http://localhost:(port)/data/hello3.txt
And I run :paste -b
And I run :open -b {clipboard}
And I wait until data/hello.txt is loaded
And I wait until data/hello2.txt is loaded
And I wait until data/hello3.txt is loaded
@ -188,7 +188,7 @@ Feature: Yanking and pasting.
http://localhost:(port)/data/hello.txt
http://localhost:(port)/data/hello2.txt
http://localhost:(port)/data/hello3.txt
And I run :paste -w
And I run :open -w {clipboard}
And I wait until data/hello.txt is loaded
And I wait until data/hello2.txt is loaded
And I wait until data/hello3.txt is loaded
@ -218,13 +218,13 @@ Feature: Yanking and pasting.
Scenario: Pasting multiple urls with an empty one
When I open about:blank
And I put "http://localhost:(port)/data/hello.txt\n\nhttp://localhost:(port)/data/hello2.txt" into the clipboard
And I run :paste -t
And I run :open -t {clipboard}
Then no crash should happen
Scenario: Pasting multiple urls with an almost empty one
When I open about:blank
And I put "http://localhost:(port)/data/hello.txt\n \nhttp://localhost:(port)/data/hello2.txt" into the clipboard
And I run :paste -t
And I run :open -t {clipboard}
Then no crash should happen
#### :paste-primary

View File

@ -329,9 +329,12 @@ class QuteProc(testprocess.Process):
"""Adjust some qutebrowser settings after starting."""
settings = [
('ui', 'message-timeout', '0'),
('network', 'ssl-strict', 'false'),
('general', 'auto-save-interval', '0'),
('general', 'new-instance-open-target.window', 'last-opened')
]
if not self._webengine:
settings.append(('network', 'ssl-strict', 'false'))
for sect, opt, value in settings:
self.set_setting(sect, opt, value)
@ -621,3 +624,4 @@ def quteproc_new(qapp, httpbin, request):
# Not calling before_test here as that would start the process
yield proc
proc.after_test(did_fail=request.node.rep_call.failed)
proc.terminate()

View File

@ -159,12 +159,12 @@ def test_custom_environment(pyproc):
@pytest.mark.posix
def test_custom_environment_no_system(monkeypatch, pyproc):
"""When env=... is given, no system environment should be present."""
monkeypatch.setenv('QUTE_TEST_ENV', 'blah')
pyproc.code = 'import os; print(os.environ.get("QUTE_TEST_ENV", "None"))'
def test_custom_environment_system_env(monkeypatch, pyproc):
"""When env=... is given, the system environment should be present."""
monkeypatch.setenv('QUTE_TEST_ENV', 'blubb')
pyproc.code = 'import os; print(os.environ["QUTE_TEST_ENV"])'
pyproc.start(env={})
pyproc.wait_for(data='None')
pyproc.wait_for(data='blubb')
class TestWaitFor:

View File

@ -251,18 +251,11 @@ class Process(QObject):
if args is None:
args = self._default_args()
if env is None:
procenv = QProcessEnvironment.systemEnvironment()
else:
procenv = QProcessEnvironment()
procenv = QProcessEnvironment.systemEnvironment()
if env is not None:
for k, v in env.items():
procenv.insert(k, v)
passthrough_vars = ['DISPLAY', 'HOME'] # so --no-xvfb works
for var in passthrough_vars:
if var in os.environ:
procenv.insert(var, os.environ[var])
self.proc.readyRead.connect(self.read_log)
self.proc.setProcessEnvironment(procenv)
self.proc.start(executable, exec_args + args)

View File

@ -54,7 +54,7 @@ def test_insert_mode(file_name, source, input_text, auto_insert, quteproc):
quteproc.send_cmd(':enter-mode caret')
quteproc.send_cmd(':toggle-selection')
quteproc.send_cmd(':move-to-prev-word')
quteproc.send_cmd(':yank-selected')
quteproc.send_cmd(':yank selection')
expected_message = '{} chars yanked to clipboard'.format(len(input_text))
quteproc.mark_expected(category='message',

View File

@ -98,9 +98,6 @@ def test_ascii_locale(httpbin, tmpdir, quteproc_new):
quteproc_new.wait_for(category='downloads',
message='Opening * with [*python*]')
quteproc_new.send_cmd(':quit')
quteproc_new.wait_for_quit()
assert len(tmpdir.listdir()) == 1
assert (tmpdir / '?-issue908.bin').exists()
@ -110,3 +107,19 @@ def test_no_loglines(quteproc_new):
quteproc_new.start(args=['--temp-basedir', '--loglines=0'] + BASE_ARGS)
quteproc_new.open_path('qute:log')
assert quteproc_new.get_content() == 'Log output was disabled.'
@pytest.mark.not_frozen
@pytest.mark.parametrize('level', ['1', '2'])
def test_optimize(quteproc_new, capfd, level):
quteproc_new.start(args=['--temp-basedir'] + BASE_ARGS,
env={'PYTHONOPTIMIZE': level})
if level == '2':
msg = ("Running on optimize level higher than 1, unexpected behavior "
"may occur.")
line = quteproc_new.wait_for(message=msg)
line.expected = True
# Waiting for quit to make sure no other warning is emitted
quteproc_new.send_cmd(':quit')
quteproc_new.wait_for_quit()

View File

@ -26,6 +26,7 @@ import signal
import pytest
@pytest.mark.parametrize('cmd', [':quit', ':later 500 quit'])
def test_smoke(cmd, capfd):
if hasattr(sys, 'frozen'):

View File

@ -29,8 +29,7 @@ from PyQt5.QtNetwork import (QNetworkRequest, QAbstractNetworkCache,
QNetworkCacheMetaData)
from PyQt5.QtWidgets import QCommonStyle, QLineEdit, QWidget
from qutebrowser.browser import browsertab
from qutebrowser.browser.webkit import history
from qutebrowser.browser import browsertab, history
from qutebrowser.config import configexc
from qutebrowser.utils import usertypes, utils
from qutebrowser.mainwindow import mainwindow
@ -256,7 +255,8 @@ class FakeWebTab(browsertab.AbstractTab):
wrapped = QWidget()
self._layout.wrap(self, wrapped)
def url(self):
def url(self, requested=False):
assert not requested
return self._url
def title(self):

View File

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

View File

@ -0,0 +1,25 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Hide unmatched rapid hints</title>
</head>
<body>
<p>When <code>hints -> hide-unmatched-rapid-hints</code> is set to true (default), rapid hints behave like normal hints, i.e. unmatched hints will be hidden as you type. Setting the option to false will disable hiding in rapid mode, which is sometimes useful (see <a href="https://github.com/The-Compiler/qutebrowser/issues/1799">#1799</a>).</p>
<p>Note that when hinting in number mode, the <code>hints -> hide-unmatched-rapid-hints</code> option affects typing the hint string (number), but not the filter (letters).</p>
<p>Here is couple of invalid links to test the behaviour:</p>
<p><a href="#foo">one</a></p>
<p><a href="#foo">two</a></p>
<p><a href="#foo">three</a></p>
<p><a href="#foo">four</a></p>
<p><a href="#foo">five</a></p>
<p><a href="#foo">six</a></p>
<p><a href="#foo">seven</a></p>
<p><a href="#foo">eight</a></p>
<p><a href="#foo">nine</a></p>
<p><a href="#foo">ten</a></p>
<p><a href="#foo">eleven</a></p>
<p><a href="#foo">twelve</a></p>
<p><a href="#foo">thirteen</a></p>
</body>
</html>

16
tests/manual/mouse.html Normal file
View File

@ -0,0 +1,16 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Mouse control</title>
</head>
<body>
<ul>
<li>Middle- or Ctrl-click on a <a href="https://www.qutebrowser.org">link</a> should open it in a new tab (fg/bg according to <code>tabs -> background-tabs</code>)</li>
<li>When clicking the link with shift, <code>background-tabs</code> should be reversed accordingly.</li>
<li>Ctrl + Mousewheel should zoom in/out</li>
<li>Back/forward keys on mouse should navigate back/forward</li>
<li>With <code>input -> rocker-gestures</code> set, no context menu should be shown, but pressing left+right/right+left buttons should navigate back/forward</li>
<li>When setting <code>input -> rocker-gestures</code> dynamically, the context menu should be hidden/shown accordingly.</li>
</body>
</html>

View File

@ -91,26 +91,35 @@ def tab(request, default_config, qtbot, tab_registry, cookiejar_and_cache):
objreg.delete('mode-manager', scope='window', window=0)
class Tab(browsertab.AbstractTab):
# pylint: disable=abstract-method
def __init__(self, win_id, parent=None):
super().__init__(win_id, parent)
mode_manager = modeman.ModeManager(0)
self.history = browsertab.AbstractHistory(self)
self.scroller = browsertab.AbstractScroller(self, parent=self)
self.caret = browsertab.AbstractCaret(win_id=self.win_id,
mode_manager=mode_manager,
tab=self, parent=self)
self.zoom = browsertab.AbstractZoom(win_id=self.win_id)
self.search = browsertab.AbstractSearch(parent=self)
self.printing = browsertab.AbstractPrinting()
def _install_event_filter(self):
pass
@pytest.mark.skipif(PYQT_VERSION < 0x050600,
reason='Causes segfaults, see #1638')
def test_tab(qtbot, view, config_stub, tab_registry):
tab_w = browsertab.AbstractTab(win_id=0)
tab_w = Tab(win_id=0)
qtbot.add_widget(tab_w)
assert tab_w.win_id == 0
assert tab_w._widget is None
mode_manager = modeman.ModeManager(0)
tab_w.history = browsertab.AbstractHistory(tab_w)
tab_w.scroller = browsertab.AbstractScroller(tab_w, parent=tab_w)
tab_w.caret = browsertab.AbstractCaret(win_id=tab_w.win_id,
mode_manager=mode_manager,
tab=tab_w, parent=tab_w)
tab_w.zoom = browsertab.AbstractZoom(win_id=tab_w.win_id)
tab_w.search = browsertab.AbstractSearch(parent=tab_w)
tab_w.printing = browsertab.AbstractPrinting()
tab_w._set_widget(view)
assert tab_w._widget is view
assert tab_w.history._tab is tab_w

View File

@ -28,7 +28,8 @@ from hypothesis import strategies
from PyQt5.QtCore import QUrl
from PyQt5.QtWebKit import QWebHistoryInterface
from qutebrowser.browser.webkit import history
from qutebrowser.browser import history
from qutebrowser.browser.webkit import webkithistory
from qutebrowser.utils import objreg
@ -371,7 +372,7 @@ def hist_interface():
title='example')
history_dict = {'http://www.example.com/': entry}
fake_hist = FakeWebHistory(history_dict)
interface = history.WebHistoryInterface(fake_hist)
interface = webkithistory.WebHistoryInterface(fake_hist)
QWebHistoryInterface.setDefaultInterface(interface)
yield
QWebHistoryInterface.setDefaultInterface(None)
@ -385,11 +386,23 @@ def test_history_interface(qtbot, webview, hist_interface):
webview.load(url)
def test_init(qapp, tmpdir, monkeypatch, fake_save_manager):
@pytest.mark.parametrize('backend', ['webengine', 'webkit'])
def test_init(backend, qapp, tmpdir, monkeypatch, fake_save_manager,
fake_args):
fake_args.backend = backend
monkeypatch.setattr(history.standarddir, 'data', lambda: str(tmpdir))
history.init(qapp)
hist = objreg.get('web-history')
assert hist.parent() is qapp
assert QWebHistoryInterface.defaultInterface()._history is hist
default_interface = QWebHistoryInterface.defaultInterface()
if backend == 'webkit':
assert default_interface._history is hist
else:
assert backend == 'webengine'
# For this to work, nothing can ever have called setDefaultInterface
# before (so we need to test webengine before webkit)
assert default_interface is None
assert fake_save_manager.add_saveable.called
objreg.delete('web-history')

View File

@ -28,13 +28,14 @@ from PyQt5.QtCore import QRect, QPoint, QUrl
from PyQt5.QtWebKit import QWebElement
import pytest
from qutebrowser.browser.webkit import webelem
from qutebrowser.browser import webelem
from qutebrowser.browser.webkit import webkitelem
def get_webelem(geometry=None, frame=None, *, null=False, style=None,
attributes=None, tagname=None, classes=None,
parent=None, js_rect_return=None, zoom_text_only=False):
"""Factory for WebElementWrapper objects based on a mock.
"""Factory for WebKitElement objects based on a mock.
Args:
geometry: The geometry of the QWebElement as QRect.
@ -117,7 +118,7 @@ def get_webelem(geometry=None, frame=None, *, null=False, style=None,
return style_dict[name]
elem.styleProperty.side_effect = _style_property
wrapped = webelem.WebElementWrapper(elem)
wrapped = webkitelem.WebKitElement(elem)
return wrapped
@ -187,7 +188,7 @@ class SelectionAndFilterTests:
webelem.Group.url]),
]
GROUPS = [e for e in webelem.Group if e != webelem.Group.focus]
GROUPS = list(webelem.Group)
COMBINATIONS = list(itertools.product(TESTS, GROUPS))
@ -215,15 +216,15 @@ 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 TestWebElementWrapper:
class TestWebKitElement:
"""Generic tests for WebElementWrapper.
"""Generic tests for WebKitElement.
Note: For some methods, there's a dedicated test class with more involved
tests.
@ -235,13 +236,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 +258,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 +286,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 +335,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 +403,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 +421,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 +434,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 +731,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 +743,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 +825,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 +840,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 +862,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 +877,7 @@ class TestIsEditable:
def stubbed_config(self, config_stub, monkeypatch):
"""Fixture to create a config stub with an input section."""
config_stub.data = {'input': {}}
monkeypatch.setattr('qutebrowser.browser.webkit.webelem.config',
monkeypatch.setattr('qutebrowser.browser.webkit.webkitelem.config',
config_stub)
return config_stub

View File

@ -21,6 +21,10 @@
"""Tests for qutebrowser.commands.cmdutils."""
import sys
import logging
import types
import pytest
from qutebrowser.commands import cmdutils, cmdexc, argparser, command
@ -291,6 +295,21 @@ class TestRegister:
else:
assert cmd._get_call_args(win_id=0) == ([expected], {})
def test_pos_arg_info(self):
@cmdutils.register()
@cmdutils.argument('foo', choices=('a', 'b'))
@cmdutils.argument('bar', choices=('x', 'y'))
@cmdutils.argument('opt')
def fun(foo, bar, opt=False):
"""Blah."""
pass
cmd = cmdutils.cmd_dict['fun']
assert cmd.get_pos_arg_info(0) == command.ArgInfo(choices=('a', 'b'))
assert cmd.get_pos_arg_info(1) == command.ArgInfo(choices=('x', 'y'))
with pytest.raises(IndexError):
cmd.get_pos_arg_info(2)
class TestArgument:
@ -339,6 +358,25 @@ class TestArgument:
assert str(excinfo.value) == "Argument marked as both count/win_id!"
def test_no_docstring(self, caplog):
with caplog.at_level(logging.WARNING):
@cmdutils.register()
def fun():
# no docstring
pass
assert len(caplog.records) == 1
msg = caplog.records[0].message
assert msg.endswith('test_cmdutils.py has no docstring')
def test_no_docstring_with_optimize(self, monkeypatch):
"""With -OO we'd get a warning on start, but no warning afterwards."""
monkeypatch.setattr(sys, 'flags', types.SimpleNamespace(optimize=2))
@cmdutils.register()
def fun():
# no docstring
pass
class TestRun:

View File

@ -27,6 +27,7 @@ from PyQt5.QtGui import QStandardItemModel
from qutebrowser.completion import completer
from qutebrowser.utils import usertypes
from qutebrowser.commands import command, cmdutils
class FakeCompletionModel(QStandardItemModel):
@ -91,24 +92,48 @@ def instances(monkeypatch):
@pytest.fixture(autouse=True)
def cmdutils_patch(monkeypatch, stubs):
"""Patch the cmdutils module to provide fake commands."""
@cmdutils.argument('section_', completion=usertypes.Completion.section)
@cmdutils.argument('option', completion=usertypes.Completion.option)
@cmdutils.argument('value', completion=usertypes.Completion.value)
def set_command(section_=None, option=None, value=None):
"""docstring."""
pass
@cmdutils.argument('topic', completion=usertypes.Completion.helptopic)
def show_help(tab=False, bg=False, window=False, topic=None):
"""docstring."""
pass
@cmdutils.argument('url', completion=usertypes.Completion.url)
@cmdutils.argument('count', count=True)
def openurl(url=None, implicit=False, bg=False, tab=False, window=False,
count=None):
"""docstring."""
pass
@cmdutils.argument('win_id', win_id=True)
@cmdutils.argument('command', completion=usertypes.Completion.command)
def bind(key, win_id, command=None, *, mode='normal', force=False):
"""docstring."""
# pylint: disable=unused-variable
pass
def tab_detach():
"""docstring."""
pass
cmds = {
'set': [usertypes.Completion.section, usertypes.Completion.option,
usertypes.Completion.value],
'help': [usertypes.Completion.helptopic],
'quickmark-load': [usertypes.Completion.quickmark_by_name],
'bookmark-load': [usertypes.Completion.bookmark_by_url],
'open': [usertypes.Completion.url],
'buffer': [usertypes.Completion.tab],
'session-load': [usertypes.Completion.sessions],
'bind': [usertypes.Completion.empty, usertypes.Completion.command],
'tab-detach': None,
'set': set_command,
'help': show_help,
'open': openurl,
'bind': bind,
'tab-detach': tab_detach,
}
cmd_utils = stubs.FakeCmdUtils({
name: stubs.FakeCommand(completion=compl)
for name, compl in cmds.items()
name: command.Command(name=name, handler=fn)
for name, fn in cmds.items()
})
monkeypatch.setattr('qutebrowser.completion.completer.cmdutils',
cmd_utils)
monkeypatch.setattr('qutebrowser.completion.completer.cmdutils', cmd_utils)
def _set_cmd_prompt(cmd, txt):
@ -143,21 +168,17 @@ def _validate_cmd_prompt(cmd, txt):
(':set general ignore-case |', usertypes.Completion.value),
(':set general huh |', None),
(':help |', usertypes.Completion.helptopic),
(':quickmark-load |', usertypes.Completion.quickmark_by_name),
(':bookmark-load |', usertypes.Completion.bookmark_by_url),
(':help |', usertypes.Completion.helptopic),
(':open |', usertypes.Completion.url),
(':buffer |', usertypes.Completion.tab),
(':session-load |', usertypes.Completion.sessions),
(':bind |', usertypes.Completion.empty),
(':bind |', None),
(':bind <c-x> |', usertypes.Completion.command),
(':bind <c-x> foo|', usertypes.Completion.command),
(':bind <c-x>| foo', usertypes.Completion.empty),
(':bind <c-x>| foo', None),
(':set| general ', usertypes.Completion.command),
(':|set general ', usertypes.Completion.command),
(':set gene|ral ignore-case', usertypes.Completion.section),
(':|', usertypes.Completion.command),
(': |', usertypes.Completion.command),
(':bookmark-load |', usertypes.Completion.bookmark_by_url),
('/|', None),
(':open -t|', None),
(':open --tab|', None),

View File

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

View File

@ -27,21 +27,22 @@ from PyQt5.QtCore import QUrl
from PyQt5.QtWidgets import QTreeView
from qutebrowser.completion.models import miscmodels, urlmodel, configmodel
from qutebrowser.browser.webkit import history
from qutebrowser.browser import history
from qutebrowser.config import sections, value
def _get_completions(model):
"""Collect all the completion entries of a model, organized by category.
def _check_completions(model, expected):
"""Check that a model contains the expected items in any order.
The result is a list of form:
[
(CategoryName: [(name, desc, misc), ...]),
(CategoryName: [(name, desc, misc), ...]),
...
]
Args:
expected: A dict of form
{
CategoryName: [(name, desc, misc), ...],
CategoryName: [(name, desc, misc), ...],
...
}
"""
completions = []
actual = {}
for i in range(0, model.rowCount()):
category = model.item(i)
entries = []
@ -50,8 +51,12 @@ def _get_completions(model):
desc = category.child(j, 1)
misc = category.child(j, 2)
entries.append((name.text(), desc.text(), misc.text()))
completions.append((category.text(), entries))
return completions
actual[category.text()] = entries
for cat_name, expected_entries in expected.items():
assert cat_name in actual
actual_items = actual[cat_name]
for expected_item in expected_entries:
assert expected_item in actual_items
def _patch_cmdutils(monkeypatch, stubs, symbol):
@ -165,7 +170,6 @@ def test_command_completion(qtmodeltester, monkeypatch, stubs, config_stub,
Validates that:
- only non-hidden and non-deprecated commands are included
- commands are sorted by name
- the command description is shown in the desc column
- the binding (if any) is shown in the misc column
- aliases are included
@ -173,55 +177,56 @@ def test_command_completion(qtmodeltester, monkeypatch, stubs, config_stub,
_patch_cmdutils(monkeypatch, stubs,
'qutebrowser.completion.models.miscmodels.cmdutils')
config_stub.data['aliases'] = {'rock': 'roll'}
key_config_stub.set_bindings_for('normal', {'s': 'stop', 'rr': 'roll'})
key_config_stub.set_bindings_for('normal', {'s': 'stop',
'rr': 'roll',
'ro': 'rock'})
model = miscmodels.CommandCompletionModel()
qtmodeltester.data_display_may_return_none = True
qtmodeltester.check(model)
actual = _get_completions(model)
assert actual == [
("Commands", [
_check_completions(model, {
"Commands": [
('stop', 'stop qutebrowser', 's'),
('drop', 'drop all user data', ''),
('rock', "Alias for 'roll'", ''),
('roll', 'never gonna give you up', 'rr'),
('stop', 'stop qutebrowser', 's')
])
]
('rock', "Alias for 'roll'", 'ro'),
]
})
def test_help_completion(qtmodeltester, monkeypatch, stubs):
def test_help_completion(qtmodeltester, monkeypatch, stubs, key_config_stub):
"""Test the results of command completion.
Validates that:
- only non-hidden and non-deprecated commands are included
- commands are sorted by name
- only non-deprecated commands are included
- the command description is shown in the desc column
- the binding (if any) is shown in the misc column
- aliases are included
- only the first line of a multiline description is shown
"""
module = 'qutebrowser.completion.models.miscmodels'
key_config_stub.set_bindings_for('normal', {'s': 'stop', 'rr': 'roll'})
_patch_cmdutils(monkeypatch, stubs, module + '.cmdutils')
_patch_configdata(monkeypatch, stubs, module + '.configdata.DATA')
model = miscmodels.HelpCompletionModel()
qtmodeltester.data_display_may_return_none = True
qtmodeltester.check(model)
actual = _get_completions(model)
assert actual == [
("Commands", [
_check_completions(model, {
"Commands": [
(':stop', 'stop qutebrowser', 's'),
(':drop', 'drop all user data', ''),
(':roll', 'never gonna give you up', ''),
(':stop', 'stop qutebrowser', '')
]),
("Settings", [
(':roll', 'never gonna give you up', 'rr'),
(':hide', '', ''),
],
"Settings": [
('general->time', 'Is an illusion.', ''),
('general->volume', 'Goes to 11', ''),
('ui->gesture', 'Waggle your hands to control qutebrowser', ''),
('ui->mind', 'Enable mind-control ui (experimental)', ''),
('ui->voice', 'Whether to respond to voice commands', ''),
])
]
]
})
def test_quickmark_completion(qtmodeltester, quickmarks):
@ -230,14 +235,13 @@ def test_quickmark_completion(qtmodeltester, quickmarks):
qtmodeltester.data_display_may_return_none = True
qtmodeltester.check(model)
actual = _get_completions(model)
assert actual == [
("Quickmarks", [
_check_completions(model, {
"Quickmarks": [
('aw', 'https://wiki.archlinux.org', ''),
('ddg', 'https://duckduckgo.com', ''),
('wiki', 'https://wikipedia.org', ''),
])
]
]
})
def test_bookmark_completion(qtmodeltester, bookmarks):
@ -246,14 +250,13 @@ def test_bookmark_completion(qtmodeltester, bookmarks):
qtmodeltester.data_display_may_return_none = True
qtmodeltester.check(model)
actual = _get_completions(model)
assert actual == [
("Bookmarks", [
_check_completions(model, {
"Bookmarks": [
('https://github.com', 'GitHub', ''),
('https://python.org', 'Welcome to Python.org', ''),
('http://qutebrowser.org', 'qutebrowser | qutebrowser', ''),
])
]
]
})
def test_url_completion(qtmodeltester, config_stub, web_history, quickmarks,
@ -271,23 +274,22 @@ def test_url_completion(qtmodeltester, config_stub, web_history, quickmarks,
qtmodeltester.data_display_may_return_none = True
qtmodeltester.check(model)
actual = _get_completions(model)
assert actual == [
("Quickmarks", [
_check_completions(model, {
"Quickmarks": [
('https://wiki.archlinux.org', 'aw', ''),
('https://duckduckgo.com', 'ddg', ''),
('https://wikipedia.org', 'wiki', ''),
]),
("Bookmarks", [
],
"Bookmarks": [
('https://github.com', 'GitHub', ''),
('https://python.org', 'Welcome to Python.org', ''),
('http://qutebrowser.org', 'qutebrowser | qutebrowser', ''),
]),
("History", [
],
"History": [
('https://python.org', 'Welcome to Python.org', '2016-03-08'),
('https://github.com', 'GitHub', '2016-05-01'),
]),
]
],
})
def test_url_completion_delete_bookmark(qtmodeltester, config_stub,
@ -332,10 +334,9 @@ def test_session_completion(qtmodeltester, session_manager_stub):
qtmodeltester.data_display_may_return_none = True
qtmodeltester.check(model)
actual = _get_completions(model)
assert actual == [
("Sessions", [('default', '', ''), ('1', '', ''), ('2', '', '')])
]
_check_completions(model, {
"Sessions": [('default', '', ''), ('1', '', ''), ('2', '', '')]
})
def test_tab_completion(qtmodeltester, fake_web_tab, app_stub, win_registry,
@ -352,17 +353,16 @@ def test_tab_completion(qtmodeltester, fake_web_tab, app_stub, win_registry,
qtmodeltester.data_display_may_return_none = True
qtmodeltester.check(model)
actual = _get_completions(model)
assert actual == [
('0', [
_check_completions(model, {
'0': [
('0/1', 'https://github.com', 'GitHub'),
('0/2', 'https://wikipedia.org', 'Wikipedia'),
('0/3', 'https://duckduckgo.com', 'DuckDuckGo')
]),
('1', [
],
'1': [
('1/1', 'https://wiki.archlinux.org', 'ArchWiki'),
])
]
]
})
def test_tab_completion_delete(qtmodeltester, fake_web_tab, qtbot, app_stub,
@ -397,13 +397,12 @@ def test_setting_section_completion(qtmodeltester, monkeypatch, stubs):
qtmodeltester.data_display_may_return_none = True
qtmodeltester.check(model)
actual = _get_completions(model)
assert actual == [
("Sections", [
_check_completions(model, {
"Sections": [
('general', 'General/miscellaneous options.', ''),
('ui', 'General options related to the user interface.', ''),
])
]
]
})
def test_setting_option_completion(qtmodeltester, monkeypatch, stubs,
@ -417,14 +416,13 @@ def test_setting_option_completion(qtmodeltester, monkeypatch, stubs,
qtmodeltester.data_display_may_return_none = True
qtmodeltester.check(model)
actual = _get_completions(model)
assert actual == [
("ui", [
_check_completions(model, {
"ui": [
('gesture', 'Waggle your hands to control qutebrowser', 'off'),
('mind', 'Enable mind-control ui (experimental)', 'on'),
('voice', 'Whether to respond to voice commands', 'sometimes'),
])
]
]
})
def test_setting_value_completion(qtmodeltester, monkeypatch, stubs,
@ -436,14 +434,43 @@ def test_setting_value_completion(qtmodeltester, monkeypatch, stubs,
qtmodeltester.data_display_may_return_none = True
qtmodeltester.check(model)
actual = _get_completions(model)
assert actual == [
("Current/Default", [
_check_completions(model, {
"Current/Default": [
('0', 'Current value', ''),
('11', 'Default value', ''),
]),
("Completions", [
],
"Completions": [
('0', '', ''),
('11', '', ''),
])
]
]
})
def test_bind_completion(qtmodeltester, monkeypatch, stubs, config_stub,
key_config_stub):
"""Test the results of keybinding command completion.
Validates that:
- only non-hidden and non-deprecated commands are included
- the command description is shown in the desc column
- the binding (if any) is shown in the misc column
- aliases are included
"""
_patch_cmdutils(monkeypatch, stubs,
'qutebrowser.completion.models.miscmodels.cmdutils')
config_stub.data['aliases'] = {'rock': 'roll'}
key_config_stub.set_bindings_for('normal', {'s': 'stop',
'rr': 'roll',
'ro': 'rock'})
model = miscmodels.BindCompletionModel()
qtmodeltester.data_display_may_return_none = True
qtmodeltester.check(model)
_check_completions(model, {
"Commands": [
('stop', 'stop qutebrowser', 's'),
('drop', 'drop all user data', ''),
('hide', '', ''),
('rock', "Alias for 'roll'", 'ro'),
]
})

View File

@ -21,9 +21,45 @@
import pytest
from PyQt5.QtCore import Qt
from qutebrowser.completion.models import base, sortfilter
def _create_model(data):
"""Create a completion model populated with the given data.
data: A list of lists, where each sub-list represents a category, each
tuple in the sub-list represents an item, and each value in the
tuple represents the item data for that column
"""
model = base.BaseCompletionModel()
for catdata in data:
cat = model.new_category('')
for itemdata in catdata:
model.new_item(cat, *itemdata)
return model
def _extract_model_data(model):
"""Express a model's data as a list for easier comparison.
Return: A list of lists, where each sub-list represents a category, each
tuple in the sub-list represents an item, and each value in the
tuple represents the item data for that column
"""
data = []
for i in range(0, model.rowCount()):
cat_idx = model.index(i, 0)
row = []
for j in range(0, model.rowCount(cat_idx)):
row.append((model.data(cat_idx.child(j, 0)),
model.data(cat_idx.child(j, 1)),
model.data(cat_idx.child(j, 2))))
data.append(row)
return data
@pytest.mark.parametrize('pattern, data, expected', [
('foo', 'barfoobar', True),
('foo', 'barFOObar', True),
@ -46,3 +82,145 @@ def test_filter_accepts_row(pattern, data, expected):
row_count = filter_model.rowCount(idx)
assert row_count == (1 if expected else 0)
@pytest.mark.parametrize('tree, first, last', [
([[('Aa',)]], 'Aa', 'Aa'),
([[('Aa',)], [('Ba',)]], 'Aa', 'Ba'),
([[('Aa',), ('Ab',), ('Ac',)], [('Ba',), ('Bb',)], [('Ca',)]],
'Aa', 'Ca'),
([[], [('Ba',)]], 'Ba', 'Ba'),
([[], [], [('Ca',)]], 'Ca', 'Ca'),
([[], [], [('Ca',), ('Cb',)]], 'Ca', 'Cb'),
([[('Aa',)], []], 'Aa', 'Aa'),
([[('Aa',)], []], 'Aa', 'Aa'),
([[('Aa',)], [], []], 'Aa', 'Aa'),
([[('Aa',)], [], [('Ca',)]], 'Aa', 'Ca'),
([[], []], None, None),
])
def test_first_last_item(tree, first, last):
"""Test that first() and last() return indexes to the first and last items.
Args:
tree: Each list represents a completion category, with each string
being an item under that category.
first: text of the first item
last: text of the last item
"""
model = _create_model(tree)
filter_model = sortfilter.CompletionFilterModel(model)
assert filter_model.data(filter_model.first_item()) == first
assert filter_model.data(filter_model.last_item()) == last
def test_set_source_model():
"""Ensure setSourceModel sets source_model and clears the pattern."""
model1 = base.BaseCompletionModel()
model2 = base.BaseCompletionModel()
filter_model = sortfilter.CompletionFilterModel(model1)
filter_model.set_pattern('foo')
# sourceModel() is cached as srcmodel, so make sure both match
assert filter_model.srcmodel is model1
assert filter_model.sourceModel() is model1
assert filter_model.pattern == 'foo'
filter_model.setSourceModel(model2)
assert filter_model.srcmodel is model2
assert filter_model.sourceModel() is model2
assert not filter_model.pattern
@pytest.mark.parametrize('tree, expected', [
([[('Aa',)]], 1),
([[('Aa',)], [('Ba',)]], 2),
([[('Aa',), ('Ab',), ('Ac',)], [('Ba',), ('Bb',)], [('Ca',)]], 6),
([[], [('Ba',)]], 1),
([[], [], [('Ca',)]], 1),
([[], [], [('Ca',), ('Cb',)]], 2),
([[('Aa',)], []], 1),
([[('Aa',)], []], 1),
([[('Aa',)], [], []], 1),
([[('Aa',)], [], [('Ca',)]], 2),
])
def test_count(tree, expected):
model = _create_model(tree)
filter_model = sortfilter.CompletionFilterModel(model)
assert filter_model.count() == expected
@pytest.mark.parametrize('pattern, dumb_sort, filter_cols, before, after', [
('foo', None, [0],
[[('foo', '', ''), ('bar', '', '')]],
[[('foo', '', '')]]),
('foo', None, [0],
[[('foob', '', ''), ('fooc', '', ''), ('fooa', '', '')]],
[[('fooa', '', ''), ('foob', '', ''), ('fooc', '', '')]]),
('foo', None, [0],
[[('foo', '', '')], [('bar', '', '')]],
[[('foo', '', '')], []]),
# prefer foobar as it starts with the pattern
('foo', None, [0],
[[('barfoo', '', ''), ('foobar', '', '')]],
[[('foobar', '', ''), ('barfoo', '', '')]]),
# however, don't rearrange categories
('foo', None, [0],
[[('barfoo', '', '')], [('foobar', '', '')]],
[[('barfoo', '', '')], [('foobar', '', '')]]),
('foo', None, [1],
[[('foo', 'bar', ''), ('bar', 'foo', '')]],
[[('bar', 'foo', '')]]),
('foo', None, [0, 1],
[[('foo', 'bar', ''), ('bar', 'foo', ''), ('bar', 'bar', '')]],
[[('foo', 'bar', ''), ('bar', 'foo', '')]]),
('foo', None, [0, 1, 2],
[[('foo', '', ''), ('bar', '')]],
[[('foo', '', '')]]),
# the fourth column is the sort role, which overrides data-based sorting
('', None, [0],
[[('two', '', '', 2), ('one', '', '', 1), ('three', '', '', 3)]],
[[('one', '', ''), ('two', '', ''), ('three', '', '')]]),
('', Qt.AscendingOrder, [0],
[[('two', '', '', 2), ('one', '', '', 1), ('three', '', '', 3)]],
[[('one', '', ''), ('two', '', ''), ('three', '', '')]]),
('', Qt.DescendingOrder, [0],
[[('two', '', '', 2), ('one', '', '', 1), ('three', '', '', 3)]],
[[('three', '', ''), ('two', '', ''), ('one', '', '')]]),
])
def test_set_pattern(pattern, dumb_sort, filter_cols, before, after):
"""Validate the filtering and sorting results of set_pattern."""
model = _create_model(before)
model.DUMB_SORT = dumb_sort
model.columns_to_filter = filter_cols
filter_model = sortfilter.CompletionFilterModel(model)
filter_model.set_pattern(pattern)
actual = _extract_model_data(filter_model)
assert actual == after
def test_sort():
"""Ensure that a sort argument passed to sort overrides DUMB_SORT.
While test_set_pattern above covers most of the sorting logic, this
particular case is easier to test separately.
"""
model = _create_model([[('B', '', '', 1),
('C', '', '', 2),
('A', '', '', 0)]])
filter_model = sortfilter.CompletionFilterModel(model)
filter_model.sort(0, Qt.AscendingOrder)
actual = _extract_model_data(filter_model)
assert actual == [[('A', '', ''), ('B', '', ''), ('C', '', '')]]
filter_model.sort(0, Qt.DescendingOrder)
actual = _extract_model_data(filter_model)
assert actual == [[('C', '', ''), ('B', '', ''), ('A', '', '')]]

View File

@ -214,7 +214,7 @@ class TestKeyConfigParser:
"""Test config.parsers.keyconf.KeyConfigParser."""
def test_cmd_binding(self, cmdline_test):
def test_cmd_binding(self, cmdline_test, config_stub):
"""Test various command bindings.
See https://github.com/The-Compiler/qutebrowser/issues/615
@ -222,6 +222,7 @@ class TestKeyConfigParser:
Args:
cmdline_test: A pytest fixture which provides testcases.
"""
config_stub.data = {'aliases': []}
kcp = keyconf.KeyConfigParser(None, None)
kcp._cur_section = 'normal'
if cmdline_test.valid:
@ -283,9 +284,24 @@ class TestKeyConfigParser:
('download-remove --all', 'download-clear'),
('hint links fill ":open {hint-url}"',
'hint links fill :open {hint-url}'),
'hint links fill :open {hint-url}'),
('hint links fill ":open -t {hint-url}"',
'hint links fill :open -t {hint-url}'),
('yank-selected', 'yank selection'),
('yank-selected --sel', 'yank selection --sel'),
('yank-selected -p', 'yank selection -s'),
('yank -t', 'yank title'),
('yank -ts', 'yank title -s'),
('yank -d', 'yank domain'),
('yank -ds', 'yank domain -s'),
('yank -p', 'yank pretty-url'),
('yank -ps', 'yank pretty-url -s'),
('paste', 'open {clipboard}'),
('paste -t', 'open -t {clipboard}'),
('paste -ws', 'open -w {primary}'),
]
)
def test_migrations(self, old, new_expected):

View File

@ -69,14 +69,14 @@ class TestArg:
config_stub.data['general']['editor'] = ['bin', 'foo', '{}', 'bar']
editor.edit("")
editor._proc._proc.start.assert_called_with(
"bin", ["foo", editor._filename, "bar"])
"bin", ["foo", editor._file.name, "bar"])
def test_placeholder_inline(self, config_stub, editor):
"""Test starting editor with placeholder arg inside of another arg."""
config_stub.data['general']['editor'] = ['bin', 'foo{}', 'bar']
editor.edit("")
editor._proc._proc.start.assert_called_with(
"bin", ["foo" + editor._filename, "bar"])
"bin", ["foo" + editor._file.name, "bar"])
class TestFileHandling:
@ -86,7 +86,7 @@ class TestFileHandling:
def test_ok(self, editor):
"""Test file handling when closing with an exit status == 0."""
editor.edit("")
filename = editor._filename
filename = editor._file.name
assert os.path.exists(filename)
assert os.path.basename(filename).startswith('qutebrowser-editor-')
editor._proc.finished.emit(0, QProcess.NormalExit)
@ -95,7 +95,7 @@ class TestFileHandling:
def test_error(self, editor):
"""Test file handling when closing with an exit status != 0."""
editor.edit("")
filename = editor._filename
filename = editor._file.name
assert os.path.exists(filename)
editor._proc._proc.exitStatus = mock.Mock(
@ -109,7 +109,7 @@ class TestFileHandling:
def test_crash(self, editor):
"""Test file handling when closing with a crash."""
editor.edit("")
filename = editor._filename
filename = editor._file.name
assert os.path.exists(filename)
editor._proc._proc.exitStatus = mock.Mock(
@ -125,7 +125,7 @@ class TestFileHandling:
def test_unreadable(self, message_mock, editor):
"""Test file handling when closing with an unreadable file."""
editor.edit("")
filename = editor._filename
filename = editor._file.name
assert os.path.exists(filename)
os.chmod(filename, 0o077)
editor._proc.finished.emit(0, QProcess.NormalExit)
@ -160,10 +160,10 @@ def test_modify(editor, initial_text, edited_text):
"""Test if inputs get modified correctly."""
editor.edit(initial_text)
with open(editor._filename, 'r', encoding='utf-8') as f:
with open(editor._file.name, 'r', encoding='utf-8') as f:
assert f.read() == initial_text
with open(editor._filename, 'w', encoding='utf-8') as f:
with open(editor._file.name, 'w', encoding='utf-8') as f:
f.write(edited_text)
editor._proc.finished.emit(0, QProcess.NormalExit)

View File

@ -126,6 +126,7 @@ class TestStringEscape:
('foobar', '"foobar"'),
('foo\\bar', r'"foo\\bar"'),
(42, '42'),
(23.42, '23.42'),
(None, 'undefined'),
(object(), TypeError),
])
@ -137,8 +138,10 @@ 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);'
assert javascript.assemble('foo', 'func', 23) == expected
@pytest.mark.parametrize('base, expected_base', [
('window', 'window'),
('foo', 'window._qutebrowser.foo'),
])
def test_assemble(base, expected_base):
expected = '"use strict";\n{}.func(23);'.format(expected_base)
assert javascript.assemble(base, 'func', 23) == expected

View File

@ -614,16 +614,18 @@ class FakeQSslSocket:
return self._version
@pytest.mark.parametrize('git_commit, harfbuzz, frozen, style, equal_qt', [
(True, True, False, True, True), # normal
(False, True, False, True, True), # no git commit
(True, False, False, True, True), # HARFBUZZ unset
(True, True, True, True, True), # frozen
(True, True, True, False, True), # no style
(True, True, False, True, False), # different Qt
@pytest.mark.parametrize(['git_commit', 'harfbuzz', 'frozen', 'style',
'equal_qt', 'with_webkit'], [
(True, True, False, True, True, True), # normal
(False, True, False, True, True, True), # no git commit
(True, False, False, True, True, True), # HARFBUZZ unset
(True, True, True, True, True, True), # frozen
(True, True, True, False, True, True), # no style
(True, True, False, True, False, True), # different Qt
(True, True, False, True, True, False), # no webkit
])
def test_version_output(git_commit, harfbuzz, frozen, style, equal_qt,
stubs, monkeypatch):
with_webkit, stubs, monkeypatch):
"""Test version.version()."""
import_path = os.path.abspath('/IMPORTPATH')
patches = {
@ -638,7 +640,7 @@ def test_version_output(git_commit, harfbuzz, frozen, style, equal_qt,
'QT VERSION' if equal_qt else 'QT RUNTIME VERSION'),
'_module_versions': lambda: ['MODULE VERSION 1', 'MODULE VERSION 2'],
'_pdfjs_version': lambda: 'PDFJS VERSION',
'qWebKitVersion': lambda: 'WEBKIT VERSION',
'qWebKitVersion': (lambda: 'WEBKIT VERSION') if with_webkit else None,
'QSslSocket': FakeQSslSocket('SSL VERSION'),
'platform.platform': lambda: 'PLATFORM',
'platform.architecture': lambda: ('ARCHITECTURE', ''),
@ -672,7 +674,7 @@ def test_version_output(git_commit, harfbuzz, frozen, style, equal_qt,
MODULE VERSION 1
MODULE VERSION 2
pdf.js: PDFJS VERSION
Webkit: WEBKIT VERSION
Webkit: {webkit}
Harfbuzz: {harfbuzz}
SSL: SSL VERSION
{style}
@ -692,6 +694,7 @@ def test_version_output(git_commit, harfbuzz, frozen, style, equal_qt,
'harfbuzz': 'HARFBUZZ' if harfbuzz else 'system',
'frozen': str(frozen),
'import_path': import_path,
'webkit': 'WEBKIT VERSION' if with_webkit else 'no'
}
expected = template.rstrip('\n').format(**substitutions)

26
tox.ini
View File

@ -6,6 +6,7 @@
[tox]
envlist = py34,py35-cov,misc,vulture,flake8,pylint,pyroma,check-manifest
distshare = {toxworkdir}
skipsdist = true
[testenv]
# https://bitbucket.org/hpk42/tox/issue/246/ - only needed for Windows though
@ -61,7 +62,6 @@ deps = {[testenv:mkvenv]deps}
# cx_Freeze doesn't support Python 3.5 yet
basepython = python3.4
passenv = {[testenv]passenv}
skip_install = true
deps =
{[testenv]deps}
-r{toxinidir}/misc/requirements/requirements-cxfreeze.txt
@ -75,7 +75,7 @@ ignore_errors = true
basepython = python3
# For global .gitignore files
passenv = HOME
deps = -r{toxinidir}/misc/requirements/requirements-pip.txt
deps =
commands =
{envpython} scripts/dev/misc_checks.py git
{envpython} scripts/dev/misc_checks.py vcs
@ -85,7 +85,9 @@ commands =
basepython = python3
deps =
-r{toxinidir}/misc/requirements/requirements-pip.txt
-r{toxinidir}/requirements.txt
-r{toxinidir}/misc/requirements/requirements-vulture.txt
setenv = PYTHONPATH={toxinidir}
commands =
{envpython} scripts/link_pyqt.py --tox {envdir}
{envpython} scripts/dev/run_vulture.py
@ -120,27 +122,28 @@ deps =
-r{toxinidir}/requirements.txt
-r{toxinidir}/misc/requirements/requirements-flake8.txt
commands =
{envpython} -m flake8
{envpython} -m flake8 {posargs:qutebrowser tests scripts}
[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