Files
AareDAQ/scripts/gui_desginer.py
T
perl_d 0135129c89
CI / lint (pull_request) Failing after 33s
CI / test (3.11) (pull_request) Skipped
CI / test (3.12) (pull_request) Skipped
CI / test (3.13) (pull_request) Skipped
CI / test-with-beamline-plugins (pxi_bec) (pull_request) Skipped
CI / test-with-beamline-plugins (pxii_bec) (pull_request) Skipped
CI / test-with-beamline-plugins (pxiii_bec) (pull_request) Skipped
CI / test-with-coverage (pull_request) Skipped
fix: as many ruff errors as possible, some type
2026-08-03 09:27:40 +02:00

588 lines
20 KiB
Python

import math
import sys
from typing import ClassVar
from PySide6.QtCore import QPointF, QRectF, Qt
from PySide6.QtGui import QColor, QFont, QFontMetrics, QLinearGradient, QPainter, QPen
from PySide6.QtWidgets import (
QApplication,
QFrame,
QHBoxLayout,
QLabel,
QPushButton,
QScrollArea,
QStackedWidget,
QVBoxLayout,
QWidget,
)
# ---------------------------------------------------------------------------
# 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: ClassVar[list[str]] = ["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())