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

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

View File

@ -44,7 +44,7 @@ ignore =
min-version = 3.4.0
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

View File

@ -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

View File

@ -99,7 +99,7 @@ Requirements
The following software and libraries are required to run qutebrowser:
* http://www.python.org/[Python] 3.5 or newer (3.6 recommended)
* http://qt.io/[Qt] 5.7.1 or newer 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]

View File

@ -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
------

View File

@ -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.

View File

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

View File

@ -93,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']+

View File

@ -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]

View File

@ -236,10 +236,11 @@
|<<tabs.close_mouse_button,tabs.close_mouse_button>>|Mouse button with which to close tabs.
|<<tabs.close_mouse_button_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&amp;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.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 MiB

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

After

Width:  |  Height:  |  Size: 46 KiB

View File

@ -47,17 +47,26 @@ Debian Stretch / Ubuntu 17.04 and 17.10
Those versions come with QtWebEngine in the repositories. This makes it possible
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`):

Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 KiB

After

Width:  |  Height:  |  Size: 113 KiB

View File

@ -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/*))

View File

@ -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

View File

@ -15,7 +15,7 @@ def get_data_files():
('../qutebrowser/img', 'img'),
('../qutebrowser/javascript', 'javascript'),
('../qutebrowser/html/doc', 'html/doc'),
('../qutebrowser/git-commit-id', ''),
('../qutebrowser/git-commit-id', '.'),
('../qutebrowser/config/configdata.yml', 'config'),
]
@ -58,14 +58,14 @@ exe = EXE(pyz,
icon=icon,
debug=False,
strip=False,
upx=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,

View File

@ -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

View File

@ -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

View File

@ -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

View File

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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

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

69
misc/userscripts/getbib Executable file
View File

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

View File

@ -28,7 +28,7 @@
if msg="$(task add "$title" "$*" 2>&1)"; then
# 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

View File

@ -26,7 +26,7 @@ __copyright__ = "Copyright 2014-2018 Florian Bruhin (The Compiler)"
__license__ = "GPL"
__maintainer__ = __author__
__email__ = "mail@qutebrowser.org"
__version_info__ = (1, 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."

View File

@ -340,7 +340,7 @@ def _open_startpage(win_id=None):
for cur_win_id in list(window_ids): # Copying as the dict could change
tabbed_browser = objreg.get('tabbed-browser', scope='window',
window=cur_win_id)
if tabbed_browser.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)

View File

@ -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,16 +228,14 @@ 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:
self._blocked_hosts.add(host)
for host in hosts:
if '.' in host and not host.endswith('.localdomain'):
self._blocked_hosts.add(host)
return True

View File

@ -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)
self.predicted_navigation.emit(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):

View File

@ -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)

View File

@ -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,12 +105,13 @@ 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]),
scriptInfo=self._meta_json(),
scriptMeta=self.script_meta,
scriptSource=self._code)
template = jinja.js_environment.get_template('greasemonkey_wrapper.js')
return template.render(
scriptName=javascript.string_escape(
"/".join([self.namespace or '', self.name])),
scriptInfo=self._meta_json(),
scriptMeta=javascript.string_escape(self.script_meta),
scriptSource=self._code)
def _meta_json(self):
return json.dumps({

View File

@ -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!")

View File

@ -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()

View File

@ -76,11 +76,11 @@ class SignalFilter(QObject):
tabbed_browser = objreg.get('tabbed-browser', scope='window',
window=self._win_id)
try:
tabidx = tabbed_browser.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))

View File

@ -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):

View File

@ -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,133 +165,92 @@ 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 set_http_headers(self):
"""Set the user agent and accept-language for the given profile.
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)
We override those per request in the URL interceptor (to allow for
per-domain values), but this one still gets used for things like
window.navigator.userAgent/.languages in JS.
"""
self._profile.setHttpUserAgent(config.val.content.headers.user_agent)
accept_language = config.val.content.headers.accept_language
if accept_language is not None:
self._profile.setHttpAcceptLanguage(accept_language)
def set_http_cache_size(self):
"""Initialize the HTTP cache size for the given profile."""
size = config.val.content.cache.size
if size is None:
size = 0
else:
size = qtutils.check_overflow(size, 'int', fatal=False)
def _set_http_headers(profile):
"""Set the user agent and accept-language for the given profile.
# 0: automatically managed by QtWebEngine
self._profile.setHttpCacheMaximumSize(size)
We override those per request in the URL interceptor (to allow for
per-domain values), but this one still gets used for things like
window.navigator.userAgent/.languages in JS.
"""
profile.setHttpUserAgent(config.val.content.headers.user_agent)
accept_language = config.val.content.headers.accept_language
if accept_language is not None:
profile.setHttpAcceptLanguage(accept_language)
def set_persistent_cookie_policy(self):
"""Set the HTTP Cookie size for the given profile."""
assert not self._profile.isOffTheRecord()
if config.val.content.cookies.store:
value = QWebEngineProfile.AllowPersistentCookies
else:
value = QWebEngineProfile.NoPersistentCookies
self._profile.setPersistentCookiesPolicy(value)
def set_dictionary_language(self, warn=True):
"""Load the given dictionaries."""
filenames = []
for code in config.val.spellcheck.languages or []:
local_filename = spell.local_filename(code)
if not local_filename:
if warn:
message.warning("Language {} is not installed - see "
"scripts/dictcli.py in qutebrowser's "
"sources".format(code))
continue
def _set_http_cache_size(profile):
"""Initialize the HTTP cache size for the given profile."""
size = config.val.content.cache.size
if size is None:
size = 0
else:
size = qtutils.check_overflow(size, 'int', fatal=False)
filenames.append(local_filename)
# 0: automatically managed by QtWebEngine
profile.setHttpCacheMaximumSize(size)
def _set_persistent_cookie_policy(profile):
"""Set the HTTP Cookie size for the given profile."""
if config.val.content.cookies.store:
value = QWebEngineProfile.AllowPersistentCookies
else:
value = QWebEngineProfile.NoPersistentCookies
profile.setPersistentCookiesPolicy(value)
def _set_dictionary_language(profile, warn=True):
filenames = []
for code in config.val.spellcheck.languages or []:
local_filename = spell.local_filename(code)
if not local_filename:
if warn:
message.warning(
"Language {} is not installed - see scripts/dictcli.py "
"in qutebrowser's sources".format(code))
continue
filenames.append(local_filename)
log.config.debug("Found dicts: {}".format(filenames))
profile.setSpellCheckLanguages(filenames)
log.config.debug("Found dicts: {}".format(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',
'content.headers.accept_language']:
_set_http_headers(default_profile)
_set_http_headers(private_profile)
if option in ['content.headers.user_agent',
'content.headers.accept_language']:
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):

View File

@ -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)

View File

@ -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:

View File

@ -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.

View File

@ -110,18 +110,18 @@ def _buffer(skip_win_id=None):
model = completionmodel.CompletionModel(column_widths=(6, 40, 54))
for win_id in objreg.window_registry:
if skip_win_id 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)

View File

@ -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:

View File

@ -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):

View File

@ -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

View File

@ -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:

View File

@ -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:

View File

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

View File

@ -324,9 +324,8 @@ window._qutebrowser.caret = (function() {
const color = axs.color.parseColor(style.backgroundColor);
if (color &&
(style.opacity < 1 &&
(color.alpha *= style.opacity),
color.alpha !== 0 &&
(el.push(color), color.alpha === 1))) {
(color.alpha *= style.opacity), color.alpha !== 0 &&
(el.push(color), color.alpha === 1))) {
iter = !0;
break;
}
@ -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;

View File

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

View File

@ -1,5 +1,5 @@
(function() {
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);
}

View File

@ -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 {

View File

@ -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,28 +178,15 @@ 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))
return QKeySequence.ExactMatch
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:
return match

View File

@ -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 = []

View File

@ -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()

View File

@ -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.
"""
path = os.path.normpath(self._file_model.filePath(index))
if index == QModelIndex():
path = os.path.join(self._file_model.rootPath(), self._to_complete)
else:
path = os.path.normpath(self._file_model.filePath(index))
if clicked:
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')]

View File

@ -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()

View File

@ -268,7 +268,7 @@ class StatusBar(QWidget):
"""Get the currently displayed tab."""
window = objreg.get('tabbed-browser', scope='window',
window=self._win_id)
return window.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.

View File

@ -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:

View File

@ -60,7 +60,7 @@ class TabWidget(QTabWidget):
self.setTabBar(bar)
bar.tabCloseRequested.connect(self.tabCloseRequested)
bar.tabMoved.connect(functools.partial(
QTimer.singleShot, 0, self._update_tab_titles))
QTimer.singleShot, 0, self.update_tab_titles))
bar.currentChanged.connect(self._on_current_changed)
bar.new_tab_requested.connect(self._on_new_tab_requested)
self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
@ -108,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):
@ -477,7 +497,8 @@ class TabBar(QTabBar):
Args:
index: The index of the tab to get a size hint for.
ellipsis: Whether to use ellipsis to calculate width
instead of the tab's text.
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)

View File

@ -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:

View File

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

View File

@ -246,7 +246,7 @@ class SessionManager(QObject):
if tabbed_browser.private:
win_data['private'] = True
for i, tab in enumerate(tabbed_browser.widgets()):
active = i == tabbed_browser.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

View File

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

View File

@ -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.

View File

@ -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)

View File

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

View File

@ -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

View File

@ -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

View File

@ -361,7 +361,7 @@ def github_upload(artifacts, tag):
repo = gh.repository('qutebrowser', 'qutebrowser')
release = None # to satisfy pylint
for release in repo.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()

View File

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

View File

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

View File

@ -3,10 +3,10 @@
<head>
<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>

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -336,13 +336,13 @@ Feature: Tab management
When I set tabs.wrap to false
And I open data/numbers/1.txt
And I run :tab-prev
Then 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

View File

@ -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)

View File

@ -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:

View File

@ -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)

View File

@ -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):

View File

@ -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,

View File

@ -68,8 +68,8 @@ def objects():
@pytest.mark.parametrize('index_of, emitted', [(0, True), (1, False)])
def test_filtering(objects, tabbed_browser_stubs, index_of, emitted):
browser = tabbed_browser_stubs[0]
browser.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

View File

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

View File

@ -40,9 +40,7 @@ def test_big_cache_size(config_stub):
"""Make sure a too big cache size is handled correctly."""
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

View File

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

View File

@ -539,12 +539,12 @@ def test_session_completion(qtmodeltester, session_manager_stub):
def test_tab_completion(qtmodeltester, fake_web_tab, app_stub, win_registry,
tabbed_browser_stubs):
tabbed_browser_stubs[0].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)
]

View File

@ -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"

View File

@ -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.'

View File

@ -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."""

View File

@ -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

View File

@ -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)

View File

@ -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,
timeout=2000) as blocker:
self.webview.setHtml(template.render(**kwargs))
with self.qtbot.waitSignal(self.tab.load_finished,
timeout=2000) as blocker:
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,
timeout=2000) as blocker:
self.webview.load(url)
with self.qtbot.waitSignal(self.tab.load_finished,
timeout=2000) as blocker:
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)

View File

@ -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()

View File

@ -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, {})

View File

@ -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)

View File

@ -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."""

View File

@ -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

View File

@ -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

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