feat: merge Sample List, Reference Tools, and the queue into one dock
Reference Tools was a separate staff-only dock tabified behind Sample List and kept vanishing (saved dock state, raise order); the automation job list was a third dock for what is conceptually the same table. One tabbed dock now holds Dewar samples + Auxiliary puck (concept from aaregui2's QueuePanel): the dewar table doubles as the queue view with the queue buttons and an Unmount button in a row beneath it, SampleQueuePanel lives on hidden as the queue engine, and queue/flag membership is edited via right-click or row drags onto the Queued and Flagged chips. Column 0 is a frozen #+status cell (mounted > queued > flagged > measured, tints context-dependent per chip filter); non-staff see Auxiliary greyed out with a Staff-only popup instead of hidden. Ctrl+L/Ctrl+R raise the Dewar/Auxiliary tab. The Sample List and log pop-outs are fully operational second views on shared models. Riders: MainWindow installs WheelValueGuard, wires the camera session badge, and moves samcam/monochromator/ABR/beam-config into the left column's Beamline group (BeamlineControls keeps only zoom/light/omega/smargon). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
+470
-108
@@ -12,16 +12,21 @@ from aarecommon.models.models import (
|
||||
BeamlineStateEnum,
|
||||
DAQStatusModel,
|
||||
SampleShortInfoList,
|
||||
SessionsStateEnum,
|
||||
TokenData,
|
||||
)
|
||||
from PySide6.QtCore import QEvent, QSettings, Qt, QTimer, Signal, Slot
|
||||
from PySide6.QtGui import QAction, QActionGroup, QGuiApplication, QKeySequence
|
||||
from PySide6.QtGui import QAction, QActionGroup, QColor, QCursor, QGuiApplication, QKeySequence
|
||||
from PySide6.QtWidgets import (
|
||||
QApplication,
|
||||
QCheckBox,
|
||||
QDockWidget,
|
||||
QFrame,
|
||||
QGraphicsColorizeEffect,
|
||||
QHBoxLayout,
|
||||
QMainWindow,
|
||||
QMessageBox,
|
||||
QPushButton,
|
||||
QScrollArea,
|
||||
QSizePolicy,
|
||||
QStackedWidget,
|
||||
@@ -38,7 +43,8 @@ from aare.gui.constants import LOGGER_NAME
|
||||
from aare.gui.models.gui_state_manager import UIStateManager
|
||||
from aare.gui.panels.automation_panel import AutomationProgressWidget
|
||||
from aare.gui.panels.axis_video_panel import AxisVideoPanel
|
||||
from aare.gui.panels.beamline_controls import BeamlineControls
|
||||
from aare.gui.panels.abr_tweak_panel import AbrTweakWidget
|
||||
from aare.gui.panels.beamline_controls import BeamConfigPanel, BeamlineControls
|
||||
from aare.gui.panels.beamline_recovery_panel import BeamlineRecoveryDialog
|
||||
from aare.gui.panels.beamline_state_panel import BeamlineStatePanel
|
||||
from aare.gui.panels.compact_automation_panel import CompactAutomationPanel
|
||||
@@ -50,10 +56,11 @@ from aare.gui.panels.local_contact_panel import LocalContactDialog
|
||||
|
||||
# panels
|
||||
from aare.gui.panels.log_panel import LogDock
|
||||
from aare.gui.panels.loop_centering_panel import LoopCenteringPanel
|
||||
from aare.gui.panels.monochromator_panel import MonochromatorPanel
|
||||
from aare.gui.panels.portrait_mode import PortraitModePanel
|
||||
from aare.gui.panels.prediction_metrics_panel import PredictionMetricsPanel
|
||||
from aare.gui.panels.reference_tools_panel import ReferenceToolsPanel
|
||||
from aare.gui.panels.samcam_panel import SamcamPanel
|
||||
from aare.gui.panels.sample_queue_panel import SampleQueuePanel
|
||||
from aare.gui.panels.smargon_trace_panel import SmargonTracePanel
|
||||
from aare.gui.panels.target_stability_panel import TargetStabilityPanel
|
||||
@@ -63,7 +70,13 @@ from aare.gui.panels.tell_sample_panel import TellSamplePanel
|
||||
from aare.gui.scan_logic.raster_grid_manager import RasterGridManager
|
||||
from aare.gui.scan_logic.rotation_scan_manager import RotationScanManager
|
||||
from aare.gui.scan_logic.sample_mount_logic import SampleMountLogic
|
||||
from aare.gui.styles import BACKGROUND, THEME_ORIGINAL, THEME_PORTRAIT, build_app_stylesheet
|
||||
from aare.gui.styles import (
|
||||
BACKGROUND,
|
||||
DOCK_CONTENT_LEFT_PAD,
|
||||
THEME_ORIGINAL,
|
||||
THEME_PORTRAIT,
|
||||
build_app_stylesheet,
|
||||
)
|
||||
|
||||
# Threads
|
||||
from aare.gui.threads.axis_video_thread import VideoThread
|
||||
@@ -71,6 +84,8 @@ from aare.gui.threads.daq_worker import DAQWorker
|
||||
from aare.gui.threads.jfjoch_viewer import JFJochDBusClient
|
||||
from aare.gui.threads.prediction_subscriber import PredictionSubscriber
|
||||
from aare.gui.tutorials.controls_help_dialog import ControlsHelpDialog
|
||||
from aare.gui.widgets.popout_window import DockTitleBar, PopoutWindow
|
||||
from aare.gui.widgets.wheel_value_guard import WheelValueGuard
|
||||
|
||||
# Tutorials
|
||||
from aare.gui.tutorials.tutorial_actions import TutorialActionExecutor
|
||||
@@ -88,7 +103,7 @@ from aare.gui.widgets.camera_image import SampleCameraImageLabel
|
||||
from aare.gui.widgets.message_box import precondition_check
|
||||
from aare.gui.widgets.no_wheel_scroll_area import NoWheelScrollArea
|
||||
from aare.gui.widgets.status_bar import StatusBar
|
||||
from aare.gui.widgets.title_label import tighten_column
|
||||
from aare.gui.widgets.title_label import TitleLabel, tighten_column
|
||||
from aare.gui.widgets.video_image import VideoGraphicsView
|
||||
|
||||
logger = setup_logger(LOGGER_NAME)
|
||||
@@ -129,7 +144,6 @@ class MainWindow(QMainWindow):
|
||||
self._cleanup_done = False
|
||||
self._default_window_state = None
|
||||
self._pre_automation_window_state = None
|
||||
self._pre_automation_ref_tools_visible = False
|
||||
self._pre_automation_left_column_visible = True
|
||||
self._pre_automation_right_column_visible = True
|
||||
self._in_compact_automation_view = False
|
||||
@@ -159,6 +173,14 @@ class MainWindow(QMainWindow):
|
||||
self._tutorial_event_bus = TutorialEventBus(self)
|
||||
self._tutorial_text_resolver = DictionaryTextResolver(MANUAL_MOUNT_TUTORIAL)
|
||||
self.state_manager = UIStateManager("PSI", "AareGUI")
|
||||
|
||||
# Wheel safety: sliders/spin boxes/combos only react to the wheel
|
||||
# while the right mouse button is held; a bare wheel just scrolls
|
||||
# the page — it can never nudge a value or move a motor.
|
||||
self._wheel_value_guard = WheelValueGuard(self)
|
||||
app_instance = QApplication.instance()
|
||||
if app_instance is not None:
|
||||
app_instance.installEventFilter(self._wheel_value_guard)
|
||||
self.viewer = JFJochDBusClient()
|
||||
|
||||
try:
|
||||
@@ -239,17 +261,80 @@ class MainWindow(QMainWindow):
|
||||
s=geom, parent=self.left_column, raster_mgr=self.raster, diffraction=diffraction
|
||||
)
|
||||
|
||||
self.loop_centering = LoopCenteringPanel(parent=self.left_column)
|
||||
|
||||
# The beamline state strip lives in a bottom toolbar row (created
|
||||
# after the docks), not in the left column. Always visible.
|
||||
self.beamline_state_panel = BeamlineStatePanel(parent=self)
|
||||
|
||||
self.left_column_layout.addWidget(self.data_collection)
|
||||
self.left_column_layout.addWidget(self.loop_centering)
|
||||
# Beamline / Experiment as tabs (like the Dewar samples dock) instead
|
||||
# of two stacked banner groups; the pages keep their banner children.
|
||||
# documentMode: no pane frame, so the fixed-width panels aren't inset.
|
||||
self.left_column_tabs = QTabWidget(self.left_column)
|
||||
self.left_column_tabs.setDocumentMode(True)
|
||||
# documentMode draws a grey base line across the bar's full width.
|
||||
self.left_column_tabs.tabBar().setDrawBase(False)
|
||||
|
||||
beamline_page = QWidget()
|
||||
beamline_layout = QVBoxLayout(beamline_page)
|
||||
beamline_layout.setContentsMargins(0, 0, 0, 0)
|
||||
self.samcam = SamcamPanel(beamline_page)
|
||||
if self._decoded_token.staff:
|
||||
self.monochromator_panel = MonochromatorPanel(beamline_page)
|
||||
self.abr_tweak = AbrTweakWidget(beamline_page)
|
||||
self.beam_config = BeamConfigPanel(beamline_page)
|
||||
self.beam_mark = self.beam_config.beam_mark
|
||||
self.beam_center = self.beam_config.beam_center
|
||||
self.beam_size = self.beam_config.beam_size
|
||||
beamline_layout.addWidget(self.monochromator_panel)
|
||||
beamline_layout.addWidget(self.abr_tweak)
|
||||
beamline_layout.addWidget(self.beam_config)
|
||||
# Samcam last: the beam panels are the ones tweaked most.
|
||||
beamline_layout.addWidget(self.samcam)
|
||||
beamline_layout.addStretch()
|
||||
tighten_column(beamline_layout)
|
||||
|
||||
experiment_page = QWidget()
|
||||
experiment_layout = QVBoxLayout(experiment_page)
|
||||
experiment_layout.setContentsMargins(0, 0, 0, 0)
|
||||
experiment_layout.addWidget(self.data_collection)
|
||||
experiment_layout.addStretch()
|
||||
tighten_column(experiment_layout)
|
||||
|
||||
self.left_column_tabs.addTab(beamline_page, "Beamline")
|
||||
self.left_column_tabs.addTab(experiment_page, "Experiment")
|
||||
|
||||
# Only the visible page counts toward the height — same trick as the
|
||||
# content stack below, else the taller page pads the other tab.
|
||||
def _only_current_left_tab_counts(index: int) -> None:
|
||||
for i in range(self.left_column_tabs.count()):
|
||||
page = self.left_column_tabs.widget(i)
|
||||
vertical = (
|
||||
QSizePolicy.Policy.Preferred if i == index else QSizePolicy.Policy.Ignored
|
||||
)
|
||||
page.setSizePolicy(QSizePolicy.Policy.Preferred, vertical)
|
||||
|
||||
self.left_column_tabs.currentChanged.connect(_only_current_left_tab_counts)
|
||||
_only_current_left_tab_counts(self.left_column_tabs.currentIndex())
|
||||
|
||||
# Dewar-tabs look: first banner flush under the tab bar (no top
|
||||
# margin) and the tab row starting at the banners' left edge. The
|
||||
# Experiment side needs two levels: the frame AND its first panel.
|
||||
beamline_first = (
|
||||
self.monochromator_panel if self._decoded_token.staff else self.samcam
|
||||
)
|
||||
for first in (
|
||||
beamline_first,
|
||||
self.data_collection,
|
||||
self.data_collection.file_path_panel,
|
||||
):
|
||||
m = first.layout().contentsMargins()
|
||||
first.layout().setContentsMargins(m.left(), 0, m.right(), m.bottom())
|
||||
self.left_column_tabs.setStyleSheet(
|
||||
"QTabWidget::tab-bar {"
|
||||
f" left: {self.samcam.layout().contentsMargins().left()}px; }}"
|
||||
)
|
||||
|
||||
self.left_column_layout.addWidget(self.left_column_tabs)
|
||||
self.left_column_layout.addStretch()
|
||||
# Same universal banner gap as inside the panel columns.
|
||||
tighten_column(self.left_column_layout)
|
||||
|
||||
top_widget_layout.addWidget(self.collection_controls_scroll)
|
||||
self.collection_controls_scroll.setWidget(self.left_column)
|
||||
@@ -257,9 +342,14 @@ class MainWindow(QMainWindow):
|
||||
Qt.ScrollBarPolicy.ScrollBarAlwaysOff
|
||||
)
|
||||
self.collection_controls_scroll.setWidgetResizable(True)
|
||||
self.collection_controls_scroll.setFixedWidth(
|
||||
max(self.data_collection.set_width, self.loop_centering.sizeHint().width()) + 10
|
||||
)
|
||||
# No frame: its border drew a line above the tab bar (Dewar tabs have
|
||||
# none). Freeze the inner column width: widgetResizable makes it track
|
||||
# the viewport, so the scrollbar appearing used to re-flow every
|
||||
# banner. Fixed width + a permanent 10px scrollbar gutter means the
|
||||
# scrollbar pops into spare space and nothing moves.
|
||||
self.collection_controls_scroll.setFrameShape(QFrame.Shape.NoFrame)
|
||||
self.left_column.setFixedWidth(self.data_collection.set_width)
|
||||
self.collection_controls_scroll.setFixedWidth(self.data_collection.set_width + 10)
|
||||
|
||||
self.video_tab = QTabWidget(parent=top_widget)
|
||||
|
||||
@@ -341,9 +431,7 @@ class MainWindow(QMainWindow):
|
||||
|
||||
self.beamline_controls_scroll = NoWheelScrollArea(top_widget)
|
||||
|
||||
self.beamline = BeamlineControls(
|
||||
self.beamline_controls_scroll, staff=self._decoded_token.staff
|
||||
)
|
||||
self.beamline = BeamlineControls(self.beamline_controls_scroll)
|
||||
top_widget_layout.addWidget(self.beamline_controls_scroll)
|
||||
self.beamline_controls_scroll.setWidget(self.beamline)
|
||||
# Resizable so the column shrinks when panels collapse; without it the
|
||||
@@ -352,7 +440,9 @@ class MainWindow(QMainWindow):
|
||||
self.beamline_controls_scroll.setHorizontalScrollBarPolicy(
|
||||
Qt.ScrollBarPolicy.ScrollBarAlwaysOff
|
||||
)
|
||||
self.beamline_controls_scroll.setFixedWidth(self.beamline.set_width + 10)
|
||||
# Same gutter math as the left column: 10px scrollbar + 2px frame, so
|
||||
# the fixed-width controls are never clipped when the scrollbar shows.
|
||||
self.beamline_controls_scroll.setFixedWidth(self.beamline.set_width + 12)
|
||||
|
||||
self.tell_samples = TellSamplePanel(samples=SampleShortInfoList(s=[]))
|
||||
self.ref_tools_panel = ReferenceToolsPanel(samples=SampleShortInfoList(s=[]))
|
||||
@@ -368,31 +458,85 @@ class MainWindow(QMainWindow):
|
||||
)
|
||||
self.compact_automation_panel.annotation_selected.connect(self._handle_compact_annotation)
|
||||
|
||||
# One dock for both lists: the old tabified Reference Tools dock was
|
||||
# staff-only and hid behind the Sample List tab, so it "sometimes"
|
||||
# showed. aaregui2 concept: one panel, Dewar + Auxiliary-puck tabs
|
||||
# that always travel together, and the dewar table doubles as the
|
||||
# queue view (status tints), so the automation controls sit under it
|
||||
# and the Automation list dock is gone. SampleQueuePanel stays alive,
|
||||
# hidden, as the queue engine; its buttons are reparented here so all
|
||||
# their existing wiring keeps working.
|
||||
dewar_tab = QWidget()
|
||||
dewar_layout = QVBoxLayout(dewar_tab)
|
||||
dewar_layout.setContentsMargins(0, 0, 0, 0)
|
||||
dewar_layout.setSpacing(2)
|
||||
dewar_layout.addWidget(self.tell_samples)
|
||||
|
||||
self.quick_unmount_button = QPushButton("⏏ Unmount", dewar_tab)
|
||||
self.quick_unmount_button.clicked.connect(lambda: self._on_manual_unmount_requested())
|
||||
|
||||
automation_row = QHBoxLayout()
|
||||
for w in (
|
||||
self.job_list_panel.play_button,
|
||||
self.job_list_panel.remove_button,
|
||||
self.job_list_panel.clear_button,
|
||||
self.quick_unmount_button,
|
||||
self.job_list_panel.park_and_dry_when_cleared,
|
||||
self.job_list_panel.pause_on_conditions_cb,
|
||||
):
|
||||
automation_row.addWidget(w)
|
||||
dewar_layout.addLayout(automation_row)
|
||||
|
||||
# "Remove selected" now unqueues the dewar-table selection — the
|
||||
# queue's own table is no longer displayed.
|
||||
self.job_list_panel.remove_button.clicked.disconnect(
|
||||
self.job_list_panel.remove_selected_samples
|
||||
)
|
||||
self.job_list_panel.remove_button.clicked.connect(self._remove_selected_from_queue)
|
||||
self.job_list_panel.hide()
|
||||
|
||||
self.sample_lists_tabs = QTabWidget()
|
||||
self.sample_lists_tabs.addTab(dewar_tab, "Dewar samples")
|
||||
self.sample_lists_tabs.addTab(self.ref_tools_panel, "Auxiliary puck")
|
||||
# Non-staff never get reference-tools data (the DAQ connect below is
|
||||
# staff-gated). Grey the tab out instead of hiding it: every role sees
|
||||
# the same view, and clicking the locked tab says why it is locked.
|
||||
if not self._decoded_token.staff:
|
||||
self.sample_lists_tabs.setTabEnabled(1, False)
|
||||
self.sample_lists_tabs.setTabToolTip(1, "Staff only")
|
||||
# Disabled tabs are skipped by tabBarClicked's hit-test, so the
|
||||
# click is caught in eventFilter via the geometric tabAt() instead.
|
||||
self.sample_lists_tabs.tabBar().installEventFilter(self)
|
||||
|
||||
# Wrapper for the left inset: QTabWidget ignores its own contents
|
||||
# margins for the tab bar, so the padding lives one level up. Aligns
|
||||
# the panel's left edge with the left column above (Loop centering).
|
||||
sample_lists_wrap = QWidget()
|
||||
sample_lists_wrap_layout = QVBoxLayout(sample_lists_wrap)
|
||||
sample_lists_wrap_layout.setContentsMargins(DOCK_CONTENT_LEFT_PAD, 0, 0, 0)
|
||||
sample_lists_wrap_layout.setSpacing(0)
|
||||
sample_lists_wrap_layout.addWidget(self.sample_lists_tabs)
|
||||
|
||||
self.tell_samples_dock = QDockWidget("Sample List", self)
|
||||
self.tell_samples_dock.setObjectName("tell_samples_dock")
|
||||
self.tell_samples_dock.setWidget(self.tell_samples)
|
||||
self.tell_samples_dock.setWidget(sample_lists_wrap)
|
||||
self.tell_samples_dock.setAllowedAreas(Qt.DockWidgetArea.BottomDockWidgetArea)
|
||||
self.addDockWidget(Qt.DockWidgetArea.BottomDockWidgetArea, self.tell_samples_dock)
|
||||
|
||||
self.ref_tools_dock = QDockWidget("Reference Tools", self)
|
||||
self.ref_tools_dock.setObjectName("ref_tools_dock")
|
||||
self.ref_tools_dock.setWidget(self.ref_tools_panel)
|
||||
self.ref_tools_dock.setAllowedAreas(Qt.DockWidgetArea.BottomDockWidgetArea)
|
||||
self.addDockWidget(Qt.DockWidgetArea.BottomDockWidgetArea, self.ref_tools_dock)
|
||||
self.tabifyDockWidget(self.ref_tools_dock, self.tell_samples_dock)
|
||||
if self._decoded_token.staff:
|
||||
self.ref_tools_dock.show()
|
||||
else:
|
||||
self.ref_tools_dock.hide()
|
||||
# No floating: popping the dock out ripped the panel from the row and
|
||||
# reshuffled the rest. The ⤢ button (in the title bar, next to ✕)
|
||||
# opens an ADDITIONAL fully-wired window; closing it changes nothing.
|
||||
self.tell_samples_dock.setFeatures(
|
||||
QDockWidget.DockWidgetFeature.DockWidgetMovable
|
||||
| QDockWidget.DockWidgetFeature.DockWidgetClosable
|
||||
)
|
||||
self._sample_popout: PopoutWindow | None = None
|
||||
self.tell_samples_dock.setTitleBarWidget(
|
||||
DockTitleBar(self.tell_samples_dock, self._open_sample_popout)
|
||||
)
|
||||
|
||||
self.sample_logic = SampleMountLogic()
|
||||
|
||||
self.job_list_dock = QDockWidget("Automation list", self)
|
||||
self.job_list_dock.setObjectName("job_list_dock")
|
||||
self.job_list_dock.setWidget(self.job_list_panel)
|
||||
self.job_list_dock.setAllowedAreas(Qt.DockWidgetArea.BottomDockWidgetArea)
|
||||
self.addDockWidget(Qt.DockWidgetArea.BottomDockWidgetArea, self.job_list_dock)
|
||||
|
||||
# Manual sample lives in the left column (DataCollectionSettings)
|
||||
# between Dataset path and Exp. Config., collapsible like its
|
||||
# neighbors — it is no longer a bottom dock.
|
||||
@@ -401,9 +545,20 @@ class MainWindow(QMainWindow):
|
||||
self.automation_progress_panel = AutomationProgressWidget()
|
||||
self.automation_progress_dock = QDockWidget("Automation progress", self)
|
||||
self.automation_progress_dock.setObjectName("automation_progress_dock")
|
||||
self.automation_progress_dock.setWidget(self.automation_progress_panel)
|
||||
# Scroll host: the panel's ~420px minimum otherwise dictates the whole
|
||||
# bottom row's height and squeezes the Beamline column into a scrollbar.
|
||||
automation_scroll = NoWheelScrollArea(self.automation_progress_dock)
|
||||
automation_scroll.setWidget(self.automation_progress_panel)
|
||||
automation_scroll.setWidgetResizable(True)
|
||||
automation_scroll.setFrameShape(QFrame.Shape.NoFrame)
|
||||
self.automation_progress_dock.setWidget(automation_scroll)
|
||||
self.automation_progress_dock.setAllowedAreas(Qt.DockWidgetArea.BottomDockWidgetArea)
|
||||
self.addDockWidget(Qt.DockWidgetArea.BottomDockWidgetArea, self.automation_progress_dock)
|
||||
# Same title-bar icons (⤢ pop-out + ✕) as Sample List / Console Log.
|
||||
self._automation_popout: PopoutWindow | None = None
|
||||
self.automation_progress_dock.setTitleBarWidget(
|
||||
DockTitleBar(self.automation_progress_dock, self._open_automation_popout)
|
||||
)
|
||||
|
||||
self.face_panel = FaceDetectionPanel()
|
||||
self.face_panel_dock = QDockWidget("Face detection", self)
|
||||
@@ -560,6 +715,19 @@ class MainWindow(QMainWindow):
|
||||
self.create_menu_bar()
|
||||
self._update_view_mode_actions()
|
||||
self._setup_global_shortcuts()
|
||||
# Default bottom-dock height: the sample list used to grab ~40% of the
|
||||
# window and squeeze the Beamline column behind a scrollbar. Before
|
||||
# the default-state capture so "reset layout" gets it too; a saved
|
||||
# user layout (restored below) still wins.
|
||||
self.resizeDocks(
|
||||
[self.tell_samples_dock, self.log_dock], [240, 240], Qt.Orientation.Vertical
|
||||
)
|
||||
# Equal oversized requests -> Qt distributes proportionally = 50/50.
|
||||
self.resizeDocks(
|
||||
[self.tell_samples_dock, self.automation_progress_dock],
|
||||
[10000, 10000],
|
||||
Qt.Orientation.Horizontal,
|
||||
)
|
||||
self._capture_default_window_state()
|
||||
self._restore_window_state()
|
||||
|
||||
@@ -623,15 +791,20 @@ class MainWindow(QMainWindow):
|
||||
if self._decoded_token.staff:
|
||||
self.daq.reference_tools.connect(self.ref_tools_panel.new_list)
|
||||
|
||||
self.beamline.samcam.changed.connect(self.daq.samcam_settings)
|
||||
self.beamline.samcam.screenshot_requested.connect(self.daq.send_screenshot_db)
|
||||
self.beamline.samcam.save_beam_location_setting.connect(
|
||||
self.samcam.changed.connect(self.daq.samcam_settings)
|
||||
self.samcam.screenshot_requested.connect(self.daq.send_screenshot_db)
|
||||
self.samcam.save_beam_location_setting.connect(
|
||||
self.daq.save_beam_location_camera_setting
|
||||
)
|
||||
self.loop_centering.find_tip.clicked.connect(self.daq.center_loop)
|
||||
self.loop_centering.bounding_box.clicked.connect(self.daq.ml_bounding_box)
|
||||
self.data_collection.find_tip.clicked.connect(self.daq.center_loop)
|
||||
self.data_collection.bounding_box.clicked.connect(self.daq.ml_bounding_box)
|
||||
self.daq.raster_generated_by_ml.connect(self.raster.update_active_grid_request)
|
||||
|
||||
# Clicking the big session badge (SESSION VACANT / Guest Mode) opens
|
||||
# the grab/request menu right at the cursor.
|
||||
self.sample_camera.session_badge_clicked.connect(
|
||||
lambda: self.status_bar.show_session_menu(QCursor.pos())
|
||||
)
|
||||
self.sample_camera.smargon.connect(self.daq.move_smargon)
|
||||
self.beamline.smargon_panel.smargon.connect(self.daq.move_smargon)
|
||||
self.sample_camera.samcam_updated.connect(self.daq.samcam_settings)
|
||||
@@ -647,36 +820,36 @@ class MainWindow(QMainWindow):
|
||||
self.beamline.illumination_panel.back_light.connect(self.daq.back_light)
|
||||
|
||||
if self._decoded_token.staff:
|
||||
self.beamline.monochromator_panel.mono_pitch_scan.connect(self.daq.mono_pitch_scan)
|
||||
self.beamline.monochromator_panel.change_energy.connect(self.daq.change_energy)
|
||||
self.beamline.abr_tweak.abr_tweak.connect(self.daq.abr_tweak)
|
||||
self.beamline.abr_tweak.abr_save.connect(self.daq.abr_save)
|
||||
self.beamline.abr_tweak.abr_goto_meas.connect(self.daq.abr_goto_meas)
|
||||
self.monochromator_panel.mono_pitch_scan.connect(self.daq.mono_pitch_scan)
|
||||
self.monochromator_panel.change_energy.connect(self.daq.change_energy)
|
||||
self.abr_tweak.abr_tweak.connect(self.daq.abr_tweak)
|
||||
self.abr_tweak.abr_save.connect(self.daq.abr_save)
|
||||
self.abr_tweak.abr_goto_meas.connect(self.daq.abr_goto_meas)
|
||||
|
||||
self.beamline.beam_mark.beam_mark_clear.connect(self.daq.beam_mark_clear)
|
||||
self.beam_mark.beam_mark_clear.connect(self.daq.beam_mark_clear)
|
||||
self.sample_camera.update_beam_mark.connect(self.daq.beam_mark_add)
|
||||
self.beamline.beam_center.beam_center.connect(self.daq.beam_center)
|
||||
self.beamline.beam_size.beam_size.connect(self.daq.beam_size_mm)
|
||||
self.beam_center.beam_center.connect(self.daq.beam_center)
|
||||
self.beam_size.beam_size.connect(self.daq.beam_size_mm)
|
||||
|
||||
self.sample_camera.load_image.connect(self.raster.load_image)
|
||||
self.sample_camera.switch_raster_grid.connect(self.data_collection.switch_to_raster)
|
||||
self.beamline.samcam.show_detections_changed.connect(self.sample_camera.set_show_detections)
|
||||
self.beamline.samcam.show_detection_polygons_changed.connect(
|
||||
self.samcam.show_detections_changed.connect(self.sample_camera.set_show_detections)
|
||||
self.samcam.show_detection_polygons_changed.connect(
|
||||
self.sample_camera.set_show_detection_polygons
|
||||
)
|
||||
self.beamline.samcam.show_target_point_changed.connect(
|
||||
self.samcam.show_target_point_changed.connect(
|
||||
self.sample_camera.set_show_target_point
|
||||
)
|
||||
self.beamline.samcam.show_target_coordinates_changed.connect(
|
||||
self.samcam.show_target_coordinates_changed.connect(
|
||||
self.sample_camera.set_show_target_coordinates
|
||||
)
|
||||
self.beamline.samcam.show_overlay_legend_changed.connect(
|
||||
self.samcam.show_overlay_legend_changed.connect(
|
||||
self.sample_camera.set_show_overlay_legend
|
||||
)
|
||||
self.beamline.samcam.compact_overlay_legend_changed.connect(
|
||||
self.samcam.compact_overlay_legend_changed.connect(
|
||||
self.sample_camera.set_compact_overlay_legend
|
||||
)
|
||||
self.beamline.samcam.target_color_changed.connect(self.sample_camera.set_target_color)
|
||||
self.samcam.target_color_changed.connect(self.sample_camera.set_target_color)
|
||||
self._restore_samcam_overlay_settings()
|
||||
|
||||
sample_feed_addr = pred_zmq_addr or zmq_addr
|
||||
@@ -757,6 +930,16 @@ class MainWindow(QMainWindow):
|
||||
self.ref_tools_panel.mount.connect(self._on_manual_mount_requested)
|
||||
self.ref_tools_panel.unmount.connect(self._on_manual_unmount_requested)
|
||||
|
||||
# Dewar table doubles as the queue view: right-click edits queue
|
||||
# membership, and every queue change repaints the status tints.
|
||||
self.tell_samples.add_to_queue.connect(
|
||||
lambda lst: self.job_list_panel.queue_samples(lst.s, replace=False)
|
||||
)
|
||||
self.tell_samples.remove_from_queue.connect(
|
||||
lambda lst: self.job_list_panel.remove_samples([s.db_id for s in lst.s])
|
||||
)
|
||||
self.job_list_panel.table_model.modelReset.connect(self._sync_queue_row_tints)
|
||||
|
||||
self.data_collection.raster.grid_size_updated.connect(self.raster.update_grid_size)
|
||||
self.data_collection.raster.exp_time_updated.connect(self.raster.update_exposure_time)
|
||||
self.data_collection.raster.transmission_updated.connect(self.raster.update_transmission)
|
||||
@@ -833,13 +1016,13 @@ class MainWindow(QMainWindow):
|
||||
self.daq.update.connect(self.prediction_thread.update_daq_status)
|
||||
|
||||
if self._decoded_token.staff:
|
||||
self.daq.update.connect(self.beamline.monochromator_panel.update_daq_status)
|
||||
self.daq.update.connect(self.beamline.beam_size.update_daq_status)
|
||||
self.daq.update.connect(self.beamline.beam_center.update_daq_status)
|
||||
self.daq.update.connect(self.beamline.abr_tweak.update_daq_status)
|
||||
self.daq.update.connect(self.beamline.beam_mark.update_daq_status)
|
||||
self.daq.update.connect(self.monochromator_panel.update_daq_status)
|
||||
self.daq.update.connect(self.beam_size.update_daq_status)
|
||||
self.daq.update.connect(self.beam_center.update_daq_status)
|
||||
self.daq.update.connect(self.abr_tweak.update_daq_status)
|
||||
self.daq.update.connect(self.beam_mark.update_daq_status)
|
||||
self.daq.update.connect(self.status_bar.update_daq_status)
|
||||
self.daq.update.connect(self.beamline.samcam.update_daq_status)
|
||||
self.daq.update.connect(self.samcam.update_daq_status)
|
||||
self.daq.update.connect(self.beamline.zoom_panel.update_daq_status)
|
||||
|
||||
self.daq.update.connect(self.sample_logic.update_daq_status)
|
||||
@@ -859,6 +1042,7 @@ class MainWindow(QMainWindow):
|
||||
self.daq.raster_scan_completed.connect(self.raster.grid_scan_completed)
|
||||
|
||||
self.daq.automated_scan_done.connect(self.job_list_panel.automated_scan_done)
|
||||
self.daq.automated_scan_done.connect(self._mark_scan_result)
|
||||
self.daq.automation_critical_failure.connect(self._on_automation_critical_failure)
|
||||
self.daq.manual_collection_critical_failure.connect(
|
||||
self._on_manual_collection_critical_failure
|
||||
@@ -902,21 +1086,30 @@ class MainWindow(QMainWindow):
|
||||
self._shortcut_raise_sample_list = QAction("Raise sample list", self)
|
||||
self._shortcut_raise_sample_list.setShortcut(QKeySequence("Ctrl+L"))
|
||||
self._shortcut_raise_sample_list.triggered.connect(
|
||||
lambda: (self.tell_samples_dock.setVisible(True), self.tell_samples_dock.raise_())
|
||||
lambda: (
|
||||
self.tell_samples_dock.setVisible(True),
|
||||
self.tell_samples_dock.raise_(),
|
||||
self.sample_lists_tabs.setCurrentIndex(0),
|
||||
)
|
||||
)
|
||||
self.addAction(self._shortcut_raise_sample_list)
|
||||
|
||||
# Reference tools now live inside the Sample List dock as the
|
||||
# Auxiliary-puck tab — Ctrl+R raises the dock on that tab.
|
||||
self._shortcut_raise_reference_tools_list = QAction("Raise reference tools", self)
|
||||
self._shortcut_raise_reference_tools_list.setShortcut(QKeySequence("Ctrl+R"))
|
||||
self._shortcut_raise_reference_tools_list.triggered.connect(
|
||||
lambda: (self.ref_tools_dock.setVisible(True), self.ref_tools_dock.raise_())
|
||||
)
|
||||
self._shortcut_raise_reference_tools_list.triggered.connect(self._raise_reference_tools)
|
||||
self.addAction(self._shortcut_raise_reference_tools_list)
|
||||
|
||||
# The automation controls live under the Dewar samples tab now.
|
||||
self._shortcut_raise_job_list = QAction("Raise automation list", self)
|
||||
self._shortcut_raise_job_list.setShortcut(QKeySequence("Ctrl+J"))
|
||||
self._shortcut_raise_job_list.triggered.connect(
|
||||
lambda: (self.job_list_dock.setVisible(True), self.job_list_dock.raise_())
|
||||
lambda: (
|
||||
self.tell_samples_dock.setVisible(True),
|
||||
self.tell_samples_dock.raise_(),
|
||||
self.sample_lists_tabs.setCurrentIndex(0),
|
||||
)
|
||||
)
|
||||
self.addAction(self._shortcut_raise_job_list)
|
||||
|
||||
@@ -952,6 +1145,139 @@ class MainWindow(QMainWindow):
|
||||
)
|
||||
self.addAction(self._shortcut_console_log)
|
||||
|
||||
@Slot()
|
||||
def _sync_queue_row_tints(self) -> None:
|
||||
self.tell_samples.table_model.set_queued_ids(
|
||||
sample.db_id for sample in self.job_list_panel.table_model.samples
|
||||
)
|
||||
|
||||
@Slot()
|
||||
def _remove_selected_from_queue(self) -> None:
|
||||
self._unqueue_panel_selection(self.tell_samples)
|
||||
|
||||
def _unqueue_panel_selection(self, panel: TellSamplePanel) -> None:
|
||||
rows = panel.table_view.selectionModel().selectedRows()
|
||||
db_ids = [panel.table_model.get_id(index.row()).db_id for index in rows]
|
||||
if db_ids:
|
||||
self.job_list_panel.remove_samples(db_ids)
|
||||
|
||||
@Slot(int, bool, str)
|
||||
def _mark_scan_result(self, db_id: int, success: bool, reply: str) -> None:
|
||||
# Display only: a failed run tints the row pale red until a later
|
||||
# success clears it. Auth errors are not the sample's fault — skip.
|
||||
if reply == "Authentication Error":
|
||||
return
|
||||
self.tell_samples.table_model.set_flagged(db_id, not success)
|
||||
|
||||
@Slot()
|
||||
def _open_sample_popout(self) -> None:
|
||||
if self._sample_popout is None:
|
||||
# Fully operational duplicate of the Sample List tab: second panel
|
||||
# instances SHARING the docked panels' models, wired to the same
|
||||
# slots — mounts, queue edits, chips and filters all work here.
|
||||
dewar_panel = TellSamplePanel(model=self.tell_samples.table_model)
|
||||
aux_panel = ReferenceToolsPanel(model=self.ref_tools_panel.table_model)
|
||||
dewar_panel.mount.connect(self._on_manual_mount_requested)
|
||||
dewar_panel.unmount.connect(self._on_manual_unmount_requested)
|
||||
dewar_panel.add_to_queue.connect(
|
||||
lambda lst: self.job_list_panel.queue_samples(lst.s, replace=False)
|
||||
)
|
||||
dewar_panel.remove_from_queue.connect(
|
||||
lambda lst: self.job_list_panel.remove_samples([s.db_id for s in lst.s])
|
||||
)
|
||||
aux_panel.mount.connect(self._on_manual_mount_requested)
|
||||
aux_panel.unmount.connect(self._on_manual_unmount_requested)
|
||||
|
||||
# One shared filter state → keep the two chip rows visually in sync.
|
||||
self.tell_samples.status_chips.buttonClicked.connect(
|
||||
lambda chip: dewar_panel.set_status_chip(chip.property("status_key"))
|
||||
)
|
||||
dewar_panel.status_chips.buttonClicked.connect(
|
||||
lambda chip: self.tell_samples.set_status_chip(chip.property("status_key"))
|
||||
)
|
||||
dewar_panel.set_status_chip(self.tell_samples.table_model.status_filter)
|
||||
|
||||
dewar_tab = QWidget()
|
||||
dewar_layout = QVBoxLayout(dewar_tab)
|
||||
dewar_layout.setContentsMargins(0, 0, 0, 0)
|
||||
dewar_layout.setSpacing(2)
|
||||
dewar_layout.addWidget(dewar_panel)
|
||||
dewar_layout.addLayout(self._clone_automation_row(dewar_panel))
|
||||
|
||||
tabs = QTabWidget()
|
||||
tabs.addTab(dewar_tab, "Dewar samples")
|
||||
tabs.addTab(aux_panel, "Auxiliary puck")
|
||||
if not self._decoded_token.staff:
|
||||
tabs.setTabEnabled(1, False)
|
||||
tabs.setTabToolTip(1, "Staff only")
|
||||
self._sample_popout = PopoutWindow("Sample List", tabs, parent=self)
|
||||
self._sample_popout.resize(1200, 500)
|
||||
self._sample_popout.show()
|
||||
self._sample_popout.raise_()
|
||||
self._sample_popout.activateWindow()
|
||||
|
||||
def _open_automation_popout(self) -> None:
|
||||
if self._automation_popout is None:
|
||||
# Mirror wired to the same feeds as the docked panel.
|
||||
panel = AutomationProgressWidget()
|
||||
self.job_list_panel.samples_in_queue_changed.connect(panel.set_samples_in_queue)
|
||||
self.job_list_panel.automation_running_changed.connect(panel.set_running)
|
||||
self.daq.automation_progress.connect(panel.set_progress)
|
||||
panel.set_samples_in_queue(len(self.job_list_panel.table_model.samples))
|
||||
panel.set_running(self.job_list_panel.is_running())
|
||||
self._automation_popout = PopoutWindow("Automation progress", panel, parent=self)
|
||||
self._automation_popout.resize(420, 520)
|
||||
self._automation_popout.show()
|
||||
self._automation_popout.raise_()
|
||||
self._automation_popout.activateWindow()
|
||||
|
||||
def _clone_automation_row(self, dewar_panel: TellSamplePanel) -> QHBoxLayout:
|
||||
"""Pop-out copy of the automation controls, driving the same queue
|
||||
engine. Run/Pause text and checkbox states stay mirrored; 'Remove
|
||||
selected' acts on the pop-out's own table selection."""
|
||||
jl = self.job_list_panel
|
||||
run_button = QPushButton(jl.play_button.text())
|
||||
run_button.clicked.connect(jl.run)
|
||||
jl.automation_running_changed.connect(
|
||||
lambda running: run_button.setText("⏸ Pause" if running else "▶ Run")
|
||||
)
|
||||
remove_button = QPushButton("🗑 Remove selected")
|
||||
remove_button.clicked.connect(lambda: self._unqueue_panel_selection(dewar_panel))
|
||||
clear_button = QPushButton("✖ Clear list")
|
||||
clear_button.clicked.connect(jl.clear)
|
||||
unmount_button = QPushButton("⏏ Unmount")
|
||||
unmount_button.clicked.connect(lambda: self._on_manual_unmount_requested())
|
||||
|
||||
row = QHBoxLayout()
|
||||
for button in (run_button, remove_button, clear_button, unmount_button):
|
||||
row.addWidget(button)
|
||||
for source in (jl.park_and_dry_when_cleared, jl.pause_on_conditions_cb):
|
||||
clone = QCheckBox(source.text())
|
||||
clone.setToolTip(source.toolTip())
|
||||
clone.setChecked(source.isChecked())
|
||||
# setChecked with an unchanged value emits nothing, so the
|
||||
# cross-connection cannot loop.
|
||||
clone.toggled.connect(source.setChecked)
|
||||
source.toggled.connect(clone.setChecked)
|
||||
row.addWidget(clone)
|
||||
return row
|
||||
|
||||
def _show_reference_tools_staff_only_popup(self) -> None:
|
||||
QMessageBox.information(
|
||||
self,
|
||||
"Staff only",
|
||||
"The Auxiliary puck (reference tools) view is available to staff accounts only.",
|
||||
)
|
||||
|
||||
@Slot()
|
||||
def _raise_reference_tools(self) -> None:
|
||||
if not self._decoded_token.staff:
|
||||
self._show_reference_tools_staff_only_popup()
|
||||
return
|
||||
self.tell_samples_dock.setVisible(True)
|
||||
self.tell_samples_dock.raise_()
|
||||
self.sample_lists_tabs.setCurrentIndex(1)
|
||||
|
||||
def _return_to_main_view_for_shutdown(self) -> None:
|
||||
try:
|
||||
if getattr(self, "content_stack", None) is None:
|
||||
@@ -979,7 +1305,7 @@ class MainWindow(QMainWindow):
|
||||
compact_overlay_legend = settings.value("samcam/compact_overlay_legend", False, type=bool)
|
||||
target_color = settings.value("samcam/target_color", "Cyan", type=str)
|
||||
|
||||
self.beamline.samcam.apply_overlay_settings(
|
||||
self.samcam.apply_overlay_settings(
|
||||
show_detections=show_detections,
|
||||
show_detection_polygons=show_detection_polygons,
|
||||
show_target_point=show_target_point,
|
||||
@@ -1112,12 +1438,10 @@ class MainWindow(QMainWindow):
|
||||
|
||||
if not self._in_compact_automation_view:
|
||||
self._pre_automation_window_state = self.saveState()
|
||||
self._pre_automation_ref_tools_visible = self.ref_tools_dock.isVisible()
|
||||
self._pre_automation_left_column_visible = self.collection_controls_scroll.isVisible()
|
||||
self._pre_automation_right_column_visible = self.beamline_controls_scroll.isVisible()
|
||||
|
||||
self.tell_samples_dock.setVisible(False)
|
||||
self.job_list_dock.setVisible(False)
|
||||
self.automation_progress_dock.setVisible(False)
|
||||
self.face_panel_dock.setVisible(False)
|
||||
self.fluor_panel_dock.setVisible(False)
|
||||
@@ -1126,9 +1450,6 @@ class MainWindow(QMainWindow):
|
||||
self.prediction_metrics_dock.setVisible(False)
|
||||
self.log_dock.setVisible(False)
|
||||
|
||||
if self._decoded_token.staff:
|
||||
self.ref_tools_dock.setVisible(False)
|
||||
|
||||
self.collection_controls_scroll.setVisible(False)
|
||||
self.beamline_controls_scroll.setVisible(False)
|
||||
|
||||
@@ -1146,15 +1467,11 @@ class MainWindow(QMainWindow):
|
||||
self.collection_controls_scroll.setVisible(self._pre_automation_left_column_visible)
|
||||
self.beamline_controls_scroll.setVisible(self._pre_automation_right_column_visible)
|
||||
|
||||
if self._decoded_token.staff:
|
||||
self.ref_tools_dock.setVisible(self._pre_automation_ref_tools_visible)
|
||||
|
||||
self._in_compact_automation_view = False
|
||||
self._update_view_mode_actions()
|
||||
|
||||
self.job_list_dock.setVisible(True)
|
||||
self.tell_samples_dock.setVisible(True)
|
||||
self.job_list_dock.raise_()
|
||||
self.tell_samples_dock.raise_()
|
||||
|
||||
@Slot()
|
||||
def _refresh_compact_queue_preview(self) -> None:
|
||||
@@ -1194,7 +1511,6 @@ class MainWindow(QMainWindow):
|
||||
# Hide all dock widgets
|
||||
for dock_attr in (
|
||||
"tell_samples_dock",
|
||||
"job_list_dock",
|
||||
"automation_progress_dock",
|
||||
"face_panel_dock",
|
||||
"fluor_panel_dock",
|
||||
@@ -1202,7 +1518,6 @@ class MainWindow(QMainWindow):
|
||||
"target_stability_dock",
|
||||
"prediction_metrics_dock",
|
||||
"log_dock",
|
||||
"ref_tools_dock",
|
||||
):
|
||||
dock = getattr(self, dock_attr, None)
|
||||
if dock is not None:
|
||||
@@ -1261,7 +1576,6 @@ class MainWindow(QMainWindow):
|
||||
self._pre_portrait_geometry = None
|
||||
|
||||
self.tell_samples_dock.setVisible(True)
|
||||
self.job_list_dock.setVisible(True)
|
||||
self.automation_progress_dock.setVisible(False)
|
||||
self.face_panel_dock.setVisible(False)
|
||||
self.fluor_panel_dock.setVisible(False)
|
||||
@@ -1435,25 +1749,6 @@ class MainWindow(QMainWindow):
|
||||
self.tell_samples_dock.visibilityChanged.connect(show_samples_action.setChecked)
|
||||
view_menu.addAction(show_samples_action)
|
||||
|
||||
if self._decoded_token.staff:
|
||||
show_reference_tools_action = QAction("Show Reference Tools", self)
|
||||
show_reference_tools_action.setCheckable(True)
|
||||
show_reference_tools_action.setChecked(True)
|
||||
show_reference_tools_action.triggered.connect(
|
||||
lambda checked: self.ref_tools_dock.setVisible(checked)
|
||||
)
|
||||
self.ref_tools_dock.visibilityChanged.connect(show_reference_tools_action.setChecked)
|
||||
view_menu.addAction(show_reference_tools_action)
|
||||
|
||||
show_job_list_action = QAction("Show job List", self)
|
||||
show_job_list_action.setCheckable(True)
|
||||
show_job_list_action.setChecked(True)
|
||||
show_job_list_action.triggered.connect(
|
||||
lambda checked: self.job_list_dock.setVisible(checked)
|
||||
)
|
||||
self.job_list_dock.visibilityChanged.connect(show_job_list_action.setChecked)
|
||||
view_menu.addAction(show_job_list_action)
|
||||
|
||||
show_face_panel_action = QAction("Show face detection", self)
|
||||
show_face_panel_action.setCheckable(True)
|
||||
show_face_panel_action.setChecked(False)
|
||||
@@ -1593,7 +1888,6 @@ class MainWindow(QMainWindow):
|
||||
self.beamline_controls_scroll.setVisible(True)
|
||||
|
||||
self.tell_samples_dock.setVisible(True)
|
||||
self.job_list_dock.setVisible(True)
|
||||
|
||||
self.face_panel_dock.setVisible(False)
|
||||
self.fluor_panel_dock.setVisible(False)
|
||||
@@ -1602,12 +1896,7 @@ class MainWindow(QMainWindow):
|
||||
self.prediction_metrics_dock.setVisible(False)
|
||||
self.log_dock.setVisible(False)
|
||||
|
||||
if self._decoded_token.staff:
|
||||
self.ref_tools_dock.setVisible(True)
|
||||
self.ref_tools_dock.raise_()
|
||||
else:
|
||||
self.ref_tools_dock.setVisible(False)
|
||||
self.tell_samples_dock.raise_()
|
||||
self.tell_samples_dock.raise_()
|
||||
|
||||
self.video_tab.setCurrentIndex(0)
|
||||
|
||||
@@ -1998,9 +2287,51 @@ class MainWindow(QMainWindow):
|
||||
def sample_view(self):
|
||||
self.video_tab.setCurrentWidget(self.sample_camera)
|
||||
|
||||
def _apply_session_gate(self, session_state) -> None:
|
||||
# No baton -> watching only: camera views stay live, every operating
|
||||
# surface is greyed. The SESSION VACANT badge (and the status bar
|
||||
# session menu) remain the way back in.
|
||||
owned = session_state in (
|
||||
SessionsStateEnum.OwnedByYou,
|
||||
SessionsStateEnum.PendingElseToYou,
|
||||
)
|
||||
if owned == getattr(self, "_session_operations_enabled", None):
|
||||
return
|
||||
self._session_operations_enabled = owned
|
||||
for widget in (
|
||||
self.left_column_tabs,
|
||||
self.beamline,
|
||||
self.tell_samples_dock.widget(),
|
||||
):
|
||||
widget.setEnabled(owned)
|
||||
if owned:
|
||||
widget.setGraphicsEffect(None)
|
||||
else:
|
||||
# Full grayscale, banners included — QSS :disabled alone
|
||||
# can't reach the custom-painted TitleLabels/inline styles.
|
||||
effect = QGraphicsColorizeEffect(widget)
|
||||
effect.setColor(QColor("#808080"))
|
||||
widget.setGraphicsEffect(effect)
|
||||
self.sample_camera.set_operations_enabled(owned)
|
||||
|
||||
# Vacant folds every panel shut; grabbing reopens exactly the ones
|
||||
# that were open before. Transient (persist=False) so the fold never
|
||||
# overwrites the user's saved per-panel choices.
|
||||
banners = self.left_column_tabs.findChildren(TitleLabel) + self.beamline.findChildren(
|
||||
TitleLabel
|
||||
)
|
||||
if owned:
|
||||
for banner in getattr(self, "_pre_vacancy_open_banners", []):
|
||||
banner.set_collapsed(False, persist=False)
|
||||
else:
|
||||
self._pre_vacancy_open_banners = [b for b in banners if not b.is_collapsed()]
|
||||
for banner in banners:
|
||||
banner.set_collapsed(True, persist=False)
|
||||
|
||||
@Slot(DAQStatusModel)
|
||||
def update_daq_status(self, s: DAQStatusModel):
|
||||
self._latest_daq_status = s
|
||||
self._apply_session_gate(getattr(getattr(s, "session", None), "session", None))
|
||||
|
||||
if self._is_automation_active():
|
||||
self._refresh_idle_activity(report_backend=False)
|
||||
@@ -2293,6 +2624,27 @@ class MainWindow(QMainWindow):
|
||||
self.state_manager.restore_window(self)
|
||||
self._restore_panel_visibility_settings()
|
||||
|
||||
def showEvent(self, event):
|
||||
super().showEvent(event)
|
||||
# First show only, and only without a saved layout: re-apply the
|
||||
# default dock split AFTER the real (maximized) geometry exists —
|
||||
# the __init__ resizeDocks ran on the pre-show size and Qt hands the
|
||||
# scale-up surplus to the sample list, skewing 50/50 into ~80/20.
|
||||
if not getattr(self, "_default_dock_split_done", False):
|
||||
self._default_dock_split_done = True
|
||||
if not self.state_manager.settings.value("main_window/state"):
|
||||
QTimer.singleShot(0, self._apply_default_dock_split)
|
||||
|
||||
def _apply_default_dock_split(self) -> None:
|
||||
self.resizeDocks(
|
||||
[self.tell_samples_dock, self.log_dock], [240, 240], Qt.Orientation.Vertical
|
||||
)
|
||||
self.resizeDocks(
|
||||
[self.tell_samples_dock, self.automation_progress_dock],
|
||||
[10000, 10000],
|
||||
Qt.Orientation.Horizontal,
|
||||
)
|
||||
|
||||
def closeEvent(self, event) -> None:
|
||||
try:
|
||||
self._return_to_main_view_for_shutdown()
|
||||
@@ -2464,6 +2816,16 @@ class MainWindow(QMainWindow):
|
||||
self._mark_user_interaction()
|
||||
except Exception as e:
|
||||
logger.debug(f"GUI interaction event filter error: {e}", exc_info=True)
|
||||
# Non-staff click on the greyed-out Auxiliary-puck tab: only installed
|
||||
# for non-staff, and tabAt() is geometric so it still sees the
|
||||
# disabled tab — explain the lock instead of silently eating the click.
|
||||
if (
|
||||
event.type() == QEvent.Type.MouseButtonPress
|
||||
and obj is self.sample_lists_tabs.tabBar()
|
||||
and obj.tabAt(event.position().toPoint()) == 1
|
||||
):
|
||||
self._show_reference_tools_staff_only_popup()
|
||||
return True
|
||||
return super().eventFilter(obj, event)
|
||||
|
||||
def _start_remote_close_countdown(
|
||||
|
||||
@@ -6,34 +6,45 @@ from PySide6.QtCore import QAbstractTableModel, QMimeData, Qt
|
||||
from PySide6.QtGui import QBrush
|
||||
|
||||
from aare.gui.constants import LOGGER_NAME
|
||||
from aare.gui.styles import SAMPLE_ROW_HIGHLIGHT_BG, SAMPLE_ROW_QUEUED_BG, WHITE, qcolor
|
||||
from aare.gui.styles import (
|
||||
SAMPLE_ROW_QUEUED_BG,
|
||||
SAMPLE_STATUS_FLAGGED_BG,
|
||||
SAMPLE_STATUS_MEASURED_BG,
|
||||
SAMPLE_STATUS_QUEUED_BG,
|
||||
qcolor,
|
||||
)
|
||||
|
||||
logger = setup_logger(LOGGER_NAME)
|
||||
|
||||
# Column 0 is display-only: the row position ("#") drawn over the status
|
||||
# color fill. Data attributes start at column 1.
|
||||
COL_STATUS = 0
|
||||
|
||||
|
||||
def get_entry(sample: SampleShortInfo, column: int):
|
||||
if column == 0:
|
||||
if column == 1:
|
||||
return sample.sample_name
|
||||
elif column == 1:
|
||||
return sample.puck_name
|
||||
elif column == 2:
|
||||
return sample.dewar_name
|
||||
return sample.puck_name
|
||||
elif column == 3:
|
||||
return sample.loc_str()
|
||||
return sample.dewar_name
|
||||
elif column == 4:
|
||||
return sample.priority
|
||||
return sample.loc_str()
|
||||
elif column == 5:
|
||||
return sample.user
|
||||
return sample.priority
|
||||
elif column == 6:
|
||||
return sample.mount_count
|
||||
return sample.user
|
||||
elif column == 7:
|
||||
return sample.raster_count
|
||||
return sample.mount_count
|
||||
elif column == 8:
|
||||
return sample.rotation_count
|
||||
return sample.raster_count
|
||||
elif column == 9:
|
||||
return sample.screening_count
|
||||
return sample.rotation_count
|
||||
elif column == 10:
|
||||
return sample.screening_count
|
||||
elif column == 11:
|
||||
return sample.comment
|
||||
return ""
|
||||
|
||||
|
||||
class UserSampleSpreadsheet(QAbstractTableModel):
|
||||
@@ -49,6 +60,7 @@ class UserSampleSpreadsheet(QAbstractTableModel):
|
||||
samples = []
|
||||
self.samples: list[SampleShortInfo] = samples
|
||||
self.header = [
|
||||
"#",
|
||||
"Sample name",
|
||||
"Puck",
|
||||
"Dewar",
|
||||
@@ -63,15 +75,23 @@ class UserSampleSpreadsheet(QAbstractTableModel):
|
||||
]
|
||||
self.current_sample = current_sample
|
||||
self.current_puck = current_puck
|
||||
self._sort_col = 3
|
||||
self._sort_col = 4 # Location
|
||||
self._sort_order = Qt.SortOrder.AscendingOrder
|
||||
|
||||
self._filters: dict[int, str] = {}
|
||||
self._filter_col: int | None = 5
|
||||
self._filter_col: int | None = 6 # User
|
||||
self._filter_value: str | None = None
|
||||
self.current_pgroup: str | None = None
|
||||
self.show_all_pgroups: bool = False
|
||||
|
||||
# Display-only status tints (aaregui2 concept: the dewar table doubles
|
||||
# as the queue view). Fed from outside; the queue itself stays in the
|
||||
# SampleQueueSpreadsheet.
|
||||
self.queued_ids: set[int] = set()
|
||||
self.flagged_ids: set[int] = set()
|
||||
# None = All; otherwise "queued" | "flagged" | "measured" (chip row).
|
||||
self.status_filter: str | None = None
|
||||
|
||||
self._sort()
|
||||
|
||||
def to_list(self) -> list[dict]:
|
||||
@@ -90,17 +110,88 @@ class UserSampleSpreadsheet(QAbstractTableModel):
|
||||
|
||||
def data(self, index, role=None):
|
||||
if role == Qt.ItemDataRole.DisplayRole:
|
||||
if index.column() == COL_STATUS:
|
||||
return index.row() + 1
|
||||
return get_entry(self._sorted_samples[index.row()], index.column())
|
||||
elif role == Qt.ItemDataRole.BackgroundRole:
|
||||
# Status lives in the "#" column, as a full cell fill under the
|
||||
# row number — rows themselves alternate grey/white (view-level)
|
||||
# and selection stays the pale blue tint.
|
||||
if index.column() == COL_STATUS:
|
||||
color = self._status_color(self._sorted_samples[index.row()])
|
||||
if color is not None:
|
||||
return QBrush(qcolor(color))
|
||||
elif role == Qt.ItemDataRole.TextAlignmentRole: # Align text to center
|
||||
return Qt.AlignmentFlag.AlignCenter
|
||||
elif role == Qt.ItemDataRole.BackgroundRole:
|
||||
if self._sorted_samples[index.row()].db_id == self.current_sample:
|
||||
return QBrush(qcolor(SAMPLE_ROW_QUEUED_BG))
|
||||
if self._sorted_samples[index.row()].puck_name == self.current_puck:
|
||||
return QBrush(qcolor(SAMPLE_ROW_HIGHLIGHT_BG))
|
||||
return QBrush(qcolor(WHITE))
|
||||
return None # For other roles, return None
|
||||
|
||||
def _status_color(self, sample: SampleShortInfo) -> str | None:
|
||||
# Mounted always wins; below that the dot depends on the active chip:
|
||||
# inside a filtered view every row carries that status, so its own
|
||||
# color is redundant — only cross-status marks show (queued view:
|
||||
# red = also flagged; flagged view: orange = put back in the queue).
|
||||
# The All view keeps the full priority queued > flagged > measured.
|
||||
if sample.db_id == self.current_sample:
|
||||
return SAMPLE_ROW_QUEUED_BG
|
||||
queued = sample.db_id in self.queued_ids
|
||||
flagged = sample.db_id in self.flagged_ids
|
||||
if self.status_filter == "queued":
|
||||
return SAMPLE_STATUS_FLAGGED_BG if flagged else None
|
||||
if self.status_filter == "flagged":
|
||||
return SAMPLE_STATUS_QUEUED_BG if queued else None
|
||||
if self.status_filter == "measured":
|
||||
if queued:
|
||||
return SAMPLE_STATUS_QUEUED_BG
|
||||
return SAMPLE_STATUS_FLAGGED_BG if flagged else None
|
||||
if queued:
|
||||
return SAMPLE_STATUS_QUEUED_BG
|
||||
if flagged:
|
||||
return SAMPLE_STATUS_FLAGGED_BG
|
||||
if self._measured(sample):
|
||||
return SAMPLE_STATUS_MEASURED_BG
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def _measured(sample: SampleShortInfo) -> bool:
|
||||
# Automatic status, never relabelled by hand: a sample counts as
|
||||
# measured once its rotation count exceeds 1.
|
||||
return isinstance(sample.rotation_count, (int, float)) and sample.rotation_count > 1
|
||||
|
||||
def set_queued_ids(self, db_ids) -> None:
|
||||
self.queued_ids = set(db_ids)
|
||||
self._status_sets_changed()
|
||||
|
||||
def set_flagged(self, db_id: int, flagged: bool) -> None:
|
||||
if flagged:
|
||||
self.flagged_ids.add(db_id)
|
||||
else:
|
||||
self.flagged_ids.discard(db_id)
|
||||
self._status_sets_changed()
|
||||
|
||||
def set_status_filter(self, status: str | None) -> None:
|
||||
self.layoutAboutToBeChanged.emit()
|
||||
self.status_filter = status
|
||||
self._sort()
|
||||
self.layoutChanged.emit()
|
||||
|
||||
def _status_sets_changed(self) -> None:
|
||||
# With a status chip active the row SET depends on the sets, not just
|
||||
# the tint — refilter; otherwise a background repaint is enough.
|
||||
if self.status_filter:
|
||||
self.layoutAboutToBeChanged.emit()
|
||||
self._sort()
|
||||
self.layoutChanged.emit()
|
||||
else:
|
||||
self._emit_tints_changed()
|
||||
|
||||
def _emit_tints_changed(self) -> None:
|
||||
if self.rowCount() > 0:
|
||||
self.dataChanged.emit(
|
||||
self.index(0, COL_STATUS),
|
||||
self.index(self.rowCount() - 1, COL_STATUS),
|
||||
[Qt.ItemDataRole.BackgroundRole],
|
||||
)
|
||||
|
||||
def headerData(self, section, orientation, role=None):
|
||||
if role == Qt.ItemDataRole.DisplayRole:
|
||||
if orientation == Qt.Orientation.Horizontal: # Column header
|
||||
@@ -114,6 +205,7 @@ class UserSampleSpreadsheet(QAbstractTableModel):
|
||||
):
|
||||
self.current_puck = current_puck
|
||||
self.current_sample = current_sample
|
||||
self._emit_tints_changed()
|
||||
|
||||
def updateData(self, samples: list[SampleShortInfo]):
|
||||
if samples != self.samples:
|
||||
@@ -123,6 +215,9 @@ class UserSampleSpreadsheet(QAbstractTableModel):
|
||||
self.endResetModel()
|
||||
|
||||
def sort(self, column, order):
|
||||
# The "#"/status column is display-only — nothing to sort by.
|
||||
if column == COL_STATUS:
|
||||
return
|
||||
self.layoutAboutToBeChanged.emit()
|
||||
self._sort_order = order
|
||||
self._sort_col = column
|
||||
@@ -131,7 +226,7 @@ class UserSampleSpreadsheet(QAbstractTableModel):
|
||||
|
||||
def _sort(self):
|
||||
filtered = self._apply_filter(self.samples)
|
||||
if self._sort_col == 3:
|
||||
if self._sort_col == 4: # Location
|
||||
self._sorted_samples = sorted(
|
||||
filtered,
|
||||
key=lambda row: row.loc_str_sort(),
|
||||
@@ -150,6 +245,14 @@ class UserSampleSpreadsheet(QAbstractTableModel):
|
||||
)
|
||||
|
||||
def _apply_filter(self, rows: list[SampleShortInfo]) -> list[SampleShortInfo]:
|
||||
# Status chip filter first (All/Queued/Flagged/Measured row).
|
||||
if self.status_filter == "queued":
|
||||
rows = [r for r in rows if r.db_id in self.queued_ids]
|
||||
elif self.status_filter == "flagged":
|
||||
rows = [r for r in rows if r.db_id in self.flagged_ids]
|
||||
elif self.status_filter == "measured":
|
||||
rows = [r for r in rows if self._measured(r)]
|
||||
|
||||
# Default filter by User using current p-group if no explicit filter set
|
||||
filters: dict[int, str] = {
|
||||
col: v for col, v in (self._filters or {}).items() if (v or "").strip()
|
||||
@@ -158,8 +261,8 @@ class UserSampleSpreadsheet(QAbstractTableModel):
|
||||
if self._filter_col is not None and (self._filter_value or "").strip():
|
||||
filters[self._filter_col] = self._filter_value
|
||||
|
||||
if 5 not in filters and self.current_pgroup and not self.show_all_pgroups:
|
||||
filters[5] = self.current_pgroup
|
||||
if 6 not in filters and self.current_pgroup and not self.show_all_pgroups:
|
||||
filters[6] = self.current_pgroup # User
|
||||
|
||||
if not filters:
|
||||
return rows
|
||||
@@ -271,7 +374,7 @@ class UserSampleSpreadsheet(QAbstractTableModel):
|
||||
break
|
||||
|
||||
# Sort appropriately
|
||||
if column == 5: # User/pgroup column
|
||||
if column == 6: # User/pgroup column
|
||||
try:
|
||||
out.sort(
|
||||
key=lambda x: (
|
||||
@@ -290,10 +393,10 @@ class UserSampleSpreadsheet(QAbstractTableModel):
|
||||
def suggested_prefixes_for_sample_name(self, limit: int = 200) -> list[str]:
|
||||
"""Get sample name prefixes from currently filtered samples (excluding column 0 filter)."""
|
||||
# Get currently filtered samples, excluding the sample name filter
|
||||
temp_filter = self._filters.pop(0, None)
|
||||
temp_filter = self._filters.pop(1, None)
|
||||
filtered_samples = self._apply_filter(self.samples)
|
||||
if temp_filter is not None:
|
||||
self._filters[0] = temp_filter
|
||||
self._filters[1] = temp_filter
|
||||
|
||||
rx = re.compile(r"^([A-Za-z]+)")
|
||||
counts: dict[str, int] = {}
|
||||
@@ -314,10 +417,10 @@ class UserSampleSpreadsheet(QAbstractTableModel):
|
||||
def suggested_prefixes_for_location(self, limit: int = 200) -> tuple[list[str], list[str]]:
|
||||
"""Get location prefixes from currently filtered samples (excluding column 3 filter)."""
|
||||
# Get currently filtered samples, excluding the location filter
|
||||
temp_filter = self._filters.pop(3, None)
|
||||
temp_filter = self._filters.pop(4, None)
|
||||
filtered_samples = self._apply_filter(self.samples)
|
||||
if temp_filter is not None:
|
||||
self._filters[3] = temp_filter
|
||||
self._filters[4] = temp_filter
|
||||
|
||||
seg_seen: set[str] = set()
|
||||
segpos_seen: set[str] = set()
|
||||
|
||||
@@ -1,13 +1,10 @@
|
||||
from PySide6.QtWidgets import QFrame, QVBoxLayout, QWidget
|
||||
|
||||
from aare.gui.panels.abr_tweak_panel import AbrTweakWidget
|
||||
from aare.gui.panels.beam_center_panel import BeamCenterWidget
|
||||
from aare.gui.panels.beam_mark_panel import BeamMarkWidget
|
||||
from aare.gui.panels.beam_size_panel import BeamSizeWidget
|
||||
from aare.gui.panels.illumination_panel import IlluminationPanel
|
||||
from aare.gui.panels.monochromator_panel import MonochromatorPanel
|
||||
from aare.gui.panels.omega_panel import OmegaPanel
|
||||
from aare.gui.panels.samcam_panel import SamcamPanel
|
||||
from aare.gui.panels.smargon_panel import SmargonPanel
|
||||
from aare.gui.panels.zoom_panel import ZoomPanel
|
||||
from aare.gui.widgets.title_label import TitleLabel, tighten_column
|
||||
@@ -23,7 +20,7 @@ class BeamConfigPanel(QWidget):
|
||||
# panels' banners in the column.
|
||||
layout = QVBoxLayout(self)
|
||||
layout.setSpacing(0)
|
||||
layout.addWidget(TitleLabel("Beam Config.", self, collapsible=True))
|
||||
layout.addWidget(TitleLabel("Beam configuration", self, collapsible=True, default_collapsed=False))
|
||||
self.beam_mark = BeamMarkWidget(self)
|
||||
self.beam_center = BeamCenterWidget(self)
|
||||
self.beam_size = BeamSizeWidget(self)
|
||||
@@ -39,13 +36,15 @@ class BeamConfigPanel(QWidget):
|
||||
class BeamlineControls(QFrame):
|
||||
set_width = 250
|
||||
|
||||
def __init__(self, parent=None, staff: bool = True):
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self.setObjectName("beamlineControls")
|
||||
self.setFixedWidth(self.set_width)
|
||||
self.setFrameShape(QFrame.Shape.StyledPanel)
|
||||
self.setFrameShadow(QFrame.Shadow.Raised)
|
||||
|
||||
# Samcam / monochromator / ABR / beam config moved to the left
|
||||
# column's "Beamline" group (main_window builds it).
|
||||
self.v_layout = QVBoxLayout(self)
|
||||
self.zoom_panel = ZoomPanel(self)
|
||||
self.v_layout.addWidget(self.zoom_panel)
|
||||
@@ -59,22 +58,6 @@ class BeamlineControls(QFrame):
|
||||
self.smargon_panel = SmargonPanel(parent=self)
|
||||
self.v_layout.addWidget(self.smargon_panel)
|
||||
|
||||
self.samcam = SamcamPanel(self)
|
||||
self.v_layout.addWidget(self.samcam)
|
||||
|
||||
if staff:
|
||||
self.monochromator_panel = MonochromatorPanel(self)
|
||||
self.abr_tweak = AbrTweakWidget(self)
|
||||
self.beam_config = BeamConfigPanel(self)
|
||||
# Aliases: main_window wires signals via beamline.beam_mark etc.
|
||||
self.beam_mark = self.beam_config.beam_mark
|
||||
self.beam_center = self.beam_config.beam_center
|
||||
self.beam_size = self.beam_config.beam_size
|
||||
|
||||
self.v_layout.addWidget(self.monochromator_panel)
|
||||
self.v_layout.addWidget(self.abr_tweak)
|
||||
self.v_layout.addWidget(self.beam_config)
|
||||
|
||||
self.v_layout.addStretch()
|
||||
tighten_column(self.v_layout)
|
||||
self.setLayout(self.v_layout)
|
||||
|
||||
@@ -9,9 +9,7 @@ from PySide6.QtWidgets import (
|
||||
QFrame,
|
||||
QGridLayout,
|
||||
QHeaderView,
|
||||
QLabel,
|
||||
QMenu,
|
||||
QPushButton,
|
||||
QTableView,
|
||||
)
|
||||
|
||||
@@ -174,11 +172,13 @@ class ReferenceToolsPanel(QFrame):
|
||||
samples: SampleShortInfoList | None = None,
|
||||
parent=None,
|
||||
refresh_interval_ms: int = 5000,
|
||||
model: ReferenceToolsModel | None = None,
|
||||
):
|
||||
"""
|
||||
:param samples: optional initial SampleShortInfoList to populate the table
|
||||
:param parent: Qt parent
|
||||
:param refresh_interval_ms: how often to call request_refresh (panel doesn't implement the request itself)
|
||||
:param model: share an existing model instead of owning one (pop-out window)
|
||||
"""
|
||||
super().__init__(parent)
|
||||
|
||||
@@ -190,26 +190,21 @@ class ReferenceToolsPanel(QFrame):
|
||||
|
||||
layout = QGridLayout(self)
|
||||
self.setLayout(layout)
|
||||
# Flush layout, matching TellSamplePanel: full-width banner, no
|
||||
# padding ring, no gap to the dock's tab row.
|
||||
layout.setContentsMargins(0, 0, 0, 0)
|
||||
layout.setSpacing(0)
|
||||
|
||||
layout.addWidget(TitleLabel("Reference tools", parent=self), 0, 0, 1, 4)
|
||||
|
||||
self.table_view = QTableView(parent=self)
|
||||
# Row colors carry the separation — no grid lines.
|
||||
self.table_view.setShowGrid(False)
|
||||
layout.addWidget(self.table_view, 1, 0, 1, 4)
|
||||
|
||||
self.curr_sample_label = QLabel("No sample mounted", parent=self)
|
||||
layout.addWidget(self.curr_sample_label, 2, 0)
|
||||
|
||||
self.unmount_button = QPushButton("Unmount", parent=self)
|
||||
layout.addWidget(self.unmount_button, 2, 1)
|
||||
self.unmount_button.clicked.connect(self._on_unmount_clicked)
|
||||
|
||||
layout.setColumnStretch(0, 1)
|
||||
layout.setColumnStretch(1, 0)
|
||||
|
||||
# initialize model with provided samples
|
||||
self.table_model = ReferenceToolsModel(rows=samples.s)
|
||||
# initialize model with provided samples (or adopt the shared one)
|
||||
self.table_model = model if model is not None else ReferenceToolsModel(rows=samples.s)
|
||||
self.table_view.setModel(self.table_model)
|
||||
self.table_view.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeMode.Stretch)
|
||||
|
||||
self.table_view.setEditTriggers(QTableView.EditTrigger.NoEditTriggers)
|
||||
logger.debug("Setting up table header")
|
||||
@@ -218,8 +213,13 @@ class ReferenceToolsPanel(QFrame):
|
||||
header.setSectionResizeMode(QHeaderView.ResizeMode.Interactive)
|
||||
logger.debug("Setting up table header")
|
||||
header.setStretchLastSection(True)
|
||||
# No bold column titles when cells are selected.
|
||||
header.setHighlightSections(False)
|
||||
self.table_view.verticalHeader().setVisible(True)
|
||||
logger.debug("Setting up table view sorting")
|
||||
# Adopt the model's current order first — a second panel on a shared
|
||||
# model must not re-sort it on open.
|
||||
header.setSortIndicator(self.table_model._sort_col, self.table_model._sort_order)
|
||||
self.table_view.setSortingEnabled(True)
|
||||
logger.debug("Setting up table view context menu")
|
||||
self.table_view.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
|
||||
@@ -227,15 +227,18 @@ class ReferenceToolsPanel(QFrame):
|
||||
self.table_view.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows)
|
||||
self.table_view.setSelectionMode(QTableView.SelectionMode.SingleSelection)
|
||||
|
||||
# Columns at full content width, sized once (see TellSamplePanel).
|
||||
self._columns_autosized = False
|
||||
if self.table_model.rowCount() > 0:
|
||||
self.table_view.resizeColumnsToContents()
|
||||
self._columns_autosized = True
|
||||
|
||||
def _selected_item(self) -> SampleShortInfo | None:
|
||||
idx = self.table_view.currentIndex()
|
||||
if not idx.isValid():
|
||||
return None
|
||||
return self.table_model.get_item(idx.row())
|
||||
|
||||
def _on_unmount_clicked(self):
|
||||
self.unmount.emit()
|
||||
|
||||
def _context_menu(self, position):
|
||||
idx = self.table_view.indexAt(position)
|
||||
|
||||
@@ -263,22 +266,13 @@ class ReferenceToolsPanel(QFrame):
|
||||
def new_list(self, samples: SampleShortInfoList):
|
||||
# signal from DAQWorker will call this with the model
|
||||
self.table_model.update_rows(rows=samples.s)
|
||||
if not self._columns_autosized and self.table_model.rowCount() > 0:
|
||||
self.table_view.resizeColumnsToContents()
|
||||
self._columns_autosized = True
|
||||
|
||||
@Slot(DAQStatusModel)
|
||||
def update_daq_status(self, status: DAQStatusModel):
|
||||
# The "No sample mounted" label and Unmount button were dropped —
|
||||
# mounted state now shows as the existing row highlight instead.
|
||||
sample = status.sample
|
||||
if sample is None:
|
||||
self.curr_sample_label.setText("No sample mounted")
|
||||
else:
|
||||
try:
|
||||
if sample.location is None:
|
||||
self.curr_sample_label.setText(
|
||||
f"Current sample: <b>{sample.sample_name} (Manual mount)</b>"
|
||||
)
|
||||
else:
|
||||
self.curr_sample_label.setText(
|
||||
f"Current sample: <b>{sample.sample_name} ({sample.location.segment}{sample.location.pos}-{sample.pin})</b>"
|
||||
)
|
||||
except Exception as e:
|
||||
logger.debug("Could not update the current sample label", exc_info=True)
|
||||
self.curr_sample_label.setText(f"Confusing information :/ {e}")
|
||||
self.table_model.update_current_reference(sample.db_id if sample is not None else None)
|
||||
|
||||
@@ -194,6 +194,13 @@ class SampleQueuePanel(QFrame):
|
||||
|
||||
self._emit_samples_in_queue_changed()
|
||||
|
||||
def remove_samples(self, db_ids) -> None:
|
||||
"""Remove the given samples from the queue. Public entry point for the
|
||||
combined dewar view, whose selection lives outside this panel."""
|
||||
for db_id in db_ids:
|
||||
self.table_model.remove_sample(db_id)
|
||||
self._emit_samples_in_queue_changed()
|
||||
|
||||
def remove_selected_samples(self):
|
||||
selected_indexes = self.table_view.selectionModel().selectedRows()
|
||||
if not selected_indexes:
|
||||
|
||||
@@ -8,30 +8,133 @@ from aarecommon.models.models import (
|
||||
from PySide6.QtCore import Qt, Signal, Slot
|
||||
from PySide6.QtWidgets import (
|
||||
QAbstractItemView,
|
||||
QButtonGroup,
|
||||
QFrame,
|
||||
QGridLayout,
|
||||
QHBoxLayout,
|
||||
QHeaderView,
|
||||
QLabel,
|
||||
QMenu,
|
||||
QPushButton,
|
||||
QTableView,
|
||||
)
|
||||
|
||||
from aare.gui.constants import LOGGER_NAME
|
||||
from aare.gui.models.user_sample_model import UserSampleSpreadsheet
|
||||
from aare.gui.styles import NOTE_TEXT
|
||||
from aare.gui.models.user_sample_model import COL_STATUS, UserSampleSpreadsheet
|
||||
from aare.gui.styles import (
|
||||
CHIP_NEUTRAL_BG,
|
||||
MUTED_TEXT,
|
||||
SAMPLE_STATUS_FLAGGED_BG,
|
||||
SAMPLE_STATUS_MEASURED_BG,
|
||||
SAMPLE_STATUS_QUEUED_BG,
|
||||
TAB_FACE_BG,
|
||||
TEXT,
|
||||
)
|
||||
from aare.gui.widgets.title_label import TitleLabel
|
||||
|
||||
logger = setup_logger(LOGGER_NAME)
|
||||
|
||||
|
||||
class FrozenColumnTableView(QTableView):
|
||||
"""QTableView with the "#"/status column frozen (Qt frozen-column
|
||||
pattern): an overlay view shares the model and selection, sits on top of
|
||||
column 0, and stays put while the rest scrolls horizontally."""
|
||||
|
||||
FROZEN_WIDTH = 36
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self.frozen = QTableView(self)
|
||||
self.frozen.setFocusPolicy(Qt.FocusPolicy.NoFocus)
|
||||
self.frozen.verticalHeader().hide()
|
||||
self.frozen.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
|
||||
self.frozen.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
|
||||
self.frozen.setShowGrid(False)
|
||||
self.frozen.setAlternatingRowColors(True)
|
||||
self.frozen.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows)
|
||||
self.frozen.setSelectionMode(QAbstractItemView.SelectionMode.ExtendedSelection)
|
||||
self.frozen.setEditTriggers(QTableView.EditTrigger.NoEditTriggers)
|
||||
self.frozen.setDragEnabled(True)
|
||||
self.frozen.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeMode.Fixed)
|
||||
self.frozen.horizontalHeader().setHighlightSections(False)
|
||||
# The overlay must not carry the L3 frame — it sits INSIDE the view.
|
||||
self.frozen.setStyleSheet("QTableView { border: none; }")
|
||||
self.viewport().stackUnder(self.frozen)
|
||||
self.frozen.verticalScrollBar().valueChanged.connect(self.verticalScrollBar().setValue)
|
||||
self.verticalScrollBar().valueChanged.connect(self.frozen.verticalScrollBar().setValue)
|
||||
|
||||
def setModel(self, model):
|
||||
super().setModel(model)
|
||||
self.frozen.setModel(model)
|
||||
# Share the selection so clicking either view highlights both.
|
||||
self.frozen.setSelectionModel(self.selectionModel())
|
||||
for col in range(1, model.columnCount()):
|
||||
self.frozen.setColumnHidden(col, True)
|
||||
self.set_frozen_width(self.FROZEN_WIDTH)
|
||||
self.frozen.show()
|
||||
|
||||
def set_frozen_width(self, width: int) -> None:
|
||||
self.setColumnWidth(0, width)
|
||||
self.frozen.setColumnWidth(0, width)
|
||||
self._update_frozen_geometry()
|
||||
|
||||
def _update_frozen_geometry(self) -> None:
|
||||
self.frozen.setGeometry(
|
||||
self.frameWidth(),
|
||||
self.frameWidth(),
|
||||
self.columnWidth(0),
|
||||
self.viewport().height() + self.horizontalHeader().height(),
|
||||
)
|
||||
|
||||
def resizeEvent(self, event):
|
||||
super().resizeEvent(event)
|
||||
self._update_frozen_geometry()
|
||||
|
||||
|
||||
class QueueDropChip(QPushButton):
|
||||
"""Filter chip that doubles as a drop target: dragging table rows onto it
|
||||
relabels them (Queued adds to the automation queue, Flagged marks them
|
||||
flagged — same mime the old queue dock took). Measured is deliberately
|
||||
NOT one of these: it is automatic, from the rotation count."""
|
||||
|
||||
samples_dropped = Signal(SampleShortInfoList)
|
||||
|
||||
def __init__(self, label: str, parent=None):
|
||||
super().__init__(label, parent)
|
||||
self.setAcceptDrops(True)
|
||||
|
||||
def dragEnterEvent(self, event):
|
||||
if event.mimeData().hasText():
|
||||
event.acceptProposedAction()
|
||||
|
||||
def dropEvent(self, event):
|
||||
try:
|
||||
samples = SampleShortInfoList.model_validate_json(event.mimeData().text())
|
||||
except Exception:
|
||||
logger.debug("Ignoring drop that is not a sample list", exc_info=True)
|
||||
return
|
||||
self.samples_dropped.emit(samples)
|
||||
event.acceptProposedAction()
|
||||
|
||||
|
||||
class TellSamplePanel(QFrame):
|
||||
mount = Signal(SampleShortInfo)
|
||||
unmount = Signal()
|
||||
state = Signal(BeamlineStateEnum)
|
||||
# Queue membership is edited from this table now (aaregui2 concept: the
|
||||
# dewar list doubles as the queue view); the queue itself lives in the
|
||||
# SampleQueuePanel these signals are wired to.
|
||||
add_to_queue = Signal(SampleShortInfoList)
|
||||
remove_from_queue = Signal(SampleShortInfoList)
|
||||
|
||||
def __init__(self, samples: SampleShortInfoList | None = None, parent=None):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
samples: SampleShortInfoList | None = None,
|
||||
parent=None,
|
||||
model: UserSampleSpreadsheet | None = None,
|
||||
):
|
||||
"""`model`: share an existing spreadsheet model instead of owning one —
|
||||
used by the pop-out window so both panels operate on the same data,
|
||||
tints and filters with no syncing."""
|
||||
super().__init__(parent)
|
||||
|
||||
if samples is None:
|
||||
@@ -41,28 +144,86 @@ class TellSamplePanel(QFrame):
|
||||
|
||||
grid_layout = QGridLayout(self)
|
||||
self.setLayout(grid_layout)
|
||||
# Flush layout: the banner spans the full panel width and sits
|
||||
# directly under the dock's tab row — no padding ring.
|
||||
grid_layout.setContentsMargins(0, 0, 0, 0)
|
||||
grid_layout.setSpacing(0)
|
||||
|
||||
grid_layout.addWidget(TitleLabel("TELL sample changer", self), 0, 0, 1, 4)
|
||||
|
||||
self.table_view = QTableView()
|
||||
grid_layout.addWidget(self.table_view, 1, 0, 1, 4)
|
||||
# Status filter row (aaregui2 concept): filters the table by queue/
|
||||
# collection status, and each checked button wears its row-tint color,
|
||||
# doubling as the legend. Styled like the Dewar/Auxiliary tab row
|
||||
# above (square tabs, touching), not pills. Colors live in styles.py.
|
||||
chip_row = QHBoxLayout()
|
||||
# Left margin 0: "All" shares the table's left edge; bottom 0: the
|
||||
# row sits directly on the table.
|
||||
chip_row.setContentsMargins(0, 2, 6, 0)
|
||||
chip_row.setSpacing(0)
|
||||
self.status_chips = QButtonGroup(self)
|
||||
self.status_chips.setExclusive(True)
|
||||
for label, key, checked_bg in (
|
||||
("All", None, CHIP_NEUTRAL_BG),
|
||||
("Queued", "queued", SAMPLE_STATUS_QUEUED_BG),
|
||||
("Flagged", "flagged", SAMPLE_STATUS_FLAGGED_BG),
|
||||
("Measured", "measured", SAMPLE_STATUS_MEASURED_BG),
|
||||
):
|
||||
if key == "queued":
|
||||
chip = QueueDropChip(label, self)
|
||||
chip.samples_dropped.connect(self.add_to_queue)
|
||||
# Deselect after the drop: the selection tint would otherwise
|
||||
# sit on top of the fresh status color and hide it.
|
||||
chip.samples_dropped.connect(lambda _: self.table_view.clearSelection())
|
||||
chip.setToolTip("Filter queued samples — or drop table rows here to queue them")
|
||||
elif key == "flagged":
|
||||
chip = QueueDropChip(label, self)
|
||||
chip.samples_dropped.connect(self._flag_dropped_samples)
|
||||
chip.samples_dropped.connect(lambda _: self.table_view.clearSelection())
|
||||
chip.setToolTip("Filter flagged samples — or drop table rows here to flag them")
|
||||
else:
|
||||
chip = QPushButton(label, self)
|
||||
if key == "measured":
|
||||
chip.setToolTip("Filter measured samples (automatic: rotation count > 1)")
|
||||
chip.setCheckable(True)
|
||||
chip.setChecked(key is None)
|
||||
chip.setProperty("status_key", key)
|
||||
chip.setCursor(Qt.CursorShape.PointingHandCursor)
|
||||
chip.setStyleSheet(
|
||||
# Same font, padding and hover hint as the QTabBar tabs above
|
||||
# (no bold, default size): unchecked tabs sit 3px lower
|
||||
# (raised-selection effect), the checked one wears its
|
||||
# row-tint fill, hover underlines just the text.
|
||||
f"QPushButton {{ background: {TAB_FACE_BG}; color: {MUTED_TEXT};"
|
||||
f" border: none;"
|
||||
f" border-top-left-radius: 4px; border-top-right-radius: 4px;"
|
||||
f" margin-top: 3px; padding: 4px 14px; }}"
|
||||
f"QPushButton:hover:!checked {{ color: {TEXT}; text-decoration: underline; }}"
|
||||
f"QPushButton:checked {{ background: {checked_bg}; color: {TEXT};"
|
||||
f" margin-top: 0px; padding: 6px 14px 5px 14px; }}"
|
||||
)
|
||||
self.status_chips.addButton(chip)
|
||||
chip_row.addWidget(chip)
|
||||
chip_row.addStretch()
|
||||
grid_layout.addLayout(chip_row, 1, 0, 1, 4)
|
||||
self.status_chips.buttonClicked.connect(
|
||||
lambda chip: self.table_model.set_status_filter(chip.property("status_key"))
|
||||
)
|
||||
|
||||
self.curr_sample_label = QLabel("No sample mounted", parent=self)
|
||||
self.curr_sample_label.setTextFormat(Qt.TextFormat.RichText)
|
||||
self.curr_sample_label.setWordWrap(True)
|
||||
grid_layout.addWidget(self.curr_sample_label, 2, 0)
|
||||
# Staggered grey/white rows tell rows apart — no grid lines, no row
|
||||
# tints; status fills the frozen "#" column and selection stays blue.
|
||||
self.table_view = FrozenColumnTableView()
|
||||
self.table_view.setShowGrid(False)
|
||||
self.table_view.setAlternatingRowColors(True)
|
||||
grid_layout.addWidget(self.table_view, 2, 0, 1, 4)
|
||||
|
||||
self.unmount_button = QPushButton("Unmount", parent=self)
|
||||
grid_layout.addWidget(self.unmount_button, 2, 1)
|
||||
self.unmount_button.clicked.connect(self.unmount_button_clicked)
|
||||
|
||||
grid_layout.setColumnStretch(0, 1)
|
||||
grid_layout.setColumnStretch(1, 0)
|
||||
|
||||
self.table_model = UserSampleSpreadsheet(samples=samples.s)
|
||||
self.table_model = model if model is not None else UserSampleSpreadsheet(samples=samples.s)
|
||||
self.table_view.setModel(self.table_model)
|
||||
self.table_view.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeMode.Stretch)
|
||||
|
||||
# Adopt the model's current order before enabling sorting: a second
|
||||
# panel on a shared model must not re-sort it to column 0 on open.
|
||||
self.table_view.horizontalHeader().setSortIndicator(
|
||||
self.table_model._sort_col, self.table_model._sort_order
|
||||
)
|
||||
self.table_view.setSortingEnabled(True)
|
||||
self.table_view.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
|
||||
self.table_view.customContextMenuRequested.connect(self.context_menu)
|
||||
@@ -74,17 +235,44 @@ class TellSamplePanel(QFrame):
|
||||
header = self.table_view.horizontalHeader()
|
||||
header.setSectionResizeMode(QHeaderView.ResizeMode.Interactive)
|
||||
header.setStretchLastSection(True)
|
||||
self.table_view.verticalHeader().setVisible(True)
|
||||
# No bold column titles when cells are selected.
|
||||
header.setHighlightSections(False)
|
||||
# Row numbers + status color live in the frozen "#" column: fixed
|
||||
# width, not resizable, stays visible on horizontal scroll.
|
||||
self.table_view.verticalHeader().setVisible(False)
|
||||
header.setSectionResizeMode(COL_STATUS, QHeaderView.ResizeMode.Fixed)
|
||||
self.table_view.set_frozen_width(FrozenColumnTableView.FROZEN_WIDTH)
|
||||
# Right-click on the frozen column behaves like the main table (the
|
||||
# handler only uses the row, which both views share).
|
||||
self.table_view.frozen.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
|
||||
self.table_view.frozen.customContextMenuRequested.connect(self.context_menu)
|
||||
|
||||
header.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
|
||||
header.customContextMenuRequested.connect(self.header_context_menu)
|
||||
|
||||
def unmount_button_clicked(self):
|
||||
self.unmount.emit()
|
||||
# Columns at full content width (horizontal scroll instead of
|
||||
# squishing); done ONCE so later data refreshes don't fight manual
|
||||
# column adjustments.
|
||||
self._columns_autosized = False
|
||||
if self.table_model.rowCount() > 0:
|
||||
self._autosize_columns()
|
||||
|
||||
def _autosize_columns(self) -> None:
|
||||
self.table_view.resizeColumnsToContents()
|
||||
header = self.table_view.horizontalHeader()
|
||||
# Cap: one long comment must not eat the whole view.
|
||||
for col in range(1, self.table_model.columnCount()):
|
||||
if header.sectionSize(col) > 300:
|
||||
self.table_view.setColumnWidth(col, 300)
|
||||
# Re-pin the frozen display column after the autosize pass.
|
||||
self.table_view.set_frozen_width(FrozenColumnTableView.FROZEN_WIDTH)
|
||||
self._columns_autosized = True
|
||||
|
||||
@Slot(SampleShortInfoList)
|
||||
def new_sample_list(self, samples: SampleShortInfoList):
|
||||
self.table_model.updateData(samples=samples.s)
|
||||
if not self._columns_autosized and self.table_model.rowCount() > 0:
|
||||
self._autosize_columns()
|
||||
|
||||
def annotate_sample_comment(self, db_id: int, comment: str) -> None:
|
||||
samples = list(self.table_model.samples)
|
||||
@@ -98,6 +286,29 @@ class TellSamplePanel(QFrame):
|
||||
|
||||
self.table_model.updateData(samples=updated_samples)
|
||||
|
||||
@Slot(SampleShortInfoList)
|
||||
def _flag_dropped_samples(self, samples: SampleShortInfoList) -> None:
|
||||
# Flagged is display state owned by the (shared) model, so relabeling
|
||||
# here reaches the docked panel and the pop-out alike.
|
||||
for sample in samples.s:
|
||||
self.table_model.set_flagged(sample.db_id, True)
|
||||
|
||||
def set_status_chip(self, key: str | None) -> None:
|
||||
"""Check the chip for `key` without firing its filter — keeps the
|
||||
main and pop-out chip rows in sync (both drive one shared model)."""
|
||||
for chip in self.status_chips.buttons():
|
||||
if chip.property("status_key") == key:
|
||||
chip.setChecked(True)
|
||||
return
|
||||
|
||||
def _selected_samples(self, clicked_row: int) -> list[SampleShortInfo]:
|
||||
"""Selected rows if the clicked row is part of the selection, else
|
||||
just the clicked row — so right-click on an unselected row acts on it."""
|
||||
rows = sorted({i.row() for i in self.table_view.selectionModel().selectedRows()})
|
||||
if clicked_row not in rows:
|
||||
rows = [clicked_row]
|
||||
return [self.table_model.get_id(r) for r in rows]
|
||||
|
||||
def context_menu(self, position):
|
||||
index = self.table_view.indexAt(position)
|
||||
if not index.isValid():
|
||||
@@ -109,18 +320,35 @@ class TellSamplePanel(QFrame):
|
||||
if sample.location is None:
|
||||
return
|
||||
|
||||
selected = [s for s in self._selected_samples(row) if s.location is not None]
|
||||
|
||||
menu = QMenu()
|
||||
count = f" ({len(selected)})" if len(selected) > 1 else ""
|
||||
add_queue_action = menu.addAction(f"Add to queue{count}")
|
||||
remove_queue_action = menu.addAction(f"Remove from queue{count}")
|
||||
menu.addSeparator()
|
||||
mount_action = menu.addAction("Mount")
|
||||
# Unmount moved here from the removed bottom-row button — same signal.
|
||||
unmount_action = menu.addAction("Unmount")
|
||||
|
||||
action = menu.exec_(self.table_view.viewport().mapToGlobal(position))
|
||||
|
||||
if action == mount_action:
|
||||
self.mount.emit(sample)
|
||||
elif action == unmount_action:
|
||||
self.unmount.emit()
|
||||
elif action == add_queue_action:
|
||||
self.add_to_queue.emit(SampleShortInfoList(s=selected))
|
||||
self.table_view.clearSelection()
|
||||
elif action == remove_queue_action:
|
||||
self.remove_from_queue.emit(SampleShortInfoList(s=selected))
|
||||
self.table_view.clearSelection()
|
||||
|
||||
def header_context_menu(self, pos):
|
||||
header = self.table_view.horizontalHeader()
|
||||
logical_index = header.logicalIndexAt(pos)
|
||||
if logical_index < 0:
|
||||
# The "#"/status column is display-only — no filter menus there.
|
||||
if logical_index < 1:
|
||||
return
|
||||
|
||||
col_name = self.table_model.header[logical_index]
|
||||
@@ -128,7 +356,7 @@ class TellSamplePanel(QFrame):
|
||||
menu = QMenu(self)
|
||||
|
||||
# Column-specific preset submenus (existing logic) ...
|
||||
if logical_index == 0:
|
||||
if logical_index == 1: # Sample name
|
||||
presets = self.table_model.suggested_prefixes_for_sample_name()
|
||||
if presets:
|
||||
prefix_menu = menu.addMenu("Filter by name prefix")
|
||||
@@ -139,7 +367,7 @@ class TellSamplePanel(QFrame):
|
||||
logical_index, vv
|
||||
)
|
||||
)
|
||||
elif logical_index == 3:
|
||||
elif logical_index == 4: # Location
|
||||
segs, segpos = self.table_model.suggested_prefixes_for_location()
|
||||
if segs:
|
||||
seg_menu = menu.addMenu("Filter by segment (A..F,X,R)")
|
||||
@@ -176,7 +404,7 @@ class TellSamplePanel(QFrame):
|
||||
clear_filter_action = menu.addAction(f"Clear filter: {col_name}")
|
||||
clear_all_action = menu.addAction("Clear all filters")
|
||||
|
||||
if logical_index == 5:
|
||||
if logical_index == 6: # User
|
||||
menu.addSeparator()
|
||||
toggle_all = menu.addAction("Show all pgroups (ignore current p-group)")
|
||||
toggle_all.setCheckable(True)
|
||||
@@ -204,48 +432,16 @@ class TellSamplePanel(QFrame):
|
||||
|
||||
@Slot(DAQStatusModel)
|
||||
def update_daq_status(self, status: DAQStatusModel):
|
||||
# Mounted state shows as the row highlight only — the "No sample
|
||||
# mounted" label and Unmount button were dropped; TELL activity text
|
||||
# lives in the beamline state panel already.
|
||||
sample = status.sample
|
||||
tell_state = status.tell_state
|
||||
|
||||
tell_details = ""
|
||||
if tell_state is not None:
|
||||
activity = tell_state.activity.display_name()
|
||||
phase = tell_state.phase.display_name() if tell_state.phase is not None else ""
|
||||
message = (tell_state.message or "").strip()
|
||||
|
||||
tell_parts = [activity]
|
||||
if phase:
|
||||
tell_parts.append(phase)
|
||||
|
||||
tell_details = " / ".join(tell_parts)
|
||||
if message:
|
||||
tell_details = f"{tell_details} — {message}"
|
||||
|
||||
if sample is None:
|
||||
base_text = "No sample mounted"
|
||||
self.table_model.updateCurrentSample(current_puck=None, current_sample=None)
|
||||
else:
|
||||
try:
|
||||
if sample.location is None:
|
||||
base_text = f"Current sample: <b>{sample.sample_name} (Manual mount)</b>"
|
||||
else:
|
||||
base_text = (
|
||||
f"Current sample: <b>{sample.sample_name} "
|
||||
f"({sample.location.segment}{sample.location.pos}-{sample.pin})</b>"
|
||||
)
|
||||
self.table_model.updateCurrentSample(
|
||||
current_puck=sample.puck_name, current_sample=sample.db_id
|
||||
)
|
||||
except Exception as e:
|
||||
logger.debug("Could not build the TELL sample panel text", exc_info=True)
|
||||
base_text = f"Confusing information :/ {e}"
|
||||
|
||||
if tell_details:
|
||||
self.curr_sample_label.setText(
|
||||
f"{base_text}<br/><span style='color:{NOTE_TEXT};'>TELL: {tell_details}</span>"
|
||||
self.table_model.updateCurrentSample(
|
||||
current_puck=sample.puck_name, current_sample=sample.db_id
|
||||
)
|
||||
else:
|
||||
self.curr_sample_label.setText(base_text)
|
||||
|
||||
if status.session.current_pgroup is not None:
|
||||
self._current_pgroup = status.session.current_pgroup
|
||||
|
||||
Reference in New Issue
Block a user