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

View File

@ -13,15 +13,30 @@ matrix:
env: DOCKER=archlinux
services: docker
- 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
- os: linux
env: DOCKER=ubuntu-xenial
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
env: TESTENV=py35 OSX=elcapitan
env: TESTENV=py36 OSX=elcapitan
osx_image: xcode7.3
# https://github.com/The-Compiler/qutebrowser/issues/2013
# https://github.com/qutebrowser/qutebrowser/issues/2013
# - os: osx
# env: TESTENV=py35 OSX=yosemite
# osx_image: xcode6.4
@ -43,14 +58,14 @@ matrix:
env: TESTENV=eslint
allow_failures:
- os: osx
env: TESTENV=py35 OSX=elcapitan
env: TESTENV=py36 OSX=elcapitan
osx_image: xcode7.3
fast_finish: true
cache:
directories:
- $HOME/.cache/pip
- $HOME/build/The-Compiler/qutebrowser/.cache
- $HOME/build/qutebrowser/qutebrowser/.cache
before_install:
# We need to do this so we pick up the system-wide python properly
@ -58,6 +73,7 @@ before_install:
install:
- bash scripts/dev/ci/travis_install.sh
- ulimit -c unlimited
script:
- bash scripts/dev/ci/travis_run.sh
@ -65,6 +81,9 @@ script:
after_success:
- '[[ $TESTENV == *-cov ]] && codecov -e TESTENV -X gcov'
after_failure:
- bash scripts/dev/ci/travis_backtrace.sh
notifications:
webhooks:
- 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.
// `Security` to invite users to upgrade in case of vulnerabilities.
v0.9.0 (unreleased)
-------------------
v0.11.0 (unreleased)
--------------------
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`
did before v0.8.0.
- 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 `ui -> hide-scrollbar` setting to hide the scrollbar independently of the
`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
~~~~~~~
@ -149,6 +271,8 @@ Changed
- `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
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
~~~~~~~~~~
@ -186,6 +310,10 @@ Fixed
- `:tab-detach` now fails correctly when there's only one tab open.
- Various small issues with the command 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
------
@ -794,7 +922,7 @@ Fixed
- Fixed horrible completion performance when the `shrink` option was set.
- 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
@ -802,7 +930,7 @@ Fixed
- 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
@ -945,7 +1073,7 @@ Fixed
- Add a timeout to pastebin HTTP replies.
- 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
@ -989,7 +1117,7 @@ Security
* Stop the icon database from being created when private-browsing is set to true.
* 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
@ -1023,7 +1151,7 @@ Security
* 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
@ -1055,7 +1183,7 @@ Fixed
* 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
@ -1113,7 +1241,7 @@ Fixed
* Ensure the docs get included in `freeze.py`.
* 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.

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.
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:
* 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]
* 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]
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:
----
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
@ -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.
Rebuilding the website
~~~~~~~~~~~~~~~~~~~~~~
If you want to rebuild the website, run `./scripts/asciidoc2html.py --website <outputdir>`.
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
workaround.
* 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.
New PyQt release
@ -638,6 +644,7 @@ New PyQt release
* See above
* Install new PyQt in Windows VM (32- and 64-bit)
* 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
~~~~~~~~~~~~~~~~~~~
@ -659,7 +666,7 @@ qutebrowser release
* `git push origin`; `git push origin v0.$x.$y`
* If committing on minor branch, cherry-pick release commit to master.
* 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.
* 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?::
Due to a Qt limitation, local files without `.html` extensions are
"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`:
+
----
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
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`
if mozplugger was subsequently removed.
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.
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
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
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),
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.::
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.
If you are reporting a segfault, make sure you read the
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:
----
# 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
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].
Install the packages:
@ -56,7 +56,7 @@ Then install the packages like this:
----
# 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
----
@ -74,7 +74,7 @@ For distributions other than Debian or if you prefer to not use the
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
@ -214,7 +214,7 @@ Prebuilt binaries
~~~~~~~~~~~~~~~~~
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.
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`
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
https://caskroom.github.io/[Homebrew Cask] package manager:
@ -272,29 +272,21 @@ qutebrowser from source.
==== 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 -s pyqt5
$ pip3.5 install qutebrowser
$ pip3 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
---------
@ -313,7 +305,7 @@ First of all, clone the repository using http://git-scm.org/[git] and switch
into the repository folder:
----
$ git clone https://github.com/The-Compiler/qutebrowser.git
$ git clone https://github.com/qutebrowser/qutebrowser.git
$ cd qutebrowser
----

View File

