Merge remote-tracking branch 'upstream/master' into filter-dict-names

Merging to investigate failed tests that seem unrelated to the PR.
This commit is contained in:
Michal Siedlaczek 2018-04-21 13:29:18 -04:00
commit c94ea5f8d4
110 changed files with 1691 additions and 957 deletions

View File

@ -44,7 +44,7 @@ ignore =
min-version = 3.4.0 min-version = 3.4.0
max-complexity = 12 max-complexity = 12
per-file-ignores = per-file-ignores =
/tests/*/test_*.py : D100,D101,D401 /tests/**/test_*.py : D100,D101,D401
/tests/unit/browser/test_history.py : N806 /tests/unit/browser/test_history.py : N806
/tests/helpers/fixtures.py : N806 /tests/helpers/fixtures.py : N806
/tests/unit/browser/webkit/http/test_content_disposition.py : D400 /tests/unit/browser/webkit/http/test_content_disposition.py : D400

View File

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

View File

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

View File

@ -18,13 +18,70 @@ breaking changes (such as renamed commands) can happen in minor releases.
v1.3.0 (unreleased) v1.3.0 (unreleased)
------------------- -------------------
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 `getbib` userscript to download bibtex information for DOIs on a page.
Changed 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. - `: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.
v1.2.1 (unreleased)
------------------- 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.
v1.2.1
------
Fixed Fixed
~~~~~ ~~~~~
@ -36,6 +93,15 @@ Fixed
- With "tox -e mkvenv-pypi", PyQt 5.10.0 is used again instead of Qt 5.10.1, - With "tox -e mkvenv-pypi", PyQt 5.10.0 is used again instead of Qt 5.10.1,
because of an issue with Qt 5.10.1 which causes qutebrowser to fail to start because of an issue with Qt 5.10.1 which causes qutebrowser to fail to start
("Could not find QtWebEngineProcess"). ("Could not find QtWebEngineProcess").
- Unbinding keys which were bound in older qutebrowser versions now doesn't
crash anymore.
- Fixed a crash when reloading a page which wasn't fully loaded with v1.2.0
- Keys on the numeric keypad now fall back to the same bindings without `Num+`
if no `Num+` binding was found.
- Fixed hinting on some pages with Qt < 5.10.
- Titles are now displayed correctly again for tabs which are cloned or loaded
from sessions.
- Shortcuts now correctly use `Ctrl` instead of `Command` on macOS again.
v1.2.0 v1.2.0
------ ------

View File

@ -670,10 +670,11 @@ qutebrowser release
~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~
* Make sure there are no unstaged changes and the tests are green. * 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. * Run `x=... y=...` to set the respective shell variables.
* Adjust `__version_info__` in `qutebrowser/__init__.py`.
* Update changelog (remove *(unreleased)*). * Update changelog (remove *(unreleased)*).
* Adjust `__version_info__` in `qutebrowser/__init__.py`.
* Commit. * Commit.
* Create annotated git tag (`git tag -s "v1.$x.$y" -m "Release v1.$x.$y"`). * Create annotated git tag (`git tag -s "v1.$x.$y" -m "Release v1.$x.$y"`).
@ -683,9 +684,11 @@ qutebrowser release
* Mark the milestone at https://github.com/qutebrowser/qutebrowser/milestones * Mark the milestone at https://github.com/qutebrowser/qutebrowser/milestones
as closed. as closed.
* Linux: Run `git checkout v1.$x.$y && python3 scripts/dev/build_release.py --upload v1.$x.$y`. * Linux: Run `git checkout v1.$x.$y && ./.venv/bin/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). * 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). * 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). * 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`
* Update `qutebrowser-git` PKGBUILD if dependencies/install changed. * Update `qutebrowser-git` PKGBUILD if dependencies/install changed.
* Announce to qutebrowser and qutebrowser-announce mailinglist. * Announce to qutebrowser and qutebrowser-announce mailinglist.

View File

@ -213,6 +213,37 @@ Why takes it longer to open an URL in qutebrowser than in chromium?::
to use webengine as backend in line 17 and change it to your to use webengine as backend in line 17 and change it to your
needs. 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 == Troubleshooting
Unable to view flash content.:: Unable to view flash content.::

View File

@ -93,6 +93,7 @@ It is possible to run or bind multiple commands by separating them with `;;`.
|<<scroll,scroll>>|Scroll the current tab in the given direction. |<<scroll,scroll>>|Scroll the current tab in the given direction.
|<<scroll-page,scroll-page>>|Scroll the frame page-wise. |<<scroll-page,scroll-page>>|Scroll the frame page-wise.
|<<scroll-px,scroll-px>>|Scroll the current tab by 'count * dx/dy' pixels. |<<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. |<<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,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. |<<search-next,search-next>>|Continue the search to the ([count]th) next term.
@ -1024,6 +1025,15 @@ Scroll the current tab by 'count * dx/dy' pixels.
==== count ==== count
multiplier 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]]
=== scroll-to-perc === scroll-to-perc
Syntax: +:scroll-to-perc [*--horizontal*] ['perc']+ Syntax: +:scroll-to-perc [*--horizontal*] ['perc']+

View File

@ -242,10 +242,10 @@ To suppress loading of any default keybindings, you can set
Loading `autoconfig.yml` Loading `autoconfig.yml`
~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~
By default, all customization done via `:set`, `:bind` and `:unbind` is All customization done via the UI (`:set`, `:bind` and `:unbind`) is
temporary as soon as a `config.py` exists. The settings done that way are always stored in the `autoconfig.yml` file, which is not loaded automatically as soon
saved in the `autoconfig.yml` file, but you'll need to explicitly load it in as a `config.py` exists. If you want those settings to be loaded, you'll need to
your `config.py` by doing: explicitly load the `autoconfig.yml` file in your `config.py` by doing:
.config.py: .config.py:
[source,python] [source,python]

View File

@ -236,10 +236,11 @@
|<<tabs.close_mouse_button,tabs.close_mouse_button>>|Mouse button with which to close tabs. |<<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.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.scale,tabs.favicons.scale>>|Scaling factor for favicons in the tab bar.
|<<tabs.favicons.show,tabs.favicons.show>>|Show favicons in the tab bar. |<<tabs.favicons.show,tabs.favicons.show>>|When to show favicons in the tab bar.
|<<tabs.indicator.padding,tabs.indicator.padding>>|Padding (in pixels) for tab indicators. |<<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.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.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.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.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. |<<tabs.new_position.related,tabs.new_position.related>>|Position of new tabs opened from another tab.
@ -259,10 +260,11 @@
|<<url.auto_search,url.auto_search>>|What search to start when something else than a URL is entered. |<<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.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.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.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.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`. |<<url.yank_ignored_parameters,url.yank_ignored_parameters>>|URL parameters to strip with `:yank url`.
|<<window.hide_wayland_decoration,window.hide_wayland_decoration>>|Hide the window decoration when using wayland. |<<window.hide_decoration,window.hide_decoration>>|Hide the window decoration.
|<<window.title_format,window.title_format>>|Format to use for the window title. The same placeholders like for |<<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.default,zoom.default>>|Default zoom level.
|<<zoom.levels,zoom.levels>>|Available zoom levels. |<<zoom.levels,zoom.levels>>|Available zoom levels.
@ -1653,11 +1655,7 @@ Type: <<types,List of Url>>
Default: Default:
- +pass:[https://www.malwaredomainlist.com/hostslist/hosts.txt]+ - +pass:[https://raw.githubusercontent.com/StevenBlack/hosts/master/hosts]+
- +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]]
=== content.host_blocking.whitelist === content.host_blocking.whitelist
@ -2823,11 +2821,17 @@ Default: +pass:[1.0]+
[[tabs.favicons.show]] [[tabs.favicons.show]]
=== tabs.favicons.show === tabs.favicons.show
Show favicons in the tab bar. When to show favicons in the tab bar.
Type: <<types,Bool>> Type: <<types,String>>
Default: +pass:[true]+ Valid values:
* +always+: Always show favicons.
* +never+: Always hide favicons.
* +pinned+: Show favicons only on pinned tabs.
Default: +pass:[always]+
[[tabs.indicator.padding]] [[tabs.indicator.padding]]
=== tabs.indicator.padding === tabs.indicator.padding
@ -2866,6 +2870,16 @@ Valid values:
Default: +pass:[ignore]+ 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]]
=== tabs.mode_on_change === tabs.mode_on_change
When switching tabs, what input mode is applied. When switching tabs, what input mode is applied.
@ -3102,6 +3116,14 @@ Default:
- +pass:[path]+ - +pass:[path]+
- +pass:[query]+ - +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]]
=== url.searchengines === url.searchengines
Search engines which can be used via the address bar. Search engines which can be used via the address bar.
@ -3137,10 +3159,12 @@ Default:
- +pass:[utm_term]+ - +pass:[utm_term]+
- +pass:[utm_content]+ - +pass:[utm_content]+
[[window.hide_wayland_decoration]] [[window.hide_decoration]]
=== window.hide_wayland_decoration === window.hide_decoration
Hide the window decoration when using wayland. Hide the window decoration.
This setting requires a restart.
This setting requires a restart on Wayland.
Type: <<types,Bool>> Type: <<types,Bool>>
@ -3273,7 +3297,7 @@ See the setting's valid values for more information on allowed values.
|TextAlignment|Alignment of text. |TextAlignment|Alignment of text.
|TimestampTemplate|An strftime-like template for timestamps. |TimestampTemplate|An strftime-like template for timestamps.
See https://docs.python.org/3/library/datetime.html#strftime-strptime-behavior for reference. See https://sqlite.org/lang_datefunc.html for reference.
|UniqueCharString|A string which may not contain duplicate chars. |UniqueCharString|A string which may not contain duplicate chars.
|Url|A URL as a string. |Url|A URL as a string.
|VerticalPosition|The position of the download bar. |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,17 +47,26 @@ Debian Stretch / Ubuntu 17.04 and 17.10
Those versions come with QtWebEngine in the repositories. This makes it possible Those versions come with QtWebEngine in the repositories. This makes it possible
to install qutebrowser via the Debian package. to install qutebrowser via the Debian package.
Download the https://packages.debian.org/sid/all/qutebrowser/download[qutebrowser] and You'll need to download three packages:
https://packages.debian.org/sid/all/python3-pypeg2/download[PyPEG2]
package from the Debian repositories.
Install the packages: - 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:
---- ----
# apt install ./python3-pypeg2_*_all.deb # apt install ./python3-pypeg2_*_all.deb
# apt install ./qutebrowser_*_all.deb # apt install ./qutebrowser*.deb
---- ----
For an update after the initial install, you only need to download/install the
qutebrowser package.
Debian Testing / Ubuntu 18.04 Debian Testing / Ubuntu 18.04
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@ -417,7 +426,11 @@ Creating a wrapper script
~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~
Running `tox` does not install a system-wide `qutebrowser` script. You can 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 You can create a simple wrapper script to start qutebrowser somewhere in your
`$PATH` (e.g. `/usr/local/bin/qutebrowser` or `~/bin/qutebrowser`): `$PATH` (e.g. `/usr/local/bin/qutebrowser` or `~/bin/qutebrowser`):

Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 KiB

After

Width:  |  Height:  |  Size: 113 KiB

View File

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

View File

