Merge branch 'master' of https://github.com/The-Compiler/qutebrowser into pintab
This commit is contained in:
commit
650b1de3b6
3
.flake8
3
.flake8
@ -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,
|
||||
|
29
.travis.yml
29
.travis.yml
@ -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
|
||||
|
@ -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.
|
||||
|
@ -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`
|
||||
|
20
FAQ.asciidoc
20
FAQ.asciidoc
@ -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
|
||||
|
@ -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
|
||||
----
|
||||
|
||||
|
@ -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:
|
||||
|
@ -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).
|
||||
|
||||
|
@ -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 |
@ -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].
|
||||
|
@ -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
|
||||
|
@ -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:
|
||||
|
||||
|
4144
misc/cheatsheet.svg
4144
misc/cheatsheet.svg
File diff suppressed because it is too large
Load Diff
Before Width: | Height: | Size: 137 KiB After Width: | Height: | Size: 170 KiB |
@ -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')
|
||||
|
@ -1,3 +1,3 @@
|
||||
# This file is automatically generated by scripts/dev/recompile_requirements.py
|
||||
|
||||
check-manifest==0.34
|
||||
check-manifest==0.35
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
8
misc/requirements/requirements-pip.txt
Normal file
8
misc/requirements/requirements-pip.txt
Normal 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
|
@ -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
|
||||
|
@ -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#
|
@ -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
|
||||
|
@ -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
|
||||
|
4
misc/requirements/requirements-pyqt.txt
Normal file
4
misc/requirements/requirements-pyqt.txt
Normal 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
|
1
misc/requirements/requirements-pyqt.txt-raw
Normal file
1
misc/requirements/requirements-pyqt.txt-raw
Normal file
@ -0,0 +1 @@
|
||||
PyQt5
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -1,11 +1,12 @@
|
||||
beautifulsoup4
|
||||
CherryPy
|
||||
cheroot
|
||||
coverage
|
||||
Flask
|
||||
httpbin
|
||||
hypothesis
|
||||
pytest
|
||||
pytest-bdd
|
||||
pytest-benchmark
|
||||
pytest-catchlog
|
||||
pytest-cov
|
||||
pytest-faulthandler
|
||||
|
@ -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
|
||||
|
@ -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
27
misc/userscripts/ripbang
Executable 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)
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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__))
|
||||
|
@ -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))
|
||||
|
@ -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(
|
||||
|
@ -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),
|
||||
|
@ -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()
|
||||
|
@ -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)
|
||||
|
@ -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."""
|
||||
|
@ -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:
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
||||
|
3
qutebrowser/browser/network/__init__.py
Normal file
3
qutebrowser/browser/network/__init__.py
Normal file
@ -0,0 +1,3 @@
|
||||
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
|
||||
|
||||
"""Modules related to network operations."""
|
315
qutebrowser/browser/network/pac.py
Normal file
315
qutebrowser/browser/network/pac.py
Normal 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)
|
@ -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:
|
@ -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
|
||||
|
@ -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.
|
||||
|
||||
|
@ -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."""
|
||||
|
@ -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:
|
||||
|
@ -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."""
|
||||
|
@ -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()
|
||||
|
@ -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)
|
||||
|
@ -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()
|
||||
|
@ -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
|
||||
|
@ -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()
|
||||
|
@ -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):
|
||||
|
@ -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()
|
||||
|
@ -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)
|
||||
|
@ -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()
|
||||
|
@ -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 "
|
||||
|
@ -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))
|
||||
|
@ -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)
|
||||
|
@ -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.
|
||||
|
@ -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()
|
||||
|
@ -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'))
|
||||
|
@ -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
|
||||
|
@ -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:
|
||||
|
@ -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)))
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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):
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
||||
|
@ -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'),
|
||||
|
@ -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):
|
||||
|
@ -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
|
||||
|
@ -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:
|
||||
|
@ -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 %}
|
||||
|
58
qutebrowser/html/history.html
Normal file
58
qutebrowser/html/history.html
Normal 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 %}
|
41
qutebrowser/html/styled.html
Normal file
41
qutebrowser/html/styled.html
Normal 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 %}
|
2
qutebrowser/javascript/.eslintignore
Normal file
2
qutebrowser/javascript/.eslintignore
Normal file
@ -0,0 +1,2 @@
|
||||
# Upstream Mozilla's code
|
||||
pac_utils.js
|
@ -36,3 +36,5 @@ rules:
|
||||
sort-keys: "off"
|
||||
no-warning-comments: "off"
|
||||
max-len: ["error", {"ignoreUrls": true}]
|
||||
capitalized-comments: "off"
|
||||
prefer-destructuring: "off"
|
||||
|
257
qutebrowser/javascript/pac_utils.js
Normal file
257
qutebrowser/javascript/pac_utils.js
Normal 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));
|
||||
}
|
@ -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;
|
||||
})();
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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.
|
||||
|
||||
|
@ -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()
|
||||
|
@ -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):
|
||||
|
@ -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.
|
||||
|
||||
|
@ -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
Loading…
Reference in New Issue
Block a user