Switch to standard OS windows, resizable dialogs, and channel-aware pixel test
- Удален кастомный безрамочный режим (FramelessWindowHint) главного окна и диалогов. - Внедрено нативное управление окнами операционной системы (изменение размеров, максимизация, минимизация). - Добавлено применение QPalette под выбранную тему (темную/свелую) и акцент для покраски системных элементов окон. - Внутреннее содержимое диалогов (StyledDialog) обернуто в QScrollArea с автоскроллом и ограничением по высоте до 720px во избежание выхода окон за пределы экрана. - TitleBar упрощен до простой верхней панели вкладок (sub-header/tabs). - В диалог теста подсветки добавлен выбор номера ленты (1..4) при тесте пикселя виртуальной матрицы, обновлена логика генерации и отправки тестовой команды. - Раздел "Бродяга морей" переименован в "Бродяги морей".
This commit is contained in:
parent
dfd4f0e0ba
commit
4ce9eb7689
@ -8,7 +8,7 @@ from PySide6.QtWidgets import QApplication
|
||||
from .main_window import MainWindow
|
||||
from .services import settings_service
|
||||
from .ui.icons import ICON_COLOR_PRIMARY, icon
|
||||
from .ui.theme import build_stylesheet, get_palette
|
||||
from .ui.theme import build_stylesheet, get_palette, create_qpalette
|
||||
|
||||
|
||||
def main() -> None:
|
||||
@ -18,6 +18,7 @@ def main() -> None:
|
||||
|
||||
palette = get_palette(settings_service.theme_mode(),
|
||||
settings_service.theme_accent())
|
||||
app.setPalette(create_qpalette(palette))
|
||||
app.setStyleSheet(build_stylesheet(palette))
|
||||
app_icon = icon("fa5s.microchip", color=ICON_COLOR_PRIMARY)
|
||||
if not app_icon.isNull():
|
||||
|
||||
@ -23,7 +23,7 @@ from PySide6.QtWidgets import (
|
||||
)
|
||||
|
||||
from .ui.icons import ICON_COLOR_ACCENT, ICON_COLOR_MUTED, ICON_COLOR_PRIMARY, icon
|
||||
from .ui.theme import build_stylesheet, get_palette
|
||||
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
|
||||
@ -32,47 +32,14 @@ from .widgets.title_bar import TitleBar
|
||||
from . import __version__
|
||||
|
||||
|
||||
class _WindowBorderOverlay(QWidget):
|
||||
"""Paints the accent border above all window content."""
|
||||
|
||||
def __init__(self, parent: QWidget, radius: int) -> None:
|
||||
super().__init__(parent)
|
||||
self._radius = radius
|
||||
self.setAttribute(Qt.WidgetAttribute.WA_TransparentForMouseEvents, True)
|
||||
self.setAttribute(Qt.WidgetAttribute.WA_StyledBackground, False)
|
||||
self.setObjectName("WindowBorderOverlay")
|
||||
|
||||
def paintEvent(self, event) -> None: # type: ignore[override]
|
||||
parent = self.parentWidget()
|
||||
if parent is None:
|
||||
return
|
||||
if parent.isMaximized() or parent.isFullScreen():
|
||||
return
|
||||
|
||||
accent = get_palette(
|
||||
settings_service.theme_mode(),
|
||||
settings_service.theme_accent(),
|
||||
).accent
|
||||
|
||||
p = QPainter(self)
|
||||
p.setRenderHint(QPainter.RenderHint.Antialiasing, True)
|
||||
p.setBrush(Qt.BrushStyle.NoBrush)
|
||||
p.setPen(QPen(QColor(accent), 2))
|
||||
rect = self.rect().adjusted(1, 1, -1, -1)
|
||||
p.drawRoundedRect(QRectF(rect), self._radius, self._radius)
|
||||
p.end()
|
||||
|
||||
|
||||
class MainWindow(QMainWindow):
|
||||
_WINDOW_RADIUS = 10
|
||||
|
||||
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 | Qt.WindowType.FramelessWindowHint)
|
||||
self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground, True)
|
||||
self.setWindowFlags(Qt.WindowType.Window)
|
||||
self.setMinimumSize(760, 500)
|
||||
|
||||
container = QWidget()
|
||||
@ -104,38 +71,12 @@ class MainWindow(QMainWindow):
|
||||
container_layout.addWidget(self.content_frame, 1)
|
||||
|
||||
self.setCentralWidget(container)
|
||||
self._border_overlay = _WindowBorderOverlay(self, self._WINDOW_RADIUS)
|
||||
self._border_overlay.setGeometry(self.rect())
|
||||
self._border_overlay.raise_()
|
||||
|
||||
start_tab = StartTab(self.new_connection_tab)
|
||||
self._add_tab(start_tab, "Главная", icon_name="fa5s.home", closable=False)
|
||||
self.tabs.setCurrentIndex(0)
|
||||
|
||||
def _update_window_mask(self) -> None:
|
||||
"""Apply rounded mask so bottom corners are physically clipped."""
|
||||
if self.isMaximized() or self.isFullScreen():
|
||||
self.clearMask()
|
||||
return
|
||||
if self.width() <= 0 or self.height() <= 0:
|
||||
return
|
||||
path = QPainterPath()
|
||||
path.addRoundedRect(QRectF(self.rect()), self._WINDOW_RADIUS, self._WINDOW_RADIUS)
|
||||
self.setMask(QRegion(path.toFillPolygon().toPolygon()))
|
||||
|
||||
def resizeEvent(self, event: QResizeEvent) -> None:
|
||||
super().resizeEvent(event)
|
||||
self._update_window_mask()
|
||||
self._border_overlay.setGeometry(self.rect())
|
||||
self._border_overlay.raise_()
|
||||
self._border_overlay.update()
|
||||
|
||||
def showEvent(self, event: QShowEvent) -> None:
|
||||
super().showEvent(event)
|
||||
self._update_window_mask()
|
||||
self._border_overlay.setGeometry(self.rect())
|
||||
self._border_overlay.raise_()
|
||||
self._border_overlay.update()
|
||||
|
||||
def new_connection_tab(self) -> None:
|
||||
tab: ConnectionTab | None = None
|
||||
@ -316,6 +257,7 @@ class MainWindow(QMainWindow):
|
||||
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
|
||||
@ -326,7 +268,6 @@ class MainWindow(QMainWindow):
|
||||
refresh()
|
||||
|
||||
self._refresh_tab_icons()
|
||||
self._border_overlay.update()
|
||||
|
||||
def apply_runtime_settings(self) -> None:
|
||||
"""Propagate runtime settings changes to all connection tabs."""
|
||||
|
||||
@ -11,6 +11,8 @@ from dataclasses import dataclass, asdict
|
||||
import os
|
||||
import tempfile
|
||||
|
||||
from PySide6.QtGui import QColor, QPalette
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ThemePalette:
|
||||
@ -219,32 +221,25 @@ def get_palette(mode: str = "light", accent: str = "green") -> ThemePalette:
|
||||
|
||||
_QSS_TEMPLATE = """
|
||||
QMainWindow#AppWindow {{
|
||||
background: transparent;
|
||||
background: {bg_base};
|
||||
}}
|
||||
|
||||
QWidget#MainContainer {{
|
||||
background: {bg_base};
|
||||
border: none;
|
||||
border-radius: 10px;
|
||||
}}
|
||||
|
||||
QFrame#ContentArea {{
|
||||
background: {bg_base};
|
||||
border: none;
|
||||
border-bottom-left-radius: 8px;
|
||||
border-bottom-right-radius: 8px;
|
||||
}}
|
||||
|
||||
QStackedWidget {{
|
||||
background: {bg_base};
|
||||
border-bottom-left-radius: 8px;
|
||||
border-bottom-right-radius: 8px;
|
||||
}}
|
||||
|
||||
QScrollArea {{
|
||||
background: {bg_base};
|
||||
border-bottom-left-radius: 8px;
|
||||
border-bottom-right-radius: 8px;
|
||||
}}
|
||||
|
||||
QScrollArea > QWidget > QWidget {{
|
||||
@ -253,11 +248,10 @@ QScrollArea > QWidget > QWidget {{
|
||||
|
||||
QWidget#TitleBar {{
|
||||
background: {accent};
|
||||
border-top-left-radius: 8px;
|
||||
border-top-right-radius: 8px;
|
||||
border-bottom: 1px solid {accent};
|
||||
}}
|
||||
|
||||
|
||||
QLabel#TitleBarTitle {{
|
||||
color: #f8fafc;
|
||||
font-size: 13px;
|
||||
@ -571,25 +565,6 @@ QMessageBox, QDialog {{
|
||||
color: {text_primary};
|
||||
}}
|
||||
|
||||
QWidget#DialogContainer {{
|
||||
background: {bg_dialog};
|
||||
border: 2px solid {accent};
|
||||
border-radius: 10px;
|
||||
}}
|
||||
|
||||
QWidget#DialogTitleBar {{
|
||||
background: {accent};
|
||||
border-top-left-radius: 8px;
|
||||
border-top-right-radius: 8px;
|
||||
border-bottom: 1px solid {accent};
|
||||
}}
|
||||
|
||||
QLabel#DialogTitle {{
|
||||
color: #f8fafc;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
}}
|
||||
|
||||
QMessageBox QLabel {{
|
||||
color: {text_primary};
|
||||
font-size: 13px;
|
||||
@ -859,6 +834,24 @@ def _arrow_svg_path(name: str, color: str, points: str) -> str:
|
||||
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-стрелки
|
||||
|
||||
@ -220,8 +220,8 @@ class LedTestDialog(StyledDialog):
|
||||
|
||||
root.addWidget(color_group)
|
||||
|
||||
# ── Бродяга морей (cmd 81) ──
|
||||
wanderer_group = QGroupBox("Бродяга морей (cmd 81)")
|
||||
# ── Бродяги морей (cmd 81) ──
|
||||
wanderer_group = QGroupBox("Бродяги морей (cmd 81)")
|
||||
wanderer_form = QFormLayout(wanderer_group)
|
||||
wanderer_form.setSpacing(6)
|
||||
|
||||
@ -328,6 +328,16 @@ class LedTestDialog(StyledDialog):
|
||||
self._m_test_type.currentIndexChanged.connect(self._on_matrix_test_type_changed)
|
||||
test_type_layout.addWidget(self._m_test_type, 1)
|
||||
|
||||
self._m_strip_label = QLabel("Лента:")
|
||||
self._m_strip_label.setVisible(False)
|
||||
test_type_layout.addWidget(self._m_strip_label)
|
||||
|
||||
self._m_strip_spin = QSpinBox()
|
||||
self._m_strip_spin.setRange(1, 4)
|
||||
self._m_strip_spin.setValue(1)
|
||||
self._m_strip_spin.setVisible(False)
|
||||
test_type_layout.addWidget(self._m_strip_spin)
|
||||
|
||||
self._m_test_param = QSpinBox()
|
||||
self._m_test_param.setRange(0, 999)
|
||||
self._m_test_param.setValue(0)
|
||||
@ -574,6 +584,9 @@ class LedTestDialog(StyledDialog):
|
||||
|
||||
def _on_matrix_test_type_changed(self, index: int) -> None:
|
||||
test_type = self._m_test_type.itemData(index)
|
||||
is_pixel_test = (test_type == 4)
|
||||
self._m_strip_label.setVisible(is_pixel_test)
|
||||
self._m_strip_spin.setVisible(is_pixel_test)
|
||||
self._m_test_param.setVisible(test_type >= 2)
|
||||
|
||||
def _send_matrix_test(self) -> None:
|
||||
@ -587,8 +600,9 @@ class LedTestDialog(StyledDialog):
|
||||
val = self._m_test_param.value()
|
||||
cmd = f"{mode.base_command}25{test_type}{val:02d}"
|
||||
elif test_type == 4:
|
||||
val = self._m_test_param.value()
|
||||
cmd = f"{mode.base_command}25{test_type}{val:03d}"
|
||||
strip = self._m_strip_spin.value()
|
||||
pixel = self._m_test_param.value()
|
||||
cmd = f"{mode.base_command}254{strip}{pixel:02d}"
|
||||
self._send(cmd)
|
||||
|
||||
def _send_matrix_scale(self, index: int) -> None:
|
||||
|
||||
@ -1,133 +1,68 @@
|
||||
"""Базовый класс для стилизованных диалоговых окон.
|
||||
|
||||
Убирает системную рамку, делает фон прозрачным,
|
||||
добавляет контейнер с зелёной обводкой и скруглёнными углами,
|
||||
мини-заголовок с кнопкой закрытия и перетаскиванием окна.
|
||||
Использует стандартные системные рамки и заголовки, поддерживает
|
||||
изменение размеров, сворачивание и разворачивание диалогов.
|
||||
Внутреннее содержимое оборачивается в прокручиваемую область (QScrollArea),
|
||||
чтобы избежать проблем с выходом окон за границы экрана при низком разрешении.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from PySide6.QtCore import QSize, Qt
|
||||
from PySide6.QtGui import QColor
|
||||
from PySide6.QtWidgets import (
|
||||
QDialog,
|
||||
QGraphicsDropShadowEffect,
|
||||
QHBoxLayout,
|
||||
QLabel,
|
||||
QToolButton,
|
||||
QScrollArea,
|
||||
QVBoxLayout,
|
||||
QWidget,
|
||||
)
|
||||
|
||||
from ...ui.icons import ICON_COLOR_DANGER, icon
|
||||
|
||||
|
||||
class StyledDialog(QDialog):
|
||||
"""QDialog без системной рамки, с обводкой, заголовком, кнопкой закрытия и ресайзом."""
|
||||
"""QDialog со стандартной рамкой, прокруткой содержимого и поддержкой изменения размеров."""
|
||||
|
||||
def __init__(self, parent: QWidget | None = None, **kwargs) -> None:
|
||||
super().__init__(parent, **kwargs)
|
||||
self.setWindowFlags(
|
||||
Qt.WindowType.Dialog
|
||||
| Qt.WindowType.FramelessWindowHint
|
||||
| Qt.WindowType.WindowMinimizeButtonHint
|
||||
| Qt.WindowType.WindowMaximizeButtonHint
|
||||
| Qt.WindowType.WindowCloseButtonHint
|
||||
)
|
||||
self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground, True)
|
||||
self.setMouseTracking(True)
|
||||
|
||||
# Внешний layout
|
||||
outer = QVBoxLayout(self)
|
||||
outer.setContentsMargins(0, 0, 0, 0)
|
||||
outer.setSpacing(0)
|
||||
# Главный макет диалога
|
||||
main_layout = QVBoxLayout(self)
|
||||
main_layout.setContentsMargins(0, 0, 0, 0)
|
||||
main_layout.setSpacing(0)
|
||||
|
||||
# Внутренний контейнер, к которому применяются стили
|
||||
self._container = QWidget(self)
|
||||
self._container.setObjectName("DialogContainer")
|
||||
self._container.setAttribute(Qt.WidgetAttribute.WA_StyledBackground, True)
|
||||
self._container.setMouseTracking(True)
|
||||
# Прокручиваемая область для содержимого
|
||||
self._scroll_area = QScrollArea(self)
|
||||
self._scroll_area.setWidgetResizable(True)
|
||||
self._scroll_area.setFrameShape(QScrollArea.Shape.NoFrame)
|
||||
self._scroll_area.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
|
||||
main_layout.addWidget(self._scroll_area)
|
||||
|
||||
outer.addWidget(self._container)
|
||||
# Виджет-контейнер для содержимого
|
||||
self._content_widget = QWidget()
|
||||
self._scroll_area.setWidget(self._content_widget)
|
||||
|
||||
container_layout = QVBoxLayout(self._container)
|
||||
container_layout.setContentsMargins(0, 0, 0, 0)
|
||||
container_layout.setSpacing(0)
|
||||
|
||||
# ── Мини-заголовок ──────────────────────────────────────────
|
||||
self._title_bar = QWidget(self._container)
|
||||
self._title_bar.setObjectName("DialogTitleBar")
|
||||
self._title_bar.setAttribute(Qt.WidgetAttribute.WA_StyledBackground, True)
|
||||
self._title_bar.setFixedHeight(32)
|
||||
|
||||
tb_layout = QHBoxLayout(self._title_bar)
|
||||
tb_layout.setContentsMargins(10, 4, 6, 4)
|
||||
tb_layout.setSpacing(4)
|
||||
|
||||
self._title_label = QLabel()
|
||||
self._title_label.setObjectName("DialogTitle")
|
||||
tb_layout.addWidget(self._title_label, 1)
|
||||
|
||||
self._close_btn = self._build_window_button()
|
||||
self._close_btn.clicked.connect(self.close)
|
||||
tb_layout.addWidget(self._close_btn)
|
||||
|
||||
container_layout.addWidget(self._title_bar)
|
||||
|
||||
# Область содержимого (sub-layout, доступен через containerLayout())
|
||||
self._body = QWidget(self._container)
|
||||
self._body_layout = QVBoxLayout(self._body)
|
||||
self._body_layout.setContentsMargins(12, 8, 12, 12)
|
||||
container_layout.addWidget(self._body, 1)
|
||||
|
||||
self._drag_offset = None
|
||||
|
||||
# ── Публичные методы ────────────────────────────────────────────
|
||||
# Макет содержимого
|
||||
self._body_layout = QVBoxLayout(self._content_widget)
|
||||
self._body_layout.setContentsMargins(12, 12, 12, 12)
|
||||
self._body_layout.setSpacing(8)
|
||||
|
||||
def setWindowTitle(self, title: str) -> None: # noqa: N802
|
||||
super().setWindowTitle(title)
|
||||
self._title_label.setText(title)
|
||||
|
||||
def containerLayout(self) -> QVBoxLayout:
|
||||
"""Вернуть layout области содержимого (под заголовком)."""
|
||||
"""Вернуть layout области содержимого (для совместимости с наследниками)."""
|
||||
return self._body_layout
|
||||
|
||||
# ── Построение кнопки ───────────────────────────────────────────
|
||||
|
||||
@staticmethod
|
||||
def _build_window_button() -> QToolButton:
|
||||
button = QToolButton()
|
||||
button.setProperty("windowControl", "close")
|
||||
button.setCursor(Qt.CursorShape.PointingHandCursor)
|
||||
button.setFixedSize(24, 20)
|
||||
button.setFocusPolicy(Qt.FocusPolicy.NoFocus)
|
||||
|
||||
close_icon = icon("fa5s.times", color=ICON_COLOR_DANGER)
|
||||
if not close_icon.isNull():
|
||||
button.setIcon(close_icon)
|
||||
else:
|
||||
button.setText("×")
|
||||
button.setIconSize(QSize(10, 10))
|
||||
|
||||
shadow = QGraphicsDropShadowEffect(button)
|
||||
shadow.setBlurRadius(10)
|
||||
shadow.setOffset(0, 2)
|
||||
shadow.setColor(QColor(0, 0, 0, 80))
|
||||
button.setGraphicsEffect(shadow)
|
||||
|
||||
return button
|
||||
|
||||
# ── Перетаскивание ───────────────────────────────────────────────
|
||||
|
||||
def mousePressEvent(self, event) -> None: # type: ignore[override]
|
||||
if event.button() == Qt.MouseButton.LeftButton:
|
||||
self._drag_offset = (
|
||||
event.globalPosition().toPoint() - self.frameGeometry().topLeft()
|
||||
)
|
||||
super().mousePressEvent(event)
|
||||
|
||||
def mouseMoveEvent(self, event) -> None: # type: ignore[override]
|
||||
if event.buttons() & Qt.MouseButton.LeftButton and self._drag_offset is not None:
|
||||
self.move(event.globalPosition().toPoint() - self._drag_offset)
|
||||
super().mouseMoveEvent(event)
|
||||
|
||||
def mouseReleaseEvent(self, event) -> None: # type: ignore[override]
|
||||
self._drag_offset = None
|
||||
super().mouseReleaseEvent(event)
|
||||
def sizeHint(self) -> QSize:
|
||||
"""Вычислить предпочтительный размер на основе содержимого, ограничивая высоту."""
|
||||
hint = self._scroll_area.widget().sizeHint()
|
||||
# Добавляем небольшой запас по ширине для вертикальной полосы прокрутки
|
||||
hint.setWidth(hint.width() + 18)
|
||||
# Ограничиваем высоту 720 пикселями на случай больших макетов (например, тест подсветки)
|
||||
if hint.height() > 720:
|
||||
hint.setHeight(720)
|
||||
return hint
|
||||
|
||||
@ -1,24 +1,18 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from PySide6.QtCore import QEvent, Qt, QSize
|
||||
from PySide6.QtGui import QColor
|
||||
from PySide6.QtCore import Qt
|
||||
from PySide6.QtWidgets import (
|
||||
QGraphicsDropShadowEffect,
|
||||
QHBoxLayout,
|
||||
QLabel,
|
||||
QTabBar,
|
||||
QToolButton,
|
||||
QWidget,
|
||||
)
|
||||
|
||||
from ..ui.icons import ICON_COLOR_PRIMARY, ICON_COLOR_DANGER, icon
|
||||
|
||||
|
||||
class TitleBar(QWidget):
|
||||
"""Верхняя панель вкладок, отображаемая под системным заголовком главного окна."""
|
||||
|
||||
def __init__(self, window: QWidget) -> None:
|
||||
super().__init__(window)
|
||||
self._window = window
|
||||
self._drag_offset = None
|
||||
|
||||
self.setObjectName("TitleBar")
|
||||
self.setAttribute(Qt.WidgetAttribute.WA_StyledBackground, True)
|
||||
@ -26,19 +20,10 @@ class TitleBar(QWidget):
|
||||
self.setFixedHeight(38)
|
||||
|
||||
layout = QHBoxLayout(self)
|
||||
layout.setContentsMargins(10, 4, 6, 4)
|
||||
layout.setContentsMargins(10, 4, 10, 4)
|
||||
layout.setSpacing(8)
|
||||
self._layout = layout
|
||||
|
||||
self.icon_label = QLabel()
|
||||
self.icon_label.setObjectName("TitleBarIcon")
|
||||
self.icon_label.setFixedSize(16, 16)
|
||||
layout.addWidget(self.icon_label)
|
||||
|
||||
self.title_label = QLabel()
|
||||
self.title_label.setObjectName("TitleBarTitle")
|
||||
layout.addWidget(self.title_label)
|
||||
|
||||
self.tabs_container = QWidget(self)
|
||||
self.tabs_container.setObjectName("TitleTabsContainer")
|
||||
self.tabs_layout = QHBoxLayout(self.tabs_container)
|
||||
@ -46,78 +31,8 @@ class TitleBar(QWidget):
|
||||
self.tabs_layout.setSpacing(0)
|
||||
layout.addWidget(self.tabs_container, 1)
|
||||
|
||||
self.min_btn = self._build_window_button("—", "minimize")
|
||||
self.min_btn.clicked.connect(self._window.showMinimized)
|
||||
layout.addWidget(self.min_btn)
|
||||
|
||||
self.max_btn = self._build_window_button("□", "maximize")
|
||||
self.max_btn.clicked.connect(self._toggle_max_restore)
|
||||
layout.addWidget(self.max_btn)
|
||||
|
||||
self.close_btn = self._build_window_button("×", "close")
|
||||
self.close_btn.clicked.connect(self._window.close)
|
||||
layout.addWidget(self.close_btn)
|
||||
|
||||
self._window.installEventFilter(self)
|
||||
self._sync_window_meta()
|
||||
|
||||
def _build_window_button(self, text: str, role: str) -> QToolButton:
|
||||
button = QToolButton(self)
|
||||
button.setText(text)
|
||||
button.setProperty("windowControl", role)
|
||||
button.setCursor(Qt.CursorShape.PointingHandCursor)
|
||||
button.setFixedSize(30, 24)
|
||||
button.setFocusPolicy(Qt.FocusPolicy.NoFocus)
|
||||
|
||||
if role == "minimize":
|
||||
minimize_icon = icon("fa5s.minus", color=ICON_COLOR_PRIMARY)
|
||||
if not minimize_icon.isNull():
|
||||
button.setIcon(minimize_icon)
|
||||
button.setText("")
|
||||
elif role == "maximize":
|
||||
maximize_icon = icon("fa5s.square", color=ICON_COLOR_PRIMARY)
|
||||
if not maximize_icon.isNull():
|
||||
button.setIcon(maximize_icon)
|
||||
button.setText("")
|
||||
elif role == "close":
|
||||
close_icon = icon("fa5s.times", color=ICON_COLOR_DANGER)
|
||||
if not close_icon.isNull():
|
||||
button.setIcon(close_icon)
|
||||
button.setText("")
|
||||
|
||||
button.setIconSize(QSize(12, 12))
|
||||
|
||||
shadow = QGraphicsDropShadowEffect(button)
|
||||
shadow.setBlurRadius(10)
|
||||
shadow.setOffset(0, 2)
|
||||
shadow.setColor(QColor(0, 0, 0, 80))
|
||||
button.setGraphicsEffect(shadow)
|
||||
|
||||
return button
|
||||
|
||||
def _sync_window_meta(self) -> None:
|
||||
self.title_label.setText(self._window.windowTitle())
|
||||
window_icon = self._window.windowIcon()
|
||||
if not window_icon.isNull():
|
||||
self.icon_label.setPixmap(window_icon.pixmap(16, 16))
|
||||
max_icon = icon(
|
||||
"fa5s.clone" if self._window.isMaximized() else "fa5s.square",
|
||||
color=ICON_COLOR_PRIMARY,
|
||||
)
|
||||
if not max_icon.isNull():
|
||||
self.max_btn.setIcon(max_icon)
|
||||
self.max_btn.setText("")
|
||||
else:
|
||||
self.max_btn.setText("❐" if self._window.isMaximized() else "□")
|
||||
|
||||
def _toggle_max_restore(self) -> None:
|
||||
if self._window.isMaximized():
|
||||
self._window.showNormal()
|
||||
else:
|
||||
self._window.showMaximized()
|
||||
self._sync_window_meta()
|
||||
|
||||
def set_tabs_widget(self, widget: QWidget) -> None:
|
||||
"""Встроить виджет вкладок в панель."""
|
||||
while self.tabs_layout.count():
|
||||
item = self.tabs_layout.takeAt(0)
|
||||
if item is not None:
|
||||
@ -125,62 +40,3 @@ class TitleBar(QWidget):
|
||||
if old_widget is not None:
|
||||
old_widget.setParent(None)
|
||||
self.tabs_layout.addWidget(widget)
|
||||
# Перехватываем события мыши на QTabBar для перетаскивания
|
||||
# за пустую область вкладок
|
||||
widget.installEventFilter(self)
|
||||
|
||||
def eventFilter(self, watched, event): # type: ignore[override]
|
||||
if watched is self._window and event.type() in (
|
||||
QEvent.Type.WindowTitleChange,
|
||||
QEvent.Type.WindowIconChange,
|
||||
QEvent.Type.WindowStateChange,
|
||||
):
|
||||
self._sync_window_meta()
|
||||
|
||||
# Перетаскивание окна за пустую область QTabBar
|
||||
if isinstance(watched, QTabBar):
|
||||
etype = event.type()
|
||||
if etype == QEvent.Type.MouseButtonPress:
|
||||
if event.button() == Qt.MouseButton.LeftButton:
|
||||
if watched.tabAt(event.position().toPoint()) == -1:
|
||||
self._drag_offset = (
|
||||
event.globalPosition().toPoint()
|
||||
- self._window.frameGeometry().topLeft()
|
||||
)
|
||||
return True
|
||||
elif etype == QEvent.Type.MouseMove:
|
||||
if event.buttons() & Qt.MouseButton.LeftButton and self._drag_offset is not None:
|
||||
if not self._window.isMaximized():
|
||||
self._window.move(event.globalPosition().toPoint() - self._drag_offset)
|
||||
return True
|
||||
elif etype == QEvent.Type.MouseButtonRelease:
|
||||
if self._drag_offset is not None:
|
||||
self._drag_offset = None
|
||||
return True
|
||||
elif etype == QEvent.Type.MouseButtonDblClick:
|
||||
if event.button() == Qt.MouseButton.LeftButton:
|
||||
if watched.tabAt(event.position().toPoint()) == -1:
|
||||
self._toggle_max_restore()
|
||||
return True
|
||||
|
||||
return super().eventFilter(watched, event)
|
||||
|
||||
def mousePressEvent(self, event) -> None: # type: ignore[override]
|
||||
if event.button() == Qt.MouseButton.LeftButton:
|
||||
self._drag_offset = event.globalPosition().toPoint() - self._window.frameGeometry().topLeft()
|
||||
super().mousePressEvent(event)
|
||||
|
||||
def mouseMoveEvent(self, event) -> None: # type: ignore[override]
|
||||
if event.buttons() & Qt.MouseButton.LeftButton and self._drag_offset is not None:
|
||||
if not self._window.isMaximized():
|
||||
self._window.move(event.globalPosition().toPoint() - self._drag_offset)
|
||||
super().mouseMoveEvent(event)
|
||||
|
||||
def mouseReleaseEvent(self, event) -> None: # type: ignore[override]
|
||||
self._drag_offset = None
|
||||
super().mouseReleaseEvent(event)
|
||||
|
||||
def mouseDoubleClickEvent(self, event) -> None: # type: ignore[override]
|
||||
if event.button() == Qt.MouseButton.LeftButton:
|
||||
self._toggle_max_restore()
|
||||
super().mouseDoubleClickEvent(event)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user