feat: synchronize Home network controls

Signed-off-by: Loren Eteval <loren.eteval@proton.me>
This commit is contained in:
Loren Eteval
2026-08-29 19:11:20 +08:00
parent 2846e1a8c6
commit 2703f3db39
6 changed files with 179 additions and 27 deletions
+6 -4
View File
@@ -633,7 +633,7 @@ class DesktopApplication(ApplicationRunner, SingletonApplication):
) = (
ConnectionController(parent=self),
RoutingController(parent=self),
SettingsController(),
SettingsController(parent=self),
)
self.connectionController.interactionEnabledChanged.connect(
@@ -656,7 +656,11 @@ class DesktopApplication(ApplicationRunner, SingletonApplication):
logger.exception('connection controller shutdown failed')
for controllerName in ('routingController', 'connectionController'):
for controllerName in (
'settingsController',
'routingController',
'connectionController',
):
controller = getattr(self, controllerName)
if isinstance(controller, QtCore.QObject):
@@ -664,8 +668,6 @@ class DesktopApplication(ApplicationRunner, SingletonApplication):
setattr(self, controllerName, None)
self.settingsController = None
def _initializeThemeDetection(self):
"""Start the one application-owned theme observer."""
logger.info('theme detect method uses timer implementation')
+35 -9
View File
@@ -35,6 +35,7 @@ __all__ = [
'LOG_AUTO_SCROLL_DOWN_SETTING',
'LOG_AUTO_CLEAR_SETTING',
'PROXY_ENDPOINT_INFO_SETTING',
'SYSTEM_PROXY_MODE_OPTIONS',
'SettingsController',
]
@@ -43,6 +44,10 @@ APPLICATION_THEME_SETTING = 'ApplicationTheme'
_LEGACY_DARK_MODE_SETTING = 'DarkMode'
LOG_AUTO_SCROLL_DOWN_SETTING = 'LogAutoScrollDown'
LOG_AUTO_CLEAR_SETTING = 'LogAutoClear'
SYSTEM_PROXY_MODE_OPTIONS = (
('Automatically Configure System Proxy', AppBuiltinProxyMode.Auto.value),
('Do Not Change System Proxy', AppBuiltinProxyMode.NoChanges.value),
)
registerAppSettings('VPNMode', isBinary=True)
registerAppSettings(
@@ -136,11 +141,16 @@ def _synchronizeLegacyDarkModeSetting():
)
class SettingsController:
class SettingsController(QtCore.QObject):
"""Apply application settings independently from their presentation."""
def __init__(self):
tunModeChanged = QtCore.Signal(bool)
systemProxyModeChanged = QtCore.Signal(str)
def __init__(self, parent=None):
"""Synchronize theme persistence after Qt application metadata is ready."""
super().__init__(parent)
_synchronizeLegacyDarkModeSetting()
@staticmethod
@@ -151,13 +161,26 @@ class SettingsController:
else:
AppSettings.turnOFF(settingName)
@classmethod
def setTUNMode(cls, enabled: bool):
@staticmethod
def tunModeAvailable() -> bool:
"""Return whether this process can enable application-managed TUN."""
if PLATFORM == 'Linux':
return not SystemRuntime.flatpakID()
return SystemRuntime.isAdmin()
def setTUNMode(self, enabled: bool):
"""Persist the global TUN mode and notify active workflows."""
if PLATFORM != 'Linux':
assert SystemRuntime.isAdmin()
cls._setBinary('VPNMode', enabled)
enabled = bool(enabled)
if AppSettings.isStateON_('VPNMode') == enabled:
return
self._setBinary('VPNMode', enabled)
self.tunModeChanged.emit(enabled)
showMBoxNewChangesNextTime()
@@ -245,13 +268,16 @@ class SettingsController:
showMBoxNewChangesNextTime()
@staticmethod
def setSystemProxyMode(mode: str):
def setSystemProxyMode(self, mode: str):
"""Persist how Furious manages the operating-system proxy."""
validModes = tuple(item.value for item in AppBuiltinProxyMode)
if mode in validModes:
AppSettings.set('SystemProxyMode', mode)
if mode not in validModes or AppSettings.get('SystemProxyMode') == mode:
return
AppSettings.set('SystemProxyMode', mode)
self.systemProxyModeChanged.emit(mode)
@classmethod
def setAutoUpdateAssets(cls, enabled: bool):
+2
View File
@@ -28,6 +28,7 @@ from .SettingsController import (
LOG_AUTO_CLEAR_SETTING,
LOG_AUTO_SCROLL_DOWN_SETTING,
PROXY_ENDPOINT_INFO_SETTING,
SYSTEM_PROXY_MODE_OPTIONS,
SettingsController,
)
@@ -36,6 +37,7 @@ __all__ = [
'LOG_AUTO_CLEAR_SETTING',
'LOG_AUTO_SCROLL_DOWN_SETTING',
'PROXY_ENDPOINT_INFO_SETTING',
'SYSTEM_PROXY_MODE_OPTIONS',
'ConnectionController',
'ConnectionError',
'ConnectionState',
+68 -2
View File
@@ -19,6 +19,7 @@
from __future__ import annotations
from Furious.Controllers import SYSTEM_PROXY_MODE_OPTIONS
from Furious.Frozenlib import *
from Furious.Interface import *
from Furious.Models import *
@@ -737,6 +738,21 @@ class HomePage(Mixins.QTranslatable, QMainWindow):
)
self.routingSelector = RoutingSelector(parent=self)
self.systemProxyComboBox = AppQComboBox(parent=self)
self.systemProxyComboBox.setContentWidthAdjustable()
self.systemProxyComboBox.setMinimumWidth(220)
self.systemProxyComboBox.setToolTip(_('System Proxy'))
for label, mode in SYSTEM_PROXY_MODE_OPTIONS:
self.systemProxyComboBox.addItem(_(label), mode)
self.tunModeLabel = AppQLabel(_('TUN Mode'), parent=self)
self.tunModeSwitch = AppQSwitch(parent=self)
self._tunModeAvailable = AppSettingsController().tunModeAvailable()
self._syncSystemProxyMode(AppSettings.get('SystemProxyMode'))
self.tunModeSwitch.syncChecked(AppSettings.isStateON_('VPNMode'))
self.searchLineEdit = AppQLineEdit()
self.searchLineEdit.setPlaceholderText(
_(
@@ -767,16 +783,25 @@ class HomePage(Mixins.QTranslatable, QMainWindow):
self.headerLayout.addWidget(self.searchLineEdit, 4)
self.headerLayout.addWidget(self.searchButton)
self.connectionLayout = QHBoxLayout()
self.connectionLayout.setContentsMargins(0, 0, 0, 0)
self.connectionLayout.setSpacing(8)
self.connectionLayout.addWidget(self.connectButton)
self.connectionLayout.addWidget(self.routingSelector)
self.connectionLayout.addWidget(self.systemProxyComboBox)
self.connectionLayout.addWidget(self.tunModeLabel)
self.connectionLayout.addWidget(self.tunModeSwitch)
self.connectionLayout.addStretch(1)
self.actionLayout = QHBoxLayout()
self.actionLayout.setContentsMargins(0, 0, 0, 0)
self.actionLayout.setSpacing(8)
self.actionLayout.addWidget(self.connectButton)
self.actionLayout.addWidget(self.routingSelector)
self.actionLayout.addWidget(self.serverButton)
self.actionLayout.addStretch(1)
self.actionLayout.addWidget(self.subscriptionFilterComboBox)
self._layout.addLayout(self.headerLayout)
self._layout.addLayout(self.connectionLayout)
self._layout.addLayout(self.actionLayout)
self._layout.addWidget(self.userServersQTableWidget, 1)
@@ -792,6 +817,19 @@ class HomePage(Mixins.QTranslatable, QMainWindow):
self.subscriptionFilterComboBox.currentIndexChanged.connect(
self.handleSubscriptionFilterChanged
)
self.systemProxyComboBox.currentIndexChanged.connect(
self.handleSystemProxyModeChanged
)
self.tunModeSwitch.toggled.connect(self.handleTUNModeChanged)
AppSettingsController().systemProxyModeChanged.connect(
self._syncSystemProxyMode
)
AppSettingsController().tunModeChanged.connect(self.tunModeSwitch.syncChecked)
AppConnectionController().interactionEnabledChanged.connect(
self.setConnectionControlsEnabled
)
self.userServersQTableWidget.subsManager.subscriptionsChanged.connect(
self.refreshSubscriptionFilter
@@ -805,9 +843,37 @@ class HomePage(Mixins.QTranslatable, QMainWindow):
self.refreshSubscriptionFilter()
self.handleServerSelectionChanged()
self.setConnectionControlsEnabled(AppConnectionController().interactionEnabled)
self.setCentralWidget(self._widget)
@QtCore.Slot(str)
def _syncSystemProxyMode(self, mode: str):
"""Select the shared system-proxy preference without writing it again."""
with Mixins.QBlockSignalContext(self.systemProxyComboBox):
index = self.systemProxyComboBox.findData(mode)
self.systemProxyComboBox.setCurrentIndex(max(0, index))
@QtCore.Slot()
def handleSystemProxyModeChanged(self):
"""Persist a system-proxy selection through the shared settings authority."""
mode = self.systemProxyComboBox.currentData()
if isinstance(mode, str):
AppSettingsController().setSystemProxyMode(mode)
@QtCore.Slot(bool)
def handleTUNModeChanged(self, enabled: bool):
"""Persist a TUN request through the shared settings authority."""
AppSettingsController().setTUNMode(enabled)
@QtCore.Slot(bool)
def setConnectionControlsEnabled(self, enabled: bool):
"""Gate connection-sensitive Home controls during lifecycle transitions."""
self.systemProxyComboBox.setEnabled(bool(enabled))
self.tunModeSwitch.setEnabled(bool(enabled) and self._tunModeAvailable)
@QtCore.Slot()
def handleServerSelectionChanged(self, *_args):
"""Apply Home's selection policy to the shared connection control."""
+19 -11
View File
@@ -19,7 +19,10 @@
from __future__ import annotations
from Furious.Controllers import APPLICATION_THEME_SETTING
from Furious.Controllers import (
APPLICATION_THEME_SETTING,
SYSTEM_PROXY_MODE_OPTIONS,
)
from Furious.Frozenlib import *
from Furious.Plugins import (
CapabilityKind,
@@ -509,10 +512,7 @@ class _ApplicationThemeSettingsCard(_SettingsCard):
class _SystemProxySettingsCard(_SettingsCard):
"""Select how Furious manages the operating-system proxy."""
Options = (
('Automatically Configure System Proxy', AppBuiltinProxyMode.Auto.value),
('Do Not Change System Proxy', AppBuiltinProxyMode.NoChanges.value),
)
Options = SYSTEM_PROXY_MODE_OPTIONS
_TranslatableOptions = (
_('Automatically Configure System Proxy'),
@@ -540,10 +540,13 @@ class _SystemProxySettingsCard(_SettingsCard):
parent=parent,
)
def sync(self):
def sync(self, mode=None):
"""Select the persisted proxy mode without writing it again."""
blocker = QtCore.QSignalBlocker(self.comboBox)
index = self.comboBox.findData(AppSettings.get('SystemProxyMode'))
selectedMode = (
mode if isinstance(mode, str) else AppSettings.get('SystemProxyMode')
)
index = self.comboBox.findData(selectedMode)
self.comboBox.setCurrentIndex(max(index, 0))
@@ -646,10 +649,7 @@ class SettingsPage(Mixins.QTranslatable, QMainWindow):
self.pluginSections = []
self._pluginActions = []
if PLATFORM == 'Linux':
self._tunModeAvailable = not SystemRuntime.flatpakID()
else:
self._tunModeAvailable = SystemRuntime.isAdmin()
self._tunModeAvailable = AppSettingsController().tunModeAvailable()
self.tunModeCard = _ToggleSettingsCard(
'shield-check.svg',
@@ -660,6 +660,10 @@ class SettingsPage(Mixins.QTranslatable, QMainWindow):
)
self.tunModeCard.checkBox.setEnabled(self._tunModeAvailable)
AppSettingsController().tunModeChanged.connect(
self.tunModeCard.checkBox.syncChecked
)
(
self.applicationThemeCard,
self.languageCard,
@@ -800,6 +804,10 @@ class SettingsPage(Mixins.QTranslatable, QMainWindow):
),
)
AppSettingsController().systemProxyModeChanged.connect(
self.systemProxyCard.sync
)
if not SystemRuntime.flatpakID():
self.connectionSection.addCard(self.tunSettingsCard)
+49 -1
View File
@@ -23,7 +23,12 @@ from Furious.Controllers.SettingsController import (
APPLICATION_THEME_SETTING,
SettingsController,
)
from Furious.Frozenlib import AppBinarySettings, AppSettings, ApplicationTheme
from Furious.Frozenlib import (
AppBinarySettings,
AppBuiltinProxyMode,
AppSettings,
ApplicationTheme,
)
from Furious.Models import (
CoreConfiguration,
ProfileMetadata,
@@ -50,6 +55,7 @@ from collections import OrderedDict
import threading
import unittest
import weakref
from unittest import mock
from tests.support import application, isolatedSettings, processQtEvents
@@ -376,6 +382,48 @@ class SettingsMigrationTest(unittest.TestCase):
ApplicationTheme.Light.value,
)
def testSharedNetworkPreferencesEmitOnlyRealChanges(self):
"""Synchronize multiple views without publishing duplicate mutations."""
application()
with (
isolatedSettings(),
mock.patch(
'Furious.Controllers.SettingsController.PLATFORM',
'Linux',
),
mock.patch(
'Furious.Controllers.SettingsController.showMBoxNewChangesNextTime'
),
):
AppSettings.turnOFF('VPNMode')
AppSettings.set(
'SystemProxyMode',
AppBuiltinProxyMode.Auto.value,
)
controller = SettingsController()
tunStates = []
proxyModes = []
controller.tunModeChanged.connect(tunStates.append)
controller.systemProxyModeChanged.connect(proxyModes.append)
controller.setTUNMode(True)
controller.setTUNMode(True)
controller.setSystemProxyMode(AppBuiltinProxyMode.NoChanges.value)
controller.setSystemProxyMode(AppBuiltinProxyMode.NoChanges.value)
self.assertEqual(tunStates, [True])
self.assertEqual(
proxyModes,
[AppBuiltinProxyMode.NoChanges.value],
)
self.assertTrue(AppSettings.isStateON_('VPNMode'))
self.assertEqual(
AppSettings.get('SystemProxyMode'),
AppBuiltinProxyMode.NoChanges.value,
)
class LogManagerTest(unittest.TestCase):
"""Verify bounded, categorized, and thread-safe structured logging."""