@ -33,7 +33,7 @@
inkscape:pageopacity="0.0" inkscape:pageopacity="0.0"
inkscape:pageshadow="2" inkscape:pageshadow="2"
inkscape:zoom="1.7536248" inkscape:zoom="1.7536248"
inkscape:cx="376.55567" inkscape:cx="430.72917"
inkscape:cy="268.64059" inkscape:cy="268.64059"
inkscape:document-units="px" inkscape:document-units="px"
inkscape:current-layer="layer1" 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" 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 id="flowPara5701-9-2"><flowSpan
style="font-weight:bold;font-family:sans-serif;-inkscape-font-specification:'Sans Bold';fill:#ff0000;stroke-width:1.06666672" style="font-weight:bold;font-family:sans-serif;-inkscape-font-specification:'Sans Bold';fill:#ff0000;stroke-width:1.06666672"
id="flowSpan5705-5-1">(10)</flowSpan> toggling settings:</flowPara><flowPara id="flowSpan5705-5-1">(12)</flowSpan> toggling settings:</flowPara><flowPara
style="font-size:10.66666698px;line-height:1.25;font-family:sans-serif;fill:#000000;stroke-width:1.06666672" 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 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" 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/img', 'img'),
('../qutebrowser/javascript', 'javascript'), ('../qutebrowser/javascript', 'javascript'),
('../qutebrowser/html/doc', 'html/doc'), ('../qutebrowser/html/doc', 'html/doc'),
('../qutebrowser/git-commit-id', ''), ('../qutebrowser/git-commit-id', '.'),
('../qutebrowser/config/configdata.yml', 'config'), ('../qutebrowser/config/configdata.yml', 'config'),
] ]
@ -58,14 +58,14 @@ exe = EXE(pyz,
icon=icon, icon=icon,
debug=False, debug=False,
strip=False, strip=False,
upx=True, upx=False,
console=False ) console=False )
coll = COLLECT(exe, coll = COLLECT(exe,
a.binaries, a.binaries,
a.zipfiles, a.zipfiles,
a.datas, a.datas,
strip=False, strip=False,
upx=True, upx=False,
name='qutebrowser') name='qutebrowser')
app = BUNDLE(coll, app = BUNDLE(coll,

View File

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

View File

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

View File

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

View File

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

View File

@ -3,16 +3,16 @@
-e git+https://github.com/PyCQA/astroid.git#egg=astroid -e git+https://github.com/PyCQA/astroid.git#egg=astroid
certifi==2018.1.18 certifi==2018.1.18
chardet==3.0.4 chardet==3.0.4
github3.py==0.9.6 github3.py==1.0.2
idna==2.6 idna==2.6
isort==4.3.4 isort==4.3.4
lazy-object-proxy==1.3.1 lazy-object-proxy==1.3.1
mccabe==0.6.1 mccabe==0.6.1
-e git+https://github.com/PyCQA/pylint.git#egg=pylint -e git+https://github.com/PyCQA/pylint.git#egg=pylint
python-dateutil==2.7.2
./scripts/dev/pylint_checkers ./scripts/dev/pylint_checkers
requests==2.18.4 requests==2.18.4
six==1.11.0 six==1.11.0
uritemplate==3.0.0 uritemplate==3.0.0
uritemplate.py==3.0.2
urllib3==1.22 urllib3==1.22
wrapt==1.10.11 wrapt==1.10.11

View File

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

View File

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

View File

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

View File

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

69
misc/userscripts/getbib Executable file
View File

@ -0,0 +1,69 @@
#!/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

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

View File

@ -26,7 +26,7 @@ __copyright__ = "Copyright 2014-2018 Florian Bruhin (The Compiler)"
__license__ = "GPL" __license__ = "GPL"
__maintainer__ = __author__ __maintainer__ = __author__
__email__ = "mail@qutebrowser.org" __email__ = "mail@qutebrowser.org"
__version_info__ = (1, 2, 0) __version_info__ = (1, 2, 1)
__version__ = '.'.join(str(e) for e in __version_info__) __version__ = '.'.join(str(e) for e in __version_info__)
__description__ = "A keyboard-driven, vim-like browser based on PyQt5." __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 for cur_win_id in list(window_ids): # Copying as the dict could change
tabbed_browser = objreg.get('tabbed-browser', scope='window', tabbed_browser = objreg.get('tabbed-browser', scope='window',
window=cur_win_id) window=cur_win_id)
if tabbed_browser.count() == 0: if tabbed_browser.widget.count() == 0:
log.init.debug("Opening start pages") log.init.debug("Opening start pages")
for url in config.val.url.start_pages: for url in config.val.url.start_pages:
tabbed_browser.tabopen(url) tabbed_browser.tabopen(url)

View File

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

View File

@ -114,6 +114,10 @@ class TabData:
netrc_used = attr.ib(False) netrc_used = attr.ib(False)
input_mode = attr.ib(usertypes.KeyMode.normal) 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: class AbstractAction:
@ -333,7 +337,14 @@ class AbstractZoom(QObject):
class AbstractCaret(QObject): class AbstractCaret(QObject):
"""Attribute of AbstractTab for caret browsing.""" """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)
def __init__(self, tab, mode_manager, parent=None): def __init__(self, tab, mode_manager, parent=None):
super().__init__(parent) super().__init__(parent)
@ -439,6 +450,9 @@ class AbstractScroller(QObject):
def to_point(self, point): def to_point(self, point):
raise NotImplementedError raise NotImplementedError
def to_anchor(self, name):
raise NotImplementedError
def delta(self, x=0, y=0): def delta(self, x=0, y=0):
raise NotImplementedError raise NotImplementedError
@ -665,8 +679,7 @@ class AbstractTab(QWidget):
objreg.register('hintmanager', hintmanager, scope='tab', objreg.register('hintmanager', hintmanager, scope='tab',
window=self.win_id, tab=self.tab_id) window=self.win_id, tab=self.tab_id)
self.predicted_navigation.connect( self.predicted_navigation.connect(self._on_predicted_navigation)
lambda url: self.title_changed.emit(url.toDisplayString()))
def _set_widget(self, widget): def _set_widget(self, widget):
# pylint: disable=protected-access # pylint: disable=protected-access
@ -715,6 +728,14 @@ class AbstractTab(QWidget):
evt.posted = True evt.posted = True
QApplication.postEvent(recipient, evt) 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) @pyqtSlot(QUrl)
def _on_url_changed(self, url): def _on_url_changed(self, url):
"""Update title when URL has changed and no title is available.""" """Update title when URL has changed and no title is available."""
@ -815,11 +836,12 @@ class AbstractTab(QWidget):
def load_status(self): def load_status(self):
return self._load_status return self._load_status
def _openurl_prepare(self, url): def _openurl_prepare(self, url, *, predict=True):
qtutils.ensure_valid(url) qtutils.ensure_valid(url)
self.predicted_navigation.emit(url) if predict:
self.predicted_navigation.emit(url)
def openurl(self, url): def openurl(self, url, *, predict=True):
raise NotImplementedError raise NotImplementedError
def reload(self, *, force=False): def reload(self, *, force=False):

View File

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

View File

@ -30,7 +30,8 @@ import textwrap
import attr import attr
from PyQt5.QtCore import pyqtSignal, QObject, QUrl from PyQt5.QtCore import pyqtSignal, QObject, QUrl
from qutebrowser.utils import log, standarddir, jinja, objreg, utils from qutebrowser.utils import (log, standarddir, jinja, objreg, utils,
javascript)
from qutebrowser.commands import cmdutils from qutebrowser.commands import cmdutils
from qutebrowser.browser import downloads from qutebrowser.browser import downloads
@ -91,7 +92,7 @@ class GreasemonkeyScript:
props = "" props = ""
script = cls(re.findall(cls.PROPS_REGEX, props), source) script = cls(re.findall(cls.PROPS_REGEX, props), source)
script.script_meta = props script.script_meta = props
if not props: if not script.includes:
script.includes = ['*'] script.includes = ['*']
return script return script
@ -104,12 +105,13 @@ class GreasemonkeyScript:
browser's debugger/inspector will not match up to the line browser's debugger/inspector will not match up to the line
numbers in the source script directly. numbers in the source script directly.
""" """
return jinja.js_environment.get_template( template = jinja.js_environment.get_template('greasemonkey_wrapper.js')
'greasemonkey_wrapper.js').render( return template.render(
scriptName="/".join([self.namespace or '', self.name]), scriptName=javascript.string_escape(
scriptInfo=self._meta_json(), "/".join([self.namespace or '', self.name])),
scriptMeta=self.script_meta, scriptInfo=self._meta_json(),
scriptSource=self._code) scriptMeta=javascript.string_escape(self.script_meta),
scriptSource=self._code)
def _meta_json(self): def _meta_json(self):
return json.dumps({ return json.dumps({

View File

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

View File

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

View File

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

View File

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

View File

@ -26,16 +26,12 @@ Module attributes:
import os import os
import sip
from PyQt5.QtGui import QFont 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.browser.webengine import spell
from qutebrowser.config import config, websettings from qutebrowser.config import config, websettings
from qutebrowser.utils import (utils, standarddir, javascript, qtutils, from qutebrowser.utils import utils, standarddir, qtutils, message, log
message, log, objreg)
# The default QWebEngineProfile # The default QWebEngineProfile
default_profile = None default_profile = None
@ -169,133 +165,92 @@ class WebEngineSettings(websettings.AbstractSettings):
self._ATTRIBUTES[name] = [value] self._ATTRIBUTES[name] = [value]
def _init_stylesheet(profile): class ProfileSetter:
"""Initialize custom stylesheets.
Partially inspired by QupZilla: """Helper to set various settings on a profile."""
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)
css = shared.get_user_stylesheet() def __init__(self, profile):
source = '\n'.join([ self._profile = profile
'"use strict";',
'window._qutebrowser = window._qutebrowser || {};',
utils.read_file('javascript/stylesheet.js'),
javascript.assemble('stylesheet', 'set_css', css),
])
script = QWebEngineScript() def init_profile(self):
script.setName('_qute_stylesheet') """Initialize settings on the given profile."""
script.setInjectionPoint(QWebEngineScript.DocumentCreation) self.set_http_headers()
script.setWorldId(QWebEngineScript.ApplicationWorld) self.set_http_cache_size()
script.setRunsOnSubFrames(True) self._profile.settings().setAttribute(
script.setSourceCode(source) QWebEngineSettings.FullScreenSupportEnabled, True)
profile.scripts().insert(script) if qtutils.version_check('5.8'):
self._profile.setSpellCheckEnabled(True)
self.set_dictionary_language()
def set_http_headers(self):
"""Set the user agent and accept-language for the given profile.
def _update_stylesheet(): We override those per request in the URL interceptor (to allow for
"""Update the custom stylesheet in existing tabs.""" per-domain values), but this one still gets used for things like
css = shared.get_user_stylesheet() window.navigator.userAgent/.languages in JS.
code = javascript.assemble('stylesheet', 'set_css', css) """
for win_id, window in objreg.window_registry.items(): self._profile.setHttpUserAgent(config.val.content.headers.user_agent)
# We could be in the middle of destroying a window here accept_language = config.val.content.headers.accept_language
if sip.isdeleted(window): if accept_language is not None:
continue self._profile.setHttpAcceptLanguage(accept_language)
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)
def _set_http_headers(profile): # 0: automatically managed by QtWebEngine
"""Set the user agent and accept-language for the given profile. self._profile.setHttpCacheMaximumSize(size)
We override those per request in the URL interceptor (to allow for def set_persistent_cookie_policy(self):
per-domain values), but this one still gets used for things like """Set the HTTP Cookie size for the given profile."""
window.navigator.userAgent/.languages in JS. assert not self._profile.isOffTheRecord()
""" if config.val.content.cookies.store:
profile.setHttpUserAgent(config.val.content.headers.user_agent) value = QWebEngineProfile.AllowPersistentCookies
accept_language = config.val.content.headers.accept_language else:
if accept_language is not None: value = QWebEngineProfile.NoPersistentCookies
profile.setHttpAcceptLanguage(accept_language) self._profile.setPersistentCookiesPolicy(value)
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
def _set_http_cache_size(profile): filenames.append(local_filename)
"""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 log.config.debug("Found dicts: {}".format(filenames))
profile.setHttpCacheMaximumSize(size) self._profile.setSpellCheckLanguages(filenames)
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): def _update_settings(option):
"""Update global settings when qwebsettings changed.""" """Update global settings when qwebsettings changed."""
global_settings.update_setting(option) global_settings.update_setting(option)
if option in ['scrolling.bar', 'content.user_stylesheets']: if option in ['content.headers.user_agent',
_init_stylesheet(default_profile) 'content.headers.accept_language']:
_init_stylesheet(private_profile) default_profile.setter.set_http_headers()
_update_stylesheet() private_profile.setter.set_http_headers()
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': elif option == 'content.cache.size':
_set_http_cache_size(default_profile) default_profile.setter.set_http_cache_size()
_set_http_cache_size(private_profile) private_profile.setter.set_http_cache_size()
elif (option == 'content.cookies.store' and elif (option == 'content.cookies.store' and
# https://bugreports.qt.io/browse/QTBUG-58650 # https://bugreports.qt.io/browse/QTBUG-58650
qtutils.version_check('5.9', compiled=False)): qtutils.version_check('5.9', compiled=False)):
_set_persistent_cookie_policy(default_profile) default_profile.setter.set_persistent_cookie_policy()
# We're not touching the private profile's cookie policy. # We're not touching the private profile's cookie policy.
elif option == 'spellcheck.languages': elif option == 'spellcheck.languages':
_set_dictionary_language(default_profile) default_profile.setter.set_dictionary_language()
_set_dictionary_language(private_profile, warn=False) private_profile.setter.set_dictionary_language(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(): def _init_profiles():
@ -303,53 +258,18 @@ def _init_profiles():
global default_profile, private_profile global default_profile, private_profile
default_profile = QWebEngineProfile.defaultProfile() default_profile = QWebEngineProfile.defaultProfile()
default_profile.setter = ProfileSetter(default_profile)
default_profile.setCachePath( default_profile.setCachePath(
os.path.join(standarddir.cache(), 'webengine')) os.path.join(standarddir.cache(), 'webengine'))
default_profile.setPersistentStoragePath( default_profile.setPersistentStoragePath(
os.path.join(standarddir.data(), 'webengine')) os.path.join(standarddir.data(), 'webengine'))
_init_profile(default_profile) default_profile.setter.init_profile()
_set_persistent_cookie_policy(default_profile) default_profile.setter.set_persistent_cookie_policy()
private_profile = QWebEngineProfile() private_profile = QWebEngineProfile()
private_profile.setter = ProfileSetter(private_profile)
assert private_profile.isOffTheRecord() assert private_profile.isOffTheRecord()
_init_profile(private_profile) private_profile.setter.init_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): def init(args):

View File

@ -33,7 +33,7 @@ from PyQt5.QtNetwork import QAuthenticator
from PyQt5.QtWidgets import QApplication from PyQt5.QtWidgets import QApplication
from PyQt5.QtWebEngineWidgets import QWebEnginePage, QWebEngineScript from PyQt5.QtWebEngineWidgets import QWebEnginePage, QWebEngineScript
from qutebrowser.config import configdata from qutebrowser.config import configdata, config
from qutebrowser.browser import browsertab, mouse, shared from qutebrowser.browser import browsertab, mouse, shared
from qutebrowser.browser.webengine import (webview, webengineelem, tabhistory, from qutebrowser.browser.webengine import (webview, webengineelem, tabhistory,
interceptor, webenginequtescheme, interceptor, webenginequtescheme,
@ -73,10 +73,6 @@ def init():
download_manager.install(webenginesettings.private_profile) download_manager.install(webenginesettings.private_profile)
objreg.register('webengine-download-manager', download_manager) 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. # Mapping worlds from usertypes.JsWorld to QWebEngineScript world IDs.
_JS_WORLD_MAP = { _JS_WORLD_MAP = {
@ -234,7 +230,14 @@ class WebEngineCaret(browsertab.AbstractCaret):
self._tab.run_js_async( self._tab.run_js_async(
javascript.assemble('caret', 'setPlatform', sys.platform)) javascript.assemble('caret', 'setPlatform', sys.platform))
self._js_call('setInitialCursor') 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)
@pyqtSlot(usertypes.KeyMode) @pyqtSlot(usertypes.KeyMode)
def _on_mode_left(self, mode): def _on_mode_left(self, mode):
@ -301,7 +304,7 @@ class WebEngineCaret(browsertab.AbstractCaret):
self._js_call('moveToEndOfDocument') self._js_call('moveToEndOfDocument')
def toggle_selection(self): def toggle_selection(self):
self._js_call('toggleSelection') self._js_call('toggleSelection', self.selection_toggled.emit)
def drop_selection(self): def drop_selection(self):
self._js_call('dropSelection') self._js_call('dropSelection')
@ -356,9 +359,8 @@ class WebEngineCaret(browsertab.AbstractCaret):
self._tab.run_js_async(js_code, lambda jsret: self._tab.run_js_async(js_code, lambda jsret:
self._follow_selected_cb(jsret, tab)) self._follow_selected_cb(jsret, tab))
def _js_call(self, command): def _js_call(self, command, callback=None):
self._tab.run_js_async( self._tab.run_js_async(javascript.assemble('caret', command), callback)
javascript.assemble('caret', command))
class WebEngineScroller(browsertab.AbstractScroller): class WebEngineScroller(browsertab.AbstractScroller):
@ -379,7 +381,7 @@ class WebEngineScroller(browsertab.AbstractScroller):
def _repeated_key_press(self, key, count=1, modifier=Qt.NoModifier): def _repeated_key_press(self, key, count=1, modifier=Qt.NoModifier):
"""Send count fake key presses to this scroller's WebEngineTab.""" """Send count fake key presses to this scroller's WebEngineTab."""
for _ in range(min(count, 5000)): for _ in range(min(count, 1000)):
self._tab.key_press(key, modifier) self._tab.key_press(key, modifier)
@pyqtSlot(QPointF) @pyqtSlot(QPointF)
@ -432,6 +434,11 @@ class WebEngineScroller(browsertab.AbstractScroller):
js_code = javascript.assemble('window', 'scroll', point.x(), point.y()) js_code = javascript.assemble('window', 'scroll', point.x(), point.y())
self._tab.run_js_async(js_code) 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): def delta(self, x=0, y=0):
self._tab.run_js_async(javascript.assemble('window', 'scrollBy', x, y)) self._tab.run_js_async(javascript.assemble('window', 'scrollBy', x, y))
@ -506,6 +513,9 @@ class WebEngineHistory(browsertab.AbstractHistory):
return qtutils.deserialize(data, self._history) return qtutils.deserialize(data, self._history)
def load_items(self, items): def load_items(self, items):
if items:
self._tab.predicted_navigation.emit(items[-1].url)
stream, _data, cur_data = tabhistory.serialize(items) stream, _data, cur_data = tabhistory.serialize(items)
qtutils.deserialize_stream(stream, self._history) qtutils.deserialize_stream(stream, self._history)
@ -627,30 +637,122 @@ class WebEngineTab(browsertab.AbstractTab):
self._set_widget(widget) self._set_widget(widget)
self._connect_signals() self._connect_signals()
self.backend = usertypes.Backend.QtWebEngine self.backend = usertypes.Backend.QtWebEngine
self._init_js()
self._child_event_filter = None self._child_event_filter = None
self._saved_zoom = None self._saved_zoom = None
self._reload_url = 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): def _init_js(self):
js_code = '\n'.join([ """Initialize global qutebrowser JavaScript."""
'"use strict";', js_code = javascript.wrap_global(
'window._qutebrowser = window._qutebrowser || {};', 'scripts',
utils.read_file('javascript/scroll.js'), utils.read_file('javascript/scroll.js'),
utils.read_file('javascript/webelem.js'), utils.read_file('javascript/webelem.js'),
utils.read_file('javascript/caret.js'), utils.read_file('javascript/caret.js'),
]) )
script = QWebEngineScript() # FIXME:qtwebengine what about subframes=True?
# We can't use DocumentCreation here as WORKAROUND for self._inject_early_js('js', js_code, subframes=True)
# https://bugreports.qt.io/browse/QTBUG-66011 self._init_stylesheet()
script.setInjectionPoint(QWebEngineScript.DocumentReady)
script.setSourceCode(js_code)
page = self._widget.page() greasemonkey = objreg.get('greasemonkey')
script.setWorldId(QWebEngineScript.ApplicationWorld) greasemonkey.scripts_reloaded.connect(self._inject_userscripts)
self._inject_userscripts()
# FIXME:qtwebengine what about runsOnSubFrames? def _init_stylesheet(self):
page.scripts().insert(script) """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)
def _install_event_filter(self): def _install_event_filter(self):
self._widget.focusProxy().installEventFilter(self._mouse_event_filter) self._widget.focusProxy().installEventFilter(self._mouse_event_filter)
@ -669,9 +771,15 @@ class WebEngineTab(browsertab.AbstractTab):
self.zoom.set_factor(self._saved_zoom) self.zoom.set_factor(self._saved_zoom)
self._saved_zoom = None self._saved_zoom = None
def openurl(self, url): 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.
"""
self._saved_zoom = self.zoom.factor() self._saved_zoom = self.zoom.factor()
self._openurl_prepare(url) self._openurl_prepare(url, predict=predict)
self._widget.load(url) self._widget.load(url)
def url(self, requested=False): def url(self, requested=False):
@ -706,7 +814,6 @@ class WebEngineTab(browsertab.AbstractTab):
self._widget.shutdown() self._widget.shutdown()
def reload(self, *, force=False): def reload(self, *, force=False):
self.predicted_navigation.emit(self.url())
if force: if force:
action = QWebEnginePage.ReloadAndBypassCache action = QWebEnginePage.ReloadAndBypassCache
else: else:
@ -915,10 +1022,10 @@ class WebEngineTab(browsertab.AbstractTab):
if ok and self._reload_url is not None: if ok and self._reload_url is not None:
# WORKAROUND for https://bugreports.qt.io/browse/QTBUG-66656 # WORKAROUND for https://bugreports.qt.io/browse/QTBUG-66656
log.config.debug( log.config.debug(
"Reloading {} because of config change".format( "Loading {} again because of config change".format(
self._reload_url.toDisplayString())) self._reload_url.toDisplayString()))
QTimer.singleShot(100, lambda url=self._reload_url: QTimer.singleShot(100, lambda url=self._reload_url:
self.openurl(url)) self.openurl(url, predict=False))
self._reload_url = None self._reload_url = None
if not qtutils.version_check('5.10', compiled=False): if not qtutils.version_check('5.10', compiled=False):
@ -931,6 +1038,7 @@ class WebEngineTab(browsertab.AbstractTab):
@pyqtSlot(QUrl) @pyqtSlot(QUrl)
def _on_predicted_navigation(self, url): def _on_predicted_navigation(self, url):
"""If we know we're going to visit an URL soon, change the settings.""" """If we know we're going to visit an URL soon, change the settings."""
super()._on_predicted_navigation(url)
self.settings.update_for_url(url) self.settings.update_for_url(url)
@pyqtSlot(usertypes.NavigationRequest) @pyqtSlot(usertypes.NavigationRequest)

View File

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

View File

@ -239,7 +239,6 @@ class BrowserPage(QWebPage):
printdiag.setAttribute(Qt.WA_DeleteOnClose) printdiag.setAttribute(Qt.WA_DeleteOnClose)
printdiag.open(lambda: frame.print(printdiag.printer())) printdiag.open(lambda: frame.print(printdiag.printer()))
@pyqtSlot('QNetworkRequest')
def on_download_requested(self, request): def on_download_requested(self, request):
"""Called when the user wants to download a link. """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)) model = completionmodel.CompletionModel(column_widths=(6, 40, 54))
for win_id in objreg.window_registry: for win_id in objreg.window_registry:
if skip_win_id and win_id == skip_win_id: if skip_win_id is not None and win_id == skip_win_id:
continue continue
tabbed_browser = objreg.get('tabbed-browser', scope='window', tabbed_browser = objreg.get('tabbed-browser', scope='window',
window=win_id) window=win_id)
if tabbed_browser.shutting_down: if tabbed_browser.shutting_down:
continue continue
tabs = [] tabs = []
for idx in range(tabbed_browser.count()): for idx in range(tabbed_browser.widget.count()):
tab = tabbed_browser.widget(idx) tab = tabbed_browser.widget.widget(idx)
tabs.append(("{}/{}".format(win_id, idx + 1), tabs.append(("{}/{}".format(win_id, idx + 1),
tab.url().toDisplayString(), tab.url().toDisplayString(),
tabbed_browser.page_title(idx))) tabbed_browser.widget.page_title(idx)))
cat = listcategory.ListCategory("{}".format(win_id), tabs, cat = listcategory.ListCategory("{}".format(win_id), tabs,
delete_func=delete_buffer) delete_func=delete_buffer)
model.add_category(cat) model.add_category(cat)

View File

@ -425,11 +425,7 @@ content.host_blocking.enabled:
content.host_blocking.lists: content.host_blocking.lists:
default: default:
- "https://www.malwaredomainlist.com/hostslist/hosts.txt" - "https://raw.githubusercontent.com/StevenBlack/hosts/master/hosts"
- "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: type:
name: List name: List
valtype: Url valtype: Url
@ -1252,9 +1248,14 @@ tabs.favicons.scale:
`tabs.padding`. `tabs.padding`.
tabs.favicons.show: tabs.favicons.show:
default: true default: always
type: Bool type:
desc: Show favicons in the tab bar. 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.
tabs.last_close: tabs.last_close:
default: ignore default: ignore
@ -1325,7 +1326,10 @@ tabs.show:
tabs.show_switching_delay: tabs.show_switching_delay:
default: 800 default: 800
type: Int type:
name: Int
minval: 0
maxval: maxint
desc: "Duration (in milliseconds) to show the tab bar before hiding it when desc: "Duration (in milliseconds) to show the tab bar before hiding it when
tabs.show is set to 'switching'." tabs.show is set to 'switching'."
@ -1406,6 +1410,19 @@ tabs.width:
desc: "Width (in pixels or as percentage of the window) of the tab bar if desc: "Width (in pixels or as percentage of the window) of the tab bar if
it's vertical." 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: tabs.width.indicator:
renamed: tabs.indicator.width renamed: tabs.indicator.width
@ -1469,6 +1486,11 @@ url.incdec_segments:
desc: URL segments where `:navigate increment/decrement` will search for desc: URL segments where `:navigate increment/decrement` will search for
a number. 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: url.searchengines:
default: default:
DEFAULT: https://duckduckgo.com/?q={} DEFAULT: https://duckduckgo.com/?q={}
@ -1513,10 +1535,15 @@ url.yank_ignored_parameters:
## window ## window
window.hide_wayland_decoration: window.hide_wayland_decoration:
renamed: window.hide_decoration
window.hide_decoration:
type: Bool type: Bool
default: false default: false
restart: true desc: |
desc: Hide the window decoration when using wayland. Hide the window decoration.
This setting requires a restart on Wayland.
window.title_format: window.title_format:
type: type:

View File

@ -268,6 +268,15 @@ class YamlConfig(QObject):
del settings['bindings.default'] del settings['bindings.default']
self._mark_changed() 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 return settings
def _validate(self, settings): def _validate(self, settings):

View File

@ -26,7 +26,8 @@ from PyQt5.QtWidgets import QMessageBox
from qutebrowser.config import (config, configdata, configfiles, configtypes, from qutebrowser.config import (config, configdata, configfiles, configtypes,
configexc, configcommands) configexc, configcommands)
from qutebrowser.utils import objreg, usertypes, log, standarddir, message from qutebrowser.utils import (objreg, usertypes, log, standarddir, message,
qtutils)
from qutebrowser.misc import msgbox, objects from qutebrowser.misc import msgbox, objects
@ -89,7 +90,7 @@ def _init_envvars():
if config.val.qt.force_platform is not None: if config.val.qt.force_platform is not None:
os.environ['QT_QPA_PLATFORM'] = config.val.qt.force_platform os.environ['QT_QPA_PLATFORM'] = config.val.qt.force_platform
if config.val.window.hide_wayland_decoration: if config.val.window.hide_decoration:
os.environ['QT_WAYLAND_DISABLE_WINDOWDECORATION'] = '1' os.environ['QT_WAYLAND_DISABLE_WINDOWDECORATION'] = '1'
if config.val.qt.highdpi: if config.val.qt.highdpi:
@ -161,4 +162,12 @@ def qt_args(namespace):
argv += ['--' + name, value] argv += ['--' + name, value]
argv += ['--' + arg for arg in config.val.qt.args] 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 return argv

View File

@ -451,7 +451,7 @@ class List(BaseType):
def from_obj(self, value): def from_obj(self, value):
if value is None: if value is None:
return [] return []
return value return [self.valtype.from_obj(v) for v in value]
def to_py(self, value): def to_py(self, value):
self._basic_py_validation(value, list) self._basic_py_validation(value, list)
@ -506,6 +506,16 @@ class ListOrValue(BaseType):
self.listtype = List(valtype, none_ok=none_ok, *args, **kwargs) self.listtype = List(valtype, none_ok=none_ok, *args, **kwargs)
self.valtype = valtype 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): def get_name(self):
return self.listtype.get_name() + ', or ' + self.valtype.get_name() return self.listtype.get_name() + ', or ' + self.valtype.get_name()
@ -533,25 +543,15 @@ class ListOrValue(BaseType):
if value is None: if value is None:
return '' return ''
if isinstance(value, list): val, typ = self._val_and_type(value)
if len(value) == 1: return typ.to_str(val)
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): def to_doc(self, value, indent=0):
if value is None: if value is None:
return 'empty' return 'empty'
if isinstance(value, list): val, typ = self._val_and_type(value)
if len(value) == 1: return typ.to_doc(val)
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): class FlagList(List):
@ -1199,7 +1199,9 @@ class Dict(BaseType):
def from_obj(self, value): def from_obj(self, value):
if value is None: if value is None:
return {} return {}
return value
return {self.keytype.from_obj(key): self.valtype.from_obj(val)
for key, val in value.items()}
def _fill_fixed_keys(self, value): def _fill_fixed_keys(self, value):
"""Fill missing fixed keys with a None-value.""" """Fill missing fixed keys with a None-value."""
@ -1623,9 +1625,7 @@ class TimestampTemplate(BaseType):
"""An strftime-like template for timestamps. """An strftime-like template for timestamps.
See See https://sqlite.org/lang_datefunc.html for reference.
https://docs.python.org/3/library/datetime.html#strftime-strptime-behavior
for reference.
""" """
def to_py(self, value): def to_py(self, value):
@ -1648,6 +1648,10 @@ class Key(BaseType):
"""A name of a key.""" """A name of a key."""
def from_obj(self, value):
"""Make sure key sequences are always normalized."""
return str(keyutils.KeySequence.parse(value))
def to_py(self, value): def to_py(self, value):
self._basic_py_validation(value, str) self._basic_py_validation(value, str)
if not value: if not value:

View File

@ -22,7 +22,7 @@
from PyQt5.QtGui import QFont from PyQt5.QtGui import QFont
from qutebrowser.config import config, configutils from qutebrowser.config import config, configutils
from qutebrowser.utils import log, usertypes, urlmatch from qutebrowser.utils import log, usertypes, urlmatch, qtutils
from qutebrowser.misc import objects from qutebrowser.misc import objects
UNSET = object() UNSET = object()
@ -141,6 +141,7 @@ class AbstractSettings:
Return: Return:
A set of settings which actually changed. A set of settings which actually changed.
""" """
qtutils.ensure_valid(url)
changed_settings = set() changed_settings = set()
for values in config.instance: for values in config.instance:
if not values.opt.supports_pattern: if not values.opt.supports_pattern:

View File

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

View File

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

View File

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

View File

@ -74,9 +74,8 @@ window._qutebrowser.webelem = (function() {
try { try {
return elem.selectionStart; return elem.selectionStart;
} catch (err) { } catch (err) {
if (err instanceof (frame if ((err instanceof DOMException ||
? frame.DOMException (frame && err instanceof frame.DOMException)) &&
: DOMException) &&
err.name === "InvalidStateError") { err.name === "InvalidStateError") {
// nothing to do, caret_position is already null // nothing to do, caret_position is already null
} else { } else {

View File

@ -108,11 +108,43 @@ class BaseKeyParser(QObject):
assert not isinstance(seq, str), seq assert not isinstance(seq, str), seq
match = sequence.matches(seq) match = sequence.matches(seq)
if match == QKeySequence.ExactMatch: if match == QKeySequence.ExactMatch:
return (match, cmd) return match, cmd
elif match == QKeySequence.PartialMatch: elif match == QKeySequence.PartialMatch:
result = 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.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 True
return False
def handle(self, e, *, dry_run=False): def handle(self, e, *, dry_run=False):
"""Handle a new keypress. """Handle a new keypress.
@ -146,28 +178,15 @@ class BaseKeyParser(QObject):
self.clear_keystring() self.clear_keystring()
return QKeySequence.NoMatch return QKeySequence.NoMatch
# First, try a straightforward match
match, binding = self._match_key(sequence) match, binding = self._match_key(sequence)
# If that doesn't match, try a key_mapping
if match == QKeySequence.NoMatch: if match == QKeySequence.NoMatch:
mapped = sequence.with_mappings(config.val.bindings.key_mappings) match, binding, sequence = self._match_without_modifiers(sequence)
if sequence != mapped: if match == QKeySequence.NoMatch:
self._debug_log("Mapped {} -> {}".format( match, binding, sequence = self._match_key_mapping(sequence)
sequence, mapped)) if match == QKeySequence.NoMatch:
match, binding = self._match_key(mapped) was_count = self._match_count(sequence, dry_run)
sequence = mapped if was_count:
return QKeySequence.ExactMatch
# If that doesn't match either, try treating it as count.
if (match == QKeySequence.NoMatch and
txt.isdigit() and
self._supports_count and
not (not self._count and txt == '0')):
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: if dry_run:
return match return match

View File

@ -58,7 +58,8 @@ def is_special(key, modifiers):
_assert_plain_key(key) _assert_plain_key(key)
_assert_plain_modifier(modifiers) _assert_plain_modifier(modifiers)
return not (_is_printable(key) and return not (_is_printable(key) and
modifiers in [Qt.ShiftModifier, Qt.NoModifier]) modifiers in [Qt.ShiftModifier, Qt.NoModifier,
Qt.KeypadModifier])
def is_modifier_key(key): def is_modifier_key(key):
@ -303,7 +304,8 @@ class KeyInfo:
key_string = key_string.lower() key_string = key_string.lower()
# "special" binding # "special" binding
assert is_special(self.key, self.modifiers) assert (is_special(self.key, self.modifiers) or
self.modifiers == Qt.KeypadModifier)
modifier_string = _modifiers_to_string(modifiers) modifier_string = _modifiers_to_string(modifiers)
return '<{}{}>'.format(modifier_string, key_string) return '<{}{}>'.format(modifier_string, key_string)
@ -505,11 +507,29 @@ class KeySequence:
not ev.text().isupper()): not ev.text().isupper()):
modifiers = Qt.KeyboardModifiers() modifiers = Qt.KeyboardModifiers()
# On macOS, swap Ctrl and Meta back
# WORKAROUND for https://bugreports.qt.io/browse/QTBUG-51293
if utils.is_mac:
if modifiers & Qt.ControlModifier and modifiers & Qt.MetaModifier:
pass
elif modifiers & Qt.ControlModifier:
modifiers &= ~Qt.ControlModifier
modifiers |= Qt.MetaModifier
elif modifiers & Qt.MetaModifier:
modifiers &= ~Qt.MetaModifier
modifiers |= Qt.ControlModifier
keys = list(self._iter_keys()) keys = list(self._iter_keys())
keys.append(key | int(modifiers)) keys.append(key | int(modifiers))
return self.__class__(*keys) return self.__class__(*keys)
def strip_modifiers(self):
"""Strip optional modifiers from keys."""
modifiers = Qt.KeypadModifier
keys = [key & ~modifiers for key in self._iter_keys()]
return self.__class__(*keys)
def with_mappings(self, mappings): def with_mappings(self, mappings):
"""Get a new KeySequence with the given mappings applied.""" """Get a new KeySequence with the given mappings applied."""
keys = [] keys = []

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -42,6 +42,7 @@ class ExternalEditor(QObject):
_proc: The GUIProcess of the editor. _proc: The GUIProcess of the editor.
_watcher: A QFileSystemWatcher to watch the edited file for changes. _watcher: A QFileSystemWatcher to watch the edited file for changes.
Only set if watch=True. Only set if watch=True.
_content: The last-saved text of the editor.
Signals: Signals:
file_updated: The text in the edited file was updated. file_updated: The text in the edited file was updated.
@ -112,19 +113,7 @@ class ExternalEditor(QObject):
if self._filename is not None: if self._filename is not None:
raise ValueError("Already editing a file!") raise ValueError("Already editing a file!")
try: try:
# Close while the external process is running, as otherwise systems self._filename = self._create_tempfile(text, 'qutebrowser-editor-')
# 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: except OSError as e:
message.error("Failed to create initial file: {}".format(e)) message.error("Failed to create initial file: {}".format(e))
return return
@ -134,6 +123,32 @@ class ExternalEditor(QObject):
line, column = self._calc_line_and_column(text, caret_position) line, column = self._calc_line_and_column(text, caret_position)
self._start_editor(line=line, column=column) 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) @pyqtSlot(str)
def _on_file_changed(self, path): def _on_file_changed(self, path):
try: try:

View File

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

View File

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

View File

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

View File

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

View File

@ -20,6 +20,9 @@
"""Utilities related to javascript interaction.""" """Utilities related to javascript interaction."""
from qutebrowser.utils import jinja
def string_escape(text): def string_escape(text):
"""Escape values special to javascript in strings. """Escape values special to javascript in strings.
@ -70,3 +73,9 @@ def assemble(module, function, *args):
parts = ['window', '_qutebrowser', module, function] parts = ['window', '_qutebrowser', module, function]
code = '"use strict";\n{}({});'.format('.'.join(parts), js_args) code = '"use strict";\n{}({});'.format('.'.join(parts), js_args)
return code 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': if tab_id == 'current':
tabbed_browser = get('tabbed-browser', scope='window', window=win_id) tabbed_browser = get('tabbed-browser', scope='window', window=win_id)
tab = tabbed_browser.currentWidget() tab = tabbed_browser.widget.currentWidget()
if tab is None: if tab is None:
raise RegistryUnavailableError('window') raise RegistryUnavailableError('window')
tab_id = tab.tab_id tab_id = tab.tab_id

View File

@ -102,6 +102,12 @@ def _get_search_url(txt):
engine = 'DEFAULT' engine = 'DEFAULT'
template = config.val.url.searchengines[engine] template = config.val.url.searchengines[engine]
url = qurl_from_user_input(template.format(urllib.parse.quote(term))) 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) qtutils.ensure_valid(url)
return url return url

View File

@ -317,8 +317,10 @@ def _chromium_version():
Qt 5.8: Chromium 53 Qt 5.8: Chromium 53
Qt 5.9: Chromium 56 Qt 5.9: Chromium 56
Qt 5.10: Chromium 61 Qt 5.10: Chromium 61
Qt 5.11: Chromium 63 Qt 5.11: Chromium 65
Qt 5.12: Chromium 65 (?) Qt 5.12: Chromium 69 (?)
Also see https://www.chromium.org/developers/calendar
""" """
if QWebEngineProfile is None: if QWebEngineProfile is None:
# This should never happen # This should never happen

View File

@ -361,7 +361,7 @@ def github_upload(artifacts, tag):
repo = gh.repository('qutebrowser', 'qutebrowser') repo = gh.repository('qutebrowser', 'qutebrowser')
release = None # to satisfy pylint release = None # to satisfy pylint
for release in repo.iter_releases(): for release in repo.releases():
if release.tag_name == tag: if release.tag_name == tag:
break break
else: else:
@ -401,14 +401,6 @@ def main():
run_asciidoc2html(args) run_asciidoc2html(args)
if os.name == 'nt': 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() artifacts = build_windows()
elif sys.platform == 'darwin': elif sys.platform == 'darwin':
artifacts = build_mac() artifacts = build_mac()

View File

@ -0,0 +1,13 @@
<html>
<!-- https://github.com/qutebrowser/qutebrowser/issues/3711 -->
<head>
<title>Issue 3711</title>
</head>
<body>
<!--
Verify no hint error occurs when hinting input range elements in iframes on qt5.9
Possibly an issue in chrome.
-->
<input min="0" max="1" step="0.001" type="range">
</body>
</html>

View File

@ -0,0 +1,11 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Issue 3771 Parent Frame</title>
</head>
<body>
<iframe src="./issue3711.html"></iframe>
</body>
</html>

View File

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

View File

@ -128,6 +128,7 @@ Feature: Opening external editors
And I run :tab-close And I run :tab-close
And I kill the waiting editor And I kill the waiting editor
Then the error "Edited element vanished" should be shown 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 # Could not get signals working on Windows
@posix @posix

View File

@ -249,6 +249,11 @@ Feature: Using hints
And I hint with args "all current" and follow a And I hint with args "all current" and follow a
Then no crash should happen Then no crash should happen
Scenario: No error when hinting ranged input in frames
When I open data/hints/issue3711_frame.html
And I hint with args "all current" and follow a
Then no crash should happen
### hints.auto_follow.timeout ### hints.auto_follow.timeout
@not_mac @flaky @not_mac @flaky

View File

@ -8,6 +8,7 @@ Feature: Javascript stuff
When I open data/javascript/consolelog.html When I open data/javascript/consolelog.html
Then the javascript message "console.log works!" should be logged Then the javascript message "console.log works!" should be logged
@flaky
Scenario: Opening/Closing a window via JS Scenario: Opening/Closing a window via JS
When I open data/javascript/window_open.html When I open data/javascript/window_open.html
And I run :tab-only And I run :tab-only
@ -15,7 +16,10 @@ Feature: Javascript stuff
And I wait for "Changing title for idx 1 to 'about:blank'" in the log And I wait for "Changing title for idx 1 to 'about:blank'" in the log
And I run :tab-focus 1 And I run :tab-focus 1
And I run :click-element id close-normal And I run :click-element id close-normal
And I wait for "[*] window closed" in the log
Then "Focus object changed: *" should be logged Then "Focus object changed: *" should be logged
And the following tabs should be open:
- data/javascript/window_open.html (active)
@qtwebkit_skip @qtwebkit_skip
Scenario: Opening/closing a modal window via JS Scenario: Opening/closing a modal window via JS
@ -25,8 +29,11 @@ Feature: Javascript stuff
And I wait for "Changing title for idx 1 to 'about:blank'" in the log And I wait for "Changing title for idx 1 to 'about:blank'" in the log
And I run :tab-focus 1 And I run :tab-focus 1
And I run :click-element id close-normal And I run :click-element id close-normal
And I wait for "[*] window closed" in the log
Then "Focus object changed: *" should be logged Then "Focus object changed: *" should be logged
And "Web*Dialog requested, but we don't support that!" 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 # https://github.com/qutebrowser/qutebrowser/issues/906
@ -39,6 +46,7 @@ Feature: Javascript stuff
And I wait for "Changing title for idx 2 to 'about:blank'" in the log And I wait for "Changing title for idx 2 to 'about:blank'" in the log
And I run :tab-focus 2 And I run :tab-focus 2
And I run :click-element id close-twice 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 Then "Requested to close * which does not exist!" should be logged
@qtwebkit_skip @flaky @qtwebkit_skip @flaky
@ -51,6 +59,7 @@ Feature: Javascript stuff
And I run :buffer window_open.html And I run :buffer window_open.html
And I run :click-element id close-twice And I run :click-element id close-twice
And I wait for "Focus object changed: *" in the log And I wait for "Focus object changed: *" in the log
And I wait for "[*] window closed" in the log
Then no crash should happen Then no crash should happen
@flaky @flaky
@ -174,3 +183,15 @@ Feature: Javascript stuff
When I set content.javascript.enabled to false When I set content.javascript.enabled to false
And I open 500 without waiting And I open 500 without waiting
Then "Showing error page for* 500" should be logged 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 # prev/next
Scenario: Navigating to previous page Scenario: Navigating to previous page
When I open data/navigate When I open data/navigate in a new tab
And I run :navigate prev And I run :navigate prev
Then data/navigate/prev.html should be loaded Then data/navigate/prev.html should be loaded

View File

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

View File

@ -101,6 +101,9 @@ def is_ignored_lowlevel_message(message):
' Error: No such file or directory', ' Error: No such file or directory',
# Qt 5.7.1 # Qt 5.7.1
'qt.network.ssl: QSslSocket: cannot call unresolved function *', '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) return any(testutils.pattern_match(pattern=pattern, value=message)
for pattern in ignored_messages) for pattern in ignored_messages)
@ -169,7 +172,7 @@ def is_ignored_chromium_message(line):
# /tmp/pytest-of-florian/pytest-32/test_webengine_download_suffix0/ # /tmp/pytest-of-florian/pytest-32/test_webengine_download_suffix0/
# downloads/download.bin: Operation not supported # downloads/download.bin: Operation not supported
('Could not set extended attribute user.xdg.* on file *: ' ('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)] # [5947:5947:0605/192837.856931:ERROR:render_process_impl.cc(112)]
# WebFrame LEAKED 1 TIMES # WebFrame LEAKED 1 TIMES
'WebFrame LEAKED 1 TIMES', 'WebFrame LEAKED 1 TIMES',
@ -192,6 +195,15 @@ def is_ignored_chromium_message(line):
# [2734:2746:1107/131154.072032:ERROR:nss_ocsp.cc(591)] No # [2734:2746:1107/131154.072032:ERROR:nss_ocsp.cc(591)] No
# URLRequestContext for NSS HTTP handler. host: ocsp.digicert.com # URLRequestContext for NSS HTTP handler. host: ocsp.digicert.com
'No URLRequestContext for NSS HTTP handler. host: *', '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) return any(testutils.pattern_match(pattern=pattern, value=message)
for pattern in ignored_messages) for pattern in ignored_messages)

View File

@ -379,6 +379,7 @@ def test_qute_settings_persistence(short_tmpdir, request, quteproc_new):
@pytest.mark.no_xvfb @pytest.mark.no_xvfb
@pytest.mark.no_ci @pytest.mark.no_ci
@pytest.mark.not_mac
def test_force_software_rendering(request, quteproc_new): def test_force_software_rendering(request, quteproc_new):
"""Make sure we can force software rendering with -s.""" """Make sure we can force software rendering with -s."""
if not request.config.webengine: if not request.config.webengine:

View File

@ -43,7 +43,8 @@ import helpers.stubs as stubsmod
import helpers.utils import helpers.utils
from qutebrowser.config import (config, configdata, configtypes, configexc, from qutebrowser.config import (config, configdata, configtypes, configexc,
configfiles) configfiles)
from qutebrowser.utils import objreg, standarddir from qutebrowser.utils import objreg, standarddir, utils
from qutebrowser.browser import greasemonkey
from qutebrowser.browser.webkit import cookies from qutebrowser.browser.webkit import cookies
from qutebrowser.misc import savemanager, sql from qutebrowser.misc import savemanager, sql
from qutebrowser.keyinput import modeman from qutebrowser.keyinput import modeman
@ -143,6 +144,47 @@ def fake_web_tab(stubs, tab_registry, mode_manager, qapp):
return stubs.FakeWebTab 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(): def _generate_cmdline_tests():
"""Generate testcases for test_split_binding.""" """Generate testcases for test_split_binding."""
@attr.s @attr.s
@ -193,11 +235,15 @@ def configdata_init():
@pytest.fixture @pytest.fixture
def config_stub(stubs, monkeypatch, configdata_init, config_tmpdir): def yaml_config_stub(config_tmpdir):
"""Fixture which provides a fake config object.""" """Fixture which provides a YamlConfig object."""
yaml_config = configfiles.YamlConfig() return configfiles.YamlConfig()
conf = config.Config(yaml_config=yaml_config)
@pytest.fixture
def config_stub(stubs, monkeypatch, configdata_init, yaml_config_stub):
"""Fixture which provides a fake config object."""
conf = config.Config(yaml_config=yaml_config_stub)
monkeypatch.setattr(config, 'instance', conf) monkeypatch.setattr(config, 'instance', conf)
container = config.ConfigContainer(conf) container = config.ConfigContainer(conf)

View File

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

View File

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

View File

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

View File

@ -1,110 +0,0 @@
# 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

@ -40,9 +40,7 @@ def test_big_cache_size(config_stub):
"""Make sure a too big cache size is handled correctly.""" """Make sure a too big cache size is handled correctly."""
config_stub.val.content.cache.size = 2 ** 63 - 1 config_stub.val.content.cache.size = 2 ** 63 - 1
profile = webenginesettings.default_profile profile = webenginesettings.default_profile
profile.setter.set_http_cache_size()
webenginesettings._set_http_cache_size(profile)
assert profile.httpCacheMaximumSize() == 2 ** 31 - 1 assert profile.httpCacheMaximumSize() == 2 ** 31 - 1

View File

@ -97,10 +97,11 @@ def test_custom_env(qtbot, monkeypatch, py_proc, runner):
f.write('\n') f.write('\n')
""") """)
with qtbot.waitSignal(runner.got_cmd, timeout=10000) as blocker: with qtbot.waitSignal(runner.finished, timeout=10000):
runner.prepare_run(cmd, *args, env=env) with qtbot.waitSignal(runner.got_cmd, timeout=10000) as blocker:
runner.store_html('') runner.prepare_run(cmd, *args, env=env)
runner.store_text('') runner.store_html('')
runner.store_text('')
data = blocker.args[0] data = blocker.args[0]
ret_env = json.loads(data) 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, def test_tab_completion(qtmodeltester, fake_web_tab, app_stub, win_registry,
tabbed_browser_stubs): tabbed_browser_stubs):
tabbed_browser_stubs[0].tabs = [ tabbed_browser_stubs[0].widget.tabs = [
fake_web_tab(QUrl('https://github.com'), 'GitHub', 0), fake_web_tab(QUrl('https://github.com'), 'GitHub', 0),
fake_web_tab(QUrl('https://wikipedia.org'), 'Wikipedia', 1), fake_web_tab(QUrl('https://wikipedia.org'), 'Wikipedia', 1),
fake_web_tab(QUrl('https://duckduckgo.com'), 'DuckDuckGo', 2), fake_web_tab(QUrl('https://duckduckgo.com'), 'DuckDuckGo', 2),
] ]
tabbed_browser_stubs[1].tabs = [ tabbed_browser_stubs[1].widget.tabs = [
fake_web_tab(QUrl('https://wiki.archlinux.org'), 'ArchWiki', 0), fake_web_tab(QUrl('https://wiki.archlinux.org'), 'ArchWiki', 0),
] ]
model = miscmodels.buffer() 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, def test_tab_completion_delete(qtmodeltester, fake_web_tab, app_stub,
win_registry, tabbed_browser_stubs): win_registry, tabbed_browser_stubs):
"""Verify closing a tab by deleting it from the completion widget.""" """Verify closing a tab by deleting it from the completion widget."""
tabbed_browser_stubs[0].tabs = [ tabbed_browser_stubs[0].widget.tabs = [
fake_web_tab(QUrl('https://github.com'), 'GitHub', 0), fake_web_tab(QUrl('https://github.com'), 'GitHub', 0),
fake_web_tab(QUrl('https://wikipedia.org'), 'Wikipedia', 1), fake_web_tab(QUrl('https://wikipedia.org'), 'Wikipedia', 1),
fake_web_tab(QUrl('https://duckduckgo.com'), 'DuckDuckGo', 2) fake_web_tab(QUrl('https://duckduckgo.com'), 'DuckDuckGo', 2)
] ]
tabbed_browser_stubs[1].tabs = [ tabbed_browser_stubs[1].widget.tabs = [
fake_web_tab(QUrl('https://wiki.archlinux.org'), 'ArchWiki', 0), fake_web_tab(QUrl('https://wiki.archlinux.org'), 'ArchWiki', 0),
] ]
model = miscmodels.buffer() model = miscmodels.buffer()
@ -588,19 +588,19 @@ def test_tab_completion_delete(qtmodeltester, fake_web_tab, app_stub,
assert model.data(idx) == '0/2' assert model.data(idx) == '0/2'
model.delete_cur_item(idx) model.delete_cur_item(idx)
actual = [tab.url() for tab in tabbed_browser_stubs[0].tabs] actual = [tab.url() for tab in tabbed_browser_stubs[0].widget.tabs]
assert actual == [QUrl('https://github.com'), assert actual == [QUrl('https://github.com'),
QUrl('https://duckduckgo.com')] QUrl('https://duckduckgo.com')]
def test_other_buffer_completion(qtmodeltester, fake_web_tab, app_stub, def test_other_buffer_completion(qtmodeltester, fake_web_tab, app_stub,
win_registry, tabbed_browser_stubs, info): win_registry, tabbed_browser_stubs, info):
tabbed_browser_stubs[0].tabs = [ tabbed_browser_stubs[0].widget.tabs = [
fake_web_tab(QUrl('https://github.com'), 'GitHub', 0), fake_web_tab(QUrl('https://github.com'), 'GitHub', 0),
fake_web_tab(QUrl('https://wikipedia.org'), 'Wikipedia', 1), fake_web_tab(QUrl('https://wikipedia.org'), 'Wikipedia', 1),
fake_web_tab(QUrl('https://duckduckgo.com'), 'DuckDuckGo', 2), fake_web_tab(QUrl('https://duckduckgo.com'), 'DuckDuckGo', 2),
] ]
tabbed_browser_stubs[1].tabs = [ tabbed_browser_stubs[1].widget.tabs = [
fake_web_tab(QUrl('https://wiki.archlinux.org'), 'ArchWiki', 0), fake_web_tab(QUrl('https://wiki.archlinux.org'), 'ArchWiki', 0),
] ]
info.win_id = 1 info.win_id = 1
@ -618,14 +618,37 @@ 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, def test_window_completion(qtmodeltester, fake_web_tab, tabbed_browser_stubs,
info): info):
tabbed_browser_stubs[0].tabs = [ tabbed_browser_stubs[0].widget.tabs = [
fake_web_tab(QUrl('https://github.com'), 'GitHub', 0), fake_web_tab(QUrl('https://github.com'), 'GitHub', 0),
fake_web_tab(QUrl('https://wikipedia.org'), 'Wikipedia', 1), fake_web_tab(QUrl('https://wikipedia.org'), 'Wikipedia', 1),
fake_web_tab(QUrl('https://duckduckgo.com'), 'DuckDuckGo', 2) fake_web_tab(QUrl('https://duckduckgo.com'), 'DuckDuckGo', 2)
] ]
tabbed_browser_stubs[1].tabs = [ tabbed_browser_stubs[1].widget.tabs = [
fake_web_tab(QUrl('https://wiki.archlinux.org'), 'ArchWiki', 0) fake_web_tab(QUrl('https://wiki.archlinux.org'), 'ArchWiki', 0)
] ]

View File

@ -339,6 +339,24 @@ class TestKeyConfig:
key_config_stub.unbind(seq) key_config_stub.unbind(seq)
assert key_config_stub.get_command(seq, mode='normal') is None assert key_config_stub.get_command(seq, mode='normal') is None
def test_unbind_old_syntax(self, yaml_config_stub, key_config_stub,
config_stub):
"""Test unbinding bindings added before the keybinding refactoring.
We used to normalize keys differently, so we can have <ctrl+q> in the
config.
See https://github.com/qutebrowser/qutebrowser/issues/3699
"""
bindings = {'normal': {'<ctrl+q>': 'nop'}}
yaml_config_stub.set_obj('bindings.commands', bindings)
config_stub.read_yaml()
key_config_stub.unbind(keyutils.KeySequence.parse('<ctrl+q>'),
save_yaml=True)
assert config.instance.get_obj('bindings.commands') == {'normal': {}}
def test_empty_command(self, key_config_stub): def test_empty_command(self, key_config_stub):
"""Try binding a key to an empty command.""" """Try binding a key to an empty command."""
message = "Can't add binding 'x' with empty command in normal mode" message = "Can't add binding 'x' with empty command in normal mode"

View File

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

View File

@ -211,8 +211,11 @@ class TestYaml:
data = autoconfig.read() data = autoconfig.read()
assert data == {'tabs.show': {'global': 'value'}} assert data == {'tabs.show': {'global': 'value'}}
@pytest.mark.parametrize('persist', [True, False]) @pytest.mark.parametrize('persist, expected', [
def test_merge_persist(self, yaml, autoconfig, persist): (True, 'persist'),
(False, 'normal'),
])
def test_merge_persist(self, yaml, autoconfig, persist, expected):
"""Tests for migration of tabs.persist_mode_on_change.""" """Tests for migration of tabs.persist_mode_on_change."""
autoconfig.write({'tabs.persist_mode_on_change': {'global': persist}}) autoconfig.write({'tabs.persist_mode_on_change': {'global': persist}})
yaml.load() yaml.load()
@ -220,8 +223,7 @@ class TestYaml:
data = autoconfig.read() data = autoconfig.read()
assert 'tabs.persist_mode_on_change' not in data assert 'tabs.persist_mode_on_change' not in data
mode = 'persist' if persist else 'normal' assert data['tabs.mode_on_change']['global'] == expected
assert data['tabs.mode_on_change']['global'] == mode
def test_bindings_default(self, yaml, autoconfig): def test_bindings_default(self, yaml, autoconfig):
"""Make sure bindings.default gets removed from autoconfig.yml.""" """Make sure bindings.default gets removed from autoconfig.yml."""
@ -233,6 +235,23 @@ class TestYaml:
data = autoconfig.read() data = autoconfig.read()
assert 'bindings.default' not in data assert 'bindings.default' not in data
@pytest.mark.parametrize('show, expected', [
(True, 'always'),
(False, 'never'),
('always', 'always'),
('never', 'never'),
('pinned', 'pinned'),
])
def test_tabs_favicons_show(self, yaml, autoconfig, show, expected):
"""Tests for migration of tabs.favicons.show."""
autoconfig.write({'tabs.favicons.show': {'global': show}})
yaml.load()
yaml._save()
data = autoconfig.read()
assert data['tabs.favicons.show']['global'] == expected
def test_renamed_key_unknown_target(self, monkeypatch, yaml, def test_renamed_key_unknown_target(self, monkeypatch, yaml,
autoconfig): autoconfig):
"""A key marked as renamed with invalid name should raise an error.""" """A key marked as renamed with invalid name should raise an error."""

View File

@ -292,7 +292,7 @@ class TestEarlyInit:
'QT_XCB_FORCE_SOFTWARE_OPENGL', '1'), 'QT_XCB_FORCE_SOFTWARE_OPENGL', '1'),
('qt.force_platform', 'toaster', 'QT_QPA_PLATFORM', 'toaster'), ('qt.force_platform', 'toaster', 'QT_QPA_PLATFORM', 'toaster'),
('qt.highdpi', True, 'QT_AUTO_SCREEN_SCALE_FACTOR', '1'), ('qt.highdpi', True, 'QT_AUTO_SCREEN_SCALE_FACTOR', '1'),
('window.hide_wayland_decoration', True, ('window.hide_decoration', True,
'QT_WAYLAND_DISABLE_WINDOWDECORATION', '1') 'QT_WAYLAND_DISABLE_WINDOWDECORATION', '1')
]) ])
def test_env_vars(self, monkeypatch, config_stub, def test_env_vars(self, monkeypatch, config_stub,
@ -347,6 +347,12 @@ class TestQtArgs:
mocker.patch.object(parser, 'exit', side_effect=Exception) mocker.patch.object(parser, 'exit', side_effect=Exception)
return parser return parser
@pytest.fixture(autouse=True)
def patch_version_check(self, monkeypatch):
"""Make sure no --disable-shared-workers argument gets added."""
monkeypatch.setattr(configinit.qtutils, 'version_check',
lambda version, compiled: True)
@pytest.mark.parametrize('args, expected', [ @pytest.mark.parametrize('args, expected', [
# No Qt arguments # No Qt arguments
(['--debug'], [sys.argv[0]]), (['--debug'], [sys.argv[0]]),
@ -382,6 +388,15 @@ class TestQtArgs:
config_stub.val.qt.args = ['bar'] config_stub.val.qt.args = ['bar']
assert configinit.qt_args(parsed) == [sys.argv[0], '--foo', '--bar'] assert configinit.qt_args(parsed) == [sys.argv[0], '--foo', '--bar']
def test_shared_workers(self, config_stub, monkeypatch, parser):
monkeypatch.setattr(configinit.qtutils, 'version_check',
lambda version, compiled: False)
monkeypatch.setattr(configinit.objects, 'backend',
usertypes.Backend.QtWebEngine)
parsed = parser.parse_args([])
expected = [sys.argv[0], '--disable-shared-workers']
assert configinit.qt_args(parsed) == expected
@pytest.mark.parametrize('arg, confval, used', [ @pytest.mark.parametrize('arg, confval, used', [
# overridden by commandline arg # overridden by commandline arg

View File

@ -533,6 +533,17 @@ class FlagListSubclass(configtypes.FlagList):
'foo', 'bar', 'baz') 'foo', 'bar', 'baz')
class FromObjType(configtypes.BaseType):
"""Config type to test from_obj for List/Dict."""
def from_obj(self, value):
return int(value)
def to_py(self, value):
return value
class TestList: class TestList:
"""Test List and FlagList.""" """Test List and FlagList."""
@ -647,6 +658,12 @@ class TestList:
with pytest.raises(AssertionError): with pytest.raises(AssertionError):
typ.to_doc([['foo']]) typ.to_doc([['foo']])
def test_from_obj_sub(self):
"""Make sure the list calls from_obj() on sub-types."""
typ = configtypes.List(valtype=FromObjType())
value = typ.from_obj(['1', '2'])
assert value == [1, 2]
class TestFlagList: class TestFlagList:
@ -1665,6 +1682,13 @@ class TestDict:
print(doc) print(doc)
assert doc == expected assert doc == expected
def test_from_obj_sub(self):
"""Make sure the dict calls from_obj() on sub-types."""
typ = configtypes.Dict(keytype=configtypes.String(),
valtype=FromObjType())
value = typ.from_obj({'1': '2'})
assert value == {'1': 2}
def unrequired_class(**kwargs): def unrequired_class(**kwargs):
return configtypes.File(required=False, **kwargs) return configtypes.File(required=False, **kwargs)

View File

@ -17,118 +17,42 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>. # along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
"""pylint conftest file for javascript test.""" """pytest conftest file for javascript tests."""
import os import os
import os.path import os.path
import logging
import pytest import pytest
import jinja2 import jinja2
from PyQt5.QtCore import QUrl from PyQt5.QtCore import QUrl
try:
from PyQt5.QtWebKit import QWebSettings
from PyQt5.QtWebKitWidgets import QWebPage
except ImportError:
# FIXME:qtwebengine Make these tests use the tab API
QWebSettings = None
QWebPage = None
try:
from PyQt5.QtWebEngineWidgets import (QWebEnginePage,
QWebEngineSettings,
QWebEngineScript)
except ImportError:
QWebEnginePage = None
QWebEngineSettings = None
QWebEngineScript = None
import helpers.utils import helpers.utils
import qutebrowser.utils.debug
from qutebrowser.utils import utils from qutebrowser.utils import utils
if QWebPage is None:
TestWebPage = None
else:
class TestWebPage(QWebPage):
"""QWebPage subclass which overrides some test methods.
Attributes:
_logger: The logger used for alerts.
"""
def __init__(self, parent=None):
super().__init__(parent)
self._logger = logging.getLogger('js-tests')
def javaScriptAlert(self, _frame, msg):
"""Log javascript alerts."""
self._logger.info("js alert: {}".format(msg))
def javaScriptConfirm(self, _frame, msg):
"""Fail tests on js confirm() as that should never happen."""
pytest.fail("js confirm: {}".format(msg))
def javaScriptPrompt(self, _frame, msg, _default):
"""Fail tests on js prompt() as that should never happen."""
pytest.fail("js prompt: {}".format(msg))
def javaScriptConsoleMessage(self, msg, line, source):
"""Fail tests on js console messages as they're used for errors."""
pytest.fail("js console ({}:{}): {}".format(source, line, msg))
if QWebEnginePage is None:
TestWebEnginePage = None
else:
class TestWebEnginePage(QWebEnginePage):
"""QWebEnginePage which overrides javascript logging methods.
Attributes:
_logger: The logger used for alerts.
"""
def __init__(self, parent=None):
super().__init__(parent)
self._logger = logging.getLogger('js-tests')
def javaScriptAlert(self, _frame, msg):
"""Log javascript alerts."""
self._logger.info("js alert: {}".format(msg))
def javaScriptConfirm(self, _frame, msg):
"""Fail tests on js confirm() as that should never happen."""
pytest.fail("js confirm: {}".format(msg))
def javaScriptPrompt(self, _frame, msg, _default):
"""Fail tests on js prompt() as that should never happen."""
pytest.fail("js prompt: {}".format(msg))
def javaScriptConsoleMessage(self, level, msg, line, source):
"""Fail tests on js console messages as they're used for errors."""
pytest.fail("[{}] js console ({}:{}): {}".format(
qutebrowser.utils.debug.qenum_key(
QWebEnginePage, level), source, line, msg))
class JSTester: class JSTester:
"""Common subclass providing basic functionality for all JS testers. """Common subclass providing basic functionality for all JS testers.
Attributes: Attributes:
webview: The webview which is used. tab: The tab object which is used.
_qtbot: The QtBot fixture from pytest-qt. qtbot: The QtBot fixture from pytest-qt.
_jinja_env: The jinja2 environment used to get templates. _jinja_env: The jinja2 environment used to get templates.
""" """
def __init__(self, webview, qtbot): def __init__(self, tab, qtbot, config_stub):
self.webview = webview self.tab = tab
self._qtbot = qtbot self.qtbot = qtbot
loader = jinja2.FileSystemLoader(os.path.dirname(__file__)) loader = jinja2.FileSystemLoader(os.path.dirname(__file__))
self._jinja_env = jinja2.Environment(loader=loader, autoescape=True) self._jinja_env = jinja2.Environment(loader=loader, autoescape=True)
# Make sure error logging via JS fails tests
config_stub.val.content.javascript.log = {
'info': 'info',
'error': 'error',
'unknown': 'error',
'warning': 'error'
}
def load(self, path, **kwargs): def load(self, path, **kwargs):
"""Load and display the given jinja test data. """Load and display the given jinja test data.
@ -139,9 +63,9 @@ class JSTester:
**kwargs: Passed to jinja's template.render(). **kwargs: Passed to jinja's template.render().
""" """
template = self._jinja_env.get_template(path) template = self._jinja_env.get_template(path)
with self._qtbot.waitSignal(self.webview.loadFinished, with self.qtbot.waitSignal(self.tab.load_finished,
timeout=2000) as blocker: timeout=2000) as blocker:
self.webview.setHtml(template.render(**kwargs)) self.tab.set_html(template.render(**kwargs))
assert blocker.args == [True] assert blocker.args == [True]
def load_file(self, path: str, force: bool = False): def load_file(self, path: str, force: bool = False):
@ -161,77 +85,13 @@ class JSTester:
url: The QUrl to load. url: The QUrl to load.
force: Whether to force loading even if the file is invalid. force: Whether to force loading even if the file is invalid.
""" """
with self._qtbot.waitSignal(self.webview.loadFinished, with self.qtbot.waitSignal(self.tab.load_finished,
timeout=2000) as blocker: timeout=2000) as blocker:
self.webview.load(url) self.tab.openurl(url)
if not force: if not force:
assert blocker.args == [True] assert blocker.args == [True]
def run_file(self, filename: str, expected=None) -> None:
class JSWebKitTester(JSTester):
"""Object returned by js_tester which provides test data and a webview.
Attributes:
webview: The webview which is used.
_qtbot: The QtBot fixture from pytest-qt.
_jinja_env: The jinja2 environment used to get templates.
"""
def __init__(self, webview, qtbot):
super().__init__(webview, qtbot)
self.webview.setPage(TestWebPage(self.webview))
def scroll_anchor(self, name):
"""Scroll the main frame to the given anchor."""
page = self.webview.page()
old_pos = page.mainFrame().scrollPosition()
page.mainFrame().scrollToAnchor(name)
new_pos = page.mainFrame().scrollPosition()
assert old_pos != new_pos
def run_file(self, filename):
"""Run a javascript file.
Args:
filename: The javascript filename, relative to
qutebrowser/javascript.
Return:
The javascript return value.
"""
source = utils.read_file(os.path.join('javascript', filename))
return self.run(source)
def run(self, source):
"""Run the given javascript source.
Args:
source: The source to run as a string.
Return:
The javascript return value.
"""
assert self.webview.settings().testAttribute(
QWebSettings.JavascriptEnabled)
return self.webview.page().mainFrame().evaluateJavaScript(source)
class JSWebEngineTester(JSTester):
"""Object returned by js_tester_webengine which provides a webview.
Attributes:
webview: The webview which is used.
_qtbot: The QtBot fixture from pytest-qt.
_jinja_env: The jinja2 environment used to get templates.
"""
def __init__(self, webview, qtbot):
super().__init__(webview, qtbot)
self.webview.setPage(TestWebEnginePage(self.webview))
def run_file(self, filename: str, expected) -> None:
"""Run a javascript file. """Run a javascript file.
Args: Args:
@ -250,24 +110,24 @@ class JSWebEngineTester(JSTester):
expected: The value expected return from the javascript execution expected: The value expected return from the javascript execution
world: The scope the javascript will run in world: The scope the javascript will run in
""" """
if world is None: callback_checker = helpers.utils.CallbackChecker(self.qtbot)
world = QWebEngineScript.ApplicationWorld self.tab.run_js_async(source, callback_checker.callback, world=world)
callback_checker = helpers.utils.CallbackChecker(self._qtbot)
assert self.webview.settings().testAttribute(
QWebEngineSettings.JavascriptEnabled)
self.webview.page().runJavaScript(source, world,
callback_checker.callback)
callback_checker.check(expected) callback_checker.check(expected)
@pytest.fixture @pytest.fixture
def js_tester_webkit(webview, qtbot): def js_tester_webkit(webkit_tab, qtbot, config_stub):
"""Fixture to test javascript snippets in webkit.""" """Fixture to test javascript snippets in webkit."""
return JSWebKitTester(webview, qtbot) return JSTester(webkit_tab, qtbot, config_stub)
@pytest.fixture @pytest.fixture
def js_tester_webengine(callback_checker, webengineview, qtbot): def js_tester_webengine(webengine_tab, qtbot, config_stub):
"""Fixture to test javascript snippets in webengine.""" """Fixture to test javascript snippets in webengine."""
return JSWebEngineTester(webengineview, qtbot) return JSTester(webengine_tab, qtbot, config_stub)
@pytest.fixture
def js_tester(web_tab, qtbot, config_stub):
"""Fixture to test javascript snippets with both backends."""
return JSTester(web_tab, qtbot, config_stub)

