Files
LorenEteval_Furious/Furious/Controllers/SettingsController.py
T
2026-08-19 11:02:53 +08:00

319 lines
11 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Copyright (C) 2024present Loren Eteval & contributors <loren.eteval@proton.me>
#
# This file is part of Furious.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
"""Apply persistent application preferences independently from their UI."""
from __future__ import annotations
from Furious.Frozenlib import *
from Furious.Qt.DynamicTranslate import SUPPORTED_LANGUAGE
from Furious.Qt.QtWidgets import showMBoxNewChangesNextTime
from Furious.Service.TrafficStatsManager import (
CLEAR_TRAFFIC_USAGE_ON_RECONNECT_SETTING,
METRICS_COLLECTION_SETTING,
)
from Furious.Service.EndpointInfoService import PROXY_ENDPOINT_INFO_SETTING
from PySide6 import QtCore
__all__ = [
'APPLICATION_THEME_SETTING',
'LOG_AUTO_SCROLL_DOWN_SETTING',
'LOG_AUTO_CLEAR_SETTING',
'PROXY_ENDPOINT_INFO_SETTING',
'SettingsController',
]
APPLICATION_THEME_SETTING = 'ApplicationTheme'
# Migrate legacy settings
_LEGACY_DARK_MODE_SETTING = 'DarkMode'
LOG_AUTO_SCROLL_DOWN_SETTING = 'LogAutoScrollDown'
LOG_AUTO_CLEAR_SETTING = 'LogAutoClear'
registerAppSettings('VPNMode', isBinary=True)
registerAppSettings(
APPLICATION_THEME_SETTING,
validRange=[theme.value for theme in ApplicationTheme],
default=ApplicationTheme.System.value,
)
registerAppSettings(_LEGACY_DARK_MODE_SETTING, isBinary=True)
registerAppSettings('UseMonochromeTrayIcon', isBinary=True)
if PLATFORM == 'Darwin':
registerAppSettings('HideDockIcon', isBinary=True)
registerAppSettings('StartupOnBoot', isBinary=True, default=AppBinarySettings.ON_)
registerAppSettings('PowerSaveMode', isBinary=True, default=AppBinarySettings.ON_)
registerAppSettings(
'ForceToLocalhostWhenSettingLocalProxy',
isBinary=True,
default=AppBinarySettings.OFF,
)
registerAppSettings(
'AutoUpdateAssetFiles', isBinary=True, default=AppBinarySettings.ON_
)
registerAppSettings(
'ShowProgressBarWhenConnecting', isBinary=True, default=AppBinarySettings.ON_
)
registerAppSettings('ShowTabAndSpacesInEditor', isBinary=True)
registerAppSettings(
LOG_AUTO_SCROLL_DOWN_SETTING,
isBinary=True,
default=AppBinarySettings.ON_,
)
registerAppSettings(
LOG_AUTO_CLEAR_SETTING,
isBinary=True,
default=AppBinarySettings.ON_,
)
registerAppSettings(
'SystemProxyMode',
validRange=list(mode.value for mode in AppBuiltinProxyMode),
)
def _legacyDarkModeValue(preference: ApplicationTheme) -> str:
"""Return the closest legacy binary representation of a preference."""
if preference == ApplicationTheme.Dark:
return AppBinarySettings.ON_
# Legacy releases represented both automatic and forced light as "not dark".
return AppBinarySettings.OFF
def _synchronizeLegacyDarkModeSetting():
"""Reconcile and retain the legacy dark-mode compatibility setting."""
settings = QtCore.QSettings()
hasLegacyValue, hasThemeValue = (
settings.contains(_LEGACY_DARK_MODE_SETTING),
settings.contains(APPLICATION_THEME_SETTING),
)
if hasLegacyValue:
legacyValue = settings.value(_LEGACY_DARK_MODE_SETTING)
legacyDarkEnabled = str(legacyValue).strip().lower() == AppBinarySettings.ON_
if not hasThemeValue:
preference = (
ApplicationTheme.Dark if legacyDarkEnabled else ApplicationTheme.System
)
AppSettings.set(APPLICATION_THEME_SETTING, preference.value)
else:
preference = ApplicationTheme(AppSettings.get(APPLICATION_THEME_SETTING))
# A mismatch means an older release changed its binary setting.
if legacyDarkEnabled and preference != ApplicationTheme.Dark:
preference = ApplicationTheme.Dark
AppSettings.set(APPLICATION_THEME_SETTING, preference.value)
elif not legacyDarkEnabled and preference == ApplicationTheme.Dark:
preference = ApplicationTheme.System
AppSettings.set(APPLICATION_THEME_SETTING, preference.value)
else:
preference = ApplicationTheme(AppSettings.get(APPLICATION_THEME_SETTING))
AppSettings.set(
_LEGACY_DARK_MODE_SETTING,
_legacyDarkModeValue(preference),
)
class SettingsController:
"""Apply application settings independently from their presentation."""
def __init__(self):
"""Synchronize theme persistence after Qt application metadata is ready."""
_synchronizeLegacyDarkModeSetting()
@staticmethod
def _setBinary(settingName: str, enabled: bool):
"""Persist one registered binary setting."""
if enabled:
AppSettings.turnON_(settingName)
else:
AppSettings.turnOFF(settingName)
@classmethod
def setTUNMode(cls, enabled: bool):
"""Persist the global TUN mode and notify active workflows."""
if PLATFORM != 'Linux':
assert SystemRuntime.isAdmin()
cls._setBinary('VPNMode', enabled)
showMBoxNewChangesNextTime()
@staticmethod
def setApplicationTheme(theme: ApplicationTheme | str):
"""Persist and immediately apply one application theme preference."""
try:
preference = ApplicationTheme(theme)
except (TypeError, ValueError):
return
AppSettings.set(APPLICATION_THEME_SETTING, preference.value)
AppSettings.set(
_LEGACY_DARK_MODE_SETTING,
_legacyDarkModeValue(preference),
)
applyThemePreference = getattr(APP(), 'applyThemePreference', None)
if callable(applyThemePreference):
applyThemePreference()
@staticmethod
def setLanguage(language: str):
"""Persist a supported UI language and refresh translated objects."""
if language not in SUPPORTED_LANGUAGE:
return
if AppSettings.get('Language') != language:
AppSettings.set('Language', language)
Mixins.QTranslatable.retranslateAll()
@classmethod
def setMonochromeTrayIcon(cls, enabled: bool):
"""Persist and immediately refresh the tray-icon presentation."""
cls._setBinary('UseMonochromeTrayIcon', enabled)
try:
if enabled:
AppSystemTray().setMonochromeIcon()
elif AppConnectionController().isConnected():
AppSystemTray().setConnectedIcon()
else:
AppSystemTray().setDisconnectedIcon()
except (AttributeError, RuntimeError):
pass
@classmethod
def setDockIconHidden(cls, enabled: bool):
"""Apply the macOS dock-icon visibility preference."""
if enabled:
APP().installDockIconVisibilityFeature()
else:
APP().installDockIconVisibilityFeature(remove=True)
cls._setBinary('HideDockIcon', enabled)
@classmethod
def setStartupOnBoot(cls, enabled: bool):
"""Apply the platform startup registration preference."""
if enabled:
StartupOnBoot.on_()
else:
StartupOnBoot.off()
cls._setBinary('StartupOnBoot', enabled)
@classmethod
def setPowerSaveMode(cls, enabled: bool):
"""Persist power-saving behavior for the next connection."""
cls._setBinary('PowerSaveMode', enabled)
showMBoxNewChangesNextTime()
@classmethod
def setForceLocalProxy(cls, enabled: bool):
"""Persist local system-proxy address normalization."""
cls._setBinary('ForceToLocalhostWhenSettingLocalProxy', enabled)
showMBoxNewChangesNextTime()
@staticmethod
def setSystemProxyMode(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)
@classmethod
def setAutoUpdateAssets(cls, enabled: bool):
"""Persist automatic core-asset updates."""
cls._setBinary('AutoUpdateAssetFiles', enabled)
@classmethod
def setConnectionProgressVisible(cls, enabled: bool):
"""Persist connection-progress visibility."""
cls._setBinary('ShowProgressBarWhenConnecting', enabled)
@classmethod
def setLogAutoScrollDown(cls, enabled: bool):
"""Persist whether the log viewer may automatically follow its tail."""
cls._setBinary(LOG_AUTO_SCROLL_DOWN_SETTING, enabled)
@classmethod
def setLogAutoClear(cls, enabled: bool, manager=None):
"""Persist and immediately apply Core-triggered log clearing."""
cls._setBinary(LOG_AUTO_CLEAR_SETTING, enabled)
if manager is None:
try:
manager = AppLogManager()
except (AttributeError, RuntimeError):
return
apply = getattr(manager, 'setAutoClearEnabled', None)
if callable(apply):
apply(enabled)
@classmethod
def setClearTrafficUsageOnReconnect(cls, enabled: bool):
"""Persist whether reconnecting starts a fresh usage session."""
cls._setBinary(CLEAR_TRAFFIC_USAGE_ON_RECONNECT_SETTING, enabled)
@classmethod
def setMetricsCollectionEnabled(cls, enabled: bool):
"""Persist and immediately apply network metrics collection."""
cls._setBinary(METRICS_COLLECTION_SETTING, enabled)
try:
AppMainWindow().trafficStatsManager.setCollectionEnabled(enabled)
except (AttributeError, RuntimeError):
pass
@classmethod
def setProxyEndpointInfoEnabled(cls, enabled: bool):
"""Persist and immediately apply privacy-sensitive endpoint inspection."""
cls._setBinary(PROXY_ENDPOINT_INFO_SETTING, enabled)
try:
AppEndpointInfoService().setEnabled(enabled)
except (AttributeError, RuntimeError):
pass
@classmethod
def setEditorWhitespaceVisible(cls, enabled: bool):
"""Apply and persist editor whitespace visibility."""
cls._setBinary('ShowTabAndSpacesInEditor', enabled)
try:
if enabled:
AppMainWindow().showTabAndSpaces()
else:
AppMainWindow().hideTabAndSpaces()
except (AttributeError, RuntimeError):
pass