283 lines
9.8 KiB
Python
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}")
|