Files
AareDAQ/src/aare/gui/panels/reference_tools_panel.py
T
perl_d b0863e5ddb
Build and Publish / test (pull_request) Failing after 1m23s
Build and Publish / build (pull_request) Skipped
Build and Publish / Build and Deploy Docs (pull_request) Skipped
style: format with ruff
2026-07-06 11:53:49 +02:00

283 lines
9.8 KiB
Python

# reference_tools_panel.py
from typing import Optional
from aarecommon.config.logger import setup_logger
from aarecommon.models.models import DAQStatusModel, SampleShortInfo, SampleShortInfoList
from PySide6.QtCore import QAbstractTableModel, QModelIndex, Qt, Signal, Slot
from PySide6.QtGui import QBrush, QColor
from PySide6.QtWidgets import (
QAbstractItemView,
QFrame,
QGridLayout,
QHeaderView,
QLabel,
QMenu,
QPushButton,
QTableView,
)
from aare.gui.widgets.title_label import TitleLabel
logger = setup_logger("aareGUI")
def get_entry(sample: SampleShortInfo, column: int):
if column == 0:
return sample.loc_str()
elif column == 1:
return sample.sample_name
elif column == 2:
return sample.mount_count
elif column == 3:
return sample.rotation_count
elif column == 4:
return sample.raster_count
elif column == 5:
return sample.screening_count
return ""
class ReferenceToolsModel(QAbstractTableModel):
def __init__(
self,
rows: Optional[list[SampleShortInfo]] | None = None,
parent=None,
current_reference: int | None = None,
):
super().__init__(parent)
self.samples: list[SampleShortInfo] = rows or []
self.current_reference = current_reference
self.header = [
"Position",
"Sample name",
"Mount count",
"Raster count",
"Rotation count",
"Screening count",
]
self.__sort_col = 0
self.__sort_order = Qt.SortOrder.AscendingOrder
self.__sorted_samples: list[SampleShortInfo] = []
if self.samples:
self._sort()
else:
self.__sorted_samples = []
def rowCount(self, parent: QModelIndex = QModelIndex()) -> int:
return len(self.__sorted_samples)
def columnCount(self, parent: QModelIndex = QModelIndex()) -> int:
return len(self.header)
def data(self, index: QModelIndex, role=None):
if not index.isValid():
return None
if index.row() >= len(self.__sorted_samples):
return None
if role == Qt.ItemDataRole.DisplayRole:
return get_entry(self.__sorted_samples[index.row()], index.column())
elif role == Qt.ItemDataRole.TextAlignmentRole:
return Qt.AlignmentFlag.AlignCenter
elif role == Qt.ItemDataRole.BackgroundRole:
if self.__sorted_samples[index.row()].db_id == self.current_reference:
return QBrush(QColor(114, 159, 207)) # darker blue
return QBrush(QColor(255, 255, 255)) # white
return None
def headerData(self, section: int, orientation, role=None):
if role != Qt.ItemDataRole.DisplayRole:
return None
if orientation == Qt.Orientation.Horizontal:
return self.header[section] if section < len(self.header) else f"Column {section + 1}"
return str(section + 1)
def update_rows(self, rows: list[SampleShortInfo]):
self.beginResetModel()
try:
self.samples = rows
if self.samples: # Only sort if we have samples
self._sort()
else:
self.__sorted_samples = [] # Clear sorted samples if no data
finally:
self.endResetModel()
def sort(self, column, order):
self.layoutAboutToBeChanged.emit()
self.__sort_order = order
self.__sort_col = column
self._sort()
self.layoutChanged.emit()
def _sort(self):
"""Sort samples, using special handling for location column."""
if not self.samples:
self.__sorted_samples = []
return
if self.__sort_col == 0:
# Special sorting for location (Position column)
self.__sorted_samples = sorted(
self.samples,
key=lambda row: row.loc_str_sort(),
reverse=(self.__sort_order == Qt.SortOrder.DescendingOrder),
)
elif self.__sort_col == 2:
# Numeric sort for Mount count; place None last on ascending, first on descending
none_sentinel = (
float("inf") if self.__sort_order == Qt.SortOrder.AscendingOrder else float("-inf")
)
self.__sorted_samples = sorted(
self.samples,
key=lambda row: (
row.mount_count if isinstance(row.mount_count, (int, float)) else none_sentinel
),
reverse=(self.__sort_order == Qt.SortOrder.DescendingOrder),
)
else:
# String sort with empty fallback
self.__sorted_samples = sorted(
self.samples,
key=lambda row: get_entry(row, self.__sort_col) or "",
reverse=(self.__sort_order == Qt.SortOrder.DescendingOrder),
)
def get_item(self, row: int) -> Optional[SampleShortInfo]:
"""Get sample at the given row index."""
if 0 <= row < len(self.__sorted_samples):
return self.__sorted_samples[row]
return None
def update_current_reference(self, reference_id: int | None):
"""Update which sample is highlighted as current reference."""
self.current_reference = reference_id
# Trigger repaint of background colors
if self.rowCount() > 0:
self.dataChanged.emit(
self.index(0, 0),
self.index(self.rowCount() - 1, self.columnCount() - 1),
[Qt.ItemDataRole.BackgroundRole],
)
class ReferenceToolsPanel(QFrame):
mount = Signal(SampleShortInfo, bool)
unmount = Signal()
def __init__(
self,
samples: SampleShortInfoList | None = None,
parent=None,
refresh_interval_ms: int = 5000,
):
"""
: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)
"""
super().__init__(parent)
if samples is None:
samples = SampleShortInfoList(s=[])
self.setFrameShape(QFrame.Shape.StyledPanel)
self.setFrameShadow(QFrame.Shadow.Raised)
layout = QGridLayout(self)
self.setLayout(layout)
layout.addWidget(TitleLabel("Reference tools", parent=self), 0, 0, 1, 4)
self.table_view = QTableView(parent=self)
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)
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")
header = self.table_view.horizontalHeader()
logger.debug("Setting up table header")
header.setSectionResizeMode(QHeaderView.ResizeMode.Interactive)
logger.debug("Setting up table header")
header.setStretchLastSection(True)
self.table_view.verticalHeader().setVisible(True)
logger.debug("Setting up table view sorting")
self.table_view.setSortingEnabled(True)
logger.debug("Setting up table view context menu")
self.table_view.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
self.table_view.customContextMenuRequested.connect(self._context_menu)
self.table_view.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows)
self.table_view.setSelectionMode(QTableView.SelectionMode.SingleSelection)
def _selected_item(self) -> Optional[SampleShortInfo]:
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)
if not idx.isValid():
return
item = self.table_model.get_item(idx.row())
if item is None:
return
menu = QMenu()
act_mount = menu.addAction("Mount")
act_unmount = menu.addAction("Unmount")
action = menu.exec_(self.table_view.viewport().mapToGlobal(position))
if action == act_mount:
self.mount.emit(item, True)
elif action == act_unmount:
self.unmount.emit()
@Slot(SampleShortInfoList)
def new_list(self, samples: SampleShortInfoList):
# signal from DAQWorker will call this with the model
self.table_model.update_rows(rows=samples.s)
@Slot(DAQStatusModel)
def update_daq_status(self, status: DAQStatusModel):
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:
self.curr_sample_label.setText(f"Confusing information :/ {e}")