Merge 'origin/master' into tab-input-mode

This commit is contained in:
Marc Jauvin 2018-03-16 14:28:36 -04:00
commit b7159d780a
158 changed files with 7670 additions and 2806 deletions

10
.flake8
View File

@ -44,11 +44,11 @@ ignore =
min-version = 3.4.0
max-complexity = 12
per-file-ignores =
tests/*/test_*.py : D100,D101,D401
tests/unit/browser/test_history.py : N806
tests/helpers/fixtures.py : N806
tests/unit/browser/webkit/http/test_content_disposition.py : D400
scripts/dev/ci/appveyor_install.py : FI53
/tests/*/test_*.py : D100,D101,D401
/tests/unit/browser/test_history.py : N806
/tests/helpers/fixtures.py : N806
/tests/unit/browser/webkit/http/test_content_disposition.py : D400
/scripts/dev/ci/appveyor_install.py : FI53
copyright-check = True
copyright-regexp = # Copyright [\d-]+ .*
copyright-min-file-size = 110

View File

@ -14,11 +14,9 @@ matrix:
services: docker
- os: linux
env: TESTENV=py36-pyqt571
- os: linux
env: TESTENV=py36-pyqt58
- os: linux
python: 3.5
env: TESTENV=py35-pyqt59
env: TESTENV=py35-pyqt571
- os: linux
env: TESTENV=py36-pyqt59-cov
- os: linux

View File

@ -8,6 +8,7 @@ graft icons
graft doc/img
graft misc/apparmor
graft misc/userscripts
graft misc/requirements
recursive-include scripts *.py *.sh *.js
include qutebrowser/utils/testfile
include qutebrowser/git-commit-id
@ -32,8 +33,6 @@ include doc/qutebrowser.1.asciidoc
include doc/changelog.asciidoc
prune tests
prune qutebrowser/3rdparty
prune misc/requirements
prune misc/docker
exclude pytest.ini
exclude qutebrowser.rcc
exclude qutebrowser/javascript/.eslintrc.yaml

View File

@ -44,8 +44,8 @@ Documentation
In addition to the topics mentioned in this README, the following documents are
available:
* https://qutebrowser.org/img/cheatsheet-big.png[Key binding cheatsheet]: +
image:https://qutebrowser.org/img/cheatsheet-small.png["qutebrowser key binding cheatsheet",link="https://qutebrowser.org/img/cheatsheet-big.png"]
* https://raw.githubusercontent.com/qutebrowser/qutebrowser/master/doc/img/cheatsheet-big.png[Key binding cheatsheet]: +
image:https://raw.githubusercontent.com/qutebrowser/qutebrowser/master/doc/img/cheatsheet-small.png["qutebrowser key binding cheatsheet",link="https://raw.githubusercontent.com/qutebrowser/qutebrowser/master/doc/img/cheatsheet-big.png"]
* link:doc/quickstart.asciidoc[Quick start guide]
* https://www.shortcutfoo.com/app/dojos/qutebrowser[Free training course] to remember those key bindings
* link:doc/faq.asciidoc[Frequently asked questions]
@ -91,7 +91,7 @@ https://lists.schokokeks.org/mailman/listinfo.cgi/qutebrowser[mailinglist] at
mailto:qutebrowser@lists.qutebrowser.org[].
For security bugs, please contact me directly at mail@qutebrowser.org, GPG ID
https://www.the-compiler.org/pubkey.asc[0xFD55A072].
https://www.the-compiler.org/pubkey.asc[0x916eb0c8fd55a072].
Requirements
------------

View File

@ -13,47 +13,75 @@ Thanks a lot to the following people who contributed to it:
Gold sponsors
~~~~~~~~~~~~~
TODO
- Iggy
- zwitschi
- 2x Anonymous
Silver sponsors
~~~~~~~~~~~~~~~
TODO
- https://benary.org[benaryorg]
- https://scratchbook.ch[Claude]
- Martin Tournoij
- http://supported.elsensohn.ch[Thomas Elsensohn]
- Christian Helbling
- Gavin Troy
- Chris King-Parra
- Tim Das Mool Wegener
Other sponsors
~~~~~~~~~~~~~~
TODO: people with t-shirts or higher pledge levels
- 7scan
- AMD1212
- Alex
- Alex Suykov
- Alexey Zhikhartsev
- Allan Nordhøy
- Anirudh Sanjeev
- Anssi Puustinen
- Anton Grensjö
- Aristaeus
- Armin Fisslthaler
- Ashley Hauck
- Benedikt Steindorf
- Bernardo Kuri
- Blaise Duszynski
- Bostan
- Bruno Oliveira
- BunnyApocalypse
- Christian Kellermann
- Colin Jacobs
- Daniel Andersson
- Daniel Nelson
- Daniel P. Schmidt
- Daniel Salby
- Danilo
- David Beley
- David Hollings
- David Keijser
- David Parrish
- Derin Yarsuvat
- Dmytro Kostiuchenko
- Eero Kari
- Epictek
- Eric
- Faure Hu
- Ferus
- Frederik Thorøe
- G4v4g4i
- Granitosaurus
- Gyula Teleki
- H
- Heinz Bruhin
- Hosaka
- Ihor Radchenko
- Iordanis Grigoriou
- Isaac Sandaljian
- Jakub Podeszwik
- Jamie Anderson
- Jasper Woudenberg
- Jay Kamat
- Jens Højgaard
- Johannes
- John Baber-Lucero
@ -61,9 +89,11 @@ TODO: people with t-shirts or higher pledge levels
- Kenichiro Ito
- Kenny Low
- Lars Ivar Igesund
- Leulas
- Lucas Aride Moulin
- Ludovic Chabant
- Lukas Gierth
- Magnus Lindström
- Marulkan
- Matthew Chun-Lum
- Matthew Cronen
@ -80,7 +110,10 @@ TODO: people with t-shirts or higher pledge levels
- Peter Rice
- Philipp Middendorf
- Pkill9
- PluMGMK
- Prescott
- ProXicT
- Ram-Z
- Robotichead
- Roshless
- Ryan Ellis
@ -90,35 +123,53 @@ TODO: people with t-shirts or higher pledge levels
- Sean Herman
- Sebastian Frysztak
- Shelby Cruver
- Simon Désaulniers
- SirCmpwn
- Soham Pal
- Stephan Jauernick
- Stewart Webb
- Sven Reinecke
- Timothée Floure
- Tom Bass
- Tom Kirchner
- Tomas Slusny
- Tomasz Kramkowski
- Tommy Thomas
- Tuscan
- Ulrich Pötter
- Vasilij Schneidermann
- Vlaaaaaaad
- XTaran
- Z2h-A6n
- ayekat
- beanieuptop
- cee
- craftyguy
- demure
- dlangevi
- epon
- evenorbert
- fishss
- gsnewmark
- guillermohs9
- hernani
- hubcaps
- jnphilipp
- lobachevsky
- neodarz
- nihlaeth
- notbenh
- nyctea
- ongy
- patrick suwanvithaya
- pyratebeard
- p≡p foundation
- randm_dave
- sabreman
- toml
- vimja
- wiz
- 44 Anonymous
- 48 Anonymous
2016
----

View File

@ -15,85 +15,198 @@ breaking changes (such as renamed commands) can happen in minor releases.
// `Fixed` for any bug fixes.
// `Security` to invite users to upgrade in case of vulnerabilities.
v1.2.0 (unreleased)
v1.3.0 (unreleased)
-------------------
Added
~~~~~
- QtWebEngine: Caret/visual mode is now supported.
- QtWebEngine: Authentication via ~/.netrc is now supported.
- A new `qute://bindings` page, opened by `:bind`, shows all keybindings.
- `:session-load` has a new `--delete` flag which deletes the
session after loading it.
- QtWebEngine: Retrying downloads is now supported with Qt 5.10 or newer.
- QtWebEngine: Hinting and other features inside same-origin frames is now
supported.
- New `cycle-inputs.js` script in `scripts/` which can be used with `:jseval -f`
to cycle through inputs.
- New `--no-last` flag for `:tab-focus` to not focus the last tab when focusing
the currently focused one.
- New `--edit` flag for `:view-source` to open the source in an external editor.
- New `statusbar.widgets` setting to configure which widgets should be shown in
which order in the statusbar.
- New `:prompt-yank` command (bound to `Alt-y` by default) to yank URLs
referenced in prompts.
Changed
~~~~~~~
- The `hist_importer.py` script now only imports URL schemes qutebrowser can
handle.
- Deleting a prefix (`:`, `/` or `?`) via backspace now leaves command mode.
- Angular 1 elements now get hints assigned.
- `:tab-only` with pinned tabs now still closes unpinned tabs.
- GreaseMonkey `@include` and `@exclude` now support
regex matches. With QtWebEngine and Qt 5.8 and newer, Qt handles the matching,
but similar functionality was added in Qt 5.11.
- The sqlite history now uses write-ahead logging which should be
a performance and stability improvement.
- The `url.incdec_segments` option now also can take `port` as possible segment.
- QtWebEngine: `:view-source` now uses Chromium's `view-source:` scheme.
- Tabs now show their full title as tooltip.
- When an editor is spawned with `:open-editor` and `:config-edit`, the changes
are now applied as soon as the file is saved in the editor.
- When there are multiple unknown keys in a autoconfig.yml, they now all get
reported in one error.
- New `tabs.mode_on_change` setting which replaces
`tabs.persist_mode_on_change`. It can now be set to `restore` which remembers
input modes (input/passthrough) per tab.
- More performance improvements when opening/closing many tabs.
- The `:version` page now has a button to pastebin the information.
- The file dialog for downloads now has basic tab completion based on the
entered text.
- `:version` now shows OS information for POSIX OS other than Linux/macOS.
- When there's an error inserting the text from an external editor, a backup
file is now saved.
Fixed
~~~~~
- QtWebEngine: Improved fullscreen handling with Qt 5.10.
- QtWebEngine: Hinting and scrolling now works properly on special
`view-source:` pages.
- QtWebEngine: Scroll positions are now restored correctly from sessions.
- QtWebKit: `:view-source` now displays a valid URL.
- URLs containing ampersands and other special chars are now shown
correctly when filtering them in the completion.
- Using hints before a page is fully loaded is now possible again.
- Tab titles for tabs loaded from sessions should now really be correct instead
of showing the URL.
- Loading URLs with customized settings from a session now avoids an additional
reload.
- The window icon and title now get set correctly again.
v1.2.1
------
Fixed
~~~~~
- qutebrowser now starts properly when the PyQt5 QOpenGLFunctions package wasn't
found.
- The keybinding cheatsheet on the quickstart page is now loaded from a local
`qute://` URL again.
- With "tox -e mkvenv-pypi", PyQt 5.10.0 is used again instead of Qt 5.10.1,
because of an issue with Qt 5.10.1 which causes qutebrowser to fail to start
("Could not find QtWebEngineProcess").
- Unbinding keys which were bound in older qutebrowser versions now doesn't
crash anymore.
- Fixed a crash when reloading a page which wasn't fully loaded with v1.2.0
- Keys on the numeric keypad now fall back to the same bindings without `Num+`
if no `Num+` binding was found.
- Fixed hinting on some pages with Qt < 5.10.
- Titles are now displayed correctly again for tabs which are cloned or loaded
from sessions.
- Shortcuts now correctly use `Ctrl` instead of `Command` on macOS again.
v1.2.0
------
Added
~~~~~
- Initial implementation of per-domain settings:
* `:set` and `:config-cycle` now have a `-u`/`--pattern` argument taking a
https://developer.chrome.com/extensions/match_patterns[URL match pattern]
for supported settings.
* `config.set` in `config.py` now takes a third argument which is the pattern.
* New `with config.pattern('...') as p:` context manager for `config.py` to
use the shorthand syntax with a pattern.
* New `tsh` keybinding to toggle scripts for the current host. With a capital
`S`, the toggle is saved. With a capital `H`, subdomains are included. With
`u` instead of `h`, the exact current URL is used.
* New `tph` keybinding to toggle plugins, with the same additional binding
described above.
- New QtWebEngine features:
* Caret/visual mode
* Authentication via ~/.netrc
* Retrying downloads with Qt 5.10 or newer
* Hinting and other features inside same-origin frames
- New flags for existing commands:
* `:session-load` has a new `--delete` flag which deletes the
session after loading it.
* New `--no-last` flag for `:tab-focus` to not focus the last tab when focusing
the currently focused one.
* New `--edit` flag for `:view-source` to open the source in an external editor.
* New `--select` flag for `:follow-hint` which acts like the given string was entered but doesn't necessary follow the hint.
- New special pages:
* `qute://bindings` (opened via `:bind`) which shows all keybindings.
* `qute://tabs` (opened via `:buffer`) which lists all tabs.
- New settings:
* `statusbar.widgets` to configure which widgets should be shown in which
order in the statusbar.
* `tabs.mode_on_change` which replaces `tabs.persist_mode_on_change`. It can
now be set to `restore` which remembers input modes (input/passthrough)
per tab.
* `input.insert_mode.auto_enter` which makes it possible to disable entering
insert mode automatically when an editable element was clicked. Together
with `input.forward_unbound_keys`, this should allow for emacs-like
"modeless" keybindings.
- New `:prompt-yank` command (bound to `Alt-y` by default) to yank URLs
referenced in prompts.
- The `hostblock_blame` script which was removed in v1.0 was updated for the new
config and re-added.
- New `cycle-inputs.js` script in `scripts/` which can be used with `:jseval -f`
to cycle through inputs.
Changed
~~~~~~~
- Complete refactoring of key input handling, with various effects:
* emacs-like keychains such as `<Ctrl-X><Ctrl-C>` can now be bound.
* Key chains can now be bound in any mode (this allows binding unused keys in
hint mode).
* Yes/no prompts don't use keybindings from the `prompt` section anymore, they
have their own `yesno` section instead.
* Trying to bind invalid keys now shows an error.
* The `bindings.default` setting can now only be set in a `config.py`, and
existing values in `autoconfig.yml` are ignored.
- Improvements for GreaseMonkey support:
* `@include` and `@exclude` now support regex matches. With QtWebEngine and Qt
5.8 and newer, Qt handles the matching, but similar functionality will be
added in Qt 5.11.
* Support for `@requires`
* Support for the GreaseMonkey 4.0 API
- The sqlite history now uses write-ahead logging which should be
a performance and stability improvement.
- When an editor is spawned with `:open-editor` and `:config-edit`, the changes
are now applied as soon as the file is saved in the editor.
- The `hist_importer.py` script now only imports URL schemes qutebrowser can
handle.
- Deleting a prefix (`:`, `/` or `?`) via backspace now leaves command mode.
- Angular 1 elements and `<summary>`/`<details>` now get hints assigned.
- `:tab-only` with pinned tabs now still closes unpinned tabs.
- The `url.incdec_segments` option now also can take `port` as possible segment.
- QtWebEngine: `:view-source` now uses Chromium's `view-source:` scheme.
- Tabs now show their full title as tooltip.
- When there are multiple unknown keys in a autoconfig.yml, they now all get
reported in one error.
- More performance improvements when opening/closing many tabs.
- The `:version` page now has a button to pastebin the information.
- Replacements like `{url}` can now be escaped as `{{url}}`.
Fixed
~~~~~
- QtWebEngine bugfixes:
* Improved fullscreen handling with Qt 5.10.
* Hinting and scrolling now works properly on special `view-source:` pages.
* Scroll positions are now restored correctly from sessions.
* `:follow-selected` should now work in more cases with Qt > 5.10.
* Incremental search now flickers less and doesn't move to the second result
when pressing Enter.
* Keys like `Ctrl-V` or `Shift-Insert` are now correctly handled/filtered with
Qt 5.10.
* Fixed hangs/segfaults on exit with Qt 5.10.1.
* Fixed favicons sometimes getting cleared with Qt 5.10.
* Qt download objects are now cleaned up properly when a download is removed.
* JavaScript messages are now not double-HTML escaped anymore on Qt < 5.11
- QtWebKit bugfixes:
* Fixed GreaseMonkey-related crashes.
* `:view-source` now displays a valid URL.
- URLs containing ampersands and other special chars are now shown correctly
when filtering them in the completion.
- `:bookmark-add "" foo` can now be used to save the current URL with a custom
title.
- `:spawn -o` now waits until the process has finished before trying to show the
output. Previously, it incorrectly showed the previous output immediately.
- QtWebEngine: Qt download objects are now cleaned up properly when a download
is removed.
- Suspended pages now should always load the correct page when being un-suspended.
- Compatibility with Python 3.7
- Exception types are now shown properly with `:config-source` and `:config-edit`.
- When using `:bookmark-add --toggle`, bookmarks are now saved properly.
- Crash when opening an invalid URL from an application on macOS.
- Crash with an empty `completion.timestamp_format`.
- Crash when `completion.min_chars` is set in some cases.
- HTML/JS resource files are now read into RAM on start to avoid crashes when
changing qutebrowser versions while it's open.
- Setting `bindings.key_mappings` to an empty value is now allowed.
- Bindings to an empty commands are now ignored rather than crashing.
Removed
~~~~~~~
- `QUTE_SELECTED_HTML` is now not set for userscripts anymore except when called
via hints.
- The `qutebrowser_viewsource` userscript has been removed as `:view-source
--edit` can now be used.
- The `qutebrowser_viewsource` userscript has been removed as
`:view-source --edit` can now be used.
- The `tabs.persist_mode_on_change` setting has been removed and replaced by
`tabs.mode_on_change`.
v1.1.2
------
Changed
~~~~~~~
- Windows/macOS releases now bundle Qt 5.10.1 which includes security fixes from
Chromium up to version 64.0.3282.140.
Fixed
~~~~~
- QtWebEngine: Crash with Qt 5.10.1 when using :undo on some tabs.
- Compatibility with Python 3.7
v1.1.1
------

View File

@ -44,8 +44,8 @@ be easy to solve]
If you prefer C++ or Javascript to Python, see the relevant issues which involve
work in those languages:
* https://github.com/qutebrowser/qutebrowser/issues?utf8=%E2%9C%93&q=is%3Aopen%20is%3Aissue%20label%3Ac%2B%2B[C++] (mostly work on Qt, the library behind qutebrowser)
* https://github.com/qutebrowser/qutebrowser/issues?q=is%3Aopen+is%3Aissue+label%3Ajavascript[JavaScript]
* https://github.com/qutebrowser/qutebrowser/issues?q=is%3Aopen+is%3Aissue+label%3A%22language%3A+c%2B%2B%22[C++] (mostly work on Qt, the library behind qutebrowser)
* https://github.com/qutebrowser/qutebrowser/issues?q=is%3Aopen+is%3Aissue+label%3A%22language%3A+javascript%22[JavaScript]
There are also some things to do if you don't want to write code:
@ -670,10 +670,11 @@ qutebrowser release
~~~~~~~~~~~~~~~~~~~
* Make sure there are no unstaged changes and the tests are green.
* Make sure all issues with the related milestone are closed.
* Run `x=... y=...` to set the respective shell variables.
* Adjust `__version_info__` in `qutebrowser/__init__.py`.
* Update changelog (remove *(unreleased)*).
* Adjust `__version_info__` in `qutebrowser/__init__.py`.
* Commit.
* Create annotated git tag (`git tag -s "v1.$x.$y" -m "Release v1.$x.$y"`).
@ -683,7 +684,7 @@ qutebrowser release
* Mark the milestone at https://github.com/qutebrowser/qutebrowser/milestones
as closed.
* Linux: Run `git checkout v1.$x.$y && python3 scripts/dev/build_release.py --upload v1.$x.$y`.
* Linux: Run `git checkout v1.$x.$y && ./.venv/bin/python3 scripts/dev/build_release.py --upload v1.$x.$y`.
* Windows: Run `git checkout v1.X.Y; C:\Python36-32\python scripts\dev\build_release.py --asciidoc C:\Python27\python C:\asciidoc-8.6.9\asciidoc.py --upload v1.X.Y` (replace X/Y by hand).
* macOS: Run `git checkout v1.X.Y && python3 scripts/dev/build_release.py --upload v1.X.Y` (replace X/Y by hand).
* On server: Run `python3 scripts/dev/download_release.py v1.X.Y` (replace X/Y by hand).

View File

@ -32,7 +32,7 @@ When qutebrowser was created, the newer
http://webkitgtk.org/reference/webkit2gtk/stable/index.html[WebKit2 API] lacked
basic features like proxy support, and almost no projects have started porting
to WebKit2. In the meantime, this situation has improved a bit, but there are
stil only a few project which have some kind of WebKit2 support (see the
still only a few projects which have some kind of WebKit2 support (see the
https://github.com/qutebrowser/qutebrowser#similar-projects[list of
alternatives]).
+
@ -70,6 +70,31 @@ But isn't Python too slow for a browser?::
and WebKit in C++, with the
https://wiki.python.org/moin/GlobalInterpreterLock[GIL] released.
Is qutebrowser secure?::
Most security issues are in the backend (which handles networking,
rendering, JavaScript, etc.) and not qutebrowser itself.
+
qutebrowser uses http://wiki.qt.io/QtWebEngine[QtWebEngine] by default.
QtWebEngine is based on Google's https://www.chromium.org/Home[Chromium]. While
Qt only updates to a new Chromium release on every minor Qt release (all ~6
months), every patch release backports security fixes from newer Chromium
versions. In other words: As long as you're using an up-to-date Qt, you should
be recieving security updates on a regular basis, without qutebrowser having to
do anything. Chromium's process isolation and
https://chromium.googlesource.com/chromium/src/+/master/docs/design/sandbox.md[sandboxing]
features are also enabled as a second line of defense.
+
http://wiki.qt.io/QtWebKit[QtWebKit] is also supported as an alternative
backend, but hasn't seen new releases
https://github.com/annulen/webkit/releases[in a while]. It also doesn't have any
process isolation or sandboxing.
+
Security issues in qutebrowser's code happen very rarely (as per March 2018,
there has been one security issue caused by qutebrowser in over four years) and
are fixed timely. To report security bugs, please contact me directly at
mail@qutebrowser.org, GPG ID
https://www.the-compiler.org/pubkey.asc[0x916eb0c8fd55a072].
Is there an adblocker?::
There is a host-based adblocker which takes /etc/hosts-like lists. A "real"
adblocker has a
@ -187,6 +212,37 @@ Why takes it longer to open an URL in qutebrowser than in chromium?::
qutebrowser if it is not running already. Also check if you want
to use webengine as backend in line 17 and change it to your
needs.
How do I make qutebrowser use greasemonkey scripts?::
There is currently no UI elements to handle managing greasemonkey scripts.
All management of what scripts are installed or disabled is done in the
filesystem by you. qutebrowser reads all files that have an extension of
`.js` from the `<data>/greasemonkey/` folder and attempts to load them.
Where `<data>` is the qutebrowser data directory shown in the `Paths`
section of the page displayed by `:version`. If you want to disable a
script just rename it, for example, to have `.disabled` on the end, after
the `.js` extension. To reload scripts from that directory run the command
`:greasemonkey-reload`.
+
Troubleshooting: to check that your script is being loaded when
`:greasemonkey-reload` runs you can start qutebrowser with the arguments
`--debug --logfilter greasemonkey,js` and check the messages on the
program's standard output for errors parsing or loading your script.
You may also see javascript errors if your script is expecting an environment
that we fail to provide.
+
Note that there are some missing features which you may run into:
. Some scripts expect `GM_xmlhttpRequest` to ignore Cross Origin Resource
Sharing restrictions, this is currently not supported, so scripts making
requests to third party sites will often fail to function correctly.
. If your backend is a QtWebEngine version 5.8, 5.9 or 5.10 then regular
expressions are not supported in `@include` or `@exclude` rules. If your
script uses them you can re-write them to use glob expressions or convert
them to `@match` rules.
See https://wiki.greasespot.net/Metadata_Block[the wiki] for more info.
. Any greasemonkey API function to do with adding UI elements is not currently
supported. That means context menu extentensions and background pages.
== Troubleshooting

View File

@ -14,6 +14,7 @@ For command arguments, there are also some variables you can use:
- `{url}` expands to the URL of the current page
- `{url:pretty}` expands to the URL in decoded format
- `{url:host}` expands to the host part of the URL
- `{clipboard}` expands to the clipboard contents
- `{primary}` expands to the primary selection contents
@ -153,7 +154,8 @@ Bind a key to a command.
If no command is given, show the current binding for the given key. Using :bind without any arguments opens a page showing all keybindings.
==== positional arguments
* +'key'+: The keychain or special key (inside `<...>`) to bind.
* +'key'+: The keychain to bind. Examples of valid keychains are `gC`, `<Ctrl-X>` or `<Ctrl-C>a`.
* +'command'+: The command to execute, with optional args.
==== optional arguments
@ -221,7 +223,7 @@ Syntax: +:buffer ['index']+
Select tab by index or url/title best match.
Focuses window if necessary when index is given. If both index and count are given, use count.
Focuses window if necessary when index is given. If both index and count are given, use count. With neither index nor count given, open the qute://tabs page.
==== positional arguments
* +'index'+: The [win_id/]index of the tab to focus. Or a substring in which case the closest match will be focused.
@ -274,7 +276,8 @@ Set all settings back to their default.
[[config-cycle]]
=== config-cycle
Syntax: +:config-cycle [*--temp*] [*--print*] 'option' ['values' ['values' ...]]+
Syntax: +:config-cycle [*--pattern* 'pattern'] [*--temp*] [*--print*]
'option' ['values' ['values' ...]]+
Cycle an option between multiple values.
@ -283,6 +286,7 @@ Cycle an option between multiple values.
* +'values'+: The values to cycle through.
==== optional arguments
* +*-u*+, +*--pattern*+: The URL pattern to use.
* +*-t*+, +*--temp*+: Set value temporarily until qutebrowser is closed.
* +*-p*+, +*--print*+: Print the value after setting.
@ -495,10 +499,16 @@ Toggle fullscreen mode.
[[greasemonkey-reload]]
=== greasemonkey-reload
Syntax: +:greasemonkey-reload [*--force*]+
Re-read Greasemonkey scripts from disk.
The scripts are read from a 'greasemonkey' subdirectory in qutebrowser's data directory (see `:version`).
==== optional arguments
* +*-f*+, +*--force*+: For any scripts that have required dependencies, re-download them.
[[help]]
=== help
Syntax: +:help [*--tab*] [*--bg*] [*--window*] ['topic']+
@ -1110,7 +1120,7 @@ Save a session.
[[set]]
=== set
Syntax: +:set [*--temp*] [*--print*] ['option'] ['value']+
Syntax: +:set [*--temp*] [*--print*] [*--pattern* 'pattern'] ['option'] ['value']+
Set an option.
@ -1123,6 +1133,7 @@ If the option name ends with '?', the value of the option is shown instead. Usin
==== optional arguments
* +*-t*+, +*--temp*+: Set value temporarily until qutebrowser is closed.
* +*-p*+, +*--print*+: Print the value after setting.
* +*-u*+, +*--pattern*+: The URL pattern to use.
[[set-cmd-text]]
=== set-cmd-text
@ -1313,7 +1324,8 @@ Syntax: +:unbind [*--mode* 'mode'] 'key'+
Unbind a keychain.
==== positional arguments
* +'key'+: The keychain or special key (inside <...>) to unbind.
* +'key'+: The keychain to unbind. See the help for `:bind` for the correct syntax for keychains.
==== optional arguments
* +*-m*+, +*--mode*+: A mode to unbind the key in (default: `normal`). See `:help bindings.commands` for the available modes.
@ -1494,13 +1506,16 @@ Drop selection and keep selection mode enabled.
[[follow-hint]]
=== follow-hint
Syntax: +:follow-hint ['keystring']+
Syntax: +:follow-hint [*--select*] ['keystring']+
Follow a hint.
==== positional arguments
* +'keystring'+: The hint to follow.
==== optional arguments
* +*-s*+, +*--select*+: Only select the given hint, don't necessarily follow it.
[[leave-mode]]
=== leave-mode
Leave the mode we're currently in.

View File

@ -63,6 +63,10 @@ customizable.
Using the link:commands.html#set[`:set`] command and command completion, you
can quickly set settings interactively, for example `:set tabs.position left`.
Some settings are also customizable for a given
https://developer.chrome.com/apps/match_patterns[URL pattern] by doing e.g.
`:set --pattern=*://example.com/ content.images false`.
To get more help about a setting, use e.g. `:help tabs.position`.
To bind and unbind keys, you can use the link:commands.html#bind[`:bind`] and
@ -147,7 +151,6 @@ prefix to preserve backslashes) or a Python regex object:
If you want to read a setting, you can use the `c` object to do so as well:
`c.colors.tabs.even.bg = c.colors.tabs.odd.bg`.
Using strings for setting names
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@ -171,6 +174,26 @@ To read a setting, use the `config.get` method:
color = config.get('colors.completion.fg')
----
Per-domain settings
~~~~~~~~~~~~~~~~~~~
Using `config.set`, some settings are also customizable for a given
https://developer.chrome.com/apps/match_patterns[URL pattern]:
[source,python]
----
config.set('content.images', False, '*://example.com/')
----
Alternatively, you can use `with config.pattern(...) as p:` to get a shortcut
similar to `c.` which is scoped to the given domain:
[source,python]
----
with config.pattern('*://example.com/') as p:
p.content.images = False
----
Binding keys
~~~~~~~~~~~~

View File