View File

@ -21,12 +21,10 @@
import pytest import pytest
# FIXME:qtwebengine Make these tests use the tab API import helpers.utils
pytest.importorskip('PyQt5.QtWebKit')
from PyQt5.QtCore import Qt QWebSettings = pytest.importorskip("PyQt5.QtWebKit").QWebSettings
from PyQt5.QtWebKit import QWebSettings QWebPage = pytest.importorskip("PyQt5.QtWebKitWidgets").QWebPage
from PyQt5.QtWebKitWidgets import QWebPage
@pytest.fixture(autouse=True) @pytest.fixture(autouse=True)
@ -53,15 +51,17 @@ class CaretTester:
def check(self): def check(self):
"""Check whether the caret is before the MARKER text.""" """Check whether the caret is before the MARKER text."""
self.js.run_file('position_caret.js') self.js.run_file('position_caret.js')
self.js.webview.triggerPageAction(QWebPage.SelectNextWord) self.js.tab.caret.toggle_selection()
assert self.js.webview.selectedText().rstrip() == "MARKER" self.js.tab.caret.move_to_next_word()
callback_checker = helpers.utils.CallbackChecker(self.js.qtbot)
self.js.tab.caret.selection(lambda text:
callback_checker.callback(text.rstrip()))
callback_checker.check('MARKER')
def check_scrolled(self): def check_scrolled(self):
"""Check if the page is scrolled down.""" """Check if the page is scrolled down."""
frame = self.js.webview.page().mainFrame() assert not self.js.tab.scroller.at_top()
minimum = frame.scrollBarMinimum(Qt.Vertical)
value = frame.scrollBarValue(Qt.Vertical)
assert value > minimum
@pytest.fixture @pytest.fixture
@ -70,7 +70,7 @@ def caret_tester(js_tester_webkit):
caret_tester = CaretTester(js_tester_webkit) caret_tester = CaretTester(js_tester_webkit)
# Showing webview here is necessary for test_scrolled_down_img to # Showing webview here is necessary for test_scrolled_down_img to
# succeed in some cases, see #1988 # succeed in some cases, see #1988
caret_tester.js.webview.show() caret_tester.js.tab.show()
return caret_tester return caret_tester
@ -82,10 +82,11 @@ def test_simple(caret_tester):
@pytest.mark.integration @pytest.mark.integration
@pytest.mark.no_xvfb
def test_scrolled_down(caret_tester): def test_scrolled_down(caret_tester):
"""Test with multiple text blocks with the viewport scrolled down.""" """Test with multiple text blocks with the viewport scrolled down."""
caret_tester.js.load('position_caret/scrolled_down.html') caret_tester.js.load('position_caret/scrolled_down.html')
caret_tester.js.scroll_anchor('anchor') caret_tester.js.tab.scroller.to_anchor('anchor')
caret_tester.check_scrolled() caret_tester.check_scrolled()
caret_tester.check() caret_tester.check()
@ -99,9 +100,10 @@ def test_invisible(caret_tester, style):
@pytest.mark.integration @pytest.mark.integration
@pytest.mark.no_xvfb
def test_scrolled_down_img(caret_tester): def test_scrolled_down_img(caret_tester):
"""Test with an image at the top with the viewport scrolled down.""" """Test with an image at the top with the viewport scrolled down."""
caret_tester.js.load('position_caret/scrolled_down_img.html') caret_tester.js.load('position_caret/scrolled_down_img.html')
caret_tester.js.scroll_anchor('anchor') caret_tester.js.tab.scroller.to_anchor('anchor')
caret_tester.check_scrolled() caret_tester.check_scrolled()
caret_tester.check() caret_tester.check()