@ -1,26 +1,26 @@
// If you are reading this in plaintext or on PyPi:
//
// 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
===========
// 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://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://travis-ci.org/The-Compiler/qutebrowser.svg?branch=master["Build Status", link="https://travis-ci.org/The-Compiler/qutebrowser"]
image:https://ci.appveyor.com/api/projects/status/9gmnuip6i1oq7046?svg=true["AppVeyor build status", link="https://ci.appveyor.com/project/The-Compiler/qutebrowser"]
image:https://codecov.io/github/The-Compiler/qutebrowser/coverage.svg?branch=master["coverage badge",link="https://codecov.io/github/The-Compiler/qutebrowser?branch=master"]
image:https://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/qutebrowser/qutebrowser.svg?branch=master["Build Status", link="https://travis-ci.org/qutebrowser/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/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
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.
@ -35,7 +35,7 @@ image:doc/img/hints.png["screenshot 4",width=300,link="doc/img/hints.png"]
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
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://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
(5.5.1 recommended) for Python 3
* 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
* Daniel Schadt
* Ryan Roden-Corrent
* Jakub Klinkovský
* Jan Verbeek
* Jakub Klinkovský
* Antoni Boucher
* Lamar Pavel
* Marshall Lochbaum
@ -165,14 +165,17 @@ Contributors, sorted by the number of commits in descending order:
* Corentin Julé
* meles5
* Philipp Hansch
* Imran Sobir
* Panagiotis Ktistakis
* Artur Shaik
* Nathan Isom
* Thorsten Wißmann
* Austin Anderson
* Fritz Reichwald
* Jimmy
* Spreadyy
* Niklas Haas
* Maciej Wołczyk
* Spreadyy
* Alexey "Averrin" Nabrodov
* nanjekyejoannah
* avk
@ -184,22 +187,25 @@ Contributors, sorted by the number of commits in descending order:
* knaggita
* Oliver Caldwell
* Julian Weigt
* Tomasz Kramkowski
* Sebastian Frysztak
* Nikolay Amiantov
* Julie Engel
* Jonas Schürmann
* error800
* Michael Hoang
* Maciej Wołczyk
* Liam BEGUIN
* Julie Engel
* Daniel Fiser
* skinnay
* Zach-Button
* Tomasz Kramkowski
* Samuel Walladge
* Peter Rice
* Ismail S
* Halfwit
* David Vogt
* Claire Cavanaugh
* rikn00
* pkill9
* kanikaa1234
* haitaka
* Nick Ginther
@ -207,19 +213,23 @@ Contributors, sorted by the number of commits in descending order:
* Michael Ilsaas
* Martin Zimmermann
* Jussi Timperi
* Fritz Reichwald
* Cosmin Popescu
* Brian Jackson
* thuck
* sbinix
* rsteube
* neeasade
* jnphilipp
* Yannis Rohloff
* Tobias Patzl
* Stefan Tatschner
* Samuel Loury
* Peter Michely
* Panashe M. Fundira
* Lucas Hoffmann
* Link
* Larry Hynes
* Kirill A. Shutemov
* Johannes Altmanninger
* Jeremy Kaplan
* Ismail
@ -233,12 +243,10 @@ Contributors, sorted by the number of commits in descending order:
* Marcelo Santos
* Joel Bradshaw
* Jean-Louis Fuchs
* Fritz V155 Reichwald
* Franz Fellner
* Eric Drechsel
* zwarag
* xd1le
* rsteube
* rmortens
* oniondreams
* issue
@ -247,6 +255,7 @@ Contributors, sorted by the number of commits in descending order:
* dylan araps
* addictedtoflames
* Xitian9
* Vasilij Schneidermann
* Tomas Orsava
* Tom Janson
* Tobias Werth
@ -260,6 +269,7 @@ Contributors, sorted by the number of commits in descending order:
* Matthias Lisin
* Marcel Schilling
* Lazlow Carmichael
* Kevin Wang
* Ján Kobezda
* Johannes Martinsson
* Jean-Christophe Petkovich
@ -274,6 +284,7 @@ Contributors, sorted by the number of commits in descending order:
* Arseniy Seroka
* Andy Balaam
* Andreas Fischer
* Akselmo
// QUTE_AUTHORS_END
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.
|<<help,help>>|Show help about a command or setting.
|<<hint,hint>>|Start hinting.
|<<history,history>>|Show browsing history.
|<<history-clear,history-clear>>|Clear all browsing history.
|<<home,home>>|Open main startpage in current tab.
|<<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.
|<<unbind,unbind>>|Unbind a keychain.
|<<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.
|<<wq,wq>>|Save open pages and quit.
|<<yank,yank>>|Yank something to the clipboard or primary selection.
@ -319,8 +320,13 @@ How many pages to go forward.
[[fullscreen]]
=== fullscreen
Syntax: +:fullscreen [*--leave*]+
Toggle fullscreen mode.
==== optional arguments
* +*-l*+, +*--leave*+: Only leave fullscreen if it was entered by the page.
[[help]]
=== help
Syntax: +:help [*--tab*] [*--bg*] [*--window*] ['topic']+
@ -413,12 +419,28 @@ Start hinting.
==== note
* 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
Syntax: +:history-clear [*--force*]+
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.
==== optional arguments
* +*-f*+, +*--force*+: Don't ask for confirmation.
[[home]]
=== home
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
Syntax: +:jseval [*--quiet*] [*--world* 'world'] 'js-code'+
Syntax: +:jseval [*--file*] [*--quiet*] [*--world* 'world'] 'js-code'+
Evaluate a JavaScript string.
==== positional arguments
* +'js-code'+: The string to evaluate.
* +'js-code'+: The string/file to evaluate.
==== optional arguments
* +*-f*+, +*--file*+: Interpret js-code as a path to a file.
* +*-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.
@ -718,7 +741,8 @@ Load a session.
[[session-save]]
=== session-save
Syntax: +:session-save [*--current*] [*--quiet*] [*--force*] ['name']+
Syntax: +:session-save [*--current*] [*--quiet*] [*--force*] [*--only-active-window*]
['name']+
Save a session.
@ -730,10 +754,11 @@ Save a session.
* +*-c*+, +*--current*+: Save the current session instead of the default.
* +*-q*+, +*--quiet*+: Don't show confirmation message.
* +*-f*+, +*--force*+: Force saving internal sessions (starting with an underline).
* +*-o*+, +*--only-active-window*+: Saves only tabs of the currently active window.
[[set]]
=== set
Syntax: +:set [*--temp*] [*--print*] ['section'] ['option'] ['value']+
Syntax: +:set [*--temp*] [*--print*] ['section'] ['option'] ['values' ['values' ...]]+
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
* +'section'+: The section where the option is in.
* +'option'+: The name of the option.
* +'value'+: The value to set.
* +'values'+: The value to set, or the values to cycle through.
==== optional arguments
* +*-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
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
@ -899,7 +923,7 @@ Re-open a closed tab (optionally skipping [count] closed tabs).
[[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
@ -971,6 +995,7 @@ How many steps to zoom out.
|==============
|Command|Description
|<<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.
|<<command-accept,command-accept>>|Execute the command currently in the commandline.
|<<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 the currently entered key chain.
[[clear-messages]]
=== clear-messages
Clear all message notifications.
[[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.
@ -1050,6 +1079,7 @@ The given filter needs to result in exactly one element, otherwise, an error is
==== optional arguments
* +*-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
@ -1138,6 +1168,9 @@ Show an info message in the statusbar.
==== positional arguments
* +'text'+: The text to show.
==== count
How many times to show the message
[[message-warning]]
=== message-warning
Syntax: +:message-warning 'text'+
@ -1405,6 +1438,8 @@ Syntax: +:scroll '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
* +'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-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-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-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.
@ -21,7 +22,7 @@
|<<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-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-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.
@ -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-hide-scrollbar,hide-scrollbar>>|Hide the main scrollbar.
|<<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-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).
@ -56,6 +57,7 @@
|<<ui-hide-wayland-decoration,hide-wayland-decoration>>|Hide the window decoration when using wayland (requires restart)
|<<ui-keyhint-blacklist,keyhint-blacklist>>|Keychains that shouldn't be shown in the keyhint dialog
|<<ui-prompt-radius,prompt-radius>>|The rounding radius for the edges of prompts.
|<<ui-prompt-filebrowser,prompt-filebrowser>>|Show a filebrowser in upload/download prompts.
|==============
.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-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-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''
@ -156,7 +158,7 @@
|<<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-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-hyperlink-auditing,hyperlink-auditing>>|Enable or disable hyperlink auditing (<a ping>).
|<<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-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-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-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.
@ -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]+
[[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]]
=== default-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
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:
@ -406,9 +416,12 @@ Valid values:
Default: +pass:[false]+
This setting is only available with the QtWebKit backend.
[[general-print-element-backgrounds]]
=== print-element-backgrounds
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:
@ -417,8 +430,6 @@ Valid values:
Default: +pass:[true]+
This setting is only available with the QtWebKit backend.
[[general-xss-auditing]]
=== xss-auditing
Whether load requests should be monitored for cross-site scripting attempts.
@ -434,7 +445,7 @@ Default: +pass:[false]+
[[general-site-specific-quirks]]
=== site-specific-quirks
Enable workarounds for broken sites.
Enable QtWebKit workarounds for broken sites.
Valid values:
@ -644,7 +655,7 @@ This setting is only available with the QtWebKit backend.
[[ui-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:
@ -727,6 +738,17 @@ The rounding radius for the edges of prompts.
Default: +pass:[8]+
[[ui-prompt-filebrowser]]
=== prompt-filebrowser
Show a filebrowser in upload/download prompts.
Valid values:
* +true+
* +false+
Default: +pass:[true]+
== 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.
This setting only works with Qt 5.8 or newer when using the QtWebEngine backend.
Valid values:
* +system+: Use the system wide proxy.
@ -780,8 +804,6 @@ Valid values:
Default: +pass:[system]+
This setting is only available with the QtWebKit backend.
[[network-proxy-dns-requests]]
=== proxy-dns-requests
Whether to send DNS requests over the configured proxy.
@ -1360,9 +1382,9 @@ Default: +pass:[true]+
[[storage-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
Loaded plugins/scripts and allowed actions.
@ -1404,14 +1426,14 @@ Default: +pass:[false]+
[[content-webgl]]
=== webgl
Enables or disables WebGL. For QtWebEngine, Qt/PyQt >= 5.7 is required for this setting.
Enables or disables WebGL.
Valid values:
* +true+
* +false+
Default: +pass:[false]+
Default: +pass:[true]+
[[content-css-regions]]
=== css-regions
@ -1502,6 +1524,7 @@ This setting is only available with the QtWebKit backend.
[[content-javascript-can-access-clipboard]]
=== javascript-can-access-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:
@ -1571,7 +1594,7 @@ This setting is only available with the QtWebKit backend.
[[content-cookies-store]]
=== cookies-store
Whether to store cookies.
Whether to store cookies. Note this option needs a restart with QtWebEngine.
Valid values:
@ -1580,8 +1603,6 @@ Valid values:
Default: +pass:[true]+
This setting is only available with the QtWebKit backend.
[[content-host-block-lists]]
=== host-block-lists
List of URLs of lists which contain hosts to block.
@ -1643,7 +1664,7 @@ Mode to use for hints.
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.
* +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]+
This setting is only available with the QtWebKit backend.
[[colors-keyhint.fg]]
=== keyhint.fg
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)
* 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 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
@ -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.
* If you just cloned the repository, you'll need to run
`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
https://lists.schokokeks.org/mailman/listinfo.cgi/qutebrowser[the mailinglist] or
https://lists.schokokeks.org/mailman/listinfo.cgi/qutebrowser-announce[the announce-only mailinglist].

View File

@ -10,7 +10,7 @@
:homepage: https://www.qutebrowser.org/
== NAME
qutebrowser - a keyboard-driven, vim-like browser based on PyQt5 and QtWebKit.
qutebrowser - a keyboard-driven, vim-like browser based on PyQt5.
== SYNOPSIS
*qutebrowser* ['-OPTION' ['...']] [':COMMAND' ['...']] ['URL' ['...']]
@ -59,6 +59,9 @@ show it.
*--backend* '{webkit,webengine}'::
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
*-l* '{critical,error,warning,info,debug,vdebug}', *--loglevel* '{critical,error,warning,info,debug,vdebug}'::
Set loglevel
@ -124,7 +127,7 @@ defaults.
== BUGS
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
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
* IRC: irc://irc.freenode.org/#qutebrowser[`#qutebrowser`] on
http://freenode.net/[Freenode]
* Github: https://github.com/The-Compiler/qutebrowser
* Github: https://github.com/qutebrowser/qutebrowser
== AUTHOR
*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_DATA_DIR`: Path of the directory containing qutebrowser's data.
- `QUTE_DOWNLOAD_DIR`: Path of the downloads directory.
- `QUTE_COMMANDLINE_TEXT`: Text currently in qutebrowser's command line.
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,
name='qutebrowser.app',
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
bundle_identifier='org.qt-project.Qt.QtWebEngineCore')

View File

@ -1,3 +1,3 @@
# 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
codecov==2.0.5
coverage==4.2
requests==2.12.1
coverage==4.3.4
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-debugger==1.4.0 # rq.filter: != 2.0.0
flake8-deprecated==1.1
flake8-docstrings==1.0.2
flake8-docstrings==1.0.3
flake8-future-import==0.4.3
flake8-mock==0.3
flake8-pep3101==0.6
flake8-pep3101==1.0
flake8-polyfill==1.0.1
flake8-putty==0.4.0
flake8-string-format==0.2.3
flake8-tidy-imports==1.0.3
flake8-tidy-imports==1.0.6
flake8-tuple==0.2.12
mccabe==0.5.2
packaging==16.8
mccabe==0.6.1
pep8-naming==0.4.1
pycodestyle==2.2.0
pycodestyle==2.3.1
pydocstyle==1.1.1
pyflakes==1.3.0
pyparsing==2.1.10
six==1.10.0
pyflakes==1.5.0

View File

@ -15,7 +15,9 @@ pydocstyle
pyflakes
# 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
#@ 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
-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/edrex/pyinstaller.git@1984_add_QtWebEngineCore#egg=PyInstaller
-e git+https://github.com/pyinstaller/pyinstaller.git@develop#egg=PyInstaller
# remove @commit-id for scm installs
#@ replace: @.*# @develop#

View File

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

View File

@ -1,14 +1,13 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py
astroid==1.4.8
astroid==1.4.9
github3.py==0.9.6
isort==4.2.5
lazy-object-proxy==1.2.2
mccabe==0.5.2
pylint==1.6.4
mccabe==0.6.1
pylint==1.6.5
./scripts/dev/pylint_checkers
requests==2.12.1
six==1.10.0
requests==2.13.0
uritemplate==3.0.0
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
docutils==0.12
docutils==0.13.1
pyroma==2.2

View File

@ -1,5 +1,5 @@
bzr+lp:beautifulsoup
git+https://github.com/cherrypy/cherrypy.git
git+https://github.com/cherrypy/cheroot.git
hg+https://bitbucket.org/ned/coveragepy
git+https://github.com/micheles/decorator.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
beautifulsoup4==4.5.1
CherryPy==8.1.2
click==6.6
coverage==4.2
decorator==4.0.10
Flask==0.11.1
beautifulsoup4==4.5.3
cheroot==5.3.0
click==6.7
coverage==4.3.4
decorator==4.0.11
EasyProcess==0.2.3
Flask==0.12
glob2==0.5
httpbin==0.5.0
hypothesis==3.6.0
hypothesis==3.6.1
itsdangerous==0.24
# Jinja2==2.8
# Jinja2==2.9.5
Mako==1.0.6
# MarkupSafe==0.23
parse==1.6.6
# MarkupSafe==1.0
parse==1.8.0
parse-type==0.3.4
py==1.4.31
pytest==3.0.4
py==1.4.33
pytest==3.0.7
pytest-bdd==2.18.1
pytest-benchmark==3.0.0
pytest-catchlog==1.2.2
pytest-cov==2.4.0
pytest-faulthandler==1.3.1
@ -28,7 +30,7 @@ pytest-repeat==0.4.1
pytest-rerunfailures==2.1.0
pytest-travis-fold==1.2.0
pytest-warnings==0.2.0
pytest-xvfb==0.3.0
six==1.10.0
vulture==0.10
Werkzeug==0.11.11
pytest-xvfb==1.0.0
PyVirtualDisplay==0.2.1
vulture==0.13
Werkzeug==0.12.1

View File

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

View File

@ -1,6 +1,6 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py
pluggy==0.4.0
py==1.4.31
tox==2.5.0
py==1.4.33
tox==2.6.0
virtualenv==15.1.0

View File

@ -1,3 +1,3 @@
# 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_skip: Tests not applicable with QtWebEngine
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_osx_xfail: Tests which fail on OS X with QtWebEngine
js_prompt: Tests needing to display a javascript prompt
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_ignore =
^SpellCheck: .*
@ -46,4 +49,6 @@ qt_log_ignore =
^load glyph failed
^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=
^QPainter::end: Painter ended with \d+ saved states
^QSslSocket: cannot resolve SSLv[23]_(client|server)_method
xfail_strict = true

View File

@ -8,3 +8,4 @@ Exec=qutebrowser %u
Terminal=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;
Keywords=Browser

View File

@ -17,9 +17,7 @@
# You should have received a copy of the GNU General Public License
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
# pylint: disable=line-too-long
"""A keyboard-driven, vim-like browser based on PyQt5 and QtWebKit."""
"""A keyboard-driven, vim-like browser based on PyQt5."""
import os.path
@ -28,8 +26,8 @@ __copyright__ = "Copyright 2014-2016 Florian Bruhin (The Compiler)"
__license__ = "GPL"
__maintainer__ = __author__
__email__ = "mail@qutebrowser.org"
__version_info__ = (0, 8, 4)
__version_info__ = (0, 10, 1)
__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__))

View File

@ -47,6 +47,7 @@ from qutebrowser.commands import cmdutils, runners, cmdexc
from qutebrowser.config import style, config, websettings, configexc
from qutebrowser.browser import (urlmarks, adblock, history, browsertab,
downloads)
from qutebrowser.browser.network import proxy
from qutebrowser.browser.webkit import cookies, cache
from qutebrowser.browser.webkit.network import networkmanager
from qutebrowser.keyinput import macros
@ -132,7 +133,6 @@ def init(args, crash_handler):
log.init.debug("Starting init...")
qApp.setQuitOnLastWindowClosed(False)
_init_icon()
utils.actute_warning()
try:
_init_modules(args, crash_handler)
@ -141,9 +141,6 @@ def init(args, crash_handler):
pre_text="Error while initializing")
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...")
event_filter = EventFilter(qApp)
qApp.installEventFilter(event_filter)
@ -154,11 +151,13 @@ def init(args, crash_handler):
config_obj.style_changed.connect(style.get_stylesheet.cache_clear)
qApp.focusChanged.connect(on_focus_changed)
_process_args(args)
QDesktopServices.setUrlHandler('http', open_desktopservices_url)
QDesktopServices.setUrlHandler('https', 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!")
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.
"""
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:
name = state_config['general']['session']
except KeyError:
# No session given as argument and none in the session file ->
# start without loading a session
return
session_manager = objreg.get('session-manager')
try:
session_manager.load(name)
except sessions.SessionNotFoundError:
@ -375,6 +377,7 @@ def _init_modules(args, crash_handler):
args: The argparse namespace.
crash_handler: The CrashHandler instance.
"""
# pylint: disable=too-many-statements
log.init.debug("Initializing prompts...")
prompt.init()
@ -386,6 +389,11 @@ def _init_modules(args, crash_handler):
log.init.debug("Initializing network...")
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...")
readline_bridge = readline.ReadlineBridge()
objreg.register('readline-bridge', readline_bridge)
@ -405,7 +413,7 @@ def _init_modules(args, crash_handler):
sessions.init(qApp)
log.init.debug("Initializing websettings...")
websettings.init()
websettings.init(args)
log.init.debug("Initializing adblock...")
host_blocker = adblock.HostBlocker()
@ -438,8 +446,9 @@ def _init_modules(args, crash_handler):
os.environ['QT_WAYLAND_DISABLE_WINDOWDECORATION'] = '1'
else:
os.environ.pop('QT_WAYLAND_DISABLE_WINDOWDECORATION', None)
macros.init()
# Init backend-specific stuff
browsertab.init(args)
browsertab.init()
def _init_late_modules(args):
@ -527,7 +536,7 @@ class Quitter:
if not os.path.isdir(cwd):
# Probably running from a python egg. Let's fallback to
# cwd=None and see if that works out.
# See https://github.com/The-Compiler/qutebrowser/issues/323
# See https://github.com/qutebrowser/qutebrowser/issues/323
cwd = None
# Add all open pages so they get reopened.
@ -713,6 +722,7 @@ class Quitter:
# Now we can hopefully quit without segfaults
log.destroy.debug("Deferring QApplication::exit...")
objreg.get('signal-handler').deactivate()
objreg.get('session-manager').delete_autosave()
# We use a singleshot timer to exit here to minimize the likelihood of
# segfaults.
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')
else:
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):
@ -147,7 +147,7 @@ class HostBlocker:
with open(filename, 'r', encoding='utf-8') as f:
for line in f:
target.add(line.strip())
except OSError:
except (OSError, UnicodeDecodeError):
log.misc.exception("Failed to read host blocklist!")
return True
@ -165,7 +165,8 @@ class HostBlocker:
if not found:
args = objreg.get('args')
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.")
@cmdutils.register(instance='host-blocker')
@ -205,6 +206,54 @@ class HostBlocker:
download.finished.connect(
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):
"""Read and merge host files.
@ -218,35 +267,18 @@ class HostBlocker:
line_count = 0
try:
f = get_fileobj(byte_io)
except (OSError, UnicodeDecodeError, zipfile.BadZipFile,
zipfile.LargeZipFile) as e:
except (OSError, zipfile.BadZipFile, zipfile.LargeZipFile,
LookupError) as e:
message.error("adblock: Error while reading {}: {} - {}".format(
byte_io.name, e.__class__.__name__, e))
return
for line in f:
line_count += 1
# Remove comments
try:
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:
ok = self._parse_line(line)
if not ok:
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))
if error_count > 0:
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.config import config
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
@ -45,7 +45,7 @@ def create(win_id, parent=None):
# Importing modules here so we don't depend on QtWebEngine without the
# argument and to avoid circular imports.
mode_manager = modeman.instance(win_id)
if objreg.get('args').backend == 'webengine':
if objects.backend == usertypes.Backend.QtWebEngine:
from qutebrowser.browser.webengine import webenginetab
tab_class = webenginetab.WebEngineTab
else:
@ -54,9 +54,9 @@ def create(win_id, parent=None):
return tab_class(win_id=win_id, mode_manager=mode_manager, parent=parent)
def init(args):
def init():
"""Initialize backend-specific modules."""
if args.backend == 'webengine':
if objects.backend == usertypes.Backend.QtWebEngine:
from qutebrowser.browser.webengine import webenginetab
webenginetab.init()
else:
@ -74,6 +74,15 @@ class UnsupportedOperationError(WebTabError):
"""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:
"""A simple namespace with a fixed set of attributes.
@ -96,6 +105,22 @@ class TabData:
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:
"""Attribute of AbstractTab for printing the page."""
@ -109,10 +134,20 @@ class AbstractPrinting:
def check_printer_support(self):
raise NotImplementedError
def check_preview_support(self):
raise NotImplementedError
def to_pdf(self, filename):
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
@ -184,7 +219,7 @@ class AbstractZoom(QObject):
# # FIXME:qtwebengine is this needed?
# # For some reason, this signal doesn't get disconnected automatically
# # when the WebView is destroyed on older PyQt versions.
# # See https://github.com/The-Compiler/qutebrowser/issues/390
# # See https://github.com/qutebrowser/qutebrowser/issues/390
# self.destroyed.connect(functools.partial(
# cfg.changed.disconnect, self.init_neighborlist))
@ -217,6 +252,9 @@ class AbstractZoom(QObject):
self.set_factor(float(level) / 100, fuzzyval=False)
return level
def _set_factor_internal(self, factor):
raise NotImplementedError
def set_factor(self, factor, *, fuzzyval=True):
"""Zoom to a given zoom factor.
@ -485,10 +523,6 @@ class AbstractTab(QWidget):
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:
history: The AbstractHistory for the current 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
given URL.
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()
@ -521,8 +562,8 @@ class AbstractTab(QWidget):
shutting_down = pyqtSignal()
contents_size_changed = pyqtSignal(QSizeF)
add_history_item = pyqtSignal(QUrl, QUrl, str) # url, requested url, title
WIDGET_CLASS = None
fullscreen_requested = pyqtSignal(bool)
renderer_process_terminated = pyqtSignal(TerminationStatus, int)
def __init__(self, win_id, mode_manager, parent=None):
self.win_id = win_id
@ -543,6 +584,7 @@ class AbstractTab(QWidget):
# self.search = AbstractSearch(parent=self)
# self.printing = AbstractPrinting()
# self.elements = AbstractElements(self)
# self.action = AbstractAction()
self.data = TabData()
self._layout = miscwidgets.WrapperLayout(self)
@ -552,7 +594,7 @@ class AbstractTab(QWidget):
self._mode_manager = mode_manager
self._load_status = usertypes.LoadStatus.none
self._mouse_event_filter = mouse.MouseEventFilter(
self, widget_class=self.WIDGET_CLASS, parent=self)
self, parent=self)
self.backend = None
# FIXME:qtwebengine Should this be public api via self.hints?
@ -571,8 +613,11 @@ class AbstractTab(QWidget):
self.zoom._widget = widget
self.search._widget = widget
self.printing._widget = widget
self.action._widget = widget
self.elements._widget = widget
self._install_event_filter()
self.zoom.set_default()
def _install_event_filter(self):
raise NotImplementedError
@ -585,7 +630,7 @@ class AbstractTab(QWidget):
self._load_status = val
self.load_status_changed.emit(val.name)
def _event_target(self):
def event_target(self):
"""Return the widget events should be sent to."""
raise NotImplementedError
@ -600,7 +645,7 @@ class AbstractTab(QWidget):
if getattr(evt, 'posted', False):
raise AssertionError("Can't re-use an event which was already "
"posted!")
recipient = self._event_target()
recipient = self.event_target()
evt.posted = True
QApplication.postEvent(recipient, evt)
@ -641,12 +686,14 @@ class AbstractTab(QWidget):
@pyqtSlot(bool)
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 self.url().scheme() == 'https':
self._set_load_status(usertypes.LoadStatus.success_https)
else:
self._set_load_status(usertypes.LoadStatus.success)
elif ok:
self._set_load_status(usertypes.LoadStatus.warn)
else:
@ -737,6 +784,14 @@ class AbstractTab(QWidget):
"""
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):
try:
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.browser import (urlmarks, browsertab, inspector, navigate,
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.utils import (message, usertypes, log, qtutils, urlutils,
objreg, utils, typing)
@ -310,13 +304,10 @@ class CommandDispatcher:
count: The tab index to open the URL in, or None.
"""
if url is None:
if tab or bg or window:
urls = [config.get('general', 'default-page')]
else:
raise cmdexc.CommandError("No URL given, but -t/-b/-w is not "
"set!")
urls = [config.get('general', 'default-page')]
else:
urls = self._parse_url_input(url)
for i, cur_url in enumerate(urls):
if not window and i > 0:
tab = False
@ -407,6 +398,43 @@ class CommandDispatcher:
if tab is not None:
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',
scope='window')
@cmdutils.argument('count', count=True)
@ -428,28 +456,17 @@ class CommandDispatcher:
tab.printing.check_pdf_support()
else:
tab.printing.check_printer_support()
if preview:
tab.printing.check_preview_support()
except browsertab.WebTabError as e:
raise cmdexc.CommandError(e)
if preview:
diag = QPrintPreviewDialog()
diag.setAttribute(Qt.WA_DeleteOnClose)
diag.setWindowFlags(diag.windowFlags() |
Qt.WindowMaximizeButtonHint |
Qt.WindowMinimizeButtonHint)
diag.paintRequested.connect(tab.printing.to_printer)
diag.exec_()
self._print_preview(tab)
elif pdf:
pdf = os.path.expanduser(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))
self._print_pdf(tab, pdf)
else:
diag = QPrintDialog()
diag.setAttribute(Qt.WA_DeleteOnClose)
diag.open(lambda: tab.printing.to_printer(diag.printer()))
self._print(tab)
@cmdutils.register(instance='command-dispatcher', scope='window')
def tab_clone(self, bg=False, window=False):
@ -465,6 +482,11 @@ class CommandDispatcher:
cmdutils.check_exclusive((bg, window), 'bw')
curtab = self._current_widget()
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
# tabs-are-windows being set)
if window:
@ -475,13 +497,15 @@ class CommandDispatcher:
new_tabbed_browser = objreg.get('tabbed-browser', scope='window',
window=newtab.win_id)
idx = new_tabbed_browser.indexOf(newtab)
new_tabbed_browser.set_page_title(idx, cur_title)
if config.get('tabs', 'show-favicons'):
new_tabbed_browser.setTabIcon(idx, curtab.icon())
if config.get('tabs', 'tabs-are-windows'):
new_tabbed_browser.window().setWindowIcon(curtab.icon())
newtab.data.keep_icon = True
newtab.history.deserialize(curtab.history.serialize())
newtab.history.deserialize(history)
newtab.zoom.set_factor(curtab.zoom.factor())
return newtab
@ -626,6 +650,9 @@ class CommandDispatcher:
def scroll(self, direction: typing.Union[str, int], count=1):
"""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:
direction: In which direction to scroll
(up/down/left/right/top/bottom).
@ -710,7 +737,7 @@ class CommandDispatcher:
"""
tab = self._current_widget()
if not tab.url().isValid():
# See https://github.com/The-Compiler/qutebrowser/issues/701
# See https://github.com/qutebrowser/qutebrowser/issues/701
return
if bottom_navigate is not None and tab.scroller.at_bottom():
@ -813,7 +840,7 @@ class CommandDispatcher:
perc = tab.zoom.offset(count)
except ValueError as 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.argument('count', count=True)
@ -828,7 +855,7 @@ class CommandDispatcher:
perc = tab.zoom.offset(-count)
except ValueError as 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.argument('count', count=True)
@ -852,7 +879,7 @@ class CommandDispatcher:
tab.zoom.set_factor(float(level) / 100)
except ValueError:
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')
def tab_only(self, prev=False, next_=False):
@ -891,7 +918,7 @@ class CommandDispatcher:
"""
if self._count() == 0:
# 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
newidx = self._current_index() - count
if newidx >= 0:
@ -911,7 +938,7 @@ class CommandDispatcher:
"""
if self._count() == 0:
# 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
newidx = self._current_index() + count
if newidx < self._count():
@ -1014,7 +1041,7 @@ class CommandDispatcher:
@cmdutils.register(instance='command-dispatcher', scope='window')
@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):
"""Select the tab given as argument/[count].
@ -1027,7 +1054,6 @@ class CommandDispatcher:
Negative indices count from the end, such that -1 is the
last tab.
count: The tab index to focus, starting with 1.
The special value 0 focuses the rightmost tab.
"""
if index == 'last':
self._tab_focus_last()
@ -1037,9 +1063,8 @@ class CommandDispatcher:
if index is None:
self.tab_next()
return
elif index == 0:
index = self._count()
elif index < 0:
if index < 0:
index = self._count() + index + 1
if 1 <= index <= self._count():
@ -1088,21 +1113,10 @@ class CommandDispatcher:
raise cmdexc.CommandError("Can't move tab to position {}!".format(
new_idx + 1))
tab = self._current_widget()
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(new_idx, 'int')
self._tabbed_browser.setUpdatesEnabled(False)
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)
self._tabbed_browser.tabBar().moveTab(cur_idx, new_idx)
@cmdutils.register(instance='command-dispatcher', scope='window',
maxsplit=0, no_replace_variables=True)
@ -1373,58 +1387,39 @@ class CommandDispatcher:
# FIXME:qtwebengine do this with the QtWebEngine download manager?
download_manager = objreg.get('qtnetwork-download-manager',
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 mhtml_:
raise cmdexc.CommandError("Can only download the current page"
" as mhtml.")
url = urlutils.qurl_from_user_input(url)
urlutils.raise_cmdexc_if_invalid(url)
if dest is None:
target = None
else:
target = downloads.FileDownloadTarget(dest)
download_manager.get(url, target=target)
download_manager.get(url, user_agent=user_agent, target=target)
elif mhtml_:
self._download_mhtml(dest)
else:
qnam = self._current_widget().networkaccessmanager()
if dest is None:
target = None
tab = self._current_widget()
if tab.backend == usertypes.Backend.QtWebEngine:
webengine_download_manager = objreg.get(
'webengine-download-manager')
try:
webengine_download_manager.get_mhtml(tab, target)
except browsertab.UnsupportedOperationError as e:
raise cmdexc.CommandError(e)
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()
if tab.backend == usertypes.Backend.QtWebEngine:
raise cmdexc.CommandError("Download --mhtml is not implemented "
"with QtWebEngine yet")
if dest is None:
suggested_fn = self._current_title() + ".mht"
suggested_fn = utils.sanitize_filename(suggested_fn)
filename = downloads.immediate_download_path()
if filename is not None:
mhtml.start_download_checked(filename, 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)
download_manager.get_mhtml(tab, target)
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')
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
# WORKAROUND for https://bitbucket.org/logilab/pylint/issue/491/
tab = self._current_widget()
@ -1471,6 +1466,18 @@ class CommandDispatcher:
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',
scope='window')
@cmdutils.argument('topic', completion=usertypes.Completion.helptopic)
@ -1544,6 +1551,10 @@ class CommandDispatcher:
return
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.editing_finished.connect(functools.partial(
self.on_editing_finished, elem))
@ -1612,7 +1623,8 @@ class CommandDispatcher:
@cmdutils.argument('filter_', choices=['id'])
def click_element(self, filter_: str, value, *,
target: usertypes.ClickTarget=
usertypes.ClickTarget.normal):
usertypes.ClickTarget.normal,
force_event=False):
"""Click the element matching the given filter.
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.
value: The value to filter for.
target: How to open the clicked element (normal/tab/tab-bg/window).
force_event: Force generating a fake click event.
"""
tab = self._current_widget()
@ -1632,7 +1645,7 @@ class CommandDispatcher:
message.error("No element found with id {}!".format(value))
return
try:
elem.click(target)
elem.click(target, force_event=force_event)
except webelem.Error as e:
message.error(str(e))
return
@ -1972,12 +1985,13 @@ class CommandDispatcher:
@cmdutils.register(instance='command-dispatcher', scope='window',
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):
"""Evaluate a JavaScript string.
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.
world: Ignored on QtWebKit. On QtWebEngine, a world ID or name to
run the snippet in.
@ -2005,6 +2019,13 @@ class CommandDispatcher:
out = out[:5000] + ' [...trimmed...]'
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.run_js_async(js_code, callback=jseval_cb, world=world)
@ -2038,11 +2059,6 @@ class CommandDispatcher:
QApplication.postEvent(window, press_event)
QApplication.postEvent(window, release_event)
else:
try:
tab = objreg.get('tab', scope='tab', tab='current')
except objreg.RegistryUnavailableError:
raise cmdexc.CommandError("No focused webview!")
tab = self._current_widget()
tab.send_event(press_event)
tab.send_event(release_event)
@ -2112,3 +2128,24 @@ class CommandDispatcher:
"""
if bg or tab or window or url != old_url:
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."""
import sys
import shlex
import html
import os.path
import collections
@ -28,15 +27,13 @@ import functools
import tempfile
import sip
from PyQt5.QtCore import (pyqtSlot, pyqtSignal, Qt, QObject, QUrl, QModelIndex,
from PyQt5.QtCore import (pyqtSlot, pyqtSignal, Qt, QObject, QModelIndex,
QTimer, QAbstractListModel)
from PyQt5.QtGui import QDesktopServices
from qutebrowser.commands import cmdexc, cmdutils
from qutebrowser.config import config
from qutebrowser.utils import (usertypes, standarddir, utils, message, log,
qtutils)
from qutebrowser.misc import guiprocess
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.
"""
# 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()
filename = utils.force_encoding(filename, 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.text = "Please enter a location for <b>{}</b>".format(
html.escape(url.toDisplayString()))
q.mode = usertypes.PromptMode.text
q.mode = usertypes.PromptMode.download
q.completed.connect(q.deleteLater)
q.default = _path_suggestion(suggested_filename)
return q
@ -197,6 +194,9 @@ class FileDownloadTarget(_DownloadTarget):
def suggested_filename(self):
return os.path.basename(self.filename)
def __str__(self):
return self.filename
class FileObjDownloadTarget(_DownloadTarget):
@ -216,6 +216,12 @@ class FileObjDownloadTarget(_DownloadTarget):
except AttributeError:
raise NoFilenameError
def __str__(self):
try:
return 'file object at {}'.format(self.fileobj.name)
except AttributeError:
return 'anonymous file object'
class OpenFileDownloadTarget(_DownloadTarget):
@ -234,6 +240,9 @@ class OpenFileDownloadTarget(_DownloadTarget):
def suggested_filename(self):
raise NoFilenameError
def __str__(self):
return 'temporary file'
class DownloadItemStats(QObject):
@ -512,30 +521,19 @@ class AbstractDownloadItem(QObject):
Args:
cmdline: The command to use as string. A `{}` is expanded to the
filename. None means to use the system's default
application. If no `{}` is found, the filename is appended
to the cmdline.
application or `default-open-dispatcher` if set. If no
`{}` is found, the filename is appended to the cmdline.
"""
assert self.successful
filename = self._get_open_filename()
if filename is None: # pragma: no cover
log.downloads.error("No filename to open the download!")
return
if cmdline is None:
log.downloads.debug("Opening {} with the system application"
.format(filename))
url = QUrl.fromLocalFile(filename)
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)
# By using a singleshot timer, we ensure that we return fast. This
# is important on systems where process creation takes long, as
# otherwise the prompt might hang around and cause bugs
# (see issue #2296)
QTimer.singleShot(0, lambda: utils.open_file(filename, cmdline))
def _ensure_can_set_filename(self, filename):
"""Make sure we can still set a filename."""
@ -564,13 +562,16 @@ class AbstractDownloadItem(QObject):
"""Set a temporary file when opening the download."""
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.
Args:
filename: The full filename to save the download to.
None: special value to stop the download.
force_overwrite: Force overwriting existing files.
remember_directory: If True, remember the directory for future
downloads.
"""
global last_used_directory
filename = os.path.expanduser(filename)
@ -600,7 +601,8 @@ class AbstractDownloadItem(QObject):
os.path.expanduser('~'))
self.basename = os.path.basename(self._filename)
last_used_directory = os.path.dirname(self._filename)
if remember_directory:
last_used_directory = os.path.dirname(self._filename)
log.downloads.debug("Setting filename to {}".format(filename))
if force_overwrite:
@ -743,7 +745,7 @@ class AbstractDownloadManager(QObject):
def _remove_item(self, download):
"""Remove a given download."""
if sip.isdeleted(self):
# https://github.com/The-Compiler/qutebrowser/issues/1242
# https://github.com/qutebrowser/qutebrowser/issues/1242
return
try:
idx = self.downloads.index(download)
@ -767,7 +769,6 @@ class AbstractDownloadManager(QObject):
def _init_filename_question(self, question, download):
"""Set up an existing filename question with a download."""
question.mode = usertypes.PromptMode.download
question.answered.connect(download.set_target)
question.cancelled.connect(download.cancel)
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.
Original bug: https://github.com/The-Compiler/qutebrowser/issues/167
Workaround bug: https://github.com/The-Compiler/qutebrowser/issues/171
Original bug: https://github.com/qutebrowser/qutebrowser/issues/167
Workaround bug: https://github.com/qutebrowser/qutebrowser/issues/171
"""
def _update_geometry():
"""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.
"""
if (config.get('hints', 'uppercase') and
self._context.hint_mode == 'letter'):
self._context.hint_mode in ['letter', 'word']):
matched = html.escape(matched.upper())
unmatched = html.escape(unmatched.upper())
else:
@ -235,7 +235,10 @@ class HintActions:
sel = (context.target == Target.yank_primary and
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)
msg = "Yanked URL to {}: {}".format(
@ -284,11 +287,13 @@ class HintActions:
prompt = False if context.rapid else None
qnam = context.tab.networkaccessmanager()
user_agent = context.tab.user_agent()
# FIXME:qtwebengine do this with QtWebEngine downloads?
download_manager = objreg.get('qtnetwork-download-manager',
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):
"""Call a userscript from a hint.
@ -311,7 +316,7 @@ class HintActions:
try:
userscripts.run_async(context.tab, cmd, *args, win_id=self._win_id,
env=env)
except userscripts.UnsupportedError as e:
except userscripts.Error as e:
raise HintingError(str(e))
def spawn(self, url, context):
@ -567,6 +572,10 @@ class HintManager(QObject):
def _start_cb(self, elems):
"""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:
message.error("There was an error while getting hint elements")
return
@ -750,6 +759,9 @@ class HintManager(QObject):
def handle_partial_key(self, keystr):
"""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))
for string, label in self._context.labels.items():
try:

View File

@ -26,9 +26,9 @@ from PyQt5.QtCore import pyqtSignal, pyqtSlot, QUrl, QObject
from qutebrowser.commands import cmdutils
from qutebrowser.utils import (utils, objreg, standarddir, log, qtutils,
usertypes)
usertypes, message)
from qutebrowser.config import config
from qutebrowser.misc import lineparser
from qutebrowser.misc import lineparser, objects
class Entry:
@ -88,7 +88,7 @@ class Entry:
if not url.isValid():
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')
if '-' in atime:
@ -230,13 +230,23 @@ class WebHistory(QObject):
self._saved_count = len(self._new_history)
@cmdutils.register(name='history-clear', instance='web-history')
def clear(self):
def clear(self, force=False):
"""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.
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.history_dict.clear()
self._temp_history.clear()
@ -293,7 +303,6 @@ def init(parent=None):
parent=parent)
objreg.register('web-history', history)
used_backend = usertypes.arg2backend[objreg.get('args').backend]
if used_backend == usertypes.Backend.QtWebKit:
if objects.backend == usertypes.Backend.QtWebKit:
from qutebrowser.browser.webkit import webkithistory
webkithistory.init(history)

View File

@ -24,9 +24,8 @@ import binascii
from PyQt5.QtWidgets import QWidget
from qutebrowser.utils import log, objreg
from qutebrowser.misc import miscwidgets
from qutebrowser.config import config
from qutebrowser.utils import log, objreg, usertypes
from qutebrowser.misc import miscwidgets, objects
def create(parent=None):
@ -37,7 +36,7 @@ def create(parent=None):
"""
# Importing modules here so we don't depend on QtWebEngine without the
# argument and to avoid circular imports.
if objreg.get('args').backend == 'webengine':
if objects.backend == usertypes.Backend.QtWebEngine:
from qutebrowser.browser.webengine import webengineinspector
return webengineinspector.WebEngineInspector(parent)
else:
@ -91,13 +90,6 @@ class AbstractWebInspector(QWidget):
state_config['geometry']['inspector'] = geom
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):
"""Inspect the given QWeb(Engine)Page."""
raise NotImplementedError

View File

@ -64,8 +64,6 @@ class MouseEventFilter(QObject):
"""Handle mouse events on a tab.
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.
_handlers: A dict of handler functions for the handled events.
_ignore_wheel_event: Whether to ignore the next wheelEvent.
@ -73,9 +71,8 @@ class MouseEventFilter(QObject):
done when the mouse is released.
"""
def __init__(self, tab, *, widget_class, parent=None):
def __init__(self, tab, *, parent=None):
super().__init__(parent)
self._widget_class = widget_class
self._tab = tab
self._handlers = {
QEvent.MouseButtonPress: self._handle_mouse_press,
@ -96,7 +93,10 @@ class MouseEventFilter(QObject):
return 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
@ -114,18 +114,26 @@ class MouseEventFilter(QObject):
e: The QWheelEvent.
"""
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
return True
if e.modifiers() & Qt.ControlModifier:
divider = config.get('input', 'mouse-zoom-divider')
if divider == 0:
return False
factor = self._tab.zoom.factor() + (e.angleDelta().y() / divider)
if factor < 0:
return False
perc = int(100 * factor)
message.info("Zoom level: {}%".format(perc))
message.info("Zoom level: {}%".format(perc), replace=True)
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
@ -201,9 +209,8 @@ class MouseEventFilter(QObject):
evtype = event.type()
if evtype not in self._handlers:
return False
if not isinstance(obj, self._widget_class):
log.mouse.debug("Ignoring {} to {} which is not an instance of "
"{}".format(event.__class__.__name__, obj,
self._widget_class))
if obj is not self._tab.event_target():
log.mouse.debug("Ignoring {} to {}".format(
event.__class__.__name__, obj))
return False
return self._handlers[evtype](event)

View File

@ -70,11 +70,11 @@ def path_up(url, count):
def _find_prevnext(prev, elems):
"""Find a prev/next element in the given list of elements."""
# 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:
if e.tag_name() != 'link' or 'rel' not in e:
if e.tag_name() not in ['link', 'a'] or 'rel' not in e:
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']))
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 qutebrowser.config import config, configtypes
from qutebrowser.utils import objreg
from qutebrowser.browser.network import pac
def init():
"""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):
"""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):
"""Get the QNetworkProxies for a query.
@ -46,6 +62,8 @@ class ProxyFactory(QNetworkProxyFactory):
proxy = config.get('network', 'proxy')
if proxy is configtypes.SYSTEM_PROXY:
proxies = QNetworkProxyFactory.systemProxyForQuery(query)
elif isinstance(proxy, pac.PACFetcher):
proxies = proxy.resolve(query)
else:
proxies = [proxy]
for p in proxies:

View File

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

View File

@ -27,7 +27,7 @@ import collections
from PyQt5.QtCore import pyqtSlot, pyqtSignal, QTimer
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.webkit import http
from qutebrowser.browser.webkit.network import networkmanager
@ -366,11 +366,12 @@ class DownloadManager(downloads.AbstractDownloadManager):
win_id, None, self)
@pyqtSlot('QUrl')
def get(self, url, **kwargs):
def get(self, url, *, user_agent=None, **kwargs):
"""Start a download with a link URL.
Args:
url: The URL to get, as QUrl
user_agent: The UA to set for the request, or None.
**kwargs: passed to get_request().
Return:
@ -380,8 +381,32 @@ class DownloadManager(downloads.AbstractDownloadManager):
urlutils.invalid_url_error(url, "start download")
return
req = QNetworkRequest(url)
if user_agent is not None:
req.setHeader(QNetworkRequest.UserAgentHeader, user_agent)
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):
"""Start a download with a QNetworkRequest.

View File

@ -24,11 +24,17 @@ Module attributes:
_HANDLERS: The handlers registered via decorators.
"""
import sys
import time
import datetime
import urllib.parse
from PyQt5.QtCore import QUrlQuery
import qutebrowser
from qutebrowser.utils import (version, utils, jinja, log, message, docutils,
objreg, usertypes)
objreg)
from qutebrowser.misc import objects
pyeval_output = ":pyeval was never called"
@ -89,8 +95,7 @@ class add_handler: # pylint: disable=invalid-name
return function
def wrapper(self, *args, **kwargs):
used_backend = usertypes.arg2backend[objreg.get('args').backend]
if self._backend is not None and used_backend != self._backend:
if self._backend is not None and objects.backend != self._backend:
return self.wrong_backend_handler(*args, **kwargs)
else:
return self._function(*args, **kwargs)
@ -158,6 +163,87 @@ def qute_bookmarks(_url):
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')
def qute_pyeval(_url):
"""Handler for qute:pyeval."""

View File

@ -66,6 +66,7 @@ def authentication_required(url, authenticator, abort_on):
if answer is not None:
authenticator.setUser(answer.user)
authenticator.setPassword(answer.password)
return answer
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")
for err in errors:
# 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))
return 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 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',
@ -119,10 +120,6 @@ class AbstractWebElement(collections.abc.MutableMapping):
"""Get the geometry for this element."""
raise NotImplementedError
def style_property(self, name, *, strategy):
"""Get the element style resolved with the given strategy."""
raise NotImplementedError
def classes(self):
"""Get a list of classes assigned to this element."""
raise NotImplementedError
@ -139,7 +136,7 @@ class AbstractWebElement(collections.abc.MutableMapping):
raise NotImplementedError
def value(self):
"""Get the value attribute for this element."""
"""Get the value attribute for this element, or None."""
raise NotImplementedError
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
elements with "display:block" style, see
https://github.com/The-Compiler/qutebrowser/issues/1298
https://github.com/qutebrowser/qutebrowser/issues/1298
Args:
elem_geometry: The geometry of the element, or None.
@ -222,18 +219,22 @@ class AbstractWebElement(collections.abc.MutableMapping):
else:
return False
def _is_editable_div(self):
"""Check if a div-element is editable.
def _is_editable_classes(self):
"""Check if an element is editable based on its classes.
Return:
True if the element is editable, False otherwise.
"""
# Beginnings of div-classes which are actually some kind of editor.
div_classes = ('CodeMirror', # Javascript editor over a textarea
'kix-', # Google Docs editor
'ace_') # http://ace.c9.io/
classes = {
'div': ['CodeMirror', # Javascript editor over a textarea
'kix-', # Google Docs editor
'ace_'], # http://ace.c9.io/
'pre': ['CodeMirror'],
}
relevant_classes = classes[self.tag_name()]
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 False
@ -264,10 +265,9 @@ class AbstractWebElement(collections.abc.MutableMapping):
return config.get('input', 'insert-mode-on-plugins') and not strict
elif tag == 'object':
return self._is_editable_object() and not strict
elif tag == 'div':
return self._is_editable_div() and not strict
else:
return False
elif tag in ['div', 'pre']:
return self._is_editable_classes() and not strict
return False
def is_text_input(self):
"""Check if this element is some kind of text box."""
@ -311,7 +311,7 @@ class AbstractWebElement(collections.abc.MutableMapping):
# 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
# 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()
if rect.width() > rect.height():
rect.setWidth(rect.height())
@ -322,14 +322,12 @@ class AbstractWebElement(collections.abc.MutableMapping):
raise Error("Element position is out of view!")
return pos
def click(self, click_target):
"""Simulate a click on the element."""
# FIXME:qtwebengine do we need this?
# self._widget.setFocus()
# For QtWebKit
self._tab.data.override_target = click_target
def _move_text_cursor(self):
"""Move cursor to end after clicking."""
raise NotImplementedError
def _click_fake_event(self, click_target):
"""Send a fake click event to the element."""
pos = self._mouse_pos()
log.webelem.debug("Sending fake click to {!r} at position {} with "
@ -358,11 +356,74 @@ class AbstractWebElement(collections.abc.MutableMapping):
for evt in events:
self._tab.send_event(evt)
def after_click():
"""Move cursor to end after clicking."""
if self.is_text_input() and self.is_editable():
self._tab.caret.move_to_end_of_document()
QTimer.singleShot(0, after_click)
QTimer.singleShot(0, self._move_text_cursor)
def _click_editable(self, click_target):
"""Fake a click on an editable input field."""
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):
"""Simulate a mouse hover over the element."""

View File

@ -19,7 +19,9 @@
"""QtWebEngine specific code for downloads."""
import re
import os.path
import urllib
import functools
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
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):
@ -45,6 +47,11 @@ class DownloadItem(downloads.AbstractDownloadItem):
qt_item.downloadProgress.connect(self.stats.on_download_progress)
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)
def _on_state_changed(self, state):
state_name = debug.qenum_key(QWebEngineDownloadItem, state)
@ -57,6 +64,9 @@ class DownloadItem(downloads.AbstractDownloadItem):
pass
elif state == QWebEngineDownloadItem.DownloadCompleted:
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.done = True
self.finished.emit()
@ -94,7 +104,8 @@ class DownloadItem(downloads.AbstractDownloadItem):
raise downloads.UnsupportedOperationError
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):
state = self._qt_item.state()
@ -122,9 +133,38 @@ class DownloadItem(downloads.AbstractDownloadItem):
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):
"""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):
"""Set up the download manager on a QWebEngineProfile."""
@ -134,12 +174,17 @@ class DownloadManager(downloads.AbstractDownloadManager):
@pyqtSlot(QWebEngineDownloadItem)
def handle_download(self, qt_item):
"""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)
self._init_item(download, auto_remove=False,
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()
if filename is not None:
# 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)
# The filename is set via the question.answered signal, connected in
# _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."""
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.browser import webelem
@ -51,9 +56,7 @@ class WebEngineElement(webelem.AbstractWebElement):
def __setitem__(self, key, val):
self._js_dict['attributes'][key] = val
js_code = javascript.assemble('webelem', 'set_attribute', self._id,
key, val)
self._tab.run_js_async(js_code)
self._js_call('set_attribute', key, val)
def __delitem__(self, key):
log.stub()
@ -64,6 +67,11 @@ class WebEngineElement(webelem.AbstractWebElement):
def __len__(self):
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):
return True
@ -71,10 +79,6 @@ class WebEngineElement(webelem.AbstractWebElement):
log.stub()
return QRect()
def style_property(self, name, *, strategy):
log.stub()
return ''
def classes(self):
"""Get a list of classes assigned to this element."""
return self._js_dict['class_name'].split()
@ -91,25 +95,23 @@ class WebEngineElement(webelem.AbstractWebElement):
return self._js_dict['outer_xml']
def value(self):
return self._js_dict['value']
return self._js_dict.get('value', None)
def set_value(self, value):
js_code = javascript.assemble('webelem', 'set_value', self._id, value)
self._tab.run_js_async(js_code)
self._js_call('set_value', value)
def insert_text(self, text):
if not self.is_editable(strict=True):
raise webelem.Error("Element is not editable!")
log.webelem.debug("Inserting text into element {!r}".format(self))
js_code = javascript.assemble('webelem', 'insert_text', self._id, text)
self._tab.run_js_async(js_code)
self._js_call('insert_text', text)
def rect_on_view(self, *, elem_geometry=None, no_js=False):
"""Get the geometry of the element relative to the webview.
Skipping of small rectangles is due to <a> elements containing other
elements with "display:block" style, see
https://github.com/The-Compiler/qutebrowser/issues/1298
https://github.com/qutebrowser/qutebrowser/issues/1298
Args:
elem_geometry: The geometry of the element, or None.
@ -146,6 +148,44 @@ class WebEngineElement(webelem.AbstractWebElement):
return QRect()
def remove_blank_target(self):
js_code = javascript.assemble('webelem', 'remove_blank_target',
self._id)
self._tab.run_js_async(js_code)
if self._js_dict['attributes'].get('target') == '_blank':
self._js_dict['attributes']['target'] = '_top'
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):
"""Set up the inspector."""
self._check_developer_extras()
try:
port = int(os.environ['QTWEBENGINE_REMOTE_DEBUGGING'])
except KeyError:
raise inspector.WebInspectorError(
"Debugging is not set up correctly. Did you restart after "
"setting developer-extras?")
"Debugging is not enabled. See 'qutebrowser --help' for "
"details.")
url = QUrl('http://localhost:{}/'.format(port))
self._widget.load(url)
self.show()

View File

@ -25,6 +25,7 @@ Module attributes:
"""
import os
import logging
# pylint: disable=no-name-in-module,import-error,useless-suppression
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
from qutebrowser.browser import shared
from qutebrowser.config import websettings, config
from qutebrowser.utils import objreg, utils, standarddir, javascript
from qutebrowser.config import config, websettings
from qutebrowser.utils import objreg, utils, standarddir, javascript, log
class Attribute(websettings.Attribute):
@ -65,6 +66,47 @@ class StaticSetter(websettings.StaticSetter):
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):
"""Initialize custom stylesheets.
@ -98,6 +140,13 @@ def _init_stylesheet(profile):
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):
"""Update global settings when qwebsettings changed."""
websettings.update_mappings(MAPPINGS, section, option)
@ -106,17 +155,31 @@ def update_settings(section, option):
_init_stylesheet(profile)
def init():
def init(args):
"""Initialize the global QWebSettings."""
if config.get('general', 'developer-extras'):
# FIXME:qtwebengine Make sure we call globalSettings *after* this...
if args.enable_webengine_inspector:
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.setCachePath(os.path.join(standarddir.cache(), 'webengine'))
profile.setPersistentStoragePath(
os.path.join(standarddir.data(), 'webengine'))
_init_profile(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)
objreg.get('config').changed.connect(update_settings)
@ -128,24 +191,17 @@ def shutdown():
# Missing QtWebEngine attributes:
# - ErrorPageEnabled (should not be exposed, but set)
# - FullScreenSupportEnabled
# - ScreenCaptureEnabled
# - Accelerated2dCanvasEnabled
# - AutoLoadIconsForPage
# - TouchIconsEnabled
# - FocusOnNavigationEnabled (5.8)
# - AllowRunningInsecureContent (5.8)
#
# Missing QtWebEngine fonts:
# - FantasyFont
# - PictographFont
#
# TODO settings on profile:
# - httpCacheMaximumSize
# - persistentCookiesPolicy
# - offTheRecord
#
# TODO settings elsewhere:
# - proxy
MAPPINGS = {
'content': {
@ -165,6 +221,11 @@ MAPPINGS = {
Attribute(QWebEngineSettings.LocalContentCanAccessRemoteUrls),
'local-content-can-access-file-urls':
Attribute(QWebEngineSettings.LocalContentCanAccessFileUrls),
# https://bugreports.qt.io/browse/QTBUG-58650
# 'cookies-store':
# PersistentCookiePolicy(),
'webgl':
Attribute(QWebEngineSettings.WebGLEnabled),
},
'input': {
'spatial-navigation':
@ -221,6 +282,9 @@ MAPPINGS = {
'storage': {
'local-storage':
Attribute(QWebEngineSettings.LocalStorageEnabled),
'cache-size':
ProfileSetter(getter='httpCacheMaximumSize',
setter='setHttpCacheMaximumSize')
},
'general': {
'xss-auditing':
@ -232,7 +296,8 @@ MAPPINGS = {
}
try:
MAPPINGS['content']['webgl'] = Attribute(QWebEngineSettings.WebGLEnabled)
MAPPINGS['general']['print-element-backgrounds'] = Attribute(
QWebEngineSettings.PrintElementBackgrounds)
except AttributeError:
# Added in Qt 5.7
# Added in Qt 5.8
pass

View File

@ -24,10 +24,12 @@
import functools
import sip
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
from PyQt5.QtWidgets import QOpenGLWidget, QApplication
from PyQt5.QtWidgets import QApplication
from PyQt5.QtWebEngineWidgets import (QWebEnginePage, QWebEngineScript,
QWebEngineProfile)
# 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,
interceptor, webenginequtescheme,
webenginedownloads)
from qutebrowser.misc import miscwidgets
from qutebrowser.utils import (usertypes, qtutils, log, javascript, utils,
objreg)
objreg, jinja)
_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):
"""QtWebEngine implementations related to printing."""
def check_pdf_support(self):
if not hasattr(self._widget.page(), 'printToPdf'):
raise browsertab.WebTabError(
"Printing to PDF is unsupported with QtWebEngine on Qt < 5.7")
return True
def check_printer_support(self):
if not hasattr(self._widget.page(), 'print'):
raise browsertab.WebTabError(
"Printing is unsupported with QtWebEngine on Qt < 5.8")
def check_preview_support(self):
raise browsertab.WebTabError(
"Printing is unsupported with QtWebEngine")
"Print previews are unsupported with QtWebEngine")
def to_pdf(self, filename):
self._widget.page().printToPdf(filename)
def to_printer(self, printer):
# Should never be called
assert False
def to_printer(self, printer, callback=None):
if callback is None:
callback = lambda _ok: None
self._widget.page().print(printer, callback)
class WebEngineSearch(browsertab.AbstractSearch):
@ -223,9 +245,6 @@ class WebEngineScroller(browsertab.AbstractScroller):
"""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):
super().__init__(tab, parent)
self._pos_perc = (0, 0)
@ -235,15 +254,10 @@ class WebEngineScroller(browsertab.AbstractScroller):
def _init_widget(self, widget):
super()._init_widget(widget)
page = widget.page()
try:
page.scrollPositionChanged.connect(self._update_pos)
except AttributeError:
log.stub('scrollPositionChanged, on Qt < 5.7')
self._pos_perc = (None, None)
page.scrollPositionChanged.connect(self._update_pos)
def _key_press(self, key, count=1):
# FIXME:qtwebengine Abort scrolling if the minimum/maximum was reached.
for _ in range(count):
for _ in range(min(count, 5000)):
press_evt = QKeyEvent(QEvent.KeyPress, key, Qt.NoModifier, 0, 0, 0)
release_evt = QKeyEvent(QEvent.KeyRelease, key, Qt.NoModifier,
0, 0, 0)
@ -355,6 +369,11 @@ class WebEngineHistory(browsertab.AbstractHistory):
return self._history.canGoForward()
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)
def deserialize(self, data):
@ -414,7 +433,7 @@ class WebEngineElements(browsertab.AbstractElements):
js_elem: The element serialized from javascript.
"""
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))
if js_elem is None:
@ -442,6 +461,7 @@ class WebEngineElements(browsertab.AbstractElements):
def find_at_pos(self, pos, callback):
assert pos.x() >= 0
assert pos.y() >= 0
pos /= self._tab.zoom.factor()
js_code = javascript.assemble('webelem', 'element_at_pos',
pos.x(), pos.y())
js_cb = functools.partial(self._js_cb_single, callback)
@ -452,8 +472,6 @@ class WebEngineTab(browsertab.AbstractTab):
"""A QtWebEngine tab in the browser."""
WIDGET_CLASS = QOpenGLWidget
def __init__(self, win_id, mode_manager, parent=None):
super().__init__(win_id=win_id, mode_manager=mode_manager,
parent=parent)
@ -466,12 +484,13 @@ class WebEngineTab(browsertab.AbstractTab):
self.search = WebEngineSearch(parent=self)
self.printing = WebEnginePrinting()
self.elements = WebEngineElements(self)
self.action = WebEngineAction()
self._set_widget(widget)
self._connect_signals()
self.backend = usertypes.Backend.QtWebEngine
self._init_js()
self._child_event_filter = None
self.needs_qtbug54419_workaround = False
self._saved_zoom = None
def _init_js(self):
js_code = '\n'.join([
@ -485,13 +504,7 @@ class WebEngineTab(browsertab.AbstractTab):
script.setSourceCode(js_code)
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?
page.scripts().insert(script)
@ -503,7 +516,15 @@ class WebEngineTab(browsertab.AbstractTab):
parent=self)
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):
self._saved_zoom = self.zoom.factor()
self._openurl_prepare(url)
self._widget.load(url)
@ -528,22 +549,16 @@ class WebEngineTab(browsertab.AbstractTab):
else:
world_id = _JS_WORLD_MAP[world]
try:
if callback is None:
self._widget.page().runJavaScript(code, world_id)
else:
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)
if callback is None:
self._widget.page().runJavaScript(code, world_id)
else:
self._widget.page().runJavaScript(code, world_id, callback)
def shutdown(self):
self.shutting_down.emit()
# WORKAROUND for
# https://bugreports.qt.io/browse/QTBUG-58563
self.search.clear()
self._widget.shutdown()
def reload(self, *, force=False):
@ -560,23 +575,24 @@ class WebEngineTab(browsertab.AbstractTab):
return self._widget.title()
def icon(self):
try:
return self._widget.icon()
except AttributeError:
log.stub('on Qt < 5.7')
return QIcon()
return self._widget.icon()
def set_html(self, html, base_url):
def set_html(self, html, base_url=None):
# FIXME:qtwebengine
# check this and raise an exception if too big:
# Warning: The content will be percent encoded before being sent to the
# renderer via IPC. This may increase its size. The maximum size of the
# percent encoded content is 2 megabytes minus 30 bytes.
if base_url is None:
base_url = QUrl()
self._widget.setHtml(html, base_url)
def networkaccessmanager(self):
return None
def user_agent(self):
return None
def clear_ssl_errors(self):
raise browsertab.UnsupportedOperationError
@ -602,31 +618,76 @@ class WebEngineTab(browsertab.AbstractTab):
@pyqtSlot(QUrl, 'QAuthenticator*')
def _on_authentication_required(self, url, authenticator):
# FIXME:qtwebengine support .netrc
shared.authentication_required(url, authenticator,
abort_on=[self.shutting_down,
self.load_started])
answer = shared.authentication_required(
url, authenticator, abort_on=[self.shutting_down,
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):
view = self._widget
page = view.page()
page.windowCloseRequested.connect(self.window_close_requested)
page.linkHovered.connect(self.link_hovered)
page.loadProgress.connect(self._on_load_progress)
page.loadStarted.connect(self._on_load_started)
page.loadFinished.connect(self._on_history_trigger)
view.titleChanged.connect(self.title_changed)
view.urlChanged.connect(self._on_url_changed)
page.loadFinished.connect(self._restore_zoom)
page.loadFinished.connect(self._on_load_finished)
page.certificate_error.connect(self._on_ssl_errors)
page.authenticationRequired.connect(self._on_authentication_required)
try:
view.iconChanged.connect(self.icon_changed)
except AttributeError:
log.stub('iconChanged, on Qt < 5.7')
try:
page.contentsSizeChanged.connect(self.contents_size_changed)
except AttributeError:
log.stub('contentsSizeChanged, on Qt < 5.7')
page.fullScreenRequested.connect(self._on_fullscreen_requested)
page.contentsSizeChanged.connect(self.contents_size_changed)
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()

View File

@ -19,10 +19,10 @@
"""The main browser widget for QtWebEngine."""
import os
import functools
from PyQt5.QtCore import pyqtSignal, pyqtSlot, QUrl, PYQT_VERSION
from PyQt5.QtGui import QPalette
# pylint: disable=no-name-in-module,import-error,useless-suppression
from PyQt5.QtWebEngineWidgets import QWebEngineView, QWebEnginePage
# pylint: enable=no-name-in-module,import-error,useless-suppression
@ -30,8 +30,8 @@ from PyQt5.QtWebEngineWidgets import QWebEngineView, QWebEnginePage
from qutebrowser.browser import shared
from qutebrowser.browser.webengine import certificateerror
from qutebrowser.config import config
from qutebrowser.utils import (log, debug, usertypes, qtutils, jinja, urlutils,
message)
from qutebrowser.utils import (log, debug, usertypes, jinja, urlutils, message,
objreg)
class WebEngineView(QWebEngineView):
@ -42,7 +42,10 @@ class WebEngineView(QWebEngineView):
super().__init__(parent)
self._win_id = win_id
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):
self.page().shutdown()
@ -67,7 +70,7 @@ class WebEngineView(QWebEngineView):
A window without decoration.
QWebEnginePage::WebBrowserBackgroundTab:
A web browser tab without hiding the current visible
WebEngineView. (Added in Qt 5.7)
WebEngineView.
Return:
The new QWebEngineView object.
@ -78,13 +81,6 @@ class WebEngineView(QWebEngineView):
log.webview.debug("createWindow with 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:
# Shift-Alt-Click
target = usertypes.ClickTarget.window
@ -99,7 +95,7 @@ class WebEngineView(QWebEngineView):
target = usertypes.ClickTarget.tab
else:
target = usertypes.ClickTarget.tab_bg
elif wintype == background_tab_wintype:
elif wintype == QWebEnginePage.WebBrowserBackgroundTab:
# Middle-click / Ctrl-Click
if background_tabs:
target = usertypes.ClickTarget.tab_bg
@ -109,15 +105,6 @@ class WebEngineView(QWebEngineView):
raise ValueError("Invalid wintype {}".format(debug_type))
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
@ -127,6 +114,7 @@ class WebEnginePage(QWebEnginePage):
Attributes:
_is_shutting_down: Whether the page is currently shutting down.
_theme_color: The theme background color.
Signals:
certificate_error: Emitted on certificate errors.
@ -136,11 +124,21 @@ class WebEnginePage(QWebEnginePage):
certificate_error = pyqtSignal()
shutting_down = pyqtSignal()
def __init__(self, parent=None):
def __init__(self, theme_color, parent=None):
super().__init__(parent)
self._is_shutting_down = False
self.featurePermissionRequested.connect(
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')
def _on_feature_permission_requested(self, url, feature):

View File

@ -48,6 +48,13 @@ class DiskCache(QNetworkDiskCache):
maxsize=self.maximumCacheSize(),
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):
"""Activate/deactivate the cache based on the config."""
if config.get('general', 'private-browsing'):
@ -55,13 +62,13 @@ class DiskCache(QNetworkDiskCache):
else:
self._activated = True
self.setCacheDirectory(os.path.join(self._cache_dir, 'http'))
self.setMaximumCacheSize(config.get('storage', 'cache-size'))
self._set_cache_size()
@pyqtSlot(str, str)
def on_config_changed(self, section, option):
"""Update cache size/activated if the config was changed."""
if (section, option) == ('storage', 'cache-size'):
self.setMaximumCacheSize(config.get('storage', 'cache-size'))
self._set_cache_size()
elif (section, option) == ('general', # pragma: no branch
'private-browsing'):
self._maybe_activate()

View File

@ -32,6 +32,7 @@ import email.generator
import email.encoders
import email.mime.multipart
import email.message
import quopri
from PyQt5.QtCore import QUrl
@ -138,6 +139,22 @@ def _check_rel(element):
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)
@ -146,7 +163,7 @@ E_BASE64 = email.encoders.encode_base64
# Encode the file using MIME quoted-printable encoding.
E_QUOPRI = email.encoders.encode_quopri
E_QUOPRI = _encode_quopri_mhtml
class MHTMLWriter:
@ -225,7 +242,7 @@ class _Downloader:
Attributes:
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.
loaded_urls: A set of QUrls of finished asset downloads.
pending_downloads: A set of unfinished (url, DownloadItem) tuples.
@ -235,9 +252,9 @@ class _Downloader:
_win_id: The window this downloader belongs to.
"""
def __init__(self, tab, dest):
def __init__(self, tab, target):
self.tab = tab
self.dest = dest
self.target = target
self.writer = None
self.loaded_urls = {tab.url()}
self.pending_downloads = set()
@ -332,8 +349,8 @@ class _Downloader:
# Using the download manager to download host-blocked urls might crash
# qute, see the comments/discussion on
# https://github.com/The-Compiler/qutebrowser/pull/962#discussion_r40256987
# and https://github.com/The-Compiler/qutebrowser/issues/1053
# https://github.com/qutebrowser/qutebrowser/pull/962#discussion_r40256987
# and https://github.com/qutebrowser/qutebrowser/issues/1053
host_blocker = objreg.get('host-blocker')
if host_blocker.is_blocked(url):
log.downloads.debug("Skipping {}, host-blocked".format(url))
@ -445,14 +462,34 @@ class _Downloader:
return
self._finished_file = True
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:
fobj = downloads.temp_download_manager.get_tmpfile(
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 open(self.dest, 'wb') as file_output:
self.writer.write_to(file_output)
with fobj:
self.writer.write_to(fobj)
except OSError as error:
message.error("Could not save file: {}".format(error))
return
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):
"""Collect done downloads and add their data to the MHTML file.
@ -484,34 +521,37 @@ class _NoCloseBytesIO(io.BytesIO):
super().close()
def _start_download(dest, tab):
def _start_download(target, tab):
"""Start downloading the current page and all assets to an MHTML file.
This will overwrite dest if it already exists.
Args:
dest: The filename where the resulting file should be saved.
target: The DownloadTarget where the resulting file should be saved.
tab: Specify the tab whose page should be loaded.
"""
loader = _Downloader(tab, dest)
loader = _Downloader(tab, target)
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.
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.
"""
# 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()
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
encoding = sys.getfilesystemencoding()
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)
@ -532,8 +572,9 @@ def start_download_checked(dest, tab):
message.error("Directory {} does not exist.".format(folder))
return
target = downloads.FileDownloadTarget(path)
if not os.path.isfile(path):
_start_download(path, tab=tab)
_start_download(target, tab=tab)
return
q = usertypes.Question()
@ -543,5 +584,5 @@ def start_download_checked(dest, tab):
html.escape(path))
q.completed.connect(q.deleteLater)
q.answered_yes.connect(functools.partial(
_start_download, path, tab=tab))
_start_download, target, tab=tab))
message.global_bridge.ask(q, blocking=False)

View File

@ -211,7 +211,8 @@ class NetworkManager(QNetworkAccessManager):
request.deleteLater()
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
"""Decide if SSL errors should be ignored or not.
@ -396,6 +397,14 @@ class NetworkManager(QNetworkAccessManager):
Return:
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()
if scheme in self._scheme_handlers:
result = self._scheme_handlers[scheme].createRequest(
@ -426,7 +435,7 @@ class NetworkManager(QNetworkAccessManager):
tab=self._tab_id)
current_url = tab.url()
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
# the middle of the webpage shutdown here.
current_url = QUrl()

View File

@ -74,7 +74,7 @@ class JSBridge(QObject):
@pyqtSlot(str, str, str)
def set(self, sectname, optname, value):
"""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
value == 'false'):
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).
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-]+')
@ -132,7 +132,7 @@ class ValueChars(str):
"""A value of an attribute.
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))

View File

@ -21,21 +21,65 @@
from PyQt5.QtCore import QByteArray, QDataStream, QIODevice, QUrl
from PyQt5.QtWebKit import qWebKitVersion
from qutebrowser.utils import qtutils
HISTORY_STREAM_VERSION = 2
BACK_FORWARD_TREE_VERSION = 2
def _encode_url(url):
"""Encode a QUrl suitable to pass to QWebHistory."""
data = bytes(QUrl.toPercentEncoding(url.toString(), b':/#?&+=@%*'))
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.
Args:
@ -53,7 +97,7 @@ def _serialize_item(i, item, stream):
### Source/WebCore/history/HistoryItem.cpp decodeBackForwardTree
## backForwardTreeEncodingVersion
stream.writeUInt32(BACK_FORWARD_TREE_VERSION)
stream.writeUInt32(2)
## size (recursion stack)
stream.writeUInt64(0)
## node->m_documentSequenceNumber
@ -137,14 +181,12 @@ def serialize(items):
else:
current_idx = 0
### Source/WebKit/qt/Api/qwebhistory.cpp operator<<
stream.writeInt(HISTORY_STREAM_VERSION)
stream.writeInt(len(items))
stream.writeInt(current_idx)
if qtutils.is_qtwebkit_ng(qWebKitVersion()):
_serialize_ng(items, current_idx, stream)
else:
_serialize_old(items, current_idx, stream)
for i, item in enumerate(items):
_serialize_item(i, item, stream)
user_data.append(item.user_data)
user_data += [item.user_data for item in items]
stream.device().reset()
qtutils.check_qdatastream(stream)

View File

@ -20,7 +20,7 @@
"""QtWebKit specific part of the web element API."""
from PyQt5.QtCore import QRect
from PyQt5.QtWebKit import QWebElement
from PyQt5.QtWebKit import QWebElement, QWebSettings
from qutebrowser.config import config
from qutebrowser.utils import log, utils, javascript
@ -96,16 +96,6 @@ class WebKitElement(webelem.AbstractWebElement):
self._check_vanished()
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):
self._check_vanished()
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
# for jsc, and running JS will fail. If that happens, fall back to
# the Python implementation.
# https://github.com/The-Compiler/qutebrowser/issues/1641
# https://github.com/qutebrowser/qutebrowser/issues/1641
return None
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
elements with "display:block" style, see
https://github.com/The-Compiler/qutebrowser/issues/1298
https://github.com/qutebrowser/qutebrowser/issues/1298
Args:
elem_geometry: The geometry of the element, or None.
@ -248,10 +238,13 @@ class WebKitElement(webelem.AbstractWebElement):
hidden_attributes = {
'visibility': 'hidden',
'display': 'none',
'opacity': '0',
}
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
elem_geometry = self._elem.geometry()
if not elem_geometry.isValid() and elem_geometry.x() == 0:
# Most likely an invisible link
@ -297,6 +290,36 @@ class WebKitElement(webelem.AbstractWebElement):
break
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):
"""Get all children recursively of a given QWebFrame.

View File

@ -23,6 +23,7 @@
from PyQt5.QtWebKitWidgets import QWebInspector
from qutebrowser.browser import inspector
from qutebrowser.config import config
class WebKitInspector(inspector.AbstractWebInspector):
@ -35,6 +36,9 @@ class WebKitInspector(inspector.AbstractWebInspector):
self._set_widget(qwebinspector)
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.show()

View File

@ -26,10 +26,10 @@ Module attributes:
import os.path
from PyQt5.QtWebKit import QWebSettings
from PyQt5.QtWebKit import QWebSettings, qWebKitVersion
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
@ -88,28 +88,32 @@ def _set_user_stylesheet():
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):
"""Update global settings when qwebsettings changed."""
if (section, option) == ('general', 'private-browsing'):
cache_path = standarddir.cache()
if config.get('general', 'private-browsing') or cache_path is None:
QWebSettings.setIconDatabasePath('')
else:
QWebSettings.setIconDatabasePath(cache_path)
_init_private_browsing()
elif section == 'ui' and option in ['hide-scrollbar', 'user-stylesheet']:
_set_user_stylesheet()
websettings.update_mappings(MAPPINGS, section, option)
def init():
def init(_args):
"""Initialize the global QWebSettings."""
cache_path = standarddir.cache()
data_path = standarddir.data()
if config.get('general', 'private-browsing'):
QWebSettings.setIconDatabasePath('')
else:
QWebSettings.setIconDatabasePath(cache_path)
_init_private_browsing()
QWebSettings.setOfflineWebApplicationCachePath(
os.path.join(cache_path, 'application-cache'))

View File

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

View File

@ -59,7 +59,7 @@ class WebView(QWebView):
super().__init__(parent)
if sys.platform == 'darwin' and qtutils.version_check('5.4'):
# WORKAROUND for https://bugreports.qt.io/browse/QTBUG-42948
# See https://github.com/The-Compiler/qutebrowser/issues/462
# See https://github.com/qutebrowser/qutebrowser/issues/462
self.setStyle(QStyleFactory.create('Fusion'))
# FIXME:qtwebengine this is only used to set the zoom factor from
# the QWebPage - we should get rid of it somehow (signals?)
@ -108,11 +108,7 @@ class WebView(QWebView):
@config.change_filter('colors', 'webpage.bg')
def _set_bg_color(self):
"""Set the webpage background color as configured.
FIXME:qtwebengine
For QtWebEngine, doing the same has no effect, so we do it in here.
"""
"""Set the webpage background color as configured."""
col = config.get('colors', 'webpage.bg')
palette = self.palette()
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,
usertypes, typing)
from qutebrowser.utils import debug as debug_utils
from qutebrowser.misc import objects
class ArgInfo:
@ -34,14 +35,11 @@ class ArgInfo:
"""Information about an argument."""
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:
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.count = count
self.zero_count = zero_count
self.flag = flag
self.hide = hide
self.metavar = metavar
@ -51,7 +49,6 @@ class ArgInfo:
def __eq__(self, other):
return (self.win_id == other.win_id and
self.count == other.count and
self.zero_count == other.zero_count and
self.flag == other.flag and
self.hide == other.hide and
self.metavar == other.metavar and
@ -61,7 +58,6 @@ class ArgInfo:
def __repr__(self):
return utils.get_repr(self, win_id=self.win_id, count=self.count,
flag=self.flag, hide=self.hide,
zero_count=self.zero_count,
metavar=self.metavar, completion=self.completion,
choices=self.choices, constructor=True)
@ -142,7 +138,6 @@ class Command:
self.opt_args = collections.OrderedDict()
self.namespace = None
self._count = None
self._zero_count = None
self.pos_args = []
self.desc = None
self.flags_with_args = []
@ -154,7 +149,7 @@ class Command:
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.
Args:
@ -164,17 +159,11 @@ class Command:
window=win_id)
self.validate_mode(mode_manager.mode)
used_backend = usertypes.arg2backend[objreg.get('args').backend]
if self.backend is not None and used_backend != self.backend:
if self.backend is not None and objects.backend != self.backend:
raise cmdexc.PrerequisitesError(
"{}: Only available with {} "
"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:
message.warning('{} is deprecated - {}'.format(self.name,
self.deprecated))
@ -246,9 +235,6 @@ class Command:
assert param.kind != inspect.Parameter.POSITIONAL_ONLY
if param.name == 'self':
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):
continue
if (param.kind == inspect.Parameter.KEYWORD_ONLY and
@ -532,7 +518,7 @@ class Command:
e.status, e))
return
self._count = count
self._check_prerequisites(win_id, count)
self._check_prerequisites(win_id)
posargs, kwargs = self._get_call_args(win_id)
log.commands.debug('Calling {}'.format(
debug_utils.format_call(self.handler, posargs, kwargs)))

View File

@ -65,9 +65,13 @@ class _QtFIFOReader(QObject):
"""(Try to) read a line from the FIFO."""
log.procs.debug("QSocketNotifier triggered!")
self._notifier.setEnabled(False)
for line in self._fifo:
self.got_line.emit(line.rstrip('\r\n'))
self._notifier.setEnabled(True)
try:
for line in self._fifo:
self.got_line.emit(line.rstrip('\r\n'))
self._notifier.setEnabled(True)
except UnicodeDecodeError as e:
log.misc.error("Invalid unicode in userscript output: {}"
.format(e))
def cleanup(self):
"""Clean up so the FIFO can be closed."""
@ -289,6 +293,9 @@ class _WindowsUserscriptRunner(_BaseUserscriptRunner):
self.got_cmd.emit(line.rstrip())
except OSError:
log.procs.exception("Failed to read command file!")
except UnicodeDecodeError as e:
log.misc.error("Invalid unicode in userscript output: {}"
.format(e))
super()._cleanup()
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_DATA_DIR'] = standarddir.data()
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)

View File

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

View File

@ -54,7 +54,7 @@ class CompletionItemDelegate(QStyledItemDelegate):
# FIXME this is horribly slow when resizing.
# 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...
# https://github.com/The-Compiler/qutebrowser/issues/121
# https://github.com/qutebrowser/qutebrowser/issues/121
def __init__(self, parent=None):
self._painter = None
@ -173,7 +173,7 @@ class CompletionItemDelegate(QStyledItemDelegate):
"""
# FIXME we probably should do eliding here. See
# qcommonstyle.cpp:viewItemDrawText
# https://github.com/The-Compiler/qutebrowser/issues/118
# https://github.com/qutebrowser/qutebrowser/issues/118
text_option = QTextOption()
if self._opt.features & QStyleOptionViewItem.WrapText:
text_option.setWrapMode(QTextOption.WordWrap)

View File

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

View File

@ -30,7 +30,7 @@ class SettingSectionCompletionModel(base.BaseCompletionModel):
"""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
COLUMN_WIDTHS = (20, 70, 10)
@ -52,7 +52,7 @@ class SettingOptionCompletionModel(base.BaseCompletionModel):
_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
COLUMN_WIDTHS = (20, 70, 10)
@ -108,7 +108,7 @@ class SettingValueCompletionModel(base.BaseCompletionModel):
_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
COLUMN_WIDTHS = (20, 70, 10)

View File

@ -32,7 +32,7 @@ class CommandCompletionModel(base.BaseCompletionModel):
"""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
COLUMN_WIDTHS = (20, 60, 20)
@ -50,9 +50,11 @@ class HelpCompletionModel(base.BaseCompletionModel):
"""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
COLUMN_WIDTHS = (20, 60, 20)
def __init__(self, parent=None):
super().__init__(parent)
self._init_commands()
@ -87,7 +89,7 @@ class QuickmarkCompletionModel(base.BaseCompletionModel):
"""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
def __init__(self, parent=None):
@ -102,7 +104,7 @@ class BookmarkCompletionModel(base.BaseCompletionModel):
"""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
def __init__(self, parent=None):
@ -117,7 +119,7 @@ class SessionCompletionModel(base.BaseCompletionModel):
"""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
def __init__(self, parent=None):
@ -160,6 +162,7 @@ class TabCompletionModel(base.BaseCompletionModel):
tab.title_changed.connect(self.rebuild)
tab.shutting_down.connect(self.delayed_rebuild)
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)
self.rebuild()
@ -248,7 +251,7 @@ class BindCompletionModel(base.BaseCompletionModel):
"""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
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.utils import (message, objreg, utils, standarddir, log,
qtutils, error, usertypes)
from qutebrowser.misc import objects
from qutebrowser.utils.usertypes import Completion
@ -233,7 +234,7 @@ def _init_misc():
# doesn't overwrite our config.
#
# 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')
for fmt in [QSettings.NativeFormat, QSettings.IniFormat]:
@ -442,6 +443,7 @@ class ConfigManager(QObject):
'html > ::-webkit-scrollbar { width: 0px; height: 0px; }': '',
'::-webkit-scrollbar { width: 0px; height: 0px; }': '',
}),
('contents', 'cache-size'): _get_value_transformer({'52428800': ''}),
}
changed = pyqtSignal(str, str)
@ -772,12 +774,12 @@ class ConfigManager(QObject):
raise cmdexc.CommandError("set: {} - {}".format(
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('option', completion=Completion.option)
@cmdutils.argument('value', completion=Completion.value)
@cmdutils.argument('values', completion=Completion.value)
@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):
"""Set an option.
@ -793,7 +795,7 @@ class ConfigManager(QObject):
Args:
section_: The section where the option is in.
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.
print_: Print the value after setting.
"""
@ -812,27 +814,46 @@ class ConfigManager(QObject):
print_ = True
else:
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]
val = self.get(section_, option)
layer = 'temp' if temp else 'conf'
if isinstance(val, bool):
self.set(layer, section_, option, str(not val).lower())
values = ['false', 'true']
else:
raise cmdexc.CommandError(
"set: Attempted inversion of non-boolean value.")
elif value is not None:
layer = 'temp' if temp else 'conf'
self.set(layer, section_, option, value)
else:
elif not values:
raise cmdexc.CommandError("set: The following arguments "
"are required: value")
layer = 'temp' if temp else 'conf'
self._set_next(layer, section_, option, values)
if print_:
with self._handle_config_error():
val = self.get(section_, option, transformed=False)
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):
"""Set an option.
@ -863,10 +884,9 @@ class ConfigManager(QObject):
# Will be handled later in .setv()
pass
else:
backend = usertypes.arg2backend[objreg.get('args').backend]
if (allowed_backends is not None and
backend not in allowed_backends):
raise configexc.BackendError(backend)
objects.backend not in allowed_backends):
raise configexc.BackendError(objects.backend)
else:
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.value import SettingValue
from qutebrowser.utils.qtutils import MAXVALS
from qutebrowser.utils import usertypes
from qutebrowser.utils import usertypes, qtutils
FIRST_COMMENT = r"""
@ -147,6 +147,13 @@ def data(readonly=False):
"The URL parameters to strip with :yank url, separated by "
"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',
SettingValue(typ.FuzzyUrl(), '${startpage}'),
"The page to open if :open -t/-b/-w is used without URL. Use "
@ -184,16 +191,22 @@ def data(readonly=False):
"icons."),
('developer-extras',
SettingValue(typ.Bool(), 'false'),
SettingValue(typ.Bool(), 'false',
backends=[usertypes.Backend.QtWebKit]),
"Enable extra tools for Web developers.\n\n"
"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',
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 "
"page is printed."),
"page is printed.\n"
"This setting only works with Qt 5.8 or newer when using the "
"QtWebEngine backend."),
('xss-auditing',
SettingValue(typ.Bool(), 'false'),
@ -206,7 +219,7 @@ def data(readonly=False):
('site-specific-quirks',
SettingValue(typ.Bool(), 'true',
backends=[usertypes.Backend.QtWebKit]),
"Enable workarounds for broken sites."),
"Enable QtWebKit workarounds for broken sites."),
('default-encoding',
SettingValue(typ.String(none_ok=True), ''),
@ -338,7 +351,8 @@ def data(readonly=False):
('smooth-scrolling',
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',
SettingValue(typ.Int(minval=-1), '-1'),
@ -390,6 +404,10 @@ def data(readonly=False):
SettingValue(typ.Int(minval=0), '8'),
"The rounding radius for the edges of prompts."),
('prompt-filebrowser',
SettingValue(typ.Bool(), 'true'),
"Show a filebrowser in upload/download prompts."),
readonly=readonly
)),
@ -420,10 +438,13 @@ def data(readonly=False):
('proxy',
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"
"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',
SettingValue(typ.Bool(), 'true',
@ -570,7 +591,7 @@ def data(readonly=False):
"disables the context menu."),
('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 "
"into zoom increments."),
@ -792,9 +813,10 @@ def data(readonly=False):
"enabled."),
('cache-size',
SettingValue(typ.Int(minval=0, maxval=MAXVALS['int64']),
'52428800'),
"Size of the HTTP network cache."),
SettingValue(typ.Int(none_ok=True, minval=0,
maxval=MAXVALS['int64']), ''),
"Size of the HTTP network cache. Empty to use the default "
"value."),
readonly=readonly
)),
@ -815,9 +837,8 @@ def data(readonly=False):
"are not affected by this setting."),
('webgl',
SettingValue(typ.Bool(), 'false'),
"Enables or disables WebGL. For QtWebEngine, Qt/PyQt >= 5.7 is "
"required for this setting."),
SettingValue(typ.Bool(), 'true'),
"Enables or disables WebGL."),
('css-regions',
SettingValue(typ.Bool(), 'true',
@ -854,7 +875,8 @@ def data(readonly=False):
('javascript-can-access-clipboard',
SettingValue(typ.Bool(), 'false'),
"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',
SettingValue(typ.Bool(), 'false'),
@ -888,9 +910,9 @@ def data(readonly=False):
"Control which cookies to accept."),
('cookies-store',
SettingValue(typ.Bool(), 'true',
backends=[usertypes.Backend.QtWebKit]),
"Whether to store cookies."),
SettingValue(typ.Bool(), 'true'),
"Whether to store cookies. Note this option needs a restart with "
"QtWebEngine."),
('host-block-lists',
SettingValue(
@ -936,7 +958,9 @@ def data(readonly=False):
('mode',
SettingValue(typ.String(
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 -> "
"chars setting."),
('word', "Use hints words based on the html "
@ -1268,8 +1292,7 @@ def data(readonly=False):
"Background color for downloads with errors."),
('webpage.bg',
SettingValue(typ.QtColor(none_ok=True), 'white',
backends=[usertypes.Backend.QtWebKit]),
SettingValue(typ.QtColor(none_ok=True), 'white'),
"Background color for webpages if unset (or empty to use the "
"theme's color)"),
@ -1543,7 +1566,8 @@ KEY_DATA = 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 :open {url:pretty}', ['go']),
('set-cmd-text -s :open -t', ['O']),
@ -1569,10 +1593,10 @@ KEY_DATA = collections.OrderedDict([
('tab-clone', ['gC']),
('reload', ['r', '<F5>']),
('reload -f', ['R', '<Ctrl-F5>']),
('back', ['H']),
('back', ['H', '<back>']),
('back -t', ['th']),
('back -w', ['wh']),
('forward', ['L']),
('forward', ['L', '<forward>']),
('forward -t', ['tl']),
('forward -w', ['wl']),
('fullscreen', ['<F11>']),
@ -1650,7 +1674,8 @@ KEY_DATA = collections.OrderedDict([
('set-cmd-text -s :buffer', ['gt']),
('tab-focus last', ['<Ctrl-Tab>']),
('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-B>']),
('scroll-page 0 0.5', ['<Ctrl-D>']),
@ -1764,8 +1789,12 @@ CHANGED_KEY_COMMANDS = [
(re.compile(r'^download-page$'), r'download'),
(re.compile(r'^cancel-download$'), r'download-cancel'),
(re.compile(r"""^search (''|"")$"""), r'clear-keychain ;; search'),
(re.compile(r'^search$'), r'clear-keychain ;; search'),
(re.compile(r"""^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 \1'),
@ -1779,7 +1808,8 @@ CHANGED_KEY_COMMANDS = [
(re.compile(r'^scroll 50 0$'), r'scroll right'),
(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'^download-remove --all$'), r'download-clear'),

View File

@ -31,7 +31,6 @@ import datetime
from PyQt5.QtCore import QUrl, Qt
from PyQt5.QtGui import QColor, QFont
from PyQt5.QtNetwork import QNetworkProxy
from PyQt5.QtWidgets import QTabWidget, QTabBar
from qutebrowser.commands import cmdutils
@ -695,30 +694,17 @@ class CssColor(BaseType):
class QssColor(CssColor):
"""Base class for a color value.
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\(.*\)',
]
"""Color used in a Qt stylesheet."""
def validate(self, value):
functions = ['rgb', 'rgba', 'hsv', 'hsva', 'qlineargradient',
'qradialgradient', 'qconicalgradient']
self._basic_validation(value)
if not value:
return
elif any(re.match(r, value) for r in self.color_func_regexes):
# QColor doesn't handle these, so we do the best we can easily
elif (any(value.startswith(func + '(') for func in functions) and
value.endswith(')')):
# QColor doesn't handle these
pass
elif QColor.isValidColor(value):
pass
@ -742,15 +728,15 @@ class Font(BaseType):
) |
# size (<float>pt | <int>px)
(?P<size>[0-9]+((\.[0-9]+)?[pP][tT]|[pP][xX]))
)\ # size/weight/style are space-separated
)* # 0-inf size/weight/style tags
(?P<family>[A-Za-z0-9, "-]*)$ # mandatory font family""", re.VERBOSE)
)\ # size/weight/style are space-separated
)* # 0-inf size/weight/style tags
(?P<family>.+)$ # mandatory font family""", re.VERBOSE)
def validate(self, value):
self._basic_validation(value)
if not value:
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")
@ -763,7 +749,7 @@ class FontFamily(Font):
if not value:
return
match = self.font_regex.match(value)
if not match:
if not match: # pragma: no cover
raise configexc.ValidationError(value, "must be a valid font")
for group in 'style', 'weight', 'namedweight', 'size':
if match.group(group):
@ -1018,12 +1004,6 @@ class Proxy(BaseType):
"""A proxy URL or special value."""
PROXY_TYPES = {
'http': QNetworkProxy.HttpProxy,
'socks': QNetworkProxy.Socks5Proxy,
'socks5': QNetworkProxy.Socks5Proxy,
}
def __init__(self, none_ok=False):
super().__init__(none_ok)
self.valid_values = ValidValues(
@ -1031,19 +1011,17 @@ class Proxy(BaseType):
('none', "Don't use any proxy"))
def validate(self, value):
from qutebrowser.utils import urlutils
self._basic_validation(value)
if not value:
return
elif value in self.valid_values:
return
url = QUrl(value)
if not url.isValid():
raise configexc.ValidationError(
value, "invalid url, {}".format(url.errorString()))
elif url.scheme() not in self.PROXY_TYPES:
raise configexc.ValidationError(value, "must be a proxy URL "
"(http://... or socks://...) or "
"system/none!")
try:
self.transform(value)
except (urlutils.InvalidUrlError, urlutils.InvalidProxyTypeError) as e:
raise configexc.ValidationError(value, e)
def complete(self):
out = []
@ -1053,25 +1031,21 @@ class Proxy(BaseType):
out.append(('socks://', 'SOCKS proxy URL'))
out.append(('socks://localhost:9050/', 'Tor via SOCKS'))
out.append(('http://localhost:8080/', 'Local HTTP proxy'))
out.append(('pac+https://example.com/proxy.pac', 'Proxy autoconfiguration file URL'))
return out
def transform(self, value):
from qutebrowser.utils import urlutils
if not value:
return None
elif value == 'system':
return SYSTEM_PROXY
elif value == 'none':
return QNetworkProxy(QNetworkProxy.NoProxy)
url = QUrl(value)
typ = self.PROXY_TYPES[url.scheme()]
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
if value == 'none':
url = QUrl('direct://')
else:
url = QUrl(value)
return urlutils.proxy_from_url(url)
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
yet, and the key isn't used anywhere else in the same section.
"""
if utils.is_special_key(keychain):
keychain = keychain.lower()
try:
bindings = self.keybindings[sectname]
except KeyError:
@ -432,11 +435,13 @@ class KeyConfigParser(QObject):
def get_reverse_bindings_for(self, section):
"""Get a dict of commands to a list of bindings for the section."""
cmd_to_keys = {}
for key, cmd in self.get_bindings_for(section).items():
cmd_to_keys.setdefault(cmd, [])
# put special bindings last
if utils.is_special_key(key):
cmd_to_keys[cmd].append(key)
else:
cmd_to_keys[cmd].insert(0, key)
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, [])
# put special bindings last
if utils.is_special_key(key):
cmd_to_keys[cmd].append(key)
else:
cmd_to_keys[cmd].insert(0, key)
return cmd_to_keys

View File

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

View File

@ -1,34 +1,66 @@
{% extends "base.html" %}
{% extends "styled.html" %}
{% block style %}
table { border: 1px solid grey; border-collapse: collapse; width: 100%;}
th, td { border: 1px solid grey; padding: 0px 5px; }
th { background: lightgrey; }
{{super()}}
h1 {
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 %}
{% block content %}
<table>
<tr>
<th><h3>Bookmark</h3></th>
<th><h3>URL</h3></th>
</tr>
{% for url, title in bookmarks %}
<tr>
<td><a href="{{url}}">{{title}}</a></td>
<td>{{url}}</td>
</tr>
{% endfor %}
<tr>
<th><h3>Quickmark</h3></th>
<th><h3>URL</h3></th>
</tr>
<h1>Quickmarks</h1>
{% if quickmarks|length %}
<table class="qmarks">
<tbody>
{% for name, url in quickmarks %}
<tr>
<td><a href="{{url}}">{{name}}</a></td>
<td>{{url}}</td>
<td class="name"><a href="{{url}}">{{name}}</a></td>
<td class="url"><a href="{{url}}">{{url}}</a></td>
</tr>
{% endfor %}
</tbody>
</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 %}

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"
no-warning-comments: "off"
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/>.
*/
/**
* 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";
window._qutebrowser.webelem = (function() {
@ -92,14 +109,17 @@ window._qutebrowser.webelem = (function() {
}
var style = win.getComputedStyle(elem, null);
// FIXME:qtwebengine do we need this <area> handling?
// visibility and display style are misleading for area tags and they
// get "display: none" by default.
// See https://github.com/vimperator/vimperator-labs/issues/236
if (elem.nodeName.toLowerCase() !== "area" && (
style.getPropertyValue("visibility") !== "visible" ||
style.getPropertyValue("display") === "none")) {
return false;
if (style.getPropertyValue("visibility") !== "visible" ||
style.getPropertyValue("display") === "none" ||
style.getPropertyValue("opacity") === "0") {
// FIXME:qtwebengine do we need this <area> handling?
// visibility and display style are misleading for area tags and
// they get "display: none" by default.
// See https://github.com/vimperator/vimperator-labs/issues/236
if (elem.nodeName.toLowerCase() !== "area" &&
!elem.classList.contains("ace_text-input")) {
return false;
}
}
return true;
@ -136,9 +156,8 @@ window._qutebrowser.webelem = (function() {
funcs.insert_text = function(id, text) {
var elem = elements[id];
var event = document.createEvent("TextEvent");
event.initTextEvent("textInput", true, true, null, text);
elem.dispatchEvent(event);
elem.focus();
document.execCommand("insertText", false, text);
};
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;
})();

View File

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

View File

@ -28,6 +28,7 @@ from qutebrowser.keyinput import modeparsers, keyparser
from qutebrowser.config import config
from qutebrowser.commands import cmdexc, cmdutils
from qutebrowser.utils import usertypes, log, objreg, utils
from qutebrowser.misc import objects
class KeyEvent:
@ -265,6 +266,16 @@ class ModeManager(QObject):
m = usertypes.KeyMode[mode]
except KeyError:
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')
@pyqtSlot(usertypes.KeyMode, str, bool)
@ -288,7 +299,7 @@ class ModeManager(QObject):
log.modes.debug("Leaving mode {}{}".format(
mode, '' if reason is None else ' (reason: {})'.format(reason)))
# 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.mode = usertypes.KeyMode.normal
self.left.emit(mode, self.mode, self._win_id)

View File

@ -267,7 +267,7 @@ class CaretKeyParser(keyparser.CommandKeyParser):
self.read_config('caret')
class RegisterKeyParser(keyparser.BaseKeyParser):
class RegisterKeyParser(keyparser.CommandKeyParser):
"""KeyParser for modes that record a register key.
@ -280,6 +280,7 @@ class RegisterKeyParser(keyparser.BaseKeyParser):
super().__init__(win_id, parent, supports_count=False,
supports_chains=False)
self._mode = mode
self.read_config('register')
def handle(self, e):
"""Override handle to always match the next key and use the register.
@ -290,12 +291,15 @@ class RegisterKeyParser(keyparser.BaseKeyParser):
Return:
True if event has been handled, False otherwise.
"""
if utils.keyevent_to_string(e) is None:
# this is a modifier key, let it pass and keep going
return False
if super().handle(e):
return True
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',
window=self._win_id)
macro_recorder = objreg.get('macro-recorder')
@ -323,7 +327,3 @@ class RegisterKeyParser(keyparser.BaseKeyParser):
def on_keyconfig_changed(self, mode):
"""RegisterKeyParser has no config section (no bindable keys)."""
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
status_position = config.get('ui', 'status-position')
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)
topleft = QPoint(left, max(height_padding, top))
bottomright = QPoint(left + width, self.status.geometry().top())
bottomright = QPoint(left + width, bottom)
elif status_position == 'top':
topleft = QPoint(left, self.status.geometry().bottom())
bottom = self.status.height() + size_hint.height()
if self.status.isVisible():
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)
bottomright = QPoint(left + width,
min(self.height() - height_padding, bottom))
@ -425,6 +437,9 @@ class MainWindow(QWidget):
# messages
message.global_bridge.show_message.connect(
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_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_link_hovered.connect(status.url.set_hover_url)
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
mode_manager.left.connect(tabs.on_mode_left)
@ -451,6 +470,13 @@ class MainWindow(QWidget):
completion_obj.on_clear_completion_selection)
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')
@pyqtSlot()
def close(self):
@ -462,14 +488,6 @@ class MainWindow(QWidget):
"""
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):
"""Extend resizewindow's resizeEvent to adjust completion.

View File

@ -31,8 +31,9 @@ class Message(QLabel):
"""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)
self.replace = replace
self.setAttribute(Qt.WA_StyledBackground, True)
stylesheet = """
padding-top: 2px;
@ -81,7 +82,7 @@ class MessageView(QWidget):
self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
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()
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'))
@pyqtSlot()
def _clear_messages(self):
def clear_messages(self):
"""Hide and delete all messages."""
for widget in self._messages:
self._vbox.removeWidget(widget)
@ -111,13 +112,17 @@ class MessageView(QWidget):
self.hide()
self._clear_timer.stop()
@pyqtSlot(usertypes.MessageLevel, str)
def show_message(self, level, text):
@pyqtSlot(usertypes.MessageLevel, str, bool)
def show_message(self, level, text, replace=False):
"""Show the given message with the given MessageLevel."""
if text == self._last_text:
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)
widget.show()
self._clear_timer.start()

View File

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

View File

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

View File

@ -98,6 +98,7 @@ class TabbedBrowser(tabwidget.TabWidget):
resized = pyqtSignal('QRect')
current_tab_changed = pyqtSignal(browsertab.AbstractTab)
new_tab = pyqtSignal(browsertab.AbstractTab, int)
page_fullscreen_requested = pyqtSignal(bool)
def __init__(self, win_id, parent=None):
super().__init__(win_id, parent)
@ -198,8 +199,13 @@ class TabbedBrowser(tabwidget.TabWidget):
functools.partial(self.on_load_started, tab))
tab.window_close_requested.connect(
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.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):
"""Get the URL of the current tab.
@ -245,12 +251,13 @@ class TabbedBrowser(tabwidget.TabWidget):
url = config.get('general', 'default-page')
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.
Args:
tab: The QWebView to be closed.
add_undo: Whether the tab close can be undone.
crashed: Whether we're closing a tab with crashed renderer process.
"""
idx = self.indexOf(tab)
if idx == -1:
@ -262,25 +269,34 @@ class TabbedBrowser(tabwidget.TabWidget):
window=self._win_id):
objreg.delete('last-focused-tab', scope='window',
window=self._win_id)
if tab.url().isValid():
history_data = tab.history.serialize()
if add_undo:
entry = UndoEntry(tab.url(), history_data, idx)
self._undo_stack.append(entry)
elif tab.url().isEmpty():
if tab.url().isEmpty():
# There are some good reasons why a URL could be empty
# (target="_blank" with a download, see [1]), so we silently ignore
# this.
# [1] https://github.com/The-Compiler/qutebrowser/issues/163
# [1] https://github.com/qutebrowser/qutebrowser/issues/163
pass
else:
# We display a warnings for URLs which are not empty but invalid -
elif not tab.url().isValid():
# 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
# way.
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()
self.removeTab(idx)
tab.deleteLater()
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()
def undo(self):
"""Undo removing of a tab."""
@ -347,7 +363,8 @@ class TabbedBrowser(tabwidget.TabWidget):
@pyqtSlot('QUrl')
@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.
Inner logic for open-tab and open-tab-bg.
@ -364,6 +381,8 @@ class TabbedBrowser(tabwidget.TabWidget):
the current.
- Explicitly opened tabs are at the very right.
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:
The opened WebView instance.
@ -374,7 +393,8 @@ class TabbedBrowser(tabwidget.TabWidget):
"explicit {}, idx {}".format(
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
window = mainwindow.MainWindow()
window.show()
@ -523,22 +543,6 @@ class TabbedBrowser(tabwidget.TabWidget):
if not self.page_title(idx):
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)
def on_icon_changed(self, tab, icon):
"""Set the icon of a tab.
@ -650,6 +654,28 @@ class TabbedBrowser(tabwidget.TabWidget):
self.update_window_title()
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):
"""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