"""
StartupPage — the home screen with a title, subtitle, and a grid of
clickable tiles for each spacr app.
"""
from __future__ import annotations
import os
from typing import Callable, List, Tuple
from PySide6.QtCore import Qt, QSize, Signal
from PySide6.QtGui import QIcon, QPixmap
from PySide6.QtWidgets import (
QFrame,
QGridLayout,
QHBoxLayout,
QLabel,
QScrollArea,
QVBoxLayout,
QWidget,
)
from ..theme import PALETTE, SPACING
from ..widgets import Divider, Tile
def _find_logo_pixmap() -> QPixmap | None:
here = os.path.dirname(os.path.abspath(__file__))
for candidate in ("logo_spacr.png", "logo_spacr_v1.png"):
p = os.path.normpath(
os.path.join(here, "..", "..", "resources", "icons", candidate)
)
if os.path.isfile(p):
pix = QPixmap(p)
if not pix.isNull():
return pix
return None
[docs]
class StartupPage(QScrollArea):
"""Home screen. `tile_clicked(str key)` fires when a tile is pressed."""
[docs]
tile_clicked = Signal(str)
def __init__(
self,
apps: List[Tuple[str, str, str, str]],
icon_provider: Callable[[str], QIcon | None],
parent=None,
):
super().__init__(parent)
self.setWidgetResizable(True)
self.setFrameShape(QScrollArea.NoFrame)
content = QWidget()
outer = QVBoxLayout(content)
outer.setContentsMargins(SPACING["xxl"], SPACING["xxl"],
SPACING["xxl"], SPACING["xxl"])
outer.setSpacing(SPACING["xl"])
# Hero header block — logo + wordmark side by side, with subtitle
# below on a full-width band.
hero = QFrame()
hero.setObjectName("Hero")
hero_col = QVBoxLayout(hero)
hero_col.setContentsMargins(SPACING["xl"], SPACING["xl"],
SPACING["xl"], SPACING["xl"])
hero_col.setSpacing(SPACING["md"])
hero_col.setAlignment(Qt.AlignCenter)
brand_row = QHBoxLayout()
brand_row.setSpacing(SPACING["lg"])
brand_row.setAlignment(Qt.AlignCenter)
logo_pix = _find_logo_pixmap()
if logo_pix is not None:
logo_lbl = QLabel()
logo_lbl.setPixmap(
logo_pix.scaled(128, 128, Qt.KeepAspectRatio,
Qt.SmoothTransformation)
)
logo_lbl.setFixedSize(128, 128)
logo_lbl.setAlignment(Qt.AlignCenter)
brand_row.addWidget(logo_lbl)
wordmark_col = QVBoxLayout()
wordmark_col.setContentsMargins(0, 0, 0, 0)
wordmark_col.setSpacing(2)
wordmark_col.setAlignment(Qt.AlignVCenter)
eyebrow = QLabel("spaCR")
eyebrow.setObjectName("Caption")
wordmark_col.addWidget(eyebrow)
title = QLabel("Spatial phenotype analysis")
title.setObjectName("Hero")
wordmark_col.addWidget(title)
brand_row.addLayout(wordmark_col)
hero_col.addLayout(brand_row)
subtitle = QLabel(
"End-to-end microscopy → single-cell measurements → "
"genotype–phenotype mapping."
)
subtitle.setObjectName("Subtitle")
subtitle.setAlignment(Qt.AlignHCenter)
subtitle.setWordWrap(True)
subtitle.setMaximumWidth(820)
hero_col.addWidget(subtitle, alignment=Qt.AlignHCenter)
outer.addWidget(hero)
outer.addSpacing(SPACING["md"])
# Group apps by section, render each section as a heading + tile grid.
sections: dict[str, list[tuple[str, str, str]]] = {}
for key, name, desc, section in apps:
sections.setdefault(section, []).append((key, name, desc))
for section_name, entries in sections.items():
hdr = QLabel(section_name.upper())
hdr.setObjectName("Caption")
hdr.setAlignment(Qt.AlignHCenter)
outer.addWidget(hdr)
divider = Divider()
outer.addWidget(divider)
grid_container = QWidget()
grid_row = QHBoxLayout(grid_container)
grid_row.setContentsMargins(0, SPACING["sm"], 0, SPACING["sm"])
grid_row.addStretch(1)
inner = QWidget()
grid = QGridLayout(inner)
grid.setContentsMargins(0, 0, 0, 0)
grid.setHorizontalSpacing(SPACING["lg"])
grid.setVerticalSpacing(SPACING["md"])
grid.setAlignment(Qt.AlignHCenter | Qt.AlignTop)
cols = 6
for i, (key, name, desc) in enumerate(entries):
icon = icon_provider(key)
tile = Tile(text=name, icon=icon, caption=name,
tile_size=128, icon_size=68)
tile.setToolTip(desc)
tile.clicked.connect(lambda k=key: self.tile_clicked.emit(k))
tile._button.installEventFilter(self)
tile._button.setProperty("hover_caption", desc)
grid.addWidget(tile, i // cols, i % cols)
grid_row.addWidget(inner)
grid_row.addStretch(1)
outer.addWidget(grid_container)
outer.addStretch(1)
# Hover-follows footer — replaced with dynamic caption on tile hover.
self._hover_footer = QLabel(
"Hover a tile to see what each app does."
)
self._hover_footer.setObjectName("SubtitleSmall")
self._hover_footer.setAlignment(Qt.AlignHCenter)
self._hover_footer.setMinimumHeight(28)
outer.addWidget(self._hover_footer)
self.setWidget(content)
[docs]
def eventFilter(self, obj, event):
"""Update the hover footer with each tile's caption on Enter/Leave."""
from PySide6.QtCore import QEvent
if event.type() == QEvent.Enter:
cap = obj.property("hover_caption")
if cap:
self._hover_footer.setText(cap)
elif event.type() == QEvent.Leave:
self._hover_footer.setText(
"Hover a tile to see what each app does."
)
return super().eventFilter(obj, event)