This commit is contained in:
Jan Verbeek 2016-11-09 12:17:04 +01:00
commit e5dec949b0
99 changed files with 2289 additions and 1233 deletions

View File

@ -23,18 +23,12 @@ exclude = .*,__pycache__,resources.py
# D402: First line should not be function's signature (false-positives) # D402: First line should not be function's signature (false-positives)
# D403: First word of the first line should be properly capitalized # D403: First word of the first line should be properly capitalized
# (false-positives) # (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 = ignore =
E128,E226,E265,E501,E402,E266,E731, E128,E226,E265,E501,E402,E266,E731,
F401, F401,
N802, N802,
P101,P102,P103, P101,P102,P103,
D102,D103,D104,D105,D209,D211,D402,D403, D102,D103,D104,D105,D209,D211,D402,D403
H101,H201,H238,H301,H306
min-version = 3.4.0 min-version = 3.4.0
max-complexity = 12 max-complexity = 12
putty-auto-ignore = True putty-auto-ignore = True

11
.gitignore vendored
View File

@ -33,7 +33,12 @@ __pycache__
/prof /prof
/venv /venv
TODO TODO
/scripts/testbrowser_cpp/Makefile /scripts/testbrowser_cpp/webkit/Makefile
/scripts/testbrowser_cpp/main.o /scripts/testbrowser_cpp/webkit/main.o
/scripts/testbrowser_cpp/testbrowser /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 /scripts/dev/pylint_checkers/qute_pylint.egg-info

View File

@ -139,6 +139,7 @@ Changed
- The `qute:settings` page now also shows option descriptions. - The `qute:settings` page now also shows option descriptions.
- `qute:version` and `qutebrowser --version` now show various important paths - `qute:version` and `qutebrowser --version` now show various important paths
- `:spawn`/userscripts now show a nicer error when a script wasn't found - `:spawn`/userscripts now show a nicer error when a script wasn't found
- Various functionality now works when javascript is disabled with QtWebKit
Deprecated Deprecated
~~~~~~~~~~ ~~~~~~~~~~
@ -168,23 +169,15 @@ Removed
Fixed 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. - `:undo` now doesn't undo tabs "closed" by `:tab-detach` anymore.
- Fixed an issue with hint chars not being cleared correctly when leaving hint - Fixed an issue with hint chars not being cleared correctly when leaving hint
mode. mode.
- `:tab-detach` now fails correctly when there's only one tab open. - `:tab-detach` now fails correctly when there's only one tab open.
- Various small issues with the command completion - 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 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 Fixed
~~~~~ ~~~~~
@ -193,6 +186,15 @@ Fixed
- Fixed `:open-editor` (`<Ctrl-e>`) on Windows - Fixed `:open-editor` (`<Ctrl-e>`) on Windows
- Fixed crash when setting `general -> auto-save-interval` to a too big value. - Fixed crash when setting `general -> auto-save-interval` to a too big value.
- Fixed crash when using hints on Void Linux. - 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 v0.8.2
------ ------

View File

@ -526,6 +526,21 @@ generate code and subsequently overwrite part or all of it. Running with all
will slow Valgrind down noticeably. 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 Style conventions
----------------- -----------------

View File

@ -272,18 +272,12 @@ qutebrowser from source.
==== Homebrew ==== Homebrew
For Homebrew, a few extra steps are necessary since Homebrew dropped QtWebKit Homebrew's builds of Qt and PyQt no longer include QtWebKit, so it is necessary
from Qt 5.6 - however, some users reported this didn't work for them, so using to build from source. The build takes several hours on an average laptop.
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:
---- ----
$ brew install python3 d-bus mysql sip xz $ brew install qt5 --with-qtwebkit
$ brew install homebrew/versions/qt55 $ brew install -s pyqt5
$ brew install --ignore-dependencies pyqt5
$ ln -s /usr/local/opt/qt55 /usr/local/opt/qt5
$ pip3.5 install qutebrowser $ pip3.5 install qutebrowser
---- ----

View File

@ -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://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"] 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 // QUTE_WEB_HIDE_END
qutebrowser is a keyboard-focused browser with a minimal GUI. It's based 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 In addition to the topics mentioned in this README, the following documents are
available: available:
* A http://qutebrowser.org/img/cheatsheet-big.png[key binding cheatsheet]: + * A https://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"] 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] * link:doc/quickstart.asciidoc[Quick start guide]
* A https://www.shortcutfoo.com/app/dojos/qutebrowser[free training course] to remember those key bindings. * A https://www.shortcutfoo.com/app/dojos/qutebrowser[free training course] to remember those key bindings.
* link:FAQ.asciidoc[Frequently asked questions] * 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[]. mailto:qutebrowser@lists.qutebrowser.org[].
For security bugs, please contact me directly at mail@qutebrowser.org, GPG ID For security bugs, please contact me directly at mail@qutebrowser.org, GPG ID
http://www.the-compiler.org/pubkey.asc[0xFD55A072]. https://www.the-compiler.org/pubkey.asc[0xFD55A072].
Requirements Requirements
------------ ------------
@ -155,6 +155,8 @@ Contributors, sorted by the number of commits in descending order:
* Alexander Cogneau * Alexander Cogneau
* Felix Van der Jeugt * Felix Van der Jeugt
* Martin Tournoij * Martin Tournoij
* Daniel Karbach
* Kevin Velghe
* Raphael Pierzina * Raphael Pierzina
* Joel Torstensson * Joel Torstensson
* Patric Schmitz * Patric Schmitz
@ -162,15 +164,14 @@ Contributors, sorted by the number of commits in descending order:
* Claude * Claude
* Corentin Julé * Corentin Julé
* meles5 * meles5
* Kevin Velghe
* Philipp Hansch * Philipp Hansch
* Daniel Karbach
* Panagiotis Ktistakis * Panagiotis Ktistakis
* Artur Shaik * Artur Shaik
* Nathan Isom * Nathan Isom
* Thorsten Wißmann * Thorsten Wißmann
* Austin Anderson * Austin Anderson
* Jimmy * Jimmy
* Spreadyy
* Niklas Haas * Niklas Haas
* Alexey "Averrin" Nabrodov * Alexey "Averrin" Nabrodov
* nanjekyejoannah * nanjekyejoannah
@ -188,6 +189,7 @@ Contributors, sorted by the number of commits in descending order:
* error800 * error800
* Michael Hoang * Michael Hoang
* Liam BEGUIN * Liam BEGUIN
* Julie Engel
* skinnay * skinnay
* Zach-Button * Zach-Button
* Tomasz Kramkowski * Tomasz Kramkowski
@ -211,7 +213,6 @@ Contributors, sorted by the number of commits in descending order:
* jnphilipp * jnphilipp
* Tobias Patzl * Tobias Patzl
* Stefan Tatschner * Stefan Tatschner
* Spreadyy
* Samuel Loury * Samuel Loury
* Peter Michely * Peter Michely
* Panashe M. Fundira * Panashe M. Fundira
@ -228,9 +229,11 @@ Contributors, sorted by the number of commits in descending order:
* Regina Hug * Regina Hug
* Mathias Fussenegger * Mathias Fussenegger
* Marcelo Santos * Marcelo Santos
* Joel Bradshaw
* Jean-Louis Fuchs * Jean-Louis Fuchs
* Fritz V155 Reichwald * Fritz V155 Reichwald
* Franz Fellner * Franz Fellner
* Eric Drechsel
* zwarag * zwarag
* xd1le * xd1le
* rsteube * rsteube
@ -256,7 +259,6 @@ Contributors, sorted by the number of commits in descending order:
* Marcel Schilling * Marcel Schilling
* Lazlow Carmichael * Lazlow Carmichael
* Ján Kobezda * Ján Kobezda
* Julie Engel
* Johannes Martinsson * Johannes Martinsson
* Jean-Christophe Petkovich * Jean-Christophe Petkovich
* Jay Kamat * Jay Kamat

View File

@ -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. |<<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. |<<open-editor,open-editor>>|Open an external editor with the currently selected form field.
|<<prompt-accept,prompt-accept>>|Accept the current prompt. |<<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. |<<prompt-open-download,prompt-open-download>>|Immediately open a download.
|<<repeat-command,repeat-command>>|Repeat the last executed command. |<<repeat-command,repeat-command>>|Repeat the last executed command.
|<<rl-backward-char,rl-backward-char>>|Move back a character. |<<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. * +'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]]
=== prompt-open-download === prompt-open-download
Syntax: +:prompt-open-download ['cmdline']+ Syntax: +:prompt-open-download ['cmdline']+

View File

@ -6,13 +6,13 @@ Documentation
The following help pages are currently available: The following help pages are currently available:
* link:quickstart.html[Quick start guide] * link:../quickstart.html[Quick start guide]
* link:FAQ.html[Frequently asked questions] * link:../../FAQ.html[Frequently asked questions]
* link:CHANGELOG.html[Change Log] * link:../../CHANGELOG.html[Change Log]
* link:commands.html[Documentation of commands] * link:commands.html[Documentation of commands]
* link:settings.html[Documentation of settings] * link:settings.html[Documentation of settings]
* link:userscripts.html[How to write userscripts] * link:../userscripts.html[How to write userscripts]
* link:CONTRIBUTING.html[Contributing to qutebrowser] * link:../../CONTRIBUTING.html[Contributing to qutebrowser]
Getting help Getting help
------------ ------------

View File

