- Добавлена обратная совместимость с прошивками, содержащими маркер no_auth. - Повторная авторизация после команды 04 и успешного обновления прошивки приведена к требуемому порядку. - В терминал команд добавлена ручная кнопка авторизации. - Реализовано OTA-обновление приложения с загрузкой платформенного артефакта, безопасной заменой и перезапуском. - Проверка обновлений учитывает только более новые версии по схеме major, minor и patch. - Обновлены имена Windows и Linux сборок, документация и тестовое покрытие.
280 lines
11 KiB
Python
280 lines
11 KiB
Python
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()
|