@ -201,6 +201,7 @@
|<<hints.uppercase,hints.uppercase>>|Make characters in hint strings uppercase.
|<<history_gap_interval,history_gap_interval>>|Maximum time (in minutes) between two history items for them to be considered being from the same browsing session.
|<<input.forward_unbound_keys,input.forward_unbound_keys>>|Which unbound keys to forward to the webview in normal mode.
|<<input.insert_mode.auto_enter,input.insert_mode.auto_enter>>|Enter insert mode if an editable element is clicked.
|<<input.insert_mode.auto_leave,input.insert_mode.auto_leave>>|Leave insert mode if a non-editable element is clicked.
|<<input.insert_mode.auto_load,input.insert_mode.auto_load>>|Automatically enter insert mode if an editable element is focused after loading the page.
|<<input.insert_mode.plugins,input.insert_mode.plugins>>|Switch to insert mode when clicking flash and other plugins.
@ -322,7 +323,7 @@ While it's possible to add bindings with this setting, it's recommended to use `
This setting is a dictionary containing mode names and dictionaries mapping keys to commands:
`{mode: {key: command}}`
If you want to map a key to another key, check the `bindings.key_mappings` setting instead.
For special keys (can't be part of a keychain), enclose them in `<`...`>`. For modifiers, you can use either `-` or `+` as delimiters, and these names:
For modifiers, you can use either `-` or `+` as delimiters, and these names:
* Control: `Control`, `Ctrl`
@ -358,11 +359,8 @@ The following modes are available:
* prompt: Entered when there's a prompt to display, like for download
locations or when invoked from JavaScript.
+
You can bind normal keys in this mode, but they will be only active when
a yes/no-prompt is asked. For other prompt modes, you can only bind
special keys.
* yesno: Entered when there's a yes/no prompt displayed.
* caret: Entered when pressing the `v` mode, used to select text using the
keyboard.
@ -379,6 +377,8 @@ Default keybindings. If you want to add bindings, modify `bindings.commands` ins
The main purpose of this setting is that you can set it to an empty dictionary if you want to load no default keybindings at all.
If you want to preserve default bindings (and get new bindings when there is an update), use `config.bind()` in `config.py` or the `:bind` command, and leave this setting alone.
This setting can only be set in config.py.
Type: <<types,Dict>>
Default:
@ -582,8 +582,20 @@ Default:
* +pass:[sk]+: +pass:[set-cmd-text -s :bind]+
* +pass:[sl]+: +pass:[set-cmd-text -s :set -t]+
* +pass:[ss]+: +pass:[set-cmd-text -s :set]+
* +pass:[tPH]+: +pass:[config-cycle -p -u *://*.{url:host}/* content.plugins ;; reload]+
* +pass:[tPh]+: +pass:[config-cycle -p -u *://{url:host}/* content.plugins ;; reload]+
* +pass:[tPu]+: +pass:[config-cycle -p -u {url} content.plugins ;; reload]+
* +pass:[tSH]+: +pass:[config-cycle -p -u *://*.{url:host}/* content.javascript.enabled ;; reload]+
* +pass:[tSh]+: +pass:[config-cycle -p -u *://{url:host}/* content.javascript.enabled ;; reload]+
* +pass:[tSu]+: +pass:[config-cycle -p -u {url} content.javascript.enabled ;; reload]+
* +pass:[th]+: +pass:[back -t]+
* +pass:[tl]+: +pass:[forward -t]+
* +pass:[tpH]+: +pass:[config-cycle -p -t -u *://*.{url:host}/* content.plugins ;; reload]+
* +pass:[tph]+: +pass:[config-cycle -p -t -u *://{url:host}/* content.plugins ;; reload]+
* +pass:[tpu]+: +pass:[config-cycle -p -t -u {url} content.plugins ;; reload]+
* +pass:[tsH]+: +pass:[config-cycle -p -t -u *://*.{url:host}/* content.javascript.enabled ;; reload]+
* +pass:[tsh]+: +pass:[config-cycle -p -t -u *://{url:host}/* content.javascript.enabled ;; reload]+
* +pass:[tsu]+: +pass:[config-cycle -p -t -u {url} content.javascript.enabled ;; reload]+
* +pass:[u]+: +pass:[undo]+
* +pass:[v]+: +pass:[enter-mode caret]+
* +pass:[wB]+: +pass:[set-cmd-text -s :bookmark-load -w]+
@ -636,11 +648,17 @@ Default:
* +pass:[&lt;Shift-Tab&gt;]+: +pass:[prompt-item-focus prev]+
* +pass:[&lt;Tab&gt;]+: +pass:[prompt-item-focus next]+
* +pass:[&lt;Up&gt;]+: +pass:[prompt-item-focus prev]+
* +pass:[n]+: +pass:[prompt-accept no]+
* +pass:[y]+: +pass:[prompt-accept yes]+
- +pass:[register]+:
* +pass:[&lt;Escape&gt;]+: +pass:[leave-mode]+
- +pass:[yesno]+:
* +pass:[&lt;Alt-Shift-Y&gt;]+: +pass:[prompt-yank --sel]+
* +pass:[&lt;Alt-Y&gt;]+: +pass:[prompt-yank]+
* +pass:[&lt;Escape&gt;]+: +pass:[leave-mode]+
* +pass:[&lt;Return&gt;]+: +pass:[prompt-accept]+
* +pass:[n]+: +pass:[prompt-accept no]+
* +pass:[y]+: +pass:[prompt-accept yes]+
[[bindings.key_mappings]]
=== bindings.key_mappings
@ -1447,6 +1465,8 @@ Default:
Enable support for the HTML 5 web application cache feature.
An application cache acts like an HTTP cache in some sense. For documents that use the application cache via JavaScript, the loader engine will first ask the application cache for the contents, before hitting the network.
This setting supports URL patterns.
Type: <<types,Bool>>
Default: +pass:[true]+
@ -1524,6 +1544,8 @@ This setting is only available with the QtWebKit backend.
=== content.dns_prefetch
Try to pre-fetch DNS entries to speed up browsing.
This setting supports URL patterns.
Type: <<types,Bool>>
Default: +pass:[true]+
@ -1535,6 +1557,8 @@ This setting is only available with the QtWebKit backend.
Expand each subframe to its contents.
This will flatten all the frames to become one scrollable page.
This setting supports URL patterns.
Type: <<types,Bool>>
Default: +pass:[false]+
@ -1651,6 +1675,8 @@ Default:
=== content.hyperlink_auditing
Enable hyperlink auditing (`<a ping>`).
This setting supports URL patterns.
Type: <<types,Bool>>
Default: +pass:[false]+
@ -1659,6 +1685,8 @@ Default: +pass:[false]+
=== content.images
Load images automatically in web pages.
This setting supports URL patterns.
Type: <<types,Bool>>
Default: +pass:[true]+
@ -1676,6 +1704,8 @@ Default: +pass:[true]+
Allow JavaScript to read from or write to the clipboard.
With QtWebEngine, writing the clipboard as response to a user interaction is always allowed.
This setting supports URL patterns.
Type: <<types,Bool>>
Default: +pass:[false]+
@ -1684,6 +1714,8 @@ Default: +pass:[false]+
=== content.javascript.can_close_tabs
Allow JavaScript to close tabs.
This setting supports URL patterns.
Type: <<types,Bool>>
Default: +pass:[false]+
@ -1694,6 +1726,8 @@ This setting is only available with the QtWebKit backend.
=== content.javascript.can_open_tabs_automatically
Allow JavaScript to open new tabs without user interaction.
This setting supports URL patterns.
Type: <<types,Bool>>
Default: +pass:[false]+
@ -1702,6 +1736,8 @@ Default: +pass:[false]+
=== content.javascript.enabled
Enable JavaScript.
This setting supports URL patterns.
Type: <<types,Bool>>
Default: +pass:[true]+
@ -1741,6 +1777,8 @@ Default: +pass:[true]+
=== content.local_content_can_access_file_urls
Allow locally loaded documents to access other local URLs.
This setting supports URL patterns.
Type: <<types,Bool>>
Default: +pass:[true]+
@ -1749,6 +1787,8 @@ Default: +pass:[true]+
=== content.local_content_can_access_remote_urls
Allow locally loaded documents to access remote URLs.
This setting supports URL patterns.
Type: <<types,Bool>>
Default: +pass:[false]+
@ -1757,6 +1797,8 @@ Default: +pass:[false]+
=== content.local_storage
Enable support for HTML 5 local storage and Web SQL.
This setting supports URL patterns.
Type: <<types,Bool>>
Default: +pass:[true]+
@ -1817,6 +1859,8 @@ This setting is only available with the QtWebKit backend.
=== content.plugins
Enable plugins in Web pages.
This setting supports URL patterns.
Type: <<types,Bool>>
Default: +pass:[false]+
@ -1825,6 +1869,8 @@ Default: +pass:[false]+
=== content.print_element_backgrounds
Draw the background color and images also when the page is printed.
This setting supports URL patterns.
Type: <<types,Bool>>
Default: +pass:[true]+
@ -1889,6 +1935,8 @@ Default: empty
=== content.webgl
Enable WebGL.
This setting supports URL patterns.
Type: <<types,Bool>>
Default: +pass:[true]+
@ -1906,6 +1954,8 @@ Default: +pass:[false]+
Monitor load requests for cross-site scripting attempts.
Suspicious scripts will be blocked and reported in the inspector's JavaScript console. Enabling this feature might have an impact on performance.
This setting supports URL patterns.
Type: <<types,Bool>>
Default: +pass:[false]+
@ -2351,6 +2401,14 @@ Valid values:
Default: +pass:[auto]+
[[input.insert_mode.auto_enter]]
=== input.insert_mode.auto_enter
Enter insert mode if an editable element is clicked.
Type: <<types,Bool>>
Default: +pass:[true]+
[[input.insert_mode.auto_leave]]
=== input.insert_mode.auto_leave
Leave insert mode if a non-editable element is clicked.
@ -2379,6 +2437,8 @@ Default: +pass:[false]+
=== input.links_included_in_focus_chain
Include hyperlinks in the keyboard focus chain when tabbing.
This setting supports URL patterns.
Type: <<types,Bool>>
Default: +pass:[true]+
@ -2406,6 +2466,8 @@ Default: +pass:[false]+
Enable spatial navigation.
Spatial navigation consists in the ability to navigate between focusable elements in a Web page, such as hyperlinks and form controls, by using Left, Right, Up and Down arrow keys. For example, if the user presses the Right key, heuristics determine whether there is an element he might be trying to reach towards the right and which element he probably wants.
This setting supports URL patterns.
Type: <<types,Bool>>
Default: +pass:[false]+
@ -2550,6 +2612,8 @@ Default: +pass:[false]+
Enable smooth scrolling for web pages.
Note smooth scrolling does not work with the `:scroll-px` command.
This setting supports URL patterns.
Type: <<types,Bool>>
Default: +pass:[false]+
@ -3137,6 +3201,8 @@ Default: +pass:[512]+
=== zoom.text_only
Apply the zoom factor on a frame only to the text or to all content.
This setting supports URL patterns.
Type: <<types,Bool>>
Default: +pass:[false]+

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1024 KiB

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 46 KiB

View File

@ -35,17 +35,21 @@ Ubuntu 16.04 doesn't come with an up-to-date engine (a new enough QtWebKit, or
QtWebEngine). However, it comes with Python 3.5, so you can
<<tox,install qutebrowser via tox>>.
You'll need some basic libraries to use the tox-installed PyQt:
----
# apt install libglib2.0-0 libgl1 libfontconfig1 libx11-xcb1 libxi6 libxrender1 libdbus-1-3
----
Debian Stretch / Ubuntu 17.04 and 17.10
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Those versions come with QtWebEngine in the repositories. This makes it possible
to install qutebrowser via the Debian package.
Get the qutebrowser package from the
https://github.com/qutebrowser/qutebrowser/releases[release page] and download
the https://qutebrowser.org/python3-pypeg2_2.15.2-1_all.deb[PyPEG2 package].
(If you are using debian testing you can just use the python3-pypeg2 package from the repos)
Download the https://packages.debian.org/sid/all/qutebrowser/download[qutebrowser] and
https://packages.debian.org/sid/all/python3-pypeg2/download[PyPEG2]
package from the Debian repositories.
Install the packages:
@ -277,6 +281,11 @@ PS C:\> Install-Package qutebrowser
----
C:\> choco install qutebrowser
----
* Scoop's client
----
C:\> scoop bucket add extras
C:\> scoop install qutebrowser
----
Manual install
~~~~~~~~~~~~~~

View File

@ -22,9 +22,9 @@ Basic keybindings to get you started
What to do now
--------------
* View the link:https://qutebrowser.org/img/cheatsheet-big.png[key binding cheatsheet]
* View the link:https://raw.githubusercontent.com/qutebrowser/qutebrowser/master/doc/img/cheatsheet-big.png[key binding cheatsheet]
to make yourself familiar with the key bindings: +
image:https://qutebrowser.org/img/cheatsheet-small.png["qutebrowser key binding cheatsheet",link="https://qutebrowser.org/img/cheatsheet-big.png"]
image:https://raw.githubusercontent.com/qutebrowser/qutebrowser/master/doc/img/cheatsheet-small.png["qutebrowser key binding cheatsheet",link="https://raw.githubusercontent.com/qutebrowser/qutebrowser/master/doc/img/cheatsheet-big.png"]
* There's also a https://www.shortcutfoo.com/app/dojos/qutebrowser[free training
course] on shortcutfoo for the keybindings - note that you need to be in
insert mode (i) for it to work.

View File

@ -32,22 +32,24 @@
objecttolerance="10"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="1.24"
inkscape:cx="305.29152"
inkscape:cy="465.48793"
inkscape:zoom="1.7536248"
inkscape:cx="430.72917"
inkscape:cy="268.64059"
inkscape:document-units="px"
inkscape:current-layer="layer1"
width="1024px"
height="640px"
showgrid="false"
inkscape:window-width="1024"
inkscape:window-height="723"
inkscape:window-width="2560"
inkscape:window-height="1440"
inkscape:window-x="0"
inkscape:window-y="0"
showguides="true"
inkscape:guide-bbox="true"
inkscape:window-maximized="1"
inkscape:snap-text-baseline="true">
inkscape:window-maximized="0"
inkscape:snap-text-baseline="true"
inkscape:measure-start="0,0"
inkscape:measure-end="0,0">
<inkscape:grid
id="GridFromPre046Settings"
type="xygrid"
@ -2688,7 +2690,8 @@
id="flowPara5711"> </flowPara></flowRoot> <flowRoot
xml:space="preserve"
id="flowRoot5691-0"
style="font-style:normal;font-weight:normal;font-size:12.80000019px;line-height:0.01%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1.06666672"><flowRegion
style="font-style:normal;font-weight:normal;font-size:12.80000019px;line-height:0.01%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1.06666672"
transform="translate(0,-10)"><flowRegion
id="flowRegion5693-7"
style="font-family:sans-serif;stroke-width:1.06666672"><rect
id="rect5695-0"
@ -3660,5 +3663,64 @@
sodipodi:role="line"
style="font-size:8.53333378px;line-height:0.89999998;stroke-width:1.06666672"
id="tspan6220">items</tspan></text>
</g>
<text
xml:space="preserve"
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:12.80000019px;line-height:0%;font-family:TlwgTypewriter;text-align:start;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1.06666672"
x="417.29486"
y="205.18887"
id="text7245-1-3"><tspan
sodipodi:role="line"
x="417.29486"
y="205.18887"
id="tspan7366-3-6"
style="font-size:9.60000038px;line-height:0.89999998;stroke-width:1.06666672"> </tspan><tspan
sodipodi:role="line"
x="417.29486"
y="213.07179"
id="tspan5293-53"
style="font-size:8.53333378px;line-height:0.89999998;stroke-width:1.06666672">toggle</tspan><tspan
sodipodi:role="line"
x="417.29486"
y="220.75179"
style="font-size:8.53333378px;line-height:0.89999998;stroke-width:1.06666672;fill:#ff0000"
id="tspan6091">(12)</tspan><tspan
sodipodi:role="line"
x="417.29486"
y="225.70012"
style="font-size:8.53333378px;line-height:0.89999998;stroke-width:1.06666672"
id="tspan6087" /><tspan
sodipodi:role="line"
x="417.29486"
y="225.70012"
style="font-size:8.53333378px;line-height:0.89999998;stroke-width:1.06666672"
id="tspan6089" /></text>
<flowRoot
transform="translate(-1.2953814,90.2721)"
xml:space="preserve"
id="flowRoot5691-0-5"
style="font-style:normal;font-weight:normal;font-size:12.80000019px;line-height:0.01%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1.06666672"><flowRegion
id="flowRegion5693-7-6"
style="font-family:sans-serif;stroke-width:1.06666672"><rect
id="rect5695-0-2"
width="344"
height="173.33333"
x="19.42783"
y="520.07886"
style="font-family:sans-serif;fill:#000000;stroke-width:1.13777781" /></flowRegion><flowPara
style="font-weight:bold;font-size:10.66666698px;line-height:1.25;font-family:sans-serif;-inkscape-font-specification:'Sans Bold';fill:#000000;stroke-width:1.06666672"
id="flowPara5701-9-2"><flowSpan
style="font-weight:bold;font-family:sans-serif;-inkscape-font-specification:'Sans Bold';fill:#ff0000;stroke-width:1.06666672"
id="flowSpan5705-5-1">(12)</flowSpan> toggling settings:</flowPara><flowPara
style="font-size:10.66666698px;line-height:1.25;font-family:sans-serif;fill:#000000;stroke-width:1.06666672"
id="flowPara6196">tsh - toggle scripts for the current host (temporarily)</flowPara><flowPara
style="font-size:10.66666698px;line-height:1.25;font-family:sans-serif;fill:#000000;stroke-width:1.06666672"
id="flowPara6200">tSh - like <flowSpan
style="font-style:italic"
id="flowSpan6202">tsh</flowSpan>, but permanently</flowPara><flowPara
style="font-size:10.66666698px;line-height:1.25;font-family:sans-serif;fill:#000000;stroke-width:1.06666672"
id="flowPara6206">tsH/tsu - like <flowSpan
style="font-style:italic"
id="flowSpan6210">tsh</flowSpan>, but including subdomains / with exact URL</flowPara><flowPara
style="font-size:10.66666698px;line-height:1.25;font-family:sans-serif;fill:#000000;stroke-width:1.06666672"
id="flowPara6208">tph - toggle plugins</flowPara></flowRoot> </g>
</svg>

Before

Width:  |  Height:  |  Size: 178 KiB

After

Width:  |  Height:  |  Size: 181 KiB

View File

@ -3,7 +3,7 @@
certifi==2018.1.18
chardet==3.0.4
codecov==2.0.15
coverage==4.5
coverage==4.5.1
idna==2.6
requests==2.18.4
urllib3==1.22

View File

@ -6,12 +6,12 @@ flake8-bugbear==18.2.0
flake8-builtins==1.0.post0
flake8-comprehensions==1.4.1
flake8-copyright==0.2.0
flake8-debugger==3.0.0
flake8-debugger==3.1.0
flake8-deprecated==1.3
flake8-docstrings==1.3.0
flake8-future-import==0.4.4
flake8-mock==0.3
flake8-per-file-ignores==0.4
flake8-per-file-ignores==0.5
flake8-polyfill==1.0.2
flake8-string-format==0.2.3
flake8-tidy-imports==1.1.0

View File

@ -1,8 +1,8 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py
appdirs==1.4.3
packaging==16.8
packaging==17.1
pyparsing==2.2.0
setuptools==38.5.0
setuptools==38.5.2
six==1.11.0
wheel==0.30.0

View File

@ -5,7 +5,7 @@ certifi==2018.1.18
chardet==3.0.4
github3.py==0.9.6
idna==2.6
isort==4.3.2
isort==4.3.4
lazy-object-proxy==1.3.1
mccabe==0.6.1
-e git+https://github.com/PyCQA/pylint.git#egg=pylint

View File

@ -5,7 +5,7 @@ certifi==2018.1.18
chardet==3.0.4
github3.py==0.9.6
idna==2.6
isort==4.3.2
isort==4.3.4
lazy-object-proxy==1.3.1
mccabe==0.6.1
pylint==1.8.2

View File

@ -0,0 +1,4 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py
PyQt5==5.10 # rq.filter: != 5.10.1
sip==4.19.8

View File

@ -0,0 +1,2 @@
PyQt5==5.10.0
#@ filter: PyQt5 != 5.10.1

View File

@ -1,4 +1,4 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py
PyQt5==5.10
sip==4.19.7
PyQt5==5.10.1
sip==4.19.8

View File

@ -5,13 +5,13 @@ beautifulsoup4==4.6.0
cheroot==6.0.0
click==6.7
# colorama==0.3.9
coverage==4.5
coverage==4.5.1
EasyProcess==0.2.3
fields==5.0.0
Flask==0.12.2
glob2==0.6
hunter==2.0.2
hypothesis==3.44.25
hypothesis==3.49.0
itsdangerous==0.24
# Jinja2==2.10
Mako==1.0.7
@ -22,18 +22,18 @@ parse-type==0.4.2
pluggy==0.6.0
py==1.5.2
py-cpuinfo==3.3.0
pytest==3.4.0
pytest==3.4.2
pytest-bdd==2.20.0
pytest-benchmark==3.1.1
pytest-cov==2.5.1
pytest-faulthandler==1.3.1
pytest-faulthandler==1.4.1
pytest-instafail==0.3.0
pytest-mock==1.6.3
pytest-mock==1.7.1
pytest-qt==2.3.1
pytest-repeat==0.4.1
pytest-rerunfailures==4.0
pytest-travis-fold==1.3.0
pytest-xvfb==1.0.0
pytest-xvfb==1.1.0
PyVirtualDisplay==0.2.1
six==1.11.0
vulture==0.26

View File

@ -52,7 +52,7 @@ die() {
if ! [ -d "$DOWNLOAD_DIR" ] ; then
die "Download directory »$DOWNLOAD_DIR« not found!"
fi
if ! which "${ROFI_CMD}" > /dev/null ; then
if ! command -v "${ROFI_CMD}" > /dev/null ; then
die "Rofi command »${ROFI_CMD}« not found in PATH!"
fi

View File

@ -220,7 +220,7 @@ user_pattern='^(user|username|login): '
GPG_OPTS=( "--quiet" "--yes" "--compress-algo=none" "--no-encrypt-to" )
GPG="gpg"
export GPG_TTY="${GPG_TTY:-$(tty 2>/dev/null)}"
which gpg2 &>/dev/null && GPG="gpg2"
command -v gpg2 &>/dev/null && GPG="gpg2"
[[ -n $GPG_AGENT_INFO || $GPG == "gpg2" ]] && GPG_OPTS+=( "--batch" "--use-agent" )
pass_backend() {

View File

@ -13,7 +13,11 @@
from __future__ import absolute_import
import codecs, os
tmpfile=os.path.expanduser('~/.local/share/qutebrowser/userscripts/readability.html')
tmpfile = os.path.join(
os.environ.get('QUTE_DATA_DIR',
os.path.expanduser('~/.local/share/qutebrowser')),
'userscripts/readability.html')
if not os.path.exists(os.path.dirname(tmpfile)):
os.makedirs(os.path.dirname(tmpfile))

52
misc/userscripts/tor_identity Executable file
View File

@ -0,0 +1,52 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# Copyright 2018 jnphilipp <mail@jnphilipp.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/>.
# Change your tor identity.
#
# Set a hotkey to launch this script, then:
# :bind ti spawn --userscript tor_identity PASSWORD
#
# Use the hotkey to change your tor identity, press 'ti' to change it.
# https://stem.torproject.org/faq.html#how-do-i-request-a-new-identity-from-tor
#
import os
import sys
try:
from stem import Signal
from stem.control import Controller
except ImportError:
if os.getenv('QUTE_FIFO'):
with open(os.environ['QUTE_FIFO'], 'w') as f:
f.write('message-error "Failed to import stem."')
else:
print('Failed to import stem.')
password = sys.argv[1]
with Controller.from_port(port=9051) as controller:
controller.authenticate(password)
controller.signal(Signal.NEWNYM)
if os.getenv('QUTE_FIFO'):
with open(os.environ['QUTE_FIFO'], 'w') as f:
f.write('message-info "Tor identity changed."')
else:
print('Tor identity changed.')

View File

@ -27,6 +27,7 @@ markers =
no_invalid_lines: Don't fail on unparseable lines in end2end tests
issue2478: Tests which are broken on Windows with QtWebEngine, https://github.com/qutebrowser/qutebrowser/issues/2478
issue3572: Tests which are broken with QtWebEngine and Qt 5.10, https://github.com/qutebrowser/qutebrowser/issues/3572
qtbug60673: Tests which are broken if the conversion from orange selection to real selection is flaky
fake_os: Fake utils.is_* to a fake operating system
unicode_locale: Tests which need an unicode locale to work
qt_log_level_fail = WARNING

View File

@ -26,7 +26,7 @@ __copyright__ = "Copyright 2014-2018 Florian Bruhin (The Compiler)"
__license__ = "GPL"
__maintainer__ = __author__
__email__ = "mail@qutebrowser.org"
__version_info__ = (1, 1, 1)
__version_info__ = (1, 2, 1)
__version__ = '.'.join(str(e) for e in __version_info__)
__description__ = "A keyboard-driven, vim-like browser based on PyQt5."

View File

@ -95,6 +95,7 @@ def run(args):
log.init.debug("Initializing directories...")
standarddir.init(args)
utils.preload_resources()
log.init.debug("Initializing config...")
configinit.early_init(args)
@ -339,7 +340,7 @@ def _open_startpage(win_id=None):
for cur_win_id in list(window_ids): # Copying as the dict could change
tabbed_browser = objreg.get('tabbed-browser', scope='window',
window=cur_win_id)
if tabbed_browser.count() == 0:
if tabbed_browser.widget.count() == 0:
log.init.debug("Opening start pages")
for url in config.val.url.start_pages:
tabbed_browser.tabopen(url)
@ -772,6 +773,8 @@ class Quitter:
pre_text="Error while saving {}".format(key))
# Disable storage so removing tempdir will work
websettings.shutdown()
# Disable application proxy factory to fix segfaults with Qt 5.10.1
proxy.shutdown()
# Re-enable faulthandler to stdout, then remove crash log
log.destroy.debug("Deactivating crash log...")
objreg.get('crash-handler').destroy_crashlogfile()
@ -840,7 +843,11 @@ class Application(QApplication):
def event(self, e):
"""Handle macOS FileOpen events."""
if e.type() == QEvent.FileOpen:
open_url(e.url(), no_raise=True)
url = e.url()
if url.isValid():
open_url(url, no_raise=True)
else:
message.error("Invalid URL: {}".format(url.errorString()))
else:
return super().event(e)
@ -878,6 +885,7 @@ class EventFilter(QObject):
self._handlers = {
QEvent.KeyPress: self._handle_key_event,
QEvent.KeyRelease: self._handle_key_event,
QEvent.ShortcutOverride: self._handle_key_event,
}
def _handle_key_event(self, event):
@ -895,7 +903,7 @@ class EventFilter(QObject):
return False
try:
man = objreg.get('mode-manager', scope='window', window='current')
return man.eventFilter(event)
return man.handle_event(event)
except objreg.RegistryUnavailableError:
# No window available yet, or not a MainWindow
return False

View File

@ -30,7 +30,8 @@ from PyQt5.QtWidgets import QWidget, QApplication
from qutebrowser.keyinput import modeman
from qutebrowser.config import config
from qutebrowser.utils import utils, objreg, usertypes, log, qtutils
from qutebrowser.utils import (utils, objreg, usertypes, log, qtutils,
urlutils, message)
from qutebrowser.misc import miscwidgets, objects
from qutebrowser.browser import mouse, hints
@ -94,6 +95,8 @@ class TabData:
keep_icon: Whether the (e.g. cloned) icon should not be cleared on page
load.
inspector: The QWebInspector used for this webview.
open_target: Where to open the next link.
Only used for QtWebKit.
override_target: Override for open_target for fake clicks (like hints).
Only used for QtWebKit.
pinned: Flag to pin the tab.
@ -104,6 +107,7 @@ class TabData:
keep_icon = attr.ib(False)
inspector = attr.ib(None)
open_target = attr.ib(usertypes.ClickTarget.normal)
override_target = attr.ib(None)
pinned = attr.ib(False)
fullscreen = attr.ib(False)
@ -342,7 +346,7 @@ class AbstractCaret(QObject):
def _on_mode_entered(self, mode):
raise NotImplementedError
def _on_mode_left(self):
def _on_mode_left(self, mode):
raise NotImplementedError
def move_to_next_line(self, count=1):
@ -612,6 +616,7 @@ class AbstractTab(QWidget):
process terminated.
arg 0: A TerminationStatus member.
arg 1: The exit code.
predicted_navigation: Emitted before we tell Qt to open a URL.
"""
window_close_requested = pyqtSignal()
@ -629,6 +634,7 @@ class AbstractTab(QWidget):
add_history_item = pyqtSignal(QUrl, QUrl, str) # url, requested url, title
fullscreen_requested = pyqtSignal(bool)
renderer_process_terminated = pyqtSignal(TerminationStatus, int)
predicted_navigation = pyqtSignal(QUrl)
def __init__(self, *, win_id, mode_manager, private, parent=None):
self.private = private
@ -659,6 +665,8 @@ class AbstractTab(QWidget):
objreg.register('hintmanager', hintmanager, scope='tab',
window=self.win_id, tab=self.tab_id)
self.predicted_navigation.connect(self._on_predicted_navigation)
def _set_widget(self, widget):
# pylint: disable=protected-access
self._widget = widget
@ -671,6 +679,7 @@ class AbstractTab(QWidget):
self.printing._widget = widget
self.action._widget = widget
self.elements._widget = widget
self.settings._settings = widget.settings()
self._install_event_filter()
self.zoom.set_default()
@ -705,6 +714,14 @@ class AbstractTab(QWidget):
evt.posted = True
QApplication.postEvent(recipient, evt)
@pyqtSlot(QUrl)
def _on_predicted_navigation(self, url):
"""Adjust the title if we are going to visit an URL soon."""
qtutils.ensure_valid(url)
url_string = url.toDisplayString()
log.webview.debug("Predicted navigation: {}".format(url_string))
self.title_changed.emit(url_string)
@pyqtSlot(QUrl)
def _on_url_changed(self, url):
"""Update title when URL has changed and no title is available."""
@ -719,6 +736,23 @@ class AbstractTab(QWidget):
self._set_load_status(usertypes.LoadStatus.loading)
self.load_started.emit()
@pyqtSlot(usertypes.NavigationRequest)
def _on_navigation_request(self, navigation):
"""Handle common acceptNavigationRequest code."""
url = utils.elide(navigation.url.toDisplayString(), 100)
log.webview.debug("navigation request: url {}, type {}, is_main_frame "
"{}".format(url,
navigation.navigation_type,
navigation.is_main_frame))
if (navigation.navigation_type == navigation.Type.link_clicked and
not navigation.url.isValid()):
msg = urlutils.get_errstring(navigation.url,
"Invalid link clicked")
message.error(msg)
self.data.open_target = usertypes.ClickTarget.normal
navigation.accepted = False
def handle_auto_insert_mode(self, ok):
"""Handle `input.insert_mode.auto_load` after loading finished."""
if not config.val.input.insert_mode.auto_load or not ok:
@ -788,11 +822,12 @@ class AbstractTab(QWidget):
def load_status(self):
return self._load_status
def _openurl_prepare(self, url):
def _openurl_prepare(self, url, *, predict=True):
qtutils.ensure_valid(url)
self.title_changed.emit(url.toDisplayString())
if predict:
self.predicted_navigation.emit(url)
def openurl(self, url):
def openurl(self, url, *, predict=True):
raise NotImplementedError
def reload(self, *, force=False):

View File

@ -27,14 +27,13 @@ import typing
from PyQt5.QtWidgets import QApplication, QTabBar, QDialog
from PyQt5.QtCore import pyqtSlot, Qt, QUrl, QEvent, QUrlQuery
from PyQt5.QtGui import QKeyEvent
from PyQt5.QtPrintSupport import QPrintDialog, QPrintPreviewDialog
from qutebrowser.commands import userscripts, cmdexc, cmdutils, runners
from qutebrowser.config import config, configdata
from qutebrowser.browser import (urlmarks, browsertab, inspector, navigate,
webelem, downloads)
from qutebrowser.keyinput import modeman
from qutebrowser.keyinput import modeman, keyutils
from qutebrowser.utils import (message, usertypes, log, qtutils, urlutils,
objreg, utils, standarddir)
from qutebrowser.utils.usertypes import KeyMode
@ -54,7 +53,6 @@ class CommandDispatcher:
cmdutils.register() decorators are run, currentWidget() will return None.
Attributes:
_editor: The ExternalEditor object.
_win_id: The window ID the CommandDispatcher is associated with.
_tabbed_browser: The TabbedBrowser used.
"""
@ -74,16 +72,16 @@ class CommandDispatcher:
def _count(self):
"""Convenience method to get the widget count."""
return self._tabbed_browser.count()
return self._tabbed_browser.widget.count()
def _set_current_index(self, idx):
"""Convenience method to set the current widget index."""
cmdutils.check_overflow(idx, 'int')
self._tabbed_browser.setCurrentIndex(idx)
self._tabbed_browser.widget.setCurrentIndex(idx)
def _current_index(self):
"""Convenience method to get the current widget index."""
return self._tabbed_browser.currentIndex()
return self._tabbed_browser.widget.currentIndex()
def _current_url(self):
"""Convenience method to get the current url."""
@ -102,7 +100,7 @@ class CommandDispatcher:
def _current_widget(self):
"""Get the currently active widget from a command."""
widget = self._tabbed_browser.currentWidget()
widget = self._tabbed_browser.widget.currentWidget()
if widget is None:
raise cmdexc.CommandError("No WebView available yet!")
return widget
@ -148,10 +146,10 @@ class CommandDispatcher:
None if no widget was found.
"""
if count is None:
return self._tabbed_browser.currentWidget()
return self._tabbed_browser.widget.currentWidget()
elif 1 <= count <= self._count():
cmdutils.check_overflow(count + 1, 'int')
return self._tabbed_browser.widget(count - 1)
return self._tabbed_browser.widget.widget(count - 1)
else:
return None
@ -164,7 +162,7 @@ class CommandDispatcher:
if not show_error:
return
raise cmdexc.CommandError("No last focused tab!")
idx = self._tabbed_browser.indexOf(tab)
idx = self._tabbed_browser.widget.indexOf(tab)
if idx == -1:
raise cmdexc.CommandError("Last focused tab vanished!")
self._set_current_index(idx)
@ -213,7 +211,7 @@ class CommandDispatcher:
what's configured in 'tabs.select_on_remove'.
count: The tab index to close, or None
"""
tabbar = self._tabbed_browser.tabBar()
tabbar = self._tabbed_browser.widget.tabBar()
selection_override = self._get_selection_override(prev, next_,
opposite)
@ -265,7 +263,7 @@ class CommandDispatcher:
return
to_pin = not tab.data.pinned
self._tabbed_browser.set_tab_pinned(tab, to_pin)
self._tabbed_browser.widget.set_tab_pinned(tab, to_pin)
@cmdutils.register(instance='command-dispatcher', name='open',
maxsplit=0, scope='window')
@ -484,7 +482,8 @@ class CommandDispatcher:
"""
cmdutils.check_exclusive((bg, window), 'bw')
curtab = self._current_widget()
cur_title = self._tabbed_browser.page_title(self._current_index())
cur_title = self._tabbed_browser.widget.page_title(
self._current_index())
try:
history = curtab.history.serialize()
except browsertab.WebTabError as e:
@ -500,18 +499,18 @@ class CommandDispatcher:
newtab = new_tabbed_browser.tabopen(background=bg)
new_tabbed_browser = objreg.get('tabbed-browser', scope='window',
window=newtab.win_id)
idx = new_tabbed_browser.indexOf(newtab)
idx = new_tabbed_browser.widget.indexOf(newtab)
new_tabbed_browser.set_page_title(idx, cur_title)
new_tabbed_browser.widget.set_page_title(idx, cur_title)
if config.val.tabs.favicons.show:
new_tabbed_browser.setTabIcon(idx, curtab.icon())
new_tabbed_browser.widget.setTabIcon(idx, curtab.icon())
if config.val.tabs.tabs_are_windows:
new_tabbed_browser.window().setWindowIcon(curtab.icon())
new_tabbed_browser.widget.window().setWindowIcon(curtab.icon())
newtab.data.keep_icon = True
newtab.history.deserialize(history)
newtab.zoom.set_factor(curtab.zoom.factor())
new_tabbed_browser.set_tab_pinned(newtab, curtab.data.pinned)
new_tabbed_browser.widget.set_tab_pinned(newtab, curtab.data.pinned)
return newtab
@cmdutils.register(instance='command-dispatcher', scope='window')
@ -847,7 +846,7 @@ class CommandDispatcher:
keep: Stay in visual mode after yanking the selection.
"""
if what == 'title':
s = self._tabbed_browser.page_title(self._current_index())
s = self._tabbed_browser.widget.page_title(self._current_index())
elif what == 'domain':
port = self._current_url().port()
s = '{}://{}{}'.format(self._current_url().scheme(),
@ -959,7 +958,7 @@ class CommandDispatcher:
force: Avoid confirmation for pinned tabs.
"""
cmdutils.check_exclusive((prev, next_), 'pn')
cur_idx = self._tabbed_browser.currentIndex()
cur_idx = self._tabbed_browser.widget.currentIndex()
assert cur_idx != -1
def _to_close(i):
@ -1076,11 +1075,11 @@ class CommandDispatcher:
tabbed_browser = objreg.get('tabbed-browser', scope='window',
window=win_id)
if not 0 < idx <= tabbed_browser.count():
if not 0 < idx <= tabbed_browser.widget.count():
raise cmdexc.CommandError(
"There's no tab with index {}!".format(idx))
return (tabbed_browser, tabbed_browser.widget(idx-1))
return (tabbed_browser, tabbed_browser.widget.widget(idx-1))
@cmdutils.register(instance='command-dispatcher', scope='window',
maxsplit=0)
@ -1092,24 +1091,26 @@ class CommandDispatcher:
Focuses window if necessary when index is given. If both index and
count are given, use count.
With neither index nor count given, open the qute://tabs page.
Args:
index: The [win_id/]index of the tab to focus. Or a substring
in which case the closest match will be focused.
count: The tab index to focus, starting with 1.
"""
if count is None and index is None:
raise cmdexc.CommandError("buffer: Either a count or the argument "
"index must be specified.")
self.openurl('qute://tabs/', tab=True)
return
if count is not None:
index = str(count)
tabbed_browser, tab = self._resolve_buffer_index(index)
window = tabbed_browser.window()
window = tabbed_browser.widget.window()
window.activateWindow()
window.raise_()
tabbed_browser.setCurrentWidget(tab)
tabbed_browser.widget.setCurrentWidget(tab)
@cmdutils.register(instance='command-dispatcher', scope='window')
@cmdutils.argument('index', choices=['last'])
@ -1193,7 +1194,7 @@ class CommandDispatcher:
cur_idx = self._current_index()
cmdutils.check_overflow(cur_idx, 'int')
cmdutils.check_overflow(new_idx, 'int')
self._tabbed_browser.tabBar().moveTab(cur_idx, new_idx)
self._tabbed_browser.widget.tabBar().moveTab(cur_idx, new_idx)
@cmdutils.register(instance='command-dispatcher', scope='window',
maxsplit=0, no_replace_variables=True)
@ -1277,10 +1278,10 @@ class CommandDispatcher:
idx = self._current_index()
if idx != -1:
env['QUTE_TITLE'] = self._tabbed_browser.page_title(idx)
env['QUTE_TITLE'] = self._tabbed_browser.widget.page_title(idx)
# FIXME:qtwebengine: If tab is None, run_async will fail!
tab = self._tabbed_browser.currentWidget()
tab = self._tabbed_browser.widget.currentWidget()
try:
url = self._tabbed_browser.current_url()
@ -1638,7 +1639,7 @@ class CommandDispatcher:
ed = editor.ExternalEditor(watch=True, parent=self._tabbed_browser)
ed.file_updated.connect(functools.partial(
self.on_file_updated, elem))
self.on_file_updated, ed, elem))
ed.editing_finished.connect(lambda: mainwindow.raise_window(
objreg.last_focused_window(), alert=False))
ed.edit(text, caret_position)
@ -1653,7 +1654,7 @@ class CommandDispatcher:
tab = self._current_widget()
tab.elements.find_focused(self._open_editor_cb)
def on_file_updated(self, elem, text):
def on_file_updated(self, ed, elem, text):
"""Write the editor text into the form field and clean up tempfile.
Callback for GUIProcess when the edited text was updated.
@ -1666,8 +1667,10 @@ class CommandDispatcher:
elem.set_value(text)
except webelem.OrphanedError as e:
message.error('Edited element vanished')
ed.backup()
except webelem.Error as e:
raise cmdexc.CommandError(str(e))
message.error(str(e))
ed.backup()
@cmdutils.register(instance='command-dispatcher', maxsplit=0,
scope='window')
@ -1776,10 +1779,10 @@ class CommandDispatcher:
"""
self.set_mark("'")
tab = self._current_widget()
if tab.search.search_displayed:
tab.search.clear()
if not text:
if tab.search.search_displayed:
tab.search.clear()
return
options = {
@ -2110,15 +2113,13 @@ class CommandDispatcher:
global_: If given, the keys are sent to the qutebrowser UI.
"""
try:
keyinfos = utils.parse_keystring(keystring)
except utils.KeyParseError as e:
sequence = keyutils.KeySequence.parse(keystring)
except keyutils.KeyParseError as e:
raise cmdexc.CommandError(str(e))
for keyinfo in keyinfos:
press_event = QKeyEvent(QEvent.KeyPress, keyinfo.key,
keyinfo.modifiers, keyinfo.text)
release_event = QKeyEvent(QEvent.KeyRelease, keyinfo.key,
keyinfo.modifiers, keyinfo.text)
for keyinfo in sequence:
press_event = keyinfo.to_event(QEvent.KeyPress)
release_event = keyinfo.to_event(QEvent.KeyRelease)
if global_:
window = QApplication.focusWindow()
@ -2218,5 +2219,5 @@ class CommandDispatcher:
pass
return
window = self._tabbed_browser.window()
window = self._tabbed_browser.widget.window()
window.setWindowState(window.windowState() ^ Qt.WindowFullScreen)

View File

@ -238,11 +238,14 @@ class FileDownloadTarget(_DownloadTarget):
Attributes:
filename: Filename where the download should be saved.
force_overwrite: Whether to overwrite the target without
prompting the user.
"""
def __init__(self, filename):
def __init__(self, filename, force_overwrite=False):
# pylint: disable=super-init-not-called
self.filename = filename
self.force_overwrite = force_overwrite
def suggested_filename(self):
return os.path.basename(self.filename)
@ -738,7 +741,8 @@ class AbstractDownloadItem(QObject):
if isinstance(target, FileObjDownloadTarget):
self._set_fileobj(target.fileobj, autoclose=False)
elif isinstance(target, FileDownloadTarget):
self._set_filename(target.filename)
self._set_filename(
target.filename, force_overwrite=target.force_overwrite)
elif isinstance(target, OpenFileDownloadTarget):
try:
fobj = temp_download_manager.get_tmpfile(self.basename)

View File

@ -23,13 +23,16 @@ import re
import os
import json
import fnmatch
import functools
import glob
import textwrap
import attr
from PyQt5.QtCore import pyqtSignal, QObject, QUrl
from qutebrowser.utils import log, standarddir, jinja, objreg
from qutebrowser.utils import log, standarddir, jinja, objreg, utils
from qutebrowser.commands import cmdutils
from qutebrowser.browser import downloads
def _scripts_dir():
@ -45,6 +48,7 @@ class GreasemonkeyScript:
self._code = code
self.includes = []
self.excludes = []
self.requires = []
self.description = None
self.name = None
self.namespace = None
@ -66,6 +70,8 @@ class GreasemonkeyScript:
self.run_at = value
elif name == 'noframes':
self.runs_on_sub_frames = False
elif name == 'require':
self.requires.append(value)
HEADER_REGEX = r'// ==UserScript==|\n+// ==/UserScript==\n'
PROPS_REGEX = r'// @(?P<prop>[^\s]+)\s*(?P<val>.*)'
@ -93,7 +99,7 @@ class GreasemonkeyScript:
"""Return the processed JavaScript code of this script.
Adorns the source code with GM_* methods for Greasemonkey
compatibility and wraps it in an IFFE to hide it within a
compatibility and wraps it in an IIFE to hide it within a
lexical scope. Note that this means line numbers in your
browser's debugger/inspector will not match up to the line
numbers in the source script directly.
@ -115,6 +121,14 @@ class GreasemonkeyScript:
'run-at': self.run_at,
})
def add_required_script(self, source):
"""Add the source of a required script to this script."""
# The additional source is indented in case it also contains a
# metadata block. Because we pass everything at once to
# QWebEngineScript and that would parse the first metadata block
# found as the valid one.
self._code = "\n".join([textwrap.indent(source, " "), self._code])
@attr.s
class MatchingScripts(object):
@ -145,15 +159,24 @@ class GreasemonkeyManager(QObject):
def __init__(self, parent=None):
super().__init__(parent)
self._run_start = []
self._run_end = []
self._run_idle = []
self._in_progress_dls = []
self.load_scripts()
@cmdutils.register(name='greasemonkey-reload',
instance='greasemonkey')
def load_scripts(self):
def load_scripts(self, force=False):
"""Re-read Greasemonkey scripts from disk.
The scripts are read from a 'greasemonkey' subdirectory in
qutebrowser's data directory (see `:version`).
Args:
force: For any scripts that have required dependencies,
re-download them.
"""
self._run_start = []
self._run_end = []
@ -169,24 +192,115 @@ class GreasemonkeyManager(QObject):
script = GreasemonkeyScript.parse(script_file.read())
if not script.name:
script.name = script_filename
if script.run_at == 'document-start':
self._run_start.append(script)
elif script.run_at == 'document-end':
self._run_end.append(script)
elif script.run_at == 'document-idle':
self._run_idle.append(script)
else:
if script.run_at:
log.greasemonkey.warning(
"Script {} has invalid run-at defined, "
"defaulting to document-end".format(script_path))
# Default as per
# https://wiki.greasespot.net/Metadata_Block#.40run-at
self._run_end.append(script)
log.greasemonkey.debug("Loaded script: {}".format(script.name))
self.add_script(script, force)
self.scripts_reloaded.emit()
def add_script(self, script, force=False):
"""Add a GreasemonkeyScript to this manager.
Args:
force: Fetch and overwrite any dependancies which are
already locally cached.
"""
if script.requires:
log.greasemonkey.debug(
"Deferring script until requirements are "
"fulfilled: {}".format(script.name))
self._get_required_scripts(script, force)
else:
self._add_script(script)
def _add_script(self, script):
if script.run_at == 'document-start':
self._run_start.append(script)
elif script.run_at == 'document-end':
self._run_end.append(script)
elif script.run_at == 'document-idle':
self._run_idle.append(script)
else:
if script.run_at:
log.greasemonkey.warning("Script {} has invalid run-at "
"defined, defaulting to "
"document-end"
.format(script.name))
# Default as per
# https://wiki.greasespot.net/Metadata_Block#.40run-at
self._run_end.append(script)
log.greasemonkey.debug("Loaded script: {}".format(script.name))
def _required_url_to_file_path(self, url):
requires_dir = os.path.join(_scripts_dir(), 'requires')
if not os.path.exists(requires_dir):
os.mkdir(requires_dir)
return os.path.join(requires_dir, utils.sanitize_filename(url))
def _on_required_download_finished(self, script, download):
self._in_progress_dls.remove(download)
if not self._add_script_with_requires(script):
log.greasemonkey.debug(
"Finished download {} for script {} "
"but some requirements are still pending"
.format(download.basename, script.name))
def _add_script_with_requires(self, script, quiet=False):
"""Add a script with pending downloads to this GreasemonkeyManager.
Specifically a script that has dependancies specified via an
`@require` rule.
Args:
script: The GreasemonkeyScript to add.
quiet: True to suppress the scripts_reloaded signal after
adding `script`.
Returns: True if the script was added, False if there are still
dependancies being downloaded.
"""
# See if we are still waiting on any required scripts for this one
for dl in self._in_progress_dls:
if dl.requested_url in script.requires:
return False
# Need to add the required scripts to the IIFE now
for url in reversed(script.requires):
target_path = self._required_url_to_file_path(url)
log.greasemonkey.debug(
"Adding required script for {} to IIFE: {}"
.format(script.name, url))
with open(target_path, encoding='utf8') as f:
script.add_required_script(f.read())
self._add_script(script)
if not quiet:
self.scripts_reloaded.emit()
return True
def _get_required_scripts(self, script, force=False):
required_dls = [(url, self._required_url_to_file_path(url))
for url in script.requires]
if not force:
required_dls = [(url, path) for (url, path) in required_dls
if not os.path.exists(path)]
if not required_dls:
# All the required files exist already
self._add_script_with_requires(script, quiet=True)
return
download_manager = objreg.get('qtnetwork-download-manager')
for url, target_path in required_dls:
target = downloads.FileDownloadTarget(target_path,
force_overwrite=True)
download = download_manager.get(QUrl(url), target=target,
auto_remove=True)
download.requested_url = url
self._in_progress_dls.append(download)
if download.successful:
self._on_required_download_finished(script, download)
else:
download.finished.connect(
functools.partial(self._on_required_download_finished,
script, download))
def scripts_for(self, url):
"""Fetch scripts that are registered to run for url.

View File

@ -682,7 +682,7 @@ class HintManager(QObject):
"""
tabbed_browser = objreg.get('tabbed-browser', scope='window',
window=self._win_id)
tab = tabbed_browser.currentWidget()
tab = tabbed_browser.widget.currentWidget()
if tab is None:
raise cmdexc.CommandError("No WebView available yet!")
@ -909,20 +909,27 @@ class HintManager(QObject):
@cmdutils.register(instance='hintmanager', scope='tab',
modes=[usertypes.KeyMode.hint])
def follow_hint(self, keystring=None):
def follow_hint(self, select=False, keystring=None):
"""Follow a hint.
Args:
select: Only select the given hint, don't necessarily follow it.
keystring: The hint to follow, or None.
"""
if keystring is None:
if self._context.to_follow is None:
raise cmdexc.CommandError("No hint to follow")
elif select:
raise cmdexc.CommandError("Can't use --select without hint.")
else:
keystring = self._context.to_follow
elif keystring not in self._context.labels:
raise cmdexc.CommandError("No hint {}!".format(keystring))
self._fire(keystring)
if select:
self.handle_partial_key(keystring)
else:
self._fire(keystring)
@pyqtSlot(usertypes.KeyMode)
def on_mode_left(self, mode):

View File

@ -151,8 +151,9 @@ class MouseEventFilter(QObject):
if elem.is_editable():
log.mouse.debug("Clicked editable element!")
modeman.enter(self._tab.win_id, usertypes.KeyMode.insert,
'click', only_if_normal=True)
if config.val.input.insert_mode.auto_enter:
modeman.enter(self._tab.win_id, usertypes.KeyMode.insert,
'click', only_if_normal=True)
else:
log.mouse.debug("Clicked non-editable element!")
if config.val.input.insert_mode.auto_leave:

View File

@ -34,6 +34,10 @@ def init():
QNetworkProxyFactory.setApplicationProxyFactory(proxy_factory)
def shutdown():
QNetworkProxyFactory.setApplicationProxyFactory(None)
class ProxyFactory(QNetworkProxyFactory):
"""Factory for proxies to be used by qutebrowser."""

View File

@ -30,8 +30,10 @@ import time
import textwrap
import mimetypes
import urllib
import collections
import pkg_resources
import sip
from PyQt5.QtCore import QUrlQuery, QUrl
import qutebrowser
@ -201,6 +203,27 @@ def qute_bookmarks(_url):
return 'text/html', html
@add_handler('tabs')
def qute_tabs(_url):
"""Handler for qute://tabs. Display information about all open tabs."""
tabs = collections.defaultdict(list)
for win_id, window in objreg.window_registry.items():
if sip.isdeleted(window):
continue
tabbed_browser = objreg.get('tabbed-browser',
scope='window',
window=win_id)
for tab in tabbed_browser.widgets():
if tab.url() not in [QUrl("qute://tabs/"), QUrl("qute://tabs")]:
urlstr = tab.url().toDisplayString()
tabs[str(win_id)].append((tab.title(), urlstr))
html = jinja.render('tabs.html',
title='Tabs',
tab_list_by_window=tabs)
return 'text/html', html
def history_data(start_time, offset=None):
"""Return history data.
@ -240,8 +263,6 @@ def qute_history(url):
return 'text/html', json.dumps(history_data(start_time, offset))
else:
if not config.val.content.javascript.enabled:
return 'text/plain', b'JavaScript is required for qute://history'
return 'text/html', jinja.render(
'history.html',
title='History',

View File

@ -74,14 +74,15 @@ def authentication_required(url, authenticator, abort_on):
return answer
def javascript_confirm(url, js_msg, abort_on):
def javascript_confirm(url, js_msg, abort_on, *, escape_msg=True):
"""Display a javascript confirm prompt."""
log.js.debug("confirm: {}".format(js_msg))
if config.val.content.javascript.modal_dialog:
raise CallSuper
js_msg = html.escape(js_msg) if escape_msg else js_msg
msg = 'From <b>{}</b>:<br/>{}'.format(html.escape(url.toDisplayString()),
html.escape(js_msg))
js_msg)
urlstr = url.toString(QUrl.RemovePassword | QUrl.FullyEncoded)
ans = message.ask('Javascript confirm', msg,
mode=usertypes.PromptMode.yesno,
@ -89,7 +90,7 @@ def javascript_confirm(url, js_msg, abort_on):
return bool(ans)
def javascript_prompt(url, js_msg, default, abort_on):
def javascript_prompt(url, js_msg, default, abort_on, *, escape_msg=True):
"""Display a javascript prompt."""
log.js.debug("prompt: {}".format(js_msg))
if config.val.content.javascript.modal_dialog:
@ -97,8 +98,9 @@ def javascript_prompt(url, js_msg, default, abort_on):
if not config.val.content.javascript.prompt:
return (False, "")
js_msg = html.escape(js_msg) if escape_msg else js_msg
msg = '<b>{}</b> asks:<br/>{}'.format(html.escape(url.toDisplayString()),
html.escape(js_msg))
js_msg)
urlstr = url.toString(QUrl.RemovePassword | QUrl.FullyEncoded)
answer = message.ask('Javascript prompt', msg,
mode=usertypes.PromptMode.text,
@ -111,7 +113,7 @@ def javascript_prompt(url, js_msg, default, abort_on):
return (True, answer)
def javascript_alert(url, js_msg, abort_on):
def javascript_alert(url, js_msg, abort_on, *, escape_msg=True):
"""Display a javascript alert."""
log.js.debug("alert: {}".format(js_msg))
if config.val.content.javascript.modal_dialog:
@ -120,8 +122,9 @@ def javascript_alert(url, js_msg, abort_on):
if not config.val.content.javascript.alert:
return
js_msg = html.escape(js_msg) if escape_msg else js_msg
msg = 'From <b>{}</b>:<br/>{}'.format(html.escape(url.toDisplayString()),
html.escape(js_msg))
js_msg)
urlstr = url.toString(QUrl.RemovePassword | QUrl.FullyEncoded)
message.ask('Javascript alert', msg, mode=usertypes.PromptMode.alert,
abort_on=abort_on, url=urlstr)

View File

@ -76,11 +76,11 @@ class SignalFilter(QObject):
tabbed_browser = objreg.get('tabbed-browser', scope='window',
window=self._win_id)
try:
tabidx = tabbed_browser.indexOf(tab)
tabidx = tabbed_browser.widget.indexOf(tab)
except RuntimeError:
# The tab has been deleted already
return
if tabidx == tabbed_browser.currentIndex():
if tabidx == tabbed_browser.widget.currentIndex():
if log_signal:
log.signals.debug("emitting: {} (tab {})".format(
debug.dbg_signal(signal, args), tabidx))

View File

@ -280,7 +280,7 @@ class BookmarkManager(UrlMarkManager):
if urlstr in self.marks:
if toggle:
del self.marks[urlstr]
self.delete(urlstr)
return False
else:
raise AlreadyExistsError("Bookmark already exists!")

View File

@ -41,8 +41,8 @@ Group = enum.Enum('Group', ['all', 'links', 'images', 'url', '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, '
'frame, iframe, link, summary, [onclick], [onmousedown], '
'[role=link], [role=option], [role=button], img, '
# Angular 1 selectors
'[ng-click], [ngClick], [data-ng-click], [x-ng-click]'),
Group.links: 'a[href], area[href], link[href], [role=link][href]',
@ -411,8 +411,9 @@ class AbstractWebElement(collections.abc.MutableMapping):
elif self.is_editable(strict=True):
log.webelem.debug("Clicking via JS focus()")
self._click_editable(click_target)
modeman.enter(self._tab.win_id, usertypes.KeyMode.insert,
'clicking input')
if config.val.input.insert_mode.auto_enter:
modeman.enter(self._tab.win_id, usertypes.KeyMode.insert,
'clicking input')
else:
self._click_fake_event(click_target)
elif click_target in [usertypes.ClickTarget.tab,

View File

@ -43,8 +43,8 @@ class WebEngineInspector(inspector.AbstractWebInspector):
port = int(os.environ['QTWEBENGINE_REMOTE_DEBUGGING'])
except KeyError:
raise inspector.WebInspectorError(
"Debugging is not enabled. See 'qutebrowser --help' for "
"details.")
"QtWebEngine inspector is not enabled. See "
"'qutebrowser --help' for details.")
url = QUrl('http://localhost:{}/'.format(port))
self._widget.load(url)
self.show()

View File

@ -17,9 +17,6 @@
# You should have received a copy of the GNU General Public License
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
# We get various "abstract but not overridden" warnings
# pylint: disable=abstract-method
"""Bridge from QWebEngineSettings to our own settings.
Module attributes:
@ -44,116 +41,132 @@ from qutebrowser.utils import (utils, standarddir, javascript, qtutils,
default_profile = None
# The QWebEngineProfile used for private (off-the-record) windows
private_profile = None
# The global WebEngineSettings object
global_settings = None
class Base(websettings.Base):
class _SettingsWrapper:
"""Base settings class with appropriate _get_global_settings."""
"""Expose a QWebEngineSettings interface which acts on all profiles.
def _get_global_settings(self):
return [default_profile.settings(), private_profile.settings()]
class Attribute(Base, websettings.Attribute):
"""A setting set via QWebEngineSettings::setAttribute."""
ENUM_BASE = QWebEngineSettings
class Setter(Base, websettings.Setter):
"""A setting set via a QWebEngineSettings setter method."""
pass
class FontFamilySetter(Base, websettings.FontFamilySetter):
"""A setter for a font family.
Gets the default value from QFont.
For read operations, the default profile value is always used.
"""
def __init__(self, font):
# Mapping from WebEngineSettings::initDefaults in
# qtwebengine/src/core/web_engine_settings.cpp
font_to_qfont = {
QWebEngineSettings.StandardFont: QFont.Serif,
QWebEngineSettings.FixedFont: QFont.Monospace,
QWebEngineSettings.SerifFont: QFont.Serif,
QWebEngineSettings.SansSerifFont: QFont.SansSerif,
QWebEngineSettings.CursiveFont: QFont.Cursive,
QWebEngineSettings.FantasyFont: QFont.Fantasy,
def __init__(self):
self._settings = [default_profile.settings(),
private_profile.settings()]
def setAttribute(self, *args, **kwargs):
for settings in self._settings:
settings.setAttribute(*args, **kwargs)
def setFontFamily(self, *args, **kwargs):
for settings in self._settings:
settings.setFontFamily(*args, **kwargs)
def setFontSize(self, *args, **kwargs):
for settings in self._settings:
settings.setFontSize(*args, **kwargs)
def setDefaultTextEncoding(self, *args, **kwargs):
for settings in self._settings:
settings.setDefaultTextEncoding(*args, **kwargs)
def testAttribute(self, *args, **kwargs):
return self._settings[0].testAttribute(*args, **kwargs)
def fontSize(self, *args, **kwargs):
return self._settings[0].fontSize(*args, **kwargs)
def fontFamily(self, *args, **kwargs):
return self._settings[0].fontFamily(*args, **kwargs)
def defaultTextEncoding(self, *args, **kwargs):
return self._settings[0].defaultTextEncoding(*args, **kwargs)
class WebEngineSettings(websettings.AbstractSettings):
"""A wrapper for the config for QWebEngineSettings."""
_ATTRIBUTES = {
'content.xss_auditing':
[QWebEngineSettings.XSSAuditingEnabled],
'content.images':
[QWebEngineSettings.AutoLoadImages],
'content.javascript.enabled':
[QWebEngineSettings.JavascriptEnabled],
'content.javascript.can_open_tabs_automatically':
[QWebEngineSettings.JavascriptCanOpenWindows],
'content.javascript.can_access_clipboard':
[QWebEngineSettings.JavascriptCanAccessClipboard],
'content.plugins':
[QWebEngineSettings.PluginsEnabled],
'content.hyperlink_auditing':
[QWebEngineSettings.HyperlinkAuditingEnabled],
'content.local_content_can_access_remote_urls':
[QWebEngineSettings.LocalContentCanAccessRemoteUrls],
'content.local_content_can_access_file_urls':
[QWebEngineSettings.LocalContentCanAccessFileUrls],
'content.webgl':
[QWebEngineSettings.WebGLEnabled],
'content.local_storage':
[QWebEngineSettings.LocalStorageEnabled],
'input.spatial_navigation':
[QWebEngineSettings.SpatialNavigationEnabled],
'input.links_included_in_focus_chain':
[QWebEngineSettings.LinksIncludedInFocusChain],
'scrolling.smooth':
[QWebEngineSettings.ScrollAnimatorEnabled],
}
_FONT_SIZES = {
'fonts.web.size.minimum':
QWebEngineSettings.MinimumFontSize,
'fonts.web.size.minimum_logical':
QWebEngineSettings.MinimumLogicalFontSize,
'fonts.web.size.default':
QWebEngineSettings.DefaultFontSize,
'fonts.web.size.default_fixed':
QWebEngineSettings.DefaultFixedFontSize,
}
_FONT_FAMILIES = {
'fonts.web.family.standard': QWebEngineSettings.StandardFont,
'fonts.web.family.fixed': QWebEngineSettings.FixedFont,
'fonts.web.family.serif': QWebEngineSettings.SerifFont,
'fonts.web.family.sans_serif': QWebEngineSettings.SansSerifFont,
'fonts.web.family.cursive': QWebEngineSettings.CursiveFont,
'fonts.web.family.fantasy': QWebEngineSettings.FantasyFont,
}
# Mapping from WebEngineSettings::initDefaults in
# qtwebengine/src/core/web_engine_settings.cpp
_FONT_TO_QFONT = {
QWebEngineSettings.StandardFont: QFont.Serif,
QWebEngineSettings.FixedFont: QFont.Monospace,
QWebEngineSettings.SerifFont: QFont.Serif,
QWebEngineSettings.SansSerifFont: QFont.SansSerif,
QWebEngineSettings.CursiveFont: QFont.Cursive,
QWebEngineSettings.FantasyFont: QFont.Fantasy,
}
def __init__(self, settings):
super().__init__(settings)
# Attributes which don't exist in all Qt versions.
new_attributes = {
# Qt 5.8
'content.print_element_backgrounds': 'PrintElementBackgrounds',
}
super().__init__(setter=QWebEngineSettings.setFontFamily, font=font,
qfont=font_to_qfont[font])
for name, attribute in new_attributes.items():
try:
value = getattr(QWebEngineSettings, attribute)
except AttributeError:
continue
class DefaultProfileSetter(websettings.Base):
"""A setting set on the QWebEngineProfile."""
def __init__(self, setter, converter=None, default=websettings.UNSET):
super().__init__(default)
self._setter = setter
self._converter = converter
def __repr__(self):
return utils.get_repr(self, setter=self._setter, constructor=True)
def _set(self, value, settings=None):
if settings is not None:
raise ValueError("'settings' may not be set with "
"DefaultProfileSetters!")
setter = getattr(default_profile, self._setter)
if self._converter is not None:
value = self._converter(value)
setter(value)
class PersistentCookiePolicy(DefaultProfileSetter):
"""The content.cookies.store setting is different from other settings."""
def __init__(self):
super().__init__('setPersistentCookiesPolicy')
def _set(self, value, settings=None):
if settings is not None:
raise ValueError("'settings' may not be set with "
"PersistentCookiePolicy!")
setter = getattr(QWebEngineProfile.defaultProfile(), self._setter)
setter(
QWebEngineProfile.AllowPersistentCookies if value else
QWebEngineProfile.NoPersistentCookies
)
class DictionaryLanguageSetter(DefaultProfileSetter):
"""Sets paths to dictionary files based on language codes."""
def __init__(self):
super().__init__('setSpellCheckLanguages', default=[])
def _find_installed(self, code):
local_filename = spell.local_filename(code)
if not local_filename:
message.warning(
"Language {} is not installed - see scripts/dictcli.py "
"in qutebrowser's sources".format(code))
return local_filename
def _set(self, value, settings=None):
if settings is not None:
raise ValueError("'settings' may not be set with "
"DictionaryLanguageSetter!")
filenames = [self._find_installed(code) for code in value]
log.config.debug("Found dicts: {}".format(filenames))
super()._set([f for f in filenames if f], settings)
self._ATTRIBUTES[name] = [value]
def _init_stylesheet(profile):
@ -210,9 +223,48 @@ def _set_http_headers(profile):
profile.setHttpAcceptLanguage(accept_language)
def _set_http_cache_size(profile):
"""Initialize the HTTP cache size for the given profile."""
size = config.val.content.cache.size
if size is None:
size = 0
else:
size = qtutils.check_overflow(size, 'int', fatal=False)
# 0: automatically managed by QtWebEngine
profile.setHttpCacheMaximumSize(size)
def _set_persistent_cookie_policy(profile):
"""Set the HTTP Cookie size for the given profile."""
if config.val.content.cookies.store:
value = QWebEngineProfile.AllowPersistentCookies
else:
value = QWebEngineProfile.NoPersistentCookies
profile.setPersistentCookiesPolicy(value)
def _set_dictionary_language(profile, warn=True):
filenames = []
for code in config.val.spellcheck.languages or []:
local_filename = spell.local_filename(code)
if not local_filename:
if warn:
message.warning(
"Language {} is not installed - see scripts/dictcli.py "
"in qutebrowser's sources".format(code))
continue
filenames.append(local_filename)
log.config.debug("Found dicts: {}".format(filenames))
profile.setSpellCheckLanguages(filenames)
def _update_settings(option):
"""Update global settings when qwebsettings changed."""
websettings.update_mappings(MAPPINGS, option)
global_settings.update_setting(option)
if option in ['scrolling.bar', 'content.user_stylesheets']:
_init_stylesheet(default_profile)
_init_stylesheet(private_profile)
@ -221,27 +273,46 @@ def _update_settings(option):
'content.headers.accept_language']:
_set_http_headers(default_profile)
_set_http_headers(private_profile)
elif option == 'content.cache.size':
_set_http_cache_size(default_profile)
_set_http_cache_size(private_profile)
elif (option == 'content.cookies.store' and
# https://bugreports.qt.io/browse/QTBUG-58650
qtutils.version_check('5.9', compiled=False)):
_set_persistent_cookie_policy(default_profile)
# We're not touching the private profile's cookie policy.
elif option == 'spellcheck.languages':
_set_dictionary_language(default_profile)
_set_dictionary_language(private_profile, warn=False)
def _init_profile(profile):
"""Init the given profile."""
_init_stylesheet(profile)
_set_http_headers(profile)
_set_http_cache_size(profile)
profile.settings().setAttribute(
QWebEngineSettings.FullScreenSupportEnabled, True)
if qtutils.version_check('5.8'):
profile.setSpellCheckEnabled(True)
_set_dictionary_language(profile)
def _init_profiles():
"""Init the two used QWebEngineProfiles."""
global default_profile, private_profile
default_profile = QWebEngineProfile.defaultProfile()
default_profile.setCachePath(
os.path.join(standarddir.cache(), 'webengine'))
default_profile.setPersistentStoragePath(
os.path.join(standarddir.data(), 'webengine'))
_init_stylesheet(default_profile)
_set_http_headers(default_profile)
_init_profile(default_profile)
_set_persistent_cookie_policy(default_profile)
private_profile = QWebEngineProfile()
assert private_profile.isOffTheRecord()
_init_stylesheet(private_profile)
_set_http_headers(private_profile)
if qtutils.version_check('5.8'):
default_profile.setSpellCheckEnabled(True)
private_profile.setSpellCheckEnabled(True)
_init_profile(private_profile)
def inject_userscripts():
@ -287,111 +358,12 @@ def init(args):
os.environ['QTWEBENGINE_REMOTE_DEBUGGING'] = str(utils.random_port())
_init_profiles()
# We need to do this here as a WORKAROUND for
# https://bugreports.qt.io/browse/QTBUG-58650
if not qtutils.version_check('5.9', compiled=False):
PersistentCookiePolicy().set(config.val.content.cookies.store)
Attribute(QWebEngineSettings.FullScreenSupportEnabled).set(True)
websettings.init_mappings(MAPPINGS)
config.instance.changed.connect(_update_settings)
global global_settings
global_settings = WebEngineSettings(_SettingsWrapper())
global_settings.init_settings()
def shutdown():
# FIXME:qtwebengine do we need to do something for a clean shutdown here?
pass
# Missing QtWebEngine attributes:
# - ScreenCaptureEnabled
# - Accelerated2dCanvasEnabled
# - AutoLoadIconsForPage
# - TouchIconsEnabled
# - FocusOnNavigationEnabled (5.8)
# - AllowRunningInsecureContent (5.8)
#
# Missing QtWebEngine fonts:
# - PictographFont
MAPPINGS = {
'content.images':
Attribute(QWebEngineSettings.AutoLoadImages),
'content.javascript.enabled':
Attribute(QWebEngineSettings.JavascriptEnabled),
'content.javascript.can_open_tabs_automatically':
Attribute(QWebEngineSettings.JavascriptCanOpenWindows),
'content.javascript.can_access_clipboard':
Attribute(QWebEngineSettings.JavascriptCanAccessClipboard),
'content.plugins':
Attribute(QWebEngineSettings.PluginsEnabled),
'content.hyperlink_auditing':
Attribute(QWebEngineSettings.HyperlinkAuditingEnabled),
'content.local_content_can_access_remote_urls':
Attribute(QWebEngineSettings.LocalContentCanAccessRemoteUrls),
'content.local_content_can_access_file_urls':
Attribute(QWebEngineSettings.LocalContentCanAccessFileUrls),
'content.webgl':
Attribute(QWebEngineSettings.WebGLEnabled),
'content.local_storage':
Attribute(QWebEngineSettings.LocalStorageEnabled),
'content.cache.size':
# 0: automatically managed by QtWebEngine
DefaultProfileSetter('setHttpCacheMaximumSize', default=0,
converter=lambda val:
qtutils.check_overflow(val, 'int', fatal=False)),
'content.xss_auditing':
Attribute(QWebEngineSettings.XSSAuditingEnabled),
'content.default_encoding':
Setter(QWebEngineSettings.setDefaultTextEncoding),
'input.spatial_navigation':
Attribute(QWebEngineSettings.SpatialNavigationEnabled),
'input.links_included_in_focus_chain':
Attribute(QWebEngineSettings.LinksIncludedInFocusChain),
'fonts.web.family.standard':
FontFamilySetter(QWebEngineSettings.StandardFont),
'fonts.web.family.fixed':
FontFamilySetter(QWebEngineSettings.FixedFont),
'fonts.web.family.serif':
FontFamilySetter(QWebEngineSettings.SerifFont),
'fonts.web.family.sans_serif':
FontFamilySetter(QWebEngineSettings.SansSerifFont),
'fonts.web.family.cursive':
FontFamilySetter(QWebEngineSettings.CursiveFont),
'fonts.web.family.fantasy':
FontFamilySetter(QWebEngineSettings.FantasyFont),
'fonts.web.size.minimum':
Setter(QWebEngineSettings.setFontSize,
args=[QWebEngineSettings.MinimumFontSize]),
'fonts.web.size.minimum_logical':
Setter(QWebEngineSettings.setFontSize,
args=[QWebEngineSettings.MinimumLogicalFontSize]),
'fonts.web.size.default':
Setter(QWebEngineSettings.setFontSize,
args=[QWebEngineSettings.DefaultFontSize]),
'fonts.web.size.default_fixed':
Setter(QWebEngineSettings.setFontSize,
args=[QWebEngineSettings.DefaultFixedFontSize]),
'scrolling.smooth':
Attribute(QWebEngineSettings.ScrollAnimatorEnabled),
}
try:
MAPPINGS['content.print_element_backgrounds'] = Attribute(
QWebEngineSettings.PrintElementBackgrounds)
except AttributeError:
# Added in Qt 5.8
pass
if qtutils.version_check('5.8'):
MAPPINGS['spellcheck.languages'] = DictionaryLanguageSetter()
if qtutils.version_check('5.9', compiled=False):
# https://bugreports.qt.io/browse/QTBUG-58650
MAPPINGS['content.cookies.store'] = PersistentCookiePolicy()

View File

@ -22,16 +22,18 @@
import math
import functools
import sys
import re
import html as html_utils
import sip
from PyQt5.QtCore import (pyqtSignal, pyqtSlot, Qt, QEvent, QPoint, QPointF,
QUrl)
from PyQt5.QtGui import QKeyEvent
QUrl, QTimer)
from PyQt5.QtGui import QKeyEvent, QIcon
from PyQt5.QtNetwork import QAuthenticator
from PyQt5.QtWidgets import QApplication
from PyQt5.QtWebEngineWidgets import QWebEnginePage, QWebEngineScript
from qutebrowser.config import configdata
from qutebrowser.browser import browsertab, mouse, shared
from qutebrowser.browser.webengine import (webview, webengineelem, tabhistory,
interceptor, webenginequtescheme,
@ -183,6 +185,12 @@ class WebEngineSearch(browsertab.AbstractSearch):
def search(self, text, *, ignore_case='never', reverse=False,
result_cb=None):
# Don't go to next entry on duplicate search
if self.text == text and self.search_displayed:
log.webview.debug("Ignoring duplicate search request"
" for {}".format(text))
return
self.text = text
self._flags = QWebEnginePage.FindFlags(0)
if self._is_case_sensitive(ignore_case):
@ -218,12 +226,21 @@ class WebEngineCaret(browsertab.AbstractCaret):
if mode != usertypes.KeyMode.caret:
return
if self._tab.search.search_displayed:
# We are currently in search mode.
# convert the search to a blue selection so we can operate on it
# https://bugreports.qt.io/browse/QTBUG-60673
self._tab.search.clear()
self._tab.run_js_async(
javascript.assemble('caret', 'setPlatform', sys.platform))
self._js_call('setInitialCursor')
@pyqtSlot(usertypes.KeyMode)
def _on_mode_left(self):
def _on_mode_left(self, mode):
if mode != usertypes.KeyMode.caret:
return
self.drop_selection()
self._js_call('disableCaret')
@ -470,7 +487,8 @@ class WebEngineHistory(browsertab.AbstractHistory):
return self._history.itemAt(i)
def _go_to_item(self, item):
return self._history.goToItem(item)
self._tab.predicted_navigation.emit(item.url())
self._history.goToItem(item)
def serialize(self):
if not qtutils.version_check('5.9', compiled=False):
@ -488,6 +506,9 @@ class WebEngineHistory(browsertab.AbstractHistory):
return qtutils.deserialize(data, self._history)
def load_items(self, items):
if items:
self._tab.predicted_navigation.emit(items[-1].url)
stream, _data, cur_data = tabhistory.serialize(items)
qtutils.deserialize_stream(stream, self._history)
@ -604,12 +625,15 @@ class WebEngineTab(browsertab.AbstractTab):
self.printing = WebEnginePrinting()
self.elements = WebEngineElements(tab=self)
self.action = WebEngineAction(tab=self)
# We're assigning settings in _set_widget
self.settings = webenginesettings.WebEngineSettings(settings=None)
self._set_widget(widget)
self._connect_signals()
self.backend = usertypes.Backend.QtWebEngine
self._init_js()
self._child_event_filter = None
self._saved_zoom = None
self._reload_url = None
def _init_js(self):
js_code = '\n'.join([
@ -648,9 +672,15 @@ class WebEngineTab(browsertab.AbstractTab):
self.zoom.set_factor(self._saved_zoom)
self._saved_zoom = None
def openurl(self, url):
def openurl(self, url, *, predict=True):
"""Open the given URL in this tab.
Arguments:
url: The QUrl to open.
predict: If set to False, predicted_navigation is not emitted.
"""
self._saved_zoom = self.zoom.factor()
self._openurl_prepare(url)
self._openurl_prepare(url, predict=predict)
self._widget.load(url)
def url(self, requested=False):
@ -682,10 +712,6 @@ class WebEngineTab(browsertab.AbstractTab):
def shutdown(self):
self.shutting_down.emit()
self.action.exit_fullscreen()
if qtutils.version_check('5.8', exact=True, compiled=False):
# WORKAROUND for
# https://bugreports.qt.io/browse/QTBUG-58563
self.search.clear()
self._widget.shutdown()
def reload(self, *, force=False):
@ -728,6 +754,16 @@ class WebEngineTab(browsertab.AbstractTab):
self.send_event(press_evt)
self.send_event(release_evt)
def _show_error_page(self, url, error):
"""Show an error page in the tab."""
log.misc.debug("Showing error page for {}".format(error))
url_string = url.toDisplayString()
error_page = jinja.render(
'error.html',
title="Error loading page: {}".format(url_string),
url=url_string, error=error)
self.set_html(error_page)
@pyqtSlot()
def _on_history_trigger(self):
try:
@ -776,13 +812,7 @@ class WebEngineTab(browsertab.AbstractTab):
sip.assign(authenticator, QAuthenticator())
# pylint: enable=no-member, useless-suppression
except AttributeError:
url_string = url.toDisplayString()
error_page = jinja.render(
'error.html',
title="Error loading page: {}".format(url_string),
url=url_string, error="Proxy authentication required",
icon='')
self.set_html(error_page)
self._show_error_page(url, "Proxy authentication required")
@pyqtSlot(QUrl, 'QAuthenticator*')
def _on_authentication_required(self, url, authenticator):
@ -802,12 +832,7 @@ class WebEngineTab(browsertab.AbstractTab):
except AttributeError:
# WORKAROUND for
# https://www.riverbankcomputing.com/pipermail/pyqt/2016-December/038400.html
url_string = url.toDisplayString()
error_page = jinja.render(
'error.html',
title="Error loading page: {}".format(url_string),
url=url_string, error="Authentication required")
self.set_html(error_page)
self._show_error_page(url, "Authentication required")
@pyqtSlot('QWebEngineFullScreenRequest')
def _on_fullscreen_requested(self, request):
@ -872,6 +897,74 @@ class WebEngineTab(browsertab.AbstractTab):
if not ok:
self._load_finished_fake.emit(False)
def _error_page_workaround(self, html):
"""Check if we're displaying a Chromium error page.
This gets only called if we got loadFinished(False) without JavaScript,
so we can display at least some error page.
WORKAROUND for https://bugreports.qt.io/browse/QTBUG-66643
Needs to check the page content as a WORKAROUND for
https://bugreports.qt.io/browse/QTBUG-66661
"""
match = re.search(r'"errorCode":"([^"]*)"', html)
if match is None:
return
self._show_error_page(self.url(), error=match.group(1))
@pyqtSlot(bool)
def _on_load_finished(self, ok):
"""Display a static error page if JavaScript is disabled."""
super()._on_load_finished(ok)
js_enabled = self.settings.test_attribute('content.javascript.enabled')
if not ok and not js_enabled:
self.dump_async(self._error_page_workaround)
if ok and self._reload_url is not None:
# WORKAROUND for https://bugreports.qt.io/browse/QTBUG-66656
log.config.debug(
"Loading {} again because of config change".format(
self._reload_url.toDisplayString()))
QTimer.singleShot(100, lambda url=self._reload_url:
self.openurl(url, predict=False))
self._reload_url = None
if not qtutils.version_check('5.10', compiled=False):
# We can't do this when we have the loadFinished workaround as that
# sometimes clears icons without loading a new page.
# In general, this is handled by Qt, but when loading takes long,
# the old icon is still displayed.
self.icon_changed.emit(QIcon())
@pyqtSlot(QUrl)
def _on_predicted_navigation(self, url):
"""If we know we're going to visit an URL soon, change the settings."""
super()._on_predicted_navigation(url)
self.settings.update_for_url(url)
@pyqtSlot(usertypes.NavigationRequest)
def _on_navigation_request(self, navigation):
super()._on_navigation_request(navigation)
if not navigation.accepted or not navigation.is_main_frame:
return
needs_reload = {
'content.plugins',
'content.javascript.enabled',
'content.javascript.can_access_clipboard',
'content.javascript.can_access_clipboard',
'content.print_element_backgrounds',
'input.spatial_navigation',
'input.spatial_navigation',
}
assert needs_reload.issubset(configdata.DATA)
changed = self.settings.update_for_url(navigation.url)
if (changed & needs_reload and navigation.navigation_type !=
navigation.Type.link_clicked):
# WORKAROUND for https://bugreports.qt.io/browse/QTBUG-66656
self._reload_url = navigation.url
def _connect_signals(self):
view = self._widget
page = view.page()
@ -886,6 +979,7 @@ class WebEngineTab(browsertab.AbstractTab):
self._on_proxy_authentication_required)
page.fullScreenRequested.connect(self._on_fullscreen_requested)
page.contentsSizeChanged.connect(self.contents_size_changed)
page.navigation_request.connect(self._on_navigation_request)
view.titleChanged.connect(self.title_changed)
view.urlChanged.connect(self._on_url_changed)
@ -906,5 +1000,7 @@ class WebEngineTab(browsertab.AbstractTab):
page.loadFinished.connect(self._restore_zoom)
page.loadFinished.connect(self._on_load_finished)
self.predicted_navigation.connect(self._on_predicted_navigation)
def event_target(self):
return self._widget.focusProxy()

View File

@ -29,8 +29,7 @@ from PyQt5.QtWebEngineWidgets import (QWebEngineView, QWebEnginePage,
from qutebrowser.browser import shared
from qutebrowser.browser.webengine import certificateerror, webenginesettings
from qutebrowser.config import config
from qutebrowser.utils import (log, debug, usertypes, jinja, urlutils, message,
objreg, qtutils)
from qutebrowser.utils import log, debug, usertypes, jinja, objreg, qtutils
class WebEngineView(QWebEngineView):
@ -124,10 +123,12 @@ class WebEnginePage(QWebEnginePage):
Signals:
certificate_error: Emitted on certificate errors.
shutting_down: Emitted when the page is shutting down.
navigation_request: Emitted on acceptNavigationRequest.
"""
certificate_error = pyqtSignal()
shutting_down = pyqtSignal()
navigation_request = pyqtSignal(usertypes.NavigationRequest)
def __init__(self, *, theme_color, profile, parent=None):
super().__init__(profile, parent)
@ -242,10 +243,12 @@ class WebEnginePage(QWebEnginePage):
"""Override javaScriptConfirm to use qutebrowser prompts."""
if self._is_shutting_down:
return False
escape_msg = qtutils.version_check('5.11', compiled=False)
try:
return shared.javascript_confirm(url, js_msg,
abort_on=[self.loadStarted,
self.shutting_down])
self.shutting_down],
escape_msg=escape_msg)
except shared.CallSuper:
return super().javaScriptConfirm(url, js_msg)
@ -255,12 +258,14 @@ class WebEnginePage(QWebEnginePage):
# https://www.riverbankcomputing.com/pipermail/pyqt/2016-November/038293.html
def javaScriptPrompt(self, url, js_msg, default):
"""Override javaScriptPrompt to use qutebrowser prompts."""
escape_msg = qtutils.version_check('5.11', compiled=False)
if self._is_shutting_down:
return (False, "")
try:
return shared.javascript_prompt(url, js_msg, default,
abort_on=[self.loadStarted,
self.shutting_down])
self.shutting_down],
escape_msg=escape_msg)
except shared.CallSuper:
return super().javaScriptPrompt(url, js_msg, default)
@ -268,10 +273,12 @@ class WebEnginePage(QWebEnginePage):
"""Override javaScriptAlert to use qutebrowser prompts."""
if self._is_shutting_down:
return
escape_msg = qtutils.version_check('5.11', compiled=False)
try:
shared.javascript_alert(url, js_msg,
abort_on=[self.loadStarted,
self.shutting_down])
self.shutting_down],
escape_msg=escape_msg)
except shared.CallSuper:
super().javaScriptAlert(url, js_msg)
@ -288,21 +295,26 @@ class WebEnginePage(QWebEnginePage):
url: QUrl,
typ: QWebEnginePage.NavigationType,
is_main_frame: bool):
"""Override acceptNavigationRequest to handle clicked links.
This only show an error on invalid links - everything else is handled
in createWindow.
"""
log.webview.debug("navigation request: url {}, type {}, is_main_frame "
"{}".format(url.toDisplayString(),
debug.qenum_key(QWebEnginePage, typ),
is_main_frame))
if (typ == QWebEnginePage.NavigationTypeLinkClicked and
not url.isValid()):
msg = urlutils.get_errstring(url, "Invalid link clicked")
message.error(msg)
return False
return True
"""Override acceptNavigationRequest to forward it to the tab API."""
type_map = {
QWebEnginePage.NavigationTypeLinkClicked:
usertypes.NavigationRequest.Type.link_clicked,
QWebEnginePage.NavigationTypeTyped:
usertypes.NavigationRequest.Type.typed,
QWebEnginePage.NavigationTypeFormSubmitted:
usertypes.NavigationRequest.Type.form_submitted,
QWebEnginePage.NavigationTypeBackForward:
usertypes.NavigationRequest.Type.back_forward,
QWebEnginePage.NavigationTypeReload:
usertypes.NavigationRequest.Type.reloaded,
QWebEnginePage.NavigationTypeOther:
usertypes.NavigationRequest.Type.other,
}
navigation = usertypes.NavigationRequest(url=url,
navigation_type=type_map[typ],
is_main_frame=is_main_frame)
self.navigation_request.emit(navigation)
return navigation.accepted
@pyqtSlot('QUrl')
def _inject_userjs(self, url):

View File

@ -17,9 +17,6 @@
# You should have received a copy of the GNU General Public License
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
# We get various "abstract but not overridden" warnings
# pylint: disable=abstract-method
"""Bridge from QWebSettings to our own settings.
Module attributes:
@ -37,85 +34,130 @@ from qutebrowser.utils import standarddir, urlutils
from qutebrowser.browser import shared
class Base(websettings.Base):
"""Base settings class with appropriate _get_global_settings."""
def _get_global_settings(self):
return [QWebSettings.globalSettings()]
# The global WebKitSettings object
global_settings = None
class Attribute(Base, websettings.Attribute):
class WebKitSettings(websettings.AbstractSettings):
"""A setting set via QWebSettings::setAttribute."""
"""A wrapper for the config for QWebSettings."""
ENUM_BASE = QWebSettings
_ATTRIBUTES = {
'content.images':
[QWebSettings.AutoLoadImages],
'content.javascript.enabled':
[QWebSettings.JavascriptEnabled],
'content.javascript.can_open_tabs_automatically':
[QWebSettings.JavascriptCanOpenWindows],
'content.javascript.can_close_tabs':
[QWebSettings.JavascriptCanCloseWindows],
'content.javascript.can_access_clipboard':
[QWebSettings.JavascriptCanAccessClipboard],
'content.plugins':
[QWebSettings.PluginsEnabled],
'content.webgl':
[QWebSettings.WebGLEnabled],
'content.hyperlink_auditing':
[QWebSettings.HyperlinkAuditingEnabled],
'content.local_content_can_access_remote_urls':
[QWebSettings.LocalContentCanAccessRemoteUrls],
'content.local_content_can_access_file_urls':
[QWebSettings.LocalContentCanAccessFileUrls],
'content.dns_prefetch':
[QWebSettings.DnsPrefetchEnabled],
'content.frame_flattening':
[QWebSettings.FrameFlatteningEnabled],
'content.cache.appcache':
[QWebSettings.OfflineWebApplicationCacheEnabled],
'content.local_storage':
[QWebSettings.LocalStorageEnabled,
QWebSettings.OfflineStorageDatabaseEnabled],
'content.developer_extras':
[QWebSettings.DeveloperExtrasEnabled],
'content.print_element_backgrounds':
[QWebSettings.PrintElementBackgrounds],
'content.xss_auditing':
[QWebSettings.XSSAuditingEnabled],
'input.spatial_navigation':
[QWebSettings.SpatialNavigationEnabled],
'input.links_included_in_focus_chain':
[QWebSettings.LinksIncludedInFocusChain],
'zoom.text_only':
[QWebSettings.ZoomTextOnly],
'scrolling.smooth':
[QWebSettings.ScrollAnimatorEnabled],
}
_FONT_SIZES = {
'fonts.web.size.minimum':
QWebSettings.MinimumFontSize,
'fonts.web.size.minimum_logical':
QWebSettings.MinimumLogicalFontSize,
'fonts.web.size.default':
QWebSettings.DefaultFontSize,
'fonts.web.size.default_fixed':
QWebSettings.DefaultFixedFontSize,
}
_FONT_FAMILIES = {
'fonts.web.family.standard': QWebSettings.StandardFont,
'fonts.web.family.fixed': QWebSettings.FixedFont,
'fonts.web.family.serif': QWebSettings.SerifFont,
'fonts.web.family.sans_serif': QWebSettings.SansSerifFont,
'fonts.web.family.cursive': QWebSettings.CursiveFont,
'fonts.web.family.fantasy': QWebSettings.FantasyFont,
}
# Mapping from QWebSettings::QWebSettings() in
# qtwebkit/Source/WebKit/qt/Api/qwebsettings.cpp
_FONT_TO_QFONT = {
QWebSettings.StandardFont: QFont.Serif,
QWebSettings.FixedFont: QFont.Monospace,
QWebSettings.SerifFont: QFont.Serif,
QWebSettings.SansSerifFont: QFont.SansSerif,
QWebSettings.CursiveFont: QFont.Cursive,
QWebSettings.FantasyFont: QFont.Fantasy,
}
class Setter(Base, websettings.Setter):
"""A setting set via a QWebSettings setter method."""
pass
def _set_user_stylesheet(settings):
"""Set the generated user-stylesheet."""
stylesheet = shared.get_user_stylesheet().encode('utf-8')
url = urlutils.data_url('text/css;charset=utf-8', stylesheet)
settings.setUserStyleSheetUrl(url)
class StaticSetter(Base, websettings.StaticSetter):
"""A setting set via a static QWebSettings setter method."""
pass
class FontFamilySetter(Base, websettings.FontFamilySetter):
"""A setter for a font family.
Gets the default value from QFont.
"""
def __init__(self, font):
# Mapping from QWebSettings::QWebSettings() in
# qtwebkit/Source/WebKit/qt/Api/qwebsettings.cpp
font_to_qfont = {
QWebSettings.StandardFont: QFont.Serif,
QWebSettings.FixedFont: QFont.Monospace,
QWebSettings.SerifFont: QFont.Serif,
QWebSettings.SansSerifFont: QFont.SansSerif,
QWebSettings.CursiveFont: QFont.Cursive,
QWebSettings.FantasyFont: QFont.Fantasy,
}
super().__init__(setter=QWebSettings.setFontFamily, font=font,
qfont=font_to_qfont[font])
class CookiePolicy(Base):
"""The ThirdPartyCookiePolicy setting is different from other settings."""
MAPPING = {
def _set_cookie_accept_policy(settings):
"""Update the content.cookies.accept setting."""
mapping = {
'all': QWebSettings.AlwaysAllowThirdPartyCookies,
'no-3rdparty': QWebSettings.AlwaysBlockThirdPartyCookies,
'never': QWebSettings.AlwaysBlockThirdPartyCookies,
'no-unknown-3rdparty': QWebSettings.AllowThirdPartyWithExistingCookies,
}
def _set(self, value, settings=None):
for obj in self._get_settings(settings):
obj.setThirdPartyCookiePolicy(self.MAPPING[value])
value = config.val.content.cookies.accept
settings.setThirdPartyCookiePolicy(mapping[value])
def _set_user_stylesheet():
"""Set the generated user-stylesheet."""
stylesheet = shared.get_user_stylesheet().encode('utf-8')
url = urlutils.data_url('text/css;charset=utf-8', stylesheet)
QWebSettings.globalSettings().setUserStyleSheetUrl(url)
def _set_cache_maximum_pages(settings):
"""Update the content.cache.maximum_pages setting."""
value = config.val.content.cache.maximum_pages
settings.setMaximumPagesInCache(value)
def _update_settings(option):
"""Update global settings when qwebsettings changed."""
global_settings.update_setting(option)
settings = QWebSettings.globalSettings()
if option in ['scrollbar.hide', 'content.user_stylesheets']:
_set_user_stylesheet()
websettings.update_mappings(MAPPINGS, option)
_set_user_stylesheet(settings)
elif option == 'content.cookies.accept':
_set_cookie_accept_policy(settings)
elif option == 'content.cache.maximum_pages':
_set_cache_maximum_pages(settings)
def init(_args):
@ -131,92 +173,20 @@ def init(_args):
QWebSettings.setOfflineStoragePath(
os.path.join(data_path, 'offline-storage'))
websettings.init_mappings(MAPPINGS)
_set_user_stylesheet()
settings = QWebSettings.globalSettings()
_set_user_stylesheet(settings)
_set_cookie_accept_policy(settings)
_set_cache_maximum_pages(settings)
config.instance.changed.connect(_update_settings)
global global_settings
global_settings = WebKitSettings(QWebSettings.globalSettings())
global_settings.init_settings()
def shutdown():
"""Disable storage so removing tmpdir will work."""
QWebSettings.setIconDatabasePath('')
QWebSettings.setOfflineWebApplicationCachePath('')
QWebSettings.globalSettings().setLocalStoragePath('')
MAPPINGS = {
'content.images':
Attribute(QWebSettings.AutoLoadImages),
'content.javascript.enabled':
Attribute(QWebSettings.JavascriptEnabled),
'content.javascript.can_open_tabs_automatically':
Attribute(QWebSettings.JavascriptCanOpenWindows),
'content.javascript.can_close_tabs':
Attribute(QWebSettings.JavascriptCanCloseWindows),
'content.javascript.can_access_clipboard':
Attribute(QWebSettings.JavascriptCanAccessClipboard),
'content.plugins':
Attribute(QWebSettings.PluginsEnabled),
'content.webgl':
Attribute(QWebSettings.WebGLEnabled),
'content.hyperlink_auditing':
Attribute(QWebSettings.HyperlinkAuditingEnabled),
'content.local_content_can_access_remote_urls':
Attribute(QWebSettings.LocalContentCanAccessRemoteUrls),
'content.local_content_can_access_file_urls':
Attribute(QWebSettings.LocalContentCanAccessFileUrls),
'content.cookies.accept':
CookiePolicy(),
'content.dns_prefetch':
Attribute(QWebSettings.DnsPrefetchEnabled),
'content.frame_flattening':
Attribute(QWebSettings.FrameFlatteningEnabled),
'content.cache.appcache':
Attribute(QWebSettings.OfflineWebApplicationCacheEnabled),
'content.local_storage':
Attribute(QWebSettings.LocalStorageEnabled,
QWebSettings.OfflineStorageDatabaseEnabled),
'content.cache.maximum_pages':
StaticSetter(QWebSettings.setMaximumPagesInCache),
'content.developer_extras':
Attribute(QWebSettings.DeveloperExtrasEnabled),
'content.print_element_backgrounds':
Attribute(QWebSettings.PrintElementBackgrounds),
'content.xss_auditing':
Attribute(QWebSettings.XSSAuditingEnabled),
'content.default_encoding':
Setter(QWebSettings.setDefaultTextEncoding),
# content.user_stylesheets is handled separately
'input.spatial_navigation':
Attribute(QWebSettings.SpatialNavigationEnabled),
'input.links_included_in_focus_chain':
Attribute(QWebSettings.LinksIncludedInFocusChain),
'fonts.web.family.standard':
FontFamilySetter(QWebSettings.StandardFont),
'fonts.web.family.fixed':
FontFamilySetter(QWebSettings.FixedFont),
'fonts.web.family.serif':
FontFamilySetter(QWebSettings.SerifFont),
'fonts.web.family.sans_serif':
FontFamilySetter(QWebSettings.SansSerifFont),
'fonts.web.family.cursive':
FontFamilySetter(QWebSettings.CursiveFont),
'fonts.web.family.fantasy':
FontFamilySetter(QWebSettings.FantasyFont),
'fonts.web.size.minimum':
Setter(QWebSettings.setFontSize, args=[QWebSettings.MinimumFontSize]),
'fonts.web.size.minimum_logical':
Setter(QWebSettings.setFontSize,
args=[QWebSettings.MinimumLogicalFontSize]),
'fonts.web.size.default':
Setter(QWebSettings.setFontSize, args=[QWebSettings.DefaultFontSize]),
'fonts.web.size.default_fixed':
Setter(QWebSettings.setFontSize,
args=[QWebSettings.DefaultFixedFontSize]),
'zoom.text_only':
Attribute(QWebSettings.ZoomTextOnly),
'scrolling.smooth':
Attribute(QWebSettings.ScrollAnimatorEnabled),
}

View File

@ -30,13 +30,14 @@ import pygments.formatters
import sip
from PyQt5.QtCore import (pyqtSlot, Qt, QEvent, QUrl, QPoint, QTimer, QSizeF,
QSize)
from PyQt5.QtGui import QKeyEvent
from PyQt5.QtGui import QKeyEvent, QIcon
from PyQt5.QtWebKitWidgets import QWebPage, QWebFrame
from PyQt5.QtWebKit import QWebSettings
from PyQt5.QtPrintSupport import QPrinter
from qutebrowser.browser import browsertab
from qutebrowser.browser.webkit import webview, tabhistory, webkitelem
from qutebrowser.browser import browsertab, shared
from qutebrowser.browser.webkit import (webview, tabhistory, webkitelem,
webkitsettings)
from qutebrowser.utils import qtutils, objreg, usertypes, utils, log, debug
@ -146,8 +147,17 @@ class WebKitSearch(browsertab.AbstractSearch):
def search(self, text, *, ignore_case='never', reverse=False,
result_cb=None):
self.search_displayed = True
# Don't go to next entry on duplicate search
if self.text == text and self.search_displayed:
log.webview.debug("Ignoring duplicate search request"
" for {}".format(text))
return
# Clear old search results, this is done automatically on QtWebEngine.
self.clear()
self.text = text
self.search_displayed = True
self._flags = QWebPage.FindWrapsAroundDocument
if self._is_case_sensitive(ignore_case):
self._flags |= QWebPage.FindCaseSensitively
@ -205,8 +215,8 @@ class WebKitCaret(browsertab.AbstractCaret):
self._widget.page().currentFrame().evaluateJavaScript(
utils.read_file('javascript/position_caret.js'))
@pyqtSlot()
def _on_mode_left(self):
@pyqtSlot(usertypes.KeyMode)
def _on_mode_left(self, _mode):
settings = self._widget.settings()
if settings.testAttribute(QWebSettings.CaretBrowsingEnabled):
if self.selection_enabled and self._widget.hasSelection():
@ -517,7 +527,8 @@ class WebKitHistory(browsertab.AbstractHistory):
return self._history.itemAt(i)
def _go_to_item(self, item):
return self._history.goToItem(item)
self._tab.predicted_navigation.emit(item.url())
self._history.goToItem(item)
def serialize(self):
return qtutils.serialize(self._history)
@ -526,6 +537,9 @@ class WebKitHistory(browsertab.AbstractHistory):
return qtutils.deserialize(data, self._history)
def load_items(self, items):
if items:
self._tab.predicted_navigation.emit(items[-1].url)
stream, _data, user_data = tabhistory.serialize(items)
qtutils.deserialize_stream(stream, self._history)
for i, data in enumerate(user_data):
@ -644,6 +658,8 @@ class WebKitTab(browsertab.AbstractTab):
self.printing = WebKitPrinting()
self.elements = WebKitElements(tab=self)
self.action = WebKitAction(tab=self)
# We're assigning settings in _set_widget
self.settings = webkitsettings.WebKitSettings(settings=None)
self._set_widget(widget)
self._connect_signals()
self.backend = usertypes.Backend.QtWebKit
@ -655,8 +671,8 @@ class WebKitTab(browsertab.AbstractTab):
settings = widget.settings()
settings.setAttribute(QWebSettings.PrivateBrowsingEnabled, True)
def openurl(self, url):
self._openurl_prepare(url)
def openurl(self, url, *, predict=True):
self._openurl_prepare(url, predict=predict)
self._widget.openurl(url)
def url(self, requested=False):
@ -730,6 +746,8 @@ class WebKitTab(browsertab.AbstractTab):
def _on_load_started(self):
super()._on_load_started()
self.networkaccessmanager().netrc_used = False
# Make sure the icon is cleared when navigating to a page without one.
self.icon_changed.emit(QIcon())
@pyqtSlot()
def _on_frame_load_finished(self):
@ -761,6 +779,31 @@ class WebKitTab(browsertab.AbstractTab):
def _on_contents_size_changed(self, size):
self.contents_size_changed.emit(QSizeF(size))
@pyqtSlot(usertypes.NavigationRequest)
def _on_navigation_request(self, navigation):
super()._on_navigation_request(navigation)
if not navigation.accepted:
return
log.webview.debug("target {} override {}".format(
self.data.open_target, self.data.override_target))
if self.data.override_target is not None:
target = self.data.override_target
self.data.override_target = None
else:
target = self.data.open_target
if (navigation.navigation_type == navigation.Type.link_clicked and
target != usertypes.ClickTarget.normal):
tab = shared.get_tab(self.win_id, target)
tab.openurl(navigation.url)
self.data.open_target = usertypes.ClickTarget.normal
navigation.accepted = False
if navigation.is_main_frame:
self.settings.update_for_url(navigation.url)
def _connect_signals(self):
view = self._widget
page = view.page()
@ -779,6 +822,7 @@ class WebKitTab(browsertab.AbstractTab):
page.frameCreated.connect(self._on_frame_created)
frame.contentsSizeChanged.connect(self._on_contents_size_changed)
frame.initialLayoutCompleted.connect(self._on_history_trigger)
page.navigation_request.connect(self._on_navigation_request)
def event_target(self):
return self._widget

View File

@ -22,6 +22,7 @@
import html
import functools
import sip
from PyQt5.QtCore import pyqtSlot, pyqtSignal, Qt, QUrl, QPoint
from PyQt5.QtGui import QDesktopServices
from PyQt5.QtNetwork import QNetworkReply, QNetworkRequest
@ -33,8 +34,7 @@ from qutebrowser.config import config
from qutebrowser.browser import pdfjs, shared
from qutebrowser.browser.webkit import http
from qutebrowser.browser.webkit.network import networkmanager
from qutebrowser.utils import (message, usertypes, log, jinja, objreg, debug,
urlutils)
from qutebrowser.utils import message, usertypes, log, jinja, objreg
class BrowserPage(QWebPage):
@ -54,10 +54,12 @@ class BrowserPage(QWebPage):
shutting_down: Emitted when the page is currently shutting down.
reloading: Emitted before a web page reloads.
arg: The URL which gets reloaded.
navigation_request: Emitted on acceptNavigationRequest.
"""
shutting_down = pyqtSignal()
reloading = pyqtSignal(QUrl)
navigation_request = pyqtSignal(usertypes.NavigationRequest)
def __init__(self, win_id, tab_id, tabdata, private, parent=None):
super().__init__(parent)
@ -70,7 +72,6 @@ class BrowserPage(QWebPage):
}
self._ignore_load_started = False
self.error_occurred = False
self.open_target = usertypes.ClickTarget.normal
self._networkmanager = networkmanager.NetworkManager(
win_id=win_id, tab_id=tab_id, private=private, parent=self)
self.setNetworkAccessManager(self._networkmanager)
@ -302,6 +303,10 @@ class BrowserPage(QWebPage):
Args:
frame: The QWebFrame to inject the user scripts into.
"""
if sip.isdeleted(frame):
log.greasemonkey.debug("_inject_userjs called for deleted frame!")
return
url = frame.url()
if url.isEmpty():
url = frame.requestedUrl()
@ -474,7 +479,7 @@ class BrowserPage(QWebPage):
source, line, msg)
def acceptNavigationRequest(self,
_frame: QWebFrame,
frame: QWebFrame,
request: QNetworkRequest,
typ: QWebPage.NavigationType):
"""Override acceptNavigationRequest to handle clicked links.
@ -486,36 +491,27 @@ class BrowserPage(QWebPage):
Checks if it should open it in a tab (middle-click or control) or not,
and then conditionally opens the URL here or in another tab/window.
"""
url = request.url()
log.webview.debug("navigation request: url {}, type {}, "
"target {} override {}".format(
url.toDisplayString(),
debug.qenum_key(QWebPage, typ),
self.open_target,
self._tabdata.override_target))
type_map = {
QWebPage.NavigationTypeLinkClicked:
usertypes.NavigationRequest.Type.link_clicked,
QWebPage.NavigationTypeFormSubmitted:
usertypes.NavigationRequest.Type.form_submitted,
QWebPage.NavigationTypeFormResubmitted:
usertypes.NavigationRequest.Type.form_resubmitted,
QWebPage.NavigationTypeBackOrForward:
usertypes.NavigationRequest.Type.back_forward,
QWebPage.NavigationTypeReload:
usertypes.NavigationRequest.Type.reloaded,
QWebPage.NavigationTypeOther:
usertypes.NavigationRequest.Type.other,
}
is_main_frame = frame is self.mainFrame()
navigation = usertypes.NavigationRequest(url=request.url(),
navigation_type=type_map[typ],
is_main_frame=is_main_frame)
if self._tabdata.override_target is not None:
target = self._tabdata.override_target
self._tabdata.override_target = None
else:
target = self.open_target
if navigation.navigation_type == navigation.Type.reloaded:
self.reloading.emit(navigation.url)
if typ == QWebPage.NavigationTypeReload:
self.reloading.emit(url)
return True
elif typ != QWebPage.NavigationTypeLinkClicked:
return True
if not url.isValid():
msg = urlutils.get_errstring(url, "Invalid link clicked")
message.error(msg)
self.open_target = usertypes.ClickTarget.normal
return False
if target == usertypes.ClickTarget.normal:
return True
tab = shared.get_tab(self._win_id, target)
tab.openurl(url)
self.open_target = usertypes.ClickTarget.normal
return False
self.navigation_request.emit(navigation)
return navigation.accepted

View File

@ -262,10 +262,10 @@ class WebView(QWebView):
target = usertypes.ClickTarget.tab_bg
else:
target = usertypes.ClickTarget.tab
self.page().open_target = target
self._tabdata.open_target = target
log.mouse.debug("Ctrl/Middle click, setting target: {}".format(
target))
else:
self.page().open_target = usertypes.ClickTarget.normal
self._tabdata.open_target = usertypes.ClickTarget.normal
log.mouse.debug("Normal click, setting normal target")
super().mousePressEvent(e)

View File

@ -26,6 +26,7 @@ For command arguments, there are also some variables you can use:
- `{url}` expands to the URL of the current page
- `{url:pretty}` expands to the URL in decoded format
- `{url:host}` expands to the host part of the URL
- `{clipboard}` expands to the clipboard contents
- `{primary}` expands to the primary selection contents

View File

@ -63,9 +63,13 @@ def replace_variables(win_id, arglist):
QUrl.FullyEncoded | QUrl.RemovePassword),
'url:pretty': lambda: _current_url(tabbed_browser).toString(
QUrl.DecodeReserved | QUrl.RemovePassword),
'url:host': lambda: _current_url(tabbed_browser).host(),
'clipboard': utils.get_clipboard,
'primary': lambda: utils.get_clipboard(selection=True),
}
for key in list(variables):
modified_key = '{' + key + '}'
variables[modified_key] = lambda x=modified_key: x
values = {}
args = []
tabbed_browser = objreg.get('tabbed-browser', scope='window',

View File

@ -60,7 +60,7 @@ class Completer(QObject):
self._timer.setSingleShot(True)
self._timer.setInterval(0)
self._timer.timeout.connect(self._update_completion)
self._last_cursor_pos = None
self._last_cursor_pos = -1
self._last_text = None
self._last_completion_func = None
self._cmd.update_completion.connect(self.schedule_completion_update)

View File

@ -22,13 +22,15 @@
from qutebrowser.config import configdata, configexc
from qutebrowser.completion.models import completionmodel, listcategory, util
from qutebrowser.commands import runners, cmdexc
from qutebrowser.keyinput import keyutils
def option(*, info):
"""A CompletionModel filled with settings and their descriptions."""
model = completionmodel.CompletionModel(column_widths=(20, 70, 10))
options = ((opt.name, opt.description, info.config.get_str(opt.name))
for opt in configdata.DATA.values())
for opt in configdata.DATA.values()
if not opt.no_autoconfig)
model.add_category(listcategory.ListCategory("Options", options))
return model
@ -36,8 +38,10 @@ def option(*, info):
def customized_option(*, info):
"""A CompletionModel filled with set settings and their descriptions."""
model = completionmodel.CompletionModel(column_widths=(20, 70, 10))
options = ((opt.name, opt.description, info.config.get_str(opt.name))
for opt, _value in info.config)
options = ((values.opt.name, values.opt.description,
info.config.get_str(values.opt.name))
for values in info.config
if values)
model.add_category(listcategory.ListCategory("Customized options",
options))
return model
@ -71,16 +75,16 @@ def value(optname, *_values, info):
return model
def bind(key, *, info):
"""A CompletionModel filled with all bindable commands and descriptions.
Args:
key: the key being bound.
"""
model = completionmodel.CompletionModel(column_widths=(20, 60, 20))
def _bind_current_default(key, info):
"""Get current/default data for the given key."""
data = []
try:
seq = keyutils.KeySequence.parse(key)
except keyutils.KeyParseError as e:
data.append(('', str(e), key))
return data
cmd_text = info.keyconf.get_command(key, 'normal')
cmd_text = info.keyconf.get_command(seq, 'normal')
if cmd_text:
parser = runners.CommandParser()
try:
@ -90,12 +94,24 @@ def bind(key, *, info):
else:
data.append((cmd_text, '(Current) {}'.format(cmd.desc), key))
cmd_text = info.keyconf.get_command(key, 'normal', default=True)
cmd_text = info.keyconf.get_command(seq, 'normal', default=True)
if cmd_text:
parser = runners.CommandParser()
cmd = parser.parse(cmd_text).cmd
data.append((cmd_text, '(Default) {}'.format(cmd.desc), key))
return data
def bind(key, *, info):
"""A CompletionModel filled with all bindable commands and descriptions.
Args:
key: the key being bound.
"""
model = completionmodel.CompletionModel(column_widths=(20, 60, 20))
data = _bind_current_default(key, info)
if data:
model.add_category(listcategory.ListCategory("Current/Default", data))

View File

@ -80,7 +80,7 @@ class HistoryCategory(QSqlQueryModel):
for i in range(len(words)))
# replace ' in timestamp-format to avoid breaking the query
timestamp_format = config.val.completion.timestamp_format
timestamp_format = config.val.completion.timestamp_format or ''
timefmt = ("strftime('{}', last_atime, 'unixepoch', 'localtime')"
.format(timestamp_format.replace("'", "`")))