@ -54,6 +54,7 @@
|<<ui-modal-js-dialog,modal-js-dialog>>|Use standard JavaScript modal dialog for alert() and confirm() |<<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-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-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'' .Quick reference for section ``network''
@ -213,8 +214,6 @@
|<<colors-completion.scrollbar.bg,completion.scrollbar.bg>>|Color of the scrollbar in completion view |<<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.fg,statusbar.fg>>|Foreground color of the statusbar.
|<<colors-statusbar.bg,statusbar.bg>>|Background 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.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.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. |<<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.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.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-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'' .Quick reference for section ``fonts''
@ -296,6 +298,7 @@
|<<fonts-messages.error,messages.error>>|Font used for error messages. |<<fonts-messages.error,messages.error>>|Font used for error messages.
|<<fonts-messages.warning,messages.warning>>|Font used for warning messages. |<<fonts-messages.warning,messages.warning>>|Font used for warning messages.
|<<fonts-messages.info,messages.info>>|Font used for info messages. |<<fonts-messages.info,messages.info>>|Font used for info messages.
|<<fonts-prompts,prompts>>|Font used for prompts.
|============== |==============
== general == general
@ -317,7 +320,7 @@ Default: +pass:[smart]+
=== startpage === startpage
The default page(s) to open at the start, separated by commas. 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]] [[general-yank-ignored-url-parameters]]
=== yank-ignored-url-parameters === yank-ignored-url-parameters
@ -706,6 +709,12 @@ Globs are supported, so ';*' will blacklist all keychainsstarting with ';'. Use
Default: empty Default: empty
[[ui-prompt-radius]]
=== prompt-radius
The rounding radius for the edges of prompts.
Default: +pass:[8]+
== network == network
Settings related to the network. Settings related to the network.
@ -1560,7 +1569,7 @@ The file can be in one of the following formats:
- One host per line - 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). - 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&amp;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&amp;mimetype=plaintext]+
[[content-host-blocking-enabled]] [[content-host-blocking-enabled]]
=== 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. Aliases for commands.
By default, no aliases are defined. Example which adds a new command `:qtb` to open qutebrowsers website: 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
Colors used in the UI. Colors used in the UI.
@ -1831,18 +1840,6 @@ Background color of the statusbar.
Default: +pass:[black]+ 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]] [[colors-statusbar.fg.insert]]
=== statusbar.fg.insert === statusbar.fg.insert
Foreground color of the statusbar in insert mode. Foreground color of the statusbar in insert mode.
@ -2184,6 +2181,24 @@ Border color of an info message.
Default: +pass:[#333333]+ 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
Fonts used for the UI, with optional style/weight/size. Fonts used for the UI, with optional style/weight/size.
@ -2322,3 +2337,9 @@ Default: +pass:[8pt ${_monospace}]+
Font used for info messages. Font used for info messages.
Default: +pass:[8pt ${_monospace}]+ Default: +pass:[8pt ${_monospace}]+
[[fonts-prompts]]
=== prompts
Font used for prompts.
Default: +pass:[8pt sans-serif]+

View File

@ -7,7 +7,7 @@
:man source: qutebrowser :man source: qutebrowser
:man manual: qutebrowser manpage :man manual: qutebrowser manpage
:toc: :toc:
:homepage: http://www.qutebrowser.org/ :homepage: https://www.qutebrowser.org/
== NAME == NAME
qutebrowser - a keyboard-driven, vim-like browser based on PyQt5 and QtWebKit. 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. mailto:qutebrowser@lists.qutebrowser.org[] instead.
For security bugs, please contact me directly at me@the-compiler.org, GPG ID 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 == COPYRIGHT
This program is free software: you can redistribute it and/or modify it under 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/>. this program. If not, see <http://www.gnu.org/licenses/>.
== RESOURCES == RESOURCES
* Website: http://www.qutebrowser.org/ * Website: https://www.qutebrowser.org/
* Mailinglist: mailto:qutebrowser@lists.qutebrowser.org[] / * Mailinglist: mailto:qutebrowser@lists.qutebrowser.org[] /
https://lists.schokokeks.org/mailman/listinfo.cgi/qutebrowser https://lists.schokokeks.org/mailman/listinfo.cgi/qutebrowser
* Announce-only mailinglist: mailto:qutebrowser-announce@lists.qutebrowser.org[] / * Announce-only mailinglist: mailto:qutebrowser-announce@lists.qutebrowser.org[] /

View File

@ -2539,7 +2539,7 @@
y="10" y="10"
style="font-size:10px" /></flowRegion><flowPara style="font-size:10px" /></flowRegion><flowPara
id="flowPara5604" 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" id="flowPara5595"
style="font-size:13px">IRC: #qutebrowser on Freenode</flowPara><flowPara style="font-size:13px">IRC: #qutebrowser on Freenode</flowPara><flowPara
id="flowPara5597" id="flowPara5597"

Before

Width:  |  Height:  |  Size: 137 KiB

After

Width:  |  Height:  |  Size: 137 KiB

View File

@ -71,4 +71,5 @@ app = BUNDLE(coll,
name='qutebrowser.app', name='qutebrowser.app',
icon=icon, icon=icon,
info_plist={'NSHighResolutionCapable': 'True'}, 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')

View File

@ -3,22 +3,19 @@
flake8==2.6.2 # rq.filter: < 3.0.0 flake8==2.6.2 # rq.filter: < 3.0.0
flake8-copyright==0.2.0 flake8-copyright==0.2.0
flake8-debugger==1.4.0 # rq.filter: != 2.0.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-docstrings==1.0.2
flake8-future-import==0.4.3 flake8-future-import==0.4.3
flake8-mock==0.3 flake8-mock==0.3
flake8-pep3101==0.4 flake8-pep3101==0.6
flake8-putty==0.4.0 flake8-putty==0.4.0
flake8-string-format==0.2.3 flake8-string-format==0.2.3
flake8-tidy-imports==1.0.2 flake8-tidy-imports==1.0.3
flake8-tuple==0.2.12 flake8-tuple==0.2.12
hacking==0.11.0
mccabe==0.5.2 mccabe==0.5.2
packaging==16.7 packaging==16.8
pbr==1.10.0
pep8==1.7.0
pep8-naming==0.4.1 pep8-naming==0.4.1
pycodestyle==2.0.0 pycodestyle==2.1.0
pydocstyle==1.1.1 pydocstyle==1.1.1
pyflakes==1.3.0 pyflakes==1.3.0
pyparsing==2.1.10 pyparsing==2.1.10

View File

@ -10,15 +10,14 @@ flake8-putty
flake8-string-format flake8-string-format
flake8-tidy-imports flake8-tidy-imports
flake8-tuple flake8-tuple
hacking
pep8-naming pep8-naming
pydocstyle pydocstyle
pyflakes pyflakes
# Pinned to 1.5.7 by hacking otherwise # Pinned to 2.0.0 otherwise
pep8==1.7.0 pycodestyle==2.1.0
# Waiting until hacking/flake8-tuple are updated # Waiting until flake8-putty updated
#@ filter: flake8 < 3.0.0 #@ filter: flake8 < 3.0.0
# https://github.com/JBKahn/flake8-debugger/issues/5 # https://github.com/JBKahn/flake8-debugger/issues/5

View File

@ -1,2 +0,0 @@
pip==8.1.2
setuptools==28.6.0

View File

@ -1,3 +1,3 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py # 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

View File

@ -1 +1,2 @@
PyInstaller # https://github.com/pyinstaller/pyinstaller/pull/2238
-e git+https://github.com/edrex/pyinstaller.git@1984_add_QtWebEngineCore#egg=PyInstaller

View File

@ -1,4 +1,4 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py # This file is automatically generated by scripts/dev/recompile_requirements.py
docutils==0.12 docutils==0.12
pyroma==2.0.2 pyroma==2.2

View File

@ -6,32 +6,29 @@ click==6.6
coverage==4.2 coverage==4.2
decorator==4.0.10 decorator==4.0.10
Flask==0.11.1 Flask==0.11.1
glob2==0.4.1 glob2==0.5
httpbin==0.5.0 httpbin==0.5.0
hypothesis==3.5.3 hypothesis==3.6.0
itsdangerous==0.24 itsdangerous==0.24
# Jinja2==2.8 # Jinja2==2.8
Mako==1.0.4 Mako==1.0.5
# MarkupSafe==0.23 # MarkupSafe==0.23
parse==1.6.6 parse==1.6.6
parse-type==0.3.4 parse-type==0.3.4
py==1.4.31 py==1.4.31
pytest==3.0.3 pytest==3.0.3
pytest-bdd==2.18.0 pytest-bdd==2.18.1
pytest-catchlog==1.2.2 pytest-catchlog==1.2.2
pytest-cov==2.4.0 pytest-cov==2.4.0
pytest-faulthandler==1.3.0 pytest-faulthandler==1.3.0
pytest-instafail==0.3.0 pytest-instafail==0.3.0
pytest-mock==1.2 pytest-mock==1.4.0
pytest-qt==2.0.0 pytest-qt==2.1.0
pytest-repeat==0.4.1 pytest-repeat==0.4.1
pytest-rerunfailures==2.0.1 pytest-rerunfailures==2.1.0
pytest-travis-fold==1.2.0 pytest-travis-fold==1.2.0
pytest-warnings==0.1.0 pytest-warnings==0.2.0
pytest-xvfb==0.3.0 pytest-xvfb==0.3.0
six==1.10.0 six==1.10.0
spark-parser==1.4.0
uncompyle6==2.9.2
vulture==0.10 vulture==0.10
Werkzeug==0.11.11 Werkzeug==0.11.11
xdis==3.1.0

View File

@ -28,7 +28,7 @@ __copyright__ = "Copyright 2014-2016 Florian Bruhin (The Compiler)"
__license__ = "GPL" __license__ = "GPL"
__maintainer__ = __author__ __maintainer__ = __author__
__email__ = "mail@qutebrowser.org" __email__ = "mail@qutebrowser.org"
__version_info__ = (0, 8, 1) __version_info__ = (0, 8, 4)
__version__ = '.'.join(str(e) for e in __version_info__) __version__ = '.'.join(str(e) for e in __version_info__)
__description__ = "A keyboard-driven, vim-like browser based on PyQt5 and QtWebKit." __description__ = "A keyboard-driven, vim-like browser based on PyQt5 and QtWebKit."

View File

@ -47,10 +47,9 @@ from qutebrowser.commands import cmdutils, runners, cmdexc
from qutebrowser.config import style, config, websettings, configexc from qutebrowser.config import style, config, websettings, configexc
from qutebrowser.browser import urlmarks, adblock, history, browsertab from qutebrowser.browser import urlmarks, adblock, history, browsertab
from qutebrowser.browser.webkit import cookies, cache, downloads from qutebrowser.browser.webkit import cookies, cache, downloads
from qutebrowser.browser.webkit.network import (webkitqutescheme, proxy, from qutebrowser.browser.webkit.network import networkmanager
networkmanager)
from qutebrowser.keyinput import macros from qutebrowser.keyinput import macros
from qutebrowser.mainwindow import mainwindow from qutebrowser.mainwindow import mainwindow, prompt
from qutebrowser.misc import (readline, ipc, savemanager, sessions, from qutebrowser.misc import (readline, ipc, savemanager, sessions,
crashsignal, earlyinit) crashsignal, earlyinit)
from qutebrowser.misc import utilcmds # pylint: disable=unused-import 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', tabbed_browser = objreg.get('tabbed-browser', scope='window',
window='last-focused') window='last-focused')
tabbed_browser.tabopen( tabbed_browser.tabopen(
QUrl('http://www.qutebrowser.org/quickstart.html')) QUrl('https://www.qutebrowser.org/quickstart.html'))
state_config['general']['quickstart-done'] = '1' state_config['general']['quickstart-done'] = '1'
@ -376,6 +375,9 @@ def _init_modules(args, crash_handler):
crash_handler: The CrashHandler instance. crash_handler: The CrashHandler instance.
""" """
# pylint: disable=too-many-statements # pylint: disable=too-many-statements
log.init.debug("Initializing prompts...")
prompt.init()
log.init.debug("Initializing save manager...") log.init.debug("Initializing save manager...")
save_manager = savemanager.SaveManager(qApp) save_manager = savemanager.SaveManager(qApp)
objreg.register('save-manager', save_manager) objreg.register('save-manager', save_manager)
@ -402,10 +404,6 @@ def _init_modules(args, crash_handler):
log.init.debug("Initializing sessions...") log.init.debug("Initializing sessions...")
sessions.init(qApp) 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...") log.init.debug("Initializing websettings...")
websettings.init() websettings.init()
@ -422,9 +420,6 @@ def _init_modules(args, crash_handler):
bookmark_manager = urlmarks.BookmarkManager(qApp) bookmark_manager = urlmarks.BookmarkManager(qApp)
objreg.register('bookmark-manager', bookmark_manager) objreg.register('bookmark-manager', bookmark_manager)
log.init.debug("Initializing proxy...")
proxy.init()
log.init.debug("Initializing cookies...") log.init.debug("Initializing cookies...")
cookie_jar = cookies.CookieJar(qApp) cookie_jar = cookies.CookieJar(qApp)
ram_cookie_jar = cookies.RAMCookieJar(qApp) ram_cookie_jar = cookies.RAMCookieJar(qApp)
@ -654,13 +649,7 @@ class Quitter:
session_manager.save(sessions.default, last_window=last_window, session_manager.save(sessions.default, last_window=last_window,
load_next_time=True) load_next_time=True)
deferrer = False if prompt.prompt_queue.shutdown():
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 shutdown was called while we were asking a question, we're in # 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 # a still sub-eventloop (which gets quit now) and not in the main
# one. # one.
@ -719,7 +708,6 @@ class Quitter:
atexit.register(shutil.rmtree, self._args.basedir, atexit.register(shutil.rmtree, self._args.basedir,
ignore_errors=True) ignore_errors=True)
# Delete temp download dir # Delete temp download dir
objreg.get('temporary-downloads').cleanup()
# If we don't kill our custom handler here we might get segfaults # If we don't kill our custom handler here we might get segfaults
log.destroy.debug("Deactivating message handler...") log.destroy.debug("Deactivating message handler...")
qInstallMessageHandler(None) qInstallMessageHandler(None)
@ -747,6 +735,7 @@ class Application(QApplication):
Attributes: Attributes:
_args: ArgumentParser instance. _args: ArgumentParser instance.
_last_focus_object: The last focused object's repr.
""" """
new_window = pyqtSignal(mainwindow.MainWindow) new_window = pyqtSignal(mainwindow.MainWindow)
@ -757,6 +746,8 @@ class Application(QApplication):
Args: Args:
Argument namespace from argparse. Argument namespace from argparse.
""" """
self._last_focus_object = None
qt_args = qtutils.get_args(args) qt_args = qtutils.get_args(args)
log.init.debug("Qt arguments: {}, based on {}".format(qt_args, args)) log.init.debug("Qt arguments: {}, based on {}".format(qt_args, args))
super().__init__(qt_args) super().__init__(qt_args)
@ -773,7 +764,10 @@ class Application(QApplication):
@pyqtSlot(QObject) @pyqtSlot(QObject)
def on_focus_object_changed(self, obj): def on_focus_object_changed(self, obj):
"""Log when the focus object changed.""" """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): def __repr__(self):
return utils.get_repr(self) return utils.get_repr(self)

View File

@ -761,10 +761,6 @@ class AbstractTab(QWidget):
""" """
raise NotImplementedError raise NotImplementedError
def has_js(self):
"""Check if qutebrowser can run javascript in this tab."""
raise NotImplementedError
def shutdown(self): def shutdown(self):
raise NotImplementedError raise NotImplementedError

View File

@ -1137,7 +1137,7 @@ class CommandDispatcher:
def quickmark_save(self): def quickmark_save(self):
"""Save the current page as a quickmark.""" """Save the current page as a quickmark."""
quickmark_manager = objreg.get('quickmark-manager') 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', @cmdutils.register(instance='command-dispatcher', scope='window',
maxsplit=0) maxsplit=0)
@ -1355,9 +1355,8 @@ class CommandDispatcher:
if dest is None: if dest is None:
suggested_fn = self._current_title() + ".mht" suggested_fn = self._current_title() + ".mht"
suggested_fn = utils.sanitize_filename(suggested_fn) suggested_fn = utils.sanitize_filename(suggested_fn)
filename, q = downloads.ask_for_filename( filename, q = downloads.ask_for_filename(suggested_fn, parent=tab,
suggested_fn, self._win_id, parent=tab, url=tab.url())
)
if filename is not None: if filename is not None:
mhtml.start_download_checked(filename, tab=tab) mhtml.start_download_checked(filename, tab=tab)
else: else:
@ -1539,8 +1538,6 @@ class CommandDispatcher:
text: The text to insert. text: The text to insert.
""" """
tab = self._current_widget() tab = self._current_widget()
if not tab.has_js():
raise cmdexc.CommandError("This command needs javascript enabled.")
def _insert_text_cb(elem): def _insert_text_cb(elem):
if elem is None: if elem is None:

View File

@ -265,9 +265,8 @@ class HintActions:
if text[0] not in modeparsers.STARTCHARS: if text[0] not in modeparsers.STARTCHARS:
raise HintingError("Invalid command text '{}'.".format(text)) raise HintingError("Invalid command text '{}'.".format(text))
bridge = objreg.get('message-bridge', scope='window', cmd = objreg.get('status-command', scope='window', window=self._win_id)
window=self._win_id) cmd.set_cmd_text(text)
bridge.set_cmd_text(text)
def download(self, elem, context): def download(self, elem, context):
"""Download a hint URL. """Download a hint URL.

View File

@ -26,6 +26,7 @@ to a file on shutdown, so it makes sense to keep them as strings here.
""" """
import os import os
import html
import os.path import os.path
import functools import functools
import collections import collections
@ -33,7 +34,7 @@ import collections
from PyQt5.QtCore import pyqtSignal, QUrl, QObject from PyQt5.QtCore import pyqtSignal, QUrl, QObject
from qutebrowser.utils import (message, usertypes, qtutils, urlutils, from qutebrowser.utils import (message, usertypes, qtutils, urlutils,
standarddir, objreg) standarddir, objreg, log)
from qutebrowser.commands import cmdutils from qutebrowser.commands import cmdutils
from qutebrowser.misc import lineparser from qutebrowser.misc import lineparser
@ -159,11 +160,10 @@ class QuickmarkManager(UrlMarkManager):
else: else:
self.marks[key] = url 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. """Prompt for a new quickmark name to be added and add it.
Args: Args:
win_id: The current window ID.
url: The quickmark url as a QUrl. url: The quickmark url as a QUrl.
""" """
if not url.isValid(): if not url.isValid():
@ -171,19 +171,19 @@ class QuickmarkManager(UrlMarkManager):
return return
urlstr = url.toString(QUrl.RemovePassword | QUrl.FullyEncoded) urlstr = url.toString(QUrl.RemovePassword | QUrl.FullyEncoded)
message.ask_async( message.ask_async(
win_id, "Add quickmark:", usertypes.PromptMode.text, "Add quickmark:", usertypes.PromptMode.text,
functools.partial(self.quickmark_add, win_id, urlstr)) 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.register(instance='quickmark-manager')
@cmdutils.argument('win_id', win_id=True) def quickmark_add(self, url, name):
def quickmark_add(self, win_id, url, name):
"""Add a new quickmark. """Add a new quickmark.
You can view all saved quickmarks on the You can view all saved quickmarks on the
link:qute://bookmarks[bookmarks page]. link:qute://bookmarks[bookmarks page].
Args: Args:
win_id: The window ID to display the errors in.
url: The url to add as quickmark. url: The url to add as quickmark.
name: The name for the new quickmark. name: The name for the new quickmark.
""" """
@ -201,10 +201,12 @@ class QuickmarkManager(UrlMarkManager):
self.marks[name] = url self.marks[name] = url
self.changed.emit() self.changed.emit()
self.added.emit(name, url) self.added.emit(name, url)
log.misc.debug("Added quickmark {} for {}".format(name, url))
if name in self.marks: if name in self.marks:
message.confirm_async( message.confirm_async(
win_id, "Override existing quickmark?", set_mark, default=True) title="Override existing quickmark?",
yes_action=set_mark, default=True)
else: else:
set_mark() set_mark()

View File

@ -162,11 +162,6 @@ class AbstractWebElement(collections.abc.MutableMapping):
"""Insert the given text into the element.""" """Insert the given text into the element."""
raise NotImplementedError 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): def rect_on_view(self, *, elem_geometry=None, no_js=False):
"""Get the geometry of the element relative to the webview. """Get the geometry of the element relative to the webview.
@ -294,16 +289,7 @@ class AbstractWebElement(collections.abc.MutableMapping):
def remove_blank_target(self): def remove_blank_target(self):
"""Remove target from link.""" """Remove target from link."""
elem = self raise NotImplementedError
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()
def resolve_url(self, baseurl): def resolve_url(self, baseurl):
"""Resolve the URL in the element's src/href attribute. """Resolve the URL in the element's src/href attribute.

