Merge branch 'qt6-v2' into master-qt6

This commit is contained in:
Florian Bruhin 2023-03-17 20:30:13 +01:00
commit 4793070db3
330 changed files with 13946 additions and 4561 deletions

View File

@ -21,7 +21,9 @@ jobs:
include:
- testenv: pylint
- testenv: flake8
- testenv: mypy
# FIXME:qt6 (lint)
# - testenv: mypy-pyqt6
- testenv: mypy-pyqt5
- testenv: docs
- testenv: vulture
- testenv: misc
@ -90,11 +92,11 @@ jobs:
image:
- archlinux-webkit
- archlinux-webengine
# - archlinux-webengine-qt6 # FIXME:qt6 activate
# - archlinux-webengine-unstable
container:
image: "qutebrowser/ci:${{ matrix.image }}"
env:
QUTE_BDD_WEBENGINE: "${{ matrix.image != 'archlinux-webkit' }}"
DOCKER: "${{ matrix.image }}"
CI: true
PYTEST_ADDOPTS: "--color=yes"
@ -119,30 +121,32 @@ jobs:
fail-fast: false
matrix:
include:
### PyQt 5.12 (Python 3.7)
- testenv: py37-pyqt512
os: ubuntu-20.04
python: "3.7"
### PyQt 5.13 (Python 3.7)
- testenv: py37-pyqt513
os: ubuntu-20.04
python: "3.7"
### PyQt 5.14 (Python 3.8)
- testenv: py38-pyqt514
os: ubuntu-20.04
python: "3.8"
### PyQt 5.15.0 (Python 3.9)
- testenv: py39-pyqt5150
### PyQt 5.15.2 (Python 3.9)
- testenv: py39-pyqt5152
os: ubuntu-20.04
python: "3.9"
### PyQt 5.15 (Python 3.10, with coverage)
- testenv: py310-pyqt515-cov
os: ubuntu-22.04
python: "3.10"
### PyQt 5.15 (Python 3.9, with coverage)
# FIXME:qt6
# - testenv: py39-pyqt515-cov
# os: ubuntu-22.04
# python: "3.10"
### PyQt 5.15 (Python 3.11)
- testenv: py311-pyqt515
os: ubuntu-20.04
python: "3.11-dev"
### PyQt 6.2 (Python 3.9)
- testenv: py39-pyqt62
os: ubuntu-20.04
python: 3.9
### PyQt 6.3 (Python 3.9)
- testenv: py39-pyqt63
os: ubuntu-20.04
python: 3.9
### PyQt 6.4 (Python 3.9)
# - testenv: py39-pyqt64
# os: ubuntu-20.04
# python: 3.9
### macOS Big Sur: PyQt 5.15 (Python 3.9 to match PyInstaller env)
- testenv: py39-pyqt515
os: macos-11

View File

@ -60,6 +60,8 @@ disable=locally-disabled,
missing-type-doc,
missing-param-doc,
useless-param-doc,
wrong-import-order, # FIXME:qt6 (lint)
ungrouped-imports, # FIXME:qt6 (lint)
[BASIC]
function-rgx=[a-z_][a-z0-9_]{2,50}$
@ -74,7 +76,8 @@ no-docstring-rgx=(^_|^main$)
class-const-naming-style = snake_case
[FORMAT]
max-line-length=88
# FIXME:qt6 (lint) down to 88 again once we use black
max-line-length=190
ignore-long-lines=(<?https?://|file://|^# Copyright 201\d|link:)
expected-line-ending-format=LF

View File

@ -32,6 +32,7 @@ include doc/qutebrowser.1.asciidoc
include doc/changelog.asciidoc
prune qutebrowser/3rdparty
exclude mypy.ini
exclude pyrightconfig.json
exclude tox.ini
exclude qutebrowser/javascript/.eslintrc.yaml
exclude qutebrowser/javascript/.eslintignore

View File

@ -83,15 +83,14 @@ Requirements
The following software and libraries are required to run qutebrowser:
* https://www.python.org/[Python] 3.7 or newer
* https://www.qt.io/[Qt] 5.12.0 or newer (5.12 LTS or 5.15 recommended, Qt 6 is
not supported yet) with the following modules:
* https://www.qt.io/[Qt], either 6.2.0 or newer, or 5.15.0 or newer, with the following modules:
- QtCore / qtbase
- QtQuick (part of qtbase or qtdeclarative in some distributions)
- QtSQL (part of qtbase in some distributions)
- QtDBus (part of qtbase in some distributions; note that a connection to DBus at
runtime is optional)
- QtOpenGL
- QtWebEngine, or
- QtWebEngine (if using Qt 5, 5.15.2 or newer), or
- alternatively QtWebKit (5.212) - **This is not recommended** due to known security
issues in QtWebKit, you most likely want to use qutebrowser with the
default QtWebEngine backend (based on Chromium) instead. Quoting the
@ -99,8 +98,8 @@ The following software and libraries are required to run qutebrowser:
_[The latest QtWebKit] release is based on [an] old WebKit revision with known
unpatched vulnerabilities. Please use it carefully and avoid visiting untrusted
websites and using it for transmission of sensitive data._
* https://www.riverbankcomputing.com/software/pyqt/intro[PyQt] 5.12.0 or newer
for Python 3
* https://www.riverbankcomputing.com/software/pyqt/intro[PyQt] 6.2.2 or newer
(Qt 6) or 5.15.0 or newer (Qt 5)
* https://palletsprojects.com/p/jinja/[jinja2]
* https://github.com/yaml/pyyaml[PyYAML]

View File

@ -57,9 +57,12 @@ Removed
- Support for Python 3.6 is dropped, as it's been
https://discuss.python.org/t/python-3-6-rides-into-the-sunset/12964[end-of-life upstream]
since December 2021. Python 3.7.0 or newer is now required.
- Support for Qt/PyQt before 5.15.0 and QtWebEngine before 5.15.2 are now
dropped, as older Qt versions are
https://endoflife.date/qt[end-of-life upstream] since mid/late 2020
(5.13/5.14) and late 2021 (5.12 LTS).
- It's planned to drop support for various legacy platforms and libraries which
are unsupported upstream, such as:
* Qt before 5.15 LTS (plus adding support for Qt 6.2+)
* The QtWebKit backend
* macOS 10.14 (via Homebrew)
* 32-bit Windows (via Qt)

View File

@ -128,6 +128,9 @@ Currently, the following tox environments are available:
- untracked git files
- VCS conflict markers
- common spelling mistakes
* http://mypy-lang.org/[mypy] for static type checking:
- `mypy-pyqt5` run mypy with PyQt5 installed
- `mypy-pyqt6` run mypy with PyQt6 installed
The default test suite is run with `tox`; the list of default
environments is obtained with `tox -l`.
@ -153,7 +156,7 @@ smallest scope which makes sense. Most of the time, this will be line scope.
false-positives, let me know! I'm still tweaking the parameters.
Running Specific Tests
Running specific tests
~~~~~~~~~~~~~~~~~~~~~~
While you are developing you often don't want to run the full test
@ -180,6 +183,15 @@ tox -e py37 -- tests/end2end/features/test_tabs_bdd.py -k undo
tox -e py37-cov -- tests/unit/browser/test_webelem.py
----
Specifying the backend for tests
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Tests automatically pick the backend based on what they manage to import. If
you have both backends available and you would like the tests to be run with a
specific one you can set either of a) the environment variable QUTE_TESTS_BACKEND
, or b) the command line argument --qute-backend, to the desired backend
(webkit/webengine).
Profiling
~~~~~~~~~
@ -219,7 +231,8 @@ Useful websites
Some resources which might be handy:
* https://doc.qt.io/qt-5/classes.html[The Qt5 reference]
* https://doc.qt.io/qt-6/classes.html[The Qt 6 reference]
* https://doc.qt.io/qt-5/classes.html[The Qt 5 reference]
* https://docs.python.org/3/library/index.html[The Python reference]
* https://httpbin.org/[httpbin, a test service for HTTP requests/responses]
* https://requestbin.com/[RequestBin, a service to inspect HTTP requests]
@ -274,7 +287,7 @@ Other
Languages] (https://www.rfc-editor.org/errata_search.php?rfc=5646[Errata])
* https://www.w3.org/TR/CSS2/[Cascading Style Sheets Level 2 Revision 1 (CSS
2.1) Specification]
* https://doc.qt.io/qt-5/stylesheet-reference.html[Qt Style Sheets Reference]
* https://doc.qt.io/qt-6/stylesheet-reference.html[Qt Style Sheets Reference]
* https://mimesniff.spec.whatwg.org/[MIME Sniffing Standard]
* https://spec.whatwg.org/[WHATWG specifications]
* https://www.w3.org/html/wg/drafts/html/master/Overview.html[HTML 5.1 Nightly]
@ -353,7 +366,7 @@ All objects can be printed by starting with the `--debug` flag and using the
The registry is mainly used for <<commands,command handlers>>, but it can
also be useful in places where using Qt's
https://doc.qt.io/qt-5/signalsandslots.html[signals and slots] mechanism would
https://doc.qt.io/qt-6/signalsandslots.html[signals and slots] mechanism would
be difficult.
Logging
@ -600,11 +613,14 @@ This is mostly useful for qutebrowser maintainers to work around issues in Qt -
The hierarchy of widgets when QtWebEngine is involved looks like this:
- qutebrowser has a `WebEngineTab` object, which is its abstraction over QtWebKit/QtWebEngine.
- The `WebEngineTab` has a `_widget` attribute, which is the https://doc.qt.io/qt-5/qwebengineview.html[QWebEngineView]
- That view has a https://doc.qt.io/qt-5/qwebenginepage.html[QWebEnginePage] for everything which doesn't require rendering.
- The view also has a layout with exactly one element (which also is its `focusProxy()`)
- That element is the https://code.qt.io/cgit/qt/qtwebengine.git/tree/src/webenginewidgets/render_widget_host_view_qt_delegate_widget.cpp[RenderWidgetHostViewQtDelegateWidget] (it inherits https://doc.qt.io/qt-5/qquickwidget.html[QQuickWidget]) - also often referred to as RWHV or RWHVQDW. It can be obtained via `sip.cast(tab._widget.focusProxy(), QQuickWidget)`.
- Calling `rootObject()` on that gives us the https://doc.qt.io/qt-5/qquickitem.html[QQuickItem] where Chromium renders into (?). With it, we can do things like `.setRotation(20)`.
- The `WebEngineTab` has a `_widget` attribute, which is the https://doc.qt.io/qt-6/qwebengineview.html[QWebEngineView]
- That view has a https://doc.qt.io/qt-6/qwebenginepage.html[QWebEnginePage] for everything which doesn't require rendering.
- The view also has a layout with exactly one element (which also is its `focusProxy()`).
- Qt 5: That element is the https://code.qt.io/cgit/qt/qtwebengine.git/tree/src/webenginewidgets/render_widget_host_view_qt_delegate_widget.cpp?h=5.15[RenderWidgetHostViewQtDelegateWidget] (it inherits https://doc.qt.io/qt-6/qquickwidget.html[QQuickWidget]) - also often referred to as RWHV or RWHVQDW.
It can be obtained via `sip.cast(tab._widget.focusProxy(), QQuickWidget)`.
- Qt 6: That element is the https://code.qt.io/cgit/qt/qtwebengine.git/tree/src/webenginewidgets/api/qwebengineview.cpp[WebEngineQuickWidget] (it inherits https://doc.qt.io/qt-6/qquickwidget.html[QQuickWidget]).
It can be obtained via `tab._widget.focusProxy()`.
- Calling `rootObject()` on that gives us the https://doc.qt.io/qt-6/qquickitem.html[QQuickItem] where Chromium renders into (?). With it, we can do things like `.setRotation(20)`.
Style conventions
-----------------
@ -676,6 +692,41 @@ Return:
- `__magic__` methods
- other methods
- overrides of Qt methods
* Type hinting: the qutebrowser codebase uses type hints liberally to enable
static type checking and autocompletion in editors.
- We use http://mypy-lang.org/[mypy] in CI jobs to perform static type
checking.
- Not all of the codebase is covered by type hints currently. We encourage
including type hints on all new code and even adding type hints to
existing code if you find yourself working on some that isn't already
covered. There are some module specific rules in the mypy config file,
`.mypy.ini`, to make type hints strictly required in some areas.
- More often than not mypy is correct when it raises issues. But don't be
afraid to add `# type: ignore[...]` statements or casts if you need to.
As an optional part of the language not all type information from third
parties is always correct. Mypy will raise a new issue if it spots an
"ignore" statement which is no longer needed because the underlying
issue has been resolved.
- One area where we have to take particular care is in code that deals
with differences between PyQt5 and PyQt6. We try to write most code in a
way that will work with either backend but when you need to deal with
differences you should use a pattern like:
+
[source,python]
----
if machinery.IS_QT5:
... # do PyQt5 specific implementation
else:
# PyQt6
... # do PyQt6 specific implementation
----
+
then you have to https://mypy.readthedocs.io/en/latest/command_line.html#cmdoption-mypy-always-true[tell]
mypy to treat `machinery.IS_QT5` as a constant value then run mypy twice to
cover both branches. There are a handful of variables in
`qutebrowser/qt/machinery.py` that mypy needs to know about. There are tox
jobs (`mypy-pyqt5` and `mypy-pyqt6`) that take care of telling mypy to use
them as constants.
Checklists
----------

View File

@ -337,8 +337,13 @@ Remove a key from a dict.
[[config-diff]]
=== config-diff
Syntax: +:config-diff [*--include-hidden*]+
Show all customized options.
==== optional arguments
* +*-i*+, +*--include-hidden*+: Also include internal qutebrowser settings.
[[config-edit]]
=== config-edit
Syntax: +:config-edit [*--no-source*]+
@ -821,7 +826,7 @@ Show an error message in the statusbar.
* +'text'+: The text to show.
==== optional arguments
* +*-r*+, +*--rich*+: Render the given text as https://doc.qt.io/qt-5/richtext-html-subset.html[Qt Rich Text].
* +*-r*+, +*--rich*+: Render the given text as https://doc.qt.io/qt-6/richtext-html-subset.html[Qt Rich Text].
[[message-info]]
@ -834,7 +839,7 @@ Show an info message in the statusbar.
* +'text'+: The text to show.
==== optional arguments
* +*-r*+, +*--rich*+: Render the given text as https://doc.qt.io/qt-5/richtext-html-subset.html[Qt Rich Text].
* +*-r*+, +*--rich*+: Render the given text as https://doc.qt.io/qt-6/richtext-html-subset.html[Qt Rich Text].
==== count
@ -850,7 +855,7 @@ Show a warning message in the statusbar.
* +'text'+: The text to show.
==== optional arguments
* +*-r*+, +*--rich*+: Render the given text as https://doc.qt.io/qt-5/richtext-html-subset.html[Qt Rich Text].
* +*-r*+, +*--rich*+: Render the given text as https://doc.qt.io/qt-6/richtext-html-subset.html[Qt Rich Text].
[[messages]]
@ -2176,7 +2181,7 @@ Syntax: +:debug-webaction 'action'+
Execute a webaction.
Available actions: https://doc.qt.io/archives/qt-5.5/qwebpage.html#WebAction-enum (WebKit) https://doc.qt.io/qt-5/qwebenginepage.html#WebAction-enum (WebEngine)
Available actions: https://doc.qt.io/archives/qt-5.5/qwebpage.html#WebAction-enum (WebKit) https://doc.qt.io/qt-6/qwebenginepage.html#WebAction-enum (WebEngine)
==== positional arguments
* +'action'+: The action to execute, e.g. MoveToNextChar.

View File

@ -118,6 +118,7 @@
|<<colors.webpage.darkmode.enabled,colors.webpage.darkmode.enabled>>|Render all web contents using a dark theme.
|<<colors.webpage.darkmode.grayscale.all,colors.webpage.darkmode.grayscale.all>>|Render all colors as grayscale.
|<<colors.webpage.darkmode.grayscale.images,colors.webpage.darkmode.grayscale.images>>|Desaturation factor for images in dark mode.
|<<colors.webpage.darkmode.increase_text_contrast,colors.webpage.darkmode.increase_text_contrast>>|Increase text contrast by drawing an outline of the uninverted color.
|<<colors.webpage.darkmode.policy.images,colors.webpage.darkmode.policy.images>>|Which images to apply dark mode to.
|<<colors.webpage.darkmode.policy.page,colors.webpage.darkmode.policy.page>>|Which pages to apply dark mode to.
|<<colors.webpage.darkmode.threshold.background,colors.webpage.darkmode.threshold.background>>|Threshold for inverting background elements with dark mode.
@ -1671,6 +1672,9 @@ Example configurations from Chromium's `chrome://flags`:
- "With selective inversion of everything": Combines the two variants
above.
- "With increased text contrast": Set
`colors.webpage.darkmode.increase_text_contrast` (QtWebEngine 6.3+)
This setting requires a restart.
This setting is only available with the QtWebEngine backend.
@ -1699,14 +1703,26 @@ If set to 0, images are left as-is. If set to 1, images are completely grayscale
This setting requires a restart.
On QtWebEngine, this setting requires Qt 5.14 or newer.
On QtWebKit, this setting is unavailable.
This setting is only available with the QtWebEngine backend.
Type: <<types,Float>>
Default: +pass:[0.0]+
[[colors.webpage.darkmode.increase_text_contrast]]
=== colors.webpage.darkmode.increase_text_contrast
Increase text contrast by drawing an outline of the uninverted color.
This setting requires a restart.
On QtWebEngine, this setting requires Qt 6.3 or newer.
On QtWebKit, this setting is unavailable.
Type: <<types,Bool>>
Default: +pass:[false]+
[[colors.webpage.darkmode.policy.images]]
=== colors.webpage.darkmode.policy.images
Which images to apply dark mode to.
@ -1733,9 +1749,7 @@ The underlying Chromium setting has been removed in QtWebEngine 5.15.3, thus thi
This setting requires a restart.
On QtWebEngine, this setting requires Qt 5.14 or newer.
On QtWebKit, this setting is unavailable.
This setting is only available with the QtWebEngine backend.
Type: <<types,String>>
@ -1754,9 +1768,7 @@ Note: This behavior is the opposite of `colors.webpage.darkmode.threshold.text`!
This setting requires a restart.
On QtWebEngine, this setting requires Qt 5.14 or newer.
On QtWebKit, this setting is unavailable.
This setting is only available with the QtWebEngine backend.
Type: <<types,Int>>
@ -1769,9 +1781,7 @@ Text colors with brightness below this threshold will be inverted, and above it
This setting requires a restart.
On QtWebEngine, this setting requires Qt 5.14 or newer.
On QtWebKit, this setting is unavailable.
This setting is only available with the QtWebEngine backend.
Type: <<types,Int>>
@ -1785,9 +1795,7 @@ The "auto" value is broken on QtWebEngine 5.15.2 due to a Qt bug. There, it will
This setting requires a restart.
On QtWebEngine, this setting requires Qt 5.14 or newer.
On QtWebKit, this setting is unavailable.
This setting is only available with the QtWebEngine backend.
Type: <<types,String>>
@ -2274,7 +2282,7 @@ Type: <<types,String>>
Valid values:
* +always+: Always send the Referer.
* +always+: Always send the Referer. With QtWebEngine 6.2+, this value is unavailable and will act like `same-domain`.
* +never+: Never send the Referer. This is not recommended, as some sites may break.
* +same-domain+: Only send the Referer for the same domain. This will still protect your privacy, but shouldn't break any sites. With QtWebEngine, the referer will still be sent for other domains, but with stripped path information.
@ -2579,8 +2587,6 @@ Allow websites to show notifications.
This setting supports link:configuring{outfilesuffix}#patterns[URL patterns].
On QtWebEngine, this setting requires Qt 5.13 or newer.
Type: <<types,BoolAsk>>
Valid values:
@ -2595,8 +2601,6 @@ Default: +pass:[ask]+
=== content.notifications.presenter
What notification presenter to use for web notifications.
Note that not all implementations support all features of notifications:
- With PyQt 5.14, any setting other than `qt` does not support the `click` and
`close` events, as well as the `tag` option to replace existing notifications.
- The `qt` and `systray` options only support showing one notification at the time
and ignore the `tag` option to replace existing notifications.
- The `herbe` option only supports showing one notification at the time and doesn't
@ -2604,16 +2608,14 @@ Note that not all implementations support all features of notifications:
- The `messages` option doesn't show icons and doesn't support the `click` and
`close` events.
On QtWebEngine, this setting requires Qt 5.14 or newer.
On QtWebKit, this setting is unavailable.
This setting is only available with the QtWebEngine backend.
Type: <<types,String>>
Valid values:
* +auto+: Tries `libnotify`, `systray` and `messages`, uses the first one available without showing error messages.
* +qt+: Use Qt's native notification presenter, based on a system tray icon. Switching from or to this value requires a restart of qutebrowser. Recommended over `systray` on PyQt 5.14.
* +qt+: Use Qt's native notification presenter, based on a system tray icon. Switching from or to this value requires a restart of qutebrowser.
* +libnotify+: Shows messages via DBus in a libnotify-compatible way. If DBus isn't available, falls back to `systray` or `messages`, but shows an error message.
* +systray+: Use a notification presenter based on a systray icon. Falls back to `libnotify` or `messages` if not systray is available. This is a reimplementation of the `qt` setting value, but with the possibility to switch to it at runtime.
* +messages+: Show notifications as qutebrowser messages. Most notification features aren't available.
@ -2629,9 +2631,7 @@ Note that with the `qt` presenter, origins are never shown.
This setting supports link:configuring{outfilesuffix}#patterns[URL patterns].
On QtWebEngine, this setting requires Qt 5.14 or newer.
On QtWebKit, this setting is unavailable.
This setting is only available with the QtWebEngine backend.
Type: <<types,Bool>>
@ -2684,9 +2684,7 @@ On Windows, if this setting is set to False, the system-wide animation setting i
This setting requires a restart.
On QtWebEngine, this setting requires Qt 5.14 or newer.
On QtWebKit, this setting is unavailable.
This setting is only available with the QtWebEngine backend.
Type: <<types,Bool>>
@ -3635,9 +3633,7 @@ On Linux, disabling this also disables Chromium's MPRIS integration.
This setting requires a restart.
On QtWebEngine, this setting requires Qt 5.14 or newer.
On QtWebKit, this setting is unavailable.
This setting is only available with the QtWebEngine backend.
Type: <<types,Bool>>
@ -3853,7 +3849,7 @@ Alternative process models use less resources, but decrease security and robustn
See the following pages for more details:
- https://www.chromium.org/developers/design-documents/process-models
- https://doc.qt.io/qt-5/qtwebengine-features.html#process-models
- https://doc.qt.io/qt-6/qtwebengine-features.html#process-models
This setting requires a restart.
@ -3950,7 +3946,7 @@ Default: +pass:[none]+
[[qt.highdpi]]
=== qt.highdpi
Turn on Qt HighDPI scaling.
This is equivalent to setting QT_AUTO_SCREEN_SCALE_FACTOR=1 or QT_ENABLE_HIGHDPI_SCALING=1 (Qt >= 5.14) in the environment.
This is equivalent to setting QT_ENABLE_HIGHDPI_SCALING=1 (Qt >= 5.14) in the environment.
It's off by default as it can cause issues with some bitmap fonts. As an alternative to this, it's possible to set font sizes and the `zoom.default` setting.
This setting requires a restart.
@ -4033,8 +4029,6 @@ Default: +pass:[true]+
=== search.wrap
Wrap around at the top and bottom of the page when advancing through text matches using `:search-next` and `:search-prev`.
On QtWebEngine, this setting requires Qt 5.14 or newer.
Type: <<types,Bool>>
Default: +pass:[true]+
@ -4809,7 +4803,7 @@ When setting from a string, pass a json-like list, e.g. `["one", "two"]`.
|Proxy|A proxy URL, or `system`/`none`.
|QssColor|A color value supporting gradients.
A value can be in one of the following formats: * `#RGB`/`#RRGGBB`/`#AARRGGBB`/`#RRRGGGBBB`/`#RRRRGGGGBBBB` * An SVG color name as specified in https://www.w3.org/TR/SVG/types.html#ColorKeywords[the W3C specification]. * transparent (no color) * `rgb(r, g, b)` / `rgba(r, g, b, a)` (values 0-255 or percentages) * `hsv(h, s, v)` / `hsva(h, s, v, a)` (values 0-255, hue 0-359) * A gradient as explained in https://doc.qt.io/qt-5/stylesheet-reference.html#list-of-property-types[the Qt documentation] under ``Gradient''
A value can be in one of the following formats: * `#RGB`/`#RRGGBB`/`#AARRGGBB`/`#RRRGGGBBB`/`#RRRRGGGGBBBB` * An SVG color name as specified in https://www.w3.org/TR/SVG/types.html#ColorKeywords[the W3C specification]. * transparent (no color) * `rgb(r, g, b)` / `rgba(r, g, b, a)` (values 0-255 or percentages) * `hsv(h, s, v)` / `hsva(h, s, v, a)` (values 0-255, hue 0-359) * A gradient as explained in https://doc.qt.io/qt-6/stylesheet-reference.html#list-of-property-types[the Qt documentation] under ``Gradient''
|QtColor|A color value.
A value can be in one of the following formats: * `#RGB`/`#RRGGBB`/`#AARRGGBB`/`#RRRGGGBBB`/`#RRRRGGGGBBBB` * An SVG color name as specified in https://www.w3.org/TR/SVG/types.html#ColorKeywords[the W3C specification]. * transparent (no color) * `rgb(r, g, b)` / `rgba(r, g, b, a)` (values 0-255 or percentages) * `hsv(h, s, v)` / `hsva(h, s, v, a)` (values 0-255, hue 0-359)

View File

@ -63,7 +63,7 @@ show it.
Which backend to use.
*--desktop-file-name* 'DESKTOP_FILE_NAME'::
Set the base name of the desktop entry for this application. Used to set the app_id under Wayland. See https://doc.qt.io/qt-5/qguiapplication.html#desktopFileName-prop
Set the base name of the desktop entry for this application. Used to set the app_id under Wayland. See https://doc.qt.io/qt-6/qguiapplication.html#desktopFileName-prop
*--untrusted-args*::
Mark all following arguments as untrusted, which enforces that they are URLs/search terms (and not flags or commands)

View File

@ -82,7 +82,7 @@ def get_data_files():
def get_hidden_imports():
imports = ['PyQt5.QtOpenGL', 'PyQt5._QOpenGLFunctions_2_0']
imports = [] if "PYINSTALLER_QT6" in os.environ else ['PyQt5.QtOpenGL']
for info in loader.walk_components():
imports.append(info.name)
return imports

View File

@ -1,5 +0,0 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py
PyQt5==5.12.3 # rq.filter: < 5.13
PyQt5-sip==12.11.1
PyQtWebEngine==5.12.1 # rq.filter: < 5.13

View File

@ -1,4 +0,0 @@
#@ filter: PyQt5 < 5.13
#@ filter: PyQtWebEngine < 5.13
PyQt5 >= 5.12, < 5.13
PyQtWebEngine >= 5.12, < 5.13

View File

@ -1,5 +0,0 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py
PyQt5==5.13.2 # rq.filter: < 5.14
PyQt5-sip==12.11.1
PyQtWebEngine==5.13.2 # rq.filter: < 5.14

View File

@ -1,4 +0,0 @@
#@ filter: PyQt5 < 5.14
#@ filter: PyQtWebEngine < 5.14
PyQt5 >= 5.13, < 5.14
PyQtWebEngine >= 5.13, < 5.14

View File

@ -1,5 +0,0 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py
PyQt5==5.14.2 # rq.filter: < 5.15
PyQt5-sip==12.11.1
PyQtWebEngine==5.14.0 # rq.filter: < 5.15

View File

@ -1,4 +0,0 @@
#@ filter: PyQt5 < 5.15
#@ filter: PyQtWebEngine < 5.15
PyQt5 >= 5.14, < 5.15
PyQtWebEngine >= 5.14, < 5.15

View File

@ -1,5 +0,0 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py
PyQt5==5.15.0 # rq.filter: == 5.15.0
PyQt5-sip==12.11.1
PyQtWebEngine==5.15.0 # rq.filter: == 5.15.0

View File

@ -1,4 +0,0 @@
#@ filter: PyQt5 == 5.15.0
#@ filter: PyQtWebEngine == 5.15.0
PyQt5 == 5.15.0
PyQtWebEngine == 5.15.0

View File

@ -0,0 +1,5 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py
PyQt5==5.15.2 # rq.filter: == 5.15.2
PyQt5-sip==12.10.1
PyQtWebEngine==5.15.2 # rq.filter: == 5.15.2

View File

@ -0,0 +1,4 @@
#@ filter: PyQt5 == 5.15.2
#@ filter: PyQtWebEngine == 5.15.2
PyQt5 == 5.15.2
PyQtWebEngine == 5.15.2

View File

@ -0,0 +1,7 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py
PyQt5==5.15.6
PyQt5-Qt5==5.15.2
PyQt5-sip==12.10.1
PyQtWebEngine==5.15.5
PyQtWebEngine-Qt5==5.15.2

View File

@ -0,0 +1,2 @@
PyQt5
PyQtWebEngine

View File

@ -0,0 +1,7 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py
PyQt6==6.2.3
PyQt6-Qt6==6.2.4
PyQt6-sip==13.3.1
PyQt6-WebEngine==6.2.1
PyQt6-WebEngine-Qt6==6.2.4

View File

@ -0,0 +1,4 @@
PyQt6 >= 6.2, < 6.3
PyQt6-Qt6 >= 6.2, < 6.3
PyQt6-WebEngine >= 6.2, < 6.3
PyQt6-WebEngine-Qt6 >= 6.2, < 6.3

View File

@ -0,0 +1,7 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py
PyQt6==6.3.1
PyQt6-Qt6==6.3.1
PyQt6-sip==13.4.0
PyQt6-WebEngine==6.3.1
PyQt6-WebEngine-Qt6==6.3.1

View File

@ -0,0 +1,4 @@
PyQt6 >= 6.3, < 6.4
PyQt6-Qt6 >= 6.3, < 6.4
PyQt6-WebEngine >= 6.3, < 6.4
PyQt6-WebEngine-Qt6 >= 6.3, < 6.4

View File

@ -0,0 +1,7 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py
PyQt6==6.4.0
PyQt6-Qt6==6.4.1
PyQt6-sip==13.4.0
PyQt6-WebEngine==6.4.0
PyQt6-WebEngine-Qt6==6.4.1

View File

@ -0,0 +1,4 @@
PyQt6 >= 6.4, < 6.5
PyQt6-Qt6 >= 6.4, < 6.5
PyQt6-WebEngine >= 6.4, < 6.5
PyQt6-WebEngine-Qt6 >= 6.4, < 6.5

View File

@ -0,0 +1,7 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py
PyQt6==6.3.1
PyQt6-Qt6==6.3.1
PyQt6-sip==13.4.0
PyQt6-WebEngine==6.3.1
PyQt6-WebEngine-Qt6==6.3.1

View File

@ -0,0 +1,4 @@
PyQt6
PyQt6-Qt6
PyQt6-WebEngine
PyQt6-WebEngine-Qt6

View File

@ -2,6 +2,8 @@ Jinja2
PyYAML
## Only used on macOS to make borderless windows resizable
# Not needed anymore with Qt 6.3, but we can't express that
# here, and it won't hurt either.
## our recompile_requirements.py can't really deal with
## platform-specific dependencies unfortunately...
# pyobjc-core

10
pyrightconfig.json Normal file
View File

@ -0,0 +1,10 @@
{
"defineConstant": {
"USE_PYQT6": false,
"USE_PYQT5": true,
"USE_PYSIDE2": false,
"USE_PYSIDE6": false,
"IS_QT5": true,
"IS_QT6": false
}
}

View File

@ -27,7 +27,6 @@ markers =
no_ci: Tests which should not run on CI.
qtwebengine_todo: Features still missing with QtWebEngine
qtwebengine_skip: Tests not applicable with QtWebEngine
qtwebengine_notifications: Tests which need QtWebEngine notification support
qtwebkit_skip: Tests not applicable with QtWebKit
qtwebengine_flaky: Tests which are flaky (and currently skipped) with QtWebEngine
qtwebengine_mac_xfail: Tests which fail on macOS with QtWebEngine
@ -35,58 +34,30 @@ markers =
no_invalid_lines: Don't fail on unparsable lines in end2end tests
fake_os: Fake utils.is_* to a fake operating system
unicode_locale: Tests which need a unicode locale to work
js_headers: Sets JS headers dynamically on QtWebEngine (unsupported on some versions)
qtwebkit_pdf_imageformat_skip: Broken on QtWebKit with PDF image format plugin installed
qtwebkit_openssl3_skip: Broken due to cheroot bug with OpenSSL 3 on QtWebKit
windows_skip: Tests which should be skipped on Windows
qt5_only: Tests which should only run with Qt 5
qt6_only: Tests which should only run with Qt 6
qt5_xfail: Tests which fail with Qt 5
qt6_xfail: Tests which fail with Qt 6
qt_log_level_fail = WARNING
qt_log_ignore =
^SpellCheck: .*
^SetProcessDpiAwareness failed: .*
^QWindowsWindow::setGeometry(Dp)?: Unable to set geometry .*
^QProcess: Destroyed while process .* is still running\.
^"Method "GetAll" with signature "s" on interface "org\.freedesktop\.DBus\.Properties" doesn't exist
^"Method \\"GetAll\\" with signature \\"s\\" on interface \\"org\.freedesktop\.DBus\.Properties\\" doesn't exist\\n"
^propsReply "Method \\"GetAll\\" with signature \\"s\\" on interface \\"org\.freedesktop\.DBus\.Properties\\" doesn't exist\\n"
^nmReply "Method \\"GetDevices\\" with signature \\"\\" on interface \\"org\.freedesktop\.NetworkManager\\" doesn't exist\\n"
^"Object path cannot be empty"
^virtual void QSslSocketBackendPrivate::transmit\(\) SSL write failed with error: -9805
^virtual void QSslSocketBackendPrivate::transmit\(\) SSLRead failed with: -9805
^Type conversion already registered from type .*
^QNetworkReplyImplPrivate::error: Internal problem, this method must only be called once\.
^QWaitCondition: Destroyed while threads are still waiting
^QXcbXSettings::QXcbXSettings\(QXcbScreen\*\) Failed to get selection owner for XSETTINGS_S atom
# GitHub Actions
^QStandardPaths: XDG_RUNTIME_DIR not set, defaulting to .*
^QObject::connect: Cannot connect \(null\)::stateChanged\(QNetworkSession::State\) to QNetworkReplyHttpImpl::_q_networkSessionStateChanged\(QNetworkSession::State\)
^QXcbClipboard: Cannot transfer data, no data available
^load glyph failed
^Error when parsing the netrc file
^Image of format '' blocked because it is not considered safe. If you are sure it is safe to do so, you can white-list the format by setting the environment variable QTWEBKIT_IMAGEFORMAT_WHITELIST=
^QPainter::end: Painter ended with \d+ saved states
^QSslSocket: cannot resolve .*
^QSslSocket: cannot call unresolved function .*
^Incompatible version of OpenSSL
^QQuickWidget::invalidateRenderControl could not make context current
^libpng warning: iCCP: known incorrect sRGB profile
^inotify_add_watch\(".*"\) failed: "No space left on device"
^QSettings::value: Empty key passed
^Icon theme ".*" not found
^Error receiving trust for a CA certificate
^QBackingStore::endPaint\(\) called with active painter.*
^QPaintDevice: Cannot destroy paint device that is being painted
^DirectWrite: CreateFontFaceFromHDC\(\) failed .*
^Attribute Qt::AA_ShareOpenGLContexts must be set before QCoreApplication is created\.
^QHttpNetworkConnectionPrivate::_q_hostLookupFinished could not de-queue request, failed to report HostNotFoundError
# test_on_focus_changed_issue1484 on macOS
^The available OpenGL surface format was either not version 3\.2 or higher or not a Core Profile.*
# tests/unit/mainwindow/test_messageview.py and
# tests/unit/mainwindow/statusbar/test_textbase.py::test_resize
# on Windows
^QWindowsWindow::setGeometry(Dp)?: Unable to set geometry .*
# tests/unit/commands/test_userscripts.py::test_killed_command
# on Windows
^QProcess: Destroyed while process .* is still running\.
xfail_strict = true
filterwarnings =
error
default:Test process .* failed to terminate!:UserWarning
ignore:_SixMetaPathImporter\.exec_module\(\) not found; falling back to load_module\(\):ImportWarning
ignore:VendorImporter\.find_spec\(\) not found; falling back to find_module\(\):ImportWarning
ignore:_SixMetaPathImporter\.find_spec\(\) not found; falling back to find_module\(\):ImportWarning
# https://github.com/ionelmc/python-hunter/issues/97
ignore:The distutils\.sysconfig module is deprecated, use sysconfig instead:DeprecationWarning
# https://github.com/certifi/python-certifi/issues/170
ignore:path is deprecated\. Use files\(\) instead\. Refer to https.//importlib-resources\.readthedocs\.io/en/latest/using\.html#migrating-from-legacy for migration advice\.:DeprecationWarning:certifi.core
# https://github.com/HypothesisWorks/hypothesis/issues/3309

View File

@ -21,7 +21,7 @@
from typing import cast, Any
from PyQt5.QtCore import QUrl
from qutebrowser.qt.core import QUrl
from qutebrowser.config import config

View File