View File

@ -117,11 +117,11 @@ def _buffer(skip_win_id=None):
if tabbed_browser.shutting_down:
continue
tabs = []
for idx in range(tabbed_browser.count()):
tab = tabbed_browser.widget(idx)
for idx in range(tabbed_browser.widget.count()):
tab = tabbed_browser.widget.widget(idx)
tabs.append(("{}/{}".format(win_id, idx + 1),
tab.url().toDisplayString(),
tabbed_browser.page_title(idx)))
tabbed_browser.widget.page_title(idx)))
cat = listcategory.ListCategory("{}".format(win_id), tabs,
delete_func=delete_buffer)
model.add_category(cat)

View File

@ -25,9 +25,10 @@ import functools
from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject
from qutebrowser.config import configdata, configexc
from qutebrowser.config import configdata, configexc, configutils
from qutebrowser.utils import utils, log, jinja
from qutebrowser.misc import objects
from qutebrowser.keyinput import keyutils
# An easy way to access the config from other code via config.val.foo
val = None
@ -37,6 +38,9 @@ key_instance = None
# Keeping track of all change filters to validate them later.
change_filters = []
# Sentinel
UNSET = object()
class change_filter: # noqa: N801,N806 pylint: disable=invalid-name
@ -132,20 +136,18 @@ class KeyConfig:
def __init__(self, config):
self._config = config
def _prepare(self, key, mode):
"""Make sure the given mode exists and normalize the key."""
def _validate(self, key, mode):
"""Validate the given key and mode."""
# Catch old usage of this code
assert isinstance(key, keyutils.KeySequence), key
if mode not in configdata.DATA['bindings.default'].default:
raise configexc.KeybindingError("Invalid mode {}!".format(mode))
if utils.is_special_key(key):
# <Ctrl-t>, <ctrl-T>, and <ctrl-t> should be considered equivalent
return utils.normalize_keystr(key)
return key
def get_bindings_for(self, mode):
"""Get the combined bindings for the given mode."""
bindings = dict(val.bindings.default[mode])
for key, binding in val.bindings.commands[mode].items():
if binding is None:
if not binding:
bindings.pop(key, None)
else:
bindings[key] = binding
@ -155,20 +157,20 @@ class KeyConfig:
"""Get a dict of commands to a list of bindings for the mode."""
cmd_to_keys = {}
bindings = self.get_bindings_for(mode)
for key, full_cmd in sorted(bindings.items()):
for seq, full_cmd in sorted(bindings.items()):
for cmd in full_cmd.split(';;'):
cmd = cmd.strip()
cmd_to_keys.setdefault(cmd, [])
# put special bindings last
if utils.is_special_key(key):
cmd_to_keys[cmd].append(key)
# Put bindings involving modifiers last
if any(info.modifiers for info in seq):
cmd_to_keys[cmd].append(str(seq))
else:
cmd_to_keys[cmd].insert(0, key)
cmd_to_keys[cmd].insert(0, str(seq))
return cmd_to_keys
def get_command(self, key, mode, default=False):
"""Get the command for a given key (or None)."""
key = self._prepare(key, mode)
self._validate(key, mode)
if default:
bindings = dict(val.bindings.default[mode])
else:
@ -182,23 +184,23 @@ class KeyConfig:
"Can't add binding '{}' with empty command in {} "
'mode'.format(key, mode))
key = self._prepare(key, mode)
self._validate(key, mode)
log.keyboard.vdebug("Adding binding {} -> {} in mode {}.".format(
key, command, mode))
bindings = self._config.get_obj('bindings.commands')
bindings = self._config.get_mutable_obj('bindings.commands')
if mode not in bindings:
bindings[mode] = {}
bindings[mode][key] = command
bindings[mode][str(key)] = command
self._config.update_mutables(save_yaml=save_yaml)
def bind_default(self, key, *, mode='normal', save_yaml=False):
"""Restore a default keybinding."""
key = self._prepare(key, mode)
self._validate(key, mode)
bindings_commands = self._config.get_obj('bindings.commands')
bindings_commands = self._config.get_mutable_obj('bindings.commands')
try:
del bindings_commands[mode][key]
del bindings_commands[mode][str(key)]
except KeyError:
raise configexc.KeybindingError(
"Can't find binding '{}' in {} mode".format(key, mode))
@ -206,18 +208,18 @@ class KeyConfig:
def unbind(self, key, *, mode='normal', save_yaml=False):
"""Unbind the given key in the given mode."""
key = self._prepare(key, mode)
self._validate(key, mode)
bindings_commands = self._config.get_obj('bindings.commands')
bindings_commands = self._config.get_mutable_obj('bindings.commands')
if val.bindings.commands[mode].get(key, None) is not None:
# In custom bindings -> remove it
del bindings_commands[mode][key]
del bindings_commands[mode][str(key)]
elif key in val.bindings.default[mode]:
# In default bindings -> shadow it with None
if mode not in bindings_commands:
bindings_commands[mode] = {}
bindings_commands[mode][key] = None
bindings_commands[mode][str(key)] = None
else:
raise configexc.KeybindingError(
"Can't find binding '{}' in {} mode".format(key, mode))
@ -229,8 +231,12 @@ class Config(QObject):
"""Main config object.
Class attributes:
MUTABLE_TYPES: Types returned from the config which could potentially
be mutated.
Attributes:
_values: A dict mapping setting names to their values.
_values: A dict mapping setting names to configutils.Values objects.
_mutables: A dictionary of mutable objects to be checked for changes.
_yaml: A YamlConfig object or None.
@ -238,19 +244,25 @@ class Config(QObject):
changed: Emitted with the option name when an option changed.
"""
MUTABLE_TYPES = (dict, list)
changed = pyqtSignal(str)
def __init__(self, yaml_config, parent=None):
super().__init__(parent)
self.changed.connect(_render_stylesheet.cache_clear)
self._values = {}
self._mutables = {}
self._yaml = yaml_config
self._init_values()
def _init_values(self):
"""Populate the self._values dict."""
self._values = {}
for name, opt in configdata.DATA.items():
self._values[name] = configutils.Values(opt)
def __iter__(self):
"""Iterate over Option, value tuples."""
for name, value in sorted(self._values.items()):
yield (self.get_opt(name), value)
"""Iterate over configutils.Values items."""
yield from self._values.values()
def init_save_manager(self, save_manager):
"""Make sure the config gets saved properly.
@ -260,24 +272,32 @@ class Config(QObject):
"""
self._yaml.init_save_manager(save_manager)
def _set_value(self, opt, value):
def _set_value(self, opt, value, pattern=None):
"""Set the given option to the given value."""
if not isinstance(objects.backend, objects.NoBackend):
if objects.backend not in opt.backends:
raise configexc.BackendError(opt.name, objects.backend)
opt.typ.to_py(value) # for validation
self._values[opt.name] = opt.typ.from_obj(value)
self._values[opt.name].add(opt.typ.from_obj(value), pattern)
self.changed.emit(opt.name)
log.config.debug("Config option changed: {} = {}".format(
opt.name, value))
def _check_yaml(self, opt, save_yaml):
"""Make sure the given option may be set in autoconfig.yml."""
if save_yaml and opt.no_autoconfig:
raise configexc.NoAutoconfigError(opt.name)
def read_yaml(self):
"""Read the YAML settings from self._yaml."""
self._yaml.load()
for name, value in self._yaml:
self._set_value(self.get_opt(name), value)
for values in self._yaml:
for scoped in values:
self._set_value(values.opt, scoped.value,
pattern=scoped.pattern)
def get_opt(self, name):
"""Get a configdata.Option object for the given setting."""
@ -290,77 +310,115 @@ class Config(QObject):
name, deleted=deleted, renamed=renamed)
raise exception from None
def get(self, name):
def get(self, name, url=None):
"""Get the given setting converted for Python code."""
opt = self.get_opt(name)
obj = self.get_obj(name, mutable=False)
obj = self.get_obj(name, url=url)
return opt.typ.to_py(obj)
def get_obj(self, name, *, mutable=True):
def _maybe_copy(self, value):
"""Copy the value if it could potentially be mutated."""
if isinstance(value, self.MUTABLE_TYPES):
# For mutable objects, create a copy so we don't accidentally
# mutate the config's internal value.
return copy.deepcopy(value)
else:
# Shouldn't be mutable (and thus hashable)
assert value.__hash__ is not None, value
return value
def get_obj(self, name, *, url=None):
"""Get the given setting as object (for YAML/config.py).
If mutable=True is set, watch the returned object for mutations.
Note that the returned values are not watched for mutation.
If a URL is given, return the value which should be used for that URL.
"""
opt = self.get_opt(name)
obj = None
self.get_opt(name) # To make sure it exists
value = self._values[name].get_for_url(url)
return self._maybe_copy(value)
def get_obj_for_pattern(self, name, *, pattern):
"""Get the given setting as object (for YAML/config.py).
This gets the overridden value for a given pattern, or
configutils.UNSET if no such override exists.
"""
self.get_opt(name) # To make sure it exists
value = self._values[name].get_for_pattern(pattern, fallback=False)
return self._maybe_copy(value)
def get_mutable_obj(self, name, *, pattern=None):
"""Get an object which can be mutated, e.g. in a config.py.
If a pattern is given, return the value for that pattern.
Note that it's impossible to get a mutable object for an URL as we
wouldn't know what pattern to apply.
"""
self.get_opt(name) # To make sure it exists
# If we allow mutation, there is a chance that prior mutations already
# entered the mutable dictionary and thus further copies are unneeded
# until update_mutables() is called
if name in self._mutables and mutable:
if name in self._mutables:
_copy, obj = self._mutables[name]
# Otherwise, we return a copy of the value stored internally, so the
# internal value can never be changed by mutating the object returned.
else:
obj = copy.deepcopy(self._values.get(name, opt.default))
# Then we watch the returned object for changes.
if isinstance(obj, (dict, list)):
if mutable:
self._mutables[name] = (copy.deepcopy(obj), obj)
else:
# Shouldn't be mutable (and thus hashable)
assert obj.__hash__ is not None, obj
return obj
return obj
def get_str(self, name):
"""Get the given setting as string."""
value = self._values[name].get_for_pattern(pattern)
copy_value = self._maybe_copy(value)
# Watch the returned object for changes if it's mutable.
if isinstance(copy_value, self.MUTABLE_TYPES):
self._mutables[name] = (value, copy_value) # old, new
return copy_value
def get_str(self, name, *, pattern=None):
"""Get the given setting as string.
If a pattern is given, get the setting for the given pattern or
configutils.UNSET.
"""
opt = self.get_opt(name)
value = self._values.get(name, opt.default)
values = self._values[name]
value = values.get_for_pattern(pattern)
return opt.typ.to_str(value)
def set_obj(self, name, value, *, save_yaml=False):
def set_obj(self, name, value, *, pattern=None, save_yaml=False):
"""Set the given setting from a YAML/config.py object.
If save_yaml=True is given, store the new value to YAML.
"""
self._set_value(self.get_opt(name), value)
opt = self.get_opt(name)
self._check_yaml(opt, save_yaml)
self._set_value(opt, value, pattern=pattern)
if save_yaml:
self._yaml[name] = value
self._yaml.set_obj(name, value, pattern=pattern)
def set_str(self, name, value, *, save_yaml=False):
def set_str(self, name, value, *, pattern=None, save_yaml=False):
"""Set the given setting from a string.
If save_yaml=True is given, store the new value to YAML.
"""
opt = self.get_opt(name)
self._check_yaml(opt, save_yaml)
converted = opt.typ.from_str(value)
log.config.debug("Setting {} (type {}) to {!r} (converted from {!r})"
.format(name, opt.typ.__class__.__name__, converted,
value))
self._set_value(opt, converted)
self._set_value(opt, converted, pattern=pattern)
if save_yaml:
self._yaml[name] = converted
self._yaml.set_obj(name, converted, pattern=pattern)
def unset(self, name, *, save_yaml=False):
def unset(self, name, *, save_yaml=False, pattern=None):
"""Set the given setting back to its default."""
self.get_opt(name)
try:
del self._values[name]
except KeyError:
return
self.changed.emit(name)
opt = self.get_opt(name)
self._check_yaml(opt, save_yaml)
changed = self._values[name].remove(pattern)
if changed:
self.changed.emit(name)
if save_yaml:
self._yaml.unset(name)
self._yaml.unset(name, pattern=pattern)
def clear(self, *, save_yaml=False):
"""Clear all settings in the config.
@ -368,10 +426,10 @@ class Config(QObject):
If save_yaml=True is given, also remove all customization from the YAML
file.
"""
old_values = self._values
self._values = {}
for name in old_values:
self.changed.emit(name)
for name, values in self._values.items():
if values:
values.clear()
self.changed.emit(name)
if save_yaml:
self._yaml.clear()
@ -397,13 +455,15 @@ class Config(QObject):
Return:
The changed config part as string.
"""
lines = []
for opt, value in self:
str_value = opt.typ.to_str(value)
lines.append('{} = {}'.format(opt.name, str_value))
if not lines:
lines = ['<Default configuration>']
return '\n'.join(lines)
blocks = []
for values in sorted(self, key=lambda v: v.opt.name):
if values:
blocks.append(str(values))
if not blocks:
return '<Default configuration>'
return '\n'.join(blocks)
class ConfigContainer:
@ -415,16 +475,21 @@ class ConfigContainer:
_prefix: The __getattr__ chain leading up to this object.
_configapi: If given, get values suitable for config.py and
add errors to the given ConfigAPI object.
_pattern: The URL pattern to be used.
"""
def __init__(self, config, configapi=None, prefix=''):
def __init__(self, config, configapi=None, prefix='', pattern=None):
self._config = config
self._prefix = prefix
self._configapi = configapi
self._pattern = pattern
if configapi is None and pattern is not None:
raise TypeError("Can't use pattern without configapi!")
def __repr__(self):
return utils.get_repr(self, constructor=True, config=self._config,
configapi=self._configapi, prefix=self._prefix)
configapi=self._configapi, prefix=self._prefix,
pattern=self._pattern)
@contextlib.contextmanager
def _handle_error(self, action, name):
@ -452,7 +517,7 @@ class ConfigContainer:
if configdata.is_valid_prefix(name):
return ConfigContainer(config=self._config,
configapi=self._configapi,
prefix=name)
prefix=name, pattern=self._pattern)
with self._handle_error('getting', name):
if self._configapi is None:
@ -460,7 +525,8 @@ class ConfigContainer:
return self._config.get(name)
else:
# access from config.py
return self._config.get_obj(name)
return self._config.get_mutable_obj(
name, pattern=self._pattern)
def __setattr__(self, attr, value):
"""Set the given option in the config."""
@ -470,7 +536,7 @@ class ConfigContainer:
name = self._join(attr)
with self._handle_error('setting', name):
self._config.set_obj(name, value)
self._config.set_obj(name, value, pattern=self._pattern)
def _join(self, attr):
"""Get the prefix joined with the given attribute."""

