Skip to content

Commit 6e0a415

Browse files
committed
Initial support for PySide6, tested on Ubuntu 25.04 VM.
1 parent 92a0e35 commit 6e0a415

23 files changed

+545
-120
lines changed

src/classes/app.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -344,6 +344,11 @@ def _tr(self, message):
344344
def cleanup(self):
345345
"""aboutToQuit signal handler for application exit"""
346346
self.log.debug("Saving settings in app.cleanup")
347+
if getattr(self, "window", None):
348+
try:
349+
self.window._shutdown()
350+
except Exception:
351+
self.log.warning("Window shutdown raised during app cleanup.", exc_info=1)
347352
try:
348353
self.settings.save()
349354
except Exception:

src/classes/ui_util.py

Lines changed: 25 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@
4040
from qt_api import QIcon, QPalette, QColor
4141
from qt_api import (
4242
QApplication, QWidget, QTabWidget, QAction)
43-
from qt_api import uic
43+
from qt_api import uic, load_ui as qt_load_ui
4444

4545
from classes.app import get_app
4646
from classes.logger import log
@@ -77,7 +77,10 @@ def load_ui(window, path):
7777
for attempt in range(1, 6):
7878
try:
7979
# Load ui from configured path
80-
uic.loadUi(path, window)
80+
if uic is not None and hasattr(uic, "loadUi"):
81+
uic.loadUi(path, window)
82+
else:
83+
qt_load_ui(path, window)
8184

8285
# Successfully loaded UI file, so clear any previously encountered errors
8386
error = None
@@ -254,11 +257,16 @@ def connect_auto_events(window, elem, name):
254257
func_name = name + "_trigger"
255258
if hasattr(window, func_name) and callable(getattr(window, func_name)):
256259
# Disconnect existing connections safely
257-
try:
258-
while True:
259-
elem.triggered.disconnect()
260-
except TypeError:
261-
pass # No more connections to disconnect
260+
while True:
261+
try:
262+
disconnected = elem.triggered.disconnect()
263+
except TypeError:
264+
break # No more connections to disconnect (PyQt)
265+
except Exception:
266+
break
267+
else:
268+
if disconnected is False:
269+
break # No more connections to disconnect (PySide)
262270
# Connect the signal to the slot
263271
elem.triggered.connect(getattr(window, func_name))
264272

@@ -267,11 +275,16 @@ def connect_auto_events(window, elem, name):
267275
func_name = name + "_click"
268276
if hasattr(window, func_name) and callable(getattr(window, func_name)):
269277
# Disconnect existing connections safely
270-
try:
271-
while True:
272-
elem.clicked.disconnect()
273-
except TypeError:
274-
pass # No more connections to disconnect
278+
while True:
279+
try:
280+
disconnected = elem.clicked.disconnect()
281+
except TypeError:
282+
break # No more connections to disconnect (PyQt)
283+
except Exception:
284+
break
285+
else:
286+
if disconnected is False:
287+
break # No more connections to disconnect (PySide)
275288
# Connect the signal to the slot
276289
elem.clicked.connect(getattr(window, func_name))
277290

@@ -314,4 +327,3 @@ def transfer_children(from_widget, to_widget):
314327
log.info(
315328
"Transferring children from '%s' to '%s'",
316329
from_widget.objectName(), to_widget.objectName())
317-

src/qt_api.py

Lines changed: 104 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
uic = None
2323
QT_API: Optional[str] = None
2424
QT_VERSION_STR: Optional[str] = None
25+
PYQT_VERSION_STR: Optional[str] = None
2526
BINDING_VERSION_STR: Optional[str] = None
2627
_MODULES = []
2728
_FAILED_IMPORT: Optional[Exception] = None
@@ -1165,6 +1166,7 @@ def _import_binding(name: str) -> Tuple:
11651166
uicMod,
11661167
QtCoreMod.QT_VERSION_STR,
11671168
QtCoreMod.PYQT_VERSION_STR,
1169+
QtCoreMod.PYQT_VERSION_STR,
11681170
)
11691171

11701172
if name == "pyside6":
@@ -1217,6 +1219,7 @@ def _import_binding(name: str) -> Tuple:
12171219
QtUiToolsMod,
12181220
QtCoreMod.__version__, # PySide binds Qt version here
12191221
QtCoreMod.__version__,
1222+
QtCoreMod.__version__,
12201223
)
12211224

