Destroy action-owned submenus

Signed-off-by: Loren Eteval <loren.eteval@proton.me>
This commit is contained in:
Loren Eteval
2026-09-19 11:12:00 +08:00
parent 15a53eb0b2
commit 29af6eab77
3 changed files with 38 additions and 1 deletions
+3 -1
View File
@@ -53,7 +53,9 @@ primitives; pages and services consume them without creating parallel registries
arbitrary worker calls to the GUI thread; choose an explicit queued owner-thread delivery boundary. A surviving
Python wrapper can already be natively invalid, so callback freshness and `shiboken6.isValid()` address different
failure modes. Neither replaces the strong owner required while asynchronous UI remains active.
- `AppQAction.callback` is strong by design, so the action owner cannot outlive the captured receiver.
- `AppQAction.callback` is strong by design, so the action owner cannot outlive the captured receiver. An action
also owns a submenu supplied without a QWidget parent and schedules its native deletion when the action dies;
`QAction.setMenu()` alone does not establish parent ownership. Explicitly parented menus retain their chosen owner.
- Every `QNetworkReply` has one manager/context owner, one freshness rule, and one terminal deletion path. Request
context must also be released when native destruction skips `finished`, including manager-first teardown with
retained Python wrappers. Use the shared network-manager tracking boundary; cleanup must not capture a reply
+6
View File
@@ -147,6 +147,12 @@ class AppQAction(Mixins.QTranslatable, Mixins.ThemeAware, QAction):
# Create reference
self._menu = menu
if menu.parent() is None:
# setMenu() associates a submenu without giving it a Qt parent.
# This action owns otherwise unparented menus; explicit widget
# parents retain their own lifetime. deleteLater is a native slot.
self.destroyed.connect(menu.deleteLater)
# Some old version PySide6 does not have setMenu method
# for QAction. Protect it. Currently only used in TrayIcon
if hasattr(self, 'setMenu') and useSetMenu:
+29
View File
@@ -220,6 +220,35 @@ class QtLifetimeTest(unittest.TestCase):
self.assertAllDestroyed(references, destroyed, 60)
def testActionOwnedMenusDieWithActionAndPreserveExplicitOwners(self):
"""QAction's menu association must not strand an unparented native menu."""
references = []
destroyed = []
for explicitOwner in (False, True):
for _ in range(30):
owner = QWidget()
menu = AppQMenu(parent=owner if explicitOwner else None)
action = AppQAction('Menu fixture', menu=menu, parent=owner)
references.append(weakref.ref(menu))
menu.destroyed.connect(lambda *_args: destroyed.append(True))
action.deleteLater()
processQtEvents()
self.assertFalse(isValid(action))
try:
self.assertEqual(isValid(menu), explicitOwner)
finally:
if isValid(menu):
menu.deleteLater()
owner.deleteLater()
processQtEvents()
del action, menu, owner
self.assertAllDestroyed(references, destroyed, 60)
def testReopenedDialogSurvivesPreviousPresentationCleanup(self):
"""A queued finish must not release the next asynchronous presentation."""
destroyed = []