View File

@ -27,11 +27,6 @@ QWebEngineProfile = QtWebEngineWidgets.QWebEngineProfile
from qutebrowser.utils import javascript from qutebrowser.utils import javascript
try:
from qutebrowser.browser.webengine import webenginesettings
except ImportError:
webenginesettings = None
DEFAULT_BODY_BG = "rgba(0, 0, 0, 0)" DEFAULT_BODY_BG = "rgba(0, 0, 0, 0)"
GREEN_BODY_BG = "rgb(0, 255, 0)" GREEN_BODY_BG = "rgb(0, 255, 0)"
@ -56,8 +51,6 @@ class StylesheetTester:
"""Initialize the stylesheet with a provided css file.""" """Initialize the stylesheet with a provided css file."""
css_path = os.path.join(os.path.dirname(__file__), css_file) css_path = os.path.join(os.path.dirname(__file__), css_file)
self.config_stub.val.content.user_stylesheets = css_path self.config_stub.val.content.user_stylesheets = css_path
p = QWebEngineProfile.defaultProfile()
webenginesettings._init_stylesheet(p)
def set_css(self, css): def set_css(self, css):
"""Set document style to `css` via stylesheet.js.""" """Set document style to `css` via stylesheet.js."""
@ -67,10 +60,12 @@ class StylesheetTester:
def check_set(self, value, css_style="background-color", def check_set(self, value, css_style="background-color",
document_element="document.body"): document_element="document.body"):
"""Check whether the css in ELEMENT is set to VALUE.""" """Check whether the css in ELEMENT is set to VALUE."""
self.js.run("window.getComputedStyle({}, null)" self.js.run("console.log({document});"
".getPropertyValue('{}');" "window.getComputedStyle({document}, null)"
.format(document_element, ".getPropertyValue('{prop}');".format(
javascript.string_escape(css_style)), value) document=document_element,
prop=javascript.string_escape(css_style)),
value)
def check_eq(self, one, two, true=True): def check_eq(self, one, two, true=True):
"""Check if one and two are equal.""" """Check if one and two are equal."""
@ -81,7 +76,7 @@ class StylesheetTester:
def stylesheet_tester(js_tester_webengine, config_stub): def stylesheet_tester(js_tester_webengine, config_stub):
"""Helper fixture to test stylesheets.""" """Helper fixture to test stylesheets."""
ss_tester = StylesheetTester(js_tester_webengine, config_stub) ss_tester = StylesheetTester(js_tester_webengine, config_stub)
ss_tester.js.webview.show() ss_tester.js.tab.show()
return ss_tester return ss_tester
@ -89,8 +84,8 @@ def stylesheet_tester(js_tester_webengine, config_stub):
'stylesheet/simple_bg_set_red.html']) 'stylesheet/simple_bg_set_red.html'])
def test_set_delayed(stylesheet_tester, page): def test_set_delayed(stylesheet_tester, page):
"""Test a delayed invocation of set_css.""" """Test a delayed invocation of set_css."""
stylesheet_tester.init_stylesheet("none.css")
stylesheet_tester.js.load(page) stylesheet_tester.js.load(page)
stylesheet_tester.init_stylesheet("none.css")
stylesheet_tester.set_css("body {background-color: rgb(0, 255, 0);}") stylesheet_tester.set_css("body {background-color: rgb(0, 255, 0);}")
stylesheet_tester.check_set("rgb(0, 255, 0)") stylesheet_tester.check_set("rgb(0, 255, 0)")
@ -99,8 +94,8 @@ def test_set_delayed(stylesheet_tester, page):
'stylesheet/simple_bg_set_red.html']) 'stylesheet/simple_bg_set_red.html'])
def test_set_clear_bg(stylesheet_tester, page): def test_set_clear_bg(stylesheet_tester, page):
"""Test setting and clearing the stylesheet.""" """Test setting and clearing the stylesheet."""
stylesheet_tester.init_stylesheet()
stylesheet_tester.js.load('stylesheet/simple.html') stylesheet_tester.js.load('stylesheet/simple.html')
stylesheet_tester.init_stylesheet()
stylesheet_tester.check_set(GREEN_BODY_BG) stylesheet_tester.check_set(GREEN_BODY_BG)
stylesheet_tester.set_css("") stylesheet_tester.set_css("")
stylesheet_tester.check_set(DEFAULT_BODY_BG) stylesheet_tester.check_set(DEFAULT_BODY_BG)
@ -108,31 +103,34 @@ def test_set_clear_bg(stylesheet_tester, page):
def test_set_xml(stylesheet_tester): def test_set_xml(stylesheet_tester):
"""Test stylesheet is applied without altering xml files.""" """Test stylesheet is applied without altering xml files."""
stylesheet_tester.init_stylesheet()
stylesheet_tester.js.load_file('stylesheet/simple.xml') stylesheet_tester.js.load_file('stylesheet/simple.xml')
stylesheet_tester.init_stylesheet()
stylesheet_tester.check_set(GREEN_BODY_BG) stylesheet_tester.check_set(GREEN_BODY_BG)
stylesheet_tester.check_eq('"html"', "document.documentElement.nodeName") stylesheet_tester.check_eq('"html"', "document.documentElement.nodeName")
def test_set_svg(stylesheet_tester): def test_set_svg(stylesheet_tester):
"""Test stylesheet is applied for svg files.""" """Test stylesheet is applied for svg files."""
stylesheet_tester.init_stylesheet()
stylesheet_tester.js.load_file('../../../misc/cheatsheet.svg') stylesheet_tester.js.load_file('../../../misc/cheatsheet.svg')
stylesheet_tester.init_stylesheet()
stylesheet_tester.check_set(GREEN_BODY_BG, stylesheet_tester.check_set(GREEN_BODY_BG,
document_element="document.documentElement") document_element="document.documentElement")
stylesheet_tester.check_eq('"svg"', "document.documentElement.nodeName") stylesheet_tester.check_eq('"svg"', "document.documentElement.nodeName")
def test_set_error(stylesheet_tester): @pytest.mark.skip(reason="Too flaky, see #3771")
def test_set_error(stylesheet_tester, config_stub):
"""Test stylesheet modifies file not found error pages.""" """Test stylesheet modifies file not found error pages."""
config_stub.changed.disconnect() # This test is flaky otherwise...
stylesheet_tester.init_stylesheet() stylesheet_tester.init_stylesheet()
stylesheet_tester.js.tab._init_stylesheet()
stylesheet_tester.js.load_file('non-existent.html', force=True) stylesheet_tester.js.load_file('non-existent.html', force=True)
stylesheet_tester.check_set(GREEN_BODY_BG) stylesheet_tester.check_set(GREEN_BODY_BG)
def test_appendchild(stylesheet_tester): def test_appendchild(stylesheet_tester):
stylesheet_tester.init_stylesheet()
stylesheet_tester.js.load('stylesheet/simple.html') stylesheet_tester.js.load('stylesheet/simple.html')
stylesheet_tester.init_stylesheet()
js_test_file_path = ('../../tests/unit/javascript/stylesheet/' js_test_file_path = ('../../tests/unit/javascript/stylesheet/'
'test_appendchild.js') 'test_appendchild.js')
stylesheet_tester.js.run_file(js_test_file_path, {}) stylesheet_tester.js.run_file(js_test_file_path, {})