View File

@ -26,9 +26,10 @@ from PyQt5.QtCore import QUrl
from qutebrowser.commands import cmdexc, cmdutils
from qutebrowser.completion.models import configmodel
from qutebrowser.utils import objreg, utils, message, standarddir
from qutebrowser.utils import objreg, message, standarddir, urlmatch
from qutebrowser.config import configtypes, configexc, configfiles, configdata
from qutebrowser.misc import editor
from qutebrowser.keyinput import keyutils
class ConfigCommands:
@ -47,17 +48,41 @@ class ConfigCommands:
except configexc.Error as e:
raise cmdexc.CommandError(str(e))
def _print_value(self, option):
def _parse_pattern(self, pattern):
"""Parse a pattern string argument to a pattern."""
if pattern is None:
return None
try:
return urlmatch.UrlPattern(pattern)
except urlmatch.ParseError as e:
raise cmdexc.CommandError("Error while parsing {}: {}"
.format(pattern, str(e)))
def _parse_key(self, key):
"""Parse a key argument."""
try:
return keyutils.KeySequence.parse(key)
except keyutils.KeyParseError as e:
raise cmdexc.CommandError(str(e))
def _print_value(self, option, pattern):
"""Print the value of the given option."""
with self._handle_config_error():
value = self._config.get_str(option)
message.info("{} = {}".format(option, value))
value = self._config.get_str(option, pattern=pattern)
text = "{} = {}".format(option, value)
if pattern is not None:
text += " for {}".format(pattern)
message.info(text)
@cmdutils.register(instance='config-commands')
@cmdutils.argument('option', completion=configmodel.option)
@cmdutils.argument('value', completion=configmodel.value)
@cmdutils.argument('win_id', win_id=True)
def set(self, win_id, option=None, value=None, temp=False, print_=False):
@cmdutils.argument('pattern', flag='u')
def set(self, win_id, option=None, value=None, temp=False, print_=False,
*, pattern=None):
"""Set an option.
If the option name ends with '?', the value of the option is shown
@ -69,6 +94,7 @@ class ConfigCommands:
Args:
option: The name of the option.
value: The value to set.
pattern: The URL pattern to use.
temp: Set value temporarily until qutebrowser is closed.
print_: Print the value after setting.
"""
@ -82,8 +108,10 @@ class ConfigCommands:
raise cmdexc.CommandError("Toggling values was moved to the "
":config-cycle command")
pattern = self._parse_pattern(pattern)
if option.endswith('?') and option != '?':
self._print_value(option[:-1])
self._print_value(option[:-1], pattern=pattern)
return
with self._handle_config_error():
@ -91,10 +119,11 @@ class ConfigCommands:
raise cmdexc.CommandError("set: The following arguments "
"are required: value")
else:
self._config.set_str(option, value, save_yaml=not temp)
self._config.set_str(option, value, pattern=pattern,
save_yaml=not temp)
if print_:
self._print_value(option)
self._print_value(option, pattern=pattern)
@cmdutils.register(instance='config-commands', maxsplit=1,
no_cmd_split=True, no_replace_variables=True)
@ -108,7 +137,8 @@ class ConfigCommands:
Using :bind without any arguments opens a page showing all keybindings.
Args:
key: The keychain or special key (inside `<...>`) to bind.
key: The keychain to bind. Examples of valid keychains are `gC`,
`<Ctrl-X>` or `<Ctrl-C>a`.
command: The command to execute, with optional args.
mode: A comma-separated list of modes to bind the key in
(default: `normal`). See `:help bindings.commands` for the
@ -121,58 +151,64 @@ class ConfigCommands:
tabbed_browser.openurl(QUrl('qute://bindings'), newtab=True)
return
seq = self._parse_key(key)
if command is None:
if default:
# :bind --default: Restore default
with self._handle_config_error():
self._keyconfig.bind_default(key, mode=mode,
self._keyconfig.bind_default(seq, mode=mode,
save_yaml=True)
return
# No --default -> print binding
if utils.is_special_key(key):
# self._keyconfig.get_command does this, but we also need it
# normalized for the output below
key = utils.normalize_keystr(key)
with self._handle_config_error():
cmd = self._keyconfig.get_command(key, mode)
cmd = self._keyconfig.get_command(seq, mode)
if cmd is None:
message.info("{} is unbound in {} mode".format(key, mode))
message.info("{} is unbound in {} mode".format(seq, mode))
else:
message.info("{} is bound to '{}' in {} mode".format(
key, cmd, mode))
seq, cmd, mode))
return
with self._handle_config_error():
self._keyconfig.bind(key, command, mode=mode, save_yaml=True)
self._keyconfig.bind(seq, command, mode=mode, save_yaml=True)
@cmdutils.register(instance='config-commands')
def unbind(self, key, *, mode='normal'):
"""Unbind a keychain.
Args:
key: The keychain or special key (inside <...>) to unbind.
key: The keychain to unbind. See the help for `:bind` for the
correct syntax for keychains.
mode: A mode to unbind the key in (default: `normal`).
See `:help bindings.commands` for the available modes.
"""
with self._handle_config_error():
self._keyconfig.unbind(key, mode=mode, save_yaml=True)
self._keyconfig.unbind(self._parse_key(key), mode=mode,
save_yaml=True)
@cmdutils.register(instance='config-commands', star_args_optional=True)
@cmdutils.argument('option', completion=configmodel.option)
@cmdutils.argument('values', completion=configmodel.value)
def config_cycle(self, option, *values, temp=False, print_=False):
@cmdutils.argument('pattern', flag='u')
def config_cycle(self, option, *values, pattern=None, temp=False,
print_=False):
"""Cycle an option between multiple values.
Args:
option: The name of the option.
values: The values to cycle through.
pattern: The URL pattern to use.
temp: Set value temporarily until qutebrowser is closed.
print_: Print the value after setting.
"""
pattern = self._parse_pattern(pattern)
with self._handle_config_error():
opt = self._config.get_opt(option)
old_value = self._config.get_obj(option, mutable=False)
old_value = self._config.get_obj_for_pattern(option,
pattern=pattern)
if not values and isinstance(opt.typ, configtypes.Bool):
values = ['true', 'false']
@ -194,10 +230,11 @@ class ConfigCommands:
value = values[0]
with self._handle_config_error():
self._config.set_obj(option, value, save_yaml=not temp)
self._config.set_obj(option, value, pattern=pattern,
save_yaml=not temp)
if print_:
self._print_value(option)
self._print_value(option, pattern=pattern)
@cmdutils.register(instance='config-commands')
@cmdutils.argument('option', completion=configmodel.customized_option)
@ -291,13 +328,16 @@ class ConfigCommands:
"overwrite!".format(filename))
if defaults:
options = [(opt, opt.default)
options = [(None, opt, opt.default)
for _name, opt in sorted(configdata.DATA.items())]
bindings = dict(configdata.DATA['bindings.default'].default)
commented = True
else:
options = list(self._config)
bindings = dict(self._config.get_obj('bindings.commands'))
options = []
for values in self._config:
for scoped in values:
options.append((scoped.pattern, values.opt, scoped.value))
bindings = dict(self._config.get_mutable_obj('bindings.commands'))
commented = False
writer = configfiles.ConfigPyWriter(options, bindings,

View File

@ -48,7 +48,9 @@ class Option:
backends = attr.ib()
raw_backends = attr.ib()
description = attr.ib()
supports_pattern = attr.ib(default=False)
restart = attr.ib(default=False)
no_autoconfig = attr.ib(default=False)
@attr.s
@ -197,7 +199,8 @@ def _read_yaml(yaml_data):
migrations = Migrations()
data = utils.yaml_load(yaml_data)
keys = {'type', 'default', 'desc', 'backend', 'restart'}
keys = {'type', 'default', 'desc', 'backend', 'restart',
'supports_pattern', 'no_autoconfig'}
for name, option in data.items():
if set(option.keys()) == {'renamed'}:
@ -223,7 +226,10 @@ def _read_yaml(yaml_data):
backends=_parse_yaml_backends(name, backends),
raw_backends=backends if isinstance(backends, dict) else None,
description=option['desc'],
restart=option.get('restart', False))
restart=option.get('restart', False),
supports_pattern=option.get('supports_pattern', False),
no_autoconfig=option.get('no_autoconfig', False),
)
# Make sure no key shadows another.
for key1 in parsed:

