Compare commits

..

19 Commits

Author SHA1 Message Date
Florian Bruhin
6145786e46 Release v1.2.1 2018-03-14 20:19:14 +01:00
Florian Bruhin
404c276774 Update changelog from master 2018-03-14 20:18:57 +01:00
Florian Bruhin
2e87ee0960 Swap Control/Meta back on macOS
Fixes #3697

(cherry picked from commit fd9e7bed7fd9842eac22ed304a094a92cc953577)
(cherry picked from commit 84c7c37e8e)
2018-03-14 20:05:09 +01:00
Florian Bruhin
ca0669253a Disable test_software_rendering on macOS
For some reason, macOS doesn't care about us disabling software rendering

(cherry picked from commit d232b3ea57)
2018-03-14 20:05:07 +01:00
Florian Bruhin
4b6bfd066c Don't emit predicted_navigation for reloads at all
When we reload a page because of a config change, we won't get another
titleChanged signal (at least sometimes).

Also, the predicted_navigation signal is worthless when reloading anyways, as
we're going to load the same URL and not something different.

Fixes #3718

(cherry picked from commit 0418a865c17c26720219e33a67c88410a6ac7181)
2018-03-14 18:18:15 +01:00
Jay Kamat
d49e6d0229 Add test for #3711
(cherry picked from commit 35beff98a9)
2018-03-14 07:45:12 +01:00
Jay Kamat
bb76931be6 Fix hinting in frames on qt5.9 with input ranges
(cherry picked from commit a6e94cf30c)
2018-03-14 07:45:08 +01:00
Florian Bruhin
41f38a244e Mark BaseKeyParser.handle as noqa
This got fixed properly in master, but can stay like this in this branch.
2018-03-13 14:43:39 +01:00
Florian Bruhin
ad5fde9d05 Fall back to non-keypad keys without any keypad bindings
Fixes #3701

(cherry picked from commit b88ac51d25)
2018-03-13 14:42:43 +01:00
Florian Bruhin
4cc4426428 Don't emit predicted_navigation with invalid URLs
Fixes #3706

(cherry picked from commit 1c9598d2c0)
2018-03-13 09:51:34 +01:00
Florian Bruhin
e5bcf99295 Fix lint
(cherry picked from commit 8c5b7bcd03)
2018-03-12 09:27:18 +01:00
Florian Bruhin
2e87539b44 Normalize keys read from the config
This makes sure the internal bindings.commands object only contains normalized
key sequences.

Fixes #3699

(cherry picked from commit 9941812127)
2018-03-12 08:03:44 +01:00
Florian Bruhin
4bff190db9 Make from_obj() work for List/Dict configtypes
We can't easily make it work for ListOrValue as we don't know which of both we
get at this point.

(cherry picked from commit 990c0707f4)
2018-03-12 08:03:42 +01:00
Florian Bruhin
8b588d2635 tests: Add a yaml_config_stub fixture
(cherry picked from commit c03ef10d54)
2018-03-12 08:03:38 +01:00
Florian Bruhin
9cab6403d4 build_release: Wait before detaching volume
This hopefully helps with detaching it properly.

(cherry picked from commit 27c2650245)
2018-03-11 21:07:39 +01:00
Florian Bruhin
d4d3e76121 Force PyQt 5.10.0 with "tox -e mkvenv-pypi"
Fixes #3662

(cherry picked from commit 30ab1d0218)
2018-03-11 21:07:35 +01:00
Florian Bruhin
7fd1b4b180 Mark another GreaseMonkey test as flaky
See #3238

(cherry picked from commit f0a649e101)
2018-03-11 21:07:29 +01:00
Florian Bruhin
2f913f5932 Fix keybinding cheatsheet URLs in quickstart.asciidoc
The URLs and the patching were changed in
96e8151cce but not in quickstart.asciidoc.

(cherry picked from commit 75ab8f077d)
2018-03-11 21:07:26 +01:00
Florian Bruhin
8a23b91134 Handle ImportError in version.opengl_vendor
Fixes #3698

(cherry picked from commit d9f7d401c6)
2018-03-11 21:07:21 +01:00
120 changed files with 1068 additions and 2150 deletions

12
.flake8
View File

