459 lines
14 KiB
Python
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()
|
|
|