Files
LorenEteval_Furious/Furious/Window/MainWindow.py
T
2026-09-09 01:00:02 +08:00

325 lines
12 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/>.
"""Provide the page-based application main window."""
from __future__ import annotations
from Furious.Frozenlib import *
from Furious.Models import CoreConfiguration, ServerProfile
from Furious.Qt import *
from Furious.Qt import gettext as _
from Furious.Service import MetricsHistory, PluginNavigationManager
from Furious.Widget.NavigationView import NavigationView
from Furious.Window.HomePage import HomePage
from Furious.Window.LogPage import LogPage
from Furious.Window.MetricsPage import MetricsPage
from Furious.Window.SettingsPage import SettingsPage
from Furious.Window.SubscriptionPage import SubscriptionPage
from PySide6 import QtCore
from typing import Union
import logging
__all__ = ['MainWindow']
logger = logging.getLogger(__name__)
# Migrate legacy settings.
registerAppSettings('ServerWidgetWindowSize')
registerAppSettings('AppMainWindowGeometry')
registerAppSettings('AppMainWindowState')
_TRANSLATABLE_NAVIGATION_LABELS = (
_('Home'),
_('Log'),
_('Subscription'),
_('Metrics'),
_('Settings'),
)
class MainWindow(AppQMainWindow):
"""Manage application pages, navigation, and global window state."""
DEFAULT_WINDOW_SIZE_DARWIN = QtCore.QSize(1500, 780)
DEFAULT_WINDOW_SIZE = (
QtCore.QSize(1800, 960) if PLATFORM != 'Darwin' else DEFAULT_WINDOW_SIZE_DARWIN
)
QT_FALLBACK_WINDOW_SIZE = QtCore.QSize(640, 480)
def __init__(self, *args, **kwargs):
"""Create and register the built-in application pages."""
super().__init__(*args, **kwargs)
self.setFocusPolicy(QtCore.Qt.FocusPolicy.StrongFocus)
self.menuBar().hide()
self.navigationView = NavigationView(parent=self)
self.homePage = HomePage(parent=self.navigationView)
self.subscriptionPage = SubscriptionPage(
self.homePage.userServersQTableWidget,
parent=self.navigationView,
)
self.metricsHistory = MetricsHistory(parent=self)
self.homePage.trafficStatsManager.sampleChanged.connect(
self.metricsHistory.recordTrafficSample
)
self.homePage.trafficStatsManager.usageHistoryReset.connect(
self.metricsHistory.clearTrafficUsageHistory
)
self.metricsPage = MetricsPage(
self.metricsHistory,
parent=self.navigationView,
)
self.logPage = AppLogPage()
self.settingsPage = SettingsPage(
tunSettingsDialogFactory=self.homePage.getGuiTUNSettings,
proxyBypassDialog=self.homePage.customizeProxyBypassDialog,
networkTestDialog=self.homePage.customizeNetworkTestDialog,
checkForUpdates=self.homePage.checkForUpdates,
openAboutPage=self.homePage.openAboutPage,
restartAsAdmin=self.homePage.restartAsAdmin,
openApplicationFolder=self.homePage.openApplicationFolder,
parent=self.navigationView,
)
AppConnectionController().interactionEnabledChanged.connect(
self.settingsPage.setConnectionControlsEnabled
)
self.settingsPage.setConnectionControlsEnabled(
AppConnectionController().interactionEnabled
)
if not isinstance(self.logPage, LogPage):
raise TypeError('application log page must be a LogPage')
# NavigationView selects the first registered page. Keep Home first
# so every new window session starts from the canonical product page.
self.navigationView.addPage(
'home',
self.homePage,
'Home',
'house-door.svg',
)
self.navigationView.addPage(
'log',
self.logPage,
'Log',
'pin-angle.svg',
)
self.navigationView.addPage(
'subscription',
self.subscriptionPage,
'Subscription',
'star.svg',
)
self.pluginNavigationManager = PluginNavigationManager()
self.pluginNavigationManager.registerPages(self.navigationView)
self.navigationView.addPage(
'metrics',
self.metricsPage,
'Metrics',
'speedometer2.svg',
)
self.navigationView.addPage(
'settings',
self.settingsPage,
'Settings',
'gear-wide-connected.svg',
placement='bottom',
)
self.setCentralWidget(self.navigationView)
# Preserve the established application-facing server-management API.
self.userServersQTableWidget = self.homePage.userServersQTableWidget
self.userSubsWindow = self.subscriptionPage
self.networkConnectivityManager = self.homePage.networkConnectivityManager
self.networkState = self.homePage.networkState
self.trafficStats = self.homePage.trafficStats
self.trafficStatsManager = self.homePage.trafficStatsManager
self.retranslate()
def showPage(self, pageId: str):
"""Navigate to a registered application page."""
self.navigationView.setCurrentPage(pageId)
def showLogPage(self):
"""Navigate directly to the unified logging page."""
self.showPage('log')
def showSettingsPage(self):
"""Navigate directly to application settings."""
self.show()
self.showPage('settings')
def showSubscriptionPage(self):
"""Navigate directly to subscription management."""
self.show()
self.showPage('subscription')
def updateSubsByUnique(self, unique: str, httpProxy: Union[str, None], **kwargs):
"""Forward a subscription update to the dedicated page controller."""
self.subscriptionPage.updateSubsByUnique(unique, httpProxy, **kwargs)
def appendNewItemByFactory(self, factory: CoreConfiguration | ServerProfile):
"""Forward a new server profile to the home page."""
self.homePage.appendNewItemByFactory(factory)
def appendNewItemsByFactories(self, factories):
"""Forward a batch of prepared profiles to the server table."""
self.homePage.appendNewItemsByFactories(factories)
def flushRow(self, row: int, item: ServerProfile):
"""Forward a server-row refresh to the home page."""
self.homePage.flushRow(row, item)
def showTabAndSpaces(self):
"""Enable whitespace markers in home-page editors."""
self.homePage.showTabAndSpaces()
def hideTabAndSpaces(self):
"""Disable whitespace markers in home-page editors."""
self.homePage.hideTabAndSpaces()
def getGuiTUNSettings(self, **kwargs):
"""Return the home page's TUN settings editor."""
return self.homePage.getGuiTUNSettings(**kwargs)
def checkForUpdates(self, **kwargs):
"""Forward update checks to the home page."""
self.homePage.checkForUpdates(**kwargs)
def resetNetworkState(self):
"""Clear the home page network state."""
self.homePage.resetNetworkState()
def setNetworkState(self, success: bool, **kwargs):
"""Update the home page network state."""
self.homePage.setNetworkState(success, **kwargs)
def prepareInitialGeometry(self):
"""Restore persisted geometry after composition and before first show."""
savedGeometry = AppSettings.get('AppMainWindowGeometry')
if savedGeometry is None:
if not self._restoreLegacyWindowSize():
self._applyDefaultWindowSize('no saved main-window geometry')
else:
try:
restored = self.restoreInitialGeometry(savedGeometry)
except Exception:
# Any non-exit exceptions
logger.exception('unexpected main-window geometry restore failure')
restored = False
if restored and self.size() == self.QT_FALLBACK_WINDOW_SIZE:
self._applyDefaultWindowSize(
'saved main-window geometry restored to the Qt fallback size'
)
elif restored:
logger.info(
f'restored main-window geometry: {self.geometry().getRect()}'
)
else:
self._applyDefaultWindowSize('saved main-window geometry was invalid')
self._restoreMainWindowState()
def _applyDefaultWindowSize(self, reason: str):
"""Apply the canonical first-launch/recovery size and center it."""
self.resize(self.DEFAULT_WINDOW_SIZE)
logger.info(
f'{reason}; using default main-window size '
f'{self.DEFAULT_WINDOW_SIZE.toTuple()}'
)
def _restoreLegacyWindowSize(self) -> bool:
"""Restore one valid legacy client size without guessing its origin."""
legacySize = AppSettings.get('ServerWidgetWindowSize')
if legacySize is None:
return False
try:
widthText, heightText = legacySize.split(',')
width, height = int(widthText), int(heightText)
if width <= 0 or height <= 0:
raise ValueError('window dimensions must be positive')
except (AttributeError, TypeError, ValueError):
logger.warning(f'ignored invalid legacy main-window size: {legacySize!r}')
return False
self.resize(width, height)
logger.info(f'migrated legacy main-window size: {(width, height)}')
return True
def _restoreMainWindowState(self):
"""Restore QMainWindow layout state independently from its geometry."""
savedState = AppSettings.get('AppMainWindowState')
if savedState is None:
return
try:
restored = self.restoreState(savedState)
except Exception:
# Any non-exit exceptions
logger.exception('unexpected main-window state restore failure')
return
if not restored:
logger.warning('saved main-window state was invalid and was ignored')
def showEvent(self, event):
"""Focus the window itself instead of an untouched child control."""
super().showEvent(event)
self.setFocus(QtCore.Qt.FocusReason.OtherFocusReason)
def retranslate(self):
"""Refresh the application window title."""
if SystemRuntime.isAdmin():
self.setWindowTitle(f'{_(APPLICATION_NAME)} ({_(ADMINISTRATOR_NAME)})')
else:
self.setWindowTitle(_(APPLICATION_NAME))
def cleanup(self):
"""Persist application-level window state."""
# Tray startup can construct MainWindow without ever presenting it. In
# that case saveGeometry() describes Qt's native pre-show placeholder,
# not a user decision, so preserve the last persisted window state.
if not self.hasPreparedInitialGeometry():
return
AppSettings.set('AppMainWindowGeometry', self.saveGeometry())
AppSettings.set('AppMainWindowState', self.saveState())