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
588 lines
20 KiB
Python
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())
|