View File

@ -240,6 +240,7 @@ content.cache.appcache:
default: true
type: Bool
backend: QtWebKit
supports_pattern: true
desc: >-
Enable support for the HTML 5 web application cache feature.
@ -298,12 +299,14 @@ content.dns_prefetch:
default: true
type: Bool
backend: QtWebKit
supports_pattern: true
desc: Try to pre-fetch DNS entries to speed up browsing.
content.frame_flattening:
default: false
type: Bool
backend: QtWebKit
supports_pattern: true
desc: >-
Expand each subframe to its contents.
@ -459,12 +462,14 @@ content.host_blocking.whitelist:
content.hyperlink_auditing:
default: false
type: Bool
supports_pattern: true
desc: Enable hyperlink auditing (`<a ping>`).
content.images:
default: true
type: Bool
desc: Load images automatically in web pages.
supports_pattern: true
content.javascript.alert:
default: true
@ -474,6 +479,7 @@ content.javascript.alert:
content.javascript.can_access_clipboard:
default: false
type: Bool
supports_pattern: true
desc: >-
Allow JavaScript to read from or write to the clipboard.
@ -484,16 +490,19 @@ content.javascript.can_close_tabs:
default: false
type: Bool
backend: QtWebKit
supports_pattern: true
desc: Allow JavaScript to close tabs.
content.javascript.can_open_tabs_automatically:
default: false
type: Bool
supports_pattern: true
desc: Allow JavaScript to open new tabs without user interaction.
content.javascript.enabled:
default: true
type: Bool
supports_pattern: true
desc: Enable JavaScript.
content.javascript.log:
@ -536,16 +545,19 @@ content.javascript.prompt:
content.local_content_can_access_remote_urls:
default: false
type: Bool
supports_pattern: true
desc: Allow locally loaded documents to access remote URLs.
content.local_content_can_access_file_urls:
default: true
type: Bool
supports_pattern: true
desc: Allow locally loaded documents to access other local URLs.
content.local_storage:
default: true
type: Bool
supports_pattern: true
desc: Enable support for HTML 5 local storage and Web SQL.
content.media_capture:
@ -583,6 +595,7 @@ content.pdfjs:
content.plugins:
default: false
type: Bool
supports_pattern: true
desc: Enable plugins in Web pages.
content.print_element_backgrounds:
@ -591,6 +604,7 @@ content.print_element_backgrounds:
backend:
QtWebKit: true
QtWebEngine: Qt 5.8
supports_pattern: true
desc: >-
Draw the background color and images also when the page is printed.
@ -631,11 +645,13 @@ content.user_stylesheets:
content.webgl:
default: true
type: Bool
supports_pattern: true
desc: Enable WebGL.
content.xss_auditing:
type: Bool
default: false
supports_pattern: true
desc: >-
Monitor load requests for cross-site scripting attempts.
@ -965,6 +981,11 @@ input.insert_mode.auto_load:
desc: Automatically enter insert mode if an editable element is focused after
loading the page.
input.insert_mode.auto_enter:
default: true
type: Bool
desc: Enter insert mode if an editable element is clicked.
input.insert_mode.auto_leave:
default: true
type: Bool
@ -978,6 +999,7 @@ input.insert_mode.plugins:
input.links_included_in_focus_chain:
default: true
type: Bool
supports_pattern: true
desc: Include hyperlinks in the keyboard focus chain when tabbing.
input.partial_timeout:
@ -1003,6 +1025,7 @@ input.rocker_gestures:
input.spatial_navigation:
default: false
type: Bool
supports_pattern: true
desc: >-
Enable spatial navigation.
@ -1083,6 +1106,7 @@ scrolling.bar:
scrolling.smooth:
type: Bool
default: false
supports_pattern: true
desc: >-
Enable smooth scrolling for web pages.
@ -1557,6 +1581,7 @@ zoom.text_only:
type: Bool
default: false
backend: QtWebKit
supports_pattern: true
desc: Apply the zoom factor on a frame only to the text or to all content.
## colors
@ -2141,6 +2166,7 @@ bindings.key_mappings:
<Ctrl-Enter>: <Ctrl-Return>
type:
name: Dict
none_ok: true
keytype: Key
valtype: Key
desc: >-
@ -2156,6 +2182,7 @@ bindings.key_mappings:
`bindings.commands`), the mapping is ignored.
bindings.default:
no_autoconfig: true
default:
normal:
<Escape>: clear-keychain ;; search ;; fullscreen --leave
@ -2309,6 +2336,18 @@ bindings.default:
<Ctrl-p>: tab-pin
q: record-macro
"@": run-macro
tsh: config-cycle -p -t -u *://{url:host}/* content.javascript.enabled ;; reload
tSh: config-cycle -p -u *://{url:host}/* content.javascript.enabled ;; reload
tsH: config-cycle -p -t -u *://*.{url:host}/* content.javascript.enabled ;; reload
tSH: config-cycle -p -u *://*.{url:host}/* content.javascript.enabled ;; reload
tsu: config-cycle -p -t -u {url} content.javascript.enabled ;; reload
tSu: config-cycle -p -u {url} content.javascript.enabled ;; reload
tph: config-cycle -p -t -u *://{url:host}/* content.plugins ;; reload
tPh: config-cycle -p -u *://{url:host}/* content.plugins ;; reload
tpH: config-cycle -p -t -u *://*.{url:host}/* content.plugins ;; reload
tPH: config-cycle -p -u *://*.{url:host}/* content.plugins ;; reload
tpu: config-cycle -p -t -u {url} content.plugins ;; reload
tPu: config-cycle -p -u {url} content.plugins ;; reload
insert:
<Ctrl-E>: open-editor
<Shift-Ins>: insert-text {primary}
@ -2353,8 +2392,6 @@ bindings.default:
<Escape>: leave-mode
prompt:
<Return>: prompt-accept
y: prompt-accept yes
n: prompt-accept no
<Ctrl-X>: prompt-open-download
<Shift-Tab>: prompt-item-focus prev
<Up>: prompt-item-focus prev
@ -2377,6 +2414,13 @@ bindings.default:
<Ctrl-H>: rl-backward-delete-char
<Ctrl-Y>: rl-yank
<Escape>: leave-mode
yesno:
<Return>: prompt-accept
y: prompt-accept yes
n: prompt-accept no
<Alt-Y>: prompt-yank
<Alt-Shift-Y>: prompt-yank --sel
<Escape>: leave-mode
caret:
v: toggle-selection
<Space>: toggle-selection
@ -2412,7 +2456,7 @@ bindings.default:
none_ok: true
keytype: String # section name
fixed_keys: ['normal', 'insert', 'hint', 'passthrough', 'command',
'prompt', 'caret', 'register']
'prompt', 'yesno', 'caret', 'register']
valtype:
name: Dict
none_ok: true
@ -2436,14 +2480,14 @@ bindings.commands:
none_ok: true
keytype: String # section name
fixed_keys: ['normal', 'insert', 'hint', 'passthrough', 'command',
'prompt', 'caret', 'register']
'prompt', 'yesno', 'caret', 'register']
valtype:
name: Dict
none_ok: true
keytype: Key
valtype:
name: Command
none_ok: true
none_ok: true # needed for :unbind
desc: >-
Keybindings mapping keys to commands in different modes.
@ -2459,7 +2503,6 @@ bindings.commands:
If you want to map a key to another key, check the `bindings.key_mappings`
setting instead.
For special keys (can't be part of a keychain), enclose them in `<`...`>`.
For modifiers, you can use either `-` or `+` as delimiters, and these
names:
@ -2508,10 +2551,8 @@ bindings.commands:
* prompt: Entered when there's a prompt to display, like for download
locations or when invoked from JavaScript.
+
You can bind normal keys in this mode, but they will be only active when
a yes/no-prompt is asked. For other prompt modes, you can only bind
special keys.
* yesno: Entered when there's a yes/no prompt displayed.
* caret: Entered when pressing the `v` mode, used to select text using the
keyboard.

View File

@ -31,6 +31,15 @@ class Error(Exception):
pass
class NoAutoconfigError(Error):
"""Raised when this option can't be set in autoconfig.yml."""
def __init__(self, name):
super().__init__("The {} setting can only be set in config.py!"
.format(name))
class BackendError(Error):
"""Raised when this setting is unavailable with the current backend."""
@ -40,6 +49,15 @@ class BackendError(Error):
"backend!".format(name, backend.name))
class NoPatternError(Error):
"""Raised when the given setting does not support URL patterns."""
def __init__(self, name):
super().__init__("The {} setting does not support URL patterns!"
.format(name))
class ValidationError(Error):
"""Raised when a value for a config type was invalid.
@ -92,6 +110,10 @@ class ConfigErrorDesc:
traceback = attr.ib(None)
def __str__(self):
if self.traceback:
return '{} - {}: {}'.format(self.text,
self.exception.__class__.__name__,
self.exception)
return '{}: {}'.format(self.text, self.exception)
def with_text(self, text):

View File

@ -32,8 +32,9 @@ import yaml
from PyQt5.QtCore import pyqtSignal, QObject, QSettings
import qutebrowser
from qutebrowser.config import configexc, config, configdata
from qutebrowser.utils import standarddir, utils, qtutils, log
from qutebrowser.config import configexc, config, configdata, configutils
from qutebrowser.keyinput import keyutils
from qutebrowser.utils import standarddir, utils, qtutils, log, urlmatch
# The StateConfig instance
@ -80,16 +81,19 @@ class YamlConfig(QObject):
VERSION: The current version number of the config file.
"""
VERSION = 1
VERSION = 2
changed = pyqtSignal()
def __init__(self, parent=None):
super().__init__(parent)
self._filename = os.path.join(standarddir.config(auto=True),
'autoconfig.yml')
self._values = {}
self._dirty = None
self._values = {}
for name, opt in configdata.DATA.items():
self._values[name] = configutils.Values(opt)
def init_save_manager(self, save_manager):
"""Make sure the config gets saved properly.
@ -98,18 +102,9 @@ class YamlConfig(QObject):
"""
save_manager.add_saveable('yaml-config', self._save, self.changed)
def __getitem__(self, name):
return self._values[name]
def __setitem__(self, name, value):
self._values[name] = value
self._mark_changed()
def __contains__(self, name):
return name in self._values
def __iter__(self):
return iter(sorted(self._values.items()))
"""Iterate over configutils.Values items."""
yield from self._values.values()
def _mark_changed(self):
"""Mark the YAML config as changed."""
@ -121,7 +116,17 @@ class YamlConfig(QObject):
if not self._dirty:
return
data = {'config_version': self.VERSION, 'global': self._values}
settings = {}
for name, values in sorted(self._values.items()):
if not values:
continue
settings[name] = {}
for scoped in values:
key = ('global' if scoped.pattern is None
else str(scoped.pattern))
settings[name][key] = scoped.value
data = {'config_version': self.VERSION, 'settings': settings}
with qtutils.savefile_open(self._filename) as f:
f.write(textwrap.dedent("""
# DO NOT edit this file by hand, qutebrowser will overwrite it.
@ -130,6 +135,29 @@ class YamlConfig(QObject):
""".lstrip('\n')))
utils.yaml_dump(data, f)
def _pop_object(self, yaml_data, key, typ):
"""Get a global object from the given data."""
if not isinstance(yaml_data, dict):
desc = configexc.ConfigErrorDesc("While loading data",
"Toplevel object is not a dict")
raise configexc.ConfigFileErrors('autoconfig.yml', [desc])
if key not in yaml_data:
desc = configexc.ConfigErrorDesc(
"While loading data",
"Toplevel object does not contain '{}' key".format(key))
raise configexc.ConfigFileErrors('autoconfig.yml', [desc])
data = yaml_data.pop(key)
if not isinstance(data, typ):
desc = configexc.ConfigErrorDesc(
"While loading data",
"'{}' object is not a {}".format(key, typ.__name__))
raise configexc.ConfigFileErrors('autoconfig.yml', [desc])
return data
def load(self):
"""Load configuration from the configured YAML file."""
try:
@ -144,76 +172,132 @@ class YamlConfig(QObject):
desc = configexc.ConfigErrorDesc("While parsing", e)
raise configexc.ConfigFileErrors('autoconfig.yml', [desc])
try:
global_obj = yaml_data['global']
except KeyError:
config_version = self._pop_object(yaml_data, 'config_version', int)
if config_version == 1:
settings = self._load_legacy_settings_object(yaml_data)
self._mark_changed()
elif config_version > self.VERSION:
desc = configexc.ConfigErrorDesc(
"While loading data",
"Toplevel object does not contain 'global' key")
raise configexc.ConfigFileErrors('autoconfig.yml', [desc])
except TypeError:
desc = configexc.ConfigErrorDesc("While loading data",
"Toplevel object is not a dict")
"While reading",
"Can't read config from incompatible newer version")
raise configexc.ConfigFileErrors('autoconfig.yml', [desc])
else:
settings = self._load_settings_object(yaml_data)
self._dirty = False
if not isinstance(global_obj, dict):
desc = configexc.ConfigErrorDesc(
"While loading data",
"'global' object is not a dict")
raise configexc.ConfigFileErrors('autoconfig.yml', [desc])
settings = self._handle_migrations(settings)
self._validate(settings)
self._build_values(settings)
self._values = global_obj
self._dirty = False
def _load_settings_object(self, yaml_data):
"""Load the settings from the settings: key."""
return self._pop_object(yaml_data, 'settings', dict)
self._handle_migrations()
self._validate()
def _load_legacy_settings_object(self, yaml_data):
data = self._pop_object(yaml_data, 'global', dict)
settings = {}
for name, value in data.items():
settings[name] = {'global': value}
return settings
def _handle_migrations(self):
def _build_values(self, settings):
"""Build up self._values from the values in the given dict."""
errors = []
for name, yaml_values in settings.items():
if not isinstance(yaml_values, dict):
errors.append(configexc.ConfigErrorDesc(
"While parsing {!r}".format(name), "value is not a dict"))
continue
values = configutils.Values(configdata.DATA[name])
if 'global' in yaml_values:
values.add(yaml_values.pop('global'))
for pattern, value in yaml_values.items():
if not isinstance(pattern, str):
errors.append(configexc.ConfigErrorDesc(
"While parsing {!r}".format(name),
"pattern is not of type string"))
continue
try:
urlpattern = urlmatch.UrlPattern(pattern)
except urlmatch.ParseError as e:
errors.append(configexc.ConfigErrorDesc(
"While parsing pattern {!r} for {!r}"
.format(pattern, name), e))
continue
values.add(value, urlpattern)
self._values[name] = values
if errors:
raise configexc.ConfigFileErrors('autoconfig.yml', errors)
def _handle_migrations(self, settings):
"""Migrate older configs to the newest format."""
# Simple renamed/deleted options
for name in list(self._values):
for name in list(settings):
if name in configdata.MIGRATIONS.renamed:
new_name = configdata.MIGRATIONS.renamed[name]
log.config.debug("Renaming {} to {}".format(name, new_name))
self._values[new_name] = self._values[name]
del self._values[name]
settings[new_name] = settings[name]
del settings[name]
self._mark_changed()
elif name in configdata.MIGRATIONS.deleted:
log.config.debug("Removing {}".format(name))
del self._values[name]
del settings[name]
self._mark_changed()
# tabs.persist_mode_on_change got merged into tabs.mode_on_change
old = 'tabs.persist_mode_on_change'
new = 'tabs.mode_on_change'
if old in self._values:
if self._values[old]:
self._values[new] = 'persist'
else:
self._values[new] = 'normal'
del self._values[old]
if old in settings:
settings[new] = {}
for scope, val in settings[old].items():
if val:
settings[new][scope] = 'persist'
else:
settings[new][scope] = 'normal'
del settings[old]
self._mark_changed()
def _validate(self):
# bindings.default can't be set in autoconfig.yml anymore, so ignore
# old values.
if 'bindings.default' in settings:
del settings['bindings.default']
self._mark_changed()
return settings
def _validate(self, settings):
"""Make sure all settings exist."""
unknown = set(self._values) - set(configdata.DATA)
unknown = []
for name in settings:
if name not in configdata.DATA:
unknown.append(name)
if unknown:
errors = [configexc.ConfigErrorDesc("While loading options",
"Unknown option {}".format(e))
for e in sorted(unknown)]
raise configexc.ConfigFileErrors('autoconfig.yml', errors)
def unset(self, name):
"""Remove the given option name if it's configured."""
try:
del self._values[name]
except KeyError:
return
def set_obj(self, name, value, *, pattern=None):
"""Set the given setting to the given value."""
self._values[name].add(value, pattern)
self._mark_changed()
def unset(self, name, *, pattern=None):
"""Remove the given option name if it's configured."""
changed = self._values[name].remove(pattern)
if changed:
self._mark_changed()
def clear(self):
"""Clear all values from the YAML file."""
self._values = []
for values in self._values.values():
values.clear()
self._mark_changed()
@ -242,6 +326,7 @@ class ConfigAPI:
@contextlib.contextmanager
def _handle_error(self, action, name):
"""Catch config-related exceptions and save them in self.errors."""
try:
yield
except configexc.ConfigFileErrors as e:
@ -251,30 +336,45 @@ class ConfigAPI:
except configexc.Error as e:
text = "While {} '{}'".format(action, name)
self.errors.append(configexc.ConfigErrorDesc(text, e))
except urlmatch.ParseError as e:
text = "While {} '{}' and parsing pattern".format(action, name)
self.errors.append(configexc.ConfigErrorDesc(text, e))
except keyutils.KeyParseError as e:
text = "While {} '{}' and parsing key".format(action, name)
self.errors.append(configexc.ConfigErrorDesc(text, e))
def finalize(self):
"""Do work which needs to be done after reading config.py."""
self._config.update_mutables()
def load_autoconfig(self):
"""Load the autoconfig.yml file which is used for :set/:bind/etc."""
with self._handle_error('reading', 'autoconfig.yml'):
read_autoconfig()
def get(self, name):
def get(self, name, pattern=None):
"""Get a setting value from the config, optionally with a pattern."""
with self._handle_error('getting', name):
return self._config.get_obj(name)
urlpattern = urlmatch.UrlPattern(pattern) if pattern else None
return self._config.get_mutable_obj(name, pattern=urlpattern)
def set(self, name, value):
def set(self, name, value, pattern=None):
"""Set a setting value in the config, optionally with a pattern."""
with self._handle_error('setting', name):
self._config.set_obj(name, value)
urlpattern = urlmatch.UrlPattern(pattern) if pattern else None
self._config.set_obj(name, value, pattern=urlpattern)
def bind(self, key, command, mode='normal'):
"""Bind a key to a command, with an optional key mode."""
with self._handle_error('binding', key):
self._keyconfig.bind(key, command, mode=mode)
seq = keyutils.KeySequence.parse(key)
self._keyconfig.bind(seq, command, mode=mode)
def unbind(self, key, mode='normal'):
"""Unbind a key from a command, with an optional key mode."""
with self._handle_error('unbinding', key):
self._keyconfig.unbind(key, mode=mode)
seq = keyutils.KeySequence.parse(key)
self._keyconfig.unbind(seq, mode=mode)
def source(self, filename):
"""Read the given config file from disk."""
@ -286,6 +386,16 @@ class ConfigAPI:
except configexc.ConfigFileErrors as e:
self.errors += e.errors
@contextlib.contextmanager
def pattern(self, pattern):
"""Get a ConfigContainer for the given pattern."""
# We need to propagate the exception so we don't need to return
# something.
urlpattern = urlmatch.UrlPattern(pattern)
container = config.ConfigContainer(config=self._config, configapi=self,
pattern=urlpattern)
yield container
class ConfigPyWriter:
@ -344,7 +454,7 @@ class ConfigPyWriter:
def _gen_options(self):
"""Generate the options part of the config."""
for opt, value in self._options:
for pattern, opt, value in self._options:
if opt.name in ['bindings.commands', 'bindings.default']:
continue
@ -363,7 +473,11 @@ class ConfigPyWriter:
except KeyError:
yield self._line("# - {}".format(val))
yield self._line('c.{} = {!r}'.format(opt.name, value))
if pattern is None:
yield self._line('c.{} = {!r}'.format(opt.name, value))
else:
yield self._line('config.set({!r}, {!r}, {!r})'.format(
opt.name, value, str(pattern)))
yield ''
def _gen_bindings(self):
@ -419,7 +533,7 @@ def read_config_py(filename, raising=False):
desc = configexc.ConfigErrorDesc("Error while compiling", e)
raise configexc.ConfigFileErrors(basename, [desc])
except SyntaxError as e:
desc = configexc.ConfigErrorDesc("Syntax Error", e,
desc = configexc.ConfigErrorDesc("Unhandled exception", e,
traceback=traceback.format_exc())
raise configexc.ConfigFileErrors(basename, [desc])

View File

@ -62,6 +62,7 @@ from PyQt5.QtWidgets import QTabWidget, QTabBar
from qutebrowser.commands import cmdutils
from qutebrowser.config import configexc
from qutebrowser.utils import standarddir, utils, qtutils, urlutils
from qutebrowser.keyinput import keyutils
SYSTEM_PROXY = object() # Return value for Proxy type
@ -450,7 +451,7 @@ class List(BaseType):
def from_obj(self, value):
if value is None:
return []
return value
return [self.valtype.from_obj(v) for v in value]
def to_py(self, value):
self._basic_py_validation(value, list)
@ -505,6 +506,16 @@ class ListOrValue(BaseType):
self.listtype = List(valtype, none_ok=none_ok, *args, **kwargs)
self.valtype = valtype
def _val_and_type(self, value):
"""Get the value and type to use for to_str/to_doc/from_str."""
if isinstance(value, list):
if len(value) == 1:
return value[0], self.valtype
else:
return value, self.listtype
else:
return value, self.valtype
def get_name(self):
return self.listtype.get_name() + ', or ' + self.valtype.get_name()
@ -532,25 +543,15 @@ class ListOrValue(BaseType):
if value is None:
return ''
if isinstance(value, list):
if len(value) == 1:
return self.valtype.to_str(value[0])
else:
return self.listtype.to_str(value)
else:
return self.valtype.to_str(value)
val, typ = self._val_and_type(value)
return typ.to_str(val)
def to_doc(self, value, indent=0):
if value is None:
return 'empty'
if isinstance(value, list):
if len(value) == 1:
return self.valtype.to_doc(value[0], indent)
else:
return self.listtype.to_doc(value, indent)
else:
return self.valtype.to_doc(value, indent)
val, typ = self._val_and_type(value)
return typ.to_doc(val)
class FlagList(List):
@ -1198,7 +1199,9 @@ class Dict(BaseType):
def from_obj(self, value):
if value is None:
return {}
return value
return {self.keytype.from_obj(key): self.valtype.from_obj(val)
for key, val in value.items()}
def _fill_fixed_keys(self, value):
"""Fill missing fixed keys with a None-value."""
@ -1647,10 +1650,16 @@ class Key(BaseType):
"""A name of a key."""
def from_obj(self, value):
"""Make sure key sequences are always normalized."""
return str(keyutils.KeySequence.parse(value))
def to_py(self, value):
self._basic_py_validation(value, str)
if not value:
return None
if utils.is_special_key(value):
value = '<{}>'.format(utils.normalize_keystr(value[1:-1]))
return value
try:
return keyutils.KeySequence.parse(value)
except keyutils.KeyParseError as e:
raise configexc.ValidationError(value, str(e))

View File

@ -0,0 +1,186 @@
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
# Copyright 2018 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/>.
"""Utilities and data structures used by various config code."""
import attr
from qutebrowser.utils import utils
from qutebrowser.config import configexc
class _UnsetObject:
"""Sentinel object."""
__slots__ = ()
def __repr__(self):
return '<UNSET>'
UNSET = _UnsetObject()
@attr.s
class ScopedValue:
"""A configuration value which is valid for a UrlPattern.
Attributes:
value: The value itself.
pattern: The UrlPattern for the value, or None for global values.
"""
value = attr.ib()
pattern = attr.ib()
class Values:
"""A collection of values for a single setting.
Currently, this is a list and iterates through all possible ScopedValues to
find matching ones.
In the future, it should be possible to optimize this by doing
pre-selection based on hosts, by making this a dict mapping the
non-wildcard part of the host to a list of matching ScopedValues.
That way, when searching for a setting for sub.example.com, we only have to
check 'sub.example.com', 'example.com', '.com' and '' instead of checking
all ScopedValues for the given setting.
Attributes:
opt: The Option being customized.
"""
def __init__(self, opt, values=None):
self.opt = opt
self._values = values or []
def __repr__(self):
return utils.get_repr(self, opt=self.opt, values=self._values,
constructor=True)
def __str__(self):
"""Get the values as human-readable string."""
if not self:
return '{}: <unchanged>'.format(self.opt.name)
lines = []
for scoped in self._values:
str_value = self.opt.typ.to_str(scoped.value)
if scoped.pattern is None:
lines.append('{} = {}'.format(self.opt.name, str_value))
else:
lines.append('{}: {} = {}'.format(
scoped.pattern, self.opt.name, str_value))
return '\n'.join(lines)
def __iter__(self):
"""Yield ScopedValue elements.
This yields in "normal" order, i.e. global and then first-set settings
first.
"""
yield from self._values
def __bool__(self):
"""Check whether this value is customized."""
return bool(self._values)
def _check_pattern_support(self, arg):
"""Make sure patterns are supported if one was given."""
if arg is not None and not self.opt.supports_pattern:
raise configexc.NoPatternError(self.opt.name)
def add(self, value, pattern=None):
"""Add a value with the given pattern to the list of values."""
self._check_pattern_support(pattern)
self.remove(pattern)
scoped = ScopedValue(value, pattern)
self._values.append(scoped)
def remove(self, pattern=None):
"""Remove the value with the given pattern.
If a matching pattern was removed, True is returned.
If no matching pattern was found, False is returned.
"""
self._check_pattern_support(pattern)
old_len = len(self._values)
self._values = [v for v in self._values if v.pattern != pattern]
return old_len != len(self._values)
def clear(self):
"""Clear all customization for this value."""
self._values = []
def _get_fallback(self, fallback):
"""Get the fallback global/default value."""
for scoped in self._values:
if scoped.pattern is None:
return scoped.value
if fallback:
return self.opt.default
else:
return UNSET
def get_for_url(self, url=None, *, fallback=True):
"""Get a config value, falling back when needed.
This first tries to find a value matching the URL (if given).
If there's no match:
With fallback=True, the global/default setting is returned.
With fallback=False, UNSET is returned.
"""
self._check_pattern_support(url)
if url is not None:
for scoped in reversed(self._values):
if scoped.pattern is not None and scoped.pattern.matches(url):
return scoped.value
if not fallback:
return UNSET
return self._get_fallback(fallback)
def get_for_pattern(self, pattern, *, fallback=True):
"""Get a value only if it's been overridden for the given pattern.
This is useful when showing values to the user.
If there's no match:
With fallback=True, the global/default setting is returned.
With fallback=False, UNSET is returned.
"""
self._check_pattern_support(pattern)
if pattern is not None:
for scoped in reversed(self._values):
if scoped.pattern == pattern:
return scoped.value
if not fallback:
return UNSET
return self._get_fallback(fallback)

View File

@ -17,195 +17,151 @@
# You should have received a copy of the GNU General Public License
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
# We get various "abstract but not overridden" warnings
# pylint: disable=abstract-method
"""Bridge from QWeb(Engine)Settings to our own settings."""
from PyQt5.QtGui import QFont
from qutebrowser.config import config
from qutebrowser.utils import log, utils, debug, usertypes
from qutebrowser.config import config, configutils
from qutebrowser.utils import log, usertypes, urlmatch, qtutils
from qutebrowser.misc import objects
UNSET = object()
class Base:
class AbstractSettings:
"""Base class for QWeb(Engine)Settings wrappers."""
"""Abstract base class for settings set via QWeb(Engine)Settings."""
def __init__(self, default=UNSET):
self._default = default
_ATTRIBUTES = None
_FONT_SIZES = None
_FONT_FAMILIES = None
_FONT_TO_QFONT = None
def _get_global_settings(self):
"""Get a list of global QWeb(Engine)Settings to use."""
raise NotImplementedError
def __init__(self, settings):
self._settings = settings
def _get_settings(self, settings):
"""Get a list of QWeb(Engine)Settings objects to use.
def set_attribute(self, name, value):
"""Set the given QWebSettings/QWebEngineSettings attribute.
Args:
settings: The QWeb(Engine)Settings instance to use, or None to use
the global instance.
If the value is configutils.UNSET, the value is reset instead.
Return:
A list of QWeb(Engine)Settings objects. The first one should be
used for reading.
True if there was a change, False otherwise.
"""
if settings is None:
return self._get_global_settings()
else:
return [settings]
old_value = self.test_attribute(name)
def set(self, value, settings=None):
"""Set the value of this setting.
Args:
value: The value to set, or None to restore the default.
settings: The QWeb(Engine)Settings instance to use, or None to use
the global instance.
"""
if value is None:
self.set_default(settings=settings)
else:
self._set(value, settings=settings)
def set_default(self, settings=None):
"""Set the default value for this setting.
Not implemented for most settings.
"""
if self._default is UNSET:
raise ValueError("No default set for {!r}".format(self))
else:
self._set(self._default, settings=settings)
def _set(self, value, settings):
"""Inner function to set the value of this setting.
Must be overridden by subclasses.
Args:
value: The value to set.
settings: The QWeb(Engine)Settings instance to use, or None to use
the global instance.
"""
raise NotImplementedError
class Attribute(Base):
"""A setting set via QWeb(Engine)Settings::setAttribute.
Attributes:
self._attributes: A list of QWeb(Engine)Settings::WebAttribute members.
"""
ENUM_BASE = None
def __init__(self, *attributes, default=UNSET):
super().__init__(default=default)
self._attributes = list(attributes)
def __repr__(self):
attributes = [debug.qenum_key(self.ENUM_BASE, attr)
for attr in self._attributes]
return utils.get_repr(self, attributes=attributes, constructor=True)
def _set(self, value, settings=None):
for obj in self._get_settings(settings):
for attribute in self._attributes:
obj.setAttribute(attribute, value)
class Setter(Base):
"""A setting set via a QWeb(Engine)Settings setter method.
This will pass the QWeb(Engine)Settings instance ("self") as first argument
to the methods, so self._setter is the *unbound* method.
Attributes:
_setter: The unbound QWeb(Engine)Settings method to set this value.
_args: An iterable of the arguments to pass to the setter (before the
value).
_unpack: Whether to unpack args (True) or pass them directly (False).
"""
def __init__(self, setter, args=(), unpack=False, default=UNSET):
super().__init__(default=default)
self._setter = setter
self._args = args
self._unpack = unpack
def __repr__(self):
return utils.get_repr(self, setter=self._setter, args=self._args,
unpack=self._unpack, constructor=True)
def _set(self, value, settings=None):
for obj in self._get_settings(settings):
args = [obj]
args.extend(self._args)
if self._unpack:
args.extend(value)
for attribute in self._ATTRIBUTES[name]:
if value is configutils.UNSET:
self._settings.resetAttribute(attribute)
new_value = self.test_attribute(name)
else:
args.append(value)
self._setter(*args)
self._settings.setAttribute(attribute, value)
new_value = value
return old_value != new_value
class StaticSetter(Setter):
def test_attribute(self, name):
"""Get the value for the given attribute.
"""A setting set via a static QWeb(Engine)Settings method.
If the setting resolves to a list of attributes, only the first
attribute is tested.
"""
return self._settings.testAttribute(self._ATTRIBUTES[name][0])
self._setter is the *bound* method.
"""
def set_font_size(self, name, value):
"""Set the given QWebSettings/QWebEngineSettings font size.
def _set(self, value, settings=None):
if settings is not None:
raise ValueError("'settings' may not be set with StaticSetters!")
args = list(self._args)
if self._unpack:
args.extend(value)
else:
args.append(value)
self._setter(*args)
Return:
True if there was a change, False otherwise.
"""
assert value is not configutils.UNSET
family = self._FONT_SIZES[name]
old_value = self._settings.fontSize(family)
self._settings.setFontSize(family, value)
return old_value != value
def set_font_family(self, name, value):
"""Set the given QWebSettings/QWebEngineSettings font family.
class FontFamilySetter(Setter):
With None (the default), QFont is used to get the default font for the
family.
"""A setter for a font family.
Return:
True if there was a change, False otherwise.
"""
assert value is not configutils.UNSET
family = self._FONT_FAMILIES[name]
if value is None:
font = QFont()
font.setStyleHint(self._FONT_TO_QFONT[family])
value = font.defaultFamily()
Gets the default value from QFont.
"""
old_value = self._settings.fontFamily(family)
self._settings.setFontFamily(family, value)
def __init__(self, setter, font, qfont):
super().__init__(setter=setter, args=[font])
self._qfont = qfont
return value != old_value
def set_default(self, settings=None):
font = QFont()
font.setStyleHint(self._qfont)
value = font.defaultFamily()
self._set(value, settings=settings)
def set_default_text_encoding(self, encoding):
"""Set the default text encoding to use.
Return:
True if there was a change, False otherwise.
"""
assert encoding is not configutils.UNSET
old_value = self._settings.defaultTextEncoding()
self._settings.setDefaultTextEncoding(encoding)
return old_value != encoding
def init_mappings(mappings):
"""Initialize all settings based on a settings mapping."""
for option, mapping in mappings.items():
value = config.instance.get(option)
log.config.vdebug("Setting {} to {!r}".format(option, value))
mapping.set(value)
def _update_setting(self, setting, value):
"""Update the given setting/value.
Unknown settings are ignored.
def update_mappings(mappings, option):
"""Update global settings when QWeb(Engine)Settings changed."""
try:
mapping = mappings[option]
except KeyError:
return
value = config.instance.get(option)
mapping.set(value)
Return:
True if there was a change, False otherwise.
"""
if setting in self._ATTRIBUTES:
return self.set_attribute(setting, value)
elif setting in self._FONT_SIZES:
return self.set_font_size(setting, value)
elif setting in self._FONT_FAMILIES:
return self.set_font_family(setting, value)
elif setting == 'content.default_encoding':
return self.set_default_text_encoding(value)
return False
def update_setting(self, setting):
"""Update the given setting."""
value = config.instance.get(setting)
self._update_setting(setting, value)
def update_for_url(self, url):
"""Update settings customized for the given tab.
Return:
A set of settings which actually changed.
"""
qtutils.ensure_valid(url)
changed_settings = set()
for values in config.instance:
if not values.opt.supports_pattern:
continue
value = values.get_for_url(url, fallback=False)
changed = self._update_setting(values.opt.name, value)
if changed:
log.config.debug("Changed for {}: {} = {}".format(
url.toDisplayString(), values.opt.name, value))
changed_settings.add(values.opt.name)
return changed_settings
def init_settings(self):
"""Set all supported settings correctly."""
for setting in (list(self._ATTRIBUTES) + list(self._FONT_SIZES) +
list(self._FONT_FAMILIES)):
self.update_setting(setting)
def init(args):
@ -217,6 +173,11 @@ def init(args):
from qutebrowser.browser.webkit import webkitsettings
webkitsettings.init(args)
# Make sure special URLs always get JS support
for pattern in ['file://*', 'chrome://*/*', 'qute://*/*']:
config.instance.set_obj('content.javascript.enabled', True,
pattern=urlmatch.UrlPattern(pattern))
def shutdown():
"""Shut down QWeb(Engine)Settings."""

View File

@ -33,7 +33,7 @@ input { width: 98%; }
<th>Setting</th>
<th>Value</th>
</tr>
{% for option in configdata.DATA.values() %}
{% for option in configdata.DATA.values() if not option.no_autoconfig %}
<tr>
<!-- FIXME: convert to string properly -->
<td class="setting">{{ option.name }} (Current: {{ confget(option.name) | string |truncate(100) }})

View File

@ -0,0 +1,58 @@
{% extends "styled.html" %}
{% block style %}
{{super()}}
h1 {
margin-bottom: 10px;
}
.url a {
color: #444;
}
th {
text-align: left;
}
.qmarks .name {
padding-left: 5px;
}
.empty-msg {
background-color: #f8f8f8;
color: #444;
display: inline-block;
text-align: center;
width: 100%;
}
details {
margin-top: 20px;
}
{% endblock %}
{% block content %}
<h1>Tab list</h1>
{% for win_id, tabs in tab_list_by_window.items() %}
<h2>Window {{ win_id }}</h2>
<table class="tabs_win{{win_id}}">
<tbody>
{% for name, url in tabs %}
<tr>
<td class="name"><a href="{{url}}">{{name}}</a></td>
<td class="url"><a href="{{url}}">{{url}}</a></td>
</tr>
{% endfor %}
</tbody>
</table>
{% endfor %}
<details>
<summary>Raw list</summary>
<code>
{% for win_id, tabs in tab_list_by_window.items() %}{% for name, url in tabs %}
{{url}}</br>{% endfor %}
{% endfor %}
</code>
</details>
{% endblock %}

View File

@ -1,22 +0,0 @@
<!DOCTYPE html>
<!--
vim: ft=html fileencoding=utf-8 sts=4 sw=4 et:
-->
<html>
<head>
<meta charset="utf-8">
<title>Error while rendering HTML</title>
</head>
<body>
<h1>Error while rendering internal qutebrowser page</h1>
<p>There was an error while rendering {pagename}.</p>
<p>This most likely happened because you updated qutebrowser but didn't restart yet.</p>
<p>If you believe this isn't the case and this is a bug, please do :report.<p>
<h2>Traceback</h2>
<pre>{traceback}</pre>
</body>
</html>

View File

@ -110,6 +110,44 @@
}
}
// Stub these two so that the gm4 polyfill script doesn't try to
// create broken versions as attributes of window.
function GM_getResourceText(caption, commandFunc, accessKey) {
console.error(`${GM_info.script.name} called unimplemented GM_getResourceText`);
}
function GM_registerMenuCommand(caption, commandFunc, accessKey) {
console.error(`${GM_info.script.name} called unimplemented GM_registerMenuCommand`);
}
// Mock the greasemonkey 4.0 async API.
const GM = {};
GM.info = GM_info;
const entries = {
'log': GM_log,
'addStyle': GM_addStyle,
'deleteValue': GM_deleteValue,
'getValue': GM_getValue,
'listValues': GM_listValues,
'openInTab': GM_openInTab,
'setValue': GM_setValue,
'xmlHttpRequest': GM_xmlhttpRequest,
}
for (newKey in entries) {
let old = entries[newKey];
if (old && (typeof GM[newKey] == 'undefined')) {
GM[newKey] = function(...args) {
return new Promise((resolve, reject) => {
try {
resolve(old(...args));
} catch (e) {
reject(e);
}
});
};
}
};
const unsafeWindow = window;
// ====== The actual user script source ====== //

View File

@ -74,9 +74,8 @@ window._qutebrowser.webelem = (function() {
try {
return elem.selectionStart;
} catch (err) {
if (err instanceof (frame
? frame.DOMException
: DOMException) &&
if ((err instanceof DOMException ||
(frame && err instanceof frame.DOMException)) &&
err.name === "InvalidStateError") {
// nothing to do, caret_position is already null
} else {
@ -331,13 +330,13 @@ window._qutebrowser.webelem = (function() {
// Function for returning a selection to python (so we can click it)
funcs.find_selected_link = () => {
const elem = window.getSelection().anchorNode;
const elem = window.getSelection().baseNode;
if (elem) {
return serialize_elem(elem.parentNode);
}
const serialized_frame_elem = run_frames((frame) => {
const node = frame.window.getSelection().anchorNode;
const node = frame.window.getSelection().baseNode;
if (node) {
return serialize_elem(node.parentNode, frame);
}

View File

@ -19,14 +19,12 @@
"""Base class for vim-like key sequence parser."""
import enum
import re
import unicodedata
from PyQt5.QtCore import pyqtSignal, QObject
from PyQt5.QtGui import QKeySequence
from qutebrowser.config import config
from qutebrowser.utils import usertypes, log, utils
from qutebrowser.keyinput import keyutils
class BaseKeyParser(QObject):
@ -43,24 +41,16 @@ class BaseKeyParser(QObject):
definitive: Keychain matches exactly.
none: No more matches possible.
Types: type of a key binding.
chain: execute() was called via a chain-like key binding
special: execute() was called via a special key binding
do_log: Whether to log keypresses or not.
passthrough: Whether unbound keys should be passed through with this
handler.
Attributes:
bindings: Bound key bindings
special_bindings: Bound special bindings (<Foo>).
_win_id: The window ID this keyparser is associated with.
_warn_on_keychains: Whether a warning should be logged when binding
keychains in a section which does not support them.
_keystring: The currently entered key sequence
_sequence: The currently entered key sequence
_modename: The name of the input mode associated with this keyparser.
_supports_count: Whether count is supported
_supports_chains: Whether keychains are supported
Signals:
keystring_updated: Emitted when the keystring is updated.
@ -76,27 +66,18 @@ class BaseKeyParser(QObject):
do_log = True
passthrough = False
Match = enum.Enum('Match', ['partial', 'definitive', 'other', 'none'])
Type = enum.Enum('Type', ['chain', 'special'])
def __init__(self, win_id, parent=None, supports_count=None,
supports_chains=False):
def __init__(self, win_id, parent=None, supports_count=True):
super().__init__(parent)
self._win_id = win_id
self._modename = None
self._keystring = ''
if supports_count is None:
supports_count = supports_chains
self._sequence = keyutils.KeySequence()
self._count = ''
self._supports_count = supports_count
self._supports_chains = supports_chains
self._warn_on_keychains = True
self.bindings = {}
self.special_bindings = {}
config.instance.changed.connect(self._on_config_changed)
def __repr__(self):
return utils.get_repr(self, supports_count=self._supports_count,
supports_chains=self._supports_chains)
return utils.get_repr(self, supports_count=self._supports_count)
def _debug_log(self, message):
"""Log a message to the debug log if logging is active.
@ -107,121 +88,11 @@ class BaseKeyParser(QObject):
if self.do_log:
log.keyboard.debug(message)
def _handle_special_key(self, e):
"""Handle a new keypress with special keys (<Foo>).
Return True if the keypress has been handled, and False if not.
Args:
e: the KeyPressEvent from Qt.
Return:
True if event has been handled, False otherwise.
"""
binding = utils.keyevent_to_string(e)
if binding is None:
self._debug_log("Ignoring only-modifier keyeevent.")
return False
if binding not in self.special_bindings:
key_mappings = config.val.bindings.key_mappings
try:
binding = key_mappings['<{}>'.format(binding)][1:-1]
except KeyError:
pass
try:
cmdstr = self.special_bindings[binding]
except KeyError:
self._debug_log("No special binding found for {}.".format(binding))
return False
count, _command = self._split_count(self._keystring)
self.execute(cmdstr, self.Type.special, count)
self.clear_keystring()
return True
def _split_count(self, keystring):
"""Get count and command from the current keystring.
Args:
keystring: The key string to split.
Return:
A (count, command) tuple.
"""
if self._supports_count:
(countstr, cmd_input) = re.fullmatch(r'(\d*)(.*)',
keystring).groups()
count = int(countstr) if countstr else None
if count == 0 and not cmd_input:
cmd_input = keystring
count = None
else:
cmd_input = keystring
count = None
return count, cmd_input
def _handle_single_key(self, e):
"""Handle a new keypress with a single key (no modifiers).
Separate the keypress into count/command, then check if it matches
any possible command, and either run the command, ignore it, or
display an error.
Args:
e: the KeyPressEvent from Qt.
Return:
A self.Match member.
"""
txt = e.text()
key = e.key()
self._debug_log("Got key: 0x{:x} / text: '{}'".format(key, txt))
if len(txt) == 1:
category = unicodedata.category(txt)
is_control_char = (category == 'Cc')
else:
is_control_char = False
if (not txt) or is_control_char:
self._debug_log("Ignoring, no text char")
return self.Match.none
count, cmd_input = self._split_count(self._keystring + txt)
match, binding = self._match_key(cmd_input)
if match == self.Match.none:
mappings = config.val.bindings.key_mappings
mapped = mappings.get(txt, None)
if mapped is not None:
txt = mapped
count, cmd_input = self._split_count(self._keystring + txt)
match, binding = self._match_key(cmd_input)
self._keystring += txt
if match == self.Match.definitive:
self._debug_log("Definitive match for '{}'.".format(
self._keystring))
self.clear_keystring()
self.execute(binding, self.Type.chain, count)
elif match == self.Match.partial:
self._debug_log("No match for '{}' (added {})".format(
self._keystring, txt))
elif match == self.Match.none:
self._debug_log("Giving up with '{}', no matches".format(
self._keystring))
self.clear_keystring()
elif match == self.Match.other:
pass
else:
raise utils.Unreachable("Invalid match value {!r}".format(match))
return match
def _match_key(self, cmd_input):
def _match_key(self, sequence):
"""Try to match a given keystring with any bound keychain.
Args:
cmd_input: The command string to find.
sequence: The command string to find.
Return:
A tuple (matchtype, binding).
@ -229,50 +100,117 @@ class BaseKeyParser(QObject):
binding: - None with Match.partial/Match.none.
- The found binding with Match.definitive.
"""
if not cmd_input:
# Only a count, no command yet, but we handled it
return (self.Match.other, None)
# A (cmd_input, binding) tuple (k, v of bindings) or None.
definitive_match = None
partial_match = False
# Check definitive match
try:
definitive_match = (cmd_input, self.bindings[cmd_input])
except KeyError:
pass
# Check partial match
for binding in self.bindings:
if definitive_match is not None and binding == definitive_match[0]:
# We already matched that one
continue
elif binding.startswith(cmd_input):
partial_match = True
break
if definitive_match is not None:
return (self.Match.definitive, definitive_match[1])
elif partial_match:
return (self.Match.partial, None)
else:
return (self.Match.none, None)
assert sequence
assert not isinstance(sequence, str)
result = QKeySequence.NoMatch
def handle(self, e):
"""Handle a new keypress and call the respective handlers.
for seq, cmd in self.bindings.items():
assert not isinstance(seq, str), seq
match = sequence.matches(seq)
if match == QKeySequence.ExactMatch:
return match, cmd
elif match == QKeySequence.PartialMatch:
result = QKeySequence.PartialMatch
return result, None
def _match_without_modifiers(self, sequence):
"""Try to match a key with optional modifiers stripped."""
self._debug_log("Trying match without modifiers")
sequence = sequence.strip_modifiers()
match, binding = self._match_key(sequence)
return match, binding, sequence
def _match_key_mapping(self, sequence):
"""Try to match a key in bindings.key_mappings."""
self._debug_log("Trying match with key_mappings")
mapped = sequence.with_mappings(config.val.bindings.key_mappings)
if sequence != mapped:
self._debug_log("Mapped {} -> {}".format(
sequence, mapped))
match, binding = self._match_key(mapped)
sequence = mapped
return match, binding, sequence
return QKeySequence.NoMatch, None, sequence
def _match_count(self, sequence, dry_run):
"""Try to match a key as count."""
txt = str(sequence[-1]) # To account for sequences changed above.
if (txt.isdigit() and self._supports_count and
not (not self._count and txt == '0')):
self._debug_log("Trying match as count")
assert len(txt) == 1, txt
if not dry_run:
self._count += txt
self.keystring_updated.emit(self._count + str(self._sequence))
return True
return False
def handle(self, e, *, dry_run=False):
"""Handle a new keypress.
Separate the keypress into count/command, then check if it matches
any possible command, and either run the command, ignore it, or
display an error.
Args:
e: the KeyPressEvent from Qt
e: the KeyPressEvent from Qt.
dry_run: Don't actually execute anything, only check whether there
would be a match.
Return:
True if the event was handled, False otherwise.
A QKeySequence match.
"""
handled = self._handle_special_key(e)
key = e.key()
txt = str(keyutils.KeyInfo.from_event(e))
self._debug_log("Got key: 0x{:x} / modifiers: 0x{:x} / text: '{}' / "
"dry_run {}".format(key, int(e.modifiers()), txt,
dry_run))
if handled or not self._supports_chains:
return handled
match = self._handle_single_key(e)
# don't emit twice if the keystring was cleared in self.clear_keystring
if self._keystring:
self.keystring_updated.emit(self._keystring)
return match != self.Match.none
if keyutils.is_modifier_key(key):
self._debug_log("Ignoring, only modifier")
return QKeySequence.NoMatch
try:
sequence = self._sequence.append_event(e)
except keyutils.KeyParseError as ex:
self._debug_log("{} Aborting keychain.".format(ex))
self.clear_keystring()
return QKeySequence.NoMatch
match, binding = self._match_key(sequence)
if match == QKeySequence.NoMatch:
match, binding, sequence = self._match_without_modifiers(sequence)
if match == QKeySequence.NoMatch:
match, binding, sequence = self._match_key_mapping(sequence)
if match == QKeySequence.NoMatch:
was_count = self._match_count(sequence, dry_run)
if was_count:
return QKeySequence.ExactMatch
if dry_run:
return match
self._sequence = sequence
if match == QKeySequence.ExactMatch:
self._debug_log("Definitive match for '{}'.".format(
sequence))
count = int(self._count) if self._count else None
self.clear_keystring()
self.execute(binding, count)
elif match == QKeySequence.PartialMatch:
self._debug_log("No match for '{}' (added {})".format(
sequence, txt))
self.keystring_updated.emit(self._count + str(sequence))
elif match == QKeySequence.NoMatch:
self._debug_log("Giving up with '{}', no matches".format(
sequence))
self.clear_keystring()
else:
raise utils.Unreachable("Invalid match value {!r}".format(match))
return match
@config.change_filter('bindings')
def _on_config_changed(self):
@ -295,37 +233,26 @@ class BaseKeyParser(QObject):
else:
self._modename = modename
self.bindings = {}
self.special_bindings = {}
for key, cmd in config.key_instance.get_bindings_for(modename).items():
assert not isinstance(key, str), key
assert cmd
self._parse_key_command(modename, key, cmd)
def _parse_key_command(self, modename, key, cmd):
"""Parse the keys and their command and store them in the object."""
if utils.is_special_key(key):
self.special_bindings[key[1:-1]] = cmd
elif self._supports_chains:
self.bindings[key] = cmd
elif self._warn_on_keychains:
log.keyboard.warning("Ignoring keychain '{}' in mode '{}' because "
"keychains are not supported there."
.format(key, modename))
def execute(self, cmdstr, keytype, count=None):
def execute(self, cmdstr, count=None):
"""Handle a completed keychain.
Args:
cmdstr: The command to execute as a string.
keytype: Type.chain or Type.special
count: The count if given.
"""
raise NotImplementedError
def clear_keystring(self):
"""Clear the currently entered key sequence."""
if self._keystring:
self._debug_log("discarding keystring '{}'.".format(
self._keystring))
self._keystring = ''
self.keystring_updated.emit(self._keystring)
if self._sequence:
self._debug_log("Clearing keystring (was: {}).".format(
self._sequence))
self._sequence = keyutils.KeySequence()
self._count = ''
self.keystring_updated.emit('')

View File

@ -1,77 +0,0 @@
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
# Copyright 2014-2018 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/>.
"""Advanced keyparsers."""
import traceback
from qutebrowser.keyinput.basekeyparser import BaseKeyParser
from qutebrowser.utils import message, utils
from qutebrowser.commands import runners, cmdexc
class CommandKeyParser(BaseKeyParser):
"""KeyChainParser for command bindings.
Attributes:
_commandrunner: CommandRunner instance.
"""
def __init__(self, win_id, parent=None, supports_count=None,
supports_chains=False):
super().__init__(win_id, parent, supports_count, supports_chains)
self._commandrunner = runners.CommandRunner(win_id)
def execute(self, cmdstr, _keytype, count=None):
try:
self._commandrunner.run(cmdstr, count)
except cmdexc.Error as e:
message.error(str(e), stack=traceback.format_exc())
class PassthroughKeyParser(CommandKeyParser):
"""KeyChainParser which passes through normal keys.
Used for insert/passthrough modes.
Attributes:
_mode: The mode this keyparser is for.
"""
do_log = False
passthrough = True
def __init__(self, win_id, mode, parent=None, warn=True):
"""Constructor.
Args:
mode: The mode this keyparser is for.
parent: Qt parent.
warn: Whether to warn if an ignored key was bound.
"""
super().__init__(win_id, parent, supports_chains=False)
self._warn_on_keychains = warn
self._read_config(mode)
self._mode = mode
def __repr__(self):
return utils.get_repr(self, mode=self._mode,
warn=self._warn_on_keychains)

View File

@ -0,0 +1,558 @@
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
# Copyright 2014-2018 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/>.
"""Our own QKeySequence-like class and related utilities."""
import itertools
import attr
from PyQt5.QtCore import Qt, QEvent
from PyQt5.QtGui import QKeySequence, QKeyEvent
from qutebrowser.utils import utils
# Map Qt::Key values to their Qt::KeyboardModifier value.
_MODIFIER_MAP = {
Qt.Key_Shift: Qt.ShiftModifier,
Qt.Key_Control: Qt.ControlModifier,
Qt.Key_Alt: Qt.AltModifier,
Qt.Key_Meta: Qt.MetaModifier,
Qt.Key_Mode_switch: Qt.GroupSwitchModifier,
}
def _assert_plain_key(key):
"""Make sure this is a key without KeyboardModifiers mixed in."""
assert not key & Qt.KeyboardModifierMask, hex(key)
def _assert_plain_modifier(key):
"""Make sure this is a modifier without a key mixed in."""
assert not key & ~Qt.KeyboardModifierMask, hex(key)
def _is_printable(key):
_assert_plain_key(key)
return key <= 0xff and key not in [Qt.Key_Space, 0x0]
def is_special(key, modifiers):
"""Check whether this key requires special key syntax."""
_assert_plain_key(key)
_assert_plain_modifier(modifiers)
return not (_is_printable(key) and
modifiers in [Qt.ShiftModifier, Qt.NoModifier])
def is_modifier_key(key):
"""Test whether the given key is a modifier.
This only considers keys which are part of Qt::KeyboardModifiers, i.e.
which would interrupt a key chain like "yY" when handled.
"""
_assert_plain_key(key)
return key in _MODIFIER_MAP
def _check_valid_utf8(s, data):
"""Make sure the given string is valid UTF-8.
Makes sure there are no chars where Qt did fall back to weird UTF-16
surrogates.
"""
try:
s.encode('utf-8')
except UnicodeEncodeError as e: # pragma: no cover
raise ValueError("Invalid encoding in 0x{:x} -> {}: {}"
.format(data, s, e))
def _key_to_string(key):
"""Convert a Qt::Key member to a meaningful name.
Args:
key: A Qt::Key member.
Return:
A name of the key as a string.
"""
_assert_plain_key(key)
special_names_str = {
# Some keys handled in a weird way by QKeySequence::toString.
# See https://bugreports.qt.io/browse/QTBUG-40030
# Most are unlikely to be ever needed, but you never know ;)
# For dead/combining keys, we return the corresponding non-combining
# key, as that's easier to add to the config.
'Super_L': 'Super L',
'Super_R': 'Super R',
'Hyper_L': 'Hyper L',
'Hyper_R': 'Hyper R',
'Direction_L': 'Direction L',
'Direction_R': 'Direction R',
'Shift': 'Shift',
'Control': 'Control',
'Meta': 'Meta',
'Alt': 'Alt',
'AltGr': 'AltGr',
'Multi_key': 'Multi key',
'SingleCandidate': 'Single Candidate',
'Mode_switch': 'Mode switch',
'Dead_Grave': '`',
'Dead_Acute': '´',
'Dead_Circumflex': '^',
'Dead_Tilde': '~',
'Dead_Macron': '¯',
'Dead_Breve': '˘',
'Dead_Abovedot': '˙',
'Dead_Diaeresis': '¨',
'Dead_Abovering': '˚',
'Dead_Doubleacute': '˝',
'Dead_Caron': 'ˇ',
'Dead_Cedilla': '¸',
'Dead_Ogonek': '˛',
'Dead_Iota': 'Iota',
'Dead_Voiced_Sound': 'Voiced Sound',
'Dead_Semivoiced_Sound': 'Semivoiced Sound',
'Dead_Belowdot': 'Belowdot',
'Dead_Hook': 'Hook',
'Dead_Horn': 'Horn',
'Memo': 'Memo',
'ToDoList': 'To Do List',
'Calendar': 'Calendar',
'ContrastAdjust': 'Contrast Adjust',
'LaunchG': 'Launch (G)',
'LaunchH': 'Launch (H)',
'MediaLast': 'Media Last',
'unknown': 'Unknown',
# For some keys, we just want a different name
'Escape': 'Escape',
}
# We now build our real special_names dict from the string mapping above.
# The reason we don't do this directly is that certain Qt versions don't
# have all the keys, so we want to ignore AttributeErrors.
special_names = {}
for k, v in special_names_str.items():
try:
special_names[getattr(Qt, 'Key_' + k)] = v
except AttributeError:
pass
special_names[0x0] = 'nil'
if key in special_names:
return special_names[key]
result = QKeySequence(key).toString()
_check_valid_utf8(result, key)
return result
def _modifiers_to_string(modifiers):
"""Convert the given Qt::KeyboardModifiers to a string.
Handles Qt.GroupSwitchModifier because Qt doesn't handle that as a
modifier.
"""
_assert_plain_modifier(modifiers)
if modifiers & Qt.GroupSwitchModifier:
modifiers &= ~Qt.GroupSwitchModifier
result = 'AltGr+'
else:
result = ''
result += QKeySequence(modifiers).toString()
_check_valid_utf8(result, modifiers)
return result
class KeyParseError(Exception):
"""Raised by _parse_single_key/parse_keystring on parse errors."""
def __init__(self, keystr, error):
if keystr is None:
msg = "Could not parse keystring: {}".format(error)
else:
msg = "Could not parse {!r}: {}".format(keystr, error)
super().__init__(msg)
def _parse_keystring(keystr):
key = ''
special = False
for c in keystr:
if c == '>':
if special:
yield _parse_special_key(key)
key = ''
special = False
else:
yield '>'
assert not key, key
elif c == '<':
special = True
elif special:
key += c
else:
yield _parse_single_key(c)
if special:
yield '<'
for c in key:
yield _parse_single_key(c)
def _parse_special_key(keystr):
"""Normalize a keystring like Ctrl-Q to a keystring like Ctrl+Q.
Args:
keystr: The key combination as a string.
Return:
The normalized keystring.
"""
keystr = keystr.lower()
replacements = (
('control', 'ctrl'),
('windows', 'meta'),
('mod1', 'alt'),
('mod4', 'meta'),
('less', '<'),
('greater', '>'),
)
for (orig, repl) in replacements:
keystr = keystr.replace(orig, repl)
for mod in ['ctrl', 'meta', 'alt', 'shift', 'num']:
keystr = keystr.replace(mod + '-', mod + '+')
return keystr
def _parse_single_key(keystr):
"""Get a keystring for QKeySequence for a single key."""
return 'Shift+' + keystr if keystr.isupper() else keystr
@attr.s
class KeyInfo:
"""A key with optional modifiers.
Attributes:
key: A Qt::Key member.
modifiers: A Qt::KeyboardModifiers enum value.
"""
key = attr.ib()
modifiers = attr.ib()
@classmethod
def from_event(cls, e):
return cls(e.key(), e.modifiers())
def __str__(self):
"""Convert this KeyInfo to a meaningful name.
Return:
A name of the key (combination) as a string.
"""
key_string = _key_to_string(self.key)
modifiers = int(self.modifiers)
if self.key in _MODIFIER_MAP:
# Don't return e.g. <Shift+Shift>
modifiers &= ~_MODIFIER_MAP[self.key]
elif _is_printable(self.key):
# "normal" binding
if not key_string: # pragma: no cover
raise ValueError("Got empty string for key 0x{:x}!"
.format(self.key))
assert len(key_string) == 1, key_string
if self.modifiers == Qt.ShiftModifier:
assert not is_special(self.key, self.modifiers)
return key_string.upper()
elif self.modifiers == Qt.NoModifier:
assert not is_special(self.key, self.modifiers)
return key_string.lower()
else:
# Use special binding syntax, but <Ctrl-a> instead of <Ctrl-A>
key_string = key_string.lower()
# "special" binding
assert is_special(self.key, self.modifiers)
modifier_string = _modifiers_to_string(modifiers)
return '<{}{}>'.format(modifier_string, key_string)
def text(self):
"""Get the text which would be displayed when pressing this key."""
control = {
Qt.Key_Space: ' ',
Qt.Key_Tab: '\t',
Qt.Key_Backspace: '\b',
Qt.Key_Return: '\r',
Qt.Key_Enter: '\r',
Qt.Key_Escape: '\x1b',
}
if self.key in control:
return control[self.key]
elif not _is_printable(self.key):
return ''
text = QKeySequence(self.key).toString()
if not self.modifiers & Qt.ShiftModifier:
text = text.lower()
return text
def to_event(self, typ=QEvent.KeyPress):
"""Get a QKeyEvent from this KeyInfo."""
return QKeyEvent(typ, self.key, self.modifiers, self.text())
def to_int(self):
"""Get the key as an integer (with key/modifiers)."""
return int(self.key) | int(self.modifiers)
class KeySequence:
"""A sequence of key presses.
This internally uses chained QKeySequence objects and exposes a nicer
interface over it.
NOTE: While private members of this class are in theory mutable, they must
not be mutated in order to ensure consistent hashing.
Attributes:
_sequences: A list of QKeySequence
Class attributes:
_MAX_LEN: The maximum amount of keys in a QKeySequence.
"""
_MAX_LEN = 4
def __init__(self, *keys):
self._sequences = []
for sub in utils.chunk(keys, self._MAX_LEN):
sequence = QKeySequence(*sub)
self._sequences.append(sequence)
if keys:
assert self
self._validate()
def __str__(self):
parts = []
for info in self:
parts.append(str(info))
return ''.join(parts)
def __iter__(self):
"""Iterate over KeyInfo objects."""
for key_and_modifiers in self._iter_keys():
key = int(key_and_modifiers) & ~Qt.KeyboardModifierMask
modifiers = Qt.KeyboardModifiers(int(key_and_modifiers) &
Qt.KeyboardModifierMask)
yield KeyInfo(key=key, modifiers=modifiers)
def __repr__(self):
return utils.get_repr(self, keys=str(self))
def __lt__(self, other):
# pylint: disable=protected-access
return self._sequences < other._sequences
def __gt__(self, other):
# pylint: disable=protected-access
return self._sequences > other._sequences
def __le__(self, other):
# pylint: disable=protected-access
return self._sequences <= other._sequences
def __ge__(self, other):
# pylint: disable=protected-access
return self._sequences >= other._sequences
def __eq__(self, other):
# pylint: disable=protected-access
return self._sequences == other._sequences
def __ne__(self, other):
# pylint: disable=protected-access
return self._sequences != other._sequences
def __hash__(self):
return hash(tuple(self._sequences))
def __len__(self):
return sum(len(seq) for seq in self._sequences)
def __bool__(self):
return bool(self._sequences)
def __getitem__(self, item):
if isinstance(item, slice):
keys = list(self._iter_keys())
return self.__class__(*keys[item])
else:
infos = list(self)
return infos[item]
def _iter_keys(self):
return itertools.chain.from_iterable(self._sequences)
def _validate(self, keystr=None):
for info in self:
if info.key < Qt.Key_Space or info.key >= Qt.Key_unknown:
raise KeyParseError(keystr, "Got invalid key!")
for seq in self._sequences:
if not seq:
raise KeyParseError(keystr, "Got invalid key!")
def matches(self, other):
"""Check whether the given KeySequence matches with this one.
We store multiple QKeySequences with <= 4 keys each, so we need to
match those pair-wise, and account for an unequal amount of sequences
as well.
"""
# pylint: disable=protected-access
if len(self._sequences) > len(other._sequences):
# If we entered more sequences than there are in the config,
# there's no way there can be a match.
return QKeySequence.NoMatch
for entered, configured in zip(self._sequences, other._sequences):
# If we get NoMatch/PartialMatch in a sequence, we can abort there.
match = entered.matches(configured)
if match != QKeySequence.ExactMatch:
return match
# We checked all common sequences and they had an ExactMatch.
#
# If there's still more sequences configured than entered, that's a
# PartialMatch, as more keypresses can still follow and new sequences
# will appear which we didn't check above.
#
# If there's the same amount of sequences configured and entered,
# that's an EqualMatch.
if len(self._sequences) == len(other._sequences):
return QKeySequence.ExactMatch
elif len(self._sequences) < len(other._sequences):
return QKeySequence.PartialMatch
else:
raise utils.Unreachable("self={!r} other={!r}".format(self, other))
def append_event(self, ev):
"""Create a new KeySequence object with the given QKeyEvent added."""
key = ev.key()
modifiers = ev.modifiers()
_assert_plain_key(key)
_assert_plain_modifier(modifiers)
if key == 0x0:
raise KeyParseError(None, "Got nil key!")
# We always remove Qt.GroupSwitchModifier because QKeySequence has no
# way to mention that in a binding anyways...
modifiers &= ~Qt.GroupSwitchModifier
# We change Qt.Key_Backtab to Key_Tab here because nobody would
# configure "Shift-Backtab" in their config.
if modifiers & Qt.ShiftModifier and key == Qt.Key_Backtab:
key = Qt.Key_Tab
# We don't care about a shift modifier with symbols (Shift-: should
# match a : binding even though we typed it with a shift on an
# US-keyboard)
#
# However, we *do* care about Shift being involved if we got an
# upper-case letter, as Shift-A should match a Shift-A binding, but not
# an "a" binding.
#
# In addition, Shift also *is* relevant when other modifiers are
# involved. Shift-Ctrl-X should not be equivalent to Ctrl-X.
if (modifiers == Qt.ShiftModifier and
_is_printable(ev.key()) and
not ev.text().isupper()):
modifiers = Qt.KeyboardModifiers()
# On macOS, swap Ctrl and Meta back
# WORKAROUND for https://bugreports.qt.io/browse/QTBUG-51293
if utils.is_mac:
if modifiers & Qt.ControlModifier and modifiers & Qt.MetaModifier:
pass
elif modifiers & Qt.ControlModifier:
modifiers &= ~Qt.ControlModifier
modifiers |= Qt.MetaModifier
elif modifiers & Qt.MetaModifier:
modifiers &= ~Qt.MetaModifier
modifiers |= Qt.ControlModifier
keys = list(self._iter_keys())
keys.append(key | int(modifiers))
return self.__class__(*keys)
def strip_modifiers(self):
"""Strip optional modifiers from keys."""
modifiers = Qt.KeypadModifier
keys = [key & ~modifiers for key in self._iter_keys()]
return self.__class__(*keys)
def with_mappings(self, mappings):
"""Get a new KeySequence with the given mappings applied."""
keys = []
for key in self._iter_keys():
key_seq = KeySequence(key)
if key_seq in mappings:
new_seq = mappings[key_seq]
assert len(new_seq) == 1
key = new_seq[0].to_int()
keys.append(key)
return self.__class__(*keys)
@classmethod
def parse(cls, keystr):
"""Parse a keystring like <Ctrl-x> or xyz and return a KeySequence."""
# pylint: disable=protected-access
new = cls()
strings = list(_parse_keystring(keystr))
for sub in utils.chunk(strings, cls._MAX_LEN):
sequence = QKeySequence(', '.join(sub))
new._sequences.append(sequence)
if keystr:
assert new, keystr
# pylint: disable=protected-access
new._validate(keystr)
return new

View File

@ -25,7 +25,7 @@ import attr
from PyQt5.QtCore import pyqtSlot, pyqtSignal, Qt, QObject, QEvent
from PyQt5.QtWidgets import QApplication
from qutebrowser.keyinput import modeparsers, keyparser
from qutebrowser.keyinput import modeparsers
from qutebrowser.config import config
from qutebrowser.commands import cmdexc, cmdutils
from qutebrowser.utils import usertypes, log, objreg, utils
@ -68,24 +68,30 @@ def init(win_id, parent):
modeman = ModeManager(win_id, parent)
objreg.register('mode-manager', modeman, scope='window', window=win_id)
keyparsers = {
KM.normal: modeparsers.NormalKeyParser(win_id, modeman),
KM.hint: modeparsers.HintKeyParser(win_id, modeman),
KM.insert: keyparser.PassthroughKeyParser(win_id, 'insert', modeman),
KM.passthrough: keyparser.PassthroughKeyParser(win_id, 'passthrough',
modeman),
KM.command: keyparser.PassthroughKeyParser(win_id, 'command', modeman),
KM.prompt: keyparser.PassthroughKeyParser(win_id, 'prompt', modeman,
warn=False),
KM.yesno: modeparsers.PromptKeyParser(win_id, modeman),
KM.caret: modeparsers.CaretKeyParser(win_id, modeman),
KM.set_mark: modeparsers.RegisterKeyParser(win_id, KM.set_mark,
modeman),
KM.jump_mark: modeparsers.RegisterKeyParser(win_id, KM.jump_mark,
modeman),
KM.record_macro: modeparsers.RegisterKeyParser(win_id, KM.record_macro,
modeman),
KM.run_macro: modeparsers.RegisterKeyParser(win_id, KM.run_macro,
modeman),
KM.normal:
modeparsers.NormalKeyParser(win_id, modeman),
KM.hint:
modeparsers.HintKeyParser(win_id, modeman),
KM.insert:
modeparsers.PassthroughKeyParser(win_id, 'insert', modeman),
KM.passthrough:
modeparsers.PassthroughKeyParser(win_id, 'passthrough', modeman),
KM.command:
modeparsers.PassthroughKeyParser(win_id, 'command', modeman),
KM.prompt:
modeparsers.PassthroughKeyParser(win_id, 'prompt', modeman),
KM.yesno:
modeparsers.PromptKeyParser(win_id, modeman),
KM.caret:
modeparsers.CaretKeyParser(win_id, modeman),
KM.set_mark:
modeparsers.RegisterKeyParser(win_id, KM.set_mark, modeman),
KM.jump_mark:
modeparsers.RegisterKeyParser(win_id, KM.jump_mark, modeman),
KM.record_macro:
modeparsers.RegisterKeyParser(win_id, KM.record_macro, modeman),
KM.run_macro:
modeparsers.RegisterKeyParser(win_id, KM.run_macro, modeman),
}
objreg.register('keyparsers', keyparsers, scope='window', window=win_id)
modeman.destroyed.connect(
@ -149,11 +155,12 @@ class ModeManager(QObject):
def __repr__(self):
return utils.get_repr(self, mode=self.mode)
def _eventFilter_keypress(self, event):
def _handle_keypress(self, event, *, dry_run=False):
"""Handle filtering of KeyPress events.
Args:
event: The KeyPress to examine.
dry_run: Don't actually handle the key, only filter it.
Return:
True if event should be filtered, False otherwise.
@ -163,7 +170,7 @@ class ModeManager(QObject):
if curmode != usertypes.KeyMode.insert:
log.modes.debug("got keypress in mode {} - delegating to "
"{}".format(curmode, utils.qualname(parser)))
handled = parser.handle(event)
match = parser.handle(event, dry_run=dry_run)
is_non_alnum = (
event.modifiers() not in [Qt.NoModifier, Qt.ShiftModifier] or
@ -171,7 +178,7 @@ class ModeManager(QObject):
forward_unbound_keys = config.val.input.forward_unbound_keys
if handled:
if match:
filter_this = True
elif (parser.passthrough or forward_unbound_keys == 'all' or
(forward_unbound_keys == 'auto' and is_non_alnum)):
@ -179,20 +186,20 @@ class ModeManager(QObject):
else:
filter_this = True
if not filter_this:
if not filter_this and not dry_run:
self._releaseevents_to_pass.add(KeyEvent.from_event(event))
if curmode != usertypes.KeyMode.insert:
focus_widget = QApplication.instance().focusWidget()
log.modes.debug("handled: {}, forward_unbound_keys: {}, "
"passthrough: {}, is_non_alnum: {} --> "
"filter: {} (focused: {!r})".format(
handled, forward_unbound_keys,
parser.passthrough, is_non_alnum, filter_this,
focus_widget))
log.modes.debug("match: {}, forward_unbound_keys: {}, "
"passthrough: {}, is_non_alnum: {}, dry_run: {} "
"--> filter: {} (focused: {!r})".format(
match, forward_unbound_keys,
parser.passthrough, is_non_alnum, dry_run,
filter_this, focus_widget))
return filter_this
def _eventFilter_keyrelease(self, event):
def _handle_keyrelease(self, event):
"""Handle filtering of KeyRelease events.
Args:
@ -315,7 +322,7 @@ class ModeManager(QObject):
raise ValueError("Can't leave normal mode!")
self.leave(self.mode, 'leave current')
def eventFilter(self, event):
def handle_event(self, event):
"""Filter all events based on the currently set mode.
Also calls the real keypress handler.
@ -331,8 +338,10 @@ class ModeManager(QObject):
return False
handlers = {
QEvent.KeyPress: self._eventFilter_keypress,
QEvent.KeyRelease: self._eventFilter_keyrelease,
QEvent.KeyPress: self._handle_keypress,
QEvent.KeyRelease: self._handle_keyrelease,
QEvent.ShortcutOverride:
functools.partial(self._handle_keypress, dry_run=True),
}
handler = handlers[event.type()]
return handler(event)

View File

@ -27,10 +27,11 @@ import traceback
import enum
from PyQt5.QtCore import pyqtSlot, Qt
from PyQt5.QtGui import QKeySequence
from qutebrowser.commands import cmdexc
from qutebrowser.commands import runners, cmdexc
from qutebrowser.config import config
from qutebrowser.keyinput import keyparser
from qutebrowser.keyinput import basekeyparser, keyutils
from qutebrowser.utils import usertypes, log, message, objreg, utils
@ -38,7 +39,26 @@ STARTCHARS = ":/?"
LastPress = enum.Enum('LastPress', ['none', 'filtertext', 'keystring'])
class NormalKeyParser(keyparser.CommandKeyParser):
class CommandKeyParser(basekeyparser.BaseKeyParser):
"""KeyChainParser for command bindings.
Attributes:
_commandrunner: CommandRunner instance.
"""
def __init__(self, win_id, parent=None, supports_count=None):
super().__init__(win_id, parent, supports_count)
self._commandrunner = runners.CommandRunner(win_id)
def execute(self, cmdstr, count=None):
try:
self._commandrunner.run(cmdstr, count)
except cmdexc.Error as e:
message.error(str(e), stack=traceback.format_exc())
class NormalKeyParser(CommandKeyParser):
"""KeyParser for normal mode with added STARTCHARS detection and more.
@ -47,8 +67,7 @@ class NormalKeyParser(keyparser.CommandKeyParser):
"""
def __init__(self, win_id, parent=None):
super().__init__(win_id, parent, supports_count=True,
supports_chains=True)
super().__init__(win_id, parent, supports_count=True)
self._read_config('normal')
self._partial_timer = usertypes.Timer(self, 'partial-match')
self._partial_timer.setSingleShot(True)
@ -59,11 +78,13 @@ class NormalKeyParser(keyparser.CommandKeyParser):
def __repr__(self):
return utils.get_repr(self)
def _handle_single_key(self, e):
"""Override _handle_single_key to abort if the key is a startchar.
def handle(self, e, *, dry_run=False):
"""Override to abort if the key is a startchar.
Args:
e: the KeyPressEvent from Qt.
dry_run: Don't actually execute anything, only check whether there
would be a match.
Return:
A self.Match member.
@ -72,9 +93,11 @@ class NormalKeyParser(keyparser.CommandKeyParser):
if self._inhibited:
self._debug_log("Ignoring key '{}', because the normal mode is "
"currently inhibited.".format(txt))
return self.Match.none
match = super()._handle_single_key(e)
if match == self.Match.partial:
return QKeySequence.NoMatch
match = super().handle(e, dry_run=dry_run)
if match == QKeySequence.PartialMatch and not dry_run:
timeout = config.val.input.partial_timeout
if timeout != 0:
self._partial_timer.setInterval(timeout)
@ -96,9 +119,9 @@ class NormalKeyParser(keyparser.CommandKeyParser):
def _clear_partial_match(self):
"""Clear a partial keystring after a timeout."""
self._debug_log("Clearing partial keystring {}".format(
self._keystring))
self._keystring = ''
self.keystring_updated.emit(self._keystring)
self._sequence))
self._sequence = keyutils.KeySequence()
self.keystring_updated.emit(str(self._sequence))
@pyqtSlot()
def _clear_inhibited(self):
@ -123,22 +146,48 @@ class NormalKeyParser(keyparser.CommandKeyParser):
pass
class PromptKeyParser(keyparser.CommandKeyParser):
class PassthroughKeyParser(CommandKeyParser):
"""KeyChainParser which passes through normal keys.
Used for insert/passthrough modes.
Attributes:
_mode: The mode this keyparser is for.
"""
do_log = False
passthrough = True
def __init__(self, win_id, mode, parent=None):
"""Constructor.
Args:
mode: The mode this keyparser is for.
parent: Qt parent.
warn: Whether to warn if an ignored key was bound.
"""
super().__init__(win_id, parent)
self._read_config(mode)
self._mode = mode
def __repr__(self):
return utils.get_repr(self, mode=self._mode)
class PromptKeyParser(CommandKeyParser):
"""KeyParser for yes/no prompts."""
def __init__(self, win_id, parent=None):
super().__init__(win_id, parent, supports_count=False,
supports_chains=True)
# We don't want an extra section for this in the config, so we just
# abuse the prompt section.
self._read_config('prompt')
super().__init__(win_id, parent, supports_count=False)
self._read_config('yesno')
def __repr__(self):
return utils.get_repr(self)
class HintKeyParser(keyparser.CommandKeyParser):
class HintKeyParser(CommandKeyParser):
"""KeyChainParser for hints.
@ -148,15 +197,14 @@ class HintKeyParser(keyparser.CommandKeyParser):
"""
def __init__(self, win_id, parent=None):
super().__init__(win_id, parent, supports_count=False,
supports_chains=True)
super().__init__(win_id, parent, supports_count=False)
self._filtertext = ''
self._last_press = LastPress.none
self._read_config('hint')
self.keystring_updated.connect(self.on_keystring_updated)
def _handle_special_key(self, e):
"""Override _handle_special_key to handle string filtering.
def _handle_filter_key(self, e):
"""Handle keys for string filtering.
Return True if the keypress has been handled, and False if not.
@ -164,78 +212,75 @@ class HintKeyParser(keyparser.CommandKeyParser):
e: the KeyPressEvent from Qt.
Return:
True if event has been handled, False otherwise.
A QKeySequence match.
"""
log.keyboard.debug("Got special key 0x{:x} text {}".format(
log.keyboard.debug("Got filter key 0x{:x} text {}".format(
e.key(), e.text()))
hintmanager = objreg.get('hintmanager', scope='tab',
window=self._win_id, tab='current')
if e.key() == Qt.Key_Backspace:
log.keyboard.debug("Got backspace, mode {}, filtertext '{}', "
"keystring '{}'".format(self._last_press,
self._filtertext,
self._keystring))
"sequence '{}'".format(self._last_press,
self._filtertext,
self._sequence))
if self._last_press == LastPress.filtertext and self._filtertext:
self._filtertext = self._filtertext[:-1]
hintmanager.filter_hints(self._filtertext)
return True
elif self._last_press == LastPress.keystring and self._keystring:
self._keystring = self._keystring[:-1]
self.keystring_updated.emit(self._keystring)
if not self._keystring and self._filtertext:
return QKeySequence.ExactMatch
elif self._last_press == LastPress.keystring and self._sequence:
self._sequence = self._sequence[:-1]
self.keystring_updated.emit(str(self._sequence))
if not self._sequence and self._filtertext:
# Switch back to hint filtering mode (this can happen only
# in numeric mode after the number has been deleted).
hintmanager.filter_hints(self._filtertext)
self._last_press = LastPress.filtertext
return True
return QKeySequence.ExactMatch
else:
return super()._handle_special_key(e)
return QKeySequence.NoMatch
elif hintmanager.current_mode() != 'number':
return super()._handle_special_key(e)
return QKeySequence.NoMatch
elif not e.text():
return super()._handle_special_key(e)
return QKeySequence.NoMatch
else:
self._filtertext += e.text()
hintmanager.filter_hints(self._filtertext)
self._last_press = LastPress.filtertext
return True
return QKeySequence.ExactMatch
def handle(self, e):
def handle(self, e, *, dry_run=False):
"""Handle a new keypress and call the respective handlers.
Args:
e: the KeyPressEvent from Qt
dry_run: Don't actually execute anything, only check whether there
would be a match.
Returns:
True if the match has been handled, False otherwise.
"""
match = self._handle_single_key(e)
if match == self.Match.partial:
self.keystring_updated.emit(self._keystring)
dry_run_match = super().handle(e, dry_run=True)
if dry_run:
return dry_run_match
if keyutils.is_special(e.key(), e.modifiers()):
log.keyboard.debug("Got special key, clearing keychain")
self.clear_keystring()
assert not dry_run
match = super().handle(e)
if match == QKeySequence.PartialMatch:
self._last_press = LastPress.keystring
return True
elif match == self.Match.definitive:
elif match == QKeySequence.ExactMatch:
self._last_press = LastPress.none
return True
elif match == self.Match.other:
return None
elif match == self.Match.none:
elif match == QKeySequence.NoMatch:
# We couldn't find a keychain so we check if it's a special key.
return self._handle_special_key(e)
return self._handle_filter_key(e)
else:
raise ValueError("Got invalid match type {}!".format(match))
def execute(self, cmdstr, keytype, count=None):
"""Handle a completed keychain."""
if not isinstance(keytype, self.Type):
raise TypeError("Type {} is no Type member!".format(keytype))
if keytype == self.Type.chain:
hintmanager = objreg.get('hintmanager', scope='tab',
window=self._win_id, tab='current')
hintmanager.handle_partial_key(cmdstr)
else:
# execute as command
super().execute(cmdstr, keytype, count)
return match
def update_bindings(self, strings, preserve_filter=False):
"""Update bindings when the hint strings changed.
@ -245,7 +290,9 @@ class HintKeyParser(keyparser.CommandKeyParser):
preserve_filter: Whether to keep the current value of
`self._filtertext`.
"""
self.bindings = {s: s for s in strings}
self._read_config()
self.bindings.update({keyutils.KeySequence.parse(s):
'follow-hint -s ' + s for s in strings})
if not preserve_filter:
self._filtertext = ''
@ -257,19 +304,18 @@ class HintKeyParser(keyparser.CommandKeyParser):
hintmanager.handle_partial_key(keystr)
class CaretKeyParser(keyparser.CommandKeyParser):
class CaretKeyParser(CommandKeyParser):
"""KeyParser for caret mode."""
passthrough = True
def __init__(self, win_id, parent=None):
super().__init__(win_id, parent, supports_count=True,
supports_chains=True)
super().__init__(win_id, parent, supports_count=True)
self._read_config('caret')
class RegisterKeyParser(keyparser.CommandKeyParser):
class RegisterKeyParser(CommandKeyParser):
"""KeyParser for modes that record a register key.
@ -279,29 +325,31 @@ class RegisterKeyParser(keyparser.CommandKeyParser):
"""
def __init__(self, win_id, mode, parent=None):
super().__init__(win_id, parent, supports_count=False,
supports_chains=False)
super().__init__(win_id, parent, supports_count=False)
self._mode = mode
self._read_config('register')
def handle(self, e):
def handle(self, e, *, dry_run=False):
"""Override handle to always match the next key and use the register.
Args:
e: the KeyPressEvent from Qt.
dry_run: Don't actually execute anything, only check whether there
would be a match.
Return:
True if event has been handled, False otherwise.
"""
if super().handle(e):
return True
match = super().handle(e, dry_run=dry_run)
if match or dry_run:
return match
if keyutils.is_special(e.key(), e.modifiers()):
# this is not a proper register key, let it pass and keep going
return QKeySequence.NoMatch
key = e.text()
if key == '' or utils.keyevent_to_string(e) is None:
# this is not a proper register key, let it pass and keep going
return False
tabbed_browser = objreg.get('tabbed-browser', scope='window',
window=self._win_id)
macro_recorder = objreg.get('macro-recorder')
@ -322,5 +370,4 @@ class RegisterKeyParser(keyparser.CommandKeyParser):
message.error(str(err), stack=traceback.format_exc())
self.request_leave.emit(self._mode, "valid register key", True)
return True
return QKeySequence.ExactMatch

View File

@ -327,7 +327,7 @@ class MainWindow(QWidget):
self.tabbed_browser)
objreg.register('command-dispatcher', dispatcher, scope='window',
window=self.win_id)
self.tabbed_browser.destroyed.connect(
self.tabbed_browser.widget.destroyed.connect(
functools.partial(objreg.delete, 'command-dispatcher',
scope='window', window=self.win_id))
@ -347,10 +347,10 @@ class MainWindow(QWidget):
def _add_widgets(self):
"""Add or readd all widgets to the VBox."""
self._vbox.removeWidget(self.tabbed_browser)
self._vbox.removeWidget(self.tabbed_browser.widget)
self._vbox.removeWidget(self._downloadview)
self._vbox.removeWidget(self.status)
widgets = [self.tabbed_browser]
widgets = [self.tabbed_browser.widget]
downloads_position = config.val.downloads.position
if downloads_position == 'top':
@ -469,7 +469,7 @@ class MainWindow(QWidget):
self.tabbed_browser.cur_scroll_perc_changed.connect(
status.percentage.set_perc)
self.tabbed_browser.tab_index_changed.connect(
self.tabbed_browser.widget.tab_index_changed.connect(
status.tabindex.on_tab_index_changed)
self.tabbed_browser.cur_url_changed.connect(status.url.set_url)
@ -518,7 +518,7 @@ class MainWindow(QWidget):
super().resizeEvent(e)
self._update_overlay_geometries()
self._downloadview.updateGeometry()
self.tabbed_browser.tabBar().refresh()
self.tabbed_browser.widget.tabBar().refresh()
def showEvent(self, e):
"""Extend showEvent to register us as the last-visible-main-window.
@ -547,7 +547,7 @@ class MainWindow(QWidget):
if crashsignal.is_crashing:
e.accept()
return
tab_count = self.tabbed_browser.count()
tab_count = self.tabbed_browser.widget.count()
download_model = objreg.get('download-model', scope='window',
window=self.win_id)
download_count = download_model.running_downloads()

