Merge 'origin/master' into tab-input-mode
This commit is contained in:
commit
b7159d780a
10
.flake8
10
.flake8
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
------------
|
||||
|
@ -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
|
||||
----
|
||||
|
@ -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
|
||||
------
|
||||
|
||||
|
@ -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).
|
||||
|
@ -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
|
||||
|
||||
|
@ -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.
|
||||
|
@ -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
|
||||
~~~~~~~~~~~~
|
||||
|
||||
|
@ -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:[<Shift-Tab>]+: +pass:[prompt-item-focus prev]+
|
||||
* +pass:[<Tab>]+: +pass:[prompt-item-focus next]+
|
||||
* +pass:[<Up>]+: +pass:[prompt-item-focus prev]+
|
||||
* +pass:[n]+: +pass:[prompt-accept no]+
|
||||
* +pass:[y]+: +pass:[prompt-accept yes]+
|
||||
- +pass:[register]+:
|
||||
|
||||
* +pass:[<Escape>]+: +pass:[leave-mode]+
|
||||
- +pass:[yesno]+:
|
||||
|
||||
* +pass:[<Alt-Shift-Y>]+: +pass:[prompt-yank --sel]+
|
||||
* +pass:[<Alt-Y>]+: +pass:[prompt-yank]+
|
||||
* +pass:[<Escape>]+: +pass:[leave-mode]+
|
||||
* +pass:[<Return>]+: +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 |
@ -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
|
||||
~~~~~~~~~~~~~~
|
||||
|
@ -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.
|
||||
|
@ -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 |
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
4
misc/requirements/requirements-pyqt-old.txt
Normal file
4
misc/requirements/requirements-pyqt-old.txt
Normal 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
|
2
misc/requirements/requirements-pyqt-old.txt-raw
Normal file
2
misc/requirements/requirements-pyqt-old.txt-raw
Normal file
@ -0,0 +1,2 @@
|
||||
PyQt5==5.10.0
|
||||
#@ filter: PyQt5 != 5.10.1
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
||||
|
@ -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() {
|
||||
|
@ -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
52
misc/userscripts/tor_identity
Executable 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.')
|
@ -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
|
||||
|
@ -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."
|
||||
|
||||
|
@ -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
|
||||
|
@ -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):
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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.
|
||||
|
||||
|
@ -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):
|
||||
|
@ -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:
|
||||
|
@ -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."""
|
||||
|
@ -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',
|
||||
|
@ -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)
|
||||
|
@ -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))
|
||||
|
@ -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!")
|
||||
|
@ -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,
|
||||
|
@ -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()
|
||||
|
@ -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()
|
||||
|
@ -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()
|
||||
|
@ -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):
|
||||
|
@ -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),
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
||||
|
@ -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',
|
||||
|
@ -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)
|
||||
|
@ -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))
|
||||
|
||||
|
@ -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("'", "`")))
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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."""
|
||||
|
@ -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,
|
||||
|
@ -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:
|
||||
|
@ -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.
|
||||
|
@ -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):
|
||||
|
@ -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])
|
||||
|
||||
|
@ -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))
|
||||
|
186
qutebrowser/config/configutils.py
Normal file
186
qutebrowser/config/configutils.py
Normal 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)
|
@ -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."""
|
||||
|
@ -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) }})
|
||||
|
58
qutebrowser/html/tabs.html
Normal file
58
qutebrowser/html/tabs.html
Normal 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 %}
|
@ -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>
|
@ -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 ====== //
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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('')
|
||||
|
@ -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)
|
558
qutebrowser/keyinput/keyutils.py
Normal file
558
qutebrowser/keyinput/keyutils.py
Normal 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
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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()
|
||||
|
@ -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')]
|
||||
|
||||
|
@ -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()
|
||||
|
@ -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.
|
||||
|
@ -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:
|
||||
|
@ -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.
|
||||
|
@ -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."""
|
||||
|
@ -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:
|
||||
|
@ -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)
|
||||
|
@ -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.")
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
||||
|
@ -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()
|
||||
|
@ -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
|
||||
|
293
qutebrowser/utils/urlmatch.py
Normal file
293
qutebrowser/utils/urlmatch.py
Normal 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
|
@ -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)
|
||||
|
@ -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]
|
||||
|
@ -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)
|
||||
|
@ -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']
|
||||
|
@ -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:
|
||||
|
@ -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',
|
||||
|
@ -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
Loading…
Reference in New Issue
Block a user