mirror of
https://github.com/LorenEteval/Furious.git
synced 2026-09-22 23:08:08 +03:00
Unify asynchronous dialog ownership
Signed-off-by: Loren Eteval <loren.eteval@proton.me>
This commit is contained in:
+3
-3
@@ -360,9 +360,9 @@ Reusable dialogs follow a different policy. A reusable dialog is normally hidden
|
|||||||
`WA_DeleteOnClose` merely to make cleanup uniform.
|
`WA_DeleteOnClose` merely to make cleanup uniform.
|
||||||
|
|
||||||
Furious implements these policies through `AppQDialog`, `AppQTransientDialog`, and
|
Furious implements these policies through `AppQDialog`, `AppQTransientDialog`, and
|
||||||
`AppQMessageBox`. `AppQMessageBox` currently has an additional registry because its
|
`AppQMessageBox`. Their asynchronous `open()` paths share the `AppQDialog` lifetime
|
||||||
QMessageBox-compatible `open()` path bypasses the base implementation; its cleanup must
|
registry; message-box presentation behavior must delegate ownership to that base path
|
||||||
remain synchronized with the base dialog policy or be deliberately consolidated.
|
rather than introduce a parallel registry.
|
||||||
|
|
||||||
## 14. Packaged and Compiled Builds Require Extra Caution
|
## 14. Packaged and Compiled Builds Require Extra Caution
|
||||||
|
|
||||||
|
|||||||
@@ -9,8 +9,8 @@ Use the `manage-qt-pyside6-lifetimes` skill for Qt ownership or lifecycle work.
|
|||||||
- Pass source text at construction when a control retains it for retranslation. Preserve focus, keyboard, shortcuts,
|
- Pass source text at construction when a control retains it for retranslation. Preserve focus, keyboard, shortcuts,
|
||||||
accessibility, translated-text growth, high-DPI, responsive layout, and both themes.
|
accessibility, translated-text growth, high-DPI, responsive layout, and both themes.
|
||||||
- `AppQMessageBox.windowTitle` is native metadata. Visible hierarchy is `heading`, `text`, then `informativeText`.
|
- `AppQMessageBox.windowTitle` is native metadata. Visible hierarchy is `heading`, `text`, then `informativeText`.
|
||||||
`AppQMessageBox.open()` bypasses `AppQDialog.open()`, so its separate message-box registry is its actual async owner;
|
`AppQMessageBox.open()` delegates asynchronous ownership to `AppQDialog.open()`; do not add a parallel message-box
|
||||||
keep that registry and the base destroyed cleanup balanced.
|
registry or release a transient box before native destruction completes.
|
||||||
- `AppStyleSheet` is the public style authority. `StyleSheets` contains internal QSS fragments that consume the semantic
|
- `AppStyleSheet` is the public style authority. `StyleSheets` contains internal QSS fragments that consume the semantic
|
||||||
palette; application code does not import fragments directly.
|
palette; application code does not import fragments directly.
|
||||||
|
|
||||||
|
|||||||
+3
-42
@@ -943,12 +943,6 @@ class AppQMessageBox(AppQTransientDialog):
|
|||||||
MultipleActionBaseWidth = 520
|
MultipleActionBaseWidth = 520
|
||||||
MaximumSurfaceWidth = 720
|
MaximumSurfaceWidth = 720
|
||||||
|
|
||||||
# AppQDialog already wires a cleanup callback for its asynchronous registry.
|
|
||||||
# This message-box-specific registry repeats that cleanup so open() can bypass
|
|
||||||
# AppQDialog.open() while preserving QMessageBox-compatible show behavior.
|
|
||||||
# The duplicate cleanup is harmless and idempotent, but future changes must
|
|
||||||
# keep both registries synchronized or consolidate them deliberately.
|
|
||||||
_openMessageBoxes = {}
|
|
||||||
_standardButtonOrder = (
|
_standardButtonOrder = (
|
||||||
StandardButton.Ok,
|
StandardButton.Ok,
|
||||||
StandardButton.Save,
|
StandardButton.Save,
|
||||||
@@ -970,19 +964,6 @@ class AppQMessageBox(AppQTransientDialog):
|
|||||||
StandardButton.RestoreDefaults,
|
StandardButton.RestoreDefaults,
|
||||||
)
|
)
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _releaseOpenMessageBox(key, *_args):
|
|
||||||
"""Release an asynchronously opened message box after destruction."""
|
|
||||||
AppQMessageBox._openMessageBoxes.pop(key, None)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _scheduleOpenMessageBoxRelease(key, *_args):
|
|
||||||
"""Release an async message box after destroyed-signal dispatch."""
|
|
||||||
QtCore.QTimer.singleShot(
|
|
||||||
0,
|
|
||||||
functools.partial(AppQMessageBox._releaseOpenMessageBox, key),
|
|
||||||
)
|
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
"""Initialize the message box while preserving QMessageBox arguments."""
|
"""Initialize the message box while preserving QMessageBox arguments."""
|
||||||
windowTitle = kwargs.pop('windowTitle', None)
|
windowTitle = kwargs.pop('windowTitle', None)
|
||||||
@@ -1011,7 +992,6 @@ class AppQMessageBox(AppQTransientDialog):
|
|||||||
|
|
||||||
super().__init__(parent=parent, **kwargs)
|
super().__init__(parent=parent, **kwargs)
|
||||||
|
|
||||||
self._lifetimeKey = object()
|
|
||||||
self._windowMask = None
|
self._windowMask = None
|
||||||
self._icon = self.Icon.NoIcon
|
self._icon = self.Icon.NoIcon
|
||||||
self._heading = ''
|
self._heading = ''
|
||||||
@@ -1026,15 +1006,6 @@ class AppQMessageBox(AppQTransientDialog):
|
|||||||
self._clickedButton = None
|
self._clickedButton = None
|
||||||
self._handlingButton = False
|
self._handlingButton = False
|
||||||
|
|
||||||
scheduleRelease = functools.partial(
|
|
||||||
AppQMessageBox._scheduleOpenMessageBoxRelease,
|
|
||||||
self._lifetimeKey,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Message boxes are delete-on-close. Their asynchronous owner releases
|
|
||||||
# them only after native destruction and its signal dispatch complete.
|
|
||||||
self.destroyed.connect(scheduleRelease)
|
|
||||||
|
|
||||||
self.setObjectName('AppMessageBox')
|
self.setObjectName('AppMessageBox')
|
||||||
|
|
||||||
windowFlags = (
|
windowFlags = (
|
||||||
@@ -1821,20 +1792,10 @@ class AppQMessageBox(AppQTransientDialog):
|
|||||||
return QDialog.exec(self)
|
return QDialog.exec(self)
|
||||||
|
|
||||||
def open(self):
|
def open(self):
|
||||||
"""Open and retain the message box until it finishes or is destroyed."""
|
"""Prepare and open using the shared transient-dialog lifetime owner."""
|
||||||
key = self._lifetimeKey
|
self._prepareForPresentation()
|
||||||
AppQMessageBox._openMessageBoxes[key] = self
|
|
||||||
|
|
||||||
try:
|
return super().open()
|
||||||
self._prepareForPresentation()
|
|
||||||
|
|
||||||
return QDialog.open(self)
|
|
||||||
except Exception:
|
|
||||||
# Any non-exit exceptions
|
|
||||||
|
|
||||||
AppQMessageBox._releaseOpenMessageBox(key)
|
|
||||||
|
|
||||||
raise
|
|
||||||
|
|
||||||
def done(self, result):
|
def done(self, result):
|
||||||
"""Release the dimming mask before completing the transient dialog."""
|
"""Release the dimming mask before completing the transient dialog."""
|
||||||
|
|||||||
@@ -123,11 +123,10 @@ class DialogGeometryTest(unittest.TestCase):
|
|||||||
application()
|
application()
|
||||||
|
|
||||||
def tearDown(self):
|
def tearDown(self):
|
||||||
"""Drain deferred deletion and require both async registries to settle."""
|
"""Drain deferred deletion and require async ownership to settle."""
|
||||||
collectAtBoundary()
|
collectAtBoundary()
|
||||||
|
|
||||||
self.assertEqual(AppQDialog._openDialogs, {})
|
self.assertEqual(AppQDialog._openDialogs, {})
|
||||||
self.assertEqual(AppQMessageBox._openMessageBoxes, {})
|
|
||||||
|
|
||||||
def dispose(self, dialog):
|
def dispose(self, dialog):
|
||||||
"""Destroy a reusable fixture after its assertion scope."""
|
"""Destroy a reusable fixture after its assertion scope."""
|
||||||
@@ -309,17 +308,25 @@ class DialogGeometryTest(unittest.TestCase):
|
|||||||
buttons=AppQMessageBox.StandardButton.Ok,
|
buttons=AppQMessageBox.StandardButton.Ok,
|
||||||
)
|
)
|
||||||
key = openBox._lifetimeKey
|
key = openBox._lifetimeKey
|
||||||
|
heldAtFinished = []
|
||||||
|
openBox.finished.connect(
|
||||||
|
lambda _result: heldAtFinished.append(key in AppQDialog._openDialogs)
|
||||||
|
)
|
||||||
|
|
||||||
openBox.open()
|
openBox.open()
|
||||||
processQtEvents()
|
processQtEvents()
|
||||||
|
|
||||||
self.assertEqual(openEvents, [True])
|
self.assertEqual(openEvents, [True])
|
||||||
self.assertIn(key, AppQMessageBox._openMessageBoxes)
|
self.assertIn(key, AppQDialog._openDialogs)
|
||||||
|
|
||||||
openBox.accept()
|
openBox.accept()
|
||||||
|
|
||||||
|
self.assertEqual(heldAtFinished, [True])
|
||||||
|
self.assertIn(key, AppQDialog._openDialogs)
|
||||||
|
|
||||||
collectAtBoundary()
|
collectAtBoundary()
|
||||||
|
|
||||||
self.assertNotIn(key, AppQMessageBox._openMessageBoxes)
|
self.assertNotIn(key, AppQDialog._openDialogs)
|
||||||
|
|
||||||
execEvents = []
|
execEvents = []
|
||||||
execBox = CountingMessageBox(
|
execBox = CountingMessageBox(
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ class IsolatedDisplayMatrixTest(unittest.TestCase):
|
|||||||
"""Keep navigation and transient dialogs stable across supported scales."""
|
"""Keep navigation and transient dialogs stable across supported scales."""
|
||||||
|
|
||||||
Script = r"""
|
Script = r"""
|
||||||
from Furious.Qt import AppQMessageBox, AppStyleSheet
|
from Furious.Qt import AppQDialog, AppQMessageBox, AppStyleSheet
|
||||||
from Furious.Widget import NavigationView
|
from Furious.Widget import NavigationView
|
||||||
|
|
||||||
from PySide6 import QtCore
|
from PySide6 import QtCore
|
||||||
@@ -96,7 +96,7 @@ navigation.close()
|
|||||||
navigation.deleteLater()
|
navigation.deleteLater()
|
||||||
collectAtBoundary()
|
collectAtBoundary()
|
||||||
|
|
||||||
assert not AppQMessageBox._openMessageBoxes
|
assert not AppQDialog._openDialogs
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def testScaleAndThemeMatrix(self):
|
def testScaleAndThemeMatrix(self):
|
||||||
|
|||||||
@@ -130,7 +130,6 @@ class QtLifetimeTest(unittest.TestCase):
|
|||||||
collectAtBoundary()
|
collectAtBoundary()
|
||||||
|
|
||||||
self.assertEqual(AppQDialog._openDialogs, {})
|
self.assertEqual(AppQDialog._openDialogs, {})
|
||||||
self.assertEqual(AppQMessageBox._openMessageBoxes, {})
|
|
||||||
|
|
||||||
def assertAllDestroyed(self, references, destroyed, expected):
|
def assertAllDestroyed(self, references, destroyed, expected):
|
||||||
"""Assert weak wrappers and native destroyed signals agree."""
|
"""Assert weak wrappers and native destroyed signals agree."""
|
||||||
|
|||||||
@@ -71,6 +71,7 @@ from Furious.Plugins.API import RoutingOption
|
|||||||
from Furious.Qt import (
|
from Furious.Qt import (
|
||||||
AppHue,
|
AppHue,
|
||||||
AppQComboBox,
|
AppQComboBox,
|
||||||
|
AppQDialog,
|
||||||
AppQMessageBox,
|
AppQMessageBox,
|
||||||
AppQSwitch,
|
AppQSwitch,
|
||||||
AppStyleSheet,
|
AppStyleSheet,
|
||||||
@@ -1631,7 +1632,7 @@ class DialogBehaviorTest(unittest.TestCase):
|
|||||||
dialog.deleteRule()
|
dialog.deleteRule()
|
||||||
dialog.editRule()
|
dialog.editRule()
|
||||||
|
|
||||||
self.assertEqual(AppQMessageBox._openMessageBoxes, {})
|
self.assertEqual(AppQDialog._openDialogs, {})
|
||||||
self.assertEqual(dialog.routing['rules'], [])
|
self.assertEqual(dialog.routing['rules'], [])
|
||||||
|
|
||||||
dialog.closeWindowButton.click()
|
dialog.closeWindowButton.click()
|
||||||
@@ -1911,7 +1912,7 @@ class DialogBehaviorTest(unittest.TestCase):
|
|||||||
finished,
|
finished,
|
||||||
[int(AppQMessageBox.StandardButton.Cancel)],
|
[int(AppQMessageBox.StandardButton.Cancel)],
|
||||||
)
|
)
|
||||||
self.assertEqual(AppQMessageBox._openMessageBoxes, {})
|
self.assertEqual(AppQDialog._openDialogs, {})
|
||||||
|
|
||||||
|
|
||||||
class SharedConnectionPresentationTest(unittest.TestCase):
|
class SharedConnectionPresentationTest(unittest.TestCase):
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ from Furious.Backends.ExternalCore import ConfigExternalCore, ExternalCoreProces
|
|||||||
from Furious.Interface import ApplicationRunner
|
from Furious.Interface import ApplicationRunner
|
||||||
from Furious.Models import LogCategory
|
from Furious.Models import LogCategory
|
||||||
from Furious.Plugins import FuriousPlugin, PluginMetadata, PluginRegistry
|
from Furious.Plugins import FuriousPlugin, PluginMetadata, PluginRegistry
|
||||||
from Furious.Qt import AppQMessageBox, AppQTransientDialog
|
from Furious.Qt import AppQDialog, AppQMessageBox, AppQTransientDialog
|
||||||
from Furious.Utility import AppMainProcess
|
from Furious.Utility import AppMainProcess
|
||||||
from Furious.Service import APPLICATION_LOG_CATEGORY, LogManager, MetricsHistory
|
from Furious.Service import APPLICATION_LOG_CATEGORY, LogManager, MetricsHistory
|
||||||
from Furious.Widget import NavigationView
|
from Furious.Widget import NavigationView
|
||||||
@@ -329,7 +329,7 @@ class VeryHeavyContractTest(unittest.TestCase):
|
|||||||
self.assertEqual(resourceSnapshot()['threads'], baseline['threads'])
|
self.assertEqual(resourceSnapshot()['threads'], baseline['threads'])
|
||||||
|
|
||||||
def testFiveHundredMessageBoxesLeaveNoRegistryEntries(self):
|
def testFiveHundredMessageBoxesLeaveNoRegistryEntries(self):
|
||||||
"""Exercise the specialized asynchronous registry at release scale."""
|
"""Exercise shared asynchronous dialog ownership at release scale."""
|
||||||
references = []
|
references = []
|
||||||
|
|
||||||
for index in range(500):
|
for index in range(500):
|
||||||
@@ -350,7 +350,7 @@ class VeryHeavyContractTest(unittest.TestCase):
|
|||||||
collectAtBoundary()
|
collectAtBoundary()
|
||||||
|
|
||||||
self.assertTrue(all(reference() is None for reference in references))
|
self.assertTrue(all(reference() is None for reference in references))
|
||||||
self.assertFalse(AppQMessageBox._openMessageBoxes)
|
self.assertFalse(AppQDialog._openDialogs)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
|||||||
Reference in New Issue
Block a user