""" 테마 시스템 - 라이트/다크 테마 지원 """ import os import tempfile # ─── 화살표 아이콘 생성 ────────────────────────────────────── _arrow_dir = os.path.join(tempfile.gettempdir(), 'clockout_arrows') os.makedirs(_arrow_dir, exist_ok=True) _light_arrows = {} _dark_arrows = {} _checkmark = '' _icons_initialized = False def _ensure_icons(): """아이콘 PNG가 필요할 때 생성 (QApplication 존재 필요)""" global _light_arrows, _dark_arrows, _checkmark, _icons_initialized if _icons_initialized: return _icons_initialized = True try: from PyQt5.QtGui import QPixmap, QPainter, QColor as _QColor, QPolygon, QPen from PyQt5.QtCore import QPoint except ImportError: return arrows = {} for name, color_hex, points in [ ('up_light', '#4A4A68', [(4, 7), (8, 3), (12, 7)]), ('down_light', '#4A4A68', [(4, 5), (8, 9), (12, 5)]), ('up_dark', '#909296', [(4, 7), (8, 3), (12, 7)]), ('down_dark', '#909296', [(4, 5), (8, 9), (12, 5)]), ]: path = os.path.join(_arrow_dir, f'{name}.png') if not os.path.exists(path): pm = QPixmap(16, 12) pm.fill(_QColor(0, 0, 0, 0)) p = QPainter(pm) p.setRenderHint(QPainter.Antialiasing) p.setPen(_QColor(color_hex)) p.setBrush(_QColor(color_hex)) poly = QPolygon([QPoint(x, y) for x, y in points]) p.drawPolygon(poly) p.end() pm.save(path, 'PNG') arrows[name] = path.replace('\\', '/') # 체크마크 아이콘 생성 checkmark_path = os.path.join(_arrow_dir, 'checkmark.png') if not os.path.exists(checkmark_path): pm = QPixmap(14, 14) pm.fill(_QColor(0, 0, 0, 0)) p = QPainter(pm) p.setRenderHint(QPainter.Antialiasing) pen = QPen(_QColor('#FFFFFF')) pen.setWidth(2) p.setPen(pen) p.drawLine(QPoint(2, 7), QPoint(5, 11)) p.drawLine(QPoint(5, 11), QPoint(11, 3)) p.end() pm.save(checkmark_path, 'PNG') _light_arrows = {k: v for k, v in arrows.items() if 'light' in k} _dark_arrows = {k: v for k, v in arrows.items() if 'dark' in k} _checkmark = checkmark_path.replace('\\', '/') # ─── 색상 정의 ─────────────────────────────────────────────── LIGHT_COLORS = { # 배경 계층 'bg_primary': '#F5F5F7', 'bg_secondary': '#FFFFFF', 'bg_tertiary': '#EDEDF0', # 인터랙션 표면 'surface_hover': '#E2E3E7', 'surface_pressed': '#D5D6DB', # 텍스트 계층 'text_primary': '#1A1A2E', 'text_secondary': '#4A4A68', 'text_tertiary': '#8E8EA0', 'text_inverse': '#FFFFFF', # 액센트 'accent_primary': '#3B82F6', 'accent_primary_hover': '#2F74EE', 'accent_primary_pressed': '#2563EB', 'accent_success': '#10B981', 'accent_success_hover': '#0EA372', 'accent_success_pressed': '#0C8F63', 'accent_warning': '#F59E0B', 'accent_danger': '#EF4444', 'accent_danger_hover': '#DC2626', 'accent_danger_pressed': '#B91C1C', # 테두리 'border_subtle': '#E5E7EB', 'border_default': '#D1D5DB', 'border_focus': '#3B82F6', # 배지 배경 'badge_overtime_bg': '#FEF3C7', 'badge_overtime_text': '#92400E', 'badge_leave_bg': '#DBEAFE', 'badge_leave_text': '#1E40AF', 'badge_total_bg': '#D1FAE5', 'badge_total_text': '#065F46', # 프로그레스 'progress_bg': '#E5E7EB', 'progress_start': '#3B82F6', 'progress_end': '#10B981', # 상태 색상 (동적) 'status_overtime': '#EF4444', 'status_warning': '#F59E0B', 'status_normal': '#10B981', 'status_break_active': '#EF4444', 'status_break_idle': '#8E8EA0', # 캘린더 날짜 배경 'cal_normal': '#D1FAE5', 'cal_overtime': '#FEE2E2', 'cal_incomplete': '#FEF9C3', # 스크롤바 'scrollbar_bg': '#F5F5F7', 'scrollbar_handle': '#C4C4CC', 'scrollbar_hover': '#A0A0B0', } DARK_COLORS = { # 배경 계층 — 모던 다크 (Notion/Linear 톤) 'bg_primary': '#1A1B1E', # 앱 배경 'bg_secondary': '#25262B', # 카드 / 패널 'bg_tertiary': '#2C2E33', # 기본 버튼 / 미묘한 채움 # 인터랙션 표면 'surface_hover': '#34363D', 'surface_pressed': '#3A3D44', # 텍스트 계층 'text_primary': '#E9ECEF', 'text_secondary': '#909296', 'text_tertiary': '#6C6E73', 'text_inverse': '#FFFFFF', # 액센트 — 단일 포인트 컬러 (주요 버튼 + 포커스 전용) 'accent_primary': '#4DABF7', 'accent_primary_hover': '#69B6F8', 'accent_primary_pressed': '#3D97E0', 'accent_success': '#51CF66', 'accent_success_hover': '#69DB7C', 'accent_success_pressed': '#43B85A', 'accent_warning': '#FAB005', 'accent_danger': '#FA5252', 'accent_danger_hover': '#FF6B6B', 'accent_danger_pressed': '#E64545', # 테두리 'border_subtle': '#2C2E33', 'border_default': '#373A40', 'border_focus': '#4DABF7', # 배지 — 플랫 (미묘한 배경 + 색조 텍스트로 미니멀 유지) 'badge_overtime_bg': '#2C2E33', 'badge_overtime_text': '#FAB005', 'badge_leave_bg': '#2C2E33', 'badge_leave_text': '#4DABF7', 'badge_total_bg': '#2C2E33', 'badge_total_text': '#51CF66', # 프로그레스 — 단일 accent 솔리드 'progress_bg': '#2C2E33', 'progress_start': '#4DABF7', 'progress_end': '#4DABF7', # 상태 색상 (동적 텍스트 피드백) 'status_overtime': '#51CF66', # 퇴근 가능(연장근무 진입) = 그린 'status_warning': '#FAB005', 'status_normal': '#51CF66', 'status_break_active': '#FA5252', 'status_break_idle': '#6C6E73', # 캘린더 날짜 배경 — 미묘한 다크 틴트 'cal_normal': '#1E3A2A', 'cal_overtime': '#3A2122', 'cal_incomplete': '#3A331E', # 스크롤바 'scrollbar_bg': '#1A1B1E', 'scrollbar_handle': '#373A40', 'scrollbar_hover': '#4DABF7', } # ─── 현재 테마 상태 ───────────────────────────────────────── class ThemeColors: """런타임에 현재 테마 색상을 참조하는 헬퍼""" current = LIGHT_COLORS @classmethod def set_theme(cls, theme_name: str): cls.current = DARK_COLORS if theme_name == 'dark' else LIGHT_COLORS @classmethod def get(cls, key: str) -> str: return cls.current.get(key, '#FF00FF') # 누락 시 눈에 띄는 색 # ─── QSS 생성 ──────────────────────────────────────────────── def generate_theme(colors: dict, is_dark: bool = False) -> str: """색상 딕셔너리로부터 전체 QSS 문자열 생성""" _ensure_icons() c = colors arrows = _dark_arrows if is_dark else _light_arrows up_arrow = arrows.get('up_dark' if is_dark else 'up_light', '') down_arrow = arrows.get('down_dark' if is_dark else 'down_light', '') checkmark = _checkmark return f""" /* ════════════════════════════════════════ 기본 위젯 ════════════════════════════════════════ */ QMainWindow, QDialog {{ background: {c['bg_primary']}; }} QWidget {{ font-family: "NanumSquare", "NanumSquareOTF", "Malgun Gothic", "맑은 고딕", sans-serif; font-size: 9.5pt; color: {c['text_primary']}; }} QWidget#central_widget {{ background: transparent; }} /* ════════════════════════════════════════ 타이포그래피 ════════════════════════════════════════ */ QLabel#app_title {{ font-size: 13pt; font-weight: bold; color: {c['text_primary']}; padding: 2px; }} QLabel#date_label {{ font-size: 9.5pt; color: {c['text_secondary']}; padding-bottom: 4px; }} QLabel#section_title {{ font-size: 9.5pt; font-weight: bold; color: {c['text_secondary']}; }} QLabel#field_label {{ font-size: 9pt; color: {c['text_secondary']}; }} /* 출근/현재 시각 — 한 줄 나란히 표시되는 중간 크기 모노스페이스 */ QLabel#time_value {{ font-family: "Consolas", "D2Coding", monospace; font-size: 15pt; font-weight: bold; color: {c['text_primary']}; }} /* 히어로 — 남은 시간 (화면에서 가장 큰 결과 표시). 카드 안에 투명 배치 */ QLabel#time_display {{ font-family: "Consolas", "D2Coding", monospace; font-size: 30pt; font-weight: bold; color: {c['text_primary']}; background: transparent; border: none; padding: 4px 0; }} QLabel#expected_time {{ font-size: 11.5pt; font-weight: bold; color: {c['text_secondary']}; padding: 2px; }} QLabel#dialog_title {{ font-size: 14pt; font-weight: bold; color: {c['text_primary']}; padding: 16px; }} QLabel#dialog_subtitle {{ font-size: 12pt; font-weight: bold; color: {c['text_primary']}; }} QLabel#stat_value {{ font-size: 10pt; font-weight: bold; color: {c['accent_primary']}; }} QLabel#note_text {{ font-size: 8.5pt; color: {c['text_tertiary']}; }} QLabel#info_text {{ font-size: 9pt; color: {c['accent_danger']}; }} /* ════════════════════════════════════════ 배지 라벨 (배경색 있는 상태 표시) ════════════════════════════════════════ */ QLabel#badge_overtime {{ font-size: 9.5pt; font-weight: bold; padding: 4px 10px; min-width: 110px; qproperty-alignment: AlignCenter; background: {c['badge_overtime_bg']}; color: {c['badge_overtime_text']}; border-radius: 8px; }} QLabel#badge_leave {{ font-size: 9.5pt; font-weight: bold; padding: 4px 10px; min-width: 110px; qproperty-alignment: AlignCenter; background: {c['badge_leave_bg']}; color: {c['badge_leave_text']}; border-radius: 8px; }} QLabel#badge_total {{ font-size: 9.5pt; font-weight: bold; padding: 4px 10px; min-width: 110px; qproperty-alignment: AlignCenter; background: {c['badge_total_bg']}; color: {c['badge_total_text']}; border-radius: 8px; }} QLabel#badge_balance {{ font-size: 12pt; font-weight: bold; padding: 10px; background: {c['bg_tertiary']}; color: {c['text_primary']}; border-radius: 8px; }} QLabel#badge_success {{ font-size: 10pt; font-weight: bold; padding: 8px; background: {c['badge_total_bg']}; color: {c['badge_total_text']}; border-radius: 8px; }} /* ════════════════════════════════════════ 구분선 ════════════════════════════════════════ */ QLabel#separator {{ background: {c['border_subtle']}; max-height: 1px; min-height: 1px; }} /* ════════════════════════════════════════ 그룹 박스 (카드) ════════════════════════════════════════ */ QGroupBox {{ background: {c['bg_secondary']}; border: 1px solid {c['border_subtle']}; border-radius: 8px; margin-top: 10px; padding: 16px; padding-top: 28px; font-size: 9.5pt; color: {c['text_primary']}; }} QGroupBox::title {{ subcontrol-origin: margin; subcontrol-position: top left; padding: 3px 10px; margin-left: 8px; font-weight: bold; color: {c['text_secondary']}; background: {c['bg_secondary']}; border-radius: 4px; }} /* ════════════════════════════════════════ 버튼 ════════════════════════════════════════ */ /* 기본 버튼 — 그라데이션/베벨 없는 플랫 (border:none 기반) */ QPushButton {{ background: {c['bg_tertiary']}; color: {c['text_primary']}; border: none; border-radius: 8px; padding: 8px 14px; font-size: 9pt; }} QPushButton:hover {{ background: {c['surface_hover']}; }} QPushButton:pressed {{ background: {c['surface_pressed']}; }} QPushButton:disabled {{ background: {c['bg_secondary']}; color: {c['text_tertiary']}; }} QPushButton:checked {{ background: {c['accent_primary']}; color: {c['text_inverse']}; }} QPushButton:focus {{ outline: none; }} /* 퇴근 버튼 — 주요 액션 (단일 포인트 컬러) */ QPushButton#clock_out_button {{ background: {c['accent_primary']}; color: {c['text_inverse']}; font-size: 11pt; font-weight: bold; padding: 11px; border: none; border-radius: 8px; }} QPushButton#clock_out_button:hover {{ background: {c['accent_primary_hover']}; }} QPushButton#clock_out_button:pressed {{ background: {c['accent_primary_pressed']}; }} /* 주요 액션 버튼 */ QPushButton#btn_primary {{ background: {c['accent_primary']}; color: {c['text_inverse']}; border: none; font-weight: bold; }} QPushButton#btn_primary:hover {{ background: {c['accent_primary_hover']}; }} QPushButton#btn_primary:pressed {{ background: {c['accent_primary_pressed']}; }} /* 위험 버튼 */ QPushButton#btn_danger {{ background: {c['accent_danger']}; color: {c['text_inverse']}; border: none; }} QPushButton#btn_danger:hover {{ background: {c['accent_danger_hover']}; }} QPushButton#btn_danger:pressed {{ background: {c['accent_danger_pressed']}; }} /* 성공 버튼 */ QPushButton#btn_success {{ background: {c['accent_success']}; color: {c['text_inverse']}; border: none; }} QPushButton#btn_success:hover {{ background: {c['accent_success_hover']}; }} QPushButton#btn_success:pressed {{ background: {c['accent_success_pressed']}; }} /* 작은 버튼 — 미묘한 표면 */ QPushButton#btn_small {{ font-size: 8.5pt; padding: 6px 10px; }} QPushButton#btn_small:hover {{ background: {c['surface_hover']}; }} QPushButton#btn_small:pressed {{ background: {c['surface_pressed']}; }} /* 하단 네비게이션 — 라인 아이콘 + 라벨, 투명 배경 (Linear/Notion 풋터 톤) */ QPushButton#nav_btn {{ background: transparent; border: none; border-radius: 8px; padding: 8px 4px; font-size: 8.5pt; color: {c['text_secondary']}; }} QPushButton#nav_btn:hover {{ background: {c['surface_hover']}; color: {c['text_primary']}; }} QPushButton#nav_btn:pressed {{ background: {c['surface_pressed']}; }} /* ════════════════════════════════════════ 입력 필드 ════════════════════════════════════════ */ QLineEdit, QTextEdit, QComboBox {{ background: {c['bg_secondary']}; border: 1px solid {c['border_default']}; border-radius: 8px; padding: 6px 8px; color: {c['text_primary']}; font-size: 9.5pt; min-height: 20px; }} QSpinBox, QDoubleSpinBox, QDateEdit, QTimeEdit {{ background: {c['bg_secondary']}; border: 1px solid {c['border_default']}; border-radius: 8px; padding: 6px 28px 6px 8px; color: {c['text_primary']}; font-size: 9.5pt; min-height: 20px; }} /* 포커스 시 보더 컬러만 포인트 컬러로 (두께 유지 → 레이아웃 흔들림 없음) */ QLineEdit:focus, QTextEdit:focus, QComboBox:focus, QSpinBox:focus, QDoubleSpinBox:focus, QDateEdit:focus, QTimeEdit:focus {{ border: 1px solid {c['border_focus']}; }} /* 비활성 입력 필드 */ QLineEdit:disabled, QTextEdit:disabled, QComboBox:disabled, QSpinBox:disabled, QDoubleSpinBox:disabled, QDateEdit:disabled, QTimeEdit:disabled {{ background: {c['bg_tertiary']}; color: {c['text_tertiary']}; border-color: {c['border_subtle']}; }} QComboBox::drop-down {{ subcontrol-origin: padding; subcontrol-position: center right; width: 20px; border: none; padding-right: 4px; }} QComboBox::down-arrow {{ image: url({down_arrow}); width: 10px; height: 8px; }} QComboBox QAbstractItemView {{ background: {c['bg_secondary']}; border: 1px solid {c['border_default']}; color: {c['text_primary']}; selection-background-color: {c['accent_primary']}; selection-color: {c['text_inverse']}; }} QSpinBox::up-button, QSpinBox::down-button, QDoubleSpinBox::up-button, QDoubleSpinBox::down-button, QDateEdit::up-button, QDateEdit::down-button, QTimeEdit::up-button, QTimeEdit::down-button {{ background: {c['bg_tertiary']}; border: 1px solid {c['border_subtle']}; width: 22px; subcontrol-origin: border; }} QSpinBox::up-button, QDoubleSpinBox::up-button, QDateEdit::up-button, QTimeEdit::up-button {{ subcontrol-position: top right; border-top-right-radius: 7px; }} QSpinBox::down-button, QDoubleSpinBox::down-button, QDateEdit::down-button, QTimeEdit::down-button {{ subcontrol-position: bottom right; border-bottom-right-radius: 7px; }} QSpinBox::up-button:hover, QSpinBox::down-button:hover, QDoubleSpinBox::up-button:hover, QDoubleSpinBox::down-button:hover, QDateEdit::up-button:hover, QDateEdit::down-button:hover, QTimeEdit::up-button:hover, QTimeEdit::down-button:hover {{ background: {c['border_default']}; }} QSpinBox::up-arrow, QDoubleSpinBox::up-arrow, QDateEdit::up-arrow, QTimeEdit::up-arrow {{ image: url({up_arrow}); width: 10px; height: 8px; }} QSpinBox::down-arrow, QDoubleSpinBox::down-arrow, QDateEdit::down-arrow, QTimeEdit::down-arrow {{ image: url({down_arrow}); width: 10px; height: 8px; }} /* ════════════════════════════════════════ 체크박스 ════════════════════════════════════════ */ QCheckBox {{ spacing: 8px; color: {c['text_primary']}; font-size: 9pt; }} QCheckBox::indicator {{ width: 18px; height: 18px; border: 2px solid {c['border_default']}; border-radius: 4px; background: {c['bg_secondary']}; }} QCheckBox::indicator:checked {{ background: {c['accent_primary']}; border-color: {c['accent_primary']}; image: url({checkmark}); }} QCheckBox::indicator:hover {{ border-color: {c['accent_primary']}; }} /* ════════════════════════════════════════ 프로그레스 바 ════════════════════════════════════════ */ QProgressBar {{ border: none; background: {c['progress_bg']}; border-radius: 3px; min-height: 6px; max-height: 6px; text-align: center; color: transparent; font-size: 0px; }} QProgressBar::chunk {{ background: {c['progress_start']}; border-radius: 3px; }} /* ════════════════════════════════════════ 테이블 ════════════════════════════════════════ */ QTableWidget {{ background: {c['bg_secondary']}; border: 1px solid {c['border_subtle']}; border-radius: 8px; gridline-color: {c['border_subtle']}; color: {c['text_primary']}; font-size: 9pt; }} QTableWidget::item {{ padding: 6px 8px; }} QTableWidget::item:selected {{ background: {c['accent_primary']}30; color: {c['text_primary']}; }} QTableWidget::item:alternate {{ background: {c['bg_tertiary']}; }} /* 헤더 위젯 배경 (세로헤더 빈 영역의 흰색 누수 방지) */ QHeaderView {{ background: {c['bg_secondary']}; border: none; }} QHeaderView::section {{ background: {c['bg_tertiary']}; color: {c['text_secondary']}; padding: 8px; border: none; font-weight: bold; font-size: 9pt; }} QHeaderView::section:horizontal {{ border-bottom: 2px solid {c['accent_primary']}; }} /* 세로헤더(행번호) — accent 밑줄 없이 미묘하게 */ QHeaderView::section:vertical {{ border-right: 1px solid {c['border_subtle']}; color: {c['text_tertiary']}; font-weight: normal; padding: 4px 8px; }} /* 테이블 좌상단 코너 버튼 (흰색 누수 방지) */ QTableView QTableCornerButton::section {{ background: {c['bg_tertiary']}; border: none; border-bottom: 2px solid {c['accent_primary']}; }} /* ════════════════════════════════════════ 탭 위젯 ════════════════════════════════════════ */ QTabWidget::pane {{ border: 1px solid {c['border_subtle']}; border-radius: 8px; background: {c['bg_secondary']}; top: -1px; }} QTabBar::tab {{ background: {c['bg_tertiary']}; color: {c['text_secondary']}; padding: 8px 20px; border: 1px solid {c['border_subtle']}; border-bottom: none; border-top-left-radius: 8px; border-top-right-radius: 8px; margin-right: 2px; font-size: 10pt; }} QTabBar::tab:selected {{ background: {c['bg_secondary']}; color: {c['accent_primary']}; font-weight: bold; border-bottom: 2px solid {c['accent_primary']}; }} QTabBar::tab:hover:!selected {{ background: {c['border_subtle']}; color: {c['text_primary']}; }} /* ════════════════════════════════════════ 스크롤바 ════════════════════════════════════════ */ QScrollBar:vertical {{ background: {c['scrollbar_bg']}; width: 10px; border: none; border-radius: 5px; }} QScrollBar::handle:vertical {{ background: {c['scrollbar_handle']}; min-height: 30px; border-radius: 5px; }} QScrollBar::handle:vertical:hover {{ background: {c['scrollbar_hover']}; }} QScrollBar::add-line:vertical, QScrollBar::sub-line:vertical {{ height: 0px; }} QScrollBar::add-page:vertical, QScrollBar::sub-page:vertical {{ background: none; }} QScrollBar:horizontal {{ background: {c['scrollbar_bg']}; height: 10px; border: none; border-radius: 5px; }} QScrollBar::handle:horizontal {{ background: {c['scrollbar_handle']}; min-width: 30px; border-radius: 5px; }} QScrollBar::handle:horizontal:hover {{ background: {c['scrollbar_hover']}; }} QScrollBar::add-line:horizontal, QScrollBar::sub-line:horizontal {{ width: 0px; }} QScrollBar::add-page:horizontal, QScrollBar::sub-page:horizontal {{ background: none; }} QScrollArea {{ border: none; background: transparent; }} QWidget#fixed_bottom {{ background: {c['bg_primary']}; border-top: 1px solid {c['border_subtle']}; }} QScrollArea > QWidget > QWidget#scroll_content {{ background: transparent; }} /* ════════════════════════════════════════ 캘린더 ════════════════════════════════════════ */ QCalendarWidget {{ background: {c['bg_secondary']}; border: 1px solid {c['border_subtle']}; border-radius: 8px; font-size: 10pt; }} QCalendarWidget QAbstractItemView {{ selection-background-color: {c['accent_primary']}; selection-color: {c['text_inverse']}; background: {c['bg_secondary']}; color: {c['text_primary']}; alternate-background-color: {c['bg_secondary']}; }} /* 요일 헤더 행 */ QCalendarWidget QAbstractItemView:enabled {{ color: {c['text_primary']}; }} QCalendarWidget QHeaderView::section {{ background: {c['bg_tertiary']}; color: {c['text_secondary']}; font-weight: bold; border: none; border-bottom: 1px solid {c['border_subtle']}; padding: 4px; }} QCalendarWidget QWidget#qt_calendar_navigationbar {{ background: {c['bg_tertiary']}; border-top-left-radius: 6px; border-top-right-radius: 6px; padding: 4px; }} QCalendarWidget QToolButton {{ color: {c['text_primary']}; background: transparent; font-size: 11pt; font-weight: bold; padding: 6px 12px; border-radius: 6px; min-width: 30px; min-height: 24px; }} QCalendarWidget QToolButton:hover {{ background: {c['accent_primary']}25; color: {c['accent_primary']}; }} QCalendarWidget QToolButton#qt_calendar_prevmonth, QCalendarWidget QToolButton#qt_calendar_nextmonth {{ qproperty-icon: none; min-width: 36px; min-height: 28px; font-size: 14pt; font-weight: bold; color: {c['accent_primary']}; background: {c['bg_secondary']}; border: 1px solid {c['border_subtle']}; border-radius: 6px; padding: 2px 8px; }} QCalendarWidget QToolButton#qt_calendar_prevmonth {{ qproperty-text: "<"; }} QCalendarWidget QToolButton#qt_calendar_nextmonth {{ qproperty-text: ">"; }} QCalendarWidget QToolButton#qt_calendar_prevmonth:hover, QCalendarWidget QToolButton#qt_calendar_nextmonth:hover {{ background: {c['accent_primary']}; color: {c['text_inverse']}; border-color: {c['accent_primary']}; }} /* ════════════════════════════════════════ 메시지 박스 ════════════════════════════════════════ */ QMessageBox, QInputDialog {{ background: {c['bg_primary']}; }} QMessageBox QLabel, QInputDialog QLabel {{ color: {c['text_primary']}; font-size: 9.5pt; }} QDialogButtonBox QPushButton {{ min-width: 70px; }} /* ════════════════════════════════════════ 툴팁 ════════════════════════════════════════ */ QToolTip {{ background: {c['bg_secondary']}; color: {c['text_primary']}; border: 1px solid {c['border_default']}; border-radius: 4px; padding: 4px 8px; font-size: 9pt; }} /* ════════════════════════════════════════ 메뉴 ════════════════════════════════════════ */ QMenu {{ background: {c['bg_secondary']}; border: 1px solid {c['border_default']}; border-radius: 8px; padding: 4px; color: {c['text_primary']}; }} QMenu::item {{ padding: 6px 24px; border-radius: 4px; }} QMenu::item:selected {{ background: {c['accent_primary']}; color: {c['text_inverse']}; }} QMenu::separator {{ height: 1px; background: {c['border_subtle']}; margin: 4px 8px; }} QMenu::icon {{ padding-left: 8px; }} """ # ─── 편의 함수 ─────────────────────────────────────────────── def get_light_theme() -> str: return generate_theme(LIGHT_COLORS, is_dark=False) def get_dark_theme() -> str: return generate_theme(DARK_COLORS, is_dark=True) def get_theme(theme_name: str = 'light') -> str: ThemeColors.set_theme(theme_name) if theme_name == 'dark': return get_dark_theme() return get_light_theme() def apply_dark_titlebar(widget, dark: bool = None): """Windows 타이틀바 다크모드 적용 (다이얼로그용)""" if dark is None: dark = ThemeColors.current is DARK_COLORS try: import ctypes hwnd = int(widget.winId()) DWMWA_USE_IMMERSIVE_DARK_MODE = 20 value = ctypes.c_int(1 if dark else 0) ctypes.windll.dwmapi.DwmSetWindowAttribute( hwnd, DWMWA_USE_IMMERSIVE_DARK_MODE, ctypes.byref(value), ctypes.sizeof(value) ) except Exception: pass