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![];
for path in ui_files.resolve() {
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"));
}
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 your glibc version is 2.35+ on AMD64 or 2.39+ on ARM64, you can skip the rest of this section.
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:
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:
'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
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
```
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
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):
def nativeEventFilter(
self, eventType: Any, message: Any
) -> tuple[bool, sip.voidptr | None]:
) -> tuple[bool, Any | None]:
if eventType == "windows_generic_MSG":
import ctypes.wintypes
@ -376,6 +376,8 @@ class AnkiApp(QApplication):
def onRecv(self) -> None:
sock = self._srv.nextPendingConnection()
if sock is None:
return
if not sock.waitForReadyRead(self.TMOUT):
sys.stderr.write(sock.errorString())
return
@ -406,14 +408,12 @@ class AnkiApp(QApplication):
QRadioButton,
QMenu,
QSlider,
# classes with PyQt5 compatibility proxy
without_qt5_compat_wrapper(QToolButton),
without_qt5_compat_wrapper(QTabBar),
QToolButton,
QTabBar,
)
if evt.type() in [QEvent.Type.Enter, QEvent.Type.HoverEnter]:
if (isinstance(src, pointer_classes) and src.isEnabled()) or (
isinstance(src, without_qt5_compat_wrapper(QComboBox))
and not src.isEditable()
isinstance(src, QComboBox) and not src.isEditable()
):
self.setOverrideCursor(QCursor(Qt.CursorShape.PointingHandCursor))
else:
@ -525,15 +525,12 @@ def setupGL(pm: aqt.profiles.ProfileManager) -> None:
QQuickWindow.setGraphicsApi(QSGRendererInterface.GraphicsApi.OpenGL)
elif driver in (VideoDriver.Software, VideoDriver.ANGLE):
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.
os.environ["QT_OPENGL"] = driver.value
elif is_mac:
QCoreApplication.setAttribute(Qt.ApplicationAttribute.AA_UseSoftwareOpenGL)
elif is_lin:
# Qt5 only
os.environ["QT_XCB_FORCE_SOFTWARE_OPENGL"] = "1"
# Required on Qt6
if "QTWEBENGINE_CHROMIUM_FLAGS" not in os.environ:
os.environ["QTWEBENGINE_CHROMIUM_FLAGS"] = "--disable-gpu"
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:
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
QCoreApplication.setApplicationName("Anki")
QGuiApplication.setDesktopFileName("anki")

View file

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

View file

@ -382,10 +382,7 @@ class Table:
hh.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
self._restore_header()
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)
# Slots
@ -495,12 +492,6 @@ class Table:
if checked:
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:
column = self._model.column_at_section(section)
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.qt import qtmajor
if qtmajor > 5 or TYPE_CHECKING:
from _aqt.forms.about_qt6 import *
else:
from _aqt.forms.about_qt5 import * # type: ignore
from _aqt.forms.about_qt6 import *

View file

@ -1,8 +1 @@
from typing import TYPE_CHECKING
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
from _aqt.forms.addcards_qt6 import *

View file

@ -1,8 +1 @@
from typing import TYPE_CHECKING
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
from _aqt.forms.addfield_qt6 import *

View file

@ -1,8 +1 @@
from typing import TYPE_CHECKING
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
from _aqt.forms.addmodel_qt6 import *

View file

@ -1,8 +1 @@
from typing import TYPE_CHECKING
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
from _aqt.forms.addonconf_qt6 import *

View file

@ -1,8 +1 @@
from typing import TYPE_CHECKING
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
from _aqt.forms.addons_qt6 import *

View file

@ -1,8 +1 @@
from typing import TYPE_CHECKING
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
from _aqt.forms.browser_qt6 import *

View file

@ -1,8 +1 @@
from typing import TYPE_CHECKING
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
from _aqt.forms.browserdisp_qt6 import *

View file

@ -1,8 +1 @@
from typing import TYPE_CHECKING
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
from _aqt.forms.browseropts_qt6 import *

View file

@ -1,8 +1 @@
from typing import TYPE_CHECKING
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
from _aqt.forms.changemap_qt6 import *

View file

@ -1,8 +1 @@
from typing import TYPE_CHECKING
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
from _aqt.forms.changemodel_qt6 import *

View file

@ -1,8 +1 @@
from typing import TYPE_CHECKING
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
from _aqt.forms.clayout_top_qt6 import *

View file

@ -1,8 +1 @@
from typing import TYPE_CHECKING
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
from _aqt.forms.customstudy_qt6 import *

View file

@ -1,8 +1 @@
from typing import TYPE_CHECKING
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
from _aqt.forms.dconf_qt6 import *

View file

@ -1,8 +1 @@
from typing import TYPE_CHECKING
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
from _aqt.forms.debug_qt6 import *

View file

@ -1,8 +1 @@
from typing import TYPE_CHECKING
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
from _aqt.forms.editcurrent_qt6 import *

View file

@ -1,8 +1 @@
from typing import TYPE_CHECKING
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
from _aqt.forms.edithtml_qt6 import *

View file

@ -1,8 +1 @@
from typing import TYPE_CHECKING
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
from _aqt.forms.emptycards_qt6 import *

View file

@ -1,8 +1 @@
from typing import TYPE_CHECKING
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
from _aqt.forms.exporting_qt6 import *

View file

@ -1,8 +1 @@
from typing import TYPE_CHECKING
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
from _aqt.forms.fields_qt6 import *

View file

@ -1,8 +1 @@
from typing import TYPE_CHECKING
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
from _aqt.forms.filtered_deck_qt6 import *

View file

@ -1,8 +1 @@
from typing import TYPE_CHECKING
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
from _aqt.forms.finddupes_qt6 import *

View file

@ -1,8 +1 @@
from typing import TYPE_CHECKING
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
from _aqt.forms.findreplace_qt6 import *

View file

@ -1,8 +1 @@
from typing import TYPE_CHECKING
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
from _aqt.forms.forget_qt6 import *

View file

@ -1,8 +1 @@
from typing import TYPE_CHECKING
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
from _aqt.forms.getaddons_qt6 import *

View file

@ -1,8 +1 @@
from typing import TYPE_CHECKING
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
from _aqt.forms.importing_qt6 import *

View file

@ -1,8 +1 @@
from typing import TYPE_CHECKING
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
from _aqt.forms.main_qt6 import *

View file

@ -1,8 +1 @@
from typing import TYPE_CHECKING
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
from _aqt.forms.modelopts_qt6 import *

View file

@ -1,8 +1 @@
from typing import TYPE_CHECKING
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
from _aqt.forms.models_qt6 import *

View file

@ -1,8 +1 @@
from typing import TYPE_CHECKING
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
from _aqt.forms.preferences_qt6 import *

View file

@ -1,8 +1 @@
from typing import TYPE_CHECKING
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
from _aqt.forms.preview_qt6 import *

View file

@ -1,8 +1 @@
from typing import TYPE_CHECKING
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
from _aqt.forms.profiles_qt6 import *

View file

@ -1,8 +1 @@
from typing import TYPE_CHECKING
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
from _aqt.forms.progress_qt6 import *

View file

@ -1,8 +1 @@
from typing import TYPE_CHECKING
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
from _aqt.forms.reposition_qt6 import *

View file

@ -1,8 +1 @@
from typing import TYPE_CHECKING
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
from _aqt.forms.setgroup_qt6 import *

View file

@ -1,8 +1 @@
from typing import TYPE_CHECKING
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
from _aqt.forms.setlang_qt6 import *

View file

@ -1,8 +1 @@
from typing import TYPE_CHECKING
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
from _aqt.forms.stats_qt6 import *

View file

@ -1,8 +1 @@
from typing import TYPE_CHECKING
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
from _aqt.forms.studydeck_qt6 import *

View file

@ -1,8 +1 @@
from typing import TYPE_CHECKING
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
from _aqt.forms.synclog_qt6 import *

View file

@ -1,8 +1 @@
from typing import TYPE_CHECKING
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
from _aqt.forms.taglimit_qt6 import *

View file

@ -1,8 +1 @@
from typing import TYPE_CHECKING
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
from _aqt.forms.template_qt6 import *

View file

@ -1,8 +1 @@
from typing import TYPE_CHECKING
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
from _aqt.forms.widgets_qt6 import *

View file

@ -189,11 +189,8 @@ class ProfileManager:
# 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
# old style enums can't be unpickled
return None
else:
return sip._unpickle_type(module, klass, args) # type: ignore

View file

@ -300,8 +300,7 @@ class ProgressManager:
def _closeWin(self) -> None:
# 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):
assert self._win is not None
if self._win and not sip.isdeleted(self._win):
self._win.cancel()
self._win = None
self._shown = 0

View file

@ -11,20 +11,12 @@ import traceback
from collections.abc import Callable
from typing import TypeVar, Union
try:
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 *
from anki._legacy import deprecated
# legacy code depends on these re-exports
from anki.utils import is_mac, is_win
# fix buggy ubuntu12.04 display of language selector
os.environ["LIBOVERLAY_SCROLLBAR"] = "0"
from .qt6 import *
def debug() -> None:
@ -52,7 +44,7 @@ qtminor = _version.minorVersion()
qtpoint = _version.microVersion()
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.")
@ -64,11 +56,6 @@ def qconnect(signal: Callable | pyqtSignal | pyqtBoundSignal, func: Callable) ->
_T = TypeVar("_T")
@deprecated(info="no longer required, and now a no-op")
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,19 +772,14 @@ class RecordDialog(QDialog):
saveGeom(self, "audioRecorder2")
def _start_recording(self) -> None:
if qtmajor > 5:
if macos_helper and platform.machine() == "arm64":
self._recorder = NativeMacRecorder(
namedtmp("rec.wav"),
)
else:
self._recorder = QtAudioInputRecorder(
namedtmp("rec.wav"), self.mw, self._parent
)
if macos_helper and platform.machine() == "arm64":
self._recorder = NativeMacRecorder(
namedtmp("rec.wav"),
)
else:
from aqt.qt.qt5_audio import QtAudioInputRecorder as Qt5Recorder
self._recorder = Qt5Recorder(namedtmp("rec.wav"), self.mw, self._parent)
self._recorder = QtAudioInputRecorder(
namedtmp("rec.wav"), self.mw, self._parent
)
self._recorder.start(self._start_timer)
def _start_timer(self) -> None:

View file

@ -6,16 +6,10 @@ from __future__ import annotations
import io
import re
import sys
from dataclasses import dataclass
from pathlib import Path
try:
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
from PyQt6.uic import compileUi
def compile(ui_file: str | Path) -> str:
@ -53,21 +47,9 @@ def with_fixes_for_qt6(code: str) -> str:
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
class UiFileAndOutputs:
ui_file: Path
qt5_file: str
qt6_file: str
@ -82,7 +64,6 @@ def get_files() -> list[UiFileAndOutputs]:
out.append(
UiFileAndOutputs(
ui_file=path,
qt5_file=outpath.replace(".ui", "_qt5.py"),
qt6_file=outpath.replace(".ui", "_qt6.py"),
)
)
@ -93,8 +74,5 @@ if __name__ == "__main__":
for entry in get_files():
stock = compile(entry.ui_file)
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:
file.write(for_qt6)