View File

@ -47,7 +47,10 @@ class WebEngineElement(webelem.AbstractWebElement):
return attrs[key] return attrs[key]
def __setitem__(self, key, val): 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): def __delitem__(self, key):
log.stub() log.stub()
@ -114,12 +117,6 @@ class WebEngineElement(webelem.AbstractWebElement):
js_code = javascript.assemble('webelem', 'insert_text', self._id, text) js_code = javascript.assemble('webelem', 'insert_text', self._id, text)
self._tab.run_js_async(js_code) 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): def rect_on_view(self, *, elem_geometry=None, no_js=False):
"""Get the geometry of the element relative to the webview. """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( log.webelem.debug("Couldn't find rectangle for {!r} ({})".format(
self, rects)) self, rects))
return QRect() return QRect()
def remove_blank_target(self):
js_code = javascript.assemble('webelem', 'remove_blank_target',
self._id)
self._tab.run_js_async(js_code)

View File

@ -402,7 +402,10 @@ class WebEngineElements(browsertab.AbstractElements):
Called with a WebEngineElement or None. Called with a WebEngineElement or None.
js_elem: The element serialized from javascript. 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: if js_elem is None:
callback(None) callback(None)
else: else:
@ -528,10 +531,6 @@ class WebEngineTab(browsertab.AbstractTab):
else: else:
self._widget.page().runJavaScript(code, callback) 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): def shutdown(self):
log.stub() log.stub()

View File

@ -28,6 +28,7 @@ import shutil
import functools import functools
import tempfile import tempfile
import collections import collections
import html
import sip import sip
from PyQt5.QtCore import (pyqtSlot, pyqtSignal, QObject, QTimer, from PyQt5.QtCore import (pyqtSlot, pyqtSignal, QObject, QTimer,
@ -119,7 +120,7 @@ def create_full_filename(basename, filename):
return None 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): prompt_download_directory=None):
"""Prepare a question for a download-path. """Prepare a question for a download-path.
@ -133,7 +134,7 @@ def ask_for_filename(suggested_filename, win_id, *, parent=None,
Args: Args:
suggested_filename: The "default"-name that is pre-entered as path. 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). parent: The parent of the question (a QObject).
prompt_download_directory: If this is something else than None, it prompt_download_directory: If this is something else than None, it
will overwrite the 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) suggested_filename = utils.force_encoding(suggested_filename, encoding)
q = usertypes.Question(parent) 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.mode = usertypes.PromptMode.text
q.completed.connect(q.deleteLater) q.completed.connect(q.deleteLater)
q.default = _path_suggestion(suggested_filename) q.default = _path_suggestion(suggested_filename)
message_bridge = objreg.get('message-bridge', scope='window', q.ask = lambda: message.global_bridge.ask(q, blocking=False)
window=win_id)
q.ask = lambda: message_bridge.ask(q, blocking=False)
return _DownloadPath(filename=None, question=q) return _DownloadPath(filename=None, question=q)
@ -382,20 +383,13 @@ class DownloadItem(QObject):
else: else:
self.set_fileobj(fileobj) self.set_fileobj(fileobj)
def _ask_confirm_question(self, msg): def _ask_confirm_question(self, title, msg):
"""Create a Question object to be asked.""" """Create a Question object to be asked."""
q = usertypes.Question(self) no_action = functools.partial(self.cancel, remove_data=False)
q.text = msg message.confirm_async(title=title, text=msg,
q.mode = usertypes.PromptMode.yesno yes_action=self._create_fileobj,
q.answered_yes.connect(self._create_fileobj) no_action=no_action, cancel_action=no_action,
q.answered_no.connect(functools.partial(self.cancel, abort_on=[self.cancelled, self.error])
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)
def _die(self, msg): def _die(self, msg):
"""Abort the download and emit an error.""" """Abort the download and emit an error."""
@ -614,14 +608,15 @@ class DownloadItem(QObject):
if os.path.isfile(self._filename): if os.path.isfile(self._filename):
# The file already exists, so ask the user if it should be # The file already exists, so ask the user if it should be
# overwritten. # overwritten.
txt = self._filename + " already exists. Overwrite?" txt = "<b>{}</b> already exists. Overwrite?".format(
self._ask_confirm_question(txt) html.escape(self._filename))
self._ask_confirm_question("Overwrite existing file?", txt)
# FIFO, device node, etc. Make sure we want to do this # FIFO, device node, etc. Make sure we want to do this
elif (os.path.exists(self._filename) and elif (os.path.exists(self._filename) and
not os.path.isdir(self._filename)): not os.path.isdir(self._filename)):
txt = (self._filename + " already exists and is a special file. " txt = ("<b>{}</b> already exists and is a special file. Write to "
"Write to this?") "it anyways?".format(html.escape(self._filename)))
self._ask_confirm_question(txt) self._ask_confirm_question("Overwrite special file?", txt)
else: else:
self._create_fileobj() self._create_fileobj()
@ -963,9 +958,9 @@ class DownloadManager(QObject):
# Neither filename nor fileobj were given, prepare a question # Neither filename nor fileobj were given, prepare a question
filename, q = ask_for_filename( filename, q = ask_for_filename(
suggested_filename, self._win_id, parent=self, suggested_filename, parent=self,
prompt_download_directory=prompt_download_directory, prompt_download_directory=prompt_download_directory,
) url=reply.url())
# User doesn't want to be asked, so just use the download_dir # User doesn't want to be asked, so just use the download_dir
if filename is not None: if filename is not None:

View File

@ -22,7 +22,9 @@
import os import os
import collections import collections
import netrc import netrc
import html
import jinja2
from PyQt5.QtCore import (pyqtSlot, pyqtSignal, PYQT_VERSION, QCoreApplication, from PyQt5.QtCore import (pyqtSlot, pyqtSignal, PYQT_VERSION, QCoreApplication,
QUrl, QByteArray) QUrl, QByteArray)
from PyQt5.QtNetwork import (QNetworkAccessManager, QNetworkReply, QSslError, from PyQt5.QtNetwork import (QNetworkAccessManager, QNetworkReply, QSslError,
@ -207,10 +209,11 @@ class NetworkManager(QNetworkAccessManager):
self.setCache(cache) self.setCache(cache)
cache.setParent(app) 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. """Ask a blocking question in the statusbar.
Args: Args:
title: The title to display to the user.
text: The text to display to the user. text: The text to display to the user.
mode: A PromptMode. mode: A PromptMode.
owner: An object which will abort the question if destroyed, or owner: An object which will abort the question if destroyed, or
@ -219,24 +222,19 @@ class NetworkManager(QNetworkAccessManager):
Return: Return:
The answer the user gave or None if the prompt was cancelled. The answer the user gave or None if the prompt was cancelled.
""" """
q = usertypes.Question() abort_on = [self.shutting_down]
q.text = text
q.mode = mode
self.shutting_down.connect(q.abort)
if owner is not None: 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 # This might be a generic network manager, e.g. one belonging to a
# DownloadManager. In this case, just skip the webview thing. # DownloadManager. In this case, just skip the webview thing.
if self._tab_id is not None: if self._tab_id is not None:
tab = objreg.get('tab', scope='tab', window=self._win_id, tab = objreg.get('tab', scope='tab', window=self._win_id,
tab=self._tab_id) tab=self._tab_id)
tab.load_started.connect(q.abort) abort_on.append(tab.load_started)
bridge = objreg.get('message-bridge', scope='window',
window=self._win_id) return message.ask(title=title, text=text, mode=mode,
bridge.ask(q, blocking=True) abort_on=abort_on, default=default)
q.deleteLater()
return q.answer
def shutdown(self): def shutdown(self):
"""Abort all running requests.""" """Abort all running requests."""
@ -283,9 +281,19 @@ class NetworkManager(QNetworkAccessManager):
return return
if ssl_strict == 'ask': if ssl_strict == 'ask':
err_string = '\n'.join('- ' + err.errorString() for err in errors) err_template = jinja2.Template("""
answer = self._ask('SSL errors - continue?\n{}'.format(err_string), Errors while loading <b>{{url.toDisplayString()}}</b>:<br/>
mode=usertypes.PromptMode.yesno, owner=reply) <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)) log.webview.debug("Asked for SSL errors, answer {}".format(answer))
if answer: if answer:
reply.ignoreSslErrors() reply.ignoreSslErrors()
@ -343,8 +351,11 @@ class NetworkManager(QNetworkAccessManager):
if user is None: if user is None:
# netrc check failed # netrc check failed
answer = self._ask("Username ({}):".format(authenticator.realm()), msg = '<b>{}</b> says:<br/>{}'.format(
mode=usertypes.PromptMode.user_pwd, html.escape(reply.url().toDisplayString()),
html.escape(authenticator.realm()))
answer = self._ask("Authentication required",
text=msg, mode=usertypes.PromptMode.user_pwd,
owner=reply) owner=reply)
if answer is not None: if answer is not None:
user, password = answer.user, answer.password user, password = answer.user, answer.password
@ -361,8 +372,11 @@ class NetworkManager(QNetworkAccessManager):
authenticator.setUser(user) authenticator.setUser(user)
authenticator.setPassword(password) authenticator.setPassword(password)
else: else:
msg = '<b>{}</b> says:<br/>{}'.format(
html.escape(proxy.hostName()),
html.escape(authenticator.realm()))
answer = self._ask( answer = self._ask(
"Proxy username ({}):".format(authenticator.realm()), "Proxy authentication required", msg,
mode=usertypes.PromptMode.user_pwd) mode=usertypes.PromptMode.user_pwd)
if answer is not None: if answer is not None:
authenticator.setUser(answer.user) authenticator.setUser(answer.user)

View File

@ -145,10 +145,11 @@ class WebKitElement(webelem.AbstractWebElement):
this.dispatchEvent(event); this.dispatchEvent(event);
""".format(javascript.string_escape(text))) """.format(javascript.string_escape(text)))
def parent(self): def _parent(self):
"""Get the parent element of this element."""
self._check_vanished() self._check_vanished()
elem = self._elem.parent() elem = self._elem.parent()
if elem is None: if elem is None or elem.isNull():
return None return None
return WebKitElement(elem, tab=self._tab) return WebKitElement(elem, tab=self._tab)
@ -283,6 +284,18 @@ class WebKitElement(webelem.AbstractWebElement):
visible_in_frame = visible_on_screen visible_in_frame = visible_on_screen
return all([visible_on_screen, visible_in_frame]) 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): def get_child_frames(startframe):
"""Get all children recursively of a given QWebFrame. """Get all children recursively of a given QWebFrame.

