mirror of
https://github.com/LorenEteval/Furious.git
synced 2026-09-22 23:08:08 +03:00
feat: synchronize Home network controls
Signed-off-by: Loren Eteval <loren.eteval@proton.me>
This commit is contained in:
@@ -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,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):
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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,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)
|
||||
|
||||
|
||||
@@ -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."""
|
||||
|
||||
Reference in New Issue
Block a user