diff --git a/Furious/Actions/Configuration.py b/Furious/Actions/Configuration.py index ab61b4a..83fc86c 100644 --- a/Furious/Actions/Configuration.py +++ b/Furious/Actions/Configuration.py @@ -15,7 +15,7 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -"""Implement tray actions for edit configuration.""" +"""Provide the tray action that foregrounds the Home page.""" from __future__ import annotations @@ -23,20 +23,41 @@ from Furious.Frozenlib import * from Furious.Qt import * from Furious.Qt import gettext as _ -__all__ = ['EditConfigurationAction'] +__all__ = ['ShowHomePageAction'] -class EditConfigurationAction(AppQAction): - """Handle the edit configuration action.""" +class ShowHomePageAction(AppQAction): + """Foreground the main window and navigate to server management.""" def __init__(self, **kwargs): - """Initialize the EditConfigurationAction.""" + """Initialize the Home-page navigation action.""" super().__init__( - _('Edit Configuration...'), - icon=bootstrapIcon('pencil-square.svg'), + _('Show Home Page...'), + icon=bootstrapIcon('house-door.svg'), **kwargs, ) + self.setToolTip(_('Show the server management Home page')) + self.setStatusTip(_('Show the server management Home page')) + def triggeredCallback(self, checked): """Handle activation of the action.""" - APP().mainWindow.show() + mainWindow = APP().mainWindow + + if mainWindow.isMinimized(): + mainWindow.showNormal() + else: + mainWindow.show() + + mainWindow.showPage('home') + mainWindow.raise_() + mainWindow.activateWindow() + + def retranslate(self): + """Refresh the action text and explanatory hover text.""" + super().retranslate() + + description = _('Show the server management Home page') + + self.setToolTip(description) + self.setStatusTip(description) diff --git a/Furious/Actions/Connection.py b/Furious/Actions/Connection.py index 3db008b..2dfe7b1 100644 --- a/Furious/Actions/Connection.py +++ b/Furious/Actions/Connection.py @@ -122,10 +122,11 @@ class ConnectAction(AppQAction): APP().systemTray.RoutingAction.setDisabled(value) APP().systemTray.SystemProxyAction.setDisabled(value) - # TODO: Need this? if PLATFORM == 'Linux' or SystemRuntime.isAdmin(): - TUNModeAction = APP().systemTray.SettingsAction.getTUNModeAction() - TUNModeAction.setDisabled(value) + try: + APP().mainWindow.settingsPage.setTUNModeControlEnabled(not value) + except (AttributeError, RuntimeError): + pass def isConnected(self) -> bool: """Return whether connected.""" diff --git a/Furious/Actions/Settings.py b/Furious/Actions/Settings.py index e1d260d..486394b 100644 --- a/Furious/Actions/Settings.py +++ b/Furious/Actions/Settings.py @@ -15,177 +15,16 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -"""Implement tray actions for settings.""" +"""Provide the compatibility action used by the Settings-page TUN control.""" from __future__ import annotations +from Furious.Controllers import SettingsController from Furious.Frozenlib import * from Furious.Qt import * from Furious.Qt import gettext as _ -from Furious.Service.TrafficStatsManager import ( - CLEAR_TRAFFIC_USAGE_ON_RECONNECT_SETTING, -) -__all__ = ['SettingsAction', 'SettingsController', 'TUNModeAction'] - -registerAppSettings('VPNMode', isBinary=True) -registerAppSettings('DarkMode', 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) - - -class SettingsController: - """Apply application settings independently from their presentation.""" - - @staticmethod - def setTUNMode(enabled: bool): - """Persist the global TUN mode and notify active workflows.""" - if PLATFORM != 'Linux': - assert SystemRuntime.isAdmin() - - if enabled: - AppSettings.turnON_('VPNMode') - else: - AppSettings.turnOFF('VPNMode') - - showMBoxNewChangesNextTime() - - @staticmethod - def setDarkMode(enabled: bool): - """Switch between the explicit dark theme and automatic mode.""" - if enabled: - AppSettings.turnON_('DarkMode') - - try: - APP().switchToDarkMode() - except Exception: - # Any non-exit exceptions - - pass - else: - AppSettings.turnOFF('DarkMode') - - try: - APP().switchToAutoMode() - except Exception: - # Any non-exit exceptions - - pass - - @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() - - @staticmethod - def setMonochromeTrayIcon(enabled: bool): - """Persist and immediately refresh the tray-icon presentation.""" - if enabled: - AppSettings.turnON_('UseMonochromeTrayIcon') - APP().systemTray.setMonochromeIcon() - else: - AppSettings.turnOFF('UseMonochromeTrayIcon') - - if APP().isSystemTrayConnected(): - APP().systemTray.setConnectedIcon() - else: - APP().systemTray.setDisconnectedIcon() - - @staticmethod - def setDockIconHidden(enabled: bool): - """Apply the macOS dock-icon visibility preference.""" - if enabled: - APP().installDockIconVisibilityFeature() - AppSettings.turnON_('HideDockIcon') - else: - APP().installDockIconVisibilityFeature(remove=True) - AppSettings.turnOFF('HideDockIcon') - - @staticmethod - def setStartupOnBoot(enabled: bool): - """Apply the platform startup registration preference.""" - if enabled: - StartupOnBoot.on_() - AppSettings.turnON_('StartupOnBoot') - else: - StartupOnBoot.off() - AppSettings.turnOFF('StartupOnBoot') - - @staticmethod - def setPowerSaveMode(enabled: bool): - """Persist power-saving behavior for the next connection.""" - if enabled: - AppSettings.turnON_('PowerSaveMode') - else: - AppSettings.turnOFF('PowerSaveMode') - - showMBoxNewChangesNextTime() - - @staticmethod - def setForceLocalProxy(enabled: bool): - """Persist local system-proxy address normalization.""" - if enabled: - AppSettings.turnON_('ForceToLocalhostWhenSettingLocalProxy') - else: - AppSettings.turnOFF('ForceToLocalhostWhenSettingLocalProxy') - - showMBoxNewChangesNextTime() - - @staticmethod - def setAutoUpdateAssets(enabled: bool): - """Persist automatic core-asset updates.""" - if enabled: - AppSettings.turnON_('AutoUpdateAssetFiles') - else: - AppSettings.turnOFF('AutoUpdateAssetFiles') - - @staticmethod - def setConnectionProgressVisible(enabled: bool): - """Persist connection-progress visibility.""" - if enabled: - AppSettings.turnON_('ShowProgressBarWhenConnecting') - else: - AppSettings.turnOFF('ShowProgressBarWhenConnecting') - - @staticmethod - def setClearTrafficUsageOnReconnect(enabled: bool): - """Persist whether reconnecting starts a fresh usage session.""" - if enabled: - AppSettings.turnON_(CLEAR_TRAFFIC_USAGE_ON_RECONNECT_SETTING) - else: - AppSettings.turnOFF(CLEAR_TRAFFIC_USAGE_ON_RECONNECT_SETTING) - - @staticmethod - def setEditorWhitespaceVisible(enabled: bool): - """Apply and persist editor whitespace visibility.""" - if enabled: - APP().mainWindow.showTabAndSpaces() - AppSettings.turnON_('ShowTabAndSpacesInEditor') - else: - APP().mainWindow.hideTabAndSpaces() - AppSettings.turnOFF('ShowTabAndSpacesInEditor') +__all__ = ['TUNModeAction'] class TUNModeAction(AppQAction): @@ -211,49 +50,3 @@ class TUNModeAction(AppQAction): def triggeredCallback(self, checked): """Handle activation of the action.""" SettingsController.setTUNMode(checked) - - -class SettingsChildAction(AppQAction): - """Handle the settings child action.""" - - def __init__(self, *args, **kwargs): - """Initialize the SettingsChildAction.""" - super().__init__(*args, **kwargs) - - def triggeredCallback(self, checked): - """Handle activation of the action.""" - if self.textCompare('Dark Mode'): - SettingsController.setDarkMode(checked) - elif self.textCompare('Use Monochrome Tray Icon'): - SettingsController.setMonochromeTrayIcon(checked) - elif self.textCompare('Hide Dock Icon'): - SettingsController.setDockIconHidden(checked) - elif self.textCompare('Startup On Boot'): - SettingsController.setStartupOnBoot(checked) - elif self.textCompare('Power Save Mode'): - SettingsController.setPowerSaveMode(checked) - elif self.textCompare('Force To 127.0.0.1 When Setting Local Proxy'): - SettingsController.setForceLocalProxy(checked) - elif self.textCompare('Automatically Update Asset Files'): - SettingsController.setAutoUpdateAssets(checked) - elif self.textCompare('Show Progress Bar When Connecting'): - SettingsController.setConnectionProgressVisible(checked) - elif self.textCompare('Show Tab And Spaces In Editor'): - SettingsController.setEditorWhitespaceVisible(checked) - - -class SettingsAction(AppQAction): - """Navigate from the system tray to the page-based settings UI.""" - - def __init__(self, **kwargs): - """Initialize the SettingsAction.""" - super().__init__( - _('Settings'), - icon=bootstrapIcon('gear-wide-connected.svg'), - callback=lambda: APP().mainWindow.showSettingsPage(), - **kwargs, - ) - - def getTUNModeAction(self) -> AppQAction: - """Return the Settings page's compatibility TUN action.""" - return APP().mainWindow.settingsPage.tunModeAction diff --git a/Furious/Actions/__init__.py b/Furious/Actions/__init__.py index 245cf8d..acf7a98 100644 --- a/Furious/Actions/__init__.py +++ b/Furious/Actions/__init__.py @@ -19,7 +19,7 @@ from __future__ import annotations -from .Configuration import EditConfigurationAction +from .Configuration import ShowHomePageAction from .Connection import ConnectAction from .Exit import ExitAction from .Import import ( @@ -31,12 +31,11 @@ from .Import import ( ) from .Language import LanguageAction from .Routing import RoutingAction -from .Settings import SettingsAction +from .Settings import TUNModeAction from .SystemProxy import SystemProxyAction __all__ = [ 'ConnectAction', - 'EditConfigurationAction', 'ExitAction', 'ImportAction', 'ImportFromFileAction', @@ -45,6 +44,7 @@ __all__ = [ 'ImportURIFromClipboardAction', 'LanguageAction', 'RoutingAction', - 'SettingsAction', + 'ShowHomePageAction', + 'TUNModeAction', 'SystemProxyAction', ] diff --git a/Furious/Application/TrayIcon.py b/Furious/Application/TrayIcon.py index 5656f8f..5fddb8b 100644 --- a/Furious/Application/TrayIcon.py +++ b/Furious/Application/TrayIcon.py @@ -59,9 +59,7 @@ class TrayIcon( SystemProxyAction(isTrayAction=True), AppQSeperator(), ImportAction(isTrayAction=True), - EditConfigurationAction(isTrayAction=True), - AppQSeperator(), - SettingsAction(isTrayAction=True), + ShowHomePageAction(isTrayAction=True), AppQSeperator(), ExitAction(isTrayAction=True), ] diff --git a/Furious/Controllers/SettingsController.py b/Furious/Controllers/SettingsController.py new file mode 100644 index 0000000..a82c214 --- /dev/null +++ b/Furious/Controllers/SettingsController.py @@ -0,0 +1,174 @@ +# Copyright (C) 2024–present Loren Eteval & contributors +# +# 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. + +"""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, +) + +__all__ = ['SettingsController'] + +registerAppSettings('VPNMode', isBinary=True) +registerAppSettings('DarkMode', 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) + + +class SettingsController: + """Apply application settings independently from their presentation.""" + + @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() + + @classmethod + def setDarkMode(cls, enabled: bool): + """Switch between the explicit dark theme and automatic mode.""" + cls._setBinary('DarkMode', enabled) + + try: + if enabled: + APP().switchToDarkMode() + else: + APP().switchToAutoMode() + except Exception: + # The controller can be exercised before the full desktop UI exists. + pass + + @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: + APP().systemTray.setMonochromeIcon() + elif APP().isSystemTrayConnected(): + APP().systemTray.setConnectedIcon() + else: + APP().systemTray.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() + + @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 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: + APP().mainWindow.trafficStatsManager.setCollectionEnabled(enabled) + except (AttributeError, RuntimeError): + pass + + @classmethod + def setEditorWhitespaceVisible(cls, enabled: bool): + """Apply and persist editor whitespace visibility.""" + cls._setBinary('ShowTabAndSpacesInEditor', enabled) + + try: + if enabled: + APP().mainWindow.showTabAndSpaces() + else: + APP().mainWindow.hideTabAndSpaces() + except (AttributeError, RuntimeError): + pass diff --git a/Furious/Controllers/__init__.py b/Furious/Controllers/__init__.py new file mode 100644 index 0000000..9e05402 --- /dev/null +++ b/Furious/Controllers/__init__.py @@ -0,0 +1,14 @@ +# Copyright (C) 2024–present Loren Eteval & contributors +# +# 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. + +"""Expose presentation-independent application controllers.""" + +from .SettingsController import SettingsController + +__all__ = ['SettingsController']