Compare commits
341 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7762017f00 | ||
|
|
2ba40acf74 | ||
|
|
388c155ebb | ||
|
|
adf2f9860d | ||
|
|
f528a5dd21 | ||
|
|
f0f1a4a1d1 | ||
|
|
ae295a7f65 | ||
|
|
68794cc2e2 | ||
|
|
acdf0a1c60 | ||
|
|
626abd3c83 | ||
|
|
979b7cfaba | ||
|
|
2b6b4e82a7 | ||
|
|
80778a9ff3 | ||
|
|
6eb8284fe0 | ||
|
|
49bdcd5a97 | ||
|
|
106e591a36 | ||
|
|
a70f864ff5 | ||
|
|
4932cc4d24 | ||
|
|
2b5f133726 | ||
|
|
5436e27e41 | ||
|
|
4adf10a2f2 | ||
|
|
b80fa7a197 | ||
|
|
3cc790afb3 | ||
|
|
91aa9f6c0c | ||
|
|
d6cacdb42f | ||
|
|
19554ba4a1 | ||
|
|
d16d9e403a | ||
|
|
432d666d25 | ||
|
|
1c3ee0db20 | ||
|
|
bdc0c0ddc1 | ||
|
|
b9c8a79f10 | ||
|
|
801e9e0334 | ||
|
|
cfa5ee2835 | ||
|
|
9c6437b3b9 | ||
|
|
486488e2cd | ||
|
|
b4f877d991 | ||
|
|
92aedf84f5 | ||
|
|
6825dfb8d8 | ||
|
|
d6c6014b85 | ||
|
|
c1ac1d702f | ||
|
|
9e50b7afcc | ||
|
|
1388880e7b | ||
|
|
30d60ea740 | ||
|
|
28cac01a1f | ||
|
|
1689cb09f8 | ||
|
|
286c71a48a | ||
|
|
c073234a8d | ||
|
|
8c286412cb | ||
|
|
b3cef948b0 | ||
|
|
fa41af63b6 | ||
|
|
e789296b7f | ||
|
|
bc9a8dd63f | ||
|
|
6640768860 | ||
|
|
c94ea5f8d4 | ||
|
|
e2d249541d | ||
|
|
2de6428830 | ||
|
|
c2472d88f1 | ||
|
|
1d2dd5bf55 | ||
|
|
178eeaed0d | ||
|
|
f1967718b7 | ||
|
|
cbb246fd0b | ||
|
|
646e92707a | ||
|
|
1021c3a330 | ||
|
|
ec57e58530 | ||
|
|
06a8a68fcb | ||
|
|
4a78519b63 | ||
|
|
d2207f66f1 | ||
|
|
23d4d72f3b | ||
|
|
4a93389356 | ||
|
|
3704e3ddd5 | ||
|
|
643bf4bc20 | ||
|
|
b89b38fd3c | ||
|
|
c5334fb683 | ||
|
|
0829511221 | ||
|
|
48b865073c | ||
|
|
3f9099613b | ||
|
|
77fa0730c8 | ||
|
|
ed76d689b0 | ||
|
|
ca311f8588 | ||
|
|
849e427231 | ||
|
|
9e628901e9 | ||
|
|
28126055da | ||
|
|
3d75d86123 | ||
|
|
03ea07e99f | ||
|
|
780ced8a52 | ||
|
|
fc33b065c2 | ||
|
|
03b7459b00 | ||
|
|
f964bf1b67 | ||
|
|
ef2a2702f5 | ||
|
|
6374b6dd4c | ||
|
|
69d642cab8 | ||
|
|
62aa9bdbb3 | ||
|
|
e35c91043e | ||
|
|
fac546e9b4 | ||
|
|
b74ddc3493 | ||
|
|
b7964d9baf | ||
|
|
76dbfa7305 | ||
|
|
feb2f99ea9 | ||
|
|
d0d5ad2eda | ||
|
|
eb18f0a2ac | ||
|
|
39c08cb582 | ||
|
|
164ea98a5b | ||
|
|
3b2c0823af | ||
|
|
9ad6cef369 | ||
|
|
423192e9c9 | ||
|
|
79823a9a0b | ||
|
|
1c0616f3ec | ||
|
|
599a3d75a4 | ||
|
|
6151d077e5 | ||
|
|
d438aa15fa | ||
|
|
7f5a79cdfd | ||
|
|
5bc794f85a | ||
|
|
d7455bcdba | ||
|
|
0b667e4701 | ||
|
|
d606cd5550 | ||
|
|
7c1de99876 | ||
|
|
2789bec1e7 | ||
|
|
1ccb464d1c | ||
|
|
758ea8b171 | ||
|
|
32568a6da4 | ||
|
|
14792472db | ||
|
|
005fa8b675 | ||
|
|
c7e5033eaa | ||
|
|
a828851640 | ||
|
|
b873cfb18a | ||
|
|
046a3dc159 | ||
|
|
68b707c749 | ||
|
|
9cff0e7367 | ||
|
|
7e66c3cf46 | ||
|
|
bf4aab79ac | ||
|
|
a6f6fdf19b | ||
|
|
e5ffcbd49f | ||
|
|
730347e449 | ||
|
|
55112b52e0 | ||
|
|
2249a88e3a | ||
|
|
6dbd6d1ddf | ||
|
|
6ecea8ef17 | ||
|
|
021bb25622 | ||
|
|
22e887045b | ||
|
|
4896765fcc | ||
|
|
d98590d712 | ||
|
|
496e6fc624 | ||
|
|
55a818b156 | ||
|
|
85be0f2801 | ||
|
|
b7b4cc7f31 | ||
|
|
056a901da0 | ||
|
|
ba9b166962 | ||
|
|
edf2652431 | ||
|
|
6755e17630 | ||
|
|
d4899240de | ||
|
|
ddbb6b5198 | ||
|
|
1087ce075d | ||
|
|
cecb79cf05 | ||
|
|
d4ea1df232 | ||
|
|
91ca7d0911 | ||
|
|
68e3ad6cba | ||
|
|
12a405965a | ||
|
|
a85ac1725f | ||
|
|
eb5684e5f7 | ||
|
|
b9bcad9c14 | ||
|
|
64b01cc076 | ||
|
|
1380fef600 | ||
|
|
8809ef02a1 | ||
|
|
3f37fcf8fa | ||
|
|
c8db9e1c76 | ||
|
|
01d8314dd8 | ||
|
|
fa21d280fa | ||
|
|
e211801e16 | ||
|
|
6db1ab0a58 | ||
|
|
aa70395925 | ||
|
|
880b33fff5 | ||
|
|
f1789effdc | ||
|
|
e095f64eb6 | ||
|
|
06df88075e | ||
|
|
00bdb60627 | ||
|
|
1fc0abb064 | ||
|
|
a1776087e0 | ||
|
|
948866f4f2 | ||
|
|
e2250d65e9 | ||
|
|
bb0c79b5a2 | ||
|
|
ff299c87a8 | ||
|
|
2d2bdad2ca | ||
|
|
07d043fe81 | ||
|
|
477da6002a | ||
|
|
d2c01d7ee6 | ||
|
|
b67a031151 | ||
|
|
a9a7f5da45 | ||
|
|
b169a1c802 | ||
|
|
764e79e505 | ||
|
|
7b7faa9f66 | ||
|
|
a6b92dbbd3 | ||
|
|
4fb940241c | ||
|
|
51f9464eb2 | ||
|
|
0645865d22 | ||
|
|
991ba54499 | ||
|
|
1cf3d66a22 | ||
|
|
300d873b18 | ||
|
|
a8bbd5fa4d | ||
|
|
2d655a7230 | ||
|
|
4d7f8e4878 | ||
|
|
bc885cc9ee | ||
|
|
a5dc8a3025 | ||
|
|
d6463d5ade | ||
|
|
7601e58c81 | ||
|
|
16bda94e2b | ||
|
|
5a20052bce | ||
|
|
e316f1768e | ||
|
|
11696f0073 | ||
|
|
f9d976880e | ||
|
|
a5f1022330 | ||
|
|
85d3d4baba | ||
|
|
561295238d | ||
|
|
0e670a597e | ||
|
|
0fd3674d9e | ||
|
|
0ee9d73fe2 | ||
|
|
81827a3150 | ||
|
|
32145d579b | ||
|
|
f230fd3abb | ||
|
|
51318d66c8 | ||
|
|
59602ec5b5 | ||
|
|
a374698693 | ||
|
|
b56988f0a4 | ||
|
|
32df91fbae | ||
|
|
f6c00babbe | ||
|
|
7eaad59be3 | ||
|
|
ea1e52ea7c | ||
|
|
b588f54a53 | ||
|
|
9031b3e535 | ||
|
|
1162e640c5 | ||
|
|
0ea7a1457d | ||
|
|
f5d7605ae0 | ||
|
|
e50068021d | ||
|
|
460bd86579 | ||
|
|
e43f0a61b9 | ||
|
|
8eb4d15805 | ||
|
|
3b7e1b3fe2 | ||
|
|
650aa532cd | ||
|
|
1f3fc756db | ||
|
|
2d5d485daf | ||
|
|
b77e43d74f | ||
|
|
5a26858e07 | ||
|
|
99ea4b98e8 | ||
|
|
da8b6fb50a | ||
|
|
33066af51d | ||
|
|
6a971e2846 | ||
|
|
f28a39571c | ||
|
|
bee04a1eec | ||
|
|
39d25c1127 | ||
|
|
07e831cee5 | ||
|
|
b3342d8f70 | ||
|
|
6465d64738 | ||
|
|
232fd19422 | ||
|
|
a4530797ea | ||
|
|
1b84bbd61d | ||
|
|
f2864c6253 | ||
|
|
8ae3047f2a | ||
|
|
b154846bdc | ||
|
|
8a3d9c0c01 | ||
|
|
62d30fe589 | ||
|
|
a52d18b700 | ||
|
|
f9e702bae5 | ||
|
|
f57e47c742 | ||
|
|
18146e2fbc | ||
|
|
f7074b80d0 | ||
|
|
fa282d574d | ||
|
|
3b0b4ffe66 | ||
|
|
a6ce188e0d | ||
|
|
01845faac5 | ||
|
|
69a013bc82 | ||
|
|
6f8eb419ae | ||
|
|
1e4b80d1ac | ||
|
|
5dbda3016b | ||
|
|
1d25b212d5 | ||
|
|
f538fc8b74 | ||
|
|
c2b995edde | ||
|
|
1d562d919e | ||
|
|
a60bae30b7 | ||
|
|
523502785a | ||
|
|
84c7c37e8e | ||
|
|
d232b3ea57 | ||
|
|
7a861b7119 | ||
|
|
a22f973c99 | ||
|
|
724e531087 | ||
|
|
5c73910a33 | ||
|
|
84bae210ab | ||
|
|
e5edc0f940 | ||
|
|
0d21265005 | ||
|
|
c0fdf19756 | ||
|
|
102b2be361 | ||
|
|
fac0f66e52 | ||
|
|
2563ecf6d8 | ||
|
|
64530375ab | ||
|
|
7278b7c2e5 | ||
|
|
35beff98a9 | ||
|
|
a6e94cf30c | ||
|
|
8e01353a94 | ||
|
|
8b9c6ccee2 | ||
|
|
b88ac51d25 | ||
|
|
73517f0a51 | ||
|
|
27966c94a6 | ||
|
|
a7b6d179d4 | ||
|
|
1c9598d2c0 | ||
|
|
dcd6bcd2f4 | ||
|
|
c590648077 | ||
|
|
80843c0b53 | ||
|
|
14d6e737fa | ||
|
|
9c613fb700 | ||
|
|
01aa1f755d | ||
|
|
3855d49821 | ||
|
|
55c24cad9a | ||
|
|
38bb3673db | ||
|
|
455f6b8a70 | ||
|
|
8c5b7bcd03 | ||
|
|
a6885a0d41 | ||
|
|
9941812127 | ||
|
|
990c0707f4 | ||
|
|
c03ef10d54 | ||
|
|
d72691ee49 | ||
|
|
29eadf7141 | ||
|
|
27c2650245 | ||
|
|
b0bf02e23a | ||
|
|
30ab1d0218 | ||
|
|
7f68affa30 | ||
|
|
b6e29d8eae | ||
|
|
591883656e | ||
|
|
f0a649e101 | ||
|
|
d0342bffc4 | ||
|
|
75ab8f077d | ||
|
|
d9f7d401c6 | ||
|
|
996561b50e | ||
|
|
4cf0311d7f | ||
|
|
cf4e472461 | ||
|
|
0ce94dae1c | ||
|
|
7e3c966afe | ||
|
|
1672995639 | ||
|
|
4a78b0519d | ||
|
|
46533c3367 | ||
|
|
a730290d40 | ||
|
|
16218a9900 | ||
|
|
e169e2165d | ||
|
|
42ac3dcda0 |
12
.flake8
@@ -32,7 +32,7 @@ exclude = .*,__pycache__,resources.py
|
||||
# D403: First word of the first line should be properly capitalized
|
||||
# (false-positives)
|
||||
# D413: Missing blank line after last section (not in pep257?)
|
||||
# A003: Builtin name for class attribute (needed for attrs)
|
||||
# A003: Builtin name for class attribute (needed for overridden methods)
|
||||
ignore =
|
||||
B001,B008,B305,
|
||||
E128,E226,E265,E501,E402,E266,E722,E731,
|
||||
@@ -44,11 +44,11 @@ ignore =
|
||||
min-version = 3.4.0
|
||||
max-complexity = 12
|
||||
per-file-ignores =
|
||||
tests/*/test_*.py : D100,D101,D401
|
||||
tests/unit/browser/test_history.py : N806
|
||||
tests/helpers/fixtures.py : N806
|
||||
tests/unit/browser/webkit/http/test_content_disposition.py : D400
|
||||
scripts/dev/ci/appveyor_install.py : FI53
|
||||
/tests/**/test_*.py : D100,D101,D401
|
||||
/tests/unit/browser/test_history.py : N806
|
||||
/tests/helpers/fixtures.py : N806
|
||||
/tests/unit/browser/webkit/http/test_content_disposition.py : D400
|
||||
/scripts/dev/ci/appveyor_install.py : FI53
|
||||
copyright-check = True
|
||||
copyright-regexp = # Copyright [\d-]+ .*
|
||||
copyright-min-file-size = 110
|
||||
|
||||
@@ -8,6 +8,7 @@ graft icons
|
||||
graft doc/img
|
||||
graft misc/apparmor
|
||||
graft misc/userscripts
|
||||
graft misc/requirements
|
||||
recursive-include scripts *.py *.sh *.js
|
||||
include qutebrowser/utils/testfile
|
||||
include qutebrowser/git-commit-id
|
||||
@@ -32,8 +33,6 @@ include doc/qutebrowser.1.asciidoc
|
||||
include doc/changelog.asciidoc
|
||||
prune tests
|
||||
prune qutebrowser/3rdparty
|
||||
prune misc/requirements
|
||||
prune misc/docker
|
||||
exclude pytest.ini
|
||||
exclude qutebrowser.rcc
|
||||
exclude qutebrowser/javascript/.eslintrc.yaml
|
||||
|
||||
@@ -99,7 +99,7 @@ Requirements
|
||||
The following software and libraries are required to run qutebrowser:
|
||||
|
||||
* http://www.python.org/[Python] 3.5 or newer (3.6 recommended)
|
||||
* http://qt.io/[Qt] 5.7.1 or newer with the following modules:
|
||||
* http://qt.io/[Qt] 5.7.1 or newer (5.10 recommended) with the following modules:
|
||||
- QtCore / qtbase
|
||||
- QtQuick (part of qtbase in some distributions)
|
||||
- QtSQL (part of qtbase in some distributions)
|
||||
@@ -109,7 +109,7 @@ The following software and libraries are required to run qutebrowser:
|
||||
link:https://github.com/annulen/webkit/wiki[updated fork] (5.212) is
|
||||
supported
|
||||
* http://www.riverbankcomputing.com/software/pyqt/intro[PyQt] 5.7.0 or newer
|
||||
(5.9.2 recommended) for Python 3
|
||||
(5.10 recommended) for Python 3
|
||||
* https://pypi.python.org/pypi/setuptools/[pkg_resources/setuptools]
|
||||
* http://fdik.org/pyPEG/[pyPEG2]
|
||||
* http://jinja.pocoo.org/[jinja2]
|
||||
|
||||
@@ -15,6 +15,109 @@ breaking changes (such as renamed commands) can happen in minor releases.
|
||||
// `Fixed` for any bug fixes.
|
||||
// `Security` to invite users to upgrade in case of vulnerabilities.
|
||||
|
||||
v1.3.0
|
||||
------
|
||||
|
||||
Added
|
||||
~~~~~
|
||||
|
||||
- New `:scroll-to-anchor` command to scroll to an anchor in the document.
|
||||
- New `url.open_base_url` option to open the base URL of a searchengine when no
|
||||
search term is given.
|
||||
- New `tabs.min_width` setting to configure the minimal width for tabs.
|
||||
- New userscripts:
|
||||
* `getbib` to download bibtex information for DOIs on a page.
|
||||
* `qute-keepass` to get passwords from KeePassX.
|
||||
|
||||
Changed
|
||||
~~~~~~~
|
||||
|
||||
- QtWebEngine: Support for JavaScript Shared Web Workers have been disabled on
|
||||
Qt versions older than 5.11 because of security issues in in Chromium.
|
||||
You can get the same effect in earlier versions via
|
||||
`:set qt.args ['disable-shared-workers']`. An equivalent workaround is also
|
||||
contained in Qt 5.9.5 and 5.10.1.
|
||||
- The file dialog for downloads now has basic tab completion based on the
|
||||
entered text.
|
||||
- `:version` now shows OS information for POSIX OS other than Linux/macOS.
|
||||
- When there's an error inserting the text from an external editor, a backup
|
||||
file is now saved.
|
||||
- The `window.hide_wayland_decoration` setting got renamed to
|
||||
`window.hide_decoration` and now also works outside of wayland.
|
||||
- The `tabs.favicons.show` setting now can take three values: `'always'` (was
|
||||
`True`), `'never'` (was `False`) and `'pinned'` (to only show favicons for
|
||||
pinned tabs).
|
||||
- Hover tooltips on tabs now always show the webpage's title.
|
||||
- The default value for `content.host_blocking.lists` was changed to only
|
||||
include https://github.com/StevenBlack/hosts[Steven Black's hosts-list] which
|
||||
combines various sources.
|
||||
- Error messages when trying to wrap when `tabs.wrap` is `False` are now logged
|
||||
to debug instead of messages.
|
||||
|
||||
Fixed
|
||||
~~~~~
|
||||
|
||||
- Using hints before a page is fully loaded is now possible again.
|
||||
- Selecting hints with the number keypad now works again.
|
||||
- Tab titles for tabs loaded from sessions should now really be correct instead
|
||||
of showing the URL.
|
||||
- Loading URLs with customized settings from a session now avoids an additional
|
||||
reload.
|
||||
- The window icon and title now get set correctly again.
|
||||
- The `tabs.switching_delay` setting now has a correct maximum value limit set.
|
||||
- The `taskadd` script now works properly when there's multi-line output.
|
||||
- QtWebEngine: Worked around issues with GreaseMonkey/stylesheets not being
|
||||
loaded correctly in some situations.
|
||||
- The statusbar now more closely reflects the caret mode state.
|
||||
- The icon on Windows should now be displayed in a higher resolution.
|
||||
- The QtWebEngine development tools (inspector) now also work when JavaScript is
|
||||
disabled globally.
|
||||
- Building `.exe` files now works when `upx` is installed on the system.
|
||||
- The keyhint widget now shows the correct text for chained modifiers.
|
||||
- Loading GreaseMonkey scripts now also works with Jinja2 2.8 (e.g. on Debian
|
||||
Stable).
|
||||
- Adding styles with GreaseMonkey on fast sites now works properly.
|
||||
- Window ID 0 is now excluded properly from `:tab-take` completion.
|
||||
- A rare crash when cancelling a download has been fixed.
|
||||
- The Makefile (intended for packagers) now supports `PREFIX` properly.
|
||||
- The workaround for a black window with Nvidia graphics is now enabled on
|
||||
non-Linux systems (like FreeBSD) as well.
|
||||
- Initial support for Qt 5.11.
|
||||
- Checking for a new version after sending a crash report now works properly
|
||||
again.
|
||||
- `@match` in Greasemonkey scripts now more closely matches the proper pattern
|
||||
syntax.
|
||||
- Searching via `/` or `?` now doesn't handle any characters in a special way.
|
||||
- Fixed crash when trying to retry some failed downloads on QtWebEngine.
|
||||
- An invalid spellcheck dictionary filename now doesn't crash anymore.
|
||||
- When no spellcheck dictionaries are configured, it's now disabled internally.
|
||||
This works around an issue with entering special characters on Facebook
|
||||
messenger.
|
||||
- The macOS release now should work again on macOS 10.11 and newer.
|
||||
|
||||
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
|
||||
------
|
||||
|
||||
|
||||
@@ -670,10 +670,11 @@ qutebrowser release
|
||||
~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
* Make sure there are no unstaged changes and the tests are green.
|
||||
* Make sure all issues with the related milestone are closed.
|
||||
* Run `x=... y=...` to set the respective shell variables.
|
||||
|
||||
* Adjust `__version_info__` in `qutebrowser/__init__.py`.
|
||||
* Update changelog (remove *(unreleased)*).
|
||||
* Adjust `__version_info__` in `qutebrowser/__init__.py`.
|
||||
* Commit.
|
||||
|
||||
* Create annotated git tag (`git tag -s "v1.$x.$y" -m "Release v1.$x.$y"`).
|
||||
@@ -683,9 +684,11 @@ qutebrowser release
|
||||
* Mark the milestone at https://github.com/qutebrowser/qutebrowser/milestones
|
||||
as closed.
|
||||
|
||||
* Linux: Run `git checkout v1.$x.$y && python3 scripts/dev/build_release.py --upload v1.$x.$y`.
|
||||
* Linux: Run `git checkout v1.$x.$y && ./.venv/bin/python3 scripts/dev/build_release.py --upload v1.$x.$y`.
|
||||
* Windows: Run `git checkout v1.X.Y; C:\Python36-32\python scripts\dev\build_release.py --asciidoc C:\Python27\python C:\asciidoc-8.6.9\asciidoc.py --upload v1.X.Y` (replace X/Y by hand).
|
||||
* macOS: Run `git checkout v1.X.Y && python3 scripts/dev/build_release.py --upload v1.X.Y` (replace X/Y by hand).
|
||||
* On server: Run `python3 scripts/dev/download_release.py v1.X.Y` (replace X/Y by hand).
|
||||
* On server:
|
||||
- Run `python3 scripts/dev/download_release.py v1.X.Y` (replace X/Y by hand).
|
||||
- Run `git pull github master && sudo python3 scripts/asciidoc2html.py --website /srv/http/qutebrowser`
|
||||
* Update `qutebrowser-git` PKGBUILD if dependencies/install changed.
|
||||
* Announce to qutebrowser and qutebrowser-announce mailinglist.
|
||||
|
||||
@@ -212,6 +212,37 @@ Why takes it longer to open an URL in qutebrowser than in chromium?::
|
||||
qutebrowser if it is not running already. Also check if you want
|
||||
to use webengine as backend in line 17 and change it to your
|
||||
needs.
|
||||
|
||||
How do I make qutebrowser use greasemonkey scripts?::
|
||||
There is currently no UI elements to handle managing greasemonkey scripts.
|
||||
All management of what scripts are installed or disabled is done in the
|
||||
filesystem by you. qutebrowser reads all files that have an extension of
|
||||
`.js` from the `<data>/greasemonkey/` folder and attempts to load them.
|
||||
Where `<data>` is the qutebrowser data directory shown in the `Paths`
|
||||
section of the page displayed by `:version`. If you want to disable a
|
||||
script just rename it, for example, to have `.disabled` on the end, after
|
||||
the `.js` extension. To reload scripts from that directory run the command
|
||||
`:greasemonkey-reload`.
|
||||
+
|
||||
Troubleshooting: to check that your script is being loaded when
|
||||
`:greasemonkey-reload` runs you can start qutebrowser with the arguments
|
||||
`--debug --logfilter greasemonkey,js` and check the messages on the
|
||||
program's standard output for errors parsing or loading your script.
|
||||
You may also see javascript errors if your script is expecting an environment
|
||||
that we fail to provide.
|
||||
+
|
||||
Note that there are some missing features which you may run into:
|
||||
|
||||
. Some scripts expect `GM_xmlhttpRequest` to ignore Cross Origin Resource
|
||||
Sharing restrictions, this is currently not supported, so scripts making
|
||||
requests to third party sites will often fail to function correctly.
|
||||
. If your backend is a QtWebEngine version 5.8, 5.9 or 5.10 then regular
|
||||
expressions are not supported in `@include` or `@exclude` rules. If your
|
||||
script uses them you can re-write them to use glob expressions or convert
|
||||
them to `@match` rules.
|
||||
See https://wiki.greasespot.net/Metadata_Block[the wiki] for more info.
|
||||
. Any greasemonkey API function to do with adding UI elements is not currently
|
||||
supported. That means context menu extentensions and background pages.
|
||||
|
||||
== Troubleshooting
|
||||
|
||||
|
||||
@@ -93,6 +93,7 @@ It is possible to run or bind multiple commands by separating them with `;;`.
|
||||
|<<scroll,scroll>>|Scroll the current tab in the given direction.
|
||||
|<<scroll-page,scroll-page>>|Scroll the frame page-wise.
|
||||
|<<scroll-px,scroll-px>>|Scroll the current tab by 'count * dx/dy' pixels.
|
||||
|<<scroll-to-anchor,scroll-to-anchor>>|Scroll to the given anchor in the document.
|
||||
|<<scroll-to-perc,scroll-to-perc>>|Scroll to a specific percentage of the page.
|
||||
|<<search,search>>|Search for a text on the current page. With no text, clear results.
|
||||
|<<search-next,search-next>>|Continue the search to the ([count]th) next term.
|
||||
@@ -1024,6 +1025,15 @@ Scroll the current tab by 'count * dx/dy' pixels.
|
||||
==== count
|
||||
multiplier
|
||||
|
||||
[[scroll-to-anchor]]
|
||||
=== scroll-to-anchor
|
||||
Syntax: +:scroll-to-anchor 'name'+
|
||||
|
||||
Scroll to the given anchor in the document.
|
||||
|
||||
==== positional arguments
|
||||
* +'name'+: The anchor to scroll to.
|
||||
|
||||
[[scroll-to-perc]]
|
||||
=== scroll-to-perc
|
||||
Syntax: +:scroll-to-perc [*--horizontal*] ['perc']+
|
||||
|
||||
@@ -242,10 +242,10 @@ To suppress loading of any default keybindings, you can set
|
||||
Loading `autoconfig.yml`
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
By default, all customization done via `:set`, `:bind` and `:unbind` is
|
||||
temporary as soon as a `config.py` exists. The settings done that way are always
|
||||
saved in the `autoconfig.yml` file, but you'll need to explicitly load it in
|
||||
your `config.py` by doing:
|
||||
All customization done via the UI (`:set`, `:bind` and `:unbind`) is
|
||||
stored in the `autoconfig.yml` file, which is not loaded automatically as soon
|
||||
as a `config.py` exists. If you want those settings to be loaded, you'll need to
|
||||
explicitly load the `autoconfig.yml` file in your `config.py` by doing:
|
||||
|
||||
.config.py:
|
||||
[source,python]
|
||||
|
||||
@@ -236,10 +236,11 @@
|
||||
|<<tabs.close_mouse_button,tabs.close_mouse_button>>|Mouse button with which to close tabs.
|
||||
|<<tabs.close_mouse_button_on_bar,tabs.close_mouse_button_on_bar>>|How to behave when the close mouse button is pressed on the tab bar.
|
||||
|<<tabs.favicons.scale,tabs.favicons.scale>>|Scaling factor for favicons in the tab bar.
|
||||
|<<tabs.favicons.show,tabs.favicons.show>>|Show favicons in the tab bar.
|
||||
|<<tabs.favicons.show,tabs.favicons.show>>|When to show favicons in the tab bar.
|
||||
|<<tabs.indicator.padding,tabs.indicator.padding>>|Padding (in pixels) for tab indicators.
|
||||
|<<tabs.indicator.width,tabs.indicator.width>>|Width (in pixels) of the progress indicator (0 to disable).
|
||||
|<<tabs.last_close,tabs.last_close>>|How to behave when the last tab is closed.
|
||||
|<<tabs.min_width,tabs.min_width>>|Minimum width (in pixels) of tabs (-1 for the default minimum size behavior).
|
||||
|<<tabs.mode_on_change,tabs.mode_on_change>>|When switching tabs, what input mode is applied.
|
||||
|<<tabs.mousewheel_switching,tabs.mousewheel_switching>>|Switch between tabs using the mouse wheel.
|
||||
|<<tabs.new_position.related,tabs.new_position.related>>|Position of new tabs opened from another tab.
|
||||
@@ -259,10 +260,11 @@
|
||||
|<<url.auto_search,url.auto_search>>|What search to start when something else than a URL is entered.
|
||||
|<<url.default_page,url.default_page>>|Page to open if :open -t/-b/-w is used without URL.
|
||||
|<<url.incdec_segments,url.incdec_segments>>|URL segments where `:navigate increment/decrement` will search for a number.
|
||||
|<<url.open_base_url,url.open_base_url>>|Open base URL of the searchengine if a searchengine shortcut is invoked without parameters.
|
||||
|<<url.searchengines,url.searchengines>>|Search engines which can be used via the address bar.
|
||||
|<<url.start_pages,url.start_pages>>|Page(s) to open at the start.
|
||||
|<<url.yank_ignored_parameters,url.yank_ignored_parameters>>|URL parameters to strip with `:yank url`.
|
||||
|<<window.hide_wayland_decoration,window.hide_wayland_decoration>>|Hide the window decoration when using wayland.
|
||||
|<<window.hide_decoration,window.hide_decoration>>|Hide the window decoration.
|
||||
|<<window.title_format,window.title_format>>|Format to use for the window title. The same placeholders like for
|
||||
|<<zoom.default,zoom.default>>|Default zoom level.
|
||||
|<<zoom.levels,zoom.levels>>|Available zoom levels.
|
||||
@@ -1653,11 +1655,7 @@ Type: <<types,List of Url>>
|
||||
|
||||
Default:
|
||||
|
||||
- +pass:[https://www.malwaredomainlist.com/hostslist/hosts.txt]+
|
||||
- +pass:[http://someonewhocares.org/hosts/hosts]+
|
||||
- +pass:[http://winhelp2002.mvps.org/hosts.zip]+
|
||||
- +pass:[http://malwaredomains.lehigh.edu/files/justdomains.zip]+
|
||||
- +pass:[https://pgl.yoyo.org/adservers/serverlist.php?hostformat=hosts&mimetype=plaintext]+
|
||||
- +pass:[https://raw.githubusercontent.com/StevenBlack/hosts/master/hosts]+
|
||||
|
||||
[[content.host_blocking.whitelist]]
|
||||
=== content.host_blocking.whitelist
|
||||
@@ -2823,11 +2821,17 @@ Default: +pass:[1.0]+
|
||||
|
||||
[[tabs.favicons.show]]
|
||||
=== tabs.favicons.show
|
||||
Show favicons in the tab bar.
|
||||
When to show favicons in the tab bar.
|
||||
|
||||
Type: <<types,Bool>>
|
||||
Type: <<types,String>>
|
||||
|
||||
Default: +pass:[true]+
|
||||
Valid values:
|
||||
|
||||
* +always+: Always show favicons.
|
||||
* +never+: Always hide favicons.
|
||||
* +pinned+: Show favicons only on pinned tabs.
|
||||
|
||||
Default: +pass:[always]+
|
||||
|
||||
[[tabs.indicator.padding]]
|
||||
=== tabs.indicator.padding
|
||||
@@ -2866,6 +2870,16 @@ Valid values:
|
||||
|
||||
Default: +pass:[ignore]+
|
||||
|
||||
[[tabs.min_width]]
|
||||
=== tabs.min_width
|
||||
Minimum width (in pixels) of tabs (-1 for the default minimum size behavior).
|
||||
This setting only applies when tabs are horizontal.
|
||||
This setting does not apply to pinned tabs, unless `tabs.pinned.shrink` is False.
|
||||
|
||||
Type: <<types,Int>>
|
||||
|
||||
Default: +pass:[-1]+
|
||||
|
||||
[[tabs.mode_on_change]]
|
||||
=== tabs.mode_on_change
|
||||
When switching tabs, what input mode is applied.
|
||||
@@ -3102,6 +3116,14 @@ Default:
|
||||
- +pass:[path]+
|
||||
- +pass:[query]+
|
||||
|
||||
[[url.open_base_url]]
|
||||
=== url.open_base_url
|
||||
Open base URL of the searchengine if a searchengine shortcut is invoked without parameters.
|
||||
|
||||
Type: <<types,Bool>>
|
||||
|
||||
Default: +pass:[false]+
|
||||
|
||||
[[url.searchengines]]
|
||||
=== url.searchengines
|
||||
Search engines which can be used via the address bar.
|
||||
@@ -3137,10 +3159,12 @@ Default:
|
||||
- +pass:[utm_term]+
|
||||
- +pass:[utm_content]+
|
||||
|
||||
[[window.hide_wayland_decoration]]
|
||||
=== window.hide_wayland_decoration
|
||||
Hide the window decoration when using wayland.
|
||||
This setting requires a restart.
|
||||
[[window.hide_decoration]]
|
||||
=== window.hide_decoration
|
||||
Hide the window decoration.
|
||||
|
||||
This setting requires a restart on Wayland.
|
||||
|
||||
|
||||
Type: <<types,Bool>>
|
||||
|
||||
@@ -3273,7 +3297,7 @@ See the setting's valid values for more information on allowed values.
|
||||
|TextAlignment|Alignment of text.
|
||||
|TimestampTemplate|An strftime-like template for timestamps.
|
||||
|
||||
See https://docs.python.org/3/library/datetime.html#strftime-strptime-behavior for reference.
|
||||
See https://sqlite.org/lang_datefunc.html for reference.
|
||||
|UniqueCharString|A string which may not contain duplicate chars.
|
||||
|Url|A URL as a string.
|
||||
|VerticalPosition|The position of the download bar.
|
||||
|
||||
|
Before Width: | Height: | Size: 1.0 MiB After Width: | Height: | Size: 1.0 MiB |
|
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 46 KiB |
@@ -47,17 +47,26 @@ Debian Stretch / Ubuntu 17.04 and 17.10
|
||||
Those versions come with QtWebEngine in the repositories. This makes it possible
|
||||
to install qutebrowser via the Debian package.
|
||||
|
||||
Download the https://packages.debian.org/sid/all/qutebrowser/download[qutebrowser] and
|
||||
https://packages.debian.org/sid/all/python3-pypeg2/download[PyPEG2]
|
||||
package from the Debian repositories.
|
||||
You'll need to download three packages:
|
||||
|
||||
Install the packages:
|
||||
- https://packages.debian.org/sid/all/python3-pypeg2/download[PyPEG2] (a library
|
||||
used by qutebrowser which is not in the earlier repositories)
|
||||
- https://packages.debian.org/sid/all/qutebrowser/download[qutebrowser] itself
|
||||
- Either https://packages.debian.org/sid/all/qutebrowser-qtwebengine/download[qutebrowser-qtwebengine]
|
||||
or https://packages.debian.org/sid/all/qutebrowser-qtwebkit/download[qutebrowser-qtwebkit]
|
||||
(or both) depending on the backend you want to use. QtWebEngine is the
|
||||
default/recommended choice.
|
||||
|
||||
After downloading, install the packages:
|
||||
|
||||
----
|
||||
# apt install ./python3-pypeg2_*_all.deb
|
||||
# apt install ./qutebrowser_*_all.deb
|
||||
# apt install ./qutebrowser*.deb
|
||||
----
|
||||
|
||||
For an update after the initial install, you only need to download/install the
|
||||
qutebrowser package.
|
||||
|
||||
Debian Testing / Ubuntu 18.04
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
@@ -417,7 +426,11 @@ Creating a wrapper script
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Running `tox` does not install a system-wide `qutebrowser` script. You can
|
||||
launch qutebrowser by doing `.venv/bin/python3 -m qutebrowser`.
|
||||
launch qutebrowser by doing:
|
||||
|
||||
----
|
||||
.venv/bin/python3 -m qutebrowser
|
||||
----
|
||||
|
||||
You can create a simple wrapper script to start qutebrowser somewhere in your
|
||||
`$PATH` (e.g. `/usr/local/bin/qutebrowser` or `~/bin/qutebrowser`):
|
||||
|
||||
@@ -22,9 +22,9 @@ Basic keybindings to get you started
|
||||
What to do now
|
||||
--------------
|
||||
|
||||
* View the link:https://qutebrowser.org/img/cheatsheet-big.png[key binding cheatsheet]
|
||||
* View the link:https://raw.githubusercontent.com/qutebrowser/qutebrowser/master/doc/img/cheatsheet-big.png[key binding cheatsheet]
|
||||
to make yourself familiar with the key bindings: +
|
||||
image:https://qutebrowser.org/img/cheatsheet-small.png["qutebrowser key binding cheatsheet",link="https://qutebrowser.org/img/cheatsheet-big.png"]
|
||||
image:https://raw.githubusercontent.com/qutebrowser/qutebrowser/master/doc/img/cheatsheet-small.png["qutebrowser key binding cheatsheet",link="https://raw.githubusercontent.com/qutebrowser/qutebrowser/master/doc/img/cheatsheet-big.png"]
|
||||
* There's also a https://www.shortcutfoo.com/app/dojos/qutebrowser[free training
|
||||
course] on shortcutfoo for the keybindings - note that you need to be in
|
||||
insert mode (i) for it to work.
|
||||
|
||||
|
Before Width: | Height: | Size: 45 KiB After Width: | Height: | Size: 113 KiB |
@@ -1,25 +1,31 @@
|
||||
PYTHON = python3
|
||||
DESTDIR = /
|
||||
PREFIX = /usr/local
|
||||
DESTDIR =
|
||||
ICONSIZES = 16 24 32 48 64 128 256 512
|
||||
|
||||
SETUPTOOLSOPTIONS =
|
||||
ifdef DESTDIR
|
||||
SETUPTOOLSOPTS = --root="$(DESTDIR)"
|
||||
endif
|
||||
|
||||
.PHONY: install
|
||||
|
||||
doc/qutebrowser.1.html:
|
||||
a2x -f manpage doc/qutebrowser.1.asciidoc
|
||||
|
||||
install: doc/qutebrowser.1.html
|
||||
$(PYTHON) setup.py install --root="$(DESTDIR)" --optimize=1
|
||||
$(PYTHON) setup.py install --prefix="$(PREFIX)" --optimize=1 $(SETUPTOOLSOPTS)
|
||||
install -Dm644 doc/qutebrowser.1 \
|
||||
"$(DESTDIR)/usr/share/man/man1/qutebrowser.1"
|
||||
"$(DESTDIR)$(PREFIX)/share/man/man1/qutebrowser.1"
|
||||
install -Dm644 misc/qutebrowser.desktop \
|
||||
"$(DESTDIR)/usr/share/applications/qutebrowser.desktop"
|
||||
"$(DESTDIR)$(PREFIX)/share/applications/qutebrowser.desktop"
|
||||
$(foreach i,$(ICONSIZES),install -Dm644 "icons/qutebrowser-$(i)x$(i).png" \
|
||||
"$(DESTDIR)/usr/share/icons/hicolor/$(i)x$(i)/apps/qutebrowser.png";)
|
||||
"$(DESTDIR)$(PREFIX)/share/icons/hicolor/$(i)x$(i)/apps/qutebrowser.png";)
|
||||
install -Dm644 icons/qutebrowser.svg \
|
||||
"$(DESTDIR)/usr/share/icons/hicolor/scalable/apps/qutebrowser.svg"
|
||||
install -Dm755 -t "$(DESTDIR)/usr/share/qutebrowser/userscripts/" \
|
||||
"$(DESTDIR)$(PREFIX)/share/icons/hicolor/scalable/apps/qutebrowser.svg"
|
||||
install -Dm755 -t "$(DESTDIR)$(PREFIX)/share/qutebrowser/userscripts/" \
|
||||
$(wildcard misc/userscripts/*)
|
||||
install -Dm755 -t "$(DESTDIR)/usr/share/qutebrowser/scripts/" \
|
||||
install -Dm755 -t "$(DESTDIR)$(PREFIX)/share/qutebrowser/scripts/" \
|
||||
$(filter-out scripts/__init__.py scripts/__pycache__ scripts/dev \
|
||||
scripts/testbrowser scripts/asciidoc2html.py scripts/setupcommon.py \
|
||||
scripts/link_pyqt.py,$(wildcard scripts/*))
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:zoom="1.7536248"
|
||||
inkscape:cx="376.55567"
|
||||
inkscape:cx="430.72917"
|
||||
inkscape:cy="268.64059"
|
||||
inkscape:document-units="px"
|
||||
inkscape:current-layer="layer1"
|
||||
@@ -3710,7 +3710,7 @@
|
||||
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">(10)</flowSpan> toggling settings:</flowPara><flowPara
|
||||
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"
|
||||
|
||||
|
Before Width: | Height: | Size: 181 KiB After Width: | Height: | Size: 181 KiB |
@@ -15,7 +15,7 @@ def get_data_files():
|
||||
('../qutebrowser/img', 'img'),
|
||||
('../qutebrowser/javascript', 'javascript'),
|
||||
('../qutebrowser/html/doc', 'html/doc'),
|
||||
('../qutebrowser/git-commit-id', ''),
|
||||
('../qutebrowser/git-commit-id', '.'),
|
||||
('../qutebrowser/config/configdata.yml', 'config'),
|
||||
]
|
||||
|
||||
@@ -58,14 +58,14 @@ exe = EXE(pyz,
|
||||
icon=icon,
|
||||
debug=False,
|
||||
strip=False,
|
||||
upx=True,
|
||||
upx=False,
|
||||
console=False )
|
||||
coll = COLLECT(exe,
|
||||
a.binaries,
|
||||
a.zipfiles,
|
||||
a.datas,
|
||||
strip=False,
|
||||
upx=True,
|
||||
upx=False,
|
||||
name='qutebrowser')
|
||||
|
||||
app = BUNDLE(coll,
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
# This file is automatically generated by scripts/dev/recompile_requirements.py
|
||||
|
||||
check-manifest==0.36
|
||||
check-manifest==0.37
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# This file is automatically generated by scripts/dev/recompile_requirements.py
|
||||
|
||||
certifi==2018.1.18
|
||||
certifi==2018.4.16
|
||||
chardet==3.0.4
|
||||
codecov==2.0.15
|
||||
coverage==4.5.1
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
attrs==17.4.0
|
||||
flake8==3.5.0
|
||||
flake8-bugbear==18.2.0
|
||||
flake8-builtins==1.0.post0
|
||||
flake8-builtins==1.3.1
|
||||
flake8-comprehensions==1.4.1
|
||||
flake8-copyright==0.2.0
|
||||
flake8-debugger==3.1.0
|
||||
@@ -11,15 +11,17 @@ flake8-deprecated==1.3
|
||||
flake8-docstrings==1.3.0
|
||||
flake8-future-import==0.4.4
|
||||
flake8-mock==0.3
|
||||
flake8-per-file-ignores==0.4
|
||||
flake8-per-file-ignores==0.6
|
||||
flake8-polyfill==1.0.2
|
||||
flake8-string-format==0.2.3
|
||||
flake8-tidy-imports==1.1.0
|
||||
flake8-tuple==0.2.13
|
||||
mccabe==0.6.1
|
||||
pathmatch==0.2.1
|
||||
pep8-naming==0.5.0
|
||||
pycodestyle==2.3.1
|
||||
pycodestyle==2.3.1 # rq.filter: < 2.4.0
|
||||
pydocstyle==2.1.1
|
||||
pyflakes==1.6.0
|
||||
six==1.11.0
|
||||
snowballstemmer==1.2.1
|
||||
typing==3.6.4
|
||||
|
||||
@@ -15,3 +15,6 @@ flake8-tuple
|
||||
pep8-naming
|
||||
pydocstyle
|
||||
pyflakes
|
||||
|
||||
# https://github.com/PyCQA/pycodestyle/issues/741
|
||||
#@ filter: pycodestyle < 2.4.0
|
||||
|
||||
@@ -3,6 +3,6 @@
|
||||
appdirs==1.4.3
|
||||
packaging==17.1
|
||||
pyparsing==2.2.0
|
||||
setuptools==38.5.1
|
||||
setuptools==39.1.0
|
||||
six==1.11.0
|
||||
wheel==0.30.0
|
||||
wheel==0.31.0
|
||||
|
||||
@@ -4,4 +4,4 @@ altgraph==0.15
|
||||
future==0.16.0
|
||||
macholib==1.9
|
||||
pefile==2017.11.5
|
||||
-e git+https://github.com/pyinstaller/pyinstaller.git@develop#egg=PyInstaller
|
||||
PyInstaller==3.3.1
|
||||
|
||||
@@ -1,4 +1 @@
|
||||
-e git+https://github.com/pyinstaller/pyinstaller.git@develop#egg=PyInstaller
|
||||
|
||||
# remove @commit-id for scm installs
|
||||
#@ replace: @.*# @develop#
|
||||
PyInstaller
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
# This file is automatically generated by scripts/dev/recompile_requirements.py
|
||||
|
||||
-e git+https://github.com/PyCQA/astroid.git#egg=astroid
|
||||
certifi==2018.1.18
|
||||
certifi==2018.4.16
|
||||
chardet==3.0.4
|
||||
github3.py==0.9.6
|
||||
github3.py==1.1.0
|
||||
idna==2.6
|
||||
isort==4.3.4
|
||||
lazy-object-proxy==1.3.1
|
||||
mccabe==0.6.1
|
||||
-e git+https://github.com/PyCQA/pylint.git#egg=pylint
|
||||
python-dateutil==2.7.2
|
||||
./scripts/dev/pylint_checkers
|
||||
requests==2.18.4
|
||||
six==1.11.0
|
||||
uritemplate==3.0.0
|
||||
uritemplate.py==3.0.2
|
||||
urllib3==1.22
|
||||
wrapt==1.10.11
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
# This file is automatically generated by scripts/dev/recompile_requirements.py
|
||||
|
||||
astroid==1.6.1
|
||||
certifi==2018.1.18
|
||||
astroid==1.6.3
|
||||
certifi==2018.4.16
|
||||
chardet==3.0.4
|
||||
github3.py==0.9.6
|
||||
github3.py==1.1.0
|
||||
idna==2.6
|
||||
isort==4.3.4
|
||||
lazy-object-proxy==1.3.1
|
||||
mccabe==0.6.1
|
||||
pylint==1.8.2
|
||||
pylint==1.8.4
|
||||
python-dateutil==2.7.2
|
||||
./scripts/dev/pylint_checkers
|
||||
requests==2.18.4
|
||||
six==1.11.0
|
||||
uritemplate==3.0.0
|
||||
uritemplate.py==3.0.2
|
||||
urllib3==1.22
|
||||
wrapt==1.10.11
|
||||
|
||||
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
@@ -0,0 +1,2 @@
|
||||
PyQt5==5.10.0
|
||||
#@ filter: PyQt5 != 5.10.1
|
||||
@@ -2,16 +2,16 @@
|
||||
|
||||
attrs==17.4.0
|
||||
beautifulsoup4==4.6.0
|
||||
cheroot==6.0.0
|
||||
cheroot==6.2.4
|
||||
click==6.7
|
||||
# colorama==0.3.9
|
||||
coverage==4.5.1
|
||||
EasyProcess==0.2.3
|
||||
fields==5.0.0
|
||||
Flask==0.12.2
|
||||
Flask==1.0.1
|
||||
glob2==0.6
|
||||
hunter==2.0.2
|
||||
hypothesis==3.48.0
|
||||
hypothesis==3.56.5
|
||||
itsdangerous==0.24
|
||||
# Jinja2==2.10
|
||||
Mako==1.0.7
|
||||
@@ -20,15 +20,15 @@ more-itertools==4.1.0
|
||||
parse==1.8.2
|
||||
parse-type==0.4.2
|
||||
pluggy==0.6.0
|
||||
py==1.5.2
|
||||
py-cpuinfo==3.3.0
|
||||
pytest==3.4.1
|
||||
pytest-bdd==2.20.0
|
||||
py==1.5.3
|
||||
py-cpuinfo==4.0.0
|
||||
pytest==3.5.1
|
||||
pytest-bdd==2.21.0
|
||||
pytest-benchmark==3.1.1
|
||||
pytest-cov==2.5.1
|
||||
pytest-faulthandler==1.4.1
|
||||
pytest-faulthandler==1.5.0
|
||||
pytest-instafail==0.3.0
|
||||
pytest-mock==1.7.1
|
||||
pytest-mock==1.9.0
|
||||
pytest-qt==2.3.1
|
||||
pytest-repeat==0.4.1
|
||||
pytest-rerunfailures==4.0
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# This file is automatically generated by scripts/dev/recompile_requirements.py
|
||||
|
||||
pluggy==0.6.0
|
||||
py==1.5.2
|
||||
py==1.5.3
|
||||
six==1.11.0
|
||||
tox==2.9.1
|
||||
virtualenv==15.1.0
|
||||
tox==3.0.0
|
||||
virtualenv==15.2.0
|
||||
|
||||
@@ -1,4 +1 @@
|
||||
tox
|
||||
|
||||
# The latest tox release still depends on pluggy < 0.4...
|
||||
pluggy==0.4.0
|
||||
|
||||
69
misc/userscripts/getbib
Executable file
@@ -0,0 +1,69 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Qutebrowser userscript scraping the current web page for DOIs and downloading
|
||||
corresponding bibtex information.
|
||||
|
||||
Set the environment variable 'QUTE_BIB_FILEPATH' to indicate the path to
|
||||
download to. Otherwise, bibtex information is downloaded to '/tmp' and hence
|
||||
deleted at reboot.
|
||||
|
||||
Installation: see qute://help/userscripts.html
|
||||
|
||||
Inspired by
|
||||
https://ocefpaf.github.io/python4oceanographers/blog/2014/05/19/doi2bibtex/
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import shutil
|
||||
import re
|
||||
from collections import Counter
|
||||
from urllib import parse as url_parse
|
||||
from urllib import request as url_request
|
||||
|
||||
|
||||
FIFO_PATH = os.getenv("QUTE_FIFO")
|
||||
|
||||
def message_fifo(message, level="warning"):
|
||||
"""Send message to qutebrowser FIFO. The level must be one of 'info',
|
||||
'warning' (default) or 'error'."""
|
||||
with open(FIFO_PATH, "w") as fifo:
|
||||
fifo.write("message-{} '{}'".format(level, message))
|
||||
|
||||
|
||||
source = os.getenv("QUTE_TEXT")
|
||||
with open(source) as f:
|
||||
text = f.read()
|
||||
|
||||
# find DOIs on page using regex
|
||||
dval = re.compile(r'(10\.(\d)+/([^(\s\>\"\<)])+)')
|
||||
# https://stackoverflow.com/a/10324802/3865876, too strict
|
||||
# dval = re.compile(r'\b(10[.][0-9]{4,}(?:[.][0-9]+)*/(?:(?!["&\'<>])\S)+)\b')
|
||||
dois = dval.findall(text)
|
||||
dois = Counter(e[0] for e in dois)
|
||||
try:
|
||||
doi = dois.most_common(1)[0][0]
|
||||
except IndexError:
|
||||
message_fifo("No DOIs found on page")
|
||||
sys.exit()
|
||||
message_fifo("Found {} DOIs on page, selecting {}".format(len(dois), doi),
|
||||
level="info")
|
||||
|
||||
# get bibtex data corresponding to DOI
|
||||
url = "http://dx.doi.org/" + url_parse.quote(doi)
|
||||
headers = dict(Accept='text/bibliography; style=bibtex')
|
||||
request = url_request.Request(url, headers=headers)
|
||||
response = url_request.urlopen(request)
|
||||
status_code = response.getcode()
|
||||
if status_code >= 400:
|
||||
message_fifo("Request returned {}".format(status_code))
|
||||
sys.exit()
|
||||
|
||||
# obtain content and format it
|
||||
bibtex = response.read().decode("utf-8").strip()
|
||||
bibtex = bibtex.replace(" ", "\n ", 1).\
|
||||
replace("}, ", "},\n ").replace("}}", "}\n}")
|
||||
|
||||
# append to file
|
||||
bib_filepath = os.getenv("QUTE_BIB_FILEPATH", "/tmp/qute.bib")
|
||||
with open(bib_filepath, "a") as f:
|
||||
f.write(bibtex + "\n\n")
|
||||
261
misc/userscripts/qute-keepass
Executable file
@@ -0,0 +1,261 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
# Copyright 2018 Jay Kamat <jaygkamat@gmail.com>
|
||||
#
|
||||
# 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/>.
|
||||
|
||||
"""This userscript allows for insertion of usernames and passwords from keepass
|
||||
databases using pykeepass. Since it is a userscript, it must be run from
|
||||
qutebrowser.
|
||||
|
||||
A sample invocation of this script is:
|
||||
|
||||
:spawn --userscript qute-keepass -p ~/KeePassFiles/MainDatabase.kdbx
|
||||
|
||||
And a sample binding
|
||||
|
||||
:bind --mode=insert <ctrl-i> spawn --userscript qute-keepass -p ~/KeePassFiles/MainDatabase.kdbx
|
||||
|
||||
-p or --path is a required argument.
|
||||
|
||||
--keyfile-path allows you to specify a keepass keyfile. If you only use a
|
||||
keyfile, also add --no-password as well. Specifying --no-password without
|
||||
--keyfile-path will lead to an error.
|
||||
|
||||
login information is inserted using :insert-text and :fake-key <Tab>, which
|
||||
means you must have a cursor in position before initiating this userscript. If
|
||||
you do not do this, you will get 'element not editable' errors.
|
||||
|
||||
If keepass takes a while to open the DB, you might want to consider reducing
|
||||
the number of transform rounds in your database settings.
|
||||
|
||||
Dependencies: pykeepass (in python3), PyQt5. Without pykeepass, you will get an
|
||||
exit code of 100.
|
||||
|
||||
********************!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!******************
|
||||
|
||||
WARNING: The login details are viewable as plaintext in qutebrowser's debug log
|
||||
(qute://log) and could be compromised if you decide to submit a crash report!
|
||||
|
||||
********************!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!******************
|
||||
|
||||
"""
|
||||
|
||||
# pylint: disable=bad-builtin
|
||||
|
||||
import argparse
|
||||
import enum
|
||||
import functools
|
||||
import os
|
||||
import shlex
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
from PyQt5.QtCore import QUrl
|
||||
from PyQt5.QtWidgets import QApplication, QInputDialog, QLineEdit
|
||||
|
||||
try:
|
||||
import pykeepass
|
||||
except ImportError as e:
|
||||
print("pykeepass not found: {}".format(str(e)), file=sys.stderr)
|
||||
|
||||
# Since this is a common error, try to print it to the FIFO if we can.
|
||||
if 'QUTE_FIFO' in os.environ:
|
||||
with open(os.environ['QUTE_FIFO'], 'w') as fifo:
|
||||
fifo.write('message-error "pykeepass failed to be imported."\n')
|
||||
fifo.flush()
|
||||
sys.exit(100)
|
||||
|
||||
argument_parser = argparse.ArgumentParser(
|
||||
description="Fill passwords using keepass.",
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
epilog=__doc__)
|
||||
argument_parser.add_argument('url', nargs='?', default=os.getenv('QUTE_URL'))
|
||||
argument_parser.add_argument('--path', '-p', required=True,
|
||||
help='Path to the keepass db.')
|
||||
argument_parser.add_argument('--keyfile-path', '-k', default=None,
|
||||
help='Path to a keepass keyfile')
|
||||
argument_parser.add_argument(
|
||||
'--no-password', action='store_true',
|
||||
help='Supply if no password is required to unlock this database. '
|
||||
'Only allowed with --keyfile-path')
|
||||
argument_parser.add_argument(
|
||||
'--dmenu-invocation', '-d', default='dmenu',
|
||||
help='Invocation used to execute a dmenu-provider')
|
||||
argument_parser.add_argument(
|
||||
'--dmenu-format', '-f', default='{title}: {username}',
|
||||
help='Format string for keys to display in dmenu.'
|
||||
' Must generate a unique string.')
|
||||
argument_parser.add_argument(
|
||||
'--no-insert-mode', '-n', dest='insert_mode', action='store_false',
|
||||
help="Don't automatically enter insert mode")
|
||||
argument_parser.add_argument(
|
||||
'--io-encoding', '-i', default='UTF-8',
|
||||
help='Encoding used to communicate with subprocesses')
|
||||
group = argument_parser.add_mutually_exclusive_group()
|
||||
group.add_argument('--username-fill-only', '-e',
|
||||
action='store_true', help='Only insert username')
|
||||
group.add_argument('--password-fill-only', '-w',
|
||||
action='store_true', help='Only insert password')
|
||||
|
||||
CMD_DELAY = 50
|
||||
|
||||
|
||||
class ExitCodes(enum.IntEnum):
|
||||
"""Stores various exit codes groups to use."""
|
||||
SUCCESS = 0
|
||||
FAILURE = 1
|
||||
# 1 is automatically used if Python throws an exception
|
||||
NO_CANDIDATES = 2
|
||||
USER_QUIT = 3
|
||||
DB_OPEN_FAIL = 4
|
||||
|
||||
INTERNAL_ERROR = 10
|
||||
|
||||
|
||||
def qute_command(command):
|
||||
with open(os.environ['QUTE_FIFO'], 'w') as fifo:
|
||||
fifo.write(command + '\n')
|
||||
fifo.flush()
|
||||
|
||||
|
||||
def stderr(to_print):
|
||||
"""Extra functionality to echo out errors to qb ui."""
|
||||
print(to_print, file=sys.stderr)
|
||||
qute_command('message-error "{}"'.format(to_print))
|
||||
|
||||
|
||||
def dmenu(items, invocation, encoding):
|
||||
"""Runs dmenu with given arguments."""
|
||||
command = shlex.split(invocation)
|
||||
process = subprocess.run(command, input='\n'.join(items).encode(encoding),
|
||||
stdout=subprocess.PIPE)
|
||||
return process.stdout.decode(encoding).strip()
|
||||
|
||||
|
||||
def get_password():
|
||||
"""Get a keepass db password from user."""
|
||||
_app = QApplication(sys.argv)
|
||||
text, ok = QInputDialog.getText(
|
||||
None, "KeePass DB Password",
|
||||
"Please enter your KeePass Master Password",
|
||||
QLineEdit.Password)
|
||||
if not ok:
|
||||
stderr('Password Prompt Rejected.')
|
||||
sys.exit(ExitCodes.USER_QUIT)
|
||||
return text
|
||||
|
||||
|
||||
def find_candidates(args, host):
|
||||
"""Finds candidates that match host"""
|
||||
file_path = os.path.expanduser(args.path)
|
||||
|
||||
# TODO find a way to keep the db open, so we don't open (and query
|
||||
# password) it every time
|
||||
|
||||
pw = None
|
||||
if not args.no_password:
|
||||
pw = get_password()
|
||||
|
||||
kf = args.keyfile_path
|
||||
if kf:
|
||||
kf = os.path.expanduser(kf)
|
||||
|
||||
try:
|
||||
kp = pykeepass.PyKeePass(file_path, password=pw, keyfile=kf)
|
||||
except Exception as e:
|
||||
stderr("There was an error opening the DB: {}".format(str(e)))
|
||||
|
||||
return kp.find_entries(url="{}{}{}".format(".*", host, ".*"), regex=True)
|
||||
|
||||
|
||||
def candidate_to_str(args, candidate):
|
||||
"""Turns candidate into a human readable string for dmenu"""
|
||||
return args.dmenu_format.format(title=candidate.title,
|
||||
url=candidate.url,
|
||||
username=candidate.username,
|
||||
path=candidate.path,
|
||||
uuid=candidate.uuid)
|
||||
|
||||
|
||||
def candidate_to_secret(candidate):
|
||||
"""Turns candidate into a generic (user, password) tuple"""
|
||||
return (candidate.username, candidate.password)
|
||||
|
||||
|
||||
def run(args):
|
||||
"""Runs qute-keepass"""
|
||||
if not args.url:
|
||||
argument_parser.print_help()
|
||||
return ExitCodes.FAILURE
|
||||
|
||||
url_host = QUrl(args.url).host()
|
||||
|
||||
if not url_host:
|
||||
stderr('{} was not parsed as a valid URL!'.format(args.url))
|
||||
return ExitCodes.INTERNAL_ERROR
|
||||
|
||||
# Find candidates matching the host of the given URL
|
||||
candidates = find_candidates(args, url_host)
|
||||
if not candidates:
|
||||
stderr('No candidates for URL {!r} found!'.format(args.url))
|
||||
return ExitCodes.NO_CANDIDATES
|
||||
|
||||
# Create a map so we can get turn the resulting string from dmenu back into
|
||||
# a candidate
|
||||
candidates_strs = list(map(functools.partial(candidate_to_str, args),
|
||||
candidates))
|
||||
candidates_map = dict(zip(candidates_strs, candidates))
|
||||
|
||||
if len(candidates) == 1:
|
||||
selection = candidates.pop()
|
||||
else:
|
||||
selection = dmenu(candidates_strs,
|
||||
args.dmenu_invocation,
|
||||
args.io_encoding)
|
||||
|
||||
if selection not in candidates_map:
|
||||
stderr("'{}' was not a valid entry!").format(selection)
|
||||
return ExitCodes.USER_QUIT
|
||||
|
||||
selection = candidates_map[selection]
|
||||
|
||||
username, password = candidate_to_secret(selection)
|
||||
|
||||
insert_mode = ';; enter-mode insert' if args.insert_mode else ''
|
||||
if args.username_fill_only:
|
||||
qute_command('insert-text {}{}'.format(username, insert_mode))
|
||||
elif args.password_fill_only:
|
||||
qute_command('insert-text {}{}'.format(password, insert_mode))
|
||||
else:
|
||||
# Enter username and password using insert-key and fake-key <Tab>
|
||||
# (which supports more passwords than fake-key only), then switch back
|
||||
# into insert-mode, so the form can be directly submitted by hitting
|
||||
# enter afterwards. It dosen't matter when we go into insert mode, but
|
||||
# the other commands need to be be executed sequentially, so we add
|
||||
# delays with later.
|
||||
qute_command('insert-text {} ;;'
|
||||
'later {} fake-key <Tab> ;;'
|
||||
'later {} insert-text {}{}'
|
||||
.format(username, CMD_DELAY,
|
||||
CMD_DELAY * 2, password, insert_mode))
|
||||
|
||||
return ExitCodes.SUCCESS
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
arguments = argument_parser.parse_args()
|
||||
sys.exit(run(arguments))
|
||||
@@ -109,6 +109,13 @@ def dmenu(items, invocation, encoding):
|
||||
return process.stdout.decode(encoding).strip()
|
||||
|
||||
|
||||
def fake_key_raw(text):
|
||||
for character in text:
|
||||
# Escape all characters by default, space requires special handling
|
||||
sequence = '" "' if character == ' ' else '\{}'.format(character)
|
||||
qute_command('fake-key {}'.format(sequence))
|
||||
|
||||
|
||||
def main(arguments):
|
||||
if not arguments.url:
|
||||
argument_parser.print_help()
|
||||
@@ -158,15 +165,19 @@ def main(arguments):
|
||||
return ExitCodes.COULD_NOT_MATCH_PASSWORD
|
||||
password = match.group(1)
|
||||
|
||||
insert_mode = ';; enter-mode insert' if arguments.insert_mode else ''
|
||||
if arguments.username_only:
|
||||
qute_command('fake-key {}{}'.format(username, insert_mode))
|
||||
fake_key_raw(username)
|
||||
elif arguments.password_only:
|
||||
qute_command('fake-key {}{}'.format(password, insert_mode))
|
||||
fake_key_raw(password)
|
||||
else:
|
||||
# Enter username and password using fake-key and <Tab> (which seems to work almost universally), then switch
|
||||
# back into insert-mode, so the form can be directly submitted by hitting enter afterwards
|
||||
qute_command('fake-key {} ;; fake-key <Tab> ;; fake-key {}{}'.format(username, password, insert_mode))
|
||||
fake_key_raw(username)
|
||||
qute_command('fake-key <Tab>')
|
||||
fake_key_raw(password)
|
||||
|
||||
if arguments.insert_mode:
|
||||
qute_command('enter-mode insert')
|
||||
|
||||
return ExitCodes.SUCCESS
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
if msg="$(task add "$title" "$*" 2>&1)"; then
|
||||
# annotate the new task with the url, send the output back to the browser
|
||||
task +LATEST annotate "$QUTE_URL"
|
||||
echo "message-info '$msg'" >> "$QUTE_FIFO"
|
||||
echo "message-info '$(echo "$msg" | head -n 1)'" >> "$QUTE_FIFO"
|
||||
else
|
||||
echo "message-error '$msg'" >> "$QUTE_FIFO"
|
||||
echo "message-error '$(echo "$msg" | head -n 1)'" >> "$QUTE_FIFO"
|
||||
fi
|
||||
|
||||
@@ -26,7 +26,7 @@ __copyright__ = "Copyright 2014-2018 Florian Bruhin (The Compiler)"
|
||||
__license__ = "GPL"
|
||||
__maintainer__ = __author__
|
||||
__email__ = "mail@qutebrowser.org"
|
||||
__version_info__ = (1, 2, 0)
|
||||
__version_info__ = (1, 3, 0)
|
||||
__version__ = '.'.join(str(e) for e in __version_info__)
|
||||
__description__ = "A keyboard-driven, vim-like browser based on PyQt5."
|
||||
|
||||
|
||||
@@ -340,7 +340,7 @@ def _open_startpage(win_id=None):
|
||||
for cur_win_id in list(window_ids): # Copying as the dict could change
|
||||
tabbed_browser = objreg.get('tabbed-browser', scope='window',
|
||||
window=cur_win_id)
|
||||
if tabbed_browser.count() == 0:
|
||||
if tabbed_browser.widget.count() == 0:
|
||||
log.init.debug("Opening start pages")
|
||||
for url in config.val.url.start_pages:
|
||||
tabbed_browser.tabopen(url)
|
||||
|
||||
@@ -94,14 +94,8 @@ class HostBlocker:
|
||||
_done_count: How many files have been read successfully.
|
||||
_local_hosts_file: The path to the blocked-hosts file.
|
||||
_config_hosts_file: The path to a blocked-hosts in ~/.config
|
||||
|
||||
Class attributes:
|
||||
WHITELISTED: Hosts which never should be blocked.
|
||||
"""
|
||||
|
||||
WHITELISTED = ('localhost', 'localhost.localdomain', 'broadcasthost',
|
||||
'local')
|
||||
|
||||
def __init__(self):
|
||||
self._blocked_hosts = set()
|
||||
self._config_blocked_hosts = set()
|
||||
@@ -234,16 +228,14 @@ class HostBlocker:
|
||||
parts = line.split()
|
||||
if len(parts) == 1:
|
||||
# "one host per line" format
|
||||
host = parts[0]
|
||||
elif len(parts) == 2:
|
||||
# /etc/hosts format
|
||||
host = parts[1]
|
||||
hosts = [parts[0]]
|
||||
else:
|
||||
log.misc.error("Failed to parse: {!r}".format(line))
|
||||
return False
|
||||
# /etc/hosts format
|
||||
hosts = parts[1:]
|
||||
|
||||
if host not in self.WHITELISTED:
|
||||
self._blocked_hosts.add(host)
|
||||
for host in hosts:
|
||||
if '.' in host and not host.endswith('.localdomain'):
|
||||
self._blocked_hosts.add(host)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
@@ -114,6 +114,10 @@ class TabData:
|
||||
netrc_used = attr.ib(False)
|
||||
input_mode = attr.ib(usertypes.KeyMode.normal)
|
||||
|
||||
def should_show_icon(self):
|
||||
return (config.val.tabs.favicons.show == 'always' or
|
||||
config.val.tabs.favicons.show == 'pinned' and self.pinned)
|
||||
|
||||
|
||||
class AbstractAction:
|
||||
|
||||
@@ -333,7 +337,14 @@ class AbstractZoom(QObject):
|
||||
|
||||
class AbstractCaret(QObject):
|
||||
|
||||
"""Attribute of AbstractTab for caret browsing."""
|
||||
"""Attribute of AbstractTab for caret browsing.
|
||||
|
||||
Signals:
|
||||
selection_toggled: Emitted when the selection was toggled.
|
||||
arg: Whether the selection is now active.
|
||||
"""
|
||||
|
||||
selection_toggled = pyqtSignal(bool)
|
||||
|
||||
def __init__(self, tab, mode_manager, parent=None):
|
||||
super().__init__(parent)
|
||||
@@ -439,6 +450,9 @@ class AbstractScroller(QObject):
|
||||
def to_point(self, point):
|
||||
raise NotImplementedError
|
||||
|
||||
def to_anchor(self, name):
|
||||
raise NotImplementedError
|
||||
|
||||
def delta(self, x=0, y=0):
|
||||
raise NotImplementedError
|
||||
|
||||
@@ -665,8 +679,7 @@ class AbstractTab(QWidget):
|
||||
objreg.register('hintmanager', hintmanager, scope='tab',
|
||||
window=self.win_id, tab=self.tab_id)
|
||||
|
||||
self.predicted_navigation.connect(
|
||||
lambda url: self.title_changed.emit(url.toDisplayString()))
|
||||
self.predicted_navigation.connect(self._on_predicted_navigation)
|
||||
|
||||
def _set_widget(self, widget):
|
||||
# pylint: disable=protected-access
|
||||
@@ -711,10 +724,24 @@ class AbstractTab(QWidget):
|
||||
if getattr(evt, 'posted', False):
|
||||
raise utils.Unreachable("Can't re-use an event which was already "
|
||||
"posted!")
|
||||
|
||||
recipient = self.event_target()
|
||||
if recipient is None:
|
||||
# https://github.com/qutebrowser/qutebrowser/issues/3888
|
||||
log.webview.warning("Unable to find event target!")
|
||||
return
|
||||
|
||||
evt.posted = True
|
||||
QApplication.postEvent(recipient, evt)
|
||||
|
||||
@pyqtSlot(QUrl)
|
||||
def _on_predicted_navigation(self, url):
|
||||
"""Adjust the title if we are going to visit an URL soon."""
|
||||
qtutils.ensure_valid(url)
|
||||
url_string = url.toDisplayString()
|
||||
log.webview.debug("Predicted navigation: {}".format(url_string))
|
||||
self.title_changed.emit(url_string)
|
||||
|
||||
@pyqtSlot(QUrl)
|
||||
def _on_url_changed(self, url):
|
||||
"""Update title when URL has changed and no title is available."""
|
||||
@@ -815,11 +842,12 @@ class AbstractTab(QWidget):
|
||||
def load_status(self):
|
||||
return self._load_status
|
||||
|
||||
def _openurl_prepare(self, url):
|
||||
def _openurl_prepare(self, url, *, predict=True):
|
||||
qtutils.ensure_valid(url)
|
||||
self.predicted_navigation.emit(url)
|
||||
if predict:
|
||||
self.predicted_navigation.emit(url)
|
||||
|
||||
def openurl(self, url):
|
||||
def openurl(self, url, *, predict=True):
|
||||
raise NotImplementedError
|
||||
|
||||
def reload(self, *, force=False):
|
||||
|
||||
@@ -53,7 +53,6 @@ class CommandDispatcher:
|
||||
cmdutils.register() decorators are run, currentWidget() will return None.
|
||||
|
||||
Attributes:
|
||||
_editor: The ExternalEditor object.
|
||||
_win_id: The window ID the CommandDispatcher is associated with.
|
||||
_tabbed_browser: The TabbedBrowser used.
|
||||
"""
|
||||
@@ -73,16 +72,16 @@ class CommandDispatcher:
|
||||
|
||||
def _count(self):
|
||||
"""Convenience method to get the widget count."""
|
||||
return self._tabbed_browser.count()
|
||||
return self._tabbed_browser.widget.count()
|
||||
|
||||
def _set_current_index(self, idx):
|
||||
"""Convenience method to set the current widget index."""
|
||||
cmdutils.check_overflow(idx, 'int')
|
||||
self._tabbed_browser.setCurrentIndex(idx)
|
||||
self._tabbed_browser.widget.setCurrentIndex(idx)
|
||||
|
||||
def _current_index(self):
|
||||
"""Convenience method to get the current widget index."""
|
||||
return self._tabbed_browser.currentIndex()
|
||||
return self._tabbed_browser.widget.currentIndex()
|
||||
|
||||
def _current_url(self):
|
||||
"""Convenience method to get the current url."""
|
||||
@@ -101,7 +100,7 @@ class CommandDispatcher:
|
||||
|
||||
def _current_widget(self):
|
||||
"""Get the currently active widget from a command."""
|
||||
widget = self._tabbed_browser.currentWidget()
|
||||
widget = self._tabbed_browser.widget.currentWidget()
|
||||
if widget is None:
|
||||
raise cmdexc.CommandError("No WebView available yet!")
|
||||
return widget
|
||||
@@ -147,10 +146,10 @@ class CommandDispatcher:
|
||||
None if no widget was found.
|
||||
"""
|
||||
if count is None:
|
||||
return self._tabbed_browser.currentWidget()
|
||||
return self._tabbed_browser.widget.currentWidget()
|
||||
elif 1 <= count <= self._count():
|
||||
cmdutils.check_overflow(count + 1, 'int')
|
||||
return self._tabbed_browser.widget(count - 1)
|
||||
return self._tabbed_browser.widget.widget(count - 1)
|
||||
else:
|
||||
return None
|
||||
|
||||
@@ -163,7 +162,7 @@ class CommandDispatcher:
|
||||
if not show_error:
|
||||
return
|
||||
raise cmdexc.CommandError("No last focused tab!")
|
||||
idx = self._tabbed_browser.indexOf(tab)
|
||||
idx = self._tabbed_browser.widget.indexOf(tab)
|
||||
if idx == -1:
|
||||
raise cmdexc.CommandError("Last focused tab vanished!")
|
||||
self._set_current_index(idx)
|
||||
@@ -212,7 +211,7 @@ class CommandDispatcher:
|
||||
what's configured in 'tabs.select_on_remove'.
|
||||
count: The tab index to close, or None
|
||||
"""
|
||||
tabbar = self._tabbed_browser.tabBar()
|
||||
tabbar = self._tabbed_browser.widget.tabBar()
|
||||
selection_override = self._get_selection_override(prev, next_,
|
||||
opposite)
|
||||
|
||||
@@ -264,7 +263,7 @@ class CommandDispatcher:
|
||||
return
|
||||
|
||||
to_pin = not tab.data.pinned
|
||||
self._tabbed_browser.set_tab_pinned(tab, to_pin)
|
||||
self._tabbed_browser.widget.set_tab_pinned(tab, to_pin)
|
||||
|
||||
@cmdutils.register(instance='command-dispatcher', name='open',
|
||||
maxsplit=0, scope='window')
|
||||
@@ -483,7 +482,8 @@ class CommandDispatcher:
|
||||
"""
|
||||
cmdutils.check_exclusive((bg, window), 'bw')
|
||||
curtab = self._current_widget()
|
||||
cur_title = self._tabbed_browser.page_title(self._current_index())
|
||||
cur_title = self._tabbed_browser.widget.page_title(
|
||||
self._current_index())
|
||||
try:
|
||||
history = curtab.history.serialize()
|
||||
except browsertab.WebTabError as e:
|
||||
@@ -499,18 +499,18 @@ class CommandDispatcher:
|
||||
newtab = new_tabbed_browser.tabopen(background=bg)
|
||||
new_tabbed_browser = objreg.get('tabbed-browser', scope='window',
|
||||
window=newtab.win_id)
|
||||
idx = new_tabbed_browser.indexOf(newtab)
|
||||
idx = new_tabbed_browser.widget.indexOf(newtab)
|
||||
|
||||
new_tabbed_browser.set_page_title(idx, cur_title)
|
||||
if config.val.tabs.favicons.show:
|
||||
new_tabbed_browser.setTabIcon(idx, curtab.icon())
|
||||
new_tabbed_browser.widget.set_page_title(idx, cur_title)
|
||||
if curtab.data.should_show_icon():
|
||||
new_tabbed_browser.widget.setTabIcon(idx, curtab.icon())
|
||||
if config.val.tabs.tabs_are_windows:
|
||||
new_tabbed_browser.window().setWindowIcon(curtab.icon())
|
||||
new_tabbed_browser.widget.window().setWindowIcon(curtab.icon())
|
||||
|
||||
newtab.data.keep_icon = True
|
||||
newtab.history.deserialize(history)
|
||||
newtab.zoom.set_factor(curtab.zoom.factor())
|
||||
new_tabbed_browser.set_tab_pinned(newtab, curtab.data.pinned)
|
||||
new_tabbed_browser.widget.set_tab_pinned(newtab, curtab.data.pinned)
|
||||
return newtab
|
||||
|
||||
@cmdutils.register(instance='command-dispatcher', scope='window')
|
||||
@@ -768,6 +768,15 @@ class CommandDispatcher:
|
||||
|
||||
self._current_widget().scroller.to_perc(x, y)
|
||||
|
||||
@cmdutils.register(instance='command-dispatcher', scope='window')
|
||||
def scroll_to_anchor(self, name):
|
||||
"""Scroll to the given anchor in the document.
|
||||
|
||||
Args:
|
||||
name: The anchor to scroll to.
|
||||
"""
|
||||
self._current_widget().scroller.to_anchor(name)
|
||||
|
||||
@cmdutils.register(instance='command-dispatcher', scope='window')
|
||||
@cmdutils.argument('count', count=True)
|
||||
@cmdutils.argument('top_navigate', metavar='ACTION',
|
||||
@@ -846,7 +855,7 @@ class CommandDispatcher:
|
||||
keep: Stay in visual mode after yanking the selection.
|
||||
"""
|
||||
if what == 'title':
|
||||
s = self._tabbed_browser.page_title(self._current_index())
|
||||
s = self._tabbed_browser.widget.page_title(self._current_index())
|
||||
elif what == 'domain':
|
||||
port = self._current_url().port()
|
||||
s = '{}://{}{}'.format(self._current_url().scheme(),
|
||||
@@ -958,7 +967,7 @@ class CommandDispatcher:
|
||||
force: Avoid confirmation for pinned tabs.
|
||||
"""
|
||||
cmdutils.check_exclusive((prev, next_), 'pn')
|
||||
cur_idx = self._tabbed_browser.currentIndex()
|
||||
cur_idx = self._tabbed_browser.widget.currentIndex()
|
||||
assert cur_idx != -1
|
||||
|
||||
def _to_close(i):
|
||||
@@ -1013,7 +1022,7 @@ class CommandDispatcher:
|
||||
elif config.val.tabs.wrap:
|
||||
self._set_current_index(newidx % self._count())
|
||||
else:
|
||||
raise cmdexc.CommandError("First tab")
|
||||
log.webview.debug("First tab")
|
||||
|
||||
@cmdutils.register(instance='command-dispatcher', scope='window')
|
||||
@cmdutils.argument('count', count=True)
|
||||
@@ -1033,7 +1042,7 @@ class CommandDispatcher:
|
||||
elif config.val.tabs.wrap:
|
||||
self._set_current_index(newidx % self._count())
|
||||
else:
|
||||
raise cmdexc.CommandError("Last tab")
|
||||
log.webview.debug("Last tab")
|
||||
|
||||
def _resolve_buffer_index(self, index):
|
||||
"""Resolve a buffer index to the tabbedbrowser and tab.
|
||||
@@ -1075,11 +1084,11 @@ class CommandDispatcher:
|
||||
|
||||
tabbed_browser = objreg.get('tabbed-browser', scope='window',
|
||||
window=win_id)
|
||||
if not 0 < idx <= tabbed_browser.count():
|
||||
if not 0 < idx <= tabbed_browser.widget.count():
|
||||
raise cmdexc.CommandError(
|
||||
"There's no tab with index {}!".format(idx))
|
||||
|
||||
return (tabbed_browser, tabbed_browser.widget(idx-1))
|
||||
return (tabbed_browser, tabbed_browser.widget.widget(idx-1))
|
||||
|
||||
@cmdutils.register(instance='command-dispatcher', scope='window',
|
||||
maxsplit=0)
|
||||
@@ -1107,10 +1116,10 @@ class CommandDispatcher:
|
||||
|
||||
tabbed_browser, tab = self._resolve_buffer_index(index)
|
||||
|
||||
window = tabbed_browser.window()
|
||||
window = tabbed_browser.widget.window()
|
||||
window.activateWindow()
|
||||
window.raise_()
|
||||
tabbed_browser.setCurrentWidget(tab)
|
||||
tabbed_browser.widget.setCurrentWidget(tab)
|
||||
|
||||
@cmdutils.register(instance='command-dispatcher', scope='window')
|
||||
@cmdutils.argument('index', choices=['last'])
|
||||
@@ -1194,7 +1203,7 @@ class CommandDispatcher:
|
||||
cur_idx = self._current_index()
|
||||
cmdutils.check_overflow(cur_idx, 'int')
|
||||
cmdutils.check_overflow(new_idx, 'int')
|
||||
self._tabbed_browser.tabBar().moveTab(cur_idx, new_idx)
|
||||
self._tabbed_browser.widget.tabBar().moveTab(cur_idx, new_idx)
|
||||
|
||||
@cmdutils.register(instance='command-dispatcher', scope='window',
|
||||
maxsplit=0, no_replace_variables=True)
|
||||
@@ -1278,10 +1287,10 @@ class CommandDispatcher:
|
||||
|
||||
idx = self._current_index()
|
||||
if idx != -1:
|
||||
env['QUTE_TITLE'] = self._tabbed_browser.page_title(idx)
|
||||
env['QUTE_TITLE'] = self._tabbed_browser.widget.page_title(idx)
|
||||
|
||||
# FIXME:qtwebengine: If tab is None, run_async will fail!
|
||||
tab = self._tabbed_browser.currentWidget()
|
||||
tab = self._tabbed_browser.widget.currentWidget()
|
||||
|
||||
try:
|
||||
url = self._tabbed_browser.current_url()
|
||||
@@ -1639,7 +1648,7 @@ class CommandDispatcher:
|
||||
|
||||
ed = editor.ExternalEditor(watch=True, parent=self._tabbed_browser)
|
||||
ed.file_updated.connect(functools.partial(
|
||||
self.on_file_updated, elem))
|
||||
self.on_file_updated, ed, elem))
|
||||
ed.editing_finished.connect(lambda: mainwindow.raise_window(
|
||||
objreg.last_focused_window(), alert=False))
|
||||
ed.edit(text, caret_position)
|
||||
@@ -1654,7 +1663,7 @@ class CommandDispatcher:
|
||||
tab = self._current_widget()
|
||||
tab.elements.find_focused(self._open_editor_cb)
|
||||
|
||||
def on_file_updated(self, elem, text):
|
||||
def on_file_updated(self, ed, elem, text):
|
||||
"""Write the editor text into the form field and clean up tempfile.
|
||||
|
||||
Callback for GUIProcess when the edited text was updated.
|
||||
@@ -1667,8 +1676,10 @@ class CommandDispatcher:
|
||||
elem.set_value(text)
|
||||
except webelem.OrphanedError as e:
|
||||
message.error('Edited element vanished')
|
||||
ed.backup()
|
||||
except webelem.Error as e:
|
||||
raise cmdexc.CommandError(str(e))
|
||||
message.error(str(e))
|
||||
ed.backup()
|
||||
|
||||
@cmdutils.register(instance='command-dispatcher', maxsplit=0,
|
||||
scope='window')
|
||||
@@ -2217,5 +2228,5 @@ class CommandDispatcher:
|
||||
pass
|
||||
return
|
||||
|
||||
window = self._tabbed_browser.window()
|
||||
window = self._tabbed_browser.widget.window()
|
||||
window.setWindowState(window.windowState() ^ Qt.WindowFullScreen)
|
||||
|
||||
@@ -30,7 +30,8 @@ import textwrap
|
||||
import attr
|
||||
from PyQt5.QtCore import pyqtSignal, QObject, QUrl
|
||||
|
||||
from qutebrowser.utils import log, standarddir, jinja, objreg, utils
|
||||
from qutebrowser.utils import (log, standarddir, jinja, objreg, utils,
|
||||
javascript, urlmatch)
|
||||
from qutebrowser.commands import cmdutils
|
||||
from qutebrowser.browser import downloads
|
||||
|
||||
@@ -47,6 +48,7 @@ class GreasemonkeyScript:
|
||||
def __init__(self, properties, code):
|
||||
self._code = code
|
||||
self.includes = []
|
||||
self.matches = []
|
||||
self.excludes = []
|
||||
self.requires = []
|
||||
self.description = None
|
||||
@@ -62,8 +64,10 @@ class GreasemonkeyScript:
|
||||
self.namespace = value
|
||||
elif name == 'description':
|
||||
self.description = value
|
||||
elif name in ['include', 'match']:
|
||||
elif name == 'include':
|
||||
self.includes.append(value)
|
||||
elif name == 'match':
|
||||
self.matches.append(value)
|
||||
elif name in ['exclude', 'exclude_match']:
|
||||
self.excludes.append(value)
|
||||
elif name == 'run-at':
|
||||
@@ -91,7 +95,7 @@ class GreasemonkeyScript:
|
||||
props = ""
|
||||
script = cls(re.findall(cls.PROPS_REGEX, props), source)
|
||||
script.script_meta = props
|
||||
if not props:
|
||||
if not script.includes and not script.matches:
|
||||
script.includes = ['*']
|
||||
return script
|
||||
|
||||
@@ -104,18 +108,19 @@ class GreasemonkeyScript:
|
||||
browser's debugger/inspector will not match up to the line
|
||||
numbers in the source script directly.
|
||||
"""
|
||||
return jinja.js_environment.get_template(
|
||||
'greasemonkey_wrapper.js').render(
|
||||
scriptName="/".join([self.namespace or '', self.name]),
|
||||
scriptInfo=self._meta_json(),
|
||||
scriptMeta=self.script_meta,
|
||||
scriptSource=self._code)
|
||||
template = jinja.js_environment.get_template('greasemonkey_wrapper.js')
|
||||
return template.render(
|
||||
scriptName=javascript.string_escape(
|
||||
"/".join([self.namespace or '', self.name])),
|
||||
scriptInfo=self._meta_json(),
|
||||
scriptMeta=javascript.string_escape(self.script_meta),
|
||||
scriptSource=self._code)
|
||||
|
||||
def _meta_json(self):
|
||||
return json.dumps({
|
||||
'name': self.name,
|
||||
'description': self.description,
|
||||
'matches': self.includes,
|
||||
'matches': self.matches,
|
||||
'includes': self.includes,
|
||||
'excludes': self.excludes,
|
||||
'run-at': self.run_at,
|
||||
@@ -141,6 +146,42 @@ class MatchingScripts(object):
|
||||
idle = attr.ib(default=attr.Factory(list))
|
||||
|
||||
|
||||
class GreasemonkeyMatcher:
|
||||
|
||||
"""Check whether scripts should be loaded for a given URL."""
|
||||
|
||||
# https://wiki.greasespot.net/Include_and_exclude_rules#Greaseable_schemes
|
||||
# Limit the schemes scripts can run on due to unreasonable levels of
|
||||
# exploitability
|
||||
GREASEABLE_SCHEMES = ['http', 'https', 'ftp', 'file']
|
||||
|
||||
def __init__(self, url):
|
||||
self._url = url
|
||||
self._url_string = url.toString(QUrl.FullyEncoded)
|
||||
self.is_greaseable = url.scheme() in self.GREASEABLE_SCHEMES
|
||||
|
||||
def _match_pattern(self, pattern):
|
||||
# For include and exclude rules if they start and end with '/' they
|
||||
# should be treated as a (ecma syntax) regular expression.
|
||||
if pattern.startswith('/') and pattern.endswith('/'):
|
||||
matches = re.search(pattern[1:-1], self._url_string, flags=re.I)
|
||||
return matches is not None
|
||||
|
||||
# Otherwise they are glob expressions.
|
||||
return fnmatch.fnmatch(self._url_string, pattern)
|
||||
|
||||
def matches(self, script):
|
||||
"""Check whether the URL matches filtering rules of the script."""
|
||||
assert self.is_greaseable
|
||||
matching_includes = any(self._match_pattern(pat)
|
||||
for pat in script.includes)
|
||||
matching_match = any(urlmatch.UrlPattern(pat).matches(self._url)
|
||||
for pat in script.matches)
|
||||
matching_excludes = any(self._match_pattern(pat)
|
||||
for pat in script.excludes)
|
||||
return (matching_includes or matching_match) and not matching_excludes
|
||||
|
||||
|
||||
class GreasemonkeyManager(QObject):
|
||||
|
||||
"""Manager of userscripts and a Greasemonkey compatible environment.
|
||||
@@ -152,10 +193,6 @@ class GreasemonkeyManager(QObject):
|
||||
"""
|
||||
|
||||
scripts_reloaded = pyqtSignal()
|
||||
# https://wiki.greasespot.net/Include_and_exclude_rules#Greaseable_schemes
|
||||
# Limit the schemes scripts can run on due to unreasonable levels of
|
||||
# exploitability
|
||||
greaseable_schemes = ['http', 'https', 'ftp', 'file']
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
@@ -307,30 +344,17 @@ class GreasemonkeyManager(QObject):
|
||||
returns a tuple of lists of scripts meant to run at (document-start,
|
||||
document-end, document-idle)
|
||||
"""
|
||||
if url.scheme() not in self.greaseable_schemes:
|
||||
matcher = GreasemonkeyMatcher(url)
|
||||
if not matcher.is_greaseable:
|
||||
return MatchingScripts(url, [], [], [])
|
||||
|
||||
string_url = url.toString(QUrl.FullyEncoded)
|
||||
|
||||
def _match(pattern):
|
||||
# For include and exclude rules if they start and end with '/' they
|
||||
# should be treated as a (ecma syntax) regular expression.
|
||||
if pattern.startswith('/') and pattern.endswith('/'):
|
||||
matches = re.search(pattern[1:-1], string_url, flags=re.I)
|
||||
return matches is not None
|
||||
|
||||
# Otherwise they are glob expressions.
|
||||
return fnmatch.fnmatch(string_url, pattern)
|
||||
|
||||
tester = (lambda script:
|
||||
any(_match(pat) for pat in script.includes) and
|
||||
not any(_match(pat) for pat in script.excludes))
|
||||
|
||||
return MatchingScripts(
|
||||
url,
|
||||
[script for script in self._run_start if tester(script)],
|
||||
[script for script in self._run_end if tester(script)],
|
||||
[script for script in self._run_idle if tester(script)]
|
||||
url=url,
|
||||
start=[script for script in self._run_start
|
||||
if matcher.matches(script)],
|
||||
end=[script for script in self._run_end
|
||||
if matcher.matches(script)],
|
||||
idle=[script for script in self._run_idle
|
||||
if matcher.matches(script)]
|
||||
)
|
||||
|
||||
def all_scripts(self):
|
||||
|
||||
@@ -682,7 +682,7 @@ class HintManager(QObject):
|
||||
"""
|
||||
tabbed_browser = objreg.get('tabbed-browser', scope='window',
|
||||
window=self._win_id)
|
||||
tab = tabbed_browser.currentWidget()
|
||||
tab = tabbed_browser.widget.currentWidget()
|
||||
if tab is None:
|
||||
raise cmdexc.CommandError("No WebView available yet!")
|
||||
|
||||
|
||||
@@ -162,6 +162,7 @@ class DownloadItem(downloads.AbstractDownloadItem):
|
||||
QTimer.singleShot(0, lambda: self._die(reply.errorString()))
|
||||
|
||||
def _do_cancel(self):
|
||||
self._read_timer.stop()
|
||||
if self._reply is not None:
|
||||
self._reply.finished.disconnect(self._on_reply_finished)
|
||||
self._reply.abort()
|
||||
|
||||
@@ -76,11 +76,11 @@ class SignalFilter(QObject):
|
||||
tabbed_browser = objreg.get('tabbed-browser', scope='window',
|
||||
window=self._win_id)
|
||||
try:
|
||||
tabidx = tabbed_browser.indexOf(tab)
|
||||
tabidx = tabbed_browser.widget.indexOf(tab)
|
||||
except RuntimeError:
|
||||
# The tab has been deleted already
|
||||
return
|
||||
if tabidx == tabbed_browser.currentIndex():
|
||||
if tabidx == tabbed_browser.widget.currentIndex():
|
||||
if log_signal:
|
||||
log.signals.debug("emitting: {} (tab {})".format(
|
||||
debug.dbg_signal(signal, args), tabidx))
|
||||
|
||||
@@ -24,16 +24,18 @@ import os
|
||||
import re
|
||||
|
||||
from PyQt5.QtCore import QLibraryInfo
|
||||
from qutebrowser.utils import log
|
||||
from qutebrowser.utils import log, message
|
||||
|
||||
dict_version_re = re.compile(r".+-(?P<version>[0-9]+-[0-9]+?)\.bdic")
|
||||
|
||||
|
||||
def version(filename):
|
||||
"""Extract the version number from the dictionary file name."""
|
||||
version_re = re.compile(r".+-(?P<version>[0-9]+-[0-9]+?)\.bdic")
|
||||
match = version_re.fullmatch(filename)
|
||||
match = dict_version_re.match(filename)
|
||||
if match is None:
|
||||
raise ValueError('the given dictionary file name is malformed: {}'
|
||||
.format(filename))
|
||||
message.warning(
|
||||
"Found a dictionary with a malformed name: {}".format(filename))
|
||||
return None
|
||||
return tuple(int(n) for n in match.group('version').split('-'))
|
||||
|
||||
|
||||
@@ -44,15 +46,23 @@ def dictionary_dir():
|
||||
|
||||
|
||||
def local_files(code):
|
||||
"""Return all installed dictionaries for the given code."""
|
||||
"""Return all installed dictionaries for the given code.
|
||||
|
||||
The returned dictionaries are sorted by version, therefore the latest will
|
||||
be the first element. The list will be empty if no dictionaries are found.
|
||||
"""
|
||||
pathname = os.path.join(dictionary_dir(), '{}*.bdic'.format(code))
|
||||
matching_dicts = glob.glob(pathname)
|
||||
files = []
|
||||
for matching_dict in sorted(matching_dicts, key=version, reverse=True):
|
||||
filename = os.path.basename(matching_dict)
|
||||
log.config.debug('Found file for dict {}: {}'.format(code, filename))
|
||||
files.append(filename)
|
||||
return files
|
||||
versioned_dicts = []
|
||||
for matching_dict in matching_dicts:
|
||||
parsed_version = version(matching_dict)
|
||||
if parsed_version is not None:
|
||||
filename = os.path.basename(matching_dict)
|
||||
log.config.debug('Found file for dict {}: {}'
|
||||
.format(code, filename))
|
||||
versioned_dicts.append((parsed_version, filename))
|
||||
return [filename for version, filename
|
||||
in sorted(versioned_dicts, reverse=True)]
|
||||
|
||||
|
||||
def local_filename(code):
|
||||
|
||||
@@ -101,7 +101,11 @@ class DownloadItem(downloads.AbstractDownloadItem):
|
||||
|
||||
def retry(self):
|
||||
state = self._qt_item.state()
|
||||
assert state == QWebEngineDownloadItem.DownloadInterrupted, state
|
||||
if state != QWebEngineDownloadItem.DownloadInterrupted:
|
||||
log.downloads.warning(
|
||||
"Trying to retry download in state {}".format(
|
||||
debug.qenum_key(QWebEngineDownloadItem, state)))
|
||||
return
|
||||
|
||||
try:
|
||||
self._qt_item.resume()
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
import os
|
||||
|
||||
from PyQt5.QtCore import QUrl
|
||||
from PyQt5.QtWebEngineWidgets import QWebEngineView
|
||||
from PyQt5.QtWebEngineWidgets import QWebEngineView, QWebEngineSettings
|
||||
|
||||
from qutebrowser.browser import inspector
|
||||
|
||||
@@ -35,6 +35,8 @@ class WebEngineInspector(inspector.AbstractWebInspector):
|
||||
super().__init__(parent)
|
||||
self.port = None
|
||||
view = QWebEngineView()
|
||||
settings = view.settings()
|
||||
settings.setAttribute(QWebEngineSettings.JavascriptEnabled, True)
|
||||
self._set_widget(view)
|
||||
|
||||
def inspect(self, _page):
|
||||
|
||||
@@ -26,16 +26,12 @@ Module attributes:
|
||||
|
||||
import os
|
||||
|
||||
import sip
|
||||
from PyQt5.QtGui import QFont
|
||||
from PyQt5.QtWebEngineWidgets import (QWebEngineSettings, QWebEngineProfile,
|
||||
QWebEngineScript)
|
||||
from PyQt5.QtWebEngineWidgets import QWebEngineSettings, QWebEngineProfile
|
||||
|
||||
from qutebrowser.browser import shared
|
||||
from qutebrowser.browser.webengine import spell
|
||||
from qutebrowser.config import config, websettings
|
||||
from qutebrowser.utils import (utils, standarddir, javascript, qtutils,
|
||||
message, log, objreg)
|
||||
from qutebrowser.utils import utils, standarddir, qtutils, message, log
|
||||
|
||||
# The default QWebEngineProfile
|
||||
default_profile = None
|
||||
@@ -169,133 +165,92 @@ class WebEngineSettings(websettings.AbstractSettings):
|
||||
self._ATTRIBUTES[name] = [value]
|
||||
|
||||
|
||||
def _init_stylesheet(profile):
|
||||
"""Initialize custom stylesheets.
|
||||
class ProfileSetter:
|
||||
|
||||
Partially inspired by QupZilla:
|
||||
https://github.com/QupZilla/qupzilla/blob/v2.0/src/lib/app/mainapplication.cpp#L1063-L1101
|
||||
"""
|
||||
old_script = profile.scripts().findScript('_qute_stylesheet')
|
||||
if not old_script.isNull():
|
||||
profile.scripts().remove(old_script)
|
||||
"""Helper to set various settings on a profile."""
|
||||
|
||||
css = shared.get_user_stylesheet()
|
||||
source = '\n'.join([
|
||||
'"use strict";',
|
||||
'window._qutebrowser = window._qutebrowser || {};',
|
||||
utils.read_file('javascript/stylesheet.js'),
|
||||
javascript.assemble('stylesheet', 'set_css', css),
|
||||
])
|
||||
def __init__(self, profile):
|
||||
self._profile = profile
|
||||
|
||||
script = QWebEngineScript()
|
||||
script.setName('_qute_stylesheet')
|
||||
script.setInjectionPoint(QWebEngineScript.DocumentCreation)
|
||||
script.setWorldId(QWebEngineScript.ApplicationWorld)
|
||||
script.setRunsOnSubFrames(True)
|
||||
script.setSourceCode(source)
|
||||
profile.scripts().insert(script)
|
||||
def init_profile(self):
|
||||
"""Initialize settings on the given profile."""
|
||||
self.set_http_headers()
|
||||
self.set_http_cache_size()
|
||||
self._profile.settings().setAttribute(
|
||||
QWebEngineSettings.FullScreenSupportEnabled, True)
|
||||
if qtutils.version_check('5.8'):
|
||||
self.set_dictionary_language()
|
||||
|
||||
def set_http_headers(self):
|
||||
"""Set the user agent and accept-language for the given profile.
|
||||
|
||||
def _update_stylesheet():
|
||||
"""Update the custom stylesheet in existing tabs."""
|
||||
css = shared.get_user_stylesheet()
|
||||
code = javascript.assemble('stylesheet', 'set_css', css)
|
||||
for win_id, window in objreg.window_registry.items():
|
||||
# We could be in the middle of destroying a window here
|
||||
if sip.isdeleted(window):
|
||||
continue
|
||||
tab_registry = objreg.get('tab-registry', scope='window',
|
||||
window=win_id)
|
||||
for tab in tab_registry.values():
|
||||
tab.run_js_async(code)
|
||||
We override those per request in the URL interceptor (to allow for
|
||||
per-domain values), but this one still gets used for things like
|
||||
window.navigator.userAgent/.languages in JS.
|
||||
"""
|
||||
self._profile.setHttpUserAgent(config.val.content.headers.user_agent)
|
||||
accept_language = config.val.content.headers.accept_language
|
||||
if accept_language is not None:
|
||||
self._profile.setHttpAcceptLanguage(accept_language)
|
||||
|
||||
def set_http_cache_size(self):
|
||||
"""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)
|
||||
|
||||
def _set_http_headers(profile):
|
||||
"""Set the user agent and accept-language for the given profile.
|
||||
# 0: automatically managed by QtWebEngine
|
||||
self._profile.setHttpCacheMaximumSize(size)
|
||||
|
||||
We override those per request in the URL interceptor (to allow for
|
||||
per-domain values), but this one still gets used for things like
|
||||
window.navigator.userAgent/.languages in JS.
|
||||
"""
|
||||
profile.setHttpUserAgent(config.val.content.headers.user_agent)
|
||||
accept_language = config.val.content.headers.accept_language
|
||||
if accept_language is not None:
|
||||
profile.setHttpAcceptLanguage(accept_language)
|
||||
def set_persistent_cookie_policy(self):
|
||||
"""Set the HTTP Cookie size for the given profile."""
|
||||
assert not self._profile.isOffTheRecord()
|
||||
if config.val.content.cookies.store:
|
||||
value = QWebEngineProfile.AllowPersistentCookies
|
||||
else:
|
||||
value = QWebEngineProfile.NoPersistentCookies
|
||||
self._profile.setPersistentCookiesPolicy(value)
|
||||
|
||||
def set_dictionary_language(self, warn=True):
|
||||
"""Load the given dictionaries."""
|
||||
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
|
||||
|
||||
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)
|
||||
filenames.append(local_filename)
|
||||
|
||||
# 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)
|
||||
log.config.debug("Found dicts: {}".format(filenames))
|
||||
self._profile.setSpellCheckLanguages(filenames)
|
||||
self._profile.setSpellCheckEnabled(bool(filenames))
|
||||
|
||||
|
||||
def _update_settings(option):
|
||||
"""Update global settings when qwebsettings changed."""
|
||||
global_settings.update_setting(option)
|
||||
|
||||
if option in ['scrolling.bar', 'content.user_stylesheets']:
|
||||
_init_stylesheet(default_profile)
|
||||
_init_stylesheet(private_profile)
|
||||
_update_stylesheet()
|
||||
elif option in ['content.headers.user_agent',
|
||||
'content.headers.accept_language']:
|
||||
_set_http_headers(default_profile)
|
||||
_set_http_headers(private_profile)
|
||||
if option in ['content.headers.user_agent',
|
||||
'content.headers.accept_language']:
|
||||
default_profile.setter.set_http_headers()
|
||||
private_profile.setter.set_http_headers()
|
||||
elif option == 'content.cache.size':
|
||||
_set_http_cache_size(default_profile)
|
||||
_set_http_cache_size(private_profile)
|
||||
default_profile.setter.set_http_cache_size()
|
||||
private_profile.setter.set_http_cache_size()
|
||||
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)
|
||||
default_profile.setter.set_persistent_cookie_policy()
|
||||
# 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)
|
||||
default_profile.setter.set_dictionary_language()
|
||||
private_profile.setter.set_dictionary_language(warn=False)
|
||||
|
||||
|
||||
def _init_profiles():
|
||||
@@ -303,53 +258,18 @@ def _init_profiles():
|
||||
global default_profile, private_profile
|
||||
|
||||
default_profile = QWebEngineProfile.defaultProfile()
|
||||
default_profile.setter = ProfileSetter(default_profile)
|
||||
default_profile.setCachePath(
|
||||
os.path.join(standarddir.cache(), 'webengine'))
|
||||
default_profile.setPersistentStoragePath(
|
||||
os.path.join(standarddir.data(), 'webengine'))
|
||||
_init_profile(default_profile)
|
||||
_set_persistent_cookie_policy(default_profile)
|
||||
default_profile.setter.init_profile()
|
||||
default_profile.setter.set_persistent_cookie_policy()
|
||||
|
||||
private_profile = QWebEngineProfile()
|
||||
private_profile.setter = ProfileSetter(private_profile)
|
||||
assert private_profile.isOffTheRecord()
|
||||
_init_profile(private_profile)
|
||||
|
||||
|
||||
def inject_userscripts():
|
||||
"""Register user JavaScript files with the global profiles."""
|
||||
# The Greasemonkey metadata block support in QtWebEngine only starts at
|
||||
# Qt 5.8. With 5.7.1, we need to inject the scripts ourselves in response
|
||||
# to urlChanged.
|
||||
if not qtutils.version_check('5.8'):
|
||||
return
|
||||
|
||||
# Since we are inserting scripts into profile.scripts they won't
|
||||
# just get replaced by new gm scripts like if we were injecting them
|
||||
# ourselves so we need to remove all gm scripts, while not removing
|
||||
# any other stuff that might have been added. Like the one for
|
||||
# stylesheets.
|
||||
greasemonkey = objreg.get('greasemonkey')
|
||||
for profile in [default_profile, private_profile]:
|
||||
scripts = profile.scripts()
|
||||
for script in scripts.toList():
|
||||
if script.name().startswith("GM-"):
|
||||
log.greasemonkey.debug('Removing script: {}'
|
||||
.format(script.name()))
|
||||
removed = scripts.remove(script)
|
||||
assert removed, script.name()
|
||||
|
||||
# Then add the new scripts.
|
||||
for script in greasemonkey.all_scripts():
|
||||
# @run-at (and @include/@exclude/@match) is parsed by
|
||||
# QWebEngineScript.
|
||||
new_script = QWebEngineScript()
|
||||
new_script.setWorldId(QWebEngineScript.MainWorld)
|
||||
new_script.setSourceCode(script.code())
|
||||
new_script.setName("GM-{}".format(script.name))
|
||||
new_script.setRunsOnSubFrames(script.runs_on_sub_frames)
|
||||
log.greasemonkey.debug('adding script: {}'
|
||||
.format(new_script.name()))
|
||||
scripts.insert(new_script)
|
||||
private_profile.setter.init_profile()
|
||||
|
||||
|
||||
def init(args):
|
||||
|
||||
@@ -33,7 +33,7 @@ from PyQt5.QtNetwork import QAuthenticator
|
||||
from PyQt5.QtWidgets import QApplication
|
||||
from PyQt5.QtWebEngineWidgets import QWebEnginePage, QWebEngineScript
|
||||
|
||||
from qutebrowser.config import configdata
|
||||
from qutebrowser.config import configdata, config
|
||||
from qutebrowser.browser import browsertab, mouse, shared
|
||||
from qutebrowser.browser.webengine import (webview, webengineelem, tabhistory,
|
||||
interceptor, webenginequtescheme,
|
||||
@@ -73,10 +73,6 @@ def init():
|
||||
download_manager.install(webenginesettings.private_profile)
|
||||
objreg.register('webengine-download-manager', download_manager)
|
||||
|
||||
greasemonkey = objreg.get('greasemonkey')
|
||||
greasemonkey.scripts_reloaded.connect(webenginesettings.inject_userscripts)
|
||||
webenginesettings.inject_userscripts()
|
||||
|
||||
|
||||
# Mapping worlds from usertypes.JsWorld to QWebEngineScript world IDs.
|
||||
_JS_WORLD_MAP = {
|
||||
@@ -234,7 +230,14 @@ class WebEngineCaret(browsertab.AbstractCaret):
|
||||
|
||||
self._tab.run_js_async(
|
||||
javascript.assemble('caret', 'setPlatform', sys.platform))
|
||||
self._js_call('setInitialCursor')
|
||||
self._js_call('setInitialCursor', self._selection_cb)
|
||||
|
||||
def _selection_cb(self, enabled):
|
||||
"""Emit selection_toggled based on setInitialCursor."""
|
||||
if enabled is None:
|
||||
log.webview.debug("Ignoring selection status None")
|
||||
return
|
||||
self.selection_toggled.emit(enabled)
|
||||
|
||||
@pyqtSlot(usertypes.KeyMode)
|
||||
def _on_mode_left(self, mode):
|
||||
@@ -301,7 +304,7 @@ class WebEngineCaret(browsertab.AbstractCaret):
|
||||
self._js_call('moveToEndOfDocument')
|
||||
|
||||
def toggle_selection(self):
|
||||
self._js_call('toggleSelection')
|
||||
self._js_call('toggleSelection', self.selection_toggled.emit)
|
||||
|
||||
def drop_selection(self):
|
||||
self._js_call('dropSelection')
|
||||
@@ -356,9 +359,8 @@ class WebEngineCaret(browsertab.AbstractCaret):
|
||||
self._tab.run_js_async(js_code, lambda jsret:
|
||||
self._follow_selected_cb(jsret, tab))
|
||||
|
||||
def _js_call(self, command):
|
||||
self._tab.run_js_async(
|
||||
javascript.assemble('caret', command))
|
||||
def _js_call(self, command, callback=None):
|
||||
self._tab.run_js_async(javascript.assemble('caret', command), callback)
|
||||
|
||||
|
||||
class WebEngineScroller(browsertab.AbstractScroller):
|
||||
@@ -379,7 +381,7 @@ class WebEngineScroller(browsertab.AbstractScroller):
|
||||
|
||||
def _repeated_key_press(self, key, count=1, modifier=Qt.NoModifier):
|
||||
"""Send count fake key presses to this scroller's WebEngineTab."""
|
||||
for _ in range(min(count, 5000)):
|
||||
for _ in range(min(count, 1000)):
|
||||
self._tab.key_press(key, modifier)
|
||||
|
||||
@pyqtSlot(QPointF)
|
||||
@@ -432,6 +434,11 @@ class WebEngineScroller(browsertab.AbstractScroller):
|
||||
js_code = javascript.assemble('window', 'scroll', point.x(), point.y())
|
||||
self._tab.run_js_async(js_code)
|
||||
|
||||
def to_anchor(self, name):
|
||||
url = self._tab.url()
|
||||
url.setFragment(name)
|
||||
self._tab.openurl(url)
|
||||
|
||||
def delta(self, x=0, y=0):
|
||||
self._tab.run_js_async(javascript.assemble('window', 'scrollBy', x, y))
|
||||
|
||||
@@ -506,6 +513,9 @@ class WebEngineHistory(browsertab.AbstractHistory):
|
||||
return qtutils.deserialize(data, self._history)
|
||||
|
||||
def load_items(self, items):
|
||||
if items:
|
||||
self._tab.predicted_navigation.emit(items[-1].url)
|
||||
|
||||
stream, _data, cur_data = tabhistory.serialize(items)
|
||||
qtutils.deserialize_stream(stream, self._history)
|
||||
|
||||
@@ -627,33 +637,127 @@ class WebEngineTab(browsertab.AbstractTab):
|
||||
self._set_widget(widget)
|
||||
self._connect_signals()
|
||||
self.backend = usertypes.Backend.QtWebEngine
|
||||
self._init_js()
|
||||
self._child_event_filter = None
|
||||
self._saved_zoom = None
|
||||
self._reload_url = None
|
||||
config.instance.changed.connect(self._on_config_changed)
|
||||
self._init_js()
|
||||
|
||||
@pyqtSlot(str)
|
||||
def _on_config_changed(self, option):
|
||||
if option in ['scrolling.bar', 'content.user_stylesheets']:
|
||||
self._init_stylesheet()
|
||||
self._update_stylesheet()
|
||||
|
||||
def _update_stylesheet(self):
|
||||
"""Update the custom stylesheet in existing tabs."""
|
||||
css = shared.get_user_stylesheet()
|
||||
code = javascript.assemble('stylesheet', 'set_css', css)
|
||||
self.run_js_async(code)
|
||||
|
||||
def _inject_early_js(self, name, js_code, *,
|
||||
world=QWebEngineScript.ApplicationWorld,
|
||||
subframes=False):
|
||||
"""Inject the given script to run early on a page load.
|
||||
|
||||
This runs the script both on DocumentCreation and DocumentReady as on
|
||||
some internal pages, DocumentCreation will not work.
|
||||
|
||||
That is a WORKAROUND for https://bugreports.qt.io/browse/QTBUG-66011
|
||||
"""
|
||||
scripts = self._widget.page().scripts()
|
||||
for injection in ['creation', 'ready']:
|
||||
injection_points = {
|
||||
'creation': QWebEngineScript.DocumentCreation,
|
||||
'ready': QWebEngineScript.DocumentReady,
|
||||
}
|
||||
script = QWebEngineScript()
|
||||
script.setInjectionPoint(injection_points[injection])
|
||||
script.setSourceCode(js_code)
|
||||
script.setWorldId(world)
|
||||
script.setRunsOnSubFrames(subframes)
|
||||
script.setName('_qute_{}_{}'.format(name, injection))
|
||||
scripts.insert(script)
|
||||
|
||||
def _remove_early_js(self, name):
|
||||
"""Remove an early QWebEngineScript."""
|
||||
scripts = self._widget.page().scripts()
|
||||
for injection in ['creation', 'ready']:
|
||||
full_name = '_qute_{}_{}'.format(name, injection)
|
||||
script = scripts.findScript(full_name)
|
||||
if not script.isNull():
|
||||
scripts.remove(script)
|
||||
|
||||
def _init_js(self):
|
||||
js_code = '\n'.join([
|
||||
'"use strict";',
|
||||
'window._qutebrowser = window._qutebrowser || {};',
|
||||
"""Initialize global qutebrowser JavaScript."""
|
||||
js_code = javascript.wrap_global(
|
||||
'scripts',
|
||||
utils.read_file('javascript/scroll.js'),
|
||||
utils.read_file('javascript/webelem.js'),
|
||||
utils.read_file('javascript/caret.js'),
|
||||
])
|
||||
script = QWebEngineScript()
|
||||
# We can't use DocumentCreation here as WORKAROUND for
|
||||
# https://bugreports.qt.io/browse/QTBUG-66011
|
||||
script.setInjectionPoint(QWebEngineScript.DocumentReady)
|
||||
script.setSourceCode(js_code)
|
||||
)
|
||||
# FIXME:qtwebengine what about subframes=True?
|
||||
self._inject_early_js('js', js_code, subframes=True)
|
||||
self._init_stylesheet()
|
||||
|
||||
page = self._widget.page()
|
||||
script.setWorldId(QWebEngineScript.ApplicationWorld)
|
||||
greasemonkey = objreg.get('greasemonkey')
|
||||
greasemonkey.scripts_reloaded.connect(self._inject_userscripts)
|
||||
self._inject_userscripts()
|
||||
|
||||
# FIXME:qtwebengine what about runsOnSubFrames?
|
||||
page.scripts().insert(script)
|
||||
def _init_stylesheet(self):
|
||||
"""Initialize custom stylesheets.
|
||||
|
||||
Partially inspired by QupZilla:
|
||||
https://github.com/QupZilla/qupzilla/blob/v2.0/src/lib/app/mainapplication.cpp#L1063-L1101
|
||||
"""
|
||||
self._remove_early_js('stylesheet')
|
||||
css = shared.get_user_stylesheet()
|
||||
js_code = javascript.wrap_global(
|
||||
'stylesheet',
|
||||
utils.read_file('javascript/stylesheet.js'),
|
||||
javascript.assemble('stylesheet', 'set_css', css),
|
||||
)
|
||||
self._inject_early_js('stylesheet', js_code, subframes=True)
|
||||
|
||||
def _inject_userscripts(self):
|
||||
"""Register user JavaScript files with the global profiles."""
|
||||
# The Greasemonkey metadata block support in QtWebEngine only starts at
|
||||
# Qt 5.8. With 5.7.1, we need to inject the scripts ourselves in
|
||||
# response to urlChanged.
|
||||
if not qtutils.version_check('5.8'):
|
||||
return
|
||||
|
||||
# Since we are inserting scripts into profile.scripts they won't
|
||||
# just get replaced by new gm scripts like if we were injecting them
|
||||
# ourselves so we need to remove all gm scripts, while not removing
|
||||
# any other stuff that might have been added. Like the one for
|
||||
# stylesheets.
|
||||
greasemonkey = objreg.get('greasemonkey')
|
||||
scripts = self._widget.page().scripts()
|
||||
for script in scripts.toList():
|
||||
if script.name().startswith("GM-"):
|
||||
log.greasemonkey.debug('Removing script: {}'
|
||||
.format(script.name()))
|
||||
removed = scripts.remove(script)
|
||||
assert removed, script.name()
|
||||
|
||||
# Then add the new scripts.
|
||||
for script in greasemonkey.all_scripts():
|
||||
# @run-at (and @include/@exclude/@match) is parsed by
|
||||
# QWebEngineScript.
|
||||
new_script = QWebEngineScript()
|
||||
new_script.setWorldId(QWebEngineScript.MainWorld)
|
||||
new_script.setSourceCode(script.code())
|
||||
new_script.setName("GM-{}".format(script.name))
|
||||
new_script.setRunsOnSubFrames(script.runs_on_sub_frames)
|
||||
log.greasemonkey.debug('adding script: {}'
|
||||
.format(new_script.name()))
|
||||
scripts.insert(new_script)
|
||||
|
||||
def _install_event_filter(self):
|
||||
self._widget.focusProxy().installEventFilter(self._mouse_event_filter)
|
||||
fp = self._widget.focusProxy()
|
||||
if fp is not None:
|
||||
fp.installEventFilter(self._mouse_event_filter)
|
||||
self._child_event_filter = mouse.ChildEventFilter(
|
||||
eventfilter=self._mouse_event_filter, widget=self._widget,
|
||||
parent=self)
|
||||
@@ -669,9 +773,17 @@ class WebEngineTab(browsertab.AbstractTab):
|
||||
self.zoom.set_factor(self._saved_zoom)
|
||||
self._saved_zoom = None
|
||||
|
||||
def openurl(self, url):
|
||||
def openurl(self, url, *, predict=True):
|
||||
"""Open the given URL in this tab.
|
||||
|
||||
Arguments:
|
||||
url: The QUrl to open.
|
||||
predict: If set to False, predicted_navigation is not emitted.
|
||||
"""
|
||||
# WORKAROUND for https://bugreports.qt.io/browse/QTBUG-68076
|
||||
self._widget.setFocus()
|
||||
self._saved_zoom = self.zoom.factor()
|
||||
self._openurl_prepare(url)
|
||||
self._openurl_prepare(url, predict=predict)
|
||||
self._widget.load(url)
|
||||
|
||||
def url(self, requested=False):
|
||||
@@ -706,7 +818,6 @@ class WebEngineTab(browsertab.AbstractTab):
|
||||
self._widget.shutdown()
|
||||
|
||||
def reload(self, *, force=False):
|
||||
self.predicted_navigation.emit(self.url())
|
||||
if force:
|
||||
action = QWebEnginePage.ReloadAndBypassCache
|
||||
else:
|
||||
@@ -915,10 +1026,10 @@ class WebEngineTab(browsertab.AbstractTab):
|
||||
if ok and self._reload_url is not None:
|
||||
# WORKAROUND for https://bugreports.qt.io/browse/QTBUG-66656
|
||||
log.config.debug(
|
||||
"Reloading {} because of config change".format(
|
||||
"Loading {} again because of config change".format(
|
||||
self._reload_url.toDisplayString()))
|
||||
QTimer.singleShot(100, lambda url=self._reload_url:
|
||||
self.openurl(url))
|
||||
self.openurl(url, predict=False))
|
||||
self._reload_url = None
|
||||
|
||||
if not qtutils.version_check('5.10', compiled=False):
|
||||
@@ -931,6 +1042,7 @@ class WebEngineTab(browsertab.AbstractTab):
|
||||
@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)
|
||||
|
||||
@@ -196,9 +196,10 @@ class WebKitCaret(browsertab.AbstractCaret):
|
||||
if mode != usertypes.KeyMode.caret:
|
||||
return
|
||||
|
||||
self.selection_enabled = self._widget.hasSelection()
|
||||
self.selection_toggled.emit(self.selection_enabled)
|
||||
settings = self._widget.settings()
|
||||
settings.setAttribute(QWebSettings.CaretBrowsingEnabled, True)
|
||||
self.selection_enabled = self._widget.hasSelection()
|
||||
|
||||
if self._widget.isVisible():
|
||||
# Sometimes the caret isn't immediately visible, but unfocusing
|
||||
@@ -363,9 +364,7 @@ class WebKitCaret(browsertab.AbstractCaret):
|
||||
|
||||
def toggle_selection(self):
|
||||
self.selection_enabled = not self.selection_enabled
|
||||
mainwindow = objreg.get('main-window', scope='window',
|
||||
window=self._tab.win_id)
|
||||
mainwindow.status.set_mode_active(usertypes.KeyMode.caret, True)
|
||||
self.selection_toggled.emit(self.selection_enabled)
|
||||
|
||||
def drop_selection(self):
|
||||
self._widget.triggerPageAction(QWebPage.MoveToNextChar)
|
||||
@@ -427,6 +426,9 @@ class WebKitScroller(browsertab.AbstractScroller):
|
||||
def to_point(self, point):
|
||||
self._widget.page().mainFrame().setScrollPosition(point)
|
||||
|
||||
def to_anchor(self, name):
|
||||
self._widget.page().mainFrame().scrollToAnchor(name)
|
||||
|
||||
def delta(self, x=0, y=0):
|
||||
qtutils.check_overflow(x, 'int')
|
||||
qtutils.check_overflow(y, 'int')
|
||||
@@ -537,6 +539,9 @@ class WebKitHistory(browsertab.AbstractHistory):
|
||||
return qtutils.deserialize(data, self._history)
|
||||
|
||||
def load_items(self, items):
|
||||
if items:
|
||||
self._tab.predicted_navigation.emit(items[-1].url)
|
||||
|
||||
stream, _data, user_data = tabhistory.serialize(items)
|
||||
qtutils.deserialize_stream(stream, self._history)
|
||||
for i, data in enumerate(user_data):
|
||||
@@ -668,8 +673,8 @@ class WebKitTab(browsertab.AbstractTab):
|
||||
settings = widget.settings()
|
||||
settings.setAttribute(QWebSettings.PrivateBrowsingEnabled, True)
|
||||
|
||||
def openurl(self, url):
|
||||
self._openurl_prepare(url)
|
||||
def openurl(self, url, *, predict=True):
|
||||
self._openurl_prepare(url, predict=predict)
|
||||
self._widget.openurl(url)
|
||||
|
||||
def url(self, requested=False):
|
||||
@@ -701,7 +706,6 @@ class WebKitTab(browsertab.AbstractTab):
|
||||
self._widget.shutdown()
|
||||
|
||||
def reload(self, *, force=False):
|
||||
self.predicted_navigation.emit(self.url())
|
||||
if force:
|
||||
action = QWebPage.ReloadAndBypassCache
|
||||
else:
|
||||
|
||||
@@ -239,7 +239,6 @@ class BrowserPage(QWebPage):
|
||||
printdiag.setAttribute(Qt.WA_DeleteOnClose)
|
||||
printdiag.open(lambda: frame.print(printdiag.printer()))
|
||||
|
||||
@pyqtSlot('QNetworkRequest')
|
||||
def on_download_requested(self, request):
|
||||
"""Called when the user wants to download a link.
|
||||
|
||||
|
||||
@@ -110,18 +110,18 @@ def _buffer(skip_win_id=None):
|
||||
model = completionmodel.CompletionModel(column_widths=(6, 40, 54))
|
||||
|
||||
for win_id in objreg.window_registry:
|
||||
if skip_win_id and win_id == skip_win_id:
|
||||
if skip_win_id is not None and win_id == skip_win_id:
|
||||
continue
|
||||
tabbed_browser = objreg.get('tabbed-browser', scope='window',
|
||||
window=win_id)
|
||||
if tabbed_browser.shutting_down:
|
||||
continue
|
||||
tabs = []
|
||||
for idx in range(tabbed_browser.count()):
|
||||
tab = tabbed_browser.widget(idx)
|
||||
for idx in range(tabbed_browser.widget.count()):
|
||||
tab = tabbed_browser.widget.widget(idx)
|
||||
tabs.append(("{}/{}".format(win_id, idx + 1),
|
||||
tab.url().toDisplayString(),
|
||||
tabbed_browser.page_title(idx)))
|
||||
tabbed_browser.widget.page_title(idx)))
|
||||
cat = listcategory.ListCategory("{}".format(win_id), tabs,
|
||||
delete_func=delete_buffer)
|
||||
model.add_category(cat)
|
||||
|
||||
@@ -425,11 +425,7 @@ content.host_blocking.enabled:
|
||||
|
||||
content.host_blocking.lists:
|
||||
default:
|
||||
- "https://www.malwaredomainlist.com/hostslist/hosts.txt"
|
||||
- "http://someonewhocares.org/hosts/hosts"
|
||||
- "http://winhelp2002.mvps.org/hosts.zip"
|
||||
- "http://malwaredomains.lehigh.edu/files/justdomains.zip"
|
||||
- "https://pgl.yoyo.org/adservers/serverlist.php?hostformat=hosts&mimetype=plaintext"
|
||||
- "https://raw.githubusercontent.com/StevenBlack/hosts/master/hosts"
|
||||
type:
|
||||
name: List
|
||||
valtype: Url
|
||||
@@ -1252,9 +1248,14 @@ tabs.favicons.scale:
|
||||
`tabs.padding`.
|
||||
|
||||
tabs.favicons.show:
|
||||
default: true
|
||||
type: Bool
|
||||
desc: Show favicons in the tab bar.
|
||||
default: always
|
||||
type:
|
||||
name: String
|
||||
valid_values:
|
||||
- always: Always show favicons.
|
||||
- never: Always hide favicons.
|
||||
- pinned: Show favicons only on pinned tabs.
|
||||
desc: When to show favicons in the tab bar.
|
||||
|
||||
tabs.last_close:
|
||||
default: ignore
|
||||
@@ -1325,7 +1326,10 @@ tabs.show:
|
||||
|
||||
tabs.show_switching_delay:
|
||||
default: 800
|
||||
type: Int
|
||||
type:
|
||||
name: Int
|
||||
minval: 0
|
||||
maxval: maxint
|
||||
desc: "Duration (in milliseconds) to show the tab bar before hiding it when
|
||||
tabs.show is set to 'switching'."
|
||||
|
||||
@@ -1406,6 +1410,19 @@ tabs.width:
|
||||
desc: "Width (in pixels or as percentage of the window) of the tab bar if
|
||||
it's vertical."
|
||||
|
||||
tabs.min_width:
|
||||
default: -1
|
||||
type:
|
||||
name: Int
|
||||
minval: -1
|
||||
maxval: maxint
|
||||
desc: >-
|
||||
Minimum width (in pixels) of tabs (-1 for the default minimum size behavior).
|
||||
|
||||
This setting only applies when tabs are horizontal.
|
||||
|
||||
This setting does not apply to pinned tabs, unless `tabs.pinned.shrink` is False.
|
||||
|
||||
tabs.width.indicator:
|
||||
renamed: tabs.indicator.width
|
||||
|
||||
@@ -1469,6 +1486,11 @@ url.incdec_segments:
|
||||
desc: URL segments where `:navigate increment/decrement` will search for
|
||||
a number.
|
||||
|
||||
url.open_base_url:
|
||||
type: Bool
|
||||
default: false
|
||||
desc: Open base URL of the searchengine if a searchengine shortcut is invoked without parameters.
|
||||
|
||||
url.searchengines:
|
||||
default:
|
||||
DEFAULT: https://duckduckgo.com/?q={}
|
||||
@@ -1513,10 +1535,15 @@ url.yank_ignored_parameters:
|
||||
## window
|
||||
|
||||
window.hide_wayland_decoration:
|
||||
renamed: window.hide_decoration
|
||||
|
||||
window.hide_decoration:
|
||||
type: Bool
|
||||
default: false
|
||||
restart: true
|
||||
desc: Hide the window decoration when using wayland.
|
||||
desc: |
|
||||
Hide the window decoration.
|
||||
|
||||
This setting requires a restart on Wayland.
|
||||
|
||||
window.title_format:
|
||||
type:
|
||||
|
||||
@@ -268,6 +268,15 @@ class YamlConfig(QObject):
|
||||
del settings['bindings.default']
|
||||
self._mark_changed()
|
||||
|
||||
# Option to show favicons only for pinned tabs changed the type of
|
||||
# tabs.favicons.show from Bool to String
|
||||
name = 'tabs.favicons.show'
|
||||
if name in settings:
|
||||
for scope, val in settings[name].items():
|
||||
if isinstance(val, bool):
|
||||
settings[name][scope] = 'always' if val else 'never'
|
||||
self._mark_changed()
|
||||
|
||||
return settings
|
||||
|
||||
def _validate(self, settings):
|
||||
|
||||
@@ -26,7 +26,8 @@ from PyQt5.QtWidgets import QMessageBox
|
||||
|
||||
from qutebrowser.config import (config, configdata, configfiles, configtypes,
|
||||
configexc, configcommands)
|
||||
from qutebrowser.utils import objreg, usertypes, log, standarddir, message
|
||||
from qutebrowser.utils import (objreg, usertypes, log, standarddir, message,
|
||||
qtutils)
|
||||
from qutebrowser.misc import msgbox, objects
|
||||
|
||||
|
||||
@@ -89,7 +90,7 @@ def _init_envvars():
|
||||
if config.val.qt.force_platform is not None:
|
||||
os.environ['QT_QPA_PLATFORM'] = config.val.qt.force_platform
|
||||
|
||||
if config.val.window.hide_wayland_decoration:
|
||||
if config.val.window.hide_decoration:
|
||||
os.environ['QT_WAYLAND_DISABLE_WINDOWDECORATION'] = '1'
|
||||
|
||||
if config.val.qt.highdpi:
|
||||
@@ -161,4 +162,12 @@ def qt_args(namespace):
|
||||
argv += ['--' + name, value]
|
||||
|
||||
argv += ['--' + arg for arg in config.val.qt.args]
|
||||
|
||||
if (objects.backend == usertypes.Backend.QtWebEngine and
|
||||
not qtutils.version_check('5.11', compiled=False)):
|
||||
# WORKAROUND equivalent to
|
||||
# https://codereview.qt-project.org/#/c/217932/
|
||||
# Needed for Qt < 5.9.5 and < 5.10.1
|
||||
argv.append('--disable-shared-workers')
|
||||
|
||||
return argv
|
||||
|
||||
@@ -451,7 +451,7 @@ class List(BaseType):
|
||||
def from_obj(self, value):
|
||||
if value is None:
|
||||
return []
|
||||
return value
|
||||
return [self.valtype.from_obj(v) for v in value]
|
||||
|
||||
def to_py(self, value):
|
||||
self._basic_py_validation(value, list)
|
||||
@@ -506,6 +506,16 @@ class ListOrValue(BaseType):
|
||||
self.listtype = List(valtype, none_ok=none_ok, *args, **kwargs)
|
||||
self.valtype = valtype
|
||||
|
||||
def _val_and_type(self, value):
|
||||
"""Get the value and type to use for to_str/to_doc/from_str."""
|
||||
if isinstance(value, list):
|
||||
if len(value) == 1:
|
||||
return value[0], self.valtype
|
||||
else:
|
||||
return value, self.listtype
|
||||
else:
|
||||
return value, self.valtype
|
||||
|
||||
def get_name(self):
|
||||
return self.listtype.get_name() + ', or ' + self.valtype.get_name()
|
||||
|
||||
@@ -533,25 +543,15 @@ class ListOrValue(BaseType):
|
||||
if value is None:
|
||||
return ''
|
||||
|
||||
if isinstance(value, list):
|
||||
if len(value) == 1:
|
||||
return self.valtype.to_str(value[0])
|
||||
else:
|
||||
return self.listtype.to_str(value)
|
||||
else:
|
||||
return self.valtype.to_str(value)
|
||||
val, typ = self._val_and_type(value)
|
||||
return typ.to_str(val)
|
||||
|
||||
def to_doc(self, value, indent=0):
|
||||
if value is None:
|
||||
return 'empty'
|
||||
|
||||
if isinstance(value, list):
|
||||
if len(value) == 1:
|
||||
return self.valtype.to_doc(value[0], indent)
|
||||
else:
|
||||
return self.listtype.to_doc(value, indent)
|
||||
else:
|
||||
return self.valtype.to_doc(value, indent)
|
||||
val, typ = self._val_and_type(value)
|
||||
return typ.to_doc(val)
|
||||
|
||||
|
||||
class FlagList(List):
|
||||
@@ -1199,7 +1199,9 @@ class Dict(BaseType):
|
||||
def from_obj(self, value):
|
||||
if value is None:
|
||||
return {}
|
||||
return value
|
||||
|
||||
return {self.keytype.from_obj(key): self.valtype.from_obj(val)
|
||||
for key, val in value.items()}
|
||||
|
||||
def _fill_fixed_keys(self, value):
|
||||
"""Fill missing fixed keys with a None-value."""
|
||||
@@ -1623,9 +1625,7 @@ class TimestampTemplate(BaseType):
|
||||
|
||||
"""An strftime-like template for timestamps.
|
||||
|
||||
See
|
||||
https://docs.python.org/3/library/datetime.html#strftime-strptime-behavior
|
||||
for reference.
|
||||
See https://sqlite.org/lang_datefunc.html for reference.
|
||||
"""
|
||||
|
||||
def to_py(self, value):
|
||||
@@ -1648,6 +1648,10 @@ class Key(BaseType):
|
||||
|
||||
"""A name of a key."""
|
||||
|
||||
def from_obj(self, value):
|
||||
"""Make sure key sequences are always normalized."""
|
||||
return str(keyutils.KeySequence.parse(value))
|
||||
|
||||
def to_py(self, value):
|
||||
self._basic_py_validation(value, str)
|
||||
if not value:
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
from PyQt5.QtGui import QFont
|
||||
|
||||
from qutebrowser.config import config, configutils
|
||||
from qutebrowser.utils import log, usertypes, urlmatch
|
||||
from qutebrowser.utils import log, usertypes, urlmatch, qtutils
|
||||
from qutebrowser.misc import objects
|
||||
|
||||
UNSET = object()
|
||||
@@ -141,6 +141,7 @@ class AbstractSettings:
|
||||
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:
|
||||
|
||||
@@ -2,3 +2,4 @@
|
||||
pac_utils.js
|
||||
# Actually a jinja template so eslint chokes on the {{}} syntax.
|
||||
greasemonkey_wrapper.js
|
||||
global_wrapper.js
|
||||
|
||||
@@ -324,9 +324,8 @@ window._qutebrowser.caret = (function() {
|
||||
const color = axs.color.parseColor(style.backgroundColor);
|
||||
if (color &&
|
||||
(style.opacity < 1 &&
|
||||
(color.alpha *= style.opacity),
|
||||
color.alpha !== 0 &&
|
||||
(el.push(color), color.alpha === 1))) {
|
||||
(color.alpha *= style.opacity), color.alpha !== 0 &&
|
||||
(el.push(color), color.alpha === 1))) {
|
||||
iter = !0;
|
||||
break;
|
||||
}
|
||||
@@ -1270,13 +1269,14 @@ window._qutebrowser.caret = (function() {
|
||||
funcs.setInitialCursor = () => {
|
||||
if (!CaretBrowsing.initiated) {
|
||||
CaretBrowsing.setInitialCursor();
|
||||
return;
|
||||
return CaretBrowsing.selectionEnabled;
|
||||
}
|
||||
|
||||
if (window.getSelection().toString().length === 0) {
|
||||
positionCaret();
|
||||
}
|
||||
CaretBrowsing.toggle();
|
||||
return CaretBrowsing.selectionEnabled;
|
||||
};
|
||||
|
||||
funcs.setPlatform = (platform) => {
|
||||
@@ -1362,6 +1362,7 @@ window._qutebrowser.caret = (function() {
|
||||
|
||||
funcs.toggleSelection = () => {
|
||||
CaretBrowsing.selectionEnabled = !CaretBrowsing.selectionEnabled;
|
||||
return CaretBrowsing.selectionEnabled;
|
||||
};
|
||||
|
||||
return funcs;
|
||||
|
||||
12
qutebrowser/javascript/global_wrapper.js
Normal file
@@ -0,0 +1,12 @@
|
||||
(function() {
|
||||
"use strict";
|
||||
if (!("_qutebrowser" in window)) {
|
||||
window._qutebrowser = {"initialized": {}};
|
||||
}
|
||||
|
||||
if (window._qutebrowser.initialized["{{name}}"]) {
|
||||
return;
|
||||
}
|
||||
{{code}}
|
||||
window._qutebrowser.initialized["{{name}}"] = true;
|
||||
})();
|
||||
@@ -1,5 +1,5 @@
|
||||
(function() {
|
||||
const _qute_script_id = "__gm_" + {{ scriptName | tojson }};
|
||||
const _qute_script_id = "__gm_{{ scriptName }}";
|
||||
|
||||
function GM_log(text) {
|
||||
console.log(text);
|
||||
@@ -7,7 +7,7 @@
|
||||
|
||||
const GM_info = {
|
||||
'script': {{ scriptInfo }},
|
||||
'scriptMetaStr': {{ scriptMeta | tojson }},
|
||||
'scriptMetaStr': "{{ scriptMeta }}",
|
||||
'scriptWillUpdate': false,
|
||||
'version': "0.0.1",
|
||||
// so scripts don't expect exportFunction
|
||||
@@ -100,11 +100,8 @@
|
||||
|
||||
const head = document.getElementsByTagName("head")[0];
|
||||
if (head === undefined) {
|
||||
document.onreadystatechange = function() {
|
||||
if (document.readyState === "interactive") {
|
||||
document.getElementsByTagName("head")[0].appendChild(oStyle);
|
||||
}
|
||||
};
|
||||
// no head yet, stick it whereever
|
||||
document.documentElement.appendChild(oStyle);
|
||||
} else {
|
||||
head.appendChild(oStyle);
|
||||
}
|
||||
|
||||
@@ -74,9 +74,8 @@ window._qutebrowser.webelem = (function() {
|
||||
try {
|
||||
return elem.selectionStart;
|
||||
} catch (err) {
|
||||
if (err instanceof (frame
|
||||
? frame.DOMException
|
||||
: DOMException) &&
|
||||
if ((err instanceof DOMException ||
|
||||
(frame && err instanceof frame.DOMException)) &&
|
||||
err.name === "InvalidStateError") {
|
||||
// nothing to do, caret_position is already null
|
||||
} else {
|
||||
|
||||
@@ -108,11 +108,43 @@ class BaseKeyParser(QObject):
|
||||
assert not isinstance(seq, str), seq
|
||||
match = sequence.matches(seq)
|
||||
if match == QKeySequence.ExactMatch:
|
||||
return (match, cmd)
|
||||
return match, cmd
|
||||
elif match == QKeySequence.PartialMatch:
|
||||
result = QKeySequence.PartialMatch
|
||||
|
||||
return (result, None)
|
||||
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.
|
||||
@@ -146,28 +178,15 @@ class BaseKeyParser(QObject):
|
||||
self.clear_keystring()
|
||||
return QKeySequence.NoMatch
|
||||
|
||||
# First, try a straightforward match
|
||||
match, binding = self._match_key(sequence)
|
||||
|
||||
# If that doesn't match, try a key_mapping
|
||||
if match == QKeySequence.NoMatch:
|
||||
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
|
||||
|
||||
# If that doesn't match either, try treating it as count.
|
||||
if (match == QKeySequence.NoMatch and
|
||||
txt.isdigit() and
|
||||
self._supports_count and
|
||||
not (not self._count and txt == '0')):
|
||||
assert len(txt) == 1, txt
|
||||
if not dry_run:
|
||||
self._count += txt
|
||||
self.keystring_updated.emit(self._count + str(self._sequence))
|
||||
return QKeySequence.ExactMatch
|
||||
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
|
||||
|
||||
@@ -58,7 +58,8 @@ def is_special(key, modifiers):
|
||||
_assert_plain_key(key)
|
||||
_assert_plain_modifier(modifiers)
|
||||
return not (_is_printable(key) and
|
||||
modifiers in [Qt.ShiftModifier, Qt.NoModifier])
|
||||
modifiers in [Qt.ShiftModifier, Qt.NoModifier,
|
||||
Qt.KeypadModifier])
|
||||
|
||||
|
||||
def is_modifier_key(key):
|
||||
@@ -303,7 +304,8 @@ class KeyInfo:
|
||||
key_string = key_string.lower()
|
||||
|
||||
# "special" binding
|
||||
assert is_special(self.key, self.modifiers)
|
||||
assert (is_special(self.key, self.modifiers) or
|
||||
self.modifiers == Qt.KeypadModifier)
|
||||
modifier_string = _modifiers_to_string(modifiers)
|
||||
return '<{}{}>'.format(modifier_string, key_string)
|
||||
|
||||
@@ -505,11 +507,29 @@ class KeySequence:
|
||||
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 = []
|
||||
|
||||
@@ -184,7 +184,8 @@ class MainWindow(QWidget):
|
||||
private = bool(private)
|
||||
self._private = private
|
||||
self.tabbed_browser = tabbedbrowser.TabbedBrowser(win_id=self.win_id,
|
||||
private=private)
|
||||
private=private,
|
||||
parent=self)
|
||||
objreg.register('tabbed-browser', self.tabbed_browser, scope='window',
|
||||
window=self.win_id)
|
||||
self._init_command_dispatcher()
|
||||
@@ -230,6 +231,7 @@ class MainWindow(QWidget):
|
||||
config.instance.changed.connect(self._on_config_changed)
|
||||
|
||||
objreg.get("app").new_window.emit(self)
|
||||
self._set_decoration(config.val.window.hide_decoration)
|
||||
|
||||
def _init_geometry(self, geometry):
|
||||
"""Initialize the window geometry or load it from disk."""
|
||||
@@ -327,7 +329,7 @@ class MainWindow(QWidget):
|
||||
self.tabbed_browser)
|
||||
objreg.register('command-dispatcher', dispatcher, scope='window',
|
||||
window=self.win_id)
|
||||
self.tabbed_browser.destroyed.connect(
|
||||
self.tabbed_browser.widget.destroyed.connect(
|
||||
functools.partial(objreg.delete, 'command-dispatcher',
|
||||
scope='window', window=self.win_id))
|
||||
|
||||
@@ -344,13 +346,15 @@ class MainWindow(QWidget):
|
||||
elif option == 'statusbar.position':
|
||||
self._add_widgets()
|
||||
self._update_overlay_geometries()
|
||||
elif option == 'window.hide_decoration':
|
||||
self._set_decoration(config.val.window.hide_decoration)
|
||||
|
||||
def _add_widgets(self):
|
||||
"""Add or readd all widgets to the VBox."""
|
||||
self._vbox.removeWidget(self.tabbed_browser)
|
||||
self._vbox.removeWidget(self.tabbed_browser.widget)
|
||||
self._vbox.removeWidget(self._downloadview)
|
||||
self._vbox.removeWidget(self.status)
|
||||
widgets = [self.tabbed_browser]
|
||||
widgets = [self.tabbed_browser.widget]
|
||||
|
||||
downloads_position = config.val.downloads.position
|
||||
if downloads_position == 'top':
|
||||
@@ -469,7 +473,7 @@ class MainWindow(QWidget):
|
||||
|
||||
self.tabbed_browser.cur_scroll_perc_changed.connect(
|
||||
status.percentage.set_perc)
|
||||
self.tabbed_browser.tab_index_changed.connect(
|
||||
self.tabbed_browser.widget.tab_index_changed.connect(
|
||||
status.tabindex.on_tab_index_changed)
|
||||
|
||||
self.tabbed_browser.cur_url_changed.connect(status.url.set_url)
|
||||
@@ -479,6 +483,10 @@ class MainWindow(QWidget):
|
||||
self.tabbed_browser.cur_link_hovered.connect(status.url.set_hover_url)
|
||||
self.tabbed_browser.cur_load_status_changed.connect(
|
||||
status.url.on_load_status_changed)
|
||||
|
||||
self.tabbed_browser.cur_caret_selection_toggled.connect(
|
||||
status.on_caret_selection_toggled)
|
||||
|
||||
self.tabbed_browser.cur_fullscreen_requested.connect(
|
||||
self._on_fullscreen_requested)
|
||||
self.tabbed_browser.cur_fullscreen_requested.connect(status.maybe_hide)
|
||||
@@ -489,6 +497,16 @@ class MainWindow(QWidget):
|
||||
completion_obj.on_clear_completion_selection)
|
||||
cmd.hide_completion.connect(completion_obj.hide)
|
||||
|
||||
def _set_decoration(self, hidden):
|
||||
"""Set the visibility of the window decoration via Qt."""
|
||||
window_flags = Qt.Window
|
||||
refresh_window = self.isVisible()
|
||||
if hidden:
|
||||
window_flags |= Qt.CustomizeWindowHint | Qt.NoDropShadowWindowHint
|
||||
self.setWindowFlags(window_flags)
|
||||
if refresh_window:
|
||||
self.show()
|
||||
|
||||
@pyqtSlot(bool)
|
||||
def _on_fullscreen_requested(self, on):
|
||||
if not config.val.content.windowed_fullscreen:
|
||||
@@ -517,7 +535,7 @@ class MainWindow(QWidget):
|
||||
super().resizeEvent(e)
|
||||
self._update_overlay_geometries()
|
||||
self._downloadview.updateGeometry()
|
||||
self.tabbed_browser.tabBar().refresh()
|
||||
self.tabbed_browser.widget.tabBar().refresh()
|
||||
|
||||
def showEvent(self, e):
|
||||
"""Extend showEvent to register us as the last-visible-main-window.
|
||||
@@ -546,7 +564,7 @@ class MainWindow(QWidget):
|
||||
if crashsignal.is_crashing:
|
||||
e.accept()
|
||||
return
|
||||
tab_count = self.tabbed_browser.count()
|
||||
tab_count = self.tabbed_browser.widget.count()
|
||||
download_model = objreg.get('download-model', scope='window',
|
||||
window=self.win_id)
|
||||
download_count = download_model.running_downloads()
|
||||
|
||||
@@ -596,6 +596,8 @@ class FilenamePrompt(_BasePrompt):
|
||||
if config.val.prompt.filebrowser:
|
||||
self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred)
|
||||
|
||||
self._to_complete = ''
|
||||
|
||||
@pyqtSlot(str)
|
||||
def _set_fileview_root(self, path, *, tabbed=False):
|
||||
"""Set the root path for the file display."""
|
||||
@@ -604,6 +606,9 @@ class FilenamePrompt(_BasePrompt):
|
||||
separators += os.altsep
|
||||
|
||||
dirname = os.path.dirname(path)
|
||||
basename = os.path.basename(path)
|
||||
if not tabbed:
|
||||
self._to_complete = ''
|
||||
|
||||
try:
|
||||
if not path:
|
||||
@@ -617,6 +622,7 @@ class FilenamePrompt(_BasePrompt):
|
||||
elif os.path.isdir(dirname) and not tabbed:
|
||||
# Input like /foo/ba -> show /foo contents
|
||||
path = dirname
|
||||
self._to_complete = basename
|
||||
else:
|
||||
return
|
||||
except OSError:
|
||||
@@ -634,7 +640,11 @@ class FilenamePrompt(_BasePrompt):
|
||||
index: The QModelIndex of the selected element.
|
||||
clicked: Whether the element was clicked.
|
||||
"""
|
||||
path = os.path.normpath(self._file_model.filePath(index))
|
||||
if index == QModelIndex():
|
||||
path = os.path.join(self._file_model.rootPath(), self._to_complete)
|
||||
else:
|
||||
path = os.path.normpath(self._file_model.filePath(index))
|
||||
|
||||
if clicked:
|
||||
path += os.sep
|
||||
else:
|
||||
@@ -696,6 +706,7 @@ class FilenamePrompt(_BasePrompt):
|
||||
assert last_index.isValid()
|
||||
|
||||
idx = selmodel.currentIndex()
|
||||
|
||||
if not idx.isValid():
|
||||
# No item selected yet
|
||||
idx = last_index if which == 'prev' else first_index
|
||||
@@ -709,10 +720,24 @@ class FilenamePrompt(_BasePrompt):
|
||||
if not idx.isValid():
|
||||
idx = last_index if which == 'prev' else first_index
|
||||
|
||||
idx = self._do_completion(idx, which)
|
||||
|
||||
selmodel.setCurrentIndex(
|
||||
idx, QItemSelectionModel.ClearAndSelect | QItemSelectionModel.Rows)
|
||||
self._insert_path(idx, clicked=False)
|
||||
|
||||
def _do_completion(self, idx, which):
|
||||
filename = self._file_model.fileName(idx)
|
||||
while not filename.startswith(self._to_complete) and idx.isValid():
|
||||
if which == 'prev':
|
||||
idx = self._file_view.indexAbove(idx)
|
||||
else:
|
||||
assert which == 'next', which
|
||||
idx = self._file_view.indexBelow(idx)
|
||||
filename = self._file_model.fileName(idx)
|
||||
|
||||
return idx
|
||||
|
||||
def _allowed_commands(self):
|
||||
return [('prompt-accept', 'Accept'), ('leave-mode', 'Abort')]
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@ class Backforward(textbase.TextBase):
|
||||
|
||||
def on_tab_cur_url_changed(self, tabs):
|
||||
"""Called on URL changes."""
|
||||
tab = tabs.currentWidget()
|
||||
tab = tabs.widget.currentWidget()
|
||||
if tab is None: # pragma: no cover
|
||||
self.setText('')
|
||||
self.hide()
|
||||
|
||||
@@ -268,7 +268,7 @@ class StatusBar(QWidget):
|
||||
"""Get the currently displayed tab."""
|
||||
window = objreg.get('tabbed-browser', scope='window',
|
||||
window=self._win_id)
|
||||
return window.currentWidget()
|
||||
return window.widget.currentWidget()
|
||||
|
||||
def set_mode_active(self, mode, val):
|
||||
"""Setter for self.{insert,command,caret}_active.
|
||||
@@ -289,17 +289,9 @@ class StatusBar(QWidget):
|
||||
log.statusbar.debug("Setting prompt flag to {}".format(val))
|
||||
self._color_flags.prompt = val
|
||||
elif mode == usertypes.KeyMode.caret:
|
||||
tab = self._current_tab()
|
||||
log.statusbar.debug("Setting caret flag - val {}, selection "
|
||||
"{}".format(val, tab.caret.selection_enabled))
|
||||
if val:
|
||||
if tab.caret.selection_enabled:
|
||||
self._set_mode_text("{} selection".format(mode.name))
|
||||
self._color_flags.caret = ColorFlags.CaretMode.selection
|
||||
else:
|
||||
self._set_mode_text(mode.name)
|
||||
self._color_flags.caret = ColorFlags.CaretMode.on
|
||||
else:
|
||||
if not val:
|
||||
# Turning on is handled in on_current_caret_selection_toggled
|
||||
log.statusbar.debug("Setting caret mode off")
|
||||
self._color_flags.caret = ColorFlags.CaretMode.off
|
||||
config.set_register_stylesheet(self, update=False)
|
||||
|
||||
@@ -377,6 +369,18 @@ class StatusBar(QWidget):
|
||||
self.maybe_hide()
|
||||
assert tab.private == self._color_flags.private
|
||||
|
||||
@pyqtSlot(bool)
|
||||
def on_caret_selection_toggled(self, selection):
|
||||
"""Update the statusbar when entering/leaving caret selection mode."""
|
||||
log.statusbar.debug("Setting caret selection {}".format(selection))
|
||||
if selection:
|
||||
self._set_mode_text("caret selection")
|
||||
self._color_flags.caret = ColorFlags.CaretMode.selection
|
||||
else:
|
||||
self._set_mode_text("caret")
|
||||
self._color_flags.caret = ColorFlags.CaretMode.on
|
||||
config.set_register_stylesheet(self, update=False)
|
||||
|
||||
def resizeEvent(self, e):
|
||||
"""Extend resizeEvent of QWidget to emit a resized signal afterwards.
|
||||
|
||||
|
||||
@@ -19,6 +19,8 @@
|
||||
|
||||
"""The commandline in the statusbar."""
|
||||
|
||||
import functools
|
||||
|
||||
from PyQt5.QtCore import pyqtSignal, pyqtSlot, Qt, QSize
|
||||
from PyQt5.QtWidgets import QSizePolicy
|
||||
|
||||
@@ -69,6 +71,26 @@ class Command(misc.MinimalLineEditMixin, misc.CommandLineEdit):
|
||||
self.textChanged.connect(self.updateGeometry)
|
||||
self.textChanged.connect(self._incremental_search)
|
||||
|
||||
self._command_dispatcher = objreg.get(
|
||||
'command-dispatcher', scope='window', window=self._win_id)
|
||||
|
||||
def _handle_search(self):
|
||||
"""Check if the currently entered text is a search, and if so, run it.
|
||||
|
||||
Return:
|
||||
True if a search was executed, False otherwise.
|
||||
"""
|
||||
search_prefixes = {
|
||||
'/': self._command_dispatcher.search,
|
||||
'?': functools.partial(
|
||||
self._command_dispatcher.search, reverse=True)
|
||||
}
|
||||
if self.prefix() in search_prefixes:
|
||||
search_fn = search_prefixes[self.prefix()]
|
||||
search_fn(self.text()[1:])
|
||||
return True
|
||||
return False
|
||||
|
||||
def prefix(self):
|
||||
"""Get the currently entered command prefix."""
|
||||
text = self.text()
|
||||
@@ -162,17 +184,17 @@ class Command(misc.MinimalLineEditMixin, misc.CommandLineEdit):
|
||||
Args:
|
||||
rapid: Run the command without closing or clearing the command bar.
|
||||
"""
|
||||
prefixes = {
|
||||
':': '',
|
||||
'/': 'search -- ',
|
||||
'?': 'search -r -- ',
|
||||
}
|
||||
text = self.text()
|
||||
self.history.append(text)
|
||||
|
||||
was_search = self._handle_search()
|
||||
|
||||
if not rapid:
|
||||
modeman.leave(self._win_id, usertypes.KeyMode.command,
|
||||
'cmd accept')
|
||||
self.got_cmd[str].emit(prefixes[text[0]] + text[1:])
|
||||
|
||||
if not was_search:
|
||||
self.got_cmd[str].emit(text[1:])
|
||||
|
||||
@cmdutils.register(instance='status-command', scope='window')
|
||||
def edit_command(self, run=False):
|
||||
@@ -253,15 +275,9 @@ class Command(misc.MinimalLineEditMixin, misc.CommandLineEdit):
|
||||
width = self.fontMetrics().width(text)
|
||||
return QSize(width, height)
|
||||
|
||||
@pyqtSlot(str)
|
||||
def _incremental_search(self, text):
|
||||
@pyqtSlot()
|
||||
def _incremental_search(self):
|
||||
if not config.val.search.incremental:
|
||||
return
|
||||
|
||||
search_prefixes = {
|
||||
'/': 'search -- ',
|
||||
'?': 'search -r -- ',
|
||||
}
|
||||
|
||||
if self.prefix() in ['/', '?']:
|
||||
self.got_cmd[str].emit(search_prefixes[text[0]] + text[1:])
|
||||
self._handle_search()
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
import functools
|
||||
|
||||
import attr
|
||||
from PyQt5.QtWidgets import QSizePolicy
|
||||
from PyQt5.QtWidgets import QSizePolicy, QWidget
|
||||
from PyQt5.QtCore import pyqtSignal, pyqtSlot, QTimer, QUrl
|
||||
from PyQt5.QtGui import QIcon
|
||||
|
||||
@@ -50,7 +50,7 @@ class TabDeletedError(Exception):
|
||||
"""Exception raised when _tab_index is called for a deleted tab."""
|
||||
|
||||
|
||||
class TabbedBrowser(tabwidget.TabWidget):
|
||||
class TabbedBrowser(QWidget):
|
||||
|
||||
"""A TabWidget with QWebViews inside.
|
||||
|
||||
@@ -104,23 +104,25 @@ class TabbedBrowser(tabwidget.TabWidget):
|
||||
cur_scroll_perc_changed = pyqtSignal(int, int)
|
||||
cur_load_status_changed = pyqtSignal(str)
|
||||
cur_fullscreen_requested = pyqtSignal(bool)
|
||||
cur_caret_selection_toggled = pyqtSignal(bool)
|
||||
close_window = pyqtSignal()
|
||||
resized = pyqtSignal('QRect')
|
||||
current_tab_changed = pyqtSignal(browsertab.AbstractTab)
|
||||
new_tab = pyqtSignal(browsertab.AbstractTab, int)
|
||||
|
||||
def __init__(self, *, win_id, private, parent=None):
|
||||
super().__init__(win_id, parent)
|
||||
super().__init__(parent)
|
||||
self.widget = tabwidget.TabWidget(win_id, parent=self)
|
||||
self._win_id = win_id
|
||||
self._tab_insert_idx_left = 0
|
||||
self._tab_insert_idx_right = -1
|
||||
self.shutting_down = False
|
||||
self.tabCloseRequested.connect(self.on_tab_close_requested)
|
||||
self.new_tab_requested.connect(self.tabopen)
|
||||
self.currentChanged.connect(self.on_current_changed)
|
||||
self.widget.tabCloseRequested.connect(self.on_tab_close_requested)
|
||||
self.widget.new_tab_requested.connect(self.tabopen)
|
||||
self.widget.currentChanged.connect(self.on_current_changed)
|
||||
self.cur_load_started.connect(self.on_cur_load_started)
|
||||
self.cur_fullscreen_requested.connect(self.tabBar().maybe_hide)
|
||||
self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
|
||||
self.cur_fullscreen_requested.connect(self.widget.tabBar().maybe_hide)
|
||||
self.widget.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
|
||||
self._undo_stack = []
|
||||
self._filter = signalfilter.SignalFilter(win_id, self)
|
||||
self._now_focused = None
|
||||
@@ -128,12 +130,12 @@ class TabbedBrowser(tabwidget.TabWidget):
|
||||
self.search_options = {}
|
||||
self._local_marks = {}
|
||||
self._global_marks = {}
|
||||
self.default_window_icon = self.window().windowIcon()
|
||||
self.default_window_icon = self.widget.window().windowIcon()
|
||||
self.private = private
|
||||
config.instance.changed.connect(self._on_config_changed)
|
||||
|
||||
def __repr__(self):
|
||||
return utils.get_repr(self, count=self.count())
|
||||
return utils.get_repr(self, count=self.widget.count())
|
||||
|
||||
@pyqtSlot(str)
|
||||
def _on_config_changed(self, option):
|
||||
@@ -142,7 +144,7 @@ class TabbedBrowser(tabwidget.TabWidget):
|
||||
elif option == 'window.title_format':
|
||||
self._update_window_title()
|
||||
elif option in ['tabs.title.format', 'tabs.title.format_pinned']:
|
||||
self._update_tab_titles()
|
||||
self.widget.update_tab_titles()
|
||||
|
||||
def _tab_index(self, tab):
|
||||
"""Get the index of a given tab.
|
||||
@@ -150,7 +152,7 @@ class TabbedBrowser(tabwidget.TabWidget):
|
||||
Raises TabDeletedError if the tab doesn't exist anymore.
|
||||
"""
|
||||
try:
|
||||
idx = self.indexOf(tab)
|
||||
idx = self.widget.indexOf(tab)
|
||||
except RuntimeError as e:
|
||||
log.webview.debug("Got invalid tab ({})!".format(e))
|
||||
raise TabDeletedError(e)
|
||||
@@ -166,8 +168,8 @@ class TabbedBrowser(tabwidget.TabWidget):
|
||||
iterating over the list.
|
||||
"""
|
||||
widgets = []
|
||||
for i in range(self.count()):
|
||||
widget = self.widget(i)
|
||||
for i in range(self.widget.count()):
|
||||
widget = self.widget.widget(i)
|
||||
if widget is None:
|
||||
log.webview.debug("Got None-widget in tabbedbrowser!")
|
||||
else:
|
||||
@@ -186,16 +188,16 @@ class TabbedBrowser(tabwidget.TabWidget):
|
||||
if field is not None and ('{' + field + '}') not in title_format:
|
||||
return
|
||||
|
||||
idx = self.currentIndex()
|
||||
idx = self.widget.currentIndex()
|
||||
if idx == -1:
|
||||
# (e.g. last tab removed)
|
||||
log.webview.debug("Not updating window title because index is -1")
|
||||
return
|
||||
fields = self.get_tab_fields(idx)
|
||||
fields = self.widget.get_tab_fields(idx)
|
||||
fields['id'] = self._win_id
|
||||
|
||||
title = title_format.format(**fields)
|
||||
self.window().setWindowTitle(title)
|
||||
self.widget.window().setWindowTitle(title)
|
||||
|
||||
def _connect_tab_signals(self, tab):
|
||||
"""Set up the needed signals for tab."""
|
||||
@@ -216,6 +218,8 @@ class TabbedBrowser(tabwidget.TabWidget):
|
||||
self._filter.create(self.cur_load_status_changed, tab))
|
||||
tab.fullscreen_requested.connect(
|
||||
self._filter.create(self.cur_fullscreen_requested, tab))
|
||||
tab.caret.selection_toggled.connect(
|
||||
self._filter.create(self.cur_caret_selection_toggled, tab))
|
||||
# misc
|
||||
tab.scroller.perc_changed.connect(self.on_scroll_pos_changed)
|
||||
tab.url_changed.connect(
|
||||
@@ -247,8 +251,8 @@ class TabbedBrowser(tabwidget.TabWidget):
|
||||
Return:
|
||||
The current URL as QUrl.
|
||||
"""
|
||||
idx = self.currentIndex()
|
||||
return super().tab_url(idx)
|
||||
idx = self.widget.currentIndex()
|
||||
return self.widget.tab_url(idx)
|
||||
|
||||
def shutdown(self):
|
||||
"""Try to shut down all tabs cleanly."""
|
||||
@@ -284,7 +288,7 @@ class TabbedBrowser(tabwidget.TabWidget):
|
||||
new_undo: Whether the undo entry should be a new item in the stack.
|
||||
"""
|
||||
last_close = config.val.tabs.last_close
|
||||
count = self.count()
|
||||
count = self.widget.count()
|
||||
|
||||
if last_close == 'ignore' and count == 1:
|
||||
return
|
||||
@@ -311,7 +315,7 @@ class TabbedBrowser(tabwidget.TabWidget):
|
||||
new_undo: Whether the undo entry should be a new item in the stack.
|
||||
crashed: Whether we're closing a tab with crashed renderer process.
|
||||
"""
|
||||
idx = self.indexOf(tab)
|
||||
idx = self.widget.indexOf(tab)
|
||||
if idx == -1:
|
||||
if crashed:
|
||||
return
|
||||
@@ -349,7 +353,7 @@ class TabbedBrowser(tabwidget.TabWidget):
|
||||
self._undo_stack[-1].append(entry)
|
||||
|
||||
tab.shutdown()
|
||||
self.removeTab(idx)
|
||||
self.widget.removeTab(idx)
|
||||
if not crashed:
|
||||
# WORKAROUND for a segfault when we delete the crashed tab.
|
||||
# see https://bugreports.qt.io/browse/QTBUG-58698
|
||||
@@ -362,14 +366,14 @@ class TabbedBrowser(tabwidget.TabWidget):
|
||||
last_close = config.val.tabs.last_close
|
||||
use_current_tab = False
|
||||
if last_close in ['blank', 'startpage', 'default-page']:
|
||||
only_one_tab_open = self.count() == 1
|
||||
no_history = len(self.widget(0).history) == 1
|
||||
only_one_tab_open = self.widget.count() == 1
|
||||
no_history = len(self.widget.widget(0).history) == 1
|
||||
urls = {
|
||||
'blank': QUrl('about:blank'),
|
||||
'startpage': config.val.url.start_pages[0],
|
||||
'default-page': config.val.url.default_page,
|
||||
}
|
||||
first_tab_url = self.widget(0).url()
|
||||
first_tab_url = self.widget.widget(0).url()
|
||||
last_close_urlstr = urls[last_close].toString().rstrip('/')
|
||||
first_tab_urlstr = first_tab_url.toString().rstrip('/')
|
||||
last_close_url_used = first_tab_urlstr == last_close_urlstr
|
||||
@@ -378,13 +382,13 @@ class TabbedBrowser(tabwidget.TabWidget):
|
||||
|
||||
for entry in reversed(self._undo_stack.pop()):
|
||||
if use_current_tab:
|
||||
newtab = self.widget(0)
|
||||
newtab = self.widget.widget(0)
|
||||
use_current_tab = False
|
||||
else:
|
||||
newtab = self.tabopen(background=False, idx=entry.index)
|
||||
|
||||
newtab.history.deserialize(entry.history)
|
||||
self.set_tab_pinned(newtab, entry.pinned)
|
||||
self.widget.set_tab_pinned(newtab, entry.pinned)
|
||||
|
||||
@pyqtSlot('QUrl', bool)
|
||||
def openurl(self, url, newtab):
|
||||
@@ -395,15 +399,15 @@ class TabbedBrowser(tabwidget.TabWidget):
|
||||
newtab: True to open URL in a new tab, False otherwise.
|
||||
"""
|
||||
qtutils.ensure_valid(url)
|
||||
if newtab or self.currentWidget() is None:
|
||||
if newtab or self.widget.currentWidget() is None:
|
||||
self.tabopen(url, background=False)
|
||||
else:
|
||||
self.currentWidget().openurl(url)
|
||||
self.widget.currentWidget().openurl(url)
|
||||
|
||||
@pyqtSlot(int)
|
||||
def on_tab_close_requested(self, idx):
|
||||
"""Close a tab via an index."""
|
||||
tab = self.widget(idx)
|
||||
tab = self.widget.widget(idx)
|
||||
if tab is None:
|
||||
log.webview.debug("Got invalid tab {} for index {}!".format(
|
||||
tab, idx))
|
||||
@@ -454,7 +458,7 @@ class TabbedBrowser(tabwidget.TabWidget):
|
||||
"related {}, idx {}".format(
|
||||
url, background, related, idx))
|
||||
|
||||
if (config.val.tabs.tabs_are_windows and self.count() > 0 and
|
||||
if (config.val.tabs.tabs_are_windows and self.widget.count() > 0 and
|
||||
not ignore_tabs_are_windows):
|
||||
window = mainwindow.MainWindow(private=self.private)
|
||||
window.show()
|
||||
@@ -464,12 +468,12 @@ class TabbedBrowser(tabwidget.TabWidget):
|
||||
related=related)
|
||||
|
||||
tab = browsertab.create(win_id=self._win_id, private=self.private,
|
||||
parent=self)
|
||||
parent=self.widget)
|
||||
self._connect_tab_signals(tab)
|
||||
|
||||
if idx is None:
|
||||
idx = self._get_new_tab_idx(related)
|
||||
self.insertTab(idx, tab, "")
|
||||
self.widget.insertTab(idx, tab, "")
|
||||
|
||||
if url is not None:
|
||||
tab.openurl(url)
|
||||
@@ -480,10 +484,13 @@ class TabbedBrowser(tabwidget.TabWidget):
|
||||
# Make sure the background tab has the correct initial size.
|
||||
# With a foreground tab, it's going to be resized correctly by the
|
||||
# layout anyways.
|
||||
tab.resize(self.currentWidget().size())
|
||||
self.tab_index_changed.emit(self.currentIndex(), self.count())
|
||||
tab.resize(self.widget.currentWidget().size())
|
||||
self.widget.tab_index_changed.emit(self.widget.currentIndex(),
|
||||
self.widget.count())
|
||||
else:
|
||||
self.setCurrentWidget(tab)
|
||||
self.widget.setCurrentWidget(tab)
|
||||
# WORKAROUND for https://bugreports.qt.io/browse/QTBUG-68076
|
||||
tab.setFocus()
|
||||
|
||||
tab.show()
|
||||
self.new_tab.emit(tab, idx)
|
||||
@@ -526,15 +533,8 @@ class TabbedBrowser(tabwidget.TabWidget):
|
||||
|
||||
def _update_favicons(self):
|
||||
"""Update favicons when config was changed."""
|
||||
for i, tab in enumerate(self.widgets()):
|
||||
if config.val.tabs.favicons.show:
|
||||
self.setTabIcon(i, tab.icon())
|
||||
if config.val.tabs.tabs_are_windows:
|
||||
self.window().setWindowIcon(tab.icon())
|
||||
else:
|
||||
self.setTabIcon(i, QIcon())
|
||||
if config.val.tabs.tabs_are_windows:
|
||||
self.window().setWindowIcon(self.default_window_icon)
|
||||
for tab in self.widgets():
|
||||
self.widget.update_tab_favicon(tab)
|
||||
|
||||
@pyqtSlot()
|
||||
def on_load_started(self, tab):
|
||||
@@ -548,14 +548,14 @@ class TabbedBrowser(tabwidget.TabWidget):
|
||||
except TabDeletedError:
|
||||
# We can get signals for tabs we already deleted...
|
||||
return
|
||||
self._update_tab_title(idx)
|
||||
self.widget.update_tab_title(idx)
|
||||
if tab.data.keep_icon:
|
||||
tab.data.keep_icon = False
|
||||
else:
|
||||
if (config.val.tabs.tabs_are_windows and
|
||||
config.val.tabs.favicons.show):
|
||||
self.window().setWindowIcon(self.default_window_icon)
|
||||
if idx == self.currentIndex():
|
||||
tab.data.should_show_icon()):
|
||||
self.widget.window().setWindowIcon(self.default_window_icon)
|
||||
if idx == self.widget.currentIndex():
|
||||
self._update_window_title()
|
||||
|
||||
@pyqtSlot()
|
||||
@@ -586,8 +586,8 @@ class TabbedBrowser(tabwidget.TabWidget):
|
||||
return
|
||||
log.webview.debug("Changing title for idx {} to '{}'".format(
|
||||
idx, text))
|
||||
self.set_page_title(idx, text)
|
||||
if idx == self.currentIndex():
|
||||
self.widget.set_page_title(idx, text)
|
||||
if idx == self.widget.currentIndex():
|
||||
self._update_window_title()
|
||||
|
||||
@pyqtSlot(browsertab.AbstractTab, QUrl)
|
||||
@@ -604,8 +604,8 @@ class TabbedBrowser(tabwidget.TabWidget):
|
||||
# We can get signals for tabs we already deleted...
|
||||
return
|
||||
|
||||
if not self.page_title(idx):
|
||||
self.set_page_title(idx, url.toDisplayString())
|
||||
if not self.widget.page_title(idx):
|
||||
self.widget.set_page_title(idx, url.toDisplayString())
|
||||
|
||||
@pyqtSlot(browsertab.AbstractTab, QIcon)
|
||||
def on_icon_changed(self, tab, icon):
|
||||
@@ -617,23 +617,23 @@ class TabbedBrowser(tabwidget.TabWidget):
|
||||
tab: The WebView where the title was changed.
|
||||
icon: The new icon
|
||||
"""
|
||||
if not config.val.tabs.favicons.show:
|
||||
if not tab.data.should_show_icon():
|
||||
return
|
||||
try:
|
||||
idx = self._tab_index(tab)
|
||||
except TabDeletedError:
|
||||
# We can get signals for tabs we already deleted...
|
||||
return
|
||||
self.setTabIcon(idx, icon)
|
||||
self.widget.setTabIcon(idx, icon)
|
||||
if config.val.tabs.tabs_are_windows:
|
||||
self.window().setWindowIcon(icon)
|
||||
self.widget.window().setWindowIcon(icon)
|
||||
|
||||
@pyqtSlot(usertypes.KeyMode)
|
||||
def on_mode_left(self, mode):
|
||||
"""Give focus to current tab if command mode was left."""
|
||||
if mode in [usertypes.KeyMode.command, usertypes.KeyMode.prompt,
|
||||
usertypes.KeyMode.yesno]:
|
||||
widget = self.currentWidget()
|
||||
widget = self.widget.currentWidget()
|
||||
log.modes.debug("Left status-input mode, focusing {!r}".format(
|
||||
widget))
|
||||
if widget is None:
|
||||
@@ -649,7 +649,7 @@ class TabbedBrowser(tabwidget.TabWidget):
|
||||
if idx == -1 or self.shutting_down:
|
||||
# closing the last tab (before quitting) or shutting down
|
||||
return
|
||||
tab = self.widget(idx)
|
||||
tab = self.widget.widget(idx)
|
||||
if tab is None:
|
||||
log.webview.debug("on_current_changed got called with invalid "
|
||||
"index {}".format(idx))
|
||||
@@ -677,8 +677,8 @@ class TabbedBrowser(tabwidget.TabWidget):
|
||||
self._now_focused = tab
|
||||
self.current_tab_changed.emit(tab)
|
||||
QTimer.singleShot(0, self._update_window_title)
|
||||
self._tab_insert_idx_left = self.currentIndex()
|
||||
self._tab_insert_idx_right = self.currentIndex() + 1
|
||||
self._tab_insert_idx_left = self.widget.currentIndex()
|
||||
self._tab_insert_idx_right = self.widget.currentIndex() + 1
|
||||
|
||||
@pyqtSlot()
|
||||
def on_cmd_return_pressed(self):
|
||||
@@ -696,9 +696,9 @@ class TabbedBrowser(tabwidget.TabWidget):
|
||||
stop = config.val.colors.tabs.indicator.stop
|
||||
system = config.val.colors.tabs.indicator.system
|
||||
color = utils.interpolate_color(start, stop, perc, system)
|
||||
self.set_tab_indicator_color(idx, color)
|
||||
self._update_tab_title(idx)
|
||||
if idx == self.currentIndex():
|
||||
self.widget.set_tab_indicator_color(idx, color)
|
||||
self.widget.update_tab_title(idx)
|
||||
if idx == self.widget.currentIndex():
|
||||
self._update_window_title()
|
||||
|
||||
def on_load_finished(self, tab, ok):
|
||||
@@ -715,23 +715,23 @@ class TabbedBrowser(tabwidget.TabWidget):
|
||||
color = utils.interpolate_color(start, stop, 100, system)
|
||||
else:
|
||||
color = config.val.colors.tabs.indicator.error
|
||||
self.set_tab_indicator_color(idx, color)
|
||||
self._update_tab_title(idx)
|
||||
if idx == self.currentIndex():
|
||||
self.widget.set_tab_indicator_color(idx, color)
|
||||
self.widget.update_tab_title(idx)
|
||||
if idx == self.widget.currentIndex():
|
||||
self._update_window_title()
|
||||
tab.handle_auto_insert_mode(ok)
|
||||
|
||||
@pyqtSlot()
|
||||
def on_scroll_pos_changed(self):
|
||||
"""Update tab and window title when scroll position changed."""
|
||||
idx = self.currentIndex()
|
||||
idx = self.widget.currentIndex()
|
||||
if idx == -1:
|
||||
# (e.g. last tab removed)
|
||||
log.webview.debug("Not updating scroll position because index is "
|
||||
"-1")
|
||||
return
|
||||
self._update_window_title('scroll_pos')
|
||||
self._update_tab_title(idx, 'scroll_pos')
|
||||
self.widget.update_tab_title(idx, 'scroll_pos')
|
||||
|
||||
def _on_renderer_process_terminated(self, tab, status, code):
|
||||
"""Show an error when a renderer process terminated."""
|
||||
@@ -764,7 +764,7 @@ class TabbedBrowser(tabwidget.TabWidget):
|
||||
# WORKAROUND for https://bugreports.qt.io/browse/QTBUG-58698
|
||||
message.error(msg)
|
||||
self._remove_tab(tab, crashed=True)
|
||||
if self.count() == 0:
|
||||
if self.widget.count() == 0:
|
||||
self.tabopen(QUrl('about:blank'))
|
||||
|
||||
def resizeEvent(self, e):
|
||||
@@ -801,7 +801,7 @@ class TabbedBrowser(tabwidget.TabWidget):
|
||||
if key != "'":
|
||||
message.error("Failed to set mark: url invalid")
|
||||
return
|
||||
point = self.currentWidget().scroller.pos_px()
|
||||
point = self.widget.currentWidget().scroller.pos_px()
|
||||
|
||||
if key.isupper():
|
||||
self._global_marks[key] = point, url
|
||||
@@ -822,7 +822,7 @@ class TabbedBrowser(tabwidget.TabWidget):
|
||||
except qtutils.QtValueError:
|
||||
urlkey = None
|
||||
|
||||
tab = self.currentWidget()
|
||||
tab = self.widget.currentWidget()
|
||||
|
||||
if key.isupper():
|
||||
if key in self._global_marks:
|
||||
|
||||
@@ -60,7 +60,7 @@ class TabWidget(QTabWidget):
|
||||
self.setTabBar(bar)
|
||||
bar.tabCloseRequested.connect(self.tabCloseRequested)
|
||||
bar.tabMoved.connect(functools.partial(
|
||||
QTimer.singleShot, 0, self._update_tab_titles))
|
||||
QTimer.singleShot, 0, self.update_tab_titles))
|
||||
bar.currentChanged.connect(self._on_current_changed)
|
||||
bar.new_tab_requested.connect(self._on_new_tab_requested)
|
||||
self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
|
||||
@@ -108,7 +108,8 @@ class TabWidget(QTabWidget):
|
||||
|
||||
bar.set_tab_data(idx, 'pinned', pinned)
|
||||
tab.data.pinned = pinned
|
||||
self._update_tab_title(idx)
|
||||
self.update_tab_favicon(tab)
|
||||
self.update_tab_title(idx)
|
||||
|
||||
def tab_indicator_color(self, idx):
|
||||
"""Get the tab indicator color for the given index."""
|
||||
@@ -117,13 +118,13 @@ class TabWidget(QTabWidget):
|
||||
def set_page_title(self, idx, title):
|
||||
"""Set the tab title user data."""
|
||||
self.tabBar().set_tab_data(idx, 'page-title', title)
|
||||
self._update_tab_title(idx)
|
||||
self.update_tab_title(idx)
|
||||
|
||||
def page_title(self, idx):
|
||||
"""Get the tab title user data."""
|
||||
return self.tabBar().page_title(idx)
|
||||
|
||||
def _update_tab_title(self, idx, field=None):
|
||||
def update_tab_title(self, idx, field=None):
|
||||
"""Update the tab text for the given tab.
|
||||
|
||||
Args:
|
||||
@@ -148,9 +149,13 @@ class TabWidget(QTabWidget):
|
||||
title = '' if fmt is None else fmt.format(**fields)
|
||||
tabbar = self.tabBar()
|
||||
|
||||
# Only change the tab title if it changes, setting the tab title causes
|
||||
# a size recalculation which is slow.
|
||||
if tabbar.tabText(idx) != title:
|
||||
tabbar.setTabText(idx, title)
|
||||
tabbar.setTabToolTip(idx, title)
|
||||
|
||||
# always show only plain title in tooltips
|
||||
tabbar.setTabToolTip(idx, fields['title'])
|
||||
|
||||
def get_tab_fields(self, idx):
|
||||
"""Get the tab field data."""
|
||||
@@ -197,20 +202,20 @@ class TabWidget(QTabWidget):
|
||||
fields['scroll_pos'] = scroll_pos
|
||||
return fields
|
||||
|
||||
def _update_tab_titles(self):
|
||||
def update_tab_titles(self):
|
||||
"""Update all texts."""
|
||||
for idx in range(self.count()):
|
||||
self._update_tab_title(idx)
|
||||
self.update_tab_title(idx)
|
||||
|
||||
def tabInserted(self, idx):
|
||||
"""Update titles when a tab was inserted."""
|
||||
super().tabInserted(idx)
|
||||
self._update_tab_titles()
|
||||
self.update_tab_titles()
|
||||
|
||||
def tabRemoved(self, idx):
|
||||
"""Update titles when a tab was removed."""
|
||||
super().tabRemoved(idx)
|
||||
self._update_tab_titles()
|
||||
self.update_tab_titles()
|
||||
|
||||
def addTab(self, page, icon_or_text, text_or_empty=None):
|
||||
"""Override addTab to use our own text setting logic.
|
||||
@@ -296,6 +301,19 @@ class TabWidget(QTabWidget):
|
||||
qtutils.ensure_valid(url)
|
||||
return url
|
||||
|
||||
def update_tab_favicon(self, tab: QWidget):
|
||||
"""Update favicon of the given tab."""
|
||||
idx = self.indexOf(tab)
|
||||
|
||||
if tab.data.should_show_icon():
|
||||
self.setTabIcon(idx, tab.icon())
|
||||
if config.val.tabs.tabs_are_windows:
|
||||
self.window().setWindowIcon(tab.icon())
|
||||
else:
|
||||
self.setTabIcon(idx, QIcon())
|
||||
if config.val.tabs.tabs_are_windows:
|
||||
self.window().setWindowIcon(self.window().windowIcon())
|
||||
|
||||
|
||||
class TabBar(QTabBar):
|
||||
|
||||
@@ -358,7 +376,9 @@ class TabBar(QTabBar):
|
||||
# Clear _minimum_tab_size_hint_helper cache when appropriate
|
||||
if option in ["tabs.indicator.padding",
|
||||
"tabs.padding",
|
||||
"tabs.indicator.width"]:
|
||||
"tabs.indicator.width",
|
||||
"tabs.min_width",
|
||||
"tabs.pinned.shrink"]:
|
||||
self._minimum_tab_size_hint_helper.cache_clear()
|
||||
|
||||
def _on_show_switching_delay_changed(self):
|
||||
@@ -477,7 +497,8 @@ class TabBar(QTabBar):
|
||||
Args:
|
||||
index: The index of the tab to get a size hint for.
|
||||
ellipsis: Whether to use ellipsis to calculate width
|
||||
instead of the tab's text.
|
||||
instead of the tab's text.
|
||||
Forced to False for pinned tabs.
|
||||
Return:
|
||||
A QSize of the smallest tab size we can make.
|
||||
"""
|
||||
@@ -489,14 +510,19 @@ class TabBar(QTabBar):
|
||||
else:
|
||||
icon_width = min(icon.actualSize(self.iconSize()).width(),
|
||||
self.iconSize().width()) + icon_padding
|
||||
|
||||
pinned = self._tab_pinned(index)
|
||||
if not self.vertical and pinned and config.val.tabs.pinned.shrink:
|
||||
# Never consider ellipsis an option for horizontal pinned tabs
|
||||
ellipsis = False
|
||||
return self._minimum_tab_size_hint_helper(self.tabText(index),
|
||||
icon_width,
|
||||
ellipsis)
|
||||
icon_width, ellipsis,
|
||||
pinned)
|
||||
|
||||
@functools.lru_cache(maxsize=2**9)
|
||||
def _minimum_tab_size_hint_helper(self, tab_text: str,
|
||||
icon_width: int,
|
||||
ellipsis: bool) -> QSize:
|
||||
ellipsis: bool, pinned: bool) -> QSize:
|
||||
"""Helper function to cache tab results.
|
||||
|
||||
Config values accessed in here should be added to _on_config_changed to
|
||||
@@ -521,6 +547,10 @@ class TabBar(QTabBar):
|
||||
height = self.fontMetrics().height() + padding_v
|
||||
width = (text_width + icon_width +
|
||||
padding_h + indicator_width)
|
||||
min_width = config.val.tabs.min_width
|
||||
if (not self.vertical and min_width > 0 and
|
||||
not pinned or not config.val.tabs.pinned.shrink):
|
||||
width = max(min_width, width)
|
||||
return QSize(width, height)
|
||||
|
||||
def _pinned_statistics(self) -> (int, int):
|
||||
@@ -550,6 +580,12 @@ class TabBar(QTabBar):
|
||||
Return:
|
||||
A QSize.
|
||||
"""
|
||||
if self.count() == 0:
|
||||
# This happens on startup on macOS.
|
||||
# We return it directly rather than setting `size' because we don't
|
||||
# want to ensure it's valid in this special case.
|
||||
return QSize()
|
||||
|
||||
minimum_size = self.minimumTabSizeHint(index)
|
||||
height = minimum_size.height()
|
||||
if self.vertical:
|
||||
@@ -562,11 +598,6 @@ class TabBar(QTabBar):
|
||||
else:
|
||||
width = int(confwidth)
|
||||
size = QSize(max(minimum_size.width(), width), height)
|
||||
elif self.count() == 0:
|
||||
# This happens on startup on macOS.
|
||||
# We return it directly rather than setting `size' because we don't
|
||||
# want to ensure it's valid in this special case.
|
||||
return QSize()
|
||||
else:
|
||||
if config.val.tabs.pinned.shrink:
|
||||
pinned = self._tab_pinned(index)
|
||||
@@ -889,7 +920,7 @@ class TabBarStyle(QCommonStyle):
|
||||
# reserve space for favicon when tab bar is vertical (issue #1968)
|
||||
position = config.val.tabs.position
|
||||
if (position in [QTabWidget.East, QTabWidget.West] and
|
||||
config.val.tabs.favicons.show):
|
||||
config.val.tabs.favicons.show != 'never'):
|
||||
tab_icon_size = icon_size
|
||||
else:
|
||||
actual_size = opt.icon.actualSize(icon_size, icon_mode, icon_state)
|
||||
|
||||
@@ -45,7 +45,7 @@ class PyPIVersionClient(QObject):
|
||||
arg: The error message, as string.
|
||||
"""
|
||||
|
||||
API_URL = 'https://pypi.python.org/pypi/{}/json'
|
||||
API_URL = 'https://pypi.org/pypi/{}/json'
|
||||
success = pyqtSignal(str)
|
||||
error = pyqtSignal(str)
|
||||
|
||||
|
||||
@@ -166,8 +166,9 @@ def _nvidia_shader_workaround():
|
||||
See https://bugs.launchpad.net/ubuntu/+source/python-qt4/+bug/941826
|
||||
"""
|
||||
assert objects.backend == usertypes.Backend.QtWebEngine, objects.backend
|
||||
if utils.is_linux:
|
||||
ctypes.CDLL(ctypes.util.find_library("GL"), mode=ctypes.RTLD_GLOBAL)
|
||||
libgl = ctypes.util.find_library("GL")
|
||||
if libgl is not None:
|
||||
ctypes.CDLL(libgl, mode=ctypes.RTLD_GLOBAL)
|
||||
|
||||
|
||||
def _handle_nouveau_graphics():
|
||||
|
||||
@@ -42,6 +42,7 @@ class ExternalEditor(QObject):
|
||||
_proc: The GUIProcess of the editor.
|
||||
_watcher: A QFileSystemWatcher to watch the edited file for changes.
|
||||
Only set if watch=True.
|
||||
_content: The last-saved text of the editor.
|
||||
|
||||
Signals:
|
||||
file_updated: The text in the edited file was updated.
|
||||
@@ -112,19 +113,7 @@ class ExternalEditor(QObject):
|
||||
if self._filename is not None:
|
||||
raise ValueError("Already editing a file!")
|
||||
try:
|
||||
# Close while the external process is running, as otherwise systems
|
||||
# with exclusive write access (e.g. Windows) may fail to update
|
||||
# the file from the external editor, see
|
||||
# https://github.com/qutebrowser/qutebrowser/issues/1767
|
||||
with tempfile.NamedTemporaryFile(
|
||||
# pylint: disable=bad-continuation
|
||||
mode='w', prefix='qutebrowser-editor-',
|
||||
encoding=config.val.editor.encoding,
|
||||
delete=False) as fobj:
|
||||
# pylint: enable=bad-continuation
|
||||
if text:
|
||||
fobj.write(text)
|
||||
self._filename = fobj.name
|
||||
self._filename = self._create_tempfile(text, 'qutebrowser-editor-')
|
||||
except OSError as e:
|
||||
message.error("Failed to create initial file: {}".format(e))
|
||||
return
|
||||
@@ -134,6 +123,32 @@ class ExternalEditor(QObject):
|
||||
line, column = self._calc_line_and_column(text, caret_position)
|
||||
self._start_editor(line=line, column=column)
|
||||
|
||||
def backup(self):
|
||||
"""Create a backup if the content has changed from the original."""
|
||||
if not self._content:
|
||||
return
|
||||
try:
|
||||
fname = self._create_tempfile(self._content,
|
||||
'qutebrowser-editor-backup-')
|
||||
message.info('Editor backup at {}'.format(fname))
|
||||
except OSError as e:
|
||||
message.error('Failed to create editor backup: {}'.format(e))
|
||||
|
||||
def _create_tempfile(self, text, prefix):
|
||||
# Close while the external process is running, as otherwise systems
|
||||
# with exclusive write access (e.g. Windows) may fail to update
|
||||
# the file from the external editor, see
|
||||
# https://github.com/qutebrowser/qutebrowser/issues/1767
|
||||
with tempfile.NamedTemporaryFile(
|
||||
# pylint: disable=bad-continuation
|
||||
mode='w', prefix=prefix,
|
||||
encoding=config.val.editor.encoding,
|
||||
delete=False) as fobj:
|
||||
# pylint: enable=bad-continuation
|
||||
if text:
|
||||
fobj.write(text)
|
||||
return fobj.name
|
||||
|
||||
@pyqtSlot(str)
|
||||
def _on_file_changed(self, path):
|
||||
try:
|
||||
|
||||
@@ -28,6 +28,21 @@ from PyQt5.QtNetwork import (QNetworkAccessManager, QNetworkRequest,
|
||||
QNetworkReply)
|
||||
|
||||
|
||||
class HTTPRequest(QNetworkRequest):
|
||||
"""A QNetworkRquest that follows (secure) redirects by default."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
try:
|
||||
self.setAttribute(QNetworkRequest.RedirectPolicyAttribute,
|
||||
QNetworkRequest.NoLessSafeRedirectPolicy)
|
||||
except AttributeError:
|
||||
# RedirectPolicyAttribute was introduced in 5.9 to replace
|
||||
# FollowRedirectsAttribute.
|
||||
self.setAttribute(QNetworkRequest.FollowRedirectsAttribute,
|
||||
True)
|
||||
|
||||
|
||||
class HTTPClient(QObject):
|
||||
|
||||
"""An HTTP client based on QNetworkAccessManager.
|
||||
@@ -63,7 +78,7 @@ class HTTPClient(QObject):
|
||||
if data is None:
|
||||
data = {}
|
||||
encoded_data = urllib.parse.urlencode(data).encode('utf-8')
|
||||
request = QNetworkRequest(url)
|
||||
request = HTTPRequest(url)
|
||||
request.setHeader(QNetworkRequest.ContentTypeHeader,
|
||||
'application/x-www-form-urlencoded;charset=utf-8')
|
||||
reply = self._nam.post(request, encoded_data)
|
||||
@@ -77,7 +92,7 @@ class HTTPClient(QObject):
|
||||
Args:
|
||||
url: The URL to access, as QUrl.
|
||||
"""
|
||||
request = QNetworkRequest(url)
|
||||
request = HTTPRequest(url)
|
||||
reply = self._nam.get(request)
|
||||
self._handle_reply(reply)
|
||||
|
||||
|
||||
@@ -130,7 +130,7 @@ class KeyHintView(QLabel):
|
||||
).format(
|
||||
html.escape(prefix),
|
||||
suffix_color,
|
||||
html.escape(str(seq[len(prefix):])),
|
||||
html.escape(str(seq)[len(prefix):]),
|
||||
html.escape(cmd)
|
||||
)
|
||||
text = '<table>{}</table>'.format(text)
|
||||
|
||||
@@ -246,7 +246,7 @@ class SessionManager(QObject):
|
||||
if tabbed_browser.private:
|
||||
win_data['private'] = True
|
||||
for i, tab in enumerate(tabbed_browser.widgets()):
|
||||
active = i == tabbed_browser.currentIndex()
|
||||
active = i == tabbed_browser.widget.currentIndex()
|
||||
win_data['tabs'].append(self._save_tab(tab, active))
|
||||
data['windows'].append(win_data)
|
||||
return data
|
||||
@@ -427,11 +427,12 @@ class SessionManager(QObject):
|
||||
if tab.get('active', False):
|
||||
tab_to_focus = i
|
||||
if new_tab.data.pinned:
|
||||
tabbed_browser.set_tab_pinned(new_tab, new_tab.data.pinned)
|
||||
tabbed_browser.widget.set_tab_pinned(new_tab,
|
||||
new_tab.data.pinned)
|
||||
if tab_to_focus is not None:
|
||||
tabbed_browser.setCurrentIndex(tab_to_focus)
|
||||
tabbed_browser.widget.setCurrentIndex(tab_to_focus)
|
||||
if win.get('active', False):
|
||||
QTimer.singleShot(0, tabbed_browser.activateWindow)
|
||||
QTimer.singleShot(0, tabbed_browser.widget.activateWindow)
|
||||
|
||||
if data['windows']:
|
||||
self.did_load = True
|
||||
|
||||
@@ -185,7 +185,7 @@ def debug_cache_stats():
|
||||
tabbed_browser = objreg.get('tabbed-browser', scope='window',
|
||||
window='last-focused')
|
||||
# pylint: disable=protected-access
|
||||
tab_bar = tabbed_browser.tabBar()
|
||||
tab_bar = tabbed_browser.widget.tabBar()
|
||||
tabbed_browser_info = tab_bar._minimum_tab_size_hint_helper.cache_info()
|
||||
# pylint: enable=protected-access
|
||||
|
||||
|
||||
@@ -87,10 +87,11 @@ def log_signals(obj):
|
||||
return ret
|
||||
|
||||
obj.__init__ = new_init
|
||||
return obj
|
||||
else:
|
||||
connect_log_slot(obj)
|
||||
|
||||
return obj
|
||||
|
||||
|
||||
def qenum_key(base, value, add_base=False, klass=None):
|
||||
"""Convert a Qt Enum value to its key as a string.
|
||||
|
||||
@@ -20,6 +20,9 @@
|
||||
"""Utilities related to javascript interaction."""
|
||||
|
||||
|
||||
from qutebrowser.utils import jinja
|
||||
|
||||
|
||||
def string_escape(text):
|
||||
"""Escape values special to javascript in strings.
|
||||
|
||||
@@ -70,3 +73,9 @@ def assemble(module, function, *args):
|
||||
parts = ['window', '_qutebrowser', module, function]
|
||||
code = '"use strict";\n{}({});'.format('.'.join(parts), js_args)
|
||||
return code
|
||||
|
||||
|
||||
def wrap_global(name, *sources):
|
||||
"""Wrap a script using window._qutebrowser."""
|
||||
template = jinja.js_environment.get_template('global_wrapper.js')
|
||||
return template.render(code='\n'.join(sources), name=name)
|
||||
|
||||
@@ -171,7 +171,7 @@ def _get_tab_registry(win_id, tab_id):
|
||||
|
||||
if tab_id == 'current':
|
||||
tabbed_browser = get('tabbed-browser', scope='window', window=win_id)
|
||||
tab = tabbed_browser.currentWidget()
|
||||
tab = tabbed_browser.widget.currentWidget()
|
||||
if tab is None:
|
||||
raise RegistryUnavailableError('window')
|
||||
tab_id = tab.tab_id
|
||||
|
||||
@@ -102,6 +102,12 @@ def _get_search_url(txt):
|
||||
engine = 'DEFAULT'
|
||||
template = config.val.url.searchengines[engine]
|
||||
url = qurl_from_user_input(template.format(urllib.parse.quote(term)))
|
||||
|
||||
if config.val.url.open_base_url and term in config.val.url.searchengines:
|
||||
url = qurl_from_user_input(config.val.url.searchengines[term])
|
||||
url.setPath(None)
|
||||
url.setFragment(None)
|
||||
url.setQuery(None)
|
||||
qtutils.ensure_valid(url)
|
||||
return url
|
||||
|
||||
|
||||
@@ -269,6 +269,8 @@ def _os_info():
|
||||
else:
|
||||
versioninfo = '.'.join(versioninfo)
|
||||
osver = ', '.join([e for e in [release, versioninfo, machine] if e])
|
||||
elif utils.is_posix:
|
||||
osver = ' '.join(platform.uname())
|
||||
else:
|
||||
osver = '?'
|
||||
lines.append('OS Version: {}'.format(osver))
|
||||
@@ -315,8 +317,10 @@ def _chromium_version():
|
||||
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 (?)
|
||||
Qt 5.11: Chromium 65
|
||||
Qt 5.12: Chromium 69 (?)
|
||||
|
||||
Also see https://www.chromium.org/developers/calendar
|
||||
"""
|
||||
if QWebEngineProfile is None:
|
||||
# This should never happen
|
||||
@@ -453,7 +457,13 @@ def opengl_vendor(): # pragma: no cover
|
||||
vp = QOpenGLVersionProfile()
|
||||
vp.setVersion(2, 0)
|
||||
|
||||
vf = ctx.versionFunctions(vp)
|
||||
try:
|
||||
vf = ctx.versionFunctions(vp)
|
||||
except ImportError as e:
|
||||
log.init.debug("opengl_vendor: Importing version functions "
|
||||
"failed: {}".format(e))
|
||||
return None
|
||||
|
||||
if vf is None:
|
||||
log.init.debug("opengl_vendor: Getting version functions failed!")
|
||||
return None
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
import os
|
||||
import os.path
|
||||
import sys
|
||||
import time
|
||||
import glob
|
||||
import shutil
|
||||
import plistlib
|
||||
@@ -195,6 +196,7 @@ def build_mac():
|
||||
'MacOS', 'qutebrowser')
|
||||
smoke_test(binary)
|
||||
finally:
|
||||
time.sleep(5)
|
||||
subprocess.run(['hdiutil', 'detach', tmpdir])
|
||||
except PermissionError as e:
|
||||
print("Failed to remove tempdir: {}".format(e))
|
||||
@@ -359,7 +361,7 @@ def github_upload(artifacts, tag):
|
||||
repo = gh.repository('qutebrowser', 'qutebrowser')
|
||||
|
||||
release = None # to satisfy pylint
|
||||
for release in repo.iter_releases():
|
||||
for release in repo.releases():
|
||||
if release.tag_name == tag:
|
||||
break
|
||||
else:
|
||||
@@ -399,14 +401,6 @@ def main():
|
||||
|
||||
run_asciidoc2html(args)
|
||||
if os.name == 'nt':
|
||||
if sys.maxsize > 2**32:
|
||||
# WORKAROUND
|
||||
print("Due to a python/Windows bug, this script needs to be run ")
|
||||
print("with a 32bit Python.")
|
||||
print()
|
||||
print("See http://bugs.python.org/issue24493 and ")
|
||||
print("https://github.com/pypa/virtualenv/issues/774")
|
||||
sys.exit(1)
|
||||
artifacts = build_windows()
|
||||
elif sys.platform == 'darwin':
|
||||
artifacts = build_mac()
|
||||
|
||||
@@ -246,6 +246,8 @@ def apply_fake_os(monkeypatch, request):
|
||||
elif name == 'linux':
|
||||
linux = True
|
||||
posix = True
|
||||
elif name == 'posix':
|
||||
posix = True
|
||||
else:
|
||||
raise ValueError("Invalid fake_os {}".format(name))
|
||||
|
||||
|
||||
13
tests/end2end/data/hints/issue3711.html
Normal file
@@ -0,0 +1,13 @@
|
||||
<html>
|
||||
<!-- https://github.com/qutebrowser/qutebrowser/issues/3711 -->
|
||||
<head>
|
||||
<title>Issue 3711</title>
|
||||
</head>
|
||||
<body>
|
||||
<!--
|
||||
Verify no hint error occurs when hinting input range elements in iframes on qt5.9
|
||||
Possibly an issue in chrome.
|
||||
-->
|
||||
<input min="0" max="1" step="0.001" type="range">
|
||||
</body>
|
||||
</html>
|
||||
11
tests/end2end/data/hints/issue3711_frame.html
Normal file
@@ -0,0 +1,11 @@
|
||||
<!DOCTYPE html>
|
||||
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Issue 3771 Parent Frame</title>
|
||||
</head>
|
||||
<body>
|
||||
<iframe src="./issue3711.html"></iframe>
|
||||
</body>
|
||||
</html>
|
||||
@@ -3,10 +3,10 @@
|
||||
|
||||
<head>
|
||||
<script type="text/javascript">
|
||||
var my_window;
|
||||
let my_window;
|
||||
|
||||
function open_modal() {
|
||||
window.open('about:blank', 'window', 'modal');
|
||||
my_window = window.open('about:blank', 'window', 'modal');
|
||||
}
|
||||
|
||||
function open_normal() {
|
||||
@@ -17,13 +17,15 @@
|
||||
window.open('', 'my_window');
|
||||
}
|
||||
|
||||
function close() {
|
||||
function close_normal() {
|
||||
my_window.close();
|
||||
console.log("window closed");
|
||||
}
|
||||
|
||||
function close_twice() {
|
||||
my_window.close();
|
||||
my_window.close();
|
||||
console.log("window closed");
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
@@ -33,7 +35,7 @@
|
||||
<button onclick="open_normal()" id="open-normal">normal</button>
|
||||
<button onclick="open_modal()" id="open-modal">modal</button>
|
||||
<button onclick="open_invalid()" id="open-invalid">invalid/no URL</button>
|
||||
<button onclick="close()" id="close-normal">close</button>
|
||||
<button onclick="close_normal()" id="close-normal">close</button>
|
||||
<button onclick="close_twice()" id="close-twice">close twice (issue 906)</button>
|
||||
|
||||
</body>
|
||||
|
||||
@@ -16,6 +16,8 @@
|
||||
BAZ<br/>
|
||||
space travel<br/>
|
||||
/slash<br/>
|
||||
-r reversed<br/>
|
||||
;; semicolons<br/>
|
||||
<a class="toselect" href="hello.txt">follow me!</a><br/>
|
||||
</p>
|
||||
</body>
|
||||
|
||||
@@ -128,6 +128,7 @@ Feature: Opening external editors
|
||||
And I run :tab-close
|
||||
And I kill the waiting editor
|
||||
Then the error "Edited element vanished" should be shown
|
||||
And the message "Editor backup at *" should be shown
|
||||
|
||||
# Could not get signals working on Windows
|
||||
@posix
|
||||
|
||||
@@ -249,6 +249,11 @@ Feature: Using hints
|
||||
And I hint with args "all current" and follow a
|
||||
Then no crash should happen
|
||||
|
||||
Scenario: No error when hinting ranged input in frames
|
||||
When I open data/hints/issue3711_frame.html
|
||||
And I hint with args "all current" and follow a
|
||||
Then no crash should happen
|
||||
|
||||
### hints.auto_follow.timeout
|
||||
|
||||
@not_mac @flaky
|
||||
|
||||
@@ -8,6 +8,7 @@ Feature: Javascript stuff
|
||||
When I open data/javascript/consolelog.html
|
||||
Then the javascript message "console.log works!" should be logged
|
||||
|
||||
@flaky
|
||||
Scenario: Opening/Closing a window via JS
|
||||
When I open data/javascript/window_open.html
|
||||
And I run :tab-only
|
||||
@@ -15,7 +16,10 @@ Feature: Javascript stuff
|
||||
And I wait for "Changing title for idx 1 to 'about:blank'" in the log
|
||||
And I run :tab-focus 1
|
||||
And I run :click-element id close-normal
|
||||
And I wait for "[*] window closed" in the log
|
||||
Then "Focus object changed: *" should be logged
|
||||
And the following tabs should be open:
|
||||
- data/javascript/window_open.html (active)
|
||||
|
||||
@qtwebkit_skip
|
||||
Scenario: Opening/closing a modal window via JS
|
||||
@@ -25,8 +29,11 @@ Feature: Javascript stuff
|
||||
And I wait for "Changing title for idx 1 to 'about:blank'" in the log
|
||||
And I run :tab-focus 1
|
||||
And I run :click-element id close-normal
|
||||
And I wait for "[*] window closed" in the log
|
||||
Then "Focus object changed: *" should be logged
|
||||
And "Web*Dialog requested, but we don't support that!" should be logged
|
||||
And the following tabs should be open:
|
||||
- data/javascript/window_open.html (active)
|
||||
|
||||
# https://github.com/qutebrowser/qutebrowser/issues/906
|
||||
|
||||
@@ -39,6 +46,7 @@ Feature: Javascript stuff
|
||||
And I wait for "Changing title for idx 2 to 'about:blank'" in the log
|
||||
And I run :tab-focus 2
|
||||
And I run :click-element id close-twice
|
||||
And I wait for "[*] window closed" in the log
|
||||
Then "Requested to close * which does not exist!" should be logged
|
||||
|
||||
@qtwebkit_skip @flaky
|
||||
@@ -51,6 +59,7 @@ Feature: Javascript stuff
|
||||
And I run :buffer window_open.html
|
||||
And I run :click-element id close-twice
|
||||
And I wait for "Focus object changed: *" in the log
|
||||
And I wait for "[*] window closed" in the log
|
||||
Then no crash should happen
|
||||
|
||||
@flaky
|
||||
@@ -130,6 +139,7 @@ Feature: Javascript stuff
|
||||
And I run :tab-next
|
||||
Then the window sizes should be the same
|
||||
|
||||
@flaky
|
||||
Scenario: Have a GreaseMonkey script run at page start
|
||||
When I have a GreaseMonkey file saved for document-start with noframes unset
|
||||
And I run :greasemonkey-reload
|
||||
@@ -173,3 +183,15 @@ Feature: Javascript stuff
|
||||
When I set content.javascript.enabled to false
|
||||
And I open 500 without waiting
|
||||
Then "Showing error page for* 500" should be logged
|
||||
|
||||
Scenario: Using JS after window.open
|
||||
When I open data/hello.txt
|
||||
And I set content.javascript.can_open_tabs_automatically to true
|
||||
And I run :jseval window.open('about:blank')
|
||||
And I open data/hello.txt
|
||||
And I run :tab-only
|
||||
And I open data/hints/html/simple.html
|
||||
And I run :hint all
|
||||
And I wait for "hints: a" in the log
|
||||
And I run :leave-mode
|
||||
Then "There was an error while getting hint elements" should not be logged
|
||||
|
||||
@@ -26,7 +26,7 @@ Feature: Using :navigate
|
||||
# prev/next
|
||||
|
||||
Scenario: Navigating to previous page
|
||||
When I open data/navigate
|
||||
When I open data/navigate in a new tab
|
||||
And I run :navigate prev
|
||||
Then data/navigate/prev.html should be loaded
|
||||
|
||||
|
||||
@@ -40,11 +40,26 @@ Feature: Searching on a page
|
||||
Then "space " should be found
|
||||
|
||||
Scenario: Searching with / and slash in search term (issue 507)
|
||||
When I run :set-cmd-text -s //slash
|
||||
When I run :set-cmd-text //slash
|
||||
And I run :command-accept
|
||||
And I wait for "search found /slash" in the log
|
||||
Then "/slash" should be found
|
||||
|
||||
Scenario: Searching with arguments at start of search term
|
||||
When I run :set-cmd-text /-r reversed
|
||||
And I run :command-accept
|
||||
And I wait for "search found -r reversed" in the log
|
||||
Then "-r reversed" should be found
|
||||
|
||||
Scenario: Searching with semicolons in search term
|
||||
When I run :set-cmd-text /;
|
||||
And I run :fake-key -g ;
|
||||
And I run :fake-key -g <space>
|
||||
And I run :fake-key -g semi
|
||||
And I run :command-accept
|
||||
And I wait for "search found ;; semi" in the log
|
||||
Then ";; semi" should be found
|
||||
|
||||
# This doesn't work because this is QtWebKit behavior.
|
||||
@xfail_norun
|
||||
Scenario: Searching text with umlauts
|
||||
|
||||
@@ -336,13 +336,13 @@ Feature: Tab management
|
||||
When I set tabs.wrap to false
|
||||
And I open data/numbers/1.txt
|
||||
And I run :tab-prev
|
||||
Then the error "First tab" should be shown
|
||||
Then "First tab" should be logged
|
||||
|
||||
Scenario: :tab-next with last tab without wrap
|
||||
When I set tabs.wrap to false
|
||||
And I open data/numbers/1.txt
|
||||
And I run :tab-next
|
||||
Then the error "Last tab" should be shown
|
||||
Then "Last tab" should be logged
|
||||
|
||||
Scenario: :tab-prev on first tab with wrap
|
||||
When I set tabs.wrap to true
|
||||
|
||||
@@ -101,6 +101,9 @@ def is_ignored_lowlevel_message(message):
|
||||
' Error: No such file or directory',
|
||||
# Qt 5.7.1
|
||||
'qt.network.ssl: QSslSocket: cannot call unresolved function *',
|
||||
# Qt 5.11
|
||||
# DevTools listening on ws://127.0.0.1:37945/devtools/browser/...
|
||||
'DevTools listening on *',
|
||||
]
|
||||
return any(testutils.pattern_match(pattern=pattern, value=message)
|
||||
for pattern in ignored_messages)
|
||||
@@ -169,7 +172,7 @@ def is_ignored_chromium_message(line):
|
||||
# /tmp/pytest-of-florian/pytest-32/test_webengine_download_suffix0/
|
||||
# downloads/download.bin: Operation not supported
|
||||
('Could not set extended attribute user.xdg.* on file *: '
|
||||
'Operation not supported'),
|
||||
'Operation not supported*'),
|
||||
# [5947:5947:0605/192837.856931:ERROR:render_process_impl.cc(112)]
|
||||
# WebFrame LEAKED 1 TIMES
|
||||
'WebFrame LEAKED 1 TIMES',
|
||||
@@ -192,6 +195,15 @@ def is_ignored_chromium_message(line):
|
||||
# [2734:2746:1107/131154.072032:ERROR:nss_ocsp.cc(591)] No
|
||||
# URLRequestContext for NSS HTTP handler. host: ocsp.digicert.com
|
||||
'No URLRequestContext for NSS HTTP handler. host: *',
|
||||
|
||||
# https://bugreports.qt.io/browse/QTBUG-66661
|
||||
# [23359:23359:0319/115812.168578:WARNING:
|
||||
# render_frame_host_impl.cc(2744)] OnDidStopLoading was called twice.
|
||||
'OnDidStopLoading was called twice.',
|
||||
|
||||
# [30412:30412:0323/074933.387250:ERROR:node_channel.cc(899)] Dropping
|
||||
# message on closed channel.
|
||||
'Dropping message on closed channel.',
|
||||
]
|
||||
return any(testutils.pattern_match(pattern=pattern, value=message)
|
||||
for pattern in ignored_messages)
|
||||
@@ -345,6 +357,10 @@ class QuteProc(testprocess.Process):
|
||||
# when calling QApplication::sync
|
||||
"Focus object changed: "
|
||||
"<PyQt5.QtWidgets.QWidget object at *>",
|
||||
# Qt >= 5.11
|
||||
"Focus object changed: "
|
||||
"<qutebrowser.browser.webengine.webview.WebEngineView object "
|
||||
"at *>",
|
||||
]
|
||||
|
||||
if (log_line.category == 'ipc' and
|
||||
|
||||
@@ -379,6 +379,7 @@ def test_qute_settings_persistence(short_tmpdir, request, quteproc_new):
|
||||
|
||||
@pytest.mark.no_xvfb
|
||||
@pytest.mark.no_ci
|
||||
@pytest.mark.not_mac
|
||||
def test_force_software_rendering(request, quteproc_new):
|
||||
"""Make sure we can force software rendering with -s."""
|
||||
if not request.config.webengine:
|
||||
|
||||
@@ -43,7 +43,8 @@ import helpers.stubs as stubsmod
|
||||
import helpers.utils
|
||||
from qutebrowser.config import (config, configdata, configtypes, configexc,
|
||||
configfiles)
|
||||
from qutebrowser.utils import objreg, standarddir
|
||||
from qutebrowser.utils import objreg, standarddir, utils
|
||||
from qutebrowser.browser import greasemonkey
|
||||
from qutebrowser.browser.webkit import cookies
|
||||
from qutebrowser.misc import savemanager, sql
|
||||
from qutebrowser.keyinput import modeman
|
||||
@@ -143,6 +144,47 @@ def fake_web_tab(stubs, tab_registry, mode_manager, qapp):
|
||||
return stubs.FakeWebTab
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def greasemonkey_manager(data_tmpdir):
|
||||
gm_manager = greasemonkey.GreasemonkeyManager()
|
||||
objreg.register('greasemonkey', gm_manager)
|
||||
yield
|
||||
objreg.delete('greasemonkey')
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def webkit_tab(qtbot, tab_registry, cookiejar_and_cache, mode_manager,
|
||||
session_manager_stub, greasemonkey_manager):
|
||||
webkittab = pytest.importorskip('qutebrowser.browser.webkit.webkittab')
|
||||
tab = webkittab.WebKitTab(win_id=0, mode_manager=mode_manager,
|
||||
private=False)
|
||||
qtbot.add_widget(tab)
|
||||
return tab
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def webengine_tab(qtbot, tab_registry, fake_args, mode_manager,
|
||||
session_manager_stub, greasemonkey_manager,
|
||||
redirect_webengine_data):
|
||||
webenginetab = pytest.importorskip(
|
||||
'qutebrowser.browser.webengine.webenginetab')
|
||||
tab = webenginetab.WebEngineTab(win_id=0, mode_manager=mode_manager,
|
||||
private=False)
|
||||
qtbot.add_widget(tab)
|
||||
return tab
|
||||
|
||||
|
||||
@pytest.fixture(params=['webkit', 'webengine'])
|
||||
def web_tab(request):
|
||||
"""A WebKitTab/WebEngineTab."""
|
||||
if request.param == 'webkit':
|
||||
return request.getfixturevalue('webkit_tab')
|
||||
elif request.param == 'webengine':
|
||||
return request.getfixturevalue('webengine_tab')
|
||||
else:
|
||||
raise utils.Unreachable
|
||||
|
||||
|
||||
def _generate_cmdline_tests():
|
||||
"""Generate testcases for test_split_binding."""
|
||||
@attr.s
|
||||
@@ -193,11 +235,15 @@ def configdata_init():
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def config_stub(stubs, monkeypatch, configdata_init, config_tmpdir):
|
||||
"""Fixture which provides a fake config object."""
|
||||
yaml_config = configfiles.YamlConfig()
|
||||
def yaml_config_stub(config_tmpdir):
|
||||
"""Fixture which provides a YamlConfig object."""
|
||||
return configfiles.YamlConfig()
|
||||
|
||||
conf = config.Config(yaml_config=yaml_config)
|
||||
|
||||
@pytest.fixture
|
||||
def config_stub(stubs, monkeypatch, configdata_init, yaml_config_stub):
|
||||
"""Fixture which provides a fake config object."""
|
||||
conf = config.Config(yaml_config=yaml_config_stub)
|
||||
monkeypatch.setattr(config, 'instance', conf)
|
||||
|
||||
container = config.ConfigContainer(conf)
|
||||
|
||||
@@ -27,6 +27,7 @@ import shutil
|
||||
|
||||
import attr
|
||||
from PyQt5.QtCore import pyqtSignal, QPoint, QProcess, QObject, QUrl
|
||||
from PyQt5.QtGui import QIcon
|
||||
from PyQt5.QtNetwork import (QNetworkRequest, QAbstractNetworkCache,
|
||||
QNetworkCacheMetaData)
|
||||
from PyQt5.QtWidgets import QCommonStyle, QLineEdit, QWidget, QTabBar
|
||||
@@ -266,6 +267,9 @@ class FakeWebTab(browsertab.AbstractTab):
|
||||
def shutdown(self):
|
||||
pass
|
||||
|
||||
def icon(self):
|
||||
return QIcon()
|
||||
|
||||
|
||||
class FakeSignal:
|
||||
|
||||
@@ -472,37 +476,55 @@ class SessionManagerStub:
|
||||
def list_sessions(self):
|
||||
return self.sessions
|
||||
|
||||
def save_autosave(self):
|
||||
pass
|
||||
|
||||
|
||||
class TabbedBrowserStub(QObject):
|
||||
|
||||
"""Stub for the tabbed-browser object."""
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self.widget = TabWidgetStub()
|
||||
self.shutting_down = False
|
||||
self.opened_url = None
|
||||
|
||||
def on_tab_close_requested(self, idx):
|
||||
del self.widget.tabs[idx]
|
||||
|
||||
def widgets(self):
|
||||
return self.widget.tabs
|
||||
|
||||
def tabopen(self, url):
|
||||
self.opened_url = url
|
||||
|
||||
def openurl(self, url, *, newtab):
|
||||
self.opened_url = url
|
||||
|
||||
|
||||
class TabWidgetStub(QObject):
|
||||
|
||||
"""Stub for the tab-widget object."""
|
||||
|
||||
new_tab = pyqtSignal(browsertab.AbstractTab, int)
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self.tabs = []
|
||||
self.shutting_down = False
|
||||
self._qtabbar = QTabBar()
|
||||
self.index_of = None
|
||||
self.current_index = None
|
||||
self.opened_url = None
|
||||
|
||||
def count(self):
|
||||
return len(self.tabs)
|
||||
|
||||
def widgets(self):
|
||||
return self.tabs
|
||||
|
||||
def widget(self, i):
|
||||
return self.tabs[i]
|
||||
|
||||
def page_title(self, i):
|
||||
return self.tabs[i].title()
|
||||
|
||||
def on_tab_close_requested(self, idx):
|
||||
del self.tabs[idx]
|
||||
|
||||
def tabBar(self):
|
||||
return self._qtabbar
|
||||
|
||||
@@ -526,12 +548,6 @@ class TabbedBrowserStub(QObject):
|
||||
return None
|
||||
return self.tabs[idx - 1]
|
||||
|
||||
def tabopen(self, url):
|
||||
self.opened_url = url
|
||||
|
||||
def openurl(self, url, *, newtab):
|
||||
self.opened_url = url
|
||||
|
||||
|
||||
class ApplicationStub(QObject):
|
||||
|
||||
|
||||
@@ -120,8 +120,10 @@ def assert_urls(host_blocker, blocked=BLOCKLIST_HOSTS,
|
||||
|
||||
Ensure URLs in 'blocked' and not in 'whitelisted' are blocked.
|
||||
All other URLs must not be blocked.
|
||||
|
||||
localhost is an example of a special case that shouldn't be blocked.
|
||||
"""
|
||||
whitelisted = list(whitelisted) + list(host_blocker.WHITELISTED)
|
||||
whitelisted = list(whitelisted) + ['localhost']
|
||||
for str_url in urls_to_check:
|
||||
url = QUrl(str_url)
|
||||
host = url.host()
|
||||
@@ -247,6 +249,16 @@ def test_successful_update(config_stub, basedir, download_stub,
|
||||
assert_urls(host_blocker, whitelisted=[])
|
||||
|
||||
|
||||
def test_parsing_multiple_hosts_on_line(config_stub, basedir, download_stub,
|
||||
data_tmpdir, tmpdir, win_registry,
|
||||
caplog):
|
||||
"""Ensure multiple hosts on a line get parsed correctly."""
|
||||
host_blocker = adblock.HostBlocker()
|
||||
bytes_host_line = ' '.join(BLOCKLIST_HOSTS).encode('utf-8')
|
||||
host_blocker._parse_line(bytes_host_line)
|
||||
assert_urls(host_blocker, whitelisted=[])
|
||||
|
||||
|
||||
def test_failed_dl_update(config_stub, basedir, download_stub,
|
||||
data_tmpdir, tmpdir, win_registry, caplog):
|
||||
"""One blocklist fails to download.
|
||||
@@ -341,7 +353,7 @@ def test_blocking_with_whitelist(config_stub, basedir, download_stub,
|
||||
"""Ensure hosts in content.host_blocking.whitelist are never blocked."""
|
||||
# Simulate adblock_update has already been run
|
||||
# by creating a file named blocked-hosts,
|
||||
# Exclude localhost from it, since localhost is in HostBlocker.WHITELISTED
|
||||
# Exclude localhost from it as localhost is never blocked via list
|
||||
filtered_blocked_hosts = BLOCKLIST_HOSTS[1:]
|
||||
blocklist = create_blocklist(data_tmpdir,
|
||||
blocked_hosts=filtered_blocked_hosts,
|
||||
|
||||