This commit is contained in:
thuck 2017-03-19 14:26:48 +01:00
commit 650b1de3b6
239 changed files with 8260 additions and 4093 deletions

View File

@ -6,6 +6,7 @@ exclude = .*,__pycache__,resources.py
# E501: Line too long # E501: Line too long
# E402: module level import not at top of file # E402: module level import not at top of file
# E266: too many leading '#' for block comment # E266: too many leading '#' for block comment
# E722: do not use bare except
# E731: do not assign a lambda expression, use a def # E731: do not assign a lambda expression, use a def
# (for pytest's __tracebackhide__) # (for pytest's __tracebackhide__)
# F401: Unused import # F401: Unused import
@ -24,7 +25,7 @@ exclude = .*,__pycache__,resources.py
# 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)
ignore = ignore =
E128,E226,E265,E501,E402,E266,E731, E128,E226,E265,E501,E402,E266,E722,E731,
F401, F401,
N802, N802,
P101,P102,P103, P101,P102,P103,

View File

@ -13,15 +13,30 @@ matrix:
env: DOCKER=archlinux env: DOCKER=archlinux
services: docker services: docker
- os: linux - os: linux
env: DOCKER=archlinux QUTE_BDD_WEBENGINE=true env: DOCKER=archlinux-webengine QUTE_BDD_WEBENGINE=true
services: docker
- os: linux
env: DOCKER=archlinux-ng
services: docker services: docker
- os: linux - os: linux
env: DOCKER=ubuntu-xenial env: DOCKER=ubuntu-xenial
services: docker services: docker
- os: linux
language: python
python: 3.6
env: TESTENV=py36-pyqt571
- os: linux
language: python
python: 3.5
env: TESTENV=py35-pyqt58
- os: linux
language: python
python: 3.6
env: TESTENV=py36-pyqt58
- os: osx - os: osx
env: TESTENV=py35 OSX=elcapitan env: TESTENV=py36 OSX=elcapitan
osx_image: xcode7.3 osx_image: xcode7.3
# https://github.com/The-Compiler/qutebrowser/issues/2013 # https://github.com/qutebrowser/qutebrowser/issues/2013
# - os: osx # - os: osx
# env: TESTENV=py35 OSX=yosemite # env: TESTENV=py35 OSX=yosemite
# osx_image: xcode6.4 # osx_image: xcode6.4
@ -43,14 +58,14 @@ matrix:
env: TESTENV=eslint env: TESTENV=eslint
allow_failures: allow_failures:
- os: osx - os: osx
env: TESTENV=py35 OSX=elcapitan env: TESTENV=py36 OSX=elcapitan
osx_image: xcode7.3 osx_image: xcode7.3
fast_finish: true fast_finish: true
cache: cache:
directories: directories:
- $HOME/.cache/pip - $HOME/.cache/pip
- $HOME/build/The-Compiler/qutebrowser/.cache - $HOME/build/qutebrowser/qutebrowser/.cache
before_install: before_install:
# We need to do this so we pick up the system-wide python properly # We need to do this so we pick up the system-wide python properly
@ -58,6 +73,7 @@ before_install:
install: install:
- bash scripts/dev/ci/travis_install.sh - bash scripts/dev/ci/travis_install.sh
- ulimit -c unlimited
script: script:
- bash scripts/dev/ci/travis_run.sh - bash scripts/dev/ci/travis_run.sh
@ -65,6 +81,9 @@ script:
after_success: after_success:
- '[[ $TESTENV == *-cov ]] && codecov -e TESTENV -X gcov' - '[[ $TESTENV == *-cov ]] && codecov -e TESTENV -X gcov'
after_failure:
- bash scripts/dev/ci/travis_backtrace.sh
notifications: notifications:
webhooks: webhooks:
- https://buildtimetrend.herokuapp.com/travis - https://buildtimetrend.herokuapp.com/travis

View File

@ -14,12 +14,131 @@ This project adheres to http://semver.org/[Semantic Versioning].
// `Fixed` for any bug fixes. // `Fixed` for any bug fixes.
// `Security` to invite users to upgrade in case of vulnerabilities. // `Security` to invite users to upgrade in case of vulnerabilities.
v0.9.0 (unreleased) v0.11.0 (unreleased)
------------------- --------------------
Added Added
~~~~~ ~~~~~
- New `:clear-messages` command to clear shown messages.
Changed
~~~~~~~
- When using QtWebEngine, the underlying Chromium version is now shown in the
version info.
Fixed
~~~~~
- Added a workaround for a black screen with QtWebEngine with some setups
(requires PyOpenGL to be installed)
v0.10.1
-------
Changed
~~~~~~~
- `--qt-arg` and `--qt-flag` can now also be used to pass arguments to Chromium when using QtWebEngine.
Fixed
~~~~~
- URLs are now redacted properly (username/password, and path/query for HTTPS) when using Proxy Autoconfig with QtWebKit
- Crash when updating adblock lists with invalid UTF8-chars in them
- Fixed the web inspector with QtWebEngine
- Version checks when starting qutebrowser now also take the Qt version PyQt was compiled against into account
- Hinting a input now doesn't select existing text anymore with QtWebKit
- The cursor now moves to the end when input elements are selected with QtWebEngine
- Download suffixes like (1) are now correctly stripped with QtWebEngine
- Crash when trying to print a tab which was closed in the meantime
- Crash when trying to open a file twice on Windows
v0.10.0
-------
Added
~~~~~
- Userscripts now have a new `$QUTE_COMMANDLINE_TEXT` environment variable, containing the current commandline contents
- New `ripbang` userscript to create a searchengine from a duckduckgo bang
- link:https://github.com/annulen/webkit/wiki[QtWebKit Reloaded] (also called QtWebKit-NG) is now fully supported
- Various new functionality with the QtWebEngine backend:
* Printing support with Qt >= 5.8
* Proxy support with Qt >= 5.8
* The `general -> print-element-backgrounds` option with Qt >= 5.8
* The `content -> cookies-store` option
* The `storage -> cache-size` option
* The `colors -> webpage.bg` option
* The HTML5 fullscreen API (e.g. youtube videos) with QtWebEngine
* `:download --mhtml`
- New `qute:history` URL and `:history` command to show the browsing history
- Open tabs are now auto-saved on each successful load and restored in case of a crash
- `:jseval` now has a `--file` flag so you can pass a javascript file
- `:session-save` now has a `--only-active-window` flag to only save the active window
- OS X builds are back, and built with QtWebEngine
Changed
~~~~~~~
- PyQt 5.7/Qt 5.7.1 is now required for the QtWebEngine backend
- Scrolling with the scrollwheel while holding shift now scrolls sideways
- New way of clicking hints which solves various small issues
- When yanking a mailto: link via hints, the mailto: prefix is now stripped
- Zoom level messages are now not stacked on top of each other anymore
- qutebrowser now automatically uses QtWebEngine if QtWebKit is unavailable
- :history-clear now asks for a confirmation, unless it's run with --force.
- `input -> mouse-zoom-divider` can now be 0 to disable zooming by mouse wheel
- `network -> proxy` can also be set to `pac+file://...` now to
use a local proxy autoconfig file (on QtWebKit)
Fixed
~~~~~
- Various bugs with Qt 5.8 and QtWebEngine:
* Segfault when closing a window
* Segfault when closing a tab with a search active
* Fixed various mouse actions (like automatically entering insert mode) not working
* Fixed hints sometimes not working
* Segfault when opening a URL after a QtWebEngine renderer process crash
- Other QtWebEngine fixes:
* Insert mode now gets entered correctly with a non-100% zoom
* Crash reports are now re-enabled when using QtWebEngine
* Fixed crashes when closing tabs while hinting
* Using :undo or :tab-clone with a view-source:// or chrome:// tab is now prevented, as it segfaults
- `:enter-mode` now refuses to enter modes which can't be entered manually (which caused crashes)
- `:record-macro` (`q`) now doesn't try to record macros for special keys without a text
- Fixed PAC (proxy autoconfig) not working with QtWebKit
- `:download --mhtml` now uses the new file dialog
- Word hints are now upper-cased correctly when hints -> uppercase is true
- Font validation is now more permissive in the config, allowing e.g. "Terminus
(TTF)" as font name
- Fixed starting on newer PyQt/sip versions with LibreSSL
- When downloading files with QtWebKit, a User-Agent header is set when possible
- Fixed showing of keybindings in the :help completion
- `:navigate prev/next` now detects `rel` attributes on `<a>` elements, and
handles multiple `rel` attributes correctly
- Fixed a crash when hinting with target `userscript` and spawning a non-existing script
- Lines in Jupyter notebook now trigger insert mode
v0.9.1
------
Fixed
~~~~~
- Prevent websites from downloading files to a location outside of the download
folder with QtWebEngine.
v0.9.0
------
Added
~~~~~
- *New dependency:* qutebrowser now depends on the Qt QML module, which is
packaged separately in some distributions (as Qt Declarative/QML/Quick).
- New `:rl-backward-kill-word` command which does what `:rl-unix-word-rubout` - New `:rl-backward-kill-word` command which does what `:rl-unix-word-rubout`
did before v0.8.0. did before v0.8.0.
- New `:rl-unix-filename-rubout` command which is similar to readline's - New `:rl-unix-filename-rubout` command which is similar to readline's
@ -52,6 +171,9 @@ Added
- New `:record-macro` (`q`) and `:run-macro` (`@`) commands for keyboard macros. - New `:record-macro` (`q`) and `:run-macro` (`@`) commands for keyboard macros.
- New `ui -> hide-scrollbar` setting to hide the scrollbar independently of the - New `ui -> hide-scrollbar` setting to hide the scrollbar independently of the
`user-stylesheet` setting. `user-stylesheet` setting.
- New `general -> default-open-dispatcher` setting to configure what to open
downloads with (instead of e.g. `xdg-open` on Linux).
- Support for PAC (proxy autoconfig) with QtWebKit
Changed Changed
~~~~~~~ ~~~~~~~
@ -149,6 +271,8 @@ Changed
- `ui -> window-title-format` now has a new `{backend} ` replacement - `ui -> window-title-format` now has a new `{backend} ` replacement
- `:hint` has a new `--add-history` argument to add the URL to the history for - `:hint` has a new `--add-history` argument to add the URL to the history for
yank/spawn targets. yank/spawn targets.
- `:set` now cycles through values if more than one argument is given.
- `:open` now opens `default-page` without an URL even without `-t`/`-b`/`-w` given.
Deprecated Deprecated
~~~~~~~~~~ ~~~~~~~~~~
@ -186,6 +310,10 @@ Fixed
- `: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
- 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
- qutebrowser now still starts with an incorrectly configured
`$XDG_RUNTIME_DIR`.
- Fixed crash when a userscript writes invalid unicode data to the FIFO
- Fixed crash when a included HTML was not found
v0.8.3 v0.8.3
------ ------
@ -794,7 +922,7 @@ Fixed
- Fixed horrible completion performance when the `shrink` option was set. - Fixed horrible completion performance when the `shrink` option was set.
- Sessions now store zoom/scroll-position correctly. - Sessions now store zoom/scroll-position correctly.
https://github.com/The-Compiler/qutebrowser/releases/tag/v0.2.1[v0.2.1] https://github.com/qutebrowser/qutebrowser/releases/tag/v0.2.1[v0.2.1]
----------------------------------------------------------------------- -----------------------------------------------------------------------
Fixed Fixed
@ -802,7 +930,7 @@ Fixed
- Added missing manpage (doc/qutebrowser.1.asciidoc) to archive. - Added missing manpage (doc/qutebrowser.1.asciidoc) to archive.
https://github.com/The-Compiler/qutebrowser/releases/tag/v0.2.0[v0.2.0] https://github.com/qutebrowser/qutebrowser/releases/tag/v0.2.0[v0.2.0]
----------------------------------------------------------------------- -----------------------------------------------------------------------
Added Added
@ -945,7 +1073,7 @@ Fixed
- Add a timeout to pastebin HTTP replies. - Add a timeout to pastebin HTTP replies.
- Various other fixes for small/rare bugs. - Various other fixes for small/rare bugs.
https://github.com/The-Compiler/qutebrowser/releases/tag/v0.1.4[v0.1.4] https://github.com/qutebrowser/qutebrowser/releases/tag/v0.1.4[v0.1.4]
----------------------------------------------------------------------- -----------------------------------------------------------------------
Changed Changed
@ -989,7 +1117,7 @@ Security
* Stop the icon database from being created when private-browsing is set to true. * Stop the icon database from being created when private-browsing is set to true.
* Disable insecure SSL ciphers. * Disable insecure SSL ciphers.
https://github.com/The-Compiler/qutebrowser/releases/tag/v0.1.3[v0.1.3] https://github.com/qutebrowser/qutebrowser/releases/tag/v0.1.3[v0.1.3]
----------------------------------------------------------------------- -----------------------------------------------------------------------
Changed Changed
@ -1023,7 +1151,7 @@ Security
* Fix for HTTP passwords accidentally being written to debug log. * Fix for HTTP passwords accidentally being written to debug log.
https://github.com/The-Compiler/qutebrowser/releases/tag/v0.1.2[v0.1.2] https://github.com/qutebrowser/qutebrowser/releases/tag/v0.1.2[v0.1.2]
----------------------------------------------------------------------- -----------------------------------------------------------------------
Changed Changed
@ -1055,7 +1183,7 @@ Fixed
* Fix user-stylesheet setting with an empty value. * Fix user-stylesheet setting with an empty value.
https://github.com/The-Compiler/qutebrowser/releases/tag/v0.1.1[v0.1.1] https://github.com/qutebrowser/qutebrowser/releases/tag/v0.1.1[v0.1.1]
----------------------------------------------------------------------- -----------------------------------------------------------------------
Added Added
@ -1113,7 +1241,7 @@ Fixed
* Ensure the docs get included in `freeze.py`. * Ensure the docs get included in `freeze.py`.
* Fix crash with `:zoom`. * Fix crash with `:zoom`.
https://github.com/The-Compiler/qutebrowser/releases/tag/v0.1[v0.1] https://github.com/qutebrowser/qutebrowser/releases/tag/v0.1[v0.1]
------------------------------------------------------------------- -------------------------------------------------------------------
Initial release. Initial release.

View File

@ -34,12 +34,12 @@ this. It might be a good idea to ask on the mailing list or IRC channel to make
sure nobody else started working on the same thing already. sure nobody else started working on the same thing already.
If you want to find something useful to do, check the If you want to find something useful to do, check the
https://github.com/The-Compiler/qutebrowser/issues[issue tracker]. Some https://github.com/qutebrowser/qutebrowser/issues[issue tracker]. Some
pointers: pointers:
* https://github.com/The-Compiler/qutebrowser/labels/easy[Issues which should * https://github.com/qutebrowser/qutebrowser/labels/easy[Issues which should
be easy to solve] be easy to solve]
* https://github.com/The-Compiler/qutebrowser/labels/not%20code[Issues which * https://github.com/qutebrowser/qutebrowser/labels/not%20code[Issues which
require little/no coding] require little/no coding]
There are also some things to do if you don't want to write code: There are also some things to do if you don't want to write code:
@ -55,7 +55,7 @@ qutebrowser uses http://git-scm.com/[git] for its development. You can clone
the repo like this: the repo like this:
---- ----
git clone https://github.com/The-Compiler/qutebrowser.git git clone https://github.com/qutebrowser/qutebrowser.git
---- ----
If you don't know git, a http://git-scm.com/[git cheatsheet] might come in If you don't know git, a http://git-scm.com/[git cheatsheet] might come in
@ -541,6 +541,12 @@ Setting up a Windows Development Environment
Note that the `flake8` tox env might not run due to encoding errors despite having LANG/LC_* set correctly. Note that the `flake8` tox env might not run due to encoding errors despite having LANG/LC_* set correctly.
Rebuilding the website
~~~~~~~~~~~~~~~~~~~~~~
If you want to rebuild the website, run `./scripts/asciidoc2html.py --website <outputdir>`.
Style conventions Style conventions
----------------- -----------------
@ -629,7 +635,7 @@ and make sure all bugs marked as resolved are actually fixed.
* Grep for `WORKAROUND` in the code and test if fixed stuff works without the * Grep for `WORKAROUND` in the code and test if fixed stuff works without the
workaround. workaround.
* Check relevant * Check relevant
https://github.com/The-Compiler/qutebrowser/issues?q=is%3Aopen+is%3Aissue+label%3Aqt[qutebrowser https://github.com/qutebrowser/qutebrowser/issues?q=is%3Aopen+is%3Aissue+label%3Aqt[qutebrowser
bugs] and check if they're fixed. bugs] and check if they're fixed.
New PyQt release New PyQt release
@ -638,6 +644,7 @@ New PyQt release
* See above * See above
* Install new PyQt in Windows VM (32- and 64-bit) * Install new PyQt in Windows VM (32- and 64-bit)
* Download new installer and update PyQt installer path in `ci_install.py`. * Download new installer and update PyQt installer path in `ci_install.py`.
* Update `tox.ini`/`.travis.yml`/`.appveyor.yml` to test new versions
qutebrowser release qutebrowser release
~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~
@ -659,7 +666,7 @@ qutebrowser release
* `git push origin`; `git push origin v0.$x.$y` * `git push origin`; `git push origin v0.$x.$y`
* If committing on minor branch, cherry-pick release commit to master. * If committing on minor branch, cherry-pick release commit to master.
* Create release on github * Create release on github
* Mark the milestone at https://github.com/The-Compiler/qutebrowser/milestones * Mark the milestone at https://github.com/qutebrowser/qutebrowser/milestones
as closed. as closed.
* Linux: Run `python3 scripts/dev/build_release.py --upload v0.$x.$y` * Linux: Run `python3 scripts/dev/build_release.py --upload v0.$x.$y`

View File

@ -105,13 +105,25 @@ It also works nicely with rapid hints:
How do I use qutebrowser with mutt?:: How do I use qutebrowser with mutt?::
Due to a Qt limitation, local files without `.html` extensions are Due to a Qt limitation, local files without `.html` extensions are
"downloaded" instead of displayed, see "downloaded" instead of displayed, see
https://github.com/The-Compiler/qutebrowser/issues/566[#566]. You can work https://github.com/qutebrowser/qutebrowser/issues/566[#566]. You can work
around this by using this in your `mailcap`: around this by using this in your `mailcap`:
+ +
---- ----
text/html; mv %s %s.html && qutebrowser %s.html >/dev/null 2>/dev/null; needsterminal; text/html; mv %s %s.html && qutebrowser %s.html >/dev/null 2>/dev/null; needsterminal;
---- ----
What is the difference between bookmarks and quickmarks?::
Bookmarks will always use the title of the website as their name, but with quickmarks
you can set your own title.
+
For example, if you bookmark multiple food recipe websites and use `:open`,
you have to type the title or address of the website.
+
When using quickmark, you can give them all names, like
`foodrecipes1`, `foodrecipes2` and so on. When you type
`:open foodrecipes`, you will see a list of all the food recipe sites,
without having to remember the exact website title or address.
== Troubleshooting == Troubleshooting
Configuration not saved after modifying config.:: Configuration not saved after modifying config.::
@ -129,7 +141,7 @@ Experiencing freezing on sites like duckduckgo and youtube.::
This issue could be caused by stale plugin files installed by `mozplugger` This issue could be caused by stale plugin files installed by `mozplugger`
if mozplugger was subsequently removed. if mozplugger was subsequently removed.
Try exiting qutebrowser and removing `~/.mozilla/plugins/mozplugger*.so`. Try exiting qutebrowser and removing `~/.mozilla/plugins/mozplugger*.so`.
See https://github.com/The-Compiler/qutebrowser/issues/357[Issue #357] See https://github.com/qutebrowser/qutebrowser/issues/357[Issue #357]
for more details. for more details.
Experiencing segfaults (crashes) on Debian systems.:: Experiencing segfaults (crashes) on Debian systems.::
@ -143,7 +155,7 @@ Segfaults on Facebook, Medium, Amazon, ...::
visiting these sites. This is caused by various bugs in Qt which have been visiting these sites. This is caused by various bugs in Qt which have been
fixed in Qt 5.4. However Debian and Ubuntu are slow to adopt or upgrade fixed in Qt 5.4. However Debian and Ubuntu are slow to adopt or upgrade
some packages. On Debian Jessie, it's recommended to use the experimental some packages. On Debian Jessie, it's recommended to use the experimental
repos as described in https://github.com/The-Compiler/qutebrowser/blob/master/INSTALL.asciidoc#on-debian--ubuntu[INSTALL]. repos as described in https://github.com/qutebrowser/qutebrowser/blob/master/INSTALL.asciidoc#on-debian--ubuntu[INSTALL].
+ +
Since Ubuntu Trusty (using Qt 5.2.1), Since Ubuntu Trusty (using Qt 5.2.1),
https://bugreports.qt.io/browse/QTBUG-42417?jql=component%20%3D%20WebKit%20and%20resolution%20%3D%20Done%20and%20fixVersion%20in%20(5.3.0%2C%20%225.3.0%20Alpha%22%2C%20%225.3.0%20Beta1%22%2C%20%225.3.0%20RC1%22%2C%205.3.1%2C%205.3.2%2C%205.4.0%2C%20%225.4.0%20Alpha%22%2C%20%225.4.0%20Beta%22%2C%20%225.4.0%20RC%22)%20and%20priority%20in%20(%22P2%3A%20Important%22%2C%20%22P1%3A%20Critical%22%2C%20%22P0%3A%20Blocker%22)[over https://bugreports.qt.io/browse/QTBUG-42417?jql=component%20%3D%20WebKit%20and%20resolution%20%3D%20Done%20and%20fixVersion%20in%20(5.3.0%2C%20%225.3.0%20Alpha%22%2C%20%225.3.0%20Beta1%22%2C%20%225.3.0%20RC1%22%2C%205.3.1%2C%205.3.2%2C%205.4.0%2C%20%225.4.0%20Alpha%22%2C%20%225.4.0%20Beta%22%2C%20%225.4.0%20RC%22)%20and%20priority%20in%20(%22P2%3A%20Important%22%2C%20%22P1%3A%20Critical%22%2C%20%22P0%3A%20Blocker%22)[over
@ -154,7 +166,7 @@ https://bugreports.qt.io/browse/QTBUG-42417?jql=component%20%3D%20WebKit%20and%2
My issue is not listed.:: My issue is not listed.::
If you experience any segfaults or crashes, you can report the issue in If you experience any segfaults or crashes, you can report the issue in
https://github.com/The-Compiler/qutebrowser/issues[the issue tracker] or https://github.com/qutebrowser/qutebrowser/issues[the issue tracker] or
using the `:report` command. using the `:report` command.
If you are reporting a segfault, make sure you read the If you are reporting a segfault, make sure you read the
link:doc/stacktrace.asciidoc[guide] on how to report them with all needed link:doc/stacktrace.asciidoc[guide] on how to report them with all needed

View File

@ -21,11 +21,11 @@ Using the packages
Install the dependencies via apt-get: Install the dependencies via apt-get:
---- ----
# apt-get install python3-lxml python-tox python3-pyqt5 python3-pyqt5.qtwebkit python3-sip python3-jinja2 python3-pygments python3-yaml # apt-get install python3-lxml python-tox python3-pyqt5 python3-pyqt5.qtwebkit python3-pyqt5.qtquick python3-sip python3-jinja2 python3-pygments python3-yaml
---- ----
Get the qutebrowser package from the Get the qutebrowser package from the
https://github.com/The-Compiler/qutebrowser/releases[release page] and download https://github.com/qutebrowser/qutebrowser/releases[release page] and download
the https://qutebrowser.org/python3-pypeg2_2.15.2-1_all.deb[PyPEG2 package]. the https://qutebrowser.org/python3-pypeg2_2.15.2-1_all.deb[PyPEG2 package].
Install the packages: Install the packages:
@ -56,7 +56,7 @@ Then install the packages like this:
---- ----
# apt-get update # apt-get update
# apt-get install -t experimental python3-pyqt5 python3-pyqt5.qtwebkit python3-sip python3-dev # apt-get install -t experimental python3-pyqt5 python3-pyqt5.qtwebkit python3-pyqt5.qtquick python3-sip python3-dev
# apt-get install python-tox # apt-get install python-tox
---- ----
@ -74,7 +74,7 @@ For distributions other than Debian or if you prefer to not use the
experimental repo: experimental repo:
---- ----
# apt-get install python3-pyqt5 python3-pyqt5.qtwebkit python-tox python3-sip python3-dev # apt-get install python3-pyqt5 python3-pyqt5.qtwebkit python3-pyqt5.qtquick python-tox python3-sip python3-dev
---- ----
To generate the documentation for the `:help` command, when using the git To generate the documentation for the `:help` command, when using the git
@ -214,7 +214,7 @@ Prebuilt binaries
~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~
Prebuilt standalone packages and MSI installers Prebuilt standalone packages and MSI installers
https://github.com/The-Compiler/qutebrowser/releases[are built] for every https://github.com/qutebrowser/qutebrowser/releases[are built] for every
release. release.
https://chocolatey.org/packages/qutebrowser[Chocolatey package] https://chocolatey.org/packages/qutebrowser[Chocolatey package]
@ -254,7 +254,7 @@ Prebuilt binary
The easiest way to install qutebrowser on OS X is to use the prebuilt `.app` The easiest way to install qutebrowser on OS X is to use the prebuilt `.app`
files from the files from the
https://github.com/The-Compiler/qutebrowser/releases[release page]. https://github.com/qutebrowser/qutebrowser/releases[release page].
This binary is also available through the This binary is also available through the
https://caskroom.github.io/[Homebrew Cask] package manager: https://caskroom.github.io/[Homebrew Cask] package manager:
@ -272,29 +272,21 @@ qutebrowser from source.
==== Homebrew ==== Homebrew
Homebrew's builds of Qt and PyQt no longer include QtWebKit, so it is necessary ----
to build from source. The build takes several hours on an average laptop. $ brew install qt5
$ pip3 install qutebrowser
----
Homebrew's builds of Qt and PyQt no longer include QtWebKit - if you need
QtWebKit support, it is necessary to build from source. The build takes several
hours on an average laptop.
---- ----
$ brew install qt5 --with-qtwebkit $ brew install qt5 --with-qtwebkit
$ brew install -s pyqt5 $ brew install -s pyqt5
$ pip3 install qutebrowser
$ pip3.5 install qutebrowser
---- ----
==== MacPorts
For MacPorts, run:
----
$ sudo port install python34 py34-jinja2 asciidoc py34-pygments py34-pyqt5
$ sudo pip3.4 install qutebrowser
----
The preferences for qutebrowser are stored in
`~/Library/Preferences/qutebrowser`, the application data is stored in
`~/Library/Application Support/qutebrowser`.
Packagers Packagers
--------- ---------
@ -313,7 +305,7 @@ First of all, clone the repository using http://git-scm.org/[git] and switch
into the repository folder: into the repository folder:
---- ----
$ git clone https://github.com/The-Compiler/qutebrowser.git $ git clone https://github.com/qutebrowser/qutebrowser.git
$ cd qutebrowser $ cd qutebrowser
---- ----

View File

@ -1,26 +1,26 @@
// If you are reading this in plaintext or on PyPi: // If you are reading this in plaintext or on PyPi:
// //
// A rendered version is available at: // A rendered version is available at:
// https://github.com/The-Compiler/qutebrowser/blob/master/README.asciidoc // https://github.com/qutebrowser/qutebrowser/blob/master/README.asciidoc
qutebrowser qutebrowser
=========== ===========
// QUTE_WEB_HIDE // QUTE_WEB_HIDE
image:icons/qutebrowser-64x64.png[qutebrowser logo] *A keyboard-driven, vim-like browser based on PyQt5 and QtWebKit.* image:icons/qutebrowser-64x64.png[qutebrowser logo] *A keyboard-driven, vim-like browser based on PyQt5 and Qt.*
image:https://img.shields.io/pypi/l/qutebrowser.svg?style=flat["license badge",link="https://github.com/The-Compiler/qutebrowser/blob/master/COPYING"] image:https://img.shields.io/pypi/l/qutebrowser.svg?style=flat["license badge",link="https://github.com/qutebrowser/qutebrowser/blob/master/COPYING"]
image:https://img.shields.io/pypi/v/qutebrowser.svg?style=flat["version badge",link="https://pypi.python.org/pypi/qutebrowser/"] image:https://img.shields.io/pypi/v/qutebrowser.svg?style=flat["version badge",link="https://pypi.python.org/pypi/qutebrowser/"]
image:https://requires.io/github/The-Compiler/qutebrowser/requirements.svg?branch=master["requirements badge",link="https://requires.io/github/The-Compiler/qutebrowser/requirements/?branch=master"] image:https://requires.io/github/qutebrowser/qutebrowser/requirements.svg?branch=master["requirements badge",link="https://requires.io/github/qutebrowser/qutebrowser/requirements/?branch=master"]
image:https://travis-ci.org/The-Compiler/qutebrowser.svg?branch=master["Build Status", link="https://travis-ci.org/The-Compiler/qutebrowser"] image:https://travis-ci.org/qutebrowser/qutebrowser.svg?branch=master["Build Status", link="https://travis-ci.org/qutebrowser/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://ci.appveyor.com/api/projects/status/5pyauww2k68bbow2/branch/master?svg=true["AppVeyor build status", link="https://ci.appveyor.com/project/qutebrowser/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/qutebrowser/qutebrowser/coverage.svg?branch=master["coverage badge",link="https://codecov.io/github/qutebrowser/qutebrowser?branch=master"]
link:https://www.qutebrowser.org[website] | link:https://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/qutebrowser/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
on Python, PyQt5 and QtWebKit and free software, licensed under the GPL. on Python and PyQt5 and free software, licensed under the GPL.
It was inspired by other browsers/addons like dwb and Vimperator/Pentadactyl. It was inspired by other browsers/addons like dwb and Vimperator/Pentadactyl.
@ -35,7 +35,7 @@ image:doc/img/hints.png["screenshot 4",width=300,link="doc/img/hints.png"]
Downloads Downloads
--------- ---------
See the https://github.com/The-Compiler/qutebrowser/releases[github releases See the https://github.com/qutebrowser/qutebrowser/releases[github releases
page] for available downloads (currently a source archive, and standalone page] for available downloads (currently a source archive, and standalone
packages as well as MSI installers for Windows). packages as well as MSI installers for Windows).
@ -99,7 +99,7 @@ The following software and libraries are required to run qutebrowser:
* http://www.python.org/[Python] 3.4 or newer * http://www.python.org/[Python] 3.4 or newer
* http://qt.io/[Qt] 5.2.0 or newer (5.5.1 recommended) * http://qt.io/[Qt] 5.2.0 or newer (5.5.1 recommended)
* QtWebKit * QtWebKit (old or link:https://github.com/annulen/webkit/wiki[reloaded]/NG) or QtWebEngine
* http://www.riverbankcomputing.com/software/pyqt/intro[PyQt] 5.2.0 or newer * http://www.riverbankcomputing.com/software/pyqt/intro[PyQt] 5.2.0 or newer
(5.5.1 recommended) for Python 3 (5.5.1 recommended) for Python 3
* https://pypi.python.org/pypi/setuptools/[pkg_resources/setuptools] * https://pypi.python.org/pypi/setuptools/[pkg_resources/setuptools]
@ -146,8 +146,8 @@ Contributors, sorted by the number of commits in descending order:
* Florian Bruhin * Florian Bruhin
* Daniel Schadt * Daniel Schadt
* Ryan Roden-Corrent * Ryan Roden-Corrent
* Jakub Klinkovský
* Jan Verbeek * Jan Verbeek
* Jakub Klinkovský
* Antoni Boucher * Antoni Boucher
* Lamar Pavel * Lamar Pavel
* Marshall Lochbaum * Marshall Lochbaum
@ -165,14 +165,17 @@ Contributors, sorted by the number of commits in descending order:
* Corentin Julé * Corentin Julé
* meles5 * meles5
* Philipp Hansch * Philipp Hansch
* Imran Sobir
* Panagiotis Ktistakis * Panagiotis Ktistakis
* Artur Shaik * Artur Shaik
* Nathan Isom * Nathan Isom
* Thorsten Wißmann * Thorsten Wißmann
* Austin Anderson * Austin Anderson
* Fritz Reichwald
* Jimmy * Jimmy
* Spreadyy
* Niklas Haas * Niklas Haas
* Maciej Wołczyk
* Spreadyy
* Alexey "Averrin" Nabrodov * Alexey "Averrin" Nabrodov
* nanjekyejoannah * nanjekyejoannah
* avk * avk
@ -184,22 +187,25 @@ Contributors, sorted by the number of commits in descending order:
* knaggita * knaggita
* Oliver Caldwell * Oliver Caldwell
* Julian Weigt * Julian Weigt
* Tomasz Kramkowski
* Sebastian Frysztak * Sebastian Frysztak
* Nikolay Amiantov
* Julie Engel
* Jonas Schürmann * Jonas Schürmann
* error800 * error800
* Michael Hoang * Michael Hoang
* Maciej Wołczyk
* Liam BEGUIN * Liam BEGUIN
* Julie Engel * Daniel Fiser
* skinnay * skinnay
* Zach-Button * Zach-Button
* Tomasz Kramkowski * Samuel Walladge
* Peter Rice * Peter Rice
* Ismail S * Ismail S
* Halfwit * Halfwit
* David Vogt * David Vogt
* Claire Cavanaugh * Claire Cavanaugh
* rikn00 * rikn00
* pkill9
* kanikaa1234 * kanikaa1234
* haitaka * haitaka
* Nick Ginther * Nick Ginther
@ -207,19 +213,23 @@ Contributors, sorted by the number of commits in descending order:
* Michael Ilsaas * Michael Ilsaas
* Martin Zimmermann * Martin Zimmermann
* Jussi Timperi * Jussi Timperi
* Fritz Reichwald * Cosmin Popescu
* Brian Jackson * Brian Jackson
* thuck * thuck
* sbinix * sbinix
* rsteube
* neeasade * neeasade
* jnphilipp * jnphilipp
* Yannis Rohloff
* Tobias Patzl * Tobias Patzl
* Stefan Tatschner * Stefan Tatschner
* Samuel Loury * Samuel Loury
* Peter Michely * Peter Michely
* Panashe M. Fundira * Panashe M. Fundira
* Lucas Hoffmann
* Link * Link
* Larry Hynes * Larry Hynes
* Kirill A. Shutemov
* Johannes Altmanninger * Johannes Altmanninger
* Jeremy Kaplan * Jeremy Kaplan
* Ismail * Ismail
@ -233,12 +243,10 @@ Contributors, sorted by the number of commits in descending order:
* Marcelo Santos * Marcelo Santos
* Joel Bradshaw * Joel Bradshaw
* Jean-Louis Fuchs * Jean-Louis Fuchs
* Fritz V155 Reichwald
* Franz Fellner * Franz Fellner
* Eric Drechsel * Eric Drechsel
* zwarag * zwarag
* xd1le * xd1le
* rsteube
* rmortens * rmortens
* oniondreams * oniondreams
* issue * issue
@ -247,6 +255,7 @@ Contributors, sorted by the number of commits in descending order:
* dylan araps * dylan araps
* addictedtoflames * addictedtoflames
* Xitian9 * Xitian9
* Vasilij Schneidermann
* Tomas Orsava * Tomas Orsava
* Tom Janson * Tom Janson
* Tobias Werth * Tobias Werth
@ -260,6 +269,7 @@ Contributors, sorted by the number of commits in descending order:
* Matthias Lisin * Matthias Lisin
* Marcel Schilling * Marcel Schilling
* Lazlow Carmichael * Lazlow Carmichael
* Kevin Wang
* Ján Kobezda * Ján Kobezda
* Johannes Martinsson * Johannes Martinsson
* Jean-Christophe Petkovich * Jean-Christophe Petkovich
@ -274,6 +284,7 @@ Contributors, sorted by the number of commits in descending order:
* Arseniy Seroka * Arseniy Seroka
* Andy Balaam * Andy Balaam
* Andreas Fischer * Andreas Fischer
* Akselmo
// QUTE_AUTHORS_END // QUTE_AUTHORS_END
The following people have contributed graphics: The following people have contributed graphics:

View File

@ -44,6 +44,7 @@ It is possible to run or bind multiple commands by separating them with `;;`.
|<<fullscreen,fullscreen>>|Toggle fullscreen mode. |<<fullscreen,fullscreen>>|Toggle fullscreen mode.
|<<help,help>>|Show help about a command or setting. |<<help,help>>|Show help about a command or setting.
|<<hint,hint>>|Start hinting. |<<hint,hint>>|Start hinting.
|<<history,history>>|Show browsing history.
|<<history-clear,history-clear>>|Clear all browsing history. |<<history-clear,history-clear>>|Clear all browsing history.
|<<home,home>>|Open main startpage in current tab. |<<home,home>>|Open main startpage in current tab.
|<<insert-text,insert-text>>|Insert text at cursor position. |<<insert-text,insert-text>>|Insert text at cursor position.
@ -84,7 +85,7 @@ It is possible to run or bind multiple commands by separating them with `;;`.
|<<tab-prev,tab-prev>>|Switch to the previous tab, or switch [count] tabs back. |<<tab-prev,tab-prev>>|Switch to the previous tab, or switch [count] tabs back.
|<<unbind,unbind>>|Unbind a keychain. |<<unbind,unbind>>|Unbind a keychain.
|<<undo,undo>>|Re-open a closed tab (optionally skipping [count] closed tabs). |<<undo,undo>>|Re-open a closed tab (optionally skipping [count] closed tabs).
|<<view-source,view-source>>|Show the source of the current page. |<<view-source,view-source>>|Show the source of the current page in a new tab.
|<<window-only,window-only>>|Close all windows except for the current one. |<<window-only,window-only>>|Close all windows except for the current one.
|<<wq,wq>>|Save open pages and quit. |<<wq,wq>>|Save open pages and quit.
|<<yank,yank>>|Yank something to the clipboard or primary selection. |<<yank,yank>>|Yank something to the clipboard or primary selection.
@ -319,8 +320,13 @@ How many pages to go forward.
[[fullscreen]] [[fullscreen]]
=== fullscreen === fullscreen
Syntax: +:fullscreen [*--leave*]+
Toggle fullscreen mode. Toggle fullscreen mode.
==== optional arguments
* +*-l*+, +*--leave*+: Only leave fullscreen if it was entered by the page.
[[help]] [[help]]
=== help === help
Syntax: +:help [*--tab*] [*--bg*] [*--window*] ['topic']+ Syntax: +:help [*--tab*] [*--bg*] [*--window*] ['topic']+
@ -413,12 +419,28 @@ Start hinting.
==== note ==== note
* This command does not split arguments after the last argument and handles quotes literally. * This command does not split arguments after the last argument and handles quotes literally.
[[history]]
=== history
Syntax: +:history [*--tab*] [*--bg*] [*--window*]+
Show browsing history.
==== optional arguments
* +*-t*+, +*--tab*+: Open in a new tab.
* +*-b*+, +*--bg*+: Open in a background tab.
* +*-w*+, +*--window*+: Open in a new window.
[[history-clear]] [[history-clear]]
=== history-clear === history-clear
Syntax: +:history-clear [*--force*]+
Clear all browsing history. Clear all browsing history.
Note this only clears the global history (e.g. `~/.local/share/qutebrowser/history` on Linux) but not cookies, the back/forward history of a tab, cache or other persistent data. Note this only clears the global history (e.g. `~/.local/share/qutebrowser/history` on Linux) but not cookies, the back/forward history of a tab, cache or other persistent data.
==== optional arguments
* +*-f*+, +*--force*+: Don't ask for confirmation.
[[home]] [[home]]
=== home === home
Open main startpage in current tab. Open main startpage in current tab.
@ -443,14 +465,15 @@ Note: Due a bug in Qt, the inspector will show incorrect request headers in the
[[jseval]] [[jseval]]
=== jseval === jseval
Syntax: +:jseval [*--quiet*] [*--world* 'world'] 'js-code'+ Syntax: +:jseval [*--file*] [*--quiet*] [*--world* 'world'] 'js-code'+
Evaluate a JavaScript string. Evaluate a JavaScript string.
==== positional arguments ==== positional arguments
* +'js-code'+: The string to evaluate. * +'js-code'+: The string/file to evaluate.
==== optional arguments ==== optional arguments
* +*-f*+, +*--file*+: Interpret js-code as a path to a file.
* +*-q*+, +*--quiet*+: Don't show resulting JS object. * +*-q*+, +*--quiet*+: Don't show resulting JS object.
* +*-w*+, +*--world*+: Ignored on QtWebKit. On QtWebEngine, a world ID or name to run the snippet in. * +*-w*+, +*--world*+: Ignored on QtWebKit. On QtWebEngine, a world ID or name to run the snippet in.
@ -718,7 +741,8 @@ Load a session.
[[session-save]] [[session-save]]
=== session-save === session-save
Syntax: +:session-save [*--current*] [*--quiet*] [*--force*] ['name']+ Syntax: +:session-save [*--current*] [*--quiet*] [*--force*] [*--only-active-window*]
['name']+
Save a session. Save a session.
@ -730,10 +754,11 @@ Save a session.
* +*-c*+, +*--current*+: Save the current session instead of the default. * +*-c*+, +*--current*+: Save the current session instead of the default.
* +*-q*+, +*--quiet*+: Don't show confirmation message. * +*-q*+, +*--quiet*+: Don't show confirmation message.
* +*-f*+, +*--force*+: Force saving internal sessions (starting with an underline). * +*-f*+, +*--force*+: Force saving internal sessions (starting with an underline).
* +*-o*+, +*--only-active-window*+: Saves only tabs of the currently active window.
[[set]] [[set]]
=== set === set
Syntax: +:set [*--temp*] [*--print*] ['section'] ['option'] ['value']+ Syntax: +:set [*--temp*] [*--print*] ['section'] ['option'] ['values' ['values' ...]]+
Set an option. Set an option.
@ -742,7 +767,7 @@ If the option name ends with '?', the value of the option is shown instead. If t
==== positional arguments ==== positional arguments
* +'section'+: The section where the option is in. * +'section'+: The section where the option is in.
* +'option'+: The name of the option. * +'option'+: The name of the option.
* +'value'+: The value to set. * +'values'+: The value to set, or the values to cycle through.
==== optional arguments ==== optional arguments
* +*-t*+, +*--temp*+: Set value temporarily. * +*-t*+, +*--temp*+: Set value temporarily.
@ -837,8 +862,7 @@ If neither count nor index are given, it behaves like tab-next. If both are give
==== count ==== count
The tab index to focus, starting with 1. The special value 0 focuses the rightmost tab. The tab index to focus, starting with 1.
[[tab-move]] [[tab-move]]
=== tab-move === tab-move
@ -899,7 +923,7 @@ Re-open a closed tab (optionally skipping [count] closed tabs).
[[view-source]] [[view-source]]
=== view-source === view-source
Show the source of the current page. Show the source of the current page in a new tab.
[[window-only]] [[window-only]]
=== window-only === window-only
@ -971,6 +995,7 @@ How many steps to zoom out.
|============== |==============
|Command|Description |Command|Description
|<<clear-keychain,clear-keychain>>|Clear the currently entered key chain. |<<clear-keychain,clear-keychain>>|Clear the currently entered key chain.
|<<clear-messages,clear-messages>>|Clear all message notifications.
|<<click-element,click-element>>|Click the element matching the given filter. |<<click-element,click-element>>|Click the element matching the given filter.
|<<command-accept,command-accept>>|Execute the command currently in the commandline. |<<command-accept,command-accept>>|Execute the command currently in the commandline.
|<<command-history-next,command-history-next>>|Go forward in the commandline history. |<<command-history-next,command-history-next>>|Go forward in the commandline history.
@ -1035,9 +1060,13 @@ How many steps to zoom out.
=== clear-keychain === clear-keychain
Clear the currently entered key chain. Clear the currently entered key chain.
[[clear-messages]]
=== clear-messages
Clear all message notifications.
[[click-element]] [[click-element]]
=== click-element === click-element
Syntax: +:click-element [*--target* 'target'] 'filter' 'value'+ Syntax: +:click-element [*--target* 'target'] [*--force-event*] 'filter' 'value'+
Click the element matching the given filter. Click the element matching the given filter.
@ -1050,6 +1079,7 @@ The given filter needs to result in exactly one element, otherwise, an error is
==== optional arguments ==== optional arguments
* +*-t*+, +*--target*+: How to open the clicked element (normal/tab/tab-bg/window). * +*-t*+, +*--target*+: How to open the clicked element (normal/tab/tab-bg/window).
* +*-f*+, +*--force-event*+: Force generating a fake click event.
[[command-accept]] [[command-accept]]
=== command-accept === command-accept
@ -1138,6 +1168,9 @@ Show an info message in the statusbar.
==== positional arguments ==== positional arguments
* +'text'+: The text to show. * +'text'+: The text to show.
==== count
How many times to show the message
[[message-warning]] [[message-warning]]
=== message-warning === message-warning
Syntax: +:message-warning 'text'+ Syntax: +:message-warning 'text'+
@ -1405,6 +1438,8 @@ Syntax: +:scroll 'direction'+
Scroll the current tab in the given direction. Scroll the current tab in the given direction.
Note you can use `:run-with-count` to have a keybinding with a bigger scroll increment.
==== positional arguments ==== positional arguments
* +'direction'+: In which direction to scroll (up/down/left/right/top/bottom). * +'direction'+: In which direction to scroll (up/down/left/right/top/bottom).

View File

@ -11,6 +11,7 @@
|<<general-ignore-case,ignore-case>>|Whether to find text on a page case-insensitively. |<<general-ignore-case,ignore-case>>|Whether to find text on a page case-insensitively.
|<<general-startpage,startpage>>|The default page(s) to open at the start, separated by commas. |<<general-startpage,startpage>>|The default page(s) to open at the start, separated by commas.
|<<general-yank-ignored-url-parameters,yank-ignored-url-parameters>>|The URL parameters to strip with :yank url, separated by commas. |<<general-yank-ignored-url-parameters,yank-ignored-url-parameters>>|The URL parameters to strip with :yank url, separated by commas.
|<<general-default-open-dispatcher,default-open-dispatcher>>|The default program used to open downloads. Set to an empty string to use the default internal handler.
|<<general-default-page,default-page>>|The page to open if :open -t/-b/-w is used without URL. Use `about:blank` for a blank page. |<<general-default-page,default-page>>|The page to open if :open -t/-b/-w is used without URL. Use `about:blank` for a blank page.
|<<general-auto-search,auto-search>>|Whether to start a search when something else than a URL is entered. |<<general-auto-search,auto-search>>|Whether to start a search when something else than a URL is entered.
|<<general-auto-save-config,auto-save-config>>|Whether to save the config automatically on quit. |<<general-auto-save-config,auto-save-config>>|Whether to save the config automatically on quit.
@ -21,7 +22,7 @@
|<<general-developer-extras,developer-extras>>|Enable extra tools for Web developers. |<<general-developer-extras,developer-extras>>|Enable extra tools for Web developers.
|<<general-print-element-backgrounds,print-element-backgrounds>>|Whether the background color and images are also drawn when the page is printed. |<<general-print-element-backgrounds,print-element-backgrounds>>|Whether the background color and images are also drawn when the page is printed.
|<<general-xss-auditing,xss-auditing>>|Whether load requests should be monitored for cross-site scripting attempts. |<<general-xss-auditing,xss-auditing>>|Whether load requests should be monitored for cross-site scripting attempts.
|<<general-site-specific-quirks,site-specific-quirks>>|Enable workarounds for broken sites. |<<general-site-specific-quirks,site-specific-quirks>>|Enable QtWebKit workarounds for broken sites.
|<<general-default-encoding,default-encoding>>|Default encoding to use for websites. |<<general-default-encoding,default-encoding>>|Default encoding to use for websites.
|<<general-new-instance-open-target,new-instance-open-target>>|How to open links in an existing instance if a new one is launched. |<<general-new-instance-open-target,new-instance-open-target>>|How to open links in an existing instance if a new one is launched.
|<<general-new-instance-open-target.window,new-instance-open-target.window>>|Which window to choose when opening links as new tabs. |<<general-new-instance-open-target.window,new-instance-open-target.window>>|Which window to choose when opening links as new tabs.
@ -47,7 +48,7 @@
|<<ui-user-stylesheet,user-stylesheet>>|User stylesheet to use (absolute filename or filename relative to the config directory). Will expand environment variables. |<<ui-user-stylesheet,user-stylesheet>>|User stylesheet to use (absolute filename or filename relative to the config directory). Will expand environment variables.
|<<ui-hide-scrollbar,hide-scrollbar>>|Hide the main scrollbar. |<<ui-hide-scrollbar,hide-scrollbar>>|Hide the main scrollbar.
|<<ui-css-media-type,css-media-type>>|Set the CSS media type. |<<ui-css-media-type,css-media-type>>|Set the CSS media type.
|<<ui-smooth-scrolling,smooth-scrolling>>|Whether to enable smooth scrolling for webpages. |<<ui-smooth-scrolling,smooth-scrolling>>|Whether to enable smooth scrolling for web pages. Note smooth scrolling does not work with the :scroll-px command.
|<<ui-remove-finished-downloads,remove-finished-downloads>>|Number of milliseconds to wait before removing finished downloads. Will not be removed if value is -1. |<<ui-remove-finished-downloads,remove-finished-downloads>>|Number of milliseconds to wait before removing finished downloads. Will not be removed if value is -1.
|<<ui-hide-statusbar,hide-statusbar>>|Whether to hide the statusbar unless a message is shown. |<<ui-hide-statusbar,hide-statusbar>>|Whether to hide the statusbar unless a message is shown.
|<<ui-statusbar-padding,statusbar-padding>>|Padding for statusbar (top, bottom, left, right). |<<ui-statusbar-padding,statusbar-padding>>|Padding for statusbar (top, bottom, left, right).
@ -56,6 +57,7 @@
|<<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. |<<ui-prompt-radius,prompt-radius>>|The rounding radius for the edges of prompts.
|<<ui-prompt-filebrowser,prompt-filebrowser>>|Show a filebrowser in upload/download prompts.
|============== |==============
.Quick reference for section ``network'' .Quick reference for section ``network''
@ -146,7 +148,7 @@
|<<storage-offline-storage-database,offline-storage-database>>|Whether support for the HTML 5 offline storage feature is enabled. |<<storage-offline-storage-database,offline-storage-database>>|Whether support for the HTML 5 offline storage feature is enabled.
|<<storage-offline-web-application-storage,offline-web-application-storage>>|Whether support for the HTML 5 web application cache feature is enabled. |<<storage-offline-web-application-storage,offline-web-application-storage>>|Whether support for the HTML 5 web application cache feature is enabled.
|<<storage-local-storage,local-storage>>|Whether support for the HTML 5 local storage feature is enabled. |<<storage-local-storage,local-storage>>|Whether support for the HTML 5 local storage feature is enabled.
|<<storage-cache-size,cache-size>>|Size of the HTTP network cache. |<<storage-cache-size,cache-size>>|Size of the HTTP network cache. Empty to use the default value.
|============== |==============
.Quick reference for section ``content'' .Quick reference for section ``content''
@ -156,7 +158,7 @@
|<<content-allow-images,allow-images>>|Whether images are automatically loaded in web pages. |<<content-allow-images,allow-images>>|Whether images are automatically loaded in web pages.
|<<content-allow-javascript,allow-javascript>>|Enables or disables the running of JavaScript programs. |<<content-allow-javascript,allow-javascript>>|Enables or disables the running of JavaScript programs.
|<<content-allow-plugins,allow-plugins>>|Enables or disables plugins in Web pages. |<<content-allow-plugins,allow-plugins>>|Enables or disables plugins in Web pages.
|<<content-webgl,webgl>>|Enables or disables WebGL. For QtWebEngine, Qt/PyQt >= 5.7 is required for this setting. |<<content-webgl,webgl>>|Enables or disables WebGL.
|<<content-css-regions,css-regions>>|Enable or disable support for CSS regions. |<<content-css-regions,css-regions>>|Enable or disable support for CSS regions.
|<<content-hyperlink-auditing,hyperlink-auditing>>|Enable or disable hyperlink auditing (<a ping>). |<<content-hyperlink-auditing,hyperlink-auditing>>|Enable or disable hyperlink auditing (<a ping>).
|<<content-geolocation,geolocation>>|Allow websites to request geolocations. |<<content-geolocation,geolocation>>|Allow websites to request geolocations.
@ -170,7 +172,7 @@
|<<content-local-content-can-access-remote-urls,local-content-can-access-remote-urls>>|Whether locally loaded documents are allowed to access remote urls. |<<content-local-content-can-access-remote-urls,local-content-can-access-remote-urls>>|Whether locally loaded documents are allowed to access remote urls.
|<<content-local-content-can-access-file-urls,local-content-can-access-file-urls>>|Whether locally loaded documents are allowed to access other local urls. |<<content-local-content-can-access-file-urls,local-content-can-access-file-urls>>|Whether locally loaded documents are allowed to access other local urls.
|<<content-cookies-accept,cookies-accept>>|Control which cookies to accept. |<<content-cookies-accept,cookies-accept>>|Control which cookies to accept.
|<<content-cookies-store,cookies-store>>|Whether to store cookies. |<<content-cookies-store,cookies-store>>|Whether to store cookies. Note this option needs a restart with QtWebEngine.
|<<content-host-block-lists,host-block-lists>>|List of URLs of lists which contain hosts to block. |<<content-host-block-lists,host-block-lists>>|List of URLs of lists which contain hosts to block.
|<<content-host-blocking-enabled,host-blocking-enabled>>|Whether host blocking is enabled. |<<content-host-blocking-enabled,host-blocking-enabled>>|Whether host blocking is enabled.
|<<content-host-blocking-whitelist,host-blocking-whitelist>>|List of domains that should always be loaded, despite being ad-blocked. |<<content-host-blocking-whitelist,host-blocking-whitelist>>|List of domains that should always be loaded, despite being ad-blocked.
@ -330,6 +332,14 @@ The URL parameters to strip with :yank url, separated by commas.
Default: +pass:[ref,utm_source,utm_medium,utm_campaign,utm_term,utm_content]+ Default: +pass:[ref,utm_source,utm_medium,utm_campaign,utm_term,utm_content]+
[[general-default-open-dispatcher]]
=== default-open-dispatcher
The default program used to open downloads. Set to an empty string to use the default internal handler.
Any {} in the string will be expanded to the filename, else the filename will be appended.
Default: empty
[[general-default-page]] [[general-default-page]]
=== default-page === default-page
The page to open if :open -t/-b/-w is used without URL. Use `about:blank` for a blank page. The page to open if :open -t/-b/-w is used without URL. Use `about:blank` for a blank page.
@ -397,7 +407,7 @@ This setting is only available with the QtWebKit backend.
=== developer-extras === developer-extras
Enable extra tools for Web developers. Enable extra tools for Web developers.
This needs to be enabled for `:inspector` to work and also adds an _Inspect_ entry to the context menu. This needs to be enabled for `:inspector` to work and also adds an _Inspect_ entry to the context menu. For QtWebEngine, see 'qutebrowser --help' instead.
Valid values: Valid values:
@ -406,9 +416,12 @@ Valid values:
Default: +pass:[false]+ Default: +pass:[false]+
This setting is only available with the QtWebKit backend.
[[general-print-element-backgrounds]] [[general-print-element-backgrounds]]
=== print-element-backgrounds === print-element-backgrounds
Whether the background color and images are also drawn when the page is printed. Whether the background color and images are also drawn when the page is printed.
This setting only works with Qt 5.8 or newer when using the QtWebEngine backend.
Valid values: Valid values:
@ -417,8 +430,6 @@ Valid values:
Default: +pass:[true]+ Default: +pass:[true]+
This setting is only available with the QtWebKit backend.
[[general-xss-auditing]] [[general-xss-auditing]]
=== xss-auditing === xss-auditing
Whether load requests should be monitored for cross-site scripting attempts. Whether load requests should be monitored for cross-site scripting attempts.
@ -434,7 +445,7 @@ Default: +pass:[false]+
[[general-site-specific-quirks]] [[general-site-specific-quirks]]
=== site-specific-quirks === site-specific-quirks
Enable workarounds for broken sites. Enable QtWebKit workarounds for broken sites.
Valid values: Valid values:
@ -644,7 +655,7 @@ This setting is only available with the QtWebKit backend.
[[ui-smooth-scrolling]] [[ui-smooth-scrolling]]
=== smooth-scrolling === smooth-scrolling
Whether to enable smooth scrolling for webpages. Whether to enable smooth scrolling for web pages. Note smooth scrolling does not work with the :scroll-px command.
Valid values: Valid values:
@ -727,6 +738,17 @@ The rounding radius for the edges of prompts.
Default: +pass:[8]+ Default: +pass:[8]+
[[ui-prompt-filebrowser]]
=== prompt-filebrowser
Show a filebrowser in upload/download prompts.
Valid values:
* +true+
* +false+
Default: +pass:[true]+
== network == network
Settings related to the network. Settings related to the network.
@ -773,6 +795,8 @@ The proxy to use.
In addition to the listed values, you can use a `socks://...` or `http://...` URL. In addition to the listed values, you can use a `socks://...` or `http://...` URL.
This setting only works with Qt 5.8 or newer when using the QtWebEngine backend.
Valid values: Valid values:
* +system+: Use the system wide proxy. * +system+: Use the system wide proxy.
@ -780,8 +804,6 @@ Valid values:
Default: +pass:[system]+ Default: +pass:[system]+
This setting is only available with the QtWebKit backend.
[[network-proxy-dns-requests]] [[network-proxy-dns-requests]]
=== proxy-dns-requests === proxy-dns-requests
Whether to send DNS requests over the configured proxy. Whether to send DNS requests over the configured proxy.
@ -1360,9 +1382,9 @@ Default: +pass:[true]+
[[storage-cache-size]] [[storage-cache-size]]
=== cache-size === cache-size
Size of the HTTP network cache. Size of the HTTP network cache. Empty to use the default value.
Default: +pass:[52428800]+ Default: empty
== content == content
Loaded plugins/scripts and allowed actions. Loaded plugins/scripts and allowed actions.
@ -1404,14 +1426,14 @@ Default: +pass:[false]+
[[content-webgl]] [[content-webgl]]
=== webgl === webgl
Enables or disables WebGL. For QtWebEngine, Qt/PyQt >= 5.7 is required for this setting. Enables or disables WebGL.
Valid values: Valid values:
* +true+ * +true+
* +false+ * +false+
Default: +pass:[false]+ Default: +pass:[true]+
[[content-css-regions]] [[content-css-regions]]
=== css-regions === css-regions
@ -1502,6 +1524,7 @@ This setting is only available with the QtWebKit backend.
[[content-javascript-can-access-clipboard]] [[content-javascript-can-access-clipboard]]
=== javascript-can-access-clipboard === javascript-can-access-clipboard
Whether JavaScript programs can read or write to the clipboard. Whether JavaScript programs can read or write to the clipboard.
With QtWebEngine, writing the clipboard as response to a user interaction is always allowed.
Valid values: Valid values:
@ -1571,7 +1594,7 @@ This setting is only available with the QtWebKit backend.
[[content-cookies-store]] [[content-cookies-store]]
=== cookies-store === cookies-store
Whether to store cookies. Whether to store cookies. Note this option needs a restart with QtWebEngine.
Valid values: Valid values:
@ -1580,8 +1603,6 @@ Valid values:
Default: +pass:[true]+ Default: +pass:[true]+
This setting is only available with the QtWebKit backend.
[[content-host-block-lists]] [[content-host-block-lists]]
=== host-block-lists === host-block-lists
List of URLs of lists which contain hosts to block. List of URLs of lists which contain hosts to block.
@ -1643,7 +1664,7 @@ Mode to use for hints.
Valid values: Valid values:
* +number+: Use numeric hints. * +number+: Use numeric hints. (In this mode you can also type letters form the hinted element to filter and reduce the number of elements that are hinted.)
* +letter+: Use the chars in the hints -> chars setting. * +letter+: Use the chars in the hints -> chars setting.
* +word+: Use hints words based on the html elements and the extra words. * +word+: Use hints words based on the html elements and the extra words.
@ -2130,8 +2151,6 @@ Background color for webpages if unset (or empty to use the theme's color)
Default: +pass:[white]+ Default: +pass:[white]+
This setting is only available with the QtWebKit backend.
[[colors-keyhint.fg]] [[colors-keyhint.fg]]
=== keyhint.fg === keyhint.fg
Text color for the keyhint widget. Text color for the keyhint widget.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 989 KiB

After

Width:  |  Height:  |  Size: 989 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

After

Width:  |  Height:  |  Size: 43 KiB

View File

@ -9,7 +9,8 @@ Basic keybindings to get you started
------------------------------------ ------------------------------------
* Use the arrow keys or `hjkl` to move around a webpage (vim-like syntax is used in quite a few places) * Use the arrow keys or `hjkl` to move around a webpage (vim-like syntax is used in quite a few places)
* To go to a new webpage, press `o`, then type a url, then press Enter (Use `O` to open the url in a new tab). If what you've typed isn't a url, then a search engine will be used instead (DuckDuckGo, by default) * To go to a new webpage, press `o`, then type a url, then press Enter (Use `O` to open the url in a new tab, `go` to edit the current URL)
* If what you've typed isn't a url, then a search engine will be used instead (DuckDuckGo, by default)
* To switch between tabs, use `J` (next tab) and `K` (previous tab), or press `<Alt-num>`, where `num` is the position of the tab to switch to * To switch between tabs, use `J` (next tab) and `K` (previous tab), or press `<Alt-num>`, where `num` is the position of the tab to switch to
* To close the current tab, press `d` (and press `u` to undo closing a tab) * To close the current tab, press `d` (and press `u` to undo closing a tab)
* Use `H` and `L` to go back and forth in the history * Use `H` and `L` to go back and forth in the history
@ -30,7 +31,7 @@ image:http://qutebrowser.org/img/cheatsheet-small.png["qutebrowser key binding c
* Run `:adblock-update` to download adblock lists and activate adblocking. * Run `:adblock-update` to download adblock lists and activate adblocking.
* If you just cloned the repository, you'll need to run * If you just cloned the repository, you'll need to run
`scripts/asciidoc2html.py` to generate the documentation. `scripts/asciidoc2html.py` to generate the documentation.
* Go to the link:qute://settings[settings page] to set up qutebrowser the way you want it. * Go to the link:qute://settings[settings page] to set up qutebrowser the way you want it. (Currently not available with the QtWebEngine backend and on the OS X build - use the `:set` command instead)
* Subscribe to * Subscribe to
https://lists.schokokeks.org/mailman/listinfo.cgi/qutebrowser[the mailinglist] or https://lists.schokokeks.org/mailman/listinfo.cgi/qutebrowser[the mailinglist] or
https://lists.schokokeks.org/mailman/listinfo.cgi/qutebrowser-announce[the announce-only mailinglist]. https://lists.schokokeks.org/mailman/listinfo.cgi/qutebrowser-announce[the announce-only mailinglist].

View File

@ -10,7 +10,7 @@
:homepage: https://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.
== SYNOPSIS == SYNOPSIS
*qutebrowser* ['-OPTION' ['...']] [':COMMAND' ['...']] ['URL' ['...']] *qutebrowser* ['-OPTION' ['...']] [':COMMAND' ['...']] ['URL' ['...']]
@ -59,6 +59,9 @@ show it.
*--backend* '{webkit,webengine}':: *--backend* '{webkit,webengine}'::
Which backend to use (webengine backend is EXPERIMENTAL!). Which backend to use (webengine backend is EXPERIMENTAL!).
*--enable-webengine-inspector*::
Enable the web inspector for QtWebEngine. Note that this is a SECURITY RISK and you should not visit untrusted websites with the inspector turned on. See https://bugreports.qt.io/browse/QTBUG-50725 for more details.
=== debug arguments === debug arguments
*-l* '{critical,error,warning,info,debug,vdebug}', *--loglevel* '{critical,error,warning,info,debug,vdebug}':: *-l* '{critical,error,warning,info,debug,vdebug}', *--loglevel* '{critical,error,warning,info,debug,vdebug}'::
Set loglevel Set loglevel
@ -124,7 +127,7 @@ defaults.
== BUGS == BUGS
Bugs are tracked in the Github issue tracker at Bugs are tracked in the Github issue tracker at
https://github.com/The-Compiler/qutebrowser/issues. https://github.com/qutebrowser/qutebrowser/issues.
If you found a bug, use the built-in ':report' command to create a bug report If you found a bug, use the built-in ':report' command to create a bug report
with all information needed. with all information needed.
@ -157,7 +160,7 @@ https://lists.schokokeks.org/mailman/listinfo.cgi/qutebrowser
https://lists.schokokeks.org/mailman/listinfo.cgi/qutebrowser-announce https://lists.schokokeks.org/mailman/listinfo.cgi/qutebrowser-announce
* IRC: irc://irc.freenode.org/#qutebrowser[`#qutebrowser`] on * IRC: irc://irc.freenode.org/#qutebrowser[`#qutebrowser`] on
http://freenode.net/[Freenode] http://freenode.net/[Freenode]
* Github: https://github.com/The-Compiler/qutebrowser * Github: https://github.com/qutebrowser/qutebrowser
== AUTHOR == AUTHOR
*qutebrowser* was written by Florian Bruhin. All contributors can be found in *qutebrowser* was written by Florian Bruhin. All contributors can be found in

View File

@ -38,6 +38,7 @@ The following environment variables will be set when a userscript is launched:
- `QUTE_CONFIG_DIR`: Path of the directory containing qutebrowser's configuration. - `QUTE_CONFIG_DIR`: Path of the directory containing qutebrowser's configuration.
- `QUTE_DATA_DIR`: Path of the directory containing qutebrowser's data. - `QUTE_DATA_DIR`: Path of the directory containing qutebrowser's data.
- `QUTE_DOWNLOAD_DIR`: Path of the downloads directory. - `QUTE_DOWNLOAD_DIR`: Path of the downloads directory.
- `QUTE_COMMANDLINE_TEXT`: Text currently in qutebrowser's command line.
In `command` mode: In `command` mode:

File diff suppressed because it is too large Load Diff

Before

Width:  |  Height:  |  Size: 137 KiB

After

Width:  |  Height:  |  Size: 170 KiB

View File

@ -70,6 +70,9 @@ coll = COLLECT(exe,
app = BUNDLE(coll, app = BUNDLE(coll,
name='qutebrowser.app', name='qutebrowser.app',
icon=icon, icon=icon,
info_plist={'NSHighResolutionCapable': 'True'}, info_plist={
'NSHighResolutionCapable': 'True',
'NSSupportsAutomaticGraphicsSwitching': 'True',
},
# https://github.com/pyinstaller/pyinstaller/blob/b78bfe530cdc2904f65ce098bdf2de08c9037abb/PyInstaller/hooks/hook-PyQt5.QtWebEngineWidgets.py#L24 # https://github.com/pyinstaller/pyinstaller/blob/b78bfe530cdc2904f65ce098bdf2de08c9037abb/PyInstaller/hooks/hook-PyQt5.QtWebEngineWidgets.py#L24
bundle_identifier='org.qt-project.Qt.QtWebEngineCore') bundle_identifier='org.qt-project.Qt.QtWebEngineCore')

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
check-manifest==0.34 check-manifest==0.35

View File

@ -1,5 +1,5 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py # This file is automatically generated by scripts/dev/recompile_requirements.py
codecov==2.0.5 codecov==2.0.5
coverage==4.2 coverage==4.3.4
requests==2.12.1 requests==2.13.0

View File

@ -4,19 +4,17 @@ 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.1 flake8-deprecated==1.1
flake8-docstrings==1.0.2 flake8-docstrings==1.0.3
flake8-future-import==0.4.3 flake8-future-import==0.4.3
flake8-mock==0.3 flake8-mock==0.3
flake8-pep3101==0.6 flake8-pep3101==1.0
flake8-polyfill==1.0.1
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.3 flake8-tidy-imports==1.0.6
flake8-tuple==0.2.12 flake8-tuple==0.2.12
mccabe==0.5.2 mccabe==0.6.1
packaging==16.8
pep8-naming==0.4.1 pep8-naming==0.4.1
pycodestyle==2.2.0 pycodestyle==2.3.1
pydocstyle==1.1.1 pydocstyle==1.1.1
pyflakes==1.3.0 pyflakes==1.5.0
pyparsing==2.1.10
six==1.10.0

View File

@ -15,7 +15,9 @@ pydocstyle
pyflakes pyflakes
# Pinned to 2.0.0 otherwise # Pinned to 2.0.0 otherwise
pycodestyle==2.2.0 pycodestyle==2.3.1
# Pinned to 0.5.3 otherwise
mccabe==0.6.1
# Waiting until flake8-putty updated # Waiting until flake8-putty updated
#@ filter: flake8 < 3.0.0 #@ filter: flake8 < 3.0.0

View File

@ -0,0 +1,8 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py
appdirs==1.4.3
packaging==16.8
pyparsing==2.2.0
setuptools==34.3.2
six==1.10.0
wheel==0.29.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
-e git+https://github.com/edrex/pyinstaller.git@0fedc28f65d74e1f5ece453abdfb5ad54e9ac5ba#egg=PyInstaller -e git+https://github.com/pyinstaller/pyinstaller.git@develop#egg=PyInstaller

View File

@ -1,2 +1,4 @@
# https://github.com/pyinstaller/pyinstaller/pull/2238 -e git+https://github.com/pyinstaller/pyinstaller.git@develop#egg=PyInstaller
-e git+https://github.com/edrex/pyinstaller.git@1984_add_QtWebEngineCore#egg=PyInstaller
# remove @commit-id for scm installs
#@ replace: @.*# @develop#

View File

@ -4,9 +4,8 @@
editdistance==0.3.1 editdistance==0.3.1
isort==4.2.5 isort==4.2.5
lazy-object-proxy==1.2.2 lazy-object-proxy==1.2.2
mccabe==0.5.2 mccabe==0.6.1
-e git+https://github.com/PyCQA/pylint.git#egg=pylint -e git+https://github.com/PyCQA/pylint.git#egg=pylint
./scripts/dev/pylint_checkers ./scripts/dev/pylint_checkers
requests==2.12.1 requests==2.13.0
six==1.10.0 wrapt==1.10.10
wrapt==1.10.8

View File

@ -1,14 +1,13 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py # This file is automatically generated by scripts/dev/recompile_requirements.py
astroid==1.4.8 astroid==1.4.9
github3.py==0.9.6 github3.py==0.9.6
isort==4.2.5 isort==4.2.5
lazy-object-proxy==1.2.2 lazy-object-proxy==1.2.2
mccabe==0.5.2 mccabe==0.6.1
pylint==1.6.4 pylint==1.6.5
./scripts/dev/pylint_checkers ./scripts/dev/pylint_checkers
requests==2.12.1 requests==2.13.0
six==1.10.0
uritemplate==3.0.0 uritemplate==3.0.0
uritemplate.py==3.0.2 uritemplate.py==3.0.2
wrapt==1.10.8 wrapt==1.10.10

View File

@ -0,0 +1,4 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py
PyQt5==5.8.1.1
sip==4.19.1

View File

@ -0,0 +1 @@
PyQt5

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.13.1
pyroma==2.2 pyroma==2.2

View File

@ -1,5 +1,5 @@
bzr+lp:beautifulsoup bzr+lp:beautifulsoup
git+https://github.com/cherrypy/cherrypy.git git+https://github.com/cherrypy/cheroot.git
hg+https://bitbucket.org/ned/coveragepy hg+https://bitbucket.org/ned/coveragepy
git+https://github.com/micheles/decorator.git git+https://github.com/micheles/decorator.git
git+https://github.com/pallets/flask.git git+https://github.com/pallets/flask.git

View File

@ -1,23 +1,25 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py # This file is automatically generated by scripts/dev/recompile_requirements.py
beautifulsoup4==4.5.1 beautifulsoup4==4.5.3
CherryPy==8.1.2 cheroot==5.3.0
click==6.6 click==6.7
coverage==4.2 coverage==4.3.4
decorator==4.0.10 decorator==4.0.11
Flask==0.11.1 EasyProcess==0.2.3
Flask==0.12
glob2==0.5 glob2==0.5
httpbin==0.5.0 httpbin==0.5.0
hypothesis==3.6.0 hypothesis==3.6.1
itsdangerous==0.24 itsdangerous==0.24
# Jinja2==2.8 # Jinja2==2.9.5
Mako==1.0.6 Mako==1.0.6
# MarkupSafe==0.23 # MarkupSafe==1.0
parse==1.6.6 parse==1.8.0
parse-type==0.3.4 parse-type==0.3.4
py==1.4.31 py==1.4.33
pytest==3.0.4 pytest==3.0.7
pytest-bdd==2.18.1 pytest-bdd==2.18.1
pytest-benchmark==3.0.0
pytest-catchlog==1.2.2 pytest-catchlog==1.2.2
pytest-cov==2.4.0 pytest-cov==2.4.0
pytest-faulthandler==1.3.1 pytest-faulthandler==1.3.1
@ -28,7 +30,7 @@ pytest-repeat==0.4.1
pytest-rerunfailures==2.1.0 pytest-rerunfailures==2.1.0
pytest-travis-fold==1.2.0 pytest-travis-fold==1.2.0
pytest-warnings==0.2.0 pytest-warnings==0.2.0
pytest-xvfb==0.3.0 pytest-xvfb==1.0.0
six==1.10.0 PyVirtualDisplay==0.2.1
vulture==0.10 vulture==0.13
Werkzeug==0.11.11 Werkzeug==0.12.1

View File

@ -1,11 +1,12 @@
beautifulsoup4 beautifulsoup4
CherryPy cheroot
coverage coverage
Flask Flask
httpbin httpbin
hypothesis hypothesis
pytest pytest
pytest-bdd pytest-bdd
pytest-benchmark
pytest-catchlog pytest-catchlog
pytest-cov pytest-cov
pytest-faulthandler pytest-faulthandler

View File

@ -1,6 +1,6 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py # This file is automatically generated by scripts/dev/recompile_requirements.py
pluggy==0.4.0 pluggy==0.4.0
py==1.4.31 py==1.4.33
tox==2.5.0 tox==2.6.0
virtualenv==15.1.0 virtualenv==15.1.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
vulture==0.10 vulture==0.13

27
misc/userscripts/ripbang Executable file
View File

@ -0,0 +1,27 @@
#!/usr/bin/env python2
#
# Adds DuckDuckGo bang as searchengine.
#
# Usage:
# :spawn --userscript ripbang [bang]...
#
# Example:
# :spawn --userscript ripbang amazon maps
#
import os, re, requests, sys, urllib
for argument in sys.argv[1:]:
bang = '!' + argument
r = requests.get('https://duckduckgo.com/',
params={'q': bang + ' SEARCHTEXT'})
searchengine = urllib.unquote(re.search("url=[^']+", r.text).group(0))
searchengine = searchengine.replace('url=', '')
searchengine = searchengine.replace('/l/?kh=-1&uddg=', '')
searchengine = searchengine.replace('SEARCHTEXT', '{}')
if os.getenv('QUTE_FIFO'):
with open(os.environ['QUTE_FIFO'], 'w') as fifo:
fifo.write('set searchengines %s %s' % (bang, searchengine))
else:
print '%s %s' % (bang, searchengine)

View File

@ -17,10 +17,13 @@ markers =
qtwebengine_todo: Features still missing with QtWebEngine qtwebengine_todo: Features still missing with QtWebEngine
qtwebengine_skip: Tests not applicable with QtWebEngine qtwebengine_skip: Tests not applicable with QtWebEngine
qtwebkit_skip: Tests not applicable with QtWebKit qtwebkit_skip: Tests not applicable with QtWebKit
qtwebkit_ng_xfail: Tests failing with QtWebKit-NG
qtwebkit_ng_skip: Tests skipped with QtWebKit-NG
qtwebengine_flaky: Tests which are flaky (and currently skipped) with QtWebEngine qtwebengine_flaky: Tests which are flaky (and currently skipped) with QtWebEngine
qtwebengine_osx_xfail: Tests which fail on OS X with QtWebEngine qtwebengine_osx_xfail: Tests which fail on OS X with QtWebEngine
js_prompt: Tests needing to display a javascript prompt js_prompt: Tests needing to display a javascript prompt
this: Used to mark tests during development this: Used to mark tests during development
no_invalid_lines: Don't fail on unparseable lines in end2end tests
qt_log_level_fail = WARNING qt_log_level_fail = WARNING
qt_log_ignore = qt_log_ignore =
^SpellCheck: .* ^SpellCheck: .*
@ -46,4 +49,6 @@ qt_log_ignore =
^load glyph failed ^load glyph failed
^Error when parsing the netrc file ^Error when parsing the netrc file
^Image of format '' blocked because it is not considered safe. If you are sure it is safe to do so, you can white-list the format by setting the environment variable QTWEBKIT_IMAGEFORMAT_WHITELIST= ^Image of format '' blocked because it is not considered safe. If you are sure it is safe to do so, you can white-list the format by setting the environment variable QTWEBKIT_IMAGEFORMAT_WHITELIST=
^QPainter::end: Painter ended with \d+ saved states
^QSslSocket: cannot resolve SSLv[23]_(client|server)_method
xfail_strict = true xfail_strict = true

View File

@ -8,3 +8,4 @@ Exec=qutebrowser %u
Terminal=false Terminal=false
StartupNotify=false StartupNotify=false
MimeType=text/html;text/xml;application/xhtml+xml;application/xml;application/rdf+xml;image/gif;image/jpeg;image/png;x-scheme-handler/http;x-scheme-handler/https; MimeType=text/html;text/xml;application/xhtml+xml;application/xml;application/rdf+xml;image/gif;image/jpeg;image/png;x-scheme-handler/http;x-scheme-handler/https;
Keywords=Browser

View File

@ -17,9 +17,7 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>. # along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
# pylint: disable=line-too-long """A keyboard-driven, vim-like browser based on PyQt5."""
"""A keyboard-driven, vim-like browser based on PyQt5 and QtWebKit."""
import os.path import os.path
@ -28,8 +26,8 @@ __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, 4) __version_info__ = (0, 10, 1)
__version__ = '.'.join(str(e) for e in __version_info__) __version__ = '.'.join(str(e) for e in __version_info__)
__description__ = "A keyboard-driven, vim-like browser based on PyQt5 and QtWebKit." __description__ = "A keyboard-driven, vim-like browser based on PyQt5."
basedir = os.path.dirname(os.path.realpath(__file__)) basedir = os.path.dirname(os.path.realpath(__file__))

View File

@ -47,6 +47,7 @@ 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,
downloads) downloads)
from qutebrowser.browser.network import proxy
from qutebrowser.browser.webkit import cookies, cache from qutebrowser.browser.webkit import cookies, cache
from qutebrowser.browser.webkit.network import networkmanager from qutebrowser.browser.webkit.network import networkmanager
from qutebrowser.keyinput import macros from qutebrowser.keyinput import macros
@ -132,7 +133,6 @@ def init(args, crash_handler):
log.init.debug("Starting init...") log.init.debug("Starting init...")
qApp.setQuitOnLastWindowClosed(False) qApp.setQuitOnLastWindowClosed(False)
_init_icon() _init_icon()
utils.actute_warning()
try: try:
_init_modules(args, crash_handler) _init_modules(args, crash_handler)
@ -141,9 +141,6 @@ def init(args, crash_handler):
pre_text="Error while initializing") pre_text="Error while initializing")
sys.exit(usertypes.Exit.err_init) sys.exit(usertypes.Exit.err_init)
QTimer.singleShot(0, functools.partial(_process_args, args))
QTimer.singleShot(10, functools.partial(_init_late_modules, args))
log.init.debug("Initializing eventfilter...") log.init.debug("Initializing eventfilter...")
event_filter = EventFilter(qApp) event_filter = EventFilter(qApp)
qApp.installEventFilter(event_filter) qApp.installEventFilter(event_filter)
@ -154,11 +151,13 @@ def init(args, crash_handler):
config_obj.style_changed.connect(style.get_stylesheet.cache_clear) config_obj.style_changed.connect(style.get_stylesheet.cache_clear)
qApp.focusChanged.connect(on_focus_changed) qApp.focusChanged.connect(on_focus_changed)
_process_args(args)
QDesktopServices.setUrlHandler('http', open_desktopservices_url) QDesktopServices.setUrlHandler('http', open_desktopservices_url)
QDesktopServices.setUrlHandler('https', open_desktopservices_url) QDesktopServices.setUrlHandler('https', open_desktopservices_url)
QDesktopServices.setUrlHandler('qute', open_desktopservices_url) QDesktopServices.setUrlHandler('qute', open_desktopservices_url)
macros.init() QTimer.singleShot(10, functools.partial(_init_late_modules, args))
log.init.debug("Init done!") log.init.debug("Init done!")
crash_handler.raise_crashdlg() crash_handler.raise_crashdlg()
@ -213,14 +212,17 @@ def _load_session(name):
name: The name of the session to load, or None to read state file. name: The name of the session to load, or None to read state file.
""" """
state_config = objreg.get('state-config') state_config = objreg.get('state-config')
if name is None: session_manager = objreg.get('session-manager')
if name is None and session_manager.exists('_autosave'):
name = '_autosave'
elif name is None:
try: try:
name = state_config['general']['session'] name = state_config['general']['session']
except KeyError: except KeyError:
# No session given as argument and none in the session file -> # No session given as argument and none in the session file ->
# start without loading a session # start without loading a session
return return
session_manager = objreg.get('session-manager')
try: try:
session_manager.load(name) session_manager.load(name)
except sessions.SessionNotFoundError: except sessions.SessionNotFoundError:
@ -375,6 +377,7 @@ def _init_modules(args, crash_handler):
args: The argparse namespace. args: The argparse namespace.
crash_handler: The CrashHandler instance. crash_handler: The CrashHandler instance.
""" """
# pylint: disable=too-many-statements
log.init.debug("Initializing prompts...") log.init.debug("Initializing prompts...")
prompt.init() prompt.init()
@ -386,6 +389,11 @@ def _init_modules(args, crash_handler):
log.init.debug("Initializing network...") log.init.debug("Initializing network...")
networkmanager.init() networkmanager.init()
if qtutils.version_check('5.8'):
# Otherwise we can only initialize it for QtWebKit because of crashes
log.init.debug("Initializing proxy...")
proxy.init()
log.init.debug("Initializing readline-bridge...") log.init.debug("Initializing readline-bridge...")
readline_bridge = readline.ReadlineBridge() readline_bridge = readline.ReadlineBridge()
objreg.register('readline-bridge', readline_bridge) objreg.register('readline-bridge', readline_bridge)
@ -405,7 +413,7 @@ def _init_modules(args, crash_handler):
sessions.init(qApp) sessions.init(qApp)
log.init.debug("Initializing websettings...") log.init.debug("Initializing websettings...")
websettings.init() websettings.init(args)
log.init.debug("Initializing adblock...") log.init.debug("Initializing adblock...")
host_blocker = adblock.HostBlocker() host_blocker = adblock.HostBlocker()
@ -438,8 +446,9 @@ def _init_modules(args, crash_handler):
os.environ['QT_WAYLAND_DISABLE_WINDOWDECORATION'] = '1' os.environ['QT_WAYLAND_DISABLE_WINDOWDECORATION'] = '1'
else: else:
os.environ.pop('QT_WAYLAND_DISABLE_WINDOWDECORATION', None) os.environ.pop('QT_WAYLAND_DISABLE_WINDOWDECORATION', None)
macros.init()
# Init backend-specific stuff # Init backend-specific stuff
browsertab.init(args) browsertab.init()
def _init_late_modules(args): def _init_late_modules(args):
@ -527,7 +536,7 @@ class Quitter:
if not os.path.isdir(cwd): if not os.path.isdir(cwd):
# Probably running from a python egg. Let's fallback to # Probably running from a python egg. Let's fallback to
# cwd=None and see if that works out. # cwd=None and see if that works out.
# See https://github.com/The-Compiler/qutebrowser/issues/323 # See https://github.com/qutebrowser/qutebrowser/issues/323
cwd = None cwd = None
# Add all open pages so they get reopened. # Add all open pages so they get reopened.
@ -713,6 +722,7 @@ class Quitter:
# Now we can hopefully quit without segfaults # Now we can hopefully quit without segfaults
log.destroy.debug("Deferring QApplication::exit...") log.destroy.debug("Deferring QApplication::exit...")
objreg.get('signal-handler').deactivate() objreg.get('signal-handler').deactivate()
objreg.get('session-manager').delete_autosave()
# We use a singleshot timer to exit here to minimize the likelihood of # We use a singleshot timer to exit here to minimize the likelihood of
# segfaults. # segfaults.
QTimer.singleShot(0, functools.partial(qApp.exit, status)) QTimer.singleShot(0, functools.partial(qApp.exit, status))

View File

@ -58,7 +58,7 @@ def get_fileobj(byte_io):
byte_io = zf.open(filename, mode='r') byte_io = zf.open(filename, mode='r')
else: else:
byte_io.seek(0) # rewind what zipfile.is_zipfile did byte_io.seek(0) # rewind what zipfile.is_zipfile did
return io.TextIOWrapper(byte_io, encoding='utf-8') return byte_io
def is_whitelisted_host(host): def is_whitelisted_host(host):
@ -147,7 +147,7 @@ class HostBlocker:
with open(filename, 'r', encoding='utf-8') as f: with open(filename, 'r', encoding='utf-8') as f:
for line in f: for line in f:
target.add(line.strip()) target.add(line.strip())
except OSError: except (OSError, UnicodeDecodeError):
log.misc.exception("Failed to read host blocklist!") log.misc.exception("Failed to read host blocklist!")
return True return True
@ -165,7 +165,8 @@ class HostBlocker:
if not found: if not found:
args = objreg.get('args') args = objreg.get('args')
if (config.get('content', 'host-block-lists') is not None and if (config.get('content', 'host-block-lists') is not None and
args.basedir is None): args.basedir is None and
config.get('content', 'host-blocking-enabled')):
message.info("Run :adblock-update to get adblock lists.") message.info("Run :adblock-update to get adblock lists.")
@cmdutils.register(instance='host-blocker') @cmdutils.register(instance='host-blocker')
@ -205,6 +206,54 @@ class HostBlocker:
download.finished.connect( download.finished.connect(
functools.partial(self.on_download_finished, download)) functools.partial(self.on_download_finished, download))
def _parse_line(self, line):
"""Parse a line from a host file.
Args:
line: The bytes object to parse.
Returns:
True if parsing succeeded, False otherwise.
"""
if line.startswith(b'#'):
# Ignoring comments early so we don't have to care about
# encoding errors in them.
return True
try:
line = line.decode('utf-8')
except UnicodeDecodeError:
log.misc.error("Failed to decode: {!r}".format(line))
return False
# Remove comments
try:
hash_idx = line.index('#')
line = line[:hash_idx]
except ValueError:
pass
line = line.strip()
# Skip empty lines
if not line:
return True
parts = line.split()
if len(parts) == 1:
# "one host per line" format
host = parts[0]
elif len(parts) == 2:
# /etc/hosts format
host = parts[1]
else:
log.misc.error("Failed to parse: {!r}".format(line))
return False
if host not in self.WHITELISTED:
self._blocked_hosts.add(host)
return True
def _merge_file(self, byte_io): def _merge_file(self, byte_io):
"""Read and merge host files. """Read and merge host files.
@ -218,35 +267,18 @@ class HostBlocker:
line_count = 0 line_count = 0
try: try:
f = get_fileobj(byte_io) f = get_fileobj(byte_io)
except (OSError, UnicodeDecodeError, zipfile.BadZipFile, except (OSError, zipfile.BadZipFile, zipfile.LargeZipFile,
zipfile.LargeZipFile) as e: LookupError) as e:
message.error("adblock: Error while reading {}: {} - {}".format( message.error("adblock: Error while reading {}: {} - {}".format(
byte_io.name, e.__class__.__name__, e)) byte_io.name, e.__class__.__name__, e))
return return
for line in f: for line in f:
line_count += 1 line_count += 1
# Remove comments ok = self._parse_line(line)
try: if not ok:
hash_idx = line.index('#')
line = line[:hash_idx]
except ValueError:
pass
line = line.strip()
# Skip empty lines
if not line:
continue
parts = line.split()
if len(parts) == 1:
# "one host per line" format
host = parts[0]
elif len(parts) == 2:
# /etc/hosts format
host = parts[1]
else:
error_count += 1 error_count += 1
continue
if host not in self.WHITELISTED:
self._blocked_hosts.add(host)
log.misc.debug("{}: read {} lines".format(byte_io.name, line_count)) log.misc.debug("{}: read {} lines".format(byte_io.name, line_count))
if error_count > 0: if error_count > 0:
message.error("adblock: {} read errors for {}".format( message.error("adblock: {} read errors for {}".format(

View File

@ -28,7 +28,7 @@ from PyQt5.QtWidgets import QWidget, QApplication
from qutebrowser.keyinput import modeman from qutebrowser.keyinput import modeman
from qutebrowser.config import config from qutebrowser.config import config
from qutebrowser.utils import utils, objreg, usertypes, log, qtutils from qutebrowser.utils import utils, objreg, usertypes, log, qtutils
from qutebrowser.misc import miscwidgets from qutebrowser.misc import miscwidgets, objects
from qutebrowser.browser import mouse, hints from qutebrowser.browser import mouse, hints
@ -45,7 +45,7 @@ def create(win_id, parent=None):
# Importing modules here so we don't depend on QtWebEngine without the # Importing modules here so we don't depend on QtWebEngine without the
# argument and to avoid circular imports. # argument and to avoid circular imports.
mode_manager = modeman.instance(win_id) mode_manager = modeman.instance(win_id)
if objreg.get('args').backend == 'webengine': if objects.backend == usertypes.Backend.QtWebEngine:
from qutebrowser.browser.webengine import webenginetab from qutebrowser.browser.webengine import webenginetab
tab_class = webenginetab.WebEngineTab tab_class = webenginetab.WebEngineTab
else: else:
@ -54,9 +54,9 @@ def create(win_id, parent=None):
return tab_class(win_id=win_id, mode_manager=mode_manager, parent=parent) return tab_class(win_id=win_id, mode_manager=mode_manager, parent=parent)
def init(args): def init():
"""Initialize backend-specific modules.""" """Initialize backend-specific modules."""
if args.backend == 'webengine': if objects.backend == usertypes.Backend.QtWebEngine:
from qutebrowser.browser.webengine import webenginetab from qutebrowser.browser.webengine import webenginetab
webenginetab.init() webenginetab.init()
else: else:
@ -74,6 +74,15 @@ class UnsupportedOperationError(WebTabError):
"""Raised when an operation is not supported with the given backend.""" """Raised when an operation is not supported with the given backend."""
TerminationStatus = usertypes.enum('TerminationStatus', [
'normal',
'abnormal', # non-zero exit status
'crashed', # e.g. segfault
'killed',
'unknown',
])
class TabData: class TabData:
"""A simple namespace with a fixed set of attributes. """A simple namespace with a fixed set of attributes.
@ -96,6 +105,22 @@ class TabData:
self.pinned = False self.pinned = False
class AbstractAction:
"""Attribute of AbstractTab for Qt WebActions."""
def __init__(self):
self._widget = None
def exit_fullscreen(self):
"""Exit the fullscreen mode."""
raise NotImplementedError
def save_page(self):
"""Save the current page."""
raise NotImplementedError
class AbstractPrinting: class AbstractPrinting:
"""Attribute of AbstractTab for printing the page.""" """Attribute of AbstractTab for printing the page."""
@ -109,10 +134,20 @@ class AbstractPrinting:
def check_printer_support(self): def check_printer_support(self):
raise NotImplementedError raise NotImplementedError
def check_preview_support(self):
raise NotImplementedError
def to_pdf(self, filename): def to_pdf(self, filename):
raise NotImplementedError raise NotImplementedError
def to_printer(self, printer): def to_printer(self, printer, callback=None):
"""Print the tab.
Args:
printer: The QPrinter to print to.
callback: Called with a boolean
(True if printing succeeded, False otherwise)
"""
raise NotImplementedError raise NotImplementedError
@ -184,7 +219,7 @@ class AbstractZoom(QObject):
# # FIXME:qtwebengine is this needed? # # FIXME:qtwebengine is this needed?
# # For some reason, this signal doesn't get disconnected automatically # # For some reason, this signal doesn't get disconnected automatically
# # when the WebView is destroyed on older PyQt versions. # # when the WebView is destroyed on older PyQt versions.
# # See https://github.com/The-Compiler/qutebrowser/issues/390 # # See https://github.com/qutebrowser/qutebrowser/issues/390
# self.destroyed.connect(functools.partial( # self.destroyed.connect(functools.partial(
# cfg.changed.disconnect, self.init_neighborlist)) # cfg.changed.disconnect, self.init_neighborlist))
@ -217,6 +252,9 @@ class AbstractZoom(QObject):
self.set_factor(float(level) / 100, fuzzyval=False) self.set_factor(float(level) / 100, fuzzyval=False)
return level return level
def _set_factor_internal(self, factor):
raise NotImplementedError
def set_factor(self, factor, *, fuzzyval=True): def set_factor(self, factor, *, fuzzyval=True):
"""Zoom to a given zoom factor. """Zoom to a given zoom factor.
@ -485,10 +523,6 @@ class AbstractTab(QWidget):
We use this to unify QWebView and QWebEngineView. We use this to unify QWebView and QWebEngineView.
Class attributes:
WIDGET_CLASS: The class of the main widget recieving events.
Needs to be overridden by subclasses.
Attributes: Attributes:
history: The AbstractHistory for the current tab. history: The AbstractHistory for the current tab.
registry: The ObjectRegistry associated with this tab. registry: The ObjectRegistry associated with this tab.
@ -506,6 +540,13 @@ class AbstractTab(QWidget):
new_tab_requested: Emitted when a new tab should be opened with the new_tab_requested: Emitted when a new tab should be opened with the
given URL. given URL.
load_status_changed: The loading status changed load_status_changed: The loading status changed
fullscreen_requested: Fullscreen display was requested by the page.
arg: True if fullscreen should be turned on,
False if it should be turned off.
renderer_process_terminated: Emitted when the underlying renderer
process terminated.
arg 0: A TerminationStatus member.
arg 1: The exit code.
""" """
window_close_requested = pyqtSignal() window_close_requested = pyqtSignal()
@ -521,8 +562,8 @@ class AbstractTab(QWidget):
shutting_down = pyqtSignal() shutting_down = pyqtSignal()
contents_size_changed = pyqtSignal(QSizeF) contents_size_changed = pyqtSignal(QSizeF)
add_history_item = pyqtSignal(QUrl, QUrl, str) # url, requested url, title add_history_item = pyqtSignal(QUrl, QUrl, str) # url, requested url, title
fullscreen_requested = pyqtSignal(bool)
WIDGET_CLASS = None renderer_process_terminated = pyqtSignal(TerminationStatus, int)
def __init__(self, win_id, mode_manager, parent=None): def __init__(self, win_id, mode_manager, parent=None):
self.win_id = win_id self.win_id = win_id
@ -543,6 +584,7 @@ class AbstractTab(QWidget):
# self.search = AbstractSearch(parent=self) # self.search = AbstractSearch(parent=self)
# self.printing = AbstractPrinting() # self.printing = AbstractPrinting()
# self.elements = AbstractElements(self) # self.elements = AbstractElements(self)
# self.action = AbstractAction()
self.data = TabData() self.data = TabData()
self._layout = miscwidgets.WrapperLayout(self) self._layout = miscwidgets.WrapperLayout(self)
@ -552,7 +594,7 @@ class AbstractTab(QWidget):
self._mode_manager = mode_manager self._mode_manager = mode_manager
self._load_status = usertypes.LoadStatus.none self._load_status = usertypes.LoadStatus.none
self._mouse_event_filter = mouse.MouseEventFilter( self._mouse_event_filter = mouse.MouseEventFilter(
self, widget_class=self.WIDGET_CLASS, parent=self) self, parent=self)
self.backend = None self.backend = None
# FIXME:qtwebengine Should this be public api via self.hints? # FIXME:qtwebengine Should this be public api via self.hints?
@ -571,8 +613,11 @@ class AbstractTab(QWidget):
self.zoom._widget = widget self.zoom._widget = widget
self.search._widget = widget self.search._widget = widget
self.printing._widget = widget self.printing._widget = widget
self.action._widget = widget
self.elements._widget = widget self.elements._widget = widget
self._install_event_filter() self._install_event_filter()
self.zoom.set_default()
def _install_event_filter(self): def _install_event_filter(self):
raise NotImplementedError raise NotImplementedError
@ -585,7 +630,7 @@ class AbstractTab(QWidget):
self._load_status = val self._load_status = val
self.load_status_changed.emit(val.name) self.load_status_changed.emit(val.name)
def _event_target(self): def event_target(self):
"""Return the widget events should be sent to.""" """Return the widget events should be sent to."""
raise NotImplementedError raise NotImplementedError
@ -600,7 +645,7 @@ class AbstractTab(QWidget):
if getattr(evt, 'posted', False): if getattr(evt, 'posted', False):
raise AssertionError("Can't re-use an event which was already " raise AssertionError("Can't re-use an event which was already "
"posted!") "posted!")
recipient = self._event_target() recipient = self.event_target()
evt.posted = True evt.posted = True
QApplication.postEvent(recipient, evt) QApplication.postEvent(recipient, evt)
@ -641,12 +686,14 @@ class AbstractTab(QWidget):
@pyqtSlot(bool) @pyqtSlot(bool)
def _on_load_finished(self, ok): def _on_load_finished(self, ok):
sess_manager = objreg.get('session-manager')
sess_manager.save_autosave()
if ok and not self._has_ssl_errors: if ok and not self._has_ssl_errors:
if self.url().scheme() == 'https': if self.url().scheme() == 'https':
self._set_load_status(usertypes.LoadStatus.success_https) self._set_load_status(usertypes.LoadStatus.success_https)
else: else:
self._set_load_status(usertypes.LoadStatus.success) self._set_load_status(usertypes.LoadStatus.success)
elif ok: elif ok:
self._set_load_status(usertypes.LoadStatus.warn) self._set_load_status(usertypes.LoadStatus.warn)
else: else:
@ -737,6 +784,14 @@ class AbstractTab(QWidget):
""" """
raise NotImplementedError raise NotImplementedError
def user_agent(self):
"""Get the user agent for this tab.
This is only implemented for QtWebKit.
For QtWebEngine, always returns None.
"""
raise NotImplementedError
def __repr__(self): def __repr__(self):
try: try:
url = utils.elide(self.url().toDisplayString(QUrl.EncodeUnicode), url = utils.elide(self.url().toDisplayString(QUrl.EncodeUnicode),

View File

@ -44,12 +44,6 @@ from qutebrowser.commands import userscripts, cmdexc, cmdutils, runners
from qutebrowser.config import config, configexc from qutebrowser.config import config, configexc
from qutebrowser.browser import (urlmarks, browsertab, inspector, navigate, from qutebrowser.browser import (urlmarks, browsertab, inspector, navigate,
webelem, downloads) webelem, downloads)
try:
from qutebrowser.browser.webkit import mhtml
except ImportError:
# Failing imports on QtWebEngine, only used in QtWebKit commands.
# FIXME:qtwebengine don't import this anymore at all
pass
from qutebrowser.keyinput import modeman from qutebrowser.keyinput import modeman
from qutebrowser.utils import (message, usertypes, log, qtutils, urlutils, from qutebrowser.utils import (message, usertypes, log, qtutils, urlutils,
objreg, utils, typing) objreg, utils, typing)
@ -310,13 +304,10 @@ class CommandDispatcher:
count: The tab index to open the URL in, or None. count: The tab index to open the URL in, or None.
""" """
if url is None: if url is None:
if tab or bg or window:
urls = [config.get('general', 'default-page')] urls = [config.get('general', 'default-page')]
else:
raise cmdexc.CommandError("No URL given, but -t/-b/-w is not "
"set!")
else: else:
urls = self._parse_url_input(url) urls = self._parse_url_input(url)
for i, cur_url in enumerate(urls): for i, cur_url in enumerate(urls):
if not window and i > 0: if not window and i > 0:
tab = False tab = False
@ -407,6 +398,43 @@ class CommandDispatcher:
if tab is not None: if tab is not None:
tab.stop() tab.stop()
def _print_preview(self, tab):
"""Show a print preview."""
def print_callback(ok):
if not ok:
message.error("Printing failed!")
tab.printing.check_preview_support()
diag = QPrintPreviewDialog(tab)
diag.setAttribute(Qt.WA_DeleteOnClose)
diag.setWindowFlags(diag.windowFlags() | Qt.WindowMaximizeButtonHint |
Qt.WindowMinimizeButtonHint)
diag.paintRequested.connect(functools.partial(
tab.printing.to_printer, callback=print_callback))
diag.exec_()
def _print_pdf(self, tab, filename):
"""Print to the given PDF file."""
tab.printing.check_pdf_support()
filename = os.path.expanduser(filename)
directory = os.path.dirname(filename)
if directory and not os.path.exists(directory):
os.mkdir(directory)
tab.printing.to_pdf(filename)
log.misc.debug("Print to file: {}".format(filename))
def _print(self, tab):
"""Print with a QPrintDialog."""
def print_callback(ok):
"""Called when printing finished."""
if not ok:
message.error("Printing failed!")
diag.deleteLater()
diag = QPrintDialog(tab)
diag.open(lambda: tab.printing.to_printer(diag.printer(),
print_callback))
@cmdutils.register(instance='command-dispatcher', name='print', @cmdutils.register(instance='command-dispatcher', name='print',
scope='window') scope='window')
@cmdutils.argument('count', count=True) @cmdutils.argument('count', count=True)
@ -428,28 +456,17 @@ class CommandDispatcher:
tab.printing.check_pdf_support() tab.printing.check_pdf_support()
else: else:
tab.printing.check_printer_support() tab.printing.check_printer_support()
if preview:
tab.printing.check_preview_support()
except browsertab.WebTabError as e: except browsertab.WebTabError as e:
raise cmdexc.CommandError(e) raise cmdexc.CommandError(e)
if preview: if preview:
diag = QPrintPreviewDialog() self._print_preview(tab)
diag.setAttribute(Qt.WA_DeleteOnClose)
diag.setWindowFlags(diag.windowFlags() |
Qt.WindowMaximizeButtonHint |
Qt.WindowMinimizeButtonHint)
diag.paintRequested.connect(tab.printing.to_printer)
diag.exec_()
elif pdf: elif pdf:
pdf = os.path.expanduser(pdf) self._print_pdf(tab, pdf)
directory = os.path.dirname(pdf)
if directory and not os.path.exists(directory):
os.mkdir(directory)
tab.printing.to_pdf(pdf)
log.misc.debug("Print to file: {}".format(pdf))
else: else:
diag = QPrintDialog() self._print(tab)
diag.setAttribute(Qt.WA_DeleteOnClose)
diag.open(lambda: tab.printing.to_printer(diag.printer()))
@cmdutils.register(instance='command-dispatcher', scope='window') @cmdutils.register(instance='command-dispatcher', scope='window')
def tab_clone(self, bg=False, window=False): def tab_clone(self, bg=False, window=False):
@ -465,6 +482,11 @@ class CommandDispatcher:
cmdutils.check_exclusive((bg, window), 'bw') cmdutils.check_exclusive((bg, window), 'bw')
curtab = self._current_widget() curtab = self._current_widget()
cur_title = self._tabbed_browser.page_title(self._current_index()) cur_title = self._tabbed_browser.page_title(self._current_index())
try:
history = curtab.history.serialize()
except browsertab.WebTabError as e:
raise cmdexc.CommandError(e)
# The new tab could be in a new tabbed_browser (e.g. because of # The new tab could be in a new tabbed_browser (e.g. because of
# tabs-are-windows being set) # tabs-are-windows being set)
if window: if window:
@ -475,13 +497,15 @@ class CommandDispatcher:
new_tabbed_browser = objreg.get('tabbed-browser', scope='window', new_tabbed_browser = objreg.get('tabbed-browser', scope='window',
window=newtab.win_id) window=newtab.win_id)
idx = new_tabbed_browser.indexOf(newtab) idx = new_tabbed_browser.indexOf(newtab)
new_tabbed_browser.set_page_title(idx, cur_title) new_tabbed_browser.set_page_title(idx, cur_title)
if config.get('tabs', 'show-favicons'): if config.get('tabs', 'show-favicons'):
new_tabbed_browser.setTabIcon(idx, curtab.icon()) new_tabbed_browser.setTabIcon(idx, curtab.icon())
if config.get('tabs', 'tabs-are-windows'): if config.get('tabs', 'tabs-are-windows'):
new_tabbed_browser.window().setWindowIcon(curtab.icon()) new_tabbed_browser.window().setWindowIcon(curtab.icon())
newtab.data.keep_icon = True newtab.data.keep_icon = True
newtab.history.deserialize(curtab.history.serialize()) newtab.history.deserialize(history)
newtab.zoom.set_factor(curtab.zoom.factor()) newtab.zoom.set_factor(curtab.zoom.factor())
return newtab return newtab
@ -626,6 +650,9 @@ class CommandDispatcher:
def scroll(self, direction: typing.Union[str, int], count=1): def scroll(self, direction: typing.Union[str, int], count=1):
"""Scroll the current tab in the given direction. """Scroll the current tab in the given direction.
Note you can use `:run-with-count` to have a keybinding with a bigger
scroll increment.
Args: Args:
direction: In which direction to scroll direction: In which direction to scroll
(up/down/left/right/top/bottom). (up/down/left/right/top/bottom).
@ -710,7 +737,7 @@ class CommandDispatcher:
""" """
tab = self._current_widget() tab = self._current_widget()
if not tab.url().isValid(): if not tab.url().isValid():
# See https://github.com/The-Compiler/qutebrowser/issues/701 # See https://github.com/qutebrowser/qutebrowser/issues/701
return return
if bottom_navigate is not None and tab.scroller.at_bottom(): if bottom_navigate is not None and tab.scroller.at_bottom():
@ -813,7 +840,7 @@ class CommandDispatcher:
perc = tab.zoom.offset(count) perc = tab.zoom.offset(count)
except ValueError as e: except ValueError as e:
raise cmdexc.CommandError(e) raise cmdexc.CommandError(e)
message.info("Zoom level: {}%".format(perc)) message.info("Zoom level: {}%".format(perc), replace=True)
@cmdutils.register(instance='command-dispatcher', scope='window') @cmdutils.register(instance='command-dispatcher', scope='window')
@cmdutils.argument('count', count=True) @cmdutils.argument('count', count=True)
@ -828,7 +855,7 @@ class CommandDispatcher:
perc = tab.zoom.offset(-count) perc = tab.zoom.offset(-count)
except ValueError as e: except ValueError as e:
raise cmdexc.CommandError(e) raise cmdexc.CommandError(e)
message.info("Zoom level: {}%".format(perc)) message.info("Zoom level: {}%".format(perc), replace=True)
@cmdutils.register(instance='command-dispatcher', scope='window') @cmdutils.register(instance='command-dispatcher', scope='window')
@cmdutils.argument('count', count=True) @cmdutils.argument('count', count=True)
@ -852,7 +879,7 @@ class CommandDispatcher:
tab.zoom.set_factor(float(level) / 100) tab.zoom.set_factor(float(level) / 100)
except ValueError: except ValueError:
raise cmdexc.CommandError("Can't zoom {}%!".format(level)) raise cmdexc.CommandError("Can't zoom {}%!".format(level))
message.info("Zoom level: {}%".format(level)) message.info("Zoom level: {}%".format(level), replace=True)
@cmdutils.register(instance='command-dispatcher', scope='window') @cmdutils.register(instance='command-dispatcher', scope='window')
def tab_only(self, prev=False, next_=False): def tab_only(self, prev=False, next_=False):
@ -891,7 +918,7 @@ class CommandDispatcher:
""" """
if self._count() == 0: if self._count() == 0:
# Running :tab-prev after last tab was closed # Running :tab-prev after last tab was closed
# See https://github.com/The-Compiler/qutebrowser/issues/1448 # See https://github.com/qutebrowser/qutebrowser/issues/1448
return return
newidx = self._current_index() - count newidx = self._current_index() - count
if newidx >= 0: if newidx >= 0:
@ -911,7 +938,7 @@ class CommandDispatcher:
""" """
if self._count() == 0: if self._count() == 0:
# Running :tab-next after last tab was closed # Running :tab-next after last tab was closed
# See https://github.com/The-Compiler/qutebrowser/issues/1448 # See https://github.com/qutebrowser/qutebrowser/issues/1448
return return
newidx = self._current_index() + count newidx = self._current_index() + count
if newidx < self._count(): if newidx < self._count():
@ -1014,7 +1041,7 @@ class CommandDispatcher:
@cmdutils.register(instance='command-dispatcher', scope='window') @cmdutils.register(instance='command-dispatcher', scope='window')
@cmdutils.argument('index', choices=['last']) @cmdutils.argument('index', choices=['last'])
@cmdutils.argument('count', count=True, zero_count=True) @cmdutils.argument('count', count=True)
def tab_focus(self, index: typing.Union[str, int]=None, count=None): def tab_focus(self, index: typing.Union[str, int]=None, count=None):
"""Select the tab given as argument/[count]. """Select the tab given as argument/[count].
@ -1027,7 +1054,6 @@ class CommandDispatcher:
Negative indices count from the end, such that -1 is the Negative indices count from the end, such that -1 is the
last tab. last tab.
count: The tab index to focus, starting with 1. count: The tab index to focus, starting with 1.
The special value 0 focuses the rightmost tab.
""" """
if index == 'last': if index == 'last':
self._tab_focus_last() self._tab_focus_last()
@ -1037,9 +1063,8 @@ class CommandDispatcher:
if index is None: if index is None:
self.tab_next() self.tab_next()
return return
elif index == 0:
index = self._count() if index < 0:
elif index < 0:
index = self._count() + index + 1 index = self._count() + index + 1
if 1 <= index <= self._count(): if 1 <= index <= self._count():
@ -1088,21 +1113,10 @@ class CommandDispatcher:
raise cmdexc.CommandError("Can't move tab to position {}!".format( raise cmdexc.CommandError("Can't move tab to position {}!".format(
new_idx + 1)) new_idx + 1))
tab = self._current_widget()
cur_idx = self._current_index() cur_idx = self._current_index()
icon = self._tabbed_browser.tabIcon(cur_idx)
label = self._tabbed_browser.page_title(cur_idx)
cmdutils.check_overflow(cur_idx, 'int') cmdutils.check_overflow(cur_idx, 'int')
cmdutils.check_overflow(new_idx, 'int') cmdutils.check_overflow(new_idx, 'int')
self._tabbed_browser.setUpdatesEnabled(False) self._tabbed_browser.tabBar().moveTab(cur_idx, new_idx)
try:
color = self._tabbed_browser.tab_indicator_color(cur_idx)
self._tabbed_browser.removeTab(cur_idx)
self._tabbed_browser.insertTab(new_idx, tab, icon, label)
self._set_current_index(new_idx)
self._tabbed_browser.set_tab_indicator_color(new_idx, color)
finally:
self._tabbed_browser.setUpdatesEnabled(True)
@cmdutils.register(instance='command-dispatcher', scope='window', @cmdutils.register(instance='command-dispatcher', scope='window',
maxsplit=0, no_replace_variables=True) maxsplit=0, no_replace_variables=True)
@ -1373,58 +1387,39 @@ class CommandDispatcher:
# FIXME:qtwebengine do this with the QtWebEngine download manager? # FIXME:qtwebengine do this with the QtWebEngine download manager?
download_manager = objreg.get('qtnetwork-download-manager', download_manager = objreg.get('qtnetwork-download-manager',
scope='window', window=self._win_id) scope='window', window=self._win_id)
target = None
if dest is not None:
target = downloads.FileDownloadTarget(dest)
tab = self._current_widget()
user_agent = tab.user_agent()
if url: if url:
if mhtml_: if mhtml_:
raise cmdexc.CommandError("Can only download the current page" raise cmdexc.CommandError("Can only download the current page"
" as mhtml.") " as mhtml.")
url = urlutils.qurl_from_user_input(url) url = urlutils.qurl_from_user_input(url)
urlutils.raise_cmdexc_if_invalid(url) urlutils.raise_cmdexc_if_invalid(url)
if dest is None: download_manager.get(url, user_agent=user_agent, target=target)
target = None
else:
target = downloads.FileDownloadTarget(dest)
download_manager.get(url, target=target)
elif mhtml_: elif mhtml_:
self._download_mhtml(dest)
else:
qnam = self._current_widget().networkaccessmanager()
if dest is None:
target = None
else:
target = downloads.FileDownloadTarget(dest)
download_manager.get(self._current_url(), qnam=qnam, target=target)
def _download_mhtml(self, dest=None):
"""Download the current page as an MHTML file, including all assets.
Args:
dest: The file path to write the download to.
"""
tab = self._current_widget() tab = self._current_widget()
if tab.backend == usertypes.Backend.QtWebEngine: if tab.backend == usertypes.Backend.QtWebEngine:
raise cmdexc.CommandError("Download --mhtml is not implemented " webengine_download_manager = objreg.get(
"with QtWebEngine yet") 'webengine-download-manager')
try:
if dest is None: webengine_download_manager.get_mhtml(tab, target)
suggested_fn = self._current_title() + ".mht" except browsertab.UnsupportedOperationError as e:
suggested_fn = utils.sanitize_filename(suggested_fn) raise cmdexc.CommandError(e)
filename = downloads.immediate_download_path()
if filename is not None:
mhtml.start_download_checked(filename, tab=tab)
else: else:
question = downloads.get_filename_question( download_manager.get_mhtml(tab, target)
suggested_filename=suggested_fn, url=tab.url(), parent=tab)
question.answered.connect(functools.partial(
mhtml.start_download_checked, tab=tab))
message.global_bridge.ask(question, blocking=False)
else: else:
mhtml.start_download_checked(dest, tab=tab) qnam = tab.networkaccessmanager()
download_manager.get(self._current_url(), user_agent=user_agent,
qnam=qnam, target=target)
@cmdutils.register(instance='command-dispatcher', scope='window') @cmdutils.register(instance='command-dispatcher', scope='window')
def view_source(self): def view_source(self):
"""Show the source of the current page.""" """Show the source of the current page in a new tab."""
# pylint: disable=no-member # pylint: disable=no-member
# WORKAROUND for https://bitbucket.org/logilab/pylint/issue/491/ # WORKAROUND for https://bitbucket.org/logilab/pylint/issue/491/
tab = self._current_widget() tab = self._current_widget()
@ -1471,6 +1466,18 @@ class CommandDispatcher:
tab.dump_async(callback, plain=plain) tab.dump_async(callback, plain=plain)
@cmdutils.register(instance='command-dispatcher', scope='window')
def history(self, tab=True, bg=False, window=False):
"""Show browsing history.
Args:
tab: Open in a new tab.
bg: Open in a background tab.
window: Open in a new window.
"""
url = QUrl('qute://history/')
self._open(url, tab, bg, window)
@cmdutils.register(instance='command-dispatcher', name='help', @cmdutils.register(instance='command-dispatcher', name='help',
scope='window') scope='window')
@cmdutils.argument('topic', completion=usertypes.Completion.helptopic) @cmdutils.argument('topic', completion=usertypes.Completion.helptopic)
@ -1544,6 +1551,10 @@ class CommandDispatcher:
return return
text = elem.value() text = elem.value()
if text is None:
message.error("Could not get text from the focused element.")
return
ed = editor.ExternalEditor(self._tabbed_browser) ed = editor.ExternalEditor(self._tabbed_browser)
ed.editing_finished.connect(functools.partial( ed.editing_finished.connect(functools.partial(
self.on_editing_finished, elem)) self.on_editing_finished, elem))
@ -1612,7 +1623,8 @@ class CommandDispatcher:
@cmdutils.argument('filter_', choices=['id']) @cmdutils.argument('filter_', choices=['id'])
def click_element(self, filter_: str, value, *, def click_element(self, filter_: str, value, *,
target: usertypes.ClickTarget= target: usertypes.ClickTarget=
usertypes.ClickTarget.normal): usertypes.ClickTarget.normal,
force_event=False):
"""Click the element matching the given filter. """Click the element matching the given filter.
The given filter needs to result in exactly one element, otherwise, an The given filter needs to result in exactly one element, otherwise, an
@ -1623,6 +1635,7 @@ class CommandDispatcher:
id: Get an element based on its ID. id: Get an element based on its ID.
value: The value to filter for. value: The value to filter for.
target: How to open the clicked element (normal/tab/tab-bg/window). target: How to open the clicked element (normal/tab/tab-bg/window).
force_event: Force generating a fake click event.
""" """
tab = self._current_widget() tab = self._current_widget()
@ -1632,7 +1645,7 @@ class CommandDispatcher:
message.error("No element found with id {}!".format(value)) message.error("No element found with id {}!".format(value))
return return
try: try:
elem.click(target) elem.click(target, force_event=force_event)
except webelem.Error as e: except webelem.Error as e:
message.error(str(e)) message.error(str(e))
return return
@ -1972,12 +1985,13 @@ class CommandDispatcher:
@cmdutils.register(instance='command-dispatcher', scope='window', @cmdutils.register(instance='command-dispatcher', scope='window',
maxsplit=0, no_cmd_split=True) maxsplit=0, no_cmd_split=True)
def jseval(self, js_code, quiet=False, *, def jseval(self, js_code, file=False, quiet=False, *,
world: typing.Union[usertypes.JsWorld, int]=None): world: typing.Union[usertypes.JsWorld, int]=None):
"""Evaluate a JavaScript string. """Evaluate a JavaScript string.
Args: Args:
js_code: The string to evaluate. js_code: The string/file to evaluate.
file: Interpret js-code as a path to a file.
quiet: Don't show resulting JS object. quiet: Don't show resulting JS object.
world: Ignored on QtWebKit. On QtWebEngine, a world ID or name to world: Ignored on QtWebKit. On QtWebEngine, a world ID or name to
run the snippet in. run the snippet in.
@ -2005,6 +2019,13 @@ class CommandDispatcher:
out = out[:5000] + ' [...trimmed...]' out = out[:5000] + ' [...trimmed...]'
message.info(out) message.info(out)
if file:
try:
with open(js_code, 'r', encoding='utf-8') as f:
js_code = f.read()
except OSError as e:
raise cmdexc.CommandError(str(e))
widget = self._current_widget() widget = self._current_widget()
widget.run_js_async(js_code, callback=jseval_cb, world=world) widget.run_js_async(js_code, callback=jseval_cb, world=world)
@ -2038,11 +2059,6 @@ class CommandDispatcher:
QApplication.postEvent(window, press_event) QApplication.postEvent(window, press_event)
QApplication.postEvent(window, release_event) QApplication.postEvent(window, release_event)
else: else:
try:
tab = objreg.get('tab', scope='tab', tab='current')
except objreg.RegistryUnavailableError:
raise cmdexc.CommandError("No focused webview!")
tab = self._current_widget() tab = self._current_widget()
tab.send_event(press_event) tab.send_event(press_event)
tab.send_event(release_event) tab.send_event(release_event)
@ -2112,3 +2128,24 @@ class CommandDispatcher:
""" """
if bg or tab or window or url != old_url: if bg or tab or window or url != old_url:
self.openurl(url=url, bg=bg, tab=tab, window=window) self.openurl(url=url, bg=bg, tab=tab, window=window)
@cmdutils.register(instance='command-dispatcher', scope='window')
def fullscreen(self, leave=False):
"""Toggle fullscreen mode.
Args:
leave: Only leave fullscreen if it was entered by the page.
"""
if leave:
tab = self._current_widget()
try:
tab.action.exit_fullscreen()
except browsertab.UnsupportedOperationError:
pass
return
window = self._tabbed_browser.window()
if window.isFullScreen():
window.showNormal()
else:
window.showFullScreen()

View File

@ -20,7 +20,6 @@
"""Shared QtWebKit/QtWebEngine code for downloads.""" """Shared QtWebKit/QtWebEngine code for downloads."""
import sys import sys
import shlex
import html import html
import os.path import os.path
import collections import collections
@ -28,15 +27,13 @@ import functools
import tempfile import tempfile
import sip import sip
from PyQt5.QtCore import (pyqtSlot, pyqtSignal, Qt, QObject, QUrl, QModelIndex, from PyQt5.QtCore import (pyqtSlot, pyqtSignal, Qt, QObject, QModelIndex,
QTimer, QAbstractListModel) QTimer, QAbstractListModel)
from PyQt5.QtGui import QDesktopServices
from qutebrowser.commands import cmdexc, cmdutils from qutebrowser.commands import cmdexc, cmdutils
from qutebrowser.config import config from qutebrowser.config import config
from qutebrowser.utils import (usertypes, standarddir, utils, message, log, from qutebrowser.utils import (usertypes, standarddir, utils, message, log,
qtutils) qtutils)
from qutebrowser.misc import guiprocess
ModelRole = usertypes.enum('ModelRole', ['item'], start=Qt.UserRole, ModelRole = usertypes.enum('ModelRole', ['item'], start=Qt.UserRole,
@ -128,7 +125,7 @@ def create_full_filename(basename, filename):
The full absolute path, or None if filename creation was not possible. The full absolute path, or None if filename creation was not possible.
""" """
# Remove chars which can't be encoded in the filename encoding. # Remove chars which can't be encoded in the filename encoding.
# See https://github.com/The-Compiler/qutebrowser/issues/427 # See https://github.com/qutebrowser/qutebrowser/issues/427
encoding = sys.getfilesystemencoding() encoding = sys.getfilesystemencoding()
filename = utils.force_encoding(filename, encoding) filename = utils.force_encoding(filename, encoding)
basename = utils.force_encoding(basename, encoding) basename = utils.force_encoding(basename, encoding)
@ -158,7 +155,7 @@ def get_filename_question(*, suggested_filename, url, parent=None):
q.title = "Save file to:" q.title = "Save file to:"
q.text = "Please enter a location for <b>{}</b>".format( q.text = "Please enter a location for <b>{}</b>".format(
html.escape(url.toDisplayString())) html.escape(url.toDisplayString()))
q.mode = usertypes.PromptMode.text q.mode = usertypes.PromptMode.download
q.completed.connect(q.deleteLater) q.completed.connect(q.deleteLater)
q.default = _path_suggestion(suggested_filename) q.default = _path_suggestion(suggested_filename)
return q return q
@ -197,6 +194,9 @@ class FileDownloadTarget(_DownloadTarget):
def suggested_filename(self): def suggested_filename(self):
return os.path.basename(self.filename) return os.path.basename(self.filename)
def __str__(self):
return self.filename
class FileObjDownloadTarget(_DownloadTarget): class FileObjDownloadTarget(_DownloadTarget):
@ -216,6 +216,12 @@ class FileObjDownloadTarget(_DownloadTarget):
except AttributeError: except AttributeError:
raise NoFilenameError raise NoFilenameError
def __str__(self):
try:
return 'file object at {}'.format(self.fileobj.name)
except AttributeError:
return 'anonymous file object'
class OpenFileDownloadTarget(_DownloadTarget): class OpenFileDownloadTarget(_DownloadTarget):
@ -234,6 +240,9 @@ class OpenFileDownloadTarget(_DownloadTarget):
def suggested_filename(self): def suggested_filename(self):
raise NoFilenameError raise NoFilenameError
def __str__(self):
return 'temporary file'
class DownloadItemStats(QObject): class DownloadItemStats(QObject):
@ -512,30 +521,19 @@ class AbstractDownloadItem(QObject):
Args: Args:
cmdline: The command to use as string. A `{}` is expanded to the cmdline: The command to use as string. A `{}` is expanded to the
filename. None means to use the system's default filename. None means to use the system's default
application. If no `{}` is found, the filename is appended application or `default-open-dispatcher` if set. If no
to the cmdline. `{}` is found, the filename is appended to the cmdline.
""" """
assert self.successful assert self.successful
filename = self._get_open_filename() filename = self._get_open_filename()
if filename is None: # pragma: no cover if filename is None: # pragma: no cover
log.downloads.error("No filename to open the download!") log.downloads.error("No filename to open the download!")
return return
# By using a singleshot timer, we ensure that we return fast. This
if cmdline is None: # is important on systems where process creation takes long, as
log.downloads.debug("Opening {} with the system application" # otherwise the prompt might hang around and cause bugs
.format(filename)) # (see issue #2296)
url = QUrl.fromLocalFile(filename) QTimer.singleShot(0, lambda: utils.open_file(filename, cmdline))
QDesktopServices.openUrl(url)
return
cmd, *args = shlex.split(cmdline)
args = [arg.replace('{}', filename) for arg in args]
if '{}' not in cmdline:
args.append(filename)
log.downloads.debug("Opening {} with {}"
.format(filename, [cmd] + args))
proc = guiprocess.GUIProcess(what='download')
proc.start_detached(cmd, args)
def _ensure_can_set_filename(self, filename): def _ensure_can_set_filename(self, filename):
"""Make sure we can still set a filename.""" """Make sure we can still set a filename."""
@ -564,13 +562,16 @@ class AbstractDownloadItem(QObject):
"""Set a temporary file when opening the download.""" """Set a temporary file when opening the download."""
raise NotImplementedError raise NotImplementedError
def _set_filename(self, filename, *, force_overwrite=False): def _set_filename(self, filename, *, force_overwrite=False,
remember_directory=True):
"""Set the filename to save the download to. """Set the filename to save the download to.
Args: Args:
filename: The full filename to save the download to. filename: The full filename to save the download to.
None: special value to stop the download. None: special value to stop the download.
force_overwrite: Force overwriting existing files. force_overwrite: Force overwriting existing files.
remember_directory: If True, remember the directory for future
downloads.
""" """
global last_used_directory global last_used_directory
filename = os.path.expanduser(filename) filename = os.path.expanduser(filename)
@ -600,6 +601,7 @@ class AbstractDownloadItem(QObject):
os.path.expanduser('~')) os.path.expanduser('~'))
self.basename = os.path.basename(self._filename) self.basename = os.path.basename(self._filename)
if remember_directory:
last_used_directory = os.path.dirname(self._filename) last_used_directory = os.path.dirname(self._filename)
log.downloads.debug("Setting filename to {}".format(filename)) log.downloads.debug("Setting filename to {}".format(filename))
@ -743,7 +745,7 @@ class AbstractDownloadManager(QObject):
def _remove_item(self, download): def _remove_item(self, download):
"""Remove a given download.""" """Remove a given download."""
if sip.isdeleted(self): if sip.isdeleted(self):
# https://github.com/The-Compiler/qutebrowser/issues/1242 # https://github.com/qutebrowser/qutebrowser/issues/1242
return return
try: try:
idx = self.downloads.index(download) idx = self.downloads.index(download)
@ -767,7 +769,6 @@ class AbstractDownloadManager(QObject):
def _init_filename_question(self, question, download): def _init_filename_question(self, question, download):
"""Set up an existing filename question with a download.""" """Set up an existing filename question with a download."""
question.mode = usertypes.PromptMode.download
question.answered.connect(download.set_target) question.answered.connect(download.set_target)
question.cancelled.connect(download.cancel) question.cancelled.connect(download.cancel)
download.cancelled.connect(question.abort) download.cancelled.connect(question.abort)

View File

@ -39,8 +39,8 @@ def update_geometry(obj):
Here we check if obj ("self") was deleted and just ignore the event if so. Here we check if obj ("self") was deleted and just ignore the event if so.
Original bug: https://github.com/The-Compiler/qutebrowser/issues/167 Original bug: https://github.com/qutebrowser/qutebrowser/issues/167
Workaround bug: https://github.com/The-Compiler/qutebrowser/issues/171 Workaround bug: https://github.com/qutebrowser/qutebrowser/issues/171
""" """
def _update_geometry(): def _update_geometry():
"""Actually update the geometry if the object still exists.""" """Actually update the geometry if the object still exists."""

View File

@ -101,7 +101,7 @@ class HintLabel(QLabel):
unmatched: The part of the text which was not typed yet. unmatched: The part of the text which was not typed yet.
""" """
if (config.get('hints', 'uppercase') and if (config.get('hints', 'uppercase') and
self._context.hint_mode == 'letter'): self._context.hint_mode in ['letter', 'word']):
matched = html.escape(matched.upper()) matched = html.escape(matched.upper())
unmatched = html.escape(unmatched.upper()) unmatched = html.escape(unmatched.upper())
else: else:
@ -235,7 +235,10 @@ class HintActions:
sel = (context.target == Target.yank_primary and sel = (context.target == Target.yank_primary and
utils.supports_selection()) utils.supports_selection())
urlstr = url.toString(QUrl.FullyEncoded | QUrl.RemovePassword) flags = QUrl.FullyEncoded | QUrl.RemovePassword
if url.scheme() == 'mailto':
flags |= QUrl.RemoveScheme
urlstr = url.toString(flags)
utils.set_clipboard(urlstr, selection=sel) utils.set_clipboard(urlstr, selection=sel)
msg = "Yanked URL to {}: {}".format( msg = "Yanked URL to {}: {}".format(
@ -284,11 +287,13 @@ class HintActions:
prompt = False if context.rapid else None prompt = False if context.rapid else None
qnam = context.tab.networkaccessmanager() qnam = context.tab.networkaccessmanager()
user_agent = context.tab.user_agent()
# FIXME:qtwebengine do this with QtWebEngine downloads? # FIXME:qtwebengine do this with QtWebEngine downloads?
download_manager = objreg.get('qtnetwork-download-manager', download_manager = objreg.get('qtnetwork-download-manager',
scope='window', window=self._win_id) scope='window', window=self._win_id)
download_manager.get(url, qnam=qnam, prompt_download_directory=prompt) download_manager.get(url, qnam=qnam, user_agent=user_agent,
prompt_download_directory=prompt)
def call_userscript(self, elem, context): def call_userscript(self, elem, context):
"""Call a userscript from a hint. """Call a userscript from a hint.
@ -311,7 +316,7 @@ class HintActions:
try: try:
userscripts.run_async(context.tab, cmd, *args, win_id=self._win_id, userscripts.run_async(context.tab, cmd, *args, win_id=self._win_id,
env=env) env=env)
except userscripts.UnsupportedError as e: except userscripts.Error as e:
raise HintingError(str(e)) raise HintingError(str(e))
def spawn(self, url, context): def spawn(self, url, context):
@ -567,6 +572,10 @@ class HintManager(QObject):
def _start_cb(self, elems): def _start_cb(self, elems):
"""Initialize the elements and labels based on the context set.""" """Initialize the elements and labels based on the context set."""
if self._context is None:
log.hints.debug("In _start_cb without context!")
return
if elems is None: if elems is None:
message.error("There was an error while getting hint elements") message.error("There was an error while getting hint elements")
return return
@ -750,6 +759,9 @@ class HintManager(QObject):
def handle_partial_key(self, keystr): def handle_partial_key(self, keystr):
"""Handle a new partial keypress.""" """Handle a new partial keypress."""
if self._context is None:
log.hints.debug("Got key without context!")
return
log.hints.debug("Handling new keystring: '{}'".format(keystr)) log.hints.debug("Handling new keystring: '{}'".format(keystr))
for string, label in self._context.labels.items(): for string, label in self._context.labels.items():
try: try:

View File

@ -26,9 +26,9 @@ from PyQt5.QtCore import pyqtSignal, pyqtSlot, QUrl, QObject
from qutebrowser.commands import cmdutils from qutebrowser.commands import cmdutils
from qutebrowser.utils import (utils, objreg, standarddir, log, qtutils, from qutebrowser.utils import (utils, objreg, standarddir, log, qtutils,
usertypes) usertypes, message)
from qutebrowser.config import config from qutebrowser.config import config
from qutebrowser.misc import lineparser from qutebrowser.misc import lineparser, objects
class Entry: class Entry:
@ -88,7 +88,7 @@ class Entry:
if not url.isValid(): if not url.isValid():
raise ValueError("Invalid URL: {}".format(url.errorString())) raise ValueError("Invalid URL: {}".format(url.errorString()))
# https://github.com/The-Compiler/qutebrowser/issues/670 # https://github.com/qutebrowser/qutebrowser/issues/670
atime = atime.lstrip('\0') atime = atime.lstrip('\0')
if '-' in atime: if '-' in atime:
@ -230,13 +230,23 @@ class WebHistory(QObject):
self._saved_count = len(self._new_history) self._saved_count = len(self._new_history)
@cmdutils.register(name='history-clear', instance='web-history') @cmdutils.register(name='history-clear', instance='web-history')
def clear(self): def clear(self, force=False):
"""Clear all browsing history. """Clear all browsing history.
Note this only clears the global history Note this only clears the global history
(e.g. `~/.local/share/qutebrowser/history` on Linux) but not cookies, (e.g. `~/.local/share/qutebrowser/history` on Linux) but not cookies,
the back/forward history of a tab, cache or other persistent data. the back/forward history of a tab, cache or other persistent data.
Args:
force: Don't ask for confirmation.
""" """
if force:
self._do_clear()
else:
message.confirm_async(self._do_clear, title="Clear all browsing "
"history?")
def _do_clear(self):
self._lineparser.clear() self._lineparser.clear()
self.history_dict.clear() self.history_dict.clear()
self._temp_history.clear() self._temp_history.clear()
@ -293,7 +303,6 @@ def init(parent=None):
parent=parent) parent=parent)
objreg.register('web-history', history) objreg.register('web-history', history)
used_backend = usertypes.arg2backend[objreg.get('args').backend] if objects.backend == usertypes.Backend.QtWebKit:
if used_backend == usertypes.Backend.QtWebKit:
from qutebrowser.browser.webkit import webkithistory from qutebrowser.browser.webkit import webkithistory
webkithistory.init(history) webkithistory.init(history)

View File

@ -24,9 +24,8 @@ import binascii
from PyQt5.QtWidgets import QWidget from PyQt5.QtWidgets import QWidget
from qutebrowser.utils import log, objreg from qutebrowser.utils import log, objreg, usertypes
from qutebrowser.misc import miscwidgets from qutebrowser.misc import miscwidgets, objects
from qutebrowser.config import config
def create(parent=None): def create(parent=None):
@ -37,7 +36,7 @@ def create(parent=None):
""" """
# Importing modules here so we don't depend on QtWebEngine without the # Importing modules here so we don't depend on QtWebEngine without the
# argument and to avoid circular imports. # argument and to avoid circular imports.
if objreg.get('args').backend == 'webengine': if objects.backend == usertypes.Backend.QtWebEngine:
from qutebrowser.browser.webengine import webengineinspector from qutebrowser.browser.webengine import webengineinspector
return webengineinspector.WebEngineInspector(parent) return webengineinspector.WebEngineInspector(parent)
else: else:
@ -91,13 +90,6 @@ class AbstractWebInspector(QWidget):
state_config['geometry']['inspector'] = geom state_config['geometry']['inspector'] = geom
super().closeEvent(e) super().closeEvent(e)
def _check_developer_extras(self):
"""Check if developer-extras are enabled."""
if not config.get('general', 'developer-extras'):
raise WebInspectorError(
"Please enable developer-extras before using the "
"webinspector!")
def inspect(self, page): def inspect(self, page):
"""Inspect the given QWeb(Engine)Page.""" """Inspect the given QWeb(Engine)Page."""
raise NotImplementedError raise NotImplementedError

View File

@ -64,8 +64,6 @@ class MouseEventFilter(QObject):
"""Handle mouse events on a tab. """Handle mouse events on a tab.
Attributes: Attributes:
_widget_class: The class of the main widget getting the events.
All other events are ignored.
_tab: The browsertab object this filter is installed on. _tab: The browsertab object this filter is installed on.
_handlers: A dict of handler functions for the handled events. _handlers: A dict of handler functions for the handled events.
_ignore_wheel_event: Whether to ignore the next wheelEvent. _ignore_wheel_event: Whether to ignore the next wheelEvent.
@ -73,9 +71,8 @@ class MouseEventFilter(QObject):
done when the mouse is released. done when the mouse is released.
""" """
def __init__(self, tab, *, widget_class, parent=None): def __init__(self, tab, *, parent=None):
super().__init__(parent) super().__init__(parent)
self._widget_class = widget_class
self._tab = tab self._tab = tab
self._handlers = { self._handlers = {
QEvent.MouseButtonPress: self._handle_mouse_press, QEvent.MouseButtonPress: self._handle_mouse_press,
@ -96,7 +93,10 @@ class MouseEventFilter(QObject):
return True return True
self._ignore_wheel_event = True self._ignore_wheel_event = True
self._tab.elements.find_at_pos(e.pos(), self._mousepress_insertmode_cb)
if e.button() != Qt.NoButton:
self._tab.elements.find_at_pos(e.pos(),
self._mousepress_insertmode_cb)
return False return False
@ -114,18 +114,26 @@ class MouseEventFilter(QObject):
e: The QWheelEvent. e: The QWheelEvent.
""" """
if self._ignore_wheel_event: if self._ignore_wheel_event:
# See https://github.com/The-Compiler/qutebrowser/issues/395 # See https://github.com/qutebrowser/qutebrowser/issues/395
self._ignore_wheel_event = False self._ignore_wheel_event = False
return True return True
if e.modifiers() & Qt.ControlModifier: if e.modifiers() & Qt.ControlModifier:
divider = config.get('input', 'mouse-zoom-divider') divider = config.get('input', 'mouse-zoom-divider')
if divider == 0:
return False
factor = self._tab.zoom.factor() + (e.angleDelta().y() / divider) factor = self._tab.zoom.factor() + (e.angleDelta().y() / divider)
if factor < 0: if factor < 0:
return False return False
perc = int(100 * factor) perc = int(100 * factor)
message.info("Zoom level: {}%".format(perc)) message.info("Zoom level: {}%".format(perc), replace=True)
self._tab.zoom.set_factor(factor) self._tab.zoom.set_factor(factor)
elif e.modifiers() & Qt.ShiftModifier:
if e.angleDelta().y() > 0:
self._tab.scroller.left()
else:
self._tab.scroller.right()
return True
return False return False
@ -201,9 +209,8 @@ class MouseEventFilter(QObject):
evtype = event.type() evtype = event.type()
if evtype not in self._handlers: if evtype not in self._handlers:
return False return False
if not isinstance(obj, self._widget_class): if obj is not self._tab.event_target():
log.mouse.debug("Ignoring {} to {} which is not an instance of " log.mouse.debug("Ignoring {} to {}".format(
"{}".format(event.__class__.__name__, obj, event.__class__.__name__, obj))
self._widget_class))
return False return False
return self._handlers[evtype](event) return self._handlers[evtype](event)

View File

@ -70,11 +70,11 @@ def path_up(url, count):
def _find_prevnext(prev, elems): def _find_prevnext(prev, elems):
"""Find a prev/next element in the given list of elements.""" """Find a prev/next element in the given list of elements."""
# First check for <link rel="prev(ious)|next"> # First check for <link rel="prev(ious)|next">
rel_values = ('prev', 'previous') if prev else ('next') rel_values = {'prev', 'previous'} if prev else {'next'}
for e in elems: for e in elems:
if e.tag_name() != 'link' or 'rel' not in e: if e.tag_name() not in ['link', 'a'] or 'rel' not in e:
continue continue
if e['rel'] in rel_values: if set(e['rel'].split(' ')) & rel_values:
log.hints.debug("Found {!r} with rel={}".format(e, e['rel'])) log.hints.debug("Found {!r} with rel={}".format(e, e['rel']))
return e return e

View File

@ -0,0 +1,3 @@
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
"""Modules related to network operations."""

View File

@ -0,0 +1,315 @@
# 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/>.
"""Evaluation of PAC scripts."""
import sys
import functools
from PyQt5.QtCore import QObject, pyqtSignal, pyqtSlot, QUrl
from PyQt5.QtNetwork import (QNetworkProxy, QNetworkRequest, QHostInfo,
QNetworkReply, QNetworkAccessManager,
QHostAddress)
from PyQt5.QtQml import QJSEngine, QJSValue
from qutebrowser.utils import log, utils, qtutils
class ParseProxyError(Exception):
"""Error while parsing PAC result string."""
pass
class EvalProxyError(Exception):
"""Error while evaluating PAC script."""
pass
def _js_slot(*args):
"""Wrap a methods as a JavaScript function.
Register a PACContext method as a JavaScript function, and catch
exceptions returning them as JavaScript Error objects.
Args:
args: Types of method arguments.
Return: Wrapped method.
"""
def _decorator(method):
@functools.wraps(method)
def new_method(self, *args, **kwargs):
try:
return method(self, *args, **kwargs)
except:
e = str(sys.exc_info()[0])
log.network.exception("PAC evaluation error")
# pylint: disable=protected-access
return self._error_con.callAsConstructor([e])
# pylint: enable=protected-access
return pyqtSlot(*args, result=QJSValue)(new_method)
return _decorator
class _PACContext(QObject):
"""Implementation of PAC API functions that require native calls.
See https://developer.mozilla.org/en-US/docs/Mozilla/Projects/Necko/Proxy_Auto-Configuration_(PAC)_file
"""
JS_DEFINITIONS = """
function dnsResolve(host) {
return PAC.dnsResolve(host);
}
function myIpAddress() {
return PAC.myIpAddress();
}
"""
def __init__(self, engine):
"""Create a new PAC API implementation instance.
Args:
engine: QJSEngine which is used for running PAC.
"""
super().__init__(parent=engine)
self._engine = engine
self._error_con = engine.globalObject().property("Error")
@_js_slot(str)
def dnsResolve(self, host):
"""Resolve a DNS hostname.
Resolves the given DNS hostname into an IP address, and returns it
in the dot-separated format as a string.
Args:
host: hostname to resolve.
"""
ips = QHostInfo.fromName(host)
if ips.error() != QHostInfo.NoError or not ips.addresses():
err_f = "Failed to resolve host during PAC evaluation: {}"
log.network.info(err_f.format(host))
return QJSValue(QJSValue.NullValue)
else:
return ips.addresses()[0].toString()
@_js_slot()
def myIpAddress(self):
"""Get host IP address.
Return the server IP address of the current machine, as a string in
the dot-separated integer format.
"""
return QHostAddress(QHostAddress.LocalHost).toString()
class PACResolver:
"""Evaluate PAC script files and resolve proxies."""
@staticmethod
def _parse_proxy_host(host_str):
host, _colon, port_str = host_str.partition(':')
try:
port = int(port_str)
except ValueError:
raise ParseProxyError("Invalid port number")
return (host, port)
@staticmethod
def _parse_proxy_entry(proxy_str):
"""Parse one proxy string entry, as described in PAC specification."""
config = [c.strip() for c in proxy_str.split(' ') if c]
if not config:
raise ParseProxyError("Empty proxy entry")
elif config[0] == "DIRECT":
if len(config) != 1:
raise ParseProxyError("Invalid number of parameters for " +
"DIRECT")
return QNetworkProxy(QNetworkProxy.NoProxy)
elif config[0] == "PROXY":
if len(config) != 2:
raise ParseProxyError("Invalid number of parameters for PROXY")
host, port = PACResolver._parse_proxy_host(config[1])
return QNetworkProxy(QNetworkProxy.HttpProxy, host, port)
elif config[0] == "SOCKS":
if len(config) != 2:
raise ParseProxyError("Invalid number of parameters for SOCKS")
host, port = PACResolver._parse_proxy_host(config[1])
return QNetworkProxy(QNetworkProxy.Socks5Proxy, host, port)
else:
err = "Unknown proxy type: {}"
raise ParseProxyError(err.format(config[0]))
@staticmethod
def _parse_proxy_string(proxy_str):
proxies = proxy_str.split(';')
return [PACResolver._parse_proxy_entry(x) for x in proxies]
def _evaluate(self, js_code, js_file):
ret = self._engine.evaluate(js_code, js_file)
if ret.isError():
err = "JavaScript error while evaluating PAC file: {}"
raise EvalProxyError(err.format(ret.toString()))
def __init__(self, pac_str):
"""Create a PAC resolver.
Args:
pac_str: JavaScript code containing PAC resolver.
"""
self._engine = QJSEngine()
self._ctx = _PACContext(self._engine)
self._engine.globalObject().setProperty(
"PAC", self._engine.newQObject(self._ctx))
self._evaluate(_PACContext.JS_DEFINITIONS, "pac_js_definitions")
self._evaluate(utils.read_file("javascript/pac_utils.js"), "pac_utils")
proxy_config = self._engine.newObject()
proxy_config.setProperty("bindings", self._engine.newObject())
self._engine.globalObject().setProperty("ProxyConfig", proxy_config)
self._evaluate(pac_str, "pac")
global_js_object = self._engine.globalObject()
self._resolver = global_js_object.property("FindProxyForURL")
if not self._resolver.isCallable():
err = "Cannot resolve FindProxyForURL function, got '{}' instead"
raise EvalProxyError(err.format(self._resolver.toString()))
def resolve(self, query, from_file=False):
"""Resolve a proxy via PAC.
Args:
query: QNetworkProxyQuery.
from_file: Whether the proxy info is coming from a file.
Return:
A list of QNetworkProxy objects in order of preference.
"""
if from_file:
string_flags = QUrl.PrettyDecoded
else:
string_flags = QUrl.RemoveUserInfo
if query.url().scheme() == 'https':
string_flags |= QUrl.RemovePath | QUrl.RemoveQuery
result = self._resolver.call([query.url().toString(string_flags),
query.peerHostName()])
result_str = result.toString()
if not result.isString():
err = "Got strange value from FindProxyForURL: '{}'"
raise EvalProxyError(err.format(result_str))
return self._parse_proxy_string(result_str)
class PACFetcher(QObject):
"""Asynchronous fetcher of PAC files."""
finished = pyqtSignal()
def __init__(self, url, parent=None):
"""Resolve a PAC proxy from URL.
Args:
url: QUrl of a PAC proxy.
"""
super().__init__(parent)
pac_prefix = "pac+"
assert url.scheme().startswith(pac_prefix)
url.setScheme(url.scheme()[len(pac_prefix):])
self._pac_url = url
self._manager = QNetworkAccessManager()
self._manager.setProxy(QNetworkProxy(QNetworkProxy.NoProxy))
self._reply = self._manager.get(QNetworkRequest(url))
self._reply.finished.connect(self._finish)
self._pac = None
self._error_message = None
@pyqtSlot()
def _finish(self):
if self._reply.error() != QNetworkReply.NoError:
error = "Can't fetch PAC file from URL, error code {}: {}"
self._error_message = error.format(
self._reply.error(), self._reply.errorString())
log.network.error(self._error_message)
else:
try:
pacscript = bytes(self._reply.readAll()).decode("utf-8")
except UnicodeError as e:
error = "Invalid encoding of a PAC file: {}"
self._error_message = error.format(e)
log.network.exception(self._error_message)
try:
self._pac = PACResolver(pacscript)
log.network.debug("Successfully evaluated PAC file.")
except EvalProxyError as e:
error = "Error in PAC evaluation: {}"
self._error_message = error.format(e)
log.network.exception(self._error_message)
self._manager = None
self._reply = None
self.finished.emit()
def _wait(self):
"""Wait until a reply from the remote server is received."""
if self._manager is not None:
loop = qtutils.EventLoop()
self.finished.connect(loop.quit)
loop.exec_()
def fetch_error(self):
"""Check if PAC script is successfully fetched.
Return None iff PAC script is downloaded and evaluated successfully,
error string otherwise.
"""
self._wait()
return self._error_message
def resolve(self, query):
"""Resolve a query via PAC.
Args: QNetworkProxyQuery.
Return a list of QNetworkProxy objects in order of preference.
"""
self._wait()
from_file = self._pac_url.scheme() == 'file'
try:
return self._pac.resolve(query, from_file=from_file)
except (EvalProxyError, ParseProxyError) as e:
log.network.exception("Error in PAC resolution: {}.".format(e))
# .invalid is guaranteed to be inaccessible in RFC 6761.
# Port 9 is for DISCARD protocol -- DISCARD servers act like
# /dev/null.
# Later NetworkManager.createRequest will detect this and display
# an error message.
error_host = "pac-resolve-error.qutebrowser.invalid"
return QNetworkProxy(QNetworkProxy.HttpProxy, error_host, 9)

View File

@ -23,17 +23,33 @@
from PyQt5.QtNetwork import QNetworkProxy, QNetworkProxyFactory from PyQt5.QtNetwork import QNetworkProxy, QNetworkProxyFactory
from qutebrowser.config import config, configtypes from qutebrowser.config import config, configtypes
from qutebrowser.utils import objreg
from qutebrowser.browser.network import pac
def init(): def init():
"""Set the application wide proxy factory.""" """Set the application wide proxy factory."""
QNetworkProxyFactory.setApplicationProxyFactory(ProxyFactory()) proxy_factory = ProxyFactory()
objreg.register('proxy-factory', proxy_factory)
QNetworkProxyFactory.setApplicationProxyFactory(proxy_factory)
class ProxyFactory(QNetworkProxyFactory): class ProxyFactory(QNetworkProxyFactory):
"""Factory for proxies to be used by qutebrowser.""" """Factory for proxies to be used by qutebrowser."""
def get_error(self):
"""Check if proxy can't be resolved.
Return:
None if proxy is correct, otherwise an error message.
"""
proxy = config.get('network', 'proxy')
if isinstance(proxy, pac.PACFetcher):
return proxy.fetch_error()
else:
return None
def queryProxy(self, query): def queryProxy(self, query):
"""Get the QNetworkProxies for a query. """Get the QNetworkProxies for a query.
@ -46,6 +62,8 @@ class ProxyFactory(QNetworkProxyFactory):
proxy = config.get('network', 'proxy') proxy = config.get('network', 'proxy')
if proxy is configtypes.SYSTEM_PROXY: if proxy is configtypes.SYSTEM_PROXY:
proxies = QNetworkProxyFactory.systemProxyForQuery(query) proxies = QNetworkProxyFactory.systemProxyForQuery(query)
elif isinstance(proxy, pac.PACFetcher):
proxies = proxy.resolve(query)
else: else:
proxies = [proxy] proxies = [proxy]
for p in proxies: for p in proxies:

View File

@ -100,6 +100,8 @@ SYSTEM_PDFJS_PATHS = [
# Debian pdf.js-common # Debian pdf.js-common
# Arch Linux pdfjs (AUR) # Arch Linux pdfjs (AUR)
'/usr/share/pdf.js/', '/usr/share/pdf.js/',
# Arch Linux pdf.js (AUR)
'/usr/share/javascript/pdf.js/',
# Debian libjs-pdf # Debian libjs-pdf
'/usr/share/javascript/pdf/', '/usr/share/javascript/pdf/',
# fallback # fallback

View File

@ -27,7 +27,7 @@ import collections
from PyQt5.QtCore import pyqtSlot, pyqtSignal, QTimer from PyQt5.QtCore import pyqtSlot, pyqtSignal, QTimer
from PyQt5.QtNetwork import QNetworkRequest, QNetworkReply from PyQt5.QtNetwork import QNetworkRequest, QNetworkReply
from qutebrowser.utils import message, usertypes, log, urlutils from qutebrowser.utils import message, usertypes, log, urlutils, utils
from qutebrowser.browser import downloads from qutebrowser.browser import downloads
from qutebrowser.browser.webkit import http from qutebrowser.browser.webkit import http
from qutebrowser.browser.webkit.network import networkmanager from qutebrowser.browser.webkit.network import networkmanager
@ -366,11 +366,12 @@ class DownloadManager(downloads.AbstractDownloadManager):
win_id, None, self) win_id, None, self)
@pyqtSlot('QUrl') @pyqtSlot('QUrl')
def get(self, url, **kwargs): def get(self, url, *, user_agent=None, **kwargs):
"""Start a download with a link URL. """Start a download with a link URL.
Args: Args:
url: The URL to get, as QUrl url: The URL to get, as QUrl
user_agent: The UA to set for the request, or None.
**kwargs: passed to get_request(). **kwargs: passed to get_request().
Return: Return:
@ -380,8 +381,32 @@ class DownloadManager(downloads.AbstractDownloadManager):
urlutils.invalid_url_error(url, "start download") urlutils.invalid_url_error(url, "start download")
return return
req = QNetworkRequest(url) req = QNetworkRequest(url)
if user_agent is not None:
req.setHeader(QNetworkRequest.UserAgentHeader, user_agent)
return self.get_request(req, **kwargs) return self.get_request(req, **kwargs)
def get_mhtml(self, tab, target):
"""Download the given tab as mhtml to the given DownloadTarget."""
assert tab.backend == usertypes.Backend.QtWebKit
from qutebrowser.browser.webkit import mhtml
if target is not None:
mhtml.start_download_checked(target, tab=tab)
return
suggested_fn = utils.sanitize_filename(tab.title() + ".mhtml")
filename = downloads.immediate_download_path()
if filename is not None:
target = downloads.FileDownloadTarget(filename)
mhtml.start_download_checked(target, tab=tab)
else:
question = downloads.get_filename_question(
suggested_filename=suggested_fn, url=tab.url(), parent=tab)
question.answered.connect(functools.partial(
mhtml.start_download_checked, tab=tab))
message.global_bridge.ask(question, blocking=False)
def get_request(self, request, *, target=None, **kwargs): def get_request(self, request, *, target=None, **kwargs):
"""Start a download with a QNetworkRequest. """Start a download with a QNetworkRequest.

View File

@ -24,11 +24,17 @@ Module attributes:
_HANDLERS: The handlers registered via decorators. _HANDLERS: The handlers registered via decorators.
""" """
import sys
import time
import datetime
import urllib.parse import urllib.parse
from PyQt5.QtCore import QUrlQuery
import qutebrowser import qutebrowser
from qutebrowser.utils import (version, utils, jinja, log, message, docutils, from qutebrowser.utils import (version, utils, jinja, log, message, docutils,
objreg, usertypes) objreg)
from qutebrowser.misc import objects
pyeval_output = ":pyeval was never called" pyeval_output = ":pyeval was never called"
@ -89,8 +95,7 @@ class add_handler: # pylint: disable=invalid-name
return function return function
def wrapper(self, *args, **kwargs): def wrapper(self, *args, **kwargs):
used_backend = usertypes.arg2backend[objreg.get('args').backend] if self._backend is not None and objects.backend != self._backend:
if self._backend is not None and used_backend != self._backend:
return self.wrong_backend_handler(*args, **kwargs) return self.wrong_backend_handler(*args, **kwargs)
else: else:
return self._function(*args, **kwargs) return self._function(*args, **kwargs)
@ -158,6 +163,87 @@ def qute_bookmarks(_url):
return 'text/html', html return 'text/html', html
@add_handler('history') # noqa
def qute_history(url):
"""Handler for qute:history. Display history."""
# Get current date from query parameter, if not given choose today.
curr_date = datetime.date.today()
try:
query_date = QUrlQuery(url).queryItemValue("date")
if query_date:
curr_date = datetime.datetime.strptime(query_date, "%Y-%m-%d")
curr_date = curr_date.date()
except ValueError:
log.misc.debug("Invalid date passed to qute:history: " + query_date)
one_day = datetime.timedelta(days=1)
next_date = curr_date + one_day
prev_date = curr_date - one_day
def history_iter(reverse):
"""Iterate through the history and get items we're interested in."""
curr_timestamp = time.mktime(curr_date.timetuple())
history = objreg.get('web-history').history_dict.values()
if reverse:
history = reversed(history)
for item in history:
# If we can't apply the reverse performance trick below,
# at least continue as early as possible with old items.
# This gets us down from 550ms to 123ms with 500k old items on my
# machine.
if item.atime < curr_timestamp and not reverse:
continue
# Convert timestamp
try:
item_atime = datetime.datetime.fromtimestamp(item.atime)
except (ValueError, OSError, OverflowError):
log.misc.debug("Invalid timestamp {}.".format(item.atime))
continue
if reverse and item_atime.date() < curr_date:
# If we could reverse the history in-place, and this entry is
# older than today, only older entries will follow, so we can
# abort here.
return
# Skip items not on curr_date
# Skip redirects
# Skip qute:// links
is_internal = item.url.scheme() == 'qute'
is_not_today = item_atime.date() != curr_date
if item.redirect or is_internal or is_not_today:
continue
# Use item's url as title if there's no title.
item_url = item.url.toDisplayString()
item_title = item.title if item.title else item_url
display_atime = item_atime.strftime("%X")
yield (item_url, item_title, display_atime)
if sys.hexversion >= 0x03050000:
# On Python >= 3.5 we can reverse the ordereddict in-place and thus
# apply an additional performance improvement in history_iter.
# On my machine, this gets us down from 550ms to 72us with 500k old
# items.
history = list(history_iter(reverse=True))
else:
# On Python 3.4, we can't do that, so we'd need to copy the entire
# history to a list. There, filter first and then reverse it here.
history = reversed(list(history_iter(reverse=False)))
html = jinja.render('history.html',
title='History',
history=history,
curr_date=curr_date,
next_date=next_date,
prev_date=prev_date,
today=datetime.date.today())
return 'text/html', html
@add_handler('pyeval') @add_handler('pyeval')
def qute_pyeval(_url): def qute_pyeval(_url):
"""Handler for qute:pyeval.""" """Handler for qute:pyeval."""

View File

@ -66,6 +66,7 @@ def authentication_required(url, authenticator, abort_on):
if answer is not None: if answer is not None:
authenticator.setUser(answer.user) authenticator.setUser(answer.user)
authenticator.setPassword(answer.password) authenticator.setPassword(answer.password)
return answer
def javascript_confirm(url, js_msg, abort_on): def javascript_confirm(url, js_msg, abort_on):
@ -157,7 +158,7 @@ def ignore_certificate_errors(url, errors, abort_on):
log.webview.debug("ssl-strict is False, only warning about errors") log.webview.debug("ssl-strict is False, only warning about errors")
for err in errors: for err in errors:
# FIXME we might want to use warn here (non-fatal error) # FIXME we might want to use warn here (non-fatal error)
# https://github.com/The-Compiler/qutebrowser/issues/114 # https://github.com/qutebrowser/qutebrowser/issues/114
message.error('Certificate error: {}'.format(err)) message.error('Certificate error: {}'.format(err))
return True return True
elif ssl_strict is True: elif ssl_strict is True:

View File

@ -33,7 +33,8 @@ from PyQt5.QtCore import QUrl, Qt, QEvent, QTimer
from PyQt5.QtGui import QMouseEvent from PyQt5.QtGui import QMouseEvent
from qutebrowser.config import config from qutebrowser.config import config
from qutebrowser.utils import log, usertypes, utils, qtutils from qutebrowser.keyinput import modeman
from qutebrowser.utils import log, usertypes, utils, qtutils, objreg
Group = usertypes.enum('Group', ['all', 'links', 'images', 'url', 'prevnext', Group = usertypes.enum('Group', ['all', 'links', 'images', 'url', 'prevnext',
@ -119,10 +120,6 @@ class AbstractWebElement(collections.abc.MutableMapping):
"""Get the geometry for this element.""" """Get the geometry for this element."""
raise NotImplementedError raise NotImplementedError
def style_property(self, name, *, strategy):
"""Get the element style resolved with the given strategy."""
raise NotImplementedError
def classes(self): def classes(self):
"""Get a list of classes assigned to this element.""" """Get a list of classes assigned to this element."""
raise NotImplementedError raise NotImplementedError
@ -139,7 +136,7 @@ class AbstractWebElement(collections.abc.MutableMapping):
raise NotImplementedError raise NotImplementedError
def value(self): def value(self):
"""Get the value attribute for this element.""" """Get the value attribute for this element, or None."""
raise NotImplementedError raise NotImplementedError
def set_value(self, value): def set_value(self, value):
@ -160,7 +157,7 @@ class AbstractWebElement(collections.abc.MutableMapping):
Skipping of small rectangles is due to <a> elements containing other Skipping of small rectangles is due to <a> elements containing other
elements with "display:block" style, see elements with "display:block" style, see
https://github.com/The-Compiler/qutebrowser/issues/1298 https://github.com/qutebrowser/qutebrowser/issues/1298
Args: Args:
elem_geometry: The geometry of the element, or None. elem_geometry: The geometry of the element, or None.
@ -222,18 +219,22 @@ class AbstractWebElement(collections.abc.MutableMapping):
else: else:
return False return False
def _is_editable_div(self): def _is_editable_classes(self):
"""Check if a div-element is editable. """Check if an element is editable based on its classes.
Return: Return:
True if the element is editable, False otherwise. True if the element is editable, False otherwise.
""" """
# Beginnings of div-classes which are actually some kind of editor. # Beginnings of div-classes which are actually some kind of editor.
div_classes = ('CodeMirror', # Javascript editor over a textarea classes = {
'div': ['CodeMirror', # Javascript editor over a textarea
'kix-', # Google Docs editor 'kix-', # Google Docs editor
'ace_') # http://ace.c9.io/ 'ace_'], # http://ace.c9.io/
'pre': ['CodeMirror'],
}
relevant_classes = classes[self.tag_name()]
for klass in self.classes(): for klass in self.classes():
if any([klass.startswith(e) for e in div_classes]): if any([klass.strip().startswith(e) for e in relevant_classes]):
return True return True
return False return False
@ -264,9 +265,8 @@ class AbstractWebElement(collections.abc.MutableMapping):
return config.get('input', 'insert-mode-on-plugins') and not strict return config.get('input', 'insert-mode-on-plugins') and not strict
elif tag == 'object': elif tag == 'object':
return self._is_editable_object() and not strict return self._is_editable_object() and not strict
elif tag == 'div': elif tag in ['div', 'pre']:
return self._is_editable_div() and not strict return self._is_editable_classes() and not strict
else:
return False return False
def is_text_input(self): def is_text_input(self):
@ -311,7 +311,7 @@ class AbstractWebElement(collections.abc.MutableMapping):
# Click the center of the largest square fitting into the top/left # Click the center of the largest square fitting into the top/left
# corner of the rectangle, this will help if part of the <a> element # corner of the rectangle, this will help if part of the <a> element
# is hidden behind other elements # is hidden behind other elements
# https://github.com/The-Compiler/qutebrowser/issues/1005 # https://github.com/qutebrowser/qutebrowser/issues/1005
rect = self.rect_on_view() rect = self.rect_on_view()
if rect.width() > rect.height(): if rect.width() > rect.height():
rect.setWidth(rect.height()) rect.setWidth(rect.height())
@ -322,14 +322,12 @@ class AbstractWebElement(collections.abc.MutableMapping):
raise Error("Element position is out of view!") raise Error("Element position is out of view!")
return pos return pos
def click(self, click_target): def _move_text_cursor(self):
"""Simulate a click on the element.""" """Move cursor to end after clicking."""
# FIXME:qtwebengine do we need this? raise NotImplementedError
# self._widget.setFocus()
# For QtWebKit
self._tab.data.override_target = click_target
def _click_fake_event(self, click_target):
"""Send a fake click event to the element."""
pos = self._mouse_pos() pos = self._mouse_pos()
log.webelem.debug("Sending fake click to {!r} at position {} with " log.webelem.debug("Sending fake click to {!r} at position {} with "
@ -358,11 +356,74 @@ class AbstractWebElement(collections.abc.MutableMapping):
for evt in events: for evt in events:
self._tab.send_event(evt) self._tab.send_event(evt)
def after_click(): QTimer.singleShot(0, self._move_text_cursor)
"""Move cursor to end after clicking."""
if self.is_text_input() and self.is_editable(): def _click_editable(self, click_target):
self._tab.caret.move_to_end_of_document() """Fake a click on an editable input field."""
QTimer.singleShot(0, after_click) raise NotImplementedError
def _click_js(self, click_target):
"""Fake a click by using the JS .click() method."""
raise NotImplementedError
def _click_href(self, click_target):
"""Fake a click on an element with a href by opening the link."""
baseurl = self._tab.url()
url = self.resolve_url(baseurl)
if url is None:
self._click_fake_event(click_target)
return
if click_target in [usertypes.ClickTarget.tab,
usertypes.ClickTarget.tab_bg]:
background = click_target == usertypes.ClickTarget.tab_bg
tabbed_browser = objreg.get('tabbed-browser', scope='window',
window=self._tab.win_id)
tabbed_browser.tabopen(url, background=background)
elif click_target == usertypes.ClickTarget.window:
from qutebrowser.mainwindow import mainwindow
window = mainwindow.MainWindow()
window.show()
window.tabbed_browser.tabopen(url)
else:
raise ValueError("Unknown ClickTarget {}".format(click_target))
def click(self, click_target, *, force_event=False):
"""Simulate a click on the element.
Args:
click_target: A usertypes.ClickTarget member, what kind of click
to simulate.
force_event: Force generating a fake mouse event.
"""
log.webelem.debug("Clicking {!r} with click_target {}, force_event {}"
.format(self, click_target, force_event))
if force_event:
self._click_fake_event(click_target)
return
href_tags = ['a', 'area', 'link']
if click_target == usertypes.ClickTarget.normal:
if self.tag_name() in href_tags:
log.webelem.debug("Clicking via JS click()")
self._click_js(click_target)
elif self.is_editable(strict=True):
log.webelem.debug("Clicking via JS focus()")
self._click_editable(click_target)
modeman.enter(self._tab.win_id, usertypes.KeyMode.insert,
'clicking input')
else:
self._click_fake_event(click_target)
elif click_target in [usertypes.ClickTarget.tab,
usertypes.ClickTarget.tab_bg,
usertypes.ClickTarget.window]:
if self.tag_name() in href_tags:
self._click_href(click_target)
else:
self._click_fake_event(click_target)
else:
raise ValueError("Unknown ClickTarget {}".format(click_target))
def hover(self): def hover(self):
"""Simulate a mouse hover over the element.""" """Simulate a mouse hover over the element."""

View File

@ -19,7 +19,9 @@
"""QtWebEngine specific code for downloads.""" """QtWebEngine specific code for downloads."""
import re
import os.path import os.path
import urllib
import functools import functools
from PyQt5.QtCore import pyqtSlot, Qt from PyQt5.QtCore import pyqtSlot, Qt
@ -28,7 +30,7 @@ from PyQt5.QtWebEngineWidgets import QWebEngineDownloadItem
# pylint: enable=no-name-in-module,import-error,useless-suppression # pylint: enable=no-name-in-module,import-error,useless-suppression
from qutebrowser.browser import downloads from qutebrowser.browser import downloads
from qutebrowser.utils import debug, usertypes, message, log from qutebrowser.utils import debug, usertypes, message, log, qtutils
class DownloadItem(downloads.AbstractDownloadItem): class DownloadItem(downloads.AbstractDownloadItem):
@ -45,6 +47,11 @@ class DownloadItem(downloads.AbstractDownloadItem):
qt_item.downloadProgress.connect(self.stats.on_download_progress) qt_item.downloadProgress.connect(self.stats.on_download_progress)
qt_item.stateChanged.connect(self._on_state_changed) qt_item.stateChanged.connect(self._on_state_changed)
def _is_page_download(self):
"""Check if this item is a page (i.e. mhtml) download."""
return (self._qt_item.savePageFormat() !=
QWebEngineDownloadItem.UnknownSaveFormat)
@pyqtSlot(QWebEngineDownloadItem.DownloadState) @pyqtSlot(QWebEngineDownloadItem.DownloadState)
def _on_state_changed(self, state): def _on_state_changed(self, state):
state_name = debug.qenum_key(QWebEngineDownloadItem, state) state_name = debug.qenum_key(QWebEngineDownloadItem, state)
@ -57,6 +64,9 @@ class DownloadItem(downloads.AbstractDownloadItem):
pass pass
elif state == QWebEngineDownloadItem.DownloadCompleted: elif state == QWebEngineDownloadItem.DownloadCompleted:
log.downloads.debug("Download {} finished".format(self.basename)) log.downloads.debug("Download {} finished".format(self.basename))
if self._is_page_download():
# Same logging as QtWebKit mhtml downloads.
log.downloads.debug("File successfully written.")
self.successful = True self.successful = True
self.done = True self.done = True
self.finished.emit() self.finished.emit()
@ -94,7 +104,8 @@ class DownloadItem(downloads.AbstractDownloadItem):
raise downloads.UnsupportedOperationError raise downloads.UnsupportedOperationError
def _set_tempfile(self, fileobj): def _set_tempfile(self, fileobj):
self._set_filename(fileobj.name, force_overwrite=True) self._set_filename(fileobj.name, force_overwrite=True,
remember_directory=False)
def _ensure_can_set_filename(self, filename): def _ensure_can_set_filename(self, filename):
state = self._qt_item.state() state = self._qt_item.state()
@ -122,9 +133,38 @@ class DownloadItem(downloads.AbstractDownloadItem):
self._qt_item.accept() self._qt_item.accept()
def _get_suggested_filename(path):
"""Convert a path we got from chromium to a suggested filename.
Chromium thinks we want to download stuff to ~/Download, so even if we
don't, we get downloads with a suffix like (1) for files existing there.
We simply strip the suffix off via regex.
See https://bugreports.qt.io/browse/QTBUG-56978
"""
filename = os.path.basename(path)
filename = re.sub(r'\([0-9]+\)(?=\.|$)', '', filename)
if not qtutils.version_check('5.8.1'):
# https://bugreports.qt.io/browse/QTBUG-58155
filename = urllib.parse.unquote(filename)
# Doing basename a *second* time because there could be a %2F in
# there...
filename = os.path.basename(filename)
return filename
class DownloadManager(downloads.AbstractDownloadManager): class DownloadManager(downloads.AbstractDownloadManager):
"""Manager for currently running downloads.""" """Manager for currently running downloads.
Attributes:
_mhtml_target: DownloadTarget for the next MHTML download.
"""
def __init__(self, parent=None):
super().__init__(parent)
self._mhtml_target = None
def install(self, profile): def install(self, profile):
"""Set up the download manager on a QWebEngineProfile.""" """Set up the download manager on a QWebEngineProfile."""
@ -134,12 +174,17 @@ class DownloadManager(downloads.AbstractDownloadManager):
@pyqtSlot(QWebEngineDownloadItem) @pyqtSlot(QWebEngineDownloadItem)
def handle_download(self, qt_item): def handle_download(self, qt_item):
"""Start a download coming from a QWebEngineProfile.""" """Start a download coming from a QWebEngineProfile."""
suggested_filename = os.path.basename(qt_item.path()) suggested_filename = _get_suggested_filename(qt_item.path())
download = DownloadItem(qt_item) download = DownloadItem(qt_item)
self._init_item(download, auto_remove=False, self._init_item(download, auto_remove=False,
suggested_filename=suggested_filename) suggested_filename=suggested_filename)
if self._mhtml_target is not None:
download.set_target(self._mhtml_target)
self._mhtml_target = None
return
filename = downloads.immediate_download_path() filename = downloads.immediate_download_path()
if filename is not None: if filename is not None:
# 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
@ -156,3 +201,10 @@ class DownloadManager(downloads.AbstractDownloadManager):
message.global_bridge.ask(question, blocking=True) message.global_bridge.ask(question, blocking=True)
# The filename is set via the question.answered signal, connected in # The filename is set via the question.answered signal, connected in
# _init_filename_question. # _init_filename_question.
def get_mhtml(self, tab, target):
"""Download the given tab as mhtml to the given target."""
assert tab.backend == usertypes.Backend.QtWebEngine
assert self._mhtml_target is None, self._mhtml_target
self._mhtml_target = target
tab.action.save_page()

View File

@ -22,7 +22,12 @@
"""QtWebEngine specific part of the web element API.""" """QtWebEngine specific part of the web element API."""
from PyQt5.QtCore import QRect from PyQt5.QtCore import QRect, Qt, QPoint, QEventLoop
from PyQt5.QtGui import QMouseEvent
from PyQt5.QtWidgets import QApplication
# pylint: disable=no-name-in-module,import-error,useless-suppression
from PyQt5.QtWebEngineWidgets import QWebEngineSettings
# pylint: enable=no-name-in-module,import-error,useless-suppression
from qutebrowser.utils import log, javascript from qutebrowser.utils import log, javascript
from qutebrowser.browser import webelem from qutebrowser.browser import webelem
@ -51,9 +56,7 @@ class WebEngineElement(webelem.AbstractWebElement):
def __setitem__(self, key, val): def __setitem__(self, key, val):
self._js_dict['attributes'][key] = val self._js_dict['attributes'][key] = val
js_code = javascript.assemble('webelem', 'set_attribute', self._id, self._js_call('set_attribute', key, val)
key, val)
self._tab.run_js_async(js_code)
def __delitem__(self, key): def __delitem__(self, key):
log.stub() log.stub()
@ -64,6 +67,11 @@ class WebEngineElement(webelem.AbstractWebElement):
def __len__(self): def __len__(self):
return len(self._js_dict['attributes']) return len(self._js_dict['attributes'])
def _js_call(self, name, *args, callback=None):
"""Wrapper to run stuff from webelem.js."""
js_code = javascript.assemble('webelem', name, self._id, *args)
self._tab.run_js_async(js_code, callback=callback)
def has_frame(self): def has_frame(self):
return True return True
@ -71,10 +79,6 @@ class WebEngineElement(webelem.AbstractWebElement):
log.stub() log.stub()
return QRect() return QRect()
def style_property(self, name, *, strategy):
log.stub()
return ''
def classes(self): def classes(self):
"""Get a list of classes assigned to this element.""" """Get a list of classes assigned to this element."""
return self._js_dict['class_name'].split() return self._js_dict['class_name'].split()
@ -91,25 +95,23 @@ class WebEngineElement(webelem.AbstractWebElement):
return self._js_dict['outer_xml'] return self._js_dict['outer_xml']
def value(self): def value(self):
return self._js_dict['value'] return self._js_dict.get('value', None)
def set_value(self, value): def set_value(self, value):
js_code = javascript.assemble('webelem', 'set_value', self._id, value) self._js_call('set_value', value)
self._tab.run_js_async(js_code)
def insert_text(self, text): def insert_text(self, text):
if not self.is_editable(strict=True): if not self.is_editable(strict=True):
raise webelem.Error("Element is not editable!") raise webelem.Error("Element is not editable!")
log.webelem.debug("Inserting text into element {!r}".format(self)) log.webelem.debug("Inserting text into element {!r}".format(self))
js_code = javascript.assemble('webelem', 'insert_text', self._id, text) self._js_call('insert_text', text)
self._tab.run_js_async(js_code)
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.
Skipping of small rectangles is due to <a> elements containing other Skipping of small rectangles is due to <a> elements containing other
elements with "display:block" style, see elements with "display:block" style, see
https://github.com/The-Compiler/qutebrowser/issues/1298 https://github.com/qutebrowser/qutebrowser/issues/1298
Args: Args:
elem_geometry: The geometry of the element, or None. elem_geometry: The geometry of the element, or None.
@ -146,6 +148,44 @@ class WebEngineElement(webelem.AbstractWebElement):
return QRect() return QRect()
def remove_blank_target(self): def remove_blank_target(self):
js_code = javascript.assemble('webelem', 'remove_blank_target', if self._js_dict['attributes'].get('target') == '_blank':
self._id) self._js_dict['attributes']['target'] = '_top'
self._tab.run_js_async(js_code) self._js_call('remove_blank_target')
def _move_text_cursor(self):
if self.is_text_input() and self.is_editable():
self._js_call('move_cursor_to_end')
def _click_editable(self, click_target):
# WORKAROUND for https://bugreports.qt.io/browse/QTBUG-58515
# pylint doesn't know about Qt.MouseEventSynthesizedBySystem
# because it was added in Qt 5.6, but we can be sure we use that with
# QtWebEngine.
# pylint: disable=no-member,useless-suppression
ev = QMouseEvent(QMouseEvent.MouseButtonPress, QPoint(0, 0),
QPoint(0, 0), QPoint(0, 0), Qt.NoButton, Qt.NoButton,
Qt.NoModifier, Qt.MouseEventSynthesizedBySystem)
# pylint: enable=no-member,useless-suppression
self._tab.send_event(ev)
# This actually "clicks" the element by calling focus() on it in JS.
self._js_call('focus')
self._move_text_cursor()
def _click_js(self, _click_target):
settings = QWebEngineSettings.globalSettings()
attribute = QWebEngineSettings.JavascriptCanOpenWindows
could_open_windows = settings.testAttribute(attribute)
settings.setAttribute(attribute, True)
# Get QtWebEngine do apply the settings
# (it does so with a 0ms QTimer...)
# This is also used in Qt's tests:
# https://github.com/qt/qtwebengine/commit/5e572e88efa7ba7c2b9138ec19e606d3e345ac90
qapp = QApplication.instance()
qapp.processEvents(QEventLoop.ExcludeSocketNotifiers |
QEventLoop.ExcludeUserInputEvents)
def reset_setting(_arg):
settings.setAttribute(attribute, could_open_windows)
self._js_call('click', callback=reset_setting)

View File

@ -41,13 +41,12 @@ class WebEngineInspector(inspector.AbstractWebInspector):
def inspect(self, _page): def inspect(self, _page):
"""Set up the inspector.""" """Set up the inspector."""
self._check_developer_extras()
try: try:
port = int(os.environ['QTWEBENGINE_REMOTE_DEBUGGING']) port = int(os.environ['QTWEBENGINE_REMOTE_DEBUGGING'])
except KeyError: except KeyError:
raise inspector.WebInspectorError( raise inspector.WebInspectorError(
"Debugging is not set up correctly. Did you restart after " "Debugging is not enabled. See 'qutebrowser --help' for "
"setting developer-extras?") "details.")
url = QUrl('http://localhost:{}/'.format(port)) url = QUrl('http://localhost:{}/'.format(port))
self._widget.load(url) self._widget.load(url)
self.show() self.show()

View File

@ -25,6 +25,7 @@ Module attributes:
""" """
import os import os
import logging
# pylint: disable=no-name-in-module,import-error,useless-suppression # pylint: disable=no-name-in-module,import-error,useless-suppression
from PyQt5.QtWebEngineWidgets import (QWebEngineSettings, QWebEngineProfile, from PyQt5.QtWebEngineWidgets import (QWebEngineSettings, QWebEngineProfile,
@ -32,8 +33,8 @@ from PyQt5.QtWebEngineWidgets import (QWebEngineSettings, QWebEngineProfile,
# pylint: enable=no-name-in-module,import-error,useless-suppression # pylint: enable=no-name-in-module,import-error,useless-suppression
from qutebrowser.browser import shared from qutebrowser.browser import shared
from qutebrowser.config import websettings, config from qutebrowser.config import config, websettings
from qutebrowser.utils import objreg, utils, standarddir, javascript from qutebrowser.utils import objreg, utils, standarddir, javascript, log
class Attribute(websettings.Attribute): class Attribute(websettings.Attribute):
@ -65,6 +66,47 @@ class StaticSetter(websettings.StaticSetter):
GLOBAL_SETTINGS = QWebEngineSettings.globalSettings GLOBAL_SETTINGS = QWebEngineSettings.globalSettings
class ProfileSetter(websettings.Base):
"""A setting set on the QWebEngineProfile."""
def __init__(self, getter, setter):
super().__init__()
self._getter = getter
self._setter = setter
def get(self, settings=None):
utils.unused(settings)
getter = getattr(QWebEngineProfile.defaultProfile(), self._getter)
return getter()
def _set(self, value, settings=None):
utils.unused(settings)
setter = getattr(QWebEngineProfile.defaultProfile(), self._setter)
setter(value)
class PersistentCookiePolicy(ProfileSetter):
"""The cookies -> store setting is different from other settings."""
def __init__(self):
super().__init__(getter='persistentCookiesPolicy',
setter='setPersistentCookiesPolicy')
def get(self, settings=None):
utils.unused(settings)
return config.get('content', 'cookies-store')
def _set(self, value, settings=None):
utils.unused(settings)
setter = getattr(QWebEngineProfile.defaultProfile(), self._setter)
setter(
QWebEngineProfile.AllowPersistentCookies if value else
QWebEngineProfile.NoPersistentCookies
)
def _init_stylesheet(profile): def _init_stylesheet(profile):
"""Initialize custom stylesheets. """Initialize custom stylesheets.
@ -98,6 +140,13 @@ def _init_stylesheet(profile):
profile.scripts().insert(script) profile.scripts().insert(script)
def _init_profile(profile):
"""Initialize settings set on the QWebEngineProfile."""
profile.setCachePath(os.path.join(standarddir.cache(), 'webengine'))
profile.setPersistentStoragePath(
os.path.join(standarddir.data(), 'webengine'))
def update_settings(section, option): def update_settings(section, option):
"""Update global settings when qwebsettings changed.""" """Update global settings when qwebsettings changed."""
websettings.update_mappings(MAPPINGS, section, option) websettings.update_mappings(MAPPINGS, section, option)
@ -106,17 +155,31 @@ def update_settings(section, option):
_init_stylesheet(profile) _init_stylesheet(profile)
def init(): def init(args):
"""Initialize the global QWebSettings.""" """Initialize the global QWebSettings."""
if config.get('general', 'developer-extras'): if args.enable_webengine_inspector:
# FIXME:qtwebengine Make sure we call globalSettings *after* this...
os.environ['QTWEBENGINE_REMOTE_DEBUGGING'] = str(utils.random_port()) os.environ['QTWEBENGINE_REMOTE_DEBUGGING'] = str(utils.random_port())
# Workaround for a black screen with some setups
# https://github.com/spyder-ide/spyder/issues/3226
if not os.environ.get('QUTE_NO_OPENGL_WORKAROUND'):
# Hide "No OpenGL_accelerate module loaded: ..." message
logging.getLogger('OpenGL.acceleratesupport').propagate = False
try:
from OpenGL import GL # pylint: disable=unused-variable
except ImportError:
pass
else:
log.misc.debug("Imported PyOpenGL as workaround")
profile = QWebEngineProfile.defaultProfile() profile = QWebEngineProfile.defaultProfile()
profile.setCachePath(os.path.join(standarddir.cache(), 'webengine')) _init_profile(profile)
profile.setPersistentStoragePath(
os.path.join(standarddir.data(), 'webengine'))
_init_stylesheet(profile) _init_stylesheet(profile)
# We need to do this here as a WORKAROUND for
# https://bugreports.qt.io/browse/QTBUG-58650
PersistentCookiePolicy().set(config.get('content', 'cookies-store'))
Attribute(QWebEngineSettings.FullScreenSupportEnabled).set(True)
websettings.init_mappings(MAPPINGS) websettings.init_mappings(MAPPINGS)
objreg.get('config').changed.connect(update_settings) objreg.get('config').changed.connect(update_settings)
@ -128,24 +191,17 @@ def shutdown():
# Missing QtWebEngine attributes: # Missing QtWebEngine attributes:
# - ErrorPageEnabled (should not be exposed, but set)
# - FullScreenSupportEnabled
# - ScreenCaptureEnabled # - ScreenCaptureEnabled
# - Accelerated2dCanvasEnabled # - Accelerated2dCanvasEnabled
# - AutoLoadIconsForPage # - AutoLoadIconsForPage
# - TouchIconsEnabled # - TouchIconsEnabled
# - FocusOnNavigationEnabled (5.8)
# - AllowRunningInsecureContent (5.8)
# #
# Missing QtWebEngine fonts: # Missing QtWebEngine fonts:
# - FantasyFont # - FantasyFont
# - PictographFont # - PictographFont
#
# TODO settings on profile:
# - httpCacheMaximumSize
# - persistentCookiesPolicy
# - offTheRecord
#
# TODO settings elsewhere:
# - proxy
MAPPINGS = { MAPPINGS = {
'content': { 'content': {
@ -165,6 +221,11 @@ MAPPINGS = {
Attribute(QWebEngineSettings.LocalContentCanAccessRemoteUrls), Attribute(QWebEngineSettings.LocalContentCanAccessRemoteUrls),
'local-content-can-access-file-urls': 'local-content-can-access-file-urls':
Attribute(QWebEngineSettings.LocalContentCanAccessFileUrls), Attribute(QWebEngineSettings.LocalContentCanAccessFileUrls),
# https://bugreports.qt.io/browse/QTBUG-58650
# 'cookies-store':
# PersistentCookiePolicy(),
'webgl':
Attribute(QWebEngineSettings.WebGLEnabled),
}, },
'input': { 'input': {
'spatial-navigation': 'spatial-navigation':
@ -221,6 +282,9 @@ MAPPINGS = {
'storage': { 'storage': {
'local-storage': 'local-storage':
Attribute(QWebEngineSettings.LocalStorageEnabled), Attribute(QWebEngineSettings.LocalStorageEnabled),
'cache-size':
ProfileSetter(getter='httpCacheMaximumSize',
setter='setHttpCacheMaximumSize')
}, },
'general': { 'general': {
'xss-auditing': 'xss-auditing':
@ -232,7 +296,8 @@ MAPPINGS = {
} }
try: try:
MAPPINGS['content']['webgl'] = Attribute(QWebEngineSettings.WebGLEnabled) MAPPINGS['general']['print-element-backgrounds'] = Attribute(
QWebEngineSettings.PrintElementBackgrounds)
except AttributeError: except AttributeError:
# Added in Qt 5.7 # Added in Qt 5.8
pass pass

View File

@ -24,10 +24,12 @@
import functools import functools
import sip
from PyQt5.QtCore import pyqtSlot, Qt, QEvent, QPoint, QUrl, QTimer from PyQt5.QtCore import pyqtSlot, Qt, QEvent, QPoint, QUrl, QTimer
from PyQt5.QtGui import QKeyEvent, QIcon from PyQt5.QtGui import QKeyEvent
from PyQt5.QtNetwork import QAuthenticator
# pylint: disable=no-name-in-module,import-error,useless-suppression # pylint: disable=no-name-in-module,import-error,useless-suppression
from PyQt5.QtWidgets import QOpenGLWidget, QApplication from PyQt5.QtWidgets import QApplication
from PyQt5.QtWebEngineWidgets import (QWebEnginePage, QWebEngineScript, from PyQt5.QtWebEngineWidgets import (QWebEnginePage, QWebEngineScript,
QWebEngineProfile) QWebEngineProfile)
# pylint: enable=no-name-in-module,import-error,useless-suppression # pylint: enable=no-name-in-module,import-error,useless-suppression
@ -36,8 +38,9 @@ from qutebrowser.browser import browsertab, mouse, shared
from qutebrowser.browser.webengine import (webview, webengineelem, tabhistory, from qutebrowser.browser.webengine import (webview, webengineelem, tabhistory,
interceptor, webenginequtescheme, interceptor, webenginequtescheme,
webenginedownloads) webenginedownloads)
from qutebrowser.misc import miscwidgets
from qutebrowser.utils import (usertypes, qtutils, log, javascript, utils, from qutebrowser.utils import (usertypes, qtutils, log, javascript, utils,
objreg) objreg, jinja)
_qute_scheme_handler = None _qute_scheme_handler = None
@ -77,25 +80,44 @@ _JS_WORLD_MAP = {
} }
class WebEngineAction(browsertab.AbstractAction):
"""QtWebKit implementations related to web actions."""
def _action(self, action):
self._widget.triggerPageAction(action)
def exit_fullscreen(self):
self._action(QWebEnginePage.ExitFullScreen)
def save_page(self):
"""Save the current page."""
self._action(QWebEnginePage.SavePage)
class WebEnginePrinting(browsertab.AbstractPrinting): class WebEnginePrinting(browsertab.AbstractPrinting):
"""QtWebEngine implementations related to printing.""" """QtWebEngine implementations related to printing."""
def check_pdf_support(self): def check_pdf_support(self):
if not hasattr(self._widget.page(), 'printToPdf'): return True
raise browsertab.WebTabError(
"Printing to PDF is unsupported with QtWebEngine on Qt < 5.7")
def check_printer_support(self): def check_printer_support(self):
if not hasattr(self._widget.page(), 'print'):
raise browsertab.WebTabError( raise browsertab.WebTabError(
"Printing is unsupported with QtWebEngine") "Printing is unsupported with QtWebEngine on Qt < 5.8")
def check_preview_support(self):
raise browsertab.WebTabError(
"Print previews are unsupported with QtWebEngine")
def to_pdf(self, filename): def to_pdf(self, filename):
self._widget.page().printToPdf(filename) self._widget.page().printToPdf(filename)
def to_printer(self, printer): def to_printer(self, printer, callback=None):
# Should never be called if callback is None:
assert False callback = lambda _ok: None
self._widget.page().print(printer, callback)
class WebEngineSearch(browsertab.AbstractSearch): class WebEngineSearch(browsertab.AbstractSearch):
@ -223,9 +245,6 @@ class WebEngineScroller(browsertab.AbstractScroller):
"""QtWebEngine implementations related to scrolling.""" """QtWebEngine implementations related to scrolling."""
# FIXME:qtwebengine
# using stuff here with a big count/argument causes memory leaks and hangs
def __init__(self, tab, parent=None): def __init__(self, tab, parent=None):
super().__init__(tab, parent) super().__init__(tab, parent)
self._pos_perc = (0, 0) self._pos_perc = (0, 0)
@ -235,15 +254,10 @@ class WebEngineScroller(browsertab.AbstractScroller):
def _init_widget(self, widget): def _init_widget(self, widget):
super()._init_widget(widget) super()._init_widget(widget)
page = widget.page() page = widget.page()
try:
page.scrollPositionChanged.connect(self._update_pos) page.scrollPositionChanged.connect(self._update_pos)
except AttributeError:
log.stub('scrollPositionChanged, on Qt < 5.7')
self._pos_perc = (None, None)
def _key_press(self, key, count=1): def _key_press(self, key, count=1):
# FIXME:qtwebengine Abort scrolling if the minimum/maximum was reached. for _ in range(min(count, 5000)):
for _ in range(count):
press_evt = QKeyEvent(QEvent.KeyPress, key, Qt.NoModifier, 0, 0, 0) press_evt = QKeyEvent(QEvent.KeyPress, key, Qt.NoModifier, 0, 0, 0)
release_evt = QKeyEvent(QEvent.KeyRelease, key, Qt.NoModifier, release_evt = QKeyEvent(QEvent.KeyRelease, key, Qt.NoModifier,
0, 0, 0) 0, 0, 0)
@ -355,6 +369,11 @@ class WebEngineHistory(browsertab.AbstractHistory):
return self._history.canGoForward() return self._history.canGoForward()
def serialize(self): def serialize(self):
# WORKAROUND for https://github.com/qutebrowser/qutebrowser/issues/2289
# FIXME:qtwebengine can we get rid of this with Qt 5.8.1?
scheme = self._history.currentItem().url().scheme()
if scheme in ['view-source', 'chrome']:
raise browsertab.WebTabError("Can't serialize special URL!")
return qtutils.serialize(self._history) return qtutils.serialize(self._history)
def deserialize(self, data): def deserialize(self, data):
@ -414,7 +433,7 @@ class WebEngineElements(browsertab.AbstractElements):
js_elem: The element serialized from javascript. js_elem: The element serialized from javascript.
""" """
debug_str = ('None' if js_elem is None debug_str = ('None' if js_elem is None
else utils.elide(repr(js_elem), 100)) else utils.elide(repr(js_elem), 1000))
log.webview.debug("Got element from JS: {}".format(debug_str)) log.webview.debug("Got element from JS: {}".format(debug_str))
if js_elem is None: if js_elem is None:
@ -442,6 +461,7 @@ class WebEngineElements(browsertab.AbstractElements):
def find_at_pos(self, pos, callback): def find_at_pos(self, pos, callback):
assert pos.x() >= 0 assert pos.x() >= 0
assert pos.y() >= 0 assert pos.y() >= 0
pos /= self._tab.zoom.factor()
js_code = javascript.assemble('webelem', 'element_at_pos', js_code = javascript.assemble('webelem', 'element_at_pos',
pos.x(), pos.y()) pos.x(), pos.y())
js_cb = functools.partial(self._js_cb_single, callback) js_cb = functools.partial(self._js_cb_single, callback)
@ -452,8 +472,6 @@ class WebEngineTab(browsertab.AbstractTab):
"""A QtWebEngine tab in the browser.""" """A QtWebEngine tab in the browser."""
WIDGET_CLASS = QOpenGLWidget
def __init__(self, win_id, mode_manager, parent=None): def __init__(self, win_id, mode_manager, parent=None):
super().__init__(win_id=win_id, mode_manager=mode_manager, super().__init__(win_id=win_id, mode_manager=mode_manager,
parent=parent) parent=parent)
@ -466,12 +484,13 @@ class WebEngineTab(browsertab.AbstractTab):
self.search = WebEngineSearch(parent=self) self.search = WebEngineSearch(parent=self)
self.printing = WebEnginePrinting() self.printing = WebEnginePrinting()
self.elements = WebEngineElements(self) self.elements = WebEngineElements(self)
self.action = WebEngineAction()
self._set_widget(widget) self._set_widget(widget)
self._connect_signals() self._connect_signals()
self.backend = usertypes.Backend.QtWebEngine self.backend = usertypes.Backend.QtWebEngine
self._init_js() self._init_js()
self._child_event_filter = None self._child_event_filter = None
self.needs_qtbug54419_workaround = False self._saved_zoom = None
def _init_js(self): def _init_js(self):
js_code = '\n'.join([ js_code = '\n'.join([
@ -485,12 +504,6 @@ class WebEngineTab(browsertab.AbstractTab):
script.setSourceCode(js_code) script.setSourceCode(js_code)
page = self._widget.page() page = self._widget.page()
try:
page.runJavaScript("", QWebEngineScript.ApplicationWorld)
except TypeError:
# We're unable to pass a world to runJavaScript
script.setWorldId(QWebEngineScript.MainWorld)
else:
script.setWorldId(QWebEngineScript.ApplicationWorld) script.setWorldId(QWebEngineScript.ApplicationWorld)
# FIXME:qtwebengine what about runsOnSubFrames? # FIXME:qtwebengine what about runsOnSubFrames?
@ -503,7 +516,15 @@ class WebEngineTab(browsertab.AbstractTab):
parent=self) parent=self)
self._widget.installEventFilter(self._child_event_filter) self._widget.installEventFilter(self._child_event_filter)
@pyqtSlot()
def _restore_zoom(self):
if self._saved_zoom is None:
return
self.zoom.set_factor(self._saved_zoom)
self._saved_zoom = None
def openurl(self, url): def openurl(self, url):
self._saved_zoom = self.zoom.factor()
self._openurl_prepare(url) self._openurl_prepare(url)
self._widget.load(url) self._widget.load(url)
@ -528,22 +549,16 @@ class WebEngineTab(browsertab.AbstractTab):
else: else:
world_id = _JS_WORLD_MAP[world] world_id = _JS_WORLD_MAP[world]
try:
if callback is None: if callback is None:
self._widget.page().runJavaScript(code, world_id) self._widget.page().runJavaScript(code, world_id)
else: else:
self._widget.page().runJavaScript(code, world_id, callback) self._widget.page().runJavaScript(code, world_id, callback)
except TypeError:
if world is not None and world != usertypes.JsWorld.jseval:
log.webview.warning("Ignoring world ID on Qt < 5.7")
# Qt < 5.7
if callback is None:
self._widget.page().runJavaScript(code)
else:
self._widget.page().runJavaScript(code, callback)
def shutdown(self): def shutdown(self):
self.shutting_down.emit() self.shutting_down.emit()
# WORKAROUND for
# https://bugreports.qt.io/browse/QTBUG-58563
self.search.clear()
self._widget.shutdown() self._widget.shutdown()
def reload(self, *, force=False): def reload(self, *, force=False):
@ -560,23 +575,24 @@ class WebEngineTab(browsertab.AbstractTab):
return self._widget.title() return self._widget.title()
def icon(self): def icon(self):
try:
return self._widget.icon() return self._widget.icon()
except AttributeError:
log.stub('on Qt < 5.7')
return QIcon()
def set_html(self, html, base_url): def set_html(self, html, base_url=None):
# FIXME:qtwebengine # FIXME:qtwebengine
# check this and raise an exception if too big: # check this and raise an exception if too big:
# Warning: The content will be percent encoded before being sent to the # Warning: The content will be percent encoded before being sent to the
# renderer via IPC. This may increase its size. The maximum size of the # renderer via IPC. This may increase its size. The maximum size of the
# percent encoded content is 2 megabytes minus 30 bytes. # percent encoded content is 2 megabytes minus 30 bytes.
if base_url is None:
base_url = QUrl()
self._widget.setHtml(html, base_url) self._widget.setHtml(html, base_url)
def networkaccessmanager(self): def networkaccessmanager(self):
return None return None
def user_agent(self):
return None
def clear_ssl_errors(self): def clear_ssl_errors(self):
raise browsertab.UnsupportedOperationError raise browsertab.UnsupportedOperationError
@ -602,31 +618,76 @@ class WebEngineTab(browsertab.AbstractTab):
@pyqtSlot(QUrl, 'QAuthenticator*') @pyqtSlot(QUrl, 'QAuthenticator*')
def _on_authentication_required(self, url, authenticator): def _on_authentication_required(self, url, authenticator):
# FIXME:qtwebengine support .netrc # FIXME:qtwebengine support .netrc
shared.authentication_required(url, authenticator, answer = shared.authentication_required(
abort_on=[self.shutting_down, url, authenticator, abort_on=[self.shutting_down,
self.load_started]) self.load_started])
if answer is None:
try:
# pylint: disable=no-member, useless-suppression
sip.assign(authenticator, QAuthenticator())
except AttributeError:
# WORKAROUND for
# https://www.riverbankcomputing.com/pipermail/pyqt/2016-December/038400.html
url_string = url.toDisplayString()
error_page = jinja.render(
'error.html',
title="Error loading page: {}".format(url_string),
url=url_string, error="Authentication required", icon='')
self.set_html(error_page)
@pyqtSlot('QWebEngineFullScreenRequest')
def _on_fullscreen_requested(self, request):
request.accept()
on = request.toggleOn()
self.fullscreen_requested.emit(on)
if on:
notification = miscwidgets.FullscreenNotification(self)
notification.show()
notification.set_timeout(3000)
@pyqtSlot(QWebEnginePage.RenderProcessTerminationStatus, int)
def _on_render_process_terminated(self, status, exitcode):
"""Show an error when the renderer process terminated."""
if (status == QWebEnginePage.AbnormalTerminationStatus and
exitcode == 256):
# WORKAROUND for https://bugreports.qt.io/browse/QTBUG-58697
status = QWebEnginePage.CrashedTerminationStatus
status_map = {
QWebEnginePage.NormalTerminationStatus:
browsertab.TerminationStatus.normal,
QWebEnginePage.AbnormalTerminationStatus:
browsertab.TerminationStatus.abnormal,
QWebEnginePage.CrashedTerminationStatus:
browsertab.TerminationStatus.crashed,
QWebEnginePage.KilledTerminationStatus:
browsertab.TerminationStatus.killed,
-1:
browsertab.TerminationStatus.unknown,
}
self.renderer_process_terminated.emit(status_map[status], exitcode)
def _connect_signals(self): def _connect_signals(self):
view = self._widget view = self._widget
page = view.page() page = view.page()
page.windowCloseRequested.connect(self.window_close_requested) page.windowCloseRequested.connect(self.window_close_requested)
page.linkHovered.connect(self.link_hovered) page.linkHovered.connect(self.link_hovered)
page.loadProgress.connect(self._on_load_progress) page.loadProgress.connect(self._on_load_progress)
page.loadStarted.connect(self._on_load_started) page.loadStarted.connect(self._on_load_started)
page.loadFinished.connect(self._on_history_trigger) page.loadFinished.connect(self._on_history_trigger)
view.titleChanged.connect(self.title_changed) page.loadFinished.connect(self._restore_zoom)
view.urlChanged.connect(self._on_url_changed)
page.loadFinished.connect(self._on_load_finished) page.loadFinished.connect(self._on_load_finished)
page.certificate_error.connect(self._on_ssl_errors) page.certificate_error.connect(self._on_ssl_errors)
page.authenticationRequired.connect(self._on_authentication_required) page.authenticationRequired.connect(self._on_authentication_required)
try: page.fullScreenRequested.connect(self._on_fullscreen_requested)
view.iconChanged.connect(self.icon_changed)
except AttributeError:
log.stub('iconChanged, on Qt < 5.7')
try:
page.contentsSizeChanged.connect(self.contents_size_changed) page.contentsSizeChanged.connect(self.contents_size_changed)
except AttributeError:
log.stub('contentsSizeChanged, on Qt < 5.7')
def _event_target(self): view.titleChanged.connect(self.title_changed)
view.urlChanged.connect(self._on_url_changed)
view.renderProcessTerminated.connect(
self._on_render_process_terminated)
view.iconChanged.connect(self.icon_changed)
def event_target(self):
return self._widget.focusProxy() return self._widget.focusProxy()

View File

@ -19,10 +19,10 @@
"""The main browser widget for QtWebEngine.""" """The main browser widget for QtWebEngine."""
import os
import functools import functools
from PyQt5.QtCore import pyqtSignal, pyqtSlot, QUrl, PYQT_VERSION from PyQt5.QtCore import pyqtSignal, pyqtSlot, QUrl, PYQT_VERSION
from PyQt5.QtGui import QPalette
# pylint: disable=no-name-in-module,import-error,useless-suppression # pylint: disable=no-name-in-module,import-error,useless-suppression
from PyQt5.QtWebEngineWidgets import QWebEngineView, QWebEnginePage from PyQt5.QtWebEngineWidgets import QWebEngineView, QWebEnginePage
# pylint: enable=no-name-in-module,import-error,useless-suppression # pylint: enable=no-name-in-module,import-error,useless-suppression
@ -30,8 +30,8 @@ from PyQt5.QtWebEngineWidgets import QWebEngineView, QWebEnginePage
from qutebrowser.browser import shared from qutebrowser.browser import shared
from qutebrowser.browser.webengine import certificateerror from qutebrowser.browser.webengine import certificateerror
from qutebrowser.config import config from qutebrowser.config import config
from qutebrowser.utils import (log, debug, usertypes, qtutils, jinja, urlutils, from qutebrowser.utils import (log, debug, usertypes, jinja, urlutils, message,
message) objreg)
class WebEngineView(QWebEngineView): class WebEngineView(QWebEngineView):
@ -42,7 +42,10 @@ class WebEngineView(QWebEngineView):
super().__init__(parent) super().__init__(parent)
self._win_id = win_id self._win_id = win_id
self._tabdata = tabdata self._tabdata = tabdata
self.setPage(WebEnginePage(parent=self))
theme_color = self.style().standardPalette().color(QPalette.Base)
page = WebEnginePage(theme_color=theme_color, parent=self)
self.setPage(page)
def shutdown(self): def shutdown(self):
self.page().shutdown() self.page().shutdown()
@ -67,7 +70,7 @@ class WebEngineView(QWebEngineView):
A window without decoration. A window without decoration.
QWebEnginePage::WebBrowserBackgroundTab: QWebEnginePage::WebBrowserBackgroundTab:
A web browser tab without hiding the current visible A web browser tab without hiding the current visible
WebEngineView. (Added in Qt 5.7) WebEngineView.
Return: Return:
The new QWebEngineView object. The new QWebEngineView object.
@ -78,13 +81,6 @@ class WebEngineView(QWebEngineView):
log.webview.debug("createWindow with type {}, background_tabs " log.webview.debug("createWindow with type {}, background_tabs "
"{}".format(debug_type, background_tabs)) "{}".format(debug_type, background_tabs))
try:
background_tab_wintype = QWebEnginePage.WebBrowserBackgroundTab
except AttributeError:
# This is unavailable with an older PyQt, but we still might get
# this with a newer Qt...
background_tab_wintype = 0x0003
if wintype == QWebEnginePage.WebBrowserWindow: if wintype == QWebEnginePage.WebBrowserWindow:
# Shift-Alt-Click # Shift-Alt-Click
target = usertypes.ClickTarget.window target = usertypes.ClickTarget.window
@ -99,7 +95,7 @@ class WebEngineView(QWebEngineView):
target = usertypes.ClickTarget.tab target = usertypes.ClickTarget.tab
else: else:
target = usertypes.ClickTarget.tab_bg target = usertypes.ClickTarget.tab_bg
elif wintype == background_tab_wintype: elif wintype == QWebEnginePage.WebBrowserBackgroundTab:
# Middle-click / Ctrl-Click # Middle-click / Ctrl-Click
if background_tabs: if background_tabs:
target = usertypes.ClickTarget.tab_bg target = usertypes.ClickTarget.tab_bg
@ -109,15 +105,6 @@ class WebEngineView(QWebEngineView):
raise ValueError("Invalid wintype {}".format(debug_type)) raise ValueError("Invalid wintype {}".format(debug_type))
tab = shared.get_tab(self._win_id, target) tab = shared.get_tab(self._win_id, target)
# WORKAROUND for https://bugreports.qt.io/browse/QTBUG-54419
vercheck = qtutils.version_check
qtbug54419_fixed = ((vercheck('5.6.2') and not vercheck('5.7.0')) or
qtutils.version_check('5.7.1') or
os.environ.get('QUTE_QTBUG54419_PATCHED', ''))
if not qtbug54419_fixed:
tab.needs_qtbug54419_workaround = True
return tab._widget # pylint: disable=protected-access return tab._widget # pylint: disable=protected-access
@ -127,6 +114,7 @@ class WebEnginePage(QWebEnginePage):
Attributes: Attributes:
_is_shutting_down: Whether the page is currently shutting down. _is_shutting_down: Whether the page is currently shutting down.
_theme_color: The theme background color.
Signals: Signals:
certificate_error: Emitted on certificate errors. certificate_error: Emitted on certificate errors.
@ -136,11 +124,21 @@ class WebEnginePage(QWebEnginePage):
certificate_error = pyqtSignal() certificate_error = pyqtSignal()
shutting_down = pyqtSignal() shutting_down = pyqtSignal()
def __init__(self, parent=None): def __init__(self, theme_color, parent=None):
super().__init__(parent) super().__init__(parent)
self._is_shutting_down = False self._is_shutting_down = False
self.featurePermissionRequested.connect( self.featurePermissionRequested.connect(
self._on_feature_permission_requested) self._on_feature_permission_requested)
self._theme_color = theme_color
self._set_bg_color()
objreg.get('config').changed.connect(self._set_bg_color)
@config.change_filter('colors', 'webpage.bg')
def _set_bg_color(self):
col = config.get('colors', 'webpage.bg')
if col is None:
col = self._theme_color
self.setBackgroundColor(col)
@pyqtSlot(QUrl, 'QWebEnginePage::Feature') @pyqtSlot(QUrl, 'QWebEnginePage::Feature')
def _on_feature_permission_requested(self, url, feature): def _on_feature_permission_requested(self, url, feature):

View File

@ -48,6 +48,13 @@ class DiskCache(QNetworkDiskCache):
maxsize=self.maximumCacheSize(), maxsize=self.maximumCacheSize(),
path=self.cacheDirectory()) path=self.cacheDirectory())
def _set_cache_size(self):
"""Set the cache size based on the config."""
size = config.get('storage', 'cache-size')
if size is None:
size = 1024 * 1024 * 50 # default from QNetworkDiskCachePrivate
self.setMaximumCacheSize(size)
def _maybe_activate(self): def _maybe_activate(self):
"""Activate/deactivate the cache based on the config.""" """Activate/deactivate the cache based on the config."""
if config.get('general', 'private-browsing'): if config.get('general', 'private-browsing'):
@ -55,13 +62,13 @@ class DiskCache(QNetworkDiskCache):
else: else:
self._activated = True self._activated = True
self.setCacheDirectory(os.path.join(self._cache_dir, 'http')) self.setCacheDirectory(os.path.join(self._cache_dir, 'http'))
self.setMaximumCacheSize(config.get('storage', 'cache-size')) self._set_cache_size()
@pyqtSlot(str, str) @pyqtSlot(str, str)
def on_config_changed(self, section, option): def on_config_changed(self, section, option):
"""Update cache size/activated if the config was changed.""" """Update cache size/activated if the config was changed."""
if (section, option) == ('storage', 'cache-size'): if (section, option) == ('storage', 'cache-size'):
self.setMaximumCacheSize(config.get('storage', 'cache-size')) self._set_cache_size()
elif (section, option) == ('general', # pragma: no branch elif (section, option) == ('general', # pragma: no branch
'private-browsing'): 'private-browsing'):
self._maybe_activate() self._maybe_activate()

View File

@ -32,6 +32,7 @@ import email.generator
import email.encoders import email.encoders
import email.mime.multipart import email.mime.multipart
import email.message import email.message
import quopri
from PyQt5.QtCore import QUrl from PyQt5.QtCore import QUrl
@ -138,6 +139,22 @@ def _check_rel(element):
return any(rel in rels for rel in must_have) return any(rel in rels for rel in must_have)
def _encode_quopri_mhtml(msg):
"""Encode the message's payload in quoted-printable.
Substitute for quopri's default 'encode_quopri' method, which needlessly
encodes all spaces and tabs, instead of only those at the end on the
line.
Args:
msg: Email message to quote.
"""
orig = msg.get_payload(decode=True)
encdata = quopri.encodestring(orig, quotetabs=False)
msg.set_payload(encdata)
msg['Content-Transfer-Encoding'] = 'quoted-printable'
MHTMLPolicy = email.policy.default.clone(linesep='\r\n', max_line_length=0) MHTMLPolicy = email.policy.default.clone(linesep='\r\n', max_line_length=0)
@ -146,7 +163,7 @@ E_BASE64 = email.encoders.encode_base64
# Encode the file using MIME quoted-printable encoding. # Encode the file using MIME quoted-printable encoding.
E_QUOPRI = email.encoders.encode_quopri E_QUOPRI = _encode_quopri_mhtml
class MHTMLWriter: class MHTMLWriter:
@ -225,7 +242,7 @@ class _Downloader:
Attributes: Attributes:
tab: The AbstractTab which contains the website that will be saved. tab: The AbstractTab which contains the website that will be saved.
dest: Destination filename. target: DownloadTarget where the file should be downloaded to.
writer: The MHTMLWriter object which is used to save the page. writer: The MHTMLWriter object which is used to save the page.
loaded_urls: A set of QUrls of finished asset downloads. loaded_urls: A set of QUrls of finished asset downloads.
pending_downloads: A set of unfinished (url, DownloadItem) tuples. pending_downloads: A set of unfinished (url, DownloadItem) tuples.
@ -235,9 +252,9 @@ class _Downloader:
_win_id: The window this downloader belongs to. _win_id: The window this downloader belongs to.
""" """
def __init__(self, tab, dest): def __init__(self, tab, target):
self.tab = tab self.tab = tab
self.dest = dest self.target = target
self.writer = None self.writer = None
self.loaded_urls = {tab.url()} self.loaded_urls = {tab.url()}
self.pending_downloads = set() self.pending_downloads = set()
@ -332,8 +349,8 @@ class _Downloader:
# Using the download manager to download host-blocked urls might crash # Using the download manager to download host-blocked urls might crash
# qute, see the comments/discussion on # qute, see the comments/discussion on
# https://github.com/The-Compiler/qutebrowser/pull/962#discussion_r40256987 # https://github.com/qutebrowser/qutebrowser/pull/962#discussion_r40256987
# and https://github.com/The-Compiler/qutebrowser/issues/1053 # and https://github.com/qutebrowser/qutebrowser/issues/1053
host_blocker = objreg.get('host-blocker') host_blocker = objreg.get('host-blocker')
if host_blocker.is_blocked(url): if host_blocker.is_blocked(url):
log.downloads.debug("Skipping {}, host-blocked".format(url)) log.downloads.debug("Skipping {}, host-blocked".format(url))
@ -445,14 +462,34 @@ class _Downloader:
return return
self._finished_file = True self._finished_file = True
log.downloads.debug("All assets downloaded, ready to finish off!") log.downloads.debug("All assets downloaded, ready to finish off!")
if isinstance(self.target, downloads.FileDownloadTarget):
fobj = open(self.target.filename, 'wb')
elif isinstance(self.target, downloads.FileObjDownloadTarget):
fobj = self.target.fileobj
elif isinstance(self.target, downloads.OpenFileDownloadTarget):
try: try:
with open(self.dest, 'wb') as file_output: fobj = downloads.temp_download_manager.get_tmpfile(
self.writer.write_to(file_output) self.tab.title() + '.mhtml')
except OSError as exc:
msg = "Download error: {}".format(exc)
message.error(msg)
return
else:
raise ValueError("Invalid DownloadTarget given: {!r}"
.format(self.target))
try:
with fobj:
self.writer.write_to(fobj)
except OSError as error: except OSError as error:
message.error("Could not save file: {}".format(error)) message.error("Could not save file: {}".format(error))
return return
log.downloads.debug("File successfully written.") log.downloads.debug("File successfully written.")
message.info("Page saved as {}".format(self.dest)) message.info("Page saved as {}".format(self.target))
if isinstance(self.target, downloads.OpenFileDownloadTarget):
utils.open_file(fobj.name, self.target.cmdline)
def _collect_zombies(self): def _collect_zombies(self):
"""Collect done downloads and add their data to the MHTML file. """Collect done downloads and add their data to the MHTML file.
@ -484,34 +521,37 @@ class _NoCloseBytesIO(io.BytesIO):
super().close() super().close()
def _start_download(dest, tab): def _start_download(target, tab):
"""Start downloading the current page and all assets to an MHTML file. """Start downloading the current page and all assets to an MHTML file.
This will overwrite dest if it already exists. This will overwrite dest if it already exists.
Args: Args:
dest: The filename where the resulting file should be saved. target: The DownloadTarget where the resulting file should be saved.
tab: Specify the tab whose page should be loaded. tab: Specify the tab whose page should be loaded.
""" """
loader = _Downloader(tab, dest) loader = _Downloader(tab, target)
loader.run() loader.run()
def start_download_checked(dest, tab): def start_download_checked(target, tab):
"""First check if dest is already a file, then start the download. """First check if dest is already a file, then start the download.
Args: Args:
dest: The filename where the resulting file should be saved. target: The DownloadTarget where the resulting file should be saved.
tab: Specify the tab whose page should be loaded. tab: Specify the tab whose page should be loaded.
""" """
# The default name is 'page title.mht' if not isinstance(target, downloads.FileDownloadTarget):
_start_download(target, tab)
return
# The default name is 'page title.mhtml'
title = tab.title() title = tab.title()
default_name = utils.sanitize_filename(title + '.mht') default_name = utils.sanitize_filename(title + '.mhtml')
# Remove characters which cannot be expressed in the file system encoding # Remove characters which cannot be expressed in the file system encoding
encoding = sys.getfilesystemencoding() encoding = sys.getfilesystemencoding()
default_name = utils.force_encoding(default_name, encoding) default_name = utils.force_encoding(default_name, encoding)
dest = utils.force_encoding(dest, encoding) dest = utils.force_encoding(target.filename, encoding)
dest = os.path.expanduser(dest) dest = os.path.expanduser(dest)
@ -532,8 +572,9 @@ def start_download_checked(dest, tab):
message.error("Directory {} does not exist.".format(folder)) message.error("Directory {} does not exist.".format(folder))
return return
target = downloads.FileDownloadTarget(path)
if not os.path.isfile(path): if not os.path.isfile(path):
_start_download(path, tab=tab) _start_download(target, tab=tab)
return return
q = usertypes.Question() q = usertypes.Question()
@ -543,5 +584,5 @@ def start_download_checked(dest, tab):
html.escape(path)) html.escape(path))
q.completed.connect(q.deleteLater) q.completed.connect(q.deleteLater)
q.answered_yes.connect(functools.partial( q.answered_yes.connect(functools.partial(
_start_download, path, tab=tab)) _start_download, target, tab=tab))
message.global_bridge.ask(q, blocking=False) message.global_bridge.ask(q, blocking=False)

View File

@ -211,7 +211,8 @@ class NetworkManager(QNetworkAccessManager):
request.deleteLater() request.deleteLater()
self.shutting_down.emit() self.shutting_down.emit()
@pyqtSlot('QNetworkReply*', 'QList<QSslError>') # No @pyqtSlot here, see
# https://github.com/qutebrowser/qutebrowser/issues/2213
def on_ssl_errors(self, reply, errors): # pragma: no mccabe def on_ssl_errors(self, reply, errors): # pragma: no mccabe
"""Decide if SSL errors should be ignored or not. """Decide if SSL errors should be ignored or not.
@ -396,6 +397,14 @@ class NetworkManager(QNetworkAccessManager):
Return: Return:
A QNetworkReply. A QNetworkReply.
""" """
proxy_factory = objreg.get('proxy-factory', None)
if proxy_factory is not None:
proxy_error = proxy_factory.get_error()
if proxy_error is not None:
return networkreply.ErrorNetworkReply(
req, proxy_error, QNetworkReply.UnknownProxyError,
self)
scheme = req.url().scheme() scheme = req.url().scheme()
if scheme in self._scheme_handlers: if scheme in self._scheme_handlers:
result = self._scheme_handlers[scheme].createRequest( result = self._scheme_handlers[scheme].createRequest(
@ -426,7 +435,7 @@ class NetworkManager(QNetworkAccessManager):
tab=self._tab_id) tab=self._tab_id)
current_url = tab.url() current_url = tab.url()
except (KeyError, RuntimeError, TypeError): except (KeyError, RuntimeError, TypeError):
# https://github.com/The-Compiler/qutebrowser/issues/889 # https://github.com/qutebrowser/qutebrowser/issues/889
# Catching RuntimeError and TypeError because we could be in # Catching RuntimeError and TypeError because we could be in
# the middle of the webpage shutdown here. # the middle of the webpage shutdown here.
current_url = QUrl() current_url = QUrl()

View File

@ -74,7 +74,7 @@ class JSBridge(QObject):
@pyqtSlot(str, str, str) @pyqtSlot(str, str, str)
def set(self, sectname, optname, value): def set(self, sectname, optname, value):
"""Slot to set a setting from qute:settings.""" """Slot to set a setting from qute:settings."""
# https://github.com/The-Compiler/qutebrowser/issues/727 # https://github.com/qutebrowser/qutebrowser/issues/727
if ((sectname, optname) == ('content', 'allow-javascript') and if ((sectname, optname) == ('content', 'allow-javascript') and
value == 'false'): value == 'false'):
message.error("Refusing to disable javascript via qute:settings " message.error("Refusing to disable javascript via qute:settings "

View File

@ -117,7 +117,7 @@ class Language(str):
"""A language-tag (RFC 5646, Section 2.1). """A language-tag (RFC 5646, Section 2.1).
FIXME: This grammar is not 100% correct yet. FIXME: This grammar is not 100% correct yet.
https://github.com/The-Compiler/qutebrowser/issues/105 https://github.com/qutebrowser/qutebrowser/issues/105
""" """
grammar = re.compile('[A-Za-z0-9-]+') grammar = re.compile('[A-Za-z0-9-]+')
@ -132,7 +132,7 @@ class ValueChars(str):
"""A value of an attribute. """A value of an attribute.
FIXME: Can we merge this with Value? FIXME: Can we merge this with Value?
https://github.com/The-Compiler/qutebrowser/issues/105 https://github.com/qutebrowser/qutebrowser/issues/105
""" """
grammar = re.compile('({}|{})*'.format(attr_char_re, hex_digit_re)) grammar = re.compile('({}|{})*'.format(attr_char_re, hex_digit_re))

View File

@ -21,21 +21,65 @@
from PyQt5.QtCore import QByteArray, QDataStream, QIODevice, QUrl from PyQt5.QtCore import QByteArray, QDataStream, QIODevice, QUrl
from PyQt5.QtWebKit import qWebKitVersion
from qutebrowser.utils import qtutils from qutebrowser.utils import qtutils
HISTORY_STREAM_VERSION = 2
BACK_FORWARD_TREE_VERSION = 2
def _encode_url(url): def _encode_url(url):
"""Encode a QUrl suitable to pass to QWebHistory.""" """Encode a QUrl suitable to pass to QWebHistory."""
data = bytes(QUrl.toPercentEncoding(url.toString(), b':/#?&+=@%*')) data = bytes(QUrl.toPercentEncoding(url.toString(), b':/#?&+=@%*'))
return data.decode('ascii') return data.decode('ascii')
def _serialize_item(i, item, stream): def _serialize_ng(items, current_idx, stream):
# {'currentItemIndex': 0,
# 'history': [{'children': [],
# 'documentSequenceNumber': 1485030525573123,
# 'documentState': [],
# 'formContentType': '',
# 'itemSequenceNumber': 1485030525573122,
# 'originalURLString': 'about:blank',
# 'pageScaleFactor': 0.0,
# 'referrer': '',
# 'scrollPosition': {'x': 0, 'y': 0},
# 'target': '',
# 'title': '',
# 'urlString': 'about:blank'}]}
data = {'currentItemIndex': current_idx, 'history': []}
for item in items:
data['history'].append(_serialize_item_ng(item))
stream.writeInt(3) # history stream version
stream.writeQVariantMap(data)
def _serialize_item_ng(item):
data = {
'originalURLString': item.original_url.toString(QUrl.FullyEncoded),
'scrollPosition': {'x': 0, 'y': 0},
'title': item.title,
'urlString': item.url.toString(QUrl.FullyEncoded),
}
try:
data['scrollPosition']['x'] = item.user_data['scroll-pos'].x()
data['scrollPosition']['y'] = item.user_data['scroll-pos'].y()
except (KeyError, TypeError):
pass
return data
def _serialize_old(items, current_idx, stream):
### Source/WebKit/qt/Api/qwebhistory.cpp operator<<
stream.writeInt(2) # history stream version
stream.writeInt(len(items))
stream.writeInt(current_idx)
for i, item in enumerate(items):
_serialize_item_old(i, item, stream)
def _serialize_item_old(i, item, stream):
"""Serialize a single WebHistoryItem into a QDataStream. """Serialize a single WebHistoryItem into a QDataStream.
Args: Args:
@ -53,7 +97,7 @@ def _serialize_item(i, item, stream):
### Source/WebCore/history/HistoryItem.cpp decodeBackForwardTree ### Source/WebCore/history/HistoryItem.cpp decodeBackForwardTree
## backForwardTreeEncodingVersion ## backForwardTreeEncodingVersion
stream.writeUInt32(BACK_FORWARD_TREE_VERSION) stream.writeUInt32(2)
## size (recursion stack) ## size (recursion stack)
stream.writeUInt64(0) stream.writeUInt64(0)
## node->m_documentSequenceNumber ## node->m_documentSequenceNumber
@ -137,14 +181,12 @@ def serialize(items):
else: else:
current_idx = 0 current_idx = 0
### Source/WebKit/qt/Api/qwebhistory.cpp operator<< if qtutils.is_qtwebkit_ng(qWebKitVersion()):
stream.writeInt(HISTORY_STREAM_VERSION) _serialize_ng(items, current_idx, stream)
stream.writeInt(len(items)) else:
stream.writeInt(current_idx) _serialize_old(items, current_idx, stream)
for i, item in enumerate(items): user_data += [item.user_data for item in items]
_serialize_item(i, item, stream)
user_data.append(item.user_data)
stream.device().reset() stream.device().reset()
qtutils.check_qdatastream(stream) qtutils.check_qdatastream(stream)

View File

@ -20,7 +20,7 @@
"""QtWebKit specific part of the web element API.""" """QtWebKit specific part of the web element API."""
from PyQt5.QtCore import QRect from PyQt5.QtCore import QRect
from PyQt5.QtWebKit import QWebElement from PyQt5.QtWebKit import QWebElement, QWebSettings
from qutebrowser.config import config from qutebrowser.config import config
from qutebrowser.utils import log, utils, javascript from qutebrowser.utils import log, utils, javascript
@ -96,16 +96,6 @@ class WebKitElement(webelem.AbstractWebElement):
self._check_vanished() self._check_vanished()
return self._elem.geometry() return self._elem.geometry()
def style_property(self, name, *, strategy):
self._check_vanished()
strategies = {
# FIXME:qtwebengine which ones do we actually need?
'inline': QWebElement.InlineStyle,
'computed': QWebElement.ComputedStyle,
}
qt_strategy = strategies[strategy]
return self._elem.styleProperty(name, qt_strategy)
def classes(self): def classes(self):
self._check_vanished() self._check_vanished()
return self._elem.classes() return self._elem.classes()
@ -162,7 +152,7 @@ class WebKitElement(webelem.AbstractWebElement):
# On e.g. Void Linux with musl libc, the stack size is too small # On e.g. Void Linux with musl libc, the stack size is too small
# for jsc, and running JS will fail. If that happens, fall back to # for jsc, and running JS will fail. If that happens, fall back to
# the Python implementation. # the Python implementation.
# https://github.com/The-Compiler/qutebrowser/issues/1641 # https://github.com/qutebrowser/qutebrowser/issues/1641
return None return None
text = utils.compact_text(self._elem.toOuterXml(), 500) text = utils.compact_text(self._elem.toOuterXml(), 500)
@ -216,7 +206,7 @@ class WebKitElement(webelem.AbstractWebElement):
Skipping of small rectangles is due to <a> elements containing other Skipping of small rectangles is due to <a> elements containing other
elements with "display:block" style, see elements with "display:block" style, see
https://github.com/The-Compiler/qutebrowser/issues/1298 https://github.com/qutebrowser/qutebrowser/issues/1298
Args: Args:
elem_geometry: The geometry of the element, or None. elem_geometry: The geometry of the element, or None.
@ -248,10 +238,13 @@ class WebKitElement(webelem.AbstractWebElement):
hidden_attributes = { hidden_attributes = {
'visibility': 'hidden', 'visibility': 'hidden',
'display': 'none', 'display': 'none',
'opacity': '0',
} }
for k, v in hidden_attributes.items(): for k, v in hidden_attributes.items():
if self._elem.styleProperty(k, QWebElement.ComputedStyle) == v: if (self._elem.styleProperty(k, QWebElement.ComputedStyle) == v and
'ace_text-input' not in self.classes()):
return False return False
elem_geometry = self._elem.geometry() elem_geometry = self._elem.geometry()
if not elem_geometry.isValid() and elem_geometry.x() == 0: if not elem_geometry.isValid() and elem_geometry.x() == 0:
# Most likely an invisible link # Most likely an invisible link
@ -297,6 +290,36 @@ class WebKitElement(webelem.AbstractWebElement):
break break
elem = elem._parent() # pylint: disable=protected-access elem = elem._parent() # pylint: disable=protected-access
def _move_text_cursor(self):
if self is None:
# old PyQt versions call the slot after the element is deleted.
return
if self.is_text_input() and self.is_editable():
self._tab.caret.move_to_end_of_document()
def _click_editable(self, click_target):
ok = self._elem.evaluateJavaScript('this.focus(); true;')
if ok:
self._move_text_cursor()
else:
log.webelem.debug("Failed to focus via JS, falling back to event")
self._click_fake_event(click_target)
def _click_js(self, click_target):
settings = QWebSettings.globalSettings()
attribute = QWebSettings.JavascriptCanOpenWindows
could_open_windows = settings.testAttribute(attribute)
settings.setAttribute(attribute, True)
ok = self._elem.evaluateJavaScript('this.click(); true;')
settings.setAttribute(attribute, could_open_windows)
if not ok:
log.webelem.debug("Failed to click via JS, falling back to event")
self._click_fake_event(click_target)
def _click_fake_event(self, click_target):
self._tab.data.override_target = click_target
super()._click_fake_event(click_target)
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

@ -23,6 +23,7 @@
from PyQt5.QtWebKitWidgets import QWebInspector from PyQt5.QtWebKitWidgets import QWebInspector
from qutebrowser.browser import inspector from qutebrowser.browser import inspector
from qutebrowser.config import config
class WebKitInspector(inspector.AbstractWebInspector): class WebKitInspector(inspector.AbstractWebInspector):
@ -35,6 +36,9 @@ class WebKitInspector(inspector.AbstractWebInspector):
self._set_widget(qwebinspector) self._set_widget(qwebinspector)
def inspect(self, page): def inspect(self, page):
self._check_developer_extras() if not config.get('general', 'developer-extras'):
raise inspector.WebInspectorError(
"Please enable developer-extras before using the "
"webinspector!")
self._widget.setPage(page) self._widget.setPage(page)
self.show() self.show()

View File

@ -26,10 +26,10 @@ Module attributes:
import os.path import os.path
from PyQt5.QtWebKit import QWebSettings from PyQt5.QtWebKit import QWebSettings, qWebKitVersion
from qutebrowser.config import config, websettings from qutebrowser.config import config, websettings
from qutebrowser.utils import standarddir, objreg, urlutils from qutebrowser.utils import standarddir, objreg, urlutils, qtutils, message
from qutebrowser.browser import shared from qutebrowser.browser import shared
@ -88,28 +88,32 @@ def _set_user_stylesheet():
QWebSettings.globalSettings().setUserStyleSheetUrl(url) QWebSettings.globalSettings().setUserStyleSheetUrl(url)
def _init_private_browsing():
if config.get('general', 'private-browsing'):
if qtutils.is_qtwebkit_ng(qWebKitVersion()):
message.warning("Private browsing is not fully implemented by "
"QtWebKit-NG!")
QWebSettings.setIconDatabasePath('')
else:
QWebSettings.setIconDatabasePath(standarddir.cache())
def update_settings(section, option): def update_settings(section, option):
"""Update global settings when qwebsettings changed.""" """Update global settings when qwebsettings changed."""
if (section, option) == ('general', 'private-browsing'): if (section, option) == ('general', 'private-browsing'):
cache_path = standarddir.cache() _init_private_browsing()
if config.get('general', 'private-browsing') or cache_path is None:
QWebSettings.setIconDatabasePath('')
else:
QWebSettings.setIconDatabasePath(cache_path)
elif section == 'ui' and option in ['hide-scrollbar', 'user-stylesheet']: elif section == 'ui' and option in ['hide-scrollbar', 'user-stylesheet']:
_set_user_stylesheet() _set_user_stylesheet()
websettings.update_mappings(MAPPINGS, section, option) websettings.update_mappings(MAPPINGS, section, option)
def init(): def init(_args):
"""Initialize the global QWebSettings.""" """Initialize the global QWebSettings."""
cache_path = standarddir.cache() cache_path = standarddir.cache()
data_path = standarddir.data() data_path = standarddir.data()
if config.get('general', 'private-browsing'):
QWebSettings.setIconDatabasePath('') _init_private_browsing()
else:
QWebSettings.setIconDatabasePath(cache_path)
QWebSettings.setOfflineWebApplicationCachePath( QWebSettings.setOfflineWebApplicationCachePath(
os.path.join(cache_path, 'application-cache')) os.path.join(cache_path, 'application-cache'))

View File

@ -32,8 +32,9 @@ 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.network import proxy
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.browser.webkit.network import webkitqutescheme
from qutebrowser.utils import qtutils, objreg, usertypes, utils, log from qutebrowser.utils import qtutils, objreg, usertypes, utils, log
@ -41,6 +42,8 @@ def init():
"""Initialize QtWebKit-specific modules.""" """Initialize QtWebKit-specific modules."""
qapp = QApplication.instance() qapp = QApplication.instance()
if not qtutils.version_check('5.8'):
# Otherwise we initialize it globally in app.py
log.init.debug("Initializing proxy...") log.init.debug("Initializing proxy...")
proxy.init() proxy.init()
@ -49,6 +52,18 @@ def init():
objreg.register('js-bridge', js_bridge) objreg.register('js-bridge', js_bridge)
class WebKitAction(browsertab.AbstractAction):
"""QtWebKit implementations related to web actions."""
def exit_fullscreen(self):
raise browsertab.UnsupportedOperationError
def save_page(self):
"""Save the current page."""
raise browsertab.UnsupportedOperationError
class WebKitPrinting(browsertab.AbstractPrinting): class WebKitPrinting(browsertab.AbstractPrinting):
"""QtWebKit implementations related to printing.""" """QtWebKit implementations related to printing."""
@ -65,13 +80,19 @@ class WebKitPrinting(browsertab.AbstractPrinting):
def check_printer_support(self): def check_printer_support(self):
self._do_check() self._do_check()
def check_preview_support(self):
self._do_check()
def to_pdf(self, filename): def to_pdf(self, filename):
printer = QPrinter() printer = QPrinter()
printer.setOutputFileName(filename) printer.setOutputFileName(filename)
self.to_printer(printer) self.to_printer(printer)
def to_printer(self, printer): def to_printer(self, printer, callback=None):
self._widget.print(printer) self._widget.print(printer)
# Can't find out whether there was an error...
if callback is not None:
callback(True)
class WebKitSearch(browsertab.AbstractSearch): class WebKitSearch(browsertab.AbstractSearch):
@ -422,7 +443,7 @@ class WebKitScroller(browsertab.AbstractScroller):
# FIXME:qtwebengine needed? # FIXME:qtwebengine needed?
# self._widget.setFocus() # self._widget.setFocus()
for _ in range(count): for _ in range(min(count, 5000)):
press_evt = QKeyEvent(QEvent.KeyPress, key, Qt.NoModifier, 0, 0, 0) press_evt = QKeyEvent(QEvent.KeyPress, key, Qt.NoModifier, 0, 0, 0)
release_evt = QKeyEvent(QEvent.KeyRelease, key, Qt.NoModifier, release_evt = QKeyEvent(QEvent.KeyRelease, key, Qt.NoModifier,
0, 0, 0) 0, 0, 0)
@ -589,8 +610,6 @@ class WebKitTab(browsertab.AbstractTab):
"""A QtWebKit tab in the browser.""" """A QtWebKit tab in the browser."""
WIDGET_CLASS = webview.WebView
def __init__(self, win_id, mode_manager, parent=None): def __init__(self, win_id, mode_manager, parent=None):
super().__init__(win_id=win_id, mode_manager=mode_manager, super().__init__(win_id=win_id, mode_manager=mode_manager,
parent=parent) parent=parent)
@ -603,9 +622,9 @@ class WebKitTab(browsertab.AbstractTab):
self.search = WebKitSearch(parent=self) self.search = WebKitSearch(parent=self)
self.printing = WebKitPrinting() self.printing = WebKitPrinting()
self.elements = WebKitElements(self) self.elements = WebKitElements(self)
self.action = WebKitAction()
self._set_widget(widget) self._set_widget(widget)
self._connect_signals() self._connect_signals()
self.zoom.set_default()
self.backend = usertypes.Backend.QtWebKit self.backend = usertypes.Backend.QtWebKit
def _install_event_filter(self): def _install_event_filter(self):
@ -671,13 +690,17 @@ class WebKitTab(browsertab.AbstractTab):
def networkaccessmanager(self): def networkaccessmanager(self):
return self._widget.page().networkAccessManager() return self._widget.page().networkAccessManager()
def user_agent(self):
page = self._widget.page()
return page.userAgentForUrl(self.url())
@pyqtSlot() @pyqtSlot()
def _on_frame_load_finished(self): def _on_frame_load_finished(self):
"""Make sure we emit an appropriate status when loading finished. """Make sure we emit an appropriate status when loading finished.
While Qt has a bool "ok" attribute for loadFinished, it always is True While Qt has a bool "ok" attribute for loadFinished, it always is True
when using error pages... See when using error pages... See
https://github.com/The-Compiler/qutebrowser/issues/84 https://github.com/qutebrowser/qutebrowser/issues/84
""" """
self._on_load_finished(not self._widget.page().error_occurred) self._on_load_finished(not self._widget.page().error_occurred)
@ -690,8 +713,8 @@ class WebKitTab(browsertab.AbstractTab):
def _on_frame_created(self, frame): def _on_frame_created(self, frame):
"""Connect the contentsSizeChanged signal of each frame.""" """Connect the contentsSizeChanged signal of each frame."""
# FIXME:qtwebengine those could theoretically regress: # FIXME:qtwebengine those could theoretically regress:
# https://github.com/The-Compiler/qutebrowser/issues/152 # https://github.com/qutebrowser/qutebrowser/issues/152
# https://github.com/The-Compiler/qutebrowser/issues/263 # https://github.com/qutebrowser/qutebrowser/issues/263
frame.contentsSizeChanged.connect(self._on_contents_size_changed) frame.contentsSizeChanged.connect(self._on_contents_size_changed)
@pyqtSlot(QSize) @pyqtSlot(QSize)
@ -717,5 +740,5 @@ class WebKitTab(browsertab.AbstractTab):
frame.contentsSizeChanged.connect(self._on_contents_size_changed) frame.contentsSizeChanged.connect(self._on_contents_size_changed)
frame.initialLayoutCompleted.connect(self._on_history_trigger) frame.initialLayoutCompleted.connect(self._on_history_trigger)
def _event_target(self): def event_target(self):
return self._widget return self._widget

View File

@ -59,7 +59,7 @@ class WebView(QWebView):
super().__init__(parent) super().__init__(parent)
if sys.platform == 'darwin' and qtutils.version_check('5.4'): if sys.platform == 'darwin' and qtutils.version_check('5.4'):
# WORKAROUND for https://bugreports.qt.io/browse/QTBUG-42948 # WORKAROUND for https://bugreports.qt.io/browse/QTBUG-42948
# See https://github.com/The-Compiler/qutebrowser/issues/462 # See https://github.com/qutebrowser/qutebrowser/issues/462
self.setStyle(QStyleFactory.create('Fusion')) self.setStyle(QStyleFactory.create('Fusion'))
# FIXME:qtwebengine this is only used to set the zoom factor from # FIXME:qtwebengine this is only used to set the zoom factor from
# the QWebPage - we should get rid of it somehow (signals?) # the QWebPage - we should get rid of it somehow (signals?)
@ -108,11 +108,7 @@ class WebView(QWebView):
@config.change_filter('colors', 'webpage.bg') @config.change_filter('colors', 'webpage.bg')
def _set_bg_color(self): def _set_bg_color(self):
"""Set the webpage background color as configured. """Set the webpage background color as configured."""
FIXME:qtwebengine
For QtWebEngine, doing the same has no effect, so we do it in here.
"""
col = config.get('colors', 'webpage.bg') col = config.get('colors', 'webpage.bg')
palette = self.palette() palette = self.palette()
if col is None: if col is None:

View File

@ -27,6 +27,7 @@ from qutebrowser.commands import cmdexc, argparser
from qutebrowser.utils import (log, utils, message, docutils, objreg, from qutebrowser.utils import (log, utils, message, docutils, objreg,
usertypes, typing) usertypes, typing)
from qutebrowser.utils import debug as debug_utils from qutebrowser.utils import debug as debug_utils
from qutebrowser.misc import objects
class ArgInfo: class ArgInfo:
@ -34,14 +35,11 @@ class ArgInfo:
"""Information about an argument.""" """Information about an argument."""
def __init__(self, win_id=False, count=False, hide=False, metavar=None, def __init__(self, win_id=False, count=False, hide=False, metavar=None,
zero_count=False, flag=None, completion=None, choices=None): flag=None, completion=None, choices=None):
if win_id and count: if win_id and count:
raise TypeError("Argument marked as both count/win_id!") raise TypeError("Argument marked as both count/win_id!")
if zero_count and not count:
raise TypeError("zero_count argument cannot exist without count!")
self.win_id = win_id self.win_id = win_id
self.count = count self.count = count
self.zero_count = zero_count
self.flag = flag self.flag = flag
self.hide = hide self.hide = hide
self.metavar = metavar self.metavar = metavar
@ -51,7 +49,6 @@ class ArgInfo:
def __eq__(self, other): def __eq__(self, other):
return (self.win_id == other.win_id and return (self.win_id == other.win_id and
self.count == other.count and self.count == other.count and
self.zero_count == other.zero_count and
self.flag == other.flag and self.flag == other.flag and
self.hide == other.hide and self.hide == other.hide and
self.metavar == other.metavar and self.metavar == other.metavar and
@ -61,7 +58,6 @@ class ArgInfo:
def __repr__(self): def __repr__(self):
return utils.get_repr(self, win_id=self.win_id, count=self.count, return utils.get_repr(self, win_id=self.win_id, count=self.count,
flag=self.flag, hide=self.hide, flag=self.flag, hide=self.hide,
zero_count=self.zero_count,
metavar=self.metavar, completion=self.completion, metavar=self.metavar, completion=self.completion,
choices=self.choices, constructor=True) choices=self.choices, constructor=True)
@ -142,7 +138,6 @@ class Command:
self.opt_args = collections.OrderedDict() self.opt_args = collections.OrderedDict()
self.namespace = None self.namespace = None
self._count = None self._count = None
self._zero_count = None
self.pos_args = [] self.pos_args = []
self.desc = None self.desc = None
self.flags_with_args = [] self.flags_with_args = []
@ -154,7 +149,7 @@ class Command:
self._inspect_func() self._inspect_func()
def _check_prerequisites(self, win_id, count): def _check_prerequisites(self, win_id):
"""Check if the command is permitted to run currently. """Check if the command is permitted to run currently.
Args: Args:
@ -164,17 +159,11 @@ class Command:
window=win_id) window=win_id)
self.validate_mode(mode_manager.mode) self.validate_mode(mode_manager.mode)
used_backend = usertypes.arg2backend[objreg.get('args').backend] if self.backend is not None and objects.backend != self.backend:
if self.backend is not None and used_backend != self.backend:
raise cmdexc.PrerequisitesError( raise cmdexc.PrerequisitesError(
"{}: Only available with {} " "{}: Only available with {} "
"backend.".format(self.name, self.backend.name)) "backend.".format(self.name, self.backend.name))
if count == 0 and not self._zero_count:
raise cmdexc.PrerequisitesError(
"{}: A zero count is not allowed for this command!"
.format(self.name))
if self.deprecated: if self.deprecated:
message.warning('{} is deprecated - {}'.format(self.name, message.warning('{} is deprecated - {}'.format(self.name,
self.deprecated)) self.deprecated))
@ -246,9 +235,6 @@ class Command:
assert param.kind != inspect.Parameter.POSITIONAL_ONLY assert param.kind != inspect.Parameter.POSITIONAL_ONLY
if param.name == 'self': if param.name == 'self':
continue continue
arg_info = self.get_arg_info(param)
if arg_info.count:
self._zero_count = arg_info.zero_count
if self._inspect_special_param(param): if self._inspect_special_param(param):
continue continue
if (param.kind == inspect.Parameter.KEYWORD_ONLY and if (param.kind == inspect.Parameter.KEYWORD_ONLY and
@ -532,7 +518,7 @@ class Command:
e.status, e)) e.status, e))
return return
self._count = count self._count = count
self._check_prerequisites(win_id, count) self._check_prerequisites(win_id)
posargs, kwargs = self._get_call_args(win_id) posargs, kwargs = self._get_call_args(win_id)
log.commands.debug('Calling {}'.format( log.commands.debug('Calling {}'.format(
debug_utils.format_call(self.handler, posargs, kwargs))) debug_utils.format_call(self.handler, posargs, kwargs)))

View File

@ -65,9 +65,13 @@ class _QtFIFOReader(QObject):
"""(Try to) read a line from the FIFO.""" """(Try to) read a line from the FIFO."""
log.procs.debug("QSocketNotifier triggered!") log.procs.debug("QSocketNotifier triggered!")
self._notifier.setEnabled(False) self._notifier.setEnabled(False)
try:
for line in self._fifo: for line in self._fifo:
self.got_line.emit(line.rstrip('\r\n')) self.got_line.emit(line.rstrip('\r\n'))
self._notifier.setEnabled(True) self._notifier.setEnabled(True)
except UnicodeDecodeError as e:
log.misc.error("Invalid unicode in userscript output: {}"
.format(e))
def cleanup(self): def cleanup(self):
"""Clean up so the FIFO can be closed.""" """Clean up so the FIFO can be closed."""
@ -289,6 +293,9 @@ class _WindowsUserscriptRunner(_BaseUserscriptRunner):
self.got_cmd.emit(line.rstrip()) self.got_cmd.emit(line.rstrip())
except OSError: except OSError:
log.procs.exception("Failed to read command file!") log.procs.exception("Failed to read command file!")
except UnicodeDecodeError as e:
log.misc.error("Invalid unicode in userscript output: {}"
.format(e))
super()._cleanup() super()._cleanup()
self.finished.emit() self.finished.emit()
@ -412,6 +419,8 @@ def run_async(tab, cmd, *args, win_id, env, verbose=False):
env['QUTE_CONFIG_DIR'] = standarddir.config() env['QUTE_CONFIG_DIR'] = standarddir.config()
env['QUTE_DATA_DIR'] = standarddir.data() env['QUTE_DATA_DIR'] = standarddir.data()
env['QUTE_DOWNLOAD_DIR'] = downloads.download_dir() env['QUTE_DOWNLOAD_DIR'] = downloads.download_dir()
env['QUTE_COMMANDLINE_TEXT'] = objreg.get('status-command', scope='window',
window=win_id).text()
cmd_path = os.path.expanduser(cmd) cmd_path = os.path.expanduser(cmd)

View File

@ -239,7 +239,7 @@ class Completer(QObject):
# This is a search or gibberish, so we don't need to complete # This is a search or gibberish, so we don't need to complete
# anything (yet) # anything (yet)
# FIXME complete searches # FIXME complete searches
# https://github.com/The-Compiler/qutebrowser/issues/32 # https://github.com/qutebrowser/qutebrowser/issues/32
completion.set_model(None) completion.set_model(None)
return return

View File

@ -54,7 +54,7 @@ class CompletionItemDelegate(QStyledItemDelegate):
# FIXME this is horribly slow when resizing. # FIXME this is horribly slow when resizing.
# We should probably cache something in _get_textdoc or so, but as soon as # We should probably cache something in _get_textdoc or so, but as soon as
# we implement eliding that cache probably isn't worth much anymore... # we implement eliding that cache probably isn't worth much anymore...
# https://github.com/The-Compiler/qutebrowser/issues/121 # https://github.com/qutebrowser/qutebrowser/issues/121
def __init__(self, parent=None): def __init__(self, parent=None):
self._painter = None self._painter = None
@ -173,7 +173,7 @@ class CompletionItemDelegate(QStyledItemDelegate):
""" """
# FIXME we probably should do eliding here. See # FIXME we probably should do eliding here. See
# qcommonstyle.cpp:viewItemDrawText # qcommonstyle.cpp:viewItemDrawText
# https://github.com/The-Compiler/qutebrowser/issues/118 # https://github.com/qutebrowser/qutebrowser/issues/118
text_option = QTextOption() text_option = QTextOption()
if self._opt.features & QStyleOptionViewItem.WrapText: if self._opt.features & QStyleOptionViewItem.WrapText:
text_option.setWrapMode(QTextOption.WordWrap) text_option.setWrapMode(QTextOption.WordWrap)

View File

@ -134,7 +134,7 @@ class CompletionView(QTreeView):
self.setUniformRowHeights(True) self.setUniformRowHeights(True)
self.hide() self.hide()
# FIXME set elidemode # FIXME set elidemode
# https://github.com/The-Compiler/qutebrowser/issues/118 # https://github.com/qutebrowser/qutebrowser/issues/118
def __repr__(self): def __repr__(self):
return utils.get_repr(self) return utils.get_repr(self)
@ -150,10 +150,15 @@ class CompletionView(QTreeView):
"""Resize the completion columns based on column_widths.""" """Resize the completion columns based on column_widths."""
width = self.size().width() width = self.size().width()
pixel_widths = [(width * perc // 100) for perc in self._column_widths] pixel_widths = [(width * perc // 100) for perc in self._column_widths]
if self.verticalScrollBar().isVisible(): if self.verticalScrollBar().isVisible():
pixel_widths[-1] -= self.style().pixelMetric( delta = self.style().pixelMetric(QStyle.PM_ScrollBarExtent) + 5
QStyle.PM_ScrollBarExtent) + 5 if pixel_widths[-1] > delta:
pixel_widths[-1] -= delta
else:
pixel_widths[-2] -= delta
for i, w in enumerate(pixel_widths): for i, w in enumerate(pixel_widths):
assert w >= 0, i
self.setColumnWidth(i, w) self.setColumnWidth(i, w)
def _next_idx(self, upwards): def _next_idx(self, upwards):

View File

@ -30,7 +30,7 @@ class SettingSectionCompletionModel(base.BaseCompletionModel):
"""A CompletionModel filled with settings sections.""" """A CompletionModel filled with settings sections."""
# https://github.com/The-Compiler/qutebrowser/issues/545 # https://github.com/qutebrowser/qutebrowser/issues/545
# pylint: disable=abstract-method # pylint: disable=abstract-method
COLUMN_WIDTHS = (20, 70, 10) COLUMN_WIDTHS = (20, 70, 10)
@ -52,7 +52,7 @@ class SettingOptionCompletionModel(base.BaseCompletionModel):
_section: The config section this model shows. _section: The config section this model shows.
""" """
# https://github.com/The-Compiler/qutebrowser/issues/545 # https://github.com/qutebrowser/qutebrowser/issues/545
# pylint: disable=abstract-method # pylint: disable=abstract-method
COLUMN_WIDTHS = (20, 70, 10) COLUMN_WIDTHS = (20, 70, 10)
@ -108,7 +108,7 @@ class SettingValueCompletionModel(base.BaseCompletionModel):
_option: The config option this model shows. _option: The config option this model shows.
""" """
# https://github.com/The-Compiler/qutebrowser/issues/545 # https://github.com/qutebrowser/qutebrowser/issues/545
# pylint: disable=abstract-method # pylint: disable=abstract-method
COLUMN_WIDTHS = (20, 70, 10) COLUMN_WIDTHS = (20, 70, 10)

View File

@ -32,7 +32,7 @@ class CommandCompletionModel(base.BaseCompletionModel):
"""A CompletionModel filled with non-hidden commands and descriptions.""" """A CompletionModel filled with non-hidden commands and descriptions."""
# https://github.com/The-Compiler/qutebrowser/issues/545 # https://github.com/qutebrowser/qutebrowser/issues/545
# pylint: disable=abstract-method # pylint: disable=abstract-method
COLUMN_WIDTHS = (20, 60, 20) COLUMN_WIDTHS = (20, 60, 20)
@ -50,9 +50,11 @@ class HelpCompletionModel(base.BaseCompletionModel):
"""A CompletionModel filled with help topics.""" """A CompletionModel filled with help topics."""
# https://github.com/The-Compiler/qutebrowser/issues/545 # https://github.com/qutebrowser/qutebrowser/issues/545
# pylint: disable=abstract-method # pylint: disable=abstract-method
COLUMN_WIDTHS = (20, 60, 20)
def __init__(self, parent=None): def __init__(self, parent=None):
super().__init__(parent) super().__init__(parent)
self._init_commands() self._init_commands()
@ -87,7 +89,7 @@ class QuickmarkCompletionModel(base.BaseCompletionModel):
"""A CompletionModel filled with all quickmarks.""" """A CompletionModel filled with all quickmarks."""
# https://github.com/The-Compiler/qutebrowser/issues/545 # https://github.com/qutebrowser/qutebrowser/issues/545
# pylint: disable=abstract-method # pylint: disable=abstract-method
def __init__(self, parent=None): def __init__(self, parent=None):
@ -102,7 +104,7 @@ class BookmarkCompletionModel(base.BaseCompletionModel):
"""A CompletionModel filled with all bookmarks.""" """A CompletionModel filled with all bookmarks."""
# https://github.com/The-Compiler/qutebrowser/issues/545 # https://github.com/qutebrowser/qutebrowser/issues/545
# pylint: disable=abstract-method # pylint: disable=abstract-method
def __init__(self, parent=None): def __init__(self, parent=None):
@ -117,7 +119,7 @@ class SessionCompletionModel(base.BaseCompletionModel):
"""A CompletionModel filled with session names.""" """A CompletionModel filled with session names."""
# https://github.com/The-Compiler/qutebrowser/issues/545 # https://github.com/qutebrowser/qutebrowser/issues/545
# pylint: disable=abstract-method # pylint: disable=abstract-method
def __init__(self, parent=None): def __init__(self, parent=None):
@ -160,6 +162,7 @@ class TabCompletionModel(base.BaseCompletionModel):
tab.title_changed.connect(self.rebuild) tab.title_changed.connect(self.rebuild)
tab.shutting_down.connect(self.delayed_rebuild) tab.shutting_down.connect(self.delayed_rebuild)
tabbed_browser.new_tab.connect(self.on_new_tab) tabbed_browser.new_tab.connect(self.on_new_tab)
tabbed_browser.tabBar().tabMoved.connect(self.rebuild)
objreg.get("app").new_window.connect(self.on_new_window) objreg.get("app").new_window.connect(self.on_new_window)
self.rebuild() self.rebuild()
@ -248,7 +251,7 @@ class BindCompletionModel(base.BaseCompletionModel):
"""A CompletionModel filled with all bindable commands and descriptions.""" """A CompletionModel filled with all bindable commands and descriptions."""
# https://github.com/The-Compiler/qutebrowser/issues/545 # https://github.com/qutebrowser/qutebrowser/issues/545
# pylint: disable=abstract-method # pylint: disable=abstract-method
COLUMN_WIDTHS = (20, 60, 20) COLUMN_WIDTHS = (20, 60, 20)

View File

@ -43,6 +43,7 @@ from qutebrowser.config.parsers import ini
from qutebrowser.commands import cmdexc, cmdutils from qutebrowser.commands import cmdexc, cmdutils
from qutebrowser.utils import (message, objreg, utils, standarddir, log, from qutebrowser.utils import (message, objreg, utils, standarddir, log,
qtutils, error, usertypes) qtutils, error, usertypes)
from qutebrowser.misc import objects
from qutebrowser.utils.usertypes import Completion from qutebrowser.utils.usertypes import Completion
@ -233,7 +234,7 @@ def _init_misc():
# doesn't overwrite our config. # doesn't overwrite our config.
# #
# This fixes one of the corruption issues here: # This fixes one of the corruption issues here:
# https://github.com/The-Compiler/qutebrowser/issues/515 # https://github.com/qutebrowser/qutebrowser/issues/515
path = os.path.join(standarddir.config(), 'qsettings') path = os.path.join(standarddir.config(), 'qsettings')
for fmt in [QSettings.NativeFormat, QSettings.IniFormat]: for fmt in [QSettings.NativeFormat, QSettings.IniFormat]:
@ -442,6 +443,7 @@ class ConfigManager(QObject):
'html > ::-webkit-scrollbar { width: 0px; height: 0px; }': '', 'html > ::-webkit-scrollbar { width: 0px; height: 0px; }': '',
'::-webkit-scrollbar { width: 0px; height: 0px; }': '', '::-webkit-scrollbar { width: 0px; height: 0px; }': '',
}), }),
('contents', 'cache-size'): _get_value_transformer({'52428800': ''}),
} }
changed = pyqtSignal(str, str) changed = pyqtSignal(str, str)
@ -772,12 +774,12 @@ class ConfigManager(QObject):
raise cmdexc.CommandError("set: {} - {}".format( raise cmdexc.CommandError("set: {} - {}".format(
e.__class__.__name__, e)) e.__class__.__name__, e))
@cmdutils.register(name='set', instance='config') @cmdutils.register(name='set', instance='config', star_args_optional=True)
@cmdutils.argument('section_', completion=Completion.section) @cmdutils.argument('section_', completion=Completion.section)
@cmdutils.argument('option', completion=Completion.option) @cmdutils.argument('option', completion=Completion.option)
@cmdutils.argument('value', completion=Completion.value) @cmdutils.argument('values', completion=Completion.value)
@cmdutils.argument('win_id', win_id=True) @cmdutils.argument('win_id', win_id=True)
def set_command(self, win_id, section_=None, option=None, value=None, def set_command(self, win_id, section_=None, option=None, *values,
temp=False, print_=False): temp=False, print_=False):
"""Set an option. """Set an option.
@ -793,7 +795,7 @@ class ConfigManager(QObject):
Args: Args:
section_: The section where the option is in. section_: The section where the option is in.
option: The name of the option. option: The name of the option.
value: The value to set. values: The value to set, or the values to cycle through.
temp: Set value temporarily. temp: Set value temporarily.
print_: Print the value after setting. print_: Print the value after setting.
""" """
@ -812,27 +814,46 @@ class ConfigManager(QObject):
print_ = True print_ = True
else: else:
with self._handle_config_error(): with self._handle_config_error():
if option.endswith('!') and option != '!' and value is None: if option.endswith('!') and option != '!' and not values:
# Handle inversion as special cases of the cycle code path
option = option[:-1] option = option[:-1]
val = self.get(section_, option) val = self.get(section_, option)
layer = 'temp' if temp else 'conf'
if isinstance(val, bool): if isinstance(val, bool):
self.set(layer, section_, option, str(not val).lower()) values = ['false', 'true']
else: else:
raise cmdexc.CommandError( raise cmdexc.CommandError(
"set: Attempted inversion of non-boolean value.") "set: Attempted inversion of non-boolean value.")
elif value is not None: elif not values:
layer = 'temp' if temp else 'conf'
self.set(layer, section_, option, value)
else:
raise cmdexc.CommandError("set: The following arguments " raise cmdexc.CommandError("set: The following arguments "
"are required: value") "are required: value")
layer = 'temp' if temp else 'conf'
self._set_next(layer, section_, option, values)
if print_: if print_:
with self._handle_config_error(): with self._handle_config_error():
val = self.get(section_, option, transformed=False) val = self.get(section_, option, transformed=False)
message.info("{} {} = {}".format(section_, option, val)) message.info("{} {} = {}".format(section_, option, val))
def _set_next(self, layer, section_, option, values):
"""Set the next value out of a list of values."""
if len(values) == 1:
# If we have only one value, just set it directly (avoid
# breaking stuff like aliases or other pseudo-settings)
self.set(layer, section_, option, values[0])
else:
# Otherwise, use the next valid value from values, or the
# first if the current value does not appear in the list
assert len(values) > 1
val = self.get(section_, option, transformed=False)
try:
idx = values.index(str(val))
idx = (idx + 1) % len(values)
value = values[idx]
except ValueError:
value = values[0]
self.set(layer, section_, option, value)
def set(self, layer, sectname, optname, value, validate=True): def set(self, layer, sectname, optname, value, validate=True):
"""Set an option. """Set an option.
@ -863,10 +884,9 @@ class ConfigManager(QObject):
# Will be handled later in .setv() # Will be handled later in .setv()
pass pass
else: else:
backend = usertypes.arg2backend[objreg.get('args').backend]
if (allowed_backends is not None and if (allowed_backends is not None and
backend not in allowed_backends): objects.backend not in allowed_backends):
raise configexc.BackendError(backend) raise configexc.BackendError(objects.backend)
else: else:
interpolated = None interpolated = None

View File

@ -35,7 +35,7 @@ from qutebrowser.config import configtypes as typ
from qutebrowser.config import sections as sect from qutebrowser.config import sections as sect
from qutebrowser.config.value import SettingValue from qutebrowser.config.value import SettingValue
from qutebrowser.utils.qtutils import MAXVALS from qutebrowser.utils.qtutils import MAXVALS
from qutebrowser.utils import usertypes from qutebrowser.utils import usertypes, qtutils
FIRST_COMMENT = r""" FIRST_COMMENT = r"""
@ -147,6 +147,13 @@ def data(readonly=False):
"The URL parameters to strip with :yank url, separated by " "The URL parameters to strip with :yank url, separated by "
"commas."), "commas."),
('default-open-dispatcher',
SettingValue(typ.String(none_ok=True), ''),
"The default program used to open downloads. Set to an empty "
"string to use the default internal handler.\n\n"
"Any {} in the string will be expanded to the filename, else "
"the filename will be appended."),
('default-page', ('default-page',
SettingValue(typ.FuzzyUrl(), '${startpage}'), SettingValue(typ.FuzzyUrl(), '${startpage}'),
"The page to open if :open -t/-b/-w is used without URL. Use " "The page to open if :open -t/-b/-w is used without URL. Use "
@ -184,16 +191,22 @@ def data(readonly=False):
"icons."), "icons."),
('developer-extras', ('developer-extras',
SettingValue(typ.Bool(), 'false'), SettingValue(typ.Bool(), 'false',
backends=[usertypes.Backend.QtWebKit]),
"Enable extra tools for Web developers.\n\n" "Enable extra tools for Web developers.\n\n"
"This needs to be enabled for `:inspector` to work and also adds " "This needs to be enabled for `:inspector` to work and also adds "
"an _Inspect_ entry to the context menu."), "an _Inspect_ entry to the context menu. For QtWebEngine, see "
"'qutebrowser --help' instead."),
('print-element-backgrounds', ('print-element-backgrounds',
SettingValue(typ.Bool(), 'true', SettingValue(typ.Bool(), 'true',
backends=[usertypes.Backend.QtWebKit]), backends=(
None if qtutils.version_check('5.8', strict=True)
else [usertypes.Backend.QtWebKit])),
"Whether the background color and images are also drawn when the " "Whether the background color and images are also drawn when the "
"page is printed."), "page is printed.\n"
"This setting only works with Qt 5.8 or newer when using the "
"QtWebEngine backend."),
('xss-auditing', ('xss-auditing',
SettingValue(typ.Bool(), 'false'), SettingValue(typ.Bool(), 'false'),
@ -206,7 +219,7 @@ def data(readonly=False):
('site-specific-quirks', ('site-specific-quirks',
SettingValue(typ.Bool(), 'true', SettingValue(typ.Bool(), 'true',
backends=[usertypes.Backend.QtWebKit]), backends=[usertypes.Backend.QtWebKit]),
"Enable workarounds for broken sites."), "Enable QtWebKit workarounds for broken sites."),
('default-encoding', ('default-encoding',
SettingValue(typ.String(none_ok=True), ''), SettingValue(typ.String(none_ok=True), ''),
@ -338,7 +351,8 @@ def data(readonly=False):
('smooth-scrolling', ('smooth-scrolling',
SettingValue(typ.Bool(), 'false'), SettingValue(typ.Bool(), 'false'),
"Whether to enable smooth scrolling for webpages."), "Whether to enable smooth scrolling for web pages. Note smooth "
"scrolling does not work with the :scroll-px command."),
('remove-finished-downloads', ('remove-finished-downloads',
SettingValue(typ.Int(minval=-1), '-1'), SettingValue(typ.Int(minval=-1), '-1'),
@ -390,6 +404,10 @@ def data(readonly=False):
SettingValue(typ.Int(minval=0), '8'), SettingValue(typ.Int(minval=0), '8'),
"The rounding radius for the edges of prompts."), "The rounding radius for the edges of prompts."),
('prompt-filebrowser',
SettingValue(typ.Bool(), 'true'),
"Show a filebrowser in upload/download prompts."),
readonly=readonly readonly=readonly
)), )),
@ -420,10 +438,13 @@ def data(readonly=False):
('proxy', ('proxy',
SettingValue(typ.Proxy(), 'system', SettingValue(typ.Proxy(), 'system',
backends=[usertypes.Backend.QtWebKit]), backends=(None if qtutils.version_check('5.8')
else [usertypes.Backend.QtWebKit])),
"The proxy to use.\n\n" "The proxy to use.\n\n"
"In addition to the listed values, you can use a `socks://...` " "In addition to the listed values, you can use a `socks://...` "
"or `http://...` URL."), "or `http://...` URL.\n\n"
"This setting only works with Qt 5.8 or newer when using the "
"QtWebEngine backend."),
('proxy-dns-requests', ('proxy-dns-requests',
SettingValue(typ.Bool(), 'true', SettingValue(typ.Bool(), 'true',
@ -570,7 +591,7 @@ def data(readonly=False):
"disables the context menu."), "disables the context menu."),
('mouse-zoom-divider', ('mouse-zoom-divider',
SettingValue(typ.Int(minval=1), '512'), SettingValue(typ.Int(minval=0), '512'),
"How much to divide the mouse wheel movements to translate them " "How much to divide the mouse wheel movements to translate them "
"into zoom increments."), "into zoom increments."),
@ -792,9 +813,10 @@ def data(readonly=False):
"enabled."), "enabled."),
('cache-size', ('cache-size',
SettingValue(typ.Int(minval=0, maxval=MAXVALS['int64']), SettingValue(typ.Int(none_ok=True, minval=0,
'52428800'), maxval=MAXVALS['int64']), ''),
"Size of the HTTP network cache."), "Size of the HTTP network cache. Empty to use the default "
"value."),
readonly=readonly readonly=readonly
)), )),
@ -815,9 +837,8 @@ def data(readonly=False):
"are not affected by this setting."), "are not affected by this setting."),
('webgl', ('webgl',
SettingValue(typ.Bool(), 'false'), SettingValue(typ.Bool(), 'true'),
"Enables or disables WebGL. For QtWebEngine, Qt/PyQt >= 5.7 is " "Enables or disables WebGL."),
"required for this setting."),
('css-regions', ('css-regions',
SettingValue(typ.Bool(), 'true', SettingValue(typ.Bool(), 'true',
@ -854,7 +875,8 @@ def data(readonly=False):
('javascript-can-access-clipboard', ('javascript-can-access-clipboard',
SettingValue(typ.Bool(), 'false'), SettingValue(typ.Bool(), 'false'),
"Whether JavaScript programs can read or write to the " "Whether JavaScript programs can read or write to the "
"clipboard."), "clipboard.\nWith QtWebEngine, writing the clipboard as response "
"to a user interaction is always allowed."),
('ignore-javascript-prompt', ('ignore-javascript-prompt',
SettingValue(typ.Bool(), 'false'), SettingValue(typ.Bool(), 'false'),
@ -888,9 +910,9 @@ def data(readonly=False):
"Control which cookies to accept."), "Control which cookies to accept."),
('cookies-store', ('cookies-store',
SettingValue(typ.Bool(), 'true', SettingValue(typ.Bool(), 'true'),
backends=[usertypes.Backend.QtWebKit]), "Whether to store cookies. Note this option needs a restart with "
"Whether to store cookies."), "QtWebEngine."),
('host-block-lists', ('host-block-lists',
SettingValue( SettingValue(
@ -936,7 +958,9 @@ def data(readonly=False):
('mode', ('mode',
SettingValue(typ.String( SettingValue(typ.String(
valid_values=typ.ValidValues( valid_values=typ.ValidValues(
('number', "Use numeric hints."), ('number', "Use numeric hints. (In this mode you can "
"also type letters form the hinted element to filter "
"and reduce the number of elements that are hinted.)"),
('letter', "Use the chars in the hints -> " ('letter', "Use the chars in the hints -> "
"chars setting."), "chars setting."),
('word', "Use hints words based on the html " ('word', "Use hints words based on the html "
@ -1268,8 +1292,7 @@ def data(readonly=False):
"Background color for downloads with errors."), "Background color for downloads with errors."),
('webpage.bg', ('webpage.bg',
SettingValue(typ.QtColor(none_ok=True), 'white', SettingValue(typ.QtColor(none_ok=True), 'white'),
backends=[usertypes.Backend.QtWebKit]),
"Background color for webpages if unset (or empty to use the " "Background color for webpages if unset (or empty to use the "
"theme's color)"), "theme's color)"),
@ -1543,7 +1566,8 @@ KEY_DATA = collections.OrderedDict([
])), ])),
('normal', collections.OrderedDict([ ('normal', collections.OrderedDict([
('clear-keychain ;; search', ['<Escape>']), ('clear-keychain ;; search ;; fullscreen --leave',
['<Escape>', '<Ctrl-[>']),
('set-cmd-text -s :open', ['o']), ('set-cmd-text -s :open', ['o']),
('set-cmd-text :open {url:pretty}', ['go']), ('set-cmd-text :open {url:pretty}', ['go']),
('set-cmd-text -s :open -t', ['O']), ('set-cmd-text -s :open -t', ['O']),
@ -1569,10 +1593,10 @@ KEY_DATA = collections.OrderedDict([
('tab-clone', ['gC']), ('tab-clone', ['gC']),
('reload', ['r', '<F5>']), ('reload', ['r', '<F5>']),
('reload -f', ['R', '<Ctrl-F5>']), ('reload -f', ['R', '<Ctrl-F5>']),
('back', ['H']), ('back', ['H', '<back>']),
('back -t', ['th']), ('back -t', ['th']),
('back -w', ['wh']), ('back -w', ['wh']),
('forward', ['L']), ('forward', ['L', '<forward>']),
('forward -t', ['tl']), ('forward -t', ['tl']),
('forward -w', ['wl']), ('forward -w', ['wl']),
('fullscreen', ['<F11>']), ('fullscreen', ['<F11>']),
@ -1650,7 +1674,8 @@ KEY_DATA = collections.OrderedDict([
('set-cmd-text -s :buffer', ['gt']), ('set-cmd-text -s :buffer', ['gt']),
('tab-focus last', ['<Ctrl-Tab>']), ('tab-focus last', ['<Ctrl-Tab>']),
('enter-mode passthrough', ['<Ctrl-V>']), ('enter-mode passthrough', ['<Ctrl-V>']),
('quit', ['<Ctrl-Q>']), ('quit', ['<Ctrl-Q>', 'ZQ']),
('wq', ['ZZ']),
('scroll-page 0 1', ['<Ctrl-F>']), ('scroll-page 0 1', ['<Ctrl-F>']),
('scroll-page 0 -1', ['<Ctrl-B>']), ('scroll-page 0 -1', ['<Ctrl-B>']),
('scroll-page 0 0.5', ['<Ctrl-D>']), ('scroll-page 0 0.5', ['<Ctrl-D>']),
@ -1764,8 +1789,12 @@ CHANGED_KEY_COMMANDS = [
(re.compile(r'^download-page$'), r'download'), (re.compile(r'^download-page$'), r'download'),
(re.compile(r'^cancel-download$'), r'download-cancel'), (re.compile(r'^cancel-download$'), r'download-cancel'),
(re.compile(r"""^search (''|"")$"""), r'clear-keychain ;; search'), (re.compile(r"""^search (''|"")$"""),
(re.compile(r'^search$'), r'clear-keychain ;; search'), r'clear-keychain ;; search ;; fullscreen --leave'),
(re.compile(r'^search$'),
r'clear-keychain ;; search ;; fullscreen --leave'),
(re.compile(r'^clear-keychain ;; search$'),
r'clear-keychain ;; search ;; fullscreen --leave'),
(re.compile(r"""^set-cmd-text ['"](.*) ['"]$"""), r'set-cmd-text -s \1'), (re.compile(r"""^set-cmd-text ['"](.*) ['"]$"""), r'set-cmd-text -s \1'),
(re.compile(r"""^set-cmd-text ['"](.*)['"]$"""), r'set-cmd-text \1'), (re.compile(r"""^set-cmd-text ['"](.*)['"]$"""), r'set-cmd-text \1'),
@ -1779,7 +1808,8 @@ CHANGED_KEY_COMMANDS = [
(re.compile(r'^scroll 50 0$'), r'scroll right'), (re.compile(r'^scroll 50 0$'), r'scroll right'),
(re.compile(r'^scroll ([-\d]+ [-\d]+)$'), r'scroll-px \1'), (re.compile(r'^scroll ([-\d]+ [-\d]+)$'), r'scroll-px \1'),
(re.compile(r'^search *;; *clear-keychain$'), r'clear-keychain ;; search'), (re.compile(r'^search *;; *clear-keychain$'),
r'clear-keychain ;; search ;; fullscreen --leave'),
(re.compile(r'^clear-keychain *;; *leave-mode$'), r'leave-mode'), (re.compile(r'^clear-keychain *;; *leave-mode$'), r'leave-mode'),
(re.compile(r'^download-remove --all$'), r'download-clear'), (re.compile(r'^download-remove --all$'), r'download-clear'),

View File

@ -31,7 +31,6 @@ import datetime
from PyQt5.QtCore import QUrl, Qt from PyQt5.QtCore import QUrl, Qt
from PyQt5.QtGui import QColor, QFont from PyQt5.QtGui import QColor, QFont
from PyQt5.QtNetwork import QNetworkProxy
from PyQt5.QtWidgets import QTabWidget, QTabBar from PyQt5.QtWidgets import QTabWidget, QTabBar
from qutebrowser.commands import cmdutils from qutebrowser.commands import cmdutils
@ -695,30 +694,17 @@ class CssColor(BaseType):
class QssColor(CssColor): class QssColor(CssColor):
"""Base class for a color value. """Color used in a Qt stylesheet."""
Class attributes:
color_func_regexes: Valid function regexes.
"""
num = r'[0-9]{1,3}%?'
color_func_regexes = [
r'rgb\({num},\s*{num},\s*{num}\)'.format(num=num),
r'rgba\({num},\s*{num},\s*{num},\s*{num}\)'.format(num=num),
r'hsv\({num},\s*{num},\s*{num}\)'.format(num=num),
r'hsva\({num},\s*{num},\s*{num},\s*{num}\)'.format(num=num),
r'qlineargradient\(.*\)',
r'qradialgradient\(.*\)',
r'qconicalgradient\(.*\)',
]
def validate(self, value): def validate(self, value):
functions = ['rgb', 'rgba', 'hsv', 'hsva', 'qlineargradient',
'qradialgradient', 'qconicalgradient']
self._basic_validation(value) self._basic_validation(value)
if not value: if not value:
return return
elif any(re.match(r, value) for r in self.color_func_regexes): elif (any(value.startswith(func + '(') for func in functions) and
# QColor doesn't handle these, so we do the best we can easily value.endswith(')')):
# QColor doesn't handle these
pass pass
elif QColor.isValidColor(value): elif QColor.isValidColor(value):
pass pass
@ -744,13 +730,13 @@ class Font(BaseType):
(?P<size>[0-9]+((\.[0-9]+)?[pP][tT]|[pP][xX])) (?P<size>[0-9]+((\.[0-9]+)?[pP][tT]|[pP][xX]))
)\ # size/weight/style are space-separated )\ # size/weight/style are space-separated
)* # 0-inf size/weight/style tags )* # 0-inf size/weight/style tags
(?P<family>[A-Za-z0-9, "-]*)$ # mandatory font family""", re.VERBOSE) (?P<family>.+)$ # mandatory font family""", re.VERBOSE)
def validate(self, value): def validate(self, value):
self._basic_validation(value) self._basic_validation(value)
if not value: if not value:
return return
elif not self.font_regex.match(value): elif not self.font_regex.match(value): # pragma: no cover
raise configexc.ValidationError(value, "must be a valid font") raise configexc.ValidationError(value, "must be a valid font")
@ -763,7 +749,7 @@ class FontFamily(Font):
if not value: if not value:
return return
match = self.font_regex.match(value) match = self.font_regex.match(value)
if not match: if not match: # pragma: no cover
raise configexc.ValidationError(value, "must be a valid font") raise configexc.ValidationError(value, "must be a valid font")
for group in 'style', 'weight', 'namedweight', 'size': for group in 'style', 'weight', 'namedweight', 'size':
if match.group(group): if match.group(group):
@ -1018,12 +1004,6 @@ class Proxy(BaseType):
"""A proxy URL or special value.""" """A proxy URL or special value."""
PROXY_TYPES = {
'http': QNetworkProxy.HttpProxy,
'socks': QNetworkProxy.Socks5Proxy,
'socks5': QNetworkProxy.Socks5Proxy,
}
def __init__(self, none_ok=False): def __init__(self, none_ok=False):
super().__init__(none_ok) super().__init__(none_ok)
self.valid_values = ValidValues( self.valid_values = ValidValues(
@ -1031,19 +1011,17 @@ class Proxy(BaseType):
('none', "Don't use any proxy")) ('none', "Don't use any proxy"))
def validate(self, value): def validate(self, value):
from qutebrowser.utils import urlutils
self._basic_validation(value) self._basic_validation(value)
if not value: if not value:
return return
elif value in self.valid_values: elif value in self.valid_values:
return return
url = QUrl(value)
if not url.isValid(): try:
raise configexc.ValidationError( self.transform(value)
value, "invalid url, {}".format(url.errorString())) except (urlutils.InvalidUrlError, urlutils.InvalidProxyTypeError) as e:
elif url.scheme() not in self.PROXY_TYPES: raise configexc.ValidationError(value, e)
raise configexc.ValidationError(value, "must be a proxy URL "
"(http://... or socks://...) or "
"system/none!")
def complete(self): def complete(self):
out = [] out = []
@ -1053,25 +1031,21 @@ class Proxy(BaseType):
out.append(('socks://', 'SOCKS proxy URL')) out.append(('socks://', 'SOCKS proxy URL'))
out.append(('socks://localhost:9050/', 'Tor via SOCKS')) out.append(('socks://localhost:9050/', 'Tor via SOCKS'))
out.append(('http://localhost:8080/', 'Local HTTP proxy')) out.append(('http://localhost:8080/', 'Local HTTP proxy'))
out.append(('pac+https://example.com/proxy.pac', 'Proxy autoconfiguration file URL'))
return out return out
def transform(self, value): def transform(self, value):
from qutebrowser.utils import urlutils
if not value: if not value:
return None return None
elif value == 'system': elif value == 'system':
return SYSTEM_PROXY return SYSTEM_PROXY
elif value == 'none':
return QNetworkProxy(QNetworkProxy.NoProxy) if value == 'none':
url = QUrl('direct://')
else:
url = QUrl(value) url = QUrl(value)
typ = self.PROXY_TYPES[url.scheme()] return urlutils.proxy_from_url(url)
proxy = QNetworkProxy(typ, url.host())
if url.port() != -1:
proxy.setPort(url.port())
if url.userName():
proxy.setUser(url.userName())
if url.password():
proxy.setPassword(url.password())
return proxy
class SearchEngineName(BaseType): class SearchEngineName(BaseType):

View File

@ -280,6 +280,9 @@ class KeyConfigParser(QObject):
A binding is considered new if both the command is not bound to any key A binding is considered new if both the command is not bound to any key
yet, and the key isn't used anywhere else in the same section. yet, and the key isn't used anywhere else in the same section.
""" """
if utils.is_special_key(keychain):
keychain = keychain.lower()
try: try:
bindings = self.keybindings[sectname] bindings = self.keybindings[sectname]
except KeyError: except KeyError:
@ -432,7 +435,9 @@ class KeyConfigParser(QObject):
def get_reverse_bindings_for(self, section): def get_reverse_bindings_for(self, section):
"""Get a dict of commands to a list of bindings for the section.""" """Get a dict of commands to a list of bindings for the section."""
cmd_to_keys = {} cmd_to_keys = {}
for key, cmd in self.get_bindings_for(section).items(): for key, full_cmd in self.get_bindings_for(section).items():
for cmd in full_cmd.split(';;'):
cmd = cmd.strip()
cmd_to_keys.setdefault(cmd, []) cmd_to_keys.setdefault(cmd, [])
# put special bindings last # put special bindings last
if utils.is_special_key(key): if utils.is_special_key(key):

View File

@ -20,7 +20,8 @@
"""Bridge from QWeb(Engine)Settings to our own settings.""" """Bridge from QWeb(Engine)Settings to our own settings."""
from qutebrowser.config import config from qutebrowser.config import config
from qutebrowser.utils import log, utils, debug, objreg from qutebrowser.utils import log, utils, debug, usertypes
from qutebrowser.misc import objects
UNSET = object() UNSET = object()
@ -259,19 +260,19 @@ def update_mappings(mappings, section, option):
mapping.set(value) mapping.set(value)
def init(): def init(args):
"""Initialize all QWeb(Engine)Settings.""" """Initialize all QWeb(Engine)Settings."""
if objreg.get('args').backend == 'webengine': if objects.backend == usertypes.Backend.QtWebEngine:
from qutebrowser.browser.webengine import webenginesettings from qutebrowser.browser.webengine import webenginesettings
webenginesettings.init() webenginesettings.init(args)
else: else:
from qutebrowser.browser.webkit import webkitsettings from qutebrowser.browser.webkit import webkitsettings
webkitsettings.init() webkitsettings.init(args)
def shutdown(): def shutdown():
"""Shut down QWeb(Engine)Settings.""" """Shut down QWeb(Engine)Settings."""
if objreg.get('args').backend == 'webengine': if objects.backend == usertypes.Backend.QtWebEngine:
from qutebrowser.browser.webengine import webenginesettings from qutebrowser.browser.webengine import webenginesettings
webenginesettings.shutdown() webenginesettings.shutdown()
else: else:

View File

@ -1,34 +1,66 @@
{% extends "base.html" %} {% extends "styled.html" %}
{% block style %} {% block style %}
table { border: 1px solid grey; border-collapse: collapse; width: 100%;} {{super()}}
th, td { border: 1px solid grey; padding: 0px 5px; } h1 {
th { background: lightgrey; } margin-bottom: 10px;
}
.url a {
color: #444;
}
th {
text-align: left;
}
.qmarks .name {
padding-left: 5px;
}
.empty-msg {
background-color: #f8f8f8;
color: #444;
display: inline-block;
text-align: center;
width: 100%;
}
{% endblock %} {% endblock %}
{% block content %} {% block content %}
<table> <h1>Quickmarks</h1>
<tr>
<th><h3>Bookmark</h3></th> {% if quickmarks|length %}
<th><h3>URL</h3></th> <table class="qmarks">
</tr> <tbody>
{% for url, title in bookmarks %}
<tr>
<td><a href="{{url}}">{{title}}</a></td>
<td>{{url}}</td>
</tr>
{% endfor %}
<tr>
<th><h3>Quickmark</h3></th>
<th><h3>URL</h3></th>
</tr>
{% for name, url in quickmarks %} {% for name, url in quickmarks %}
<tr> <tr>
<td><a href="{{url}}">{{name}}</a></td> <td class="name"><a href="{{url}}">{{name}}</a></td>
<td>{{url}}</td> <td class="url"><a href="{{url}}">{{url}}</a></td>
</tr> </tr>
{% endfor %} {% endfor %}
</tbody>
</table> </table>
{% else %}
<span class="empty-msg">You have no quickmarks</span>
{% endif %}
<h1>Bookmarks</h1>
{% if bookmarks|length %}
<table class="bmarks">
<tbody>
{% for url, title in bookmarks %}
<tr>
<td class="name"><a href="{{url}}">{{title | default(url, true)}}</a></td>
<td class="url"><a href="{{url}}">{{url}}</a></td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<span class="empty-msg">You have no bookmarks</span>
{% endif %}
{% endblock %} {% endblock %}

View File

@ -0,0 +1,58 @@
{% extends "styled.html" %}
{% block style %}
{{super()}}
body {
max-width: 1440px;
}
td.title {
word-break: break-all;
}
td.time {
color: #555;
text-align: right;
white-space: nowrap;
}
.date {
color: #888;
font-size: 14pt;
padding-left: 25px;
}
.pagination-link {
display: inline-block;
margin-bottom: 10px;
margin-top: 10px;
padding-right: 10px;
}
.pagination-link > a {
color: #333;
font-weight: bold;
}
{% endblock %}
{% block content %}
<h1>Browsing history <span class="date">{{curr_date.strftime("%a, %d %B %Y")}}</span></h1>
<table>
<tbody>
{% for url, title, time in history %}
<tr>
<td class="title"><a href="{{url}}">{{title}}</a></td>
<td class="time">{{time}}</td>
</tr>
{% endfor %}
</tbody>
</table>
<span class="pagination-link"><a href="qute://history/?date={{prev_date.strftime("%Y-%m-%d")}}" rel="prev">Previous</a></span>
{% if today >= next_date %}
<span class="pagination-link"><a href="qute://history/?date={{next_date.strftime("%Y-%m-%d")}}" rel="next">Next</a></span>
{% endif %}
{% endblock %}

View File

@ -0,0 +1,41 @@
{% extends "base.html" %}
{% block style %}
a {
text-decoration: none;
color: #2562dc
}
a:hover {
text-decoration: underline;
}
body {
background: #fefefe;
font-family: sans-serif;
margin: 0 auto;
max-width: 1280px;
padding-left: 20px;
padding-right: 20px;
}
h1 {
color: #444;
font-weight: normal;
}
table {
border-collapse: collapse;
width: 100%;
}
tbody tr:nth-child(odd) {
background-color: #f8f8f8;
}
td {
max-width: 50%;
padding: 2px 5px;
text-align: left;
}
{% endblock %}

View File

@ -0,0 +1,2 @@
# Upstream Mozilla's code
pac_utils.js

View File

@ -36,3 +36,5 @@ rules:
sort-keys: "off" sort-keys: "off"
no-warning-comments: "off" no-warning-comments: "off"
max-len: ["error", {"ignoreUrls": true}] max-len: ["error", {"ignoreUrls": true}]
capitalized-comments: "off"
prefer-destructuring: "off"

View File

@ -0,0 +1,257 @@
/* ***** BEGIN LICENSE BLOCK *****
* Version: MPL 1.1/GPL 2.0/LGPL 2.1
*
* The contents of this file are subject to the Mozilla Public License Version
* 1.1 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
* http://www.mozilla.org/MPL/
*
* Software distributed under the License is distributed on an "AS IS" basis,
* WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
* for the specific language governing rights and limitations under the
* License.
*
* The Original Code is mozilla.org code.
*
* The Initial Developer of the Original Code is
* Netscape Communications Corporation.
* Portions created by the Initial Developer are Copyright (C) 1998
* the Initial Developer. All Rights Reserved.
*
* Contributor(s):
* Akhil Arora <akhil.arora@sun.com>
* Tomi Leppikangas <Tomi.Leppikangas@oulu.fi>
* Darin Fisher <darin@meer.net>
*
* Alternatively, the contents of this file may be used under the terms of
* either the GNU General Public License Version 2 or later (the "GPL"), or
* the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
* in which case the provisions of the GPL or the LGPL are applicable instead
* of those above. If you wish to allow use of your version of this file only
* under the terms of either the GPL or the LGPL, and not to allow others to
* use your version of this file under the terms of the MPL, indicate your
* decision by deleting the provisions above and replace them with the notice
* and other provisions required by the GPL or the LGPL. If you do not delete
* the provisions above, a recipient may use your version of this file under
* the terms of any one of the MPL, the GPL or the LGPL.
*
* ***** END LICENSE BLOCK ***** */
/*
Script for Proxy Auto Config in the new world order.
- Gagan Saksena 04/24/00
*/
function dnsDomainIs(host, domain) {
return (host.length >= domain.length &&
host.substring(host.length - domain.length) == domain);
}
function dnsDomainLevels(host) {
return host.split('.').length-1;
}
function convert_addr(ipchars) {
var bytes = ipchars.split('.');
var result = ((bytes[0] & 0xff) << 24) |
((bytes[1] & 0xff) << 16) |
((bytes[2] & 0xff) << 8) |
(bytes[3] & 0xff);
return result;
}
function isInNet(ipaddr, pattern, maskstr) {
var test = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/
.exec(ipaddr);
if (test == null) {
ipaddr = dnsResolve(ipaddr);
if (ipaddr == null)
return false;
} else if (test[1] > 255 || test[2] > 255 ||
test[3] > 255 || test[4] > 255) {
return false; // not an IP address
}
var host = convert_addr(ipaddr);
var pat = convert_addr(pattern);
var mask = convert_addr(maskstr);
return ((host & mask) == (pat & mask));
}
function isPlainHostName(host) {
return (host.search('\\.') == -1);
}
function isResolvable(host) {
var ip = dnsResolve(host);
return (ip != null);
}
function localHostOrDomainIs(host, hostdom) {
return (host == hostdom) ||
(hostdom.lastIndexOf(host + '.', 0) == 0);
}
function shExpMatch(url, pattern) {
pattern = pattern.replace(/\./g, '\\.');
pattern = pattern.replace(/\*/g, '.*');
pattern = pattern.replace(/\?/g, '.');
var newRe = new RegExp('^'+pattern+'$');
return newRe.test(url);
}
var wdays = {SUN: 0, MON: 1, TUE: 2, WED: 3, THU: 4, FRI: 5, SAT: 6};
var months = {JAN: 0, FEB: 1, MAR: 2, APR: 3, MAY: 4, JUN: 5, JUL: 6,
AUG: 7, SEP: 8, OCT: 9, NOV: 10, DEC: 11};
function weekdayRange() {
function getDay(weekday) {
if (weekday in wdays) {
return wdays[weekday];
}
return -1;
}
var date = new Date();
var argc = arguments.length;
var wday;
if (argc < 1)
return false;
if (arguments[argc - 1] == 'GMT') {
argc--;
wday = date.getUTCDay();
} else {
wday = date.getDay();
}
var wd1 = getDay(arguments[0]);
var wd2 = (argc == 2) ? getDay(arguments[1]) : wd1;
return (wd1 == -1 || wd2 == -1) ? false
: (wd1 <= wday && wday <= wd2);
}
function dateRange() {
function getMonth(name) {
if (name in months) {
return months[name];
}
return -1;
}
var date = new Date();
var argc = arguments.length;
if (argc < 1) {
return false;
}
var isGMT = (arguments[argc - 1] == 'GMT');
if (isGMT) {
argc--;
}
// function will work even without explict handling of this case
if (argc == 1) {
var tmp = parseInt(arguments[0]);
if (isNaN(tmp)) {
return ((isGMT ? date.getUTCMonth() : date.getMonth()) ==
getMonth(arguments[0]));
} else if (tmp < 32) {
return ((isGMT ? date.getUTCDate() : date.getDate()) == tmp);
} else {
return ((isGMT ? date.getUTCFullYear() : date.getFullYear()) ==
tmp);
}
}
var year = date.getFullYear();
var date1, date2;
date1 = new Date(year, 0, 1, 0, 0, 0);
date2 = new Date(year, 11, 31, 23, 59, 59);
var adjustMonth = false;
for (var i = 0; i < (argc >> 1); i++) {
var tmp = parseInt(arguments[i]);
if (isNaN(tmp)) {
var mon = getMonth(arguments[i]);
date1.setMonth(mon);
} else if (tmp < 32) {
adjustMonth = (argc <= 2);
date1.setDate(tmp);
} else {
date1.setFullYear(tmp);
}
}
for (var i = (argc >> 1); i < argc; i++) {
var tmp = parseInt(arguments[i]);
if (isNaN(tmp)) {
var mon = getMonth(arguments[i]);
date2.setMonth(mon);
} else if (tmp < 32) {
date2.setDate(tmp);
} else {
date2.setFullYear(tmp);
}
}
if (adjustMonth) {
date1.setMonth(date.getMonth());
date2.setMonth(date.getMonth());
}
if (isGMT) {
var tmp = date;
tmp.setFullYear(date.getUTCFullYear());
tmp.setMonth(date.getUTCMonth());
tmp.setDate(date.getUTCDate());
tmp.setHours(date.getUTCHours());
tmp.setMinutes(date.getUTCMinutes());
tmp.setSeconds(date.getUTCSeconds());
date = tmp;
}
return ((date1 <= date) && (date <= date2));
}
function timeRange() {
var argc = arguments.length;
var date = new Date();
var isGMT= false;
if (argc < 1) {
return false;
}
if (arguments[argc - 1] == 'GMT') {
isGMT = true;
argc--;
}
var hour = isGMT ? date.getUTCHours() : date.getHours();
var date1, date2;
date1 = new Date();
date2 = new Date();
if (argc == 1) {
return (hour == arguments[0]);
} else if (argc == 2) {
return ((arguments[0] <= hour) && (hour <= arguments[1]));
} else {
switch (argc) {
case 6:
date1.setSeconds(arguments[2]);
date2.setSeconds(arguments[5]);
case 4:
var middle = argc >> 1;
date1.setHours(arguments[0]);
date1.setMinutes(arguments[1]);
date2.setHours(arguments[middle]);
date2.setMinutes(arguments[middle + 1]);
if (middle == 2) {
date2.setSeconds(59);
}
break;
default:
throw 'timeRange: bad number of arguments'
}
}
if (isGMT) {
date.setFullYear(date.getUTCFullYear());
date.setMonth(date.getUTCMonth());
date.setDate(date.getUTCDate());
date.setHours(date.getUTCHours());
date.setMinutes(date.getUTCMinutes());
date.setSeconds(date.getUTCSeconds());
}
return ((date1 <= date) && (date <= date2));
}

View File

@ -17,6 +17,23 @@
* along with qutebrowser. If not, see <http://www.gnu.org/licenses/>. * along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
*/ */
/**
* The connection for web elements between Python and Javascript works like
* this:
*
* - Python calls into Javascript and invokes a function to find elements (like
* find_all, focus_element, element_at_pos or element_by_id).
* - Javascript gets the requested element, and calls serialize_elem on it.
* - serialize_elem saves the javascript element object in "elements", gets some
* attributes from the element, and assigns an ID (index into 'elements') to
* it.
* - Python gets this information and constructs a Python wrapper object with
* the information it got right away, and the ID.
* - When Python wants to modify an element, it calls javascript again with the
* element ID.
* - Javascript gets the element from the elements array, and modifies it.
*/
"use strict"; "use strict";
window._qutebrowser.webelem = (function() { window._qutebrowser.webelem = (function() {
@ -92,15 +109,18 @@ window._qutebrowser.webelem = (function() {
} }
var style = win.getComputedStyle(elem, null); var style = win.getComputedStyle(elem, null);
if (style.getPropertyValue("visibility") !== "visible" ||
style.getPropertyValue("display") === "none" ||
style.getPropertyValue("opacity") === "0") {
// FIXME:qtwebengine do we need this <area> handling? // FIXME:qtwebengine do we need this <area> handling?
// visibility and display style are misleading for area tags and they // visibility and display style are misleading for area tags and
// get "display: none" by default. // they get "display: none" by default.
// See https://github.com/vimperator/vimperator-labs/issues/236 // See https://github.com/vimperator/vimperator-labs/issues/236
if (elem.nodeName.toLowerCase() !== "area" && ( if (elem.nodeName.toLowerCase() !== "area" &&
style.getPropertyValue("visibility") !== "visible" || !elem.classList.contains("ace_text-input")) {
style.getPropertyValue("display") === "none")) {
return false; return false;
} }
}
return true; return true;
} }
@ -136,9 +156,8 @@ window._qutebrowser.webelem = (function() {
funcs.insert_text = function(id, text) { funcs.insert_text = function(id, text) {
var elem = elements[id]; var elem = elements[id];
var event = document.createEvent("TextEvent"); elem.focus();
event.initTextEvent("textInput", true, true, null, text); document.execCommand("insertText", false, text);
elem.dispatchEvent(event);
}; };
funcs.element_at_pos = function(x, y) { funcs.element_at_pos = function(x, y) {
@ -174,5 +193,21 @@ window._qutebrowser.webelem = (function() {
} }
}; };
funcs.click = function(id) {
var elem = elements[id];
elem.click();
};
funcs.focus = function(id) {
var elem = elements[id];
elem.focus();
};
funcs.move_cursor_to_end = function(id) {
var elem = elements[id];
elem.selectionStart = elem.value.length;
elem.selectionEnd = elem.value.length;
};
return funcs; return funcs;
})(); })();

View File

@ -147,6 +147,9 @@ class BaseKeyParser(QObject):
(countstr, cmd_input) = re.match(r'^(\d*)(.*)', (countstr, cmd_input) = re.match(r'^(\d*)(.*)',
self._keystring).groups() self._keystring).groups()
count = int(countstr) if countstr else None count = int(countstr) if countstr else None
if count == 0 and not cmd_input:
cmd_input = self._keystring
count = None
else: else:
cmd_input = self._keystring cmd_input = self._keystring
count = None count = None

View File

@ -28,6 +28,7 @@ from qutebrowser.keyinput import modeparsers, keyparser
from qutebrowser.config import config from qutebrowser.config import config
from qutebrowser.commands import cmdexc, cmdutils from qutebrowser.commands import cmdexc, cmdutils
from qutebrowser.utils import usertypes, log, objreg, utils from qutebrowser.utils import usertypes, log, objreg, utils
from qutebrowser.misc import objects
class KeyEvent: class KeyEvent:
@ -265,6 +266,16 @@ class ModeManager(QObject):
m = usertypes.KeyMode[mode] m = usertypes.KeyMode[mode]
except KeyError: except KeyError:
raise cmdexc.CommandError("Mode {} does not exist!".format(mode)) raise cmdexc.CommandError("Mode {} does not exist!".format(mode))
if m in [usertypes.KeyMode.hint, usertypes.KeyMode.command,
usertypes.KeyMode.yesno, usertypes.KeyMode.prompt]:
raise cmdexc.CommandError(
"Mode {} can't be entered manually!".format(mode))
elif (m == usertypes.KeyMode.caret and
objects.backend == usertypes.Backend.QtWebEngine):
raise cmdexc.CommandError("Caret mode is not supported with "
"QtWebEngine yet.")
self.enter(m, 'command') self.enter(m, 'command')
@pyqtSlot(usertypes.KeyMode, str, bool) @pyqtSlot(usertypes.KeyMode, str, bool)
@ -288,7 +299,7 @@ class ModeManager(QObject):
log.modes.debug("Leaving mode {}{}".format( log.modes.debug("Leaving mode {}{}".format(
mode, '' if reason is None else ' (reason: {})'.format(reason))) mode, '' if reason is None else ' (reason: {})'.format(reason)))
# leaving a mode implies clearing keychain, see # leaving a mode implies clearing keychain, see
# https://github.com/The-Compiler/qutebrowser/issues/1805 # https://github.com/qutebrowser/qutebrowser/issues/1805
self.clear_keychain() self.clear_keychain()
self.mode = usertypes.KeyMode.normal self.mode = usertypes.KeyMode.normal
self.left.emit(mode, self.mode, self._win_id) self.left.emit(mode, self.mode, self._win_id)

View File

@ -267,7 +267,7 @@ class CaretKeyParser(keyparser.CommandKeyParser):
self.read_config('caret') self.read_config('caret')
class RegisterKeyParser(keyparser.BaseKeyParser): class RegisterKeyParser(keyparser.CommandKeyParser):
"""KeyParser for modes that record a register key. """KeyParser for modes that record a register key.
@ -280,6 +280,7 @@ class RegisterKeyParser(keyparser.BaseKeyParser):
super().__init__(win_id, parent, supports_count=False, super().__init__(win_id, parent, supports_count=False,
supports_chains=False) supports_chains=False)
self._mode = mode self._mode = mode
self.read_config('register')
def handle(self, e): def handle(self, e):
"""Override handle to always match the next key and use the register. """Override handle to always match the next key and use the register.
@ -290,12 +291,15 @@ class RegisterKeyParser(keyparser.BaseKeyParser):
Return: Return:
True if event has been handled, False otherwise. True if event has been handled, False otherwise.
""" """
if utils.keyevent_to_string(e) is None: if super().handle(e):
# this is a modifier key, let it pass and keep going return True
return False
key = e.text() key = e.text()
if key == '' or utils.keyevent_to_string(e) is None:
# this is not a proper register key, let it pass and keep going
return False
tabbed_browser = objreg.get('tabbed-browser', scope='window', tabbed_browser = objreg.get('tabbed-browser', scope='window',
window=self._win_id) window=self._win_id)
macro_recorder = objreg.get('macro-recorder') macro_recorder = objreg.get('macro-recorder')
@ -323,7 +327,3 @@ class RegisterKeyParser(keyparser.BaseKeyParser):
def on_keyconfig_changed(self, mode): def on_keyconfig_changed(self, mode):
"""RegisterKeyParser has no config section (no bindable keys).""" """RegisterKeyParser has no config section (no bindable keys)."""
pass pass
def execute(self, cmdstr, _keytype, count=None):
"""Should never be called on RegisterKeyParser."""
assert False

View File

@ -238,13 +238,25 @@ class MainWindow(QWidget):
height_padding = 20 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() if self.status.isVisible():
status_height = self.status.height()
bottom = self.status.geometry().top()
else:
status_height = 0
bottom = self.height()
top = self.height() - status_height - size_hint.height()
top = qtutils.check_overflow(top, 'int', fatal=False) top = qtutils.check_overflow(top, 'int', fatal=False)
topleft = QPoint(left, max(height_padding, top)) topleft = QPoint(left, max(height_padding, top))
bottomright = QPoint(left + width, self.status.geometry().top()) bottomright = QPoint(left + width, bottom)
elif status_position == 'top': elif status_position == 'top':
topleft = QPoint(left, self.status.geometry().bottom()) if self.status.isVisible():
bottom = self.status.height() + size_hint.height() status_height = self.status.height()
top = self.status.geometry().bottom()
else:
status_height = 0
top = 0
topleft = QPoint(left, top)
bottom = status_height + size_hint.height()
bottom = qtutils.check_overflow(bottom, 'int', fatal=False) bottom = qtutils.check_overflow(bottom, 'int', fatal=False)
bottomright = QPoint(left + width, bottomright = QPoint(left + width,
min(self.height() - height_padding, bottom)) min(self.height() - height_padding, bottom))
@ -425,6 +437,9 @@ class MainWindow(QWidget):
# messages # messages
message.global_bridge.show_message.connect( message.global_bridge.show_message.connect(
self._messageview.show_message) self._messageview.show_message)
message.global_bridge.flush()
message.global_bridge.clear_messages.connect(
self._messageview.clear_messages)
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)
@ -444,6 +459,10 @@ class MainWindow(QWidget):
tabs.cur_url_changed.connect(status.url.set_url) tabs.cur_url_changed.connect(status.url.set_url)
tabs.cur_link_hovered.connect(status.url.set_hover_url) tabs.cur_link_hovered.connect(status.url.set_hover_url)
tabs.cur_load_status_changed.connect(status.url.on_load_status_changed) tabs.cur_load_status_changed.connect(status.url.on_load_status_changed)
tabs.page_fullscreen_requested.connect(
self._on_page_fullscreen_requested)
tabs.page_fullscreen_requested.connect(
status.on_page_fullscreen_requested)
# command input / completion # command input / completion
mode_manager.left.connect(tabs.on_mode_left) mode_manager.left.connect(tabs.on_mode_left)
@ -451,6 +470,13 @@ class MainWindow(QWidget):
completion_obj.on_clear_completion_selection) completion_obj.on_clear_completion_selection)
cmd.hide_completion.connect(completion_obj.hide) cmd.hide_completion.connect(completion_obj.hide)
@pyqtSlot(bool)
def _on_page_fullscreen_requested(self, on):
if on:
self.showFullScreen()
else:
self.showNormal()
@cmdutils.register(instance='main-window', scope='window') @cmdutils.register(instance='main-window', scope='window')
@pyqtSlot() @pyqtSlot()
def close(self): def close(self):
@ -462,14 +488,6 @@ class MainWindow(QWidget):
""" """
super().close() super().close()
@cmdutils.register(instance='main-window', scope='window')
def fullscreen(self):
"""Toggle fullscreen mode."""
if self.isFullScreen():
self.showNormal()
else:
self.showFullScreen()
def resizeEvent(self, e): def resizeEvent(self, e):
"""Extend resizewindow's resizeEvent to adjust completion. """Extend resizewindow's resizeEvent to adjust completion.

View File

@ -31,8 +31,9 @@ class Message(QLabel):
"""A single error/warning/info message.""" """A single error/warning/info message."""
def __init__(self, level, text, parent=None): def __init__(self, level, text, replace, parent=None):
super().__init__(text, parent) super().__init__(text, parent)
self.replace = replace
self.setAttribute(Qt.WA_StyledBackground, True) self.setAttribute(Qt.WA_StyledBackground, True)
stylesheet = """ stylesheet = """
padding-top: 2px; padding-top: 2px;
@ -81,7 +82,7 @@ class MessageView(QWidget):
self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
self._clear_timer = QTimer() self._clear_timer = QTimer()
self._clear_timer.timeout.connect(self._clear_messages) self._clear_timer.timeout.connect(self.clear_messages)
self._set_clear_timer_interval() self._set_clear_timer_interval()
objreg.get('config').changed.connect(self._set_clear_timer_interval) objreg.get('config').changed.connect(self._set_clear_timer_interval)
@ -100,7 +101,7 @@ class MessageView(QWidget):
self._clear_timer.setInterval(config.get('ui', 'message-timeout')) self._clear_timer.setInterval(config.get('ui', 'message-timeout'))
@pyqtSlot() @pyqtSlot()
def _clear_messages(self): def clear_messages(self):
"""Hide and delete all messages.""" """Hide and delete all messages."""
for widget in self._messages: for widget in self._messages:
self._vbox.removeWidget(widget) self._vbox.removeWidget(widget)
@ -111,13 +112,17 @@ class MessageView(QWidget):
self.hide() self.hide()
self._clear_timer.stop() self._clear_timer.stop()
@pyqtSlot(usertypes.MessageLevel, str) @pyqtSlot(usertypes.MessageLevel, str, bool)
def show_message(self, level, text): def show_message(self, level, text, replace=False):
"""Show the given message with the given MessageLevel.""" """Show the given message with the given MessageLevel."""
if text == self._last_text: if text == self._last_text:
return return
widget = Message(level, text, parent=self) if replace and self._messages and self._messages[-1].replace:
old = self._messages.pop()
old.hide()
widget = Message(level, text, replace=replace, parent=self)
self._vbox.addWidget(widget) self._vbox.addWidget(widget)
widget.show() widget.show()
self._clear_timer.start() self._clear_timer.start()

View File

@ -25,12 +25,12 @@ import collections
import sip import sip
from PyQt5.QtCore import (pyqtSlot, pyqtSignal, Qt, QTimer, QDir, QModelIndex, from PyQt5.QtCore import (pyqtSlot, pyqtSignal, Qt, QTimer, QDir, QModelIndex,
QItemSelectionModel, QObject) QItemSelectionModel, QObject, QEventLoop)
from PyQt5.QtWidgets import (QWidget, QGridLayout, QVBoxLayout, QLineEdit, from PyQt5.QtWidgets import (QWidget, QGridLayout, QVBoxLayout, QLineEdit,
QLabel, QFileSystemModel, QTreeView, QSizePolicy) QLabel, QFileSystemModel, QTreeView, QSizePolicy)
from qutebrowser.browser import downloads from qutebrowser.browser import downloads
from qutebrowser.config import style from qutebrowser.config import style, config
from qutebrowser.utils import usertypes, log, utils, qtutils, objreg, message from qutebrowser.utils import usertypes, log, utils, qtutils, objreg, message
from qutebrowser.keyinput import modeman from qutebrowser.keyinput import modeman
from qutebrowser.commands import cmdutils, cmdexc from qutebrowser.commands import cmdutils, cmdexc
@ -112,7 +112,7 @@ class PromptQueue(QObject):
if not sip.isdeleted(question): if not sip.isdeleted(question):
# the question could already be deleted, e.g. by a cancelled # the question could already be deleted, e.g. by a cancelled
# download. See # download. See
# https://github.com/The-Compiler/qutebrowser/issues/415 # https://github.com/qutebrowser/qutebrowser/issues/415
self.ask_question(question, blocking=False) self.ask_question(question, blocking=False)
def shutdown(self): def shutdown(self):
@ -153,7 +153,7 @@ class PromptQueue(QObject):
if self._shutting_down: if self._shutting_down:
# If we're currently shutting down we have to ignore this question # If we're currently shutting down we have to ignore this question
# to avoid segfaults - see # to avoid segfaults - see
# https://github.com/The-Compiler/qutebrowser/issues/95 # https://github.com/qutebrowser/qutebrowser/issues/95
log.prompt.debug("Ignoring question because we're shutting down.") log.prompt.debug("Ignoring question because we're shutting down.")
question.abort() question.abort()
return None return None
@ -184,7 +184,7 @@ class PromptQueue(QObject):
question.completed.connect(loop.quit) question.completed.connect(loop.quit)
question.completed.connect(loop.deleteLater) question.completed.connect(loop.deleteLater)
log.prompt.debug("Starting loop.exec_() for {}".format(question)) log.prompt.debug("Starting loop.exec_() for {}".format(question))
loop.exec_() loop.exec_(QEventLoop.ExcludeSocketNotifiers)
log.prompt.debug("Ending loop.exec_() for {}".format(question)) log.prompt.debug("Ending loop.exec_() for {}".format(question))
log.prompt.debug("Restoring old question {}".format(old_question)) log.prompt.debug("Restoring old question {}".format(old_question))
@ -564,6 +564,8 @@ class FilenamePrompt(_BasePrompt):
self.setFocusProxy(self._lineedit) self.setFocusProxy(self._lineedit)
self._init_key_label() self._init_key_label()
if config.get('ui', 'prompt-filebrowser'):
self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred) self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred)
@pyqtSlot(str) @pyqtSlot(str)
@ -624,7 +626,12 @@ class FilenamePrompt(_BasePrompt):
self._file_model = QFileSystemModel(self) self._file_model = QFileSystemModel(self)
self._file_view.setModel(self._file_model) self._file_view.setModel(self._file_model)
self._file_view.clicked.connect(self._insert_path) self._file_view.clicked.connect(self._insert_path)
if config.get('ui', 'prompt-filebrowser'):
self._vbox.addWidget(self._file_view) self._vbox.addWidget(self._file_view)
else:
self._file_view.hide()
# Only show name # Only show name
self._file_view.setHeaderHidden(True) self._file_view.setHeaderHidden(True)
for col in range(1, 4): for col in range(1, 4):

View File

@ -46,6 +46,8 @@ class StatusBar(QWidget):
_hbox: The main QHBoxLayout. _hbox: The main QHBoxLayout.
_stack: The QStackedLayout with cmd/txt widgets. _stack: The QStackedLayout with cmd/txt widgets.
_win_id: The window ID the statusbar is associated with. _win_id: The window ID the statusbar is associated with.
_page_fullscreen: Whether the webpage (e.g. a video) is shown
fullscreen.
Class attributes: Class attributes:
_prompt_active: If we're currently in prompt-mode. _prompt_active: If we're currently in prompt-mode.
@ -143,6 +145,7 @@ class StatusBar(QWidget):
self._win_id = win_id self._win_id = win_id
self._option = None self._option = None
self._page_fullscreen = False
self._hbox = QHBoxLayout(self) self._hbox = QHBoxLayout(self)
self.set_hbox_padding() self.set_hbox_padding()
@ -193,7 +196,7 @@ class StatusBar(QWidget):
def maybe_hide(self): def maybe_hide(self):
"""Hide the statusbar if it's configured to do so.""" """Hide the statusbar if it's configured to do so."""
hide = config.get('ui', 'hide-statusbar') hide = config.get('ui', 'hide-statusbar')
if hide: if hide or self._page_fullscreen:
self.hide() self.hide()
else: else:
self.show() self.show()
@ -306,6 +309,11 @@ class StatusBar(QWidget):
usertypes.KeyMode.yesno]: usertypes.KeyMode.yesno]:
self.set_mode_active(old_mode, False) self.set_mode_active(old_mode, False)
@pyqtSlot(bool)
def on_page_fullscreen_requested(self, on):
self._page_fullscreen = on
self.maybe_hide()
def resizeEvent(self, e): def resizeEvent(self, e):
"""Extend resizeEvent of QWidget to emit a resized signal afterwards. """Extend resizeEvent of QWidget to emit a resized signal afterwards.

View File

@ -98,6 +98,7 @@ class TabbedBrowser(tabwidget.TabWidget):
resized = pyqtSignal('QRect') resized = pyqtSignal('QRect')
current_tab_changed = pyqtSignal(browsertab.AbstractTab) current_tab_changed = pyqtSignal(browsertab.AbstractTab)
new_tab = pyqtSignal(browsertab.AbstractTab, int) new_tab = pyqtSignal(browsertab.AbstractTab, int)
page_fullscreen_requested = pyqtSignal(bool)
def __init__(self, win_id, parent=None): def __init__(self, win_id, parent=None):
super().__init__(win_id, parent) super().__init__(win_id, parent)
@ -198,8 +199,13 @@ class TabbedBrowser(tabwidget.TabWidget):
functools.partial(self.on_load_started, tab)) functools.partial(self.on_load_started, tab))
tab.window_close_requested.connect( tab.window_close_requested.connect(
functools.partial(self.on_window_close_requested, tab)) functools.partial(self.on_window_close_requested, tab))
tab.renderer_process_terminated.connect(
functools.partial(self._on_renderer_process_terminated, tab))
tab.new_tab_requested.connect(self.tabopen) tab.new_tab_requested.connect(self.tabopen)
tab.add_history_item.connect(objreg.get('web-history').add_from_tab) tab.add_history_item.connect(objreg.get('web-history').add_from_tab)
tab.fullscreen_requested.connect(self.page_fullscreen_requested)
tab.fullscreen_requested.connect(
self.tabBar().on_page_fullscreen_requested)
def current_url(self): def current_url(self):
"""Get the URL of the current tab. """Get the URL of the current tab.
@ -245,12 +251,13 @@ class TabbedBrowser(tabwidget.TabWidget):
url = config.get('general', 'default-page') url = config.get('general', 'default-page')
self.openurl(url, newtab=True) self.openurl(url, newtab=True)
def _remove_tab(self, tab, *, add_undo=True): def _remove_tab(self, tab, *, add_undo=True, crashed=False):
"""Remove a tab from the tab list and delete it properly. """Remove a tab from the tab list and delete it properly.
Args: Args:
tab: The QWebView to be closed. tab: The QWebView to be closed.
add_undo: Whether the tab close can be undone. add_undo: Whether the tab close can be undone.
crashed: Whether we're closing a tab with crashed renderer process.
""" """
idx = self.indexOf(tab) idx = self.indexOf(tab)
if idx == -1: if idx == -1:
@ -262,24 +269,33 @@ class TabbedBrowser(tabwidget.TabWidget):
window=self._win_id): window=self._win_id):
objreg.delete('last-focused-tab', scope='window', objreg.delete('last-focused-tab', scope='window',
window=self._win_id) window=self._win_id)
if tab.url().isValid():
history_data = tab.history.serialize() if tab.url().isEmpty():
if add_undo:
entry = UndoEntry(tab.url(), history_data, idx)
self._undo_stack.append(entry)
elif tab.url().isEmpty():
# There are some good reasons why a URL could be empty # There are some good reasons why a URL could be empty
# (target="_blank" with a download, see [1]), so we silently ignore # (target="_blank" with a download, see [1]), so we silently ignore
# this. # this.
# [1] https://github.com/The-Compiler/qutebrowser/issues/163 # [1] https://github.com/qutebrowser/qutebrowser/issues/163
pass pass
else: elif not tab.url().isValid():
# We display a warnings for URLs which are not empty but invalid - # We display a warning for URLs which are not empty but invalid -
# but we don't return here because we want the tab to close either # but we don't return here because we want the tab to close either
# way. # way.
urlutils.invalid_url_error(tab.url(), "saving tab") urlutils.invalid_url_error(tab.url(), "saving tab")
elif add_undo:
try:
history_data = tab.history.serialize()
except browsertab.WebTabError:
pass # special URL
else:
entry = UndoEntry(tab.url(), history_data, idx)
self._undo_stack.append(entry)
tab.shutdown() tab.shutdown()
self.removeTab(idx) self.removeTab(idx)
if not crashed:
# WORKAROUND for a segfault when we delete the crashed tab.
# see https://bugreports.qt.io/browse/QTBUG-58698
tab.layout().unwrap()
tab.deleteLater() tab.deleteLater()
def undo(self): def undo(self):
@ -347,7 +363,8 @@ class TabbedBrowser(tabwidget.TabWidget):
@pyqtSlot('QUrl') @pyqtSlot('QUrl')
@pyqtSlot('QUrl', bool) @pyqtSlot('QUrl', bool)
def tabopen(self, url=None, background=None, explicit=False, idx=None): def tabopen(self, url=None, background=None, explicit=False, idx=None, *,
ignore_tabs_are_windows=False):
"""Open a new tab with a given URL. """Open a new tab with a given URL.
Inner logic for open-tab and open-tab-bg. Inner logic for open-tab and open-tab-bg.
@ -364,6 +381,8 @@ class TabbedBrowser(tabwidget.TabWidget):
the current. the current.
- Explicitly opened tabs are at the very right. - Explicitly opened tabs are at the very right.
idx: The index where the new tab should be opened. idx: The index where the new tab should be opened.
ignore_tabs_are_windows: If given, never open a new window, even
with tabs-are-windows set.
Return: Return:
The opened WebView instance. The opened WebView instance.
@ -374,7 +393,8 @@ class TabbedBrowser(tabwidget.TabWidget):
"explicit {}, idx {}".format( "explicit {}, idx {}".format(
url, background, explicit, idx)) url, background, explicit, idx))
if config.get('tabs', 'tabs-are-windows') and self.count() > 0: if (config.get('tabs', 'tabs-are-windows') and self.count() > 0 and
not ignore_tabs_are_windows):
from qutebrowser.mainwindow import mainwindow from qutebrowser.mainwindow import mainwindow
window = mainwindow.MainWindow() window = mainwindow.MainWindow()
window.show() window.show()
@ -523,22 +543,6 @@ class TabbedBrowser(tabwidget.TabWidget):
if not self.page_title(idx): if not self.page_title(idx):
self.set_page_title(idx, url.toDisplayString()) self.set_page_title(idx, url.toDisplayString())
# If needed, re-open the tab as a workaround for QTBUG-54419.
# See https://bugreports.qt.io/browse/QTBUG-54419
background = self.currentIndex() != idx
if (tab.backend == usertypes.Backend.QtWebEngine and
tab.needs_qtbug54419_workaround):
log.misc.debug("Doing QTBUG-54419 workaround for {}, "
"url {}".format(tab, url))
self.setUpdatesEnabled(False)
try:
self.tabopen(url, background=background, idx=idx)
self.close_tab(tab, add_undo=False)
finally:
self.setUpdatesEnabled(True)
tab.needs_qtbug54419_workaround = False
@pyqtSlot(browsertab.AbstractTab, QIcon) @pyqtSlot(browsertab.AbstractTab, QIcon)
def on_icon_changed(self, tab, icon): def on_icon_changed(self, tab, icon):
"""Set the icon of a tab. """Set the icon of a tab.
@ -650,6 +654,28 @@ class TabbedBrowser(tabwidget.TabWidget):
self.update_window_title() self.update_window_title()
self.update_tab_title(idx) self.update_tab_title(idx)
def _on_renderer_process_terminated(self, tab, status, code):
"""Show an error when a renderer process terminated."""
if status == browsertab.TerminationStatus.normal:
pass
elif status == browsertab.TerminationStatus.abnormal:
message.error("Renderer process exited with status {}".format(
code))
elif status == browsertab.TerminationStatus.crashed:
message.error("Renderer process crashed")
elif status == browsertab.TerminationStatus.killed:
message.error("Renderer process was killed")
elif status == browsertab.TerminationStatus.unknown:
message.error("Renderer process did not start")
else:
raise ValueError("Invalid status {}".format(status))
# WORKAROUND for https://bugreports.qt.io/browse/QTBUG-58698
# FIXME:qtwebengine can we disable this with Qt 5.8.1?
self._remove_tab(tab, crashed=True)
if self.count() == 0:
self.tabopen(QUrl('about:blank'))
def resizeEvent(self, e): def resizeEvent(self, e):
"""Extend resizeEvent of QWidget to emit a resized signal afterwards. """Extend resizeEvent of QWidget to emit a resized signal afterwards.

Some files were not shown because too many files have changed in this diff Show More