12221225
if name == "pyqt5":
@@ -1267,6 +1270,7 @@ def _import_binding(name: str) -> Tuple:
12671270
uicMod,
12681271
QtCoreMod.QT_VERSION_STR,
12691272
QtCoreMod.PYQT_VERSION_STR,
1273+
QtCoreMod.PYQT_VERSION_STR,
12701274
)
12711275

12721276
if name == "pyside2":
@@ -1323,6 +1327,7 @@ def _import_binding(name: str) -> Tuple:
13231327
QtUiToolsMod,
13241328
QtCoreMod.__version__,
13251329
QtCoreMod.__version__,
1330+
QtCoreMod.__version__,
13261331
)
13271332

13281333
raise ImportError(f"Unknown binding '{name}'")
@@ -1331,7 +1336,7 @@ def _import_binding(name: str) -> Tuple:
13311336
def _select_binding() -> str:
13321337
"""Select and load the first available binding."""
13331338
global QtCore, QtGui, QtWidgets, QtSvg, QtWebEngineCore, QtWebEngineWidgets, QtWebChannel, QtWebKitWidgets
1334-
global Signal, Slot, Property, QRegularExpression, QState, QStateMachine, uic, QT_API, QT_VERSION_STR, BINDING_VERSION_STR, _MODULES
1339+
global Signal, Slot, Property, QRegularExpression, QState, QStateMachine, uic, QT_API, QT_VERSION_STR, PYQT_VERSION_STR, BINDING_VERSION_STR, _MODULES
13351340
global _FAILED_IMPORT, _SELECTING
13361341

