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
|
min-version = 3.4.0
|
||||||
max-complexity = 12
|
max-complexity = 12
|
||||||
per-file-ignores =
|
per-file-ignores =
|
||||||
tests/*/test_*.py : D100,D101,D401
|
/tests/*/test_*.py : D100,D101,D401
|
||||||
tests/unit/browser/test_history.py : N806
|
/tests/unit/browser/test_history.py : N806
|
||||||
tests/helpers/fixtures.py : N806
|
/tests/helpers/fixtures.py : N806
|
||||||
tests/unit/browser/webkit/http/test_content_disposition.py : D400
|
/tests/unit/browser/webkit/http/test_content_disposition.py : D400
|
||||||
scripts/dev/ci/appveyor_install.py : FI53
|
/scripts/dev/ci/appveyor_install.py : FI53
|
||||||
copyright-check = True
|
copyright-check = True
|
||||||
copyright-regexp = # Copyright [\d-]+ .*
|
copyright-regexp = # Copyright [\d-]+ .*
|
||||||
copyright-min-file-size = 110
|
copyright-min-file-size = 110
|
||||||
|
@ -14,11 +14,9 @@ matrix:
|
|||||||
services: docker
|
services: docker
|
||||||
- os: linux
|
- os: linux
|
||||||
env: TESTENV=py36-pyqt571
|
env: TESTENV=py36-pyqt571
|
||||||
- os: linux
|
|
||||||
env: TESTENV=py36-pyqt58
|
|
||||||
- os: linux
|
- os: linux
|
||||||
python: 3.5
|
python: 3.5
|
||||||
env: TESTENV=py35-pyqt59
|
env: TESTENV=py35-pyqt571
|
||||||
- os: linux
|
- os: linux
|
||||||
env: TESTENV=py36-pyqt59-cov
|
env: TESTENV=py36-pyqt59-cov
|
||||||
- os: linux
|
- os: linux
|
||||||
|
@ -8,6 +8,7 @@ graft icons
|
|||||||
graft doc/img
|
graft doc/img
|
||||||
graft misc/apparmor
|
graft misc/apparmor
|
||||||
graft misc/userscripts
|
graft misc/userscripts
|
||||||
|
graft misc/requirements
|
||||||
recursive-include scripts *.py *.sh *.js
|
recursive-include scripts *.py *.sh *.js
|
||||||
include qutebrowser/utils/testfile
|
include qutebrowser/utils/testfile
|
||||||
include qutebrowser/git-commit-id
|
include qutebrowser/git-commit-id
|
||||||
@ -32,8 +33,6 @@ include doc/qutebrowser.1.asciidoc
|
|||||||
include doc/changelog.asciidoc
|
include doc/changelog.asciidoc
|
||||||
prune tests
|
prune tests
|
||||||
prune qutebrowser/3rdparty
|
prune qutebrowser/3rdparty
|
||||||
prune misc/requirements
|
|
||||||
prune misc/docker
|
|
||||||
exclude pytest.ini
|
exclude pytest.ini
|
||||||
exclude qutebrowser.rcc
|
exclude qutebrowser.rcc
|
||||||
exclude qutebrowser/javascript/.eslintrc.yaml
|
exclude qutebrowser/javascript/.eslintrc.yaml
|
||||||
|
@ -44,8 +44,8 @@ Documentation
|
|||||||
In addition to the topics mentioned in this README, the following documents are
|
In addition to the topics mentioned in this README, the following documents are
|
||||||
available:
|
available:
|
||||||
|
|
||||||
* https://qutebrowser.org/img/cheatsheet-big.png[Key binding cheatsheet]: +
|
* https://raw.githubusercontent.com/qutebrowser/qutebrowser/master/doc/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"]
|
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]
|
* link:doc/quickstart.asciidoc[Quick start guide]
|
||||||
* https://www.shortcutfoo.com/app/dojos/qutebrowser[Free training course] to remember those key bindings
|
* https://www.shortcutfoo.com/app/dojos/qutebrowser[Free training course] to remember those key bindings
|
||||||
* link:doc/faq.asciidoc[Frequently asked questions]
|
* 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[].
|
mailto:qutebrowser@lists.qutebrowser.org[].
|
||||||
|
|
||||||
For security bugs, please contact me directly at mail@qutebrowser.org, GPG ID
|
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
|
Requirements
|
||||||
------------
|
------------
|
||||||
|
@ -13,47 +13,75 @@ Thanks a lot to the following people who contributed to it:
|
|||||||
Gold sponsors
|
Gold sponsors
|
||||||
~~~~~~~~~~~~~
|
~~~~~~~~~~~~~
|
||||||
|
|
||||||
TODO
|
- Iggy
|
||||||
|
- zwitschi
|
||||||
|
- 2x Anonymous
|
||||||
|
|
||||||
Silver sponsors
|
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
|
Other sponsors
|
||||||
~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~
|
||||||
|
|
||||||
TODO: people with t-shirts or higher pledge levels
|
|
||||||
|
|
||||||
- 7scan
|
- 7scan
|
||||||
|
- AMD1212
|
||||||
|
- Alex
|
||||||
- Alex Suykov
|
- Alex Suykov
|
||||||
- Alexey Zhikhartsev
|
- Alexey Zhikhartsev
|
||||||
- Allan Nordhøy
|
- Allan Nordhøy
|
||||||
- Anirudh Sanjeev
|
- Anirudh Sanjeev
|
||||||
- Anssi Puustinen
|
- Anssi Puustinen
|
||||||
|
- Anton Grensjö
|
||||||
|
- Aristaeus
|
||||||
|
- Armin Fisslthaler
|
||||||
|
- Ashley Hauck
|
||||||
- Benedikt Steindorf
|
- Benedikt Steindorf
|
||||||
- Bernardo Kuri
|
- Bernardo Kuri
|
||||||
- Blaise Duszynski
|
- Blaise Duszynski
|
||||||
- Bostan
|
- Bostan
|
||||||
- Bruno Oliveira
|
- Bruno Oliveira
|
||||||
|
- BunnyApocalypse
|
||||||
|
- Christian Kellermann
|
||||||
- Colin Jacobs
|
- Colin Jacobs
|
||||||
- Daniel Andersson
|
- Daniel Andersson
|
||||||
|
- Daniel Nelson
|
||||||
|
- Daniel P. Schmidt
|
||||||
|
- Daniel Salby
|
||||||
- Danilo
|
- Danilo
|
||||||
- David Beley
|
- David Beley
|
||||||
- David Hollings
|
- David Hollings
|
||||||
|
- David Keijser
|
||||||
- David Parrish
|
- David Parrish
|
||||||
- Derin Yarsuvat
|
- Derin Yarsuvat
|
||||||
- Dmytro Kostiuchenko
|
- Dmytro Kostiuchenko
|
||||||
|
- Eero Kari
|
||||||
|
- Epictek
|
||||||
|
- Eric
|
||||||
|
- Faure Hu
|
||||||
|
- Ferus
|
||||||
- Frederik Thorøe
|
- Frederik Thorøe
|
||||||
- G4v4g4i
|
- G4v4g4i
|
||||||
|
- Granitosaurus
|
||||||
- Gyula Teleki
|
- Gyula Teleki
|
||||||
- H
|
- H
|
||||||
|
- Heinz Bruhin
|
||||||
- Hosaka
|
- Hosaka
|
||||||
|
- Ihor Radchenko
|
||||||
- Iordanis Grigoriou
|
- Iordanis Grigoriou
|
||||||
- Isaac Sandaljian
|
- Isaac Sandaljian
|
||||||
- Jakub Podeszwik
|
- Jakub Podeszwik
|
||||||
- Jamie Anderson
|
- Jamie Anderson
|
||||||
- Jasper Woudenberg
|
- Jasper Woudenberg
|
||||||
|
- Jay Kamat
|
||||||
- Jens Højgaard
|
- Jens Højgaard
|
||||||
- Johannes
|
- Johannes
|
||||||
- John Baber-Lucero
|
- John Baber-Lucero
|
||||||
@ -61,9 +89,11 @@ TODO: people with t-shirts or higher pledge levels
|
|||||||
- Kenichiro Ito
|
- Kenichiro Ito
|
||||||
- Kenny Low
|
- Kenny Low
|
||||||
- Lars Ivar Igesund
|
- Lars Ivar Igesund
|
||||||
|
- Leulas
|
||||||
- Lucas Aride Moulin
|
- Lucas Aride Moulin
|
||||||
- Ludovic Chabant
|
- Ludovic Chabant
|
||||||
- Lukas Gierth
|
- Lukas Gierth
|
||||||
|
- Magnus Lindström
|
||||||
- Marulkan
|
- Marulkan
|
||||||
- Matthew Chun-Lum
|
- Matthew Chun-Lum
|
||||||
- Matthew Cronen
|
- Matthew Cronen
|
||||||
@ -80,7 +110,10 @@ TODO: people with t-shirts or higher pledge levels
|
|||||||
- Peter Rice
|
- Peter Rice
|
||||||
- Philipp Middendorf
|
- Philipp Middendorf
|
||||||
- Pkill9
|
- Pkill9
|
||||||
|
- PluMGMK
|
||||||
- Prescott
|
- Prescott
|
||||||
|
- ProXicT
|
||||||
|
- Ram-Z
|
||||||
- Robotichead
|
- Robotichead
|
||||||
- Roshless
|
- Roshless
|
||||||
- Ryan Ellis
|
- Ryan Ellis
|
||||||
@ -90,35 +123,53 @@ TODO: people with t-shirts or higher pledge levels
|
|||||||
- Sean Herman
|
- Sean Herman
|
||||||
- Sebastian Frysztak
|
- Sebastian Frysztak
|
||||||
- Shelby Cruver
|
- Shelby Cruver
|
||||||
|
- Simon Désaulniers
|
||||||
- SirCmpwn
|
- SirCmpwn
|
||||||
- Soham Pal
|
- Soham Pal
|
||||||
|
- Stephan Jauernick
|
||||||
- Stewart Webb
|
- Stewart Webb
|
||||||
- Sven Reinecke
|
- Sven Reinecke
|
||||||
|
- Timothée Floure
|
||||||
- Tom Bass
|
- Tom Bass
|
||||||
|
- Tom Kirchner
|
||||||
- Tomas Slusny
|
- Tomas Slusny
|
||||||
- Tomasz Kramkowski
|
- Tomasz Kramkowski
|
||||||
- Tommy Thomas
|
- Tommy Thomas
|
||||||
|
- Tuscan
|
||||||
|
- Ulrich Pötter
|
||||||
- Vasilij Schneidermann
|
- Vasilij Schneidermann
|
||||||
- Vlaaaaaaad
|
- Vlaaaaaaad
|
||||||
|
- XTaran
|
||||||
|
- Z2h-A6n
|
||||||
|
- ayekat
|
||||||
- beanieuptop
|
- beanieuptop
|
||||||
|
- cee
|
||||||
|
- craftyguy
|
||||||
- demure
|
- demure
|
||||||
|
- dlangevi
|
||||||
|
- epon
|
||||||
- evenorbert
|
- evenorbert
|
||||||
- fishss
|
- fishss
|
||||||
- gsnewmark
|
- gsnewmark
|
||||||
- guillermohs9
|
- guillermohs9
|
||||||
|
- hernani
|
||||||
- hubcaps
|
- hubcaps
|
||||||
|
- jnphilipp
|
||||||
- lobachevsky
|
- lobachevsky
|
||||||
- neodarz
|
- neodarz
|
||||||
- nihlaeth
|
- nihlaeth
|
||||||
- notbenh
|
- notbenh
|
||||||
|
- nyctea
|
||||||
|
- ongy
|
||||||
- patrick suwanvithaya
|
- patrick suwanvithaya
|
||||||
- pyratebeard
|
- pyratebeard
|
||||||
|
- p≡p foundation
|
||||||
- randm_dave
|
- randm_dave
|
||||||
- sabreman
|
- sabreman
|
||||||
- toml
|
- toml
|
||||||
- vimja
|
- vimja
|
||||||
- wiz
|
- wiz
|
||||||
- 44 Anonymous
|
- 48 Anonymous
|
||||||
|
|
||||||
2016
|
2016
|
||||||
----
|
----
|
||||||
|
@ -15,85 +15,198 @@ breaking changes (such as renamed commands) can happen in minor releases.
|
|||||||
// `Fixed` for any bug fixes.
|
// `Fixed` for any bug fixes.
|
||||||
// `Security` to invite users to upgrade in case of vulnerabilities.
|
// `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
|
Changed
|
||||||
~~~~~~~
|
~~~~~~~
|
||||||
|
|
||||||
- The `hist_importer.py` script now only imports URL schemes qutebrowser can
|
- The file dialog for downloads now has basic tab completion based on the
|
||||||
handle.
|
entered text.
|
||||||
- Deleting a prefix (`:`, `/` or `?`) via backspace now leaves command mode.
|
- `:version` now shows OS information for POSIX OS other than Linux/macOS.
|
||||||
- Angular 1 elements now get hints assigned.
|
- When there's an error inserting the text from an external editor, a backup
|
||||||
- `:tab-only` with pinned tabs now still closes unpinned tabs.
|
file is now saved.
|
||||||
- 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.
|
|
||||||
|
|
||||||
Fixed
|
Fixed
|
||||||
~~~~~
|
~~~~~
|
||||||
|
|
||||||
- QtWebEngine: Improved fullscreen handling with Qt 5.10.
|
- Using hints before a page is fully loaded is now possible again.
|
||||||
- QtWebEngine: Hinting and scrolling now works properly on special
|
- Tab titles for tabs loaded from sessions should now really be correct instead
|
||||||
`view-source:` pages.
|
of showing the URL.
|
||||||
- QtWebEngine: Scroll positions are now restored correctly from sessions.
|
- Loading URLs with customized settings from a session now avoids an additional
|
||||||
- QtWebKit: `:view-source` now displays a valid URL.
|
reload.
|
||||||
- URLs containing ampersands and other special chars are now shown
|
- The window icon and title now get set correctly again.
|
||||||
correctly when filtering them in the completion.
|
|
||||||
|
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
|
- `:bookmark-add "" foo` can now be used to save the current URL with a custom
|
||||||
title.
|
title.
|
||||||
- `:spawn -o` now waits until the process has finished before trying to show the
|
- `:spawn -o` now waits until the process has finished before trying to show the
|
||||||
output. Previously, it incorrectly showed the previous output immediately.
|
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.
|
- 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
|
Removed
|
||||||
~~~~~~~
|
~~~~~~~
|
||||||
|
|
||||||
- `QUTE_SELECTED_HTML` is now not set for userscripts anymore except when called
|
- `QUTE_SELECTED_HTML` is now not set for userscripts anymore except when called
|
||||||
via hints.
|
via hints.
|
||||||
- The `qutebrowser_viewsource` userscript has been removed as `:view-source
|
- The `qutebrowser_viewsource` userscript has been removed as
|
||||||
--edit` can now be used.
|
`:view-source --edit` can now be used.
|
||||||
- The `tabs.persist_mode_on_change` setting has been removed and replaced by
|
- The `tabs.persist_mode_on_change` setting has been removed and replaced by
|
||||||
`tabs.mode_on_change`.
|
`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
|
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
|
If you prefer C++ or Javascript to Python, see the relevant issues which involve
|
||||||
work in those languages:
|
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%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%3Ajavascript[JavaScript]
|
* 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:
|
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 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.
|
* Run `x=... y=...` to set the respective shell variables.
|
||||||
|
|
||||||
* Adjust `__version_info__` in `qutebrowser/__init__.py`.
|
|
||||||
* Update changelog (remove *(unreleased)*).
|
* Update changelog (remove *(unreleased)*).
|
||||||
|
* Adjust `__version_info__` in `qutebrowser/__init__.py`.
|
||||||
* Commit.
|
* Commit.
|
||||||
|
|
||||||
* Create annotated git tag (`git tag -s "v1.$x.$y" -m "Release v1.$x.$y"`).
|
* 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
|
* Mark the milestone at https://github.com/qutebrowser/qutebrowser/milestones
|
||||||
as closed.
|
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).
|
* 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).
|
* 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).
|
* 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
|
http://webkitgtk.org/reference/webkit2gtk/stable/index.html[WebKit2 API] lacked
|
||||||
basic features like proxy support, and almost no projects have started porting
|
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
|
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
|
https://github.com/qutebrowser/qutebrowser#similar-projects[list of
|
||||||
alternatives]).
|
alternatives]).
|
||||||
+
|
+
|
||||||
@ -70,6 +70,31 @@ But isn't Python too slow for a browser?::
|
|||||||
and WebKit in C++, with the
|
and WebKit in C++, with the
|
||||||
https://wiki.python.org/moin/GlobalInterpreterLock[GIL] released.
|
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?::
|
Is there an adblocker?::
|
||||||
There is a host-based adblocker which takes /etc/hosts-like lists. A "real"
|
There is a host-based adblocker which takes /etc/hosts-like lists. A "real"
|
||||||
adblocker has a
|
adblocker has a
|
||||||
@ -188,6 +213,37 @@ Why takes it longer to open an URL in qutebrowser than in chromium?::
|
|||||||
to use webengine as backend in line 17 and change it to your
|
to use webengine as backend in line 17 and change it to your
|
||||||
needs.
|
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
|
== Troubleshooting
|
||||||
|
|
||||||
Unable to view flash content.::
|
Unable to view flash content.::
|
||||||
|
@ -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}` expands to the URL of the current page
|
||||||
- `{url:pretty}` expands to the URL in decoded format
|
- `{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
|
- `{clipboard}` expands to the clipboard contents
|
||||||
- `{primary}` expands to the primary selection 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.
|
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
|
==== 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.
|
* +'command'+: The command to execute, with optional args.
|
||||||
|
|
||||||
==== optional arguments
|
==== optional arguments
|
||||||
@ -221,7 +223,7 @@ Syntax: +:buffer ['index']+
|
|||||||
|
|
||||||
Select tab by index or url/title best match.
|
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
|
==== positional arguments
|
||||||
* +'index'+: The [win_id/]index of the tab to focus. Or a substring in which case the closest match will be focused.
|
* +'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]]
|
||||||
=== 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.
|
Cycle an option between multiple values.
|
||||||
|
|
||||||
@ -283,6 +286,7 @@ Cycle an option between multiple values.
|
|||||||
* +'values'+: The values to cycle through.
|
* +'values'+: The values to cycle through.
|
||||||
|
|
||||||
==== optional arguments
|
==== optional arguments
|
||||||
|
* +*-u*+, +*--pattern*+: The URL pattern to use.
|
||||||
* +*-t*+, +*--temp*+: Set value temporarily until qutebrowser is closed.
|
* +*-t*+, +*--temp*+: Set value temporarily until qutebrowser is closed.
|
||||||
* +*-p*+, +*--print*+: Print the value after setting.
|
* +*-p*+, +*--print*+: Print the value after setting.
|
||||||
|
|
||||||
@ -495,10 +499,16 @@ Toggle fullscreen mode.
|
|||||||
|
|
||||||
[[greasemonkey-reload]]
|
[[greasemonkey-reload]]
|
||||||
=== greasemonkey-reload
|
=== greasemonkey-reload
|
||||||
|
Syntax: +:greasemonkey-reload [*--force*]+
|
||||||
|
|
||||||
Re-read Greasemonkey scripts from disk.
|
Re-read Greasemonkey scripts from disk.
|
||||||
|
|
||||||
The scripts are read from a 'greasemonkey' subdirectory in qutebrowser's data directory (see `:version`).
|
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]]
|
||||||
=== help
|
=== help
|
||||||
Syntax: +:help [*--tab*] [*--bg*] [*--window*] ['topic']+
|
Syntax: +:help [*--tab*] [*--bg*] [*--window*] ['topic']+
|
||||||
@ -1110,7 +1120,7 @@ Save a session.
|
|||||||
|
|
||||||
[[set]]
|
[[set]]
|
||||||
=== set
|
=== set
|
||||||
Syntax: +:set [*--temp*] [*--print*] ['option'] ['value']+
|
Syntax: +:set [*--temp*] [*--print*] [*--pattern* 'pattern'] ['option'] ['value']+
|
||||||
|
|
||||||
Set an option.
|
Set an option.
|
||||||
|
|
||||||
@ -1123,6 +1133,7 @@ If the option name ends with '?', the value of the option is shown instead. Usin
|
|||||||
==== optional arguments
|
==== optional arguments
|
||||||
* +*-t*+, +*--temp*+: Set value temporarily until qutebrowser is closed.
|
* +*-t*+, +*--temp*+: Set value temporarily until qutebrowser is closed.
|
||||||
* +*-p*+, +*--print*+: Print the value after setting.
|
* +*-p*+, +*--print*+: Print the value after setting.
|
||||||
|
* +*-u*+, +*--pattern*+: The URL pattern to use.
|
||||||
|
|
||||||
[[set-cmd-text]]
|
[[set-cmd-text]]
|
||||||
=== set-cmd-text
|
=== set-cmd-text
|
||||||
@ -1313,7 +1324,8 @@ Syntax: +:unbind [*--mode* 'mode'] 'key'+
|
|||||||
Unbind a keychain.
|
Unbind a keychain.
|
||||||
|
|
||||||
==== positional arguments
|
==== 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
|
==== optional arguments
|
||||||
* +*-m*+, +*--mode*+: A mode to unbind the key in (default: `normal`). See `:help bindings.commands` for the available modes.
|
* +*-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]]
|
||||||
=== follow-hint
|
=== follow-hint
|
||||||
Syntax: +:follow-hint ['keystring']+
|
Syntax: +:follow-hint [*--select*] ['keystring']+
|
||||||
|
|
||||||
Follow a hint.
|
Follow a hint.
|
||||||
|
|
||||||
==== positional arguments
|
==== positional arguments
|
||||||
* +'keystring'+: The hint to follow.
|
* +'keystring'+: The hint to follow.
|
||||||
|
|
||||||
|
==== optional arguments
|
||||||
|
* +*-s*+, +*--select*+: Only select the given hint, don't necessarily follow it.
|
||||||
|
|
||||||
[[leave-mode]]
|
[[leave-mode]]
|
||||||
=== leave-mode
|
=== leave-mode
|
||||||
Leave the mode we're currently in.
|
Leave the mode we're currently in.
|
||||||
|
@ -63,6 +63,10 @@ customizable.
|
|||||||
Using the link:commands.html#set[`:set`] command and command completion, you
|
Using the link:commands.html#set[`:set`] command and command completion, you
|
||||||
can quickly set settings interactively, for example `:set tabs.position left`.
|
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 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
|
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:
|
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`.
|
`c.colors.tabs.even.bg = c.colors.tabs.odd.bg`.
|
||||||
|
|
||||||
|
|
||||||
Using strings for setting names
|
Using strings for setting names
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
@ -171,6 +174,26 @@ To read a setting, use the `config.get` method:
|
|||||||
color = config.get('colors.completion.fg')
|
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
|
Binding keys
|
||||||
~~~~~~~~~~~~
|
~~~~~~~~~~~~
|
||||||
|
|
||||||
|
@ -201,6 +201,7 @@
|
|||||||
|<<hints.uppercase,hints.uppercase>>|Make characters in hint strings uppercase.
|
|<<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.
|
|<<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.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_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.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.
|
|<<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:
|
This setting is a dictionary containing mode names and dictionaries mapping keys to commands:
|
||||||
`{mode: {key: command}}`
|
`{mode: {key: command}}`
|
||||||
If you want to map a key to another key, check the `bindings.key_mappings` setting instead.
|
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`
|
* Control: `Control`, `Ctrl`
|
||||||
|
|
||||||
@ -358,11 +359,8 @@ The following modes are available:
|
|||||||
|
|
||||||
* prompt: Entered when there's a prompt to display, like for download
|
* prompt: Entered when there's a prompt to display, like for download
|
||||||
locations or when invoked from JavaScript.
|
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
|
* caret: Entered when pressing the `v` mode, used to select text using the
|
||||||
keyboard.
|
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.
|
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.
|
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>>
|
Type: <<types,Dict>>
|
||||||
|
|
||||||
Default:
|
Default:
|
||||||
@ -582,8 +582,20 @@ Default:
|
|||||||
* +pass:[sk]+: +pass:[set-cmd-text -s :bind]+
|
* +pass:[sk]+: +pass:[set-cmd-text -s :bind]+
|
||||||
* +pass:[sl]+: +pass:[set-cmd-text -s :set -t]+
|
* +pass:[sl]+: +pass:[set-cmd-text -s :set -t]+
|
||||||
* +pass:[ss]+: +pass:[set-cmd-text -s :set]+
|
* +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:[th]+: +pass:[back -t]+
|
||||||
* +pass:[tl]+: +pass:[forward -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:[u]+: +pass:[undo]+
|
||||||
* +pass:[v]+: +pass:[enter-mode caret]+
|
* +pass:[v]+: +pass:[enter-mode caret]+
|
||||||
* +pass:[wB]+: +pass:[set-cmd-text -s :bookmark-load -w]+
|
* +pass:[wB]+: +pass:[set-cmd-text -s :bookmark-load -w]+
|
||||||
@ -636,11 +648,17 @@ Default:
|
|||||||
* +pass:[<Shift-Tab>]+: +pass:[prompt-item-focus prev]+
|
* +pass:[<Shift-Tab>]+: +pass:[prompt-item-focus prev]+
|
||||||
* +pass:[<Tab>]+: +pass:[prompt-item-focus next]+
|
* +pass:[<Tab>]+: +pass:[prompt-item-focus next]+
|
||||||
* +pass:[<Up>]+: +pass:[prompt-item-focus prev]+
|
* +pass:[<Up>]+: +pass:[prompt-item-focus prev]+
|
||||||
* +pass:[n]+: +pass:[prompt-accept no]+
|
|
||||||
* +pass:[y]+: +pass:[prompt-accept yes]+
|
|
||||||
- +pass:[register]+:
|
- +pass:[register]+:
|
||||||
|
|
||||||
* +pass:[<Escape>]+: +pass:[leave-mode]+
|
* +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]]
|
||||||
=== bindings.key_mappings
|
=== bindings.key_mappings
|
||||||
@ -1447,6 +1465,8 @@ Default:
|
|||||||
Enable support for the HTML 5 web application cache feature.
|
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.
|
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>>
|
Type: <<types,Bool>>
|
||||||
|
|
||||||
Default: +pass:[true]+
|
Default: +pass:[true]+
|
||||||
@ -1524,6 +1544,8 @@ This setting is only available with the QtWebKit backend.
|
|||||||
=== content.dns_prefetch
|
=== content.dns_prefetch
|
||||||
Try to pre-fetch DNS entries to speed up browsing.
|
Try to pre-fetch DNS entries to speed up browsing.
|
||||||
|
|
||||||
|
This setting supports URL patterns.
|
||||||
|
|
||||||
Type: <<types,Bool>>
|
Type: <<types,Bool>>
|
||||||
|
|
||||||
Default: +pass:[true]+
|
Default: +pass:[true]+
|
||||||
@ -1535,6 +1557,8 @@ This setting is only available with the QtWebKit backend.
|
|||||||
Expand each subframe to its contents.
|
Expand each subframe to its contents.
|
||||||
This will flatten all the frames to become one scrollable page.
|
This will flatten all the frames to become one scrollable page.
|
||||||
|
|
||||||
|
This setting supports URL patterns.
|
||||||
|
|
||||||
Type: <<types,Bool>>
|
Type: <<types,Bool>>
|
||||||
|
|
||||||
Default: +pass:[false]+
|
Default: +pass:[false]+
|
||||||
@ -1651,6 +1675,8 @@ Default:
|
|||||||
=== content.hyperlink_auditing
|
=== content.hyperlink_auditing
|
||||||
Enable hyperlink auditing (`<a ping>`).
|
Enable hyperlink auditing (`<a ping>`).
|
||||||
|
|
||||||
|
This setting supports URL patterns.
|
||||||
|
|
||||||
Type: <<types,Bool>>
|
Type: <<types,Bool>>
|
||||||
|
|
||||||
Default: +pass:[false]+
|
Default: +pass:[false]+
|
||||||
@ -1659,6 +1685,8 @@ Default: +pass:[false]+
|
|||||||
=== content.images
|
=== content.images
|
||||||
Load images automatically in web pages.
|
Load images automatically in web pages.
|
||||||
|
|
||||||
|
This setting supports URL patterns.
|
||||||
|
|
||||||
Type: <<types,Bool>>
|
Type: <<types,Bool>>
|
||||||
|
|
||||||
Default: +pass:[true]+
|
Default: +pass:[true]+
|
||||||
@ -1676,6 +1704,8 @@ Default: +pass:[true]+
|
|||||||
Allow JavaScript to read from or write to the clipboard.
|
Allow JavaScript to read from or write to the clipboard.
|
||||||
With QtWebEngine, writing the clipboard as response to a user interaction is always allowed.
|
With QtWebEngine, writing the clipboard as response to a user interaction is always allowed.
|
||||||
|
|
||||||
|
This setting supports URL patterns.
|
||||||
|
|
||||||
Type: <<types,Bool>>
|
Type: <<types,Bool>>
|
||||||
|
|
||||||
Default: +pass:[false]+
|
Default: +pass:[false]+
|
||||||
@ -1684,6 +1714,8 @@ Default: +pass:[false]+
|
|||||||
=== content.javascript.can_close_tabs
|
=== content.javascript.can_close_tabs
|
||||||
Allow JavaScript to close tabs.
|
Allow JavaScript to close tabs.
|
||||||
|
|
||||||
|
This setting supports URL patterns.
|
||||||
|
|
||||||
Type: <<types,Bool>>
|
Type: <<types,Bool>>
|
||||||
|
|
||||||
Default: +pass:[false]+
|
Default: +pass:[false]+
|
||||||
@ -1694,6 +1726,8 @@ This setting is only available with the QtWebKit backend.
|
|||||||
=== content.javascript.can_open_tabs_automatically
|
=== content.javascript.can_open_tabs_automatically
|
||||||
Allow JavaScript to open new tabs without user interaction.
|
Allow JavaScript to open new tabs without user interaction.
|
||||||
|
|
||||||
|
This setting supports URL patterns.
|
||||||
|
|
||||||
Type: <<types,Bool>>
|
Type: <<types,Bool>>
|
||||||
|
|
||||||
Default: +pass:[false]+
|
Default: +pass:[false]+
|
||||||
@ -1702,6 +1736,8 @@ Default: +pass:[false]+
|
|||||||
=== content.javascript.enabled
|
=== content.javascript.enabled
|
||||||
Enable JavaScript.
|
Enable JavaScript.
|
||||||
|
|
||||||
|
This setting supports URL patterns.
|
||||||
|
|
||||||
Type: <<types,Bool>>
|
Type: <<types,Bool>>
|
||||||
|
|
||||||
Default: +pass:[true]+
|
Default: +pass:[true]+
|
||||||
@ -1741,6 +1777,8 @@ Default: +pass:[true]+
|
|||||||
=== content.local_content_can_access_file_urls
|
=== content.local_content_can_access_file_urls
|
||||||
Allow locally loaded documents to access other local URLs.
|
Allow locally loaded documents to access other local URLs.
|
||||||
|
|
||||||
|
This setting supports URL patterns.
|
||||||
|
|
||||||
Type: <<types,Bool>>
|
Type: <<types,Bool>>
|
||||||
|
|
||||||
Default: +pass:[true]+
|
Default: +pass:[true]+
|
||||||
@ -1749,6 +1787,8 @@ Default: +pass:[true]+
|
|||||||
=== content.local_content_can_access_remote_urls
|
=== content.local_content_can_access_remote_urls
|
||||||
Allow locally loaded documents to access remote URLs.
|
Allow locally loaded documents to access remote URLs.
|
||||||
|
|
||||||
|
This setting supports URL patterns.
|
||||||
|
|
||||||
Type: <<types,Bool>>
|
Type: <<types,Bool>>
|
||||||
|
|
||||||
Default: +pass:[false]+
|
Default: +pass:[false]+
|
||||||
@ -1757,6 +1797,8 @@ Default: +pass:[false]+
|
|||||||
=== content.local_storage
|
=== content.local_storage
|
||||||
Enable support for HTML 5 local storage and Web SQL.
|
Enable support for HTML 5 local storage and Web SQL.
|
||||||
|
|
||||||
|
This setting supports URL patterns.
|
||||||
|
|
||||||
Type: <<types,Bool>>
|
Type: <<types,Bool>>
|
||||||
|
|
||||||
Default: +pass:[true]+
|
Default: +pass:[true]+
|
||||||
@ -1817,6 +1859,8 @@ This setting is only available with the QtWebKit backend.
|
|||||||
=== content.plugins
|
=== content.plugins
|
||||||
Enable plugins in Web pages.
|
Enable plugins in Web pages.
|
||||||
|
|
||||||
|
This setting supports URL patterns.
|
||||||
|
|
||||||
Type: <<types,Bool>>
|
Type: <<types,Bool>>
|
||||||
|
|
||||||
Default: +pass:[false]+
|
Default: +pass:[false]+
|
||||||
@ -1825,6 +1869,8 @@ Default: +pass:[false]+
|
|||||||
=== content.print_element_backgrounds
|
=== content.print_element_backgrounds
|
||||||
Draw the background color and images also when the page is printed.
|
Draw the background color and images also when the page is printed.
|
||||||
|
|
||||||
|
This setting supports URL patterns.
|
||||||
|
|
||||||
Type: <<types,Bool>>
|
Type: <<types,Bool>>
|
||||||
|
|
||||||
Default: +pass:[true]+
|
Default: +pass:[true]+
|
||||||
@ -1889,6 +1935,8 @@ Default: empty
|
|||||||
=== content.webgl
|
=== content.webgl
|
||||||
Enable WebGL.
|
Enable WebGL.
|
||||||
|
|
||||||
|
This setting supports URL patterns.
|
||||||
|
|
||||||
Type: <<types,Bool>>
|
Type: <<types,Bool>>
|
||||||
|
|
||||||
Default: +pass:[true]+
|
Default: +pass:[true]+
|
||||||
@ -1906,6 +1954,8 @@ Default: +pass:[false]+
|
|||||||
Monitor load requests for cross-site scripting attempts.
|
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.
|
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>>
|
Type: <<types,Bool>>
|
||||||
|
|
||||||
Default: +pass:[false]+
|
Default: +pass:[false]+
|
||||||
@ -2351,6 +2401,14 @@ Valid values:
|
|||||||
|
|
||||||
Default: +pass:[auto]+
|
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]]
|
||||||
=== input.insert_mode.auto_leave
|
=== input.insert_mode.auto_leave
|
||||||
Leave insert mode if a non-editable element is clicked.
|
Leave insert mode if a non-editable element is clicked.
|
||||||
@ -2379,6 +2437,8 @@ Default: +pass:[false]+
|
|||||||
=== input.links_included_in_focus_chain
|
=== input.links_included_in_focus_chain
|
||||||
Include hyperlinks in the keyboard focus chain when tabbing.
|
Include hyperlinks in the keyboard focus chain when tabbing.
|
||||||
|
|
||||||
|
This setting supports URL patterns.
|
||||||
|
|
||||||
Type: <<types,Bool>>
|
Type: <<types,Bool>>
|
||||||
|
|
||||||
Default: +pass:[true]+
|
Default: +pass:[true]+
|
||||||
@ -2406,6 +2466,8 @@ Default: +pass:[false]+
|
|||||||
Enable spatial navigation.
|
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.
|
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>>
|
Type: <<types,Bool>>
|
||||||
|
|
||||||
Default: +pass:[false]+
|
Default: +pass:[false]+
|
||||||
@ -2550,6 +2612,8 @@ Default: +pass:[false]+
|
|||||||
Enable smooth scrolling for web pages.
|
Enable smooth scrolling for web pages.
|
||||||
Note smooth scrolling does not work with the `:scroll-px` command.
|
Note smooth scrolling does not work with the `:scroll-px` command.
|
||||||
|
|
||||||
|
This setting supports URL patterns.
|
||||||
|
|
||||||
Type: <<types,Bool>>
|
Type: <<types,Bool>>
|
||||||
|
|
||||||
Default: +pass:[false]+
|
Default: +pass:[false]+
|
||||||
@ -3137,6 +3201,8 @@ Default: +pass:[512]+
|
|||||||
=== zoom.text_only
|
=== zoom.text_only
|
||||||
Apply the zoom factor on a frame only to the text or to all content.
|
Apply the zoom factor on a frame only to the text or to all content.
|
||||||
|
|
||||||
|
This setting supports URL patterns.
|
||||||
|
|
||||||
Type: <<types,Bool>>
|
Type: <<types,Bool>>
|
||||||
|
|
||||||
Default: +pass:[false]+
|
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
|
QtWebEngine). However, it comes with Python 3.5, so you can
|
||||||
<<tox,install qutebrowser via tox>>.
|
<<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
|
Debian Stretch / Ubuntu 17.04 and 17.10
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
Those versions come with QtWebEngine in the repositories. This makes it possible
|
Those versions come with QtWebEngine in the repositories. This makes it possible
|
||||||
to install qutebrowser via the Debian package.
|
to install qutebrowser via the Debian package.
|
||||||
|
|
||||||
Get the qutebrowser package from the
|
Download the https://packages.debian.org/sid/all/qutebrowser/download[qutebrowser] and
|
||||||
https://github.com/qutebrowser/qutebrowser/releases[release page] and download
|
https://packages.debian.org/sid/all/python3-pypeg2/download[PyPEG2]
|
||||||
the https://qutebrowser.org/python3-pypeg2_2.15.2-1_all.deb[PyPEG2 package].
|
package from the Debian repositories.
|
||||||
|
|
||||||
(If you are using debian testing you can just use the python3-pypeg2 package from the repos)
|
|
||||||
|
|
||||||
Install the packages:
|
Install the packages:
|
||||||
|
|
||||||
@ -277,6 +281,11 @@ PS C:\> Install-Package qutebrowser
|
|||||||
----
|
----
|
||||||
C:\> choco install qutebrowser
|
C:\> choco install qutebrowser
|
||||||
----
|
----
|
||||||
|
* Scoop's client
|
||||||
|
----
|
||||||
|
C:\> scoop bucket add extras
|
||||||
|
C:\> scoop install qutebrowser
|
||||||
|
----
|
||||||
|
|
||||||
Manual install
|
Manual install
|
||||||
~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~
|
||||||
|
@ -22,9 +22,9 @@ Basic keybindings to get you started
|
|||||||
What to do now
|
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: +
|
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
|
* 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
|
course] on shortcutfoo for the keybindings - note that you need to be in
|
||||||
insert mode (i) for it to work.
|
insert mode (i) for it to work.
|
||||||
|
@ -32,22 +32,24 @@
|
|||||||
objecttolerance="10"
|
objecttolerance="10"
|
||||||
inkscape:pageopacity="0.0"
|
inkscape:pageopacity="0.0"
|
||||||
inkscape:pageshadow="2"
|
inkscape:pageshadow="2"
|
||||||
inkscape:zoom="1.24"
|
inkscape:zoom="1.7536248"
|
||||||
inkscape:cx="305.29152"
|
inkscape:cx="430.72917"
|
||||||
inkscape:cy="465.48793"
|
inkscape:cy="268.64059"
|
||||||
inkscape:document-units="px"
|
inkscape:document-units="px"
|
||||||
inkscape:current-layer="layer1"
|
inkscape:current-layer="layer1"
|
||||||
width="1024px"
|
width="1024px"
|
||||||
height="640px"
|
height="640px"
|
||||||
showgrid="false"
|
showgrid="false"
|
||||||
inkscape:window-width="1024"
|
inkscape:window-width="2560"
|
||||||
inkscape:window-height="723"
|
inkscape:window-height="1440"
|
||||||
inkscape:window-x="0"
|
inkscape:window-x="0"
|
||||||
inkscape:window-y="0"
|
inkscape:window-y="0"
|
||||||
showguides="true"
|
showguides="true"
|
||||||
inkscape:guide-bbox="true"
|
inkscape:guide-bbox="true"
|
||||||
inkscape:window-maximized="1"
|
inkscape:window-maximized="0"
|
||||||
inkscape:snap-text-baseline="true">
|
inkscape:snap-text-baseline="true"
|
||||||
|
inkscape:measure-start="0,0"
|
||||||
|
inkscape:measure-end="0,0">
|
||||||
<inkscape:grid
|
<inkscape:grid
|
||||||
id="GridFromPre046Settings"
|
id="GridFromPre046Settings"
|
||||||
type="xygrid"
|
type="xygrid"
|
||||||
@ -2688,7 +2690,8 @@
|
|||||||
id="flowPara5711"> </flowPara></flowRoot> <flowRoot
|
id="flowPara5711"> </flowPara></flowRoot> <flowRoot
|
||||||
xml:space="preserve"
|
xml:space="preserve"
|
||||||
id="flowRoot5691-0"
|
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"
|
id="flowRegion5693-7"
|
||||||
style="font-family:sans-serif;stroke-width:1.06666672"><rect
|
style="font-family:sans-serif;stroke-width:1.06666672"><rect
|
||||||
id="rect5695-0"
|
id="rect5695-0"
|
||||||
@ -3660,5 +3663,64 @@
|
|||||||
sodipodi:role="line"
|
sodipodi:role="line"
|
||||||
style="font-size:8.53333378px;line-height:0.89999998;stroke-width:1.06666672"
|
style="font-size:8.53333378px;line-height:0.89999998;stroke-width:1.06666672"
|
||||||
id="tspan6220">items</tspan></text>
|
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>
|
</svg>
|
||||||
|
Before Width: | Height: | Size: 178 KiB After Width: | Height: | Size: 181 KiB |
@ -3,7 +3,7 @@
|
|||||||
certifi==2018.1.18
|
certifi==2018.1.18
|
||||||
chardet==3.0.4
|
chardet==3.0.4
|
||||||
codecov==2.0.15
|
codecov==2.0.15
|
||||||
coverage==4.5
|
coverage==4.5.1
|
||||||
idna==2.6
|
idna==2.6
|
||||||
requests==2.18.4
|
requests==2.18.4
|
||||||
urllib3==1.22
|
urllib3==1.22
|
||||||
|
@ -6,12 +6,12 @@ flake8-bugbear==18.2.0
|
|||||||
flake8-builtins==1.0.post0
|
flake8-builtins==1.0.post0
|
||||||
flake8-comprehensions==1.4.1
|
flake8-comprehensions==1.4.1
|
||||||
flake8-copyright==0.2.0
|
flake8-copyright==0.2.0
|
||||||
flake8-debugger==3.0.0
|
flake8-debugger==3.1.0
|
||||||
flake8-deprecated==1.3
|
flake8-deprecated==1.3
|
||||||
flake8-docstrings==1.3.0
|
flake8-docstrings==1.3.0
|
||||||
flake8-future-import==0.4.4
|
flake8-future-import==0.4.4
|
||||||
flake8-mock==0.3
|
flake8-mock==0.3
|
||||||
flake8-per-file-ignores==0.4
|
flake8-per-file-ignores==0.5
|
||||||
flake8-polyfill==1.0.2
|
flake8-polyfill==1.0.2
|
||||||
flake8-string-format==0.2.3
|
flake8-string-format==0.2.3
|
||||||
flake8-tidy-imports==1.1.0
|
flake8-tidy-imports==1.1.0
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
# This file is automatically generated by scripts/dev/recompile_requirements.py
|
# This file is automatically generated by scripts/dev/recompile_requirements.py
|
||||||
|
|
||||||
appdirs==1.4.3
|
appdirs==1.4.3
|
||||||
packaging==16.8
|
packaging==17.1
|
||||||
pyparsing==2.2.0
|
pyparsing==2.2.0
|
||||||
setuptools==38.5.0
|
setuptools==38.5.2
|
||||||
six==1.11.0
|
six==1.11.0
|
||||||
wheel==0.30.0
|
wheel==0.30.0
|
||||||
|
@ -5,7 +5,7 @@ certifi==2018.1.18
|
|||||||
chardet==3.0.4
|
chardet==3.0.4
|
||||||
github3.py==0.9.6
|
github3.py==0.9.6
|
||||||
idna==2.6
|
idna==2.6
|
||||||
isort==4.3.2
|
isort==4.3.4
|
||||||
lazy-object-proxy==1.3.1
|
lazy-object-proxy==1.3.1
|
||||||
mccabe==0.6.1
|
mccabe==0.6.1
|
||||||
-e git+https://github.com/PyCQA/pylint.git#egg=pylint
|
-e git+https://github.com/PyCQA/pylint.git#egg=pylint
|
||||||
|
@ -5,7 +5,7 @@ certifi==2018.1.18
|
|||||||
chardet==3.0.4
|
chardet==3.0.4
|
||||||
github3.py==0.9.6
|
github3.py==0.9.6
|
||||||
idna==2.6
|
idna==2.6
|
||||||
isort==4.3.2
|
isort==4.3.4
|
||||||
lazy-object-proxy==1.3.1
|
lazy-object-proxy==1.3.1
|
||||||
mccabe==0.6.1
|
mccabe==0.6.1
|
||||||
pylint==1.8.2
|
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
|
# This file is automatically generated by scripts/dev/recompile_requirements.py
|
||||||
|
|
||||||
PyQt5==5.10
|
PyQt5==5.10.1
|
||||||
sip==4.19.7
|
sip==4.19.8
|
||||||
|
@ -5,13 +5,13 @@ beautifulsoup4==4.6.0
|
|||||||
cheroot==6.0.0
|
cheroot==6.0.0
|
||||||
click==6.7
|
click==6.7
|
||||||
# colorama==0.3.9
|
# colorama==0.3.9
|
||||||
coverage==4.5
|
coverage==4.5.1
|
||||||
EasyProcess==0.2.3
|
EasyProcess==0.2.3
|
||||||
fields==5.0.0
|
fields==5.0.0
|
||||||
Flask==0.12.2
|
Flask==0.12.2
|
||||||
glob2==0.6
|
glob2==0.6
|
||||||
hunter==2.0.2
|
hunter==2.0.2
|
||||||
hypothesis==3.44.25
|
hypothesis==3.49.0
|
||||||
itsdangerous==0.24
|
itsdangerous==0.24
|
||||||
# Jinja2==2.10
|
# Jinja2==2.10
|
||||||
Mako==1.0.7
|
Mako==1.0.7
|
||||||
@ -22,18 +22,18 @@ parse-type==0.4.2
|
|||||||
pluggy==0.6.0
|
pluggy==0.6.0
|
||||||
py==1.5.2
|
py==1.5.2
|
||||||
py-cpuinfo==3.3.0
|
py-cpuinfo==3.3.0
|
||||||
pytest==3.4.0
|
pytest==3.4.2
|
||||||
pytest-bdd==2.20.0
|
pytest-bdd==2.20.0
|
||||||
pytest-benchmark==3.1.1
|
pytest-benchmark==3.1.1
|
||||||
pytest-cov==2.5.1
|
pytest-cov==2.5.1
|
||||||
pytest-faulthandler==1.3.1
|
pytest-faulthandler==1.4.1
|
||||||
pytest-instafail==0.3.0
|
pytest-instafail==0.3.0
|
||||||
pytest-mock==1.6.3
|
pytest-mock==1.7.1
|
||||||
pytest-qt==2.3.1
|
pytest-qt==2.3.1
|
||||||
pytest-repeat==0.4.1
|
pytest-repeat==0.4.1
|
||||||
pytest-rerunfailures==4.0
|
pytest-rerunfailures==4.0
|
||||||
pytest-travis-fold==1.3.0
|
pytest-travis-fold==1.3.0
|
||||||
pytest-xvfb==1.0.0
|
pytest-xvfb==1.1.0
|
||||||
PyVirtualDisplay==0.2.1
|
PyVirtualDisplay==0.2.1
|
||||||
six==1.11.0
|
six==1.11.0
|
||||||
vulture==0.26
|
vulture==0.26
|
||||||
|
@ -52,7 +52,7 @@ die() {
|
|||||||
if ! [ -d "$DOWNLOAD_DIR" ] ; then
|
if ! [ -d "$DOWNLOAD_DIR" ] ; then
|
||||||
die "Download directory »$DOWNLOAD_DIR« not found!"
|
die "Download directory »$DOWNLOAD_DIR« not found!"
|
||||||
fi
|
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!"
|
die "Rofi command »${ROFI_CMD}« not found in PATH!"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
@ -220,7 +220,7 @@ user_pattern='^(user|username|login): '
|
|||||||
GPG_OPTS=( "--quiet" "--yes" "--compress-algo=none" "--no-encrypt-to" )
|
GPG_OPTS=( "--quiet" "--yes" "--compress-algo=none" "--no-encrypt-to" )
|
||||||
GPG="gpg"
|
GPG="gpg"
|
||||||
export GPG_TTY="${GPG_TTY:-$(tty 2>/dev/null)}"
|
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" )
|
[[ -n $GPG_AGENT_INFO || $GPG == "gpg2" ]] && GPG_OPTS+=( "--batch" "--use-agent" )
|
||||||
|
|
||||||
pass_backend() {
|
pass_backend() {
|
||||||
|
@ -13,7 +13,11 @@
|
|||||||
from __future__ import absolute_import
|
from __future__ import absolute_import
|
||||||
import codecs, os
|
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)):
|
if not os.path.exists(os.path.dirname(tmpfile)):
|
||||||
os.makedirs(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
|
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
|
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
|
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
|
fake_os: Fake utils.is_* to a fake operating system
|
||||||
unicode_locale: Tests which need an unicode locale to work
|
unicode_locale: Tests which need an unicode locale to work
|
||||||
qt_log_level_fail = WARNING
|
qt_log_level_fail = WARNING
|
||||||
|
@ -26,7 +26,7 @@ __copyright__ = "Copyright 2014-2018 Florian Bruhin (The Compiler)"
|
|||||||
__license__ = "GPL"
|
__license__ = "GPL"
|
||||||
__maintainer__ = __author__
|
__maintainer__ = __author__
|
||||||
__email__ = "mail@qutebrowser.org"
|
__email__ = "mail@qutebrowser.org"
|
||||||
__version_info__ = (1, 1, 1)
|
__version_info__ = (1, 2, 1)
|
||||||
__version__ = '.'.join(str(e) for e in __version_info__)
|
__version__ = '.'.join(str(e) for e in __version_info__)
|
||||||
__description__ = "A keyboard-driven, vim-like browser based on PyQt5."
|
__description__ = "A keyboard-driven, vim-like browser based on PyQt5."
|
||||||
|
|
||||||
|
@ -95,6 +95,7 @@ def run(args):
|
|||||||
|
|
||||||
log.init.debug("Initializing directories...")
|
log.init.debug("Initializing directories...")
|
||||||
standarddir.init(args)
|
standarddir.init(args)
|
||||||
|
utils.preload_resources()
|
||||||
|
|
||||||
log.init.debug("Initializing config...")
|
log.init.debug("Initializing config...")
|
||||||
configinit.early_init(args)
|
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
|
for cur_win_id in list(window_ids): # Copying as the dict could change
|
||||||
tabbed_browser = objreg.get('tabbed-browser', scope='window',
|
tabbed_browser = objreg.get('tabbed-browser', scope='window',
|
||||||
window=cur_win_id)
|
window=cur_win_id)
|
||||||
if tabbed_browser.count() == 0:
|
if tabbed_browser.widget.count() == 0:
|
||||||
log.init.debug("Opening start pages")
|
log.init.debug("Opening start pages")
|
||||||
for url in config.val.url.start_pages:
|
for url in config.val.url.start_pages:
|
||||||
tabbed_browser.tabopen(url)
|
tabbed_browser.tabopen(url)
|
||||||
@ -772,6 +773,8 @@ class Quitter:
|
|||||||
pre_text="Error while saving {}".format(key))
|
pre_text="Error while saving {}".format(key))
|
||||||
# Disable storage so removing tempdir will work
|
# Disable storage so removing tempdir will work
|
||||||
websettings.shutdown()
|
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
|
# Re-enable faulthandler to stdout, then remove crash log
|
||||||
log.destroy.debug("Deactivating crash log...")
|
log.destroy.debug("Deactivating crash log...")
|
||||||
objreg.get('crash-handler').destroy_crashlogfile()
|
objreg.get('crash-handler').destroy_crashlogfile()
|
||||||
@ -840,7 +843,11 @@ class Application(QApplication):
|
|||||||
def event(self, e):
|
def event(self, e):
|
||||||
"""Handle macOS FileOpen events."""
|
"""Handle macOS FileOpen events."""
|
||||||
if e.type() == QEvent.FileOpen:
|
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:
|
else:
|
||||||
return super().event(e)
|
return super().event(e)
|
||||||
|
|
||||||
@ -878,6 +885,7 @@ class EventFilter(QObject):
|
|||||||
self._handlers = {
|
self._handlers = {
|
||||||
QEvent.KeyPress: self._handle_key_event,
|
QEvent.KeyPress: self._handle_key_event,
|
||||||
QEvent.KeyRelease: self._handle_key_event,
|
QEvent.KeyRelease: self._handle_key_event,
|
||||||
|
QEvent.ShortcutOverride: self._handle_key_event,
|
||||||
}
|
}
|
||||||
|
|
||||||
def _handle_key_event(self, event):
|
def _handle_key_event(self, event):
|
||||||
@ -895,7 +903,7 @@ class EventFilter(QObject):
|
|||||||
return False
|
return False
|
||||||
try:
|
try:
|
||||||
man = objreg.get('mode-manager', scope='window', window='current')
|
man = objreg.get('mode-manager', scope='window', window='current')
|
||||||
return man.eventFilter(event)
|
return man.handle_event(event)
|
||||||
except objreg.RegistryUnavailableError:
|
except objreg.RegistryUnavailableError:
|
||||||
# No window available yet, or not a MainWindow
|
# No window available yet, or not a MainWindow
|
||||||
return False
|
return False
|
||||||
|
@ -30,7 +30,8 @@ from PyQt5.QtWidgets import QWidget, QApplication
|
|||||||
|
|
||||||
from qutebrowser.keyinput import modeman
|
from qutebrowser.keyinput import modeman
|
||||||
from qutebrowser.config import config
|
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.misc import miscwidgets, objects
|
||||||
from qutebrowser.browser import mouse, hints
|
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
|
keep_icon: Whether the (e.g. cloned) icon should not be cleared on page
|
||||||
load.
|
load.
|
||||||
inspector: The QWebInspector used for this webview.
|
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).
|
override_target: Override for open_target for fake clicks (like hints).
|
||||||
Only used for QtWebKit.
|
Only used for QtWebKit.
|
||||||
pinned: Flag to pin the tab.
|
pinned: Flag to pin the tab.
|
||||||
@ -104,6 +107,7 @@ class TabData:
|
|||||||
|
|
||||||
keep_icon = attr.ib(False)
|
keep_icon = attr.ib(False)
|
||||||
inspector = attr.ib(None)
|
inspector = attr.ib(None)
|
||||||
|
open_target = attr.ib(usertypes.ClickTarget.normal)
|
||||||
override_target = attr.ib(None)
|
override_target = attr.ib(None)
|
||||||
pinned = attr.ib(False)
|
pinned = attr.ib(False)
|
||||||
fullscreen = attr.ib(False)
|
fullscreen = attr.ib(False)
|
||||||
@ -342,7 +346,7 @@ class AbstractCaret(QObject):
|
|||||||
def _on_mode_entered(self, mode):
|
def _on_mode_entered(self, mode):
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
def _on_mode_left(self):
|
def _on_mode_left(self, mode):
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
def move_to_next_line(self, count=1):
|
def move_to_next_line(self, count=1):
|
||||||
@ -612,6 +616,7 @@ class AbstractTab(QWidget):
|
|||||||
process terminated.
|
process terminated.
|
||||||
arg 0: A TerminationStatus member.
|
arg 0: A TerminationStatus member.
|
||||||
arg 1: The exit code.
|
arg 1: The exit code.
|
||||||
|
predicted_navigation: Emitted before we tell Qt to open a URL.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
window_close_requested = pyqtSignal()
|
window_close_requested = pyqtSignal()
|
||||||
@ -629,6 +634,7 @@ class AbstractTab(QWidget):
|
|||||||
add_history_item = pyqtSignal(QUrl, QUrl, str) # url, requested url, title
|
add_history_item = pyqtSignal(QUrl, QUrl, str) # url, requested url, title
|
||||||
fullscreen_requested = pyqtSignal(bool)
|
fullscreen_requested = pyqtSignal(bool)
|
||||||
renderer_process_terminated = pyqtSignal(TerminationStatus, int)
|
renderer_process_terminated = pyqtSignal(TerminationStatus, int)
|
||||||
|
predicted_navigation = pyqtSignal(QUrl)
|
||||||
|
|
||||||
def __init__(self, *, win_id, mode_manager, private, parent=None):
|
def __init__(self, *, win_id, mode_manager, private, parent=None):
|
||||||
self.private = private
|
self.private = private
|
||||||
@ -659,6 +665,8 @@ class AbstractTab(QWidget):
|
|||||||
objreg.register('hintmanager', hintmanager, scope='tab',
|
objreg.register('hintmanager', hintmanager, scope='tab',
|
||||||
window=self.win_id, tab=self.tab_id)
|
window=self.win_id, tab=self.tab_id)
|
||||||
|
|
||||||
|
self.predicted_navigation.connect(self._on_predicted_navigation)
|
||||||
|
|
||||||
def _set_widget(self, widget):
|
def _set_widget(self, widget):
|
||||||
# pylint: disable=protected-access
|
# pylint: disable=protected-access
|
||||||
self._widget = widget
|
self._widget = widget
|
||||||
@ -671,6 +679,7 @@ class AbstractTab(QWidget):
|
|||||||
self.printing._widget = widget
|
self.printing._widget = widget
|
||||||
self.action._widget = widget
|
self.action._widget = widget
|
||||||
self.elements._widget = widget
|
self.elements._widget = widget
|
||||||
|
self.settings._settings = widget.settings()
|
||||||
|
|
||||||
self._install_event_filter()
|
self._install_event_filter()
|
||||||
self.zoom.set_default()
|
self.zoom.set_default()
|
||||||
@ -705,6 +714,14 @@ class AbstractTab(QWidget):
|
|||||||
evt.posted = True
|
evt.posted = True
|
||||||
QApplication.postEvent(recipient, evt)
|
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)
|
@pyqtSlot(QUrl)
|
||||||
def _on_url_changed(self, url):
|
def _on_url_changed(self, url):
|
||||||
"""Update title when URL has changed and no title is available."""
|
"""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._set_load_status(usertypes.LoadStatus.loading)
|
||||||
self.load_started.emit()
|
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):
|
def handle_auto_insert_mode(self, ok):
|
||||||
"""Handle `input.insert_mode.auto_load` after loading finished."""
|
"""Handle `input.insert_mode.auto_load` after loading finished."""
|
||||||
if not config.val.input.insert_mode.auto_load or not ok:
|
if not config.val.input.insert_mode.auto_load or not ok:
|
||||||
@ -788,11 +822,12 @@ class AbstractTab(QWidget):
|
|||||||
def load_status(self):
|
def load_status(self):
|
||||||
return self._load_status
|
return self._load_status
|
||||||
|
|
||||||
def _openurl_prepare(self, url):
|
def _openurl_prepare(self, url, *, predict=True):
|
||||||
qtutils.ensure_valid(url)
|
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
|
raise NotImplementedError
|
||||||
|
|
||||||
def reload(self, *, force=False):
|
def reload(self, *, force=False):
|
||||||
|
@ -27,14 +27,13 @@ import typing
|
|||||||
|
|
||||||
from PyQt5.QtWidgets import QApplication, QTabBar, QDialog
|
from PyQt5.QtWidgets import QApplication, QTabBar, QDialog
|
||||||
from PyQt5.QtCore import pyqtSlot, Qt, QUrl, QEvent, QUrlQuery
|
from PyQt5.QtCore import pyqtSlot, Qt, QUrl, QEvent, QUrlQuery
|
||||||
from PyQt5.QtGui import QKeyEvent
|
|
||||||
from PyQt5.QtPrintSupport import QPrintDialog, QPrintPreviewDialog
|
from PyQt5.QtPrintSupport import QPrintDialog, QPrintPreviewDialog
|
||||||
|
|
||||||
from qutebrowser.commands import userscripts, cmdexc, cmdutils, runners
|
from qutebrowser.commands import userscripts, cmdexc, cmdutils, runners
|
||||||
from qutebrowser.config import config, configdata
|
from qutebrowser.config import config, configdata
|
||||||
from qutebrowser.browser import (urlmarks, browsertab, inspector, navigate,
|
from qutebrowser.browser import (urlmarks, browsertab, inspector, navigate,
|
||||||
webelem, downloads)
|
webelem, downloads)
|
||||||
from qutebrowser.keyinput import modeman
|
from qutebrowser.keyinput import modeman, keyutils
|
||||||
from qutebrowser.utils import (message, usertypes, log, qtutils, urlutils,
|
from qutebrowser.utils import (message, usertypes, log, qtutils, urlutils,
|
||||||
objreg, utils, standarddir)
|
objreg, utils, standarddir)
|
||||||
from qutebrowser.utils.usertypes import KeyMode
|
from qutebrowser.utils.usertypes import KeyMode
|
||||||
@ -54,7 +53,6 @@ class CommandDispatcher:
|
|||||||
cmdutils.register() decorators are run, currentWidget() will return None.
|
cmdutils.register() decorators are run, currentWidget() will return None.
|
||||||
|
|
||||||
Attributes:
|
Attributes:
|
||||||
_editor: The ExternalEditor object.
|
|
||||||
_win_id: The window ID the CommandDispatcher is associated with.
|
_win_id: The window ID the CommandDispatcher is associated with.
|
||||||
_tabbed_browser: The TabbedBrowser used.
|
_tabbed_browser: The TabbedBrowser used.
|
||||||
"""
|
"""
|
||||||
@ -74,16 +72,16 @@ class CommandDispatcher:
|
|||||||
|
|
||||||
def _count(self):
|
def _count(self):
|
||||||
"""Convenience method to get the widget count."""
|
"""Convenience method to get the widget count."""
|
||||||
return self._tabbed_browser.count()
|
return self._tabbed_browser.widget.count()
|
||||||
|
|
||||||
def _set_current_index(self, idx):
|
def _set_current_index(self, idx):
|
||||||
"""Convenience method to set the current widget index."""
|
"""Convenience method to set the current widget index."""
|
||||||
cmdutils.check_overflow(idx, 'int')
|
cmdutils.check_overflow(idx, 'int')
|
||||||
self._tabbed_browser.setCurrentIndex(idx)
|
self._tabbed_browser.widget.setCurrentIndex(idx)
|
||||||
|
|
||||||
def _current_index(self):
|
def _current_index(self):
|
||||||
"""Convenience method to get the current widget index."""
|
"""Convenience method to get the current widget index."""
|
||||||
return self._tabbed_browser.currentIndex()
|
return self._tabbed_browser.widget.currentIndex()
|
||||||
|
|
||||||
def _current_url(self):
|
def _current_url(self):
|
||||||
"""Convenience method to get the current url."""
|
"""Convenience method to get the current url."""
|
||||||
@ -102,7 +100,7 @@ class CommandDispatcher:
|
|||||||
|
|
||||||
def _current_widget(self):
|
def _current_widget(self):
|
||||||
"""Get the currently active widget from a command."""
|
"""Get the currently active widget from a command."""
|
||||||
widget = self._tabbed_browser.currentWidget()
|
widget = self._tabbed_browser.widget.currentWidget()
|
||||||
if widget is None:
|
if widget is None:
|
||||||
raise cmdexc.CommandError("No WebView available yet!")
|
raise cmdexc.CommandError("No WebView available yet!")
|
||||||
return widget
|
return widget
|
||||||
@ -148,10 +146,10 @@ class CommandDispatcher:
|
|||||||
None if no widget was found.
|
None if no widget was found.
|
||||||
"""
|
"""
|
||||||
if count is None:
|
if count is None:
|
||||||
return self._tabbed_browser.currentWidget()
|
return self._tabbed_browser.widget.currentWidget()
|
||||||
elif 1 <= count <= self._count():
|
elif 1 <= count <= self._count():
|
||||||
cmdutils.check_overflow(count + 1, 'int')
|
cmdutils.check_overflow(count + 1, 'int')
|
||||||
return self._tabbed_browser.widget(count - 1)
|
return self._tabbed_browser.widget.widget(count - 1)
|
||||||
else:
|
else:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@ -164,7 +162,7 @@ class CommandDispatcher:
|
|||||||
if not show_error:
|
if not show_error:
|
||||||
return
|
return
|
||||||
raise cmdexc.CommandError("No last focused tab!")
|
raise cmdexc.CommandError("No last focused tab!")
|
||||||
idx = self._tabbed_browser.indexOf(tab)
|
idx = self._tabbed_browser.widget.indexOf(tab)
|
||||||
if idx == -1:
|
if idx == -1:
|
||||||
raise cmdexc.CommandError("Last focused tab vanished!")
|
raise cmdexc.CommandError("Last focused tab vanished!")
|
||||||
self._set_current_index(idx)
|
self._set_current_index(idx)
|
||||||
@ -213,7 +211,7 @@ class CommandDispatcher:
|
|||||||
what's configured in 'tabs.select_on_remove'.
|
what's configured in 'tabs.select_on_remove'.
|
||||||
count: The tab index to close, or None
|
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_,
|
selection_override = self._get_selection_override(prev, next_,
|
||||||
opposite)
|
opposite)
|
||||||
|
|
||||||
@ -265,7 +263,7 @@ class CommandDispatcher:
|
|||||||
return
|
return
|
||||||
|
|
||||||
to_pin = not tab.data.pinned
|
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',
|
@cmdutils.register(instance='command-dispatcher', name='open',
|
||||||
maxsplit=0, scope='window')
|
maxsplit=0, scope='window')
|
||||||
@ -484,7 +482,8 @@ class CommandDispatcher:
|
|||||||
"""
|
"""
|
||||||
cmdutils.check_exclusive((bg, window), 'bw')
|
cmdutils.check_exclusive((bg, window), 'bw')
|
||||||
curtab = self._current_widget()
|
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:
|
try:
|
||||||
history = curtab.history.serialize()
|
history = curtab.history.serialize()
|
||||||
except browsertab.WebTabError as e:
|
except browsertab.WebTabError as e:
|
||||||
@ -500,18 +499,18 @@ class CommandDispatcher:
|
|||||||
newtab = new_tabbed_browser.tabopen(background=bg)
|
newtab = new_tabbed_browser.tabopen(background=bg)
|
||||||
new_tabbed_browser = objreg.get('tabbed-browser', scope='window',
|
new_tabbed_browser = objreg.get('tabbed-browser', scope='window',
|
||||||
window=newtab.win_id)
|
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:
|
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:
|
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.data.keep_icon = True
|
||||||
newtab.history.deserialize(history)
|
newtab.history.deserialize(history)
|
||||||
newtab.zoom.set_factor(curtab.zoom.factor())
|
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
|
return newtab
|
||||||
|
|
||||||
@cmdutils.register(instance='command-dispatcher', scope='window')
|
@cmdutils.register(instance='command-dispatcher', scope='window')
|
||||||
@ -847,7 +846,7 @@ class CommandDispatcher:
|
|||||||
keep: Stay in visual mode after yanking the selection.
|
keep: Stay in visual mode after yanking the selection.
|
||||||
"""
|
"""
|
||||||
if what == 'title':
|
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':
|
elif what == 'domain':
|
||||||
port = self._current_url().port()
|
port = self._current_url().port()
|
||||||
s = '{}://{}{}'.format(self._current_url().scheme(),
|
s = '{}://{}{}'.format(self._current_url().scheme(),
|
||||||
@ -959,7 +958,7 @@ class CommandDispatcher:
|
|||||||
force: Avoid confirmation for pinned tabs.
|
force: Avoid confirmation for pinned tabs.
|
||||||
"""
|
"""
|
||||||
cmdutils.check_exclusive((prev, next_), 'pn')
|
cmdutils.check_exclusive((prev, next_), 'pn')
|
||||||
cur_idx = self._tabbed_browser.currentIndex()
|
cur_idx = self._tabbed_browser.widget.currentIndex()
|
||||||
assert cur_idx != -1
|
assert cur_idx != -1
|
||||||
|
|
||||||
def _to_close(i):
|
def _to_close(i):
|
||||||
@ -1076,11 +1075,11 @@ class CommandDispatcher:
|
|||||||
|
|
||||||
tabbed_browser = objreg.get('tabbed-browser', scope='window',
|
tabbed_browser = objreg.get('tabbed-browser', scope='window',
|
||||||
window=win_id)
|
window=win_id)
|
||||||
if not 0 < idx <= tabbed_browser.count():
|
if not 0 < idx <= tabbed_browser.widget.count():
|
||||||
raise cmdexc.CommandError(
|
raise cmdexc.CommandError(
|
||||||
"There's no tab with index {}!".format(idx))
|
"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',
|
@cmdutils.register(instance='command-dispatcher', scope='window',
|
||||||
maxsplit=0)
|
maxsplit=0)
|
||||||
@ -1092,24 +1091,26 @@ class CommandDispatcher:
|
|||||||
Focuses window if necessary when index is given. If both index and
|
Focuses window if necessary when index is given. If both index and
|
||||||
count are given, use count.
|
count are given, use count.
|
||||||
|
|
||||||
|
With neither index nor count given, open the qute://tabs page.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
index: The [win_id/]index of the tab to focus. Or a substring
|
index: The [win_id/]index of the tab to focus. Or a substring
|
||||||
in which case the closest match will be focused.
|
in which case the closest match will be focused.
|
||||||
count: The tab index to focus, starting with 1.
|
count: The tab index to focus, starting with 1.
|
||||||
"""
|
"""
|
||||||
if count is None and index is None:
|
if count is None and index is None:
|
||||||
raise cmdexc.CommandError("buffer: Either a count or the argument "
|
self.openurl('qute://tabs/', tab=True)
|
||||||
"index must be specified.")
|
return
|
||||||
|
|
||||||
if count is not None:
|
if count is not None:
|
||||||
index = str(count)
|
index = str(count)
|
||||||
|
|
||||||
tabbed_browser, tab = self._resolve_buffer_index(index)
|
tabbed_browser, tab = self._resolve_buffer_index(index)
|
||||||
|
|
||||||
window = tabbed_browser.window()
|
window = tabbed_browser.widget.window()
|
||||||
window.activateWindow()
|
window.activateWindow()
|
||||||
window.raise_()
|
window.raise_()
|
||||||
tabbed_browser.setCurrentWidget(tab)
|
tabbed_browser.widget.setCurrentWidget(tab)
|
||||||
|
|
||||||
@cmdutils.register(instance='command-dispatcher', scope='window')
|
@cmdutils.register(instance='command-dispatcher', scope='window')
|
||||||
@cmdutils.argument('index', choices=['last'])
|
@cmdutils.argument('index', choices=['last'])
|
||||||
@ -1193,7 +1194,7 @@ class CommandDispatcher:
|
|||||||
cur_idx = self._current_index()
|
cur_idx = self._current_index()
|
||||||
cmdutils.check_overflow(cur_idx, 'int')
|
cmdutils.check_overflow(cur_idx, 'int')
|
||||||
cmdutils.check_overflow(new_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',
|
@cmdutils.register(instance='command-dispatcher', scope='window',
|
||||||
maxsplit=0, no_replace_variables=True)
|
maxsplit=0, no_replace_variables=True)
|
||||||
@ -1277,10 +1278,10 @@ class CommandDispatcher:
|
|||||||
|
|
||||||
idx = self._current_index()
|
idx = self._current_index()
|
||||||
if idx != -1:
|
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!
|
# FIXME:qtwebengine: If tab is None, run_async will fail!
|
||||||
tab = self._tabbed_browser.currentWidget()
|
tab = self._tabbed_browser.widget.currentWidget()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
url = self._tabbed_browser.current_url()
|
url = self._tabbed_browser.current_url()
|
||||||
@ -1638,7 +1639,7 @@ class CommandDispatcher:
|
|||||||
|
|
||||||
ed = editor.ExternalEditor(watch=True, parent=self._tabbed_browser)
|
ed = editor.ExternalEditor(watch=True, parent=self._tabbed_browser)
|
||||||
ed.file_updated.connect(functools.partial(
|
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(
|
ed.editing_finished.connect(lambda: mainwindow.raise_window(
|
||||||
objreg.last_focused_window(), alert=False))
|
objreg.last_focused_window(), alert=False))
|
||||||
ed.edit(text, caret_position)
|
ed.edit(text, caret_position)
|
||||||
@ -1653,7 +1654,7 @@ class CommandDispatcher:
|
|||||||
tab = self._current_widget()
|
tab = self._current_widget()
|
||||||
tab.elements.find_focused(self._open_editor_cb)
|
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.
|
"""Write the editor text into the form field and clean up tempfile.
|
||||||
|
|
||||||
Callback for GUIProcess when the edited text was updated.
|
Callback for GUIProcess when the edited text was updated.
|
||||||
@ -1666,8 +1667,10 @@ class CommandDispatcher:
|
|||||||
elem.set_value(text)
|
elem.set_value(text)
|
||||||
except webelem.OrphanedError as e:
|
except webelem.OrphanedError as e:
|
||||||
message.error('Edited element vanished')
|
message.error('Edited element vanished')
|
||||||
|
ed.backup()
|
||||||
except webelem.Error as e:
|
except webelem.Error as e:
|
||||||
raise cmdexc.CommandError(str(e))
|
message.error(str(e))
|
||||||
|
ed.backup()
|
||||||
|
|
||||||
@cmdutils.register(instance='command-dispatcher', maxsplit=0,
|
@cmdutils.register(instance='command-dispatcher', maxsplit=0,
|
||||||
scope='window')
|
scope='window')
|
||||||
@ -1776,10 +1779,10 @@ class CommandDispatcher:
|
|||||||
"""
|
"""
|
||||||
self.set_mark("'")
|
self.set_mark("'")
|
||||||
tab = self._current_widget()
|
tab = self._current_widget()
|
||||||
if tab.search.search_displayed:
|
|
||||||
tab.search.clear()
|
|
||||||
|
|
||||||
if not text:
|
if not text:
|
||||||
|
if tab.search.search_displayed:
|
||||||
|
tab.search.clear()
|
||||||
return
|
return
|
||||||
|
|
||||||
options = {
|
options = {
|
||||||
@ -2110,15 +2113,13 @@ class CommandDispatcher:
|
|||||||
global_: If given, the keys are sent to the qutebrowser UI.
|
global_: If given, the keys are sent to the qutebrowser UI.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
keyinfos = utils.parse_keystring(keystring)
|
sequence = keyutils.KeySequence.parse(keystring)
|
||||||
except utils.KeyParseError as e:
|
except keyutils.KeyParseError as e:
|
||||||
raise cmdexc.CommandError(str(e))
|
raise cmdexc.CommandError(str(e))
|
||||||
|
|
||||||
for keyinfo in keyinfos:
|
for keyinfo in sequence:
|
||||||
press_event = QKeyEvent(QEvent.KeyPress, keyinfo.key,
|
press_event = keyinfo.to_event(QEvent.KeyPress)
|
||||||
keyinfo.modifiers, keyinfo.text)
|
release_event = keyinfo.to_event(QEvent.KeyRelease)
|
||||||
release_event = QKeyEvent(QEvent.KeyRelease, keyinfo.key,
|
|
||||||
keyinfo.modifiers, keyinfo.text)
|
|
||||||
|
|
||||||
if global_:
|
if global_:
|
||||||
window = QApplication.focusWindow()
|
window = QApplication.focusWindow()
|
||||||
@ -2218,5 +2219,5 @@ class CommandDispatcher:
|
|||||||
pass
|
pass
|
||||||
return
|
return
|
||||||
|
|
||||||
window = self._tabbed_browser.window()
|
window = self._tabbed_browser.widget.window()
|
||||||
window.setWindowState(window.windowState() ^ Qt.WindowFullScreen)
|
window.setWindowState(window.windowState() ^ Qt.WindowFullScreen)
|
||||||
|
@ -238,11 +238,14 @@ class FileDownloadTarget(_DownloadTarget):
|
|||||||
|
|
||||||
Attributes:
|
Attributes:
|
||||||
filename: Filename where the download should be saved.
|
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
|
# pylint: disable=super-init-not-called
|
||||||
self.filename = filename
|
self.filename = filename
|
||||||
|
self.force_overwrite = force_overwrite
|
||||||
|
|
||||||
def suggested_filename(self):
|
def suggested_filename(self):
|
||||||
return os.path.basename(self.filename)
|
return os.path.basename(self.filename)
|
||||||
@ -738,7 +741,8 @@ class AbstractDownloadItem(QObject):
|
|||||||
if isinstance(target, FileObjDownloadTarget):
|
if isinstance(target, FileObjDownloadTarget):
|
||||||
self._set_fileobj(target.fileobj, autoclose=False)
|
self._set_fileobj(target.fileobj, autoclose=False)
|
||||||
elif isinstance(target, FileDownloadTarget):
|
elif isinstance(target, FileDownloadTarget):
|
||||||
self._set_filename(target.filename)
|
self._set_filename(
|
||||||
|
target.filename, force_overwrite=target.force_overwrite)
|
||||||
elif isinstance(target, OpenFileDownloadTarget):
|
elif isinstance(target, OpenFileDownloadTarget):
|
||||||
try:
|
try:
|
||||||
fobj = temp_download_manager.get_tmpfile(self.basename)
|
fobj = temp_download_manager.get_tmpfile(self.basename)
|
||||||
|
@ -23,13 +23,16 @@ import re
|
|||||||
import os
|
import os
|
||||||
import json
|
import json
|
||||||
import fnmatch
|
import fnmatch
|
||||||
|
import functools
|
||||||
import glob
|
import glob
|
||||||
|
import textwrap
|
||||||
|
|
||||||
import attr
|
import attr
|
||||||
from PyQt5.QtCore import pyqtSignal, QObject, QUrl
|
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.commands import cmdutils
|
||||||
|
from qutebrowser.browser import downloads
|
||||||
|
|
||||||
|
|
||||||
def _scripts_dir():
|
def _scripts_dir():
|
||||||
@ -45,6 +48,7 @@ class GreasemonkeyScript:
|
|||||||
self._code = code
|
self._code = code
|
||||||
self.includes = []
|
self.includes = []
|
||||||
self.excludes = []
|
self.excludes = []
|
||||||
|
self.requires = []
|
||||||
self.description = None
|
self.description = None
|
||||||
self.name = None
|
self.name = None
|
||||||
self.namespace = None
|
self.namespace = None
|
||||||
@ -66,6 +70,8 @@ class GreasemonkeyScript:
|
|||||||
self.run_at = value
|
self.run_at = value
|
||||||
elif name == 'noframes':
|
elif name == 'noframes':
|
||||||
self.runs_on_sub_frames = False
|
self.runs_on_sub_frames = False
|
||||||
|
elif name == 'require':
|
||||||
|
self.requires.append(value)
|
||||||
|
|
||||||
HEADER_REGEX = r'// ==UserScript==|\n+// ==/UserScript==\n'
|
HEADER_REGEX = r'// ==UserScript==|\n+// ==/UserScript==\n'
|
||||||
PROPS_REGEX = r'// @(?P<prop>[^\s]+)\s*(?P<val>.*)'
|
PROPS_REGEX = r'// @(?P<prop>[^\s]+)\s*(?P<val>.*)'
|
||||||
@ -93,7 +99,7 @@ class GreasemonkeyScript:
|
|||||||
"""Return the processed JavaScript code of this script.
|
"""Return the processed JavaScript code of this script.
|
||||||
|
|
||||||
Adorns the source code with GM_* methods for Greasemonkey
|
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
|
lexical scope. Note that this means line numbers in your
|
||||||
browser's debugger/inspector will not match up to the line
|
browser's debugger/inspector will not match up to the line
|
||||||
numbers in the source script directly.
|
numbers in the source script directly.
|
||||||
@ -115,6 +121,14 @@ class GreasemonkeyScript:
|
|||||||
'run-at': self.run_at,
|
'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
|
@attr.s
|
||||||
class MatchingScripts(object):
|
class MatchingScripts(object):
|
||||||
@ -145,15 +159,24 @@ class GreasemonkeyManager(QObject):
|
|||||||
|
|
||||||
def __init__(self, parent=None):
|
def __init__(self, parent=None):
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
|
self._run_start = []
|
||||||
|
self._run_end = []
|
||||||
|
self._run_idle = []
|
||||||
|
self._in_progress_dls = []
|
||||||
|
|
||||||
self.load_scripts()
|
self.load_scripts()
|
||||||
|
|
||||||
@cmdutils.register(name='greasemonkey-reload',
|
@cmdutils.register(name='greasemonkey-reload',
|
||||||
instance='greasemonkey')
|
instance='greasemonkey')
|
||||||
def load_scripts(self):
|
def load_scripts(self, force=False):
|
||||||
"""Re-read Greasemonkey scripts from disk.
|
"""Re-read Greasemonkey scripts from disk.
|
||||||
|
|
||||||
The scripts are read from a 'greasemonkey' subdirectory in
|
The scripts are read from a 'greasemonkey' subdirectory in
|
||||||
qutebrowser's data directory (see `:version`).
|
qutebrowser's data directory (see `:version`).
|
||||||
|
|
||||||
|
Args:
|
||||||
|
force: For any scripts that have required dependencies,
|
||||||
|
re-download them.
|
||||||
"""
|
"""
|
||||||
self._run_start = []
|
self._run_start = []
|
||||||
self._run_end = []
|
self._run_end = []
|
||||||
@ -169,7 +192,25 @@ class GreasemonkeyManager(QObject):
|
|||||||
script = GreasemonkeyScript.parse(script_file.read())
|
script = GreasemonkeyScript.parse(script_file.read())
|
||||||
if not script.name:
|
if not script.name:
|
||||||
script.name = script_filename
|
script.name = script_filename
|
||||||
|
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':
|
if script.run_at == 'document-start':
|
||||||
self._run_start.append(script)
|
self._run_start.append(script)
|
||||||
elif script.run_at == 'document-end':
|
elif script.run_at == 'document-end':
|
||||||
@ -178,14 +219,87 @@ class GreasemonkeyManager(QObject):
|
|||||||
self._run_idle.append(script)
|
self._run_idle.append(script)
|
||||||
else:
|
else:
|
||||||
if script.run_at:
|
if script.run_at:
|
||||||
log.greasemonkey.warning(
|
log.greasemonkey.warning("Script {} has invalid run-at "
|
||||||
"Script {} has invalid run-at defined, "
|
"defined, defaulting to "
|
||||||
"defaulting to document-end".format(script_path))
|
"document-end"
|
||||||
|
.format(script.name))
|
||||||
# Default as per
|
# Default as per
|
||||||
# https://wiki.greasespot.net/Metadata_Block#.40run-at
|
# https://wiki.greasespot.net/Metadata_Block#.40run-at
|
||||||
self._run_end.append(script)
|
self._run_end.append(script)
|
||||||
log.greasemonkey.debug("Loaded script: {}".format(script.name))
|
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()
|
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):
|
def scripts_for(self, url):
|
||||||
"""Fetch scripts that are registered to run for 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',
|
tabbed_browser = objreg.get('tabbed-browser', scope='window',
|
||||||
window=self._win_id)
|
window=self._win_id)
|
||||||
tab = tabbed_browser.currentWidget()
|
tab = tabbed_browser.widget.currentWidget()
|
||||||
if tab is None:
|
if tab is None:
|
||||||
raise cmdexc.CommandError("No WebView available yet!")
|
raise cmdexc.CommandError("No WebView available yet!")
|
||||||
|
|
||||||
@ -909,19 +909,26 @@ class HintManager(QObject):
|
|||||||
|
|
||||||
@cmdutils.register(instance='hintmanager', scope='tab',
|
@cmdutils.register(instance='hintmanager', scope='tab',
|
||||||
modes=[usertypes.KeyMode.hint])
|
modes=[usertypes.KeyMode.hint])
|
||||||
def follow_hint(self, keystring=None):
|
def follow_hint(self, select=False, keystring=None):
|
||||||
"""Follow a hint.
|
"""Follow a hint.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
|
select: Only select the given hint, don't necessarily follow it.
|
||||||
keystring: The hint to follow, or None.
|
keystring: The hint to follow, or None.
|
||||||
"""
|
"""
|
||||||
if keystring is None:
|
if keystring is None:
|
||||||
if self._context.to_follow is None:
|
if self._context.to_follow is None:
|
||||||
raise cmdexc.CommandError("No hint to follow")
|
raise cmdexc.CommandError("No hint to follow")
|
||||||
|
elif select:
|
||||||
|
raise cmdexc.CommandError("Can't use --select without hint.")
|
||||||
else:
|
else:
|
||||||
keystring = self._context.to_follow
|
keystring = self._context.to_follow
|
||||||
elif keystring not in self._context.labels:
|
elif keystring not in self._context.labels:
|
||||||
raise cmdexc.CommandError("No hint {}!".format(keystring))
|
raise cmdexc.CommandError("No hint {}!".format(keystring))
|
||||||
|
|
||||||
|
if select:
|
||||||
|
self.handle_partial_key(keystring)
|
||||||
|
else:
|
||||||
self._fire(keystring)
|
self._fire(keystring)
|
||||||
|
|
||||||
@pyqtSlot(usertypes.KeyMode)
|
@pyqtSlot(usertypes.KeyMode)
|
||||||
|
@ -151,6 +151,7 @@ class MouseEventFilter(QObject):
|
|||||||
|
|
||||||
if elem.is_editable():
|
if elem.is_editable():
|
||||||
log.mouse.debug("Clicked editable element!")
|
log.mouse.debug("Clicked editable element!")
|
||||||
|
if config.val.input.insert_mode.auto_enter:
|
||||||
modeman.enter(self._tab.win_id, usertypes.KeyMode.insert,
|
modeman.enter(self._tab.win_id, usertypes.KeyMode.insert,
|
||||||
'click', only_if_normal=True)
|
'click', only_if_normal=True)
|
||||||
else:
|
else:
|
||||||
|
@ -34,6 +34,10 @@ def init():
|
|||||||
QNetworkProxyFactory.setApplicationProxyFactory(proxy_factory)
|
QNetworkProxyFactory.setApplicationProxyFactory(proxy_factory)
|
||||||
|
|
||||||
|
|
||||||
|
def shutdown():
|
||||||
|
QNetworkProxyFactory.setApplicationProxyFactory(None)
|
||||||
|
|
||||||
|
|
||||||
class ProxyFactory(QNetworkProxyFactory):
|
class ProxyFactory(QNetworkProxyFactory):
|
||||||
|
|
||||||
"""Factory for proxies to be used by qutebrowser."""
|
"""Factory for proxies to be used by qutebrowser."""
|
||||||
|
@ -30,8 +30,10 @@ import time
|
|||||||
import textwrap
|
import textwrap
|
||||||
import mimetypes
|
import mimetypes
|
||||||
import urllib
|
import urllib
|
||||||
|
import collections
|
||||||
|
|
||||||
import pkg_resources
|
import pkg_resources
|
||||||
|
import sip
|
||||||
from PyQt5.QtCore import QUrlQuery, QUrl
|
from PyQt5.QtCore import QUrlQuery, QUrl
|
||||||
|
|
||||||
import qutebrowser
|
import qutebrowser
|
||||||
@ -201,6 +203,27 @@ def qute_bookmarks(_url):
|
|||||||
return 'text/html', html
|
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):
|
def history_data(start_time, offset=None):
|
||||||
"""Return history data.
|
"""Return history data.
|
||||||
|
|
||||||
@ -240,8 +263,6 @@ def qute_history(url):
|
|||||||
|
|
||||||
return 'text/html', json.dumps(history_data(start_time, offset))
|
return 'text/html', json.dumps(history_data(start_time, offset))
|
||||||
else:
|
else:
|
||||||
if not config.val.content.javascript.enabled:
|
|
||||||
return 'text/plain', b'JavaScript is required for qute://history'
|
|
||||||
return 'text/html', jinja.render(
|
return 'text/html', jinja.render(
|
||||||
'history.html',
|
'history.html',
|
||||||
title='History',
|
title='History',
|
||||||
|
@ -74,14 +74,15 @@ def authentication_required(url, authenticator, abort_on):
|
|||||||
return answer
|
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."""
|
"""Display a javascript confirm prompt."""
|
||||||
log.js.debug("confirm: {}".format(js_msg))
|
log.js.debug("confirm: {}".format(js_msg))
|
||||||
if config.val.content.javascript.modal_dialog:
|
if config.val.content.javascript.modal_dialog:
|
||||||
raise CallSuper
|
raise CallSuper
|
||||||
|
|
||||||
|
js_msg = html.escape(js_msg) if escape_msg else js_msg
|
||||||
msg = 'From <b>{}</b>:<br/>{}'.format(html.escape(url.toDisplayString()),
|
msg = 'From <b>{}</b>:<br/>{}'.format(html.escape(url.toDisplayString()),
|
||||||
html.escape(js_msg))
|
js_msg)
|
||||||
urlstr = url.toString(QUrl.RemovePassword | QUrl.FullyEncoded)
|
urlstr = url.toString(QUrl.RemovePassword | QUrl.FullyEncoded)
|
||||||
ans = message.ask('Javascript confirm', msg,
|
ans = message.ask('Javascript confirm', msg,
|
||||||
mode=usertypes.PromptMode.yesno,
|
mode=usertypes.PromptMode.yesno,
|
||||||
@ -89,7 +90,7 @@ def javascript_confirm(url, js_msg, abort_on):
|
|||||||
return bool(ans)
|
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."""
|
"""Display a javascript prompt."""
|
||||||
log.js.debug("prompt: {}".format(js_msg))
|
log.js.debug("prompt: {}".format(js_msg))
|
||||||
if config.val.content.javascript.modal_dialog:
|
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:
|
if not config.val.content.javascript.prompt:
|
||||||
return (False, "")
|
return (False, "")
|
||||||
|
|
||||||
|
js_msg = html.escape(js_msg) if escape_msg else js_msg
|
||||||
msg = '<b>{}</b> asks:<br/>{}'.format(html.escape(url.toDisplayString()),
|
msg = '<b>{}</b> asks:<br/>{}'.format(html.escape(url.toDisplayString()),
|
||||||
html.escape(js_msg))
|
js_msg)
|
||||||
urlstr = url.toString(QUrl.RemovePassword | QUrl.FullyEncoded)
|
urlstr = url.toString(QUrl.RemovePassword | QUrl.FullyEncoded)
|
||||||
answer = message.ask('Javascript prompt', msg,
|
answer = message.ask('Javascript prompt', msg,
|
||||||
mode=usertypes.PromptMode.text,
|
mode=usertypes.PromptMode.text,
|
||||||
@ -111,7 +113,7 @@ def javascript_prompt(url, js_msg, default, abort_on):
|
|||||||
return (True, answer)
|
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."""
|
"""Display a javascript alert."""
|
||||||
log.js.debug("alert: {}".format(js_msg))
|
log.js.debug("alert: {}".format(js_msg))
|
||||||
if config.val.content.javascript.modal_dialog:
|
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:
|
if not config.val.content.javascript.alert:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
js_msg = html.escape(js_msg) if escape_msg else js_msg
|
||||||
msg = 'From <b>{}</b>:<br/>{}'.format(html.escape(url.toDisplayString()),
|
msg = 'From <b>{}</b>:<br/>{}'.format(html.escape(url.toDisplayString()),
|
||||||
html.escape(js_msg))
|
js_msg)
|
||||||
urlstr = url.toString(QUrl.RemovePassword | QUrl.FullyEncoded)
|
urlstr = url.toString(QUrl.RemovePassword | QUrl.FullyEncoded)
|
||||||
message.ask('Javascript alert', msg, mode=usertypes.PromptMode.alert,
|
message.ask('Javascript alert', msg, mode=usertypes.PromptMode.alert,
|
||||||
abort_on=abort_on, url=urlstr)
|
abort_on=abort_on, url=urlstr)
|
||||||
|
@ -76,11 +76,11 @@ class SignalFilter(QObject):
|
|||||||
tabbed_browser = objreg.get('tabbed-browser', scope='window',
|
tabbed_browser = objreg.get('tabbed-browser', scope='window',
|
||||||
window=self._win_id)
|
window=self._win_id)
|
||||||
try:
|
try:
|
||||||
tabidx = tabbed_browser.indexOf(tab)
|
tabidx = tabbed_browser.widget.indexOf(tab)
|
||||||
except RuntimeError:
|
except RuntimeError:
|
||||||
# The tab has been deleted already
|
# The tab has been deleted already
|
||||||
return
|
return
|
||||||
if tabidx == tabbed_browser.currentIndex():
|
if tabidx == tabbed_browser.widget.currentIndex():
|
||||||
if log_signal:
|
if log_signal:
|
||||||
log.signals.debug("emitting: {} (tab {})".format(
|
log.signals.debug("emitting: {} (tab {})".format(
|
||||||
debug.dbg_signal(signal, args), tabidx))
|
debug.dbg_signal(signal, args), tabidx))
|
||||||
|
@ -280,7 +280,7 @@ class BookmarkManager(UrlMarkManager):
|
|||||||
|
|
||||||
if urlstr in self.marks:
|
if urlstr in self.marks:
|
||||||
if toggle:
|
if toggle:
|
||||||
del self.marks[urlstr]
|
self.delete(urlstr)
|
||||||
return False
|
return False
|
||||||
else:
|
else:
|
||||||
raise AlreadyExistsError("Bookmark already exists!")
|
raise AlreadyExistsError("Bookmark already exists!")
|
||||||
|
@ -41,8 +41,8 @@ Group = enum.Enum('Group', ['all', 'links', 'images', 'url', 'inputs'])
|
|||||||
|
|
||||||
SELECTORS = {
|
SELECTORS = {
|
||||||
Group.all: ('a, area, textarea, select, input:not([type=hidden]), button, '
|
Group.all: ('a, area, textarea, select, input:not([type=hidden]), button, '
|
||||||
'frame, iframe, link, [onclick], [onmousedown], [role=link], '
|
'frame, iframe, link, summary, [onclick], [onmousedown], '
|
||||||
'[role=option], [role=button], img, '
|
'[role=link], [role=option], [role=button], img, '
|
||||||
# Angular 1 selectors
|
# Angular 1 selectors
|
||||||
'[ng-click], [ngClick], [data-ng-click], [x-ng-click]'),
|
'[ng-click], [ngClick], [data-ng-click], [x-ng-click]'),
|
||||||
Group.links: 'a[href], area[href], link[href], [role=link][href]',
|
Group.links: 'a[href], area[href], link[href], [role=link][href]',
|
||||||
@ -411,6 +411,7 @@ class AbstractWebElement(collections.abc.MutableMapping):
|
|||||||
elif self.is_editable(strict=True):
|
elif self.is_editable(strict=True):
|
||||||
log.webelem.debug("Clicking via JS focus()")
|
log.webelem.debug("Clicking via JS focus()")
|
||||||
self._click_editable(click_target)
|
self._click_editable(click_target)
|
||||||
|
if config.val.input.insert_mode.auto_enter:
|
||||||
modeman.enter(self._tab.win_id, usertypes.KeyMode.insert,
|
modeman.enter(self._tab.win_id, usertypes.KeyMode.insert,
|
||||||
'clicking input')
|
'clicking input')
|
||||||
else:
|
else:
|
||||||
|
@ -43,8 +43,8 @@ class WebEngineInspector(inspector.AbstractWebInspector):
|
|||||||
port = int(os.environ['QTWEBENGINE_REMOTE_DEBUGGING'])
|
port = int(os.environ['QTWEBENGINE_REMOTE_DEBUGGING'])
|
||||||
except KeyError:
|
except KeyError:
|
||||||
raise inspector.WebInspectorError(
|
raise inspector.WebInspectorError(
|
||||||
"Debugging is not enabled. See 'qutebrowser --help' for "
|
"QtWebEngine inspector is not enabled. See "
|
||||||
"details.")
|
"'qutebrowser --help' for details.")
|
||||||
url = QUrl('http://localhost:{}/'.format(port))
|
url = QUrl('http://localhost:{}/'.format(port))
|
||||||
self._widget.load(url)
|
self._widget.load(url)
|
||||||
self.show()
|
self.show()
|
||||||
|
@ -17,9 +17,6 @@
|
|||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
|
# 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.
|
"""Bridge from QWebEngineSettings to our own settings.
|
||||||
|
|
||||||
Module attributes:
|
Module attributes:
|
||||||
@ -44,41 +41,110 @@ from qutebrowser.utils import (utils, standarddir, javascript, qtutils,
|
|||||||
default_profile = None
|
default_profile = None
|
||||||
# The QWebEngineProfile used for private (off-the-record) windows
|
# The QWebEngineProfile used for private (off-the-record) windows
|
||||||
private_profile = None
|
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):
|
For read operations, the default profile value is always used.
|
||||||
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.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, font):
|
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
|
# Mapping from WebEngineSettings::initDefaults in
|
||||||
# qtwebengine/src/core/web_engine_settings.cpp
|
# qtwebengine/src/core/web_engine_settings.cpp
|
||||||
font_to_qfont = {
|
_FONT_TO_QFONT = {
|
||||||
QWebEngineSettings.StandardFont: QFont.Serif,
|
QWebEngineSettings.StandardFont: QFont.Serif,
|
||||||
QWebEngineSettings.FixedFont: QFont.Monospace,
|
QWebEngineSettings.FixedFont: QFont.Monospace,
|
||||||
QWebEngineSettings.SerifFont: QFont.Serif,
|
QWebEngineSettings.SerifFont: QFont.Serif,
|
||||||
@ -86,74 +152,21 @@ class FontFamilySetter(Base, websettings.FontFamilySetter):
|
|||||||
QWebEngineSettings.CursiveFont: QFont.Cursive,
|
QWebEngineSettings.CursiveFont: QFont.Cursive,
|
||||||
QWebEngineSettings.FantasyFont: QFont.Fantasy,
|
QWebEngineSettings.FantasyFont: QFont.Fantasy,
|
||||||
}
|
}
|
||||||
super().__init__(setter=QWebEngineSettings.setFontFamily, font=font,
|
|
||||||
qfont=font_to_qfont[font])
|
|
||||||
|
|
||||||
|
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',
|
||||||
|
}
|
||||||
|
for name, attribute in new_attributes.items():
|
||||||
|
try:
|
||||||
|
value = getattr(QWebEngineSettings, attribute)
|
||||||
|
except AttributeError:
|
||||||
|
continue
|
||||||
|
|
||||||
class DefaultProfileSetter(websettings.Base):
|
self._ATTRIBUTES[name] = [value]
|
||||||
|
|
||||||
"""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)
|
|
||||||
|
|
||||||
|
|
||||||
def _init_stylesheet(profile):
|
def _init_stylesheet(profile):
|
||||||
@ -210,9 +223,48 @@ def _set_http_headers(profile):
|
|||||||
profile.setHttpAcceptLanguage(accept_language)
|
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):
|
def _update_settings(option):
|
||||||
"""Update global settings when qwebsettings changed."""
|
"""Update global settings when qwebsettings changed."""
|
||||||
websettings.update_mappings(MAPPINGS, option)
|
global_settings.update_setting(option)
|
||||||
|
|
||||||
if option in ['scrolling.bar', 'content.user_stylesheets']:
|
if option in ['scrolling.bar', 'content.user_stylesheets']:
|
||||||
_init_stylesheet(default_profile)
|
_init_stylesheet(default_profile)
|
||||||
_init_stylesheet(private_profile)
|
_init_stylesheet(private_profile)
|
||||||
@ -221,27 +273,46 @@ def _update_settings(option):
|
|||||||
'content.headers.accept_language']:
|
'content.headers.accept_language']:
|
||||||
_set_http_headers(default_profile)
|
_set_http_headers(default_profile)
|
||||||
_set_http_headers(private_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():
|
def _init_profiles():
|
||||||
"""Init the two used QWebEngineProfiles."""
|
"""Init the two used QWebEngineProfiles."""
|
||||||
global default_profile, private_profile
|
global default_profile, private_profile
|
||||||
|
|
||||||
default_profile = QWebEngineProfile.defaultProfile()
|
default_profile = QWebEngineProfile.defaultProfile()
|
||||||
default_profile.setCachePath(
|
default_profile.setCachePath(
|
||||||
os.path.join(standarddir.cache(), 'webengine'))
|
os.path.join(standarddir.cache(), 'webengine'))
|
||||||
default_profile.setPersistentStoragePath(
|
default_profile.setPersistentStoragePath(
|
||||||
os.path.join(standarddir.data(), 'webengine'))
|
os.path.join(standarddir.data(), 'webengine'))
|
||||||
_init_stylesheet(default_profile)
|
_init_profile(default_profile)
|
||||||
_set_http_headers(default_profile)
|
_set_persistent_cookie_policy(default_profile)
|
||||||
|
|
||||||
private_profile = QWebEngineProfile()
|
private_profile = QWebEngineProfile()
|
||||||
assert private_profile.isOffTheRecord()
|
assert private_profile.isOffTheRecord()
|
||||||
_init_stylesheet(private_profile)
|
_init_profile(private_profile)
|
||||||
_set_http_headers(private_profile)
|
|
||||||
|
|
||||||
if qtutils.version_check('5.8'):
|
|
||||||
default_profile.setSpellCheckEnabled(True)
|
|
||||||
private_profile.setSpellCheckEnabled(True)
|
|
||||||
|
|
||||||
|
|
||||||
def inject_userscripts():
|
def inject_userscripts():
|
||||||
@ -287,111 +358,12 @@ def init(args):
|
|||||||
os.environ['QTWEBENGINE_REMOTE_DEBUGGING'] = str(utils.random_port())
|
os.environ['QTWEBENGINE_REMOTE_DEBUGGING'] = str(utils.random_port())
|
||||||
|
|
||||||
_init_profiles()
|
_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)
|
config.instance.changed.connect(_update_settings)
|
||||||
|
|
||||||
|
global global_settings
|
||||||
|
global_settings = WebEngineSettings(_SettingsWrapper())
|
||||||
|
global_settings.init_settings()
|
||||||
|
|
||||||
|
|
||||||
def shutdown():
|
def shutdown():
|
||||||
# FIXME:qtwebengine do we need to do something for a clean shutdown here?
|
|
||||||
pass
|
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 math
|
||||||
import functools
|
import functools
|
||||||
import sys
|
import sys
|
||||||
|
import re
|
||||||
import html as html_utils
|
import html as html_utils
|
||||||
|
|
||||||
import sip
|
import sip
|
||||||
from PyQt5.QtCore import (pyqtSignal, pyqtSlot, Qt, QEvent, QPoint, QPointF,
|
from PyQt5.QtCore import (pyqtSignal, pyqtSlot, Qt, QEvent, QPoint, QPointF,
|
||||||
QUrl)
|
QUrl, QTimer)
|
||||||
from PyQt5.QtGui import QKeyEvent
|
from PyQt5.QtGui import QKeyEvent, QIcon
|
||||||
from PyQt5.QtNetwork import QAuthenticator
|
from PyQt5.QtNetwork import QAuthenticator
|
||||||
from PyQt5.QtWidgets import QApplication
|
from PyQt5.QtWidgets import QApplication
|
||||||
from PyQt5.QtWebEngineWidgets import QWebEnginePage, QWebEngineScript
|
from PyQt5.QtWebEngineWidgets import QWebEnginePage, QWebEngineScript
|
||||||
|
|
||||||
|
from qutebrowser.config import configdata
|
||||||
from qutebrowser.browser import browsertab, mouse, shared
|
from qutebrowser.browser import browsertab, mouse, shared
|
||||||
from qutebrowser.browser.webengine import (webview, webengineelem, tabhistory,
|
from qutebrowser.browser.webengine import (webview, webengineelem, tabhistory,
|
||||||
interceptor, webenginequtescheme,
|
interceptor, webenginequtescheme,
|
||||||
@ -183,6 +185,12 @@ class WebEngineSearch(browsertab.AbstractSearch):
|
|||||||
|
|
||||||
def search(self, text, *, ignore_case='never', reverse=False,
|
def search(self, text, *, ignore_case='never', reverse=False,
|
||||||
result_cb=None):
|
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.text = text
|
||||||
self._flags = QWebEnginePage.FindFlags(0)
|
self._flags = QWebEnginePage.FindFlags(0)
|
||||||
if self._is_case_sensitive(ignore_case):
|
if self._is_case_sensitive(ignore_case):
|
||||||
@ -218,12 +226,21 @@ class WebEngineCaret(browsertab.AbstractCaret):
|
|||||||
if mode != usertypes.KeyMode.caret:
|
if mode != usertypes.KeyMode.caret:
|
||||||
return
|
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(
|
self._tab.run_js_async(
|
||||||
javascript.assemble('caret', 'setPlatform', sys.platform))
|
javascript.assemble('caret', 'setPlatform', sys.platform))
|
||||||
self._js_call('setInitialCursor')
|
self._js_call('setInitialCursor')
|
||||||
|
|
||||||
@pyqtSlot(usertypes.KeyMode)
|
@pyqtSlot(usertypes.KeyMode)
|
||||||
def _on_mode_left(self):
|
def _on_mode_left(self, mode):
|
||||||
|
if mode != usertypes.KeyMode.caret:
|
||||||
|
return
|
||||||
|
|
||||||
self.drop_selection()
|
self.drop_selection()
|
||||||
self._js_call('disableCaret')
|
self._js_call('disableCaret')
|
||||||
|
|
||||||
@ -470,7 +487,8 @@ class WebEngineHistory(browsertab.AbstractHistory):
|
|||||||
return self._history.itemAt(i)
|
return self._history.itemAt(i)
|
||||||
|
|
||||||
def _go_to_item(self, item):
|
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):
|
def serialize(self):
|
||||||
if not qtutils.version_check('5.9', compiled=False):
|
if not qtutils.version_check('5.9', compiled=False):
|
||||||
@ -488,6 +506,9 @@ class WebEngineHistory(browsertab.AbstractHistory):
|
|||||||
return qtutils.deserialize(data, self._history)
|
return qtutils.deserialize(data, self._history)
|
||||||
|
|
||||||
def load_items(self, items):
|
def load_items(self, items):
|
||||||
|
if items:
|
||||||
|
self._tab.predicted_navigation.emit(items[-1].url)
|
||||||
|
|
||||||
stream, _data, cur_data = tabhistory.serialize(items)
|
stream, _data, cur_data = tabhistory.serialize(items)
|
||||||
qtutils.deserialize_stream(stream, self._history)
|
qtutils.deserialize_stream(stream, self._history)
|
||||||
|
|
||||||
@ -604,12 +625,15 @@ class WebEngineTab(browsertab.AbstractTab):
|
|||||||
self.printing = WebEnginePrinting()
|
self.printing = WebEnginePrinting()
|
||||||
self.elements = WebEngineElements(tab=self)
|
self.elements = WebEngineElements(tab=self)
|
||||||
self.action = WebEngineAction(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._set_widget(widget)
|
||||||
self._connect_signals()
|
self._connect_signals()
|
||||||
self.backend = usertypes.Backend.QtWebEngine
|
self.backend = usertypes.Backend.QtWebEngine
|
||||||
self._init_js()
|
self._init_js()
|
||||||
self._child_event_filter = None
|
self._child_event_filter = None
|
||||||
self._saved_zoom = None
|
self._saved_zoom = None
|
||||||
|
self._reload_url = None
|
||||||
|
|
||||||
def _init_js(self):
|
def _init_js(self):
|
||||||
js_code = '\n'.join([
|
js_code = '\n'.join([
|
||||||
@ -648,9 +672,15 @@ class WebEngineTab(browsertab.AbstractTab):
|
|||||||
self.zoom.set_factor(self._saved_zoom)
|
self.zoom.set_factor(self._saved_zoom)
|
||||||
self._saved_zoom = None
|
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._saved_zoom = self.zoom.factor()
|
||||||
self._openurl_prepare(url)
|
self._openurl_prepare(url, predict=predict)
|
||||||
self._widget.load(url)
|
self._widget.load(url)
|
||||||
|
|
||||||
def url(self, requested=False):
|
def url(self, requested=False):
|
||||||
@ -682,10 +712,6 @@ class WebEngineTab(browsertab.AbstractTab):
|
|||||||
def shutdown(self):
|
def shutdown(self):
|
||||||
self.shutting_down.emit()
|
self.shutting_down.emit()
|
||||||
self.action.exit_fullscreen()
|
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()
|
self._widget.shutdown()
|
||||||
|
|
||||||
def reload(self, *, force=False):
|
def reload(self, *, force=False):
|
||||||
@ -728,6 +754,16 @@ class WebEngineTab(browsertab.AbstractTab):
|
|||||||
self.send_event(press_evt)
|
self.send_event(press_evt)
|
||||||
self.send_event(release_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()
|
@pyqtSlot()
|
||||||
def _on_history_trigger(self):
|
def _on_history_trigger(self):
|
||||||
try:
|
try:
|
||||||
@ -776,13 +812,7 @@ class WebEngineTab(browsertab.AbstractTab):
|
|||||||
sip.assign(authenticator, QAuthenticator())
|
sip.assign(authenticator, QAuthenticator())
|
||||||
# pylint: enable=no-member, useless-suppression
|
# pylint: enable=no-member, useless-suppression
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
url_string = url.toDisplayString()
|
self._show_error_page(url, "Proxy authentication required")
|
||||||
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)
|
|
||||||
|
|
||||||
@pyqtSlot(QUrl, 'QAuthenticator*')
|
@pyqtSlot(QUrl, 'QAuthenticator*')
|
||||||
def _on_authentication_required(self, url, authenticator):
|
def _on_authentication_required(self, url, authenticator):
|
||||||
@ -802,12 +832,7 @@ class WebEngineTab(browsertab.AbstractTab):
|
|||||||
except AttributeError:
|
except AttributeError:
|
||||||
# WORKAROUND for
|
# WORKAROUND for
|
||||||
# https://www.riverbankcomputing.com/pipermail/pyqt/2016-December/038400.html
|
# https://www.riverbankcomputing.com/pipermail/pyqt/2016-December/038400.html
|
||||||
url_string = url.toDisplayString()
|
self._show_error_page(url, "Authentication required")
|
||||||
error_page = jinja.render(
|
|
||||||
'error.html',
|
|
||||||
title="Error loading page: {}".format(url_string),
|
|
||||||
url=url_string, error="Authentication required")
|
|
||||||
self.set_html(error_page)
|
|
||||||
|
|
||||||
@pyqtSlot('QWebEngineFullScreenRequest')
|
@pyqtSlot('QWebEngineFullScreenRequest')
|
||||||
def _on_fullscreen_requested(self, request):
|
def _on_fullscreen_requested(self, request):
|
||||||
@ -872,6 +897,74 @@ class WebEngineTab(browsertab.AbstractTab):
|
|||||||
if not ok:
|
if not ok:
|
||||||
self._load_finished_fake.emit(False)
|
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):
|
def _connect_signals(self):
|
||||||
view = self._widget
|
view = self._widget
|
||||||
page = view.page()
|
page = view.page()
|
||||||
@ -886,6 +979,7 @@ class WebEngineTab(browsertab.AbstractTab):
|
|||||||
self._on_proxy_authentication_required)
|
self._on_proxy_authentication_required)
|
||||||
page.fullScreenRequested.connect(self._on_fullscreen_requested)
|
page.fullScreenRequested.connect(self._on_fullscreen_requested)
|
||||||
page.contentsSizeChanged.connect(self.contents_size_changed)
|
page.contentsSizeChanged.connect(self.contents_size_changed)
|
||||||
|
page.navigation_request.connect(self._on_navigation_request)
|
||||||
|
|
||||||
view.titleChanged.connect(self.title_changed)
|
view.titleChanged.connect(self.title_changed)
|
||||||
view.urlChanged.connect(self._on_url_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._restore_zoom)
|
||||||
page.loadFinished.connect(self._on_load_finished)
|
page.loadFinished.connect(self._on_load_finished)
|
||||||
|
|
||||||
|
self.predicted_navigation.connect(self._on_predicted_navigation)
|
||||||
|
|
||||||
def event_target(self):
|
def event_target(self):
|
||||||
return self._widget.focusProxy()
|
return self._widget.focusProxy()
|
||||||
|
@ -29,8 +29,7 @@ from PyQt5.QtWebEngineWidgets import (QWebEngineView, QWebEnginePage,
|
|||||||
from qutebrowser.browser import shared
|
from qutebrowser.browser import shared
|
||||||
from qutebrowser.browser.webengine import certificateerror, webenginesettings
|
from qutebrowser.browser.webengine import certificateerror, webenginesettings
|
||||||
from qutebrowser.config import config
|
from qutebrowser.config import config
|
||||||
from qutebrowser.utils import (log, debug, usertypes, jinja, urlutils, message,
|
from qutebrowser.utils import log, debug, usertypes, jinja, objreg, qtutils
|
||||||
objreg, qtutils)
|
|
||||||
|
|
||||||
|
|
||||||
class WebEngineView(QWebEngineView):
|
class WebEngineView(QWebEngineView):
|
||||||
@ -124,10 +123,12 @@ class WebEnginePage(QWebEnginePage):
|
|||||||
Signals:
|
Signals:
|
||||||
certificate_error: Emitted on certificate errors.
|
certificate_error: Emitted on certificate errors.
|
||||||
shutting_down: Emitted when the page is shutting down.
|
shutting_down: Emitted when the page is shutting down.
|
||||||
|
navigation_request: Emitted on acceptNavigationRequest.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
certificate_error = pyqtSignal()
|
certificate_error = pyqtSignal()
|
||||||
shutting_down = pyqtSignal()
|
shutting_down = pyqtSignal()
|
||||||
|
navigation_request = pyqtSignal(usertypes.NavigationRequest)
|
||||||
|
|
||||||
def __init__(self, *, theme_color, profile, parent=None):
|
def __init__(self, *, theme_color, profile, parent=None):
|
||||||
super().__init__(profile, parent)
|
super().__init__(profile, parent)
|
||||||
@ -242,10 +243,12 @@ class WebEnginePage(QWebEnginePage):
|
|||||||
"""Override javaScriptConfirm to use qutebrowser prompts."""
|
"""Override javaScriptConfirm to use qutebrowser prompts."""
|
||||||
if self._is_shutting_down:
|
if self._is_shutting_down:
|
||||||
return False
|
return False
|
||||||
|
escape_msg = qtutils.version_check('5.11', compiled=False)
|
||||||
try:
|
try:
|
||||||
return shared.javascript_confirm(url, js_msg,
|
return shared.javascript_confirm(url, js_msg,
|
||||||
abort_on=[self.loadStarted,
|
abort_on=[self.loadStarted,
|
||||||
self.shutting_down])
|
self.shutting_down],
|
||||||
|
escape_msg=escape_msg)
|
||||||
except shared.CallSuper:
|
except shared.CallSuper:
|
||||||
return super().javaScriptConfirm(url, js_msg)
|
return super().javaScriptConfirm(url, js_msg)
|
||||||
|
|
||||||
@ -255,12 +258,14 @@ class WebEnginePage(QWebEnginePage):
|
|||||||
# https://www.riverbankcomputing.com/pipermail/pyqt/2016-November/038293.html
|
# https://www.riverbankcomputing.com/pipermail/pyqt/2016-November/038293.html
|
||||||
def javaScriptPrompt(self, url, js_msg, default):
|
def javaScriptPrompt(self, url, js_msg, default):
|
||||||
"""Override javaScriptPrompt to use qutebrowser prompts."""
|
"""Override javaScriptPrompt to use qutebrowser prompts."""
|
||||||
|
escape_msg = qtutils.version_check('5.11', compiled=False)
|
||||||
if self._is_shutting_down:
|
if self._is_shutting_down:
|
||||||
return (False, "")
|
return (False, "")
|
||||||
try:
|
try:
|
||||||
return shared.javascript_prompt(url, js_msg, default,
|
return shared.javascript_prompt(url, js_msg, default,
|
||||||
abort_on=[self.loadStarted,
|
abort_on=[self.loadStarted,
|
||||||
self.shutting_down])
|
self.shutting_down],
|
||||||
|
escape_msg=escape_msg)
|
||||||
except shared.CallSuper:
|
except shared.CallSuper:
|
||||||
return super().javaScriptPrompt(url, js_msg, default)
|
return super().javaScriptPrompt(url, js_msg, default)
|
||||||
|
|
||||||
@ -268,10 +273,12 @@ class WebEnginePage(QWebEnginePage):
|
|||||||
"""Override javaScriptAlert to use qutebrowser prompts."""
|
"""Override javaScriptAlert to use qutebrowser prompts."""
|
||||||
if self._is_shutting_down:
|
if self._is_shutting_down:
|
||||||
return
|
return
|
||||||
|
escape_msg = qtutils.version_check('5.11', compiled=False)
|
||||||
try:
|
try:
|
||||||
shared.javascript_alert(url, js_msg,
|
shared.javascript_alert(url, js_msg,
|
||||||
abort_on=[self.loadStarted,
|
abort_on=[self.loadStarted,
|
||||||
self.shutting_down])
|
self.shutting_down],
|
||||||
|
escape_msg=escape_msg)
|
||||||
except shared.CallSuper:
|
except shared.CallSuper:
|
||||||
super().javaScriptAlert(url, js_msg)
|
super().javaScriptAlert(url, js_msg)
|
||||||
|
|
||||||
@ -288,21 +295,26 @@ class WebEnginePage(QWebEnginePage):
|
|||||||
url: QUrl,
|
url: QUrl,
|
||||||
typ: QWebEnginePage.NavigationType,
|
typ: QWebEnginePage.NavigationType,
|
||||||
is_main_frame: bool):
|
is_main_frame: bool):
|
||||||
"""Override acceptNavigationRequest to handle clicked links.
|
"""Override acceptNavigationRequest to forward it to the tab API."""
|
||||||
|
type_map = {
|
||||||
This only show an error on invalid links - everything else is handled
|
QWebEnginePage.NavigationTypeLinkClicked:
|
||||||
in createWindow.
|
usertypes.NavigationRequest.Type.link_clicked,
|
||||||
"""
|
QWebEnginePage.NavigationTypeTyped:
|
||||||
log.webview.debug("navigation request: url {}, type {}, is_main_frame "
|
usertypes.NavigationRequest.Type.typed,
|
||||||
"{}".format(url.toDisplayString(),
|
QWebEnginePage.NavigationTypeFormSubmitted:
|
||||||
debug.qenum_key(QWebEnginePage, typ),
|
usertypes.NavigationRequest.Type.form_submitted,
|
||||||
is_main_frame))
|
QWebEnginePage.NavigationTypeBackForward:
|
||||||
if (typ == QWebEnginePage.NavigationTypeLinkClicked and
|
usertypes.NavigationRequest.Type.back_forward,
|
||||||
not url.isValid()):
|
QWebEnginePage.NavigationTypeReload:
|
||||||
msg = urlutils.get_errstring(url, "Invalid link clicked")
|
usertypes.NavigationRequest.Type.reloaded,
|
||||||
message.error(msg)
|
QWebEnginePage.NavigationTypeOther:
|
||||||
return False
|
usertypes.NavigationRequest.Type.other,
|
||||||
return True
|
}
|
||||||
|
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')
|
@pyqtSlot('QUrl')
|
||||||
def _inject_userjs(self, url):
|
def _inject_userjs(self, url):
|
||||||
|
@ -17,9 +17,6 @@
|
|||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
|
# 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.
|
"""Bridge from QWebSettings to our own settings.
|
||||||
|
|
||||||
Module attributes:
|
Module attributes:
|
||||||
@ -37,46 +34,85 @@ from qutebrowser.utils import standarddir, urlutils
|
|||||||
from qutebrowser.browser import shared
|
from qutebrowser.browser import shared
|
||||||
|
|
||||||
|
|
||||||
class Base(websettings.Base):
|
# The global WebKitSettings object
|
||||||
|
global_settings = None
|
||||||
"""Base settings class with appropriate _get_global_settings."""
|
|
||||||
|
|
||||||
def _get_global_settings(self):
|
|
||||||
return [QWebSettings.globalSettings()]
|
|
||||||
|
|
||||||
|
|
||||||
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],
|
||||||
|
|
||||||
class Setter(Base, websettings.Setter):
|
'zoom.text_only':
|
||||||
|
[QWebSettings.ZoomTextOnly],
|
||||||
|
'scrolling.smooth':
|
||||||
|
[QWebSettings.ScrollAnimatorEnabled],
|
||||||
|
}
|
||||||
|
|
||||||
"""A setting set via a QWebSettings setter method."""
|
_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,
|
||||||
|
}
|
||||||
|
|
||||||
pass
|
_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,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
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
|
# Mapping from QWebSettings::QWebSettings() in
|
||||||
# qtwebkit/Source/WebKit/qt/Api/qwebsettings.cpp
|
# qtwebkit/Source/WebKit/qt/Api/qwebsettings.cpp
|
||||||
font_to_qfont = {
|
_FONT_TO_QFONT = {
|
||||||
QWebSettings.StandardFont: QFont.Serif,
|
QWebSettings.StandardFont: QFont.Serif,
|
||||||
QWebSettings.FixedFont: QFont.Monospace,
|
QWebSettings.FixedFont: QFont.Monospace,
|
||||||
QWebSettings.SerifFont: QFont.Serif,
|
QWebSettings.SerifFont: QFont.Serif,
|
||||||
@ -84,38 +120,44 @@ class FontFamilySetter(Base, websettings.FontFamilySetter):
|
|||||||
QWebSettings.CursiveFont: QFont.Cursive,
|
QWebSettings.CursiveFont: QFont.Cursive,
|
||||||
QWebSettings.FantasyFont: QFont.Fantasy,
|
QWebSettings.FantasyFont: QFont.Fantasy,
|
||||||
}
|
}
|
||||||
super().__init__(setter=QWebSettings.setFontFamily, font=font,
|
|
||||||
qfont=font_to_qfont[font])
|
|
||||||
|
|
||||||
|
|
||||||
class CookiePolicy(Base):
|
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)
|
||||||
|
|
||||||
"""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,
|
'all': QWebSettings.AlwaysAllowThirdPartyCookies,
|
||||||
'no-3rdparty': QWebSettings.AlwaysBlockThirdPartyCookies,
|
'no-3rdparty': QWebSettings.AlwaysBlockThirdPartyCookies,
|
||||||
'never': QWebSettings.AlwaysBlockThirdPartyCookies,
|
'never': QWebSettings.AlwaysBlockThirdPartyCookies,
|
||||||
'no-unknown-3rdparty': QWebSettings.AllowThirdPartyWithExistingCookies,
|
'no-unknown-3rdparty': QWebSettings.AllowThirdPartyWithExistingCookies,
|
||||||
}
|
}
|
||||||
|
value = config.val.content.cookies.accept
|
||||||
def _set(self, value, settings=None):
|
settings.setThirdPartyCookiePolicy(mapping[value])
|
||||||
for obj in self._get_settings(settings):
|
|
||||||
obj.setThirdPartyCookiePolicy(self.MAPPING[value])
|
|
||||||
|
|
||||||
|
|
||||||
def _set_user_stylesheet():
|
def _set_cache_maximum_pages(settings):
|
||||||
"""Set the generated user-stylesheet."""
|
"""Update the content.cache.maximum_pages setting."""
|
||||||
stylesheet = shared.get_user_stylesheet().encode('utf-8')
|
value = config.val.content.cache.maximum_pages
|
||||||
url = urlutils.data_url('text/css;charset=utf-8', stylesheet)
|
settings.setMaximumPagesInCache(value)
|
||||||
QWebSettings.globalSettings().setUserStyleSheetUrl(url)
|
|
||||||
|
|
||||||
|
|
||||||
def _update_settings(option):
|
def _update_settings(option):
|
||||||
"""Update global settings when qwebsettings changed."""
|
"""Update global settings when qwebsettings changed."""
|
||||||
|
global_settings.update_setting(option)
|
||||||
|
|
||||||
|
settings = QWebSettings.globalSettings()
|
||||||
if option in ['scrollbar.hide', 'content.user_stylesheets']:
|
if option in ['scrollbar.hide', 'content.user_stylesheets']:
|
||||||
_set_user_stylesheet()
|
_set_user_stylesheet(settings)
|
||||||
websettings.update_mappings(MAPPINGS, option)
|
elif option == 'content.cookies.accept':
|
||||||
|
_set_cookie_accept_policy(settings)
|
||||||
|
elif option == 'content.cache.maximum_pages':
|
||||||
|
_set_cache_maximum_pages(settings)
|
||||||
|
|
||||||
|
|
||||||
def init(_args):
|
def init(_args):
|
||||||
@ -131,92 +173,20 @@ def init(_args):
|
|||||||
QWebSettings.setOfflineStoragePath(
|
QWebSettings.setOfflineStoragePath(
|
||||||
os.path.join(data_path, 'offline-storage'))
|
os.path.join(data_path, 'offline-storage'))
|
||||||
|
|
||||||
websettings.init_mappings(MAPPINGS)
|
settings = QWebSettings.globalSettings()
|
||||||
_set_user_stylesheet()
|
_set_user_stylesheet(settings)
|
||||||
|
_set_cookie_accept_policy(settings)
|
||||||
|
_set_cache_maximum_pages(settings)
|
||||||
|
|
||||||
config.instance.changed.connect(_update_settings)
|
config.instance.changed.connect(_update_settings)
|
||||||
|
|
||||||
|
global global_settings
|
||||||
|
global_settings = WebKitSettings(QWebSettings.globalSettings())
|
||||||
|
global_settings.init_settings()
|
||||||
|
|
||||||
|
|
||||||
def shutdown():
|
def shutdown():
|
||||||
"""Disable storage so removing tmpdir will work."""
|
"""Disable storage so removing tmpdir will work."""
|
||||||
QWebSettings.setIconDatabasePath('')
|
QWebSettings.setIconDatabasePath('')
|
||||||
QWebSettings.setOfflineWebApplicationCachePath('')
|
QWebSettings.setOfflineWebApplicationCachePath('')
|
||||||
QWebSettings.globalSettings().setLocalStoragePath('')
|
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
|
import sip
|
||||||
from PyQt5.QtCore import (pyqtSlot, Qt, QEvent, QUrl, QPoint, QTimer, QSizeF,
|
from PyQt5.QtCore import (pyqtSlot, Qt, QEvent, QUrl, QPoint, QTimer, QSizeF,
|
||||||
QSize)
|
QSize)
|
||||||
from PyQt5.QtGui import QKeyEvent
|
from PyQt5.QtGui import QKeyEvent, QIcon
|
||||||
from PyQt5.QtWebKitWidgets import QWebPage, QWebFrame
|
from PyQt5.QtWebKitWidgets import QWebPage, QWebFrame
|
||||||
from PyQt5.QtWebKit import QWebSettings
|
from PyQt5.QtWebKit import QWebSettings
|
||||||
from PyQt5.QtPrintSupport import QPrinter
|
from PyQt5.QtPrintSupport import QPrinter
|
||||||
|
|
||||||
from qutebrowser.browser import browsertab
|
from qutebrowser.browser import browsertab, shared
|
||||||
from qutebrowser.browser.webkit import webview, tabhistory, webkitelem
|
from qutebrowser.browser.webkit import (webview, tabhistory, webkitelem,
|
||||||
|
webkitsettings)
|
||||||
from qutebrowser.utils import qtutils, objreg, usertypes, utils, log, debug
|
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,
|
def search(self, text, *, ignore_case='never', reverse=False,
|
||||||
result_cb=None):
|
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.text = text
|
||||||
|
self.search_displayed = True
|
||||||
self._flags = QWebPage.FindWrapsAroundDocument
|
self._flags = QWebPage.FindWrapsAroundDocument
|
||||||
if self._is_case_sensitive(ignore_case):
|
if self._is_case_sensitive(ignore_case):
|
||||||
self._flags |= QWebPage.FindCaseSensitively
|
self._flags |= QWebPage.FindCaseSensitively
|
||||||
@ -205,8 +215,8 @@ class WebKitCaret(browsertab.AbstractCaret):
|
|||||||
self._widget.page().currentFrame().evaluateJavaScript(
|
self._widget.page().currentFrame().evaluateJavaScript(
|
||||||
utils.read_file('javascript/position_caret.js'))
|
utils.read_file('javascript/position_caret.js'))
|
||||||
|
|
||||||
@pyqtSlot()
|
@pyqtSlot(usertypes.KeyMode)
|
||||||
def _on_mode_left(self):
|
def _on_mode_left(self, _mode):
|
||||||
settings = self._widget.settings()
|
settings = self._widget.settings()
|
||||||
if settings.testAttribute(QWebSettings.CaretBrowsingEnabled):
|
if settings.testAttribute(QWebSettings.CaretBrowsingEnabled):
|
||||||
if self.selection_enabled and self._widget.hasSelection():
|
if self.selection_enabled and self._widget.hasSelection():
|
||||||
@ -517,7 +527,8 @@ class WebKitHistory(browsertab.AbstractHistory):
|
|||||||
return self._history.itemAt(i)
|
return self._history.itemAt(i)
|
||||||
|
|
||||||
def _go_to_item(self, item):
|
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):
|
def serialize(self):
|
||||||
return qtutils.serialize(self._history)
|
return qtutils.serialize(self._history)
|
||||||
@ -526,6 +537,9 @@ class WebKitHistory(browsertab.AbstractHistory):
|
|||||||
return qtutils.deserialize(data, self._history)
|
return qtutils.deserialize(data, self._history)
|
||||||
|
|
||||||
def load_items(self, items):
|
def load_items(self, items):
|
||||||
|
if items:
|
||||||
|
self._tab.predicted_navigation.emit(items[-1].url)
|
||||||
|
|
||||||
stream, _data, user_data = tabhistory.serialize(items)
|
stream, _data, user_data = tabhistory.serialize(items)
|
||||||
qtutils.deserialize_stream(stream, self._history)
|
qtutils.deserialize_stream(stream, self._history)
|
||||||
for i, data in enumerate(user_data):
|
for i, data in enumerate(user_data):
|
||||||
@ -644,6 +658,8 @@ class WebKitTab(browsertab.AbstractTab):
|
|||||||
self.printing = WebKitPrinting()
|
self.printing = WebKitPrinting()
|
||||||
self.elements = WebKitElements(tab=self)
|
self.elements = WebKitElements(tab=self)
|
||||||
self.action = WebKitAction(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._set_widget(widget)
|
||||||
self._connect_signals()
|
self._connect_signals()
|
||||||
self.backend = usertypes.Backend.QtWebKit
|
self.backend = usertypes.Backend.QtWebKit
|
||||||
@ -655,8 +671,8 @@ class WebKitTab(browsertab.AbstractTab):
|
|||||||
settings = widget.settings()
|
settings = widget.settings()
|
||||||
settings.setAttribute(QWebSettings.PrivateBrowsingEnabled, True)
|
settings.setAttribute(QWebSettings.PrivateBrowsingEnabled, True)
|
||||||
|
|
||||||
def openurl(self, url):
|
def openurl(self, url, *, predict=True):
|
||||||
self._openurl_prepare(url)
|
self._openurl_prepare(url, predict=predict)
|
||||||
self._widget.openurl(url)
|
self._widget.openurl(url)
|
||||||
|
|
||||||
def url(self, requested=False):
|
def url(self, requested=False):
|
||||||
@ -730,6 +746,8 @@ class WebKitTab(browsertab.AbstractTab):
|
|||||||
def _on_load_started(self):
|
def _on_load_started(self):
|
||||||
super()._on_load_started()
|
super()._on_load_started()
|
||||||
self.networkaccessmanager().netrc_used = False
|
self.networkaccessmanager().netrc_used = False
|
||||||
|
# Make sure the icon is cleared when navigating to a page without one.
|
||||||
|
self.icon_changed.emit(QIcon())
|
||||||
|
|
||||||
@pyqtSlot()
|
@pyqtSlot()
|
||||||
def _on_frame_load_finished(self):
|
def _on_frame_load_finished(self):
|
||||||
@ -761,6 +779,31 @@ class WebKitTab(browsertab.AbstractTab):
|
|||||||
def _on_contents_size_changed(self, size):
|
def _on_contents_size_changed(self, size):
|
||||||
self.contents_size_changed.emit(QSizeF(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):
|
def _connect_signals(self):
|
||||||
view = self._widget
|
view = self._widget
|
||||||
page = view.page()
|
page = view.page()
|
||||||
@ -779,6 +822,7 @@ class WebKitTab(browsertab.AbstractTab):
|
|||||||
page.frameCreated.connect(self._on_frame_created)
|
page.frameCreated.connect(self._on_frame_created)
|
||||||
frame.contentsSizeChanged.connect(self._on_contents_size_changed)
|
frame.contentsSizeChanged.connect(self._on_contents_size_changed)
|
||||||
frame.initialLayoutCompleted.connect(self._on_history_trigger)
|
frame.initialLayoutCompleted.connect(self._on_history_trigger)
|
||||||
|
page.navigation_request.connect(self._on_navigation_request)
|
||||||
|
|
||||||
def event_target(self):
|
def event_target(self):
|
||||||
return self._widget
|
return self._widget
|
||||||
|
@ -22,6 +22,7 @@
|
|||||||
import html
|
import html
|
||||||
import functools
|
import functools
|
||||||
|
|
||||||
|
import sip
|
||||||
from PyQt5.QtCore import pyqtSlot, pyqtSignal, Qt, QUrl, QPoint
|
from PyQt5.QtCore import pyqtSlot, pyqtSignal, Qt, QUrl, QPoint
|
||||||
from PyQt5.QtGui import QDesktopServices
|
from PyQt5.QtGui import QDesktopServices
|
||||||
from PyQt5.QtNetwork import QNetworkReply, QNetworkRequest
|
from PyQt5.QtNetwork import QNetworkReply, QNetworkRequest
|
||||||
@ -33,8 +34,7 @@ from qutebrowser.config import config
|
|||||||
from qutebrowser.browser import pdfjs, shared
|
from qutebrowser.browser import pdfjs, shared
|
||||||
from qutebrowser.browser.webkit import http
|
from qutebrowser.browser.webkit import http
|
||||||
from qutebrowser.browser.webkit.network import networkmanager
|
from qutebrowser.browser.webkit.network import networkmanager
|
||||||
from qutebrowser.utils import (message, usertypes, log, jinja, objreg, debug,
|
from qutebrowser.utils import message, usertypes, log, jinja, objreg
|
||||||
urlutils)
|
|
||||||
|
|
||||||
|
|
||||||
class BrowserPage(QWebPage):
|
class BrowserPage(QWebPage):
|
||||||
@ -54,10 +54,12 @@ class BrowserPage(QWebPage):
|
|||||||
shutting_down: Emitted when the page is currently shutting down.
|
shutting_down: Emitted when the page is currently shutting down.
|
||||||
reloading: Emitted before a web page reloads.
|
reloading: Emitted before a web page reloads.
|
||||||
arg: The URL which gets reloaded.
|
arg: The URL which gets reloaded.
|
||||||
|
navigation_request: Emitted on acceptNavigationRequest.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
shutting_down = pyqtSignal()
|
shutting_down = pyqtSignal()
|
||||||
reloading = pyqtSignal(QUrl)
|
reloading = pyqtSignal(QUrl)
|
||||||
|
navigation_request = pyqtSignal(usertypes.NavigationRequest)
|
||||||
|
|
||||||
def __init__(self, win_id, tab_id, tabdata, private, parent=None):
|
def __init__(self, win_id, tab_id, tabdata, private, parent=None):
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
@ -70,7 +72,6 @@ class BrowserPage(QWebPage):
|
|||||||
}
|
}
|
||||||
self._ignore_load_started = False
|
self._ignore_load_started = False
|
||||||
self.error_occurred = False
|
self.error_occurred = False
|
||||||
self.open_target = usertypes.ClickTarget.normal
|
|
||||||
self._networkmanager = networkmanager.NetworkManager(
|
self._networkmanager = networkmanager.NetworkManager(
|
||||||
win_id=win_id, tab_id=tab_id, private=private, parent=self)
|
win_id=win_id, tab_id=tab_id, private=private, parent=self)
|
||||||
self.setNetworkAccessManager(self._networkmanager)
|
self.setNetworkAccessManager(self._networkmanager)
|
||||||
@ -302,6 +303,10 @@ class BrowserPage(QWebPage):
|
|||||||
Args:
|
Args:
|
||||||
frame: The QWebFrame to inject the user scripts into.
|
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()
|
url = frame.url()
|
||||||
if url.isEmpty():
|
if url.isEmpty():
|
||||||
url = frame.requestedUrl()
|
url = frame.requestedUrl()
|
||||||
@ -474,7 +479,7 @@ class BrowserPage(QWebPage):
|
|||||||
source, line, msg)
|
source, line, msg)
|
||||||
|
|
||||||
def acceptNavigationRequest(self,
|
def acceptNavigationRequest(self,
|
||||||
_frame: QWebFrame,
|
frame: QWebFrame,
|
||||||
request: QNetworkRequest,
|
request: QNetworkRequest,
|
||||||
typ: QWebPage.NavigationType):
|
typ: QWebPage.NavigationType):
|
||||||
"""Override acceptNavigationRequest to handle clicked links.
|
"""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,
|
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.
|
and then conditionally opens the URL here or in another tab/window.
|
||||||
"""
|
"""
|
||||||
url = request.url()
|
type_map = {
|
||||||
log.webview.debug("navigation request: url {}, type {}, "
|
QWebPage.NavigationTypeLinkClicked:
|
||||||
"target {} override {}".format(
|
usertypes.NavigationRequest.Type.link_clicked,
|
||||||
url.toDisplayString(),
|
QWebPage.NavigationTypeFormSubmitted:
|
||||||
debug.qenum_key(QWebPage, typ),
|
usertypes.NavigationRequest.Type.form_submitted,
|
||||||
self.open_target,
|
QWebPage.NavigationTypeFormResubmitted:
|
||||||
self._tabdata.override_target))
|
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:
|
if navigation.navigation_type == navigation.Type.reloaded:
|
||||||
target = self._tabdata.override_target
|
self.reloading.emit(navigation.url)
|
||||||
self._tabdata.override_target = None
|
|
||||||
else:
|
|
||||||
target = self.open_target
|
|
||||||
|
|
||||||
if typ == QWebPage.NavigationTypeReload:
|
self.navigation_request.emit(navigation)
|
||||||
self.reloading.emit(url)
|
return navigation.accepted
|
||||||
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
|
|
||||||
|
@ -262,10 +262,10 @@ class WebView(QWebView):
|
|||||||
target = usertypes.ClickTarget.tab_bg
|
target = usertypes.ClickTarget.tab_bg
|
||||||
else:
|
else:
|
||||||
target = usertypes.ClickTarget.tab
|
target = usertypes.ClickTarget.tab
|
||||||
self.page().open_target = target
|
self._tabdata.open_target = target
|
||||||
log.mouse.debug("Ctrl/Middle click, setting target: {}".format(
|
log.mouse.debug("Ctrl/Middle click, setting target: {}".format(
|
||||||
target))
|
target))
|
||||||
else:
|
else:
|
||||||
self.page().open_target = usertypes.ClickTarget.normal
|
self._tabdata.open_target = usertypes.ClickTarget.normal
|
||||||
log.mouse.debug("Normal click, setting normal target")
|
log.mouse.debug("Normal click, setting normal target")
|
||||||
super().mousePressEvent(e)
|
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}` expands to the URL of the current page
|
||||||
- `{url:pretty}` expands to the URL in decoded format
|
- `{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
|
- `{clipboard}` expands to the clipboard contents
|
||||||
- `{primary}` expands to the primary selection contents
|
- `{primary}` expands to the primary selection contents
|
||||||
|
|
||||||
|
@ -63,9 +63,13 @@ def replace_variables(win_id, arglist):
|
|||||||
QUrl.FullyEncoded | QUrl.RemovePassword),
|
QUrl.FullyEncoded | QUrl.RemovePassword),
|
||||||
'url:pretty': lambda: _current_url(tabbed_browser).toString(
|
'url:pretty': lambda: _current_url(tabbed_browser).toString(
|
||||||
QUrl.DecodeReserved | QUrl.RemovePassword),
|
QUrl.DecodeReserved | QUrl.RemovePassword),
|
||||||
|
'url:host': lambda: _current_url(tabbed_browser).host(),
|
||||||
'clipboard': utils.get_clipboard,
|
'clipboard': utils.get_clipboard,
|
||||||
'primary': lambda: utils.get_clipboard(selection=True),
|
'primary': lambda: utils.get_clipboard(selection=True),
|
||||||
}
|
}
|
||||||
|
for key in list(variables):
|
||||||
|
modified_key = '{' + key + '}'
|
||||||
|
variables[modified_key] = lambda x=modified_key: x
|
||||||
values = {}
|
values = {}
|
||||||
args = []
|
args = []
|
||||||
tabbed_browser = objreg.get('tabbed-browser', scope='window',
|
tabbed_browser = objreg.get('tabbed-browser', scope='window',
|
||||||
|
@ -60,7 +60,7 @@ class Completer(QObject):
|
|||||||
self._timer.setSingleShot(True)
|
self._timer.setSingleShot(True)
|
||||||
self._timer.setInterval(0)
|
self._timer.setInterval(0)
|
||||||
self._timer.timeout.connect(self._update_completion)
|
self._timer.timeout.connect(self._update_completion)
|
||||||
self._last_cursor_pos = None
|
self._last_cursor_pos = -1
|
||||||
self._last_text = None
|
self._last_text = None
|
||||||
self._last_completion_func = None
|
self._last_completion_func = None
|
||||||
self._cmd.update_completion.connect(self.schedule_completion_update)
|
self._cmd.update_completion.connect(self.schedule_completion_update)
|
||||||
|
@ -22,13 +22,15 @@
|
|||||||
from qutebrowser.config import configdata, configexc
|
from qutebrowser.config import configdata, configexc
|
||||||
from qutebrowser.completion.models import completionmodel, listcategory, util
|
from qutebrowser.completion.models import completionmodel, listcategory, util
|
||||||
from qutebrowser.commands import runners, cmdexc
|
from qutebrowser.commands import runners, cmdexc
|
||||||
|
from qutebrowser.keyinput import keyutils
|
||||||
|
|
||||||
|
|
||||||
def option(*, info):
|
def option(*, info):
|
||||||
"""A CompletionModel filled with settings and their descriptions."""
|
"""A CompletionModel filled with settings and their descriptions."""
|
||||||
model = completionmodel.CompletionModel(column_widths=(20, 70, 10))
|
model = completionmodel.CompletionModel(column_widths=(20, 70, 10))
|
||||||
options = ((opt.name, opt.description, info.config.get_str(opt.name))
|
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))
|
model.add_category(listcategory.ListCategory("Options", options))
|
||||||
return model
|
return model
|
||||||
|
|
||||||
@ -36,8 +38,10 @@ def option(*, info):
|
|||||||
def customized_option(*, info):
|
def customized_option(*, info):
|
||||||
"""A CompletionModel filled with set settings and their descriptions."""
|
"""A CompletionModel filled with set settings and their descriptions."""
|
||||||
model = completionmodel.CompletionModel(column_widths=(20, 70, 10))
|
model = completionmodel.CompletionModel(column_widths=(20, 70, 10))
|
||||||
options = ((opt.name, opt.description, info.config.get_str(opt.name))
|
options = ((values.opt.name, values.opt.description,
|
||||||
for opt, _value in info.config)
|
info.config.get_str(values.opt.name))
|
||||||
|
for values in info.config
|
||||||
|
if values)
|
||||||
model.add_category(listcategory.ListCategory("Customized options",
|
model.add_category(listcategory.ListCategory("Customized options",
|
||||||
options))
|
options))
|
||||||
return model
|
return model
|
||||||
@ -71,16 +75,16 @@ def value(optname, *_values, info):
|
|||||||
return model
|
return model
|
||||||
|
|
||||||
|
|
||||||
def bind(key, *, info):
|
def _bind_current_default(key, info):
|
||||||
"""A CompletionModel filled with all bindable commands and descriptions.
|
"""Get current/default data for the given key."""
|
||||||
|
|
||||||
Args:
|
|
||||||
key: the key being bound.
|
|
||||||
"""
|
|
||||||
model = completionmodel.CompletionModel(column_widths=(20, 60, 20))
|
|
||||||
data = []
|
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:
|
if cmd_text:
|
||||||
parser = runners.CommandParser()
|
parser = runners.CommandParser()
|
||||||
try:
|
try:
|
||||||
@ -90,12 +94,24 @@ def bind(key, *, info):
|
|||||||
else:
|
else:
|
||||||
data.append((cmd_text, '(Current) {}'.format(cmd.desc), key))
|
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:
|
if cmd_text:
|
||||||
parser = runners.CommandParser()
|
parser = runners.CommandParser()
|
||||||
cmd = parser.parse(cmd_text).cmd
|
cmd = parser.parse(cmd_text).cmd
|
||||||
data.append((cmd_text, '(Default) {}'.format(cmd.desc), key))
|
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:
|
if data:
|
||||||
model.add_category(listcategory.ListCategory("Current/Default", data))
|
model.add_category(listcategory.ListCategory("Current/Default", data))
|
||||||
|
|
||||||
|
@ -80,7 +80,7 @@ class HistoryCategory(QSqlQueryModel):
|
|||||||
for i in range(len(words)))
|
for i in range(len(words)))
|
||||||
|
|
||||||
# replace ' in timestamp-format to avoid breaking the query
|
# 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')"
|
timefmt = ("strftime('{}', last_atime, 'unixepoch', 'localtime')"
|
||||||
.format(timestamp_format.replace("'", "`")))
|
.format(timestamp_format.replace("'", "`")))
|
||||||
|
|
||||||
|
@ -117,11 +117,11 @@ def _buffer(skip_win_id=None):
|
|||||||
if tabbed_browser.shutting_down:
|
if tabbed_browser.shutting_down:
|
||||||
continue
|
continue
|
||||||
tabs = []
|
tabs = []
|
||||||
for idx in range(tabbed_browser.count()):
|
for idx in range(tabbed_browser.widget.count()):
|
||||||
tab = tabbed_browser.widget(idx)
|
tab = tabbed_browser.widget.widget(idx)
|
||||||
tabs.append(("{}/{}".format(win_id, idx + 1),
|
tabs.append(("{}/{}".format(win_id, idx + 1),
|
||||||
tab.url().toDisplayString(),
|
tab.url().toDisplayString(),
|
||||||
tabbed_browser.page_title(idx)))
|
tabbed_browser.widget.page_title(idx)))
|
||||||
cat = listcategory.ListCategory("{}".format(win_id), tabs,
|
cat = listcategory.ListCategory("{}".format(win_id), tabs,
|
||||||
delete_func=delete_buffer)
|
delete_func=delete_buffer)
|
||||||
model.add_category(cat)
|
model.add_category(cat)
|
||||||
|
@ -25,9 +25,10 @@ import functools
|
|||||||
|
|
||||||
from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject
|
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.utils import utils, log, jinja
|
||||||
from qutebrowser.misc import objects
|
from qutebrowser.misc import objects
|
||||||
|
from qutebrowser.keyinput import keyutils
|
||||||
|
|
||||||
# An easy way to access the config from other code via config.val.foo
|
# An easy way to access the config from other code via config.val.foo
|
||||||
val = None
|
val = None
|
||||||
@ -37,6 +38,9 @@ key_instance = None
|
|||||||
# Keeping track of all change filters to validate them later.
|
# Keeping track of all change filters to validate them later.
|
||||||
change_filters = []
|
change_filters = []
|
||||||
|
|
||||||
|
# Sentinel
|
||||||
|
UNSET = object()
|
||||||
|
|
||||||
|
|
||||||
class change_filter: # noqa: N801,N806 pylint: disable=invalid-name
|
class change_filter: # noqa: N801,N806 pylint: disable=invalid-name
|
||||||
|
|
||||||
@ -132,20 +136,18 @@ class KeyConfig:
|
|||||||
def __init__(self, config):
|
def __init__(self, config):
|
||||||
self._config = config
|
self._config = config
|
||||||
|
|
||||||
def _prepare(self, key, mode):
|
def _validate(self, key, mode):
|
||||||
"""Make sure the given mode exists and normalize the key."""
|
"""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:
|
if mode not in configdata.DATA['bindings.default'].default:
|
||||||
raise configexc.KeybindingError("Invalid mode {}!".format(mode))
|
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):
|
def get_bindings_for(self, mode):
|
||||||
"""Get the combined bindings for the given mode."""
|
"""Get the combined bindings for the given mode."""
|
||||||
bindings = dict(val.bindings.default[mode])
|
bindings = dict(val.bindings.default[mode])
|
||||||
for key, binding in val.bindings.commands[mode].items():
|
for key, binding in val.bindings.commands[mode].items():
|
||||||
if binding is None:
|
if not binding:
|
||||||
bindings.pop(key, None)
|
bindings.pop(key, None)
|
||||||
else:
|
else:
|
||||||
bindings[key] = binding
|
bindings[key] = binding
|
||||||
@ -155,20 +157,20 @@ class KeyConfig:
|
|||||||
"""Get a dict of commands to a list of bindings for the mode."""
|
"""Get a dict of commands to a list of bindings for the mode."""
|
||||||
cmd_to_keys = {}
|
cmd_to_keys = {}
|
||||||
bindings = self.get_bindings_for(mode)
|
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(';;'):
|
for cmd in full_cmd.split(';;'):
|
||||||
cmd = cmd.strip()
|
cmd = cmd.strip()
|
||||||
cmd_to_keys.setdefault(cmd, [])
|
cmd_to_keys.setdefault(cmd, [])
|
||||||
# put special bindings last
|
# Put bindings involving modifiers last
|
||||||
if utils.is_special_key(key):
|
if any(info.modifiers for info in seq):
|
||||||
cmd_to_keys[cmd].append(key)
|
cmd_to_keys[cmd].append(str(seq))
|
||||||
else:
|
else:
|
||||||
cmd_to_keys[cmd].insert(0, key)
|
cmd_to_keys[cmd].insert(0, str(seq))
|
||||||
return cmd_to_keys
|
return cmd_to_keys
|
||||||
|
|
||||||
def get_command(self, key, mode, default=False):
|
def get_command(self, key, mode, default=False):
|
||||||
"""Get the command for a given key (or None)."""
|
"""Get the command for a given key (or None)."""
|
||||||
key = self._prepare(key, mode)
|
self._validate(key, mode)
|
||||||
if default:
|
if default:
|
||||||
bindings = dict(val.bindings.default[mode])
|
bindings = dict(val.bindings.default[mode])
|
||||||
else:
|
else:
|
||||||
@ -182,23 +184,23 @@ class KeyConfig:
|
|||||||
"Can't add binding '{}' with empty command in {} "
|
"Can't add binding '{}' with empty command in {} "
|
||||||
'mode'.format(key, mode))
|
'mode'.format(key, mode))
|
||||||
|
|
||||||
key = self._prepare(key, mode)
|
self._validate(key, mode)
|
||||||
log.keyboard.vdebug("Adding binding {} -> {} in mode {}.".format(
|
log.keyboard.vdebug("Adding binding {} -> {} in mode {}.".format(
|
||||||
key, command, mode))
|
key, command, mode))
|
||||||
|
|
||||||
bindings = self._config.get_obj('bindings.commands')
|
bindings = self._config.get_mutable_obj('bindings.commands')
|
||||||
if mode not in bindings:
|
if mode not in bindings:
|
||||||
bindings[mode] = {}
|
bindings[mode] = {}
|
||||||
bindings[mode][key] = command
|
bindings[mode][str(key)] = command
|
||||||
self._config.update_mutables(save_yaml=save_yaml)
|
self._config.update_mutables(save_yaml=save_yaml)
|
||||||
|
|
||||||
def bind_default(self, key, *, mode='normal', save_yaml=False):
|
def bind_default(self, key, *, mode='normal', save_yaml=False):
|
||||||
"""Restore a default keybinding."""
|
"""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:
|
try:
|
||||||
del bindings_commands[mode][key]
|
del bindings_commands[mode][str(key)]
|
||||||
except KeyError:
|
except KeyError:
|
||||||
raise configexc.KeybindingError(
|
raise configexc.KeybindingError(
|
||||||
"Can't find binding '{}' in {} mode".format(key, mode))
|
"Can't find binding '{}' in {} mode".format(key, mode))
|
||||||
@ -206,18 +208,18 @@ class KeyConfig:
|
|||||||
|
|
||||||
def unbind(self, key, *, mode='normal', save_yaml=False):
|
def unbind(self, key, *, mode='normal', save_yaml=False):
|
||||||
"""Unbind the given key in the given mode."""
|
"""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:
|
if val.bindings.commands[mode].get(key, None) is not None:
|
||||||
# In custom bindings -> remove it
|
# In custom bindings -> remove it
|
||||||
del bindings_commands[mode][key]
|
del bindings_commands[mode][str(key)]
|
||||||
elif key in val.bindings.default[mode]:
|
elif key in val.bindings.default[mode]:
|
||||||
# In default bindings -> shadow it with None
|
# In default bindings -> shadow it with None
|
||||||
if mode not in bindings_commands:
|
if mode not in bindings_commands:
|
||||||
bindings_commands[mode] = {}
|
bindings_commands[mode] = {}
|
||||||
bindings_commands[mode][key] = None
|
bindings_commands[mode][str(key)] = None
|
||||||
else:
|
else:
|
||||||
raise configexc.KeybindingError(
|
raise configexc.KeybindingError(
|
||||||
"Can't find binding '{}' in {} mode".format(key, mode))
|
"Can't find binding '{}' in {} mode".format(key, mode))
|
||||||
@ -229,8 +231,12 @@ class Config(QObject):
|
|||||||
|
|
||||||
"""Main config object.
|
"""Main config object.
|
||||||
|
|
||||||
|
Class attributes:
|
||||||
|
MUTABLE_TYPES: Types returned from the config which could potentially
|
||||||
|
be mutated.
|
||||||
|
|
||||||
Attributes:
|
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.
|
_mutables: A dictionary of mutable objects to be checked for changes.
|
||||||
_yaml: A YamlConfig object or None.
|
_yaml: A YamlConfig object or None.
|
||||||
|
|
||||||
@ -238,19 +244,25 @@ class Config(QObject):
|
|||||||
changed: Emitted with the option name when an option changed.
|
changed: Emitted with the option name when an option changed.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
MUTABLE_TYPES = (dict, list)
|
||||||
changed = pyqtSignal(str)
|
changed = pyqtSignal(str)
|
||||||
|
|
||||||
def __init__(self, yaml_config, parent=None):
|
def __init__(self, yaml_config, parent=None):
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
self.changed.connect(_render_stylesheet.cache_clear)
|
self.changed.connect(_render_stylesheet.cache_clear)
|
||||||
self._values = {}
|
|
||||||
self._mutables = {}
|
self._mutables = {}
|
||||||
self._yaml = yaml_config
|
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):
|
def __iter__(self):
|
||||||
"""Iterate over Option, value tuples."""
|
"""Iterate over configutils.Values items."""
|
||||||
for name, value in sorted(self._values.items()):
|
yield from self._values.values()
|
||||||
yield (self.get_opt(name), value)
|
|
||||||
|
|
||||||
def init_save_manager(self, save_manager):
|
def init_save_manager(self, save_manager):
|
||||||
"""Make sure the config gets saved properly.
|
"""Make sure the config gets saved properly.
|
||||||
@ -260,24 +272,32 @@ class Config(QObject):
|
|||||||
"""
|
"""
|
||||||
self._yaml.init_save_manager(save_manager)
|
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."""
|
"""Set the given option to the given value."""
|
||||||
if not isinstance(objects.backend, objects.NoBackend):
|
if not isinstance(objects.backend, objects.NoBackend):
|
||||||
if objects.backend not in opt.backends:
|
if objects.backend not in opt.backends:
|
||||||
raise configexc.BackendError(opt.name, objects.backend)
|
raise configexc.BackendError(opt.name, objects.backend)
|
||||||
|
|
||||||
opt.typ.to_py(value) # for validation
|
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)
|
self.changed.emit(opt.name)
|
||||||
log.config.debug("Config option changed: {} = {}".format(
|
log.config.debug("Config option changed: {} = {}".format(
|
||||||
opt.name, value))
|
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):
|
def read_yaml(self):
|
||||||
"""Read the YAML settings from self._yaml."""
|
"""Read the YAML settings from self._yaml."""
|
||||||
self._yaml.load()
|
self._yaml.load()
|
||||||
for name, value in self._yaml:
|
for values in self._yaml:
|
||||||
self._set_value(self.get_opt(name), value)
|
for scoped in values:
|
||||||
|
self._set_value(values.opt, scoped.value,
|
||||||
|
pattern=scoped.pattern)
|
||||||
|
|
||||||
def get_opt(self, name):
|
def get_opt(self, name):
|
||||||
"""Get a configdata.Option object for the given setting."""
|
"""Get a configdata.Option object for the given setting."""
|
||||||
@ -290,77 +310,115 @@ class Config(QObject):
|
|||||||
name, deleted=deleted, renamed=renamed)
|
name, deleted=deleted, renamed=renamed)
|
||||||
raise exception from None
|
raise exception from None
|
||||||
|
|
||||||
def get(self, name):
|
def get(self, name, url=None):
|
||||||
"""Get the given setting converted for Python code."""
|
"""Get the given setting converted for Python code."""
|
||||||
opt = self.get_opt(name)
|
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)
|
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).
|
"""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)
|
self.get_opt(name) # To make sure it exists
|
||||||
obj = None
|
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
|
# If we allow mutation, there is a chance that prior mutations already
|
||||||
# entered the mutable dictionary and thus further copies are unneeded
|
# entered the mutable dictionary and thus further copies are unneeded
|
||||||
# until update_mutables() is called
|
# until update_mutables() is called
|
||||||
if name in self._mutables and mutable:
|
if name in self._mutables:
|
||||||
_copy, obj = self._mutables[name]
|
_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):
|
value = self._values[name].get_for_pattern(pattern)
|
||||||
"""Get the given setting as string."""
|
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)
|
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)
|
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.
|
"""Set the given setting from a YAML/config.py object.
|
||||||
|
|
||||||
If save_yaml=True is given, store the new value to YAML.
|
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:
|
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.
|
"""Set the given setting from a string.
|
||||||
|
|
||||||
If save_yaml=True is given, store the new value to YAML.
|
If save_yaml=True is given, store the new value to YAML.
|
||||||
"""
|
"""
|
||||||
opt = self.get_opt(name)
|
opt = self.get_opt(name)
|
||||||
|
self._check_yaml(opt, save_yaml)
|
||||||
converted = opt.typ.from_str(value)
|
converted = opt.typ.from_str(value)
|
||||||
log.config.debug("Setting {} (type {}) to {!r} (converted from {!r})"
|
log.config.debug("Setting {} (type {}) to {!r} (converted from {!r})"
|
||||||
.format(name, opt.typ.__class__.__name__, converted,
|
.format(name, opt.typ.__class__.__name__, converted,
|
||||||
value))
|
value))
|
||||||
self._set_value(opt, converted)
|
self._set_value(opt, converted, pattern=pattern)
|
||||||
if save_yaml:
|
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."""
|
"""Set the given setting back to its default."""
|
||||||
self.get_opt(name)
|
opt = self.get_opt(name)
|
||||||
try:
|
self._check_yaml(opt, save_yaml)
|
||||||
del self._values[name]
|
changed = self._values[name].remove(pattern)
|
||||||
except KeyError:
|
if changed:
|
||||||
return
|
|
||||||
self.changed.emit(name)
|
self.changed.emit(name)
|
||||||
|
|
||||||
if save_yaml:
|
if save_yaml:
|
||||||
self._yaml.unset(name)
|
self._yaml.unset(name, pattern=pattern)
|
||||||
|
|
||||||
def clear(self, *, save_yaml=False):
|
def clear(self, *, save_yaml=False):
|
||||||
"""Clear all settings in the config.
|
"""Clear all settings in the config.
|
||||||
@ -368,9 +426,9 @@ class Config(QObject):
|
|||||||
If save_yaml=True is given, also remove all customization from the YAML
|
If save_yaml=True is given, also remove all customization from the YAML
|
||||||
file.
|
file.
|
||||||
"""
|
"""
|
||||||
old_values = self._values
|
for name, values in self._values.items():
|
||||||
self._values = {}
|
if values:
|
||||||
for name in old_values:
|
values.clear()
|
||||||
self.changed.emit(name)
|
self.changed.emit(name)
|
||||||
|
|
||||||
if save_yaml:
|
if save_yaml:
|
||||||
@ -397,13 +455,15 @@ class Config(QObject):
|
|||||||
Return:
|
Return:
|
||||||
The changed config part as string.
|
The changed config part as string.
|
||||||
"""
|
"""
|
||||||
lines = []
|
blocks = []
|
||||||
for opt, value in self:
|
for values in sorted(self, key=lambda v: v.opt.name):
|
||||||
str_value = opt.typ.to_str(value)
|
if values:
|
||||||
lines.append('{} = {}'.format(opt.name, str_value))
|
blocks.append(str(values))
|
||||||
if not lines:
|
|
||||||
lines = ['<Default configuration>']
|
if not blocks:
|
||||||
return '\n'.join(lines)
|
return '<Default configuration>'
|
||||||
|
|
||||||
|
return '\n'.join(blocks)
|
||||||
|
|
||||||
|
|
||||||
class ConfigContainer:
|
class ConfigContainer:
|
||||||
@ -415,16 +475,21 @@ class ConfigContainer:
|
|||||||
_prefix: The __getattr__ chain leading up to this object.
|
_prefix: The __getattr__ chain leading up to this object.
|
||||||
_configapi: If given, get values suitable for config.py and
|
_configapi: If given, get values suitable for config.py and
|
||||||
add errors to the given ConfigAPI object.
|
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._config = config
|
||||||
self._prefix = prefix
|
self._prefix = prefix
|
||||||
self._configapi = configapi
|
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):
|
def __repr__(self):
|
||||||
return utils.get_repr(self, constructor=True, config=self._config,
|
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
|
@contextlib.contextmanager
|
||||||
def _handle_error(self, action, name):
|
def _handle_error(self, action, name):
|
||||||
@ -452,7 +517,7 @@ class ConfigContainer:
|
|||||||
if configdata.is_valid_prefix(name):
|
if configdata.is_valid_prefix(name):
|
||||||
return ConfigContainer(config=self._config,
|
return ConfigContainer(config=self._config,
|
||||||
configapi=self._configapi,
|
configapi=self._configapi,
|
||||||
prefix=name)
|
prefix=name, pattern=self._pattern)
|
||||||
|
|
||||||
with self._handle_error('getting', name):
|
with self._handle_error('getting', name):
|
||||||
if self._configapi is None:
|
if self._configapi is None:
|
||||||
@ -460,7 +525,8 @@ class ConfigContainer:
|
|||||||
return self._config.get(name)
|
return self._config.get(name)
|
||||||
else:
|
else:
|
||||||
# access from config.py
|
# 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):
|
def __setattr__(self, attr, value):
|
||||||
"""Set the given option in the config."""
|
"""Set the given option in the config."""
|
||||||
@ -470,7 +536,7 @@ class ConfigContainer:
|
|||||||
|
|
||||||
name = self._join(attr)
|
name = self._join(attr)
|
||||||
with self._handle_error('setting', name):
|
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):
|
def _join(self, attr):
|
||||||
"""Get the prefix joined with the given attribute."""
|
"""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.commands import cmdexc, cmdutils
|
||||||
from qutebrowser.completion.models import configmodel
|
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.config import configtypes, configexc, configfiles, configdata
|
||||||
from qutebrowser.misc import editor
|
from qutebrowser.misc import editor
|
||||||
|
from qutebrowser.keyinput import keyutils
|
||||||
|
|
||||||
|
|
||||||
class ConfigCommands:
|
class ConfigCommands:
|
||||||
@ -47,17 +48,41 @@ class ConfigCommands:
|
|||||||
except configexc.Error as e:
|
except configexc.Error as e:
|
||||||
raise cmdexc.CommandError(str(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."""
|
"""Print the value of the given option."""
|
||||||
with self._handle_config_error():
|
with self._handle_config_error():
|
||||||
value = self._config.get_str(option)
|
value = self._config.get_str(option, pattern=pattern)
|
||||||
message.info("{} = {}".format(option, value))
|
|
||||||
|
text = "{} = {}".format(option, value)
|
||||||
|
if pattern is not None:
|
||||||
|
text += " for {}".format(pattern)
|
||||||
|
message.info(text)
|
||||||
|
|
||||||
@cmdutils.register(instance='config-commands')
|
@cmdutils.register(instance='config-commands')
|
||||||
@cmdutils.argument('option', completion=configmodel.option)
|
@cmdutils.argument('option', completion=configmodel.option)
|
||||||
@cmdutils.argument('value', completion=configmodel.value)
|
@cmdutils.argument('value', completion=configmodel.value)
|
||||||
@cmdutils.argument('win_id', win_id=True)
|
@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.
|
"""Set an option.
|
||||||
|
|
||||||
If the option name ends with '?', the value of the option is shown
|
If the option name ends with '?', the value of the option is shown
|
||||||
@ -69,6 +94,7 @@ class ConfigCommands:
|
|||||||
Args:
|
Args:
|
||||||
option: The name of the option.
|
option: The name of the option.
|
||||||
value: The value to set.
|
value: The value to set.
|
||||||
|
pattern: The URL pattern to use.
|
||||||
temp: Set value temporarily until qutebrowser is closed.
|
temp: Set value temporarily until qutebrowser is closed.
|
||||||
print_: Print the value after setting.
|
print_: Print the value after setting.
|
||||||
"""
|
"""
|
||||||
@ -82,8 +108,10 @@ class ConfigCommands:
|
|||||||
raise cmdexc.CommandError("Toggling values was moved to the "
|
raise cmdexc.CommandError("Toggling values was moved to the "
|
||||||
":config-cycle command")
|
":config-cycle command")
|
||||||
|
|
||||||
|
pattern = self._parse_pattern(pattern)
|
||||||
|
|
||||||
if option.endswith('?') and option != '?':
|
if option.endswith('?') and option != '?':
|
||||||
self._print_value(option[:-1])
|
self._print_value(option[:-1], pattern=pattern)
|
||||||
return
|
return
|
||||||
|
|
||||||
with self._handle_config_error():
|
with self._handle_config_error():
|
||||||
@ -91,10 +119,11 @@ class ConfigCommands:
|
|||||||
raise cmdexc.CommandError("set: The following arguments "
|
raise cmdexc.CommandError("set: The following arguments "
|
||||||
"are required: value")
|
"are required: value")
|
||||||
else:
|
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_:
|
if print_:
|
||||||
self._print_value(option)
|
self._print_value(option, pattern=pattern)
|
||||||
|
|
||||||
@cmdutils.register(instance='config-commands', maxsplit=1,
|
@cmdutils.register(instance='config-commands', maxsplit=1,
|
||||||
no_cmd_split=True, no_replace_variables=True)
|
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.
|
Using :bind without any arguments opens a page showing all keybindings.
|
||||||
|
|
||||||
Args:
|
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.
|
command: The command to execute, with optional args.
|
||||||
mode: A comma-separated list of modes to bind the key in
|
mode: A comma-separated list of modes to bind the key in
|
||||||
(default: `normal`). See `:help bindings.commands` for the
|
(default: `normal`). See `:help bindings.commands` for the
|
||||||
@ -121,58 +151,64 @@ class ConfigCommands:
|
|||||||
tabbed_browser.openurl(QUrl('qute://bindings'), newtab=True)
|
tabbed_browser.openurl(QUrl('qute://bindings'), newtab=True)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
seq = self._parse_key(key)
|
||||||
|
|
||||||
if command is None:
|
if command is None:
|
||||||
if default:
|
if default:
|
||||||
# :bind --default: Restore default
|
# :bind --default: Restore default
|
||||||
with self._handle_config_error():
|
with self._handle_config_error():
|
||||||
self._keyconfig.bind_default(key, mode=mode,
|
self._keyconfig.bind_default(seq, mode=mode,
|
||||||
save_yaml=True)
|
save_yaml=True)
|
||||||
return
|
return
|
||||||
|
|
||||||
# No --default -> print binding
|
# 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():
|
with self._handle_config_error():
|
||||||
cmd = self._keyconfig.get_command(key, mode)
|
cmd = self._keyconfig.get_command(seq, mode)
|
||||||
if cmd is None:
|
if cmd is None:
|
||||||
message.info("{} is unbound in {} mode".format(key, mode))
|
message.info("{} is unbound in {} mode".format(seq, mode))
|
||||||
else:
|
else:
|
||||||
message.info("{} is bound to '{}' in {} mode".format(
|
message.info("{} is bound to '{}' in {} mode".format(
|
||||||
key, cmd, mode))
|
seq, cmd, mode))
|
||||||
return
|
return
|
||||||
|
|
||||||
with self._handle_config_error():
|
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')
|
@cmdutils.register(instance='config-commands')
|
||||||
def unbind(self, key, *, mode='normal'):
|
def unbind(self, key, *, mode='normal'):
|
||||||
"""Unbind a keychain.
|
"""Unbind a keychain.
|
||||||
|
|
||||||
Args:
|
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`).
|
mode: A mode to unbind the key in (default: `normal`).
|
||||||
See `:help bindings.commands` for the available modes.
|
See `:help bindings.commands` for the available modes.
|
||||||
"""
|
"""
|
||||||
with self._handle_config_error():
|
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.register(instance='config-commands', star_args_optional=True)
|
||||||
@cmdutils.argument('option', completion=configmodel.option)
|
@cmdutils.argument('option', completion=configmodel.option)
|
||||||
@cmdutils.argument('values', completion=configmodel.value)
|
@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.
|
"""Cycle an option between multiple values.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
option: The name of the option.
|
option: The name of the option.
|
||||||
values: The values to cycle through.
|
values: The values to cycle through.
|
||||||
|
pattern: The URL pattern to use.
|
||||||
temp: Set value temporarily until qutebrowser is closed.
|
temp: Set value temporarily until qutebrowser is closed.
|
||||||
print_: Print the value after setting.
|
print_: Print the value after setting.
|
||||||
"""
|
"""
|
||||||
|
pattern = self._parse_pattern(pattern)
|
||||||
|
|
||||||
with self._handle_config_error():
|
with self._handle_config_error():
|
||||||
opt = self._config.get_opt(option)
|
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):
|
if not values and isinstance(opt.typ, configtypes.Bool):
|
||||||
values = ['true', 'false']
|
values = ['true', 'false']
|
||||||
@ -194,10 +230,11 @@ class ConfigCommands:
|
|||||||
value = values[0]
|
value = values[0]
|
||||||
|
|
||||||
with self._handle_config_error():
|
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_:
|
if print_:
|
||||||
self._print_value(option)
|
self._print_value(option, pattern=pattern)
|
||||||
|
|
||||||
@cmdutils.register(instance='config-commands')
|
@cmdutils.register(instance='config-commands')
|
||||||
@cmdutils.argument('option', completion=configmodel.customized_option)
|
@cmdutils.argument('option', completion=configmodel.customized_option)
|
||||||
@ -291,13 +328,16 @@ class ConfigCommands:
|
|||||||
"overwrite!".format(filename))
|
"overwrite!".format(filename))
|
||||||
|
|
||||||
if defaults:
|
if defaults:
|
||||||
options = [(opt, opt.default)
|
options = [(None, opt, opt.default)
|
||||||
for _name, opt in sorted(configdata.DATA.items())]
|
for _name, opt in sorted(configdata.DATA.items())]
|
||||||
bindings = dict(configdata.DATA['bindings.default'].default)
|
bindings = dict(configdata.DATA['bindings.default'].default)
|
||||||
commented = True
|
commented = True
|
||||||
else:
|
else:
|
||||||
options = list(self._config)
|
options = []
|
||||||
bindings = dict(self._config.get_obj('bindings.commands'))
|
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
|
commented = False
|
||||||
|
|
||||||
writer = configfiles.ConfigPyWriter(options, bindings,
|
writer = configfiles.ConfigPyWriter(options, bindings,
|
||||||
|
@ -48,7 +48,9 @@ class Option:
|
|||||||
backends = attr.ib()
|
backends = attr.ib()
|
||||||
raw_backends = attr.ib()
|
raw_backends = attr.ib()
|
||||||
description = attr.ib()
|
description = attr.ib()
|
||||||
|
supports_pattern = attr.ib(default=False)
|
||||||
restart = attr.ib(default=False)
|
restart = attr.ib(default=False)
|
||||||
|
no_autoconfig = attr.ib(default=False)
|
||||||
|
|
||||||
|
|
||||||
@attr.s
|
@attr.s
|
||||||
@ -197,7 +199,8 @@ def _read_yaml(yaml_data):
|
|||||||
migrations = Migrations()
|
migrations = Migrations()
|
||||||
data = utils.yaml_load(yaml_data)
|
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():
|
for name, option in data.items():
|
||||||
if set(option.keys()) == {'renamed'}:
|
if set(option.keys()) == {'renamed'}:
|
||||||
@ -223,7 +226,10 @@ def _read_yaml(yaml_data):
|
|||||||
backends=_parse_yaml_backends(name, backends),
|
backends=_parse_yaml_backends(name, backends),
|
||||||
raw_backends=backends if isinstance(backends, dict) else None,
|
raw_backends=backends if isinstance(backends, dict) else None,
|
||||||
description=option['desc'],
|
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.
|
# Make sure no key shadows another.
|
||||||
for key1 in parsed:
|
for key1 in parsed:
|
||||||
|
@ -240,6 +240,7 @@ content.cache.appcache:
|
|||||||
default: true
|
default: true
|
||||||
type: Bool
|
type: Bool
|
||||||
backend: QtWebKit
|
backend: QtWebKit
|
||||||
|
supports_pattern: true
|
||||||
desc: >-
|
desc: >-
|
||||||
Enable support for the HTML 5 web application cache feature.
|
Enable support for the HTML 5 web application cache feature.
|
||||||
|
|
||||||
@ -298,12 +299,14 @@ content.dns_prefetch:
|
|||||||
default: true
|
default: true
|
||||||
type: Bool
|
type: Bool
|
||||||
backend: QtWebKit
|
backend: QtWebKit
|
||||||
|
supports_pattern: true
|
||||||
desc: Try to pre-fetch DNS entries to speed up browsing.
|
desc: Try to pre-fetch DNS entries to speed up browsing.
|
||||||
|
|
||||||
content.frame_flattening:
|
content.frame_flattening:
|
||||||
default: false
|
default: false
|
||||||
type: Bool
|
type: Bool
|
||||||
backend: QtWebKit
|
backend: QtWebKit
|
||||||
|
supports_pattern: true
|
||||||
desc: >-
|
desc: >-
|
||||||
Expand each subframe to its contents.
|
Expand each subframe to its contents.
|
||||||
|
|
||||||
@ -459,12 +462,14 @@ content.host_blocking.whitelist:
|
|||||||
content.hyperlink_auditing:
|
content.hyperlink_auditing:
|
||||||
default: false
|
default: false
|
||||||
type: Bool
|
type: Bool
|
||||||
|
supports_pattern: true
|
||||||
desc: Enable hyperlink auditing (`<a ping>`).
|
desc: Enable hyperlink auditing (`<a ping>`).
|
||||||
|
|
||||||
content.images:
|
content.images:
|
||||||
default: true
|
default: true
|
||||||
type: Bool
|
type: Bool
|
||||||
desc: Load images automatically in web pages.
|
desc: Load images automatically in web pages.
|
||||||
|
supports_pattern: true
|
||||||
|
|
||||||
content.javascript.alert:
|
content.javascript.alert:
|
||||||
default: true
|
default: true
|
||||||
@ -474,6 +479,7 @@ content.javascript.alert:
|
|||||||
content.javascript.can_access_clipboard:
|
content.javascript.can_access_clipboard:
|
||||||
default: false
|
default: false
|
||||||
type: Bool
|
type: Bool
|
||||||
|
supports_pattern: true
|
||||||
desc: >-
|
desc: >-
|
||||||
Allow JavaScript to read from or write to the clipboard.
|
Allow JavaScript to read from or write to the clipboard.
|
||||||
|
|
||||||
@ -484,16 +490,19 @@ content.javascript.can_close_tabs:
|
|||||||
default: false
|
default: false
|
||||||
type: Bool
|
type: Bool
|
||||||
backend: QtWebKit
|
backend: QtWebKit
|
||||||
|
supports_pattern: true
|
||||||
desc: Allow JavaScript to close tabs.
|
desc: Allow JavaScript to close tabs.
|
||||||
|
|
||||||
content.javascript.can_open_tabs_automatically:
|
content.javascript.can_open_tabs_automatically:
|
||||||
default: false
|
default: false
|
||||||
type: Bool
|
type: Bool
|
||||||
|
supports_pattern: true
|
||||||
desc: Allow JavaScript to open new tabs without user interaction.
|
desc: Allow JavaScript to open new tabs without user interaction.
|
||||||
|
|
||||||
content.javascript.enabled:
|
content.javascript.enabled:
|
||||||
default: true
|
default: true
|
||||||
type: Bool
|
type: Bool
|
||||||
|
supports_pattern: true
|
||||||
desc: Enable JavaScript.
|
desc: Enable JavaScript.
|
||||||
|
|
||||||
content.javascript.log:
|
content.javascript.log:
|
||||||
@ -536,16 +545,19 @@ content.javascript.prompt:
|
|||||||
content.local_content_can_access_remote_urls:
|
content.local_content_can_access_remote_urls:
|
||||||
default: false
|
default: false
|
||||||
type: Bool
|
type: Bool
|
||||||
|
supports_pattern: true
|
||||||
desc: Allow locally loaded documents to access remote URLs.
|
desc: Allow locally loaded documents to access remote URLs.
|
||||||
|
|
||||||
content.local_content_can_access_file_urls:
|
content.local_content_can_access_file_urls:
|
||||||
default: true
|
default: true
|
||||||
type: Bool
|
type: Bool
|
||||||
|
supports_pattern: true
|
||||||
desc: Allow locally loaded documents to access other local URLs.
|
desc: Allow locally loaded documents to access other local URLs.
|
||||||
|
|
||||||
content.local_storage:
|
content.local_storage:
|
||||||
default: true
|
default: true
|
||||||
type: Bool
|
type: Bool
|
||||||
|
supports_pattern: true
|
||||||
desc: Enable support for HTML 5 local storage and Web SQL.
|
desc: Enable support for HTML 5 local storage and Web SQL.
|
||||||
|
|
||||||
content.media_capture:
|
content.media_capture:
|
||||||
@ -583,6 +595,7 @@ content.pdfjs:
|
|||||||
content.plugins:
|
content.plugins:
|
||||||
default: false
|
default: false
|
||||||
type: Bool
|
type: Bool
|
||||||
|
supports_pattern: true
|
||||||
desc: Enable plugins in Web pages.
|
desc: Enable plugins in Web pages.
|
||||||
|
|
||||||
content.print_element_backgrounds:
|
content.print_element_backgrounds:
|
||||||
@ -591,6 +604,7 @@ content.print_element_backgrounds:
|
|||||||
backend:
|
backend:
|
||||||
QtWebKit: true
|
QtWebKit: true
|
||||||
QtWebEngine: Qt 5.8
|
QtWebEngine: Qt 5.8
|
||||||
|
supports_pattern: true
|
||||||
desc: >-
|
desc: >-
|
||||||
Draw the background color and images also when the page is printed.
|
Draw the background color and images also when the page is printed.
|
||||||
|
|
||||||
@ -631,11 +645,13 @@ content.user_stylesheets:
|
|||||||
content.webgl:
|
content.webgl:
|
||||||
default: true
|
default: true
|
||||||
type: Bool
|
type: Bool
|
||||||
|
supports_pattern: true
|
||||||
desc: Enable WebGL.
|
desc: Enable WebGL.
|
||||||
|
|
||||||
content.xss_auditing:
|
content.xss_auditing:
|
||||||
type: Bool
|
type: Bool
|
||||||
default: false
|
default: false
|
||||||
|
supports_pattern: true
|
||||||
desc: >-
|
desc: >-
|
||||||
Monitor load requests for cross-site scripting attempts.
|
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
|
desc: Automatically enter insert mode if an editable element is focused after
|
||||||
loading the page.
|
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:
|
input.insert_mode.auto_leave:
|
||||||
default: true
|
default: true
|
||||||
type: Bool
|
type: Bool
|
||||||
@ -978,6 +999,7 @@ input.insert_mode.plugins:
|
|||||||
input.links_included_in_focus_chain:
|
input.links_included_in_focus_chain:
|
||||||
default: true
|
default: true
|
||||||
type: Bool
|
type: Bool
|
||||||
|
supports_pattern: true
|
||||||
desc: Include hyperlinks in the keyboard focus chain when tabbing.
|
desc: Include hyperlinks in the keyboard focus chain when tabbing.
|
||||||
|
|
||||||
input.partial_timeout:
|
input.partial_timeout:
|
||||||
@ -1003,6 +1025,7 @@ input.rocker_gestures:
|
|||||||
input.spatial_navigation:
|
input.spatial_navigation:
|
||||||
default: false
|
default: false
|
||||||
type: Bool
|
type: Bool
|
||||||
|
supports_pattern: true
|
||||||
desc: >-
|
desc: >-
|
||||||
Enable spatial navigation.
|
Enable spatial navigation.
|
||||||
|
|
||||||
@ -1083,6 +1106,7 @@ scrolling.bar:
|
|||||||
scrolling.smooth:
|
scrolling.smooth:
|
||||||
type: Bool
|
type: Bool
|
||||||
default: false
|
default: false
|
||||||
|
supports_pattern: true
|
||||||
desc: >-
|
desc: >-
|
||||||
Enable smooth scrolling for web pages.
|
Enable smooth scrolling for web pages.
|
||||||
|
|
||||||
@ -1557,6 +1581,7 @@ zoom.text_only:
|
|||||||
type: Bool
|
type: Bool
|
||||||
default: false
|
default: false
|
||||||
backend: QtWebKit
|
backend: QtWebKit
|
||||||
|
supports_pattern: true
|
||||||
desc: Apply the zoom factor on a frame only to the text or to all content.
|
desc: Apply the zoom factor on a frame only to the text or to all content.
|
||||||
|
|
||||||
## colors
|
## colors
|
||||||
@ -2141,6 +2166,7 @@ bindings.key_mappings:
|
|||||||
<Ctrl-Enter>: <Ctrl-Return>
|
<Ctrl-Enter>: <Ctrl-Return>
|
||||||
type:
|
type:
|
||||||
name: Dict
|
name: Dict
|
||||||
|
none_ok: true
|
||||||
keytype: Key
|
keytype: Key
|
||||||
valtype: Key
|
valtype: Key
|
||||||
desc: >-
|
desc: >-
|
||||||
@ -2156,6 +2182,7 @@ bindings.key_mappings:
|
|||||||
`bindings.commands`), the mapping is ignored.
|
`bindings.commands`), the mapping is ignored.
|
||||||
|
|
||||||
bindings.default:
|
bindings.default:
|
||||||
|
no_autoconfig: true
|
||||||
default:
|
default:
|
||||||
normal:
|
normal:
|
||||||
<Escape>: clear-keychain ;; search ;; fullscreen --leave
|
<Escape>: clear-keychain ;; search ;; fullscreen --leave
|
||||||
@ -2309,6 +2336,18 @@ bindings.default:
|
|||||||
<Ctrl-p>: tab-pin
|
<Ctrl-p>: tab-pin
|
||||||
q: record-macro
|
q: record-macro
|
||||||
"@": run-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:
|
insert:
|
||||||
<Ctrl-E>: open-editor
|
<Ctrl-E>: open-editor
|
||||||
<Shift-Ins>: insert-text {primary}
|
<Shift-Ins>: insert-text {primary}
|
||||||
@ -2353,8 +2392,6 @@ bindings.default:
|
|||||||
<Escape>: leave-mode
|
<Escape>: leave-mode
|
||||||
prompt:
|
prompt:
|
||||||
<Return>: prompt-accept
|
<Return>: prompt-accept
|
||||||
y: prompt-accept yes
|
|
||||||
n: prompt-accept no
|
|
||||||
<Ctrl-X>: prompt-open-download
|
<Ctrl-X>: prompt-open-download
|
||||||
<Shift-Tab>: prompt-item-focus prev
|
<Shift-Tab>: prompt-item-focus prev
|
||||||
<Up>: prompt-item-focus prev
|
<Up>: prompt-item-focus prev
|
||||||
@ -2377,6 +2414,13 @@ bindings.default:
|
|||||||
<Ctrl-H>: rl-backward-delete-char
|
<Ctrl-H>: rl-backward-delete-char
|
||||||
<Ctrl-Y>: rl-yank
|
<Ctrl-Y>: rl-yank
|
||||||
<Escape>: leave-mode
|
<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:
|
caret:
|
||||||
v: toggle-selection
|
v: toggle-selection
|
||||||
<Space>: toggle-selection
|
<Space>: toggle-selection
|
||||||
@ -2412,7 +2456,7 @@ bindings.default:
|
|||||||
none_ok: true
|
none_ok: true
|
||||||
keytype: String # section name
|
keytype: String # section name
|
||||||
fixed_keys: ['normal', 'insert', 'hint', 'passthrough', 'command',
|
fixed_keys: ['normal', 'insert', 'hint', 'passthrough', 'command',
|
||||||
'prompt', 'caret', 'register']
|
'prompt', 'yesno', 'caret', 'register']
|
||||||
valtype:
|
valtype:
|
||||||
name: Dict
|
name: Dict
|
||||||
none_ok: true
|
none_ok: true
|
||||||
@ -2436,14 +2480,14 @@ bindings.commands:
|
|||||||
none_ok: true
|
none_ok: true
|
||||||
keytype: String # section name
|
keytype: String # section name
|
||||||
fixed_keys: ['normal', 'insert', 'hint', 'passthrough', 'command',
|
fixed_keys: ['normal', 'insert', 'hint', 'passthrough', 'command',
|
||||||
'prompt', 'caret', 'register']
|
'prompt', 'yesno', 'caret', 'register']
|
||||||
valtype:
|
valtype:
|
||||||
name: Dict
|
name: Dict
|
||||||
none_ok: true
|
none_ok: true
|
||||||
keytype: Key
|
keytype: Key
|
||||||
valtype:
|
valtype:
|
||||||
name: Command
|
name: Command
|
||||||
none_ok: true
|
none_ok: true # needed for :unbind
|
||||||
desc: >-
|
desc: >-
|
||||||
Keybindings mapping keys to commands in different modes.
|
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`
|
If you want to map a key to another key, check the `bindings.key_mappings`
|
||||||
setting instead.
|
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
|
For modifiers, you can use either `-` or `+` as delimiters, and these
|
||||||
names:
|
names:
|
||||||
|
|
||||||
@ -2508,10 +2551,8 @@ bindings.commands:
|
|||||||
|
|
||||||
* prompt: Entered when there's a prompt to display, like for download
|
* prompt: Entered when there's a prompt to display, like for download
|
||||||
locations or when invoked from JavaScript.
|
locations or when invoked from JavaScript.
|
||||||
+
|
|
||||||
You can bind normal keys in this mode, but they will be only active when
|
* yesno: Entered when there's a yes/no prompt displayed.
|
||||||
a yes/no-prompt is asked. For other prompt modes, you can only bind
|
|
||||||
special keys.
|
|
||||||
|
|
||||||
* caret: Entered when pressing the `v` mode, used to select text using the
|
* caret: Entered when pressing the `v` mode, used to select text using the
|
||||||
keyboard.
|
keyboard.
|
||||||
|
@ -31,6 +31,15 @@ class Error(Exception):
|
|||||||
pass
|
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):
|
class BackendError(Error):
|
||||||
|
|
||||||
"""Raised when this setting is unavailable with the current backend."""
|
"""Raised when this setting is unavailable with the current backend."""
|
||||||
@ -40,6 +49,15 @@ class BackendError(Error):
|
|||||||
"backend!".format(name, backend.name))
|
"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):
|
class ValidationError(Error):
|
||||||
|
|
||||||
"""Raised when a value for a config type was invalid.
|
"""Raised when a value for a config type was invalid.
|
||||||
@ -92,6 +110,10 @@ class ConfigErrorDesc:
|
|||||||
traceback = attr.ib(None)
|
traceback = attr.ib(None)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
|
if self.traceback:
|
||||||
|
return '{} - {}: {}'.format(self.text,
|
||||||
|
self.exception.__class__.__name__,
|
||||||
|
self.exception)
|
||||||
return '{}: {}'.format(self.text, self.exception)
|
return '{}: {}'.format(self.text, self.exception)
|
||||||
|
|
||||||
def with_text(self, text):
|
def with_text(self, text):
|
||||||
|
@ -32,8 +32,9 @@ import yaml
|
|||||||
from PyQt5.QtCore import pyqtSignal, QObject, QSettings
|
from PyQt5.QtCore import pyqtSignal, QObject, QSettings
|
||||||
|
|
||||||
import qutebrowser
|
import qutebrowser
|
||||||
from qutebrowser.config import configexc, config, configdata
|
from qutebrowser.config import configexc, config, configdata, configutils
|
||||||
from qutebrowser.utils import standarddir, utils, qtutils, log
|
from qutebrowser.keyinput import keyutils
|
||||||
|
from qutebrowser.utils import standarddir, utils, qtutils, log, urlmatch
|
||||||
|
|
||||||
|
|
||||||
# The StateConfig instance
|
# The StateConfig instance
|
||||||
@ -80,16 +81,19 @@ class YamlConfig(QObject):
|
|||||||
VERSION: The current version number of the config file.
|
VERSION: The current version number of the config file.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
VERSION = 1
|
VERSION = 2
|
||||||
changed = pyqtSignal()
|
changed = pyqtSignal()
|
||||||
|
|
||||||
def __init__(self, parent=None):
|
def __init__(self, parent=None):
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
self._filename = os.path.join(standarddir.config(auto=True),
|
self._filename = os.path.join(standarddir.config(auto=True),
|
||||||
'autoconfig.yml')
|
'autoconfig.yml')
|
||||||
self._values = {}
|
|
||||||
self._dirty = None
|
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):
|
def init_save_manager(self, save_manager):
|
||||||
"""Make sure the config gets saved properly.
|
"""Make sure the config gets saved properly.
|
||||||
|
|
||||||
@ -98,18 +102,9 @@ class YamlConfig(QObject):
|
|||||||
"""
|
"""
|
||||||
save_manager.add_saveable('yaml-config', self._save, self.changed)
|
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):
|
def __iter__(self):
|
||||||
return iter(sorted(self._values.items()))
|
"""Iterate over configutils.Values items."""
|
||||||
|
yield from self._values.values()
|
||||||
|
|
||||||
def _mark_changed(self):
|
def _mark_changed(self):
|
||||||
"""Mark the YAML config as changed."""
|
"""Mark the YAML config as changed."""
|
||||||
@ -121,7 +116,17 @@ class YamlConfig(QObject):
|
|||||||
if not self._dirty:
|
if not self._dirty:
|
||||||
return
|
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:
|
with qtutils.savefile_open(self._filename) as f:
|
||||||
f.write(textwrap.dedent("""
|
f.write(textwrap.dedent("""
|
||||||
# DO NOT edit this file by hand, qutebrowser will overwrite it.
|
# DO NOT edit this file by hand, qutebrowser will overwrite it.
|
||||||
@ -130,6 +135,29 @@ class YamlConfig(QObject):
|
|||||||
""".lstrip('\n')))
|
""".lstrip('\n')))
|
||||||
utils.yaml_dump(data, f)
|
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):
|
def load(self):
|
||||||
"""Load configuration from the configured YAML file."""
|
"""Load configuration from the configured YAML file."""
|
||||||
try:
|
try:
|
||||||
@ -144,76 +172,132 @@ class YamlConfig(QObject):
|
|||||||
desc = configexc.ConfigErrorDesc("While parsing", e)
|
desc = configexc.ConfigErrorDesc("While parsing", e)
|
||||||
raise configexc.ConfigFileErrors('autoconfig.yml', [desc])
|
raise configexc.ConfigFileErrors('autoconfig.yml', [desc])
|
||||||
|
|
||||||
try:
|
config_version = self._pop_object(yaml_data, 'config_version', int)
|
||||||
global_obj = yaml_data['global']
|
if config_version == 1:
|
||||||
except KeyError:
|
settings = self._load_legacy_settings_object(yaml_data)
|
||||||
|
self._mark_changed()
|
||||||
|
elif config_version > self.VERSION:
|
||||||
desc = configexc.ConfigErrorDesc(
|
desc = configexc.ConfigErrorDesc(
|
||||||
"While loading data",
|
"While reading",
|
||||||
"Toplevel object does not contain 'global' key")
|
"Can't read config from incompatible newer version")
|
||||||
raise configexc.ConfigFileErrors('autoconfig.yml', [desc])
|
raise configexc.ConfigFileErrors('autoconfig.yml', [desc])
|
||||||
except TypeError:
|
else:
|
||||||
desc = configexc.ConfigErrorDesc("While loading data",
|
settings = self._load_settings_object(yaml_data)
|
||||||
"Toplevel object is not a dict")
|
|
||||||
raise configexc.ConfigFileErrors('autoconfig.yml', [desc])
|
|
||||||
|
|
||||||
if not isinstance(global_obj, dict):
|
|
||||||
desc = configexc.ConfigErrorDesc(
|
|
||||||
"While loading data",
|
|
||||||
"'global' object is not a dict")
|
|
||||||
raise configexc.ConfigFileErrors('autoconfig.yml', [desc])
|
|
||||||
|
|
||||||
self._values = global_obj
|
|
||||||
self._dirty = False
|
self._dirty = False
|
||||||
|
|
||||||
self._handle_migrations()
|
settings = self._handle_migrations(settings)
|
||||||
self._validate()
|
self._validate(settings)
|
||||||
|
self._build_values(settings)
|
||||||
|
|
||||||
def _handle_migrations(self):
|
def _load_settings_object(self, yaml_data):
|
||||||
|
"""Load the settings from the settings: key."""
|
||||||
|
return self._pop_object(yaml_data, 'settings', dict)
|
||||||
|
|
||||||
|
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 _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."""
|
"""Migrate older configs to the newest format."""
|
||||||
# Simple renamed/deleted options
|
# Simple renamed/deleted options
|
||||||
for name in list(self._values):
|
for name in list(settings):
|
||||||
if name in configdata.MIGRATIONS.renamed:
|
if name in configdata.MIGRATIONS.renamed:
|
||||||
new_name = configdata.MIGRATIONS.renamed[name]
|
new_name = configdata.MIGRATIONS.renamed[name]
|
||||||
log.config.debug("Renaming {} to {}".format(name, new_name))
|
log.config.debug("Renaming {} to {}".format(name, new_name))
|
||||||
self._values[new_name] = self._values[name]
|
settings[new_name] = settings[name]
|
||||||
del self._values[name]
|
del settings[name]
|
||||||
self._mark_changed()
|
self._mark_changed()
|
||||||
elif name in configdata.MIGRATIONS.deleted:
|
elif name in configdata.MIGRATIONS.deleted:
|
||||||
log.config.debug("Removing {}".format(name))
|
log.config.debug("Removing {}".format(name))
|
||||||
del self._values[name]
|
del settings[name]
|
||||||
self._mark_changed()
|
self._mark_changed()
|
||||||
|
|
||||||
# tabs.persist_mode_on_change got merged into tabs.mode_on_change
|
# tabs.persist_mode_on_change got merged into tabs.mode_on_change
|
||||||
old = 'tabs.persist_mode_on_change'
|
old = 'tabs.persist_mode_on_change'
|
||||||
new = 'tabs.mode_on_change'
|
new = 'tabs.mode_on_change'
|
||||||
if old in self._values:
|
if old in settings:
|
||||||
if self._values[old]:
|
settings[new] = {}
|
||||||
self._values[new] = 'persist'
|
for scope, val in settings[old].items():
|
||||||
|
if val:
|
||||||
|
settings[new][scope] = 'persist'
|
||||||
else:
|
else:
|
||||||
self._values[new] = 'normal'
|
settings[new][scope] = 'normal'
|
||||||
del self._values[old]
|
|
||||||
|
del settings[old]
|
||||||
self._mark_changed()
|
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."""
|
"""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:
|
if unknown:
|
||||||
errors = [configexc.ConfigErrorDesc("While loading options",
|
errors = [configexc.ConfigErrorDesc("While loading options",
|
||||||
"Unknown option {}".format(e))
|
"Unknown option {}".format(e))
|
||||||
for e in sorted(unknown)]
|
for e in sorted(unknown)]
|
||||||
raise configexc.ConfigFileErrors('autoconfig.yml', errors)
|
raise configexc.ConfigFileErrors('autoconfig.yml', errors)
|
||||||
|
|
||||||
def unset(self, name):
|
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."""
|
"""Remove the given option name if it's configured."""
|
||||||
try:
|
changed = self._values[name].remove(pattern)
|
||||||
del self._values[name]
|
if changed:
|
||||||
except KeyError:
|
|
||||||
return
|
|
||||||
self._mark_changed()
|
self._mark_changed()
|
||||||
|
|
||||||
def clear(self):
|
def clear(self):
|
||||||
"""Clear all values from the YAML file."""
|
"""Clear all values from the YAML file."""
|
||||||
self._values = []
|
for values in self._values.values():
|
||||||
|
values.clear()
|
||||||
self._mark_changed()
|
self._mark_changed()
|
||||||
|
|
||||||
|
|
||||||
@ -242,6 +326,7 @@ class ConfigAPI:
|
|||||||
|
|
||||||
@contextlib.contextmanager
|
@contextlib.contextmanager
|
||||||
def _handle_error(self, action, name):
|
def _handle_error(self, action, name):
|
||||||
|
"""Catch config-related exceptions and save them in self.errors."""
|
||||||
try:
|
try:
|
||||||
yield
|
yield
|
||||||
except configexc.ConfigFileErrors as e:
|
except configexc.ConfigFileErrors as e:
|
||||||
@ -251,30 +336,45 @@ class ConfigAPI:
|
|||||||
except configexc.Error as e:
|
except configexc.Error as e:
|
||||||
text = "While {} '{}'".format(action, name)
|
text = "While {} '{}'".format(action, name)
|
||||||
self.errors.append(configexc.ConfigErrorDesc(text, e))
|
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):
|
def finalize(self):
|
||||||
"""Do work which needs to be done after reading config.py."""
|
"""Do work which needs to be done after reading config.py."""
|
||||||
self._config.update_mutables()
|
self._config.update_mutables()
|
||||||
|
|
||||||
def load_autoconfig(self):
|
def load_autoconfig(self):
|
||||||
|
"""Load the autoconfig.yml file which is used for :set/:bind/etc."""
|
||||||
with self._handle_error('reading', 'autoconfig.yml'):
|
with self._handle_error('reading', 'autoconfig.yml'):
|
||||||
read_autoconfig()
|
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):
|
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):
|
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'):
|
def bind(self, key, command, mode='normal'):
|
||||||
|
"""Bind a key to a command, with an optional key mode."""
|
||||||
with self._handle_error('binding', key):
|
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'):
|
def unbind(self, key, mode='normal'):
|
||||||
|
"""Unbind a key from a command, with an optional key mode."""
|
||||||
with self._handle_error('unbinding', key):
|
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):
|
def source(self, filename):
|
||||||
"""Read the given config file from disk."""
|
"""Read the given config file from disk."""
|
||||||
@ -286,6 +386,16 @@ class ConfigAPI:
|
|||||||
except configexc.ConfigFileErrors as e:
|
except configexc.ConfigFileErrors as e:
|
||||||
self.errors += e.errors
|
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:
|
class ConfigPyWriter:
|
||||||
|
|
||||||
@ -344,7 +454,7 @@ class ConfigPyWriter:
|
|||||||
|
|
||||||
def _gen_options(self):
|
def _gen_options(self):
|
||||||
"""Generate the options part of the config."""
|
"""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']:
|
if opt.name in ['bindings.commands', 'bindings.default']:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
@ -363,7 +473,11 @@ class ConfigPyWriter:
|
|||||||
except KeyError:
|
except KeyError:
|
||||||
yield self._line("# - {}".format(val))
|
yield self._line("# - {}".format(val))
|
||||||
|
|
||||||
|
if pattern is None:
|
||||||
yield self._line('c.{} = {!r}'.format(opt.name, value))
|
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 ''
|
yield ''
|
||||||
|
|
||||||
def _gen_bindings(self):
|
def _gen_bindings(self):
|
||||||
@ -419,7 +533,7 @@ def read_config_py(filename, raising=False):
|
|||||||
desc = configexc.ConfigErrorDesc("Error while compiling", e)
|
desc = configexc.ConfigErrorDesc("Error while compiling", e)
|
||||||
raise configexc.ConfigFileErrors(basename, [desc])
|
raise configexc.ConfigFileErrors(basename, [desc])
|
||||||
except SyntaxError as e:
|
except SyntaxError as e:
|
||||||
desc = configexc.ConfigErrorDesc("Syntax Error", e,
|
desc = configexc.ConfigErrorDesc("Unhandled exception", e,
|
||||||
traceback=traceback.format_exc())
|
traceback=traceback.format_exc())
|
||||||
raise configexc.ConfigFileErrors(basename, [desc])
|
raise configexc.ConfigFileErrors(basename, [desc])
|
||||||
|
|
||||||
|
@ -62,6 +62,7 @@ from PyQt5.QtWidgets import QTabWidget, QTabBar
|
|||||||
from qutebrowser.commands import cmdutils
|
from qutebrowser.commands import cmdutils
|
||||||
from qutebrowser.config import configexc
|
from qutebrowser.config import configexc
|
||||||
from qutebrowser.utils import standarddir, utils, qtutils, urlutils
|
from qutebrowser.utils import standarddir, utils, qtutils, urlutils
|
||||||
|
from qutebrowser.keyinput import keyutils
|
||||||
|
|
||||||
|
|
||||||
SYSTEM_PROXY = object() # Return value for Proxy type
|
SYSTEM_PROXY = object() # Return value for Proxy type
|
||||||
@ -450,7 +451,7 @@ class List(BaseType):
|
|||||||
def from_obj(self, value):
|
def from_obj(self, value):
|
||||||
if value is None:
|
if value is None:
|
||||||
return []
|
return []
|
||||||
return value
|
return [self.valtype.from_obj(v) for v in value]
|
||||||
|
|
||||||
def to_py(self, value):
|
def to_py(self, value):
|
||||||
self._basic_py_validation(value, list)
|
self._basic_py_validation(value, list)
|
||||||
@ -505,6 +506,16 @@ class ListOrValue(BaseType):
|
|||||||
self.listtype = List(valtype, none_ok=none_ok, *args, **kwargs)
|
self.listtype = List(valtype, none_ok=none_ok, *args, **kwargs)
|
||||||
self.valtype = valtype
|
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):
|
def get_name(self):
|
||||||
return self.listtype.get_name() + ', or ' + self.valtype.get_name()
|
return self.listtype.get_name() + ', or ' + self.valtype.get_name()
|
||||||
|
|
||||||
@ -532,25 +543,15 @@ class ListOrValue(BaseType):
|
|||||||
if value is None:
|
if value is None:
|
||||||
return ''
|
return ''
|
||||||
|
|
||||||
if isinstance(value, list):
|
val, typ = self._val_and_type(value)
|
||||||
if len(value) == 1:
|
return typ.to_str(val)
|
||||||
return self.valtype.to_str(value[0])
|
|
||||||
else:
|
|
||||||
return self.listtype.to_str(value)
|
|
||||||
else:
|
|
||||||
return self.valtype.to_str(value)
|
|
||||||
|
|
||||||
def to_doc(self, value, indent=0):
|
def to_doc(self, value, indent=0):
|
||||||
if value is None:
|
if value is None:
|
||||||
return 'empty'
|
return 'empty'
|
||||||
|
|
||||||
if isinstance(value, list):
|
val, typ = self._val_and_type(value)
|
||||||
if len(value) == 1:
|
return typ.to_doc(val)
|
||||||
return self.valtype.to_doc(value[0], indent)
|
|
||||||
else:
|
|
||||||
return self.listtype.to_doc(value, indent)
|
|
||||||
else:
|
|
||||||
return self.valtype.to_doc(value, indent)
|
|
||||||
|
|
||||||
|
|
||||||
class FlagList(List):
|
class FlagList(List):
|
||||||
@ -1198,7 +1199,9 @@ class Dict(BaseType):
|
|||||||
def from_obj(self, value):
|
def from_obj(self, value):
|
||||||
if value is None:
|
if value is None:
|
||||||
return {}
|
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):
|
def _fill_fixed_keys(self, value):
|
||||||
"""Fill missing fixed keys with a None-value."""
|
"""Fill missing fixed keys with a None-value."""
|
||||||
@ -1647,10 +1650,16 @@ class Key(BaseType):
|
|||||||
|
|
||||||
"""A name of a key."""
|
"""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):
|
def to_py(self, value):
|
||||||
self._basic_py_validation(value, str)
|
self._basic_py_validation(value, str)
|
||||||
if not value:
|
if not value:
|
||||||
return None
|
return None
|
||||||
if utils.is_special_key(value):
|
|
||||||
value = '<{}>'.format(utils.normalize_keystr(value[1:-1]))
|
try:
|
||||||
return value
|
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
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
|
# 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."""
|
"""Bridge from QWeb(Engine)Settings to our own settings."""
|
||||||
|
|
||||||
from PyQt5.QtGui import QFont
|
from PyQt5.QtGui import QFont
|
||||||
|
|
||||||
from qutebrowser.config import config
|
from qutebrowser.config import config, configutils
|
||||||
from qutebrowser.utils import log, utils, debug, usertypes
|
from qutebrowser.utils import log, usertypes, urlmatch, qtutils
|
||||||
from qutebrowser.misc import objects
|
from qutebrowser.misc import objects
|
||||||
|
|
||||||
UNSET = object()
|
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):
|
_ATTRIBUTES = None
|
||||||
self._default = default
|
_FONT_SIZES = None
|
||||||
|
_FONT_FAMILIES = None
|
||||||
|
_FONT_TO_QFONT = None
|
||||||
|
|
||||||
def _get_global_settings(self):
|
def __init__(self, settings):
|
||||||
"""Get a list of global QWeb(Engine)Settings to use."""
|
self._settings = settings
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
def _get_settings(self, settings):
|
def set_attribute(self, name, value):
|
||||||
"""Get a list of QWeb(Engine)Settings objects to use.
|
"""Set the given QWebSettings/QWebEngineSettings attribute.
|
||||||
|
|
||||||
Args:
|
If the value is configutils.UNSET, the value is reset instead.
|
||||||
settings: The QWeb(Engine)Settings instance to use, or None to use
|
|
||||||
the global instance.
|
|
||||||
|
|
||||||
Return:
|
Return:
|
||||||
A list of QWeb(Engine)Settings objects. The first one should be
|
True if there was a change, False otherwise.
|
||||||
used for reading.
|
|
||||||
"""
|
"""
|
||||||
if settings is None:
|
old_value = self.test_attribute(name)
|
||||||
return self._get_global_settings()
|
|
||||||
|
for attribute in self._ATTRIBUTES[name]:
|
||||||
|
if value is configutils.UNSET:
|
||||||
|
self._settings.resetAttribute(attribute)
|
||||||
|
new_value = self.test_attribute(name)
|
||||||
else:
|
else:
|
||||||
return [settings]
|
self._settings.setAttribute(attribute, value)
|
||||||
|
new_value = value
|
||||||
|
|
||||||
def set(self, value, settings=None):
|
return old_value != new_value
|
||||||
"""Set the value of this setting.
|
|
||||||
|
|
||||||
Args:
|
def test_attribute(self, name):
|
||||||
value: The value to set, or None to restore the default.
|
"""Get the value for the given attribute.
|
||||||
settings: The QWeb(Engine)Settings instance to use, or None to use
|
|
||||||
the global instance.
|
If the setting resolves to a list of attributes, only the first
|
||||||
|
attribute is tested.
|
||||||
"""
|
"""
|
||||||
|
return self._settings.testAttribute(self._ATTRIBUTES[name][0])
|
||||||
|
|
||||||
|
def set_font_size(self, name, value):
|
||||||
|
"""Set the given QWebSettings/QWebEngineSettings font size.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
With None (the default), QFont is used to get the default font for the
|
||||||
|
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:
|
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)
|
|
||||||
else:
|
|
||||||
args.append(value)
|
|
||||||
self._setter(*args)
|
|
||||||
|
|
||||||
|
|
||||||
class StaticSetter(Setter):
|
|
||||||
|
|
||||||
"""A setting set via a static QWeb(Engine)Settings method.
|
|
||||||
|
|
||||||
self._setter is the *bound* method.
|
|
||||||
"""
|
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
||||||
|
|
||||||
class FontFamilySetter(Setter):
|
|
||||||
|
|
||||||
"""A setter for a font family.
|
|
||||||
|
|
||||||
Gets the default value from QFont.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, setter, font, qfont):
|
|
||||||
super().__init__(setter=setter, args=[font])
|
|
||||||
self._qfont = qfont
|
|
||||||
|
|
||||||
def set_default(self, settings=None):
|
|
||||||
font = QFont()
|
font = QFont()
|
||||||
font.setStyleHint(self._qfont)
|
font.setStyleHint(self._FONT_TO_QFONT[family])
|
||||||
value = font.defaultFamily()
|
value = font.defaultFamily()
|
||||||
self._set(value, settings=settings)
|
|
||||||
|
|
||||||
|
old_value = self._settings.fontFamily(family)
|
||||||
|
self._settings.setFontFamily(family, value)
|
||||||
|
|
||||||
def init_mappings(mappings):
|
return value != old_value
|
||||||
"""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 set_default_text_encoding(self, encoding):
|
||||||
|
"""Set the default text encoding to use.
|
||||||
|
|
||||||
def update_mappings(mappings, option):
|
Return:
|
||||||
"""Update global settings when QWeb(Engine)Settings changed."""
|
True if there was a change, False otherwise.
|
||||||
try:
|
"""
|
||||||
mapping = mappings[option]
|
assert encoding is not configutils.UNSET
|
||||||
except KeyError:
|
old_value = self._settings.defaultTextEncoding()
|
||||||
return
|
self._settings.setDefaultTextEncoding(encoding)
|
||||||
value = config.instance.get(option)
|
return old_value != encoding
|
||||||
mapping.set(value)
|
|
||||||
|
def _update_setting(self, setting, value):
|
||||||
|
"""Update the given setting/value.
|
||||||
|
|
||||||
|
Unknown settings are ignored.
|
||||||
|
|
||||||
|
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):
|
def init(args):
|
||||||
@ -217,6 +173,11 @@ def init(args):
|
|||||||
from qutebrowser.browser.webkit import webkitsettings
|
from qutebrowser.browser.webkit import webkitsettings
|
||||||
webkitsettings.init(args)
|
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():
|
def shutdown():
|
||||||
"""Shut down QWeb(Engine)Settings."""
|
"""Shut down QWeb(Engine)Settings."""
|
||||||
|
@ -33,7 +33,7 @@ input { width: 98%; }
|
|||||||
<th>Setting</th>
|
<th>Setting</th>
|
||||||
<th>Value</th>
|
<th>Value</th>
|
||||||
</tr>
|
</tr>
|
||||||
{% for option in configdata.DATA.values() %}
|
{% for option in configdata.DATA.values() if not option.no_autoconfig %}
|
||||||
<tr>
|
<tr>
|
||||||
<!-- FIXME: convert to string properly -->
|
<!-- FIXME: convert to string properly -->
|
||||||
<td class="setting">{{ option.name }} (Current: {{ confget(option.name) | string |truncate(100) }})
|
<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;
|
const unsafeWindow = window;
|
||||||
|
|
||||||
// ====== The actual user script source ====== //
|
// ====== The actual user script source ====== //
|
||||||
|
@ -74,9 +74,8 @@ window._qutebrowser.webelem = (function() {
|
|||||||
try {
|
try {
|
||||||
return elem.selectionStart;
|
return elem.selectionStart;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (err instanceof (frame
|
if ((err instanceof DOMException ||
|
||||||
? frame.DOMException
|
(frame && err instanceof frame.DOMException)) &&
|
||||||
: DOMException) &&
|
|
||||||
err.name === "InvalidStateError") {
|
err.name === "InvalidStateError") {
|
||||||
// nothing to do, caret_position is already null
|
// nothing to do, caret_position is already null
|
||||||
} else {
|
} else {
|
||||||
@ -331,13 +330,13 @@ window._qutebrowser.webelem = (function() {
|
|||||||
|
|
||||||
// Function for returning a selection to python (so we can click it)
|
// Function for returning a selection to python (so we can click it)
|
||||||
funcs.find_selected_link = () => {
|
funcs.find_selected_link = () => {
|
||||||
const elem = window.getSelection().anchorNode;
|
const elem = window.getSelection().baseNode;
|
||||||
if (elem) {
|
if (elem) {
|
||||||
return serialize_elem(elem.parentNode);
|
return serialize_elem(elem.parentNode);
|
||||||
}
|
}
|
||||||
|
|
||||||
const serialized_frame_elem = run_frames((frame) => {
|
const serialized_frame_elem = run_frames((frame) => {
|
||||||
const node = frame.window.getSelection().anchorNode;
|
const node = frame.window.getSelection().baseNode;
|
||||||
if (node) {
|
if (node) {
|
||||||
return serialize_elem(node.parentNode, frame);
|
return serialize_elem(node.parentNode, frame);
|
||||||
}
|
}
|
||||||
|
@ -19,14 +19,12 @@
|
|||||||
|
|
||||||
"""Base class for vim-like key sequence parser."""
|
"""Base class for vim-like key sequence parser."""
|
||||||
|
|
||||||
import enum
|
|
||||||
import re
|
|
||||||
import unicodedata
|
|
||||||
|
|
||||||
from PyQt5.QtCore import pyqtSignal, QObject
|
from PyQt5.QtCore import pyqtSignal, QObject
|
||||||
|
from PyQt5.QtGui import QKeySequence
|
||||||
|
|
||||||
from qutebrowser.config import config
|
from qutebrowser.config import config
|
||||||
from qutebrowser.utils import usertypes, log, utils
|
from qutebrowser.utils import usertypes, log, utils
|
||||||
|
from qutebrowser.keyinput import keyutils
|
||||||
|
|
||||||
|
|
||||||
class BaseKeyParser(QObject):
|
class BaseKeyParser(QObject):
|
||||||
@ -43,24 +41,16 @@ class BaseKeyParser(QObject):
|
|||||||
definitive: Keychain matches exactly.
|
definitive: Keychain matches exactly.
|
||||||
none: No more matches possible.
|
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.
|
do_log: Whether to log keypresses or not.
|
||||||
passthrough: Whether unbound keys should be passed through with this
|
passthrough: Whether unbound keys should be passed through with this
|
||||||
handler.
|
handler.
|
||||||
|
|
||||||
Attributes:
|
Attributes:
|
||||||
bindings: Bound key bindings
|
bindings: Bound key bindings
|
||||||
special_bindings: Bound special bindings (<Foo>).
|
|
||||||
_win_id: The window ID this keyparser is associated with.
|
_win_id: The window ID this keyparser is associated with.
|
||||||
_warn_on_keychains: Whether a warning should be logged when binding
|
_sequence: The currently entered key sequence
|
||||||
keychains in a section which does not support them.
|
|
||||||
_keystring: The currently entered key sequence
|
|
||||||
_modename: The name of the input mode associated with this keyparser.
|
_modename: The name of the input mode associated with this keyparser.
|
||||||
_supports_count: Whether count is supported
|
_supports_count: Whether count is supported
|
||||||
_supports_chains: Whether keychains are supported
|
|
||||||
|
|
||||||
Signals:
|
Signals:
|
||||||
keystring_updated: Emitted when the keystring is updated.
|
keystring_updated: Emitted when the keystring is updated.
|
||||||
@ -76,27 +66,18 @@ class BaseKeyParser(QObject):
|
|||||||
do_log = True
|
do_log = True
|
||||||
passthrough = False
|
passthrough = False
|
||||||
|
|
||||||
Match = enum.Enum('Match', ['partial', 'definitive', 'other', 'none'])
|
def __init__(self, win_id, parent=None, supports_count=True):
|
||||||
Type = enum.Enum('Type', ['chain', 'special'])
|
|
||||||
|
|
||||||
def __init__(self, win_id, parent=None, supports_count=None,
|
|
||||||
supports_chains=False):
|
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
self._win_id = win_id
|
self._win_id = win_id
|
||||||
self._modename = None
|
self._modename = None
|
||||||
self._keystring = ''
|
self._sequence = keyutils.KeySequence()
|
||||||
if supports_count is None:
|
self._count = ''
|
||||||
supports_count = supports_chains
|
|
||||||
self._supports_count = supports_count
|
self._supports_count = supports_count
|
||||||
self._supports_chains = supports_chains
|
|
||||||
self._warn_on_keychains = True
|
|
||||||
self.bindings = {}
|
self.bindings = {}
|
||||||
self.special_bindings = {}
|
|
||||||
config.instance.changed.connect(self._on_config_changed)
|
config.instance.changed.connect(self._on_config_changed)
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return utils.get_repr(self, supports_count=self._supports_count,
|
return utils.get_repr(self, supports_count=self._supports_count)
|
||||||
supports_chains=self._supports_chains)
|
|
||||||
|
|
||||||
def _debug_log(self, message):
|
def _debug_log(self, message):
|
||||||
"""Log a message to the debug log if logging is active.
|
"""Log a message to the debug log if logging is active.
|
||||||
@ -107,121 +88,11 @@ class BaseKeyParser(QObject):
|
|||||||
if self.do_log:
|
if self.do_log:
|
||||||
log.keyboard.debug(message)
|
log.keyboard.debug(message)
|
||||||
|
|
||||||
def _handle_special_key(self, e):
|
def _match_key(self, sequence):
|
||||||
"""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):
|
|
||||||
"""Try to match a given keystring with any bound keychain.
|
"""Try to match a given keystring with any bound keychain.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
cmd_input: The command string to find.
|
sequence: The command string to find.
|
||||||
|
|
||||||
Return:
|
Return:
|
||||||
A tuple (matchtype, binding).
|
A tuple (matchtype, binding).
|
||||||
@ -229,50 +100,117 @@ class BaseKeyParser(QObject):
|
|||||||
binding: - None with Match.partial/Match.none.
|
binding: - None with Match.partial/Match.none.
|
||||||
- The found binding with Match.definitive.
|
- The found binding with Match.definitive.
|
||||||
"""
|
"""
|
||||||
if not cmd_input:
|
assert sequence
|
||||||
# Only a count, no command yet, but we handled it
|
assert not isinstance(sequence, str)
|
||||||
return (self.Match.other, None)
|
result = QKeySequence.NoMatch
|
||||||
# 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)
|
|
||||||
|
|
||||||
def handle(self, e):
|
for seq, cmd in self.bindings.items():
|
||||||
"""Handle a new keypress and call the respective handlers.
|
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:
|
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:
|
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:
|
if keyutils.is_modifier_key(key):
|
||||||
return handled
|
self._debug_log("Ignoring, only modifier")
|
||||||
match = self._handle_single_key(e)
|
return QKeySequence.NoMatch
|
||||||
# don't emit twice if the keystring was cleared in self.clear_keystring
|
|
||||||
if self._keystring:
|
try:
|
||||||
self.keystring_updated.emit(self._keystring)
|
sequence = self._sequence.append_event(e)
|
||||||
return match != self.Match.none
|
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')
|
@config.change_filter('bindings')
|
||||||
def _on_config_changed(self):
|
def _on_config_changed(self):
|
||||||
@ -295,37 +233,26 @@ class BaseKeyParser(QObject):
|
|||||||
else:
|
else:
|
||||||
self._modename = modename
|
self._modename = modename
|
||||||
self.bindings = {}
|
self.bindings = {}
|
||||||
self.special_bindings = {}
|
|
||||||
|
|
||||||
for key, cmd in config.key_instance.get_bindings_for(modename).items():
|
for key, cmd in config.key_instance.get_bindings_for(modename).items():
|
||||||
|
assert not isinstance(key, str), key
|
||||||
assert cmd
|
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
|
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.
|
"""Handle a completed keychain.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
cmdstr: The command to execute as a string.
|
cmdstr: The command to execute as a string.
|
||||||
keytype: Type.chain or Type.special
|
|
||||||
count: The count if given.
|
count: The count if given.
|
||||||
"""
|
"""
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
def clear_keystring(self):
|
def clear_keystring(self):
|
||||||
"""Clear the currently entered key sequence."""
|
"""Clear the currently entered key sequence."""
|
||||||
if self._keystring:
|
if self._sequence:
|
||||||
self._debug_log("discarding keystring '{}'.".format(
|
self._debug_log("Clearing keystring (was: {}).".format(
|
||||||
self._keystring))
|
self._sequence))
|
||||||
self._keystring = ''
|
self._sequence = keyutils.KeySequence()
|
||||||
self.keystring_updated.emit(self._keystring)
|
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.QtCore import pyqtSlot, pyqtSignal, Qt, QObject, QEvent
|
||||||
from PyQt5.QtWidgets import QApplication
|
from PyQt5.QtWidgets import QApplication
|
||||||
|
|
||||||
from qutebrowser.keyinput import modeparsers, keyparser
|
from qutebrowser.keyinput import modeparsers
|
||||||
from qutebrowser.config import config
|
from qutebrowser.config import config
|
||||||
from qutebrowser.commands import cmdexc, cmdutils
|
from qutebrowser.commands import cmdexc, cmdutils
|
||||||
from qutebrowser.utils import usertypes, log, objreg, utils
|
from qutebrowser.utils import usertypes, log, objreg, utils
|
||||||
@ -68,24 +68,30 @@ def init(win_id, parent):
|
|||||||
modeman = ModeManager(win_id, parent)
|
modeman = ModeManager(win_id, parent)
|
||||||
objreg.register('mode-manager', modeman, scope='window', window=win_id)
|
objreg.register('mode-manager', modeman, scope='window', window=win_id)
|
||||||
keyparsers = {
|
keyparsers = {
|
||||||
KM.normal: modeparsers.NormalKeyParser(win_id, modeman),
|
KM.normal:
|
||||||
KM.hint: modeparsers.HintKeyParser(win_id, modeman),
|
modeparsers.NormalKeyParser(win_id, modeman),
|
||||||
KM.insert: keyparser.PassthroughKeyParser(win_id, 'insert', modeman),
|
KM.hint:
|
||||||
KM.passthrough: keyparser.PassthroughKeyParser(win_id, 'passthrough',
|
modeparsers.HintKeyParser(win_id, modeman),
|
||||||
modeman),
|
KM.insert:
|
||||||
KM.command: keyparser.PassthroughKeyParser(win_id, 'command', modeman),
|
modeparsers.PassthroughKeyParser(win_id, 'insert', modeman),
|
||||||
KM.prompt: keyparser.PassthroughKeyParser(win_id, 'prompt', modeman,
|
KM.passthrough:
|
||||||
warn=False),
|
modeparsers.PassthroughKeyParser(win_id, 'passthrough', modeman),
|
||||||
KM.yesno: modeparsers.PromptKeyParser(win_id, modeman),
|
KM.command:
|
||||||
KM.caret: modeparsers.CaretKeyParser(win_id, modeman),
|
modeparsers.PassthroughKeyParser(win_id, 'command', modeman),
|
||||||
KM.set_mark: modeparsers.RegisterKeyParser(win_id, KM.set_mark,
|
KM.prompt:
|
||||||
modeman),
|
modeparsers.PassthroughKeyParser(win_id, 'prompt', modeman),
|
||||||
KM.jump_mark: modeparsers.RegisterKeyParser(win_id, KM.jump_mark,
|
KM.yesno:
|
||||||
modeman),
|
modeparsers.PromptKeyParser(win_id, modeman),
|
||||||
KM.record_macro: modeparsers.RegisterKeyParser(win_id, KM.record_macro,
|
KM.caret:
|
||||||
modeman),
|
modeparsers.CaretKeyParser(win_id, modeman),
|
||||||
KM.run_macro: modeparsers.RegisterKeyParser(win_id, KM.run_macro,
|
KM.set_mark:
|
||||||
modeman),
|
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)
|
objreg.register('keyparsers', keyparsers, scope='window', window=win_id)
|
||||||
modeman.destroyed.connect(
|
modeman.destroyed.connect(
|
||||||
@ -149,11 +155,12 @@ class ModeManager(QObject):
|
|||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return utils.get_repr(self, mode=self.mode)
|
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.
|
"""Handle filtering of KeyPress events.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
event: The KeyPress to examine.
|
event: The KeyPress to examine.
|
||||||
|
dry_run: Don't actually handle the key, only filter it.
|
||||||
|
|
||||||
Return:
|
Return:
|
||||||
True if event should be filtered, False otherwise.
|
True if event should be filtered, False otherwise.
|
||||||
@ -163,7 +170,7 @@ class ModeManager(QObject):
|
|||||||
if curmode != usertypes.KeyMode.insert:
|
if curmode != usertypes.KeyMode.insert:
|
||||||
log.modes.debug("got keypress in mode {} - delegating to "
|
log.modes.debug("got keypress in mode {} - delegating to "
|
||||||
"{}".format(curmode, utils.qualname(parser)))
|
"{}".format(curmode, utils.qualname(parser)))
|
||||||
handled = parser.handle(event)
|
match = parser.handle(event, dry_run=dry_run)
|
||||||
|
|
||||||
is_non_alnum = (
|
is_non_alnum = (
|
||||||
event.modifiers() not in [Qt.NoModifier, Qt.ShiftModifier] or
|
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
|
forward_unbound_keys = config.val.input.forward_unbound_keys
|
||||||
|
|
||||||
if handled:
|
if match:
|
||||||
filter_this = True
|
filter_this = True
|
||||||
elif (parser.passthrough or forward_unbound_keys == 'all' or
|
elif (parser.passthrough or forward_unbound_keys == 'all' or
|
||||||
(forward_unbound_keys == 'auto' and is_non_alnum)):
|
(forward_unbound_keys == 'auto' and is_non_alnum)):
|
||||||
@ -179,20 +186,20 @@ class ModeManager(QObject):
|
|||||||
else:
|
else:
|
||||||
filter_this = True
|
filter_this = True
|
||||||
|
|
||||||
if not filter_this:
|
if not filter_this and not dry_run:
|
||||||
self._releaseevents_to_pass.add(KeyEvent.from_event(event))
|
self._releaseevents_to_pass.add(KeyEvent.from_event(event))
|
||||||
|
|
||||||
if curmode != usertypes.KeyMode.insert:
|
if curmode != usertypes.KeyMode.insert:
|
||||||
focus_widget = QApplication.instance().focusWidget()
|
focus_widget = QApplication.instance().focusWidget()
|
||||||
log.modes.debug("handled: {}, forward_unbound_keys: {}, "
|
log.modes.debug("match: {}, forward_unbound_keys: {}, "
|
||||||
"passthrough: {}, is_non_alnum: {} --> "
|
"passthrough: {}, is_non_alnum: {}, dry_run: {} "
|
||||||
"filter: {} (focused: {!r})".format(
|
"--> filter: {} (focused: {!r})".format(
|
||||||
handled, forward_unbound_keys,
|
match, forward_unbound_keys,
|
||||||
parser.passthrough, is_non_alnum, filter_this,
|
parser.passthrough, is_non_alnum, dry_run,
|
||||||
focus_widget))
|
filter_this, focus_widget))
|
||||||
return filter_this
|
return filter_this
|
||||||
|
|
||||||
def _eventFilter_keyrelease(self, event):
|
def _handle_keyrelease(self, event):
|
||||||
"""Handle filtering of KeyRelease events.
|
"""Handle filtering of KeyRelease events.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@ -315,7 +322,7 @@ class ModeManager(QObject):
|
|||||||
raise ValueError("Can't leave normal mode!")
|
raise ValueError("Can't leave normal mode!")
|
||||||
self.leave(self.mode, 'leave current')
|
self.leave(self.mode, 'leave current')
|
||||||
|
|
||||||
def eventFilter(self, event):
|
def handle_event(self, event):
|
||||||
"""Filter all events based on the currently set mode.
|
"""Filter all events based on the currently set mode.
|
||||||
|
|
||||||
Also calls the real keypress handler.
|
Also calls the real keypress handler.
|
||||||
@ -331,8 +338,10 @@ class ModeManager(QObject):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
handlers = {
|
handlers = {
|
||||||
QEvent.KeyPress: self._eventFilter_keypress,
|
QEvent.KeyPress: self._handle_keypress,
|
||||||
QEvent.KeyRelease: self._eventFilter_keyrelease,
|
QEvent.KeyRelease: self._handle_keyrelease,
|
||||||
|
QEvent.ShortcutOverride:
|
||||||
|
functools.partial(self._handle_keypress, dry_run=True),
|
||||||
}
|
}
|
||||||
handler = handlers[event.type()]
|
handler = handlers[event.type()]
|
||||||
return handler(event)
|
return handler(event)
|
||||||
|
@ -27,10 +27,11 @@ import traceback
|
|||||||
import enum
|
import enum
|
||||||
|
|
||||||
from PyQt5.QtCore import pyqtSlot, Qt
|
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.config import config
|
||||||
from qutebrowser.keyinput import keyparser
|
from qutebrowser.keyinput import basekeyparser, keyutils
|
||||||
from qutebrowser.utils import usertypes, log, message, objreg, utils
|
from qutebrowser.utils import usertypes, log, message, objreg, utils
|
||||||
|
|
||||||
|
|
||||||
@ -38,7 +39,26 @@ STARTCHARS = ":/?"
|
|||||||
LastPress = enum.Enum('LastPress', ['none', 'filtertext', 'keystring'])
|
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.
|
"""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):
|
def __init__(self, win_id, parent=None):
|
||||||
super().__init__(win_id, parent, supports_count=True,
|
super().__init__(win_id, parent, supports_count=True)
|
||||||
supports_chains=True)
|
|
||||||
self._read_config('normal')
|
self._read_config('normal')
|
||||||
self._partial_timer = usertypes.Timer(self, 'partial-match')
|
self._partial_timer = usertypes.Timer(self, 'partial-match')
|
||||||
self._partial_timer.setSingleShot(True)
|
self._partial_timer.setSingleShot(True)
|
||||||
@ -59,11 +78,13 @@ class NormalKeyParser(keyparser.CommandKeyParser):
|
|||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return utils.get_repr(self)
|
return utils.get_repr(self)
|
||||||
|
|
||||||
def _handle_single_key(self, e):
|
def handle(self, e, *, dry_run=False):
|
||||||
"""Override _handle_single_key to abort if the key is a startchar.
|
"""Override to abort if the key is a startchar.
|
||||||
|
|
||||||
Args:
|
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:
|
Return:
|
||||||
A self.Match member.
|
A self.Match member.
|
||||||
@ -72,9 +93,11 @@ class NormalKeyParser(keyparser.CommandKeyParser):
|
|||||||
if self._inhibited:
|
if self._inhibited:
|
||||||
self._debug_log("Ignoring key '{}', because the normal mode is "
|
self._debug_log("Ignoring key '{}', because the normal mode is "
|
||||||
"currently inhibited.".format(txt))
|
"currently inhibited.".format(txt))
|
||||||
return self.Match.none
|
return QKeySequence.NoMatch
|
||||||
match = super()._handle_single_key(e)
|
|
||||||
if match == self.Match.partial:
|
match = super().handle(e, dry_run=dry_run)
|
||||||
|
|
||||||
|
if match == QKeySequence.PartialMatch and not dry_run:
|
||||||
timeout = config.val.input.partial_timeout
|
timeout = config.val.input.partial_timeout
|
||||||
if timeout != 0:
|
if timeout != 0:
|
||||||
self._partial_timer.setInterval(timeout)
|
self._partial_timer.setInterval(timeout)
|
||||||
@ -96,9 +119,9 @@ class NormalKeyParser(keyparser.CommandKeyParser):
|
|||||||
def _clear_partial_match(self):
|
def _clear_partial_match(self):
|
||||||
"""Clear a partial keystring after a timeout."""
|
"""Clear a partial keystring after a timeout."""
|
||||||
self._debug_log("Clearing partial keystring {}".format(
|
self._debug_log("Clearing partial keystring {}".format(
|
||||||
self._keystring))
|
self._sequence))
|
||||||
self._keystring = ''
|
self._sequence = keyutils.KeySequence()
|
||||||
self.keystring_updated.emit(self._keystring)
|
self.keystring_updated.emit(str(self._sequence))
|
||||||
|
|
||||||
@pyqtSlot()
|
@pyqtSlot()
|
||||||
def _clear_inhibited(self):
|
def _clear_inhibited(self):
|
||||||
@ -123,22 +146,48 @@ class NormalKeyParser(keyparser.CommandKeyParser):
|
|||||||
pass
|
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."""
|
"""KeyParser for yes/no prompts."""
|
||||||
|
|
||||||
def __init__(self, win_id, parent=None):
|
def __init__(self, win_id, parent=None):
|
||||||
super().__init__(win_id, parent, supports_count=False,
|
super().__init__(win_id, parent, supports_count=False)
|
||||||
supports_chains=True)
|
self._read_config('yesno')
|
||||||
# We don't want an extra section for this in the config, so we just
|
|
||||||
# abuse the prompt section.
|
|
||||||
self._read_config('prompt')
|
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return utils.get_repr(self)
|
return utils.get_repr(self)
|
||||||
|
|
||||||
|
|
||||||
class HintKeyParser(keyparser.CommandKeyParser):
|
class HintKeyParser(CommandKeyParser):
|
||||||
|
|
||||||
"""KeyChainParser for hints.
|
"""KeyChainParser for hints.
|
||||||
|
|
||||||
@ -148,15 +197,14 @@ class HintKeyParser(keyparser.CommandKeyParser):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, win_id, parent=None):
|
def __init__(self, win_id, parent=None):
|
||||||
super().__init__(win_id, parent, supports_count=False,
|
super().__init__(win_id, parent, supports_count=False)
|
||||||
supports_chains=True)
|
|
||||||
self._filtertext = ''
|
self._filtertext = ''
|
||||||
self._last_press = LastPress.none
|
self._last_press = LastPress.none
|
||||||
self._read_config('hint')
|
self._read_config('hint')
|
||||||
self.keystring_updated.connect(self.on_keystring_updated)
|
self.keystring_updated.connect(self.on_keystring_updated)
|
||||||
|
|
||||||
def _handle_special_key(self, e):
|
def _handle_filter_key(self, e):
|
||||||
"""Override _handle_special_key to handle string filtering.
|
"""Handle keys for string filtering.
|
||||||
|
|
||||||
Return True if the keypress has been handled, and False if not.
|
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.
|
e: the KeyPressEvent from Qt.
|
||||||
|
|
||||||
Return:
|
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()))
|
e.key(), e.text()))
|
||||||
hintmanager = objreg.get('hintmanager', scope='tab',
|
hintmanager = objreg.get('hintmanager', scope='tab',
|
||||||
window=self._win_id, tab='current')
|
window=self._win_id, tab='current')
|
||||||
if e.key() == Qt.Key_Backspace:
|
if e.key() == Qt.Key_Backspace:
|
||||||
log.keyboard.debug("Got backspace, mode {}, filtertext '{}', "
|
log.keyboard.debug("Got backspace, mode {}, filtertext '{}', "
|
||||||
"keystring '{}'".format(self._last_press,
|
"sequence '{}'".format(self._last_press,
|
||||||
self._filtertext,
|
self._filtertext,
|
||||||
self._keystring))
|
self._sequence))
|
||||||
if self._last_press == LastPress.filtertext and self._filtertext:
|
if self._last_press == LastPress.filtertext and self._filtertext:
|
||||||
self._filtertext = self._filtertext[:-1]
|
self._filtertext = self._filtertext[:-1]
|
||||||
hintmanager.filter_hints(self._filtertext)
|
hintmanager.filter_hints(self._filtertext)
|
||||||
return True
|
return QKeySequence.ExactMatch
|
||||||
elif self._last_press == LastPress.keystring and self._keystring:
|
elif self._last_press == LastPress.keystring and self._sequence:
|
||||||
self._keystring = self._keystring[:-1]
|
self._sequence = self._sequence[:-1]
|
||||||
self.keystring_updated.emit(self._keystring)
|
self.keystring_updated.emit(str(self._sequence))
|
||||||
if not self._keystring and self._filtertext:
|
if not self._sequence and self._filtertext:
|
||||||
# Switch back to hint filtering mode (this can happen only
|
# Switch back to hint filtering mode (this can happen only
|
||||||
# in numeric mode after the number has been deleted).
|
# in numeric mode after the number has been deleted).
|
||||||
hintmanager.filter_hints(self._filtertext)
|
hintmanager.filter_hints(self._filtertext)
|
||||||
self._last_press = LastPress.filtertext
|
self._last_press = LastPress.filtertext
|
||||||
return True
|
return QKeySequence.ExactMatch
|
||||||
else:
|
else:
|
||||||
return super()._handle_special_key(e)
|
return QKeySequence.NoMatch
|
||||||
elif hintmanager.current_mode() != 'number':
|
elif hintmanager.current_mode() != 'number':
|
||||||
return super()._handle_special_key(e)
|
return QKeySequence.NoMatch
|
||||||
elif not e.text():
|
elif not e.text():
|
||||||
return super()._handle_special_key(e)
|
return QKeySequence.NoMatch
|
||||||
else:
|
else:
|
||||||
self._filtertext += e.text()
|
self._filtertext += e.text()
|
||||||
hintmanager.filter_hints(self._filtertext)
|
hintmanager.filter_hints(self._filtertext)
|
||||||
self._last_press = LastPress.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.
|
"""Handle a new keypress and call the respective handlers.
|
||||||
|
|
||||||
Args:
|
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.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
True if the match has been handled, False otherwise.
|
True if the match has been handled, False otherwise.
|
||||||
"""
|
"""
|
||||||
match = self._handle_single_key(e)
|
dry_run_match = super().handle(e, dry_run=True)
|
||||||
if match == self.Match.partial:
|
if dry_run:
|
||||||
self.keystring_updated.emit(self._keystring)
|
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
|
self._last_press = LastPress.keystring
|
||||||
return True
|
elif match == QKeySequence.ExactMatch:
|
||||||
elif match == self.Match.definitive:
|
|
||||||
self._last_press = LastPress.none
|
self._last_press = LastPress.none
|
||||||
return True
|
elif match == QKeySequence.NoMatch:
|
||||||
elif match == self.Match.other:
|
|
||||||
return None
|
|
||||||
elif match == self.Match.none:
|
|
||||||
# We couldn't find a keychain so we check if it's a special key.
|
# 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:
|
else:
|
||||||
raise ValueError("Got invalid match type {}!".format(match))
|
raise ValueError("Got invalid match type {}!".format(match))
|
||||||
|
|
||||||
def execute(self, cmdstr, keytype, count=None):
|
return match
|
||||||
"""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)
|
|
||||||
|
|
||||||
def update_bindings(self, strings, preserve_filter=False):
|
def update_bindings(self, strings, preserve_filter=False):
|
||||||
"""Update bindings when the hint strings changed.
|
"""Update bindings when the hint strings changed.
|
||||||
@ -245,7 +290,9 @@ class HintKeyParser(keyparser.CommandKeyParser):
|
|||||||
preserve_filter: Whether to keep the current value of
|
preserve_filter: Whether to keep the current value of
|
||||||
`self._filtertext`.
|
`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:
|
if not preserve_filter:
|
||||||
self._filtertext = ''
|
self._filtertext = ''
|
||||||
|
|
||||||
@ -257,19 +304,18 @@ class HintKeyParser(keyparser.CommandKeyParser):
|
|||||||
hintmanager.handle_partial_key(keystr)
|
hintmanager.handle_partial_key(keystr)
|
||||||
|
|
||||||
|
|
||||||
class CaretKeyParser(keyparser.CommandKeyParser):
|
class CaretKeyParser(CommandKeyParser):
|
||||||
|
|
||||||
"""KeyParser for caret mode."""
|
"""KeyParser for caret mode."""
|
||||||
|
|
||||||
passthrough = True
|
passthrough = True
|
||||||
|
|
||||||
def __init__(self, win_id, parent=None):
|
def __init__(self, win_id, parent=None):
|
||||||
super().__init__(win_id, parent, supports_count=True,
|
super().__init__(win_id, parent, supports_count=True)
|
||||||
supports_chains=True)
|
|
||||||
self._read_config('caret')
|
self._read_config('caret')
|
||||||
|
|
||||||
|
|
||||||
class RegisterKeyParser(keyparser.CommandKeyParser):
|
class RegisterKeyParser(CommandKeyParser):
|
||||||
|
|
||||||
"""KeyParser for modes that record a register key.
|
"""KeyParser for modes that record a register key.
|
||||||
|
|
||||||
@ -279,29 +325,31 @@ class RegisterKeyParser(keyparser.CommandKeyParser):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, win_id, mode, parent=None):
|
def __init__(self, win_id, mode, parent=None):
|
||||||
super().__init__(win_id, parent, supports_count=False,
|
super().__init__(win_id, parent, supports_count=False)
|
||||||
supports_chains=False)
|
|
||||||
self._mode = mode
|
self._mode = mode
|
||||||
self._read_config('register')
|
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.
|
"""Override handle to always match the next key and use the register.
|
||||||
|
|
||||||
Args:
|
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:
|
Return:
|
||||||
True if event has been handled, False otherwise.
|
True if event has been handled, False otherwise.
|
||||||
"""
|
"""
|
||||||
if super().handle(e):
|
match = super().handle(e, dry_run=dry_run)
|
||||||
return True
|
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()
|
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',
|
tabbed_browser = objreg.get('tabbed-browser', scope='window',
|
||||||
window=self._win_id)
|
window=self._win_id)
|
||||||
macro_recorder = objreg.get('macro-recorder')
|
macro_recorder = objreg.get('macro-recorder')
|
||||||
@ -322,5 +370,4 @@ class RegisterKeyParser(keyparser.CommandKeyParser):
|
|||||||
message.error(str(err), stack=traceback.format_exc())
|
message.error(str(err), stack=traceback.format_exc())
|
||||||
|
|
||||||
self.request_leave.emit(self._mode, "valid register key", True)
|
self.request_leave.emit(self._mode, "valid register key", True)
|
||||||
|
return QKeySequence.ExactMatch
|
||||||
return True
|
|
||||||
|
@ -327,7 +327,7 @@ class MainWindow(QWidget):
|
|||||||
self.tabbed_browser)
|
self.tabbed_browser)
|
||||||
objreg.register('command-dispatcher', dispatcher, scope='window',
|
objreg.register('command-dispatcher', dispatcher, scope='window',
|
||||||
window=self.win_id)
|
window=self.win_id)
|
||||||
self.tabbed_browser.destroyed.connect(
|
self.tabbed_browser.widget.destroyed.connect(
|
||||||
functools.partial(objreg.delete, 'command-dispatcher',
|
functools.partial(objreg.delete, 'command-dispatcher',
|
||||||
scope='window', window=self.win_id))
|
scope='window', window=self.win_id))
|
||||||
|
|
||||||
@ -347,10 +347,10 @@ class MainWindow(QWidget):
|
|||||||
|
|
||||||
def _add_widgets(self):
|
def _add_widgets(self):
|
||||||
"""Add or readd all widgets to the VBox."""
|
"""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._downloadview)
|
||||||
self._vbox.removeWidget(self.status)
|
self._vbox.removeWidget(self.status)
|
||||||
widgets = [self.tabbed_browser]
|
widgets = [self.tabbed_browser.widget]
|
||||||
|
|
||||||
downloads_position = config.val.downloads.position
|
downloads_position = config.val.downloads.position
|
||||||
if downloads_position == 'top':
|
if downloads_position == 'top':
|
||||||
@ -469,7 +469,7 @@ class MainWindow(QWidget):
|
|||||||
|
|
||||||
self.tabbed_browser.cur_scroll_perc_changed.connect(
|
self.tabbed_browser.cur_scroll_perc_changed.connect(
|
||||||
status.percentage.set_perc)
|
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)
|
status.tabindex.on_tab_index_changed)
|
||||||
|
|
||||||
self.tabbed_browser.cur_url_changed.connect(status.url.set_url)
|
self.tabbed_browser.cur_url_changed.connect(status.url.set_url)
|
||||||
@ -518,7 +518,7 @@ class MainWindow(QWidget):
|
|||||||
super().resizeEvent(e)
|
super().resizeEvent(e)
|
||||||
self._update_overlay_geometries()
|
self._update_overlay_geometries()
|
||||||
self._downloadview.updateGeometry()
|
self._downloadview.updateGeometry()
|
||||||
self.tabbed_browser.tabBar().refresh()
|
self.tabbed_browser.widget.tabBar().refresh()
|
||||||
|
|
||||||
def showEvent(self, e):
|
def showEvent(self, e):
|
||||||
"""Extend showEvent to register us as the last-visible-main-window.
|
"""Extend showEvent to register us as the last-visible-main-window.
|
||||||
@ -547,7 +547,7 @@ class MainWindow(QWidget):
|
|||||||
if crashsignal.is_crashing:
|
if crashsignal.is_crashing:
|
||||||
e.accept()
|
e.accept()
|
||||||
return
|
return
|
||||||
tab_count = self.tabbed_browser.count()
|
tab_count = self.tabbed_browser.widget.count()
|
||||||
download_model = objreg.get('download-model', scope='window',
|
download_model = objreg.get('download-model', scope='window',
|
||||||
window=self.win_id)
|
window=self.win_id)
|
||||||
download_count = download_model.running_downloads()
|
download_count = download_model.running_downloads()
|
||||||
|
@ -507,8 +507,8 @@ class _BasePrompt(QWidget):
|
|||||||
self._key_grid = QGridLayout()
|
self._key_grid = QGridLayout()
|
||||||
self._key_grid.setVerticalSpacing(0)
|
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(
|
||||||
all_bindings = config.key_instance.get_reverse_bindings_for('prompt')
|
self.KEY_MODE.name)
|
||||||
labels = []
|
labels = []
|
||||||
|
|
||||||
for cmd, text in self._allowed_commands():
|
for cmd, text in self._allowed_commands():
|
||||||
@ -596,6 +596,8 @@ class FilenamePrompt(_BasePrompt):
|
|||||||
if config.val.prompt.filebrowser:
|
if config.val.prompt.filebrowser:
|
||||||
self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred)
|
self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred)
|
||||||
|
|
||||||
|
self._to_complete = ''
|
||||||
|
|
||||||
@pyqtSlot(str)
|
@pyqtSlot(str)
|
||||||
def _set_fileview_root(self, path, *, tabbed=False):
|
def _set_fileview_root(self, path, *, tabbed=False):
|
||||||
"""Set the root path for the file display."""
|
"""Set the root path for the file display."""
|
||||||
@ -604,6 +606,9 @@ class FilenamePrompt(_BasePrompt):
|
|||||||
separators += os.altsep
|
separators += os.altsep
|
||||||
|
|
||||||
dirname = os.path.dirname(path)
|
dirname = os.path.dirname(path)
|
||||||
|
basename = os.path.basename(path)
|
||||||
|
if not tabbed:
|
||||||
|
self._to_complete = ''
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if not path:
|
if not path:
|
||||||
@ -617,6 +622,7 @@ class FilenamePrompt(_BasePrompt):
|
|||||||
elif os.path.isdir(dirname) and not tabbed:
|
elif os.path.isdir(dirname) and not tabbed:
|
||||||
# Input like /foo/ba -> show /foo contents
|
# Input like /foo/ba -> show /foo contents
|
||||||
path = dirname
|
path = dirname
|
||||||
|
self._to_complete = basename
|
||||||
else:
|
else:
|
||||||
return
|
return
|
||||||
except OSError:
|
except OSError:
|
||||||
@ -634,7 +640,11 @@ class FilenamePrompt(_BasePrompt):
|
|||||||
index: The QModelIndex of the selected element.
|
index: The QModelIndex of the selected element.
|
||||||
clicked: Whether the element was clicked.
|
clicked: Whether the element was clicked.
|
||||||
"""
|
"""
|
||||||
|
if index == QModelIndex():
|
||||||
|
path = os.path.join(self._file_model.rootPath(), self._to_complete)
|
||||||
|
else:
|
||||||
path = os.path.normpath(self._file_model.filePath(index))
|
path = os.path.normpath(self._file_model.filePath(index))
|
||||||
|
|
||||||
if clicked:
|
if clicked:
|
||||||
path += os.sep
|
path += os.sep
|
||||||
else:
|
else:
|
||||||
@ -696,6 +706,7 @@ class FilenamePrompt(_BasePrompt):
|
|||||||
assert last_index.isValid()
|
assert last_index.isValid()
|
||||||
|
|
||||||
idx = selmodel.currentIndex()
|
idx = selmodel.currentIndex()
|
||||||
|
|
||||||
if not idx.isValid():
|
if not idx.isValid():
|
||||||
# No item selected yet
|
# No item selected yet
|
||||||
idx = last_index if which == 'prev' else first_index
|
idx = last_index if which == 'prev' else first_index
|
||||||
@ -709,10 +720,24 @@ class FilenamePrompt(_BasePrompt):
|
|||||||
if not idx.isValid():
|
if not idx.isValid():
|
||||||
idx = last_index if which == 'prev' else first_index
|
idx = last_index if which == 'prev' else first_index
|
||||||
|
|
||||||
|
idx = self._do_completion(idx, which)
|
||||||
|
|
||||||
selmodel.setCurrentIndex(
|
selmodel.setCurrentIndex(
|
||||||
idx, QItemSelectionModel.ClearAndSelect | QItemSelectionModel.Rows)
|
idx, QItemSelectionModel.ClearAndSelect | QItemSelectionModel.Rows)
|
||||||
self._insert_path(idx, clicked=False)
|
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):
|
def _allowed_commands(self):
|
||||||
return [('prompt-accept', 'Accept'), ('leave-mode', 'Abort')]
|
return [('prompt-accept', 'Accept'), ('leave-mode', 'Abort')]
|
||||||
|
|
||||||
|
@ -32,7 +32,7 @@ class Backforward(textbase.TextBase):
|
|||||||
|
|
||||||
def on_tab_cur_url_changed(self, tabs):
|
def on_tab_cur_url_changed(self, tabs):
|
||||||
"""Called on URL changes."""
|
"""Called on URL changes."""
|
||||||
tab = tabs.currentWidget()
|
tab = tabs.widget.currentWidget()
|
||||||
if tab is None: # pragma: no cover
|
if tab is None: # pragma: no cover
|
||||||
self.setText('')
|
self.setText('')
|
||||||
self.hide()
|
self.hide()
|
||||||
|
@ -268,7 +268,7 @@ class StatusBar(QWidget):
|
|||||||
"""Get the currently displayed tab."""
|
"""Get the currently displayed tab."""
|
||||||
window = objreg.get('tabbed-browser', scope='window',
|
window = objreg.get('tabbed-browser', scope='window',
|
||||||
window=self._win_id)
|
window=self._win_id)
|
||||||
return window.currentWidget()
|
return window.widget.currentWidget()
|
||||||
|
|
||||||
def set_mode_active(self, mode, val):
|
def set_mode_active(self, mode, val):
|
||||||
"""Setter for self.{insert,command,caret}_active.
|
"""Setter for self.{insert,command,caret}_active.
|
||||||
|
@ -22,7 +22,7 @@
|
|||||||
import functools
|
import functools
|
||||||
|
|
||||||
import attr
|
import attr
|
||||||
from PyQt5.QtWidgets import QSizePolicy
|
from PyQt5.QtWidgets import QSizePolicy, QWidget
|
||||||
from PyQt5.QtCore import pyqtSignal, pyqtSlot, QTimer, QUrl
|
from PyQt5.QtCore import pyqtSignal, pyqtSlot, QTimer, QUrl
|
||||||
from PyQt5.QtGui import QIcon
|
from PyQt5.QtGui import QIcon
|
||||||
|
|
||||||
@ -50,7 +50,7 @@ class TabDeletedError(Exception):
|
|||||||
"""Exception raised when _tab_index is called for a deleted tab."""
|
"""Exception raised when _tab_index is called for a deleted tab."""
|
||||||
|
|
||||||
|
|
||||||
class TabbedBrowser(tabwidget.TabWidget):
|
class TabbedBrowser(QWidget):
|
||||||
|
|
||||||
"""A TabWidget with QWebViews inside.
|
"""A TabWidget with QWebViews inside.
|
||||||
|
|
||||||
@ -110,17 +110,18 @@ class TabbedBrowser(tabwidget.TabWidget):
|
|||||||
new_tab = pyqtSignal(browsertab.AbstractTab, int)
|
new_tab = pyqtSignal(browsertab.AbstractTab, int)
|
||||||
|
|
||||||
def __init__(self, *, win_id, private, parent=None):
|
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._win_id = win_id
|
||||||
self._tab_insert_idx_left = 0
|
self._tab_insert_idx_left = 0
|
||||||
self._tab_insert_idx_right = -1
|
self._tab_insert_idx_right = -1
|
||||||
self.shutting_down = False
|
self.shutting_down = False
|
||||||
self.tabCloseRequested.connect(self.on_tab_close_requested)
|
self.widget.tabCloseRequested.connect(self.on_tab_close_requested)
|
||||||
self.new_tab_requested.connect(self.tabopen)
|
self.widget.new_tab_requested.connect(self.tabopen)
|
||||||
self.currentChanged.connect(self.on_current_changed)
|
self.widget.currentChanged.connect(self.on_current_changed)
|
||||||
self.cur_load_started.connect(self.on_cur_load_started)
|
self.cur_load_started.connect(self.on_cur_load_started)
|
||||||
self.cur_fullscreen_requested.connect(self.tabBar().maybe_hide)
|
self.cur_fullscreen_requested.connect(self.widget.tabBar().maybe_hide)
|
||||||
self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
|
self.widget.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
|
||||||
self._undo_stack = []
|
self._undo_stack = []
|
||||||
self._filter = signalfilter.SignalFilter(win_id, self)
|
self._filter = signalfilter.SignalFilter(win_id, self)
|
||||||
self._now_focused = None
|
self._now_focused = None
|
||||||
@ -128,12 +129,12 @@ class TabbedBrowser(tabwidget.TabWidget):
|
|||||||
self.search_options = {}
|
self.search_options = {}
|
||||||
self._local_marks = {}
|
self._local_marks = {}
|
||||||
self._global_marks = {}
|
self._global_marks = {}
|
||||||
self.default_window_icon = self.window().windowIcon()
|
self.default_window_icon = self.widget.window().windowIcon()
|
||||||
self.private = private
|
self.private = private
|
||||||
config.instance.changed.connect(self._on_config_changed)
|
config.instance.changed.connect(self._on_config_changed)
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return utils.get_repr(self, count=self.count())
|
return utils.get_repr(self, count=self.widget.count())
|
||||||
|
|
||||||
@pyqtSlot(str)
|
@pyqtSlot(str)
|
||||||
def _on_config_changed(self, option):
|
def _on_config_changed(self, option):
|
||||||
@ -142,7 +143,7 @@ class TabbedBrowser(tabwidget.TabWidget):
|
|||||||
elif option == 'window.title_format':
|
elif option == 'window.title_format':
|
||||||
self._update_window_title()
|
self._update_window_title()
|
||||||
elif option in ['tabs.title.format', 'tabs.title.format_pinned']:
|
elif option in ['tabs.title.format', 'tabs.title.format_pinned']:
|
||||||
self._update_tab_titles()
|
self.widget.update_tab_titles()
|
||||||
|
|
||||||
def _tab_index(self, tab):
|
def _tab_index(self, tab):
|
||||||
"""Get the index of a given 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.
|
Raises TabDeletedError if the tab doesn't exist anymore.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
idx = self.indexOf(tab)
|
idx = self.widget.indexOf(tab)
|
||||||
except RuntimeError as e:
|
except RuntimeError as e:
|
||||||
log.webview.debug("Got invalid tab ({})!".format(e))
|
log.webview.debug("Got invalid tab ({})!".format(e))
|
||||||
raise TabDeletedError(e)
|
raise TabDeletedError(e)
|
||||||
@ -166,8 +167,8 @@ class TabbedBrowser(tabwidget.TabWidget):
|
|||||||
iterating over the list.
|
iterating over the list.
|
||||||
"""
|
"""
|
||||||
widgets = []
|
widgets = []
|
||||||
for i in range(self.count()):
|
for i in range(self.widget.count()):
|
||||||
widget = self.widget(i)
|
widget = self.widget.widget(i)
|
||||||
if widget is None:
|
if widget is None:
|
||||||
log.webview.debug("Got None-widget in tabbedbrowser!")
|
log.webview.debug("Got None-widget in tabbedbrowser!")
|
||||||
else:
|
else:
|
||||||
@ -186,16 +187,16 @@ class TabbedBrowser(tabwidget.TabWidget):
|
|||||||
if field is not None and ('{' + field + '}') not in title_format:
|
if field is not None and ('{' + field + '}') not in title_format:
|
||||||
return
|
return
|
||||||
|
|
||||||
idx = self.currentIndex()
|
idx = self.widget.currentIndex()
|
||||||
if idx == -1:
|
if idx == -1:
|
||||||
# (e.g. last tab removed)
|
# (e.g. last tab removed)
|
||||||
log.webview.debug("Not updating window title because index is -1")
|
log.webview.debug("Not updating window title because index is -1")
|
||||||
return
|
return
|
||||||
fields = self.get_tab_fields(idx)
|
fields = self.widget.get_tab_fields(idx)
|
||||||
fields['id'] = self._win_id
|
fields['id'] = self._win_id
|
||||||
|
|
||||||
title = title_format.format(**fields)
|
title = title_format.format(**fields)
|
||||||
self.window().setWindowTitle(title)
|
self.widget.window().setWindowTitle(title)
|
||||||
|
|
||||||
def _connect_tab_signals(self, tab):
|
def _connect_tab_signals(self, tab):
|
||||||
"""Set up the needed signals for tab."""
|
"""Set up the needed signals for tab."""
|
||||||
@ -247,8 +248,8 @@ class TabbedBrowser(tabwidget.TabWidget):
|
|||||||
Return:
|
Return:
|
||||||
The current URL as QUrl.
|
The current URL as QUrl.
|
||||||
"""
|
"""
|
||||||
idx = self.currentIndex()
|
idx = self.widget.currentIndex()
|
||||||
return super().tab_url(idx)
|
return self.widget.tab_url(idx)
|
||||||
|
|
||||||
def shutdown(self):
|
def shutdown(self):
|
||||||
"""Try to shut down all tabs cleanly."""
|
"""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.
|
new_undo: Whether the undo entry should be a new item in the stack.
|
||||||
"""
|
"""
|
||||||
last_close = config.val.tabs.last_close
|
last_close = config.val.tabs.last_close
|
||||||
count = self.count()
|
count = self.widget.count()
|
||||||
|
|
||||||
if last_close == 'ignore' and count == 1:
|
if last_close == 'ignore' and count == 1:
|
||||||
return
|
return
|
||||||
@ -311,7 +312,7 @@ class TabbedBrowser(tabwidget.TabWidget):
|
|||||||
new_undo: Whether the undo entry should be a new item in the stack.
|
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.
|
crashed: Whether we're closing a tab with crashed renderer process.
|
||||||
"""
|
"""
|
||||||
idx = self.indexOf(tab)
|
idx = self.widget.indexOf(tab)
|
||||||
if idx == -1:
|
if idx == -1:
|
||||||
if crashed:
|
if crashed:
|
||||||
return
|
return
|
||||||
@ -349,7 +350,7 @@ class TabbedBrowser(tabwidget.TabWidget):
|
|||||||
self._undo_stack[-1].append(entry)
|
self._undo_stack[-1].append(entry)
|
||||||
|
|
||||||
tab.shutdown()
|
tab.shutdown()
|
||||||
self.removeTab(idx)
|
self.widget.removeTab(idx)
|
||||||
if not crashed:
|
if not crashed:
|
||||||
# WORKAROUND for a segfault when we delete the crashed tab.
|
# WORKAROUND for a segfault when we delete the crashed tab.
|
||||||
# see https://bugreports.qt.io/browse/QTBUG-58698
|
# see https://bugreports.qt.io/browse/QTBUG-58698
|
||||||
@ -362,14 +363,14 @@ class TabbedBrowser(tabwidget.TabWidget):
|
|||||||
last_close = config.val.tabs.last_close
|
last_close = config.val.tabs.last_close
|
||||||
use_current_tab = False
|
use_current_tab = False
|
||||||
if last_close in ['blank', 'startpage', 'default-page']:
|
if last_close in ['blank', 'startpage', 'default-page']:
|
||||||
only_one_tab_open = self.count() == 1
|
only_one_tab_open = self.widget.count() == 1
|
||||||
no_history = len(self.widget(0).history) == 1
|
no_history = len(self.widget.widget(0).history) == 1
|
||||||
urls = {
|
urls = {
|
||||||
'blank': QUrl('about:blank'),
|
'blank': QUrl('about:blank'),
|
||||||
'startpage': config.val.url.start_pages[0],
|
'startpage': config.val.url.start_pages[0],
|
||||||
'default-page': config.val.url.default_page,
|
'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('/')
|
last_close_urlstr = urls[last_close].toString().rstrip('/')
|
||||||
first_tab_urlstr = first_tab_url.toString().rstrip('/')
|
first_tab_urlstr = first_tab_url.toString().rstrip('/')
|
||||||
last_close_url_used = first_tab_urlstr == last_close_urlstr
|
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()):
|
for entry in reversed(self._undo_stack.pop()):
|
||||||
if use_current_tab:
|
if use_current_tab:
|
||||||
self.openurl(entry.url, newtab=False)
|
newtab = self.widget.widget(0)
|
||||||
newtab = self.widget(0)
|
|
||||||
use_current_tab = False
|
use_current_tab = False
|
||||||
else:
|
else:
|
||||||
newtab = self.tabopen(entry.url, background=False,
|
newtab = self.tabopen(background=False, idx=entry.index)
|
||||||
idx=entry.index)
|
|
||||||
|
|
||||||
newtab.history.deserialize(entry.history)
|
newtab.history.deserialize(entry.history)
|
||||||
self.set_tab_pinned(newtab, entry.pinned)
|
self.widget.set_tab_pinned(newtab, entry.pinned)
|
||||||
|
|
||||||
@pyqtSlot('QUrl', bool)
|
@pyqtSlot('QUrl', bool)
|
||||||
def openurl(self, url, newtab):
|
def openurl(self, url, newtab):
|
||||||
@ -397,15 +396,15 @@ class TabbedBrowser(tabwidget.TabWidget):
|
|||||||
newtab: True to open URL in a new tab, False otherwise.
|
newtab: True to open URL in a new tab, False otherwise.
|
||||||
"""
|
"""
|
||||||
qtutils.ensure_valid(url)
|
qtutils.ensure_valid(url)
|
||||||
if newtab or self.currentWidget() is None:
|
if newtab or self.widget.currentWidget() is None:
|
||||||
self.tabopen(url, background=False)
|
self.tabopen(url, background=False)
|
||||||
else:
|
else:
|
||||||
self.currentWidget().openurl(url)
|
self.widget.currentWidget().openurl(url)
|
||||||
|
|
||||||
@pyqtSlot(int)
|
@pyqtSlot(int)
|
||||||
def on_tab_close_requested(self, idx):
|
def on_tab_close_requested(self, idx):
|
||||||
"""Close a tab via an index."""
|
"""Close a tab via an index."""
|
||||||
tab = self.widget(idx)
|
tab = self.widget.widget(idx)
|
||||||
if tab is None:
|
if tab is None:
|
||||||
log.webview.debug("Got invalid tab {} for index {}!".format(
|
log.webview.debug("Got invalid tab {} for index {}!".format(
|
||||||
tab, idx))
|
tab, idx))
|
||||||
@ -456,7 +455,7 @@ class TabbedBrowser(tabwidget.TabWidget):
|
|||||||
"related {}, idx {}".format(
|
"related {}, idx {}".format(
|
||||||
url, background, related, idx))
|
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):
|
not ignore_tabs_are_windows):
|
||||||
window = mainwindow.MainWindow(private=self.private)
|
window = mainwindow.MainWindow(private=self.private)
|
||||||
window.show()
|
window.show()
|
||||||
@ -466,12 +465,12 @@ class TabbedBrowser(tabwidget.TabWidget):
|
|||||||
related=related)
|
related=related)
|
||||||
|
|
||||||
tab = browsertab.create(win_id=self._win_id, private=self.private,
|
tab = browsertab.create(win_id=self._win_id, private=self.private,
|
||||||
parent=self)
|
parent=self.widget)
|
||||||
self._connect_tab_signals(tab)
|
self._connect_tab_signals(tab)
|
||||||
|
|
||||||
if idx is None:
|
if idx is None:
|
||||||
idx = self._get_new_tab_idx(related)
|
idx = self._get_new_tab_idx(related)
|
||||||
self.insertTab(idx, tab, "")
|
self.widget.insertTab(idx, tab, "")
|
||||||
|
|
||||||
if url is not None:
|
if url is not None:
|
||||||
tab.openurl(url)
|
tab.openurl(url)
|
||||||
@ -482,10 +481,11 @@ class TabbedBrowser(tabwidget.TabWidget):
|
|||||||
# Make sure the background tab has the correct initial size.
|
# Make sure the background tab has the correct initial size.
|
||||||
# With a foreground tab, it's going to be resized correctly by the
|
# With a foreground tab, it's going to be resized correctly by the
|
||||||
# layout anyways.
|
# layout anyways.
|
||||||
tab.resize(self.currentWidget().size())
|
tab.resize(self.widget.currentWidget().size())
|
||||||
self.tab_index_changed.emit(self.currentIndex(), self.count())
|
self.widget.tab_index_changed.emit(self.widget.currentIndex(),
|
||||||
|
self.widget.count())
|
||||||
else:
|
else:
|
||||||
self.setCurrentWidget(tab)
|
self.widget.setCurrentWidget(tab)
|
||||||
|
|
||||||
tab.show()
|
tab.show()
|
||||||
self.new_tab.emit(tab, idx)
|
self.new_tab.emit(tab, idx)
|
||||||
@ -530,13 +530,14 @@ class TabbedBrowser(tabwidget.TabWidget):
|
|||||||
"""Update favicons when config was changed."""
|
"""Update favicons when config was changed."""
|
||||||
for i, tab in enumerate(self.widgets()):
|
for i, tab in enumerate(self.widgets()):
|
||||||
if config.val.tabs.favicons.show:
|
if config.val.tabs.favicons.show:
|
||||||
self.setTabIcon(i, tab.icon())
|
self.widget.setTabIcon(i, tab.icon())
|
||||||
if config.val.tabs.tabs_are_windows:
|
if config.val.tabs.tabs_are_windows:
|
||||||
self.window().setWindowIcon(tab.icon())
|
self.widget.window().setWindowIcon(tab.icon())
|
||||||
else:
|
else:
|
||||||
self.setTabIcon(i, QIcon())
|
self.widget.setTabIcon(i, QIcon())
|
||||||
if config.val.tabs.tabs_are_windows:
|
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()
|
@pyqtSlot()
|
||||||
def on_load_started(self, tab):
|
def on_load_started(self, tab):
|
||||||
@ -550,15 +551,14 @@ class TabbedBrowser(tabwidget.TabWidget):
|
|||||||
except TabDeletedError:
|
except TabDeletedError:
|
||||||
# We can get signals for tabs we already deleted...
|
# We can get signals for tabs we already deleted...
|
||||||
return
|
return
|
||||||
self._update_tab_title(idx)
|
self.widget.update_tab_title(idx)
|
||||||
if tab.data.keep_icon:
|
if tab.data.keep_icon:
|
||||||
tab.data.keep_icon = False
|
tab.data.keep_icon = False
|
||||||
else:
|
else:
|
||||||
self.setTabIcon(idx, QIcon())
|
|
||||||
if (config.val.tabs.tabs_are_windows and
|
if (config.val.tabs.tabs_are_windows and
|
||||||
config.val.tabs.favicons.show):
|
config.val.tabs.favicons.show):
|
||||||
self.window().setWindowIcon(self.default_window_icon)
|
self.widget.window().setWindowIcon(self.default_window_icon)
|
||||||
if idx == self.currentIndex():
|
if idx == self.widget.currentIndex():
|
||||||
self._update_window_title()
|
self._update_window_title()
|
||||||
|
|
||||||
@pyqtSlot()
|
@pyqtSlot()
|
||||||
@ -589,8 +589,8 @@ class TabbedBrowser(tabwidget.TabWidget):
|
|||||||
return
|
return
|
||||||
log.webview.debug("Changing title for idx {} to '{}'".format(
|
log.webview.debug("Changing title for idx {} to '{}'".format(
|
||||||
idx, text))
|
idx, text))
|
||||||
self.set_page_title(idx, text)
|
self.widget.set_page_title(idx, text)
|
||||||
if idx == self.currentIndex():
|
if idx == self.widget.currentIndex():
|
||||||
self._update_window_title()
|
self._update_window_title()
|
||||||
|
|
||||||
@pyqtSlot(browsertab.AbstractTab, QUrl)
|
@pyqtSlot(browsertab.AbstractTab, QUrl)
|
||||||
@ -607,8 +607,8 @@ class TabbedBrowser(tabwidget.TabWidget):
|
|||||||
# We can get signals for tabs we already deleted...
|
# We can get signals for tabs we already deleted...
|
||||||
return
|
return
|
||||||
|
|
||||||
if not self.page_title(idx):
|
if not self.widget.page_title(idx):
|
||||||
self.set_page_title(idx, url.toDisplayString())
|
self.widget.set_page_title(idx, url.toDisplayString())
|
||||||
|
|
||||||
@pyqtSlot(browsertab.AbstractTab, QIcon)
|
@pyqtSlot(browsertab.AbstractTab, QIcon)
|
||||||
def on_icon_changed(self, tab, icon):
|
def on_icon_changed(self, tab, icon):
|
||||||
@ -627,23 +627,23 @@ class TabbedBrowser(tabwidget.TabWidget):
|
|||||||
except TabDeletedError:
|
except TabDeletedError:
|
||||||
# We can get signals for tabs we already deleted...
|
# We can get signals for tabs we already deleted...
|
||||||
return
|
return
|
||||||
self.setTabIcon(idx, icon)
|
self.widget.setTabIcon(idx, icon)
|
||||||
if config.val.tabs.tabs_are_windows:
|
if config.val.tabs.tabs_are_windows:
|
||||||
self.window().setWindowIcon(icon)
|
self.widget.window().setWindowIcon(icon)
|
||||||
|
|
||||||
@pyqtSlot(usertypes.KeyMode)
|
@pyqtSlot(usertypes.KeyMode)
|
||||||
def on_mode_entered(self, mode):
|
def on_mode_entered(self, mode):
|
||||||
"""Save input mode when tabs.mode_on_change = restore."""
|
"""Save input mode when tabs.mode_on_change = restore."""
|
||||||
if (config.val.tabs.mode_on_change == 'restore' and
|
if (config.val.tabs.mode_on_change == 'restore' and
|
||||||
mode in modeman.INPUT_MODES):
|
mode in modeman.INPUT_MODES):
|
||||||
tab = self.currentWidget()
|
tab = self.widget.currentWidget()
|
||||||
if tab is not None:
|
if tab is not None:
|
||||||
tab.data.input_mode = mode
|
tab.data.input_mode = mode
|
||||||
|
|
||||||
@pyqtSlot(usertypes.KeyMode)
|
@pyqtSlot(usertypes.KeyMode)
|
||||||
def on_mode_left(self, mode):
|
def on_mode_left(self, mode):
|
||||||
"""Give focus to current tab if command mode was left."""
|
"""Give focus to current tab if command mode was left."""
|
||||||
widget = self.currentWidget()
|
widget = self.widget.currentWidget()
|
||||||
if widget is None:
|
if widget is None:
|
||||||
return
|
return
|
||||||
if mode in [usertypes.KeyMode.command] + modeman.PROMPT_MODES:
|
if mode in [usertypes.KeyMode.command] + modeman.PROMPT_MODES:
|
||||||
@ -660,7 +660,7 @@ class TabbedBrowser(tabwidget.TabWidget):
|
|||||||
if idx == -1 or self.shutting_down:
|
if idx == -1 or self.shutting_down:
|
||||||
# closing the last tab (before quitting) or shutting down
|
# closing the last tab (before quitting) or shutting down
|
||||||
return
|
return
|
||||||
tab = self.widget(idx)
|
tab = self.widget.widget(idx)
|
||||||
if tab is None:
|
if tab is None:
|
||||||
log.webview.debug("on_current_changed got called with invalid "
|
log.webview.debug("on_current_changed got called with invalid "
|
||||||
"index {}".format(idx))
|
"index {}".format(idx))
|
||||||
@ -690,8 +690,8 @@ class TabbedBrowser(tabwidget.TabWidget):
|
|||||||
self._now_focused = tab
|
self._now_focused = tab
|
||||||
self.current_tab_changed.emit(tab)
|
self.current_tab_changed.emit(tab)
|
||||||
QTimer.singleShot(0, self._update_window_title)
|
QTimer.singleShot(0, self._update_window_title)
|
||||||
self._tab_insert_idx_left = self.currentIndex()
|
self._tab_insert_idx_left = self.widget.currentIndex()
|
||||||
self._tab_insert_idx_right = self.currentIndex() + 1
|
self._tab_insert_idx_right = self.widget.currentIndex() + 1
|
||||||
|
|
||||||
@pyqtSlot()
|
@pyqtSlot()
|
||||||
def on_cmd_return_pressed(self):
|
def on_cmd_return_pressed(self):
|
||||||
@ -709,9 +709,9 @@ class TabbedBrowser(tabwidget.TabWidget):
|
|||||||
stop = config.val.colors.tabs.indicator.stop
|
stop = config.val.colors.tabs.indicator.stop
|
||||||
system = config.val.colors.tabs.indicator.system
|
system = config.val.colors.tabs.indicator.system
|
||||||
color = utils.interpolate_color(start, stop, perc, system)
|
color = utils.interpolate_color(start, stop, perc, system)
|
||||||
self.set_tab_indicator_color(idx, color)
|
self.widget.set_tab_indicator_color(idx, color)
|
||||||
self._update_tab_title(idx)
|
self.widget.update_tab_title(idx)
|
||||||
if idx == self.currentIndex():
|
if idx == self.widget.currentIndex():
|
||||||
self._update_window_title()
|
self._update_window_title()
|
||||||
|
|
||||||
def on_load_finished(self, tab, ok):
|
def on_load_finished(self, tab, ok):
|
||||||
@ -728,23 +728,23 @@ class TabbedBrowser(tabwidget.TabWidget):
|
|||||||
color = utils.interpolate_color(start, stop, 100, system)
|
color = utils.interpolate_color(start, stop, 100, system)
|
||||||
else:
|
else:
|
||||||
color = config.val.colors.tabs.indicator.error
|
color = config.val.colors.tabs.indicator.error
|
||||||
self.set_tab_indicator_color(idx, color)
|
self.widget.set_tab_indicator_color(idx, color)
|
||||||
self._update_tab_title(idx)
|
self.widget.update_tab_title(idx)
|
||||||
if idx == self.currentIndex():
|
if idx == self.widget.currentIndex():
|
||||||
self._update_window_title()
|
self._update_window_title()
|
||||||
tab.handle_auto_insert_mode(ok)
|
tab.handle_auto_insert_mode(ok)
|
||||||
|
|
||||||
@pyqtSlot()
|
@pyqtSlot()
|
||||||
def on_scroll_pos_changed(self):
|
def on_scroll_pos_changed(self):
|
||||||
"""Update tab and window title when scroll position changed."""
|
"""Update tab and window title when scroll position changed."""
|
||||||
idx = self.currentIndex()
|
idx = self.widget.currentIndex()
|
||||||
if idx == -1:
|
if idx == -1:
|
||||||
# (e.g. last tab removed)
|
# (e.g. last tab removed)
|
||||||
log.webview.debug("Not updating scroll position because index is "
|
log.webview.debug("Not updating scroll position because index is "
|
||||||
"-1")
|
"-1")
|
||||||
return
|
return
|
||||||
self._update_window_title('scroll_pos')
|
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):
|
def _on_renderer_process_terminated(self, tab, status, code):
|
||||||
"""Show an error when a renderer process terminated."""
|
"""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
|
# WORKAROUND for https://bugreports.qt.io/browse/QTBUG-58698
|
||||||
message.error(msg)
|
message.error(msg)
|
||||||
self._remove_tab(tab, crashed=True)
|
self._remove_tab(tab, crashed=True)
|
||||||
if self.count() == 0:
|
if self.widget.count() == 0:
|
||||||
self.tabopen(QUrl('about:blank'))
|
self.tabopen(QUrl('about:blank'))
|
||||||
|
|
||||||
def resizeEvent(self, e):
|
def resizeEvent(self, e):
|
||||||
@ -814,7 +814,7 @@ class TabbedBrowser(tabwidget.TabWidget):
|
|||||||
if key != "'":
|
if key != "'":
|
||||||
message.error("Failed to set mark: url invalid")
|
message.error("Failed to set mark: url invalid")
|
||||||
return
|
return
|
||||||
point = self.currentWidget().scroller.pos_px()
|
point = self.widget.currentWidget().scroller.pos_px()
|
||||||
|
|
||||||
if key.isupper():
|
if key.isupper():
|
||||||
self._global_marks[key] = point, url
|
self._global_marks[key] = point, url
|
||||||
@ -835,7 +835,7 @@ class TabbedBrowser(tabwidget.TabWidget):
|
|||||||
except qtutils.QtValueError:
|
except qtutils.QtValueError:
|
||||||
urlkey = None
|
urlkey = None
|
||||||
|
|
||||||
tab = self.currentWidget()
|
tab = self.widget.currentWidget()
|
||||||
|
|
||||||
if key.isupper():
|
if key.isupper():
|
||||||
if key in self._global_marks:
|
if key in self._global_marks:
|
||||||
|
@ -60,7 +60,7 @@ class TabWidget(QTabWidget):
|
|||||||
self.setTabBar(bar)
|
self.setTabBar(bar)
|
||||||
bar.tabCloseRequested.connect(self.tabCloseRequested)
|
bar.tabCloseRequested.connect(self.tabCloseRequested)
|
||||||
bar.tabMoved.connect(functools.partial(
|
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.currentChanged.connect(self._on_current_changed)
|
||||||
bar.new_tab_requested.connect(self._on_new_tab_requested)
|
bar.new_tab_requested.connect(self._on_new_tab_requested)
|
||||||
self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
|
self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
|
||||||
@ -108,7 +108,7 @@ class TabWidget(QTabWidget):
|
|||||||
|
|
||||||
bar.set_tab_data(idx, 'pinned', pinned)
|
bar.set_tab_data(idx, 'pinned', pinned)
|
||||||
tab.data.pinned = pinned
|
tab.data.pinned = pinned
|
||||||
self._update_tab_title(idx)
|
self.update_tab_title(idx)
|
||||||
|
|
||||||
def tab_indicator_color(self, idx):
|
def tab_indicator_color(self, idx):
|
||||||
"""Get the tab indicator color for the given index."""
|
"""Get the tab indicator color for the given index."""
|
||||||
@ -117,13 +117,13 @@ class TabWidget(QTabWidget):
|
|||||||
def set_page_title(self, idx, title):
|
def set_page_title(self, idx, title):
|
||||||
"""Set the tab title user data."""
|
"""Set the tab title user data."""
|
||||||
self.tabBar().set_tab_data(idx, 'page-title', title)
|
self.tabBar().set_tab_data(idx, 'page-title', title)
|
||||||
self._update_tab_title(idx)
|
self.update_tab_title(idx)
|
||||||
|
|
||||||
def page_title(self, idx):
|
def page_title(self, idx):
|
||||||
"""Get the tab title user data."""
|
"""Get the tab title user data."""
|
||||||
return self.tabBar().page_title(idx)
|
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.
|
"""Update the tab text for the given tab.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@ -197,20 +197,20 @@ class TabWidget(QTabWidget):
|
|||||||
fields['scroll_pos'] = scroll_pos
|
fields['scroll_pos'] = scroll_pos
|
||||||
return fields
|
return fields
|
||||||
|
|
||||||
def _update_tab_titles(self):
|
def update_tab_titles(self):
|
||||||
"""Update all texts."""
|
"""Update all texts."""
|
||||||
for idx in range(self.count()):
|
for idx in range(self.count()):
|
||||||
self._update_tab_title(idx)
|
self.update_tab_title(idx)
|
||||||
|
|
||||||
def tabInserted(self, idx):
|
def tabInserted(self, idx):
|
||||||
"""Update titles when a tab was inserted."""
|
"""Update titles when a tab was inserted."""
|
||||||
super().tabInserted(idx)
|
super().tabInserted(idx)
|
||||||
self._update_tab_titles()
|
self.update_tab_titles()
|
||||||
|
|
||||||
def tabRemoved(self, idx):
|
def tabRemoved(self, idx):
|
||||||
"""Update titles when a tab was removed."""
|
"""Update titles when a tab was removed."""
|
||||||
super().tabRemoved(idx)
|
super().tabRemoved(idx)
|
||||||
self._update_tab_titles()
|
self.update_tab_titles()
|
||||||
|
|
||||||
def addTab(self, page, icon_or_text, text_or_empty=None):
|
def addTab(self, page, icon_or_text, text_or_empty=None):
|
||||||
"""Override addTab to use our own text setting logic.
|
"""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,
|
from PyQt5.QtCore import (qVersion, QT_VERSION, PYQT_VERSION,
|
||||||
PYQT_VERSION_STR)
|
PYQT_VERSION_STR)
|
||||||
from pkg_resources import parse_version
|
from pkg_resources import parse_version
|
||||||
|
from qutebrowser.utils import log
|
||||||
if (QT_VERSION < 0x050701 or PYQT_VERSION < 0x050700 or
|
if (QT_VERSION < 0x050701 or PYQT_VERSION < 0x050700 or
|
||||||
parse_version(qVersion()) < parse_version('5.7.1')):
|
parse_version(qVersion()) < parse_version('5.7.1')):
|
||||||
text = ("Fatal error: Qt >= 5.7.1 and PyQt >= 5.7 are required, "
|
text = ("Fatal error: Qt >= 5.7.1 and PyQt >= 5.7 are required, "
|
||||||
@ -179,6 +180,10 @@ def check_qt_version():
|
|||||||
PYQT_VERSION_STR))
|
PYQT_VERSION_STR))
|
||||||
_die(text)
|
_die(text)
|
||||||
|
|
||||||
|
if qVersion().startswith('5.8.'):
|
||||||
|
log.init.warning("Running qutebrowser with Qt 5.8 is untested and "
|
||||||
|
"unsupported!")
|
||||||
|
|
||||||
|
|
||||||
def check_ssl_support():
|
def check_ssl_support():
|
||||||
"""Check if SSL support is available."""
|
"""Check if SSL support is available."""
|
||||||
|
@ -42,6 +42,7 @@ class ExternalEditor(QObject):
|
|||||||
_proc: The GUIProcess of the editor.
|
_proc: The GUIProcess of the editor.
|
||||||
_watcher: A QFileSystemWatcher to watch the edited file for changes.
|
_watcher: A QFileSystemWatcher to watch the edited file for changes.
|
||||||
Only set if watch=True.
|
Only set if watch=True.
|
||||||
|
_content: The last-saved text of the editor.
|
||||||
|
|
||||||
Signals:
|
Signals:
|
||||||
file_updated: The text in the edited file was updated.
|
file_updated: The text in the edited file was updated.
|
||||||
@ -112,19 +113,7 @@ class ExternalEditor(QObject):
|
|||||||
if self._filename is not None:
|
if self._filename is not None:
|
||||||
raise ValueError("Already editing a file!")
|
raise ValueError("Already editing a file!")
|
||||||
try:
|
try:
|
||||||
# Close while the external process is running, as otherwise systems
|
self._filename = self._create_tempfile(text, 'qutebrowser-editor-')
|
||||||
# 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
|
|
||||||
except OSError as e:
|
except OSError as e:
|
||||||
message.error("Failed to create initial file: {}".format(e))
|
message.error("Failed to create initial file: {}".format(e))
|
||||||
return
|
return
|
||||||
@ -134,6 +123,32 @@ class ExternalEditor(QObject):
|
|||||||
line, column = self._calc_line_and_column(text, caret_position)
|
line, column = self._calc_line_and_column(text, caret_position)
|
||||||
self._start_editor(line=line, column=column)
|
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)
|
@pyqtSlot(str)
|
||||||
def _on_file_changed(self, path):
|
def _on_file_changed(self, path):
|
||||||
try:
|
try:
|
||||||
|
@ -34,6 +34,7 @@ from PyQt5.QtCore import pyqtSlot, pyqtSignal, Qt
|
|||||||
from qutebrowser.config import config
|
from qutebrowser.config import config
|
||||||
from qutebrowser.utils import utils, usertypes
|
from qutebrowser.utils import utils, usertypes
|
||||||
from qutebrowser.commands import cmdutils
|
from qutebrowser.commands import cmdutils
|
||||||
|
from qutebrowser.keyinput import keyutils
|
||||||
|
|
||||||
|
|
||||||
class KeyHintView(QLabel):
|
class KeyHintView(QLabel):
|
||||||
@ -105,9 +106,8 @@ class KeyHintView(QLabel):
|
|||||||
|
|
||||||
bindings_dict = config.key_instance.get_bindings_for(modename)
|
bindings_dict = config.key_instance.get_bindings_for(modename)
|
||||||
bindings = [(k, v) for (k, v) in sorted(bindings_dict.items())
|
bindings = [(k, v) for (k, v) in sorted(bindings_dict.items())
|
||||||
if k.startswith(prefix) and
|
if keyutils.KeySequence.parse(prefix).matches(k) and
|
||||||
not utils.is_special_key(k) and
|
not blacklisted(str(k)) and
|
||||||
not blacklisted(k) and
|
|
||||||
(takes_count(v) or not countstr)]
|
(takes_count(v) or not countstr)]
|
||||||
|
|
||||||
if not bindings:
|
if not bindings:
|
||||||
@ -120,7 +120,7 @@ class KeyHintView(QLabel):
|
|||||||
suffix_color = html.escape(config.val.colors.keyhint.suffix.fg)
|
suffix_color = html.escape(config.val.colors.keyhint.suffix.fg)
|
||||||
|
|
||||||
text = ''
|
text = ''
|
||||||
for key, cmd in bindings:
|
for seq, cmd in bindings:
|
||||||
text += (
|
text += (
|
||||||
"<tr>"
|
"<tr>"
|
||||||
"<td>{}</td>"
|
"<td>{}</td>"
|
||||||
@ -130,7 +130,7 @@ class KeyHintView(QLabel):
|
|||||||
).format(
|
).format(
|
||||||
html.escape(prefix),
|
html.escape(prefix),
|
||||||
suffix_color,
|
suffix_color,
|
||||||
html.escape(key[len(prefix):]),
|
html.escape(str(seq[len(prefix):])),
|
||||||
html.escape(cmd)
|
html.escape(cmd)
|
||||||
)
|
)
|
||||||
text = '<table>{}</table>'.format(text)
|
text = '<table>{}</table>'.format(text)
|
||||||
|
@ -25,8 +25,8 @@ from PyQt5.QtWidgets import (QLineEdit, QWidget, QHBoxLayout, QLabel,
|
|||||||
from PyQt5.QtGui import QValidator, QPainter
|
from PyQt5.QtGui import QValidator, QPainter
|
||||||
|
|
||||||
from qutebrowser.config import config
|
from qutebrowser.config import config
|
||||||
from qutebrowser.utils import utils, qtutils, log, usertypes
|
from qutebrowser.utils import utils
|
||||||
from qutebrowser.misc import cmdhistory, objects
|
from qutebrowser.misc import cmdhistory
|
||||||
|
|
||||||
|
|
||||||
class MinimalLineEditMixin:
|
class MinimalLineEditMixin:
|
||||||
@ -260,16 +260,6 @@ class WrapperLayout(QLayout):
|
|||||||
self._widget = widget
|
self._widget = widget
|
||||||
container.setFocusProxy(widget)
|
container.setFocusProxy(widget)
|
||||||
widget.setParent(container)
|
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):
|
def unwrap(self):
|
||||||
self._widget.setParent(None)
|
self._widget.setParent(None)
|
||||||
@ -293,8 +283,6 @@ class FullscreenNotification(QLabel):
|
|||||||
bindings = all_bindings.get('fullscreen --leave')
|
bindings = all_bindings.get('fullscreen --leave')
|
||||||
if bindings:
|
if bindings:
|
||||||
key = bindings[0]
|
key = bindings[0]
|
||||||
if utils.is_special_key(key):
|
|
||||||
key = key.strip('<>').capitalize()
|
|
||||||
self.setText("Press {} to exit fullscreen.".format(key))
|
self.setText("Press {} to exit fullscreen.".format(key))
|
||||||
else:
|
else:
|
||||||
self.setText("Page is now fullscreen.")
|
self.setText("Page is now fullscreen.")
|
||||||
|
@ -60,7 +60,7 @@ class PastebinClient(QObject):
|
|||||||
self._client = client
|
self._client = client
|
||||||
self._api_url = api_url
|
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.
|
"""Paste the text into a pastebin and return the URL.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@ -68,6 +68,7 @@ class PastebinClient(QObject):
|
|||||||
title: The post title.
|
title: The post title.
|
||||||
text: The text to post.
|
text: The text to post.
|
||||||
parent: The parent paste to reply to.
|
parent: The parent paste to reply to.
|
||||||
|
private: Whether to paste privately.
|
||||||
"""
|
"""
|
||||||
data = {
|
data = {
|
||||||
'text': text,
|
'text': text,
|
||||||
@ -77,6 +78,9 @@ class PastebinClient(QObject):
|
|||||||
}
|
}
|
||||||
if parent is not None:
|
if parent is not None:
|
||||||
data['reply'] = parent
|
data['reply'] = parent
|
||||||
|
if private:
|
||||||
|
data['private'] = '1'
|
||||||
|
|
||||||
url = QUrl(urllib.parse.urljoin(self._api_url, 'create'))
|
url = QUrl(urllib.parse.urljoin(self._api_url, 'create'))
|
||||||
self._client.post(url, data)
|
self._client.post(url, data)
|
||||||
|
|
||||||
|
@ -246,7 +246,7 @@ class SessionManager(QObject):
|
|||||||
if tabbed_browser.private:
|
if tabbed_browser.private:
|
||||||
win_data['private'] = True
|
win_data['private'] = True
|
||||||
for i, tab in enumerate(tabbed_browser.widgets()):
|
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))
|
win_data['tabs'].append(self._save_tab(tab, active))
|
||||||
data['windows'].append(win_data)
|
data['windows'].append(win_data)
|
||||||
return data
|
return data
|
||||||
@ -427,11 +427,12 @@ class SessionManager(QObject):
|
|||||||
if tab.get('active', False):
|
if tab.get('active', False):
|
||||||
tab_to_focus = i
|
tab_to_focus = i
|
||||||
if new_tab.data.pinned:
|
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:
|
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):
|
if win.get('active', False):
|
||||||
QTimer.singleShot(0, tabbed_browser.activateWindow)
|
QTimer.singleShot(0, tabbed_browser.widget.activateWindow)
|
||||||
|
|
||||||
if data['windows']:
|
if data['windows']:
|
||||||
self.did_load = True
|
self.did_load = True
|
||||||
|
@ -185,7 +185,7 @@ def debug_cache_stats():
|
|||||||
tabbed_browser = objreg.get('tabbed-browser', scope='window',
|
tabbed_browser = objreg.get('tabbed-browser', scope='window',
|
||||||
window='last-focused')
|
window='last-focused')
|
||||||
# pylint: disable=protected-access
|
# 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()
|
tabbed_browser_info = tab_bar._minimum_tab_size_hint_helper.cache_info()
|
||||||
# pylint: enable=protected-access
|
# pylint: enable=protected-access
|
||||||
|
|
||||||
|
@ -22,12 +22,10 @@
|
|||||||
import os
|
import os
|
||||||
import os.path
|
import os.path
|
||||||
import contextlib
|
import contextlib
|
||||||
import traceback
|
|
||||||
import mimetypes
|
import mimetypes
|
||||||
import html
|
import html
|
||||||
|
|
||||||
import jinja2
|
import jinja2
|
||||||
import jinja2.exceptions
|
|
||||||
from PyQt5.QtCore import QUrl
|
from PyQt5.QtCore import QUrl
|
||||||
|
|
||||||
from qutebrowser.utils import utils, urlutils, log
|
from qutebrowser.utils import utils, urlutils, log
|
||||||
@ -125,14 +123,7 @@ class Environment(jinja2.Environment):
|
|||||||
|
|
||||||
def render(template, **kwargs):
|
def render(template, **kwargs):
|
||||||
"""Render the given template and pass the given arguments to it."""
|
"""Render the given template and pass the given arguments to it."""
|
||||||
try:
|
|
||||||
return environment.get_template(template).render(**kwargs)
|
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)
|
|
||||||
|
|
||||||
|
|
||||||
environment = Environment()
|
environment = Environment()
|
||||||
|
@ -171,7 +171,7 @@ def _get_tab_registry(win_id, tab_id):
|
|||||||
|
|
||||||
if tab_id == 'current':
|
if tab_id == 'current':
|
||||||
tabbed_browser = get('tabbed-browser', scope='window', window=win_id)
|
tabbed_browser = get('tabbed-browser', scope='window', window=win_id)
|
||||||
tab = tabbed_browser.currentWidget()
|
tab = tabbed_browser.widget.currentWidget()
|
||||||
if tab is None:
|
if tab is None:
|
||||||
raise RegistryUnavailableError('window')
|
raise RegistryUnavailableError('window')
|
||||||
tab_id = tab.tab_id
|
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 collections.abc
|
||||||
import enum
|
import enum
|
||||||
|
|
||||||
|
import attr
|
||||||
from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject, QTimer
|
from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject, QTimer
|
||||||
|
|
||||||
from qutebrowser.utils import log, qtutils, utils
|
from qutebrowser.utils import log, qtutils, utils
|
||||||
@ -394,3 +395,24 @@ class AbstractCertificateErrorWrapper:
|
|||||||
|
|
||||||
def is_overridable(self):
|
def is_overridable(self):
|
||||||
raise NotImplementedError
|
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 sys
|
||||||
import enum
|
import enum
|
||||||
import json
|
import json
|
||||||
import collections
|
|
||||||
import datetime
|
import datetime
|
||||||
import traceback
|
import traceback
|
||||||
import functools
|
import functools
|
||||||
import contextlib
|
import contextlib
|
||||||
import socket
|
import socket
|
||||||
import shlex
|
import shlex
|
||||||
|
import glob
|
||||||
|
|
||||||
import attr
|
from PyQt5.QtCore import QUrl
|
||||||
from PyQt5.QtCore import Qt, QUrl
|
from PyQt5.QtGui import QColor, QClipboard, QDesktopServices
|
||||||
from PyQt5.QtGui import QKeySequence, QColor, QClipboard, QDesktopServices
|
|
||||||
from PyQt5.QtWidgets import QApplication
|
from PyQt5.QtWidgets import QApplication
|
||||||
import pkg_resources
|
import pkg_resources
|
||||||
import yaml
|
import yaml
|
||||||
@ -48,11 +47,12 @@ except ImportError: # pragma: no cover
|
|||||||
YAML_C_EXT = False
|
YAML_C_EXT = False
|
||||||
|
|
||||||
import qutebrowser
|
import qutebrowser
|
||||||
from qutebrowser.utils import qtutils, log, debug
|
from qutebrowser.utils import qtutils, log
|
||||||
|
|
||||||
|
|
||||||
fake_clipboard = None
|
fake_clipboard = None
|
||||||
log_clipboard = False
|
log_clipboard = False
|
||||||
|
_resource_cache = {}
|
||||||
|
|
||||||
is_mac = sys.platform.startswith('darwin')
|
is_mac = sys.platform.startswith('darwin')
|
||||||
is_linux = sys.platform.startswith('linux')
|
is_linux = sys.platform.startswith('linux')
|
||||||
@ -142,6 +142,15 @@ def compact_text(text, elidelength=None):
|
|||||||
return out
|
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):
|
def read_file(filename, binary=False):
|
||||||
"""Get the contents of a file contained with qutebrowser.
|
"""Get the contents of a file contained with qutebrowser.
|
||||||
|
|
||||||
@ -153,6 +162,9 @@ def read_file(filename, binary=False):
|
|||||||
Return:
|
Return:
|
||||||
The file contents as string.
|
The file contents as string.
|
||||||
"""
|
"""
|
||||||
|
if not binary and filename in _resource_cache:
|
||||||
|
return _resource_cache[filename]
|
||||||
|
|
||||||
if hasattr(sys, 'frozen'):
|
if hasattr(sys, 'frozen'):
|
||||||
# PyInstaller doesn't support pkg_resources :(
|
# PyInstaller doesn't support pkg_resources :(
|
||||||
# https://github.com/pyinstaller/pyinstaller/wiki/FAQ#misc
|
# 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)
|
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):
|
class FakeIOStream(io.TextIOBase):
|
||||||
|
|
||||||
"""A fake file-like stream which calls a function for write-calls."""
|
"""A fake file-like stream which calls a function for write-calls."""
|
||||||
@ -915,3 +670,14 @@ def yaml_dump(data, f=None):
|
|||||||
return None
|
return None
|
||||||
else:
|
else:
|
||||||
return yaml_data.decode('utf-8')
|
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:
|
else:
|
||||||
versioninfo = '.'.join(versioninfo)
|
versioninfo = '.'.join(versioninfo)
|
||||||
osver = ', '.join([e for e in [release, versioninfo, machine] if e])
|
osver = ', '.join([e for e in [release, versioninfo, machine] if e])
|
||||||
|
elif utils.is_posix:
|
||||||
|
osver = ' '.join(platform.uname())
|
||||||
else:
|
else:
|
||||||
osver = '?'
|
osver = '?'
|
||||||
lines.append('OS Version: {}'.format(osver))
|
lines.append('OS Version: {}'.format(osver))
|
||||||
@ -305,7 +307,19 @@ def _pdfjs_version():
|
|||||||
|
|
||||||
|
|
||||||
def _chromium_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:
|
if QWebEngineProfile is None:
|
||||||
# This should never happen
|
# This should never happen
|
||||||
return 'unavailable'
|
return 'unavailable'
|
||||||
@ -441,7 +455,13 @@ def opengl_vendor(): # pragma: no cover
|
|||||||
vp = QOpenGLVersionProfile()
|
vp = QOpenGLVersionProfile()
|
||||||
vp.setVersion(2, 0)
|
vp.setVersion(2, 0)
|
||||||
|
|
||||||
|
try:
|
||||||
vf = ctx.versionFunctions(vp)
|
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:
|
if vf is None:
|
||||||
log.init.debug("opengl_vendor: Getting version functions failed!")
|
log.init.debug("opengl_vendor: Getting version functions failed!")
|
||||||
return None
|
return None
|
||||||
@ -453,7 +473,7 @@ def opengl_vendor(): # pragma: no cover
|
|||||||
old_context.makeCurrent(old_surface)
|
old_context.makeCurrent(old_surface)
|
||||||
|
|
||||||
|
|
||||||
def pastebin_version():
|
def pastebin_version(pbclient=None):
|
||||||
"""Pastebin the version and log the url to messages."""
|
"""Pastebin the version and log the url to messages."""
|
||||||
def _yank_url(url):
|
def _yank_url(url):
|
||||||
utils.set_clipboard(url)
|
utils.set_clipboard(url)
|
||||||
@ -478,7 +498,7 @@ def pastebin_version():
|
|||||||
http_client = httpclient.HTTPClient()
|
http_client = httpclient.HTTPClient()
|
||||||
|
|
||||||
misc_api = pastebin.PastebinClient.MISC_API_URL
|
misc_api = pastebin.PastebinClient.MISC_API_URL
|
||||||
pbclient = pastebin.PastebinClient(http_client, parent=app,
|
pbclient = pbclient or pastebin.PastebinClient(http_client, parent=app,
|
||||||
api_url=misc_api)
|
api_url=misc_api)
|
||||||
|
|
||||||
pbclient.success.connect(_on_paste_version_success)
|
pbclient.success.connect(_on_paste_version_success)
|
||||||
@ -486,4 +506,5 @@ def pastebin_version():
|
|||||||
|
|
||||||
pbclient.paste(getpass.getuser(),
|
pbclient.paste(getpass.getuser(),
|
||||||
"qute version info {}".format(qutebrowser.__version__),
|
"qute version info {}".format(qutebrowser.__version__),
|
||||||
version())
|
version(),
|
||||||
|
private=True)
|
||||||
|
@ -85,9 +85,9 @@ class AsciiDoc:
|
|||||||
|
|
||||||
# patch image links to use local copy
|
# patch image links to use local copy
|
||||||
replacements = [
|
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"),
|
"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")
|
"qute://help/img/cheatsheet-small.png")
|
||||||
]
|
]
|
||||||
asciidoc_args = ['-a', 'source-highlighter=pygments']
|
asciidoc_args = ['-a', 'source-highlighter=pygments']
|
||||||
|
@ -24,6 +24,7 @@
|
|||||||
import os
|
import os
|
||||||
import os.path
|
import os.path
|
||||||
import sys
|
import sys
|
||||||
|
import time
|
||||||
import glob
|
import glob
|
||||||
import shutil
|
import shutil
|
||||||
import plistlib
|
import plistlib
|
||||||
@ -195,6 +196,7 @@ def build_mac():
|
|||||||
'MacOS', 'qutebrowser')
|
'MacOS', 'qutebrowser')
|
||||||
smoke_test(binary)
|
smoke_test(binary)
|
||||||
finally:
|
finally:
|
||||||
|
time.sleep(5)
|
||||||
subprocess.run(['hdiutil', 'detach', tmpdir])
|
subprocess.run(['hdiutil', 'detach', tmpdir])
|
||||||
except PermissionError as e:
|
except PermissionError as e:
|
||||||
print("Failed to remove tempdir: {}".format(e))
|
print("Failed to remove tempdir: {}".format(e))
|
||||||
@ -359,7 +361,7 @@ def github_upload(artifacts, tag):
|
|||||||
repo = gh.repository('qutebrowser', 'qutebrowser')
|
repo = gh.repository('qutebrowser', 'qutebrowser')
|
||||||
|
|
||||||
release = None # to satisfy pylint
|
release = None # to satisfy pylint
|
||||||
for release in repo.iter_releases():
|
for release in repo.releases():
|
||||||
if release.tag_name == tag:
|
if release.tag_name == tag:
|
||||||
break
|
break
|
||||||
else:
|
else:
|
||||||
|
@ -86,6 +86,8 @@ PERFECT_FILES = [
|
|||||||
|
|
||||||
('tests/unit/keyinput/test_basekeyparser.py',
|
('tests/unit/keyinput/test_basekeyparser.py',
|
||||||
'keyinput/basekeyparser.py'),
|
'keyinput/basekeyparser.py'),
|
||||||
|
('tests/unit/keyinput/test_keyutils.py',
|
||||||
|
'keyinput/keyutils.py'),
|
||||||
|
|
||||||
('tests/unit/misc/test_autoupdate.py',
|
('tests/unit/misc/test_autoupdate.py',
|
||||||
'misc/autoupdate.py'),
|
'misc/autoupdate.py'),
|
||||||
@ -143,6 +145,8 @@ PERFECT_FILES = [
|
|||||||
'config/configinit.py'),
|
'config/configinit.py'),
|
||||||
('tests/unit/config/test_configcommands.py',
|
('tests/unit/config/test_configcommands.py',
|
||||||
'config/configcommands.py'),
|
'config/configcommands.py'),
|
||||||
|
('tests/unit/config/test_configutils.py',
|
||||||
|
'config/configutils.py'),
|
||||||
|
|
||||||
('tests/unit/utils/test_qtutils.py',
|
('tests/unit/utils/test_qtutils.py',
|
||||||
'utils/qtutils.py'),
|
'utils/qtutils.py'),
|
||||||
@ -164,11 +168,15 @@ PERFECT_FILES = [
|
|||||||
'utils/error.py'),
|
'utils/error.py'),
|
||||||
('tests/unit/utils/test_javascript.py',
|
('tests/unit/utils/test_javascript.py',
|
||||||
'utils/javascript.py'),
|
'utils/javascript.py'),
|
||||||
|
('tests/unit/utils/test_urlmatch.py',
|
||||||
|
'utils/urlmatch.py'),
|
||||||
|
|
||||||
(None,
|
(None,
|
||||||
'completion/models/util.py'),
|
'completion/models/util.py'),
|
||||||
('tests/unit/completion/test_models.py',
|
('tests/unit/completion/test_models.py',
|
||||||
'completion/models/urlmodel.py'),
|
'completion/models/urlmodel.py'),
|
||||||
|
('tests/unit/completion/test_models.py',
|
||||||
|
'completion/models/configmodel.py'),
|
||||||
('tests/unit/completion/test_histcategory.py',
|
('tests/unit/completion/test_histcategory.py',
|
||||||
'completion/models/histcategory.py'),
|
'completion/models/histcategory.py'),
|
||||||
('tests/unit/completion/test_listcategory.py',
|
('tests/unit/completion/test_listcategory.py',
|
||||||
|
@ -83,7 +83,9 @@ elif [[ $TRAVIS_OS_NAME == osx ]]; then
|
|||||||
sudo -H python get-pip.py
|
sudo -H python get-pip.py
|
||||||
|
|
||||||
brew --version
|
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
|
pip_install -r misc/requirements/requirements-tox.txt
|
||||||
python3 -m pip --version
|
python3 -m pip --version
|
||||||
@ -101,5 +103,8 @@ case $TESTENV in
|
|||||||
*)
|
*)
|
||||||
pip_install pip
|
pip_install pip
|
||||||
pip_install -r misc/requirements/requirements-tox.txt
|
pip_install -r misc/requirements/requirements-tox.txt
|
||||||
|
if [[ $TESTENV == *-cov ]]; then
|
||||||
|
pip_install -r misc/requirements/requirements-codecov.txt
|
||||||
|
fi
|
||||||
;;
|
;;
|
||||||
esac
|
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