@@ -32,7 +32,7 @@ exclude = .*,__pycache__,resources.py
# D403: First word of the first line should be properly capitalized
# (false-positives)
# D413: Missing blank line after last section (not in pep257?)
# A003: Builtin name for class attribute (needed for overridden methods)
# A003: Builtin name for class attribute (needed for attrs)
ignore =
B001,B008,B305,
E128,E226,E265,E501,E402,E266,E722,E731,
@@ -44,11 +44,11 @@ ignore =
min-version = 3.4.0
max-complexity = 12
per-file-ignores =
/tests/**/test_*.py : D100,D101,D401
/tests/unit/browser/test_history.py : N806
/tests/helpers/fixtures.py : N806
/tests/unit/browser/webkit/http/test_content_disposition.py : D400
/scripts/dev/ci/appveyor_install.py : FI53
tests/*/test_*.py : D100,D101,D401
tests/unit/browser/test_history.py : N806
tests/helpers/fixtures.py : N806
tests/unit/browser/webkit/http/test_content_disposition.py : D400
scripts/dev/ci/appveyor_install.py : FI53
copyright-check = True
copyright-regexp = # Copyright [\d-]+ .*
copyright-min-file-size = 110

View File

@@ -8,7 +8,6 @@ graft icons
graft doc/img
graft misc/apparmor
graft misc/userscripts
graft misc/requirements
recursive-include scripts *.py *.sh *.js
include qutebrowser/utils/testfile
include qutebrowser/git-commit-id
@@ -33,6 +32,8 @@ include doc/qutebrowser.1.asciidoc
include doc/changelog.asciidoc
prune tests
prune qutebrowser/3rdparty
prune misc/requirements
prune misc/docker
exclude pytest.ini
exclude qutebrowser.rcc
exclude qutebrowser/javascript/.eslintrc.yaml

View File

@@ -99,7 +99,7 @@ Requirements
The following software and libraries are required to run qutebrowser:
* http://www.python.org/[Python] 3.5 or newer (3.6 recommended)
* http://qt.io/[Qt] 5.7.1 or newer (5.10 recommended) with the following modules:
* http://qt.io/[Qt] 5.7.1 or newer with the following modules:
- QtCore / qtbase
- QtQuick (part of qtbase in some distributions)
- QtSQL (part of qtbase in some distributions)
@@ -109,7 +109,7 @@ The following software and libraries are required to run qutebrowser:
link:https://github.com/annulen/webkit/wiki[updated fork] (5.212) is
supported
* http://www.riverbankcomputing.com/software/pyqt/intro[PyQt] 5.7.0 or newer
(5.10 recommended) for Python 3
(5.9.2 recommended) for Python 3
* https://pypi.python.org/pypi/setuptools/[pkg_resources/setuptools]
* http://fdik.org/pyPEG/[pyPEG2]
* http://jinja.pocoo.org/[jinja2]

View File

@@ -15,100 +15,6 @@ breaking changes (such as renamed commands) can happen in minor releases.
// `Fixed` for any bug fixes.
// `Security` to invite users to upgrade in case of vulnerabilities.
v1.3.1
------
Fixed
~~~~~
- Work around a bug in Qt 5.11 where only the top/bottom half of the window is used.
This workaround is incomplete, but fixes the majority of the cases where this happens.
- Work around keyboard focus issues with Qt 5.11.
- Work around an issue in Qt 5.11 where e.g. activating JavaScript per-domain
needed a manual reload in some cases.
- Don't crash when a ² key is pressed (e.g. on AZERTY keyboards).
- Don't crash when a tab is opened and quickly closed again.
v1.3.0
------
Added
~~~~~
- New `:scroll-to-anchor` command to scroll to an anchor in the document.
- New `url.open_base_url` option to open the base URL of a searchengine when no
search term is given.
- New `tabs.min_width` setting to configure the minimal width for tabs.
- New userscripts:
* `getbib` to download bibtex information for DOIs on a page.
* `qute-keepass` to get passwords from KeePassX.
Changed
~~~~~~~
- QtWebEngine: Support for JavaScript Shared Web Workers have been disabled on
Qt versions older than 5.11 because of security issues in in Chromium.
You can get the same effect in earlier versions via
`:set qt.args ['disable-shared-workers']`. An equivalent workaround is also
contained in Qt 5.9.5 and 5.10.1.
- The file dialog for downloads now has basic tab completion based on the
entered text.
- `:version` now shows OS information for POSIX OS other than Linux/macOS.
- When there's an error inserting the text from an external editor, a backup
file is now saved.
- The `window.hide_wayland_decoration` setting got renamed to
`window.hide_decoration` and now also works outside of wayland.
- The `tabs.favicons.show` setting now can take three values: `'always'` (was
`True`), `'never'` (was `False`) and `'pinned'` (to only show favicons for
pinned tabs).
- Hover tooltips on tabs now always show the webpage's title.
- The default value for `content.host_blocking.lists` was changed to only
include https://github.com/StevenBlack/hosts[Steven Black's hosts-list] which
combines various sources.
- Error messages when trying to wrap when `tabs.wrap` is `False` are now logged
to debug instead of messages.
Fixed
~~~~~
- Using hints before a page is fully loaded is now possible again.
- Selecting hints with the number keypad now works again.
- Tab titles for tabs loaded from sessions should now really be correct instead
of showing the URL.
- Loading URLs with customized settings from a session now avoids an additional
reload.
- The window icon and title now get set correctly again.
- The `tabs.switching_delay` setting now has a correct maximum value limit set.
- The `taskadd` script now works properly when there's multi-line output.
- QtWebEngine: Worked around issues with GreaseMonkey/stylesheets not being
loaded correctly in some situations.
- The statusbar now more closely reflects the caret mode state.
- The icon on Windows should now be displayed in a higher resolution.
- The QtWebEngine development tools (inspector) now also work when JavaScript is
disabled globally.
- Building `.exe` files now works when `upx` is installed on the system.
- The keyhint widget now shows the correct text for chained modifiers.
- Loading GreaseMonkey scripts now also works with Jinja2 2.8 (e.g. on Debian
Stable).
- Adding styles with GreaseMonkey on fast sites now works properly.
- Window ID 0 is now excluded properly from `:tab-take` completion.
- A rare crash when cancelling a download has been fixed.
- The Makefile (intended for packagers) now supports `PREFIX` properly.
- The workaround for a black window with Nvidia graphics is now enabled on
non-Linux systems (like FreeBSD) as well.
- Initial support for Qt 5.11.
- Checking for a new version after sending a crash report now works properly
again.
- `@match` in Greasemonkey scripts now more closely matches the proper pattern
syntax.
- Searching via `/` or `?` now doesn't handle any characters in a special way.
- Fixed crash when trying to retry some failed downloads on QtWebEngine.
- An invalid spellcheck dictionary filename now doesn't crash anymore.
- When no spellcheck dictionaries are configured, it's now disabled internally.
This works around an issue with entering special characters on Facebook
messenger.
- The macOS release now should work again on macOS 10.11 and newer.
v1.2.1
------

View File

@@ -670,11 +670,10 @@ qutebrowser release
~~~~~~~~~~~~~~~~~~~
* Make sure there are no unstaged changes and the tests are green.
* Make sure all issues with the related milestone are closed.
* Run `x=... y=...` to set the respective shell variables.
* Update changelog (remove *(unreleased)*).
* Adjust `__version_info__` in `qutebrowser/__init__.py`.
* Update changelog (remove *(unreleased)*).
* Commit.
* Create annotated git tag (`git tag -s "v1.$x.$y" -m "Release v1.$x.$y"`).
@@ -684,11 +683,9 @@ qutebrowser release
* Mark the milestone at https://github.com/qutebrowser/qutebrowser/milestones
as closed.
* Linux: Run `git checkout v1.$x.$y && ./.venv/bin/python3 scripts/dev/build_release.py --upload v1.$x.$y`.
* Linux: Run `git checkout v1.$x.$y && python3 scripts/dev/build_release.py --upload v1.$x.$y`.
* Windows: Run `git checkout v1.X.Y; C:\Python36-32\python scripts\dev\build_release.py --asciidoc C:\Python27\python C:\asciidoc-8.6.9\asciidoc.py --upload v1.X.Y` (replace X/Y by hand).
* macOS: Run `git checkout v1.X.Y && python3 scripts/dev/build_release.py --upload v1.X.Y` (replace X/Y by hand).
* On server:
- Run `python3 scripts/dev/download_release.py v1.X.Y` (replace X/Y by hand).
- Run `git pull github master && sudo python3 scripts/asciidoc2html.py --website /srv/http/qutebrowser`
* On server: Run `python3 scripts/dev/download_release.py v1.X.Y` (replace X/Y by hand).
* Update `qutebrowser-git` PKGBUILD if dependencies/install changed.
* Announce to qutebrowser and qutebrowser-announce mailinglist.

View File

@@ -212,37 +212,6 @@ Why takes it longer to open an URL in qutebrowser than in chromium?::
qutebrowser if it is not running already. Also check if you want
to use webengine as backend in line 17 and change it to your
needs.
How do I make qutebrowser use greasemonkey scripts?::
There is currently no UI elements to handle managing greasemonkey scripts.
All management of what scripts are installed or disabled is done in the
filesystem by you. qutebrowser reads all files that have an extension of
`.js` from the `<data>/greasemonkey/` folder and attempts to load them.
Where `<data>` is the qutebrowser data directory shown in the `Paths`
section of the page displayed by `:version`. If you want to disable a
script just rename it, for example, to have `.disabled` on the end, after
the `.js` extension. To reload scripts from that directory run the command
`:greasemonkey-reload`.
+
Troubleshooting: to check that your script is being loaded when
`:greasemonkey-reload` runs you can start qutebrowser with the arguments
`--debug --logfilter greasemonkey,js` and check the messages on the
program's standard output for errors parsing or loading your script.
You may also see javascript errors if your script is expecting an environment
that we fail to provide.
+
Note that there are some missing features which you may run into:
. Some scripts expect `GM_xmlhttpRequest` to ignore Cross Origin Resource
Sharing restrictions, this is currently not supported, so scripts making
requests to third party sites will often fail to function correctly.
. If your backend is a QtWebEngine version 5.8, 5.9 or 5.10 then regular
expressions are not supported in `@include` or `@exclude` rules. If your
script uses them you can re-write them to use glob expressions or convert
them to `@match` rules.
See https://wiki.greasespot.net/Metadata_Block[the wiki] for more info.
. Any greasemonkey API function to do with adding UI elements is not currently
supported. That means context menu extentensions and background pages.
== Troubleshooting

View File

@@ -93,7 +93,6 @@ It is possible to run or bind multiple commands by separating them with `;;`.
|<<scroll,scroll>>|Scroll the current tab in the given direction.
|<<scroll-page,scroll-page>>|Scroll the frame page-wise.
|<<scroll-px,scroll-px>>|Scroll the current tab by 'count * dx/dy' pixels.
|<<scroll-to-anchor,scroll-to-anchor>>|Scroll to the given anchor in the document.
|<<scroll-to-perc,scroll-to-perc>>|Scroll to a specific percentage of the page.
|<<search,search>>|Search for a text on the current page. With no text, clear results.
|<<search-next,search-next>>|Continue the search to the ([count]th) next term.
@@ -1025,15 +1024,6 @@ Scroll the current tab by 'count * dx/dy' pixels.
==== count
multiplier
[[scroll-to-anchor]]
=== scroll-to-anchor
Syntax: +:scroll-to-anchor 'name'+
Scroll to the given anchor in the document.
==== positional arguments
* +'name'+: The anchor to scroll to.
[[scroll-to-perc]]
=== scroll-to-perc
Syntax: +:scroll-to-perc [*--horizontal*] ['perc']+

View File

@@ -3,28 +3,44 @@ Configuring qutebrowser
IMPORTANT: qutebrowser's configuration system was completely rewritten in
September 2017. This information is not applicable to older releases, and older
information elsewhere might be outdated.
information elsewhere might be outdated. **If you had an old configuration
around and upgraded, this page will automatically open once**. To view it at a
later time, use the `:help` command.
qutebrowser's config files
--------------------------
Migrating older configurations
------------------------------
qutebrowser releases before v1.0.0 had a `qutebrowser.conf` and `keys.conf`
file. Those are not used anymore since that release - see
<<migrating,"Migrating older configurations">> for information on how to
migrate to the new config.
qutebrowser does no automatic migration for the new configuration. However,
there's a special link:qute://configdiff/old[configdiff] page
(`qute://configdiff/old`) in qutebrowser, which will show you the changes you
did in your old configuration, compared to the old defaults.
When using `:set` and `:bind`, changes are saved to an `autoconfig.yml` file
automatically. If you don't want to have a config file which is curated by
hand, you can simply use those - see
<<autoconfig,"Configuring qutebrowser via the user interface">> for details.
Other changes in default settings:
For more advanced configuration, you can write a `config.py` file - see
<<configpy,"Configuring qutebrowser via config.py">>. As soon as a `config.py`
exists, the `autoconfig.yml` file **is not read anymore** by default. You need
to <<configpy-autoconfig,load it by hand>> if you want settings done via
`:set`/`:bind` to still persist.
- In v1.1.x and newer, `<Up>` and `<Down>` navigate through command history
if no text was entered yet.
With v1.0.x, they always navigate through command history instead of selecting
completion items. Use `<Tab>`/`<Shift-Tab>` to cycle through the completion
instead.
You can get back the old behavior by doing:
+
----
:bind -m command <Up> completion-item-focus prev
:bind -m command <Down> completion-item-focus next
----
+
or always navigate through command history with
+
----
:bind -m command <Up> command-history-prev
:bind -m command <Down> command-history-next
----
- The default for `completion.web_history_max_items` is now set to `-1`, showing
an unlimited number of items in the completion for `:open` as the new
sqlite-based completion is much faster. If the `:open` completion is too slow
on your machine, set an appropriate limit again.
[[autoconfig]]
Configuring qutebrowser via the user interface
----------------------------------------------
@@ -72,7 +88,6 @@ link:commands.html#config-clear[`:config-clear`] to reset the entire configurati
and link:commands.html#config-cycle[`:config-cycle`] to cycle a setting between
different values.
[[configpy]]
Configuring qutebrowser via config.py
-------------------------------------
@@ -224,14 +239,13 @@ config.bind(',v', 'spawn mpv {url}')
To suppress loading of any default keybindings, you can set
`c.bindings.default = {}`.
[[configpy-autoconfig]]
Loading `autoconfig.yml`
~~~~~~~~~~~~~~~~~~~~~~~~
All customization done via the UI (`:set`, `:bind` and `:unbind`) is
stored in the `autoconfig.yml` file, which is not loaded automatically as soon
as a `config.py` exists. If you want those settings to be loaded, you'll need to
explicitly load the `autoconfig.yml` file in your `config.py` by doing:
By default, all customization done via `:set`, `:bind` and `:unbind` is
temporary as soon as a `config.py` exists. The settings done that way are always
saved in the `autoconfig.yml` file, but you'll need to explicitly load it in
your `config.py` by doing:
.config.py:
[source,python]
@@ -415,38 +429,3 @@ from qutebrowser.config.config import ConfigContainer # noqa: F401
config = config # type: ConfigAPI # noqa: F821 pylint: disable=E0602,C0103
c = c # type: ConfigContainer # noqa: F821 pylint: disable=E0602,C0103
----
[[migrating]]
Migrating older configurations
------------------------------
qutebrowser does no automatic migration for the new configuration. However,
there's a special link:qute://configdiff/old[configdiff] page
(`qute://configdiff/old`) in qutebrowser, which will show you the changes you
did in your old configuration, compared to the old defaults.
Other changes in default settings:
- In v1.1.x and newer, `<Up>` and `<Down>` navigate through command history
if no text was entered yet.
With v1.0.x, they always navigate through command history instead of selecting
completion items. Use `<Tab>`/`<Shift-Tab>` to cycle through the completion
instead.
You can get back the old behavior by doing:
+
----
:bind -m command <Up> completion-item-focus prev
:bind -m command <Down> completion-item-focus next
----
+
or always navigate through command history with
+
----
:bind -m command <Up> command-history-prev
:bind -m command <Down> command-history-next
----
- The default for `completion.web_history_max_items` is now set to `-1`, showing
an unlimited number of items in the completion for `:open` as the new
sqlite-based completion is much faster. If the `:open` completion is too slow
on your machine, set an appropriate limit again.

View File

@@ -236,11 +236,10 @@
|<<tabs.close_mouse_button,tabs.close_mouse_button>>|Mouse button with which to close tabs.
|<<tabs.close_mouse_button_on_bar,tabs.close_mouse_button_on_bar>>|How to behave when the close mouse button is pressed on the tab bar.
|<<tabs.favicons.scale,tabs.favicons.scale>>|Scaling factor for favicons in the tab bar.
|<<tabs.favicons.show,tabs.favicons.show>>|When to show favicons in the tab bar.
|<<tabs.favicons.show,tabs.favicons.show>>|Show favicons in the tab bar.
|<<tabs.indicator.padding,tabs.indicator.padding>>|Padding (in pixels) for tab indicators.
|<<tabs.indicator.width,tabs.indicator.width>>|Width (in pixels) of the progress indicator (0 to disable).
|<<tabs.last_close,tabs.last_close>>|How to behave when the last tab is closed.
|<<tabs.min_width,tabs.min_width>>|Minimum width (in pixels) of tabs (-1 for the default minimum size behavior).
|<<tabs.mode_on_change,tabs.mode_on_change>>|When switching tabs, what input mode is applied.
|<<tabs.mousewheel_switching,tabs.mousewheel_switching>>|Switch between tabs using the mouse wheel.
|<<tabs.new_position.related,tabs.new_position.related>>|Position of new tabs opened from another tab.
@@ -260,11 +259,10 @@
|<<url.auto_search,url.auto_search>>|What search to start when something else than a URL is entered.
|<<url.default_page,url.default_page>>|Page to open if :open -t/-b/-w is used without URL.
|<<url.incdec_segments,url.incdec_segments>>|URL segments where `:navigate increment/decrement` will search for a number.
|<<url.open_base_url,url.open_base_url>>|Open base URL of the searchengine if a searchengine shortcut is invoked without parameters.
|<<url.searchengines,url.searchengines>>|Search engines which can be used via the address bar.
|<<url.start_pages,url.start_pages>>|Page(s) to open at the start.
|<<url.yank_ignored_parameters,url.yank_ignored_parameters>>|URL parameters to strip with `:yank url`.
|<<window.hide_decoration,window.hide_decoration>>|Hide the window decoration.
|<<window.hide_wayland_decoration,window.hide_wayland_decoration>>|Hide the window decoration when using wayland.
|<<window.title_format,window.title_format>>|Format to use for the window title. The same placeholders like for
|<<zoom.default,zoom.default>>|Default zoom level.
|<<zoom.levels,zoom.levels>>|Available zoom levels.
@@ -1655,7 +1653,11 @@ Type: <<types,List of Url>>
Default:
- +pass:[https://raw.githubusercontent.com/StevenBlack/hosts/master/hosts]+
- +pass:[https://www.malwaredomainlist.com/hostslist/hosts.txt]+
- +pass:[http://someonewhocares.org/hosts/hosts]+
- +pass:[http://winhelp2002.mvps.org/hosts.zip]+
- +pass:[http://malwaredomains.lehigh.edu/files/justdomains.zip]+
- +pass:[https://pgl.yoyo.org/adservers/serverlist.php?hostformat=hosts&amp;mimetype=plaintext]+
[[content.host_blocking.whitelist]]
=== content.host_blocking.whitelist
@@ -2821,17 +2823,11 @@ Default: +pass:[1.0]+
[[tabs.favicons.show]]
=== tabs.favicons.show
When to show favicons in the tab bar.
Show favicons in the tab bar.
Type: <<types,String>>
Type: <<types,Bool>>
Valid values:
* +always+: Always show favicons.
* +never+: Always hide favicons.
* +pinned+: Show favicons only on pinned tabs.
Default: +pass:[always]+
Default: +pass:[true]+
[[tabs.indicator.padding]]
=== tabs.indicator.padding
@@ -2870,16 +2866,6 @@ Valid values:
Default: +pass:[ignore]+
[[tabs.min_width]]
=== tabs.min_width
Minimum width (in pixels) of tabs (-1 for the default minimum size behavior).
This setting only applies when tabs are horizontal.
This setting does not apply to pinned tabs, unless `tabs.pinned.shrink` is False.
Type: <<types,Int>>
Default: +pass:[-1]+
[[tabs.mode_on_change]]
=== tabs.mode_on_change
When switching tabs, what input mode is applied.
@@ -3116,14 +3102,6 @@ Default:
- +pass:[path]+
- +pass:[query]+
[[url.open_base_url]]
=== url.open_base_url
Open base URL of the searchengine if a searchengine shortcut is invoked without parameters.
Type: <<types,Bool>>
Default: +pass:[false]+
[[url.searchengines]]
=== url.searchengines
Search engines which can be used via the address bar.
@@ -3159,12 +3137,10 @@ Default:
- +pass:[utm_term]+
- +pass:[utm_content]+
[[window.hide_decoration]]
=== window.hide_decoration
Hide the window decoration.
This setting requires a restart on Wayland.
[[window.hide_wayland_decoration]]
=== window.hide_wayland_decoration
Hide the window decoration when using wayland.
This setting requires a restart.
Type: <<types,Bool>>
@@ -3297,7 +3273,7 @@ See the setting's valid values for more information on allowed values.
|TextAlignment|Alignment of text.
|TimestampTemplate|An strftime-like template for timestamps.
See https://sqlite.org/lang_datefunc.html for reference.
See https://docs.python.org/3/library/datetime.html#strftime-strptime-behavior for reference.
|UniqueCharString|A string which may not contain duplicate chars.
|Url|A URL as a string.
|VerticalPosition|The position of the download bar.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 MiB

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

After

Width:  |  Height:  |  Size: 46 KiB

View File

@@ -47,26 +47,17 @@ Debian Stretch / Ubuntu 17.04 and 17.10
Those versions come with QtWebEngine in the repositories. This makes it possible
to install qutebrowser via the Debian package.
You'll need to download three packages:
Download the https://packages.debian.org/sid/all/qutebrowser/download[qutebrowser] and
https://packages.debian.org/sid/all/python3-pypeg2/download[PyPEG2]
package from the Debian repositories.
- https://packages.debian.org/sid/all/python3-pypeg2/download[PyPEG2] (a library
used by qutebrowser which is not in the earlier repositories)
- https://packages.debian.org/sid/all/qutebrowser/download[qutebrowser] itself
- Either https://packages.debian.org/sid/all/qutebrowser-qtwebengine/download[qutebrowser-qtwebengine]
or https://packages.debian.org/sid/all/qutebrowser-qtwebkit/download[qutebrowser-qtwebkit]
(or both) depending on the backend you want to use. QtWebEngine is the
default/recommended choice.
After downloading, install the packages:
Install the packages:
----
# apt install ./python3-pypeg2_*_all.deb
# apt install ./qutebrowser*.deb
# apt install ./qutebrowser_*_all.deb
----
For an update after the initial install, you only need to download/install the
qutebrowser package.
Debian Testing / Ubuntu 18.04
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -426,11 +417,7 @@ Creating a wrapper script
~~~~~~~~~~~~~~~~~~~~~~~~~
Running `tox` does not install a system-wide `qutebrowser` script. You can
launch qutebrowser by doing:
----
.venv/bin/python3 -m qutebrowser
----
launch qutebrowser by doing `.venv/bin/python3 -m qutebrowser`.
You can create a simple wrapper script to start qutebrowser somewhere in your
`$PATH` (e.g. `/usr/local/bin/qutebrowser` or `~/bin/qutebrowser`):

Binary file not shown.

Before

Width:  |  Height:  |  Size: 113 KiB

After

Width:  |  Height:  |  Size: 45 KiB

View File

@@ -1,31 +1,25 @@
PYTHON = python3
PREFIX = /usr/local
DESTDIR =
DESTDIR = /
ICONSIZES = 16 24 32 48 64 128 256 512
SETUPTOOLSOPTIONS =
ifdef DESTDIR
SETUPTOOLSOPTS = --root="$(DESTDIR)"
endif
.PHONY: install
doc/qutebrowser.1.html:
a2x -f manpage doc/qutebrowser.1.asciidoc
install: doc/qutebrowser.1.html
$(PYTHON) setup.py install --prefix="$(PREFIX)" --optimize=1 $(SETUPTOOLSOPTS)
$(PYTHON) setup.py install --root="$(DESTDIR)" --optimize=1
install -Dm644 doc/qutebrowser.1 \
"$(DESTDIR)$(PREFIX)/share/man/man1/qutebrowser.1"
"$(DESTDIR)/usr/share/man/man1/qutebrowser.1"
install -Dm644 misc/qutebrowser.desktop \
"$(DESTDIR)$(PREFIX)/share/applications/qutebrowser.desktop"
"$(DESTDIR)/usr/share/applications/qutebrowser.desktop"
$(foreach i,$(ICONSIZES),install -Dm644 "icons/qutebrowser-$(i)x$(i).png" \
"$(DESTDIR)$(PREFIX)/share/icons/hicolor/$(i)x$(i)/apps/qutebrowser.png";)
"$(DESTDIR)/usr/share/icons/hicolor/$(i)x$(i)/apps/qutebrowser.png";)
install -Dm644 icons/qutebrowser.svg \
"$(DESTDIR)$(PREFIX)/share/icons/hicolor/scalable/apps/qutebrowser.svg"
install -Dm755 -t "$(DESTDIR)$(PREFIX)/share/qutebrowser/userscripts/" \
"$(DESTDIR)/usr/share/icons/hicolor/scalable/apps/qutebrowser.svg"
install -Dm755 -t "$(DESTDIR)/usr/share/qutebrowser/userscripts/" \
$(wildcard misc/userscripts/*)
install -Dm755 -t "$(DESTDIR)$(PREFIX)/share/qutebrowser/scripts/" \
install -Dm755 -t "$(DESTDIR)/usr/share/qutebrowser/scripts/" \
$(filter-out scripts/__init__.py scripts/__pycache__ scripts/dev \
scripts/testbrowser scripts/asciidoc2html.py scripts/setupcommon.py \
scripts/link_pyqt.py,$(wildcard scripts/*))

View File

@@ -33,7 +33,7 @@
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="1.7536248"
inkscape:cx="430.72917"
inkscape:cx="376.55567"
inkscape:cy="268.64059"
inkscape:document-units="px"
inkscape:current-layer="layer1"
@@ -3710,7 +3710,7 @@
style="font-weight:bold;font-size:10.66666698px;line-height:1.25;font-family:sans-serif;-inkscape-font-specification:'Sans Bold';fill:#000000;stroke-width:1.06666672"
id="flowPara5701-9-2"><flowSpan
style="font-weight:bold;font-family:sans-serif;-inkscape-font-specification:'Sans Bold';fill:#ff0000;stroke-width:1.06666672"
id="flowSpan5705-5-1">(12)</flowSpan> toggling settings:</flowPara><flowPara
id="flowSpan5705-5-1">(10)</flowSpan> toggling settings:</flowPara><flowPara
style="font-size:10.66666698px;line-height:1.25;font-family:sans-serif;fill:#000000;stroke-width:1.06666672"
id="flowPara6196">tsh - toggle scripts for the current host (temporarily)</flowPara><flowPara
style="font-size:10.66666698px;line-height:1.25;font-family:sans-serif;fill:#000000;stroke-width:1.06666672"

Before

Width:  |  Height:  |  Size: 181 KiB

After

Width:  |  Height:  |  Size: 181 KiB

View File

@@ -15,7 +15,7 @@ def get_data_files():
('../qutebrowser/img', 'img'),
('../qutebrowser/javascript', 'javascript'),
('../qutebrowser/html/doc', 'html/doc'),
('../qutebrowser/git-commit-id', '.'),
('../qutebrowser/git-commit-id', ''),
('../qutebrowser/config/configdata.yml', 'config'),
]
@@ -58,14 +58,14 @@ exe = EXE(pyz,
icon=icon,
debug=False,
strip=False,
upx=False,
upx=True,
console=False )
coll = COLLECT(exe,
a.binaries,
a.zipfiles,
a.datas,
strip=False,
upx=False,
upx=True,
name='qutebrowser')
app = BUNDLE(coll,

View File

@@ -1,3 +1,3 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py
check-manifest==0.37
check-manifest==0.36

View File

@@ -1,6 +1,6 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py
certifi==2018.4.16
certifi==2018.1.18
chardet==3.0.4
codecov==2.0.15
coverage==4.5.1

View File

@@ -3,7 +3,7 @@
attrs==17.4.0
flake8==3.5.0
flake8-bugbear==18.2.0
flake8-builtins==1.3.1
flake8-builtins==1.0.post0
flake8-comprehensions==1.4.1
flake8-copyright==0.2.0
flake8-debugger==3.1.0
@@ -11,17 +11,15 @@ flake8-deprecated==1.3
flake8-docstrings==1.3.0
flake8-future-import==0.4.4
flake8-mock==0.3
flake8-per-file-ignores==0.6
flake8-per-file-ignores==0.4
flake8-polyfill==1.0.2
flake8-string-format==0.2.3
flake8-tidy-imports==1.1.0
flake8-tuple==0.2.13
mccabe==0.6.1
pathmatch==0.2.1
pep8-naming==0.5.0
pycodestyle==2.3.1 # rq.filter: < 2.4.0
pycodestyle==2.3.1
pydocstyle==2.1.1
pyflakes==1.6.0
six==1.11.0
snowballstemmer==1.2.1
typing==3.6.4

View File

@@ -15,6 +15,3 @@ flake8-tuple
pep8-naming
pydocstyle
pyflakes
# https://github.com/PyCQA/pycodestyle/issues/741
#@ filter: pycodestyle < 2.4.0

View File

@@ -3,6 +3,6 @@
appdirs==1.4.3
packaging==17.1
pyparsing==2.2.0
setuptools==39.1.0
setuptools==38.5.1
six==1.11.0
wheel==0.31.0
wheel==0.30.0

View File

@@ -4,4 +4,4 @@ altgraph==0.15
future==0.16.0
macholib==1.9
pefile==2017.11.5
PyInstaller==3.3.1
-e git+https://github.com/pyinstaller/pyinstaller.git@develop#egg=PyInstaller

View File

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

View File

@@ -1,18 +1,18 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py
-e git+https://github.com/PyCQA/astroid.git#egg=astroid
certifi==2018.4.16
certifi==2018.1.18
chardet==3.0.4
github3.py==1.1.0
github3.py==0.9.6
idna==2.6
isort==4.3.4
lazy-object-proxy==1.3.1
mccabe==0.6.1
-e git+https://github.com/PyCQA/pylint.git#egg=pylint
python-dateutil==2.7.2
./scripts/dev/pylint_checkers
requests==2.18.4
six==1.11.0
uritemplate==3.0.0
uritemplate.py==3.0.2
urllib3==1.22
wrapt==1.10.11

View File

@@ -1,18 +1,18 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py
astroid==1.6.3
certifi==2018.4.16
astroid==1.6.1
certifi==2018.1.18
chardet==3.0.4
github3.py==1.1.0
github3.py==0.9.6
idna==2.6
isort==4.3.4
lazy-object-proxy==1.3.1
mccabe==0.6.1
pylint==1.8.4
python-dateutil==2.7.2
pylint==1.8.2
./scripts/dev/pylint_checkers
requests==2.18.4
six==1.11.0
uritemplate==3.0.0
uritemplate.py==3.0.2
urllib3==1.22
wrapt==1.10.11

View File

@@ -2,16 +2,16 @@
attrs==17.4.0
beautifulsoup4==4.6.0
cheroot==6.2.4
cheroot==6.0.0
click==6.7
# colorama==0.3.9
coverage==4.5.1
EasyProcess==0.2.3
fields==5.0.0
Flask==1.0.1
Flask==0.12.2
glob2==0.6
hunter==2.0.2
hypothesis==3.56.5
hypothesis==3.48.0
itsdangerous==0.24
# Jinja2==2.10
Mako==1.0.7
@@ -20,15 +20,15 @@ more-itertools==4.1.0
parse==1.8.2
parse-type==0.4.2
pluggy==0.6.0
py==1.5.3
py-cpuinfo==4.0.0
pytest==3.5.1
pytest-bdd==2.21.0
py==1.5.2
py-cpuinfo==3.3.0
pytest==3.4.1
pytest-bdd==2.20.0
pytest-benchmark==3.1.1
pytest-cov==2.5.1
pytest-faulthandler==1.5.0
pytest-faulthandler==1.4.1
pytest-instafail==0.3.0
pytest-mock==1.9.0
pytest-mock==1.7.1
pytest-qt==2.3.1
pytest-repeat==0.4.1
pytest-rerunfailures==4.0

View File

@@ -1,7 +1,7 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py
pluggy==0.6.0
py==1.5.3
py==1.5.2
six==1.11.0
tox==3.0.0
virtualenv==15.2.0
tox==2.9.1
virtualenv==15.1.0

View File

@@ -1 +1,4 @@
tox
# The latest tox release still depends on pluggy < 0.4...
pluggy==0.4.0

View File

@@ -1,69 +0,0 @@
#!/usr/bin/env python3
"""Qutebrowser userscript scraping the current web page for DOIs and downloading
corresponding bibtex information.
Set the environment variable 'QUTE_BIB_FILEPATH' to indicate the path to
download to. Otherwise, bibtex information is downloaded to '/tmp' and hence
deleted at reboot.
Installation: see qute://help/userscripts.html
Inspired by
https://ocefpaf.github.io/python4oceanographers/blog/2014/05/19/doi2bibtex/
"""
import os
import sys
import shutil
import re
from collections import Counter
from urllib import parse as url_parse
from urllib import request as url_request
FIFO_PATH = os.getenv("QUTE_FIFO")
def message_fifo(message, level="warning"):
"""Send message to qutebrowser FIFO. The level must be one of 'info',
'warning' (default) or 'error'."""
with open(FIFO_PATH, "w") as fifo:
fifo.write("message-{} '{}'".format(level, message))
source = os.getenv("QUTE_TEXT")
with open(source) as f:
text = f.read()
# find DOIs on page using regex
dval = re.compile(r'(10\.(\d)+/([^(\s\>\"\<)])+)')
# https://stackoverflow.com/a/10324802/3865876, too strict
# dval = re.compile(r'\b(10[.][0-9]{4,}(?:[.][0-9]+)*/(?:(?!["&\'<>])\S)+)\b')
dois = dval.findall(text)
dois = Counter(e[0] for e in dois)
try:
doi = dois.most_common(1)[0][0]
except IndexError:
message_fifo("No DOIs found on page")
sys.exit()
message_fifo("Found {} DOIs on page, selecting {}".format(len(dois), doi),
level="info")
# get bibtex data corresponding to DOI
url = "http://dx.doi.org/" + url_parse.quote(doi)
headers = dict(Accept='text/bibliography; style=bibtex')
request = url_request.Request(url, headers=headers)
response = url_request.urlopen(request)
status_code = response.getcode()
if status_code >= 400:
message_fifo("Request returned {}".format(status_code))
sys.exit()
# obtain content and format it
bibtex = response.read().decode("utf-8").strip()
bibtex = bibtex.replace(" ", "\n ", 1).\
replace("}, ", "},\n ").replace("}}", "}\n}")
# append to file
bib_filepath = os.getenv("QUTE_BIB_FILEPATH", "/tmp/qute.bib")
with open(bib_filepath, "a") as f:
f.write(bibtex + "\n\n")

View File

@@ -1,261 +0,0 @@
#!/usr/bin/env python3
# Copyright 2018 Jay Kamat <jaygkamat@gmail.com>
#
# 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/>.
"""This userscript allows for insertion of usernames and passwords from keepass
databases using pykeepass. Since it is a userscript, it must be run from
qutebrowser.
A sample invocation of this script is:
:spawn --userscript qute-keepass -p ~/KeePassFiles/MainDatabase.kdbx
And a sample binding
:bind --mode=insert <ctrl-i> spawn --userscript qute-keepass -p ~/KeePassFiles/MainDatabase.kdbx
-p or --path is a required argument.
--keyfile-path allows you to specify a keepass keyfile. If you only use a
keyfile, also add --no-password as well. Specifying --no-password without
--keyfile-path will lead to an error.
login information is inserted using :insert-text and :fake-key <Tab>, which
means you must have a cursor in position before initiating this userscript. If
you do not do this, you will get 'element not editable' errors.
If keepass takes a while to open the DB, you might want to consider reducing
the number of transform rounds in your database settings.
Dependencies: pykeepass (in python3), PyQt5. Without pykeepass, you will get an
exit code of 100.
********************!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!******************
WARNING: The login details are viewable as plaintext in qutebrowser's debug log
(qute://log) and could be compromised if you decide to submit a crash report!
********************!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!******************
"""
# pylint: disable=bad-builtin
import argparse
import enum
import functools
import os
import shlex
import subprocess
import sys
from PyQt5.QtCore import QUrl
from PyQt5.QtWidgets import QApplication, QInputDialog, QLineEdit
try:
import pykeepass
except ImportError as e:
print("pykeepass not found: {}".format(str(e)), file=sys.stderr)
# Since this is a common error, try to print it to the FIFO if we can.
if 'QUTE_FIFO' in os.environ:
with open(os.environ['QUTE_FIFO'], 'w') as fifo:
fifo.write('message-error "pykeepass failed to be imported."\n')
fifo.flush()
sys.exit(100)
argument_parser = argparse.ArgumentParser(
description="Fill passwords using keepass.",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog=__doc__)
argument_parser.add_argument('url', nargs='?', default=os.getenv('QUTE_URL'))
argument_parser.add_argument('--path', '-p', required=True,
help='Path to the keepass db.')
argument_parser.add_argument('--keyfile-path', '-k', default=None,
help='Path to a keepass keyfile')
argument_parser.add_argument(
'--no-password', action='store_true',
help='Supply if no password is required to unlock this database. '
'Only allowed with --keyfile-path')
argument_parser.add_argument(
'--dmenu-invocation', '-d', default='dmenu',
help='Invocation used to execute a dmenu-provider')
argument_parser.add_argument(
'--dmenu-format', '-f', default='{title}: {username}',
help='Format string for keys to display in dmenu.'
' Must generate a unique string.')
argument_parser.add_argument(
'--no-insert-mode', '-n', dest='insert_mode', action='store_false',
help="Don't automatically enter insert mode")
argument_parser.add_argument(
'--io-encoding', '-i', default='UTF-8',
help='Encoding used to communicate with subprocesses')
group = argument_parser.add_mutually_exclusive_group()
group.add_argument('--username-fill-only', '-e',
action='store_true', help='Only insert username')
group.add_argument('--password-fill-only', '-w',
action='store_true', help='Only insert password')
CMD_DELAY = 50
class ExitCodes(enum.IntEnum):
"""Stores various exit codes groups to use."""
SUCCESS = 0
FAILURE = 1
# 1 is automatically used if Python throws an exception
NO_CANDIDATES = 2
USER_QUIT = 3
DB_OPEN_FAIL = 4
INTERNAL_ERROR = 10
def qute_command(command):
with open(os.environ['QUTE_FIFO'], 'w') as fifo:
fifo.write(command + '\n')
fifo.flush()
def stderr(to_print):
"""Extra functionality to echo out errors to qb ui."""
print(to_print, file=sys.stderr)
qute_command('message-error "{}"'.format(to_print))
def dmenu(items, invocation, encoding):
"""Runs dmenu with given arguments."""
command = shlex.split(invocation)
process = subprocess.run(command, input='\n'.join(items).encode(encoding),
stdout=subprocess.PIPE)
return process.stdout.decode(encoding).strip()
def get_password():
"""Get a keepass db password from user."""
_app = QApplication(sys.argv)
text, ok = QInputDialog.getText(
None, "KeePass DB Password",
"Please enter your KeePass Master Password",
QLineEdit.Password)
if not ok:
stderr('Password Prompt Rejected.')
sys.exit(ExitCodes.USER_QUIT)
return text
def find_candidates(args, host):
"""Finds candidates that match host"""
file_path = os.path.expanduser(args.path)
# TODO find a way to keep the db open, so we don't open (and query
# password) it every time
pw = None
if not args.no_password:
pw = get_password()
kf = args.keyfile_path
if kf:
kf = os.path.expanduser(kf)
try:
kp = pykeepass.PyKeePass(file_path, password=pw, keyfile=kf)
except Exception as e:
stderr("There was an error opening the DB: {}".format(str(e)))
return kp.find_entries(url="{}{}{}".format(".*", host, ".*"), regex=True)
def candidate_to_str(args, candidate):
"""Turns candidate into a human readable string for dmenu"""
return args.dmenu_format.format(title=candidate.title,
url=candidate.url,
username=candidate.username,
path=candidate.path,
uuid=candidate.uuid)
def candidate_to_secret(candidate):
"""Turns candidate into a generic (user, password) tuple"""
return (candidate.username, candidate.password)
def run(args):
"""Runs qute-keepass"""
if not args.url:
argument_parser.print_help()
return ExitCodes.FAILURE
url_host = QUrl(args.url).host()
if not url_host:
stderr('{} was not parsed as a valid URL!'.format(args.url))
return ExitCodes.INTERNAL_ERROR
# Find candidates matching the host of the given URL
candidates = find_candidates(args, url_host)
if not candidates:
stderr('No candidates for URL {!r} found!'.format(args.url))
return ExitCodes.NO_CANDIDATES
# Create a map so we can get turn the resulting string from dmenu back into
# a candidate
candidates_strs = list(map(functools.partial(candidate_to_str, args),
candidates))
candidates_map = dict(zip(candidates_strs, candidates))
if len(candidates) == 1:
selection = candidates.pop()
else:
selection = dmenu(candidates_strs,
args.dmenu_invocation,
args.io_encoding)
if selection not in candidates_map:
stderr("'{}' was not a valid entry!").format(selection)
return ExitCodes.USER_QUIT
selection = candidates_map[selection]
username, password = candidate_to_secret(selection)
insert_mode = ';; enter-mode insert' if args.insert_mode else ''
if args.username_fill_only:
qute_command('insert-text {}{}'.format(username, insert_mode))
elif args.password_fill_only:
qute_command('insert-text {}{}'.format(password, insert_mode))
else:
# Enter username and password using insert-key and fake-key <Tab>
# (which supports more passwords than fake-key only), then switch back
# into insert-mode, so the form can be directly submitted by hitting
# enter afterwards. It dosen't matter when we go into insert mode, but
# the other commands need to be be executed sequentially, so we add
# delays with later.
qute_command('insert-text {} ;;'
'later {} fake-key <Tab> ;;'
'later {} insert-text {}{}'
.format(username, CMD_DELAY,
CMD_DELAY * 2, password, insert_mode))
return ExitCodes.SUCCESS
if __name__ == '__main__':
arguments = argument_parser.parse_args()
sys.exit(run(arguments))

View File

@@ -109,13 +109,6 @@ def dmenu(items, invocation, encoding):
return process.stdout.decode(encoding).strip()
def fake_key_raw(text):
for character in text:
# Escape all characters by default, space requires special handling
sequence = '" "' if character == ' ' else '\{}'.format(character)
qute_command('fake-key {}'.format(sequence))
def main(arguments):
if not arguments.url:
argument_parser.print_help()
@@ -165,19 +158,15 @@ def main(arguments):
return ExitCodes.COULD_NOT_MATCH_PASSWORD
password = match.group(1)
insert_mode = ';; enter-mode insert' if arguments.insert_mode else ''
if arguments.username_only:
fake_key_raw(username)
qute_command('fake-key {}{}'.format(username, insert_mode))
elif arguments.password_only:
fake_key_raw(password)
qute_command('fake-key {}{}'.format(password, insert_mode))
else:
# Enter username and password using fake-key and <Tab> (which seems to work almost universally), then switch
# back into insert-mode, so the form can be directly submitted by hitting enter afterwards
fake_key_raw(username)
qute_command('fake-key <Tab>')
fake_key_raw(password)
if arguments.insert_mode:
qute_command('enter-mode insert')
qute_command('fake-key {} ;; fake-key <Tab> ;; fake-key {}{}'.format(username, password, insert_mode))
return ExitCodes.SUCCESS

View File

@@ -28,7 +28,7 @@
if msg="$(task add "$title" "$*" 2>&1)"; then
# annotate the new task with the url, send the output back to the browser
task +LATEST annotate "$QUTE_URL"
echo "message-info '$(echo "$msg" | head -n 1)'" >> "$QUTE_FIFO"
echo "message-info '$msg'" >> "$QUTE_FIFO"
else
echo "message-error '$(echo "$msg" | head -n 1)'" >> "$QUTE_FIFO"
echo "message-error '$msg'" >> "$QUTE_FIFO"
fi

View File

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

View File

@@ -340,7 +340,7 @@ def _open_startpage(win_id=None):
for cur_win_id in list(window_ids): # Copying as the dict could change
tabbed_browser = objreg.get('tabbed-browser', scope='window',
window=cur_win_id)
if tabbed_browser.widget.count() == 0:
if tabbed_browser.count() == 0:
log.init.debug("Opening start pages")
for url in config.val.url.start_pages:
tabbed_browser.tabopen(url)

View File

@@ -94,8 +94,14 @@ class HostBlocker:
_done_count: How many files have been read successfully.
_local_hosts_file: The path to the blocked-hosts file.
_config_hosts_file: The path to a blocked-hosts in ~/.config
Class attributes:
WHITELISTED: Hosts which never should be blocked.
"""
WHITELISTED = ('localhost', 'localhost.localdomain', 'broadcasthost',
'local')
def __init__(self):
self._blocked_hosts = set()
self._config_blocked_hosts = set()
@@ -228,14 +234,16 @@ class HostBlocker:
parts = line.split()
if len(parts) == 1:
# "one host per line" format
hosts = [parts[0]]
else:
host = parts[0]
elif len(parts) == 2:
# /etc/hosts format
hosts = parts[1:]
host = parts[1]
else:
log.misc.error("Failed to parse: {!r}".format(line))
return False
for host in hosts:
if '.' in host and not host.endswith('.localdomain'):
self._blocked_hosts.add(host)
if host not in self.WHITELISTED:
self._blocked_hosts.add(host)
return True

View File

@@ -114,10 +114,6 @@ class TabData:
netrc_used = attr.ib(False)
input_mode = attr.ib(usertypes.KeyMode.normal)
def should_show_icon(self):
return (config.val.tabs.favicons.show == 'always' or
config.val.tabs.favicons.show == 'pinned' and self.pinned)
class AbstractAction:
@@ -337,14 +333,7 @@ class AbstractZoom(QObject):
class AbstractCaret(QObject):
"""Attribute of AbstractTab for caret browsing.
Signals:
selection_toggled: Emitted when the selection was toggled.
arg: Whether the selection is now active.
"""
selection_toggled = pyqtSignal(bool)
"""Attribute of AbstractTab for caret browsing."""
def __init__(self, tab, mode_manager, parent=None):
super().__init__(parent)
@@ -450,9 +439,6 @@ class AbstractScroller(QObject):
def to_point(self, point):
raise NotImplementedError
def to_anchor(self, name):
raise NotImplementedError
def delta(self, x=0, y=0):
raise NotImplementedError
@@ -679,7 +665,8 @@ class AbstractTab(QWidget):
objreg.register('hintmanager', hintmanager, scope='tab',
window=self.win_id, tab=self.tab_id)
self.predicted_navigation.connect(self._on_predicted_navigation)
self.predicted_navigation.connect(
lambda url: self.title_changed.emit(url.toDisplayString()))
def _set_widget(self, widget):
# pylint: disable=protected-access
@@ -724,24 +711,10 @@ class AbstractTab(QWidget):
if getattr(evt, 'posted', False):
raise utils.Unreachable("Can't re-use an event which was already "
"posted!")
recipient = self.event_target()
if recipient is None:
# https://github.com/qutebrowser/qutebrowser/issues/3888
log.webview.warning("Unable to find event target!")
return
evt.posted = True
QApplication.postEvent(recipient, evt)
@pyqtSlot(QUrl)
def _on_predicted_navigation(self, url):
"""Adjust the title if we are going to visit an URL soon."""
qtutils.ensure_valid(url)
url_string = url.toDisplayString()
log.webview.debug("Predicted navigation: {}".format(url_string))
self.title_changed.emit(url_string)
@pyqtSlot(QUrl)
def _on_url_changed(self, url):
"""Update title when URL has changed and no title is available."""
@@ -842,12 +815,11 @@ class AbstractTab(QWidget):
def load_status(self):
return self._load_status
def _openurl_prepare(self, url, *, predict=True):
def _openurl_prepare(self, url):
qtutils.ensure_valid(url)
if predict:
self.predicted_navigation.emit(url)
self.predicted_navigation.emit(url)
def openurl(self, url, *, predict=True):
def openurl(self, url):
raise NotImplementedError
def reload(self, *, force=False):

View File

@@ -53,6 +53,7 @@ class CommandDispatcher:
cmdutils.register() decorators are run, currentWidget() will return None.
Attributes:
_editor: The ExternalEditor object.
_win_id: The window ID the CommandDispatcher is associated with.
_tabbed_browser: The TabbedBrowser used.
"""
@@ -72,16 +73,16 @@ class CommandDispatcher:
def _count(self):
"""Convenience method to get the widget count."""
return self._tabbed_browser.widget.count()
return self._tabbed_browser.count()
def _set_current_index(self, idx):
"""Convenience method to set the current widget index."""
cmdutils.check_overflow(idx, 'int')
self._tabbed_browser.widget.setCurrentIndex(idx)
self._tabbed_browser.setCurrentIndex(idx)
def _current_index(self):
"""Convenience method to get the current widget index."""
return self._tabbed_browser.widget.currentIndex()
return self._tabbed_browser.currentIndex()
def _current_url(self):
"""Convenience method to get the current url."""
@@ -100,7 +101,7 @@ class CommandDispatcher:
def _current_widget(self):
"""Get the currently active widget from a command."""
widget = self._tabbed_browser.widget.currentWidget()
widget = self._tabbed_browser.currentWidget()
if widget is None:
raise cmdexc.CommandError("No WebView available yet!")
return widget
@@ -146,10 +147,10 @@ class CommandDispatcher:
None if no widget was found.
"""
if count is None:
return self._tabbed_browser.widget.currentWidget()
return self._tabbed_browser.currentWidget()
elif 1 <= count <= self._count():
cmdutils.check_overflow(count + 1, 'int')
return self._tabbed_browser.widget.widget(count - 1)
return self._tabbed_browser.widget(count - 1)
else:
return None
@@ -162,7 +163,7 @@ class CommandDispatcher:
if not show_error:
return
raise cmdexc.CommandError("No last focused tab!")
idx = self._tabbed_browser.widget.indexOf(tab)
idx = self._tabbed_browser.indexOf(tab)
if idx == -1:
raise cmdexc.CommandError("Last focused tab vanished!")
self._set_current_index(idx)
@@ -211,7 +212,7 @@ class CommandDispatcher:
what's configured in 'tabs.select_on_remove'.
count: The tab index to close, or None
"""
tabbar = self._tabbed_browser.widget.tabBar()
tabbar = self._tabbed_browser.tabBar()
selection_override = self._get_selection_override(prev, next_,
opposite)
@@ -263,7 +264,7 @@ class CommandDispatcher:
return
to_pin = not tab.data.pinned
self._tabbed_browser.widget.set_tab_pinned(tab, to_pin)
self._tabbed_browser.set_tab_pinned(tab, to_pin)
@cmdutils.register(instance='command-dispatcher', name='open',
maxsplit=0, scope='window')
@@ -482,8 +483,7 @@ class CommandDispatcher:
"""
cmdutils.check_exclusive((bg, window), 'bw')
curtab = self._current_widget()
cur_title = self._tabbed_browser.widget.page_title(
self._current_index())
cur_title = self._tabbed_browser.page_title(self._current_index())
try:
history = curtab.history.serialize()
except browsertab.WebTabError as e:
@@ -499,18 +499,18 @@ class CommandDispatcher:
newtab = new_tabbed_browser.tabopen(background=bg)
new_tabbed_browser = objreg.get('tabbed-browser', scope='window',
window=newtab.win_id)
idx = new_tabbed_browser.widget.indexOf(newtab)
idx = new_tabbed_browser.indexOf(newtab)
new_tabbed_browser.widget.set_page_title(idx, cur_title)
if curtab.data.should_show_icon():
new_tabbed_browser.widget.setTabIcon(idx, curtab.icon())
new_tabbed_browser.set_page_title(idx, cur_title)
if config.val.tabs.favicons.show:
new_tabbed_browser.setTabIcon(idx, curtab.icon())
if config.val.tabs.tabs_are_windows:
new_tabbed_browser.widget.window().setWindowIcon(curtab.icon())
new_tabbed_browser.window().setWindowIcon(curtab.icon())
newtab.data.keep_icon = True
newtab.history.deserialize(history)
newtab.zoom.set_factor(curtab.zoom.factor())
new_tabbed_browser.widget.set_tab_pinned(newtab, curtab.data.pinned)
new_tabbed_browser.set_tab_pinned(newtab, curtab.data.pinned)
return newtab
@cmdutils.register(instance='command-dispatcher', scope='window')
@@ -768,15 +768,6 @@ class CommandDispatcher:
self._current_widget().scroller.to_perc(x, y)
@cmdutils.register(instance='command-dispatcher', scope='window')
def scroll_to_anchor(self, name):
"""Scroll to the given anchor in the document.
Args:
name: The anchor to scroll to.
"""
self._current_widget().scroller.to_anchor(name)
@cmdutils.register(instance='command-dispatcher', scope='window')
@cmdutils.argument('count', count=True)
@cmdutils.argument('top_navigate', metavar='ACTION',
@@ -855,7 +846,7 @@ class CommandDispatcher:
keep: Stay in visual mode after yanking the selection.
"""
if what == 'title':
s = self._tabbed_browser.widget.page_title(self._current_index())
s = self._tabbed_browser.page_title(self._current_index())
elif what == 'domain':
port = self._current_url().port()
s = '{}://{}{}'.format(self._current_url().scheme(),
@@ -967,7 +958,7 @@ class CommandDispatcher:
force: Avoid confirmation for pinned tabs.
"""
cmdutils.check_exclusive((prev, next_), 'pn')
cur_idx = self._tabbed_browser.widget.currentIndex()
cur_idx = self._tabbed_browser.currentIndex()
assert cur_idx != -1
def _to_close(i):
@@ -1022,7 +1013,7 @@ class CommandDispatcher:
elif config.val.tabs.wrap:
self._set_current_index(newidx % self._count())
else:
log.webview.debug("First tab")
raise cmdexc.CommandError("First tab")
@cmdutils.register(instance='command-dispatcher', scope='window')
@cmdutils.argument('count', count=True)
@@ -1042,7 +1033,7 @@ class CommandDispatcher:
elif config.val.tabs.wrap:
self._set_current_index(newidx % self._count())
else:
log.webview.debug("Last tab")
raise cmdexc.CommandError("Last tab")
def _resolve_buffer_index(self, index):
"""Resolve a buffer index to the tabbedbrowser and tab.
@@ -1084,11 +1075,11 @@ class CommandDispatcher:
tabbed_browser = objreg.get('tabbed-browser', scope='window',
window=win_id)
if not 0 < idx <= tabbed_browser.widget.count():
if not 0 < idx <= tabbed_browser.count():
raise cmdexc.CommandError(
"There's no tab with index {}!".format(idx))
return (tabbed_browser, tabbed_browser.widget.widget(idx-1))
return (tabbed_browser, tabbed_browser.widget(idx-1))
@cmdutils.register(instance='command-dispatcher', scope='window',
maxsplit=0)
@@ -1116,10 +1107,10 @@ class CommandDispatcher:
tabbed_browser, tab = self._resolve_buffer_index(index)
window = tabbed_browser.widget.window()
window = tabbed_browser.window()
window.activateWindow()
window.raise_()
tabbed_browser.widget.setCurrentWidget(tab)
tabbed_browser.setCurrentWidget(tab)
@cmdutils.register(instance='command-dispatcher', scope='window')
@cmdutils.argument('index', choices=['last'])
@@ -1203,7 +1194,7 @@ class CommandDispatcher:
cur_idx = self._current_index()
cmdutils.check_overflow(cur_idx, 'int')
cmdutils.check_overflow(new_idx, 'int')
self._tabbed_browser.widget.tabBar().moveTab(cur_idx, new_idx)
self._tabbed_browser.tabBar().moveTab(cur_idx, new_idx)
@cmdutils.register(instance='command-dispatcher', scope='window',
maxsplit=0, no_replace_variables=True)
@@ -1287,10 +1278,10 @@ class CommandDispatcher:
idx = self._current_index()
if idx != -1:
env['QUTE_TITLE'] = self._tabbed_browser.widget.page_title(idx)
env['QUTE_TITLE'] = self._tabbed_browser.page_title(idx)
# FIXME:qtwebengine: If tab is None, run_async will fail!
tab = self._tabbed_browser.widget.currentWidget()
tab = self._tabbed_browser.currentWidget()
try:
url = self._tabbed_browser.current_url()
@@ -1648,7 +1639,7 @@ class CommandDispatcher:
ed = editor.ExternalEditor(watch=True, parent=self._tabbed_browser)
ed.file_updated.connect(functools.partial(
self.on_file_updated, ed, elem))
self.on_file_updated, elem))
ed.editing_finished.connect(lambda: mainwindow.raise_window(
objreg.last_focused_window(), alert=False))
ed.edit(text, caret_position)
@@ -1663,7 +1654,7 @@ class CommandDispatcher:
tab = self._current_widget()
tab.elements.find_focused(self._open_editor_cb)
def on_file_updated(self, ed, elem, text):
def on_file_updated(self, elem, text):
"""Write the editor text into the form field and clean up tempfile.
Callback for GUIProcess when the edited text was updated.
@@ -1676,10 +1667,8 @@ class CommandDispatcher:
elem.set_value(text)
except webelem.OrphanedError as e:
message.error('Edited element vanished')
ed.backup()
except webelem.Error as e:
message.error(str(e))
ed.backup()
raise cmdexc.CommandError(str(e))
@cmdutils.register(instance='command-dispatcher', maxsplit=0,
scope='window')
@@ -2228,5 +2217,5 @@ class CommandDispatcher:
pass
return
window = self._tabbed_browser.widget.window()
window = self._tabbed_browser.window()
window.setWindowState(window.windowState() ^ Qt.WindowFullScreen)

View File

@@ -30,8 +30,7 @@ import textwrap
import attr
from PyQt5.QtCore import pyqtSignal, QObject, QUrl
from qutebrowser.utils import (log, standarddir, jinja, objreg, utils,
javascript, urlmatch)
from qutebrowser.utils import log, standarddir, jinja, objreg, utils
from qutebrowser.commands import cmdutils
from qutebrowser.browser import downloads
@@ -48,7 +47,6 @@ class GreasemonkeyScript:
def __init__(self, properties, code):
self._code = code
self.includes = []
self.matches = []
self.excludes = []
self.requires = []
self.description = None
@@ -64,10 +62,8 @@ class GreasemonkeyScript:
self.namespace = value
elif name == 'description':
self.description = value
elif name == 'include':
elif name in ['include', 'match']:
self.includes.append(value)
elif name == 'match':
self.matches.append(value)
elif name in ['exclude', 'exclude_match']:
self.excludes.append(value)
elif name == 'run-at':
@@ -95,7 +91,7 @@ class GreasemonkeyScript:
props = ""
script = cls(re.findall(cls.PROPS_REGEX, props), source)
script.script_meta = props
if not script.includes and not script.matches:
if not props:
script.includes = ['*']
return script
@@ -108,19 +104,18 @@ class GreasemonkeyScript:
browser's debugger/inspector will not match up to the line
numbers in the source script directly.
"""
template = jinja.js_environment.get_template('greasemonkey_wrapper.js')
return template.render(
scriptName=javascript.string_escape(
"/".join([self.namespace or '', self.name])),
scriptInfo=self._meta_json(),
scriptMeta=javascript.string_escape(self.script_meta),
scriptSource=self._code)
return jinja.js_environment.get_template(
'greasemonkey_wrapper.js').render(
scriptName="/".join([self.namespace or '', self.name]),
scriptInfo=self._meta_json(),
scriptMeta=self.script_meta,
scriptSource=self._code)
def _meta_json(self):
return json.dumps({
'name': self.name,
'description': self.description,
'matches': self.matches,
'matches': self.includes,
'includes': self.includes,
'excludes': self.excludes,
'run-at': self.run_at,
@@ -146,42 +141,6 @@ class MatchingScripts(object):
idle = attr.ib(default=attr.Factory(list))
class GreasemonkeyMatcher:
"""Check whether scripts should be loaded for a given URL."""
# https://wiki.greasespot.net/Include_and_exclude_rules#Greaseable_schemes
# Limit the schemes scripts can run on due to unreasonable levels of
# exploitability
GREASEABLE_SCHEMES = ['http', 'https', 'ftp', 'file']
def __init__(self, url):
self._url = url
self._url_string = url.toString(QUrl.FullyEncoded)
self.is_greaseable = url.scheme() in self.GREASEABLE_SCHEMES
def _match_pattern(self, pattern):
# For include and exclude rules if they start and end with '/' they
# should be treated as a (ecma syntax) regular expression.
if pattern.startswith('/') and pattern.endswith('/'):
matches = re.search(pattern[1:-1], self._url_string, flags=re.I)
return matches is not None
# Otherwise they are glob expressions.
return fnmatch.fnmatch(self._url_string, pattern)
def matches(self, script):
"""Check whether the URL matches filtering rules of the script."""
assert self.is_greaseable
matching_includes = any(self._match_pattern(pat)
for pat in script.includes)
matching_match = any(urlmatch.UrlPattern(pat).matches(self._url)
for pat in script.matches)
matching_excludes = any(self._match_pattern(pat)
for pat in script.excludes)
return (matching_includes or matching_match) and not matching_excludes
class GreasemonkeyManager(QObject):
"""Manager of userscripts and a Greasemonkey compatible environment.
@@ -193,6 +152,10 @@ class GreasemonkeyManager(QObject):
"""
scripts_reloaded = pyqtSignal()
# https://wiki.greasespot.net/Include_and_exclude_rules#Greaseable_schemes
# Limit the schemes scripts can run on due to unreasonable levels of
# exploitability
greaseable_schemes = ['http', 'https', 'ftp', 'file']
def __init__(self, parent=None):
super().__init__(parent)
@@ -344,17 +307,30 @@ class GreasemonkeyManager(QObject):
returns a tuple of lists of scripts meant to run at (document-start,
document-end, document-idle)
"""
matcher = GreasemonkeyMatcher(url)
if not matcher.is_greaseable:
if url.scheme() not in self.greaseable_schemes:
return MatchingScripts(url, [], [], [])
string_url = url.toString(QUrl.FullyEncoded)
def _match(pattern):
# For include and exclude rules if they start and end with '/' they
# should be treated as a (ecma syntax) regular expression.
if pattern.startswith('/') and pattern.endswith('/'):
matches = re.search(pattern[1:-1], string_url, flags=re.I)
return matches is not None
# Otherwise they are glob expressions.
return fnmatch.fnmatch(string_url, pattern)
tester = (lambda script:
any(_match(pat) for pat in script.includes) and
not any(_match(pat) for pat in script.excludes))
return MatchingScripts(
url=url,
start=[script for script in self._run_start
if matcher.matches(script)],
end=[script for script in self._run_end
if matcher.matches(script)],
idle=[script for script in self._run_idle
if matcher.matches(script)]
url,
[script for script in self._run_start if tester(script)],
[script for script in self._run_end if tester(script)],
[script for script in self._run_idle if tester(script)]
)
def all_scripts(self):

View File

@@ -682,7 +682,7 @@ class HintManager(QObject):
"""
tabbed_browser = objreg.get('tabbed-browser', scope='window',
window=self._win_id)
tab = tabbed_browser.widget.currentWidget()
tab = tabbed_browser.currentWidget()
if tab is None:
raise cmdexc.CommandError("No WebView available yet!")

View File

@@ -22,7 +22,7 @@
from PyQt5.QtCore import QObject, QEvent, Qt, QTimer
from qutebrowser.config import config
from qutebrowser.utils import message, log, usertypes, qtutils
from qutebrowser.utils import message, log, usertypes
from qutebrowser.keyinput import modeman
@@ -54,14 +54,6 @@ class ChildEventFilter(QObject):
obj, child))
assert obj is self._widget
child.installEventFilter(self._filter)
if qtutils.version_check('5.11', compiled=False, exact=True):
# WORKAROUND for https://bugreports.qt.io/browse/QTBUG-68076
QTimer.singleShot(0, self._widget.setFocus)
elif event.type() == QEvent.ChildRemoved:
child = event.child()
log.mouse.debug("{}: removed child {}".format(obj, child))
return False

View File

@@ -162,7 +162,6 @@ class DownloadItem(downloads.AbstractDownloadItem):
QTimer.singleShot(0, lambda: self._die(reply.errorString()))
def _do_cancel(self):
self._read_timer.stop()
if self._reply is not None:
self._reply.finished.disconnect(self._on_reply_finished)
self._reply.abort()

View File

@@ -76,11 +76,11 @@ class SignalFilter(QObject):
tabbed_browser = objreg.get('tabbed-browser', scope='window',
window=self._win_id)
try:
tabidx = tabbed_browser.widget.indexOf(tab)
tabidx = tabbed_browser.indexOf(tab)
except RuntimeError:
# The tab has been deleted already
return
if tabidx == tabbed_browser.widget.currentIndex():
if tabidx == tabbed_browser.currentIndex():
if log_signal:
log.signals.debug("emitting: {} (tab {})".format(
debug.dbg_signal(signal, args), tabidx))

View File

@@ -24,18 +24,16 @@ import os
import re
from PyQt5.QtCore import QLibraryInfo
from qutebrowser.utils import log, message
dict_version_re = re.compile(r".+-(?P<version>[0-9]+-[0-9]+?)\.bdic")
from qutebrowser.utils import log
def version(filename):
"""Extract the version number from the dictionary file name."""
match = dict_version_re.match(filename)
version_re = re.compile(r".+-(?P<version>[0-9]+-[0-9]+?)\.bdic")
match = version_re.fullmatch(filename)
if match is None:
message.warning(
"Found a dictionary with a malformed name: {}".format(filename))
return None
raise ValueError('the given dictionary file name is malformed: {}'
.format(filename))
return tuple(int(n) for n in match.group('version').split('-'))
@@ -46,23 +44,15 @@ def dictionary_dir():
def local_files(code):
"""Return all installed dictionaries for the given code.
The returned dictionaries are sorted by version, therefore the latest will
be the first element. The list will be empty if no dictionaries are found.
"""
"""Return all installed dictionaries for the given code."""
pathname = os.path.join(dictionary_dir(), '{}*.bdic'.format(code))
matching_dicts = glob.glob(pathname)
versioned_dicts = []
for matching_dict in matching_dicts:
parsed_version = version(matching_dict)
if parsed_version is not None:
filename = os.path.basename(matching_dict)
log.config.debug('Found file for dict {}: {}'
.format(code, filename))
versioned_dicts.append((parsed_version, filename))
return [filename for version, filename
in sorted(versioned_dicts, reverse=True)]
files = []
for matching_dict in sorted(matching_dicts, key=version, reverse=True):
filename = os.path.basename(matching_dict)
log.config.debug('Found file for dict {}: {}'.format(code, filename))
files.append(filename)
return files
def local_filename(code):

View File

@@ -101,11 +101,7 @@ class DownloadItem(downloads.AbstractDownloadItem):
def retry(self):
state = self._qt_item.state()
if state != QWebEngineDownloadItem.DownloadInterrupted:
log.downloads.warning(
"Trying to retry download in state {}".format(
debug.qenum_key(QWebEngineDownloadItem, state)))
return
assert state == QWebEngineDownloadItem.DownloadInterrupted, state
try:
self._qt_item.resume()

View File

@@ -22,7 +22,7 @@
import os
from PyQt5.QtCore import QUrl
from PyQt5.QtWebEngineWidgets import QWebEngineView, QWebEngineSettings
from PyQt5.QtWebEngineWidgets import QWebEngineView
from qutebrowser.browser import inspector
@@ -35,8 +35,6 @@ class WebEngineInspector(inspector.AbstractWebInspector):
super().__init__(parent)
self.port = None
view = QWebEngineView()
settings = view.settings()
settings.setAttribute(QWebEngineSettings.JavascriptEnabled, True)
self._set_widget(view)
def inspect(self, _page):

View File

@@ -26,12 +26,16 @@ Module attributes:
import os
import sip
from PyQt5.QtGui import QFont
from PyQt5.QtWebEngineWidgets import QWebEngineSettings, QWebEngineProfile
from PyQt5.QtWebEngineWidgets import (QWebEngineSettings, QWebEngineProfile,
QWebEngineScript)
from qutebrowser.browser import shared
from qutebrowser.browser.webengine import spell
from qutebrowser.config import config, websettings
from qutebrowser.utils import utils, standarddir, qtutils, message, log
from qutebrowser.utils import (utils, standarddir, javascript, qtutils,
message, log, objreg)
# The default QWebEngineProfile
default_profile = None
@@ -165,92 +169,133 @@ class WebEngineSettings(websettings.AbstractSettings):
self._ATTRIBUTES[name] = [value]
class ProfileSetter:
def _init_stylesheet(profile):
"""Initialize custom stylesheets.
"""Helper to set various settings on a profile."""
Partially inspired by QupZilla:
https://github.com/QupZilla/qupzilla/blob/v2.0/src/lib/app/mainapplication.cpp#L1063-L1101
"""
old_script = profile.scripts().findScript('_qute_stylesheet')
if not old_script.isNull():
profile.scripts().remove(old_script)
def __init__(self, profile):
self._profile = profile
css = shared.get_user_stylesheet()
source = '\n'.join([
'"use strict";',
'window._qutebrowser = window._qutebrowser || {};',
utils.read_file('javascript/stylesheet.js'),
javascript.assemble('stylesheet', 'set_css', css),
])
def init_profile(self):
"""Initialize settings on the given profile."""
self.set_http_headers()
self.set_http_cache_size()
self._profile.settings().setAttribute(
QWebEngineSettings.FullScreenSupportEnabled, True)
if qtutils.version_check('5.8'):
self.set_dictionary_language()
script = QWebEngineScript()
script.setName('_qute_stylesheet')
script.setInjectionPoint(QWebEngineScript.DocumentCreation)
script.setWorldId(QWebEngineScript.ApplicationWorld)
script.setRunsOnSubFrames(True)
script.setSourceCode(source)
profile.scripts().insert(script)
def set_http_headers(self):
"""Set the user agent and accept-language for the given profile.
We override those per request in the URL interceptor (to allow for
per-domain values), but this one still gets used for things like
window.navigator.userAgent/.languages in JS.
"""
self._profile.setHttpUserAgent(config.val.content.headers.user_agent)
accept_language = config.val.content.headers.accept_language
if accept_language is not None:
self._profile.setHttpAcceptLanguage(accept_language)
def _update_stylesheet():
"""Update the custom stylesheet in existing tabs."""
css = shared.get_user_stylesheet()
code = javascript.assemble('stylesheet', 'set_css', css)
for win_id, window in objreg.window_registry.items():
# We could be in the middle of destroying a window here
if sip.isdeleted(window):
continue
tab_registry = objreg.get('tab-registry', scope='window',
window=win_id)
for tab in tab_registry.values():
tab.run_js_async(code)
def set_http_cache_size(self):
"""Initialize the HTTP cache size for the given profile."""
size = config.val.content.cache.size
if size is None:
size = 0
else:
size = qtutils.check_overflow(size, 'int', fatal=False)
# 0: automatically managed by QtWebEngine
self._profile.setHttpCacheMaximumSize(size)
def _set_http_headers(profile):
"""Set the user agent and accept-language for the given profile.
def set_persistent_cookie_policy(self):
"""Set the HTTP Cookie size for the given profile."""
assert not self._profile.isOffTheRecord()
if config.val.content.cookies.store:
value = QWebEngineProfile.AllowPersistentCookies
else:
value = QWebEngineProfile.NoPersistentCookies
self._profile.setPersistentCookiesPolicy(value)
We override those per request in the URL interceptor (to allow for
per-domain values), but this one still gets used for things like
window.navigator.userAgent/.languages in JS.
"""
profile.setHttpUserAgent(config.val.content.headers.user_agent)
accept_language = config.val.content.headers.accept_language
if accept_language is not None:
profile.setHttpAcceptLanguage(accept_language)
def set_dictionary_language(self, warn=True):
"""Load the given dictionaries."""
filenames = []
for code in config.val.spellcheck.languages or []:
local_filename = spell.local_filename(code)
if not local_filename:
if warn:
message.warning("Language {} is not installed - see "
"scripts/dictcli.py in qutebrowser's "
"sources".format(code))
continue
filenames.append(local_filename)
def _set_http_cache_size(profile):
"""Initialize the HTTP cache size for the given profile."""
size = config.val.content.cache.size
if size is None:
size = 0
else:
size = qtutils.check_overflow(size, 'int', fatal=False)
log.config.debug("Found dicts: {}".format(filenames))
self._profile.setSpellCheckLanguages(filenames)
self._profile.setSpellCheckEnabled(bool(filenames))
# 0: automatically managed by QtWebEngine
profile.setHttpCacheMaximumSize(size)
def _set_persistent_cookie_policy(profile):
"""Set the HTTP Cookie size for the given profile."""
if config.val.content.cookies.store:
value = QWebEngineProfile.AllowPersistentCookies
else:
value = QWebEngineProfile.NoPersistentCookies
profile.setPersistentCookiesPolicy(value)
def _set_dictionary_language(profile, warn=True):
filenames = []
for code in config.val.spellcheck.languages or []:
local_filename = spell.local_filename(code)
if not local_filename:
if warn:
message.warning(
"Language {} is not installed - see scripts/dictcli.py "
"in qutebrowser's sources".format(code))
continue
filenames.append(local_filename)
log.config.debug("Found dicts: {}".format(filenames))
profile.setSpellCheckLanguages(filenames)
def _update_settings(option):
"""Update global settings when qwebsettings changed."""
global_settings.update_setting(option)
if option in ['content.headers.user_agent',
'content.headers.accept_language']:
default_profile.setter.set_http_headers()
private_profile.setter.set_http_headers()
if option in ['scrolling.bar', 'content.user_stylesheets']:
_init_stylesheet(default_profile)
_init_stylesheet(private_profile)
_update_stylesheet()
elif option in ['content.headers.user_agent',
'content.headers.accept_language']:
_set_http_headers(default_profile)
_set_http_headers(private_profile)
elif option == 'content.cache.size':
default_profile.setter.set_http_cache_size()
private_profile.setter.set_http_cache_size()
_set_http_cache_size(default_profile)
_set_http_cache_size(private_profile)
elif (option == 'content.cookies.store' and
# https://bugreports.qt.io/browse/QTBUG-58650
qtutils.version_check('5.9', compiled=False)):
default_profile.setter.set_persistent_cookie_policy()
_set_persistent_cookie_policy(default_profile)
# We're not touching the private profile's cookie policy.
elif option == 'spellcheck.languages':
default_profile.setter.set_dictionary_language()
private_profile.setter.set_dictionary_language(warn=False)
_set_dictionary_language(default_profile)
_set_dictionary_language(private_profile, warn=False)
def _init_profile(profile):
"""Init the given profile."""
_init_stylesheet(profile)
_set_http_headers(profile)
_set_http_cache_size(profile)
profile.settings().setAttribute(
QWebEngineSettings.FullScreenSupportEnabled, True)
if qtutils.version_check('5.8'):
profile.setSpellCheckEnabled(True)
_set_dictionary_language(profile)
def _init_profiles():
@@ -258,18 +303,53 @@ def _init_profiles():
global default_profile, private_profile
default_profile = QWebEngineProfile.defaultProfile()
default_profile.setter = ProfileSetter(default_profile)
default_profile.setCachePath(
os.path.join(standarddir.cache(), 'webengine'))
default_profile.setPersistentStoragePath(
os.path.join(standarddir.data(), 'webengine'))
default_profile.setter.init_profile()
default_profile.setter.set_persistent_cookie_policy()
_init_profile(default_profile)
_set_persistent_cookie_policy(default_profile)
private_profile = QWebEngineProfile()
private_profile.setter = ProfileSetter(private_profile)
assert private_profile.isOffTheRecord()
private_profile.setter.init_profile()
_init_profile(private_profile)
def inject_userscripts():
"""Register user JavaScript files with the global profiles."""
# The Greasemonkey metadata block support in QtWebEngine only starts at
# Qt 5.8. With 5.7.1, we need to inject the scripts ourselves in response
# to urlChanged.
if not qtutils.version_check('5.8'):
return
# Since we are inserting scripts into profile.scripts they won't
# just get replaced by new gm scripts like if we were injecting them
# ourselves so we need to remove all gm scripts, while not removing
# any other stuff that might have been added. Like the one for
# stylesheets.
greasemonkey = objreg.get('greasemonkey')
for profile in [default_profile, private_profile]:
scripts = profile.scripts()
for script in scripts.toList():
if script.name().startswith("GM-"):
log.greasemonkey.debug('Removing script: {}'
.format(script.name()))
removed = scripts.remove(script)
assert removed, script.name()
# Then add the new scripts.
for script in greasemonkey.all_scripts():
# @run-at (and @include/@exclude/@match) is parsed by
# QWebEngineScript.
new_script = QWebEngineScript()
new_script.setWorldId(QWebEngineScript.MainWorld)
new_script.setSourceCode(script.code())
new_script.setName("GM-{}".format(script.name))
new_script.setRunsOnSubFrames(script.runs_on_sub_frames)
log.greasemonkey.debug('adding script: {}'
.format(new_script.name()))
scripts.insert(new_script)
def init(args):

View File

@@ -30,10 +30,10 @@ from PyQt5.QtCore import (pyqtSignal, pyqtSlot, Qt, QEvent, QPoint, QPointF,
QUrl, QTimer)
from PyQt5.QtGui import QKeyEvent, QIcon
from PyQt5.QtNetwork import QAuthenticator
from PyQt5.QtWidgets import QApplication, QWidget
from PyQt5.QtWidgets import QApplication
from PyQt5.QtWebEngineWidgets import QWebEnginePage, QWebEngineScript
from qutebrowser.config import configdata, config
from qutebrowser.config import configdata
from qutebrowser.browser import browsertab, mouse, shared
from qutebrowser.browser.webengine import (webview, webengineelem, tabhistory,
interceptor, webenginequtescheme,
@@ -73,6 +73,10 @@ def init():
download_manager.install(webenginesettings.private_profile)
objreg.register('webengine-download-manager', download_manager)
greasemonkey = objreg.get('greasemonkey')
greasemonkey.scripts_reloaded.connect(webenginesettings.inject_userscripts)
webenginesettings.inject_userscripts()
# Mapping worlds from usertypes.JsWorld to QWebEngineScript world IDs.
_JS_WORLD_MAP = {
@@ -230,14 +234,7 @@ class WebEngineCaret(browsertab.AbstractCaret):
self._tab.run_js_async(
javascript.assemble('caret', 'setPlatform', sys.platform))
self._js_call('setInitialCursor', self._selection_cb)
def _selection_cb(self, enabled):
"""Emit selection_toggled based on setInitialCursor."""
if enabled is None:
log.webview.debug("Ignoring selection status None")
return
self.selection_toggled.emit(enabled)
self._js_call('setInitialCursor')
@pyqtSlot(usertypes.KeyMode)
def _on_mode_left(self, mode):
@@ -304,7 +301,7 @@ class WebEngineCaret(browsertab.AbstractCaret):
self._js_call('moveToEndOfDocument')
def toggle_selection(self):
self._js_call('toggleSelection', self.selection_toggled.emit)
self._js_call('toggleSelection')
def drop_selection(self):
self._js_call('dropSelection')
@@ -359,8 +356,9 @@ class WebEngineCaret(browsertab.AbstractCaret):
self._tab.run_js_async(js_code, lambda jsret:
self._follow_selected_cb(jsret, tab))
def _js_call(self, command, callback=None):
self._tab.run_js_async(javascript.assemble('caret', command), callback)
def _js_call(self, command):
self._tab.run_js_async(
javascript.assemble('caret', command))
class WebEngineScroller(browsertab.AbstractScroller):
@@ -381,7 +379,7 @@ class WebEngineScroller(browsertab.AbstractScroller):
def _repeated_key_press(self, key, count=1, modifier=Qt.NoModifier):
"""Send count fake key presses to this scroller's WebEngineTab."""
for _ in range(min(count, 1000)):
for _ in range(min(count, 5000)):
self._tab.key_press(key, modifier)
@pyqtSlot(QPointF)
@@ -434,11 +432,6 @@ class WebEngineScroller(browsertab.AbstractScroller):
js_code = javascript.assemble('window', 'scroll', point.x(), point.y())
self._tab.run_js_async(js_code)
def to_anchor(self, name):
url = self._tab.url()
url.setFragment(name)
self._tab.openurl(url)
def delta(self, x=0, y=0):
self._tab.run_js_async(javascript.assemble('window', 'scrollBy', x, y))
@@ -513,9 +506,6 @@ class WebEngineHistory(browsertab.AbstractHistory):
return qtutils.deserialize(data, self._history)
def load_items(self, items):
if items:
self._tab.predicted_navigation.emit(items[-1].url)
stream, _data, cur_data = tabhistory.serialize(items)
qtutils.deserialize_stream(stream, self._history)
@@ -637,127 +627,33 @@ class WebEngineTab(browsertab.AbstractTab):
self._set_widget(widget)
self._connect_signals()
self.backend = usertypes.Backend.QtWebEngine
self._init_js()
self._child_event_filter = None
self._saved_zoom = None
self._reload_url = None
config.instance.changed.connect(self._on_config_changed)
self._init_js()
@pyqtSlot(str)
def _on_config_changed(self, option):
if option in ['scrolling.bar', 'content.user_stylesheets']:
self._init_stylesheet()
self._update_stylesheet()
def _update_stylesheet(self):
"""Update the custom stylesheet in existing tabs."""
css = shared.get_user_stylesheet()
code = javascript.assemble('stylesheet', 'set_css', css)
self.run_js_async(code)
def _inject_early_js(self, name, js_code, *,
world=QWebEngineScript.ApplicationWorld,
subframes=False):
"""Inject the given script to run early on a page load.
This runs the script both on DocumentCreation and DocumentReady as on
some internal pages, DocumentCreation will not work.
That is a WORKAROUND for https://bugreports.qt.io/browse/QTBUG-66011
"""
scripts = self._widget.page().scripts()
for injection in ['creation', 'ready']:
injection_points = {
'creation': QWebEngineScript.DocumentCreation,
'ready': QWebEngineScript.DocumentReady,
}
script = QWebEngineScript()
script.setInjectionPoint(injection_points[injection])
script.setSourceCode(js_code)
script.setWorldId(world)
script.setRunsOnSubFrames(subframes)
script.setName('_qute_{}_{}'.format(name, injection))
scripts.insert(script)
def _remove_early_js(self, name):
"""Remove an early QWebEngineScript."""
scripts = self._widget.page().scripts()
for injection in ['creation', 'ready']:
full_name = '_qute_{}_{}'.format(name, injection)
script = scripts.findScript(full_name)
if not script.isNull():
scripts.remove(script)
def _init_js(self):
"""Initialize global qutebrowser JavaScript."""
js_code = javascript.wrap_global(
'scripts',
js_code = '\n'.join([
'"use strict";',
'window._qutebrowser = window._qutebrowser || {};',
utils.read_file('javascript/scroll.js'),
utils.read_file('javascript/webelem.js'),
utils.read_file('javascript/caret.js'),
)
# FIXME:qtwebengine what about subframes=True?
self._inject_early_js('js', js_code, subframes=True)
self._init_stylesheet()
])
script = QWebEngineScript()
# We can't use DocumentCreation here as WORKAROUND for
# https://bugreports.qt.io/browse/QTBUG-66011
script.setInjectionPoint(QWebEngineScript.DocumentReady)
script.setSourceCode(js_code)
greasemonkey = objreg.get('greasemonkey')
greasemonkey.scripts_reloaded.connect(self._inject_userscripts)
self._inject_userscripts()
page = self._widget.page()
script.setWorldId(QWebEngineScript.ApplicationWorld)
def _init_stylesheet(self):
"""Initialize custom stylesheets.
Partially inspired by QupZilla:
https://github.com/QupZilla/qupzilla/blob/v2.0/src/lib/app/mainapplication.cpp#L1063-L1101
"""
self._remove_early_js('stylesheet')
css = shared.get_user_stylesheet()
js_code = javascript.wrap_global(
'stylesheet',
utils.read_file('javascript/stylesheet.js'),
javascript.assemble('stylesheet', 'set_css', css),
)
self._inject_early_js('stylesheet', js_code, subframes=True)
def _inject_userscripts(self):
"""Register user JavaScript files with the global profiles."""
# The Greasemonkey metadata block support in QtWebEngine only starts at
# Qt 5.8. With 5.7.1, we need to inject the scripts ourselves in
# response to urlChanged.
if not qtutils.version_check('5.8'):
return
# Since we are inserting scripts into profile.scripts they won't
# just get replaced by new gm scripts like if we were injecting them
# ourselves so we need to remove all gm scripts, while not removing
# any other stuff that might have been added. Like the one for
# stylesheets.
greasemonkey = objreg.get('greasemonkey')
scripts = self._widget.page().scripts()
for script in scripts.toList():
if script.name().startswith("GM-"):
log.greasemonkey.debug('Removing script: {}'
.format(script.name()))
removed = scripts.remove(script)
assert removed, script.name()
# Then add the new scripts.
for script in greasemonkey.all_scripts():
# @run-at (and @include/@exclude/@match) is parsed by
# QWebEngineScript.
new_script = QWebEngineScript()
new_script.setWorldId(QWebEngineScript.MainWorld)
new_script.setSourceCode(script.code())
new_script.setName("GM-{}".format(script.name))
new_script.setRunsOnSubFrames(script.runs_on_sub_frames)
log.greasemonkey.debug('adding script: {}'
.format(new_script.name()))
scripts.insert(new_script)
# FIXME:qtwebengine what about runsOnSubFrames?
page.scripts().insert(script)
def _install_event_filter(self):
fp = self._widget.focusProxy()
if fp is not None:
fp.installEventFilter(self._mouse_event_filter)
self._widget.focusProxy().installEventFilter(self._mouse_event_filter)
self._child_event_filter = mouse.ChildEventFilter(
eventfilter=self._mouse_event_filter, widget=self._widget,
parent=self)
@@ -773,15 +669,9 @@ class WebEngineTab(browsertab.AbstractTab):
self.zoom.set_factor(self._saved_zoom)
self._saved_zoom = None
def openurl(self, url, *, predict=True):
"""Open the given URL in this tab.
Arguments:
url: The QUrl to open.
predict: If set to False, predicted_navigation is not emitted.
"""
def openurl(self, url):
self._saved_zoom = self.zoom.factor()
self._openurl_prepare(url, predict=predict)
self._openurl_prepare(url)
self._widget.load(url)
def url(self, requested=False):
@@ -1024,11 +914,10 @@ class WebEngineTab(browsertab.AbstractTab):
if ok and self._reload_url is not None:
# WORKAROUND for https://bugreports.qt.io/browse/QTBUG-66656
log.config.debug(
"Loading {} again because of config change".format(
"Reloading {} because of config change".format(
self._reload_url.toDisplayString()))
QTimer.singleShot(100, functools.partial(self.openurl,
self._reload_url,
predict=False))
QTimer.singleShot(100, lambda url=self._reload_url:
self.openurl(url))
self._reload_url = None
if not qtutils.version_check('5.10', compiled=False):
@@ -1041,39 +930,16 @@ class WebEngineTab(browsertab.AbstractTab):
@pyqtSlot(QUrl)
def _on_predicted_navigation(self, url):
"""If we know we're going to visit an URL soon, change the settings."""
super()._on_predicted_navigation(url)
qtutils.ensure_valid(url)
self.settings.update_for_url(url)
@pyqtSlot(usertypes.NavigationRequest)
def _on_navigation_request(self, navigation):
super()._on_navigation_request(navigation)
if qtutils.version_check('5.11.0', exact=True, compiled=False):
# WORKAROUND for https://bugreports.qt.io/browse/QTBUG-68224
layout = self._widget.layout()
count = layout.count()
children = self._widget.findChildren(QWidget)
if not count and children:
log.webview.warning("Found children not in layout: {}, "
"focus proxy {} (QTBUG-68224)".format(
children, self._widget.focusProxy()))
if count > 1:
log.webview.debug("Found {} widgets! (QTBUG-68224)"
.format(count))
for i in range(count):
item = layout.itemAt(i)
if item is None:
continue
widget = item.widget()
if widget is not self._widget.focusProxy():
log.webview.debug("Removing widget {} (QTBUG-68224)"
.format(widget))
layout.removeWidget(widget)
if not navigation.accepted or not navigation.is_main_frame:
return
settings_needing_reload = {
needs_reload = {
'content.plugins',
'content.javascript.enabled',
'content.javascript.can_access_clipboard',
@@ -1082,20 +948,11 @@ class WebEngineTab(browsertab.AbstractTab):
'input.spatial_navigation',
'input.spatial_navigation',
}
assert settings_needing_reload.issubset(configdata.DATA)
assert needs_reload.issubset(configdata.DATA)
changed = self.settings.update_for_url(navigation.url)
reload_needed = changed & settings_needing_reload
# On Qt < 5.11, we don't don't need a reload when type == link_clicked.
# On Qt 5.11.0, we always need a reload.
# TODO on Qt > 5.11.0, we hopefully never need a reload:
# https://codereview.qt-project.org/#/c/229525/1
if not qtutils.version_check('5.11.0', exact=True, compiled=False):
if navigation.navigation_type != navigation.Type.link_clicked:
reload_needed = False
if reload_needed:
if (changed & needs_reload and navigation.navigation_type !=
navigation.Type.link_clicked):
# WORKAROUND for https://bugreports.qt.io/browse/QTBUG-66656
self._reload_url = navigation.url

View File

@@ -196,10 +196,9 @@ class WebKitCaret(browsertab.AbstractCaret):
if mode != usertypes.KeyMode.caret:
return
self.selection_enabled = self._widget.hasSelection()
self.selection_toggled.emit(self.selection_enabled)
settings = self._widget.settings()
settings.setAttribute(QWebSettings.CaretBrowsingEnabled, True)
self.selection_enabled = self._widget.hasSelection()
if self._widget.isVisible():
# Sometimes the caret isn't immediately visible, but unfocusing
@@ -364,7 +363,9 @@ class WebKitCaret(browsertab.AbstractCaret):
def toggle_selection(self):
self.selection_enabled = not self.selection_enabled
self.selection_toggled.emit(self.selection_enabled)
mainwindow = objreg.get('main-window', scope='window',
window=self._tab.win_id)
mainwindow.status.set_mode_active(usertypes.KeyMode.caret, True)
def drop_selection(self):
self._widget.triggerPageAction(QWebPage.MoveToNextChar)
@@ -426,9 +427,6 @@ class WebKitScroller(browsertab.AbstractScroller):
def to_point(self, point):
self._widget.page().mainFrame().setScrollPosition(point)
def to_anchor(self, name):
self._widget.page().mainFrame().scrollToAnchor(name)
def delta(self, x=0, y=0):
qtutils.check_overflow(x, 'int')
qtutils.check_overflow(y, 'int')
@@ -539,9 +537,6 @@ class WebKitHistory(browsertab.AbstractHistory):
return qtutils.deserialize(data, self._history)
def load_items(self, items):
if items:
self._tab.predicted_navigation.emit(items[-1].url)
stream, _data, user_data = tabhistory.serialize(items)
qtutils.deserialize_stream(stream, self._history)
for i, data in enumerate(user_data):
@@ -673,8 +668,8 @@ class WebKitTab(browsertab.AbstractTab):
settings = widget.settings()
settings.setAttribute(QWebSettings.PrivateBrowsingEnabled, True)
def openurl(self, url, *, predict=True):
self._openurl_prepare(url, predict=predict)
def openurl(self, url):
self._openurl_prepare(url)
self._widget.openurl(url)
def url(self, requested=False):

View File

@@ -239,6 +239,7 @@ class BrowserPage(QWebPage):
printdiag.setAttribute(Qt.WA_DeleteOnClose)
printdiag.open(lambda: frame.print(printdiag.printer()))
@pyqtSlot('QNetworkRequest')
def on_download_requested(self, request):
"""Called when the user wants to download a link.

View File

@@ -110,18 +110,18 @@ def _buffer(skip_win_id=None):
model = completionmodel.CompletionModel(column_widths=(6, 40, 54))
for win_id in objreg.window_registry:
if skip_win_id is not None and win_id == skip_win_id:
if skip_win_id and win_id == skip_win_id:
continue
tabbed_browser = objreg.get('tabbed-browser', scope='window',
window=win_id)
if tabbed_browser.shutting_down:
continue
tabs = []
for idx in range(tabbed_browser.widget.count()):
tab = tabbed_browser.widget.widget(idx)
for idx in range(tabbed_browser.count()):
tab = tabbed_browser.widget(idx)
tabs.append(("{}/{}".format(win_id, idx + 1),
tab.url().toDisplayString(),
tabbed_browser.widget.page_title(idx)))
tabbed_browser.page_title(idx)))
cat = listcategory.ListCategory("{}".format(win_id), tabs,
delete_func=delete_buffer)
model.add_category(cat)

View File

@@ -425,7 +425,11 @@ content.host_blocking.enabled:
content.host_blocking.lists:
default:
- "https://raw.githubusercontent.com/StevenBlack/hosts/master/hosts"
- "https://www.malwaredomainlist.com/hostslist/hosts.txt"
- "http://someonewhocares.org/hosts/hosts"
- "http://winhelp2002.mvps.org/hosts.zip"
- "http://malwaredomains.lehigh.edu/files/justdomains.zip"
- "https://pgl.yoyo.org/adservers/serverlist.php?hostformat=hosts&mimetype=plaintext"
type:
name: List
valtype: Url
@@ -1248,14 +1252,9 @@ tabs.favicons.scale:
`tabs.padding`.
tabs.favicons.show:
default: always
type:
name: String
valid_values:
- always: Always show favicons.
- never: Always hide favicons.
- pinned: Show favicons only on pinned tabs.
desc: When to show favicons in the tab bar.
default: true
type: Bool
desc: Show favicons in the tab bar.
tabs.last_close:
default: ignore
@@ -1326,10 +1325,7 @@ tabs.show:
tabs.show_switching_delay:
default: 800
type:
name: Int
minval: 0
maxval: maxint
type: Int
desc: "Duration (in milliseconds) to show the tab bar before hiding it when
tabs.show is set to 'switching'."
@@ -1410,19 +1406,6 @@ tabs.width:
desc: "Width (in pixels or as percentage of the window) of the tab bar if
it's vertical."
tabs.min_width:
default: -1
type:
name: Int
minval: -1
maxval: maxint
desc: >-
Minimum width (in pixels) of tabs (-1 for the default minimum size behavior).
This setting only applies when tabs are horizontal.
This setting does not apply to pinned tabs, unless `tabs.pinned.shrink` is False.
tabs.width.indicator:
renamed: tabs.indicator.width
@@ -1486,11 +1469,6 @@ url.incdec_segments:
desc: URL segments where `:navigate increment/decrement` will search for
a number.
url.open_base_url:
type: Bool
default: false
desc: Open base URL of the searchengine if a searchengine shortcut is invoked without parameters.
url.searchengines:
default:
DEFAULT: https://duckduckgo.com/?q={}
@@ -1535,15 +1513,10 @@ url.yank_ignored_parameters:
## window
window.hide_wayland_decoration:
renamed: window.hide_decoration
window.hide_decoration:
type: Bool
default: false
desc: |
Hide the window decoration.
This setting requires a restart on Wayland.
restart: true
desc: Hide the window decoration when using wayland.
window.title_format:
type:

View File

@@ -268,15 +268,6 @@ class YamlConfig(QObject):
del settings['bindings.default']
self._mark_changed()
# Option to show favicons only for pinned tabs changed the type of
# tabs.favicons.show from Bool to String
name = 'tabs.favicons.show'
if name in settings:
for scope, val in settings[name].items():
if isinstance(val, bool):
settings[name][scope] = 'always' if val else 'never'
self._mark_changed()
return settings
def _validate(self, settings):

View File

@@ -26,8 +26,7 @@ from PyQt5.QtWidgets import QMessageBox
from qutebrowser.config import (config, configdata, configfiles, configtypes,
configexc, configcommands)
from qutebrowser.utils import (objreg, usertypes, log, standarddir, message,
qtutils)
from qutebrowser.utils import objreg, usertypes, log, standarddir, message
from qutebrowser.misc import msgbox, objects
@@ -90,7 +89,7 @@ def _init_envvars():
if config.val.qt.force_platform is not None:
os.environ['QT_QPA_PLATFORM'] = config.val.qt.force_platform
if config.val.window.hide_decoration:
if config.val.window.hide_wayland_decoration:
os.environ['QT_WAYLAND_DISABLE_WINDOWDECORATION'] = '1'
if config.val.qt.highdpi:
@@ -162,12 +161,4 @@ def qt_args(namespace):
argv += ['--' + name, value]
argv += ['--' + arg for arg in config.val.qt.args]
if (objects.backend == usertypes.Backend.QtWebEngine and
not qtutils.version_check('5.11', compiled=False)):
# WORKAROUND equivalent to
# https://codereview.qt-project.org/#/c/217932/
# Needed for Qt < 5.9.5 and < 5.10.1
argv.append('--disable-shared-workers')
return argv

View File

@@ -506,16 +506,6 @@ class ListOrValue(BaseType):
self.listtype = List(valtype, none_ok=none_ok, *args, **kwargs)
self.valtype = valtype
def _val_and_type(self, value):
"""Get the value and type to use for to_str/to_doc/from_str."""
if isinstance(value, list):
if len(value) == 1:
return value[0], self.valtype
else:
return value, self.listtype
else:
return value, self.valtype
def get_name(self):
return self.listtype.get_name() + ', or ' + self.valtype.get_name()
@@ -543,15 +533,25 @@ class ListOrValue(BaseType):
if value is None:
return ''
val, typ = self._val_and_type(value)
return typ.to_str(val)
if isinstance(value, list):
if len(value) == 1:
return self.valtype.to_str(value[0])
else:
return self.listtype.to_str(value)
else:
return self.valtype.to_str(value)
def to_doc(self, value, indent=0):
if value is None:
return 'empty'
val, typ = self._val_and_type(value)
return typ.to_doc(val)
if isinstance(value, list):
if len(value) == 1:
return self.valtype.to_doc(value[0], indent)
else:
return self.listtype.to_doc(value, indent)
else:
return self.valtype.to_doc(value, indent)
class FlagList(List):
@@ -1625,7 +1625,9 @@ class TimestampTemplate(BaseType):
"""An strftime-like template for timestamps.
See https://sqlite.org/lang_datefunc.html for reference.
See
https://docs.python.org/3/library/datetime.html#strftime-strptime-behavior
for reference.
"""
def to_py(self, value):

View File

@@ -2,4 +2,3 @@
pac_utils.js
# Actually a jinja template so eslint chokes on the {{}} syntax.
greasemonkey_wrapper.js
global_wrapper.js

View File

@@ -324,8 +324,9 @@ window._qutebrowser.caret = (function() {
const color = axs.color.parseColor(style.backgroundColor);
if (color &&
(style.opacity < 1 &&
(color.alpha *= style.opacity), color.alpha !== 0 &&
(el.push(color), color.alpha === 1))) {
(color.alpha *= style.opacity),
color.alpha !== 0 &&
(el.push(color), color.alpha === 1))) {
iter = !0;
break;
}
@@ -1269,14 +1270,13 @@ window._qutebrowser.caret = (function() {
funcs.setInitialCursor = () => {
if (!CaretBrowsing.initiated) {
CaretBrowsing.setInitialCursor();
return CaretBrowsing.selectionEnabled;
return;
}
if (window.getSelection().toString().length === 0) {
positionCaret();
}
CaretBrowsing.toggle();
return CaretBrowsing.selectionEnabled;
};
funcs.setPlatform = (platform) => {
@@ -1362,7 +1362,6 @@ window._qutebrowser.caret = (function() {
funcs.toggleSelection = () => {
CaretBrowsing.selectionEnabled = !CaretBrowsing.selectionEnabled;
return CaretBrowsing.selectionEnabled;
};
return funcs;

View File

@@ -1,12 +0,0 @@
(function() {
"use strict";
if (!("_qutebrowser" in window)) {
window._qutebrowser = {"initialized": {}};
}
if (window._qutebrowser.initialized["{{name}}"]) {
return;
}
{{code}}
window._qutebrowser.initialized["{{name}}"] = true;
})();

View File

@@ -1,5 +1,5 @@
(function() {
const _qute_script_id = "__gm_{{ scriptName }}";
const _qute_script_id = "__gm_" + {{ scriptName | tojson }};
function GM_log(text) {
console.log(text);
@@ -7,7 +7,7 @@
const GM_info = {
'script': {{ scriptInfo }},
'scriptMetaStr': "{{ scriptMeta }}",
'scriptMetaStr': {{ scriptMeta | tojson }},
'scriptWillUpdate': false,
'version': "0.0.1",
// so scripts don't expect exportFunction
@@ -100,8 +100,11 @@
const head = document.getElementsByTagName("head")[0];
if (head === undefined) {
// no head yet, stick it whereever
document.documentElement.appendChild(oStyle);
document.onreadystatechange = function() {
if (document.readyState === "interactive") {
document.getElementsByTagName("head")[0].appendChild(oStyle);
}
};
} else {
head.appendChild(oStyle);
}

View File

@@ -19,8 +19,6 @@
"""Base class for vim-like key sequence parser."""
import string
from PyQt5.QtCore import pyqtSignal, QObject
from PyQt5.QtGui import QKeySequence
@@ -110,45 +108,13 @@ class BaseKeyParser(QObject):
assert not isinstance(seq, str), seq
match = sequence.matches(seq)
if match == QKeySequence.ExactMatch:
return match, cmd
return (match, cmd)
elif match == QKeySequence.PartialMatch:
result = QKeySequence.PartialMatch
return result, None
return (result, None)
def _match_without_modifiers(self, sequence):
"""Try to match a key with optional modifiers stripped."""
self._debug_log("Trying match without modifiers")
sequence = sequence.strip_modifiers()
match, binding = self._match_key(sequence)
return match, binding, sequence
def _match_key_mapping(self, sequence):
"""Try to match a key in bindings.key_mappings."""
self._debug_log("Trying match with key_mappings")
mapped = sequence.with_mappings(config.val.bindings.key_mappings)
if sequence != mapped:
self._debug_log("Mapped {} -> {}".format(
sequence, mapped))
match, binding = self._match_key(mapped)
sequence = mapped
return match, binding, sequence
return QKeySequence.NoMatch, None, sequence
def _match_count(self, sequence, dry_run):
"""Try to match a key as count."""
txt = str(sequence[-1]) # To account for sequences changed above.
if (txt in string.digits and self._supports_count and
not (not self._count and txt == '0')):
self._debug_log("Trying match as count")
assert len(txt) == 1, txt
if not dry_run:
self._count += txt
self.keystring_updated.emit(self._count + str(self._sequence))
return True
return False
def handle(self, e, *, dry_run=False):
def handle(self, e, *, dry_run=False): # noqa
"""Handle a new keypress.
Separate the keypress into count/command, then check if it matches
@@ -180,15 +146,38 @@ class BaseKeyParser(QObject):
self.clear_keystring()
return QKeySequence.NoMatch
# First, try a straightforward match
self._debug_log("Trying simple match")
match, binding = self._match_key(sequence)
# Then try without optional modifiers
if match == QKeySequence.NoMatch:
match, binding, sequence = self._match_without_modifiers(sequence)
self._debug_log("Trying match without modifiers")
sequence = sequence.strip_modifiers()
match, binding = self._match_key(sequence)
# If that doesn't match, try a key_mapping
if match == QKeySequence.NoMatch:
match, binding, sequence = self._match_key_mapping(sequence)
if match == QKeySequence.NoMatch:
was_count = self._match_count(sequence, dry_run)
if was_count:
return QKeySequence.ExactMatch
self._debug_log("Trying match with key_mappings")
mapped = sequence.with_mappings(config.val.bindings.key_mappings)
if sequence != mapped:
self._debug_log("Mapped {} -> {}".format(
sequence, mapped))
match, binding = self._match_key(mapped)
sequence = mapped
# If that doesn't match either, try treating it as count.
txt = str(sequence[-1]) # To account for sequences changed above.
if (match == QKeySequence.NoMatch and
txt.isdigit() and
self._supports_count and
not (not self._count and txt == '0')):
self._debug_log("Trying match as count")
assert len(txt) == 1, txt
if not dry_run:
self._count += txt
self.keystring_updated.emit(self._count + str(self._sequence))
return QKeySequence.ExactMatch
if dry_run:
return match

View File

@@ -58,8 +58,7 @@ def is_special(key, modifiers):
_assert_plain_key(key)
_assert_plain_modifier(modifiers)
return not (_is_printable(key) and
modifiers in [Qt.ShiftModifier, Qt.NoModifier,
Qt.KeypadModifier])
modifiers in [Qt.ShiftModifier, Qt.NoModifier])
def is_modifier_key(key):
@@ -304,8 +303,7 @@ class KeyInfo:
key_string = key_string.lower()
# "special" binding
assert (is_special(self.key, self.modifiers) or
self.modifiers == Qt.KeypadModifier)
assert is_special(self.key, self.modifiers)
modifier_string = _modifiers_to_string(modifiers)
return '<{}{}>'.format(modifier_string, key_string)

View File

@@ -184,8 +184,7 @@ class MainWindow(QWidget):
private = bool(private)
self._private = private
self.tabbed_browser = tabbedbrowser.TabbedBrowser(win_id=self.win_id,
private=private,
parent=self)
private=private)
objreg.register('tabbed-browser', self.tabbed_browser, scope='window',
window=self.win_id)
self._init_command_dispatcher()
@@ -231,7 +230,6 @@ class MainWindow(QWidget):
config.instance.changed.connect(self._on_config_changed)
objreg.get("app").new_window.emit(self)
self._set_decoration(config.val.window.hide_decoration)
def _init_geometry(self, geometry):
"""Initialize the window geometry or load it from disk."""
@@ -329,7 +327,7 @@ class MainWindow(QWidget):
self.tabbed_browser)
objreg.register('command-dispatcher', dispatcher, scope='window',
window=self.win_id)
self.tabbed_browser.widget.destroyed.connect(
self.tabbed_browser.destroyed.connect(
functools.partial(objreg.delete, 'command-dispatcher',
scope='window', window=self.win_id))
@@ -346,15 +344,13 @@ class MainWindow(QWidget):
elif option == 'statusbar.position':
self._add_widgets()
self._update_overlay_geometries()
elif option == 'window.hide_decoration':
self._set_decoration(config.val.window.hide_decoration)
def _add_widgets(self):
"""Add or readd all widgets to the VBox."""
self._vbox.removeWidget(self.tabbed_browser.widget)
self._vbox.removeWidget(self.tabbed_browser)
self._vbox.removeWidget(self._downloadview)
self._vbox.removeWidget(self.status)
widgets = [self.tabbed_browser.widget]
widgets = [self.tabbed_browser]
downloads_position = config.val.downloads.position
if downloads_position == 'top':
@@ -473,7 +469,7 @@ class MainWindow(QWidget):
self.tabbed_browser.cur_scroll_perc_changed.connect(
status.percentage.set_perc)
self.tabbed_browser.widget.tab_index_changed.connect(
self.tabbed_browser.tab_index_changed.connect(
status.tabindex.on_tab_index_changed)
self.tabbed_browser.cur_url_changed.connect(status.url.set_url)
@@ -483,10 +479,6 @@ class MainWindow(QWidget):
self.tabbed_browser.cur_link_hovered.connect(status.url.set_hover_url)
self.tabbed_browser.cur_load_status_changed.connect(
status.url.on_load_status_changed)
self.tabbed_browser.cur_caret_selection_toggled.connect(
status.on_caret_selection_toggled)
self.tabbed_browser.cur_fullscreen_requested.connect(
self._on_fullscreen_requested)
self.tabbed_browser.cur_fullscreen_requested.connect(status.maybe_hide)
@@ -497,16 +489,6 @@ class MainWindow(QWidget):
completion_obj.on_clear_completion_selection)
cmd.hide_completion.connect(completion_obj.hide)
def _set_decoration(self, hidden):
"""Set the visibility of the window decoration via Qt."""
window_flags = Qt.Window
refresh_window = self.isVisible()
if hidden:
window_flags |= Qt.CustomizeWindowHint | Qt.NoDropShadowWindowHint
self.setWindowFlags(window_flags)
if refresh_window:
self.show()
@pyqtSlot(bool)
def _on_fullscreen_requested(self, on):
if not config.val.content.windowed_fullscreen:
@@ -535,7 +517,7 @@ class MainWindow(QWidget):
super().resizeEvent(e)
self._update_overlay_geometries()
self._downloadview.updateGeometry()
self.tabbed_browser.widget.tabBar().refresh()
self.tabbed_browser.tabBar().refresh()
def showEvent(self, e):
"""Extend showEvent to register us as the last-visible-main-window.
@@ -564,7 +546,7 @@ class MainWindow(QWidget):
if crashsignal.is_crashing:
e.accept()
return
tab_count = self.tabbed_browser.widget.count()
tab_count = self.tabbed_browser.count()
download_model = objreg.get('download-model', scope='window',
window=self.win_id)
download_count = download_model.running_downloads()

View File

@@ -596,8 +596,6 @@ class FilenamePrompt(_BasePrompt):
if config.val.prompt.filebrowser:
self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred)
self._to_complete = ''
@pyqtSlot(str)
def _set_fileview_root(self, path, *, tabbed=False):
"""Set the root path for the file display."""
@@ -606,9 +604,6 @@ class FilenamePrompt(_BasePrompt):
separators += os.altsep
dirname = os.path.dirname(path)
basename = os.path.basename(path)
if not tabbed:
self._to_complete = ''
try:
if not path:
@@ -622,7 +617,6 @@ class FilenamePrompt(_BasePrompt):
elif os.path.isdir(dirname) and not tabbed:
# Input like /foo/ba -> show /foo contents
path = dirname
self._to_complete = basename
else:
return
except OSError:
@@ -640,11 +634,7 @@ class FilenamePrompt(_BasePrompt):
index: The QModelIndex of the selected element.
clicked: Whether the element was clicked.
"""
if index == QModelIndex():
path = os.path.join(self._file_model.rootPath(), self._to_complete)
else:
path = os.path.normpath(self._file_model.filePath(index))
path = os.path.normpath(self._file_model.filePath(index))
if clicked:
path += os.sep
else:
@@ -706,7 +696,6 @@ class FilenamePrompt(_BasePrompt):
assert last_index.isValid()
idx = selmodel.currentIndex()
if not idx.isValid():
# No item selected yet
idx = last_index if which == 'prev' else first_index
@@ -720,24 +709,10 @@ class FilenamePrompt(_BasePrompt):
if not idx.isValid():
idx = last_index if which == 'prev' else first_index
idx = self._do_completion(idx, which)
selmodel.setCurrentIndex(
idx, QItemSelectionModel.ClearAndSelect | QItemSelectionModel.Rows)
self._insert_path(idx, clicked=False)
def _do_completion(self, idx, which):
filename = self._file_model.fileName(idx)
while not filename.startswith(self._to_complete) and idx.isValid():
if which == 'prev':
idx = self._file_view.indexAbove(idx)
else:
assert which == 'next', which
idx = self._file_view.indexBelow(idx)
filename = self._file_model.fileName(idx)
return idx
def _allowed_commands(self):
return [('prompt-accept', 'Accept'), ('leave-mode', 'Abort')]

View File

@@ -32,7 +32,7 @@ class Backforward(textbase.TextBase):
def on_tab_cur_url_changed(self, tabs):
"""Called on URL changes."""
tab = tabs.widget.currentWidget()
tab = tabs.currentWidget()
if tab is None: # pragma: no cover
self.setText('')
self.hide()

View File

@@ -268,7 +268,7 @@ class StatusBar(QWidget):
"""Get the currently displayed tab."""
window = objreg.get('tabbed-browser', scope='window',
window=self._win_id)
return window.widget.currentWidget()
return window.currentWidget()
def set_mode_active(self, mode, val):
"""Setter for self.{insert,command,caret}_active.
@@ -289,9 +289,17 @@ class StatusBar(QWidget):
log.statusbar.debug("Setting prompt flag to {}".format(val))
self._color_flags.prompt = val
elif mode == usertypes.KeyMode.caret:
if not val:
# Turning on is handled in on_current_caret_selection_toggled
log.statusbar.debug("Setting caret mode off")
tab = self._current_tab()
log.statusbar.debug("Setting caret flag - val {}, selection "
"{}".format(val, tab.caret.selection_enabled))
if val:
if tab.caret.selection_enabled:
self._set_mode_text("{} selection".format(mode.name))
self._color_flags.caret = ColorFlags.CaretMode.selection
else:
self._set_mode_text(mode.name)
self._color_flags.caret = ColorFlags.CaretMode.on
else:
self._color_flags.caret = ColorFlags.CaretMode.off
config.set_register_stylesheet(self, update=False)
@@ -369,18 +377,6 @@ class StatusBar(QWidget):
self.maybe_hide()
assert tab.private == self._color_flags.private
@pyqtSlot(bool)
def on_caret_selection_toggled(self, selection):
"""Update the statusbar when entering/leaving caret selection mode."""
log.statusbar.debug("Setting caret selection {}".format(selection))
if selection:
self._set_mode_text("caret selection")
self._color_flags.caret = ColorFlags.CaretMode.selection
else:
self._set_mode_text("caret")
self._color_flags.caret = ColorFlags.CaretMode.on
config.set_register_stylesheet(self, update=False)
def resizeEvent(self, e):
"""Extend resizeEvent of QWidget to emit a resized signal afterwards.

View File

@@ -19,8 +19,6 @@
"""The commandline in the statusbar."""
import functools
from PyQt5.QtCore import pyqtSignal, pyqtSlot, Qt, QSize
from PyQt5.QtWidgets import QSizePolicy
@@ -71,26 +69,6 @@ class Command(misc.MinimalLineEditMixin, misc.CommandLineEdit):
self.textChanged.connect(self.updateGeometry)
self.textChanged.connect(self._incremental_search)
self._command_dispatcher = objreg.get(
'command-dispatcher', scope='window', window=self._win_id)
def _handle_search(self):
"""Check if the currently entered text is a search, and if so, run it.
Return:
True if a search was executed, False otherwise.
"""
search_prefixes = {
'/': self._command_dispatcher.search,
'?': functools.partial(
self._command_dispatcher.search, reverse=True)
}
if self.prefix() in search_prefixes:
search_fn = search_prefixes[self.prefix()]
search_fn(self.text()[1:])
return True
return False
def prefix(self):
"""Get the currently entered command prefix."""
text = self.text()
@@ -184,17 +162,17 @@ class Command(misc.MinimalLineEditMixin, misc.CommandLineEdit):
Args:
rapid: Run the command without closing or clearing the command bar.
"""
prefixes = {
':': '',
'/': 'search -- ',
'?': 'search -r -- ',
}
text = self.text()
self.history.append(text)
was_search = self._handle_search()
if not rapid:
modeman.leave(self._win_id, usertypes.KeyMode.command,
'cmd accept')
if not was_search:
self.got_cmd[str].emit(text[1:])
self.got_cmd[str].emit(prefixes[text[0]] + text[1:])
@cmdutils.register(instance='status-command', scope='window')
def edit_command(self, run=False):
@@ -275,9 +253,15 @@ class Command(misc.MinimalLineEditMixin, misc.CommandLineEdit):
width = self.fontMetrics().width(text)
return QSize(width, height)
@pyqtSlot()
def _incremental_search(self):
@pyqtSlot(str)
def _incremental_search(self, text):
if not config.val.search.incremental:
return
self._handle_search()
search_prefixes = {
'/': 'search -- ',
'?': 'search -r -- ',
}
if self.prefix() in ['/', '?']:
self.got_cmd[str].emit(search_prefixes[text[0]] + text[1:])

View File

@@ -22,7 +22,7 @@
import functools
import attr
from PyQt5.QtWidgets import QSizePolicy, QWidget
from PyQt5.QtWidgets import QSizePolicy
from PyQt5.QtCore import pyqtSignal, pyqtSlot, QTimer, QUrl
from PyQt5.QtGui import QIcon
@@ -50,7 +50,7 @@ class TabDeletedError(Exception):
"""Exception raised when _tab_index is called for a deleted tab."""
class TabbedBrowser(QWidget):
class TabbedBrowser(tabwidget.TabWidget):
"""A TabWidget with QWebViews inside.
@@ -104,25 +104,23 @@ class TabbedBrowser(QWidget):
cur_scroll_perc_changed = pyqtSignal(int, int)
cur_load_status_changed = pyqtSignal(str)
cur_fullscreen_requested = pyqtSignal(bool)
cur_caret_selection_toggled = pyqtSignal(bool)
close_window = pyqtSignal()
resized = pyqtSignal('QRect')
current_tab_changed = pyqtSignal(browsertab.AbstractTab)
new_tab = pyqtSignal(browsertab.AbstractTab, int)
def __init__(self, *, win_id, private, parent=None):
super().__init__(parent)
self.widget = tabwidget.TabWidget(win_id, parent=self)
super().__init__(win_id, parent)
self._win_id = win_id
self._tab_insert_idx_left = 0
self._tab_insert_idx_right = -1
self.shutting_down = False
self.widget.tabCloseRequested.connect(self.on_tab_close_requested)
self.widget.new_tab_requested.connect(self.tabopen)
self.widget.currentChanged.connect(self.on_current_changed)
self.tabCloseRequested.connect(self.on_tab_close_requested)
self.new_tab_requested.connect(self.tabopen)
self.currentChanged.connect(self.on_current_changed)
self.cur_load_started.connect(self.on_cur_load_started)
self.cur_fullscreen_requested.connect(self.widget.tabBar().maybe_hide)
self.widget.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
self.cur_fullscreen_requested.connect(self.tabBar().maybe_hide)
self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
self._undo_stack = []
self._filter = signalfilter.SignalFilter(win_id, self)
self._now_focused = None
@@ -130,12 +128,12 @@ class TabbedBrowser(QWidget):
self.search_options = {}
self._local_marks = {}
self._global_marks = {}
self.default_window_icon = self.widget.window().windowIcon()
self.default_window_icon = self.window().windowIcon()
self.private = private
config.instance.changed.connect(self._on_config_changed)
def __repr__(self):
return utils.get_repr(self, count=self.widget.count())
return utils.get_repr(self, count=self.count())
@pyqtSlot(str)
def _on_config_changed(self, option):
@@ -144,7 +142,7 @@ class TabbedBrowser(QWidget):
elif option == 'window.title_format':
self._update_window_title()
elif option in ['tabs.title.format', 'tabs.title.format_pinned']:
self.widget.update_tab_titles()
self._update_tab_titles()
def _tab_index(self, tab):
"""Get the index of a given tab.
@@ -152,7 +150,7 @@ class TabbedBrowser(QWidget):
Raises TabDeletedError if the tab doesn't exist anymore.
"""
try:
idx = self.widget.indexOf(tab)
idx = self.indexOf(tab)
except RuntimeError as e:
log.webview.debug("Got invalid tab ({})!".format(e))
raise TabDeletedError(e)
@@ -168,8 +166,8 @@ class TabbedBrowser(QWidget):
iterating over the list.
"""
widgets = []
for i in range(self.widget.count()):
widget = self.widget.widget(i)
for i in range(self.count()):
widget = self.widget(i)
if widget is None:
log.webview.debug("Got None-widget in tabbedbrowser!")
else:
@@ -188,16 +186,16 @@ class TabbedBrowser(QWidget):
if field is not None and ('{' + field + '}') not in title_format:
return
idx = self.widget.currentIndex()
idx = self.currentIndex()
if idx == -1:
# (e.g. last tab removed)
log.webview.debug("Not updating window title because index is -1")
return
fields = self.widget.get_tab_fields(idx)
fields = self.get_tab_fields(idx)
fields['id'] = self._win_id
title = title_format.format(**fields)
self.widget.window().setWindowTitle(title)
self.window().setWindowTitle(title)
def _connect_tab_signals(self, tab):
"""Set up the needed signals for tab."""
@@ -218,8 +216,6 @@ class TabbedBrowser(QWidget):
self._filter.create(self.cur_load_status_changed, tab))
tab.fullscreen_requested.connect(
self._filter.create(self.cur_fullscreen_requested, tab))
tab.caret.selection_toggled.connect(
self._filter.create(self.cur_caret_selection_toggled, tab))
# misc
tab.scroller.perc_changed.connect(self.on_scroll_pos_changed)
tab.url_changed.connect(
@@ -251,8 +247,8 @@ class TabbedBrowser(QWidget):
Return:
The current URL as QUrl.
"""
idx = self.widget.currentIndex()
return self.widget.tab_url(idx)
idx = self.currentIndex()
return super().tab_url(idx)
def shutdown(self):
"""Try to shut down all tabs cleanly."""
@@ -288,7 +284,7 @@ class TabbedBrowser(QWidget):
new_undo: Whether the undo entry should be a new item in the stack.
"""
last_close = config.val.tabs.last_close
count = self.widget.count()
count = self.count()
if last_close == 'ignore' and count == 1:
return
@@ -315,7 +311,7 @@ class TabbedBrowser(QWidget):
new_undo: Whether the undo entry should be a new item in the stack.
crashed: Whether we're closing a tab with crashed renderer process.
"""
idx = self.widget.indexOf(tab)
idx = self.indexOf(tab)
if idx == -1:
if crashed:
return
@@ -353,7 +349,7 @@ class TabbedBrowser(QWidget):
self._undo_stack[-1].append(entry)
tab.shutdown()
self.widget.removeTab(idx)
self.removeTab(idx)
if not crashed:
# WORKAROUND for a segfault when we delete the crashed tab.
# see https://bugreports.qt.io/browse/QTBUG-58698
@@ -366,14 +362,14 @@ class TabbedBrowser(QWidget):
last_close = config.val.tabs.last_close
use_current_tab = False
if last_close in ['blank', 'startpage', 'default-page']:
only_one_tab_open = self.widget.count() == 1
no_history = len(self.widget.widget(0).history) == 1
only_one_tab_open = self.count() == 1
no_history = len(self.widget(0).history) == 1
urls = {
'blank': QUrl('about:blank'),
'startpage': config.val.url.start_pages[0],
'default-page': config.val.url.default_page,
}
first_tab_url = self.widget.widget(0).url()
first_tab_url = self.widget(0).url()
last_close_urlstr = urls[last_close].toString().rstrip('/')
first_tab_urlstr = first_tab_url.toString().rstrip('/')
last_close_url_used = first_tab_urlstr == last_close_urlstr
@@ -382,13 +378,13 @@ class TabbedBrowser(QWidget):
for entry in reversed(self._undo_stack.pop()):
if use_current_tab:
newtab = self.widget.widget(0)
newtab = self.widget(0)
use_current_tab = False
else:
newtab = self.tabopen(background=False, idx=entry.index)
newtab.history.deserialize(entry.history)
self.widget.set_tab_pinned(newtab, entry.pinned)
self.set_tab_pinned(newtab, entry.pinned)
@pyqtSlot('QUrl', bool)
def openurl(self, url, newtab):
@@ -399,15 +395,15 @@ class TabbedBrowser(QWidget):
newtab: True to open URL in a new tab, False otherwise.
"""
qtutils.ensure_valid(url)
if newtab or self.widget.currentWidget() is None:
if newtab or self.currentWidget() is None:
self.tabopen(url, background=False)
else:
self.widget.currentWidget().openurl(url)
self.currentWidget().openurl(url)
@pyqtSlot(int)
def on_tab_close_requested(self, idx):
"""Close a tab via an index."""
tab = self.widget.widget(idx)
tab = self.widget(idx)
if tab is None:
log.webview.debug("Got invalid tab {} for index {}!".format(
tab, idx))
@@ -458,7 +454,7 @@ class TabbedBrowser(QWidget):
"related {}, idx {}".format(
url, background, related, idx))
if (config.val.tabs.tabs_are_windows and self.widget.count() > 0 and
if (config.val.tabs.tabs_are_windows and self.count() > 0 and
not ignore_tabs_are_windows):
window = mainwindow.MainWindow(private=self.private)
window.show()
@@ -468,12 +464,12 @@ class TabbedBrowser(QWidget):
related=related)
tab = browsertab.create(win_id=self._win_id, private=self.private,
parent=self.widget)
parent=self)
self._connect_tab_signals(tab)
if idx is None:
idx = self._get_new_tab_idx(related)
self.widget.insertTab(idx, tab, "")
self.insertTab(idx, tab, "")
if url is not None:
tab.openurl(url)
@@ -484,14 +480,10 @@ class TabbedBrowser(QWidget):
# Make sure the background tab has the correct initial size.
# With a foreground tab, it's going to be resized correctly by the
# layout anyways.
tab.resize(self.widget.currentWidget().size())
self.widget.tab_index_changed.emit(self.widget.currentIndex(),
self.widget.count())
tab.resize(self.currentWidget().size())
self.tab_index_changed.emit(self.currentIndex(), self.count())
else:
self.widget.setCurrentWidget(tab)
# WORKAROUND for https://bugreports.qt.io/browse/QTBUG-68076
# Still seems to be needed with Qt 5.11.1
tab.setFocus()
self.setCurrentWidget(tab)
tab.show()
self.new_tab.emit(tab, idx)
@@ -534,8 +526,15 @@ class TabbedBrowser(QWidget):
def _update_favicons(self):
"""Update favicons when config was changed."""
for tab in self.widgets():
self.widget.update_tab_favicon(tab)
for i, tab in enumerate(self.widgets()):
if config.val.tabs.favicons.show:
self.setTabIcon(i, tab.icon())
if config.val.tabs.tabs_are_windows:
self.window().setWindowIcon(tab.icon())
else:
self.setTabIcon(i, QIcon())
if config.val.tabs.tabs_are_windows:
self.window().setWindowIcon(self.default_window_icon)
@pyqtSlot()
def on_load_started(self, tab):
@@ -549,14 +548,14 @@ class TabbedBrowser(QWidget):
except TabDeletedError:
# We can get signals for tabs we already deleted...
return
self.widget.update_tab_title(idx)
self._update_tab_title(idx)
if tab.data.keep_icon:
tab.data.keep_icon = False
else:
if (config.val.tabs.tabs_are_windows and
tab.data.should_show_icon()):
self.widget.window().setWindowIcon(self.default_window_icon)
if idx == self.widget.currentIndex():
config.val.tabs.favicons.show):
self.window().setWindowIcon(self.default_window_icon)
if idx == self.currentIndex():
self._update_window_title()
@pyqtSlot()
@@ -587,8 +586,8 @@ class TabbedBrowser(QWidget):
return
log.webview.debug("Changing title for idx {} to '{}'".format(
idx, text))
self.widget.set_page_title(idx, text)
if idx == self.widget.currentIndex():
self.set_page_title(idx, text)
if idx == self.currentIndex():
self._update_window_title()
@pyqtSlot(browsertab.AbstractTab, QUrl)
@@ -605,8 +604,8 @@ class TabbedBrowser(QWidget):
# We can get signals for tabs we already deleted...
return
if not self.widget.page_title(idx):
self.widget.set_page_title(idx, url.toDisplayString())
if not self.page_title(idx):
self.set_page_title(idx, url.toDisplayString())
@pyqtSlot(browsertab.AbstractTab, QIcon)
def on_icon_changed(self, tab, icon):
@@ -618,23 +617,23 @@ class TabbedBrowser(QWidget):
tab: The WebView where the title was changed.
icon: The new icon
"""
if not tab.data.should_show_icon():
if not config.val.tabs.favicons.show:
return
try:
idx = self._tab_index(tab)
except TabDeletedError:
# We can get signals for tabs we already deleted...
return
self.widget.setTabIcon(idx, icon)
self.setTabIcon(idx, icon)
if config.val.tabs.tabs_are_windows:
self.widget.window().setWindowIcon(icon)
self.window().setWindowIcon(icon)
@pyqtSlot(usertypes.KeyMode)
def on_mode_left(self, mode):
"""Give focus to current tab if command mode was left."""
if mode in [usertypes.KeyMode.command, usertypes.KeyMode.prompt,
usertypes.KeyMode.yesno]:
widget = self.widget.currentWidget()
widget = self.currentWidget()
log.modes.debug("Left status-input mode, focusing {!r}".format(
widget))
if widget is None:
@@ -650,7 +649,7 @@ class TabbedBrowser(QWidget):
if idx == -1 or self.shutting_down:
# closing the last tab (before quitting) or shutting down
return
tab = self.widget.widget(idx)
tab = self.widget(idx)
if tab is None:
log.webview.debug("on_current_changed got called with invalid "
"index {}".format(idx))
@@ -678,8 +677,8 @@ class TabbedBrowser(QWidget):
self._now_focused = tab
self.current_tab_changed.emit(tab)
QTimer.singleShot(0, self._update_window_title)
self._tab_insert_idx_left = self.widget.currentIndex()
self._tab_insert_idx_right = self.widget.currentIndex() + 1
self._tab_insert_idx_left = self.currentIndex()
self._tab_insert_idx_right = self.currentIndex() + 1
@pyqtSlot()
def on_cmd_return_pressed(self):
@@ -697,9 +696,9 @@ class TabbedBrowser(QWidget):
stop = config.val.colors.tabs.indicator.stop
system = config.val.colors.tabs.indicator.system
color = utils.interpolate_color(start, stop, perc, system)
self.widget.set_tab_indicator_color(idx, color)
self.widget.update_tab_title(idx)
if idx == self.widget.currentIndex():
self.set_tab_indicator_color(idx, color)
self._update_tab_title(idx)
if idx == self.currentIndex():
self._update_window_title()
def on_load_finished(self, tab, ok):
@@ -716,23 +715,23 @@ class TabbedBrowser(QWidget):
color = utils.interpolate_color(start, stop, 100, system)
else:
color = config.val.colors.tabs.indicator.error
self.widget.set_tab_indicator_color(idx, color)
self.widget.update_tab_title(idx)
if idx == self.widget.currentIndex():
self.set_tab_indicator_color(idx, color)
self._update_tab_title(idx)
if idx == self.currentIndex():
self._update_window_title()
tab.handle_auto_insert_mode(ok)
@pyqtSlot()
def on_scroll_pos_changed(self):
"""Update tab and window title when scroll position changed."""
idx = self.widget.currentIndex()
idx = self.currentIndex()
if idx == -1:
# (e.g. last tab removed)
log.webview.debug("Not updating scroll position because index is "
"-1")
return
self._update_window_title('scroll_pos')
self.widget.update_tab_title(idx, 'scroll_pos')
self._update_tab_title(idx, 'scroll_pos')
def _on_renderer_process_terminated(self, tab, status, code):
"""Show an error when a renderer process terminated."""
@@ -765,7 +764,7 @@ class TabbedBrowser(QWidget):
# WORKAROUND for https://bugreports.qt.io/browse/QTBUG-58698
message.error(msg)
self._remove_tab(tab, crashed=True)
if self.widget.count() == 0:
if self.count() == 0:
self.tabopen(QUrl('about:blank'))
def resizeEvent(self, e):
@@ -802,7 +801,7 @@ class TabbedBrowser(QWidget):
if key != "'":
message.error("Failed to set mark: url invalid")
return
point = self.widget.currentWidget().scroller.pos_px()
point = self.currentWidget().scroller.pos_px()
if key.isupper():
self._global_marks[key] = point, url
@@ -823,7 +822,7 @@ class TabbedBrowser(QWidget):
except qtutils.QtValueError:
urlkey = None
tab = self.widget.currentWidget()
tab = self.currentWidget()
if key.isupper():
if key in self._global_marks:

View File

@@ -60,7 +60,7 @@ class TabWidget(QTabWidget):
self.setTabBar(bar)
bar.tabCloseRequested.connect(self.tabCloseRequested)
bar.tabMoved.connect(functools.partial(
QTimer.singleShot, 0, self.update_tab_titles))
QTimer.singleShot, 0, self._update_tab_titles))
bar.currentChanged.connect(self._on_current_changed)
bar.new_tab_requested.connect(self._on_new_tab_requested)
self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
@@ -108,8 +108,7 @@ class TabWidget(QTabWidget):
bar.set_tab_data(idx, 'pinned', pinned)
tab.data.pinned = pinned
self.update_tab_favicon(tab)
self.update_tab_title(idx)
self._update_tab_title(idx)
def tab_indicator_color(self, idx):
"""Get the tab indicator color for the given index."""
@@ -118,13 +117,13 @@ class TabWidget(QTabWidget):
def set_page_title(self, idx, title):
"""Set the tab title user data."""
self.tabBar().set_tab_data(idx, 'page-title', title)
self.update_tab_title(idx)
self._update_tab_title(idx)
def page_title(self, idx):
"""Get the tab title user data."""
return self.tabBar().page_title(idx)
def update_tab_title(self, idx, field=None):
def _update_tab_title(self, idx, field=None):
"""Update the tab text for the given tab.
Args:
@@ -149,13 +148,9 @@ class TabWidget(QTabWidget):
title = '' if fmt is None else fmt.format(**fields)
tabbar = self.tabBar()
# Only change the tab title if it changes, setting the tab title causes
# a size recalculation which is slow.
if tabbar.tabText(idx) != title:
tabbar.setTabText(idx, title)
# always show only plain title in tooltips
tabbar.setTabToolTip(idx, fields['title'])
tabbar.setTabToolTip(idx, title)
def get_tab_fields(self, idx):
"""Get the tab field data."""
@@ -202,20 +197,20 @@ class TabWidget(QTabWidget):
fields['scroll_pos'] = scroll_pos
return fields
def update_tab_titles(self):
def _update_tab_titles(self):
"""Update all texts."""
for idx in range(self.count()):
self.update_tab_title(idx)
self._update_tab_title(idx)
def tabInserted(self, idx):
"""Update titles when a tab was inserted."""
super().tabInserted(idx)
self.update_tab_titles()
self._update_tab_titles()
def tabRemoved(self, idx):
"""Update titles when a tab was removed."""
super().tabRemoved(idx)
self.update_tab_titles()
self._update_tab_titles()
def addTab(self, page, icon_or_text, text_or_empty=None):
"""Override addTab to use our own text setting logic.
@@ -301,19 +296,6 @@ class TabWidget(QTabWidget):
qtutils.ensure_valid(url)
return url
def update_tab_favicon(self, tab: QWidget):
"""Update favicon of the given tab."""
idx = self.indexOf(tab)
if tab.data.should_show_icon():
self.setTabIcon(idx, tab.icon())
if config.val.tabs.tabs_are_windows:
self.window().setWindowIcon(tab.icon())
else:
self.setTabIcon(idx, QIcon())
if config.val.tabs.tabs_are_windows:
self.window().setWindowIcon(self.window().windowIcon())
class TabBar(QTabBar):
@@ -376,9 +358,7 @@ class TabBar(QTabBar):
# Clear _minimum_tab_size_hint_helper cache when appropriate
if option in ["tabs.indicator.padding",
"tabs.padding",
"tabs.indicator.width",
"tabs.min_width",
"tabs.pinned.shrink"]:
"tabs.indicator.width"]:
self._minimum_tab_size_hint_helper.cache_clear()
def _on_show_switching_delay_changed(self):
@@ -497,8 +477,7 @@ class TabBar(QTabBar):
Args:
index: The index of the tab to get a size hint for.
ellipsis: Whether to use ellipsis to calculate width
instead of the tab's text.
Forced to False for pinned tabs.
instead of the tab's text.
Return:
A QSize of the smallest tab size we can make.
"""
@@ -510,19 +489,14 @@ class TabBar(QTabBar):
else:
icon_width = min(icon.actualSize(self.iconSize()).width(),
self.iconSize().width()) + icon_padding
pinned = self._tab_pinned(index)
if not self.vertical and pinned and config.val.tabs.pinned.shrink:
# Never consider ellipsis an option for horizontal pinned tabs
ellipsis = False
return self._minimum_tab_size_hint_helper(self.tabText(index),
icon_width, ellipsis,
pinned)
icon_width,
ellipsis)
@functools.lru_cache(maxsize=2**9)
def _minimum_tab_size_hint_helper(self, tab_text: str,
icon_width: int,
ellipsis: bool, pinned: bool) -> QSize:
ellipsis: bool) -> QSize:
"""Helper function to cache tab results.
Config values accessed in here should be added to _on_config_changed to
@@ -547,10 +521,6 @@ class TabBar(QTabBar):
height = self.fontMetrics().height() + padding_v
width = (text_width + icon_width +
padding_h + indicator_width)
min_width = config.val.tabs.min_width
if (not self.vertical and min_width > 0 and
not pinned or not config.val.tabs.pinned.shrink):
width = max(min_width, width)
return QSize(width, height)
def _pinned_statistics(self) -> (int, int):
@@ -580,12 +550,6 @@ class TabBar(QTabBar):
Return:
A QSize.
"""
if self.count() == 0:
# This happens on startup on macOS.
# We return it directly rather than setting `size' because we don't
# want to ensure it's valid in this special case.
return QSize()
minimum_size = self.minimumTabSizeHint(index)
height = minimum_size.height()
if self.vertical:
@@ -598,6 +562,11 @@ class TabBar(QTabBar):
else:
width = int(confwidth)
size = QSize(max(minimum_size.width(), width), height)
elif self.count() == 0:
# This happens on startup on macOS.
# We return it directly rather than setting `size' because we don't
# want to ensure it's valid in this special case.
return QSize()
else:
if config.val.tabs.pinned.shrink:
pinned = self._tab_pinned(index)
@@ -920,7 +889,7 @@ class TabBarStyle(QCommonStyle):
# reserve space for favicon when tab bar is vertical (issue #1968)
position = config.val.tabs.position
if (position in [QTabWidget.East, QTabWidget.West] and
config.val.tabs.favicons.show != 'never'):
config.val.tabs.favicons.show):
tab_icon_size = icon_size
else:
actual_size = opt.icon.actualSize(icon_size, icon_mode, icon_state)

View File

@@ -45,7 +45,7 @@ class PyPIVersionClient(QObject):
arg: The error message, as string.
"""
API_URL = 'https://pypi.org/pypi/{}/json'
API_URL = 'https://pypi.python.org/pypi/{}/json'
success = pyqtSignal(str)
error = pyqtSignal(str)

View File

@@ -166,9 +166,8 @@ def _nvidia_shader_workaround():
See https://bugs.launchpad.net/ubuntu/+source/python-qt4/+bug/941826
"""
assert objects.backend == usertypes.Backend.QtWebEngine, objects.backend
libgl = ctypes.util.find_library("GL")
if libgl is not None:
ctypes.CDLL(libgl, mode=ctypes.RTLD_GLOBAL)
if utils.is_linux:
ctypes.CDLL(ctypes.util.find_library("GL"), mode=ctypes.RTLD_GLOBAL)
def _handle_nouveau_graphics():

View File

@@ -42,7 +42,6 @@ class ExternalEditor(QObject):
_proc: The GUIProcess of the editor.
_watcher: A QFileSystemWatcher to watch the edited file for changes.
Only set if watch=True.
_content: The last-saved text of the editor.
Signals:
file_updated: The text in the edited file was updated.
@@ -113,7 +112,19 @@ class ExternalEditor(QObject):
if self._filename is not None:
raise ValueError("Already editing a file!")
try:
self._filename = self._create_tempfile(text, 'qutebrowser-editor-')
# Close while the external process is running, as otherwise systems
# with exclusive write access (e.g. Windows) may fail to update
# the file from the external editor, see
# https://github.com/qutebrowser/qutebrowser/issues/1767
with tempfile.NamedTemporaryFile(
# pylint: disable=bad-continuation
mode='w', prefix='qutebrowser-editor-',
encoding=config.val.editor.encoding,
delete=False) as fobj:
# pylint: enable=bad-continuation
if text:
fobj.write(text)
self._filename = fobj.name
except OSError as e:
message.error("Failed to create initial file: {}".format(e))
return
@@ -123,32 +134,6 @@ class ExternalEditor(QObject):
line, column = self._calc_line_and_column(text, caret_position)
self._start_editor(line=line, column=column)
def backup(self):
"""Create a backup if the content has changed from the original."""
if not self._content:
return
try:
fname = self._create_tempfile(self._content,
'qutebrowser-editor-backup-')
message.info('Editor backup at {}'.format(fname))
except OSError as e:
message.error('Failed to create editor backup: {}'.format(e))
def _create_tempfile(self, text, prefix):
# Close while the external process is running, as otherwise systems
# with exclusive write access (e.g. Windows) may fail to update
# the file from the external editor, see
# https://github.com/qutebrowser/qutebrowser/issues/1767
with tempfile.NamedTemporaryFile(
# pylint: disable=bad-continuation
mode='w', prefix=prefix,
encoding=config.val.editor.encoding,
delete=False) as fobj:
# pylint: enable=bad-continuation
if text:
fobj.write(text)
return fobj.name
@pyqtSlot(str)
def _on_file_changed(self, path):
try:

View File

@@ -28,21 +28,6 @@ from PyQt5.QtNetwork import (QNetworkAccessManager, QNetworkRequest,
QNetworkReply)
class HTTPRequest(QNetworkRequest):
"""A QNetworkRquest that follows (secure) redirects by default."""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
try:
self.setAttribute(QNetworkRequest.RedirectPolicyAttribute,
QNetworkRequest.NoLessSafeRedirectPolicy)
except AttributeError:
# RedirectPolicyAttribute was introduced in 5.9 to replace
# FollowRedirectsAttribute.
self.setAttribute(QNetworkRequest.FollowRedirectsAttribute,
True)
class HTTPClient(QObject):
"""An HTTP client based on QNetworkAccessManager.
@@ -78,7 +63,7 @@ class HTTPClient(QObject):
if data is None:
data = {}
encoded_data = urllib.parse.urlencode(data).encode('utf-8')
request = HTTPRequest(url)
request = QNetworkRequest(url)
request.setHeader(QNetworkRequest.ContentTypeHeader,
'application/x-www-form-urlencoded;charset=utf-8')
reply = self._nam.post(request, encoded_data)
@@ -92,7 +77,7 @@ class HTTPClient(QObject):
Args:
url: The URL to access, as QUrl.
"""
request = HTTPRequest(url)
request = QNetworkRequest(url)
reply = self._nam.get(request)
self._handle_reply(reply)

View File

@@ -130,7 +130,7 @@ class KeyHintView(QLabel):
).format(
html.escape(prefix),
suffix_color,
html.escape(str(seq)[len(prefix):]),
html.escape(str(seq[len(prefix):])),
html.escape(cmd)
)
text = '<table>{}</table>'.format(text)

View File

@@ -246,7 +246,7 @@ class SessionManager(QObject):
if tabbed_browser.private:
win_data['private'] = True
for i, tab in enumerate(tabbed_browser.widgets()):
active = i == tabbed_browser.widget.currentIndex()
active = i == tabbed_browser.currentIndex()
win_data['tabs'].append(self._save_tab(tab, active))
data['windows'].append(win_data)
return data
@@ -427,12 +427,11 @@ class SessionManager(QObject):
if tab.get('active', False):
tab_to_focus = i
if new_tab.data.pinned:
tabbed_browser.widget.set_tab_pinned(new_tab,
new_tab.data.pinned)
tabbed_browser.set_tab_pinned(new_tab, new_tab.data.pinned)
if tab_to_focus is not None:
tabbed_browser.widget.setCurrentIndex(tab_to_focus)
tabbed_browser.setCurrentIndex(tab_to_focus)
if win.get('active', False):
QTimer.singleShot(0, tabbed_browser.widget.activateWindow)
QTimer.singleShot(0, tabbed_browser.activateWindow)
if data['windows']:
self.did_load = True

View File

@@ -185,7 +185,7 @@ def debug_cache_stats():
tabbed_browser = objreg.get('tabbed-browser', scope='window',
window='last-focused')
# pylint: disable=protected-access
tab_bar = tabbed_browser.widget.tabBar()
tab_bar = tabbed_browser.tabBar()
tabbed_browser_info = tab_bar._minimum_tab_size_hint_helper.cache_info()
# pylint: enable=protected-access

View File

@@ -87,11 +87,10 @@ def log_signals(obj):
return ret
obj.__init__ = new_init
return obj
else:
connect_log_slot(obj)
return obj
def qenum_key(base, value, add_base=False, klass=None):
"""Convert a Qt Enum value to its key as a string.

View File

@@ -20,9 +20,6 @@
"""Utilities related to javascript interaction."""
from qutebrowser.utils import jinja
def string_escape(text):
"""Escape values special to javascript in strings.
@@ -73,9 +70,3 @@ def assemble(module, function, *args):
parts = ['window', '_qutebrowser', module, function]
code = '"use strict";\n{}({});'.format('.'.join(parts), js_args)
return code
def wrap_global(name, *sources):
"""Wrap a script using window._qutebrowser."""
template = jinja.js_environment.get_template('global_wrapper.js')
return template.render(code='\n'.join(sources), name=name)

View File

@@ -171,7 +171,7 @@ def _get_tab_registry(win_id, tab_id):
if tab_id == 'current':
tabbed_browser = get('tabbed-browser', scope='window', window=win_id)
tab = tabbed_browser.widget.currentWidget()
tab = tabbed_browser.currentWidget()
if tab is None:
raise RegistryUnavailableError('window')
tab_id = tab.tab_id

View File

@@ -102,12 +102,6 @@ def _get_search_url(txt):
engine = 'DEFAULT'
template = config.val.url.searchengines[engine]
url = qurl_from_user_input(template.format(urllib.parse.quote(term)))
if config.val.url.open_base_url and term in config.val.url.searchengines:
url = qurl_from_user_input(config.val.url.searchengines[term])
url.setPath(None)
url.setFragment(None)
url.setQuery(None)
qtutils.ensure_valid(url)
return url

View File

@@ -269,8 +269,6 @@ def _os_info():
else:
versioninfo = '.'.join(versioninfo)
osver = ', '.join([e for e in [release, versioninfo, machine] if e])
elif utils.is_posix:
osver = ' '.join(platform.uname())
else:
osver = '?'
lines.append('OS Version: {}'.format(osver))
@@ -317,10 +315,8 @@ def _chromium_version():
Qt 5.8: Chromium 53
Qt 5.9: Chromium 56
Qt 5.10: Chromium 61
Qt 5.11: Chromium 65
Qt 5.12: Chromium 69 (?)
Also see https://www.chromium.org/developers/calendar
Qt 5.11: Chromium 63
Qt 5.12: Chromium 65 (?)
"""
if QWebEngineProfile is None:
# This should never happen

View File

@@ -361,7 +361,7 @@ def github_upload(artifacts, tag):
repo = gh.repository('qutebrowser', 'qutebrowser')
release = None # to satisfy pylint
for release in repo.releases():
for release in repo.iter_releases():
if release.tag_name == tag:
break
else:
@@ -401,6 +401,14 @@ def main():
run_asciidoc2html(args)
if os.name == 'nt':
if sys.maxsize > 2**32:
# WORKAROUND
print("Due to a python/Windows bug, this script needs to be run ")
print("with a 32bit Python.")
print()
print("See http://bugs.python.org/issue24493 and ")
print("https://github.com/pypa/virtualenv/issues/774")
sys.exit(1)
artifacts = build_windows()
elif sys.platform == 'darwin':
artifacts = build_mac()

View File

@@ -246,8 +246,6 @@ def apply_fake_os(monkeypatch, request):
elif name == 'linux':
linux = True
posix = True
elif name == 'posix':
posix = True
else:
raise ValueError("Invalid fake_os {}".format(name))

View File

@@ -3,10 +3,10 @@
<head>
<script type="text/javascript">
let my_window;
var my_window;
function open_modal() {
my_window = window.open('about:blank', 'window', 'modal');
window.open('about:blank', 'window', 'modal');
}
function open_normal() {
@@ -17,15 +17,13 @@
window.open('', 'my_window');
}
function close_normal() {
function close() {
my_window.close();
console.log("window closed");
}
function close_twice() {
my_window.close();
my_window.close();
console.log("window closed");
}
</script>
</head>
@@ -35,7 +33,7 @@
<button onclick="open_normal()" id="open-normal">normal</button>
<button onclick="open_modal()" id="open-modal">modal</button>
<button onclick="open_invalid()" id="open-invalid">invalid/no URL</button>
<button onclick="close_normal()" id="close-normal">close</button>
<button onclick="close()" id="close-normal">close</button>
<button onclick="close_twice()" id="close-twice">close twice (issue 906)</button>
</body>

View File

@@ -16,8 +16,6 @@
BAZ<br/>
space travel<br/>
/slash<br/>
-r reversed<br/>
;; semicolons<br/>
<a class="toselect" href="hello.txt">follow me!</a><br/>
</p>
</body>

View File

@@ -128,7 +128,6 @@ Feature: Opening external editors
And I run :tab-close
And I kill the waiting editor
Then the error "Edited element vanished" should be shown
And the message "Editor backup at *" should be shown
# Could not get signals working on Windows
@posix

View File

@@ -8,7 +8,6 @@ Feature: Javascript stuff
When I open data/javascript/consolelog.html
Then the javascript message "console.log works!" should be logged
@flaky
Scenario: Opening/Closing a window via JS
When I open data/javascript/window_open.html
And I run :tab-only
@@ -16,10 +15,7 @@ Feature: Javascript stuff
And I wait for "Changing title for idx 1 to 'about:blank'" in the log
And I run :tab-focus 1
And I run :click-element id close-normal
And I wait for "[*] window closed" in the log
Then "Focus object changed: *" should be logged
And the following tabs should be open:
- data/javascript/window_open.html (active)
@qtwebkit_skip
Scenario: Opening/closing a modal window via JS
@@ -29,11 +25,8 @@ Feature: Javascript stuff
And I wait for "Changing title for idx 1 to 'about:blank'" in the log
And I run :tab-focus 1
And I run :click-element id close-normal
And I wait for "[*] window closed" in the log
Then "Focus object changed: *" should be logged
And "Web*Dialog requested, but we don't support that!" should be logged
And the following tabs should be open:
- data/javascript/window_open.html (active)
# https://github.com/qutebrowser/qutebrowser/issues/906
@@ -46,7 +39,6 @@ Feature: Javascript stuff
And I wait for "Changing title for idx 2 to 'about:blank'" in the log
And I run :tab-focus 2
And I run :click-element id close-twice
And I wait for "[*] window closed" in the log
Then "Requested to close * which does not exist!" should be logged
@qtwebkit_skip @flaky
@@ -59,7 +51,6 @@ Feature: Javascript stuff
And I run :buffer window_open.html
And I run :click-element id close-twice
And I wait for "Focus object changed: *" in the log
And I wait for "[*] window closed" in the log
Then no crash should happen
@flaky
@@ -183,15 +174,3 @@ Feature: Javascript stuff
When I set content.javascript.enabled to false
And I open 500 without waiting
Then "Showing error page for* 500" should be logged
Scenario: Using JS after window.open
When I open data/hello.txt
And I set content.javascript.can_open_tabs_automatically to true
And I run :jseval window.open('about:blank')
And I open data/hello.txt
And I run :tab-only
And I open data/hints/html/simple.html
And I run :hint all
And I wait for "hints: a" in the log
And I run :leave-mode
Then "There was an error while getting hint elements" should not be logged

View File

@@ -26,7 +26,7 @@ Feature: Using :navigate
# prev/next
Scenario: Navigating to previous page
When I open data/navigate in a new tab
When I open data/navigate
And I run :navigate prev
Then data/navigate/prev.html should be loaded

View File

@@ -40,26 +40,11 @@ Feature: Searching on a page
Then "space " should be found
Scenario: Searching with / and slash in search term (issue 507)
When I run :set-cmd-text //slash
When I run :set-cmd-text -s //slash
And I run :command-accept
And I wait for "search found /slash" in the log
Then "/slash" should be found
Scenario: Searching with arguments at start of search term
When I run :set-cmd-text /-r reversed
And I run :command-accept
And I wait for "search found -r reversed" in the log
Then "-r reversed" should be found
Scenario: Searching with semicolons in search term
When I run :set-cmd-text /;
And I run :fake-key -g ;
And I run :fake-key -g <space>
And I run :fake-key -g semi
And I run :command-accept
And I wait for "search found ;; semi" in the log
Then ";; semi" should be found
# This doesn't work because this is QtWebKit behavior.
@xfail_norun
Scenario: Searching text with umlauts

View File

@@ -336,13 +336,13 @@ Feature: Tab management
When I set tabs.wrap to false
And I open data/numbers/1.txt
And I run :tab-prev
Then "First tab" should be logged
Then the error "First tab" should be shown
Scenario: :tab-next with last tab without wrap
When I set tabs.wrap to false
And I open data/numbers/1.txt
And I run :tab-next
Then "Last tab" should be logged
Then the error "Last tab" should be shown
Scenario: :tab-prev on first tab with wrap
When I set tabs.wrap to true

View File

@@ -101,9 +101,6 @@ def is_ignored_lowlevel_message(message):
' Error: No such file or directory',
# Qt 5.7.1
'qt.network.ssl: QSslSocket: cannot call unresolved function *',
# Qt 5.11
# DevTools listening on ws://127.0.0.1:37945/devtools/browser/...
'DevTools listening on *',
]
return any(testutils.pattern_match(pattern=pattern, value=message)
for pattern in ignored_messages)
@@ -172,7 +169,7 @@ def is_ignored_chromium_message(line):
# /tmp/pytest-of-florian/pytest-32/test_webengine_download_suffix0/
# downloads/download.bin: Operation not supported
('Could not set extended attribute user.xdg.* on file *: '
'Operation not supported*'),
'Operation not supported'),
# [5947:5947:0605/192837.856931:ERROR:render_process_impl.cc(112)]
# WebFrame LEAKED 1 TIMES
'WebFrame LEAKED 1 TIMES',
@@ -195,15 +192,6 @@ def is_ignored_chromium_message(line):
# [2734:2746:1107/131154.072032:ERROR:nss_ocsp.cc(591)] No
# URLRequestContext for NSS HTTP handler. host: ocsp.digicert.com
'No URLRequestContext for NSS HTTP handler. host: *',
# https://bugreports.qt.io/browse/QTBUG-66661
# [23359:23359:0319/115812.168578:WARNING:
# render_frame_host_impl.cc(2744)] OnDidStopLoading was called twice.
'OnDidStopLoading was called twice.',
# [30412:30412:0323/074933.387250:ERROR:node_channel.cc(899)] Dropping
# message on closed channel.
'Dropping message on closed channel.',
]
return any(testutils.pattern_match(pattern=pattern, value=message)
for pattern in ignored_messages)
@@ -357,10 +345,6 @@ class QuteProc(testprocess.Process):
# when calling QApplication::sync
"Focus object changed: "
"<PyQt5.QtWidgets.QWidget object at *>",
# Qt >= 5.11
"Focus object changed: "
"<qutebrowser.browser.webengine.webview.WebEngineView object "
"at *>",
]
if (log_line.category == 'ipc' and

View File

@@ -43,8 +43,7 @@ import helpers.stubs as stubsmod
import helpers.utils
from qutebrowser.config import (config, configdata, configtypes, configexc,
configfiles)
from qutebrowser.utils import objreg, standarddir, utils
from qutebrowser.browser import greasemonkey
from qutebrowser.utils import objreg, standarddir
from qutebrowser.browser.webkit import cookies
from qutebrowser.misc import savemanager, sql
from qutebrowser.keyinput import modeman
@@ -144,47 +143,6 @@ def fake_web_tab(stubs, tab_registry, mode_manager, qapp):
return stubs.FakeWebTab
@pytest.fixture
def greasemonkey_manager(data_tmpdir):
gm_manager = greasemonkey.GreasemonkeyManager()
objreg.register('greasemonkey', gm_manager)
yield
objreg.delete('greasemonkey')
@pytest.fixture
def webkit_tab(qtbot, tab_registry, cookiejar_and_cache, mode_manager,
session_manager_stub, greasemonkey_manager):
webkittab = pytest.importorskip('qutebrowser.browser.webkit.webkittab')
tab = webkittab.WebKitTab(win_id=0, mode_manager=mode_manager,
private=False)
qtbot.add_widget(tab)
return tab
@pytest.fixture
def webengine_tab(qtbot, tab_registry, fake_args, mode_manager,
session_manager_stub, greasemonkey_manager,
redirect_webengine_data):
webenginetab = pytest.importorskip(
'qutebrowser.browser.webengine.webenginetab')
tab = webenginetab.WebEngineTab(win_id=0, mode_manager=mode_manager,
private=False)
qtbot.add_widget(tab)
return tab
@pytest.fixture(params=['webkit', 'webengine'])
def web_tab(request):
"""A WebKitTab/WebEngineTab."""
if request.param == 'webkit':
return request.getfixturevalue('webkit_tab')
elif request.param == 'webengine':
return request.getfixturevalue('webengine_tab')
else:
raise utils.Unreachable
def _generate_cmdline_tests():
"""Generate testcases for test_split_binding."""
@attr.s

View File

@@ -27,7 +27,6 @@ import shutil
import attr
from PyQt5.QtCore import pyqtSignal, QPoint, QProcess, QObject, QUrl
from PyQt5.QtGui import QIcon
from PyQt5.QtNetwork import (QNetworkRequest, QAbstractNetworkCache,
QNetworkCacheMetaData)
from PyQt5.QtWidgets import QCommonStyle, QLineEdit, QWidget, QTabBar
@@ -267,9 +266,6 @@ class FakeWebTab(browsertab.AbstractTab):
def shutdown(self):
pass
def icon(self):
return QIcon()
class FakeSignal:
@@ -476,55 +472,37 @@ class SessionManagerStub:
def list_sessions(self):
return self.sessions
def save_autosave(self):
pass
class TabbedBrowserStub(QObject):
"""Stub for the tabbed-browser object."""
def __init__(self, parent=None):
super().__init__(parent)
self.widget = TabWidgetStub()
self.shutting_down = False
self.opened_url = None
def on_tab_close_requested(self, idx):
del self.widget.tabs[idx]
def widgets(self):
return self.widget.tabs
def tabopen(self, url):
self.opened_url = url
def openurl(self, url, *, newtab):
self.opened_url = url
class TabWidgetStub(QObject):
"""Stub for the tab-widget object."""
new_tab = pyqtSignal(browsertab.AbstractTab, int)
def __init__(self, parent=None):
super().__init__(parent)
self.tabs = []
self.shutting_down = False
self._qtabbar = QTabBar()
self.index_of = None
self.current_index = None
self.opened_url = None
def count(self):
return len(self.tabs)
def widgets(self):
return self.tabs
def widget(self, i):
return self.tabs[i]
def page_title(self, i):
return self.tabs[i].title()
def on_tab_close_requested(self, idx):
del self.tabs[idx]
def tabBar(self):
return self._qtabbar
@@ -548,6 +526,12 @@ class TabWidgetStub(QObject):
return None
return self.tabs[idx - 1]
def tabopen(self, url):
self.opened_url = url
def openurl(self, url, *, newtab):
self.opened_url = url
class ApplicationStub(QObject):

View File

@@ -120,10 +120,8 @@ def assert_urls(host_blocker, blocked=BLOCKLIST_HOSTS,
Ensure URLs in 'blocked' and not in 'whitelisted' are blocked.
All other URLs must not be blocked.
localhost is an example of a special case that shouldn't be blocked.
"""
whitelisted = list(whitelisted) + ['localhost']
whitelisted = list(whitelisted) + list(host_blocker.WHITELISTED)
for str_url in urls_to_check:
url = QUrl(str_url)
host = url.host()
@@ -249,16 +247,6 @@ def test_successful_update(config_stub, basedir, download_stub,
assert_urls(host_blocker, whitelisted=[])
def test_parsing_multiple_hosts_on_line(config_stub, basedir, download_stub,
data_tmpdir, tmpdir, win_registry,
caplog):
"""Ensure multiple hosts on a line get parsed correctly."""
host_blocker = adblock.HostBlocker()
bytes_host_line = ' '.join(BLOCKLIST_HOSTS).encode('utf-8')
host_blocker._parse_line(bytes_host_line)
assert_urls(host_blocker, whitelisted=[])
def test_failed_dl_update(config_stub, basedir, download_stub,
data_tmpdir, tmpdir, win_registry, caplog):
"""One blocklist fails to download.
@@ -353,7 +341,7 @@ def test_blocking_with_whitelist(config_stub, basedir, download_stub,
"""Ensure hosts in content.host_blocking.whitelist are never blocked."""
# Simulate adblock_update has already been run
# by creating a file named blocked-hosts,
# Exclude localhost from it as localhost is never blocked via list
# Exclude localhost from it, since localhost is in HostBlocker.WHITELISTED
filtered_blocked_hosts = BLOCKLIST_HOSTS[1:]
blocklist = create_blocklist(data_tmpdir,
blocked_hosts=filtered_blocked_hosts,

View File

@@ -68,8 +68,8 @@ def objects():
@pytest.mark.parametrize('index_of, emitted', [(0, True), (1, False)])
def test_filtering(objects, tabbed_browser_stubs, index_of, emitted):
browser = tabbed_browser_stubs[0]
browser.widget.current_index = 0
browser.widget.index_of = index_of
browser.current_index = 0
browser.index_of = index_of
objects.signaller.signal.emit('foo')
if emitted:
assert objects.signaller.filtered_signal_arg == 'foo'
@@ -80,8 +80,8 @@ def test_filtering(objects, tabbed_browser_stubs, index_of, emitted):
@pytest.mark.parametrize('index_of, verb', [(0, 'emitting'), (1, 'ignoring')])
def test_logging(caplog, objects, tabbed_browser_stubs, index_of, verb):
browser = tabbed_browser_stubs[0]
browser.widget.current_index = 0
browser.widget.index_of = index_of
browser.current_index = 0
browser.index_of = index_of
with caplog.at_level(logging.DEBUG, logger='signals'):
objects.signaller.signal.emit('foo')
@@ -94,8 +94,8 @@ def test_logging(caplog, objects, tabbed_browser_stubs, index_of, verb):
@pytest.mark.parametrize('index_of', [0, 1])
def test_no_logging(caplog, objects, tabbed_browser_stubs, index_of):
browser = tabbed_browser_stubs[0]
browser.widget.current_index = 0
browser.widget.index_of = index_of
browser.current_index = 0
browser.index_of = index_of
with caplog.at_level(logging.DEBUG, logger='signals'):
objects.signaller.link_hovered.emit('foo')
@@ -106,7 +106,7 @@ def test_no_logging(caplog, objects, tabbed_browser_stubs, index_of):
def test_runtime_error(objects, tabbed_browser_stubs):
"""Test that there's no crash if indexOf() raises RuntimeError."""
browser = tabbed_browser_stubs[0]
browser.widget.current_index = 0
browser.widget.index_of = RuntimeError
browser.current_index = 0
browser.index_of = RuntimeError
objects.signaller.signal.emit('foo')
assert objects.signaller.filtered_signal_arg is None

View File

@@ -0,0 +1,110 @@
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
# Copyright 2016-2018 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
#
# This file is part of qutebrowser.
#
# qutebrowser is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# qutebrowser is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
import pytest
from qutebrowser.browser import browsertab
from qutebrowser.utils import utils
pytestmark = pytest.mark.usefixtures('redirect_webengine_data')
try:
from PyQt5.QtWebKitWidgets import QWebView
except ImportError:
QWebView = None
try:
from PyQt5.QtWebEngineWidgets import QWebEngineView
except ImportError:
QWebEngineView = None
@pytest.fixture(params=[QWebView, QWebEngineView])
def view(qtbot, config_stub, request):
if request.param is None:
pytest.skip("View not available")
v = request.param()
qtbot.add_widget(v)
return v
@pytest.fixture(params=['webkit', 'webengine'])
def tab(request, qtbot, tab_registry, cookiejar_and_cache, mode_manager):
if request.param == 'webkit':
webkittab = pytest.importorskip('qutebrowser.browser.webkit.webkittab')
tab_class = webkittab.WebKitTab
elif request.param == 'webengine':
webenginetab = pytest.importorskip(
'qutebrowser.browser.webengine.webenginetab')
tab_class = webenginetab.WebEngineTab
else:
raise utils.Unreachable
t = tab_class(win_id=0, mode_manager=mode_manager)
qtbot.add_widget(t)
yield t
class Zoom(browsertab.AbstractZoom):
def _set_factor_internal(self, _factor):
pass
def factor(self):
raise utils.Unreachable
class Tab(browsertab.AbstractTab):
# pylint: disable=abstract-method
def __init__(self, win_id, mode_manager, parent=None):
super().__init__(win_id=win_id, mode_manager=mode_manager,
parent=parent)
self.history = browsertab.AbstractHistory(self)
self.scroller = browsertab.AbstractScroller(self, parent=self)
self.caret = browsertab.AbstractCaret(mode_manager=mode_manager,
tab=self, parent=self)
self.zoom = Zoom(tab=self)
self.search = browsertab.AbstractSearch(parent=self)
self.printing = browsertab.AbstractPrinting()
self.elements = browsertab.AbstractElements(tab=self)
self.action = browsertab.AbstractAction(tab=self)
def _install_event_filter(self):
pass
@pytest.mark.xfail(run=False, reason='Causes segfaults, see #1638')
def test_tab(qtbot, view, config_stub, tab_registry, mode_manager):
tab_w = Tab(win_id=0, mode_manager=mode_manager)
qtbot.add_widget(tab_w)
assert tab_w.win_id == 0
assert tab_w._widget is None
tab_w._set_widget(view)
assert tab_w._widget is view
assert tab_w.history._tab is tab_w
assert tab_w.history._history is view.history()
assert view.parent() is tab_w
with qtbot.waitExposed(tab_w):
tab_w.show()

View File

@@ -17,69 +17,31 @@
# You should have received a copy of the GNU General Public License
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
"""Tests for qutebrowser.browser.webengine.spell module."""
import logging
import os
from PyQt5.QtCore import QLibraryInfo
import pytest
from qutebrowser.browser.webengine import spell
from qutebrowser.utils import usertypes
def test_version(message_mock, caplog):
"""Tests parsing dictionary version from its file name."""
def test_version():
assert spell.version('en-US-8-0.bdic') == (8, 0)
assert spell.version('pl-PL-3-0.bdic') == (3, 0)
with caplog.at_level(logging.WARNING):
assert spell.version('malformed_filename') is None
msg = message_mock.getmsg(usertypes.MessageLevel.warning)
expected = ("Found a dictionary with a malformed name: malformed_filename")
assert msg.text == expected
with pytest.raises(ValueError):
spell.version('malformed_filename')
def test_dictionary_dir(monkeypatch):
monkeypatch.setattr(QLibraryInfo, 'location', lambda _: 'datapath')
assert spell.dictionary_dir() == os.path.join('datapath',
'qtwebengine_dictionaries')
def test_local_filename_dictionary_does_not_exist(monkeypatch):
"""Tests retrieving local filename when the dir doesn't exits."""
def test_local_filename_dictionary_does_not_exist(tmpdir, monkeypatch):
monkeypatch.setattr(
spell, 'dictionary_dir', lambda: '/some-non-existing-dir')
assert not spell.local_filename('en-US')
def test_local_filename_dictionary_not_installed(tmpdir, monkeypatch):
"""Tests retrieving local filename when the dict not installed."""
monkeypatch.setattr(spell, 'dictionary_dir', lambda: str(tmpdir))
assert not spell.local_filename('en-US')
def test_local_filename_not_installed_malformed(tmpdir, monkeypatch, caplog):
"""Tests retrieving local filename when the only file is malformed."""
monkeypatch.setattr(spell, 'dictionary_dir', lambda: str(tmpdir))
(tmpdir / 'en-US.bdic').ensure()
with caplog.at_level(logging.WARNING):
assert not spell.local_filename('en-US')
def test_local_filename_dictionary_installed(tmpdir, monkeypatch):
"""Tests retrieving local filename when the dict installed."""
monkeypatch.setattr(spell, 'dictionary_dir', lambda: str(tmpdir))
for lang_file in ['en-US-11-0.bdic', 'en-US-7-1.bdic', 'pl-PL-3-0.bdic']:
(tmpdir / lang_file).ensure()
assert spell.local_filename('en-US') == 'en-US-11-0'
assert spell.local_filename('pl-PL') == 'pl-PL-3-0'
def test_local_filename_installed_malformed(tmpdir, monkeypatch, caplog):
"""Tests retrieving local filename when the dict installed.
In this usecase, another existing file is malformed."""
monkeypatch.setattr(spell, 'dictionary_dir', lambda: str(tmpdir))
for lang_file in ['en-US-11-0.bdic', 'en-US-7-1.bdic', 'en-US.bdic']:
(tmpdir / lang_file).ensure()
with caplog.at_level(logging.WARNING):
assert spell.local_filename('en-US') == 'en-US-11-0'

View File

@@ -40,7 +40,9 @@ def test_big_cache_size(config_stub):
"""Make sure a too big cache size is handled correctly."""
config_stub.val.content.cache.size = 2 ** 63 - 1
profile = webenginesettings.default_profile
profile.setter.set_http_cache_size()
webenginesettings._set_http_cache_size(profile)
assert profile.httpCacheMaximumSize() == 2 ** 31 - 1
@@ -73,14 +75,3 @@ def test_existing_dict(config_stub, monkeypatch):
webenginesettings.private_profile]:
assert profile.isSpellCheckEnabled()
assert profile.spellCheckLanguages() == ['en-US-8-0']
@pytest.mark.skipif(
not qtutils.version_check('5.8'), reason="Needs Qt 5.8 or newer")
def test_spell_check_disabled(config_stub, monkeypatch):
monkeypatch.setattr(objects, 'backend', usertypes.Backend.QtWebEngine)
config_stub.val.spellcheck.languages = []
webenginesettings._update_settings('spellcheck.languages')
for profile in [webenginesettings.default_profile,
webenginesettings.private_profile]:
assert not profile.isSpellCheckEnabled()

View File

@@ -47,32 +47,15 @@ def test_element_js_webkit(webview, js_enabled, expected):
@pytest.mark.usefixtures('redirect_webengine_data')
@pytest.mark.parametrize('js_enabled, world, expected', [
# main world
(True, 0, 2.0),
(False, 0, None),
# application world
(True, 1, 2.0),
(False, 1, 2.0),
# user world
(True, 2, 2.0),
(False, 2, 2.0),
])
def test_simple_js_webengine(callback_checker, webengineview, qapp,
js_enabled, world, expected):
@pytest.mark.parametrize('js_enabled, expected', [(True, 2.0), (False, 2.0)])
def test_simple_js_webengine(callback_checker, webengineview, js_enabled,
expected):
"""With QtWebEngine, runJavaScript works even when JS is off."""
# If we get there (because of the webengineview fixture) we can be certain
# QtWebEngine is available
from PyQt5.QtWebEngineWidgets import QWebEngineSettings, QWebEngineScript
from PyQt5.QtWebEngineWidgets import QWebEngineSettings
webengineview.settings().setAttribute(QWebEngineSettings.JavascriptEnabled,
js_enabled)
assert world in [QWebEngineScript.MainWorld,
QWebEngineScript.ApplicationWorld,
QWebEngineScript.UserWorld]
settings = webengineview.settings()
settings.setAttribute(QWebEngineSettings.JavascriptEnabled, js_enabled)
qapp.processEvents()
page = webengineview.page()
page.runJavaScript('1 + 1', world, callback_checker.callback)
webengineview.page().runJavaScript('1 + 1', callback_checker.callback)
callback_checker.check(expected)

View File

@@ -97,11 +97,10 @@ def test_custom_env(qtbot, monkeypatch, py_proc, runner):
f.write('\n')
""")
with qtbot.waitSignal(runner.finished, timeout=10000):
with qtbot.waitSignal(runner.got_cmd, timeout=10000) as blocker:
runner.prepare_run(cmd, *args, env=env)
runner.store_html('')
runner.store_text('')
with qtbot.waitSignal(runner.got_cmd, timeout=10000) as blocker:
runner.prepare_run(cmd, *args, env=env)
runner.store_html('')
runner.store_text('')
data = blocker.args[0]
ret_env = json.loads(data)

View File

@@ -539,12 +539,12 @@ def test_session_completion(qtmodeltester, session_manager_stub):
def test_tab_completion(qtmodeltester, fake_web_tab, app_stub, win_registry,
tabbed_browser_stubs):
tabbed_browser_stubs[0].widget.tabs = [
tabbed_browser_stubs[0].tabs = [
fake_web_tab(QUrl('https://github.com'), 'GitHub', 0),
fake_web_tab(QUrl('https://wikipedia.org'), 'Wikipedia', 1),
fake_web_tab(QUrl('https://duckduckgo.com'), 'DuckDuckGo', 2),
]
tabbed_browser_stubs[1].widget.tabs = [
tabbed_browser_stubs[1].tabs = [
fake_web_tab(QUrl('https://wiki.archlinux.org'), 'ArchWiki', 0),
]
model = miscmodels.buffer()
@@ -567,12 +567,12 @@ def test_tab_completion(qtmodeltester, fake_web_tab, app_stub, win_registry,
def test_tab_completion_delete(qtmodeltester, fake_web_tab, app_stub,
win_registry, tabbed_browser_stubs):
"""Verify closing a tab by deleting it from the completion widget."""
tabbed_browser_stubs[0].widget.tabs = [
tabbed_browser_stubs[0].tabs = [
fake_web_tab(QUrl('https://github.com'), 'GitHub', 0),
fake_web_tab(QUrl('https://wikipedia.org'), 'Wikipedia', 1),
fake_web_tab(QUrl('https://duckduckgo.com'), 'DuckDuckGo', 2)
]
tabbed_browser_stubs[1].widget.tabs = [
tabbed_browser_stubs[1].tabs = [
fake_web_tab(QUrl('https://wiki.archlinux.org'), 'ArchWiki', 0),
]
model = miscmodels.buffer()
@@ -588,19 +588,19 @@ def test_tab_completion_delete(qtmodeltester, fake_web_tab, app_stub,
assert model.data(idx) == '0/2'
model.delete_cur_item(idx)
actual = [tab.url() for tab in tabbed_browser_stubs[0].widget.tabs]
actual = [tab.url() for tab in tabbed_browser_stubs[0].tabs]
assert actual == [QUrl('https://github.com'),
QUrl('https://duckduckgo.com')]
def test_other_buffer_completion(qtmodeltester, fake_web_tab, app_stub,
win_registry, tabbed_browser_stubs, info):
tabbed_browser_stubs[0].widget.tabs = [
tabbed_browser_stubs[0].tabs = [
fake_web_tab(QUrl('https://github.com'), 'GitHub', 0),
fake_web_tab(QUrl('https://wikipedia.org'), 'Wikipedia', 1),
fake_web_tab(QUrl('https://duckduckgo.com'), 'DuckDuckGo', 2),
]
tabbed_browser_stubs[1].widget.tabs = [
tabbed_browser_stubs[1].tabs = [
fake_web_tab(QUrl('https://wiki.archlinux.org'), 'ArchWiki', 0),
]
info.win_id = 1
@@ -618,37 +618,14 @@ def test_other_buffer_completion(qtmodeltester, fake_web_tab, app_stub,
})
def test_other_buffer_completion_id0(qtmodeltester, fake_web_tab, app_stub,
win_registry, tabbed_browser_stubs, info):
tabbed_browser_stubs[0].widget.tabs = [
fake_web_tab(QUrl('https://github.com'), 'GitHub', 0),
fake_web_tab(QUrl('https://wikipedia.org'), 'Wikipedia', 1),
fake_web_tab(QUrl('https://duckduckgo.com'), 'DuckDuckGo', 2),
]
tabbed_browser_stubs[1].widget.tabs = [
fake_web_tab(QUrl('https://wiki.archlinux.org'), 'ArchWiki', 0),
]
info.win_id = 0
model = miscmodels.other_buffer(info=info)
model.set_pattern('')
qtmodeltester.data_display_may_return_none = True
qtmodeltester.check(model)
_check_completions(model, {
'1': [
('1/1', 'https://wiki.archlinux.org', 'ArchWiki'),
],
})
def test_window_completion(qtmodeltester, fake_web_tab, tabbed_browser_stubs,
info):
tabbed_browser_stubs[0].widget.tabs = [
tabbed_browser_stubs[0].tabs = [
fake_web_tab(QUrl('https://github.com'), 'GitHub', 0),
fake_web_tab(QUrl('https://wikipedia.org'), 'Wikipedia', 1),
fake_web_tab(QUrl('https://duckduckgo.com'), 'DuckDuckGo', 2)
]
tabbed_browser_stubs[1].widget.tabs = [
tabbed_browser_stubs[1].tabs = [
fake_web_tab(QUrl('https://wiki.archlinux.org'), 'ArchWiki', 0)
]

View File

@@ -371,8 +371,7 @@ class TestEdit:
"""Tests for :config-edit."""
pytestmark = pytest.mark.usefixtures('config_tmpdir', 'data_tmpdir',
'config_stub', 'key_config_stub',
'qapp')
'config_stub', 'key_config_stub')
def test_no_source(self, commands, mocker):
mock = mocker.patch('qutebrowser.config.configcommands.editor.'

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