13371342
if _FAILED_IMPORT:
@@ -1366,6 +1371,7 @@ def _select_binding() -> str:
13661371
QStateMachine,
13671372
uic,
13681373
QT_VERSION_STR,
1374+
PYQT_VERSION_STR,
13691375
BINDING_VERSION_STR,
13701376
) = _import_binding(candidate)
13711377
logger.info(
@@ -1420,12 +1426,106 @@ def load_ui(path: str, baseinstance=None):
14201426
from importlib import import_module
14211427

14221428
QtUiTools = import_module("PySide6.QtUiTools" if QT_API == "pyside6" else "PySide2.QtUiTools") # type: ignore
1423-
loader = QtUiTools.QUiLoader()
1429+
if baseinstance is not None:
1430+
class UiLoader(QtUiTools.QUiLoader):
1431+
def __init__(self, base):
1432+
super().__init__(base)
1433+
self.base = base
1434+
1435+
def createWidget(self, class_name, parent=None, name=""):
1436+
if parent is None and self.base is not None:
1437+
return self.base
1438+
widget = super().createWidget(class_name, parent, name)
1439+
if self.base is not None and name:
1440+
setattr(self.base, name, widget)
1441+
return widget
1442+
1443+
def createAction(self, parent=None, name=""):
1444+
if parent is None and self.base is not None:
1445+
parent = self.base
1446+
action = super().createAction(parent, name)
1447+
if self.base is not None and name:
1448+
setattr(self.base, name, action)
1449+
return action
1450+
1451+
loader = UiLoader(baseinstance)
1452+
setattr(baseinstance, "_qt_ui_loader", loader)
1453+
else:
1454+
loader = QtUiTools.QUiLoader()
14241455
ui_file = QtCore.QFile(path)
14251456
if not ui_file.open(QtCore.QFile.ReadOnly):
14261457
raise IOError(f"Cannot open UI file: {path}")
14271458
try:
1428-
return loader.load(ui_file, baseinstance)
1459+
widget = loader.load(ui_file)
1460+
if baseinstance is not None:
1461+
if widget is not None and widget is not baseinstance:
1462+
setattr(baseinstance, "_qt_loaded_ui", widget)
1463+
main_window_type = getattr(QtWidgets, "QMainWindow", None)
1464+
if main_window_type and isinstance(baseinstance, main_window_type) and isinstance(widget, main_window_type):
1465+
central = widget.centralWidget()
1466+
if central is not None:
1467+
baseinstance.setCentralWidget(central)
1468+
menubar = widget.menuBar()
1469+
if menubar is not None:
1470+
baseinstance.setMenuBar(menubar)
1471+
statusbar = widget.statusBar()
1472+
if statusbar is not None:
1473+
baseinstance.setStatusBar(statusbar)
1474+
tool_bar_type = getattr(QtWidgets, "QToolBar", None)
1475+
if tool_bar_type:
1476+
for toolbar in widget.findChildren(tool_bar_type):
1477+
baseinstance.addToolBar(toolbar)
1478+
dock_type = getattr(QtWidgets, "QDockWidget", None)
1479+
if dock_type:
1480+
for dock in widget.findChildren(dock_type):
1481+
try:
1482+
area = widget.dockWidgetArea(dock)
1483+
except Exception:
1484+
area = None
1485+
if area is None or area == QtCore.Qt.NoDockWidgetArea:
1486+
baseinstance.addDockWidget(QtCore.Qt.LeftDockWidgetArea, dock)
1487+
else:
1488+
baseinstance.addDockWidget(area, dock)
1489+
if widget is not None:
1490+
main_window_type = getattr(QtWidgets, "QMainWindow", None)
1491+
if main_window_type and isinstance(baseinstance, main_window_type):
1492+
root = baseinstance
1493+
else:
1494+
root = widget
1495+
else:
1496+
root = baseinstance
1497+
qaction_type = getattr(QtGui, "QAction", None)
1498+
actions = []
1499+
seen = set()
1500+
if qaction_type is not None:
1501+
for holder in (root, baseinstance, loader):
1502+
if holder is None:
1503+
continue
1504+
for act in holder.findChildren(qaction_type):
1505+
ident = id(act)
1506+
if ident in seen:
1507+
continue
1508+
seen.add(ident)
1509+
actions.append(act)
1510+
for act in actions:
1511+
try:
1512+
act.setParent(baseinstance)
1513+
except Exception:
1514+
pass
1515+
for obj in root.findChildren(QtCore.QObject):
1516+
obj_name = obj.objectName()
1517+
if obj_name and not hasattr(baseinstance, obj_name):
1518+
setattr(baseinstance, obj_name, obj)
1519+
if widget is not None and widget is not baseinstance:
1520+
widget_type = getattr(QtWidgets, "QWidget", None)
1521+
if widget_type and isinstance(baseinstance, widget_type) and isinstance(widget, widget_type):
1522+
try:
1523+
if baseinstance.layout() is None and widget.layout() is not None:
1524+
baseinstance.setLayout(widget.layout())
1525+
except Exception:
1526+
pass
1527+
return baseinstance
1528+
return widget
14291529
finally:
14301530
ui_file.close()
14311531

@@ -1500,6 +1600,7 @@ def __getattr__(name):
15001600
"QLibraryInfo",
15011601
"QT_API",
15021602
"QT_VERSION_STR",
1603+
"PYQT_VERSION_STR",
15031604
"BINDING_VERSION_STR",
15041605
"ensure_binding",
15051606
"load_ui",

src/themes/base.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -132,8 +132,14 @@ def set_toolbar_buttons(self, toolbar, icon_size=24, settings=None):
132132
"""Iterate through toolbar button settings, and apply them to each button.
133133
[{"text": "", "icon": ""},...]
134134
"""
135-
# List of colors for demonstration
136-
toolbar.clear()
135+
from qt_api import QT_API, isdeleted
136+
137+
# Clear toolbar without deleting actions on PySide6
138+
if QT_API == "pyside6":
139+
for action in list(toolbar.actions()):
140+
toolbar.removeAction(action)
141+
else:
142+
toolbar.clear()
137143

138144
# Set icon size
139145
qsize_icon = QSize(icon_size, icon_size)
@@ -178,6 +184,8 @@ def set_toolbar_buttons(self, toolbar, icon_size=24, settings=None):
178184

179185
# Create button from action
180186
if button_action:
187+
if QT_API == "pyside6" and isdeleted(button_action):
188+
continue
181189
toolbar.addAction(button_action)
182190
button_action.setVisible(button_visible)
183191
button = toolbar.widgetForAction(button_action)

src/windows/cutting.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -202,7 +202,7 @@ def __init__(self, file=None, preview=False):
202202
self.initialized = True
203203

204204
def eventFilter(self, obj, event):
205-
if event.type() == event.KeyPress and obj is self.txtName:
205+
if event.type() == QEvent.KeyPress and obj is self.txtName:
206206
# Handle ENTER key to create new clip
207207
if event.key() == Qt.Key_Return or event.key() == Qt.Key_Enter:
208208
if self.btnAddClip.isEnabled():
@@ -426,4 +426,3 @@ def closeEvent(self, event):
426426
self.r.Close()
427427
self.clip.Close()
428428
self.r.ClearAllCache()
429-

0 commit comments

Comments
 (0)