Anki/qt/aqt/qt/qt5_compat.py
Matthias Metelka 52f52724fa
Add orientation toggle to browser view menu (#2074)
* Use horizontal orientation on browser splitter by default

* Add View menu action to toggle browser orientation

* Add shortcut for toggleOrientation action

based on the most popular add-on.

* Try to fix typing issue

* Make orientation respond to aspect ratio

aspect ratio < 1 means vertical orientation, >= 1 horizontal

* Implement three-way switch for browser orientation

* Fix typing

* Add separator before QWidgetAction

* Use submenu instead of widget and adjust enum

* Add accelerators; move non-accelerator strings into separate .ftl (dae)

* Move BrowserLayout to its own file (dae)
2022-09-20 12:56:59 +10:00

411 lines
15 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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"]