View File

@ -26,19 +26,27 @@ import xml.etree.ElementTree
from PyQt5.QtCore import (pyqtSlot, Qt, QEvent, QUrl, QPoint, QTimer, QSizeF, from PyQt5.QtCore import (pyqtSlot, Qt, QEvent, QUrl, QPoint, QTimer, QSizeF,
QSize) QSize)
from PyQt5.QtGui import QKeyEvent from PyQt5.QtGui import QKeyEvent
from PyQt5.QtWidgets import QApplication
from PyQt5.QtWebKitWidgets import QWebPage, QWebFrame from PyQt5.QtWebKitWidgets import QWebPage, QWebFrame
from PyQt5.QtWebKit import QWebSettings from PyQt5.QtWebKit import QWebSettings
from PyQt5.QtPrintSupport import QPrinter from PyQt5.QtPrintSupport import QPrinter
from qutebrowser.browser import browsertab from qutebrowser.browser import browsertab
from qutebrowser.browser.webkit import webview, tabhistory, webkitelem 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 from qutebrowser.utils import qtutils, objreg, usertypes, utils, log
def init(): def init():
"""Initialize QtWebKit-specific modules.""" """Initialize QtWebKit-specific modules."""
# FIXME:qtwebengine Move things we don't need with QtWebEngine here. qapp = QApplication.instance()
pass
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): class WebKitPrinting(browsertab.AbstractPrinting):
@ -624,14 +632,11 @@ class WebKitTab(browsertab.AbstractTab):
def run_js_async(self, code, callback=None, *, world=None): def run_js_async(self, code, callback=None, *, world=None):
if world is not None and world != usertypes.JsWorld.jseval: if world is not None and world != usertypes.JsWorld.jseval:
log.webview.warning("Ignoring world ID {}".format(world)) 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: if callback is not None:
callback(result) callback(result)
def has_js(self):
settings = QWebSettings.globalSettings()
return settings.testAttribute(QWebSettings.JavascriptEnabled)
def icon(self): def icon(self):
return self._widget.icon() return self._widget.icon()

View File

