PythonApplicationTemplate/app/core/settings_manager.py

210 lines
8.9 KiB
Python

from PyQt6.QtCore import QSettings
from app.core.observer_manager import ObserverManager, NotificationType
from app.core.theme_manager import ThemeManager
from os import path
import app.utils.paths as paths
import json
import logging
from typing import Dict, Any, Union
# Configure logging
logger: logging.Logger = logging.getLogger(__name__)
class SettingsManager:
"""
Gestion des paramètres utilisateurs avec sauvegarde persistante via QSettings.
Notifie les changements via ObserverManager.
"""
def __init__(self, observer_manager: ObserverManager, theme_manager: ThemeManager) -> None:
self.observer_manager: ObserverManager = observer_manager
self.theme_manager: ThemeManager = theme_manager
# Hardcoded fallback settings in case files are missing
self.fallback_settings: Dict[str, Any] = {
"theme": "dark",
"lang": "en",
"window_size": {"width": 1000, "height": 600},
"maximized": False
}
self.fallback_config: Dict[str, Any] = {
"app_name": "Application",
"python_version": "3.11.7",
"app_os": "Windows",
"app_version": "1.0.0",
"architecture": "x64",
"icon_path": "data/assets/icon.ico",
"main_script": "main.py",
"git_repo": ""
}
# Load default settings
self.default_settings: Dict[str, Any] = self._load_default_settings()
# Load config
self.config: Dict[str, Any] = self._load_config()
# Initialize QSettings with error handling
self.settings: QSettings = self._initialize_qsettings()
# Set initial theme
try:
self.theme_manager.set_theme(self.get_theme())
except Exception as e:
logger.error(f"Error setting initial theme: {e}")
self.theme_manager.set_theme(self.fallback_settings["theme"])
def _load_default_settings(self) -> Dict[str, Any]:
"""Load default settings with error handling"""
try:
defaults_path = path.join(paths.get_data_dir(), "others", "defaults_settings.json")
if path.exists(defaults_path):
with open(defaults_path, 'r', encoding='utf-8') as f:
settings = json.load(f)
# Validate required keys
for key in self.fallback_settings.keys():
if key not in settings:
logger.warning(f"Missing key '{key}' in defaults_settings.json, using fallback")
settings[key] = self.fallback_settings[key]
return settings
else:
logger.warning(f"defaults_settings.json not found, using fallback settings")
return self.fallback_settings.copy()
except (json.JSONDecodeError, FileNotFoundError, KeyError, UnicodeDecodeError) as e:
logger.error(f"Error loading default settings: {e}")
return self.fallback_settings.copy()
except Exception as e:
logger.error(f"Unexpected error loading default settings: {e}")
return self.fallback_settings.copy()
def _load_config(self) -> Dict[str, Any]:
"""Load config.json with error handling"""
try:
config_path = paths.resource_path("config.json")
if path.exists(config_path):
with open(config_path, 'r', encoding='utf-8') as f:
config = json.load(f)
# Validate required keys
for key in self.fallback_config.keys():
if key not in config:
logger.warning(f"Missing key '{key}' in config.json, using fallback")
config[key] = self.fallback_config[key]
return config
else:
logger.warning("config.json not found, using fallback config")
return self.fallback_config.copy()
except (json.JSONDecodeError, FileNotFoundError, KeyError, UnicodeDecodeError) as e:
logger.error(f"Error loading config: {e}")
return self.fallback_config.copy()
except Exception as e:
logger.error(f"Unexpected error loading config: {e}")
return self.fallback_config.copy()
def _initialize_qsettings(self) -> QSettings:
"""Initialize QSettings with error handling"""
try:
app_name = self.get_config("app_name")
user_data_dir = paths.get_user_data_dir(app_name)
settings_path = path.join(user_data_dir, f"{app_name}.ini")
return QSettings(settings_path, QSettings.Format.IniFormat)
except Exception as e:
logger.error(f"Error initializing QSettings: {e}")
# Fallback to memory-only settings
return QSettings()
# Config
def get_config(self, key: str) -> Any:
"""Get configuration value by key"""
try:
return self.config.get(key, self.fallback_config.get(key))
except Exception as e:
logger.error(f"Error getting config key '{key}': {e}")
return self.fallback_config.get(key)
# Theme
def get_theme(self) -> str:
"""Get the current theme"""
try:
return self.settings.value("theme", self.default_settings.get("theme"))
except Exception as e:
logger.error(f"Error getting theme: {e}")
return self.fallback_settings["theme"]
def set_theme(self, mode: str) -> None:
"""Set the application theme"""
try:
if mode != self.get_theme():
self.settings.setValue("theme", mode)
self.theme_manager.set_theme(mode)
self.observer_manager.notify(NotificationType.THEME)
except Exception as e:
logger.error(f"Error setting theme: {e}")
# Language
def get_language(self) -> str:
"""Get the current language"""
try:
return self.settings.value("lang", self.default_settings.get("lang"))
except Exception as e:
logger.error(f"Error getting language: {e}")
return self.fallback_settings["lang"]
def set_language(self, lang_code: str) -> None:
"""Set the application language"""
try:
if lang_code != self.get_language():
self.settings.setValue("lang", lang_code)
self.observer_manager.notify(NotificationType.LANGUAGE)
except Exception as e:
logger.error(f"Error setting language: {e}")
# Window size and maximized
def get_window_size(self) -> Dict[str, int]:
"""Get the window size"""
try:
size = self.settings.value("window_size", self.default_settings.get("window_size"))
# Validate window size values
if isinstance(size, dict) and "width" in size and "height" in size:
width = int(size["width"]) if size["width"] > 0 else self.fallback_settings["window_size"]["width"]
height = int(size["height"]) if size["height"] > 0 else self.fallback_settings["window_size"]["height"]
return {"width": width, "height": height}
else:
return self.fallback_settings["window_size"].copy()
except Exception as e:
logger.error(f"Error getting window size: {e}")
return self.fallback_settings["window_size"].copy()
def set_window_size(self, width: int, height: int) -> None:
"""Set the window size"""
try:
# Validate input values
if width <= 0 or height <= 0:
logger.warning(f"Invalid window size: {width}x{height}")
return
current_size = self.get_window_size()
if current_size["width"] != width or current_size["height"] != height:
new_size = {"width": width, "height": height}
self.settings.setValue("window_size", new_size)
except Exception as e:
logger.error(f"Error setting window size: {e}")
def get_maximized(self) -> bool:
"""Check if the window is maximized"""
try:
value = self.settings.value("maximized", self.default_settings.get("maximized"))
if isinstance(value, str):
return value.lower() == "true"
elif isinstance(value, bool):
return value
else:
return self.fallback_settings["maximized"]
except Exception as e:
logger.error(f"Error getting maximized state: {e}")
return self.fallback_settings["maximized"]
def set_maximized(self, maximized: bool) -> None:
"""Set the window maximized state"""
try:
self.settings.setValue("maximized", maximized)
except Exception as e:
logger.error(f"Error setting maximized state: {e}")