View File

@ -507,8 +507,8 @@ class _BasePrompt(QWidget):
self._key_grid = QGridLayout()
self._key_grid.setVerticalSpacing(0)
# The bindings are all in the 'prompt' mode, even for yesno prompts
all_bindings = config.key_instance.get_reverse_bindings_for('prompt')
all_bindings = config.key_instance.get_reverse_bindings_for(
self.KEY_MODE.name)
labels = []
for cmd, text in self._allowed_commands():
@ -596,6 +596,8 @@ class FilenamePrompt(_BasePrompt):
if config.val.prompt.filebrowser:
self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred)
self._to_complete = ''
@pyqtSlot(str)
def _set_fileview_root(self, path, *, tabbed=False):
"""Set the root path for the file display."""
@ -604,6 +606,9 @@ class FilenamePrompt(_BasePrompt):
separators += os.altsep
dirname = os.path.dirname(path)
basename = os.path.basename(path)
if not tabbed:
self._to_complete = ''
try:
if not path:
@ -617,6 +622,7 @@ class FilenamePrompt(_BasePrompt):
elif os.path.isdir(dirname) and not tabbed:
# Input like /foo/ba -> show /foo contents
path = dirname
self._to_complete = basename
else:
return
except OSError:
@ -634,7 +640,11 @@ class FilenamePrompt(_BasePrompt):
index: The QModelIndex of the selected element.
clicked: Whether the element was clicked.
"""
path = os.path.normpath(self._file_model.filePath(index))
if index == QModelIndex():
path = os.path.join(self._file_model.rootPath(), self._to_complete)
else:
path = os.path.normpath(self._file_model.filePath(index))
if clicked:
path += os.sep
else:
@ -696,6 +706,7 @@ class FilenamePrompt(_BasePrompt):
assert last_index.isValid()
idx = selmodel.currentIndex()
if not idx.isValid():
# No item selected yet
idx = last_index if which == 'prev' else first_index
@ -709,10 +720,24 @@ class FilenamePrompt(_BasePrompt):
if not idx.isValid():
idx = last_index if which == 'prev' else first_index
idx = self._do_completion(idx, which)
selmodel.setCurrentIndex(
idx, QItemSelectionModel.ClearAndSelect | QItemSelectionModel.Rows)
self._insert_path(idx, clicked=False)
def _do_completion(self, idx, which):
filename = self._file_model.fileName(idx)
while not filename.startswith(self._to_complete) and idx.isValid():
if which == 'prev':
idx = self._file_view.indexAbove(idx)
else:
assert which == 'next', which
idx = self._file_view.indexBelow(idx)
filename = self._file_model.fileName(idx)
return idx
def _allowed_commands(self):
return [('prompt-accept', 'Accept'), ('leave-mode', 'Abort')]

View File

@ -32,7 +32,7 @@ class Backforward(textbase.TextBase):
def on_tab_cur_url_changed(self, tabs):
"""Called on URL changes."""
tab = tabs.currentWidget()
tab = tabs.widget.currentWidget()
if tab is None: # pragma: no cover
self.setText('')
self.hide()

View File

@ -268,7 +268,7 @@ class StatusBar(QWidget):
"""Get the currently displayed tab."""
window = objreg.get('tabbed-browser', scope='window',
window=self._win_id)
return window.currentWidget()
return window.widget.currentWidget()
def set_mode_active(self, mode, val):
"""Setter for self.{insert,command,caret}_active.