@ -19,6 +19,7 @@
"""The main browser widgets.""" """The main browser widgets."""
import html
import functools import functools
from PyQt5.QtCore import pyqtSlot, pyqtSignal, PYQT_VERSION, Qt, QUrl, QPoint from PyQt5.QtCore import pyqtSlot, pyqtSignal, PYQT_VERSION, Qt, QUrl, QPoint
@ -93,13 +94,19 @@ class BrowserPage(QWebPage):
# of a bug in PyQt. # of a bug in PyQt.
# See http://www.riverbankcomputing.com/pipermail/pyqt/2014-June/034385.html # 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.""" """Override javaScriptPrompt to use the statusbar."""
if (self._is_shutting_down or if (self._is_shutting_down or
config.get('content', 'ignore-javascript-prompt')): config.get('content', 'ignore-javascript-prompt')):
return (False, "") return (False, "")
answer = self._ask("js: {}".format(msg), usertypes.PromptMode.text, msg = '<b>{}</b> asks:<br/>{}'.format(
default) 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: if answer is None:
return (False, "") return (False, "")
else: else:
@ -134,11 +141,12 @@ class BrowserPage(QWebPage):
# QDesktopServices::openUrl with info.url directly - however it # QDesktopServices::openUrl with info.url directly - however it
# works when we construct a copy of it. # works when we construct a copy of it.
url = QUrl(info.url) url = QUrl(info.url)
msg = "Open external application for {}-link?\nURL: {}".format( scheme = url.scheme()
url.scheme(), url.toDisplayString())
message.confirm_async( message.confirm_async(
self._win_id, msg, title="Open external application for {}-link?".format(scheme),
functools.partial(QDesktopServices.openUrl, url)) text="URL: <b>{}</b>".format(
html.escape(url.toDisplayString())),
yes_action=functools.partial(QDesktopServices.openUrl, url))
return True return True
elif (info.domain, info.error) in ignored_errors: elif (info.domain, info.error) in ignored_errors:
log.webview.debug("Ignored error on {}: {} (error domain: {}, " log.webview.debug("Ignored error on {}: {} (error domain: {}, "
@ -168,11 +176,11 @@ class BrowserPage(QWebPage):
log.webview.debug("Error domain: {}, error code: {}".format( log.webview.debug("Error domain: {}, error code: {}".format(
info.domain, info.error)) info.domain, info.error))
title = "Error loading page: {}".format(urlstr) title = "Error loading page: {}".format(urlstr)
html = jinja.render( error_html = jinja.render(
'error.html', 'error.html',
title=title, url=urlstr, error=error_str, icon='', title=title, url=urlstr, error=error_str, icon='',
qutescheme=False) qutescheme=False)
errpage.content = html.encode('utf-8') errpage.content = error_html.encode('utf-8')
errpage.encoding = 'utf-8' errpage.encoding = 'utf-8'
return True return True
@ -196,29 +204,6 @@ class BrowserPage(QWebPage):
suggested_file) suggested_file)
return True 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): def _show_pdfjs(self, reply):
"""Show the reply with pdfjs.""" """Show the reply with pdfjs."""
try: try:
@ -333,11 +318,6 @@ class BrowserPage(QWebPage):
} }
config_val = config.get(*options[feature]) config_val = config.get(*options[feature])
if config_val == 'ask': 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 = { msgs = {
QWebPage.Notifications: 'show notifications', QWebPage.Notifications: 'show notifications',
QWebPage.Geolocation: 'access your location', QWebPage.Geolocation: 'access your location',
@ -345,30 +325,28 @@ class BrowserPage(QWebPage):
host = frame.url().host() host = frame.url().host()
if host: if host:
q.text = "Allow the website at {} to {}?".format( text = "Allow the website at <b>{}</b> to {}?".format(
frame.url().host(), msgs[feature]) html.escape(frame.url().toDisplayString()), msgs[feature])
else: else:
q.text = "Allow the website to {}?".format(msgs[feature]) text = "Allow the website to {}?".format(msgs[feature])
yes_action = functools.partial( yes_action = functools.partial(
self.setFeaturePermission, frame, feature, self.setFeaturePermission, frame, feature,
QWebPage.PermissionGrantedByUser) QWebPage.PermissionGrantedByUser)
q.answered_yes.connect(yes_action)
no_action = functools.partial( no_action = functools.partial(
self.setFeaturePermission, frame, feature, self.setFeaturePermission, frame, feature,
QWebPage.PermissionDeniedByUser) QWebPage.PermissionDeniedByUser)
q.answered_no.connect(no_action)
q.cancelled.connect(no_action)
self.shutting_down.connect(q.abort) question = message.confirm_async(yes_action=yes_action,
q.completed.connect(q.deleteLater) no_action=no_action,
cancel_action=no_action,
self.featurePermissionRequestCanceled.connect(functools.partial( abort_on=[self.shutting_down,
self.on_feature_permission_cancelled, q, frame, feature)) self.loadStarted],
self.loadStarted.connect(q.abort) title='Permission request',
text=text)
bridge.ask(q, blocking=False) self.featurePermissionRequestCanceled.connect(
functools.partial(self.on_feature_permission_cancelled,
question, frame, feature))
elif config_val: elif config_val:
self.setFeaturePermission(frame, feature, self.setFeaturePermission(frame, feature,
QWebPage.PermissionGrantedByUser) QWebPage.PermissionGrantedByUser)
@ -469,27 +447,37 @@ class BrowserPage(QWebPage):
return super().extension(ext, opt, out) return super().extension(ext, opt, out)
return handler(opt, out) return handler(opt, out)
def javaScriptAlert(self, frame, msg): def javaScriptAlert(self, frame, js_msg):
"""Override javaScriptAlert to use the statusbar.""" """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'): if config.get('ui', 'modal-js-dialog'):
return super().javaScriptAlert(frame, msg) return super().javaScriptAlert(frame, js_msg)
if (self._is_shutting_down or if (self._is_shutting_down or
config.get('content', 'ignore-javascript-alert')): config.get('content', 'ignore-javascript-alert')):
return 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.""" """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'): if config.get('ui', 'modal-js-dialog'):
return super().javaScriptConfirm(frame, msg) return super().javaScriptConfirm(frame, js_msg)
if self._is_shutting_down: if self._is_shutting_down:
return False 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) return bool(ans)
def javaScriptConsoleMessage(self, msg, line, source): def javaScriptConsoleMessage(self, msg, line, source):

View File

@ -427,9 +427,16 @@ class Command:
if isinstance(typ, tuple): if isinstance(typ, tuple):
raise TypeError("{}: Legacy tuple type annotation!".format( raise TypeError("{}: Legacy tuple type annotation!".format(
self.name)) self.name))
elif issubclass(typ, typing.Union): elif type(typ) is type(typing.Union): # flake8: disable=E721
# this is... slightly evil, I know # 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: if param.default is not inspect.Parameter.empty:
types.append(type(param.default)) types.append(type(param.default))
choices = self.get_arg_info(param).choices choices = self.get_arg_info(param).choices

View File

@ -391,6 +391,8 @@ class ConfigManager(QObject):
('colors', 'statusbar.bg.error'): 'messages.bg.error', ('colors', 'statusbar.bg.error'): 'messages.bg.error',
('colors', 'statusbar.fg.warning'): 'messages.fg.warning', ('colors', 'statusbar.fg.warning'): 'messages.fg.warning',
('colors', 'statusbar.bg.warning'): 'messages.bg.warning', ('colors', 'statusbar.bg.warning'): 'messages.bg.warning',
('colors', 'statusbar.fg.prompt'): 'prompts.fg',
('colors', 'statusbar.bg.prompt'): 'prompts.bg',
} }
DELETED_OPTIONS = [ DELETED_OPTIONS = [
('colors', 'tab.separator'), ('colors', 'tab.separator'),

View File

@ -90,7 +90,7 @@ SECTION_DESC = {
"Aliases for commands.\n" "Aliases for commands.\n"
"By default, no aliases are defined. Example which adds a new command " "By default, no aliases are defined. Example which adds a new command "
"`:qtb` to open qutebrowsers website:\n\n" "`:qtb` to open qutebrowsers website:\n\n"
"`qtb = open http://www.qutebrowser.org/`"), "`qtb = open https://www.qutebrowser.org/`"),
'colors': ( 'colors': (
"Colors used in the UI.\n" "Colors used in the UI.\n"
"A value can be in one of the following format:\n\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."), "Whether to find text on a page case-insensitively."),
('startpage', ('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."), "The default page(s) to open at the start, separated by commas."),
('yank-ignored-url-parameters', ('yank-ignored-url-parameters',
@ -383,6 +384,10 @@ def data(readonly=False):
"Globs are supported, so ';*' will blacklist all keychains" "Globs are supported, so ';*' will blacklist all keychains"
"starting with ';'. Use '*' to disable keyhints"), "starting with ';'. Use '*' to disable keyhints"),
('prompt-radius',
SettingValue(typ.Int(minval=0), '8'),
"The rounding radius for the edges of prompts."),
readonly=readonly readonly=readonly
)), )),
@ -871,11 +876,11 @@ def data(readonly=False):
('host-block-lists', ('host-block-lists',
SettingValue( SettingValue(
typ.List(typ.Url(), none_ok=True), 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://someonewhocares.org/hosts/hosts,'
'http://winhelp2002.mvps.org/hosts.zip,' 'http://winhelp2002.mvps.org/hosts.zip,'
'http://malwaredomains.lehigh.edu/files/justdomains.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'), 'hostformat=hosts&mimetype=plaintext'),
"List of URLs of lists which contain hosts to block.\n\n" "List of URLs of lists which contain hosts to block.\n\n"
"The file can be in one of the following formats:\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'), SettingValue(typ.QssColor(), 'black'),
"Background color of the statusbar."), "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', ('statusbar.fg.insert',
SettingValue(typ.QssColor(), '${statusbar.fg}'), SettingValue(typ.QssColor(), '${statusbar.fg}'),
"Foreground color of the statusbar in insert mode."), "Foreground color of the statusbar in insert mode."),
@ -1305,6 +1302,18 @@ def data(readonly=False):
SettingValue(typ.QssColor(), '#333333'), SettingValue(typ.QssColor(), '#333333'),
"Border color of an info message."), "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 readonly=readonly
)), )),
@ -1406,6 +1415,10 @@ def data(readonly=False):
SettingValue(typ.Font(), DEFAULT_FONT_SIZE + ' ${_monospace}'), SettingValue(typ.Font(), DEFAULT_FONT_SIZE + ' ${_monospace}'),
"Font used for info messages."), "Font used for info messages."),
('prompts',
SettingValue(typ.Font(), DEFAULT_FONT_SIZE + ' sans-serif'),
"Font used for prompts."),
readonly=readonly readonly=readonly
)), )),
]) ])
@ -1673,6 +1686,8 @@ KEY_DATA = collections.OrderedDict([
('prompt-accept yes', ['y']), ('prompt-accept yes', ['y']),
('prompt-accept no', ['n']), ('prompt-accept no', ['n']),
('prompt-open-download', ['<Ctrl-X>']), ('prompt-open-download', ['<Ctrl-X>']),
('prompt-item-focus prev', ['<Shift-Tab>', '<Up>']),
('prompt-item-focus next', ['<Tab>', '<Down>']),
])), ])),
('command,prompt', collections.OrderedDict([ ('command,prompt', collections.OrderedDict([

View File

@ -46,7 +46,7 @@ def get_stylesheet(template_str):
config=objreg.get('config')) 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. """Set the stylesheet for an object based on it's STYLESHEET attribute.
Also, register an update when the config is changed. Also, register an update when the config is changed.
@ -54,23 +54,20 @@ def set_register_stylesheet(obj, *, generator=None):
Args: Args:
obj: The object to set the stylesheet for and register. obj: The object to set the stylesheet for and register.
Must have a STYLESHEET attribute. 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(obj.STYLESHEET)
qss = get_stylesheet(stylesheet)
log.config.vdebug("stylesheet for {}: {}".format( log.config.vdebug("stylesheet for {}: {}".format(
obj.__class__.__name__, qss)) obj.__class__.__name__, qss))
obj.setStyleSheet(qss) obj.setStyleSheet(qss)
objreg.get('config').changed.connect( 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.""" """Update the stylesheet for obj."""
get_stylesheet.cache_clear()
if not sip.isdeleted(obj): if not sip.isdeleted(obj):
stylesheet = generator() if generator is not None else obj.STYLESHEET obj.setStyleSheet(get_stylesheet(obj.STYLESHEET))
obj.setStyleSheet(get_stylesheet(stylesheet))
class ColorDict(collections.UserDict): class ColorDict(collections.UserDict):

View File

@ -90,6 +90,9 @@ li {
<li> <li>
If you have installed a packaged version of qutebrowser, make sure If you have installed a packaged version of qutebrowser, make sure
the required packages for pdf.js are also installed. 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>
<li> <li>

View File

@ -155,5 +155,23 @@ window._qutebrowser.webelem = (function() {
return serialize_elem(elem); 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; return funcs;
})(); })();

View File

@ -24,13 +24,14 @@ import base64
import itertools import itertools
import functools import functools
import jinja2
from PyQt5.QtCore import pyqtSlot, QRect, QPoint, QTimer, Qt from PyQt5.QtCore import pyqtSlot, QRect, QPoint, QTimer, Qt
from PyQt5.QtWidgets import QWidget, QVBoxLayout, QApplication, QSizePolicy from PyQt5.QtWidgets import QWidget, QVBoxLayout, QApplication, QSizePolicy
from qutebrowser.commands import runners, cmdutils from qutebrowser.commands import runners, cmdutils
from qutebrowser.config import config from qutebrowser.config import config
from qutebrowser.utils import message, log, usertypes, qtutils, objreg, utils 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.mainwindow.statusbar import bar
from qutebrowser.completion import completionwidget, completer from qutebrowser.completion import completionwidget, completer
from qutebrowser.keyinput import modeman from qutebrowser.keyinput import modeman
@ -175,17 +176,24 @@ class MainWindow(QWidget):
self._init_completion() self._init_completion()
log.init.debug("Initializing modes...")
modeman.init(self.win_id, self)
self._commandrunner = runners.CommandRunner(self.win_id, self._commandrunner = runners.CommandRunner(self.win_id,
partial_match=True) partial_match=True)
self._keyhint = keyhintwidget.KeyHintView(self.win_id, self) 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._messageview = messageview.MessageView(parent=self)
self._overlays.append((self._messageview, self._add_overlay(self._messageview, self._messageview.update_geometry)
self._messageview.update_geometry))
log.init.debug("Initializing modes...") self._prompt_container = prompt.PromptContainer(self.win_id, self)
modeman.init(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: if geometry is not None:
self._load_geometry(geometry) self._load_geometry(geometry)
@ -206,36 +214,40 @@ class MainWindow(QWidget):
objreg.get("app").new_window.emit(self) objreg.get("app").new_window.emit(self)
def _update_overlay_geometry(self, widget=None): def _add_overlay(self, widget, signal, *, centered=False, padding=0):
"""Reposition/resize the given overlay. self._overlays.append((widget, signal, centered, padding))
If no widget is given, reposition/resize all overlays. def _update_overlay_geometries(self):
""" """Update the size/position of all overlays."""
if widget is None: for w, _signal, centered, padding in self._overlays:
for w, _signal in self._overlays: self._update_overlay_geometry(w, centered, padding)
self._update_overlay_geometry(w)
return
def _update_overlay_geometry(self, widget, centered, padding):
"""Reposition/resize the given overlay."""
if not widget.isVisible(): if not widget.isVisible():
return return
size_hint = widget.sizeHint() size_hint = widget.sizeHint()
if widget.sizePolicy().horizontalPolicy() == QSizePolicy.Expanding: if widget.sizePolicy().horizontalPolicy() == QSizePolicy.Expanding:
width = self.width() width = self.width() - 2 * padding
left = padding
else: else:
width = size_hint.width() 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') status_position = config.get('ui', 'status-position')
if status_position == 'bottom': if status_position == 'bottom':
top = self.height() - self.status.height() - size_hint.height() top = self.height() - self.status.height() - size_hint.height()
top = qtutils.check_overflow(top, 'int', fatal=False) top = qtutils.check_overflow(top, 'int', fatal=False)
topleft = QPoint(0, top) topleft = QPoint(left, max(height_padding, top))
bottomright = QPoint(width, self.status.geometry().top()) bottomright = QPoint(left + width, self.status.geometry().top())
elif status_position == '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 = self.status.height() + size_hint.height()
bottom = qtutils.check_overflow(bottom, 'int', fatal=False) bottom = qtutils.check_overflow(bottom, 'int', fatal=False)
bottomright = QPoint(width, bottom) bottomright = QPoint(left + width,
min(self.height() - height_padding, bottom))
else: else:
raise ValueError("Invalid position {}!".format(status_position)) raise ValueError("Invalid position {}!".format(status_position))
@ -261,8 +273,7 @@ class MainWindow(QWidget):
completer_obj.on_selection_changed) completer_obj.on_selection_changed)
objreg.register('completion', self._completion, scope='window', objreg.register('completion', self._completion, scope='window',
window=self.win_id) window=self.win_id)
self._overlays.append((self._completion, self._add_overlay(self._completion, self._completion.update_geometry)
self._completion.update_geometry))
def _init_command_dispatcher(self): def _init_command_dispatcher(self):
dispatcher = commands.CommandDispatcher(self.win_id, dispatcher = commands.CommandDispatcher(self.win_id,
@ -282,12 +293,12 @@ class MainWindow(QWidget):
if section != 'ui': if section != 'ui':
return return
if option == 'statusbar-padding': if option == 'statusbar-padding':
self._update_overlay_geometry() self._update_overlay_geometries()
elif option == 'downloads-position': elif option == 'downloads-position':
self._add_widgets() self._add_widgets()
elif option == 'status-position': elif option == 'status-position':
self._add_widgets() self._add_widgets()
self._update_overlay_geometry() self._update_overlay_geometries()
def _add_widgets(self): def _add_widgets(self):
"""Add or readd all widgets to the VBox.""" """Add or readd all widgets to the VBox."""
@ -350,10 +361,11 @@ class MainWindow(QWidget):
def _connect_overlay_signals(self): def _connect_overlay_signals(self):
"""Connect the resize signal and resize everything once.""" """Connect the resize signal and resize everything once."""
for widget, signal in self._overlays: for widget, signal, centered, padding in self._overlays:
signal.connect( signal.connect(
functools.partial(self._update_overlay_geometry, widget)) functools.partial(self._update_overlay_geometry, widget,
self._update_overlay_geometry(widget) centered, padding))
self._update_overlay_geometry(widget, centered, padding)
def _set_default_geometry(self): def _set_default_geometry(self):
"""Set some sensible default geometry.""" """Set some sensible default geometry."""
@ -374,7 +386,6 @@ class MainWindow(QWidget):
cmd = self._get_object('status-command') cmd = self._get_object('status-command')
message_bridge = self._get_object('message-bridge') message_bridge = self._get_object('message-bridge')
mode_manager = self._get_object('mode-manager') mode_manager = self._get_object('mode-manager')
prompter = self._get_object('prompter')
# misc # misc
self.tabbed_browser.close_window.connect(self.close) 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.entered.connect(status.on_mode_entered)
mode_manager.left.connect(status.on_mode_left) mode_manager.left.connect(status.on_mode_left)
mode_manager.left.connect(cmd.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 # commands
keyparsers[usertypes.KeyMode.normal].keystring_updated.connect( 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_set_text.connect(status.set_text)
message_bridge.s_maybe_reset_text.connect(status.txt.maybe_reset_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 # statusbar
tabs.current_tab_changed.connect(status.prog.on_tab_changed) tabs.current_tab_changed.connect(status.prog.on_tab_changed)
@ -459,7 +467,7 @@ class MainWindow(QWidget):
e: The QResizeEvent e: The QResizeEvent
""" """
super().resizeEvent(e) super().resizeEvent(e)
self._update_overlay_geometry() self._update_overlay_geometries()
self._downloadview.updateGeometry() self._downloadview.updateGeometry()
self.tabbed_browser.tabBar().refresh() self.tabbed_browser.tabBar().refresh()
@ -507,10 +515,17 @@ class MainWindow(QWidget):
"download is" if download_count == 1 else "downloads are")) "download is" if download_count == 1 else "downloads are"))
# Process all quit messages that user must confirm # Process all quit messages that user must confirm
if quit_texts or 'always' in confirm_quit: if quit_texts or 'always' in confirm_quit:
text = '\n'.join(['Really quit?'] + quit_texts) msg = jinja2.Template("""
confirmed = message.ask(self.win_id, text, <ul>
usertypes.PromptMode.yesno, {% 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) default=True)
# Stop asking if the user cancels # Stop asking if the user cancels
if not confirmed: if not confirmed:
log.destroy.debug("Cancelling closing of window {}".format( log.destroy.debug("Cancelling closing of window {}".format(

View 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)

View File

@ -25,8 +25,7 @@ from PyQt5.QtWidgets import QWidget, QHBoxLayout, QStackedLayout, QSizePolicy
from qutebrowser.config import config, style from qutebrowser.config import config, style
from qutebrowser.utils import usertypes, log, objreg, utils from qutebrowser.utils import usertypes, log, objreg, utils
from qutebrowser.mainwindow.statusbar import (command, progress, keystring, from qutebrowser.mainwindow.statusbar import (command, progress, keystring,
percentage, url, prompt, percentage, url, tabindex)
tabindex)
from qutebrowser.mainwindow.statusbar import text as textwidget 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"],
QWidget#StatusBar[prompt_active="true"] QLabel, QWidget#StatusBar[prompt_active="true"] QLabel,
QWidget#StatusBar[prompt_active="true"] QLineEdit { QWidget#StatusBar[prompt_active="true"] QLineEdit {
color: {{ color['statusbar.fg.prompt'] }}; color: {{ color['prompts.fg'] }};
background-color: {{ color['statusbar.bg.prompt'] }}; background-color: {{ color['prompts.bg'] }};
} }
QWidget#StatusBar[insert_active="true"], QWidget#StatusBar[insert_active="true"],
@ -162,16 +161,9 @@ class StatusBar(QWidget):
self.txt = textwidget.Text() self.txt = textwidget.Text()
self._stack.addWidget(self.txt) 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.show_cmd.connect(self._show_cmd_widget)
self.cmd.hide_cmd.connect(self._hide_cmd_widget) self.cmd.hide_cmd.connect(self._hide_cmd_widget)
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.keystring = keystring.KeyString()
self._hbox.addWidget(self.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.""" """Getter for self.prompt_active, so it can be used as Qt property."""
return self._prompt_active 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) @pyqtProperty(bool)
def command_active(self): def command_active(self):
"""Getter for self.command_active, so it can be used as Qt property.""" """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: if mode == usertypes.KeyMode.command:
log.statusbar.debug("Setting command_active to {}".format(val)) log.statusbar.debug("Setting command_active to {}".format(val))
self._command_active = 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: elif mode == usertypes.KeyMode.caret:
tab = objreg.get('tabbed-browser', scope='window', tab = objreg.get('tabbed-browser', scope='window',
window=self._win_id).currentWidget() window=self._win_id).currentWidget()
@ -285,21 +270,6 @@ class StatusBar(QWidget):
self._stack.setCurrentWidget(self.txt) self._stack.setCurrentWidget(self.txt)
self.maybe_hide() 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) @pyqtSlot(str)
def set_text(self, val): def set_text(self, val):
"""Set a normal (persistent) text in the status bar.""" """Set a normal (persistent) text in the status bar."""
@ -314,7 +284,9 @@ class StatusBar(QWidget):
self._set_mode_text(mode.name) self._set_mode_text(mode.name)
if mode in [usertypes.KeyMode.insert, if mode in [usertypes.KeyMode.insert,
usertypes.KeyMode.command, usertypes.KeyMode.command,
usertypes.KeyMode.caret]: usertypes.KeyMode.caret,
usertypes.KeyMode.prompt,
usertypes.KeyMode.yesno]:
self.set_mode_active(mode, True) self.set_mode_active(mode, True)
@pyqtSlot(usertypes.KeyMode, usertypes.KeyMode) @pyqtSlot(usertypes.KeyMode, usertypes.KeyMode)
@ -329,7 +301,9 @@ class StatusBar(QWidget):
self.txt.set_text(self.txt.Text.normal, '') self.txt.set_text(self.txt.Text.normal, '')
if old_mode in [usertypes.KeyMode.insert, if old_mode in [usertypes.KeyMode.insert,
usertypes.KeyMode.command, usertypes.KeyMode.command,
usertypes.KeyMode.caret]: usertypes.KeyMode.caret,
usertypes.KeyMode.prompt,
usertypes.KeyMode.yesno]:
self.set_mode_active(old_mode, False) self.set_mode_active(old_mode, False)
def resizeEvent(self, e): def resizeEvent(self, e):

View File

@ -77,7 +77,6 @@ class Command(misc.MinimalLineEditMixin, misc.CommandLineEdit):
else: else:
return '' return ''
@pyqtSlot(str)
def set_cmd_text(self, text): def set_cmd_text(self, text):
"""Preset the statusbar to some text. """Preset the statusbar to some text.

View File

@ -36,6 +36,7 @@ class Progress(QProgressBar):
border-radius: 0px; border-radius: 0px;
border: 2px solid transparent; border: 2px solid transparent;
background-color: transparent; background-color: transparent;
font: {{ font['statusbar'] }};
} }
QProgressBar::chunk { QProgressBar::chunk {

View File

@ -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)

View File

@ -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)

View File

@ -558,6 +558,11 @@ class TabbedBrowser(tabwidget.TabWidget):
# closing the last tab (before quitting) or shutting down # closing the last tab (before quitting) or shutting down
return return
tab = self.widget(idx) tab = self.widget(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)) log.modes.debug("Current tab changed, focusing {!r}".format(tab))
tab.setFocus() tab.setFocus()
for mode in [usertypes.KeyMode.hint, usertypes.KeyMode.insert, for mode in [usertypes.KeyMode.hint, usertypes.KeyMode.insert,

View File

@ -381,7 +381,7 @@ class _CrashDialog(QDialog):
lines = ['The report has been sent successfully. Thanks!'] lines = ['The report has been sent successfully. Thanks!']
lines.append("There was an error while getting the newest version: " lines.append("There was an error while getting the newest version: "
"{}. Please check for a new version on " "{}. 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)) "by yourself.".format(msg))
text = '<br/><br/>'.join(lines) text = '<br/><br/>'.join(lines)
self.finish() self.finish()

View File

@ -362,6 +362,9 @@ class IPCServer(QObject):
@pyqtSlot() @pyqtSlot()
def on_timeout(self): def on_timeout(self):
"""Cancel the current connection if it was idle for too long.""" """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 " log.ipc.error("IPC connection timed out "
"(socket 0x{:x}).".format(id(self._socket))) "(socket 0x{:x}).".format(id(self._socket)))
self._socket.disconnectFromServer() self._socket.disconnectFromServer()

View File

@ -45,6 +45,19 @@ class KeyHintView(QLabel):
update_geometry: Emitted when this widget should be resized/positioned. 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() update_geometry = pyqtSignal()
def __init__(self, win_id, parent=None): 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 = usertypes.Timer(self, 'keyhint_show')
self._show_timer.setInterval(500) self._show_timer.setInterval(500)
self._show_timer.timeout.connect(self.show) self._show_timer.timeout.connect(self.show)
style.set_register_stylesheet(self, style.set_register_stylesheet(self)
generator=self._generate_stylesheet)
def __repr__(self): def __repr__(self):
return utils.get_repr(self, win_id=self._win_id) return utils.get_repr(self, win_id=self._win_id)
@ -67,22 +79,6 @@ class KeyHintView(QLabel):
self.update_geometry.emit() self.update_geometry.emit()
super().showEvent(e) 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) @pyqtSlot(str)
def update_keyhint(self, modename, prefix): def update_keyhint(self, modename, prefix):
"""Show hints for the given prefix (or hide if prefix is empty). """Show hints for the given prefix (or hide if prefix is empty).

View File

@ -92,6 +92,12 @@ class BaseLineParser(QObject):
Args: Args:
mode: The mode to use ('a'/'r'/'w') 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 assert self._configfile is not None
if self._opened: if self._opened:

View File

@ -20,7 +20,8 @@
"""Misc. utility commands exposed to the user.""" """Misc. utility commands exposed to the user."""
import functools import functools
import types import os
import signal
import traceback import traceback
try: try:
@ -142,10 +143,7 @@ def debug_crash(typ='exception'):
typ: either 'exception' or 'segfault'. typ: either 'exception' or 'segfault'.
""" """
if typ == 'segfault': if typ == 'segfault':
# From python's Lib/test/crashers/bogus_code_obj.py os.kill(os.getpid(), signal.SIGSEGV)
co = types.CodeType(0, 0, 0, 0, 0, b'\x04\x71\x00\x00', (), (), (),
'', '', 1, b'')
exec(co)
raise Exception("Segfault failed (wat.)") raise Exception("Segfault failed (wat.)")
else: else:
raise Exception("Forced crash") raise Exception("Forced crash")
@ -173,12 +171,15 @@ def debug_console():
try: try:
con_widget = objreg.get('debug-console') con_widget = objreg.get('debug-console')
except KeyError: except KeyError:
log.misc.debug('initializing debug console')
con_widget = consolewidget.ConsoleWidget() con_widget = consolewidget.ConsoleWidget()
objreg.register('debug-console', con_widget) objreg.register('debug-console', con_widget)
if con_widget.isVisible(): if con_widget.isVisible():
log.misc.debug('hiding debug console')
con_widget.hide() con_widget.hide()
else: else:
log.misc.debug('showing debug console')
con_widget.show() con_widget.show()

View File

@ -94,7 +94,7 @@ LOGGER_NAMES = [
'commands', 'signals', 'downloads', 'commands', 'signals', 'downloads',
'js', 'qt', 'rfc6266', 'ipc', 'shlexer', 'js', 'qt', 'rfc6266', 'ipc', 'shlexer',
'save', 'message', 'config', 'sessions', 'save', 'message', 'config', 'sessions',
'webelem' 'webelem', 'prompt'
] ]
@ -139,6 +139,7 @@ message = logging.getLogger('message')
config = logging.getLogger('config') config = logging.getLogger('config')
sessions = logging.getLogger('sessions') sessions = logging.getLogger('sessions')
webelem = logging.getLogger('webelem') webelem = logging.getLogger('webelem')
prompt = logging.getLogger('prompt')
ram_handler = None ram_handler = None
@ -406,6 +407,10 @@ def qt_message_handler(msg_type, context, msg):
"Chromium-based browser to ", "Chromium-based browser to ",
# https://github.com/The-Compiler/qutebrowser/issues/1287 # https://github.com/The-Compiler/qutebrowser/issues/1287
"QXcbClipboard: SelectionRequest too old", "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': if sys.platform == 'darwin':
suppressed_msgs += [ suppressed_msgs += [

View File

@ -26,7 +26,7 @@ import traceback
from PyQt5.QtCore import pyqtSignal, QObject 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): def _log_stack(typ, stack):
@ -76,70 +76,83 @@ def info(message):
global_bridge.show_message.emit(usertypes.MessageLevel.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). """Ask a modular question in the statusbar (blocking).
Args: Args:
win_id: The ID of the window which is calling this function.
message: The message to display to the user. message: The message to display to the user.
mode: A PromptMode. mode: A PromptMode.
default: The default value to display. default: The default value to display.
text: Additional text to show
abort_on: A list of signals which abort the question if emitted.
Return: Return:
The answer the user gave or None if the prompt was cancelled. The answer the user gave or None if the prompt was cancelled.
""" """
q = usertypes.Question() question = _build_question(*args, **kwargs) # pylint: disable=missing-kwoa
q.text = message global_bridge.ask(question, blocking=True)
q.mode = mode answer = question.answer
q.default = default question.deleteLater()
bridge = objreg.get('message-bridge', scope='window', window=win_id) return answer
bridge.ask(q, blocking=True)
q.deleteLater()
return q.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. """Ask an async question in the statusbar.
Args: Args:
win_id: The ID of the window which is calling this function.
message: The message to display to the user. message: The message to display to the user.
mode: A PromptMode. mode: A PromptMode.
handler: The function to get called with the answer as argument. handler: The function to get called with the answer as argument.
default: The default value to display. default: The default value to display.
text: Additional text to show.
""" """
if not isinstance(mode, usertypes.PromptMode): question = _build_question(title, mode=mode, **kwargs)
raise TypeError("Mode {} is no PromptMode member!".format(mode)) question.answered.connect(handler)
q = usertypes.Question() question.completed.connect(question.deleteLater)
q.text = message global_bridge.ask(question, blocking=False)
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)
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. """Ask a yes/no question to the user and execute the given actions.
Args: Args:
win_id: The ID of the window which is calling this function.
message: The message to display to the user. message: The message to display to the user.
yes_action: Callable to be called when the user answered yes. yes_action: Callable to be called when the user answered yes.
no_action: Callable to be called when the user answered no. 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. default: True/False to set a default value, or None.
text: Additional text to show.
Return:
The question object.
""" """
q = usertypes.Question() kwargs['mode'] = usertypes.PromptMode.yesno
q.text = message question = _build_question(*args, **kwargs) # pylint: disable=missing-kwoa
q.mode = usertypes.PromptMode.yesno question.answered_yes.connect(yes_action)
q.default = default
q.answered_yes.connect(yes_action)
if no_action is not None: if no_action is not None:
q.answered_no.connect(no_action) question.answered_no.connect(no_action)
q.completed.connect(q.deleteLater) if cancel_action is not None:
bridge = objreg.get('message-bridge', scope='window', window=win_id) question.cancelled.connect(cancel_action)
bridge.ask(q, blocking=False)
question.completed.connect(question.deleteLater)
global_bridge.ask(question, blocking=False)
return question
class GlobalMessageBridge(QObject): class GlobalMessageBridge(QObject):
@ -150,9 +163,34 @@ class GlobalMessageBridge(QObject):
show_message: Show a message show_message: Show a message
arg 0: A MessageLevel member arg 0: A MessageLevel member
arg 1: The text to show 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) 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): class MessageBridge(QObject):
@ -164,36 +202,14 @@ class MessageBridge(QObject):
arg: The text to set. arg: The text to set.
s_maybe_reset_text: Reset the text if it hasn't been changed yet. s_maybe_reset_text: Reset the text if it hasn't been changed yet.
arg: The expected text. 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_set_text = pyqtSignal(str)
s_maybe_reset_text = pyqtSignal(str) s_maybe_reset_text = pyqtSignal(str)
s_set_cmd_text = pyqtSignal(str)
s_question = pyqtSignal(usertypes.Question, bool)
def __repr__(self): def __repr__(self):
return utils.get_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): def set_text(self, text, *, log_stack=False):
"""Set the normal text of the statusbar. """Set the normal text of the statusbar.
@ -214,19 +230,5 @@ class MessageBridge(QObject):
""" """
self.s_maybe_reset_text.emit(str(text)) 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() global_bridge = GlobalMessageBridge()

View File

@ -143,7 +143,12 @@ class ObjectRegistry(collections.UserDict):
"""Dump all objects as a list of strings.""" """Dump all objects as a list of strings."""
lines = [] lines = []
for name, obj in self.data.items(): 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 return lines

View File

@ -335,10 +335,11 @@ class Question(QObject):
For yesno, None (no default), True or False. For yesno, None (no default), True or False.
For text, a default text as string. For text, a default text as string.
For user_pwd, a default username 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. 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). answer: The value the user entered (as password for user_pwd).
is_aborted: Whether the question was aborted. is_aborted: Whether the question was aborted.
interrupted: Whether the question was interrupted by another one.
Signals: Signals:
answered: Emitted when the question has been answered by the user. answered: Emitted when the question has been answered by the user.
@ -364,14 +365,15 @@ class Question(QObject):
super().__init__(parent) super().__init__(parent)
self._mode = None self._mode = None
self.default = None self.default = None
self.title = None
self.text = None self.text = None
self.user = None
self.answer = None self.answer = None
self.is_aborted = False self.is_aborted = False
self.interrupted = False
def __repr__(self): def __repr__(self):
return utils.get_repr(self, text=self.text, mode=self._mode, return utils.get_repr(self, title=self.title, text=self.text,
default=self.default) mode=self._mode, default=self.default)
@property @property
def mode(self): def mode(self):
@ -405,6 +407,9 @@ class Question(QObject):
@pyqtSlot() @pyqtSlot()
def abort(self): def abort(self):
"""Abort the question.""" """Abort the question."""
if self.is_aborted:
log.misc.debug("Question was already aborted")
return
self.is_aborted = True self.is_aborted = True
try: try:
self.aborted.emit() self.aborted.emit()

View File

@ -258,7 +258,7 @@ def github_upload(artifacts, tag):
with open(filename, 'rb') as f: with open(filename, 'rb') as f:
basename = os.path.basename(filename) basename = os.path.basename(filename)
asset = release.upload_asset(mimetype, basename, f) asset = release.upload_asset(mimetype, basename, f)
asset.edit(filename, description) asset.edit(basename, description)
def pypi_upload(artifacts): def pypi_upload(artifacts):

View File

@ -115,8 +115,6 @@ PERFECT_FILES = [
'qutebrowser/mainwindow/statusbar/tabindex.py'), 'qutebrowser/mainwindow/statusbar/tabindex.py'),
('tests/unit/mainwindow/statusbar/test_textbase.py', ('tests/unit/mainwindow/statusbar/test_textbase.py',
'qutebrowser/mainwindow/statusbar/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', ('tests/unit/mainwindow/statusbar/test_url.py',
'qutebrowser/mainwindow/statusbar/url.py'), 'qutebrowser/mainwindow/statusbar/url.py'),
('tests/unit/mainwindow/test_messageview.py', ('tests/unit/mainwindow/test_messageview.py',

View File

@ -47,7 +47,7 @@ def pip_install(pkg):
print("Getting PyQt5...") print("Getting PyQt5...")
qt_version = '5.5.1' qt_version = '5.5.1'
pyqt_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( 'PyQt5-{}-gpl-Py3.4-Qt{}-x32.exe'.format(
pyqt_version, qt_version)) pyqt_version, qt_version))
@ -61,8 +61,8 @@ except (OSError, IOError):
print("Installing PyQt5...") print("Installing PyQt5...")
subprocess.check_call([r'C:\install-PyQt5.exe', '/S']) subprocess.check_call([r'C:\install-PyQt5.exe', '/S'])
print("Installing pip/tox") print("Installing tox")
pip_install(r'-rmisc\requirements\requirements-pip.txt') pip_install('pip')
pip_install(r'-rmisc\requirements\requirements-tox.txt') pip_install(r'-rmisc\requirements\requirements-tox.txt')
print("Linking Python...") print("Linking Python...")

View File

@ -54,7 +54,7 @@ brew_install() {
pip_install() { pip_install() {
# this uses python2 # 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() { npm_install() {
@ -95,7 +95,7 @@ elif [[ $TRAVIS_OS_NAME == osx ]]; then
brew --version brew --version
brew_install python3 qt5 pyqt5 brew_install python3 qt5 pyqt5
pip_install tox pip_install -r misc/requirements/requirements-tox.txt
pip --version pip --version
tox --version tox --version
check_pyqt check_pyqt
@ -105,14 +105,14 @@ fi
pyqt_pkgs="python3-pyqt5 python3-pyqt5.qtwebkit" pyqt_pkgs="python3-pyqt5 python3-pyqt5.qtwebkit"
pip_install pip pip_install pip
pip_install tox pip_install -r misc/requirements/requirements-tox.txt
pip --version pip --version
tox --version tox --version
case $TESTENV in case $TESTENV in
py34-cov) py34-cov)
pip_install codecov pip_install -r misc/requirements/requirements-codecov.txt
apt_install xvfb $pyqt_pkgs libpython3.4-dev apt_install xvfb $pyqt_pkgs libpython3.4-dev
check_pyqt check_pyqt
;; ;;

View File

@ -77,7 +77,11 @@ def get_info(pid):
for line in output.split('\n'): for line in output.split('\n'):
if not line.strip(): if not line.strip():
continue continue
try:
key, value = line.split(':', maxsplit=1) key, value = line.split(':', maxsplit=1)
except ValueError:
# systemd stack output
continue
data[key.strip()] = value.strip() data[key.strip()] = value.strip()
return data return data

View File

@ -104,7 +104,7 @@ setupdata = {
'version': '.'.join(str(e) for e in _get_constant('version_info')), 'version': '.'.join(str(e) for e in _get_constant('version_info')),
'description': _get_constant('description'), 'description': _get_constant('description'),
'long_description': read_file('README.asciidoc'), 'long_description': read_file('README.asciidoc'),
'url': 'http://www.qutebrowser.org/', 'url': 'https://www.qutebrowser.org/',
'requires': ['pypeg2', 'jinja2', 'pygments', 'PyYAML'], 'requires': ['pypeg2', 'jinja2', 'pygments', 'PyYAML'],
'author': _get_constant('author'), 'author': _get_constant('author'),
'author_email': _get_constant('email'), 'author_email': _get_constant('email'),

View 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();
}

View File

@ -0,0 +1,6 @@
QT += core widgets webenginewidgets
TARGET = testbrowser
TEMPLATE = app
SOURCES += main.cpp

View File

@ -121,7 +121,7 @@ def pytest_collection_modifyitems(config, items):
if item.get_marker('xfail_norun'): if item.get_marker('xfail_norun'):
item.add_marker(pytest.mark.xfail(run=False)) item.add_marker(pytest.mark.xfail(run=False))
if item.get_marker('flaky_once'): if item.get_marker('flaky_once'):
item.add_marker(pytest.mark.flaky(reruns=1)) item.add_marker(pytest.mark.flaky())
if deselected: if deselected:
deselected_items.append(item) deselected_items.append(item)

View File

@ -3,13 +3,14 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<script type="text/javascript"> <script type="text/javascript">
function prompter() { function prompter(defaultval) {
var reply = prompt("js prompt", "") var reply = prompt("js prompt", defaultval);
console.log("Prompt reply: " + reply) console.log("Prompt reply: " + reply);
} }
</script> </script>
</head> </head>
<body> <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> </body>
</html> </html>

View File

@ -188,6 +188,7 @@ def open_path(quteproc, path):
do_not_wait_suffix = ' without waiting' do_not_wait_suffix = ' without waiting'
as_url_suffix = ' as a URL' as_url_suffix = ' as a URL'
while True:
if path.endswith(new_tab_suffix): if path.endswith(new_tab_suffix):
path = path[:-len(new_tab_suffix)] path = path[:-len(new_tab_suffix)]
new_tab = True new_tab = True
@ -197,10 +198,11 @@ def open_path(quteproc, path):
elif path.endswith(as_url_suffix): elif path.endswith(as_url_suffix):
path = path[:-len(as_url_suffix)] path = path[:-len(as_url_suffix)]
as_url = True as_url = True
elif path.endswith(do_not_wait_suffix):
if path.endswith(do_not_wait_suffix):
path = path[:-len(do_not_wait_suffix)] path = path[:-len(do_not_wait_suffix)]
wait = False wait = False
else:
break
quteproc.open_path(path, new_tab=new_tab, new_window=new_window, quteproc.open_path(path, new_tab=new_tab, new_window=new_window,
as_url=as_url, wait=wait) as_url=as_url, wait=wait)

View File

@ -63,7 +63,7 @@ Feature: Downloading things from a website.
And I set storage -> prompt-download-directory to true And I set storage -> prompt-download-directory to true
And I open data/downloads/issue1243.html And I open data/downloads/issue1243.html
And I hint with args "links download" and follow a 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 Then the error "Download error: No handler found for qute://!" should be shown
Scenario: Downloading a data: link (issue 1214) 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 set storage -> prompt-download-directory to true
And I open data/downloads/issue1214.html And I open data/downloads/issue1214.html
And I hint with args "links download" and follow a 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 And I run :leave-mode
Then no crash should happen Then no crash should happen
@ -338,7 +338,7 @@ Feature: Downloading things from a website.
When I set storage -> prompt-download-directory to true When I set storage -> prompt-download-directory to true
And I open data/downloads/issue1725.html And I open data/downloads/issue1725.html
And I run :click-element id long-link 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 directly open the download
And I wait until the download is finished And I wait until the download is finished
Then "Opening * with [*python*]" should be logged 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 run :click-element id download
And I wait until the download is finished And I wait until the download is finished
Then the downloaded file test.pdf should exist 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

View File

@ -180,6 +180,11 @@ Feature: Using hints
- data/hints/iframe_target.html - data/hints/iframe_target.html
- data/hello.txt (active) - 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 ### hints -> auto-follow-timeout
@not_osx @not_osx

View File

@ -71,3 +71,8 @@ Feature: Javascript stuff
And I run :tab-only 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'); } 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 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

View File

@ -45,20 +45,6 @@ Feature: Various utility commands.
When I run :set-cmd-text foo When I run :set-cmd-text foo
Then the error "Invalid command text 'foo'." should be shown 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 ## :jseval
Scenario: :jseval Scenario: :jseval
@ -243,16 +229,6 @@ Feature: Various utility commands.
And I run :view-source And I run :view-source
Then the error "Already viewing source!" should be shown 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 # :help
Scenario: :help without topic Scenario: :help without topic
@ -348,7 +324,7 @@ Feature: Various utility commands.
And I open data/misc/test.pdf And I open data/misc/test.pdf
And I wait for "[qute://pdfjs/*] PDF * (PDF.js: *)" in the log And I wait for "[qute://pdfjs/*] PDF * (PDF.js: *)" in the log
And I run :jseval document.getElementById("download").click() 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 And I run :leave-mode
Then no crash should happen Then no crash should happen
@ -496,31 +472,6 @@ Feature: Various utility commands.
Then qute://log?level=error should be loaded Then qute://log?level=error should be loaded
And the page should contain the plaintext "No messages to show." 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 ## https://github.com/The-Compiler/qutebrowser/issues/1523
Scenario: Completing a single option argument Scenario: Completing a single option argument
@ -561,51 +512,6 @@ Feature: Various utility commands.
And I set general -> private-browsing to false And I set general -> private-browsing to false
Then the page should contain the plaintext "Local storage status: not working" 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 Scenario: Using 0 as count
When I run :scroll down with count 0 When I run :scroll down with count 0
Then the error "scroll: A zero count is not allowed for this command!" should be shown 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 run :command-accept
And I set general -> private-browsing to false And I set general -> private-browsing to false
Then the message "blah" should be shown 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

View File

@ -49,6 +49,14 @@ Feature: Prompts
And I run :prompt-accept And I run :prompt-accept
Then the javascript message "Prompt reply: prompt test" should be logged 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 @pyqt>=5.3.1
Scenario: Rejected javascript prompt Scenario: Rejected javascript prompt
When I open data/prompt/jsprompt.html When I open data/prompt/jsprompt.html
@ -58,6 +66,68 @@ Feature: Prompts
And I run :leave-mode And I run :leave-mode
Then the javascript message "Prompt reply: null" should be logged 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) # Shift-Insert with prompt (issue 1299)
@ -72,6 +142,17 @@ Feature: Prompts
And I run :prompt-accept And I run :prompt-accept
Then the javascript message "Prompt reply: insert test" should be logged 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 @pyqt>=5.3.1
Scenario: Using content -> ignore-javascript-prompt Scenario: Using content -> ignore-javascript-prompt
When I set content -> ignore-javascript-prompt to true When I set content -> ignore-javascript-prompt to true
@ -219,6 +300,44 @@ Feature: Prompts
"user": "user" "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 # :prompt-accept with value argument
Scenario: Javascript alert with value Scenario: Javascript alert with value
@ -249,3 +368,102 @@ Feature: Prompts
And I run :prompt-accept yes And I run :prompt-accept yes
Then the javascript message "confirm reply: true" should be logged Then the javascript message "confirm reply: true" should be logged
And the error "Invalid value nope - expected yes/no!" should be shown 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

View File

@ -31,8 +31,8 @@ pytestmark = pytest.mark.qtwebengine_todo("Downloads not implemented yet",
PROMPT_MSG = ("Asking question <qutebrowser.utils.usertypes.Question " PROMPT_MSG = ("Asking question <qutebrowser.utils.usertypes.Question "
"default={!r} mode=<PromptMode.download: 5> " "default={!r} mode=<PromptMode.download: 5> text=* "
"text='Save file to:'>, *") "title='Save file to:'>, *")
@bdd.given("I set up a temporary download dir") @bdd.given("I set up a temporary download dir")

View File

@ -39,8 +39,7 @@ def wait_ssl_page_finished_loading(quteproc, ssl_server):
@bdd.when("I wait for a prompt") @bdd.when("I wait for a prompt")
def wait_for_prompt(quteproc): def wait_for_prompt(quteproc):
quteproc.wait_for(message='Entering mode KeyMode.* (reason: question ' quteproc.wait_for(message='Asking question *')
'asked)')
@bdd.then("no prompt should be shown") @bdd.then("no prompt should be shown")

View 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')

View 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

View File

@ -256,6 +256,19 @@ Feature: Yanking and pasting.
# Compare # Compare
Then the javascript message "textarea contents: Hello world" should be logged 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 Scenario: Inserting text into a text field at specific position
When I set general -> log-javascript-console to info When I set general -> log-javascript-console to info
And I open data/paste_primary.html And I open data/paste_primary.html

View File

@ -152,7 +152,7 @@ def test_process_never_started(qtbot, quit_pyproc):
def test_wait_signal_raising(qtbot): def test_wait_signal_raising(qtbot):
"""testprocess._wait_signal should raise by default.""" """testprocess._wait_signal should raise by default."""
proc = testprocess.Process() proc = testprocess.Process()
with pytest.raises(qtbot.SignalTimeoutError): with pytest.raises(qtbot.TimeoutError):
with proc._wait_signal(proc.proc.started, timeout=0): with proc._wait_signal(proc.proc.started, timeout=0):
pass pass

View File

@ -67,12 +67,12 @@ class Request(testprocess.Line):
'/favicon.ico': [http.client.NOT_FOUND], '/favicon.ico': [http.client.NOT_FOUND],
'/does-not-exist': [http.client.NOT_FOUND], '/does-not-exist': [http.client.NOT_FOUND],
'/does-not-exist-2': [http.client.NOT_FOUND], '/does-not-exist-2': [http.client.NOT_FOUND],
'/basic-auth/user/password': '/status/404': [http.client.NOT_FOUND],
[http.client.UNAUTHORIZED, http.client.OK],
'/custom/redirect-later': [http.client.FOUND], '/custom/redirect-later': [http.client.FOUND],
'/custom/redirect-self': [http.client.FOUND], '/custom/redirect-self': [http.client.FOUND],
'/redirect-to': [http.client.FOUND], '/redirect-to': [http.client.FOUND],
'/status/404': [http.client.NOT_FOUND],
'/cookies/set': [http.client.FOUND], '/cookies/set': [http.client.FOUND],
} }
for i in range(15): for i in range(15):
@ -81,6 +81,10 @@ class Request(testprocess.Line):
http.client.FOUND] http.client.FOUND]
path_to_statuses['/absolute-redirect/{}'.format(i)] = [ path_to_statuses['/absolute-redirect/{}'.format(i)] = [
http.client.FOUND] 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] default_statuses = [http.client.OK, http.client.NOT_MODIFIED]
sanitized = QUrl('http://localhost' + self.path).path() # Remove ?foo sanitized = QUrl('http://localhost' + self.path).path() # Remove ?foo

