mirror of
https://github.com/ankitects/anki.git
synced 2025-09-18 22:12:21 -04:00

* 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)
411 lines
15 KiB
Python
411 lines
15 KiB
Python
# 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"]
|