Merge https://github.com/The-Compiler/qutebrowser into macros
This commit is contained in:
commit
e5dec949b0
8
.flake8
8
.flake8
@ -23,18 +23,12 @@ exclude = .*,__pycache__,resources.py
|
||||
# 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
|
||||
# H301: one import per line
|
||||
# H306: imports not in alphabetical order
|
||||
ignore =
|
||||
E128,E226,E265,E501,E402,E266,E731,
|
||||
F401,
|
||||
N802,
|
||||
P101,P102,P103,
|
||||
D102,D103,D104,D105,D209,D211,D402,D403,
|
||||
H101,H201,H238,H301,H306
|
||||
D102,D103,D104,D105,D209,D211,D402,D403
|
||||
min-version = 3.4.0
|
||||
max-complexity = 12
|
||||
putty-auto-ignore = True
|
||||
|
11
.gitignore
vendored
11
.gitignore
vendored
@ -33,7 +33,12 @@ __pycache__
|
||||
/prof
|
||||
/venv
|
||||
TODO
|
||||
/scripts/testbrowser_cpp/Makefile
|
||||
/scripts/testbrowser_cpp/main.o
|
||||
/scripts/testbrowser_cpp/testbrowser
|
||||
/scripts/testbrowser_cpp/webkit/Makefile
|
||||
/scripts/testbrowser_cpp/webkit/main.o
|
||||
/scripts/testbrowser_cpp/webkit/testbrowser
|
||||
/scripts/testbrowser_cpp/webkit/.qmake.stash
|
||||
/scripts/testbrowser_cpp/webengine/Makefile
|
||||
/scripts/testbrowser_cpp/webengine/main.o
|
||||
/scripts/testbrowser_cpp/webengine/testbrowser
|
||||
/scripts/testbrowser_cpp/webengine/.qmake.stash
|
||||
/scripts/dev/pylint_checkers/qute_pylint.egg-info
|
||||
|
@ -139,6 +139,7 @@ Changed
|
||||
- The `qute:settings` page now also shows option descriptions.
|
||||
- `qute:version` and `qutebrowser --version` now show various important paths
|
||||
- `:spawn`/userscripts now show a nicer error when a script wasn't found
|
||||
- Various functionality now works when javascript is disabled with QtWebKit
|
||||
|
||||
Deprecated
|
||||
~~~~~~~~~~
|
||||
@ -168,23 +169,15 @@ Removed
|
||||
Fixed
|
||||
~~~~~
|
||||
|
||||
- `:bind` can now be used to bind to an alias (binding by editing `keys.conf`
|
||||
already worked before)
|
||||
- The command completion now updates correctly when changing aliases
|
||||
- `:undo` now doesn't undo tabs "closed" by `:tab-detach` anymore.
|
||||
- Fixed an issue with hint chars not being cleared correctly when leaving hint
|
||||
mode.
|
||||
- `:tab-detach` now fails correctly when there's only one tab open.
|
||||
- Various small issues with the command completion
|
||||
- The tabbar now displays correctly with the Adwaita Qt theme
|
||||
- The default `sk` keybinding now sets the commandline to `:bind` correctly
|
||||
- Fixed hang when using multiple spaces in a row with the URL completion
|
||||
- Fixed crash when closing a window without focusing it
|
||||
- Userscripts now can access QUTE_FIFO correctly on Windows
|
||||
- Compatibility with pdfjs v1.6.210
|
||||
|
||||
v0.8.3 (unreleased)
|
||||
-------------------
|
||||
v0.8.3
|
||||
------
|
||||
|
||||
Fixed
|
||||
~~~~~
|
||||
@ -193,6 +186,15 @@ Fixed
|
||||
- Fixed `:open-editor` (`<Ctrl-e>`) on Windows
|
||||
- Fixed crash when setting `general -> auto-save-interval` to a too big value.
|
||||
- Fixed crash when using hints on Void Linux.
|
||||
- Fixed compatibility with Python 3.5.2+ on Debian unstable
|
||||
- Compatibility with pdfjs v1.6.210
|
||||
- `:bind` can now be used to bind to an alias (binding by editing `keys.conf`
|
||||
already worked before)
|
||||
- The command completion now updates correctly when changing aliases
|
||||
- The tabbar now displays correctly with the Adwaita Qt theme
|
||||
- The default `sk` keybinding now sets the commandline to `:bind` correctly
|
||||
- Fixed crash when closing a window without focusing it
|
||||
- Userscripts now can access QUTE_FIFO correctly on Windows
|
||||
|
||||
v0.8.2
|
||||
------
|
||||
|
@ -526,6 +526,21 @@ generate code and subsequently overwrite part or all of it. Running with all
|
||||
will slow Valgrind down noticeably.
|
||||
____
|
||||
|
||||
Setting up a Windows Development Environment
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
* Install https://www.python.org/downloads/release/python-344/[Python 3.4]
|
||||
* Install https://sourceforge.net/projects/pyqt/files/PyQt5/PyQt-5.5.1/[PyQt 5.5]
|
||||
* Create a file at `C:\Windows\system32\python3.bat` with the following content:
|
||||
`@C:\Python34\python %*`
|
||||
This will make the Python 3.4 interpreter available as `python3`, which is used by various development scripts.
|
||||
* Install git from the https://git-scm.com/download/win[git-scm downloads page]
|
||||
Try not to enable `core.autocrlf`, since that will cause `flake8` to complain a lot. Use an editor that can deal with plain line feeds instead.
|
||||
* Clone your favourite qutebrowser repository.
|
||||
* To install tox, open an elevated cmd, enter your working directory and run `pip install -rmisc/requirements/requirements-tox.txt`.
|
||||
|
||||
Note that the `flake8` tox env might not run due to encoding errors despite having LANG/LC_* set correctly.
|
||||
|
||||
Style conventions
|
||||
-----------------
|
||||
|
||||
|
@ -272,18 +272,12 @@ qutebrowser from source.
|
||||
|
||||
==== Homebrew
|
||||
|
||||
For Homebrew, a few extra steps are necessary since Homebrew dropped QtWebKit
|
||||
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:
|
||||
Homebrew's builds of Qt and PyQt no longer include QtWebKit, so it is necessary
|
||||
to build from source. The build takes several hours on an average laptop.
|
||||
|
||||
----
|
||||
$ brew install python3 d-bus mysql sip xz
|
||||
$ brew install homebrew/versions/qt55
|
||||
$ brew install --ignore-dependencies pyqt5
|
||||
$ ln -s /usr/local/opt/qt55 /usr/local/opt/qt5
|
||||
$ brew install qt5 --with-qtwebkit
|
||||
$ brew install -s pyqt5
|
||||
|
||||
$ pip3.5 install qutebrowser
|
||||
----
|
||||
|
@ -16,7 +16,7 @@ image:https://travis-ci.org/The-Compiler/qutebrowser.svg?branch=master["Build St
|
||||
image:https://ci.appveyor.com/api/projects/status/9gmnuip6i1oq7046?svg=true["AppVeyor build status", link="https://ci.appveyor.com/project/The-Compiler/qutebrowser"]
|
||||
image:https://codecov.io/github/The-Compiler/qutebrowser/coverage.svg?branch=master["coverage badge",link="https://codecov.io/github/The-Compiler/qutebrowser?branch=master"]
|
||||
|
||||
link:http://www.qutebrowser.org[website] | link:http://blog.qutebrowser.org[blog] | link:https://github.com/The-Compiler/qutebrowser/releases[releases]
|
||||
link:https://www.qutebrowser.org[website] | link:https://blog.qutebrowser.org[blog] | link:https://github.com/The-Compiler/qutebrowser/releases[releases]
|
||||
// QUTE_WEB_HIDE_END
|
||||
|
||||
qutebrowser is a keyboard-focused browser with a minimal GUI. It's based
|
||||
@ -48,8 +48,8 @@ Documentation
|
||||
In addition to the topics mentioned in this README, the following documents are
|
||||
available:
|
||||
|
||||
* A http://qutebrowser.org/img/cheatsheet-big.png[key binding cheatsheet]: +
|
||||
image:http://qutebrowser.org/img/cheatsheet-small.png["qutebrowser key binding cheatsheet",link="http://qutebrowser.org/img/cheatsheet-big.png"]
|
||||
* A https://qutebrowser.org/img/cheatsheet-big.png[key binding cheatsheet]: +
|
||||
image:https://qutebrowser.org/img/cheatsheet-small.png["qutebrowser key binding cheatsheet",link="https://qutebrowser.org/img/cheatsheet-big.png"]
|
||||
* link:doc/quickstart.asciidoc[Quick start guide]
|
||||
* A https://www.shortcutfoo.com/app/dojos/qutebrowser[free training course] to remember those key bindings.
|
||||
* link:FAQ.asciidoc[Frequently asked questions]
|
||||
@ -90,7 +90,7 @@ https://lists.schokokeks.org/mailman/listinfo.cgi/qutebrowser[mailinglist] at
|
||||
mailto:qutebrowser@lists.qutebrowser.org[].
|
||||
|
||||
For security bugs, please contact me directly at mail@qutebrowser.org, GPG ID
|
||||
http://www.the-compiler.org/pubkey.asc[0xFD55A072].
|
||||
https://www.the-compiler.org/pubkey.asc[0xFD55A072].
|
||||
|
||||
Requirements
|
||||
------------
|
||||
@ -155,6 +155,8 @@ Contributors, sorted by the number of commits in descending order:
|
||||
* Alexander Cogneau
|
||||
* Felix Van der Jeugt
|
||||
* Martin Tournoij
|
||||
* Daniel Karbach
|
||||
* Kevin Velghe
|
||||
* Raphael Pierzina
|
||||
* Joel Torstensson
|
||||
* Patric Schmitz
|
||||
@ -162,15 +164,14 @@ Contributors, sorted by the number of commits in descending order:
|
||||
* Claude
|
||||
* Corentin Julé
|
||||
* meles5
|
||||
* Kevin Velghe
|
||||
* Philipp Hansch
|
||||
* Daniel Karbach
|
||||
* Panagiotis Ktistakis
|
||||
* Artur Shaik
|
||||
* Nathan Isom
|
||||
* Thorsten Wißmann
|
||||
* Austin Anderson
|
||||
* Jimmy
|
||||
* Spreadyy
|
||||
* Niklas Haas
|
||||
* Alexey "Averrin" Nabrodov
|
||||
* nanjekyejoannah
|
||||
@ -188,6 +189,7 @@ Contributors, sorted by the number of commits in descending order:
|
||||
* error800
|
||||
* Michael Hoang
|
||||
* Liam BEGUIN
|
||||
* Julie Engel
|
||||
* skinnay
|
||||
* Zach-Button
|
||||
* Tomasz Kramkowski
|
||||
@ -211,7 +213,6 @@ Contributors, sorted by the number of commits in descending order:
|
||||
* jnphilipp
|
||||
* Tobias Patzl
|
||||
* Stefan Tatschner
|
||||
* Spreadyy
|
||||
* Samuel Loury
|
||||
* Peter Michely
|
||||
* Panashe M. Fundira
|
||||
@ -228,9 +229,11 @@ Contributors, sorted by the number of commits in descending order:
|
||||
* Regina Hug
|
||||
* Mathias Fussenegger
|
||||
* Marcelo Santos
|
||||
* Joel Bradshaw
|
||||
* Jean-Louis Fuchs
|
||||
* Fritz V155 Reichwald
|
||||
* Franz Fellner
|
||||
* Eric Drechsel
|
||||
* zwarag
|
||||
* xd1le
|
||||
* rsteube
|
||||
@ -256,7 +259,6 @@ Contributors, sorted by the number of commits in descending order:
|
||||
* Marcel Schilling
|
||||
* Lazlow Carmichael
|
||||
* Ján Kobezda
|
||||
* Julie Engel
|
||||
* Johannes Martinsson
|
||||
* Jean-Christophe Petkovich
|
||||
* Jay Kamat
|
||||
|
@ -1000,6 +1000,7 @@ How many steps to zoom out.
|
||||
|<<move-to-start-of-prev-block,move-to-start-of-prev-block>>|Move the cursor or selection to the start of previous block.
|
||||
|<<open-editor,open-editor>>|Open an external editor with the currently selected form field.
|
||||
|<<prompt-accept,prompt-accept>>|Accept the current prompt.
|
||||
|<<prompt-item-focus,prompt-item-focus>>|Shift the focus of the prompt file completion menu to another item.
|
||||
|<<prompt-open-download,prompt-open-download>>|Immediately open a download.
|
||||
|<<repeat-command,repeat-command>>|Repeat the last executed command.
|
||||
|<<rl-backward-char,rl-backward-char>>|Move back a character.
|
||||
@ -1252,6 +1253,15 @@ Accept the current prompt.
|
||||
* +'value'+: If given, uses this value instead of the entered one. For boolean prompts, "yes"/"no" are accepted as value.
|
||||
|
||||
|
||||
[[prompt-item-focus]]
|
||||
=== prompt-item-focus
|
||||
Syntax: +:prompt-item-focus 'which'+
|
||||
|
||||
Shift the focus of the prompt file completion menu to another item.
|
||||
|
||||
==== positional arguments
|
||||
* +'which'+: 'next', 'prev'
|
||||
|
||||
[[prompt-open-download]]
|
||||
=== prompt-open-download
|
||||
Syntax: +:prompt-open-download ['cmdline']+
|
||||
|
@ -6,13 +6,13 @@ Documentation
|
||||
|
||||
The following help pages are currently available:
|
||||
|
||||
* link:quickstart.html[Quick start guide]
|
||||
* link:FAQ.html[Frequently asked questions]
|
||||
* link:CHANGELOG.html[Change Log]
|
||||
* link:../quickstart.html[Quick start guide]
|
||||
* link:../../FAQ.html[Frequently asked questions]
|
||||
* link:../../CHANGELOG.html[Change Log]
|
||||
* link:commands.html[Documentation of commands]
|
||||
* link:settings.html[Documentation of settings]
|
||||
* link:userscripts.html[How to write userscripts]
|
||||
* link:CONTRIBUTING.html[Contributing to qutebrowser]
|
||||
* link:../userscripts.html[How to write userscripts]
|
||||
* link:../../CONTRIBUTING.html[Contributing to qutebrowser]
|
||||
|
||||
Getting help
|
||||
------------
|
||||
|
@ -54,6 +54,7 @@
|
||||
|<<ui-modal-js-dialog,modal-js-dialog>>|Use standard JavaScript modal dialog for alert() and confirm()
|
||||
|<<ui-hide-wayland-decoration,hide-wayland-decoration>>|Hide the window decoration when using wayland (requires restart)
|
||||
|<<ui-keyhint-blacklist,keyhint-blacklist>>|Keychains that shouldn't be shown in the keyhint dialog
|
||||
|<<ui-prompt-radius,prompt-radius>>|The rounding radius for the edges of prompts.
|
||||
|==============
|
||||
|
||||
.Quick reference for section ``network''
|
||||
@ -213,8 +214,6 @@
|
||||
|<<colors-completion.scrollbar.bg,completion.scrollbar.bg>>|Color of the scrollbar in completion view
|
||||
|<<colors-statusbar.fg,statusbar.fg>>|Foreground color of the statusbar.
|
||||
|<<colors-statusbar.bg,statusbar.bg>>|Background color of the statusbar.
|
||||
|<<colors-statusbar.fg.prompt,statusbar.fg.prompt>>|Foreground color of the statusbar if there is a prompt.
|
||||
|<<colors-statusbar.bg.prompt,statusbar.bg.prompt>>|Background color of the statusbar if there is a prompt.
|
||||
|<<colors-statusbar.fg.insert,statusbar.fg.insert>>|Foreground color of the statusbar in insert mode.
|
||||
|<<colors-statusbar.bg.insert,statusbar.bg.insert>>|Background color of the statusbar in insert mode.
|
||||
|<<colors-statusbar.fg.command,statusbar.fg.command>>|Foreground color of the statusbar in command mode.
|
||||
@ -268,6 +267,9 @@
|
||||
|<<colors-messages.fg.info,messages.fg.info>>|Foreground color an info message.
|
||||
|<<colors-messages.bg.info,messages.bg.info>>|Background color of an info message.
|
||||
|<<colors-messages.border.info,messages.border.info>>|Border color of an info message.
|
||||
|<<colors-prompts.fg,prompts.fg>>|Foreground color for prompts.
|
||||
|<<colors-prompts.bg,prompts.bg>>|Background color for prompts.
|
||||
|<<colors-prompts.selected.bg,prompts.selected.bg>>|Background color for the selected item in filename prompts.
|
||||
|==============
|
||||
|
||||
.Quick reference for section ``fonts''
|
||||
@ -296,6 +298,7 @@
|
||||
|<<fonts-messages.error,messages.error>>|Font used for error messages.
|
||||
|<<fonts-messages.warning,messages.warning>>|Font used for warning messages.
|
||||
|<<fonts-messages.info,messages.info>>|Font used for info messages.
|
||||
|<<fonts-prompts,prompts>>|Font used for prompts.
|
||||
|==============
|
||||
|
||||
== general
|
||||
@ -317,7 +320,7 @@ Default: +pass:[smart]+
|
||||
=== startpage
|
||||
The default page(s) to open at the start, separated by commas.
|
||||
|
||||
Default: +pass:[https://duckduckgo.com]+
|
||||
Default: +pass:[https://start.duckduckgo.com]+
|
||||
|
||||
[[general-yank-ignored-url-parameters]]
|
||||
=== yank-ignored-url-parameters
|
||||
@ -706,6 +709,12 @@ Globs are supported, so ';*' will blacklist all keychainsstarting with ';'. Use
|
||||
|
||||
Default: empty
|
||||
|
||||
[[ui-prompt-radius]]
|
||||
=== prompt-radius
|
||||
The rounding radius for the edges of prompts.
|
||||
|
||||
Default: +pass:[8]+
|
||||
|
||||
== network
|
||||
Settings related to the network.
|
||||
|
||||
@ -1560,7 +1569,7 @@ The file can be in one of the following formats:
|
||||
- One host per line
|
||||
- A zip-file of any of the above, with either only one file, or a file named 'hosts' (with any extension).
|
||||
|
||||
Default: +pass:[http://www.malwaredomainlist.com/hostslist/hosts.txt,http://someonewhocares.org/hosts/hosts,http://winhelp2002.mvps.org/hosts.zip,http://malwaredomains.lehigh.edu/files/justdomains.zip,http://pgl.yoyo.org/adservers/serverlist.php?hostformat=hosts&mimetype=plaintext]+
|
||||
Default: +pass:[https://www.malwaredomainlist.com/hostslist/hosts.txt,http://someonewhocares.org/hosts/hosts,http://winhelp2002.mvps.org/hosts.zip,http://malwaredomains.lehigh.edu/files/justdomains.zip,https://pgl.yoyo.org/adservers/serverlist.php?hostformat=hosts&mimetype=plaintext]+
|
||||
|
||||
[[content-host-blocking-enabled]]
|
||||
=== host-blocking-enabled
|
||||
@ -1718,7 +1727,7 @@ The searchengine named `DEFAULT` is used when `general -> auto-search` is true a
|
||||
Aliases for commands.
|
||||
By default, no aliases are defined. Example which adds a new command `:qtb` to open qutebrowsers website:
|
||||
|
||||
`qtb = open http://www.qutebrowser.org/`
|
||||
`qtb = open https://www.qutebrowser.org/`
|
||||
|
||||
== colors
|
||||
Colors used in the UI.
|
||||
@ -1831,18 +1840,6 @@ Background color of the statusbar.
|
||||
|
||||
Default: +pass:[black]+
|
||||
|
||||
[[colors-statusbar.fg.prompt]]
|
||||
=== statusbar.fg.prompt
|
||||
Foreground color of the statusbar if there is a prompt.
|
||||
|
||||
Default: +pass:[${statusbar.fg}]+
|
||||
|
||||
[[colors-statusbar.bg.prompt]]
|
||||
=== statusbar.bg.prompt
|
||||
Background color of the statusbar if there is a prompt.
|
||||
|
||||
Default: +pass:[darkblue]+
|
||||
|
||||
[[colors-statusbar.fg.insert]]
|
||||
=== statusbar.fg.insert
|
||||
Foreground color of the statusbar in insert mode.
|
||||
@ -2184,6 +2181,24 @@ Border color of an info message.
|
||||
|
||||
Default: +pass:[#333333]+
|
||||
|
||||
[[colors-prompts.fg]]
|
||||
=== prompts.fg
|
||||
Foreground color for prompts.
|
||||
|
||||
Default: +pass:[white]+
|
||||
|
||||
[[colors-prompts.bg]]
|
||||
=== prompts.bg
|
||||
Background color for prompts.
|
||||
|
||||
Default: +pass:[darkblue]+
|
||||
|
||||
[[colors-prompts.selected.bg]]
|
||||
=== prompts.selected.bg
|
||||
Background color for the selected item in filename prompts.
|
||||
|
||||
Default: +pass:[#308cc6]+
|
||||
|
||||
== fonts
|
||||
Fonts used for the UI, with optional style/weight/size.
|
||||
|
||||
@ -2322,3 +2337,9 @@ Default: +pass:[8pt ${_monospace}]+
|
||||
Font used for info messages.
|
||||
|
||||
Default: +pass:[8pt ${_monospace}]+
|
||||
|
||||
[[fonts-prompts]]
|
||||
=== prompts
|
||||
Font used for prompts.
|
||||
|
||||
Default: +pass:[8pt sans-serif]+
|
||||
|
@ -7,7 +7,7 @@
|
||||
:man source: qutebrowser
|
||||
:man manual: qutebrowser manpage
|
||||
:toc:
|
||||
:homepage: http://www.qutebrowser.org/
|
||||
:homepage: https://www.qutebrowser.org/
|
||||
|
||||
== NAME
|
||||
qutebrowser - a keyboard-driven, vim-like browser based on PyQt5 and QtWebKit.
|
||||
@ -143,7 +143,7 @@ https://lists.schokokeks.org/mailman/listinfo.cgi/qutebrowser[mailinglist] at
|
||||
mailto:qutebrowser@lists.qutebrowser.org[] instead.
|
||||
|
||||
For security bugs, please contact me directly at me@the-compiler.org, GPG ID
|
||||
http://www.the-compiler.org/pubkey.asc[0xFD55A072].
|
||||
https://www.the-compiler.org/pubkey.asc[0xFD55A072].
|
||||
|
||||
== COPYRIGHT
|
||||
This program is free software: you can redistribute it and/or modify it under
|
||||
@ -159,7 +159,7 @@ You should have received a copy of the GNU General Public License along with
|
||||
this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
== RESOURCES
|
||||
* Website: http://www.qutebrowser.org/
|
||||
* Website: https://www.qutebrowser.org/
|
||||
* Mailinglist: mailto:qutebrowser@lists.qutebrowser.org[] /
|
||||
https://lists.schokokeks.org/mailman/listinfo.cgi/qutebrowser
|
||||
* Announce-only mailinglist: mailto:qutebrowser-announce@lists.qutebrowser.org[] /
|
||||
|
@ -2539,7 +2539,7 @@
|
||||
y="10"
|
||||
style="font-size:10px" /></flowRegion><flowPara
|
||||
id="flowPara5604"
|
||||
style="font-size:13px">Website: http://www.qutebrowser.org/ </flowPara><flowPara
|
||||
style="font-size:13px">Website: https://www.qutebrowser.org/ </flowPara><flowPara
|
||||
id="flowPara5595"
|
||||
style="font-size:13px">IRC: #qutebrowser on Freenode</flowPara><flowPara
|
||||
id="flowPara5597"
|
||||
|
Before Width: | Height: | Size: 137 KiB After Width: | Height: | Size: 137 KiB |
@ -71,4 +71,5 @@ app = BUNDLE(coll,
|
||||
name='qutebrowser.app',
|
||||
icon=icon,
|
||||
info_plist={'NSHighResolutionCapable': 'True'},
|
||||
bundle_identifier=None)
|
||||
# https://github.com/pyinstaller/pyinstaller/blob/b78bfe530cdc2904f65ce098bdf2de08c9037abb/PyInstaller/hooks/hook-PyQt5.QtWebEngineWidgets.py#L24
|
||||
bundle_identifier='org.qt-project.Qt.QtWebEngineCore')
|
||||
|
@ -3,22 +3,19 @@
|
||||
flake8==2.6.2 # rq.filter: < 3.0.0
|
||||
flake8-copyright==0.2.0
|
||||
flake8-debugger==1.4.0 # rq.filter: != 2.0.0
|
||||
flake8-deprecated==1.0
|
||||
flake8-deprecated==1.1
|
||||
flake8-docstrings==1.0.2
|
||||
flake8-future-import==0.4.3
|
||||
flake8-mock==0.3
|
||||
flake8-pep3101==0.4
|
||||
flake8-pep3101==0.6
|
||||
flake8-putty==0.4.0
|
||||
flake8-string-format==0.2.3
|
||||
flake8-tidy-imports==1.0.2
|
||||
flake8-tidy-imports==1.0.3
|
||||
flake8-tuple==0.2.12
|
||||
hacking==0.11.0
|
||||
mccabe==0.5.2
|
||||
packaging==16.7
|
||||
pbr==1.10.0
|
||||
pep8==1.7.0
|
||||
packaging==16.8
|
||||
pep8-naming==0.4.1
|
||||
pycodestyle==2.0.0
|
||||
pycodestyle==2.1.0
|
||||
pydocstyle==1.1.1
|
||||
pyflakes==1.3.0
|
||||
pyparsing==2.1.10
|
||||
|
@ -10,15 +10,14 @@ flake8-putty
|
||||
flake8-string-format
|
||||
flake8-tidy-imports
|
||||
flake8-tuple
|
||||
hacking
|
||||
pep8-naming
|
||||
pydocstyle
|
||||
pyflakes
|
||||
|
||||
# Pinned to 1.5.7 by hacking otherwise
|
||||
pep8==1.7.0
|
||||
# Pinned to 2.0.0 otherwise
|
||||
pycodestyle==2.1.0
|
||||
|
||||
# Waiting until hacking/flake8-tuple are updated
|
||||
# Waiting until flake8-putty updated
|
||||
#@ filter: flake8 < 3.0.0
|
||||
|
||||
# https://github.com/JBKahn/flake8-debugger/issues/5
|
||||
|
@ -1,2 +0,0 @@
|
||||
pip==8.1.2
|
||||
setuptools==28.6.0
|
@ -1,3 +1,3 @@
|
||||
# This file is automatically generated by scripts/dev/recompile_requirements.py
|
||||
|
||||
PyInstaller==3.2
|
||||
-e git+https://github.com/edrex/pyinstaller.git@0fedc28f65d74e1f5ece453abdfb5ad54e9ac5ba#egg=PyInstaller
|
||||
|
@ -1 +1,2 @@
|
||||
PyInstaller
|
||||
# https://github.com/pyinstaller/pyinstaller/pull/2238
|
||||
-e git+https://github.com/edrex/pyinstaller.git@1984_add_QtWebEngineCore#egg=PyInstaller
|
||||
|
@ -1,4 +1,4 @@
|
||||
# This file is automatically generated by scripts/dev/recompile_requirements.py
|
||||
|
||||
docutils==0.12
|
||||
pyroma==2.0.2
|
||||
pyroma==2.2
|
||||
|
@ -6,32 +6,29 @@ click==6.6
|
||||
coverage==4.2
|
||||
decorator==4.0.10
|
||||
Flask==0.11.1
|
||||
glob2==0.4.1
|
||||
glob2==0.5
|
||||
httpbin==0.5.0
|
||||
hypothesis==3.5.3
|
||||
hypothesis==3.6.0
|
||||
itsdangerous==0.24
|
||||
# Jinja2==2.8
|
||||
Mako==1.0.4
|
||||
Mako==1.0.5
|
||||
# MarkupSafe==0.23
|
||||
parse==1.6.6
|
||||
parse-type==0.3.4
|
||||
py==1.4.31
|
||||
pytest==3.0.3
|
||||
pytest-bdd==2.18.0
|
||||
pytest-bdd==2.18.1
|
||||
pytest-catchlog==1.2.2
|
||||
pytest-cov==2.4.0
|
||||
pytest-faulthandler==1.3.0
|
||||
pytest-instafail==0.3.0
|
||||
pytest-mock==1.2
|
||||
pytest-qt==2.0.0
|
||||
pytest-mock==1.4.0
|
||||
pytest-qt==2.1.0
|
||||
pytest-repeat==0.4.1
|
||||
pytest-rerunfailures==2.0.1
|
||||
pytest-rerunfailures==2.1.0
|
||||
pytest-travis-fold==1.2.0
|
||||
pytest-warnings==0.1.0
|
||||
pytest-warnings==0.2.0
|
||||
pytest-xvfb==0.3.0
|
||||
six==1.10.0
|
||||
spark-parser==1.4.0
|
||||
uncompyle6==2.9.2
|
||||
vulture==0.10
|
||||
Werkzeug==0.11.11
|
||||
xdis==3.1.0
|
||||
|
@ -28,7 +28,7 @@ __copyright__ = "Copyright 2014-2016 Florian Bruhin (The Compiler)"
|
||||
__license__ = "GPL"
|
||||
__maintainer__ = __author__
|
||||
__email__ = "mail@qutebrowser.org"
|
||||
__version_info__ = (0, 8, 1)
|
||||
__version_info__ = (0, 8, 4)
|
||||
__version__ = '.'.join(str(e) for e in __version_info__)
|
||||
__description__ = "A keyboard-driven, vim-like browser based on PyQt5 and QtWebKit."
|
||||
|
||||
|
@ -47,10 +47,9 @@ from qutebrowser.commands import cmdutils, runners, cmdexc
|
||||
from qutebrowser.config import style, config, websettings, configexc
|
||||
from qutebrowser.browser import urlmarks, adblock, history, browsertab
|
||||
from qutebrowser.browser.webkit import cookies, cache, downloads
|
||||
from qutebrowser.browser.webkit.network import (webkitqutescheme, proxy,
|
||||
networkmanager)
|
||||
from qutebrowser.browser.webkit.network import networkmanager
|
||||
from qutebrowser.keyinput import macros
|
||||
from qutebrowser.mainwindow import mainwindow
|
||||
from qutebrowser.mainwindow import mainwindow, prompt
|
||||
from qutebrowser.misc import (readline, ipc, savemanager, sessions,
|
||||
crashsignal, earlyinit)
|
||||
from qutebrowser.misc import utilcmds # pylint: disable=unused-import
|
||||
@ -332,7 +331,7 @@ def _open_quickstart(args):
|
||||
tabbed_browser = objreg.get('tabbed-browser', scope='window',
|
||||
window='last-focused')
|
||||
tabbed_browser.tabopen(
|
||||
QUrl('http://www.qutebrowser.org/quickstart.html'))
|
||||
QUrl('https://www.qutebrowser.org/quickstart.html'))
|
||||
state_config['general']['quickstart-done'] = '1'
|
||||
|
||||
|
||||
@ -376,6 +375,9 @@ def _init_modules(args, crash_handler):
|
||||
crash_handler: The CrashHandler instance.
|
||||
"""
|
||||
# pylint: disable=too-many-statements
|
||||
log.init.debug("Initializing prompts...")
|
||||
prompt.init()
|
||||
|
||||
log.init.debug("Initializing save manager...")
|
||||
save_manager = savemanager.SaveManager(qApp)
|
||||
objreg.register('save-manager', save_manager)
|
||||
@ -402,10 +404,6 @@ def _init_modules(args, crash_handler):
|
||||
log.init.debug("Initializing sessions...")
|
||||
sessions.init(qApp)
|
||||
|
||||
log.init.debug("Initializing js-bridge...")
|
||||
js_bridge = webkitqutescheme.JSBridge(qApp)
|
||||
objreg.register('js-bridge', js_bridge)
|
||||
|
||||
log.init.debug("Initializing websettings...")
|
||||
websettings.init()
|
||||
|
||||
@ -422,9 +420,6 @@ def _init_modules(args, crash_handler):
|
||||
bookmark_manager = urlmarks.BookmarkManager(qApp)
|
||||
objreg.register('bookmark-manager', bookmark_manager)
|
||||
|
||||
log.init.debug("Initializing proxy...")
|
||||
proxy.init()
|
||||
|
||||
log.init.debug("Initializing cookies...")
|
||||
cookie_jar = cookies.CookieJar(qApp)
|
||||
ram_cookie_jar = cookies.RAMCookieJar(qApp)
|
||||
@ -654,13 +649,7 @@ class Quitter:
|
||||
session_manager.save(sessions.default, last_window=last_window,
|
||||
load_next_time=True)
|
||||
|
||||
deferrer = False
|
||||
for win_id in objreg.window_registry:
|
||||
prompter = objreg.get('prompter', None, scope='window',
|
||||
window=win_id)
|
||||
if prompter is not None and prompter.shutdown():
|
||||
deferrer = True
|
||||
if deferrer:
|
||||
if prompt.prompt_queue.shutdown():
|
||||
# If shutdown was called while we were asking a question, we're in
|
||||
# a still sub-eventloop (which gets quit now) and not in the main
|
||||
# one.
|
||||
@ -719,7 +708,6 @@ class Quitter:
|
||||
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)
|
||||
@ -747,6 +735,7 @@ class Application(QApplication):
|
||||
|
||||
Attributes:
|
||||
_args: ArgumentParser instance.
|
||||
_last_focus_object: The last focused object's repr.
|
||||
"""
|
||||
|
||||
new_window = pyqtSignal(mainwindow.MainWindow)
|
||||
@ -757,6 +746,8 @@ class Application(QApplication):
|
||||
Args:
|
||||
Argument namespace from argparse.
|
||||
"""
|
||||
self._last_focus_object = None
|
||||
|
||||
qt_args = qtutils.get_args(args)
|
||||
log.init.debug("Qt arguments: {}, based on {}".format(qt_args, args))
|
||||
super().__init__(qt_args)
|
||||
@ -773,7 +764,10 @@ class Application(QApplication):
|
||||
@pyqtSlot(QObject)
|
||||
def on_focus_object_changed(self, obj):
|
||||
"""Log when the focus object changed."""
|
||||
log.misc.debug("Focus object changed: {!r}".format(obj))
|
||||
output = repr(obj)
|
||||
if self._last_focus_object != output:
|
||||
log.misc.debug("Focus object changed: {}".format(output))
|
||||
self._last_focus_object = output
|
||||
|
||||
def __repr__(self):
|
||||
return utils.get_repr(self)
|
||||
|
@ -761,10 +761,6 @@ class AbstractTab(QWidget):
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def has_js(self):
|
||||
"""Check if qutebrowser can run javascript in this tab."""
|
||||
raise NotImplementedError
|
||||
|
||||
def shutdown(self):
|
||||
raise NotImplementedError
|
||||
|
||||
|
@ -1137,7 +1137,7 @@ class CommandDispatcher:
|
||||
def quickmark_save(self):
|
||||
"""Save the current page as a quickmark."""
|
||||
quickmark_manager = objreg.get('quickmark-manager')
|
||||
quickmark_manager.prompt_save(self._win_id, self._current_url())
|
||||
quickmark_manager.prompt_save(self._current_url())
|
||||
|
||||
@cmdutils.register(instance='command-dispatcher', scope='window',
|
||||
maxsplit=0)
|
||||
@ -1355,9 +1355,8 @@ class CommandDispatcher:
|
||||
if dest is None:
|
||||
suggested_fn = self._current_title() + ".mht"
|
||||
suggested_fn = utils.sanitize_filename(suggested_fn)
|
||||
filename, q = downloads.ask_for_filename(
|
||||
suggested_fn, self._win_id, parent=tab,
|
||||
)
|
||||
filename, q = downloads.ask_for_filename(suggested_fn, parent=tab,
|
||||
url=tab.url())
|
||||
if filename is not None:
|
||||
mhtml.start_download_checked(filename, tab=tab)
|
||||
else:
|
||||
@ -1539,8 +1538,6 @@ class CommandDispatcher:
|
||||
text: The text to insert.
|
||||
"""
|
||||
tab = self._current_widget()
|
||||
if not tab.has_js():
|
||||
raise cmdexc.CommandError("This command needs javascript enabled.")
|
||||
|
||||
def _insert_text_cb(elem):
|
||||
if elem is None:
|
||||
|
@ -265,9 +265,8 @@ class HintActions:
|
||||
if text[0] not in modeparsers.STARTCHARS:
|
||||
raise HintingError("Invalid command text '{}'.".format(text))
|
||||
|
||||
bridge = objreg.get('message-bridge', scope='window',
|
||||
window=self._win_id)
|
||||
bridge.set_cmd_text(text)
|
||||
cmd = objreg.get('status-command', scope='window', window=self._win_id)
|
||||
cmd.set_cmd_text(text)
|
||||
|
||||
def download(self, elem, context):
|
||||
"""Download a hint URL.
|
||||
|
@ -26,6 +26,7 @@ to a file on shutdown, so it makes sense to keep them as strings here.
|
||||
"""
|
||||
|
||||
import os
|
||||
import html
|
||||
import os.path
|
||||
import functools
|
||||
import collections
|
||||
@ -33,7 +34,7 @@ import collections
|
||||
from PyQt5.QtCore import pyqtSignal, QUrl, QObject
|
||||
|
||||
from qutebrowser.utils import (message, usertypes, qtutils, urlutils,
|
||||
standarddir, objreg)
|
||||
standarddir, objreg, log)
|
||||
from qutebrowser.commands import cmdutils
|
||||
from qutebrowser.misc import lineparser
|
||||
|
||||
@ -159,11 +160,10 @@ class QuickmarkManager(UrlMarkManager):
|
||||
else:
|
||||
self.marks[key] = url
|
||||
|
||||
def prompt_save(self, win_id, url):
|
||||
def prompt_save(self, url):
|
||||
"""Prompt for a new quickmark name to be added and add it.
|
||||
|
||||
Args:
|
||||
win_id: The current window ID.
|
||||
url: The quickmark url as a QUrl.
|
||||
"""
|
||||
if not url.isValid():
|
||||
@ -171,19 +171,19 @@ class QuickmarkManager(UrlMarkManager):
|
||||
return
|
||||
urlstr = url.toString(QUrl.RemovePassword | QUrl.FullyEncoded)
|
||||
message.ask_async(
|
||||
win_id, "Add quickmark:", usertypes.PromptMode.text,
|
||||
functools.partial(self.quickmark_add, win_id, urlstr))
|
||||
"Add quickmark:", usertypes.PromptMode.text,
|
||||
functools.partial(self.quickmark_add, urlstr),
|
||||
text="Please enter a quickmark name for<br/><b>{}</b>".format(
|
||||
html.escape(url.toDisplayString())))
|
||||
|
||||
@cmdutils.register(instance='quickmark-manager')
|
||||
@cmdutils.argument('win_id', win_id=True)
|
||||
def quickmark_add(self, win_id, url, name):
|
||||
def quickmark_add(self, 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.
|
||||
name: The name for the new quickmark.
|
||||
"""
|
||||
@ -201,10 +201,12 @@ class QuickmarkManager(UrlMarkManager):
|
||||
self.marks[name] = url
|
||||
self.changed.emit()
|
||||
self.added.emit(name, url)
|
||||
log.misc.debug("Added quickmark {} for {}".format(name, url))
|
||||
|
||||
if name in self.marks:
|
||||
message.confirm_async(
|
||||
win_id, "Override existing quickmark?", set_mark, default=True)
|
||||
title="Override existing quickmark?",
|
||||
yes_action=set_mark, default=True)
|
||||
else:
|
||||
set_mark()
|
||||
|
||||
|
@ -162,11 +162,6 @@ class AbstractWebElement(collections.abc.MutableMapping):
|
||||
"""Insert the given text into the element."""
|
||||
raise NotImplementedError
|
||||
|
||||
def parent(self):
|
||||
"""Get the parent element of this element."""
|
||||
# FIXME:qtwebengine get rid of this?
|
||||
raise NotImplementedError
|
||||
|
||||
def rect_on_view(self, *, elem_geometry=None, no_js=False):
|
||||
"""Get the geometry of the element relative to the webview.
|
||||
|
||||
@ -294,16 +289,7 @@ class AbstractWebElement(collections.abc.MutableMapping):
|
||||
|
||||
def remove_blank_target(self):
|
||||
"""Remove target from link."""
|
||||
elem = self
|
||||
for _ in range(5):
|
||||
if elem is None:
|
||||
break
|
||||
tag = elem.tag_name()
|
||||
if tag == 'a' or tag == 'area':
|
||||
if elem.get('target', None) == '_blank':
|
||||
elem['target'] = '_top'
|
||||
break
|
||||
elem = elem.parent()
|
||||
raise NotImplementedError
|
||||
|
||||
def resolve_url(self, baseurl):
|
||||
"""Resolve the URL in the element's src/href attribute.
|
||||
|
@ -47,7 +47,10 @@ class WebEngineElement(webelem.AbstractWebElement):
|
||||
return attrs[key]
|
||||
|
||||
def __setitem__(self, key, val):
|
||||
log.stub()
|
||||
self._js_dict['attributes'][key] = val
|
||||
js_code = javascript.assemble('webelem', 'set_attribute', self._id,
|
||||
key, val)
|
||||
self._tab.run_js_async(js_code)
|
||||
|
||||
def __delitem__(self, key):
|
||||
log.stub()
|
||||
@ -114,12 +117,6 @@ class WebEngineElement(webelem.AbstractWebElement):
|
||||
js_code = javascript.assemble('webelem', 'insert_text', self._id, text)
|
||||
self._tab.run_js_async(js_code)
|
||||
|
||||
def parent(self):
|
||||
"""Get the parent element of this element."""
|
||||
# FIXME:qtwebengine get rid of this?
|
||||
log.stub()
|
||||
return None
|
||||
|
||||
def rect_on_view(self, *, elem_geometry=None, no_js=False):
|
||||
"""Get the geometry of the element relative to the webview.
|
||||
|
||||
@ -160,3 +157,8 @@ class WebEngineElement(webelem.AbstractWebElement):
|
||||
log.webelem.debug("Couldn't find rectangle for {!r} ({})".format(
|
||||
self, rects))
|
||||
return QRect()
|
||||
|
||||
def remove_blank_target(self):
|
||||
js_code = javascript.assemble('webelem', 'remove_blank_target',
|
||||
self._id)
|
||||
self._tab.run_js_async(js_code)
|
||||
|
@ -402,7 +402,10 @@ class WebEngineElements(browsertab.AbstractElements):
|
||||
Called with a WebEngineElement or None.
|
||||
js_elem: The element serialized from javascript.
|
||||
"""
|
||||
log.webview.debug("Got element from JS: {!r}".format(js_elem))
|
||||
debug_str = ('None' if js_elem is None
|
||||
else utils.elide(repr(js_elem), 100))
|
||||
log.webview.debug("Got element from JS: {}".format(debug_str))
|
||||
|
||||
if js_elem is None:
|
||||
callback(None)
|
||||
else:
|
||||
@ -528,10 +531,6 @@ class WebEngineTab(browsertab.AbstractTab):
|
||||
else:
|
||||
self._widget.page().runJavaScript(code, callback)
|
||||
|
||||
def has_js(self):
|
||||
# QtWebEngine can run JS even if the page can't
|
||||
return True
|
||||
|
||||
def shutdown(self):
|
||||
log.stub()
|
||||
|
||||
|
@ -28,6 +28,7 @@ import shutil
|
||||
import functools
|
||||
import tempfile
|
||||
import collections
|
||||
import html
|
||||
|
||||
import sip
|
||||
from PyQt5.QtCore import (pyqtSlot, pyqtSignal, QObject, QTimer,
|
||||
@ -119,7 +120,7 @@ def create_full_filename(basename, filename):
|
||||
return None
|
||||
|
||||
|
||||
def ask_for_filename(suggested_filename, win_id, *, parent=None,
|
||||
def ask_for_filename(suggested_filename, *, url, parent=None,
|
||||
prompt_download_directory=None):
|
||||
"""Prepare a question for a download-path.
|
||||
|
||||
@ -133,7 +134,7 @@ def ask_for_filename(suggested_filename, win_id, *, parent=None,
|
||||
|
||||
Args:
|
||||
suggested_filename: The "default"-name that is pre-entered as path.
|
||||
win_id: The window where the question will be asked.
|
||||
url: The URL the download originated from.
|
||||
parent: The parent of the question (a QObject).
|
||||
prompt_download_directory: If this is something else than None, it
|
||||
will overwrite the
|
||||
@ -150,14 +151,14 @@ def ask_for_filename(suggested_filename, win_id, *, parent=None,
|
||||
suggested_filename = utils.force_encoding(suggested_filename, encoding)
|
||||
|
||||
q = usertypes.Question(parent)
|
||||
q.text = "Save file to:"
|
||||
q.title = "Save file to:"
|
||||
q.text = "Please enter a location for <b>{}</b>".format(
|
||||
html.escape(url.toDisplayString()))
|
||||
q.mode = usertypes.PromptMode.text
|
||||
q.completed.connect(q.deleteLater)
|
||||
q.default = _path_suggestion(suggested_filename)
|
||||
|
||||
message_bridge = objreg.get('message-bridge', scope='window',
|
||||
window=win_id)
|
||||
q.ask = lambda: message_bridge.ask(q, blocking=False)
|
||||
q.ask = lambda: message.global_bridge.ask(q, blocking=False)
|
||||
return _DownloadPath(filename=None, question=q)
|
||||
|
||||
|
||||
@ -382,20 +383,13 @@ class DownloadItem(QObject):
|
||||
else:
|
||||
self.set_fileobj(fileobj)
|
||||
|
||||
def _ask_confirm_question(self, msg):
|
||||
def _ask_confirm_question(self, title, msg):
|
||||
"""Create a Question object to be asked."""
|
||||
q = usertypes.Question(self)
|
||||
q.text = msg
|
||||
q.mode = usertypes.PromptMode.yesno
|
||||
q.answered_yes.connect(self._create_fileobj)
|
||||
q.answered_no.connect(functools.partial(self.cancel,
|
||||
remove_data=False))
|
||||
q.cancelled.connect(functools.partial(self.cancel, remove_data=False))
|
||||
self.cancelled.connect(q.abort)
|
||||
self.error.connect(q.abort)
|
||||
message_bridge = objreg.get('message-bridge', scope='window',
|
||||
window=self._win_id)
|
||||
message_bridge.ask(q, blocking=False)
|
||||
no_action = functools.partial(self.cancel, remove_data=False)
|
||||
message.confirm_async(title=title, text=msg,
|
||||
yes_action=self._create_fileobj,
|
||||
no_action=no_action, cancel_action=no_action,
|
||||
abort_on=[self.cancelled, self.error])
|
||||
|
||||
def _die(self, msg):
|
||||
"""Abort the download and emit an error."""
|
||||
@ -614,14 +608,15 @@ class DownloadItem(QObject):
|
||||
if os.path.isfile(self._filename):
|
||||
# The file already exists, so ask the user if it should be
|
||||
# overwritten.
|
||||
txt = self._filename + " already exists. Overwrite?"
|
||||
self._ask_confirm_question(txt)
|
||||
txt = "<b>{}</b> already exists. Overwrite?".format(
|
||||
html.escape(self._filename))
|
||||
self._ask_confirm_question("Overwrite existing file?", txt)
|
||||
# FIFO, device node, etc. Make sure we want to do this
|
||||
elif (os.path.exists(self._filename) and
|
||||
not os.path.isdir(self._filename)):
|
||||
txt = (self._filename + " already exists and is a special file. "
|
||||
"Write to this?")
|
||||
self._ask_confirm_question(txt)
|
||||
txt = ("<b>{}</b> already exists and is a special file. Write to "
|
||||
"it anyways?".format(html.escape(self._filename)))
|
||||
self._ask_confirm_question("Overwrite special file?", txt)
|
||||
else:
|
||||
self._create_fileobj()
|
||||
|
||||
@ -963,9 +958,9 @@ class DownloadManager(QObject):
|
||||
|
||||
# Neither filename nor fileobj were given, prepare a question
|
||||
filename, q = ask_for_filename(
|
||||
suggested_filename, self._win_id, parent=self,
|
||||
suggested_filename, parent=self,
|
||||
prompt_download_directory=prompt_download_directory,
|
||||
)
|
||||
url=reply.url())
|
||||
|
||||
# User doesn't want to be asked, so just use the download_dir
|
||||
if filename is not None:
|
||||
|
@ -22,7 +22,9 @@
|
||||
import os
|
||||
import collections
|
||||
import netrc
|
||||
import html
|
||||
|
||||
import jinja2
|
||||
from PyQt5.QtCore import (pyqtSlot, pyqtSignal, PYQT_VERSION, QCoreApplication,
|
||||
QUrl, QByteArray)
|
||||
from PyQt5.QtNetwork import (QNetworkAccessManager, QNetworkReply, QSslError,
|
||||
@ -207,10 +209,11 @@ class NetworkManager(QNetworkAccessManager):
|
||||
self.setCache(cache)
|
||||
cache.setParent(app)
|
||||
|
||||
def _ask(self, text, mode, owner=None):
|
||||
def _ask(self, title, text, mode, owner=None, default=None):
|
||||
"""Ask a blocking question in the statusbar.
|
||||
|
||||
Args:
|
||||
title: The title to display to the user.
|
||||
text: The text to display to the user.
|
||||
mode: A PromptMode.
|
||||
owner: An object which will abort the question if destroyed, or
|
||||
@ -219,24 +222,19 @@ class NetworkManager(QNetworkAccessManager):
|
||||
Return:
|
||||
The answer the user gave or None if the prompt was cancelled.
|
||||
"""
|
||||
q = usertypes.Question()
|
||||
q.text = text
|
||||
q.mode = mode
|
||||
self.shutting_down.connect(q.abort)
|
||||
abort_on = [self.shutting_down]
|
||||
if owner is not None:
|
||||
owner.destroyed.connect(q.abort)
|
||||
abort_on.append(owner.destroyed)
|
||||
|
||||
# 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:
|
||||
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)
|
||||
q.deleteLater()
|
||||
return q.answer
|
||||
abort_on.append(tab.load_started)
|
||||
|
||||
return message.ask(title=title, text=text, mode=mode,
|
||||
abort_on=abort_on, default=default)
|
||||
|
||||
def shutdown(self):
|
||||
"""Abort all running requests."""
|
||||
@ -283,9 +281,19 @@ class NetworkManager(QNetworkAccessManager):
|
||||
return
|
||||
|
||||
if ssl_strict == 'ask':
|
||||
err_string = '\n'.join('- ' + err.errorString() for err in errors)
|
||||
answer = self._ask('SSL errors - continue?\n{}'.format(err_string),
|
||||
mode=usertypes.PromptMode.yesno, owner=reply)
|
||||
err_template = jinja2.Template("""
|
||||
Errors while loading <b>{{url.toDisplayString()}}</b>:<br/>
|
||||
<ul>
|
||||
{% for err in errors %}
|
||||
<li>{{err.errorString()}}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
""".strip())
|
||||
msg = err_template.render(url=reply.url(), errors=errors)
|
||||
|
||||
answer = self._ask('SSL errors - continue?', msg,
|
||||
mode=usertypes.PromptMode.yesno, owner=reply,
|
||||
default=False)
|
||||
log.webview.debug("Asked for SSL errors, answer {}".format(answer))
|
||||
if answer:
|
||||
reply.ignoreSslErrors()
|
||||
@ -343,8 +351,11 @@ class NetworkManager(QNetworkAccessManager):
|
||||
|
||||
if user is None:
|
||||
# netrc check failed
|
||||
answer = self._ask("Username ({}):".format(authenticator.realm()),
|
||||
mode=usertypes.PromptMode.user_pwd,
|
||||
msg = '<b>{}</b> says:<br/>{}'.format(
|
||||
html.escape(reply.url().toDisplayString()),
|
||||
html.escape(authenticator.realm()))
|
||||
answer = self._ask("Authentication required",
|
||||
text=msg, mode=usertypes.PromptMode.user_pwd,
|
||||
owner=reply)
|
||||
if answer is not None:
|
||||
user, password = answer.user, answer.password
|
||||
@ -361,8 +372,11 @@ class NetworkManager(QNetworkAccessManager):
|
||||
authenticator.setUser(user)
|
||||
authenticator.setPassword(password)
|
||||
else:
|
||||
msg = '<b>{}</b> says:<br/>{}'.format(
|
||||
html.escape(proxy.hostName()),
|
||||
html.escape(authenticator.realm()))
|
||||
answer = self._ask(
|
||||
"Proxy username ({}):".format(authenticator.realm()),
|
||||
"Proxy authentication required", msg,
|
||||
mode=usertypes.PromptMode.user_pwd)
|
||||
if answer is not None:
|
||||
authenticator.setUser(answer.user)
|
||||
|
@ -145,10 +145,11 @@ class WebKitElement(webelem.AbstractWebElement):
|
||||
this.dispatchEvent(event);
|
||||
""".format(javascript.string_escape(text)))
|
||||
|
||||
def parent(self):
|
||||
def _parent(self):
|
||||
"""Get the parent element of this element."""
|
||||
self._check_vanished()
|
||||
elem = self._elem.parent()
|
||||
if elem is None:
|
||||
if elem is None or elem.isNull():
|
||||
return None
|
||||
return WebKitElement(elem, tab=self._tab)
|
||||
|
||||
@ -283,6 +284,18 @@ class WebKitElement(webelem.AbstractWebElement):
|
||||
visible_in_frame = visible_on_screen
|
||||
return all([visible_on_screen, visible_in_frame])
|
||||
|
||||
def remove_blank_target(self):
|
||||
elem = self
|
||||
for _ in range(5):
|
||||
if elem is None:
|
||||
break
|
||||
tag = elem.tag_name()
|
||||
if tag == 'a' or tag == 'area':
|
||||
if elem.get('target', None) == '_blank':
|
||||
elem['target'] = '_top'
|
||||
break
|
||||
elem = elem._parent() # pylint: disable=protected-access
|
||||
|
||||
|
||||
def get_child_frames(startframe):
|
||||
"""Get all children recursively of a given QWebFrame.
|
||||
|
@ -26,19 +26,27 @@ import xml.etree.ElementTree
|
||||
from PyQt5.QtCore import (pyqtSlot, Qt, QEvent, QUrl, QPoint, QTimer, QSizeF,
|
||||
QSize)
|
||||
from PyQt5.QtGui import QKeyEvent
|
||||
from PyQt5.QtWidgets import QApplication
|
||||
from PyQt5.QtWebKitWidgets import QWebPage, QWebFrame
|
||||
from PyQt5.QtWebKit import QWebSettings
|
||||
from PyQt5.QtPrintSupport import QPrinter
|
||||
|
||||
from qutebrowser.browser import browsertab
|
||||
from qutebrowser.browser.webkit import webview, tabhistory, webkitelem
|
||||
from qutebrowser.browser.webkit.network import proxy, webkitqutescheme
|
||||
from qutebrowser.utils import qtutils, objreg, usertypes, utils, log
|
||||
|
||||
|
||||
def init():
|
||||
"""Initialize QtWebKit-specific modules."""
|
||||
# FIXME:qtwebengine Move things we don't need with QtWebEngine here.
|
||||
pass
|
||||
qapp = QApplication.instance()
|
||||
|
||||
log.init.debug("Initializing proxy...")
|
||||
proxy.init()
|
||||
|
||||
log.init.debug("Initializing js-bridge...")
|
||||
js_bridge = webkitqutescheme.JSBridge(qapp)
|
||||
objreg.register('js-bridge', js_bridge)
|
||||
|
||||
|
||||
class WebKitPrinting(browsertab.AbstractPrinting):
|
||||
@ -624,14 +632,11 @@ class WebKitTab(browsertab.AbstractTab):
|
||||
def run_js_async(self, code, callback=None, *, world=None):
|
||||
if world is not None and world != usertypes.JsWorld.jseval:
|
||||
log.webview.warning("Ignoring world ID {}".format(world))
|
||||
result = self._widget.page().mainFrame().evaluateJavaScript(code)
|
||||
document_element = self._widget.page().mainFrame().documentElement()
|
||||
result = document_element.evaluateJavaScript(code)
|
||||
if callback is not None:
|
||||
callback(result)
|
||||
|
||||
def has_js(self):
|
||||
settings = QWebSettings.globalSettings()
|
||||
return settings.testAttribute(QWebSettings.JavascriptEnabled)
|
||||
|
||||
def icon(self):
|
||||
return self._widget.icon()
|
||||
|
||||
|
@ -19,6 +19,7 @@
|
||||
|
||||
"""The main browser widgets."""
|
||||
|
||||
import html
|
||||
import functools
|
||||
|
||||
from PyQt5.QtCore import pyqtSlot, pyqtSignal, PYQT_VERSION, Qt, QUrl, QPoint
|
||||
@ -93,13 +94,19 @@ class BrowserPage(QWebPage):
|
||||
# of a bug in PyQt.
|
||||
# See http://www.riverbankcomputing.com/pipermail/pyqt/2014-June/034385.html
|
||||
|
||||
def javaScriptPrompt(self, _frame, msg, default):
|
||||
def javaScriptPrompt(self, _frame, js_msg, default):
|
||||
"""Override javaScriptPrompt to use the statusbar."""
|
||||
if (self._is_shutting_down or
|
||||
config.get('content', 'ignore-javascript-prompt')):
|
||||
return (False, "")
|
||||
answer = self._ask("js: {}".format(msg), usertypes.PromptMode.text,
|
||||
default)
|
||||
msg = '<b>{}</b> asks:<br/>{}'.format(
|
||||
html.escape(self.mainFrame().url().toDisplayString()),
|
||||
html.escape(js_msg))
|
||||
answer = message.ask('Javascript prompt', msg,
|
||||
mode=usertypes.PromptMode.text,
|
||||
default=default,
|
||||
abort_on=[self.loadStarted,
|
||||
self.shutting_down])
|
||||
if answer is None:
|
||||
return (False, "")
|
||||
else:
|
||||
@ -134,11 +141,12 @@ class BrowserPage(QWebPage):
|
||||
# QDesktopServices::openUrl with info.url directly - however it
|
||||
# works when we construct a copy of it.
|
||||
url = QUrl(info.url)
|
||||
msg = "Open external application for {}-link?\nURL: {}".format(
|
||||
url.scheme(), url.toDisplayString())
|
||||
scheme = url.scheme()
|
||||
message.confirm_async(
|
||||
self._win_id, msg,
|
||||
functools.partial(QDesktopServices.openUrl, url))
|
||||
title="Open external application for {}-link?".format(scheme),
|
||||
text="URL: <b>{}</b>".format(
|
||||
html.escape(url.toDisplayString())),
|
||||
yes_action=functools.partial(QDesktopServices.openUrl, url))
|
||||
return True
|
||||
elif (info.domain, info.error) in ignored_errors:
|
||||
log.webview.debug("Ignored error on {}: {} (error domain: {}, "
|
||||
@ -168,11 +176,11 @@ class BrowserPage(QWebPage):
|
||||
log.webview.debug("Error domain: {}, error code: {}".format(
|
||||
info.domain, info.error))
|
||||
title = "Error loading page: {}".format(urlstr)
|
||||
html = jinja.render(
|
||||
error_html = jinja.render(
|
||||
'error.html',
|
||||
title=title, url=urlstr, error=error_str, icon='',
|
||||
qutescheme=False)
|
||||
errpage.content = html.encode('utf-8')
|
||||
errpage.content = error_html.encode('utf-8')
|
||||
errpage.encoding = 'utf-8'
|
||||
return True
|
||||
|
||||
@ -196,29 +204,6 @@ class BrowserPage(QWebPage):
|
||||
suggested_file)
|
||||
return True
|
||||
|
||||
def _ask(self, text, mode, default=None):
|
||||
"""Ask a blocking question in the statusbar.
|
||||
|
||||
Args:
|
||||
text: The text to display to the user.
|
||||
mode: A PromptMode.
|
||||
default: The default value to display.
|
||||
|
||||
Return:
|
||||
The answer the user gave or None if the prompt was cancelled.
|
||||
"""
|
||||
q = usertypes.Question()
|
||||
q.text = text
|
||||
q.mode = mode
|
||||
q.default = default
|
||||
self.loadStarted.connect(q.abort)
|
||||
self.shutting_down.connect(q.abort)
|
||||
bridge = objreg.get('message-bridge', scope='window',
|
||||
window=self._win_id)
|
||||
bridge.ask(q, blocking=True)
|
||||
q.deleteLater()
|
||||
return q.answer
|
||||
|
||||
def _show_pdfjs(self, reply):
|
||||
"""Show the reply with pdfjs."""
|
||||
try:
|
||||
@ -333,11 +318,6 @@ class BrowserPage(QWebPage):
|
||||
}
|
||||
config_val = config.get(*options[feature])
|
||||
if config_val == 'ask':
|
||||
bridge = objreg.get('message-bridge', scope='window',
|
||||
window=self._win_id)
|
||||
q = usertypes.Question(bridge)
|
||||
q.mode = usertypes.PromptMode.yesno
|
||||
|
||||
msgs = {
|
||||
QWebPage.Notifications: 'show notifications',
|
||||
QWebPage.Geolocation: 'access your location',
|
||||
@ -345,30 +325,28 @@ class BrowserPage(QWebPage):
|
||||
|
||||
host = frame.url().host()
|
||||
if host:
|
||||
q.text = "Allow the website at {} to {}?".format(
|
||||
frame.url().host(), msgs[feature])
|
||||
text = "Allow the website at <b>{}</b> to {}?".format(
|
||||
html.escape(frame.url().toDisplayString()), msgs[feature])
|
||||
else:
|
||||
q.text = "Allow the website to {}?".format(msgs[feature])
|
||||
text = "Allow the website to {}?".format(msgs[feature])
|
||||
|
||||
yes_action = functools.partial(
|
||||
self.setFeaturePermission, frame, feature,
|
||||
QWebPage.PermissionGrantedByUser)
|
||||
q.answered_yes.connect(yes_action)
|
||||
|
||||
no_action = functools.partial(
|
||||
self.setFeaturePermission, frame, feature,
|
||||
QWebPage.PermissionDeniedByUser)
|
||||
q.answered_no.connect(no_action)
|
||||
q.cancelled.connect(no_action)
|
||||
|
||||
self.shutting_down.connect(q.abort)
|
||||
q.completed.connect(q.deleteLater)
|
||||
|
||||
self.featurePermissionRequestCanceled.connect(functools.partial(
|
||||
self.on_feature_permission_cancelled, q, frame, feature))
|
||||
self.loadStarted.connect(q.abort)
|
||||
|
||||
bridge.ask(q, blocking=False)
|
||||
question = message.confirm_async(yes_action=yes_action,
|
||||
no_action=no_action,
|
||||
cancel_action=no_action,
|
||||
abort_on=[self.shutting_down,
|
||||
self.loadStarted],
|
||||
title='Permission request',
|
||||
text=text)
|
||||
self.featurePermissionRequestCanceled.connect(
|
||||
functools.partial(self.on_feature_permission_cancelled,
|
||||
question, frame, feature))
|
||||
elif config_val:
|
||||
self.setFeaturePermission(frame, feature,
|
||||
QWebPage.PermissionGrantedByUser)
|
||||
@ -469,27 +447,37 @@ class BrowserPage(QWebPage):
|
||||
return super().extension(ext, opt, out)
|
||||
return handler(opt, out)
|
||||
|
||||
def javaScriptAlert(self, frame, msg):
|
||||
def javaScriptAlert(self, frame, js_msg):
|
||||
"""Override javaScriptAlert to use the statusbar."""
|
||||
log.js.debug("alert: {}".format(msg))
|
||||
log.js.debug("alert: {}".format(js_msg))
|
||||
if config.get('ui', 'modal-js-dialog'):
|
||||
return super().javaScriptAlert(frame, msg)
|
||||
return super().javaScriptAlert(frame, js_msg)
|
||||
|
||||
if (self._is_shutting_down or
|
||||
config.get('content', 'ignore-javascript-alert')):
|
||||
return
|
||||
self._ask("[js alert] {}".format(msg), usertypes.PromptMode.alert)
|
||||
|
||||
def javaScriptConfirm(self, frame, msg):
|
||||
msg = 'From <b>{}</b>:<br/>{}'.format(
|
||||
html.escape(self.mainFrame().url().toDisplayString()),
|
||||
html.escape(js_msg))
|
||||
message.ask('Javascript alert', msg, mode=usertypes.PromptMode.alert,
|
||||
abort_on=[self.loadStarted, self.shutting_down])
|
||||
|
||||
def javaScriptConfirm(self, frame, js_msg):
|
||||
"""Override javaScriptConfirm to use the statusbar."""
|
||||
log.js.debug("confirm: {}".format(msg))
|
||||
log.js.debug("confirm: {}".format(js_msg))
|
||||
if config.get('ui', 'modal-js-dialog'):
|
||||
return super().javaScriptConfirm(frame, msg)
|
||||
return super().javaScriptConfirm(frame, js_msg)
|
||||
|
||||
if self._is_shutting_down:
|
||||
return False
|
||||
ans = self._ask("[js confirm] {}".format(msg),
|
||||
usertypes.PromptMode.yesno)
|
||||
|
||||
msg = 'From <b>{}</b>:<br/>{}'.format(
|
||||
html.escape(self.mainFrame().url().toDisplayString()),
|
||||
html.escape(js_msg))
|
||||
ans = message.ask('Javascript confirm', msg,
|
||||
mode=usertypes.PromptMode.yesno,
|
||||
abort_on=[self.loadStarted, self.shutting_down])
|
||||
return bool(ans)
|
||||
|
||||
def javaScriptConsoleMessage(self, msg, line, source):
|
||||
|
@ -427,9 +427,16 @@ class Command:
|
||||
if isinstance(typ, tuple):
|
||||
raise TypeError("{}: Legacy tuple type annotation!".format(
|
||||
self.name))
|
||||
elif issubclass(typ, typing.Union):
|
||||
elif type(typ) is type(typing.Union): # flake8: disable=E721
|
||||
# this is... slightly evil, I know
|
||||
types = list(typ.__union_params__) # pylint: disable=no-member
|
||||
# We also can't use isinstance here because typing.Union doesn't
|
||||
# support that.
|
||||
# pylint: disable=no-member,useless-suppression
|
||||
try:
|
||||
types = list(typ.__union_params__)
|
||||
except AttributeError:
|
||||
types = list(typ.__args__)
|
||||
# pylint: enable=no-member,useless-suppression
|
||||
if param.default is not inspect.Parameter.empty:
|
||||
types.append(type(param.default))
|
||||
choices = self.get_arg_info(param).choices
|
||||
|
@ -391,6 +391,8 @@ class ConfigManager(QObject):
|
||||
('colors', 'statusbar.bg.error'): 'messages.bg.error',
|
||||
('colors', 'statusbar.fg.warning'): 'messages.fg.warning',
|
||||
('colors', 'statusbar.bg.warning'): 'messages.bg.warning',
|
||||
('colors', 'statusbar.fg.prompt'): 'prompts.fg',
|
||||
('colors', 'statusbar.bg.prompt'): 'prompts.bg',
|
||||
}
|
||||
DELETED_OPTIONS = [
|
||||
('colors', 'tab.separator'),
|
||||
|
@ -90,7 +90,7 @@ SECTION_DESC = {
|
||||
"Aliases for commands.\n"
|
||||
"By default, no aliases are defined. Example which adds a new command "
|
||||
"`:qtb` to open qutebrowsers website:\n\n"
|
||||
"`qtb = open http://www.qutebrowser.org/`"),
|
||||
"`qtb = open https://www.qutebrowser.org/`"),
|
||||
'colors': (
|
||||
"Colors used in the UI.\n"
|
||||
"A value can be in one of the following format:\n\n"
|
||||
@ -136,7 +136,8 @@ def data(readonly=False):
|
||||
"Whether to find text on a page case-insensitively."),
|
||||
|
||||
('startpage',
|
||||
SettingValue(typ.List(typ.String()), 'https://duckduckgo.com'),
|
||||
SettingValue(typ.List(typ.String()),
|
||||
'https://start.duckduckgo.com'),
|
||||
"The default page(s) to open at the start, separated by commas."),
|
||||
|
||||
('yank-ignored-url-parameters',
|
||||
@ -383,6 +384,10 @@ def data(readonly=False):
|
||||
"Globs are supported, so ';*' will blacklist all keychains"
|
||||
"starting with ';'. Use '*' to disable keyhints"),
|
||||
|
||||
('prompt-radius',
|
||||
SettingValue(typ.Int(minval=0), '8'),
|
||||
"The rounding radius for the edges of prompts."),
|
||||
|
||||
readonly=readonly
|
||||
)),
|
||||
|
||||
@ -871,11 +876,11 @@ def data(readonly=False):
|
||||
('host-block-lists',
|
||||
SettingValue(
|
||||
typ.List(typ.Url(), none_ok=True),
|
||||
'http://www.malwaredomainlist.com/hostslist/hosts.txt,'
|
||||
'https://www.malwaredomainlist.com/hostslist/hosts.txt,'
|
||||
'http://someonewhocares.org/hosts/hosts,'
|
||||
'http://winhelp2002.mvps.org/hosts.zip,'
|
||||
'http://malwaredomains.lehigh.edu/files/justdomains.zip,'
|
||||
'http://pgl.yoyo.org/adservers/serverlist.php?'
|
||||
'https://pgl.yoyo.org/adservers/serverlist.php?'
|
||||
'hostformat=hosts&mimetype=plaintext'),
|
||||
"List of URLs of lists which contain hosts to block.\n\n"
|
||||
"The file can be in one of the following formats:\n\n"
|
||||
@ -1074,14 +1079,6 @@ def data(readonly=False):
|
||||
SettingValue(typ.QssColor(), 'black'),
|
||||
"Background color of the statusbar."),
|
||||
|
||||
('statusbar.fg.prompt',
|
||||
SettingValue(typ.QssColor(), '${statusbar.fg}'),
|
||||
"Foreground color of the statusbar if there is a prompt."),
|
||||
|
||||
('statusbar.bg.prompt',
|
||||
SettingValue(typ.QssColor(), 'darkblue'),
|
||||
"Background color of the statusbar if there is a prompt."),
|
||||
|
||||
('statusbar.fg.insert',
|
||||
SettingValue(typ.QssColor(), '${statusbar.fg}'),
|
||||
"Foreground color of the statusbar in insert mode."),
|
||||
@ -1305,6 +1302,18 @@ def data(readonly=False):
|
||||
SettingValue(typ.QssColor(), '#333333'),
|
||||
"Border color of an info message."),
|
||||
|
||||
('prompts.fg',
|
||||
SettingValue(typ.QssColor(), 'white'),
|
||||
"Foreground color for prompts."),
|
||||
|
||||
('prompts.bg',
|
||||
SettingValue(typ.QssColor(), 'darkblue'),
|
||||
"Background color for prompts."),
|
||||
|
||||
('prompts.selected.bg',
|
||||
SettingValue(typ.QssColor(), '#308cc6'),
|
||||
"Background color for the selected item in filename prompts."),
|
||||
|
||||
readonly=readonly
|
||||
)),
|
||||
|
||||
@ -1406,6 +1415,10 @@ def data(readonly=False):
|
||||
SettingValue(typ.Font(), DEFAULT_FONT_SIZE + ' ${_monospace}'),
|
||||
"Font used for info messages."),
|
||||
|
||||
('prompts',
|
||||
SettingValue(typ.Font(), DEFAULT_FONT_SIZE + ' sans-serif'),
|
||||
"Font used for prompts."),
|
||||
|
||||
readonly=readonly
|
||||
)),
|
||||
])
|
||||
@ -1673,6 +1686,8 @@ KEY_DATA = collections.OrderedDict([
|
||||
('prompt-accept yes', ['y']),
|
||||
('prompt-accept no', ['n']),
|
||||
('prompt-open-download', ['<Ctrl-X>']),
|
||||
('prompt-item-focus prev', ['<Shift-Tab>', '<Up>']),
|
||||
('prompt-item-focus next', ['<Tab>', '<Down>']),
|
||||
])),
|
||||
|
||||
('command,prompt', collections.OrderedDict([
|
||||
|
@ -46,7 +46,7 @@ def get_stylesheet(template_str):
|
||||
config=objreg.get('config'))
|
||||
|
||||
|
||||
def set_register_stylesheet(obj, *, generator=None):
|
||||
def set_register_stylesheet(obj):
|
||||
"""Set the stylesheet for an object based on it's STYLESHEET attribute.
|
||||
|
||||
Also, register an update when the config is changed.
|
||||
@ -54,23 +54,20 @@ def set_register_stylesheet(obj, *, generator=None):
|
||||
Args:
|
||||
obj: The object to set the stylesheet for and register.
|
||||
Must have a STYLESHEET attribute.
|
||||
generator: If set, call the given function to dynamically generate a
|
||||
stylesheet instead.
|
||||
"""
|
||||
stylesheet = generator() if generator is not None else obj.STYLESHEET
|
||||
qss = get_stylesheet(stylesheet)
|
||||
qss = get_stylesheet(obj.STYLESHEET)
|
||||
log.config.vdebug("stylesheet for {}: {}".format(
|
||||
obj.__class__.__name__, qss))
|
||||
obj.setStyleSheet(qss)
|
||||
objreg.get('config').changed.connect(
|
||||
functools.partial(_update_stylesheet, obj, generator=generator))
|
||||
functools.partial(_update_stylesheet, obj))
|
||||
|
||||
|
||||
def _update_stylesheet(obj, *, generator):
|
||||
def _update_stylesheet(obj):
|
||||
"""Update the stylesheet for obj."""
|
||||
get_stylesheet.cache_clear()
|
||||
if not sip.isdeleted(obj):
|
||||
stylesheet = generator() if generator is not None else obj.STYLESHEET
|
||||
obj.setStyleSheet(get_stylesheet(stylesheet))
|
||||
obj.setStyleSheet(get_stylesheet(obj.STYLESHEET))
|
||||
|
||||
|
||||
class ColorDict(collections.UserDict):
|
||||
|
@ -90,6 +90,9 @@ li {
|
||||
<li>
|
||||
If you have installed a packaged version of qutebrowser, make sure
|
||||
the required packages for pdf.js are also installed.
|
||||
<br/>
|
||||
The package is named <b>pdfjs</b> on Archlinux (AUR) and
|
||||
<b>libjs-pdf</b> on Debian.
|
||||
</li>
|
||||
|
||||
<li>
|
||||
|
@ -155,5 +155,23 @@ window._qutebrowser.webelem = (function() {
|
||||
return serialize_elem(elem);
|
||||
};
|
||||
|
||||
funcs.set_attribute = function(id, name, value) {
|
||||
elements[id].setAttribute(name, value);
|
||||
};
|
||||
|
||||
funcs.remove_blank_target = function(id) {
|
||||
var elem = elements[id];
|
||||
while (elem !== null) {
|
||||
var tag = elem.tagName.toLowerCase();
|
||||
if (tag === "a" || tag === "area") {
|
||||
if (elem.getAttribute("target") === "_blank") {
|
||||
elem.setAttribute("target", "_top");
|
||||
}
|
||||
break;
|
||||
}
|
||||
elem = elem.parentElement;
|
||||
}
|
||||
};
|
||||
|
||||
return funcs;
|
||||
})();
|
||||
|
@ -24,13 +24,14 @@ import base64
|
||||
import itertools
|
||||
import functools
|
||||
|
||||
import jinja2
|
||||
from PyQt5.QtCore import pyqtSlot, QRect, QPoint, QTimer, Qt
|
||||
from PyQt5.QtWidgets import QWidget, QVBoxLayout, QApplication, QSizePolicy
|
||||
|
||||
from qutebrowser.commands import runners, cmdutils
|
||||
from qutebrowser.config import config
|
||||
from qutebrowser.utils import message, log, usertypes, qtutils, objreg, utils
|
||||
from qutebrowser.mainwindow import tabbedbrowser, messageview
|
||||
from qutebrowser.mainwindow import tabbedbrowser, messageview, prompt
|
||||
from qutebrowser.mainwindow.statusbar import bar
|
||||
from qutebrowser.completion import completionwidget, completer
|
||||
from qutebrowser.keyinput import modeman
|
||||
@ -175,17 +176,24 @@ class MainWindow(QWidget):
|
||||
|
||||
self._init_completion()
|
||||
|
||||
log.init.debug("Initializing modes...")
|
||||
modeman.init(self.win_id, self)
|
||||
|
||||
self._commandrunner = runners.CommandRunner(self.win_id,
|
||||
partial_match=True)
|
||||
|
||||
self._keyhint = keyhintwidget.KeyHintView(self.win_id, self)
|
||||
self._overlays.append((self._keyhint, self._keyhint.update_geometry))
|
||||
self._add_overlay(self._keyhint, self._keyhint.update_geometry)
|
||||
self._messageview = messageview.MessageView(parent=self)
|
||||
self._overlays.append((self._messageview,
|
||||
self._messageview.update_geometry))
|
||||
self._add_overlay(self._messageview, self._messageview.update_geometry)
|
||||
|
||||
log.init.debug("Initializing modes...")
|
||||
modeman.init(self.win_id, self)
|
||||
self._prompt_container = prompt.PromptContainer(self.win_id, self)
|
||||
self._add_overlay(self._prompt_container,
|
||||
self._prompt_container.update_geometry,
|
||||
centered=True, padding=10)
|
||||
objreg.register('prompt-container', self._prompt_container,
|
||||
scope='window', window=self.win_id)
|
||||
self._prompt_container.hide()
|
||||
|
||||
if geometry is not None:
|
||||
self._load_geometry(geometry)
|
||||
@ -206,36 +214,40 @@ class MainWindow(QWidget):
|
||||
|
||||
objreg.get("app").new_window.emit(self)
|
||||
|
||||
def _update_overlay_geometry(self, widget=None):
|
||||
"""Reposition/resize the given overlay.
|
||||
def _add_overlay(self, widget, signal, *, centered=False, padding=0):
|
||||
self._overlays.append((widget, signal, centered, padding))
|
||||
|
||||
If no widget is given, reposition/resize all overlays.
|
||||
"""
|
||||
if widget is None:
|
||||
for w, _signal in self._overlays:
|
||||
self._update_overlay_geometry(w)
|
||||
return
|
||||
def _update_overlay_geometries(self):
|
||||
"""Update the size/position of all overlays."""
|
||||
for w, _signal, centered, padding in self._overlays:
|
||||
self._update_overlay_geometry(w, centered, padding)
|
||||
|
||||
def _update_overlay_geometry(self, widget, centered, padding):
|
||||
"""Reposition/resize the given overlay."""
|
||||
if not widget.isVisible():
|
||||
return
|
||||
|
||||
size_hint = widget.sizeHint()
|
||||
if widget.sizePolicy().horizontalPolicy() == QSizePolicy.Expanding:
|
||||
width = self.width()
|
||||
width = self.width() - 2 * padding
|
||||
left = padding
|
||||
else:
|
||||
width = size_hint.width()
|
||||
left = (self.width() - size_hint.width()) / 2 if centered else 0
|
||||
|
||||
height_padding = 20
|
||||
status_position = config.get('ui', 'status-position')
|
||||
if status_position == 'bottom':
|
||||
top = self.height() - self.status.height() - size_hint.height()
|
||||
top = qtutils.check_overflow(top, 'int', fatal=False)
|
||||
topleft = QPoint(0, top)
|
||||
bottomright = QPoint(width, self.status.geometry().top())
|
||||
topleft = QPoint(left, max(height_padding, top))
|
||||
bottomright = QPoint(left + width, self.status.geometry().top())
|
||||
elif status_position == 'top':
|
||||
topleft = self.status.geometry().bottomLeft()
|
||||
topleft = QPoint(left, self.status.geometry().bottom())
|
||||
bottom = self.status.height() + size_hint.height()
|
||||
bottom = qtutils.check_overflow(bottom, 'int', fatal=False)
|
||||
bottomright = QPoint(width, bottom)
|
||||
bottomright = QPoint(left + width,
|
||||
min(self.height() - height_padding, bottom))
|
||||
else:
|
||||
raise ValueError("Invalid position {}!".format(status_position))
|
||||
|
||||
@ -261,8 +273,7 @@ class MainWindow(QWidget):
|
||||
completer_obj.on_selection_changed)
|
||||
objreg.register('completion', self._completion, scope='window',
|
||||
window=self.win_id)
|
||||
self._overlays.append((self._completion,
|
||||
self._completion.update_geometry))
|
||||
self._add_overlay(self._completion, self._completion.update_geometry)
|
||||
|
||||
def _init_command_dispatcher(self):
|
||||
dispatcher = commands.CommandDispatcher(self.win_id,
|
||||
@ -282,12 +293,12 @@ class MainWindow(QWidget):
|
||||
if section != 'ui':
|
||||
return
|
||||
if option == 'statusbar-padding':
|
||||
self._update_overlay_geometry()
|
||||
self._update_overlay_geometries()
|
||||
elif option == 'downloads-position':
|
||||
self._add_widgets()
|
||||
elif option == 'status-position':
|
||||
self._add_widgets()
|
||||
self._update_overlay_geometry()
|
||||
self._update_overlay_geometries()
|
||||
|
||||
def _add_widgets(self):
|
||||
"""Add or readd all widgets to the VBox."""
|
||||
@ -350,10 +361,11 @@ class MainWindow(QWidget):
|
||||
|
||||
def _connect_overlay_signals(self):
|
||||
"""Connect the resize signal and resize everything once."""
|
||||
for widget, signal in self._overlays:
|
||||
for widget, signal, centered, padding in self._overlays:
|
||||
signal.connect(
|
||||
functools.partial(self._update_overlay_geometry, widget))
|
||||
self._update_overlay_geometry(widget)
|
||||
functools.partial(self._update_overlay_geometry, widget,
|
||||
centered, padding))
|
||||
self._update_overlay_geometry(widget, centered, padding)
|
||||
|
||||
def _set_default_geometry(self):
|
||||
"""Set some sensible default geometry."""
|
||||
@ -374,7 +386,6 @@ class MainWindow(QWidget):
|
||||
cmd = self._get_object('status-command')
|
||||
message_bridge = self._get_object('message-bridge')
|
||||
mode_manager = self._get_object('mode-manager')
|
||||
prompter = self._get_object('prompter')
|
||||
|
||||
# misc
|
||||
self.tabbed_browser.close_window.connect(self.close)
|
||||
@ -384,7 +395,7 @@ class MainWindow(QWidget):
|
||||
mode_manager.entered.connect(status.on_mode_entered)
|
||||
mode_manager.left.connect(status.on_mode_left)
|
||||
mode_manager.left.connect(cmd.on_mode_left)
|
||||
mode_manager.left.connect(prompter.on_mode_left)
|
||||
mode_manager.left.connect(message.global_bridge.mode_left)
|
||||
|
||||
# commands
|
||||
keyparsers[usertypes.KeyMode.normal].keystring_updated.connect(
|
||||
@ -407,9 +418,6 @@ class MainWindow(QWidget):
|
||||
|
||||
message_bridge.s_set_text.connect(status.set_text)
|
||||
message_bridge.s_maybe_reset_text.connect(status.txt.maybe_reset_text)
|
||||
message_bridge.s_set_cmd_text.connect(cmd.set_cmd_text)
|
||||
message_bridge.s_question.connect(prompter.ask_question,
|
||||
Qt.DirectConnection)
|
||||
|
||||
# statusbar
|
||||
tabs.current_tab_changed.connect(status.prog.on_tab_changed)
|
||||
@ -459,7 +467,7 @@ class MainWindow(QWidget):
|
||||
e: The QResizeEvent
|
||||
"""
|
||||
super().resizeEvent(e)
|
||||
self._update_overlay_geometry()
|
||||
self._update_overlay_geometries()
|
||||
self._downloadview.updateGeometry()
|
||||
self.tabbed_browser.tabBar().refresh()
|
||||
|
||||
@ -507,10 +515,17 @@ class MainWindow(QWidget):
|
||||
"download is" if download_count == 1 else "downloads are"))
|
||||
# Process all quit messages that user must confirm
|
||||
if quit_texts or 'always' in confirm_quit:
|
||||
text = '\n'.join(['Really quit?'] + quit_texts)
|
||||
confirmed = message.ask(self.win_id, text,
|
||||
usertypes.PromptMode.yesno,
|
||||
msg = jinja2.Template("""
|
||||
<ul>
|
||||
{% for text in quit_texts %}
|
||||
<li>{{text}}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
""".strip()).render(quit_texts=quit_texts)
|
||||
confirmed = message.ask('Really quit?', msg,
|
||||
mode=usertypes.PromptMode.yesno,
|
||||
default=True)
|
||||
|
||||
# Stop asking if the user cancels
|
||||
if not confirmed:
|
||||
log.destroy.debug("Cancelling closing of window {}".format(
|
||||
|
829
qutebrowser/mainwindow/prompt.py
Normal file
829
qutebrowser/mainwindow/prompt.py
Normal file
@ -0,0 +1,829 @@
|
||||
# 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/>.
|
||||
|
||||
"""Showing prompts above the statusbar."""
|
||||
|
||||
import os.path
|
||||
import html
|
||||
import collections
|
||||
|
||||
import sip
|
||||
from PyQt5.QtCore import (pyqtSlot, pyqtSignal, Qt, QTimer, QDir, QModelIndex,
|
||||
QItemSelectionModel, QObject)
|
||||
from PyQt5.QtWidgets import (QWidget, QGridLayout, QVBoxLayout, QLineEdit,
|
||||
QLabel, QFileSystemModel, QTreeView, QSizePolicy)
|
||||
|
||||
from qutebrowser.config import style
|
||||
from qutebrowser.utils import usertypes, log, utils, qtutils, objreg, message
|
||||
from qutebrowser.keyinput import modeman
|
||||
from qutebrowser.commands import cmdutils, cmdexc
|
||||
|
||||
|
||||
prompt_queue = None
|
||||
|
||||
|
||||
AuthTuple = collections.namedtuple('AuthTuple', ['user', 'password'])
|
||||
|
||||
|
||||
class Error(Exception):
|
||||
|
||||
"""Base class for errors in this module."""
|
||||
|
||||
|
||||
class UnsupportedOperationError(Exception):
|
||||
|
||||
"""Raised when the prompt class doesn't support the requested operation."""
|
||||
|
||||
|
||||
class PromptQueue(QObject):
|
||||
|
||||
"""Global manager and queue for upcoming prompts.
|
||||
|
||||
The way in which multiple questions are handled deserves some explanation.
|
||||
|
||||
If a question is blocking, we *need* to ask it immediately, and can't wait
|
||||
for previous questions to finish. We could theoretically ask a blocking
|
||||
question inside of another blocking one, so in ask_question we simply save
|
||||
the current question on the stack, let the user answer the *most recent*
|
||||
question, and then restore the previous state.
|
||||
|
||||
With a non-blocking question, things are a bit easier. We simply add it to
|
||||
self._queue if we're still busy handling another question, since it can be
|
||||
answered at any time.
|
||||
|
||||
In either case, as soon as we finished handling a question, we call
|
||||
_pop_later() which schedules a _pop to ask the next question in _queue. We
|
||||
schedule it rather than doing it immediately because then the order of how
|
||||
things happen is clear, e.g. on_mode_left can't happen after we already set
|
||||
up the *new* question.
|
||||
|
||||
Attributes:
|
||||
_shutting_down: Whether we're currently shutting down the prompter and
|
||||
should ignore future questions to avoid segfaults.
|
||||
_loops: A list of local EventLoops to spin in when blocking.
|
||||
_queue: A deque of waiting questions.
|
||||
_question: The current Question object if we're handling a question.
|
||||
|
||||
Signals:
|
||||
show_prompts: Emitted with a Question object when prompts should be
|
||||
shown.
|
||||
"""
|
||||
|
||||
show_prompts = pyqtSignal(usertypes.Question)
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self._question = None
|
||||
self._shutting_down = False
|
||||
self._loops = []
|
||||
self._queue = collections.deque()
|
||||
message.global_bridge.mode_left.connect(self._on_mode_left)
|
||||
|
||||
def __repr__(self):
|
||||
return utils.get_repr(self, loops=len(self._loops),
|
||||
queue=len(self._queue), question=self._question)
|
||||
|
||||
def _pop_later(self):
|
||||
"""Helper to call self._pop as soon as everything else is done."""
|
||||
QTimer.singleShot(0, self._pop)
|
||||
|
||||
def _pop(self):
|
||||
"""Pop a question from the queue and ask it, if there are any."""
|
||||
log.prompt.debug("Popping from queue {}".format(self._queue))
|
||||
if self._queue:
|
||||
question = self._queue.popleft()
|
||||
if not sip.isdeleted(question):
|
||||
# the question could already be deleted, e.g. by a cancelled
|
||||
# download. See
|
||||
# https://github.com/The-Compiler/qutebrowser/issues/415
|
||||
self.ask_question(question, blocking=False)
|
||||
|
||||
def shutdown(self):
|
||||
"""Cancel all blocking questions.
|
||||
|
||||
Quits and removes all running event loops.
|
||||
|
||||
Return:
|
||||
True if loops needed to be aborted,
|
||||
False otherwise.
|
||||
"""
|
||||
log.prompt.debug("Shutting down with loops {}".format(self._loops))
|
||||
self._shutting_down = True
|
||||
if self._loops:
|
||||
for loop in self._loops:
|
||||
loop.quit()
|
||||
loop.deleteLater()
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
@pyqtSlot(usertypes.Question, bool)
|
||||
def ask_question(self, question, blocking):
|
||||
"""Display a prompt for a given question.
|
||||
|
||||
Args:
|
||||
question: The Question object to ask.
|
||||
blocking: If True, this function blocks and returns the result.
|
||||
|
||||
Return:
|
||||
The answer of the user when blocking=True.
|
||||
None if blocking=False.
|
||||
"""
|
||||
log.prompt.debug("Asking question {}, blocking {}, loops {}, queue "
|
||||
"{}".format(question, blocking, self._loops,
|
||||
self._queue))
|
||||
|
||||
if self._shutting_down:
|
||||
# If we're currently shutting down we have to ignore this question
|
||||
# to avoid segfaults - see
|
||||
# https://github.com/The-Compiler/qutebrowser/issues/95
|
||||
log.prompt.debug("Ignoring question because we're shutting down.")
|
||||
question.abort()
|
||||
return None
|
||||
|
||||
if self._question is not None and not blocking:
|
||||
# We got an async question, but we're already busy with one, so we
|
||||
# just queue it up for later.
|
||||
log.prompt.debug("Adding {} to queue.".format(question))
|
||||
self._queue.append(question)
|
||||
return
|
||||
|
||||
if blocking:
|
||||
# If we're blocking we save the old question on the stack, so we
|
||||
# can restore it after exec, if exec gets called multiple times.
|
||||
log.prompt.debug("New question is blocking, saving {}".format(
|
||||
self._question))
|
||||
old_question = self._question
|
||||
if old_question is not None:
|
||||
old_question.interrupted = True
|
||||
|
||||
self._question = question
|
||||
self.show_prompts.emit(question)
|
||||
|
||||
if blocking:
|
||||
loop = qtutils.EventLoop()
|
||||
self._loops.append(loop)
|
||||
loop.destroyed.connect(lambda: self._loops.remove(loop))
|
||||
question.completed.connect(loop.quit)
|
||||
question.completed.connect(loop.deleteLater)
|
||||
log.prompt.debug("Starting loop.exec_() for {}".format(question))
|
||||
loop.exec_()
|
||||
log.prompt.debug("Ending loop.exec_() for {}".format(question))
|
||||
|
||||
log.prompt.debug("Restoring old question {}".format(old_question))
|
||||
self._question = old_question
|
||||
self.show_prompts.emit(old_question)
|
||||
if old_question is None:
|
||||
# Nothing left to restore, so we can go back to popping async
|
||||
# questions.
|
||||
if self._queue:
|
||||
self._pop_later()
|
||||
|
||||
return question.answer
|
||||
else:
|
||||
question.completed.connect(self._pop_later)
|
||||
|
||||
@pyqtSlot(usertypes.KeyMode)
|
||||
def _on_mode_left(self, mode):
|
||||
"""Abort question when a prompt mode was left."""
|
||||
if mode not in [usertypes.KeyMode.prompt, usertypes.KeyMode.yesno]:
|
||||
return
|
||||
if self._question is None:
|
||||
return
|
||||
|
||||
log.prompt.debug("Left mode {}, hiding {}".format(
|
||||
mode, self._question))
|
||||
self.show_prompts.emit(None)
|
||||
if self._question.answer is None and not self._question.is_aborted:
|
||||
log.prompt.debug("Cancelling {} because {} was left".format(
|
||||
self._question, mode))
|
||||
self._question.cancel()
|
||||
self._question = None
|
||||
|
||||
|
||||
class PromptContainer(QWidget):
|
||||
|
||||
"""Container for prompts to be shown above the statusbar.
|
||||
|
||||
This is a per-window object, however each window shows the same prompt.
|
||||
|
||||
Attributes:
|
||||
_layout: The layout used to show prompts in.
|
||||
_win_id: The window ID this object is associated with.
|
||||
|
||||
Signals:
|
||||
update_geometry: Emitted when the geometry should be updated.
|
||||
"""
|
||||
|
||||
STYLESHEET = """
|
||||
{% set prompt_radius = config.get('ui', 'prompt-radius') %}
|
||||
QWidget#PromptContainer {
|
||||
{% if config.get('ui', 'status-position') == 'top' %}
|
||||
border-bottom-left-radius: {{ prompt_radius }}px;
|
||||
border-bottom-right-radius: {{ prompt_radius }}px;
|
||||
{% else %}
|
||||
border-top-left-radius: {{ prompt_radius }}px;
|
||||
border-top-right-radius: {{ prompt_radius }}px;
|
||||
{% endif %}
|
||||
}
|
||||
|
||||
QWidget {
|
||||
font: {{ font['prompts'] }};
|
||||
color: {{ color['prompts.fg'] }};
|
||||
background-color: {{ color['prompts.bg'] }};
|
||||
}
|
||||
|
||||
QTreeView {
|
||||
selection-background-color: {{ color['prompts.selected.bg'] }};
|
||||
}
|
||||
|
||||
QTreeView::item:selected, QTreeView::item:selected:hover {
|
||||
background-color: {{ color['prompts.selected.bg'] }};
|
||||
}
|
||||
"""
|
||||
update_geometry = pyqtSignal()
|
||||
|
||||
def __init__(self, win_id, parent=None):
|
||||
super().__init__(parent)
|
||||
self._layout = QVBoxLayout(self)
|
||||
self._layout.setContentsMargins(10, 10, 10, 10)
|
||||
self._win_id = win_id
|
||||
self._prompt = None
|
||||
|
||||
self.setObjectName('PromptContainer')
|
||||
self.setAttribute(Qt.WA_StyledBackground, True)
|
||||
style.set_register_stylesheet(self)
|
||||
|
||||
message.global_bridge.prompt_done.connect(self._on_prompt_done)
|
||||
prompt_queue.show_prompts.connect(self._on_show_prompts)
|
||||
message.global_bridge.mode_left.connect(self._on_global_mode_left)
|
||||
|
||||
def __repr__(self):
|
||||
return utils.get_repr(self, win_id=self._win_id)
|
||||
|
||||
@pyqtSlot(usertypes.Question)
|
||||
def _on_show_prompts(self, question):
|
||||
"""Show a prompt for the given question.
|
||||
|
||||
Args:
|
||||
question: A Question object or None.
|
||||
"""
|
||||
item = self._layout.takeAt(0)
|
||||
if item is not None:
|
||||
widget = item.widget()
|
||||
log.prompt.debug("Deleting old prompt {}".format(widget))
|
||||
widget.hide()
|
||||
widget.deleteLater()
|
||||
|
||||
if question is None:
|
||||
log.prompt.debug("No prompts left, hiding prompt container.")
|
||||
self._prompt = None
|
||||
self.hide()
|
||||
return
|
||||
|
||||
classes = {
|
||||
usertypes.PromptMode.yesno: YesNoPrompt,
|
||||
usertypes.PromptMode.text: LineEditPrompt,
|
||||
usertypes.PromptMode.user_pwd: AuthenticationPrompt,
|
||||
usertypes.PromptMode.download: DownloadFilenamePrompt,
|
||||
usertypes.PromptMode.alert: AlertPrompt,
|
||||
}
|
||||
klass = classes[question.mode]
|
||||
prompt = klass(question)
|
||||
|
||||
log.prompt.debug("Displaying prompt {}".format(prompt))
|
||||
self._prompt = prompt
|
||||
|
||||
if not question.interrupted:
|
||||
# If this question was interrupted, we already connected the signal
|
||||
question.aborted.connect(
|
||||
lambda: modeman.maybe_leave(self._win_id, prompt.KEY_MODE,
|
||||
'aborted'))
|
||||
modeman.enter(self._win_id, prompt.KEY_MODE, 'question asked')
|
||||
|
||||
self.setSizePolicy(prompt.sizePolicy())
|
||||
self._layout.addWidget(prompt)
|
||||
prompt.show()
|
||||
self.show()
|
||||
prompt.setFocus()
|
||||
self.update_geometry.emit()
|
||||
|
||||
@pyqtSlot(usertypes.KeyMode)
|
||||
def _on_prompt_done(self, key_mode):
|
||||
"""Leave the prompt mode in this window if a question was answered."""
|
||||
modeman.maybe_leave(self._win_id, key_mode, ':prompt-accept')
|
||||
|
||||
@pyqtSlot(usertypes.KeyMode)
|
||||
def _on_global_mode_left(self, mode):
|
||||
"""Leave prompt/yesno mode in this window if it was left elsewhere.
|
||||
|
||||
This ensures no matter where a prompt was answered, we leave the prompt
|
||||
mode and dispose of the prompt object in every window.
|
||||
"""
|
||||
if mode not in [usertypes.KeyMode.prompt, usertypes.KeyMode.yesno]:
|
||||
return
|
||||
modeman.maybe_leave(self._win_id, mode, 'left in other window')
|
||||
item = self._layout.takeAt(0)
|
||||
if item is not None:
|
||||
widget = item.widget()
|
||||
log.prompt.debug("Deleting prompt {}".format(widget))
|
||||
widget.hide()
|
||||
widget.deleteLater()
|
||||
|
||||
@cmdutils.register(instance='prompt-container', hide=True, scope='window',
|
||||
modes=[usertypes.KeyMode.prompt,
|
||||
usertypes.KeyMode.yesno])
|
||||
def prompt_accept(self, value=None):
|
||||
"""Accept the current prompt.
|
||||
|
||||
//
|
||||
|
||||
This executes the next action depending on the question mode, e.g. asks
|
||||
for the password or leaves the mode.
|
||||
|
||||
Args:
|
||||
value: If given, uses this value instead of the entered one.
|
||||
For boolean prompts, "yes"/"no" are accepted as value.
|
||||
"""
|
||||
question = self._prompt.question
|
||||
try:
|
||||
done = self._prompt.accept(value)
|
||||
except Error as e:
|
||||
raise cmdexc.CommandError(str(e))
|
||||
if done:
|
||||
message.global_bridge.prompt_done.emit(self._prompt.KEY_MODE)
|
||||
question.done()
|
||||
|
||||
@cmdutils.register(instance='prompt-container', hide=True, scope='window',
|
||||
modes=[usertypes.KeyMode.yesno],
|
||||
deprecated='Use :prompt-accept yes instead!')
|
||||
def prompt_yes(self):
|
||||
"""Answer yes to a yes/no prompt."""
|
||||
self.prompt_accept('yes')
|
||||
|
||||
@cmdutils.register(instance='prompt-container', hide=True, scope='window',
|
||||
modes=[usertypes.KeyMode.yesno],
|
||||
deprecated='Use :prompt-accept no instead!')
|
||||
def prompt_no(self):
|
||||
"""Answer no to a yes/no prompt."""
|
||||
self.prompt_accept('no')
|
||||
|
||||
@cmdutils.register(instance='prompt-container', hide=True, scope='window',
|
||||
modes=[usertypes.KeyMode.prompt], maxsplit=0)
|
||||
def prompt_open_download(self, cmdline: str=None):
|
||||
"""Immediately open a download.
|
||||
|
||||
If no specific command is given, this will use the system's default
|
||||
application to open the file.
|
||||
|
||||
Args:
|
||||
cmdline: The command which should be used to open the file. A `{}`
|
||||
is expanded to the temporary file name. If no `{}` is
|
||||
present, the filename is automatically appended to the
|
||||
cmdline.
|
||||
"""
|
||||
try:
|
||||
self._prompt.download_open(cmdline)
|
||||
except UnsupportedOperationError:
|
||||
pass
|
||||
|
||||
@cmdutils.register(instance='prompt-container', hide=True, scope='window',
|
||||
modes=[usertypes.KeyMode.prompt])
|
||||
@cmdutils.argument('which', choices=['next', 'prev'])
|
||||
def prompt_item_focus(self, which):
|
||||
"""Shift the focus of the prompt file completion menu to another item.
|
||||
|
||||
Args:
|
||||
which: 'next', 'prev'
|
||||
"""
|
||||
try:
|
||||
self._prompt.item_focus(which)
|
||||
except UnsupportedOperationError:
|
||||
pass
|
||||
|
||||
|
||||
class LineEdit(QLineEdit):
|
||||
|
||||
"""A line edit used in prompts."""
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self.setStyleSheet("""
|
||||
QLineEdit {
|
||||
border: 1px solid grey;
|
||||
background-color: transparent;
|
||||
}
|
||||
""")
|
||||
self.setAttribute(Qt.WA_MacShowFocusRect, False)
|
||||
|
||||
def keyPressEvent(self, e):
|
||||
"""Override keyPressEvent to paste primary selection on Shift + Ins."""
|
||||
if e.key() == Qt.Key_Insert and e.modifiers() == Qt.ShiftModifier:
|
||||
try:
|
||||
text = utils.get_clipboard(selection=True)
|
||||
except utils.ClipboardError: # pragma: no cover
|
||||
pass
|
||||
else:
|
||||
e.accept()
|
||||
self.insert(text)
|
||||
return
|
||||
super().keyPressEvent(e)
|
||||
|
||||
def __repr__(self):
|
||||
return utils.get_repr(self)
|
||||
|
||||
|
||||
class _BasePrompt(QWidget):
|
||||
|
||||
"""Base class for all prompts."""
|
||||
|
||||
KEY_MODE = usertypes.KeyMode.prompt
|
||||
|
||||
def __init__(self, question, parent=None):
|
||||
super().__init__(parent)
|
||||
self.question = question
|
||||
self._vbox = QVBoxLayout(self)
|
||||
self._vbox.setSpacing(15)
|
||||
self._key_grid = None
|
||||
|
||||
def __repr__(self):
|
||||
return utils.get_repr(self, question=self.question, constructor=True)
|
||||
|
||||
def _init_texts(self, question):
|
||||
assert question.title is not None, question
|
||||
title = '<font size="4"><b>{}</b></font>'.format(
|
||||
html.escape(question.title))
|
||||
title_label = QLabel(title, self)
|
||||
self._vbox.addWidget(title_label)
|
||||
if question.text is not None:
|
||||
# Not doing any HTML escaping here as the text can be formatted
|
||||
text_label = QLabel(question.text)
|
||||
self._vbox.addWidget(text_label)
|
||||
|
||||
def _init_key_label(self):
|
||||
assert self._key_grid is None, self._key_grid
|
||||
self._key_grid = QGridLayout()
|
||||
self._key_grid.setVerticalSpacing(0)
|
||||
|
||||
key_config = objreg.get('key-config')
|
||||
# The bindings are all in the 'prompt' mode, even for yesno prompts
|
||||
all_bindings = key_config.get_reverse_bindings_for('prompt')
|
||||
labels = []
|
||||
|
||||
for cmd, text in self._allowed_commands():
|
||||
bindings = all_bindings.get(cmd, [])
|
||||
if bindings:
|
||||
binding = None
|
||||
preferred = ['<enter>', '<escape>']
|
||||
for pref in preferred:
|
||||
if pref in bindings:
|
||||
binding = pref
|
||||
if binding is None:
|
||||
binding = bindings[0]
|
||||
key_label = QLabel('<b>{}</b>'.format(html.escape(binding)))
|
||||
text_label = QLabel(text)
|
||||
labels.append((key_label, text_label))
|
||||
|
||||
for i, (key_label, text_label) in enumerate(labels):
|
||||
self._key_grid.addWidget(key_label, i, 0)
|
||||
self._key_grid.addWidget(text_label, i, 1)
|
||||
|
||||
self._vbox.addLayout(self._key_grid)
|
||||
|
||||
def accept(self, value=None):
|
||||
raise NotImplementedError
|
||||
|
||||
def download_open(self, _cmdline):
|
||||
"""Open the download directly if this is a download prompt."""
|
||||
raise UnsupportedOperationError
|
||||
|
||||
def item_focus(self, _which):
|
||||
"""Switch to next file item if this is a filename prompt.."""
|
||||
raise UnsupportedOperationError
|
||||
|
||||
def _allowed_commands(self):
|
||||
"""Get the commands we could run as response to this message."""
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class LineEditPrompt(_BasePrompt):
|
||||
|
||||
"""A prompt for a single text value."""
|
||||
|
||||
def __init__(self, question, parent=None):
|
||||
super().__init__(question, parent)
|
||||
self._lineedit = LineEdit(self)
|
||||
self._init_texts(question)
|
||||
self._vbox.addWidget(self._lineedit)
|
||||
if question.default:
|
||||
self._lineedit.setText(question.default)
|
||||
self.setFocusProxy(self._lineedit)
|
||||
self._init_key_label()
|
||||
|
||||
def accept(self, value=None):
|
||||
text = value if value is not None else self._lineedit.text()
|
||||
self.question.answer = text
|
||||
return True
|
||||
|
||||
def _allowed_commands(self):
|
||||
return [('prompt-accept', 'Accept'), ('leave-mode', 'Abort')]
|
||||
|
||||
|
||||
class FilenamePrompt(_BasePrompt):
|
||||
|
||||
"""A prompt for a filename."""
|
||||
|
||||
def __init__(self, question, parent=None):
|
||||
super().__init__(question, parent)
|
||||
self._init_texts(question)
|
||||
self._init_fileview()
|
||||
self._set_fileview_root(question.default)
|
||||
|
||||
self._lineedit = LineEdit(self)
|
||||
if question.default:
|
||||
self._lineedit.setText(question.default)
|
||||
self._lineedit.textEdited.connect(self._set_fileview_root)
|
||||
self._vbox.addWidget(self._lineedit)
|
||||
|
||||
self.setFocusProxy(self._lineedit)
|
||||
self._init_key_label()
|
||||
self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred)
|
||||
|
||||
@pyqtSlot(str)
|
||||
def _set_fileview_root(self, path, *, tabbed=False):
|
||||
"""Set the root path for the file display."""
|
||||
separators = os.sep
|
||||
if os.altsep is not None:
|
||||
separators += os.altsep
|
||||
|
||||
dirname = os.path.dirname(path)
|
||||
|
||||
try:
|
||||
if not path:
|
||||
pass
|
||||
elif path in separators and os.path.isdir(path):
|
||||
# Input "/" -> don't strip anything
|
||||
pass
|
||||
elif path[-1] in separators and os.path.isdir(path):
|
||||
# Input like /foo/bar/ -> show /foo/bar/ contents
|
||||
path = path.rstrip(separators)
|
||||
elif os.path.isdir(dirname) and not tabbed:
|
||||
# Input like /foo/ba -> show /foo contents
|
||||
path = dirname
|
||||
else:
|
||||
return
|
||||
except OSError:
|
||||
log.prompt.exception("Failed to get directory information")
|
||||
return
|
||||
|
||||
root = self._file_model.setRootPath(path)
|
||||
self._file_view.setRootIndex(root)
|
||||
|
||||
@pyqtSlot(QModelIndex)
|
||||
def _insert_path(self, index, *, clicked=True):
|
||||
"""Handle an element selection.
|
||||
|
||||
Args:
|
||||
index: The QModelIndex of the selected element.
|
||||
clicked: Whether the element was clicked.
|
||||
"""
|
||||
path = os.path.normpath(self._file_model.filePath(index))
|
||||
if clicked:
|
||||
path += os.sep
|
||||
else:
|
||||
# On Windows, when we have C:\foo and tab over .., we get C:\
|
||||
path = path.rstrip(os.sep)
|
||||
|
||||
log.prompt.debug('Inserting path {}'.format(path))
|
||||
self._lineedit.setText(path)
|
||||
self._lineedit.setFocus()
|
||||
self._set_fileview_root(path, tabbed=True)
|
||||
if clicked:
|
||||
# Avoid having a ..-subtree highlighted
|
||||
self._file_view.setCurrentIndex(QModelIndex())
|
||||
|
||||
def _init_fileview(self):
|
||||
self._file_view = QTreeView(self)
|
||||
self._file_model = QFileSystemModel(self)
|
||||
self._file_view.setModel(self._file_model)
|
||||
self._file_view.clicked.connect(self._insert_path)
|
||||
self._vbox.addWidget(self._file_view)
|
||||
# Only show name
|
||||
self._file_view.setHeaderHidden(True)
|
||||
for col in range(1, 4):
|
||||
self._file_view.setColumnHidden(col, True)
|
||||
# Nothing selected initially
|
||||
self._file_view.setCurrentIndex(QModelIndex())
|
||||
# The model needs to be sorted so we get the correct first/last index
|
||||
self._file_model.directoryLoaded.connect(
|
||||
lambda: self._file_model.sort(0))
|
||||
|
||||
def accept(self, value=None):
|
||||
text = value if value is not None else self._lineedit.text()
|
||||
self.question.answer = text
|
||||
return True
|
||||
|
||||
def item_focus(self, which):
|
||||
# This duplicates some completion code, but I don't see a nicer way...
|
||||
assert which in ['prev', 'next'], which
|
||||
selmodel = self._file_view.selectionModel()
|
||||
|
||||
parent = self._file_view.rootIndex()
|
||||
first_index = self._file_model.index(0, 0, parent)
|
||||
row = self._file_model.rowCount(parent) - 1
|
||||
last_index = self._file_model.index(row, 0, parent)
|
||||
|
||||
if not first_index.isValid():
|
||||
# No entries
|
||||
return
|
||||
|
||||
assert last_index.isValid()
|
||||
|
||||
idx = selmodel.currentIndex()
|
||||
if not idx.isValid():
|
||||
# No item selected yet
|
||||
idx = last_index if which == 'prev' else first_index
|
||||
elif which == 'prev':
|
||||
idx = self._file_view.indexAbove(idx)
|
||||
else:
|
||||
assert which == 'next', which
|
||||
idx = self._file_view.indexBelow(idx)
|
||||
|
||||
# wrap around if we arrived at beginning/end
|
||||
if not idx.isValid():
|
||||
idx = last_index if which == 'prev' else first_index
|
||||
|
||||
selmodel.setCurrentIndex(
|
||||
idx, QItemSelectionModel.ClearAndSelect | QItemSelectionModel.Rows)
|
||||
self._insert_path(idx, clicked=False)
|
||||
|
||||
def _allowed_commands(self):
|
||||
return [('prompt-accept', 'Accept'), ('leave-mode', 'Abort')]
|
||||
|
||||
|
||||
class DownloadFilenamePrompt(FilenamePrompt):
|
||||
|
||||
"""A prompt for a filename for downloads."""
|
||||
|
||||
def __init__(self, question, parent=None):
|
||||
super().__init__(question, parent)
|
||||
self._file_model.setFilter(QDir.AllDirs | QDir.Drives | QDir.NoDot)
|
||||
|
||||
def accept(self, value=None):
|
||||
text = value if value is not None else self._lineedit.text()
|
||||
self.question.answer = usertypes.FileDownloadTarget(text)
|
||||
return True
|
||||
|
||||
def download_open(self, cmdline):
|
||||
self.question.answer = usertypes.OpenFileDownloadTarget(cmdline)
|
||||
self.question.done()
|
||||
message.global_bridge.prompt_done.emit(self.KEY_MODE)
|
||||
|
||||
def _allowed_commands(self):
|
||||
cmds = [
|
||||
('prompt-accept', 'Accept'),
|
||||
('leave-mode', 'Abort'),
|
||||
('prompt-open-download', "Open download"),
|
||||
]
|
||||
return cmds
|
||||
|
||||
|
||||
class AuthenticationPrompt(_BasePrompt):
|
||||
|
||||
"""A prompt for username/password."""
|
||||
|
||||
def __init__(self, question, parent=None):
|
||||
super().__init__(question, parent)
|
||||
self._init_texts(question)
|
||||
|
||||
user_label = QLabel("Username:", self)
|
||||
self._user_lineedit = LineEdit(self)
|
||||
|
||||
password_label = QLabel("Password:", self)
|
||||
self._password_lineedit = LineEdit(self)
|
||||
self._password_lineedit.setEchoMode(QLineEdit.Password)
|
||||
|
||||
grid = QGridLayout()
|
||||
grid.addWidget(user_label, 1, 0)
|
||||
grid.addWidget(self._user_lineedit, 1, 1)
|
||||
grid.addWidget(password_label, 2, 0)
|
||||
grid.addWidget(self._password_lineedit, 2, 1)
|
||||
self._vbox.addLayout(grid)
|
||||
self._init_key_label()
|
||||
|
||||
assert not question.default, question.default
|
||||
self.setFocusProxy(self._user_lineedit)
|
||||
|
||||
def accept(self, value=None):
|
||||
if value is not None:
|
||||
if ':' not in value:
|
||||
raise Error("Value needs to be in the format "
|
||||
"username:password, but {} was given".format(
|
||||
value))
|
||||
username, password = value.split(':', maxsplit=1)
|
||||
self.question.answer = AuthTuple(username, password)
|
||||
return True
|
||||
elif self._user_lineedit.hasFocus():
|
||||
# Earlier, tab was bound to :prompt-accept, so to still support
|
||||
# that we simply switch the focus when tab was pressed.
|
||||
self._password_lineedit.setFocus()
|
||||
return False
|
||||
else:
|
||||
self.question.answer = AuthTuple(self._user_lineedit.text(),
|
||||
self._password_lineedit.text())
|
||||
return True
|
||||
|
||||
def item_focus(self, which):
|
||||
"""Support switching between fields with tab."""
|
||||
assert which in ['prev', 'next'], which
|
||||
if which == 'next' and self._user_lineedit.hasFocus():
|
||||
self._password_lineedit.setFocus()
|
||||
elif which == 'prev' and self._password_lineedit.hasFocus():
|
||||
self._user_lineedit.setFocus()
|
||||
|
||||
def _allowed_commands(self):
|
||||
return [('prompt-accept', "Accept"),
|
||||
('leave-mode', "Abort")]
|
||||
|
||||
|
||||
class YesNoPrompt(_BasePrompt):
|
||||
|
||||
"""A prompt with yes/no answers."""
|
||||
|
||||
KEY_MODE = usertypes.KeyMode.yesno
|
||||
|
||||
def __init__(self, question, parent=None):
|
||||
super().__init__(question, parent)
|
||||
self._init_texts(question)
|
||||
self._init_key_label()
|
||||
|
||||
def accept(self, value=None):
|
||||
if value is None:
|
||||
if self.question.default is None:
|
||||
raise Error("No default value was set for this question!")
|
||||
self.question.answer = self.question.default
|
||||
elif value == 'yes':
|
||||
self.question.answer = True
|
||||
elif value == 'no':
|
||||
self.question.answer = False
|
||||
else:
|
||||
raise Error("Invalid value {} - expected yes/no!".format(value))
|
||||
return True
|
||||
|
||||
def _allowed_commands(self):
|
||||
cmds = [
|
||||
('prompt-accept yes', "Yes"),
|
||||
('prompt-accept no', "No"),
|
||||
]
|
||||
|
||||
if self.question.default is not None:
|
||||
assert self.question.default in [True, False]
|
||||
default = 'yes' if self.question.default else 'no'
|
||||
cmds.append(('prompt-accept', "Use default ({})".format(default)))
|
||||
|
||||
cmds.append(('leave-mode', "Abort"))
|
||||
return cmds
|
||||
|
||||
|
||||
class AlertPrompt(_BasePrompt):
|
||||
|
||||
"""A prompt without any answer possibility."""
|
||||
|
||||
def __init__(self, question, parent=None):
|
||||
super().__init__(question, parent)
|
||||
self._init_texts(question)
|
||||
self._init_key_label()
|
||||
|
||||
def accept(self, value=None):
|
||||
if value is not None:
|
||||
raise Error("No value is permitted with alert prompts!")
|
||||
# Simply mark prompt as done without setting self.question.answer
|
||||
return True
|
||||
|
||||
def _allowed_commands(self):
|
||||
return [('prompt-accept', "Hide")]
|
||||
|
||||
|
||||
def init():
|
||||
"""Initialize global prompt objects."""
|
||||
global prompt_queue
|
||||
prompt_queue = PromptQueue()
|
||||
objreg.register('prompt-queue', prompt_queue) # for commands
|
||||
message.global_bridge.ask_question.connect(
|
||||
prompt_queue.ask_question, Qt.DirectConnection)
|
@ -25,8 +25,7 @@ from PyQt5.QtWidgets import QWidget, QHBoxLayout, QStackedLayout, QSizePolicy
|
||||
from qutebrowser.config import config, style
|
||||
from qutebrowser.utils import usertypes, log, objreg, utils
|
||||
from qutebrowser.mainwindow.statusbar import (command, progress, keystring,
|
||||
percentage, url, prompt,
|
||||
tabindex)
|
||||
percentage, url, tabindex)
|
||||
from qutebrowser.mainwindow.statusbar import text as textwidget
|
||||
|
||||
|
||||
@ -113,8 +112,8 @@ class StatusBar(QWidget):
|
||||
QWidget#StatusBar[prompt_active="true"],
|
||||
QWidget#StatusBar[prompt_active="true"] QLabel,
|
||||
QWidget#StatusBar[prompt_active="true"] QLineEdit {
|
||||
color: {{ color['statusbar.fg.prompt'] }};
|
||||
background-color: {{ color['statusbar.bg.prompt'] }};
|
||||
color: {{ color['prompts.fg'] }};
|
||||
background-color: {{ color['prompts.bg'] }};
|
||||
}
|
||||
|
||||
QWidget#StatusBar[insert_active="true"],
|
||||
@ -162,16 +161,9 @@ class StatusBar(QWidget):
|
||||
self.txt = textwidget.Text()
|
||||
self._stack.addWidget(self.txt)
|
||||
|
||||
self.prompt = prompt.Prompt(win_id)
|
||||
self._stack.addWidget(self.prompt)
|
||||
|
||||
self.cmd.show_cmd.connect(self._show_cmd_widget)
|
||||
self.cmd.hide_cmd.connect(self._hide_cmd_widget)
|
||||
self._hide_cmd_widget()
|
||||
prompter = objreg.get('prompter', scope='window', window=self._win_id)
|
||||
prompter.show_prompt.connect(self._show_prompt_widget)
|
||||
prompter.hide_prompt.connect(self._hide_prompt_widget)
|
||||
self._hide_prompt_widget()
|
||||
|
||||
self.keystring = keystring.KeyString()
|
||||
self._hbox.addWidget(self.keystring)
|
||||
@ -216,16 +208,6 @@ class StatusBar(QWidget):
|
||||
"""Getter for self.prompt_active, so it can be used as Qt property."""
|
||||
return self._prompt_active
|
||||
|
||||
def _set_prompt_active(self, val):
|
||||
"""Setter for self.prompt_active.
|
||||
|
||||
Re-set the stylesheet after setting the value, so everything gets
|
||||
updated by Qt properly.
|
||||
"""
|
||||
log.statusbar.debug("Setting prompt_active to {}".format(val))
|
||||
self._prompt_active = val
|
||||
self.setStyleSheet(style.get_stylesheet(self.STYLESHEET))
|
||||
|
||||
@pyqtProperty(bool)
|
||||
def command_active(self):
|
||||
"""Getter for self.command_active, so it can be used as Qt property."""
|
||||
@ -253,6 +235,9 @@ class StatusBar(QWidget):
|
||||
if mode == usertypes.KeyMode.command:
|
||||
log.statusbar.debug("Setting command_active to {}".format(val))
|
||||
self._command_active = val
|
||||
elif mode in [usertypes.KeyMode.prompt, usertypes.KeyMode.yesno]:
|
||||
log.statusbar.debug("Setting prompt_active to {}".format(val))
|
||||
self._prompt_active = val
|
||||
elif mode == usertypes.KeyMode.caret:
|
||||
tab = objreg.get('tabbed-browser', scope='window',
|
||||
window=self._win_id).currentWidget()
|
||||
@ -285,21 +270,6 @@ class StatusBar(QWidget):
|
||||
self._stack.setCurrentWidget(self.txt)
|
||||
self.maybe_hide()
|
||||
|
||||
def _show_prompt_widget(self):
|
||||
"""Show prompt widget instead of temporary text."""
|
||||
if self._stack.currentWidget() is self.prompt:
|
||||
return
|
||||
self._set_prompt_active(True)
|
||||
self._stack.setCurrentWidget(self.prompt)
|
||||
self.show()
|
||||
|
||||
def _hide_prompt_widget(self):
|
||||
"""Show temporary text instead of prompt widget."""
|
||||
self._set_prompt_active(False)
|
||||
log.statusbar.debug("Hiding prompt widget")
|
||||
self._stack.setCurrentWidget(self.txt)
|
||||
self.maybe_hide()
|
||||
|
||||
@pyqtSlot(str)
|
||||
def set_text(self, val):
|
||||
"""Set a normal (persistent) text in the status bar."""
|
||||
@ -314,7 +284,9 @@ class StatusBar(QWidget):
|
||||
self._set_mode_text(mode.name)
|
||||
if mode in [usertypes.KeyMode.insert,
|
||||
usertypes.KeyMode.command,
|
||||
usertypes.KeyMode.caret]:
|
||||
usertypes.KeyMode.caret,
|
||||
usertypes.KeyMode.prompt,
|
||||
usertypes.KeyMode.yesno]:
|
||||
self.set_mode_active(mode, True)
|
||||
|
||||
@pyqtSlot(usertypes.KeyMode, usertypes.KeyMode)
|
||||
@ -329,7 +301,9 @@ class StatusBar(QWidget):
|
||||
self.txt.set_text(self.txt.Text.normal, '')
|
||||
if old_mode in [usertypes.KeyMode.insert,
|
||||
usertypes.KeyMode.command,
|
||||
usertypes.KeyMode.caret]:
|
||||
usertypes.KeyMode.caret,
|
||||
usertypes.KeyMode.prompt,
|
||||
usertypes.KeyMode.yesno]:
|
||||
self.set_mode_active(old_mode, False)
|
||||
|
||||
def resizeEvent(self, e):
|
||||
|
@ -77,7 +77,6 @@ class Command(misc.MinimalLineEditMixin, misc.CommandLineEdit):
|
||||
else:
|
||||
return ''
|
||||
|
||||
@pyqtSlot(str)
|
||||
def set_cmd_text(self, text):
|
||||
"""Preset the statusbar to some text.
|
||||
|
||||
|
@ -36,6 +36,7 @@ class Progress(QProgressBar):
|
||||
border-radius: 0px;
|
||||
border: 2px solid transparent;
|
||||
background-color: transparent;
|
||||
font: {{ font['statusbar'] }};
|
||||
}
|
||||
|
||||
QProgressBar::chunk {
|
||||
|
@ -1,84 +0,0 @@
|
||||
# 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/>.
|
||||
|
||||
"""Prompt shown in the statusbar."""
|
||||
|
||||
import functools
|
||||
|
||||
from PyQt5.QtCore import QSize
|
||||
from PyQt5.QtWidgets import QHBoxLayout, QWidget, QLineEdit, QSizePolicy
|
||||
|
||||
from qutebrowser.mainwindow.statusbar import textbase, prompter
|
||||
from qutebrowser.utils import objreg, utils
|
||||
from qutebrowser.misc import miscwidgets as misc
|
||||
|
||||
|
||||
class PromptLineEdit(misc.MinimalLineEditMixin, QLineEdit):
|
||||
|
||||
"""QLineEdit with a minimal stylesheet."""
|
||||
|
||||
def __init__(self, parent=None):
|
||||
QLineEdit.__init__(self, parent)
|
||||
misc.MinimalLineEditMixin.__init__(self)
|
||||
self.textChanged.connect(self.updateGeometry)
|
||||
|
||||
def sizeHint(self):
|
||||
"""Dynamically calculate the needed size."""
|
||||
height = super().sizeHint().height()
|
||||
text = self.text()
|
||||
if not text:
|
||||
text = 'x'
|
||||
width = self.fontMetrics().width(text)
|
||||
return QSize(width, height)
|
||||
|
||||
|
||||
class Prompt(QWidget):
|
||||
|
||||
"""The prompt widget shown in the statusbar.
|
||||
|
||||
Attributes:
|
||||
txt: The TextBase instance (QLabel) used to display the prompt text.
|
||||
lineedit: The MinimalLineEdit instance (QLineEdit) used for the input.
|
||||
_hbox: The QHBoxLayout used to display the text and prompt.
|
||||
"""
|
||||
|
||||
def __init__(self, win_id, parent=None):
|
||||
super().__init__(parent)
|
||||
objreg.register('prompt', self, scope='window', window=win_id)
|
||||
self._hbox = QHBoxLayout(self)
|
||||
self._hbox.setContentsMargins(0, 0, 0, 0)
|
||||
self._hbox.setSpacing(5)
|
||||
|
||||
self.txt = textbase.TextBase()
|
||||
self._hbox.addWidget(self.txt)
|
||||
|
||||
self.lineedit = PromptLineEdit()
|
||||
self.lineedit.setSizePolicy(QSizePolicy.MinimumExpanding,
|
||||
QSizePolicy.Fixed)
|
||||
self._hbox.addWidget(self.lineedit)
|
||||
|
||||
prompter_obj = prompter.Prompter(win_id)
|
||||
objreg.register('prompter', prompter_obj, scope='window',
|
||||
window=win_id)
|
||||
self.destroyed.connect(
|
||||
functools.partial(objreg.delete, 'prompter', scope='window',
|
||||
window=win_id))
|
||||
|
||||
def __repr__(self):
|
||||
return utils.get_repr(self)
|
@ -1,411 +0,0 @@
|
||||
# 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/>.
|
||||
|
||||
"""Manager for questions to be shown in the statusbar."""
|
||||
|
||||
import sip
|
||||
import collections
|
||||
|
||||
from PyQt5.QtCore import pyqtSlot, pyqtSignal, QTimer, QObject
|
||||
from PyQt5.QtWidgets import QLineEdit
|
||||
|
||||
from qutebrowser.keyinput import modeman
|
||||
from qutebrowser.commands import cmdutils, cmdexc
|
||||
from qutebrowser.utils import usertypes, log, qtutils, objreg, utils
|
||||
|
||||
|
||||
PromptContext = collections.namedtuple('PromptContext',
|
||||
['question', 'text', 'input_text',
|
||||
'echo_mode', 'input_visible'])
|
||||
AuthTuple = collections.namedtuple('AuthTuple', ['user', 'password'])
|
||||
|
||||
|
||||
class Prompter(QObject):
|
||||
|
||||
"""Manager for questions to be shown in the statusbar.
|
||||
|
||||
The way in which multiple questions are handled deserves some explanation.
|
||||
|
||||
If a question is blocking, we *need* to ask it immediately, and can't wait
|
||||
for previous questions to finish. We could theoretically ask a blocking
|
||||
question inside of another blocking one, so in ask_question we simply save
|
||||
the current prompt state on the stack, let the user answer the *most
|
||||
recent* question, and then restore the previous state.
|
||||
|
||||
With a non-blocking question, things are a bit easier. We simply add it to
|
||||
self._queue if we're still busy handling another question, since it can be
|
||||
answered at any time.
|
||||
|
||||
In either case, as soon as we finished handling a question, we call
|
||||
_pop_later() which schedules a _pop to ask the next question in _queue. We
|
||||
schedule it rather than doing it immediately because then the order of how
|
||||
things happen is clear, e.g. on_mode_left can't happen after we already set
|
||||
up the *new* question.
|
||||
|
||||
Class Attributes:
|
||||
KEY_MODES: A mapping of PromptModes to KeyModes.
|
||||
|
||||
Attributes:
|
||||
_shutting_down: Whether we're currently shutting down the prompter and
|
||||
should ignore future questions to avoid segfaults.
|
||||
_question: A Question object with the question to be asked to the user.
|
||||
_loops: A list of local EventLoops to spin in when blocking.
|
||||
_queue: A deque of waiting questions.
|
||||
_busy: If we're currently busy with asking a question.
|
||||
_win_id: The window ID this object is associated with.
|
||||
|
||||
Signals:
|
||||
show_prompt: Emitted when the prompt widget should be shown.
|
||||
hide_prompt: Emitted when the prompt widget should be hidden.
|
||||
"""
|
||||
|
||||
KEY_MODES = {
|
||||
usertypes.PromptMode.yesno: usertypes.KeyMode.yesno,
|
||||
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()
|
||||
hide_prompt = pyqtSignal()
|
||||
|
||||
def __init__(self, win_id, parent=None):
|
||||
super().__init__(parent)
|
||||
self._shutting_down = False
|
||||
self._question = None
|
||||
self._loops = []
|
||||
self._queue = collections.deque()
|
||||
self._busy = False
|
||||
self._win_id = win_id
|
||||
|
||||
def __repr__(self):
|
||||
return utils.get_repr(self, loops=len(self._loops),
|
||||
question=self._question, queue=len(self._queue),
|
||||
busy=self._busy)
|
||||
|
||||
def _pop_later(self):
|
||||
"""Helper to call self._pop as soon as everything else is done."""
|
||||
QTimer.singleShot(0, self._pop)
|
||||
|
||||
def _pop(self):
|
||||
"""Pop a question from the queue and ask it, if there are any."""
|
||||
log.statusbar.debug("Popping from queue {}".format(self._queue))
|
||||
if self._queue:
|
||||
question = self._queue.popleft()
|
||||
if not sip.isdeleted(question):
|
||||
# the question could already be deleted, e.g. by a cancelled
|
||||
# download. See
|
||||
# https://github.com/The-Compiler/qutebrowser/issues/415
|
||||
self.ask_question(question, blocking=False)
|
||||
|
||||
def _get_ctx(self):
|
||||
"""Get a PromptContext based on the current state."""
|
||||
if not self._busy:
|
||||
return None
|
||||
prompt = objreg.get('prompt', scope='window', window=self._win_id)
|
||||
ctx = PromptContext(question=self._question,
|
||||
text=prompt.txt.text(),
|
||||
input_text=prompt.lineedit.text(),
|
||||
echo_mode=prompt.lineedit.echoMode(),
|
||||
input_visible=prompt.lineedit.isVisible())
|
||||
return ctx
|
||||
|
||||
def _restore_ctx(self, ctx):
|
||||
"""Restore state from a PromptContext.
|
||||
|
||||
Args:
|
||||
ctx: A PromptContext previously saved by _get_ctx, or None.
|
||||
|
||||
Return: True if a context was restored, False otherwise.
|
||||
"""
|
||||
log.statusbar.debug("Restoring context {}".format(ctx))
|
||||
if ctx is None:
|
||||
self.hide_prompt.emit()
|
||||
self._busy = False
|
||||
return False
|
||||
self._question = ctx.question
|
||||
prompt = objreg.get('prompt', scope='window', window=self._win_id)
|
||||
prompt.txt.setText(ctx.text)
|
||||
prompt.lineedit.setText(ctx.input_text)
|
||||
prompt.lineedit.setEchoMode(ctx.echo_mode)
|
||||
prompt.lineedit.setVisible(ctx.input_visible)
|
||||
self.show_prompt.emit()
|
||||
mode = self.KEY_MODES[ctx.question.mode]
|
||||
ctx.question.aborted.connect(
|
||||
lambda: modeman.maybe_leave(self._win_id, mode, 'aborted'))
|
||||
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)
|
||||
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()
|
||||
self.show_prompt.emit()
|
||||
self._busy = True
|
||||
|
||||
def shutdown(self):
|
||||
"""Cancel all blocking questions.
|
||||
|
||||
Quits and removes all running event loops.
|
||||
|
||||
Return:
|
||||
True if loops needed to be aborted,
|
||||
False otherwise.
|
||||
"""
|
||||
self._shutting_down = True
|
||||
if self._loops:
|
||||
for loop in self._loops:
|
||||
loop.quit()
|
||||
loop.deleteLater()
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
@pyqtSlot(usertypes.KeyMode)
|
||||
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]:
|
||||
prompt.txt.setText('')
|
||||
prompt.lineedit.clear()
|
||||
prompt.lineedit.setEchoMode(QLineEdit.Normal)
|
||||
self.hide_prompt.emit()
|
||||
self._busy = False
|
||||
if self._question.answer is None and not self._question.is_aborted:
|
||||
self._question.cancel()
|
||||
|
||||
@cmdutils.register(instance='prompter', hide=True, scope='window',
|
||||
modes=[usertypes.KeyMode.prompt,
|
||||
usertypes.KeyMode.yesno])
|
||||
def prompt_accept(self, value=None):
|
||||
"""Accept the current prompt.
|
||||
|
||||
//
|
||||
|
||||
This executes the next action depending on the question mode, e.g. asks
|
||||
for the password or leaves the mode.
|
||||
|
||||
Args:
|
||||
value: If given, uses this value instead of the entered one.
|
||||
For boolean prompts, "yes"/"no" are accepted as value.
|
||||
"""
|
||||
prompt = objreg.get('prompt', scope='window', window=self._win_id)
|
||||
text = value if value is not None else prompt.lineedit.text()
|
||||
|
||||
if (self._question.mode == usertypes.PromptMode.user_pwd and
|
||||
self._question.user is None):
|
||||
# User just entered a username
|
||||
self._question.user = text
|
||||
prompt.txt.setText("Password:")
|
||||
prompt.lineedit.clear()
|
||||
prompt.lineedit.setEchoMode(QLineEdit.Password)
|
||||
elif self._question.mode == usertypes.PromptMode.user_pwd:
|
||||
# User just entered a password
|
||||
self._question.answer = AuthTuple(self._question.user, text)
|
||||
modeman.maybe_leave(self._win_id, usertypes.KeyMode.prompt,
|
||||
'prompt accept')
|
||||
self._question.done()
|
||||
elif self._question.mode == usertypes.PromptMode.text:
|
||||
# User just entered text.
|
||||
self._question.answer = text
|
||||
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(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.
|
||||
if value is None:
|
||||
self._question.answer = self._question.default
|
||||
elif value == 'yes':
|
||||
self._question.answer = True
|
||||
elif value == 'no':
|
||||
self._question.answer = False
|
||||
else:
|
||||
raise cmdexc.CommandError("Invalid value {} - expected "
|
||||
"yes/no!".format(value))
|
||||
modeman.maybe_leave(self._win_id, usertypes.KeyMode.yesno,
|
||||
'yesno accept')
|
||||
self._question.done()
|
||||
elif self._question.mode == usertypes.PromptMode.alert:
|
||||
if value is not None:
|
||||
raise cmdexc.CommandError("No value is permitted with alert "
|
||||
"prompts!")
|
||||
# User acknowledged an alert
|
||||
self._question.answer = None
|
||||
modeman.maybe_leave(self._win_id, usertypes.KeyMode.prompt,
|
||||
'alert accept')
|
||||
self._question.done()
|
||||
else:
|
||||
raise ValueError("Invalid question mode!")
|
||||
|
||||
@cmdutils.register(instance='prompter', hide=True, scope='window',
|
||||
modes=[usertypes.KeyMode.yesno],
|
||||
deprecated='Use :prompt-accept yes instead!')
|
||||
def prompt_yes(self):
|
||||
"""Answer yes to a yes/no prompt."""
|
||||
if self._question.mode != usertypes.PromptMode.yesno:
|
||||
# We just ignore this if we don't have a yes/no question.
|
||||
return
|
||||
self._question.answer = True
|
||||
modeman.maybe_leave(self._win_id, usertypes.KeyMode.yesno,
|
||||
'yesno accept')
|
||||
self._question.done()
|
||||
|
||||
@cmdutils.register(instance='prompter', hide=True, scope='window',
|
||||
modes=[usertypes.KeyMode.yesno],
|
||||
deprecated='Use :prompt-accept no instead!')
|
||||
def prompt_no(self):
|
||||
"""Answer no to a yes/no prompt."""
|
||||
if self._question.mode != usertypes.PromptMode.yesno:
|
||||
# We just ignore this if we don't have a yes/no question.
|
||||
return
|
||||
self._question.answer = False
|
||||
modeman.maybe_leave(self._win_id, usertypes.KeyMode.yesno,
|
||||
'prompt accept')
|
||||
self._question.done()
|
||||
|
||||
@cmdutils.register(instance='prompter', hide=True, scope='window',
|
||||
modes=[usertypes.KeyMode.prompt], maxsplit=0)
|
||||
def prompt_open_download(self, cmdline: str=None):
|
||||
"""Immediately open a download.
|
||||
|
||||
If no specific command is given, this will use the system's default
|
||||
application to open the file.
|
||||
|
||||
Args:
|
||||
cmdline: The command which should be used to open the file. A `{}`
|
||||
is expanded to the temporary file name. If no `{}` is
|
||||
present, the filename is automatically appended to the
|
||||
cmdline.
|
||||
"""
|
||||
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(cmdline)
|
||||
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.
|
||||
|
||||
Args:
|
||||
question: The Question object to ask.
|
||||
blocking: If True, this function blocks and returns the result.
|
||||
|
||||
Return:
|
||||
The answer of the user when blocking=True.
|
||||
None if blocking=False.
|
||||
"""
|
||||
log.statusbar.debug("Asking question {}, blocking {}, loops {}, queue "
|
||||
"{}".format(question, blocking, self._loops,
|
||||
self._queue))
|
||||
|
||||
if self._shutting_down:
|
||||
# If we're currently shutting down we have to ignore this question
|
||||
# to avoid segfaults - see
|
||||
# https://github.com/The-Compiler/qutebrowser/issues/95
|
||||
log.statusbar.debug("Ignoring question because we're shutting "
|
||||
"down.")
|
||||
question.abort()
|
||||
return None
|
||||
|
||||
if self._busy and not blocking:
|
||||
# We got an async question, but we're already busy with one, so we
|
||||
# just queue it up for later.
|
||||
log.statusbar.debug("Adding {} to queue.".format(question))
|
||||
self._queue.append(question)
|
||||
return
|
||||
|
||||
if blocking:
|
||||
# If we're blocking we save the old state on the stack, so we can
|
||||
# restore it after exec, if exec gets called multiple times.
|
||||
context = self._get_ctx()
|
||||
|
||||
self._question = question
|
||||
self._display_question()
|
||||
mode = self.KEY_MODES[self._question.mode]
|
||||
question.aborted.connect(
|
||||
lambda: modeman.maybe_leave(self._win_id, mode, 'aborted'))
|
||||
modeman.enter(self._win_id, mode, 'question asked')
|
||||
if blocking:
|
||||
loop = qtutils.EventLoop()
|
||||
self._loops.append(loop)
|
||||
loop.destroyed.connect(lambda: self._loops.remove(loop))
|
||||
question.completed.connect(loop.quit)
|
||||
question.completed.connect(loop.deleteLater)
|
||||
loop.exec_()
|
||||
if not self._restore_ctx(context):
|
||||
# Nothing left to restore, so we can go back to popping async
|
||||
# questions.
|
||||
if self._queue:
|
||||
self._pop_later()
|
||||
return self._question.answer
|
||||
else:
|
||||
question.completed.connect(self._pop_later)
|
@ -558,6 +558,11 @@ class TabbedBrowser(tabwidget.TabWidget):
|
||||
# closing the last tab (before quitting) or shutting down
|
||||
return
|
||||
tab = self.widget(idx)
|
||||
if tab is None:
|
||||
log.webview.debug("on_current_changed got called with invalid "
|
||||
"index {}".format(idx))
|
||||
return
|
||||
|
||||
log.modes.debug("Current tab changed, focusing {!r}".format(tab))
|
||||
tab.setFocus()
|
||||
for mode in [usertypes.KeyMode.hint, usertypes.KeyMode.insert,
|
||||
|
@ -381,7 +381,7 @@ class _CrashDialog(QDialog):
|
||||
lines = ['The report has been sent successfully. Thanks!']
|
||||
lines.append("There was an error while getting the newest version: "
|
||||
"{}. Please check for a new version on "
|
||||
"<a href=http://www.qutebrowser.org/>qutebrowser.org</a> "
|
||||
"<a href=https://www.qutebrowser.org/>qutebrowser.org</a> "
|
||||
"by yourself.".format(msg))
|
||||
text = '<br/><br/>'.join(lines)
|
||||
self.finish()
|
||||
|
@ -362,6 +362,9 @@ class IPCServer(QObject):
|
||||
@pyqtSlot()
|
||||
def on_timeout(self):
|
||||
"""Cancel the current connection if it was idle for too long."""
|
||||
if self._socket is None: # pragma: no cover
|
||||
log.ipc.error("on_timeout got called with None socket!")
|
||||
return
|
||||
log.ipc.error("IPC connection timed out "
|
||||
"(socket 0x{:x}).".format(id(self._socket)))
|
||||
self._socket.disconnectFromServer()
|
||||
|
@ -45,6 +45,19 @@ class KeyHintView(QLabel):
|
||||
update_geometry: Emitted when this widget should be resized/positioned.
|
||||
"""
|
||||
|
||||
STYLESHEET = """
|
||||
QLabel {
|
||||
font: {{ font['keyhint'] }};
|
||||
color: {{ color['keyhint.fg'] }};
|
||||
background-color: {{ color['keyhint.bg'] }};
|
||||
padding: 6px;
|
||||
{% if config.get('ui', 'status-position') == 'top' %}
|
||||
border-bottom-right-radius: 6px;
|
||||
{% else %}
|
||||
border-top-right-radius: 6px;
|
||||
{% endif %}
|
||||
}
|
||||
"""
|
||||
update_geometry = pyqtSignal()
|
||||
|
||||
def __init__(self, win_id, parent=None):
|
||||
@ -56,8 +69,7 @@ class KeyHintView(QLabel):
|
||||
self._show_timer = usertypes.Timer(self, 'keyhint_show')
|
||||
self._show_timer.setInterval(500)
|
||||
self._show_timer.timeout.connect(self.show)
|
||||
style.set_register_stylesheet(self,
|
||||
generator=self._generate_stylesheet)
|
||||
style.set_register_stylesheet(self)
|
||||
|
||||
def __repr__(self):
|
||||
return utils.get_repr(self, win_id=self._win_id)
|
||||
@ -67,22 +79,6 @@ class KeyHintView(QLabel):
|
||||
self.update_geometry.emit()
|
||||
super().showEvent(e)
|
||||
|
||||
def _generate_stylesheet(self):
|
||||
"""Generate a stylesheet with the right edge rounded."""
|
||||
stylesheet = """
|
||||
QLabel {
|
||||
font: {{ font['keyhint'] }};
|
||||
color: {{ color['keyhint.fg'] }};
|
||||
background-color: {{ color['keyhint.bg'] }};
|
||||
padding: 6px;
|
||||
border-EDGE-radius: 6px;
|
||||
}
|
||||
"""
|
||||
if config.get('ui', 'status-position') == 'top':
|
||||
return stylesheet.replace('EDGE', 'bottom-right')
|
||||
else:
|
||||
return stylesheet.replace('EDGE', 'top-right')
|
||||
|
||||
@pyqtSlot(str)
|
||||
def update_keyhint(self, modename, prefix):
|
||||
"""Show hints for the given prefix (or hide if prefix is empty).
|
||||
|
@ -92,6 +92,12 @@ class BaseLineParser(QObject):
|
||||
|
||||
Args:
|
||||
mode: The mode to use ('a'/'r'/'w')
|
||||
|
||||
Raises:
|
||||
IOError: if the file is already open
|
||||
|
||||
Yields:
|
||||
a file object for the config file
|
||||
"""
|
||||
assert self._configfile is not None
|
||||
if self._opened:
|
||||
|
@ -20,7 +20,8 @@
|
||||
"""Misc. utility commands exposed to the user."""
|
||||
|
||||
import functools
|
||||
import types
|
||||
import os
|
||||
import signal
|
||||
import traceback
|
||||
|
||||
try:
|
||||
@ -142,10 +143,7 @@ def debug_crash(typ='exception'):
|
||||
typ: either 'exception' or 'segfault'.
|
||||
"""
|
||||
if typ == 'segfault':
|
||||
# From python's Lib/test/crashers/bogus_code_obj.py
|
||||
co = types.CodeType(0, 0, 0, 0, 0, b'\x04\x71\x00\x00', (), (), (),
|
||||
'', '', 1, b'')
|
||||
exec(co)
|
||||
os.kill(os.getpid(), signal.SIGSEGV)
|
||||
raise Exception("Segfault failed (wat.)")
|
||||
else:
|
||||
raise Exception("Forced crash")
|
||||
@ -173,12 +171,15 @@ def debug_console():
|
||||
try:
|
||||
con_widget = objreg.get('debug-console')
|
||||
except KeyError:
|
||||
log.misc.debug('initializing debug console')
|
||||
con_widget = consolewidget.ConsoleWidget()
|
||||
objreg.register('debug-console', con_widget)
|
||||
|
||||
if con_widget.isVisible():
|
||||
log.misc.debug('hiding debug console')
|
||||
con_widget.hide()
|
||||
else:
|
||||
log.misc.debug('showing debug console')
|
||||
con_widget.show()
|
||||
|
||||
|
||||
|
@ -94,7 +94,7 @@ LOGGER_NAMES = [
|
||||
'commands', 'signals', 'downloads',
|
||||
'js', 'qt', 'rfc6266', 'ipc', 'shlexer',
|
||||
'save', 'message', 'config', 'sessions',
|
||||
'webelem'
|
||||
'webelem', 'prompt'
|
||||
]
|
||||
|
||||
|
||||
@ -139,6 +139,7 @@ message = logging.getLogger('message')
|
||||
config = logging.getLogger('config')
|
||||
sessions = logging.getLogger('sessions')
|
||||
webelem = logging.getLogger('webelem')
|
||||
prompt = logging.getLogger('prompt')
|
||||
|
||||
|
||||
ram_handler = None
|
||||
@ -406,6 +407,10 @@ def qt_message_handler(msg_type, context, msg):
|
||||
"Chromium-based browser to ",
|
||||
# https://github.com/The-Compiler/qutebrowser/issues/1287
|
||||
"QXcbClipboard: SelectionRequest too old",
|
||||
# https://github.com/The-Compiler/qutebrowser/issues/2071
|
||||
'QXcbWindow: Unhandled client message: ""',
|
||||
# No idea where this comes from...
|
||||
"QObject::disconnect: Unexpected null parameter",
|
||||
]
|
||||
if sys.platform == 'darwin':
|
||||
suppressed_msgs += [
|
||||
|
@ -26,7 +26,7 @@ import traceback
|
||||
|
||||
from PyQt5.QtCore import pyqtSignal, QObject
|
||||
|
||||
from qutebrowser.utils import usertypes, log, objreg, utils
|
||||
from qutebrowser.utils import usertypes, log, utils
|
||||
|
||||
|
||||
def _log_stack(typ, stack):
|
||||
@ -76,70 +76,83 @@ def info(message):
|
||||
global_bridge.show_message.emit(usertypes.MessageLevel.info, message)
|
||||
|
||||
|
||||
def ask(win_id, message, mode, default=None):
|
||||
def _build_question(title, text=None, *, mode, default=None, abort_on=()):
|
||||
"""Common function for ask/ask_async."""
|
||||
if not isinstance(mode, usertypes.PromptMode):
|
||||
raise TypeError("Mode {} is no PromptMode member!".format(mode))
|
||||
question = usertypes.Question()
|
||||
question.title = title
|
||||
question.text = text
|
||||
question.mode = mode
|
||||
question.default = default
|
||||
for sig in abort_on:
|
||||
sig.connect(question.abort)
|
||||
return question
|
||||
|
||||
|
||||
def ask(*args, **kwargs):
|
||||
"""Ask a modular question in the statusbar (blocking).
|
||||
|
||||
Args:
|
||||
win_id: The ID of the window which is calling this function.
|
||||
message: The message to display to the user.
|
||||
mode: A PromptMode.
|
||||
default: The default value to display.
|
||||
text: Additional text to show
|
||||
abort_on: A list of signals which abort the question if emitted.
|
||||
|
||||
Return:
|
||||
The answer the user gave or None if the prompt was cancelled.
|
||||
"""
|
||||
q = usertypes.Question()
|
||||
q.text = message
|
||||
q.mode = mode
|
||||
q.default = default
|
||||
bridge = objreg.get('message-bridge', scope='window', window=win_id)
|
||||
bridge.ask(q, blocking=True)
|
||||
q.deleteLater()
|
||||
return q.answer
|
||||
question = _build_question(*args, **kwargs) # pylint: disable=missing-kwoa
|
||||
global_bridge.ask(question, blocking=True)
|
||||
answer = question.answer
|
||||
question.deleteLater()
|
||||
return answer
|
||||
|
||||
|
||||
def ask_async(win_id, message, mode, handler, default=None):
|
||||
def ask_async(title, mode, handler, **kwargs):
|
||||
"""Ask an async question in the statusbar.
|
||||
|
||||
Args:
|
||||
win_id: The ID of the window which is calling this function.
|
||||
message: The message to display to the user.
|
||||
mode: A PromptMode.
|
||||
handler: The function to get called with the answer as argument.
|
||||
default: The default value to display.
|
||||
text: Additional text to show.
|
||||
"""
|
||||
if not isinstance(mode, usertypes.PromptMode):
|
||||
raise TypeError("Mode {} is no PromptMode member!".format(mode))
|
||||
q = usertypes.Question()
|
||||
q.text = message
|
||||
q.mode = mode
|
||||
q.default = default
|
||||
q.answered.connect(handler)
|
||||
q.completed.connect(q.deleteLater)
|
||||
bridge = objreg.get('message-bridge', scope='window', window=win_id)
|
||||
bridge.ask(q, blocking=False)
|
||||
question = _build_question(title, mode=mode, **kwargs)
|
||||
question.answered.connect(handler)
|
||||
question.completed.connect(question.deleteLater)
|
||||
global_bridge.ask(question, blocking=False)
|
||||
|
||||
|
||||
def confirm_async(win_id, message, yes_action, no_action=None, default=None):
|
||||
def confirm_async(yes_action, no_action=None, cancel_action=None,
|
||||
*args, **kwargs):
|
||||
"""Ask a yes/no question to the user and execute the given actions.
|
||||
|
||||
Args:
|
||||
win_id: The ID of the window which is calling this function.
|
||||
message: The message to display to the user.
|
||||
yes_action: Callable to be called when the user answered yes.
|
||||
no_action: Callable to be called when the user answered no.
|
||||
cancel_action: Callable to be called when the user cancelled the
|
||||
question.
|
||||
default: True/False to set a default value, or None.
|
||||
text: Additional text to show.
|
||||
|
||||
Return:
|
||||
The question object.
|
||||
"""
|
||||
q = usertypes.Question()
|
||||
q.text = message
|
||||
q.mode = usertypes.PromptMode.yesno
|
||||
q.default = default
|
||||
q.answered_yes.connect(yes_action)
|
||||
kwargs['mode'] = usertypes.PromptMode.yesno
|
||||
question = _build_question(*args, **kwargs) # pylint: disable=missing-kwoa
|
||||
question.answered_yes.connect(yes_action)
|
||||
if no_action is not None:
|
||||
q.answered_no.connect(no_action)
|
||||
q.completed.connect(q.deleteLater)
|
||||
bridge = objreg.get('message-bridge', scope='window', window=win_id)
|
||||
bridge.ask(q, blocking=False)
|
||||
question.answered_no.connect(no_action)
|
||||
if cancel_action is not None:
|
||||
question.cancelled.connect(cancel_action)
|
||||
|
||||
question.completed.connect(question.deleteLater)
|
||||
global_bridge.ask(question, blocking=False)
|
||||
return question
|
||||
|
||||
|
||||
class GlobalMessageBridge(QObject):
|
||||
@ -150,9 +163,34 @@ class GlobalMessageBridge(QObject):
|
||||
show_message: Show a message
|
||||
arg 0: A MessageLevel member
|
||||
arg 1: The text to show
|
||||
prompt_done: Emitted when a prompt was answered somewhere.
|
||||
ask_question: Ask a question to the user.
|
||||
arg 0: The Question object to ask.
|
||||
arg 1: Whether to block (True) or ask async (False).
|
||||
|
||||
IMPORTANT: Slots need to be connected to this signal via
|
||||
a Qt.DirectConnection!
|
||||
mode_left: Emitted when a keymode was left in any window.
|
||||
"""
|
||||
|
||||
show_message = pyqtSignal(usertypes.MessageLevel, str)
|
||||
prompt_done = pyqtSignal(usertypes.KeyMode)
|
||||
ask_question = pyqtSignal(usertypes.Question, bool)
|
||||
mode_left = pyqtSignal(usertypes.KeyMode)
|
||||
|
||||
def ask(self, question, blocking, *, log_stack=False):
|
||||
"""Ask a question to the user.
|
||||
|
||||
Note this method doesn't return the answer, it only blocks. The caller
|
||||
needs to construct a Question object and get the answer.
|
||||
|
||||
Args:
|
||||
question: A Question object.
|
||||
blocking: Whether to return immediately or wait until the
|
||||
question is answered.
|
||||
log_stack: ignored
|
||||
"""
|
||||
self.ask_question.emit(question, blocking)
|
||||
|
||||
|
||||
class MessageBridge(QObject):
|
||||
@ -164,36 +202,14 @@ class MessageBridge(QObject):
|
||||
arg: The text to set.
|
||||
s_maybe_reset_text: Reset the text if it hasn't been changed yet.
|
||||
arg: The expected text.
|
||||
s_set_cmd_text: Pre-set a text for the commandline prompt.
|
||||
arg: The text to set.
|
||||
|
||||
s_question: Ask a question to the user in the statusbar.
|
||||
arg 0: The Question object to ask.
|
||||
arg 1: Whether to block (True) or ask async (False).
|
||||
|
||||
IMPORTANT: Slots need to be connected to this signal via a
|
||||
Qt.DirectConnection!
|
||||
"""
|
||||
|
||||
s_set_text = pyqtSignal(str)
|
||||
s_maybe_reset_text = pyqtSignal(str)
|
||||
s_set_cmd_text = pyqtSignal(str)
|
||||
s_question = pyqtSignal(usertypes.Question, bool)
|
||||
|
||||
def __repr__(self):
|
||||
return utils.get_repr(self)
|
||||
|
||||
def set_cmd_text(self, text, *, log_stack=False):
|
||||
"""Set the command text of the statusbar.
|
||||
|
||||
Args:
|
||||
text: The text to set.
|
||||
log_stack: ignored
|
||||
"""
|
||||
text = str(text)
|
||||
log.message.debug(text)
|
||||
self.s_set_cmd_text.emit(text)
|
||||
|
||||
def set_text(self, text, *, log_stack=False):
|
||||
"""Set the normal text of the statusbar.
|
||||
|
||||
@ -214,19 +230,5 @@ class MessageBridge(QObject):
|
||||
"""
|
||||
self.s_maybe_reset_text.emit(str(text))
|
||||
|
||||
def ask(self, question, blocking, *, log_stack=False):
|
||||
"""Ask a question to the user.
|
||||
|
||||
Note this method doesn't return the answer, it only blocks. The caller
|
||||
needs to construct a Question object and get the answer.
|
||||
|
||||
Args:
|
||||
question: A Question object.
|
||||
blocking: Whether to return immediately or wait until the
|
||||
question is answered.
|
||||
log_stack: ignored
|
||||
"""
|
||||
self.s_question.emit(question, blocking)
|
||||
|
||||
|
||||
global_bridge = GlobalMessageBridge()
|
||||
|
@ -143,7 +143,12 @@ class ObjectRegistry(collections.UserDict):
|
||||
"""Dump all objects as a list of strings."""
|
||||
lines = []
|
||||
for name, obj in self.data.items():
|
||||
lines.append("{}: {}".format(name, repr(obj)))
|
||||
try:
|
||||
obj_repr = repr(obj)
|
||||
except (RuntimeError, TypeError):
|
||||
# Underlying object deleted probably
|
||||
obj_repr = '<deleted>'
|
||||
lines.append("{}: {}".format(name, obj_repr))
|
||||
return lines
|
||||
|
||||
|
||||
|
@ -335,10 +335,11 @@ class Question(QObject):
|
||||
For yesno, None (no default), True or False.
|
||||
For text, a default text as string.
|
||||
For user_pwd, a default username as string.
|
||||
title: The question title to show.
|
||||
text: The prompt text to display to the user.
|
||||
user: The value the user entered as username.
|
||||
answer: The value the user entered (as password for user_pwd).
|
||||
is_aborted: Whether the question was aborted.
|
||||
interrupted: Whether the question was interrupted by another one.
|
||||
|
||||
Signals:
|
||||
answered: Emitted when the question has been answered by the user.
|
||||
@ -364,14 +365,15 @@ class Question(QObject):
|
||||
super().__init__(parent)
|
||||
self._mode = None
|
||||
self.default = None
|
||||
self.title = None
|
||||
self.text = None
|
||||
self.user = None
|
||||
self.answer = None
|
||||
self.is_aborted = False
|
||||
self.interrupted = False
|
||||
|
||||
def __repr__(self):
|
||||
return utils.get_repr(self, text=self.text, mode=self._mode,
|
||||
default=self.default)
|
||||
return utils.get_repr(self, title=self.title, text=self.text,
|
||||
mode=self._mode, default=self.default)
|
||||
|
||||
@property
|
||||
def mode(self):
|
||||
@ -405,6 +407,9 @@ class Question(QObject):
|
||||
@pyqtSlot()
|
||||
def abort(self):
|
||||
"""Abort the question."""
|
||||
if self.is_aborted:
|
||||
log.misc.debug("Question was already aborted")
|
||||
return
|
||||
self.is_aborted = True
|
||||
try:
|
||||
self.aborted.emit()
|
||||
|
@ -258,7 +258,7 @@ def github_upload(artifacts, tag):
|
||||
with open(filename, 'rb') as f:
|
||||
basename = os.path.basename(filename)
|
||||
asset = release.upload_asset(mimetype, basename, f)
|
||||
asset.edit(filename, description)
|
||||
asset.edit(basename, description)
|
||||
|
||||
|
||||
def pypi_upload(artifacts):
|
||||
|
@ -115,8 +115,6 @@ PERFECT_FILES = [
|
||||
'qutebrowser/mainwindow/statusbar/tabindex.py'),
|
||||
('tests/unit/mainwindow/statusbar/test_textbase.py',
|
||||
'qutebrowser/mainwindow/statusbar/textbase.py'),
|
||||
('tests/unit/mainwindow/statusbar/test_prompt.py',
|
||||
'qutebrowser/mainwindow/statusbar/prompt.py'),
|
||||
('tests/unit/mainwindow/statusbar/test_url.py',
|
||||
'qutebrowser/mainwindow/statusbar/url.py'),
|
||||
('tests/unit/mainwindow/test_messageview.py',
|
||||
|
@ -47,7 +47,7 @@ def pip_install(pkg):
|
||||
print("Getting PyQt5...")
|
||||
qt_version = '5.5.1'
|
||||
pyqt_version = '5.5.1'
|
||||
pyqt_url = ('http://www.qutebrowser.org/pyqt/'
|
||||
pyqt_url = ('https://www.qutebrowser.org/pyqt/'
|
||||
'PyQt5-{}-gpl-Py3.4-Qt{}-x32.exe'.format(
|
||||
pyqt_version, qt_version))
|
||||
|
||||
@ -61,8 +61,8 @@ except (OSError, IOError):
|
||||
print("Installing PyQt5...")
|
||||
subprocess.check_call([r'C:\install-PyQt5.exe', '/S'])
|
||||
|
||||
print("Installing pip/tox")
|
||||
pip_install(r'-rmisc\requirements\requirements-pip.txt')
|
||||
print("Installing tox")
|
||||
pip_install('pip')
|
||||
pip_install(r'-rmisc\requirements\requirements-tox.txt')
|
||||
|
||||
print("Linking Python...")
|
||||
|
@ -54,7 +54,7 @@ brew_install() {
|
||||
|
||||
pip_install() {
|
||||
# this uses python2
|
||||
travis_retry sudo -H python -m pip install -r misc/requirements/requirements-$1.txt
|
||||
travis_retry sudo -H python -m pip install "$@"
|
||||
}
|
||||
|
||||
npm_install() {
|
||||
@ -95,7 +95,7 @@ elif [[ $TRAVIS_OS_NAME == osx ]]; then
|
||||
brew --version
|
||||
brew_install python3 qt5 pyqt5
|
||||
|
||||
pip_install tox
|
||||
pip_install -r misc/requirements/requirements-tox.txt
|
||||
pip --version
|
||||
tox --version
|
||||
check_pyqt
|
||||
@ -105,14 +105,14 @@ fi
|
||||
pyqt_pkgs="python3-pyqt5 python3-pyqt5.qtwebkit"
|
||||
|
||||
pip_install pip
|
||||
pip_install tox
|
||||
pip_install -r misc/requirements/requirements-tox.txt
|
||||
|
||||
pip --version
|
||||
tox --version
|
||||
|
||||
case $TESTENV in
|
||||
py34-cov)
|
||||
pip_install codecov
|
||||
pip_install -r misc/requirements/requirements-codecov.txt
|
||||
apt_install xvfb $pyqt_pkgs libpython3.4-dev
|
||||
check_pyqt
|
||||
;;
|
||||
|
@ -77,7 +77,11 @@ def get_info(pid):
|
||||
for line in output.split('\n'):
|
||||
if not line.strip():
|
||||
continue
|
||||
try:
|
||||
key, value = line.split(':', maxsplit=1)
|
||||
except ValueError:
|
||||
# systemd stack output
|
||||
continue
|
||||
data[key.strip()] = value.strip()
|
||||
return data
|
||||
|
||||
|
@ -104,7 +104,7 @@ setupdata = {
|
||||
'version': '.'.join(str(e) for e in _get_constant('version_info')),
|
||||
'description': _get_constant('description'),
|
||||
'long_description': read_file('README.asciidoc'),
|
||||
'url': 'http://www.qutebrowser.org/',
|
||||
'url': 'https://www.qutebrowser.org/',
|
||||
'requires': ['pypeg2', 'jinja2', 'pygments', 'PyYAML'],
|
||||
'author': _get_constant('author'),
|
||||
'author_email': _get_constant('email'),
|
||||
|
13
scripts/testbrowser_cpp/webengine/main.cpp
Normal file
13
scripts/testbrowser_cpp/webengine/main.cpp
Normal file
@ -0,0 +1,13 @@
|
||||
#include <QApplication>
|
||||
#include <QWebEngineView>
|
||||
#include <QUrl>
|
||||
|
||||
|
||||
int main(int argc, char *argv[])
|
||||
{
|
||||
QApplication app(argc, argv);
|
||||
QWebEngineView view;
|
||||
view.load(QUrl(argv[1]));
|
||||
view.show();
|
||||
return app.exec();
|
||||
}
|
6
scripts/testbrowser_cpp/webengine/testbrowser.pro
Normal file
6
scripts/testbrowser_cpp/webengine/testbrowser.pro
Normal file
@ -0,0 +1,6 @@
|
||||
QT += core widgets webenginewidgets
|
||||
|
||||
TARGET = testbrowser
|
||||
TEMPLATE = app
|
||||
|
||||
SOURCES += main.cpp
|
@ -121,7 +121,7 @@ def pytest_collection_modifyitems(config, items):
|
||||
if item.get_marker('xfail_norun'):
|
||||
item.add_marker(pytest.mark.xfail(run=False))
|
||||
if item.get_marker('flaky_once'):
|
||||
item.add_marker(pytest.mark.flaky(reruns=1))
|
||||
item.add_marker(pytest.mark.flaky())
|
||||
|
||||
if deselected:
|
||||
deselected_items.append(item)
|
||||
|
@ -3,13 +3,14 @@
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<script type="text/javascript">
|
||||
function prompter() {
|
||||
var reply = prompt("js prompt", "")
|
||||
console.log("Prompt reply: " + reply)
|
||||
function prompter(defaultval) {
|
||||
var reply = prompt("js prompt", defaultval);
|
||||
console.log("Prompt reply: " + reply);
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<input type="button" onclick="prompter()" value="Show prompt" id="button">
|
||||
<input type="button" onclick="prompter('')" value="Show prompt" id="button">
|
||||
<input type="button" onclick="prompter('default')" value="Show prompt with default value" id="button-default">
|
||||
</body>
|
||||
</html>
|
||||
|
@ -188,6 +188,7 @@ def open_path(quteproc, path):
|
||||
do_not_wait_suffix = ' without waiting'
|
||||
as_url_suffix = ' as a URL'
|
||||
|
||||
while True:
|
||||
if path.endswith(new_tab_suffix):
|
||||
path = path[:-len(new_tab_suffix)]
|
||||
new_tab = True
|
||||
@ -197,10 +198,11 @@ def open_path(quteproc, path):
|
||||
elif path.endswith(as_url_suffix):
|
||||
path = path[:-len(as_url_suffix)]
|
||||
as_url = True
|
||||
|
||||
if path.endswith(do_not_wait_suffix):
|
||||
elif path.endswith(do_not_wait_suffix):
|
||||
path = path[:-len(do_not_wait_suffix)]
|
||||
wait = False
|
||||
else:
|
||||
break
|
||||
|
||||
quteproc.open_path(path, new_tab=new_tab, new_window=new_window,
|
||||
as_url=as_url, wait=wait)
|
||||
|
@ -63,7 +63,7 @@ Feature: Downloading things from a website.
|
||||
And I set storage -> prompt-download-directory to true
|
||||
And I open data/downloads/issue1243.html
|
||||
And I hint with args "links download" and follow a
|
||||
And I wait for "Asking question <qutebrowser.utils.usertypes.Question default='qutebrowser-download' mode=<PromptMode.download: 5> text='Save file to:'>, *" in the log
|
||||
And I wait for "Asking question <qutebrowser.utils.usertypes.Question default='qutebrowser-download' mode=<PromptMode.download: 5> text=* title='Save file to:'>, *" in the log
|
||||
Then the error "Download error: No handler found for qute://!" should be shown
|
||||
|
||||
Scenario: Downloading a data: link (issue 1214)
|
||||
@ -71,7 +71,7 @@ Feature: Downloading things from a website.
|
||||
And I set storage -> prompt-download-directory to true
|
||||
And I open data/downloads/issue1214.html
|
||||
And I hint with args "links download" and follow a
|
||||
And I wait for "Asking question <qutebrowser.utils.usertypes.Question default='binary blob' mode=<PromptMode.download: 5> text='Save file to:'>, *" in the log
|
||||
And I wait for "Asking question <qutebrowser.utils.usertypes.Question default='binary blob' mode=<PromptMode.download: 5> text=* title='Save file to:'>, *" in the log
|
||||
And I run :leave-mode
|
||||
Then no crash should happen
|
||||
|
||||
@ -338,7 +338,7 @@ Feature: Downloading things from a website.
|
||||
When I set storage -> prompt-download-directory to true
|
||||
And I open data/downloads/issue1725.html
|
||||
And I run :click-element id long-link
|
||||
And I wait for "Asking question <qutebrowser.utils.usertypes.Question default=* mode=<PromptMode.download: 5> text='Save file to:'>, *" in the log
|
||||
And I wait for "Asking question <qutebrowser.utils.usertypes.Question default=* mode=<PromptMode.download: 5> text=* title='Save file to:'>, *" in the log
|
||||
And I directly open the download
|
||||
And I wait until the download is finished
|
||||
Then "Opening * with [*python*]" should be logged
|
||||
@ -484,3 +484,15 @@ Feature: Downloading things from a website.
|
||||
And I run :click-element id download
|
||||
And I wait until the download is finished
|
||||
Then the downloaded file test.pdf should exist
|
||||
|
||||
Scenario: Answering a question for a cancelled download (#415)
|
||||
When I set storage -> prompt-download-directory to true
|
||||
And I run :download http://localhost:(port)/data/downloads/download.bin
|
||||
And I wait for "Asking question <qutebrowser.utils.usertypes.Question default='*' mode=<PromptMode.download: 5> text=* title='Save file to:'>, *" in the log
|
||||
And I run :download http://localhost:(port)/data/downloads/download2.bin
|
||||
And I wait for "Asking question <qutebrowser.utils.usertypes.Question default='*' mode=<PromptMode.download: 5> text=* title='Save file to:'>, *" in the log
|
||||
And I run :download-cancel with count 2
|
||||
And I run :prompt-accept
|
||||
And I wait until the download is finished
|
||||
Then the downloaded file download.bin should exist
|
||||
And the downloaded file download2.bin should not exist
|
||||
|
@ -180,6 +180,11 @@ Feature: Using hints
|
||||
- data/hints/iframe_target.html
|
||||
- data/hello.txt (active)
|
||||
|
||||
Scenario: Clicking on iframe with :hint all current
|
||||
When I open data/hints/iframe.html
|
||||
And I hint with args "all current" and follow a
|
||||
Then no crash should happen
|
||||
|
||||
### hints -> auto-follow-timeout
|
||||
|
||||
@not_osx
|
||||
|
@ -71,3 +71,8 @@ Feature: Javascript stuff
|
||||
And I run :tab-only
|
||||
And I run :jseval if (window.open('about:blank')) { console.log('window opened'); } else { console.log('error while opening window'); }
|
||||
Then the javascript message "error while opening window" should be logged
|
||||
|
||||
Scenario: Executing jseval when javascript is disabled
|
||||
When I set content -> allow-javascript to false
|
||||
And I run :jseval console.log('jseval executed')
|
||||
Then the javascript message "jseval executed" should be logged
|
||||
|
@ -45,20 +45,6 @@ Feature: Various utility commands.
|
||||
When I run :set-cmd-text foo
|
||||
Then the error "Invalid command text 'foo'." should be shown
|
||||
|
||||
## :message-*
|
||||
|
||||
Scenario: :message-error
|
||||
When I run :message-error "Hello World"
|
||||
Then the error "Hello World" should be shown
|
||||
|
||||
Scenario: :message-info
|
||||
When I run :message-info "Hello World"
|
||||
Then the message "Hello World" should be shown
|
||||
|
||||
Scenario: :message-warning
|
||||
When I run :message-warning "Hello World"
|
||||
Then the warning "Hello World" should be shown
|
||||
|
||||
## :jseval
|
||||
|
||||
Scenario: :jseval
|
||||
@ -243,16 +229,6 @@ Feature: Various utility commands.
|
||||
And I run :view-source
|
||||
Then the error "Already viewing source!" should be shown
|
||||
|
||||
# :debug-console
|
||||
|
||||
@no_xvfb
|
||||
Scenario: :debug-console smoke test
|
||||
When I run :debug-console
|
||||
And I wait for "Focus object changed: <qutebrowser.misc.consolewidget.ConsoleLineEdit *>" in the log
|
||||
And I run :debug-console
|
||||
And I wait for "Focus object changed: *" in the log
|
||||
Then no crash should happen
|
||||
|
||||
# :help
|
||||
|
||||
Scenario: :help without topic
|
||||
@ -348,7 +324,7 @@ Feature: Various utility commands.
|
||||
And I open data/misc/test.pdf
|
||||
And I wait for "[qute://pdfjs/*] PDF * (PDF.js: *)" in the log
|
||||
And I run :jseval document.getElementById("download").click()
|
||||
And I wait for "Asking question <qutebrowser.utils.usertypes.Question default='test.pdf' mode=<PromptMode.download: 5> text='Save file to:'>, *" in the log
|
||||
And I wait for "Asking question <qutebrowser.utils.usertypes.Question default='test.pdf' mode=<PromptMode.download: 5> text=* title='Save file to:'>, *" in the log
|
||||
And I run :leave-mode
|
||||
Then no crash should happen
|
||||
|
||||
@ -496,31 +472,6 @@ Feature: Various utility commands.
|
||||
Then qute://log?level=error should be loaded
|
||||
And the page should contain the plaintext "No messages to show."
|
||||
|
||||
Scenario: Using :debug-log-capacity
|
||||
When I run :debug-log-capacity 100
|
||||
And I run :message-info oldstuff
|
||||
And I run :repeat 20 message-info otherstuff
|
||||
And I run :message-info newstuff
|
||||
And I open qute:log
|
||||
Then the page should contain the plaintext "newstuff"
|
||||
And the page should not contain the plaintext "oldstuff"
|
||||
|
||||
Scenario: Using :debug-log-capacity with negative capacity
|
||||
When I run :debug-log-capacity -1
|
||||
Then the error "Can't set a negative log capacity!" should be shown
|
||||
|
||||
# :debug-log-level / :debug-log-filter
|
||||
# Other :debug-log-{level,filter} features are tested in
|
||||
# unit/utils/test_log.py as using them would break end2end tests.
|
||||
|
||||
Scenario: Using debug-log-level with invalid level
|
||||
When I run :debug-log-level hello
|
||||
Then the error "level: Invalid value hello - expected one of: vdebug, debug, info, warning, error, critical" should be shown
|
||||
|
||||
Scenario: Using debug-log-filter with invalid filter
|
||||
When I run :debug-log-filter blah
|
||||
Then the error "filters: Invalid value blah - expected one of: statusbar, *" should be shown
|
||||
|
||||
## https://github.com/The-Compiler/qutebrowser/issues/1523
|
||||
|
||||
Scenario: Completing a single option argument
|
||||
@ -561,51 +512,6 @@ Feature: Various utility commands.
|
||||
And I set general -> private-browsing to false
|
||||
Then the page should contain the plaintext "Local storage status: not working"
|
||||
|
||||
Scenario: :repeat-command
|
||||
Given I open data/scroll/simple.html
|
||||
And I run :tab-only
|
||||
When I run :scroll down
|
||||
And I run :repeat-command
|
||||
And I run :scroll up
|
||||
Then the page should be scrolled vertically
|
||||
|
||||
Scenario: :repeat-command with count
|
||||
Given I open data/scroll/simple.html
|
||||
And I run :tab-only
|
||||
When I run :scroll down with count 3
|
||||
And I wait until the scroll position changed
|
||||
And I run :scroll up
|
||||
And I wait until the scroll position changed
|
||||
And I run :repeat-command with count 2
|
||||
And I wait until the scroll position changed to 0/0
|
||||
Then the page should not be scrolled
|
||||
|
||||
Scenario: :repeat-command with not-normal command inbetween
|
||||
Given I open data/scroll/simple.html
|
||||
And I run :tab-only
|
||||
When I run :scroll down with count 3
|
||||
And I wait until the scroll position changed
|
||||
And I run :scroll up
|
||||
And I wait until the scroll position changed
|
||||
And I run :prompt-accept
|
||||
And I run :repeat-command with count 2
|
||||
And I wait until the scroll position changed to 0/0
|
||||
Then the page should not be scrolled
|
||||
And the error "prompt-accept: This command is only allowed in prompt/yesno mode, not normal." should be shown
|
||||
|
||||
@qtwebengine_createWindow
|
||||
Scenario: :repeat-command with mode-switching command
|
||||
Given I open data/hints/link_blank.html
|
||||
And I run :tab-only
|
||||
When I hint with args "all"
|
||||
And I run :leave-mode
|
||||
And I run :repeat-command
|
||||
And I run :follow-hint a
|
||||
And I wait until data/hello.txt is loaded
|
||||
Then the following tabs should be open:
|
||||
- data/hints/link_blank.html
|
||||
- data/hello.txt (active)
|
||||
|
||||
Scenario: Using 0 as count
|
||||
When I run :scroll down with count 0
|
||||
Then the error "scroll: A zero count is not allowed for this command!" should be shown
|
||||
@ -762,13 +668,3 @@ Feature: Various utility commands.
|
||||
And I run :command-accept
|
||||
And I set general -> private-browsing to false
|
||||
Then the message "blah" should be shown
|
||||
|
||||
## :run-with-count
|
||||
|
||||
Scenario: :run-with-count
|
||||
When I run :run-with-count 2 scroll down
|
||||
Then "command called: scroll ['down'] (count=2)" should be logged
|
||||
|
||||
Scenario: :run-with-count with count
|
||||
When I run :run-with-count 2 scroll down with count 3
|
||||
Then "command called: scroll ['down'] (count=6)" should be logged
|
||||
|
@ -49,6 +49,14 @@ Feature: Prompts
|
||||
And I run :prompt-accept
|
||||
Then the javascript message "Prompt reply: prompt test" should be logged
|
||||
|
||||
@pyqt>=5.3.1
|
||||
Scenario: Javascript prompt with default
|
||||
When I open data/prompt/jsprompt.html
|
||||
And I run :click-element id button-default
|
||||
And I wait for a prompt
|
||||
And I run :prompt-accept
|
||||
Then the javascript message "Prompt reply: default" should be logged
|
||||
|
||||
@pyqt>=5.3.1
|
||||
Scenario: Rejected javascript prompt
|
||||
When I open data/prompt/jsprompt.html
|
||||
@ -58,6 +66,68 @@ Feature: Prompts
|
||||
And I run :leave-mode
|
||||
Then the javascript message "Prompt reply: null" should be logged
|
||||
|
||||
# Multiple prompts
|
||||
|
||||
Scenario: Blocking question interrupted by blocking one
|
||||
When I set content -> ignore-javascript-alert to false
|
||||
And I open data/prompt/jsalert.html
|
||||
And I run :click-element id button
|
||||
And I wait for a prompt
|
||||
And I open data/prompt/jsconfirm.html in a new tab
|
||||
And I run :click-element id button
|
||||
And I wait for a prompt
|
||||
# JS confirm
|
||||
And I run :prompt-accept yes
|
||||
# JS alert
|
||||
And I run :prompt-accept
|
||||
Then the javascript message "confirm reply: true" should be logged
|
||||
And the javascript message "Alert done" should be logged
|
||||
|
||||
Scenario: Blocking question interrupted by async one
|
||||
When I set content -> ignore-javascript-alert to false
|
||||
And I set content -> notifications to ask
|
||||
And I open data/prompt/jsalert.html
|
||||
And I run :click-element id button
|
||||
And I wait for a prompt
|
||||
And I open data/prompt/notifications.html in a new tab
|
||||
And I run :click-element id button
|
||||
And I wait for a prompt
|
||||
# JS alert
|
||||
And I run :prompt-accept
|
||||
# notification permission
|
||||
And I run :prompt-accept yes
|
||||
Then the javascript message "Alert done" should be logged
|
||||
And the javascript message "notification permission granted" should be logged
|
||||
|
||||
Scenario: Async question interrupted by async one
|
||||
When I set content -> notifications to ask
|
||||
And I open data/prompt/notifications.html in a new tab
|
||||
And I run :click-element id button
|
||||
And I wait for a prompt
|
||||
And I run :quickmark-save
|
||||
And I wait for a prompt
|
||||
# notification permission
|
||||
And I run :prompt-accept yes
|
||||
# quickmark
|
||||
And I run :prompt-accept test
|
||||
Then the javascript message "notification permission granted" should be logged
|
||||
And "Added quickmark test for *" should be logged
|
||||
|
||||
Scenario: Async question interrupted by blocking one
|
||||
When I set content -> notifications to ask
|
||||
And I set content -> ignore-javascript-alert to false
|
||||
And I open data/prompt/notifications.html in a new tab
|
||||
And I run :click-element id button
|
||||
And I wait for a prompt
|
||||
And I open data/prompt/jsalert.html in a new tab
|
||||
And I run :click-element id button
|
||||
And I wait for a prompt
|
||||
# JS alert
|
||||
And I run :prompt-accept
|
||||
# notification permission
|
||||
And I run :prompt-accept yes
|
||||
Then the javascript message "Alert done" should be logged
|
||||
And the javascript message "notification permission granted" should be logged
|
||||
|
||||
# Shift-Insert with prompt (issue 1299)
|
||||
|
||||
@ -72,6 +142,17 @@ Feature: Prompts
|
||||
And I run :prompt-accept
|
||||
Then the javascript message "Prompt reply: insert test" should be logged
|
||||
|
||||
@pyqt>=5.3.1
|
||||
Scenario: Pasting via shift-insert without it being supported
|
||||
When selection is not supported
|
||||
And I put "insert test" into the primary selection
|
||||
And I open data/prompt/jsprompt.html
|
||||
And I run :click-element id button
|
||||
And I wait for a prompt
|
||||
And I press the keys "<Shift-Insert>"
|
||||
And I run :prompt-accept
|
||||
Then the javascript message "Prompt reply: " should be logged
|
||||
|
||||
@pyqt>=5.3.1
|
||||
Scenario: Using content -> ignore-javascript-prompt
|
||||
When I set content -> ignore-javascript-prompt to true
|
||||
@ -219,6 +300,44 @@ Feature: Prompts
|
||||
"user": "user"
|
||||
}
|
||||
|
||||
Scenario: Authentication with :prompt-accept value
|
||||
When I open about:blank in a new tab
|
||||
And I open basic-auth/user/password without waiting
|
||||
And I wait for a prompt
|
||||
And I run :prompt-accept user:password
|
||||
And I wait until basic-auth/user/password is loaded
|
||||
Then the json on the page should be:
|
||||
{
|
||||
"authenticated": true,
|
||||
"user": "user"
|
||||
}
|
||||
|
||||
Scenario: Authentication with invalid :prompt-accept value
|
||||
When I open about:blank in a new tab
|
||||
And I open basic-auth/user/password without waiting
|
||||
And I wait for a prompt
|
||||
And I run :prompt-accept foo
|
||||
And I run :prompt-accept user:password
|
||||
Then the error "Value needs to be in the format username:password, but foo was given" should be shown
|
||||
|
||||
Scenario: Tabbing between username and password
|
||||
When I open about:blank in a new tab
|
||||
And I open basic-auth/user/password without waiting
|
||||
And I wait for a prompt
|
||||
And I press the keys "us"
|
||||
And I run :prompt-item-focus next
|
||||
And I press the keys "password"
|
||||
And I run :prompt-item-focus prev
|
||||
And I press the keys "er"
|
||||
And I run :prompt-accept
|
||||
And I run :prompt-accept
|
||||
And I wait until basic-auth/user/password is loaded
|
||||
Then the json on the page should be:
|
||||
{
|
||||
"authenticated": true,
|
||||
"user": "user"
|
||||
}
|
||||
|
||||
# :prompt-accept with value argument
|
||||
|
||||
Scenario: Javascript alert with value
|
||||
@ -249,3 +368,102 @@ Feature: Prompts
|
||||
And I run :prompt-accept yes
|
||||
Then the javascript message "confirm reply: true" should be logged
|
||||
And the error "Invalid value nope - expected yes/no!" should be shown
|
||||
|
||||
Scenario: Javascript confirm with default value
|
||||
When I open data/prompt/jsconfirm.html
|
||||
And I run :click-element id button
|
||||
And I wait for a prompt
|
||||
And I run :prompt-accept
|
||||
And I run :prompt-accept yes
|
||||
Then the javascript message "confirm reply: true" should be logged
|
||||
And the error "No default value was set for this question!" should be shown
|
||||
|
||||
Scenario: Javascript confirm with deprecated :prompt-yes command
|
||||
When I open data/prompt/jsconfirm.html
|
||||
And I run :click-element id button
|
||||
And I wait for a prompt
|
||||
And I run :prompt-yes
|
||||
Then the javascript message "confirm reply: true" should be logged
|
||||
And the warning "prompt-yes is deprecated - Use :prompt-accept yes instead!" should be shown
|
||||
|
||||
Scenario: Javascript confirm with deprecated :prompt-no command
|
||||
When I open data/prompt/jsconfirm.html
|
||||
And I run :click-element id button
|
||||
And I wait for a prompt
|
||||
And I run :prompt-no
|
||||
Then the javascript message "confirm reply: false" should be logged
|
||||
And the warning "prompt-no is deprecated - Use :prompt-accept no instead!" should be shown
|
||||
|
||||
# Other
|
||||
|
||||
Scenario: Shutting down with a question
|
||||
When I open data/prompt/jsconfirm.html
|
||||
And I run :click-element id button
|
||||
And I wait for a prompt
|
||||
And I run :quit
|
||||
Then the javascript message "confirm reply: false" should be logged
|
||||
And qutebrowser should quit
|
||||
|
||||
Scenario: Using :prompt-open-download with a prompt which does not support it
|
||||
When I open data/hello.txt
|
||||
And I run :quickmark-save
|
||||
And I wait for a prompt
|
||||
And I run :prompt-open-download
|
||||
And I run :prompt-accept test-prompt-open-download
|
||||
Then "Added quickmark test-prompt-open-download for *" should be logged
|
||||
|
||||
Scenario: Using :prompt-item-focus with a prompt which does not support it
|
||||
When I open data/hello.txt
|
||||
And I run :quickmark-save
|
||||
And I wait for a prompt
|
||||
And I run :prompt-item-focus next
|
||||
And I run :prompt-accept test-prompt-item-focus
|
||||
Then "Added quickmark test-prompt-item-focus for *" should be logged
|
||||
|
||||
Scenario: Getting question in command mode
|
||||
When I open data/hello.txt
|
||||
And I run :later 500 quickmark-save
|
||||
And I run :set-cmd-text :
|
||||
And I wait for a prompt
|
||||
And I run :prompt-accept prompt-in-command-mode
|
||||
Then "Added quickmark prompt-in-command-mode for *" should be logged
|
||||
|
||||
# https://github.com/The-Compiler/qutebrowser/issues/1093
|
||||
Scenario: Keyboard focus with multiple auth prompts
|
||||
When I open basic-auth/user1/password1 without waiting
|
||||
And I open basic-auth/user2/password2 in a new tab without waiting
|
||||
And I wait for a prompt
|
||||
And I wait for a prompt
|
||||
# Second prompt (showed first)
|
||||
And I press the keys "user2"
|
||||
And I press the key "<Enter>"
|
||||
And I press the keys "password2"
|
||||
And I press the key "<Enter>"
|
||||
And I wait until basic-auth/user2/password2 is loaded
|
||||
# First prompt
|
||||
And I press the keys "user1"
|
||||
And I press the key "<Enter>"
|
||||
And I press the keys "password1"
|
||||
And I press the key "<Enter>"
|
||||
And I wait until basic-auth/user1/password1 is loaded
|
||||
# We're on the second page
|
||||
Then the json on the page should be:
|
||||
{
|
||||
"authenticated": true,
|
||||
"user": "user2"
|
||||
}
|
||||
|
||||
# https://github.com/The-Compiler/qutebrowser/issues/1249#issuecomment-175205531
|
||||
# https://github.com/The-Compiler/qutebrowser/pull/2054#issuecomment-258285544
|
||||
Scenario: Interrupting SSL prompt during a notification prompt
|
||||
When I set content -> notifications to ask
|
||||
And I set network -> ssl-strict to ask
|
||||
And I open data/prompt/notifications.html in a new tab
|
||||
And I run :click-element id button
|
||||
And I wait for a prompt
|
||||
And I open about:blank in a new tab
|
||||
And I load an SSL page
|
||||
And I wait for a prompt
|
||||
And I run :tab-close
|
||||
And I run :prompt-accept yes
|
||||
Then the javascript message "notification permission granted" should be logged
|
||||
|
@ -31,8 +31,8 @@ pytestmark = pytest.mark.qtwebengine_todo("Downloads not implemented yet",
|
||||
|
||||
|
||||
PROMPT_MSG = ("Asking question <qutebrowser.utils.usertypes.Question "
|
||||
"default={!r} mode=<PromptMode.download: 5> "
|
||||
"text='Save file to:'>, *")
|
||||
"default={!r} mode=<PromptMode.download: 5> text=* "
|
||||
"title='Save file to:'>, *")
|
||||
|
||||
|
||||
@bdd.given("I set up a temporary download dir")
|
||||
|
@ -39,8 +39,7 @@ def wait_ssl_page_finished_loading(quteproc, ssl_server):
|
||||
|
||||
@bdd.when("I wait for a prompt")
|
||||
def wait_for_prompt(quteproc):
|
||||
quteproc.wait_for(message='Entering mode KeyMode.* (reason: question '
|
||||
'asked)')
|
||||
quteproc.wait_for(message='Asking question *')
|
||||
|
||||
|
||||
@bdd.then("no prompt should be shown")
|
||||
|
22
tests/end2end/features/test_utilcmds_bdd.py
Normal file
22
tests/end2end/features/test_utilcmds_bdd.py
Normal file
@ -0,0 +1,22 @@
|
||||
# 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/>.
|
||||
|
||||
import pytest_bdd as bdd
|
||||
|
||||
bdd.scenarios('utilcmds.feature')
|
165
tests/end2end/features/utilcmds.feature
Normal file
165
tests/end2end/features/utilcmds.feature
Normal file
@ -0,0 +1,165 @@
|
||||
Feature: Miscellaneous utility commands exposed to the user.
|
||||
|
||||
Background:
|
||||
Given I open data/scroll/simple.html
|
||||
And I run :tab-only
|
||||
|
||||
## :later
|
||||
|
||||
Scenario: :later before
|
||||
When I run :later 500 scroll down
|
||||
Then the page should not be scrolled
|
||||
# wait for scroll to execture so we don't ruin our future
|
||||
And the page should be scrolled vertically
|
||||
|
||||
Scenario: :later after
|
||||
When I run :later 500 scroll down
|
||||
And I wait 0.6s
|
||||
Then the page should be scrolled vertically
|
||||
|
||||
# for some reason, argparser gives us the error instead, see #2046
|
||||
@xfail
|
||||
Scenario: :later with negative delay
|
||||
When I run :later -1 scroll down
|
||||
Then the error "I can't run something in the past!" should be shown
|
||||
|
||||
Scenario: :later with humongous delay
|
||||
When I run :later 36893488147419103232 scroll down
|
||||
Then the error "Numeric argument is too large for internal int representation." should be shown
|
||||
|
||||
## :repeat
|
||||
|
||||
Scenario: :repeat simple
|
||||
When I run :repeat 5 scroll-px 10 0
|
||||
And I wait until the scroll position changed to 50/0
|
||||
# Then already covered by above And
|
||||
|
||||
Scenario: :repeat zero times
|
||||
When I run :repeat 0 scroll-px 10 0
|
||||
And I wait 0.01s
|
||||
Then the page should not be scrolled
|
||||
|
||||
## :run-with-count
|
||||
|
||||
Scenario: :run-with-count
|
||||
When I run :run-with-count 2 scroll down
|
||||
Then "command called: scroll ['down'] (count=2)" should be logged
|
||||
|
||||
Scenario: :run-with-count with count
|
||||
When I run :run-with-count 2 scroll down with count 3
|
||||
Then "command called: scroll ['down'] (count=6)" should be logged
|
||||
|
||||
## :message-*
|
||||
|
||||
Scenario: :message-error
|
||||
When I run :message-error "Hello World"
|
||||
Then the error "Hello World" should be shown
|
||||
|
||||
Scenario: :message-info
|
||||
When I run :message-info "Hello World"
|
||||
Then the message "Hello World" should be shown
|
||||
|
||||
Scenario: :message-warning
|
||||
When I run :message-warning "Hello World"
|
||||
Then the warning "Hello World" should be shown
|
||||
|
||||
# argparser again
|
||||
@xfail
|
||||
Scenario: :repeat negative times
|
||||
When I run :repeat -4 scroll-px 10 0
|
||||
Then the error "A negative count doesn't make sense." should be shown
|
||||
And the page should not be scrolled
|
||||
|
||||
## :debug-all-objects
|
||||
|
||||
Scenario: :debug-all-objects
|
||||
When I run :debug-all-objects
|
||||
Then "*Qt widgets - *Qt objects - *" should be logged
|
||||
|
||||
## :debug-cache-stats
|
||||
|
||||
Scenario: :debug-cache-stats
|
||||
When I run :debug-cache-stats
|
||||
Then "config: CacheInfo(*)" should be logged
|
||||
And "style: CacheInfo(*)" should be logged
|
||||
|
||||
## :debug-console
|
||||
|
||||
@no_xvfb
|
||||
Scenario: :debug-console smoke test
|
||||
When I run :debug-console
|
||||
And I wait for "Focus object changed: <qutebrowser.misc.consolewidget.ConsoleLineEdit *>" in the log
|
||||
And I run :debug-console
|
||||
And I wait for "Focus object changed: *" in the log
|
||||
Then "initializing debug console" should be logged
|
||||
And "showing debug console" should be logged
|
||||
And "hiding debug console" should be logged
|
||||
And no crash should happen
|
||||
|
||||
## :repeat-command
|
||||
|
||||
Scenario: :repeat-command
|
||||
When I run :scroll down
|
||||
And I run :repeat-command
|
||||
And I run :scroll up
|
||||
Then the page should be scrolled vertically
|
||||
|
||||
Scenario: :repeat-command with count
|
||||
When I run :scroll down with count 3
|
||||
And I wait until the scroll position changed
|
||||
And I run :scroll up
|
||||
And I wait until the scroll position changed
|
||||
And I run :repeat-command with count 2
|
||||
And I wait until the scroll position changed to 0/0
|
||||
Then the page should not be scrolled
|
||||
|
||||
Scenario: :repeat-command with not-normal command inbetween
|
||||
When I run :scroll down with count 3
|
||||
And I wait until the scroll position changed
|
||||
And I run :scroll up
|
||||
And I wait until the scroll position changed
|
||||
And I run :prompt-accept
|
||||
And I run :repeat-command with count 2
|
||||
And I wait until the scroll position changed to 0/0
|
||||
Then the page should not be scrolled
|
||||
And the error "prompt-accept: This command is only allowed in prompt/yesno mode, not normal." should be shown
|
||||
|
||||
@qtwebengine_createWindow
|
||||
Scenario: :repeat-command with mode-switching command
|
||||
When I open data/hints/link_blank.html
|
||||
And I run :tab-only
|
||||
And I hint with args "all"
|
||||
And I run :leave-mode
|
||||
And I run :repeat-command
|
||||
And I run :follow-hint a
|
||||
And I wait until data/hello.txt is loaded
|
||||
Then the following tabs should be open:
|
||||
- data/hints/link_blank.html
|
||||
- data/hello.txt (active)
|
||||
|
||||
## :debug-log-capacity
|
||||
|
||||
Scenario: Using :debug-log-capacity
|
||||
When I run :debug-log-capacity 100
|
||||
And I run :message-info oldstuff
|
||||
And I run :repeat 20 message-info otherstuff
|
||||
And I run :message-info newstuff
|
||||
And I open qute:log
|
||||
Then the page should contain the plaintext "newstuff"
|
||||
And the page should not contain the plaintext "oldstuff"
|
||||
|
||||
Scenario: Using :debug-log-capacity with negative capacity
|
||||
When I run :debug-log-capacity -1
|
||||
Then the error "Can't set a negative log capacity!" should be shown
|
||||
|
||||
## :debug-log-level / :debug-log-filter
|
||||
# Other :debug-log-{level,filter} features are tested in
|
||||
# unit/utils/test_log.py as using them would break end2end tests.
|
||||
|
||||
Scenario: Using debug-log-level with invalid level
|
||||
When I run :debug-log-level hello
|
||||
Then the error "level: Invalid value hello - expected one of: vdebug, debug, info, warning, error, critical" should be shown
|
||||
|
||||
Scenario: Using debug-log-filter with invalid filter
|
||||
When I run :debug-log-filter blah
|
||||
Then the error "filters: Invalid value blah - expected one of: statusbar, *" should be shown
|
@ -256,6 +256,19 @@ Feature: Yanking and pasting.
|
||||
# Compare
|
||||
Then the javascript message "textarea contents: Hello world" should be logged
|
||||
|
||||
Scenario: Inserting text into an empty text field with javascript disabled
|
||||
When I set general -> log-javascript-console to info
|
||||
And I set content -> allow-javascript to false
|
||||
And I open data/paste_primary.html
|
||||
And I run :click-element id qute-textarea
|
||||
And I wait for "Clicked editable element!" in the log
|
||||
And I run :insert-text Hello world
|
||||
And I run :jseval console.log("textarea contents: " + document.getElementById('qute-textarea').value);
|
||||
# Enable javascript again for the other tests
|
||||
And I set content -> allow-javascript to true
|
||||
# Compare
|
||||
Then the javascript message "textarea contents: Hello world" should be logged
|
||||
|
||||
Scenario: Inserting text into a text field at specific position
|
||||
When I set general -> log-javascript-console to info
|
||||
And I open data/paste_primary.html
|
||||
|
@ -152,7 +152,7 @@ def test_process_never_started(qtbot, quit_pyproc):
|
||||
def test_wait_signal_raising(qtbot):
|
||||
"""testprocess._wait_signal should raise by default."""
|
||||
proc = testprocess.Process()
|
||||
with pytest.raises(qtbot.SignalTimeoutError):
|
||||
with pytest.raises(qtbot.TimeoutError):
|
||||
with proc._wait_signal(proc.proc.started, timeout=0):
|
||||
pass
|
||||
|
||||
|
@ -67,12 +67,12 @@ class Request(testprocess.Line):
|
||||
'/favicon.ico': [http.client.NOT_FOUND],
|
||||
'/does-not-exist': [http.client.NOT_FOUND],
|
||||
'/does-not-exist-2': [http.client.NOT_FOUND],
|
||||
'/basic-auth/user/password':
|
||||
[http.client.UNAUTHORIZED, http.client.OK],
|
||||
'/status/404': [http.client.NOT_FOUND],
|
||||
|
||||
'/custom/redirect-later': [http.client.FOUND],
|
||||
'/custom/redirect-self': [http.client.FOUND],
|
||||
'/redirect-to': [http.client.FOUND],
|
||||
'/status/404': [http.client.NOT_FOUND],
|
||||
|
||||
'/cookies/set': [http.client.FOUND],
|
||||
}
|
||||
for i in range(15):
|
||||
@ -81,6 +81,10 @@ class Request(testprocess.Line):
|
||||
http.client.FOUND]
|
||||
path_to_statuses['/absolute-redirect/{}'.format(i)] = [
|
||||
http.client.FOUND]
|
||||
for suffix in ['', '1', '2']:
|
||||
key = '/basic-auth/user{}/password{}'.format(suffix, suffix)
|
||||
path_to_statuses[key] = [http.client.UNAUTHORIZED, http.client.OK]
|
||||
|
||||
default_statuses = [http.client.OK, http.client.NOT_MODIFIED]
|
||||
|
||||
sanitized = QUrl('http://localhost' + self.path).path() # Remove ?foo
|
||||
|
@ -131,8 +131,8 @@ def fake_statusbar(qtbot):
|
||||
statusbar.container = container
|
||||
vbox.addWidget(statusbar)
|
||||
|
||||
with qtbot.waitExposed(container):
|
||||
container.show()
|
||||
qtbot.waitForWindowShown(container)
|
||||
return statusbar
|
||||
|
||||
|
||||
|
@ -157,7 +157,7 @@ def pattern_match(*, pattern, value):
|
||||
True on a match, False otherwise.
|
||||
"""
|
||||
re_pattern = '.*'.join(re.escape(part) for part in pattern.split('*'))
|
||||
return re.fullmatch(re_pattern, value) is not None
|
||||
return re.fullmatch(re_pattern, value, flags=re.DOTALL) is not None
|
||||
|
||||
|
||||
def abs_datapath():
|
||||
|
@ -1,8 +1,8 @@
|
||||
<head>
|
||||
<script type="text/javascript">
|
||||
function prompter() {
|
||||
var reply = prompt("js prompt", "")
|
||||
alert("JS alert: " + reply + "!")
|
||||
var reply = prompt("js prompt", "");
|
||||
alert("JS alert: " + reply + "!");
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
|
@ -121,5 +121,5 @@ def test_tab(qtbot, view, config_stub, tab_registry, mode_manager):
|
||||
assert tab_w.history._history is view.history()
|
||||
assert view.parent() is tab_w
|
||||
|
||||
with qtbot.waitExposed(tab_w):
|
||||
tab_w.show()
|
||||
qtbot.waitForWindowShown(tab_w)
|
||||
|
@ -396,7 +396,7 @@ class TestDefaultConfig:
|
||||
If it did change, place a new qutebrowser-vx.y.z.conf in old_configs
|
||||
and then increment the version.
|
||||
"""
|
||||
assert qutebrowser.__version__ == '0.8.1'
|
||||
assert qutebrowser.__version__ == '0.8.4'
|
||||
|
||||
@pytest.mark.parametrize('filename',
|
||||
os.listdir(os.path.join(os.path.dirname(__file__), 'old_configs')),
|
||||
|
@ -59,24 +59,6 @@ class Obj(QObject):
|
||||
self.rendered_stylesheet = stylesheet
|
||||
|
||||
|
||||
class GeneratedObj(QObject):
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self.rendered_stylesheet = None
|
||||
self._generated = False
|
||||
|
||||
def setStyleSheet(self, stylesheet):
|
||||
self.rendered_stylesheet = stylesheet
|
||||
|
||||
def generate(self):
|
||||
if not self._generated:
|
||||
self._generated = True
|
||||
return 'one'
|
||||
else:
|
||||
return 'two'
|
||||
|
||||
|
||||
@pytest.mark.parametrize('delete', [True, False])
|
||||
def test_set_register_stylesheet(delete, qtbot, config_stub, caplog):
|
||||
config_stub.data = {'fonts': {'foo': 'bar'}, 'colors': {}}
|
||||
@ -105,15 +87,6 @@ def test_set_register_stylesheet(delete, qtbot, config_stub, caplog):
|
||||
assert obj.rendered_stylesheet == expected
|
||||
|
||||
|
||||
def test_set_register_stylesheet_generator(qtbot, config_stub):
|
||||
config_stub.data = {'fonts': {}, 'colors': {}}
|
||||
obj = GeneratedObj()
|
||||
style.set_register_stylesheet(obj, generator=obj.generate)
|
||||
assert obj.rendered_stylesheet == 'one'
|
||||
config_stub.changed.emit('foo', 'bar')
|
||||
assert obj.rendered_stylesheet == 'two'
|
||||
|
||||
|
||||
class TestColorDict:
|
||||
|
||||
@pytest.mark.parametrize('key, expected', [
|
||||
|
@ -1,57 +0,0 @@
|
||||
# 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/>.
|
||||
|
||||
"""Test Prompt widget."""
|
||||
|
||||
import sip
|
||||
|
||||
import pytest
|
||||
|
||||
from qutebrowser.mainwindow.statusbar.prompt import Prompt
|
||||
from qutebrowser.utils import objreg
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def prompt(qtbot, win_registry):
|
||||
prompt = Prompt(0)
|
||||
qtbot.addWidget(prompt)
|
||||
|
||||
yield prompt
|
||||
|
||||
# If we don't clean up here, this test will remove 'prompter' from the
|
||||
# objreg at some point in the future, which will cause some other test to
|
||||
# fail.
|
||||
sip.delete(prompt)
|
||||
|
||||
|
||||
def test_prompt(prompt):
|
||||
prompt.show()
|
||||
objreg.get('prompt', scope='window', window=0)
|
||||
objreg.get('prompter', scope='window', window=0)
|
||||
|
||||
|
||||
@pytest.mark.xfail(reason="This test is broken and I don't get why")
|
||||
def test_resizing(fake_statusbar, prompt):
|
||||
fake_statusbar.hbox.addWidget(prompt)
|
||||
|
||||
prompt.txt.setText("Blah?")
|
||||
old_width = prompt.lineedit.width()
|
||||
|
||||
prompt.lineedit.setText("Hello World" * 100)
|
||||
assert prompt.lineedit.width() > old_width
|
@ -51,7 +51,6 @@ def test_elided_text(fake_statusbar, qtbot, elidemode, check):
|
||||
long_string = 'Hello world! ' * 100
|
||||
label.setText(long_string)
|
||||
label.show()
|
||||
qtbot.waitForWindowShown(label)
|
||||
|
||||
assert check(label._elided_text)
|
||||
|
||||
@ -74,8 +73,8 @@ def test_resize(qtbot):
|
||||
long_string = 'Hello world! ' * 20
|
||||
label.setText(long_string)
|
||||
|
||||
with qtbot.waitExposed(label):
|
||||
label.show()
|
||||
qtbot.waitForWindowShown(label)
|
||||
|
||||
text_1 = label._elided_text
|
||||
label.resize(20, 50)
|
||||
|
@ -55,8 +55,8 @@ def view(qtbot, config_stub):
|
||||
usertypes.MessageLevel.warning,
|
||||
usertypes.MessageLevel.error])
|
||||
def test_single_message(qtbot, view, level):
|
||||
with qtbot.waitExposed(view):
|
||||
view.show_message(level, 'test')
|
||||
qtbot.waitForWindowShown(view)
|
||||
assert view._messages[0].isVisible()
|
||||
|
||||
|
||||
|
93
tests/unit/mainwindow/test_prompt.py
Normal file
93
tests/unit/mainwindow/test_prompt.py
Normal file
@ -0,0 +1,93 @@
|
||||
# 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/>.
|
||||
|
||||
import os
|
||||
|
||||
import pytest
|
||||
from PyQt5.QtCore import Qt
|
||||
|
||||
from qutebrowser.mainwindow import prompt as promptmod
|
||||
from qutebrowser.utils import usertypes
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def setup(qapp, key_config_stub):
|
||||
key_config_stub.set_bindings_for('prompt', {})
|
||||
|
||||
|
||||
class TestFileCompletion:
|
||||
|
||||
@pytest.fixture
|
||||
def get_prompt(self, qtbot):
|
||||
"""Get a function to display a prompt with a path."""
|
||||
def _get_prompt_func(path):
|
||||
question = usertypes.Question()
|
||||
question.title = "test"
|
||||
question.default = path
|
||||
|
||||
prompt = promptmod.DownloadFilenamePrompt(question)
|
||||
qtbot.add_widget(prompt)
|
||||
with qtbot.wait_signal(prompt._file_model.directoryLoaded):
|
||||
pass
|
||||
assert prompt._lineedit.text() == path
|
||||
|
||||
return prompt
|
||||
return _get_prompt_func
|
||||
|
||||
@pytest.mark.parametrize('steps, where, subfolder', [
|
||||
(1, 'next', '..'),
|
||||
(1, 'prev', 'c'),
|
||||
(2, 'next', 'a'),
|
||||
(2, 'prev', 'b'),
|
||||
])
|
||||
def test_simple_completion(self, tmpdir, get_prompt, steps, where,
|
||||
subfolder):
|
||||
"""Simply trying to tab through items."""
|
||||
for directory in 'abc':
|
||||
(tmpdir / directory).ensure(dir=True)
|
||||
|
||||
prompt = get_prompt(str(tmpdir) + os.sep)
|
||||
|
||||
for _ in range(steps):
|
||||
prompt.item_focus(where)
|
||||
|
||||
assert prompt._lineedit.text() == str(tmpdir / subfolder)
|
||||
|
||||
def test_backspacing_path(self, qtbot, tmpdir, get_prompt):
|
||||
"""When we start deleting a path we want to see the subdir."""
|
||||
for directory in ['bar', 'foo']:
|
||||
(tmpdir / directory).ensure(dir=True)
|
||||
|
||||
prompt = get_prompt(str(tmpdir / 'foo') + os.sep)
|
||||
|
||||
# Deleting /f[oo/]
|
||||
with qtbot.wait_signal(prompt._file_model.directoryLoaded):
|
||||
for _ in range(3):
|
||||
qtbot.keyPress(prompt._lineedit, Qt.Key_Backspace)
|
||||
|
||||
# We should now show / again, so tabbing twice gives us .. -> bar
|
||||
prompt.item_focus('next')
|
||||
prompt.item_focus('next')
|
||||
assert prompt._lineedit.text() == str(tmpdir / 'bar')
|
||||
|
||||
@pytest.mark.linux
|
||||
def test_root_path(self, get_prompt):
|
||||
"""With / as path, show root contents."""
|
||||
prompt = get_prompt('/')
|
||||
assert prompt._file_model.rootPath() == '/'
|
@ -72,5 +72,6 @@ class TestTabWidget:
|
||||
icon = QIcon(pixmap)
|
||||
tab = fake_web_tab()
|
||||
widget.addTab(tab, icon, 'foobar')
|
||||
|
||||
with qtbot.waitExposed(widget):
|
||||
widget.show()
|
||||
qtbot.waitForWindowShown(widget)
|
||||
|
@ -50,10 +50,13 @@ def keyhint(qtbot, config_stub, key_config_stub):
|
||||
'colors': {
|
||||
'keyhint.fg': 'white',
|
||||
'keyhint.fg.suffix': 'yellow',
|
||||
'keyhint.bg': 'black'
|
||||
'keyhint.bg': 'black',
|
||||
},
|
||||
'fonts': {'keyhint': 'Comic Sans'},
|
||||
'ui': {'keyhint-blacklist': '', 'status-position': 'bottom'},
|
||||
'ui': {
|
||||
'keyhint-blacklist': '',
|
||||
'status-position': 'bottom',
|
||||
},
|
||||
}
|
||||
keyhint = KeyHintView(0, None)
|
||||
qtbot.add_widget(keyhint)
|
||||
@ -63,8 +66,8 @@ def keyhint(qtbot, config_stub, key_config_stub):
|
||||
|
||||
def test_show_and_hide(qtbot, keyhint):
|
||||
with qtbot.waitSignal(keyhint.update_geometry):
|
||||
with qtbot.waitExposed(keyhint):
|
||||
keyhint.show()
|
||||
qtbot.waitForWindowShown(keyhint)
|
||||
keyhint.update_keyhint('normal', '')
|
||||
assert not keyhint.isVisible()
|
||||
|
||||
|
@ -21,14 +21,13 @@
|
||||
|
||||
import os
|
||||
import pytest
|
||||
from unittest import mock
|
||||
|
||||
from qutebrowser.misc import lineparser as lineparsermod
|
||||
|
||||
|
||||
class TestBaseLineParser:
|
||||
|
||||
"""Tests for BaseLineParser."""
|
||||
|
||||
CONFDIR = "this really doesn't matter"
|
||||
FILENAME = "and neither does this"
|
||||
|
||||
@ -53,6 +52,45 @@ class TestBaseLineParser:
|
||||
lineparser._prepare_save()
|
||||
os_mock.makedirs.assert_called_with(self.CONFDIR, 0o755)
|
||||
|
||||
def test_prepare_save_no_config(self, mocker):
|
||||
"""Test if _prepare_save doesn't create a None config dir."""
|
||||
os_mock = mocker.patch('qutebrowser.misc.lineparser.os')
|
||||
os_mock.path.exists.return_value = True
|
||||
|
||||
lineparser = lineparsermod.BaseLineParser(None, self.FILENAME)
|
||||
assert not lineparser._prepare_save()
|
||||
assert not os_mock.makedirs.called
|
||||
|
||||
def test_double_open(self, mocker, lineparser):
|
||||
"""Test if _open refuses reentry."""
|
||||
mocker.patch('builtins.open', mock.mock_open())
|
||||
|
||||
with lineparser._open('r'):
|
||||
with pytest.raises(IOError) as excinf:
|
||||
with lineparser._open('r'):
|
||||
pass
|
||||
assert str(excinf.value) == 'Refusing to double-open AppendLineParser.'
|
||||
|
||||
def test_binary(self, mocker):
|
||||
"""Test if _open and _write correctly handle binary files."""
|
||||
open_mock = mock.mock_open()
|
||||
mocker.patch('builtins.open', open_mock)
|
||||
|
||||
testdata = b'\xf0\xff'
|
||||
|
||||
lineparser = lineparsermod.BaseLineParser(
|
||||
self.CONFDIR, self.FILENAME, binary=True)
|
||||
with lineparser._open('r') as f:
|
||||
lineparser._write(f, [testdata])
|
||||
|
||||
open_mock.assert_called_once_with(
|
||||
os.path.join(self.CONFDIR, self.FILENAME), 'rb')
|
||||
|
||||
open_mock().write.assert_has_calls([
|
||||
mock.call(testdata),
|
||||
mock.call(b'\n')
|
||||
])
|
||||
|
||||
|
||||
class TestLineParser:
|
||||
|
||||
@ -63,7 +101,18 @@ class TestLineParser:
|
||||
lp.save()
|
||||
return lp
|
||||
|
||||
def test_init(self, tmpdir):
|
||||
"""Test if creating a line parser correctly reads its file."""
|
||||
(tmpdir / 'file').write('one\ntwo\n')
|
||||
lineparser = lineparsermod.LineParser(str(tmpdir), 'file')
|
||||
assert lineparser.data == ['one', 'two']
|
||||
|
||||
(tmpdir / 'file').write_binary(b'\xfe\n\xff\n')
|
||||
lineparser = lineparsermod.LineParser(str(tmpdir), 'file', binary=True)
|
||||
assert lineparser.data == [b'\xfe', b'\xff']
|
||||
|
||||
def test_clear(self, tmpdir, lineparser):
|
||||
"""Test if clear() empties its file."""
|
||||
lineparser.data = ['one', 'two']
|
||||
lineparser.save()
|
||||
assert (tmpdir / 'file').read() == 'one\ntwo\n'
|
||||
@ -71,11 +120,23 @@ class TestLineParser:
|
||||
assert not lineparser.data
|
||||
assert (tmpdir / 'file').read() == ''
|
||||
|
||||
def test_double_open(self, lineparser):
|
||||
"""Test if save() bails on an already open file."""
|
||||
with lineparser._open('r'):
|
||||
with pytest.raises(IOError):
|
||||
lineparser.save()
|
||||
|
||||
def test_prepare_save(self, tmpdir, lineparser):
|
||||
"""Test if save() bails when _prepare_save() returns False."""
|
||||
(tmpdir / 'file').write('pristine\n')
|
||||
lineparser.data = ['changed']
|
||||
lineparser._prepare_save = lambda: False
|
||||
lineparser.save()
|
||||
assert (tmpdir / 'file').read() == 'pristine\n'
|
||||
|
||||
|
||||
class TestAppendLineParser:
|
||||
|
||||
"""Tests for AppendLineParser."""
|
||||
|
||||
BASE_DATA = ['old data 1', 'old data 2']
|
||||
|
||||
@pytest.fixture
|
||||
@ -97,7 +158,17 @@ class TestAppendLineParser:
|
||||
lineparser.save()
|
||||
assert (tmpdir / 'file').read() == self._get_expected(new_data)
|
||||
|
||||
def test_save_without_configdir(self, tmpdir, lineparser):
|
||||
"""Test save() failing because no configdir was set."""
|
||||
new_data = ['new data 1', 'new data 2']
|
||||
lineparser.new_data = new_data
|
||||
lineparser._configdir = None
|
||||
assert not lineparser.save()
|
||||
# make sure new data is still there
|
||||
assert lineparser.new_data == new_data
|
||||
|
||||
def test_clear(self, tmpdir, lineparser):
|
||||
"""Check if calling clear() empties both pending and persisted data."""
|
||||
lineparser.new_data = ['one', 'two']
|
||||
lineparser.save()
|
||||
assert (tmpdir / 'file').read() == "old data 1\nold data 2\none\ntwo\n"
|
||||
@ -108,6 +179,14 @@ class TestAppendLineParser:
|
||||
assert not lineparser.new_data
|
||||
assert (tmpdir / 'file').read() == ""
|
||||
|
||||
def test_clear_without_configdir(self, tmpdir, lineparser):
|
||||
"""Test clear() failing because no configdir was set."""
|
||||
new_data = ['new data 1', 'new data 2']
|
||||
lineparser.new_data = new_data
|
||||
lineparser._configdir = None
|
||||
assert not lineparser.clear()
|
||||
assert lineparser.new_data == new_data
|
||||
|
||||
def test_iter_without_open(self, lineparser):
|
||||
"""Test __iter__ without having called open()."""
|
||||
with pytest.raises(ValueError):
|
||||
|
148
tests/unit/misc/test_utilcmds.py
Normal file
148
tests/unit/misc/test_utilcmds.py
Normal file
@ -0,0 +1,148 @@
|
||||
# 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/>.
|
||||
|
||||
"""Tests for qutebrowser.misc.utilcmds."""
|
||||
|
||||
import contextlib
|
||||
import logging
|
||||
import os
|
||||
import pytest
|
||||
import signal
|
||||
import time
|
||||
|
||||
from qutebrowser.misc import utilcmds
|
||||
|
||||
from qutebrowser.commands import cmdexc
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def _trapped_segv(handler):
|
||||
"""Temporarily install given signal handler for SIGSEGV."""
|
||||
old_handler = signal.signal(signal.SIGSEGV, handler)
|
||||
yield
|
||||
signal.signal(signal.SIGSEGV, old_handler)
|
||||
|
||||
|
||||
def test_debug_crash_exception():
|
||||
"""Verify that debug_crash crashes as intended."""
|
||||
with pytest.raises(Exception) as excinfo:
|
||||
utilcmds.debug_crash(typ='exception')
|
||||
assert str(excinfo.value) == 'Forced crash'
|
||||
|
||||
|
||||
@pytest.mark.skipif(os.name == 'nt',
|
||||
reason="current CPython/win can't recover from SIGSEGV")
|
||||
def test_debug_crash_segfault():
|
||||
"""Verify that debug_crash crashes as intended."""
|
||||
caught = False
|
||||
|
||||
def _handler(num, frame):
|
||||
"""Temporary handler for segfault."""
|
||||
nonlocal caught
|
||||
caught = num == signal.SIGSEGV
|
||||
|
||||
with _trapped_segv(_handler):
|
||||
# since we handle the segfault, execution will continue and run into
|
||||
# the "Segfault failed (wat.)" Exception
|
||||
with pytest.raises(Exception) as excinfo:
|
||||
utilcmds.debug_crash(typ='segfault')
|
||||
time.sleep(0.001)
|
||||
assert caught
|
||||
assert 'Segfault failed' in str(excinfo.value)
|
||||
|
||||
|
||||
def test_debug_trace(mocker):
|
||||
"""Check if hunter.trace is properly called."""
|
||||
# but only if hunter is available
|
||||
pytest.importorskip('hunter')
|
||||
hunter_mock = mocker.patch('qutebrowser.misc.utilcmds.hunter')
|
||||
utilcmds.debug_trace(1)
|
||||
assert hunter_mock.trace.assert_called_with(1)
|
||||
|
||||
|
||||
def test_debug_trace_exception(mocker):
|
||||
"""Check that exceptions thrown by hunter.trace are handled."""
|
||||
def _mock_exception():
|
||||
"""Side effect for testing debug_trace's reraise."""
|
||||
raise Exception('message')
|
||||
|
||||
hunter_mock = mocker.patch('qutebrowser.misc.utilcmds.hunter')
|
||||
hunter_mock.trace.side_effect = _mock_exception
|
||||
with pytest.raises(cmdexc.CommandError) as excinfo:
|
||||
utilcmds.debug_trace()
|
||||
assert str(excinfo.value) == 'Exception: message'
|
||||
|
||||
|
||||
def test_debug_trace_no_hunter(monkeypatch):
|
||||
"""Test that an error is shown if debug_trace is called without hunter."""
|
||||
monkeypatch.setattr(utilcmds, 'hunter', None)
|
||||
with pytest.raises(cmdexc.CommandError) as excinfo:
|
||||
utilcmds.debug_trace()
|
||||
assert str(excinfo.value) == "You need to install 'hunter' to use this " \
|
||||
"command!"
|
||||
|
||||
|
||||
def test_repeat_command_initial(mocker, mode_manager):
|
||||
"""Test repeat_command first-time behavior.
|
||||
|
||||
If :repeat-command is called initially, it should err, because there's
|
||||
nothing to repeat.
|
||||
"""
|
||||
objreg_mock = mocker.patch('qutebrowser.misc.utilcmds.objreg')
|
||||
objreg_mock.get.return_value = mode_manager
|
||||
with pytest.raises(cmdexc.CommandError) as excinfo:
|
||||
utilcmds.repeat_command(win_id=0)
|
||||
assert str(excinfo.value) == "You didn't do anything yet."
|
||||
|
||||
|
||||
def test_debug_log_level(mocker):
|
||||
"""Test interactive log level changing."""
|
||||
formatter_mock = mocker.patch(
|
||||
'qutebrowser.misc.utilcmds.log.change_console_formatter')
|
||||
handler_mock = mocker.patch(
|
||||
'qutebrowser.misc.utilcmds.log.console_handler')
|
||||
utilcmds.debug_log_level(level='debug')
|
||||
formatter_mock.assert_called_with(logging.DEBUG)
|
||||
handler_mock.setLevel.assert_called_with(logging.DEBUG)
|
||||
|
||||
|
||||
class FakeWindow:
|
||||
|
||||
"""Mock class for window_only."""
|
||||
|
||||
def __init__(self, deleted=False):
|
||||
self.closed = False
|
||||
self.deleted = deleted
|
||||
|
||||
def close(self):
|
||||
"""Flag as closed."""
|
||||
self.closed = True
|
||||
|
||||
|
||||
def test_window_only(mocker, monkeypatch):
|
||||
"""Verify that window_only doesn't close the current or deleted windows."""
|
||||
test_windows = {0: FakeWindow(), 1: FakeWindow(True), 2: FakeWindow()}
|
||||
winreg_mock = mocker.patch('qutebrowser.misc.utilcmds.objreg')
|
||||
winreg_mock.window_registry = test_windows
|
||||
sip_mock = mocker.patch('qutebrowser.misc.utilcmds.sip')
|
||||
sip_mock.isdeleted.side_effect = lambda window: window.deleted
|
||||
utilcmds.window_only(current_win_id=0)
|
||||
assert not test_windows[0].closed
|
||||
assert not test_windows[1].closed
|
||||
assert test_windows[2].closed
|
@ -771,7 +771,7 @@ class TestPyQIODevice:
|
||||
with pytest.raises(io.UnsupportedOperation):
|
||||
pyqiodev.seek(0, whence)
|
||||
|
||||
@pytest.mark.flaky(reruns=1)
|
||||
@pytest.mark.flaky()
|
||||
def test_qprocess(self, py_proc):
|
||||
"""Test PyQIODevice with a QProcess which is non-sequential.
|
||||
|
||||
|
7
tox.ini
7
tox.ini
@ -15,7 +15,6 @@ setenv =
|
||||
PYTEST_QT_API=pyqt5
|
||||
passenv = PYTHON DISPLAY XAUTHORITY HOME USERNAME USER CI TRAVIS XDG_* QUTE_*
|
||||
deps =
|
||||
-r{toxinidir}/misc/requirements/requirements-pip.txt
|
||||
-r{toxinidir}/requirements.txt
|
||||
-r{toxinidir}/misc/requirements/requirements-tests.txt
|
||||
commands =
|
||||
@ -93,7 +92,6 @@ commands =
|
||||
[testenv:vulture]
|
||||
basepython = python3
|
||||
deps =
|
||||
-r{toxinidir}/misc/requirements/requirements-pip.txt
|
||||
-r{toxinidir}/requirements.txt
|
||||
-r{toxinidir}/misc/requirements/requirements-vulture.txt
|
||||
setenv = PYTHONPATH={toxinidir}
|
||||
@ -137,7 +135,6 @@ commands =
|
||||
basepython = python3
|
||||
passenv =
|
||||
deps =
|
||||
-r{toxinidir}/misc/requirements/requirements-pip.txt
|
||||
-r{toxinidir}/misc/requirements/requirements-pyroma.txt
|
||||
commands =
|
||||
{envdir}/bin/pyroma .
|
||||
@ -146,7 +143,6 @@ commands =
|
||||
basepython = python3
|
||||
passenv =
|
||||
deps =
|
||||
-r{toxinidir}/misc/requirements/requirements-pip.txt
|
||||
-r{toxinidir}/misc/requirements/requirements-check-manifest.txt
|
||||
commands =
|
||||
{envdir}/bin/check-manifest --ignore 'qutebrowser/git-commit-id,qutebrowser/html/doc,qutebrowser/html/doc/*,*/__pycache__'
|
||||
@ -156,7 +152,6 @@ basepython = python3
|
||||
whitelist_externals = git
|
||||
passenv = TRAVIS_PULL_REQUEST
|
||||
deps =
|
||||
-r{toxinidir}/misc/requirements/requirements-pip.txt
|
||||
-r{toxinidir}/requirements.txt
|
||||
commands =
|
||||
{envpython} scripts/link_pyqt.py --tox {envdir}
|
||||
@ -169,7 +164,6 @@ commands =
|
||||
# fail if we didn't have a fallback defined.
|
||||
basepython = {env:PYTHON:}/python.exe
|
||||
deps =
|
||||
-r{toxinidir}/misc/requirements/requirements-pip.txt
|
||||
-r{toxinidir}/requirements.txt
|
||||
-r{toxinidir}/misc/requirements/requirements-cxfreeze.txt
|
||||
commands =
|
||||
@ -180,7 +174,6 @@ commands =
|
||||
[testenv:pyinstaller]
|
||||
basepython = python3
|
||||
deps =
|
||||
-r{toxinidir}/misc/requirements/requirements-pip.txt
|
||||
-r{toxinidir}/requirements.txt
|
||||
-r{toxinidir}/misc/requirements/requirements-pyinstaller.txt
|
||||
commands =
|
||||
|
@ -8,6 +8,7 @@
|
||||
</div>
|
||||
<div id="menu">
|
||||
<a href="/index.html">Home</a>
|
||||
<a href="/doc/help/">Help</a>
|
||||
<a href="/FAQ.html">FAQ</a>
|
||||
<a href="/INSTALL.html">Install</a>
|
||||
<a href="/CHANGELOG.html">Changelog</a>
|
||||
|
47
www/qute.css
47
www/qute.css
@ -7,6 +7,15 @@ body {
|
||||
font-family: "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||
-webkit-text-size-adjust: none;
|
||||
color: #333333;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 6px 0;
|
||||
}
|
||||
|
||||
#preamble > .sectionbody > .paragraph > p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
#header {
|
||||
@ -67,8 +76,17 @@ body {
|
||||
background-color: #1e89c6;
|
||||
}
|
||||
|
||||
.sect1 {
|
||||
.sect1,
|
||||
div.footnote {
|
||||
padding: 10px 40px;
|
||||
margin: 0 auto;
|
||||
max-width: 1200px;
|
||||
}
|
||||
|
||||
#footnotes > hr {
|
||||
max-width: 1200px;
|
||||
margin: 20px auto 10px;
|
||||
width: calc(100% - 80px);
|
||||
}
|
||||
|
||||
.sect2 {
|
||||
@ -76,7 +94,7 @@ body {
|
||||
}
|
||||
|
||||
div.footnote {
|
||||
padding: 10px 40px;
|
||||
padding: 10px 40px 30px;
|
||||
}
|
||||
|
||||
hr {
|
||||
@ -136,6 +154,29 @@ code {
|
||||
padding: 10px 10px;
|
||||
background-color: #DDDDDD;
|
||||
border-radius: 4px;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
/*Display table as something that is not a table*/
|
||||
.admonitionblock table,
|
||||
.admonitionblock tbody,
|
||||
.admonitionblock tr,
|
||||
.admonitionblock td {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.admonitionblock td {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.admonitionblock .title {
|
||||
color: #0A396E;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.admonitionblock td.content {
|
||||
padding-left: 10px;
|
||||
border-left: 1px solid #ccc;
|
||||
}
|
||||
|
||||
table td {
|
||||
@ -174,7 +215,7 @@ table td {
|
||||
}
|
||||
|
||||
</style>
|
||||
<meta name="viewport" content="width=device-width; initial-scale=1.0; maximum-scale=1.0;" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0" />
|
||||
<link href="media/font.css" rel="stylesheet" type="text/css" />
|
||||
<link rel="icon" href="media/favicon.png" type="image/png" />
|
||||
<style type="text/css">
|
||||
|
Loading…
Reference in New Issue
Block a user