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()