plate-config-app/app/main_window.py
Danil Iarosh add8aff0af feat: release version 2.0.0
- Добавлена обратная совместимость с прошивками, содержащими маркер no_auth.

- Повторная авторизация после команды 04 и успешного обновления прошивки приведена к требуемому порядку.

- В терминал команд добавлена ручная кнопка авторизации.

- Реализовано OTA-обновление приложения с загрузкой платформенного артефакта, безопасной заменой и перезапуском.

- Проверка обновлений учитывает только более новые версии по схеме major, minor и patch.

- Обновлены имена Windows и Linux сборок, документация и тестовое покрытие.
2026-09-16 10:01:35 +03:00

280 lines
11 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

from __future__ import annotations
from PySide6.QtCore import QSize, Qt, QRectF
from PySide6.QtGui import (
QCloseEvent,
QColor,
QPainter,
QPainterPath,
QPen,
QRegion,
QResizeEvent,
QShowEvent,
)
from PySide6.QtWidgets import (
QFrame,
QHBoxLayout,
QMainWindow,
QStackedWidget,
QTabBar,
QToolButton,
QVBoxLayout,
QWidget,
)
from .ui.icons import ICON_COLOR_ACCENT, ICON_COLOR_MUTED, ICON_COLOR_PRIMARY, icon
from .ui.theme import build_stylesheet, get_palette, create_qpalette
from .services import settings_service
from .widgets.connection_tab import ConnectionTab
from .widgets.start_tab import StartTab
from .widgets.title_bar import TitleBar
from . import __version__
class MainWindow(QMainWindow):
def __init__(self) -> None:
super().__init__()
self.setObjectName("AppWindow")
self.setWindowTitle(f"Plate Config App v{__version__}")
self.resize(820, 560)
self.setWindowFlags(Qt.WindowType.Window)
self.setMinimumSize(760, 500)
container = QWidget()
container.setObjectName("MainContainer")
container_layout = QVBoxLayout(container)
container_layout.setContentsMargins(0, 0, 0, 0)
container_layout.setSpacing(0)
self.title_bar = TitleBar(self)
container_layout.addWidget(self.title_bar)
self.tabs = QTabBar()
self.tabs.setObjectName("TitleTabs")
self.tabs.setDrawBase(False)
self.tabs.setExpanding(False)
self.tabs.setElideMode(Qt.TextElideMode.ElideRight)
self.tabs.setIconSize(QSize(12, 12))
self.tabs.currentChanged.connect(self._on_tab_changed)
self.title_bar.set_tabs_widget(self.tabs)
self.content_frame = QFrame()
self.content_frame.setObjectName("ContentArea")
content_layout = QVBoxLayout(self.content_frame)
content_layout.setContentsMargins(0, 0, 0, 0)
content_layout.setSpacing(0)
self.pages = QStackedWidget()
content_layout.addWidget(self.pages)
container_layout.addWidget(self.content_frame, 1)
self.setCentralWidget(container)
start_tab = StartTab(self.new_connection_tab)
self._add_tab(start_tab, "Главная", icon_name="fa5s.home", closable=False)
self.tabs.setCurrentIndex(0)
def new_connection_tab(self) -> None:
tab: ConnectionTab | None = None
def get_index() -> int:
if tab is None:
return -1
return self.pages.indexOf(tab)
tab = ConnectionTab(
tab_index_getter=get_index,
set_tab_title=self._set_tab_title,
close_tab=self._close_tab,
set_tab_indicator=self._set_tab_indicator,
)
index = self._add_tab(tab, "Новое соединение", icon_name="fa5s.plug", closable=True)
self.tabs.setCurrentIndex(index)
# ── Управление вкладками ────────────────────────────────────────
def _close_tab(self, index: int) -> None:
if index == 0:
return
widget = self.pages.widget(index)
if widget is None:
return
if isinstance(widget, ConnectionTab):
widget.close_serial()
self.pages.removeWidget(widget)
widget.deleteLater()
self.tabs.removeTab(index)
# обновляем текущую вкладку и иконки (индексы сдвинулись)
if self.tabs.count() > 0:
self.tabs.setCurrentIndex(min(index, self.tabs.count() - 1))
self._refresh_tab_icons()
def closeEvent(self, event: QCloseEvent) -> None:
for i in range(self.pages.count()):
widget = self.pages.widget(i)
if isinstance(widget, ConnectionTab):
widget.close_serial()
elif isinstance(widget, StartTab):
widget.shutdown()
super().closeEvent(event)
def _attach_close_button(self, index: int) -> None:
# Контейнер: [индикатор] [кнопка закрытия]
container = QWidget(self.tabs)
container.setObjectName("TabRightContainer")
h = QHBoxLayout(container)
h.setContentsMargins(2, 0, 4, 0)
h.setSpacing(4)
# Индикатор состояния (скрыт по умолчанию)
indicator = QWidget(container)
indicator.setObjectName("TabStatusDot")
indicator.setAttribute(Qt.WidgetAttribute.WA_StyledBackground, True)
indicator.setFixedSize(8, 8)
indicator.setProperty("dotColor", "none")
indicator.hide()
h.addWidget(indicator, 0, Qt.AlignmentFlag.AlignVCenter)
close_button = QToolButton(container)
close_button.setText("×")
close_icon = icon("fa5s.times", color=ICON_COLOR_MUTED)
if not close_icon.isNull():
close_button.setIcon(close_icon)
close_button.setText("")
close_button.setCursor(Qt.CursorShape.PointingHandCursor)
close_button.setObjectName("TabCloseButton")
close_button.setProperty("tabClose", True)
close_button.setAutoRaise(True)
close_button.setFixedSize(14, 14)
close_button.setIconSize(QSize(10, 10))
close_button.setFocusPolicy(Qt.FocusPolicy.NoFocus)
close_button.clicked.connect(self._close_tab_from_button)
h.addWidget(close_button, 0, Qt.AlignmentFlag.AlignVCenter)
# Фиксируем размер с учётом индикатора (даже скрытого),
# чтобы QTabBar не пересчитывал позицию при показе/скрытии
indicator.show()
full_size = container.sizeHint()
indicator.hide()
container.setFixedSize(full_size)
self.tabs.setTabButton(index, QTabBar.ButtonPosition.RightSide, container)
def _set_tab_indicator(self, index: int, color: str) -> None:
"""Установить цвет индикатора (green / yellow / red / none) для вкладки."""
container = self.tabs.tabButton(index, QTabBar.ButtonPosition.RightSide)
if container is None:
return
indicator = container.findChild(QWidget, "TabStatusDot")
if indicator is None:
return
if color == "none":
indicator.hide()
else:
indicator.setProperty("dotColor", color)
indicator.show()
# Перезагрузить стиль
style = indicator.style()
style.unpolish(indicator)
style.polish(indicator)
indicator.update()
def _refresh_tab_icons(self) -> None:
"""Обновляет иконки вкладок и иконки кнопок закрытия в зависимости от текущей вкладки."""
current = self.tabs.currentIndex()
for index in range(self.tabs.count()):
clr = ICON_COLOR_ACCENT if index == current else ICON_COLOR_PRIMARY
page = self.pages.widget(index)
icon_name = getattr(page, "_tab_icon_name", None)
if icon_name:
tab_icon = icon(icon_name, color=clr)
if not tab_icon.isNull():
self.tabs.setTabIcon(index, tab_icon)
# Кнопка закрытия теперь внутри контейнера
container = self.tabs.tabButton(index, QTabBar.ButtonPosition.RightSide)
if container is not None:
close_btn = container.findChild(QToolButton, "TabCloseButton")
if close_btn is not None:
close_icon = icon("fa5s.times", color=clr)
if not close_icon.isNull():
close_btn.setIcon(close_icon)
def _close_tab_from_button(self) -> None:
sender = self.sender()
for index in range(self.tabs.count()):
container = self.tabs.tabButton(index, QTabBar.ButtonPosition.RightSide)
if container is not None:
close_btn = container.findChild(QToolButton, "TabCloseButton")
if close_btn is sender:
self._close_tab(index)
return
def _add_tab(self, widget: QWidget, title: str, icon_name: str, *, closable: bool) -> int:
index = self.pages.addWidget(widget)
self.tabs.addTab(title)
# сохраняем имя иконки в виджете для дальнейшего обновления цвета
setattr(widget, "_tab_icon_name", icon_name)
tab_icon = icon(icon_name, color=ICON_COLOR_MUTED)
if not tab_icon.isNull():
self.tabs.setTabIcon(index, tab_icon)
if closable:
self._attach_close_button(index)
else:
self.tabs.setTabButton(index, QTabBar.ButtonPosition.LeftSide, None)
self.tabs.setTabButton(index, QTabBar.ButtonPosition.RightSide, None)
# синхронизируем цвета иконок
self._refresh_tab_icons()
return index
def _set_tab_title(self, index: int, title: str) -> None:
if 0 <= index < self.tabs.count():
self.tabs.setTabText(index, title)
# поставить иконку "подключено" и сохранить её имя для управления цветом
page = self.pages.widget(index)
if page is not None:
setattr(page, "_tab_icon_name", "fa5s.microchip")
connected_icon = icon("fa5s.microchip", color=ICON_COLOR_PRIMARY)
if not connected_icon.isNull():
self.tabs.setTabIcon(index, connected_icon)
self._refresh_tab_icons()
def _on_tab_changed(self, index: int) -> None:
if index >= 0:
self.pages.setCurrentIndex(index)
self._refresh_tab_icons()
# ── Theme ────────────────────────────────────────────────────
def apply_theme(self, mode: str, accent: str) -> None:
"""Apply a new theme palette and refresh all icons."""
from PySide6.QtWidgets import QApplication
app = QApplication.instance()
if isinstance(app, QApplication):
palette = get_palette(mode, accent)
app.setPalette(create_qpalette(palette))
app.setStyleSheet(build_stylesheet(palette))
# Refresh icons on every page
for i in range(self.pages.count()):
page = self.pages.widget(i)
refresh = getattr(page, "refresh_icons", None)
if callable(refresh):
refresh()
self._refresh_tab_icons()
def apply_runtime_settings(self) -> None:
"""Propagate runtime settings changes to all connection tabs."""
for i in range(self.pages.count()):
page = self.pages.widget(i)
if isinstance(page, ConnectionTab):
page.apply_runtime_settings()