Drop remaining qt5 code

This commit is contained in:
Damien Elmes 2025-06-20 16:11:17 +07:00
parent cd411927cc
commit 8e20973c52
55 changed files with 71 additions and 973 deletions

View file

@ -38,7 +38,6 @@ fn build_forms(build: &mut Build) -> Result<()> {
let mut py_files = vec![]; let mut py_files = vec![];
for path in ui_files.resolve() { for path in ui_files.resolve() {
let outpath = outdir.join(path.file_name().unwrap()).into_string(); let outpath = outdir.join(path.file_name().unwrap()).into_string();
py_files.push(outpath.replace(".ui", "_qt5.py"));
py_files.push(outpath.replace(".ui", "_qt6.py")); py_files.push(outpath.replace(".ui", "_qt6.py"));
} }
build.add_action( build.add_action(

View file

@ -51,13 +51,8 @@ Anki requires a recent glibc.
If you are using a distro that uses musl, Anki will not work. If you are using a distro that uses musl, Anki will not work.
If your glibc version is 2.35+ on AMD64 or 2.39+ on ARM64, you can skip the rest of this section. You can use your system's Qt libraries if they are Qt 6.2 or later, if
you wish. After installing the system libraries (eg:
If your system has an older glibc, you won't be able to use the PyQt wheels that are
available in pip/PyPy, and will need to use your system-installed PyQt instead.
Your distro will also need to have Python 3.9 or later.
After installing the system libraries (eg:
'sudo apt install python3-pyqt6.qt{quick,webengine} python3-venv pyqt6-dev-tools'), 'sudo apt install python3-pyqt6.qt{quick,webengine} python3-venv pyqt6-dev-tools'),
find the place they are installed (eg '/usr/lib/python3/dist-packages'). On modern Ubuntu, you'll find the place they are installed (eg '/usr/lib/python3/dist-packages'). On modern Ubuntu, you'll
also need 'sudo apt remove python3-protobuf'. Then before running any commands like './run', tell Anki where also need 'sudo apt remove python3-protobuf'. Then before running any commands like './run', tell Anki where
@ -68,12 +63,6 @@ export PYTHONPATH=/usr/lib/python3/dist-packages
export PYTHON_BINARY=/usr/bin/python3 export PYTHON_BINARY=/usr/bin/python3
``` ```
There are a few things to be aware of:
- You should use ./run and not tools/run-qt5\*, even if your system libraries are Qt5.
- If your system libraries are Qt5, when creating an aqt wheel, the wheel will not work
on Qt6 environments.
## Packaging considerations ## Packaging considerations
Python, node and protoc are downloaded as part of the build. You can optionally define Python, node and protoc are downloaded as part of the build. You can optionally define

View file

@ -284,7 +284,7 @@ def setupLangAndBackend(
class NativeEventFilter(QAbstractNativeEventFilter): class NativeEventFilter(QAbstractNativeEventFilter):
def nativeEventFilter( def nativeEventFilter(
self, eventType: Any, message: Any self, eventType: Any, message: Any
) -> tuple[bool, sip.voidptr | None]: ) -> tuple[bool, Any | None]:
if eventType == "windows_generic_MSG": if eventType == "windows_generic_MSG":
import ctypes.wintypes import ctypes.wintypes
@ -376,6 +376,8 @@ class AnkiApp(QApplication):
def onRecv(self) -> None: def onRecv(self) -> None:
sock = self._srv.nextPendingConnection() sock = self._srv.nextPendingConnection()
if sock is None:
return
if not sock.waitForReadyRead(self.TMOUT): if not sock.waitForReadyRead(self.TMOUT):
sys.stderr.write(sock.errorString()) sys.stderr.write(sock.errorString())
return return
@ -406,14 +408,12 @@ class AnkiApp(QApplication):
QRadioButton, QRadioButton,
QMenu, QMenu,
QSlider, QSlider,
# classes with PyQt5 compatibility proxy QToolButton,
without_qt5_compat_wrapper(QToolButton), QTabBar,
without_qt5_compat_wrapper(QTabBar),
) )
if evt.type() in [QEvent.Type.Enter, QEvent.Type.HoverEnter]: if evt.type() in [QEvent.Type.Enter, QEvent.Type.HoverEnter]:
if (isinstance(src, pointer_classes) and src.isEnabled()) or ( if (isinstance(src, pointer_classes) and src.isEnabled()) or (
isinstance(src, without_qt5_compat_wrapper(QComboBox)) isinstance(src, QComboBox) and not src.isEditable()
and not src.isEditable()
): ):
self.setOverrideCursor(QCursor(Qt.CursorShape.PointingHandCursor)) self.setOverrideCursor(QCursor(Qt.CursorShape.PointingHandCursor))
else: else:
@ -525,15 +525,12 @@ def setupGL(pm: aqt.profiles.ProfileManager) -> None:
QQuickWindow.setGraphicsApi(QSGRendererInterface.GraphicsApi.OpenGL) QQuickWindow.setGraphicsApi(QSGRendererInterface.GraphicsApi.OpenGL)
elif driver in (VideoDriver.Software, VideoDriver.ANGLE): elif driver in (VideoDriver.Software, VideoDriver.ANGLE):
if is_win: if is_win:
# on Windows, this appears to be sufficient on Qt5/Qt6. # on Windows, this appears to be sufficient
# On Qt6, ANGLE is excluded by the enum. # On Qt6, ANGLE is excluded by the enum.
os.environ["QT_OPENGL"] = driver.value os.environ["QT_OPENGL"] = driver.value
elif is_mac: elif is_mac:
QCoreApplication.setAttribute(Qt.ApplicationAttribute.AA_UseSoftwareOpenGL) QCoreApplication.setAttribute(Qt.ApplicationAttribute.AA_UseSoftwareOpenGL)
elif is_lin: elif is_lin:
# Qt5 only
os.environ["QT_XCB_FORCE_SOFTWARE_OPENGL"] = "1"
# Required on Qt6
if "QTWEBENGINE_CHROMIUM_FLAGS" not in os.environ: if "QTWEBENGINE_CHROMIUM_FLAGS" not in os.environ:
os.environ["QTWEBENGINE_CHROMIUM_FLAGS"] = "--disable-gpu" os.environ["QTWEBENGINE_CHROMIUM_FLAGS"] = "--disable-gpu"
if qtmajor > 5: if qtmajor > 5:
@ -663,12 +660,6 @@ def _run(argv: list[str] | None = None, exec: bool = True) -> AnkiApp | None:
if is_win and "QT_QPA_PLATFORM" not in os.environ: if is_win and "QT_QPA_PLATFORM" not in os.environ:
os.environ["QT_QPA_PLATFORM"] = "windows:altgr" os.environ["QT_QPA_PLATFORM"] = "windows:altgr"
# Disable sandbox on Qt5 PyPi/packaged builds, as it causes blank screens on modern
# glibc versions. We check for specific patch versions, because distros may have
# fixed the issue in their own Qt builds.
if is_lin and qtfullversion in ([5, 15, 2], [5, 14, 1]):
os.environ["QTWEBENGINE_DISABLE_SANDBOX"] = "1"
# create the app # create the app
QCoreApplication.setApplicationName("Anki") QCoreApplication.setApplicationName("Anki")
QGuiApplication.setDesktopFileName("anki") QGuiApplication.setDesktopFileName("anki")

View file

@ -325,15 +325,13 @@ class DataModel(QAbstractTableModel):
return 0 return 0
return self.len_columns() return self.len_columns()
_QFont = without_qt5_compat_wrapper(QFont)
def data(self, index: QModelIndex = QModelIndex(), role: int = 0) -> Any: def data(self, index: QModelIndex = QModelIndex(), role: int = 0) -> Any:
if not index.isValid(): if not index.isValid():
return QVariant() return QVariant()
if role == Qt.ItemDataRole.FontRole: if role == Qt.ItemDataRole.FontRole:
if not self.column_at(index).uses_cell_font: if not self.column_at(index).uses_cell_font:
return QVariant() return QVariant()
qfont = self._QFont() qfont = QFont()
row = self.get_row(index) row = self.get_row(index)
qfont.setFamily(row.font_name) qfont.setFamily(row.font_name)
qfont.setPixelSize(row.font_size) qfont.setPixelSize(row.font_size)

View file

@ -382,9 +382,6 @@ class Table:
hh.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu) hh.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
self._restore_header() self._restore_header()
qconnect(hh.customContextMenuRequested, self._on_header_context) qconnect(hh.customContextMenuRequested, self._on_header_context)
if qtmajor == 5:
qconnect(hh.sortIndicatorChanged, self._on_sort_column_changed_qt5)
else:
qconnect(hh.sortIndicatorChanged, self._on_sort_column_changed) qconnect(hh.sortIndicatorChanged, self._on_sort_column_changed)
qconnect(hh.sectionMoved, self._on_column_moved) qconnect(hh.sectionMoved, self._on_column_moved)
@ -495,12 +492,6 @@ class Table:
if checked: if checked:
self._scroll_to_column(self._model.len_columns() - 1) self._scroll_to_column(self._model.len_columns() - 1)
def _on_sort_column_changed_qt5(self, section: int, order: int) -> None:
self._on_sort_column_changed(
section,
Qt.SortOrder.AscendingOrder if not order else Qt.SortOrder.DescendingOrder,
)
def _on_sort_column_changed(self, section: int, order: Qt.SortOrder) -> None: def _on_sort_column_changed(self, section: int, order: Qt.SortOrder) -> None:
column = self._model.column_at_section(section) column = self._model.column_at_section(section)
sorting = column.sorting_notes if self.is_notes_mode() else column.sorting_cards sorting = column.sorting_notes if self.is_notes_mode() else column.sorting_cards

View file

@ -1,8 +1 @@
from typing import TYPE_CHECKING from _aqt.forms.about_qt6 import *
from aqt.qt import qtmajor
if qtmajor > 5 or TYPE_CHECKING:
from _aqt.forms.about_qt6 import *
else:
from _aqt.forms.about_qt5 import * # type: ignore

View file

@ -1,8 +1 @@
from typing import TYPE_CHECKING from _aqt.forms.addcards_qt6 import *
from aqt.qt import qtmajor
if qtmajor > 5 or TYPE_CHECKING:
from _aqt.forms.addcards_qt6 import *
else:
from _aqt.forms.addcards_qt5 import * # type: ignore

View file

@ -1,8 +1 @@
from typing import TYPE_CHECKING from _aqt.forms.addfield_qt6 import *
from aqt.qt import qtmajor
if qtmajor > 5 or TYPE_CHECKING:
from _aqt.forms.addfield_qt6 import *
else:
from _aqt.forms.addfield_qt5 import * # type: ignore

View file

@ -1,8 +1 @@
from typing import TYPE_CHECKING from _aqt.forms.addmodel_qt6 import *
from aqt.qt import qtmajor
if qtmajor > 5 or TYPE_CHECKING:
from _aqt.forms.addmodel_qt6 import *
else:
from _aqt.forms.addmodel_qt5 import * # type: ignore

View file

@ -1,8 +1 @@
from typing import TYPE_CHECKING from _aqt.forms.addonconf_qt6 import *
from aqt.qt import qtmajor
if qtmajor > 5 or TYPE_CHECKING:
from _aqt.forms.addonconf_qt6 import *
else:
from _aqt.forms.addonconf_qt5 import * # type: ignore

View file

@ -1,8 +1 @@
from typing import TYPE_CHECKING from _aqt.forms.addons_qt6 import *
from aqt.qt import qtmajor
if qtmajor > 5 or TYPE_CHECKING:
from _aqt.forms.addons_qt6 import *
else:
from _aqt.forms.addons_qt5 import * # type: ignore

View file

@ -1,8 +1 @@
from typing import TYPE_CHECKING from _aqt.forms.browser_qt6 import *
from aqt.qt import qtmajor
if qtmajor > 5 or TYPE_CHECKING:
from _aqt.forms.browser_qt6 import *
else:
from _aqt.forms.browser_qt5 import * # type: ignore

View file

@ -1,8 +1 @@
from typing import TYPE_CHECKING from _aqt.forms.browserdisp_qt6 import *
from aqt.qt import qtmajor
if qtmajor > 5 or TYPE_CHECKING:
from _aqt.forms.browserdisp_qt6 import *
else:
from _aqt.forms.browserdisp_qt5 import * # type: ignore

View file

@ -1,8 +1 @@
from typing import TYPE_CHECKING from _aqt.forms.browseropts_qt6 import *
from aqt.qt import qtmajor
if qtmajor > 5 or TYPE_CHECKING:
from _aqt.forms.browseropts_qt6 import *
else:
from _aqt.forms.browseropts_qt5 import * # type: ignore

View file

@ -1,8 +1 @@
from typing import TYPE_CHECKING from _aqt.forms.changemap_qt6 import *
from aqt.qt import qtmajor
if qtmajor > 5 or TYPE_CHECKING:
from _aqt.forms.changemap_qt6 import *
else:
from _aqt.forms.changemap_qt5 import * # type: ignore

View file

@ -1,8 +1 @@
from typing import TYPE_CHECKING from _aqt.forms.changemodel_qt6 import *
from aqt.qt import qtmajor
if qtmajor > 5 or TYPE_CHECKING:
from _aqt.forms.changemodel_qt6 import *
else:
from _aqt.forms.changemodel_qt5 import * # type: ignore

View file

@ -1,8 +1 @@
from typing import TYPE_CHECKING from _aqt.forms.clayout_top_qt6 import *
from aqt.qt import qtmajor
if qtmajor > 5 or TYPE_CHECKING:
from _aqt.forms.clayout_top_qt6 import *
else:
from _aqt.forms.clayout_top_qt5 import * # type: ignore

View file

@ -1,8 +1 @@
from typing import TYPE_CHECKING from _aqt.forms.customstudy_qt6 import *
from aqt.qt import qtmajor
if qtmajor > 5 or TYPE_CHECKING:
from _aqt.forms.customstudy_qt6 import *
else:
from _aqt.forms.customstudy_qt5 import * # type: ignore

View file

@ -1,8 +1 @@
from typing import TYPE_CHECKING from _aqt.forms.dconf_qt6 import *
from aqt.qt import qtmajor
if qtmajor > 5 or TYPE_CHECKING:
from _aqt.forms.dconf_qt6 import *
else:
from _aqt.forms.dconf_qt5 import * # type: ignore

View file

@ -1,8 +1 @@
from typing import TYPE_CHECKING from _aqt.forms.debug_qt6 import *
from aqt.qt import qtmajor
if qtmajor > 5 or TYPE_CHECKING:
from _aqt.forms.debug_qt6 import *
else:
from _aqt.forms.debug_qt5 import * # type: ignore

View file

@ -1,8 +1 @@
from typing import TYPE_CHECKING from _aqt.forms.editcurrent_qt6 import *
from aqt.qt import qtmajor
if qtmajor > 5 or TYPE_CHECKING:
from _aqt.forms.editcurrent_qt6 import *
else:
from _aqt.forms.editcurrent_qt5 import * # type: ignore

View file

@ -1,8 +1 @@
from typing import TYPE_CHECKING from _aqt.forms.edithtml_qt6 import *
from aqt.qt import qtmajor
if qtmajor > 5 or TYPE_CHECKING:
from _aqt.forms.edithtml_qt6 import *
else:
from _aqt.forms.edithtml_qt5 import * # type: ignore

View file

@ -1,8 +1 @@
from typing import TYPE_CHECKING from _aqt.forms.emptycards_qt6 import *
from aqt.qt import qtmajor
if qtmajor > 5 or TYPE_CHECKING:
from _aqt.forms.emptycards_qt6 import *
else:
from _aqt.forms.emptycards_qt5 import * # type: ignore

View file

@ -1,8 +1 @@
from typing import TYPE_CHECKING from _aqt.forms.exporting_qt6 import *
from aqt.qt import qtmajor
if qtmajor > 5 or TYPE_CHECKING:
from _aqt.forms.exporting_qt6 import *
else:
from _aqt.forms.exporting_qt5 import * # type: ignore

View file

@ -1,8 +1 @@
from typing import TYPE_CHECKING from _aqt.forms.fields_qt6 import *
from aqt.qt import qtmajor
if qtmajor > 5 or TYPE_CHECKING:
from _aqt.forms.fields_qt6 import *
else:
from _aqt.forms.fields_qt5 import * # type: ignore

View file

@ -1,8 +1 @@
from typing import TYPE_CHECKING from _aqt.forms.filtered_deck_qt6 import *
from aqt.qt import qtmajor
if qtmajor > 5 or TYPE_CHECKING:
from _aqt.forms.filtered_deck_qt6 import *
else:
from _aqt.forms.filtered_deck_qt5 import * # type: ignore

View file

@ -1,8 +1 @@
from typing import TYPE_CHECKING from _aqt.forms.finddupes_qt6 import *
from aqt.qt import qtmajor
if qtmajor > 5 or TYPE_CHECKING:
from _aqt.forms.finddupes_qt6 import *
else:
from _aqt.forms.finddupes_qt5 import * # type: ignore

View file

@ -1,8 +1 @@
from typing import TYPE_CHECKING from _aqt.forms.findreplace_qt6 import *
from aqt.qt import qtmajor
if qtmajor > 5 or TYPE_CHECKING:
from _aqt.forms.findreplace_qt6 import *
else:
from _aqt.forms.findreplace_qt5 import * # type: ignore

View file

@ -1,8 +1 @@
from typing import TYPE_CHECKING from _aqt.forms.forget_qt6 import *
from aqt.qt import qtmajor
if qtmajor > 5 or TYPE_CHECKING:
from _aqt.forms.forget_qt6 import *
else:
from _aqt.forms.forget_qt5 import * # type: ignore

View file

@ -1,8 +1 @@
from typing import TYPE_CHECKING from _aqt.forms.getaddons_qt6 import *
from aqt.qt import qtmajor
if qtmajor > 5 or TYPE_CHECKING:
from _aqt.forms.getaddons_qt6 import *
else:
from _aqt.forms.getaddons_qt5 import * # type: ignore

View file

@ -1,8 +1 @@
from typing import TYPE_CHECKING from _aqt.forms.importing_qt6 import *
from aqt.qt import qtmajor
if qtmajor > 5 or TYPE_CHECKING:
from _aqt.forms.importing_qt6 import *
else:
from _aqt.forms.importing_qt5 import * # type: ignore

View file

@ -1,8 +1 @@
from typing import TYPE_CHECKING from _aqt.forms.main_qt6 import *
from aqt.qt import qtmajor
if qtmajor > 5 or TYPE_CHECKING:
from _aqt.forms.main_qt6 import *
else:
from _aqt.forms.main_qt5 import * # type: ignore

View file

@ -1,8 +1 @@
from typing import TYPE_CHECKING from _aqt.forms.modelopts_qt6 import *
from aqt.qt import qtmajor
if qtmajor > 5 or TYPE_CHECKING:
from _aqt.forms.modelopts_qt6 import *
else:
from _aqt.forms.modelopts_qt5 import * # type: ignore

View file

@ -1,8 +1 @@
from typing import TYPE_CHECKING from _aqt.forms.models_qt6 import *
from aqt.qt import qtmajor
if qtmajor > 5 or TYPE_CHECKING:
from _aqt.forms.models_qt6 import *
else:
from _aqt.forms.models_qt5 import * # type: ignore

View file

@ -1,8 +1 @@
from typing import TYPE_CHECKING from _aqt.forms.preferences_qt6 import *
from aqt.qt import qtmajor
if qtmajor > 5 or TYPE_CHECKING:
from _aqt.forms.preferences_qt6 import *
else:
from _aqt.forms.preferences_qt5 import * # type: ignore

View file

@ -1,8 +1 @@
from typing import TYPE_CHECKING from _aqt.forms.preview_qt6 import *
from aqt.qt import qtmajor
if qtmajor > 5 or TYPE_CHECKING:
from _aqt.forms.preview_qt6 import *
else:
from _aqt.forms.preview_qt5 import * # type: ignore

View file

@ -1,8 +1 @@
from typing import TYPE_CHECKING from _aqt.forms.profiles_qt6 import *
from aqt.qt import qtmajor
if qtmajor > 5 or TYPE_CHECKING:
from _aqt.forms.profiles_qt6 import *
else:
from _aqt.forms.profiles_qt5 import * # type: ignore

View file

@ -1,8 +1 @@
from typing import TYPE_CHECKING from _aqt.forms.progress_qt6 import *
from aqt.qt import qtmajor
if qtmajor > 5 or TYPE_CHECKING:
from _aqt.forms.progress_qt6 import *
else:
from _aqt.forms.progress_qt5 import * # type: ignore

View file

@ -1,8 +1 @@
from typing import TYPE_CHECKING from _aqt.forms.reposition_qt6 import *
from aqt.qt import qtmajor
if qtmajor > 5 or TYPE_CHECKING:
from _aqt.forms.reposition_qt6 import *
else:
from _aqt.forms.reposition_qt5 import * # type: ignore

View file

@ -1,8 +1 @@
from typing import TYPE_CHECKING from _aqt.forms.setgroup_qt6 import *
from aqt.qt import qtmajor
if qtmajor > 5 or TYPE_CHECKING:
from _aqt.forms.setgroup_qt6 import *
else:
from _aqt.forms.setgroup_qt5 import * # type: ignore

View file

@ -1,8 +1 @@
from typing import TYPE_CHECKING from _aqt.forms.setlang_qt6 import *
from aqt.qt import qtmajor
if qtmajor > 5 or TYPE_CHECKING:
from _aqt.forms.setlang_qt6 import *
else:
from _aqt.forms.setlang_qt5 import * # type: ignore

View file

@ -1,8 +1 @@
from typing import TYPE_CHECKING from _aqt.forms.stats_qt6 import *
from aqt.qt import qtmajor
if qtmajor > 5 or TYPE_CHECKING:
from _aqt.forms.stats_qt6 import *
else:
from _aqt.forms.stats_qt5 import * # type: ignore

View file

@ -1,8 +1 @@
from typing import TYPE_CHECKING from _aqt.forms.studydeck_qt6 import *
from aqt.qt import qtmajor
if qtmajor > 5 or TYPE_CHECKING:
from _aqt.forms.studydeck_qt6 import *
else:
from _aqt.forms.studydeck_qt5 import * # type: ignore

View file

@ -1,8 +1 @@
from typing import TYPE_CHECKING from _aqt.forms.synclog_qt6 import *
from aqt.qt import qtmajor
if qtmajor > 5 or TYPE_CHECKING:
from _aqt.forms.synclog_qt6 import *
else:
from _aqt.forms.synclog_qt5 import * # type: ignore

View file

@ -1,8 +1 @@
from typing import TYPE_CHECKING from _aqt.forms.taglimit_qt6 import *
from aqt.qt import qtmajor
if qtmajor > 5 or TYPE_CHECKING:
from _aqt.forms.taglimit_qt6 import *
else:
from _aqt.forms.taglimit_qt5 import * # type: ignore

View file

@ -1,8 +1 @@
from typing import TYPE_CHECKING from _aqt.forms.template_qt6 import *
from aqt.qt import qtmajor
if qtmajor > 5 or TYPE_CHECKING:
from _aqt.forms.template_qt6 import *
else:
from _aqt.forms.template_qt5 import * # type: ignore

View file

@ -1,8 +1 @@
from typing import TYPE_CHECKING from _aqt.forms.widgets_qt6 import *
from aqt.qt import qtmajor
if qtmajor > 5 or TYPE_CHECKING:
from _aqt.forms.widgets_qt6 import *
else:
from _aqt.forms.widgets_qt5 import * # type: ignore

View file

@ -189,9 +189,6 @@ class ProfileManager:
# return the bytes directly # return the bytes directly
return args[0] return args[0]
elif name == "_unpickle_enum": elif name == "_unpickle_enum":
if qtmajor == 5:
return sip._unpickle_enum(module, klass, args) # type: ignore
else:
# old style enums can't be unpickled # old style enums can't be unpickled
return None return None
else: else:

View file

@ -300,8 +300,7 @@ class ProgressManager:
def _closeWin(self) -> None: def _closeWin(self) -> None:
# if the parent window has been deleted, the progress dialog may have # if the parent window has been deleted, the progress dialog may have
# already been dropped; delete it if it hasn't been # already been dropped; delete it if it hasn't been
if not sip.isdeleted(self._win): if self._win and not sip.isdeleted(self._win):
assert self._win is not None
self._win.cancel() self._win.cancel()
self._win = None self._win = None
self._shown = 0 self._shown = 0

View file

@ -11,20 +11,12 @@ import traceback
from collections.abc import Callable from collections.abc import Callable
from typing import TypeVar, Union from typing import TypeVar, Union
try: from anki._legacy import deprecated
import PyQt6
except Exception:
from .qt5 import * # type: ignore
else:
if os.getenv("ENABLE_QT5_COMPAT"):
print("Running with temporary Qt5 compatibility shims.")
from . import qt5_compat # needs to be imported first
from .qt6 import *
# legacy code depends on these re-exports
from anki.utils import is_mac, is_win from anki.utils import is_mac, is_win
# fix buggy ubuntu12.04 display of language selector from .qt6 import *
os.environ["LIBOVERLAY_SCROLLBAR"] = "0"
def debug() -> None: def debug() -> None:
@ -52,7 +44,7 @@ qtminor = _version.minorVersion()
qtpoint = _version.microVersion() qtpoint = _version.microVersion()
qtfullversion = _version.segments() qtfullversion = _version.segments()
if qtmajor < 5 or (qtmajor == 5 and qtminor < 14): if qtmajor == 6 and qtminor < 2:
raise Exception("Anki does not support your Qt version.") raise Exception("Anki does not support your Qt version.")
@ -64,11 +56,6 @@ def qconnect(signal: Callable | pyqtSignal | pyqtBoundSignal, func: Callable) ->
_T = TypeVar("_T") _T = TypeVar("_T")
@deprecated(info="no longer required, and now a no-op")
def without_qt5_compat_wrapper(cls: _T) -> _T: def without_qt5_compat_wrapper(cls: _T) -> _T:
"""Remove Qt5 compat wrapper from Qt class, if active.
Only needed for a few Qt APIs that deal with QVariants."""
if fn := getattr(cls, "_without_compat_wrapper", None):
return fn()
else:
return cls return cls

View file

@ -1,22 +0,0 @@
# Copyright: Ankitects Pty Ltd and contributors
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
# make sure not to optimize imports on this file
# pylint: skip-file
"""
PyQt5 imports
"""
from PyQt5.QtCore import * # type: ignore
from PyQt5.QtGui import * # type: ignore
from PyQt5.QtNetwork import QLocalServer, QLocalSocket, QNetworkProxy # type: ignore
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

View file

@ -1,99 +0,0 @@
# 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 audio code
"""
import wave
from collections.abc import Callable
from concurrent.futures import Future
from typing import cast
import aqt
from . import * # isort:skip
from ..sound import Recorder # isort:skip
from ..utils import showWarning # isort:skip
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 ( # type: ignore
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 = 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:
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)

View file

@ -1,411 +0,0 @@
# Copyright: Ankitects Pty Ltd and contributors
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
# type: ignore
# pylint: disable=unused-import
"""
Patches and aliases that provide a PyQt5 PyQt6 compatibility shim for add-ons
"""
import sys
import types
import typing
import PyQt6.QtCore
import PyQt6.QtDBus
import PyQt6.QtGui
import PyQt6.QtNetwork
import PyQt6.QtPrintSupport
import PyQt6.QtWebChannel
import PyQt6.QtWebEngineCore
import PyQt6.QtWebEngineWidgets
import PyQt6.QtWidgets
from anki._legacy import print_deprecation_warning
# Globally alias PyQt5 to PyQt6
# #########################################################################
sys.modules["PyQt5"] = PyQt6
# Need to alias QtCore explicitly as sip otherwise complains about repeat registration
sys.modules["PyQt5.QtCore"] = PyQt6.QtCore
# Need to alias QtWidgets and QtGui explicitly to facilitate patches
sys.modules["PyQt5.QtGui"] = PyQt6.QtGui
sys.modules["PyQt5.QtWidgets"] = PyQt6.QtWidgets
# Needed to maintain import order between QtWebEngineWidgets and QCoreApplication:
sys.modules["PyQt5.QtWebEngineWidgets"] = PyQt6.QtWebEngineWidgets
# Register other aliased top-level Qt modules just to be safe:
sys.modules["PyQt5.QtWebEngineCore"] = PyQt6.QtWebEngineCore
sys.modules["PyQt5.QtWebChannel"] = PyQt6.QtWebChannel
sys.modules["PyQt5.QtNetwork"] = PyQt6.QtNetwork
# Alias sip
sys.modules["sip"] = PyQt6.sip
# Restore QWebEnginePage.view()
# ########################################################################
from PyQt6.QtWebEngineCore import QWebEnginePage
from PyQt6.QtWebEngineWidgets import QWebEngineView
def qwebenginepage_view(page: QWebEnginePage) -> QWebEnginePage:
print_deprecation_warning(
"'QWebEnginePage.view()' is deprecated. "
"Please use 'QWebEngineView.forPage(page)'"
)
return QWebEngineView.forPage(page)
PyQt6.QtWebEngineCore.QWebEnginePage.view = qwebenginepage_view
# Alias removed exec_ methods to exec
# ########################################################################
from PyQt6.QtCore import QCoreApplication, QEventLoop, QThread
from PyQt6.QtGui import QDrag, QGuiApplication
from PyQt6.QtWidgets import QApplication, QDialog, QMenu
# This helper function is needed as aliasing exec_ to exec directly will cause
# an unbound method error, even when wrapped with types.MethodType
def qt_exec_(object, *args, **kwargs):
class_name = object.__class__.__name__
print_deprecation_warning(
f"'{class_name}.exec_()' is deprecated. Please use '{class_name}.exec()'"
)
return object.exec(*args, **kwargs)
QCoreApplication.exec_ = qt_exec_
QEventLoop.exec_ = qt_exec_
QThread.exec_ = qt_exec_
QDrag.exec_ = qt_exec_
QGuiApplication.exec_ = qt_exec_
QApplication.exec_ = qt_exec_
QDialog.exec_ = qt_exec_
QMenu.exec_ = qt_exec_
# Graciously handle removed Qt resource system
# ########################################################################
# Given that add-ons mostly use the Qt resource system to equip UI elements with
# icons which oftentimes are not essential to the core UX , printing a warning
# instead of preventing the add-on from loading seems appropriate.
def qt_resource_system_call(*args, **kwargs):
print_deprecation_warning(
"The Qt resource system no longer works on PyQt6. "
"Use QDir.addSearchPath() or mw.addonManager.setWebExports() instead."
)
PyQt6.QtCore.qRegisterResourceData = qt_resource_system_call
PyQt6.QtCore.qUnregisterResourceData = qt_resource_system_call
# Patch unscoped enums back in, aliasing them to scoped enums
# ########################################################################
PyQt6.QtWidgets.QDockWidget.AllDockWidgetFeatures = (
PyQt6.QtWidgets.QDockWidget.DockWidgetFeature.DockWidgetClosable
| PyQt6.QtWidgets.QDockWidget.DockWidgetFeature.DockWidgetMovable
| PyQt6.QtWidgets.QDockWidget.DockWidgetFeature.DockWidgetFloatable
)
# when we subclass QIcon, icons fail to show when returned by getData()
# in a tableview/treeview, so we need to manually alias these
PyQt6.QtGui.QIcon.Active = PyQt6.QtGui.QIcon.Mode.Active
PyQt6.QtGui.QIcon.Disabled = PyQt6.QtGui.QIcon.Mode.Disabled
PyQt6.QtGui.QIcon.Normal = PyQt6.QtGui.QIcon.Mode.Normal
PyQt6.QtGui.QIcon.Selected = PyQt6.QtGui.QIcon.Mode.Selected
PyQt6.QtGui.QIcon.Off = PyQt6.QtGui.QIcon.State.Off
PyQt6.QtGui.QIcon.On = PyQt6.QtGui.QIcon.State.On
# This is the subset of enums used in all public Anki add-ons as of 2021-10-19.
# Please note that this list is likely to be incomplete as the process used to
# find them probably missed dynamically constructed enums.
# Also, as mostly only public Anki add-ons were taken into consideration,
# some enums in other add-ons might not be included. In those cases please
# consider filing a PR to extend the assignments below.
# Important: These patches are not meant to provide compatibility for all
# add-ons going forward, but simply to maintain support with already
# existing add-ons. Add-on authors should take heed to use scoped enums
# in any future code changes.
# (module, [(type_name, enums)])
_enum_map = (
(
PyQt6.QtCore,
[
("QEvent", ("Type",)),
("QEventLoop", ("ProcessEventsFlag",)),
("QIODevice", ("OpenModeFlag",)),
("QItemSelectionModel", ("SelectionFlag",)),
("QLocale", ("Country", "Language")),
("QMetaType", ("Type",)),
("QProcess", ("ProcessState", "ProcessChannel")),
("QStandardPaths", ("StandardLocation",)),
(
"Qt",
(
"AlignmentFlag",
"ApplicationAttribute",
"ArrowType",
"AspectRatioMode",
"BrushStyle",
"CaseSensitivity",
"CheckState",
"ConnectionType",
"ContextMenuPolicy",
"CursorShape",
"DateFormat",
"DayOfWeek",
"DockWidgetArea",
"FindChildOption",
"FocusPolicy",
"FocusReason",
"GlobalColor",
"HighDpiScaleFactorRoundingPolicy",
"ImageConversionFlag",
"InputMethodHint",
"ItemDataRole",
"ItemFlag",
"KeyboardModifier",
"LayoutDirection",
"MatchFlag",
"Modifier",
"MouseButton",
"Orientation",
"PenCapStyle",
"PenJoinStyle",
"PenStyle",
"ScrollBarPolicy",
"ShortcutContext",
"SortOrder",
"TextElideMode",
"TextFlag",
"TextFormat",
"TextInteractionFlag",
"ToolBarArea",
"ToolButtonStyle",
"TransformationMode",
"WidgetAttribute",
"WindowModality",
"WindowState",
"WindowType",
"Key",
),
),
("QThread", ("Priority",)),
],
),
(PyQt6.QtDBus, [("QDBus", ("CallMode",))]),
(
PyQt6.QtGui,
[
("QAction", ("MenuRole", "ActionEvent")),
("QClipboard", ("Mode",)),
("QColor", ("NameFormat",)),
("QFont", ("Style", "Weight", "StyleHint")),
("QFontDatabase", ("WritingSystem", "SystemFont")),
("QImage", ("Format",)),
("QKeySequence", ("SequenceFormat", "StandardKey")),
("QMovie", ("CacheMode",)),
("QPageLayout", ("Orientation",)),
("QPageSize", ("PageSizeId",)),
("QPainter", ("RenderHint",)),
("QPalette", ("ColorRole", "ColorGroup")),
("QTextCharFormat", ("UnderlineStyle",)),
("QTextCursor", ("MoveOperation", "MoveMode", "SelectionType")),
("QTextFormat", ("Property",)),
("QTextOption", ("WrapMode",)),
("QValidator", ("State",)),
],
),
(PyQt6.QtNetwork, [("QHostAddress", ("SpecialAddress",))]),
(PyQt6.QtPrintSupport, [("QPrinter", ("Unit",))]),
(
PyQt6.QtWebEngineCore,
[
("QWebEnginePage", ("WebWindowType", "FindFlag", "WebAction")),
("QWebEngineProfile", ("PersistentCookiesPolicy", "HttpCacheType")),
("QWebEngineScript", ("ScriptWorldId", "InjectionPoint")),
("QWebEngineSettings", ("FontSize", "WebAttribute")),
],
),
(
PyQt6.QtWidgets,
[
(
"QAbstractItemView",
(
"CursorAction",
"DropIndicatorPosition",
"ScrollMode",
"EditTrigger",
"SelectionMode",
"SelectionBehavior",
"DragDropMode",
"ScrollHint",
),
),
("QAbstractScrollArea", ("SizeAdjustPolicy",)),
("QAbstractSpinBox", ("ButtonSymbols",)),
("QBoxLayout", ("Direction",)),
("QColorDialog", ("ColorDialogOption",)),
("QComboBox", ("SizeAdjustPolicy", "InsertPolicy")),
("QCompleter", ("CompletionMode",)),
("QDateTimeEdit", ("Section",)),
("QDialog", ("DialogCode",)),
("QDialogButtonBox", ("StandardButton", "ButtonRole")),
("QDockWidget", ("DockWidgetFeature",)),
("QFileDialog", ("Option", "FileMode", "AcceptMode", "DialogLabel")),
("QFormLayout", ("FieldGrowthPolicy", "ItemRole")),
("QFrame", ("Shape", "Shadow")),
("QGraphicsItem", ("GraphicsItemFlag",)),
("QGraphicsPixmapItem", ("ShapeMode",)),
("QGraphicsView", ("ViewportAnchor", "DragMode")),
("QHeaderView", ("ResizeMode",)),
("QLayout", ("SizeConstraint",)),
("QLineEdit", ("EchoMode",)),
(
"QListView",
("Flow", "BrowserLayout", "ResizeMode", "Movement", "ViewMode"),
),
("QListWidgetItem", ("ItemType",)),
("QMessageBox", ("StandardButton", "Icon", "ButtonRole")),
("QPlainTextEdit", ("LineWrapMode",)),
("QProgressBar", ("Direction",)),
("QRubberBand", ("Shape",)),
("QSizePolicy", ("ControlType", "Policy")),
("QSlider", ("TickPosition",)),
(
"QStyle",
(
"SubElement",
"ComplexControl",
"StandardPixmap",
"ControlElement",
"PixelMetric",
"StateFlag",
"SubControl",
),
),
("QSystemTrayIcon", ("MessageIcon", "ActivationReason")),
("QTabBar", ("ButtonPosition",)),
("QTabWidget", ("TabShape", "TabPosition")),
("QTextEdit", ("LineWrapMode",)),
("QToolButton", ("ToolButtonPopupMode",)),
("QWizard", ("WizardStyle", "WizardOption")),
],
),
)
_renamed_enum_cases = {
"QComboBox": {
"AdjustToMinimumContentsLength": "AdjustToMinimumContentsLengthWithIcon"
},
"QDialogButtonBox": {"No": "NoButton"},
"QPainter": {"HighQualityAntialiasing": "Antialiasing"},
"QPalette": {"Background": "Window", "Foreground": "WindowText"},
"Qt": {"MatchRegExp": "MatchRegularExpression", "MidButton": "MiddleButton"},
}
# This works by wrapping each enum-containing Qt class (eg QAction) in a proxy.
# When an attribute is missing from the underlying Qt class, __getattr__ is
# called, and we try fetching the attribute from each of the declared enums
# for that module. If a match is found, a deprecation warning is printed.
#
# Looping through enumerations is not particularly efficient on a large type like
# Qt, but we only pay the cost when an attribute is not found. In the worst case,
# it's about 50ms per 1000 failed lookups on the Qt module.
def _instrument_type(
module: types.ModuleType, type_name: str, enums: list[str]
) -> None:
type = getattr(module, type_name)
renamed_attrs = _renamed_enum_cases.get(type_name, {})
class QtClassProxyType(type.__class__):
def __getattr__(cls, provided_name): # pylint: disable=no-self-argument
# we know this is not an enum
if provided_name == "__pyqtSignature__":
raise AttributeError
name = renamed_attrs.get(provided_name) or provided_name
for enum_name in enums:
enum = getattr(type, enum_name)
try:
val = getattr(enum, name)
except AttributeError:
continue
print_deprecation_warning(
f"'{type_name}.{provided_name}' will stop working. Please use '{type_name}.{enum_name}.{name}' instead."
)
return val
return getattr(type, name)
class QtClassProxy(
type, metaclass=QtClassProxyType
): # pylint: disable=invalid-metaclass
@staticmethod
def _without_compat_wrapper():
return type
setattr(module, type_name, QtClassProxy)
for module, type_to_enum_list in _enum_map:
for type_name, enums in type_to_enum_list:
_instrument_type(module, type_name, enums)
# Alias classes shifted between QtWidgets and QtGui
##########################################################################
PyQt6.QtWidgets.QAction = PyQt6.QtGui.QAction
PyQt6.QtWidgets.QActionGroup = PyQt6.QtGui.QActionGroup
PyQt6.QtWidgets.QShortcut = PyQt6.QtGui.QShortcut
# Alias classes shifted between QtWebEngineWidgets and QtWebEngineCore
##########################################################################
PyQt6.QtWebEngineWidgets.QWebEnginePage = PyQt6.QtWebEngineCore.QWebEnginePage
PyQt6.QtWebEngineWidgets.QWebEngineHistory = PyQt6.QtWebEngineCore.QWebEngineHistory
PyQt6.QtWebEngineWidgets.QWebEngineProfile = PyQt6.QtWebEngineCore.QWebEngineProfile
PyQt6.QtWebEngineWidgets.QWebEngineScript = PyQt6.QtWebEngineCore.QWebEngineScript
PyQt6.QtWebEngineWidgets.QWebEngineScriptCollection = (
PyQt6.QtWebEngineCore.QWebEngineScriptCollection
)
PyQt6.QtWebEngineWidgets.QWebEngineClientCertificateSelection = (
PyQt6.QtWebEngineCore.QWebEngineClientCertificateSelection
)
PyQt6.QtWebEngineWidgets.QWebEngineSettings = PyQt6.QtWebEngineCore.QWebEngineSettings
PyQt6.QtWebEngineWidgets.QWebEngineFullScreenRequest = (
PyQt6.QtWebEngineCore.QWebEngineFullScreenRequest
)
PyQt6.QtWebEngineWidgets.QWebEngineContextMenuData = (
PyQt6.QtWebEngineCore.QWebEngineContextMenuRequest
)
PyQt6.QtWebEngineWidgets.QWebEngineDownloadItem = (
PyQt6.QtWebEngineCore.QWebEngineDownloadRequest
)
# Aliases for other miscellaneous class changes
##########################################################################
PyQt6.QtCore.QRegExp = PyQt6.QtCore.QRegularExpression
# Mock the removed PyQt5.Qt module
##########################################################################
sys.modules["PyQt5.Qt"] = sys.modules["aqt.qt"]
# support 'from PyQt5 import Qt', as it's an alias to PyQt6
PyQt6.Qt = sys.modules["aqt.qt"]

View file

@ -772,7 +772,6 @@ class RecordDialog(QDialog):
saveGeom(self, "audioRecorder2") saveGeom(self, "audioRecorder2")
def _start_recording(self) -> None: def _start_recording(self) -> None:
if qtmajor > 5:
if macos_helper and platform.machine() == "arm64": if macos_helper and platform.machine() == "arm64":
self._recorder = NativeMacRecorder( self._recorder = NativeMacRecorder(
namedtmp("rec.wav"), namedtmp("rec.wav"),
@ -781,10 +780,6 @@ class RecordDialog(QDialog):
self._recorder = QtAudioInputRecorder( self._recorder = QtAudioInputRecorder(
namedtmp("rec.wav"), self.mw, self._parent namedtmp("rec.wav"), self.mw, self._parent
) )
else:
from aqt.qt.qt5_audio import QtAudioInputRecorder as Qt5Recorder
self._recorder = Qt5Recorder(namedtmp("rec.wav"), self.mw, self._parent)
self._recorder.start(self._start_timer) self._recorder.start(self._start_timer)
def _start_timer(self) -> None: def _start_timer(self) -> None:

View file

@ -6,16 +6,10 @@ from __future__ import annotations
import io import io
import re import re
import sys import sys
from dataclasses import dataclass
from pathlib import Path from pathlib import Path
try: from PyQt6.uic import compileUi
from PyQt6.uic import compileUi
except ImportError:
# ARM64 Linux builds may not have access to PyQt6, and may have aliased
# it to PyQt5. We allow fallback, but the _qt6.py files will not be valid.
from PyQt5.uic import compileUi # type: ignore
from dataclasses import dataclass
def compile(ui_file: str | Path) -> str: def compile(ui_file: str | Path) -> str:
@ -53,21 +47,9 @@ def with_fixes_for_qt6(code: str) -> str:
return "\n".join(outlines) return "\n".join(outlines)
def with_fixes_for_qt5(code: str) -> str:
code = code.replace(
"from PyQt5 import QtCore, QtGui, QtWidgets",
"from PyQt5 import QtCore, QtGui, QtWidgets\nfrom aqt.utils import tr\n",
)
code = code.replace("Qt6", "Qt5")
code = code.replace("QtGui.QAction", "QtWidgets.QAction")
code = code.replace("import icons_rc", "")
return code
@dataclass @dataclass
class UiFileAndOutputs: class UiFileAndOutputs:
ui_file: Path ui_file: Path
qt5_file: str
qt6_file: str qt6_file: str
@ -82,7 +64,6 @@ def get_files() -> list[UiFileAndOutputs]:
out.append( out.append(
UiFileAndOutputs( UiFileAndOutputs(
ui_file=path, ui_file=path,
qt5_file=outpath.replace(".ui", "_qt5.py"),
qt6_file=outpath.replace(".ui", "_qt6.py"), qt6_file=outpath.replace(".ui", "_qt6.py"),
) )
) )
@ -93,8 +74,5 @@ if __name__ == "__main__":
for entry in get_files(): for entry in get_files():
stock = compile(entry.ui_file) stock = compile(entry.ui_file)
for_qt6 = with_fixes_for_qt6(stock) for_qt6 = with_fixes_for_qt6(stock)
for_qt5 = with_fixes_for_qt5(for_qt6)
with open(entry.qt5_file, "w") as file:
file.write(for_qt5)
with open(entry.qt6_file, "w") as file: with open(entry.qt6_file, "w") as file:
file.write(for_qt6) file.write(for_qt6)