diff --git trackma/ui/qt/__init__.py trackma/ui/qt/__init__.py index 1df20c2..e1f6a33 100644 --- trackma/ui/qt/__init__.py +++ trackma/ui/qt/__init__.py @@ -39,10 +39,12 @@ def main(): debug = True try: - from PyQt6.QtWidgets import QApplication, QMessageBox + from trackma.ui.qt.compat import QtWidgets, exec_, QT_VERSION + QApplication = QtWidgets.QApplication + QMessageBox = QtWidgets.QMessageBox except ImportError: - print("Couldn't import Qt6 dependencies. " - "Make sure you installed the PyQt6 package.") + print("Couldn't import Qt dependencies. " + "Make sure you installed PyQt4, PyQt5, or PyQt6.") try: from PIL import Image @@ -53,7 +55,9 @@ def main(): app = QApplication(sys.argv) app.setApplicationName("trackma") - app.setDesktopFileName("trackma-qt") + # setDesktopFileName was introduced in Qt 5.7, not available in Qt 4 + if QT_VERSION >= 5: + app.setDesktopFileName("trackma-qt") if os.name == "nt": import ctypes myappid = 'trackma' + utils.VERSION @@ -61,6 +65,22 @@ def main(): try: # keep the variable around to prevent it from being gc'ed main_window = MainWindow(debug) - sys.exit(app.exec()) + ret = exec_(app) + + # Explicit cleanup to avoid PyQt4 crash on exit (especially on macOS) + # This helps prevent segfaults during Python finalization + # Note: PyQt4/SIP may still crash in Py_FinalizeEx on macOS during cleanup + # This is a known SIP/PyQt4 issue with object deletion order + if QT_VERSION == 4: + try: + # Ensure window is closed and deleted before Python cleanup + if main_window: + main_window.close() + del main_window + except: + pass + + sys.exit(ret) except utils.TrackmaFatal as e: - QMessageBox.critical(None, 'Fatal Error', "{0}".format(e), QMessageBox.StandardButton.Ok) + from trackma.ui.qt.compat import QtCompat + QMessageBox.critical(None, 'Fatal Error', "{0}".format(e), QtCompat.Ok()) diff --git trackma/ui/qt/accounts.py trackma/ui/qt/accounts.py index 22a1032..3f313d8 100644 --- trackma/ui/qt/accounts.py +++ trackma/ui/qt/accounts.py @@ -14,12 +14,26 @@ # along with this program. If not, see . # -from PyQt6 import QtCore, QtGui -from PyQt6.QtWidgets import (QAbstractItemView, QCheckBox, QComboBox, QDialog, QDialogButtonBox, QFormLayout, - QHBoxLayout, QHeaderView, QLabel, QLineEdit, QMessageBox, QPushButton, QTableWidget, - QTableWidgetItem, QVBoxLayout) +from trackma.ui.qt.compat import QtCore, QtGui, exec_ +from trackma.ui.qt.compat import QtWidgets +QAbstractItemView = QtWidgets.QAbstractItemView +QCheckBox = QtWidgets.QCheckBox +QComboBox = QtWidgets.QComboBox +QDialog = QtWidgets.QDialog +QDialogButtonBox = QtWidgets.QDialogButtonBox +QFormLayout = QtWidgets.QFormLayout +QHBoxLayout = QtWidgets.QHBoxLayout +QHeaderView = QtWidgets.QHeaderView +QLabel = QtWidgets.QLabel +QLineEdit = QtWidgets.QLineEdit +QMessageBox = QtWidgets.QMessageBox +QPushButton = QtWidgets.QPushButton +QTableWidget = QtWidgets.QTableWidget +QTableWidgetItem = QtWidgets.QTableWidgetItem +QVBoxLayout = QtWidgets.QVBoxLayout from trackma import utils +from trackma.ui.qt.compat import QtCompat class AccountDialog(QDialog): @@ -40,11 +54,11 @@ class AccountDialog(QDialog): # Create list self.table = QTableWidget() self.table.horizontalHeader().setHighlightSections(False) - self.table.setSelectionMode(QAbstractItemView.SelectionMode.SingleSelection) - self.table.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows) - self.table.setEditTriggers(QAbstractItemView.EditTrigger.NoEditTriggers) + self.table.setSelectionMode(QtCompat.SingleSelection()) + self.table.setSelectionBehavior(QtCompat.SelectRows()) + self.table.setEditTriggers(QtCompat.NoEditTriggers()) self.table.verticalHeader().hide() - self.table.setGridStyle(QtCore.Qt.PenStyle.NoPen) + self.table.setGridStyle(QtCompat.NoPen()) self.table.doubleClicked.connect(self.select) self.table.itemSelectionChanged.connect(self.update_selection) @@ -61,11 +75,11 @@ class AccountDialog(QDialog): self.edit_btns.addItem('Delete') self.edit_btns.addItem('Purge') self.edit_btns.setItemData( - 1, 'Change the local password/PIN for this account', QtCore.Qt.ItemDataRole.ToolTipRole) + 1, 'Change the local password/PIN for this account', QtCompat.ToolTipRole()) self.edit_btns.setItemData( - 2, 'Remove this account from Trackma', QtCore.Qt.ItemDataRole.ToolTipRole) + 2, 'Remove this account from Trackma', QtCompat.ToolTipRole()) self.edit_btns.setItemData( - 3, 'Clear local DB for this account', QtCore.Qt.ItemDataRole.ToolTipRole) + 3, 'Clear local DB for this account', QtCompat.ToolTipRole()) self.edit_btns.setCurrentIndex(0) self.edit_btns.blockSignals(False) self.edit_btns.activated.connect(self.s_edit) @@ -137,17 +151,17 @@ class AccountDialog(QDialog): def delete(self): reply = QMessageBox.question( - self, 'Confirmation', 'Do you want to delete the selected account?', QMessageBox.StandardButton.Yes, QMessageBox.StandardButton.No) + self, 'Confirmation', 'Do you want to delete the selected account?', QtCompat.Yes(), QtCompat.No()) - if reply == QMessageBox.StandardButton.Yes: + if reply == QtCompat.Yes(): self.accountman.delete_account(self.selected_account_num) self.rebuild() def purge(self): reply = QMessageBox.question( - self, 'Confirmation', 'Do you want to purge the selected account\'s local data?', QMessageBox.StandardButton.Yes, QMessageBox.StandardButton.No) + self, 'Confirmation', 'Do you want to purge the selected account\'s local data?', QtCompat.Yes(), QtCompat.No()) - if reply == QMessageBox.StandardButton.Yes: + if reply == QtCompat.Yes(): self.accountman.purge_account(self.selected_account_num) self.rebuild() @@ -167,7 +181,7 @@ class AccountDialog(QDialog): self.table.setItem(i, 1, AccountItem( k, account['api'], self.icons.get(account['api']))) - self.table.horizontalHeader().setSectionResizeMode(0, QHeaderView.ResizeMode.Stretch) + self.table.horizontalHeader().setSectionResizeMode(0, QtCompat.Stretch()) def select(self, checked): if not self.selected_account_num: @@ -182,7 +196,7 @@ class AccountDialog(QDialog): self.close() def _error(self, msg): - QMessageBox.critical(self, 'Error', str(msg), QMessageBox.StandardButton.Ok) + QMessageBox.critical(self, 'Error', str(msg), QtCompat.Ok()) class AccountItem(QTableWidgetItem): @@ -230,8 +244,8 @@ class AccountAddDialog(QDialog): formlayout.addRow(self.lbl_password, pin_layout) bottombox = QDialogButtonBox() - bottombox.addButton(QDialogButtonBox.StandardButton.Save) - bottombox.addButton(QDialogButtonBox.StandardButton.Cancel) + bottombox.addButton(QtWidgets.QDialogButtonBox.Save) + bottombox.addButton(QtWidgets.QDialogButtonBox.Cancel) bottombox.accepted.connect(self.validate) bottombox.rejected.connect(self.reject) @@ -242,7 +256,7 @@ class AccountAddDialog(QDialog): if self.edit: self.username.setEnabled(False) self.api.setCurrentIndex( - self.api.findData(api, QtCore.Qt.ItemDataRole.UserRole)) + self.api.findData(api, QtCompat.UserRole())) self.api.setEnabled(False) # Finish layouts @@ -281,25 +295,25 @@ class AccountAddDialog(QDialog): if self.adding_api[2] in [utils.Login.OAUTH, utils.Login.OAUTH_PKCE]: self.lbl_username.setText('Name:') self.lbl_password.setText('PIN:') - self.password.setEchoMode(QLineEdit.EchoMode.Normal) + self.password.setEchoMode(QtCompat.Normal()) self.api_auth.show() self.adding_allow = False else: self.lbl_username.setText('Username:') self.lbl_password.setText('Password:') - self.password.setEchoMode(QLineEdit.EchoMode.Password) + self.password.setEchoMode(QtCompat.Password()) self.api_auth.hide() self.adding_allow = True def _error(self, msg): - QMessageBox.critical(self, 'Error', msg, QMessageBox.StandardButton.Ok) + QMessageBox.critical(self, 'Error', msg, QtCompat.Ok()) @staticmethod def do(parent=None, icons=None, edit=False, username='', password='', api='', extra={}): dialog = AccountAddDialog(parent, icons, edit, username, password, api) - result = dialog.exec() + result = exec_(dialog) - if result == QDialog.DialogCode.Accepted: + if result == QtWidgets.QDialog.Accepted: currentIndex = dialog.api.currentIndex() return ( str(dialog.username.text()), diff --git trackma/ui/qt/add.py trackma/ui/qt/add.py index 60f0530..10f28fa 100644 --- trackma/ui/qt/add.py +++ trackma/ui/qt/add.py @@ -16,13 +16,25 @@ from datetime import date -from PyQt6 import QtCore -from PyQt6.QtWidgets import (QComboBox, QDialog, QDialogButtonBox, QHBoxLayout, QLineEdit, QMessageBox, QPushButton, - QRadioButton, QSpinBox, QSplitter, QStackedWidget, QVBoxLayout) +from trackma.ui.qt.compat import QtCore +from trackma.ui.qt.compat import QtWidgets +QComboBox = QtWidgets.QComboBox +QDialog = QtWidgets.QDialog +QDialogButtonBox = QtWidgets.QDialogButtonBox +QHBoxLayout = QtWidgets.QHBoxLayout +QLineEdit = QtWidgets.QLineEdit +QMessageBox = QtWidgets.QMessageBox +QPushButton = QtWidgets.QPushButton +QRadioButton = QtWidgets.QRadioButton +QSpinBox = QtWidgets.QSpinBox +QSplitter = QtWidgets.QSplitter +QStackedWidget = QtWidgets.QStackedWidget +QVBoxLayout = QtWidgets.QVBoxLayout from trackma import utils from trackma.ui.qt.details import DetailsDialog from trackma.ui.qt.widgets import AddCardView, AddTableDetailsView +from trackma.ui.qt.compat import QtCompat class AddDialog(QDialog): @@ -63,7 +75,7 @@ class AddDialog(QDialog): top_layout.addWidget(self.search_rad) top_layout.addWidget(self.search_txt) else: - top_layout.setAlignment(QtCore.Qt.AlignmentFlag.AlignRight) + top_layout.setAlignment(QtCompat.AlignRight()) top_layout.addWidget(self.search_btn) @@ -91,10 +103,10 @@ class AddDialog(QDialog): filters_layout.addWidget(self.season_combo) filters_layout.addWidget(self.season_year) - filters_layout.setAlignment(QtCore.Qt.AlignmentFlag.AlignLeft) + filters_layout.setAlignment(QtCompat.AlignLeft()) filters_layout.addWidget(QSplitter()) else: - filters_layout.setAlignment(QtCore.Qt.AlignmentFlag.AlignRight) + filters_layout.setAlignment(QtCompat.AlignRight()) view_combo = QComboBox() view_combo.addItem('Card view') @@ -121,9 +133,9 @@ class AddDialog(QDialog): # self.set_results([{'id': 1, 'title': 'Hola', 'image': 'https://omaera.org/icon.png'}]) bottom_buttons = QDialogButtonBox() - bottom_buttons.addButton("Cancel", QDialogButtonBox.ButtonRole.RejectRole) + bottom_buttons.addButton("Cancel", QtCompat.RejectRole()) self.add_btn = bottom_buttons.addButton( - "Add", QDialogButtonBox.ButtonRole.AcceptRole) + "Add", QtCompat.AcceptRole()) self.add_btn.setEnabled(False) bottom_buttons.accepted.connect(self.s_add) bottom_buttons.rejected.connect(self.close) diff --git trackma/ui/qt/compat.py trackma/ui/qt/compat.py new file mode 100644 index 0000000..e0293cb --- /dev/null +++ trackma/ui/qt/compat.py @@ -0,0 +1,927 @@ +# This file is part of Trackma. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# + +""" +PyQt compatibility layer for supporting PyQt4, PyQt5, and PyQt6. +""" + +try: + from PyQt6 import QtCore, QtGui, QtWidgets, QtNetwork + from PyQt6.QtCore import Qt, QVariant + from PyQt6.QtGui import QAction, QActionGroup + QT_VERSION = 6 + + # Enum access helpers for PyQt6 + class QtCompat: + @staticmethod + def AlignCenter(): + return Qt.AlignmentFlag.AlignCenter + + @staticmethod + def AlignTop(): + return Qt.AlignmentFlag.AlignTop + + @staticmethod + def AlignVCenter(): + return Qt.AlignmentFlag.AlignVCenter + + @staticmethod + def AlignHCenter(): + return Qt.AlignmentFlag.AlignHCenter + + @staticmethod + def Key_Delete(): + return Qt.Key.Key_Delete + + @staticmethod + def AscendingOrder(): + return Qt.SortOrder.AscendingOrder + + @staticmethod + def CaseSensitive(): + return Qt.CaseSensitivity.CaseSensitive + + @staticmethod + def CaseInsensitive(): + return Qt.CaseSensitivity.CaseInsensitive + + @staticmethod + def Horizontal(): + return Qt.Orientation.Horizontal + + @staticmethod + def MiddleButton(): + return Qt.MouseButton.MiddleButton + + @staticmethod + def CustomContextMenu(): + return Qt.ContextMenuPolicy.CustomContextMenu + + @staticmethod + def NoPen(): + return Qt.PenStyle.NoPen + + @staticmethod + def DisplayRole(): + return Qt.ItemDataRole.DisplayRole + + @staticmethod + def DecorationRole(): + return Qt.ItemDataRole.DecorationRole + + @staticmethod + def BackgroundRole(): + return Qt.ItemDataRole.BackgroundRole + + @staticmethod + def TextAlignmentRole(): + return Qt.ItemDataRole.TextAlignmentRole + + @staticmethod + def ToolTipRole(): + return Qt.ItemDataRole.ToolTipRole + + @staticmethod + def EditRole(): + return Qt.ItemDataRole.EditRole + + @staticmethod + def UserRole(): + return Qt.ItemDataRole.UserRole + + @staticmethod + def ItemIsSelectable(): + return Qt.ItemFlag.ItemIsSelectable + + @staticmethod + def ItemIsEnabled(): + return Qt.ItemFlag.ItemIsEnabled + + @staticmethod + def ItemNeverHasChildren(): + return Qt.ItemFlag.ItemNeverHasChildren + + @staticmethod + def ItemIsEditable(): + return Qt.ItemFlag.ItemIsEditable + + @staticmethod + def KeepAspectRatio(): + return Qt.AspectRatioMode.KeepAspectRatio + + @staticmethod + def SmoothTransformation(): + return Qt.TransformationMode.SmoothTransformation + + @staticmethod + def ElideRight(): + return Qt.TextElideMode.ElideRight + + @staticmethod + def RichText(): + return Qt.TextFormat.RichText + + @staticmethod + def TextBrowserInteraction(): + return Qt.TextInteractionFlag.TextBrowserInteraction + + @staticmethod + def Light(): + return QtGui.QPalette.ColorRole.Light + + @staticmethod + def WA_ShowWithoutActivating(): + return Qt.WidgetAttribute.WA_ShowWithoutActivating + + @staticmethod + def Question(): + return QtWidgets.QMessageBox.Icon.Question + + @staticmethod + def Ok(): + return QtWidgets.QMessageBox.StandardButton.Ok + + @staticmethod + def Yes(): + return QtWidgets.QMessageBox.StandardButton.Yes + + @staticmethod + def No(): + return QtWidgets.QMessageBox.StandardButton.No + + @staticmethod + def Cancel(): + return QtWidgets.QMessageBox.StandardButton.Cancel + + @staticmethod + def State_On(): + return QtWidgets.QStyle.StateFlag.State_On + + @staticmethod + def State_NoChange(): + return QtWidgets.QStyle.StateFlag.State_NoChange + + @staticmethod + def State_Off(): + return QtWidgets.QStyle.StateFlag.State_Off + + @staticmethod + def PE_IndicatorMenuCheckMark(): + return QtWidgets.QStyle.PrimitiveElement.PE_IndicatorMenuCheckMark + + @staticmethod + def GlobalColor_transparent(): + return Qt.GlobalColor.transparent + + @staticmethod + def Stretch(): + return QtWidgets.QHeaderView.ResizeMode.Stretch + + @staticmethod + def Fixed(): + return QtWidgets.QHeaderView.ResizeMode.Fixed + + @staticmethod + def SingleSelection(): + return QtWidgets.QAbstractItemView.SelectionMode.SingleSelection + + @staticmethod + def SelectRows(): + return QtWidgets.QAbstractItemView.SelectionBehavior.SelectRows + + @staticmethod + def AllEditTriggers(): + return QtWidgets.QAbstractItemView.EditTrigger.AllEditTriggers + + @staticmethod + def NoEditTriggers(): + return QtWidgets.QAbstractItemView.EditTrigger.NoEditTriggers + + @staticmethod + def Trigger(): + return QtWidgets.QSystemTrayIcon.ActivationReason.Trigger + + @staticmethod + def LeftToRight(): + return QtWidgets.QListView.Flow.LeftToRight + + # PyQt6 enum compatibility wrappers + @staticmethod + def AlternateBase(): + return QtGui.QPalette.ColorRole.AlternateBase + + @staticmethod + def WindowText(): + return QtGui.QPalette.ColorRole.WindowText + + @staticmethod + def Window(): + return QtGui.QPalette.ColorRole.Window + + @staticmethod + def Bold(): + return QtGui.QFont.Weight.Bold + + @staticmethod + def Password(): + return QtWidgets.QLineEdit.EchoMode.Password + + @staticmethod + def Normal(): + return QtWidgets.QLineEdit.EchoMode.Normal + + @staticmethod + def RejectRole(): + return QtWidgets.QDialogButtonBox.ButtonRole.RejectRole + + @staticmethod + def AcceptRole(): + return QtWidgets.QDialogButtonBox.ButtonRole.AcceptRole + + @staticmethod + def CompositionMode_Overlay(): + return QtGui.QPainter.CompositionMode.CompositionMode_Overlay + + @staticmethod + def CompositionMode_Source(): + return QtGui.QPainter.CompositionMode.CompositionMode_Source + + @staticmethod + def CompositionMode_SourceOver(): + return QtGui.QPainter.CompositionMode.CompositionMode_SourceOver + + @staticmethod + def CompositionMode_SourceAtop(): + return QtGui.QPainter.CompositionMode.CompositionMode_SourceAtop + + @staticmethod + def CE_ProgressBar(): + return QtWidgets.QStyle.ControlElement.CE_ProgressBar + + @staticmethod + def CE_ProgressBarLabel(): + return QtWidgets.QStyle.ControlElement.CE_ProgressBarLabel + + @staticmethod + def TextWordWrap(): + return Qt.TextFlag.TextWordWrap + + @staticmethod + def State_Selected(): + return QtWidgets.QStyle.StateFlag.State_Selected + + @staticmethod + def AlignRight(): + return Qt.AlignmentFlag.AlignRight + + @staticmethod + def AlignLeft(): + return Qt.AlignmentFlag.AlignLeft + + @staticmethod + def NoFocus(): + return Qt.FocusPolicy.NoFocus + +except ImportError: + try: + from PyQt5 import QtCore, QtGui, QtWidgets, QtNetwork + from PyQt5.QtCore import Qt, QVariant + from PyQt5.QtWidgets import QAction, QActionGroup + QT_VERSION = 5 + + # PyQt5 uses the same enum syntax as PyQt4 + class QtCompat: + @staticmethod + def AlignCenter(): + return Qt.AlignCenter + + @staticmethod + def AlignTop(): + return Qt.AlignTop + + @staticmethod + def AlignVCenter(): + return Qt.AlignVCenter + + @staticmethod + def AlignHCenter(): + return Qt.AlignHCenter + + @staticmethod + def Key_Delete(): + return Qt.Key_Delete + + @staticmethod + def AscendingOrder(): + return Qt.AscendingOrder + + @staticmethod + def CaseSensitive(): + return Qt.CaseSensitive + + @staticmethod + def CaseInsensitive(): + return Qt.CaseInsensitive + + @staticmethod + def Horizontal(): + return Qt.Horizontal + + @staticmethod + def MiddleButton(): + return Qt.MiddleButton + + @staticmethod + def CustomContextMenu(): + return Qt.CustomContextMenu + + @staticmethod + def NoPen(): + return Qt.NoPen + + @staticmethod + def DisplayRole(): + return Qt.DisplayRole + + @staticmethod + def DecorationRole(): + return Qt.DecorationRole + + @staticmethod + def BackgroundRole(): + return Qt.BackgroundRole + + @staticmethod + def TextAlignmentRole(): + return Qt.TextAlignmentRole + + @staticmethod + def ToolTipRole(): + return Qt.ToolTipRole + + @staticmethod + def EditRole(): + return Qt.EditRole + + @staticmethod + def UserRole(): + return Qt.UserRole + + @staticmethod + def ItemIsSelectable(): + return Qt.ItemIsSelectable + + @staticmethod + def ItemIsEnabled(): + return Qt.ItemIsEnabled + + @staticmethod + def ItemNeverHasChildren(): + return Qt.ItemNeverHasChildren + + @staticmethod + def ItemIsEditable(): + return Qt.ItemIsEditable + + @staticmethod + def KeepAspectRatio(): + return Qt.KeepAspectRatio + + @staticmethod + def SmoothTransformation(): + return Qt.SmoothTransformation + + @staticmethod + def ElideRight(): + return Qt.ElideRight + + @staticmethod + def RichText(): + return Qt.RichText + + @staticmethod + def TextBrowserInteraction(): + return Qt.TextBrowserInteraction + + @staticmethod + def Light(): + return QtGui.QPalette.Light + + @staticmethod + def WA_ShowWithoutActivating(): + return Qt.WA_ShowWithoutActivating + + @staticmethod + def Question(): + return QtWidgets.QMessageBox.Question + + @staticmethod + def Ok(): + return QtWidgets.QMessageBox.Ok + + @staticmethod + def Yes(): + return QtWidgets.QMessageBox.Yes + + @staticmethod + def No(): + return QtWidgets.QMessageBox.No + + @staticmethod + def Cancel(): + return QtWidgets.QMessageBox.Cancel + + @staticmethod + def State_On(): + return QtWidgets.QStyle.State_On + + @staticmethod + def State_NoChange(): + return QtWidgets.QStyle.State_NoChange + + @staticmethod + def State_Off(): + return QtWidgets.QStyle.State_Off + + @staticmethod + def PE_IndicatorMenuCheckMark(): + return QtWidgets.QStyle.PE_IndicatorMenuCheckMark + + @staticmethod + def GlobalColor_transparent(): + return Qt.transparent + + @staticmethod + def Stretch(): + return QtWidgets.QHeaderView.Stretch + + @staticmethod + def Fixed(): + return QtWidgets.QHeaderView.Fixed + + @staticmethod + def SingleSelection(): + return QtWidgets.QAbstractItemView.SingleSelection + + @staticmethod + def SelectRows(): + return QtWidgets.QAbstractItemView.SelectRows + + @staticmethod + def AllEditTriggers(): + return QtWidgets.QAbstractItemView.AllEditTriggers + + @staticmethod + def NoEditTriggers(): + return QtWidgets.QAbstractItemView.NoEditTriggers + + @staticmethod + def Trigger(): + return QtWidgets.QSystemTrayIcon.Trigger + + @staticmethod + def LeftToRight(): + return QtWidgets.QListView.LeftToRight + + # PyQt6 enum compatibility wrappers + @staticmethod + def AlternateBase(): + return QtGui.QPalette.AlternateBase + + @staticmethod + def WindowText(): + return QtGui.QPalette.WindowText + + @staticmethod + def Window(): + return QtGui.QPalette.Window + + @staticmethod + def Bold(): + return QtGui.QFont.Bold + + @staticmethod + def Password(): + return QtWidgets.QLineEdit.Password + + @staticmethod + def Normal(): + return QtWidgets.QLineEdit.Normal + + @staticmethod + def RejectRole(): + return QtWidgets.QDialogButtonBox.RejectRole + + @staticmethod + def AcceptRole(): + return QtWidgets.QDialogButtonBox.AcceptRole + + @staticmethod + def CompositionMode_Overlay(): + return QtGui.QPainter.CompositionMode_Overlay + + @staticmethod + def CompositionMode_Source(): + return QtGui.QPainter.CompositionMode_Source + + @staticmethod + def CompositionMode_SourceOver(): + return QtGui.QPainter.CompositionMode_SourceOver + + @staticmethod + def CompositionMode_SourceAtop(): + return QtGui.QPainter.CompositionMode_SourceAtop + + @staticmethod + def CE_ProgressBar(): + return QtWidgets.QStyle.CE_ProgressBar + + @staticmethod + def CE_ProgressBarLabel(): + return QtWidgets.QStyle.CE_ProgressBarLabel + + @staticmethod + def TextWordWrap(): + return Qt.TextWordWrap + + @staticmethod + def State_Selected(): + return QtWidgets.QStyle.State_Selected + + @staticmethod + def AlignRight(): + return Qt.AlignRight + + @staticmethod + def AlignLeft(): + return Qt.AlignLeft + + @staticmethod + def NoFocus(): + return Qt.NoFocus + + except ImportError: + from PyQt4 import QtCore, QtGui, QtNetwork + from PyQt4.QtCore import Qt, QVariant + from PyQt4.QtGui import QAction, QActionGroup + QT_VERSION = 4 + + # In PyQt4, QtWidgets is part of QtGui + QtWidgets = QtGui + + # In PyQt4, QSortFilterProxyModel is in QtGui, not QtCore + # Add it to QtCore for compatibility + QtCore.QSortFilterProxyModel = QtGui.QSortFilterProxyModel + + # PyQt4 uses specific Qt wrapper types (ItemFlags, Alignment) that need proper construction + # When ORing flags together, we need to ensure the result is the right type + # Store the Qt types for use in QtCompat methods + _ItemFlags = QtCore.Qt.ItemFlags + _Alignment = QtCore.Qt.Alignment + + # In PyQt4, several QHeaderView methods were renamed in Qt 5 + # Create proper wrapper methods for compatibility + _original_setMovable = QtGui.QHeaderView.setMovable + _original_setResizeMode = QtGui.QHeaderView.setResizeMode + + def setSectionsMovable(self, movable): + return _original_setMovable(self, movable) + + def setSectionResizeMode(self, *args): + return _original_setResizeMode(self, *args) + + QtGui.QHeaderView.setSectionsMovable = setSectionsMovable + QtGui.QHeaderView.setSectionResizeMode = setSectionResizeMode + + # In PyQt4, QLineEdit.setClearButtonEnabled doesn't exist (introduced in Qt 5.2) + # Add a no-op method for compatibility + if not hasattr(QtGui.QLineEdit, 'setClearButtonEnabled'): + def setClearButtonEnabled(self, enable): + # No-op for Qt 4 - this feature doesn't exist + pass + QtGui.QLineEdit.setClearButtonEnabled = setClearButtonEnabled + + # In PyQt4, QIcon.fromTheme doesn't properly support fallback icons + # Wrap it to ensure fallback works correctly + _original_fromTheme = QtGui.QIcon.fromTheme + @staticmethod + def fromTheme_compat(name, fallback=None): + icon = _original_fromTheme(name) + # If theme icon is null/empty and we have a fallback, use it + if fallback and icon.isNull(): + return fallback + return icon + QtGui.QIcon.fromTheme = fromTheme_compat + + # PyQt4 uses the same enum syntax as PyQt5 + class QtCompat: + @staticmethod + def AlignCenter(): + return Qt.AlignCenter + + @staticmethod + def AlignTop(): + return Qt.AlignTop + + @staticmethod + def AlignVCenter(): + return Qt.AlignVCenter + + @staticmethod + def AlignHCenter(): + return Qt.AlignHCenter + + @staticmethod + def Key_Delete(): + return Qt.Key_Delete + + @staticmethod + def AscendingOrder(): + return Qt.AscendingOrder + + @staticmethod + def CaseSensitive(): + return Qt.CaseSensitive + + @staticmethod + def CaseInsensitive(): + return Qt.CaseInsensitive + + @staticmethod + def Horizontal(): + return Qt.Horizontal + + @staticmethod + def MiddleButton(): + return Qt.MiddleButton + + @staticmethod + def CustomContextMenu(): + return Qt.CustomContextMenu + + @staticmethod + def NoPen(): + return Qt.NoPen + + @staticmethod + def DisplayRole(): + return Qt.DisplayRole + + @staticmethod + def DecorationRole(): + return Qt.DecorationRole + + @staticmethod + def BackgroundRole(): + return Qt.BackgroundRole + + @staticmethod + def TextAlignmentRole(): + return Qt.TextAlignmentRole + + @staticmethod + def ToolTipRole(): + return Qt.ToolTipRole + + @staticmethod + def EditRole(): + return Qt.EditRole + + @staticmethod + def UserRole(): + return Qt.UserRole + + @staticmethod + def ItemIsSelectable(): + return Qt.ItemIsSelectable + + @staticmethod + def ItemIsEnabled(): + return Qt.ItemIsEnabled + + @staticmethod + def ItemNeverHasChildren(): + # ItemNeverHasChildren was introduced in Qt 5.0, not available in Qt 4.x + # Return 0 (no flag) for PyQt4 compatibility + return 0 + + @staticmethod + def ItemIsEditable(): + return Qt.ItemIsEditable + + @staticmethod + def KeepAspectRatio(): + return Qt.KeepAspectRatio + + @staticmethod + def SmoothTransformation(): + return Qt.SmoothTransformation + + @staticmethod + def ElideRight(): + return Qt.ElideRight + + @staticmethod + def RichText(): + return Qt.RichText + + @staticmethod + def TextBrowserInteraction(): + return Qt.TextBrowserInteraction + + @staticmethod + def Light(): + return QtGui.QPalette.Light + + @staticmethod + def WA_ShowWithoutActivating(): + return Qt.WA_ShowWithoutActivating + + @staticmethod + def Question(): + return QtGui.QMessageBox.Question + + @staticmethod + def Ok(): + return QtGui.QMessageBox.Ok + + @staticmethod + def Yes(): + return QtGui.QMessageBox.Yes + + @staticmethod + def No(): + return QtGui.QMessageBox.No + + @staticmethod + def Cancel(): + return QtGui.QMessageBox.Cancel + + @staticmethod + def State_On(): + return QtGui.QStyle.State_On + + @staticmethod + def State_NoChange(): + return QtGui.QStyle.State_NoChange + + @staticmethod + def State_Off(): + return QtGui.QStyle.State_Off + + @staticmethod + def PE_IndicatorMenuCheckMark(): + return QtGui.QStyle.PE_IndicatorMenuCheckMark + + @staticmethod + def GlobalColor_transparent(): + return Qt.transparent + + @staticmethod + def Stretch(): + return QtGui.QHeaderView.Stretch + + @staticmethod + def Fixed(): + return QtGui.QHeaderView.Fixed + + @staticmethod + def SingleSelection(): + return QtGui.QAbstractItemView.SingleSelection + + @staticmethod + def SelectRows(): + return QtGui.QAbstractItemView.SelectRows + + @staticmethod + def AllEditTriggers(): + return QtGui.QAbstractItemView.AllEditTriggers + + @staticmethod + def NoEditTriggers(): + return QtGui.QAbstractItemView.NoEditTriggers + + @staticmethod + def Trigger(): + return QtGui.QSystemTrayIcon.Trigger + + @staticmethod + def LeftToRight(): + return QtGui.QListView.LeftToRight + + # PyQt6 enum compatibility wrappers + @staticmethod + def AlternateBase(): + return QtGui.QPalette.AlternateBase + + @staticmethod + def WindowText(): + return QtGui.QPalette.WindowText + + @staticmethod + def Window(): + return QtGui.QPalette.Window + + @staticmethod + def Bold(): + return QtGui.QFont.Bold + + @staticmethod + def Password(): + return QtGui.QLineEdit.Password + + @staticmethod + def Normal(): + return QtGui.QLineEdit.Normal + + @staticmethod + def RejectRole(): + return QtGui.QDialogButtonBox.RejectRole + + @staticmethod + def AcceptRole(): + return QtGui.QDialogButtonBox.AcceptRole + + @staticmethod + def CompositionMode_Overlay(): + return QtGui.QPainter.CompositionMode_Overlay + + @staticmethod + def CompositionMode_Source(): + return QtGui.QPainter.CompositionMode_Source + + @staticmethod + def CompositionMode_SourceOver(): + return QtGui.QPainter.CompositionMode_SourceOver + + @staticmethod + def CompositionMode_SourceAtop(): + return QtGui.QPainter.CompositionMode_SourceAtop + + @staticmethod + def CE_ProgressBar(): + return QtGui.QStyle.CE_ProgressBar + + @staticmethod + def CE_ProgressBarLabel(): + return QtGui.QStyle.CE_ProgressBarLabel + + @staticmethod + def TextWordWrap(): + return Qt.TextWordWrap + + @staticmethod + def State_Selected(): + return QtGui.QStyle.State_Selected + + @staticmethod + def AlignRight(): + return Qt.AlignRight + + @staticmethod + def AlignLeft(): + return Qt.AlignLeft + + @staticmethod + def NoFocus(): + return Qt.NoFocus + + +# Helper function for exec compatibility +def exec_(widget): + """Call exec() or exec_() depending on Qt version.""" + if QT_VERSION >= 6: + return widget.exec() + else: + return widget.exec_() + + +# Helper functions for PyQt4 type wrapping +# PyQt4 requires specific Qt wrapper types (ItemFlags, Alignment) instead of raw ints +def ItemFlags(flags): + """Wrap item flags in the proper Qt type for PyQt4 compatibility.""" + if QT_VERSION == 4: + return QtCore.Qt.ItemFlags(flags) + else: + return flags + + +def Alignment(alignment): + """Wrap alignment in the proper Qt type for PyQt4 compatibility.""" + if QT_VERSION == 4: + return QtCore.Qt.Alignment(alignment) + else: + return alignment diff --git trackma/ui/qt/delegates.py trackma/ui/qt/delegates.py index 527ea00..dd467b2 100644 --- trackma/ui/qt/delegates.py +++ trackma/ui/qt/delegates.py @@ -1,7 +1,12 @@ -from PyQt6 import QtCore, QtGui -from PyQt6.QtWidgets import QDoubleSpinBox, QStyle, QStyleOptionProgressBar, QStyledItemDelegate +from trackma.ui.qt.compat import QtCore, QtGui +from trackma.ui.qt.compat import QtWidgets +QDoubleSpinBox = QtWidgets.QDoubleSpinBox +QStyle = QtWidgets.QStyle +QStyleOptionProgressBar = QtWidgets.QStyleOptionProgressBar +QStyledItemDelegate = QtWidgets.QStyledItemDelegate from trackma.ui.qt.util import getColor +from trackma.ui.qt.compat import QtCompat MARGIN = 5 PADDING = 5 @@ -25,9 +30,9 @@ class AddListDelegate(QStyledItemDelegate): # Get theme colors palette = QtGui.QPalette() - self.alternatebasecolor = palette.color(palette.ColorRole.AlternateBase) - self.windowtextcolor = palette.color(palette.ColorRole.WindowText) - self.windowcolor = palette.color(palette.ColorRole.Window) + self.alternatebasecolor = palette.color(QtCompat.AlternateBase()) + self.windowtextcolor = palette.color(QtCompat.WindowText()) + self.windowcolor = palette.color(QtCompat.Window()) super().__init__(parent) @@ -41,11 +46,11 @@ class AddListDelegate(QStyledItemDelegate): QtCore.QMargins(MARGIN, MARGIN, MARGIN, MARGIN) data = index.data() - thumb = index.data(QtCore.Qt.ItemDataRole.DecorationRole) + thumb = index.data(QtCompat.DecorationRole()) painter.save() - color = index.data(QtCore.Qt.ItemDataRole.BackgroundRole) + color = index.data(QtCompat.BackgroundRole()) # Draw background box painter.setPen(QtGui.QPen(self.alternatebasecolor)) @@ -55,7 +60,7 @@ class AddListDelegate(QStyledItemDelegate): # Prepare to draw inside baseRect = outerRect - \ QtCore.QMargins(PADDING, PADDING, PADDING, PADDING) - painter.setPen(QtCore.Qt.PenStyle.NoPen) + painter.setPen(QtCompat.NoPen()) # Draw thumbnail (if any) if thumb: @@ -69,7 +74,7 @@ class AddListDelegate(QStyledItemDelegate): # Set our font to bold bfont = QtGui.QFont(self.font) - bfont.setWeight(QtGui.QFont.Weight.Bold) + bfont.setWeight(QtCompat.Bold()) painter.setFont(bfont) painter.setPen(QtGui.QPen(QtGui.QColor(10, 10, 10))) @@ -78,7 +83,7 @@ class AddListDelegate(QStyledItemDelegate): textRect -= QtCore.QMargins(5, 0, 5, 0) # Draw title - painter.drawText(textRect, QtCore.Qt.AlignmentFlag.AlignVCenter, data['title']) + painter.drawText(textRect, QtCompat.AlignVCenter(), data['title']) painter.setPen(QtGui.QPen(self.windowtextcolor)) @@ -87,9 +92,9 @@ class AddListDelegate(QStyledItemDelegate): dataRect = textRect.adjusted(75, 0, 0, 0) textRect.translate(0, self.fh + 10) - painter.drawText(textRect, QtCore.Qt.AlignmentFlag.AlignTop, "Date") + painter.drawText(textRect, QtCompat.AlignTop(), "Date") textRect.translate(0, self.fh + 5) - painter.drawText(textRect, QtCore.Qt.AlignmentFlag.AlignTop, "Episodes") + painter.drawText(textRect, QtCompat.AlignTop(), "Episodes") # Draw data painter.setFont(self.font) @@ -105,10 +110,10 @@ class AddListDelegate(QStyledItemDelegate): d_end = '?' dataRect.translate(0, self.fh + 10) - painter.drawText(dataRect, QtCore.Qt.AlignmentFlag.AlignTop, + painter.drawText(dataRect, QtCompat.AlignTop(), "{} to {}".format(d_from, d_end)) dataRect.translate(0, self.fh + 5) - painter.drawText(dataRect, QtCore.Qt.AlignmentFlag.AlignTop, + painter.drawText(dataRect, QtCompat.AlignTop(), str(data.get('total') or '?')) # Draw synopsis @@ -116,12 +121,12 @@ class AddListDelegate(QStyledItemDelegate): textRect.setBottomRight(baseRect.bottomRight()) if 'extra' in data: - painter.drawText(textRect, QtCore.Qt.AlignmentFlag.AlignTop | QtCore.Qt.TextFlag.TextWordWrap, self._get_extra( + painter.drawText(textRect, QtCompat.AlignTop() | QtCompat.TextWordWrap(), self._get_extra( data['extra'], 'Synopsis')) # Draw select box - if option.state & QStyle.StateFlag.State_Selected: - painter.setCompositionMode(QtGui.QPainter.CompositionMode.CompositionMode_Overlay) + if option.state & QtCompat.State_Selected(): + painter.setCompositionMode(QtCompat.CompositionMode_Overlay()) # painter.setOpacity(0.5) painter.fillRect(outerRect, option.palette.highlight()) @@ -173,11 +178,11 @@ class ShowsTableDelegate(QStyledItemDelegate): prog_options.rect = rect prog_options.text = '%d%%' % (value*100/maximum) prog_options.textVisible = self._show_text - option.widget.style().drawControl(QStyle.ControlElement.CE_ProgressBar, prog_options, painter) + option.widget.style().drawControl(QtCompat.CE_ProgressBar(), prog_options, painter) elif self._bar_style is self.BarStyle04: painter.setBrush(getColor(self.colors['progress_bg'])) - painter.setPen(QtCore.Qt.GlobalColor.transparent) + painter.setPen(QtCompat.GlobalColor_transparent()) painter.drawRect(rect) self.paintSubValue(painter, rect, subvalue, maximum) if value > 0: @@ -195,25 +200,25 @@ class ShowsTableDelegate(QStyledItemDelegate): elif self._bar_style is self.BarStyleHybrid: painter.setCompositionMode( - QtGui.QPainter.CompositionMode.CompositionMode_Source) - painter.fillRect(rect, QtCore.Qt.GlobalColor.transparent) + QtCompat.CompositionMode_Source()) + painter.fillRect(rect, QtCompat.GlobalColor_transparent()) painter.setCompositionMode( - QtGui.QPainter.CompositionMode.CompositionMode_SourceOver) + QtCompat.CompositionMode_SourceOver()) prog_options = QStyleOptionProgressBar() prog_options.maximum = maximum prog_options.progress = value prog_options.rect = rect prog_options.text = '%d%%' % (value*100/maximum) - option.widget.style().drawControl(QStyle.ControlElement.CE_ProgressBar, prog_options, painter) + option.widget.style().drawControl(QtCompat.CE_ProgressBar(), prog_options, painter) painter.setCompositionMode( - QtGui.QPainter.CompositionMode.CompositionMode_SourceAtop) - painter.setPen(QtCore.Qt.GlobalColor.transparent) + QtCompat.CompositionMode_SourceAtop()) + painter.setPen(QtCompat.GlobalColor_transparent()) self.paintSubValue(painter, rect, subvalue, maximum) self.paintEpisodes(painter, rect, episodes, maximum) painter.setCompositionMode( - QtGui.QPainter.CompositionMode.CompositionMode_SourceOver) + QtCompat.CompositionMode_SourceOver()) if self._show_text: - option.widget.style().drawControl(QStyle.ControlElement.CE_ProgressBarLabel, prog_options, painter) + option.widget.style().drawControl(QtCompat.CE_ProgressBarLabel(), prog_options, painter) painter.restore() else: @@ -261,7 +266,7 @@ class ShowsTableDelegate(QStyledItemDelegate): def setEditorData(self, editor, index): (value, maximum, decimals, step) = index.model().data( - index, QtCore.Qt.ItemDataRole.EditRole) + index, QtCompat.EditRole()) editor.setMaximum(maximum or 999) editor.setDecimals(decimals or 0) @@ -272,8 +277,8 @@ class ShowsTableDelegate(QStyledItemDelegate): def setModelData(self, editor, model, index): editor.interpretText() - old_value = index.model().data(index, QtCore.Qt.ItemDataRole.EditRole)[0] + old_value = index.model().data(index, QtCompat.EditRole())[0] new_value = editor.value() if new_value != old_value: - model.setData(index, new_value, QtCore.Qt.ItemDataRole.EditRole) + model.setData(index, new_value, QtCompat.EditRole()) diff --git trackma/ui/qt/details.py trackma/ui/qt/details.py index 9c52d28..1140304 100644 --- trackma/ui/qt/details.py +++ trackma/ui/qt/details.py @@ -14,7 +14,10 @@ # along with this program. If not, see . # -from PyQt6.QtWidgets import QDialog, QDialogButtonBox, QVBoxLayout +from trackma.ui.qt.compat import QtWidgets +QDialog = QtWidgets.QDialog +QDialogButtonBox = QtWidgets.QDialogButtonBox +QVBoxLayout = QtWidgets.QVBoxLayout from trackma.ui.qt.widgets import DetailsWidget @@ -29,7 +32,7 @@ class DetailsDialog(QDialog): main_layout = QVBoxLayout() details = DetailsWidget(self, worker) - bottom_buttons = QDialogButtonBox(QDialogButtonBox.StandardButton.Close) + bottom_buttons = QDialogButtonBox(QtWidgets.QDialogButtonBox.Close) bottom_buttons.setCenterButtons(True) bottom_buttons.rejected.connect(self.close) diff --git trackma/ui/qt/mainwindow.py trackma/ui/qt/mainwindow.py index 210d243..866c685 100644 --- trackma/ui/qt/mainwindow.py +++ trackma/ui/qt/mainwindow.py @@ -17,12 +17,33 @@ import base64 import os -from PyQt6 import QtCore, QtGui -from PyQt6.QtGui import QAction, QActionGroup -from PyQt6.QtWidgets import (QAbstractItemView, QApplication, QCheckBox, QComboBox, - QDoubleSpinBox, QFormLayout, QHBoxLayout, QHeaderView, QInputDialog, QLabel, QLineEdit, - QMainWindow, QMenu, QMessageBox, QProgressBar, QPushButton, QSpinBox, QStyle, - QStyleOptionButton, QSystemTrayIcon, QTabBar, QToolButton, QVBoxLayout, QWidget) +from trackma.ui.qt.compat import QtCore, QtGui, exec_, QT_VERSION +from trackma.ui.qt.compat import QAction, QActionGroup +from trackma.ui.qt.compat import QtWidgets +QAbstractItemView = QtWidgets.QAbstractItemView +QApplication = QtWidgets.QApplication +QCheckBox = QtWidgets.QCheckBox +QComboBox = QtWidgets.QComboBox +QDoubleSpinBox = QtWidgets.QDoubleSpinBox +QFormLayout = QtWidgets.QFormLayout +QHBoxLayout = QtWidgets.QHBoxLayout +QHeaderView = QtWidgets.QHeaderView +QInputDialog = QtWidgets.QInputDialog +QLabel = QtWidgets.QLabel +QLineEdit = QtWidgets.QLineEdit +QMainWindow = QtWidgets.QMainWindow +QMenu = QtWidgets.QMenu +QMessageBox = QtWidgets.QMessageBox +QProgressBar = QtWidgets.QProgressBar +QPushButton = QtWidgets.QPushButton +QSpinBox = QtWidgets.QSpinBox +QStyle = QtWidgets.QStyle +QStyleOptionButton = QtWidgets.QStyleOptionButton +QSystemTrayIcon = QtWidgets.QSystemTrayIcon +QTabBar = QtWidgets.QTabBar +QToolButton = QtWidgets.QToolButton +QVBoxLayout = QtWidgets.QVBoxLayout +QWidget = QtWidgets.QWidget from trackma import messenger from trackma import utils @@ -34,6 +55,7 @@ from trackma.ui.qt.settings import SettingsDialog from trackma.ui.qt.util import FilterBar, getIcon from trackma.ui.qt.widgets import ShowsTableView from trackma.ui.qt.workers import EngineWorker, ImageWorker +from trackma.ui.qt.compat import QtCompat class MainWindow(QMainWindow): """ @@ -148,7 +170,7 @@ class MainWindow(QMainWindow): self.action_add.triggered.connect(self.s_add) self.action_delete = QAction(getIcon('edit-delete'), '&Delete', self) self.action_delete.setStatusTip('Remove this show from your list.') - self.action_delete.setShortcut(QtCore.Qt.Key.Key_Delete) + self.action_delete.setShortcut(QtCompat.Key_Delete()) self.action_delete.triggered.connect(self.s_delete) action_quit = QAction(getIcon('application-exit'), '&Quit', self) action_quit.setShortcut('Ctrl+Q') @@ -217,16 +239,16 @@ class MainWindow(QMainWindow): # Make icons for viewed episodes rect = QtCore.QSize(16, 16) buffer = QtGui.QPixmap(rect) - ep_icon_states = {'all': QStyle.StateFlag.State_On, - 'part': QStyle.StateFlag.State_NoChange, - 'none': QStyle.StateFlag.State_Off} + ep_icon_states = {'all': QtCompat.State_On(), + 'part': QtCompat.State_NoChange(), + 'none': QtCompat.State_Off()} self.ep_icons = {} for key, state in ep_icon_states.items(): - buffer.fill(QtCore.Qt.GlobalColor.transparent) + buffer.fill(QtCompat.GlobalColor_transparent()) painter = QtGui.QPainter(buffer) opt = QStyleOptionButton() opt.state = state - self.style().drawPrimitive(QStyle.PrimitiveElement.PE_IndicatorMenuCheckMark, opt, painter) + self.style().drawPrimitive(QtCompat.PE_IndicatorMenuCheckMark(), opt, painter) self.ep_icons[key] = QtGui.QIcon(buffer) painter.end() @@ -348,7 +370,7 @@ class MainWindow(QMainWindow): self.show_image = QLabel('Trackma-qt') self.show_image.setFixedHeight(149) self.show_image.setMinimumWidth(100) - self.show_image.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) + self.show_image.setAlignment(QtCompat.AlignCenter()) show_progress_label = QLabel('Progress:') self.show_progress = QSpinBox() self.show_progress.setMinimumWidth(spinbox_width) @@ -390,7 +412,7 @@ class MainWindow(QMainWindow): small_btns_hbox.addWidget(self.show_dec_btn) small_btns_hbox.addWidget(self.show_play_btn) small_btns_hbox.addWidget(self.show_inc_btn) - small_btns_hbox.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) + small_btns_hbox.setAlignment(QtCompat.AlignCenter()) left_box.addRow(self.show_image) left_box.addRow(self.show_progress_bar) @@ -514,11 +536,11 @@ class MainWindow(QMainWindow): def error(self, msg): self.status('Error: {}'.format(msg)) - QMessageBox.critical(self, 'Error', str(msg), QMessageBox.StandardButton.Ok) + QMessageBox.critical(self, 'Error', str(msg), QtCompat.Ok()) def fatal(self, msg): QMessageBox.critical( - self, 'Fatal Error', "Fatal Error! Reason:\n\n{0}".format(msg), QMessageBox.StandardButton.Ok) + self, 'Fatal Error', "Fatal Error! Reason:\n\n{0}".format(msg), QtCompat.Ok()) self.accountman.set_default(None) self._busy() self.finish = False @@ -632,9 +654,9 @@ class MainWindow(QMainWindow): def _apply_view(self): if self.config['inline_edit']: - self.view.setEditTriggers(QAbstractItemView.EditTrigger.AllEditTriggers) + self.view.setEditTriggers(QtCompat.AllEditTriggers()) else: - self.view.setEditTriggers(QAbstractItemView.EditTrigger.NoEditTriggers) + self.view.setEditTriggers(QtCompat.NoEditTriggers()) def _apply_tray(self): if self.tray.isVisible() and not self.config['show_tray']: @@ -766,8 +788,8 @@ class MainWindow(QMainWindow): if column not in self.api_config['visible_columns']: self.view.setColumnHidden(i, True) - self.view.horizontalHeader().setSectionResizeMode(1, QHeaderView.ResizeMode.Stretch) - self.view.horizontalHeader().setSectionResizeMode(3, QHeaderView.ResizeMode.Fixed) + self.view.horizontalHeader().setSectionResizeMode(1, QtCompat.Stretch()) + self.view.horizontalHeader().setSectionResizeMode(3, QtCompat.Fixed()) # Recover column state if self.config['remember_columns'] and isinstance(self.api_config['columns_state'], str): @@ -819,7 +841,7 @@ class MainWindow(QMainWindow): # Update information metrics = QtGui.QFontMetrics(self.show_title.font()) title = metrics.elidedText( - show['title'], QtCore.Qt.TextElideMode.ElideRight, self.show_title.width()) + show['title'], QtCompat.ElideRight(), self.show_title.width()) self.show_title.setText(title) self.show_progress.setValue(show['my_progress']) @@ -993,7 +1015,7 @@ class MainWindow(QMainWindow): self.show() def s_tray_clicked(self, reason): - if reason == QSystemTrayIcon.ActivationReason.Trigger: + if reason == QtCompat.Trigger(): self.s_hide() def s_busy(self): @@ -1012,7 +1034,11 @@ class MainWindow(QMainWindow): def s_update_sort(self, index, order): self.config['sort_index'] = index - self.config['sort_order'] = order.value + # In PyQt6, enums have .value; in PyQt4/5 they are already integers + if QT_VERSION >= 6: + self.config['sort_order'] = order.value + else: + self.config['sort_order'] = int(order) def s_download_image(self): show = self.worker.engine.get_show_info(self.selected_show_id) @@ -1060,9 +1086,9 @@ class MainWindow(QMainWindow): self.view.model().setFilterColumns(expr_dict) if self.show_filter_casesens.isChecked(): - self.view.model().setFilterCaseSensitivity(QtCore.Qt.CaseSensitivity.CaseSensitive) + self.view.model().setFilterCaseSensitivity(QtCompat.CaseSensitive()) else: - self.view.model().setFilterCaseSensitivity(QtCore.Qt.CaseSensitivity.CaseInsensitive) + self.view.model().setFilterCaseSensitivity(QtCompat.CaseInsensitive()) self.view.model().setFilterFixedString(expression) def s_filter_invert_changed(self): @@ -1156,9 +1182,9 @@ class MainWindow(QMainWindow): show = self.worker.engine.get_show_info(self.selected_show_id) reply = QMessageBox.question(self, 'Confirmation', 'Are you sure you want to delete %s?' % show['title'], - QMessageBox.StandardButton.Yes, QMessageBox.StandardButton.No) + QtCompat.Yes(), QtCompat.No()) - if reply == QMessageBox.StandardButton.Yes: + if reply == QtCompat.Yes(): self.worker_call('delete_show', self.r_generic, show) def s_scan_library(self): @@ -1193,11 +1219,11 @@ class MainWindow(QMainWindow): reply = QMessageBox.question(self, 'Confirmation', 'There are %d unsynced changes. Do you want to send them first? (Choosing No will discard them!)' % len( queue), - QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No | QMessageBox.StandardButton.Cancel) + QtCompat.Yes() | QtCompat.No() | QtCompat.Cancel()) - if reply == QMessageBox.StandardButton.Yes: + if reply == QtCompat.Yes(): self.s_send(True) - elif reply == QMessageBox.StandardButton.No: + elif reply == QtCompat.No(): self._busy(True) self.worker_call('list_download', self.r_list_retrieved) else: @@ -1250,7 +1276,7 @@ class MainWindow(QMainWindow): dialog = SettingsDialog( None, self.worker, self.config, self.configfile) dialog.saved.connect(self._update_config) - dialog.exec() + exec_(dialog) def s_about(self): QMessageBox.about(self, 'About Trackma-qt %s' % utils.VERSION, @@ -1266,7 +1292,10 @@ class MainWindow(QMainWindow): def s_show_menu_columns(self, pos): globalPos = self.sender().mapToGlobal(pos) globalPos += QtCore.QPoint(3, 3) - self.menu_columns.exec(globalPos) + if QT_VERSION >= 6: + self.menu_columns.exec(globalPos) + else: + self.menu_columns.exec_(globalPos) def s_toggle_column(self, visible): w = self.sender() @@ -1341,9 +1370,9 @@ class MainWindow(QMainWindow): box = QMessageBox(self) box.setWindowTitle("Update prompt") box.setText(f"Do you want to update {show['title']} to {episode}?") - box.setIcon(QMessageBox.Icon.Question) - box.setStandardButtons(QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No) - box.setAttribute(QtCore.Qt.WidgetAttribute.WA_ShowWithoutActivating) + box.setIcon(QtCompat.Question()) + box.setStandardButtons(QtCompat.Yes() | QtCompat.No()) + box.setAttribute(QtCompat.WA_ShowWithoutActivating()) box.setModal(False) box.accepted.connect(lambda: self.worker_call('set_episode', self.r_generic, @@ -1357,7 +1386,7 @@ class MainWindow(QMainWindow): addwindow = AddDialog( None, self.worker, current_status, default=show['title']) addwindow.setModal(True) - if addwindow.exec(): + if exec_(addwindow): self.worker_call('set_episode', self.r_generic, addwindow.selected_show['id'], episode) diff --git trackma/ui/qt/models.py trackma/ui/qt/models.py index 180b2cf..d6f6a7e 100644 --- trackma/ui/qt/models.py +++ trackma/ui/qt/models.py @@ -1,10 +1,11 @@ import datetime -from PyQt6 import QtCore, QtGui +from trackma.ui.qt.compat import QtCore, QtGui, QT_VERSION, ItemFlags, Alignment from trackma import utils from trackma.ui.qt.thumbs import ThumbManager from trackma.ui.qt.util import getColor, getIcon +from trackma.ui.qt.compat import QtCompat class ShowListModel(QtCore.QAbstractTableModel): @@ -32,10 +33,11 @@ class ShowListModel(QtCore.QAbstractTableModel): editable_columns = [COL_MY_PROGRESS, COL_MY_SCORE] - common_flags = \ - QtCore.Qt.ItemFlag.ItemIsSelectable | \ - QtCore.Qt.ItemFlag.ItemIsEnabled | \ - QtCore.Qt.ItemFlag.ItemNeverHasChildren + common_flags = ItemFlags( + QtCompat.ItemIsSelectable() | \ + QtCompat.ItemIsEnabled() | \ + QtCompat.ItemNeverHasChildren() + ) date_format = "%Y-%m-%d" @@ -154,9 +156,9 @@ class ShowListModel(QtCore.QAbstractTableModel): return len(self.columns) def headerData(self, section, orientation, role): - if role == QtCore.Qt.ItemDataRole.DisplayRole and orientation == QtCore.Qt.Orientation.Horizontal: + if role == QtCompat.DisplayRole() and orientation == QtCompat.Horizontal(): return self.columns[section] - elif role == QtCore.Qt.ItemDataRole.ToolTipRole and orientation == QtCore.Qt.Orientation.Horizontal: + elif role == QtCompat.ToolTipRole() and orientation == QtCompat.Horizontal(): if section == ShowListModel.COL_LAST_UPDATED: return 'Date and time of the last synced update' @@ -175,7 +177,7 @@ class ShowListModel(QtCore.QAbstractTableModel): row, column = index.row(), index.column() show = self.showlist[row] - if role == QtCore.Qt.ItemDataRole.DisplayRole: + if role == QtCompat.DisplayRole(): if column == ShowListModel.COL_ID: return show['id'] elif column == ShowListModel.COL_TITLE: @@ -215,15 +217,15 @@ class ShowListModel(QtCore.QAbstractTableModel): return show.get('my_tags', '-') elif column == ShowListModel.COL_MY_STATUS: return self.mediainfo['statuses_dict'][show['my_status']] - elif role == QtCore.Qt.ItemDataRole.BackgroundRole: + elif role == QtCompat.BackgroundRole(): return self.colors.get(row) - elif role == QtCore.Qt.ItemDataRole.DecorationRole: + elif role == QtCompat.DecorationRole(): if column == ShowListModel.COL_TITLE and show['id'] in self.playing: return getIcon('media-playback-start') - elif role == QtCore.Qt.ItemDataRole.TextAlignmentRole: + elif role == QtCompat.TextAlignmentRole(): if column in [ShowListModel.COL_MY_PROGRESS, ShowListModel.COL_MY_SCORE]: - return QtCore.Qt.AlignmentFlag.AlignHCenter | QtCore.Qt.AlignmentFlag.AlignVCenter - elif role == QtCore.Qt.ItemDataRole.ToolTipRole: + return Alignment(QtCompat.AlignHCenter() | QtCompat.AlignVCenter()) + elif role == QtCompat.ToolTipRole(): if column == ShowListModel.COL_PERCENT: tooltip = "Watched: %d
" % show['my_progress'] if self.eps.get(row): @@ -238,7 +240,7 @@ class ShowListModel(QtCore.QAbstractTableModel): return tooltip elif column == ShowListModel.COL_LAST_UPDATED: return utils.format_local_time(show.get('my_last_update')) - elif role == QtCore.Qt.ItemDataRole.EditRole: + elif role == QtCompat.EditRole(): if column == ShowListModel.COL_MY_PROGRESS: return (show['my_progress'], show['total'], 0, 1) elif column == ShowListModel.COL_MY_SCORE: @@ -249,14 +251,14 @@ class ShowListModel(QtCore.QAbstractTableModel): decimals = 0 return (show['my_score'], self.mediainfo['score_max'], decimals, self.mediainfo['score_step']) - elif role == QtCore.Qt.ItemDataRole.UserRole: + elif role == QtCompat.UserRole(): if column == ShowListModel.COL_LAST_UPDATED: dt = show.get('my_last_update') return dt.timestamp() if dt is not None else 0 def flags(self, index): if index.column() in self.editable_columns: - return self.common_flags | QtCore.Qt.ItemFlag.ItemIsEditable + return ItemFlags(self.common_flags | QtCompat.ItemIsEditable()) else: return self.common_flags @@ -284,13 +286,13 @@ class AddTableModel(QtCore.QAbstractTableModel): return 3 def headerData(self, section, orientation, role): - if role == QtCore.Qt.ItemDataRole.DisplayRole and orientation == QtCore.Qt.Orientation.Horizontal: + if role == QtCompat.DisplayRole() and orientation == QtCompat.Horizontal(): return self.columns[section] def data(self, index, role): row, column = index.row(), index.column() - if role == QtCore.Qt.ItemDataRole.DisplayRole: + if role == QtCompat.DisplayRole(): item = self.results[row] if column == 0: @@ -322,7 +324,7 @@ class AddListModel(QtCore.QAbstractListModel): def gotThumb(self, iid, thumb): iid = int(iid) self.thumbs[iid] = thumb.scaled( - 100, 140, QtCore.Qt.AspectRatioMode.KeepAspectRatio, QtCore.Qt.TransformationMode.SmoothTransformation) + 100, 140, QtCompat.KeepAspectRatio(), QtCompat.SmoothTransformation()) self.dataChanged.emit(self.index(iid), self.index(iid)) @@ -345,7 +347,7 @@ class AddListModel(QtCore.QAbstractListModel): if self.pool.exists(filename): self.thumbs[row] = self.pool.getThumb(filename).scaled( - 100, 140, QtCore.Qt.AspectRatioMode.KeepAspectRatio, QtCore.Qt.TransformationMode.SmoothTransformation) + 100, 140, QtCompat.KeepAspectRatio(), QtCompat.SmoothTransformation()) else: self.pool.queueDownload(row, item['image'], filename) @@ -359,11 +361,11 @@ class AddListModel(QtCore.QAbstractListModel): def data(self, index, role): row = index.row() - if role == QtCore.Qt.ItemDataRole.DisplayRole: + if role == QtCompat.DisplayRole(): return self.results[row] - elif role == QtCore.Qt.ItemDataRole.DecorationRole: + elif role == QtCompat.DecorationRole(): return self.thumbs.get(row) - elif role == QtCore.Qt.ItemDataRole.BackgroundRole: + elif role == QtCompat.BackgroundRole(): t = self.results[row].get('type') if t == utils.Type.TV: return QtGui.QColor(202, 253, 150) @@ -381,8 +383,8 @@ class AddListModel(QtCore.QAbstractListModel): class AddListProxy(QtCore.QSortFilterProxyModel): def lessThan(self, left, right): - leftData = self.sourceModel().data(left, QtCore.Qt.ItemDataRole.DisplayRole) - rightData = self.sourceModel().data(right, QtCore.Qt.ItemDataRole.DisplayRole) + leftData = self.sourceModel().data(left, QtCompat.DisplayRole()) + rightData = self.sourceModel().data(right, QtCompat.DisplayRole()) return leftData['type'] < rightData['type'] @@ -415,7 +417,7 @@ class ShowListProxy(QtCore.QSortFilterProxyModel): for col in range(self.sourceModel().columnCount(source_parent)): index = self.sourceModel().index(source_row, col) if (col in self.filter_columns and - self.filter_columns[col] not in str(self.sourceModel().data(index, QtCore.Qt.ItemDataRole.DisplayRole))): + self.filter_columns[col] not in str(self.sourceModel().data(index, QtCompat.DisplayRole()))): return self.filter_invert return self.filter_invert != super(ShowListProxy, self).filterAcceptsRow(source_row, source_parent) @@ -424,8 +426,8 @@ class ShowListProxy(QtCore.QSortFilterProxyModel): col = left.column() if col == ShowListModel.COL_LAST_UPDATED: - lv = self.sourceModel().data(left, QtCore.Qt.ItemDataRole.UserRole) - rv = self.sourceModel().data(right, QtCore.Qt.ItemDataRole.UserRole) + lv = self.sourceModel().data(left, QtCompat.UserRole()) + rv = self.sourceModel().data(right, QtCompat.UserRole()) lnum = lv if isinstance(lv, (int, float)) else 0 rnum = rv if isinstance(rv, (int, float)) else 0 diff --git trackma/ui/qt/settings.py trackma/ui/qt/settings.py index 16b13a0..8ef0dfb 100644 --- trackma/ui/qt/settings.py +++ trackma/ui/qt/settings.py @@ -14,16 +14,38 @@ # along with this program. If not, see . # -from PyQt6 import QtCore -from PyQt6.QtWidgets import (QAbstractItemView, QCheckBox, QColorDialog, QComboBox, QDialog, QDialogButtonBox, - QFileDialog, QFormLayout, QFrame, QGridLayout, QGroupBox, QLabel, QLineEdit, QListWidget, - QListWidgetItem, QPushButton, QRadioButton, QScrollArea, QSpinBox, QSplitter, - QStackedWidget, QTabWidget, QVBoxLayout, QWidget) +from trackma.ui.qt.compat import QtCore +from trackma.ui.qt.compat import QtWidgets +QAbstractItemView = QtWidgets.QAbstractItemView +QCheckBox = QtWidgets.QCheckBox +QColorDialog = QtWidgets.QColorDialog +QComboBox = QtWidgets.QComboBox +QDialog = QtWidgets.QDialog +QDialogButtonBox = QtWidgets.QDialogButtonBox +QFileDialog = QtWidgets.QFileDialog +QFormLayout = QtWidgets.QFormLayout +QFrame = QtWidgets.QFrame +QGridLayout = QtWidgets.QGridLayout +QGroupBox = QtWidgets.QGroupBox +QLabel = QtWidgets.QLabel +QLineEdit = QtWidgets.QLineEdit +QListWidget = QtWidgets.QListWidget +QListWidgetItem = QtWidgets.QListWidgetItem +QPushButton = QtWidgets.QPushButton +QRadioButton = QtWidgets.QRadioButton +QScrollArea = QtWidgets.QScrollArea +QSpinBox = QtWidgets.QSpinBox +QSplitter = QtWidgets.QSplitter +QStackedWidget = QtWidgets.QStackedWidget +QTabWidget = QtWidgets.QTabWidget +QVBoxLayout = QtWidgets.QVBoxLayout +QWidget = QtWidgets.QWidget from trackma import utils from trackma.ui.qt.delegates import ShowsTableDelegate from trackma.ui.qt.themedcolorpicker import ThemedColorPicker from trackma.ui.qt.util import FilterBar, getColor, getIcon +from trackma.ui.qt.compat import QtCompat class SettingsDialog(QDialog): @@ -55,7 +77,7 @@ class SettingsDialog(QDialog): getIcon('window-new'), 'User Interface', self.category_list) category_theme = QListWidgetItem( getIcon('applications-graphics'), 'Theme', self.category_list) - self.category_list.setSelectionMode(QAbstractItemView.SelectionMode.SingleSelection) + self.category_list.setSelectionMode(QtCompat.SingleSelection()) self.category_list.setCurrentRow(0) self.category_list.setMaximumWidth( self.category_list.sizeHintForColumn(0) + 15) @@ -65,7 +87,7 @@ class SettingsDialog(QDialog): # Media tab page_media = QWidget() page_media_layout = QVBoxLayout() - page_media_layout.setAlignment(QtCore.Qt.AlignmentFlag.AlignTop) + page_media_layout.setAlignment(QtCompat.AlignTop()) # Group: Media settings g_media = QGroupBox('Media settings') @@ -116,7 +138,7 @@ class SettingsDialog(QDialog): self.plex_port = QLineEdit() self.plex_user = QLineEdit() self.plex_passw = QLineEdit() - self.plex_passw.setEchoMode(QLineEdit.EchoMode.Password) + self.plex_passw.setEchoMode(QtCompat.Password()) self.plex_obey_wait = QCheckBox() self.plex_ssl = QCheckBox() @@ -175,7 +197,7 @@ class SettingsDialog(QDialog): self.kodi_port = QLineEdit() self.kodi_user = QLineEdit() self.kodi_passw = QLineEdit() - self.kodi_passw.setEchoMode(QLineEdit.EchoMode.Password) + self.kodi_passw.setEchoMode(QtCompat.Password()) self.kodi_obey_wait = QCheckBox() g_kodi_layout = QGridLayout() @@ -207,7 +229,7 @@ class SettingsDialog(QDialog): # Library tab page_library = QWidget() page_library_layout = QVBoxLayout() - page_library_layout.setAlignment(QtCore.Qt.AlignmentFlag.AlignTop) + page_library_layout.setAlignment(QtCompat.AlignTop()) # Group: Library g_playnext = QGroupBox('Library') @@ -219,14 +241,14 @@ class SettingsDialog(QDialog): self.player_browse = QPushButton('Browse...') self.player_browse.clicked.connect(self.s_player_browse) lbl_searchdirs = QLabel('Media directories') - lbl_searchdirs.setAlignment(QtCore.Qt.AlignmentFlag.AlignTop) + lbl_searchdirs.setAlignment(QtCompat.AlignTop()) self.searchdirs = QListWidget() self.searchdirs_add = QPushButton('Add...') self.searchdirs_add.clicked.connect(self.s_searchdirs_add) self.searchdirs_remove = QPushButton('Remove') self.searchdirs_remove.clicked.connect(self.s_searchdirs_remove) self.searchdirs_buttons = QVBoxLayout() - self.searchdirs_buttons.setAlignment(QtCore.Qt.AlignmentFlag.AlignTop) + self.searchdirs_buttons.setAlignment(QtCompat.AlignTop()) self.searchdirs_buttons.addWidget(self.searchdirs_add) self.searchdirs_buttons.addWidget(self.searchdirs_remove) self.searchdirs_buttons.addWidget(QSplitter()) @@ -272,7 +294,7 @@ class SettingsDialog(QDialog): # Sync tab page_sync = QWidget() page_sync_layout = QVBoxLayout() - page_sync_layout.setAlignment(QtCore.Qt.AlignmentFlag.AlignTop) + page_sync_layout.setAlignment(QtCompat.AlignTop()) # Group: Autoretrieve g_autoretrieve = QGroupBox('Autoretrieve') @@ -340,7 +362,7 @@ class SettingsDialog(QDialog): # UI tab page_ui = QWidget() page_ui_layout = QFormLayout() - page_ui_layout.setAlignment(QtCore.Qt.AlignmentFlag.AlignTop) + page_ui_layout.setAlignment(QtCompat.AlignTop()) # Group: Icon g_icon = QGroupBox('Notification Icon') @@ -397,7 +419,7 @@ class SettingsDialog(QDialog): # Theming tab page_theme = QWidget() page_theme_layout = QFormLayout() - page_theme_layout.setAlignment(QtCore.Qt.AlignmentFlag.AlignTop) + page_theme_layout.setAlignment(QtCompat.AlignTop()) # Group: Episode Bar g_ep_bar = QGroupBox('Episode Bar') @@ -443,7 +465,7 @@ class SettingsDialog(QDialog): for (key2, label) in self.colors[key1]: self.color_buttons.append(QPushButton()) # self.color_buttons[-1].setStyleSheet('background-color: ' + getColor(self.config['colors'][key]).name()) - self.color_buttons[-1].setFocusPolicy(QtCore.Qt.FocusPolicy.NoFocus) + self.color_buttons[-1].setFocusPolicy(QtCompat.NoFocus()) self.color_buttons[-1].clicked.connect( self.s_color_picker(key2, False)) self.syscolor_buttons.append(QPushButton('System Colors')) @@ -473,12 +495,12 @@ class SettingsDialog(QDialog): # Bottom buttons bottombox = QDialogButtonBox( - QDialogButtonBox.StandardButton.Ok - | QDialogButtonBox.StandardButton.Apply - | QDialogButtonBox.StandardButton.Cancel + QtWidgets.QDialogButtonBox.Ok + | QtWidgets.QDialogButtonBox.Apply + | QtWidgets.QDialogButtonBox.Cancel ) bottombox.accepted.connect(self.s_save) - bottombox.button(QDialogButtonBox.StandardButton.Apply).clicked.connect(self._save) + bottombox.button(QtWidgets.QDialogButtonBox.Apply).clicked.connect(self._save) bottombox.rejected.connect(self.reject) # Main layout finish diff --git trackma/ui/qt/themedcolorpicker.py trackma/ui/qt/themedcolorpicker.py index 3252344..fc51ddb 100644 --- trackma/ui/qt/themedcolorpicker.py +++ trackma/ui/qt/themedcolorpicker.py @@ -14,8 +14,14 @@ # along with this program. If not, see . # -from PyQt6 import QtCore, QtGui -from PyQt6.QtWidgets import QDialog, QDialogButtonBox, QGridLayout, QPushButton, QVBoxLayout +from trackma.ui.qt.compat import QtCore, QtGui, exec_ +from trackma.ui.qt.compat import QtWidgets +from trackma.ui.qt.compat import QtCompat +QDialog = QtWidgets.QDialog +QDialogButtonBox = QtWidgets.QDialogButtonBox +QGridLayout = QtWidgets.QGridLayout +QPushButton = QtWidgets.QPushButton +QVBoxLayout = QtWidgets.QVBoxLayout class ThemedColorPicker(QDialog): @@ -37,14 +43,14 @@ class ThemedColorPicker(QDialog): self.colors.append(QPushButton()) self.colors[-1].setStyleSheet('background-color: ' + QtGui.QColor( QtGui.QPalette().color(QtGui.QPalette.ColorGroup(group), QtGui.QPalette.ColorRole(role))).name()) - self.colors[-1].setFocusPolicy(QtCore.Qt.FocusPolicy.NoFocus) + self.colors[-1].setFocusPolicy(QtCompat.NoFocus()) self.colors[-1].clicked.connect(self.s_select(group, role)) colorbox.addWidget(self.colors[-1], row, col, 1, 1) col += 1 row += 1 bottombox = QDialogButtonBox() - bottombox.addButton(QDialogButtonBox.StandardButton.Ok) - bottombox.addButton(QDialogButtonBox.StandardButton.Cancel) + bottombox.addButton(QtWidgets.QDialogButtonBox.Ok) + bottombox.addButton(QtWidgets.QDialogButtonBox.Cancel) bottombox.accepted.connect(self.accept) bottombox.rejected.connect(self.reject) layout.addLayout(colorbox) @@ -60,9 +66,9 @@ class ThemedColorPicker(QDialog): @staticmethod def do(parent=None, default=None): dialog = ThemedColorPicker(parent, default) - result = dialog.exec() + result = exec_(dialog) - if result == QDialog.DialogCode.Accepted: + if result == QtWidgets.QDialog.Accepted: return dialog.colorString else: return None diff --git trackma/ui/qt/thumbs.py trackma/ui/qt/thumbs.py index b097f32..01b471a 100644 --- trackma/ui/qt/thumbs.py +++ trackma/ui/qt/thumbs.py @@ -1,7 +1,8 @@ import os import queue -from PyQt6 import QtCore, QtGui, QtNetwork +from trackma.ui.qt.compat import QtCore, QtGui, QtNetwork +from trackma.ui.qt.compat import QtCompat ATTRIB_FILE = QtNetwork.QNetworkRequest.Attribute(1000) ATTRIB_ID = QtNetwork.QNetworkRequest.Attribute(1001) @@ -59,7 +60,7 @@ class ThumbManager(QtCore.QObject): data = reply.readAll() image = QtGui.QImage.fromData(data) thumb = image.scaled( - 200, 280, QtCore.Qt.AspectRatioMode.KeepAspectRatio, QtCore.Qt.TransformationMode.SmoothTransformation) + 200, 280, QtCompat.KeepAspectRatio(), QtCompat.SmoothTransformation()) thumb.save(fname) self.downloads.pop(fname) diff --git trackma/ui/qt/util.py trackma/ui/qt/util.py index dec5575..31119f0 100644 --- trackma/ui/qt/util.py +++ trackma/ui/qt/util.py @@ -14,9 +14,8 @@ # along with this program. If not, see . # -from PyQt6 import QtGui - from trackma import utils +from trackma.ui.qt.compat import QtGui, QtWidgets, QtCore, QT_VERSION, QtCompat class FilterBar: @@ -30,8 +29,72 @@ class FilterBar: def getIcon(icon_name): - fallback = QtGui.QIcon(utils.DATADIR + '/qtui/{}.png'.format(icon_name)) - return QtGui.QIcon.fromTheme(icon_name, fallback) + # Try theme icon first + icon = QtGui.QIcon.fromTheme(icon_name) + + # If theme icon is available, use it + if not icon.isNull(): + return icon + + # Otherwise, try the fallback file + fallback_path = utils.DATADIR + '/qtui/{}.png'.format(icon_name) + import os + if os.path.exists(fallback_path): + return QtGui.QIcon(fallback_path) + + # If no file exists, create a simple icon using QStyle or text + # This provides basic functionality especially for PyQt4/older systems + style = QtWidgets.QApplication.style() + + # Map icon names to QStyle standard pixmaps + # Note: Some standard pixmaps are only available in Qt5+ + QStyle = QtWidgets.QStyle + style_map = { + 'list-add': getattr(QStyle, 'SP_ArrowUp', None), + 'list-remove': getattr(QStyle, 'SP_ArrowDown', None), + 'edit-delete': getattr(QStyle, 'SP_TrashIcon', None), + 'help-about': QStyle.SP_MessageBoxInformation, + 'application-exit': QStyle.SP_DialogCloseButton, + 'folder': QStyle.SP_DirIcon, + 'view-refresh': getattr(QStyle, 'SP_BrowserReload', None), + } + + # Try to get standard pixmap + if icon_name in style_map and style_map[icon_name] is not None: + try: + pixmap = style.standardPixmap(style_map[icon_name]) + if not pixmap.isNull(): + return QtGui.QIcon(pixmap) + except: + pass + + # For icons without standard pixmaps, create a simple text-based icon + pixmap = QtGui.QPixmap(16, 16) + pixmap.fill(QtCompat.GlobalColor_transparent()) + painter = QtGui.QPainter(pixmap) + + # Draw simple symbols + font = painter.font() + font.setPixelSize(14) + font.setBold(True) + painter.setFont(font) + + text_map = { + 'list-add': '+', + 'list-remove': '−', # minus sign + 'media-playback-start': '▶', + 'media-skip-forward': '⏭', + 'edit-find': '🔍', + 'edit-delete': '×', + 'view-refresh': '↻', + } + + if icon_name in text_map: + painter.drawText(pixmap.rect(), QtCompat.AlignCenter(), text_map[icon_name]) + + painter.end() + + return QtGui.QIcon(pixmap) def getColor(colorString): diff --git trackma/ui/qt/widgets.py trackma/ui/qt/widgets.py index 2f1077b..c3d9026 100644 --- trackma/ui/qt/widgets.py +++ trackma/ui/qt/widgets.py @@ -16,14 +16,24 @@ import os -from PyQt6 import QtCore, QtGui -from PyQt6.QtWidgets import (QAbstractItemView, QGridLayout, QHeaderView, QLabel, QListView, QScrollArea, QSplitter, - QTableView, QVBoxLayout, QWidget) +from trackma.ui.qt.compat import QtCore, QtGui, exec_ +from trackma.ui.qt.compat import QtWidgets +QAbstractItemView = QtWidgets.QAbstractItemView +QGridLayout = QtWidgets.QGridLayout +QHeaderView = QtWidgets.QHeaderView +QLabel = QtWidgets.QLabel +QListView = QtWidgets.QListView +QScrollArea = QtWidgets.QScrollArea +QSplitter = QtWidgets.QSplitter +QTableView = QtWidgets.QTableView +QVBoxLayout = QtWidgets.QVBoxLayout +QWidget = QtWidgets.QWidget from trackma import utils from trackma.ui.qt.delegates import AddListDelegate, ShowsTableDelegate from trackma.ui.qt.models import AddListModel, AddListProxy, AddTableModel, ShowListModel, ShowListProxy from trackma.ui.qt.workers import ImageWorker +from trackma.ui.qt.compat import QtCompat class DetailsWidget(QWidget): @@ -40,20 +50,20 @@ class DetailsWidget(QWidget): show_title_font = QtGui.QFont() show_title_font.setBold(True) show_title_font.setPointSize(12) - self.show_title.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) + self.show_title.setAlignment(QtCompat.AlignCenter()) self.show_title.setFont(show_title_font) info_area = QWidget() info_layout = QGridLayout() self.show_image = QLabel() - self.show_image.setAlignment(QtCore.Qt.AlignmentFlag.AlignTop) + self.show_image.setAlignment(QtCompat.AlignTop()) self.show_info = QLabel() self.show_info.setWordWrap(True) - self.show_info.setAlignment(QtCore.Qt.AlignmentFlag.AlignTop) + self.show_info.setAlignment(QtCompat.AlignTop()) self.show_description = QLabel() self.show_description.setWordWrap(True) - self.show_description.setAlignment(QtCore.Qt.AlignmentFlag.AlignTop) + self.show_description.setAlignment(QtCompat.AlignTop()) info_layout.addWidget(self.show_image, 0, 0, 1, 1) info_layout.addWidget(self.show_info, 1, 0, 1, 1) @@ -62,7 +72,7 @@ class DetailsWidget(QWidget): info_area.setLayout(info_layout) scroll_area = QScrollArea() - scroll_area.setBackgroundRole(QtGui.QPalette.ColorRole.Light) + scroll_area.setBackgroundRole(QtCompat.Light()) scroll_area.setWidgetResizable(True) scroll_area.setWidget(info_area) @@ -79,12 +89,12 @@ class DetailsWidget(QWidget): def load(self, show): metrics = QtGui.QFontMetrics(self.show_title.font()) title = metrics.elidedText( - show['title'], QtCore.Qt.TextElideMode.ElideRight, self.show_title.width()) + show['title'], QtCompat.ElideRight(), self.show_title.width()) self.show_title.setText("%s" % (show['url'], title)) - self.show_title.setTextFormat(QtCore.Qt.TextFormat.RichText) + self.show_title.setTextFormat(QtCompat.RichText()) self.show_title.setTextInteractionFlags( - QtCore.Qt.TextInteractionFlag.TextBrowserInteraction) + QtCompat.TextBrowserInteraction()) self.show_title.setOpenExternalLinks(True) # Load show info @@ -161,21 +171,25 @@ class ShowsTableView(QTableView): self.setModel(proxymodel) self.setItemDelegate(ShowsTableDelegate(self, palette=palette)) - self.setSelectionMode(QAbstractItemView.SelectionMode.SingleSelection) - self.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows) + self.setSelectionMode(QtCompat.SingleSelection()) + self.setSelectionBehavior(QtCompat.SelectRows()) self.horizontalHeader().setHighlightSections(False) self.horizontalHeader().setSectionsMovable(True) - self.horizontalHeader().setContextMenuPolicy(QtCore.Qt.ContextMenuPolicy.CustomContextMenu) + self.horizontalHeader().setContextMenuPolicy(QtCompat.CustomContextMenu()) self.verticalHeader().hide() - self.setGridStyle(QtCore.Qt.PenStyle.NoPen) + self.setGridStyle(QtCompat.NoPen()) def contextMenuEvent(self, event): - action = self.context_menu.exec(event.globalPos()) + from trackma.ui.qt.compat import QT_VERSION + if QT_VERSION >= 6: + action = self.context_menu.exec(event.globalPos()) + else: + action = self.context_menu.exec_(event.globalPos()) def mousePressEvent(self, event): super().mousePressEvent(event) - if event.button() == QtCore.Qt.MouseButton.MiddleButton: + if event.button() == QtCompat.MiddleButton(): self.middleClicked.emit() @@ -188,14 +202,14 @@ class AddCardView(QListView): m = AddListModel(api_info=api_info) proxy = AddListProxy() proxy.setSourceModel(m) - proxy.sort(0, QtCore.Qt.SortOrder.AscendingOrder) + proxy.sort(0, QtCompat.AscendingOrder()) self.setItemDelegate(AddListDelegate()) - self.setFlow(QListView.Flow.LeftToRight) + self.setFlow(QtCompat.LeftToRight()) self.setWrapping(True) - self.setSelectionMode(QAbstractItemView.SelectionMode.SingleSelection) - self.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows) - self.setEditTriggers(QAbstractItemView.EditTrigger.NoEditTriggers) + self.setSelectionMode(QtCompat.SingleSelection()) + self.setSelectionBehavior(QtCompat.SelectRows()) + self.setEditTriggers(QtCompat.NoEditTriggers()) self.setModel(proxy) self.selectionModel().currentRowChanged.connect(self.s_show_selected) @@ -230,17 +244,17 @@ class AddTableDetailsView(QSplitter): proxy = QtCore.QSortFilterProxyModel() proxy.setSourceModel(m) - self.table.setGridStyle(QtCore.Qt.PenStyle.NoPen) - self.table.setSelectionMode(QAbstractItemView.SelectionMode.SingleSelection) - self.table.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows) - self.table.setEditTriggers(QAbstractItemView.EditTrigger.NoEditTriggers) + self.table.setGridStyle(QtCompat.NoPen()) + self.table.setSelectionMode(QtCompat.SingleSelection()) + self.table.setSelectionBehavior(QtCompat.SelectRows()) + self.table.setEditTriggers(QtCompat.NoEditTriggers()) self.table.setModel(proxy) # Allow sorting but don't sort by default - self.table.horizontalHeader().setSortIndicator(-1, QtCore.Qt.SortOrder.AscendingOrder) + self.table.horizontalHeader().setSortIndicator(-1, QtCompat.AscendingOrder()) self.table.setSortingEnabled(True) - self.table.horizontalHeader().setSectionResizeMode(0, QHeaderView.ResizeMode.Stretch) + self.table.horizontalHeader().setSectionResizeMode(0, QtCompat.Stretch()) self.table.selectionModel().currentRowChanged.connect(self.s_show_selected) self.addWidget(self.table) diff --git trackma/ui/qt/workers.py trackma/ui/qt/workers.py index 7d916f8..9a6c818 100644 --- trackma/ui/qt/workers.py +++ trackma/ui/qt/workers.py @@ -20,7 +20,7 @@ import urllib.request from io import BytesIO from PIL import Image -from PyQt6 import QtCore +from trackma.ui.qt.compat import QtCore from trackma import utils from trackma.engine import Engine