View File

@ -22,7 +22,7 @@
import functools
import attr
from PyQt5.QtWidgets import QSizePolicy
from PyQt5.QtWidgets import QSizePolicy, QWidget
from PyQt5.QtCore import pyqtSignal, pyqtSlot, QTimer, QUrl
from PyQt5.QtGui import QIcon
@ -50,7 +50,7 @@ class TabDeletedError(Exception):
"""Exception raised when _tab_index is called for a deleted tab."""
class TabbedBrowser(tabwidget.TabWidget):
class TabbedBrowser(QWidget):
"""A TabWidget with QWebViews inside.
@ -110,17 +110,18 @@ class TabbedBrowser(tabwidget.TabWidget):
new_tab = pyqtSignal(browsertab.AbstractTab, int)
def __init__(self, *, win_id, private, parent=None):
super().__init__(win_id, parent)
super().__init__(parent)
self.widget = tabwidget.TabWidget(win_id, parent=self)
self._win_id = win_id
self._tab_insert_idx_left = 0
self._tab_insert_idx_right = -1
self.shutting_down = False
self.tabCloseRequested.connect(self.on_tab_close_requested)
self.new_tab_requested.connect(self.tabopen)
self.currentChanged.connect(self.on_current_changed)
self.widget.tabCloseRequested.connect(self.on_tab_close_requested)
self.widget.new_tab_requested.connect(self.tabopen)
self.widget.currentChanged.connect(self.on_current_changed)
self.cur_load_started.connect(self.on_cur_load_started)
self.cur_fullscreen_requested.connect(self.tabBar().maybe_hide)
self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
self.cur_fullscreen_requested.connect(self.widget.tabBar().maybe_hide)
self.widget.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
self._undo_stack = []
self._filter = signalfilter.SignalFilter(win_id, self)
self._now_focused = None
@ -128,12 +129,12 @@ class TabbedBrowser(tabwidget.TabWidget):
self.search_options = {}
self._local_marks = {}
self._global_marks = {}
self.default_window_icon = self.window().windowIcon()
self.default_window_icon = self.widget.window().windowIcon()
self.private = private
config.instance.changed.connect(self._on_config_changed)
def __repr__(self):
return utils.get_repr(self, count=self.count())
return utils.get_repr(self, count=self.widget.count())
@pyqtSlot(str)
def _on_config_changed(self, option):
@ -142,7 +143,7 @@ class TabbedBrowser(tabwidget.TabWidget):
elif option == 'window.title_format':
self._update_window_title()
elif option in ['tabs.title.format', 'tabs.title.format_pinned']:
self._update_tab_titles()
self.widget.update_tab_titles()
def _tab_index(self, tab):
"""Get the index of a given tab.
@ -150,7 +151,7 @@ class TabbedBrowser(tabwidget.TabWidget):
Raises TabDeletedError if the tab doesn't exist anymore.
"""
try:
idx = self.indexOf(tab)
idx = self.widget.indexOf(tab)
except RuntimeError as e:
log.webview.debug("Got invalid tab ({})!".format(e))
raise TabDeletedError(e)
@ -166,8 +167,8 @@ class TabbedBrowser(tabwidget.TabWidget):
iterating over the list.
"""
widgets = []
for i in range(self.count()):
widget = self.widget(i)
for i in range(self.widget.count()):
widget = self.widget.widget(i)
if widget is None:
log.webview.debug("Got None-widget in tabbedbrowser!")
else:
@ -186,16 +187,16 @@ class TabbedBrowser(tabwidget.TabWidget):
if field is not None and ('{' + field + '}') not in title_format:
return
idx = self.currentIndex()
idx = self.widget.currentIndex()
if idx == -1:
# (e.g. last tab removed)
log.webview.debug("Not updating window title because index is -1")
return
fields = self.get_tab_fields(idx)
fields = self.widget.get_tab_fields(idx)
fields['id'] = self._win_id
title = title_format.format(**fields)
self.window().setWindowTitle(title)
self.widget.window().setWindowTitle(title)
def _connect_tab_signals(self, tab):
"""Set up the needed signals for tab."""
@ -247,8 +248,8 @@ class TabbedBrowser(tabwidget.TabWidget):
Return:
The current URL as QUrl.
"""
idx = self.currentIndex()
return super().tab_url(idx)
idx = self.widget.currentIndex()
return self.widget.tab_url(idx)
def shutdown(self):
"""Try to shut down all tabs cleanly."""
@ -284,7 +285,7 @@ class TabbedBrowser(tabwidget.TabWidget):
new_undo: Whether the undo entry should be a new item in the stack.
"""
last_close = config.val.tabs.last_close
count = self.count()
count = self.widget.count()
if last_close == 'ignore' and count == 1:
return
@ -311,7 +312,7 @@ class TabbedBrowser(tabwidget.TabWidget):
new_undo: Whether the undo entry should be a new item in the stack.
crashed: Whether we're closing a tab with crashed renderer process.
"""
idx = self.indexOf(tab)
idx = self.widget.indexOf(tab)
if idx == -1:
if crashed:
return
@ -349,7 +350,7 @@ class TabbedBrowser(tabwidget.TabWidget):
self._undo_stack[-1].append(entry)
tab.shutdown()
self.removeTab(idx)
self.widget.removeTab(idx)
if not crashed:
# WORKAROUND for a segfault when we delete the crashed tab.
# see https://bugreports.qt.io/browse/QTBUG-58698
@ -362,14 +363,14 @@ class TabbedBrowser(tabwidget.TabWidget):
last_close = config.val.tabs.last_close
use_current_tab = False
if last_close in ['blank', 'startpage', 'default-page']:
only_one_tab_open = self.count() == 1
no_history = len(self.widget(0).history) == 1
only_one_tab_open = self.widget.count() == 1
no_history = len(self.widget.widget(0).history) == 1
urls = {
'blank': QUrl('about:blank'),
'startpage': config.val.url.start_pages[0],
'default-page': config.val.url.default_page,
}
first_tab_url = self.widget(0).url()
first_tab_url = self.widget.widget(0).url()
last_close_urlstr = urls[last_close].toString().rstrip('/')
first_tab_urlstr = first_tab_url.toString().rstrip('/')
last_close_url_used = first_tab_urlstr == last_close_urlstr
@ -378,15 +379,13 @@ class TabbedBrowser(tabwidget.TabWidget):
for entry in reversed(self._undo_stack.pop()):
if use_current_tab:
self.openurl(entry.url, newtab=False)
newtab = self.widget(0)
newtab = self.widget.widget(0)
use_current_tab = False
else:
newtab = self.tabopen(entry.url, background=False,
idx=entry.index)
newtab = self.tabopen(background=False, idx=entry.index)
newtab.history.deserialize(entry.history)
self.set_tab_pinned(newtab, entry.pinned)
self.widget.set_tab_pinned(newtab, entry.pinned)
@pyqtSlot('QUrl', bool)
def openurl(self, url, newtab):
@ -397,15 +396,15 @@ class TabbedBrowser(tabwidget.TabWidget):
newtab: True to open URL in a new tab, False otherwise.
"""
qtutils.ensure_valid(url)
if newtab or self.currentWidget() is None:
if newtab or self.widget.currentWidget() is None:
self.tabopen(url, background=False)
else:
self.currentWidget().openurl(url)
self.widget.currentWidget().openurl(url)
@pyqtSlot(int)
def on_tab_close_requested(self, idx):
"""Close a tab via an index."""
tab = self.widget(idx)
tab = self.widget.widget(idx)
if tab is None:
log.webview.debug("Got invalid tab {} for index {}!".format(
tab, idx))
@ -456,7 +455,7 @@ class TabbedBrowser(tabwidget.TabWidget):
"related {}, idx {}".format(
url, background, related, idx))
if (config.val.tabs.tabs_are_windows and self.count() > 0 and
if (config.val.tabs.tabs_are_windows and self.widget.count() > 0 and
not ignore_tabs_are_windows):
window = mainwindow.MainWindow(private=self.private)
window.show()
@ -466,12 +465,12 @@ class TabbedBrowser(tabwidget.TabWidget):
related=related)
tab = browsertab.create(win_id=self._win_id, private=self.private,
parent=self)
parent=self.widget)
self._connect_tab_signals(tab)
if idx is None:
idx = self._get_new_tab_idx(related)
self.insertTab(idx, tab, "")
self.widget.insertTab(idx, tab, "")
if url is not None:
tab.openurl(url)
@ -482,10 +481,11 @@ class TabbedBrowser(tabwidget.TabWidget):
# Make sure the background tab has the correct initial size.
# With a foreground tab, it's going to be resized correctly by the
# layout anyways.
tab.resize(self.currentWidget().size())
self.tab_index_changed.emit(self.currentIndex(), self.count())
tab.resize(self.widget.currentWidget().size())
self.widget.tab_index_changed.emit(self.widget.currentIndex(),
self.widget.count())
else:
self.setCurrentWidget(tab)
self.widget.setCurrentWidget(tab)
tab.show()
self.new_tab.emit(tab, idx)
@ -530,13 +530,14 @@ class TabbedBrowser(tabwidget.TabWidget):
"""Update favicons when config was changed."""
for i, tab in enumerate(self.widgets()):
if config.val.tabs.favicons.show:
self.setTabIcon(i, tab.icon())
self.widget.setTabIcon(i, tab.icon())
if config.val.tabs.tabs_are_windows:
self.window().setWindowIcon(tab.icon())
self.widget.window().setWindowIcon(tab.icon())
else:
self.setTabIcon(i, QIcon())
self.widget.setTabIcon(i, QIcon())
if config.val.tabs.tabs_are_windows:
self.window().setWindowIcon(self.default_window_icon)
window = self.widget.window()
window.setWindowIcon(self.default_window_icon)
@pyqtSlot()
def on_load_started(self, tab):
@ -550,15 +551,14 @@ class TabbedBrowser(tabwidget.TabWidget):
except TabDeletedError:
# We can get signals for tabs we already deleted...
return
self._update_tab_title(idx)
self.widget.update_tab_title(idx)
if tab.data.keep_icon:
tab.data.keep_icon = False
else:
self.setTabIcon(idx, QIcon())
if (config.val.tabs.tabs_are_windows and
config.val.tabs.favicons.show):
self.window().setWindowIcon(self.default_window_icon)
if idx == self.currentIndex():
self.widget.window().setWindowIcon(self.default_window_icon)
if idx == self.widget.currentIndex():
self._update_window_title()
@pyqtSlot()
@ -589,8 +589,8 @@ class TabbedBrowser(tabwidget.TabWidget):
return
log.webview.debug("Changing title for idx {} to '{}'".format(
idx, text))
self.set_page_title(idx, text)
if idx == self.currentIndex():
self.widget.set_page_title(idx, text)
if idx == self.widget.currentIndex():
self._update_window_title()
@pyqtSlot(browsertab.AbstractTab, QUrl)
@ -607,8 +607,8 @@ class TabbedBrowser(tabwidget.TabWidget):
# We can get signals for tabs we already deleted...
return
if not self.page_title(idx):
self.set_page_title(idx, url.toDisplayString())
if not self.widget.page_title(idx):
self.widget.set_page_title(idx, url.toDisplayString())
@pyqtSlot(browsertab.AbstractTab, QIcon)
def on_icon_changed(self, tab, icon):
@ -627,23 +627,23 @@ class TabbedBrowser(tabwidget.TabWidget):
except TabDeletedError:
# We can get signals for tabs we already deleted...
return
self.setTabIcon(idx, icon)
self.widget.setTabIcon(idx, icon)
if config.val.tabs.tabs_are_windows:
self.window().setWindowIcon(icon)
self.widget.window().setWindowIcon(icon)
@pyqtSlot(usertypes.KeyMode)
def on_mode_entered(self, mode):
"""Save input mode when tabs.mode_on_change = restore."""
if (config.val.tabs.mode_on_change == 'restore' and
mode in modeman.INPUT_MODES):
tab = self.currentWidget()
tab = self.widget.currentWidget()
if tab is not None:
tab.data.input_mode = mode
@pyqtSlot(usertypes.KeyMode)
def on_mode_left(self, mode):
"""Give focus to current tab if command mode was left."""
widget = self.currentWidget()
widget = self.widget.currentWidget()
if widget is None:
return
if mode in [usertypes.KeyMode.command] + modeman.PROMPT_MODES:
@ -660,7 +660,7 @@ class TabbedBrowser(tabwidget.TabWidget):
if idx == -1 or self.shutting_down:
# closing the last tab (before quitting) or shutting down
return
tab = self.widget(idx)
tab = self.widget.widget(idx)
if tab is None:
log.webview.debug("on_current_changed got called with invalid "
"index {}".format(idx))
@ -690,8 +690,8 @@ class TabbedBrowser(tabwidget.TabWidget):
self._now_focused = tab
self.current_tab_changed.emit(tab)
QTimer.singleShot(0, self._update_window_title)
self._tab_insert_idx_left = self.currentIndex()
self._tab_insert_idx_right = self.currentIndex() + 1
self._tab_insert_idx_left = self.widget.currentIndex()
self._tab_insert_idx_right = self.widget.currentIndex() + 1
@pyqtSlot()
def on_cmd_return_pressed(self):
@ -709,9 +709,9 @@ class TabbedBrowser(tabwidget.TabWidget):
stop = config.val.colors.tabs.indicator.stop
system = config.val.colors.tabs.indicator.system
color = utils.interpolate_color(start, stop, perc, system)
self.set_tab_indicator_color(idx, color)
self._update_tab_title(idx)
if idx == self.currentIndex():
self.widget.set_tab_indicator_color(idx, color)
self.widget.update_tab_title(idx)
if idx == self.widget.currentIndex():
self._update_window_title()
def on_load_finished(self, tab, ok):
@ -728,23 +728,23 @@ class TabbedBrowser(tabwidget.TabWidget):
color = utils.interpolate_color(start, stop, 100, system)
else:
color = config.val.colors.tabs.indicator.error
self.set_tab_indicator_color(idx, color)
self._update_tab_title(idx)
if idx == self.currentIndex():
self.widget.set_tab_indicator_color(idx, color)
self.widget.update_tab_title(idx)
if idx == self.widget.currentIndex():
self._update_window_title()
tab.handle_auto_insert_mode(ok)
@pyqtSlot()
def on_scroll_pos_changed(self):
"""Update tab and window title when scroll position changed."""
idx = self.currentIndex()
idx = self.widget.currentIndex()
if idx == -1:
# (e.g. last tab removed)
log.webview.debug("Not updating scroll position because index is "
"-1")
return
self._update_window_title('scroll_pos')
self._update_tab_title(idx, 'scroll_pos')
self.widget.update_tab_title(idx, 'scroll_pos')
def _on_renderer_process_terminated(self, tab, status, code):
"""Show an error when a renderer process terminated."""
@ -777,7 +777,7 @@ class TabbedBrowser(tabwidget.TabWidget):
# WORKAROUND for https://bugreports.qt.io/browse/QTBUG-58698
message.error(msg)
self._remove_tab(tab, crashed=True)
if self.count() == 0:
if self.widget.count() == 0:
self.tabopen(QUrl('about:blank'))
def resizeEvent(self, e):
@ -814,7 +814,7 @@ class TabbedBrowser(tabwidget.TabWidget):
if key != "'":
message.error("Failed to set mark: url invalid")
return
point = self.currentWidget().scroller.pos_px()
point = self.widget.currentWidget().scroller.pos_px()
if key.isupper():
self._global_marks[key] = point, url
@ -835,7 +835,7 @@ class TabbedBrowser(tabwidget.TabWidget):
except qtutils.QtValueError:
urlkey = None
tab = self.currentWidget()
tab = self.widget.currentWidget()
if key.isupper():
if key in self._global_marks:

