# -*- coding: utf-8 -*- """ 스타일 관리자 모듈 UI 스타일을 중앙에서 관리합니다. 이 모듈은 다음 기능을 제공합니다: - 테마별 색상 관리 - 폰트 설정 관리 - 위젯별 스타일시트 생성 - 텍스트 크기 기반 높이 계산 """ from typing import Dict, Optional from PySide6.QtGui import QFont, QFontMetrics from core.config import ConfigManager from core.logger import get_logger logger = get_logger(__name__) class StyleManager: """ 스타일 관리자 클래스 싱글톤 패턴으로 애플리케이션 전역에서 하나의 인스턴스만 사용합니다. """ _instance: Optional['StyleManager'] = None _initialized = False def __new__(cls): """싱글톤 패턴 구현""" if cls._instance is None: cls._instance = super().__new__(cls) return cls._instance def __init__(self): """스타일 관리자 초기화""" if StyleManager._initialized: return self.config = ConfigManager() StyleManager._initialized = True logger.info("스타일 관리자 초기화 완료") # ======================================================================== # 색상 관리 # ======================================================================== def get_colors(self) -> Dict[str, str]: """ 현재 테마의 색상 딕셔너리 반환 Returns: 색상 딕셔너리 """ theme = self.config.theme if theme == 'dark': return { # 배경 "bg_primary": "#0f172a", "bg_secondary": "#1e293b", "bg_tertiary": "#334155", "bg_hover": "#475569", # 텍스트 "text_primary": "#f8fafc", # 밝은 텍스트 (어두운 배경용) "text_secondary": "#cbd5e1", "text_tertiary": "#94a3b8", "text_disabled": "#64748b", # 테두리 "border": "#334155", "border_light": "#475569", "border_dark": "#1e293b", # 강조 "accent": "#3b82f6", "accent_hover": "#2563eb", "accent_light": "#60a5fa", # 상태 "success": "#22c55e", "warning": "#f59e0b", "error": "#ef4444", "info": "#3b82f6", # 입력 필드 "input_bg": "#1e293b", "input_border": "#475569", "input_focus": "#3b82f6", "input_text": "#f8fafc", # 버튼 "btn_primary_bg": "#3b82f6", "btn_primary_hover": "#2563eb", "btn_primary_text": "#ffffff", "btn_secondary_bg": "#334155", "btn_secondary_hover": "#475569", "btn_secondary_text": "#f8fafc", } else: # light return { # 배경 "bg_primary": "#ffffff", "bg_secondary": "#f8fafc", "bg_tertiary": "#f1f5f9", "bg_hover": "#e2e8f0", # 텍스트 "text_primary": "#1e293b", # 어두운 텍스트 (밝은 배경용) "text_secondary": "#475569", "text_tertiary": "#64748b", "text_disabled": "#94a3b8", # 테두리 "border": "#e2e8f0", "border_light": "#f1f5f9", "border_dark": "#cbd5e1", # 강조 "accent": "#3b82f6", "accent_hover": "#2563eb", "accent_light": "#60a5fa", # 상태 "success": "#22c55e", "warning": "#f59e0b", "error": "#ef4444", "info": "#3b82f6", # 입력 필드 "input_bg": "#ffffff", "input_border": "#e2e8f0", "input_focus": "#3b82f6", "input_text": "#1e293b", # 버튼 "btn_primary_bg": "#3b82f6", "btn_primary_hover": "#2563eb", "btn_primary_text": "#ffffff", "btn_secondary_bg": "#e2e8f0", "btn_secondary_hover": "#cbd5e1", "btn_secondary_text": "#1e293b", } def get_color(self, key: str) -> str: """ 특정 색상 가져오기 Args: key: 색상 키 Returns: 색상 코드 """ colors = self.get_colors() return colors.get(key, "#000000") # ======================================================================== # 폰트 관리 # ======================================================================== def get_font(self, area: str, style: str) -> QFont: """ UI 영역별 폰트 반환 Args: area: 영역 (info_bar, section, todo, memo, daily, status, dialog) style: 스타일 (title, content, header, label, input, button 등) Returns: QFont 객체 """ font_config = self.config.get_ui_font(area, style) font = QFont( font_config.get("family", "GmarketSans"), font_config.get("size", 13) ) weight = font_config.get("weight", "normal") if weight == "bold": font.setWeight(QFont.Bold) elif weight == "medium": font.setWeight(QFont.Medium) else: font.setWeight(QFont.Normal) return font def get_font_size(self, area: str, style: str) -> int: """폰트 크기만 반환""" font_config = self.config.get_ui_font(area, style) return font_config.get("size", 13) # ======================================================================== # 높이 계산 # ======================================================================== def calculate_input_height( self, font: Optional[QFont] = None, area: str = "dialog", style: str = "input", min_height: int = 32, padding: int = 8 ) -> int: """ 텍스트 크기를 기반으로 입력 필드 높이 계산 Args: font: QFont 객체 (None이면 area, style로부터 가져옴) area: UI 영역 style: 스타일 min_height: 최소 높이 padding: 상하 패딩 Returns: 계산된 높이 (픽셀) """ if font is None: font = self.get_font(area, style) metrics = QFontMetrics(font) font_height = metrics.height() # 폰트 높이 + 상하 패딩 calculated_height = font_height + (padding * 2) # 최소 높이 보장 return max(calculated_height, min_height) def calculate_label_height( self, font: Optional[QFont] = None, area: str = "dialog", style: str = "label", min_height: int = 20, padding: int = 4 ) -> int: """ 텍스트 크기를 기반으로 레이블 높이 계산 Args: font: QFont 객체 area: UI 영역 style: 스타일 min_height: 최소 높이 padding: 상하 패딩 Returns: 계산된 높이 (픽셀) """ if font is None: font = self.get_font(area, style) metrics = QFontMetrics(font) font_height = metrics.height() calculated_height = font_height + (padding * 2) return max(calculated_height, min_height) # ======================================================================== # 스타일시트 생성 # ======================================================================== def get_input_stylesheet( self, area: str = "dialog", style: str = "input", height: Optional[int] = None ) -> str: """ 입력 필드 스타일시트 생성 Args: area: UI 영역 style: 스타일 height: 높이 (None이면 자동 계산) Returns: 스타일시트 문자열 """ colors = self.get_colors() if height is None: height = self.calculate_input_height(area=area, style=style) font = self.get_font(area, style) font_size = font.pointSize() font_family = font.family() return f""" QLineEdit, QTextEdit, QComboBox, QSpinBox {{ background-color: {colors['input_bg']}; color: {colors['input_text']}; border: 1px solid {colors['input_border']}; border-radius: 6px; padding: {height // 4}px 12px; font-family: '{font_family}'; font-size: {font_size}pt; min-height: {height}px; }} QLineEdit:focus, QTextEdit:focus, QComboBox:focus, QSpinBox:focus {{ border-color: {colors['input_focus']}; outline: none; }} QComboBox::drop-down {{ border: none; width: 25px; }} QComboBox::down-arrow {{ image: none; border-left: 5px solid transparent; border-right: 5px solid transparent; border-top: 6px solid {colors['input_text']}; margin-right: 5px; }} QComboBox QAbstractItemView {{ background-color: {colors['input_bg']}; color: {colors['input_text']}; border: 1px solid {colors['input_border']}; selection-background-color: {colors['accent']}; selection-color: {colors['btn_primary_text']}; }} """ def get_label_stylesheet( self, area: str = "dialog", style: str = "label", height: Optional[int] = None ) -> str: """ 레이블 스타일시트 생성 Args: area: UI 영역 style: 스타일 height: 높이 (None이면 자동 계산) Returns: 스타일시트 문자열 """ colors = self.get_colors() if height is None: height = self.calculate_label_height(area=area, style=style) font = self.get_font(area, style) font_size = font.pointSize() font_family = font.family() font_weight = "bold" if font.weight() == QFont.Bold else "normal" return f""" QLabel[dialogLabel="true"] {{ color: {colors['text_primary']}; font-family: '{font_family}'; font-size: {font_size}pt; font-weight: {font_weight}; min-height: {height}px; padding: {height // 4}px 0px; }} """ def get_button_stylesheet( self, style_type: str = "primary", area: str = "dialog", style: str = "button" ) -> str: """ 버튼 스타일시트 생성 Args: style_type: 버튼 타입 (primary, secondary, outline, danger) area: UI 영역 style: 스타일 Returns: 스타일시트 문자열 """ colors = self.get_colors() font = self.get_font(area, style) font_size = font.pointSize() font_family = font.family() if style_type == "primary": bg = colors['btn_primary_bg'] hover = colors['btn_primary_hover'] text = colors['btn_primary_text'] elif style_type == "secondary": bg = colors['btn_secondary_bg'] hover = colors['btn_secondary_hover'] text = colors['btn_secondary_text'] elif style_type == "outline": bg = "transparent" hover = colors['bg_hover'] text = colors['text_primary'] border_value = f"1px solid {colors['border']}" elif style_type == "danger": bg = colors['error'] hover = "#dc2626" text = "#ffffff" border_value = "none" else: bg = colors['btn_secondary_bg'] hover = colors['btn_secondary_hover'] text = colors['btn_secondary_text'] border_value = "none" border_style = f"border: {border_value};" return f""" QPushButton {{ background-color: {bg}; color: {text}; {border_style} border-radius: 6px; padding: 8px 16px; font-family: '{font_family}'; font-size: {font_size}pt; min-height: {self.calculate_input_height(area=area, style=style)}px; }} QPushButton:hover {{ background-color: {hover}; }} QPushButton:pressed {{ background-color: {hover}; opacity: 0.9; }} QPushButton:disabled {{ background-color: {colors['bg_tertiary']}; color: {colors['text_disabled']}; opacity: 0.5; }} """ def get_dialog_stylesheet(self) -> str: """다이얼로그 전체 스타일시트 생성""" colors = self.get_colors() return f""" QDialog {{ background-color: {colors['bg_secondary']}; color: {colors['text_primary']}; }} """ # 편의 함수 def get_style_manager() -> StyleManager: """스타일 관리자 인스턴스 반환""" return StyleManager()