- Удален кастомный безрамочный режим (FramelessWindowHint) главного окна и диалогов. - Внедрено нативное управление окнами операционной системы (изменение размеров, максимизация, минимизация). - Добавлено применение QPalette под выбранную тему (темную/свелую) и акцент для покраски системных элементов окон. - Внутреннее содержимое диалогов (StyledDialog) обернуто в QScrollArea с автоскроллом и ограничением по высоте до 720px во избежание выхода окон за пределы экрана. - TitleBar упрощен до простой верхней панели вкладок (sub-header/tabs). - В диалог теста подсветки добавлен выбор номера ленты (1..4) при тесте пикселя виртуальной матрицы, обновлена логика генерации и отправки тестовой команды. - Раздел "Бродяга морей" переименован в "Бродяги морей".
865 lines
23 KiB
Python
865 lines
23 KiB
Python
"""Parametrised application stylesheet.
|
|
|
|
Supports 4 theme variants: light/dark x green/blue.
|
|
build_stylesheet(palette) renders the QSS template with the given palette.
|
|
APP_STYLESHEET is the default (light-green) for backward compatibility.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass, asdict
|
|
import os
|
|
import tempfile
|
|
|
|
from PySide6.QtGui import QColor, QPalette
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class ThemePalette:
|
|
# backgrounds
|
|
bg_base: str
|
|
bg_card: str
|
|
bg_input: str
|
|
bg_dialog: str
|
|
bg_tooltip: str
|
|
|
|
# text
|
|
text_primary: str
|
|
text_secondary: str
|
|
text_muted: str
|
|
text_on_accent: str
|
|
|
|
# borders
|
|
border: str
|
|
border_light: str
|
|
|
|
# accent family
|
|
accent: str
|
|
accent_dark: str
|
|
accent_darker: str
|
|
accent_border: str
|
|
accent_bg_subtle: str
|
|
accent_text: str
|
|
|
|
# generic buttons
|
|
btn_bg: str
|
|
btn_border: str
|
|
btn_hover: str
|
|
btn_pressed: str
|
|
|
|
# primary buttons
|
|
btn_primary_bg: str
|
|
btn_primary_hover: str
|
|
btn_primary_pressed: str
|
|
|
|
# title-tabs
|
|
tab_bg: str
|
|
tab_text: str
|
|
tab_border: str
|
|
tab_selected_bg: str
|
|
tab_selected_text: str
|
|
tab_hover_bg: str
|
|
|
|
# content tabs
|
|
ctab_pane_border: str
|
|
ctab_pane_bg: str
|
|
ctab_bg: str
|
|
ctab_text: str
|
|
ctab_border: str
|
|
ctab_selected_bg: str
|
|
ctab_selected_text: str
|
|
ctab_hover_bg: str
|
|
ctab_close_bg: str
|
|
ctab_close_text: str
|
|
ctab_close_hover_bg: str
|
|
ctab_close_hover_text: str
|
|
|
|
# status badges
|
|
status_ready_border: str
|
|
status_ready_bg: str
|
|
status_ready_text: str
|
|
status_warn_border: str
|
|
status_warn_bg: str
|
|
status_warn_text: str
|
|
status_error_border: str
|
|
status_error_bg: str
|
|
status_error_text: str
|
|
|
|
# input indicator
|
|
indicator_bg: str
|
|
indicator_border: str
|
|
indicator_text: str
|
|
indicator_active_bg: str
|
|
indicator_active_border: str
|
|
indicator_active_text: str
|
|
|
|
# hero badge
|
|
hero_badge_color: str
|
|
hero_badge_bg: str
|
|
hero_badge_border: str
|
|
|
|
# icon colours (for qtawesome icons on regular buttons)
|
|
icon_default: str
|
|
icon_muted: str
|
|
|
|
|
|
_LIGHT_GREEN = ThemePalette(
|
|
bg_base="#e9edf2", bg_card="#ffffff", bg_input="#fdfefe",
|
|
bg_dialog="#f8fafc", bg_tooltip="#1e293b",
|
|
text_primary="#1f2937", text_secondary="#334155",
|
|
text_muted="#64748b", text_on_accent="#ffffff",
|
|
border="#cad3df", border_light="#d7e0eb",
|
|
accent="#16a34a", accent_dark="#15803d", accent_darker="#166534",
|
|
accent_border="#1d7a45", accent_bg_subtle="#dcfce7", accent_text="#166534",
|
|
btn_bg="#ecf2f9", btn_border="#c7d4e3", btn_hover="#dce8f5", btn_pressed="#c8d9eb",
|
|
btn_primary_bg="#0f766e", btn_primary_hover="#115e59", btn_primary_pressed="#134e4a",
|
|
tab_bg="#166534", tab_text="#ecfdf5", tab_border="#1d7a45",
|
|
tab_selected_bg="#f8fafc", tab_selected_text="#166534", tab_hover_bg="#1e6f3f",
|
|
ctab_pane_border="#cad3df", ctab_pane_bg="#eef2f6",
|
|
ctab_bg="#e8eef5", ctab_text="#334155", ctab_border="#c7d2df",
|
|
ctab_selected_bg="#ffffff", ctab_selected_text="#0f172a", ctab_hover_bg="#f3f7fb",
|
|
ctab_close_bg="#dcfce7", ctab_close_text="#166534",
|
|
ctab_close_hover_bg="#fee2e2", ctab_close_hover_text="#b91c1c",
|
|
status_ready_border="#86efac", status_ready_bg="#dcfce7", status_ready_text="#166534",
|
|
status_warn_border="#f4d8a8", status_warn_bg="#fff7e6", status_warn_text="#8a5a00",
|
|
status_error_border="#fca5a5", status_error_bg="#fef2f2", status_error_text="#991b1b",
|
|
indicator_bg="#e2e8f0", indicator_border="#cbd5e1", indicator_text="#64748b",
|
|
indicator_active_bg="#22c55e", indicator_active_border="#16a34a", indicator_active_text="#ffffff",
|
|
hero_badge_color="#0f766e", hero_badge_bg="#d1fae5", hero_badge_border="#99f6e4",
|
|
icon_default="#1f2937", icon_muted="#64748b",
|
|
)
|
|
|
|
_LIGHT_BLUE = ThemePalette(
|
|
bg_base="#e9edf2", bg_card="#ffffff", bg_input="#fdfefe",
|
|
bg_dialog="#f8fafc", bg_tooltip="#1e293b",
|
|
text_primary="#1f2937", text_secondary="#334155",
|
|
text_muted="#64748b", text_on_accent="#ffffff",
|
|
border="#cad3df", border_light="#d7e0eb",
|
|
accent="#2563eb", accent_dark="#1d4ed8", accent_darker="#1e40af",
|
|
accent_border="#3b82f6", accent_bg_subtle="#dbeafe", accent_text="#1e40af",
|
|
btn_bg="#ecf2f9", btn_border="#c7d4e3", btn_hover="#dce8f5", btn_pressed="#c8d9eb",
|
|
btn_primary_bg="#2563eb", btn_primary_hover="#1d4ed8", btn_primary_pressed="#1e40af",
|
|
tab_bg="#1e40af", tab_text="#dbeafe", tab_border="#3b82f6",
|
|
tab_selected_bg="#f8fafc", tab_selected_text="#1e40af", tab_hover_bg="#1e3a8a",
|
|
ctab_pane_border="#cad3df", ctab_pane_bg="#eef2f6",
|
|
ctab_bg="#e8eef5", ctab_text="#334155", ctab_border="#c7d2df",
|
|
ctab_selected_bg="#ffffff", ctab_selected_text="#0f172a", ctab_hover_bg="#f3f7fb",
|
|
ctab_close_bg="#dbeafe", ctab_close_text="#1e40af",
|
|
ctab_close_hover_bg="#fee2e2", ctab_close_hover_text="#b91c1c",
|
|
status_ready_border="#86efac", status_ready_bg="#dcfce7", status_ready_text="#166534",
|
|
status_warn_border="#f4d8a8", status_warn_bg="#fff7e6", status_warn_text="#8a5a00",
|
|
status_error_border="#fca5a5", status_error_bg="#fef2f2", status_error_text="#991b1b",
|
|
indicator_bg="#e2e8f0", indicator_border="#cbd5e1", indicator_text="#64748b",
|
|
indicator_active_bg="#3b82f6", indicator_active_border="#2563eb", indicator_active_text="#ffffff",
|
|
hero_badge_color="#1d4ed8", hero_badge_bg="#dbeafe", hero_badge_border="#93c5fd",
|
|
icon_default="#1f2937", icon_muted="#64748b",
|
|
)
|
|
|
|
_DARK_GREEN = ThemePalette(
|
|
bg_base="#1e1e2e", bg_card="#2a2a3c", bg_input="#33334a",
|
|
bg_dialog="#252538", bg_tooltip="#f8fafc",
|
|
text_primary="#e2e8f0", text_secondary="#cbd5e1",
|
|
text_muted="#94a3b8", text_on_accent="#ffffff",
|
|
border="#3f3f5a", border_light="#4a4a66",
|
|
accent="#22c55e", accent_dark="#16a34a", accent_darker="#15803d",
|
|
accent_border="#22c55e", accent_bg_subtle="#16382a", accent_text="#4ade80",
|
|
btn_bg="#33334a", btn_border="#4a4a66", btn_hover="#3d3d56", btn_pressed="#4a4a66",
|
|
btn_primary_bg="#16a34a", btn_primary_hover="#15803d", btn_primary_pressed="#166534",
|
|
tab_bg="#15803d", tab_text="#d1fae5", tab_border="#22c55e",
|
|
tab_selected_bg="#e2e8f0", tab_selected_text="#15803d", tab_hover_bg="#166534",
|
|
ctab_pane_border="#3f3f5a", ctab_pane_bg="#252538",
|
|
ctab_bg="#2a2a3c", ctab_text="#cbd5e1", ctab_border="#3f3f5a",
|
|
ctab_selected_bg="#33334a", ctab_selected_text="#e2e8f0", ctab_hover_bg="#33334a",
|
|
ctab_close_bg="#16382a", ctab_close_text="#4ade80",
|
|
ctab_close_hover_bg="#4c1d1d", ctab_close_hover_text="#fca5a5",
|
|
status_ready_border="#22c55e", status_ready_bg="#16382a", status_ready_text="#4ade80",
|
|
status_warn_border="#ca8a04", status_warn_bg="#3b2f09", status_warn_text="#facc15",
|
|
status_error_border="#ef4444", status_error_bg="#4c1d1d", status_error_text="#fca5a5",
|
|
indicator_bg="#33334a", indicator_border="#4a4a66", indicator_text="#94a3b8",
|
|
indicator_active_bg="#22c55e", indicator_active_border="#16a34a", indicator_active_text="#ffffff",
|
|
hero_badge_color="#4ade80", hero_badge_bg="#16382a", hero_badge_border="#22c55e",
|
|
icon_default="#e2e8f0", icon_muted="#94a3b8",
|
|
)
|
|
|
|
_DARK_BLUE = ThemePalette(
|
|
bg_base="#1e1e2e", bg_card="#2a2a3c", bg_input="#33334a",
|
|
bg_dialog="#252538", bg_tooltip="#f8fafc",
|
|
text_primary="#e2e8f0", text_secondary="#cbd5e1",
|
|
text_muted="#94a3b8", text_on_accent="#ffffff",
|
|
border="#3f3f5a", border_light="#4a4a66",
|
|
accent="#3b82f6", accent_dark="#2563eb", accent_darker="#1d4ed8",
|
|
accent_border="#3b82f6", accent_bg_subtle="#1e2a4a", accent_text="#60a5fa",
|
|
btn_bg="#33334a", btn_border="#4a4a66", btn_hover="#3d3d56", btn_pressed="#4a4a66",
|
|
btn_primary_bg="#2563eb", btn_primary_hover="#1d4ed8", btn_primary_pressed="#1e40af",
|
|
tab_bg="#1d4ed8", tab_text="#dbeafe", tab_border="#3b82f6",
|
|
tab_selected_bg="#e2e8f0", tab_selected_text="#1d4ed8", tab_hover_bg="#1e40af",
|
|
ctab_pane_border="#3f3f5a", ctab_pane_bg="#252538",
|
|
ctab_bg="#2a2a3c", ctab_text="#cbd5e1", ctab_border="#3f3f5a",
|
|
ctab_selected_bg="#33334a", ctab_selected_text="#e2e8f0", ctab_hover_bg="#33334a",
|
|
ctab_close_bg="#1e2a4a", ctab_close_text="#60a5fa",
|
|
ctab_close_hover_bg="#4c1d1d", ctab_close_hover_text="#fca5a5",
|
|
status_ready_border="#22c55e", status_ready_bg="#16382a", status_ready_text="#4ade80",
|
|
status_warn_border="#ca8a04", status_warn_bg="#3b2f09", status_warn_text="#facc15",
|
|
status_error_border="#ef4444", status_error_bg="#4c1d1d", status_error_text="#fca5a5",
|
|
indicator_bg="#33334a", indicator_border="#4a4a66", indicator_text="#94a3b8",
|
|
indicator_active_bg="#3b82f6", indicator_active_border="#2563eb", indicator_active_text="#ffffff",
|
|
hero_badge_color="#60a5fa", hero_badge_bg="#1e2a4a", hero_badge_border="#3b82f6",
|
|
icon_default="#e2e8f0", icon_muted="#94a3b8",
|
|
)
|
|
|
|
PALETTES: dict[tuple[str, str], ThemePalette] = {
|
|
("light", "green"): _LIGHT_GREEN,
|
|
("light", "blue"): _LIGHT_BLUE,
|
|
("dark", "green"): _DARK_GREEN,
|
|
("dark", "blue"): _DARK_BLUE,
|
|
}
|
|
|
|
|
|
def get_palette(mode: str = "light", accent: str = "green") -> ThemePalette:
|
|
return PALETTES.get((mode, accent), _LIGHT_GREEN)
|
|
|
|
|
|
_QSS_TEMPLATE = """
|
|
QMainWindow#AppWindow {{
|
|
background: {bg_base};
|
|
}}
|
|
|
|
QWidget#MainContainer {{
|
|
background: {bg_base};
|
|
border: none;
|
|
}}
|
|
|
|
QFrame#ContentArea {{
|
|
background: {bg_base};
|
|
border: none;
|
|
}}
|
|
|
|
QStackedWidget {{
|
|
background: {bg_base};
|
|
}}
|
|
|
|
QScrollArea {{
|
|
background: {bg_base};
|
|
}}
|
|
|
|
QScrollArea > QWidget > QWidget {{
|
|
background: {bg_base};
|
|
}}
|
|
|
|
QWidget#TitleBar {{
|
|
background: {accent};
|
|
border-bottom: 1px solid {accent};
|
|
}}
|
|
|
|
|
|
QLabel#TitleBarTitle {{
|
|
color: #f8fafc;
|
|
font-size: 13px;
|
|
font-weight: 600;
|
|
}}
|
|
|
|
QLabel#TitleBarIcon {{
|
|
background: transparent;
|
|
}}
|
|
|
|
QWidget#TitleTabsContainer {{
|
|
background: transparent;
|
|
}}
|
|
|
|
QTabBar#TitleTabs {{
|
|
background: transparent;
|
|
}}
|
|
|
|
QTabBar#TitleTabs::tab {{
|
|
background: {tab_bg};
|
|
color: {tab_text};
|
|
border: 1px solid {tab_border};
|
|
border-radius: 7px;
|
|
padding: 4px 20px 4px 10px;
|
|
margin-right: 4px;
|
|
min-height: 18px;
|
|
}}
|
|
|
|
QTabBar#TitleTabs::tab:selected {{
|
|
background: {tab_selected_bg};
|
|
color: {tab_selected_text};
|
|
border-color: {accent_bg_subtle};
|
|
font-weight: 600;
|
|
}}
|
|
|
|
QTabBar#TitleTabs::tab:hover:!selected {{
|
|
background: {tab_hover_bg};
|
|
}}
|
|
|
|
QTabBar#TitleTabs QToolButton[tabClose="true"] {{
|
|
background: transparent;
|
|
border: none;
|
|
border-radius: 7px;
|
|
margin-right: 6px;
|
|
padding: 0px;
|
|
}}
|
|
|
|
QTabBar#TitleTabs QToolButton[tabClose="true"]:hover {{
|
|
background: rgba(255, 255, 255, 0.25);
|
|
}}
|
|
|
|
QWidget#TabStatusDot {{
|
|
border-radius: 4px;
|
|
min-width: 8px; min-height: 8px;
|
|
max-width: 8px; max-height: 8px;
|
|
}}
|
|
QWidget#TabStatusDot[dotColor="green"] {{ background: #22c55e; }}
|
|
QWidget#TabStatusDot[dotColor="yellow"] {{ background: #facc15; }}
|
|
QWidget#TabStatusDot[dotColor="red"] {{ background: #ef4444; }}
|
|
QWidget#TabStatusDot[dotColor="none"] {{ background: transparent; border: none; }}
|
|
|
|
QWidget#TabRightContainer {{
|
|
background: transparent;
|
|
}}
|
|
|
|
QToolButton[windowControl="minimize"],
|
|
QToolButton[windowControl="maximize"],
|
|
QToolButton[windowControl="close"] {{
|
|
background: rgba(255, 255, 255, 0.12);
|
|
color: #d8e4f1;
|
|
border: none;
|
|
border-radius: 6px;
|
|
}}
|
|
|
|
QToolButton[windowControl="minimize"]:hover,
|
|
QToolButton[windowControl="maximize"]:hover,
|
|
QToolButton[windowControl="close"]:hover {{
|
|
background: rgba(255, 255, 255, 0.25);
|
|
}}
|
|
|
|
QTabWidget::pane {{
|
|
border: 1px solid {ctab_pane_border};
|
|
border-radius: 8px;
|
|
top: -1px;
|
|
background: {ctab_pane_bg};
|
|
}}
|
|
|
|
QTabBar::tab {{
|
|
background: {ctab_bg};
|
|
color: {ctab_text};
|
|
border: 1px solid {ctab_border};
|
|
border-bottom: none;
|
|
padding: 6px 12px;
|
|
margin-right: 2px;
|
|
min-height: 25px;
|
|
border-top-left-radius: 8px;
|
|
border-top-right-radius: 8px;
|
|
}}
|
|
|
|
QTabBar::tab:selected {{
|
|
background: {ctab_selected_bg};
|
|
border-color: {ctab_pane_border};
|
|
color: {ctab_selected_text};
|
|
font-weight: 600;
|
|
}}
|
|
|
|
QTabBar::tab:hover:!selected {{
|
|
background: {ctab_hover_bg};
|
|
}}
|
|
|
|
QToolButton[tabClose="true"] {{
|
|
background: {ctab_close_bg};
|
|
color: {ctab_close_text};
|
|
border: none;
|
|
border-radius: 7px;
|
|
padding: 0px;
|
|
font-size: 11px;
|
|
font-weight: 600;
|
|
}}
|
|
|
|
QToolButton[tabClose="true"]:hover {{
|
|
background: {ctab_close_hover_bg};
|
|
color: {ctab_close_hover_text};
|
|
}}
|
|
|
|
QToolButton[tabClose="true"]:pressed {{
|
|
background: {ctab_close_hover_bg};
|
|
color: {ctab_close_hover_text};
|
|
}}
|
|
|
|
QFrame#HeroCard,
|
|
QFrame#PanelCard {{
|
|
background: {bg_card};
|
|
border: 1px solid {border_light};
|
|
border-radius: 14px;
|
|
}}
|
|
|
|
QLabel#HeroBadge {{
|
|
color: {hero_badge_color};
|
|
background: {hero_badge_bg};
|
|
border: 1px solid {hero_badge_border};
|
|
border-radius: 9px;
|
|
padding: 4px 10px;
|
|
font-weight: 600;
|
|
}}
|
|
|
|
QLabel#HeroTitle {{
|
|
color: {text_primary};
|
|
font-size: 22px;
|
|
font-weight: 700;
|
|
}}
|
|
|
|
QLabel#HeroHint {{
|
|
color: {text_muted};
|
|
font-size: 13px;
|
|
}}
|
|
|
|
QLabel#StatusBadge {{
|
|
border-radius: 10px;
|
|
border: 1px solid {status_error_border};
|
|
background: {status_error_bg};
|
|
color: {status_error_text};
|
|
padding: 6px 10px;
|
|
font-weight: 600;
|
|
}}
|
|
|
|
QLabel#StatusBadge[state="need-auth"] {{
|
|
border-color: {status_warn_border};
|
|
background: {status_warn_bg};
|
|
color: {status_warn_text};
|
|
}}
|
|
|
|
QLabel#StatusBadge[state="ready"] {{
|
|
border-color: {status_ready_border};
|
|
background: {status_ready_bg};
|
|
color: {status_ready_text};
|
|
}}
|
|
|
|
QPushButton#ReconnectBtn {{
|
|
background: transparent;
|
|
border: 1px solid {border};
|
|
border-radius: 6px;
|
|
padding: 4px;
|
|
}}
|
|
QPushButton#ReconnectBtn:hover {{
|
|
background: {btn_bg};
|
|
}}
|
|
QPushButton#ReconnectBtn:pressed {{
|
|
background: {border};
|
|
}}
|
|
|
|
QLabel#SectionTitle {{
|
|
color: {text_secondary};
|
|
font-size: 15px;
|
|
font-weight: 700;
|
|
}}
|
|
|
|
QGroupBox {{
|
|
border: 1px solid {border};
|
|
border-radius: 10px;
|
|
margin-top: 10px;
|
|
padding-top: 10px;
|
|
color: {text_secondary};
|
|
font-weight: 600;
|
|
}}
|
|
|
|
QGroupBox::title {{
|
|
subcontrol-origin: margin;
|
|
left: 10px;
|
|
padding: 0 6px;
|
|
color: {text_muted};
|
|
font-size: 12px;
|
|
}}
|
|
|
|
QLabel {{
|
|
color: {text_primary};
|
|
}}
|
|
|
|
QComboBox {{
|
|
background: {bg_input};
|
|
border: 1px solid {border};
|
|
border-radius: 8px;
|
|
padding: 6px 28px 6px 10px;
|
|
min-height: 18px;
|
|
color: {text_primary};
|
|
selection-background-color: {btn_primary_bg};
|
|
selection-color: {text_on_accent};
|
|
}}
|
|
|
|
QComboBox:hover {{
|
|
border-color: {text_muted};
|
|
}}
|
|
|
|
QComboBox:focus {{
|
|
border-color: {btn_primary_bg};
|
|
}}
|
|
|
|
QComboBox::drop-down {{
|
|
subcontrol-origin: padding;
|
|
subcontrol-position: center right;
|
|
width: 24px;
|
|
border: none;
|
|
border-left: 1px solid {border};
|
|
border-top-right-radius: 8px;
|
|
border-bottom-right-radius: 8px;
|
|
background: {btn_bg};
|
|
}}
|
|
|
|
QComboBox::drop-down:hover {{
|
|
background: {btn_hover};
|
|
}}
|
|
|
|
QComboBox::down-arrow {{
|
|
image: url("{_arrow_down_svg}");
|
|
width: 10px;
|
|
height: 10px;
|
|
}}
|
|
|
|
QComboBox::down-arrow:on {{
|
|
image: url("{_arrow_up_svg}");
|
|
}}
|
|
|
|
QComboBox QAbstractItemView {{
|
|
background: {bg_card};
|
|
color: {text_primary};
|
|
border: 1px solid {border};
|
|
selection-background-color: {btn_primary_bg};
|
|
selection-color: {text_on_accent};
|
|
outline: 0;
|
|
}}
|
|
|
|
QPushButton {{
|
|
background: {btn_bg};
|
|
border: 1px solid {btn_border};
|
|
border-radius: 9px;
|
|
color: {text_primary};
|
|
padding: 7px 12px;
|
|
font-weight: 600;
|
|
}}
|
|
|
|
QPushButton:hover {{
|
|
background: {btn_hover};
|
|
}}
|
|
|
|
QPushButton:pressed {{
|
|
background: {btn_pressed};
|
|
}}
|
|
|
|
QPushButton[role="primary"] {{
|
|
background: {btn_primary_bg};
|
|
border-color: {btn_primary_bg};
|
|
color: {text_on_accent};
|
|
}}
|
|
|
|
QPushButton[role="primary"]:hover {{
|
|
background: {btn_primary_hover};
|
|
}}
|
|
|
|
QPushButton[role="primary"]:pressed {{
|
|
background: {btn_primary_pressed};
|
|
}}
|
|
|
|
QPushButton[role="ghost"] {{
|
|
background: {bg_card};
|
|
border-color: {border};
|
|
color: {text_muted};
|
|
}}
|
|
|
|
QMessageBox, QDialog {{
|
|
background: {bg_dialog};
|
|
color: {text_primary};
|
|
}}
|
|
|
|
QMessageBox QLabel {{
|
|
color: {text_primary};
|
|
font-size: 13px;
|
|
}}
|
|
|
|
QMessageBox QPushButton {{
|
|
background: {btn_bg};
|
|
border: 1px solid {btn_border};
|
|
border-radius: 9px;
|
|
color: {text_primary};
|
|
padding: 7px 18px;
|
|
font-weight: 600;
|
|
min-width: 70px;
|
|
}}
|
|
|
|
QMessageBox QPushButton:hover {{
|
|
background: {btn_hover};
|
|
}}
|
|
|
|
QMessageBox QPushButton:pressed {{
|
|
background: {btn_pressed};
|
|
}}
|
|
|
|
QLabel#InputIndicator {{
|
|
background: {indicator_bg};
|
|
border: 1px solid {indicator_border};
|
|
border-radius: 6px;
|
|
color: {indicator_text};
|
|
font-size: 11px;
|
|
font-weight: 600;
|
|
}}
|
|
|
|
QLabel#InputIndicator[active="true"] {{
|
|
background: {indicator_active_bg};
|
|
border-color: {indicator_active_border};
|
|
color: {indicator_active_text};
|
|
}}
|
|
|
|
QLabel#LogLabel {{
|
|
color: {text_muted};
|
|
font-size: 12px;
|
|
padding: 4px;
|
|
}}
|
|
|
|
QSpinBox {{
|
|
background: {bg_input};
|
|
border: 1px solid {border};
|
|
border-radius: 6px;
|
|
padding: 4px 22px 4px 6px;
|
|
color: {text_primary};
|
|
min-height: 18px;
|
|
}}
|
|
|
|
QSpinBox:focus {{
|
|
border-color: {btn_primary_bg};
|
|
}}
|
|
|
|
QSpinBox::up-button {{
|
|
subcontrol-origin: border;
|
|
subcontrol-position: top right;
|
|
width: 20px;
|
|
border: none;
|
|
border-left: 1px solid {border};
|
|
border-top-right-radius: 6px;
|
|
background: {btn_bg};
|
|
}}
|
|
|
|
QSpinBox::up-button:hover {{
|
|
background: {btn_hover};
|
|
}}
|
|
|
|
QSpinBox::up-button:pressed {{
|
|
background: {btn_pressed};
|
|
}}
|
|
|
|
QSpinBox::up-arrow {{
|
|
image: url("{_arrow_up_svg}");
|
|
width: 8px;
|
|
height: 8px;
|
|
}}
|
|
|
|
QSpinBox::down-button {{
|
|
subcontrol-origin: border;
|
|
subcontrol-position: bottom right;
|
|
width: 20px;
|
|
border: none;
|
|
border-left: 1px solid {border};
|
|
border-bottom-right-radius: 6px;
|
|
background: {btn_bg};
|
|
}}
|
|
|
|
QSpinBox::down-button:hover {{
|
|
background: {btn_hover};
|
|
}}
|
|
|
|
QSpinBox::down-button:pressed {{
|
|
background: {btn_pressed};
|
|
}}
|
|
|
|
QSpinBox::down-arrow {{
|
|
image: url("{_arrow_down_svg}");
|
|
width: 8px;
|
|
height: 8px;
|
|
}}
|
|
|
|
QToolButton#AccentSwatch {{
|
|
border: 2px solid {border};
|
|
border-radius: 16px;
|
|
min-width: 28px; max-width: 28px;
|
|
min-height: 28px; max-height: 28px;
|
|
}}
|
|
|
|
QToolButton#AccentSwatch:checked {{
|
|
border-color: {text_primary};
|
|
border-width: 3px;
|
|
}}
|
|
|
|
QPushButton:checked {{
|
|
background: {accent_bg_subtle};
|
|
border-color: {accent};
|
|
color: {accent_text};
|
|
}}
|
|
|
|
QTextEdit, QPlainTextEdit {{
|
|
background: {bg_input};
|
|
color: {text_primary};
|
|
border: 1px solid {border};
|
|
border-radius: 6px;
|
|
padding: 4px;
|
|
}}
|
|
|
|
QLineEdit {{
|
|
background: {bg_input};
|
|
color: {text_primary};
|
|
border: 1px solid {border};
|
|
border-radius: 8px;
|
|
padding: 6px 10px;
|
|
min-height: 18px;
|
|
}}
|
|
|
|
QLineEdit:focus {{
|
|
border-color: {btn_primary_bg};
|
|
}}
|
|
|
|
QProgressBar {{
|
|
background: {bg_input};
|
|
border: 1px solid {border};
|
|
border-radius: 6px;
|
|
text-align: center;
|
|
color: {text_primary};
|
|
min-height: 16px;
|
|
}}
|
|
|
|
QProgressBar::chunk {{
|
|
background: {accent};
|
|
border-radius: 5px;
|
|
}}
|
|
|
|
QScrollBar:vertical {{
|
|
background: transparent;
|
|
width: 8px;
|
|
margin: 0;
|
|
}}
|
|
|
|
QScrollBar::handle:vertical {{
|
|
background: {border};
|
|
border-radius: 4px;
|
|
min-height: 24px;
|
|
}}
|
|
|
|
QScrollBar::handle:vertical:hover {{
|
|
background: {text_muted};
|
|
}}
|
|
|
|
QScrollBar::add-line:vertical,
|
|
QScrollBar::sub-line:vertical {{
|
|
height: 0;
|
|
}}
|
|
|
|
QScrollBar:horizontal {{
|
|
background: transparent;
|
|
height: 8px;
|
|
margin: 0;
|
|
}}
|
|
|
|
QScrollBar::handle:horizontal {{
|
|
background: {border};
|
|
border-radius: 4px;
|
|
min-width: 24px;
|
|
}}
|
|
|
|
QScrollBar::handle:horizontal:hover {{
|
|
background: {text_muted};
|
|
}}
|
|
|
|
QScrollBar::add-line:horizontal,
|
|
QScrollBar::sub-line:horizontal {{
|
|
width: 0;
|
|
}}
|
|
|
|
/* ── Checkbox ─────────────────────────────────── */
|
|
QCheckBox {{
|
|
color: {text_primary};
|
|
spacing: 6px;
|
|
}}
|
|
|
|
/* ── PRO-mode toggle ──────────────────────────── */
|
|
QCheckBox#ProToggle {{
|
|
color: {text_muted};
|
|
font-size: 12px;
|
|
spacing: 6px;
|
|
}}
|
|
QCheckBox#ProToggle::indicator {{
|
|
width: 36px;
|
|
height: 20px;
|
|
border-radius: 10px;
|
|
background: {border};
|
|
border: 1px solid {border_light};
|
|
}}
|
|
QCheckBox#ProToggle::indicator:checked {{
|
|
background: {accent};
|
|
border-color: {accent_dark};
|
|
}}
|
|
|
|
/* ── Collapsible section header ───────────────── */
|
|
QPushButton#CollapsibleHeader {{
|
|
background: {btn_bg};
|
|
border: 1px solid {border};
|
|
border-radius: 8px;
|
|
color: {text_primary};
|
|
padding: 8px 12px;
|
|
font-weight: 700;
|
|
font-size: 13px;
|
|
text-align: left;
|
|
}}
|
|
QPushButton#CollapsibleHeader:hover {{
|
|
background: {btn_hover};
|
|
border-color: {text_muted};
|
|
}}
|
|
QPushButton#CollapsibleHeader:pressed {{
|
|
background: {btn_pressed};
|
|
}}
|
|
"""
|
|
|
|
|
|
_ARROW_SVG_DIR: str | None = None
|
|
|
|
|
|
def _arrow_svg_path(name: str, color: str, points: str) -> str:
|
|
"""Создать SVG-файл стрелки и вернуть путь (forward-slash для QSS)."""
|
|
global _ARROW_SVG_DIR # noqa: PLW0603
|
|
if _ARROW_SVG_DIR is None:
|
|
_ARROW_SVG_DIR = tempfile.mkdtemp(prefix="plate_arrows_")
|
|
# Уникальное имя: name + hex-цвет без #
|
|
fname = f"{name}_{color.lstrip('#')}.svg"
|
|
fpath = os.path.join(_ARROW_SVG_DIR, fname)
|
|
if not os.path.exists(fpath):
|
|
svg = (
|
|
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 12 12">'
|
|
f'<polyline points="{points}" fill="none" '
|
|
f'stroke="{color}" stroke-width="2" '
|
|
'stroke-linecap="round" stroke-linejoin="round"/>'
|
|
'</svg>'
|
|
)
|
|
with open(fpath, "w", encoding="utf-8") as f:
|
|
f.write(svg)
|
|
return fpath.replace("\\", "/")
|
|
|
|
|
|
def create_qpalette(palette: ThemePalette) -> QPalette:
|
|
qpal = QPalette()
|
|
qpal.setColor(QPalette.Window, QColor(palette.bg_base))
|
|
qpal.setColor(QPalette.WindowText, QColor(palette.text_primary))
|
|
qpal.setColor(QPalette.Base, QColor(palette.bg_card))
|
|
qpal.setColor(QPalette.AlternateBase, QColor(palette.bg_input))
|
|
qpal.setColor(QPalette.ToolTipBase, QColor(palette.bg_tooltip))
|
|
qpal.setColor(QPalette.ToolTipText, QColor(
|
|
palette.text_on_accent if "#1e" in palette.bg_tooltip or "#0" in palette.bg_tooltip else palette.text_primary
|
|
))
|
|
qpal.setColor(QPalette.Text, QColor(palette.text_primary))
|
|
qpal.setColor(QPalette.Button, QColor(palette.btn_bg))
|
|
qpal.setColor(QPalette.ButtonText, QColor(palette.text_primary))
|
|
qpal.setColor(QPalette.Highlight, QColor(palette.accent))
|
|
qpal.setColor(QPalette.HighlightedText, QColor(palette.text_on_accent))
|
|
return qpal
|
|
|
|
|
|
def build_stylesheet(palette: ThemePalette) -> str:
|
|
d = asdict(palette)
|
|
# Генерируем SVG-стрелки
|
|
muted = d["text_muted"]
|
|
d["_arrow_down_svg"] = _arrow_svg_path("arrow_down", muted, "2,4 6,8 10,4")
|
|
d["_arrow_up_svg"] = _arrow_svg_path("arrow_up", muted, "2,8 6,4 10,8")
|
|
return _QSS_TEMPLATE.format(**d)
|
|
|
|
|
|
APP_STYLESHEET = build_stylesheet(_LIGHT_GREEN)
|