handOver2/ui/styles/style_manager.py

459 lines
14 KiB
Python

# -*- 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()