Compare commits
508 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b12c984846 | ||
|
|
3801960c61 | ||
|
|
07e0ae5584 | ||
|
|
3ccb691e9f | ||
|
|
878fa26247 | ||
|
|
214641301c | ||
|
|
7f9af096cd | ||
|
|
99ea20d0d1 | ||
|
|
9bd48c4277 | ||
|
|
3ea6d4c527 | ||
|
|
0f07198271 | ||
|
|
49977a32c4 | ||
|
|
70d6f90f08 | ||
|
|
d70f3a0417 | ||
|
|
d5cf8ef894 | ||
|
|
36b0054238 | ||
|
|
f4f6a3dac1 | ||
|
|
e4d896401d | ||
|
|
da64db853e | ||
|
|
dab17b801e | ||
|
|
ded733c674 | ||
|
|
028e7239ed | ||
|
|
2751e57958 | ||
|
|
c618983b3d | ||
|
|
b8ea3d3c39 | ||
|
|
3efb41f743 | ||
|
|
9f45a27a2a | ||
|
|
1564563662 | ||
|
|
c3f53312af | ||
|
|
e887d9a381 | ||
|
|
3ffb726b98 | ||
|
|
f52c7f13d3 | ||
|
|
9eeda62adf | ||
|
|
1781d0fba3 | ||
|
|
9758b52d91 | ||
|
|
02731743c0 | ||
|
|
04fdce9058 | ||
|
|
cba25d2bbb | ||
|
|
3d4a01ef4c | ||
|
|
19077c8b47 | ||
|
|
91b754d6ea | ||
|
|
1f320b8686 | ||
|
|
02a06732ff | ||
|
|
78fd614237 | ||
|
|
11bf5c8809 | ||
|
|
d1cc542835 | ||
|
|
40a3e24b05 | ||
|
|
64f208486e | ||
|
|
36bb5cf285 | ||
|
|
8bbfcc01be | ||
|
|
12e7b4f244 | ||
|
|
85be6565fc | ||
|
|
f040fd5a6d | ||
|
|
ef01566621 | ||
|
|
76eab7617b | ||
|
|
e9660ad676 | ||
|
|
ff682606ab | ||
|
|
19949101c6 | ||
|
|
83005bc072 | ||
|
|
63e466f019 | ||
|
|
ac2553794c | ||
|
|
aabee4828e | ||
|
|
a1c4e6e2cf | ||
|
|
5f2dc53d94 | ||
|
|
c4fb43df58 | ||
|
|
c7eec246d3 | ||
|
|
bf06f4a4d7 | ||
|
|
811361dbbe | ||
|
|
dfbd31f35f | ||
|
|
6130226a23 | ||
|
|
d271c31edf | ||
|
|
4161f4d6eb | ||
|
|
70f14329e3 | ||
|
|
48dbf505ce | ||
|
|
7b3406a3e4 | ||
|
|
ea8e1f1131 | ||
|
|
8a290bf9b2 | ||
|
|
202883fc03 | ||
|
|
d867a789c2 | ||
|
|
7d36847f77 | ||
|
|
f597b052c6 | ||
|
|
5ae9d985b1 | ||
|
|
f589e44700 | ||
|
|
d9b546701e | ||
|
|
a6695ea1be | ||
|
|
9c9b367887 | ||
|
|
cee5d6b97f | ||
|
|
34583d1565 | ||
|
|
1f71520bb2 | ||
|
|
daaa5ff5c5 | ||
|
|
6f65973237 | ||
|
|
701c2fe7d0 | ||
|
|
d710a98f46 | ||
|
|
e2c4e6301f | ||
|
|
695864281b | ||
|
|
7adc8ab2d6 | ||
|
|
83906d223a | ||
|
|
321e5319c6 | ||
|
|
d079caafa8 | ||
|
|
a8dc940b73 | ||
|
|
675e6eca23 | ||
|
|
7b9d38e438 | ||
|
|
325846f20a | ||
|
|
2848ab1904 | ||
|
|
b995b9d5da | ||
|
|
7f8d4fa97a | ||
|
|
da417bff3e | ||
|
|
ff89ae7839 | ||
|
|
b59a1766c8 | ||
|
|
a436468026 | ||
|
|
c9176b7c58 | ||
|
|
a7509d5978 | ||
|
|
13cbdbb8bd | ||
|
|
029ffe3fc7 | ||
|
|
5b1cca92ab | ||
|
|
4f97b6342d | ||
|
|
558ef290e4 | ||
|
|
68f5ed4fa4 | ||
|
|
f5359b67a2 | ||
|
|
c83a8a64dc | ||
|
|
602d10c495 | ||
|
|
b78b89f04f | ||
|
|
e0ab70c8cf | ||
|
|
9c49900f9e | ||
|
|
e35dfe7aa3 | ||
|
|
444bd7244a | ||
|
|
4328169274 | ||
|
|
7597221776 | ||
|
|
940b124253 | ||
|
|
0cd39ae4ff | ||
|
|
bbc46d28ff | ||
|
|
46bdfa2932 | ||
|
|
e5cab11979 | ||
|
|
1c86669628 | ||
|
|
40455b2692 | ||
|
|
54c00deb5b | ||
|
|
a088f238e5 | ||
|
|
ed5fb4de4a | ||
|
|
17175ec3d4 | ||
|
|
05c09a1476 | ||
|
|
d42d980dda | ||
|
|
16e1f8eac9 | ||
|
|
220203dd9c | ||
|
|
203dad0004 | ||
|
|
8795f89c88 | ||
|
|
7608805c9a | ||
|
|
b130c2a284 | ||
|
|
81b2688c70 | ||
|
|
c060f9e5c2 | ||
|
|
cafe7181c7 | ||
|
|
64b32ec87d | ||
|
|
f0da508c21 | ||
|
|
038f803180 | ||
|
|
6de8f85e04 | ||
|
|
995b601222 | ||
|
|
9f6b3973d3 | ||
|
|
77035851a3 | ||
|
|
fce825f9df | ||
|
|
cd4eff364a | ||
|
|
ee4b24a5dc | ||
|
|
7a39021d41 | ||
|
|
37c6d63ddf | ||
|
|
6b2b096f3c | ||
|
|
62ae793a24 | ||
|
|
508c0f21fa | ||
|
|
d315a3131d | ||
|
|
ed3198db4e | ||
|
|
31b4f2e383 | ||
|
|
fc2787dd72 | ||
|
|
cb84cbf730 | ||
|
|
fc999f247b | ||
|
|
4f9be56d7d | ||
|
|
9f9e41687f | ||
|
|
3d4cf1fc92 | ||
|
|
d91c922b4c | ||
|
|
ed5af29ac9 | ||
|
|
f2c52f96a1 | ||
|
|
8b67d68d4a | ||
|
|
e80475ed57 | ||
|
|
d0aea24568 | ||
|
|
914f9db8ca | ||
|
|
f46d6cbe27 | ||
|
|
9b75e661e2 | ||
|
|
5d6eedcd49 | ||
|
|
a470bfc3f3 | ||
|
|
5cbd540e15 | ||
|
|
7e36884cbd | ||
|
|
64dc099d51 | ||
|
|
6a07d231f4 | ||
|
|
a64937122d | ||
|
|
a5d2d3109e | ||
|
|
1d237b0569 | ||
|
|
63b8d225e8 | ||
|
|
216d8c3c13 | ||
|
|
c558a8b4ad | ||
|
|
e9d606a782 | ||
|
|
6ae232d8fc | ||
|
|
8567fffdad | ||
|
|
b4f993e2ab | ||
|
|
43c1f62e39 | ||
|
|
a6a030e92f | ||
|
|
86f381a3b7 | ||
|
|
01b7b27bda | ||
|
|
b791095324 | ||
|
|
2649418c0b | ||
|
|
d2ece6b542 | ||
|
|
34e39bed4e | ||
|
|
fd8e66136f | ||
|
|
52e14950f1 | ||
|
|
7859df74c4 | ||
|
|
b07cca023b | ||
|
|
2136d00aa2 | ||
|
|
949172809d | ||
|
|
e36123735b | ||
|
|
0ab601aaf3 | ||
|
|
11cd5f8653 | ||
|
|
67eea1678e | ||
|
|
dcf39538a3 | ||
|
|
fd7342b055 | ||
|
|
394d9d1404 | ||
|
|
5ad66c29e1 | ||
|
|
babbd0771c | ||
|
|
7360ea69ba | ||
|
|
06a6daee34 | ||
|
|
cc11af5e28 | ||
|
|
b0fa821bc3 | ||
|
|
b1fda1b0ef | ||
|
|
55784f9783 | ||
|
|
334f6cda4f | ||
|
|
7c6dd60f35 | ||
|
|
37d20023b3 | ||
|
|
f9eecaf584 | ||
|
|
782561462b | ||
|
|
09f4c2199e | ||
|
|
b8086d1d13 | ||
|
|
2befebaf3a | ||
|
|
ecd399181d | ||
|
|
3bb5b5d1c9 | ||
|
|
b5c1db6bae | ||
|
|
9898b1af37 | ||
|
|
a8c55ffe08 | ||
|
|
3b0fb84c47 | ||
|
|
e4b0b7fffd | ||
|
|
70fb9bfd51 | ||
|
|
dde8ac6844 | ||
|
|
b999090a51 | ||
|
|
0207f8758b | ||
|
|
2fd01e57e6 | ||
|
|
ee7b4256a9 | ||
|
|
d9516b9c1d | ||
|
|
3c99436950 | ||
|
|
868f781f4d | ||
|
|
1c9d0857cb | ||
|
|
9421db8869 | ||
|
|
40f0aa0023 | ||
|
|
fea25d715c | ||
|
|
3fe851ed84 | ||
|
|
5420d6d2ae | ||
|
|
7e3e9618b2 | ||
|
|
e1bad17f2a | ||
|
|
b23ddb31c9 | ||
|
|
be02bfb37d | ||
|
|
df2c50aa60 | ||
|
|
de60ad04dc | ||
|
|
f72f82fb0c | ||
|
|
b78de501c2 | ||
|
|
47ce6aff89 | ||
|
|
ec053f8007 | ||
|
|
40d28b80bf | ||
|
|
2bd07937e5 | ||
|
|
09f025628f | ||
|
|
7444f83dbf | ||
|
|
17466b4f26 | ||
|
|
4e5a7a891e | ||
|
|
5107a87291 | ||
|
|
7b37d85150 | ||
|
|
e0cd878606 | ||
|
|
0719101b6f | ||
|
|
21b282ce29 | ||
|
|
a6307497c0 | ||
|
|
edafa7c99f | ||
|
|
3c71337698 | ||
|
|
00b287117a | ||
|
|
0937a64f1c | ||
|
|
675f95a2e4 | ||
|
|
12fc0821c0 | ||
|
|
9c5143786c | ||
|
|
a1f4dcd542 | ||
|
|
822e193682 | ||
|
|
04ee021bdb | ||
|
|
78f425c98b | ||
|
|
70b7314b76 | ||
|
|
deb0a10973 | ||
|
|
4de48620e3 | ||
|
|
a62e2a0c27 | ||
|
|
09c3528585 | ||
|
|
5dfd8d68bf | ||
|
|
3c3043eeae | ||
|
|
86f63e1ae6 | ||
|
|
4d650c8dfd | ||
|
|
1148184892 | ||
|
|
5dd4b2d56a | ||
|
|
52efa9f185 | ||
|
|
b0ba2125a3 | ||
|
|
aebc29337a | ||
|
|
59c9ee88e5 | ||
|
|
94b856c565 | ||
|
|
0b88c5d413 | ||
|
|
515d16f137 | ||
|
|
9f130c6b27 | ||
|
|
5c535213ad | ||
|
|
ac4186a0f0 | ||
|
|
5fe2230e1f | ||
|
|
16c397a9d2 | ||
|
|
edb65ecf50 | ||
|
|
cd95f94ac8 | ||
|
|
21753bc65f | ||
|
|
e21edd3e18 | ||
|
|
90614d1fe3 | ||
|
|
34d3d2cda6 | ||
|
|
56852821e8 | ||
|
|
7319ced0bc | ||
|
|
67ffa67968 | ||
|
|
7cbe174f1e | ||
|
|
7e607a0cf2 | ||
|
|
4fea285740 | ||
|
|
363f3d7ea7 | ||
|
|
ed716b2b90 | ||
|
|
2d590c581d | ||
|
|
6a42e0c96c | ||
|
|
55753171f1 | ||
|
|
3ee58fdea3 | ||
|
|
0c1e266073 | ||
|
|
5b9ae8bc85 | ||
|
|
37c3dbbc7d | ||
|
|
d2dd32b979 | ||
|
|
8e5a86fb13 | ||
|
|
bf286f8c74 | ||
|
|
115021b8ea | ||
|
|
b0a391932a | ||
|
|
048f7dcaf5 | ||
|
|
4305966f7b | ||
|
|
29ee605c79 | ||
|
|
674b316db3 | ||
|
|
ade9b17b22 | ||
|
|
f3979ad908 | ||
|
|
ebf9bc4e0a | ||
|
|
a58c3ff0c6 | ||
|
|
28ea459c05 | ||
|
|
89cdef851d | ||
|
|
8d625393ec | ||
|
|
6dbbd894b8 | ||
|
|
2e23ad59dc | ||
|
|
9bd2c60488 | ||
|
|
44eefc2c3b | ||
|
|
06adfc5bff | ||
|
|
db1ba9ac88 | ||
|
|
7ddbd24c30 | ||
|
|
53c942a2de | ||
|
|
794a275383 | ||
|
|
8ba04b460e | ||
|
|
ef03a79956 | ||
|
|
b429b919b5 | ||
|
|
80238ec2ac | ||
|
|
e0d5c3d2b1 | ||
|
|
ab123b8c80 | ||
|
|
60cc72b5a6 | ||
|
|
294560ec6d | ||
|
|
30131f0ec4 | ||
|
|
eb990d4bd5 | ||
|
|
70117265d6 | ||
|
|
6c2c9438a7 | ||
|
|
85bd29a7c8 | ||
|
|
efa3cbf04f | ||
|
|
4c9417ac6e | ||
|
|
965459ab36 | ||
|
|
27d635a394 | ||
|
|
955478799b | ||
|
|
a6a8bf9304 | ||
|
|
68e373df44 | ||
|
|
7f690c3f3f | ||
|
|
1ea28890b5 | ||
|
|
9bfff1c685 | ||
|
|
13e8ed53d6 | ||
|
|
8039e7ab74 | ||
|
|
e3c6a0b766 | ||
|
|
d1f6ae99b5 | ||
|
|
9ad76011c7 | ||
|
|
9f464fd283 | ||
|
|
f6cd73c784 | ||
|
|
3a481a2fa5 | ||
|
|
dce3e0fb78 | ||
|
|
596a3841dd | ||
|
|
07edcce697 | ||
|
|
ee9d3b6a49 | ||
|
|
4863df5ac8 | ||
|
|
e2b521a408 | ||
|
|
bce06d6f43 | ||
|
|
67ada03414 | ||
|
|
6ce3ad68f8 | ||
|
|
4172e39045 | ||
|
|
bd506b186b | ||
|
|
3a73351779 | ||
|
|
e3500e8bdf | ||
|
|
39fee34b91 | ||
|
|
b262c34ed9 | ||
|
|
9678fd1e09 | ||
|
|
cc67dba9f1 | ||
|
|
274644e83d | ||
|
|
2ec820f366 | ||
|
|
a3b0e7c1cb | ||
|
|
b178099f44 | ||
|
|
43812b6d2b | ||
|
|
f1de4cc0cf | ||
|
|
ac9fee310d | ||
|
|
a6dbdc3e84 | ||
|
|
af37272246 | ||
|
|
1c2aca5e82 | ||
|
|
aa7282819e | ||
|
|
d45acb0388 | ||
|
|
94ec712ea8 | ||
|
|
4f32d94f5f | ||
|
|
4ae3df62c5 | ||
|
|
b527cf53d2 | ||
|
|
1209d4192f | ||
|
|
6fbbc3f123 | ||
|
|
dece5dda78 | ||
|
|
ead437be82 | ||
|
|
49d3e9ece8 | ||
|
|
ce8315b720 | ||
|
|
fcbb5b8bac | ||
|
|
50c1d85137 | ||
|
|
eb463ab2d3 | ||
|
|
8d9a699b5b | ||
|
|
4178f73ed9 | ||
|
|
849706e310 | ||
|
|
ee59b133c0 | ||
|
|
f50b8bb8e8 | ||
|
|
f94ee172c9 | ||
|
|
d6012ad95c | ||
|
|
9d49f5a57d | ||
|
|
b6652ad6bc | ||
|
|
409de10fb4 | ||
|
|
8321c1a90f | ||
|
|
f5b1019d4d | ||
|
|
5255a71bc5 | ||
|
|
d49fa7c4a3 | ||
|
|
16f043034f | ||
|
|
610f9b7068 | ||
|
|
baf8d00a20 | ||
|
|
b037ec489f | ||
|
|
555bdb75b5 | ||
|
|
f6fbb098cc | ||
|
|
080f9f5bc2 | ||
|
|
900ad1ba6d | ||
|
|
3c43639cf1 | ||
|
|
f10841e003 | ||
|
|
99d921f169 | ||
|
|
7b63aea4ad | ||
|
|
a0a53ad435 | ||
|
|
3e167f7e2f | ||
|
|
0f34e6b374 | ||
|
|
ad83950410 | ||
|
|
2ab1d35a7c | ||
|
|
320b9cac3f | ||
|
|
03fbacd93c | ||
|
|
64731c2053 | ||
|
|
f9afa190b1 | ||
|
|
cc1899ebca | ||
|
|
980cf5ada1 | ||
|
|
3c2c7ecaae | ||
|
|
5e9fa2b57e | ||
|
|
cd136b7b33 | ||
|
|
87496617a4 | ||
|
|
7c350a29d5 | ||
|
|
7a6d26ef86 | ||
|
|
81f25251a5 | ||
|
|
f654013372 | ||
|
|
72e5bf35e1 | ||
|
|
eda6fc6e17 | ||
|
|
bd8c576322 | ||
|
|
81c69421c5 | ||
|
|
d8a01d84b3 | ||
|
|
bfeba3cee6 | ||
|
|
9e1c7e0117 | ||
|
|
2b285740d9 | ||
|
|
522b938974 | ||
|
|
8eee3f6bce | ||
|
|
f8301b185e | ||
|
|
b099f64efa | ||
|
|
5d4c1653e2 | ||
|
|
9578a3324e | ||
|
|
9b394e4111 | ||
|
|
f43f526c5b | ||
|
|
4fccc89d7d | ||
|
|
f70a75978b | ||
|
|
77953e73f5 | ||
|
|
8121b02f5c | ||
|
|
a83b86085a | ||
|
|
bc757b2b8c | ||
|
|
4b883c089e | ||
|
|
370c8a8b07 | ||
|
|
7ee99ba043 | ||
|
|
f9a1c8b83a | ||
|
|
bf32c544a2 | ||
|
|
cfe360b95e | ||
|
|
054e9ab439 | ||
|
|
5c1401b0a1 |
20
.flake8
20
.flake8
@@ -1,5 +1,5 @@
|
||||
[flake8]
|
||||
exclude = .venv,.hypothesis,.git,__pycache__,resources.py
|
||||
exclude = .*,__pycache__,resources.py
|
||||
# E128: continuation line under-indented for visual indent
|
||||
# E226: missing whitespace around arithmetic operator
|
||||
# E265: Block comment should start with '#'
|
||||
@@ -8,17 +8,6 @@ exclude = .venv,.hypothesis,.git,__pycache__,resources.py
|
||||
# E266: too many leading '#' for block comment
|
||||
# F401: Unused import
|
||||
# N802: function name should be lowercase
|
||||
# L101: The __init__ method of classes must not have a docstring
|
||||
# L102: A docstring was incorrectly formatted.
|
||||
# L103: A test docstring must not start with any form of the words "test", ...
|
||||
# L201: Container literals must have a trailing comma
|
||||
# L202: print is not allowed except for debugging.
|
||||
# L203: pdb and compatible modules are not allowed except for debugging.
|
||||
# L204: Implicit string literal concatenation is only allowed if every string
|
||||
# being concatenated is parenthesize
|
||||
# L207: pass is only necessary in non-optional suites containing no other
|
||||
# statements.
|
||||
# L302: The line was too long.
|
||||
# P101: format string does contain unindexed parameters
|
||||
# P102: docstring does contain unindexed parameters
|
||||
# P103: other string does contain unindexed parameters
|
||||
@@ -30,6 +19,8 @@ exclude = .venv,.hypothesis,.git,__pycache__,resources.py
|
||||
# D211: No blank lines allowed before class docstring
|
||||
# (PEP257 got changed, but let's stick to the old standard)
|
||||
# D402: First line should not be function's signature (false-positives)
|
||||
# D403: First word of the first line should be properly capitalized
|
||||
# (false-positives)
|
||||
# H101: Use TODO(NAME)
|
||||
# H201: bare except
|
||||
# H238: Use new-stule classes
|
||||
@@ -39,9 +30,8 @@ ignore =
|
||||
E128,E226,E265,E501,E402,E266,
|
||||
F401,
|
||||
N802,
|
||||
L101,L102,L103,L201,L202,L203,L204,L207,L302,
|
||||
P101,P102,P103,
|
||||
D102,D103,D104,D105,D209,D211,D402,
|
||||
D102,D103,D104,D105,D209,D211,D402,D403,
|
||||
H101,H201,H238,H301,H306
|
||||
min-version = 3.4.0
|
||||
max-complexity = 12
|
||||
@@ -51,7 +41,7 @@ putty-ignore =
|
||||
/# pylint: disable=wildcard-import/ : +F403
|
||||
/# pragma: no mccabe/ : +C901
|
||||
tests/*/test_*.py : +D100,D101,D401
|
||||
tests/unit/browser/http/test_content_disposition.py : +D400
|
||||
tests/unit/browser/webkit/http/test_content_disposition.py : +D400
|
||||
scripts/dev/ci/appveyor_install.py : +FI53
|
||||
copyright-check = True
|
||||
copyright-regexp = # Copyright [\d-]+ .*
|
||||
|
||||
4
.github/ISSUE_TEMPLATE.md
vendored
Normal file
4
.github/ISSUE_TEMPLATE.md
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
Please remember to mention your version info (qutebrowser, Qt, PyQt,
|
||||
OS/distribution) from the `qute:version` page or `qutebrowser --version`
|
||||
|
||||
---
|
||||
14
.pylintrc
14
.pylintrc
@@ -6,7 +6,10 @@ extension-pkg-whitelist=PyQt5,sip
|
||||
load-plugins=qute_pylint.config,
|
||||
qute_pylint.modeline,
|
||||
qute_pylint.openencoding,
|
||||
qute_pylint.settrace
|
||||
qute_pylint.settrace,
|
||||
pylint.extensions.bad_builtin,
|
||||
pylint.extensions.docstyle
|
||||
persistent=n
|
||||
|
||||
[MESSAGES CONTROL]
|
||||
enable=all
|
||||
@@ -14,6 +17,7 @@ disable=no-self-use,
|
||||
fixme,
|
||||
global-statement,
|
||||
locally-disabled,
|
||||
locally-enabled,
|
||||
too-many-ancestors,
|
||||
too-few-public-methods,
|
||||
too-many-public-methods,
|
||||
@@ -32,12 +36,14 @@ disable=no-self-use,
|
||||
ungrouped-imports,
|
||||
redefined-variable-type,
|
||||
suppressed-message,
|
||||
too-many-return-statements
|
||||
too-many-return-statements,
|
||||
duplicate-code,
|
||||
wrong-import-position
|
||||
|
||||
[BASIC]
|
||||
function-rgx=[a-z_][a-z0-9_]{2,50}$
|
||||
const-rgx=[A-Za-z_][A-Za-z0-9_]{0,30}$
|
||||
method-rgx=[a-z_][A-Za-z0-9_]{2,50}$
|
||||
method-rgx=[a-z_][A-Za-z0-9_]{1,50}$
|
||||
attr-rgx=[a-z_][a-z0-9_]{0,30}$
|
||||
argument-rgx=[a-z_][a-z0-9_]{0,30}$
|
||||
variable-rgx=[a-z_][a-z0-9_]{0,30}$
|
||||
@@ -66,5 +72,5 @@ valid-metaclass-classmethod-first-arg=cls
|
||||
# https://bitbucket.org/logilab/pylint/issues/690/
|
||||
# UnsetObject because pylint infers any objreg.get(...) as UnsetObject.
|
||||
ignored-classes=qutebrowser.utils.objreg.UnsetObject,
|
||||
qutebrowser.browser.webelem.WebElementWrapper,
|
||||
qutebrowser.browser.webkit.webelem.WebElementWrapper,
|
||||
scripts.dev.check_coverage.MsgType
|
||||
|
||||
@@ -14,6 +14,64 @@ This project adheres to http://semver.org/[Semantic Versioning].
|
||||
// `Fixed` for any bug fixes.
|
||||
// `Security` to invite users to upgrade in case of vulnerabilities.
|
||||
|
||||
v0.8.0
|
||||
------
|
||||
|
||||
Added
|
||||
~~~~~
|
||||
|
||||
- New `:repeat-command` command (mapped to `.`) to repeat the last command.
|
||||
Note that two former default bundings conflict with that binding, unbinding
|
||||
them via `:unbind .i` and `:unbind .o` is recommended.
|
||||
- New `qute:bookmarks` page which displays all bookmarks and quickmarks.
|
||||
- New `:prompt-open-download` (bound to `Ctrl-X`) which can be used to open a
|
||||
download directly when getting the filename prompt.
|
||||
- New `{host}` replacement for tab- and window titles which evaluates
|
||||
to the current host.
|
||||
- New default binding `;t` for `:hint input`.
|
||||
- New variables `$QUTE_CONFIG_DIR`, `$QUTE_DATA_DIR` and
|
||||
`$QUTE_DOWNLOAD_DIR` available for userscripts.
|
||||
- New option `ui` -> `status-position` to configure the position of the
|
||||
status bar (top/bottom).
|
||||
- New `--pdf <filename>` argument for `:print` which can be used to generate a
|
||||
PDF without a dialog.
|
||||
|
||||
Changed
|
||||
~~~~~~~
|
||||
|
||||
- `:scroll-perc` now prefers a count over the argument given to it, which means
|
||||
`gg` can be used with a count.
|
||||
- Aliases can now use `;;` to have an alias which executed multiple commands.
|
||||
- `:edit-url` now does nothing if the URL isn't changed in the spawned editor.
|
||||
- `:bookmark-add` can now be passed a URL and title to add that as a bookmark
|
||||
rather than the current page.
|
||||
- New `taskadd` userscript to add a taskwarrior task annotated with the
|
||||
current URL.
|
||||
- `:bookmark-del` and `:quickmark-del` now delete the current page's URL if none
|
||||
is given.
|
||||
|
||||
Fixed
|
||||
-----
|
||||
|
||||
- Compatibility with PyQt 5.7
|
||||
- Fixed some configuration values being lost when a config option gets removed
|
||||
from qutebrowser's code.
|
||||
- Fix crash when downloading with a full disk
|
||||
- Using `:jump-mark` (e.g. `''`) when the current URL is invalid doesn't crash
|
||||
anymore.
|
||||
|
||||
Removed
|
||||
-------
|
||||
|
||||
- The ability to display status messages from webpages, as well as the related
|
||||
`ui -> display-statusbar-messages` setting.
|
||||
- The `general -> wrap-search` setting as searches now always wrap.
|
||||
According to a quick straw poll and prior crash logs, almost nobody is using
|
||||
`wrap-search = false`, and turning off wrapping is not possible with
|
||||
QtWebEngine.
|
||||
- `:edit-url` now doesn't accept a count anymore as its behavior was confusing
|
||||
and it doesn't make much sense to add a count.
|
||||
|
||||
v0.7.0
|
||||
------
|
||||
|
||||
@@ -38,6 +96,7 @@ Added
|
||||
- New `hints -> find-implementation` to select which implementation (JS/Python)
|
||||
should be used to find hints on a page. The `javascript` implementation is
|
||||
better, but slower.
|
||||
- New `inputs` group for `:hint` to hint text input fields.
|
||||
|
||||
Changed
|
||||
~~~~~~~
|
||||
@@ -81,7 +140,7 @@ Fixed
|
||||
`-v` argument for `:spawn` or pass flags to the spawned commands.
|
||||
- Various fixes for hinting corner-cases where following a link didn't work or
|
||||
the hint was drawn at the wrong position.
|
||||
- Fixed crash when downloading from an URL with SSL errors
|
||||
- Fixed crash when downloading from a URL with SSL errors
|
||||
- Close file handles correctly when a download failed
|
||||
- Fixed crash when using `;Y` (`:hint links yank-primary`) on a system without
|
||||
primary selection
|
||||
@@ -90,7 +149,7 @@ Fixed
|
||||
- Fixed a crash when entering `:-- ` in the commandline
|
||||
- Fixed `:debug-console` with PyQt 5.6
|
||||
- Fixed qutebrowser not starting when `sys.stderr` is `None`
|
||||
- Fixed crash when cancelling a download which belongs to a MHTML download
|
||||
- Fixed crash when cancelling a download which belongs to an MHTML download
|
||||
- Fixed rebinding of keybindings being case-sensitive
|
||||
- Fix for tab indicators getting lost when moving tabs
|
||||
- Fixed handling of backspace in number hinting mode
|
||||
@@ -492,7 +551,7 @@ Fixed
|
||||
~~~~~
|
||||
|
||||
- Scrolling should now work more reliably on some pages where arrow keys worked but `hjkl` didn't.
|
||||
- Small improvements when checking if an input is an URL or not.
|
||||
- Small improvements when checking if an input is a URL or not.
|
||||
- Fixed wrong cursor position when completing the first item in the completion.
|
||||
- Fixed exception when using search engines with {foo} in their name.
|
||||
- Fixed a bug where the same title was shown for all tabs on some systems.
|
||||
@@ -504,7 +563,7 @@ Fixed
|
||||
- Various fixes for deprecated key bindings and auto-migrations.
|
||||
- Workaround for qutebrowser not starting when there are NUL-bytes in the history (because of a currently unknown bug).
|
||||
- Fixed handling of keybindings containing Ctrl/Meta on OS X.
|
||||
- Fixed crash when downloading an URL without filename (e.g. magnet links) via "Save as...".
|
||||
- Fixed crash when downloading a URL without filename (e.g. magnet links) via "Save as...".
|
||||
- Fixed exception when starting qutebrowser with `:set` as argument.
|
||||
- Fixed horrible completion performance when the `shrink` option was set.
|
||||
- Sessions now store zoom/scroll-position correctly.
|
||||
@@ -655,7 +714,7 @@ Fixed
|
||||
- Scroll completion to top when showing it.
|
||||
- Handle unencodable file paths in config types correctly.
|
||||
- Fix for crash when executing a delayed command (because of a shadowed keybinding) and then unfocusing the window.
|
||||
- Fix for crash when hinting on a page which doesn't have an URL yet.
|
||||
- Fix for crash when hinting on a page which doesn't have a URL yet.
|
||||
- Fix exception when using `:set-cmd-text` with an empty argument.
|
||||
- Add a timeout to pastebin HTTP replies.
|
||||
- Various other fixes for small/rare bugs.
|
||||
|
||||
@@ -619,9 +619,9 @@ https://github.com/The-Compiler/qutebrowser/issues?q=is%3Aopen+is%3Aissue+label%
|
||||
bugs] and check if they're fixed.
|
||||
* As soon as Homebrew updated, update the custom OS X bottle:
|
||||
- Update https://github.com/The-Compiler/homebrew-qt5-webkit/blob/master/Formula/qt5.rb[qt5.rb]
|
||||
- `brew install --build-from-source --verbose qt5.rb`
|
||||
- `brew bottle qt5`
|
||||
- `brew install --build-from-source --verbose pyqt5`
|
||||
- `brew install --build-from-source --build-bottle --verbose qt5.rb`
|
||||
- `brew bottle qt5.rb`
|
||||
- `brew install --build-from-source --build-bottle --verbose pyqt5`
|
||||
- `brew bottle pyqt5`
|
||||
- Upload bottles to github
|
||||
- Adjust `scripts/dev/ci/travis_install.sh`
|
||||
@@ -669,5 +669,5 @@ as closed.
|
||||
- `scp bb-win8:proj/qutebrowser/qutebrowser-0.X.Y-windows.zip .`
|
||||
- `aunpack qutebrowser-0.X.Y-windows.zip`
|
||||
- `sudo mv qutebrowser-0.X.Y-windows/* /srv/http/qutebrowser/releases/v0.X.Y/windows`
|
||||
* Update `qutebrowser-git` PKGBUILD
|
||||
* Update `qutebrowser-git` PKGBUILD if dependencies/install changed
|
||||
* Announce to qutebrowser mailinglist
|
||||
|
||||
@@ -229,11 +229,17 @@ Then <<tox,install qutebrowser via tox>>.
|
||||
On OS X
|
||||
-------
|
||||
|
||||
To install qutebrowser on OS X, you'll want a package manager, e.g.
|
||||
http://brew.sh/[Homebrew] or https://www.macports.org/[MacPorts].
|
||||
The easiest way to install qutebrowser on OS X is to use the prebuilt `.app`
|
||||
files from the
|
||||
https://github.com/The-Compiler/qutebrowser/releases[release page].
|
||||
|
||||
Alternatively, you can install the dependencies via a package manager (like
|
||||
http://brew.sh/[Homebrew] or https://www.macports.org/[MacPorts]) and run
|
||||
qutebrowser from source.
|
||||
|
||||
For Homebrew, a few extra steps are necessary since Homebrew dropped QtWebKit
|
||||
from Qt 5.6.
|
||||
from Qt 5.6 - however, some users reported this didn't work for them, so using
|
||||
the `.app` is strongly encouraged.
|
||||
|
||||
This installs a Qt 5.5 and symlinks it so PyQt5 will work with it instead of Qt
|
||||
5.6. This requires that `qt5` is not installed via Homebrew:
|
||||
|
||||
@@ -6,7 +6,8 @@ graft qutebrowser/html
|
||||
graft qutebrowser/3rdparty
|
||||
graft icons
|
||||
graft doc/img
|
||||
graft misc
|
||||
graft misc/apparmor
|
||||
graft misc/userscripts
|
||||
recursive-include scripts *.py
|
||||
include qutebrowser/utils/testfile
|
||||
include qutebrowser/git-commit-id
|
||||
@@ -15,16 +16,20 @@ include qutebrowser.desktop
|
||||
include requirements.txt
|
||||
include tox.ini
|
||||
include qutebrowser.py
|
||||
include misc/cheatsheet.svg
|
||||
|
||||
prune www
|
||||
prune scripts/dev
|
||||
prune scripts/testbrowser_cpp
|
||||
prune .github
|
||||
exclude scripts/asciidoc2html.py
|
||||
exclude doc/notes
|
||||
recursive-exclude doc *.asciidoc
|
||||
include doc/qutebrowser.1.asciidoc
|
||||
prune tests
|
||||
prune qutebrowser/3rdparty
|
||||
prune misc/requirements
|
||||
prune misc/docker
|
||||
exclude .editorconfig
|
||||
exclude pytest.ini
|
||||
exclude qutebrowser.rcc
|
||||
@@ -38,6 +43,7 @@ exclude .travis.yml
|
||||
exclude codecov.yml
|
||||
exclude .pydocstylerc
|
||||
exclude misc/appveyor_install.py
|
||||
exclude misc/qutebrowser.spec
|
||||
exclude .flake8
|
||||
|
||||
global-exclude __pycache__ *.pyc *.pyo
|
||||
|
||||
@@ -141,8 +141,8 @@ Contributors, sorted by the number of commits in descending order:
|
||||
// QUTE_AUTHORS_START
|
||||
* Florian Bruhin
|
||||
* Daniel Schadt
|
||||
* Antoni Boucher
|
||||
* Ryan Roden-Corrent
|
||||
* Antoni Boucher
|
||||
* Lamar Pavel
|
||||
* Bruno Oliveira
|
||||
* Alexander Cogneau
|
||||
@@ -151,6 +151,7 @@ Contributors, sorted by the number of commits in descending order:
|
||||
* Jakub Klinkovský
|
||||
* Raphael Pierzina
|
||||
* Joel Torstensson
|
||||
* Jan Verbeek
|
||||
* Tarcisio Fedrizzi
|
||||
* Patric Schmitz
|
||||
* Claude
|
||||
@@ -164,6 +165,7 @@ Contributors, sorted by the number of commits in descending order:
|
||||
* Kevin Velghe
|
||||
* Austin Anderson
|
||||
* Jimmy
|
||||
* Marshall Lochbaum
|
||||
* Alexey "Averrin" Nabrodov
|
||||
* avk
|
||||
* ZDarian
|
||||
@@ -178,11 +180,14 @@ Contributors, sorted by the number of commits in descending order:
|
||||
* skinnay
|
||||
* Zach-Button
|
||||
* Tomasz Kramkowski
|
||||
* Ismail S
|
||||
* Halfwit
|
||||
* David Vogt
|
||||
* rikn00
|
||||
* kanikaa1234
|
||||
* haitaka
|
||||
* Nick Ginther
|
||||
* Michał Góral
|
||||
* Michael Ilsaas
|
||||
* Martin Zimmermann
|
||||
* Fritz Reichwald
|
||||
@@ -194,16 +199,20 @@ Contributors, sorted by the number of commits in descending order:
|
||||
* Stefan Tatschner
|
||||
* Samuel Loury
|
||||
* Peter Michely
|
||||
* Panashe M. Fundira
|
||||
* Link
|
||||
* Larry Hynes
|
||||
* Johannes Altmanninger
|
||||
* Jeremy Kaplan
|
||||
* Ismail
|
||||
* Edgar Hipp
|
||||
* Daryl Finlay
|
||||
* adam
|
||||
* Samir Benmendil
|
||||
* Regina Hug
|
||||
* Mathias Fussenegger
|
||||
* Marcelo Santos
|
||||
* Jan Verbeek
|
||||
* Jean-Louis Fuchs
|
||||
* Fritz V155 Reichwald
|
||||
* Franz Fellner
|
||||
* zwarag
|
||||
@@ -229,6 +238,7 @@ Contributors, sorted by the number of commits in descending order:
|
||||
* HalosGhost
|
||||
* Gregor Pohl
|
||||
* Eivind Uggedal
|
||||
* Dietrich Daroch
|
||||
* Daniel Lu
|
||||
* Arseniy Seroka
|
||||
* Andy Balaam
|
||||
@@ -283,6 +293,7 @@ problems and helpful hints:
|
||||
|
||||
Also, thanks to:
|
||||
|
||||
* Everyone contributing to the link:doc/backers.asciidoc[crowdfunding].
|
||||
* Everyone who had the patience to test qutebrowser before v0.1.
|
||||
* Everyone triaging/fixing my bugs in the
|
||||
https://bugreports.qt.io/secure/Dashboard.jspa[Qt bugtracker]
|
||||
|
||||
172
doc/backers.asciidoc
Normal file
172
doc/backers.asciidoc
Normal file
@@ -0,0 +1,172 @@
|
||||
Crowdfunding backers
|
||||
====================
|
||||
|
||||
Mid-2016, qutebrowser did run a http://igg.me/at/qutebrowser[crowdfunding] for
|
||||
QtWebEngine support in qutebrowser.
|
||||
|
||||
Thanks a lot to the following people who contributed to it:
|
||||
|
||||
Gold sponsors
|
||||
-------------
|
||||
|
||||
- Chris Salzberg
|
||||
- Clayton Craft
|
||||
- Jean-Louis Fuchs
|
||||
- Matthias Lisin
|
||||
- 1 Anonymous
|
||||
|
||||
Day sponsors
|
||||
------------
|
||||
|
||||
- Agent 42
|
||||
- Iggy Jackson
|
||||
- James B
|
||||
- Rudi Seitz
|
||||
- Tim „Das MooL“ Wegener
|
||||
- amd1212
|
||||
- gavtroy
|
||||
- 4 Anonymous
|
||||
|
||||
Other sponsors
|
||||
--------------
|
||||
|
||||
- AP M
|
||||
- Alessandro Balzano
|
||||
- Allan Nordhøy
|
||||
- Andor Uhlar
|
||||
- Andreas Leppert
|
||||
- Andreas Saga Romsdal
|
||||
- Andrew Rogers / tuxlovesyou
|
||||
- André Glüpker
|
||||
- Arian Sanusi
|
||||
- Arin Lares
|
||||
- Assaf Lavie
|
||||
- Baptiste Wicht
|
||||
- Benjamin Richter
|
||||
- Benjamin Schnitzler
|
||||
- Bernardo Kuri
|
||||
- Boris Kourtoukov
|
||||
- Brian Buccola
|
||||
- Bruno Oliveira
|
||||
- Bryan Gilbert
|
||||
- Cassandra Rebecca Ruppen
|
||||
- Charles Saternos
|
||||
- Chris H
|
||||
- Christian Karl
|
||||
- Christian Lange
|
||||
- Christian Strasser
|
||||
- Colin O'Brien
|
||||
- Corsin Pfister
|
||||
- Cosmin Popescu
|
||||
- Daniel Andersson
|
||||
- David Wilson
|
||||
- Demure Demeanor
|
||||
- Doug Stone-Weaver
|
||||
- Eero Kari
|
||||
- Enric Morales
|
||||
- Eric Krohn
|
||||
- Eskild Hustvedt
|
||||
- Federico Panico
|
||||
- Felix Van der Jeugt
|
||||
- Francis Tseng
|
||||
- Geir Isene
|
||||
- George Voronin
|
||||
- German Correa
|
||||
- Grady Martin
|
||||
- Gregor Böhl
|
||||
- Guilherme Stein
|
||||
- Hannes Doyle
|
||||
- Hasan Soydabas
|
||||
- Ian Scott
|
||||
- Jacob Boldman
|
||||
- Jacob Wikmark
|
||||
- Jan Verbeek
|
||||
- Jarrod Seccombe
|
||||
- Joel Bradshaw
|
||||
- Johannes Martinsson
|
||||
- Jonas Schürmann
|
||||
- Josh Medeiros
|
||||
- José Alberto Orejuela García
|
||||
- Julie Engel
|
||||
- Jörg Behrmann
|
||||
- Jørgen Skancke
|
||||
- Kevin Velghe
|
||||
- Konstantin Shmelkov
|
||||
- Kyle Frazer
|
||||
- Lukas Gierth
|
||||
- Mar v Leeuwaarde
|
||||
- Marek Roszman
|
||||
- Marius Betz
|
||||
- Marius Krämer
|
||||
- Markus Schmidinger
|
||||
- Martin Gabelmann
|
||||
- Martin Zimmermann
|
||||
- Mathias Fußenegger
|
||||
- Maxime Wack
|
||||
- Michał Góral
|
||||
- Nathan Isom
|
||||
- Nathanael Philipp
|
||||
- Nils Stål
|
||||
- Oliver Hope
|
||||
- Oskar Nyberg
|
||||
- Pablo Navarro
|
||||
- Panashe M. Fundira
|
||||
- Patric Schmitz
|
||||
- Pete M
|
||||
- Peter Smith
|
||||
- Phil Collins
|
||||
- Philipp Hansch
|
||||
- Philipp Kuhnz
|
||||
- Raphael Khaiat
|
||||
- Raphael Pierzina
|
||||
- Renan Guilherme
|
||||
- Rick Losie
|
||||
- Robert Cross
|
||||
- Roy Van Ginneken
|
||||
- Rupus Reinefjord
|
||||
- Ryan Roden-Corrent
|
||||
- Samir Benmendil
|
||||
- Simon Giotta
|
||||
- Stephen England
|
||||
- Sverrir H Steindorsson
|
||||
- Tarcisio Fedrizzi
|
||||
- Thorsten Wißmann
|
||||
- Timon Stampfli
|
||||
- Tjelvar Olsson
|
||||
- Tomasz Kramkowski
|
||||
- Tsukiko Tsutsukakushi
|
||||
- Vasilij Schneidermann
|
||||
- Vinney Cavallo
|
||||
- Wesly Grefrath
|
||||
- Will Ware
|
||||
- Yousaf Khurshid
|
||||
- Zach Schultz
|
||||
- averrin
|
||||
- ben hengst
|
||||
- colin
|
||||
- craigtski47
|
||||
- dag.robole
|
||||
- daniel.m.kao
|
||||
- diepfann3
|
||||
- eamonn oneil
|
||||
- esakaforever
|
||||
- francois47
|
||||
- glspisso
|
||||
- gmccoy4242
|
||||
- gtcee3
|
||||
- jonathf
|
||||
- lapinski.maciej
|
||||
- lauri.hakko
|
||||
- ljanzen
|
||||
- mutilx9
|
||||
- nussgipfel
|
||||
- oed
|
||||
- p p
|
||||
- r.c.bruno.andre
|
||||
- robert.perce
|
||||
- sghctoma
|
||||
- targy
|
||||
- freelancer
|
||||
- pupu
|
||||
- regines
|
||||
- 37 Anonymous
|
||||
@@ -8,7 +8,7 @@
|
||||
|<<adblock-update,adblock-update>>|Update the adblock block lists.
|
||||
|<<back,back>>|Go back in the history of the current tab.
|
||||
|<<bind,bind>>|Bind a key to a command.
|
||||
|<<bookmark-add,bookmark-add>>|Save the current page as a bookmark.
|
||||
|<<bookmark-add,bookmark-add>>|Save the current page as a bookmark, or a specific url.
|
||||
|<<bookmark-del,bookmark-del>>|Delete a bookmark.
|
||||
|<<bookmark-load,bookmark-load>>|Load a bookmark.
|
||||
|<<buffer,buffer>>|Select tab by index or url/title best match.
|
||||
@@ -116,16 +116,25 @@ Bind a key to a command.
|
||||
|
||||
[[bookmark-add]]
|
||||
=== bookmark-add
|
||||
Save the current page as a bookmark.
|
||||
Syntax: +:bookmark-add ['url'] ['title']+
|
||||
|
||||
Save the current page as a bookmark, or a specific url.
|
||||
|
||||
If no url and title are provided, then save the current page as a bookmark. If a url and title have been provided, then save the given url as a bookmark with the provided title. You can view all saved bookmarks on the link:qute://bookmarks[bookmarks page].
|
||||
|
||||
==== positional arguments
|
||||
* +'url'+: url to save as a bookmark. If None, use url of current page.
|
||||
* +'title'+: title of the new bookmark.
|
||||
|
||||
[[bookmark-del]]
|
||||
=== bookmark-del
|
||||
Syntax: +:bookmark-del 'url'+
|
||||
Syntax: +:bookmark-del ['url']+
|
||||
|
||||
Delete a bookmark.
|
||||
|
||||
==== positional arguments
|
||||
* +'url'+: The URL of the bookmark to delete.
|
||||
* +'url'+: The url of the bookmark to delete. If not given, use the current page's url.
|
||||
|
||||
|
||||
==== note
|
||||
* This command does not split arguments after the last argument and handles quotes literally.
|
||||
@@ -243,9 +252,6 @@ The editor which should be launched can be configured via the `general -> editor
|
||||
* +*-t*+, +*--tab*+: Open in a new tab.
|
||||
* +*-w*+, +*--window*+: Open in a new window.
|
||||
|
||||
==== count
|
||||
The tab index to open the URL in.
|
||||
|
||||
[[fake-key]]
|
||||
=== fake-key
|
||||
Syntax: +:fake-key [*--global*] 'keystring'+
|
||||
@@ -308,6 +314,7 @@ Start hinting.
|
||||
- `all`: All clickable elements.
|
||||
- `links`: Only links.
|
||||
- `images`: Only images.
|
||||
- `inputs`: Only input fields.
|
||||
|
||||
|
||||
|
||||
@@ -489,12 +496,13 @@ If the pasted text contains newlines, each line gets opened in its own tab.
|
||||
|
||||
[[print]]
|
||||
=== print
|
||||
Syntax: +:print [*--preview*]+
|
||||
Syntax: +:print [*--preview*] [*--pdf* 'file']+
|
||||
|
||||
Print the current/[count]th tab.
|
||||
|
||||
==== optional arguments
|
||||
* +*-p*+, +*--preview*+: Show preview instead of printing.
|
||||
* +*-f*+, +*--pdf*+: The file path to write the PDF to.
|
||||
|
||||
==== count
|
||||
The tab index to print.
|
||||
@@ -505,18 +513,22 @@ Syntax: +:quickmark-add 'url' 'name'+
|
||||
|
||||
Add a new quickmark.
|
||||
|
||||
You can view all saved quickmarks on the link:qute://bookmarks[bookmarks page].
|
||||
|
||||
==== positional arguments
|
||||
* +'url'+: The url to add as quickmark.
|
||||
* +'name'+: The name for the new quickmark.
|
||||
|
||||
[[quickmark-del]]
|
||||
=== quickmark-del
|
||||
Syntax: +:quickmark-del 'name'+
|
||||
Syntax: +:quickmark-del ['name']+
|
||||
|
||||
Delete a quickmark.
|
||||
|
||||
==== positional arguments
|
||||
* +'name'+: The name of the quickmark to delete.
|
||||
* +'name'+: The name of the quickmark to delete. If not given, delete the quickmark for the current page (choosing one arbitrarily
|
||||
if there are more than one).
|
||||
|
||||
|
||||
==== note
|
||||
* This command does not split arguments after the last argument and handles quotes literally.
|
||||
@@ -924,7 +936,9 @@ How many steps to zoom out.
|
||||
|<<paste-primary,paste-primary>>|Paste the primary selection at cursor position.
|
||||
|<<prompt-accept,prompt-accept>>|Accept the current prompt.
|
||||
|<<prompt-no,prompt-no>>|Answer no to a yes/no prompt.
|
||||
|<<prompt-open-download,prompt-open-download>>|Immediately open a download.
|
||||
|<<prompt-yes,prompt-yes>>|Answer yes to a yes/no prompt.
|
||||
|<<repeat-command,repeat-command>>|Repeat the last executed command.
|
||||
|<<rl-backward-char,rl-backward-char>>|Move back a character.
|
||||
|<<rl-backward-delete-char,rl-backward-delete-char>>|Delete the character before the cursor.
|
||||
|<<rl-backward-word,rl-backward-word>>|Move back to the start of the current or previous word.
|
||||
@@ -1147,10 +1161,21 @@ Accept the current prompt.
|
||||
=== prompt-no
|
||||
Answer no to a yes/no prompt.
|
||||
|
||||
[[prompt-open-download]]
|
||||
=== prompt-open-download
|
||||
Immediately open a download.
|
||||
|
||||
[[prompt-yes]]
|
||||
=== prompt-yes
|
||||
Answer yes to a yes/no prompt.
|
||||
|
||||
[[repeat-command]]
|
||||
=== repeat-command
|
||||
Repeat the last executed command.
|
||||
|
||||
==== count
|
||||
Which count to pass the command.
|
||||
|
||||
[[rl-backward-char]]
|
||||
=== rl-backward-char
|
||||
Move back a character.
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
|==============
|
||||
|Setting|Description
|
||||
|<<general-ignore-case,ignore-case>>|Whether to find text on a page case-insensitively.
|
||||
|<<general-wrap-search,wrap-search>>|Whether to wrap finding text to the top when arriving at the end.
|
||||
|<<general-startpage,startpage>>|The default page(s) to open at the start, separated by commas.
|
||||
|<<general-default-page,default-page>>|The page to open if :open -t/-b/-w is used without URL. Use `about:blank` for a blank page.
|
||||
|<<general-auto-search,auto-search>>|Whether to start a search when something else than a URL is entered.
|
||||
@@ -33,10 +32,10 @@
|
||||
|<<ui-zoom-levels,zoom-levels>>|The available zoom levels, separated by commas.
|
||||
|<<ui-default-zoom,default-zoom>>|The default zoom level.
|
||||
|<<ui-downloads-position,downloads-position>>|Where to show the downloaded files.
|
||||
|<<ui-status-position,status-position>>|The position of the status bar.
|
||||
|<<ui-message-timeout,message-timeout>>|Time (in ms) to show messages in the statusbar for.
|
||||
|<<ui-message-unfocused,message-unfocused>>|Whether to show messages in unfocused windows.
|
||||
|<<ui-confirm-quit,confirm-quit>>|Whether to confirm quitting the application.
|
||||
|<<ui-display-statusbar-messages,display-statusbar-messages>>|Whether to display javascript statusbar messages.
|
||||
|<<ui-zoom-text-only,zoom-text-only>>|Whether the zoom factor on a frame applies only to the text or to all content.
|
||||
|<<ui-frame-flattening,frame-flattening>>|Whether to expand each subframe to its contents.
|
||||
|<<ui-user-stylesheet,user-stylesheet>>|User stylesheet to use (absolute filename, filename relative to the config directory or CSS string). Will expand environment variables.
|
||||
@@ -300,17 +299,6 @@ Valid values:
|
||||
|
||||
Default: +pass:[smart]+
|
||||
|
||||
[[general-wrap-search]]
|
||||
=== wrap-search
|
||||
Whether to wrap finding text to the top when arriving at the end.
|
||||
|
||||
Valid values:
|
||||
|
||||
* +true+
|
||||
* +false+
|
||||
|
||||
Default: +pass:[true]+
|
||||
|
||||
[[general-startpage]]
|
||||
=== startpage
|
||||
The default page(s) to open at the start, separated by commas.
|
||||
@@ -516,6 +504,17 @@ Valid values:
|
||||
|
||||
Default: +pass:[top]+
|
||||
|
||||
[[ui-status-position]]
|
||||
=== status-position
|
||||
The position of the status bar.
|
||||
|
||||
Valid values:
|
||||
|
||||
* +top+
|
||||
* +bottom+
|
||||
|
||||
Default: +pass:[bottom]+
|
||||
|
||||
[[ui-message-timeout]]
|
||||
=== message-timeout
|
||||
Time (in ms) to show messages in the statusbar for.
|
||||
@@ -546,17 +545,6 @@ Valid values:
|
||||
|
||||
Default: +pass:[never]+
|
||||
|
||||
[[ui-display-statusbar-messages]]
|
||||
=== display-statusbar-messages
|
||||
Whether to display javascript statusbar messages.
|
||||
|
||||
Valid values:
|
||||
|
||||
* +true+
|
||||
* +false+
|
||||
|
||||
Default: +pass:[false]+
|
||||
|
||||
[[ui-zoom-text-only]]
|
||||
=== zoom-text-only
|
||||
Whether the zoom factor on a frame applies only to the text or to all content.
|
||||
@@ -637,6 +625,7 @@ The format to use for the window title. The following placeholders are defined:
|
||||
* `{title_sep}`: The string ` - ` if a title is set, empty otherwise.
|
||||
* `{id}`: The internal window ID of this window.
|
||||
* `{scroll_pos}`: The page scroll position.
|
||||
* `{host}`: The host of the current web page.
|
||||
|
||||
Default: +pass:[{perc}{title}{title_sep}qutebrowser]+
|
||||
|
||||
@@ -1153,6 +1142,7 @@ The format to use for the tab title. The following placeholders are defined:
|
||||
* `{index}`: The index of this tab.
|
||||
* `{id}`: The internal tab ID of this tab.
|
||||
* `{scroll_pos}`: The page scroll position.
|
||||
* `{host}`: The host of the current web page.
|
||||
|
||||
Default: +pass:[{index}: {title}]+
|
||||
|
||||
@@ -1664,7 +1654,7 @@ Colors used in the UI.
|
||||
A value can be in one of the following format:
|
||||
|
||||
* `#RGB`/`#RRGGBB`/`#RRRGGGBBB`/`#RRRRGGGGBBBB`
|
||||
* A SVG color name as specified in http://www.w3.org/TR/SVG/types.html#ColorKeywords[the W3C specification].
|
||||
* An SVG color name as specified in http://www.w3.org/TR/SVG/types.html#ColorKeywords[the W3C specification].
|
||||
* transparent (no color)
|
||||
* `rgb(r, g, b)` / `rgba(r, g, b, a)` (values 0-255 or percentages)
|
||||
* `hsv(h, s, v)` / `hsva(h, s, v, a)` (values 0-255, hue 0-359)
|
||||
|
||||
@@ -65,6 +65,9 @@ show it.
|
||||
*--target* '{auto,tab,tab-bg,tab-silent,tab-bg-silent,window}'::
|
||||
How URLs should be opened if there is already a qutebrowser instance running.
|
||||
|
||||
*--backend* '{webkit,webengine}'::
|
||||
Which backend to use (webengine backend is EXPERIMENTAL!).
|
||||
|
||||
=== debug arguments
|
||||
*-l* '{critical,error,warning,info,debug,vdebug}', *--loglevel* '{critical,error,warning,info,debug,vdebug}'::
|
||||
Set loglevel
|
||||
|
||||
@@ -50,7 +50,7 @@ $ git checkout symbols
|
||||
$ export DEBUG_CFLAGS='-ggdb3 -fvar-tracking-assignments -Og'
|
||||
$ export DEBUG_CXXFLAGS='-ggdb3 -fvar-tracking-assignments -Og'
|
||||
$ cd qt5
|
||||
$ makepkg -si --pkg qt5-base-debug,qt5-webkit-debug
|
||||
$ makepkg -si --pkg qt5-base-debug,qt5-webkit-debug,qt5-webengine-debug
|
||||
$ cd ../pyqt5
|
||||
$ makepkg -si --pkg pyqt5-common-debug,python-pyqt5-debug
|
||||
----
|
||||
@@ -76,7 +76,7 @@ Server = http://qutebrowser.org/qt-debug/$arch
|
||||
Then install the packages:
|
||||
|
||||
----
|
||||
# pacman -Suy pyqt5-common-debug python-pyqt5-debug qt5-base-debug qt5-webkit-debug
|
||||
# pacman -Suy pyqt5-common-debug python-pyqt5-debug qt5-base-debug qt5-webkit-debug,qt5-webengine-debug
|
||||
----
|
||||
|
||||
The `-debug` packages conflict with the non-debug variants - it's safe to
|
||||
|
||||
@@ -33,6 +33,9 @@ The following environment variables will be set when a userscript is launched:
|
||||
- `QUTE_FIFO`: The FIFO or file to write commands to.
|
||||
- `QUTE_HTML`: Path of a file containing the HTML source of the current page.
|
||||
- `QUTE_TEXT`: Path of a file containing the plaintext of the current page.
|
||||
- `QUTE_CONFIG_DIR`: Path of the directory containing qutebrowser's configuration.
|
||||
- `QUTE_DATA_DIR`: Path of the directory containing qutebrowser's data.
|
||||
- `QUTE_DOWNLOAD_DIR`: Path of the downloads directory.
|
||||
|
||||
In `command` mode:
|
||||
|
||||
|
||||
@@ -1,29 +1,26 @@
|
||||
# This file is automatically generated by scripts/dev/recompile_requirements.py
|
||||
|
||||
ebb-lint==0.4.4
|
||||
flake8==2.5.4
|
||||
flake8-copyright==0.1
|
||||
flake8==2.6.2 # rq.filter: < 3.0.0
|
||||
flake8-copyright==0.2.0
|
||||
flake8-debugger==1.4.0
|
||||
flake8-deprecated==1.0
|
||||
flake8-docstrings==0.2.6
|
||||
flake8-future-import==0.4.1
|
||||
flake8-docstrings==0.2.8
|
||||
flake8-future-import==0.4.3
|
||||
flake8-mock==0.2
|
||||
flake8-pep3101==0.3
|
||||
flake8-putty==0.3.2
|
||||
flake8-pep3101==0.4
|
||||
flake8-putty==0.4.0
|
||||
flake8-string-format==0.2.2
|
||||
flake8-tidy-imports==1.0.0
|
||||
flake8-tuple==0.2.9
|
||||
flake8-tidy-imports==1.0.2
|
||||
flake8-tuple==0.2.12
|
||||
hacking==0.11.0
|
||||
intervaltree==2.1.0
|
||||
mccabe==0.5.0
|
||||
packaging==16.7
|
||||
pbr==1.10.0
|
||||
pep257==0.7.0 # still needed by flake8-docstrings but ignored
|
||||
pep8==1.7.0
|
||||
pep8-naming==0.3.3
|
||||
pep8-naming==0.4.1
|
||||
pycodestyle==2.0.0
|
||||
pydocstyle==1.0.0
|
||||
pyflakes==1.2.3
|
||||
pyparsing==2.1.4
|
||||
pyparsing==2.1.5
|
||||
six==1.10.0
|
||||
sortedcontainers==1.5.3
|
||||
venusian==1.0
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
ebb-lint
|
||||
flake8
|
||||
flake8<3.0.0
|
||||
flake8-copyright
|
||||
flake8-debugger
|
||||
flake8-deprecated
|
||||
@@ -16,6 +15,9 @@ pep8-naming
|
||||
pydocstyle
|
||||
pyflakes
|
||||
|
||||
mccabe==0.5.0
|
||||
pep8==1.7.0
|
||||
|
||||
#@ comment: pep257 still needed by flake8-docstrings but ignored
|
||||
|
||||
# Waiting until hacking/flake8-tuple are updated
|
||||
#@ filter: flake8 < 3.0.0
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
# This file is automatically generated by scripts/dev/recompile_requirements.py
|
||||
|
||||
astroid==1.4.6
|
||||
colorama==0.3.7
|
||||
astroid==1.4.7
|
||||
isort==4.2.5
|
||||
lazy-object-proxy==1.2.2
|
||||
pylint==1.5.6
|
||||
mccabe==0.5.0
|
||||
pylint==1.6.4
|
||||
./scripts/dev/pylint_checkers
|
||||
requests==2.10.0
|
||||
six==1.10.0
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
# This file is automatically generated by scripts/dev/recompile_requirements.py
|
||||
|
||||
beautifulsoup4==4.4.1
|
||||
CherryPy==6.0.1
|
||||
beautifulsoup4==4.5.0
|
||||
CherryPy==7.1.0
|
||||
coverage==4.1
|
||||
decorator==4.0.10
|
||||
Flask==0.10.1 # rq.filter: < 0.11.0
|
||||
glob2==0.4.1
|
||||
httpbin==0.4.1
|
||||
hypothesis==3.4.0
|
||||
hypothesis==3.4.2
|
||||
itsdangerous==0.24
|
||||
# Jinja2==2.8
|
||||
Mako==1.0.4
|
||||
@@ -16,17 +16,17 @@ parse==1.6.6
|
||||
parse-type==0.3.4
|
||||
py==1.4.31
|
||||
pytest==2.9.2
|
||||
pytest-bdd==2.16.1
|
||||
pytest-bdd==2.17.0
|
||||
pytest-catchlog==1.2.2
|
||||
pytest-cov==2.2.1
|
||||
pytest-cov==2.3.0
|
||||
pytest-faulthandler==1.3.0
|
||||
pytest-instafail==0.3.0
|
||||
pytest-mock==1.1
|
||||
pytest-qt==1.11.0
|
||||
pytest-repeat==0.2
|
||||
pytest-repeat==0.3.0
|
||||
pytest-rerunfailures==2.0.0
|
||||
pytest-travis-fold==1.2.0
|
||||
pytest-xvfb==0.2.0
|
||||
six==1.10.0
|
||||
vulture==0.8.1
|
||||
vulture==0.10
|
||||
Werkzeug==0.11.10
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
# This file is automatically generated by scripts/dev/recompile_requirements.py
|
||||
|
||||
vulture==0.8.1
|
||||
vulture==0.10
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/bin/bash
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Copyright 2015 Zach-Button <zachrey.button@gmail.com>
|
||||
#
|
||||
@@ -39,10 +39,9 @@
|
||||
|
||||
[ -z "$QUTE_URL" ] && QUTE_URL='http://google.com'
|
||||
|
||||
url=$(echo "$QUTE_URL" | cat - ~/.config/qutebrowser/quickmarks ~/.local/share/qutebrowser/history | dmenu -l 15 -p qutebrowser)
|
||||
url=$(echo $url | sed -E 's/[^ ]+ +//g' | egrep "https?:" || echo $url)
|
||||
url=$(echo "$QUTE_URL" | cat - "$QUTE_CONFIG_DIR/quickmarks" "$QUTE_DATA_DIR/history" | dmenu -l 15 -p qutebrowser)
|
||||
url=$(echo "$url" | sed -E 's/[^ ]+ +//g' | egrep "https?:" || echo "$url")
|
||||
|
||||
[ -z "${url// }" ] && exit
|
||||
|
||||
echo "open $url" >> "$QUTE_FIFO" || qutebrowser "$url"
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/bin/bash -e
|
||||
#!/usr/bin/env bash
|
||||
# Both standalone script and qutebrowser userscript that opens a rofi menu with
|
||||
# all files from the download director and opens the selected file. It works
|
||||
# both as a userscript and a standalone script that is called from outside of
|
||||
@@ -18,7 +18,10 @@
|
||||
# Thorsten Wißmann, 2015 (thorsten` on freenode)
|
||||
# Any feedback is welcome!
|
||||
|
||||
set -e
|
||||
|
||||
# open a file from the download directory using rofi
|
||||
DOWNLOAD_DIR=${DOWNLOAD_DIR:-$QUTE_DOWNLOAD_DIR}
|
||||
DOWNLOAD_DIR=${DOWNLOAD_DIR:-$HOME/Downloads}
|
||||
# the name of the rofi command
|
||||
ROFI_CMD=${ROFI_CMD:-rofi}
|
||||
@@ -49,7 +52,7 @@ die() {
|
||||
if ! [ -d "$DOWNLOAD_DIR" ] ; then
|
||||
die "Download directory »$DOWNLOAD_DIR« not found!"
|
||||
fi
|
||||
if ! $(which "${ROFI_CMD}" > /dev/null ) ; then
|
||||
if ! which "${ROFI_CMD}" > /dev/null ; then
|
||||
die "Rofi command »${ROFI_CMD}« not found in PATH!"
|
||||
fi
|
||||
|
||||
@@ -109,5 +112,3 @@ fi
|
||||
msg info "Opening »$file« (of type $filetype) with ${application%.desktop}"
|
||||
|
||||
xdg-open "$path" &
|
||||
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/bin/bash -e
|
||||
#!/usr/bin/env bash
|
||||
help() {
|
||||
blink=$'\e[1;31m' reset=$'\e[0m'
|
||||
cat <<EOF
|
||||
@@ -39,6 +39,7 @@ Configuration:
|
||||
EOF
|
||||
}
|
||||
|
||||
set -o errexit
|
||||
set -o pipefail
|
||||
shopt -s nocasematch # make regexp matching in bash case insensitive
|
||||
|
||||
@@ -61,7 +62,7 @@ die() {
|
||||
}
|
||||
|
||||
javascript_escape() {
|
||||
# print the first argument in a escaped way, such that it can safely
|
||||
# print the first argument in an escaped way, such that it can safely
|
||||
# be used within javascripts double quotes
|
||||
sed "s,[\\\'\"],\\\&,g" <<< "$1"
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/bin/bash
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Copyright 2015 Zach-Button <zachrey.button@gmail.com>
|
||||
#
|
||||
@@ -17,7 +17,7 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
#
|
||||
#
|
||||
# This script fetches the unprocessed HTML source for a page and opens it in vim.
|
||||
# :bind gf spawn --userscript qutebrowser_viewsource
|
||||
#
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
# If you would like to set a custom colorscheme/font use these dirs.
|
||||
# https://github.com/halfwit/dotfiles/blob/master/.config/dmenu/bemenucolors
|
||||
readonly confdir=${XDG_CONFIG_HOME:-$HOME/.config}
|
||||
readonly datadir=${XDG_DATA_HOME:-$HOME/.local/share}
|
||||
|
||||
readonly optsfile=$confdir/dmenu/bemenucolors
|
||||
|
||||
@@ -15,17 +14,17 @@ create_menu() {
|
||||
# Check quickmarks
|
||||
while read -r url; do
|
||||
printf -- '%s\n' "$url"
|
||||
done < "$confdir"/qutebrowser/quickmarks
|
||||
done < "$QUTE_CONFIG_DIR"/quickmarks
|
||||
|
||||
# Next bookmarks
|
||||
while read -r url _; do
|
||||
printf -- '%s\n' "$url"
|
||||
done < "$confdir"/qutebrowser/bookmarks/urls
|
||||
done < "$QUTE_CONFIG_DIR"/bookmarks/urls
|
||||
|
||||
# Finally history
|
||||
while read -r _ url; do
|
||||
printf -- '%s\n' "$url"
|
||||
done < "$datadir"/qutebrowser/history
|
||||
done < "$QUTE_DATA_DIR"/history
|
||||
}
|
||||
|
||||
get_selection() {
|
||||
|
||||
36
misc/userscripts/taskadd
Executable file
36
misc/userscripts/taskadd
Executable file
@@ -0,0 +1,36 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# Behavior:
|
||||
# Userscript for qutebrowser which adds a task to taskwarrior.
|
||||
# If run as a command (:spawn --userscript taskadd), it creates a new task
|
||||
# with the description equal to the current page title and annotates it with
|
||||
# the current page url. Additional arguments are passed along so you can add
|
||||
# mods to the task (e.g. priority, due date, tags).
|
||||
#
|
||||
# Example:
|
||||
# :spawn --userscript taskadd due:eod pri:H
|
||||
#
|
||||
# To enable passing along extra args, I suggest using a mapping like:
|
||||
# :bind <somekey> set-cmd-text -s :spawn --userscript taskadd
|
||||
#
|
||||
# If run from hint mode, it uses the selected hint text as the description
|
||||
# and the selected hint url as the annotation.
|
||||
#
|
||||
# Ryan Roden-Corrent (rcorre), 2016
|
||||
# Any feedback is welcome!
|
||||
#
|
||||
# For more info on Taskwarrior, see http://taskwarrior.org/
|
||||
|
||||
# use either the current page title or the hint text as the task description
|
||||
[[ $QUTE_MODE == 'hints' ]] && title=$QUTE_SELECTED_TEXT || title=$QUTE_TITLE
|
||||
|
||||
# try to add the task and grab the output
|
||||
msg="$(task add $title $@ 2>&1)"
|
||||
|
||||
if [[ $? == 0 ]]; 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
|
||||
else
|
||||
echo "message-error '$msg'" >> $QUTE_FIFO
|
||||
fi
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/bin/bash -e
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# Behavior:
|
||||
# Userscript for qutebrowser which views the current web page in mpv using
|
||||
@@ -24,6 +24,8 @@
|
||||
# Thorsten Wißmann, 2015 (thorsten` on freenode)
|
||||
# Any feedback is welcome!
|
||||
|
||||
set -e
|
||||
|
||||
if [ -z "$QUTE_FIFO" ] ; then
|
||||
cat 1>&2 <<EOF
|
||||
Error: $0 can not be run as a standalone script.
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
[pytest]
|
||||
norecursedirs = .tox .venv
|
||||
addopts = --strict -rfEw --faulthandler-timeout=70 --instafail
|
||||
markers =
|
||||
gui: Tests using the GUI (e.g. spawning widgets)
|
||||
@@ -13,7 +12,6 @@ markers =
|
||||
frozen: Tests which can only be run if sys.frozen is True.
|
||||
integration: Tests which test a bigger portion of code
|
||||
end2end: End to end tests which run qutebrowser as subprocess
|
||||
pyqt531_or_newer: Needs PyQt 5.3.1 or newer.
|
||||
xfail_norun: xfail the test with out running it
|
||||
ci: Tests which should only run on CI.
|
||||
flaky_once: Try to rerun this test once if it fails
|
||||
@@ -37,6 +35,6 @@ qt_log_ignore =
|
||||
^Geoclue error: Process org\.freedesktop\.Geoclue\.Master exited with status 127
|
||||
^QObject::connect: Cannot connect \(null\)::stateChanged\(QNetworkSession::State\) to QNetworkReplyHttpImpl::_q_networkSessionStateChanged\(QNetworkSession::State\)
|
||||
^QXcbClipboard: Cannot transfer data, no data available
|
||||
^Image of format '' blocked because it is not considered safe. If you are sure it is safe to do so, you can white-list the format by setting the environment variable QTWEBKIT_IMAGEFORMAT_WHITELIST=
|
||||
^load glyph failed
|
||||
qt_wait_signal_raising = true
|
||||
xfail_strict = true
|
||||
|
||||
@@ -28,7 +28,7 @@ __copyright__ = "Copyright 2014-2016 Florian Bruhin (The Compiler)"
|
||||
__license__ = "GPL"
|
||||
__maintainer__ = __author__
|
||||
__email__ = "mail@qutebrowser.org"
|
||||
__version_info__ = (0, 7, 0)
|
||||
__version_info__ = (0, 8, 0)
|
||||
__version__ = '.'.join(str(e) for e in __version_info__)
|
||||
__description__ = "A keyboard-driven, vim-like browser based on PyQt5 and QtWebKit."
|
||||
|
||||
|
||||
@@ -46,8 +46,10 @@ import qutebrowser.resources
|
||||
from qutebrowser.completion.models import instances as completionmodels
|
||||
from qutebrowser.commands import cmdutils, runners, cmdexc
|
||||
from qutebrowser.config import style, config, websettings, configexc
|
||||
from qutebrowser.browser import urlmarks, cookies, cache, adblock, history
|
||||
from qutebrowser.browser.network import qutescheme, proxy, networkmanager
|
||||
from qutebrowser.browser import urlmarks, adblock
|
||||
from qutebrowser.browser.webkit import cookies, cache, history, downloads
|
||||
from qutebrowser.browser.webkit.network import (qutescheme, proxy,
|
||||
networkmanager)
|
||||
from qutebrowser.mainwindow import mainwindow
|
||||
from qutebrowser.misc import readline, ipc, savemanager, sessions, crashsignal
|
||||
from qutebrowser.misc import utilcmds # pylint: disable=unused-import
|
||||
@@ -62,12 +64,7 @@ qApp = None
|
||||
def run(args):
|
||||
"""Initialize everything and run the application."""
|
||||
if args.version:
|
||||
print(version.version(short=True))
|
||||
print()
|
||||
print()
|
||||
print(qutebrowser.__copyright__)
|
||||
print()
|
||||
print(version.GPL_BOILERPLATE.strip())
|
||||
print(version.version())
|
||||
sys.exit(usertypes.Exit.ok)
|
||||
|
||||
if args.temp_basedir:
|
||||
@@ -166,7 +163,7 @@ def _init_icon():
|
||||
"""Initialize the icon of qutebrowser."""
|
||||
icon = QIcon()
|
||||
fallback_icon = QIcon()
|
||||
for size in (16, 24, 32, 48, 64, 96, 128, 256, 512):
|
||||
for size in [16, 24, 32, 48, 64, 96, 128, 256, 512]:
|
||||
filename = ':/icons/qutebrowser-{}x{}.png'.format(size, size)
|
||||
pixmap = QPixmap(filename)
|
||||
qtutils.ensure_not_null(pixmap)
|
||||
@@ -278,7 +275,7 @@ def process_pos_args(args, via_ipc=False, cwd=None, target_arg=None):
|
||||
message.error('current', "Error in startup argument '{}': "
|
||||
"{}".format(cmd, e))
|
||||
else:
|
||||
background = open_target in ('tab-bg', 'tab-bg-silent')
|
||||
background = open_target in ['tab-bg', 'tab-bg-silent']
|
||||
tabbed_browser.tabopen(url, background=background,
|
||||
explicit=True)
|
||||
|
||||
@@ -340,7 +337,6 @@ def _save_version():
|
||||
state_config['general']['version'] = qutebrowser.__version__
|
||||
|
||||
|
||||
@pyqtSlot('QWidget*', 'QWidget*')
|
||||
def on_focus_changed(_old, new):
|
||||
"""Register currently focused main window in the object registry."""
|
||||
if not isinstance(new, QWidget):
|
||||
@@ -358,9 +354,8 @@ def on_focus_changed(_old, new):
|
||||
_maybe_hide_mouse_cursor()
|
||||
|
||||
|
||||
@pyqtSlot(QUrl)
|
||||
def open_desktopservices_url(url):
|
||||
"""Handler to open an URL via QDesktopServices."""
|
||||
"""Handler to open a URL via QDesktopServices."""
|
||||
win_id = mainwindow.get_window(via_ipc=True, force_window=False)
|
||||
tabbed_browser = objreg.get('tabbed-browser', scope='window',
|
||||
window=win_id)
|
||||
@@ -441,6 +436,8 @@ def _init_modules(args, crash_handler):
|
||||
os.environ.pop('QT_WAYLAND_DISABLE_WINDOWDECORATION', None)
|
||||
_maybe_hide_mouse_cursor()
|
||||
objreg.get('config').changed.connect(_maybe_hide_mouse_cursor)
|
||||
temp_downloads = downloads.TempDownloadManager(qApp)
|
||||
objreg.register('temporary-downloads', temp_downloads)
|
||||
|
||||
|
||||
def _init_late_modules(args):
|
||||
@@ -479,7 +476,6 @@ class Quitter:
|
||||
self._shutting_down = False
|
||||
self._args = args
|
||||
|
||||
@pyqtSlot()
|
||||
def on_last_window_closed(self):
|
||||
"""Slot which gets invoked when the last window was closed."""
|
||||
self.shutdown(last_window=True)
|
||||
@@ -494,7 +490,7 @@ class Quitter:
|
||||
else:
|
||||
path = os.path.abspath(os.path.dirname(qutebrowser.__file__))
|
||||
if not os.path.isdir(path):
|
||||
# Probably running from an python egg.
|
||||
# Probably running from a python egg.
|
||||
return
|
||||
|
||||
for dirpath, _dirnames, filenames in os.walk(path):
|
||||
@@ -527,7 +523,7 @@ class Quitter:
|
||||
cwd = os.path.join(os.path.abspath(os.path.dirname(
|
||||
qutebrowser.__file__)), '..')
|
||||
if not os.path.isdir(cwd):
|
||||
# Probably running from an python egg. Let's fallback to
|
||||
# Probably running from a python egg. Let's fallback to
|
||||
# cwd=None and see if that works out.
|
||||
# See https://github.com/The-Compiler/qutebrowser/issues/323
|
||||
cwd = None
|
||||
@@ -714,6 +710,8 @@ class Quitter:
|
||||
not restart):
|
||||
atexit.register(shutil.rmtree, self._args.basedir,
|
||||
ignore_errors=True)
|
||||
# Delete temp download dir
|
||||
objreg.get('temporary-downloads').cleanup()
|
||||
# If we don't kill our custom handler here we might get segfaults
|
||||
log.destroy.debug("Deactivating message handler...")
|
||||
qInstallMessageHandler(None)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
|
||||
|
||||
# Copyright 2014-2016 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
|
||||
# Copyright 2016 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
|
||||
#
|
||||
# This file is part of qutebrowser.
|
||||
#
|
||||
|
||||
@@ -27,7 +27,7 @@ import zipfile
|
||||
import fnmatch
|
||||
|
||||
from qutebrowser.config import config
|
||||
from qutebrowser.utils import objreg, standarddir, log, message
|
||||
from qutebrowser.utils import objreg, standarddir, log, message, usertypes
|
||||
from qutebrowser.commands import cmdutils, cmdexc
|
||||
|
||||
|
||||
@@ -48,7 +48,7 @@ def guess_zip_filename(zf):
|
||||
|
||||
|
||||
def get_fileobj(byte_io):
|
||||
"""Get an usable file object to read the hosts file from."""
|
||||
"""Get a usable file object to read the hosts file from."""
|
||||
byte_io.seek(0) # rewind downloaded file
|
||||
if zipfile.is_zipfile(byte_io):
|
||||
byte_io.seek(0) # rewind what zipfile.is_zipfile did
|
||||
@@ -210,7 +210,8 @@ class HostBlocker:
|
||||
else:
|
||||
fobj = io.BytesIO()
|
||||
fobj.name = 'adblock: ' + url.host()
|
||||
download = download_manager.get(url, fileobj=fobj,
|
||||
target = usertypes.FileObjDownloadTarget(fobj)
|
||||
download = download_manager.get(url, target=target,
|
||||
auto_remove=True)
|
||||
self._in_progress.append(download)
|
||||
download.finished.connect(
|
||||
|
||||
641
qutebrowser/browser/browsertab.py
Normal file
641
qutebrowser/browser/browsertab.py
Normal file
@@ -0,0 +1,641 @@
|
||||
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
|
||||
|
||||
# Copyright 2016 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
|
||||
#
|
||||
# This file is part of qutebrowser.
|
||||
#
|
||||
# qutebrowser is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# qutebrowser is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""Base class for a wrapper over QWebView/QWebEngineView."""
|
||||
|
||||
import itertools
|
||||
|
||||
from PyQt5.QtCore import pyqtSignal, pyqtSlot, QUrl, QObject, QPoint
|
||||
from PyQt5.QtGui import QIcon
|
||||
from PyQt5.QtWidgets import QWidget, QLayout
|
||||
|
||||
from qutebrowser.keyinput import modeman
|
||||
from qutebrowser.config import config
|
||||
from qutebrowser.utils import utils, objreg, usertypes, message, log, qtutils
|
||||
|
||||
|
||||
tab_id_gen = itertools.count(0)
|
||||
|
||||
|
||||
def create(win_id, parent=None):
|
||||
"""Get a QtWebKit/QtWebEngine tab object.
|
||||
|
||||
Args:
|
||||
win_id: The window ID where the tab will be shown.
|
||||
parent: The Qt parent to set.
|
||||
"""
|
||||
# Importing modules here so we don't depend on QtWebEngine without the
|
||||
# argument and to avoid circular imports.
|
||||
mode_manager = modeman.instance(win_id)
|
||||
if objreg.get('args').backend == 'webengine':
|
||||
from qutebrowser.browser.webengine import webenginetab
|
||||
tab_class = webenginetab.WebEngineTab
|
||||
else:
|
||||
from qutebrowser.browser.webkit import webkittab
|
||||
tab_class = webkittab.WebKitTab
|
||||
return tab_class(win_id=win_id, mode_manager=mode_manager, parent=parent)
|
||||
|
||||
|
||||
class WebTabError(Exception):
|
||||
|
||||
"""Base class for various errors."""
|
||||
|
||||
|
||||
class WrapperLayout(QLayout):
|
||||
|
||||
"""A Qt layout which simply wraps a single widget.
|
||||
|
||||
This is used so the widget is hidden behind a AbstractTab API and can't
|
||||
easily be accidentally accessed.
|
||||
"""
|
||||
|
||||
def __init__(self, widget, parent=None):
|
||||
super().__init__(parent)
|
||||
self._widget = widget
|
||||
|
||||
def addItem(self, _widget):
|
||||
raise AssertionError("Should never be called!")
|
||||
|
||||
def sizeHint(self):
|
||||
return self._widget.sizeHint()
|
||||
|
||||
def itemAt(self, _index): # pragma: no cover
|
||||
# For some reason this sometimes gets called by Qt.
|
||||
return None
|
||||
|
||||
def takeAt(self, _index):
|
||||
raise AssertionError("Should never be called!")
|
||||
|
||||
def setGeometry(self, rect):
|
||||
self._widget.setGeometry(rect)
|
||||
|
||||
|
||||
class TabData:
|
||||
|
||||
"""A simple namespace with a fixed set of attributes.
|
||||
|
||||
Attributes:
|
||||
keep_icon: Whether the (e.g. cloned) icon should not be cleared on page
|
||||
load.
|
||||
inspector: The QWebInspector used for this webview.
|
||||
viewing_source: Set if we're currently showing a source view.
|
||||
"""
|
||||
|
||||
__slots__ = ['keep_icon', 'viewing_source', 'inspector']
|
||||
|
||||
def __init__(self):
|
||||
self.keep_icon = False
|
||||
self.viewing_source = False
|
||||
self.inspector = None
|
||||
|
||||
|
||||
class AbstractPrinting:
|
||||
|
||||
"""Attribute of AbstractTab for printing the page."""
|
||||
|
||||
def __init__(self):
|
||||
self._widget = None
|
||||
|
||||
def check_pdf_support(self):
|
||||
raise NotImplementedError
|
||||
|
||||
def check_printer_support(self):
|
||||
raise NotImplementedError
|
||||
|
||||
def to_pdf(self, filename):
|
||||
raise NotImplementedError
|
||||
|
||||
def to_printer(self, printer):
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class AbstractSearch(QObject):
|
||||
|
||||
"""Attribute of AbstractTab for doing searches.
|
||||
|
||||
Attributes:
|
||||
text: The last thing this view was searched for.
|
||||
_flags: The flags of the last search (needs to be set by subclasses).
|
||||
_widget: The underlying WebView widget.
|
||||
"""
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self._widget = None
|
||||
self.text = None
|
||||
|
||||
def search(self, text, *, ignore_case=False, reverse=False,
|
||||
result_cb=None):
|
||||
"""Find the given text on the page.
|
||||
|
||||
Args:
|
||||
text: The text to search for.
|
||||
ignore_case: Search case-insensitively. (True/False/'smart')
|
||||
reverse: Reverse search direction.
|
||||
result_cb: Called with a bool indicating whether a match was found.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def clear(self):
|
||||
"""Clear the current search."""
|
||||
raise NotImplementedError
|
||||
|
||||
def prev_result(self, *, result_cb=None):
|
||||
"""Go to the previous result of the current search.
|
||||
|
||||
Args:
|
||||
result_cb: Called with a bool indicating whether a match was found.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def next_result(self, *, result_cb=None):
|
||||
"""Go to the next result of the current search.
|
||||
|
||||
Args:
|
||||
result_cb: Called with a bool indicating whether a match was found.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class AbstractZoom(QObject):
|
||||
|
||||
"""Attribute of AbstractTab for controlling zoom.
|
||||
|
||||
Attributes:
|
||||
_neighborlist: A NeighborList with the zoom levels.
|
||||
_default_zoom_changed: Whether the zoom was changed from the default.
|
||||
"""
|
||||
|
||||
def __init__(self, win_id, parent=None):
|
||||
super().__init__(parent)
|
||||
self._widget = None
|
||||
self._win_id = win_id
|
||||
self._default_zoom_changed = False
|
||||
self._init_neighborlist()
|
||||
objreg.get('config').changed.connect(self._on_config_changed)
|
||||
|
||||
# # FIXME:qtwebengine is this needed?
|
||||
# # For some reason, this signal doesn't get disconnected automatically
|
||||
# # when the WebView is destroyed on older PyQt versions.
|
||||
# # See https://github.com/The-Compiler/qutebrowser/issues/390
|
||||
# self.destroyed.connect(functools.partial(
|
||||
# cfg.changed.disconnect, self.init_neighborlist))
|
||||
|
||||
@pyqtSlot(str, str)
|
||||
def _on_config_changed(self, section, option):
|
||||
if section == 'ui' and option in ['zoom-levels', 'default-zoom']:
|
||||
if not self._default_zoom_changed:
|
||||
factor = float(config.get('ui', 'default-zoom')) / 100
|
||||
self._set_factor_internal(factor)
|
||||
self._default_zoom_changed = False
|
||||
self._init_neighborlist()
|
||||
|
||||
def _init_neighborlist(self):
|
||||
"""Initialize self._neighborlist."""
|
||||
levels = config.get('ui', 'zoom-levels')
|
||||
self._neighborlist = usertypes.NeighborList(
|
||||
levels, mode=usertypes.NeighborList.Modes.edge)
|
||||
self._neighborlist.fuzzyval = config.get('ui', 'default-zoom')
|
||||
|
||||
def offset(self, offset):
|
||||
"""Increase/Decrease the zoom level by the given offset.
|
||||
|
||||
Args:
|
||||
offset: The offset in the zoom level list.
|
||||
|
||||
Return:
|
||||
The new zoom percentage.
|
||||
"""
|
||||
level = self._neighborlist.getitem(offset)
|
||||
self.set_factor(float(level) / 100, fuzzyval=False)
|
||||
return level
|
||||
|
||||
def set_factor(self, factor, *, fuzzyval=True):
|
||||
"""Zoom to a given zoom factor.
|
||||
|
||||
Args:
|
||||
factor: The zoom factor as float.
|
||||
fuzzyval: Whether to set the NeighborLists fuzzyval.
|
||||
"""
|
||||
if fuzzyval:
|
||||
self._neighborlist.fuzzyval = int(factor * 100)
|
||||
if factor < 0:
|
||||
raise ValueError("Can't zoom to factor {}!".format(factor))
|
||||
self._default_zoom_changed = True
|
||||
self._set_factor_internal(factor)
|
||||
|
||||
def factor(self):
|
||||
raise NotImplementedError
|
||||
|
||||
def set_default(self):
|
||||
default_zoom = config.get('ui', 'default-zoom')
|
||||
self._set_factor_internal(float(default_zoom) / 100)
|
||||
|
||||
@pyqtSlot(QPoint)
|
||||
def _on_mouse_wheel_zoom(self, delta):
|
||||
"""Handle zooming via mousewheel requested by the web view."""
|
||||
divider = config.get('input', 'mouse-zoom-divider')
|
||||
factor = self.factor() + delta.y() / divider
|
||||
if factor < 0:
|
||||
return
|
||||
perc = int(100 * factor)
|
||||
message.info(self._win_id, "Zoom level: {}%".format(perc))
|
||||
self._neighborlist.fuzzyval = perc
|
||||
self._set_factor_internal(factor)
|
||||
self._default_zoom_changed = True
|
||||
|
||||
|
||||
class AbstractCaret(QObject):
|
||||
|
||||
"""Attribute of AbstractTab for caret browsing."""
|
||||
|
||||
def __init__(self, win_id, tab, mode_manager, parent=None):
|
||||
super().__init__(parent)
|
||||
self._tab = tab
|
||||
self._win_id = win_id
|
||||
self._widget = None
|
||||
self.selection_enabled = False
|
||||
mode_manager.entered.connect(self._on_mode_entered)
|
||||
mode_manager.left.connect(self._on_mode_left)
|
||||
|
||||
def _on_mode_entered(self, mode):
|
||||
raise NotImplementedError
|
||||
|
||||
def _on_mode_left(self):
|
||||
raise NotImplementedError
|
||||
|
||||
def move_to_next_line(self, count=1):
|
||||
raise NotImplementedError
|
||||
|
||||
def move_to_prev_line(self, count=1):
|
||||
raise NotImplementedError
|
||||
|
||||
def move_to_next_char(self, count=1):
|
||||
raise NotImplementedError
|
||||
|
||||
def move_to_prev_char(self, count=1):
|
||||
raise NotImplementedError
|
||||
|
||||
def move_to_end_of_word(self, count=1):
|
||||
raise NotImplementedError
|
||||
|
||||
def move_to_next_word(self, count=1):
|
||||
raise NotImplementedError
|
||||
|
||||
def move_to_prev_word(self, count=1):
|
||||
raise NotImplementedError
|
||||
|
||||
def move_to_start_of_line(self):
|
||||
raise NotImplementedError
|
||||
|
||||
def move_to_end_of_line(self):
|
||||
raise NotImplementedError
|
||||
|
||||
def move_to_start_of_next_block(self, count=1):
|
||||
raise NotImplementedError
|
||||
|
||||
def move_to_start_of_prev_block(self, count=1):
|
||||
raise NotImplementedError
|
||||
|
||||
def move_to_end_of_next_block(self, count=1):
|
||||
raise NotImplementedError
|
||||
|
||||
def move_to_end_of_prev_block(self, count=1):
|
||||
raise NotImplementedError
|
||||
|
||||
def move_to_start_of_document(self):
|
||||
raise NotImplementedError
|
||||
|
||||
def move_to_end_of_document(self):
|
||||
raise NotImplementedError
|
||||
|
||||
def toggle_selection(self):
|
||||
raise NotImplementedError
|
||||
|
||||
def drop_selection(self):
|
||||
raise NotImplementedError
|
||||
|
||||
def has_selection(self):
|
||||
raise NotImplementedError
|
||||
|
||||
def selection(self, html=False):
|
||||
raise NotImplementedError
|
||||
|
||||
def follow_selected(self, *, tab=False):
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class AbstractScroller(QObject):
|
||||
|
||||
"""Attribute of AbstractTab to manage scroll position."""
|
||||
|
||||
perc_changed = pyqtSignal(int, int)
|
||||
|
||||
def __init__(self, tab, parent=None):
|
||||
super().__init__(parent)
|
||||
self._tab = tab
|
||||
self._widget = None
|
||||
|
||||
def _init_widget(self, widget):
|
||||
self._widget = widget
|
||||
|
||||
def pos_px(self):
|
||||
raise NotImplementedError
|
||||
|
||||
def pos_perc(self):
|
||||
raise NotImplementedError
|
||||
|
||||
def to_perc(self, x=None, y=None):
|
||||
raise NotImplementedError
|
||||
|
||||
def to_point(self, point):
|
||||
raise NotImplementedError
|
||||
|
||||
def delta(self, x=0, y=0):
|
||||
raise NotImplementedError
|
||||
|
||||
def delta_page(self, x=0, y=0):
|
||||
raise NotImplementedError
|
||||
|
||||
def up(self, count=1):
|
||||
raise NotImplementedError
|
||||
|
||||
def down(self, count=1):
|
||||
raise NotImplementedError
|
||||
|
||||
def left(self, count=1):
|
||||
raise NotImplementedError
|
||||
|
||||
def right(self, count=1):
|
||||
raise NotImplementedError
|
||||
|
||||
def top(self):
|
||||
raise NotImplementedError
|
||||
|
||||
def bottom(self):
|
||||
raise NotImplementedError
|
||||
|
||||
def page_up(self, count=1):
|
||||
raise NotImplementedError
|
||||
|
||||
def page_down(self, count=1):
|
||||
raise NotImplementedError
|
||||
|
||||
def at_top(self):
|
||||
raise NotImplementedError
|
||||
|
||||
def at_bottom(self):
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class AbstractHistory:
|
||||
|
||||
"""The history attribute of a AbstractTab."""
|
||||
|
||||
def __init__(self, tab):
|
||||
self._tab = tab
|
||||
self._history = None
|
||||
|
||||
def __len__(self):
|
||||
return len(self._history)
|
||||
|
||||
def __iter__(self):
|
||||
return iter(self._history.items())
|
||||
|
||||
def current_idx(self):
|
||||
raise NotImplementedError
|
||||
|
||||
def back(self):
|
||||
raise NotImplementedError
|
||||
|
||||
def forward(self):
|
||||
raise NotImplementedError
|
||||
|
||||
def can_go_back(self):
|
||||
raise NotImplementedError
|
||||
|
||||
def can_go_forward(self):
|
||||
raise NotImplementedError
|
||||
|
||||
def serialize(self):
|
||||
"""Serialize into an opaque format understood by self.deserialize."""
|
||||
raise NotImplementedError
|
||||
|
||||
def deserialize(self, data):
|
||||
"""Serialize from a format produced by self.serialize."""
|
||||
raise NotImplementedError
|
||||
|
||||
def load_items(self, items):
|
||||
"""Deserialize from a list of WebHistoryItems."""
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class AbstractTab(QWidget):
|
||||
|
||||
"""A wrapper over the given widget to hide its API and expose another one.
|
||||
|
||||
We use this to unify QWebView and QWebEngineView.
|
||||
|
||||
Attributes:
|
||||
history: The AbstractHistory for the current tab.
|
||||
registry: The ObjectRegistry associated with this tab.
|
||||
|
||||
_load_status: loading status of this page
|
||||
Accessible via load_status() method.
|
||||
_has_ssl_errors: Whether SSL errors happened.
|
||||
Needs to be set by subclasses.
|
||||
|
||||
for properties, see WebView/WebEngineView docs.
|
||||
|
||||
Signals:
|
||||
See related Qt signals.
|
||||
|
||||
new_tab_requested: Emitted when a new tab should be opened with the
|
||||
given URL.
|
||||
load_status_changed: The loading status changed
|
||||
"""
|
||||
|
||||
window_close_requested = pyqtSignal()
|
||||
link_hovered = pyqtSignal(str)
|
||||
load_started = pyqtSignal()
|
||||
load_progress = pyqtSignal(int)
|
||||
load_finished = pyqtSignal(bool)
|
||||
icon_changed = pyqtSignal(QIcon)
|
||||
title_changed = pyqtSignal(str)
|
||||
load_status_changed = pyqtSignal(str)
|
||||
new_tab_requested = pyqtSignal(QUrl)
|
||||
url_changed = pyqtSignal(QUrl)
|
||||
shutting_down = pyqtSignal()
|
||||
|
||||
def __init__(self, win_id, parent=None):
|
||||
self.win_id = win_id
|
||||
self.tab_id = next(tab_id_gen)
|
||||
super().__init__(parent)
|
||||
|
||||
self.registry = objreg.ObjectRegistry()
|
||||
tab_registry = objreg.get('tab-registry', scope='window',
|
||||
window=win_id)
|
||||
tab_registry[self.tab_id] = self
|
||||
objreg.register('tab', self, registry=self.registry)
|
||||
|
||||
# self.history = AbstractHistory(self)
|
||||
# self.scroller = AbstractScroller(self, parent=self)
|
||||
# self.caret = AbstractCaret(win_id=win_id, tab=self, mode_manager=...,
|
||||
# parent=self)
|
||||
# self.zoom = AbstractZoom(win_id=win_id)
|
||||
# self.search = AbstractSearch(parent=self)
|
||||
# self.printing = AbstractPrinting()
|
||||
self.data = TabData()
|
||||
self._layout = None
|
||||
self._widget = None
|
||||
self._progress = 0
|
||||
self._has_ssl_errors = False
|
||||
self._load_status = usertypes.LoadStatus.none
|
||||
self.backend = None
|
||||
|
||||
def _set_widget(self, widget):
|
||||
# pylint: disable=protected-access
|
||||
self._layout = WrapperLayout(widget, self)
|
||||
self._widget = widget
|
||||
self.history._history = widget.history()
|
||||
self.scroller._init_widget(widget)
|
||||
self.caret._widget = widget
|
||||
self.zoom._widget = widget
|
||||
self.search._widget = widget
|
||||
self.printing._widget = widget
|
||||
widget.mouse_wheel_zoom.connect(self.zoom._on_mouse_wheel_zoom)
|
||||
widget.setParent(self)
|
||||
self.setFocusProxy(widget)
|
||||
|
||||
def _set_load_status(self, val):
|
||||
"""Setter for load_status."""
|
||||
if not isinstance(val, usertypes.LoadStatus):
|
||||
raise TypeError("Type {} is no LoadStatus member!".format(val))
|
||||
log.webview.debug("load status for {}: {}".format(repr(self), val))
|
||||
self._load_status = val
|
||||
self.load_status_changed.emit(val.name)
|
||||
|
||||
@pyqtSlot(QUrl)
|
||||
def _on_url_changed(self, url):
|
||||
"""Update title when URL has changed and no title is available."""
|
||||
if url.isValid() and not self.title():
|
||||
self.title_changed.emit(url.toDisplayString())
|
||||
self.url_changed.emit(url)
|
||||
|
||||
@pyqtSlot()
|
||||
def _on_load_started(self):
|
||||
self._progress = 0
|
||||
self._has_ssl_errors = False
|
||||
self.data.viewing_source = False
|
||||
self._set_load_status(usertypes.LoadStatus.loading)
|
||||
self.load_started.emit()
|
||||
|
||||
@pyqtSlot(bool)
|
||||
def _on_load_finished(self, ok):
|
||||
if ok and not self._has_ssl_errors:
|
||||
if self.url().scheme() == 'https':
|
||||
self._set_load_status(usertypes.LoadStatus.success_https)
|
||||
else:
|
||||
self._set_load_status(usertypes.LoadStatus.success)
|
||||
|
||||
elif ok:
|
||||
self._set_load_status(usertypes.LoadStatus.warn)
|
||||
else:
|
||||
self._set_load_status(usertypes.LoadStatus.error)
|
||||
self.load_finished.emit(ok)
|
||||
if not self.title():
|
||||
self.title_changed.emit(self.url().toDisplayString())
|
||||
|
||||
@pyqtSlot(int)
|
||||
def _on_load_progress(self, perc):
|
||||
self._progress = perc
|
||||
self.load_progress.emit(perc)
|
||||
|
||||
@pyqtSlot()
|
||||
def _on_ssl_errors(self):
|
||||
self._has_ssl_errors = True
|
||||
|
||||
def url(self):
|
||||
raise NotImplementedError
|
||||
|
||||
def progress(self):
|
||||
return self._progress
|
||||
|
||||
def load_status(self):
|
||||
return self._load_status
|
||||
|
||||
def _openurl_prepare(self, url):
|
||||
qtutils.ensure_valid(url)
|
||||
self.title_changed.emit(url.toDisplayString())
|
||||
|
||||
def openurl(self, url):
|
||||
raise NotImplementedError
|
||||
|
||||
def reload(self, *, force=False):
|
||||
raise NotImplementedError
|
||||
|
||||
def stop(self):
|
||||
raise NotImplementedError
|
||||
|
||||
def clear_ssl_errors(self):
|
||||
raise NotImplementedError
|
||||
|
||||
def dump_async(self, callback, *, plain=False):
|
||||
"""Dump the current page to a file ascync.
|
||||
|
||||
The given callback will be called with the result when dumping is
|
||||
complete.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def run_js_async(self, code, callback=None):
|
||||
"""Run javascript async.
|
||||
|
||||
The given callback will be called with the result when running JS is
|
||||
complete.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def run_js_blocking(self, code):
|
||||
"""Run javascript and block.
|
||||
|
||||
This returns the result to the caller. Its use should be avoided when
|
||||
possible as it runs a local event loop for QtWebEngine.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def shutdown(self):
|
||||
raise NotImplementedError
|
||||
|
||||
def title(self):
|
||||
raise NotImplementedError
|
||||
|
||||
def icon(self):
|
||||
raise NotImplementedError
|
||||
|
||||
def set_html(self, html, base_url):
|
||||
raise NotImplementedError
|
||||
|
||||
def __repr__(self):
|
||||
try:
|
||||
url = utils.elide(self.url().toDisplayString(QUrl.EncodeUnicode),
|
||||
100)
|
||||
except AttributeError:
|
||||
url = '<AttributeError>'
|
||||
return utils.get_repr(self, tab_id=self.tab_id, url=url)
|
||||
File diff suppressed because it is too large
Load Diff
@@ -25,7 +25,7 @@ import sip
|
||||
from PyQt5.QtCore import pyqtSlot, QSize, Qt, QTimer
|
||||
from PyQt5.QtWidgets import QListView, QSizePolicy, QMenu
|
||||
|
||||
from qutebrowser.browser import downloads
|
||||
from qutebrowser.browser.webkit import downloads
|
||||
from qutebrowser.config import style
|
||||
from qutebrowser.utils import qtutils, utils, objreg
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ from PyQt5.QtWebKitWidgets import QWebPage
|
||||
|
||||
from qutebrowser.config import config
|
||||
from qutebrowser.keyinput import modeman, modeparsers
|
||||
from qutebrowser.browser import webelem
|
||||
from qutebrowser.browser.webkit import webelem
|
||||
from qutebrowser.commands import userscripts, cmdexc, cmdutils, runners
|
||||
from qutebrowser.utils import usertypes, log, qtutils, message, objreg, utils
|
||||
|
||||
@@ -52,7 +52,6 @@ class WordHintingError(Exception):
|
||||
"""Exception raised on errors during word hinting."""
|
||||
|
||||
|
||||
@pyqtSlot(usertypes.KeyMode)
|
||||
def on_mode_entered(mode, win_id):
|
||||
"""Stop hinting when insert mode was entered."""
|
||||
if mode == usertypes.KeyMode.insert:
|
||||
@@ -84,6 +83,7 @@ class HintContext:
|
||||
args: Custom arguments for userscript/spawn
|
||||
rapid: Whether to do rapid hinting.
|
||||
mainframe: The main QWebFrame where we started hinting in.
|
||||
tab: The WebTab object we started hinting in.
|
||||
group: The group of web elements to hint.
|
||||
"""
|
||||
|
||||
@@ -98,6 +98,7 @@ class HintContext:
|
||||
self.destroyed_frames = []
|
||||
self.args = []
|
||||
self.mainframe = None
|
||||
self.tab = None
|
||||
self.group = None
|
||||
|
||||
def get_args(self, urlstr):
|
||||
@@ -199,6 +200,7 @@ class HintManager(QObject):
|
||||
window=self._win_id)
|
||||
message_bridge.maybe_reset_text(text)
|
||||
self._context = None
|
||||
self._filterstr = None
|
||||
|
||||
def _hint_strings(self, elems):
|
||||
"""Calculate the hint strings for elems.
|
||||
@@ -569,7 +571,6 @@ class HintManager(QObject):
|
||||
"""
|
||||
cmd = context.args[0]
|
||||
args = context.args[1:]
|
||||
frame = context.mainframe
|
||||
env = {
|
||||
'QUTE_MODE': 'hints',
|
||||
'QUTE_SELECTED_TEXT': str(elem),
|
||||
@@ -578,8 +579,12 @@ class HintManager(QObject):
|
||||
url = self._resolve_url(elem, context.baseurl)
|
||||
if url is not None:
|
||||
env['QUTE_URL'] = url.toString(QUrl.FullyEncoded)
|
||||
env.update(userscripts.store_source(frame))
|
||||
userscripts.run(cmd, *args, win_id=self._win_id, env=env)
|
||||
|
||||
try:
|
||||
userscripts.run_async(context.tab, cmd, *args, win_id=self._win_id,
|
||||
env=env)
|
||||
except userscripts.UnsupportedError as e:
|
||||
message.error(self._win_id, str(e), immediately=True)
|
||||
|
||||
def _spawn(self, url, context):
|
||||
"""Spawn a simple command from a hint.
|
||||
@@ -603,7 +608,7 @@ class HintManager(QObject):
|
||||
Return:
|
||||
A QUrl with the absolute URL, or None.
|
||||
"""
|
||||
for attr in ('href', 'src'):
|
||||
for attr in ['href', 'src']:
|
||||
if attr in elem:
|
||||
text = elem[attr].strip()
|
||||
break
|
||||
@@ -672,8 +677,8 @@ class HintManager(QObject):
|
||||
"""
|
||||
if not isinstance(target, Target):
|
||||
raise TypeError("Target {} is no Target member!".format(target))
|
||||
if target in (Target.userscript, Target.spawn, Target.run,
|
||||
Target.fill):
|
||||
if target in [Target.userscript, Target.spawn, Target.run,
|
||||
Target.fill]:
|
||||
if not args:
|
||||
raise cmdexc.CommandError(
|
||||
"'args' is required with target userscript/spawn/run/"
|
||||
@@ -752,12 +757,13 @@ class HintManager(QObject):
|
||||
window=self._win_id)
|
||||
tabbed_browser.tabopen(url, background=background)
|
||||
else:
|
||||
webview = objreg.get('webview', scope='tab', window=self._win_id,
|
||||
tab=self._tab_id)
|
||||
webview.openurl(url)
|
||||
tab = objreg.get('tab', scope='tab', window=self._win_id,
|
||||
tab=self._tab_id)
|
||||
tab.openurl(url)
|
||||
|
||||
@cmdutils.register(instance='hintmanager', scope='tab', name='hint',
|
||||
star_args_optional=True, maxsplit=2)
|
||||
star_args_optional=True, maxsplit=2,
|
||||
backend=usertypes.Backend.QtWebKit)
|
||||
@cmdutils.argument('win_id', win_id=True)
|
||||
def start(self, rapid=False, group=webelem.Group.all, target=Target.normal,
|
||||
*args, win_id):
|
||||
@@ -772,6 +778,7 @@ class HintManager(QObject):
|
||||
- `all`: All clickable elements.
|
||||
- `links`: Only links.
|
||||
- `images`: Only images.
|
||||
- `inputs`: Only input fields.
|
||||
|
||||
target: What to do with the selected element.
|
||||
|
||||
@@ -810,10 +817,12 @@ class HintManager(QObject):
|
||||
"""
|
||||
tabbed_browser = objreg.get('tabbed-browser', scope='window',
|
||||
window=self._win_id)
|
||||
widget = tabbed_browser.currentWidget()
|
||||
if widget is None:
|
||||
tab = tabbed_browser.currentWidget()
|
||||
if tab is None:
|
||||
raise cmdexc.CommandError("No WebView available yet!")
|
||||
mainframe = widget.page().mainFrame()
|
||||
# FIXME:qtwebengine have a proper API for this
|
||||
page = tab._widget.page() # pylint: disable=protected-access
|
||||
mainframe = page.mainFrame()
|
||||
if mainframe is None:
|
||||
raise cmdexc.CommandError("No frame focused!")
|
||||
mode_manager = objreg.get('mode-manager', scope='window',
|
||||
@@ -836,6 +845,7 @@ class HintManager(QObject):
|
||||
|
||||
self._check_args(target, *args)
|
||||
self._context = HintContext()
|
||||
self._context.tab = tab
|
||||
self._context.target = target
|
||||
self._context.rapid = rapid
|
||||
try:
|
||||
|
||||
@@ -23,7 +23,7 @@ import os
|
||||
|
||||
from PyQt5.QtCore import QUrl
|
||||
|
||||
from qutebrowser.browser import webelem
|
||||
from qutebrowser.browser.webkit import webelem
|
||||
from qutebrowser.utils import utils
|
||||
|
||||
|
||||
@@ -69,7 +69,7 @@ def _generate_pdfjs_script(url):
|
||||
|
||||
|
||||
def fix_urls(asset):
|
||||
"""Take a html page and replace each relative URL with an absolute.
|
||||
"""Take an html page and replace each relative URL with an absolute.
|
||||
|
||||
This is specialized for pdf.js files and not a general purpose function.
|
||||
|
||||
|
||||
@@ -40,8 +40,7 @@ class SignalFilter(QObject):
|
||||
BLACKLIST: List of signal names which should not be logged.
|
||||
"""
|
||||
|
||||
BLACKLIST = ['cur_scroll_perc_changed', 'cur_progress',
|
||||
'cur_statusbar_message', 'cur_link_hovered']
|
||||
BLACKLIST = ['cur_scroll_perc_changed', 'cur_progress', 'cur_link_hovered']
|
||||
|
||||
def __init__(self, win_id, parent=None):
|
||||
super().__init__(parent)
|
||||
|
||||
@@ -32,8 +32,9 @@ import collections
|
||||
|
||||
from PyQt5.QtCore import pyqtSignal, QUrl, QObject
|
||||
|
||||
from qutebrowser.utils import message, usertypes, urlutils, standarddir, objreg
|
||||
from qutebrowser.commands import cmdexc, cmdutils
|
||||
from qutebrowser.utils import (message, usertypes, qtutils, urlutils,
|
||||
standarddir, objreg)
|
||||
from qutebrowser.commands import cmdutils
|
||||
from qutebrowser.misc import lineparser
|
||||
|
||||
|
||||
@@ -178,6 +179,9 @@ class QuickmarkManager(UrlMarkManager):
|
||||
def quickmark_add(self, win_id, url, name):
|
||||
"""Add a new quickmark.
|
||||
|
||||
You can view all saved quickmarks on the
|
||||
link:qute://bookmarks[bookmarks page].
|
||||
|
||||
Args:
|
||||
win_id: The window ID to display the errors in.
|
||||
url: The url to add as quickmark.
|
||||
@@ -204,19 +208,22 @@ class QuickmarkManager(UrlMarkManager):
|
||||
else:
|
||||
set_mark()
|
||||
|
||||
@cmdutils.register(instance='quickmark-manager', maxsplit=0)
|
||||
@cmdutils.argument('name',
|
||||
completion=usertypes.Completion.quickmark_by_name)
|
||||
def quickmark_del(self, name):
|
||||
"""Delete a quickmark.
|
||||
def get_by_qurl(self, url):
|
||||
"""Look up a quickmark by QUrl, returning its name.
|
||||
|
||||
Args:
|
||||
name: The name of the quickmark to delete.
|
||||
Takes O(n) time, where n is the number of quickmarks.
|
||||
Use a name instead where possible.
|
||||
"""
|
||||
qtutils.ensure_valid(url)
|
||||
urlstr = url.toString(QUrl.RemovePassword | QUrl.FullyEncoded)
|
||||
|
||||
try:
|
||||
self.delete(name)
|
||||
except KeyError:
|
||||
raise cmdexc.CommandError("Quickmark '{}' not found!".format(name))
|
||||
index = list(self.marks.values()).index(urlstr)
|
||||
key = list(self.marks.keys())[index]
|
||||
except ValueError:
|
||||
raise DoesNotExistError(
|
||||
"Quickmark for '{}' not found!".format(urlstr))
|
||||
return key
|
||||
|
||||
def get(self, name):
|
||||
"""Get the URL of the quickmark named name as a QUrl."""
|
||||
@@ -284,16 +291,3 @@ class BookmarkManager(UrlMarkManager):
|
||||
self.marks[urlstr] = title
|
||||
self.changed.emit()
|
||||
self.added.emit(title, urlstr)
|
||||
|
||||
@cmdutils.register(instance='bookmark-manager', maxsplit=0)
|
||||
@cmdutils.argument('url', completion=usertypes.Completion.bookmark_by_url)
|
||||
def bookmark_del(self, url):
|
||||
"""Delete a bookmark.
|
||||
|
||||
Args:
|
||||
url: The URL of the bookmark to delete.
|
||||
"""
|
||||
try:
|
||||
self.delete(url)
|
||||
except KeyError:
|
||||
raise cmdexc.CommandError("Bookmark '{}' not found!".format(url))
|
||||
|
||||
20
qutebrowser/browser/webengine/__init__.py
Normal file
20
qutebrowser/browser/webengine/__init__.py
Normal file
@@ -0,0 +1,20 @@
|
||||
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
|
||||
|
||||
# Copyright 2014-2016 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
|
||||
#
|
||||
# This file is part of qutebrowser.
|
||||
#
|
||||
# qutebrowser is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# qutebrowser is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""Classes related to the browser widgets for QtWebEngine."""
|
||||
427
qutebrowser/browser/webengine/webenginetab.py
Normal file
427
qutebrowser/browser/webengine/webenginetab.py
Normal file
@@ -0,0 +1,427 @@
|
||||
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
|
||||
|
||||
# Copyright 2016 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
|
||||
#
|
||||
# This file is part of qutebrowser.
|
||||
#
|
||||
# qutebrowser is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# qutebrowser is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
# FIXME:qtwebengine remove this once the stubs are gone
|
||||
# pylint: disable=unused-variable
|
||||
|
||||
"""Wrapper over a QWebEngineView."""
|
||||
|
||||
from PyQt5.QtCore import pyqtSlot, Qt, QEvent, QPoint
|
||||
from PyQt5.QtGui import QKeyEvent, QIcon
|
||||
from PyQt5.QtWidgets import QApplication
|
||||
# pylint: disable=no-name-in-module,import-error,useless-suppression
|
||||
from PyQt5.QtWebEngineWidgets import QWebEnginePage
|
||||
# pylint: enable=no-name-in-module,import-error,useless-suppression
|
||||
|
||||
from qutebrowser.browser import browsertab
|
||||
from qutebrowser.browser.webengine import webview
|
||||
from qutebrowser.utils import usertypes, qtutils, log, utils
|
||||
|
||||
|
||||
class WebEnginePrinting(browsertab.AbstractPrinting):
|
||||
|
||||
"""QtWebEngine implementations related to printing."""
|
||||
|
||||
def check_pdf_support(self):
|
||||
if not hasattr(self._widget.page(), 'printToPdf'):
|
||||
raise browsertab.WebTabError(
|
||||
"Printing to PDF is unsupported with QtWebEngine on Qt > 5.7")
|
||||
|
||||
def check_printer_support(self):
|
||||
raise browsertab.WebTabError(
|
||||
"Printing is unsupported with QtWebEngine")
|
||||
|
||||
def to_pdf(self, filename):
|
||||
self._widget.page().printToPdf(filename)
|
||||
|
||||
def to_printer(self, printer):
|
||||
# Should never be called
|
||||
assert False
|
||||
|
||||
|
||||
class WebEngineSearch(browsertab.AbstractSearch):
|
||||
|
||||
"""QtWebEngine implementations related to searching on the page."""
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self._flags = QWebEnginePage.FindFlags(0)
|
||||
|
||||
def _find(self, text, flags, cb=None):
|
||||
"""Call findText on the widget with optional callback."""
|
||||
if cb is None:
|
||||
self._widget.findText(text, flags)
|
||||
else:
|
||||
self._widget.findText(text, flags, cb)
|
||||
|
||||
def search(self, text, *, ignore_case=False, reverse=False,
|
||||
result_cb=None):
|
||||
flags = QWebEnginePage.FindFlags(0)
|
||||
if ignore_case == 'smart':
|
||||
if not text.islower():
|
||||
flags |= QWebEnginePage.FindCaseSensitively
|
||||
elif not ignore_case:
|
||||
flags |= QWebEnginePage.FindCaseSensitively
|
||||
if reverse:
|
||||
flags |= QWebEnginePage.FindBackward
|
||||
|
||||
self.text = text
|
||||
self._flags = flags
|
||||
self._find(text, flags, result_cb)
|
||||
|
||||
def clear(self):
|
||||
self._widget.findText('')
|
||||
|
||||
def prev_result(self, *, result_cb=None):
|
||||
# The int() here makes sure we get a copy of the flags.
|
||||
flags = QWebEnginePage.FindFlags(int(self._flags))
|
||||
if flags & QWebEnginePage.FindBackward:
|
||||
flags &= ~QWebEnginePage.FindBackward
|
||||
else:
|
||||
flags |= QWebEnginePage.FindBackward
|
||||
self._find(self.text, self._flags, result_cb)
|
||||
|
||||
def next_result(self, *, result_cb=None):
|
||||
self._find(self.text, self._flags, result_cb)
|
||||
|
||||
|
||||
class WebEngineCaret(browsertab.AbstractCaret):
|
||||
|
||||
"""QtWebEngine implementations related to moving the cursor/selection."""
|
||||
|
||||
@pyqtSlot(usertypes.KeyMode)
|
||||
def _on_mode_entered(self, mode):
|
||||
log.stub()
|
||||
|
||||
@pyqtSlot(usertypes.KeyMode)
|
||||
def _on_mode_left(self):
|
||||
log.stub()
|
||||
|
||||
def move_to_next_line(self, count=1):
|
||||
log.stub()
|
||||
|
||||
def move_to_prev_line(self, count=1):
|
||||
log.stub()
|
||||
|
||||
def move_to_next_char(self, count=1):
|
||||
log.stub()
|
||||
|
||||
def move_to_prev_char(self, count=1):
|
||||
log.stub()
|
||||
|
||||
def move_to_end_of_word(self, count=1):
|
||||
log.stub()
|
||||
|
||||
def move_to_next_word(self, count=1):
|
||||
log.stub()
|
||||
|
||||
def move_to_prev_word(self, count=1):
|
||||
log.stub()
|
||||
|
||||
def move_to_start_of_line(self):
|
||||
log.stub()
|
||||
|
||||
def move_to_end_of_line(self):
|
||||
log.stub()
|
||||
|
||||
def move_to_start_of_next_block(self, count=1):
|
||||
log.stub()
|
||||
|
||||
def move_to_start_of_prev_block(self, count=1):
|
||||
log.stub()
|
||||
|
||||
def move_to_end_of_next_block(self, count=1):
|
||||
log.stub()
|
||||
|
||||
def move_to_end_of_prev_block(self, count=1):
|
||||
log.stub()
|
||||
|
||||
def move_to_start_of_document(self):
|
||||
log.stub()
|
||||
|
||||
def move_to_end_of_document(self):
|
||||
log.stub()
|
||||
|
||||
def toggle_selection(self):
|
||||
log.stub()
|
||||
|
||||
def drop_selection(self):
|
||||
log.stub()
|
||||
|
||||
def has_selection(self):
|
||||
return self._widget.hasSelection()
|
||||
|
||||
def selection(self, html=False):
|
||||
if html:
|
||||
raise NotImplementedError
|
||||
return self._widget.selectedText()
|
||||
|
||||
def follow_selected(self, *, tab=False):
|
||||
log.stub()
|
||||
|
||||
|
||||
class WebEngineScroller(browsertab.AbstractScroller):
|
||||
|
||||
"""QtWebEngine implementations related to scrolling."""
|
||||
|
||||
def __init__(self, tab, parent=None):
|
||||
super().__init__(tab, parent)
|
||||
self._pos_perc = (None, None)
|
||||
self._pos_px = QPoint()
|
||||
|
||||
def _init_widget(self, widget):
|
||||
super()._init_widget(widget)
|
||||
page = widget.page()
|
||||
try:
|
||||
page.scrollPositionChanged.connect(
|
||||
self._on_scroll_pos_changed)
|
||||
except AttributeError:
|
||||
log.stub('scrollPositionChanged, on Qt < 5.7')
|
||||
self._on_scroll_pos_changed()
|
||||
|
||||
def _key_press(self, key, count=1):
|
||||
# FIXME:qtwebengine Abort scrolling if the minimum/maximum was reached.
|
||||
press_evt = QKeyEvent(QEvent.KeyPress, key, Qt.NoModifier, 0, 0, 0)
|
||||
release_evt = QKeyEvent(QEvent.KeyRelease, key, Qt.NoModifier, 0, 0, 0)
|
||||
recipient = self._widget.focusProxy()
|
||||
for _ in range(count):
|
||||
# If we get a segfault here, we might want to try sendEvent
|
||||
# instead.
|
||||
QApplication.postEvent(recipient, press_evt)
|
||||
QApplication.postEvent(recipient, release_evt)
|
||||
|
||||
@pyqtSlot()
|
||||
def _on_scroll_pos_changed(self):
|
||||
"""Update the scroll position attributes when it changed."""
|
||||
def update_scroll_pos(jsret):
|
||||
"""Callback after getting scroll position via JS."""
|
||||
assert isinstance(jsret, dict)
|
||||
self._pos_perc = (jsret['perc']['x'], jsret['perc']['y'])
|
||||
self._pos_px = QPoint(jsret['px']['x'], jsret['px']['y'])
|
||||
self.perc_changed.emit(*self._pos_perc)
|
||||
|
||||
js_code = """
|
||||
{scroll_js}
|
||||
scroll_pos();
|
||||
""".format(scroll_js=utils.read_file('javascript/scroll.js'))
|
||||
self._tab.run_js_async(js_code, update_scroll_pos)
|
||||
|
||||
def pos_px(self):
|
||||
return self._pos_px
|
||||
|
||||
def pos_perc(self):
|
||||
return self._pos_perc
|
||||
|
||||
def to_perc(self, x=None, y=None):
|
||||
js_code = """
|
||||
{scroll_js}
|
||||
scroll_to_perc({x}, {y});
|
||||
""".format(scroll_js=utils.read_file('javascript/scroll.js'),
|
||||
x='undefined' if x is None else x,
|
||||
y='undefined' if y is None else y)
|
||||
self._tab.run_js_async(js_code)
|
||||
|
||||
def to_point(self, point):
|
||||
self._tab.run_js_async("window.scroll({x}, {y});".format(
|
||||
x=point.x(), y=point.y()))
|
||||
|
||||
def delta(self, x=0, y=0):
|
||||
self._tab.run_js_async("window.scrollBy({x}, {y});".format(x=x, y=y))
|
||||
|
||||
def delta_page(self, x=0, y=0):
|
||||
js_code = """
|
||||
{scroll_js}
|
||||
scroll_delta_page({x}, {y});
|
||||
""".format(scroll_js=utils.read_file('javascript/scroll.js'), x=x, y=y)
|
||||
self._tab.run_js_async(js_code)
|
||||
|
||||
def up(self, count=1):
|
||||
self._key_press(Qt.Key_Up, count)
|
||||
|
||||
def down(self, count=1):
|
||||
self._key_press(Qt.Key_Down, count)
|
||||
|
||||
def left(self, count=1):
|
||||
self._key_press(Qt.Key_Left, count)
|
||||
|
||||
def right(self, count=1):
|
||||
self._key_press(Qt.Key_Right, count)
|
||||
|
||||
def top(self):
|
||||
self._key_press(Qt.Key_Home)
|
||||
|
||||
def bottom(self):
|
||||
self._key_press(Qt.Key_End)
|
||||
|
||||
def page_up(self, count=1):
|
||||
self._key_press(Qt.Key_PageUp, count)
|
||||
|
||||
def page_down(self, count=1):
|
||||
self._key_press(Qt.Key_PageDown, count)
|
||||
|
||||
def at_top(self):
|
||||
return self.pos_px().y() == 0
|
||||
|
||||
def at_bottom(self):
|
||||
log.stub()
|
||||
|
||||
|
||||
class WebEngineHistory(browsertab.AbstractHistory):
|
||||
|
||||
"""QtWebEngine implementations related to page history."""
|
||||
|
||||
def current_idx(self):
|
||||
return self._history.currentItemIndex()
|
||||
|
||||
def back(self):
|
||||
self._history.back()
|
||||
|
||||
def forward(self):
|
||||
self._history.forward()
|
||||
|
||||
def can_go_back(self):
|
||||
return self._history.canGoBack()
|
||||
|
||||
def can_go_forward(self):
|
||||
return self._history.canGoForward()
|
||||
|
||||
def serialize(self):
|
||||
return qtutils.serialize(self._history)
|
||||
|
||||
def deserialize(self, data):
|
||||
return qtutils.deserialize(data, self._history)
|
||||
|
||||
def load_items(self, items):
|
||||
log.stub()
|
||||
|
||||
|
||||
class WebEngineZoom(browsertab.AbstractZoom):
|
||||
|
||||
"""QtWebEngine implementations related to zooming."""
|
||||
|
||||
def _set_factor_internal(self, factor):
|
||||
self._widget.setZoomFactor(factor)
|
||||
|
||||
def factor(self):
|
||||
return self._widget.zoomFactor()
|
||||
|
||||
|
||||
class WebEngineTab(browsertab.AbstractTab):
|
||||
|
||||
"""A QtWebEngine tab in the browser."""
|
||||
|
||||
def __init__(self, win_id, mode_manager, parent=None):
|
||||
super().__init__(win_id)
|
||||
widget = webview.WebEngineView()
|
||||
self.history = WebEngineHistory(self)
|
||||
self.scroller = WebEngineScroller(self, parent=self)
|
||||
self.caret = WebEngineCaret(win_id=win_id, mode_manager=mode_manager,
|
||||
tab=self, parent=self)
|
||||
self.zoom = WebEngineZoom(win_id=win_id, parent=self)
|
||||
self.search = WebEngineSearch(parent=self)
|
||||
self.printing = WebEnginePrinting()
|
||||
self._set_widget(widget)
|
||||
self._connect_signals()
|
||||
self.backend = usertypes.Backend.QtWebEngine
|
||||
|
||||
def openurl(self, url):
|
||||
self._openurl_prepare(url)
|
||||
self._widget.load(url)
|
||||
|
||||
def url(self):
|
||||
return self._widget.url()
|
||||
|
||||
def dump_async(self, callback, *, plain=False):
|
||||
if plain:
|
||||
self._widget.page().toPlainText(callback)
|
||||
else:
|
||||
self._widget.page().toHtml(callback)
|
||||
|
||||
def run_js_async(self, code, callback=None):
|
||||
if callback is None:
|
||||
self._widget.page().runJavaScript(code)
|
||||
else:
|
||||
self._widget.page().runJavaScript(code, callback)
|
||||
|
||||
def run_js_blocking(self, code):
|
||||
unset = object()
|
||||
loop = qtutils.EventLoop()
|
||||
js_ret = unset
|
||||
|
||||
def js_cb(val):
|
||||
"""Handle return value from JS and stop blocking."""
|
||||
nonlocal js_ret
|
||||
js_ret = val
|
||||
loop.quit()
|
||||
|
||||
self.run_js_async(code, js_cb)
|
||||
loop.exec_() # blocks until loop.quit() in js_cb
|
||||
assert js_ret is not unset
|
||||
|
||||
return js_ret
|
||||
|
||||
def shutdown(self):
|
||||
log.stub()
|
||||
|
||||
def reload(self, *, force=False):
|
||||
if force:
|
||||
action = QWebEnginePage.ReloadAndBypassCache
|
||||
else:
|
||||
action = QWebEnginePage.Reload
|
||||
self._widget.triggerPageAction(action)
|
||||
|
||||
def stop(self):
|
||||
self._widget.stop()
|
||||
|
||||
def title(self):
|
||||
return self._widget.title()
|
||||
|
||||
def icon(self):
|
||||
try:
|
||||
return self._widget.icon()
|
||||
except AttributeError:
|
||||
log.stub('on Qt < 5.7')
|
||||
return QIcon()
|
||||
|
||||
def set_html(self, html, base_url):
|
||||
# FIXME:qtwebengine
|
||||
# check this and raise an exception if too big:
|
||||
# Warning: The content will be percent encoded before being sent to the
|
||||
# renderer via IPC. This may increase its size. The maximum size of the
|
||||
# percent encoded content is 2 megabytes minus 30 bytes.
|
||||
self._widget.setHtml(html, base_url)
|
||||
|
||||
def clear_ssl_errors(self):
|
||||
log.stub()
|
||||
|
||||
def _connect_signals(self):
|
||||
view = self._widget
|
||||
page = view.page()
|
||||
page.windowCloseRequested.connect(self.window_close_requested)
|
||||
page.linkHovered.connect(self.link_hovered)
|
||||
page.loadProgress.connect(self._on_load_progress)
|
||||
page.loadStarted.connect(self._on_load_started)
|
||||
view.titleChanged.connect(self.title_changed)
|
||||
view.urlChanged.connect(self._on_url_changed)
|
||||
page.loadFinished.connect(self._on_load_finished)
|
||||
page.certificate_error.connect(self._on_ssl_errors)
|
||||
try:
|
||||
view.iconChanged.connect(self.icon_changed)
|
||||
except AttributeError:
|
||||
log.stub('iconChanged, on Qt < 5.7')
|
||||
79
qutebrowser/browser/webengine/webview.py
Normal file
79
qutebrowser/browser/webengine/webview.py
Normal file
@@ -0,0 +1,79 @@
|
||||
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
|
||||
|
||||
# Copyright 2014-2016 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
|
||||
#
|
||||
# This file is part of qutebrowser.
|
||||
#
|
||||
# qutebrowser is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# qutebrowser is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""The main browser widget for QtWebEngine."""
|
||||
|
||||
|
||||
from PyQt5.QtCore import pyqtSignal, Qt, QPoint
|
||||
# pylint: disable=no-name-in-module,import-error,useless-suppression
|
||||
from PyQt5.QtWebEngineWidgets import QWebEngineView, QWebEnginePage
|
||||
# pylint: enable=no-name-in-module,import-error,useless-suppression
|
||||
|
||||
from qutebrowser.config import config
|
||||
from qutebrowser.utils import log
|
||||
|
||||
|
||||
class WebEngineView(QWebEngineView):
|
||||
|
||||
"""Custom QWebEngineView subclass with qutebrowser-specific features."""
|
||||
|
||||
mouse_wheel_zoom = pyqtSignal(QPoint)
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self.setPage(WebEnginePage(self))
|
||||
|
||||
def wheelEvent(self, e):
|
||||
"""Zoom on Ctrl-Mousewheel.
|
||||
|
||||
Args:
|
||||
e: The QWheelEvent.
|
||||
"""
|
||||
if e.modifiers() & Qt.ControlModifier:
|
||||
e.accept()
|
||||
self.mouse_wheel_zoom.emit(e.angleDelta())
|
||||
else:
|
||||
super().wheelEvent(e)
|
||||
|
||||
|
||||
class WebEnginePage(QWebEnginePage):
|
||||
|
||||
"""Custom QWebEnginePage subclass with qutebrowser-specific features."""
|
||||
|
||||
certificate_error = pyqtSignal()
|
||||
|
||||
def certificateError(self, error):
|
||||
self.certificate_error.emit()
|
||||
return super().certificateError(error)
|
||||
|
||||
def javaScriptConsoleMessage(self, level, msg, line, source):
|
||||
"""Log javascript messages to qutebrowser's log."""
|
||||
# FIXME:qtwebengine maybe unify this in the tab api somehow?
|
||||
setting = config.get('general', 'log-javascript-console')
|
||||
if setting == 'none':
|
||||
return
|
||||
|
||||
level_to_logger = {
|
||||
QWebEnginePage.InfoMessageLevel: log.js.info,
|
||||
QWebEnginePage.WarningMessageLevel: log.js.warning,
|
||||
QWebEnginePage.ErrorMessageLevel: log.js.error,
|
||||
}
|
||||
logstring = "[{}:{}] {}".format(source, line, msg)
|
||||
logger = level_to_logger[level]
|
||||
logger(logstring)
|
||||
20
qutebrowser/browser/webkit/__init__.py
Normal file
20
qutebrowser/browser/webkit/__init__.py
Normal file
@@ -0,0 +1,20 @@
|
||||
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
|
||||
|
||||
# Copyright 2014-2016 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
|
||||
#
|
||||
# This file is part of qutebrowser.
|
||||
#
|
||||
# qutebrowser is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# qutebrowser is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""Classes related to the browser widgets for QtWebKit."""
|
||||
@@ -37,9 +37,6 @@ class RAMCookieJar(QNetworkCookieJar):
|
||||
|
||||
changed = pyqtSignal()
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
|
||||
def __repr__(self):
|
||||
return utils.get_repr(self, count=len(self.allCookies()))
|
||||
|
||||
@@ -25,6 +25,7 @@ import sys
|
||||
import os.path
|
||||
import shutil
|
||||
import functools
|
||||
import tempfile
|
||||
import collections
|
||||
|
||||
import sip
|
||||
@@ -39,8 +40,8 @@ from qutebrowser.config import config
|
||||
from qutebrowser.commands import cmdexc, cmdutils
|
||||
from qutebrowser.utils import (message, usertypes, log, utils, urlutils,
|
||||
objreg, standarddir, qtutils)
|
||||
from qutebrowser.browser import http
|
||||
from qutebrowser.browser.network import networkmanager
|
||||
from qutebrowser.browser.webkit import http
|
||||
from qutebrowser.browser.webkit.network import networkmanager
|
||||
|
||||
|
||||
ModelRole = usertypes.enum('ModelRole', ['item'], start=Qt.UserRole,
|
||||
@@ -280,6 +281,7 @@ class DownloadItem(QObject):
|
||||
_read_timer: A Timer which reads the QNetworkReply into self._buffer
|
||||
periodically.
|
||||
_win_id: The window ID the DownloadItem runs in.
|
||||
_dead: Whether the Download has _die()'d.
|
||||
|
||||
Signals:
|
||||
data_changed: The downloads metadata changed.
|
||||
@@ -328,6 +330,7 @@ class DownloadItem(QObject):
|
||||
self.init_reply(reply)
|
||||
self._win_id = win_id
|
||||
self.raw_headers = {}
|
||||
self._dead = False
|
||||
|
||||
def __repr__(self):
|
||||
return utils.get_repr(self, basename=self.basename)
|
||||
@@ -345,7 +348,7 @@ class DownloadItem(QObject):
|
||||
errmsg = ""
|
||||
else:
|
||||
errmsg = " - {}".format(self.error_msg)
|
||||
if all(e is None for e in (perc, remaining, self.stats.total)):
|
||||
if all(e is None for e in [perc, remaining, self.stats.total]):
|
||||
return ('{index}: {name} [{speed:>10}|{down}]{errmsg}'.format(
|
||||
index=self.index, name=self.basename, speed=speed,
|
||||
down=down, errmsg=errmsg))
|
||||
@@ -395,6 +398,21 @@ class DownloadItem(QObject):
|
||||
def _die(self, msg):
|
||||
"""Abort the download and emit an error."""
|
||||
assert not self.successful
|
||||
# Prevent actions if calling _die() twice. This might happen if the
|
||||
# error handler correctly connects, and the error occurs in init_reply
|
||||
# between reply.error.connect and the reply.error() check. In this
|
||||
# case, the connected error handlers will be called twice, once via the
|
||||
# direct error.emit() and once here in _die(). The stacks look like
|
||||
# this then:
|
||||
# <networkmanager error.emit> -> on_reply_error -> _die ->
|
||||
# self.error.emit()
|
||||
# and
|
||||
# [init_reply -> <single shot timer> ->] <lambda in init_reply> ->
|
||||
# self.error.emit()
|
||||
# which may lead to duplicate error messages (and failing tests)
|
||||
if self._dead:
|
||||
return
|
||||
self._dead = True
|
||||
self._read_timer.stop()
|
||||
self.reply.downloadProgress.disconnect()
|
||||
self.reply.finished.disconnect()
|
||||
@@ -413,7 +431,10 @@ class DownloadItem(QObject):
|
||||
self.done = True
|
||||
self.data_changed.emit()
|
||||
if self.fileobj is not None:
|
||||
self.fileobj.close()
|
||||
try:
|
||||
self.fileobj.close()
|
||||
except OSError:
|
||||
log.downloads.exception("Error while closing file object")
|
||||
|
||||
def init_reply(self, reply):
|
||||
"""Set a new reply and connect its signals.
|
||||
@@ -438,7 +459,7 @@ class DownloadItem(QObject):
|
||||
# Here no signals are connected to the DownloadItem yet, so we use a
|
||||
# singleShot QTimer to emit them after they are connected.
|
||||
if reply.error() != QNetworkReply.NoError:
|
||||
QTimer.singleShot(0, lambda: self.error.emit(reply.errorString()))
|
||||
QTimer.singleShot(0, lambda: self._die(reply.errorString()))
|
||||
|
||||
def get_status_color(self, position):
|
||||
"""Choose an appropriate color for presenting the download's status.
|
||||
@@ -448,7 +469,7 @@ class DownloadItem(QObject):
|
||||
"""
|
||||
# pylint: disable=bad-config-call
|
||||
# WORKAROUND for https://bitbucket.org/logilab/astroid/issue/104/
|
||||
assert position in ("fg", "bg")
|
||||
assert position in ["fg", "bg"]
|
||||
start = config.get('colors', 'downloads.{}.start'.format(position))
|
||||
stop = config.get('colors', 'downloads.{}.stop'.format(position))
|
||||
system = config.get('colors', 'downloads.{}.system'.format(position))
|
||||
@@ -510,7 +531,13 @@ class DownloadItem(QObject):
|
||||
def open_file(self):
|
||||
"""Open the downloaded file."""
|
||||
assert self.successful
|
||||
url = QUrl.fromLocalFile(self._filename)
|
||||
filename = self._filename
|
||||
if filename is None:
|
||||
filename = getattr(self.fileobj, 'name', None)
|
||||
if filename is None:
|
||||
log.downloads.error("No filename to open the download!")
|
||||
return
|
||||
url = QUrl.fromLocalFile(filename)
|
||||
QDesktopServices.openUrl(url)
|
||||
|
||||
def set_filename(self, filename):
|
||||
@@ -675,7 +702,7 @@ class DownloadItem(QObject):
|
||||
self.raw_headers[bytes(key)] = bytes(value)
|
||||
|
||||
def _handle_redirect(self):
|
||||
"""Handle a HTTP redirect.
|
||||
"""Handle an HTTP redirect.
|
||||
|
||||
Return:
|
||||
True if the download was redirected, False otherwise.
|
||||
@@ -735,6 +762,9 @@ class DownloadManager(QAbstractListModel):
|
||||
def _postprocess_question(self, q):
|
||||
"""Postprocess a Question object that is asked."""
|
||||
q.destroyed.connect(functools.partial(self.questions.remove, q))
|
||||
# We set the mode here so that other code that uses ask_for_filename
|
||||
# doesn't need to handle the special download mode.
|
||||
q.mode = usertypes.PromptMode.download
|
||||
self.questions.append(q)
|
||||
|
||||
@pyqtSlot()
|
||||
@@ -754,10 +784,7 @@ class DownloadManager(QAbstractListModel):
|
||||
**kwargs: passed to get_request().
|
||||
|
||||
Return:
|
||||
If the download could start immediately, (fileobj/filename given),
|
||||
the created DownloadItem.
|
||||
|
||||
If not, None.
|
||||
The created DownloadItem.
|
||||
"""
|
||||
if not url.isValid():
|
||||
urlutils.invalid_url_error(self._win_id, url, "start download")
|
||||
@@ -765,27 +792,17 @@ class DownloadManager(QAbstractListModel):
|
||||
req = QNetworkRequest(url)
|
||||
return self.get_request(req, **kwargs)
|
||||
|
||||
def get_request(self, request, *, fileobj=None, filename=None,
|
||||
prompt_download_directory=None, **kwargs):
|
||||
def get_request(self, request, *, target=None, **kwargs):
|
||||
"""Start a download with a QNetworkRequest.
|
||||
|
||||
Args:
|
||||
request: The QNetworkRequest to download.
|
||||
fileobj: The file object to write the answer to.
|
||||
filename: A path to write the data to.
|
||||
prompt_download_directory: Whether to prompt for the download dir
|
||||
or automatically download. If None, the
|
||||
config is used.
|
||||
target: Where to save the download as usertypes.DownloadTarget.
|
||||
**kwargs: Passed to fetch_request.
|
||||
|
||||
Return:
|
||||
If the download could start immediately, (fileobj/filename given),
|
||||
the created DownloadItem.
|
||||
|
||||
If not, None.
|
||||
The created DownloadItem.
|
||||
"""
|
||||
if fileobj is not None and filename is not None: # pragma: no cover
|
||||
raise TypeError("Only one of fileobj/filename may be given!")
|
||||
# WORKAROUND for Qt corrupting data loaded from cache:
|
||||
# https://bugreports.qt.io/browse/QTBUG-42757
|
||||
request.setAttribute(QNetworkRequest.CacheLoadControlAttribute,
|
||||
@@ -813,27 +830,10 @@ class DownloadManager(QAbstractListModel):
|
||||
if suggested_fn is None:
|
||||
suggested_fn = 'qutebrowser-download'
|
||||
|
||||
# We won't need a question if a filename or fileobj is already given
|
||||
if fileobj is None and filename is None:
|
||||
filename, q = ask_for_filename(
|
||||
suggested_fn, self._win_id, parent=self,
|
||||
prompt_download_directory=prompt_download_directory
|
||||
)
|
||||
|
||||
if fileobj is not None or filename is not None:
|
||||
return self.fetch_request(request,
|
||||
fileobj=fileobj,
|
||||
filename=filename,
|
||||
suggested_filename=suggested_fn,
|
||||
**kwargs)
|
||||
q.answered.connect(
|
||||
lambda fn: self.fetch_request(request,
|
||||
filename=fn,
|
||||
suggested_filename=suggested_fn,
|
||||
**kwargs))
|
||||
self._postprocess_question(q)
|
||||
q.ask()
|
||||
return None
|
||||
return self.fetch_request(request,
|
||||
target=target,
|
||||
suggested_filename=suggested_fn,
|
||||
**kwargs)
|
||||
|
||||
def fetch_request(self, request, *, page=None, **kwargs):
|
||||
"""Download a QNetworkRequest to disk.
|
||||
@@ -854,27 +854,25 @@ class DownloadManager(QAbstractListModel):
|
||||
return self.fetch(reply, **kwargs)
|
||||
|
||||
@pyqtSlot('QNetworkReply')
|
||||
def fetch(self, reply, *, fileobj=None, filename=None, auto_remove=False,
|
||||
def fetch(self, reply, *, target=None, auto_remove=False,
|
||||
suggested_filename=None, prompt_download_directory=None):
|
||||
"""Download a QNetworkReply to disk.
|
||||
|
||||
Args:
|
||||
reply: The QNetworkReply to download.
|
||||
fileobj: The file object to write the answer to.
|
||||
filename: A path to write the data to.
|
||||
target: Where to save the download as usertypes.DownloadTarget.
|
||||
auto_remove: Whether to remove the download even if
|
||||
ui -> remove-finished-downloads is set to -1.
|
||||
|
||||
Return:
|
||||
The created DownloadItem.
|
||||
"""
|
||||
if fileobj is not None and filename is not None: # pragma: no cover
|
||||
raise TypeError("Only one of fileobj/filename may be given!")
|
||||
if not suggested_filename:
|
||||
if filename is not None:
|
||||
suggested_filename = os.path.basename(filename)
|
||||
elif fileobj is not None and getattr(fileobj, 'name', None):
|
||||
suggested_filename = fileobj.name
|
||||
if isinstance(target, usertypes.FileDownloadTarget):
|
||||
suggested_filename = os.path.basename(target.filename)
|
||||
elif (isinstance(target, usertypes.FileObjDownloadTarget) and
|
||||
getattr(target.fileobj, 'name', None)):
|
||||
suggested_filename = target.fileobj.name
|
||||
else:
|
||||
_, suggested_filename = http.parse_content_disposition(reply)
|
||||
log.downloads.debug("fetch: {} -> {}".format(reply.url(),
|
||||
@@ -906,13 +904,8 @@ class DownloadManager(QAbstractListModel):
|
||||
if not self._update_timer.isActive():
|
||||
self._update_timer.start()
|
||||
|
||||
if fileobj is not None:
|
||||
download.set_fileobj(fileobj)
|
||||
download.autoclose = False
|
||||
return download
|
||||
|
||||
if filename is not None:
|
||||
download.set_filename(filename)
|
||||
if target is not None:
|
||||
self._set_download_target(download, suggested_filename, target)
|
||||
return download
|
||||
|
||||
# Neither filename nor fileobj were given, prepare a question
|
||||
@@ -923,12 +916,15 @@ class DownloadManager(QAbstractListModel):
|
||||
|
||||
# User doesn't want to be asked, so just use the download_dir
|
||||
if filename is not None:
|
||||
download.set_filename(filename)
|
||||
target = usertypes.FileDownloadTarget(filename)
|
||||
self._set_download_target(download, suggested_filename, target)
|
||||
return download
|
||||
|
||||
# Ask the user for a filename
|
||||
self._postprocess_question(q)
|
||||
q.answered.connect(download.set_filename)
|
||||
q.answered.connect(
|
||||
functools.partial(self._set_download_target, download,
|
||||
suggested_filename))
|
||||
q.cancelled.connect(download.cancel)
|
||||
download.cancelled.connect(q.abort)
|
||||
download.error.connect(q.abort)
|
||||
@@ -936,6 +932,28 @@ class DownloadManager(QAbstractListModel):
|
||||
|
||||
return download
|
||||
|
||||
def _set_download_target(self, download, suggested_filename, target):
|
||||
"""Set the target for a given download.
|
||||
|
||||
Args:
|
||||
download: The download to set the filename for.
|
||||
suggested_filename: The suggested filename.
|
||||
target: The usertypes.DownloadTarget for this download.
|
||||
"""
|
||||
if isinstance(target, usertypes.FileObjDownloadTarget):
|
||||
download.set_fileobj(target.fileobj)
|
||||
download.autoclose = False
|
||||
elif isinstance(target, usertypes.FileDownloadTarget):
|
||||
download.set_filename(target.filename)
|
||||
elif isinstance(target, usertypes.OpenFileDownloadTarget):
|
||||
tmp_manager = objreg.get('temporary-downloads')
|
||||
fobj = tmp_manager.get_tmpfile(suggested_filename)
|
||||
download.finished.connect(download.open_file)
|
||||
download.autoclose = True
|
||||
download.set_fileobj(fobj)
|
||||
else:
|
||||
log.downloads.error("Unknown download target: {}".format(target))
|
||||
|
||||
def raise_no_download(self, count):
|
||||
"""Raise an exception that the download doesn't exist.
|
||||
|
||||
@@ -1038,7 +1056,7 @@ class DownloadManager(QAbstractListModel):
|
||||
|
||||
@pyqtSlot(QNetworkRequest, QNetworkReply)
|
||||
def on_redirect(self, download, request, reply):
|
||||
"""Handle a HTTP redirect of a download.
|
||||
"""Handle an HTTP redirect of a download.
|
||||
|
||||
Args:
|
||||
download: The old DownloadItem.
|
||||
@@ -1246,3 +1264,59 @@ class DownloadManager(QAbstractListModel):
|
||||
The number of unfinished downloads.
|
||||
"""
|
||||
return sum(1 for download in self.downloads if not download.done)
|
||||
|
||||
|
||||
class TempDownloadManager(QObject):
|
||||
|
||||
"""Manager to handle temporary download files.
|
||||
|
||||
The downloads are downloaded to a temporary location and then openened with
|
||||
the system standard application. The temporary files are deleted when
|
||||
qutebrowser is shutdown.
|
||||
|
||||
Attributes:
|
||||
files: A list of NamedTemporaryFiles of downloaded items.
|
||||
"""
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self.files = []
|
||||
self._tmpdir = None
|
||||
|
||||
def cleanup(self):
|
||||
"""Clean up any temporary files."""
|
||||
if self._tmpdir is not None:
|
||||
self._tmpdir.cleanup()
|
||||
self._tmpdir = None
|
||||
|
||||
def _get_tmpdir(self):
|
||||
"""Return the temporary directory that is used for downloads.
|
||||
|
||||
The directory is created lazily on first access.
|
||||
|
||||
Return:
|
||||
The tempfile.TemporaryDirectory that is used.
|
||||
"""
|
||||
if self._tmpdir is None:
|
||||
self._tmpdir = tempfile.TemporaryDirectory(
|
||||
prefix='qutebrowser-downloads-')
|
||||
return self._tmpdir
|
||||
|
||||
def get_tmpfile(self, suggested_name):
|
||||
"""Return a temporary file in the temporary downloads directory.
|
||||
|
||||
The files are kept as long as qutebrowser is running and automatically
|
||||
cleaned up at program exit.
|
||||
|
||||
Args:
|
||||
suggested_name: str of the "suggested"/original filename. Used as a
|
||||
suffix, so any file extenions are preserved.
|
||||
|
||||
Return:
|
||||
A tempfile.NamedTemporaryFile that should be used to save the file.
|
||||
"""
|
||||
tmpdir = self._get_tmpdir()
|
||||
fobj = tempfile.NamedTemporaryFile(dir=tmpdir.name, delete=False,
|
||||
suffix=suggested_name)
|
||||
self.files.append(fobj)
|
||||
return fobj
|
||||
@@ -125,7 +125,7 @@ class WebHistoryInterface(QWebHistoryInterface):
|
||||
pass
|
||||
|
||||
def historyContains(self, url_string):
|
||||
"""Called by WebKit to determine if an URL is contained in the history.
|
||||
"""Called by WebKit to determine if a URL is contained in the history.
|
||||
|
||||
Args:
|
||||
url_string: The URL (as string) to check for.
|
||||
@@ -285,10 +285,10 @@ class WebHistory(QObject):
|
||||
self.cleared.emit()
|
||||
|
||||
def add_url(self, url, title="", *, redirect=False, atime=None):
|
||||
"""Called by WebKit when an URL should be added to the history.
|
||||
"""Called by WebKit when a URL should be added to the history.
|
||||
|
||||
Args:
|
||||
url: An url (as QUrl) to add to the history.
|
||||
url: A url (as QUrl) to add to the history.
|
||||
redirect: Whether the entry was redirected to another URL
|
||||
(hidden in completion)
|
||||
atime: Override the atime used to add the entry
|
||||
@@ -23,7 +23,7 @@
|
||||
import os.path
|
||||
|
||||
from qutebrowser.utils import log
|
||||
from qutebrowser.browser import rfc6266
|
||||
from qutebrowser.browser.webkit import rfc6266
|
||||
|
||||
from PyQt5.QtNetwork import QNetworkRequest
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""Utils for writing a MHTML file."""
|
||||
"""Utils for writing an MHTML file."""
|
||||
|
||||
import functools
|
||||
import io
|
||||
@@ -34,7 +34,7 @@ import email.message
|
||||
|
||||
from PyQt5.QtCore import QUrl
|
||||
|
||||
from qutebrowser.browser import webelem, downloads
|
||||
from qutebrowser.browser.webkit import webelem, downloads
|
||||
from qutebrowser.utils import log, objreg, message, usertypes, utils, urlutils
|
||||
|
||||
try:
|
||||
@@ -83,7 +83,7 @@ def _get_css_imports_cssutils(data, inline=False):
|
||||
|
||||
Args:
|
||||
data: The content of the stylesheet to scan as string.
|
||||
inline: True if the argument is a inline HTML style attribute.
|
||||
inline: True if the argument is an inline HTML style attribute.
|
||||
"""
|
||||
# We don't care about invalid CSS data, this will only litter the log
|
||||
# output with CSS errors
|
||||
@@ -112,7 +112,7 @@ def _get_css_imports(data, inline=False):
|
||||
|
||||
Args:
|
||||
data: The content of the stylesheet to scan as string.
|
||||
inline: True if the argument is a inline HTML style attribute.
|
||||
inline: True if the argument is an inline HTML style attribute.
|
||||
"""
|
||||
if cssutils is None:
|
||||
return _get_css_imports_regex(data)
|
||||
@@ -149,7 +149,7 @@ E_QUOPRI = email.encoders.encode_quopri
|
||||
|
||||
class MHTMLWriter:
|
||||
|
||||
"""A class for outputting multiple files to a MHTML document.
|
||||
"""A class for outputting multiple files to an MHTML document.
|
||||
|
||||
Attributes:
|
||||
root_content: The root content as bytes.
|
||||
@@ -222,7 +222,7 @@ class _Downloader:
|
||||
"""A class to download whole websites.
|
||||
|
||||
Attributes:
|
||||
web_view: The QWebView which contains the website that will be saved.
|
||||
tab: The AbstractTab which contains the website that will be saved.
|
||||
dest: Destination filename.
|
||||
writer: The MHTMLWriter object which is used to save the page.
|
||||
loaded_urls: A set of QUrls of finished asset downloads.
|
||||
@@ -233,15 +233,15 @@ class _Downloader:
|
||||
_win_id: The window this downloader belongs to.
|
||||
"""
|
||||
|
||||
def __init__(self, web_view, dest):
|
||||
self.web_view = web_view
|
||||
def __init__(self, tab, dest):
|
||||
self.tab = tab
|
||||
self.dest = dest
|
||||
self.writer = None
|
||||
self.loaded_urls = {web_view.url()}
|
||||
self.loaded_urls = {tab.url()}
|
||||
self.pending_downloads = set()
|
||||
self._finished_file = False
|
||||
self._used = False
|
||||
self._win_id = web_view.win_id
|
||||
self._win_id = tab.win_id
|
||||
|
||||
def run(self):
|
||||
"""Download and save the page.
|
||||
@@ -252,8 +252,11 @@ class _Downloader:
|
||||
if self._used:
|
||||
raise ValueError("Downloader already used")
|
||||
self._used = True
|
||||
web_url = self.web_view.url()
|
||||
web_frame = self.web_view.page().mainFrame()
|
||||
web_url = self.tab.url()
|
||||
|
||||
# FIXME:qtwebengine have a proper API for this
|
||||
page = self.tab._widget.page() # pylint: disable=protected-access
|
||||
web_frame = page.mainFrame()
|
||||
|
||||
self.writer = MHTMLWriter(
|
||||
web_frame.toHtml().encode('utf-8'),
|
||||
@@ -316,7 +319,7 @@ class _Downloader:
|
||||
Args:
|
||||
url: The file to download as QUrl.
|
||||
"""
|
||||
if url.scheme() not in {'http', 'https'}:
|
||||
if url.scheme() not in ['http', 'https']:
|
||||
return
|
||||
# Prevent loading an asset twice
|
||||
if url in self.loaded_urls:
|
||||
@@ -340,7 +343,8 @@ class _Downloader:
|
||||
|
||||
download_manager = objreg.get('download-manager', scope='window',
|
||||
window=self._win_id)
|
||||
item = download_manager.get(url, fileobj=_NoCloseBytesIO(),
|
||||
target = usertypes.FileObjDownloadTarget(_NoCloseBytesIO())
|
||||
item = download_manager.get(url, target=target,
|
||||
auto_remove=True)
|
||||
self.pending_downloads.add((url, item))
|
||||
item.finished.connect(functools.partial(self._finished, url, item))
|
||||
@@ -479,28 +483,28 @@ class _NoCloseBytesIO(io.BytesIO):
|
||||
super().close()
|
||||
|
||||
|
||||
def _start_download(dest, web_view):
|
||||
"""Start downloading the current page and all assets to a MHTML file.
|
||||
def _start_download(dest, tab):
|
||||
"""Start downloading the current page and all assets to an MHTML file.
|
||||
|
||||
This will overwrite dest if it already exists.
|
||||
|
||||
Args:
|
||||
dest: The filename where the resulting file should be saved.
|
||||
web_view: Specify the webview whose page should be loaded.
|
||||
tab: Specify the tab whose page should be loaded.
|
||||
"""
|
||||
loader = _Downloader(web_view, dest)
|
||||
loader = _Downloader(tab, dest)
|
||||
loader.run()
|
||||
|
||||
|
||||
def start_download_checked(dest, web_view):
|
||||
def start_download_checked(dest, tab):
|
||||
"""First check if dest is already a file, then start the download.
|
||||
|
||||
Args:
|
||||
dest: The filename where the resulting file should be saved.
|
||||
web_view: Specify the webview whose page should be loaded.
|
||||
tab: Specify the tab whose page should be loaded.
|
||||
"""
|
||||
# The default name is 'page title.mht'
|
||||
title = web_view.title()
|
||||
title = tab.title()
|
||||
default_name = utils.sanitize_filename(title + '.mht')
|
||||
|
||||
# Remove characters which cannot be expressed in the file system encoding
|
||||
@@ -524,12 +528,12 @@ def start_download_checked(dest, web_view):
|
||||
# saving the file anyway.
|
||||
if not os.path.isdir(os.path.dirname(path)):
|
||||
folder = os.path.dirname(path)
|
||||
message.error(web_view.win_id,
|
||||
message.error(tab.win_id,
|
||||
"Directory {} does not exist.".format(folder))
|
||||
return
|
||||
|
||||
if not os.path.isfile(path):
|
||||
_start_download(path, web_view=web_view)
|
||||
_start_download(path, tab=tab)
|
||||
return
|
||||
|
||||
q = usertypes.Question()
|
||||
@@ -537,7 +541,7 @@ def start_download_checked(dest, web_view):
|
||||
q.text = "{} exists. Overwrite?".format(path)
|
||||
q.completed.connect(q.deleteLater)
|
||||
q.answered_yes.connect(functools.partial(
|
||||
_start_download, path, web_view=web_view))
|
||||
_start_download, path, tab=tab))
|
||||
message_bridge = objreg.get('message-bridge', scope='window',
|
||||
window=web_view.win_id)
|
||||
window=tab.win_id)
|
||||
message_bridge.ask(q, blocking=False)
|
||||
@@ -25,7 +25,7 @@
|
||||
|
||||
import os
|
||||
|
||||
from qutebrowser.browser.network import schemehandler, networkreply
|
||||
from qutebrowser.browser.webkit.network import schemehandler, networkreply
|
||||
from qutebrowser.utils import jinja
|
||||
|
||||
|
||||
@@ -31,8 +31,8 @@ from PyQt5.QtNetwork import (QNetworkAccessManager, QNetworkReply, QSslError,
|
||||
from qutebrowser.config import config
|
||||
from qutebrowser.utils import (message, log, usertypes, utils, objreg, qtutils,
|
||||
urlutils, debug)
|
||||
from qutebrowser.browser.network import qutescheme, networkreply
|
||||
from qutebrowser.browser.network import filescheme
|
||||
from qutebrowser.browser.webkit.network import qutescheme, networkreply
|
||||
from qutebrowser.browser.webkit.network import filescheme
|
||||
|
||||
|
||||
HOSTBLOCK_ERROR_STRING = '%HOSTBLOCK%'
|
||||
@@ -228,9 +228,9 @@ class NetworkManager(QNetworkAccessManager):
|
||||
# This might be a generic network manager, e.g. one belonging to a
|
||||
# DownloadManager. In this case, just skip the webview thing.
|
||||
if self._tab_id is not None:
|
||||
webview = objreg.get('webview', scope='tab', window=self._win_id,
|
||||
tab=self._tab_id)
|
||||
webview.loadStarted.connect(q.abort)
|
||||
tab = objreg.get('tab', scope='tab', window=self._win_id,
|
||||
tab=self._tab_id)
|
||||
tab.load_started.connect(q.abort)
|
||||
bridge = objreg.get('message-bridge', scope='window',
|
||||
window=self._win_id)
|
||||
bridge.ask(q, blocking=True)
|
||||
@@ -479,9 +479,9 @@ class NetworkManager(QNetworkAccessManager):
|
||||
|
||||
if self._tab_id is not None:
|
||||
try:
|
||||
webview = objreg.get('webview', scope='tab',
|
||||
window=self._win_id, tab=self._tab_id)
|
||||
current_url = webview.url()
|
||||
tab = objreg.get('tab', scope='tab', window=self._win_id,
|
||||
tab=self._tab_id)
|
||||
current_url = tab.url()
|
||||
except (KeyError, RuntimeError, TypeError):
|
||||
# https://github.com/The-Compiler/qutebrowser/issues/889
|
||||
# Catching RuntimeError and TypeError because we could be in
|
||||
@@ -33,7 +33,7 @@ from PyQt5.QtNetwork import QNetworkReply
|
||||
|
||||
import qutebrowser
|
||||
from qutebrowser.browser import pdfjs
|
||||
from qutebrowser.browser.network import schemehandler, networkreply
|
||||
from qutebrowser.browser.webkit.network import schemehandler, networkreply
|
||||
from qutebrowser.utils import (version, utils, jinja, log, message, docutils,
|
||||
objreg)
|
||||
from qutebrowser.config import configexc, configdata
|
||||
@@ -89,7 +89,7 @@ class QuteSchemeHandler(schemehandler.SchemeHandler):
|
||||
"""
|
||||
path = request.url().path()
|
||||
host = request.url().host()
|
||||
# An url like "qute:foo" is split as "scheme:path", not "scheme:host".
|
||||
# A url like "qute:foo" is split as "scheme:path", not "scheme:host".
|
||||
log.misc.debug("url: {}, path: {}, host {}".format(
|
||||
request.url().toDisplayString(), path, host))
|
||||
try:
|
||||
@@ -123,9 +123,6 @@ class JSBridge(QObject):
|
||||
|
||||
"""Javascript-bridge for special qute:... pages."""
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
|
||||
@pyqtSlot(int, str, str, str)
|
||||
def set(self, win_id, sectname, optname, value):
|
||||
"""Slot to set a setting from qute:settings."""
|
||||
@@ -261,3 +258,18 @@ def qute_pdfjs(_win_id, request):
|
||||
"pdfjs resource requested but not found: {}".format(e.path))
|
||||
raise QuteSchemeError("Can't find pdfjs resource '{}'".format(e.path),
|
||||
QNetworkReply.ContentNotFoundError)
|
||||
|
||||
|
||||
@add_handler('bookmarks')
|
||||
def qute_bookmarks(_win_id, _request):
|
||||
"""Handler for qute:bookmarks. Display all quickmarks / bookmarks."""
|
||||
bookmarks = sorted(objreg.get('bookmark-manager').marks.items(),
|
||||
key=lambda x: x[1]) # Sort by title
|
||||
quickmarks = sorted(objreg.get('quickmark-manager').marks.items(),
|
||||
key=lambda x: x[0]) # Sort by name
|
||||
|
||||
html = jinja.render('bookmarks.html',
|
||||
title='Bookmarks',
|
||||
bookmarks=bookmarks,
|
||||
quickmarks=quickmarks)
|
||||
return html.encode('UTF-8', errors='xmlcharrefreplace')
|
||||
@@ -59,7 +59,7 @@ class TabHistoryItem:
|
||||
|
||||
|
||||
def _encode_url(url):
|
||||
"""Encode an QUrl suitable to pass to QWebHistory."""
|
||||
"""Encode a QUrl suitable to pass to QWebHistory."""
|
||||
data = bytes(QUrl.toPercentEncoding(url.toString(), b':/#?&+=@%*'))
|
||||
return data.decode('ascii')
|
||||
|
||||
@@ -83,7 +83,7 @@ class WebElementWrapper(collections.abc.MutableMapping):
|
||||
if elem.isNull():
|
||||
raise IsNullError('{} is a null element!'.format(elem))
|
||||
self._elem = elem
|
||||
for name in ('addClass', 'appendInside', 'appendOutside',
|
||||
for name in ['addClass', 'appendInside', 'appendOutside',
|
||||
'attributeNS', 'classes', 'clone', 'document',
|
||||
'encloseContentsWith', 'encloseWith',
|
||||
'evaluateJavaScript', 'findAll', 'findFirst',
|
||||
@@ -97,7 +97,7 @@ class WebElementWrapper(collections.abc.MutableMapping):
|
||||
'setInnerXml', 'setOuterXml', 'setPlainText',
|
||||
'setStyleProperty', 'styleProperty', 'tagName',
|
||||
'takeFromDocument', 'toInnerXml', 'toOuterXml',
|
||||
'toggleClass', 'webFrame', '__eq__', '__ne__'):
|
||||
'toggleClass', 'webFrame', '__eq__', '__ne__']:
|
||||
# We don't wrap some methods for which we have better alternatives:
|
||||
# - Mapping access for attributeNames/hasAttribute/setAttribute/
|
||||
# attribute/removeAttribute.
|
||||
@@ -194,7 +194,7 @@ class WebElementWrapper(collections.abc.MutableMapping):
|
||||
"""
|
||||
self._check_vanished()
|
||||
try:
|
||||
return self['contenteditable'].lower() not in ('false', 'inherit')
|
||||
return self['contenteditable'].lower() not in ['false', 'inherit']
|
||||
except KeyError:
|
||||
return False
|
||||
|
||||
@@ -269,7 +269,7 @@ class WebElementWrapper(collections.abc.MutableMapping):
|
||||
return self._is_editable_input()
|
||||
elif tag == 'textarea':
|
||||
return self.is_writable()
|
||||
elif tag in ('embed', 'applet'):
|
||||
elif tag in ['embed', 'applet']:
|
||||
# Flash/Java/...
|
||||
return config.get('input', 'insert-mode-on-plugins') and not strict
|
||||
elif tag == 'object':
|
||||
@@ -284,7 +284,7 @@ class WebElementWrapper(collections.abc.MutableMapping):
|
||||
self._check_vanished()
|
||||
roles = ('combobox', 'textbox')
|
||||
tag = self._elem.tagName().lower()
|
||||
return self.get('role', None) in roles or tag in ('input', 'textarea')
|
||||
return self.get('role', None) in roles or tag in ['input', 'textarea']
|
||||
|
||||
def remove_blank_target(self):
|
||||
"""Remove target from link."""
|
||||
589
qutebrowser/browser/webkit/webkittab.py
Normal file
589
qutebrowser/browser/webkit/webkittab.py
Normal file
@@ -0,0 +1,589 @@
|
||||
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
|
||||
|
||||
# Copyright 2016 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
|
||||
#
|
||||
# This file is part of qutebrowser.
|
||||
#
|
||||
# qutebrowser is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# qutebrowser is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""Wrapper over our (QtWebKit) WebView."""
|
||||
|
||||
import sys
|
||||
import functools
|
||||
import xml.etree.ElementTree
|
||||
|
||||
from PyQt5.QtCore import pyqtSlot, Qt, QEvent, QUrl, QPoint, QTimer
|
||||
from PyQt5.QtGui import QKeyEvent
|
||||
from PyQt5.QtWebKitWidgets import QWebPage
|
||||
from PyQt5.QtWebKit import QWebSettings
|
||||
from PyQt5.QtPrintSupport import QPrinter
|
||||
|
||||
from qutebrowser.browser import browsertab
|
||||
from qutebrowser.browser.webkit import webview, tabhistory
|
||||
from qutebrowser.utils import qtutils, objreg, usertypes, utils
|
||||
|
||||
|
||||
class WebKitPrinting(browsertab.AbstractPrinting):
|
||||
|
||||
"""QtWebKit implementations related to printing."""
|
||||
|
||||
def _do_check(self):
|
||||
if not qtutils.check_print_compat():
|
||||
# WORKAROUND (remove this when we bump the requirements to 5.3.0)
|
||||
raise browsertab.WebTabError(
|
||||
"Printing on Qt < 5.3.0 on Windows is broken, please upgrade!")
|
||||
|
||||
def check_pdf_support(self):
|
||||
self._do_check()
|
||||
|
||||
def check_printer_support(self):
|
||||
self._do_check()
|
||||
|
||||
def to_pdf(self, filename):
|
||||
printer = QPrinter()
|
||||
printer.setOutputFileName(filename)
|
||||
self.to_printer(printer)
|
||||
|
||||
def to_printer(self, printer):
|
||||
self._widget.print(printer)
|
||||
|
||||
|
||||
class WebKitSearch(browsertab.AbstractSearch):
|
||||
|
||||
"""QtWebKit implementations related to searching on the page."""
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self._flags = QWebPage.FindFlags(0)
|
||||
|
||||
def _call_cb(self, callback, found):
|
||||
"""Call the given callback if it's non-None.
|
||||
|
||||
Delays the call via a QTimer so the website is re-rendered in between.
|
||||
|
||||
Args:
|
||||
callback: What to call
|
||||
found: If the text was found
|
||||
"""
|
||||
if callback is not None:
|
||||
QTimer.singleShot(0, functools.partial(callback, found))
|
||||
|
||||
def clear(self):
|
||||
# We first clear the marked text, then the highlights
|
||||
self._widget.findText('')
|
||||
self._widget.findText('', QWebPage.HighlightAllOccurrences)
|
||||
|
||||
def search(self, text, *, ignore_case=False, reverse=False,
|
||||
result_cb=None):
|
||||
flags = QWebPage.FindWrapsAroundDocument
|
||||
if ignore_case == 'smart':
|
||||
if not text.islower():
|
||||
flags |= QWebPage.FindCaseSensitively
|
||||
elif not ignore_case:
|
||||
flags |= QWebPage.FindCaseSensitively
|
||||
if reverse:
|
||||
flags |= QWebPage.FindBackward
|
||||
# We actually search *twice* - once to highlight everything, then again
|
||||
# to get a mark so we can navigate.
|
||||
found = self._widget.findText(text, flags)
|
||||
self._widget.findText(text, flags | QWebPage.HighlightAllOccurrences)
|
||||
self.text = text
|
||||
self._flags = flags
|
||||
self._call_cb(result_cb, found)
|
||||
|
||||
def next_result(self, *, result_cb=None):
|
||||
found = self._widget.findText(self.text, self._flags)
|
||||
self._call_cb(result_cb, found)
|
||||
|
||||
def prev_result(self, *, result_cb=None):
|
||||
# The int() here makes sure we get a copy of the flags.
|
||||
flags = QWebPage.FindFlags(int(self._flags))
|
||||
if flags & QWebPage.FindBackward:
|
||||
flags &= ~QWebPage.FindBackward
|
||||
else:
|
||||
flags |= QWebPage.FindBackward
|
||||
found = self._widget.findText(self.text, flags)
|
||||
self._call_cb(result_cb, found)
|
||||
|
||||
|
||||
class WebKitCaret(browsertab.AbstractCaret):
|
||||
|
||||
"""QtWebKit implementations related to moving the cursor/selection."""
|
||||
|
||||
@pyqtSlot(usertypes.KeyMode)
|
||||
def _on_mode_entered(self, mode):
|
||||
if mode != usertypes.KeyMode.caret:
|
||||
return
|
||||
|
||||
settings = self._widget.settings()
|
||||
settings.setAttribute(QWebSettings.CaretBrowsingEnabled, True)
|
||||
self.selection_enabled = bool(self.selection())
|
||||
|
||||
if self._widget.isVisible():
|
||||
# Sometimes the caret isn't immediately visible, but unfocusing
|
||||
# and refocusing it fixes that.
|
||||
self._widget.clearFocus()
|
||||
self._widget.setFocus(Qt.OtherFocusReason)
|
||||
|
||||
# Move the caret to the first element in the viewport if there
|
||||
# isn't any text which is already selected.
|
||||
#
|
||||
# Note: We can't use hasSelection() here, as that's always
|
||||
# true in caret mode.
|
||||
if not self.selection():
|
||||
self._widget.page().currentFrame().evaluateJavaScript(
|
||||
utils.read_file('javascript/position_caret.js'))
|
||||
|
||||
@pyqtSlot()
|
||||
def _on_mode_left(self):
|
||||
settings = self._widget.settings()
|
||||
if settings.testAttribute(QWebSettings.CaretBrowsingEnabled):
|
||||
if self.selection_enabled and self._widget.hasSelection():
|
||||
# Remove selection if it exists
|
||||
self._widget.triggerPageAction(QWebPage.MoveToNextChar)
|
||||
settings.setAttribute(QWebSettings.CaretBrowsingEnabled, False)
|
||||
self.selection_enabled = False
|
||||
|
||||
def move_to_next_line(self, count=1):
|
||||
if not self.selection_enabled:
|
||||
act = QWebPage.MoveToNextLine
|
||||
else:
|
||||
act = QWebPage.SelectNextLine
|
||||
for _ in range(count):
|
||||
self._widget.triggerPageAction(act)
|
||||
|
||||
def move_to_prev_line(self, count=1):
|
||||
if not self.selection_enabled:
|
||||
act = QWebPage.MoveToPreviousLine
|
||||
else:
|
||||
act = QWebPage.SelectPreviousLine
|
||||
for _ in range(count):
|
||||
self._widget.triggerPageAction(act)
|
||||
|
||||
def move_to_next_char(self, count=1):
|
||||
if not self.selection_enabled:
|
||||
act = QWebPage.MoveToNextChar
|
||||
else:
|
||||
act = QWebPage.SelectNextChar
|
||||
for _ in range(count):
|
||||
self._widget.triggerPageAction(act)
|
||||
|
||||
def move_to_prev_char(self, count=1):
|
||||
if not self.selection_enabled:
|
||||
act = QWebPage.MoveToPreviousChar
|
||||
else:
|
||||
act = QWebPage.SelectPreviousChar
|
||||
for _ in range(count):
|
||||
self._widget.triggerPageAction(act)
|
||||
|
||||
def move_to_end_of_word(self, count=1):
|
||||
if not self.selection_enabled:
|
||||
act = [QWebPage.MoveToNextWord]
|
||||
if sys.platform == 'win32': # pragma: no cover
|
||||
act.append(QWebPage.MoveToPreviousChar)
|
||||
else:
|
||||
act = [QWebPage.SelectNextWord]
|
||||
if sys.platform == 'win32': # pragma: no cover
|
||||
act.append(QWebPage.SelectPreviousChar)
|
||||
for _ in range(count):
|
||||
for a in act:
|
||||
self._widget.triggerPageAction(a)
|
||||
|
||||
def move_to_next_word(self, count=1):
|
||||
if not self.selection_enabled:
|
||||
act = [QWebPage.MoveToNextWord]
|
||||
if sys.platform != 'win32': # pragma: no branch
|
||||
act.append(QWebPage.MoveToNextChar)
|
||||
else:
|
||||
act = [QWebPage.SelectNextWord]
|
||||
if sys.platform != 'win32': # pragma: no branch
|
||||
act.append(QWebPage.SelectNextChar)
|
||||
for _ in range(count):
|
||||
for a in act:
|
||||
self._widget.triggerPageAction(a)
|
||||
|
||||
def move_to_prev_word(self, count=1):
|
||||
if not self.selection_enabled:
|
||||
act = QWebPage.MoveToPreviousWord
|
||||
else:
|
||||
act = QWebPage.SelectPreviousWord
|
||||
for _ in range(count):
|
||||
self._widget.triggerPageAction(act)
|
||||
|
||||
def move_to_start_of_line(self):
|
||||
if not self.selection_enabled:
|
||||
act = QWebPage.MoveToStartOfLine
|
||||
else:
|
||||
act = QWebPage.SelectStartOfLine
|
||||
self._widget.triggerPageAction(act)
|
||||
|
||||
def move_to_end_of_line(self):
|
||||
if not self.selection_enabled:
|
||||
act = QWebPage.MoveToEndOfLine
|
||||
else:
|
||||
act = QWebPage.SelectEndOfLine
|
||||
self._widget.triggerPageAction(act)
|
||||
|
||||
def move_to_start_of_next_block(self, count=1):
|
||||
if not self.selection_enabled:
|
||||
act = [QWebPage.MoveToNextLine,
|
||||
QWebPage.MoveToStartOfBlock]
|
||||
else:
|
||||
act = [QWebPage.SelectNextLine,
|
||||
QWebPage.SelectStartOfBlock]
|
||||
for _ in range(count):
|
||||
for a in act:
|
||||
self._widget.triggerPageAction(a)
|
||||
|
||||
def move_to_start_of_prev_block(self, count=1):
|
||||
if not self.selection_enabled:
|
||||
act = [QWebPage.MoveToPreviousLine,
|
||||
QWebPage.MoveToStartOfBlock]
|
||||
else:
|
||||
act = [QWebPage.SelectPreviousLine,
|
||||
QWebPage.SelectStartOfBlock]
|
||||
for _ in range(count):
|
||||
for a in act:
|
||||
self._widget.triggerPageAction(a)
|
||||
|
||||
def move_to_end_of_next_block(self, count=1):
|
||||
if not self.selection_enabled:
|
||||
act = [QWebPage.MoveToNextLine,
|
||||
QWebPage.MoveToEndOfBlock]
|
||||
else:
|
||||
act = [QWebPage.SelectNextLine,
|
||||
QWebPage.SelectEndOfBlock]
|
||||
for _ in range(count):
|
||||
for a in act:
|
||||
self._widget.triggerPageAction(a)
|
||||
|
||||
def move_to_end_of_prev_block(self, count=1):
|
||||
if not self.selection_enabled:
|
||||
act = [QWebPage.MoveToPreviousLine, QWebPage.MoveToEndOfBlock]
|
||||
else:
|
||||
act = [QWebPage.SelectPreviousLine, QWebPage.SelectEndOfBlock]
|
||||
for _ in range(count):
|
||||
for a in act:
|
||||
self._widget.triggerPageAction(a)
|
||||
|
||||
def move_to_start_of_document(self):
|
||||
if not self.selection_enabled:
|
||||
act = QWebPage.MoveToStartOfDocument
|
||||
else:
|
||||
act = QWebPage.SelectStartOfDocument
|
||||
self._widget.triggerPageAction(act)
|
||||
|
||||
def move_to_end_of_document(self):
|
||||
if not self.selection_enabled:
|
||||
act = QWebPage.MoveToEndOfDocument
|
||||
else:
|
||||
act = QWebPage.SelectEndOfDocument
|
||||
self._widget.triggerPageAction(act)
|
||||
|
||||
def toggle_selection(self):
|
||||
self.selection_enabled = not self.selection_enabled
|
||||
mainwindow = objreg.get('main-window', scope='window',
|
||||
window=self._win_id)
|
||||
mainwindow.status.set_mode_active(usertypes.KeyMode.caret, True)
|
||||
|
||||
def drop_selection(self):
|
||||
self._widget.triggerPageAction(QWebPage.MoveToNextChar)
|
||||
|
||||
def has_selection(self):
|
||||
return self._widget.hasSelection()
|
||||
|
||||
def selection(self, html=False):
|
||||
if html:
|
||||
return self._widget.selectedHtml()
|
||||
return self._widget.selectedText()
|
||||
|
||||
def follow_selected(self, *, tab=False):
|
||||
if not self.has_selection():
|
||||
return
|
||||
if QWebSettings.globalSettings().testAttribute(
|
||||
QWebSettings.JavascriptEnabled):
|
||||
if tab:
|
||||
self._widget.page().open_target = usertypes.ClickTarget.tab
|
||||
self._tab.run_js_async(
|
||||
'window.getSelection().anchorNode.parentNode.click()')
|
||||
else:
|
||||
selection = self.selection(html=True)
|
||||
try:
|
||||
selected_element = xml.etree.ElementTree.fromstring(
|
||||
'<html>{}</html>'.format(selection)).find('a')
|
||||
except xml.etree.ElementTree.ParseError:
|
||||
raise browsertab.WebTabError('Could not parse selected '
|
||||
'element!')
|
||||
|
||||
if selected_element is not None:
|
||||
try:
|
||||
url = selected_element.attrib['href']
|
||||
except KeyError:
|
||||
raise browsertab.WebTabError('Anchor element without '
|
||||
'href!')
|
||||
url = self._tab.url().resolved(QUrl(url))
|
||||
if tab:
|
||||
self._tab.new_tab_requested.emit(url)
|
||||
else:
|
||||
self._tab.openurl(url)
|
||||
|
||||
|
||||
class WebKitZoom(browsertab.AbstractZoom):
|
||||
|
||||
"""QtWebKit implementations related to zooming."""
|
||||
|
||||
def _set_factor_internal(self, factor):
|
||||
self._widget.setZoomFactor(factor)
|
||||
|
||||
def factor(self):
|
||||
return self._widget.zoomFactor()
|
||||
|
||||
|
||||
class WebKitScroller(browsertab.AbstractScroller):
|
||||
|
||||
"""QtWebKit implementations related to scrolling."""
|
||||
|
||||
# FIXME:qtwebengine When to use the main frame, when the current one?
|
||||
|
||||
def pos_px(self):
|
||||
return self._widget.page().mainFrame().scrollPosition()
|
||||
|
||||
def pos_perc(self):
|
||||
return self._widget.scroll_pos
|
||||
|
||||
def to_point(self, point):
|
||||
self._widget.page().mainFrame().setScrollPosition(point)
|
||||
|
||||
def delta(self, x=0, y=0):
|
||||
qtutils.check_overflow(x, 'int')
|
||||
qtutils.check_overflow(y, 'int')
|
||||
self._widget.page().mainFrame().scroll(x, y)
|
||||
|
||||
def delta_page(self, x=0.0, y=0.0):
|
||||
if y.is_integer():
|
||||
y = int(y)
|
||||
if y == 0:
|
||||
pass
|
||||
elif y < 0:
|
||||
self.page_up(count=-y)
|
||||
elif y > 0:
|
||||
self.page_down(count=y)
|
||||
y = 0
|
||||
if x == 0 and y == 0:
|
||||
return
|
||||
size = self._widget.page().mainFrame().geometry()
|
||||
self.delta(x * size.width(), y * size.height())
|
||||
|
||||
def to_perc(self, x=None, y=None):
|
||||
if x is None and y == 0:
|
||||
self.top()
|
||||
elif x is None and y == 100:
|
||||
self.bottom()
|
||||
else:
|
||||
for val, orientation in [(x, Qt.Horizontal), (y, Qt.Vertical)]:
|
||||
if val is not None:
|
||||
val = qtutils.check_overflow(val, 'int', fatal=False)
|
||||
frame = self._widget.page().mainFrame()
|
||||
m = frame.scrollBarMaximum(orientation)
|
||||
if m == 0:
|
||||
continue
|
||||
frame.setScrollBarValue(orientation, int(m * val / 100))
|
||||
|
||||
def _key_press(self, key, count=1, getter_name=None, direction=None):
|
||||
frame = self._widget.page().mainFrame()
|
||||
press_evt = QKeyEvent(QEvent.KeyPress, key, Qt.NoModifier, 0, 0, 0)
|
||||
release_evt = QKeyEvent(QEvent.KeyRelease, key, Qt.NoModifier, 0, 0, 0)
|
||||
getter = None if getter_name is None else getattr(frame, getter_name)
|
||||
|
||||
# FIXME:qtwebengine needed?
|
||||
# self._widget.setFocus()
|
||||
|
||||
for _ in range(count):
|
||||
# Abort scrolling if the minimum/maximum was reached.
|
||||
if (getter is not None and
|
||||
frame.scrollBarValue(direction) == getter(direction)):
|
||||
return
|
||||
self._widget.keyPressEvent(press_evt)
|
||||
self._widget.keyReleaseEvent(release_evt)
|
||||
|
||||
def up(self, count=1):
|
||||
self._key_press(Qt.Key_Up, count, 'scrollBarMinimum', Qt.Vertical)
|
||||
|
||||
def down(self, count=1):
|
||||
self._key_press(Qt.Key_Down, count, 'scrollBarMaximum', Qt.Vertical)
|
||||
|
||||
def left(self, count=1):
|
||||
self._key_press(Qt.Key_Left, count, 'scrollBarMinimum', Qt.Horizontal)
|
||||
|
||||
def right(self, count=1):
|
||||
self._key_press(Qt.Key_Right, count, 'scrollBarMaximum', Qt.Horizontal)
|
||||
|
||||
def top(self):
|
||||
self._key_press(Qt.Key_Home)
|
||||
|
||||
def bottom(self):
|
||||
self._key_press(Qt.Key_End)
|
||||
|
||||
def page_up(self, count=1):
|
||||
self._key_press(Qt.Key_PageUp, count, 'scrollBarMinimum', Qt.Vertical)
|
||||
|
||||
def page_down(self, count=1):
|
||||
self._key_press(Qt.Key_PageDown, count, 'scrollBarMaximum',
|
||||
Qt.Vertical)
|
||||
|
||||
def at_top(self):
|
||||
return self.pos_px().y() == 0
|
||||
|
||||
def at_bottom(self):
|
||||
frame = self._widget.page().currentFrame()
|
||||
return self.pos_px().y() >= frame.scrollBarMaximum(Qt.Vertical)
|
||||
|
||||
|
||||
class WebKitHistory(browsertab.AbstractHistory):
|
||||
|
||||
"""QtWebKit implementations related to page history."""
|
||||
|
||||
def current_idx(self):
|
||||
return self._history.currentItemIndex()
|
||||
|
||||
def back(self):
|
||||
self._history.back()
|
||||
|
||||
def forward(self):
|
||||
self._history.forward()
|
||||
|
||||
def can_go_back(self):
|
||||
return self._history.canGoBack()
|
||||
|
||||
def can_go_forward(self):
|
||||
return self._history.canGoForward()
|
||||
|
||||
def serialize(self):
|
||||
return qtutils.serialize(self._history)
|
||||
|
||||
def deserialize(self, data):
|
||||
return qtutils.deserialize(data, self._history)
|
||||
|
||||
def load_items(self, items):
|
||||
stream, _data, user_data = tabhistory.serialize(items)
|
||||
qtutils.deserialize_stream(stream, self._history)
|
||||
for i, data in enumerate(user_data):
|
||||
self._history.itemAt(i).setUserData(data)
|
||||
cur_data = self._history.currentItem().userData()
|
||||
if cur_data is not None:
|
||||
if 'zoom' in cur_data:
|
||||
self._tab.zoom.set_factor(cur_data['zoom'])
|
||||
if ('scroll-pos' in cur_data and
|
||||
self._tab.scroller.pos_px() == QPoint(0, 0)):
|
||||
QTimer.singleShot(0, functools.partial(
|
||||
self._tab.scroller.to_point, cur_data['scroll-pos']))
|
||||
|
||||
|
||||
class WebKitTab(browsertab.AbstractTab):
|
||||
|
||||
"""A QtWebKit tab in the browser."""
|
||||
|
||||
def __init__(self, win_id, mode_manager, parent=None):
|
||||
super().__init__(win_id)
|
||||
widget = webview.WebView(win_id, self.tab_id, tab=self)
|
||||
self.history = WebKitHistory(self)
|
||||
self.scroller = WebKitScroller(self, parent=self)
|
||||
self.caret = WebKitCaret(win_id=win_id, mode_manager=mode_manager,
|
||||
tab=self, parent=self)
|
||||
self.zoom = WebKitZoom(win_id=win_id, parent=self)
|
||||
self.search = WebKitSearch(parent=self)
|
||||
self.printing = WebKitPrinting()
|
||||
self._set_widget(widget)
|
||||
self._connect_signals()
|
||||
self.zoom.set_default()
|
||||
self.backend = usertypes.Backend.QtWebKit
|
||||
|
||||
def openurl(self, url):
|
||||
self._openurl_prepare(url)
|
||||
self._widget.openurl(url)
|
||||
|
||||
def url(self):
|
||||
return self._widget.url()
|
||||
|
||||
def dump_async(self, callback, *, plain=False):
|
||||
frame = self._widget.page().mainFrame()
|
||||
if plain:
|
||||
callback(frame.toPlainText())
|
||||
else:
|
||||
callback(frame.toHtml())
|
||||
|
||||
def run_js_async(self, code, callback=None):
|
||||
result = self.run_js_blocking(code)
|
||||
if callback is not None:
|
||||
callback(result)
|
||||
|
||||
def run_js_blocking(self, code):
|
||||
return self._widget.page().mainFrame().evaluateJavaScript(code)
|
||||
|
||||
def icon(self):
|
||||
return self._widget.icon()
|
||||
|
||||
def shutdown(self):
|
||||
self._widget.shutdown()
|
||||
|
||||
def reload(self, *, force=False):
|
||||
if force:
|
||||
action = QWebPage.ReloadAndBypassCache
|
||||
else:
|
||||
action = QWebPage.Reload
|
||||
self._widget.triggerPageAction(action)
|
||||
|
||||
def stop(self):
|
||||
self._widget.stop()
|
||||
|
||||
def title(self):
|
||||
return self._widget.title()
|
||||
|
||||
def clear_ssl_errors(self):
|
||||
nam = self._widget.page().networkAccessManager()
|
||||
nam.clear_all_ssl_errors()
|
||||
|
||||
def set_html(self, html, base_url):
|
||||
self._widget.setHtml(html, base_url)
|
||||
|
||||
@pyqtSlot()
|
||||
def _on_frame_load_finished(self):
|
||||
"""Make sure we emit an appropriate status when loading finished.
|
||||
|
||||
While Qt has a bool "ok" attribute for loadFinished, it always is True
|
||||
when using error pages... See
|
||||
https://github.com/The-Compiler/qutebrowser/issues/84
|
||||
"""
|
||||
self._on_load_finished(not self._widget.page().error_occurred)
|
||||
|
||||
@pyqtSlot()
|
||||
def _on_webkit_icon_changed(self):
|
||||
"""Emit iconChanged with a QIcon like QWebEngineView does."""
|
||||
self.icon_changed.emit(self._widget.icon())
|
||||
|
||||
def _connect_signals(self):
|
||||
view = self._widget
|
||||
page = view.page()
|
||||
frame = page.mainFrame()
|
||||
page.windowCloseRequested.connect(self.window_close_requested)
|
||||
page.linkHovered.connect(self.link_hovered)
|
||||
page.loadProgress.connect(self._on_load_progress)
|
||||
frame.loadStarted.connect(self._on_load_started)
|
||||
view.scroll_pos_changed.connect(self.scroller.perc_changed)
|
||||
view.titleChanged.connect(self.title_changed)
|
||||
view.urlChanged.connect(self._on_url_changed)
|
||||
view.shutting_down.connect(self.shutting_down)
|
||||
page.networkAccessManager().sslErrors.connect(self._on_ssl_errors)
|
||||
frame.loadFinished.connect(self._on_frame_load_finished)
|
||||
view.iconChanged.connect(self._on_webkit_icon_changed)
|
||||
@@ -21,8 +21,7 @@
|
||||
|
||||
import functools
|
||||
|
||||
from PyQt5.QtCore import (pyqtSlot, pyqtSignal, PYQT_VERSION, Qt, QUrl, QPoint,
|
||||
QTimer)
|
||||
from PyQt5.QtCore import pyqtSlot, pyqtSignal, PYQT_VERSION, Qt, QUrl, QPoint
|
||||
from PyQt5.QtGui import QDesktopServices
|
||||
from PyQt5.QtNetwork import QNetworkReply, QNetworkRequest
|
||||
from PyQt5.QtWidgets import QFileDialog
|
||||
@@ -30,8 +29,9 @@ from PyQt5.QtPrintSupport import QPrintDialog
|
||||
from PyQt5.QtWebKitWidgets import QWebPage
|
||||
|
||||
from qutebrowser.config import config
|
||||
from qutebrowser.browser import http, tabhistory, pdfjs
|
||||
from qutebrowser.browser.network import networkmanager
|
||||
from qutebrowser.browser import pdfjs
|
||||
from qutebrowser.browser.webkit import http
|
||||
from qutebrowser.browser.webkit.network import networkmanager
|
||||
from qutebrowser.utils import (message, usertypes, log, jinja, qtutils, utils,
|
||||
objreg, debug, urlutils)
|
||||
|
||||
@@ -242,23 +242,6 @@ class BrowserPage(QWebPage):
|
||||
else:
|
||||
nam.shutdown()
|
||||
|
||||
def load_history(self, entries):
|
||||
"""Load the history from a list of TabHistoryItem objects."""
|
||||
stream, _data, user_data = tabhistory.serialize(entries)
|
||||
history = self.history()
|
||||
qtutils.deserialize_stream(stream, history)
|
||||
for i, data in enumerate(user_data):
|
||||
history.itemAt(i).setUserData(data)
|
||||
cur_data = history.currentItem().userData()
|
||||
if cur_data is not None:
|
||||
frame = self.mainFrame()
|
||||
if 'zoom' in cur_data:
|
||||
frame.page().view().zoom_perc(cur_data['zoom'] * 100)
|
||||
if ('scroll-pos' in cur_data and
|
||||
frame.scrollPosition() == QPoint(0, 0)):
|
||||
QTimer.singleShot(0, functools.partial(
|
||||
frame.setScrollPosition, cur_data['scroll-pos']))
|
||||
|
||||
def display_content(self, reply, mimetype):
|
||||
"""Display a QNetworkReply with an explicitly set mimetype."""
|
||||
self.mainFrame().setContent(reply.readAll(), mimetype, reply.url())
|
||||
@@ -317,7 +300,7 @@ class BrowserPage(QWebPage):
|
||||
else:
|
||||
reply.finished.connect(functools.partial(
|
||||
self.display_content, reply, 'image/jpeg'))
|
||||
elif (mimetype in {'application/pdf', 'application/x-pdf'} and
|
||||
elif (mimetype in ['application/pdf', 'application/x-pdf'] and
|
||||
config.get('content', 'enable-pdfjs')):
|
||||
# Use pdf.js to display the page
|
||||
self._show_pdfjs(reply)
|
||||
@@ -435,7 +418,7 @@ class BrowserPage(QWebPage):
|
||||
if data is None:
|
||||
return
|
||||
if 'zoom' in data:
|
||||
frame.page().view().zoom_perc(data['zoom'] * 100)
|
||||
frame.page().view().tab.zoom.set_factor(data['zoom'])
|
||||
if 'scroll-pos' in data and frame.scrollPosition() == QPoint(0, 0):
|
||||
frame.setScrollPosition(data['scroll-pos'])
|
||||
|
||||
@@ -20,10 +20,8 @@
|
||||
"""The main browser widgets."""
|
||||
|
||||
import sys
|
||||
import itertools
|
||||
import functools
|
||||
|
||||
from PyQt5.QtCore import pyqtSignal, pyqtSlot, Qt, QTimer, QUrl
|
||||
from PyQt5.QtCore import pyqtSignal, pyqtSlot, Qt, QTimer, QUrl, QPoint
|
||||
from PyQt5.QtGui import QPalette
|
||||
from PyQt5.QtWidgets import QApplication, QStyleFactory
|
||||
from PyQt5.QtWebKit import QWebSettings
|
||||
@@ -32,44 +30,23 @@ from PyQt5.QtWebKitWidgets import QWebView, QWebPage, QWebFrame
|
||||
from qutebrowser.config import config
|
||||
from qutebrowser.keyinput import modeman
|
||||
from qutebrowser.utils import message, log, usertypes, utils, qtutils, objreg
|
||||
from qutebrowser.browser import webpage, hints, webelem
|
||||
|
||||
|
||||
LoadStatus = usertypes.enum('LoadStatus', ['none', 'success', 'success_https',
|
||||
'error', 'warn', 'loading'])
|
||||
|
||||
|
||||
tab_id_gen = itertools.count(0)
|
||||
from qutebrowser.browser import hints
|
||||
from qutebrowser.browser.webkit import webpage, webelem
|
||||
|
||||
|
||||
class WebView(QWebView):
|
||||
|
||||
"""One browser tab in TabbedBrowser.
|
||||
|
||||
Our own subclass of a QWebView with some added bells and whistles.
|
||||
"""Custom QWebView subclass with qutebrowser-specific features.
|
||||
|
||||
Attributes:
|
||||
tab: The WebKitTab object for this WebView
|
||||
hintmanager: The HintManager instance for this view.
|
||||
progress: loading progress of this page.
|
||||
scroll_pos: The current scroll position as (x%, y%) tuple.
|
||||
statusbar_message: The current javascript statusbar message.
|
||||
inspector: The QWebInspector used for this webview.
|
||||
load_status: loading status of this page (index into LoadStatus)
|
||||
viewing_source: Whether the webview is currently displaying source
|
||||
code.
|
||||
keep_icon: Whether the (e.g. cloned) icon should not be cleared on page
|
||||
load.
|
||||
registry: The ObjectRegistry associated with this tab.
|
||||
tab_id: The tab ID of the view.
|
||||
win_id: The window ID of the view.
|
||||
search_text: The text of the last search.
|
||||
search_flags: The search flags of the last search.
|
||||
_has_ssl_errors: Whether SSL errors occurred during loading.
|
||||
_zoom: A NeighborList with the zoom levels.
|
||||
_tab_id: The tab ID of the view.
|
||||
_old_scroll_pos: The old scroll position.
|
||||
_check_insertmode: If True, in mouseReleaseEvent we should check if we
|
||||
need to enter/leave insert mode.
|
||||
_default_zoom_changed: Whether the zoom was changed from the default.
|
||||
_ignore_wheel_event: Ignore the next wheel event.
|
||||
See https://github.com/The-Compiler/qutebrowser/issues/395
|
||||
|
||||
@@ -77,72 +54,44 @@ class WebView(QWebView):
|
||||
scroll_pos_changed: Scroll percentage of current tab changed.
|
||||
arg 1: x-position in %.
|
||||
arg 2: y-position in %.
|
||||
linkHovered: QWebPages linkHovered signal exposed.
|
||||
load_status_changed: The loading status changed
|
||||
url_text_changed: Current URL string changed.
|
||||
mouse_wheel_zoom: Emitted when the page should be zoomed because the
|
||||
mousewheel was used with ctrl.
|
||||
arg 1: The angle delta of the wheel event (QPoint)
|
||||
shutting_down: Emitted when the view is shutting down.
|
||||
"""
|
||||
|
||||
scroll_pos_changed = pyqtSignal(int, int)
|
||||
linkHovered = pyqtSignal(str, str, str)
|
||||
load_status_changed = pyqtSignal(str)
|
||||
url_text_changed = pyqtSignal(str)
|
||||
shutting_down = pyqtSignal()
|
||||
mouse_wheel_zoom = pyqtSignal(QPoint)
|
||||
|
||||
def __init__(self, win_id, parent=None):
|
||||
def __init__(self, win_id, tab_id, tab, parent=None):
|
||||
super().__init__(parent)
|
||||
if sys.platform == 'darwin' and qtutils.version_check('5.4'):
|
||||
# WORKAROUND for https://bugreports.qt.io/browse/QTBUG-42948
|
||||
# See https://github.com/The-Compiler/qutebrowser/issues/462
|
||||
self.setStyle(QStyleFactory.create('Fusion'))
|
||||
self.tab = tab
|
||||
self.win_id = win_id
|
||||
self.load_status = LoadStatus.none
|
||||
self._check_insertmode = False
|
||||
self.inspector = None
|
||||
self.scroll_pos = (-1, -1)
|
||||
self.statusbar_message = ''
|
||||
self._old_scroll_pos = (-1, -1)
|
||||
self._zoom = None
|
||||
self._has_ssl_errors = False
|
||||
self._ignore_wheel_event = False
|
||||
self.keep_icon = False
|
||||
self.search_text = None
|
||||
self.search_flags = 0
|
||||
self.selection_enabled = False
|
||||
self.init_neighborlist()
|
||||
self._set_bg_color()
|
||||
cfg = objreg.get('config')
|
||||
cfg.changed.connect(self.init_neighborlist)
|
||||
# For some reason, this signal doesn't get disconnected automatically
|
||||
# when the WebView is destroyed on older PyQt versions.
|
||||
# See https://github.com/The-Compiler/qutebrowser/issues/390
|
||||
self.destroyed.connect(functools.partial(
|
||||
cfg.changed.disconnect, self.init_neighborlist))
|
||||
self.cur_url = QUrl()
|
||||
self.progress = 0
|
||||
self.registry = objreg.ObjectRegistry()
|
||||
self.tab_id = next(tab_id_gen)
|
||||
tab_registry = objreg.get('tab-registry', scope='window',
|
||||
window=win_id)
|
||||
tab_registry[self.tab_id] = self
|
||||
objreg.register('webview', self, registry=self.registry)
|
||||
self._tab_id = tab_id
|
||||
|
||||
page = self._init_page()
|
||||
hintmanager = hints.HintManager(win_id, self.tab_id, self)
|
||||
hintmanager = hints.HintManager(win_id, self._tab_id, self)
|
||||
hintmanager.mouse_event.connect(self.on_mouse_event)
|
||||
hintmanager.start_hinting.connect(page.on_start_hinting)
|
||||
hintmanager.stop_hinting.connect(page.on_stop_hinting)
|
||||
objreg.register('hintmanager', hintmanager, registry=self.registry)
|
||||
objreg.register('hintmanager', hintmanager, scope='tab', window=win_id,
|
||||
tab=tab_id)
|
||||
mode_manager = objreg.get('mode-manager', scope='window',
|
||||
window=win_id)
|
||||
mode_manager.entered.connect(self.on_mode_entered)
|
||||
mode_manager.left.connect(self.on_mode_left)
|
||||
self.viewing_source = False
|
||||
self.setZoomFactor(float(config.get('ui', 'default-zoom')) / 100)
|
||||
self._default_zoom_changed = False
|
||||
if config.get('input', 'rocker-gestures'):
|
||||
self.setContextMenuPolicy(Qt.PreventContextMenu)
|
||||
self.urlChanged.connect(self.on_url_changed)
|
||||
self.loadProgress.connect(lambda p: setattr(self, 'progress', p))
|
||||
objreg.get('config').changed.connect(self.on_config_changed)
|
||||
|
||||
@pyqtSlot()
|
||||
@@ -152,30 +101,24 @@ class WebView(QWebView):
|
||||
no_formatting = QUrl.UrlFormattingOption(0)
|
||||
orig_url = self.page().mainFrame().requestedUrl()
|
||||
if (orig_url.isValid() and
|
||||
not orig_url.matches(self.cur_url, no_formatting)):
|
||||
not orig_url.matches(self.url(), no_formatting)):
|
||||
# If the url of the page is different than the url of the link
|
||||
# originally clicked, save them both.
|
||||
history.add_url(orig_url, self.title(), redirect=True)
|
||||
history.add_url(self.cur_url, self.title())
|
||||
history.add_url(self.url(), self.title())
|
||||
|
||||
def _init_page(self):
|
||||
"""Initialize the QWebPage used by this view."""
|
||||
page = webpage.BrowserPage(self.win_id, self.tab_id, self)
|
||||
page = webpage.BrowserPage(self.win_id, self._tab_id, self)
|
||||
self.setPage(page)
|
||||
page.linkHovered.connect(self.linkHovered)
|
||||
page.mainFrame().loadStarted.connect(self.on_load_started)
|
||||
page.mainFrame().loadFinished.connect(self.on_load_finished)
|
||||
page.mainFrame().initialLayoutCompleted.connect(
|
||||
self.on_initial_layout_completed)
|
||||
page.statusBarMessage.connect(
|
||||
lambda msg: setattr(self, 'statusbar_message', msg))
|
||||
page.networkAccessManager().sslErrors.connect(
|
||||
lambda *args: setattr(self, '_has_ssl_errors', True))
|
||||
return page
|
||||
|
||||
def __repr__(self):
|
||||
url = utils.elide(self.url().toDisplayString(QUrl.EncodeUnicode), 100)
|
||||
return utils.get_repr(self, tab_id=self.tab_id, url=url)
|
||||
return utils.get_repr(self, tab_id=self._tab_id, url=url)
|
||||
|
||||
def __del__(self):
|
||||
# Explicitly releasing the page here seems to prevent some segfaults
|
||||
@@ -190,14 +133,6 @@ class WebView(QWebView):
|
||||
# deleted
|
||||
pass
|
||||
|
||||
def _set_load_status(self, val):
|
||||
"""Setter for load_status."""
|
||||
if not isinstance(val, LoadStatus):
|
||||
raise TypeError("Type {} is no LoadStatus member!".format(val))
|
||||
log.webview.debug("load status for {}: {}".format(repr(self), val))
|
||||
self.load_status = val
|
||||
self.load_status_changed.emit(val.name)
|
||||
|
||||
def _set_bg_color(self):
|
||||
"""Set the webpage background color as configured."""
|
||||
col = config.get('colors', 'webpage.bg')
|
||||
@@ -209,14 +144,8 @@ class WebView(QWebView):
|
||||
|
||||
@pyqtSlot(str, str)
|
||||
def on_config_changed(self, section, option):
|
||||
"""Reinitialize the zoom neighborlist if related config changed."""
|
||||
if section == 'ui' and option in ('zoom-levels', 'default-zoom'):
|
||||
if not self._default_zoom_changed:
|
||||
self.setZoomFactor(float(config.get('ui', 'default-zoom')) /
|
||||
100)
|
||||
self._default_zoom_changed = False
|
||||
self.init_neighborlist()
|
||||
elif section == 'input' and option == 'rocker-gestures':
|
||||
"""Update rocker gestures/background color."""
|
||||
if section == 'input' and option == 'rocker-gestures':
|
||||
if config.get('input', 'rocker-gestures'):
|
||||
self.setContextMenuPolicy(Qt.PreventContextMenu)
|
||||
else:
|
||||
@@ -224,27 +153,20 @@ class WebView(QWebView):
|
||||
elif section == 'colors' and option == 'webpage.bg':
|
||||
self._set_bg_color()
|
||||
|
||||
def init_neighborlist(self):
|
||||
"""Initialize the _zoom neighborlist."""
|
||||
levels = config.get('ui', 'zoom-levels')
|
||||
self._zoom = usertypes.NeighborList(
|
||||
levels, mode=usertypes.NeighborList.Modes.edge)
|
||||
self._zoom.fuzzyval = config.get('ui', 'default-zoom')
|
||||
|
||||
def _mousepress_backforward(self, e):
|
||||
"""Handle back/forward mouse button presses.
|
||||
|
||||
Args:
|
||||
e: The QMouseEvent.
|
||||
"""
|
||||
if e.button() in (Qt.XButton1, Qt.LeftButton):
|
||||
if e.button() in [Qt.XButton1, Qt.LeftButton]:
|
||||
# Back button on mice which have it, or rocker gesture
|
||||
if self.page().history().canGoBack():
|
||||
self.back()
|
||||
else:
|
||||
message.error(self.win_id, "At beginning of history.",
|
||||
immediately=True)
|
||||
elif e.button() in (Qt.XButton2, Qt.RightButton):
|
||||
elif e.button() in [Qt.XButton2, Qt.RightButton]:
|
||||
# Forward button on mice which have it, or rocker gesture
|
||||
if self.page().history().canGoForward():
|
||||
self.forward()
|
||||
@@ -356,12 +278,6 @@ class WebView(QWebView):
|
||||
Args:
|
||||
url: The URL to load as QUrl
|
||||
"""
|
||||
qtutils.ensure_valid(url)
|
||||
urlstr = url.toDisplayString()
|
||||
log.webview.debug("New title: {}".format(urlstr))
|
||||
self.titleChanged.emit(urlstr)
|
||||
self.cur_url = url
|
||||
self.url_text_changed.emit(url.toDisplayString())
|
||||
self.load(url)
|
||||
if url.scheme() == 'qute':
|
||||
frame = self.page().mainFrame()
|
||||
@@ -380,45 +296,6 @@ class WebView(QWebView):
|
||||
bridge = objreg.get('js-bridge')
|
||||
frame.addToJavaScriptWindowObject('qute', bridge)
|
||||
|
||||
def zoom_perc(self, perc, fuzzyval=True):
|
||||
"""Zoom to a given zoom percentage.
|
||||
|
||||
Args:
|
||||
perc: The zoom percentage as int.
|
||||
fuzzyval: Whether to set the NeighborLists fuzzyval.
|
||||
"""
|
||||
if fuzzyval:
|
||||
self._zoom.fuzzyval = int(perc)
|
||||
if perc < 0:
|
||||
raise ValueError("Can't zoom {}%!".format(perc))
|
||||
self.setZoomFactor(float(perc) / 100)
|
||||
self._default_zoom_changed = True
|
||||
|
||||
def zoom(self, offset):
|
||||
"""Increase/Decrease the zoom level.
|
||||
|
||||
Args:
|
||||
offset: The offset in the zoom level list.
|
||||
|
||||
Return:
|
||||
The new zoom percentage.
|
||||
"""
|
||||
level = self._zoom.getitem(offset)
|
||||
self.zoom_perc(level, fuzzyval=False)
|
||||
return level
|
||||
|
||||
@pyqtSlot('QUrl')
|
||||
def on_url_changed(self, url):
|
||||
"""Update cur_url when URL has changed.
|
||||
|
||||
If the URL is invalid, we just ignore it here.
|
||||
"""
|
||||
if url.isValid():
|
||||
self.cur_url = url
|
||||
self.url_text_changed.emit(url.toDisplayString())
|
||||
if not self.title():
|
||||
self.titleChanged.emit(self.url().toDisplayString())
|
||||
|
||||
@pyqtSlot('QMouseEvent')
|
||||
def on_mouse_event(self, evt):
|
||||
"""Post a new mouse event from a hintmanager."""
|
||||
@@ -426,14 +303,6 @@ class WebView(QWebView):
|
||||
self.setFocus()
|
||||
QApplication.postEvent(self, evt)
|
||||
|
||||
@pyqtSlot()
|
||||
def on_load_started(self):
|
||||
"""Leave insert/hint mode and set vars when a new page is loading."""
|
||||
self.progress = 0
|
||||
self.viewing_source = False
|
||||
self._has_ssl_errors = False
|
||||
self._set_load_status(LoadStatus.loading)
|
||||
|
||||
@pyqtSlot()
|
||||
def on_load_finished(self):
|
||||
"""Handle a finished page load.
|
||||
@@ -443,18 +312,6 @@ class WebView(QWebView):
|
||||
See https://github.com/The-Compiler/qutebrowser/issues/84
|
||||
"""
|
||||
ok = not self.page().error_occurred
|
||||
if ok and not self._has_ssl_errors:
|
||||
if self.cur_url.scheme() == 'https':
|
||||
self._set_load_status(LoadStatus.success_https)
|
||||
else:
|
||||
self._set_load_status(LoadStatus.success)
|
||||
|
||||
elif ok:
|
||||
self._set_load_status(LoadStatus.warn)
|
||||
else:
|
||||
self._set_load_status(LoadStatus.error)
|
||||
if not self.title():
|
||||
self.titleChanged.emit(self.url().toDisplayString())
|
||||
self._handle_auto_insert_mode(ok)
|
||||
|
||||
def _handle_auto_insert_mode(self, ok):
|
||||
@@ -480,91 +337,21 @@ class WebView(QWebView):
|
||||
@pyqtSlot(usertypes.KeyMode)
|
||||
def on_mode_entered(self, mode):
|
||||
"""Ignore attempts to focus the widget if in any status-input mode."""
|
||||
if mode in (usertypes.KeyMode.command, usertypes.KeyMode.prompt,
|
||||
usertypes.KeyMode.yesno):
|
||||
if mode in [usertypes.KeyMode.command, usertypes.KeyMode.prompt,
|
||||
usertypes.KeyMode.yesno]:
|
||||
log.webview.debug("Ignoring focus because mode {} was "
|
||||
"entered.".format(mode))
|
||||
self.setFocusPolicy(Qt.NoFocus)
|
||||
elif mode == usertypes.KeyMode.caret:
|
||||
settings = self.settings()
|
||||
settings.setAttribute(QWebSettings.CaretBrowsingEnabled, True)
|
||||
self.selection_enabled = bool(self.page().selectedText())
|
||||
|
||||
if self.isVisible():
|
||||
# Sometimes the caret isn't immediately visible, but unfocusing
|
||||
# and refocusing it fixes that.
|
||||
self.clearFocus()
|
||||
self.setFocus(Qt.OtherFocusReason)
|
||||
|
||||
# Move the caret to the first element in the viewport if there
|
||||
# isn't any text which is already selected.
|
||||
#
|
||||
# Note: We can't use hasSelection() here, as that's always
|
||||
# true in caret mode.
|
||||
if not self.page().selectedText():
|
||||
self.page().currentFrame().evaluateJavaScript(
|
||||
utils.read_file('javascript/position_caret.js'))
|
||||
|
||||
@pyqtSlot(usertypes.KeyMode)
|
||||
def on_mode_left(self, mode):
|
||||
"""Restore focus policy if status-input modes were left."""
|
||||
if mode in (usertypes.KeyMode.command, usertypes.KeyMode.prompt,
|
||||
usertypes.KeyMode.yesno):
|
||||
if mode in [usertypes.KeyMode.command, usertypes.KeyMode.prompt,
|
||||
usertypes.KeyMode.yesno]:
|
||||
log.webview.debug("Restoring focus policy because mode {} was "
|
||||
"left.".format(mode))
|
||||
elif mode == usertypes.KeyMode.caret:
|
||||
settings = self.settings()
|
||||
if settings.testAttribute(QWebSettings.CaretBrowsingEnabled):
|
||||
if self.selection_enabled and self.hasSelection():
|
||||
# Remove selection if it exists
|
||||
self.triggerPageAction(QWebPage.MoveToNextChar)
|
||||
settings.setAttribute(QWebSettings.CaretBrowsingEnabled, False)
|
||||
self.selection_enabled = False
|
||||
|
||||
self.setFocusPolicy(Qt.WheelFocus)
|
||||
|
||||
def search(self, text, flags):
|
||||
"""Search for text in the current page.
|
||||
|
||||
Args:
|
||||
text: The text to search for.
|
||||
flags: The QWebPage::FindFlags.
|
||||
"""
|
||||
log.webview.debug("Searching with text '{}' and flags "
|
||||
"0x{:04x}.".format(text, int(flags)))
|
||||
old_scroll_pos = self.scroll_pos
|
||||
flags = QWebPage.FindFlags(flags)
|
||||
found = self.findText(text, flags)
|
||||
backward = flags & QWebPage.FindBackward
|
||||
|
||||
if not found and not flags & QWebPage.HighlightAllOccurrences and text:
|
||||
# User disabled wrapping; but findText() just returns False. If we
|
||||
# have a selection, we know there's a match *somewhere* on the page
|
||||
if (not flags & QWebPage.FindWrapsAroundDocument and
|
||||
self.hasSelection()):
|
||||
if not backward:
|
||||
message.warning(self.win_id, "Search hit BOTTOM without "
|
||||
"match for: {}".format(text),
|
||||
immediately=True)
|
||||
else:
|
||||
message.warning(self.win_id, "Search hit TOP without "
|
||||
"match for: {}".format(text),
|
||||
immediately=True)
|
||||
else:
|
||||
message.warning(self.win_id, "Text '{}' not found on "
|
||||
"page!".format(text), immediately=True)
|
||||
else:
|
||||
def check_scroll_pos():
|
||||
"""Check if the scroll position got smaller and show info."""
|
||||
if not backward and self.scroll_pos < old_scroll_pos:
|
||||
message.info(self.win_id, "Search hit BOTTOM, continuing "
|
||||
"at TOP", immediately=True)
|
||||
elif backward and self.scroll_pos > old_scroll_pos:
|
||||
message.info(self.win_id, "Search hit TOP, continuing at "
|
||||
"BOTTOM", immediately=True)
|
||||
# We first want QWebPage to refresh.
|
||||
QTimer.singleShot(0, check_scroll_pos)
|
||||
|
||||
def createWindow(self, wintype):
|
||||
"""Called by Qt when a page wants to create a new window.
|
||||
|
||||
@@ -589,7 +376,8 @@ class WebView(QWebView):
|
||||
"support that!")
|
||||
tabbed_browser = objreg.get('tabbed-browser', scope='window',
|
||||
window=self.win_id)
|
||||
return tabbed_browser.tabopen(background=False)
|
||||
# pylint: disable=protected-access
|
||||
return tabbed_browser.tabopen(background=False)._widget
|
||||
|
||||
def paintEvent(self, e):
|
||||
"""Extend paintEvent to emit a signal if the scroll position changed.
|
||||
@@ -636,7 +424,7 @@ class WebView(QWebView):
|
||||
is_rocker_gesture = (config.get('input', 'rocker-gestures') and
|
||||
e.buttons() == Qt.LeftButton | Qt.RightButton)
|
||||
|
||||
if e.button() in (Qt.XButton1, Qt.XButton2) or is_rocker_gesture:
|
||||
if e.button() in [Qt.XButton1, Qt.XButton2] or is_rocker_gesture:
|
||||
self._mousepress_backforward(e)
|
||||
super().mousePressEvent(e)
|
||||
return
|
||||
@@ -671,14 +459,6 @@ class WebView(QWebView):
|
||||
return
|
||||
if e.modifiers() & Qt.ControlModifier:
|
||||
e.accept()
|
||||
divider = config.get('input', 'mouse-zoom-divider')
|
||||
factor = self.zoomFactor() + e.angleDelta().y() / divider
|
||||
if factor < 0:
|
||||
return
|
||||
perc = int(100 * factor)
|
||||
message.info(self.win_id, "Zoom level: {}%".format(perc))
|
||||
self._zoom.fuzzyval = perc
|
||||
self.setZoomFactor(factor)
|
||||
self._default_zoom_changed = True
|
||||
self.mouse_wheel_zoom.emit(e.angleDelta())
|
||||
else:
|
||||
super().wheelEvent(e)
|
||||
@@ -25,7 +25,7 @@ Defined here to avoid circular dependency hell.
|
||||
|
||||
class CommandError(Exception):
|
||||
|
||||
"""Raised when a command encounters a error while running."""
|
||||
"""Raised when a command encounters an error while running."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
@@ -80,6 +80,8 @@ class Command:
|
||||
parser: The ArgumentParser to use to parse this command.
|
||||
flags_with_args: A list of flags which take an argument.
|
||||
no_cmd_split: If true, ';;' to split sub-commands is ignored.
|
||||
backend: Which backend the command works with (or None if it works with
|
||||
both)
|
||||
_qute_args: The saved data from @cmdutils.argument
|
||||
_needs_js: Whether the command needs javascript enabled
|
||||
_modes: The modes the command can be executed in.
|
||||
@@ -92,7 +94,8 @@ class Command:
|
||||
def __init__(self, *, handler, name, instance=None, maxsplit=None,
|
||||
hide=False, modes=None, not_modes=None, needs_js=False,
|
||||
debug=False, ignore_args=False, deprecated=False,
|
||||
no_cmd_split=False, star_args_optional=False, scope='global'):
|
||||
no_cmd_split=False, star_args_optional=False, scope='global',
|
||||
backend=None):
|
||||
# I really don't know how to solve this in a better way, I tried.
|
||||
# pylint: disable=too-many-locals
|
||||
if modes is not None and not_modes is not None:
|
||||
@@ -123,6 +126,8 @@ class Command:
|
||||
self.ignore_args = ignore_args
|
||||
self.handler = handler
|
||||
self.no_cmd_split = no_cmd_split
|
||||
self.backend = backend
|
||||
|
||||
self.docparser = docutils.DocstringParser(handler)
|
||||
self.parser = argparser.ArgumentParser(
|
||||
name, description=self.docparser.short_desc,
|
||||
@@ -170,10 +175,22 @@ class Command:
|
||||
raise cmdexc.PrerequisitesError(
|
||||
"{}: This command is not allowed in {} mode.".format(
|
||||
self.name, mode_names))
|
||||
|
||||
if self._needs_js and not QWebSettings.globalSettings().testAttribute(
|
||||
QWebSettings.JavascriptEnabled):
|
||||
raise cmdexc.PrerequisitesError(
|
||||
"{}: This command needs javascript enabled.".format(self.name))
|
||||
|
||||
backend_mapping = {
|
||||
'webkit': usertypes.Backend.QtWebKit,
|
||||
'webengine': usertypes.Backend.QtWebEngine,
|
||||
}
|
||||
used_backend = backend_mapping[objreg.get('args').backend]
|
||||
if self.backend is not None and used_backend != self.backend:
|
||||
raise cmdexc.PrerequisitesError(
|
||||
"{}: Only available with {} "
|
||||
"backend.".format(self.name, self.backend.name))
|
||||
|
||||
if self.deprecated:
|
||||
message.warning(win_id, '{} is deprecated - {}'.format(
|
||||
self.name, self.deprecated))
|
||||
@@ -483,6 +500,9 @@ class Command:
|
||||
dbgout = ["command called:", self.name]
|
||||
if args:
|
||||
dbgout.append(str(args))
|
||||
elif args is None:
|
||||
args = []
|
||||
|
||||
if count is not None:
|
||||
dbgout.append("(count={})".format(count))
|
||||
log.commands.debug(' '.join(dbgout))
|
||||
@@ -497,8 +517,8 @@ class Command:
|
||||
e.status, e))
|
||||
return
|
||||
self._count = count
|
||||
posargs, kwargs = self._get_call_args(win_id)
|
||||
self._check_prerequisites(win_id)
|
||||
posargs, kwargs = self._get_call_args(win_id)
|
||||
log.commands.debug('Calling {}'.format(
|
||||
debug_utils.format_call(self.handler, posargs, kwargs)))
|
||||
self.handler(*posargs, **kwargs)
|
||||
|
||||
@@ -26,12 +26,13 @@ from PyQt5.QtCore import pyqtSlot, QUrl, QObject
|
||||
|
||||
from qutebrowser.config import config, configexc
|
||||
from qutebrowser.commands import cmdexc, cmdutils
|
||||
from qutebrowser.utils import message, log, objreg, qtutils
|
||||
from qutebrowser.utils import message, objreg, qtutils
|
||||
from qutebrowser.misc import split
|
||||
|
||||
|
||||
ParseResult = collections.namedtuple('ParseResult', ['cmd', 'args', 'cmdline',
|
||||
'count'])
|
||||
last_command = {}
|
||||
|
||||
|
||||
def _current_url(tabbed_browser):
|
||||
@@ -80,21 +81,22 @@ class CommandRunner(QObject):
|
||||
self._partial_match = partial_match
|
||||
self._win_id = win_id
|
||||
|
||||
def _get_alias(self, text):
|
||||
def _get_alias(self, text, default=None):
|
||||
"""Get an alias from the config.
|
||||
|
||||
Args:
|
||||
text: The text to parse.
|
||||
default : Default value to return when alias was not found.
|
||||
|
||||
Return:
|
||||
None if no alias was found.
|
||||
The new command string if an alias was found.
|
||||
The new command string if an alias was found. Default value
|
||||
otherwise.
|
||||
"""
|
||||
parts = text.strip().split(maxsplit=1)
|
||||
try:
|
||||
alias = config.get('aliases', parts[0])
|
||||
except (configexc.NoOptionError, configexc.NoSectionError):
|
||||
return None
|
||||
return default
|
||||
try:
|
||||
new_cmd = '{} {}'.format(alias, parts[1])
|
||||
except IndexError:
|
||||
@@ -103,7 +105,7 @@ class CommandRunner(QObject):
|
||||
new_cmd += ' '
|
||||
return new_cmd
|
||||
|
||||
def parse_all(self, text, *args, **kwargs):
|
||||
def parse_all(self, text, aliases=True, *args, **kwargs):
|
||||
"""Split a command on ;; and parse all parts.
|
||||
|
||||
If the first command in the commandline is a non-split one, it only
|
||||
@@ -111,11 +113,15 @@ class CommandRunner(QObject):
|
||||
|
||||
Args:
|
||||
text: Text to parse.
|
||||
aliases: Whether to handle aliases.
|
||||
*args/**kwargs: Passed to parse().
|
||||
|
||||
Yields:
|
||||
ParseResult tuples.
|
||||
"""
|
||||
if aliases:
|
||||
text = self._get_alias(text, text)
|
||||
|
||||
if ';;' in text:
|
||||
# Get the first command and check if it doesn't want to have ;;
|
||||
# split.
|
||||
@@ -159,12 +165,11 @@ class CommandRunner(QObject):
|
||||
cmdline = text.split()
|
||||
return ParseResult(cmd=None, args=None, cmdline=cmdline, count=count)
|
||||
|
||||
def parse(self, text, *, aliases=True, fallback=False, keep=False):
|
||||
def parse(self, text, *, fallback=False, keep=False):
|
||||
"""Split the commandline text into command and arguments.
|
||||
|
||||
Args:
|
||||
text: Text to parse.
|
||||
aliases: Whether to handle aliases.
|
||||
fallback: Whether to do a fallback splitting when the command was
|
||||
unknown.
|
||||
keep: Whether to keep special chars and whitespace
|
||||
@@ -178,13 +183,6 @@ class CommandRunner(QObject):
|
||||
if not cmdstr and not fallback:
|
||||
raise cmdexc.NoSuchCommandError("No command given")
|
||||
|
||||
if aliases:
|
||||
new_cmd = self._get_alias(text)
|
||||
if new_cmd is not None:
|
||||
log.commands.debug("Re-parsing with '{}'.".format(new_cmd))
|
||||
return self.parse(new_cmd, aliases=False, fallback=fallback,
|
||||
keep=keep)
|
||||
|
||||
if self._partial_match:
|
||||
cmdstr = self._completion_match(cmdstr)
|
||||
|
||||
@@ -216,7 +214,7 @@ class CommandRunner(QObject):
|
||||
cmdstr modified to the matching completion or unmodified
|
||||
"""
|
||||
matches = []
|
||||
for valid_command in cmdutils.cmd_dict.keys():
|
||||
for valid_command in cmdutils.cmd_dict:
|
||||
if valid_command.find(cmdstr) == 0:
|
||||
matches.append(valid_command)
|
||||
if len(matches) == 1:
|
||||
@@ -274,6 +272,10 @@ class CommandRunner(QObject):
|
||||
count: The count to pass to the command.
|
||||
"""
|
||||
for result in self.parse_all(text):
|
||||
mode_manager = objreg.get('mode-manager', scope='window',
|
||||
window=self._win_id)
|
||||
cur_mode = mode_manager.mode
|
||||
|
||||
args = replace_variables(self._win_id, result.args)
|
||||
if count is not None:
|
||||
if result.count is not None:
|
||||
@@ -285,6 +287,11 @@ class CommandRunner(QObject):
|
||||
else:
|
||||
result.cmd.run(self._win_id, args)
|
||||
|
||||
if result.cmdline[0] != 'repeat-command':
|
||||
last_command[cur_mode] = (
|
||||
self._parse_count(text)[1],
|
||||
count if count is not None else result.count)
|
||||
|
||||
@pyqtSlot(str, int)
|
||||
@pyqtSlot(str)
|
||||
def run_safely(self, text, count=None):
|
||||
|
||||
@@ -26,9 +26,10 @@ import tempfile
|
||||
from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject, QSocketNotifier
|
||||
|
||||
from qutebrowser.utils import message, log, objreg, standarddir
|
||||
from qutebrowser.commands import runners, cmdexc
|
||||
from qutebrowser.commands import runners
|
||||
from qutebrowser.config import config
|
||||
from qutebrowser.misc import guiprocess
|
||||
from qutebrowser.browser.webkit import downloads
|
||||
|
||||
|
||||
class _QtFIFOReader(QObject):
|
||||
@@ -85,6 +86,10 @@ class _BaseUserscriptRunner(QObject):
|
||||
_proc: The GUIProcess which is being executed.
|
||||
_win_id: The window ID this runner is associated with.
|
||||
_cleaned_up: Whether temporary files were cleaned up.
|
||||
_text_stored: Set when the page text was stored async.
|
||||
_html_stored: Set when the page html was stored async.
|
||||
_args: Arguments to pass to _run_process.
|
||||
_kwargs: Keyword arguments to pass to _run_process.
|
||||
|
||||
Signals:
|
||||
got_cmd: Emitted when a new command arrived and should be executed.
|
||||
@@ -100,9 +105,41 @@ class _BaseUserscriptRunner(QObject):
|
||||
self._win_id = win_id
|
||||
self._filepath = None
|
||||
self._proc = None
|
||||
self._env = None
|
||||
self._env = {}
|
||||
self._text_stored = False
|
||||
self._html_stored = False
|
||||
self._args = None
|
||||
self._kwargs = None
|
||||
|
||||
def _run_process(self, cmd, *args, env, verbose):
|
||||
def store_text(self, text):
|
||||
"""Called as callback when the text is ready from the web backend."""
|
||||
with tempfile.NamedTemporaryFile(mode='w', encoding='utf-8',
|
||||
suffix='.txt',
|
||||
delete=False) as txt_file:
|
||||
txt_file.write(text)
|
||||
self._env['QUTE_TEXT'] = txt_file.name
|
||||
|
||||
self._text_stored = True
|
||||
log.procs.debug("Text stored from webview")
|
||||
if self._text_stored and self._html_stored:
|
||||
log.procs.debug("Both text/HTML stored, kicking off userscript!")
|
||||
self._run_process(*self._args, **self._kwargs)
|
||||
|
||||
def store_html(self, html):
|
||||
"""Called as callback when the html is ready from the web backend."""
|
||||
with tempfile.NamedTemporaryFile(mode='w', encoding='utf-8',
|
||||
suffix='.html',
|
||||
delete=False) as html_file:
|
||||
html_file.write(html)
|
||||
self._env['QUTE_HTML'] = html_file.name
|
||||
|
||||
self._html_stored = True
|
||||
log.procs.debug("HTML stored from webview")
|
||||
if self._text_stored and self._html_stored:
|
||||
log.procs.debug("Both text/HTML stored, kicking off userscript!")
|
||||
self._run_process(*self._args, **self._kwargs)
|
||||
|
||||
def _run_process(self, cmd, *args, env=None, verbose=False):
|
||||
"""Start the given command.
|
||||
|
||||
Args:
|
||||
@@ -111,7 +148,7 @@ class _BaseUserscriptRunner(QObject):
|
||||
env: A dictionary of environment variables to add.
|
||||
verbose: Show notifications when the command started/exited.
|
||||
"""
|
||||
self._env = {'QUTE_FIFO': self._filepath}
|
||||
self._env['QUTE_FIFO'] = self._filepath
|
||||
if env is not None:
|
||||
self._env.update(env)
|
||||
self._proc = guiprocess.GUIProcess(self._win_id, 'userscript',
|
||||
@@ -143,18 +180,19 @@ class _BaseUserscriptRunner(QObject):
|
||||
fn, e))
|
||||
self._filepath = None
|
||||
self._proc = None
|
||||
self._env = None
|
||||
self._env = {}
|
||||
self._text_stored = False
|
||||
self._html_stored = False
|
||||
|
||||
def run(self, cmd, *args, env=None, verbose=False):
|
||||
"""Run the userscript given.
|
||||
def prepare_run(self, *args, **kwargs):
|
||||
"""Prepare running the userscript given.
|
||||
|
||||
Needs to be overridden by subclasses.
|
||||
The script will actually run after store_text and store_html have been
|
||||
called.
|
||||
|
||||
Args:
|
||||
cmd: The command to be started.
|
||||
*args: The arguments to hand to the command
|
||||
env: A dictionary of environment variables to add.
|
||||
verbose: Show notifications when the command started/exited.
|
||||
Passed to _run_process.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
@@ -189,7 +227,10 @@ class _POSIXUserscriptRunner(_BaseUserscriptRunner):
|
||||
super().__init__(win_id, parent)
|
||||
self._reader = None
|
||||
|
||||
def run(self, cmd, *args, env=None, verbose=False):
|
||||
def prepare_run(self, *args, **kwargs):
|
||||
self._args = args
|
||||
self._kwargs = kwargs
|
||||
|
||||
try:
|
||||
# tempfile.mktemp is deprecated and discouraged, but we use it here
|
||||
# to create a FIFO since the only other alternative would be to
|
||||
@@ -208,8 +249,6 @@ class _POSIXUserscriptRunner(_BaseUserscriptRunner):
|
||||
self._reader = _QtFIFOReader(self._filepath)
|
||||
self._reader.got_line.connect(self.got_cmd)
|
||||
|
||||
self._run_process(cmd, *args, env=env, verbose=verbose)
|
||||
|
||||
@pyqtSlot()
|
||||
def on_proc_finished(self):
|
||||
self._cleanup()
|
||||
@@ -279,86 +318,35 @@ class _WindowsUserscriptRunner(_BaseUserscriptRunner):
|
||||
"""Read back the commands when the process finished."""
|
||||
self._cleanup()
|
||||
|
||||
def run(self, cmd, *args, env=None, verbose=False):
|
||||
def prepare_run(self, *args, **kwargs):
|
||||
self._args = args
|
||||
self._kwargs = kwargs
|
||||
|
||||
try:
|
||||
self._oshandle, self._filepath = tempfile.mkstemp(text=True)
|
||||
except OSError as e:
|
||||
message.error(self._win_id, "Error while creating tempfile: "
|
||||
"{}".format(e))
|
||||
return
|
||||
self._run_process(cmd, *args, env=env, verbose=verbose)
|
||||
|
||||
|
||||
class _DummyUserscriptRunner(QObject):
|
||||
class UnsupportedError(Exception):
|
||||
|
||||
"""Simple dummy runner which displays an error when using userscripts.
|
||||
"""Raised when userscripts aren't supported on this platform."""
|
||||
|
||||
Used on unknown systems since we don't know what (or if any) approach will
|
||||
work there.
|
||||
|
||||
Signals:
|
||||
finished: Always emitted.
|
||||
"""
|
||||
|
||||
finished = pyqtSignal()
|
||||
|
||||
def __init__(self, win_id, parent=None):
|
||||
# pylint: disable=unused-argument
|
||||
super().__init__(parent)
|
||||
|
||||
def run(self, cmd, *args, env=None, verbose=False):
|
||||
"""Print an error as userscripts are not supported."""
|
||||
# pylint: disable=unused-argument,unused-variable
|
||||
self.finished.emit()
|
||||
raise cmdexc.CommandError(
|
||||
"Userscripts are not supported on this platform!")
|
||||
def __str__(self):
|
||||
return "Userscripts are not supported on this platform!"
|
||||
|
||||
|
||||
# Here we basically just assign a generic UserscriptRunner class which does the
|
||||
# right thing depending on the platform.
|
||||
if os.name == 'posix':
|
||||
UserscriptRunner = _POSIXUserscriptRunner
|
||||
elif os.name == 'nt': # pragma: no cover
|
||||
UserscriptRunner = _WindowsUserscriptRunner
|
||||
else: # pragma: no cover
|
||||
UserscriptRunner = _DummyUserscriptRunner
|
||||
def run_async(tab, cmd, *args, win_id, env, verbose=False):
|
||||
"""Run a userscript after dumping page html/source.
|
||||
|
||||
|
||||
def store_source(frame):
|
||||
"""Store HTML/plaintext in files.
|
||||
|
||||
This writes files containing the HTML/plaintext source of the page, and
|
||||
returns a dict with the paths as QUTE_HTML/QUTE_TEXT.
|
||||
|
||||
Args:
|
||||
frame: The QWebFrame to get the info from, or None to do nothing.
|
||||
|
||||
Return:
|
||||
A dictionary with the needed environment variables.
|
||||
|
||||
Warning:
|
||||
The caller is responsible to delete the files after using them!
|
||||
"""
|
||||
if frame is None:
|
||||
return {}
|
||||
env = {}
|
||||
with tempfile.NamedTemporaryFile(mode='w', encoding='utf-8',
|
||||
suffix='.html',
|
||||
delete=False) as html_file:
|
||||
html_file.write(frame.toHtml())
|
||||
env['QUTE_HTML'] = html_file.name
|
||||
with tempfile.NamedTemporaryFile(mode='w', encoding='utf-8',
|
||||
suffix='.txt',
|
||||
delete=False) as txt_file:
|
||||
txt_file.write(frame.toPlainText())
|
||||
env['QUTE_TEXT'] = txt_file.name
|
||||
return env
|
||||
|
||||
|
||||
def run(cmd, *args, win_id, env, verbose=False):
|
||||
"""Convenience method to run a userscript.
|
||||
Raises:
|
||||
UnsupportedError if userscripts are not supported on the current
|
||||
platform.
|
||||
|
||||
Args:
|
||||
tab: The WebKitTab/WebEngineTab to get the source from.
|
||||
cmd: The userscript binary to run.
|
||||
*args: The arguments to pass to the userscript.
|
||||
win_id: The window id the userscript is executed in.
|
||||
@@ -368,7 +356,14 @@ def run(cmd, *args, win_id, env, verbose=False):
|
||||
tabbed_browser = objreg.get('tabbed-browser', scope='window',
|
||||
window=win_id)
|
||||
commandrunner = runners.CommandRunner(win_id, parent=tabbed_browser)
|
||||
runner = UserscriptRunner(win_id, tabbed_browser)
|
||||
|
||||
if os.name == 'posix':
|
||||
runner = _POSIXUserscriptRunner(win_id, tabbed_browser)
|
||||
elif os.name == 'nt': # pragma: no cover
|
||||
runner = _WindowsUserscriptRunner(win_id, tabbed_browser)
|
||||
else: # pragma: no cover
|
||||
raise UnsupportedError
|
||||
|
||||
runner.got_cmd.connect(
|
||||
lambda cmd:
|
||||
log.commands.debug("Got userscript command: {}".format(cmd)))
|
||||
@@ -376,6 +371,15 @@ def run(cmd, *args, win_id, env, verbose=False):
|
||||
user_agent = config.get('network', 'user-agent')
|
||||
if user_agent is not None:
|
||||
env['QUTE_USER_AGENT'] = user_agent
|
||||
config_dir = standarddir.config()
|
||||
if config_dir is not None:
|
||||
env['QUTE_CONFIG_DIR'] = config_dir
|
||||
data_dir = standarddir.data()
|
||||
if data_dir is not None:
|
||||
env['QUTE_DATA_DIR'] = data_dir
|
||||
download_dir = downloads.download_dir()
|
||||
if download_dir is not None:
|
||||
env['QUTE_DOWNLOAD_DIR'] = download_dir
|
||||
cmd_path = os.path.expanduser(cmd)
|
||||
|
||||
# if cmd is not given as an absolute path, look it up
|
||||
@@ -388,6 +392,9 @@ def run(cmd, *args, win_id, env, verbose=False):
|
||||
"userscripts", cmd)
|
||||
log.misc.debug("Userscript to run: {}".format(cmd_path))
|
||||
|
||||
runner.run(cmd_path, *args, env=env, verbose=verbose)
|
||||
runner.finished.connect(commandrunner.deleteLater)
|
||||
runner.finished.connect(runner.deleteLater)
|
||||
|
||||
runner.prepare_run(cmd_path, *args, env=env, verbose=verbose)
|
||||
tab.dump_async(runner.store_html)
|
||||
tab.dump_async(runner.store_text, plain=True)
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject, QTimer
|
||||
|
||||
from qutebrowser.config import config
|
||||
from qutebrowser.commands import cmdexc, cmdutils, runners
|
||||
from qutebrowser.commands import cmdutils, runners
|
||||
from qutebrowser.utils import usertypes, log, objreg, utils
|
||||
from qutebrowser.completion.models import instances, sortfilter
|
||||
|
||||
@@ -354,7 +354,7 @@ class Completer(QObject):
|
||||
if completion.enabled:
|
||||
completion.show()
|
||||
|
||||
def split(self, keep=False, aliases=False):
|
||||
def split(self, keep=False):
|
||||
"""Get the text split up in parts.
|
||||
|
||||
Args:
|
||||
@@ -371,7 +371,7 @@ class Completer(QObject):
|
||||
# the whitespace.
|
||||
return [text]
|
||||
runner = runners.CommandRunner(self._win_id)
|
||||
result = runner.parse(text, fallback=True, aliases=aliases, keep=keep)
|
||||
result = runner.parse(text, fallback=True, keep=keep)
|
||||
parts = result.cmdline
|
||||
if self._empty_item_idx is not None:
|
||||
log.completion.debug("Empty element queued at {}, "
|
||||
@@ -485,16 +485,3 @@ class Completer(QObject):
|
||||
"""Select the next completion item."""
|
||||
self._open_completion_if_needed()
|
||||
self.next_prev_item.emit(False)
|
||||
|
||||
@cmdutils.register(instance='completion', hide=True,
|
||||
modes=[usertypes.KeyMode.command], scope='window')
|
||||
def completion_item_del(self):
|
||||
"""Delete the current completion item."""
|
||||
completion = objreg.get('completion', scope='window',
|
||||
window=self._win_id)
|
||||
if not completion.currentIndex().isValid():
|
||||
raise cmdexc.CommandError("No item selected!")
|
||||
try:
|
||||
self.model().srcmodel.delete_cur_item(completion)
|
||||
except NotImplementedError:
|
||||
raise cmdexc.CommandError("Cannot delete this item.")
|
||||
|
||||
@@ -29,7 +29,8 @@ from PyQt5.QtCore import pyqtSlot, pyqtSignal, Qt, QItemSelectionModel
|
||||
from qutebrowser.config import config, style
|
||||
from qutebrowser.completion import completiondelegate, completer
|
||||
from qutebrowser.completion.models import base
|
||||
from qutebrowser.utils import qtutils, objreg, utils
|
||||
from qutebrowser.utils import qtutils, objreg, utils, usertypes
|
||||
from qutebrowser.commands import cmdexc, cmdutils
|
||||
|
||||
|
||||
class CompletionView(QTreeView):
|
||||
@@ -277,3 +278,14 @@ class CompletionView(QTreeView):
|
||||
if scrollbar is not None:
|
||||
scrollbar.setValue(scrollbar.minimum())
|
||||
super().showEvent(e)
|
||||
|
||||
@cmdutils.register(instance='completion', hide=True,
|
||||
modes=[usertypes.KeyMode.command], scope='window')
|
||||
def completion_item_del(self):
|
||||
"""Delete the current completion item."""
|
||||
if not self.currentIndex().isValid():
|
||||
raise cmdexc.CommandError("No item selected!")
|
||||
try:
|
||||
self.model().srcmodel.delete_cur_item(self)
|
||||
except NotImplementedError:
|
||||
raise cmdexc.CommandError("Cannot delete this item.")
|
||||
|
||||
@@ -20,15 +20,13 @@
|
||||
"""Global instances of the completion models.
|
||||
|
||||
Module attributes:
|
||||
_instances: An dict of available completions.
|
||||
_instances: A dict of available completions.
|
||||
INITIALIZERS: A {usertypes.Completion: callable} dict of functions to
|
||||
initialize completions.
|
||||
"""
|
||||
|
||||
import functools
|
||||
|
||||
from PyQt5.QtCore import pyqtSlot
|
||||
|
||||
from qutebrowser.completion.models import (miscmodels, urlmodel, configmodel,
|
||||
base)
|
||||
from qutebrowser.utils import objreg, usertypes, log, debug
|
||||
@@ -84,7 +82,6 @@ def _init_setting_completions():
|
||||
_instances[usertypes.Completion.value][sectname][opt] = val_model
|
||||
|
||||
|
||||
@pyqtSlot()
|
||||
def init_quickmark_completions():
|
||||
"""Initialize quickmark completion models."""
|
||||
log.completion.debug("Initializing quickmark completion.")
|
||||
@@ -96,7 +93,6 @@ def init_quickmark_completions():
|
||||
_instances[usertypes.Completion.quickmark_by_name] = model
|
||||
|
||||
|
||||
@pyqtSlot()
|
||||
def init_bookmark_completions():
|
||||
"""Initialize bookmark completion models."""
|
||||
log.completion.debug("Initializing bookmark completion.")
|
||||
@@ -108,7 +104,6 @@ def init_bookmark_completions():
|
||||
_instances[usertypes.Completion.bookmark_by_url] = model
|
||||
|
||||
|
||||
@pyqtSlot()
|
||||
def init_session_completion():
|
||||
"""Initialize session completion model."""
|
||||
log.completion.debug("Initializing session completion.")
|
||||
|
||||
@@ -19,12 +19,11 @@
|
||||
|
||||
"""Misc. CompletionModels."""
|
||||
|
||||
from collections import defaultdict
|
||||
from PyQt5.QtCore import Qt, QTimer, pyqtSlot
|
||||
|
||||
from qutebrowser.browser import webview
|
||||
from qutebrowser.browser import browsertab
|
||||
from qutebrowser.config import config, configdata
|
||||
from qutebrowser.utils import objreg, log, qtutils, utils
|
||||
from qutebrowser.utils import objreg, log, qtutils
|
||||
from qutebrowser.commands import cmdutils
|
||||
from qutebrowser.completion.models import base
|
||||
|
||||
@@ -53,14 +52,8 @@ class CommandCompletionModel(base.BaseCompletionModel):
|
||||
cat = self.new_category("Commands")
|
||||
|
||||
# map each command to its bound keys and show these in the misc column
|
||||
keyconf = objreg.get('key-config')
|
||||
cmd_to_keys = defaultdict(list)
|
||||
for key, cmd in keyconf.get_bindings_for('normal').items():
|
||||
# put special bindings last
|
||||
if utils.is_special_key(key):
|
||||
cmd_to_keys[cmd].append(key)
|
||||
else:
|
||||
cmd_to_keys[cmd].insert(0, key)
|
||||
key_config = objreg.get('key-config')
|
||||
cmd_to_keys = key_config.get_reverse_bindings_for('normal')
|
||||
for (name, desc) in sorted(cmdlist):
|
||||
self.new_item(cat, name, desc, ', '.join(cmd_to_keys[name]))
|
||||
|
||||
@@ -183,7 +176,7 @@ class TabCompletionModel(base.BaseCompletionModel):
|
||||
window=win_id)
|
||||
for i in range(tabbed_browser.count()):
|
||||
tab = tabbed_browser.widget(i)
|
||||
tab.url_text_changed.connect(self.rebuild)
|
||||
tab.url_changed.connect(self.rebuild)
|
||||
tab.shutting_down.connect(self.delayed_rebuild)
|
||||
tabbed_browser.new_tab.connect(self.on_new_tab)
|
||||
objreg.get("app").new_window.connect(self.on_new_window)
|
||||
@@ -193,10 +186,10 @@ class TabCompletionModel(base.BaseCompletionModel):
|
||||
"""Add hooks to new windows."""
|
||||
window.tabbed_browser.new_tab.connect(self.on_new_tab)
|
||||
|
||||
@pyqtSlot(webview.WebView)
|
||||
@pyqtSlot(browsertab.AbstractTab)
|
||||
def on_new_tab(self, tab):
|
||||
"""Add hooks to new tabs."""
|
||||
tab.url_text_changed.connect(self.rebuild)
|
||||
tab.url_changed.connect(self.rebuild)
|
||||
tab.shutting_down.connect(self.delayed_rebuild)
|
||||
self.rebuild()
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ import contextlib
|
||||
import collections
|
||||
import collections.abc
|
||||
|
||||
from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject, QUrl, QSettings
|
||||
from PyQt5.QtCore import pyqtSignal, QObject, QUrl, QSettings
|
||||
|
||||
from qutebrowser.config import configdata, configexc, textwrapper
|
||||
from qutebrowser.config.parsers import ini, keyconf
|
||||
@@ -94,8 +94,6 @@ class change_filter: # pylint: disable=invalid-name
|
||||
The decorated function.
|
||||
"""
|
||||
if self._function:
|
||||
@pyqtSlot(str, str)
|
||||
@pyqtSlot()
|
||||
@functools.wraps(func)
|
||||
def wrapper(sectname=None, optname=None):
|
||||
if sectname is None and optname is None:
|
||||
@@ -108,8 +106,6 @@ class change_filter: # pylint: disable=invalid-name
|
||||
else:
|
||||
return func()
|
||||
else:
|
||||
@pyqtSlot(str, str)
|
||||
@pyqtSlot()
|
||||
@functools.wraps(func)
|
||||
def wrapper(wrapper_self, sectname=None, optname=None):
|
||||
if sectname is None and optname is None:
|
||||
@@ -211,7 +207,7 @@ def _init_misc():
|
||||
"""Initialize misc. config-related files."""
|
||||
save_manager = objreg.get('save-manager')
|
||||
state_config = ini.ReadWriteConfigParser(standarddir.data(), 'state')
|
||||
for sect in ('general', 'geometry'):
|
||||
for sect in ['general', 'geometry']:
|
||||
try:
|
||||
state_config.add_section(sect)
|
||||
except configparser.DuplicateSectionError:
|
||||
@@ -242,7 +238,7 @@ def _init_misc():
|
||||
path = os.devnull
|
||||
else:
|
||||
path = os.path.join(standarddir.config(), 'qsettings')
|
||||
for fmt in (QSettings.NativeFormat, QSettings.IniFormat):
|
||||
for fmt in [QSettings.NativeFormat, QSettings.IniFormat]:
|
||||
QSettings.setPath(fmt, QSettings.UserScope, path)
|
||||
|
||||
|
||||
@@ -310,7 +306,7 @@ class ConfigManager(QObject):
|
||||
sections: The configuration data as an OrderedDict.
|
||||
_fname: The filename to be opened.
|
||||
_configdir: The dictionary to read the config from and save it in.
|
||||
_interpolation: An configparser.Interpolation object
|
||||
_interpolation: A configparser.Interpolation object
|
||||
_proxies: configparser.SectionProxy objects for sections.
|
||||
_initialized: Whether the ConfigManager is fully initialized yet.
|
||||
|
||||
@@ -346,11 +342,15 @@ class ConfigManager(QObject):
|
||||
DELETED_OPTIONS = [
|
||||
('colors', 'tab.separator'),
|
||||
('colors', 'tabs.separator'),
|
||||
('colors', 'tab.seperator'), # pragma: no spellcheck
|
||||
('colors', 'tabs.seperator'), # pragma: no spellcheck
|
||||
('colors', 'completion.item.bg'),
|
||||
('tabs', 'indicator-space'),
|
||||
('tabs', 'hide-auto'),
|
||||
('tabs', 'auto-hide'),
|
||||
('tabs', 'hide-always'),
|
||||
('ui', 'display-statusbar-messages'),
|
||||
('general', 'wrap-search'),
|
||||
]
|
||||
CHANGED_OPTIONS = {
|
||||
('content', 'cookies-accept'):
|
||||
@@ -494,7 +494,7 @@ class ConfigManager(QObject):
|
||||
for sectname in cp:
|
||||
if sectname in self.RENAMED_SECTIONS:
|
||||
sectname = self.RENAMED_SECTIONS[sectname]
|
||||
if sectname is not 'DEFAULT' and sectname not in self.sections:
|
||||
if sectname != 'DEFAULT' and sectname not in self.sections:
|
||||
if not relaxed:
|
||||
raise configexc.NoSectionError(sectname)
|
||||
for sectname in self.sections:
|
||||
@@ -516,7 +516,7 @@ class ConfigManager(QObject):
|
||||
k = k[1:]
|
||||
|
||||
if (sectname, k) in self.DELETED_OPTIONS:
|
||||
return
|
||||
continue
|
||||
if (sectname, k) in self.RENAMED_OPTIONS:
|
||||
k = self.RENAMED_OPTIONS[sectname, k]
|
||||
if (sectname, k) in self.CHANGED_OPTIONS:
|
||||
@@ -549,7 +549,7 @@ class ConfigManager(QObject):
|
||||
"""Notify other objects the config has changed."""
|
||||
log.config.debug("Config option changed: {} -> {}".format(
|
||||
sectname, optname))
|
||||
if sectname in ('colors', 'fonts'):
|
||||
if sectname in ['colors', 'fonts']:
|
||||
self.style_changed.emit(sectname, optname)
|
||||
self.changed.emit(sectname, optname)
|
||||
|
||||
|
||||
@@ -94,7 +94,7 @@ SECTION_DESC = {
|
||||
"Colors used in the UI.\n"
|
||||
"A value can be in one of the following format:\n\n"
|
||||
" * `#RGB`/`#RRGGBB`/`#RRRGGGBBB`/`#RRRRGGGGBBBB`\n"
|
||||
" * A SVG color name as specified in http://www.w3.org/TR/SVG/"
|
||||
" * An SVG color name as specified in http://www.w3.org/TR/SVG/"
|
||||
"types.html#ColorKeywords[the W3C specification].\n"
|
||||
" * transparent (no color)\n"
|
||||
" * `rgb(r, g, b)` / `rgba(r, g, b, a)` (values 0-255 or "
|
||||
@@ -134,11 +134,6 @@ def data(readonly=False):
|
||||
SettingValue(typ.IgnoreCase(), 'smart'),
|
||||
"Whether to find text on a page case-insensitively."),
|
||||
|
||||
('wrap-search',
|
||||
SettingValue(typ.Bool(), 'true'),
|
||||
"Whether to wrap finding text to the top when arriving at the "
|
||||
"end."),
|
||||
|
||||
('startpage',
|
||||
SettingValue(typ.List(), 'https://duckduckgo.com'),
|
||||
"The default page(s) to open at the start, separated by commas."),
|
||||
@@ -272,6 +267,10 @@ def data(readonly=False):
|
||||
SettingValue(typ.VerticalPosition(), 'top'),
|
||||
"Where to show the downloaded files."),
|
||||
|
||||
('status-position',
|
||||
SettingValue(typ.VerticalPosition(), 'bottom'),
|
||||
"The position of the status bar."),
|
||||
|
||||
('message-timeout',
|
||||
SettingValue(typ.Int(), '2000'),
|
||||
"Time (in ms) to show messages in the statusbar for."),
|
||||
@@ -284,10 +283,6 @@ def data(readonly=False):
|
||||
SettingValue(typ.ConfirmQuit(), 'never'),
|
||||
"Whether to confirm quitting the application."),
|
||||
|
||||
('display-statusbar-messages',
|
||||
SettingValue(typ.Bool(), 'false'),
|
||||
"Whether to display javascript statusbar messages."),
|
||||
|
||||
('zoom-text-only',
|
||||
SettingValue(typ.Bool(), 'false'),
|
||||
"Whether the zoom factor on a frame applies only to the text or "
|
||||
@@ -330,7 +325,7 @@ def data(readonly=False):
|
||||
('window-title-format',
|
||||
SettingValue(typ.FormatString(fields=['perc', 'perc_raw', 'title',
|
||||
'title_sep', 'id',
|
||||
'scroll_pos']),
|
||||
'scroll_pos', 'host']),
|
||||
'{perc}{title}{title_sep}qutebrowser'),
|
||||
"The format to use for the window title. The following "
|
||||
"placeholders are defined:\n\n"
|
||||
@@ -340,7 +335,8 @@ def data(readonly=False):
|
||||
"* `{title_sep}`: The string ` - ` if a title is set, empty "
|
||||
"otherwise.\n"
|
||||
"* `{id}`: The internal window ID of this window.\n"
|
||||
"* `{scroll_pos}`: The page scroll position."),
|
||||
"* `{scroll_pos}`: The page scroll position.\n"
|
||||
"* `{host}`: The host of the current web page."),
|
||||
|
||||
('hide-mouse-cursor',
|
||||
SettingValue(typ.Bool(), 'false'),
|
||||
@@ -627,7 +623,7 @@ def data(readonly=False):
|
||||
('title-format',
|
||||
SettingValue(typ.FormatString(
|
||||
fields=['perc', 'perc_raw', 'title', 'title_sep', 'index',
|
||||
'id', 'scroll_pos']), '{index}: {title}'),
|
||||
'id', 'scroll_pos', 'host']), '{index}: {title}'),
|
||||
"The format to use for the tab title. The following placeholders "
|
||||
"are defined:\n\n"
|
||||
"* `{perc}`: The percentage as a string like `[10%]`.\n"
|
||||
@@ -637,7 +633,8 @@ def data(readonly=False):
|
||||
"otherwise.\n"
|
||||
"* `{index}`: The index of this tab.\n"
|
||||
"* `{id}`: The internal tab ID of this tab.\n"
|
||||
"* `{scroll_pos}`: The page scroll position."),
|
||||
"* `{scroll_pos}`: The page scroll position.\n"
|
||||
"* `{host}`: The host of the current web page."),
|
||||
|
||||
('title-alignment',
|
||||
SettingValue(typ.TextAlignment(), 'left'),
|
||||
@@ -886,7 +883,7 @@ def data(readonly=False):
|
||||
('chars',
|
||||
SettingValue(typ.UniqueCharString(minlen=2, completions=[
|
||||
('asdfghjkl', "Home row"),
|
||||
('dhtnaoeu', "Home row (Dvorak)"),
|
||||
('aoeuidnths', "Home row (Dvorak)"),
|
||||
('abcdefghijklmnopqrstuvwxyz', "All letters"),
|
||||
]), 'asdfghjkl'),
|
||||
"Chars used for hint strings."),
|
||||
@@ -1461,15 +1458,14 @@ KEY_DATA = collections.OrderedDict([
|
||||
('hint all hover', [';h']),
|
||||
('hint images', [';i']),
|
||||
('hint images tab', [';I']),
|
||||
('hint images tab-bg', ['.i']),
|
||||
('hint links fill ":open {hint-url}"', [';o']),
|
||||
('hint links fill ":open -t {hint-url}"', [';O']),
|
||||
('hint links fill ":open -b {hint-url}"', ['.o']),
|
||||
('hint links yank', [';y']),
|
||||
('hint links yank-primary', [';Y']),
|
||||
('hint --rapid links tab-bg', [';r']),
|
||||
('hint --rapid links window', [';R']),
|
||||
('hint links download', [';d']),
|
||||
('hint inputs', [';t']),
|
||||
('scroll left', ['h']),
|
||||
('scroll down', ['j']),
|
||||
('scroll up', ['k']),
|
||||
@@ -1548,6 +1544,7 @@ KEY_DATA = collections.OrderedDict([
|
||||
('open qute:settings', ['Ss']),
|
||||
('follow-selected', RETURN_KEYS),
|
||||
('follow-selected -t', ['<Ctrl-Return>', '<Ctrl-Enter>']),
|
||||
('repeat-command', ['.']),
|
||||
])),
|
||||
|
||||
('insert', collections.OrderedDict([
|
||||
@@ -1577,6 +1574,7 @@ KEY_DATA = collections.OrderedDict([
|
||||
('prompt-accept', RETURN_KEYS),
|
||||
('prompt-yes', ['y']),
|
||||
('prompt-no', ['n']),
|
||||
('prompt-open-download', ['<Ctrl-X>']),
|
||||
])),
|
||||
|
||||
('command,prompt', collections.OrderedDict([
|
||||
|
||||
@@ -481,7 +481,7 @@ class IntList(List):
|
||||
|
||||
class Float(BaseType):
|
||||
|
||||
"""Base class for an float setting.
|
||||
"""Base class for a float setting.
|
||||
|
||||
Attributes:
|
||||
minval: Minimum value (inclusive).
|
||||
@@ -1342,7 +1342,7 @@ class AutoSearch(BaseType):
|
||||
self._basic_validation(value)
|
||||
if not value:
|
||||
return
|
||||
elif value.lower() in ('naive', 'dns'):
|
||||
elif value.lower() in ['naive', 'dns']:
|
||||
pass
|
||||
else:
|
||||
self.booltype.validate(value)
|
||||
@@ -1350,7 +1350,7 @@ class AutoSearch(BaseType):
|
||||
def transform(self, value):
|
||||
if not value:
|
||||
return None
|
||||
elif value.lower() in ('naive', 'dns'):
|
||||
elif value.lower() in ['naive', 'dns']:
|
||||
return value.lower()
|
||||
elif self.booltype.transform(value):
|
||||
# boolean true is an alias for naive matching
|
||||
@@ -1644,7 +1644,7 @@ class UserAgent(BaseType):
|
||||
|
||||
class TimestampTemplate(BaseType):
|
||||
|
||||
"""A strftime-like template for timestamps.
|
||||
"""An strftime-like template for timestamps.
|
||||
|
||||
See
|
||||
https://docs.python.org/3/library/datetime.html#strftime-strptime-behavior
|
||||
|
||||
@@ -419,3 +419,14 @@ class KeyConfigParser(QObject):
|
||||
bindings = {k: v for k, v in bindings.items()
|
||||
if v != self.UNBOUND_COMMAND}
|
||||
return bindings
|
||||
|
||||
def get_reverse_bindings_for(self, section):
|
||||
"""Get a dict of commands to a list of bindings for the section."""
|
||||
cmd_to_keys = collections.defaultdict(list)
|
||||
for key, cmd in self.get_bindings_for(section).items():
|
||||
# put special bindings last
|
||||
if utils.is_special_key(key):
|
||||
cmd_to_keys[cmd].append(key)
|
||||
else:
|
||||
cmd_to_keys[cmd].insert(0, key)
|
||||
return cmd_to_keys
|
||||
|
||||
@@ -54,7 +54,7 @@ class Section:
|
||||
|
||||
def __iter__(self):
|
||||
"""Iterate over all set values."""
|
||||
return self.values.__iter__()
|
||||
return iter(self.values)
|
||||
|
||||
def __bool__(self):
|
||||
"""Get boolean state of section."""
|
||||
|
||||
34
qutebrowser/html/bookmarks.html
Normal file
34
qutebrowser/html/bookmarks.html
Normal file
@@ -0,0 +1,34 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block style %}
|
||||
table { border: 1px solid grey; border-collapse: collapse; width: 100%;}
|
||||
th, td { border: 1px solid grey; padding: 0px 5px; }
|
||||
th { background: lightgrey; }
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<th><h3>Bookmark</h3></th>
|
||||
<th><h3>URL</h3></th>
|
||||
</tr>
|
||||
{% for url, title in bookmarks %}
|
||||
<tr>
|
||||
<td><a href="{{url}}">{{title}}</a></td>
|
||||
<td>{{url}}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
<tr>
|
||||
<th><h3>Quickmark</h3></th>
|
||||
<th><h3>URL</h3></th>
|
||||
</tr>
|
||||
{% for name, url in quickmarks %}
|
||||
<tr>
|
||||
<td><a href="{{url}}">{{name}}</a></td>
|
||||
<td>{{url}}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
|
||||
{% endblock %}
|
||||
67
qutebrowser/javascript/scroll.js
Normal file
67
qutebrowser/javascript/scroll.js
Normal file
@@ -0,0 +1,67 @@
|
||||
/**
|
||||
* Copyright 2016 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
|
||||
*
|
||||
* This file is part of qutebrowser.
|
||||
*
|
||||
* qutebrowser is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* qutebrowser is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
function scroll_to_perc(x, y) {
|
||||
var elem = document.documentElement;
|
||||
var x_px = window.scrollX;
|
||||
var y_px = window.scrollY;
|
||||
|
||||
if (x !== undefined) {
|
||||
x_px = (elem.scrollWidth - elem.clientWidth) / 100 * x;
|
||||
}
|
||||
|
||||
if (y !== undefined) {
|
||||
y_px = (elem.scrollHeight - elem.clientHeight) / 100 * y;
|
||||
}
|
||||
|
||||
window.scroll(x_px, y_px);
|
||||
}
|
||||
|
||||
function scroll_delta_page(x, y) {
|
||||
var dx = document.documentElement.clientWidth * x;
|
||||
var dy = document.documentElement.clientHeight * y;
|
||||
window.scrollBy(dx, dy);
|
||||
}
|
||||
|
||||
function scroll_pos() {
|
||||
var elem = document.documentElement;
|
||||
var dx = (elem.scrollWidth - elem.clientWidth);
|
||||
var dy = (elem.scrollHeight - elem.clientHeight);
|
||||
|
||||
var perc_x, perc_y;
|
||||
|
||||
if (dx === 0) {
|
||||
perc_x = 0;
|
||||
} else {
|
||||
perc_x = 100 / dx * window.scrollX;
|
||||
}
|
||||
|
||||
if (dy === 0) {
|
||||
perc_y = 0;
|
||||
} else {
|
||||
perc_y = 100 / dy * window.scrollY;
|
||||
}
|
||||
|
||||
var pos_perc = {'x': perc_x, 'y': perc_y};
|
||||
var pos_px = {'x': window.scrollX, 'y': window.scrollY};
|
||||
var pos = {'perc': pos_perc, 'px': pos_px};
|
||||
|
||||
// console.log(JSON.stringify(pos));
|
||||
return pos;
|
||||
}
|
||||
@@ -170,7 +170,7 @@ class ModeManager(QObject):
|
||||
handled = parser.handle(event)
|
||||
|
||||
is_non_alnum = (
|
||||
event.modifiers() not in (Qt.NoModifier, Qt.ShiftModifier) or
|
||||
event.modifiers() not in [Qt.NoModifier, Qt.ShiftModifier] or
|
||||
not event.text().strip())
|
||||
|
||||
if handled:
|
||||
|
||||
@@ -34,7 +34,8 @@ from qutebrowser.mainwindow import tabbedbrowser
|
||||
from qutebrowser.mainwindow.statusbar import bar
|
||||
from qutebrowser.completion import completionwidget
|
||||
from qutebrowser.keyinput import modeman
|
||||
from qutebrowser.browser import hints, downloads, downloadview, commands
|
||||
from qutebrowser.browser import commands, downloadview, hints
|
||||
from qutebrowser.browser.webkit import downloads
|
||||
from qutebrowser.misc import crashsignal, keyhintwidget
|
||||
|
||||
|
||||
@@ -76,7 +77,7 @@ def get_window(via_ipc, force_window=False, force_tab=False,
|
||||
win_id = window.win_id
|
||||
window_to_raise = window
|
||||
win_id = window.win_id
|
||||
if open_target not in ('tab-silent', 'tab-bg-silent'):
|
||||
if open_target not in ['tab-silent', 'tab-bg-silent']:
|
||||
window_to_raise = window
|
||||
if window_to_raise is not None:
|
||||
window_to_raise.setWindowState(
|
||||
@@ -195,28 +196,41 @@ class MainWindow(QWidget):
|
||||
@pyqtSlot(str, str)
|
||||
def on_config_changed(self, section, option):
|
||||
"""Resize the completion if related config options changed."""
|
||||
if section == 'completion' and option in ('height', 'shrink'):
|
||||
if section == 'completion' and option in ['height', 'shrink']:
|
||||
self.resize_completion()
|
||||
elif section == 'ui' and option == 'statusbar-padding':
|
||||
self.resize_completion()
|
||||
elif section == 'ui' and option == 'downloads-position':
|
||||
self._add_widgets()
|
||||
elif section == 'ui' and option == 'status-position':
|
||||
self._add_widgets()
|
||||
self.resize_completion()
|
||||
|
||||
def _add_widgets(self):
|
||||
"""Add or readd all widgets to the VBox."""
|
||||
self._vbox.removeWidget(self.tabbed_browser)
|
||||
self._vbox.removeWidget(self._downloadview)
|
||||
self._vbox.removeWidget(self.status)
|
||||
position = config.get('ui', 'downloads-position')
|
||||
if position == 'top':
|
||||
self._vbox.addWidget(self._downloadview)
|
||||
self._vbox.addWidget(self.tabbed_browser)
|
||||
elif position == 'bottom':
|
||||
self._vbox.addWidget(self.tabbed_browser)
|
||||
self._vbox.addWidget(self._downloadview)
|
||||
downloads_position = config.get('ui', 'downloads-position')
|
||||
status_position = config.get('ui', 'status-position')
|
||||
widgets = [self.tabbed_browser]
|
||||
|
||||
if downloads_position == 'top':
|
||||
widgets.insert(0, self._downloadview)
|
||||
elif downloads_position == 'bottom':
|
||||
widgets.append(self._downloadview)
|
||||
else:
|
||||
raise ValueError("Invalid position {}!".format(position))
|
||||
self._vbox.addWidget(self.status)
|
||||
raise ValueError("Invalid position {}!".format(downloads_position))
|
||||
|
||||
if status_position == 'top':
|
||||
widgets.insert(0, self.status)
|
||||
elif status_position == 'bottom':
|
||||
widgets.append(self.status)
|
||||
else:
|
||||
raise ValueError("Invalid position {}!".format(status_position))
|
||||
|
||||
for widget in widgets:
|
||||
self._vbox.addWidget(widget)
|
||||
|
||||
def _load_state_geometry(self):
|
||||
"""Load the geometry from the state file."""
|
||||
@@ -331,12 +345,8 @@ class MainWindow(QWidget):
|
||||
|
||||
tabs.tab_index_changed.connect(status.tabindex.on_tab_index_changed)
|
||||
|
||||
tabs.current_tab_changed.connect(status.txt.on_tab_changed)
|
||||
tabs.cur_statusbar_message.connect(status.txt.on_statusbar_message)
|
||||
tabs.cur_load_started.connect(status.txt.on_load_started)
|
||||
|
||||
tabs.current_tab_changed.connect(status.url.on_tab_changed)
|
||||
tabs.cur_url_text_changed.connect(status.url.set_url)
|
||||
tabs.cur_url_changed.connect(status.url.set_url)
|
||||
tabs.cur_link_hovered.connect(status.url.set_hover_url)
|
||||
tabs.cur_load_status_changed.connect(status.url.on_load_status_changed)
|
||||
|
||||
@@ -369,12 +379,19 @@ class MainWindow(QWidget):
|
||||
height = contents_height
|
||||
else:
|
||||
contents_height = -1
|
||||
# hpoint now would be the bottom-left edge of the widget if it was on
|
||||
# the top of the main window.
|
||||
topleft_y = self.height() - self.status.height() - height
|
||||
topleft_y = qtutils.check_overflow(topleft_y, 'int', fatal=False)
|
||||
topleft = QPoint(0, topleft_y)
|
||||
bottomright = self.status.geometry().topRight()
|
||||
status_position = config.get('ui', 'status-position')
|
||||
if status_position == 'bottom':
|
||||
top = self.height() - self.status.height() - height
|
||||
top = qtutils.check_overflow(top, 'int', fatal=False)
|
||||
topleft = QPoint(0, top)
|
||||
bottomright = self.status.geometry().topRight()
|
||||
elif status_position == 'top':
|
||||
topleft = self.status.geometry().bottomLeft()
|
||||
bottom = self.status.height() + height
|
||||
bottom = qtutils.check_overflow(bottom, 'int', fatal=False)
|
||||
bottomright = QPoint(self.width(), bottom)
|
||||
else:
|
||||
raise ValueError("Invalid position {}!".format(status_position))
|
||||
rect = QRect(topleft, bottomright)
|
||||
log.misc.debug('completion rect: {}'.format(rect))
|
||||
if rect.isValid():
|
||||
|
||||
@@ -329,12 +329,12 @@ class StatusBar(QWidget):
|
||||
log.statusbar.debug("Setting command_active to {}".format(val))
|
||||
self._command_active = val
|
||||
elif mode == usertypes.KeyMode.caret:
|
||||
webview = objreg.get('tabbed-browser', scope='window',
|
||||
window=self._win_id).currentWidget()
|
||||
tab = objreg.get('tabbed-browser', scope='window',
|
||||
window=self._win_id).currentWidget()
|
||||
log.statusbar.debug("Setting caret_mode - val {}, selection "
|
||||
"{}".format(val, webview.selection_enabled))
|
||||
"{}".format(val, tab.caret.selection_enabled))
|
||||
if val:
|
||||
if webview.selection_enabled:
|
||||
if tab.caret.selection_enabled:
|
||||
self._set_mode_text("{} selection".format(mode.name))
|
||||
self._caret_mode = CaretMode.selection
|
||||
else:
|
||||
@@ -519,9 +519,9 @@ class StatusBar(QWidget):
|
||||
window=self._win_id)
|
||||
if keyparsers[mode].passthrough:
|
||||
self._set_mode_text(mode.name)
|
||||
if mode in (usertypes.KeyMode.insert,
|
||||
if mode in [usertypes.KeyMode.insert,
|
||||
usertypes.KeyMode.command,
|
||||
usertypes.KeyMode.caret):
|
||||
usertypes.KeyMode.caret]:
|
||||
self.set_mode_active(mode, True)
|
||||
|
||||
@pyqtSlot(usertypes.KeyMode, usertypes.KeyMode)
|
||||
@@ -534,9 +534,9 @@ class StatusBar(QWidget):
|
||||
self._set_mode_text(new_mode.name)
|
||||
else:
|
||||
self.txt.set_text(self.txt.Text.normal, '')
|
||||
if old_mode in (usertypes.KeyMode.insert,
|
||||
if old_mode in [usertypes.KeyMode.insert,
|
||||
usertypes.KeyMode.command,
|
||||
usertypes.KeyMode.caret):
|
||||
usertypes.KeyMode.caret]:
|
||||
self.set_mode_active(old_mode, False)
|
||||
|
||||
@config.change_filter('ui', 'message-timeout')
|
||||
|
||||
@@ -21,8 +21,8 @@
|
||||
|
||||
from PyQt5.QtCore import pyqtSlot
|
||||
|
||||
from qutebrowser.browser import browsertab
|
||||
from qutebrowser.mainwindow.statusbar import textbase
|
||||
from qutebrowser.browser import webview
|
||||
|
||||
|
||||
class Percentage(textbase.TextBase):
|
||||
@@ -46,10 +46,12 @@ class Percentage(textbase.TextBase):
|
||||
self.setText('[top]')
|
||||
elif y == 100:
|
||||
self.setText('[bot]')
|
||||
elif y is None:
|
||||
self.setText('[???]')
|
||||
else:
|
||||
self.setText('[{:2}%]'.format(y))
|
||||
|
||||
@pyqtSlot(webview.WebView)
|
||||
@pyqtSlot(browsertab.AbstractTab)
|
||||
def on_tab_changed(self, tab):
|
||||
"""Update scroll position when tab changed."""
|
||||
self.set_perc(*tab.scroll_pos)
|
||||
self.set_perc(*tab.scroller.pos_perc())
|
||||
|
||||
@@ -22,9 +22,9 @@
|
||||
from PyQt5.QtCore import pyqtSlot, QSize
|
||||
from PyQt5.QtWidgets import QProgressBar, QSizePolicy
|
||||
|
||||
from qutebrowser.browser import webview
|
||||
from qutebrowser.browser import browsertab
|
||||
from qutebrowser.config import style
|
||||
from qutebrowser.utils import utils
|
||||
from qutebrowser.utils import utils, usertypes
|
||||
|
||||
|
||||
class Progress(QProgressBar):
|
||||
@@ -59,15 +59,15 @@ class Progress(QProgressBar):
|
||||
self.setValue(0)
|
||||
self.show()
|
||||
|
||||
@pyqtSlot(webview.WebView)
|
||||
@pyqtSlot(browsertab.AbstractTab)
|
||||
def on_tab_changed(self, tab):
|
||||
"""Set the correct value when the current tab changed."""
|
||||
if self is None: # pragma: no branch
|
||||
# This should never happen, but for some weird reason it does
|
||||
# sometimes.
|
||||
return # pragma: no cover
|
||||
self.setValue(tab.progress)
|
||||
if tab.load_status == webview.LoadStatus.loading:
|
||||
self.setValue(tab.progress())
|
||||
if tab.load_status() == usertypes.LoadStatus.loading:
|
||||
self.show()
|
||||
else:
|
||||
self.hide()
|
||||
|
||||
@@ -80,6 +80,7 @@ class Prompter(QObject):
|
||||
usertypes.PromptMode.text: usertypes.KeyMode.prompt,
|
||||
usertypes.PromptMode.user_pwd: usertypes.KeyMode.prompt,
|
||||
usertypes.PromptMode.alert: usertypes.KeyMode.prompt,
|
||||
usertypes.PromptMode.download: usertypes.KeyMode.prompt,
|
||||
}
|
||||
|
||||
show_prompt = pyqtSignal()
|
||||
@@ -152,33 +153,49 @@ class Prompter(QObject):
|
||||
modeman.enter(self._win_id, mode, 'question asked')
|
||||
return True
|
||||
|
||||
def _display_question_yesno(self, prompt):
|
||||
"""Display a yes/no question."""
|
||||
if self._question.default is None:
|
||||
suffix = ""
|
||||
elif self._question.default:
|
||||
suffix = " (yes)"
|
||||
else:
|
||||
suffix = " (no)"
|
||||
prompt.txt.setText(self._question.text + suffix)
|
||||
prompt.lineedit.hide()
|
||||
|
||||
def _display_question_input(self, prompt):
|
||||
"""Display a question with an input."""
|
||||
text = self._question.text
|
||||
if self._question.mode == usertypes.PromptMode.download:
|
||||
key_mode = self.KEY_MODES[self._question.mode]
|
||||
key_config = objreg.get('key-config')
|
||||
all_bindings = key_config.get_reverse_bindings_for(key_mode.name)
|
||||
bindings = all_bindings.get('prompt-open-download', [])
|
||||
if bindings:
|
||||
text += ' ({} to open)'.format(bindings[0])
|
||||
prompt.txt.setText(text)
|
||||
if self._question.default:
|
||||
prompt.lineedit.setText(self._question.default)
|
||||
prompt.lineedit.show()
|
||||
|
||||
def _display_question_alert(self, prompt):
|
||||
"""Display a JS alert 'question'."""
|
||||
prompt.txt.setText(self._question.text + ' (ok)')
|
||||
prompt.lineedit.hide()
|
||||
|
||||
def _display_question(self):
|
||||
"""Display the question saved in self._question."""
|
||||
prompt = objreg.get('prompt', scope='window', window=self._win_id)
|
||||
if self._question.mode == usertypes.PromptMode.yesno:
|
||||
if self._question.default is None:
|
||||
suffix = ""
|
||||
elif self._question.default:
|
||||
suffix = " (yes)"
|
||||
else:
|
||||
suffix = " (no)"
|
||||
prompt.txt.setText(self._question.text + suffix)
|
||||
prompt.lineedit.hide()
|
||||
elif self._question.mode == usertypes.PromptMode.text:
|
||||
prompt.txt.setText(self._question.text)
|
||||
if self._question.default:
|
||||
prompt.lineedit.setText(self._question.default)
|
||||
prompt.lineedit.show()
|
||||
elif self._question.mode == usertypes.PromptMode.user_pwd:
|
||||
prompt.txt.setText(self._question.text)
|
||||
if self._question.default:
|
||||
prompt.lineedit.setText(self._question.default)
|
||||
prompt.lineedit.show()
|
||||
elif self._question.mode == usertypes.PromptMode.alert:
|
||||
prompt.txt.setText(self._question.text + ' (ok)')
|
||||
prompt.lineedit.hide()
|
||||
else:
|
||||
raise ValueError("Invalid prompt mode!")
|
||||
handlers = {
|
||||
usertypes.PromptMode.yesno: self._display_question_yesno,
|
||||
usertypes.PromptMode.text: self._display_question_input,
|
||||
usertypes.PromptMode.user_pwd: self._display_question_input,
|
||||
usertypes.PromptMode.download: self._display_question_input,
|
||||
usertypes.PromptMode.alert: self._display_question_alert,
|
||||
}
|
||||
handler = handlers[self._question.mode]
|
||||
handler(prompt)
|
||||
log.modes.debug("Question asked, focusing {!r}".format(
|
||||
prompt.lineedit))
|
||||
prompt.lineedit.setFocus()
|
||||
@@ -207,7 +224,7 @@ class Prompter(QObject):
|
||||
def on_mode_left(self, mode):
|
||||
"""Clear and reset input when the mode was left."""
|
||||
prompt = objreg.get('prompt', scope='window', window=self._win_id)
|
||||
if mode in (usertypes.KeyMode.prompt, usertypes.KeyMode.yesno):
|
||||
if mode in [usertypes.KeyMode.prompt, usertypes.KeyMode.yesno]:
|
||||
prompt.txt.setText('')
|
||||
prompt.lineedit.clear()
|
||||
prompt.lineedit.setEchoMode(QLineEdit.Normal)
|
||||
@@ -248,6 +265,13 @@ class Prompter(QObject):
|
||||
modeman.maybe_leave(self._win_id, usertypes.KeyMode.prompt,
|
||||
'prompt accept')
|
||||
self._question.done()
|
||||
elif self._question.mode == usertypes.PromptMode.download:
|
||||
# User just entered a path for a download.
|
||||
target = usertypes.FileDownloadTarget(prompt.lineedit.text())
|
||||
self._question.answer = target
|
||||
modeman.maybe_leave(self._win_id, usertypes.KeyMode.prompt,
|
||||
'prompt accept')
|
||||
self._question.done()
|
||||
elif self._question.mode == usertypes.PromptMode.yesno:
|
||||
# User wants to accept the default of a yes/no question.
|
||||
self._question.answer = self._question.default
|
||||
@@ -287,6 +311,18 @@ class Prompter(QObject):
|
||||
'prompt accept')
|
||||
self._question.done()
|
||||
|
||||
@cmdutils.register(instance='prompter', hide=True, scope='window',
|
||||
modes=[usertypes.KeyMode.prompt])
|
||||
def prompt_open_download(self):
|
||||
"""Immediately open a download."""
|
||||
if self._question.mode != usertypes.PromptMode.download:
|
||||
# We just ignore this if we don't have a download question.
|
||||
return
|
||||
self._question.answer = usertypes.OpenFileDownloadTarget()
|
||||
modeman.maybe_leave(self._win_id, usertypes.KeyMode.prompt,
|
||||
'download open')
|
||||
self._question.done()
|
||||
|
||||
@pyqtSlot(usertypes.Question, bool)
|
||||
def ask_question(self, question, blocking):
|
||||
"""Display a question in the statusbar.
|
||||
|
||||
@@ -21,10 +21,8 @@
|
||||
|
||||
from PyQt5.QtCore import pyqtSlot
|
||||
|
||||
from qutebrowser.config import config
|
||||
from qutebrowser.mainwindow.statusbar import textbase
|
||||
from qutebrowser.utils import usertypes, log, objreg
|
||||
from qutebrowser.browser import webview
|
||||
from qutebrowser.utils import usertypes, log
|
||||
|
||||
|
||||
class Text(textbase.TextBase):
|
||||
@@ -34,20 +32,17 @@ class Text(textbase.TextBase):
|
||||
Attributes:
|
||||
_normaltext: The "permanent" text. Never automatically cleared.
|
||||
_temptext: The temporary text to display.
|
||||
_jstext: The text javascript wants to display.
|
||||
|
||||
The temptext is shown from StatusBar when a temporary text or error is
|
||||
available. If not, the permanent text is shown.
|
||||
"""
|
||||
|
||||
Text = usertypes.enum('Text', ['normal', 'temp', 'js'])
|
||||
Text = usertypes.enum('Text', ['normal', 'temp'])
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self._normaltext = ''
|
||||
self._temptext = ''
|
||||
self._jstext = ''
|
||||
objreg.get('config').changed.connect(self.update_text)
|
||||
|
||||
def set_text(self, which, text):
|
||||
"""Set a text.
|
||||
@@ -62,8 +57,6 @@ class Text(textbase.TextBase):
|
||||
self._normaltext = text
|
||||
elif which is self.Text.temp:
|
||||
self._temptext = text
|
||||
elif which is self.Text.js:
|
||||
self._jstext = text
|
||||
else:
|
||||
raise ValueError("Invalid value {} for which!".format(which))
|
||||
self.update_text()
|
||||
@@ -77,29 +70,11 @@ class Text(textbase.TextBase):
|
||||
else:
|
||||
log.statusbar.debug("Ignoring reset: '{}'".format(text))
|
||||
|
||||
@config.change_filter('ui', 'display-statusbar-messages')
|
||||
def update_text(self):
|
||||
"""Update QLabel text when needed."""
|
||||
if self._temptext:
|
||||
self.setText(self._temptext)
|
||||
elif self._jstext and config.get('ui', 'display-statusbar-messages'):
|
||||
self.setText(self._jstext)
|
||||
elif self._normaltext:
|
||||
self.setText(self._normaltext)
|
||||
else:
|
||||
self.setText('')
|
||||
|
||||
@pyqtSlot(str)
|
||||
def on_statusbar_message(self, val):
|
||||
"""Called when javascript tries to set a statusbar message."""
|
||||
self._jstext = val
|
||||
|
||||
@pyqtSlot()
|
||||
def on_load_started(self):
|
||||
"""Clear jstext when page loading started."""
|
||||
self._jstext = ''
|
||||
|
||||
@pyqtSlot(webview.WebView)
|
||||
def on_tab_changed(self, tab):
|
||||
"""Set the correct jstext when the current tab changed."""
|
||||
self._jstext = tab.statusbar_message
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
|
||||
from PyQt5.QtCore import pyqtSlot, pyqtProperty, Qt, QUrl
|
||||
|
||||
from qutebrowser.browser import webview
|
||||
from qutebrowser.browser import browsertab
|
||||
from qutebrowser.mainwindow.statusbar import textbase
|
||||
from qutebrowser.config import style
|
||||
from qutebrowser.utils import usertypes
|
||||
@@ -119,29 +119,32 @@ class UrlText(textbase.TextBase):
|
||||
Args:
|
||||
status_str: The LoadStatus as string.
|
||||
"""
|
||||
status = webview.LoadStatus[status_str]
|
||||
if status in (webview.LoadStatus.success,
|
||||
webview.LoadStatus.success_https,
|
||||
webview.LoadStatus.error,
|
||||
webview.LoadStatus.warn):
|
||||
status = usertypes.LoadStatus[status_str]
|
||||
if status in [usertypes.LoadStatus.success,
|
||||
usertypes.LoadStatus.success_https,
|
||||
usertypes.LoadStatus.error,
|
||||
usertypes.LoadStatus.warn]:
|
||||
self._normal_url_type = UrlType[status_str]
|
||||
else:
|
||||
self._normal_url_type = UrlType.normal
|
||||
self._update_url()
|
||||
|
||||
@pyqtSlot(str)
|
||||
def set_url(self, s):
|
||||
@pyqtSlot(QUrl)
|
||||
def set_url(self, url):
|
||||
"""Setter to be used as a Qt slot.
|
||||
|
||||
Args:
|
||||
s: The URL to set as string.
|
||||
url: The URL to set as QUrl, or None.
|
||||
"""
|
||||
self._normal_url = s
|
||||
if url is None:
|
||||
self._normal_url = None
|
||||
else:
|
||||
self._normal_url = url.toDisplayString()
|
||||
self._normal_url_type = UrlType.normal
|
||||
self._update_url()
|
||||
|
||||
@pyqtSlot(str, str, str)
|
||||
def set_hover_url(self, link, _title, _text):
|
||||
@pyqtSlot(str)
|
||||
def set_hover_url(self, link):
|
||||
"""Setter to be used as a Qt slot.
|
||||
|
||||
Saves old shown URL in self._old_url and restores it later if a link is
|
||||
@@ -149,8 +152,6 @@ class UrlText(textbase.TextBase):
|
||||
|
||||
Args:
|
||||
link: The link which was hovered (string)
|
||||
_title: The title of the hovered link (string)
|
||||
_text: The text of the hovered link (string)
|
||||
"""
|
||||
if link:
|
||||
qurl = QUrl(link)
|
||||
@@ -162,10 +163,10 @@ class UrlText(textbase.TextBase):
|
||||
self._hover_url = None
|
||||
self._update_url()
|
||||
|
||||
@pyqtSlot(webview.WebView)
|
||||
@pyqtSlot(browsertab.AbstractTab)
|
||||
def on_tab_changed(self, tab):
|
||||
"""Update URL if the tab changed."""
|
||||
self._hover_url = None
|
||||
self._normal_url = tab.cur_url.toDisplayString()
|
||||
self.on_load_status_changed(tab.load_status.name)
|
||||
self._normal_url = tab.url().toDisplayString()
|
||||
self.on_load_status_changed(tab.load_status().name)
|
||||
self._update_url()
|
||||
|
||||
@@ -29,7 +29,7 @@ from PyQt5.QtGui import QIcon
|
||||
from qutebrowser.config import config
|
||||
from qutebrowser.keyinput import modeman
|
||||
from qutebrowser.mainwindow import tabwidget
|
||||
from qutebrowser.browser import signalfilter, webview
|
||||
from qutebrowser.browser import signalfilter, browsertab
|
||||
from qutebrowser.utils import (log, usertypes, utils, qtutils, objreg,
|
||||
urlutils, message)
|
||||
|
||||
@@ -55,8 +55,8 @@ class TabbedBrowser(tabwidget.TabWidget):
|
||||
emitted if the signal occurred in the current tab.
|
||||
|
||||
Attributes:
|
||||
search_text/search_flags: Search parameters which are shared between
|
||||
all tabs.
|
||||
search_text/search_options: Search parameters which are shared between
|
||||
all tabs.
|
||||
_win_id: The window ID this tabbedbrowser is associated with.
|
||||
_filter: A SignalFilter instance.
|
||||
_now_focused: The tab which is focused now.
|
||||
@@ -70,13 +70,11 @@ class TabbedBrowser(tabwidget.TabWidget):
|
||||
default_window_icon: The qutebrowser window icon
|
||||
|
||||
Signals:
|
||||
cur_progress: Progress of the current tab changed (loadProgress).
|
||||
cur_load_started: Current tab started loading (loadStarted)
|
||||
cur_load_finished: Current tab finished loading (loadFinished)
|
||||
cur_statusbar_message: Current tab got a statusbar message
|
||||
(statusBarMessage)
|
||||
cur_url_text_changed: Current URL text changed.
|
||||
cur_link_hovered: Link hovered in current tab (linkHovered)
|
||||
cur_progress: Progress of the current tab changed (load_progress).
|
||||
cur_load_started: Current tab started loading (load_started)
|
||||
cur_load_finished: Current tab finished loading (load_finished)
|
||||
cur_url_changed: Current URL changed.
|
||||
cur_link_hovered: Link hovered in current tab (link_hovered)
|
||||
cur_scroll_perc_changed: Scroll percentage of current tab changed.
|
||||
arg 1: x-position in %.
|
||||
arg 2: y-position in %.
|
||||
@@ -85,22 +83,21 @@ class TabbedBrowser(tabwidget.TabWidget):
|
||||
resized: Emitted when the browser window has resized, so the completion
|
||||
widget can adjust its size to it.
|
||||
arg: The new size.
|
||||
current_tab_changed: The current tab changed to the emitted WebView.
|
||||
current_tab_changed: The current tab changed to the emitted tab.
|
||||
new_tab: Emits the new WebView and its index when a new tab is opened.
|
||||
"""
|
||||
|
||||
cur_progress = pyqtSignal(int)
|
||||
cur_load_started = pyqtSignal()
|
||||
cur_load_finished = pyqtSignal(bool)
|
||||
cur_statusbar_message = pyqtSignal(str)
|
||||
cur_url_text_changed = pyqtSignal(str)
|
||||
cur_link_hovered = pyqtSignal(str, str, str)
|
||||
cur_url_changed = pyqtSignal(QUrl)
|
||||
cur_link_hovered = pyqtSignal(str)
|
||||
cur_scroll_perc_changed = pyqtSignal(int, int)
|
||||
cur_load_status_changed = pyqtSignal(str)
|
||||
close_window = pyqtSignal()
|
||||
resized = pyqtSignal('QRect')
|
||||
current_tab_changed = pyqtSignal(webview.WebView)
|
||||
new_tab = pyqtSignal(webview.WebView, int)
|
||||
current_tab_changed = pyqtSignal(browsertab.AbstractTab)
|
||||
new_tab = pyqtSignal(browsertab.AbstractTab, int)
|
||||
|
||||
def __init__(self, win_id, parent=None):
|
||||
super().__init__(win_id, parent)
|
||||
@@ -116,7 +113,7 @@ class TabbedBrowser(tabwidget.TabWidget):
|
||||
self._filter = signalfilter.SignalFilter(win_id, self)
|
||||
self._now_focused = None
|
||||
self.search_text = None
|
||||
self.search_flags = 0
|
||||
self.search_options = {}
|
||||
self._local_marks = {}
|
||||
self._global_marks = {}
|
||||
self.default_window_icon = self.window().windowIcon()
|
||||
@@ -161,67 +158,46 @@ class TabbedBrowser(tabwidget.TabWidget):
|
||||
# (e.g. last tab removed)
|
||||
log.webview.debug("Not updating window title because index is -1")
|
||||
return
|
||||
tabtitle = self.page_title(idx)
|
||||
widget = self.widget(idx)
|
||||
|
||||
fields = {}
|
||||
if widget.load_status == webview.LoadStatus.loading:
|
||||
fields['perc'] = '[{}%] '.format(widget.progress)
|
||||
else:
|
||||
fields['perc'] = ''
|
||||
fields['perc_raw'] = widget.progress
|
||||
fields['title'] = tabtitle
|
||||
fields['title_sep'] = ' - ' if tabtitle else ''
|
||||
fields = self.get_tab_fields(idx)
|
||||
fields['id'] = self._win_id
|
||||
y = widget.scroll_pos[1]
|
||||
if y <= 0:
|
||||
scroll_pos = 'top'
|
||||
elif y >= 100:
|
||||
scroll_pos = 'bot'
|
||||
else:
|
||||
scroll_pos = '{:2}%'.format(y)
|
||||
|
||||
fields['scroll_pos'] = scroll_pos
|
||||
fmt = config.get('ui', 'window-title-format')
|
||||
self.window().setWindowTitle(fmt.format(**fields))
|
||||
|
||||
def _connect_tab_signals(self, tab):
|
||||
"""Set up the needed signals for tab."""
|
||||
page = tab.page()
|
||||
frame = page.mainFrame()
|
||||
# filtered signals
|
||||
tab.linkHovered.connect(
|
||||
tab.link_hovered.connect(
|
||||
self._filter.create(self.cur_link_hovered, tab))
|
||||
tab.loadProgress.connect(
|
||||
tab.load_progress.connect(
|
||||
self._filter.create(self.cur_progress, tab))
|
||||
frame.loadFinished.connect(
|
||||
tab.load_finished.connect(
|
||||
self._filter.create(self.cur_load_finished, tab))
|
||||
frame.loadStarted.connect(
|
||||
tab.load_started.connect(
|
||||
self._filter.create(self.cur_load_started, tab))
|
||||
tab.statusBarMessage.connect(
|
||||
self._filter.create(self.cur_statusbar_message, tab))
|
||||
tab.scroll_pos_changed.connect(
|
||||
tab.scroller.perc_changed.connect(
|
||||
self._filter.create(self.cur_scroll_perc_changed, tab))
|
||||
tab.scroll_pos_changed.connect(self.on_scroll_pos_changed)
|
||||
tab.url_text_changed.connect(
|
||||
self._filter.create(self.cur_url_text_changed, tab))
|
||||
tab.scroller.perc_changed.connect(self.on_scroll_pos_changed)
|
||||
tab.url_changed.connect(
|
||||
self._filter.create(self.cur_url_changed, tab))
|
||||
tab.load_status_changed.connect(
|
||||
self._filter.create(self.cur_load_status_changed, tab))
|
||||
tab.url_text_changed.connect(
|
||||
functools.partial(self.on_url_text_changed, tab))
|
||||
tab.url_changed.connect(
|
||||
functools.partial(self.on_url_changed, tab))
|
||||
# misc
|
||||
tab.titleChanged.connect(
|
||||
tab.title_changed.connect(
|
||||
functools.partial(self.on_title_changed, tab))
|
||||
tab.iconChanged.connect(
|
||||
tab.icon_changed.connect(
|
||||
functools.partial(self.on_icon_changed, tab))
|
||||
tab.loadProgress.connect(
|
||||
tab.load_progress.connect(
|
||||
functools.partial(self.on_load_progress, tab))
|
||||
frame.loadFinished.connect(
|
||||
tab.load_finished.connect(
|
||||
functools.partial(self.on_load_finished, tab))
|
||||
frame.loadStarted.connect(
|
||||
tab.load_started.connect(
|
||||
functools.partial(self.on_load_started, tab))
|
||||
page.windowCloseRequested.connect(
|
||||
tab.window_close_requested.connect(
|
||||
functools.partial(self.on_window_close_requested, tab))
|
||||
tab.new_tab_requested.connect(self.tabopen)
|
||||
|
||||
def current_url(self):
|
||||
"""Get the URL of the current tab.
|
||||
@@ -231,14 +207,8 @@ class TabbedBrowser(tabwidget.TabWidget):
|
||||
Return:
|
||||
The current URL as QUrl.
|
||||
"""
|
||||
widget = self.currentWidget()
|
||||
if widget is None:
|
||||
url = QUrl()
|
||||
else:
|
||||
url = widget.cur_url
|
||||
# It's possible for url to be invalid, but the caller will handle that.
|
||||
qtutils.ensure_valid(url)
|
||||
return url
|
||||
idx = self.currentIndex()
|
||||
return super().tab_url(idx)
|
||||
|
||||
def shutdown(self):
|
||||
"""Try to shut down all tabs cleanly."""
|
||||
@@ -288,12 +258,12 @@ class TabbedBrowser(tabwidget.TabWidget):
|
||||
window=self._win_id):
|
||||
objreg.delete('last-focused-tab', scope='window',
|
||||
window=self._win_id)
|
||||
if tab.cur_url.isValid():
|
||||
history_data = qtutils.serialize(tab.history())
|
||||
entry = UndoEntry(tab.cur_url, history_data)
|
||||
if tab.url().isValid():
|
||||
history_data = tab.history.serialize()
|
||||
entry = UndoEntry(tab.url(), history_data)
|
||||
self._undo_stack.append(entry)
|
||||
elif tab.cur_url.isEmpty():
|
||||
# There are some good reasons why an URL could be empty
|
||||
elif tab.url().isEmpty():
|
||||
# There are some good reasons why a URL could be empty
|
||||
# (target="_blank" with a download, see [1]), so we silently ignore
|
||||
# this.
|
||||
# [1] https://github.com/The-Compiler/qutebrowser/issues/163
|
||||
@@ -302,7 +272,7 @@ class TabbedBrowser(tabwidget.TabWidget):
|
||||
# We display a warnings for URLs which are not empty but invalid -
|
||||
# but we don't return here because we want the tab to close either
|
||||
# way.
|
||||
urlutils.invalid_url_error(self._win_id, tab.cur_url, "saving tab")
|
||||
urlutils.invalid_url_error(self._win_id, tab.url(), "saving tab")
|
||||
tab.shutdown()
|
||||
self.removeTab(idx)
|
||||
tab.deleteLater()
|
||||
@@ -314,13 +284,13 @@ class TabbedBrowser(tabwidget.TabWidget):
|
||||
use_current_tab = False
|
||||
if last_close in ['blank', 'startpage', 'default-page']:
|
||||
only_one_tab_open = self.count() == 1
|
||||
no_history = self.widget(0).history().count() == 1
|
||||
no_history = len(self.widget(0).history) == 1
|
||||
urls = {
|
||||
'blank': QUrl('about:blank'),
|
||||
'startpage': QUrl(config.get('general', 'startpage')[0]),
|
||||
'default-page': config.get('general', 'default-page'),
|
||||
}
|
||||
first_tab_url = self.widget(0).page().mainFrame().requestedUrl()
|
||||
first_tab_url = self.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
|
||||
@@ -335,7 +305,7 @@ class TabbedBrowser(tabwidget.TabWidget):
|
||||
else:
|
||||
newtab = self.tabopen(url, background=False)
|
||||
|
||||
qtutils.deserialize(history_data, newtab.history())
|
||||
newtab.history.deserialize(history_data)
|
||||
|
||||
@pyqtSlot('QUrl', bool)
|
||||
def openurl(self, url, newtab):
|
||||
@@ -361,7 +331,7 @@ class TabbedBrowser(tabwidget.TabWidget):
|
||||
return
|
||||
self.close_tab(tab)
|
||||
|
||||
@pyqtSlot(webview.WebView)
|
||||
@pyqtSlot(browsertab.AbstractTab)
|
||||
def on_window_close_requested(self, widget):
|
||||
"""Close a tab with a widget given."""
|
||||
try:
|
||||
@@ -370,6 +340,7 @@ class TabbedBrowser(tabwidget.TabWidget):
|
||||
log.webview.debug("Requested to close {!r} which does not "
|
||||
"exist!".format(widget))
|
||||
|
||||
@pyqtSlot('QUrl')
|
||||
@pyqtSlot('QUrl', bool)
|
||||
def tabopen(self, url=None, background=None, explicit=False):
|
||||
"""Open a new tab with a given URL.
|
||||
@@ -401,10 +372,13 @@ class TabbedBrowser(tabwidget.TabWidget):
|
||||
tabbed_browser = objreg.get('tabbed-browser', scope='window',
|
||||
window=window.win_id)
|
||||
return tabbed_browser.tabopen(url, background, explicit)
|
||||
tab = webview.WebView(self._win_id, self)
|
||||
|
||||
tab = browsertab.create(win_id=self._win_id, parent=self)
|
||||
self._connect_tab_signals(tab)
|
||||
|
||||
idx = self._get_new_tab_idx(explicit)
|
||||
self.insertTab(idx, tab, "")
|
||||
|
||||
if url is not None:
|
||||
tab.openurl(url)
|
||||
if background is None:
|
||||
@@ -480,8 +454,8 @@ class TabbedBrowser(tabwidget.TabWidget):
|
||||
# We can get signals for tabs we already deleted...
|
||||
return
|
||||
self.update_tab_title(idx)
|
||||
if tab.keep_icon:
|
||||
tab.keep_icon = False
|
||||
if tab.data.keep_icon:
|
||||
tab.data.keep_icon = False
|
||||
else:
|
||||
self.setTabIcon(idx, QIcon())
|
||||
if (config.get('tabs', 'tabs-are-windows') and
|
||||
@@ -498,11 +472,11 @@ class TabbedBrowser(tabwidget.TabWidget):
|
||||
modeman.maybe_leave(self._win_id, usertypes.KeyMode.hint,
|
||||
'load started')
|
||||
|
||||
@pyqtSlot(webview.WebView, str)
|
||||
@pyqtSlot(browsertab.AbstractTab, str)
|
||||
def on_title_changed(self, tab, text):
|
||||
"""Set the title of a tab.
|
||||
|
||||
Slot for the titleChanged signal of any tab.
|
||||
Slot for the title_changed signal of any tab.
|
||||
|
||||
Args:
|
||||
tab: The WebView where the title was changed.
|
||||
@@ -522,8 +496,8 @@ class TabbedBrowser(tabwidget.TabWidget):
|
||||
if idx == self.currentIndex():
|
||||
self.update_window_title()
|
||||
|
||||
@pyqtSlot(webview.WebView, str)
|
||||
def on_url_text_changed(self, tab, url):
|
||||
@pyqtSlot(browsertab.AbstractTab, QUrl)
|
||||
def on_url_changed(self, tab, url):
|
||||
"""Set the new URL as title if there's no title yet.
|
||||
|
||||
Args:
|
||||
@@ -536,16 +510,17 @@ 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)
|
||||
self.set_page_title(idx, url.toDisplayString())
|
||||
|
||||
@pyqtSlot(webview.WebView)
|
||||
def on_icon_changed(self, tab):
|
||||
@pyqtSlot(browsertab.AbstractTab, QIcon)
|
||||
def on_icon_changed(self, tab, icon):
|
||||
"""Set the icon of a tab.
|
||||
|
||||
Slot for the iconChanged signal of any tab.
|
||||
|
||||
Args:
|
||||
tab: The WebView where the title was changed.
|
||||
icon: The new icon
|
||||
"""
|
||||
if not config.get('tabs', 'show-favicons'):
|
||||
return
|
||||
@@ -554,15 +529,15 @@ class TabbedBrowser(tabwidget.TabWidget):
|
||||
except TabDeletedError:
|
||||
# We can get signals for tabs we already deleted...
|
||||
return
|
||||
self.setTabIcon(idx, tab.icon())
|
||||
self.setTabIcon(idx, icon)
|
||||
if config.get('tabs', 'tabs-are-windows'):
|
||||
self.window().setWindowIcon(tab.icon())
|
||||
self.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):
|
||||
if mode in [usertypes.KeyMode.command, usertypes.KeyMode.prompt,
|
||||
usertypes.KeyMode.yesno]:
|
||||
widget = self.currentWidget()
|
||||
log.modes.debug("Left status-input mode, focusing {!r}".format(
|
||||
widget))
|
||||
@@ -579,8 +554,8 @@ class TabbedBrowser(tabwidget.TabWidget):
|
||||
tab = self.widget(idx)
|
||||
log.modes.debug("Current tab changed, focusing {!r}".format(tab))
|
||||
tab.setFocus()
|
||||
for mode in (usertypes.KeyMode.hint, usertypes.KeyMode.insert,
|
||||
usertypes.KeyMode.caret, usertypes.KeyMode.passthrough):
|
||||
for mode in [usertypes.KeyMode.hint, usertypes.KeyMode.insert,
|
||||
usertypes.KeyMode.caret, usertypes.KeyMode.passthrough]:
|
||||
modeman.maybe_leave(self._win_id, mode, 'tab changed')
|
||||
if self._now_focused is not None:
|
||||
objreg.register('last-focused-tab', self._now_focused, update=True,
|
||||
@@ -612,25 +587,20 @@ class TabbedBrowser(tabwidget.TabWidget):
|
||||
if idx == self.currentIndex():
|
||||
self.update_window_title()
|
||||
|
||||
def on_load_finished(self, tab):
|
||||
"""Adjust tab indicator when loading finished.
|
||||
|
||||
We don't take loadFinished's ok argument here as it always seems to be
|
||||
true when the QWebPage has an ErrorPageExtension implemented.
|
||||
See https://github.com/The-Compiler/qutebrowser/issues/84
|
||||
"""
|
||||
def on_load_finished(self, tab, ok):
|
||||
"""Adjust tab indicator when loading finished."""
|
||||
try:
|
||||
idx = self._tab_index(tab)
|
||||
except TabDeletedError:
|
||||
# We can get signals for tabs we already deleted...
|
||||
return
|
||||
if tab.page().error_occurred:
|
||||
color = config.get('colors', 'tabs.indicator.error')
|
||||
else:
|
||||
if ok:
|
||||
start = config.get('colors', 'tabs.indicator.start')
|
||||
stop = config.get('colors', 'tabs.indicator.stop')
|
||||
system = config.get('colors', 'tabs.indicator.system')
|
||||
color = utils.interpolate_color(start, stop, 100, system)
|
||||
else:
|
||||
color = config.get('colors', 'tabs.indicator.error')
|
||||
self.set_tab_indicator_color(idx, color)
|
||||
self.update_tab_title(idx)
|
||||
if idx == self.currentIndex():
|
||||
@@ -676,7 +646,7 @@ class TabbedBrowser(tabwidget.TabWidget):
|
||||
if key != "'":
|
||||
message.error(self._win_id, "Failed to set mark: url invalid")
|
||||
return
|
||||
point = self.currentWidget().page().currentFrame().scrollPosition()
|
||||
point = self.currentWidget().scroller.pos_px()
|
||||
|
||||
if key.isupper():
|
||||
self._global_marks[key] = point, url
|
||||
@@ -691,21 +661,29 @@ class TabbedBrowser(tabwidget.TabWidget):
|
||||
Args:
|
||||
key: mark identifier; capital indicates a global mark
|
||||
"""
|
||||
# consider urls that differ only in fragment to be identical
|
||||
urlkey = self.current_url().adjusted(QUrl.RemoveFragment)
|
||||
frame = self.currentWidget().page().currentFrame()
|
||||
try:
|
||||
# consider urls that differ only in fragment to be identical
|
||||
urlkey = self.current_url().adjusted(QUrl.RemoveFragment)
|
||||
except qtutils.QtValueError:
|
||||
urlkey = None
|
||||
|
||||
if key.isupper() and key in self._global_marks:
|
||||
point, url = self._global_marks[key]
|
||||
tab = self.currentWidget()
|
||||
|
||||
@pyqtSlot(bool)
|
||||
def callback(ok):
|
||||
if ok:
|
||||
self.cur_load_finished.disconnect(callback)
|
||||
frame.setScrollPosition(point)
|
||||
if key.isupper():
|
||||
if key in self._global_marks:
|
||||
point, url = self._global_marks[key]
|
||||
|
||||
self.openurl(url, newtab=False)
|
||||
self.cur_load_finished.connect(callback)
|
||||
def callback(ok):
|
||||
if ok:
|
||||
self.cur_load_finished.disconnect(callback)
|
||||
tab.scroller.to_point(point)
|
||||
|
||||
self.openurl(url, newtab=False)
|
||||
self.cur_load_finished.connect(callback)
|
||||
else:
|
||||
message.error(self._win_id, "Mark {} is not set".format(key))
|
||||
elif urlkey is None:
|
||||
message.error(self._win_id, "Current URL is invalid!")
|
||||
elif urlkey in self._local_marks and key in self._local_marks[urlkey]:
|
||||
point = self._local_marks[urlkey][key]
|
||||
|
||||
@@ -714,6 +692,6 @@ class TabbedBrowser(tabwidget.TabWidget):
|
||||
# "'" would just jump to the current position every time
|
||||
self.set_mark("'")
|
||||
|
||||
frame.setScrollPosition(point)
|
||||
tab.scroller.to_point(point)
|
||||
else:
|
||||
message.error(self._win_id, "Mark {} is not set".format(key))
|
||||
|
||||
@@ -22,14 +22,13 @@
|
||||
import collections
|
||||
import functools
|
||||
|
||||
from PyQt5.QtCore import pyqtSignal, pyqtSlot, Qt, QSize, QRect, QTimer
|
||||
from PyQt5.QtCore import pyqtSignal, pyqtSlot, Qt, QSize, QRect, QTimer, QUrl
|
||||
from PyQt5.QtWidgets import (QTabWidget, QTabBar, QSizePolicy, QCommonStyle,
|
||||
QStyle, QStylePainter, QStyleOptionTab)
|
||||
from PyQt5.QtGui import QIcon, QPalette, QColor
|
||||
|
||||
from qutebrowser.utils import qtutils, objreg, utils, usertypes
|
||||
from qutebrowser.config import config
|
||||
from qutebrowser.browser import webview
|
||||
|
||||
|
||||
PixelMetrics = usertypes.enum('PixelMetrics', ['icon_padding'],
|
||||
@@ -73,7 +72,7 @@ class TabWidget(QTabWidget):
|
||||
position = config.get('tabs', 'position')
|
||||
selection_behavior = config.get('tabs', 'select-on-remove')
|
||||
self.setTabPosition(position)
|
||||
tabbar.vertical = position in (QTabWidget.West, QTabWidget.East)
|
||||
tabbar.vertical = position in [QTabWidget.West, QTabWidget.East]
|
||||
tabbar.setSelectionBehaviorOnRemove(selection_behavior)
|
||||
tabbar.refresh()
|
||||
|
||||
@@ -99,21 +98,38 @@ class TabWidget(QTabWidget):
|
||||
|
||||
def update_tab_title(self, idx):
|
||||
"""Update the tab text for the given tab."""
|
||||
widget = self.widget(idx)
|
||||
page_title = self.page_title(idx).replace('&', '&&')
|
||||
fields = self.get_tab_fields(idx)
|
||||
fields['title'] = fields['title'].replace('&', '&&')
|
||||
fields['index'] = idx + 1
|
||||
|
||||
fmt = config.get('tabs', 'title-format')
|
||||
self.tabBar().setTabText(idx, fmt.format(**fields))
|
||||
|
||||
def get_tab_fields(self, idx):
|
||||
"""Get the tab field data."""
|
||||
tab = self.widget(idx)
|
||||
page_title = self.page_title(idx)
|
||||
|
||||
fields = {}
|
||||
if widget.load_status == webview.LoadStatus.loading:
|
||||
fields['perc'] = '[{}%] '.format(widget.progress)
|
||||
fields['id'] = tab.tab_id
|
||||
fields['title'] = page_title
|
||||
fields['title_sep'] = ' - ' if page_title else ''
|
||||
fields['perc_raw'] = tab.progress()
|
||||
|
||||
if tab.load_status() == usertypes.LoadStatus.loading:
|
||||
fields['perc'] = '[{}%] '.format(tab.progress())
|
||||
else:
|
||||
fields['perc'] = ''
|
||||
fields['perc_raw'] = widget.progress
|
||||
fields['title'] = page_title
|
||||
fields['index'] = idx + 1
|
||||
fields['id'] = widget.tab_id
|
||||
fields['title_sep'] = ' - ' if page_title else ''
|
||||
y = widget.scroll_pos[1]
|
||||
if y <= 0:
|
||||
|
||||
try:
|
||||
fields['host'] = self.tab_url(idx).host()
|
||||
except qtutils.QtValueError:
|
||||
fields['host'] = ''
|
||||
|
||||
y = tab.scroller.pos_perc()[1]
|
||||
if y is None:
|
||||
scroll_pos = '???'
|
||||
elif y <= 0:
|
||||
scroll_pos = 'top'
|
||||
elif y >= 100:
|
||||
scroll_pos = 'bot'
|
||||
@@ -121,9 +137,7 @@ class TabWidget(QTabWidget):
|
||||
scroll_pos = '{:2}%'.format(y)
|
||||
|
||||
fields['scroll_pos'] = scroll_pos
|
||||
|
||||
fmt = config.get('tabs', 'title-format')
|
||||
self.tabBar().setTabText(idx, fmt.format(**fields))
|
||||
return fields
|
||||
|
||||
@config.change_filter('tabs', 'title-format')
|
||||
def update_tab_titles(self):
|
||||
@@ -205,6 +219,21 @@ class TabWidget(QTabWidget):
|
||||
self.tabBar().on_change()
|
||||
self.tab_index_changed.emit(index, self.count())
|
||||
|
||||
def tab_url(self, idx):
|
||||
"""Get the URL of the tab at the given index.
|
||||
|
||||
Return:
|
||||
The tab URL as QUrl.
|
||||
"""
|
||||
tab = self.widget(idx)
|
||||
if tab is None:
|
||||
url = QUrl()
|
||||
else:
|
||||
url = tab.url()
|
||||
# It's possible for url to be invalid, but the caller will handle that.
|
||||
qtutils.ensure_valid(url)
|
||||
return url
|
||||
|
||||
|
||||
class TabBar(QTabBar):
|
||||
|
||||
@@ -513,11 +542,11 @@ class TabBarStyle(QCommonStyle):
|
||||
style: The base/"parent" style.
|
||||
"""
|
||||
self._style = style
|
||||
for method in ('drawComplexControl', 'drawItemPixmap',
|
||||
for method in ['drawComplexControl', 'drawItemPixmap',
|
||||
'generatedIconPixmap', 'hitTestComplexControl',
|
||||
'itemPixmapRect', 'itemTextRect', 'polish', 'styleHint',
|
||||
'subControlRect', 'unpolish', 'drawItemText',
|
||||
'sizeFromContents', 'drawPrimitive'):
|
||||
'sizeFromContents', 'drawPrimitive']:
|
||||
target = getattr(self._style, method)
|
||||
setattr(self, method, functools.partial(target))
|
||||
super().__init__()
|
||||
|
||||
@@ -36,8 +36,8 @@ from PyQt5.QtWidgets import (QDialog, QLabel, QTextEdit, QPushButton,
|
||||
|
||||
import qutebrowser
|
||||
from qutebrowser.utils import version, log, utils, objreg
|
||||
from qutebrowser.misc import miscwidgets, autoupdate, msgbox, httpclient
|
||||
from qutebrowser.browser.network import pastebin
|
||||
from qutebrowser.misc import (miscwidgets, autoupdate, msgbox, httpclient,
|
||||
pastebin)
|
||||
from qutebrowser.config import config
|
||||
|
||||
|
||||
@@ -420,9 +420,6 @@ class ExceptionCrashDialog(_CrashDialog):
|
||||
text = "<b>Argh! qutebrowser crashed unexpectedly.</b>"
|
||||
self._lbl.setText(text)
|
||||
|
||||
def _init_buttons(self):
|
||||
super()._init_buttons()
|
||||
|
||||
def _init_checkboxes(self):
|
||||
"""Add checkboxes to the dialog."""
|
||||
super()._init_checkboxes()
|
||||
|
||||
@@ -116,7 +116,7 @@ class CrashHandler(QObject):
|
||||
window=win_id)
|
||||
for tab in tabbed_browser.widgets():
|
||||
try:
|
||||
urlstr = tab.cur_url.toString(
|
||||
urlstr = tab.url().toString(
|
||||
QUrl.RemovePassword | QUrl.FullyEncoded)
|
||||
if urlstr:
|
||||
win_pages.append(urlstr)
|
||||
@@ -311,7 +311,7 @@ class SignalHandler(QObject):
|
||||
# pylint: disable=import-error,no-member,useless-suppression
|
||||
import fcntl
|
||||
read_fd, write_fd = os.pipe()
|
||||
for fd in (read_fd, write_fd):
|
||||
for fd in [read_fd, write_fd]:
|
||||
flags = fcntl.fcntl(fd, fcntl.F_GETFL)
|
||||
fcntl.fcntl(fd, fcntl.F_SETFL, flags | os.O_NONBLOCK)
|
||||
self._notifier = QSocketNotifier(read_fd, QSocketNotifier.Read,
|
||||
|
||||
@@ -44,21 +44,27 @@ except ImportError:
|
||||
# initialization needs to take place before that!
|
||||
|
||||
|
||||
def _missing_str(name, *, windows=None, pip=None):
|
||||
def _missing_str(name, *, windows=None, pip=None, webengine=False):
|
||||
"""Get an error string for missing packages.
|
||||
|
||||
Args:
|
||||
name: The name of the package.
|
||||
windows: String to be displayed for Windows.
|
||||
pip: pypi package name.
|
||||
webengine: Whether this is checking the QtWebEngine package
|
||||
"""
|
||||
blocks = ["Fatal error: <b>{}</b> is required to run qutebrowser but "
|
||||
"could not be imported! Maybe it's not installed?".format(name)]
|
||||
lines = ['Please search for the python3 version of {} in your '
|
||||
'distributions packages, or install it via pip.'.format(name)]
|
||||
blocks.append('<br />'.join(lines))
|
||||
lines = ['<b>If you installed a qutebrowser package for your '
|
||||
'distribution, please report this as a bug.</b>']
|
||||
if webengine:
|
||||
lines = ['Note QtWebEngine is not available for some distributions '
|
||||
'(like Debian/Ubuntu), so you need to start without '
|
||||
'--backend webengine there.']
|
||||
else:
|
||||
lines = ['<b>If you installed a qutebrowser package for your '
|
||||
'distribution, please report this as a bug.</b>']
|
||||
blocks.append('<br />'.join(lines))
|
||||
if windows is not None:
|
||||
lines = ["<b>On Windows:</b>"]
|
||||
@@ -169,7 +175,7 @@ def fix_harfbuzz(args):
|
||||
else:
|
||||
log.init.debug("Using old harfbuzz engine (auto)")
|
||||
os.environ['QT_HARFBUZZ'] = 'old'
|
||||
elif args.harfbuzz in ('old', 'new'):
|
||||
elif args.harfbuzz in ['old', 'new']:
|
||||
# forced harfbuzz variant
|
||||
# FIXME looking at the Qt code, 'new' isn't a valid value, but leaving
|
||||
# it empty and using new yields different behavior...
|
||||
@@ -184,7 +190,7 @@ def fix_harfbuzz(args):
|
||||
def check_pyqt_core():
|
||||
"""Check if PyQt core is installed."""
|
||||
try:
|
||||
import PyQt5.QtCore
|
||||
import PyQt5.QtCore # pylint: disable=unused-variable
|
||||
except ImportError as e:
|
||||
text = _missing_str('PyQt5',
|
||||
windows="Use the installer by Riverbank computing "
|
||||
@@ -230,7 +236,7 @@ def check_ssl_support():
|
||||
_die(text)
|
||||
|
||||
|
||||
def check_libraries():
|
||||
def check_libraries(args):
|
||||
"""Check if all needed Python libraries are installed."""
|
||||
modules = {
|
||||
'PyQt5.QtWebKit': _missing_str("PyQt5.QtWebKit"),
|
||||
@@ -257,6 +263,9 @@ def check_libraries():
|
||||
"or Install via pip.",
|
||||
pip="PyYAML"),
|
||||
}
|
||||
if args.backend == 'webengine':
|
||||
modules['PyQt5.QtWebEngineWidgets'] = _missing_str("QtWebEngine",
|
||||
webengine=True)
|
||||
for name, text in modules.items():
|
||||
try:
|
||||
importlib.import_module(name)
|
||||
@@ -300,13 +309,14 @@ def earlyinit(args):
|
||||
# Here we check if QtCore is available, and if not, print a message to the
|
||||
# console or via Tk.
|
||||
check_pyqt_core()
|
||||
# Init logging as early as possible
|
||||
init_log(args)
|
||||
# Now the faulthandler is enabled we fix the Qt harfbuzzing library, before
|
||||
# importing QtWidgets.
|
||||
fix_harfbuzz(args)
|
||||
# Now we can be sure QtCore is available, so we can print dialogs on
|
||||
# errors, so people only using the GUI notice them as well.
|
||||
check_qt_version()
|
||||
check_ssl_support()
|
||||
remove_inputhook()
|
||||
check_libraries()
|
||||
init_log(args)
|
||||
check_libraries(args)
|
||||
check_ssl_support()
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""A HTTP client based on QNetworkAccessManager."""
|
||||
"""An HTTP client based on QNetworkAccessManager."""
|
||||
|
||||
import functools
|
||||
import urllib.request
|
||||
@@ -30,7 +30,7 @@ from PyQt5.QtNetwork import (QNetworkAccessManager, QNetworkRequest,
|
||||
|
||||
class HTTPClient(QObject):
|
||||
|
||||
"""A HTTP client based on QNetworkAccessManager.
|
||||
"""An HTTP client based on QNetworkAccessManager.
|
||||
|
||||
Intended for APIs, automatically decodes data.
|
||||
|
||||
|
||||
@@ -263,8 +263,8 @@ class IPCServer(QObject):
|
||||
log.ipc.debug("We can read a line immediately.")
|
||||
self.on_ready_read()
|
||||
socket.error.connect(self.on_error)
|
||||
if socket.error() not in (QLocalSocket.UnknownSocketError,
|
||||
QLocalSocket.PeerClosedError):
|
||||
if socket.error() not in [QLocalSocket.UnknownSocketError,
|
||||
QLocalSocket.PeerClosedError]:
|
||||
log.ipc.debug("We got an error immediately.")
|
||||
self.on_error(socket.error())
|
||||
socket.disconnected.connect(self.on_disconnected)
|
||||
@@ -311,7 +311,7 @@ class IPCServer(QObject):
|
||||
self._handle_invalid_data()
|
||||
return
|
||||
|
||||
for name in ('args', 'target_arg'):
|
||||
for name in ['args', 'target_arg']:
|
||||
if name not in json_data:
|
||||
log.ipc.error("Missing {}: {}".format(name, decoded.strip()))
|
||||
self._handle_invalid_data()
|
||||
@@ -493,8 +493,8 @@ def send_to_running_instance(socketname, command, target_arg, *,
|
||||
socket.waitForDisconnected(CONNECT_TIMEOUT)
|
||||
return True
|
||||
else:
|
||||
if socket.error() not in (QLocalSocket.ConnectionRefusedError,
|
||||
QLocalSocket.ServerNotFoundError):
|
||||
if socket.error() not in [QLocalSocket.ConnectionRefusedError,
|
||||
QLocalSocket.ServerNotFoundError]:
|
||||
raise SocketError("connecting to running instance", socket)
|
||||
else:
|
||||
log.ipc.debug("No existing instance present (error {})".format(
|
||||
|
||||
@@ -26,7 +26,7 @@ from PyQt5.QtWidgets import QMessageBox
|
||||
|
||||
def msgbox(parent, title, text, *, icon, buttons=QMessageBox.Ok,
|
||||
on_finished=None, plain_text=None):
|
||||
"""Display an QMessageBox with the given icon.
|
||||
"""Display a QMessageBox with the given icon.
|
||||
|
||||
Args:
|
||||
parent: The parent to set for the message box.
|
||||
|
||||
@@ -158,7 +158,23 @@ class ReadlineBridge:
|
||||
widget = self._widget()
|
||||
if widget is None:
|
||||
return
|
||||
widget.cursorWordBackward(True)
|
||||
cursor_position = widget.cursorPosition()
|
||||
text = widget.text()
|
||||
|
||||
target_position = cursor_position
|
||||
|
||||
is_word_boundary = True
|
||||
while is_word_boundary and target_position > 0:
|
||||
is_word_boundary = text[target_position - 1] == " "
|
||||
target_position -= 1
|
||||
|
||||
is_word_boundary = False
|
||||
while not is_word_boundary and target_position > 0:
|
||||
is_word_boundary = text[target_position - 1] == " "
|
||||
target_position -= 1
|
||||
|
||||
moveby = cursor_position - target_position - 1
|
||||
widget.cursorBackward(True, moveby)
|
||||
self._deleted[widget] = widget.selectedText()
|
||||
widget.del_()
|
||||
|
||||
|
||||
@@ -67,7 +67,6 @@ class Saveable:
|
||||
save_on_exit=self._save_on_exit,
|
||||
filename=self._filename)
|
||||
|
||||
@pyqtSlot()
|
||||
def mark_dirty(self):
|
||||
"""Mark this saveable as dirty (having changes)."""
|
||||
log.save.debug("Marking {} as dirty.".format(self._name))
|
||||
|
||||
@@ -31,7 +31,7 @@ try:
|
||||
except ImportError: # pragma: no cover
|
||||
from yaml import SafeLoader as YamlLoader, SafeDumper as YamlDumper
|
||||
|
||||
from qutebrowser.browser import tabhistory
|
||||
from qutebrowser.browser.webkit import tabhistory
|
||||
from qutebrowser.utils import (standarddir, objreg, qtutils, log, usertypes,
|
||||
message)
|
||||
from qutebrowser.commands import cmdexc, cmdutils
|
||||
@@ -130,6 +130,55 @@ class SessionManager(QObject):
|
||||
else:
|
||||
return True
|
||||
|
||||
def _save_tab_item(self, tab, idx, item):
|
||||
"""Save a single history item in a tab.
|
||||
|
||||
Args:
|
||||
tab: The tab to save.
|
||||
idx: The index of the current history item.
|
||||
item: The history item.
|
||||
|
||||
Return:
|
||||
A dict with the saved data for this item.
|
||||
"""
|
||||
data = {
|
||||
'url': bytes(item.url().toEncoded()).decode('ascii'),
|
||||
}
|
||||
|
||||
if item.title():
|
||||
data['title'] = item.title()
|
||||
else:
|
||||
# https://github.com/The-Compiler/qutebrowser/issues/879
|
||||
if tab.history.current_idx() == idx:
|
||||
data['title'] = tab.title()
|
||||
else:
|
||||
data['title'] = data['url']
|
||||
|
||||
if item.originalUrl() != item.url():
|
||||
encoded = item.originalUrl().toEncoded()
|
||||
data['original-url'] = bytes(encoded).decode('ascii')
|
||||
|
||||
if tab.history.current_idx() == idx:
|
||||
data['active'] = True
|
||||
|
||||
try:
|
||||
user_data = item.userData()
|
||||
except AttributeError:
|
||||
# QtWebEngine
|
||||
user_data = None
|
||||
|
||||
if tab.history.current_idx() == idx:
|
||||
pos = tab.scroller.pos_px()
|
||||
data['zoom'] = tab.zoom.factor()
|
||||
data['scroll-pos'] = {'x': pos.x(), 'y': pos.y()}
|
||||
elif user_data is not None:
|
||||
if 'zoom' in user_data:
|
||||
data['zoom'] = user_data['zoom']
|
||||
if 'scroll-pos' in user_data:
|
||||
pos = user_data['scroll-pos']
|
||||
data['scroll-pos'] = {'x': pos.x(), 'y': pos.y()}
|
||||
return data
|
||||
|
||||
def _save_tab(self, tab, active):
|
||||
"""Get a dict with data for a single tab.
|
||||
|
||||
@@ -140,42 +189,9 @@ class SessionManager(QObject):
|
||||
data = {'history': []}
|
||||
if active:
|
||||
data['active'] = True
|
||||
history = tab.page().history()
|
||||
for idx, item in enumerate(history.items()):
|
||||
for idx, item in enumerate(tab.history):
|
||||
qtutils.ensure_valid(item)
|
||||
|
||||
item_data = {
|
||||
'url': bytes(item.url().toEncoded()).decode('ascii'),
|
||||
}
|
||||
|
||||
if item.title():
|
||||
item_data['title'] = item.title()
|
||||
else:
|
||||
# https://github.com/The-Compiler/qutebrowser/issues/879
|
||||
if history.currentItemIndex() == idx:
|
||||
item_data['title'] = tab.page().mainFrame().title()
|
||||
else:
|
||||
item_data['title'] = item_data['url']
|
||||
|
||||
if item.originalUrl() != item.url():
|
||||
encoded = item.originalUrl().toEncoded()
|
||||
item_data['original-url'] = bytes(encoded).decode('ascii')
|
||||
|
||||
if history.currentItemIndex() == idx:
|
||||
item_data['active'] = True
|
||||
|
||||
user_data = item.userData()
|
||||
if history.currentItemIndex() == idx:
|
||||
pos = tab.page().mainFrame().scrollPosition()
|
||||
item_data['zoom'] = tab.zoomFactor()
|
||||
item_data['scroll-pos'] = {'x': pos.x(), 'y': pos.y()}
|
||||
elif user_data is not None:
|
||||
if 'zoom' in user_data:
|
||||
item_data['zoom'] = user_data['zoom']
|
||||
if 'scroll-pos' in user_data:
|
||||
pos = user_data['scroll-pos']
|
||||
item_data['scroll-pos'] = {'x': pos.x(), 'y': pos.y()}
|
||||
|
||||
item_data = self._save_tab_item(tab, idx, item)
|
||||
data['history'].append(item_data)
|
||||
return data
|
||||
|
||||
@@ -300,9 +316,9 @@ class SessionManager(QObject):
|
||||
active=active, user_data=user_data)
|
||||
entries.append(entry)
|
||||
if active:
|
||||
new_tab.titleChanged.emit(histentry['title'])
|
||||
new_tab.title_changed.emit(histentry['title'])
|
||||
try:
|
||||
new_tab.page().load_history(entries)
|
||||
new_tab.history.load_items(entries)
|
||||
except ValueError as e:
|
||||
raise SessionError(e)
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ try:
|
||||
except ImportError:
|
||||
hunter = None
|
||||
|
||||
from qutebrowser.browser.network import qutescheme
|
||||
from qutebrowser.browser.webkit.network import qutescheme
|
||||
from qutebrowser.utils import log, objreg, usertypes, message, debug, utils
|
||||
from qutebrowser.commands import cmdutils, runners, cmdexc
|
||||
from qutebrowser.config import style
|
||||
@@ -217,3 +217,20 @@ def debug_set_fake_clipboard(s=None):
|
||||
utils.log_clipboard = True
|
||||
else:
|
||||
utils.fake_clipboard = s
|
||||
|
||||
|
||||
@cmdutils.register(hide=True)
|
||||
@cmdutils.argument('win_id', win_id=True)
|
||||
@cmdutils.argument('count', count=True)
|
||||
def repeat_command(win_id, count=None):
|
||||
"""Repeat the last executed command.
|
||||
|
||||
Args:
|
||||
count: Which count to pass the command.
|
||||
"""
|
||||
mode_manager = objreg.get('mode-manager', scope='window', window=win_id)
|
||||
if mode_manager.mode not in runners.last_command:
|
||||
raise cmdexc.CommandError("You didn't do anything yet.")
|
||||
cmd = runners.last_command[mode_manager.mode]
|
||||
commandrunner = runners.CommandRunner(win_id)
|
||||
commandrunner.run(cmd[0], count if count is not None else cmd[1])
|
||||
|
||||
@@ -69,6 +69,10 @@ def get_argparser():
|
||||
'tab-silent', 'tab-bg-silent', 'window'],
|
||||
help="How URLs should be opened if there is already a "
|
||||
"qutebrowser instance running.")
|
||||
parser.add_argument('--backend', choices=['webkit', 'webengine'],
|
||||
help="Which backend to use (webengine backend is "
|
||||
"EXPERIMENTAL!).", default='webkit')
|
||||
|
||||
parser.add_argument('--json-args', help=argparse.SUPPRESS)
|
||||
parser.add_argument('--temp-basedir-restarted', help=argparse.SUPPRESS)
|
||||
|
||||
|
||||
@@ -61,7 +61,7 @@ def _guess_autoescape(template_name):
|
||||
if template_name is None or '.' not in template_name:
|
||||
return False
|
||||
ext = template_name.rsplit('.', 1)[1]
|
||||
return ext in ('html', 'htm', 'xml')
|
||||
return ext in ['html', 'htm', 'xml']
|
||||
|
||||
|
||||
def resource_url(path):
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user