View File

@ -60,7 +60,7 @@ class TabWidget(QTabWidget):
self.setTabBar(bar)
bar.tabCloseRequested.connect(self.tabCloseRequested)
bar.tabMoved.connect(functools.partial(
QTimer.singleShot, 0, self._update_tab_titles))
QTimer.singleShot, 0, self.update_tab_titles))
bar.currentChanged.connect(self._on_current_changed)
bar.new_tab_requested.connect(self._on_new_tab_requested)
self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
@ -108,7 +108,7 @@ class TabWidget(QTabWidget):
bar.set_tab_data(idx, 'pinned', pinned)
tab.data.pinned = pinned
self._update_tab_title(idx)
self.update_tab_title(idx)
def tab_indicator_color(self, idx):
"""Get the tab indicator color for the given index."""
@ -117,13 +117,13 @@ class TabWidget(QTabWidget):
def set_page_title(self, idx, title):
"""Set the tab title user data."""
self.tabBar().set_tab_data(idx, 'page-title', title)
self._update_tab_title(idx)
self.update_tab_title(idx)
def page_title(self, idx):
"""Get the tab title user data."""
return self.tabBar().page_title(idx)
def _update_tab_title(self, idx, field=None):
def update_tab_title(self, idx, field=None):
"""Update the tab text for the given tab.
Args:
@ -197,20 +197,20 @@ class TabWidget(QTabWidget):
fields['scroll_pos'] = scroll_pos
return fields
def _update_tab_titles(self):
def update_tab_titles(self):
"""Update all texts."""
for idx in range(self.count()):
self._update_tab_title(idx)
self.update_tab_title(idx)
def tabInserted(self, idx):
"""Update titles when a tab was inserted."""
super().tabInserted(idx)
self._update_tab_titles()
self.update_tab_titles()
def tabRemoved(self, idx):
"""Update titles when a tab was removed."""
super().tabRemoved(idx)
self._update_tab_titles()
self.update_tab_titles()
def addTab(self, page, icon_or_text, text_or_empty=None):
"""Override addTab to use our own text setting logic.

View File

@ -172,6 +172,7 @@ def check_qt_version():
from PyQt5.QtCore import (qVersion, QT_VERSION, PYQT_VERSION,
PYQT_VERSION_STR)
from pkg_resources import parse_version
from qutebrowser.utils import log
if (QT_VERSION < 0x050701 or PYQT_VERSION < 0x050700 or
parse_version(qVersion()) < parse_version('5.7.1')):
text = ("Fatal error: Qt >= 5.7.1 and PyQt >= 5.7 are required, "
@ -179,6 +180,10 @@ def check_qt_version():
PYQT_VERSION_STR))
_die(text)
if qVersion().startswith('5.8.'):
log.init.warning("Running qutebrowser with Qt 5.8 is untested and "
"unsupported!")
def check_ssl_support():
"""Check if SSL support is available."""

View File

@ -42,6 +42,7 @@ class ExternalEditor(QObject):
_proc: The GUIProcess of the editor.
_watcher: A QFileSystemWatcher to watch the edited file for changes.
Only set if watch=True.
_content: The last-saved text of the editor.
Signals:
file_updated: The text in the edited file was updated.
@ -112,19 +113,7 @@ class ExternalEditor(QObject):
if self._filename is not None:
raise ValueError("Already editing a file!")
try:
# 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/qutebrowser/qutebrowser/issues/1767
with tempfile.NamedTemporaryFile(
# pylint: disable=bad-continuation
mode='w', prefix='qutebrowser-editor-',
encoding=config.val.editor.encoding,
delete=False) as fobj:
# pylint: enable=bad-continuation
if text:
fobj.write(text)
self._filename = fobj.name
self._filename = self._create_tempfile(text, 'qutebrowser-editor-')
except OSError as e:
message.error("Failed to create initial file: {}".format(e))
return
@ -134,6 +123,32 @@ class ExternalEditor(QObject):
line, column = self._calc_line_and_column(text, caret_position)
self._start_editor(line=line, column=column)
def backup(self):
"""Create a backup if the content has changed from the original."""
if not self._content:
return
try:
fname = self._create_tempfile(self._content,
'qutebrowser-editor-backup-')
message.info('Editor backup at {}'.format(fname))
except OSError as e:
message.error('Failed to create editor backup: {}'.format(e))
def _create_tempfile(self, text, prefix):
# 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/qutebrowser/qutebrowser/issues/1767
with tempfile.NamedTemporaryFile(
# pylint: disable=bad-continuation
mode='w', prefix=prefix,
encoding=config.val.editor.encoding,
delete=False) as fobj:
# pylint: enable=bad-continuation
if text:
fobj.write(text)
return fobj.name
@pyqtSlot(str)
def _on_file_changed(self, path):
try:

View File

@ -34,6 +34,7 @@ from PyQt5.QtCore import pyqtSlot, pyqtSignal, Qt
from qutebrowser.config import config
from qutebrowser.utils import utils, usertypes
from qutebrowser.commands import cmdutils
from qutebrowser.keyinput import keyutils
class KeyHintView(QLabel):
@ -105,9 +106,8 @@ class KeyHintView(QLabel):
bindings_dict = config.key_instance.get_bindings_for(modename)
bindings = [(k, v) for (k, v) in sorted(bindings_dict.items())
if k.startswith(prefix) and
not utils.is_special_key(k) and
not blacklisted(k) and
if keyutils.KeySequence.parse(prefix).matches(k) and
not blacklisted(str(k)) and
(takes_count(v) or not countstr)]
if not bindings:
@ -120,7 +120,7 @@ class KeyHintView(QLabel):
suffix_color = html.escape(config.val.colors.keyhint.suffix.fg)
text = ''
for key, cmd in bindings:
for seq, cmd in bindings:
text += (
"<tr>"
"<td>{}</td>"
@ -130,7 +130,7 @@ class KeyHintView(QLabel):
).format(
html.escape(prefix),
suffix_color,
html.escape(key[len(prefix):]),
html.escape(str(seq[len(prefix):])),
html.escape(cmd)
)
text = '<table>{}</table>'.format(text)

View File

@ -25,8 +25,8 @@ from PyQt5.QtWidgets import (QLineEdit, QWidget, QHBoxLayout, QLabel,
from PyQt5.QtGui import QValidator, QPainter
from qutebrowser.config import config
from qutebrowser.utils import utils, qtutils, log, usertypes
from qutebrowser.misc import cmdhistory, objects
from qutebrowser.utils import utils
from qutebrowser.misc import cmdhistory
class MinimalLineEditMixin:
@ -260,16 +260,6 @@ class WrapperLayout(QLayout):
self._widget = widget
container.setFocusProxy(widget)
widget.setParent(container)
if (qtutils.version_check('5.8.0', exact=True, compiled=False) and
objects.backend == usertypes.Backend.QtWebEngine and
container.window() and
container.window().windowHandle() and
not container.window().windowHandle().isActive()):
log.misc.debug("Calling QApplication::sync...")
# WORKAROUND for:
# https://bugreports.qt.io/browse/QTBUG-56652
# https://codereview.qt-project.org/#/c/176113/5//ALL,unified
QApplication.sync()
def unwrap(self):
self._widget.setParent(None)
@ -293,8 +283,6 @@ class FullscreenNotification(QLabel):
bindings = all_bindings.get('fullscreen --leave')
if bindings:
key = bindings[0]
if utils.is_special_key(key):
key = key.strip('<>').capitalize()
self.setText("Press {} to exit fullscreen.".format(key))
else:
self.setText("Page is now fullscreen.")

View File

@ -60,7 +60,7 @@ class PastebinClient(QObject):
self._client = client
self._api_url = api_url
def paste(self, name, title, text, parent=None):
def paste(self, name, title, text, parent=None, private=False):
"""Paste the text into a pastebin and return the URL.
Args:
@ -68,6 +68,7 @@ class PastebinClient(QObject):
title: The post title.
text: The text to post.
parent: The parent paste to reply to.
private: Whether to paste privately.
"""
data = {
'text': text,
@ -77,6 +78,9 @@ class PastebinClient(QObject):
}
if parent is not None:
data['reply'] = parent
if private:
data['private'] = '1'
url = QUrl(urllib.parse.urljoin(self._api_url, 'create'))
self._client.post(url, data)

View File

@ -246,7 +246,7 @@ class SessionManager(QObject):
if tabbed_browser.private:
win_data['private'] = True
for i, tab in enumerate(tabbed_browser.widgets()):
active = i == tabbed_browser.currentIndex()
active = i == tabbed_browser.widget.currentIndex()
win_data['tabs'].append(self._save_tab(tab, active))
data['windows'].append(win_data)
return data
@ -427,11 +427,12 @@ class SessionManager(QObject):
if tab.get('active', False):
tab_to_focus = i
if new_tab.data.pinned:
tabbed_browser.set_tab_pinned(new_tab, new_tab.data.pinned)
tabbed_browser.widget.set_tab_pinned(new_tab,
new_tab.data.pinned)
if tab_to_focus is not None:
tabbed_browser.setCurrentIndex(tab_to_focus)
tabbed_browser.widget.setCurrentIndex(tab_to_focus)
if win.get('active', False):
QTimer.singleShot(0, tabbed_browser.activateWindow)
QTimer.singleShot(0, tabbed_browser.widget.activateWindow)
if data['windows']:
self.did_load = True

View File

@ -185,7 +185,7 @@ def debug_cache_stats():
tabbed_browser = objreg.get('tabbed-browser', scope='window',
window='last-focused')
# pylint: disable=protected-access
tab_bar = tabbed_browser.tabBar()
tab_bar = tabbed_browser.widget.tabBar()
tabbed_browser_info = tab_bar._minimum_tab_size_hint_helper.cache_info()
# pylint: enable=protected-access

View File

@ -22,12 +22,10 @@
import os
import os.path
import contextlib
import traceback
import mimetypes
import html
import jinja2
import jinja2.exceptions
from PyQt5.QtCore import QUrl
from qutebrowser.utils import utils, urlutils, log
@ -125,14 +123,7 @@ class Environment(jinja2.Environment):
def render(template, **kwargs):
"""Render the given template and pass the given arguments to it."""
try:
return environment.get_template(template).render(**kwargs)
except jinja2.exceptions.UndefinedError:
log.misc.exception("UndefinedError while rendering " + template)
err_path = os.path.join('html', 'undef_error.html')
err_template = utils.read_file(err_path)
tb = traceback.format_exc()
return err_template.format(pagename=template, traceback=tb)
return environment.get_template(template).render(**kwargs)
environment = Environment()

View File

@ -171,7 +171,7 @@ def _get_tab_registry(win_id, tab_id):
if tab_id == 'current':
tabbed_browser = get('tabbed-browser', scope='window', window=win_id)
tab = tabbed_browser.currentWidget()
tab = tabbed_browser.widget.currentWidget()
if tab is None:
raise RegistryUnavailableError('window')
tab_id = tab.tab_id

View File

@ -0,0 +1,293 @@
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
# Copyright 2018 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/>.
"""A Chromium-like URL matching pattern.
See:
https://developer.chrome.com/apps/match_patterns
https://cs.chromium.org/chromium/src/extensions/common/url_pattern.cc
https://cs.chromium.org/chromium/src/extensions/common/url_pattern.h
"""
import ipaddress
import fnmatch
import urllib.parse
from qutebrowser.utils import utils, qtutils
class ParseError(Exception):
"""Raised when a pattern could not be parsed."""
class UrlPattern:
"""A Chromium-like URL matching pattern.
Class attributes:
_DEFAULT_PORTS: The default ports used for schemes which support ports.
_SCHEMES_WITHOUT_HOST: Schemes which don't need a host.
Attributes:
_pattern: The given pattern as string.
_match_all: Whether the pattern should match all URLs.
_match_subdomains: Whether the pattern should match subdomains of the
given host.
_scheme: The scheme to match to, or None to match any scheme.
Note that with Chromium, '*'/None only matches http/https and
not file/ftp. We deviate from that as per-URL settings aren't
security relevant.
_host: The host to match to, or None for any host.
_path: The path to match to, or None for any path.
_port: The port to match to as integer, or None for any port.
"""
_DEFAULT_PORTS = {'https': 443, 'http': 80, 'ftp': 21}
_SCHEMES_WITHOUT_HOST = ['about', 'file', 'data', 'javascript']
def __init__(self, pattern):
# Make sure all attributes are initialized if we exit early.
self._pattern = pattern
self._match_all = False
self._match_subdomains = False
self._scheme = None
self._host = None
self._path = None
self._port = None
# > The special pattern <all_urls> matches any URL that starts with a
# > permitted scheme.
if pattern == '<all_urls>':
self._match_all = True
return
if '\0' in pattern:
raise ParseError("May not contain NUL byte")
pattern = self._fixup_pattern(pattern)
# We use urllib.parse instead of QUrl here because it can handle
# hosts with * in them.
try:
parsed = urllib.parse.urlparse(pattern)
except ValueError as e:
raise ParseError(str(e))
assert parsed is not None
self._init_scheme(parsed)
self._init_host(parsed)
self._init_path(parsed)
self._init_port(parsed)
def _to_tuple(self):
"""Get a pattern with information used for __eq__/__hash__."""
return (self._match_all, self._match_subdomains, self._scheme,
self._host, self._path, self._port)
def __hash__(self):
return hash(self._to_tuple())
def __eq__(self, other):
if not isinstance(other, UrlPattern):
return NotImplemented
# pylint: disable=protected-access
return self._to_tuple() == other._to_tuple()
def __repr__(self):
return utils.get_repr(self, pattern=self._pattern, constructor=True)
def __str__(self):
return self._pattern
def _fixup_pattern(self, pattern):
"""Make sure the given pattern is parseable by urllib.parse."""
if pattern.startswith('*:'): # Any scheme, but *:// is unparseable
pattern = 'any:' + pattern[2:]
schemes = tuple(s + ':' for s in self._SCHEMES_WITHOUT_HOST)
if '://' not in pattern and not pattern.startswith(schemes):
pattern = 'any://' + pattern
# Chromium handles file://foo like file:///foo
# FIXME This doesn't actually strip the hostname correctly.
if (pattern.startswith('file://') and
not pattern.startswith('file:///')):
pattern = 'file:///' + pattern[len("file://"):]
return pattern
def _init_scheme(self, parsed):
"""Parse the scheme from the given URL.
Deviation from Chromium:
- We assume * when no scheme has been given.
"""
assert parsed.scheme, parsed
if parsed.scheme == 'any':
self._scheme = None
return
self._scheme = parsed.scheme
def _init_path(self, parsed):
"""Parse the path from the given URL.
Deviation from Chromium:
- We assume * when no path has been given.
"""
if self._scheme == 'about' and not parsed.path.strip():
raise ParseError("Pattern without path")
if parsed.path == '/*':
self._path = None
elif parsed.path == '':
# When the user doesn't add a trailing slash, we assume the pattern
# matches any path.
self._path = None
else:
self._path = parsed.path
def _init_host(self, parsed):
"""Parse the host from the given URL.
Deviation from Chromium:
- http://:1234/ is not a valid URL because it has no host.
"""
if parsed.hostname is None or not parsed.hostname.strip():
if self._scheme not in self._SCHEMES_WITHOUT_HOST:
raise ParseError("Pattern without host")
assert self._host is None
return
# FIXME what about multiple dots?
host_parts = parsed.hostname.rstrip('.').split('.')
if host_parts[0] == '*':
host_parts = host_parts[1:]
self._match_subdomains = True
if not host_parts:
self._host = None
return
self._host = '.'.join(host_parts)
if self._host.endswith('.*'):
# Special case to have a nicer error
raise ParseError("TLD wildcards are not implemented yet")
elif '*' in self._host:
# Only * or *.foo is allowed as host.
raise ParseError("Invalid host wildcard")
def _init_port(self, parsed):
"""Parse the port from the given URL.
Deviation from Chromium:
- We use None instead of "*" if there's no port filter.
"""
if parsed.netloc.endswith(':*'):
# We can't access parsed.port as it tries to run int()
self._port = None
elif parsed.netloc.endswith(':'):
raise ParseError("Invalid port: Port is empty")
else:
try:
self._port = parsed.port
except ValueError as e:
raise ParseError("Invalid port: {}".format(e))
if (self._scheme not in list(self._DEFAULT_PORTS) + [None] and
self._port is not None):
raise ParseError("Ports are unsupported with {} scheme".format(
self._scheme))
def _matches_scheme(self, scheme):
return self._scheme is None or self._scheme == scheme
def _matches_host(self, host):
# FIXME what about multiple dots?
host = host.rstrip('.')
# If we have no host in the match pattern, that means that we're
# matching all hosts, which means we have a match no matter what the
# test host is.
# Contrary to Chromium, we don't need to check for
# self._match_subdomains, as we want to return True here for e.g.
# file:// as well.
if self._host is None:
return True
# If the hosts are exactly equal, we have a match.
if host == self._host:
return True
# Otherwise, we can only match if our match pattern matches subdomains.
if not self._match_subdomains:
return False
# We don't do subdomain matching against IP addresses, so we can give
# up now if the test host is an IP address.
if not utils.raises(ValueError, ipaddress.ip_address, host):
return False
# Check if the test host is a subdomain of our host.
if len(host) <= (len(self._host) + 1):
return False
if not host.endswith(self._host):
return False
return host[len(host) - len(self._host) - 1] == '.'
def _matches_port(self, scheme, port):
if port == -1 and scheme in self._DEFAULT_PORTS:
port = self._DEFAULT_PORTS[scheme]
return self._port is None or self._port == port
def _matches_path(self, path):
if self._path is None:
return True
# Match 'google.com' with 'google.com/'
if path + '/*' == self._path:
return True
# FIXME Chromium seems to have a more optimized glob matching which
# doesn't rely on regexes. Do we need that too?
return fnmatch.fnmatchcase(path, self._path)
def matches(self, qurl):
"""Check if the pattern matches the given QUrl."""
qtutils.ensure_valid(qurl)
if self._match_all:
return True
if not self._matches_scheme(qurl.scheme()):
return False
# FIXME ignore for file:// like Chromium?
if not self._matches_host(qurl.host()):
return False
if not self._matches_port(qurl.scheme(), qurl.port()):
return False
if not self._matches_path(qurl.path()):
return False
return True

View File

@ -27,6 +27,7 @@ import operator
import collections.abc
import enum
import attr
from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject, QTimer
from qutebrowser.utils import log, qtutils, utils
@ -394,3 +395,24 @@ class AbstractCertificateErrorWrapper:
def is_overridable(self):
raise NotImplementedError
@attr.s
class NavigationRequest:
"""A request to navigate to the given URL."""
Type = enum.Enum('Type', [
'link_clicked',
'typed', # QtWebEngine only
'form_submitted',
'form_resubmitted', # QtWebKit only
'back_forward',
'reloaded',
'other'
])
url = attr.ib()
navigation_type = attr.ib()
is_main_frame = attr.ib()
accepted = attr.ib(default=True)

View File

@ -26,17 +26,16 @@ import re
import sys
import enum
import json
import collections
import datetime
import traceback
import functools
import contextlib
import socket
import shlex
import glob
import attr
from PyQt5.QtCore import Qt, QUrl
from PyQt5.QtGui import QKeySequence, QColor, QClipboard, QDesktopServices
from PyQt5.QtCore import QUrl
from PyQt5.QtGui import QColor, QClipboard, QDesktopServices
from PyQt5.QtWidgets import QApplication
import pkg_resources
import yaml
@ -48,11 +47,12 @@ except ImportError: # pragma: no cover
YAML_C_EXT = False
import qutebrowser
from qutebrowser.utils import qtutils, log, debug
from qutebrowser.utils import qtutils, log
fake_clipboard = None
log_clipboard = False
_resource_cache = {}
is_mac = sys.platform.startswith('darwin')
is_linux = sys.platform.startswith('linux')
@ -142,6 +142,15 @@ def compact_text(text, elidelength=None):
return out
def preload_resources():
"""Load resource files into the cache."""
for subdir, pattern in [('html', '*.html'), ('javascript', '*.js')]:
path = resource_filename(subdir)
for full_path in glob.glob(os.path.join(path, pattern)):
sub_path = '/'.join([subdir, os.path.basename(full_path)])
_resource_cache[sub_path] = read_file(sub_path)
def read_file(filename, binary=False):
"""Get the contents of a file contained with qutebrowser.
@ -153,6 +162,9 @@ def read_file(filename, binary=False):
Return:
The file contents as string.
"""
if not binary and filename in _resource_cache:
return _resource_cache[filename]
if hasattr(sys, 'frozen'):
# PyInstaller doesn't support pkg_resources :(
# https://github.com/pyinstaller/pyinstaller/wiki/FAQ#misc
@ -285,263 +297,6 @@ def format_size(size, base=1024, suffix=''):
return '{:.02f}{}{}'.format(size, prefixes[-1], suffix)
def key_to_string(key):
"""Convert a Qt::Key member to a meaningful name.
Args:
key: A Qt::Key member.
Return:
A name of the key as a string.
"""
special_names_str = {
# Some keys handled in a weird way by QKeySequence::toString.
# See https://bugreports.qt.io/browse/QTBUG-40030
# Most are unlikely to be ever needed, but you never know ;)
# For dead/combining keys, we return the corresponding non-combining
# key, as that's easier to add to the config.
'Key_Blue': 'Blue',
'Key_Calendar': 'Calendar',
'Key_ChannelDown': 'Channel Down',
'Key_ChannelUp': 'Channel Up',
'Key_ContrastAdjust': 'Contrast Adjust',
'Key_Dead_Abovedot': '˙',
'Key_Dead_Abovering': '˚',
'Key_Dead_Acute': '´',
'Key_Dead_Belowdot': 'Belowdot',
'Key_Dead_Breve': '˘',
'Key_Dead_Caron': 'ˇ',
'Key_Dead_Cedilla': '¸',
'Key_Dead_Circumflex': '^',
'Key_Dead_Diaeresis': '¨',
'Key_Dead_Doubleacute': '˝',
'Key_Dead_Grave': '`',
'Key_Dead_Hook': 'Hook',
'Key_Dead_Horn': 'Horn',
'Key_Dead_Iota': 'Iota',
'Key_Dead_Macron': '¯',
'Key_Dead_Ogonek': '˛',
'Key_Dead_Semivoiced_Sound': 'Semivoiced Sound',
'Key_Dead_Tilde': '~',
'Key_Dead_Voiced_Sound': 'Voiced Sound',
'Key_Exit': 'Exit',
'Key_Green': 'Green',
'Key_Guide': 'Guide',
'Key_Info': 'Info',
'Key_LaunchG': 'LaunchG',
'Key_LaunchH': 'LaunchH',
'Key_MediaLast': 'MediaLast',
'Key_Memo': 'Memo',
'Key_MicMute': 'Mic Mute',
'Key_Mode_switch': 'Mode switch',
'Key_Multi_key': 'Multi key',
'Key_PowerDown': 'Power Down',
'Key_Red': 'Red',
'Key_Settings': 'Settings',
'Key_SingleCandidate': 'Single Candidate',
'Key_ToDoList': 'Todo List',
'Key_TouchpadOff': 'Touchpad Off',
'Key_TouchpadOn': 'Touchpad On',
'Key_TouchpadToggle': 'Touchpad toggle',
'Key_Yellow': 'Yellow',
'Key_Alt': 'Alt',
'Key_AltGr': 'AltGr',
'Key_Control': 'Control',
'Key_Direction_L': 'Direction L',
'Key_Direction_R': 'Direction R',
'Key_Hyper_L': 'Hyper L',
'Key_Hyper_R': 'Hyper R',
'Key_Meta': 'Meta',
'Key_Shift': 'Shift',
'Key_Super_L': 'Super L',
'Key_Super_R': 'Super R',
'Key_unknown': 'Unknown',
}
# We now build our real special_names dict from the string mapping above.
# The reason we don't do this directly is that certain Qt versions don't
# have all the keys, so we want to ignore AttributeErrors.
special_names = {}
for k, v in special_names_str.items():
try:
special_names[getattr(Qt, k)] = v
except AttributeError:
pass
# Now we check if the key is any special one - if not, we use
# QKeySequence::toString.
try:
return special_names[key]
except KeyError:
name = QKeySequence(key).toString()
morphings = {
'Backtab': 'Tab',
'Esc': 'Escape',
}
if name in morphings:
return morphings[name]
else:
return name
def keyevent_to_string(e):
"""Convert a QKeyEvent to a meaningful name.
Args:
e: A QKeyEvent.
Return:
A name of the key (combination) as a string or
None if only modifiers are pressed..
"""
if is_mac:
# Qt swaps Ctrl/Meta on macOS, so we switch it back here so the user
# can use it in the config as expected. See:
# https://github.com/qutebrowser/qutebrowser/issues/110
# http://doc.qt.io/qt-5.4/osx-issues.html#special-keys
modmask2str = collections.OrderedDict([
(Qt.MetaModifier, 'Ctrl'),
(Qt.AltModifier, 'Alt'),
(Qt.ControlModifier, 'Meta'),
(Qt.ShiftModifier, 'Shift'),
])
else:
modmask2str = collections.OrderedDict([
(Qt.ControlModifier, 'Ctrl'),
(Qt.AltModifier, 'Alt'),
(Qt.MetaModifier, 'Meta'),
(Qt.ShiftModifier, 'Shift'),
])
modifiers = (Qt.Key_Control, Qt.Key_Alt, Qt.Key_Shift, Qt.Key_Meta,
Qt.Key_AltGr, Qt.Key_Super_L, Qt.Key_Super_R, Qt.Key_Hyper_L,
Qt.Key_Hyper_R, Qt.Key_Direction_L, Qt.Key_Direction_R)
if e.key() in modifiers:
# Only modifier pressed
return None
mod = e.modifiers()
parts = []
for (mask, s) in modmask2str.items():
if mod & mask and s not in parts:
parts.append(s)
parts.append(key_to_string(e.key()))
return normalize_keystr('+'.join(parts))
@attr.s(repr=False)
class KeyInfo:
"""Stores information about a key, like used in a QKeyEvent.
Attributes:
key: Qt::Key
modifiers: Qt::KeyboardModifiers
text: str
"""
key = attr.ib()
modifiers = attr.ib()
text = attr.ib()
def __repr__(self):
if self.modifiers is None:
modifiers = None
else:
#modifiers = qflags_key(Qt, self.modifiers)
modifiers = hex(int(self.modifiers))
return get_repr(self, constructor=True,
key=debug.qenum_key(Qt, self.key),
modifiers=modifiers, text=self.text)
class KeyParseError(Exception):
"""Raised by _parse_single_key/parse_keystring on parse errors."""
def __init__(self, keystr, error):
super().__init__("Could not parse {!r}: {}".format(keystr, error))
def is_special_key(keystr):
"""True if keystr is a 'special' keystring (e.g. <ctrl-x> or <space>)."""
return keystr.startswith('<') and keystr.endswith('>')
def _parse_single_key(keystr):
"""Convert a single key string to a (Qt.Key, Qt.Modifiers, text) tuple."""
if is_special_key(keystr):
# Special key
keystr = keystr[1:-1]
elif len(keystr) == 1:
# vim-like key
pass
else:
raise KeyParseError(keystr, "Expecting either a single key or a "
"<Ctrl-x> like keybinding.")
seq = QKeySequence(normalize_keystr(keystr), QKeySequence.PortableText)
if len(seq) != 1:
raise KeyParseError(keystr, "Got {} keys instead of 1.".format(
len(seq)))
result = seq[0]
if result == Qt.Key_unknown:
raise KeyParseError(keystr, "Got unknown key.")
modifier_mask = int(Qt.ShiftModifier | Qt.ControlModifier |
Qt.AltModifier | Qt.MetaModifier | Qt.KeypadModifier |
Qt.GroupSwitchModifier)
assert Qt.Key_unknown & ~modifier_mask == Qt.Key_unknown
modifiers = result & modifier_mask
key = result & ~modifier_mask
if len(keystr) == 1 and keystr.isupper():
modifiers |= Qt.ShiftModifier
assert key != 0, key
key = Qt.Key(key)
modifiers = Qt.KeyboardModifiers(modifiers)
# Let's hope this is accurate...
if len(keystr) == 1 and not modifiers:
text = keystr
elif len(keystr) == 1 and modifiers == Qt.ShiftModifier:
text = keystr.upper()
else:
text = ''
return KeyInfo(key, modifiers, text)
def parse_keystring(keystr):
"""Parse a keystring like <Ctrl-x> or xyz and return a KeyInfo list."""
if is_special_key(keystr):
return [_parse_single_key(keystr)]
else:
return [_parse_single_key(char) for char in keystr]
def normalize_keystr(keystr):
"""Normalize a keystring like Ctrl-Q to a keystring like Ctrl+Q.
Args:
keystr: The key combination as a string.
Return:
The normalized keystring.
"""
keystr = keystr.lower()
replacements = (
('control', 'ctrl'),
('windows', 'meta'),
('mod1', 'alt'),
('mod4', 'meta'),
)
for (orig, repl) in replacements:
keystr = keystr.replace(orig, repl)
for mod in ['ctrl', 'meta', 'alt', 'shift']:
keystr = keystr.replace(mod + '-', mod + '+')
return keystr
class FakeIOStream(io.TextIOBase):
"""A fake file-like stream which calls a function for write-calls."""
@ -915,3 +670,14 @@ def yaml_dump(data, f=None):
return None
else:
return yaml_data.decode('utf-8')
def chunk(elems, n):
"""Yield successive n-sized chunks from elems.
If elems % n != 0, the last chunk will be smaller.
"""
if n < 1:
raise ValueError("n needs to be at least 1!")
for i in range(0, len(elems), n):
yield elems[i:i + n]

View File

@ -269,6 +269,8 @@ def _os_info():
else:
versioninfo = '.'.join(versioninfo)
osver = ', '.join([e for e in [release, versioninfo, machine] if e])
elif utils.is_posix:
osver = ' '.join(platform.uname())
else:
osver = '?'
lines.append('OS Version: {}'.format(osver))
@ -305,7 +307,19 @@ def _pdfjs_version():
def _chromium_version():
"""Get the Chromium version for QtWebEngine."""
"""Get the Chromium version for QtWebEngine.
This can also be checked by looking at this file with the right Qt tag:
https://github.com/qt/qtwebengine/blob/dev/tools/scripts/version_resolver.py#L41
Quick reference:
Qt 5.7: Chromium 49
Qt 5.8: Chromium 53
Qt 5.9: Chromium 56
Qt 5.10: Chromium 61
Qt 5.11: Chromium 63
Qt 5.12: Chromium 65 (?)
"""
if QWebEngineProfile is None:
# This should never happen
return 'unavailable'
@ -441,7 +455,13 @@ def opengl_vendor(): # pragma: no cover
vp = QOpenGLVersionProfile()
vp.setVersion(2, 0)
vf = ctx.versionFunctions(vp)
try:
vf = ctx.versionFunctions(vp)
except ImportError as e:
log.init.debug("opengl_vendor: Importing version functions "
"failed: {}".format(e))
return None
if vf is None:
log.init.debug("opengl_vendor: Getting version functions failed!")
return None
@ -453,7 +473,7 @@ def opengl_vendor(): # pragma: no cover
old_context.makeCurrent(old_surface)
def pastebin_version():
def pastebin_version(pbclient=None):
"""Pastebin the version and log the url to messages."""
def _yank_url(url):
utils.set_clipboard(url)
@ -478,12 +498,13 @@ def pastebin_version():
http_client = httpclient.HTTPClient()
misc_api = pastebin.PastebinClient.MISC_API_URL
pbclient = pastebin.PastebinClient(http_client, parent=app,
api_url=misc_api)
pbclient = pbclient or pastebin.PastebinClient(http_client, parent=app,
api_url=misc_api)
pbclient.success.connect(_on_paste_version_success)
pbclient.error.connect(_on_paste_version_err)
pbclient.paste(getpass.getuser(),
"qute version info {}".format(qutebrowser.__version__),
version())
version(),
private=True)

View File

@ -85,9 +85,9 @@ class AsciiDoc:
# patch image links to use local copy
replacements = [
("https://qutebrowser.org/img/cheatsheet-big.png",
("https://raw.githubusercontent.com/qutebrowser/qutebrowser/master/doc/img/cheatsheet-big.png",
"qute://help/img/cheatsheet-big.png"),
("https://qutebrowser.org/img/cheatsheet-small.png",
("https://raw.githubusercontent.com/qutebrowser/qutebrowser/master/doc/img/cheatsheet-small.png",
"qute://help/img/cheatsheet-small.png")
]
asciidoc_args = ['-a', 'source-highlighter=pygments']

View File

@ -24,6 +24,7 @@
import os
import os.path
import sys
import time
import glob
import shutil
import plistlib
@ -195,6 +196,7 @@ def build_mac():
'MacOS', 'qutebrowser')
smoke_test(binary)
finally:
time.sleep(5)
subprocess.run(['hdiutil', 'detach', tmpdir])
except PermissionError as e:
print("Failed to remove tempdir: {}".format(e))
@ -359,7 +361,7 @@ def github_upload(artifacts, tag):
repo = gh.repository('qutebrowser', 'qutebrowser')
release = None # to satisfy pylint
for release in repo.iter_releases():
for release in repo.releases():
if release.tag_name == tag:
break
else:

View File

@ -86,6 +86,8 @@ PERFECT_FILES = [
('tests/unit/keyinput/test_basekeyparser.py',
'keyinput/basekeyparser.py'),
('tests/unit/keyinput/test_keyutils.py',
'keyinput/keyutils.py'),
('tests/unit/misc/test_autoupdate.py',
'misc/autoupdate.py'),
@ -143,6 +145,8 @@ PERFECT_FILES = [
'config/configinit.py'),
('tests/unit/config/test_configcommands.py',
'config/configcommands.py'),
('tests/unit/config/test_configutils.py',
'config/configutils.py'),
('tests/unit/utils/test_qtutils.py',
'utils/qtutils.py'),
@ -164,11 +168,15 @@ PERFECT_FILES = [
'utils/error.py'),
('tests/unit/utils/test_javascript.py',
'utils/javascript.py'),
('tests/unit/utils/test_urlmatch.py',
'utils/urlmatch.py'),
(None,
'completion/models/util.py'),
('tests/unit/completion/test_models.py',
'completion/models/urlmodel.py'),
('tests/unit/completion/test_models.py',
'completion/models/configmodel.py'),
('tests/unit/completion/test_histcategory.py',
'completion/models/histcategory.py'),
('tests/unit/completion/test_listcategory.py',

View File

@ -83,7 +83,9 @@ elif [[ $TRAVIS_OS_NAME == osx ]]; then
sudo -H python get-pip.py
brew --version
brew_install python3 qt5 pyqt5 libyaml
brew update
brew upgrade python
brew install qt5 pyqt5 libyaml
pip_install -r misc/requirements/requirements-tox.txt
python3 -m pip --version
@ -101,5 +103,8 @@ case $TESTENV in
*)
pip_install pip
pip_install -r misc/requirements/requirements-tox.txt
if [[ $TESTENV == *-cov ]]; then
pip_install -r misc/requirements/requirements-codecov.txt
fi
;;
esac

Some files were not shown because too many files have changed in this diff Show More