View File

@ -131,8 +131,8 @@ def fake_statusbar(qtbot):
statusbar.container = container statusbar.container = container
vbox.addWidget(statusbar) vbox.addWidget(statusbar)
with qtbot.waitExposed(container):
container.show() container.show()
qtbot.waitForWindowShown(container)
return statusbar return statusbar

View File

@ -157,7 +157,7 @@ def pattern_match(*, pattern, value):
True on a match, False otherwise. True on a match, False otherwise.
""" """
re_pattern = '.*'.join(re.escape(part) for part in pattern.split('*')) 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(): def abs_datapath():

View File

@ -1,8 +1,8 @@
<head> <head>
<script type="text/javascript"> <script type="text/javascript">
function prompter() { function prompter() {
var reply = prompt("js prompt", "") var reply = prompt("js prompt", "");
alert("JS alert: " + reply + "!") alert("JS alert: " + reply + "!");
} }
</script> </script>
</head> </head>

View File

@ -121,5 +121,5 @@ def test_tab(qtbot, view, config_stub, tab_registry, mode_manager):
assert tab_w.history._history is view.history() assert tab_w.history._history is view.history()
assert view.parent() is tab_w assert view.parent() is tab_w
with qtbot.waitExposed(tab_w):
tab_w.show() tab_w.show()
qtbot.waitForWindowShown(tab_w)

