# app/utils/theme_manager.py """ Theme Manager ============= Centralizza la gestione del tema dell'applicazione. Elimina hard-coded styles e fornisce stylesheet generati dal config.yaml. Features: - Genera QSS (Qt StyleSheet) dal config.yaml - Fornisce utility per colori e font del tema - Supporta theme switching (light/dark future) - Validazione del theme config - Caching degli stylesheet generati """ import yaml from pathlib import Path from typing import Dict, Any, Optional from loguru import logger class ThemeManager: """ Gestore centralizzato del tema dell'applicazione. Usage: theme_mgr = ThemeManager() stylesheet = theme_mgr.get_main_stylesheet() app.setStyleSheet(stylesheet) # Oppure per widget specifici button_style = theme_mgr.get_button_stylesheet() btn.setStyleSheet(button_style) """ def __init__(self, config_path: Optional[Path] = None): """ Inizializza il ThemeManager. Args: config_path: Path al config.yaml. Se None, usa il default. """ self.config_path = config_path or self._get_default_config_path() self.theme_config = self._load_theme_config() self._stylesheet_cache = {} logger.info(f"ThemeManager inizializzato con tema: {self.theme_config.get('name', 'Default')}") def _get_default_config_path(self) -> Path: """Ritorna il path di default al config.yaml""" return Path(__file__).parent.parent / "config" / "config.yaml" def _load_theme_config(self) -> Dict[str, Any]: """Carica la configurazione del tema dal YAML""" try: with open(self.config_path, 'r', encoding='utf-8') as f: full_config = yaml.safe_load(f) theme = full_config.get('theme', {}) # Validazione base if not theme: logger.warning("Nessun tema trovato nel config.yaml, uso defaults") return self._get_default_theme() return theme except Exception as e: logger.error(f"Errore caricamento theme config: {e}", exc_info=True) return self._get_default_theme() def _get_default_theme(self) -> Dict[str, Any]: """Ritorna un tema di fallback con valori Microsoft-style""" return { 'primary_color': '#0078D4', 'background_primary': '#FFFFFF', 'background_panel': '#F3F2F1', 'text_primary': '#323130', 'text_secondary': '#605E5C', 'border_color': '#EDEBE9', 'border_dark': '#D2D0CE', 'fonts': { 'system': 'Segoe UI', 'primary_size': 12, 'small_size': 11, 'large_size': 14 }, 'spacing': { 'sm': 8, 'md': 12, 'lg': 16 } } # ========== GETTER UTILITIES ========== def get_color(self, key: str, default: str = '#000000') -> str: """ Ottiene un colore dal tema. Args: key: Chiave del colore (es. 'primary_color', 'text_primary') default: Colore di fallback se la chiave non esiste Returns: Stringa colore in formato hex (#RRGGBB) """ return self.theme_config.get(key, default) def get_font_family(self) -> str: """Ottiene il font family principale""" fonts = self.theme_config.get('fonts', {}) return fonts.get('system', 'Segoe UI') def get_font_size(self, size_key: str = 'primary_size') -> int: """ Ottiene una dimensione font dal tema. Args: size_key: Chiave della dimensione (primary_size, small_size, large_size, etc.) Returns: Dimensione in px """ fonts = self.theme_config.get('fonts', {}) return fonts.get(size_key, 12) def get_spacing(self, size_key: str = 'md') -> int: """ Ottiene uno spacing dal tema. Args: size_key: Chiave dello spacing (xs, sm, md, lg, xl) Returns: Spacing in px """ spacing = self.theme_config.get('spacing', {}) return spacing.get(size_key, 12) # ========== STYLESHEET GENERATORS ========== def get_main_stylesheet(self) -> str: """ Genera il main application stylesheet. Sostituisce gli hard-coded styles sparsi nel codice. Returns: QSS completo per l'applicazione """ if 'main' in self._stylesheet_cache: return self._stylesheet_cache['main'] qss = f""" /* ======================================== MyICR Suite - Main Application Stylesheet Generated by ThemeManager ======================================== */ /* === GLOBAL DEFAULTS === */ QWidget {{ font-family: {self.get_font_family()}; font-size: {self.get_font_size('primary_size')}px; color: {self.get_color('text_primary')}; background-color: {self.get_color('background_primary')}; }} QMainWindow {{ background-color: {self.get_color('background_panel')}; }} /* === BUTTONS === */ QPushButton {{ background-color: {self.get_color('background_secondary', '#FAFAFA')}; border: 1px solid {self.get_color('border_color')}; border-radius: 2px; padding: {self.get_spacing('sm')}px {self.get_spacing('md')}px; color: {self.get_color('text_primary')}; min-height: 24px; }} QPushButton:hover {{ background-color: {self.get_color('background_hover', '#F3F2F1')}; border: 1px solid {self.get_color('primary_color')}; }} QPushButton:pressed {{ background-color: {self.get_color('primary_pressed', '#005A9E')}; color: white; }} QPushButton:disabled {{ background-color: {self.get_color('background_panel')}; color: {self.get_color('text_light', '#8A8886')}; border: 1px solid {self.get_color('border_color')}; }} /* === PRIMARY BUTTON (Accent) === */ QPushButton[primary="true"] {{ background-color: {self.get_color('primary_color')}; color: white; border: 1px solid {self.get_color('primary_color')}; font-weight: 500; }} QPushButton[primary="true"]:hover {{ background-color: {self.get_color('primary_hover', '#106EBE')}; }} QPushButton[primary="true"]:pressed {{ background-color: {self.get_color('primary_pressed')}; }} /* === COMBO BOX === */ QComboBox {{ background-color: white; border: 1px solid {self.get_color('border_color')}; border-radius: 2px; padding: 4px 8px; min-height: 24px; }} QComboBox:hover {{ border: 1px solid {self.get_color('primary_color')}; }} QComboBox::drop-down {{ border: none; width: 20px; }} QComboBox QAbstractItemView {{ background-color: white; border: 1px solid {self.get_color('border_dark')}; selection-background-color: {self.get_color('primary_light', '#DEECF9')}; selection-color: {self.get_color('text_primary')}; }} /* === LINE EDIT === */ QLineEdit, QTextEdit, QPlainTextEdit {{ background-color: white; border: 1px solid {self.get_color('border_color')}; border-radius: 2px; padding: 4px 8px; selection-background-color: {self.get_color('primary_light')}; }} QLineEdit:focus, QTextEdit:focus {{ border: 1px solid {self.get_color('primary_color')}; border-bottom: 2px solid {self.get_color('primary_color')}; }} /* === LABELS === */ QLabel {{ background-color: transparent; color: {self.get_color('text_primary')}; }} QLabel[heading="true"] {{ font-size: {self.get_font_size('large_size')}px; font-weight: 600; color: {self.get_color('text_primary')}; }} QLabel[secondary="true"] {{ color: {self.get_color('text_secondary')}; }} /* === FRAMES / PANELS === */ QFrame[panel="true"] {{ background-color: white; border: 1px solid {self.get_color('border_color')}; border-radius: 2px; }} /* === MENU BAR === */ QMenuBar {{ background-color: {self.get_color('background_panel')}; border-bottom: 1px solid {self.get_color('border_dark')}; spacing: 4px; padding: 2px; }} QMenuBar::item {{ background-color: transparent; padding: 4px 12px; border-radius: 2px; }} QMenuBar::item:selected {{ background-color: {self.get_color('background_hover')}; }} QMenuBar::item:pressed {{ background-color: {self.get_color('primary_light')}; }} /* === MENU === */ QMenu {{ background-color: white; border: 1px solid {self.get_color('border_dark')}; padding: 4px; }} QMenu::item {{ padding: 6px 24px; border-radius: 2px; }} QMenu::item:selected {{ background-color: {self.get_color('primary_light')}; }} /* === STATUS BAR === */ QStatusBar {{ background-color: {self.get_color('background_panel')}; border-top: 1px solid {self.get_color('border_dark')}; color: {self.get_color('text_secondary')}; padding: {self.get_spacing('sm')}px; }} /* === TABLE WIDGET === */ QTableWidget {{ background-color: white; border: 1px solid {self.get_color('border_color')}; gridline-color: {self.get_color('border_color')}; selection-background-color: {self.get_color('primary_light')}; selection-color: {self.get_color('text_primary')}; }} QTableWidget::item {{ padding: 4px; border: none; }} QTableWidget::item:hover {{ background-color: {self.get_color('background_hover')}; }} QHeaderView::section {{ background-color: {self.get_color('background_panel')}; color: {self.get_color('text_primary')}; border: none; border-right: 1px solid {self.get_color('border_color')}; border-bottom: 1px solid {self.get_color('border_dark')}; padding: 6px; font-weight: 600; }} QHeaderView::section:hover {{ background-color: {self.get_color('background_hover')}; }} /* === SCROLLBAR === */ QScrollBar:vertical {{ background-color: {self.get_color('background_panel')}; width: 12px; border: none; }} QScrollBar::handle:vertical {{ background-color: {self.get_color('border_dark')}; min-height: 30px; border-radius: 6px; margin: 2px; }} QScrollBar::handle:vertical:hover {{ background-color: {self.get_color('secondary_color', '#605E5C')}; }} QScrollBar::add-line:vertical, QScrollBar::sub-line:vertical {{ height: 0px; }} QScrollBar:horizontal {{ background-color: {self.get_color('background_panel')}; height: 12px; border: none; }} QScrollBar::handle:horizontal {{ background-color: {self.get_color('border_dark')}; min-width: 30px; border-radius: 6px; margin: 2px; }} QScrollBar::handle:horizontal:hover {{ background-color: {self.get_color('secondary_color')}; }} /* === TAB WIDGET === */ QTabWidget::pane {{ border: 1px solid {self.get_color('border_color')}; border-top: none; background-color: white; }} QTabBar::tab {{ background-color: {self.get_color('background_panel')}; border: 1px solid {self.get_color('border_color')}; border-bottom: none; padding: 6px 16px; margin-right: 2px; }} QTabBar::tab:selected {{ background-color: white; border-bottom: 2px solid {self.get_color('primary_color')}; }} QTabBar::tab:hover {{ background-color: {self.get_color('background_hover')}; }} /* === TOOLTIPS === */ QToolTip {{ background-color: {self.get_color('text_primary')}; color: white; border: none; padding: 4px 8px; border-radius: 2px; font-size: {self.get_font_size('small_size')}px; }} """ self._stylesheet_cache['main'] = qss return qss def get_button_stylesheet(self, is_primary: bool = False) -> str: """ Genera stylesheet per un bottone singolo. Args: is_primary: Se True, genera stile accent button Returns: QSS per QPushButton """ if is_primary: return f""" QPushButton {{ background-color: {self.get_color('primary_color')}; color: white; border: 1px solid {self.get_color('primary_color')}; border-radius: 2px; padding: {self.get_spacing('sm')}px {self.get_spacing('md')}px; font-weight: 500; min-height: 28px; }} QPushButton:hover {{ background-color: {self.get_color('primary_hover')}; }} QPushButton:pressed {{ background-color: {self.get_color('primary_pressed')}; }} QPushButton:disabled {{ background-color: {self.get_color('border_color')}; color: {self.get_color('text_light')}; }} """ else: return f""" QPushButton {{ background-color: {self.get_color('background_secondary')}; border: 1px solid {self.get_color('border_color')}; border-radius: 2px; padding: {self.get_spacing('sm')}px {self.get_spacing('md')}px; color: {self.get_color('text_primary')}; min-height: 28px; }} QPushButton:hover {{ background-color: {self.get_color('background_hover')}; border: 1px solid {self.get_color('primary_color')}; }} QPushButton:pressed {{ background-color: {self.get_color('primary_light')}; }} QPushButton:disabled {{ background-color: {self.get_color('background_panel')}; color: {self.get_color('text_light')}; }} """ def get_panel_stylesheet(self, with_border: bool = True) -> str: """ Genera stylesheet per un panel/frame. Args: with_border: Se True, include bordo Returns: QSS per QFrame """ border_style = f"border: 1px solid {self.get_color('border_color')};" if with_border else "border: none;" return f""" QFrame {{ background-color: white; {border_style} border-radius: 2px; }} """ def clear_cache(self): """Pulisce la cache degli stylesheet (utile per theme switching)""" self._stylesheet_cache.clear() logger.info("Theme stylesheet cache cleared") def reload_theme(self): """Ricarica il tema dal config.yaml""" self.theme_config = self._load_theme_config() self.clear_cache() logger.info("Theme config reloaded") # ========== SINGLETON INSTANCE ========== _theme_manager_instance: Optional[ThemeManager] = None def get_theme_manager() -> ThemeManager: """ Ottiene l'istanza singleton del ThemeManager. Returns: ThemeManager instance """ global _theme_manager_instance if _theme_manager_instance is None: _theme_manager_instance = ThemeManager() return _theme_manager_instance