From 2a3072191fb6e33bba6b06d5a123e7e16a326ec9 Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Tue, 5 Oct 2021 10:17:56 +1000 Subject: [PATCH 01/16] avoid importing directly from PyQt5 where possible --- qt/aqt/deckconf.py | 2 -- qt/aqt/pinnedmodules.py | 8 ++++++-- qt/aqt/reviewer.py | 2 -- qt/aqt/taskman.py | 4 +--- qt/aqt/utils.py | 12 ------------ qt/tests/run_pylint.py | 5 ++++- 6 files changed, 11 insertions(+), 22 deletions(-) diff --git a/qt/aqt/deckconf.py b/qt/aqt/deckconf.py index 89d250fe1..c7d5ac753 100644 --- a/qt/aqt/deckconf.py +++ b/qt/aqt/deckconf.py @@ -6,8 +6,6 @@ from __future__ import annotations from operator import itemgetter from typing import Any -from PyQt5.QtWidgets import QLineEdit - import aqt from anki.consts import NEW_CARDS_RANDOM from anki.decks import DeckConfigDict diff --git a/qt/aqt/pinnedmodules.py b/qt/aqt/pinnedmodules.py index a8cbaafe6..3586d4ff5 100644 --- a/qt/aqt/pinnedmodules.py +++ b/qt/aqt/pinnedmodules.py @@ -26,8 +26,12 @@ import typing import uuid # other modules we require that may not be automatically included -import PyQt5.QtSvg -import PyQt5.QtMultimedia +try: + import PyQt6.QtSvg # type: ignore + import PyQt6.QtMultimedia # type: ignore +except: + import PyQt5.QtSvg # type: ignore + import PyQt5.QtMultimedia # type: ignore import socks import pyaudio diff --git a/qt/aqt/reviewer.py b/qt/aqt/reviewer.py index 7d82b146f..31ba40529 100644 --- a/qt/aqt/reviewer.py +++ b/qt/aqt/reviewer.py @@ -13,8 +13,6 @@ from dataclasses import dataclass from enum import Enum, auto from typing import Any, Callable, Literal, Match, Sequence, cast -from PyQt5.QtCore import Qt - from anki import hooks from anki.cards import Card, CardId from anki.collection import Config, OpChanges, OpChangesWithCount diff --git a/qt/aqt/taskman.py b/qt/aqt/taskman.py index 3c62679ab..b763f96d4 100644 --- a/qt/aqt/taskman.py +++ b/qt/aqt/taskman.py @@ -14,10 +14,8 @@ from concurrent.futures.thread import ThreadPoolExecutor from threading import Lock from typing import Any, Callable -from PyQt5.QtCore import QObject, pyqtSignal - import aqt -from aqt.qt import QWidget, qconnect +from aqt.qt import * Closure = Callable[[], None] diff --git a/qt/aqt/utils.py b/qt/aqt/utils.py index 0fef71009..e4b80f594 100644 --- a/qt/aqt/utils.py +++ b/qt/aqt/utils.py @@ -9,18 +9,6 @@ import sys from functools import wraps from typing import TYPE_CHECKING, Any, Literal, Sequence, cast -from PyQt5.QtWidgets import ( - QAction, - QDialog, - QDialogButtonBox, - QFileDialog, - QHeaderView, - QMenu, - QPushButton, - QSplitter, - QWidget, -) - import aqt from anki.collection import Collection, HelpPage from anki.lang import TR, tr_legacyglobal # pylint: disable=unused-import diff --git a/qt/tests/run_pylint.py b/qt/tests/run_pylint.py index 964c7656e..fa9b1ad6e 100644 --- a/qt/tests/run_pylint.py +++ b/qt/tests/run_pylint.py @@ -6,7 +6,10 @@ import os import subprocess import sys -import PyQt5 +try: + import PyQt6 +except: + import PyQt5 from pylint.lint import Run if __name__ == "__main__": From 1701937cbaa9357c035921755842c84cf8c4941f Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Tue, 5 Oct 2021 09:41:26 +1000 Subject: [PATCH 02/16] expose pyqt6 packages - not yet used --- defs.bzl | 6 ++ pip/pyqt6/BUILD.bazel | 0 pip/pyqt6/defs.bzl | 47 +++++++++ pip/pyqt6/install_pyqt6.py | 196 +++++++++++++++++++++++++++++++++++ qt/.pylintrc | 2 +- qt/mypy.ini | 3 + scripts/copyright_headers.py | 1 + 7 files changed, 254 insertions(+), 1 deletion(-) create mode 100644 pip/pyqt6/BUILD.bazel create mode 100644 pip/pyqt6/defs.bzl create mode 100644 pip/pyqt6/install_pyqt6.py diff --git a/defs.bzl b/defs.bzl index 4c75a192b..1042004ac 100644 --- a/defs.bzl +++ b/defs.bzl @@ -9,6 +9,7 @@ load("@build_bazel_rules_nodejs//:index.bzl", "node_repositories", "yarn_install load("@io_bazel_rules_sass//:defs.bzl", "sass_repositories") load("@com_github_ali5h_rules_pip//:defs.bzl", "pip_import") load("//pip/pyqt5:defs.bzl", "install_pyqt5") +load("//pip/pyqt6:defs.bzl", "install_pyqt6") anki_version = "2.1.49" @@ -43,6 +44,11 @@ def setup_deps(): python_runtime = "@python//:python", ) + install_pyqt6( + name = "pyqt6", + python_runtime = "@python//:python", + ) + node_repositories(package_json = ["@ankidesktop//:package.json"]) yarn_install( diff --git a/pip/pyqt6/BUILD.bazel b/pip/pyqt6/BUILD.bazel new file mode 100644 index 000000000..e69de29bb diff --git a/pip/pyqt6/defs.bzl b/pip/pyqt6/defs.bzl new file mode 100644 index 000000000..ca4519cff --- /dev/null +++ b/pip/pyqt6/defs.bzl @@ -0,0 +1,47 @@ +# based off https://github.com/ali5h/rules_pip/blob/master/defs.bzl + +pip_vendor_label = Label("@com_github_ali5h_rules_pip//:third_party/py/easy_install.py") + +def _execute(repository_ctx, arguments, quiet = False): + pip_vendor = str(repository_ctx.path(pip_vendor_label).dirname) + return repository_ctx.execute(arguments, environment = { + "PYTHONPATH": pip_vendor, + }, quiet = quiet) + +def _install_pyqt6_impl(repository_ctx): + python_interpreter = repository_ctx.attr.python_interpreter + if repository_ctx.attr.python_runtime: + python_interpreter = repository_ctx.path(repository_ctx.attr.python_runtime) + + args = [ + python_interpreter, + repository_ctx.path(repository_ctx.attr._script), + repository_ctx.path("."), + ] + + result = _execute(repository_ctx, args, quiet = repository_ctx.attr.quiet) + if result.return_code: + fail("failed: %s (%s)" % (result.stdout, result.stderr)) + +install_pyqt6 = repository_rule( + attrs = { + "python_interpreter": attr.string(default = "python", doc = """ +The command to run the Python interpreter used to invoke pip and unpack the +wheels. +"""), + "python_runtime": attr.label(doc = """ +The label to the Python run-time interpreted used to invoke pip and unpack the wheels. +If the label is specified it will overwrite the python_interpreter attribute. +"""), + "_script": attr.label( + executable = True, + default = Label("//pip/pyqt6:install_pyqt6.py"), + cfg = "host", + ), + "quiet": attr.bool( + default = True, + doc = "If stdout and stderr should be printed to the terminal.", + ), + }, + implementation = _install_pyqt6_impl, +) diff --git a/pip/pyqt6/install_pyqt6.py b/pip/pyqt6/install_pyqt6.py new file mode 100644 index 000000000..a04193c11 --- /dev/null +++ b/pip/pyqt6/install_pyqt6.py @@ -0,0 +1,196 @@ +# based on https://github.com/ali5h/rules_pip/blob/master/src/whl.py +# MIT + +"""downloads and parses info of a pkg and generates a BUILD file for it""" +import argparse +import glob +import logging +import os +import re +import shutil +import subprocess +import sys + +import pkginfo + +from pip._internal.commands import create_command +from pip._vendor import pkg_resources + + +def _create_nspkg_init(dirpath): + """Creates an init file to enable namespacing""" + if not os.path.exists(dirpath): + # Handle missing namespace packages by ignoring them + return + nspkg_init = os.path.join(dirpath, "__init__.py") + with open(nspkg_init, "w") as nspkg: + nspkg.write("__path__ = __import__('pkgutil').extend_path(__path__, __name__)") + + +def install_package(pkg, directory, pip_args): + """Downloads wheel for a package. Assumes python binary provided has + pip and wheel package installed. + + Args: + pkg: package name + directory: destination directory to download the wheel file in + python: python binary path used to run pip command + pip_args: extra pip args sent to pip + Returns: + str: path to the wheel file + """ + pip_args = [ + "--isolated", + "--disable-pip-version-check", + "--target", + directory, + "--no-deps", + "--ignore-requires-python", + pkg, + ] + pip_args + cmd = create_command("install") + cmd.main(pip_args) + + # need dist-info directory for pkg_resources to be able to find the packages + dist_info = glob.glob(os.path.join(directory, "*.dist-info"))[0] + # fix namespace packages by adding proper __init__.py files + namespace_packages = os.path.join(dist_info, "namespace_packages.txt") + if os.path.exists(namespace_packages): + with open(namespace_packages) as nspkg: + for line in nspkg.readlines(): + namespace = line.strip().replace(".", os.sep) + if namespace: + _create_nspkg_init(os.path.join(directory, namespace)) + + # PEP 420 -- Implicit Namespace Packages + if (sys.version_info[0], sys.version_info[1]) >= (3, 3): + for dirpath, dirnames, filenames in os.walk(directory): + # we are only interested in dirs with no init file + if "__init__.py" in filenames: + dirnames[:] = [] + continue + # remove bin and dist-info dirs + for ignored in ("bin", os.path.basename(dist_info)): + if ignored in dirnames: + dirnames.remove(ignored) + _create_nspkg_init(dirpath) + + return pkginfo.Wheel(dist_info) + + +def _cleanup(directory, pattern): + for p in glob.glob(os.path.join(directory, pattern)): + shutil.rmtree(p) + + +fix_none = re.compile(r"(\s*None) =") + + +def copy_and_fix_pyi(source, dest): + "Fix broken PyQt types." + with open(source) as input_file: + with open(dest, "w") as output_file: + for line in input_file.readlines(): + # inheriting from the missing sip.sipwrapper definition + # causes missing attributes not to be detected, as it's treating + # the class as inheriting from Any + line = line.replace("PyQt6.sip.wrapper", "object") + line = line.replace("PyQt6.sip.wrapper", "object") + # # remove blanket getattr in QObject which also causes missing + # # attributes not to be detected + if "def __getattr__(self, name: str) -> typing.Any" in line: + continue + output_file.write(line) + + +def merge_files(root, source): + for dirpath, _dirnames, filenames in os.walk(source): + target_dir = os.path.join(root, os.path.relpath(dirpath, source)) + if not os.path.exists(target_dir): + os.mkdir(target_dir) + for fname in filenames: + source_path = os.path.join(dirpath, fname) + target_path = os.path.join(target_dir, fname) + if not os.path.exists(target_path): + if fname.endswith(".pyi"): + copy_and_fix_pyi(source_path, target_path) + else: + shutil.copy2(source_path, target_path) + + +def main(): + base = sys.argv[1] + + local_site_packages = os.environ.get("PYTHON_SITE_PACKAGES") + if local_site_packages: + subprocess.run( + [ + "rsync", + "-ai", + "--include=PyQt**", + "--exclude=*", + local_site_packages, + base + "/", + ], + check=True, + ) + with open(os.path.join(base, "__init__.py"), "w") as file: + pass + + else: + packages = [ + ("pyqt6", "pyqt6==6.2.0"), + ("pyqt6-qt6", "pyqt6-qt6==6.2.0"), + ("pyqt6-webengine", "pyqt6-webengine==6.2.0"), + ("pyqt6-webengine-qt6", "pyqt6-webengine-qt6==6.2.0"), + ("pyqt6-sip", "pyqt6_sip==13.1.0"), + ] + + for (name, with_version) in packages: + # install package in subfolder + folder = os.path.join(base, "temp") + _pkg = install_package(with_version, folder, []) + # merge into parent + merge_files(base, folder) + shutil.rmtree(folder) + + # add missing py.typed file + with open(os.path.join(base, "py.typed"), "w") as file: + pass + + result = """ +load("@rules_python//python:defs.bzl", "py_library") + +package(default_visibility = ["//visibility:public"]) + +py_library( + name = "pkg", + srcs = glob(["**/*.py"]), + data = glob(["**/*"], exclude = [ + "**/*.py", + "**/*.pyc", + "**/* *", + "BUILD", + "WORKSPACE", + "bin/*", + "__pycache__", + # these make building slower + "Qt/qml/**", + "**/*.sip", + "**/*.png", + ]), + # This makes this directory a top-level in the python import + # search path for anything that depends on this. + imports = ["."], +) +""" + + # clean up + _cleanup(base, "__pycache__") + + with open(os.path.join(base, "BUILD"), "w") as f: + f.write(result) + + +if __name__ == "__main__": + main() diff --git a/qt/.pylintrc b/qt/.pylintrc index 3cbdc97e9..510c08da4 100644 --- a/qt/.pylintrc +++ b/qt/.pylintrc @@ -1,6 +1,6 @@ [MASTER] persistent = no -extension-pkg-whitelist=PyQt5 +extension-pkg-whitelist=PyQt5,PyQt6 ignore = forms,hooks_gen.py [TYPECHECK] diff --git a/qt/mypy.ini b/qt/mypy.ini index 10a49657d..bb304e0f4 100644 --- a/qt/mypy.ini +++ b/qt/mypy.ini @@ -67,3 +67,6 @@ ignore_missing_imports = True disallow_untyped_defs=false [mypy-anki.*] disallow_untyped_defs=false + +[mypy-PyQt6.*] +ignore_errors = True \ No newline at end of file diff --git a/scripts/copyright_headers.py b/scripts/copyright_headers.py index 3aed02dba..4173c542d 100644 --- a/scripts/copyright_headers.py +++ b/scripts/copyright_headers.py @@ -7,6 +7,7 @@ from pathlib import Path nonstandard_header = { "pip/pyqt5/install_pyqt5.py", + "pip/pyqt6/install_pyqt6.py", "pylib/anki/importing/pauker.py", "pylib/anki/importing/supermemo_xml.py", "pylib/anki/statsbg.py", From 4d0a915610c5e56e9e6d18a5371f5e1dbf323284 Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Tue, 5 Oct 2021 10:38:07 +1000 Subject: [PATCH 03/16] move QtAudioInputRecorder into separate PyQt5-only file New API required for PyQt6. --- qt/aqt/qt5.py | 98 +++++++++++++++++++++++++++++++++++++++++++++++++ qt/aqt/sound.py | 93 +++++----------------------------------------- 2 files changed, 108 insertions(+), 83 deletions(-) create mode 100644 qt/aqt/qt5.py diff --git a/qt/aqt/qt5.py b/qt/aqt/qt5.py new file mode 100644 index 000000000..8cdf3aa72 --- /dev/null +++ b/qt/aqt/qt5.py @@ -0,0 +1,98 @@ +# Copyright: Ankitects Pty Ltd and contributors +# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html + +""" +PyQt5-only code +""" + +import wave +from concurrent.futures import Future +from typing import cast + +import aqt + +from .qt import * +from .sound import Recorder +from .utils import showWarning + + +class QtAudioInputRecorder(Recorder): + def __init__(self, output_path: str, mw: aqt.AnkiQt, parent: QWidget) -> None: + super().__init__(output_path) + + self.mw = mw + self._parent = parent + + from PyQt5.QtMultimedia import QAudioDeviceInfo, QAudioFormat, QAudioInput + + format = QAudioFormat() + format.setChannelCount(1) + format.setSampleRate(44100) + format.setSampleSize(16) + format.setCodec("audio/pcm") + format.setByteOrder(QAudioFormat.LittleEndian) + format.setSampleType(QAudioFormat.SignedInt) + + device = QAudioDeviceInfo.defaultInputDevice() + if not device.isFormatSupported(format): + format = device.nearestFormat(format) + print("format changed") + print("channels", format.channelCount()) + print("rate", format.sampleRate()) + print("size", format.sampleSize()) + self._format = format + + self._audio_input = QAudioInput(device, format, parent) + + def start(self, on_done: Callable[[], None]) -> None: + self._iodevice = self._audio_input.start() + self._buffer = b"" + self._iodevice.readyRead.connect(self._on_read_ready) # type: ignore + super().start(on_done) + + def _on_read_ready(self) -> None: + self._buffer += cast(bytes, self._iodevice.readAll()) + + def stop(self, on_done: Callable[[str], None]) -> None: + def on_stop_timer() -> None: + # read anything remaining in buffer & stop + self._on_read_ready() + self._audio_input.stop() + + if err := self._audio_input.error(): + showWarning(f"recording failed: {err}") + return + + def write_file() -> None: + # swallow the first 300ms to allow audio device to quiesce + wait = int(44100 * self.STARTUP_DELAY) + if len(self._buffer) <= wait: + return + self._buffer = self._buffer[wait:] + + # write out the wave file + wf = wave.open(self.output_path, "wb") + wf.setnchannels(self._format.channelCount()) + wf.setsampwidth(self._format.sampleSize() // 8) + wf.setframerate(self._format.sampleRate()) + wf.writeframes(self._buffer) + wf.close() + + def and_then(fut: Future) -> None: + fut.result() + Recorder.stop(self, on_done) + + self.mw.taskman.run_in_background(write_file, and_then) + + # schedule the stop for half a second in the future, + # to avoid truncating the end of the recording + self._stop_timer = t = QTimer(self._parent) + t.timeout.connect(on_stop_timer) # type: ignore + t.setSingleShot(True) + t.start(500) + + +def prompt_for_mic_permission() -> None: + from PyQt5.QtMultimedia import QAudioDeviceInfo + + QAudioDeviceInfo.defaultInputDevice() diff --git a/qt/aqt/sound.py b/qt/aqt/sound.py index d0d088889..3670413b1 100644 --- a/qt/aqt/sound.py +++ b/qt/aqt/sound.py @@ -15,7 +15,7 @@ import wave from abc import ABC, abstractmethod from concurrent.futures import Future from operator import itemgetter -from typing import TYPE_CHECKING, Any, Callable, cast +from typing import Any, Callable from markdown import markdown @@ -40,9 +40,6 @@ from aqt.utils import ( tr, ) -if TYPE_CHECKING: - from PyQt5.QtMultimedia import QAudioRecorder - # AV player protocol ########################################################################## @@ -549,80 +546,11 @@ class Recorder(ABC): ########################################################################## -class QtAudioInputRecorder(Recorder): - def __init__(self, output_path: str, mw: aqt.AnkiQt, parent: QWidget) -> None: - super().__init__(output_path) +def prompt_for_mic_permission() -> None: + if qtmajor == 5 and isMac: + from .qt5 import prompt_for_mic_permission - self.mw = mw - self._parent = parent - - from PyQt5.QtMultimedia import QAudioDeviceInfo, QAudioFormat, QAudioInput - - format = QAudioFormat() - format.setChannelCount(1) - format.setSampleRate(44100) - format.setSampleSize(16) - format.setCodec("audio/pcm") - format.setByteOrder(QAudioFormat.LittleEndian) - format.setSampleType(QAudioFormat.SignedInt) - - device = QAudioDeviceInfo.defaultInputDevice() - if not device.isFormatSupported(format): - format = device.nearestFormat(format) - print("format changed") - print("channels", format.channelCount()) - print("rate", format.sampleRate()) - print("size", format.sampleSize()) - self._format = format - - self._audio_input = QAudioInput(device, format, parent) - - def start(self, on_done: Callable[[], None]) -> None: - self._iodevice = self._audio_input.start() - self._buffer = b"" - self._iodevice.readyRead.connect(self._on_read_ready) # type: ignore - super().start(on_done) - - def _on_read_ready(self) -> None: - self._buffer += cast(bytes, self._iodevice.readAll()) - - def stop(self, on_done: Callable[[str], None]) -> None: - def on_stop_timer() -> None: - # read anything remaining in buffer & stop - self._on_read_ready() - self._audio_input.stop() - - if err := self._audio_input.error(): - showWarning(f"recording failed: {err}") - return - - def write_file() -> None: - # swallow the first 300ms to allow audio device to quiesce - wait = int(44100 * self.STARTUP_DELAY) - if len(self._buffer) <= wait: - return - self._buffer = self._buffer[wait:] - - # write out the wave file - wf = wave.open(self.output_path, "wb") - wf.setnchannels(self._format.channelCount()) - wf.setsampwidth(self._format.sampleSize() // 8) - wf.setframerate(self._format.sampleRate()) - wf.writeframes(self._buffer) - wf.close() - - def and_then(fut: Future) -> None: - fut.result() - Recorder.stop(self, on_done) - - self.mw.taskman.run_in_background(write_file, and_then) - - # schedule the stop for half a second in the future, - # to avoid truncating the end of the recording - self._stop_timer = t = QTimer(self._parent) - t.timeout.connect(on_stop_timer) # type: ignore - t.setSingleShot(True) - t.start(500) + prompt_for_mic_permission() # PyAudio recording @@ -645,11 +573,8 @@ class PyAudioThreadedRecorder(threading.Thread): self._startup_delay = startup_delay self.finish = False # though we're using pyaudio here, we rely on Qt to trigger - # the permission prompt on macOS - if isMac and qtminor > 12: - from PyQt5.QtMultimedia import QAudioDeviceInfo - - QAudioDeviceInfo.defaultInputDevice() + # the permission prompt + prompt_for_mic_permission() def run(self) -> None: chunk = 1024 @@ -761,9 +686,11 @@ class RecordDialog(QDialog): def _start_recording(self) -> None: driver = self.mw.pm.recording_driver() - if driver is RecordingDriver.PyAudio: + if driver is RecordingDriver.PyAudio or qtmajor > 5: self._recorder = PyAudioRecorder(self.mw, namedtmp("rec.wav")) elif driver is RecordingDriver.QtAudioInput: + from .qt5 import QtAudioInputRecorder + self._recorder = QtAudioInputRecorder( namedtmp("rec.wav"), self.mw, self._parent ) From dfcefaebe351413f264c5b661e2131b3ed32f897 Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Thu, 7 Oct 2021 15:36:50 +1000 Subject: [PATCH 04/16] miscellaneous pyqt6 compat fixes - add a few gates for qt5-specific behaviour - prepare for some changes to the typings in qt6 - map pickled Qt5 ByteArrays to Qt6 when running Qt6 --- qt/aqt/__init__.py | 2 -- qt/aqt/main.py | 2 +- qt/aqt/profiles.py | 41 ++++++++++++++++++++++++----------------- qt/aqt/qt.py | 7 +++++-- qt/aqt/qt5.py | 6 +++++- qt/aqt/switch.py | 3 ++- qt/aqt/tagedit.py | 2 +- qt/aqt/utils.py | 6 ++++-- qt/aqt/webview.py | 4 ++-- 9 files changed, 44 insertions(+), 29 deletions(-) diff --git a/qt/aqt/__init__.py b/qt/aqt/__init__.py index 4e29129ac..f910bbc1f 100644 --- a/qt/aqt/__init__.py +++ b/qt/aqt/__init__.py @@ -372,8 +372,6 @@ def setupGL(pm: aqt.profiles.ProfileManager) -> None: category = "debug" elif category == QtFatalMsg: category = "fatal" - elif category == QtSystemMsg: - category = "system" else: category = "unknown" context = "" diff --git a/qt/aqt/main.py b/qt/aqt/main.py index 344186cc1..967819ff8 100644 --- a/qt/aqt/main.py +++ b/qt/aqt/main.py @@ -1032,7 +1032,7 @@ title="{}" {}>{}""".format( def clearStateShortcuts(self) -> None: for qs in self.stateShortcuts: - sip.delete(qs) + sip.delete(qs) # type: ignore self.stateShortcuts = [] def onStudyKey(self) -> None: diff --git a/qt/aqt/profiles.py b/qt/aqt/profiles.py index 0016db4e1..3832dca5f 100644 --- a/qt/aqt/profiles.py +++ b/qt/aqt/profiles.py @@ -163,25 +163,32 @@ class ProfileManager: def _unpickle(self, data: bytes) -> Any: class Unpickler(pickle.Unpickler): - def find_class(self, module: str, name: str) -> Any: - if module == "PyQt5.sip": - try: - import PyQt5.sip # pylint: disable=unused-import - except: - # use old sip location - module = "sip" - fn = super().find_class(module, name) - if module == "sip" and name == "_unpickle_type": + def find_class(self, class_module: str, name: str) -> Any: + # handle sip lookup ourselves, mapping to current Qt version + if class_module == "sip" or class_module.endswith(".sip"): - def wrapper(mod, obj, args) -> Any: # type: ignore - if mod.startswith("PyQt4") and obj == "QByteArray": - # can't trust str objects from python 2 - return QByteArray() - return fn(mod, obj, args) + def unpickle_type(module: str, klass: str, args: Any) -> Any: + if qtmajor > 5: + module = module.replace("Qt5", "Qt6") + else: + module = module.replace("Qt6", "Qt5") + if klass == "QByteArray": + if module.startswith("PyQt4"): + # can't trust str objects from python 2 + return QByteArray() + else: + # return the bytes directly + return args[0] + elif name == "_unpickle_enum": + if qtmajor == 5: + return sip._unpickle_enum(module, klass, args) # type: ignore + else: + # old style enums can't be unpickled + return None + else: + return sip._unpickle_type(module, klass, args) # type: ignore - return wrapper - else: - return fn + return unpickle_type up = Unpickler(io.BytesIO(data), errors="ignore") return up.load() diff --git a/qt/aqt/qt.py b/qt/aqt/qt.py index 46cf9f6e7..1d53b9fcf 100644 --- a/qt/aqt/qt.py +++ b/qt/aqt/qt.py @@ -57,6 +57,9 @@ if qtmajor < 5 or (qtmajor == 5 and qtminor < 14): raise Exception("Anki does not support your Qt version.") -def qconnect(signal: Union[Callable, pyqtSignal], func: Callable) -> None: - "Helper to work around type checking not working with signal.connect(func)." +def qconnect( + signal: Union[Callable, pyqtSignal, pyqtBoundSignal], func: Callable +) -> None: + """Helper to work around type checking not working with signal.connect(func). + Not needed in PyQt6""" signal.connect(func) # type: ignore diff --git a/qt/aqt/qt5.py b/qt/aqt/qt5.py index 8cdf3aa72..401c1fc1a 100644 --- a/qt/aqt/qt5.py +++ b/qt/aqt/qt5.py @@ -23,7 +23,11 @@ class QtAudioInputRecorder(Recorder): self.mw = mw self._parent = parent - from PyQt5.QtMultimedia import QAudioDeviceInfo, QAudioFormat, QAudioInput + from PyQt5.QtMultimedia import ( # type: ignore + QAudioDeviceInfo, + QAudioFormat, + QAudioInput, + ) format = QAudioFormat() format.setChannelCount(1) diff --git a/qt/aqt/switch.py b/qt/aqt/switch.py index bc97b44fd..f9ad05343 100644 --- a/qt/aqt/switch.py +++ b/qt/aqt/switch.py @@ -1,5 +1,6 @@ # Copyright: Ankitects Pty Ltd and contributors # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html +from typing import cast from aqt import colors from aqt.qt import * @@ -128,7 +129,7 @@ class Switch(QAbstractButton): self._animate_toggle() def _animate_toggle(self) -> None: - animation = QPropertyAnimation(self, b"position", self) + animation = QPropertyAnimation(self, cast(QByteArray, b"position"), self) animation.setDuration(100) animation.setStartValue(self.start_position) animation.setEndValue(self.end_position) diff --git a/qt/aqt/tagedit.py b/qt/aqt/tagedit.py index b616b1737..69c31d320 100644 --- a/qt/aqt/tagedit.py +++ b/qt/aqt/tagedit.py @@ -99,7 +99,7 @@ class TagEdit(QLineEdit): self._completer.popup().hide() def hideCompleter(self) -> None: - if sip.isdeleted(self._completer): + if sip.isdeleted(self._completer): # type: ignore return self._completer.popup().hide() diff --git a/qt/aqt/utils.py b/qt/aqt/utils.py index e4b80f594..1aa282820 100644 --- a/qt/aqt/utils.py +++ b/qt/aqt/utils.py @@ -866,6 +866,8 @@ Add-ons, last update check: {} # adapted from version detection in qutebrowser def opengl_vendor() -> str | None: + if qtmajor != 5: + return "unknown" old_context = QOpenGLContext.currentContext() old_surface = None if old_context is None else old_context.surface() @@ -886,11 +888,11 @@ def opengl_vendor() -> str | None: # Can't use versionFunctions there return None - vp = QOpenGLVersionProfile() + vp = QOpenGLVersionProfile() # type: ignore vp.setVersion(2, 0) try: - vf = ctx.versionFunctions(vp) + vf = ctx.versionFunctions(vp) # type: ignore except ImportError as e: return None diff --git a/qt/aqt/webview.py b/qt/aqt/webview.py index a05e4d1ba..9b7399489 100644 --- a/qt/aqt/webview.py +++ b/qt/aqt/webview.py @@ -350,9 +350,9 @@ class AnkiWebView(QWebEngineView): if webscale: return float(webscale) - if isMac: + if qtmajor > 5 or isMac: return 1 - screen = QApplication.desktop().screen() + screen = QApplication.desktop().screen() # type: ignore if screen is None: return 1 From b0272f03360cd96f920b48047c7bdb22e3974b13 Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Tue, 5 Oct 2021 13:18:43 +1000 Subject: [PATCH 05/16] generate pyqt6 forms --- qt/aqt/forms/BUILD.bazel | 47 +++++++++++++++---- qt/aqt/forms/__init___qt6.py | 1 + qt/aqt/forms/about.py | 6 ++- qt/aqt/forms/about_qt6.py | 1 + qt/aqt/forms/addcards.py | 6 ++- qt/aqt/forms/addcards_qt6.py | 1 + qt/aqt/forms/addfield.py | 6 ++- qt/aqt/forms/addfield_qt6.py | 1 + qt/aqt/forms/addmodel.py | 6 ++- qt/aqt/forms/addmodel_qt6.py | 1 + qt/aqt/forms/addonconf.py | 6 ++- qt/aqt/forms/addonconf_qt6.py | 1 + qt/aqt/forms/addons.py | 6 ++- qt/aqt/forms/addons_qt6.py | 1 + qt/aqt/forms/browser.py | 6 ++- qt/aqt/forms/browser_qt6.py | 1 + qt/aqt/forms/browserdisp.py | 6 ++- qt/aqt/forms/browserdisp_qt6.py | 1 + qt/aqt/forms/browseropts.py | 6 ++- qt/aqt/forms/browseropts_qt6.py | 1 + qt/aqt/forms/{build_ui.py => build_ui_qt5.py} | 0 qt/aqt/forms/build_ui_qt5_qt6.py | 1 + qt/aqt/forms/build_ui_qt6.py | 39 +++++++++++++++ qt/aqt/forms/build_ui_qt6_qt6.py | 1 + qt/aqt/forms/changemap.py | 6 ++- qt/aqt/forms/changemap_qt6.py | 1 + qt/aqt/forms/changemodel.py | 6 ++- qt/aqt/forms/changemodel_qt6.py | 1 + qt/aqt/forms/clayout_top.py | 6 ++- qt/aqt/forms/clayout_top_qt6.py | 1 + qt/aqt/forms/compile.bzl | 20 ++++---- qt/aqt/forms/customstudy.py | 6 ++- qt/aqt/forms/customstudy_qt6.py | 1 + qt/aqt/forms/dconf.py | 6 ++- qt/aqt/forms/dconf_qt6.py | 1 + qt/aqt/forms/debug.py | 6 ++- qt/aqt/forms/debug_qt6.py | 1 + qt/aqt/forms/editaddon.py | 6 ++- qt/aqt/forms/editaddon_qt6.py | 1 + qt/aqt/forms/editcurrent.py | 6 ++- qt/aqt/forms/editcurrent_qt6.py | 1 + qt/aqt/forms/edithtml.py | 6 ++- qt/aqt/forms/edithtml_qt6.py | 1 + qt/aqt/forms/emptycards.py | 6 ++- qt/aqt/forms/emptycards_qt6.py | 1 + qt/aqt/forms/exporting.py | 6 ++- qt/aqt/forms/exporting_qt6.py | 1 + qt/aqt/forms/fields.py | 6 ++- qt/aqt/forms/fields_qt6.py | 1 + qt/aqt/forms/filtered_deck.py | 6 ++- qt/aqt/forms/filtered_deck_qt6.py | 1 + qt/aqt/forms/finddupes.py | 6 ++- qt/aqt/forms/finddupes_qt6.py | 1 + qt/aqt/forms/findreplace.py | 6 ++- qt/aqt/forms/findreplace_qt6.py | 1 + qt/aqt/forms/getaddons.py | 6 ++- qt/aqt/forms/getaddons_qt6.py | 1 + qt/aqt/forms/importing.py | 6 ++- qt/aqt/forms/importing_qt6.py | 1 + qt/aqt/forms/main.py | 6 ++- qt/aqt/forms/main_qt6.py | 1 + qt/aqt/forms/modelopts.py | 6 ++- qt/aqt/forms/modelopts_qt6.py | 1 + qt/aqt/forms/models.py | 6 ++- qt/aqt/forms/models_qt6.py | 1 + qt/aqt/forms/preferences.py | 6 ++- qt/aqt/forms/preferences_qt6.py | 1 + qt/aqt/forms/preview.py | 6 ++- qt/aqt/forms/preview_qt6.py | 1 + qt/aqt/forms/profiles.py | 6 ++- qt/aqt/forms/profiles_qt6.py | 1 + qt/aqt/forms/progress.py | 6 ++- qt/aqt/forms/progress_qt6.py | 1 + qt/aqt/forms/reposition.py | 6 ++- qt/aqt/forms/reposition_qt6.py | 1 + qt/aqt/forms/setgroup.py | 6 ++- qt/aqt/forms/setgroup_qt6.py | 1 + qt/aqt/forms/setlang.py | 6 ++- qt/aqt/forms/setlang_qt6.py | 1 + qt/aqt/forms/stats.py | 6 ++- qt/aqt/forms/stats_qt6.py | 1 + qt/aqt/forms/studydeck.py | 6 ++- qt/aqt/forms/studydeck_qt6.py | 1 + qt/aqt/forms/synclog.py | 6 ++- qt/aqt/forms/synclog_qt6.py | 1 + qt/aqt/forms/taglimit.py | 6 ++- qt/aqt/forms/taglimit_qt6.py | 1 + qt/aqt/forms/template.py | 6 ++- qt/aqt/forms/template_qt6.py | 1 + qt/mypy.ini | 6 ++- 90 files changed, 341 insertions(+), 61 deletions(-) create mode 120000 qt/aqt/forms/__init___qt6.py mode change 120000 => 100644 qt/aqt/forms/about.py create mode 120000 qt/aqt/forms/about_qt6.py mode change 120000 => 100644 qt/aqt/forms/addcards.py create mode 120000 qt/aqt/forms/addcards_qt6.py mode change 120000 => 100644 qt/aqt/forms/addfield.py create mode 120000 qt/aqt/forms/addfield_qt6.py mode change 120000 => 100644 qt/aqt/forms/addmodel.py create mode 120000 qt/aqt/forms/addmodel_qt6.py mode change 120000 => 100644 qt/aqt/forms/addonconf.py create mode 120000 qt/aqt/forms/addonconf_qt6.py mode change 120000 => 100644 qt/aqt/forms/addons.py create mode 120000 qt/aqt/forms/addons_qt6.py mode change 120000 => 100644 qt/aqt/forms/browser.py create mode 120000 qt/aqt/forms/browser_qt6.py mode change 120000 => 100644 qt/aqt/forms/browserdisp.py create mode 120000 qt/aqt/forms/browserdisp_qt6.py mode change 120000 => 100644 qt/aqt/forms/browseropts.py create mode 120000 qt/aqt/forms/browseropts_qt6.py rename qt/aqt/forms/{build_ui.py => build_ui_qt5.py} (100%) create mode 120000 qt/aqt/forms/build_ui_qt5_qt6.py create mode 100644 qt/aqt/forms/build_ui_qt6.py create mode 120000 qt/aqt/forms/build_ui_qt6_qt6.py mode change 120000 => 100644 qt/aqt/forms/changemap.py create mode 120000 qt/aqt/forms/changemap_qt6.py mode change 120000 => 100644 qt/aqt/forms/changemodel.py create mode 120000 qt/aqt/forms/changemodel_qt6.py mode change 120000 => 100644 qt/aqt/forms/clayout_top.py create mode 120000 qt/aqt/forms/clayout_top_qt6.py mode change 120000 => 100644 qt/aqt/forms/customstudy.py create mode 120000 qt/aqt/forms/customstudy_qt6.py mode change 120000 => 100644 qt/aqt/forms/dconf.py create mode 120000 qt/aqt/forms/dconf_qt6.py mode change 120000 => 100644 qt/aqt/forms/debug.py create mode 120000 qt/aqt/forms/debug_qt6.py mode change 120000 => 100644 qt/aqt/forms/editaddon.py create mode 120000 qt/aqt/forms/editaddon_qt6.py mode change 120000 => 100644 qt/aqt/forms/editcurrent.py create mode 120000 qt/aqt/forms/editcurrent_qt6.py mode change 120000 => 100644 qt/aqt/forms/edithtml.py create mode 120000 qt/aqt/forms/edithtml_qt6.py mode change 120000 => 100644 qt/aqt/forms/emptycards.py create mode 120000 qt/aqt/forms/emptycards_qt6.py mode change 120000 => 100644 qt/aqt/forms/exporting.py create mode 120000 qt/aqt/forms/exporting_qt6.py mode change 120000 => 100644 qt/aqt/forms/fields.py create mode 120000 qt/aqt/forms/fields_qt6.py mode change 120000 => 100644 qt/aqt/forms/filtered_deck.py create mode 120000 qt/aqt/forms/filtered_deck_qt6.py mode change 120000 => 100644 qt/aqt/forms/finddupes.py create mode 120000 qt/aqt/forms/finddupes_qt6.py mode change 120000 => 100644 qt/aqt/forms/findreplace.py create mode 120000 qt/aqt/forms/findreplace_qt6.py mode change 120000 => 100644 qt/aqt/forms/getaddons.py create mode 120000 qt/aqt/forms/getaddons_qt6.py mode change 120000 => 100644 qt/aqt/forms/importing.py create mode 120000 qt/aqt/forms/importing_qt6.py mode change 120000 => 100644 qt/aqt/forms/main.py create mode 120000 qt/aqt/forms/main_qt6.py mode change 120000 => 100644 qt/aqt/forms/modelopts.py create mode 120000 qt/aqt/forms/modelopts_qt6.py mode change 120000 => 100644 qt/aqt/forms/models.py create mode 120000 qt/aqt/forms/models_qt6.py mode change 120000 => 100644 qt/aqt/forms/preferences.py create mode 120000 qt/aqt/forms/preferences_qt6.py mode change 120000 => 100644 qt/aqt/forms/preview.py create mode 120000 qt/aqt/forms/preview_qt6.py mode change 120000 => 100644 qt/aqt/forms/profiles.py create mode 120000 qt/aqt/forms/profiles_qt6.py mode change 120000 => 100644 qt/aqt/forms/progress.py create mode 120000 qt/aqt/forms/progress_qt6.py mode change 120000 => 100644 qt/aqt/forms/reposition.py create mode 120000 qt/aqt/forms/reposition_qt6.py mode change 120000 => 100644 qt/aqt/forms/setgroup.py create mode 120000 qt/aqt/forms/setgroup_qt6.py mode change 120000 => 100644 qt/aqt/forms/setlang.py create mode 120000 qt/aqt/forms/setlang_qt6.py mode change 120000 => 100644 qt/aqt/forms/stats.py create mode 120000 qt/aqt/forms/stats_qt6.py mode change 120000 => 100644 qt/aqt/forms/studydeck.py create mode 120000 qt/aqt/forms/studydeck_qt6.py mode change 120000 => 100644 qt/aqt/forms/synclog.py create mode 120000 qt/aqt/forms/synclog_qt6.py mode change 120000 => 100644 qt/aqt/forms/taglimit.py create mode 120000 qt/aqt/forms/taglimit_qt6.py mode change 120000 => 100644 qt/aqt/forms/template.py create mode 120000 qt/aqt/forms/template_qt6.py diff --git a/qt/aqt/forms/BUILD.bazel b/qt/aqt/forms/BUILD.bazel index 931df6037..aedbaf453 100644 --- a/qt/aqt/forms/BUILD.bazel +++ b/qt/aqt/forms/BUILD.bazel @@ -1,17 +1,48 @@ load("@rules_python//python:defs.bzl", "py_binary") load("compile.bzl", "compile_all") +py_binary( + name = "build_ui_qt5", + srcs = ["build_ui_qt5.py"], + legacy_create_init = False, + deps = ["@pyqt5//:pkg"], +) + +py_binary( + name = "build_ui_qt6", + srcs = ["build_ui_qt6.py"], + legacy_create_init = False, + deps = ["@pyqt6//:pkg"], +) + compile_all( + name = "forms_qt5", srcs = glob(["*.ui"]), - group = "forms", + builder = "build_ui_qt5", + suffix = "_qt5", +) + +compile_all( + name = "forms_qt6", + srcs = glob(["*.ui"]), + builder = "build_ui_qt6", + suffix = "_qt6", +) + +filegroup( + name = "forms", + srcs = glob( + ["*.py"], + exclude = [ + "*_qt5.py", + "*_qt6.py", + "build_ui*.py", + ], + ) + [ + ":forms_qt6", + ":forms_qt5", + ], visibility = [ "//qt/aqt:__pkg__", ], ) - -py_binary( - name = "build_ui", - srcs = ["build_ui.py"], - legacy_create_init = False, - deps = ["@pyqt5//:pkg"], -) diff --git a/qt/aqt/forms/__init___qt6.py b/qt/aqt/forms/__init___qt6.py new file mode 120000 index 000000000..d09f3c7c6 --- /dev/null +++ b/qt/aqt/forms/__init___qt6.py @@ -0,0 +1 @@ +../../../bazel-bin/qt/aqt/forms/__init___qt6.py \ No newline at end of file diff --git a/qt/aqt/forms/about.py b/qt/aqt/forms/about.py deleted file mode 120000 index e70429a4a..000000000 --- a/qt/aqt/forms/about.py +++ /dev/null @@ -1 +0,0 @@ -../../../bazel-bin/qt/aqt/forms/about.py \ No newline at end of file diff --git a/qt/aqt/forms/about.py b/qt/aqt/forms/about.py new file mode 100644 index 000000000..8622ff868 --- /dev/null +++ b/qt/aqt/forms/about.py @@ -0,0 +1,5 @@ +from aqt.qt import qtmajor +if qtmajor > 5: + from .about_qt6 import * +else: + from .about_qt5 import * # type: ignore diff --git a/qt/aqt/forms/about_qt6.py b/qt/aqt/forms/about_qt6.py new file mode 120000 index 000000000..9346a7ee6 --- /dev/null +++ b/qt/aqt/forms/about_qt6.py @@ -0,0 +1 @@ +../../../bazel-bin/qt/aqt/forms/about_qt6.py \ No newline at end of file diff --git a/qt/aqt/forms/addcards.py b/qt/aqt/forms/addcards.py deleted file mode 120000 index f54c8fe40..000000000 --- a/qt/aqt/forms/addcards.py +++ /dev/null @@ -1 +0,0 @@ -../../../bazel-bin/qt/aqt/forms/addcards.py \ No newline at end of file diff --git a/qt/aqt/forms/addcards.py b/qt/aqt/forms/addcards.py new file mode 100644 index 000000000..74082d63f --- /dev/null +++ b/qt/aqt/forms/addcards.py @@ -0,0 +1,5 @@ +from aqt.qt import qtmajor +if qtmajor > 5: + from .addcards_qt6 import * +else: + from .addcards_qt5 import * # type: ignore diff --git a/qt/aqt/forms/addcards_qt6.py b/qt/aqt/forms/addcards_qt6.py new file mode 120000 index 000000000..2023fe24a --- /dev/null +++ b/qt/aqt/forms/addcards_qt6.py @@ -0,0 +1 @@ +../../../bazel-bin/qt/aqt/forms/addcards_qt6.py \ No newline at end of file diff --git a/qt/aqt/forms/addfield.py b/qt/aqt/forms/addfield.py deleted file mode 120000 index 1a54703f0..000000000 --- a/qt/aqt/forms/addfield.py +++ /dev/null @@ -1 +0,0 @@ -../../../bazel-bin/qt/aqt/forms/addfield.py \ No newline at end of file diff --git a/qt/aqt/forms/addfield.py b/qt/aqt/forms/addfield.py new file mode 100644 index 000000000..839061794 --- /dev/null +++ b/qt/aqt/forms/addfield.py @@ -0,0 +1,5 @@ +from aqt.qt import qtmajor +if qtmajor > 5: + from .addfield_qt6 import * +else: + from .addfield_qt5 import * # type: ignore diff --git a/qt/aqt/forms/addfield_qt6.py b/qt/aqt/forms/addfield_qt6.py new file mode 120000 index 000000000..6074f2a7e --- /dev/null +++ b/qt/aqt/forms/addfield_qt6.py @@ -0,0 +1 @@ +../../../bazel-bin/qt/aqt/forms/addfield_qt6.py \ No newline at end of file diff --git a/qt/aqt/forms/addmodel.py b/qt/aqt/forms/addmodel.py deleted file mode 120000 index 319620610..000000000 --- a/qt/aqt/forms/addmodel.py +++ /dev/null @@ -1 +0,0 @@ -../../../bazel-bin/qt/aqt/forms/addmodel.py \ No newline at end of file diff --git a/qt/aqt/forms/addmodel.py b/qt/aqt/forms/addmodel.py new file mode 100644 index 000000000..6128f1649 --- /dev/null +++ b/qt/aqt/forms/addmodel.py @@ -0,0 +1,5 @@ +from aqt.qt import qtmajor +if qtmajor > 5: + from .addmodel_qt6 import * +else: + from .addmodel_qt5 import * # type: ignore diff --git a/qt/aqt/forms/addmodel_qt6.py b/qt/aqt/forms/addmodel_qt6.py new file mode 120000 index 000000000..c82ec7f3a --- /dev/null +++ b/qt/aqt/forms/addmodel_qt6.py @@ -0,0 +1 @@ +../../../bazel-bin/qt/aqt/forms/addmodel_qt6.py \ No newline at end of file diff --git a/qt/aqt/forms/addonconf.py b/qt/aqt/forms/addonconf.py deleted file mode 120000 index de53ecee1..000000000 --- a/qt/aqt/forms/addonconf.py +++ /dev/null @@ -1 +0,0 @@ -../../../bazel-bin/qt/aqt/forms/addonconf.py \ No newline at end of file diff --git a/qt/aqt/forms/addonconf.py b/qt/aqt/forms/addonconf.py new file mode 100644 index 000000000..759f9b11f --- /dev/null +++ b/qt/aqt/forms/addonconf.py @@ -0,0 +1,5 @@ +from aqt.qt import qtmajor +if qtmajor > 5: + from .addonconf_qt6 import * +else: + from .addonconf_qt5 import * # type: ignore diff --git a/qt/aqt/forms/addonconf_qt6.py b/qt/aqt/forms/addonconf_qt6.py new file mode 120000 index 000000000..660196e9d --- /dev/null +++ b/qt/aqt/forms/addonconf_qt6.py @@ -0,0 +1 @@ +../../../bazel-bin/qt/aqt/forms/addonconf_qt6.py \ No newline at end of file diff --git a/qt/aqt/forms/addons.py b/qt/aqt/forms/addons.py deleted file mode 120000 index a13b69944..000000000 --- a/qt/aqt/forms/addons.py +++ /dev/null @@ -1 +0,0 @@ -../../../bazel-bin/qt/aqt/forms/addons.py \ No newline at end of file diff --git a/qt/aqt/forms/addons.py b/qt/aqt/forms/addons.py new file mode 100644 index 000000000..fd423756e --- /dev/null +++ b/qt/aqt/forms/addons.py @@ -0,0 +1,5 @@ +from aqt.qt import qtmajor +if qtmajor > 5: + from .addons_qt6 import * +else: + from .addons_qt5 import * # type: ignore diff --git a/qt/aqt/forms/addons_qt6.py b/qt/aqt/forms/addons_qt6.py new file mode 120000 index 000000000..50257706b --- /dev/null +++ b/qt/aqt/forms/addons_qt6.py @@ -0,0 +1 @@ +../../../bazel-bin/qt/aqt/forms/addons_qt6.py \ No newline at end of file diff --git a/qt/aqt/forms/browser.py b/qt/aqt/forms/browser.py deleted file mode 120000 index 592ca70b4..000000000 --- a/qt/aqt/forms/browser.py +++ /dev/null @@ -1 +0,0 @@ -../../../bazel-bin/qt/aqt/forms/browser.py \ No newline at end of file diff --git a/qt/aqt/forms/browser.py b/qt/aqt/forms/browser.py new file mode 100644 index 000000000..8f443ff4a --- /dev/null +++ b/qt/aqt/forms/browser.py @@ -0,0 +1,5 @@ +from aqt.qt import qtmajor +if qtmajor > 5: + from .browser_qt6 import * +else: + from .browser_qt5 import * # type: ignore diff --git a/qt/aqt/forms/browser_qt6.py b/qt/aqt/forms/browser_qt6.py new file mode 120000 index 000000000..d3e9e9e7b --- /dev/null +++ b/qt/aqt/forms/browser_qt6.py @@ -0,0 +1 @@ +../../../bazel-bin/qt/aqt/forms/browser_qt6.py \ No newline at end of file diff --git a/qt/aqt/forms/browserdisp.py b/qt/aqt/forms/browserdisp.py deleted file mode 120000 index d1017e9e2..000000000 --- a/qt/aqt/forms/browserdisp.py +++ /dev/null @@ -1 +0,0 @@ -../../../bazel-bin/qt/aqt/forms/browserdisp.py \ No newline at end of file diff --git a/qt/aqt/forms/browserdisp.py b/qt/aqt/forms/browserdisp.py new file mode 100644 index 000000000..c5bc36bd3 --- /dev/null +++ b/qt/aqt/forms/browserdisp.py @@ -0,0 +1,5 @@ +from aqt.qt import qtmajor +if qtmajor > 5: + from .browserdisp_qt6 import * +else: + from .browserdisp_qt5 import * # type: ignore diff --git a/qt/aqt/forms/browserdisp_qt6.py b/qt/aqt/forms/browserdisp_qt6.py new file mode 120000 index 000000000..17b1ce3a0 --- /dev/null +++ b/qt/aqt/forms/browserdisp_qt6.py @@ -0,0 +1 @@ +../../../bazel-bin/qt/aqt/forms/browserdisp_qt6.py \ No newline at end of file diff --git a/qt/aqt/forms/browseropts.py b/qt/aqt/forms/browseropts.py deleted file mode 120000 index 2f97ae88f..000000000 --- a/qt/aqt/forms/browseropts.py +++ /dev/null @@ -1 +0,0 @@ -../../../bazel-bin/qt/aqt/forms/browseropts.py \ No newline at end of file diff --git a/qt/aqt/forms/browseropts.py b/qt/aqt/forms/browseropts.py new file mode 100644 index 000000000..d0fd9dbca --- /dev/null +++ b/qt/aqt/forms/browseropts.py @@ -0,0 +1,5 @@ +from aqt.qt import qtmajor +if qtmajor > 5: + from .browseropts_qt6 import * +else: + from .browseropts_qt5 import * # type: ignore diff --git a/qt/aqt/forms/browseropts_qt6.py b/qt/aqt/forms/browseropts_qt6.py new file mode 120000 index 000000000..85414176c --- /dev/null +++ b/qt/aqt/forms/browseropts_qt6.py @@ -0,0 +1 @@ +../../../bazel-bin/qt/aqt/forms/browseropts_qt6.py \ No newline at end of file diff --git a/qt/aqt/forms/build_ui.py b/qt/aqt/forms/build_ui_qt5.py similarity index 100% rename from qt/aqt/forms/build_ui.py rename to qt/aqt/forms/build_ui_qt5.py diff --git a/qt/aqt/forms/build_ui_qt5_qt6.py b/qt/aqt/forms/build_ui_qt5_qt6.py new file mode 120000 index 000000000..b90c66ba6 --- /dev/null +++ b/qt/aqt/forms/build_ui_qt5_qt6.py @@ -0,0 +1 @@ +../../../bazel-bin/qt/aqt/forms/build_ui_qt5_qt6.py \ No newline at end of file diff --git a/qt/aqt/forms/build_ui_qt6.py b/qt/aqt/forms/build_ui_qt6.py new file mode 100644 index 000000000..26d230c10 --- /dev/null +++ b/qt/aqt/forms/build_ui_qt6.py @@ -0,0 +1,39 @@ +import re +import sys +import io +from PyQt6.uic import compileUi + +ui_file = sys.argv[1] +py_file = sys.argv[2] +buf = io.StringIO() +compileUi(open(ui_file), buf) + +outdata = buf.getvalue() +outdata = outdata.replace( + "from PyQt6 import QtCore, QtGui, QtWidgets", + "from PyQt6 import QtCore, QtGui, QtWidgets\nfrom aqt.utils import tr\n" +) +outdata = re.sub( + r'(?:QtGui\.QApplication\.)?_?translate\(".*?", "(.*?)"', "tr.\\1(", outdata +) + + +outlines = [] +qt_bad_types = [ + ".connect(", +] +for line in outdata.splitlines(): + for substr in qt_bad_types: + if substr in line: + line = line + " # type: ignore" + break + if line == "from . import icons_rc": + continue + line = line.replace(":/icons/", "icons:") + line = line.replace("QAction.PreferencesRole", "QAction.MenuRole.PreferencesRole") + line = line.replace("QAction.AboutRole", "QAction.MenuRole.AboutRole") + line = line.replace("QComboBox.AdjustToMinimumContentsLength", "QComboBox.SizeAdjustPolicy.AdjustToMinimumContentsLength") + outlines.append(line) + +with open(py_file, "w") as file: + file.write("\n".join(outlines)) diff --git a/qt/aqt/forms/build_ui_qt6_qt6.py b/qt/aqt/forms/build_ui_qt6_qt6.py new file mode 120000 index 000000000..70bd11c7c --- /dev/null +++ b/qt/aqt/forms/build_ui_qt6_qt6.py @@ -0,0 +1 @@ +../../../bazel-bin/qt/aqt/forms/build_ui_qt6_qt6.py \ No newline at end of file diff --git a/qt/aqt/forms/changemap.py b/qt/aqt/forms/changemap.py deleted file mode 120000 index 96f98d078..000000000 --- a/qt/aqt/forms/changemap.py +++ /dev/null @@ -1 +0,0 @@ -../../../bazel-bin/qt/aqt/forms/changemap.py \ No newline at end of file diff --git a/qt/aqt/forms/changemap.py b/qt/aqt/forms/changemap.py new file mode 100644 index 000000000..d6dc2d060 --- /dev/null +++ b/qt/aqt/forms/changemap.py @@ -0,0 +1,5 @@ +from aqt.qt import qtmajor +if qtmajor > 5: + from .changemap_qt6 import * +else: + from .changemap_qt5 import * # type: ignore diff --git a/qt/aqt/forms/changemap_qt6.py b/qt/aqt/forms/changemap_qt6.py new file mode 120000 index 000000000..7dfb40223 --- /dev/null +++ b/qt/aqt/forms/changemap_qt6.py @@ -0,0 +1 @@ +../../../bazel-bin/qt/aqt/forms/changemap_qt6.py \ No newline at end of file diff --git a/qt/aqt/forms/changemodel.py b/qt/aqt/forms/changemodel.py deleted file mode 120000 index 8a83a2d08..000000000 --- a/qt/aqt/forms/changemodel.py +++ /dev/null @@ -1 +0,0 @@ -../../../bazel-bin/qt/aqt/forms/changemodel.py \ No newline at end of file diff --git a/qt/aqt/forms/changemodel.py b/qt/aqt/forms/changemodel.py new file mode 100644 index 000000000..2b0a10492 --- /dev/null +++ b/qt/aqt/forms/changemodel.py @@ -0,0 +1,5 @@ +from aqt.qt import qtmajor +if qtmajor > 5: + from .changemodel_qt6 import * +else: + from .changemodel_qt5 import * # type: ignore diff --git a/qt/aqt/forms/changemodel_qt6.py b/qt/aqt/forms/changemodel_qt6.py new file mode 120000 index 000000000..d2b9bfda9 --- /dev/null +++ b/qt/aqt/forms/changemodel_qt6.py @@ -0,0 +1 @@ +../../../bazel-bin/qt/aqt/forms/changemodel_qt6.py \ No newline at end of file diff --git a/qt/aqt/forms/clayout_top.py b/qt/aqt/forms/clayout_top.py deleted file mode 120000 index 1a1d15638..000000000 --- a/qt/aqt/forms/clayout_top.py +++ /dev/null @@ -1 +0,0 @@ -../../../bazel-bin/qt/aqt/forms/clayout_top.py \ No newline at end of file diff --git a/qt/aqt/forms/clayout_top.py b/qt/aqt/forms/clayout_top.py new file mode 100644 index 000000000..9c15a8f07 --- /dev/null +++ b/qt/aqt/forms/clayout_top.py @@ -0,0 +1,5 @@ +from aqt.qt import qtmajor +if qtmajor > 5: + from .clayout_top_qt6 import * +else: + from .clayout_top_qt5 import * # type: ignore diff --git a/qt/aqt/forms/clayout_top_qt6.py b/qt/aqt/forms/clayout_top_qt6.py new file mode 120000 index 000000000..63bb2ba0a --- /dev/null +++ b/qt/aqt/forms/clayout_top_qt6.py @@ -0,0 +1 @@ +../../../bazel-bin/qt/aqt/forms/clayout_top_qt6.py \ No newline at end of file diff --git a/qt/aqt/forms/compile.bzl b/qt/aqt/forms/compile.bzl index 36a654f70..4b6751c0f 100644 --- a/qt/aqt/forms/compile.bzl +++ b/qt/aqt/forms/compile.bzl @@ -1,28 +1,28 @@ -def compile(name, ui_file, py_file): +def compile(name, ui_file, py_file, builder): native.genrule( name = name, srcs = [ui_file], outs = [py_file], - cmd = "$(location build_ui) $(location {ui_file}) $(location {py_file})".format( + cmd = "$(location {builder}) $(location {ui_file}) $(location {py_file})".format( + builder = builder, ui_file = ui_file, py_file = py_file, ), tools = [ - "build_ui", + builder, ], message = "Building UI", ) -def compile_all(group, srcs, visibility): +def compile_all(name, builder, srcs, suffix): py_files = [] for ui_file in srcs: - name = ui_file.replace(".ui", "") - py_file = name + ".py" + fname = ui_file.replace(".ui", "") + suffix + py_file = fname + ".py" py_files.append(py_file) - compile(name, ui_file, py_file) + compile(fname, ui_file, py_file, builder) native.filegroup( - name = group, - srcs = py_files + ["__init__.py"], - visibility = visibility, + name = name, + srcs = py_files, ) diff --git a/qt/aqt/forms/customstudy.py b/qt/aqt/forms/customstudy.py deleted file mode 120000 index 7e062decc..000000000 --- a/qt/aqt/forms/customstudy.py +++ /dev/null @@ -1 +0,0 @@ -../../../bazel-bin/qt/aqt/forms/customstudy.py \ No newline at end of file diff --git a/qt/aqt/forms/customstudy.py b/qt/aqt/forms/customstudy.py new file mode 100644 index 000000000..315e65d7d --- /dev/null +++ b/qt/aqt/forms/customstudy.py @@ -0,0 +1,5 @@ +from aqt.qt import qtmajor +if qtmajor > 5: + from .customstudy_qt6 import * +else: + from .customstudy_qt5 import * # type: ignore diff --git a/qt/aqt/forms/customstudy_qt6.py b/qt/aqt/forms/customstudy_qt6.py new file mode 120000 index 000000000..fc030c146 --- /dev/null +++ b/qt/aqt/forms/customstudy_qt6.py @@ -0,0 +1 @@ +../../../bazel-bin/qt/aqt/forms/customstudy_qt6.py \ No newline at end of file diff --git a/qt/aqt/forms/dconf.py b/qt/aqt/forms/dconf.py deleted file mode 120000 index f90767bfa..000000000 --- a/qt/aqt/forms/dconf.py +++ /dev/null @@ -1 +0,0 @@ -../../../bazel-bin/qt/aqt/forms/dconf.py \ No newline at end of file diff --git a/qt/aqt/forms/dconf.py b/qt/aqt/forms/dconf.py new file mode 100644 index 000000000..6f540938f --- /dev/null +++ b/qt/aqt/forms/dconf.py @@ -0,0 +1,5 @@ +from aqt.qt import qtmajor +if qtmajor > 5: + from .dconf_qt6 import * +else: + from .dconf_qt5 import * # type: ignore diff --git a/qt/aqt/forms/dconf_qt6.py b/qt/aqt/forms/dconf_qt6.py new file mode 120000 index 000000000..1ee73eefd --- /dev/null +++ b/qt/aqt/forms/dconf_qt6.py @@ -0,0 +1 @@ +../../../bazel-bin/qt/aqt/forms/dconf_qt6.py \ No newline at end of file diff --git a/qt/aqt/forms/debug.py b/qt/aqt/forms/debug.py deleted file mode 120000 index eeb139524..000000000 --- a/qt/aqt/forms/debug.py +++ /dev/null @@ -1 +0,0 @@ -../../../bazel-bin/qt/aqt/forms/debug.py \ No newline at end of file diff --git a/qt/aqt/forms/debug.py b/qt/aqt/forms/debug.py new file mode 100644 index 000000000..e4bcd75c1 --- /dev/null +++ b/qt/aqt/forms/debug.py @@ -0,0 +1,5 @@ +from aqt.qt import qtmajor +if qtmajor > 5: + from .debug_qt6 import * +else: + from .debug_qt5 import * # type: ignore diff --git a/qt/aqt/forms/debug_qt6.py b/qt/aqt/forms/debug_qt6.py new file mode 120000 index 000000000..29accf4d0 --- /dev/null +++ b/qt/aqt/forms/debug_qt6.py @@ -0,0 +1 @@ +../../../bazel-bin/qt/aqt/forms/debug_qt6.py \ No newline at end of file diff --git a/qt/aqt/forms/editaddon.py b/qt/aqt/forms/editaddon.py deleted file mode 120000 index 1f692c333..000000000 --- a/qt/aqt/forms/editaddon.py +++ /dev/null @@ -1 +0,0 @@ -../../../bazel-bin/qt/aqt/forms/editaddon.py \ No newline at end of file diff --git a/qt/aqt/forms/editaddon.py b/qt/aqt/forms/editaddon.py new file mode 100644 index 000000000..da7a02b89 --- /dev/null +++ b/qt/aqt/forms/editaddon.py @@ -0,0 +1,5 @@ +from aqt.qt import qtmajor +if qtmajor > 5: + from .editaddon_qt6 import * +else: + from .editaddon_qt5 import * # type: ignore diff --git a/qt/aqt/forms/editaddon_qt6.py b/qt/aqt/forms/editaddon_qt6.py new file mode 120000 index 000000000..61a93235e --- /dev/null +++ b/qt/aqt/forms/editaddon_qt6.py @@ -0,0 +1 @@ +../../../bazel-bin/qt/aqt/forms/editaddon_qt6.py \ No newline at end of file diff --git a/qt/aqt/forms/editcurrent.py b/qt/aqt/forms/editcurrent.py deleted file mode 120000 index 7298dc774..000000000 --- a/qt/aqt/forms/editcurrent.py +++ /dev/null @@ -1 +0,0 @@ -../../../bazel-bin/qt/aqt/forms/editcurrent.py \ No newline at end of file diff --git a/qt/aqt/forms/editcurrent.py b/qt/aqt/forms/editcurrent.py new file mode 100644 index 000000000..a9f03c6ea --- /dev/null +++ b/qt/aqt/forms/editcurrent.py @@ -0,0 +1,5 @@ +from aqt.qt import qtmajor +if qtmajor > 5: + from .editcurrent_qt6 import * +else: + from .editcurrent_qt5 import * # type: ignore diff --git a/qt/aqt/forms/editcurrent_qt6.py b/qt/aqt/forms/editcurrent_qt6.py new file mode 120000 index 000000000..182216a50 --- /dev/null +++ b/qt/aqt/forms/editcurrent_qt6.py @@ -0,0 +1 @@ +../../../bazel-bin/qt/aqt/forms/editcurrent_qt6.py \ No newline at end of file diff --git a/qt/aqt/forms/edithtml.py b/qt/aqt/forms/edithtml.py deleted file mode 120000 index 865dee6af..000000000 --- a/qt/aqt/forms/edithtml.py +++ /dev/null @@ -1 +0,0 @@ -../../../bazel-bin/qt/aqt/forms/edithtml.py \ No newline at end of file diff --git a/qt/aqt/forms/edithtml.py b/qt/aqt/forms/edithtml.py new file mode 100644 index 000000000..92f792344 --- /dev/null +++ b/qt/aqt/forms/edithtml.py @@ -0,0 +1,5 @@ +from aqt.qt import qtmajor +if qtmajor > 5: + from .edithtml_qt6 import * +else: + from .edithtml_qt5 import * # type: ignore diff --git a/qt/aqt/forms/edithtml_qt6.py b/qt/aqt/forms/edithtml_qt6.py new file mode 120000 index 000000000..4d5732294 --- /dev/null +++ b/qt/aqt/forms/edithtml_qt6.py @@ -0,0 +1 @@ +../../../bazel-bin/qt/aqt/forms/edithtml_qt6.py \ No newline at end of file diff --git a/qt/aqt/forms/emptycards.py b/qt/aqt/forms/emptycards.py deleted file mode 120000 index 7a520f0d4..000000000 --- a/qt/aqt/forms/emptycards.py +++ /dev/null @@ -1 +0,0 @@ -../../../bazel-bin/qt/aqt/forms/emptycards.py \ No newline at end of file diff --git a/qt/aqt/forms/emptycards.py b/qt/aqt/forms/emptycards.py new file mode 100644 index 000000000..616ea2ed4 --- /dev/null +++ b/qt/aqt/forms/emptycards.py @@ -0,0 +1,5 @@ +from aqt.qt import qtmajor +if qtmajor > 5: + from .emptycards_qt6 import * +else: + from .emptycards_qt5 import * # type: ignore diff --git a/qt/aqt/forms/emptycards_qt6.py b/qt/aqt/forms/emptycards_qt6.py new file mode 120000 index 000000000..7c3b36e0f --- /dev/null +++ b/qt/aqt/forms/emptycards_qt6.py @@ -0,0 +1 @@ +../../../bazel-bin/qt/aqt/forms/emptycards_qt6.py \ No newline at end of file diff --git a/qt/aqt/forms/exporting.py b/qt/aqt/forms/exporting.py deleted file mode 120000 index 46f6c94a9..000000000 --- a/qt/aqt/forms/exporting.py +++ /dev/null @@ -1 +0,0 @@ -../../../bazel-bin/qt/aqt/forms/exporting.py \ No newline at end of file diff --git a/qt/aqt/forms/exporting.py b/qt/aqt/forms/exporting.py new file mode 100644 index 000000000..2ce625bcd --- /dev/null +++ b/qt/aqt/forms/exporting.py @@ -0,0 +1,5 @@ +from aqt.qt import qtmajor +if qtmajor > 5: + from .exporting_qt6 import * +else: + from .exporting_qt5 import * # type: ignore diff --git a/qt/aqt/forms/exporting_qt6.py b/qt/aqt/forms/exporting_qt6.py new file mode 120000 index 000000000..d3e88464f --- /dev/null +++ b/qt/aqt/forms/exporting_qt6.py @@ -0,0 +1 @@ +../../../bazel-bin/qt/aqt/forms/exporting_qt6.py \ No newline at end of file diff --git a/qt/aqt/forms/fields.py b/qt/aqt/forms/fields.py deleted file mode 120000 index ce2d542bd..000000000 --- a/qt/aqt/forms/fields.py +++ /dev/null @@ -1 +0,0 @@ -../../../bazel-bin/qt/aqt/forms/fields.py \ No newline at end of file diff --git a/qt/aqt/forms/fields.py b/qt/aqt/forms/fields.py new file mode 100644 index 000000000..2c1512701 --- /dev/null +++ b/qt/aqt/forms/fields.py @@ -0,0 +1,5 @@ +from aqt.qt import qtmajor +if qtmajor > 5: + from .fields_qt6 import * +else: + from .fields_qt5 import * # type: ignore diff --git a/qt/aqt/forms/fields_qt6.py b/qt/aqt/forms/fields_qt6.py new file mode 120000 index 000000000..b404fab2c --- /dev/null +++ b/qt/aqt/forms/fields_qt6.py @@ -0,0 +1 @@ +../../../bazel-bin/qt/aqt/forms/fields_qt6.py \ No newline at end of file diff --git a/qt/aqt/forms/filtered_deck.py b/qt/aqt/forms/filtered_deck.py deleted file mode 120000 index 39713291a..000000000 --- a/qt/aqt/forms/filtered_deck.py +++ /dev/null @@ -1 +0,0 @@ -../../../bazel-bin/qt/aqt/forms/filtered_deck.py \ No newline at end of file diff --git a/qt/aqt/forms/filtered_deck.py b/qt/aqt/forms/filtered_deck.py new file mode 100644 index 000000000..48a9dd52e --- /dev/null +++ b/qt/aqt/forms/filtered_deck.py @@ -0,0 +1,5 @@ +from aqt.qt import qtmajor +if qtmajor > 5: + from .filtered_deck_qt6 import * +else: + from .filtered_deck_qt5 import * # type: ignore diff --git a/qt/aqt/forms/filtered_deck_qt6.py b/qt/aqt/forms/filtered_deck_qt6.py new file mode 120000 index 000000000..0c8fa97de --- /dev/null +++ b/qt/aqt/forms/filtered_deck_qt6.py @@ -0,0 +1 @@ +../../../bazel-bin/qt/aqt/forms/filtered_deck_qt6.py \ No newline at end of file diff --git a/qt/aqt/forms/finddupes.py b/qt/aqt/forms/finddupes.py deleted file mode 120000 index fc7c7bf1d..000000000 --- a/qt/aqt/forms/finddupes.py +++ /dev/null @@ -1 +0,0 @@ -../../../bazel-bin/qt/aqt/forms/finddupes.py \ No newline at end of file diff --git a/qt/aqt/forms/finddupes.py b/qt/aqt/forms/finddupes.py new file mode 100644 index 000000000..4f7db20b3 --- /dev/null +++ b/qt/aqt/forms/finddupes.py @@ -0,0 +1,5 @@ +from aqt.qt import qtmajor +if qtmajor > 5: + from .finddupes_qt6 import * +else: + from .finddupes_qt5 import * # type: ignore diff --git a/qt/aqt/forms/finddupes_qt6.py b/qt/aqt/forms/finddupes_qt6.py new file mode 120000 index 000000000..5951a4907 --- /dev/null +++ b/qt/aqt/forms/finddupes_qt6.py @@ -0,0 +1 @@ +../../../bazel-bin/qt/aqt/forms/finddupes_qt6.py \ No newline at end of file diff --git a/qt/aqt/forms/findreplace.py b/qt/aqt/forms/findreplace.py deleted file mode 120000 index 7d7e10e7a..000000000 --- a/qt/aqt/forms/findreplace.py +++ /dev/null @@ -1 +0,0 @@ -../../../bazel-bin/qt/aqt/forms/findreplace.py \ No newline at end of file diff --git a/qt/aqt/forms/findreplace.py b/qt/aqt/forms/findreplace.py new file mode 100644 index 000000000..1cdd2e075 --- /dev/null +++ b/qt/aqt/forms/findreplace.py @@ -0,0 +1,5 @@ +from aqt.qt import qtmajor +if qtmajor > 5: + from .findreplace_qt6 import * +else: + from .findreplace_qt5 import * # type: ignore diff --git a/qt/aqt/forms/findreplace_qt6.py b/qt/aqt/forms/findreplace_qt6.py new file mode 120000 index 000000000..863e253d2 --- /dev/null +++ b/qt/aqt/forms/findreplace_qt6.py @@ -0,0 +1 @@ +../../../bazel-bin/qt/aqt/forms/findreplace_qt6.py \ No newline at end of file diff --git a/qt/aqt/forms/getaddons.py b/qt/aqt/forms/getaddons.py deleted file mode 120000 index 5acc58c23..000000000 --- a/qt/aqt/forms/getaddons.py +++ /dev/null @@ -1 +0,0 @@ -../../../bazel-bin/qt/aqt/forms/getaddons.py \ No newline at end of file diff --git a/qt/aqt/forms/getaddons.py b/qt/aqt/forms/getaddons.py new file mode 100644 index 000000000..262703bd6 --- /dev/null +++ b/qt/aqt/forms/getaddons.py @@ -0,0 +1,5 @@ +from aqt.qt import qtmajor +if qtmajor > 5: + from .getaddons_qt6 import * +else: + from .getaddons_qt5 import * # type: ignore diff --git a/qt/aqt/forms/getaddons_qt6.py b/qt/aqt/forms/getaddons_qt6.py new file mode 120000 index 000000000..7898b6932 --- /dev/null +++ b/qt/aqt/forms/getaddons_qt6.py @@ -0,0 +1 @@ +../../../bazel-bin/qt/aqt/forms/getaddons_qt6.py \ No newline at end of file diff --git a/qt/aqt/forms/importing.py b/qt/aqt/forms/importing.py deleted file mode 120000 index 4d04d5af0..000000000 --- a/qt/aqt/forms/importing.py +++ /dev/null @@ -1 +0,0 @@ -../../../bazel-bin/qt/aqt/forms/importing.py \ No newline at end of file diff --git a/qt/aqt/forms/importing.py b/qt/aqt/forms/importing.py new file mode 100644 index 000000000..5dd58c0fc --- /dev/null +++ b/qt/aqt/forms/importing.py @@ -0,0 +1,5 @@ +from aqt.qt import qtmajor +if qtmajor > 5: + from .importing_qt6 import * +else: + from .importing_qt5 import * # type: ignore diff --git a/qt/aqt/forms/importing_qt6.py b/qt/aqt/forms/importing_qt6.py new file mode 120000 index 000000000..f089cecd0 --- /dev/null +++ b/qt/aqt/forms/importing_qt6.py @@ -0,0 +1 @@ +../../../bazel-bin/qt/aqt/forms/importing_qt6.py \ No newline at end of file diff --git a/qt/aqt/forms/main.py b/qt/aqt/forms/main.py deleted file mode 120000 index 7d0246bd4..000000000 --- a/qt/aqt/forms/main.py +++ /dev/null @@ -1 +0,0 @@ -../../../bazel-bin/qt/aqt/forms/main.py \ No newline at end of file diff --git a/qt/aqt/forms/main.py b/qt/aqt/forms/main.py new file mode 100644 index 000000000..00a04ef13 --- /dev/null +++ b/qt/aqt/forms/main.py @@ -0,0 +1,5 @@ +from aqt.qt import qtmajor +if qtmajor > 5: + from .main_qt6 import * +else: + from .main_qt5 import * # type: ignore diff --git a/qt/aqt/forms/main_qt6.py b/qt/aqt/forms/main_qt6.py new file mode 120000 index 000000000..ceeeaef2f --- /dev/null +++ b/qt/aqt/forms/main_qt6.py @@ -0,0 +1 @@ +../../../bazel-bin/qt/aqt/forms/main_qt6.py \ No newline at end of file diff --git a/qt/aqt/forms/modelopts.py b/qt/aqt/forms/modelopts.py deleted file mode 120000 index cb08b0791..000000000 --- a/qt/aqt/forms/modelopts.py +++ /dev/null @@ -1 +0,0 @@ -../../../bazel-bin/qt/aqt/forms/modelopts.py \ No newline at end of file diff --git a/qt/aqt/forms/modelopts.py b/qt/aqt/forms/modelopts.py new file mode 100644 index 000000000..f6166c8d4 --- /dev/null +++ b/qt/aqt/forms/modelopts.py @@ -0,0 +1,5 @@ +from aqt.qt import qtmajor +if qtmajor > 5: + from .modelopts_qt6 import * +else: + from .modelopts_qt5 import * # type: ignore diff --git a/qt/aqt/forms/modelopts_qt6.py b/qt/aqt/forms/modelopts_qt6.py new file mode 120000 index 000000000..ffdc4f397 --- /dev/null +++ b/qt/aqt/forms/modelopts_qt6.py @@ -0,0 +1 @@ +../../../bazel-bin/qt/aqt/forms/modelopts_qt6.py \ No newline at end of file diff --git a/qt/aqt/forms/models.py b/qt/aqt/forms/models.py deleted file mode 120000 index 6f38bb2f7..000000000 --- a/qt/aqt/forms/models.py +++ /dev/null @@ -1 +0,0 @@ -../../../bazel-bin/qt/aqt/forms/models.py \ No newline at end of file diff --git a/qt/aqt/forms/models.py b/qt/aqt/forms/models.py new file mode 100644 index 000000000..ddb565a58 --- /dev/null +++ b/qt/aqt/forms/models.py @@ -0,0 +1,5 @@ +from aqt.qt import qtmajor +if qtmajor > 5: + from .models_qt6 import * +else: + from .models_qt5 import * # type: ignore diff --git a/qt/aqt/forms/models_qt6.py b/qt/aqt/forms/models_qt6.py new file mode 120000 index 000000000..76fd177b2 --- /dev/null +++ b/qt/aqt/forms/models_qt6.py @@ -0,0 +1 @@ +../../../bazel-bin/qt/aqt/forms/models_qt6.py \ No newline at end of file diff --git a/qt/aqt/forms/preferences.py b/qt/aqt/forms/preferences.py deleted file mode 120000 index 15982ca41..000000000 --- a/qt/aqt/forms/preferences.py +++ /dev/null @@ -1 +0,0 @@ -../../../bazel-bin/qt/aqt/forms/preferences.py \ No newline at end of file diff --git a/qt/aqt/forms/preferences.py b/qt/aqt/forms/preferences.py new file mode 100644 index 000000000..0b31698eb --- /dev/null +++ b/qt/aqt/forms/preferences.py @@ -0,0 +1,5 @@ +from aqt.qt import qtmajor +if qtmajor > 5: + from .preferences_qt6 import * +else: + from .preferences_qt5 import * # type: ignore diff --git a/qt/aqt/forms/preferences_qt6.py b/qt/aqt/forms/preferences_qt6.py new file mode 120000 index 000000000..c3cddef94 --- /dev/null +++ b/qt/aqt/forms/preferences_qt6.py @@ -0,0 +1 @@ +../../../bazel-bin/qt/aqt/forms/preferences_qt6.py \ No newline at end of file diff --git a/qt/aqt/forms/preview.py b/qt/aqt/forms/preview.py deleted file mode 120000 index 877162b89..000000000 --- a/qt/aqt/forms/preview.py +++ /dev/null @@ -1 +0,0 @@ -../../../bazel-bin/qt/aqt/forms/preview.py \ No newline at end of file diff --git a/qt/aqt/forms/preview.py b/qt/aqt/forms/preview.py new file mode 100644 index 000000000..f20222c81 --- /dev/null +++ b/qt/aqt/forms/preview.py @@ -0,0 +1,5 @@ +from aqt.qt import qtmajor +if qtmajor > 5: + from .preview_qt6 import * +else: + from .preview_qt5 import * # type: ignore diff --git a/qt/aqt/forms/preview_qt6.py b/qt/aqt/forms/preview_qt6.py new file mode 120000 index 000000000..ebd0c6300 --- /dev/null +++ b/qt/aqt/forms/preview_qt6.py @@ -0,0 +1 @@ +../../../bazel-bin/qt/aqt/forms/preview_qt6.py \ No newline at end of file diff --git a/qt/aqt/forms/profiles.py b/qt/aqt/forms/profiles.py deleted file mode 120000 index 9643ad8ae..000000000 --- a/qt/aqt/forms/profiles.py +++ /dev/null @@ -1 +0,0 @@ -../../../bazel-bin/qt/aqt/forms/profiles.py \ No newline at end of file diff --git a/qt/aqt/forms/profiles.py b/qt/aqt/forms/profiles.py new file mode 100644 index 000000000..55c9b6784 --- /dev/null +++ b/qt/aqt/forms/profiles.py @@ -0,0 +1,5 @@ +from aqt.qt import qtmajor +if qtmajor > 5: + from .profiles_qt6 import * +else: + from .profiles_qt5 import * # type: ignore diff --git a/qt/aqt/forms/profiles_qt6.py b/qt/aqt/forms/profiles_qt6.py new file mode 120000 index 000000000..e16ba8816 --- /dev/null +++ b/qt/aqt/forms/profiles_qt6.py @@ -0,0 +1 @@ +../../../bazel-bin/qt/aqt/forms/profiles_qt6.py \ No newline at end of file diff --git a/qt/aqt/forms/progress.py b/qt/aqt/forms/progress.py deleted file mode 120000 index 22dd1eb4a..000000000 --- a/qt/aqt/forms/progress.py +++ /dev/null @@ -1 +0,0 @@ -../../../bazel-bin/qt/aqt/forms/progress.py \ No newline at end of file diff --git a/qt/aqt/forms/progress.py b/qt/aqt/forms/progress.py new file mode 100644 index 000000000..2af563f54 --- /dev/null +++ b/qt/aqt/forms/progress.py @@ -0,0 +1,5 @@ +from aqt.qt import qtmajor +if qtmajor > 5: + from .progress_qt6 import * +else: + from .progress_qt5 import * # type: ignore diff --git a/qt/aqt/forms/progress_qt6.py b/qt/aqt/forms/progress_qt6.py new file mode 120000 index 000000000..596cba869 --- /dev/null +++ b/qt/aqt/forms/progress_qt6.py @@ -0,0 +1 @@ +../../../bazel-bin/qt/aqt/forms/progress_qt6.py \ No newline at end of file diff --git a/qt/aqt/forms/reposition.py b/qt/aqt/forms/reposition.py deleted file mode 120000 index 68a9c20b5..000000000 --- a/qt/aqt/forms/reposition.py +++ /dev/null @@ -1 +0,0 @@ -../../../bazel-bin/qt/aqt/forms/reposition.py \ No newline at end of file diff --git a/qt/aqt/forms/reposition.py b/qt/aqt/forms/reposition.py new file mode 100644 index 000000000..8f35e51cd --- /dev/null +++ b/qt/aqt/forms/reposition.py @@ -0,0 +1,5 @@ +from aqt.qt import qtmajor +if qtmajor > 5: + from .reposition_qt6 import * +else: + from .reposition_qt5 import * # type: ignore diff --git a/qt/aqt/forms/reposition_qt6.py b/qt/aqt/forms/reposition_qt6.py new file mode 120000 index 000000000..a0b8c335f --- /dev/null +++ b/qt/aqt/forms/reposition_qt6.py @@ -0,0 +1 @@ +../../../bazel-bin/qt/aqt/forms/reposition_qt6.py \ No newline at end of file diff --git a/qt/aqt/forms/setgroup.py b/qt/aqt/forms/setgroup.py deleted file mode 120000 index e3227b7a9..000000000 --- a/qt/aqt/forms/setgroup.py +++ /dev/null @@ -1 +0,0 @@ -../../../bazel-bin/qt/aqt/forms/setgroup.py \ No newline at end of file diff --git a/qt/aqt/forms/setgroup.py b/qt/aqt/forms/setgroup.py new file mode 100644 index 000000000..c04c3d627 --- /dev/null +++ b/qt/aqt/forms/setgroup.py @@ -0,0 +1,5 @@ +from aqt.qt import qtmajor +if qtmajor > 5: + from .setgroup_qt6 import * +else: + from .setgroup_qt5 import * # type: ignore diff --git a/qt/aqt/forms/setgroup_qt6.py b/qt/aqt/forms/setgroup_qt6.py new file mode 120000 index 000000000..7d7f72a1b --- /dev/null +++ b/qt/aqt/forms/setgroup_qt6.py @@ -0,0 +1 @@ +../../../bazel-bin/qt/aqt/forms/setgroup_qt6.py \ No newline at end of file diff --git a/qt/aqt/forms/setlang.py b/qt/aqt/forms/setlang.py deleted file mode 120000 index ae38ab548..000000000 --- a/qt/aqt/forms/setlang.py +++ /dev/null @@ -1 +0,0 @@ -../../../bazel-bin/qt/aqt/forms/setlang.py \ No newline at end of file diff --git a/qt/aqt/forms/setlang.py b/qt/aqt/forms/setlang.py new file mode 100644 index 000000000..10107e5c0 --- /dev/null +++ b/qt/aqt/forms/setlang.py @@ -0,0 +1,5 @@ +from aqt.qt import qtmajor +if qtmajor > 5: + from .setlang_qt6 import * +else: + from .setlang_qt5 import * # type: ignore diff --git a/qt/aqt/forms/setlang_qt6.py b/qt/aqt/forms/setlang_qt6.py new file mode 120000 index 000000000..50ec3d564 --- /dev/null +++ b/qt/aqt/forms/setlang_qt6.py @@ -0,0 +1 @@ +../../../bazel-bin/qt/aqt/forms/setlang_qt6.py \ No newline at end of file diff --git a/qt/aqt/forms/stats.py b/qt/aqt/forms/stats.py deleted file mode 120000 index 9ba8aa2d0..000000000 --- a/qt/aqt/forms/stats.py +++ /dev/null @@ -1 +0,0 @@ -../../../bazel-bin/qt/aqt/forms/stats.py \ No newline at end of file diff --git a/qt/aqt/forms/stats.py b/qt/aqt/forms/stats.py new file mode 100644 index 000000000..a2e5be948 --- /dev/null +++ b/qt/aqt/forms/stats.py @@ -0,0 +1,5 @@ +from aqt.qt import qtmajor +if qtmajor > 5: + from .stats_qt6 import * +else: + from .stats_qt5 import * # type: ignore diff --git a/qt/aqt/forms/stats_qt6.py b/qt/aqt/forms/stats_qt6.py new file mode 120000 index 000000000..156dd54a4 --- /dev/null +++ b/qt/aqt/forms/stats_qt6.py @@ -0,0 +1 @@ +../../../bazel-bin/qt/aqt/forms/stats_qt6.py \ No newline at end of file diff --git a/qt/aqt/forms/studydeck.py b/qt/aqt/forms/studydeck.py deleted file mode 120000 index 4dbf7513a..000000000 --- a/qt/aqt/forms/studydeck.py +++ /dev/null @@ -1 +0,0 @@ -../../../bazel-bin/qt/aqt/forms/studydeck.py \ No newline at end of file diff --git a/qt/aqt/forms/studydeck.py b/qt/aqt/forms/studydeck.py new file mode 100644 index 000000000..00f2ee6b7 --- /dev/null +++ b/qt/aqt/forms/studydeck.py @@ -0,0 +1,5 @@ +from aqt.qt import qtmajor +if qtmajor > 5: + from .studydeck_qt6 import * +else: + from .studydeck_qt5 import * # type: ignore diff --git a/qt/aqt/forms/studydeck_qt6.py b/qt/aqt/forms/studydeck_qt6.py new file mode 120000 index 000000000..224c0d594 --- /dev/null +++ b/qt/aqt/forms/studydeck_qt6.py @@ -0,0 +1 @@ +../../../bazel-bin/qt/aqt/forms/studydeck_qt6.py \ No newline at end of file diff --git a/qt/aqt/forms/synclog.py b/qt/aqt/forms/synclog.py deleted file mode 120000 index fff84ce3b..000000000 --- a/qt/aqt/forms/synclog.py +++ /dev/null @@ -1 +0,0 @@ -../../../bazel-bin/qt/aqt/forms/synclog.py \ No newline at end of file diff --git a/qt/aqt/forms/synclog.py b/qt/aqt/forms/synclog.py new file mode 100644 index 000000000..efc0d6f71 --- /dev/null +++ b/qt/aqt/forms/synclog.py @@ -0,0 +1,5 @@ +from aqt.qt import qtmajor +if qtmajor > 5: + from .synclog_qt6 import * +else: + from .synclog_qt5 import * # type: ignore diff --git a/qt/aqt/forms/synclog_qt6.py b/qt/aqt/forms/synclog_qt6.py new file mode 120000 index 000000000..26c206838 --- /dev/null +++ b/qt/aqt/forms/synclog_qt6.py @@ -0,0 +1 @@ +../../../bazel-bin/qt/aqt/forms/synclog_qt6.py \ No newline at end of file diff --git a/qt/aqt/forms/taglimit.py b/qt/aqt/forms/taglimit.py deleted file mode 120000 index b0edb113d..000000000 --- a/qt/aqt/forms/taglimit.py +++ /dev/null @@ -1 +0,0 @@ -../../../bazel-bin/qt/aqt/forms/taglimit.py \ No newline at end of file diff --git a/qt/aqt/forms/taglimit.py b/qt/aqt/forms/taglimit.py new file mode 100644 index 000000000..2ee3398d5 --- /dev/null +++ b/qt/aqt/forms/taglimit.py @@ -0,0 +1,5 @@ +from aqt.qt import qtmajor +if qtmajor > 5: + from .taglimit_qt6 import * +else: + from .taglimit_qt5 import * # type: ignore diff --git a/qt/aqt/forms/taglimit_qt6.py b/qt/aqt/forms/taglimit_qt6.py new file mode 120000 index 000000000..f340c989b --- /dev/null +++ b/qt/aqt/forms/taglimit_qt6.py @@ -0,0 +1 @@ +../../../bazel-bin/qt/aqt/forms/taglimit_qt6.py \ No newline at end of file diff --git a/qt/aqt/forms/template.py b/qt/aqt/forms/template.py deleted file mode 120000 index 9afe0aac7..000000000 --- a/qt/aqt/forms/template.py +++ /dev/null @@ -1 +0,0 @@ -../../../bazel-bin/qt/aqt/forms/template.py \ No newline at end of file diff --git a/qt/aqt/forms/template.py b/qt/aqt/forms/template.py new file mode 100644 index 000000000..73a332c0a --- /dev/null +++ b/qt/aqt/forms/template.py @@ -0,0 +1,5 @@ +from aqt.qt import qtmajor +if qtmajor > 5: + from .template_qt6 import * +else: + from .template_qt5 import * # type: ignore diff --git a/qt/aqt/forms/template_qt6.py b/qt/aqt/forms/template_qt6.py new file mode 120000 index 000000000..284420bf2 --- /dev/null +++ b/qt/aqt/forms/template_qt6.py @@ -0,0 +1 @@ +../../../bazel-bin/qt/aqt/forms/template_qt6.py \ No newline at end of file diff --git a/qt/mypy.ini b/qt/mypy.ini index bb304e0f4..c8dcf12e9 100644 --- a/qt/mypy.ini +++ b/qt/mypy.ini @@ -64,9 +64,11 @@ ignore_missing_imports = True ignore_missing_imports = True [mypy-aqt.forms.*] -disallow_untyped_defs=false +disallow_untyped_defs = false + [mypy-anki.*] disallow_untyped_defs=false [mypy-PyQt6.*] -ignore_errors = True \ No newline at end of file +ignore_errors = True +ignore_missing_imports = True From a7812dedc096627692ab3d7e64b90be632f52134 Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Tue, 5 Oct 2021 13:53:01 +1000 Subject: [PATCH 06/16] switch to new-style PyQt scoped enums and Qt6 The enum changes should work on PyQt 5.x, and are required in PyQt 6.x. They are not supported by the PyQt5 typings however, so we need to run our tests with PyQt6. --- qt/.pylintrc | 2 +- qt/BUILD.bazel | 6 +- qt/aqt/BUILD.bazel | 2 +- qt/aqt/__init__.py | 51 ++++++++------ qt/aqt/about.py | 4 +- qt/aqt/addcards.py | 13 ++-- qt/aqt/addons.py | 54 +++++++++------ qt/aqt/browser/browser.py | 10 +-- qt/aqt/browser/card_info.py | 4 +- qt/aqt/browser/find_and_replace.py | 8 ++- qt/aqt/browser/find_duplicates.py | 4 +- qt/aqt/browser/previewer.py | 12 ++-- qt/aqt/browser/sidebar/model.py | 39 +++++++---- qt/aqt/browser/sidebar/searchbar.py | 4 +- qt/aqt/browser/sidebar/toolbar.py | 2 +- qt/aqt/browser/sidebar/tree.py | 38 ++++++---- qt/aqt/browser/table/model.py | 25 ++++--- qt/aqt/browser/table/table.py | 69 ++++++++++--------- qt/aqt/changenotetype.py | 2 +- qt/aqt/clayout.py | 16 +++-- qt/aqt/customstudy.py | 4 +- qt/aqt/deckconf.py | 6 +- qt/aqt/deckdescription.py | 6 +- qt/aqt/deckoptions.py | 4 +- qt/aqt/editcurrent.py | 4 +- qt/aqt/editor.py | 19 ++--- qt/aqt/emptycards.py | 2 +- qt/aqt/exporting.py | 4 +- qt/aqt/fields.py | 28 +++++--- qt/aqt/filtered_deck.py | 6 +- qt/aqt/forms/findreplace.ui | 10 +-- qt/aqt/importing.py | 9 +-- qt/aqt/main.py | 21 +++--- qt/aqt/mediacheck.py | 12 ++-- qt/aqt/mediasync.py | 6 +- qt/aqt/models.py | 6 +- qt/aqt/operations/scheduling.py | 2 +- qt/aqt/preferences.py | 10 ++- qt/aqt/profiles.py | 4 +- qt/aqt/progress.py | 8 +-- qt/aqt/qt.py | 42 ++++++++---- qt/aqt/qt5.py | 2 + qt/aqt/reviewer.py | 8 +-- qt/aqt/sound.py | 9 ++- qt/aqt/stats.py | 12 ++-- qt/aqt/studydeck.py | 24 +++---- qt/aqt/switch.py | 16 +++-- qt/aqt/sync.py | 8 +-- qt/aqt/tagedit.py | 29 ++++---- qt/aqt/taglimit.py | 18 +++-- qt/aqt/theme.py | 46 ++++++++----- qt/aqt/update.py | 10 +-- qt/aqt/utils.py | 103 +++++++++++++++------------- qt/aqt/webview.py | 45 ++++++------ qt/dmypy.py | 2 +- qt/mypy.ini | 1 + 56 files changed, 526 insertions(+), 385 deletions(-) diff --git a/qt/.pylintrc b/qt/.pylintrc index 510c08da4..6107eeed3 100644 --- a/qt/.pylintrc +++ b/qt/.pylintrc @@ -1,6 +1,6 @@ [MASTER] persistent = no -extension-pkg-whitelist=PyQt5,PyQt6 +extension-pkg-whitelist=PyQt6 ignore = forms,hooks_gen.py [TYPECHECK] diff --git a/qt/BUILD.bazel b/qt/BUILD.bazel index a2e4bfd65..93d5473b3 100644 --- a/qt/BUILD.bazel +++ b/qt/BUILD.bazel @@ -45,21 +45,21 @@ py_test( args = [ "aqt", "$(location mypy.ini)", - "$(location @pyqt5//:__init__.py)", + "$(location @pyqt6//:__init__.py)", "$(location //pip/stubs:extendsitepkgs)", ], data = [ "mypy.ini", "//pip/stubs", "//pip/stubs:extendsitepkgs", - "@pyqt5//:__init__.py", + "@pyqt6//:__init__.py", ], env = {"EXTRA_SITE_PACKAGES": "$(location //pip/stubs)"}, main = "tests/run_mypy.py", deps = [ "//pylib/anki", "//qt/aqt:aqt_without_data", - "@pyqt5//:pkg", + "@pyqt6//:pkg", requirement("mypy"), ], ) diff --git a/qt/aqt/BUILD.bazel b/qt/aqt/BUILD.bazel index e4bac550a..ad816a68b 100644 --- a/qt/aqt/BUILD.bazel +++ b/qt/aqt/BUILD.bazel @@ -50,7 +50,7 @@ aqt_deps = [ requirement("waitress"), requirement("send2trash"), requirement("jsonschema"), - "@pyqt5//:pkg", + "@pyqt6//:pkg", ] + select({ "@bazel_tools//src/conditions:host_windows": [ requirement("psutil"), diff --git a/qt/aqt/__init__.py b/qt/aqt/__init__.py index f910bbc1f..ea2742d7d 100644 --- a/qt/aqt/__init__.py +++ b/qt/aqt/__init__.py @@ -99,8 +99,10 @@ class DialogManager: def open(self, name: str, *args: Any, **kwargs: Any) -> Any: (creator, instance) = self._dialogs[name] if instance: - if instance.windowState() & Qt.WindowMinimized: - instance.setWindowState(instance.windowState() & ~Qt.WindowMinimized) + if instance.windowState() & Qt.WindowState.WindowMinimized: + instance.setWindowState( + instance.windowState() & ~Qt.WindowState.WindowMinimized + ) instance.activateWindow() instance.raise_() if hasattr(instance, "reopen"): @@ -224,9 +226,9 @@ def setupLangAndBackend( # switch direction for RTL languages if anki.lang.is_rtl(lang): - app.setLayoutDirection(Qt.RightToLeft) + app.setLayoutDirection(Qt.LayoutDirection.RightToLeft) else: - app.setLayoutDirection(Qt.LeftToRight) + app.setLayoutDirection(Qt.LayoutDirection.LeftToRight) # load qt translations _qtrans = QTranslator() @@ -238,7 +240,10 @@ def setupLangAndBackend( os.path.join(aqt_data_folder(), "..", "qt_translations") ) else: - qt_dir = QLibraryInfo.location(QLibraryInfo.TranslationsPath) + if qtmajor == 5: + qt_dir = QLibraryInfo.location(QLibraryInfo.TranslationsPath) # type: ignore + else: + qt_dir = QLibraryInfo.path(QLibraryInfo.LibraryPath.TranslationsPath) qt_lang = lang.replace("-", "_") if _qtrans.load(f"qtbase_{qt_lang}", qt_dir): app.installTranslator(_qtrans) @@ -285,7 +290,7 @@ class AnkiApp(QApplication): def sendMsg(self, txt: str) -> bool: sock = QLocalSocket(self) - sock.connectToServer(self.KEY, QIODevice.WriteOnly) + sock.connectToServer(self.KEY, QIODevice.OpenModeFlag.WriteOnly) if not sock.waitForConnected(self.TMOUT): # first instance or previous instance dead return False @@ -315,7 +320,7 @@ class AnkiApp(QApplication): ################################################## def event(self, evt: QEvent) -> bool: - if evt.type() == QEvent.FileOpen: + if evt.type() == QEvent.Type.FileOpen: self.appMsg.emit(evt.file() or "raise") # type: ignore return True return QApplication.event(self, evt) @@ -360,17 +365,17 @@ def setupGL(pm: aqt.profiles.ProfileManager) -> None: # catch opengl errors def msgHandler(category: Any, ctx: Any, msg: Any) -> None: - if category == QtDebugMsg: + if category == QtMsgType.QtDebugMsg: category = "debug" - elif category == QtInfoMsg: + elif category == QtMsgType.QtInfoMsg: category = "info" - elif category == QtWarningMsg: + elif category == QtMsgType.QtWarningMsg: category = "warning" - elif category == QtCriticalMsg: + elif category == QtMsgType.QtCriticalMsg: category = "critical" - elif category == QtDebugMsg: + elif category == QtMsgType.QtDebugMsg: category = "debug" - elif category == QtFatalMsg: + elif category == QtMsgType.QtFatalMsg: category = "fatal" else: category = "unknown" @@ -405,7 +410,7 @@ def setupGL(pm: aqt.profiles.ProfileManager) -> None: if isWin: os.environ["QT_OPENGL"] = driver.value elif isMac: - QCoreApplication.setAttribute(Qt.AA_UseSoftwareOpenGL) + QCoreApplication.setAttribute(Qt.ApplicationAttribute.AA_UseSoftwareOpenGL) elif isLin: os.environ["QT_XCB_FORCE_SOFTWARE_OPENGL"] = "1" @@ -499,15 +504,15 @@ def _run(argv: Optional[list[str]] = None, exec: bool = True) -> Optional[AnkiAp os.environ["QT_SCALE_FACTOR"] = str(pm.uiScale()) # opt in to full hidpi support? - if not os.environ.get("ANKI_NOHIGHDPI"): - QCoreApplication.setAttribute(Qt.AA_EnableHighDpiScaling) - QCoreApplication.setAttribute(Qt.AA_UseHighDpiPixmaps) + if not os.environ.get("ANKI_NOHIGHDPI") and qtmajor == 5: + QCoreApplication.setAttribute(Qt.ApplicationAttribute.AA_EnableHighDpiScaling) # type: ignore + QCoreApplication.setAttribute(Qt.ApplicationAttribute.AA_UseHighDpiPixmaps) # type: ignore os.environ["QT_ENABLE_HIGHDPI_SCALING"] = "1" os.environ["QT_SCALE_FACTOR_ROUNDING_POLICY"] = "PassThrough" # Opt into software rendering. Useful for buggy systems. if os.environ.get("ANKI_SOFTWAREOPENGL"): - QCoreApplication.setAttribute(Qt.AA_UseSoftwareOpenGL) + QCoreApplication.setAttribute(Qt.ApplicationAttribute.AA_UseSoftwareOpenGL) if ( isWin @@ -535,11 +540,13 @@ def _run(argv: Optional[list[str]] = None, exec: bool = True) -> Optional[AnkiAp # disable icons on mac; this must be done before window created if isMac: - app.setAttribute(Qt.AA_DontShowIconsInMenus) + app.setAttribute(Qt.ApplicationAttribute.AA_DontShowIconsInMenus) # disable help button in title bar on qt versions that support it - if isWin and qtminor >= 10: - QApplication.setAttribute(Qt.AA_DisableWindowContextHelpButton) + if isWin and qtmajor == 5 and qtminor >= 10: + QApplication.setAttribute( + QApplication.Attribute.AA_DisableWindowContextHelpButton # type: ignore + ) # proxy configured? from urllib.request import getproxies, proxy_bypass @@ -559,7 +566,7 @@ def _run(argv: Optional[list[str]] = None, exec: bool = True) -> Optional[AnkiAp if disable_proxies: print("webview proxy use disabled") proxy = QNetworkProxy() - proxy.setType(QNetworkProxy.NoProxy) + proxy.setType(QNetworkProxy.ProxyType.NoProxy) QNetworkProxy.setApplicationProxy(proxy) # we must have a usable temp dir diff --git a/qt/aqt/about.py b/qt/aqt/about.py index 357d346ec..3f5d47164 100644 --- a/qt/aqt/about.py +++ b/qt/aqt/about.py @@ -88,8 +88,8 @@ def show(mw: aqt.AnkiQt) -> QDialog: btn = QPushButton(tr.about_copy_debug_info()) qconnect(btn.clicked, onCopy) - abt.buttonBox.addButton(btn, QDialogButtonBox.ActionRole) - abt.buttonBox.button(QDialogButtonBox.Ok).setFocus() + abt.buttonBox.addButton(btn, QDialogButtonBox.ButtonRole.ActionRole) + abt.buttonBox.button(QDialogButtonBox.StandardButton.Ok).setFocus() # WebView contents ###################################################################### diff --git a/qt/aqt/addcards.py b/qt/aqt/addcards.py index f5280c233..7d42ba133 100644 --- a/qt/aqt/addcards.py +++ b/qt/aqt/addcards.py @@ -35,7 +35,7 @@ from aqt.utils import ( class AddCards(QDialog): def __init__(self, mw: AnkiQt) -> None: - QDialog.__init__(self, None, Qt.Window) + QDialog.__init__(self, None, Qt.WindowType.Window) mw.garbage_collect_on_dialog_finish(self) self.mw = mw self.col = mw.col @@ -85,7 +85,7 @@ class AddCards(QDialog): def setupButtons(self) -> None: bb = self.form.buttonBox - ar = QDialogButtonBox.ActionRole + ar = QDialogButtonBox.ButtonRole.ActionRole # add self.addButton = bb.addButton(tr.actions_add(), ar) qconnect(self.addButton.clicked, self.add_current_note) @@ -97,11 +97,11 @@ class AddCards(QDialog): # close self.closeButton = QPushButton(tr.actions_close()) self.closeButton.setAutoDefault(False) - bb.addButton(self.closeButton, QDialogButtonBox.RejectRole) + bb.addButton(self.closeButton, QDialogButtonBox.ButtonRole.RejectRole) # help self.helpButton = QPushButton(tr.actions_help(), clicked=self.helpRequested) # type: ignore self.helpButton.setAutoDefault(False) - bb.addButton(self.helpButton, QDialogButtonBox.HelpRole) + bb.addButton(self.helpButton, QDialogButtonBox.ButtonRole.HelpRole) # history b = bb.addButton(f"{tr.adding_history()} {downArrow()}", ar) if isMac: @@ -266,7 +266,10 @@ class AddCards(QDialog): def keyPressEvent(self, evt: QKeyEvent) -> None: "Show answer on RET or register answer." - if evt.key() in (Qt.Key_Enter, Qt.Key_Return) and self.editor.tags.hasFocus(): + if ( + evt.key() in (Qt.Key.Key_Enter, Qt.Key.Key_Return) + and self.editor.tags.hasFocus() + ): evt.accept() return return QDialog.keyPressEvent(self, evt) diff --git a/qt/aqt/addons.py b/qt/aqt/addons.py index 5a94e048a..c8c69b6de 100644 --- a/qt/aqt/addons.py +++ b/qt/aqt/addons.py @@ -804,7 +804,7 @@ class AddonsDialog(QDialog): name = self.name_for_addon_list(addon) item = QListWidgetItem(name, addonList) if self.should_grey(addon): - item.setForeground(Qt.gray) + item.setForeground(Qt.GlobalColor.gray) if addon.dir_name in selected: item.setSelected(True) @@ -947,7 +947,7 @@ class GetAddons(QDialog): self.form = aqt.forms.getaddons.Ui_Dialog() self.form.setupUi(self) b = self.form.buttonBox.addButton( - tr.addons_browse_addons(), QDialogButtonBox.ActionRole + tr.addons_browse_addons(), QDialogButtonBox.ButtonRole.ActionRole ) qconnect(b.clicked, self.onBrowse) disable_help_button(self) @@ -1183,7 +1183,7 @@ class ChooseAddonsToUpdateList(QListWidget): ) self.ignore_check_evt = False self.setup() - self.setContextMenuPolicy(Qt.CustomContextMenu) + self.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu) qconnect(self.itemClicked, self.on_click) qconnect(self.itemChanged, self.on_check) qconnect(self.itemDoubleClicked, self.on_double_click) @@ -1191,7 +1191,9 @@ class ChooseAddonsToUpdateList(QListWidget): def setup(self) -> None: header_item = QListWidgetItem(tr.addons_choose_update_update_all(), self) - header_item.setFlags(Qt.ItemFlag(Qt.ItemIsUserCheckable | Qt.ItemIsEnabled)) + header_item.setFlags( + Qt.ItemFlag(Qt.ItemFlag.ItemIsUserCheckable | Qt.ItemFlag.ItemIsEnabled) + ) self.header_item = header_item for update_info in self.updated_addons: addon_id = update_info.id @@ -1204,22 +1206,22 @@ class ChooseAddonsToUpdateList(QListWidget): addon_label = f"{update_time:%Y-%m-%d} {addon_name}" item = QListWidgetItem(addon_label, self) # Not user checkable because it overlaps with itemClicked signal - item.setFlags(Qt.ItemFlag(Qt.ItemIsEnabled)) + item.setFlags(Qt.ItemFlag(Qt.ItemFlag.ItemIsEnabled)) if update_enabled: - item.setCheckState(Qt.Checked) + item.setCheckState(Qt.CheckState.Checked) else: - item.setCheckState(Qt.Unchecked) + item.setCheckState(Qt.CheckState.Unchecked) item.setData(self.ADDON_ID_ROLE, addon_id) self.refresh_header_check_state() def bool_to_check(self, check_bool: bool) -> Qt.CheckState: if check_bool: - return Qt.Checked + return Qt.CheckState.Checked else: - return Qt.Unchecked + return Qt.CheckState.Unchecked def checked(self, item: QListWidgetItem) -> bool: - return item.checkState() == Qt.Checked + return item.checkState() == Qt.CheckState.Checked def on_click(self, item: QListWidgetItem) -> None: if item == self.header_item: @@ -1262,9 +1264,9 @@ class ChooseAddonsToUpdateList(QListWidget): for i in range(1, self.count()): item = self.item(i) if not self.checked(item): - self.check_item(self.header_item, Qt.Unchecked) + self.check_item(self.header_item, Qt.CheckState.Unchecked) return - self.check_item(self.header_item, Qt.Checked) + self.check_item(self.header_item, Qt.CheckState.Checked) def get_selected_addon_ids(self) -> list[int]: addon_ids = [] @@ -1290,7 +1292,7 @@ class ChooseAddonsToUpdateDialog(QDialog): ) -> None: QDialog.__init__(self, parent) self.setWindowTitle(tr.addons_choose_update_window_title()) - self.setWindowModality(Qt.WindowModal) + self.setWindowModality(Qt.WindowModality.WindowModal) self.mgr = mgr self.updated_addons = updated_addons self.setup() @@ -1306,9 +1308,14 @@ class ChooseAddonsToUpdateDialog(QDialog): layout.addWidget(addons_list_widget) self.addons_list_widget = addons_list_widget - button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel) # type: ignore - qconnect(button_box.button(QDialogButtonBox.Ok).clicked, self.accept) - qconnect(button_box.button(QDialogButtonBox.Cancel).clicked, self.reject) + button_box = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel) # type: ignore + qconnect( + button_box.button(QDialogButtonBox.StandardButton.Ok).clicked, self.accept + ) + qconnect( + button_box.button(QDialogButtonBox.StandardButton.Cancel).clicked, + self.reject, + ) layout.addWidget(button_box) self.setLayout(layout) @@ -1317,7 +1324,7 @@ class ChooseAddonsToUpdateDialog(QDialog): ret = self.exec() saveGeom(self, "addonsChooseUpdate") self.addons_list_widget.save_check_state() - if ret == QDialog.Accepted: + if ret == QDialog.DialogCode.Accepted: return self.addons_list_widget.get_selected_addon_ids() else: return [] @@ -1475,7 +1482,9 @@ class ConfigEditor(QDialog): self.mgr = dlg.mgr self.form = aqt.forms.addonconf.Ui_Dialog() self.form.setupUi(self) - restore = self.form.buttonBox.button(QDialogButtonBox.RestoreDefaults) + restore = self.form.buttonBox.button( + QDialogButtonBox.StandardButton.RestoreDefaults + ) qconnect(restore.clicked, self.onRestoreDefaults) self.setupFonts() self.updateHelp() @@ -1498,7 +1507,7 @@ class ConfigEditor(QDialog): tooltip(tr.addons_restored_defaults(), parent=self) def setupFonts(self) -> None: - font_mono = QFontDatabase.systemFont(QFontDatabase.FixedFont) + font_mono = QFontDatabase.systemFont(QFontDatabase.SystemFont.FixedFont) font_mono.setPointSize(font_mono.pointSize() + 1) self.form.editor.setFont(font_mono) @@ -1600,9 +1609,12 @@ def installAddonPackages( parent=parent, title=tr.addons_install_anki_addon(), type="warning", - customBtns=[QMessageBox.No, QMessageBox.Yes], + customBtns=[ + QMessageBox.StandardButton.No, + QMessageBox.StandardButton.Yes, + ], ) - == QMessageBox.Yes + == QMessageBox.StandardButton.Yes ): return False diff --git a/qt/aqt/browser/browser.py b/qt/aqt/browser/browser.py index 34a6133fe..1c90beeaf 100644 --- a/qt/aqt/browser/browser.py +++ b/qt/aqt/browser/browser.py @@ -104,7 +104,7 @@ class Browser(QMainWindow): search -- set and perform search; caller must ensure validity """ - QMainWindow.__init__(self, None, Qt.Window) + QMainWindow.__init__(self, None, Qt.WindowType.Window) self.mw = mw self.col = self.mw.col self.lastFilter = "" @@ -255,7 +255,7 @@ class Browser(QMainWindow): onsuccess() def keyPressEvent(self, evt: QKeyEvent) -> None: - if evt.key() == Qt.Key_Escape: + if evt.key() == Qt.Key.Key_Escape: self.close() else: super().keyPressEvent(evt) @@ -490,9 +490,9 @@ class Browser(QMainWindow): def setupSidebar(self) -> None: dw = self.sidebarDockWidget = QDockWidget(tr.browsing_sidebar(), self) - dw.setFeatures(QDockWidget.NoDockWidgetFeatures) + dw.setFeatures(QDockWidget.DockWidgetFeature.NoDockWidgetFeatures) dw.setObjectName("Sidebar") - dw.setAllowedAreas(Qt.LeftDockWidgetArea) + dw.setAllowedAreas(Qt.DockWidgetArea.LeftDockWidgetArea) self.sidebar = SidebarTreeView(self) self.sidebarTree = self.sidebar # legacy alias @@ -513,7 +513,7 @@ class Browser(QMainWindow): self.sidebarDockWidget.setFloating(False) self.sidebarDockWidget.setTitleBarWidget(QWidget()) - self.addDockWidget(Qt.LeftDockWidgetArea, dw) + self.addDockWidget(Qt.DockWidgetArea.LeftDockWidgetArea, dw) # schedule sidebar to refresh after browser window has loaded, so the # UI is more responsive diff --git a/qt/aqt/browser/card_info.py b/qt/aqt/browser/card_info.py index 046870362..b059bfaa5 100644 --- a/qt/aqt/browser/card_info.py +++ b/qt/aqt/browser/card_info.py @@ -28,7 +28,7 @@ class CardInfoDialog(QDialog): self.show() def _setup_ui(self, card_id: CardId) -> None: - self.setWindowModality(Qt.ApplicationModal) + self.setWindowModality(Qt.WindowModality.ApplicationModal) self.mw.garbage_collect_on_dialog_finish(self) disable_help_button(self) restoreGeom(self, self.GEOMETRY_KEY) @@ -40,7 +40,7 @@ class CardInfoDialog(QDialog): layout = QVBoxLayout() layout.setContentsMargins(0, 0, 0, 0) layout.addWidget(self.web) - buttons = QDialogButtonBox(QDialogButtonBox.Close) + buttons = QDialogButtonBox(QDialogButtonBox.StandardButton.Close) buttons.setContentsMargins(10, 0, 10, 10) layout.addWidget(buttons) qconnect(buttons.rejected, self.reject) diff --git a/qt/aqt/browser/find_and_replace.py b/qt/aqt/browser/find_and_replace.py index 60b2f3d77..39add80e6 100644 --- a/qt/aqt/browser/find_and_replace.py +++ b/qt/aqt/browser/find_and_replace.py @@ -73,16 +73,18 @@ class FindAndReplaceDialog(QDialog): disable_help_button(self) self.form = aqt.forms.findreplace.Ui_Dialog() self.form.setupUi(self) - self.setWindowModality(Qt.WindowModal) + self.setWindowModality(Qt.WindowModality.WindowModal) self._find_history = restore_combo_history( self.form.find, self.COMBO_NAME + "Find" ) - self.form.find.completer().setCaseSensitivity(Qt.CaseSensitive) + self.form.find.completer().setCaseSensitivity(Qt.CaseSensitivity.CaseSensitive) self._replace_history = restore_combo_history( self.form.replace, self.COMBO_NAME + "Replace" ) - self.form.replace.completer().setCaseSensitivity(Qt.CaseSensitive) + self.form.replace.completer().setCaseSensitivity( + Qt.CaseSensitivity.CaseSensitive + ) if not self.note_ids: # no selected notes to affect diff --git a/qt/aqt/browser/find_duplicates.py b/qt/aqt/browser/find_duplicates.py index bd5c3237d..4af17cfe2 100644 --- a/qt/aqt/browser/find_duplicates.py +++ b/qt/aqt/browser/find_duplicates.py @@ -71,7 +71,7 @@ class FindDuplicatesDialog(QDialog): ).run_in_background() search = form.buttonBox.addButton( - tr.actions_search(), QDialogButtonBox.ActionRole + tr.actions_search(), QDialogButtonBox.ButtonRole.ActionRole ) qconnect(search.clicked, on_click) self.show() @@ -80,7 +80,7 @@ class FindDuplicatesDialog(QDialog): self._dupes = dupes if not self._dupesButton: self._dupesButton = b = self.form.buttonBox.addButton( - tr.browsing_tag_duplicates(), QDialogButtonBox.ActionRole + tr.browsing_tag_duplicates(), QDialogButtonBox.ButtonRole.ActionRole ) qconnect(b.clicked, self._tag_duplicates) text = "" diff --git a/qt/aqt/browser/previewer.py b/qt/aqt/browser/previewer.py index 774354070..190d65ecb 100644 --- a/qt/aqt/browser/previewer.py +++ b/qt/aqt/browser/previewer.py @@ -46,14 +46,14 @@ class Previewer(QDialog): def __init__( self, parent: QWidget, mw: AnkiQt, on_close: Callable[[], None] ) -> None: - super().__init__(None, Qt.Window) + super().__init__(None, Qt.WindowType.Window) mw.garbage_collect_on_dialog_finish(self) self._open = True self._parent = parent self._close_callback = on_close self.mw = mw icon = QIcon() - icon.addPixmap(QPixmap("icons:anki.png"), QIcon.Normal, QIcon.Off) + icon.addPixmap(QPixmap("icons:anki.png"), QIcon.Mode.Normal, QIcon.State.Off) disable_help_button(self) self.setWindowIcon(icon) @@ -86,7 +86,7 @@ class Previewer(QDialog): self.bbox = QDialogButtonBox() self._replay = self.bbox.addButton( - tr.actions_replay_audio(), QDialogButtonBox.ActionRole + tr.actions_replay_audio(), QDialogButtonBox.ButtonRole.ActionRole ) self._replay.setAutoDefault(False) self._replay.setShortcut(QKeySequence("R")) @@ -96,7 +96,7 @@ class Previewer(QDialog): both_sides_button = QCheckBox(tr.qt_misc_back_side_only()) both_sides_button.setShortcut(QKeySequence("B")) both_sides_button.setToolTip(tr.actions_shortcut_key(val="B")) - self.bbox.addButton(both_sides_button, QDialogButtonBox.ActionRole) + self.bbox.addButton(both_sides_button, QDialogButtonBox.ButtonRole.ActionRole) self._show_both_sides = self.mw.col.get_config_bool( Config.Bool.PREVIEW_BOTH_SIDES ) @@ -266,12 +266,12 @@ class MultiCardPreviewer(Previewer): def _create_gui(self) -> None: super()._create_gui() - self._prev = self.bbox.addButton("<", QDialogButtonBox.ActionRole) + self._prev = self.bbox.addButton("<", QDialogButtonBox.ButtonRole.ActionRole) self._prev.setAutoDefault(False) self._prev.setShortcut(QKeySequence("Left")) self._prev.setToolTip(tr.qt_misc_shortcut_key_left_arrow()) - self._next = self.bbox.addButton(">", QDialogButtonBox.ActionRole) + self._next = self.bbox.addButton(">", QDialogButtonBox.ButtonRole.ActionRole) self._next.setAutoDefault(True) self._next.setShortcut(QKeySequence("Right")) self._next.setToolTip(tr.qt_misc_shortcut_key_right_arrow_or_enter()) diff --git a/qt/aqt/browser/sidebar/model.py b/qt/aqt/browser/sidebar/model.py index 6376f176f..e9b3062d3 100644 --- a/qt/aqt/browser/sidebar/model.py +++ b/qt/aqt/browser/sidebar/model.py @@ -76,35 +76,48 @@ class SidebarModel(QAbstractItemModel): return self.createIndex(row, 0, parentItem) - def data(self, index: QModelIndex, role: int = Qt.DisplayRole) -> QVariant: + def data( + self, index: QModelIndex, role: int = Qt.ItemDataRole.DisplayRole + ) -> QVariant: if not index.isValid(): return QVariant() - if role not in (Qt.DisplayRole, Qt.DecorationRole, Qt.ToolTipRole, Qt.EditRole): + if role not in ( + Qt.ItemDataRole.DisplayRole, + Qt.ItemDataRole.DecorationRole, + Qt.ItemDataRole.ToolTipRole, + Qt.ItemDataRole.EditRole, + ): return QVariant() item: SidebarItem = index.internalPointer() - if role in (Qt.DisplayRole, Qt.EditRole): + if role in (Qt.ItemDataRole.DisplayRole, Qt.ItemDataRole.EditRole): return QVariant(item.name) - if role == Qt.ToolTipRole: + if role == Qt.ItemDataRole.ToolTipRole: return QVariant(item.tooltip) return QVariant(theme_manager.icon_from_resources(item.icon)) - def setData(self, index: QModelIndex, text: str, _role: int = Qt.EditRole) -> bool: + def setData( + self, index: QModelIndex, text: str, _role: int = Qt.ItemDataRole.EditRole + ) -> bool: return self.sidebar._on_rename(index.internalPointer(), text) - def supportedDropActions(self) -> Qt.DropActions: - return cast(Qt.DropActions, Qt.MoveAction) + def supportedDropActions(self) -> Qt.DropAction: + return cast(Qt.DropAction, Qt.DropAction.MoveAction) - def flags(self, index: QModelIndex) -> Qt.ItemFlags: + def flags(self, index: QModelIndex) -> Qt.ItemFlag: if not index.isValid(): - return cast(Qt.ItemFlags, Qt.ItemIsEnabled) - flags = Qt.ItemIsEnabled | Qt.ItemIsSelectable | Qt.ItemIsDragEnabled + return cast(Qt.ItemFlag, Qt.ItemFlag.ItemIsEnabled) + flags = ( + Qt.ItemFlag.ItemIsEnabled + | Qt.ItemFlag.ItemIsSelectable + | Qt.ItemFlag.ItemIsDragEnabled + ) item: SidebarItem = index.internalPointer() if item.item_type in self.sidebar.valid_drop_types: - flags |= Qt.ItemIsDropEnabled + flags |= Qt.ItemFlag.ItemIsDropEnabled if item.item_type.is_editable(): - flags |= Qt.ItemIsEditable + flags |= Qt.ItemFlag.ItemIsEditable - return cast(Qt.ItemFlags, flags) + return flags diff --git a/qt/aqt/browser/sidebar/searchbar.py b/qt/aqt/browser/sidebar/searchbar.py index b06ad7fcb..ae08f22fb 100644 --- a/qt/aqt/browser/sidebar/searchbar.py +++ b/qt/aqt/browser/sidebar/searchbar.py @@ -43,9 +43,9 @@ class SidebarSearchBar(QLineEdit): self.sidebar.search_for(self.text()) def keyPressEvent(self, evt: QKeyEvent) -> None: - if evt.key() in (Qt.Key_Up, Qt.Key_Down): + if evt.key() in (Qt.Key.Key_Up, Qt.Key.Key_Down): self.sidebar.setFocus() - elif evt.key() in (Qt.Key_Enter, Qt.Key_Return): + elif evt.key() in (Qt.Key.Key_Enter, Qt.Key.Key_Return): self.onSearch() else: QLineEdit.keyPressEvent(self, evt) diff --git a/qt/aqt/browser/sidebar/toolbar.py b/qt/aqt/browser/sidebar/toolbar.py index 61a0b0b5e..3f544682d 100644 --- a/qt/aqt/browser/sidebar/toolbar.py +++ b/qt/aqt/browser/sidebar/toolbar.py @@ -29,7 +29,7 @@ class SidebarToolbar(QToolBar): qconnect(self._action_group.triggered, self._on_action_group_triggered) self._setup_tools() self.setIconSize(QSize(16, 16)) - self.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) + self.setSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Fixed) self.setStyle(QStyleFactory.create("fusion")) def _setup_tools(self) -> None: diff --git a/qt/aqt/browser/sidebar/tree.py b/qt/aqt/browser/sidebar/tree.py index c48e45491..02be98c58 100644 --- a/qt/aqt/browser/sidebar/tree.py +++ b/qt/aqt/browser/sidebar/tree.py @@ -79,14 +79,14 @@ class SidebarTreeView(QTreeView): self.valid_drop_types: tuple[SidebarItemType, ...] = () self._refresh_needed = False - self.setContextMenuPolicy(Qt.CustomContextMenu) + self.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu) self.customContextMenuRequested.connect(self.onContextMenu) # type: ignore self.setUniformRowHeights(True) self.setHeaderHidden(True) self.setIndentation(15) self.setAutoExpandDelay(600) self.setDragDropOverwriteMode(False) - self.setEditTriggers(QAbstractItemView.EditKeyPressed) + self.setEditTriggers(QAbstractItemView.EditTrigger.EditKeyPressed) qconnect(self.expanded, self._on_expansion) qconnect(self.collapsed, self._on_collapse) @@ -122,12 +122,12 @@ class SidebarTreeView(QTreeView): def tool(self, tool: SidebarTool) -> None: self._tool = tool if tool == SidebarTool.SEARCH: - selection_mode = QAbstractItemView.SingleSelection - drag_drop_mode = QAbstractItemView.NoDragDrop + selection_mode = QAbstractItemView.SelectionMode.SingleSelection + drag_drop_mode = QAbstractItemView.DragDropMode.NoDragDrop double_click_expands = False else: - selection_mode = QAbstractItemView.ExtendedSelection - drag_drop_mode = QAbstractItemView.InternalMove + selection_mode = QAbstractItemView.SelectionMode.ExtendedSelection + drag_drop_mode = QAbstractItemView.DragDropMode.InternalMove double_click_expands = True self.setSelectionMode(selection_mode) self.setDragDropMode(drag_drop_mode) @@ -191,9 +191,9 @@ class SidebarTreeView(QTreeView): if current := self.find_item(current.has_same_id): index = self.model().index_for_item(current) self.selectionModel().setCurrentIndex( - index, QItemSelectionModel.SelectCurrent + index, QItemSelectionModel.SelectionFlag.SelectCurrent ) - self.scrollTo(index, QAbstractItemView.PositionAtCenter) + self.scrollTo(index, QAbstractItemView.ScrollHint.PositionAtCenter) def find_item( self, @@ -247,9 +247,12 @@ class SidebarTreeView(QTreeView): self.setExpanded(idx, True) if item.is_highlighted() and scroll_to_first_match: self.selectionModel().setCurrentIndex( - idx, QItemSelectionModel.SelectCurrent + idx, + QItemSelectionModel.SelectionFlag.SelectCurrent, + ) + self.scrollTo( + idx, QAbstractItemView.ScrollHint.PositionAtCenter ) - self.scrollTo(idx, QAbstractItemView.PositionAtCenter) scroll_to_first_match = False expand_node(parent or QModelIndex()) @@ -301,22 +304,29 @@ class SidebarTreeView(QTreeView): def dropEvent(self, event: QDropEvent) -> None: model = self.model() - target_item = model.item_for_index(self.indexAt(event.pos())) + if qtmajor == 5: + pos = event.pos() # type: ignore + else: + pos = event.position().toPoint() + target_item = model.item_for_index(self.indexAt(pos)) if self.handle_drag_drop(self._selected_items(), target_item): event.acceptProposedAction() def mouseReleaseEvent(self, event: QMouseEvent) -> None: super().mouseReleaseEvent(event) - if self.tool == SidebarTool.SEARCH and event.button() == Qt.LeftButton: + if ( + self.tool == SidebarTool.SEARCH + and event.button() == Qt.MouseButton.LeftButton + ): if (index := self.currentIndex()) == self.indexAt(event.pos()): self._on_search(index) def keyPressEvent(self, event: QKeyEvent) -> None: index = self.currentIndex() - if event.key() in (Qt.Key_Return, Qt.Key_Enter): + if event.key() in (Qt.Key.Key_Return, Qt.Key.Key_Enter): if not self.isPersistentEditorOpen(index): self._on_search(index) - elif event.key() == Qt.Key_Delete: + elif event.key() == Qt.Key.Key_Delete: self._on_delete_key(index) else: super().keyPressEvent(event) diff --git a/qt/aqt/browser/table/model.py b/qt/aqt/browser/table/model.py index f2ab6270b..e997bf230 100644 --- a/qt/aqt/browser/table/model.py +++ b/qt/aqt/browser/table/model.py @@ -3,7 +3,7 @@ from __future__ import annotations import time -from typing import Any, Callable, Sequence, cast +from typing import Any, Callable, Sequence import aqt from anki.cards import Card, CardId @@ -307,7 +307,7 @@ class DataModel(QAbstractTableModel): def data(self, index: QModelIndex = QModelIndex(), role: int = 0) -> Any: if not index.isValid(): return QVariant() - if role == Qt.FontRole: + if role == Qt.ItemDataRole.FontRole: if not self.column_at(index).uses_cell_font: return QVariant() qfont = QFont() @@ -315,30 +315,33 @@ class DataModel(QAbstractTableModel): qfont.setFamily(row.font_name) qfont.setPixelSize(row.font_size) return qfont - elif role == Qt.TextAlignmentRole: - align: Qt.AlignmentFlag | int = Qt.AlignVCenter + elif role == Qt.ItemDataRole.TextAlignmentRole: + align: Qt.AlignmentFlag | int = Qt.AlignmentFlag.AlignVCenter if self.column_at(index).alignment == Columns.ALIGNMENT_CENTER: - align |= Qt.AlignHCenter + align |= Qt.AlignmentFlag.AlignHCenter return align - elif role == Qt.DisplayRole: + elif role == Qt.ItemDataRole.DisplayRole: return self.get_cell(index).text - elif role == Qt.ToolTipRole and self._want_tooltips: + elif role == Qt.ItemDataRole.ToolTipRole and self._want_tooltips: return self.get_cell(index).text return QVariant() def headerData( self, section: int, orientation: Qt.Orientation, role: int = 0 ) -> str | None: - if orientation == Qt.Horizontal and role == Qt.DisplayRole: + if ( + orientation == Qt.Orientation.Horizontal + and role == Qt.ItemDataRole.DisplayRole + ): return self._state.column_label(self.column_at_section(section)) return None - def flags(self, index: QModelIndex) -> Qt.ItemFlags: + def flags(self, index: QModelIndex) -> Qt.ItemFlag: # shortcut for large selections (Ctrl+A) to avoid fetching large numbers of rows at once if row := self.get_cached_row(index): if row.is_deleted: - return Qt.ItemFlags(Qt.NoItemFlags) - return cast(Qt.ItemFlags, Qt.ItemIsEnabled | Qt.ItemIsSelectable) + return Qt.ItemFlag(Qt.ItemFlag.NoItemFlags) + return Qt.ItemFlag.ItemIsEnabled | Qt.ItemFlag.ItemIsSelectable def addon_column_fillin(key: str) -> Column: diff --git a/qt/aqt/browser/table/table.py b/qt/aqt/browser/table/table.py index f1eef337a..d08858b46 100644 --- a/qt/aqt/browser/table/table.py +++ b/qt/aqt/browser/table/table.py @@ -2,7 +2,7 @@ # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html from __future__ import annotations -from typing import Any, Callable, Sequence, cast +from typing import Any, Callable, Sequence import aqt import aqt.forms @@ -127,10 +127,8 @@ class Table: self.select_all() self._view.selectionModel().select( selection, - cast( - QItemSelectionModel.SelectionFlags, - QItemSelectionModel.Deselect | QItemSelectionModel.Rows, - ), + QItemSelectionModel.SelectionFlag.Deselect + | QItemSelectionModel.SelectionFlag.Rows, ) def select_single_card(self, card_id: CardId) -> None: @@ -202,10 +200,10 @@ class Table: # Move cursor def to_previous_row(self) -> None: - self._move_current(QAbstractItemView.MoveUp) + self._move_current(QAbstractItemView.CursorAction.MoveUp) def to_next_row(self) -> None: - self._move_current(QAbstractItemView.MoveDown) + self._move_current(QAbstractItemView.CursorAction.MoveDown) def to_first_row(self) -> None: self._move_current_to_row(0) @@ -248,7 +246,8 @@ class Table: def clear_current(self) -> None: self._view.selectionModel().setCurrentIndex( - QModelIndex(), QItemSelectionModel.NoUpdate + QModelIndex(), + QItemSelectionModel.SelectionFlag.NoUpdate, ) # Private methods @@ -268,7 +267,10 @@ class Table: index = self._model.index( row, self._view.horizontalHeader().logicalIndex(column) ) - self._view.selectionModel().setCurrentIndex(index, QItemSelectionModel.NoUpdate) + self._view.selectionModel().setCurrentIndex( + index, + QItemSelectionModel.SelectionFlag.NoUpdate, + ) def _reset_selection(self) -> None: """Remove selection and focus without emitting signals. @@ -286,7 +288,9 @@ class Table: self._model.index(row, 0), self._model.index(row, self._model.len_columns() - 1), ) - self._view.selectionModel().select(selection, QItemSelectionModel.SelectCurrent) + self._view.selectionModel().select( + selection, QItemSelectionModel.SelectionFlag.SelectCurrent + ) def _set_sort_indicator(self) -> None: hh = self._view.horizontalHeader() @@ -295,9 +299,9 @@ class Table: hh.setSortIndicatorShown(False) return if self._state.sort_backwards: - order = Qt.DescendingOrder + order = Qt.SortOrder.DescendingOrder else: - order = Qt.AscendingOrder + order = Qt.SortOrder.AscendingOrder hh.blockSignals(True) hh.setSortIndicator(index, order) hh.blockSignals(False) @@ -305,9 +309,10 @@ class Table: def _set_column_sizes(self) -> None: hh = self._view.horizontalHeader() - hh.setSectionResizeMode(QHeaderView.Interactive) + hh.setSectionResizeMode(QHeaderView.ResizeMode.Interactive) hh.setSectionResizeMode( - hh.logicalIndex(self._model.len_columns() - 1), QHeaderView.Stretch + hh.logicalIndex(self._model.len_columns() - 1), + QHeaderView.ResizeMode.Stretch, ) # this must be set post-resize or it doesn't work hh.setCascadingSectionResizes(False) @@ -334,7 +339,7 @@ class Table: ) qconnect(self._view.selectionModel().currentChanged, self._on_current_changed) self._view.setWordWrap(False) - self._view.setHorizontalScrollMode(QAbstractItemView.ScrollPerPixel) + self._view.setHorizontalScrollMode(QAbstractItemView.ScrollMode.ScrollPerPixel) self._view.horizontalScrollBar().setSingleStep(10) self._update_font() if not theme_manager.night_mode: @@ -346,7 +351,7 @@ class Table: self._view.setStyleSheet( f"QTableView {{ gridline-color: {colors.FRAME_BG} }}" ) - self._view.setContextMenuPolicy(Qt.CustomContextMenu) + self._view.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu) qconnect(self._view.customContextMenuRequested, self._on_context_menu) def _update_font(self) -> None: @@ -369,7 +374,7 @@ class Table: hh.setHighlightSections(False) hh.setMinimumSectionSize(50) hh.setSectionsMovable(True) - hh.setContextMenuPolicy(Qt.CustomContextMenu) + hh.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu) self._restore_header() qconnect(hh.customContextMenuRequested, self._on_header_context) qconnect(hh.sortIndicatorChanged, self._on_sort_column_changed) @@ -573,7 +578,9 @@ class Table: visible = top_border >= 0 and bottom_border < self._view.viewport().height() if not visible or scroll_even_if_visible: horizontal = self._view.horizontalScrollBar().value() - self._view.scrollTo(self._model.index(row, 0), self._view.PositionAtTop) + self._view.scrollTo( + self._model.index(row, 0), QAbstractItemView.ScrollHint.PositionAtTop + ) self._view.horizontalScrollBar().setValue(horizontal) def _scroll_to_column(self, column: int) -> None: @@ -583,26 +590,26 @@ class Table: if not visible: vertical = self._view.verticalScrollBar().value() self._view.scrollTo( - self._model.index(0, column), self._view.PositionAtCenter + self._model.index(0, column), + QAbstractItemView.ScrollHint.PositionAtCenter, ) self._view.verticalScrollBar().setValue(vertical) - def _move_current(self, direction: int, index: QModelIndex = None) -> None: + def _move_current( + self, direction: QAbstractItemView.CursorAction, index: QModelIndex = None + ) -> None: if not self.has_current(): return if index is None: index = self._view.moveCursor( - cast(QAbstractItemView.CursorAction, direction), + direction, self.browser.mw.app.keyboardModifiers(), ) self._view.selectionModel().setCurrentIndex( index, - cast( - QItemSelectionModel.SelectionFlag, - QItemSelectionModel.Clear - | QItemSelectionModel.Select - | QItemSelectionModel.Rows, - ), + QItemSelectionModel.SelectionFlag.Clear + | QItemSelectionModel.SelectionFlag.Select + | QItemSelectionModel.SelectionFlag.Rows, ) def _move_current_to_row(self, row: int) -> None: @@ -614,10 +621,8 @@ class Table: selection = QItemSelection(new, old) self._view.selectionModel().select( selection, - cast( - QItemSelectionModel.SelectionFlag, - QItemSelectionModel.SelectCurrent | QItemSelectionModel.Rows, - ), + QItemSelectionModel.SelectionFlag.SelectCurrent + | QItemSelectionModel.SelectionFlag.Rows, ) @@ -630,7 +635,7 @@ class StatusDelegate(QItemDelegate): self, painter: QPainter, option: QStyleOptionViewItem, index: QModelIndex ) -> None: if self._model.get_cell(index).is_rtl: - option.direction = Qt.RightToLeft + option.direction = Qt.LayoutDirection.RightToLeft if row_color := self._model.get_row(index).color: brush = QBrush(theme_manager.qcolor(row_color)) painter.save() diff --git a/qt/aqt/changenotetype.py b/qt/aqt/changenotetype.py index 89be796fb..d66209931 100644 --- a/qt/aqt/changenotetype.py +++ b/qt/aqt/changenotetype.py @@ -43,7 +43,7 @@ class ChangeNotetypeDialog(QDialog): self.show() def _setup_ui(self, notetype_id: NotetypeId) -> None: - self.setWindowModality(Qt.ApplicationModal) + self.setWindowModality(Qt.WindowModality.ApplicationModal) self.mw.garbage_collect_on_dialog_finish(self) self.setMinimumWidth(400) disable_help_button(self) diff --git a/qt/aqt/clayout.py b/qt/aqt/clayout.py index a499c5f42..ca081af87 100644 --- a/qt/aqt/clayout.py +++ b/qt/aqt/clayout.py @@ -45,7 +45,7 @@ class CardLayout(QDialog): parent: Optional[QWidget] = None, fill_empty: bool = False, ) -> None: - QDialog.__init__(self, parent or mw, Qt.Window) + QDialog.__init__(self, parent or mw, Qt.WindowType.Window) mw.garbage_collect_on_dialog_finish(self) self.mw = aqt.mw self.note = note @@ -80,7 +80,7 @@ class CardLayout(QDialog): self.redraw_everything() restoreGeom(self, "CardLayout") restoreSplitter(self.mainArea, "CardLayoutMainArea") - self.setWindowModality(Qt.ApplicationModal) + self.setWindowModality(Qt.WindowModality.ApplicationModal) self.show() # take the focus away from the first input area when starting up, # as users tend to accidentally type into the template @@ -108,7 +108,9 @@ class CardLayout(QDialog): def setupTopArea(self) -> None: self.topArea = QWidget() - self.topArea.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Minimum) + self.topArea.setSizePolicy( + QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Minimum + ) self.topAreaForm = aqt.forms.clayout_top.Ui_Form() self.topAreaForm.setupUi(self.topArea) self.topAreaForm.templateOptions.setText( @@ -215,8 +217,8 @@ class CardLayout(QDialog): def setupMainArea(self) -> None: split = self.mainArea = QSplitter() - split.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) - split.setOrientation(Qt.Horizontal) + split.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding) + split.setOrientation(Qt.Orientation.Horizontal) left = QWidget() tform = self.tform = aqt.forms.template.Ui_Form() tform.setupUi(left) @@ -305,7 +307,7 @@ class CardLayout(QDialog): if not editor.find(text): # try again from top cursor = editor.textCursor() - cursor.movePosition(QTextCursor.Start) + cursor.movePosition(QTextCursor.MoveOperation.Start) editor.setTextCursor(cursor) if not editor.find(text): tooltip("No matches found.") @@ -752,7 +754,7 @@ class CardLayout(QDialog): if t["did"]: te.setText(self.col.decks.get(t["did"])["name"]) te.selectAll() - bb = QDialogButtonBox(QDialogButtonBox.Close) + bb = QDialogButtonBox(QDialogButtonBox.StandardButton.Close) qconnect(bb.rejected, d.close) l.addWidget(bb) d.setLayout(l) diff --git a/qt/aqt/customstudy.py b/qt/aqt/customstudy.py index 8b056342b..878149e48 100644 --- a/qt/aqt/customstudy.py +++ b/qt/aqt/customstudy.py @@ -30,7 +30,7 @@ class CustomStudy(QDialog): self.created_custom_study = False f.setupUi(self) disable_help_button(self) - self.setWindowModality(Qt.WindowModal) + self.setWindowModality(Qt.WindowModality.WindowModal) self.setupSignals() f.radioNew.click() self.exec() @@ -116,7 +116,7 @@ class CustomStudy(QDialog): f.spin.setValue(sval) f.preSpin.setText(pre) f.postSpin.setText(post) - f.buttonBox.button(QDialogButtonBox.Ok).setText(ok) + f.buttonBox.button(QDialogButtonBox.StandardButton.Ok).setText(ok) self.radioIdx = idx def accept(self) -> None: diff --git a/qt/aqt/deckconf.py b/qt/aqt/deckconf.py index c7d5ac753..40432613f 100644 --- a/qt/aqt/deckconf.py +++ b/qt/aqt/deckconf.py @@ -40,13 +40,15 @@ class DeckConf(QDialog): self.mw.checkpoint(tr.actions_options()) self.setupCombos() self.setupConfs() - self.setWindowModality(Qt.WindowModal) + self.setWindowModality(Qt.WindowModality.WindowModal) qconnect( self.form.buttonBox.helpRequested, lambda: openHelp(HelpPage.DECK_OPTIONS) ) qconnect(self.form.confOpts.clicked, self.confOpts) qconnect( - self.form.buttonBox.button(QDialogButtonBox.RestoreDefaults).clicked, + self.form.buttonBox.button( + QDialogButtonBox.StandardButton.RestoreDefaults + ).clicked, self.onRestore, ) self.setWindowTitle( diff --git a/qt/aqt/deckdescription.py b/qt/aqt/deckdescription.py index 064883613..ccd534601 100644 --- a/qt/aqt/deckdescription.py +++ b/qt/aqt/deckdescription.py @@ -17,7 +17,7 @@ class DeckDescriptionDialog(QDialog): silentlyClose = True def __init__(self, mw: aqt.main.AnkiQt) -> None: - QDialog.__init__(self, mw, Qt.Window) + QDialog.__init__(self, mw, Qt.WindowType.Window) self.mw = mw # set on success @@ -39,7 +39,7 @@ class DeckDescriptionDialog(QDialog): def _setup_ui(self) -> None: self.setWindowTitle(tr.scheduling_description()) - self.setWindowModality(Qt.ApplicationModal) + self.setWindowModality(Qt.WindowModality.ApplicationModal) self.mw.garbage_collect_on_dialog_finish(self) self.setMinimumWidth(400) disable_help_button(self) @@ -58,7 +58,7 @@ class DeckDescriptionDialog(QDialog): box.addWidget(self.description) button_box = QDialogButtonBox() - ok = button_box.addButton(QDialogButtonBox.Ok) + ok = button_box.addButton(QDialogButtonBox.StandardButton.Ok) qconnect(ok.clicked, self.save_and_accept) box.addWidget(button_box) diff --git a/qt/aqt/deckoptions.py b/qt/aqt/deckoptions.py index 3a89969d0..76bf44dcb 100644 --- a/qt/aqt/deckoptions.py +++ b/qt/aqt/deckoptions.py @@ -28,14 +28,14 @@ class DeckOptionsDialog(QDialog): silentlyClose = True def __init__(self, mw: aqt.main.AnkiQt, deck: DeckDict) -> None: - QDialog.__init__(self, mw, Qt.Window) + QDialog.__init__(self, mw, Qt.WindowType.Window) self.mw = mw self._deck = deck self._setup_ui() self.show() def _setup_ui(self) -> None: - self.setWindowModality(Qt.ApplicationModal) + self.setWindowModality(Qt.WindowModality.ApplicationModal) self.mw.garbage_collect_on_dialog_finish(self) self.setMinimumWidth(400) disable_help_button(self) diff --git a/qt/aqt/editcurrent.py b/qt/aqt/editcurrent.py index fa934f305..4d841b5bc 100644 --- a/qt/aqt/editcurrent.py +++ b/qt/aqt/editcurrent.py @@ -12,7 +12,7 @@ from aqt.utils import disable_help_button, restoreGeom, saveGeom, tr class EditCurrent(QDialog): def __init__(self, mw: aqt.AnkiQt) -> None: - QDialog.__init__(self, None, Qt.Window) + QDialog.__init__(self, None, Qt.WindowType.Window) mw.garbage_collect_on_dialog_finish(self) self.mw = mw self.form = aqt.forms.editcurrent.Ui_Dialog() @@ -21,7 +21,7 @@ class EditCurrent(QDialog): disable_help_button(self) self.setMinimumHeight(400) self.setMinimumWidth(250) - self.form.buttonBox.button(QDialogButtonBox.Close).setShortcut( + self.form.buttonBox.button(QDialogButtonBox.StandardButton.Close).setShortcut( QKeySequence("Ctrl+Return") ) self.editor = aqt.editor.Editor(self.mw, self.form.fieldsArea, self) diff --git a/qt/aqt/editor.py b/qt/aqt/editor.py index 5fce6df39..620802ed4 100644 --- a/qt/aqt/editor.py +++ b/qt/aqt/editor.py @@ -902,7 +902,7 @@ $editorToolbar.then(({{ toolbar }}) => toolbar.appendGroup({{ @deprecated(info=_js_legacy) def _onHtmlEdit(self, field: int) -> None: - d = QDialog(self.widget, Qt.Window) + d = QDialog(self.widget, Qt.WindowType.Window) form = aqt.forms.edithtml.Ui_Dialog() form.setupUi(d) restoreGeom(d, "htmlEditor") @@ -911,11 +911,11 @@ $editorToolbar.then(({{ toolbar }}) => toolbar.appendGroup({{ form.buttonBox.helpRequested, lambda: openHelp(HelpPage.EDITING_FEATURES) ) font = QFont("Courier") - font.setStyleHint(QFont.TypeWriter) + font.setStyleHint(QFont.StyleHint.TypeWriter) form.textEdit.setFont(font) form.textEdit.setPlainText(self.note.fields[field]) d.show() - form.textEdit.moveCursor(QTextCursor.End) + form.textEdit.moveCursor(QTextCursor.MoveOperation.End) d.exec() html = form.textEdit.toPlainText() if html.find(">") > -1: @@ -997,7 +997,10 @@ $editorToolbar.then(({{ toolbar }}) => toolbar.appendGroup({{ def onChangeCol(self) -> None: if isLin: new = QColorDialog.getColor( - QColor(self.fcolour), None, None, QColorDialog.DontUseNativeDialog + QColor(self.fcolour), + None, + None, + QColorDialog.ColorDialogOption.DontUseNativeDialog, ) else: new = QColorDialog.getColor(QColor(self.fcolour), None) @@ -1118,10 +1121,10 @@ class EditorWebView(AnkiWebView): self._flagAnkiText() def onCut(self) -> None: - self.triggerPageAction(QWebEnginePage.Cut) + self.triggerPageAction(QWebEnginePage.WebAction.Cut) def onCopy(self) -> None: - self.triggerPageAction(QWebEnginePage.Copy) + self.triggerPageAction(QWebEnginePage.WebAction.Copy) def _wantsExtendedPaste(self) -> bool: strip_html = self.editor.mw.col.get_config_bool( @@ -1140,10 +1143,10 @@ class EditorWebView(AnkiWebView): self.editor.doPaste(html, internal, extended) def onPaste(self) -> None: - self._onPaste(QClipboard.Clipboard) + self._onPaste(QClipboard.Mode.Clipboard) def onMiddleClickPaste(self) -> None: - self._onPaste(QClipboard.Selection) + self._onPaste(QClipboard.Mode.Selection) def dragEnterEvent(self, evt: QDragEnterEvent) -> None: evt.accept() diff --git a/qt/aqt/emptycards.py b/qt/aqt/emptycards.py index a84bc309f..1048caddd 100644 --- a/qt/aqt/emptycards.py +++ b/qt/aqt/emptycards.py @@ -63,7 +63,7 @@ class EmptyCardsDialog(QDialog): qconnect(self.finished, on_finished) self._delete_button = self.form.buttonBox.addButton( - tr.empty_cards_delete_button(), QDialogButtonBox.ActionRole + tr.empty_cards_delete_button(), QDialogButtonBox.ButtonRole.ActionRole ) self._delete_button.setAutoDefault(False) qconnect(self._delete_button.clicked, self._on_delete) diff --git a/qt/aqt/exporting.py b/qt/aqt/exporting.py index dbcdf6260..7fbfbe001 100644 --- a/qt/aqt/exporting.py +++ b/qt/aqt/exporting.py @@ -31,7 +31,7 @@ class ExportDialog(QDialog): did: DeckId | None = None, cids: list[CardId] | None = None, ): - QDialog.__init__(self, mw, Qt.Window) + QDialog.__init__(self, mw, Qt.WindowType.Window) self.mw = mw self.col = mw.col.weakref() self.frm = aqt.forms.exporting.Ui_ExportDialog() @@ -64,7 +64,7 @@ class ExportDialog(QDialog): self.frm.deck.addItems(self.decks) # save button b = QPushButton(tr.exporting_export()) - self.frm.buttonBox.addButton(b, QDialogButtonBox.AcceptRole) + self.frm.buttonBox.addButton(b, QDialogButtonBox.ButtonRole.AcceptRole) # set default option if accessed through deck button if did: name = self.mw.col.decks.get(did)["name"] diff --git a/qt/aqt/fields.py b/qt/aqt/fields.py index 9048e935e..00155a79b 100644 --- a/qt/aqt/fields.py +++ b/qt/aqt/fields.py @@ -45,13 +45,19 @@ class FieldDialog(QDialog): without_unicode_isolation(tr.fields_fields_for(val=self.model["name"])) ) disable_help_button(self) - self.form.buttonBox.button(QDialogButtonBox.Help).setAutoDefault(False) - self.form.buttonBox.button(QDialogButtonBox.Cancel).setAutoDefault(False) - self.form.buttonBox.button(QDialogButtonBox.Save).setAutoDefault(False) + self.form.buttonBox.button(QDialogButtonBox.StandardButton.Help).setAutoDefault( + False + ) + self.form.buttonBox.button( + QDialogButtonBox.StandardButton.Cancel + ).setAutoDefault(False) + self.form.buttonBox.button(QDialogButtonBox.StandardButton.Save).setAutoDefault( + False + ) self.currentIdx: Optional[int] = None self.fillFields() self.setupSignals() - self.form.fieldList.setDragDropMode(QAbstractItemView.InternalMove) + self.form.fieldList.setDragDropMode(QAbstractItemView.DragDropMode.InternalMove) self.form.fieldList.dropEvent = self.onDrop # type: ignore[assignment] self.form.fieldList.setCurrentRow(open_at) self.exec() @@ -77,15 +83,21 @@ class FieldDialog(QDialog): def onDrop(self, ev: QDropEvent) -> None: fieldList = self.form.fieldList indicatorPos = fieldList.dropIndicatorPosition() - dropPos = fieldList.indexAt(ev.pos()).row() + if qtmajor == 5: + pos = ev.pos() # type: ignore + else: + pos = ev.position().toPoint() + dropPos = fieldList.indexAt(pos).row() idx = self.currentIdx if dropPos == idx: return - if indicatorPos == QAbstractItemView.OnViewport: # to bottom. + if ( + indicatorPos == QAbstractItemView.DropIndicatorPosition.OnViewport + ): # to bottom. movePos = fieldList.count() - 1 - elif indicatorPos == QAbstractItemView.AboveItem: + elif indicatorPos == QAbstractItemView.DropIndicatorPosition.AboveItem: movePos = dropPos - elif indicatorPos == QAbstractItemView.BelowItem: + elif indicatorPos == QAbstractItemView.DropIndicatorPosition.BelowItem: movePos = dropPos + 1 # the item in idx is removed thus subtract 1. if idx < dropPos: diff --git a/qt/aqt/filtered_deck.py b/qt/aqt/filtered_deck.py index 1876521ba..e225cb0e7 100644 --- a/qt/aqt/filtered_deck.py +++ b/qt/aqt/filtered_deck.py @@ -90,7 +90,7 @@ class FilteredDeckConfigDialog(QDialog): QPushButton[label="hint"] {{ color: {grey} }}""" ) disable_help_button(self) - self.setWindowModality(Qt.WindowModal) + self.setWindowModality(Qt.WindowModality.WindowModal) qconnect( self.form.buttonBox.helpRequested, lambda: openHelp(HelpPage.FILTERED_DECK) ) @@ -117,7 +117,9 @@ class FilteredDeckConfigDialog(QDialog): build_label = tr.actions_rebuild() else: build_label = tr.decks_build() - self.form.buttonBox.button(QDialogButtonBox.Ok).setText(build_label) + self.form.buttonBox.button(QDialogButtonBox.StandardButton.Ok).setText( + build_label + ) form.resched.setChecked(config.reschedule) self._onReschedToggled(0) diff --git a/qt/aqt/forms/findreplace.ui b/qt/aqt/forms/findreplace.ui index 8d08770ab..ec11bbc24 100644 --- a/qt/aqt/forms/findreplace.ui +++ b/qt/aqt/forms/findreplace.ui @@ -6,8 +6,8 @@ 0 0 - 377 - 224 + 479 + 247 @@ -31,7 +31,7 @@ QComboBox::NoInsert - QComboBox::AdjustToMinimumContentsLength + QComboBox::AdjustToMinimumContentsLengthWithIcon @@ -59,7 +59,7 @@ - QComboBox::AdjustToMinimumContentsLength + QComboBox::AdjustToMinimumContentsLengthWithIcon @@ -95,7 +95,7 @@ QComboBox::NoInsert - QComboBox::AdjustToMinimumContentsLength + QComboBox::AdjustToMinimumContentsLengthWithIcon diff --git a/qt/aqt/importing.py b/qt/aqt/importing.py index 46f852e3d..7260ac16f 100644 --- a/qt/aqt/importing.py +++ b/qt/aqt/importing.py @@ -35,7 +35,7 @@ from aqt.utils import ( class ChangeMap(QDialog): def __init__(self, mw: AnkiQt, model: dict, current: str) -> None: - QDialog.__init__(self, mw, Qt.Window) + QDialog.__init__(self, mw, Qt.WindowType.Window) self.mw = mw self.model = model self.frm = aqt.forms.changemap.Ui_ChangeMap() @@ -85,13 +85,14 @@ class ImportDialog(QDialog): _DEFAULT_FILE_DELIMITER = "\t" def __init__(self, mw: AnkiQt, importer: Any) -> None: - QDialog.__init__(self, mw, Qt.Window) + QDialog.__init__(self, mw, Qt.WindowType.Window) self.mw = mw self.importer = importer self.frm = aqt.forms.importing.Ui_ImportDialog() self.frm.setupUi(self) qconnect( - self.frm.buttonBox.button(QDialogButtonBox.Help).clicked, self.helpRequested + self.frm.buttonBox.button(QDialogButtonBox.StandardButton.Help).clicked, + self.helpRequested, ) disable_help_button(self) self.setupMappingFrame() @@ -108,7 +109,7 @@ class ImportDialog(QDialog): self.frm.tagModified.setCol(self.mw.col) # import button b = QPushButton(tr.actions_import()) - self.frm.buttonBox.addButton(b, QDialogButtonBox.AcceptRole) + self.frm.buttonBox.addButton(b, QDialogButtonBox.ButtonRole.AcceptRole) self.exec() def setupOptions(self) -> None: diff --git a/qt/aqt/main.py b/qt/aqt/main.py index 967819ff8..1249e764f 100644 --- a/qt/aqt/main.py +++ b/qt/aqt/main.py @@ -107,10 +107,7 @@ class AnkiQt(QMainWindow): self.pm = profileManager # init rest of app self.safeMode = ( - bool( - cast(Qt.KeyboardModifier, self.app.queryKeyboardModifiers()) - & Qt.ShiftModifier - ) + bool(self.app.queryKeyboardModifiers() & Qt.KeyboardModifier.ShiftModifier) or self.opts.safemode ) try: @@ -817,15 +814,15 @@ title="{}" {}>{}""".format( self.form.setupUi(self) # toolbar tweb = self.toolbarWeb = aqt.webview.AnkiWebView(title="top toolbar") - tweb.setFocusPolicy(Qt.WheelFocus) + tweb.setFocusPolicy(Qt.FocusPolicy.WheelFocus) self.toolbar = aqt.toolbar.Toolbar(self, tweb) # main area self.web = aqt.webview.AnkiWebView(title="main webview") - self.web.setFocusPolicy(Qt.WheelFocus) + self.web.setFocusPolicy(Qt.FocusPolicy.WheelFocus) self.web.setMinimumWidth(400) # bottom area sweb = self.bottomWeb = aqt.webview.AnkiWebView(title="bottom toolbar") - sweb.setFocusPolicy(Qt.WheelFocus) + sweb.setFocusPolicy(Qt.FocusPolicy.WheelFocus) # add in a layout self.mainLayout = QVBoxLayout() self.mainLayout.setContentsMargins(0, 0, 0, 0) @@ -991,7 +988,7 @@ title="{}" {}>{}""".format( def raiseMain(self) -> bool: if not self.app.activeWindow(): # make sure window is shown - self.setWindowState(self.windowState() & ~Qt.WindowMinimized) # type: ignore + self.setWindowState(self.windowState() & ~Qt.WindowState.WindowMinimized) # type: ignore return True def setupStyle(self) -> None: @@ -1393,7 +1390,7 @@ title="{}" {}>{}""".format( frm.setupUi(d) restoreGeom(d, "DebugConsoleWindow") restoreSplitter(frm.splitter, "DebugConsoleWindow") - font = QFontDatabase.systemFont(QFontDatabase.FixedFont) + font = QFontDatabase.systemFont(QFontDatabase.SystemFont.FixedFont) font.setPointSize(frm.text.font().pointSize() + 1) frm.text.setFont(font) frm.log.setFont(font) @@ -1485,7 +1482,7 @@ title="{}" {}>{}""".format( def onDebugPrint(self, frm: aqt.forms.debug.Ui_Dialog) -> None: cursor = frm.text.textCursor() position = cursor.position() - cursor.select(QTextCursor.LineUnderCursor) + cursor.select(QTextCursor.SelectionType.LineUnderCursor) line = cursor.selectedText() pfx, sfx = "pp(", ")" if not line.startswith(pfx): @@ -1566,7 +1563,7 @@ title="{}" {}>{}""".format( cast(QAction, action).setStatusTip("") def onMacMinimize(self) -> None: - self.setWindowState(self.windowState() | Qt.WindowMinimized) # type: ignore + self.setWindowState(self.windowState() | Qt.WindowState.WindowMinimized) # type: ignore # Single instance support ########################################################################## @@ -1606,7 +1603,7 @@ title="{}" {}>{}""".format( if isWin: # on windows we can raise the window by minimizing and restoring self.showMinimized() - self.setWindowState(Qt.WindowActive) + self.setWindowState(Qt.WindowState.WindowActive) self.showNormal() else: # on osx we can raise the window. on unity the icon in the tray will just flash. diff --git a/qt/aqt/mediacheck.py b/qt/aqt/mediacheck.py index 299031444..fd58d4274 100644 --- a/qt/aqt/mediacheck.py +++ b/qt/aqt/mediacheck.py @@ -105,33 +105,33 @@ class MediaChecker: text = QPlainTextEdit() text.setReadOnly(True) text.setPlainText(report) - text.setWordWrapMode(QTextOption.NoWrap) + text.setWordWrapMode(QTextOption.WrapMode.NoWrap) layout.addWidget(text) - box = QDialogButtonBox(QDialogButtonBox.Close) + box = QDialogButtonBox(QDialogButtonBox.StandardButton.Close) layout.addWidget(box) if output.unused: b = QPushButton(tr.media_check_delete_unused()) b.setAutoDefault(False) - box.addButton(b, QDialogButtonBox.RejectRole) + box.addButton(b, QDialogButtonBox.ButtonRole.RejectRole) qconnect(b.clicked, lambda c: self._on_trash_files(output.unused)) if output.missing: if any(map(lambda x: x.startswith("latex-"), output.missing)): b = QPushButton(tr.media_check_render_latex()) b.setAutoDefault(False) - box.addButton(b, QDialogButtonBox.RejectRole) + box.addButton(b, QDialogButtonBox.ButtonRole.RejectRole) qconnect(b.clicked, self._on_render_latex) if output.have_trash: b = QPushButton(tr.media_check_empty_trash()) b.setAutoDefault(False) - box.addButton(b, QDialogButtonBox.RejectRole) + box.addButton(b, QDialogButtonBox.ButtonRole.RejectRole) qconnect(b.clicked, lambda c: self._on_empty_trash()) b = QPushButton(tr.media_check_restore_trash()) b.setAutoDefault(False) - box.addButton(b, QDialogButtonBox.RejectRole) + box.addButton(b, QDialogButtonBox.ButtonRole.RejectRole) qconnect(b.clicked, lambda c: self._on_restore_trash()) qconnect(box.rejected, diag.reject) diff --git a/qt/aqt/mediasync.py b/qt/aqt/mediasync.py index 8b8e4a92d..1c5a366ff 100644 --- a/qt/aqt/mediasync.py +++ b/qt/aqt/mediasync.py @@ -162,7 +162,9 @@ class MediaSyncDialog(QDialog): self.abort_button = QPushButton(tr.sync_abort_button()) qconnect(self.abort_button.clicked, self._on_abort) self.abort_button.setAutoDefault(False) - self.form.buttonBox.addButton(self.abort_button, QDialogButtonBox.ActionRole) + self.form.buttonBox.addButton( + self.abort_button, QDialogButtonBox.ButtonRole.ActionRole + ) self.abort_button.setHidden(not self._syncer.is_syncing()) gui_hooks.media_sync_did_progress.append(self._on_log_entry) @@ -171,7 +173,7 @@ class MediaSyncDialog(QDialog): self.form.plainTextEdit.setPlainText( "\n".join(self._entry_to_text(x) for x in syncer.entries()) ) - self.form.plainTextEdit.moveCursor(QTextCursor.End) + self.form.plainTextEdit.moveCursor(QTextCursor.MoveOperation.End) self.show() def reject(self) -> None: diff --git a/qt/aqt/models.py b/qt/aqt/models.py index e395b3d3d..5904acbda 100644 --- a/qt/aqt/models.py +++ b/qt/aqt/models.py @@ -48,7 +48,7 @@ class Models(QDialog): parent = parent or mw self.fromMain = fromMain self.selected_notetype_id = selected_notetype_id - QDialog.__init__(self, parent, Qt.Window) + QDialog.__init__(self, parent, Qt.WindowType.Window) self.col = mw.col.weakref() assert self.col self.mm = self.col.models @@ -97,7 +97,7 @@ class Models(QDialog): default_buttons.append((tr.notetypes_options(), self.onAdvanced)) for label, func in gui_hooks.models_did_init_buttons(default_buttons, self): - button = box.addButton(label, QDialogButtonBox.ActionRole) + button = box.addButton(label, QDialogButtonBox.ButtonRole.ActionRole) qconnect(button.clicked, func) qconnect(f.modelsList.itemDoubleClicked, self.onRename) @@ -235,7 +235,7 @@ class AddModel(QDialog): self.parent_ = parent or mw self.mw = mw self.col = mw.col - QDialog.__init__(self, self.parent_, Qt.Window) + QDialog.__init__(self, self.parent_, Qt.WindowType.Window) self.model = None self.dialog = aqt.forms.addmodel.Ui_Dialog() self.dialog.setupUi(self) diff --git a/qt/aqt/operations/scheduling.py b/qt/aqt/operations/scheduling.py index 52bc95f01..7cfeea6f8 100644 --- a/qt/aqt/operations/scheduling.py +++ b/qt/aqt/operations/scheduling.py @@ -93,7 +93,7 @@ def reposition_new_cards_dialog( d = QDialog(parent) disable_help_button(d) - d.setWindowModality(Qt.WindowModal) + d.setWindowModality(Qt.WindowModality.WindowModal) frm = aqt.forms.reposition.Ui_Dialog() frm.setupUi(d) diff --git a/qt/aqt/preferences.py b/qt/aqt/preferences.py index 6a163df97..1ebb87e4a 100644 --- a/qt/aqt/preferences.py +++ b/qt/aqt/preferences.py @@ -16,14 +16,18 @@ from aqt.utils import HelpPage, disable_help_button, openHelp, showInfo, showWar class Preferences(QDialog): def __init__(self, mw: AnkiQt) -> None: - QDialog.__init__(self, mw, Qt.Window) + QDialog.__init__(self, mw, Qt.WindowType.Window) self.mw = mw self.prof = self.mw.pm.profile self.form = aqt.forms.preferences.Ui_Preferences() self.form.setupUi(self) disable_help_button(self) - self.form.buttonBox.button(QDialogButtonBox.Help).setAutoDefault(False) - self.form.buttonBox.button(QDialogButtonBox.Close).setAutoDefault(False) + self.form.buttonBox.button(QDialogButtonBox.StandardButton.Help).setAutoDefault( + False + ) + self.form.buttonBox.button( + QDialogButtonBox.StandardButton.Close + ).setAutoDefault(False) qconnect( self.form.buttonBox.helpRequested, lambda: openHelp(HelpPage.PREFERENCES) ) diff --git a/qt/aqt/profiles.py b/qt/aqt/profiles.py index 3832dca5f..ce1fabef6 100644 --- a/qt/aqt/profiles.py +++ b/qt/aqt/profiles.py @@ -461,9 +461,9 @@ create table if not exists profiles code = obj[1] name = obj[0] r = QMessageBox.question( - None, "Anki", tr.profiles_confirm_lang_choice(lang=name), QMessageBox.Yes | QMessageBox.No, QMessageBox.No # type: ignore + None, "Anki", tr.profiles_confirm_lang_choice(lang=name), QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, QMessageBox.StandardButton.No # type: ignore ) - if r != QMessageBox.Yes: + if r != QMessageBox.StandardButton.Yes: return self.setDefaultLang(f.lang.currentRow()) self.setLang(code) diff --git a/qt/aqt/progress.py b/qt/aqt/progress.py index 2d8b8e290..0e89195e3 100644 --- a/qt/aqt/progress.py +++ b/qt/aqt/progress.py @@ -92,7 +92,7 @@ class ProgressManager: self._win.form.progressBar.setTextVisible(False) self._win.form.label.setText(label) self._win.setWindowTitle("Anki") - self._win.setWindowModality(Qt.ApplicationModal) + self._win.setWindowModality(Qt.WindowModality.ApplicationModal) self._win.setMinimumWidth(300) self._busy_cursor_timer = QTimer(self.mw) self._busy_cursor_timer.setSingleShot(True) @@ -177,7 +177,7 @@ class ProgressManager: elap = time.time() - self._shown if elap >= 0.5: break - self.app.processEvents(QEventLoop.ExcludeUserInputEvents) # type: ignore #possibly related to https://github.com/python/mypy/issues/6910 + self.app.processEvents(QEventLoop.ProcessEventsFlag.ExcludeUserInputEvents) # type: ignore #possibly related to https://github.com/python/mypy/issues/6910 # if the parent window has been deleted, the progress dialog may have # already been dropped; delete it if it hasn't been if not sip.isdeleted(self._win): @@ -186,7 +186,7 @@ class ProgressManager: self._shown = 0 def _set_busy_cursor(self) -> None: - self.mw.app.setOverrideCursor(QCursor(Qt.WaitCursor)) + self.mw.app.setOverrideCursor(QCursor(Qt.CursorShape.WaitCursor)) def _restore_cursor(self) -> None: self.app.restoreOverrideCursor() @@ -235,6 +235,6 @@ class ProgressDialog(QDialog): evt.ignore() def keyPressEvent(self, evt: QKeyEvent) -> None: - if evt.key() == Qt.Key_Escape: + if evt.key() == Qt.Key.Key_Escape: evt.ignore() self.wantCancel = True diff --git a/qt/aqt/qt.py b/qt/aqt/qt.py index 1d53b9fcf..541a3fb8c 100644 --- a/qt/aqt/qt.py +++ b/qt/aqt/qt.py @@ -9,14 +9,34 @@ import sys import traceback from typing import Callable, Union -from PyQt5.Qt import * # type: ignore -from PyQt5.QtCore import * -from PyQt5.QtCore import pyqtRemoveInputHook # pylint: disable=no-name-in-module -from PyQt5.QtGui import * # type: ignore -from PyQt5.QtNetwork import QLocalServer, QLocalSocket, QNetworkProxy -from PyQt5.QtWebChannel import QWebChannel -from PyQt5.QtWebEngineWidgets import * -from PyQt5.QtWidgets import * +try: + from PyQt6 import sip + from PyQt6.QtCore import * + + # conflicting Qt and qFuzzyCompare definitions require an ignore + from PyQt6.QtGui import * # type: ignore[misc] + from PyQt6.QtNetwork import QLocalServer, QLocalSocket, QNetworkProxy + from PyQt6.QtWebChannel import QWebChannel + from PyQt6.QtWebEngineCore import * + from PyQt6.QtWebEngineWidgets import * + from PyQt6.QtWidgets import * +except: + from PyQt5.QtCore import * # type: ignore + from PyQt5.QtGui import * # type: ignore + from PyQt5.QtNetwork import ( # type: ignore + QLocalServer, + QLocalSocket, + QNetworkProxy, + ) + from PyQt5.QtWebChannel import QWebChannel # type: ignore + from PyQt5.QtWebEngineCore import * # type: ignore + from PyQt5.QtWebEngineWidgets import * # type: ignore + from PyQt5.QtWidgets import * # type: ignore + + try: + from PyQt5 import sip # type: ignore + except ImportError: + import sip # type: ignore from anki.utils import isMac, isWin @@ -24,12 +44,6 @@ from anki.utils import isMac, isWin os.environ["LIBOVERLAY_SCROLLBAR"] = "0" -try: - from PyQt5 import sip -except ImportError: - import sip # type: ignore - - def debug() -> None: from pdb import set_trace diff --git a/qt/aqt/qt5.py b/qt/aqt/qt5.py index 401c1fc1a..927eccbf1 100644 --- a/qt/aqt/qt5.py +++ b/qt/aqt/qt5.py @@ -1,6 +1,8 @@ # Copyright: Ankitects Pty Ltd and contributors # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html +# pylint: skip-file + """ PyQt5-only code """ diff --git a/qt/aqt/reviewer.py b/qt/aqt/reviewer.py index 31ba40529..afc309655 100644 --- a/qt/aqt/reviewer.py +++ b/qt/aqt/reviewer.py @@ -433,11 +433,11 @@ class Reviewer: return [ ("e", self.mw.onEditCurrent), (" ", self.onEnterKey), - (Qt.Key_Return, self.onEnterKey), - (Qt.Key_Enter, self.onEnterKey), + (Qt.Key.Key_Return, self.onEnterKey), + (Qt.Key.Key_Enter, self.onEnterKey), ("m", self.showContextMenu), ("r", self.replayAudio), - (Qt.Key_F5, self.replayAudio), + (Qt.Key.Key_F5, self.replayAudio), *( (f"Ctrl+{flag.index}", self.set_flag_func(flag.index)) for flag in self.mw.flags.all() @@ -875,7 +875,7 @@ time = %(time)d; part2 = tr.studying_minute(count=mins) fin = tr.studying_finish() diag = askUserDialog(f"{part1} {part2}", [tr.studying_continue(), fin]) - diag.setIcon(QMessageBox.Information) + diag.setIcon(QMessageBox.Icon.Information) if diag.run() == fin: self.mw.moveToState("deckBrowser") return True diff --git a/qt/aqt/sound.py b/qt/aqt/sound.py index 3670413b1..b7d3a5c99 100644 --- a/qt/aqt/sound.py +++ b/qt/aqt/sound.py @@ -666,15 +666,18 @@ class RecordDialog(QDialog): hbox.addWidget(self.label) v = QVBoxLayout() v.addLayout(hbox) - buts = QDialogButtonBox.Save | QDialogButtonBox.Cancel + buts = ( + QDialogButtonBox.StandardButton.Save + | QDialogButtonBox.StandardButton.Cancel + ) b = QDialogButtonBox(buts) # type: ignore v.addWidget(b) self.setLayout(v) - save_button = b.button(QDialogButtonBox.Save) + save_button = b.button(QDialogButtonBox.StandardButton.Save) save_button.setDefault(True) save_button.setAutoDefault(True) qconnect(save_button.clicked, self.accept) - cancel_button = b.button(QDialogButtonBox.Cancel) + cancel_button = b.button(QDialogButtonBox.StandardButton.Cancel) cancel_button.setDefault(False) cancel_button.setAutoDefault(False) qconnect(cancel_button.clicked, self.reject) diff --git a/qt/aqt/stats.py b/qt/aqt/stats.py index 755a3d355..cd2b32d5d 100644 --- a/qt/aqt/stats.py +++ b/qt/aqt/stats.py @@ -25,7 +25,7 @@ class NewDeckStats(QDialog): """New deck stats.""" def __init__(self, mw: aqt.main.AnkiQt) -> None: - QDialog.__init__(self, mw, Qt.Window) + QDialog.__init__(self, mw, Qt.WindowType.Window) mw.garbage_collect_on_dialog_finish(self) self.mw = mw self.name = "deckStats" @@ -40,7 +40,9 @@ class NewDeckStats(QDialog): f.groupBox.setVisible(False) f.groupBox_2.setVisible(False) restoreGeom(self, self.name) - b = f.buttonBox.addButton(tr.statistics_save_pdf(), QDialogButtonBox.ActionRole) + b = f.buttonBox.addButton( + tr.statistics_save_pdf(), QDialogButtonBox.ButtonRole.ActionRole + ) qconnect(b.clicked, self.saveImage) b.setAutoDefault(False) maybeHideClose(self.form.buttonBox) @@ -104,7 +106,7 @@ class DeckStats(QDialog): """Legacy deck stats, used by some add-ons.""" def __init__(self, mw: aqt.main.AnkiQt) -> None: - QDialog.__init__(self, mw, Qt.Window) + QDialog.__init__(self, mw, Qt.WindowType.Window) mw.garbage_collect_on_dialog_finish(self) self.mw = mw self.name = "deckStats" @@ -123,7 +125,9 @@ class DeckStats(QDialog): self.setStyleSheet("QGroupBox { border: 0; }") f.setupUi(self) restoreGeom(self, self.name) - b = f.buttonBox.addButton(tr.statistics_save_pdf(), QDialogButtonBox.ActionRole) + b = f.buttonBox.addButton( + tr.statistics_save_pdf(), QDialogButtonBox.ButtonRole.ActionRole + ) qconnect(b.clicked, self.saveImage) b.setAutoDefault(False) qconnect(f.groups.clicked, lambda: self.changeScope("deck")) diff --git a/qt/aqt/studydeck.py b/qt/aqt/studydeck.py index 7403e6437..adc432ca1 100644 --- a/qt/aqt/studydeck.py +++ b/qt/aqt/studydeck.py @@ -49,18 +49,18 @@ class StudyDeck(QDialog): disable_help_button(self) if not cancel: self.form.buttonBox.removeButton( - self.form.buttonBox.button(QDialogButtonBox.Cancel) + self.form.buttonBox.button(QDialogButtonBox.StandardButton.Cancel) ) if buttons is not None: for button_or_label in buttons: self.form.buttonBox.addButton( - button_or_label, QDialogButtonBox.ActionRole + button_or_label, QDialogButtonBox.ButtonRole.ActionRole ) else: b = QPushButton(tr.actions_add()) b.setShortcut(QKeySequence("Ctrl+N")) b.setToolTip(shortcut(tr.decks_add_new_deck_ctrlandn())) - self.form.buttonBox.addButton(b, QDialogButtonBox.ActionRole) + self.form.buttonBox.addButton(b, QDialogButtonBox.ButtonRole.ActionRole) qconnect(b.clicked, self.onAddDeck) if title: self.setWindowTitle(title) @@ -78,9 +78,9 @@ class StudyDeck(QDialog): self.origNames = names() self.name: Optional[str] = None self.ok = self.form.buttonBox.addButton( - accept or tr.decks_study(), QDialogButtonBox.AcceptRole + accept or tr.decks_study(), QDialogButtonBox.ButtonRole.AcceptRole ) - self.setWindowModality(Qt.WindowModal) + self.setWindowModality(Qt.WindowModality.WindowModal) qconnect(self.form.buttonBox.helpRequested, lambda: openHelp(help)) qconnect(self.form.filter.textEdited, self.redraw) qconnect(self.form.list.itemDoubleClicked, self.accept) @@ -90,20 +90,20 @@ class StudyDeck(QDialog): self.exec() def eventFilter(self, obj: QObject, evt: QEvent) -> bool: - if isinstance(evt, QKeyEvent) and evt.type() == QEvent.KeyPress: + if isinstance(evt, QKeyEvent) and evt.type() == QEvent.Type.KeyPress: new_row = current_row = self.form.list.currentRow() rows_count = self.form.list.count() key = evt.key() - if key == Qt.Key_Up: + if key == Qt.Key.Key_Up: new_row = current_row - 1 - elif key == Qt.Key_Down: + elif key == Qt.Key.Key_Down: new_row = current_row + 1 elif ( - int(evt.modifiers()) & Qt.ControlModifier - and Qt.Key_1 <= key <= Qt.Key_9 + evt.modifiers() & Qt.KeyboardModifier.ControlModifier + and Qt.Key.Key_1 <= key <= Qt.Key.Key_9 ): - row_index = key - Qt.Key_1 + row_index = key - Qt.Key.Key_1 if row_index < rows_count: new_row = row_index @@ -126,7 +126,7 @@ class StudyDeck(QDialog): else: idx = 0 l.setCurrentRow(idx) - l.scrollToItem(l.item(idx), QAbstractItemView.PositionAtCenter) + l.scrollToItem(l.item(idx), QAbstractItemView.ScrollHint.PositionAtCenter) def _matches(self, name: str, filt: str) -> bool: name = name.lower() diff --git a/qt/aqt/switch.py b/qt/aqt/switch.py index f9ad05343..2d8a610e1 100644 --- a/qt/aqt/switch.py +++ b/qt/aqt/switch.py @@ -28,7 +28,7 @@ class Switch(QAbstractButton): super().__init__(parent=parent) self.setCheckable(True) super().setChecked(False) - self.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) + self.setSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Fixed) self._left_label = left_label self._right_label = right_label self._left_color = left_color @@ -76,8 +76,8 @@ class Switch(QAbstractButton): def paintEvent(self, _event: QPaintEvent) -> None: painter = QPainter(self) - painter.setRenderHint(QPainter.Antialiasing, True) - painter.setPen(Qt.NoPen) + painter.setRenderHint(QPainter.RenderHint.Antialiasing, True) + painter.setPen(Qt.PenStyle.NoPen) self._paint_path(painter) self._paint_knob(painter) self._paint_label(painter) @@ -113,15 +113,17 @@ class Switch(QAbstractButton): font = painter.font() font.setPixelSize(int(1.2 * self._knob_radius)) painter.setFont(font) - painter.drawText(self._current_knob_rectangle(), Qt.AlignCenter, self.label) + painter.drawText( + self._current_knob_rectangle(), Qt.AlignmentFlag.AlignCenter, self.label + ) def mouseReleaseEvent(self, event: QMouseEvent) -> None: super().mouseReleaseEvent(event) - if event.button() == Qt.LeftButton: + if event.button() == Qt.MouseButton.LeftButton: self._animate_toggle() - def enterEvent(self, event: QEvent) -> None: - self.setCursor(Qt.PointingHandCursor) + def enterEvent(self, event: QEnterEvent) -> None: + self.setCursor(Qt.CursorShape.PointingHandCursor) super().enterEvent(event) def toggle(self) -> None: diff --git a/qt/aqt/sync.py b/qt/aqt/sync.py index f84df9ce8..2da20d933 100644 --- a/qt/aqt/sync.py +++ b/qt/aqt/sync.py @@ -292,7 +292,7 @@ def get_id_and_pass_from_user( diag = QDialog(mw) diag.setWindowTitle("Anki") disable_help_button(diag) - diag.setWindowModality(Qt.WindowModal) + diag.setWindowModality(Qt.WindowModality.WindowModal) vbox = QVBoxLayout() info_label = QLabel( without_unicode_isolation( @@ -313,11 +313,11 @@ def get_id_and_pass_from_user( g.addWidget(l2, 1, 0) passwd = QLineEdit() passwd.setText(password) - passwd.setEchoMode(QLineEdit.Password) + passwd.setEchoMode(QLineEdit.EchoMode.Password) g.addWidget(passwd, 1, 1) vbox.addLayout(g) - bb = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel) # type: ignore - bb.button(QDialogButtonBox.Ok).setAutoDefault(True) + bb = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel) # type: ignore + bb.button(QDialogButtonBox.StandardButton.Ok).setAutoDefault(True) qconnect(bb.accepted, diag.accept) qconnect(bb.rejected, diag.reject) vbox.addWidget(bb) diff --git a/qt/aqt/tagedit.py b/qt/aqt/tagedit.py index 69c31d320..90cbf56ed 100644 --- a/qt/aqt/tagedit.py +++ b/qt/aqt/tagedit.py @@ -26,9 +26,9 @@ class TagEdit(QLineEdit): self._completer = TagCompleter(self.model, parent, self) else: self._completer = QCompleter(self.model, parent) - self._completer.setCompletionMode(QCompleter.PopupCompletion) - self._completer.setCaseSensitivity(Qt.CaseInsensitive) - self._completer.setFilterMode(Qt.MatchContains) + self._completer.setCompletionMode(QCompleter.CompletionMode.PopupCompletion) + self._completer.setCaseSensitivity(Qt.CaseSensitivity.CaseInsensitive) + self._completer.setFilterMode(Qt.MatchFlag.MatchContains) self.setCompleter(self._completer) def setCol(self, col: Collection) -> None: @@ -45,12 +45,15 @@ class TagEdit(QLineEdit): QLineEdit.focusInEvent(self, evt) def keyPressEvent(self, evt: QKeyEvent) -> None: - if evt.key() in (Qt.Key_Up, Qt.Key_Down): + if evt.key() in (Qt.Key.Key_Up, Qt.Key.Key_Down): # show completer on arrow key up/down if not self._completer.popup().isVisible(): self.showCompleter() return - if evt.key() == Qt.Key_Tab and int(evt.modifiers()) & Qt.ControlModifier: + if ( + evt.key() == Qt.Key.Key_Tab + and evt.modifiers() & Qt.KeyboardModifier.ControlModifier + ): # select next completion if not self._completer.popup().isVisible(): self.showCompleter() @@ -61,7 +64,7 @@ class TagEdit(QLineEdit): self._completer.setCurrentRow(0) return if ( - evt.key() in (Qt.Key_Enter, Qt.Key_Return) + evt.key() in (Qt.Key.Key_Enter, Qt.Key.Key_Return) and self._completer.popup().isVisible() ): # apply first completion if no suggestion selected @@ -78,13 +81,13 @@ class TagEdit(QLineEdit): # if it's a modifier, don't show return if evt.key() not in ( - Qt.Key_Enter, - Qt.Key_Return, - Qt.Key_Escape, - Qt.Key_Space, - Qt.Key_Tab, - Qt.Key_Backspace, - Qt.Key_Delete, + Qt.Key.Key_Enter, + Qt.Key.Key_Return, + Qt.Key.Key_Escape, + Qt.Key.Key_Space, + Qt.Key.Key_Tab, + Qt.Key.Key_Backspace, + Qt.Key.Key_Delete, ): self.showCompleter() gui_hooks.tag_editor_did_process_key(self, evt) diff --git a/qt/aqt/taglimit.py b/qt/aqt/taglimit.py index 1affd4550..0a04ef3ad 100644 --- a/qt/aqt/taglimit.py +++ b/qt/aqt/taglimit.py @@ -13,7 +13,7 @@ from aqt.utils import disable_help_button, restoreGeom, saveGeom, showWarning, t class TagLimit(QDialog): def __init__(self, mw: AnkiQt, parent: CustomStudy) -> None: - QDialog.__init__(self, parent, Qt.Window) + QDialog.__init__(self, parent, Qt.WindowType.Window) self.tags: str = "" self.tags_list: list[str] = [] self.mw = mw @@ -23,11 +23,15 @@ class TagLimit(QDialog): self.dialog.setupUi(self) disable_help_button(self) s = QShortcut( - QKeySequence("ctrl+d"), self.dialog.activeList, context=Qt.WidgetShortcut + QKeySequence("ctrl+d"), + self.dialog.activeList, + context=Qt.ShortcutContext.WidgetShortcut, ) qconnect(s.activated, self.dialog.activeList.clearSelection) s = QShortcut( - QKeySequence("ctrl+d"), self.dialog.inactiveList, context=Qt.WidgetShortcut + QKeySequence("ctrl+d"), + self.dialog.inactiveList, + context=Qt.ShortcutContext.WidgetShortcut, ) qconnect(s.activated, self.dialog.inactiveList.clearSelection) self.rebuildTagList() @@ -54,19 +58,19 @@ class TagLimit(QDialog): item = QListWidgetItem(t.replace("_", " ")) self.dialog.activeList.addItem(item) if t in yesHash: - mode = QItemSelectionModel.Select + mode = QItemSelectionModel.SelectionFlag.Select self.dialog.activeCheck.setChecked(True) else: - mode = QItemSelectionModel.Deselect + mode = QItemSelectionModel.SelectionFlag.Deselect idx = self.dialog.activeList.indexFromItem(item) self.dialog.activeList.selectionModel().select(idx, mode) # inactive item = QListWidgetItem(t.replace("_", " ")) self.dialog.inactiveList.addItem(item) if t in noHash: - mode = QItemSelectionModel.Select + mode = QItemSelectionModel.SelectionFlag.Select else: - mode = QItemSelectionModel.Deselect + mode = QItemSelectionModel.SelectionFlag.Deselect idx = self.dialog.inactiveList.indexFromItem(item) self.dialog.inactiveList.selectionModel().select(idx, mode) diff --git a/qt/aqt/theme.py b/qt/aqt/theme.py index 5148b8228..f95799d50 100644 --- a/qt/aqt/theme.py +++ b/qt/aqt/theme.py @@ -92,7 +92,9 @@ class ThemeManager: icon = QIcon(path.path) pixmap = icon.pixmap(16) painter = QPainter(pixmap) - painter.setCompositionMode(QPainter.CompositionMode_SourceIn) + painter.setCompositionMode( + QPainter.CompositionMode.CompositionMode_SourceIn + ) painter.fillRect(pixmap.rect(), QColor(path.current_color(self.night_mode))) painter.end() icon = QIcon(pixmap) @@ -207,34 +209,44 @@ QTabWidget {{ background-color: {}; }} palette = QPalette() text_fg = self.qcolor(colors.TEXT_FG) - palette.setColor(QPalette.WindowText, text_fg) - palette.setColor(QPalette.ToolTipText, text_fg) - palette.setColor(QPalette.Text, text_fg) - palette.setColor(QPalette.ButtonText, text_fg) + palette.setColor(QPalette.ColorRole.WindowText, text_fg) + palette.setColor(QPalette.ColorRole.ToolTipText, text_fg) + palette.setColor(QPalette.ColorRole.Text, text_fg) + palette.setColor(QPalette.ColorRole.ButtonText, text_fg) hlbg = self.qcolor(colors.HIGHLIGHT_BG) hlbg.setAlpha(64) - palette.setColor(QPalette.HighlightedText, self.qcolor(colors.HIGHLIGHT_FG)) - palette.setColor(QPalette.Highlight, hlbg) + palette.setColor( + QPalette.ColorRole.HighlightedText, self.qcolor(colors.HIGHLIGHT_FG) + ) + palette.setColor(QPalette.ColorRole.Highlight, hlbg) window_bg = self.qcolor(colors.WINDOW_BG) - palette.setColor(QPalette.Window, window_bg) - palette.setColor(QPalette.AlternateBase, window_bg) + palette.setColor(QPalette.ColorRole.Window, window_bg) + palette.setColor(QPalette.ColorRole.AlternateBase, window_bg) - palette.setColor(QPalette.Button, QColor("#454545")) + palette.setColor(QPalette.ColorRole.Button, QColor("#454545")) frame_bg = self.qcolor(colors.FRAME_BG) - palette.setColor(QPalette.Base, frame_bg) - palette.setColor(QPalette.ToolTipBase, frame_bg) + palette.setColor(QPalette.ColorRole.Base, frame_bg) + palette.setColor(QPalette.ColorRole.ToolTipBase, frame_bg) disabled_color = self.qcolor(colors.DISABLED) - palette.setColor(QPalette.Disabled, QPalette.Text, disabled_color) - palette.setColor(QPalette.Disabled, QPalette.ButtonText, disabled_color) - palette.setColor(QPalette.Disabled, QPalette.HighlightedText, disabled_color) + palette.setColor( + QPalette.ColorGroup.Disabled, QPalette.ColorRole.Text, disabled_color + ) + palette.setColor( + QPalette.ColorGroup.Disabled, QPalette.ColorRole.ButtonText, disabled_color + ) + palette.setColor( + QPalette.ColorGroup.Disabled, + QPalette.ColorRole.HighlightedText, + disabled_color, + ) - palette.setColor(QPalette.Link, self.qcolor(colors.LINK)) + palette.setColor(QPalette.ColorRole.Link, self.qcolor(colors.LINK)) - palette.setColor(QPalette.BrightText, Qt.red) + palette.setColor(QPalette.ColorRole.BrightText, Qt.GlobalColor.red) app.setPalette(palette) diff --git a/qt/aqt/update.py b/qt/aqt/update.py index 912c0beae..4b5a00fbd 100644 --- a/qt/aqt/update.py +++ b/qt/aqt/update.py @@ -59,17 +59,17 @@ class LatestVersionFinder(QThread): def askAndUpdate(mw: aqt.AnkiQt, ver: str) -> None: baseStr = tr.qt_misc_anki_updatedanki_has_been_released(val=ver) msg = QMessageBox(mw) - msg.setStandardButtons(QMessageBox.Yes | QMessageBox.No) # type: ignore - msg.setIcon(QMessageBox.Information) + msg.setStandardButtons(QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No) # type: ignore + msg.setIcon(QMessageBox.Icon.Information) msg.setText(baseStr + tr.qt_misc_would_you_like_to_download_it()) button = QPushButton(tr.qt_misc_ignore_this_update()) - msg.addButton(button, QMessageBox.RejectRole) - msg.setDefaultButton(QMessageBox.Yes) + msg.addButton(button, QMessageBox.ButtonRole.RejectRole) + msg.setDefaultButton(QMessageBox.StandardButton.Yes) ret = msg.exec() if msg.clickedButton() == button: # ignore this update mw.pm.meta["suppressUpdate"] = ver - elif ret == QMessageBox.Yes: + elif ret == QMessageBox.StandardButton.Yes: openLink(aqt.appWebsite) diff --git a/qt/aqt/utils.py b/qt/aqt/utils.py index 1aa282820..3d8dd4120 100644 --- a/qt/aqt/utils.py +++ b/qt/aqt/utils.py @@ -7,7 +7,7 @@ import re import subprocess import sys from functools import wraps -from typing import TYPE_CHECKING, Any, Literal, Sequence, cast +from typing import TYPE_CHECKING, Any, Literal, Sequence import aqt from anki.collection import Collection, HelpPage @@ -96,16 +96,16 @@ def showInfo( else: parent_widget = parent if type == "warning": - icon = QMessageBox.Warning + icon = QMessageBox.Icon.Warning elif type == "critical": - icon = QMessageBox.Critical + icon = QMessageBox.Icon.Critical else: - icon = QMessageBox.Information + icon = QMessageBox.Icon.Information mb = QMessageBox(parent_widget) # if textFormat == "plain": - mb.setTextFormat(Qt.PlainText) + mb.setTextFormat(Qt.TextFormat.PlainText) elif textFormat == "rich": - mb.setTextFormat(Qt.RichText) + mb.setTextFormat(Qt.TextFormat.RichText) elif textFormat is not None: raise Exception("unexpected textFormat type") mb.setText(text) @@ -119,10 +119,10 @@ def showInfo( default = b mb.setDefaultButton(default) else: - b = mb.addButton(QMessageBox.Ok) + b = mb.addButton(QMessageBox.StandardButton.Ok) b.setDefault(True) if help: - b = mb.addButton(QMessageBox.Help) + b = mb.addButton(QMessageBox.StandardButton.Help) qconnect(b.clicked, lambda: openHelp(help)) b.setAutoDefault(False) return mb.exec() @@ -152,7 +152,7 @@ def showText( # used by the importer text = QPlainTextEdit() text.setReadOnly(True) - text.setWordWrapMode(QTextOption.NoWrap) + text.setWordWrapMode(QTextOption.WrapMode.NoWrap) text.setPlainText(txt) else: text = QTextBrowser() @@ -162,7 +162,7 @@ def showText( else: text.setHtml(txt) layout.addWidget(text) - box = QDialogButtonBox(QDialogButtonBox.Close) + box = QDialogButtonBox(QDialogButtonBox.StandardButton.Close) layout.addWidget(box) if copyBtn: @@ -171,7 +171,7 @@ def showText( btn = QPushButton(tr.qt_misc_copy_to_clipboard()) qconnect(btn.clicked, onCopy) - box.addButton(btn, QDialogButtonBox.ActionRole) + box.addButton(btn, QDialogButtonBox.ButtonRole.ActionRole) def onReject() -> None: if geomKey: @@ -209,20 +209,20 @@ def askUser( parent = aqt.mw.app.activeWindow() if not msgfunc: msgfunc = QMessageBox.question - sb = QMessageBox.Yes | QMessageBox.No + sb = QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No if help: - sb |= QMessageBox.Help + sb |= QMessageBox.StandardButton.Help while 1: if defaultno: - default = QMessageBox.No + default = QMessageBox.StandardButton.No else: - default = QMessageBox.Yes - r = msgfunc(parent, title, text, cast(QMessageBox.StandardButtons, sb), default) - if r == QMessageBox.Help: + default = QMessageBox.StandardButton.Yes + r = msgfunc(parent, title, text, sb, default) + if r == QMessageBox.StandardButton.Help: openHelp(help) else: break - return r == QMessageBox.Yes + return r == QMessageBox.StandardButton.Yes class ButtonedDialog(QMessageBox): @@ -238,12 +238,12 @@ class ButtonedDialog(QMessageBox): self._buttons: list[QPushButton] = [] self.setWindowTitle(title) self.help = help - self.setIcon(QMessageBox.Warning) + self.setIcon(QMessageBox.Icon.Warning) self.setText(text) for b in buttons: - self._buttons.append(self.addButton(b, QMessageBox.AcceptRole)) + self._buttons.append(self.addButton(b, QMessageBox.ButtonRole.AcceptRole)) if help: - self.addButton(tr.actions_help(), QMessageBox.HelpRole) + self.addButton(tr.actions_help(), QMessageBox.ButtonRole.HelpRole) buttons.append(tr.actions_help()) def run(self) -> str: @@ -300,16 +300,21 @@ class GetTextDialog(QDialog): self.l.setText(default) self.l.selectAll() v.addWidget(self.l) - buts = QDialogButtonBox.Ok | QDialogButtonBox.Cancel + buts = ( + QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel + ) if help: - buts |= QDialogButtonBox.Help + buts |= QDialogButtonBox.StandardButton.Help b = QDialogButtonBox(buts) # type: ignore v.addWidget(b) self.setLayout(v) - qconnect(b.button(QDialogButtonBox.Ok).clicked, self.accept) - qconnect(b.button(QDialogButtonBox.Cancel).clicked, self.reject) + qconnect(b.button(QDialogButtonBox.StandardButton.Ok).clicked, self.accept) + qconnect(b.button(QDialogButtonBox.StandardButton.Cancel).clicked, self.reject) if help: - qconnect(b.button(QDialogButtonBox.Help).clicked, self.helpRequested) + qconnect( + b.button(QDialogButtonBox.StandardButton.Help).clicked, + self.helpRequested, + ) def accept(self) -> None: return QDialog.accept(self) @@ -337,7 +342,7 @@ def getText( d = GetTextDialog( parent, prompt, help=help, edit=edit, default=default, title=title, **kwargs ) - d.setWindowModality(Qt.WindowModal) + d.setWindowModality(Qt.WindowModality.WindowModal) if geomKey: restoreGeom(d, geomKey) ret = d.exec() @@ -363,7 +368,7 @@ def chooseList( parent = aqt.mw.app.activeWindow() d = QDialog(parent) disable_help_button(d) - d.setWindowModality(Qt.WindowModal) + d.setWindowModality(Qt.WindowModality.WindowModal) l = QVBoxLayout() d.setLayout(l) t = QLabel(prompt) @@ -372,7 +377,7 @@ def chooseList( c.addItems(choices) c.setCurrentRow(startrow) l.addWidget(c) - bb = QDialogButtonBox(QDialogButtonBox.Ok) + bb = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok) qconnect(bb.accepted, d.accept) l.addWidget(bb) d.exec() @@ -393,9 +398,9 @@ def getTag( def disable_help_button(widget: QWidget) -> None: "Disable the help button in the window titlebar." - flags_int = int(widget.windowFlags()) & ~Qt.WindowContextHelpButtonHint - flags = Qt.WindowFlags(flags_int) # type: ignore - widget.setWindowFlags(flags) + widget.setWindowFlags( + widget.windowFlags() & ~Qt.WindowType.WindowContextHelpButtonHint + ) # File handling @@ -420,7 +425,11 @@ def getFile( else: dirkey = None d = QFileDialog(parent) - mode = QFileDialog.ExistingFiles if multi else QFileDialog.ExistingFile + mode = ( + QFileDialog.FileMode.ExistingFiles + if multi + else QFileDialog.FileMode.ExistingFile + ) d.setFileMode(mode) if os.path.exists(dir): d.setDirectory(dir) @@ -459,7 +468,9 @@ def getSaveFile( variable. The file dialog will default to open with FNAME.""" config_key = f"{dir_description}Directory" - defaultPath = QStandardPaths.writableLocation(QStandardPaths.DocumentsLocation) + defaultPath = QStandardPaths.writableLocation( + QStandardPaths.StandardLocation.DocumentsLocation + ) base = aqt.mw.pm.profile.get(config_key, defaultPath) path = os.path.join(base, fname) file = QFileDialog.getSaveFileName( @@ -467,7 +478,7 @@ def getSaveFile( title, path, f"{key} (*{ext})", - options=QFileDialog.DontConfirmOverwrite, + options=QFileDialog.Option.DontConfirmOverwrite, )[0] if file: # add extension @@ -485,7 +496,7 @@ def getSaveFile( def saveGeom(widget: QWidget, key: str) -> None: key += "Geom" - if isMac and int(widget.windowState()) & Qt.WindowFullScreen: + if isMac and (widget.windowState() & Qt.WindowState.WindowFullScreen): geom = None else: geom = widget.saveGeometry() @@ -646,7 +657,7 @@ def shortcut(key: str) -> str: def maybeHideClose(bbox: QDialogButtonBox) -> None: if isMac: - b = bbox.button(QDialogButtonBox.Close) + b = bbox.button(QDialogButtonBox.StandardButton.Close) if b: bbox.removeButton(b) @@ -706,13 +717,13 @@ def tooltip( """, aw, ) - lab.setFrameStyle(QFrame.Panel) + lab.setFrameStyle(QFrame.Shape.Panel) lab.setLineWidth(2) - lab.setWindowFlags(Qt.ToolTip) + lab.setWindowFlags(Qt.WindowType.ToolTip) if not theme_manager.night_mode: p = QPalette() - p.setColor(QPalette.Window, QColor("#feffc4")) - p.setColor(QPalette.WindowText, QColor("#000000")) + p.setColor(QPalette.ColorRole.Window, QColor("#feffc4")) + p.setColor(QPalette.ColorRole.WindowText, QColor("#000000")) lab.setPalette(p) lab.move(aw.mapToGlobal(QPoint(0 + x_offset, aw.height() - y_offset))) lab.show() @@ -888,7 +899,7 @@ def opengl_vendor() -> str | None: # Can't use versionFunctions there return None - vp = QOpenGLVersionProfile() # type: ignore + vp = QOpenGLVersionProfile() # type: ignore # pylint: disable=undefined-variable vp.setVersion(2, 0) try: @@ -969,16 +980,16 @@ class KeyboardModifiersPressed: def __init__(self) -> None: from aqt import mw - self._modifiers = int(mw.app.keyboardModifiers()) + self._modifiers = mw.app.keyboardModifiers() @property def shift(self) -> bool: - return bool(self._modifiers & Qt.ShiftModifier) + return bool(self._modifiers & Qt.KeyboardModifier.ShiftModifier) @property def control(self) -> bool: - return bool(self._modifiers & Qt.ControlModifier) + return bool(self._modifiers & Qt.KeyboardModifier.ControlModifier) @property def alt(self) -> bool: - return bool(self._modifiers & Qt.AltModifier) + return bool(self._modifiers & Qt.KeyboardModifier.AltModifier) diff --git a/qt/aqt/webview.py b/qt/aqt/webview.py index 9b7399489..72c0cb976 100644 --- a/qt/aqt/webview.py +++ b/qt/aqt/webview.py @@ -48,7 +48,7 @@ class AnkiWebPage(QWebEnginePage): qwebchannel = ":/qtwebchannel/qwebchannel.js" jsfile = QFile(qwebchannel) - if not jsfile.open(QIODevice.ReadOnly): + if not jsfile.open(QIODevice.OpenModeFlag.ReadOnly): print(f"Error opening '{qwebchannel}': {jsfile.error()}", file=sys.stderr) jstext = bytes(cast(bytes, jsfile.readAll())).decode("utf-8") jsfile.close() @@ -74,8 +74,8 @@ class AnkiWebPage(QWebEnginePage): }); """ ) - script.setWorldId(QWebEngineScript.MainWorld) - script.setInjectionPoint(QWebEngineScript.DocumentReady) + script.setWorldId(QWebEngineScript.ScriptWorldId.MainWorld) + script.setInjectionPoint(QWebEngineScript.InjectionPoint.DocumentReady) script.setRunsOnSubFrames(False) self.profile().scripts().insert(script) @@ -92,11 +92,11 @@ class AnkiWebPage(QWebEnginePage): srcID = "" else: srcID = serverbaseurl.sub("", srcID[:80], 1) - if level == QWebEnginePage.InfoMessageLevel: + if level == QWebEnginePage.JavaScriptConsoleMessageLevel.InfoMessageLevel: level_str = "info" - elif level == QWebEnginePage.WarningMessageLevel: + elif level == QWebEnginePage.JavaScriptConsoleMessageLevel.WarningMessageLevel: level_str = "warning" - elif level == QWebEnginePage.ErrorMessageLevel: + elif level == QWebEnginePage.JavaScriptConsoleMessageLevel.ErrorMessageLevel: level_str = "error" else: level_str = str(level) @@ -135,7 +135,9 @@ class AnkiWebPage(QWebEnginePage): # catch buggy links from aqt import mw - if url.matches(QUrl(mw.serverURL()), cast(Any, QUrl.RemoveFragment)): + if url.matches( + QUrl(mw.serverURL()), cast(Any, QUrl.UrlFormattingOption.RemoveFragment) + ): print("onclick handler needs to return false") return False # load all other links in browser @@ -240,14 +242,14 @@ class AnkiWebView(QWebEngineView): self.requiresCol = True self.setPage(self._page) - self._page.profile().setHttpCacheType(QWebEngineProfile.NoCache) + self._page.profile().setHttpCacheType(QWebEngineProfile.HttpCacheType.NoCache) self.resetHandlers() self.allowDrops = False self._filterSet = False QShortcut( # type: ignore QKeySequence("Esc"), self, - context=Qt.WidgetWithChildrenShortcut, + context=Qt.ShortcutContext.WidgetWithChildrenShortcut, activated=self.onEsc, ) @@ -258,8 +260,11 @@ class AnkiWebView(QWebEngineView): # disable pinch to zoom gesture if isinstance(evt, QNativeGestureEvent): return True - elif isinstance(evt, QMouseEvent) and evt.type() == QEvent.MouseButtonRelease: - if evt.button() == Qt.MidButton and isLin: + elif ( + isinstance(evt, QMouseEvent) + and evt.type() == QEvent.Type.MouseButtonRelease + ): + if evt.button() == Qt.MouseButton.MiddleButton and isLin: self.onMiddleClickPaste() return True return False @@ -286,19 +291,19 @@ class AnkiWebView(QWebEngineView): w = w.parent() def onCopy(self) -> None: - self.triggerPageAction(QWebEnginePage.Copy) + self.triggerPageAction(QWebEnginePage.WebAction.Copy) def onCut(self) -> None: - self.triggerPageAction(QWebEnginePage.Cut) + self.triggerPageAction(QWebEnginePage.WebAction.Cut) def onPaste(self) -> None: - self.triggerPageAction(QWebEnginePage.Paste) + self.triggerPageAction(QWebEnginePage.WebAction.Paste) def onMiddleClickPaste(self) -> None: - self.triggerPageAction(QWebEnginePage.Paste) + self.triggerPageAction(QWebEnginePage.WebAction.Paste) def onSelectAll(self) -> None: - self.triggerPageAction(QWebEnginePage.SelectAll) + self.triggerPageAction(QWebEnginePage.WebAction.SelectAll) def contextMenuEvent(self, evt: QContextMenuEvent) -> None: m = QMenu(self) @@ -386,11 +391,11 @@ class AnkiWebView(QWebEngineView): # standard palette does not return correct window color on macOS return QColor("#ececec") else: - return theme_manager.default_palette.color(QPalette.Window) + return theme_manager.default_palette.color(QPalette.ColorRole.Window) def standard_css(self) -> str: palette = theme_manager.default_palette - color_hl = palette.color(QPalette.Highlight).name() + color_hl = palette.color(QPalette.ColorRole.Highlight).name() if isWin: # T: include a font for your language on Windows, eg: "Segoe UI", "MS Mincho" @@ -406,8 +411,8 @@ button { -webkit-appearance: none; background: #fff; border: 1px solid #ccc; border-radius:5px; font-family: Helvetica }""" else: family = self.font().family() - color_hl_txt = palette.color(QPalette.HighlightedText).name() - color_btn = palette.color(QPalette.Button).name() + color_hl_txt = palette.color(QPalette.ColorRole.HighlightedText).name() + color_btn = palette.color(QPalette.ColorRole.Button).name() font = f'font-size:14px;font-family:"{family}";' button_style = """ /* Buttons */ diff --git a/qt/dmypy.py b/qt/dmypy.py index 968061146..f8c420c35 100755 --- a/qt/dmypy.py +++ b/qt/dmypy.py @@ -36,7 +36,7 @@ if subprocess.run( os.path.abspath("pip/stubs/extendsitepkgs"), ], env={ - "MYPYPATH": "bazel-bin/qt/dmypy.runfiles/pyqt5", + "MYPYPATH": "bazel-bin/qt/dmypy.runfiles/pyqt6", "EXTRA_SITE_PACKAGES": os.path.abspath(os.getenv("EXTRA_SITE_PACKAGES")), }, cwd=workspace, diff --git a/qt/mypy.ini b/qt/mypy.ini index c8dcf12e9..cff9c7490 100644 --- a/qt/mypy.ini +++ b/qt/mypy.ini @@ -54,6 +54,7 @@ ignore_missing_imports = True ignore_missing_imports = True [mypy-PyQt5.*] ignore_errors = True +ignore_missing_imports = True [mypy-win32com.client] ignore_missing_imports = True [mypy-darkdetect] From 36fdc607c674fc2d41b3a20ed77d1b42aa24aac5 Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Tue, 5 Oct 2021 14:05:53 +1000 Subject: [PATCH 07/16] add run-qt5 script to run with 5.x --- qt/BUILD.bazel | 14 ++++++++++++++ qt/aqt/BUILD.bazel | 19 ++++++++++++++++--- run-qt5 | 24 ++++++++++++++++++++++++ 3 files changed, 54 insertions(+), 3 deletions(-) create mode 100755 run-qt5 diff --git a/qt/BUILD.bazel b/qt/BUILD.bazel index 93d5473b3..c07d8cec7 100644 --- a/qt/BUILD.bazel +++ b/qt/BUILD.bazel @@ -133,6 +133,20 @@ py_binary( ], ) +py_binary( + name = "runanki_qt5", + srcs = [ + "bazelfixes.py", + "runanki.py", + ], + imports = ["."], + main = "runanki.py", + deps = [ + "//pylib/anki", + "//qt/aqt:aqt_with_data_qt5", + ], +) + py_binary( name = "dmypy", srcs = [ diff --git a/qt/aqt/BUILD.bazel b/qt/aqt/BUILD.bazel index ad816a68b..788cae435 100644 --- a/qt/aqt/BUILD.bazel +++ b/qt/aqt/BUILD.bazel @@ -50,7 +50,6 @@ aqt_deps = [ requirement("waitress"), requirement("send2trash"), requirement("jsonschema"), - "@pyqt6//:pkg", ] + select({ "@bazel_tools//src/conditions:host_windows": [ requirement("psutil"), @@ -66,7 +65,9 @@ py_library( srcs = _py_srcs_and_forms, data = aqt_core_data, visibility = ["//visibility:public"], - deps = aqt_deps, + deps = aqt_deps + [ + "@pyqt6//:pkg", + ], ) py_library( @@ -74,7 +75,19 @@ py_library( srcs = _py_srcs_and_forms, data = aqt_core_data + ["//qt/aqt/data"], visibility = ["//visibility:public"], - deps = aqt_deps, + deps = aqt_deps + [ + "@pyqt6//:pkg", + ], +) + +py_library( + name = "aqt_with_data_qt5", + srcs = _py_srcs_and_forms, + data = aqt_core_data + ["//qt/aqt/data"], + visibility = ["//visibility:public"], + deps = aqt_deps + [ + "@pyqt5//:pkg", + ], ) py_package( diff --git a/run-qt5 b/run-qt5 new file mode 100755 index 000000000..9fd6ef056 --- /dev/null +++ b/run-qt5 @@ -0,0 +1,24 @@ +#!/bin/bash + +set -e + +run_linux() { + bazel run $BUILDARGS //qt:runanki_qt5 -- $* +} + +run_mac() { + # QtWebEngineProcess is unable to locate icudtl.dat from a symlinked tree, + # so we need to copy the files into a working folder before running on a Mac. + workspace=$(dirname $0) + bazel build $BUILDARGS //qt:runanki_qt5 && \ + rsync -aiL --exclude=anki/external --delete -f'-p __pycache__' \ + $workspace/bazel-bin/qt/runanki* $workspace/bazel-copy/ && \ + $workspace/bazel-copy/runanki_qt5 $* +} + +export PYTHONWARNINGS=default +if [[ "$OSTYPE" == "darwin"* ]]; then + run_mac $* +else + run_linux $* +fi From 0d246c9e0be164b73c6f972a1d1b9ffa8d5b5810 Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Fri, 15 Oct 2021 12:54:59 +1000 Subject: [PATCH 08/16] update wheel definitions to require Python 3.9; make PyQt optional While we do require PyQt, it's not possible to declare that we require either 5 or 6, and so we need to mark it as optional. Instead, we provide optional dependencies, so the user can e.g. 'pip install aqt[qt6]' --- pylib/anki/BUILD.bazel | 2 +- qt/aqt/BUILD.bazel | 17 ++++++++++++----- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/pylib/anki/BUILD.bazel b/pylib/anki/BUILD.bazel index f0c3c0e1f..d98080060 100644 --- a/pylib/anki/BUILD.bazel +++ b/pylib/anki/BUILD.bazel @@ -76,7 +76,7 @@ py_wheel( "//platforms:linux_x86_64": "manylinux2014_x86_64", "//platforms:linux_arm64": "manylinux2014_aarch64", }), - python_tag = "cp38", + python_tag = "cp39", python_version = ">=3.9", requires = [ "beautifulsoup4", diff --git a/qt/aqt/BUILD.bazel b/qt/aqt/BUILD.bazel index 788cae435..778db0ede 100644 --- a/qt/aqt/BUILD.bazel +++ b/qt/aqt/BUILD.bazel @@ -107,10 +107,20 @@ py_wheel( entry_points = { "console_scripts": ["anki = aqt:run"], }, + extra_requires = { + "qt5": [ + "pyqt5>=5.14", + "pyqtwebengine", + ], + "qt6": [ + "pyqt6>=6.2", + "pyqt6-webengine>=6.2", + ], + }, homepage = "https://apps.ankiweb.net", license = "License :: OSI Approved :: GNU Affero General Public License v3 or later (AGPLv3+)", python_tag = "py3", - python_version = ">=3.8", + python_version = ">=3.9", requires = [ "beautifulsoup4", "requests", @@ -119,12 +129,9 @@ py_wheel( "flask", "flask_cors", "waitress", - "pyqt5>=5.12", - "pyqtwebengine", 'psutil; sys.platform == "win32"', 'pywin32; sys.platform == "win32"', - 'winrt==1.0.20239.1; sys.platform == "win32" and platform_release == "10" and python_version == "3.8"', - 'winrt; sys.platform == "win32" and platform_release == "10" and python_version >= "3.9"', + 'winrt; sys.platform == "win32"', "anki==" + anki_version, ], strip_path_prefixes = [ From 58d17a51f3b8b045935acaf978c73e2d7f748049 Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Thu, 7 Oct 2021 16:26:29 +1000 Subject: [PATCH 09/16] fix incorrect web background color being picked up in qt6+win/lin --- qt/aqt/theme.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/qt/aqt/theme.py b/qt/aqt/theme.py index f95799d50..5d43cb9b2 100644 --- a/qt/aqt/theme.py +++ b/qt/aqt/theme.py @@ -9,7 +9,16 @@ from dataclasses import dataclass from anki.utils import isMac from aqt import QApplication, colors, gui_hooks, isWin from aqt.platform import set_dark_mode -from aqt.qt import QColor, QIcon, QPainter, QPalette, QPixmap, QStyleFactory, Qt +from aqt.qt import ( + QColor, + QGuiApplication, + QIcon, + QPainter, + QPalette, + QPixmap, + QStyleFactory, + Qt, +) @dataclass @@ -135,7 +144,7 @@ class ThemeManager: return QColor(self.color(colors)) def apply_style(self, app: QApplication) -> None: - self.default_palette = app.style().standardPalette() + self.default_palette = QGuiApplication.palette() self._apply_palette(app) self._apply_style(app) From e2c8dc92c1782ab0389565fd8df8f5168aeb1751 Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Thu, 7 Oct 2021 16:43:35 +1000 Subject: [PATCH 10/16] fix placeholder text color on win/lin --- qt/aqt/theme.py | 1 + 1 file changed, 1 insertion(+) diff --git a/qt/aqt/theme.py b/qt/aqt/theme.py index 5d43cb9b2..7fde423aa 100644 --- a/qt/aqt/theme.py +++ b/qt/aqt/theme.py @@ -241,6 +241,7 @@ QTabWidget {{ background-color: {}; }} palette.setColor(QPalette.ColorRole.ToolTipBase, frame_bg) disabled_color = self.qcolor(colors.DISABLED) + palette.setColor(QPalette.ColorRole.PlaceholderText, disabled_color) palette.setColor( QPalette.ColorGroup.Disabled, QPalette.ColorRole.Text, disabled_color ) From 2e9b4b34548b4ca163c48c0522dcea27d328e492 Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Thu, 7 Oct 2021 17:51:00 +1000 Subject: [PATCH 11/16] hide the video driver selection in qt6 --- qt/aqt/preferences.py | 1 + 1 file changed, 1 insertion(+) diff --git a/qt/aqt/preferences.py b/qt/aqt/preferences.py index 1ebb87e4a..c0cce5612 100644 --- a/qt/aqt/preferences.py +++ b/qt/aqt/preferences.py @@ -307,6 +307,7 @@ for you than the default driver, please let us know on the Anki forums.""" self.form.video_driver.setCurrentIndex( self.video_drivers.index(self.mw.pm.video_driver()) ) + self.form.video_driver.setVisible(qtmajor == 5) def update_video_driver(self) -> None: new_driver = self.video_drivers[self.form.video_driver.currentIndex()] From 7962c8107f6e04cb485f0d92efe50288194c3ab3 Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Fri, 8 Oct 2021 15:55:38 +1000 Subject: [PATCH 12/16] qt recording support for qt6 + fix inefficient bytes concatenation --- qt/aqt/qt.py | 3 +- qt/aqt/qt5.py | 6 ++-- qt/aqt/sound.py | 87 +++++++++++++++++++++++++++++++++++++++++++++---- 3 files changed, 85 insertions(+), 11 deletions(-) diff --git a/qt/aqt/qt.py b/qt/aqt/qt.py index 541a3fb8c..ffc6f6512 100644 --- a/qt/aqt/qt.py +++ b/qt/aqt/qt.py @@ -74,6 +74,5 @@ if qtmajor < 5 or (qtmajor == 5 and qtminor < 14): def qconnect( signal: Union[Callable, pyqtSignal, pyqtBoundSignal], func: Callable ) -> None: - """Helper to work around type checking not working with signal.connect(func). - Not needed in PyQt6""" + """Helper to work around type checking not working with signal.connect(func).""" signal.connect(func) # type: ignore diff --git a/qt/aqt/qt5.py b/qt/aqt/qt5.py index 927eccbf1..6c1e68f63 100644 --- a/qt/aqt/qt5.py +++ b/qt/aqt/qt5.py @@ -52,12 +52,12 @@ class QtAudioInputRecorder(Recorder): def start(self, on_done: Callable[[], None]) -> None: self._iodevice = self._audio_input.start() - self._buffer = b"" - self._iodevice.readyRead.connect(self._on_read_ready) # type: ignore + self._buffer = bytearray() + qconnect(self._iodevice.readyRead, self._on_read_ready) super().start(on_done) def _on_read_ready(self) -> None: - self._buffer += cast(bytes, self._iodevice.readAll()) + self._buffer.extend(cast(bytes, self._iodevice.readAll())) def stop(self, on_done: Callable[[str], None]) -> None: def on_stop_timer() -> None: diff --git a/qt/aqt/sound.py b/qt/aqt/sound.py index b7d3a5c99..3642a9157 100644 --- a/qt/aqt/sound.py +++ b/qt/aqt/sound.py @@ -15,7 +15,7 @@ import wave from abc import ABC, abstractmethod from concurrent.futures import Future from operator import itemgetter -from typing import Any, Callable +from typing import Any, Callable, cast from markdown import markdown @@ -551,6 +551,78 @@ def prompt_for_mic_permission() -> None: from .qt5 import prompt_for_mic_permission prompt_for_mic_permission() + else: + # no longer seems to be required, perhaps due to newer macOS sdk? + pass + + +class QtAudioInputRecorder(Recorder): + def __init__(self, output_path: str, mw: aqt.AnkiQt, parent: QWidget) -> None: + super().__init__(output_path) + + self.mw = mw + self._parent = parent + + from PyQt6.QtMultimedia import QAudioFormat, QAudioSource # type: ignore + + format = QAudioFormat() + format.setChannelCount(1) + format.setSampleRate(44100) + format.setSampleFormat(QAudioFormat.SampleFormat.Int16) + + source = QAudioSource(format, parent) + + self._format = source.format() + self._audio_input = source + + def start(self, on_done: Callable[[], None]) -> None: + self._iodevice = self._audio_input.start() + self._buffer = bytearray() + qconnect(self._iodevice.readyRead, self._on_read_ready) + super().start(on_done) + + def _on_read_ready(self) -> None: + self._buffer.extend(cast(bytes, self._iodevice.readAll())) + + def stop(self, on_done: Callable[[str], None]) -> None: + from PyQt6.QtMultimedia import QAudio + + def on_stop_timer() -> None: + # read anything remaining in buffer & stop + self._on_read_ready() + self._audio_input.stop() + + if (err := self._audio_input.error()) != QAudio.Error.NoError: + showWarning(f"recording failed: {err}") + return + + def write_file() -> None: + # swallow the first 300ms to allow audio device to quiesce + wait = int(44100 * self.STARTUP_DELAY) + if len(self._buffer) <= wait: + return + self._buffer = self._buffer[wait:] + + # write out the wave file + wf = wave.open(self.output_path, "wb") + wf.setnchannels(self._format.channelCount()) + wf.setsampwidth(2) + wf.setframerate(self._format.sampleRate()) + wf.writeframes(self._buffer) + wf.close() + + def and_then(fut: Future) -> None: + fut.result() + Recorder.stop(self, on_done) + + self.mw.taskman.run_in_background(write_file, and_then) + + # schedule the stop for half a second in the future, + # to avoid truncating the end of the recording + self._stop_timer = t = QTimer(self._parent) + t.timeout.connect(on_stop_timer) # type: ignore + t.setSingleShot(True) + t.start(500) # PyAudio recording @@ -689,14 +761,17 @@ class RecordDialog(QDialog): def _start_recording(self) -> None: driver = self.mw.pm.recording_driver() - if driver is RecordingDriver.PyAudio or qtmajor > 5: + if driver is RecordingDriver.PyAudio: self._recorder = PyAudioRecorder(self.mw, namedtmp("rec.wav")) elif driver is RecordingDriver.QtAudioInput: - from .qt5 import QtAudioInputRecorder + if qtmajor > 5: + self._recorder = QtAudioInputRecorder( + namedtmp("rec.wav"), self.mw, self._parent + ) + else: + from .qt5 import QtAudioInputRecorder as Qt5Recorder - self._recorder = QtAudioInputRecorder( - namedtmp("rec.wav"), self.mw, self._parent - ) + self._recorder = Qt5Recorder(namedtmp("rec.wav"), self.mw, self._parent) else: assert_exhaustive(driver) self._recorder.start(self._start_timer) From 6e48a59a8a93694a3ae43e88fcee157281945988 Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Fri, 8 Oct 2021 16:15:02 +1000 Subject: [PATCH 13/16] update manylinux version for new build env --- pylib/anki/BUILD.bazel | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/pylib/anki/BUILD.bazel b/pylib/anki/BUILD.bazel index d98080060..afe4509b7 100644 --- a/pylib/anki/BUILD.bazel +++ b/pylib/anki/BUILD.bazel @@ -73,8 +73,8 @@ py_wheel( platform = select({ "//platforms:windows_x86_64": "win_amd64", "//platforms:macos_x86_64": "macosx_10_7_x86_64", - "//platforms:linux_x86_64": "manylinux2014_x86_64", - "//platforms:linux_arm64": "manylinux2014_aarch64", + "//platforms:linux_x86_64": "manylinux_2_28_x86_64", + "//platforms:linux_arm64": "manylinux_2_28_aarch64", }), python_tag = "cp39", python_version = ">=3.9", @@ -124,7 +124,10 @@ py_library( ":proto_py", ], # includes the .pyi files - data = [":proto_py", "py.typed"], + data = [ + "py.typed", + ":proto_py", + ], imports = [".."], visibility = ["//visibility:public"], ) From caa76c8b966521c6b439bcfdbdf6082fd37667d0 Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Fri, 8 Oct 2021 16:27:22 +1000 Subject: [PATCH 14/16] update macOS wheel minimum to 10.13 The coarsetime crate already had us limited to 10.12+, and the wheel had just not been updated to reflect that. Increased to 10.13, as that's the minimum the Qt 5.14 libraries support. --- pylib/anki/BUILD.bazel | 2 +- pylib/rsbridge/BUILD.bazel | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pylib/anki/BUILD.bazel b/pylib/anki/BUILD.bazel index afe4509b7..abb7fe324 100644 --- a/pylib/anki/BUILD.bazel +++ b/pylib/anki/BUILD.bazel @@ -72,7 +72,7 @@ py_wheel( }, platform = select({ "//platforms:windows_x86_64": "win_amd64", - "//platforms:macos_x86_64": "macosx_10_7_x86_64", + "//platforms:macos_x86_64": "macosx_10_13_x86_64", "//platforms:linux_x86_64": "manylinux_2_28_x86_64", "//platforms:linux_arm64": "manylinux_2_28_aarch64", }), diff --git a/pylib/rsbridge/BUILD.bazel b/pylib/rsbridge/BUILD.bazel index fb81244e7..66502a1c5 100644 --- a/pylib/rsbridge/BUILD.bazel +++ b/pylib/rsbridge/BUILD.bazel @@ -18,7 +18,7 @@ rust_library( ): [ "-Clink-arg=-undefined", "-Clink-arg=dynamic_lookup", - "-Clink-arg=-mmacosx-version-min=10.7", + "-Clink-arg=-mmacosx-version-min=10.13", ], "//conditions:default": [], }), From 52642d693beef4674c7803e389dd38172d847831 Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Mon, 11 Oct 2021 18:23:38 +1000 Subject: [PATCH 15/16] drop PyAudio support I do not recall anyone reporting that it worked better than the Qt implementation for them, and the lack of recent wheels on PyPI is a pain. We can always add it back in the future if enough people come out of the woodwork to report they were using it. --- qt/aqt/forms/preferences.ui | 8 --- qt/aqt/pinnedmodules.py | 1 - qt/aqt/preferences.py | 37 +----------- qt/aqt/profiles.py | 17 ------ qt/aqt/qt5.py | 6 -- qt/aqt/sound.py | 114 +++--------------------------------- 6 files changed, 8 insertions(+), 175 deletions(-) diff --git a/qt/aqt/forms/preferences.ui b/qt/aqt/forms/preferences.ui index e705d0b05..085c10881 100644 --- a/qt/aqt/forms/preferences.ui +++ b/qt/aqt/forms/preferences.ui @@ -107,13 +107,6 @@ - - - - - - - @@ -616,7 +609,6 @@ paste_strips_formatting nightMode useCurrent - recording_driver default_search_text uiScale showEstimates diff --git a/qt/aqt/pinnedmodules.py b/qt/aqt/pinnedmodules.py index 3586d4ff5..8fe9e2c39 100644 --- a/qt/aqt/pinnedmodules.py +++ b/qt/aqt/pinnedmodules.py @@ -33,7 +33,6 @@ except: import PyQt5.QtSvg # type: ignore import PyQt5.QtMultimedia # type: ignore import socks -import pyaudio # legacy compat import anki.storage diff --git a/qt/aqt/preferences.py b/qt/aqt/preferences.py index c0cce5612..e10029b94 100644 --- a/qt/aqt/preferences.py +++ b/qt/aqt/preferences.py @@ -9,7 +9,7 @@ from anki.collection import OpChanges from anki.consts import newCardSchedulingLabels from aqt import AnkiQt from aqt.operations.collection import set_preferences -from aqt.profiles import RecordingDriver, VideoDriver +from aqt.profiles import VideoDriver from aqt.qt import * from aqt.utils import HelpPage, disable_help_button, openHelp, showInfo, showWarning, tr @@ -134,48 +134,13 @@ class Preferences(QDialog): def setup_profile(self) -> None: "Setup options stored in the user profile." - self.setup_recording_driver() self.setup_network() self.setup_backup() def update_profile(self) -> None: - self.update_recording_driver() self.update_network() self.update_backup() - # Profile: recording driver - ###################################################################### - - def setup_recording_driver(self) -> None: - self._recording_drivers = [ - RecordingDriver.QtAudioInput, - RecordingDriver.PyAudio, - ] - # The plan is to phase out PyAudio soon, so will hold off on - # making this string translatable for now. - self.form.recording_driver.addItems( - [ - f"Voice recording driver: {driver.value}" - for driver in self._recording_drivers - ] - ) - self.form.recording_driver.setCurrentIndex( - self._recording_drivers.index(self.mw.pm.recording_driver()) - ) - - def update_recording_driver(self) -> None: - new_audio_driver = self._recording_drivers[ - self.form.recording_driver.currentIndex() - ] - if self.mw.pm.recording_driver() != new_audio_driver: - self.mw.pm.set_recording_driver(new_audio_driver) - if new_audio_driver == RecordingDriver.PyAudio: - showInfo( - """\ -The PyAudio driver will likely be removed in a future update. If you find it works better \ -for you than the default driver, please let us know on the Anki forums.""" - ) - # Profile: network ###################################################################### diff --git a/qt/aqt/profiles.py b/qt/aqt/profiles.py index ce1fabef6..59ba16572 100644 --- a/qt/aqt/profiles.py +++ b/qt/aqt/profiles.py @@ -31,11 +31,6 @@ from aqt.utils import disable_help_button, showWarning, tr # - Saves in sqlite rather than a flat file so the config can't be corrupted -class RecordingDriver(Enum): - PyAudio = "PyAudio" - QtAudioInput = "Qt" - - class VideoDriver(Enum): OpenGL = "auto" ANGLE = "angle" @@ -556,18 +551,6 @@ create table if not exists profiles def set_auto_sync_media_minutes(self, val: int) -> None: self.profile["autoSyncMediaMinutes"] = val - def recording_driver(self) -> RecordingDriver: - if driver := self.profile.get("recordingDriver"): - try: - return RecordingDriver(driver) - except ValueError: - # revert to default - pass - return RecordingDriver.QtAudioInput - - def set_recording_driver(self, driver: RecordingDriver) -> None: - self.profile["recordingDriver"] = driver.value - def show_browser_table_tooltips(self) -> bool: return self.profile.get("browserTableTooltips", True) diff --git a/qt/aqt/qt5.py b/qt/aqt/qt5.py index 6c1e68f63..d87a3fa2c 100644 --- a/qt/aqt/qt5.py +++ b/qt/aqt/qt5.py @@ -96,9 +96,3 @@ class QtAudioInputRecorder(Recorder): t.timeout.connect(on_stop_timer) # type: ignore t.setSingleShot(True) t.start(500) - - -def prompt_for_mic_permission() -> None: - from PyQt5.QtMultimedia import QAudioDeviceInfo - - QAudioDeviceInfo.defaultInputDevice() diff --git a/qt/aqt/sound.py b/qt/aqt/sound.py index 3642a9157..ee6c38ae2 100644 --- a/qt/aqt/sound.py +++ b/qt/aqt/sound.py @@ -9,7 +9,6 @@ import platform import re import subprocess import sys -import threading import time import wave from abc import ABC, abstractmethod @@ -23,11 +22,9 @@ import aqt from anki import hooks from anki.cards import Card from anki.sound import AV_REF_RE, AVTag, SoundOrVideoTag -from anki.types import assert_exhaustive from anki.utils import isLin, isMac, isWin, namedtmp from aqt import gui_hooks from aqt.mpv import MPV, MPVBase, MPVCommandError -from aqt.profiles import RecordingDriver from aqt.qt import * from aqt.taskman import TaskManager from aqt.utils import ( @@ -546,16 +543,6 @@ class Recorder(ABC): ########################################################################## -def prompt_for_mic_permission() -> None: - if qtmajor == 5 and isMac: - from .qt5 import prompt_for_mic_permission - - prompt_for_mic_permission() - else: - # no longer seems to be required, perhaps due to newer macOS sdk? - pass - - class QtAudioInputRecorder(Recorder): def __init__(self, output_path: str, mw: aqt.AnkiQt, parent: QWidget) -> None: super().__init__(output_path) @@ -625,87 +612,6 @@ class QtAudioInputRecorder(Recorder): t.start(500) -# PyAudio recording -########################################################################## - -try: - import pyaudio -except: - pyaudio = None - - -PYAU_CHANNELS = 1 -PYAU_INPUT_INDEX: int | None = None - - -class PyAudioThreadedRecorder(threading.Thread): - def __init__(self, output_path: str, startup_delay: float) -> None: - threading.Thread.__init__(self) - self._output_path = output_path - self._startup_delay = startup_delay - self.finish = False - # though we're using pyaudio here, we rely on Qt to trigger - # the permission prompt - prompt_for_mic_permission() - - def run(self) -> None: - chunk = 1024 - p = pyaudio.PyAudio() - - rate = int(p.get_default_input_device_info()["defaultSampleRate"]) - PYAU_FORMAT = pyaudio.paInt16 - - stream = p.open( - format=PYAU_FORMAT, - channels=PYAU_CHANNELS, - rate=rate, - input=True, - input_device_index=PYAU_INPUT_INDEX, - frames_per_buffer=chunk, - ) - - # swallow the first 300ms to allow audio device to quiesce - wait = int(rate * self._startup_delay) - stream.read(wait, exception_on_overflow=False) - - # read data in a loop until self.finish is set - data = b"" - while not self.finish: - data += stream.read(chunk, exception_on_overflow=False) - - # write out the wave file - stream.close() - p.terminate() - wf = wave.open(self._output_path, "wb") - wf.setnchannels(PYAU_CHANNELS) - wf.setsampwidth(p.get_sample_size(PYAU_FORMAT)) - wf.setframerate(rate) - wf.writeframes(data) - wf.close() - - -class PyAudioRecorder(Recorder): - def __init__(self, mw: aqt.AnkiQt, output_path: str) -> None: - super().__init__(output_path) - self.mw = mw - - def start(self, on_done: Callable[[], None]) -> None: - self.thread = PyAudioThreadedRecorder(self.output_path, self.STARTUP_DELAY) - self.thread.start() - super().start(on_done) - - def stop(self, on_done: Callable[[str], None]) -> None: - # ensure at least a second captured - while self.duration() < 1: - time.sleep(0.1) - - def func(fut: Future) -> None: - Recorder.stop(self, on_done) - - self.thread.finish = True - self.mw.taskman.run_in_background(self.thread.join, func) - - # Recording dialog ########################################################################## @@ -760,20 +666,14 @@ class RecordDialog(QDialog): saveGeom(self, "audioRecorder2") def _start_recording(self) -> None: - driver = self.mw.pm.recording_driver() - if driver is RecordingDriver.PyAudio: - self._recorder = PyAudioRecorder(self.mw, namedtmp("rec.wav")) - elif driver is RecordingDriver.QtAudioInput: - if qtmajor > 5: - self._recorder = QtAudioInputRecorder( - namedtmp("rec.wav"), self.mw, self._parent - ) - else: - from .qt5 import QtAudioInputRecorder as Qt5Recorder - - self._recorder = Qt5Recorder(namedtmp("rec.wav"), self.mw, self._parent) + if qtmajor > 5: + self._recorder = QtAudioInputRecorder( + namedtmp("rec.wav"), self.mw, self._parent + ) else: - assert_exhaustive(driver) + from .qt5 import QtAudioInputRecorder as Qt5Recorder + + self._recorder = Qt5Recorder(namedtmp("rec.wav"), self.mw, self._parent) self._recorder.start(self._start_timer) def _start_timer(self) -> None: From b10aebc8b8861c418b99a492a48a8b6023e5068f Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Fri, 15 Oct 2021 10:53:28 +1000 Subject: [PATCH 16/16] use separate header state for each Qt version https://forums.ankiweb.net/t/new-toolkit-and-packaging-test-windows/14081/15 --- qt/aqt/utils.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/qt/aqt/utils.py b/qt/aqt/utils.py index 3d8dd4120..6deb98459 100644 --- a/qt/aqt/utils.py +++ b/qt/aqt/utils.py @@ -568,15 +568,19 @@ def restoreSplitter(widget: QSplitter, key: str) -> None: widget.restoreState(aqt.mw.pm.profile[key]) +def _header_key(key: str) -> str: + # not compatible across major versions + qt_suffix = f"Qt{qtmajor}" if qtmajor > 5 else "" + return f"{key}Header{qt_suffix}" + + def saveHeader(widget: QHeaderView, key: str) -> None: - key += "Header" - aqt.mw.pm.profile[key] = widget.saveState() + aqt.mw.pm.profile[_header_key(key)] = widget.saveState() def restoreHeader(widget: QHeaderView, key: str) -> None: - key += "Header" - if aqt.mw.pm.profile.get(key): - widget.restoreState(aqt.mw.pm.profile[key]) + if state := aqt.mw.pm.profile.get(_header_key(key)): + widget.restoreState(state) def save_is_checked(widget: QCheckBox, key: str) -> None: