LED-mapper/theme.py
Danil Iarosh d8c95a15e2 feat: Improve editor overlays and stability
- Добавлена подложка с видимостью, прозрачностью, масштабом, перемещением и редактированием через отдельное меню.

- Добавлен инструмент создания свободных светодиодов и редактирование X/Y для вручную созданных точек в таблице.

- Упрощена таблица выбранных LED: единый ID, координаты X/Y и UID в конце.

- Добавлены настраиваемые горячие клавиши для всех действий и переключатель отображения id светодиодов.

- Улучшена отрисовка линий: стрелки направления, изгибы при полном и частичном визуальном наложении сегментов.

- Оптимизировано переключение активных линий, выбор точек и поиск наложений соединений для снижения подвисаний.

- Исправлена логика ручных LED: row/col рассчитываются по сетке 34 мм, а ручные точки не сдвигают якорь сетки.

- Обновлена сборка EXE: имя использует тег, короткий хэш или дату/время для dirty-репозитория.

- Расширен smoke-набор до 113 проверок для новых функций и регрессий.
2026-05-25 13:39:59 +03:00

600 lines
18 KiB
Python

from __future__ import annotations
import sys
from pathlib import Path
from PySide6.QtCore import Qt
from PySide6.QtGui import QColor, QPalette
from PySide6.QtWidgets import QApplication
ASSET_DIR = Path(__file__).resolve().parent / "assets"
def asset_url(name: str) -> str:
return ASSET_DIR.joinpath(name).as_posix()
def apply_app_theme(theme: str) -> None:
app = QApplication.instance()
if app is None:
return
effective = effective_theme(theme)
app.setStyle("Fusion")
if effective == "dark":
app.setPalette(dark_palette())
app.setStyleSheet(dark_stylesheet())
else:
app.setPalette(light_palette())
app.setStyleSheet(light_stylesheet())
def effective_theme(theme: str) -> str:
if theme == "dark":
return "dark"
if theme == "light":
return "light"
return system_theme()
def system_theme() -> str:
app = QApplication.instance()
if app is not None:
try:
color_scheme = app.styleHints().colorScheme()
if color_scheme == Qt.ColorScheme.Dark:
return "dark"
if color_scheme == Qt.ColorScheme.Light:
return "light"
except (AttributeError, TypeError):
pass
if sys.platform.startswith("win"):
try:
import winreg
with winreg.OpenKey(
winreg.HKEY_CURRENT_USER,
r"Software\Microsoft\Windows\CurrentVersion\Themes\Personalize",
) as key:
apps_use_light_theme, _ = winreg.QueryValueEx(key, "AppsUseLightTheme")
return "light" if int(apps_use_light_theme) else "dark"
except OSError:
pass
if app is not None:
return "dark" if app.style().standardPalette().color(QPalette.Window).lightness() < 128 else "light"
return "light"
def light_palette() -> QPalette:
palette = QPalette()
palette.setColor(QPalette.Window, QColor("#f8fafc"))
palette.setColor(QPalette.WindowText, QColor("#111827"))
palette.setColor(QPalette.Base, QColor("#ffffff"))
palette.setColor(QPalette.AlternateBase, QColor("#f1f5f9"))
palette.setColor(QPalette.ToolTipBase, QColor("#ffffff"))
palette.setColor(QPalette.ToolTipText, QColor("#111827"))
palette.setColor(QPalette.Text, QColor("#111827"))
palette.setColor(QPalette.Button, QColor("#e5e7eb"))
palette.setColor(QPalette.ButtonText, QColor("#111827"))
palette.setColor(QPalette.Highlight, QColor("#c084fc"))
palette.setColor(QPalette.HighlightedText, QColor("#111827"))
return palette
def dark_palette() -> QPalette:
palette = QPalette()
palette.setColor(QPalette.Window, QColor("#1f1f1f"))
palette.setColor(QPalette.WindowText, QColor("#f8fafc"))
palette.setColor(QPalette.Base, QColor("#2b2b2b"))
palette.setColor(QPalette.AlternateBase, QColor("#353535"))
palette.setColor(QPalette.ToolTipBase, QColor("#111827"))
palette.setColor(QPalette.ToolTipText, QColor("#f8fafc"))
palette.setColor(QPalette.Text, QColor("#f8fafc"))
palette.setColor(QPalette.Button, QColor("#2f2f2f"))
palette.setColor(QPalette.ButtonText, QColor("#f8fafc"))
palette.setColor(QPalette.Highlight, QColor("#c084fc"))
palette.setColor(QPalette.HighlightedText, QColor("#111827"))
return palette
def light_stylesheet() -> str:
return """
QWidget {
background-color: #f8fafc;
color: #111827;
selection-background-color: #93c5fd;
selection-color: #111827;
}
QGraphicsView {
background-color: #ffffff;
border: none;
}
QMenuBar {
background-color: #f1f5f9;
color: #111827;
}
QMenuBar::item {
background: transparent;
padding: 5px 10px;
}
QMenuBar::item:selected {
background-color: #e2e8f0;
}
QMenu {
background-color: #ffffff;
color: #111827;
border: 1px solid #cbd5e1;
}
QMenu::item {
padding: 5px 24px 5px 24px;
}
QMenu::item:selected {
background-color: #dbeafe;
}
QMenu::item:disabled {
color: #94a3b8;
background-color: transparent;
}
QMenu::item:selected:disabled {
color: #94a3b8;
background-color: transparent;
}
QPushButton, QToolButton {
background-color: #e5e7eb;
color: #111827;
border: 1px solid #cbd5e1;
border-radius: 4px;
padding: 4px 8px;
}
QPushButton:hover, QToolButton:hover {
background-color: #dbeafe;
}
QPushButton:disabled, QToolButton:disabled {
background-color: #f1f5f9;
color: #94a3b8;
border-color: #e2e8f0;
}
QTreeWidget, QTableWidget, QListWidget, QLineEdit, QComboBox, QSpinBox {
background-color: #ffffff;
color: #111827;
alternate-background-color: #f1f5f9;
border: 1px solid #cbd5e1;
border-radius: 4px;
min-height: 24px;
selection-background-color: #bfdbfe;
selection-color: #111827;
}
QLineEdit, QComboBox, QSpinBox {
padding: 2px 8px;
}
QComboBox {
padding-right: 28px;
}
QComboBox:hover, QSpinBox:hover, QLineEdit:hover {
border-color: #94a3b8;
}
QComboBox:focus, QSpinBox:focus, QLineEdit:focus {
border-color: #60a5fa;
}
QComboBox::drop-down {
subcontrol-origin: padding;
subcontrol-position: top right;
width: 24px;
border-left: 1px solid #cbd5e1;
border-top-right-radius: 4px;
border-bottom-right-radius: 4px;
background-color: #f1f5f9;
}
QComboBox::drop-down:hover {
background-color: #e2e8f0;
}
QComboBox::down-arrow {
image: url("__INPUT_DOWN_ARROW__");
width: 10px;
height: 10px;
}
QSpinBox {
padding-right: 28px;
}
QSpinBox::up-button, QSpinBox::down-button {
subcontrol-origin: border;
width: 24px;
border-left: 1px solid #cbd5e1;
background-color: #f1f5f9;
}
QSpinBox::up-button {
subcontrol-position: top right;
border-top-right-radius: 4px;
}
QSpinBox::down-button {
subcontrol-position: bottom right;
border-bottom-right-radius: 4px;
}
QSpinBox::up-button:hover, QSpinBox::down-button:hover {
background-color: #e2e8f0;
}
QSpinBox::up-arrow {
image: url("__INPUT_UP_ARROW__");
width: 10px;
height: 10px;
}
QSpinBox::down-arrow {
image: url("__INPUT_DOWN_ARROW__");
width: 10px;
height: 10px;
}
QComboBox QAbstractItemView {
background-color: #ffffff;
color: #111827;
border: 1px solid #cbd5e1;
selection-background-color: #bfdbfe;
selection-color: #111827;
}
QHeaderView::section {
background-color: #e5e7eb;
color: #111827;
border: 1px solid #cbd5e1;
padding: 4px;
}
QSplitter::handle {
background-color: #e2e8f0;
}
QStatusBar {
background-color: #f1f5f9;
color: #111827;
}
QToolTip {
background-color: #ffffff;
color: #111827;
border: 1px solid #cbd5e1;
}
QScrollBar:vertical {
background: #f1f5f9;
width: 12px;
margin: 12px 0 12px 0;
border: none;
}
QScrollBar::handle:vertical {
background: #94a3b8;
min-height: 28px;
margin: 0 4px;
border-radius: 2px;
}
QScrollBar::handle:vertical:hover {
background: #64748b;
margin: 0 2px;
border-radius: 4px;
}
QScrollBar::add-page:vertical,
QScrollBar::sub-page:vertical {
background: transparent;
border: none;
height: 0;
}
QScrollBar::sub-line:vertical,
QScrollBar::add-line:vertical {
background: #f1f5f9;
border: none;
height: 12px;
subcontrol-origin: margin;
}
QScrollBar::sub-line:vertical {
subcontrol-position: top;
}
QScrollBar::add-line:vertical {
subcontrol-position: bottom;
}
QScrollBar::up-arrow:vertical {
image: url("__UP_ARROW__");
width: 8px;
height: 8px;
}
QScrollBar::down-arrow:vertical {
image: url("__DOWN_ARROW__");
width: 8px;
height: 8px;
}
QScrollBar:horizontal {
background: #f1f5f9;
height: 12px;
margin: 0 12px 0 12px;
border: none;
}
QScrollBar::handle:horizontal {
background: #94a3b8;
min-width: 28px;
margin: 4px 0;
border-radius: 2px;
}
QScrollBar::handle:horizontal:hover {
background: #64748b;
margin: 2px 0;
border-radius: 4px;
}
QScrollBar::add-page:horizontal,
QScrollBar::sub-page:horizontal {
background: transparent;
border: none;
width: 0;
}
QScrollBar::sub-line:horizontal,
QScrollBar::add-line:horizontal {
background: #f1f5f9;
border: none;
width: 12px;
subcontrol-origin: margin;
}
QScrollBar::sub-line:horizontal {
subcontrol-position: left;
}
QScrollBar::add-line:horizontal {
subcontrol-position: right;
}
QScrollBar::left-arrow:horizontal {
image: url("__LEFT_ARROW__");
width: 8px;
height: 8px;
}
QScrollBar::right-arrow:horizontal {
image: url("__RIGHT_ARROW__");
width: 8px;
height: 8px;
}
""".replace("__INPUT_UP_ARROW__", asset_url("input-arrow-up-light.svg")) \
.replace("__INPUT_DOWN_ARROW__", asset_url("input-arrow-down-light.svg")) \
.replace("__UP_ARROW__", asset_url("scrollbar-arrow-up-light.svg")) \
.replace("__DOWN_ARROW__", asset_url("scrollbar-arrow-down-light.svg")) \
.replace("__LEFT_ARROW__", asset_url("scrollbar-arrow-left-light.svg")) \
.replace("__RIGHT_ARROW__", asset_url("scrollbar-arrow-right-light.svg"))
def dark_stylesheet() -> str:
return """
QWidget {
background-color: #1f1f1f;
color: #f8fafc;
selection-background-color: #3b82f6;
selection-color: #f8fafc;
}
QGraphicsView {
background-color: #2b2b2b;
border: none;
}
QMenuBar {
background-color: #242424;
color: #f8fafc;
}
QMenuBar::item {
background: transparent;
padding: 5px 10px;
}
QMenuBar::item:selected {
background-color: #343434;
}
QMenu {
background-color: #242424;
color: #f8fafc;
border: 1px solid #444444;
}
QMenu::item {
padding: 5px 24px 5px 24px;
}
QMenu::item:selected {
background-color: #3a3a3a;
}
QMenu::item:disabled {
color: #737373;
background-color: transparent;
}
QMenu::item:selected:disabled {
color: #737373;
background-color: transparent;
}
QPushButton, QToolButton {
background-color: #2f2f2f;
color: #f8fafc;
border: 1px solid #4b5563;
border-radius: 4px;
padding: 4px 8px;
}
QPushButton:hover, QToolButton:hover {
background-color: #3f3f46;
}
QPushButton:disabled, QToolButton:disabled {
background-color: #262626;
color: #737373;
border-color: #3a3a3a;
}
QTreeWidget, QTableWidget, QListWidget, QLineEdit, QComboBox, QSpinBox {
background-color: #2b2b2b;
color: #f8fafc;
alternate-background-color: #353535;
border: 1px solid #444444;
border-radius: 4px;
min-height: 24px;
selection-background-color: #3b82f6;
selection-color: #f8fafc;
}
QLineEdit, QComboBox, QSpinBox {
padding: 2px 8px;
}
QComboBox {
padding-right: 28px;
}
QComboBox:hover, QSpinBox:hover, QLineEdit:hover {
border-color: #737373;
}
QComboBox:focus, QSpinBox:focus, QLineEdit:focus {
border-color: #60a5fa;
}
QComboBox::drop-down {
subcontrol-origin: padding;
subcontrol-position: top right;
width: 24px;
border-left: 1px solid #444444;
border-top-right-radius: 4px;
border-bottom-right-radius: 4px;
background-color: #262626;
}
QComboBox::drop-down:hover {
background-color: #3f3f46;
}
QComboBox::down-arrow {
image: url("__INPUT_DOWN_ARROW__");
width: 10px;
height: 10px;
}
QSpinBox {
padding-right: 28px;
}
QSpinBox::up-button, QSpinBox::down-button {
subcontrol-origin: border;
width: 24px;
border-left: 1px solid #444444;
background-color: #262626;
}
QSpinBox::up-button {
subcontrol-position: top right;
border-top-right-radius: 4px;
}
QSpinBox::down-button {
subcontrol-position: bottom right;
border-bottom-right-radius: 4px;
}
QSpinBox::up-button:hover, QSpinBox::down-button:hover {
background-color: #3f3f46;
}
QSpinBox::up-arrow {
image: url("__INPUT_UP_ARROW__");
width: 10px;
height: 10px;
}
QSpinBox::down-arrow {
image: url("__INPUT_DOWN_ARROW__");
width: 10px;
height: 10px;
}
QComboBox QAbstractItemView {
background-color: #2b2b2b;
color: #f8fafc;
border: 1px solid #444444;
selection-background-color: #3b82f6;
selection-color: #f8fafc;
}
QHeaderView::section {
background-color: #3a3a3a;
color: #f8fafc;
border: 1px solid #444444;
padding: 4px;
}
QSplitter::handle {
background-color: #111827;
}
QStatusBar {
background-color: #1f1f1f;
color: #f8fafc;
}
QToolTip {
background-color: #111827;
color: #f8fafc;
border: 1px solid #4b5563;
}
QScrollBar:vertical {
background: #1f1f1f;
width: 12px;
margin: 12px 0 12px 0;
border: none;
}
QScrollBar::handle:vertical {
background: #737373;
min-height: 28px;
margin: 0 4px;
border-radius: 2px;
}
QScrollBar::handle:vertical:hover {
background: #a3a3a3;
margin: 0 2px;
border-radius: 4px;
}
QScrollBar::add-page:vertical,
QScrollBar::sub-page:vertical {
background: transparent;
border: none;
height: 0;
}
QScrollBar::sub-line:vertical,
QScrollBar::add-line:vertical {
background: #1f1f1f;
border: none;
height: 12px;
subcontrol-origin: margin;
}
QScrollBar::sub-line:vertical {
subcontrol-position: top;
}
QScrollBar::add-line:vertical {
subcontrol-position: bottom;
}
QScrollBar::up-arrow:vertical {
image: url("__UP_ARROW__");
width: 8px;
height: 8px;
}
QScrollBar::down-arrow:vertical {
image: url("__DOWN_ARROW__");
width: 8px;
height: 8px;
}
QScrollBar:horizontal {
background: #1f1f1f;
height: 12px;
margin: 0 12px 0 12px;
border: none;
}
QScrollBar::handle:horizontal {
background: #737373;
min-width: 28px;
margin: 4px 0;
border-radius: 2px;
}
QScrollBar::handle:horizontal:hover {
background: #a3a3a3;
margin: 2px 0;
border-radius: 4px;
}
QScrollBar::add-page:horizontal,
QScrollBar::sub-page:horizontal {
background: transparent;
border: none;
width: 0;
}
QScrollBar::sub-line:horizontal,
QScrollBar::add-line:horizontal {
background: #1f1f1f;
border: none;
width: 12px;
subcontrol-origin: margin;
}
QScrollBar::sub-line:horizontal {
subcontrol-position: left;
}
QScrollBar::add-line:horizontal {
subcontrol-position: right;
}
QScrollBar::left-arrow:horizontal {
image: url("__LEFT_ARROW__");
width: 8px;
height: 8px;
}
QScrollBar::right-arrow:horizontal {
image: url("__RIGHT_ARROW__");
width: 8px;
height: 8px;
}
""".replace("__INPUT_UP_ARROW__", asset_url("input-arrow-up-dark.svg")) \
.replace("__INPUT_DOWN_ARROW__", asset_url("input-arrow-down-dark.svg")) \
.replace("__UP_ARROW__", asset_url("scrollbar-arrow-up-dark.svg")) \
.replace("__DOWN_ARROW__", asset_url("scrollbar-arrow-down-dark.svg")) \
.replace("__LEFT_ARROW__", asset_url("scrollbar-arrow-left-dark.svg")) \
.replace("__RIGHT_ARROW__", asset_url("scrollbar-arrow-right-dark.svg"))