View File

@ -396,7 +396,7 @@ class TestDefaultConfig:
If it did change, place a new qutebrowser-vx.y.z.conf in old_configs If it did change, place a new qutebrowser-vx.y.z.conf in old_configs
and then increment the version. and then increment the version.
""" """
assert qutebrowser.__version__ == '0.8.1' assert qutebrowser.__version__ == '0.8.4'
@pytest.mark.parametrize('filename', @pytest.mark.parametrize('filename',
os.listdir(os.path.join(os.path.dirname(__file__), 'old_configs')), os.listdir(os.path.join(os.path.dirname(__file__), 'old_configs')),

View File

@ -59,24 +59,6 @@ class Obj(QObject):
self.rendered_stylesheet = stylesheet 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]) @pytest.mark.parametrize('delete', [True, False])
def test_set_register_stylesheet(delete, qtbot, config_stub, caplog): def test_set_register_stylesheet(delete, qtbot, config_stub, caplog):
config_stub.data = {'fonts': {'foo': 'bar'}, 'colors': {}} 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 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: class TestColorDict:
@pytest.mark.parametrize('key, expected', [ @pytest.mark.parametrize('key, expected', [

View File

@ -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

View File

@ -51,7 +51,6 @@ def test_elided_text(fake_statusbar, qtbot, elidemode, check):
long_string = 'Hello world! ' * 100 long_string = 'Hello world! ' * 100
label.setText(long_string) label.setText(long_string)
label.show() label.show()
qtbot.waitForWindowShown(label)
assert check(label._elided_text) assert check(label._elided_text)
@ -74,8 +73,8 @@ def test_resize(qtbot):
long_string = 'Hello world! ' * 20 long_string = 'Hello world! ' * 20
label.setText(long_string) label.setText(long_string)
with qtbot.waitExposed(label):
label.show() label.show()
qtbot.waitForWindowShown(label)
text_1 = label._elided_text text_1 = label._elided_text
label.resize(20, 50) label.resize(20, 50)

View File

@ -55,8 +55,8 @@ def view(qtbot, config_stub):
usertypes.MessageLevel.warning, usertypes.MessageLevel.warning,
usertypes.MessageLevel.error]) usertypes.MessageLevel.error])
def test_single_message(qtbot, view, level): def test_single_message(qtbot, view, level):
with qtbot.waitExposed(view):
view.show_message(level, 'test') view.show_message(level, 'test')
qtbot.waitForWindowShown(view)
assert view._messages[0].isVisible() assert view._messages[0].isVisible()

