Merge remote-tracking branch 'upstream/master' into filter-dict-names
Merging to investigate failed tests that seem unrelated to the PR.
2
.flake8
@ -44,7 +44,7 @@ ignore =
|
||||
min-version = 3.4.0
|
||||
max-complexity = 12
|
||||
per-file-ignores =
|
||||
/tests/*/test_*.py : D100,D101,D401
|
||||
/tests/**/test_*.py : D100,D101,D401
|
||||
/tests/unit/browser/test_history.py : N806
|
||||
/tests/helpers/fixtures.py : N806
|
||||
/tests/unit/browser/webkit/http/test_content_disposition.py : D400
|
||||
|
@ -8,6 +8,7 @@ graft icons
|
||||
graft doc/img
|
||||
graft misc/apparmor
|
||||
graft misc/userscripts
|
||||
graft misc/requirements
|
||||
recursive-include scripts *.py *.sh *.js
|
||||
include qutebrowser/utils/testfile
|
||||
include qutebrowser/git-commit-id
|
||||
@ -32,8 +33,6 @@ include doc/qutebrowser.1.asciidoc
|
||||
include doc/changelog.asciidoc
|
||||
prune tests
|
||||
prune qutebrowser/3rdparty
|
||||
prune misc/requirements
|
||||
prune misc/docker
|
||||
exclude pytest.ini
|
||||
exclude qutebrowser.rcc
|
||||
exclude qutebrowser/javascript/.eslintrc.yaml
|
||||
|
@ -99,7 +99,7 @@ Requirements
|
||||
The following software and libraries are required to run qutebrowser:
|
||||
|
||||
* http://www.python.org/[Python] 3.5 or newer (3.6 recommended)
|
||||
* http://qt.io/[Qt] 5.7.1 or newer with the following modules:
|
||||
* http://qt.io/[Qt] 5.7.1 or newer (5.10 recommended) with the following modules:
|
||||
- QtCore / qtbase
|
||||
- QtQuick (part of qtbase in some distributions)
|
||||
- QtSQL (part of qtbase in some distributions)
|
||||
@ -109,7 +109,7 @@ The following software and libraries are required to run qutebrowser:
|
||||
link:https://github.com/annulen/webkit/wiki[updated fork] (5.212) is
|
||||
supported
|
||||
* http://www.riverbankcomputing.com/software/pyqt/intro[PyQt] 5.7.0 or newer
|
||||
(5.9.2 recommended) for Python 3
|
||||
(5.10 recommended) for Python 3
|
||||
* https://pypi.python.org/pypi/setuptools/[pkg_resources/setuptools]
|
||||
* http://fdik.org/pyPEG/[pyPEG2]
|
||||
* http://jinja.pocoo.org/[jinja2]
|
||||
|
@ -18,13 +18,70 @@ breaking changes (such as renamed commands) can happen in minor releases.
|
||||
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
|
||||
~~~~~~~
|
||||
|
||||
- QtWebEngine: Support for JavaScript Shared Web Workers have been disabled on
|
||||
Qt versions older than 5.11 because of security issues in in Chromium.
|
||||
You can get the same effect in earlier versions via
|
||||
`:set qt.args ['disable-shared-workers']`. An equivalent workaround is also
|
||||
contained in Qt 5.9.5 and 5.10.1.
|
||||
- The file dialog for downloads now has basic tab completion based on the
|
||||
entered text.
|
||||
- `:version` now shows OS information for POSIX OS other than Linux/macOS.
|
||||
- When there's an error inserting the text from an external editor, a backup
|
||||
file is now saved.
|
||||
- The `window.hide_wayland_decoration` setting got renamed to
|
||||
`window.hide_decoration` and now also works outside of wayland.
|
||||
- The `tabs.favicons.show` setting now can take three values: `'always'` (was
|
||||
`True`), `'never'` (was `False`) and `'pinned'` (to only show favicons for
|
||||
pinned tabs).
|
||||
- Hover tooltips on tabs now always show the webpage's title.
|
||||
- The default value for `content.host_blocking.lists` was changed to only
|
||||
include https://github.com/StevenBlack/hosts[Steven Black's hosts-list] which
|
||||
combines various sources.
|
||||
- Error messages when trying to wrap when `tabs.wrap` is `False` are now logged
|
||||
to debug instead of messages.
|
||||
|
||||
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
|
||||
~~~~~
|
||||
@ -36,6 +93,15 @@ Fixed
|
||||
- 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
|
||||
("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
|
||||
------
|
||||
|
@ -670,10 +670,11 @@ qutebrowser release
|
||||
~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
* Make sure there are no unstaged changes and the tests are green.
|
||||
* Make sure all issues with the related milestone are closed.
|
||||
* Run `x=... y=...` to set the respective shell variables.
|
||||
|
||||
* Adjust `__version_info__` in `qutebrowser/__init__.py`.
|
||||
* Update changelog (remove *(unreleased)*).
|
||||
* Adjust `__version_info__` in `qutebrowser/__init__.py`.
|
||||
* Commit.
|
||||
|
||||
* 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
|
||||
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).
|
||||
* 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.
|
||||
* Announce to qutebrowser and qutebrowser-announce mailinglist.
|
||||
|
@ -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
|
||||
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
|
||||
|
||||
Unable to view flash content.::
|
||||
|
@ -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-page,scroll-page>>|Scroll the frame page-wise.
|
||||
|<<scroll-px,scroll-px>>|Scroll the current tab by 'count * dx/dy' pixels.
|
||||
|<<scroll-to-anchor,scroll-to-anchor>>|Scroll to the given anchor in the document.
|
||||
|<<scroll-to-perc,scroll-to-perc>>|Scroll to a specific percentage of the page.
|
||||
|<<search,search>>|Search for a text on the current page. With no text, clear results.
|
||||
|<<search-next,search-next>>|Continue the search to the ([count]th) next term.
|
||||
@ -1024,6 +1025,15 @@ Scroll the current tab by 'count * dx/dy' pixels.
|
||||
==== count
|
||||
multiplier
|
||||
|
||||
[[scroll-to-anchor]]
|
||||
=== scroll-to-anchor
|
||||
Syntax: +:scroll-to-anchor 'name'+
|
||||
|
||||
Scroll to the given anchor in the document.
|
||||
|
||||
==== positional arguments
|
||||
* +'name'+: The anchor to scroll to.
|
||||
|
||||
[[scroll-to-perc]]
|
||||
=== scroll-to-perc
|
||||
Syntax: +:scroll-to-perc [*--horizontal*] ['perc']+
|
||||
|
@ -242,10 +242,10 @@ To suppress loading of any default keybindings, you can set
|
||||
Loading `autoconfig.yml`
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
By default, all customization done via `:set`, `:bind` and `:unbind` is
|
||||
temporary as soon as a `config.py` exists. The settings done that way are always
|
||||
saved in the `autoconfig.yml` file, but you'll need to explicitly load it in
|
||||
your `config.py` by doing:
|
||||
All customization done via the UI (`:set`, `:bind` and `:unbind`) is
|
||||
stored in the `autoconfig.yml` file, which is not loaded automatically as soon
|
||||
as a `config.py` exists. If you want those settings to be loaded, you'll need to
|
||||
explicitly load the `autoconfig.yml` file in your `config.py` by doing:
|
||||
|
||||
.config.py:
|
||||
[source,python]
|
||||
|
@ -236,10 +236,11 @@
|
||||
|<<tabs.close_mouse_button,tabs.close_mouse_button>>|Mouse button with which to close tabs.
|
||||
|<<tabs.close_mouse_button_on_bar,tabs.close_mouse_button_on_bar>>|How to behave when the close mouse button is pressed on the tab bar.
|
||||
|<<tabs.favicons.scale,tabs.favicons.scale>>|Scaling factor for favicons in the tab bar.
|
||||
|<<tabs.favicons.show,tabs.favicons.show>>|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.width,tabs.indicator.width>>|Width (in pixels) of the progress indicator (0 to disable).
|
||||
|<<tabs.last_close,tabs.last_close>>|How to behave when the last tab is closed.
|
||||
|<<tabs.min_width,tabs.min_width>>|Minimum width (in pixels) of tabs (-1 for the default minimum size behavior).
|
||||
|<<tabs.mode_on_change,tabs.mode_on_change>>|When switching tabs, what input mode is applied.
|
||||
|<<tabs.mousewheel_switching,tabs.mousewheel_switching>>|Switch between tabs using the mouse wheel.
|
||||
|<<tabs.new_position.related,tabs.new_position.related>>|Position of new tabs opened from another tab.
|
||||
@ -259,10 +260,11 @@
|
||||
|<<url.auto_search,url.auto_search>>|What search to start when something else than a URL is entered.
|
||||
|<<url.default_page,url.default_page>>|Page to open if :open -t/-b/-w is used without URL.
|
||||
|<<url.incdec_segments,url.incdec_segments>>|URL segments where `:navigate increment/decrement` will search for a number.
|
||||
|<<url.open_base_url,url.open_base_url>>|Open base URL of the searchengine if a searchengine shortcut is invoked without parameters.
|
||||
|<<url.searchengines,url.searchengines>>|Search engines which can be used via the address bar.
|
||||
|<<url.start_pages,url.start_pages>>|Page(s) to open at the start.
|
||||
|<<url.yank_ignored_parameters,url.yank_ignored_parameters>>|URL parameters to strip with `:yank url`.
|
||||
|<<window.hide_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
|
||||
|<<zoom.default,zoom.default>>|Default zoom level.
|
||||
|<<zoom.levels,zoom.levels>>|Available zoom levels.
|
||||
@ -1653,11 +1655,7 @@ Type: <<types,List of Url>>
|
||||
|
||||
Default:
|
||||
|
||||
- +pass:[https://www.malwaredomainlist.com/hostslist/hosts.txt]+
|
||||
- +pass:[http://someonewhocares.org/hosts/hosts]+
|
||||
- +pass:[http://winhelp2002.mvps.org/hosts.zip]+
|
||||
- +pass:[http://malwaredomains.lehigh.edu/files/justdomains.zip]+
|
||||
- +pass:[https://pgl.yoyo.org/adservers/serverlist.php?hostformat=hosts&mimetype=plaintext]+
|
||||
- +pass:[https://raw.githubusercontent.com/StevenBlack/hosts/master/hosts]+
|
||||
|
||||
[[content.host_blocking.whitelist]]
|
||||
=== content.host_blocking.whitelist
|
||||
@ -2823,11 +2821,17 @@ Default: +pass:[1.0]+
|
||||
|
||||
[[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
|
||||
@ -2866,6 +2870,16 @@ Valid values:
|
||||
|
||||
Default: +pass:[ignore]+
|
||||
|
||||
[[tabs.min_width]]
|
||||
=== tabs.min_width
|
||||
Minimum width (in pixels) of tabs (-1 for the default minimum size behavior).
|
||||
This setting only applies when tabs are horizontal.
|
||||
This setting does not apply to pinned tabs, unless `tabs.pinned.shrink` is False.
|
||||
|
||||
Type: <<types,Int>>
|
||||
|
||||
Default: +pass:[-1]+
|
||||
|
||||
[[tabs.mode_on_change]]
|
||||
=== tabs.mode_on_change
|
||||
When switching tabs, what input mode is applied.
|
||||
@ -3102,6 +3116,14 @@ Default:
|
||||
- +pass:[path]+
|
||||
- +pass:[query]+
|
||||
|
||||
[[url.open_base_url]]
|
||||
=== url.open_base_url
|
||||
Open base URL of the searchengine if a searchengine shortcut is invoked without parameters.
|
||||
|
||||
Type: <<types,Bool>>
|
||||
|
||||
Default: +pass:[false]+
|
||||
|
||||
[[url.searchengines]]
|
||||
=== url.searchengines
|
||||
Search engines which can be used via the address bar.
|
||||
@ -3137,10 +3159,12 @@ Default:
|
||||
- +pass:[utm_term]+
|
||||
- +pass:[utm_content]+
|
||||
|
||||
[[window.hide_wayland_decoration]]
|
||||
=== window.hide_wayland_decoration
|
||||
Hide the window decoration when using wayland.
|
||||
This setting requires a restart.
|
||||
[[window.hide_decoration]]
|
||||
=== window.hide_decoration
|
||||
Hide the window decoration.
|
||||
|
||||
This setting requires a restart on Wayland.
|
||||
|
||||
|
||||
Type: <<types,Bool>>
|
||||
|
||||
@ -3273,7 +3297,7 @@ See the setting's valid values for more information on allowed values.
|
||||
|TextAlignment|Alignment of text.
|
||||
|TimestampTemplate|An strftime-like template for timestamps.
|
||||
|
||||
See https://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.
|
||||
|Url|A URL as a string.
|
||||
|VerticalPosition|The position of the download bar.
|
||||
|
Before Width: | Height: | Size: 1.0 MiB After Width: | Height: | Size: 1.0 MiB |
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 46 KiB |
@ -47,17 +47,26 @@ Debian Stretch / Ubuntu 17.04 and 17.10
|
||||
Those versions come with QtWebEngine in the repositories. This makes it possible
|
||||
to install qutebrowser via the Debian package.
|
||||
|
||||
Download the https://packages.debian.org/sid/all/qutebrowser/download[qutebrowser] and
|
||||
https://packages.debian.org/sid/all/python3-pypeg2/download[PyPEG2]
|
||||
package from the Debian repositories.
|
||||
You'll need to download three packages:
|
||||
|
||||
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 ./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
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
@ -417,7 +426,11 @@ Creating a wrapper script
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Running `tox` does not install a system-wide `qutebrowser` script. You can
|
||||
launch qutebrowser by doing `.venv/bin/python3 -m qutebrowser`.
|
||||
launch qutebrowser by doing:
|
||||
|
||||
----
|
||||
.venv/bin/python3 -m qutebrowser
|
||||
----
|
||||
|
||||
You can create a simple wrapper script to start qutebrowser somewhere in your
|
||||
`$PATH` (e.g. `/usr/local/bin/qutebrowser` or `~/bin/qutebrowser`):
|
||||
|
Before Width: | Height: | Size: 45 KiB After Width: | Height: | Size: 113 KiB |
@ -1,25 +1,31 @@
|
||||
PYTHON = python3
|
||||
DESTDIR = /
|
||||
PREFIX = /usr/local
|
||||
DESTDIR =
|
||||
ICONSIZES = 16 24 32 48 64 128 256 512
|
||||
|
||||
SETUPTOOLSOPTIONS =
|
||||
ifdef DESTDIR
|
||||
SETUPTOOLSOPTS = --root="$(DESTDIR)"
|
||||
endif
|
||||
|
||||
.PHONY: install
|
||||
|
||||
doc/qutebrowser.1.html:
|
||||
a2x -f manpage doc/qutebrowser.1.asciidoc
|
||||
|
||||
install: doc/qutebrowser.1.html
|
||||
$(PYTHON) setup.py install --root="$(DESTDIR)" --optimize=1
|
||||
$(PYTHON) setup.py install --prefix="$(PREFIX)" --optimize=1 $(SETUPTOOLSOPTS)
|
||||
install -Dm644 doc/qutebrowser.1 \
|
||||
"$(DESTDIR)/usr/share/man/man1/qutebrowser.1"
|
||||
"$(DESTDIR)$(PREFIX)/share/man/man1/qutebrowser.1"
|
||||
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" \
|
||||
"$(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 \
|
||||
"$(DESTDIR)/usr/share/icons/hicolor/scalable/apps/qutebrowser.svg"
|
||||
install -Dm755 -t "$(DESTDIR)/usr/share/qutebrowser/userscripts/" \
|
||||
"$(DESTDIR)$(PREFIX)/share/icons/hicolor/scalable/apps/qutebrowser.svg"
|
||||
install -Dm755 -t "$(DESTDIR)$(PREFIX)/share/qutebrowser/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 \
|
||||
scripts/testbrowser scripts/asciidoc2html.py scripts/setupcommon.py \
|
||||
scripts/link_pyqt.py,$(wildcard scripts/*))
|
||||
|
@ -33,7 +33,7 @@
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:zoom="1.7536248"
|
||||
inkscape:cx="376.55567"
|
||||
inkscape:cx="430.72917"
|
||||
inkscape:cy="268.64059"
|
||||
inkscape:document-units="px"
|
||||
inkscape:current-layer="layer1"
|
||||
@ -3710,7 +3710,7 @@
|
||||
style="font-weight:bold;font-size:10.66666698px;line-height:1.25;font-family:sans-serif;-inkscape-font-specification:'Sans Bold';fill:#000000;stroke-width:1.06666672"
|
||||
id="flowPara5701-9-2"><flowSpan
|
||||
style="font-weight:bold;font-family:sans-serif;-inkscape-font-specification:'Sans Bold';fill:#ff0000;stroke-width:1.06666672"
|
||||
id="flowSpan5705-5-1">(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"
|
||||
id="flowPara6196">tsh - toggle scripts for the current host (temporarily)</flowPara><flowPara
|
||||
style="font-size:10.66666698px;line-height:1.25;font-family:sans-serif;fill:#000000;stroke-width:1.06666672"
|
||||
|
Before Width: | Height: | Size: 181 KiB After Width: | Height: | Size: 181 KiB |
@ -15,7 +15,7 @@ def get_data_files():
|
||||
('../qutebrowser/img', 'img'),
|
||||
('../qutebrowser/javascript', 'javascript'),
|
||||
('../qutebrowser/html/doc', 'html/doc'),
|
||||
('../qutebrowser/git-commit-id', ''),
|
||||
('../qutebrowser/git-commit-id', '.'),
|
||||
('../qutebrowser/config/configdata.yml', 'config'),
|
||||
]
|
||||
|
||||
@ -58,14 +58,14 @@ exe = EXE(pyz,
|
||||
icon=icon,
|
||||
debug=False,
|
||||
strip=False,
|
||||
upx=True,
|
||||
upx=False,
|
||||
console=False )
|
||||
coll = COLLECT(exe,
|
||||
a.binaries,
|
||||
a.zipfiles,
|
||||
a.datas,
|
||||
strip=False,
|
||||
upx=True,
|
||||
upx=False,
|
||||
name='qutebrowser')
|
||||
|
||||
app = BUNDLE(coll,
|
||||
|
@ -3,7 +3,7 @@
|
||||
attrs==17.4.0
|
||||
flake8==3.5.0
|
||||
flake8-bugbear==18.2.0
|
||||
flake8-builtins==1.0.post0
|
||||
flake8-builtins==1.2.2
|
||||
flake8-comprehensions==1.4.1
|
||||
flake8-copyright==0.2.0
|
||||
flake8-debugger==3.1.0
|
||||
@ -11,15 +11,17 @@ flake8-deprecated==1.3
|
||||
flake8-docstrings==1.3.0
|
||||
flake8-future-import==0.4.4
|
||||
flake8-mock==0.3
|
||||
flake8-per-file-ignores==0.5
|
||||
flake8-per-file-ignores==0.6
|
||||
flake8-polyfill==1.0.2
|
||||
flake8-string-format==0.2.3
|
||||
flake8-tidy-imports==1.1.0
|
||||
flake8-tuple==0.2.13
|
||||
mccabe==0.6.1
|
||||
pathmatch==0.2.1
|
||||
pep8-naming==0.5.0
|
||||
pycodestyle==2.3.1
|
||||
pydocstyle==2.1.1
|
||||
pyflakes==1.6.0
|
||||
six==1.11.0
|
||||
snowballstemmer==1.2.1
|
||||
typing==3.6.4
|
||||
|
@ -3,6 +3,6 @@
|
||||
appdirs==1.4.3
|
||||
packaging==17.1
|
||||
pyparsing==2.2.0
|
||||
setuptools==38.5.1
|
||||
setuptools==39.0.1
|
||||
six==1.11.0
|
||||
wheel==0.30.0
|
||||
wheel==0.31.0
|
||||
|
@ -4,4 +4,4 @@ altgraph==0.15
|
||||
future==0.16.0
|
||||
macholib==1.9
|
||||
pefile==2017.11.5
|
||||
-e git+https://github.com/pyinstaller/pyinstaller.git@develop#egg=PyInstaller
|
||||
PyInstaller==3.3.1
|
||||
|
@ -1,4 +1 @@
|
||||
-e git+https://github.com/pyinstaller/pyinstaller.git@develop#egg=PyInstaller
|
||||
|
||||
# remove @commit-id for scm installs
|
||||
#@ replace: @.*# @develop#
|
||||
PyInstaller
|
||||
|
@ -3,16 +3,16 @@
|
||||
-e git+https://github.com/PyCQA/astroid.git#egg=astroid
|
||||
certifi==2018.1.18
|
||||
chardet==3.0.4
|
||||
github3.py==0.9.6
|
||||
github3.py==1.0.2
|
||||
idna==2.6
|
||||
isort==4.3.4
|
||||
lazy-object-proxy==1.3.1
|
||||
mccabe==0.6.1
|
||||
-e git+https://github.com/PyCQA/pylint.git#egg=pylint
|
||||
python-dateutil==2.7.2
|
||||
./scripts/dev/pylint_checkers
|
||||
requests==2.18.4
|
||||
six==1.11.0
|
||||
uritemplate==3.0.0
|
||||
uritemplate.py==3.0.2
|
||||
urllib3==1.22
|
||||
wrapt==1.10.11
|
||||
|
@ -1,18 +1,18 @@
|
||||
# This file is automatically generated by scripts/dev/recompile_requirements.py
|
||||
|
||||
astroid==1.6.1
|
||||
astroid==1.6.3
|
||||
certifi==2018.1.18
|
||||
chardet==3.0.4
|
||||
github3.py==0.9.6
|
||||
github3.py==1.0.2
|
||||
idna==2.6
|
||||
isort==4.3.4
|
||||
lazy-object-proxy==1.3.1
|
||||
mccabe==0.6.1
|
||||
pylint==1.8.2
|
||||
pylint==1.8.4
|
||||
python-dateutil==2.7.2
|
||||
./scripts/dev/pylint_checkers
|
||||
requests==2.18.4
|
||||
six==1.11.0
|
||||
uritemplate==3.0.0
|
||||
uritemplate.py==3.0.2
|
||||
urllib3==1.22
|
||||
wrapt==1.10.11
|
||||
|
@ -2,7 +2,7 @@
|
||||
|
||||
attrs==17.4.0
|
||||
beautifulsoup4==4.6.0
|
||||
cheroot==6.0.0
|
||||
cheroot==6.1.2
|
||||
click==6.7
|
||||
# colorama==0.3.9
|
||||
coverage==4.5.1
|
||||
@ -11,7 +11,7 @@ fields==5.0.0
|
||||
Flask==0.12.2
|
||||
glob2==0.6
|
||||
hunter==2.0.2
|
||||
hypothesis==3.48.0
|
||||
hypothesis==3.55.1
|
||||
itsdangerous==0.24
|
||||
# Jinja2==2.10
|
||||
Mako==1.0.7
|
||||
@ -20,15 +20,15 @@ more-itertools==4.1.0
|
||||
parse==1.8.2
|
||||
parse-type==0.4.2
|
||||
pluggy==0.6.0
|
||||
py==1.5.2
|
||||
py-cpuinfo==3.3.0
|
||||
pytest==3.4.1
|
||||
pytest-bdd==2.20.0
|
||||
py==1.5.3
|
||||
py-cpuinfo==4.0.0
|
||||
pytest==3.5.0
|
||||
pytest-bdd==2.21.0
|
||||
pytest-benchmark==3.1.1
|
||||
pytest-cov==2.5.1
|
||||
pytest-faulthandler==1.4.1
|
||||
pytest-faulthandler==1.5.0
|
||||
pytest-instafail==0.3.0
|
||||
pytest-mock==1.7.1
|
||||
pytest-mock==1.8.0
|
||||
pytest-qt==2.3.1
|
||||
pytest-repeat==0.4.1
|
||||
pytest-rerunfailures==4.0
|
||||
|
@ -1,7 +1,7 @@
|
||||
# This file is automatically generated by scripts/dev/recompile_requirements.py
|
||||
|
||||
pluggy==0.6.0
|
||||
py==1.5.2
|
||||
py==1.5.3
|
||||
six==1.11.0
|
||||
tox==2.9.1
|
||||
virtualenv==15.1.0
|
||||
tox==3.0.0
|
||||
virtualenv==15.2.0
|
||||
|
@ -1,4 +1 @@
|
||||
tox
|
||||
|
||||
# The latest tox release still depends on pluggy < 0.4...
|
||||
pluggy==0.4.0
|
||||
|
69
misc/userscripts/getbib
Executable 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")
|
@ -28,7 +28,7 @@
|
||||
if msg="$(task add "$title" "$*" 2>&1)"; then
|
||||
# annotate the new task with the url, send the output back to the browser
|
||||
task +LATEST annotate "$QUTE_URL"
|
||||
echo "message-info '$msg'" >> "$QUTE_FIFO"
|
||||
echo "message-info '$(echo "$msg" | head -n 1)'" >> "$QUTE_FIFO"
|
||||
else
|
||||
echo "message-error '$msg'" >> "$QUTE_FIFO"
|
||||
echo "message-error '$(echo "$msg" | head -n 1)'" >> "$QUTE_FIFO"
|
||||
fi
|
||||
|
@ -26,7 +26,7 @@ __copyright__ = "Copyright 2014-2018 Florian Bruhin (The Compiler)"
|
||||
__license__ = "GPL"
|
||||
__maintainer__ = __author__
|
||||
__email__ = "mail@qutebrowser.org"
|
||||
__version_info__ = (1, 2, 0)
|
||||
__version_info__ = (1, 2, 1)
|
||||
__version__ = '.'.join(str(e) for e in __version_info__)
|
||||
__description__ = "A keyboard-driven, vim-like browser based on PyQt5."
|
||||
|
||||
|
@ -340,7 +340,7 @@ def _open_startpage(win_id=None):
|
||||
for cur_win_id in list(window_ids): # Copying as the dict could change
|
||||
tabbed_browser = objreg.get('tabbed-browser', scope='window',
|
||||
window=cur_win_id)
|
||||
if tabbed_browser.count() == 0:
|
||||
if tabbed_browser.widget.count() == 0:
|
||||
log.init.debug("Opening start pages")
|
||||
for url in config.val.url.start_pages:
|
||||
tabbed_browser.tabopen(url)
|
||||
|
@ -94,14 +94,8 @@ class HostBlocker:
|
||||
_done_count: How many files have been read successfully.
|
||||
_local_hosts_file: The path to the blocked-hosts file.
|
||||
_config_hosts_file: The path to a blocked-hosts in ~/.config
|
||||
|
||||
Class attributes:
|
||||
WHITELISTED: Hosts which never should be blocked.
|
||||
"""
|
||||
|
||||
WHITELISTED = ('localhost', 'localhost.localdomain', 'broadcasthost',
|
||||
'local')
|
||||
|
||||
def __init__(self):
|
||||
self._blocked_hosts = set()
|
||||
self._config_blocked_hosts = set()
|
||||
@ -234,15 +228,13 @@ class HostBlocker:
|
||||
parts = line.split()
|
||||
if len(parts) == 1:
|
||||
# "one host per line" format
|
||||
host = parts[0]
|
||||
elif len(parts) == 2:
|
||||
# /etc/hosts format
|
||||
host = parts[1]
|
||||
hosts = [parts[0]]
|
||||
else:
|
||||
log.misc.error("Failed to parse: {!r}".format(line))
|
||||
return False
|
||||
# /etc/hosts format
|
||||
hosts = parts[1:]
|
||||
|
||||
if host not in self.WHITELISTED:
|
||||
for host in hosts:
|
||||
if '.' in host and not host.endswith('.localdomain'):
|
||||
self._blocked_hosts.add(host)
|
||||
|
||||
return True
|
||||
|
@ -114,6 +114,10 @@ class TabData:
|
||||
netrc_used = attr.ib(False)
|
||||
input_mode = attr.ib(usertypes.KeyMode.normal)
|
||||
|
||||
def should_show_icon(self):
|
||||
return (config.val.tabs.favicons.show == 'always' or
|
||||
config.val.tabs.favicons.show == 'pinned' and self.pinned)
|
||||
|
||||
|
||||
class AbstractAction:
|
||||
|
||||
@ -333,7 +337,14 @@ class AbstractZoom(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):
|
||||
super().__init__(parent)
|
||||
@ -439,6 +450,9 @@ class AbstractScroller(QObject):
|
||||
def to_point(self, point):
|
||||
raise NotImplementedError
|
||||
|
||||
def to_anchor(self, name):
|
||||
raise NotImplementedError
|
||||
|
||||
def delta(self, x=0, y=0):
|
||||
raise NotImplementedError
|
||||
|
||||
@ -665,8 +679,7 @@ class AbstractTab(QWidget):
|
||||
objreg.register('hintmanager', hintmanager, scope='tab',
|
||||
window=self.win_id, tab=self.tab_id)
|
||||
|
||||
self.predicted_navigation.connect(
|
||||
lambda url: self.title_changed.emit(url.toDisplayString()))
|
||||
self.predicted_navigation.connect(self._on_predicted_navigation)
|
||||
|
||||
def _set_widget(self, widget):
|
||||
# pylint: disable=protected-access
|
||||
@ -715,6 +728,14 @@ class AbstractTab(QWidget):
|
||||
evt.posted = True
|
||||
QApplication.postEvent(recipient, evt)
|
||||
|
||||
@pyqtSlot(QUrl)
|
||||
def _on_predicted_navigation(self, url):
|
||||
"""Adjust the title if we are going to visit an URL soon."""
|
||||
qtutils.ensure_valid(url)
|
||||
url_string = url.toDisplayString()
|
||||
log.webview.debug("Predicted navigation: {}".format(url_string))
|
||||
self.title_changed.emit(url_string)
|
||||
|
||||
@pyqtSlot(QUrl)
|
||||
def _on_url_changed(self, url):
|
||||
"""Update title when URL has changed and no title is available."""
|
||||
@ -815,11 +836,12 @@ class AbstractTab(QWidget):
|
||||
def load_status(self):
|
||||
return self._load_status
|
||||
|
||||
def _openurl_prepare(self, url):
|
||||
def _openurl_prepare(self, url, *, predict=True):
|
||||
qtutils.ensure_valid(url)
|
||||
if predict:
|
||||
self.predicted_navigation.emit(url)
|
||||
|
||||
def openurl(self, url):
|
||||
def openurl(self, url, *, predict=True):
|
||||
raise NotImplementedError
|
||||
|
||||
def reload(self, *, force=False):
|
||||
|
@ -53,7 +53,6 @@ class CommandDispatcher:
|
||||
cmdutils.register() decorators are run, currentWidget() will return None.
|
||||
|
||||
Attributes:
|
||||
_editor: The ExternalEditor object.
|
||||
_win_id: The window ID the CommandDispatcher is associated with.
|
||||
_tabbed_browser: The TabbedBrowser used.
|
||||
"""
|
||||
@ -73,16 +72,16 @@ class CommandDispatcher:
|
||||
|
||||
def _count(self):
|
||||
"""Convenience method to get the widget count."""
|
||||
return self._tabbed_browser.count()
|
||||
return self._tabbed_browser.widget.count()
|
||||
|
||||
def _set_current_index(self, idx):
|
||||
"""Convenience method to set the current widget index."""
|
||||
cmdutils.check_overflow(idx, 'int')
|
||||
self._tabbed_browser.setCurrentIndex(idx)
|
||||
self._tabbed_browser.widget.setCurrentIndex(idx)
|
||||
|
||||
def _current_index(self):
|
||||
"""Convenience method to get the current widget index."""
|
||||
return self._tabbed_browser.currentIndex()
|
||||
return self._tabbed_browser.widget.currentIndex()
|
||||
|
||||
def _current_url(self):
|
||||
"""Convenience method to get the current url."""
|
||||
@ -101,7 +100,7 @@ class CommandDispatcher:
|
||||
|
||||
def _current_widget(self):
|
||||
"""Get the currently active widget from a command."""
|
||||
widget = self._tabbed_browser.currentWidget()
|
||||
widget = self._tabbed_browser.widget.currentWidget()
|
||||
if widget is None:
|
||||
raise cmdexc.CommandError("No WebView available yet!")
|
||||
return widget
|
||||
@ -147,10 +146,10 @@ class CommandDispatcher:
|
||||
None if no widget was found.
|
||||
"""
|
||||
if count is None:
|
||||
return self._tabbed_browser.currentWidget()
|
||||
return self._tabbed_browser.widget.currentWidget()
|
||||
elif 1 <= count <= self._count():
|
||||
cmdutils.check_overflow(count + 1, 'int')
|
||||
return self._tabbed_browser.widget(count - 1)
|
||||
return self._tabbed_browser.widget.widget(count - 1)
|
||||
else:
|
||||
return None
|
||||
|
||||
@ -163,7 +162,7 @@ class CommandDispatcher:
|
||||
if not show_error:
|
||||
return
|
||||
raise cmdexc.CommandError("No last focused tab!")
|
||||
idx = self._tabbed_browser.indexOf(tab)
|
||||
idx = self._tabbed_browser.widget.indexOf(tab)
|
||||
if idx == -1:
|
||||
raise cmdexc.CommandError("Last focused tab vanished!")
|
||||
self._set_current_index(idx)
|
||||
@ -212,7 +211,7 @@ class CommandDispatcher:
|
||||
what's configured in 'tabs.select_on_remove'.
|
||||
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_,
|
||||
opposite)
|
||||
|
||||
@ -264,7 +263,7 @@ class CommandDispatcher:
|
||||
return
|
||||
|
||||
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',
|
||||
maxsplit=0, scope='window')
|
||||
@ -483,7 +482,8 @@ class CommandDispatcher:
|
||||
"""
|
||||
cmdutils.check_exclusive((bg, window), 'bw')
|
||||
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:
|
||||
history = curtab.history.serialize()
|
||||
except browsertab.WebTabError as e:
|
||||
@ -499,18 +499,18 @@ class CommandDispatcher:
|
||||
newtab = new_tabbed_browser.tabopen(background=bg)
|
||||
new_tabbed_browser = objreg.get('tabbed-browser', scope='window',
|
||||
window=newtab.win_id)
|
||||
idx = new_tabbed_browser.indexOf(newtab)
|
||||
idx = new_tabbed_browser.widget.indexOf(newtab)
|
||||
|
||||
new_tabbed_browser.set_page_title(idx, cur_title)
|
||||
if config.val.tabs.favicons.show:
|
||||
new_tabbed_browser.setTabIcon(idx, curtab.icon())
|
||||
new_tabbed_browser.widget.set_page_title(idx, cur_title)
|
||||
if curtab.data.should_show_icon():
|
||||
new_tabbed_browser.widget.setTabIcon(idx, curtab.icon())
|
||||
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.history.deserialize(history)
|
||||
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
|
||||
|
||||
@cmdutils.register(instance='command-dispatcher', scope='window')
|
||||
@ -768,6 +768,15 @@ class CommandDispatcher:
|
||||
|
||||
self._current_widget().scroller.to_perc(x, y)
|
||||
|
||||
@cmdutils.register(instance='command-dispatcher', scope='window')
|
||||
def scroll_to_anchor(self, name):
|
||||
"""Scroll to the given anchor in the document.
|
||||
|
||||
Args:
|
||||
name: The anchor to scroll to.
|
||||
"""
|
||||
self._current_widget().scroller.to_anchor(name)
|
||||
|
||||
@cmdutils.register(instance='command-dispatcher', scope='window')
|
||||
@cmdutils.argument('count', count=True)
|
||||
@cmdutils.argument('top_navigate', metavar='ACTION',
|
||||
@ -846,7 +855,7 @@ class CommandDispatcher:
|
||||
keep: Stay in visual mode after yanking the selection.
|
||||
"""
|
||||
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':
|
||||
port = self._current_url().port()
|
||||
s = '{}://{}{}'.format(self._current_url().scheme(),
|
||||
@ -958,7 +967,7 @@ class CommandDispatcher:
|
||||
force: Avoid confirmation for pinned tabs.
|
||||
"""
|
||||
cmdutils.check_exclusive((prev, next_), 'pn')
|
||||
cur_idx = self._tabbed_browser.currentIndex()
|
||||
cur_idx = self._tabbed_browser.widget.currentIndex()
|
||||
assert cur_idx != -1
|
||||
|
||||
def _to_close(i):
|
||||
@ -1013,7 +1022,7 @@ class CommandDispatcher:
|
||||
elif config.val.tabs.wrap:
|
||||
self._set_current_index(newidx % self._count())
|
||||
else:
|
||||
raise cmdexc.CommandError("First tab")
|
||||
log.webview.debug("First tab")
|
||||
|
||||
@cmdutils.register(instance='command-dispatcher', scope='window')
|
||||
@cmdutils.argument('count', count=True)
|
||||
@ -1033,7 +1042,7 @@ class CommandDispatcher:
|
||||
elif config.val.tabs.wrap:
|
||||
self._set_current_index(newidx % self._count())
|
||||
else:
|
||||
raise cmdexc.CommandError("Last tab")
|
||||
log.webview.debug("Last tab")
|
||||
|
||||
def _resolve_buffer_index(self, index):
|
||||
"""Resolve a buffer index to the tabbedbrowser and tab.
|
||||
@ -1075,11 +1084,11 @@ class CommandDispatcher:
|
||||
|
||||
tabbed_browser = objreg.get('tabbed-browser', scope='window',
|
||||
window=win_id)
|
||||
if not 0 < idx <= tabbed_browser.count():
|
||||
if not 0 < idx <= tabbed_browser.widget.count():
|
||||
raise cmdexc.CommandError(
|
||||
"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',
|
||||
maxsplit=0)
|
||||
@ -1107,10 +1116,10 @@ class CommandDispatcher:
|
||||
|
||||
tabbed_browser, tab = self._resolve_buffer_index(index)
|
||||
|
||||
window = tabbed_browser.window()
|
||||
window = tabbed_browser.widget.window()
|
||||
window.activateWindow()
|
||||
window.raise_()
|
||||
tabbed_browser.setCurrentWidget(tab)
|
||||
tabbed_browser.widget.setCurrentWidget(tab)
|
||||
|
||||
@cmdutils.register(instance='command-dispatcher', scope='window')
|
||||
@cmdutils.argument('index', choices=['last'])
|
||||
@ -1194,7 +1203,7 @@ class CommandDispatcher:
|
||||
cur_idx = self._current_index()
|
||||
cmdutils.check_overflow(cur_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',
|
||||
maxsplit=0, no_replace_variables=True)
|
||||
@ -1278,10 +1287,10 @@ class CommandDispatcher:
|
||||
|
||||
idx = self._current_index()
|
||||
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!
|
||||
tab = self._tabbed_browser.currentWidget()
|
||||
tab = self._tabbed_browser.widget.currentWidget()
|
||||
|
||||
try:
|
||||
url = self._tabbed_browser.current_url()
|
||||
@ -1639,7 +1648,7 @@ class CommandDispatcher:
|
||||
|
||||
ed = editor.ExternalEditor(watch=True, parent=self._tabbed_browser)
|
||||
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(
|
||||
objreg.last_focused_window(), alert=False))
|
||||
ed.edit(text, caret_position)
|
||||
@ -1654,7 +1663,7 @@ class CommandDispatcher:
|
||||
tab = self._current_widget()
|
||||
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.
|
||||
|
||||
Callback for GUIProcess when the edited text was updated.
|
||||
@ -1667,8 +1676,10 @@ class CommandDispatcher:
|
||||
elem.set_value(text)
|
||||
except webelem.OrphanedError as e:
|
||||
message.error('Edited element vanished')
|
||||
ed.backup()
|
||||
except webelem.Error as e:
|
||||
raise cmdexc.CommandError(str(e))
|
||||
message.error(str(e))
|
||||
ed.backup()
|
||||
|
||||
@cmdutils.register(instance='command-dispatcher', maxsplit=0,
|
||||
scope='window')
|
||||
@ -2217,5 +2228,5 @@ class CommandDispatcher:
|
||||
pass
|
||||
return
|
||||
|
||||
window = self._tabbed_browser.window()
|
||||
window = self._tabbed_browser.widget.window()
|
||||
window.setWindowState(window.windowState() ^ Qt.WindowFullScreen)
|
||||
|
@ -30,7 +30,8 @@ import textwrap
|
||||
import attr
|
||||
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.browser import downloads
|
||||
|
||||
@ -91,7 +92,7 @@ class GreasemonkeyScript:
|
||||
props = ""
|
||||
script = cls(re.findall(cls.PROPS_REGEX, props), source)
|
||||
script.script_meta = props
|
||||
if not props:
|
||||
if not script.includes:
|
||||
script.includes = ['*']
|
||||
return script
|
||||
|
||||
@ -104,11 +105,12 @@ class GreasemonkeyScript:
|
||||
browser's debugger/inspector will not match up to the line
|
||||
numbers in the source script directly.
|
||||
"""
|
||||
return jinja.js_environment.get_template(
|
||||
'greasemonkey_wrapper.js').render(
|
||||
scriptName="/".join([self.namespace or '', self.name]),
|
||||
template = jinja.js_environment.get_template('greasemonkey_wrapper.js')
|
||||
return template.render(
|
||||
scriptName=javascript.string_escape(
|
||||
"/".join([self.namespace or '', self.name])),
|
||||
scriptInfo=self._meta_json(),
|
||||
scriptMeta=self.script_meta,
|
||||
scriptMeta=javascript.string_escape(self.script_meta),
|
||||
scriptSource=self._code)
|
||||
|
||||
def _meta_json(self):
|
||||
|
@ -682,7 +682,7 @@ class HintManager(QObject):
|
||||
"""
|
||||
tabbed_browser = objreg.get('tabbed-browser', scope='window',
|
||||
window=self._win_id)
|
||||
tab = tabbed_browser.currentWidget()
|
||||
tab = tabbed_browser.widget.currentWidget()
|
||||
if tab is None:
|
||||
raise cmdexc.CommandError("No WebView available yet!")
|
||||
|
||||
|
@ -162,6 +162,7 @@ class DownloadItem(downloads.AbstractDownloadItem):
|
||||
QTimer.singleShot(0, lambda: self._die(reply.errorString()))
|
||||
|
||||
def _do_cancel(self):
|
||||
self._read_timer.stop()
|
||||
if self._reply is not None:
|
||||
self._reply.finished.disconnect(self._on_reply_finished)
|
||||
self._reply.abort()
|
||||
|
@ -76,11 +76,11 @@ class SignalFilter(QObject):
|
||||
tabbed_browser = objreg.get('tabbed-browser', scope='window',
|
||||
window=self._win_id)
|
||||
try:
|
||||
tabidx = tabbed_browser.indexOf(tab)
|
||||
tabidx = tabbed_browser.widget.indexOf(tab)
|
||||
except RuntimeError:
|
||||
# The tab has been deleted already
|
||||
return
|
||||
if tabidx == tabbed_browser.currentIndex():
|
||||
if tabidx == tabbed_browser.widget.currentIndex():
|
||||
if log_signal:
|
||||
log.signals.debug("emitting: {} (tab {})".format(
|
||||
debug.dbg_signal(signal, args), tabidx))
|
||||
|
@ -22,7 +22,7 @@
|
||||
import os
|
||||
|
||||
from PyQt5.QtCore import QUrl
|
||||
from PyQt5.QtWebEngineWidgets import QWebEngineView
|
||||
from PyQt5.QtWebEngineWidgets import QWebEngineView, QWebEngineSettings
|
||||
|
||||
from qutebrowser.browser import inspector
|
||||
|
||||
@ -35,6 +35,8 @@ class WebEngineInspector(inspector.AbstractWebInspector):
|
||||
super().__init__(parent)
|
||||
self.port = None
|
||||
view = QWebEngineView()
|
||||
settings = view.settings()
|
||||
settings.setAttribute(QWebEngineSettings.JavascriptEnabled, True)
|
||||
self._set_widget(view)
|
||||
|
||||
def inspect(self, _page):
|
||||
|
@ -26,16 +26,12 @@ Module attributes:
|
||||
|
||||
import os
|
||||
|
||||
import sip
|
||||
from PyQt5.QtGui import QFont
|
||||
from PyQt5.QtWebEngineWidgets import (QWebEngineSettings, QWebEngineProfile,
|
||||
QWebEngineScript)
|
||||
from PyQt5.QtWebEngineWidgets import QWebEngineSettings, QWebEngineProfile
|
||||
|
||||
from qutebrowser.browser import shared
|
||||
from qutebrowser.browser.webengine import spell
|
||||
from qutebrowser.config import config, websettings
|
||||
from qutebrowser.utils import (utils, standarddir, javascript, qtutils,
|
||||
message, log, objreg)
|
||||
from qutebrowser.utils import utils, standarddir, qtutils, message, log
|
||||
|
||||
# The default QWebEngineProfile
|
||||
default_profile = None
|
||||
@ -169,61 +165,36 @@ class WebEngineSettings(websettings.AbstractSettings):
|
||||
self._ATTRIBUTES[name] = [value]
|
||||
|
||||
|
||||
def _init_stylesheet(profile):
|
||||
"""Initialize custom stylesheets.
|
||||
class ProfileSetter:
|
||||
|
||||
Partially inspired by QupZilla:
|
||||
https://github.com/QupZilla/qupzilla/blob/v2.0/src/lib/app/mainapplication.cpp#L1063-L1101
|
||||
"""
|
||||
old_script = profile.scripts().findScript('_qute_stylesheet')
|
||||
if not old_script.isNull():
|
||||
profile.scripts().remove(old_script)
|
||||
"""Helper to set various settings on a profile."""
|
||||
|
||||
css = shared.get_user_stylesheet()
|
||||
source = '\n'.join([
|
||||
'"use strict";',
|
||||
'window._qutebrowser = window._qutebrowser || {};',
|
||||
utils.read_file('javascript/stylesheet.js'),
|
||||
javascript.assemble('stylesheet', 'set_css', css),
|
||||
])
|
||||
def __init__(self, profile):
|
||||
self._profile = profile
|
||||
|
||||
script = QWebEngineScript()
|
||||
script.setName('_qute_stylesheet')
|
||||
script.setInjectionPoint(QWebEngineScript.DocumentCreation)
|
||||
script.setWorldId(QWebEngineScript.ApplicationWorld)
|
||||
script.setRunsOnSubFrames(True)
|
||||
script.setSourceCode(source)
|
||||
profile.scripts().insert(script)
|
||||
def init_profile(self):
|
||||
"""Initialize settings on the given profile."""
|
||||
self.set_http_headers()
|
||||
self.set_http_cache_size()
|
||||
self._profile.settings().setAttribute(
|
||||
QWebEngineSettings.FullScreenSupportEnabled, True)
|
||||
if qtutils.version_check('5.8'):
|
||||
self._profile.setSpellCheckEnabled(True)
|
||||
self.set_dictionary_language()
|
||||
|
||||
|
||||
def _update_stylesheet():
|
||||
"""Update the custom stylesheet in existing tabs."""
|
||||
css = shared.get_user_stylesheet()
|
||||
code = javascript.assemble('stylesheet', 'set_css', css)
|
||||
for win_id, window in objreg.window_registry.items():
|
||||
# We could be in the middle of destroying a window here
|
||||
if sip.isdeleted(window):
|
||||
continue
|
||||
tab_registry = objreg.get('tab-registry', scope='window',
|
||||
window=win_id)
|
||||
for tab in tab_registry.values():
|
||||
tab.run_js_async(code)
|
||||
|
||||
|
||||
def _set_http_headers(profile):
|
||||
def set_http_headers(self):
|
||||
"""Set the user agent and accept-language for the given profile.
|
||||
|
||||
We override those per request in the URL interceptor (to allow for
|
||||
per-domain values), but this one still gets used for things like
|
||||
window.navigator.userAgent/.languages in JS.
|
||||
"""
|
||||
profile.setHttpUserAgent(config.val.content.headers.user_agent)
|
||||
self._profile.setHttpUserAgent(config.val.content.headers.user_agent)
|
||||
accept_language = config.val.content.headers.accept_language
|
||||
if accept_language is not None:
|
||||
profile.setHttpAcceptLanguage(accept_language)
|
||||
self._profile.setHttpAcceptLanguage(accept_language)
|
||||
|
||||
|
||||
def _set_http_cache_size(profile):
|
||||
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:
|
||||
@ -232,70 +203,54 @@ def _set_http_cache_size(profile):
|
||||
size = qtutils.check_overflow(size, 'int', fatal=False)
|
||||
|
||||
# 0: automatically managed by QtWebEngine
|
||||
profile.setHttpCacheMaximumSize(size)
|
||||
self._profile.setHttpCacheMaximumSize(size)
|
||||
|
||||
|
||||
def _set_persistent_cookie_policy(profile):
|
||||
def set_persistent_cookie_policy(self):
|
||||
"""Set the HTTP Cookie size for the given profile."""
|
||||
assert not self._profile.isOffTheRecord()
|
||||
if config.val.content.cookies.store:
|
||||
value = QWebEngineProfile.AllowPersistentCookies
|
||||
else:
|
||||
value = QWebEngineProfile.NoPersistentCookies
|
||||
profile.setPersistentCookiesPolicy(value)
|
||||
self._profile.setPersistentCookiesPolicy(value)
|
||||
|
||||
|
||||
def _set_dictionary_language(profile, warn=True):
|
||||
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))
|
||||
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)
|
||||
self._profile.setSpellCheckLanguages(filenames)
|
||||
|
||||
|
||||
def _update_settings(option):
|
||||
"""Update global settings when qwebsettings changed."""
|
||||
global_settings.update_setting(option)
|
||||
|
||||
if option in ['scrolling.bar', 'content.user_stylesheets']:
|
||||
_init_stylesheet(default_profile)
|
||||
_init_stylesheet(private_profile)
|
||||
_update_stylesheet()
|
||||
elif option in ['content.headers.user_agent',
|
||||
if option in ['content.headers.user_agent',
|
||||
'content.headers.accept_language']:
|
||||
_set_http_headers(default_profile)
|
||||
_set_http_headers(private_profile)
|
||||
default_profile.setter.set_http_headers()
|
||||
private_profile.setter.set_http_headers()
|
||||
elif option == 'content.cache.size':
|
||||
_set_http_cache_size(default_profile)
|
||||
_set_http_cache_size(private_profile)
|
||||
default_profile.setter.set_http_cache_size()
|
||||
private_profile.setter.set_http_cache_size()
|
||||
elif (option == 'content.cookies.store' and
|
||||
# https://bugreports.qt.io/browse/QTBUG-58650
|
||||
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.
|
||||
elif option == 'spellcheck.languages':
|
||||
_set_dictionary_language(default_profile)
|
||||
_set_dictionary_language(private_profile, warn=False)
|
||||
|
||||
|
||||
def _init_profile(profile):
|
||||
"""Init the given profile."""
|
||||
_init_stylesheet(profile)
|
||||
_set_http_headers(profile)
|
||||
_set_http_cache_size(profile)
|
||||
profile.settings().setAttribute(
|
||||
QWebEngineSettings.FullScreenSupportEnabled, True)
|
||||
if qtutils.version_check('5.8'):
|
||||
profile.setSpellCheckEnabled(True)
|
||||
_set_dictionary_language(profile)
|
||||
default_profile.setter.set_dictionary_language()
|
||||
private_profile.setter.set_dictionary_language(warn=False)
|
||||
|
||||
|
||||
def _init_profiles():
|
||||
@ -303,53 +258,18 @@ def _init_profiles():
|
||||
global default_profile, private_profile
|
||||
|
||||
default_profile = QWebEngineProfile.defaultProfile()
|
||||
default_profile.setter = ProfileSetter(default_profile)
|
||||
default_profile.setCachePath(
|
||||
os.path.join(standarddir.cache(), 'webengine'))
|
||||
default_profile.setPersistentStoragePath(
|
||||
os.path.join(standarddir.data(), 'webengine'))
|
||||
_init_profile(default_profile)
|
||||
_set_persistent_cookie_policy(default_profile)
|
||||
default_profile.setter.init_profile()
|
||||
default_profile.setter.set_persistent_cookie_policy()
|
||||
|
||||
private_profile = QWebEngineProfile()
|
||||
private_profile.setter = ProfileSetter(private_profile)
|
||||
assert private_profile.isOffTheRecord()
|
||||
_init_profile(private_profile)
|
||||
|
||||
|
||||
def inject_userscripts():
|
||||
"""Register user JavaScript files with the global profiles."""
|
||||
# The Greasemonkey metadata block support in QtWebEngine only starts at
|
||||
# Qt 5.8. With 5.7.1, we need to inject the scripts ourselves in response
|
||||
# to urlChanged.
|
||||
if not qtutils.version_check('5.8'):
|
||||
return
|
||||
|
||||
# Since we are inserting scripts into profile.scripts they won't
|
||||
# just get replaced by new gm scripts like if we were injecting them
|
||||
# ourselves so we need to remove all gm scripts, while not removing
|
||||
# any other stuff that might have been added. Like the one for
|
||||
# stylesheets.
|
||||
greasemonkey = objreg.get('greasemonkey')
|
||||
for profile in [default_profile, private_profile]:
|
||||
scripts = profile.scripts()
|
||||
for script in scripts.toList():
|
||||
if script.name().startswith("GM-"):
|
||||
log.greasemonkey.debug('Removing script: {}'
|
||||
.format(script.name()))
|
||||
removed = scripts.remove(script)
|
||||
assert removed, script.name()
|
||||
|
||||
# Then add the new scripts.
|
||||
for script in greasemonkey.all_scripts():
|
||||
# @run-at (and @include/@exclude/@match) is parsed by
|
||||
# QWebEngineScript.
|
||||
new_script = QWebEngineScript()
|
||||
new_script.setWorldId(QWebEngineScript.MainWorld)
|
||||
new_script.setSourceCode(script.code())
|
||||
new_script.setName("GM-{}".format(script.name))
|
||||
new_script.setRunsOnSubFrames(script.runs_on_sub_frames)
|
||||
log.greasemonkey.debug('adding script: {}'
|
||||
.format(new_script.name()))
|
||||
scripts.insert(new_script)
|
||||
private_profile.setter.init_profile()
|
||||
|
||||
|
||||
def init(args):
|
||||
|
@ -33,7 +33,7 @@ from PyQt5.QtNetwork import QAuthenticator
|
||||
from PyQt5.QtWidgets import QApplication
|
||||
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.webengine import (webview, webengineelem, tabhistory,
|
||||
interceptor, webenginequtescheme,
|
||||
@ -73,10 +73,6 @@ def init():
|
||||
download_manager.install(webenginesettings.private_profile)
|
||||
objreg.register('webengine-download-manager', download_manager)
|
||||
|
||||
greasemonkey = objreg.get('greasemonkey')
|
||||
greasemonkey.scripts_reloaded.connect(webenginesettings.inject_userscripts)
|
||||
webenginesettings.inject_userscripts()
|
||||
|
||||
|
||||
# Mapping worlds from usertypes.JsWorld to QWebEngineScript world IDs.
|
||||
_JS_WORLD_MAP = {
|
||||
@ -234,7 +230,14 @@ class WebEngineCaret(browsertab.AbstractCaret):
|
||||
|
||||
self._tab.run_js_async(
|
||||
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)
|
||||
def _on_mode_left(self, mode):
|
||||
@ -301,7 +304,7 @@ class WebEngineCaret(browsertab.AbstractCaret):
|
||||
self._js_call('moveToEndOfDocument')
|
||||
|
||||
def toggle_selection(self):
|
||||
self._js_call('toggleSelection')
|
||||
self._js_call('toggleSelection', self.selection_toggled.emit)
|
||||
|
||||
def drop_selection(self):
|
||||
self._js_call('dropSelection')
|
||||
@ -356,9 +359,8 @@ class WebEngineCaret(browsertab.AbstractCaret):
|
||||
self._tab.run_js_async(js_code, lambda jsret:
|
||||
self._follow_selected_cb(jsret, tab))
|
||||
|
||||
def _js_call(self, command):
|
||||
self._tab.run_js_async(
|
||||
javascript.assemble('caret', command))
|
||||
def _js_call(self, command, callback=None):
|
||||
self._tab.run_js_async(javascript.assemble('caret', command), callback)
|
||||
|
||||
|
||||
class WebEngineScroller(browsertab.AbstractScroller):
|
||||
@ -379,7 +381,7 @@ class WebEngineScroller(browsertab.AbstractScroller):
|
||||
|
||||
def _repeated_key_press(self, key, count=1, modifier=Qt.NoModifier):
|
||||
"""Send count fake key presses to this scroller's WebEngineTab."""
|
||||
for _ in range(min(count, 5000)):
|
||||
for _ in range(min(count, 1000)):
|
||||
self._tab.key_press(key, modifier)
|
||||
|
||||
@pyqtSlot(QPointF)
|
||||
@ -432,6 +434,11 @@ class WebEngineScroller(browsertab.AbstractScroller):
|
||||
js_code = javascript.assemble('window', 'scroll', point.x(), point.y())
|
||||
self._tab.run_js_async(js_code)
|
||||
|
||||
def to_anchor(self, name):
|
||||
url = self._tab.url()
|
||||
url.setFragment(name)
|
||||
self._tab.openurl(url)
|
||||
|
||||
def delta(self, x=0, y=0):
|
||||
self._tab.run_js_async(javascript.assemble('window', 'scrollBy', x, y))
|
||||
|
||||
@ -506,6 +513,9 @@ class WebEngineHistory(browsertab.AbstractHistory):
|
||||
return qtutils.deserialize(data, self._history)
|
||||
|
||||
def load_items(self, items):
|
||||
if items:
|
||||
self._tab.predicted_navigation.emit(items[-1].url)
|
||||
|
||||
stream, _data, cur_data = tabhistory.serialize(items)
|
||||
qtutils.deserialize_stream(stream, self._history)
|
||||
|
||||
@ -627,30 +637,122 @@ class WebEngineTab(browsertab.AbstractTab):
|
||||
self._set_widget(widget)
|
||||
self._connect_signals()
|
||||
self.backend = usertypes.Backend.QtWebEngine
|
||||
self._init_js()
|
||||
self._child_event_filter = None
|
||||
self._saved_zoom = None
|
||||
self._reload_url = None
|
||||
config.instance.changed.connect(self._on_config_changed)
|
||||
self._init_js()
|
||||
|
||||
@pyqtSlot(str)
|
||||
def _on_config_changed(self, option):
|
||||
if option in ['scrolling.bar', 'content.user_stylesheets']:
|
||||
self._init_stylesheet()
|
||||
self._update_stylesheet()
|
||||
|
||||
def _update_stylesheet(self):
|
||||
"""Update the custom stylesheet in existing tabs."""
|
||||
css = shared.get_user_stylesheet()
|
||||
code = javascript.assemble('stylesheet', 'set_css', css)
|
||||
self.run_js_async(code)
|
||||
|
||||
def _inject_early_js(self, name, js_code, *,
|
||||
world=QWebEngineScript.ApplicationWorld,
|
||||
subframes=False):
|
||||
"""Inject the given script to run early on a page load.
|
||||
|
||||
This runs the script both on DocumentCreation and DocumentReady as on
|
||||
some internal pages, DocumentCreation will not work.
|
||||
|
||||
That is a WORKAROUND for https://bugreports.qt.io/browse/QTBUG-66011
|
||||
"""
|
||||
scripts = self._widget.page().scripts()
|
||||
for injection in ['creation', 'ready']:
|
||||
injection_points = {
|
||||
'creation': QWebEngineScript.DocumentCreation,
|
||||
'ready': QWebEngineScript.DocumentReady,
|
||||
}
|
||||
script = QWebEngineScript()
|
||||
script.setInjectionPoint(injection_points[injection])
|
||||
script.setSourceCode(js_code)
|
||||
script.setWorldId(world)
|
||||
script.setRunsOnSubFrames(subframes)
|
||||
script.setName('_qute_{}_{}'.format(name, injection))
|
||||
scripts.insert(script)
|
||||
|
||||
def _remove_early_js(self, name):
|
||||
"""Remove an early QWebEngineScript."""
|
||||
scripts = self._widget.page().scripts()
|
||||
for injection in ['creation', 'ready']:
|
||||
full_name = '_qute_{}_{}'.format(name, injection)
|
||||
script = scripts.findScript(full_name)
|
||||
if not script.isNull():
|
||||
scripts.remove(script)
|
||||
|
||||
def _init_js(self):
|
||||
js_code = '\n'.join([
|
||||
'"use strict";',
|
||||
'window._qutebrowser = window._qutebrowser || {};',
|
||||
"""Initialize global qutebrowser JavaScript."""
|
||||
js_code = javascript.wrap_global(
|
||||
'scripts',
|
||||
utils.read_file('javascript/scroll.js'),
|
||||
utils.read_file('javascript/webelem.js'),
|
||||
utils.read_file('javascript/caret.js'),
|
||||
])
|
||||
script = QWebEngineScript()
|
||||
# We can't use DocumentCreation here as WORKAROUND for
|
||||
# https://bugreports.qt.io/browse/QTBUG-66011
|
||||
script.setInjectionPoint(QWebEngineScript.DocumentReady)
|
||||
script.setSourceCode(js_code)
|
||||
)
|
||||
# FIXME:qtwebengine what about subframes=True?
|
||||
self._inject_early_js('js', js_code, subframes=True)
|
||||
self._init_stylesheet()
|
||||
|
||||
page = self._widget.page()
|
||||
script.setWorldId(QWebEngineScript.ApplicationWorld)
|
||||
greasemonkey = objreg.get('greasemonkey')
|
||||
greasemonkey.scripts_reloaded.connect(self._inject_userscripts)
|
||||
self._inject_userscripts()
|
||||
|
||||
# FIXME:qtwebengine what about runsOnSubFrames?
|
||||
page.scripts().insert(script)
|
||||
def _init_stylesheet(self):
|
||||
"""Initialize custom stylesheets.
|
||||
|
||||
Partially inspired by QupZilla:
|
||||
https://github.com/QupZilla/qupzilla/blob/v2.0/src/lib/app/mainapplication.cpp#L1063-L1101
|
||||
"""
|
||||
self._remove_early_js('stylesheet')
|
||||
css = shared.get_user_stylesheet()
|
||||
js_code = javascript.wrap_global(
|
||||
'stylesheet',
|
||||
utils.read_file('javascript/stylesheet.js'),
|
||||
javascript.assemble('stylesheet', 'set_css', css),
|
||||
)
|
||||
self._inject_early_js('stylesheet', js_code, subframes=True)
|
||||
|
||||
def _inject_userscripts(self):
|
||||
"""Register user JavaScript files with the global profiles."""
|
||||
# The Greasemonkey metadata block support in QtWebEngine only starts at
|
||||
# Qt 5.8. With 5.7.1, we need to inject the scripts ourselves in
|
||||
# response to urlChanged.
|
||||
if not qtutils.version_check('5.8'):
|
||||
return
|
||||
|
||||
# Since we are inserting scripts into profile.scripts they won't
|
||||
# just get replaced by new gm scripts like if we were injecting them
|
||||
# ourselves so we need to remove all gm scripts, while not removing
|
||||
# any other stuff that might have been added. Like the one for
|
||||
# stylesheets.
|
||||
greasemonkey = objreg.get('greasemonkey')
|
||||
scripts = self._widget.page().scripts()
|
||||
for script in scripts.toList():
|
||||
if script.name().startswith("GM-"):
|
||||
log.greasemonkey.debug('Removing script: {}'
|
||||
.format(script.name()))
|
||||
removed = scripts.remove(script)
|
||||
assert removed, script.name()
|
||||
|
||||
# Then add the new scripts.
|
||||
for script in greasemonkey.all_scripts():
|
||||
# @run-at (and @include/@exclude/@match) is parsed by
|
||||
# QWebEngineScript.
|
||||
new_script = QWebEngineScript()
|
||||
new_script.setWorldId(QWebEngineScript.MainWorld)
|
||||
new_script.setSourceCode(script.code())
|
||||
new_script.setName("GM-{}".format(script.name))
|
||||
new_script.setRunsOnSubFrames(script.runs_on_sub_frames)
|
||||
log.greasemonkey.debug('adding script: {}'
|
||||
.format(new_script.name()))
|
||||
scripts.insert(new_script)
|
||||
|
||||
def _install_event_filter(self):
|
||||
self._widget.focusProxy().installEventFilter(self._mouse_event_filter)
|
||||
@ -669,9 +771,15 @@ class WebEngineTab(browsertab.AbstractTab):
|
||||
self.zoom.set_factor(self._saved_zoom)
|
||||
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._openurl_prepare(url)
|
||||
self._openurl_prepare(url, predict=predict)
|
||||
self._widget.load(url)
|
||||
|
||||
def url(self, requested=False):
|
||||
@ -706,7 +814,6 @@ class WebEngineTab(browsertab.AbstractTab):
|
||||
self._widget.shutdown()
|
||||
|
||||
def reload(self, *, force=False):
|
||||
self.predicted_navigation.emit(self.url())
|
||||
if force:
|
||||
action = QWebEnginePage.ReloadAndBypassCache
|
||||
else:
|
||||
@ -915,10 +1022,10 @@ class WebEngineTab(browsertab.AbstractTab):
|
||||
if ok and self._reload_url is not None:
|
||||
# WORKAROUND for https://bugreports.qt.io/browse/QTBUG-66656
|
||||
log.config.debug(
|
||||
"Reloading {} because of config change".format(
|
||||
"Loading {} again because of config change".format(
|
||||
self._reload_url.toDisplayString()))
|
||||
QTimer.singleShot(100, lambda url=self._reload_url:
|
||||
self.openurl(url))
|
||||
self.openurl(url, predict=False))
|
||||
self._reload_url = None
|
||||
|
||||
if not qtutils.version_check('5.10', compiled=False):
|
||||
@ -931,6 +1038,7 @@ class WebEngineTab(browsertab.AbstractTab):
|
||||
@pyqtSlot(QUrl)
|
||||
def _on_predicted_navigation(self, url):
|
||||
"""If we know we're going to visit an URL soon, change the settings."""
|
||||
super()._on_predicted_navigation(url)
|
||||
self.settings.update_for_url(url)
|
||||
|
||||
@pyqtSlot(usertypes.NavigationRequest)
|
||||
|
@ -196,9 +196,10 @@ class WebKitCaret(browsertab.AbstractCaret):
|
||||
if mode != usertypes.KeyMode.caret:
|
||||
return
|
||||
|
||||
self.selection_enabled = self._widget.hasSelection()
|
||||
self.selection_toggled.emit(self.selection_enabled)
|
||||
settings = self._widget.settings()
|
||||
settings.setAttribute(QWebSettings.CaretBrowsingEnabled, True)
|
||||
self.selection_enabled = self._widget.hasSelection()
|
||||
|
||||
if self._widget.isVisible():
|
||||
# Sometimes the caret isn't immediately visible, but unfocusing
|
||||
@ -363,9 +364,7 @@ class WebKitCaret(browsertab.AbstractCaret):
|
||||
|
||||
def toggle_selection(self):
|
||||
self.selection_enabled = not self.selection_enabled
|
||||
mainwindow = objreg.get('main-window', scope='window',
|
||||
window=self._tab.win_id)
|
||||
mainwindow.status.set_mode_active(usertypes.KeyMode.caret, True)
|
||||
self.selection_toggled.emit(self.selection_enabled)
|
||||
|
||||
def drop_selection(self):
|
||||
self._widget.triggerPageAction(QWebPage.MoveToNextChar)
|
||||
@ -427,6 +426,9 @@ class WebKitScroller(browsertab.AbstractScroller):
|
||||
def to_point(self, point):
|
||||
self._widget.page().mainFrame().setScrollPosition(point)
|
||||
|
||||
def to_anchor(self, name):
|
||||
self._widget.page().mainFrame().scrollToAnchor(name)
|
||||
|
||||
def delta(self, x=0, y=0):
|
||||
qtutils.check_overflow(x, 'int')
|
||||
qtutils.check_overflow(y, 'int')
|
||||
@ -537,6 +539,9 @@ class WebKitHistory(browsertab.AbstractHistory):
|
||||
return qtutils.deserialize(data, self._history)
|
||||
|
||||
def load_items(self, items):
|
||||
if items:
|
||||
self._tab.predicted_navigation.emit(items[-1].url)
|
||||
|
||||
stream, _data, user_data = tabhistory.serialize(items)
|
||||
qtutils.deserialize_stream(stream, self._history)
|
||||
for i, data in enumerate(user_data):
|
||||
@ -668,8 +673,8 @@ class WebKitTab(browsertab.AbstractTab):
|
||||
settings = widget.settings()
|
||||
settings.setAttribute(QWebSettings.PrivateBrowsingEnabled, True)
|
||||
|
||||
def openurl(self, url):
|
||||
self._openurl_prepare(url)
|
||||
def openurl(self, url, *, predict=True):
|
||||
self._openurl_prepare(url, predict=predict)
|
||||
self._widget.openurl(url)
|
||||
|
||||
def url(self, requested=False):
|
||||
@ -701,7 +706,6 @@ class WebKitTab(browsertab.AbstractTab):
|
||||
self._widget.shutdown()
|
||||
|
||||
def reload(self, *, force=False):
|
||||
self.predicted_navigation.emit(self.url())
|
||||
if force:
|
||||
action = QWebPage.ReloadAndBypassCache
|
||||
else:
|
||||
|
@ -239,7 +239,6 @@ class BrowserPage(QWebPage):
|
||||
printdiag.setAttribute(Qt.WA_DeleteOnClose)
|
||||
printdiag.open(lambda: frame.print(printdiag.printer()))
|
||||
|
||||
@pyqtSlot('QNetworkRequest')
|
||||
def on_download_requested(self, request):
|
||||
"""Called when the user wants to download a link.
|
||||
|
||||
|
@ -110,18 +110,18 @@ def _buffer(skip_win_id=None):
|
||||
model = completionmodel.CompletionModel(column_widths=(6, 40, 54))
|
||||
|
||||
for win_id in objreg.window_registry:
|
||||
if skip_win_id and win_id == skip_win_id:
|
||||
if skip_win_id is not None and win_id == skip_win_id:
|
||||
continue
|
||||
tabbed_browser = objreg.get('tabbed-browser', scope='window',
|
||||
window=win_id)
|
||||
if tabbed_browser.shutting_down:
|
||||
continue
|
||||
tabs = []
|
||||
for idx in range(tabbed_browser.count()):
|
||||
tab = tabbed_browser.widget(idx)
|
||||
for idx in range(tabbed_browser.widget.count()):
|
||||
tab = tabbed_browser.widget.widget(idx)
|
||||
tabs.append(("{}/{}".format(win_id, idx + 1),
|
||||
tab.url().toDisplayString(),
|
||||
tabbed_browser.page_title(idx)))
|
||||
tabbed_browser.widget.page_title(idx)))
|
||||
cat = listcategory.ListCategory("{}".format(win_id), tabs,
|
||||
delete_func=delete_buffer)
|
||||
model.add_category(cat)
|
||||
|
@ -425,11 +425,7 @@ content.host_blocking.enabled:
|
||||
|
||||
content.host_blocking.lists:
|
||||
default:
|
||||
- "https://www.malwaredomainlist.com/hostslist/hosts.txt"
|
||||
- "http://someonewhocares.org/hosts/hosts"
|
||||
- "http://winhelp2002.mvps.org/hosts.zip"
|
||||
- "http://malwaredomains.lehigh.edu/files/justdomains.zip"
|
||||
- "https://pgl.yoyo.org/adservers/serverlist.php?hostformat=hosts&mimetype=plaintext"
|
||||
- "https://raw.githubusercontent.com/StevenBlack/hosts/master/hosts"
|
||||
type:
|
||||
name: List
|
||||
valtype: Url
|
||||
@ -1252,9 +1248,14 @@ tabs.favicons.scale:
|
||||
`tabs.padding`.
|
||||
|
||||
tabs.favicons.show:
|
||||
default: true
|
||||
type: Bool
|
||||
desc: Show favicons in the tab bar.
|
||||
default: always
|
||||
type:
|
||||
name: String
|
||||
valid_values:
|
||||
- always: Always show favicons.
|
||||
- never: Always hide favicons.
|
||||
- pinned: Show favicons only on pinned tabs.
|
||||
desc: When to show favicons in the tab bar.
|
||||
|
||||
tabs.last_close:
|
||||
default: ignore
|
||||
@ -1325,7 +1326,10 @@ tabs.show:
|
||||
|
||||
tabs.show_switching_delay:
|
||||
default: 800
|
||||
type: Int
|
||||
type:
|
||||
name: Int
|
||||
minval: 0
|
||||
maxval: maxint
|
||||
desc: "Duration (in milliseconds) to show the tab bar before hiding it when
|
||||
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
|
||||
it's vertical."
|
||||
|
||||
tabs.min_width:
|
||||
default: -1
|
||||
type:
|
||||
name: Int
|
||||
minval: -1
|
||||
maxval: maxint
|
||||
desc: >-
|
||||
Minimum width (in pixels) of tabs (-1 for the default minimum size behavior).
|
||||
|
||||
This setting only applies when tabs are horizontal.
|
||||
|
||||
This setting does not apply to pinned tabs, unless `tabs.pinned.shrink` is False.
|
||||
|
||||
tabs.width.indicator:
|
||||
renamed: tabs.indicator.width
|
||||
|
||||
@ -1469,6 +1486,11 @@ url.incdec_segments:
|
||||
desc: URL segments where `:navigate increment/decrement` will search for
|
||||
a number.
|
||||
|
||||
url.open_base_url:
|
||||
type: Bool
|
||||
default: false
|
||||
desc: Open base URL of the searchengine if a searchengine shortcut is invoked without parameters.
|
||||
|
||||
url.searchengines:
|
||||
default:
|
||||
DEFAULT: https://duckduckgo.com/?q={}
|
||||
@ -1513,10 +1535,15 @@ url.yank_ignored_parameters:
|
||||
## window
|
||||
|
||||
window.hide_wayland_decoration:
|
||||
renamed: window.hide_decoration
|
||||
|
||||
window.hide_decoration:
|
||||
type: Bool
|
||||
default: false
|
||||
restart: true
|
||||
desc: Hide the window decoration when using wayland.
|
||||
desc: |
|
||||
Hide the window decoration.
|
||||
|
||||
This setting requires a restart on Wayland.
|
||||
|
||||
window.title_format:
|
||||
type:
|
||||
|
@ -268,6 +268,15 @@ class YamlConfig(QObject):
|
||||
del settings['bindings.default']
|
||||
self._mark_changed()
|
||||
|
||||
# Option to show favicons only for pinned tabs changed the type of
|
||||
# tabs.favicons.show from Bool to String
|
||||
name = 'tabs.favicons.show'
|
||||
if name in settings:
|
||||
for scope, val in settings[name].items():
|
||||
if isinstance(val, bool):
|
||||
settings[name][scope] = 'always' if val else 'never'
|
||||
self._mark_changed()
|
||||
|
||||
return settings
|
||||
|
||||
def _validate(self, settings):
|
||||
|
@ -26,7 +26,8 @@ from PyQt5.QtWidgets import QMessageBox
|
||||
|
||||
from qutebrowser.config import (config, configdata, configfiles, configtypes,
|
||||
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
|
||||
|
||||
|
||||
@ -89,7 +90,7 @@ def _init_envvars():
|
||||
if config.val.qt.force_platform is not None:
|
||||
os.environ['QT_QPA_PLATFORM'] = config.val.qt.force_platform
|
||||
|
||||
if config.val.window.hide_wayland_decoration:
|
||||
if config.val.window.hide_decoration:
|
||||
os.environ['QT_WAYLAND_DISABLE_WINDOWDECORATION'] = '1'
|
||||
|
||||
if config.val.qt.highdpi:
|
||||
@ -161,4 +162,12 @@ def qt_args(namespace):
|
||||
argv += ['--' + name, value]
|
||||
|
||||
argv += ['--' + arg for arg in config.val.qt.args]
|
||||
|
||||
if (objects.backend == usertypes.Backend.QtWebEngine and
|
||||
not qtutils.version_check('5.11', compiled=False)):
|
||||
# WORKAROUND equivalent to
|
||||
# https://codereview.qt-project.org/#/c/217932/
|
||||
# Needed for Qt < 5.9.5 and < 5.10.1
|
||||
argv.append('--disable-shared-workers')
|
||||
|
||||
return argv
|
||||
|
@ -451,7 +451,7 @@ class List(BaseType):
|
||||
def from_obj(self, value):
|
||||
if value is None:
|
||||
return []
|
||||
return value
|
||||
return [self.valtype.from_obj(v) for v in value]
|
||||
|
||||
def to_py(self, value):
|
||||
self._basic_py_validation(value, list)
|
||||
@ -506,6 +506,16 @@ class ListOrValue(BaseType):
|
||||
self.listtype = List(valtype, none_ok=none_ok, *args, **kwargs)
|
||||
self.valtype = valtype
|
||||
|
||||
def _val_and_type(self, value):
|
||||
"""Get the value and type to use for to_str/to_doc/from_str."""
|
||||
if isinstance(value, list):
|
||||
if len(value) == 1:
|
||||
return value[0], self.valtype
|
||||
else:
|
||||
return value, self.listtype
|
||||
else:
|
||||
return value, self.valtype
|
||||
|
||||
def get_name(self):
|
||||
return self.listtype.get_name() + ', or ' + self.valtype.get_name()
|
||||
|
||||
@ -533,25 +543,15 @@ class ListOrValue(BaseType):
|
||||
if value is None:
|
||||
return ''
|
||||
|
||||
if isinstance(value, list):
|
||||
if len(value) == 1:
|
||||
return self.valtype.to_str(value[0])
|
||||
else:
|
||||
return self.listtype.to_str(value)
|
||||
else:
|
||||
return self.valtype.to_str(value)
|
||||
val, typ = self._val_and_type(value)
|
||||
return typ.to_str(val)
|
||||
|
||||
def to_doc(self, value, indent=0):
|
||||
if value is None:
|
||||
return 'empty'
|
||||
|
||||
if isinstance(value, list):
|
||||
if len(value) == 1:
|
||||
return self.valtype.to_doc(value[0], indent)
|
||||
else:
|
||||
return self.listtype.to_doc(value, indent)
|
||||
else:
|
||||
return self.valtype.to_doc(value, indent)
|
||||
val, typ = self._val_and_type(value)
|
||||
return typ.to_doc(val)
|
||||
|
||||
|
||||
class FlagList(List):
|
||||
@ -1199,7 +1199,9 @@ class Dict(BaseType):
|
||||
def from_obj(self, value):
|
||||
if value is None:
|
||||
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):
|
||||
"""Fill missing fixed keys with a None-value."""
|
||||
@ -1623,9 +1625,7 @@ class TimestampTemplate(BaseType):
|
||||
|
||||
"""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.
|
||||
"""
|
||||
|
||||
def to_py(self, value):
|
||||
@ -1648,6 +1648,10 @@ class Key(BaseType):
|
||||
|
||||
"""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):
|
||||
self._basic_py_validation(value, str)
|
||||
if not value:
|
||||
|
@ -22,7 +22,7 @@
|
||||
from PyQt5.QtGui import QFont
|
||||
|
||||
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
|
||||
|
||||
UNSET = object()
|
||||
@ -141,6 +141,7 @@ class AbstractSettings:
|
||||
Return:
|
||||
A set of settings which actually changed.
|
||||
"""
|
||||
qtutils.ensure_valid(url)
|
||||
changed_settings = set()
|
||||
for values in config.instance:
|
||||
if not values.opt.supports_pattern:
|
||||
|
@ -2,3 +2,4 @@
|
||||
pac_utils.js
|
||||
# Actually a jinja template so eslint chokes on the {{}} syntax.
|
||||
greasemonkey_wrapper.js
|
||||
global_wrapper.js
|
||||
|
@ -324,8 +324,7 @@ window._qutebrowser.caret = (function() {
|
||||
const color = axs.color.parseColor(style.backgroundColor);
|
||||
if (color &&
|
||||
(style.opacity < 1 &&
|
||||
(color.alpha *= style.opacity),
|
||||
color.alpha !== 0 &&
|
||||
(color.alpha *= style.opacity), color.alpha !== 0 &&
|
||||
(el.push(color), color.alpha === 1))) {
|
||||
iter = !0;
|
||||
break;
|
||||
@ -1270,13 +1269,14 @@ window._qutebrowser.caret = (function() {
|
||||
funcs.setInitialCursor = () => {
|
||||
if (!CaretBrowsing.initiated) {
|
||||
CaretBrowsing.setInitialCursor();
|
||||
return;
|
||||
return CaretBrowsing.selectionEnabled;
|
||||
}
|
||||
|
||||
if (window.getSelection().toString().length === 0) {
|
||||
positionCaret();
|
||||
}
|
||||
CaretBrowsing.toggle();
|
||||
return CaretBrowsing.selectionEnabled;
|
||||
};
|
||||
|
||||
funcs.setPlatform = (platform) => {
|
||||
@ -1362,6 +1362,7 @@ window._qutebrowser.caret = (function() {
|
||||
|
||||
funcs.toggleSelection = () => {
|
||||
CaretBrowsing.selectionEnabled = !CaretBrowsing.selectionEnabled;
|
||||
return CaretBrowsing.selectionEnabled;
|
||||
};
|
||||
|
||||
return funcs;
|
||||
|
12
qutebrowser/javascript/global_wrapper.js
Normal 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;
|
||||
})();
|
@ -1,5 +1,5 @@
|
||||
(function() {
|
||||
const _qute_script_id = "__gm_" + {{ scriptName | tojson }};
|
||||
const _qute_script_id = "__gm_{{ scriptName }}";
|
||||
|
||||
function GM_log(text) {
|
||||
console.log(text);
|
||||
@ -7,7 +7,7 @@
|
||||
|
||||
const GM_info = {
|
||||
'script': {{ scriptInfo }},
|
||||
'scriptMetaStr': {{ scriptMeta | tojson }},
|
||||
'scriptMetaStr': "{{ scriptMeta }}",
|
||||
'scriptWillUpdate': false,
|
||||
'version': "0.0.1",
|
||||
// so scripts don't expect exportFunction
|
||||
@ -100,11 +100,8 @@
|
||||
|
||||
const head = document.getElementsByTagName("head")[0];
|
||||
if (head === undefined) {
|
||||
document.onreadystatechange = function() {
|
||||
if (document.readyState === "interactive") {
|
||||
document.getElementsByTagName("head")[0].appendChild(oStyle);
|
||||
}
|
||||
};
|
||||
// no head yet, stick it whereever
|
||||
document.documentElement.appendChild(oStyle);
|
||||
} else {
|
||||
head.appendChild(oStyle);
|
||||
}
|
||||
|
@ -74,9 +74,8 @@ window._qutebrowser.webelem = (function() {
|
||||
try {
|
||||
return elem.selectionStart;
|
||||
} catch (err) {
|
||||
if (err instanceof (frame
|
||||
? frame.DOMException
|
||||
: DOMException) &&
|
||||
if ((err instanceof DOMException ||
|
||||
(frame && err instanceof frame.DOMException)) &&
|
||||
err.name === "InvalidStateError") {
|
||||
// nothing to do, caret_position is already null
|
||||
} else {
|
||||
|
@ -108,11 +108,43 @@ class BaseKeyParser(QObject):
|
||||
assert not isinstance(seq, str), seq
|
||||
match = sequence.matches(seq)
|
||||
if match == QKeySequence.ExactMatch:
|
||||
return (match, cmd)
|
||||
return match, cmd
|
||||
elif match == QKeySequence.PartialMatch:
|
||||
result = QKeySequence.PartialMatch
|
||||
|
||||
return (result, None)
|
||||
return result, None
|
||||
|
||||
def _match_without_modifiers(self, sequence):
|
||||
"""Try to match a key with optional modifiers stripped."""
|
||||
self._debug_log("Trying match without modifiers")
|
||||
sequence = sequence.strip_modifiers()
|
||||
match, binding = self._match_key(sequence)
|
||||
return match, binding, sequence
|
||||
|
||||
def _match_key_mapping(self, sequence):
|
||||
"""Try to match a key in bindings.key_mappings."""
|
||||
self._debug_log("Trying match with key_mappings")
|
||||
mapped = sequence.with_mappings(config.val.bindings.key_mappings)
|
||||
if sequence != mapped:
|
||||
self._debug_log("Mapped {} -> {}".format(
|
||||
sequence, mapped))
|
||||
match, binding = self._match_key(mapped)
|
||||
sequence = mapped
|
||||
return match, binding, sequence
|
||||
return QKeySequence.NoMatch, None, sequence
|
||||
|
||||
def _match_count(self, sequence, dry_run):
|
||||
"""Try to match a key as count."""
|
||||
txt = str(sequence[-1]) # To account for sequences changed above.
|
||||
if (txt.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):
|
||||
"""Handle a new keypress.
|
||||
@ -146,27 +178,14 @@ class BaseKeyParser(QObject):
|
||||
self.clear_keystring()
|
||||
return QKeySequence.NoMatch
|
||||
|
||||
# First, try a straightforward match
|
||||
match, binding = self._match_key(sequence)
|
||||
|
||||
# If that doesn't match, try a key_mapping
|
||||
if match == QKeySequence.NoMatch:
|
||||
mapped = sequence.with_mappings(config.val.bindings.key_mappings)
|
||||
if sequence != mapped:
|
||||
self._debug_log("Mapped {} -> {}".format(
|
||||
sequence, mapped))
|
||||
match, binding = self._match_key(mapped)
|
||||
sequence = mapped
|
||||
|
||||
# If that doesn't match either, try treating it as count.
|
||||
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))
|
||||
match, binding, sequence = self._match_without_modifiers(sequence)
|
||||
if match == QKeySequence.NoMatch:
|
||||
match, binding, sequence = self._match_key_mapping(sequence)
|
||||
if match == QKeySequence.NoMatch:
|
||||
was_count = self._match_count(sequence, dry_run)
|
||||
if was_count:
|
||||
return QKeySequence.ExactMatch
|
||||
|
||||
if dry_run:
|
||||
|
@ -58,7 +58,8 @@ def is_special(key, modifiers):
|
||||
_assert_plain_key(key)
|
||||
_assert_plain_modifier(modifiers)
|
||||
return not (_is_printable(key) and
|
||||
modifiers in [Qt.ShiftModifier, Qt.NoModifier])
|
||||
modifiers in [Qt.ShiftModifier, Qt.NoModifier,
|
||||
Qt.KeypadModifier])
|
||||
|
||||
|
||||
def is_modifier_key(key):
|
||||
@ -303,7 +304,8 @@ class KeyInfo:
|
||||
key_string = key_string.lower()
|
||||
|
||||
# "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)
|
||||
return '<{}{}>'.format(modifier_string, key_string)
|
||||
|
||||
@ -505,11 +507,29 @@ class KeySequence:
|
||||
not ev.text().isupper()):
|
||||
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.append(key | int(modifiers))
|
||||
|
||||
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):
|
||||
"""Get a new KeySequence with the given mappings applied."""
|
||||
keys = []
|
||||
|
@ -184,7 +184,8 @@ class MainWindow(QWidget):
|
||||
private = bool(private)
|
||||
self._private = private
|
||||
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',
|
||||
window=self.win_id)
|
||||
self._init_command_dispatcher()
|
||||
@ -230,6 +231,7 @@ class MainWindow(QWidget):
|
||||
config.instance.changed.connect(self._on_config_changed)
|
||||
|
||||
objreg.get("app").new_window.emit(self)
|
||||
self._set_decoration(config.val.window.hide_decoration)
|
||||
|
||||
def _init_geometry(self, geometry):
|
||||
"""Initialize the window geometry or load it from disk."""
|
||||
@ -327,7 +329,7 @@ class MainWindow(QWidget):
|
||||
self.tabbed_browser)
|
||||
objreg.register('command-dispatcher', dispatcher, scope='window',
|
||||
window=self.win_id)
|
||||
self.tabbed_browser.destroyed.connect(
|
||||
self.tabbed_browser.widget.destroyed.connect(
|
||||
functools.partial(objreg.delete, 'command-dispatcher',
|
||||
scope='window', window=self.win_id))
|
||||
|
||||
@ -344,13 +346,15 @@ class MainWindow(QWidget):
|
||||
elif option == 'statusbar.position':
|
||||
self._add_widgets()
|
||||
self._update_overlay_geometries()
|
||||
elif option == 'window.hide_decoration':
|
||||
self._set_decoration(config.val.window.hide_decoration)
|
||||
|
||||
def _add_widgets(self):
|
||||
"""Add or readd all widgets to the VBox."""
|
||||
self._vbox.removeWidget(self.tabbed_browser)
|
||||
self._vbox.removeWidget(self.tabbed_browser.widget)
|
||||
self._vbox.removeWidget(self._downloadview)
|
||||
self._vbox.removeWidget(self.status)
|
||||
widgets = [self.tabbed_browser]
|
||||
widgets = [self.tabbed_browser.widget]
|
||||
|
||||
downloads_position = config.val.downloads.position
|
||||
if downloads_position == 'top':
|
||||
@ -469,7 +473,7 @@ class MainWindow(QWidget):
|
||||
|
||||
self.tabbed_browser.cur_scroll_perc_changed.connect(
|
||||
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)
|
||||
|
||||
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_load_status_changed.connect(
|
||||
status.url.on_load_status_changed)
|
||||
|
||||
self.tabbed_browser.cur_caret_selection_toggled.connect(
|
||||
status.on_caret_selection_toggled)
|
||||
|
||||
self.tabbed_browser.cur_fullscreen_requested.connect(
|
||||
self._on_fullscreen_requested)
|
||||
self.tabbed_browser.cur_fullscreen_requested.connect(status.maybe_hide)
|
||||
@ -489,6 +497,16 @@ class MainWindow(QWidget):
|
||||
completion_obj.on_clear_completion_selection)
|
||||
cmd.hide_completion.connect(completion_obj.hide)
|
||||
|
||||
def _set_decoration(self, hidden):
|
||||
"""Set the visibility of the window decoration via Qt."""
|
||||
window_flags = Qt.Window
|
||||
refresh_window = self.isVisible()
|
||||
if hidden:
|
||||
window_flags |= Qt.CustomizeWindowHint | Qt.NoDropShadowWindowHint
|
||||
self.setWindowFlags(window_flags)
|
||||
if refresh_window:
|
||||
self.show()
|
||||
|
||||
@pyqtSlot(bool)
|
||||
def _on_fullscreen_requested(self, on):
|
||||
if not config.val.content.windowed_fullscreen:
|
||||
@ -517,7 +535,7 @@ class MainWindow(QWidget):
|
||||
super().resizeEvent(e)
|
||||
self._update_overlay_geometries()
|
||||
self._downloadview.updateGeometry()
|
||||
self.tabbed_browser.tabBar().refresh()
|
||||
self.tabbed_browser.widget.tabBar().refresh()
|
||||
|
||||
def showEvent(self, e):
|
||||
"""Extend showEvent to register us as the last-visible-main-window.
|
||||
@ -546,7 +564,7 @@ class MainWindow(QWidget):
|
||||
if crashsignal.is_crashing:
|
||||
e.accept()
|
||||
return
|
||||
tab_count = self.tabbed_browser.count()
|
||||
tab_count = self.tabbed_browser.widget.count()
|
||||
download_model = objreg.get('download-model', scope='window',
|
||||
window=self.win_id)
|
||||
download_count = download_model.running_downloads()
|
||||
|
@ -596,6 +596,8 @@ class FilenamePrompt(_BasePrompt):
|
||||
if config.val.prompt.filebrowser:
|
||||
self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred)
|
||||
|
||||
self._to_complete = ''
|
||||
|
||||
@pyqtSlot(str)
|
||||
def _set_fileview_root(self, path, *, tabbed=False):
|
||||
"""Set the root path for the file display."""
|
||||
@ -604,6 +606,9 @@ class FilenamePrompt(_BasePrompt):
|
||||
separators += os.altsep
|
||||
|
||||
dirname = os.path.dirname(path)
|
||||
basename = os.path.basename(path)
|
||||
if not tabbed:
|
||||
self._to_complete = ''
|
||||
|
||||
try:
|
||||
if not path:
|
||||
@ -617,6 +622,7 @@ class FilenamePrompt(_BasePrompt):
|
||||
elif os.path.isdir(dirname) and not tabbed:
|
||||
# Input like /foo/ba -> show /foo contents
|
||||
path = dirname
|
||||
self._to_complete = basename
|
||||
else:
|
||||
return
|
||||
except OSError:
|
||||
@ -634,7 +640,11 @@ class FilenamePrompt(_BasePrompt):
|
||||
index: The QModelIndex of the selected element.
|
||||
clicked: Whether the element was clicked.
|
||||
"""
|
||||
if index == QModelIndex():
|
||||
path = os.path.join(self._file_model.rootPath(), self._to_complete)
|
||||
else:
|
||||
path = os.path.normpath(self._file_model.filePath(index))
|
||||
|
||||
if clicked:
|
||||
path += os.sep
|
||||
else:
|
||||
@ -696,6 +706,7 @@ class FilenamePrompt(_BasePrompt):
|
||||
assert last_index.isValid()
|
||||
|
||||
idx = selmodel.currentIndex()
|
||||
|
||||
if not idx.isValid():
|
||||
# No item selected yet
|
||||
idx = last_index if which == 'prev' else first_index
|
||||
@ -709,10 +720,24 @@ class FilenamePrompt(_BasePrompt):
|
||||
if not idx.isValid():
|
||||
idx = last_index if which == 'prev' else first_index
|
||||
|
||||
idx = self._do_completion(idx, which)
|
||||
|
||||
selmodel.setCurrentIndex(
|
||||
idx, QItemSelectionModel.ClearAndSelect | QItemSelectionModel.Rows)
|
||||
self._insert_path(idx, clicked=False)
|
||||
|
||||
def _do_completion(self, idx, which):
|
||||
filename = self._file_model.fileName(idx)
|
||||
while not filename.startswith(self._to_complete) and idx.isValid():
|
||||
if which == 'prev':
|
||||
idx = self._file_view.indexAbove(idx)
|
||||
else:
|
||||
assert which == 'next', which
|
||||
idx = self._file_view.indexBelow(idx)
|
||||
filename = self._file_model.fileName(idx)
|
||||
|
||||
return idx
|
||||
|
||||
def _allowed_commands(self):
|
||||
return [('prompt-accept', 'Accept'), ('leave-mode', 'Abort')]
|
||||
|
||||
|
@ -32,7 +32,7 @@ class Backforward(textbase.TextBase):
|
||||
|
||||
def on_tab_cur_url_changed(self, tabs):
|
||||
"""Called on URL changes."""
|
||||
tab = tabs.currentWidget()
|
||||
tab = tabs.widget.currentWidget()
|
||||
if tab is None: # pragma: no cover
|
||||
self.setText('')
|
||||
self.hide()
|
||||
|
@ -268,7 +268,7 @@ class StatusBar(QWidget):
|
||||
"""Get the currently displayed tab."""
|
||||
window = objreg.get('tabbed-browser', scope='window',
|
||||
window=self._win_id)
|
||||
return window.currentWidget()
|
||||
return window.widget.currentWidget()
|
||||
|
||||
def set_mode_active(self, mode, val):
|
||||
"""Setter for self.{insert,command,caret}_active.
|
||||
@ -289,17 +289,9 @@ class StatusBar(QWidget):
|
||||
log.statusbar.debug("Setting prompt flag to {}".format(val))
|
||||
self._color_flags.prompt = val
|
||||
elif mode == usertypes.KeyMode.caret:
|
||||
tab = self._current_tab()
|
||||
log.statusbar.debug("Setting caret flag - val {}, selection "
|
||||
"{}".format(val, tab.caret.selection_enabled))
|
||||
if val:
|
||||
if tab.caret.selection_enabled:
|
||||
self._set_mode_text("{} selection".format(mode.name))
|
||||
self._color_flags.caret = ColorFlags.CaretMode.selection
|
||||
else:
|
||||
self._set_mode_text(mode.name)
|
||||
self._color_flags.caret = ColorFlags.CaretMode.on
|
||||
else:
|
||||
if not val:
|
||||
# Turning on is handled in on_current_caret_selection_toggled
|
||||
log.statusbar.debug("Setting caret mode off")
|
||||
self._color_flags.caret = ColorFlags.CaretMode.off
|
||||
config.set_register_stylesheet(self, update=False)
|
||||
|
||||
@ -377,6 +369,18 @@ class StatusBar(QWidget):
|
||||
self.maybe_hide()
|
||||
assert tab.private == self._color_flags.private
|
||||
|
||||
@pyqtSlot(bool)
|
||||
def on_caret_selection_toggled(self, selection):
|
||||
"""Update the statusbar when entering/leaving caret selection mode."""
|
||||
log.statusbar.debug("Setting caret selection {}".format(selection))
|
||||
if selection:
|
||||
self._set_mode_text("caret selection")
|
||||
self._color_flags.caret = ColorFlags.CaretMode.selection
|
||||
else:
|
||||
self._set_mode_text("caret")
|
||||
self._color_flags.caret = ColorFlags.CaretMode.on
|
||||
config.set_register_stylesheet(self, update=False)
|
||||
|
||||
def resizeEvent(self, e):
|
||||
"""Extend resizeEvent of QWidget to emit a resized signal afterwards.
|
||||
|
||||
|
@ -22,7 +22,7 @@
|
||||
import functools
|
||||
|
||||
import attr
|
||||
from PyQt5.QtWidgets import QSizePolicy
|
||||
from PyQt5.QtWidgets import QSizePolicy, QWidget
|
||||
from PyQt5.QtCore import pyqtSignal, pyqtSlot, QTimer, QUrl
|
||||
from PyQt5.QtGui import QIcon
|
||||
|
||||
@ -50,7 +50,7 @@ class TabDeletedError(Exception):
|
||||
"""Exception raised when _tab_index is called for a deleted tab."""
|
||||
|
||||
|
||||
class TabbedBrowser(tabwidget.TabWidget):
|
||||
class TabbedBrowser(QWidget):
|
||||
|
||||
"""A TabWidget with QWebViews inside.
|
||||
|
||||
@ -104,23 +104,25 @@ class TabbedBrowser(tabwidget.TabWidget):
|
||||
cur_scroll_perc_changed = pyqtSignal(int, int)
|
||||
cur_load_status_changed = pyqtSignal(str)
|
||||
cur_fullscreen_requested = pyqtSignal(bool)
|
||||
cur_caret_selection_toggled = pyqtSignal(bool)
|
||||
close_window = pyqtSignal()
|
||||
resized = pyqtSignal('QRect')
|
||||
current_tab_changed = pyqtSignal(browsertab.AbstractTab)
|
||||
new_tab = pyqtSignal(browsertab.AbstractTab, int)
|
||||
|
||||
def __init__(self, *, win_id, private, parent=None):
|
||||
super().__init__(win_id, parent)
|
||||
super().__init__(parent)
|
||||
self.widget = tabwidget.TabWidget(win_id, parent=self)
|
||||
self._win_id = win_id
|
||||
self._tab_insert_idx_left = 0
|
||||
self._tab_insert_idx_right = -1
|
||||
self.shutting_down = False
|
||||
self.tabCloseRequested.connect(self.on_tab_close_requested)
|
||||
self.new_tab_requested.connect(self.tabopen)
|
||||
self.currentChanged.connect(self.on_current_changed)
|
||||
self.widget.tabCloseRequested.connect(self.on_tab_close_requested)
|
||||
self.widget.new_tab_requested.connect(self.tabopen)
|
||||
self.widget.currentChanged.connect(self.on_current_changed)
|
||||
self.cur_load_started.connect(self.on_cur_load_started)
|
||||
self.cur_fullscreen_requested.connect(self.tabBar().maybe_hide)
|
||||
self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
|
||||
self.cur_fullscreen_requested.connect(self.widget.tabBar().maybe_hide)
|
||||
self.widget.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
|
||||
self._undo_stack = []
|
||||
self._filter = signalfilter.SignalFilter(win_id, self)
|
||||
self._now_focused = None
|
||||
@ -128,12 +130,12 @@ class TabbedBrowser(tabwidget.TabWidget):
|
||||
self.search_options = {}
|
||||
self._local_marks = {}
|
||||
self._global_marks = {}
|
||||
self.default_window_icon = self.window().windowIcon()
|
||||
self.default_window_icon = self.widget.window().windowIcon()
|
||||
self.private = private
|
||||
config.instance.changed.connect(self._on_config_changed)
|
||||
|
||||
def __repr__(self):
|
||||
return utils.get_repr(self, count=self.count())
|
||||
return utils.get_repr(self, count=self.widget.count())
|
||||
|
||||
@pyqtSlot(str)
|
||||
def _on_config_changed(self, option):
|
||||
@ -142,7 +144,7 @@ class TabbedBrowser(tabwidget.TabWidget):
|
||||
elif option == 'window.title_format':
|
||||
self._update_window_title()
|
||||
elif option in ['tabs.title.format', 'tabs.title.format_pinned']:
|
||||
self._update_tab_titles()
|
||||
self.widget.update_tab_titles()
|
||||
|
||||
def _tab_index(self, 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.
|
||||
"""
|
||||
try:
|
||||
idx = self.indexOf(tab)
|
||||
idx = self.widget.indexOf(tab)
|
||||
except RuntimeError as e:
|
||||
log.webview.debug("Got invalid tab ({})!".format(e))
|
||||
raise TabDeletedError(e)
|
||||
@ -166,8 +168,8 @@ class TabbedBrowser(tabwidget.TabWidget):
|
||||
iterating over the list.
|
||||
"""
|
||||
widgets = []
|
||||
for i in range(self.count()):
|
||||
widget = self.widget(i)
|
||||
for i in range(self.widget.count()):
|
||||
widget = self.widget.widget(i)
|
||||
if widget is None:
|
||||
log.webview.debug("Got None-widget in tabbedbrowser!")
|
||||
else:
|
||||
@ -186,16 +188,16 @@ class TabbedBrowser(tabwidget.TabWidget):
|
||||
if field is not None and ('{' + field + '}') not in title_format:
|
||||
return
|
||||
|
||||
idx = self.currentIndex()
|
||||
idx = self.widget.currentIndex()
|
||||
if idx == -1:
|
||||
# (e.g. last tab removed)
|
||||
log.webview.debug("Not updating window title because index is -1")
|
||||
return
|
||||
fields = self.get_tab_fields(idx)
|
||||
fields = self.widget.get_tab_fields(idx)
|
||||
fields['id'] = self._win_id
|
||||
|
||||
title = title_format.format(**fields)
|
||||
self.window().setWindowTitle(title)
|
||||
self.widget.window().setWindowTitle(title)
|
||||
|
||||
def _connect_tab_signals(self, 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))
|
||||
tab.fullscreen_requested.connect(
|
||||
self._filter.create(self.cur_fullscreen_requested, tab))
|
||||
tab.caret.selection_toggled.connect(
|
||||
self._filter.create(self.cur_caret_selection_toggled, tab))
|
||||
# misc
|
||||
tab.scroller.perc_changed.connect(self.on_scroll_pos_changed)
|
||||
tab.url_changed.connect(
|
||||
@ -247,8 +251,8 @@ class TabbedBrowser(tabwidget.TabWidget):
|
||||
Return:
|
||||
The current URL as QUrl.
|
||||
"""
|
||||
idx = self.currentIndex()
|
||||
return super().tab_url(idx)
|
||||
idx = self.widget.currentIndex()
|
||||
return self.widget.tab_url(idx)
|
||||
|
||||
def shutdown(self):
|
||||
"""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.
|
||||
"""
|
||||
last_close = config.val.tabs.last_close
|
||||
count = self.count()
|
||||
count = self.widget.count()
|
||||
|
||||
if last_close == 'ignore' and count == 1:
|
||||
return
|
||||
@ -311,7 +315,7 @@ class TabbedBrowser(tabwidget.TabWidget):
|
||||
new_undo: Whether the undo entry should be a new item in the stack.
|
||||
crashed: Whether we're closing a tab with crashed renderer process.
|
||||
"""
|
||||
idx = self.indexOf(tab)
|
||||
idx = self.widget.indexOf(tab)
|
||||
if idx == -1:
|
||||
if crashed:
|
||||
return
|
||||
@ -349,7 +353,7 @@ class TabbedBrowser(tabwidget.TabWidget):
|
||||
self._undo_stack[-1].append(entry)
|
||||
|
||||
tab.shutdown()
|
||||
self.removeTab(idx)
|
||||
self.widget.removeTab(idx)
|
||||
if not crashed:
|
||||
# WORKAROUND for a segfault when we delete the crashed tab.
|
||||
# see https://bugreports.qt.io/browse/QTBUG-58698
|
||||
@ -362,14 +366,14 @@ class TabbedBrowser(tabwidget.TabWidget):
|
||||
last_close = config.val.tabs.last_close
|
||||
use_current_tab = False
|
||||
if last_close in ['blank', 'startpage', 'default-page']:
|
||||
only_one_tab_open = self.count() == 1
|
||||
no_history = len(self.widget(0).history) == 1
|
||||
only_one_tab_open = self.widget.count() == 1
|
||||
no_history = len(self.widget.widget(0).history) == 1
|
||||
urls = {
|
||||
'blank': QUrl('about:blank'),
|
||||
'startpage': config.val.url.start_pages[0],
|
||||
'default-page': config.val.url.default_page,
|
||||
}
|
||||
first_tab_url = self.widget(0).url()
|
||||
first_tab_url = self.widget.widget(0).url()
|
||||
last_close_urlstr = urls[last_close].toString().rstrip('/')
|
||||
first_tab_urlstr = first_tab_url.toString().rstrip('/')
|
||||
last_close_url_used = first_tab_urlstr == last_close_urlstr
|
||||
@ -378,13 +382,13 @@ class TabbedBrowser(tabwidget.TabWidget):
|
||||
|
||||
for entry in reversed(self._undo_stack.pop()):
|
||||
if use_current_tab:
|
||||
newtab = self.widget(0)
|
||||
newtab = self.widget.widget(0)
|
||||
use_current_tab = False
|
||||
else:
|
||||
newtab = self.tabopen(background=False, idx=entry.index)
|
||||
|
||||
newtab.history.deserialize(entry.history)
|
||||
self.set_tab_pinned(newtab, entry.pinned)
|
||||
self.widget.set_tab_pinned(newtab, entry.pinned)
|
||||
|
||||
@pyqtSlot('QUrl', bool)
|
||||
def openurl(self, url, newtab):
|
||||
@ -395,15 +399,15 @@ class TabbedBrowser(tabwidget.TabWidget):
|
||||
newtab: True to open URL in a new tab, False otherwise.
|
||||
"""
|
||||
qtutils.ensure_valid(url)
|
||||
if newtab or self.currentWidget() is None:
|
||||
if newtab or self.widget.currentWidget() is None:
|
||||
self.tabopen(url, background=False)
|
||||
else:
|
||||
self.currentWidget().openurl(url)
|
||||
self.widget.currentWidget().openurl(url)
|
||||
|
||||
@pyqtSlot(int)
|
||||
def on_tab_close_requested(self, idx):
|
||||
"""Close a tab via an index."""
|
||||
tab = self.widget(idx)
|
||||
tab = self.widget.widget(idx)
|
||||
if tab is None:
|
||||
log.webview.debug("Got invalid tab {} for index {}!".format(
|
||||
tab, idx))
|
||||
@ -454,7 +458,7 @@ class TabbedBrowser(tabwidget.TabWidget):
|
||||
"related {}, idx {}".format(
|
||||
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):
|
||||
window = mainwindow.MainWindow(private=self.private)
|
||||
window.show()
|
||||
@ -464,12 +468,12 @@ class TabbedBrowser(tabwidget.TabWidget):
|
||||
related=related)
|
||||
|
||||
tab = browsertab.create(win_id=self._win_id, private=self.private,
|
||||
parent=self)
|
||||
parent=self.widget)
|
||||
self._connect_tab_signals(tab)
|
||||
|
||||
if idx is None:
|
||||
idx = self._get_new_tab_idx(related)
|
||||
self.insertTab(idx, tab, "")
|
||||
self.widget.insertTab(idx, tab, "")
|
||||
|
||||
if url is not None:
|
||||
tab.openurl(url)
|
||||
@ -480,10 +484,11 @@ class TabbedBrowser(tabwidget.TabWidget):
|
||||
# Make sure the background tab has the correct initial size.
|
||||
# With a foreground tab, it's going to be resized correctly by the
|
||||
# layout anyways.
|
||||
tab.resize(self.currentWidget().size())
|
||||
self.tab_index_changed.emit(self.currentIndex(), self.count())
|
||||
tab.resize(self.widget.currentWidget().size())
|
||||
self.widget.tab_index_changed.emit(self.widget.currentIndex(),
|
||||
self.widget.count())
|
||||
else:
|
||||
self.setCurrentWidget(tab)
|
||||
self.widget.setCurrentWidget(tab)
|
||||
|
||||
tab.show()
|
||||
self.new_tab.emit(tab, idx)
|
||||
@ -526,15 +531,8 @@ class TabbedBrowser(tabwidget.TabWidget):
|
||||
|
||||
def _update_favicons(self):
|
||||
"""Update favicons when config was changed."""
|
||||
for i, tab in enumerate(self.widgets()):
|
||||
if config.val.tabs.favicons.show:
|
||||
self.setTabIcon(i, tab.icon())
|
||||
if config.val.tabs.tabs_are_windows:
|
||||
self.window().setWindowIcon(tab.icon())
|
||||
else:
|
||||
self.setTabIcon(i, QIcon())
|
||||
if config.val.tabs.tabs_are_windows:
|
||||
self.window().setWindowIcon(self.default_window_icon)
|
||||
for tab in self.widgets():
|
||||
self.widget.update_tab_favicon(tab)
|
||||
|
||||
@pyqtSlot()
|
||||
def on_load_started(self, tab):
|
||||
@ -548,14 +546,14 @@ class TabbedBrowser(tabwidget.TabWidget):
|
||||
except TabDeletedError:
|
||||
# We can get signals for tabs we already deleted...
|
||||
return
|
||||
self._update_tab_title(idx)
|
||||
self.widget.update_tab_title(idx)
|
||||
if tab.data.keep_icon:
|
||||
tab.data.keep_icon = False
|
||||
else:
|
||||
if (config.val.tabs.tabs_are_windows and
|
||||
config.val.tabs.favicons.show):
|
||||
self.window().setWindowIcon(self.default_window_icon)
|
||||
if idx == self.currentIndex():
|
||||
tab.data.should_show_icon()):
|
||||
self.widget.window().setWindowIcon(self.default_window_icon)
|
||||
if idx == self.widget.currentIndex():
|
||||
self._update_window_title()
|
||||
|
||||
@pyqtSlot()
|
||||
@ -586,8 +584,8 @@ class TabbedBrowser(tabwidget.TabWidget):
|
||||
return
|
||||
log.webview.debug("Changing title for idx {} to '{}'".format(
|
||||
idx, text))
|
||||
self.set_page_title(idx, text)
|
||||
if idx == self.currentIndex():
|
||||
self.widget.set_page_title(idx, text)
|
||||
if idx == self.widget.currentIndex():
|
||||
self._update_window_title()
|
||||
|
||||
@pyqtSlot(browsertab.AbstractTab, QUrl)
|
||||
@ -604,8 +602,8 @@ class TabbedBrowser(tabwidget.TabWidget):
|
||||
# We can get signals for tabs we already deleted...
|
||||
return
|
||||
|
||||
if not self.page_title(idx):
|
||||
self.set_page_title(idx, url.toDisplayString())
|
||||
if not self.widget.page_title(idx):
|
||||
self.widget.set_page_title(idx, url.toDisplayString())
|
||||
|
||||
@pyqtSlot(browsertab.AbstractTab, QIcon)
|
||||
def on_icon_changed(self, tab, icon):
|
||||
@ -617,23 +615,23 @@ class TabbedBrowser(tabwidget.TabWidget):
|
||||
tab: The WebView where the title was changed.
|
||||
icon: The new icon
|
||||
"""
|
||||
if not config.val.tabs.favicons.show:
|
||||
if not tab.data.should_show_icon():
|
||||
return
|
||||
try:
|
||||
idx = self._tab_index(tab)
|
||||
except TabDeletedError:
|
||||
# We can get signals for tabs we already deleted...
|
||||
return
|
||||
self.setTabIcon(idx, icon)
|
||||
self.widget.setTabIcon(idx, icon)
|
||||
if config.val.tabs.tabs_are_windows:
|
||||
self.window().setWindowIcon(icon)
|
||||
self.widget.window().setWindowIcon(icon)
|
||||
|
||||
@pyqtSlot(usertypes.KeyMode)
|
||||
def on_mode_left(self, mode):
|
||||
"""Give focus to current tab if command mode was left."""
|
||||
if mode in [usertypes.KeyMode.command, usertypes.KeyMode.prompt,
|
||||
usertypes.KeyMode.yesno]:
|
||||
widget = self.currentWidget()
|
||||
widget = self.widget.currentWidget()
|
||||
log.modes.debug("Left status-input mode, focusing {!r}".format(
|
||||
widget))
|
||||
if widget is None:
|
||||
@ -649,7 +647,7 @@ class TabbedBrowser(tabwidget.TabWidget):
|
||||
if idx == -1 or self.shutting_down:
|
||||
# closing the last tab (before quitting) or shutting down
|
||||
return
|
||||
tab = self.widget(idx)
|
||||
tab = self.widget.widget(idx)
|
||||
if tab is None:
|
||||
log.webview.debug("on_current_changed got called with invalid "
|
||||
"index {}".format(idx))
|
||||
@ -677,8 +675,8 @@ class TabbedBrowser(tabwidget.TabWidget):
|
||||
self._now_focused = tab
|
||||
self.current_tab_changed.emit(tab)
|
||||
QTimer.singleShot(0, self._update_window_title)
|
||||
self._tab_insert_idx_left = self.currentIndex()
|
||||
self._tab_insert_idx_right = self.currentIndex() + 1
|
||||
self._tab_insert_idx_left = self.widget.currentIndex()
|
||||
self._tab_insert_idx_right = self.widget.currentIndex() + 1
|
||||
|
||||
@pyqtSlot()
|
||||
def on_cmd_return_pressed(self):
|
||||
@ -696,9 +694,9 @@ class TabbedBrowser(tabwidget.TabWidget):
|
||||
stop = config.val.colors.tabs.indicator.stop
|
||||
system = config.val.colors.tabs.indicator.system
|
||||
color = utils.interpolate_color(start, stop, perc, system)
|
||||
self.set_tab_indicator_color(idx, color)
|
||||
self._update_tab_title(idx)
|
||||
if idx == self.currentIndex():
|
||||
self.widget.set_tab_indicator_color(idx, color)
|
||||
self.widget.update_tab_title(idx)
|
||||
if idx == self.widget.currentIndex():
|
||||
self._update_window_title()
|
||||
|
||||
def on_load_finished(self, tab, ok):
|
||||
@ -715,23 +713,23 @@ class TabbedBrowser(tabwidget.TabWidget):
|
||||
color = utils.interpolate_color(start, stop, 100, system)
|
||||
else:
|
||||
color = config.val.colors.tabs.indicator.error
|
||||
self.set_tab_indicator_color(idx, color)
|
||||
self._update_tab_title(idx)
|
||||
if idx == self.currentIndex():
|
||||
self.widget.set_tab_indicator_color(idx, color)
|
||||
self.widget.update_tab_title(idx)
|
||||
if idx == self.widget.currentIndex():
|
||||
self._update_window_title()
|
||||
tab.handle_auto_insert_mode(ok)
|
||||
|
||||
@pyqtSlot()
|
||||
def on_scroll_pos_changed(self):
|
||||
"""Update tab and window title when scroll position changed."""
|
||||
idx = self.currentIndex()
|
||||
idx = self.widget.currentIndex()
|
||||
if idx == -1:
|
||||
# (e.g. last tab removed)
|
||||
log.webview.debug("Not updating scroll position because index is "
|
||||
"-1")
|
||||
return
|
||||
self._update_window_title('scroll_pos')
|
||||
self._update_tab_title(idx, 'scroll_pos')
|
||||
self.widget.update_tab_title(idx, 'scroll_pos')
|
||||
|
||||
def _on_renderer_process_terminated(self, tab, status, code):
|
||||
"""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
|
||||
message.error(msg)
|
||||
self._remove_tab(tab, crashed=True)
|
||||
if self.count() == 0:
|
||||
if self.widget.count() == 0:
|
||||
self.tabopen(QUrl('about:blank'))
|
||||
|
||||
def resizeEvent(self, e):
|
||||
@ -801,7 +799,7 @@ class TabbedBrowser(tabwidget.TabWidget):
|
||||
if key != "'":
|
||||
message.error("Failed to set mark: url invalid")
|
||||
return
|
||||
point = self.currentWidget().scroller.pos_px()
|
||||
point = self.widget.currentWidget().scroller.pos_px()
|
||||
|
||||
if key.isupper():
|
||||
self._global_marks[key] = point, url
|
||||
@ -822,7 +820,7 @@ class TabbedBrowser(tabwidget.TabWidget):
|
||||
except qtutils.QtValueError:
|
||||
urlkey = None
|
||||
|
||||
tab = self.currentWidget()
|
||||
tab = self.widget.currentWidget()
|
||||
|
||||
if key.isupper():
|
||||
if key in self._global_marks:
|
||||
|
@ -60,7 +60,7 @@ class TabWidget(QTabWidget):
|
||||
self.setTabBar(bar)
|
||||
bar.tabCloseRequested.connect(self.tabCloseRequested)
|
||||
bar.tabMoved.connect(functools.partial(
|
||||
QTimer.singleShot, 0, self._update_tab_titles))
|
||||
QTimer.singleShot, 0, self.update_tab_titles))
|
||||
bar.currentChanged.connect(self._on_current_changed)
|
||||
bar.new_tab_requested.connect(self._on_new_tab_requested)
|
||||
self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
|
||||
@ -108,7 +108,8 @@ class TabWidget(QTabWidget):
|
||||
|
||||
bar.set_tab_data(idx, '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):
|
||||
"""Get the tab indicator color for the given index."""
|
||||
@ -117,13 +118,13 @@ class TabWidget(QTabWidget):
|
||||
def set_page_title(self, idx, title):
|
||||
"""Set the tab title user data."""
|
||||
self.tabBar().set_tab_data(idx, 'page-title', title)
|
||||
self._update_tab_title(idx)
|
||||
self.update_tab_title(idx)
|
||||
|
||||
def page_title(self, idx):
|
||||
"""Get the tab title user data."""
|
||||
return self.tabBar().page_title(idx)
|
||||
|
||||
def _update_tab_title(self, idx, field=None):
|
||||
def update_tab_title(self, idx, field=None):
|
||||
"""Update the tab text for the given tab.
|
||||
|
||||
Args:
|
||||
@ -148,9 +149,13 @@ class TabWidget(QTabWidget):
|
||||
title = '' if fmt is None else fmt.format(**fields)
|
||||
tabbar = self.tabBar()
|
||||
|
||||
# Only change the tab title if it changes, setting the tab title causes
|
||||
# a size recalculation which is slow.
|
||||
if tabbar.tabText(idx) != title:
|
||||
tabbar.setTabText(idx, title)
|
||||
tabbar.setTabToolTip(idx, title)
|
||||
|
||||
# always show only plain title in tooltips
|
||||
tabbar.setTabToolTip(idx, fields['title'])
|
||||
|
||||
def get_tab_fields(self, idx):
|
||||
"""Get the tab field data."""
|
||||
@ -197,20 +202,20 @@ class TabWidget(QTabWidget):
|
||||
fields['scroll_pos'] = scroll_pos
|
||||
return fields
|
||||
|
||||
def _update_tab_titles(self):
|
||||
def update_tab_titles(self):
|
||||
"""Update all texts."""
|
||||
for idx in range(self.count()):
|
||||
self._update_tab_title(idx)
|
||||
self.update_tab_title(idx)
|
||||
|
||||
def tabInserted(self, idx):
|
||||
"""Update titles when a tab was inserted."""
|
||||
super().tabInserted(idx)
|
||||
self._update_tab_titles()
|
||||
self.update_tab_titles()
|
||||
|
||||
def tabRemoved(self, idx):
|
||||
"""Update titles when a tab was removed."""
|
||||
super().tabRemoved(idx)
|
||||
self._update_tab_titles()
|
||||
self.update_tab_titles()
|
||||
|
||||
def addTab(self, page, icon_or_text, text_or_empty=None):
|
||||
"""Override addTab to use our own text setting logic.
|
||||
@ -296,6 +301,19 @@ class TabWidget(QTabWidget):
|
||||
qtutils.ensure_valid(url)
|
||||
return url
|
||||
|
||||
def update_tab_favicon(self, tab: QWidget):
|
||||
"""Update favicon of the given tab."""
|
||||
idx = self.indexOf(tab)
|
||||
|
||||
if tab.data.should_show_icon():
|
||||
self.setTabIcon(idx, tab.icon())
|
||||
if config.val.tabs.tabs_are_windows:
|
||||
self.window().setWindowIcon(tab.icon())
|
||||
else:
|
||||
self.setTabIcon(idx, QIcon())
|
||||
if config.val.tabs.tabs_are_windows:
|
||||
self.window().setWindowIcon(self.window().windowIcon())
|
||||
|
||||
|
||||
class TabBar(QTabBar):
|
||||
|
||||
@ -358,7 +376,9 @@ class TabBar(QTabBar):
|
||||
# Clear _minimum_tab_size_hint_helper cache when appropriate
|
||||
if option in ["tabs.indicator.padding",
|
||||
"tabs.padding",
|
||||
"tabs.indicator.width"]:
|
||||
"tabs.indicator.width",
|
||||
"tabs.min_width",
|
||||
"tabs.pinned.shrink"]:
|
||||
self._minimum_tab_size_hint_helper.cache_clear()
|
||||
|
||||
def _on_show_switching_delay_changed(self):
|
||||
@ -478,6 +498,7 @@ class TabBar(QTabBar):
|
||||
index: The index of the tab to get a size hint for.
|
||||
ellipsis: Whether to use ellipsis to calculate width
|
||||
instead of the tab's text.
|
||||
Forced to False for pinned tabs.
|
||||
Return:
|
||||
A QSize of the smallest tab size we can make.
|
||||
"""
|
||||
@ -489,14 +510,19 @@ class TabBar(QTabBar):
|
||||
else:
|
||||
icon_width = min(icon.actualSize(self.iconSize()).width(),
|
||||
self.iconSize().width()) + icon_padding
|
||||
|
||||
pinned = self._tab_pinned(index)
|
||||
if not self.vertical and pinned and config.val.tabs.pinned.shrink:
|
||||
# Never consider ellipsis an option for horizontal pinned tabs
|
||||
ellipsis = False
|
||||
return self._minimum_tab_size_hint_helper(self.tabText(index),
|
||||
icon_width,
|
||||
ellipsis)
|
||||
icon_width, ellipsis,
|
||||
pinned)
|
||||
|
||||
@functools.lru_cache(maxsize=2**9)
|
||||
def _minimum_tab_size_hint_helper(self, tab_text: str,
|
||||
icon_width: int,
|
||||
ellipsis: bool) -> QSize:
|
||||
ellipsis: bool, pinned: bool) -> QSize:
|
||||
"""Helper function to cache tab results.
|
||||
|
||||
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
|
||||
width = (text_width + icon_width +
|
||||
padding_h + indicator_width)
|
||||
min_width = config.val.tabs.min_width
|
||||
if (not self.vertical and min_width > 0 and
|
||||
not pinned or not config.val.tabs.pinned.shrink):
|
||||
width = max(min_width, width)
|
||||
return QSize(width, height)
|
||||
|
||||
def _pinned_statistics(self) -> (int, int):
|
||||
@ -550,6 +580,12 @@ class TabBar(QTabBar):
|
||||
Return:
|
||||
A QSize.
|
||||
"""
|
||||
if self.count() == 0:
|
||||
# This happens on startup on macOS.
|
||||
# We return it directly rather than setting `size' because we don't
|
||||
# want to ensure it's valid in this special case.
|
||||
return QSize()
|
||||
|
||||
minimum_size = self.minimumTabSizeHint(index)
|
||||
height = minimum_size.height()
|
||||
if self.vertical:
|
||||
@ -562,11 +598,6 @@ class TabBar(QTabBar):
|
||||
else:
|
||||
width = int(confwidth)
|
||||
size = QSize(max(minimum_size.width(), width), height)
|
||||
elif self.count() == 0:
|
||||
# This happens on startup on macOS.
|
||||
# We return it directly rather than setting `size' because we don't
|
||||
# want to ensure it's valid in this special case.
|
||||
return QSize()
|
||||
else:
|
||||
if config.val.tabs.pinned.shrink:
|
||||
pinned = self._tab_pinned(index)
|
||||
@ -889,7 +920,7 @@ class TabBarStyle(QCommonStyle):
|
||||
# reserve space for favicon when tab bar is vertical (issue #1968)
|
||||
position = config.val.tabs.position
|
||||
if (position in [QTabWidget.East, QTabWidget.West] and
|
||||
config.val.tabs.favicons.show):
|
||||
config.val.tabs.favicons.show != 'never'):
|
||||
tab_icon_size = icon_size
|
||||
else:
|
||||
actual_size = opt.icon.actualSize(icon_size, icon_mode, icon_state)
|
||||
|
@ -42,6 +42,7 @@ class ExternalEditor(QObject):
|
||||
_proc: The GUIProcess of the editor.
|
||||
_watcher: A QFileSystemWatcher to watch the edited file for changes.
|
||||
Only set if watch=True.
|
||||
_content: The last-saved text of the editor.
|
||||
|
||||
Signals:
|
||||
file_updated: The text in the edited file was updated.
|
||||
@ -112,19 +113,7 @@ class ExternalEditor(QObject):
|
||||
if self._filename is not None:
|
||||
raise ValueError("Already editing a file!")
|
||||
try:
|
||||
# Close while the external process is running, as otherwise systems
|
||||
# with exclusive write access (e.g. Windows) may fail to update
|
||||
# the file from the external editor, see
|
||||
# https://github.com/qutebrowser/qutebrowser/issues/1767
|
||||
with tempfile.NamedTemporaryFile(
|
||||
# pylint: disable=bad-continuation
|
||||
mode='w', prefix='qutebrowser-editor-',
|
||||
encoding=config.val.editor.encoding,
|
||||
delete=False) as fobj:
|
||||
# pylint: enable=bad-continuation
|
||||
if text:
|
||||
fobj.write(text)
|
||||
self._filename = fobj.name
|
||||
self._filename = self._create_tempfile(text, 'qutebrowser-editor-')
|
||||
except OSError as e:
|
||||
message.error("Failed to create initial file: {}".format(e))
|
||||
return
|
||||
@ -134,6 +123,32 @@ class ExternalEditor(QObject):
|
||||
line, column = self._calc_line_and_column(text, caret_position)
|
||||
self._start_editor(line=line, column=column)
|
||||
|
||||
def backup(self):
|
||||
"""Create a backup if the content has changed from the original."""
|
||||
if not self._content:
|
||||
return
|
||||
try:
|
||||
fname = self._create_tempfile(self._content,
|
||||
'qutebrowser-editor-backup-')
|
||||
message.info('Editor backup at {}'.format(fname))
|
||||
except OSError as e:
|
||||
message.error('Failed to create editor backup: {}'.format(e))
|
||||
|
||||
def _create_tempfile(self, text, prefix):
|
||||
# Close while the external process is running, as otherwise systems
|
||||
# with exclusive write access (e.g. Windows) may fail to update
|
||||
# the file from the external editor, see
|
||||
# https://github.com/qutebrowser/qutebrowser/issues/1767
|
||||
with tempfile.NamedTemporaryFile(
|
||||
# pylint: disable=bad-continuation
|
||||
mode='w', prefix=prefix,
|
||||
encoding=config.val.editor.encoding,
|
||||
delete=False) as fobj:
|
||||
# pylint: enable=bad-continuation
|
||||
if text:
|
||||
fobj.write(text)
|
||||
return fobj.name
|
||||
|
||||
@pyqtSlot(str)
|
||||
def _on_file_changed(self, path):
|
||||
try:
|
||||
|
@ -130,7 +130,7 @@ class KeyHintView(QLabel):
|
||||
).format(
|
||||
html.escape(prefix),
|
||||
suffix_color,
|
||||
html.escape(str(seq[len(prefix):])),
|
||||
html.escape(str(seq)[len(prefix):]),
|
||||
html.escape(cmd)
|
||||
)
|
||||
text = '<table>{}</table>'.format(text)
|
||||
|
@ -246,7 +246,7 @@ class SessionManager(QObject):
|
||||
if tabbed_browser.private:
|
||||
win_data['private'] = True
|
||||
for i, tab in enumerate(tabbed_browser.widgets()):
|
||||
active = i == tabbed_browser.currentIndex()
|
||||
active = i == tabbed_browser.widget.currentIndex()
|
||||
win_data['tabs'].append(self._save_tab(tab, active))
|
||||
data['windows'].append(win_data)
|
||||
return data
|
||||
@ -427,11 +427,12 @@ class SessionManager(QObject):
|
||||
if tab.get('active', False):
|
||||
tab_to_focus = i
|
||||
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:
|
||||
tabbed_browser.setCurrentIndex(tab_to_focus)
|
||||
tabbed_browser.widget.setCurrentIndex(tab_to_focus)
|
||||
if win.get('active', False):
|
||||
QTimer.singleShot(0, tabbed_browser.activateWindow)
|
||||
QTimer.singleShot(0, tabbed_browser.widget.activateWindow)
|
||||
|
||||
if data['windows']:
|
||||
self.did_load = True
|
||||
|
@ -185,7 +185,7 @@ def debug_cache_stats():
|
||||
tabbed_browser = objreg.get('tabbed-browser', scope='window',
|
||||
window='last-focused')
|
||||
# pylint: disable=protected-access
|
||||
tab_bar = tabbed_browser.tabBar()
|
||||
tab_bar = tabbed_browser.widget.tabBar()
|
||||
tabbed_browser_info = tab_bar._minimum_tab_size_hint_helper.cache_info()
|
||||
# pylint: enable=protected-access
|
||||
|
||||
|
@ -87,10 +87,11 @@ def log_signals(obj):
|
||||
return ret
|
||||
|
||||
obj.__init__ = new_init
|
||||
return obj
|
||||
else:
|
||||
connect_log_slot(obj)
|
||||
|
||||
return obj
|
||||
|
||||
|
||||
def qenum_key(base, value, add_base=False, klass=None):
|
||||
"""Convert a Qt Enum value to its key as a string.
|
||||
|
@ -20,6 +20,9 @@
|
||||
"""Utilities related to javascript interaction."""
|
||||
|
||||
|
||||
from qutebrowser.utils import jinja
|
||||
|
||||
|
||||
def string_escape(text):
|
||||
"""Escape values special to javascript in strings.
|
||||
|
||||
@ -70,3 +73,9 @@ def assemble(module, function, *args):
|
||||
parts = ['window', '_qutebrowser', module, function]
|
||||
code = '"use strict";\n{}({});'.format('.'.join(parts), js_args)
|
||||
return code
|
||||
|
||||
|
||||
def wrap_global(name, *sources):
|
||||
"""Wrap a script using window._qutebrowser."""
|
||||
template = jinja.js_environment.get_template('global_wrapper.js')
|
||||
return template.render(code='\n'.join(sources), name=name)
|
||||
|
@ -171,7 +171,7 @@ def _get_tab_registry(win_id, tab_id):
|
||||
|
||||
if tab_id == 'current':
|
||||
tabbed_browser = get('tabbed-browser', scope='window', window=win_id)
|
||||
tab = tabbed_browser.currentWidget()
|
||||
tab = tabbed_browser.widget.currentWidget()
|
||||
if tab is None:
|
||||
raise RegistryUnavailableError('window')
|
||||
tab_id = tab.tab_id
|
||||
|
@ -102,6 +102,12 @@ def _get_search_url(txt):
|
||||
engine = 'DEFAULT'
|
||||
template = config.val.url.searchengines[engine]
|
||||
url = qurl_from_user_input(template.format(urllib.parse.quote(term)))
|
||||
|
||||
if config.val.url.open_base_url and term in config.val.url.searchengines:
|
||||
url = qurl_from_user_input(config.val.url.searchengines[term])
|
||||
url.setPath(None)
|
||||
url.setFragment(None)
|
||||
url.setQuery(None)
|
||||
qtutils.ensure_valid(url)
|
||||
return url
|
||||
|
||||
|
@ -317,8 +317,10 @@ def _chromium_version():
|
||||
Qt 5.8: Chromium 53
|
||||
Qt 5.9: Chromium 56
|
||||
Qt 5.10: Chromium 61
|
||||
Qt 5.11: Chromium 63
|
||||
Qt 5.12: Chromium 65 (?)
|
||||
Qt 5.11: Chromium 65
|
||||
Qt 5.12: Chromium 69 (?)
|
||||
|
||||
Also see https://www.chromium.org/developers/calendar
|
||||
"""
|
||||
if QWebEngineProfile is None:
|
||||
# This should never happen
|
||||
|
@ -361,7 +361,7 @@ def github_upload(artifacts, tag):
|
||||
repo = gh.repository('qutebrowser', 'qutebrowser')
|
||||
|
||||
release = None # to satisfy pylint
|
||||
for release in repo.iter_releases():
|
||||
for release in repo.releases():
|
||||
if release.tag_name == tag:
|
||||
break
|
||||
else:
|
||||
@ -401,14 +401,6 @@ def main():
|
||||
|
||||
run_asciidoc2html(args)
|
||||
if os.name == 'nt':
|
||||
if sys.maxsize > 2**32:
|
||||
# WORKAROUND
|
||||
print("Due to a python/Windows bug, this script needs to be run ")
|
||||
print("with a 32bit Python.")
|
||||
print()
|
||||
print("See http://bugs.python.org/issue24493 and ")
|
||||
print("https://github.com/pypa/virtualenv/issues/774")
|
||||
sys.exit(1)
|
||||
artifacts = build_windows()
|
||||
elif sys.platform == 'darwin':
|
||||
artifacts = build_mac()
|
||||
|
13
tests/end2end/data/hints/issue3711.html
Normal 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>
|
11
tests/end2end/data/hints/issue3711_frame.html
Normal 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>
|
@ -3,10 +3,10 @@
|
||||
|
||||
<head>
|
||||
<script type="text/javascript">
|
||||
var my_window;
|
||||
let my_window;
|
||||
|
||||
function open_modal() {
|
||||
window.open('about:blank', 'window', 'modal');
|
||||
my_window = window.open('about:blank', 'window', 'modal');
|
||||
}
|
||||
|
||||
function open_normal() {
|
||||
@ -17,13 +17,15 @@
|
||||
window.open('', 'my_window');
|
||||
}
|
||||
|
||||
function close() {
|
||||
function close_normal() {
|
||||
my_window.close();
|
||||
console.log("window closed");
|
||||
}
|
||||
|
||||
function close_twice() {
|
||||
my_window.close();
|
||||
my_window.close();
|
||||
console.log("window closed");
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
@ -33,7 +35,7 @@
|
||||
<button onclick="open_normal()" id="open-normal">normal</button>
|
||||
<button onclick="open_modal()" id="open-modal">modal</button>
|
||||
<button onclick="open_invalid()" id="open-invalid">invalid/no URL</button>
|
||||
<button onclick="close()" 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>
|
||||
|
||||
</body>
|
||||
|
@ -128,6 +128,7 @@ Feature: Opening external editors
|
||||
And I run :tab-close
|
||||
And I kill the waiting editor
|
||||
Then the error "Edited element vanished" should be shown
|
||||
And the message "Editor backup at *" should be shown
|
||||
|
||||
# Could not get signals working on Windows
|
||||
@posix
|
||||
|
@ -249,6 +249,11 @@ Feature: Using hints
|
||||
And I hint with args "all current" and follow a
|
||||
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
|
||||
|
||||
@not_mac @flaky
|
||||
|
@ -8,6 +8,7 @@ Feature: Javascript stuff
|
||||
When I open data/javascript/consolelog.html
|
||||
Then the javascript message "console.log works!" should be logged
|
||||
|
||||
@flaky
|
||||
Scenario: Opening/Closing a window via JS
|
||||
When I open data/javascript/window_open.html
|
||||
And I run :tab-only
|
||||
@ -15,7 +16,10 @@ Feature: Javascript stuff
|
||||
And I wait for "Changing title for idx 1 to 'about:blank'" in the log
|
||||
And I run :tab-focus 1
|
||||
And I run :click-element id close-normal
|
||||
And I wait for "[*] window closed" in the log
|
||||
Then "Focus object changed: *" should be logged
|
||||
And the following tabs should be open:
|
||||
- data/javascript/window_open.html (active)
|
||||
|
||||
@qtwebkit_skip
|
||||
Scenario: Opening/closing a modal window via JS
|
||||
@ -25,8 +29,11 @@ Feature: Javascript stuff
|
||||
And I wait for "Changing title for idx 1 to 'about:blank'" in the log
|
||||
And I run :tab-focus 1
|
||||
And I run :click-element id close-normal
|
||||
And I wait for "[*] window closed" in the log
|
||||
Then "Focus object changed: *" should be logged
|
||||
And "Web*Dialog requested, but we don't support that!" should be logged
|
||||
And the following tabs should be open:
|
||||
- data/javascript/window_open.html (active)
|
||||
|
||||
# https://github.com/qutebrowser/qutebrowser/issues/906
|
||||
|
||||
@ -39,6 +46,7 @@ Feature: Javascript stuff
|
||||
And I wait for "Changing title for idx 2 to 'about:blank'" in the log
|
||||
And I run :tab-focus 2
|
||||
And I run :click-element id close-twice
|
||||
And I wait for "[*] window closed" in the log
|
||||
Then "Requested to close * which does not exist!" should be logged
|
||||
|
||||
@qtwebkit_skip @flaky
|
||||
@ -51,6 +59,7 @@ Feature: Javascript stuff
|
||||
And I run :buffer window_open.html
|
||||
And I run :click-element id close-twice
|
||||
And I wait for "Focus object changed: *" in the log
|
||||
And I wait for "[*] window closed" in the log
|
||||
Then no crash should happen
|
||||
|
||||
@flaky
|
||||
@ -174,3 +183,15 @@ Feature: Javascript stuff
|
||||
When I set content.javascript.enabled to false
|
||||
And I open 500 without waiting
|
||||
Then "Showing error page for* 500" should be logged
|
||||
|
||||
Scenario: Using JS after window.open
|
||||
When I open data/hello.txt
|
||||
And I set content.javascript.can_open_tabs_automatically to true
|
||||
And I run :jseval window.open('about:blank')
|
||||
And I open data/hello.txt
|
||||
And I run :tab-only
|
||||
And I open data/hints/html/simple.html
|
||||
And I run :hint all
|
||||
And I wait for "hints: a" in the log
|
||||
And I run :leave-mode
|
||||
Then "There was an error while getting hint elements" should not be logged
|
||||
|
@ -26,7 +26,7 @@ Feature: Using :navigate
|
||||
# prev/next
|
||||
|
||||
Scenario: Navigating to previous page
|
||||
When I open data/navigate
|
||||
When I open data/navigate in a new tab
|
||||
And I run :navigate prev
|
||||
Then data/navigate/prev.html should be loaded
|
||||
|
||||
|
@ -336,13 +336,13 @@ Feature: Tab management
|
||||
When I set tabs.wrap to false
|
||||
And I open data/numbers/1.txt
|
||||
And I run :tab-prev
|
||||
Then the error "First tab" should be shown
|
||||
Then "First tab" should be logged
|
||||
|
||||
Scenario: :tab-next with last tab without wrap
|
||||
When I set tabs.wrap to false
|
||||
And I open data/numbers/1.txt
|
||||
And I run :tab-next
|
||||
Then the error "Last tab" should be shown
|
||||
Then "Last tab" should be logged
|
||||
|
||||
Scenario: :tab-prev on first tab with wrap
|
||||
When I set tabs.wrap to true
|
||||
|
@ -101,6 +101,9 @@ def is_ignored_lowlevel_message(message):
|
||||
' Error: No such file or directory',
|
||||
# Qt 5.7.1
|
||||
'qt.network.ssl: QSslSocket: cannot call unresolved function *',
|
||||
# Qt 5.11
|
||||
# DevTools listening on ws://127.0.0.1:37945/devtools/browser/...
|
||||
'DevTools listening on *',
|
||||
]
|
||||
return any(testutils.pattern_match(pattern=pattern, value=message)
|
||||
for pattern in ignored_messages)
|
||||
@ -169,7 +172,7 @@ def is_ignored_chromium_message(line):
|
||||
# /tmp/pytest-of-florian/pytest-32/test_webengine_download_suffix0/
|
||||
# downloads/download.bin: Operation not supported
|
||||
('Could not set extended attribute user.xdg.* on file *: '
|
||||
'Operation not supported'),
|
||||
'Operation not supported*'),
|
||||
# [5947:5947:0605/192837.856931:ERROR:render_process_impl.cc(112)]
|
||||
# WebFrame LEAKED 1 TIMES
|
||||
'WebFrame LEAKED 1 TIMES',
|
||||
@ -192,6 +195,15 @@ def is_ignored_chromium_message(line):
|
||||
# [2734:2746:1107/131154.072032:ERROR:nss_ocsp.cc(591)] No
|
||||
# URLRequestContext for NSS HTTP handler. host: ocsp.digicert.com
|
||||
'No URLRequestContext for NSS HTTP handler. host: *',
|
||||
|
||||
# https://bugreports.qt.io/browse/QTBUG-66661
|
||||
# [23359:23359:0319/115812.168578:WARNING:
|
||||
# render_frame_host_impl.cc(2744)] OnDidStopLoading was called twice.
|
||||
'OnDidStopLoading was called twice.',
|
||||
|
||||
# [30412:30412:0323/074933.387250:ERROR:node_channel.cc(899)] Dropping
|
||||
# message on closed channel.
|
||||
'Dropping message on closed channel.',
|
||||
]
|
||||
return any(testutils.pattern_match(pattern=pattern, value=message)
|
||||
for pattern in ignored_messages)
|
||||
|
@ -379,6 +379,7 @@ def test_qute_settings_persistence(short_tmpdir, request, quteproc_new):
|
||||
|
||||
@pytest.mark.no_xvfb
|
||||
@pytest.mark.no_ci
|
||||
@pytest.mark.not_mac
|
||||
def test_force_software_rendering(request, quteproc_new):
|
||||
"""Make sure we can force software rendering with -s."""
|
||||
if not request.config.webengine:
|
||||
|
@ -43,7 +43,8 @@ import helpers.stubs as stubsmod
|
||||
import helpers.utils
|
||||
from qutebrowser.config import (config, configdata, configtypes, configexc,
|
||||
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.misc import savemanager, sql
|
||||
from qutebrowser.keyinput import modeman
|
||||
@ -143,6 +144,47 @@ def fake_web_tab(stubs, tab_registry, mode_manager, qapp):
|
||||
return stubs.FakeWebTab
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def greasemonkey_manager(data_tmpdir):
|
||||
gm_manager = greasemonkey.GreasemonkeyManager()
|
||||
objreg.register('greasemonkey', gm_manager)
|
||||
yield
|
||||
objreg.delete('greasemonkey')
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def webkit_tab(qtbot, tab_registry, cookiejar_and_cache, mode_manager,
|
||||
session_manager_stub, greasemonkey_manager):
|
||||
webkittab = pytest.importorskip('qutebrowser.browser.webkit.webkittab')
|
||||
tab = webkittab.WebKitTab(win_id=0, mode_manager=mode_manager,
|
||||
private=False)
|
||||
qtbot.add_widget(tab)
|
||||
return tab
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def webengine_tab(qtbot, tab_registry, fake_args, mode_manager,
|
||||
session_manager_stub, greasemonkey_manager,
|
||||
redirect_webengine_data):
|
||||
webenginetab = pytest.importorskip(
|
||||
'qutebrowser.browser.webengine.webenginetab')
|
||||
tab = webenginetab.WebEngineTab(win_id=0, mode_manager=mode_manager,
|
||||
private=False)
|
||||
qtbot.add_widget(tab)
|
||||
return tab
|
||||
|
||||
|
||||
@pytest.fixture(params=['webkit', 'webengine'])
|
||||
def web_tab(request):
|
||||
"""A WebKitTab/WebEngineTab."""
|
||||
if request.param == 'webkit':
|
||||
return request.getfixturevalue('webkit_tab')
|
||||
elif request.param == 'webengine':
|
||||
return request.getfixturevalue('webengine_tab')
|
||||
else:
|
||||
raise utils.Unreachable
|
||||
|
||||
|
||||
def _generate_cmdline_tests():
|
||||
"""Generate testcases for test_split_binding."""
|
||||
@attr.s
|
||||
@ -193,11 +235,15 @@ def configdata_init():
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def config_stub(stubs, monkeypatch, configdata_init, config_tmpdir):
|
||||
"""Fixture which provides a fake config object."""
|
||||
yaml_config = configfiles.YamlConfig()
|
||||
def yaml_config_stub(config_tmpdir):
|
||||
"""Fixture which provides a YamlConfig object."""
|
||||
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)
|
||||
|
||||
container = config.ConfigContainer(conf)
|
||||
|
@ -27,6 +27,7 @@ import shutil
|
||||
|
||||
import attr
|
||||
from PyQt5.QtCore import pyqtSignal, QPoint, QProcess, QObject, QUrl
|
||||
from PyQt5.QtGui import QIcon
|
||||
from PyQt5.QtNetwork import (QNetworkRequest, QAbstractNetworkCache,
|
||||
QNetworkCacheMetaData)
|
||||
from PyQt5.QtWidgets import QCommonStyle, QLineEdit, QWidget, QTabBar
|
||||
@ -266,6 +267,9 @@ class FakeWebTab(browsertab.AbstractTab):
|
||||
def shutdown(self):
|
||||
pass
|
||||
|
||||
def icon(self):
|
||||
return QIcon()
|
||||
|
||||
|
||||
class FakeSignal:
|
||||
|
||||
@ -472,37 +476,55 @@ class SessionManagerStub:
|
||||
def list_sessions(self):
|
||||
return self.sessions
|
||||
|
||||
def save_autosave(self):
|
||||
pass
|
||||
|
||||
|
||||
class TabbedBrowserStub(QObject):
|
||||
|
||||
"""Stub for the tabbed-browser object."""
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self.widget = TabWidgetStub()
|
||||
self.shutting_down = False
|
||||
self.opened_url = None
|
||||
|
||||
def on_tab_close_requested(self, idx):
|
||||
del self.widget.tabs[idx]
|
||||
|
||||
def widgets(self):
|
||||
return self.widget.tabs
|
||||
|
||||
def tabopen(self, url):
|
||||
self.opened_url = url
|
||||
|
||||
def openurl(self, url, *, newtab):
|
||||
self.opened_url = url
|
||||
|
||||
|
||||
class TabWidgetStub(QObject):
|
||||
|
||||
"""Stub for the tab-widget object."""
|
||||
|
||||
new_tab = pyqtSignal(browsertab.AbstractTab, int)
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self.tabs = []
|
||||
self.shutting_down = False
|
||||
self._qtabbar = QTabBar()
|
||||
self.index_of = None
|
||||
self.current_index = None
|
||||
self.opened_url = None
|
||||
|
||||
def count(self):
|
||||
return len(self.tabs)
|
||||
|
||||
def widgets(self):
|
||||
return self.tabs
|
||||
|
||||
def widget(self, i):
|
||||
return self.tabs[i]
|
||||
|
||||
def page_title(self, i):
|
||||
return self.tabs[i].title()
|
||||
|
||||
def on_tab_close_requested(self, idx):
|
||||
del self.tabs[idx]
|
||||
|
||||
def tabBar(self):
|
||||
return self._qtabbar
|
||||
|
||||
@ -526,12 +548,6 @@ class TabbedBrowserStub(QObject):
|
||||
return None
|
||||
return self.tabs[idx - 1]
|
||||
|
||||
def tabopen(self, url):
|
||||
self.opened_url = url
|
||||
|
||||
def openurl(self, url, *, newtab):
|
||||
self.opened_url = url
|
||||
|
||||
|
||||
class ApplicationStub(QObject):
|
||||
|
||||
|
@ -120,8 +120,10 @@ def assert_urls(host_blocker, blocked=BLOCKLIST_HOSTS,
|
||||
|
||||
Ensure URLs in 'blocked' and not in 'whitelisted' are blocked.
|
||||
All other URLs must not be blocked.
|
||||
|
||||
localhost is an example of a special case that shouldn't be blocked.
|
||||
"""
|
||||
whitelisted = list(whitelisted) + list(host_blocker.WHITELISTED)
|
||||
whitelisted = list(whitelisted) + ['localhost']
|
||||
for str_url in urls_to_check:
|
||||
url = QUrl(str_url)
|
||||
host = url.host()
|
||||
@ -247,6 +249,16 @@ def test_successful_update(config_stub, basedir, download_stub,
|
||||
assert_urls(host_blocker, whitelisted=[])
|
||||
|
||||
|
||||
def test_parsing_multiple_hosts_on_line(config_stub, basedir, download_stub,
|
||||
data_tmpdir, tmpdir, win_registry,
|
||||
caplog):
|
||||
"""Ensure multiple hosts on a line get parsed correctly."""
|
||||
host_blocker = adblock.HostBlocker()
|
||||
bytes_host_line = ' '.join(BLOCKLIST_HOSTS).encode('utf-8')
|
||||
host_blocker._parse_line(bytes_host_line)
|
||||
assert_urls(host_blocker, whitelisted=[])
|
||||
|
||||
|
||||
def test_failed_dl_update(config_stub, basedir, download_stub,
|
||||
data_tmpdir, tmpdir, win_registry, caplog):
|
||||
"""One blocklist fails to download.
|
||||
@ -341,7 +353,7 @@ def test_blocking_with_whitelist(config_stub, basedir, download_stub,
|
||||
"""Ensure hosts in content.host_blocking.whitelist are never blocked."""
|
||||
# Simulate adblock_update has already been run
|
||||
# by creating a file named blocked-hosts,
|
||||
# Exclude localhost from it, since localhost is in HostBlocker.WHITELISTED
|
||||
# Exclude localhost from it as localhost is never blocked via list
|
||||
filtered_blocked_hosts = BLOCKLIST_HOSTS[1:]
|
||||
blocklist = create_blocklist(data_tmpdir,
|
||||
blocked_hosts=filtered_blocked_hosts,
|
||||
|
@ -68,8 +68,8 @@ def objects():
|
||||
@pytest.mark.parametrize('index_of, emitted', [(0, True), (1, False)])
|
||||
def test_filtering(objects, tabbed_browser_stubs, index_of, emitted):
|
||||
browser = tabbed_browser_stubs[0]
|
||||
browser.current_index = 0
|
||||
browser.index_of = index_of
|
||||
browser.widget.current_index = 0
|
||||
browser.widget.index_of = index_of
|
||||
objects.signaller.signal.emit('foo')
|
||||
if emitted:
|
||||
assert objects.signaller.filtered_signal_arg == 'foo'
|
||||
@ -80,8 +80,8 @@ def test_filtering(objects, tabbed_browser_stubs, index_of, emitted):
|
||||
@pytest.mark.parametrize('index_of, verb', [(0, 'emitting'), (1, 'ignoring')])
|
||||
def test_logging(caplog, objects, tabbed_browser_stubs, index_of, verb):
|
||||
browser = tabbed_browser_stubs[0]
|
||||
browser.current_index = 0
|
||||
browser.index_of = index_of
|
||||
browser.widget.current_index = 0
|
||||
browser.widget.index_of = index_of
|
||||
|
||||
with caplog.at_level(logging.DEBUG, logger='signals'):
|
||||
objects.signaller.signal.emit('foo')
|
||||
@ -94,8 +94,8 @@ def test_logging(caplog, objects, tabbed_browser_stubs, index_of, verb):
|
||||
@pytest.mark.parametrize('index_of', [0, 1])
|
||||
def test_no_logging(caplog, objects, tabbed_browser_stubs, index_of):
|
||||
browser = tabbed_browser_stubs[0]
|
||||
browser.current_index = 0
|
||||
browser.index_of = index_of
|
||||
browser.widget.current_index = 0
|
||||
browser.widget.index_of = index_of
|
||||
|
||||
with caplog.at_level(logging.DEBUG, logger='signals'):
|
||||
objects.signaller.link_hovered.emit('foo')
|
||||
@ -106,7 +106,7 @@ def test_no_logging(caplog, objects, tabbed_browser_stubs, index_of):
|
||||
def test_runtime_error(objects, tabbed_browser_stubs):
|
||||
"""Test that there's no crash if indexOf() raises RuntimeError."""
|
||||
browser = tabbed_browser_stubs[0]
|
||||
browser.current_index = 0
|
||||
browser.index_of = RuntimeError
|
||||
browser.widget.current_index = 0
|
||||
browser.widget.index_of = RuntimeError
|
||||
objects.signaller.signal.emit('foo')
|
||||
assert objects.signaller.filtered_signal_arg is None
|
||||
|
@ -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()
|
@ -40,9 +40,7 @@ def test_big_cache_size(config_stub):
|
||||
"""Make sure a too big cache size is handled correctly."""
|
||||
config_stub.val.content.cache.size = 2 ** 63 - 1
|
||||
profile = webenginesettings.default_profile
|
||||
|
||||
webenginesettings._set_http_cache_size(profile)
|
||||
|
||||
profile.setter.set_http_cache_size()
|
||||
assert profile.httpCacheMaximumSize() == 2 ** 31 - 1
|
||||
|
||||
|
||||
|
@ -97,6 +97,7 @@ def test_custom_env(qtbot, monkeypatch, py_proc, runner):
|
||||
f.write('\n')
|
||||
""")
|
||||
|
||||
with qtbot.waitSignal(runner.finished, timeout=10000):
|
||||
with qtbot.waitSignal(runner.got_cmd, timeout=10000) as blocker:
|
||||
runner.prepare_run(cmd, *args, env=env)
|
||||
runner.store_html('')
|
||||
|
@ -539,12 +539,12 @@ def test_session_completion(qtmodeltester, session_manager_stub):
|
||||
|
||||
def test_tab_completion(qtmodeltester, fake_web_tab, app_stub, win_registry,
|
||||
tabbed_browser_stubs):
|
||||
tabbed_browser_stubs[0].tabs = [
|
||||
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].tabs = [
|
||||
tabbed_browser_stubs[1].widget.tabs = [
|
||||
fake_web_tab(QUrl('https://wiki.archlinux.org'), 'ArchWiki', 0),
|
||||
]
|
||||
model = miscmodels.buffer()
|
||||
@ -567,12 +567,12 @@ def test_tab_completion(qtmodeltester, fake_web_tab, app_stub, win_registry,
|
||||
def test_tab_completion_delete(qtmodeltester, fake_web_tab, app_stub,
|
||||
win_registry, tabbed_browser_stubs):
|
||||
"""Verify closing a tab by deleting it from the completion widget."""
|
||||
tabbed_browser_stubs[0].tabs = [
|
||||
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].tabs = [
|
||||
tabbed_browser_stubs[1].widget.tabs = [
|
||||
fake_web_tab(QUrl('https://wiki.archlinux.org'), 'ArchWiki', 0),
|
||||
]
|
||||
model = miscmodels.buffer()
|
||||
@ -588,19 +588,19 @@ def test_tab_completion_delete(qtmodeltester, fake_web_tab, app_stub,
|
||||
assert model.data(idx) == '0/2'
|
||||
|
||||
model.delete_cur_item(idx)
|
||||
actual = [tab.url() for tab in tabbed_browser_stubs[0].tabs]
|
||||
actual = [tab.url() for tab in tabbed_browser_stubs[0].widget.tabs]
|
||||
assert actual == [QUrl('https://github.com'),
|
||||
QUrl('https://duckduckgo.com')]
|
||||
|
||||
|
||||
def test_other_buffer_completion(qtmodeltester, fake_web_tab, app_stub,
|
||||
win_registry, tabbed_browser_stubs, info):
|
||||
tabbed_browser_stubs[0].tabs = [
|
||||
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].tabs = [
|
||||
tabbed_browser_stubs[1].widget.tabs = [
|
||||
fake_web_tab(QUrl('https://wiki.archlinux.org'), 'ArchWiki', 0),
|
||||
]
|
||||
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,
|
||||
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://wikipedia.org'), 'Wikipedia', 1),
|
||||
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)
|
||||
]
|
||||
|
||||
|
@ -339,6 +339,24 @@ class TestKeyConfig:
|
||||
key_config_stub.unbind(seq)
|
||||
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):
|
||||
"""Try binding a key to an empty command."""
|
||||
message = "Can't add binding 'x' with empty command in normal mode"
|
||||
|
@ -371,7 +371,8 @@ class TestEdit:
|
||||
"""Tests for :config-edit."""
|
||||
|
||||
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):
|
||||
mock = mocker.patch('qutebrowser.config.configcommands.editor.'
|
||||
|
@ -211,8 +211,11 @@ class TestYaml:
|
||||
data = autoconfig.read()
|
||||
assert data == {'tabs.show': {'global': 'value'}}
|
||||
|
||||
@pytest.mark.parametrize('persist', [True, False])
|
||||
def test_merge_persist(self, yaml, autoconfig, persist):
|
||||
@pytest.mark.parametrize('persist, expected', [
|
||||
(True, 'persist'),
|
||||
(False, 'normal'),
|
||||
])
|
||||
def test_merge_persist(self, yaml, autoconfig, persist, expected):
|
||||
"""Tests for migration of tabs.persist_mode_on_change."""
|
||||
autoconfig.write({'tabs.persist_mode_on_change': {'global': persist}})
|
||||
yaml.load()
|
||||
@ -220,8 +223,7 @@ class TestYaml:
|
||||
|
||||
data = autoconfig.read()
|
||||
assert 'tabs.persist_mode_on_change' not in data
|
||||
mode = 'persist' if persist else 'normal'
|
||||
assert data['tabs.mode_on_change']['global'] == mode
|
||||
assert data['tabs.mode_on_change']['global'] == expected
|
||||
|
||||
def test_bindings_default(self, yaml, autoconfig):
|
||||
"""Make sure bindings.default gets removed from autoconfig.yml."""
|
||||
@ -233,6 +235,23 @@ class TestYaml:
|
||||
data = autoconfig.read()
|
||||
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,
|
||||
autoconfig):
|
||||
"""A key marked as renamed with invalid name should raise an error."""
|
||||
|
@ -292,7 +292,7 @@ class TestEarlyInit:
|
||||
'QT_XCB_FORCE_SOFTWARE_OPENGL', '1'),
|
||||
('qt.force_platform', 'toaster', 'QT_QPA_PLATFORM', 'toaster'),
|
||||
('qt.highdpi', True, 'QT_AUTO_SCREEN_SCALE_FACTOR', '1'),
|
||||
('window.hide_wayland_decoration', True,
|
||||
('window.hide_decoration', True,
|
||||
'QT_WAYLAND_DISABLE_WINDOWDECORATION', '1')
|
||||
])
|
||||
def test_env_vars(self, monkeypatch, config_stub,
|
||||
@ -347,6 +347,12 @@ class TestQtArgs:
|
||||
mocker.patch.object(parser, 'exit', side_effect=Exception)
|
||||
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', [
|
||||
# No Qt arguments
|
||||
(['--debug'], [sys.argv[0]]),
|
||||
@ -382,6 +388,15 @@ class TestQtArgs:
|
||||
config_stub.val.qt.args = ['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', [
|
||||
# overridden by commandline arg
|
||||
|
@ -533,6 +533,17 @@ class FlagListSubclass(configtypes.FlagList):
|
||||
'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:
|
||||
|
||||
"""Test List and FlagList."""
|
||||
@ -647,6 +658,12 @@ class TestList:
|
||||
with pytest.raises(AssertionError):
|
||||
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:
|
||||
|
||||
@ -1665,6 +1682,13 @@ class TestDict:
|
||||
print(doc)
|
||||
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):
|
||||
return configtypes.File(required=False, **kwargs)
|
||||
|
@ -17,118 +17,42 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# 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.path
|
||||
import logging
|
||||
|
||||
import pytest
|
||||
import jinja2
|
||||
|
||||
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 qutebrowser.utils.debug
|
||||
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:
|
||||
|
||||
"""Common subclass providing basic functionality for all JS testers.
|
||||
|
||||
Attributes:
|
||||
webview: The webview which is used.
|
||||
_qtbot: The QtBot fixture from pytest-qt.
|
||||
tab: The tab object which is used.
|
||||
qtbot: The QtBot fixture from pytest-qt.
|
||||
_jinja_env: The jinja2 environment used to get templates.
|
||||
"""
|
||||
|
||||
def __init__(self, webview, qtbot):
|
||||
self.webview = webview
|
||||
self._qtbot = qtbot
|
||||
def __init__(self, tab, qtbot, config_stub):
|
||||
self.tab = tab
|
||||
self.qtbot = qtbot
|
||||
loader = jinja2.FileSystemLoader(os.path.dirname(__file__))
|
||||
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):
|
||||
"""Load and display the given jinja test data.
|
||||
@ -139,9 +63,9 @@ class JSTester:
|
||||
**kwargs: Passed to jinja's template.render().
|
||||
"""
|
||||
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:
|
||||
self.webview.setHtml(template.render(**kwargs))
|
||||
self.tab.set_html(template.render(**kwargs))
|
||||
assert blocker.args == [True]
|
||||
|
||||
def load_file(self, path: str, force: bool = False):
|
||||
@ -161,77 +85,13 @@ class JSTester:
|
||||
url: The QUrl to load.
|
||||
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:
|
||||
self.webview.load(url)
|
||||
self.tab.openurl(url)
|
||||
if not force:
|
||||
assert blocker.args == [True]
|
||||
|
||||
|
||||
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:
|
||||
def run_file(self, filename: str, expected=None) -> None:
|
||||
"""Run a javascript file.
|
||||
|
||||
Args:
|
||||
@ -250,24 +110,24 @@ class JSWebEngineTester(JSTester):
|
||||
expected: The value expected return from the javascript execution
|
||||
world: The scope the javascript will run in
|
||||
"""
|
||||
if world is None:
|
||||
world = QWebEngineScript.ApplicationWorld
|
||||
|
||||
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 = helpers.utils.CallbackChecker(self.qtbot)
|
||||
self.tab.run_js_async(source, callback_checker.callback, world=world)
|
||||
callback_checker.check(expected)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def js_tester_webkit(webview, qtbot):
|
||||
def js_tester_webkit(webkit_tab, qtbot, config_stub):
|
||||
"""Fixture to test javascript snippets in webkit."""
|
||||
return JSWebKitTester(webview, qtbot)
|
||||
return JSTester(webkit_tab, qtbot, config_stub)
|
||||
|
||||
|
||||
@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."""
|
||||
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)
|
||||
|
@ -21,12 +21,10 @@
|
||||
|
||||
import pytest
|
||||
|
||||
# FIXME:qtwebengine Make these tests use the tab API
|
||||
pytest.importorskip('PyQt5.QtWebKit')
|
||||
import helpers.utils
|
||||
|
||||
from PyQt5.QtCore import Qt
|
||||
from PyQt5.QtWebKit import QWebSettings
|
||||
from PyQt5.QtWebKitWidgets import QWebPage
|
||||
QWebSettings = pytest.importorskip("PyQt5.QtWebKit").QWebSettings
|
||||
QWebPage = pytest.importorskip("PyQt5.QtWebKitWidgets").QWebPage
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
@ -53,15 +51,17 @@ class CaretTester:
|
||||
def check(self):
|
||||
"""Check whether the caret is before the MARKER text."""
|
||||
self.js.run_file('position_caret.js')
|
||||
self.js.webview.triggerPageAction(QWebPage.SelectNextWord)
|
||||
assert self.js.webview.selectedText().rstrip() == "MARKER"
|
||||
self.js.tab.caret.toggle_selection()
|
||||
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):
|
||||
"""Check if the page is scrolled down."""
|
||||
frame = self.js.webview.page().mainFrame()
|
||||
minimum = frame.scrollBarMinimum(Qt.Vertical)
|
||||
value = frame.scrollBarValue(Qt.Vertical)
|
||||
assert value > minimum
|
||||
assert not self.js.tab.scroller.at_top()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@ -70,7 +70,7 @@ def caret_tester(js_tester_webkit):
|
||||
caret_tester = CaretTester(js_tester_webkit)
|
||||
# Showing webview here is necessary for test_scrolled_down_img to
|
||||
# succeed in some cases, see #1988
|
||||
caret_tester.js.webview.show()
|
||||
caret_tester.js.tab.show()
|
||||
return caret_tester
|
||||
|
||||
|
||||
@ -82,10 +82,11 @@ def test_simple(caret_tester):
|
||||
|
||||
|
||||
@pytest.mark.integration
|
||||
@pytest.mark.no_xvfb
|
||||
def test_scrolled_down(caret_tester):
|
||||
"""Test with multiple text blocks with the viewport scrolled down."""
|
||||
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()
|
||||
|
||||
@ -99,9 +100,10 @@ def test_invisible(caret_tester, style):
|
||||
|
||||
|
||||
@pytest.mark.integration
|
||||
@pytest.mark.no_xvfb
|
||||
def test_scrolled_down_img(caret_tester):
|
||||
"""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.scroll_anchor('anchor')
|
||||
caret_tester.js.tab.scroller.to_anchor('anchor')
|
||||
caret_tester.check_scrolled()
|
||||
caret_tester.check()
|
||||
|
@ -27,11 +27,6 @@ QWebEngineProfile = QtWebEngineWidgets.QWebEngineProfile
|
||||
|
||||
from qutebrowser.utils import javascript
|
||||
|
||||
try:
|
||||
from qutebrowser.browser.webengine import webenginesettings
|
||||
except ImportError:
|
||||
webenginesettings = None
|
||||
|
||||
|
||||
DEFAULT_BODY_BG = "rgba(0, 0, 0, 0)"
|
||||
GREEN_BODY_BG = "rgb(0, 255, 0)"
|
||||
@ -56,8 +51,6 @@ class StylesheetTester:
|
||||
"""Initialize the stylesheet with a provided css file."""
|
||||
css_path = os.path.join(os.path.dirname(__file__), css_file)
|
||||
self.config_stub.val.content.user_stylesheets = css_path
|
||||
p = QWebEngineProfile.defaultProfile()
|
||||
webenginesettings._init_stylesheet(p)
|
||||
|
||||
def set_css(self, css):
|
||||
"""Set document style to `css` via stylesheet.js."""
|
||||
@ -67,10 +60,12 @@ class StylesheetTester:
|
||||
def check_set(self, value, css_style="background-color",
|
||||
document_element="document.body"):
|
||||
"""Check whether the css in ELEMENT is set to VALUE."""
|
||||
self.js.run("window.getComputedStyle({}, null)"
|
||||
".getPropertyValue('{}');"
|
||||
.format(document_element,
|
||||
javascript.string_escape(css_style)), value)
|
||||
self.js.run("console.log({document});"
|
||||
"window.getComputedStyle({document}, null)"
|
||||
".getPropertyValue('{prop}');".format(
|
||||
document=document_element,
|
||||
prop=javascript.string_escape(css_style)),
|
||||
value)
|
||||
|
||||
def check_eq(self, one, two, true=True):
|
||||
"""Check if one and two are equal."""
|
||||
@ -81,7 +76,7 @@ class StylesheetTester:
|
||||
def stylesheet_tester(js_tester_webengine, config_stub):
|
||||
"""Helper fixture to test stylesheets."""
|
||||
ss_tester = StylesheetTester(js_tester_webengine, config_stub)
|
||||
ss_tester.js.webview.show()
|
||||
ss_tester.js.tab.show()
|
||||
return ss_tester
|
||||
|
||||
|
||||
@ -89,8 +84,8 @@ def stylesheet_tester(js_tester_webengine, config_stub):
|
||||
'stylesheet/simple_bg_set_red.html'])
|
||||
def test_set_delayed(stylesheet_tester, page):
|
||||
"""Test a delayed invocation of set_css."""
|
||||
stylesheet_tester.init_stylesheet("none.css")
|
||||
stylesheet_tester.js.load(page)
|
||||
stylesheet_tester.init_stylesheet("none.css")
|
||||
stylesheet_tester.set_css("body {background-color: 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'])
|
||||
def test_set_clear_bg(stylesheet_tester, page):
|
||||
"""Test setting and clearing the stylesheet."""
|
||||
stylesheet_tester.init_stylesheet()
|
||||
stylesheet_tester.js.load('stylesheet/simple.html')
|
||||
stylesheet_tester.init_stylesheet()
|
||||
stylesheet_tester.check_set(GREEN_BODY_BG)
|
||||
stylesheet_tester.set_css("")
|
||||
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):
|
||||
"""Test stylesheet is applied without altering xml files."""
|
||||
stylesheet_tester.init_stylesheet()
|
||||
stylesheet_tester.js.load_file('stylesheet/simple.xml')
|
||||
stylesheet_tester.init_stylesheet()
|
||||
stylesheet_tester.check_set(GREEN_BODY_BG)
|
||||
stylesheet_tester.check_eq('"html"', "document.documentElement.nodeName")
|
||||
|
||||
|
||||
def test_set_svg(stylesheet_tester):
|
||||
"""Test stylesheet is applied for svg files."""
|
||||
stylesheet_tester.init_stylesheet()
|
||||
stylesheet_tester.js.load_file('../../../misc/cheatsheet.svg')
|
||||
stylesheet_tester.init_stylesheet()
|
||||
stylesheet_tester.check_set(GREEN_BODY_BG,
|
||||
document_element="document.documentElement")
|
||||
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."""
|
||||
config_stub.changed.disconnect() # This test is flaky otherwise...
|
||||
stylesheet_tester.init_stylesheet()
|
||||
stylesheet_tester.js.tab._init_stylesheet()
|
||||
stylesheet_tester.js.load_file('non-existent.html', force=True)
|
||||
stylesheet_tester.check_set(GREEN_BODY_BG)
|
||||
|
||||
|
||||
def test_appendchild(stylesheet_tester):
|
||||
stylesheet_tester.init_stylesheet()
|
||||
stylesheet_tester.js.load('stylesheet/simple.html')
|
||||
stylesheet_tester.init_stylesheet()
|
||||
js_test_file_path = ('../../tests/unit/javascript/stylesheet/'
|
||||
'test_appendchild.js')
|
||||
stylesheet_tester.js.run_file(js_test_file_path, {})
|
||||
|
@ -47,15 +47,32 @@ def test_element_js_webkit(webview, js_enabled, expected):
|
||||
|
||||
|
||||
@pytest.mark.usefixtures('redirect_webengine_data')
|
||||
@pytest.mark.parametrize('js_enabled, expected', [(True, 2.0), (False, 2.0)])
|
||||
def test_simple_js_webengine(callback_checker, webengineview, js_enabled,
|
||||
expected):
|
||||
@pytest.mark.parametrize('js_enabled, world, expected', [
|
||||
# main world
|
||||
(True, 0, 2.0),
|
||||
(False, 0, None),
|
||||
# application world
|
||||
(True, 1, 2.0),
|
||||
(False, 1, 2.0),
|
||||
# user world
|
||||
(True, 2, 2.0),
|
||||
(False, 2, 2.0),
|
||||
])
|
||||
def test_simple_js_webengine(callback_checker, webengineview, qapp,
|
||||
js_enabled, world, expected):
|
||||
"""With QtWebEngine, runJavaScript works even when JS is off."""
|
||||
# If we get there (because of the webengineview fixture) we can be certain
|
||||
# QtWebEngine is available
|
||||
from PyQt5.QtWebEngineWidgets import QWebEngineSettings
|
||||
webengineview.settings().setAttribute(QWebEngineSettings.JavascriptEnabled,
|
||||
js_enabled)
|
||||
from PyQt5.QtWebEngineWidgets import QWebEngineSettings, QWebEngineScript
|
||||
|
||||
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)
|
@ -25,6 +25,7 @@ from PyQt5.QtCore import Qt
|
||||
import pytest
|
||||
|
||||
from qutebrowser.keyinput import basekeyparser, keyutils
|
||||
from qutebrowser.utils import utils
|
||||
|
||||
|
||||
# Alias because we need this a lot in here.
|
||||
@ -153,14 +154,16 @@ class TestHandle:
|
||||
keyparser._read_config('prompt')
|
||||
|
||||
def test_valid_key(self, fake_keyevent, keyparser):
|
||||
keyparser.handle(fake_keyevent(Qt.Key_A, Qt.ControlModifier))
|
||||
keyparser.handle(fake_keyevent(Qt.Key_X, Qt.ControlModifier))
|
||||
modifier = Qt.MetaModifier if utils.is_mac else 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)
|
||||
assert not keyparser._sequence
|
||||
|
||||
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_A, Qt.ControlModifier))
|
||||
keyparser.handle(fake_keyevent(Qt.Key_A, modifier))
|
||||
keyparser.execute.assert_called_once_with('message-info ctrla', 5)
|
||||
|
||||
@pytest.mark.parametrize('keys', [
|
||||
@ -198,13 +201,34 @@ class TestHandle:
|
||||
keyparser.execute.assert_called_with('message-info ba', None)
|
||||
assert not keyparser._sequence
|
||||
|
||||
@pytest.mark.parametrize('key, number', [(Qt.Key_0, 0), (Qt.Key_1, 1)])
|
||||
def test_number_press(self, handle_text, keyparser, key, number):
|
||||
handle_text(key)
|
||||
@pytest.mark.parametrize('key, modifiers, number', [
|
||||
(Qt.Key_0, Qt.NoModifier, 0),
|
||||
(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)
|
||||
keyparser.execute.assert_called_once_with(command, None)
|
||||
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):
|
||||
config_stub.val.bindings.commands = {'normal': {'ü': 'message-info ü'}}
|
||||
keyparser._read_config('normal')
|
||||
@ -215,6 +239,15 @@ class TestHandle:
|
||||
handle_text(Qt.Key_X)
|
||||
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):
|
||||
"""with a conflicting binding/mapping, the binding should win."""
|
||||
handle_text(Qt.Key_B)
|
||||
@ -296,6 +329,15 @@ class TestCount:
|
||||
assert sig1.args == ('4',)
|
||||
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):
|
||||
"""Test that the keystring is cleared and the signal is emitted."""
|
||||
|
@ -28,6 +28,7 @@ from PyQt5.QtWidgets import QWidget
|
||||
|
||||
from tests.unit.keyinput import key_data
|
||||
from qutebrowser.keyinput import keyutils
|
||||
from qutebrowser.utils import utils
|
||||
|
||||
|
||||
@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', [
|
||||
('a', Qt.Key_B, Qt.NoModifier, 'b', 'ab'),
|
||||
('a', Qt.Key_B, Qt.ShiftModifier, 'B', 'aB'),
|
||||
('a', Qt.Key_B, Qt.ControlModifier | Qt.ShiftModifier, 'B',
|
||||
'a<Ctrl+Shift+b>'),
|
||||
('a', Qt.Key_B, Qt.AltModifier | Qt.ShiftModifier, 'B',
|
||||
'a<Alt+Shift+b>'),
|
||||
|
||||
# Modifier stripping with symbols
|
||||
('', Qt.Key_Colon, Qt.NoModifier, ':', ':'),
|
||||
('', Qt.Key_Colon, Qt.ShiftModifier, ':', ':'),
|
||||
('', Qt.Key_Colon, Qt.ControlModifier | Qt.ShiftModifier, ':',
|
||||
'<Ctrl+Shift+:>'),
|
||||
('', Qt.Key_Colon, Qt.AltModifier | Qt.ShiftModifier, ':',
|
||||
'<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
|
||||
('', Qt.Key_Backtab, Qt.NoModifier, '', '<Backtab>'),
|
||||
('', Qt.Key_Backtab, Qt.ShiftModifier, '', '<Shift+Tab>'),
|
||||
('', Qt.Key_Backtab, Qt.ControlModifier | Qt.ShiftModifier, '',
|
||||
'<Control+Shift+Tab>'),
|
||||
('', Qt.Key_Backtab, Qt.AltModifier | Qt.ShiftModifier, '',
|
||||
'<Alt+Shift+Tab>'),
|
||||
|
||||
# Stripping of Qt.GroupSwitchModifier
|
||||
('', Qt.Key_A, Qt.GroupSwitchModifier, 'a', 'a'),
|
||||
@ -370,6 +379,27 @@ class TestKeySequence:
|
||||
new = seq.append_event(event)
|
||||
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])
|
||||
def test_append_event_invalid(self, key):
|
||||
seq = keyutils.KeySequence()
|
||||
@ -377,6 +407,15 @@ class TestKeySequence:
|
||||
with pytest.raises(keyutils.KeyParseError):
|
||||
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):
|
||||
seq = keyutils.KeySequence.parse('foobar')
|
||||
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_X, Qt.ControlModifier, True),
|
||||
(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):
|
||||
assert keyutils.is_special(key, modifiers) == special
|
||||
|
@ -96,3 +96,11 @@ class TestHintKeyParser:
|
||||
assert match == QKeySequence.ExactMatch
|
||||
|
||||
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
|
||||
|