Danil Iarosh 4ce9eb7689 Switch to standard OS windows, resizable dialogs, and channel-aware pixel test
- Удален кастомный безрамочный режим (FramelessWindowHint) главного окна и диалогов.
- Внедрено нативное управление окнами операционной системы (изменение размеров, максимизация, минимизация).
- Добавлено применение QPalette под выбранную тему (темную/свелую) и акцент для покраски системных элементов окон.
- Внутреннее содержимое диалогов (StyledDialog) обернуто в QScrollArea с автоскроллом и ограничением по высоте до 720px во избежание выхода окон за пределы экрана.
- TitleBar упрощен до простой верхней панели вкладок (sub-header/tabs).
- В диалог теста подсветки добавлен выбор номера ленты (1..4) при тесте пикселя виртуальной матрицы, обновлена логика генерации и отправки тестовой команды.
- Раздел "Бродяга морей" переименован в "Бродяги морей".
2026-07-01 14:58:11 +03:00

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)