mirror of
https://github.com/LorenEteval/Furious.git
synced 2026-10-13 09:48:10 +03:00
Keep navigation state session-local
Signed-off-by: Loren Eteval <loren.eteval@proton.me>
This commit is contained in:
@@ -215,7 +215,7 @@ class NavigationView(Mixins.QTranslatable, Mixins.ThemeAware, QWidget):
|
||||
translatable: bool = True,
|
||||
placement: str = 'top',
|
||||
):
|
||||
"""Register a page and create its navigation control."""
|
||||
"""Register a page, selecting the first registered page initially."""
|
||||
if not isinstance(pageId, str) or not pageId:
|
||||
raise ValueError('pageId must be a non-empty string')
|
||||
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
- `MainWindow` owns long-lived built-in pages, navigation, page-level managers, and compatibility aliases. Pages remain
|
||||
stable for the window lifetime; plugin pages are owned by navigation plus `PluginNavigationManager`.
|
||||
- Main-window page selection and navigation expansion are session-local. Register Home first as the canonical initial
|
||||
page and keep new navigation views collapsed; never persist or restore either state from a previous process.
|
||||
- Pages and dialogs adapt controllers/services/repositories to user interaction. Do not create a competing state
|
||||
authority; prefer controller/service APIs over new cross-page reach-through.
|
||||
- Long-lived pages create controls, timers, and signal connections once. Show/hide activates lazy rendering or refresh
|
||||
|
||||
@@ -45,8 +45,6 @@ logger = logging.getLogger(__name__)
|
||||
registerAppSettings('ServerWidgetWindowSize')
|
||||
registerAppSettings('AppMainWindowGeometry')
|
||||
registerAppSettings('AppMainWindowState')
|
||||
registerAppSettings('AppMainWindowSelectedPage', default='home')
|
||||
registerAppSettings('AppMainWindowNavigationExpanded', isBinary=True)
|
||||
|
||||
_TRANSLATABLE_NAVIGATION_LABELS = (
|
||||
_('Home'),
|
||||
@@ -114,6 +112,8 @@ class MainWindow(AppQMainWindow):
|
||||
if not isinstance(self.logPage, LogPage):
|
||||
raise TypeError('application log page must be a LogPage')
|
||||
|
||||
# NavigationView selects the first registered page. Keep Home first
|
||||
# so every new window session starts from the canonical product page.
|
||||
self.navigationView.addPage(
|
||||
'home',
|
||||
self.homePage,
|
||||
@@ -147,16 +147,6 @@ class MainWindow(AppQMainWindow):
|
||||
'gear-wide-connected.svg',
|
||||
placement='bottom',
|
||||
)
|
||||
self.navigationView.pageChanged.connect(self._pageChanged)
|
||||
self.navigationView.expandedChanged.connect(self._navigationExpandedChanged)
|
||||
|
||||
self.navigationView.setExpanded(
|
||||
AppSettings.isStateON_('AppMainWindowNavigationExpanded'),
|
||||
animated=False,
|
||||
)
|
||||
self.navigationView.setCurrentPage(
|
||||
str(AppSettings.get('AppMainWindowSelectedPage'))
|
||||
)
|
||||
self.setCentralWidget(self.navigationView)
|
||||
|
||||
# Preserve the established application-facing server-management API.
|
||||
@@ -169,19 +159,6 @@ class MainWindow(AppQMainWindow):
|
||||
|
||||
self.retranslate()
|
||||
|
||||
@QtCore.Slot(str)
|
||||
def _pageChanged(self, pageId: str):
|
||||
"""Persist the current page selected through navigation."""
|
||||
AppSettings.set('AppMainWindowSelectedPage', pageId)
|
||||
|
||||
@QtCore.Slot(bool)
|
||||
def _navigationExpandedChanged(self, expanded: bool):
|
||||
"""Persist the navigation menu expansion state."""
|
||||
if expanded:
|
||||
AppSettings.turnON_('AppMainWindowNavigationExpanded')
|
||||
else:
|
||||
AppSettings.turnOFF('AppMainWindowNavigationExpanded')
|
||||
|
||||
def showPage(self, pageId: str):
|
||||
"""Navigate to a registered application page."""
|
||||
self.navigationView.setCurrentPage(pageId)
|
||||
|
||||
@@ -25,7 +25,7 @@ from Furious.Controllers.ConnectionController import ConnectionController
|
||||
from Furious.Controllers.RoutingController import RoutingController
|
||||
from Furious.Controllers.SettingsController import SettingsController
|
||||
from Furious.Frozenlib import AppSettings
|
||||
from Furious.Service.LogManager import LogManager
|
||||
from Furious.Service import LogManager, PluginNavigationManager
|
||||
from Furious.Qt import AppQMainWindow
|
||||
from Furious.Widget.NavigationView import NavigationView
|
||||
from Furious.Window.HomePage import HomePage
|
||||
@@ -44,6 +44,8 @@ from tests.support import (
|
||||
processQtEvents,
|
||||
)
|
||||
|
||||
from contextlib import contextmanager
|
||||
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
@@ -97,6 +99,66 @@ class _GeometryWindow(AppQMainWindow):
|
||||
self.setCentralWidget(QWidget(parent=self))
|
||||
|
||||
|
||||
class _InertManager:
|
||||
"""Expose only the idle runtime collection used by controllers in UI tests."""
|
||||
|
||||
runtimes = ()
|
||||
|
||||
|
||||
@contextmanager
|
||||
def _realMainWindow():
|
||||
"""Own one fully composed MainWindow and its isolated application services."""
|
||||
app = application()
|
||||
oldServices = (
|
||||
app.connectionController,
|
||||
app.routingController,
|
||||
app.settingsController,
|
||||
app.logManager,
|
||||
app.logPage,
|
||||
)
|
||||
connectionController = ConnectionController(
|
||||
parent=app,
|
||||
coreManager=_InertManager(),
|
||||
updatesManager=_InertManager(),
|
||||
)
|
||||
logManager = LogManager(parent=app)
|
||||
routingController = None
|
||||
window = None
|
||||
|
||||
try:
|
||||
app.connectionController = connectionController
|
||||
routingController = RoutingController(parent=app)
|
||||
app.routingController = routingController
|
||||
app.settingsController = SettingsController()
|
||||
app.logManager = logManager
|
||||
app.logPage = LogPage(manager=logManager)
|
||||
|
||||
with patch.object(HomePage, 'serverImportActions', return_value=()):
|
||||
window = MainWindow()
|
||||
|
||||
yield window
|
||||
finally:
|
||||
if window is not None:
|
||||
window.close()
|
||||
window.deleteLater()
|
||||
collectAtBoundary()
|
||||
|
||||
(
|
||||
app.connectionController,
|
||||
app.routingController,
|
||||
app.settingsController,
|
||||
app.logManager,
|
||||
app.logPage,
|
||||
) = oldServices
|
||||
|
||||
if routingController is not None:
|
||||
routingController.deleteLater()
|
||||
|
||||
connectionController.deleteLater()
|
||||
logManager.deleteLater()
|
||||
collectAtBoundary()
|
||||
|
||||
|
||||
class AppQMainWindowLifecycleTest(unittest.TestCase):
|
||||
"""Keep shared first-show mechanics deterministic and subclass-safe."""
|
||||
|
||||
@@ -202,6 +264,151 @@ class AppQMainWindowLifecycleTest(unittest.TestCase):
|
||||
collectAtBoundary()
|
||||
|
||||
|
||||
class MainWindowNavigationSessionTest(unittest.TestCase):
|
||||
"""Keep page and expansion state live in one window, not in settings."""
|
||||
|
||||
FormerPageKey = 'AppMainWindowSelectedPage'
|
||||
FormerExpansionKey = 'AppMainWindowNavigationExpanded'
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
"""Create the process-wide headless QApplication."""
|
||||
application()
|
||||
|
||||
def tearDown(self):
|
||||
"""Destroy windows and drain deferred deletion between tests."""
|
||||
for window in list(AppQMainWindow._openWindows.values()):
|
||||
window.close()
|
||||
window.deleteLater()
|
||||
|
||||
collectAtBoundary()
|
||||
|
||||
def assertCanonicalPage(self, window: MainWindow):
|
||||
"""Assert the navigation selection and stack both point to Home."""
|
||||
self.assertEqual(window.navigationView.currentPageId(), 'home')
|
||||
self.assertIs(window.navigationView.page('home'), window.homePage)
|
||||
self.assertIs(
|
||||
window.navigationView.pageStack.currentWidget(),
|
||||
window.homePage,
|
||||
)
|
||||
|
||||
def testNewSessionUsesCanonicalPageAndIgnoresFormerValues(self):
|
||||
"""Ignore every representative value stored by the retired setting."""
|
||||
self.assertNotIn(self.FormerPageKey, AppSettings.SettingsPool)
|
||||
self.assertNotIn(self.FormerExpansionKey, AppSettings.SettingsPool)
|
||||
|
||||
formerValues = (
|
||||
None,
|
||||
'settings',
|
||||
'log',
|
||||
'metrics',
|
||||
'plugin:example:status',
|
||||
'removed-page',
|
||||
)
|
||||
|
||||
for formerValue in formerValues:
|
||||
with self.subTest(formerValue=formerValue), isolatedSettings() as settings:
|
||||
if formerValue is not None:
|
||||
settings.setValue(self.FormerPageKey, formerValue)
|
||||
|
||||
with _realMainWindow() as window:
|
||||
self.assertCanonicalPage(window)
|
||||
self.assertFalse(window.navigationView.isExpanded())
|
||||
|
||||
def testNavigationDoesNotCreateOrUpdateFormerSetting(self):
|
||||
"""Keep both absent and stale obsolete keys untouched by navigation."""
|
||||
with isolatedSettings() as settings, _realMainWindow() as window:
|
||||
window.showPage('settings')
|
||||
window.navigationView.setExpanded(True, animated=False)
|
||||
window.cleanup()
|
||||
|
||||
self.assertFalse(settings.contains(self.FormerPageKey))
|
||||
self.assertFalse(settings.contains(self.FormerExpansionKey))
|
||||
|
||||
with isolatedSettings() as settings:
|
||||
settings.setValue(self.FormerPageKey, 'log')
|
||||
settings.setValue(self.FormerExpansionKey, '1')
|
||||
|
||||
with _realMainWindow() as window:
|
||||
self.assertCanonicalPage(window)
|
||||
self.assertFalse(window.navigationView.isExpanded())
|
||||
window.showPage('settings')
|
||||
window.navigationView.setExpanded(True, animated=False)
|
||||
window.cleanup()
|
||||
|
||||
self.assertEqual(settings.value(self.FormerPageKey), 'log')
|
||||
self.assertEqual(settings.value(self.FormerExpansionKey), '1')
|
||||
|
||||
def testNewWindowResetsSessionStateButRestoresGeometry(self):
|
||||
"""Reset page and expansion while retaining persistent geometry."""
|
||||
expectedSize = QtCore.QSize(760, 600)
|
||||
|
||||
with isolatedSettings() as settings:
|
||||
with _realMainWindow() as firstWindow:
|
||||
firstWindow.show()
|
||||
firstWindow.resize(expectedSize)
|
||||
firstWindow.navigationView.setExpanded(True, animated=False)
|
||||
firstWindow.showPage('settings')
|
||||
firstWindow.cleanup()
|
||||
|
||||
self.assertEqual(
|
||||
firstWindow.navigationView.currentPageId(),
|
||||
'settings',
|
||||
)
|
||||
self.assertTrue(firstWindow.navigationView.isExpanded())
|
||||
|
||||
self.assertFalse(settings.contains(self.FormerPageKey))
|
||||
self.assertFalse(settings.contains(self.FormerExpansionKey))
|
||||
|
||||
with _realMainWindow() as secondWindow:
|
||||
secondWindow.show()
|
||||
|
||||
self.assertCanonicalPage(secondWindow)
|
||||
self.assertFalse(secondWindow.navigationView.isExpanded())
|
||||
self.assertEqual(secondWindow.size(), expectedSize)
|
||||
|
||||
def testHideAndShowPreservesLivePage(self):
|
||||
"""Do not reset the page while reusing the same MainWindow instance."""
|
||||
with isolatedSettings(), _realMainWindow() as window:
|
||||
window.show()
|
||||
window.showPage('settings')
|
||||
window.hide()
|
||||
window.show()
|
||||
processQtEvents()
|
||||
|
||||
self.assertEqual(window.navigationView.currentPageId(), 'settings')
|
||||
self.assertIs(
|
||||
window.navigationView.pageStack.currentWidget(),
|
||||
window.settingsPage,
|
||||
)
|
||||
|
||||
def testPluginRegistrationCannotReplaceCanonicalPage(self):
|
||||
"""Keep Home selected even when a stale plugin page is registered."""
|
||||
|
||||
def registerPluginPage(_manager, navigationView):
|
||||
pluginPage = QWidget(parent=navigationView)
|
||||
navigationView.addPage(
|
||||
'plugin:example:status',
|
||||
pluginPage,
|
||||
'Plugin',
|
||||
'house-door.svg',
|
||||
)
|
||||
|
||||
with isolatedSettings() as settings:
|
||||
settings.setValue(self.FormerPageKey, 'plugin:example:status')
|
||||
|
||||
with patch.object(
|
||||
PluginNavigationManager,
|
||||
'registerPages',
|
||||
autospec=True,
|
||||
side_effect=registerPluginPage,
|
||||
), _realMainWindow() as window:
|
||||
self.assertCanonicalPage(window)
|
||||
self.assertIsNotNone(
|
||||
window.navigationView.page('plugin:example:status')
|
||||
)
|
||||
|
||||
|
||||
class MainWindowGeometryTest(unittest.TestCase):
|
||||
"""Keep restoration driven by persisted-state validity, not dimensions."""
|
||||
|
||||
@@ -232,110 +439,53 @@ class MainWindowGeometryTest(unittest.TestCase):
|
||||
def testRealMainWindowUsesSharedLifecycleAfterComposition(self):
|
||||
"""Construct the complete page hierarchy before first-show preparation."""
|
||||
app = application()
|
||||
oldServices = (
|
||||
app.connectionController,
|
||||
app.routingController,
|
||||
app.settingsController,
|
||||
app.logManager,
|
||||
app.logPage,
|
||||
)
|
||||
with isolatedSettings(), _realMainWindow() as window:
|
||||
with patch('Furious.Qt.QtWidgets.moveToCenter') as moveToCenter:
|
||||
window.show()
|
||||
|
||||
class _InertManager:
|
||||
"""Expose only the idle runtime collection used by the controller."""
|
||||
self.assertEqual(
|
||||
sum(
|
||||
isinstance(widget, MainWindow)
|
||||
for widget in app.topLevelWidgets()
|
||||
),
|
||||
1,
|
||||
)
|
||||
self.assertEqual(len(window.findChildren(NavigationView)), 1)
|
||||
self.assertIs(window.centralWidget(), window.navigationView)
|
||||
|
||||
runtimes = ()
|
||||
self.assertEqual(window.size(), window.DEFAULT_WINDOW_SIZE)
|
||||
moveToCenter.assert_called_once_with(window)
|
||||
|
||||
connectionController = ConnectionController(
|
||||
parent=app,
|
||||
coreManager=_InertManager(),
|
||||
updatesManager=_InertManager(),
|
||||
)
|
||||
logManager = LogManager(parent=app)
|
||||
routingController = None
|
||||
window = None
|
||||
window.move(83, 97)
|
||||
movedPosition = QtCore.QPoint(window.pos())
|
||||
window.hide()
|
||||
window.show()
|
||||
|
||||
try:
|
||||
with isolatedSettings():
|
||||
app.connectionController = connectionController
|
||||
routingController = RoutingController(parent=app)
|
||||
app.routingController = routingController
|
||||
app.settingsController = SettingsController()
|
||||
app.logManager = logManager
|
||||
app.logPage = LogPage(manager=logManager)
|
||||
self.assertEqual(window.pos(), movedPosition)
|
||||
moveToCenter.assert_called_once_with(window)
|
||||
|
||||
with patch.object(HomePage, 'serverImportActions', return_value=()):
|
||||
window = MainWindow()
|
||||
expectedPages = {
|
||||
'home': window.homePage,
|
||||
'log': window.logPage,
|
||||
'subscription': window.subscriptionPage,
|
||||
'metrics': window.metricsPage,
|
||||
'settings': window.settingsPage,
|
||||
}
|
||||
|
||||
with patch('Furious.Qt.QtWidgets.moveToCenter') as moveToCenter:
|
||||
window.show()
|
||||
for pageId, page in expectedPages.items():
|
||||
with self.subTest(pageId=pageId):
|
||||
window.showPage(pageId)
|
||||
processQtEvents()
|
||||
|
||||
self.assertEqual(
|
||||
sum(
|
||||
isinstance(widget, MainWindow)
|
||||
for widget in app.topLevelWidgets()
|
||||
),
|
||||
1,
|
||||
window.navigationView.currentPageId(),
|
||||
pageId,
|
||||
)
|
||||
self.assertIs(window.navigationView.page(pageId), page)
|
||||
self.assertIs(
|
||||
window.navigationView.pageStack.currentWidget(),
|
||||
page,
|
||||
)
|
||||
self.assertEqual(len(window.findChildren(NavigationView)), 1)
|
||||
self.assertIs(window.centralWidget(), window.navigationView)
|
||||
|
||||
self.assertEqual(window.size(), window.DEFAULT_WINDOW_SIZE)
|
||||
moveToCenter.assert_called_once_with(window)
|
||||
|
||||
window.move(83, 97)
|
||||
movedPosition = QtCore.QPoint(window.pos())
|
||||
window.hide()
|
||||
window.show()
|
||||
|
||||
self.assertEqual(window.pos(), movedPosition)
|
||||
moveToCenter.assert_called_once_with(window)
|
||||
|
||||
expectedPages = {
|
||||
'home': window.homePage,
|
||||
'log': window.logPage,
|
||||
'subscription': window.subscriptionPage,
|
||||
'metrics': window.metricsPage,
|
||||
'settings': window.settingsPage,
|
||||
}
|
||||
|
||||
for pageId, page in expectedPages.items():
|
||||
with self.subTest(pageId=pageId):
|
||||
window.showPage(pageId)
|
||||
processQtEvents()
|
||||
|
||||
self.assertEqual(
|
||||
window.navigationView.currentPageId(),
|
||||
pageId,
|
||||
)
|
||||
self.assertIs(window.navigationView.page(pageId), page)
|
||||
self.assertIs(
|
||||
window.navigationView.pageStack.currentWidget(),
|
||||
page,
|
||||
)
|
||||
|
||||
window.close()
|
||||
window.deleteLater()
|
||||
window = None
|
||||
collectAtBoundary()
|
||||
finally:
|
||||
if window is not None:
|
||||
window.close()
|
||||
window.deleteLater()
|
||||
|
||||
(
|
||||
app.connectionController,
|
||||
app.routingController,
|
||||
app.settingsController,
|
||||
app.logManager,
|
||||
app.logPage,
|
||||
) = oldServices
|
||||
|
||||
if routingController is not None:
|
||||
routingController.deleteLater()
|
||||
|
||||
connectionController.deleteLater()
|
||||
logManager.deleteLater()
|
||||
collectAtBoundary()
|
||||
|
||||
def testFirstLaunchUsesCanonicalDefault(self):
|
||||
"""Use the product default when modern and legacy settings are absent."""
|
||||
|
||||
Reference in New Issue
Block a user