@ -23,7 +23,7 @@
import io
from PyQt5.QtCore import QObject, pyqtSignal, pyqtSlot, QUrl
from qutebrowser.qt.core import QObject, pyqtSignal, pyqtSlot, QUrl
from qutebrowser.browser import downloads, qtnetworkdownloads
from qutebrowser.utils import objreg

View File

@ -46,9 +46,10 @@ import datetime
import argparse
from typing import Iterable, Optional
from PyQt5.QtWidgets import QApplication, QWidget
from PyQt5.QtGui import QDesktopServices, QPixmap, QIcon
from PyQt5.QtCore import pyqtSlot, QUrl, QObject, QEvent, pyqtSignal, Qt
from qutebrowser.qt import machinery
from qutebrowser.qt.widgets import QApplication, QWidget
from qutebrowser.qt.gui import QDesktopServices, QPixmap, QIcon
from qutebrowser.qt.core import pyqtSlot, QUrl, QObject, QEvent, pyqtSignal, Qt
import qutebrowser
from qutebrowser.commands import runners
@ -149,7 +150,6 @@ def init(*, args: argparse.Namespace) -> None:
quitter.instance.shutting_down.connect(QApplication.closeAllWindows)
_init_icon()
_init_pulseaudio()
loader.init()
loader.load_components()
@ -195,27 +195,12 @@ def _init_icon():
objects.qapp.setWindowIcon(icon)
def _init_pulseaudio():
"""Set properties for PulseAudio.
WORKAROUND for https://bugreports.qt.io/browse/QTBUG-85363
Affected Qt versions:
- Older than 5.11 (which is unsupported)
- 5.14.0 to 5.15.0 (inclusive)
However, we set this on all versions so that qutebrowser's icon gets picked
up as well.
"""
for prop in ['application.name', 'application.icon_name']:
os.environ['PULSE_PROP_OVERRIDE_' + prop] = 'qutebrowser'
def _process_args(args):
"""Open startpage etc. and process commandline args."""
if not args.override_restore:
sessions.load_default(args.session)
new_window = None
if not sessions.session_manager.did_load:
log.init.debug("Initializing main window...")
private = args.target == 'private-window'
@ -226,15 +211,17 @@ def _process_args(args):
error.handle_fatal_exc(err, 'Cannot start in private mode',
no_err_windows=args.no_err_windows)
sys.exit(usertypes.Exit.err_init)
window = mainwindow.MainWindow(private=private)
if not args.nowindow:
window.show()
objects.qapp.setActiveWindow(window)
new_window = mainwindow.MainWindow(private=private)
process_pos_args(args.command)
_open_startpage()
_open_special_pages(args)
if new_window is not None and not args.nowindow:
new_window.show()
objects.qapp.setActiveWindow(new_window)
delta = datetime.datetime.now() - earlyinit.START_TIME
log.init.debug("Init finished after {}s".format(delta.total_seconds()))
@ -258,26 +245,30 @@ def process_pos_args(args, via_ipc=False, cwd=None, target_arg=None):
if command_target in {'window', 'private-window'}:
command_target = 'tab-silent'
win_id: Optional[int] = None
window: Optional[mainwindow.MainWindow] = None
if via_ipc and (not args or args == ['']):
win_id = mainwindow.get_window(via_ipc=via_ipc,
target=new_window_target)
_open_startpage(win_id)
window = mainwindow.get_window(via_ipc=via_ipc, target=new_window_target)
_open_startpage(window)
window.show()
window.maybe_raise()
return
for cmd in args:
if cmd.startswith(':'):
if win_id is None:
win_id = mainwindow.get_window(via_ipc=via_ipc,
target=command_target)
if window is None:
window = mainwindow.get_window(via_ipc=via_ipc, target=command_target)
# FIXME preserving old behavior, but we probably shouldn't be
# doing this...
# See https://github.com/qutebrowser/qutebrowser/issues/5094
window.maybe_raise()
log.init.debug("Startup cmd {!r}".format(cmd))
commandrunner = runners.CommandRunner(win_id)
commandrunner = runners.CommandRunner(window.win_id)
commandrunner.run_safely(cmd[1:])
elif not cmd:
log.init.debug("Empty argument")
win_id = mainwindow.get_window(via_ipc=via_ipc,
target=new_window_target)
window = mainwindow.get_window(via_ipc=via_ipc, target=new_window_target)
else:
if via_ipc and target_arg and target_arg != 'auto':
open_target = target_arg
@ -291,7 +282,7 @@ def process_pos_args(args, via_ipc=False, cwd=None, target_arg=None):
message.error("Error in startup argument '{}': {}".format(
cmd, e))
else:
win_id = open_url(url, target=open_target, via_ipc=via_ipc)
window = open_url(url, target=open_target, via_ipc=via_ipc)
def open_url(url, target=None, no_raise=False, via_ipc=True):
@ -304,39 +295,37 @@ def open_url(url, target=None, no_raise=False, via_ipc=True):
via_ipc: Whether the arguments were transmitted over IPC.
Return:
ID of a window that was used to open URL
The MainWindow of a window that was used to open the URL.
"""
target = target or config.val.new_instance_open_target
background = target in {'tab-bg', 'tab-bg-silent'}
win_id = mainwindow.get_window(via_ipc=via_ipc, target=target,
no_raise=no_raise)
tabbed_browser = objreg.get('tabbed-browser', scope='window',
window=win_id)
window = mainwindow.get_window(via_ipc=via_ipc, target=target, no_raise=no_raise)
log.init.debug("About to open URL: {}".format(url.toDisplayString()))
tabbed_browser.tabopen(url, background=background, related=False)
return win_id
window.tabbed_browser.tabopen(url, background=background, related=False)
window.show()
window.maybe_raise()
return window
def _open_startpage(win_id=None):
def _open_startpage(window: Optional[mainwindow.MainWindow] = None) -> None:
"""Open startpage.
The startpage is never opened if the given windows are not empty.
Args:
win_id: If None, open startpage in all empty windows.
window: If None, open startpage in all empty windows.
If set, open the startpage in the given window.
"""
if win_id is not None:
window_ids: Iterable[int] = [win_id]
if window is not None:
windows: Iterable[mainwindow.MainWindow] = [window]
else:
window_ids = objreg.window_registry
for cur_win_id in list(window_ids): # Copying as the dict could change
tabbed_browser = objreg.get('tabbed-browser', scope='window',
window=cur_win_id)
if tabbed_browser.widget.count() == 0:
windows = objreg.window_registry.values()
for cur_window in list(windows): # Copying as the dict could change
if cur_window.tabbed_browser.widget.count() == 0:
log.init.debug("Opening start pages")
for url in config.val.url.start_pages:
tabbed_browser.tabopen(url)
cur_window.tabbed_browser.tabopen(url)
def _open_special_pages(args):
@ -369,8 +358,17 @@ def _open_special_pages(args):
'qute://warning/webkit'),
('session-warning-shown',
qtutils.version_check('5.15', compiled=False),
True,
'qute://warning/sessions'),
('sandboxing-warning-shown',
(
hasattr(sys, "frozen") and
utils.is_mac and
machinery.IS_QT6 and
os.environ.get("QTWEBENGINE_DISABLE_SANDBOX") == "1"
),
'qute://warning/sandboxing'),
]
if 'quickstart-done' not in general_sect:
@ -432,10 +430,10 @@ def on_focus_changed(_old, new):
def open_desktopservices_url(url):
"""Handler to open a URL via QDesktopServices."""
target = config.val.new_instance_open_target
win_id = mainwindow.get_window(via_ipc=True, target=target)
tabbed_browser = objreg.get('tabbed-browser', scope='window',
window=win_id)
tabbed_browser.tabopen(url)
window = mainwindow.get_window(via_ipc=True, target=target)
window.tabbed_browser.tabopen(url)
window.show()
window.maybe_raise()
# This is effectively a @config.change_filter
@ -563,7 +561,12 @@ class Application(QApplication):
self.launch_time = datetime.datetime.now()
self.focusObjectChanged.connect(self.on_focus_object_changed)
self.setAttribute(Qt.AA_UseHighDpiPixmaps, True)
try:
self.setAttribute(Qt.ApplicationAttribute.AA_UseHighDpiPixmaps, True)
except AttributeError:
# default and removed in Qt 6
pass
self.new_window.connect(self._on_new_window)
@ -582,7 +585,7 @@ class Application(QApplication):
def event(self, e):
"""Handle macOS FileOpen events."""
if e.type() != QEvent.FileOpen:
if e.type() != QEvent.Type.FileOpen:
return super().event(e)
url = e.url()

View File

@ -26,17 +26,17 @@ import dataclasses
from typing import (cast, TYPE_CHECKING, Any, Callable, Iterable, List, Optional,
Sequence, Set, Type, Union, Tuple)
from PyQt5.QtCore import (pyqtSignal, pyqtSlot, QUrl, QObject, QSizeF, Qt,
from qutebrowser.qt.core import (pyqtSignal, pyqtSlot, QUrl, QObject, QSizeF, Qt,
QEvent, QPoint, QRect)
from PyQt5.QtGui import QKeyEvent, QIcon, QPixmap
from PyQt5.QtWidgets import QWidget, QApplication, QDialog
from PyQt5.QtPrintSupport import QPrintDialog, QPrinter
from PyQt5.QtNetwork import QNetworkAccessManager
from qutebrowser.qt.gui import QKeyEvent, QIcon, QPixmap
from qutebrowser.qt.widgets import QApplication, QWidget
from qutebrowser.qt.printsupport import QPrintDialog, QPrinter
from qutebrowser.qt.network import QNetworkAccessManager
if TYPE_CHECKING:
from PyQt5.QtWebKit import QWebHistory, QWebHistoryItem
from PyQt5.QtWebKitWidgets import QWebPage, QWebView
from PyQt5.QtWebEngineWidgets import (
from qutebrowser.qt.webkit import QWebHistory, QWebHistoryItem
from qutebrowser.qt.webkitwidgets import QWebPage, QWebView
from qutebrowser.qt.webenginewidgets import (
QWebEngineHistory, QWebEngineHistoryItem, QWebEnginePage, QWebEngineView)
from qutebrowser.keyinput import modeman
@ -153,7 +153,6 @@ class AbstractAction:
"""Attribute ``action`` of AbstractTab for Qt WebActions."""
action_class: Type[Union['QWebPage', 'QWebEnginePage']]
action_base: Type[Union['QWebPage.WebAction', 'QWebEnginePage.WebAction']]
def __init__(self, tab: 'AbstractTab') -> None:
@ -170,10 +169,10 @@ class AbstractAction:
def run_string(self, name: str) -> None:
"""Run a webaction based on its name."""
member = getattr(self.action_class, name, None)
if not isinstance(member, self.action_base):
raise WebTabError("{} is not a valid web action!".format(name))
assert member is not None # for mypy
try:
member = getattr(self.action_base, name)
except AttributeError:
raise WebTabError(f"{name} is not a valid web action!")
self._widget.triggerPageAction(member)
def show_source(self, pygments: bool = False) -> None:
@ -226,13 +225,37 @@ class AbstractAction:
self._tab.dump_async(show_source_cb)
class AbstractPrinting:
class AbstractPrinting(QObject):
"""Attribute ``printing`` of AbstractTab for printing the page."""
def __init__(self, tab: 'AbstractTab') -> None:
printing_finished = pyqtSignal(bool)
pdf_printing_finished = pyqtSignal(str, bool) # filename, ok
def __init__(self, tab: 'AbstractTab', parent: QWidget = None) -> None:
super().__init__(parent)
self._widget = cast(_WidgetType, None)
self._tab = tab
self._dialog: Optional[QPrintDialog] = None
self.printing_finished.connect(self._on_printing_finished)
self.pdf_printing_finished.connect(self._on_pdf_printing_finished)
@pyqtSlot(bool)
def _on_printing_finished(self, ok: bool) -> None:
# Only reporting error here, as the user has feedback from the dialog
# (and probably their printer) already.
if not ok:
message.error("Printing failed!")
if self._dialog is not None:
self._dialog.deleteLater()
self._dialog = None
@pyqtSlot(str, bool)
def _on_pdf_printing_finished(self, path: str, ok: bool) -> None:
if ok:
message.info(f"Printed to {path}")
else:
message.error(f"Printing to {path} failed!")
def check_pdf_support(self) -> None:
"""Check whether writing to PDFs is supported.
@ -250,41 +273,23 @@ class AbstractPrinting:
"""
raise NotImplementedError
def to_pdf(self, filename: str) -> bool:
def to_pdf(self, filename: str) -> None:
"""Print the tab to a PDF with the given filename."""
raise NotImplementedError
def to_printer(self, printer: QPrinter,
callback: Callable[[bool], None] = None) -> None:
def to_printer(self, printer: QPrinter) -> None:
"""Print the tab.
Args:
printer: The QPrinter to print to.
callback: Called with a boolean
(True if printing succeeded, False otherwise)
"""
raise NotImplementedError
def show_dialog(self) -> None:
"""Print with a QPrintDialog."""
def print_callback(ok: bool) -> None:
"""Called when printing finished."""
if not ok:
message.error("Printing failed!")
diag.deleteLater()
def do_print() -> None:
"""Called when the dialog was closed."""
self.to_printer(diag.printer(), print_callback)
diag = QPrintDialog(self._tab)
if utils.is_mac:
# For some reason we get a segfault when using open() on macOS
ret = diag.exec()
if ret == QDialog.Accepted:
do_print()
else:
diag.open(do_print)
self._dialog = dialog = QPrintDialog(self._tab)
self._dialog.open(lambda: self.to_printer(dialog.printer()))
# Gets cleaned up in on_printing_finished
@dataclasses.dataclass
@ -603,9 +608,9 @@ class AbstractCaret(QObject):
def _follow_enter(self, tab: bool) -> None:
"""Follow a link by faking an enter press."""
if tab:
self._tab.fake_key_press(Qt.Key_Enter, modifier=Qt.ControlModifier)
self._tab.fake_key_press(Qt.Key.Key_Enter, modifier=Qt.KeyboardModifier.ControlModifier)
else:
self._tab.fake_key_press(Qt.Key_Enter)
self._tab.fake_key_press(Qt.Key.Key_Enter)
def follow_selected(self, *, tab: bool = False) -> None:
raise NotImplementedError
@ -1238,10 +1243,10 @@ class AbstractTab(QWidget):
def fake_key_press(self,
key: Qt.Key,
modifier: Qt.KeyboardModifier = Qt.NoModifier) -> None:
modifier: Qt.KeyboardModifier = Qt.KeyboardModifier.NoModifier) -> None:
"""Send a fake key event to this tab."""
press_evt = QKeyEvent(QEvent.KeyPress, key, modifier, 0, 0, 0)
release_evt = QKeyEvent(QEvent.KeyRelease, key, modifier,
press_evt = QKeyEvent(QEvent.Type.KeyPress, key, modifier, 0, 0, 0)
release_evt = QKeyEvent(QEvent.Type.KeyRelease, key, modifier,
0, 0, 0)
self.send_event(press_evt)
self.send_event(release_evt)
@ -1315,8 +1320,8 @@ class AbstractTab(QWidget):
def __repr__(self) -> str:
try:
qurl = self.url()
url = qurl.toDisplayString(
QUrl.EncodeUnicode) # type: ignore[arg-type]
as_unicode = QUrl.ComponentFormattingOption.EncodeUnicode
url = qurl.toDisplayString(as_unicode) # type: ignore[arg-type]
except (AttributeError, RuntimeError) as exc:
url = '<{}>'.format(exc.__class__.__name__)
else:

View File

@ -24,8 +24,8 @@ import shlex
import functools
from typing import cast, Callable, Dict, Union, Optional
from PyQt5.QtWidgets import QApplication, QTabBar
from PyQt5.QtCore import Qt, QUrl, QEvent, QUrlQuery
from qutebrowser.qt.widgets import QApplication, QTabBar
from qutebrowser.qt.core import Qt, QUrl, QEvent, QUrlQuery
from qutebrowser.commands import userscripts, runners
from qutebrowser.api import cmdutils
@ -70,9 +70,7 @@ class CommandDispatcher:
raise cmdutils.CommandError("Private windows are unavailable with "
"the single-process process model.")
new_window = mainwindow.MainWindow(private=private)
new_window.show()
return new_window.tabbed_browser
return mainwindow.MainWindow(private=private).tabbed_browser
def _count(self) -> int:
"""Convenience method to get the widget count."""
@ -109,8 +107,15 @@ class CommandDispatcher:
raise cmdutils.CommandError("No WebView available yet!")
return widget
def _open(self, url, tab=False, background=False, window=False,
related=False, private=None):
def _open(
self,
url: QUrl,
tab: bool = False,
background: bool = False,
window: bool = False,
related: bool = False,
private: Optional[bool] = None,
) -> None:
"""Helper function to open a page.
Args:
@ -123,13 +128,15 @@ class CommandDispatcher:
"""
urlutils.raise_cmdexc_if_invalid(url)
tabbed_browser = self._tabbed_browser
cmdutils.check_exclusive((tab, background, window, private), 'tbwp')
cmdutils.check_exclusive((tab, background, window, private or False), 'tbwp')
if window and private is None:
private = self._tabbed_browser.is_private
if window or private:
assert isinstance(private, bool)
tabbed_browser = self._new_tabbed_browser(private)
tabbed_browser.tabopen(url)
tabbed_browser.window().show()
elif tab:
tabbed_browser.tabopen(url, background=False, related=related)
elif background:
@ -192,21 +199,21 @@ class CommandDispatcher:
what's configured in 'tabs.select_on_remove'.
Return:
QTabBar.SelectLeftTab, QTabBar.SelectRightTab, or None if no change
QTabBar.SelectionBehavior.SelectLeftTab, QTabBar.SelectionBehavior.SelectRightTab, or None if no change
should be made.
"""
cmdutils.check_exclusive((prev, next_, opposite), 'pno')
if prev:
return QTabBar.SelectLeftTab
return QTabBar.SelectionBehavior.SelectLeftTab
elif next_:
return QTabBar.SelectRightTab
return QTabBar.SelectionBehavior.SelectRightTab
elif opposite:
conf_selection = config.val.tabs.select_on_remove
if conf_selection == QTabBar.SelectLeftTab:
return QTabBar.SelectRightTab
elif conf_selection == QTabBar.SelectRightTab:
return QTabBar.SelectLeftTab
elif conf_selection == QTabBar.SelectPreviousTab:
if conf_selection == QTabBar.SelectionBehavior.SelectLeftTab:
return QTabBar.SelectionBehavior.SelectRightTab
elif conf_selection == QTabBar.SelectionBehavior.SelectRightTab:
return QTabBar.SelectionBehavior.SelectLeftTab
elif conf_selection == QTabBar.SelectionBehavior.SelectPreviousTab:
raise cmdutils.CommandError(
"-o is not supported with 'tabs.select_on_remove' set to "
"'last-used'!")
@ -403,14 +410,17 @@ class CommandDispatcher:
except browsertab.WebTabError as e:
raise cmdutils.CommandError(e)
# The new tab could be in a new tabbed_browser (e.g. because of
# tabs.tabs_are_windows being set)
if window or private:
new_tabbed_browser = self._new_tabbed_browser(
private=self._tabbed_browser.is_private or private)
else:
new_tabbed_browser = self._tabbed_browser
newtab = new_tabbed_browser.tabopen(background=bg)
new_tabbed_browser.window().show()
# The new tab could be in a new tabbed_browser (e.g. because of
# tabs.tabs_are_windows being set)
new_tabbed_browser = objreg.get('tabbed-browser', scope='window',
window=newtab.win_id)
idx = new_tabbed_browser.widget.indexOf(newtab)
@ -498,6 +508,8 @@ class CommandDispatcher:
"The window with id {} is not private".format(win_id))
tabbed_browser.tabopen(self._current_url())
tabbed_browser.window().show()
if not keep:
self._tabbed_browser.close_tab(self._current_widget(),
add_undo=False,
@ -706,9 +718,9 @@ class CommandDispatcher:
assert what in ['url', 'pretty-url'], what
if what == 'pretty-url':
flags = QUrl.RemovePassword | QUrl.DecodeReserved
flags = QUrl.UrlFormattingOption.RemovePassword | QUrl.ComponentFormattingOption.DecodeReserved
else:
flags = QUrl.RemovePassword | QUrl.FullyEncoded
flags = QUrl.UrlFormattingOption.RemovePassword | QUrl.ComponentFormattingOption.FullyEncoded
url = QUrl(self._current_url())
url_query = QUrlQuery()
@ -1200,7 +1212,7 @@ class CommandDispatcher:
except qtutils.QtValueError:
pass
else:
env['QUTE_URL'] = url.toString(QUrl.FullyEncoded)
env['QUTE_URL'] = url.toString(QUrl.ComponentFormattingOption.FullyEncoded)
try:
runner = userscripts.run_async(
@ -1330,8 +1342,8 @@ class CommandDispatcher:
current page's url.
"""
if url is None:
url = self._current_url().toString(QUrl.RemovePassword |
QUrl.FullyEncoded)
url = self._current_url().toString(QUrl.UrlFormattingOption.RemovePassword |
QUrl.ComponentFormattingOption.FullyEncoded)
try:
objreg.get('bookmark-manager').delete(url)
except KeyError:
@ -1774,8 +1786,8 @@ class CommandDispatcher:
raise cmdutils.CommandError(str(e))
for keyinfo in sequence:
press_event = keyinfo.to_event(QEvent.KeyPress)
release_event = keyinfo.to_event(QEvent.KeyRelease)
press_event = keyinfo.to_event(QEvent.Type.KeyPress)
release_event = keyinfo.to_event(QEvent.Type.KeyRelease)
if global_:
window = QApplication.focusWindow()
@ -1882,9 +1894,9 @@ class CommandDispatcher:
if not window.isFullScreen():
window.state_before_fullscreen = window.windowState()
if enter:
window.setWindowState(window.windowState() | Qt.WindowFullScreen)
window.setWindowState(window.windowState() | Qt.WindowState.WindowFullScreen)
else:
window.setWindowState(window.windowState() ^ Qt.WindowFullScreen)
window.setWindowState(window.windowState() ^ Qt.WindowState.WindowFullScreen)
log.misc.debug('state before fullscreen: {}'.format(
debug.qflags_key(Qt, window.state_before_fullscreen)))

View File

@ -30,7 +30,7 @@ import tempfile
import enum
from typing import Any, Dict, IO, List, MutableSequence, Optional, Union
from PyQt5.QtCore import (pyqtSlot, pyqtSignal, Qt, QObject, QModelIndex,
from qutebrowser.qt.core import (pyqtSlot, pyqtSignal, Qt, QObject, QModelIndex,
QTimer, QAbstractListModel, QUrl)
from qutebrowser.browser import pdfjs
@ -45,7 +45,7 @@ class ModelRole(enum.IntEnum):
"""Custom download model roles."""
item = Qt.UserRole
item = Qt.ItemDataRole.UserRole
# Remember the last used directory
@ -188,7 +188,7 @@ def get_filename_question(*, suggested_filename, url, parent=None):
q.title = "Save file to:"
q.text = "Please enter a location for <b>{}</b>".format(
html.escape(url.toDisplayString()))
q.url = url.toString(QUrl.RemovePassword | QUrl.FullyEncoded)
q.url = url.toString(QUrl.UrlFormattingOption.RemovePassword | QUrl.ComponentFormattingOption.FullyEncoded)
q.mode = usertypes.PromptMode.download
q.completed.connect(q.deleteLater)
q.default = _path_suggestion(suggested_filename)
@ -1266,10 +1266,10 @@ class DownloadModel(QAbstractListModel):
idx = self.index(self.rowCount() - 1)
return idx
def headerData(self, section, orientation, role=Qt.DisplayRole):
def headerData(self, section, orientation, role=Qt.ItemDataRole.DisplayRole):
"""Simple constant header."""
if (section == 0 and orientation == Qt.Horizontal and
role == Qt.DisplayRole):
if (section == 0 and orientation == Qt.Orientation.Horizontal and
role == Qt.ItemDataRole.DisplayRole):
return "Downloads"
else:
return ""
@ -1283,15 +1283,15 @@ class DownloadModel(QAbstractListModel):
return None
item = self[index.row()]
if role == Qt.DisplayRole:
if role == Qt.ItemDataRole.DisplayRole:
data: Any = str(item)
elif role == Qt.ForegroundRole:
elif role == Qt.ItemDataRole.ForegroundRole:
data = item.get_status_color('fg')
elif role == Qt.BackgroundRole:
elif role == Qt.ItemDataRole.BackgroundRole:
data = item.get_status_color('bg')
elif role == ModelRole.item:
data = item
elif role == Qt.ToolTipRole:
elif role == Qt.ItemDataRole.ToolTipRole:
if item.error_msg is None:
data = None
else:
@ -1303,11 +1303,11 @@ class DownloadModel(QAbstractListModel):
def flags(self, index):
"""Override flags so items aren't selectable.
The default would be Qt.ItemIsEnabled | Qt.ItemIsSelectable.
The default would be Qt.ItemFlag.ItemIsEnabled | Qt.ItemFlag.ItemIsSelectable.
"""
if not index.isValid():
return Qt.ItemFlags()
return Qt.ItemIsEnabled | Qt.ItemNeverHasChildren
return Qt.ItemFlag.NoItemFlags
return Qt.ItemFlag.ItemIsEnabled | Qt.ItemFlag.ItemNeverHasChildren
def rowCount(self, parent=QModelIndex()):
"""Get count of active downloads."""

View File

@ -22,8 +22,8 @@
import functools
from typing import Callable, MutableSequence, Tuple, Union
from PyQt5.QtCore import pyqtSlot, QSize, Qt
from PyQt5.QtWidgets import QListView, QSizePolicy, QMenu, QStyleFactory
from qutebrowser.qt.core import pyqtSlot, QSize, Qt
from qutebrowser.qt.widgets import QListView, QSizePolicy, QMenu, QStyleFactory
from qutebrowser.browser import downloads
from qutebrowser.config import stylesheet
@ -62,11 +62,11 @@ class DownloadView(QListView):
if not utils.is_mac:
self.setStyle(QStyleFactory.create('Fusion'))
stylesheet.set_register(self)
self.setResizeMode(QListView.Adjust)
self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
self.setSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.Fixed)
self.setFocusPolicy(Qt.NoFocus)
self.setFlow(QListView.LeftToRight)
self.setResizeMode(QListView.ResizeMode.Adjust)
self.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
self.setSizePolicy(QSizePolicy.Policy.MinimumExpanding, QSizePolicy.Policy.Fixed)
self.setFocusPolicy(Qt.FocusPolicy.NoFocus)
self.setFlow(QListView.Flow.LeftToRight)
self.setSpacing(1)
self._menu = None
model.rowsInserted.connect(self._update_geometry)
@ -74,7 +74,7 @@ class DownloadView(QListView):
model.dataChanged.connect(self._update_geometry)
self.setModel(model)
self.setWrapping(True)
self.setContextMenuPolicy(Qt.CustomContextMenu)
self.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
self.customContextMenuRequested.connect(self.show_context_menu)
self.clicked.connect(self.on_clicked)

View File

@ -19,11 +19,11 @@
"""Event handling for a browser tab."""
from PyQt5.QtCore import QObject, QEvent, Qt, QTimer
from qutebrowser.qt import machinery
from qutebrowser.qt.core import QObject, QEvent, Qt, QTimer
from qutebrowser.config import config
from qutebrowser.utils import message, log, usertypes, qtutils
from qutebrowser.misc import objects
from qutebrowser.utils import log, message, usertypes
from qutebrowser.keyinput import modeman
@ -48,7 +48,7 @@ class ChildEventFilter(QObject):
def eventFilter(self, obj, event):
"""Act on ChildAdded events."""
if event.type() == QEvent.ChildAdded:
if event.type() == QEvent.Type.ChildAdded:
child = event.child()
log.misc.debug("{} got new child {}, installing filter"
.format(obj, child))
@ -58,7 +58,7 @@ class ChildEventFilter(QObject):
assert obj is self._widget
child.installEventFilter(self._filter)
elif event.type() == QEvent.ChildRemoved:
elif event.type() == QEvent.Type.ChildRemoved:
child = event.child()
log.misc.debug("{}: removed child {}".format(obj, child))
@ -81,10 +81,9 @@ class TabEventFilter(QObject):
super().__init__(parent)
self._tab = tab
self._handlers = {
QEvent.MouseButtonPress: self._handle_mouse_press,
QEvent.MouseButtonRelease: self._handle_mouse_release,
QEvent.Wheel: self._handle_wheel,
QEvent.KeyRelease: self._handle_key_release,
QEvent.Type.MouseButtonPress: self._handle_mouse_press,
QEvent.Type.MouseButtonRelease: self._handle_mouse_release,
QEvent.Type.Wheel: self._handle_wheel,
}
self._ignore_wheel_event = False
self._check_insertmode_on_release = False
@ -99,10 +98,13 @@ class TabEventFilter(QObject):
True if the event should be filtered, False otherwise.
"""
is_rocker_gesture = (config.val.input.mouse.rocker_gestures and
e.buttons() == Qt.LeftButton | Qt.RightButton)
e.buttons() == Qt.MouseButton.LeftButton | Qt.MouseButton.RightButton)
if e.button() in [Qt.XButton1, Qt.XButton2] or is_rocker_gesture:
self._mousepress_backforward(e)
if e.button() in [Qt.MouseButton.XButton1, Qt.MouseButton.XButton2] or is_rocker_gesture:
if not machinery.IS_QT6:
self._mousepress_backforward(e)
# FIXME:qt6 For some reason, this doesn't filter the action on
# Qt 6...
return True
self._ignore_wheel_event = True
@ -112,7 +114,7 @@ class TabEventFilter(QObject):
log.mouse.warning("Ignoring invalid click at {}".format(pos))
return False
if e.button() != Qt.NoButton:
if e.button() != Qt.MouseButton.NoButton:
self._tab.elements.find_at_pos(pos, self._mousepress_insertmode_cb)
return False
@ -150,7 +152,7 @@ class TabEventFilter(QObject):
if mode == usertypes.KeyMode.hint:
return True
elif e.modifiers() & Qt.ControlModifier:
elif e.modifiers() & Qt.KeyboardModifier.ControlModifier:
if mode == usertypes.KeyMode.passthrough:
return False
@ -170,21 +172,6 @@ class TabEventFilter(QObject):
return False
def _handle_key_release(self, e):
"""Ignore repeated key release events going to the website.
WORKAROUND for https://bugreports.qt.io/browse/QTBUG-77208
Args:
e: The QKeyEvent.
Return:
True if the event should be filtered, False otherwise.
"""
return (e.isAutoRepeat() and
not qtutils.version_check('5.14', compiled=False) and
objects.backend == usertypes.Backend.QtWebEngine)
def _mousepress_insertmode_cb(self, elem):
"""Check if the clicked element is editable."""
if elem is None:
@ -240,17 +227,17 @@ class TabEventFilter(QObject):
True if the event should be filtered, False otherwise.
"""
if (not config.val.input.mouse.back_forward_buttons and
e.button() in [Qt.XButton1, Qt.XButton2]):
e.button() in [Qt.MouseButton.XButton1, Qt.MouseButton.XButton2]):
# Back and forward on mice are disabled
return
if e.button() in [Qt.XButton1, Qt.LeftButton]:
if e.button() in [Qt.MouseButton.XButton1, Qt.MouseButton.LeftButton]:
# Back button on mice which have it, or rocker gesture
if self._tab.history.can_go_back():
self._tab.history.back()
else:
message.error("At beginning of history.")
elif e.button() in [Qt.XButton2, Qt.RightButton]:
elif e.button() in [Qt.MouseButton.XButton2, Qt.MouseButton.RightButton]:
# Forward button on mice which have it, or rocker gesture
if self._tab.history.can_go_forward():
self._tab.history.forward()

View File

@ -29,7 +29,7 @@ import textwrap
import dataclasses
from typing import cast, List, Sequence, Tuple, Optional
from PyQt5.QtCore import pyqtSignal, QObject, QUrl
from qutebrowser.qt.core import pyqtSignal, QObject, QUrl
from qutebrowser.utils import (log, standarddir, jinja, objreg, utils,
javascript, urlmatch, version, usertypes, message)
@ -145,7 +145,7 @@ class GreasemonkeyScript:
def needs_document_end_workaround(self):
"""Check whether to force @run-at document-end.
This needs to be done on QtWebEngine (since Qt 5.12) for known-broken scripts.
This needs to be done on QtWebEngine for known-broken scripts.
On Qt 5.12, accessing the DOM isn't possible with "@run-at
document-start". It was documented to be impossible before, but seems
@ -270,7 +270,7 @@ class GreasemonkeyMatcher:
def __init__(self, url):
self._url = url
self._url_string = url.toString(QUrl.FullyEncoded)
self._url_string = url.toString(QUrl.ComponentFormattingOption.FullyEncoded)
self.is_greaseable = url.scheme() in self.GREASEABLE_SCHEMES
def _match_pattern(self, pattern):

View File

@ -30,8 +30,8 @@ from string import ascii_lowercase
from typing import (TYPE_CHECKING, Callable, Dict, Iterable, Iterator, List, Mapping,
MutableSequence, Optional, Sequence, Set)
from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject, Qt, QUrl
from PyQt5.QtWidgets import QLabel
from qutebrowser.qt.core import pyqtSignal, pyqtSlot, QObject, Qt, QUrl
from qutebrowser.qt.widgets import QLabel
from qutebrowser.config import config, configexc
from qutebrowser.keyinput import modeman, modeparsers, basekeyparser
@ -92,12 +92,12 @@ class HintLabel(QLabel):
self._context = context
self.elem = elem
self.setTextFormat(Qt.RichText)
self.setTextFormat(Qt.TextFormat.RichText)
# Make sure we can style the background via a style sheet, and we don't
# get any extra text indent from Qt.
# The real stylesheet lives in mainwindow.py for performance reasons..
self.setAttribute(Qt.WA_StyledBackground, True)
self.setAttribute(Qt.WidgetAttribute.WA_StyledBackground, True)
self.setIndent(0)
self._context.tab.contents_size_changed.connect(self._move_to_elem)
@ -252,9 +252,9 @@ class HintActions:
sel = (context.target == Target.yank_primary and
utils.supports_selection())
flags = QUrl.FullyEncoded | QUrl.RemovePassword
flags = QUrl.ComponentFormattingOption.FullyEncoded | QUrl.UrlFormattingOption.RemovePassword
if url.scheme() == 'mailto':
flags |= QUrl.RemoveScheme # type: ignore[operator]
flags |= QUrl.UrlFormattingOption.RemoveScheme # type: ignore[operator]
urlstr = url.toString(flags)
new_content = urlstr
@ -276,14 +276,14 @@ class HintActions:
def run_cmd(self, url: QUrl, context: HintContext) -> None:
"""Run the command based on a hint URL."""
urlstr = url.toString(QUrl.FullyEncoded) # type: ignore[arg-type]
urlstr = url.toString(QUrl.ComponentFormattingOption.FullyEncoded) # type: ignore[arg-type]
args = context.get_args(urlstr)
commandrunner = runners.CommandRunner(self._win_id)
commandrunner.run_safely(' '.join(args))
def preset_cmd_text(self, url: QUrl, context: HintContext) -> None:
"""Preset a commandline text based on a hint URL."""
flags = QUrl.FullyEncoded
flags = QUrl.ComponentFormattingOption.FullyEncoded
urlstr = url.toDisplayString(flags) # type: ignore[arg-type]
args = context.get_args(urlstr)
text = ' '.join(args)
@ -325,7 +325,7 @@ class HintActions:
cmd = context.args[0]
args = context.args[1:]
flags = QUrl.FullyEncoded
flags = QUrl.ComponentFormattingOption.FullyEncoded
env = {
'QUTE_MODE': 'hints',
@ -356,7 +356,8 @@ class HintActions:
url: The URL to open as a QUrl.
context: The HintContext to use.
"""
urlstr = url.toString(QUrl.FullyEncoded | QUrl.RemovePassword)
urlstr = url.toString(
QUrl.ComponentFormattingOption.FullyEncoded | QUrl.UrlFormattingOption.RemovePassword)
args = context.get_args(urlstr)
commandrunner = runners.CommandRunner(self._win_id)
commandrunner.run_safely('spawn ' + ' '.join(args))

View File

@ -25,8 +25,8 @@ import contextlib
import pathlib
from typing import cast, Mapping, MutableSequence, Optional
from PyQt5.QtCore import pyqtSlot, QUrl, QObject, pyqtSignal
from PyQt5.QtWidgets import QProgressDialog, QApplication
from qutebrowser.qt.core import pyqtSlot, QUrl, QObject, pyqtSignal
from qutebrowser.qt.widgets import QProgressDialog, QApplication
from qutebrowser.config import config
from qutebrowser.api import cmdutils
@ -435,10 +435,10 @@ class WebHistory(sql.SqlTable):
}, replace=True)
def _format_url(self, url):
return url.toString(QUrl.RemovePassword | QUrl.FullyEncoded)
return url.toString(QUrl.UrlFormattingOption.RemovePassword | QUrl.ComponentFormattingOption.FullyEncoded)
def _format_completion_url(self, url):
return url.toString(QUrl.RemovePassword)
return url.toString(QUrl.UrlFormattingOption.RemovePassword)
@cmdutils.register()

View File

@ -24,9 +24,9 @@ import binascii
import enum
from typing import cast, Optional, Any
from PyQt5.QtWidgets import QWidget
from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject, QEvent
from PyQt5.QtGui import QCloseEvent
from qutebrowser.qt.widgets import QWidget
from qutebrowser.qt.core import pyqtSignal, pyqtSlot, QObject, QEvent
from qutebrowser.qt.gui import QCloseEvent
from qutebrowser.browser import eventfilter
from qutebrowser.config import configfiles, config
@ -74,7 +74,7 @@ class _EventFilter(QObject):
def eventFilter(self, _obj: QObject, event: QEvent) -> bool:
"""Translate mouse presses to a clicked signal."""
if event.type() == QEvent.MouseButtonPress:
if event.type() == QEvent.Type.MouseButtonPress:
self.clicked.emit()
return False

View File

@ -23,7 +23,7 @@ import re
import posixpath
from typing import Optional, Set
from PyQt5.QtCore import QUrl
from qutebrowser.qt.core import QUrl
from qutebrowser.browser import webelem
from qutebrowser.config import config
@ -42,24 +42,24 @@ class Error(Exception):
# of information. (host and path use FullyDecoded by default)
_URL_SEGMENTS = [
('host',
lambda url: url.host(QUrl.FullyEncoded),
lambda url, host: url.setHost(host, QUrl.StrictMode)),
lambda url: url.host(QUrl.ComponentFormattingOption.FullyEncoded),
lambda url, host: url.setHost(host, QUrl.ParsingMode.StrictMode)),
('port',
lambda url: str(url.port()) if url.port() > 0 else '',
lambda url, x: url.setPort(int(x))),
('path',
lambda url: url.path(QUrl.FullyEncoded),
lambda url, path: url.setPath(path, QUrl.StrictMode)),
lambda url: url.path(QUrl.ComponentFormattingOption.FullyEncoded),
lambda url, path: url.setPath(path, QUrl.ParsingMode.StrictMode)),
('query',
lambda url: url.query(QUrl.FullyEncoded),
lambda url, query: url.setQuery(query, QUrl.StrictMode)),
lambda url: url.query(QUrl.ComponentFormattingOption.FullyEncoded),
lambda url, query: url.setQuery(query, QUrl.ParsingMode.StrictMode)),
('anchor',
lambda url: url.fragment(QUrl.FullyEncoded),
lambda url, fragment: url.setFragment(fragment, QUrl.StrictMode)),
lambda url: url.fragment(QUrl.ComponentFormattingOption.FullyEncoded),
lambda url, fragment: url.setFragment(fragment, QUrl.ParsingMode.StrictMode)),
]
@ -130,14 +130,14 @@ def path_up(url, count):
count: The number of levels to go up in the url.
"""
urlutils.ensure_valid(url)
url = url.adjusted(QUrl.RemoveFragment | QUrl.RemoveQuery)
path = url.path(QUrl.FullyEncoded)
url = url.adjusted(QUrl.UrlFormattingOption.RemoveFragment | QUrl.UrlFormattingOption.RemoveQuery)
path = url.path(QUrl.ComponentFormattingOption.FullyEncoded)
if not path or path == '/':
raise Error("Can't go up!")
for _i in range(0, min(count, path.count('/'))):
path = posixpath.join(path, posixpath.pardir)
path = posixpath.normpath(path)
url.setPath(path, QUrl.StrictMode)
url.setPath(path, QUrl.ParsingMode.StrictMode)
return url
@ -146,7 +146,7 @@ def strip(url, count):
if count != 1:
raise Error("Count is not supported when stripping URL components")
urlutils.ensure_valid(url)
return url.adjusted(QUrl.RemoveFragment | QUrl.RemoveQuery)
return url.adjusted(QUrl.UrlFormattingOption.RemoveFragment | QUrl.UrlFormattingOption.RemoveQuery)
def _find_prevnext(prev, elems):
@ -219,10 +219,10 @@ def prevnext(*, browsertab, win_id, baseurl, prev=False,
if window:
new_window = mainwindow.MainWindow(
private=cur_tabbed_browser.is_private)
new_window.show()
tabbed_browser = objreg.get('tabbed-browser', scope='window',
window=new_window.win_id)
tabbed_browser.tabopen(url, background=False)
new_window.show()
elif tab:
cur_tabbed_browser.tabopen(url, background=background)
else:

View File

@ -23,11 +23,11 @@ import sys
import functools
from typing import Optional
from PyQt5.QtCore import QObject, pyqtSignal, pyqtSlot, QUrl
from PyQt5.QtNetwork import (QNetworkProxy, QNetworkRequest, QHostInfo,
from qutebrowser.qt.core import QObject, pyqtSignal, pyqtSlot, QUrl
from qutebrowser.qt.network import (QNetworkProxy, QNetworkRequest, QHostInfo,
QNetworkReply, QNetworkAccessManager,
QHostAddress)
from PyQt5.QtQml import QJSEngine, QJSValue
from qutebrowser.qt.qml import QJSEngine, QJSValue
from qutebrowser.utils import log, utils, qtutils, resources
@ -109,10 +109,10 @@ class _PACContext(QObject):
host: hostname to resolve.
"""
ips = QHostInfo.fromName(host)
if ips.error() != QHostInfo.NoError or not ips.addresses():
if ips.error() != QHostInfo.HostInfoError.NoError or not ips.addresses():
err_f = "Failed to resolve host during PAC evaluation: {}"
log.network.info(err_f.format(host))
return QJSValue(QJSValue.NullValue)
return QJSValue(QJSValue.SpecialValue.NullValue)
else:
return ips.addresses()[0].toString()
@ -123,7 +123,7 @@ class _PACContext(QObject):
Return the server IP address of the current machine, as a string in
the dot-separated integer format.
"""
return QHostAddress(QHostAddress.LocalHost).toString()
return QHostAddress(QHostAddress.SpecialAddress.LocalHost).toString()
class PACResolver:
@ -150,17 +150,17 @@ class PACResolver:
if len(config) != 1:
raise ParseProxyError("Invalid number of parameters for " +
"DIRECT")
return QNetworkProxy(QNetworkProxy.NoProxy)
return QNetworkProxy(QNetworkProxy.ProxyType.NoProxy)
elif config[0] == "PROXY":
if len(config) != 2:
raise ParseProxyError("Invalid number of parameters for PROXY")
host, port = PACResolver._parse_proxy_host(config[1])
return QNetworkProxy(QNetworkProxy.HttpProxy, host, port)
return QNetworkProxy(QNetworkProxy.ProxyType.HttpProxy, host, port)
elif config[0] in ["SOCKS", "SOCKS5"]:
if len(config) != 2:
raise ParseProxyError("Invalid number of parameters for SOCKS")
host, port = PACResolver._parse_proxy_host(config[1])
return QNetworkProxy(QNetworkProxy.Socks5Proxy, host, port)
return QNetworkProxy(QNetworkProxy.ProxyType.Socks5Proxy, host, port)
else:
err = "Unknown proxy type: {}"
raise ParseProxyError(err.format(config[0]))
@ -184,7 +184,7 @@ class PACResolver:
"""
self._engine = QJSEngine()
self._engine.installExtensions(QJSEngine.ConsoleExtension)
self._engine.installExtensions(QJSEngine.Extension.ConsoleExtension)
self._ctx = _PACContext(self._engine)
self._engine.globalObject().setProperty(
@ -215,12 +215,12 @@ class PACResolver:
qtutils.ensure_valid(query.url())
if from_file:
string_flags = QUrl.PrettyDecoded
string_flags = QUrl.ComponentFormattingOption.PrettyDecoded
else:
string_flags = QUrl.RemoveUserInfo # type: ignore[assignment]
string_flags = QUrl.UrlFormattingOption.RemoveUserInfo # type: ignore[assignment]
if query.url().scheme() == 'https':
string_flags |= QUrl.RemovePath # type: ignore[assignment]
string_flags |= QUrl.RemoveQuery # type: ignore[assignment]
string_flags |= QUrl.UrlFormattingOption.RemovePath # type: ignore[assignment]
string_flags |= QUrl.UrlFormattingOption.RemoveQuery # type: ignore[assignment]
result = self._resolver.call([query.url().toString(string_flags),
query.peerHostName()])
@ -255,7 +255,7 @@ class PACFetcher(QObject):
# WORKAROUND for a hang when messages are printed, see our
# NetworkAccessManager subclass for details.
self._manager: Optional[QNetworkAccessManager] = QNetworkAccessManager()
self._manager.setProxy(QNetworkProxy(QNetworkProxy.NoProxy))
self._manager.setProxy(QNetworkProxy(QNetworkProxy.ProxyType.NoProxy))
self._pac = None
self._error_message = None
self._reply = None
@ -275,7 +275,7 @@ class PACFetcher(QObject):
@pyqtSlot()
def _finish(self):
assert self._reply is not None
if self._reply.error() != QNetworkReply.NoError:
if self._reply.error() != QNetworkReply.NetworkError.NoError:
error = "Can't fetch PAC file from URL, error code {}: {}"
self._error_message = error.format(
self._reply.error(), self._reply.errorString())
@ -337,4 +337,4 @@ class PACFetcher(QObject):
# Later NetworkManager.createRequest will detect this and display
# an error message.
error_host = "pac-resolve-error.qutebrowser.invalid"
return [QNetworkProxy(QNetworkProxy.HttpProxy, error_host, 9)]
return [QNetworkProxy(QNetworkProxy.ProxyType.HttpProxy, error_host, 9)]

View File

@ -19,8 +19,8 @@
"""Handling of proxies."""
from PyQt5.QtCore import QUrl, pyqtSlot
from PyQt5.QtNetwork import QNetworkProxy, QNetworkProxyFactory
from qutebrowser.qt.core import QUrl, pyqtSlot
from qutebrowser.qt.network import QNetworkProxy, QNetworkProxyFactory
from qutebrowser.config import config, configtypes
from qutebrowser.utils import message, usertypes, urlutils, utils
@ -73,11 +73,11 @@ class ProxyFactory(QNetworkProxyFactory):
return None
def _set_capabilities(self, proxy):
if proxy.type() == QNetworkProxy.NoProxy:
if proxy.type() == QNetworkProxy.ProxyType.NoProxy:
return
capabilities = proxy.capabilities()
lookup_cap = QNetworkProxy.HostNameLookupCapability
lookup_cap = QNetworkProxy.Capability.HostNameLookupCapability
if config.val.content.proxy_dns_requests:
capabilities |= lookup_cap
else:
@ -97,7 +97,7 @@ class ProxyFactory(QNetworkProxyFactory):
if proxy is configtypes.SYSTEM_PROXY:
# On Linux, use "export http_proxy=socks5://host:port" to manually
# set system proxy.
# ref. https://doc.qt.io/qt-5/qnetworkproxyfactory.html#systemProxyForQuery
# ref. https://doc.qt.io/qt-6/qnetworkproxyfactory.html#systemProxyForQuery
proxies = QNetworkProxyFactory.systemProxyForQuery(query)
elif isinstance(proxy, pac.PACFetcher):
if objects.backend == usertypes.Backend.QtWebEngine:

View File

@ -22,7 +22,7 @@
import os
from PyQt5.QtCore import QUrl, QUrlQuery
from qutebrowser.qt.core import QUrl, QUrlQuery
from qutebrowser.utils import resources, javascript, jinja, standarddir, log
from qutebrowser.config import config
@ -96,7 +96,7 @@ def _generate_pdfjs_script(filename):
url.setQuery(url_query)
js_url = javascript.to_js(
url.toString(QUrl.FullyEncoded)) # type: ignore[arg-type]
url.toString(QUrl.ComponentFormattingOption.FullyEncoded)) # type: ignore[arg-type]
return jinja.js_environment.from_string("""
document.addEventListener("DOMContentLoaded", function() {
@ -245,7 +245,7 @@ def get_main_url(filename: str, original_url: QUrl) -> QUrl:
query = QUrlQuery()
query.addQueryItem('filename', filename) # read from our JS
query.addQueryItem('file', '') # to avoid pdfjs opening the default PDF
urlstr = original_url.toString(QUrl.FullyEncoded) # type: ignore[arg-type]
urlstr = original_url.toString(QUrl.ComponentFormattingOption.FullyEncoded) # type: ignore[arg-type]
query.addQueryItem('source', urlstr)
url.setQuery(query)
return url

View File

@ -26,9 +26,9 @@ import functools
import dataclasses
from typing import Dict, IO, Optional
from PyQt5.QtCore import pyqtSlot, pyqtSignal, QTimer, QUrl
from PyQt5.QtWidgets import QApplication
from PyQt5.QtNetwork import QNetworkRequest, QNetworkReply, QNetworkAccessManager
from qutebrowser.qt.core import pyqtSlot, pyqtSignal, QTimer, QUrl
from qutebrowser.qt.widgets import QApplication
from qutebrowser.qt.network import QNetworkRequest, QNetworkReply, QNetworkAccessManager
from qutebrowser.config import config, websettings
from qutebrowser.utils import message, usertypes, log, urlutils, utils, debug, objreg
@ -62,12 +62,8 @@ class DownloadItem(downloads.AbstractDownloadItem):
As soon as we know the file object, we copy self._buffer over and the next
readyRead will write to the real file object.
Class attributes:
_MAX_REDIRECTS: The maximum redirection count.
Attributes:
_retry_info: A _RetryInfo instance.
_redirects: How many time we were redirected already.
_buffer: A BytesIO object to buffer incoming data until we know the
target file.
_read_timer: A Timer which reads the QNetworkReply into self._buffer
@ -82,7 +78,6 @@ class DownloadItem(downloads.AbstractDownloadItem):
arg 0: The new DownloadItem
"""
_MAX_REDIRECTS = 10
adopt_download = pyqtSignal(object) # DownloadItem
def __init__(self, reply, manager):
@ -102,7 +97,6 @@ class DownloadItem(downloads.AbstractDownloadItem):
self._read_timer = usertypes.Timer(self, name='download-read-timer')
self._read_timer.setInterval(500)
self._read_timer.timeout.connect(self._on_read_timer_timeout)
self._redirects = 0
self._url = reply.url()
self._init_reply(reply)
@ -123,10 +117,12 @@ class DownloadItem(downloads.AbstractDownloadItem):
if self._reply is None:
log.downloads.debug("Reply gone while dying")
return
self._reply.downloadProgress.disconnect()
self._reply.finished.disconnect()
self._reply.error.disconnect()
self._reply.errorOccurred.disconnect()
self._reply.readyRead.disconnect()
with log.hide_qt_warning('QNetworkReplyImplPrivate::error: Internal '
'problem, this method must only be called '
'once.'):
@ -135,11 +131,22 @@ class DownloadItem(downloads.AbstractDownloadItem):
self._reply.deleteLater()
self._reply = None
if self.fileobj is not None:
pos = self.fileobj.tell()
log.downloads.debug(f"File position at error: {pos}")
try:
self.fileobj.close()
except OSError:
log.downloads.exception("Error while closing file object")
if pos == 0:
# Emtpy remaining file
filename = self._get_open_filename()
log.downloads.debug(f"Removing empty file at {filename}")
try:
os.remove(filename)
except OSError:
log.downloads.exception("Error while removing empty file")
def _init_reply(self, reply):
"""Set a new reply and connect its signals.
@ -150,11 +157,14 @@ class DownloadItem(downloads.AbstractDownloadItem):
self.successful = False
self._reply = reply
reply.setReadBufferSize(16 * 1024 * 1024) # 16 MB
reply.downloadProgress.connect(self.stats.on_download_progress)
reply.finished.connect(self._on_reply_finished)
reply.error.connect(self._on_reply_error)
reply.errorOccurred.connect(self._on_reply_error)
reply.readyRead.connect(self._on_ready_read)
reply.metaDataChanged.connect(self._on_meta_data_changed)
reply.redirected.connect(self._on_redirected)
self._retry_info = _RetryInfo(request=reply.request(),
manager=reply.manager())
if not self.fileobj:
@ -162,9 +172,16 @@ class DownloadItem(downloads.AbstractDownloadItem):
# We could have got signals before we connected slots to them.
# Here no signals are connected to the DownloadItem yet, so we use a
# singleShot QTimer to emit them after they are connected.
if reply.error() != QNetworkReply.NoError:
if reply.error() != QNetworkReply.NetworkError.NoError:
QTimer.singleShot(0, lambda: self._die(reply.errorString()))
@pyqtSlot(QUrl)
def _on_redirected(self, url):
if self._reply is None:
log.downloads.warning(f"redirected: REPLY GONE -> {url}")
else:
log.downloads.debug(f"redirected: {self._reply.url()} -> {url}")
def _do_cancel(self):
self._read_timer.stop()
if self._reply is not None:
@ -286,7 +303,7 @@ class DownloadItem(downloads.AbstractDownloadItem):
self.fileobj.write(self._reply.readAll())
if self._autoclose:
self.fileobj.close()
self.successful = self._reply.error() == QNetworkReply.NoError
self.successful = self._reply.error() == QNetworkReply.NetworkError.NoError
self._reply.close()
self._reply.deleteLater()
self._reply = None
@ -307,9 +324,6 @@ class DownloadItem(downloads.AbstractDownloadItem):
return
self._read_timer.stop()
self.stats.finish()
is_redirected = self._handle_redirect()
if is_redirected:
return
log.downloads.debug("Reply finished, fileobj {}".format(self.fileobj))
if self.fileobj is not None:
# We can do a "delayed" write immediately to empty the buffer and
@ -334,7 +348,7 @@ class DownloadItem(downloads.AbstractDownloadItem):
@pyqtSlot('QNetworkReply::NetworkError')
def _on_reply_error(self, code):
"""Handle QNetworkReply errors."""
if code == QNetworkReply.OperationCanceledError:
if code == QNetworkReply.NetworkError.OperationCanceledError:
return
if self._reply is None:
@ -364,48 +378,6 @@ class DownloadItem(downloads.AbstractDownloadItem):
for key, value in self._reply.rawHeaderPairs():
self.raw_headers[bytes(key)] = bytes(value)
def _handle_redirect(self):
"""Handle an HTTP redirect.
Return:
True if the download was redirected, False otherwise.
"""
assert self._reply is not None
redirect = self._reply.attribute(
QNetworkRequest.RedirectionTargetAttribute)
if redirect is None or redirect.isEmpty():
return False
new_url = self._reply.url().resolved(redirect)
new_request = self._reply.request()
if new_url == new_request.url():
return False
if self._redirects > self._MAX_REDIRECTS:
self._die("Maximum redirection count reached!")
self.delete()
return True # so on_reply_finished aborts
log.downloads.debug("{}: Handling redirect".format(self))
self._redirects += 1
new_request.setUrl(new_url)
old_reply = self._reply
assert old_reply is not None
old_reply.finished.disconnect(self._on_reply_finished)
self._read_timer.stop()
self._reply = None
if self.fileobj is not None:
self.fileobj.seek(0)
log.downloads.debug("redirected: {} -> {}".format(
old_reply.url(), new_request.url()))
new_reply = old_reply.manager().get(new_request)
self._init_reply(new_reply)
old_reply.deleteLater()
return True
def _uses_nam(self, nam):
"""Check if this download uses the given QNetworkAccessManager."""
assert self._retry_info is not None
@ -422,8 +394,17 @@ class DownloadManager(downloads.AbstractDownloadManager):
Attributes:
_networkmanager: A NetworkManager for generic downloads.
Class attributes:
_MAX_REDIRECTS: The maximum redirection count.
"""
# Same as many browsers
# https://fetch.spec.whatwg.org/#http-redirect-fetch
# https://source.chromium.org/chromium/chromium/src/+/main:net/url_request/url_request.h;l=97;drc=3c19a2edb96d3d5b56a7481349a357fdbdf8ecf0
# https://stackoverflow.com/questions/9384474/in-chrome-how-many-redirects-are-too-many
_MAX_REDIRECTS = 20
def __init__(self, parent=None):
super().__init__(parent)
self._networkmanager = networkmanager.NetworkManager(
@ -447,11 +428,19 @@ class DownloadManager(downloads.AbstractDownloadManager):
return None
req = QNetworkRequest(url)
user_agent = websettings.user_agent(url)
req.setHeader(QNetworkRequest.UserAgentHeader, user_agent)
user_agent = websettings.user_agent(url)
req.setHeader(QNetworkRequest.KnownHeaders.UserAgentHeader, user_agent)
if not cache:
req.setAttribute(QNetworkRequest.CacheSaveControlAttribute, False)
req.setAttribute(QNetworkRequest.Attribute.CacheSaveControlAttribute, False)
# Needed for Qt 5, default on Qt 6
# We don't set this on the QNAM because QtWebKit handles redirects manually.
req.setAttribute(
QNetworkRequest.Attribute.RedirectPolicyAttribute,
QNetworkRequest.RedirectPolicy.NoLessSafeRedirectPolicy,
)
req.setMaximumRedirectsAllowed(self._MAX_REDIRECTS)
return self.get_request(req, **kwargs)
@ -511,8 +500,8 @@ class DownloadManager(downloads.AbstractDownloadManager):
"""
# WORKAROUND for Qt corrupting data loaded from cache:
# https://bugreports.qt.io/browse/QTBUG-42757
request.setAttribute(QNetworkRequest.CacheLoadControlAttribute,
QNetworkRequest.AlwaysNetwork)
request.setAttribute(QNetworkRequest.Attribute.CacheLoadControlAttribute,
QNetworkRequest.CacheLoadControl.AlwaysNetwork)
if suggested_fn is None:
suggested_fn = self._get_suggested_filename(request)

View File

@ -34,7 +34,7 @@ import collections
import secrets
from typing import TypeVar, Callable, Dict, List, Optional, Union, Sequence, Tuple
from PyQt5.QtCore import QUrlQuery, QUrl
from qutebrowser.qt.core import QUrlQuery, QUrl
import qutebrowser
from qutebrowser.browser import pdfjs, downloads, history
@ -128,7 +128,9 @@ def data_for_url(url: QUrl) -> Tuple[str, bytes]:
Return:
A (mimetype, data) tuple.
"""
norm_url = url.adjusted(QUrl.NormalizePathSegments | QUrl.StripTrailingSlash)
norm_url = url.adjusted(
QUrl.UrlFormattingOption.NormalizePathSegments |
QUrl.UrlFormattingOption.StripTrailingSlash)
if norm_url != url:
raise Redirect(norm_url)
@ -423,8 +425,8 @@ def qute_help(url: QUrl) -> _HandlerRet:
def _qute_settings_set(url: QUrl) -> _HandlerRet:
"""Handler for qute://settings/set."""
query = QUrlQuery(url)
option = query.queryItemValue('option', QUrl.FullyDecoded)
value = query.queryItemValue('value', QUrl.FullyDecoded)
option = query.queryItemValue('option', QUrl.ComponentFormattingOption.FullyDecoded)
value = query.queryItemValue('value', QUrl.ComponentFormattingOption.FullyDecoded)
# https://github.com/qutebrowser/qutebrowser/issues/727
if option == 'content.javascript.enabled' and value == 'false':
@ -498,10 +500,11 @@ def qute_back(url: QUrl) -> _HandlerRet:
@add_handler('configdiff')
def qute_configdiff(_url: QUrl) -> _HandlerRet:
def qute_configdiff(url: QUrl) -> _HandlerRet:
"""Handler for qute://configdiff."""
data = config.instance.dump_userconfig().encode('utf-8')
return 'text/plain', data
include_hidden = QUrlQuery(url).queryItemValue('include_hidden') == 'true'
dump = config.instance.dump_userconfig(include_hidden=include_hidden)
return 'text/plain', dump.encode('utf-8')
@add_handler('pastebin-version')
@ -579,6 +582,9 @@ def qute_warning(url: QUrl) -> _HandlerRet:
title='Qt 5.15 sessions warning',
datadir=standarddir.data(),
sep=os.sep)
elif path == '/sandboxing':
src = jinja.render('warning-sandboxing.html',
title='Qt 6 macOS sandboxing warning')
else:
raise NotFoundError("Invalid warning page {}".format(path))
return 'text/html', src

View File

@ -27,7 +27,7 @@ import netrc
import tempfile
from typing import Callable, Mapping, List, Optional, Iterable, Iterator
from PyQt5.QtCore import QUrl, pyqtBoundSignal
from qutebrowser.qt.core import QUrl, pyqtBoundSignal
from qutebrowser.config import config
from qutebrowser.utils import (usertypes, message, log, objreg, jinja, utils,
@ -72,7 +72,7 @@ def authentication_required(url, authenticator, abort_on):
else:
msg = '<b>{}</b> needs authentication'.format(
html.escape(url.toDisplayString()))
urlstr = url.toString(QUrl.RemovePassword | QUrl.FullyEncoded)
urlstr = url.toString(QUrl.UrlFormattingOption.RemovePassword | QUrl.ComponentFormattingOption.FullyEncoded)
answer = message.ask(title="Authentication required", text=msg,
mode=usertypes.PromptMode.user_pwd,
abort_on=abort_on, url=urlstr)
@ -95,7 +95,7 @@ def javascript_confirm(url, js_msg, abort_on):
msg = 'From <b>{}</b>:<br/>{}'.format(html.escape(url.toDisplayString()),
_format_msg(js_msg))
urlstr = url.toString(QUrl.RemovePassword | QUrl.FullyEncoded)
urlstr = url.toString(QUrl.UrlFormattingOption.RemovePassword | QUrl.ComponentFormattingOption.FullyEncoded)
ans = message.ask('Javascript confirm', msg,
mode=usertypes.PromptMode.yesno,
abort_on=abort_on, url=urlstr)
@ -112,7 +112,7 @@ def javascript_prompt(url, js_msg, default, abort_on):
msg = '<b>{}</b> asks:<br/>{}'.format(html.escape(url.toDisplayString()),
_format_msg(js_msg))
urlstr = url.toString(QUrl.RemovePassword | QUrl.FullyEncoded)
urlstr = url.toString(QUrl.UrlFormattingOption.RemovePassword | QUrl.ComponentFormattingOption.FullyEncoded)
answer = message.ask('Javascript prompt', msg,
mode=usertypes.PromptMode.text,
default=default,
@ -135,7 +135,7 @@ def javascript_alert(url, js_msg, abort_on):
msg = 'From <b>{}</b>:<br/>{}'.format(html.escape(url.toDisplayString()),
_format_msg(js_msg))
urlstr = url.toString(QUrl.RemovePassword | QUrl.FullyEncoded)
urlstr = url.toString(QUrl.UrlFormattingOption.RemovePassword | QUrl.ComponentFormattingOption.FullyEncoded)
message.ask('Javascript alert', msg, mode=usertypes.PromptMode.alert,
abort_on=abort_on, url=urlstr)
@ -205,13 +205,13 @@ def javascript_log_message(
logger(logstring)
def ignore_certificate_error(
def handle_certificate_error(
*,
request_url: QUrl,
first_party_url: QUrl,
error: usertypes.AbstractCertificateErrorWrapper,
abort_on: Iterable[pyqtBoundSignal],
) -> bool:
) -> None:
"""Display a certificate error question.
Args:
@ -219,9 +219,6 @@ def ignore_certificate_error(
first_party_url: The URL of the page we're visiting. Might be an invalid QUrl.
error: A single error.
abort_on: Signals aborting a question.
Return:
True if the error should be ignored, False otherwise.
"""
conf = config.instance.get('content.tls.certificate_errors', url=request_url)
log.network.debug(f"Certificate error {error!r}, config {conf}")
@ -234,7 +231,7 @@ def ignore_certificate_error(
first_party_url.isValid() and
not request_url.matches(
first_party_url,
QUrl.RemoveScheme)) # type: ignore[arg-type]
QUrl.UrlFormattingOption.RemoveScheme)) # type: ignore[arg-type]
if conf == 'ask' or conf == 'ask-block-thirdparty' and not is_resource:
err_template = jinja.environment.from_string("""
@ -263,28 +260,46 @@ def ignore_certificate_error(
is_resource=is_resource,
error=error,
)
urlstr = request_url.toString(
QUrl.RemovePassword | QUrl.FullyEncoded) # type: ignore[arg-type]
ignore = message.ask(title="Certificate error", text=msg,
mode=usertypes.PromptMode.yesno, default=False,
abort_on=abort_on, url=urlstr)
if ignore is None:
# prompt aborted
ignore = False
return ignore
QUrl.UrlFormattingOption.RemovePassword | QUrl.ComponentFormattingOption.FullyEncoded) # type: ignore[arg-type]
title = "Certificate error"
try:
error.defer()
except usertypes.UndeferrableError:
# QtNetwork / QtWebKit and buggy PyQt versions
# Show blocking question prompt
ignore = message.ask(title=title, text=msg,
mode=usertypes.PromptMode.yesno, default=False,
abort_on=abort_on, url=urlstr)
if ignore:
error.accept_certificate()
else: # includes None, i.e. prompt aborted
error.reject_certificate()
else:
# Show non-blocking question prompt
message.confirm_async(
title=title,
text=msg,
abort_on=abort_on,
url=urlstr,
yes_action=error.accept_certificate,
no_action=error.reject_certificate,
cancel_action=error.reject_certificate,
)
elif conf == 'load-insecurely':
message.error(f'Certificate error: {error}')
return True
error.accept_certificate()
elif conf == 'block':
return False
error.reject_certificate()
elif conf == 'ask-block-thirdparty' and is_resource:
log.network.error(
f"Certificate error in resource load: {error}\n"
f" request URL: {request_url.toDisplayString()}\n"
f" first party URL: {first_party_url.toDisplayString()}")
return False
raise utils.Unreachable(conf, is_resource)
error.reject_certificate()
else:
raise utils.Unreachable(conf, is_resource)
def feature_permission(url, option, msg, yes_action, no_action, abort_on,
@ -307,7 +322,7 @@ def feature_permission(url, option, msg, yes_action, no_action, abort_on,
config_val = config.instance.get(option, url=url)
if config_val == 'ask':
if url.isValid():
urlstr = url.toString(QUrl.RemovePassword | QUrl.FullyEncoded)
urlstr = url.toString(QUrl.UrlFormattingOption.RemovePassword | QUrl.ComponentFormattingOption.FullyEncoded)
text = "Allow the website at <b>{}</b> to {}?".format(
html.escape(url.toDisplayString()), msg)
else:
@ -345,23 +360,19 @@ def get_tab(win_id, target):
win_id: The window ID to open new tabs in
target: A usertypes.ClickTarget
"""
if target == usertypes.ClickTarget.tab:
bg_tab = False
elif target == usertypes.ClickTarget.tab_bg:
bg_tab = True
elif target == usertypes.ClickTarget.window:
tabbed_browser = objreg.get('tabbed-browser', scope='window',
window=win_id)
tabbed_browser = objreg.get('tabbed-browser', scope='window', window=win_id)
if target == usertypes.ClickTarget.window:
window = mainwindow.MainWindow(private=tabbed_browser.is_private)
tab = window.tabbed_browser.tabopen(url=None, background=False)
window.show()
win_id = window.win_id
bg_tab = False
else:
raise ValueError("Invalid ClickTarget {}".format(target))
return tab
elif target in [usertypes.ClickTarget.tab, usertypes.ClickTarget.tab_bg]:
return tabbed_browser.tabopen(
url=None,
background=target == usertypes.ClickTarget.tab_bg,
)
tabbed_browser = objreg.get('tabbed-browser', scope='window',
window=win_id)
return tabbed_browser.tabopen(url=None, background=bg_tab)
raise ValueError(f"Invalid ClickTarget {target}")
def get_user_stylesheet(searching=False):
@ -381,7 +392,7 @@ def get_user_stylesheet(searching=False):
css += '\nhtml > ::-webkit-scrollbar { width: 0px; height: 0px; }'
if (objects.backend == usertypes.Backend.QtWebEngine and
version.qtwebengine_versions().chromium_major in [69, 73, 80, 87] and
version.qtwebengine_versions().chromium_major in [87, 90] and
config.val.colors.webpage.darkmode.enabled and
config.val.colors.webpage.darkmode.policy.images == 'smart' and
config.val.content.site_specific_quirks.enabled and

View File

@ -21,7 +21,7 @@
import functools
from PyQt5.QtCore import QObject
from qutebrowser.qt.core import QObject
from qutebrowser.utils import debug, log, objreg

View File

@ -32,7 +32,7 @@ import functools
import collections
from typing import MutableMapping
from PyQt5.QtCore import pyqtSignal, QUrl, QObject
from qutebrowser.qt.core import pyqtSignal, QUrl, QObject
from qutebrowser.utils import (message, usertypes, qtutils, urlutils,
standarddir, objreg, log)
@ -151,7 +151,7 @@ class QuickmarkManager(UrlMarkManager):
if not url.isValid():
urlutils.invalid_url_error(url, "save quickmark")
return
urlstr = url.toString(QUrl.RemovePassword | QUrl.FullyEncoded)
urlstr = url.toString(QUrl.UrlFormattingOption.RemovePassword | QUrl.ComponentFormattingOption.FullyEncoded)
message.ask_async(
"Add quickmark:", usertypes.PromptMode.text,
functools.partial(self.quickmark_add, urlstr),
@ -198,7 +198,7 @@ class QuickmarkManager(UrlMarkManager):
Use a name instead where possible.
"""
qtutils.ensure_valid(url)
urlstr = url.toString(QUrl.RemovePassword | QUrl.FullyEncoded)
urlstr = url.toString(QUrl.UrlFormattingOption.RemovePassword | QUrl.ComponentFormattingOption.FullyEncoded)
try:
index = list(self.marks.values()).index(urlstr)
@ -270,7 +270,7 @@ class BookmarkManager(UrlMarkManager):
errstr = urlutils.get_errstring(url)
raise InvalidUrlError(errstr)
urlstr = url.toString(QUrl.RemovePassword | QUrl.FullyEncoded)
urlstr = url.toString(QUrl.UrlFormattingOption.RemovePassword | QUrl.ComponentFormattingOption.FullyEncoded)
if urlstr in self.marks:
if toggle:

View File

@ -19,11 +19,12 @@
"""Generic web element related code."""
from typing import cast, TYPE_CHECKING, Iterator, Optional, Set, Union, Dict
from typing import Iterator, Optional, Set, TYPE_CHECKING, Union, Dict
import collections.abc
from PyQt5.QtCore import QUrl, Qt, QEvent, QTimer, QRect, QPoint
from PyQt5.QtGui import QMouseEvent
from qutebrowser.qt import machinery
from qutebrowser.qt.core import QUrl, Qt, QEvent, QTimer, QRect, QPointF
from qutebrowser.qt.gui import QMouseEvent
from qutebrowser.config import config
from qutebrowser.keyinput import modeman
@ -35,6 +36,11 @@ if TYPE_CHECKING:
JsValueType = Union[int, float, str, None]
if machinery.IS_QT6:
KeybordModifierType = Qt.KeyboardModifier
else:
KeybordModifierType = Union[Qt.KeyboardModifiers, Qt.KeyboardModifier]
class Error(Exception):
@ -317,7 +323,7 @@ class AbstractWebElement(collections.abc.MutableMapping): # type: ignore[type-a
"""Return True if clicking this element needs user interaction."""
raise NotImplementedError
def _mouse_pos(self) -> QPoint:
def _mouse_pos(self) -> QPointF:
"""Get the position to click/hover."""
# Click the center of the largest square fitting into the top/left
# corner of the rectangle, this will help if part of the <a> element
@ -331,38 +337,37 @@ class AbstractWebElement(collections.abc.MutableMapping): # type: ignore[type-a
pos = rect.center()
if pos.x() < 0 or pos.y() < 0:
raise Error("Element position is out of view!")
return pos
return QPointF(pos)
def _move_text_cursor(self) -> None:
"""Move cursor to end after clicking."""
raise NotImplementedError
def _click_fake_event(self, click_target: usertypes.ClickTarget,
button: Qt.MouseButton = Qt.LeftButton) -> None:
button: Qt.MouseButton = Qt.MouseButton.LeftButton) -> None:
"""Send a fake click event to the element."""
pos = self._mouse_pos()
log.webelem.debug("Sending fake click to {!r} at position {} with "
"target {}".format(self, pos, click_target))
target_modifiers: Dict[usertypes.ClickTarget, Qt.KeyboardModifiers] = {
usertypes.ClickTarget.normal: cast(Qt.KeyboardModifiers, Qt.NoModifier),
usertypes.ClickTarget.window: Qt.AltModifier | Qt.ShiftModifier,
usertypes.ClickTarget.tab: cast(Qt.KeyboardModifiers, Qt.ControlModifier),
usertypes.ClickTarget.tab_bg:
cast(Qt.KeyboardModifiers, Qt.ControlModifier),
target_modifiers: Dict[usertypes.ClickTarget, KeybordModifierType] = {
usertypes.ClickTarget.normal: Qt.KeyboardModifier.NoModifier,
usertypes.ClickTarget.window: Qt.KeyboardModifier.AltModifier | Qt.KeyboardModifier.ShiftModifier,
usertypes.ClickTarget.tab: Qt.KeyboardModifier.ControlModifier,
usertypes.ClickTarget.tab_bg: Qt.KeyboardModifier.ControlModifier,
}
if config.val.tabs.background:
target_modifiers[usertypes.ClickTarget.tab] |= Qt.ShiftModifier
target_modifiers[usertypes.ClickTarget.tab] |= Qt.KeyboardModifier.ShiftModifier
else:
target_modifiers[usertypes.ClickTarget.tab_bg] |= Qt.ShiftModifier
target_modifiers[usertypes.ClickTarget.tab_bg] |= Qt.KeyboardModifier.ShiftModifier
modifiers = target_modifiers[click_target]
events = [
QMouseEvent(QEvent.MouseMove, pos, Qt.NoButton, Qt.NoButton, Qt.NoModifier),
QMouseEvent(QEvent.MouseButtonPress, pos, button, button, modifiers),
QMouseEvent(QEvent.MouseButtonRelease, pos, button, Qt.NoButton, modifiers),
QMouseEvent(QEvent.Type.MouseMove, pos, Qt.MouseButton.NoButton, Qt.MouseButton.NoButton, Qt.KeyboardModifier.NoModifier),
QMouseEvent(QEvent.Type.MouseButtonPress, pos, button, button, modifiers),
QMouseEvent(QEvent.Type.MouseButtonRelease, pos, button, Qt.MouseButton.NoButton, modifiers),
]
for evt in events:
@ -400,8 +405,8 @@ class AbstractWebElement(collections.abc.MutableMapping): # type: ignore[type-a
elif click_target == usertypes.ClickTarget.window:
from qutebrowser.mainwindow import mainwindow
window = mainwindow.MainWindow(private=tabbed_browser.is_private)
window.show()
window.tabbed_browser.tabopen(url)
window.show()
else:
raise ValueError("Unknown ClickTarget {}".format(click_target))
@ -446,11 +451,11 @@ class AbstractWebElement(collections.abc.MutableMapping): # type: ignore[type-a
def hover(self) -> None:
"""Simulate a mouse hover over the element."""
pos = self._mouse_pos()
event = QMouseEvent(QEvent.MouseMove, pos, Qt.NoButton, Qt.NoButton,
Qt.NoModifier)
event = QMouseEvent(QEvent.Type.MouseMove, pos, Qt.MouseButton.NoButton, Qt.MouseButton.NoButton,
Qt.KeyboardModifier.NoModifier)
self._tab.send_event(event)
def right_click(self) -> None:
"""Simulate a right-click on the element."""
self._click_fake_event(usertypes.ClickTarget.normal,
button=Qt.RightButton)
button=Qt.MouseButton.RightButton)

View File

@ -19,27 +19,54 @@
"""Wrapper over a QWebEngineCertificateError."""
from PyQt5.QtCore import QUrl
from PyQt5.QtWebEngineWidgets import QWebEngineCertificateError
from typing import Any
from qutebrowser.qt import machinery
from qutebrowser.qt.core import QUrl
from qutebrowser.qt.webenginecore import QWebEngineCertificateError
from qutebrowser.utils import usertypes, utils, debug
class CertificateErrorWrapper(usertypes.AbstractCertificateErrorWrapper):
"""A wrapper over a QWebEngineCertificateError."""
"""A wrapper over a QWebEngineCertificateError.
Support both Qt 5 and 6.
"""
def __init__(self, error: QWebEngineCertificateError) -> None:
super().__init__()
self._error = error
self.ignore = False
def __str__(self) -> str:
return self._error.errorDescription()
if machinery.IS_QT5:
return self._error.errorDescription()
else:
return self._error.description()
def _type(self) -> Any: # QWebEngineCertificateError.Type or .Error
if machinery.IS_QT5:
return self._error.error()
else:
return self._error.type()
def reject_certificate(self) -> None:
super().reject_certificate()
self._error.rejectCertificate()
def accept_certificate(self) -> None:
super().accept_certificate()
if machinery.IS_QT5:
self._error.ignoreCertificateError()
else:
self._error.acceptCertificate()
def __repr__(self) -> str:
return utils.get_repr(
self,
error=debug.qenum_key(QWebEngineCertificateError, self._error.error()),
error=debug.qenum_key(QWebEngineCertificateError, self._type()),
string=str(self))
def url(self) -> QUrl:
@ -47,3 +74,8 @@ class CertificateErrorWrapper(usertypes.AbstractCertificateErrorWrapper):
def is_overridable(self) -> bool:
return self._error.isOverridable()
def defer(self) -> None:
# WORKAROUND for https://www.riverbankcomputing.com/pipermail/pyqt/2022-April/044585.html
# (PyQt 5.15.6, 6.2.3, 6.3.0)
raise usertypes.UndeferrableError("PyQt bug")

View File

@ -21,8 +21,8 @@
Overview of blink setting names based on the Qt version:
Qt 5.10
-------
Qt 5.10 (UNSUPPORTED)
---------------------
First implementation, called "high contrast mode".
@ -31,16 +31,16 @@ First implementation, called "high contrast mode".
- highContrastContrast (float)
- highContractImagePolicy (kFilterAll/kFilterNone)
Qt 5.11, 5.12, 5.13
-------------------
Qt 5.11, 5.12, 5.13 (UNSUPPORTED)
---------------------------------
New "smart" image policy.
- Mode/Grayscale/Contrast as above
- highContractImagePolicy (kFilterAll/kFilterNone/kFilterSmart [new!])
Qt 5.14
-------
Qt 5.14 (UNSUPPORTED)
---------------------
Renamed to "darkMode".
@ -54,8 +54,8 @@ Renamed to "darkMode".
- darkModeBackgroundBrightnessThreshold (int) [new!]
- darkModeImageGrayscale (float) [new!]
Qt 5.15.0 and 5.15.1
--------------------
Qt 5.15.0 and 5.15.1 (UNSUPPORTED)
----------------------------------
"darkMode" split into "darkModeEnabled" and "darkModeInversionAlgorithm".
@ -90,6 +90,17 @@ https://chromium-review.googlesource.com/c/chromium/src/+/2232922
- Now needs to be 0 for dark and 1 for light
(before: 0 no preference / 1 dark / 2 light)
Qt 6.2
------
No significant changes over 5.15.3
Qt 6.3
------
- New IncreaseTextContrast:
https://chromium-review.googlesource.com/c/chromium/src/+/2893236
"""
import os
@ -111,12 +122,9 @@ class Variant(enum.Enum):
"""A dark mode variant."""
qt_511_to_513 = enum.auto()
qt_514 = enum.auto()
qt_515_0 = enum.auto()
qt_515_1 = enum.auto()
qt_515_2 = enum.auto()
qt_515_3 = enum.auto()
qt_63 = enum.auto()
# Mapping from a colors.webpage.darkmode.algorithm setting value to
@ -128,9 +136,6 @@ _ALGORITHMS = {
'lightness-hsl': 3, # kInvertLightness
'lightness-cielab': 4, # kInvertLightnessLAB
}
# kInvertLightnessLAB is not available with Qt < 5.14
_ALGORITHMS_BEFORE_QT_514 = _ALGORITHMS.copy()
_ALGORITHMS_BEFORE_QT_514['lightness-cielab'] = _ALGORITHMS['lightness-hsl']
# Qt >= 5.15.3, based on dark_mode_settings.h
_ALGORITHMS_NEW = {
# 0: kSimpleInvertForTesting (not exposed)
@ -160,6 +165,11 @@ _BOOLS = {
False: 'false',
}
_INT_BOOLS = {
True: '1',
False: '0',
}
@dataclasses.dataclass
class _Setting:
@ -229,18 +239,40 @@ class _Definition:
"""Get a new _Definition object with a changed attribute.
NOTE: This does *not* copy the settings list. Both objects will reference the
same list.
same (immutable) tuple.
"""
new = copy.copy(self)
setattr(new, attr, value)
return new
def copy_add_setting(self, setting: _Setting) -> '_Definition':
"""Get a new _Definition object with an additional setting."""
new = copy.copy(self)
new._settings = self._settings + (setting,) # pylint: disable=protected-access
return new
# Our defaults for policy.images are different from Chromium's, so we mark it as
# mandatory setting - except on Qt 5.15.0 where we don't, so we don't get the
# workaround warning below if the setting wasn't explicitly customized.
# mandatory setting.
_DEFINITIONS: MutableMapping[Variant, _Definition] = {
Variant.qt_515_2: _Definition(
_Setting('enabled', 'Enabled', _BOOLS),
_Setting('algorithm', 'InversionAlgorithm', _ALGORITHMS),
_Setting('policy.images', 'ImagePolicy', _IMAGE_POLICIES),
_Setting('contrast', 'Contrast'),
_Setting('grayscale.all', 'Grayscale', _BOOLS),
_Setting('policy.page', 'PagePolicy', _PAGE_POLICIES),
_Setting('threshold.text', 'TextBrightnessThreshold'),
_Setting('threshold.background', 'BackgroundBrightnessThreshold'),
_Setting('grayscale.images', 'ImageGrayscale'),
mandatory={'enabled', 'policy.images'},
prefix='forceDarkMode',
),
Variant.qt_515_3: _Definition(
# Different switch for settings
_Setting('enabled', 'forceDarkModeEnabled', _BOOLS),
@ -258,77 +290,34 @@ _DEFINITIONS: MutableMapping[Variant, _Definition] = {
prefix='',
switch_names={'enabled': _BLINK_SETTINGS, None: 'dark-mode-settings'},
),
# Qt 5.15.1 and 5.15.2 get added below, since there are only minor differences.
Variant.qt_515_0: _Definition(
# 'policy.images' not mandatory because it's broken
_Setting('enabled', 'Enabled', _BOOLS),
_Setting('algorithm', 'InversionAlgorithm', _ALGORITHMS),
_Setting('policy.images', 'ImagePolicy', _IMAGE_POLICIES),
_Setting('contrast', 'Contrast'),
_Setting('grayscale.all', 'Grayscale', _BOOLS),
_Setting('policy.page', 'PagePolicy', _PAGE_POLICIES),
_Setting('threshold.text', 'TextBrightnessThreshold'),
_Setting('threshold.background', 'BackgroundBrightnessThreshold'),
_Setting('grayscale.images', 'ImageGrayscale'),
mandatory={'enabled'},
prefix='darkMode',
),
Variant.qt_514: _Definition(
_Setting('algorithm', '', _ALGORITHMS), # new: kInvertLightnessLAB
_Setting('policy.images', 'ImagePolicy', _IMAGE_POLICIES),
_Setting('contrast', 'Contrast'),
_Setting('grayscale.all', 'Grayscale', _BOOLS),
_Setting('policy.page', 'PagePolicy', _PAGE_POLICIES),
_Setting('threshold.text', 'TextBrightnessThreshold'),
_Setting('threshold.background', 'BackgroundBrightnessThreshold'),
_Setting('grayscale.images', 'ImageGrayscale'),
mandatory={'algorithm', 'policy.images'},
prefix='darkMode',
),
Variant.qt_511_to_513: _Definition(
_Setting('algorithm', 'Mode', _ALGORITHMS_BEFORE_QT_514),
_Setting('policy.images', 'ImagePolicy', _IMAGE_POLICIES),
_Setting('contrast', 'Contrast'),
_Setting('grayscale.all', 'Grayscale', _BOOLS),
mandatory={'algorithm', 'policy.images'},
prefix='highContrast',
),
}
_DEFINITIONS[Variant.qt_515_1] = (
_DEFINITIONS[Variant.qt_515_0].copy_with('mandatory', {'enabled', 'policy.images'}))
_DEFINITIONS[Variant.qt_515_2] = (
_DEFINITIONS[Variant.qt_515_1].copy_with('prefix', 'forceDarkMode'))
_DEFINITIONS[Variant.qt_63] = _DEFINITIONS[Variant.qt_515_3].copy_add_setting(
_Setting('increase_text_contrast', 'IncreaseTextContrast', _INT_BOOLS),
)
_PREFERRED_COLOR_SCHEME_DEFINITIONS = {
# With older Qt versions, this is passed in qtargs.py as --force-dark-mode
# instead.
_SettingValType = Union[str, usertypes.Unset]
_PREFERRED_COLOR_SCHEME_DEFINITIONS: Mapping[Variant, Mapping[_SettingValType, str]] = {
Variant.qt_515_2: {
# 0: no-preference (not exposed)
"dark": "1",
"light": "2",
# WORKAROUND for https://bugreports.qt.io/browse/QTBUG-89753
# Fall back to "light" instead of "no preference" (which was removed from the
# standard)
"auto": "2",
usertypes.UNSET: "2",
},
## Qt 5.15.2
# 0: no-preference (not exposed)
(Variant.qt_515_2, "dark"): "1",
(Variant.qt_515_2, "light"): "2",
# WORKAROUND for https://bugreports.qt.io/browse/QTBUG-89753
# Fall back to "light" instead of "no preference" (which was removed from the
# standard)
(Variant.qt_515_2, "auto"): "2",
(Variant.qt_515_2, usertypes.UNSET): "2",
Variant.qt_515_3: {
"dark": "0",
"light": "1",
},
## Qt >= 5.15.3
(Variant.qt_515_3, "dark"): "0",
(Variant.qt_515_3, "light"): "1",
Variant.qt_63: {
"dark": "0",
"light": "1",
}
}
@ -341,7 +330,9 @@ def _variant(versions: version.WebEngineVersions) -> Variant:
except KeyError:
log.init.warning(f"Ignoring invalid QUTE_DARKMODE_VARIANT={env_var}")
if (versions.webengine == utils.VersionNumber(5, 15, 2) and
if versions.webengine >= utils.VersionNumber(6, 3):
return Variant.qt_63
elif (versions.webengine == utils.VersionNumber(5, 15, 2) and
versions.chromium_major == 87):
# WORKAROUND for Gentoo packaging something newer as 5.15.2...
return Variant.qt_515_3
@ -349,14 +340,6 @@ def _variant(versions: version.WebEngineVersions) -> Variant:
return Variant.qt_515_3
elif versions.webengine >= utils.VersionNumber(5, 15, 2):
return Variant.qt_515_2
elif versions.webengine == utils.VersionNumber(5, 15, 1):
return Variant.qt_515_1
elif versions.webengine == utils.VersionNumber(5, 15):
return Variant.qt_515_0
elif versions.webengine >= utils.VersionNumber(5, 14):
return Variant.qt_514
elif versions.webengine >= utils.VersionNumber(5, 11):
return Variant.qt_511_to_513
raise utils.Unreachable(versions.webengine)
@ -387,12 +370,11 @@ def settings(
key, val = pair.split('=', maxsplit=1)
result[_BLINK_SETTINGS].append((key, val))
preferred_color_scheme_key = (
variant,
config.instance.get("colors.webpage.preferred_color_scheme", fallback=False),
)
if preferred_color_scheme_key in _PREFERRED_COLOR_SCHEME_DEFINITIONS:
value = _PREFERRED_COLOR_SCHEME_DEFINITIONS[preferred_color_scheme_key]
preferred_color_scheme_key = config.instance.get(
"colors.webpage.preferred_color_scheme", fallback=False)
preferred_color_scheme_defs = _PREFERRED_COLOR_SCHEME_DEFINITIONS[variant]
if preferred_color_scheme_key in preferred_color_scheme_defs:
value = preferred_color_scheme_defs[preferred_color_scheme_key]
result[_BLINK_SETTINGS].append(("preferredColorScheme", value))
if not config.val.colors.webpage.darkmode.enabled:
@ -411,14 +393,6 @@ def settings(
if isinstance(value, usertypes.Unset):
continue
if (setting.option == 'policy.images' and value == 'smart' and
variant == Variant.qt_515_0):
# WORKAROUND for
# https://codereview.qt-project.org/c/qt/qtwebengine-chromium/+/304211
log.init.warning("Ignoring colors.webpage.darkmode.policy.images = smart "
"because of Qt 5.15.0 bug")
continue
result[switch_name].append(setting.chromium_tuple(value))
return result

View File

@ -19,13 +19,13 @@
"""A request interceptor taking care of adblocking and custom headers."""
from PyQt5.QtCore import QUrl, QByteArray
from PyQt5.QtWebEngineCore import (QWebEngineUrlRequestInterceptor,
from qutebrowser.qt.core import QUrl, QByteArray
from qutebrowser.qt.webenginecore import (QWebEngineUrlRequestInterceptor,
QWebEngineUrlRequestInfo)
from qutebrowser.config import websettings, config
from qutebrowser.browser import shared
from qutebrowser.utils import utils, log, debug, qtutils
from qutebrowser.utils import debug, log
from qutebrowser.extensions import interceptors
from qutebrowser.misc import objects
@ -71,80 +71,71 @@ class RequestInterceptor(QWebEngineUrlRequestInterceptor):
# extension ResourceTypes. If a ResourceType is added to Qt, this table
# should be updated too.
self._resource_types = {
QWebEngineUrlRequestInfo.ResourceTypeMainFrame:
QWebEngineUrlRequestInfo.ResourceType.ResourceTypeMainFrame:
interceptors.ResourceType.main_frame,
QWebEngineUrlRequestInfo.ResourceTypeSubFrame:
QWebEngineUrlRequestInfo.ResourceType.ResourceTypeSubFrame:
interceptors.ResourceType.sub_frame,
QWebEngineUrlRequestInfo.ResourceTypeStylesheet:
QWebEngineUrlRequestInfo.ResourceType.ResourceTypeStylesheet:
interceptors.ResourceType.stylesheet,
QWebEngineUrlRequestInfo.ResourceTypeScript:
QWebEngineUrlRequestInfo.ResourceType.ResourceTypeScript:
interceptors.ResourceType.script,
QWebEngineUrlRequestInfo.ResourceTypeImage:
QWebEngineUrlRequestInfo.ResourceType.ResourceTypeImage:
interceptors.ResourceType.image,
QWebEngineUrlRequestInfo.ResourceTypeFontResource:
QWebEngineUrlRequestInfo.ResourceType.ResourceTypeFontResource:
interceptors.ResourceType.font_resource,
QWebEngineUrlRequestInfo.ResourceTypeSubResource:
QWebEngineUrlRequestInfo.ResourceType.ResourceTypeSubResource:
interceptors.ResourceType.sub_resource,
QWebEngineUrlRequestInfo.ResourceTypeObject:
QWebEngineUrlRequestInfo.ResourceType.ResourceTypeObject:
interceptors.ResourceType.object,
QWebEngineUrlRequestInfo.ResourceTypeMedia:
QWebEngineUrlRequestInfo.ResourceType.ResourceTypeMedia:
interceptors.ResourceType.media,
QWebEngineUrlRequestInfo.ResourceTypeWorker:
QWebEngineUrlRequestInfo.ResourceType.ResourceTypeWorker:
interceptors.ResourceType.worker,
QWebEngineUrlRequestInfo.ResourceTypeSharedWorker:
QWebEngineUrlRequestInfo.ResourceType.ResourceTypeSharedWorker:
interceptors.ResourceType.shared_worker,
QWebEngineUrlRequestInfo.ResourceTypePrefetch:
QWebEngineUrlRequestInfo.ResourceType.ResourceTypePrefetch:
interceptors.ResourceType.prefetch,
QWebEngineUrlRequestInfo.ResourceTypeFavicon:
QWebEngineUrlRequestInfo.ResourceType.ResourceTypeFavicon:
interceptors.ResourceType.favicon,
QWebEngineUrlRequestInfo.ResourceTypeXhr:
QWebEngineUrlRequestInfo.ResourceType.ResourceTypeXhr:
interceptors.ResourceType.xhr,
QWebEngineUrlRequestInfo.ResourceTypePing:
QWebEngineUrlRequestInfo.ResourceType.ResourceTypePing:
interceptors.ResourceType.ping,
QWebEngineUrlRequestInfo.ResourceTypeServiceWorker:
QWebEngineUrlRequestInfo.ResourceType.ResourceTypeServiceWorker:
interceptors.ResourceType.service_worker,
QWebEngineUrlRequestInfo.ResourceTypeCspReport:
QWebEngineUrlRequestInfo.ResourceType.ResourceTypeCspReport:
interceptors.ResourceType.csp_report,
QWebEngineUrlRequestInfo.ResourceTypePluginResource:
QWebEngineUrlRequestInfo.ResourceType.ResourceTypePluginResource:
interceptors.ResourceType.plugin_resource,
QWebEngineUrlRequestInfo.ResourceTypeUnknown:
QWebEngineUrlRequestInfo.ResourceType.ResourceTypeUnknown:
interceptors.ResourceType.unknown,
QWebEngineUrlRequestInfo.ResourceType.ResourceTypeNavigationPreloadMainFrame:
interceptors.ResourceType.preload_main_frame,
QWebEngineUrlRequestInfo.ResourceType.ResourceTypeNavigationPreloadSubFrame:
interceptors.ResourceType.preload_sub_frame,
}
try:
preload_main_frame = (QWebEngineUrlRequestInfo.
ResourceTypeNavigationPreloadMainFrame)
preload_sub_frame = (QWebEngineUrlRequestInfo.
ResourceTypeNavigationPreloadSubFrame)
except AttributeError:
# Added in Qt 5.14
pass
else:
self._resource_types[preload_main_frame] = (
interceptors.ResourceType.preload_main_frame)
self._resource_types[preload_sub_frame] = (
interceptors.ResourceType.preload_sub_frame)
new_types = {
"WebSocket": interceptors.ResourceType.websocket, # added in Qt 6.4
}
for qt_name, qb_value in new_types.items():
qt_value = getattr(
QWebEngineUrlRequestInfo.ResourceType,
f"ResourceType{qt_name}",
None,
)
if qt_value is not None:
self._resource_types[qt_value] = qb_value
def install(self, profile):
"""Install the interceptor on the given QWebEngineProfile."""
try:
# Qt >= 5.13, GUI thread
profile.setUrlRequestInterceptor(self)
except AttributeError:
# Qt 5.12, IO thread
profile.setRequestInterceptor(self)
profile.setUrlRequestInterceptor(self)
# Gets called in the IO thread -> showing crash window will fail
@utils.prevent_exceptions(None, not qtutils.version_check('5.13'))
def interceptRequest(self, info):
"""Handle the given request.
Reimplementing this virtual function and setting the interceptor on a
profile makes it possible to intercept URL requests.
On Qt < 5.13, this function is executed on the IO thread, and therefore
running long tasks here will block networking.
info contains the information about the URL request and will track
internally whether its members have been altered.
@ -181,7 +172,7 @@ class RequestInterceptor(QWebEngineUrlRequestInterceptor):
info.resourceType())))
resource_type = interceptors.ResourceType.unknown
is_xhr = info.resourceType() == QWebEngineUrlRequestInfo.ResourceTypeXhr
is_xhr = info.resourceType() == QWebEngineUrlRequestInfo.ResourceType.ResourceTypeXhr
if ((url.scheme(), url.host(), url.path()) ==
('qute', 'settings', '/set')):
@ -214,9 +205,6 @@ class RequestInterceptor(QWebEngineUrlRequestInterceptor):
continue
info.setHttpHeader(header, value)
# Note this is ignored before Qt 5.12.4 and 5.13.1 due to
# https://bugreports.qt.io/browse/QTBUG-60203 - there, we set the
# commandline-flag in qtargs.py instead.
if config.cache['content.headers.referer'] == 'never':
info.setHttpHeader(b'Referer', b'')

View File

@ -50,23 +50,24 @@ import functools
import subprocess
from typing import Any, List, Dict, Optional, Iterator, Type, TYPE_CHECKING
from PyQt5.QtCore import (Qt, QObject, QVariant, QMetaType, QByteArray, pyqtSlot,
from qutebrowser.qt import machinery
from qutebrowser.qt.core import (Qt, QObject, QVariant, QMetaType, QByteArray, pyqtSlot,
pyqtSignal, QTimer, QProcess, QUrl)
from PyQt5.QtGui import QImage, QIcon, QPixmap
from PyQt5.QtDBus import (QDBusConnection, QDBusInterface, QDBus, QDBusServiceWatcher,
from qutebrowser.qt.gui import QImage, QIcon, QPixmap
from qutebrowser.qt.dbus import (QDBusConnection, QDBusInterface, QDBus, QDBusServiceWatcher,
QDBusArgument, QDBusMessage, QDBusError)
from PyQt5.QtWidgets import QSystemTrayIcon
from qutebrowser.qt.widgets import QSystemTrayIcon
if TYPE_CHECKING:
# putting these behind TYPE_CHECKING also means this module is importable
# on installs that don't have these
from PyQt5.QtWebEngineCore import QWebEngineNotification
from PyQt5.QtWebEngineWidgets import QWebEngineProfile
from qutebrowser.qt.webenginecore import QWebEngineNotification
from qutebrowser.qt.webenginewidgets import QWebEngineProfile
from qutebrowser.config import config
from qutebrowser.misc import objects
from qutebrowser.utils import (
qtutils, log, utils, debug, message, version, objreg, resources,
qtutils, log, utils, debug, message, objreg, resources,
)
from qutebrowser.qt import sip
@ -74,12 +75,6 @@ from qutebrowser.qt import sip
bridge: Optional['NotificationBridgePresenter'] = None
def _notifications_supported() -> bool:
"""Check whether the current QtWebEngine version has notification support."""
versions = version.qtwebengine_versions(avoid_init=True)
return versions.webengine >= utils.VersionNumber(5, 14)
def init() -> None:
"""Initialize the DBus notification presenter, if applicable.
@ -94,9 +89,6 @@ def init() -> None:
# to its usefulness.
return
if not _notifications_supported():
return
global bridge
bridge = NotificationBridgePresenter()
@ -140,7 +132,7 @@ class DBusError(Error):
}
def __init__(self, msg: QDBusMessage) -> None:
assert msg.type() == QDBusMessage.ErrorMessage
assert msg.type() == QDBusMessage.MessageType.ErrorMessage
self.error = msg.errorName()
self.error_message = msg.errorMessage()
self.is_fatal = self.error not in self._NON_FATAL_ERRORS
@ -208,7 +200,6 @@ class NotificationBridgePresenter(QObject):
"""Notification presenter which bridges notifications to an adapter.
Takes care of:
- Working around bugs in PyQt 5.14
- Storing currently shown notifications, using an ID returned by the adapter.
- Initializing a suitable adapter when the first notification is shown.
- Switching out adapters if the current one emitted its error signal.
@ -216,7 +207,6 @@ class NotificationBridgePresenter(QObject):
def __init__(self, parent: QObject = None) -> None:
super().__init__(parent)
assert _notifications_supported()
self._active_notifications: Dict[int, 'QWebEngineNotification'] = {}
self._adapter: Optional[AbstractNotificationAdapter] = None
@ -280,24 +270,7 @@ class NotificationBridgePresenter(QObject):
def install(self, profile: "QWebEngineProfile") -> None:
"""Set the profile to use this bridge as the presenter."""
# WORKAROUND for
# https://www.riverbankcomputing.com/pipermail/pyqt/2020-May/042916.html
# Fixed in PyQtWebEngine 5.15.0
# PYQT_WEBENGINE_VERSION was added with PyQtWebEngine 5.13, but if we're here,
# we already did a version check above.
from PyQt5.QtWebEngine import PYQT_WEBENGINE_VERSION
if PYQT_WEBENGINE_VERSION < 0x050F00:
# PyQtWebEngine unrefs the callback after it's called, for some
# reason. So we call setNotificationPresenter again to *increase*
# its refcount to prevent it from getting GC'd. Otherwise, random
# methods start getting called with the notification as `self`, or
# segfaults happen, or other badness.
def _present_and_reset(qt_notification: "QWebEngineNotification") -> None:
profile.setNotificationPresenter(_present_and_reset)
self.present(qt_notification)
profile.setNotificationPresenter(_present_and_reset)
else:
profile.setNotificationPresenter(self.present)
profile.setNotificationPresenter(self.present)
def present(self, qt_notification: "QWebEngineNotification") -> None:
"""Show a notification using the configured adapter.
@ -349,18 +322,11 @@ class NotificationBridgePresenter(QObject):
f"Finding notification for tag {new_notification.tag()}, "
f"origin {new_notification.origin()}")
try:
for notification_id, notification in sorted(
self._active_notifications.items(), reverse=True):
if notification.matches(new_notification):
log.misc.debug(f"Found match: {notification_id}")
return notification_id
except RuntimeError:
# WORKAROUND for
# https://www.riverbankcomputing.com/pipermail/pyqt/2020-May/042918.html
# (also affects .matches)
log.misc.debug(
f"Ignoring notification tag {new_notification.tag()!r} due to PyQt bug")
for notification_id, notification in sorted(
self._active_notifications.items(), reverse=True):
if notification.matches(new_notification):
log.misc.debug(f"Found match: {notification_id}")
return notification_id
log.misc.debug("Did not find match")
return None
@ -381,13 +347,7 @@ class NotificationBridgePresenter(QObject):
# Notification from a different application
return
try:
notification.close()
except RuntimeError:
# WORKAROUND for
# https://www.riverbankcomputing.com/pipermail/pyqt/2020-May/042918.html
log.misc.debug(f"Ignoring close request for notification {notification_id} "
"due to PyQt bug")
notification.close()
@pyqtSlot(int)
def _on_adapter_clicked(self, notification_id: int) -> None:
@ -405,21 +365,14 @@ class NotificationBridgePresenter(QObject):
log.misc.debug("Did not find matching notification, ignoring")
return
try:
notification.click()
except RuntimeError:
# WORKAROUND for
# https://www.riverbankcomputing.com/pipermail/pyqt/2020-May/042918.html
log.misc.debug(f"Ignoring click request for notification {notification_id} "
"due to PyQt bug")
return
notification.click()
self._focus_first_matching_tab(notification)
def _focus_first_matching_tab(self, notification: "QWebEngineNotification") -> None:
for win_id in objreg.window_registry:
tabbedbrowser = objreg.get("tabbed-browser", window=win_id, scope="window")
for idx, tab in enumerate(tabbedbrowser.widgets()):
if tab.url().matches(notification.origin(), QUrl.RemovePath):
if tab.url().matches(notification.origin(), QUrl.UrlFormattingOption.RemovePath):
tabbedbrowser.widget.setCurrentIndex(idx)
return
log.misc.debug(f"No matching tab found for {notification.origin()}")
@ -514,7 +467,7 @@ class SystrayNotificationAdapter(AbstractNotificationAdapter):
"""Convert a QImage to a QIcon."""
if image.isNull():
return QIcon()
pixmap = QPixmap.fromImage(image, Qt.NoFormatConversion)
pixmap = QPixmap.fromImage(image, Qt.ImageConversionFlag.NoFormatConversion)
assert not pixmap.isNull()
icon = QIcon(pixmap)
assert not icon.isNull()
@ -665,7 +618,7 @@ class HerbeNotificationAdapter(AbstractNotificationAdapter):
signals, we can't do much - emitting self.error would just go use herbe again,
so there's no point.
"""
if status == QProcess.CrashExit:
if status == QProcess.ExitStatus.CrashExit:
pass
elif code == 0:
self.click_id.emit(pid)
@ -681,7 +634,7 @@ class HerbeNotificationAdapter(AbstractNotificationAdapter):
@pyqtSlot(QProcess.ProcessError)
def _on_error(self, error: QProcess.ProcessError) -> None:
if error == QProcess.Crashed:
if error == QProcess.ProcessError.Crashed:
return
name = debug.qenum_key(QProcess.ProcessError, error)
raise Error(f'herbe process error: {name}')
@ -737,7 +690,13 @@ class _ServerCapabilities:
def _as_uint32(x: int) -> QVariant:
"""Convert the given int to an uint32 for DBus."""
variant = QVariant(x)
successful = variant.convert(QVariant.UInt)
if machinery.IS_QT5:
target_type = QVariant.Type.UInt
else: # Qt 6
target_type = QMetaType(QMetaType.Type.UInt.value)
successful = variant.convert(target_type)
assert successful
return variant
@ -763,7 +722,6 @@ class DBusNotificationAdapter(AbstractNotificationAdapter):
def __init__(self, parent: QObject = None) -> None:
super().__init__(parent)
assert _notifications_supported()
if utils.is_windows:
# The QDBusConnection destructor seems to cause error messages (and
@ -781,7 +739,7 @@ class DBusNotificationAdapter(AbstractNotificationAdapter):
self._watcher = QDBusServiceWatcher(
self.SERVICE,
bus,
QDBusServiceWatcher.WatchForUnregistration,
QDBusServiceWatcher.WatchModeFlag.WatchForUnregistration,
self,
)
self._watcher.serviceUnregistered.connect(self._on_service_unregistered)
@ -900,8 +858,8 @@ class DBusNotificationAdapter(AbstractNotificationAdapter):
def _get_server_info(self) -> None:
"""Query notification server information and set quirks."""
reply = self.interface.call(QDBus.BlockWithGui, "GetServerInformation")
self._verify_message(reply, "ssss", QDBusMessage.ReplyMessage)
reply = self.interface.call(QDBus.CallMode.BlockWithGui, "GetServerInformation")
self._verify_message(reply, "ssss", QDBusMessage.MessageType.ReplyMessage)
name, vendor, ver, spec_version = reply.arguments()
log.misc.debug(
@ -945,11 +903,11 @@ class DBusNotificationAdapter(AbstractNotificationAdapter):
Raises DBusError if the signature doesn't match.
"""
assert expected_type not in [
QDBusMessage.ErrorMessage,
QDBusMessage.InvalidMessage,
QDBusMessage.MessageType.ErrorMessage,
QDBusMessage.MessageType.InvalidMessage,
], expected_type
if msg.type() == QDBusMessage.ErrorMessage:
if msg.type() == QDBusMessage.MessageType.ErrorMessage:
raise DBusError(msg)
signature = msg.signature()
@ -997,7 +955,10 @@ class DBusNotificationAdapter(AbstractNotificationAdapter):
actions = []
if self._capabilities.actions:
actions = ['default', 'Activate'] # key, name
return QDBusArgument(actions, QMetaType.QStringList)
return QDBusArgument(
actions,
qtutils.extract_enum_val(QMetaType.Type.QStringList),
)
def _get_hints_arg(self, *, origin_url: QUrl, icon: QImage) -> Dict[str, Any]:
"""Get the hints argument for present()."""
@ -1037,7 +998,7 @@ class DBusNotificationAdapter(AbstractNotificationAdapter):
) -> Any:
"""Wrapper around DBus call to use keyword args."""
return self.interface.call(
QDBus.BlockWithGui,
QDBus.CallMode.BlockWithGui,
"Notify",
appname,
replaces_id,
@ -1077,7 +1038,7 @@ class DBusNotificationAdapter(AbstractNotificationAdapter):
)
try:
self._verify_message(reply, "u", QDBusMessage.ReplyMessage)
self._verify_message(reply, "u", QDBusMessage.MessageType.ReplyMessage)
except DBusError as e:
if e.is_fatal:
raise
@ -1097,10 +1058,10 @@ class DBusNotificationAdapter(AbstractNotificationAdapter):
bits_per_color = 8
has_alpha = qimage.hasAlphaChannel()
if has_alpha:
image_format = QImage.Format_RGBA8888
image_format = QImage.Format.Format_RGBA8888
channel_count = 4
else:
image_format = QImage.Format_RGB888
image_format = QImage.Format.Format_RGB888
channel_count = 3
qimage.convertTo(image_format)
@ -1117,14 +1078,7 @@ class DBusNotificationAdapter(AbstractNotificationAdapter):
image_data.add(bits_per_color)
image_data.add(channel_count)
try:
size = qimage.sizeInBytes()
except TypeError:
# WORKAROUND for
# https://www.riverbankcomputing.com/pipermail/pyqt/2020-May/042919.html
# byteCount() is obsolete, but sizeInBytes() is only available with
# SIP >= 5.3.0.
size = qimage.byteCount()
size = qimage.sizeInBytes()
# Despite the spec not mandating this, many notification daemons mandate that
# the last scanline does not have any padding bytes.
@ -1166,11 +1120,11 @@ class DBusNotificationAdapter(AbstractNotificationAdapter):
def _handle_close(self, msg: QDBusMessage) -> None:
"""Handle NotificationClosed from DBus."""
try:
self._verify_message(msg, "uu", QDBusMessage.SignalMessage)
self._verify_message(msg, "uu", QDBusMessage.MessageType.SignalMessage)
except Error:
if not self._quirks.wrong_closes_type:
raise
self._verify_message(msg, "ui", QDBusMessage.SignalMessage)
self._verify_message(msg, "ui", QDBusMessage.MessageType.SignalMessage)
notification_id, _close_reason = msg.arguments()
self.close_id.emit(notification_id)
@ -1178,7 +1132,7 @@ class DBusNotificationAdapter(AbstractNotificationAdapter):
@pyqtSlot(QDBusMessage)
def _handle_action(self, msg: QDBusMessage) -> None:
"""Handle ActionInvoked from DBus."""
self._verify_message(msg, "us", QDBusMessage.SignalMessage)
self._verify_message(msg, "us", QDBusMessage.MessageType.SignalMessage)
notification_id, action_key = msg.arguments()
if action_key == "default":
self.click_id.emit(notification_id)
@ -1187,7 +1141,7 @@ class DBusNotificationAdapter(AbstractNotificationAdapter):
def on_web_closed(self, notification_id: int) -> None:
"""Send CloseNotification if a notification was closed from JS."""
self.interface.call(
QDBus.NoBlock,
QDBus.CallMode.NoBlock,
"CloseNotification",
_as_uint32(notification_id),
)
@ -1195,10 +1149,10 @@ class DBusNotificationAdapter(AbstractNotificationAdapter):
def _fetch_capabilities(self) -> None:
"""Fetch capabilities from the notification server."""
reply = self.interface.call(
QDBus.BlockWithGui,
QDBus.CallMode.BlockWithGui,
"GetCapabilities",
)
self._verify_message(reply, "as", QDBusMessage.ReplyMessage)
self._verify_message(reply, "as", QDBusMessage.MessageType.ReplyMessage)
caplist = reply.arguments()[0]
self._capabilities = _ServerCapabilities.from_list(caplist)
@ -1225,7 +1179,7 @@ class DBusNotificationAdapter(AbstractNotificationAdapter):
prefix = None
elif self._capabilities.body_markup and self._capabilities.body_hyperlinks:
href = html.escape(
origin_url.toString(QUrl.FullyEncoded) # type: ignore[arg-type]
origin_url.toString(QUrl.ComponentFormattingOption.FullyEncoded) # type: ignore[arg-type]
)
text = html.escape(urlstr, quote=False)
prefix = f'<a href="{href}">{text}</a>'

View File

@ -19,7 +19,7 @@
"""QWebHistory serializer for QtWebEngine."""
from PyQt5.QtCore import QByteArray, QDataStream, QIODevice, QUrl
from qutebrowser.qt.core import QByteArray, QDataStream, QIODevice, QUrl
from qutebrowser.utils import qtutils
@ -124,7 +124,7 @@ def serialize(items):
segfault!
"""
data = QByteArray()
stream = QDataStream(data, QIODevice.ReadWrite)
stream = QDataStream(data, QIODevice.OpenModeFlag.ReadWrite)
cur_user_data = None
current_idx = None

View File

@ -23,8 +23,9 @@ import re
import os.path
import functools
from PyQt5.QtCore import pyqtSlot, Qt, QUrl, QObject
from PyQt5.QtWebEngineWidgets import QWebEngineDownloadItem
from qutebrowser.qt import machinery
from qutebrowser.qt.core import pyqtSlot, Qt, QUrl, QObject
from qutebrowser.qt.webenginecore import QWebEngineDownloadRequest
from qutebrowser.browser import downloads, pdfjs
from qutebrowser.utils import (debug, usertypes, message, log, objreg, urlutils,
@ -33,19 +34,34 @@ from qutebrowser.utils import (debug, usertypes, message, log, objreg, urlutils,
class DownloadItem(downloads.AbstractDownloadItem):
"""A wrapper over a QWebEngineDownloadItem.
"""A wrapper over a QWebEngineDownloadRequest.
Attributes:
_qt_item: The wrapped item.
"""
def __init__(self, qt_item: QWebEngineDownloadItem,
def __init__(self, qt_item: QWebEngineDownloadRequest,
manager: downloads.AbstractDownloadManager,
parent: QObject = None) -> None:
super().__init__(manager=manager, parent=manager)
self._qt_item = qt_item
qt_item.downloadProgress.connect(self.stats.on_download_progress)
qt_item.stateChanged.connect(self._on_state_changed)
if machinery.IS_QT5:
qt_item.downloadProgress.connect(self.stats.on_download_progress)
else: # Qt 6
qt_item.receivedBytesChanged.connect(
lambda: self.stats.on_download_progress(
qt_item.receivedBytes(),
qt_item.totalBytes(),
)
)
qt_item.totalBytesChanged.connect(
lambda: self.stats.on_download_progress(
qt_item.receivedBytes(),
qt_item.totalBytes(),
)
)
qt_item.stateChanged.connect(
self._on_state_changed)
# Ensure wrapped qt_item is deleted manually when the wrapper object
# is deleted. See https://github.com/qutebrowser/qutebrowser/issues/3373
@ -54,19 +70,19 @@ class DownloadItem(downloads.AbstractDownloadItem):
def _is_page_download(self):
"""Check if this item is a page (i.e. mhtml) download."""
return (self._qt_item.savePageFormat() !=
QWebEngineDownloadItem.UnknownSaveFormat)
QWebEngineDownloadRequest.SavePageFormat.UnknownSaveFormat)
@pyqtSlot(QWebEngineDownloadItem.DownloadState)
@pyqtSlot(QWebEngineDownloadRequest.DownloadState)
def _on_state_changed(self, state):
state_name = debug.qenum_key(QWebEngineDownloadItem, state)
state_name = debug.qenum_key(QWebEngineDownloadRequest, state)
log.downloads.debug("State for {!r} changed to {}".format(
self, state_name))
if state == QWebEngineDownloadItem.DownloadRequested:
if state == QWebEngineDownloadRequest.DownloadState.DownloadRequested:
pass
elif state == QWebEngineDownloadItem.DownloadInProgress:
elif state == QWebEngineDownloadRequest.DownloadState.DownloadInProgress:
pass
elif state == QWebEngineDownloadItem.DownloadCompleted:
elif state == QWebEngineDownloadRequest.DownloadState.DownloadCompleted:
log.downloads.debug("Download {} finished".format(self.basename))
if self._is_page_download():
# Same logging as QtWebKit mhtml downloads.
@ -75,12 +91,12 @@ class DownloadItem(downloads.AbstractDownloadItem):
self.done = True
self.finished.emit()
self.stats.finish()
elif state == QWebEngineDownloadItem.DownloadCancelled:
elif state == QWebEngineDownloadRequest.DownloadState.DownloadCancelled:
self.successful = False
self.done = True
self.cancelled.emit()
self.stats.finish()
elif state == QWebEngineDownloadItem.DownloadInterrupted:
elif state == QWebEngineDownloadRequest.DownloadState.DownloadInterrupted:
self.successful = False
reason = self._qt_item.interruptReasonString()
self._die(reason)
@ -89,24 +105,28 @@ class DownloadItem(downloads.AbstractDownloadItem):
"{}".format(state_name))
def _do_die(self):
progress_signal = self._qt_item.downloadProgress
progress_signal.disconnect()
if self._qt_item.state() != QWebEngineDownloadItem.DownloadInterrupted:
if machinery.IS_QT5:
self._qt_item.downloadProgress.disconnect()
else: # Qt 6
self._qt_item.receivedBytesChanged.disconnect()
self._qt_item.totalBytesChanged.disconnect()
if self._qt_item.state() != QWebEngineDownloadRequest.DownloadState.DownloadInterrupted:
self._qt_item.cancel()
def _do_cancel(self):
state = self._qt_item.state()
state_name = debug.qenum_key(QWebEngineDownloadItem, state)
assert state not in [QWebEngineDownloadItem.DownloadCompleted,
QWebEngineDownloadItem.DownloadCancelled], state_name
state_name = debug.qenum_key(QWebEngineDownloadRequest, state)
assert state not in [QWebEngineDownloadRequest.DownloadState.DownloadCompleted,
QWebEngineDownloadRequest.DownloadState.DownloadCancelled], state_name
self._qt_item.cancel()
def retry(self):
state = self._qt_item.state()
if state != QWebEngineDownloadItem.DownloadInterrupted:
if state != QWebEngineDownloadRequest.DownloadState.DownloadInterrupted:
log.downloads.warning(
"Refusing to retry download in state {}".format(
debug.qenum_key(QWebEngineDownloadItem, state)))
debug.qenum_key(QWebEngineDownloadRequest, state)))
return
self._qt_item.resume()
@ -131,8 +151,8 @@ class DownloadItem(downloads.AbstractDownloadItem):
def _ensure_can_set_filename(self, filename):
state = self._qt_item.state()
if state != QWebEngineDownloadItem.DownloadRequested:
state_name = debug.qenum_key(QWebEngineDownloadItem, state)
if state != QWebEngineDownloadRequest.DownloadState.DownloadRequested:
state_name = debug.qenum_key(QWebEngineDownloadRequest, state)
raise ValueError("Trying to set filename {} on {!r} which is "
"state {} (not in requested state)!".format(
filename, self, state_name))
@ -174,12 +194,8 @@ class DownloadItem(downloads.AbstractDownloadItem):
assert self._filename is not None
dirname, basename = os.path.split(self._filename)
try:
# Qt 5.14
self._qt_item.setDownloadDirectory(dirname)
self._qt_item.setDownloadFileName(basename)
except AttributeError:
self._qt_item.setPath(self._filename)
self._qt_item.setDownloadDirectory(dirname)
self._qt_item.setDownloadFileName(basename)
self._qt_item.accept()
@ -245,12 +261,12 @@ class DownloadManager(downloads.AbstractDownloadManager):
def install(self, profile):
"""Set up the download manager on a QWebEngineProfile."""
profile.downloadRequested.connect(self.handle_download,
Qt.DirectConnection)
Qt.ConnectionType.DirectConnection)
@pyqtSlot(QWebEngineDownloadItem)
@pyqtSlot(QWebEngineDownloadRequest)
def handle_download(self, qt_item):
"""Start a download coming from a QWebEngineProfile."""
qt_filename = os.path.basename(qt_item.path()) # FIXME use 5.14 API
qt_filename = qt_item.downloadFileName()
mime_type = qt_item.mimeType()
url = qt_item.url()

View File

@ -22,9 +22,9 @@
from typing import (
TYPE_CHECKING, Any, Callable, Dict, Iterator, Optional, Set, Tuple, Union)
from PyQt5.QtCore import QRect, QEventLoop
from PyQt5.QtWidgets import QApplication
from PyQt5.QtWebEngineWidgets import QWebEngineSettings
from qutebrowser.qt.core import QRect, QEventLoop
from qutebrowser.qt.widgets import QApplication
from qutebrowser.qt.webenginecore import QWebEngineSettings
from qutebrowser.utils import log, javascript, urlutils, usertypes, utils
from qutebrowser.browser import webelem
@ -231,7 +231,6 @@ class WebEngineElement(webelem.AbstractWebElement):
return url.scheme() not in urlutils.WEBENGINE_SCHEMES
def _click_editable(self, click_target: usertypes.ClickTarget) -> None:
self._tab.setFocus() # Needed as WORKAROUND on Qt 5.12
# This actually "clicks" the element by calling focus() on it in JS.
self._js_call('focus')
self._move_text_cursor()
@ -242,7 +241,7 @@ class WebEngineElement(webelem.AbstractWebElement):
view = self._tab._widget
assert view is not None
# pylint: enable=protected-access
attribute = QWebEngineSettings.JavascriptCanOpenWindows
attribute = QWebEngineSettings.WebAttribute.JavascriptCanOpenWindows
could_open_windows = view.settings().testAttribute(attribute)
view.settings().setAttribute(attribute, True)
@ -251,8 +250,8 @@ class WebEngineElement(webelem.AbstractWebElement):
# This is also used in Qt's tests:
# https://github.com/qt/qtwebengine/commit/5e572e88efa7ba7c2b9138ec19e606d3e345ac90
QApplication.processEvents(
QEventLoop.ExcludeSocketNotifiers |
QEventLoop.ExcludeUserInputEvents)
QEventLoop.ProcessEventsFlag.ExcludeSocketNotifiers |
QEventLoop.ProcessEventsFlag.ExcludeUserInputEvents)
def reset_setting(_arg: Any) -> None:
"""Set the JavascriptCanOpenWindows setting to its old value."""

View File

@ -19,16 +19,17 @@
"""Customized QWebInspector for QtWebEngine."""
import pathlib
from typing import Optional
from PyQt5.QtCore import QLibraryInfo
from PyQt5.QtWebEngineWidgets import QWebEngineView, QWebEnginePage
from PyQt5.QtWidgets import QWidget
from qutebrowser.qt import machinery
from qutebrowser.qt.webenginewidgets import QWebEngineView
from qutebrowser.qt.webenginecore import QWebEnginePage
from qutebrowser.qt.widgets import QWidget
from qutebrowser.browser import inspector
from qutebrowser.browser.webengine import webenginesettings
from qutebrowser.browser.webengine import webenginesettings, webview
from qutebrowser.misc import miscwidgets
from qutebrowser.utils import version, usertypes
from qutebrowser.utils import version, usertypes, qtutils
from qutebrowser.keyinput import modeman
@ -49,9 +50,14 @@ class WebEngineInspectorView(QWebEngineView):
See WebEngineView.createWindow for details.
"""
view = self.page().inspectedPage().view()
assert isinstance(view, QWebEngineView), view
return view.createWindow(wintype)
inspected_page = self.page().inspectedPage()
if machinery.IS_QT5:
view = inspected_page.view()
assert isinstance(view, QWebEngineView), view
return view.createWindow(wintype)
else: # Qt 6
newpage = inspected_page.createWindow(wintype)
return webview.WebEngineView.forPage(newpage)
class WebEngineInspector(inspector.AbstractWebInspector):
@ -65,12 +71,7 @@ class WebEngineInspector(inspector.AbstractWebInspector):
parent: QWidget = None) -> None:
super().__init__(splitter, win_id, parent)
self._check_devtools_resources()
view = WebEngineInspectorView()
self._settings = webenginesettings.WebEngineSettings(view.settings())
self._set_widget(view)
page = view.page()
page.windowCloseRequested.connect(self._on_window_close_requested)
self._settings: Optional[webenginesettings.WebEngineSettings] = None
def _on_window_close_requested(self) -> None:
"""Called when the 'x' was clicked in the devtools."""
@ -92,7 +93,7 @@ class WebEngineInspector(inspector.AbstractWebInspector):
if dist is None or dist.parsed != version.Distribution.fedora:
return
data_path = pathlib.Path(QLibraryInfo.location(QLibraryInfo.DataPath))
data_path = qtutils.library_path(qtutils.LibraryPath.data)
pak = data_path / 'resources' / 'qtwebengine_devtools_resources.pak'
if not pak.exists():
raise inspector.Error("QtWebEngine devtools resources not found, "
@ -100,8 +101,22 @@ class WebEngineInspector(inspector.AbstractWebInspector):
"Fedora package.")
def inspect(self, page: QWebEnginePage) -> None:
if not self._widget:
view = WebEngineInspectorView()
inspector_page = QWebEnginePage(
page.profile(),
self
)
inspector_page.windowCloseRequested.connect(self._on_window_close_requested)
view.setPage(inspector_page)
self._settings = webenginesettings.WebEngineSettings(view.settings())
self._set_widget(view)
inspector_page = self._widget.page()
assert inspector_page.profile() == page.profile()
inspector_page.setInspectedPage(page)
assert self._settings is not None
self._settings.update_for_url(inspector_page.requestedUrl())
def _needs_recreate(self) -> bool:

View File

@ -19,8 +19,8 @@
"""QtWebEngine specific qute://* handlers and glue code."""
from PyQt5.QtCore import QBuffer, QIODevice, QUrl
from PyQt5.QtWebEngineCore import (QWebEngineUrlSchemeHandler,
from qutebrowser.qt.core import QBuffer, QIODevice, QUrl
from qutebrowser.qt.webenginecore import (QWebEngineUrlSchemeHandler,
QWebEngineUrlRequestJob,
QWebEngineUrlScheme)
@ -67,7 +67,7 @@ class QuteSchemeHandler(QWebEngineUrlSchemeHandler):
log.network.warning("Blocking malicious request from {} to {}"
.format(initiator.toDisplayString(),
request_url.toDisplayString()))
job.fail(QWebEngineUrlRequestJob.RequestDenied)
job.fail(QWebEngineUrlRequestJob.Error.RequestDenied)
return False
return True
@ -87,7 +87,7 @@ class QuteSchemeHandler(QWebEngineUrlSchemeHandler):
return
if job.requestMethod() != b'GET':
job.fail(QWebEngineUrlRequestJob.RequestDenied)
job.fail(QWebEngineUrlRequestJob.Error.RequestDenied)
return
assert url.scheme() == 'qute'
@ -98,15 +98,15 @@ class QuteSchemeHandler(QWebEngineUrlSchemeHandler):
except qutescheme.Error as e:
errors = {
qutescheme.NotFoundError:
QWebEngineUrlRequestJob.UrlNotFound,
QWebEngineUrlRequestJob.Error.UrlNotFound,
qutescheme.UrlInvalidError:
QWebEngineUrlRequestJob.UrlInvalid,
QWebEngineUrlRequestJob.Error.UrlInvalid,
qutescheme.RequestDeniedError:
QWebEngineUrlRequestJob.RequestDenied,
QWebEngineUrlRequestJob.Error.RequestDenied,
qutescheme.SchemeOSError:
QWebEngineUrlRequestJob.UrlNotFound,
QWebEngineUrlRequestJob.Error.UrlNotFound,
qutescheme.Error:
QWebEngineUrlRequestJob.RequestFailed,
QWebEngineUrlRequestJob.Error.RequestFailed,
}
exctype = type(e)
log.network.error(f"{exctype.__name__} while handling qute://* URL: {e}")
@ -121,7 +121,7 @@ class QuteSchemeHandler(QWebEngineUrlSchemeHandler):
# because that somehow segfaults...
# https://www.riverbankcomputing.com/pipermail/pyqt/2016-September/038075.html
buf = QBuffer(parent=self)
buf.open(QIODevice.WriteOnly)
buf.open(QIODevice.OpenModeFlag.WriteOnly)
buf.write(data)
buf.seek(0)
buf.close()
@ -138,6 +138,6 @@ def init():
assert not QWebEngineUrlScheme.schemeByName(b'qute').name()
scheme = QWebEngineUrlScheme(b'qute')
scheme.setFlags(
QWebEngineUrlScheme.LocalScheme |
QWebEngineUrlScheme.LocalAccessAllowed)
QWebEngineUrlScheme.Flag.LocalScheme |
QWebEngineUrlScheme.Flag.LocalAccessAllowed)
QWebEngineUrlScheme.registerScheme(scheme)

View File

@ -26,11 +26,13 @@ Module attributes:
import os
import operator
import pathlib
from typing import cast, Any, List, Optional, Tuple, Union, TYPE_CHECKING
from PyQt5.QtGui import QFont
from PyQt5.QtWidgets import QApplication
from PyQt5.QtWebEngineWidgets import QWebEngineSettings, QWebEngineProfile
from qutebrowser.qt import machinery
from qutebrowser.qt.gui import QFont
from qutebrowser.qt.widgets import QApplication
from qutebrowser.qt.webenginecore import QWebEngineSettings, QWebEngineProfile
from qutebrowser.browser import history
from qutebrowser.browser.webengine import (spell, webenginequtescheme, cookies,
@ -110,91 +112,91 @@ class WebEngineSettings(websettings.AbstractSettings):
_ATTRIBUTES = {
'content.xss_auditing':
Attr(QWebEngineSettings.XSSAuditingEnabled),
Attr(QWebEngineSettings.WebAttribute.XSSAuditingEnabled),
'content.images':
Attr(QWebEngineSettings.AutoLoadImages),
Attr(QWebEngineSettings.WebAttribute.AutoLoadImages),
'content.javascript.enabled':
Attr(QWebEngineSettings.JavascriptEnabled),
Attr(QWebEngineSettings.WebAttribute.JavascriptEnabled),
'content.javascript.can_open_tabs_automatically':
Attr(QWebEngineSettings.JavascriptCanOpenWindows),
Attr(QWebEngineSettings.WebAttribute.JavascriptCanOpenWindows),
'content.plugins':
Attr(QWebEngineSettings.PluginsEnabled),
Attr(QWebEngineSettings.WebAttribute.PluginsEnabled),
'content.hyperlink_auditing':
Attr(QWebEngineSettings.HyperlinkAuditingEnabled),
Attr(QWebEngineSettings.WebAttribute.HyperlinkAuditingEnabled),
'content.local_content_can_access_remote_urls':
Attr(QWebEngineSettings.LocalContentCanAccessRemoteUrls),
Attr(QWebEngineSettings.WebAttribute.LocalContentCanAccessRemoteUrls),
'content.local_content_can_access_file_urls':
Attr(QWebEngineSettings.LocalContentCanAccessFileUrls),
Attr(QWebEngineSettings.WebAttribute.LocalContentCanAccessFileUrls),
'content.webgl':
Attr(QWebEngineSettings.WebGLEnabled),
Attr(QWebEngineSettings.WebAttribute.WebGLEnabled),
'content.local_storage':
Attr(QWebEngineSettings.LocalStorageEnabled),
Attr(QWebEngineSettings.WebAttribute.LocalStorageEnabled),
'content.desktop_capture':
Attr(QWebEngineSettings.ScreenCaptureEnabled,
Attr(QWebEngineSettings.WebAttribute.ScreenCaptureEnabled,
converter=lambda val: True if val == 'ask' else val),
# 'ask' is handled via the permission system
'input.spatial_navigation':
Attr(QWebEngineSettings.SpatialNavigationEnabled),
Attr(QWebEngineSettings.WebAttribute.SpatialNavigationEnabled),
'input.links_included_in_focus_chain':
Attr(QWebEngineSettings.LinksIncludedInFocusChain),
Attr(QWebEngineSettings.WebAttribute.LinksIncludedInFocusChain),
'scrolling.smooth':
Attr(QWebEngineSettings.ScrollAnimatorEnabled),
Attr(QWebEngineSettings.WebAttribute.ScrollAnimatorEnabled),
'content.print_element_backgrounds':
Attr(QWebEngineSettings.PrintElementBackgrounds),
Attr(QWebEngineSettings.WebAttribute.PrintElementBackgrounds),
'content.autoplay':
Attr(QWebEngineSettings.PlaybackRequiresUserGesture,
Attr(QWebEngineSettings.WebAttribute.PlaybackRequiresUserGesture,
converter=operator.not_),
'content.dns_prefetch':
Attr(QWebEngineSettings.DnsPrefetchEnabled),
Attr(QWebEngineSettings.WebAttribute.DnsPrefetchEnabled),
'tabs.favicons.show':
Attr(QWebEngineSettings.AutoLoadIconsForPage,
Attr(QWebEngineSettings.WebAttribute.AutoLoadIconsForPage,
converter=lambda val: val != 'never'),
}
_FONT_SIZES = {
'fonts.web.size.minimum':
QWebEngineSettings.MinimumFontSize,
QWebEngineSettings.FontSize.MinimumFontSize,
'fonts.web.size.minimum_logical':
QWebEngineSettings.MinimumLogicalFontSize,
QWebEngineSettings.FontSize.MinimumLogicalFontSize,
'fonts.web.size.default':
QWebEngineSettings.DefaultFontSize,
QWebEngineSettings.FontSize.DefaultFontSize,
'fonts.web.size.default_fixed':
QWebEngineSettings.DefaultFixedFontSize,
QWebEngineSettings.FontSize.DefaultFixedFontSize,
}
_FONT_FAMILIES = {
'fonts.web.family.standard': QWebEngineSettings.StandardFont,
'fonts.web.family.fixed': QWebEngineSettings.FixedFont,
'fonts.web.family.serif': QWebEngineSettings.SerifFont,
'fonts.web.family.sans_serif': QWebEngineSettings.SansSerifFont,
'fonts.web.family.cursive': QWebEngineSettings.CursiveFont,
'fonts.web.family.fantasy': QWebEngineSettings.FantasyFont,
'fonts.web.family.standard': QWebEngineSettings.FontFamily.StandardFont,
'fonts.web.family.fixed': QWebEngineSettings.FontFamily.FixedFont,
'fonts.web.family.serif': QWebEngineSettings.FontFamily.SerifFont,
'fonts.web.family.sans_serif': QWebEngineSettings.FontFamily.SansSerifFont,
'fonts.web.family.cursive': QWebEngineSettings.FontFamily.CursiveFont,
'fonts.web.family.fantasy': QWebEngineSettings.FontFamily.FantasyFont,
}
_UNKNOWN_URL_SCHEME_POLICY = {
'disallow':
QWebEngineSettings.DisallowUnknownUrlSchemes,
QWebEngineSettings.UnknownUrlSchemePolicy.DisallowUnknownUrlSchemes,
'allow-from-user-interaction':
QWebEngineSettings.AllowUnknownUrlSchemesFromUserInteraction,
QWebEngineSettings.UnknownUrlSchemePolicy.AllowUnknownUrlSchemesFromUserInteraction,
'allow-all':
QWebEngineSettings.AllowAllUnknownUrlSchemes,
QWebEngineSettings.UnknownUrlSchemePolicy.AllowAllUnknownUrlSchemes,
}
# Mapping from WebEngineSettings::initDefaults in
# qtwebengine/src/core/web_engine_settings.cpp
_FONT_TO_QFONT = {
QWebEngineSettings.StandardFont: QFont.Serif,
QWebEngineSettings.FixedFont: QFont.Monospace,
QWebEngineSettings.SerifFont: QFont.Serif,
QWebEngineSettings.SansSerifFont: QFont.SansSerif,
QWebEngineSettings.CursiveFont: QFont.Cursive,
QWebEngineSettings.FantasyFont: QFont.Fantasy,
QWebEngineSettings.FontFamily.StandardFont: QFont.StyleHint.Serif,
QWebEngineSettings.FontFamily.FixedFont: QFont.StyleHint.Monospace,
QWebEngineSettings.FontFamily.SerifFont: QFont.StyleHint.Serif,
QWebEngineSettings.FontFamily.SansSerifFont: QFont.StyleHint.SansSerif,
QWebEngineSettings.FontFamily.CursiveFont: QFont.StyleHint.Cursive,
QWebEngineSettings.FontFamily.FantasyFont: QFont.StyleHint.Fantasy,
}
_JS_CLIPBOARD_SETTINGS = {
@ -256,15 +258,10 @@ class ProfileSetter:
'content.cache.size': self.set_http_cache_size,
'content.cookies.store': self.set_persistent_cookie_policy,
'spellcheck.languages': self.set_dictionary_language,
'content.headers.user_agent': self.set_http_headers,
'content.headers.accept_language': self.set_http_headers,
}
# WORKAROUND for https://bugreports.qt.io/browse/QTBUG-75884
# (note this isn't actually fixed properly before Qt 5.15)
header_bug_fixed = qtutils.version_check('5.15', compiled=False)
if header_bug_fixed:
for name in ['user_agent', 'accept_language']:
self._name_to_method[f'content.headers.{name}'] = self.set_http_headers
def update_setting(self, name):
"""Update a setting based on its name."""
try:
@ -286,15 +283,10 @@ class ProfileSetter:
settings = self._profile.settings()
settings.setAttribute(
QWebEngineSettings.FullScreenSupportEnabled, True)
QWebEngineSettings.WebAttribute.FullScreenSupportEnabled, True)
settings.setAttribute(
QWebEngineSettings.FocusOnNavigationEnabled, False)
try:
settings.setAttribute(QWebEngineSettings.PdfViewerEnabled, False)
except AttributeError:
# Added in Qt 5.13
pass
QWebEngineSettings.WebAttribute.FocusOnNavigationEnabled, False)
settings.setAttribute(QWebEngineSettings.WebAttribute.PdfViewerEnabled, False)
def set_http_headers(self):
"""Set the user agent and accept-language for the given profile.
@ -326,9 +318,9 @@ class ProfileSetter:
if self._profile.isOffTheRecord():
return
if config.val.content.cookies.store:
value = QWebEngineProfile.AllowPersistentCookies
value = QWebEngineProfile.PersistentCookiesPolicy.AllowPersistentCookies
else:
value = QWebEngineProfile.NoPersistentCookies
value = QWebEngineProfile.PersistentCookiesPolicy.NoPersistentCookies
self._profile.setPersistentCookiesPolicy(value)
def set_dictionary_language(self):
@ -397,7 +389,11 @@ def _init_default_profile():
"""Init the default QWebEngineProfile."""
global default_profile
default_profile = QWebEngineProfile.defaultProfile()
if machinery.IS_QT6:
default_profile = QWebEngineProfile("Default")
else:
default_profile = QWebEngineProfile.defaultProfile()
assert not default_profile.isOffTheRecord()
assert parsed_user_agent is None # avoid earlier profile initialization
non_ua_version = version.qtwebengine_versions(avoid_init=True)
@ -470,6 +466,7 @@ def _init_site_specific_quirks():
# Needed because Slack adds an error which prevents using it relatively
# aggressively, despite things actually working fine.
# September 2020: Qt 5.12 works, but Qt <= 5.11 shows the error.
# FIXME:qt6 Still needed?
# https://github.com/qutebrowser/qutebrowser/issues/4669
("ua-slack", 'https://*.slack.com/*', new_chrome_ua),
]
@ -489,20 +486,37 @@ def _init_site_specific_quirks():
)
def _init_devtools_settings():
"""Make sure the devtools always get images/JS permissions."""
settings: List[Tuple[str, Any]] = [
def _init_default_settings():
"""Set permissions required for internal functionality.
- Make sure the devtools always get images/JS permissions.
- On Qt 6, make sure files in the data path can load external resources.
"""
devtools_settings: List[Tuple[str, Any]] = [
('content.javascript.enabled', True),
('content.images', True),
('content.cookies.accept', 'all'),
]
for setting, value in settings:
for setting, value in devtools_settings:
for pattern in ['chrome-devtools://*', 'devtools://*']:
config.instance.set_obj(setting, value,
pattern=urlmatch.UrlPattern(pattern),
hide_userconfig=True)
if machinery.IS_QT6:
userscripts_settings: List[Tuple[str, Any]] = [
("content.local_content_can_access_remote_urls", True),
("content.local_content_can_access_file_urls", False),
]
# https://codereview.qt-project.org/c/qt/qtwebengine/+/375672
url = pathlib.Path(standarddir.data(), "userscripts").as_uri()
for setting, value in userscripts_settings:
config.instance.set_obj(setting,
value,
pattern=urlmatch.UrlPattern(f"{url}/*"),
hide_userconfig=True)
def init():
"""Initialize the global QWebSettings."""
@ -543,7 +557,7 @@ def init():
log.init.debug("Misc initialization...")
_init_site_specific_quirks()
_init_devtools_settings()
_init_default_settings()
def shutdown():

View File

@ -26,11 +26,11 @@ import re
import html as html_utils
from typing import cast, Union, Optional
from PyQt5.QtCore import (pyqtSignal, pyqtSlot, Qt, QPoint, QPointF, QTimer, QUrl,
from qutebrowser.qt.core import (pyqtSignal, pyqtSlot, Qt, QPoint, QPointF, QTimer, QUrl,
QObject)
from PyQt5.QtNetwork import QAuthenticator
from PyQt5.QtWebEngineWidgets import (QWebEnginePage, QWebEngineView, QWebEngineScript,
QWebEngineHistory)
from qutebrowser.qt.network import QAuthenticator
from qutebrowser.qt.webenginewidgets import QWebEngineView
from qutebrowser.qt.webenginecore import QWebEnginePage, QWebEngineScript, QWebEngineHistory
from qutebrowser.config import config
from qutebrowser.browser import browsertab, eventfilter, shared, webelem, greasemonkey
@ -40,16 +40,16 @@ from qutebrowser.browser.webengine import (webview, webengineelem, tabhistory,
from qutebrowser.utils import (usertypes, qtutils, log, javascript, utils,
resources, message, jinja, debug, version)
from qutebrowser.qt import sip
from qutebrowser.qt import sip, machinery
from qutebrowser.misc import objects, miscwidgets
# Mapping worlds from usertypes.JsWorld to QWebEngineScript world IDs.
_JS_WORLD_MAP = {
usertypes.JsWorld.main: QWebEngineScript.MainWorld,
usertypes.JsWorld.application: QWebEngineScript.ApplicationWorld,
usertypes.JsWorld.user: QWebEngineScript.UserWorld,
usertypes.JsWorld.jseval: QWebEngineScript.UserWorld + 1,
usertypes.JsWorld.main: QWebEngineScript.ScriptWorldId.MainWorld,
usertypes.JsWorld.application: QWebEngineScript.ScriptWorldId.ApplicationWorld,
usertypes.JsWorld.user: QWebEngineScript.ScriptWorldId.UserWorld,
usertypes.JsWorld.jseval: QWebEngineScript.ScriptWorldId.UserWorld + 1,
}
@ -58,23 +58,21 @@ class WebEngineAction(browsertab.AbstractAction):
"""QtWebEngine implementations related to web actions."""
_widget: webview.WebEngineView
action_class = QWebEnginePage
action_base = QWebEnginePage.WebAction
def exit_fullscreen(self):
self._widget.triggerPageAction(QWebEnginePage.ExitFullScreen)
self._widget.triggerPageAction(QWebEnginePage.WebAction.ExitFullScreen)
def save_page(self):
"""Save the current page."""
self._widget.triggerPageAction(QWebEnginePage.SavePage)
self._widget.triggerPageAction(QWebEnginePage.WebAction.SavePage)
def show_source(self, pygments=False):
if pygments:
self._show_source_pygments()
return
self._widget.triggerPageAction(QWebEnginePage.ViewSource)
self._widget.triggerPageAction(QWebEnginePage.WebAction.ViewSource)
class WebEnginePrinting(browsertab.AbstractPrinting):
@ -83,6 +81,14 @@ class WebEnginePrinting(browsertab.AbstractPrinting):
_widget: webview.WebEngineView
def connect_signals(self):
"""Called from WebEngineTab.connect_signals."""
page = self._widget.page()
page.pdfPrintingFinished.connect(self.pdf_printing_finished)
if machinery.IS_QT6:
self._widget.printFinished.connect(self.printing_finished)
# Qt 5 uses callbacks instead
def check_pdf_support(self):
pass
@ -93,8 +99,11 @@ class WebEnginePrinting(browsertab.AbstractPrinting):
def to_pdf(self, filename):
self._widget.page().printToPdf(filename)
def to_printer(self, printer, callback=lambda ok: None):
self._widget.page().print(printer, callback)
def to_printer(self, printer):
if machinery.IS_QT5:
self._widget.page().print(printer, self.printing_finished.emit)
else: # Qt 6
self._widget.print(printer)
@dataclasses.dataclass
@ -159,22 +168,6 @@ class WebEngineSearch(browsertab.AbstractSearch):
def connect_signals(self):
"""Connect the signals necessary for this class to function."""
# The API necessary to stop wrapping was added in this version
if not qtutils.version_check("5.14"):
return
try:
# pylint: disable=unused-import
from PyQt5.QtWebEngineCore import QWebEngineFindTextResult
except ImportError:
# WORKAROUND for some odd PyQt/packaging bug where the
# findTextResult signal is available, but QWebEngineFindTextResult
# is not. Seems to happen on e.g. Gentoo.
log.webview.warning("Could not import QWebEngineFindTextResult "
"despite running on Qt 5.14. You might need "
"to rebuild PyQtWebEngine.")
return
self._widget.page().findTextFinished.connect(self._on_find_finished)
def _find(self, text, flags, callback, caller):
@ -182,7 +175,7 @@ class WebEngineSearch(browsertab.AbstractSearch):
self.search_displayed = True
self._pending_searches += 1
def wrapped_callback(found):
def wrapped_callback(cb_arg):
"""Wrap the callback to do debug logging."""
self._pending_searches -= 1
if self._pending_searches > 0:
@ -200,6 +193,11 @@ class WebEngineSearch(browsertab.AbstractSearch):
"widget")
return
# bool in Qt 5, QWebEngineFindTextResult in Qt 6
# Once we drop Qt 5, we might also want to call callbacks with the
# QWebEngineFindTextResult instead of the bool.
found = cb_arg if isinstance(cb_arg, bool) else cb_arg.numberOfMatches() > 0
found_text = 'found' if found else "didn't find"
if flags:
flag_text = f'with flags {flags}'
@ -400,6 +398,7 @@ class WebEngineCaret(browsertab.AbstractCaret):
# https://bugreports.qt.io/browse/QTBUG-53134
# Even on Qt 5.10 selectedText() seems to work poorly, see
# https://github.com/qutebrowser/qutebrowser/issues/3523
# FIXME:qt6 Reevaluate?
self._tab.run_js_async(javascript.assemble('caret', 'getSelection'),
callback)
@ -506,7 +505,7 @@ class WebEngineScroller(browsertab.AbstractScroller):
page = widget.page()
page.scrollPositionChanged.connect(self._update_pos)
def _repeated_key_press(self, key, count=1, modifier=Qt.NoModifier):
def _repeated_key_press(self, key, count=1, modifier=Qt.KeyboardModifier.NoModifier):
"""Send count fake key presses to this scroller's WebEngineTab."""
for _ in range(min(count, 1000)):
self._tab.fake_key_press(key, modifier)
@ -585,28 +584,28 @@ class WebEngineScroller(browsertab.AbstractScroller):
self._tab.run_js_async(js_code)
def up(self, count=1):
self._repeated_key_press(Qt.Key_Up, count)
self._repeated_key_press(Qt.Key.Key_Up, count)
def down(self, count=1):
self._repeated_key_press(Qt.Key_Down, count)
self._repeated_key_press(Qt.Key.Key_Down, count)
def left(self, count=1):
self._repeated_key_press(Qt.Key_Left, count)
self._repeated_key_press(Qt.Key.Key_Left, count)
def right(self, count=1):
self._repeated_key_press(Qt.Key_Right, count)
self._repeated_key_press(Qt.Key.Key_Right, count)
def top(self):
self._tab.fake_key_press(Qt.Key_Home)
self._tab.fake_key_press(Qt.Key.Key_Home)
def bottom(self):
self._tab.fake_key_press(Qt.Key_End)
self._tab.fake_key_press(Qt.Key.Key_End)
def page_up(self, count=1):
self._repeated_key_press(Qt.Key_PageUp, count)
self._repeated_key_press(Qt.Key.Key_PageUp, count)
def page_down(self, count=1):
self._repeated_key_press(Qt.Key_PageDown, count)
self._repeated_key_press(Qt.Key.Key_PageDown, count)
def at_top(self):
return self.pos_px().y() == 0
@ -650,11 +649,13 @@ class WebEngineHistoryPrivate(browsertab.AbstractHistoryPrivate):
self._tab.load_url(url)
def load_items(self, items):
webengine_version = version.qtwebengine_versions().webengine
if webengine_version >= utils.VersionNumber(5, 15):
self._load_items_workaround(items)
return
self._load_items_workaround(items)
def _load_items_proper(self, items):
"""Load session items properly.
Currently unused, but should be revived.
"""
if items:
self._tab.before_load_started.emit(items[-1].url)
@ -813,7 +814,7 @@ class WebEngineAudio(browsertab.AbstractAudio):
self._overridden = False
# Implements the intended two-second delay specified at
# https://doc.qt.io/qt-5/qwebenginepage.html#recentlyAudibleChanged
# https://doc.qt.io/archives/qt-5.14/qwebenginepage.html#recentlyAudibleChanged
delay_ms = 2000
self._silence_timer = QTimer(self)
self._silence_timer.setSingleShot(True)
@ -845,15 +846,10 @@ class WebEngineAudio(browsertab.AbstractAudio):
timer.start()
def set_muted(self, muted: bool, override: bool = False) -> None:
was_muted = self.is_muted()
self._overridden = override
assert self._widget is not None
page = self._widget.page()
page.setAudioMuted(muted)
if was_muted != muted and qtutils.version_check('5.15'):
# WORKAROUND for https://bugreports.qt.io/browse/QTBUG-85118
# so that the tab title at least updates the muted indicator
self.muted_changed.emit(muted)
def is_muted(self):
page = self._widget.page()
@ -881,29 +877,26 @@ class _WebEnginePermissions(QObject):
_widget: webview.WebEngineView
# Using 0 as WORKAROUND for:
# https://www.riverbankcomputing.com/pipermail/pyqt/2019-July/041903.html
_options = {
0: 'content.notifications.enabled',
QWebEnginePage.Geolocation: 'content.geolocation',
QWebEnginePage.MediaAudioCapture: 'content.media.audio_capture',
QWebEnginePage.MediaVideoCapture: 'content.media.video_capture',
QWebEnginePage.MediaAudioVideoCapture: 'content.media.audio_video_capture',
QWebEnginePage.MouseLock: 'content.mouse_lock',
QWebEnginePage.DesktopVideoCapture: 'content.desktop_capture',
QWebEnginePage.DesktopAudioVideoCapture: 'content.desktop_capture',
QWebEnginePage.Feature.Notifications: 'content.notifications.enabled',
QWebEnginePage.Feature.Geolocation: 'content.geolocation',
QWebEnginePage.Feature.MediaAudioCapture: 'content.media.audio_capture',
QWebEnginePage.Feature.MediaVideoCapture: 'content.media.video_capture',
QWebEnginePage.Feature.MediaAudioVideoCapture: 'content.media.audio_video_capture',
QWebEnginePage.Feature.MouseLock: 'content.mouse_lock',
QWebEnginePage.Feature.DesktopVideoCapture: 'content.desktop_capture',
QWebEnginePage.Feature.DesktopAudioVideoCapture: 'content.desktop_capture',
}
_messages = {
0: 'show notifications',
QWebEnginePage.Geolocation: 'access your location',
QWebEnginePage.MediaAudioCapture: 'record audio',
QWebEnginePage.MediaVideoCapture: 'record video',
QWebEnginePage.MediaAudioVideoCapture: 'record audio/video',
QWebEnginePage.MouseLock: 'hide your mouse pointer',
QWebEnginePage.DesktopVideoCapture: 'capture your desktop',
QWebEnginePage.DesktopAudioVideoCapture: 'capture your desktop and audio',
QWebEnginePage.Feature.Notifications: 'show notifications',
QWebEnginePage.Feature.Geolocation: 'access your location',
QWebEnginePage.Feature.MediaAudioCapture: 'record audio',
QWebEnginePage.Feature.MediaVideoCapture: 'record video',
QWebEnginePage.Feature.MediaAudioVideoCapture: 'record audio/video',
QWebEnginePage.Feature.MouseLock: 'hide your mouse pointer',
QWebEnginePage.Feature.DesktopVideoCapture: 'capture your desktop',
QWebEnginePage.Feature.DesktopAudioVideoCapture: 'capture your desktop and audio',
}
def __init__(self, tab, parent=None):
@ -944,22 +937,15 @@ class _WebEnginePermissions(QObject):
page = self._widget.page()
grant_permission = functools.partial(
page.setFeaturePermission, url, feature,
QWebEnginePage.PermissionGrantedByUser)
QWebEnginePage.PermissionPolicy.PermissionGrantedByUser)
deny_permission = functools.partial(
page.setFeaturePermission, url, feature,
QWebEnginePage.PermissionDeniedByUser)
QWebEnginePage.PermissionPolicy.PermissionDeniedByUser)
permission_str = debug.qenum_key(QWebEnginePage, feature)
if not url.isValid():
# WORKAROUND for https://bugreports.qt.io/browse/QTBUG-85116
is_qtbug = (qtutils.version_check('5.15.0',
compiled=False,
exact=True) and
self._tab.is_private and
feature == QWebEnginePage.Notifications)
logger = log.webview.debug if is_qtbug else log.webview.warning
logger("Ignoring feature permission {} for invalid URL {}".format(
log.webview.warning("Ignoring feature permission {} for invalid URL {}".format(
permission_str, url))
deny_permission()
return
@ -970,20 +956,8 @@ class _WebEnginePermissions(QObject):
deny_permission()
return
if (
feature in [QWebEnginePage.DesktopVideoCapture,
QWebEnginePage.DesktopAudioVideoCapture] and
qtutils.version_check('5.13', compiled=False) and
not qtutils.version_check('5.13.2', compiled=False)
):
# WORKAROUND for https://bugreports.qt.io/browse/QTBUG-78016
log.webview.warning("Ignoring desktop sharing request due to "
"crashes in Qt < 5.13.2")
deny_permission()
return
question = shared.feature_permission(
url=url.adjusted(QUrl.RemovePath),
url=url.adjusted(QUrl.UrlFormattingOption.RemovePath),
option=self._options[feature], msg=self._messages[feature],
yes_action=grant_permission, no_action=deny_permission,
abort_on=[self._tab.abort_questions])
@ -1010,7 +984,7 @@ class _WebEnginePermissions(QObject):
def _on_quota_requested(self, request):
size = utils.format_size(request.requestedSize())
shared.feature_permission(
url=request.origin().adjusted(QUrl.RemovePath),
url=request.origin().adjusted(QUrl.UrlFormattingOption.RemovePath),
option='content.persistent_storage',
msg='use {} of persistent storage'.format(size),
yes_action=request.accept, no_action=request.reject,
@ -1019,7 +993,7 @@ class _WebEnginePermissions(QObject):
def _on_register_protocol_handler_requested(self, request):
shared.feature_permission(
url=request.origin().adjusted(QUrl.RemovePath),
url=request.origin().adjusted(QUrl.UrlFormattingOption.RemovePath),
option='content.register_protocol_handler',
msg='open all {} links'.format(request.scheme()),
yes_action=request.accept, no_action=request.reject,
@ -1032,8 +1006,8 @@ class _Quirk:
filename: str
injection_point: QWebEngineScript.InjectionPoint = (
QWebEngineScript.DocumentCreation)
world: QWebEngineScript.ScriptWorldId = QWebEngineScript.MainWorld
QWebEngineScript.InjectionPoint.DocumentCreation)
world: QWebEngineScript.ScriptWorldId = QWebEngineScript.ScriptWorldId.MainWorld
predicate: bool = True
name: Optional[str] = None
@ -1074,8 +1048,8 @@ class _WebEngineScripts(QObject):
self._tab.run_js_async(code)
def _inject_js(self, name, js_code, *,
world=QWebEngineScript.ApplicationWorld,
injection_point=QWebEngineScript.DocumentCreation,
world=QWebEngineScript.ScriptWorldId.ApplicationWorld,
injection_point=QWebEngineScript.InjectionPoint.DocumentCreation,
subframes=False):
"""Inject the given script to run early on a page load."""
script = QWebEngineScript()
@ -1089,9 +1063,13 @@ class _WebEngineScripts(QObject):
def _remove_js(self, name):
"""Remove an early QWebEngineScript."""
scripts = self._widget.page().scripts()
script = scripts.findScript(f'_qute_{name}')
if not script.isNull():
scripts.remove(script)
if machinery.IS_QT6:
for script in scripts.find(f'_qute_{name}'):
scripts.remove(script)
else: # Qt 5
script = scripts.findScript(f'_qute_{name}')
if not script.isNull():
scripts.remove(script)
def init(self):
"""Initialize global qutebrowser JavaScript."""
@ -1184,13 +1162,13 @@ class _WebEngineScripts(QObject):
# Corresponds to "@run-at document-end" which is the default according to
# https://wiki.greasespot.net/Metadata_Block#.40run-at - however,
# QtWebEngine uses QWebEngineScript.Deferred (@run-at document-idle) as
# QtWebEngine uses QWebEngineScript.InjectionPoint.Deferred (@run-at document-idle) as
# default.
#
# NOTE that this needs to be done before setSourceCode, so that
# QtWebEngine's parsing of GreaseMonkey tags will override it if there is a
# @run-at comment.
new_script.setInjectionPoint(QWebEngineScript.DocumentReady)
new_script.setInjectionPoint(QWebEngineScript.InjectionPoint.DocumentReady)
new_script.setSourceCode(script.code())
new_script.setName(script.full_name())
@ -1199,22 +1177,20 @@ class _WebEngineScripts(QObject):
if script.needs_document_end_workaround():
log.greasemonkey.debug(
f"Forcing @run-at document-end for {script.name}")
new_script.setInjectionPoint(QWebEngineScript.DocumentReady)
new_script.setInjectionPoint(QWebEngineScript.InjectionPoint.DocumentReady)
log.greasemonkey.debug(f'adding script: {new_script.name()}')
page_scripts.insert(new_script)
def _inject_site_specific_quirks(self):
"""Add site-specific quirk scripts."""
if not config.val.content.site_specific_quirks.enabled:
return
def _get_quirks(self):
"""Get a list of all available JS quirks."""
versions = version.qtwebengine_versions()
quirks = [
return [
# FIXME:qt6 Double check which of those are still required
_Quirk(
'whatsapp_web',
injection_point=QWebEngineScript.DocumentReady,
world=QWebEngineScript.ApplicationWorld,
injection_point=QWebEngineScript.InjectionPoint.DocumentReady,
world=QWebEngineScript.ScriptWorldId.ApplicationWorld,
),
_Quirk('discord'),
_Quirk(
@ -1222,25 +1198,23 @@ class _WebEngineScripts(QObject):
# will be an UA quirk once we set the JS UA as well
name='ua-googledocs',
),
_Quirk(
'string_replaceall',
predicate=versions.webengine < utils.VersionNumber(5, 15, 3),
),
_Quirk(
'globalthis',
predicate=versions.webengine < utils.VersionNumber(5, 13),
),
_Quirk(
'object_fromentries',
predicate=versions.webengine < utils.VersionNumber(5, 13),
),
_Quirk(
'array_at',
predicate=versions.webengine < utils.VersionNumber(6, 2),
predicate=versions.webengine < utils.VersionNumber(6, 3),
),
]
for quirk in quirks:
def _inject_site_specific_quirks(self):
"""Add site-specific quirk scripts."""
if not config.val.content.site_specific_quirks.enabled:
return
for quirk in self._get_quirks():
if not quirk.predicate:
continue
src = resources.read_file(f'javascript/quirks/{quirk.filename}.user.js')
@ -1297,6 +1271,7 @@ class WebEngineTab(browsertab.AbstractTab):
_widget: QWebEngineView
search: WebEngineSearch
audio: WebEngineAudio
printing: WebEnginePrinting
def __init__(self, *, win_id, mode_manager, private, parent=None):
super().__init__(win_id=win_id,
@ -1311,7 +1286,7 @@ class WebEngineTab(browsertab.AbstractTab):
tab=self, parent=self)
self.zoom = WebEngineZoom(tab=self, parent=self)
self.search = WebEngineSearch(tab=self, parent=self)
self.printing = WebEnginePrinting(tab=self)
self.printing = WebEnginePrinting(tab=self, parent=self)
self.elements = WebEngineElements(tab=self)
self.action = WebEngineAction(tab=self)
self.audio = WebEngineAudio(tab=self, parent=self)
@ -1327,6 +1302,9 @@ class WebEngineTab(browsertab.AbstractTab):
self._child_event_filter = None
self._saved_zoom = None
self._scripts.init()
# WORKAROUND for https://bugreports.qt.io/browse/QTBUG-65223
self._needs_qtbug65223_workaround = (
version.qtwebengine_versions().webengine < utils.VersionNumber(5, 15, 5))
def _set_widget(self, widget):
# pylint: disable=protected-access
@ -1384,7 +1362,7 @@ class WebEngineTab(browsertab.AbstractTab):
def run_js_async(self, code, callback=None, *, world=None):
world_id_type = Union[QWebEngineScript.ScriptWorldId, int]
if world is None:
world_id: world_id_type = QWebEngineScript.ApplicationWorld
world_id: world_id_type = QWebEngineScript.ScriptWorldId.ApplicationWorld
elif isinstance(world, int):
world_id = world
if not 0 <= world_id <= qtutils.MAX_WORLD_ID:
@ -1401,9 +1379,9 @@ class WebEngineTab(browsertab.AbstractTab):
def reload(self, *, force=False):
if force:
action = QWebEnginePage.ReloadAndBypassCache
action = QWebEnginePage.WebAction.ReloadAndBypassCache
else:
action = QWebEnginePage.Reload
action = QWebEnginePage.WebAction.Reload
self._widget.triggerPageAction(action)
def stop(self):
@ -1412,13 +1390,9 @@ class WebEngineTab(browsertab.AbstractTab):
def title(self):
return self._widget.title()
def renderer_process_pid(self) -> Optional[int]:
def renderer_process_pid(self) -> int:
page = self._widget.page()
try:
return page.renderProcessPid()
except AttributeError:
# Added in Qt 5.15
return None
return page.renderProcessPid()
def icon(self):
return self._widget.icon()
@ -1460,7 +1434,7 @@ class WebEngineTab(browsertab.AbstractTab):
title_url = QUrl(url)
title_url.setScheme('')
title_url_str = title_url.toDisplayString(
QUrl.RemoveScheme) # type: ignore[arg-type]
QUrl.UrlFormattingOption.RemoveScheme) # type: ignore[arg-type]
if title == title_url_str.strip('/'):
title = ""
@ -1477,7 +1451,7 @@ class WebEngineTab(browsertab.AbstractTab):
"""Called when a proxy needs authentication."""
msg = "<b>{}</b> requires a username and password.".format(
html_utils.escape(proxy_host))
urlstr = url.toString(QUrl.RemovePassword | QUrl.FullyEncoded)
urlstr = url.toString(QUrl.UrlFormattingOption.RemovePassword | QUrl.ComponentFormattingOption.FullyEncoded)
answer = message.ask(
title="Proxy authentication required", text=msg,
mode=usertypes.PromptMode.user_pwd,
@ -1526,19 +1500,19 @@ class WebEngineTab(browsertab.AbstractTab):
@pyqtSlot(QWebEnginePage.RenderProcessTerminationStatus, int)
def _on_render_process_terminated(self, status, exitcode):
"""Show an error when the renderer process terminated."""
if (status == QWebEnginePage.AbnormalTerminationStatus and
if (status == QWebEnginePage.RenderProcessTerminationStatus.AbnormalTerminationStatus and
exitcode == 256):
# WORKAROUND for https://bugreports.qt.io/browse/QTBUG-58697
status = QWebEnginePage.CrashedTerminationStatus
status = QWebEnginePage.RenderProcessTerminationStatus.CrashedTerminationStatus
status_map = {
QWebEnginePage.NormalTerminationStatus:
QWebEnginePage.RenderProcessTerminationStatus.NormalTerminationStatus:
browsertab.TerminationStatus.normal,
QWebEnginePage.AbnormalTerminationStatus:
QWebEnginePage.RenderProcessTerminationStatus.AbnormalTerminationStatus:
browsertab.TerminationStatus.abnormal,
QWebEnginePage.CrashedTerminationStatus:
QWebEnginePage.RenderProcessTerminationStatus.CrashedTerminationStatus:
browsertab.TerminationStatus.crashed,
QWebEnginePage.KilledTerminationStatus:
QWebEnginePage.RenderProcessTerminationStatus.KilledTerminationStatus:
browsertab.TerminationStatus.killed,
-1:
browsertab.TerminationStatus.unknown,
@ -1568,24 +1542,25 @@ class WebEngineTab(browsertab.AbstractTab):
@pyqtSlot(int)
def _on_load_progress(self, perc: int) -> None:
"""QtWebEngine-specific loadProgress workarounds.
WORKAROUND for https://bugreports.qt.io/browse/QTBUG-65223
"""
"""QtWebEngine-specific loadProgress workarounds."""
super()._on_load_progress(perc)
if (perc == 100 and
self.load_status() != usertypes.LoadStatus.error):
if (
self._needs_qtbug65223_workaround and
perc == 100 and
self.load_status() != usertypes.LoadStatus.error
):
self._update_load_status(ok=True)
@pyqtSlot(bool)
def _on_load_finished(self, ok: bool) -> None:
"""QtWebEngine-specific loadFinished workarounds."""
"""QtWebEngine-specific loadFinished code."""
super()._on_load_finished(ok)
if not ok:
# WORKAROUND for https://bugreports.qt.io/browse/QTBUG-65223
if not self._needs_qtbug65223_workaround or not ok:
# With the workaround, this should only run with ok=False
self._update_load_status(ok)
if not ok:
self.dump_async(functools.partial(
self._error_page_workaround,
self.settings.test_attribute('content.javascript.enabled')))
@ -1605,7 +1580,7 @@ class WebEngineTab(browsertab.AbstractTab):
log.network.debug("First party URL: {}".format(first_party_url))
if error.is_overridable():
error.ignore = shared.ignore_certificate_error(
shared.handle_certificate_error(
request_url=url,
first_party_url=first_party_url,
error=error,
@ -1618,28 +1593,6 @@ class WebEngineTab(browsertab.AbstractTab):
log.network.debug("ignore {}, URL {}, requested {}".format(
error.ignore, url, self.url(requested=True)))
# WORKAROUND for https://codereview.qt-project.org/c/qt/qtwebengine/+/270556
show_non_overr_cert_error = (
not error.is_overridable() and (
# Affected Qt versions:
# 5.13 before 5.13.2
# 5.12 before 5.12.6
# < 5.12 (which is unsupported)
(qtutils.version_check('5.13') and
not qtutils.version_check('5.13.2')) or
(qtutils.version_check('5.12') and
not qtutils.version_check('5.12.6'))
)
)
# We can't really know when to show an error page, as the error might
# have happened when loading some resource.
is_resource = (
first_party_url.isValid() and
url.matches(first_party_url, QUrl.RemoveScheme))
if show_non_overr_cert_error and is_resource:
self._show_error_page(url, str(error))
@pyqtSlot()
def _on_print_requested(self):
"""Slot for window.print() in JS."""
@ -1648,27 +1601,30 @@ class WebEngineTab(browsertab.AbstractTab):
except browsertab.WebTabError as e:
message.error(str(e))
@pyqtSlot(QUrl)
def _on_url_changed(self, url: QUrl) -> None:
"""Update settings for the current URL.
Normally this is done below in _on_navigation_request, but we also need
to do it here as WORKAROUND for
https://bugreports.qt.io/browse/QTBUG-77137
Since update_for_url() is idempotent, it doesn't matter much if we end
up doing it twice.
"""
super()._on_url_changed(url)
if (url.isValid() and
qtutils.version_check('5.13') and
not qtutils.version_check('5.14')):
self.settings.update_for_url(url)
@pyqtSlot(usertypes.NavigationRequest)
def _on_navigation_request(self, navigation):
super()._on_navigation_request(navigation)
local_schemes = {"qute", "file"}
qtwe_ver = version.qtwebengine_versions().webengine
if (
navigation.accepted and
self.url().scheme().lower() in local_schemes and
navigation.url.scheme().lower() not in local_schemes and
(navigation.navigation_type ==
usertypes.NavigationRequest.Type.link_clicked) and
navigation.is_main_frame and
(utils.VersionNumber(6, 2) <= qtwe_ver < utils.VersionNumber(6, 2, 5) or
utils.VersionNumber(6, 3) <= qtwe_ver < utils.VersionNumber(6, 3, 1))
):
# WORKAROUND for https://bugreports.qt.io/browse/QTBUG-103778
log.webview.debug(
"Working around blocked request from local page "
f"{self.url().toDisplayString()}"
)
navigation.accepted = False
self.load_url(navigation.url)
if not navigation.accepted or not navigation.is_main_frame:
return
@ -1721,16 +1677,7 @@ class WebEngineTab(browsertab.AbstractTab):
page.contentsSizeChanged.connect(self.contents_size_changed)
page.navigation_request.connect(self._on_navigation_request)
page.printRequested.connect(self._on_print_requested)
try:
# pylint: disable=unused-import
from PyQt5.QtWebEngineWidgets import (
QWebEngineClientCertificateSelection)
except ImportError:
pass
else:
page.selectClientCertificate.connect(
self._on_select_client_certificate)
page.selectClientCertificate.connect(self._on_select_client_certificate)
view.titleChanged.connect(self.title_changed)
view.urlChanged.connect(self._on_url_changed)
@ -1741,12 +1688,7 @@ class WebEngineTab(browsertab.AbstractTab):
page.loadFinished.connect(self._on_history_trigger)
page.loadFinished.connect(self._restore_zoom)
page.loadFinished.connect(self._on_load_finished)
try:
page.renderProcessPidChanged.connect(self._on_renderer_process_pid_changed)
except AttributeError:
# Added in Qt 5.15.0
pass
page.renderProcessPidChanged.connect(self._on_renderer_process_pid_changed)
self.shutting_down.connect(self.abort_questions)
self.load_started.connect(self.abort_questions)
@ -1754,5 +1696,6 @@ class WebEngineTab(browsertab.AbstractTab):
# pylint: disable=protected-access
self.audio._connect_signals()
self.search.connect_signals()
self.printing.connect_signals()
self._permissions.connect_signals()
self._scripts.connect_signals()

View File

@ -21,9 +21,11 @@
from typing import List, Iterable
from PyQt5.QtCore import pyqtSignal, QUrl
from PyQt5.QtGui import QPalette
from PyQt5.QtWebEngineWidgets import QWebEngineView, QWebEnginePage
from qutebrowser.qt import machinery
from qutebrowser.qt.core import pyqtSignal, pyqtSlot, QUrl
from qutebrowser.qt.gui import QPalette
from qutebrowser.qt.webenginewidgets import QWebEngineView
from qutebrowser.qt.webenginecore import QWebEnginePage, QWebEngineCertificateError
from qutebrowser.browser import shared
from qutebrowser.browser.webengine import webenginesettings, certificateerror
@ -32,8 +34,8 @@ from qutebrowser.utils import log, debug, usertypes
_QB_FILESELECTION_MODES = {
QWebEnginePage.FileSelectOpen: shared.FileSelectionMode.single_file,
QWebEnginePage.FileSelectOpenMultiple: shared.FileSelectionMode.multiple_files,
QWebEnginePage.FileSelectionMode.FileSelectOpen: shared.FileSelectionMode.single_file,
QWebEnginePage.FileSelectionMode.FileSelectOpenMultiple: shared.FileSelectionMode.multiple_files,
# WORKAROUND for https://bugreports.qt.io/browse/QTBUG-91489
#
# QtWebEngine doesn't expose this value from its internal
@ -54,7 +56,7 @@ class WebEngineView(QWebEngineView):
self._win_id = win_id
self._tabdata = tabdata
theme_color = self.style().standardPalette().color(QPalette.Base)
theme_color = self.style().standardPalette().color(QPalette.ColorRole.Base)
if private:
assert webenginesettings.private_profile is not None
profile = webenginesettings.private_profile
@ -106,21 +108,21 @@ class WebEngineView(QWebEngineView):
log.webview.debug("createWindow with type {}, background {}".format(
debug_type, background))
if wintype == QWebEnginePage.WebBrowserWindow:
if wintype == QWebEnginePage.WebWindowType.WebBrowserWindow:
# Shift-Alt-Click
target = usertypes.ClickTarget.window
elif wintype == QWebEnginePage.WebDialog:
elif wintype == QWebEnginePage.WebWindowType.WebDialog:
log.webview.warning("{} requested, but we don't support "
"that!".format(debug_type))
target = usertypes.ClickTarget.tab
elif wintype == QWebEnginePage.WebBrowserTab:
elif wintype == QWebEnginePage.WebWindowType.WebBrowserTab:
# Middle-click / Ctrl-Click with Shift
# FIXME:qtwebengine this also affects target=_blank links...
if background:
target = usertypes.ClickTarget.tab
else:
target = usertypes.ClickTarget.tab_bg
elif wintype == QWebEnginePage.WebBrowserBackgroundTab:
elif wintype == QWebEnginePage.WebWindowType.WebBrowserBackgroundTab:
# Middle-click / Ctrl-Click
if background:
target = usertypes.ClickTarget.tab_bg
@ -150,8 +152,9 @@ class WebEnginePage(QWebEnginePage):
Signals:
certificate_error: Emitted on certificate errors.
Needs to be directly connected to a slot setting the
'ignore' attribute.
Needs to be directly connected to a slot calling
.accept_certificate(), .reject_certificate, or
.defer().
shutting_down: Emitted when the page is shutting down.
navigation_request: Emitted on acceptNavigationRequest.
"""
@ -160,12 +163,43 @@ class WebEnginePage(QWebEnginePage):
shutting_down = pyqtSignal()
navigation_request = pyqtSignal(usertypes.NavigationRequest)
_JS_LOG_LEVEL_MAPPING = {
QWebEnginePage.JavaScriptConsoleMessageLevel.InfoMessageLevel:
usertypes.JsLogLevel.info,
QWebEnginePage.JavaScriptConsoleMessageLevel.WarningMessageLevel:
usertypes.JsLogLevel.warning,
QWebEnginePage.JavaScriptConsoleMessageLevel.ErrorMessageLevel:
usertypes.JsLogLevel.error,
}
_NAVIGATION_TYPE_MAPPING = {
QWebEnginePage.NavigationType.NavigationTypeLinkClicked:
usertypes.NavigationRequest.Type.link_clicked,
QWebEnginePage.NavigationType.NavigationTypeTyped:
usertypes.NavigationRequest.Type.typed,
QWebEnginePage.NavigationType.NavigationTypeFormSubmitted:
usertypes.NavigationRequest.Type.form_submitted,
QWebEnginePage.NavigationType.NavigationTypeBackForward:
usertypes.NavigationRequest.Type.back_forward,
QWebEnginePage.NavigationType.NavigationTypeReload:
usertypes.NavigationRequest.Type.reload,
QWebEnginePage.NavigationType.NavigationTypeOther:
usertypes.NavigationRequest.Type.other,
QWebEnginePage.NavigationType.NavigationTypeRedirect:
usertypes.NavigationRequest.Type.redirect,
}
def __init__(self, *, theme_color, profile, parent=None):
super().__init__(profile, parent)
self._is_shutting_down = False
self._theme_color = theme_color
self._set_bg_color()
config.instance.changed.connect(self._set_bg_color)
if machinery.IS_QT6:
self.certificateError.connect( # pylint: disable=no-member
self._handle_certificate_error
)
# Qt 5: Overridden method instead of signal
@config.change_filter('colors.webpage.bg')
def _set_bg_color(self):
@ -178,11 +212,17 @@ class WebEnginePage(QWebEnginePage):
self._is_shutting_down = True
self.shutting_down.emit()
def certificateError(self, error):
@pyqtSlot(QWebEngineCertificateError)
def _handle_certificate_error(self, qt_error):
"""Handle certificate errors coming from Qt."""
error = certificateerror.CertificateErrorWrapper(error)
error = certificateerror.CertificateErrorWrapper(qt_error)
self.certificate_error.emit(error)
return error.ignore
# Right now, we never defer accepting, due to a PyQt bug
return error.certificate_was_accepted()
if machinery.IS_QT5:
# Overridden method instead of signal
certificateError = _handle_certificate_error # noqa: N815
def javaScriptConfirm(self, url, js_msg):
"""Override javaScriptConfirm to use qutebrowser prompts."""
@ -216,42 +256,16 @@ class WebEnginePage(QWebEnginePage):
def javaScriptConsoleMessage(self, level, msg, line, source):
"""Log javascript messages to qutebrowser's log."""
level_map = {
QWebEnginePage.InfoMessageLevel: usertypes.JsLogLevel.info,
QWebEnginePage.WarningMessageLevel: usertypes.JsLogLevel.warning,
QWebEnginePage.ErrorMessageLevel: usertypes.JsLogLevel.error,
}
shared.javascript_log_message(level_map[level], source, line, msg)
shared.javascript_log_message(self._JS_LOG_LEVEL_MAPPING[level], source, line, msg)
def acceptNavigationRequest(self,
url: QUrl,
typ: QWebEnginePage.NavigationType,
is_main_frame: bool) -> bool:
"""Override acceptNavigationRequest to forward it to the tab API."""
type_map = {
QWebEnginePage.NavigationTypeLinkClicked:
usertypes.NavigationRequest.Type.link_clicked,
QWebEnginePage.NavigationTypeTyped:
usertypes.NavigationRequest.Type.typed,
QWebEnginePage.NavigationTypeFormSubmitted:
usertypes.NavigationRequest.Type.form_submitted,
QWebEnginePage.NavigationTypeBackForward:
usertypes.NavigationRequest.Type.back_forward,
QWebEnginePage.NavigationTypeReload:
usertypes.NavigationRequest.Type.reloaded,
QWebEnginePage.NavigationTypeOther:
usertypes.NavigationRequest.Type.other,
}
try:
type_map[QWebEnginePage.NavigationTypeRedirect] = (
usertypes.NavigationRequest.Type.redirect)
except AttributeError:
# Added in Qt 5.14
pass
navigation = usertypes.NavigationRequest(
url=url,
navigation_type=type_map.get(
navigation_type=self._NAVIGATION_TYPE_MAPPING.get(
typ, usertypes.NavigationRequest.Type.other),
is_main_frame=is_main_frame)
self.navigation_request.emit(navigation)

View File

@ -22,7 +22,7 @@
from typing import cast
import os.path
from PyQt5.QtNetwork import QNetworkDiskCache
from qutebrowser.qt.network import QNetworkDiskCache
from qutebrowser.config import config
from qutebrowser.utils import utils, standarddir

View File

@ -19,19 +19,25 @@
"""A wrapper over a list of QSslErrors."""
from typing import Sequence
from typing import Sequence, Optional
from PyQt5.QtNetwork import QSslError
from qutebrowser.qt.network import QSslError, QNetworkReply
from qutebrowser.utils import usertypes, utils, debug, jinja
from qutebrowser.utils import usertypes, utils, debug, jinja, urlutils
class CertificateErrorWrapper(usertypes.AbstractCertificateErrorWrapper):
"""A wrapper over a list of QSslErrors."""
def __init__(self, errors: Sequence[QSslError]) -> None:
def __init__(self, reply: QNetworkReply, errors: Sequence[QSslError]) -> None:
super().__init__()
self._reply = reply
self._errors = tuple(errors) # needs to be hashable
try:
self._host_tpl: Optional[urlutils.HostTupleType] = urlutils.host_tuple(reply.url())
except ValueError:
self._host_tpl = None
def __str__(self) -> str:
return '\n'.join(err.errorString() for err in self._errors)
@ -43,16 +49,25 @@ class CertificateErrorWrapper(usertypes.AbstractCertificateErrorWrapper):
string=str(self))
def __hash__(self) -> int:
return hash(self._errors)
return hash((self._host_tpl, self._errors))
def __eq__(self, other: object) -> bool:
if not isinstance(other, CertificateErrorWrapper):
return NotImplemented
return self._errors == other._errors
return self._errors == other._errors and self._host_tpl == other._host_tpl
def is_overridable(self) -> bool:
return True
def defer(self) -> None:
raise usertypes.UndeferrableError("Never deferrable")
def accept_certificate(self) -> None:
super().accept_certificate()
self._reply.ignoreSslErrors()
# Not overriding reject_certificate because that's default in QNetworkReply
def html(self):
if len(self._errors) == 1:
return super().html()

View File

@ -21,8 +21,8 @@
from typing import Sequence
from PyQt5.QtNetwork import QNetworkCookie, QNetworkCookieJar
from PyQt5.QtCore import pyqtSignal, QDateTime
from qutebrowser.qt.network import QNetworkCookie, QNetworkCookieJar
from qutebrowser.qt.core import pyqtSignal, QDateTime
from qutebrowser.config import config
from qutebrowser.utils import utils, standarddir, objreg, log

View File

@ -25,7 +25,7 @@ import dataclasses
import os.path
from typing import Type
from PyQt5.QtNetwork import QNetworkRequest
from qutebrowser.qt.network import QNetworkRequest
from qutebrowser.utils import log, utils
@ -191,7 +191,7 @@ def parse_content_type(reply):
A [mimetype, rest] list, or [None, None] if unset.
Rest can be None.
"""
content_type = reply.header(QNetworkRequest.ContentTypeHeader)
content_type = reply.header(QNetworkRequest.KnownHeaders.ContentTypeHeader)
if content_type is None:
return [None, None]
if ';' in content_type:

View File

@ -36,7 +36,7 @@ import quopri
import dataclasses
from typing import MutableMapping, Set, Tuple, Callable
from PyQt5.QtCore import QUrl
from qutebrowser.qt.core import QUrl
from qutebrowser.browser import downloads
from qutebrowser.browser.webkit import webkitelem

View File

@ -24,8 +24,8 @@ import html
import dataclasses
from typing import TYPE_CHECKING, Dict, MutableMapping, Optional, Set
from PyQt5.QtCore import pyqtSlot, pyqtSignal, QUrl, QByteArray
from PyQt5.QtNetwork import (QNetworkAccessManager, QNetworkReply, QSslConfiguration,
from qutebrowser.qt.core import pyqtSlot, pyqtSignal, QUrl, QByteArray
from qutebrowser.qt.network import (QNetworkAccessManager, QNetworkReply, QSslConfiguration,
QNetworkProxy)
from qutebrowser.config import config
@ -79,7 +79,7 @@ def _is_secure_cipher(cipher):
return False
# OpenSSL should already protect against this in a better way
# elif (('CBC3' in tokens or 'CBC' in tokens) and (cipher.protocol() not in
# [QSsl.TlsV1_0, QSsl.TlsV1_1, QSsl.TlsV1_2])):
# [QSsl.SslProtocol.TlsV1_0, QSsl.SslProtocol.TlsV1_1, QSsl.SslProtocol.TlsV1_2])):
# # https://en.wikipedia.org/wiki/POODLE
# return False
### These things should never happen as those are already filtered out by
@ -103,8 +103,8 @@ def _is_secure_cipher(cipher):
def init():
"""Disable insecure SSL ciphers on old Qt versions."""
sslconfig = QSslConfiguration.defaultConfiguration()
default_ciphers = sslconfig.ciphers()
ssl_config = QSslConfiguration.defaultConfiguration()
default_ciphers = ssl_config.ciphers()
log.init.vdebug( # type: ignore[attr-defined]
"Default Qt ciphers: {}".format(
', '.join(c.name() for c in default_ciphers)))
@ -120,7 +120,7 @@ def init():
if bad_ciphers:
log.init.debug("Disabling bad ciphers: {}".format(
', '.join(c.name() for c in bad_ciphers)))
sslconfig.setCiphers(good_ciphers)
ssl_config.setCiphers(good_ciphers)
_SavedErrorsType = MutableMapping[
@ -239,12 +239,16 @@ class NetworkManager(QNetworkAccessManager):
def shutdown(self):
"""Abort all running requests."""
self.setNetworkAccessible(QNetworkAccessManager.NotAccessible)
try:
self.setNetworkAccessible(QNetworkAccessManager.NetworkAccessibility.NotAccessible)
except AttributeError:
# Qt 5 only, deprecated seemingly without replacement.
pass
self.shutting_down.emit()
# No @pyqtSlot here, see
# https://github.com/qutebrowser/qutebrowser/issues/2213
def on_ssl_errors(self, reply, qt_errors): # noqa: C901 pragma: no mccabe
def on_ssl_errors(self, reply, qt_errors):
"""Decide if SSL errors should be ignored or not.
This slot is called on SSL/TLS errors by the self.sslErrors signal.
@ -253,7 +257,7 @@ class NetworkManager(QNetworkAccessManager):
reply: The QNetworkReply that is encountering the errors.
qt_errors: A list of errors.
"""
errors = certificateerror.CertificateErrorWrapper(qt_errors)
errors = certificateerror.CertificateErrorWrapper(reply, qt_errors)
log.network.debug("Certificate errors: {!r}".format(errors))
try:
host_tpl: Optional[urlutils.HostTupleType] = urlutils.host_tuple(
@ -281,14 +285,14 @@ class NetworkManager(QNetworkAccessManager):
tab = self._get_tab()
first_party_url = QUrl() if tab is None else tab.data.last_navigation.url
ignore = shared.ignore_certificate_error(
shared.handle_certificate_error(
request_url=reply.url(),
first_party_url=first_party_url,
error=errors,
abort_on=abort_on,
)
if ignore:
reply.ignoreSslErrors()
if errors.certificate_was_accepted():
if host_tpl is not None:
self._accepted_ssl_errors[host_tpl].add(errors)
elif host_tpl is not None:
@ -406,14 +410,14 @@ class NetworkManager(QNetworkAccessManager):
proxy_error = proxymod.application_factory.get_error()
if proxy_error is not None:
return networkreply.ErrorNetworkReply(
req, proxy_error, QNetworkReply.UnknownProxyError,
req, proxy_error, QNetworkReply.NetworkError.UnknownProxyError,
self)
if not req.url().isValid():
log.network.debug("Ignoring invalid requested URL: {}".format(
req.url().errorString()))
return networkreply.ErrorNetworkReply(
req, "Invalid request URL", QNetworkReply.HostNotFoundError,
req, "Invalid request URL", QNetworkReply.NetworkError.HostNotFoundError,
self)
for header, value in shared.custom_headers(url=req.url()):
@ -433,7 +437,7 @@ class NetworkManager(QNetworkAccessManager):
interceptors.run(request)
if request.is_blocked:
return networkreply.ErrorNetworkReply(
req, HOSTBLOCK_ERROR_STRING, QNetworkReply.ContentAccessDenied,
req, HOSTBLOCK_ERROR_STRING, QNetworkReply.NetworkError.ContentAccessDenied,
self)
if 'log-requests' in objects.debug_flags:

View File

@ -26,8 +26,8 @@
"""Special network replies.."""
from PyQt5.QtNetwork import QNetworkReply, QNetworkRequest
from PyQt5.QtCore import pyqtSlot, QIODevice, QByteArray, QTimer
from qutebrowser.qt.network import QNetworkReply, QNetworkRequest
from qutebrowser.qt.core import pyqtSlot, QIODevice, QByteArray, QTimer
class FixedDataNetworkReply(QNetworkReply):
@ -49,13 +49,13 @@ class FixedDataNetworkReply(QNetworkReply):
self.setRequest(request)
self.setUrl(request.url())
self.setOpenMode(QIODevice.ReadOnly)
self.setOpenMode(QIODevice.OpenModeFlag.ReadOnly)
self.setHeader(QNetworkRequest.ContentTypeHeader, mimeType)
self.setHeader(QNetworkRequest.ContentLengthHeader,
self.setHeader(QNetworkRequest.KnownHeaders.ContentTypeHeader, mimeType)
self.setHeader(QNetworkRequest.KnownHeaders.ContentLengthHeader,
QByteArray.number(len(fileData)))
self.setAttribute(QNetworkRequest.HttpStatusCodeAttribute, 200)
self.setAttribute(QNetworkRequest.HttpReasonPhraseAttribute, 'OK')
self.setAttribute(QNetworkRequest.Attribute.HttpStatusCodeAttribute, 200)
self.setAttribute(QNetworkRequest.Attribute.HttpReasonPhraseAttribute, 'OK')
# For some reason, a segfault will be triggered if these lambdas aren't
# there.
# pylint: disable=unnecessary-lambda
@ -114,9 +114,9 @@ class ErrorNetworkReply(QNetworkReply):
self.setUrl(req.url())
# We don't actually want to read anything, but we still need to open
# the device to avoid getting a warning.
self.setOpenMode(QIODevice.ReadOnly)
self.setOpenMode(QIODevice.OpenModeFlag.ReadOnly)
self.setError(error, errorstring)
QTimer.singleShot(0, lambda: self.error.emit(error))
QTimer.singleShot(0, lambda: self.errorOccurred.emit(error))
QTimer.singleShot(0, lambda: self.finished.emit())
def abort(self):
@ -143,7 +143,7 @@ class RedirectNetworkReply(QNetworkReply):
def __init__(self, new_url, parent=None):
super().__init__(parent)
self.setAttribute(QNetworkRequest.RedirectionTargetAttribute, new_url)
self.setAttribute(QNetworkRequest.Attribute.RedirectionTargetAttribute, new_url)
QTimer.singleShot(0, lambda: self.finished.emit())
def abort(self):

View File

@ -19,8 +19,8 @@
"""QtWebKit specific qute://* handlers and glue code."""
from PyQt5.QtCore import QUrl
from PyQt5.QtNetwork import QNetworkReply, QNetworkAccessManager
from qutebrowser.qt.core import QUrl
from qutebrowser.qt.network import QNetworkReply, QNetworkAccessManager
from qutebrowser.browser import qutescheme
from qutebrowser.browser.webkit.network import networkreply
@ -38,10 +38,10 @@ def handler(request, operation, current_url):
Return:
A QNetworkReply.
"""
if operation != QNetworkAccessManager.GetOperation:
if operation != QNetworkAccessManager.Operation.GetOperation:
return networkreply.ErrorNetworkReply(
request, "Unsupported request type",
QNetworkReply.ContentOperationNotPermittedError)
QNetworkReply.NetworkError.ContentOperationNotPermittedError)
url = request.url()
@ -53,22 +53,22 @@ def handler(request, operation, current_url):
url.toDisplayString()))
return networkreply.ErrorNetworkReply(
request, "Invalid qute://settings request",
QNetworkReply.ContentAccessDenied)
QNetworkReply.NetworkError.ContentAccessDenied)
try:
mimetype, data = qutescheme.data_for_url(url)
except qutescheme.Error as e:
errors = {
qutescheme.NotFoundError:
QNetworkReply.ContentNotFoundError,
QNetworkReply.NetworkError.ContentNotFoundError,
qutescheme.UrlInvalidError:
QNetworkReply.ContentOperationNotPermittedError,
QNetworkReply.NetworkError.ContentOperationNotPermittedError,
qutescheme.RequestDeniedError:
QNetworkReply.ContentAccessDenied,
QNetworkReply.NetworkError.ContentAccessDenied,
qutescheme.SchemeOSError:
QNetworkReply.ContentNotFoundError,
QNetworkReply.NetworkError.ContentNotFoundError,
qutescheme.Error:
QNetworkReply.InternalServerError,
QNetworkReply.NetworkError.InternalServerError,
}
exctype = type(e)
log.misc.error("{} while handling qute://* URL".format(

View File

@ -21,7 +21,7 @@
from typing import Any, List, Mapping
from PyQt5.QtCore import QByteArray, QDataStream, QIODevice, QUrl
from qutebrowser.qt.core import QByteArray, QDataStream, QIODevice, QUrl
from qutebrowser.utils import qtutils
@ -50,10 +50,10 @@ def _serialize_items(items, current_idx, stream):
def _serialize_item(item):
data = {
'originalURLString': item.original_url.toString(QUrl.FullyEncoded),
'originalURLString': item.original_url.toString(QUrl.ComponentFormattingOption.FullyEncoded),
'scrollPosition': {'x': 0, 'y': 0},
'title': item.title,
'urlString': item.url.toString(QUrl.FullyEncoded),
'urlString': item.url.toString(QUrl.ComponentFormattingOption.FullyEncoded),
}
try:
data['scrollPosition']['x'] = item.user_data['scroll-pos'].x()
@ -80,7 +80,7 @@ def serialize(items):
segfault!
"""
data = QByteArray()
stream = QDataStream(data, QIODevice.ReadWrite)
stream = QDataStream(data, QIODevice.OpenModeFlag.ReadWrite)
user_data: List[Mapping[str, Any]] = []
current_idx = None

View File

@ -17,13 +17,16 @@
# You should have received a copy of the GNU General Public License
# along with qutebrowser. If not, see <https://www.gnu.org/licenses/>.
# FIXME:qt6 (lint)
# pylint: disable=no-name-in-module
"""QtWebKit specific part of the web element API."""
from typing import cast, TYPE_CHECKING, Iterator, List, Optional, Set
from PyQt5.QtCore import QRect, Qt
from PyQt5.QtWebKit import QWebElement, QWebSettings
from PyQt5.QtWebKitWidgets import QWebFrame
from qutebrowser.qt.core import QRect, Qt
from qutebrowser.qt.webkit import QWebElement, QWebSettings
from qutebrowser.qt.webkitwidgets import QWebFrame
from qutebrowser.config import config
from qutebrowser.utils import log, utils, javascript, usertypes
@ -276,7 +279,7 @@ class WebKitElement(webelem.AbstractWebElement):
def _is_hidden_css(self) -> bool:
"""Check if the given element is hidden via CSS."""
attr_values = {
attr: self._elem.styleProperty(attr, QWebElement.ComputedStyle)
attr: self._elem.styleProperty(attr, QWebElement.StyleResolveStrategy.ComputedStyle)
for attr in ['visibility', 'display', 'opacity']
}
invisible = attr_values['visibility'] == 'hidden'
@ -362,7 +365,7 @@ class WebKitElement(webelem.AbstractWebElement):
def _click_js(self, click_target: usertypes.ClickTarget) -> None:
settings = QWebSettings.globalSettings()
attribute = QWebSettings.JavascriptCanOpenWindows
attribute = QWebSettings.WebAttribute.JavascriptCanOpenWindows
could_open_windows = settings.testAttribute(attribute)
settings.setAttribute(attribute, True)
ok = self._elem.evaluateJavaScript('this.click(); true;')
@ -372,7 +375,7 @@ class WebKitElement(webelem.AbstractWebElement):
self._click_fake_event(click_target)
def _click_fake_event(self, click_target: usertypes.ClickTarget,
button: Qt.MouseButton = Qt.LeftButton) -> None:
button: Qt.MouseButton = Qt.MouseButton.LeftButton) -> None:
self._tab.data.override_target = click_target
super()._click_fake_event(click_target)

View File

@ -17,11 +17,14 @@
# You should have received a copy of the GNU General Public License
# along with qutebrowser. If not, see <https://www.gnu.org/licenses/>.
# FIXME:qt6 (lint)
# pylint: disable=no-name-in-module
"""QtWebKit specific part of history."""
import functools
from PyQt5.QtWebKit import QWebHistoryInterface
from qutebrowser.qt.webkit import QWebHistoryInterface
from qutebrowser.utils import debug
from qutebrowser.misc import debugcachestats

View File

@ -17,11 +17,14 @@
# You should have received a copy of the GNU General Public License
# along with qutebrowser. If not, see <https://www.gnu.org/licenses/>.
# FIXME:qt6 (lint)
# pylint: disable=no-name-in-module
"""Customized QWebInspector for QtWebKit."""
from PyQt5.QtWebKit import QWebSettings
from PyQt5.QtWebKitWidgets import QWebInspector, QWebPage
from PyQt5.QtWidgets import QWidget
from qutebrowser.qt.webkit import QWebSettings
from qutebrowser.qt.webkitwidgets import QWebInspector, QWebPage
from qutebrowser.qt.widgets import QWidget
from qutebrowser.browser import inspector
from qutebrowser.misc import miscwidgets
@ -40,5 +43,5 @@ class WebKitInspector(inspector.AbstractWebInspector):
def inspect(self, page: QWebPage) -> None:
settings = QWebSettings.globalSettings()
settings.setAttribute(QWebSettings.DeveloperExtrasEnabled, True)
settings.setAttribute(QWebSettings.WebAttribute.DeveloperExtrasEnabled, True)
self._widget.setPage(page)

View File

@ -17,6 +17,9 @@
# You should have received a copy of the GNU General Public License
# along with qutebrowser. If not, see <https://www.gnu.org/licenses/>.
# FIXME:qt6 (lint)
# pylint: disable=no-name-in-module
"""Bridge from QWebSettings to our own settings.
Module attributes:
@ -27,10 +30,10 @@ Module attributes:
from typing import cast
import os.path
from PyQt5.QtCore import QUrl
from PyQt5.QtGui import QFont
from PyQt5.QtWebKit import QWebSettings
from PyQt5.QtWebKitWidgets import QWebPage
from qutebrowser.qt.core import QUrl
from qutebrowser.qt.gui import QFont
from qutebrowser.qt.webkit import QWebSettings
from qutebrowser.qt.webkitwidgets import QWebPage
from qutebrowser.config import config, websettings
from qutebrowser.config.websettings import AttributeInfo as Attr
@ -50,82 +53,82 @@ class WebKitSettings(websettings.AbstractSettings):
_ATTRIBUTES = {
'content.images':
Attr(QWebSettings.AutoLoadImages),
Attr(QWebSettings.WebAttribute.AutoLoadImages),
'content.javascript.enabled':
Attr(QWebSettings.JavascriptEnabled),
Attr(QWebSettings.WebAttribute.JavascriptEnabled),
'content.javascript.can_open_tabs_automatically':
Attr(QWebSettings.JavascriptCanOpenWindows),
Attr(QWebSettings.WebAttribute.JavascriptCanOpenWindows),
'content.javascript.can_close_tabs':
Attr(QWebSettings.JavascriptCanCloseWindows),
Attr(QWebSettings.WebAttribute.JavascriptCanCloseWindows),
'content.javascript.clipboard':
Attr(QWebSettings.JavascriptCanAccessClipboard,
Attr(QWebSettings.WebAttribute.JavascriptCanAccessClipboard,
converter=lambda val: val != "none"),
'content.plugins':
Attr(QWebSettings.PluginsEnabled),
Attr(QWebSettings.WebAttribute.PluginsEnabled),
'content.webgl':
Attr(QWebSettings.WebGLEnabled),
Attr(QWebSettings.WebAttribute.WebGLEnabled),
'content.hyperlink_auditing':
Attr(QWebSettings.HyperlinkAuditingEnabled),
Attr(QWebSettings.WebAttribute.HyperlinkAuditingEnabled),
'content.local_content_can_access_remote_urls':
Attr(QWebSettings.LocalContentCanAccessRemoteUrls),
Attr(QWebSettings.WebAttribute.LocalContentCanAccessRemoteUrls),
'content.local_content_can_access_file_urls':
Attr(QWebSettings.LocalContentCanAccessFileUrls),
Attr(QWebSettings.WebAttribute.LocalContentCanAccessFileUrls),
'content.dns_prefetch':
Attr(QWebSettings.DnsPrefetchEnabled),
Attr(QWebSettings.WebAttribute.DnsPrefetchEnabled),
'content.frame_flattening':
Attr(QWebSettings.FrameFlatteningEnabled),
Attr(QWebSettings.WebAttribute.FrameFlatteningEnabled),
'content.cache.appcache':
Attr(QWebSettings.OfflineWebApplicationCacheEnabled),
Attr(QWebSettings.WebAttribute.OfflineWebApplicationCacheEnabled),
'content.local_storage':
Attr(QWebSettings.LocalStorageEnabled,
QWebSettings.OfflineStorageDatabaseEnabled),
Attr(QWebSettings.WebAttribute.LocalStorageEnabled,
QWebSettings.WebAttribute.OfflineStorageDatabaseEnabled),
'content.print_element_backgrounds':
Attr(QWebSettings.PrintElementBackgrounds),
Attr(QWebSettings.WebAttribute.PrintElementBackgrounds),
'content.xss_auditing':
Attr(QWebSettings.XSSAuditingEnabled),
Attr(QWebSettings.WebAttribute.XSSAuditingEnabled),
'content.site_specific_quirks.enabled':
Attr(QWebSettings.SiteSpecificQuirksEnabled),
Attr(QWebSettings.WebAttribute.SiteSpecificQuirksEnabled),
'input.spatial_navigation':
Attr(QWebSettings.SpatialNavigationEnabled),
Attr(QWebSettings.WebAttribute.SpatialNavigationEnabled),
'input.links_included_in_focus_chain':
Attr(QWebSettings.LinksIncludedInFocusChain),
Attr(QWebSettings.WebAttribute.LinksIncludedInFocusChain),
'zoom.text_only':
Attr(QWebSettings.ZoomTextOnly),
Attr(QWebSettings.WebAttribute.ZoomTextOnly),
'scrolling.smooth':
Attr(QWebSettings.ScrollAnimatorEnabled),
Attr(QWebSettings.WebAttribute.ScrollAnimatorEnabled),
}
_FONT_SIZES = {
'fonts.web.size.minimum':
QWebSettings.MinimumFontSize,
QWebSettings.FontSize.MinimumFontSize,
'fonts.web.size.minimum_logical':
QWebSettings.MinimumLogicalFontSize,
QWebSettings.FontSize.MinimumLogicalFontSize,
'fonts.web.size.default':
QWebSettings.DefaultFontSize,
QWebSettings.FontSize.DefaultFontSize,
'fonts.web.size.default_fixed':
QWebSettings.DefaultFixedFontSize,
QWebSettings.FontSize.DefaultFixedFontSize,
}
_FONT_FAMILIES = {
'fonts.web.family.standard': QWebSettings.StandardFont,
'fonts.web.family.fixed': QWebSettings.FixedFont,
'fonts.web.family.serif': QWebSettings.SerifFont,
'fonts.web.family.sans_serif': QWebSettings.SansSerifFont,
'fonts.web.family.cursive': QWebSettings.CursiveFont,
'fonts.web.family.fantasy': QWebSettings.FantasyFont,
'fonts.web.family.standard': QWebSettings.FontFamily.StandardFont,
'fonts.web.family.fixed': QWebSettings.FontFamily.FixedFont,
'fonts.web.family.serif': QWebSettings.FontFamily.SerifFont,
'fonts.web.family.sans_serif': QWebSettings.FontFamily.SansSerifFont,
'fonts.web.family.cursive': QWebSettings.FontFamily.CursiveFont,
'fonts.web.family.fantasy': QWebSettings.FontFamily.FantasyFont,
}
# Mapping from QWebSettings::QWebSettings() in
# qtwebkit/Source/WebKit/qt/Api/qwebsettings.cpp
_FONT_TO_QFONT = {
QWebSettings.StandardFont: QFont.Serif,
QWebSettings.FixedFont: QFont.Monospace,
QWebSettings.SerifFont: QFont.Serif,
QWebSettings.SansSerifFont: QFont.SansSerif,
QWebSettings.CursiveFont: QFont.Cursive,
QWebSettings.FantasyFont: QFont.Fantasy,
QWebSettings.FontFamily.StandardFont: QFont.StyleHint.Serif,
QWebSettings.FontFamily.FixedFont: QFont.StyleHint.Monospace,
QWebSettings.FontFamily.SerifFont: QFont.StyleHint.Serif,
QWebSettings.FontFamily.SansSerifFont: QFont.StyleHint.SansSerif,
QWebSettings.FontFamily.CursiveFont: QFont.StyleHint.Cursive,
QWebSettings.FontFamily.FantasyFont: QFont.StyleHint.Fantasy,
}
@ -139,10 +142,10 @@ def _set_user_stylesheet(settings):
def _set_cookie_accept_policy(settings):
"""Update the content.cookies.accept setting."""
mapping = {
'all': QWebSettings.AlwaysAllowThirdPartyCookies,
'no-3rdparty': QWebSettings.AlwaysBlockThirdPartyCookies,
'never': QWebSettings.AlwaysBlockThirdPartyCookies,
'no-unknown-3rdparty': QWebSettings.AllowThirdPartyWithExistingCookies,
'all': QWebSettings.ThirdPartyCookiePolicy.AlwaysAllowThirdPartyCookies,
'no-3rdparty': QWebSettings.ThirdPartyCookiePolicy.AlwaysBlockThirdPartyCookies,
'never': QWebSettings.ThirdPartyCookiePolicy.AlwaysBlockThirdPartyCookies,
'no-unknown-3rdparty': QWebSettings.ThirdPartyCookiePolicy.AllowThirdPartyWithExistingCookies,
}
value = config.val.content.cookies.accept
settings.setThirdPartyCookiePolicy(mapping[value])

View File

@ -17,6 +17,9 @@
# You should have received a copy of the GNU General Public License
# along with qutebrowser. If not, see <https://www.gnu.org/licenses/>.
# FIXME:qt6 (lint)
# pylint: disable=no-name-in-module
"""Wrapper over our (QtWebKit) WebView."""
import re
@ -24,12 +27,12 @@ import functools
import xml.etree.ElementTree
from typing import cast, Iterable, Optional
from PyQt5.QtCore import pyqtSlot, Qt, QUrl, QPoint, QTimer, QSizeF, QSize
from PyQt5.QtGui import QIcon
from PyQt5.QtWidgets import QWidget
from PyQt5.QtWebKitWidgets import QWebPage, QWebFrame
from PyQt5.QtWebKit import QWebSettings, QWebHistory, QWebElement
from PyQt5.QtPrintSupport import QPrinter
from qutebrowser.qt.core import pyqtSlot, Qt, QUrl, QPoint, QTimer, QSizeF, QSize
from qutebrowser.qt.gui import QIcon
from qutebrowser.qt.widgets import QWidget
from qutebrowser.qt.webkitwidgets import QWebPage, QWebFrame
from qutebrowser.qt.webkit import QWebSettings, QWebHistory, QWebElement
from qutebrowser.qt.printsupport import QPrinter
from qutebrowser.browser import browsertab, shared
from qutebrowser.browser.webkit import (webview, webpage, tabhistory, webkitelem,
@ -44,7 +47,6 @@ class WebKitAction(browsertab.AbstractAction):
"""QtWebKit implementations related to web actions."""
action_class = QWebPage
action_base = QWebPage.WebAction
_widget: webview.WebView
@ -67,9 +69,9 @@ class WebKitAction(browsertab.AbstractAction):
"""
new_actions = {
# https://github.com/qtwebkit/qtwebkit/commit/a96d9ef5d24b02d996ad14ff050d0e485c9ddc97
'RequestClose': QWebPage.ToggleVideoFullscreen + 1,
'RequestClose': QWebPage.WebAction.ToggleVideoFullscreen + 1,
# https://github.com/qtwebkit/qtwebkit/commit/96b9ba6269a5be44343635a7aaca4a153ea0366b
'Unselect': QWebPage.ToggleVideoFullscreen + 2,
'Unselect': QWebPage.WebAction.ToggleVideoFullscreen + 2,
}
if name in new_actions:
self._widget.triggerPageAction(new_actions[name]) # type: ignore[arg-type]
@ -93,13 +95,14 @@ class WebKitPrinting(browsertab.AbstractPrinting):
def to_pdf(self, filename):
printer = QPrinter()
printer.setOutputFileName(filename)
self.to_printer(printer)
def to_printer(self, printer, callback=None):
self._widget.print(printer)
# Can't find out whether there was an error...
if callback is not None:
callback(True)
self.pdf_printing_finished.emit(filename, True)
def to_printer(self, printer):
self._widget.print(printer)
# Can't find out whether there was an error...
self.printing_finished.emit(True)
class WebKitSearch(browsertab.AbstractSearch):
@ -118,9 +121,9 @@ class WebKitSearch(browsertab.AbstractSearch):
def _args_to_flags(self, reverse, ignore_case):
flags = self._empty_flags()
if self._is_case_sensitive(ignore_case):
flags |= QWebPage.FindCaseSensitively
flags |= QWebPage.FindFlag.FindCaseSensitively
if reverse:
flags |= QWebPage.FindBackward
flags |= QWebPage.FindFlag.FindBackward
return flags
def _call_cb(self, callback, found, text, flags, caller):
@ -139,7 +142,7 @@ class WebKitSearch(browsertab.AbstractSearch):
# Removing FindWrapsAroundDocument to get the same logging as with
# QtWebEngine
debug_flags = debug.qflags_key(
QWebPage, flags & ~QWebPage.FindWrapsAroundDocument,
QWebPage, flags & ~QWebPage.FindFlag.FindWrapsAroundDocument,
klass=QWebPage.FindFlag)
if debug_flags != '0x0000':
flag_text = 'with flags {}'.format(debug_flags)
@ -171,7 +174,7 @@ class WebKitSearch(browsertab.AbstractSearch):
# We first clear the marked text, then the highlights
self._widget.findText('')
self._widget.findText(
'', QWebPage.HighlightAllOccurrences) # type: ignore[arg-type]
'', QWebPage.FindFlag.HighlightAllOccurrences) # type: ignore[arg-type]
def search(self, text, *, ignore_case=usertypes.IgnoreCase.never,
reverse=False, result_cb=None):
@ -192,7 +195,7 @@ class WebKitSearch(browsertab.AbstractSearch):
# to get a mark so we can navigate.
found = self._widget.findText(text, self._flags)
self._widget.findText(text,
self._flags | QWebPage.HighlightAllOccurrences)
self._flags | QWebPage.FindFlag.HighlightAllOccurrences)
self._call_cb(result_cb, found, text, self._flags, 'search')
def next_result(self, *, wrap=False, callback=None):
@ -202,7 +205,7 @@ class WebKitSearch(browsertab.AbstractSearch):
int(self._flags)) # type: ignore[call-overload]
if wrap:
flags |= QWebPage.FindWrapsAroundDocument
flags |= QWebPage.FindFlag.FindWrapsAroundDocument
found = self._widget.findText(self.text, flags) # type: ignore[arg-type]
self._call_cb(callback, found, self.text, flags, 'next_result')
@ -213,13 +216,13 @@ class WebKitSearch(browsertab.AbstractSearch):
flags = QWebPage.FindFlags(
int(self._flags)) # type: ignore[call-overload]
if flags & QWebPage.FindBackward:
flags &= ~QWebPage.FindBackward
if flags & QWebPage.FindFlag.FindBackward:
flags &= ~QWebPage.FindFlag.FindBackward
else:
flags |= QWebPage.FindBackward
flags |= QWebPage.FindFlag.FindBackward
if wrap:
flags |= QWebPage.FindWrapsAroundDocument
flags |= QWebPage.FindFlag.FindWrapsAroundDocument
found = self._widget.findText(self.text, flags) # type: ignore[arg-type]
self._call_cb(callback, found, self.text, flags, 'prev_result')
@ -249,13 +252,13 @@ class WebKitCaret(browsertab.AbstractCaret):
self._selection_state = browsertab.SelectionState.none
self.selection_toggled.emit(self._selection_state)
settings = self._widget.settings()
settings.setAttribute(QWebSettings.CaretBrowsingEnabled, True)
settings.setAttribute(QWebSettings.WebAttribute.CaretBrowsingEnabled, True)
if self._widget.isVisible():
# Sometimes the caret isn't immediately visible, but unfocusing
# and refocusing it fixes that.
self._widget.clearFocus()
self._widget.setFocus(Qt.OtherFocusReason)
self._widget.setFocus(Qt.FocusReason.OtherFocusReason)
# Move the caret to the first element in the viewport if there
# isn't any text which is already selected.
@ -269,19 +272,19 @@ class WebKitCaret(browsertab.AbstractCaret):
@pyqtSlot(usertypes.KeyMode)
def _on_mode_left(self, _mode):
settings = self._widget.settings()
if settings.testAttribute(QWebSettings.CaretBrowsingEnabled):
if settings.testAttribute(QWebSettings.WebAttribute.CaretBrowsingEnabled):
if (self._selection_state is not browsertab.SelectionState.none and
self._widget.hasSelection()):
# Remove selection if it exists
self._widget.triggerPageAction(QWebPage.MoveToNextChar)
settings.setAttribute(QWebSettings.CaretBrowsingEnabled, False)
self._widget.triggerPageAction(QWebPage.WebAction.MoveToNextChar)
settings.setAttribute(QWebSettings.WebAttribute.CaretBrowsingEnabled, False)
self._selection_state = browsertab.SelectionState.none
def move_to_next_line(self, count=1):
if self._selection_state is not browsertab.SelectionState.none:
act = QWebPage.SelectNextLine
act = QWebPage.WebAction.SelectNextLine
else:
act = QWebPage.MoveToNextLine
act = QWebPage.WebAction.MoveToNextLine
for _ in range(count):
self._widget.triggerPageAction(act)
if self._selection_state is browsertab.SelectionState.line:
@ -289,9 +292,9 @@ class WebKitCaret(browsertab.AbstractCaret):
def move_to_prev_line(self, count=1):
if self._selection_state is not browsertab.SelectionState.none:
act = QWebPage.SelectPreviousLine
act = QWebPage.WebAction.SelectPreviousLine
else:
act = QWebPage.MoveToPreviousLine
act = QWebPage.WebAction.MoveToPreviousLine
for _ in range(count):
self._widget.triggerPageAction(act)
if self._selection_state is browsertab.SelectionState.line:
@ -299,89 +302,89 @@ class WebKitCaret(browsertab.AbstractCaret):
def move_to_next_char(self, count=1):
if self._selection_state is browsertab.SelectionState.normal:
act = QWebPage.SelectNextChar
act = QWebPage.WebAction.SelectNextChar
elif self._selection_state is browsertab.SelectionState.line:
return
else:
act = QWebPage.MoveToNextChar
act = QWebPage.WebAction.MoveToNextChar
for _ in range(count):
self._widget.triggerPageAction(act)
def move_to_prev_char(self, count=1):
if self._selection_state is browsertab.SelectionState.normal:
act = QWebPage.SelectPreviousChar
act = QWebPage.WebAction.SelectPreviousChar
elif self._selection_state is browsertab.SelectionState.line:
return
else:
act = QWebPage.MoveToPreviousChar
act = QWebPage.WebAction.MoveToPreviousChar
for _ in range(count):
self._widget.triggerPageAction(act)
def move_to_end_of_word(self, count=1):
if self._selection_state is browsertab.SelectionState.normal:
act = [QWebPage.SelectNextWord]
act = [QWebPage.WebAction.SelectNextWord]
if utils.is_windows: # pragma: no cover
act.append(QWebPage.SelectPreviousChar)
act.append(QWebPage.WebAction.SelectPreviousChar)
elif self._selection_state is browsertab.SelectionState.line:
return
else:
act = [QWebPage.MoveToNextWord]
act = [QWebPage.WebAction.MoveToNextWord]
if utils.is_windows: # pragma: no cover
act.append(QWebPage.MoveToPreviousChar)
act.append(QWebPage.WebAction.MoveToPreviousChar)
for _ in range(count):
for a in act:
self._widget.triggerPageAction(a)
def move_to_next_word(self, count=1):
if self._selection_state is browsertab.SelectionState.normal:
act = [QWebPage.SelectNextWord]
act = [QWebPage.WebAction.SelectNextWord]
if not utils.is_windows: # pragma: no branch
act.append(QWebPage.SelectNextChar)
act.append(QWebPage.WebAction.SelectNextChar)
elif self._selection_state is browsertab.SelectionState.line:
return
else:
act = [QWebPage.MoveToNextWord]
act = [QWebPage.WebAction.MoveToNextWord]
if not utils.is_windows: # pragma: no branch
act.append(QWebPage.MoveToNextChar)
act.append(QWebPage.WebAction.MoveToNextChar)
for _ in range(count):
for a in act:
self._widget.triggerPageAction(a)
def move_to_prev_word(self, count=1):
if self._selection_state is browsertab.SelectionState.normal:
act = QWebPage.SelectPreviousWord
act = QWebPage.WebAction.SelectPreviousWord
elif self._selection_state is browsertab.SelectionState.line:
return
else:
act = QWebPage.MoveToPreviousWord
act = QWebPage.WebAction.MoveToPreviousWord
for _ in range(count):
self._widget.triggerPageAction(act)
def move_to_start_of_line(self):
if self._selection_state is browsertab.SelectionState.normal:
act = QWebPage.SelectStartOfLine
act = QWebPage.WebAction.SelectStartOfLine
elif self._selection_state is browsertab.SelectionState.line:
return
else:
act = QWebPage.MoveToStartOfLine
act = QWebPage.WebAction.MoveToStartOfLine
self._widget.triggerPageAction(act)
def move_to_end_of_line(self):
if self._selection_state is browsertab.SelectionState.normal:
act = QWebPage.SelectEndOfLine
act = QWebPage.WebAction.SelectEndOfLine
elif self._selection_state is browsertab.SelectionState.line:
return
else:
act = QWebPage.MoveToEndOfLine
act = QWebPage.WebAction.MoveToEndOfLine
self._widget.triggerPageAction(act)
def move_to_start_of_next_block(self, count=1):
if self._selection_state is not browsertab.SelectionState.none:
act = [QWebPage.SelectNextLine,
QWebPage.SelectStartOfBlock]
act = [QWebPage.WebAction.SelectNextLine,
QWebPage.WebAction.SelectStartOfBlock]
else:
act = [QWebPage.MoveToNextLine,
QWebPage.MoveToStartOfBlock]
act = [QWebPage.WebAction.MoveToNextLine,
QWebPage.WebAction.MoveToStartOfBlock]
for _ in range(count):
for a in act:
self._widget.triggerPageAction(a)
@ -390,11 +393,11 @@ class WebKitCaret(browsertab.AbstractCaret):
def move_to_start_of_prev_block(self, count=1):
if self._selection_state is not browsertab.SelectionState.none:
act = [QWebPage.SelectPreviousLine,
QWebPage.SelectStartOfBlock]
act = [QWebPage.WebAction.SelectPreviousLine,
QWebPage.WebAction.SelectStartOfBlock]
else:
act = [QWebPage.MoveToPreviousLine,
QWebPage.MoveToStartOfBlock]
act = [QWebPage.WebAction.MoveToPreviousLine,
QWebPage.WebAction.MoveToStartOfBlock]
for _ in range(count):
for a in act:
self._widget.triggerPageAction(a)
@ -403,11 +406,11 @@ class WebKitCaret(browsertab.AbstractCaret):
def move_to_end_of_next_block(self, count=1):
if self._selection_state is not browsertab.SelectionState.none:
act = [QWebPage.SelectNextLine,
QWebPage.SelectEndOfBlock]
act = [QWebPage.WebAction.SelectNextLine,
QWebPage.WebAction.SelectEndOfBlock]
else:
act = [QWebPage.MoveToNextLine,
QWebPage.MoveToEndOfBlock]
act = [QWebPage.WebAction.MoveToNextLine,
QWebPage.WebAction.MoveToEndOfBlock]
for _ in range(count):
for a in act:
self._widget.triggerPageAction(a)
@ -416,9 +419,9 @@ class WebKitCaret(browsertab.AbstractCaret):
def move_to_end_of_prev_block(self, count=1):
if self._selection_state is not browsertab.SelectionState.none:
act = [QWebPage.SelectPreviousLine, QWebPage.SelectEndOfBlock]
act = [QWebPage.WebAction.SelectPreviousLine, QWebPage.WebAction.SelectEndOfBlock]
else:
act = [QWebPage.MoveToPreviousLine, QWebPage.MoveToEndOfBlock]
act = [QWebPage.WebAction.MoveToPreviousLine, QWebPage.WebAction.MoveToEndOfBlock]
for _ in range(count):
for a in act:
self._widget.triggerPageAction(a)
@ -427,18 +430,18 @@ class WebKitCaret(browsertab.AbstractCaret):
def move_to_start_of_document(self):
if self._selection_state is not browsertab.SelectionState.none:
act = QWebPage.SelectStartOfDocument
act = QWebPage.WebAction.SelectStartOfDocument
else:
act = QWebPage.MoveToStartOfDocument
act = QWebPage.WebAction.MoveToStartOfDocument
self._widget.triggerPageAction(act)
if self._selection_state is browsertab.SelectionState.line:
self._select_line()
def move_to_end_of_document(self):
if self._selection_state is not browsertab.SelectionState.none:
act = QWebPage.SelectEndOfDocument
act = QWebPage.WebAction.SelectEndOfDocument
else:
act = QWebPage.MoveToEndOfDocument
act = QWebPage.WebAction.MoveToEndOfDocument
self._widget.triggerPageAction(act)
def toggle_selection(self, line=False):
@ -455,7 +458,7 @@ class WebKitCaret(browsertab.AbstractCaret):
self.selection_toggled.emit(self._selection_state)
def drop_selection(self):
self._widget.triggerPageAction(QWebPage.MoveToNextChar)
self._widget.triggerPageAction(QWebPage.WebAction.MoveToNextChar)
def selection(self, callback):
callback(self._widget.selectedText())
@ -470,9 +473,9 @@ class WebKitCaret(browsertab.AbstractCaret):
}""")
def _select_line(self):
self._widget.triggerPageAction(QWebPage.SelectStartOfLine)
self._widget.triggerPageAction(QWebPage.WebAction.SelectStartOfLine)
self.reverse_selection()
self._widget.triggerPageAction(QWebPage.SelectEndOfLine)
self._widget.triggerPageAction(QWebPage.WebAction.SelectEndOfLine)
self.reverse_selection()
def _select_line_to_end(self):
@ -480,11 +483,11 @@ class WebKitCaret(browsertab.AbstractCaret):
# of focus) has to be checked before moving selection
# to the end of line
if self._js_selection_left_to_right():
self._widget.triggerPageAction(QWebPage.SelectEndOfLine)
self._widget.triggerPageAction(QWebPage.WebAction.SelectEndOfLine)
def _select_line_to_start(self):
if not self._js_selection_left_to_right():
self._widget.triggerPageAction(QWebPage.SelectStartOfLine)
self._widget.triggerPageAction(QWebPage.WebAction.SelectStartOfLine)
def _js_selection_left_to_right(self):
"""Return True iff the selection's direction is left to right."""
@ -497,7 +500,7 @@ class WebKitCaret(browsertab.AbstractCaret):
def _follow_selected(self, *, tab=False):
if QWebSettings.globalSettings().testAttribute(
QWebSettings.JavascriptEnabled):
QWebSettings.WebAttribute.JavascriptEnabled):
if tab:
self._tab.data.override_target = usertypes.ClickTarget.tab
self._tab.run_js_async("""
@ -599,7 +602,7 @@ class WebKitScroller(browsertab.AbstractScroller):
elif x is None and y == 100:
self.bottom()
else:
for val, orientation in [(x, Qt.Horizontal), (y, Qt.Vertical)]:
for val, orientation in [(x, Qt.Orientation.Horizontal), (y, Qt.Orientation.Vertical)]:
if val is not None:
frame = self._widget.page().mainFrame()
maximum = frame.scrollBarMaximum(orientation)
@ -624,36 +627,36 @@ class WebKitScroller(browsertab.AbstractScroller):
self._tab.fake_key_press(key)
def up(self, count=1):
self._key_press(Qt.Key_Up, count, 'scrollBarMinimum', Qt.Vertical)
self._key_press(Qt.Key.Key_Up, count, 'scrollBarMinimum', Qt.Orientation.Vertical)
def down(self, count=1):
self._key_press(Qt.Key_Down, count, 'scrollBarMaximum', Qt.Vertical)
self._key_press(Qt.Key.Key_Down, count, 'scrollBarMaximum', Qt.Orientation.Vertical)
def left(self, count=1):
self._key_press(Qt.Key_Left, count, 'scrollBarMinimum', Qt.Horizontal)
self._key_press(Qt.Key.Key_Left, count, 'scrollBarMinimum', Qt.Orientation.Horizontal)
def right(self, count=1):
self._key_press(Qt.Key_Right, count, 'scrollBarMaximum', Qt.Horizontal)
self._key_press(Qt.Key.Key_Right, count, 'scrollBarMaximum', Qt.Orientation.Horizontal)
def top(self):
self._key_press(Qt.Key_Home)
self._key_press(Qt.Key.Key_Home)
def bottom(self):
self._key_press(Qt.Key_End)
self._key_press(Qt.Key.Key_End)
def page_up(self, count=1):
self._key_press(Qt.Key_PageUp, count, 'scrollBarMinimum', Qt.Vertical)
self._key_press(Qt.Key.Key_PageUp, count, 'scrollBarMinimum', Qt.Orientation.Vertical)
def page_down(self, count=1):
self._key_press(Qt.Key_PageDown, count, 'scrollBarMaximum',
Qt.Vertical)
self._key_press(Qt.Key.Key_PageDown, count, 'scrollBarMaximum',
Qt.Orientation.Vertical)
def at_top(self):
return self.pos_px().y() == 0
def at_bottom(self):
frame = self._widget.page().currentFrame()
return self.pos_px().y() >= frame.scrollBarMaximum(Qt.Vertical)
return self.pos_px().y() >= frame.scrollBarMaximum(Qt.Orientation.Vertical)
class WebKitHistoryPrivate(browsertab.AbstractHistoryPrivate):
@ -882,7 +885,7 @@ class WebKitTab(browsertab.AbstractTab):
tab=self, parent=self)
self.zoom = WebKitZoom(tab=self, parent=self)
self.search = WebKitSearch(tab=self, parent=self)
self.printing = WebKitPrinting(tab=self)
self.printing = WebKitPrinting(tab=self, parent=self)
self.elements = WebKitElements(tab=self)
self.action = WebKitAction(tab=self)
self.audio = WebKitAudio(tab=self, parent=self)
@ -899,7 +902,7 @@ class WebKitTab(browsertab.AbstractTab):
def _make_private(self, widget):
settings = widget.settings()
settings.setAttribute(QWebSettings.PrivateBrowsingEnabled, True)
settings.setAttribute(QWebSettings.WebAttribute.PrivateBrowsingEnabled, True)
def load_url(self, url):
self._load_url_prepare(url)
@ -931,9 +934,9 @@ class WebKitTab(browsertab.AbstractTab):
def reload(self, *, force=False):
if force:
action = QWebPage.ReloadAndBypassCache
action = QWebPage.WebAction.ReloadAndBypassCache
else:
action = QWebPage.Reload
action = QWebPage.WebAction.Reload
self._widget.triggerPageAction(action)
def stop(self):

View File

@ -17,17 +17,20 @@
# You should have received a copy of the GNU General Public License
# along with qutebrowser. If not, see <https://www.gnu.org/licenses/>.
# FIXME:qt6 (lint)
# pylint: disable=no-name-in-module
"""The main browser widgets."""
import html
import functools
from PyQt5.QtCore import pyqtSlot, pyqtSignal, Qt, QUrl, QPoint
from PyQt5.QtGui import QDesktopServices
from PyQt5.QtNetwork import QNetworkReply, QNetworkRequest
from PyQt5.QtWidgets import QFileDialog
from PyQt5.QtPrintSupport import QPrintDialog
from PyQt5.QtWebKitWidgets import QWebPage, QWebFrame
from qutebrowser.qt.core import pyqtSlot, pyqtSignal, Qt, QUrl, QPoint
from qutebrowser.qt.gui import QDesktopServices
from qutebrowser.qt.network import QNetworkReply, QNetworkRequest
from qutebrowser.qt.widgets import QFileDialog
from qutebrowser.qt.printsupport import QPrintDialog
from qutebrowser.qt.webkitwidgets import QWebPage, QWebFrame
from qutebrowser.config import websettings, config
from qutebrowser.browser import pdfjs, shared, downloads, greasemonkey
@ -67,8 +70,8 @@ class BrowserPage(QWebPage):
self._tabdata = tabdata
self._is_shutting_down = False
self._extension_handlers = {
QWebPage.ErrorPageExtension: self._handle_errorpage,
QWebPage.ChooseMultipleFilesExtension: self._handle_multiple_files,
QWebPage.Extension.ErrorPageExtension: self._handle_errorpage,
QWebPage.Extension.ChooseMultipleFilesExtension: self._handle_multiple_files,
}
self._ignore_load_started = False
self.error_occurred = False
@ -134,16 +137,16 @@ class BrowserPage(QWebPage):
False if no error page should be displayed, True otherwise.
"""
ignored_errors = [
(QWebPage.QtNetwork, QNetworkReply.OperationCanceledError),
(QWebPage.ErrorDomain.QtNetwork, QNetworkReply.NetworkError.OperationCanceledError),
# "Loading is handled by the media engine"
(QWebPage.WebKit, 203),
(QWebPage.ErrorDomain.WebKit, 203),
# "Frame load interrupted by policy change"
(QWebPage.WebKit, 102),
(QWebPage.ErrorDomain.WebKit, 102),
]
errpage.baseUrl = info.url
urlstr = info.url.toDisplayString()
if (info.domain, info.error) == (QWebPage.QtNetwork,
QNetworkReply.ProtocolUnknownError):
if (info.domain, info.error) == (QWebPage.ErrorDomain.QtNetwork,
QNetworkReply.NetworkError.ProtocolUnknownError):
# For some reason, we get a segfault when we use
# QDesktopServices::openUrl with info.url directly - however it
# works when we construct a copy of it.
@ -154,7 +157,7 @@ class BrowserPage(QWebPage):
text="URL: <b>{}</b>".format(
html.escape(url.toDisplayString())),
yes_action=functools.partial(QDesktopServices.openUrl, url),
url=info.url.toString(QUrl.RemovePassword | QUrl.FullyEncoded))
url=info.url.toString(QUrl.UrlFormattingOption.RemovePassword | QUrl.ComponentFormattingOption.FullyEncoded))
return True
elif (info.domain, info.error) in ignored_errors:
log.webview.debug("Ignored error on {}: {} (error domain: {}, "
@ -251,7 +254,7 @@ class BrowserPage(QWebPage):
def on_print_requested(self, frame):
"""Handle printing when requested via javascript."""
printdiag = QPrintDialog()
printdiag.setAttribute(Qt.WA_DeleteOnClose)
printdiag.setAttribute(Qt.WidgetAttribute.WA_DeleteOnClose)
printdiag.open(lambda: frame.print(printdiag.printer()))
def on_download_requested(self, request):
@ -357,24 +360,24 @@ class BrowserPage(QWebPage):
return
options = {
QWebPage.Notifications: 'content.notifications.enabled',
QWebPage.Geolocation: 'content.geolocation',
QWebPage.Feature.Notifications: 'content.notifications.enabled',
QWebPage.Feature.Geolocation: 'content.geolocation',
}
messages = {
QWebPage.Notifications: 'show notifications',
QWebPage.Geolocation: 'access your location',
QWebPage.Feature.Notifications: 'show notifications',
QWebPage.Feature.Geolocation: 'access your location',
}
yes_action = functools.partial(
self.setFeaturePermission, frame, feature,
QWebPage.PermissionGrantedByUser)
QWebPage.PermissionPolicy.PermissionGrantedByUser)
no_action = functools.partial(
self.setFeaturePermission, frame, feature,
QWebPage.PermissionDeniedByUser)
QWebPage.PermissionPolicy.PermissionDeniedByUser)
url = frame.url().adjusted(QUrl.RemoveUserInfo | # type: ignore[operator]
QUrl.RemovePath |
QUrl.RemoveQuery |
QUrl.RemoveFragment)
url = frame.url().adjusted(QUrl.UrlFormattingOption.RemoveUserInfo | # type: ignore[operator]
QUrl.UrlFormattingOption.RemovePath |
QUrl.UrlFormattingOption.RemoveQuery |
QUrl.UrlFormattingOption.RemoveFragment)
question = shared.feature_permission(
url=url,
option=options[feature], msg=messages[feature],
@ -507,17 +510,17 @@ class BrowserPage(QWebPage):
and then conditionally opens the URL here or in another tab/window.
"""
type_map = {
QWebPage.NavigationTypeLinkClicked:
QWebPage.NavigationType.NavigationTypeLinkClicked:
usertypes.NavigationRequest.Type.link_clicked,
QWebPage.NavigationTypeFormSubmitted:
QWebPage.NavigationType.NavigationTypeFormSubmitted:
usertypes.NavigationRequest.Type.form_submitted,
QWebPage.NavigationTypeFormResubmitted:
QWebPage.NavigationType.NavigationTypeFormResubmitted:
usertypes.NavigationRequest.Type.form_resubmitted,
QWebPage.NavigationTypeBackOrForward:
QWebPage.NavigationType.NavigationTypeBackOrForward:
usertypes.NavigationRequest.Type.back_forward,
QWebPage.NavigationTypeReload:
usertypes.NavigationRequest.Type.reloaded,
QWebPage.NavigationTypeOther:
QWebPage.NavigationType.NavigationTypeReload:
usertypes.NavigationRequest.Type.reload,
QWebPage.NavigationType.NavigationTypeOther:
usertypes.NavigationRequest.Type.other,
}
is_main_frame = frame is self.mainFrame()
@ -525,7 +528,7 @@ class BrowserPage(QWebPage):
navigation_type=type_map[typ],
is_main_frame=is_main_frame)
if navigation.navigation_type == navigation.Type.reloaded:
if navigation.navigation_type == navigation.Type.reload:
self.reloading.emit(navigation.url)
self.navigation_request.emit(navigation)

View File

@ -17,11 +17,14 @@
# You should have received a copy of the GNU General Public License
# along with qutebrowser. If not, see <https://www.gnu.org/licenses/>.
# FIXME:qt6 (lint)
# pylint: disable=no-name-in-module
"""The main browser widgets."""
from PyQt5.QtCore import pyqtSignal, Qt, QUrl
from PyQt5.QtWebKit import QWebSettings
from PyQt5.QtWebKitWidgets import QWebView, QWebPage
from qutebrowser.qt.core import pyqtSignal, Qt, QUrl
from qutebrowser.qt.webkit import QWebSettings
from qutebrowser.qt.webkitwidgets import QWebView, QWebPage
from qutebrowser.config import config, stylesheet
from qutebrowser.keyinput import modeman
@ -74,15 +77,15 @@ class WebView(QWebView):
tabdata=tab.data, private=private,
parent=self)
page.setVisibilityState(
QWebPage.VisibilityStateVisible if self.isVisible()
else QWebPage.VisibilityStateHidden)
QWebPage.VisibilityState.VisibilityStateVisible if self.isVisible()
else QWebPage.VisibilityState.VisibilityStateHidden)
self.setPage(page)
stylesheet.set_register(self)
def __repr__(self):
flags = QUrl.EncodeUnicode
flags = QUrl.ComponentFormattingOption.EncodeUnicode
urlstr = self.url().toDisplayString(flags) # type: ignore[arg-type]
url = utils.elide(urlstr, 100)
return utils.get_repr(self, tab_id=self._tab_id, url=url)
@ -107,7 +110,7 @@ class WebView(QWebView):
# quitting it seems.
log.destroy.debug("Shutting down {!r}.".format(self))
settings = self.settings()
settings.setAttribute(QWebSettings.JavascriptEnabled, False)
settings.setAttribute(QWebSettings.WebAttribute.JavascriptEnabled, False)
self.stop()
page = self.page()
assert isinstance(page, webpage.BrowserPage), page
@ -134,7 +137,7 @@ class WebView(QWebView):
"""
debug_type = debug.qenum_key(QWebPage, wintype)
log.webview.debug("createWindow with type {}".format(debug_type))
if wintype == QWebPage.WebModalDialog:
if wintype == QWebPage.WebWindowType.WebModalDialog:
log.webview.warning("WebModalDialog requested, but we don't "
"support that!")
tabbed_browser = objreg.get('tabbed-browser', scope='window',
@ -156,12 +159,12 @@ class WebView(QWebView):
e: The QPaintEvent.
"""
frame = self.page().mainFrame()
new_pos = (frame.scrollBarValue(Qt.Horizontal),
frame.scrollBarValue(Qt.Vertical))
new_pos = (frame.scrollBarValue(Qt.Orientation.Horizontal),
frame.scrollBarValue(Qt.Orientation.Vertical))
if self._old_scroll_pos != new_pos:
self._old_scroll_pos = new_pos
m = (frame.scrollBarMaximum(Qt.Horizontal),
frame.scrollBarMaximum(Qt.Vertical))
m = (frame.scrollBarMaximum(Qt.Orientation.Horizontal),
frame.scrollBarMaximum(Qt.Orientation.Vertical))
perc = (round(100 * new_pos[0] / m[0]) if m[0] != 0 else 0,
round(100 * new_pos[1] / m[1]) if m[1] != 0 else 0)
self.scroll_pos = perc
@ -187,7 +190,7 @@ class WebView(QWebView):
e: The QShowEvent.
"""
super().showEvent(e)
self.page().setVisibilityState(QWebPage.VisibilityStateVisible)
self.page().setVisibilityState(QWebPage.VisibilityState.VisibilityStateVisible)
def hideEvent(self, e):
"""Extend hideEvent to set the page visibility state to hidden.
@ -196,16 +199,16 @@ class WebView(QWebView):
e: The QHideEvent.
"""
super().hideEvent(e)
self.page().setVisibilityState(QWebPage.VisibilityStateHidden)
self.page().setVisibilityState(QWebPage.VisibilityState.VisibilityStateHidden)
def mousePressEvent(self, e):
"""Set the tabdata ClickTarget on a mousepress.
This is implemented here as we don't need it for QtWebEngine.
"""
if e.button() == Qt.MidButton or e.modifiers() & Qt.ControlModifier:
if e.button() == Qt.MouseButton.MidButton or e.modifiers() & Qt.KeyboardModifier.ControlModifier:
background = config.val.tabs.background
if e.modifiers() & Qt.ShiftModifier:
if e.modifiers() & Qt.KeyboardModifier.ShiftModifier:
background = not background
if background:
target = usertypes.ClickTarget.tab_bg

View File

@ -21,7 +21,7 @@
import argparse
from PyQt5.QtCore import QUrl
from qutebrowser.qt.core import QUrl
from qutebrowser.commands import cmdexc
from qutebrowser.utils import utils, objreg, log

View File

@ -24,7 +24,7 @@ import re
import contextlib
from typing import TYPE_CHECKING, Callable, Dict, Iterator, Mapping, MutableMapping
from PyQt5.QtCore import pyqtSlot, QUrl, QObject
from qutebrowser.qt.core import pyqtSlot, QUrl, QObject
from qutebrowser.api import cmdutils
from qutebrowser.commands import cmdexc, parser
@ -55,9 +55,9 @@ def _init_variable_replacements() -> Mapping[str, _ReplacementFunction]:
"""Return a dict from variable replacements to fns processing them."""
replacements: Dict[str, _ReplacementFunction] = {
'url': lambda tb: _url(tb).toString(
QUrl.FullyEncoded | QUrl.RemovePassword),
QUrl.ComponentFormattingOption.FullyEncoded | QUrl.UrlFormattingOption.RemovePassword),
'url:pretty': lambda tb: _url(tb).toString(
QUrl.DecodeReserved | QUrl.RemovePassword),
QUrl.ComponentFormattingOption.DecodeReserved | QUrl.UrlFormattingOption.RemovePassword),
'url:domain': lambda tb: "{}://{}{}".format(
_url(tb).scheme(), _url(tb).host(),
":" + str(_url(tb).port()) if _url(tb).port() != -1 else ""),

View File

@ -24,7 +24,7 @@ import os.path
import tempfile
from typing import cast, Any, MutableMapping, Tuple
from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject, QSocketNotifier
from qutebrowser.qt.core import pyqtSignal, pyqtSlot, QObject, QSocketNotifier
import qutebrowser
from qutebrowser.utils import message, log, objreg, standarddir, utils
@ -62,7 +62,7 @@ class _QtFIFOReader(QObject):
# pylint: enable=no-member,useless-suppression
self._fifo = os.fdopen(fd, 'r')
self._notifier = QSocketNotifier(cast(sip.voidptr, fd),
QSocketNotifier.Read, self)
QSocketNotifier.Type.Read, self)
self._notifier.activated.connect(self.read_line)
@pyqtSlot()

View File

@ -22,7 +22,7 @@
import dataclasses
from typing import TYPE_CHECKING
from PyQt5.QtCore import pyqtSlot, QObject, QTimer
from qutebrowser.qt.core import pyqtSlot, QObject, QTimer
from qutebrowser.config import config
from qutebrowser.commands import parser, cmdexc

View File

@ -25,9 +25,9 @@ We use this to be able to highlight parts of the text.
import re
import html
from PyQt5.QtWidgets import QStyle, QStyleOptionViewItem, QStyledItemDelegate
from PyQt5.QtCore import QRectF, QRegularExpression, QSize, Qt
from PyQt5.QtGui import (QIcon, QPalette, QTextDocument, QTextOption,
from qutebrowser.qt.widgets import QStyle, QStyleOptionViewItem, QStyledItemDelegate
from qutebrowser.qt.core import QRectF, QRegularExpression, QSize, Qt
from qutebrowser.qt.gui import (QIcon, QPalette, QTextDocument, QTextOption,
QAbstractTextDocumentLayout, QSyntaxHighlighter,
QTextCharFormat)
@ -46,7 +46,7 @@ class _Highlighter(QSyntaxHighlighter):
words.sort(key=len, reverse=True)
pat = "|".join(re.escape(word) for word in words)
self._expression = QRegularExpression(
pat, QRegularExpression.CaseInsensitiveOption
pat, QRegularExpression.PatternOption.CaseInsensitiveOption
)
qtutils.ensure_valid(self._expression)
@ -95,7 +95,7 @@ class CompletionItemDelegate(QStyledItemDelegate):
"""Draw the background of an ItemViewItem."""
assert self._opt is not None
assert self._style is not None
self._style.drawPrimitive(self._style.PE_PanelItemViewItem, self._opt,
self._style.drawPrimitive(QStyle.PrimitiveElement.PE_PanelItemViewItem, self._opt,
self._painter, self._opt.widget)
def _draw_icon(self):
@ -104,18 +104,18 @@ class CompletionItemDelegate(QStyledItemDelegate):
assert self._style is not None
icon_rect = self._style.subElementRect(
self._style.SE_ItemViewItemDecoration, self._opt, self._opt.widget)
QStyle.SubElement.SE_ItemViewItemDecoration, self._opt, self._opt.widget)
if not icon_rect.isValid():
# The rect seems to be wrong in all kind of ways if no icon should
# be displayed.
return
mode = QIcon.Normal
if not self._opt.state & QStyle.State_Enabled:
mode = QIcon.Disabled
elif self._opt.state & QStyle.State_Selected:
mode = QIcon.Selected
state = QIcon.On if self._opt.state & QStyle.State_Open else QIcon.Off
mode = QIcon.Mode.Normal
if not self._opt.state & QStyle.StateFlag.State_Enabled:
mode = QIcon.Mode.Disabled
elif self._opt.state & QStyle.StateFlag.State_Selected:
mode = QIcon.Mode.Selected
state = QIcon.State.On if self._opt.state & QStyle.StateFlag.State_Open else QIcon.State.Off
self._opt.icon.paint(self._painter, icon_rect,
self._opt.decorationAlignment, mode, state)
@ -135,9 +135,9 @@ class CompletionItemDelegate(QStyledItemDelegate):
return
text_rect_ = self._style.subElementRect(
self._style.SE_ItemViewItemText, self._opt, self._opt.widget)
QStyle.SubElement.SE_ItemViewItemText, self._opt, self._opt.widget)
qtutils.ensure_valid(text_rect_)
margin = self._style.pixelMetric(QStyle.PM_FocusFrameHMargin,
margin = self._style.pixelMetric(QStyle.PixelMetric.PM_FocusFrameHMargin,
self._opt, self._opt.widget) + 1
# remove width padding
text_rect = text_rect_.adjusted(margin, 0, -margin, 0)
@ -149,24 +149,24 @@ class CompletionItemDelegate(QStyledItemDelegate):
text_rect.adjust(0, -2, 0, -2)
self._painter.save()
state = self._opt.state
if state & QStyle.State_Enabled and state & QStyle.State_Active:
cg = QPalette.Normal
elif state & QStyle.State_Enabled:
cg = QPalette.Inactive
if state & QStyle.StateFlag.State_Enabled and state & QStyle.StateFlag.State_Active:
cg = QPalette.ColorGroup.Normal
elif state & QStyle.StateFlag.State_Enabled:
cg = QPalette.ColorGroup.Inactive
else:
cg = QPalette.Disabled
cg = QPalette.ColorGroup.Disabled
if state & QStyle.State_Selected:
if state & QStyle.StateFlag.State_Selected:
self._painter.setPen(self._opt.palette.color(
cg, QPalette.HighlightedText))
cg, QPalette.ColorRole.HighlightedText))
# This is a dirty fix for the text jumping by one pixel for
# whatever reason.
text_rect.adjust(0, -1, 0, 0)
else:
self._painter.setPen(self._opt.palette.color(cg, QPalette.Text))
self._painter.setPen(self._opt.palette.color(cg, QPalette.ColorRole.Text))
if state & QStyle.State_Editing:
self._painter.setPen(self._opt.palette.color(cg, QPalette.Text))
if state & QStyle.StateFlag.State_Editing:
self._painter.setPen(self._opt.palette.color(cg, QPalette.ColorRole.Text))
self._painter.drawRect(text_rect_.adjusted(0, 0, -1, -1))
self._painter.translate(text_rect.left(), text_rect.top())
@ -188,9 +188,9 @@ class CompletionItemDelegate(QStyledItemDelegate):
clip = QRectF(0, 0, rect.width(), rect.height())
self._painter.save()
if self._opt.state & QStyle.State_Selected:
if self._opt.state & QStyle.StateFlag.State_Selected:
color = config.cache['colors.completion.item.selected.fg']
elif not self._opt.state & QStyle.State_Enabled:
elif not self._opt.state & QStyle.StateFlag.State_Enabled:
color = config.cache['colors.completion.category.fg']
else:
colors = config.cache['colors.completion.fg']
@ -199,7 +199,7 @@ class CompletionItemDelegate(QStyledItemDelegate):
self._painter.setPen(color)
ctx = QAbstractTextDocumentLayout.PaintContext()
ctx.palette.setColor(QPalette.Text, self._painter.pen().color())
ctx.palette.setColor(QPalette.ColorRole.Text, self._painter.pen().color())
if clip.isValid():
self._painter.setClipRect(clip)
ctx.clip = clip
@ -217,10 +217,10 @@ class CompletionItemDelegate(QStyledItemDelegate):
# qcommonstyle.cpp:viewItemDrawText
# https://github.com/qutebrowser/qutebrowser/issues/118
text_option = QTextOption()
if self._opt.features & QStyleOptionViewItem.WrapText:
text_option.setWrapMode(QTextOption.WordWrap)
if self._opt.features & QStyleOptionViewItem.ViewItemFeature.WrapText:
text_option.setWrapMode(QTextOption.WrapMode.WordWrap)
else:
text_option.setWrapMode(QTextOption.ManualWrap)
text_option.setWrapMode(QTextOption.WrapMode.ManualWrap)
text_option.setTextDirection(self._opt.direction)
text_option.setAlignment(QStyle.visualAlignment(
self._opt.direction, self._opt.displayAlignment))
@ -238,7 +238,7 @@ class CompletionItemDelegate(QStyledItemDelegate):
pattern = view.pattern
columns_to_filter = index.model().columns_to_filter(index)
if index.column() in columns_to_filter and pattern:
if self._opt.state & QStyle.State_Selected:
if self._opt.state & QStyle.StateFlag.State_Selected:
color = config.val.colors.completion.item.selected.match.fg
else:
color = config.val.colors.completion.match.fg
@ -255,23 +255,23 @@ class CompletionItemDelegate(QStyledItemDelegate):
assert self._opt is not None
assert self._style is not None
state = self._opt.state
if not state & QStyle.State_HasFocus:
if not state & QStyle.StateFlag.State_HasFocus:
return
o = self._opt
o.rect = self._style.subElementRect(
self._style.SE_ItemViewItemFocusRect, self._opt, self._opt.widget)
o.state |= int(QStyle.State_KeyboardFocusChange | QStyle.State_Item)
QStyle.SubElement.SE_ItemViewItemFocusRect, self._opt, self._opt.widget)
o.state |= QStyle.StateFlag.State_KeyboardFocusChange | QStyle.StateFlag.State_Item
qtutils.ensure_valid(o.rect)
if state & QStyle.State_Enabled:
cg = QPalette.Normal
if state & QStyle.StateFlag.State_Enabled:
cg = QPalette.ColorGroup.Normal
else:
cg = QPalette.Disabled
if state & QStyle.State_Selected:
role = QPalette.Highlight
cg = QPalette.ColorGroup.Disabled
if state & QStyle.StateFlag.State_Selected:
role = QPalette.ColorRole.Highlight
else:
role = QPalette.Window
role = QPalette.ColorRole.Window
o.backgroundColor = self._opt.palette.color(cg, role)
self._style.drawPrimitive(QStyle.PE_FrameFocusRect, o, self._painter,
self._style.drawPrimitive(QStyle.PrimitiveElement.PE_FrameFocusRect, o, self._painter,
self._opt.widget)
def sizeHint(self, option, index):
@ -287,7 +287,7 @@ class CompletionItemDelegate(QStyledItemDelegate):
Return:
A QSize with the recommended size.
"""
value = index.data(Qt.SizeHintRole)
value = index.data(Qt.ItemDataRole.SizeHintRole)
if value is not None:
return value
self._opt = QStyleOptionViewItem(option)
@ -298,7 +298,7 @@ class CompletionItemDelegate(QStyledItemDelegate):
assert self._doc is not None
docsize = self._doc.size().toSize()
size = self._style.sizeFromContents(QStyle.CT_ItemViewItem, self._opt,
size = self._style.sizeFromContents(QStyle.ContentsType.CT_ItemViewItem, self._opt,
docsize, self._opt.widget)
qtutils.ensure_valid(size)
return size + QSize(10, 3)

View File

@ -25,8 +25,8 @@ subclasses to provide completions.
from typing import TYPE_CHECKING, Optional
from PyQt5.QtWidgets import QTreeView, QSizePolicy, QStyleFactory, QWidget
from PyQt5.QtCore import pyqtSlot, pyqtSignal, Qt, QItemSelectionModel, QSize
from qutebrowser.qt.widgets import QTreeView, QSizePolicy, QStyleFactory, QWidget
from qutebrowser.qt.core import pyqtSlot, pyqtSignal, Qt, QItemSelectionModel, QSize
from qutebrowser.config import config, stylesheet
from qutebrowser.completion import completiondelegate
@ -127,14 +127,14 @@ class CompletionView(QTreeView):
self.setItemDelegate(self._delegate)
self.setStyle(QStyleFactory.create('Fusion'))
stylesheet.set_register(self)
self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
self.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed)
self.setHeaderHidden(True)
self.setAlternatingRowColors(True)
self.setIndentation(0)
self.setItemsExpandable(False)
self.setExpandsOnDoubleClick(False)
self.setAnimated(False)
self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
self.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
# WORKAROUND
# This is a workaround for weird race conditions with invalid
# item indexes leading to segfaults in Qt.
@ -277,18 +277,20 @@ class CompletionView(QTreeView):
direction = -1 if upwards else 1
while True:
idx = idx.sibling(idx.row() + direction, 0)
if not idx.isValid() and upwards:
if idx.isValid():
child = model.index(0, 0, idx)
if child.isValid():
self.scrollTo(idx) # scroll to ensure the category is visible
return child
elif upwards:
# wrap around to the first item of the last category
return model.last_item().sibling(0, 0)
elif not idx.isValid() and not upwards:
else:
# wrap around to the first item of the first category
idx = model.first_item()
self.scrollTo(idx.parent())
return idx
elif idx.isValid() and idx.child(0, 0).isValid():
# scroll to ensure the category is visible
self.scrollTo(idx)
return idx.child(0, 0)
raise utils.Unreachable
@ -339,8 +341,8 @@ class CompletionView(QTreeView):
selmodel.setCurrentIndex(
idx,
QItemSelectionModel.ClearAndSelect |
QItemSelectionModel.Rows)
QItemSelectionModel.SelectionFlag.ClearAndSelect |
QItemSelectionModel.SelectionFlag.Rows)
# if the last item is focused, try to fetch more
next_idx = self.indexBelow(idx)

View File

@ -21,7 +21,7 @@
from typing import MutableSequence
from PyQt5.QtCore import Qt, QModelIndex, QAbstractItemModel
from qutebrowser.qt.core import Qt, QModelIndex, QAbstractItemModel
from qutebrowser.utils import log, qtutils, utils
from qutebrowser.api import cmdutils
@ -63,7 +63,7 @@ class CompletionModel(QAbstractItemModel):
"""Add a completion category to the model."""
self._categories.append(cat)
def data(self, index, role=Qt.DisplayRole):
def data(self, index, role=Qt.ItemDataRole.DisplayRole):
"""Return the item data for index.
Override QAbstractItemModel::data.
@ -74,7 +74,7 @@ class CompletionModel(QAbstractItemModel):
Return: The item data, or None on an invalid index.
"""
if role != Qt.DisplayRole:
if role != Qt.ItemDataRole.DisplayRole:
return None
cat = self._cat_from_idx(index)
if cat:
@ -94,17 +94,17 @@ class CompletionModel(QAbstractItemModel):
Override QAbstractItemModel::flags.
Return: The item flags, or Qt.NoItemFlags on error.
Return: The item flags, or Qt.ItemFlag.NoItemFlags on error.
"""
if not index.isValid():
return Qt.NoItemFlags
return Qt.ItemFlag.NoItemFlags
if index.parent().isValid():
# item
return (Qt.ItemIsEnabled | Qt.ItemIsSelectable |
Qt.ItemNeverHasChildren)
return (Qt.ItemFlag.ItemIsEnabled | Qt.ItemFlag.ItemIsSelectable |
Qt.ItemFlag.ItemNeverHasChildren)
else:
# category
return Qt.NoItemFlags
return Qt.ItemFlag.NoItemFlags
def index(self, row, col, parent=QModelIndex()):
"""Get an index into the model.

View File

@ -31,7 +31,7 @@ import os
import os.path
from typing import List, Optional, Iterable
from PyQt5.QtCore import QAbstractListModel, QModelIndex, QObject, Qt, QUrl
from qutebrowser.qt.core import QAbstractListModel, QModelIndex, QObject, Qt, QUrl
from qutebrowser.config import config
from qutebrowser.utils import log
@ -100,9 +100,9 @@ class FilePathCategory(QAbstractListModel):
paths = self._glob(expanded)
self._paths = sorted(self._contract_user(val, path) for path in paths)
def data(self, index: QModelIndex, role: int = Qt.DisplayRole) -> Optional[str]:
def data(self, index: QModelIndex, role: int = Qt.ItemDataRole.DisplayRole) -> Optional[str]:
"""Implement abstract method in QAbstractListModel."""
if role == Qt.DisplayRole and index.column() == 0:
if role == Qt.ItemDataRole.DisplayRole and index.column() == 0:
return self._paths[index.row()]
return None

View File

@ -21,8 +21,8 @@
from typing import Optional
from PyQt5.QtSql import QSqlQueryModel
from PyQt5.QtWidgets import QWidget
from qutebrowser.qt.sql import QSqlQueryModel
from qutebrowser.qt.widgets import QWidget
from qutebrowser.misc import sql
from qutebrowser.utils import debug, message, log

View File

@ -22,9 +22,9 @@
import re
from typing import Iterable, Tuple
from PyQt5.QtCore import QSortFilterProxyModel, QRegularExpression
from PyQt5.QtGui import QStandardItem, QStandardItemModel
from PyQt5.QtWidgets import QWidget
from qutebrowser.qt.core import QSortFilterProxyModel, QRegularExpression
from qutebrowser.qt.gui import QStandardItem, QStandardItemModel
from qutebrowser.qt.widgets import QWidget
from qutebrowser.completion.models import util
from qutebrowser.utils import qtutils, log
@ -66,7 +66,7 @@ class ListCategory(QSortFilterProxyModel):
val = re.sub(r' +', r' ', val) # See #1919
val = re.escape(val)
val = val.replace(r'\ ', '.*')
rx = QRegularExpression(val, QRegularExpression.CaseInsensitiveOption)
rx = QRegularExpression(val, QRegularExpression.PatternOption.CaseInsensitiveOption)
qtutils.ensure_valid(rx)
self.setFilterRegularExpression(rx)
self.invalidate()

View File

@ -21,7 +21,7 @@
from typing import Dict, Sequence
from PyQt5.QtCore import QAbstractItemModel
from qutebrowser.qt.core import QAbstractItemModel
from qutebrowser.completion.models import (completionmodel, filepathcategory,
listcategory, histcategory)

View File

@ -27,7 +27,7 @@ import contextlib
import subprocess
from typing import Optional, IO, Iterator
from PyQt5.QtCore import QUrl
from qutebrowser.qt.core import QUrl
from qutebrowser.api import (
hook,
@ -109,6 +109,7 @@ _RESOURCE_TYPE_STRINGS = {
ResourceType.plugin_resource: "other",
ResourceType.preload_main_frame: "other",
ResourceType.preload_sub_frame: "other",
ResourceType.websocket: "websocket",
ResourceType.unknown: "other",
None: "",
}

View File

@ -26,7 +26,7 @@ import logging
import pathlib
from typing import cast, IO, Set
from PyQt5.QtCore import QUrl
from qutebrowser.qt.core import QUrl
from qutebrowser.api import (
hook,

View File

@ -34,8 +34,8 @@ try:
except ImportError:
hunter = None
from PyQt5.QtCore import Qt
from PyQt5.QtPrintSupport import QPrintPreviewDialog
from qutebrowser.qt.core import Qt
from qutebrowser.qt.printsupport import QPrintPreviewDialog
from qutebrowser.api import cmdutils, apitypes, message, config
@ -81,11 +81,11 @@ def _print_preview(tab: apitypes.Tab) -> None:
tab.printing.check_preview_support()
diag = QPrintPreviewDialog(tab)
diag.setAttribute(Qt.WA_DeleteOnClose)
diag.setAttribute(Qt.WidgetAttribute.WA_DeleteOnClose)
diag.setWindowFlags(
diag.windowFlags() |
Qt.WindowMaximizeButtonHint |
Qt.WindowMinimizeButtonHint)
Qt.WindowType.WindowMaximizeButtonHint |
Qt.WindowType.WindowMinimizeButtonHint)
diag.paintRequested.connect(functools.partial(
tab.printing.to_printer, callback=print_callback))
diag.exec()
@ -324,7 +324,7 @@ def debug_webaction(tab: apitypes.Tab, action: str, count: int = 1) -> None:
Available actions:
https://doc.qt.io/archives/qt-5.5/qwebpage.html#WebAction-enum (WebKit)
https://doc.qt.io/qt-5/qwebenginepage.html#WebAction-enum (WebEngine)
https://doc.qt.io/qt-6/qwebenginepage.html#WebAction-enum (WebEngine)
Args:
action: The action to execute, e.g. MoveToNextChar.
@ -365,7 +365,7 @@ def message_error(text: str, rich: bool = False) -> None:
Args:
text: The text to show.
rich: Render the given text as
https://doc.qt.io/qt-5/richtext-html-subset.html[Qt Rich Text].
https://doc.qt.io/qt-6/richtext-html-subset.html[Qt Rich Text].
"""
message.error(text, rich=rich)
@ -379,7 +379,7 @@ def message_info(text: str, count: int = 1, rich: bool = False) -> None:
text: The text to show.
count: How many times to show the message.
rich: Render the given text as
https://doc.qt.io/qt-5/richtext-html-subset.html[Qt Rich Text].
https://doc.qt.io/qt-6/richtext-html-subset.html[Qt Rich Text].
"""
for _ in range(count):
message.info(text, rich=rich)
@ -392,7 +392,7 @@ def message_warning(text: str, rich: bool = False) -> None:
Args:
text: The text to show.
rich: Render the given text as
https://doc.qt.io/qt-5/richtext-html-subset.html[Qt Rich Text].
https://doc.qt.io/qt-6/richtext-html-subset.html[Qt Rich Text].
"""
message.warning(text, rich=rich)

View File

@ -22,7 +22,7 @@
import os
from typing import Iterable, Optional, MutableMapping, Any, Callable
from PyQt5.QtWidgets import QApplication, QLineEdit
from qutebrowser.qt.widgets import QApplication, QLineEdit
from qutebrowser.api import cmdutils

View File

@ -24,7 +24,7 @@ import os
import functools
from typing import IO, List, Optional
from PyQt5.QtCore import QUrl, QObject, pyqtSignal
from qutebrowser.qt.core import QUrl, QObject, pyqtSignal
from qutebrowser.api import downloads, message, config

View File

@ -25,7 +25,7 @@ import functools
from typing import (TYPE_CHECKING, Any, Callable, Dict, Iterator, List, Mapping,
MutableMapping, MutableSequence, Optional, Tuple, cast)
from PyQt5.QtCore import pyqtSignal, QObject, QUrl
from qutebrowser.qt.core import pyqtSignal, QObject, QUrl
from qutebrowser.commands import cmdexc, parser
from qutebrowser.config import configdata, configexc, configutils
@ -560,15 +560,18 @@ class Config(QObject):
log.config.debug("{} was mutated, updating".format(name))
self.set_obj(name, new_value, save_yaml=save_yaml)
def dump_userconfig(self) -> str:
def dump_userconfig(self, *, include_hidden: bool = False) -> str:
"""Get the part of the config which was changed by the user.
Args:
include_hidden: Include default scoped configs.
Return:
The changed config part as string.
"""
lines: List[str] = []
for values in sorted(self, key=lambda v: v.opt.name):
lines += values.dump()
lines += values.dump(include_hidden=include_hidden)
if not lines:
return '<Default configuration>'

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