Files
AareDAQ/scripts/gui_desginer.py
T
2026-07-06 15:39:02 +02:00

587 lines
20 KiB
Python

from PySide6.QtWidgets import (
QApplication,
QWidget,
QVBoxLayout,
QHBoxLayout,
QLabel,
QFrame,
QPushButton,
QScrollArea,
QStackedWidget,
)
from PySide6.QtCore import Qt, QPointF, QRectF
from PySide6.QtGui import QPainter, QColor, QPen, QLinearGradient, QFont, QFontMetrics
import sys
import math
# ---------------------------------------------------------------------------
# Colour palette
# ---------------------------------------------------------------------------
BG = "#071018"
CARD_BG = "#0E1A26"
ACCENT = "#62D8C8"
ACCENT_DIM = "#1A3A36"
TEXT = "#F5F7FA"
SUBTEXT = "#8A9BB0"
BUTTON_BG = "#132131"
LED_OFF = "#1C2E3E"
LED_ON = ACCENT
ACTIVE_STEP = "#FFFFFF"
STYLE = f"""
QWidget {{
background: {BG};
color: {TEXT};
font-family: 'Inter', 'SF Pro Display', Arial, sans-serif;
font-size: 14px;
}}
QScrollArea {{
border: none;
background: transparent;
}}
QScrollBar:vertical {{
background: {CARD_BG};
width: 4px;
border-radius: 2px;
}}
QScrollBar::handle:vertical {{
background: {ACCENT_DIM};
border-radius: 2px;
min-height: 20px;
}}
QScrollBar::add-line:vertical, QScrollBar::sub-line:vertical {{
height: 0px;
}}
"""
# ---------------------------------------------------------------------------
# Orbit / camera placeholder widget
# ---------------------------------------------------------------------------
class OrbitWidget(QWidget):
def paintEvent(self, event):
p = QPainter(self)
p.setRenderHint(QPainter.Antialiasing)
w, h = self.width(), self.height()
cx, cy = w / 2, h / 2
# Background gradient
bg = QLinearGradient(0, 0, w, h)
bg.setColorAt(0, QColor("#0D2A2C"))
bg.setColorAt(1, QColor("#0D1E2E"))
p.fillRect(self.rect(), bg)
# Corner brackets
pen = QPen(QColor(ACCENT))
pen.setWidth(2)
p.setPen(pen)
size = 28
margin = 24
for x, y, dx, dy in [
(margin, margin, 1, 1),
(w - margin, margin, -1, 1),
(margin, h - margin, 1, -1),
(w - margin, h - margin, -1, -1),
]:
p.drawLine(x, y, x + dx * size, y)
p.drawLine(x, y, x, y + dy * size)
# Orbit rings
pen.setWidth(1)
pen.setColor(QColor(ACCENT + "80")) # semi-transparent
p.setPen(pen)
for r in [38, 68, 98]:
p.drawEllipse(QPointF(cx, cy), r, r)
# Centre dot
p.setBrush(QColor(ACCENT))
p.setPen(Qt.NoPen)
p.drawEllipse(QPointF(cx, cy), 9, 9)
# Orbiting dots on outer ring
p.setBrush(QColor("#F5FFFF"))
for angle in range(0, 360, 45):
rad = math.radians(angle)
ox = cx + math.cos(rad) * 98
oy = cy + math.sin(rad) * 98
p.drawEllipse(QPointF(ox, oy), 4, 4)
# ---------------------------------------------------------------------------
# LED step indicator
# ---------------------------------------------------------------------------
class LEDStages(QWidget):
STEPS = ["Mount", "Centre", "Raster", "Collect"]
def __init__(self, active_step: int = 1, parent=None):
"""
active_step: 0-based index of the currently active step.
Steps before active_step are shown as 'done' (filled accent),
active_step is highlighted, later steps are dim.
"""
super().__init__(parent)
self._active = active_step
self.setFixedHeight(72)
def set_active_step(self, step: int):
self._active = step
self.update()
def paintEvent(self, event):
p = QPainter(self)
p.setRenderHint(QPainter.Antialiasing)
w = self.width()
n = len(self.STEPS)
step_w = w / n
led_r = 10
cy = 44 # vertical centre of LEDs
label_y = 16
for i, name in enumerate(self.STEPS):
cx = step_w * i + step_w / 2
# --- connector line to next step ---
if i < n - 1:
next_cx = step_w * (i + 1) + step_w / 2
line_color = QColor(ACCENT) if i < self._active else QColor(LED_OFF)
pen = QPen(line_color, 2)
p.setPen(pen)
p.drawLine(QPointF(cx + led_r + 3, cy), QPointF(next_cx - led_r - 3, cy))
# --- LED circle ---
p.setPen(Qt.NoPen)
if i < self._active:
# completed
p.setBrush(QColor(ACCENT))
p.drawEllipse(QPointF(cx, cy), led_r, led_r)
# tick mark
pen = QPen(QColor(BG), 2)
pen.setCapStyle(Qt.RoundCap)
p.setPen(pen)
p.drawLine(QPointF(cx - 4, cy), QPointF(cx - 1, cy + 3))
p.drawLine(QPointF(cx - 1, cy + 3), QPointF(cx + 4, cy - 3))
elif i == self._active:
# active — bright with glow ring
glow_pen = QPen(QColor(ACCENT + "55"), 4)
p.setPen(glow_pen)
p.setBrush(Qt.NoBrush)
p.drawEllipse(QPointF(cx, cy), led_r + 4, led_r + 4)
p.setPen(Qt.NoPen)
p.setBrush(QColor(ACTIVE_STEP))
p.drawEllipse(QPointF(cx, cy), led_r, led_r)
else:
# pending
p.setBrush(QColor(LED_OFF))
p.drawEllipse(QPointF(cx, cy), led_r, led_r)
# --- label ---
label_color = QColor(ACCENT) if i <= self._active else QColor(SUBTEXT)
p.setPen(label_color)
font = QFont("Inter", 10)
if i == self._active:
font.setWeight(QFont.Weight.DemiBold)
p.setFont(font)
fm = QFontMetrics(font)
text_w = fm.horizontalAdvance(name)
p.drawText(
QRectF(cx - text_w / 2 - 4, 0, text_w + 8, label_y + 2), Qt.AlignCenter, name
)
# ---------------------------------------------------------------------------
# Transport / control button
# ---------------------------------------------------------------------------
class ControlButton(QPushButton):
def __init__(self, symbol: str, primary: bool = False, parent=None):
super().__init__(parent)
self._symbol = symbol
self._primary = primary
self._hovered = False
size = 64 if primary else 52
self.setFixedSize(size, size)
self.setMouseTracking(True)
def enterEvent(self, event):
self._hovered = True
self.update()
def leaveEvent(self, event):
self._hovered = False
self.update()
def paintEvent(self, event):
p = QPainter(self)
p.setRenderHint(QPainter.Antialiasing)
rect = self.rect()
cx, cy = rect.width() / 2, rect.height() / 2
r = min(rect.width(), rect.height()) / 2 - 2
if self._primary:
if self._hovered:
p.setBrush(QColor("#FFFFFF"))
else:
p.setBrush(QColor(ACCENT))
p.setPen(Qt.NoPen)
p.drawEllipse(QPointF(cx, cy), r, r)
sym_color = QColor(BG)
else:
if self._hovered:
p.setBrush(QColor(ACCENT))
else:
p.setBrush(QColor(BUTTON_BG))
p.setPen(Qt.NoPen)
p.drawEllipse(QPointF(cx, cy), r, r)
sym_color = QColor(TEXT) if not self._hovered else QColor(BG)
p.setPen(QPen(sym_color, 2))
font = QFont("Arial", 16 if self._primary else 13)
p.setFont(font)
p.drawText(rect, Qt.AlignCenter, self._symbol)
# ---------------------------------------------------------------------------
# Queue item card
# ---------------------------------------------------------------------------
class QueueItemCard(QFrame):
def __init__(
self,
index: int | str,
title: str,
subtitle: str,
frames_done: int = 0,
frames_total: int = 0,
is_next: bool = False,
parent=None,
):
super().__init__(parent)
self._index = index
self._title = title
self._subtitle = subtitle
self._frames_done = frames_done
self._frames_total = frames_total
self._is_next = is_next
self.setFixedHeight(72)
self.setStyleSheet(f"""
QFrame {{
background: {"#112030" if is_next else "#0C1720"};
border-radius: 14px;
border: {"1px solid " + ACCENT_DIM if is_next else "none"};
}}
""")
self._build_layout()
def _build_layout(self):
layout = QHBoxLayout(self)
layout.setContentsMargins(14, 0, 14, 0)
layout.setSpacing(12)
# Index badge / play icon
badge = QLabel()
badge.setFixedSize(32, 32)
badge.setAlignment(Qt.AlignCenter)
if self._is_next:
badge.setText("▶")
badge.setStyleSheet(f"""
color: {ACCENT};
background: {ACCENT_DIM};
border-radius: 16px;
font-size: 12px;
font-weight: bold;
""")
else:
badge.setText(str(self._index))
badge.setStyleSheet(f"""
color: {SUBTEXT};
background: {BUTTON_BG};
border-radius: 16px;
font-size: 12px;
""")
layout.addWidget(badge)
# Text block
text_col = QVBoxLayout()
text_col.setSpacing(2)
text_col.setContentsMargins(0, 0, 0, 0)
title_lbl = QLabel(self._title)
title_lbl.setStyleSheet(
f"color: {TEXT}; font-size: 13px; font-weight: 600; background: transparent;"
)
sub_lbl = QLabel(self._subtitle)
sub_lbl.setStyleSheet(f"color: {SUBTEXT}; font-size: 11px; background: transparent;")
text_col.addWidget(title_lbl)
text_col.addWidget(sub_lbl)
layout.addLayout(text_col, stretch=1)
# Frame count + mini bar
right_col = QVBoxLayout()
right_col.setSpacing(4)
right_col.setContentsMargins(0, 0, 0, 0)
right_col.setAlignment(Qt.AlignRight | Qt.AlignVCenter)
if self._frames_total > 0:
frame_lbl = QLabel(f"{self._frames_done:,} / {self._frames_total:,}")
frame_lbl.setAlignment(Qt.AlignRight)
color = ACCENT if self._is_next else SUBTEXT
frame_lbl.setStyleSheet(f"color: {color}; font-size: 11px; background: transparent;")
right_col.addWidget(frame_lbl)
bar = MiniProgressBar(self._frames_done, self._frames_total)
right_col.addWidget(bar)
layout.addLayout(right_col)
class MiniProgressBar(QWidget):
def __init__(self, done: int, total: int, parent=None):
super().__init__(parent)
self._done = done
self._total = total
self.setFixedSize(80, 4)
def paintEvent(self, event):
p = QPainter(self)
p.setRenderHint(QPainter.Antialiasing)
w, h = self.width(), self.height()
# background track
p.setBrush(QColor(LED_OFF))
p.setPen(Qt.NoPen)
p.drawRoundedRect(0, 0, w, h, 2, 2)
# fill
if self._total > 0:
fill_w = max(4, int(w * self._done / self._total))
p.setBrush(QColor(ACCENT))
p.drawRoundedRect(0, 0, fill_w, h, 2, 2)
# ---------------------------------------------------------------------------
# Main SAM Camera widget
# ---------------------------------------------------------------------------
class SampleCamera(QWidget):
def __init__(self):
super().__init__()
self.setWindowFlags(Qt.FramelessWindowHint)
self.setAttribute(Qt.WA_TranslucentBackground)
self.resize(390, 860)
self.setStyleSheet(STYLE)
self._stack = QStackedWidget()
self._player_page = self._build_player()
self._queue_page = self._build_queue()
self._stack.addWidget(self._player_page)
self._stack.addWidget(self._queue_page)
root = QVBoxLayout(self)
root.setContentsMargins(12, 12, 12, 12)
root.addWidget(self._stack)
# ------------------------------------------------------------------
# Player page
# ------------------------------------------------------------------
def _build_player(self) -> QWidget:
page = QWidget()
layout = QVBoxLayout(page)
layout.setSpacing(10)
layout.setContentsMargins(0, 0, 0, 0)
# ── Title ──────────────────────────────────────────────────────
title = QLabel("S A M C A M E R A")
title.setAlignment(Qt.AlignCenter)
title.setStyleSheet(
f"color: {ACCENT}; font-size: 13px; letter-spacing: 3px; font-weight: 600;"
)
layout.addWidget(title)
# ── Camera card ────────────────────────────────────────────────
cam_card = QFrame()
cam_card.setStyleSheet(f"QFrame {{ background: {CARD_BG}; border-radius: 18px; }}")
cam_card_layout = QVBoxLayout(cam_card)
cam_card_layout.setContentsMargins(0, 0, 0, 0)
cam_card_layout.setSpacing(0)
orbit = OrbitWidget()
orbit.setMinimumHeight(220)
cam_card_layout.addWidget(orbit)
layout.addWidget(cam_card)
# ── Sample name ────────────────────────────────────────────────
name_lbl = QLabel("Crystal Plate 14 · Well C7")
name_lbl.setStyleSheet(f"color: {TEXT}; font-size: 18px; font-weight: 700;")
sub_lbl = QLabel("Serial MX · 1 kHz · Beamline X06SA")
sub_lbl.setStyleSheet(f"color: {SUBTEXT}; font-size: 12px;")
layout.addWidget(name_lbl)
layout.addWidget(sub_lbl)
# ── LED step indicator ─────────────────────────────────────────
self._leds = LEDStages(active_step=1) # 0=Mount done, 1=Centre active
layout.addWidget(self._leds)
# ── Transport controls ─────────────────────────────────────────
ctrl_frame = QFrame()
ctrl_frame.setStyleSheet(f"QFrame {{ background: {CARD_BG}; border-radius: 18px; }}")
ctrl_layout = QHBoxLayout(ctrl_frame)
ctrl_layout.setContentsMargins(16, 12, 16, 12)
ctrl_layout.setSpacing(0)
buttons = [
("⏮", False),
("⏪", False),
("⏸", True), # primary / highlighted
("⏩", False),
("⏭", False),
]
for sym, primary in buttons:
btn = ControlButton(sym, primary=primary)
ctrl_layout.addWidget(btn, alignment=Qt.AlignCenter)
if not primary:
ctrl_layout.addStretch(1)
layout.addWidget(ctrl_frame)
# ── Up next header ─────────────────────────────────────────────
up_next_row = QHBoxLayout()
up_next_lbl = QLabel("UP NEXT")
up_next_lbl.setStyleSheet(
f"color: {ACCENT}; font-size: 11px; letter-spacing: 2px; font-weight: 700;"
)
samples_lbl = QLabel("5 SAMPLES")
samples_lbl.setStyleSheet(f"color: {SUBTEXT}; font-size: 11px; letter-spacing: 1px;")
up_next_row.addWidget(up_next_lbl)
up_next_row.addStretch()
up_next_row.addWidget(samples_lbl)
layout.addLayout(up_next_row)
# ── Queue preview cards ────────────────────────────────────────
queue_items = [
("Crystal Plate 14 · Well C8", "Serial MX · 1 kHz", 4100, 6000, True),
("Crystal Plate 15 · Grid Scan", "Raster · 10 Hz", 0, 2000, False),
("Crystal Plate 13 · Well A1", "Serial MX · 1 kHz", 0, 5000, False),
("Crystal Plate 13 · Well B3", "Serial MX · 1 kHz", 0, 4500, False),
("Crystal Plate 14 · Well D5", "Serial MX · 1 kHz", 0, 6000, False),
]
queue_widget = QWidget()
queue_layout = QVBoxLayout(queue_widget)
queue_layout.setSpacing(6)
queue_layout.setContentsMargins(0, 0, 0, 0)
for i, (title, sub, done, total, is_next) in enumerate(queue_items):
card = QueueItemCard(
index="▶" if is_next else i + 1,
title=title,
subtitle=sub,
frames_done=done,
frames_total=total,
is_next=is_next,
)
queue_layout.addWidget(card)
layout.addWidget(queue_widget)
# ── View full queue button ────────────────────────────────────
view_btn = self._accent_button("VIEW FULL QUEUE ☰")
view_btn.clicked.connect(lambda: self._stack.setCurrentWidget(self._queue_page))
layout.addWidget(view_btn)
return page
# ------------------------------------------------------------------
# Queue / full list page
# ------------------------------------------------------------------
def _build_queue(self) -> QWidget:
page = QWidget()
layout = QVBoxLayout(page)
layout.setSpacing(10)
layout.setContentsMargins(0, 0, 0, 0)
title = QLabel("SAMPLE QUEUE")
title.setAlignment(Qt.AlignCenter)
title.setStyleSheet(
f"color: {ACCENT}; font-size: 13px; letter-spacing: 3px; font-weight: 700;"
)
layout.addWidget(title)
scroll = QScrollArea()
scroll.setWidgetResizable(True)
scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
inner = QWidget()
inner_layout = QVBoxLayout(inner)
inner_layout.setSpacing(6)
inner_layout.setContentsMargins(0, 4, 0, 4)
for i in range(1, 20):
card = QueueItemCard(
index=i,
title=f"Crystal Plate {i} · Well C{i}",
subtitle="Serial MX · 1 kHz",
frames_done=0,
frames_total=6000,
)
inner_layout.addWidget(card)
scroll.setWidget(inner)
layout.addWidget(scroll, stretch=1)
back_btn = self._accent_button("← BACK TO CAMERA")
back_btn.clicked.connect(lambda: self._stack.setCurrentWidget(self._player_page))
layout.addWidget(back_btn)
return page
# ------------------------------------------------------------------
# Helper: styled accent button
# ------------------------------------------------------------------
@staticmethod
def _accent_button(text: str) -> QPushButton:
btn = QPushButton(text)
btn.setFixedHeight(48)
btn.setStyleSheet(f"""
QPushButton {{
background: transparent;
border: 1.5px solid {ACCENT};
border-radius: 14px;
color: {ACCENT};
font-size: 12px;
font-weight: 700;
letter-spacing: 1.5px;
}}
QPushButton:hover {{
background: {ACCENT_DIM};
}}
QPushButton:pressed {{
background: {ACCENT};
color: {BG};
}}
""")
return btn
# ---------------------------------------------------------------------------
# Entry point
# ---------------------------------------------------------------------------
def export_png():
app = QApplication.instance() or QApplication(sys.argv)
ui = SampleCamera()
ui.show()
app.processEvents()
ui._stack.setCurrentWidget(ui._player_page)
app.processEvents()
ui.grab().save("sam_camera_player.png")
ui._stack.setCurrentWidget(ui._queue_page)
app.processEvents()
ui.grab().save("sam_camera_queue.png")
if __name__ == "__main__":
app = QApplication(sys.argv)
ui = SampleCamera()
ui.show()
sys.exit(app.exec())