mirror of
https://github.com/bec-project/bec_widgets.git
synced 2026-05-06 06:44:23 +02:00
321 lines
11 KiB
Python
321 lines
11 KiB
Python
from __future__ import annotations
|
|
|
|
from typing import List
|
|
|
|
from pydantic import BaseModel
|
|
from qtpy.QtCore import QEventLoop
|
|
from qtpy.QtWidgets import (
|
|
QDialog,
|
|
QDialogButtonBox,
|
|
QFormLayout,
|
|
QHBoxLayout,
|
|
QLabel,
|
|
QMessageBox,
|
|
QPushButton,
|
|
QStackedLayout,
|
|
QVBoxLayout,
|
|
QWidget,
|
|
)
|
|
|
|
from bec_widgets import BECWidget
|
|
from bec_widgets.utils.error_popups import SafeSlot
|
|
from bec_widgets.widgets.control.device_input.device_combobox.device_combobox import DeviceComboBox
|
|
from bec_widgets.widgets.control.device_input.signal_combobox.signal_combobox import SignalComboBox
|
|
from bec_widgets.widgets.plots.waveform.waveform import Waveform
|
|
|
|
|
|
class ViewTourSteps(BaseModel):
|
|
"""Model representing tour steps for a view.
|
|
|
|
Attributes:
|
|
view_title: The human-readable title of the view.
|
|
step_ids: List of registered step IDs in the order they should appear.
|
|
"""
|
|
|
|
view_title: str
|
|
step_ids: List[str]
|
|
|
|
|
|
class ViewBase(BECWidget, QWidget):
|
|
"""Wrapper for a content widget used inside the main app's stacked view.
|
|
|
|
Subclasses can implement `on_enter` and `on_exit` to run custom logic when the view becomes visible or is about to be hidden.
|
|
|
|
Args:
|
|
content (QWidget): The actual view widget to display.
|
|
parent (QWidget | None): Parent widget.
|
|
view_id (str | None): Optional view view_id, useful for debugging or introspection.
|
|
title (str | None): Optional human-readable title.
|
|
"""
|
|
|
|
RPC = True
|
|
PLUGIN = False
|
|
USER_ACCESS = ["activate"]
|
|
RPC_CONTENT_CLASS: type[QWidget] | None = None
|
|
RPC_CONTENT_ATTR = "content"
|
|
|
|
def __init__(
|
|
self,
|
|
parent: QWidget | None = None,
|
|
content: QWidget | None = None,
|
|
*,
|
|
view_id: str | None = None,
|
|
title: str | None = None,
|
|
**kwargs,
|
|
):
|
|
super().__init__(parent=parent, **kwargs)
|
|
self.content: QWidget | None = None
|
|
self.view_id = view_id
|
|
self.view_title = title
|
|
|
|
lay = QVBoxLayout(self)
|
|
lay.setContentsMargins(0, 0, 0, 0)
|
|
lay.setSpacing(0)
|
|
|
|
if content is not None:
|
|
self.set_content(content)
|
|
|
|
def set_content(self, content: QWidget) -> None:
|
|
"""Replace the current content widget with a new one."""
|
|
if self.content is not None:
|
|
self.content.setParent(None)
|
|
self.content = content
|
|
self.layout().addWidget(content)
|
|
|
|
@SafeSlot()
|
|
def on_enter(self) -> None:
|
|
"""Called after the view becomes current/visible.
|
|
|
|
Default implementation does nothing. Override in subclasses.
|
|
"""
|
|
pass
|
|
|
|
@SafeSlot()
|
|
def on_exit(self) -> bool:
|
|
"""Called before the view is switched away/hidden.
|
|
|
|
Return True to allow switching, or False to veto.
|
|
Default implementation allows switching.
|
|
"""
|
|
return True
|
|
|
|
@SafeSlot()
|
|
def activate(self) -> None:
|
|
"""Switch the parent application to this view."""
|
|
if not self.view_id:
|
|
raise ValueError("Cannot switch view without a view_id.")
|
|
|
|
parent = self.parent()
|
|
while parent is not None:
|
|
if hasattr(parent, "set_current"):
|
|
parent.set_current(self.view_id)
|
|
return
|
|
parent = parent.parent()
|
|
raise RuntimeError("Could not find a parent application with set_current().")
|
|
|
|
def cleanup(self):
|
|
if self.content is not None:
|
|
self.content.close()
|
|
self.content.deleteLater()
|
|
super().cleanup()
|
|
|
|
def register_tour_steps(self, guided_tour, main_app) -> ViewTourSteps | None:
|
|
"""Register this view's components with the guided tour.
|
|
|
|
Args:
|
|
guided_tour: The GuidedTour instance to register with.
|
|
main_app: The main application instance (for accessing set_current).
|
|
|
|
Returns:
|
|
ViewTourSteps | None: A model containing the view title and step IDs,
|
|
or None if this view has no tour steps.
|
|
|
|
Override this method in subclasses to register view-specific components.
|
|
"""
|
|
return None
|
|
|
|
|
|
####################################################################################################
|
|
# Example views for demonstration/testing purposes
|
|
####################################################################################################
|
|
|
|
|
|
# --- Popup UI version ---
|
|
class WaveformViewPopup(ViewBase): # pragma: no cover
|
|
def __init__(self, parent=None, *args, **kwargs):
|
|
super().__init__(parent=parent, *args, **kwargs)
|
|
|
|
self.waveform = Waveform(parent=self)
|
|
self.set_content(self.waveform)
|
|
|
|
@SafeSlot()
|
|
def on_enter(self) -> None:
|
|
dialog = QDialog(self)
|
|
dialog.setWindowTitle("Configure Waveform View")
|
|
|
|
label = QLabel("Select device and signal for the waveform plot:", parent=dialog)
|
|
|
|
# same as in the CurveRow used in waveform
|
|
self.device_edit = DeviceComboBox(parent=self)
|
|
self.device_edit.insertItem(0, "")
|
|
self.device_edit.setEditable(True)
|
|
self.device_edit.setCurrentIndex(0)
|
|
self.signal_edit = SignalComboBox(parent=self)
|
|
self.signal_edit.include_config_signals = False
|
|
self.signal_edit.insertItem(0, "")
|
|
self.signal_edit.setEditable(True)
|
|
self.device_edit.currentTextChanged.connect(self.signal_edit.set_device)
|
|
self.device_edit.device_reset.connect(self.signal_edit.reset_selection)
|
|
|
|
form = QFormLayout()
|
|
form.addRow(label)
|
|
form.addRow("Device", self.device_edit)
|
|
form.addRow("Signal", self.signal_edit)
|
|
|
|
buttons = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel, parent=dialog)
|
|
buttons.accepted.connect(dialog.accept)
|
|
buttons.rejected.connect(dialog.reject)
|
|
|
|
v = QVBoxLayout(dialog)
|
|
v.addLayout(form)
|
|
v.addWidget(buttons)
|
|
|
|
if dialog.exec_() == QDialog.Accepted:
|
|
self.waveform.plot(
|
|
device_y=self.device_edit.currentText(), signal_y=self.signal_edit.currentText()
|
|
)
|
|
|
|
@SafeSlot()
|
|
def on_exit(self) -> bool:
|
|
ans = QMessageBox.question(
|
|
self,
|
|
"Switch and clear?",
|
|
"Do you want to switch views and clear the plot?",
|
|
QMessageBox.Yes | QMessageBox.No,
|
|
QMessageBox.No,
|
|
)
|
|
if ans == QMessageBox.Yes:
|
|
self.waveform.clear_all()
|
|
return True
|
|
return False
|
|
|
|
|
|
# --- Inline stacked UI version ---
|
|
class WaveformViewInline(ViewBase): # pragma: no cover
|
|
def __init__(self, parent=None, *args, **kwargs):
|
|
super().__init__(parent=parent, *args, **kwargs)
|
|
|
|
# Root layout for this view uses a stacked layout
|
|
self.stack = QStackedLayout()
|
|
container = QWidget(self)
|
|
container.setLayout(self.stack)
|
|
self.set_content(container)
|
|
|
|
# --- Page 0: Settings page (inline form)
|
|
self.settings_page = QWidget()
|
|
sp_layout = QVBoxLayout(self.settings_page)
|
|
sp_layout.setContentsMargins(16, 16, 16, 16)
|
|
sp_layout.setSpacing(12)
|
|
|
|
title = QLabel("Select device and signal for the waveform plot:", parent=self.settings_page)
|
|
self.device_edit = DeviceComboBox(parent=self.settings_page)
|
|
self.device_edit.insertItem(0, "")
|
|
self.device_edit.setEditable(True)
|
|
self.device_edit.setCurrentIndex(0)
|
|
|
|
self.entry_edit = SignalComboBox(parent=self.settings_page)
|
|
self.entry_edit.include_config_signals = False
|
|
self.entry_edit.insertItem(0, "")
|
|
self.entry_edit.setEditable(True)
|
|
self.device_edit.currentTextChanged.connect(self.entry_edit.set_device)
|
|
self.device_edit.device_reset.connect(self.entry_edit.reset_selection)
|
|
|
|
form = QFormLayout()
|
|
form.addRow(title)
|
|
form.addRow("Device", self.device_edit)
|
|
form.addRow("Signal", self.entry_edit)
|
|
|
|
btn_row = QHBoxLayout()
|
|
ok_btn = QPushButton("OK", parent=self.settings_page)
|
|
cancel_btn = QPushButton("Cancel", parent=self.settings_page)
|
|
btn_row.addStretch(1)
|
|
btn_row.addWidget(cancel_btn)
|
|
btn_row.addWidget(ok_btn)
|
|
|
|
sp_layout.addLayout(form)
|
|
sp_layout.addLayout(btn_row)
|
|
|
|
# --- Page 1: Waveform page
|
|
self.waveform_page = QWidget()
|
|
wf_layout = QVBoxLayout(self.waveform_page)
|
|
wf_layout.setContentsMargins(0, 0, 0, 0)
|
|
self.waveform = Waveform(parent=self.waveform_page)
|
|
wf_layout.addWidget(self.waveform)
|
|
|
|
# --- Page 2: Exit confirmation page (inline)
|
|
self.confirm_page = QWidget()
|
|
cp_layout = QVBoxLayout(self.confirm_page)
|
|
cp_layout.setContentsMargins(16, 16, 16, 16)
|
|
cp_layout.setSpacing(12)
|
|
qlabel = QLabel("Do you want to switch views and clear the plot?", parent=self.confirm_page)
|
|
cp_buttons = QHBoxLayout()
|
|
no_btn = QPushButton("No", parent=self.confirm_page)
|
|
yes_btn = QPushButton("Yes", parent=self.confirm_page)
|
|
cp_buttons.addStretch(1)
|
|
cp_buttons.addWidget(no_btn)
|
|
cp_buttons.addWidget(yes_btn)
|
|
cp_layout.addWidget(qlabel)
|
|
cp_layout.addLayout(cp_buttons)
|
|
|
|
# Add pages to the stack
|
|
self.stack.addWidget(self.settings_page) # index 0
|
|
self.stack.addWidget(self.waveform_page) # index 1
|
|
self.stack.addWidget(self.confirm_page) # index 2
|
|
|
|
# Wire settings buttons
|
|
ok_btn.clicked.connect(self._apply_settings_and_show_waveform)
|
|
cancel_btn.clicked.connect(self._show_waveform_without_changes)
|
|
|
|
# Prepare result holder for the inline confirmation
|
|
self._exit_choice_yes = None
|
|
yes_btn.clicked.connect(lambda: self._exit_reply(True))
|
|
no_btn.clicked.connect(lambda: self._exit_reply(False))
|
|
|
|
@SafeSlot()
|
|
def on_enter(self) -> None:
|
|
# Always start on the settings page when entering
|
|
self.stack.setCurrentIndex(0)
|
|
|
|
@SafeSlot()
|
|
def on_exit(self) -> bool:
|
|
# Show inline confirmation page and synchronously wait for a choice
|
|
# -> trick to make the choice blocking, however popup would be cleaner solution
|
|
self._exit_choice_yes = None
|
|
self.stack.setCurrentIndex(2)
|
|
loop = QEventLoop()
|
|
self._exit_loop = loop
|
|
loop.exec_()
|
|
|
|
if self._exit_choice_yes:
|
|
self.waveform.clear_all()
|
|
return True
|
|
# Revert to waveform view if user cancelled switching
|
|
self.stack.setCurrentIndex(1)
|
|
return False
|
|
|
|
def _apply_settings_and_show_waveform(self):
|
|
dev = self.device_edit.currentText()
|
|
sig = self.entry_edit.currentText()
|
|
if dev and sig:
|
|
self.waveform.plot(device_y=dev, signal_y=sig)
|
|
self.stack.setCurrentIndex(1)
|
|
|
|
def _show_waveform_without_changes(self):
|
|
# Just show waveform page without plotting
|
|
self.stack.setCurrentIndex(1)
|
|
|
|
def _exit_reply(self, yes: bool):
|
|
self._exit_choice_yes = bool(yes)
|
|
if hasattr(self, "_exit_loop") and self._exit_loop.isRunning():
|
|
self._exit_loop.quit()
|