From ec6d01e4b13c0fd271eaa1e444df66d99e19cb7b Mon Sep 17 00:00:00 2001 From: Loren Eteval Date: Sun, 30 Aug 2026 17:29:31 +0800 Subject: [PATCH] fix: preserve TUN switch animations Keep user-initiated switch animation running when the shared settings controller echoes the committed TUN state, and animate the peer surface without emitting a recursive request. Preserve snap synchronization for initialization and rollback, with real Qt coverage for both directions and rapid toggles. Signed-off-by: Loren Eteval --- Furious/Qt/QtWidgets.py | 17 +++++++++++++ Furious/Window/HomePage.py | 4 ++- Furious/Window/SettingsPage.py | 2 +- tests/test_qt_interactions.py | 46 ++++++++++++++++++++++++++++++++-- 4 files changed, 65 insertions(+), 4 deletions(-) diff --git a/Furious/Qt/QtWidgets.py b/Furious/Qt/QtWidgets.py index 9e8eae7..22981d7 100644 --- a/Furious/Qt/QtWidgets.py +++ b/Furious/Qt/QtWidgets.py @@ -148,6 +148,23 @@ class AppQSwitch(Mixins.ThemeAware, QCheckBox): del blocker + def syncCheckedAnimated(self, checked: bool): + """Animate changed external state without emitting another request.""" + checked = bool(checked) + + if self.isChecked() == checked: + # A user toggle already started the canonical animation before its + # committed state was broadcast back through the shared authority. + return + + blocker = QtCore.QSignalBlocker(self) + + self.setChecked(checked) + + del blocker + + self._animateToggle(checked) + def hitButton(self, position: QtCore.QPoint) -> bool: """Make the complete custom-painted switch track interactive.""" return self.rect().contains(position) diff --git a/Furious/Window/HomePage.py b/Furious/Window/HomePage.py index 8789b79..1bcf54f 100644 --- a/Furious/Window/HomePage.py +++ b/Furious/Window/HomePage.py @@ -825,7 +825,9 @@ class HomePage(Mixins.QTranslatable, QMainWindow): AppSettingsController().systemProxyModeChanged.connect( self._syncSystemProxyMode ) - AppSettingsController().tunModeChanged.connect(self.tunModeSwitch.syncChecked) + AppSettingsController().tunModeChanged.connect( + self.tunModeSwitch.syncCheckedAnimated + ) AppConnectionController().interactionEnabledChanged.connect( self.setConnectionControlsEnabled diff --git a/Furious/Window/SettingsPage.py b/Furious/Window/SettingsPage.py index e3724e2..fcbbb07 100644 --- a/Furious/Window/SettingsPage.py +++ b/Furious/Window/SettingsPage.py @@ -661,7 +661,7 @@ class SettingsPage(Mixins.QTranslatable, QMainWindow): self.tunModeCard.checkBox.setEnabled(self._tunModeAvailable) AppSettingsController().tunModeChanged.connect( - self.tunModeCard.checkBox.syncChecked + self.tunModeCard.checkBox.syncCheckedAnimated ) ( diff --git a/tests/test_qt_interactions.py b/tests/test_qt_interactions.py index 33c6ed6..fd7a617 100644 --- a/tests/test_qt_interactions.py +++ b/tests/test_qt_interactions.py @@ -25,7 +25,7 @@ from Furious.Frozenlib import AppBuiltinProxyMode, AppSettings from Furious.Models import CoreConfiguration, ServerProfile from Furious.Plugins.API import RoutingOption from Furious.Repository import Storage, SubscriptionGroup -from Furious.Qt import AppQDialog +from Furious.Qt import AppQDialog, AppQSwitch from Furious.Widget.RoutingSelector import RoutingSelector from Furious.Widget.ServerTableView import ServerTableView from Furious.Window.HomePage import HomePage @@ -690,7 +690,9 @@ class SharedSettingsQtWorkflowTest(unittest.TestCase): ) settingsController.systemProxyModeChanged.connect(proxyCard.sync) - settingsController.tunModeChanged.connect(tunCard.checkBox.syncChecked) + settingsController.tunModeChanged.connect( + tunCard.checkBox.syncCheckedAnimated + ) settingsLayout.addWidget(proxyCard) settingsLayout.addWidget(tunCard) @@ -740,10 +742,20 @@ class SharedSettingsQtWorkflowTest(unittest.TestCase): QtCore.Qt.MouseButton.LeftButton, pos=home.tunModeSwitch.rect().center(), ) + + QTest.qWait(40) + + for switch in (home.tunModeSwitch, tunCard.checkBox): + self.assertGreater(switch.thumbPosition, 0.0) + self.assertLess(switch.thumbPosition, 1.0) + + QTest.qWait(AppQSwitch.AnimationDuration) processQtEvents() self.assertTrue(home.tunModeSwitch.isChecked()) self.assertTrue(tunCard.checkBox.isChecked()) + self.assertEqual(home.tunModeSwitch.thumbPosition, 1.0) + self.assertEqual(tunCard.checkBox.thumbPosition, 1.0) self.assertEqual(tunStates, [True]) QTest.mouseClick( @@ -751,12 +763,42 @@ class SharedSettingsQtWorkflowTest(unittest.TestCase): QtCore.Qt.MouseButton.LeftButton, pos=tunCard.checkBox.rect().center(), ) + + QTest.qWait(40) + + for switch in (home.tunModeSwitch, tunCard.checkBox): + self.assertGreater(switch.thumbPosition, 0.0) + self.assertLess(switch.thumbPosition, 1.0) + + QTest.qWait(AppQSwitch.AnimationDuration) processQtEvents() self.assertFalse(home.tunModeSwitch.isChecked()) self.assertFalse(tunCard.checkBox.isChecked()) + self.assertEqual(home.tunModeSwitch.thumbPosition, 0.0) + self.assertEqual(tunCard.checkBox.thumbPosition, 0.0) self.assertEqual(tunStates, [True, False]) + QTest.mouseClick( + home.tunModeSwitch, + QtCore.Qt.MouseButton.LeftButton, + pos=home.tunModeSwitch.rect().center(), + ) + QTest.qWait(20) + QTest.mouseClick( + home.tunModeSwitch, + QtCore.Qt.MouseButton.LeftButton, + pos=home.tunModeSwitch.rect().center(), + ) + QTest.qWait(AppQSwitch.AnimationDuration + 40) + processQtEvents() + + self.assertFalse(home.tunModeSwitch.isChecked()) + self.assertFalse(tunCard.checkBox.isChecked()) + self.assertEqual(home.tunModeSwitch.thumbPosition, 0.0) + self.assertEqual(tunCard.checkBox.thumbPosition, 0.0) + self.assertEqual(tunStates, [True, False, True, False]) + connectionController.setInteractionEnabled(False) processQtEvents()