Conventional-Commit-driven version bumps (scripts/bump.py), a commit-msg validation hook, and a /release-notes workflow that produces a human-reviewed CHANGELOG.md. Adds an in-app version badge + 'What's new' dialog on the launcher. The version is single-sourced from pyproject.toml (cim_suite.__version__), and the deterministic bump backbone lives in scripts/release/ with tests in tests/release/. Marks the 1.0.0 baseline. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
147 lines
4.7 KiB
Python
147 lines
4.7 KiB
Python
"""The landing page: a cable panel + a grid of module cards.
|
|
|
|
The cable panel resolves the active service cable; module cards stay disabled until
|
|
a source is selected. Available cards emit ``moduleChosen`` when Open is clicked.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from collections.abc import Callable, Sequence
|
|
|
|
from PySide6.QtCore import Qt, Signal
|
|
from PySide6.QtWidgets import (
|
|
QFrame,
|
|
QGridLayout,
|
|
QHBoxLayout,
|
|
QLabel,
|
|
QPushButton,
|
|
QScrollArea,
|
|
QVBoxLayout,
|
|
QWidget,
|
|
)
|
|
|
|
import cim_suite
|
|
from cim_suite.core.module import Module
|
|
from cim_suite.core.transport.port_scan import DetectedPort, scan_ports
|
|
from cim_suite.core.ui.theme import Space
|
|
|
|
from .cable_panel import CablePanel
|
|
from .whats_new import WhatsNewDialog
|
|
|
|
_COLUMNS = 3
|
|
|
|
|
|
class _Card(QFrame):
|
|
def __init__(self, module: Module) -> None:
|
|
super().__init__()
|
|
self.setObjectName("ModuleCard")
|
|
self.setProperty("available", "true" if module.available else "false")
|
|
|
|
layout = QVBoxLayout(self)
|
|
layout.setContentsMargins(Space.LG, Space.LG, Space.LG, Space.LG)
|
|
layout.setSpacing(Space.SM)
|
|
|
|
title = QLabel(module.title)
|
|
title.setObjectName("ModuleCardTitle")
|
|
title.setWordWrap(True)
|
|
|
|
summary = QLabel(module.summary)
|
|
summary.setObjectName("ModuleCardSummary")
|
|
summary.setWordWrap(True)
|
|
|
|
layout.addWidget(title)
|
|
layout.addWidget(summary)
|
|
layout.addStretch(1)
|
|
|
|
if module.available:
|
|
self.open_button = QPushButton("Open")
|
|
self.open_button.setProperty("variant", "primary")
|
|
layout.addWidget(self.open_button, alignment=Qt.AlignmentFlag.AlignLeft)
|
|
else:
|
|
badge = QLabel("Coming soon")
|
|
badge.setObjectName("ModuleCardBadge")
|
|
layout.addWidget(badge, alignment=Qt.AlignmentFlag.AlignLeft)
|
|
self.open_button = None
|
|
|
|
|
|
class LauncherView(QScrollArea):
|
|
"""Emits ``moduleChosen`` with the module when an available card's Open is clicked.
|
|
|
|
Module Open buttons are disabled until the cable panel reports a selected source.
|
|
"""
|
|
|
|
moduleChosen = Signal(object)
|
|
|
|
def __init__(
|
|
self,
|
|
modules: Sequence[Module],
|
|
*,
|
|
include_simulator: bool = False,
|
|
scan_fn: Callable[..., list[DetectedPort]] = scan_ports,
|
|
) -> None:
|
|
super().__init__()
|
|
self.setWidgetResizable(True)
|
|
self._scan_fn = scan_fn
|
|
self._open_buttons: list[QPushButton] = []
|
|
|
|
page = QWidget()
|
|
page.setObjectName("LauncherPage")
|
|
outer = QVBoxLayout(page)
|
|
outer.setContentsMargins(Space.XL, Space.LG, Space.XL, Space.XL)
|
|
outer.setSpacing(Space.MD)
|
|
|
|
header = QHBoxLayout()
|
|
heading = QLabel("CIMTechniques Service Suite")
|
|
heading.setObjectName("LauncherHeading")
|
|
header.addWidget(heading)
|
|
header.addStretch(1)
|
|
|
|
version = QLabel(f"v{cim_suite.__version__}")
|
|
version.setObjectName("LauncherVersion")
|
|
header.addWidget(version, alignment=Qt.AlignmentFlag.AlignVCenter)
|
|
|
|
self._whats_new_btn = QPushButton("What's new")
|
|
self._whats_new_btn.setObjectName("WhatsNewButton")
|
|
self._whats_new_btn.clicked.connect(self._show_whats_new)
|
|
header.addWidget(self._whats_new_btn, alignment=Qt.AlignmentFlag.AlignVCenter)
|
|
outer.addLayout(header)
|
|
|
|
self._cable = CablePanel(include_simulator=include_simulator, scan_fn=scan_fn)
|
|
self._cable.sourceChanged.connect(self._on_source_changed)
|
|
outer.addWidget(self._cable)
|
|
|
|
grid = QGridLayout()
|
|
grid.setSpacing(Space.LG)
|
|
for i, module in enumerate(modules):
|
|
card = _Card(module)
|
|
if card.open_button is not None:
|
|
self._open_buttons.append(card.open_button)
|
|
card.open_button.clicked.connect(
|
|
lambda _=False, m=module: self.moduleChosen.emit(m)
|
|
)
|
|
grid.addWidget(card, i // _COLUMNS, i % _COLUMNS)
|
|
outer.addLayout(grid)
|
|
outer.addStretch(1)
|
|
|
|
self.setWidget(page)
|
|
self._set_cards_enabled(False)
|
|
|
|
# --- cable selection ---------------------------------------------------
|
|
def active_source(self) -> str:
|
|
return self._cable.active_source()
|
|
|
|
def rescan(self) -> int:
|
|
# Keep the panel's scan source in sync (tests reassign self._scan_fn).
|
|
self._cable.set_scan_fn(self._scan_fn)
|
|
return self._cable.rescan()
|
|
|
|
def _show_whats_new(self) -> None:
|
|
WhatsNewDialog(self).exec()
|
|
|
|
def _on_source_changed(self, source: str) -> None:
|
|
self._set_cards_enabled(bool(source))
|
|
|
|
def _set_cards_enabled(self, enabled: bool) -> None:
|
|
for btn in self._open_buttons:
|
|
btn.setEnabled(enabled)
|