View 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() == '/'

View File

@ -72,5 +72,6 @@ class TestTabWidget:
icon = QIcon(pixmap) icon = QIcon(pixmap)
tab = fake_web_tab() tab = fake_web_tab()
widget.addTab(tab, icon, 'foobar') widget.addTab(tab, icon, 'foobar')
with qtbot.waitExposed(widget):
widget.show() widget.show()
qtbot.waitForWindowShown(widget)

View File

@ -50,10 +50,13 @@ def keyhint(qtbot, config_stub, key_config_stub):
'colors': { 'colors': {
'keyhint.fg': 'white', 'keyhint.fg': 'white',
'keyhint.fg.suffix': 'yellow', 'keyhint.fg.suffix': 'yellow',
'keyhint.bg': 'black' 'keyhint.bg': 'black',
}, },
'fonts': {'keyhint': 'Comic Sans'}, 'fonts': {'keyhint': 'Comic Sans'},
'ui': {'keyhint-blacklist': '', 'status-position': 'bottom'}, 'ui': {
'keyhint-blacklist': '',
'status-position': 'bottom',
},
} }
keyhint = KeyHintView(0, None) keyhint = KeyHintView(0, None)
qtbot.add_widget(keyhint) qtbot.add_widget(keyhint)
@ -63,8 +66,8 @@ def keyhint(qtbot, config_stub, key_config_stub):
def test_show_and_hide(qtbot, keyhint): def test_show_and_hide(qtbot, keyhint):
with qtbot.waitSignal(keyhint.update_geometry): with qtbot.waitSignal(keyhint.update_geometry):
with qtbot.waitExposed(keyhint):
keyhint.show() keyhint.show()
qtbot.waitForWindowShown(keyhint)
keyhint.update_keyhint('normal', '') keyhint.update_keyhint('normal', '')
assert not keyhint.isVisible() assert not keyhint.isVisible()

View File

@ -21,14 +21,13 @@
import os import os
import pytest import pytest
from unittest import mock
from qutebrowser.misc import lineparser as lineparsermod from qutebrowser.misc import lineparser as lineparsermod
class TestBaseLineParser: class TestBaseLineParser:
"""Tests for BaseLineParser."""
CONFDIR = "this really doesn't matter" CONFDIR = "this really doesn't matter"
FILENAME = "and neither does this" FILENAME = "and neither does this"
@ -53,6 +52,45 @@ class TestBaseLineParser:
lineparser._prepare_save() lineparser._prepare_save()
os_mock.makedirs.assert_called_with(self.CONFDIR, 0o755) 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: class TestLineParser:
@ -63,7 +101,18 @@ class TestLineParser:
lp.save() lp.save()
return lp 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): def test_clear(self, tmpdir, lineparser):
"""Test if clear() empties its file."""
lineparser.data = ['one', 'two'] lineparser.data = ['one', 'two']
lineparser.save() lineparser.save()
assert (tmpdir / 'file').read() == 'one\ntwo\n' assert (tmpdir / 'file').read() == 'one\ntwo\n'
@ -71,11 +120,23 @@ class TestLineParser:
assert not lineparser.data assert not lineparser.data
assert (tmpdir / 'file').read() == '' 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: class TestAppendLineParser:
"""Tests for AppendLineParser."""
BASE_DATA = ['old data 1', 'old data 2'] BASE_DATA = ['old data 1', 'old data 2']
@pytest.fixture @pytest.fixture
@ -97,7 +158,17 @@ class TestAppendLineParser:
lineparser.save() lineparser.save()
assert (tmpdir / 'file').read() == self._get_expected(new_data) 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): def test_clear(self, tmpdir, lineparser):
"""Check if calling clear() empties both pending and persisted data."""
lineparser.new_data = ['one', 'two'] lineparser.new_data = ['one', 'two']
lineparser.save() lineparser.save()
assert (tmpdir / 'file').read() == "old data 1\nold data 2\none\ntwo\n" 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 not lineparser.new_data
assert (tmpdir / 'file').read() == "" 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): def test_iter_without_open(self, lineparser):
"""Test __iter__ without having called open().""" """Test __iter__ without having called open()."""
with pytest.raises(ValueError): with pytest.raises(ValueError):

View 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

View File

@ -771,7 +771,7 @@ class TestPyQIODevice:
with pytest.raises(io.UnsupportedOperation): with pytest.raises(io.UnsupportedOperation):
pyqiodev.seek(0, whence) pyqiodev.seek(0, whence)
@pytest.mark.flaky(reruns=1) @pytest.mark.flaky()
def test_qprocess(self, py_proc): def test_qprocess(self, py_proc):
"""Test PyQIODevice with a QProcess which is non-sequential. """Test PyQIODevice with a QProcess which is non-sequential.

View File

@ -15,7 +15,6 @@ setenv =
PYTEST_QT_API=pyqt5 PYTEST_QT_API=pyqt5
passenv = PYTHON DISPLAY XAUTHORITY HOME USERNAME USER CI TRAVIS XDG_* QUTE_* passenv = PYTHON DISPLAY XAUTHORITY HOME USERNAME USER CI TRAVIS XDG_* QUTE_*
deps = deps =
-r{toxinidir}/misc/requirements/requirements-pip.txt
-r{toxinidir}/requirements.txt -r{toxinidir}/requirements.txt
-r{toxinidir}/misc/requirements/requirements-tests.txt -r{toxinidir}/misc/requirements/requirements-tests.txt
commands = commands =
@ -93,7 +92,6 @@ commands =
[testenv:vulture] [testenv:vulture]
basepython = python3 basepython = python3
deps = deps =
-r{toxinidir}/misc/requirements/requirements-pip.txt
-r{toxinidir}/requirements.txt -r{toxinidir}/requirements.txt
-r{toxinidir}/misc/requirements/requirements-vulture.txt -r{toxinidir}/misc/requirements/requirements-vulture.txt
setenv = PYTHONPATH={toxinidir} setenv = PYTHONPATH={toxinidir}
@ -137,7 +135,6 @@ commands =
basepython = python3 basepython = python3
passenv = passenv =
deps = deps =
-r{toxinidir}/misc/requirements/requirements-pip.txt
-r{toxinidir}/misc/requirements/requirements-pyroma.txt -r{toxinidir}/misc/requirements/requirements-pyroma.txt
commands = commands =
{envdir}/bin/pyroma . {envdir}/bin/pyroma .
@ -146,7 +143,6 @@ commands =
basepython = python3 basepython = python3
passenv = passenv =
deps = deps =
-r{toxinidir}/misc/requirements/requirements-pip.txt
-r{toxinidir}/misc/requirements/requirements-check-manifest.txt -r{toxinidir}/misc/requirements/requirements-check-manifest.txt
commands = commands =
{envdir}/bin/check-manifest --ignore 'qutebrowser/git-commit-id,qutebrowser/html/doc,qutebrowser/html/doc/*,*/__pycache__' {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 whitelist_externals = git
passenv = TRAVIS_PULL_REQUEST passenv = TRAVIS_PULL_REQUEST
deps = deps =
-r{toxinidir}/misc/requirements/requirements-pip.txt
-r{toxinidir}/requirements.txt -r{toxinidir}/requirements.txt
commands = commands =
{envpython} scripts/link_pyqt.py --tox {envdir} {envpython} scripts/link_pyqt.py --tox {envdir}
@ -169,7 +164,6 @@ commands =
# fail if we didn't have a fallback defined. # fail if we didn't have a fallback defined.
basepython = {env:PYTHON:}/python.exe basepython = {env:PYTHON:}/python.exe
deps = deps =
-r{toxinidir}/misc/requirements/requirements-pip.txt
-r{toxinidir}/requirements.txt -r{toxinidir}/requirements.txt
-r{toxinidir}/misc/requirements/requirements-cxfreeze.txt -r{toxinidir}/misc/requirements/requirements-cxfreeze.txt
commands = commands =
@ -180,7 +174,6 @@ commands =
[testenv:pyinstaller] [testenv:pyinstaller]
basepython = python3 basepython = python3
deps = deps =
-r{toxinidir}/misc/requirements/requirements-pip.txt
-r{toxinidir}/requirements.txt -r{toxinidir}/requirements.txt
-r{toxinidir}/misc/requirements/requirements-pyinstaller.txt -r{toxinidir}/misc/requirements/requirements-pyinstaller.txt
commands = commands =

View File

@ -8,6 +8,7 @@
</div> </div>
<div id="menu"> <div id="menu">
<a href="/index.html">Home</a> <a href="/index.html">Home</a>
<a href="/doc/help/">Help</a>
<a href="/FAQ.html">FAQ</a> <a href="/FAQ.html">FAQ</a>
<a href="/INSTALL.html">Install</a> <a href="/INSTALL.html">Install</a>
<a href="/CHANGELOG.html">Changelog</a> <a href="/CHANGELOG.html">Changelog</a>

View File

@ -7,6 +7,15 @@ body {
font-family: "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif; font-family: "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif;
-webkit-text-size-adjust: none; -webkit-text-size-adjust: none;
color: #333333; color: #333333;
overflow-y: scroll;
}
p {
margin: 6px 0;
}
#preamble > .sectionbody > .paragraph > p {
margin: 0;
} }
#header { #header {
@ -67,8 +76,17 @@ body {
background-color: #1e89c6; background-color: #1e89c6;
} }
.sect1 { .sect1,
div.footnote {
padding: 10px 40px; padding: 10px 40px;
margin: 0 auto;
max-width: 1200px;
}
#footnotes > hr {
max-width: 1200px;
margin: 20px auto 10px;
width: calc(100% - 80px);
} }
.sect2 { .sect2 {
@ -76,7 +94,7 @@ body {
} }
div.footnote { div.footnote {
padding: 10px 40px; padding: 10px 40px 30px;
} }
hr { hr {
@ -136,6 +154,29 @@ code {
padding: 10px 10px; padding: 10px 10px;
background-color: #DDDDDD; background-color: #DDDDDD;
border-radius: 4px; 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 { table td {
@ -174,7 +215,7 @@ table td {
} }
</style> </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 href="media/font.css" rel="stylesheet" type="text/css" />
<link rel="icon" href="media/favicon.png" type="image/png" /> <link rel="icon" href="media/favicon.png" type="image/png" />
<style type="text/css"> <style type="text/css">