- 모던 다크 미니멀 테마(NanumSquare 번들, 단일 accent #4DABF7, 8px radius, flat 버튼, 다크 기본값) - 라인 아이콘 시스템(ui/icons.py, QtSvg) — 앱 전반 이모지 교체 - 다크 깨짐 수정: 테이블 헤더/코너 흰색, 도움말 탭 흰 라인, 트레이/미니위젯 메뉴 - fix: 자동 적립 OFF가 자동 퇴근 경로에서 무시되던 버그(게이팅) - feat: 연장근무 적립 기록 삭제(우클릭) - 테스트 3건 추가 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1036 lines
28 KiB
Python
1036 lines
28 KiB
Python
"""
|
|
테마 시스템 - 라이트/다크 테마 지원
|
|
"""
|
|
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
|
|
|
|
|