View File

@ -47,15 +47,32 @@ def test_element_js_webkit(webview, js_enabled, expected):
@pytest.mark.usefixtures('redirect_webengine_data') @pytest.mark.usefixtures('redirect_webengine_data')
@pytest.mark.parametrize('js_enabled, expected', [(True, 2.0), (False, 2.0)]) @pytest.mark.parametrize('js_enabled, world, expected', [
def test_simple_js_webengine(callback_checker, webengineview, js_enabled, # main world
expected): (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):
"""With QtWebEngine, runJavaScript works even when JS is off.""" """With QtWebEngine, runJavaScript works even when JS is off."""
# If we get there (because of the webengineview fixture) we can be certain # If we get there (because of the webengineview fixture) we can be certain
# QtWebEngine is available # QtWebEngine is available
from PyQt5.QtWebEngineWidgets import QWebEngineSettings from PyQt5.QtWebEngineWidgets import QWebEngineSettings, QWebEngineScript
webengineview.settings().setAttribute(QWebEngineSettings.JavascriptEnabled,
js_enabled)
webengineview.page().runJavaScript('1 + 1', callback_checker.callback) 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)
callback_checker.check(expected) callback_checker.check(expected)

View File

@ -25,6 +25,7 @@ from PyQt5.QtCore import Qt
import pytest import pytest
from qutebrowser.keyinput import basekeyparser, keyutils from qutebrowser.keyinput import basekeyparser, keyutils
from qutebrowser.utils import utils
# Alias because we need this a lot in here. # Alias because we need this a lot in here.
@ -153,14 +154,16 @@ class TestHandle:
keyparser._read_config('prompt') keyparser._read_config('prompt')
def test_valid_key(self, fake_keyevent, keyparser): def test_valid_key(self, fake_keyevent, keyparser):
keyparser.handle(fake_keyevent(Qt.Key_A, Qt.ControlModifier)) modifier = Qt.MetaModifier if utils.is_mac else Qt.ControlModifier
keyparser.handle(fake_keyevent(Qt.Key_X, Qt.ControlModifier)) keyparser.handle(fake_keyevent(Qt.Key_A, modifier))
keyparser.handle(fake_keyevent(Qt.Key_X, modifier))
keyparser.execute.assert_called_once_with('message-info ctrla', None) keyparser.execute.assert_called_once_with('message-info ctrla', None)
assert not keyparser._sequence assert not keyparser._sequence
def test_valid_key_count(self, fake_keyevent, keyparser): def test_valid_key_count(self, fake_keyevent, keyparser):
modifier = Qt.MetaModifier if utils.is_mac else Qt.ControlModifier
keyparser.handle(fake_keyevent(Qt.Key_5)) keyparser.handle(fake_keyevent(Qt.Key_5))
keyparser.handle(fake_keyevent(Qt.Key_A, Qt.ControlModifier)) keyparser.handle(fake_keyevent(Qt.Key_A, modifier))
keyparser.execute.assert_called_once_with('message-info ctrla', 5) keyparser.execute.assert_called_once_with('message-info ctrla', 5)
@pytest.mark.parametrize('keys', [ @pytest.mark.parametrize('keys', [
@ -198,13 +201,34 @@ class TestHandle:
keyparser.execute.assert_called_with('message-info ba', None) keyparser.execute.assert_called_with('message-info ba', None)
assert not keyparser._sequence assert not keyparser._sequence
@pytest.mark.parametrize('key, number', [(Qt.Key_0, 0), (Qt.Key_1, 1)]) @pytest.mark.parametrize('key, modifiers, number', [
def test_number_press(self, handle_text, keyparser, key, number): (Qt.Key_0, Qt.NoModifier, 0),
handle_text(key) (Qt.Key_1, Qt.NoModifier, 1),
(Qt.Key_1, Qt.KeypadModifier, 1),
])
def test_number_press(self, fake_keyevent, keyparser,
key, modifiers, number):
keyparser.handle(fake_keyevent(key, modifiers))
command = 'message-info {}'.format(number) command = 'message-info {}'.format(number)
keyparser.execute.assert_called_once_with(command, None) keyparser.execute.assert_called_once_with(command, None)
assert not keyparser._sequence assert not keyparser._sequence
@pytest.mark.parametrize('modifiers, text', [
(Qt.NoModifier, '2'),
(Qt.KeypadModifier, 'num-2'),
])
def test_number_press_keypad(self, fake_keyevent, keyparser, config_stub,
modifiers, text):
"""Make sure a <Num+2> binding overrides the 2 binding."""
config_stub.val.bindings.commands = {'normal': {
'2': 'message-info 2',
'<Num+2>': 'message-info num-2'}}
keyparser._read_config('normal')
keyparser.handle(fake_keyevent(Qt.Key_2, modifiers))
command = 'message-info {}'.format(text)
keyparser.execute.assert_called_once_with(command, None)
assert not keyparser._sequence
def test_umlauts(self, handle_text, keyparser, config_stub): def test_umlauts(self, handle_text, keyparser, config_stub):
config_stub.val.bindings.commands = {'normal': {'ü': 'message-info ü'}} config_stub.val.bindings.commands = {'normal': {'ü': 'message-info ü'}}
keyparser._read_config('normal') keyparser._read_config('normal')
@ -215,6 +239,15 @@ class TestHandle:
handle_text(Qt.Key_X) handle_text(Qt.Key_X)
keyparser.execute.assert_called_once_with('message-info a', None) keyparser.execute.assert_called_once_with('message-info a', None)
def test_mapping_keypad(self, config_stub, fake_keyevent, keyparser):
"""Make sure falling back to non-numpad keys works with mappings."""
config_stub.val.bindings.commands = {'normal': {'a': 'nop'}}
config_stub.val.bindings.key_mappings = {'1': 'a'}
keyparser._read_config('normal')
keyparser.handle(fake_keyevent(Qt.Key_1, Qt.KeypadModifier))
keyparser.execute.assert_called_once_with('nop', None)
def test_binding_and_mapping(self, config_stub, handle_text, keyparser): def test_binding_and_mapping(self, config_stub, handle_text, keyparser):
"""with a conflicting binding/mapping, the binding should win.""" """with a conflicting binding/mapping, the binding should win."""
handle_text(Qt.Key_B) handle_text(Qt.Key_B)
@ -296,6 +329,15 @@ class TestCount:
assert sig1.args == ('4',) assert sig1.args == ('4',)
assert sig2.args == ('42',) assert sig2.args == ('42',)
def test_numpad(self, fake_keyevent, keyparser):
"""Make sure we can enter a count via numpad."""
for key, modifiers in [(Qt.Key_4, Qt.KeypadModifier),
(Qt.Key_2, Qt.KeypadModifier),
(Qt.Key_B, Qt.NoModifier),
(Qt.Key_A, Qt.NoModifier)]:
keyparser.handle(fake_keyevent(key, modifiers))
keyparser.execute.assert_called_once_with('message-info ba', 42)
def test_clear_keystring(qtbot, keyparser): def test_clear_keystring(qtbot, keyparser):
"""Test that the keystring is cleared and the signal is emitted.""" """Test that the keystring is cleared and the signal is emitted."""

View File

@ -28,6 +28,7 @@ from PyQt5.QtWidgets import QWidget
from tests.unit.keyinput import key_data from tests.unit.keyinput import key_data
from qutebrowser.keyinput import keyutils from qutebrowser.keyinput import keyutils
from qutebrowser.utils import utils
@pytest.fixture(params=key_data.KEYS, ids=lambda k: k.attribute) @pytest.fixture(params=key_data.KEYS, ids=lambda k: k.attribute)
@ -346,20 +347,28 @@ class TestKeySequence:
@pytest.mark.parametrize('old, key, modifiers, text, expected', [ @pytest.mark.parametrize('old, key, modifiers, text, expected', [
('a', Qt.Key_B, Qt.NoModifier, 'b', 'ab'), ('a', Qt.Key_B, Qt.NoModifier, 'b', 'ab'),
('a', Qt.Key_B, Qt.ShiftModifier, 'B', 'aB'), ('a', Qt.Key_B, Qt.ShiftModifier, 'B', 'aB'),
('a', Qt.Key_B, Qt.ControlModifier | Qt.ShiftModifier, 'B', ('a', Qt.Key_B, Qt.AltModifier | Qt.ShiftModifier, 'B',
'a<Ctrl+Shift+b>'), 'a<Alt+Shift+b>'),
# Modifier stripping with symbols # Modifier stripping with symbols
('', Qt.Key_Colon, Qt.NoModifier, ':', ':'), ('', Qt.Key_Colon, Qt.NoModifier, ':', ':'),
('', Qt.Key_Colon, Qt.ShiftModifier, ':', ':'), ('', Qt.Key_Colon, Qt.ShiftModifier, ':', ':'),
('', Qt.Key_Colon, Qt.ControlModifier | Qt.ShiftModifier, ':', ('', Qt.Key_Colon, Qt.AltModifier | Qt.ShiftModifier, ':',
'<Ctrl+Shift+:>'), '<Alt+Shift+:>'),
# Swapping Control/Meta on macOS
('', Qt.Key_A, Qt.ControlModifier, '',
'<Meta+A>' if utils.is_mac else '<Ctrl+A>'),
('', Qt.Key_A, Qt.ControlModifier | Qt.ShiftModifier, '',
'<Meta+Shift+A>' if utils.is_mac else '<Ctrl+Shift+A>'),
('', Qt.Key_A, Qt.MetaModifier, '',
'<Ctrl+A>' if utils.is_mac else '<Meta+A>'),
# Handling of Backtab # Handling of Backtab
('', Qt.Key_Backtab, Qt.NoModifier, '', '<Backtab>'), ('', Qt.Key_Backtab, Qt.NoModifier, '', '<Backtab>'),
('', Qt.Key_Backtab, Qt.ShiftModifier, '', '<Shift+Tab>'), ('', Qt.Key_Backtab, Qt.ShiftModifier, '', '<Shift+Tab>'),
('', Qt.Key_Backtab, Qt.ControlModifier | Qt.ShiftModifier, '', ('', Qt.Key_Backtab, Qt.AltModifier | Qt.ShiftModifier, '',
'<Control+Shift+Tab>'), '<Alt+Shift+Tab>'),
# Stripping of Qt.GroupSwitchModifier # Stripping of Qt.GroupSwitchModifier
('', Qt.Key_A, Qt.GroupSwitchModifier, 'a', 'a'), ('', Qt.Key_A, Qt.GroupSwitchModifier, 'a', 'a'),
@ -370,6 +379,27 @@ class TestKeySequence:
new = seq.append_event(event) new = seq.append_event(event)
assert new == keyutils.KeySequence.parse(expected) assert new == keyutils.KeySequence.parse(expected)
@pytest.mark.fake_os('mac')
@pytest.mark.parametrize('modifiers, expected', [
(Qt.ControlModifier,
Qt.MetaModifier),
(Qt.MetaModifier,
Qt.ControlModifier),
(Qt.ControlModifier | Qt.MetaModifier,
Qt.ControlModifier | Qt.MetaModifier),
(Qt.ControlModifier | Qt.ShiftModifier,
Qt.MetaModifier | Qt.ShiftModifier),
(Qt.MetaModifier | Qt.ShiftModifier,
Qt.ControlModifier | Qt.ShiftModifier),
(Qt.ShiftModifier, Qt.ShiftModifier),
])
def test_fake_mac(self, fake_keyevent, modifiers, expected):
"""Make sure Control/Meta are swapped with a simulated Mac."""
seq = keyutils.KeySequence()
event = fake_keyevent(key=Qt.Key_A, modifiers=modifiers)
new = seq.append_event(event)
assert new[0] == keyutils.KeyInfo(Qt.Key_A, expected)
@pytest.mark.parametrize('key', [Qt.Key_unknown, 0x0]) @pytest.mark.parametrize('key', [Qt.Key_unknown, 0x0])
def test_append_event_invalid(self, key): def test_append_event_invalid(self, key):
seq = keyutils.KeySequence() seq = keyutils.KeySequence()
@ -377,6 +407,15 @@ class TestKeySequence:
with pytest.raises(keyutils.KeyParseError): with pytest.raises(keyutils.KeyParseError):
seq.append_event(event) seq.append_event(event)
def test_strip_modifiers(self):
seq = keyutils.KeySequence(Qt.Key_0,
Qt.Key_1 | Qt.KeypadModifier,
Qt.Key_A | Qt.ControlModifier)
expected = keyutils.KeySequence(Qt.Key_0,
Qt.Key_1,
Qt.Key_A | Qt.ControlModifier)
assert seq.strip_modifiers() == expected
def test_with_mappings(self): def test_with_mappings(self):
seq = keyutils.KeySequence.parse('foobar') seq = keyutils.KeySequence.parse('foobar')
mappings = {keyutils.KeySequence('b'): keyutils.KeySequence('t')} mappings = {keyutils.KeySequence('b'): keyutils.KeySequence('t')}
@ -479,6 +518,8 @@ def test_is_printable(key, printable):
(Qt.Key_Escape, Qt.ControlModifier, True), (Qt.Key_Escape, Qt.ControlModifier, True),
(Qt.Key_X, Qt.ControlModifier, True), (Qt.Key_X, Qt.ControlModifier, True),
(Qt.Key_X, Qt.NoModifier, False), (Qt.Key_X, Qt.NoModifier, False),
(Qt.Key_2, Qt.KeypadModifier, False),
(Qt.Key_2, Qt.NoModifier, False),
]) ])
def test_is_special(key, modifiers, special): def test_is_special(key, modifiers, special):
assert keyutils.is_special(key, modifiers) == special assert keyutils.is_special(key, modifiers) == special

View File

@ -96,3 +96,11 @@ class TestHintKeyParser:
assert match == QKeySequence.ExactMatch assert match == QKeySequence.ExactMatch
keyparser.execute.assert_called_with('follow-hint -s as', None) keyparser.execute.assert_called_with('follow-hint -s as', None)
def test_numberkey_hint_match(self, keyparser, fake_keyevent):
keyparser.update_bindings(['21', '22'])
match = keyparser.handle(fake_keyevent(Qt.Key_2, Qt.KeypadModifier))
assert match == QKeySequence.PartialMatch
match = keyparser.handle(fake_keyevent(Qt.Key_2, Qt.KeypadModifier))
assert match == QKeySequence.ExactMatch

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