GUI:" tell_sample_panel.py added filters to sample spreadsheet

This commit is contained in:
2025-11-03 16:27:05 +01:00
parent 8ef53955d4
commit 6b23509445
2 changed files with 224 additions and 4 deletions
+143 -3
View File
@@ -1,3 +1,5 @@
import re
from PySide6.QtCore import QAbstractTableModel, Qt, QMimeData
from PySide6.QtGui import QBrush, QColor
@@ -30,7 +32,7 @@ class UserSampleSpreadsheet(QAbstractTableModel):
parent=None,
samples: list[SampleShortInfo] | None = None,
current_puck: str | None = None,
current_sample: int | None = None,
current_sample: int | None = None
):
super().__init__(parent)
if samples is None:
@@ -50,8 +52,16 @@ class UserSampleSpreadsheet(QAbstractTableModel):
self.current_puck = current_puck
self.__sort_col = 3
self.__sort_order = Qt.SortOrder.AscendingOrder
self.__filters: dict[int, str] = {}
self.__filter_col: int | None = 5
self.__filter_value: str | None = None
self.current_pgroup: str | None = None
self.show_all_pgroups: bool = False
self._sort()
def rowCount(self, parent=None):
return len(self.__sorted_samples)
@@ -104,9 +114,10 @@ class UserSampleSpreadsheet(QAbstractTableModel):
def _sort(self):
filtered = self._apply_filter(self.samples)
if self.__sort_col == 3:
self.__sorted_samples = sorted(
self.samples,
filtered,
key=lambda row: row.loc_str_sort(),
reverse=(
self.__sort_order == Qt.SortOrder.DescendingOrder
@@ -115,13 +126,35 @@ class UserSampleSpreadsheet(QAbstractTableModel):
else:
# Sort the samples based on the specified column and order
self.__sorted_samples = sorted(
self.samples,
filtered,
key=lambda row: get_entry(row, self.__sort_col),
reverse=(
self.__sort_order == Qt.SortOrder.DescendingOrder
), # Reverse for descending order
)
def _apply_filter(self, rows: list[SampleShortInfo]) -> list[SampleShortInfo]:
# 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()}
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 not filters:
return rows
def matches(sample: SampleShortInfo) -> bool:
for col, txt in filters.items():
val = get_entry(sample, col)
s = "" if val is None else str(val)
if str(txt).strip().lower() not in s.strip().lower():
return False
return True
return [r for r in rows if matches(r)]
def flags(self, index):
default_flags = super().flags(index)
@@ -145,3 +178,110 @@ class UserSampleSpreadsheet(QAbstractTableModel):
def get_id(self, row: int) -> SampleShortInfo:
return self.__sorted_samples[row]
def set_filter(self, field: str, text: str | None):
try:
col = self.header.index(field)
except ValueError:
col = None
self.layoutAboutToBeChanged.emit()
self.__filter_col = col if text and text.strip() and col is not None else None
self.__filter_value = text.strip() if text else None
# mirror into multi-filter map
if col is not None:
if text and text.strip():
self.__filters[col] = text.strip()
else:
self.__filters.pop(col, None)
self._sort()
self.layoutChanged.emit()
def clear_filter(self):
self.layoutAboutToBeChanged.emit()
self.__filter_col = None
self.__filter_value = None
self.__filters.clear()
self._sort()
self.layoutChanged.emit()
def set_default_user_filter(self, pgroup: str | None):
self.layoutAboutToBeChanged.emit()
self.current_pgroup = (pgroup or "").strip() or None
self._sort()
self.layoutChanged.emit()
def set_column_filter(self, column: int, text: str | None):
self.layoutAboutToBeChanged.emit()
if text and text.strip():
self.__filters[column] = text.strip()
else:
self.__filters.pop(column, None)
self._sort()
self.layoutChanged.emit()
def clear_all_column_filters(self):
self.clear_filter()
def set_show_all_pgroups(self, show_all: bool):
self.layoutAboutToBeChanged.emit()
self.show_all_pgroups = bool(show_all)
self._sort()
self.layoutChanged.emit()
def unique_values_for_column(self, column: int, limit: int = 200) -> list[str]:
seen: set[str] = set()
out: list[str] = []
for s in self.samples:
v = get_entry(s, column)
if v is None:
continue
txt = str(v).strip()
if not txt:
continue
if txt not in seen:
seen.add(txt)
out.append(txt)
if len(out) >= limit:
break
if column == 5:
try:
out.sort(key=lambda x: int(x[1:]) if x and x[0].lower() == "p" and x[1:].isdigit() else float("inf"))
except Exception:
out.sort()
else:
out.sort()
return out
def suggested_prefixes_for_sample_name(self, limit: int = 200) -> list[str]:
rx = re.compile(r"^([A-Za-z]+)")
counts: dict[str, int] = {}
for s in self.samples:
name = (s.sample_name or "").strip()
if not name:
continue
m = rx.match(name)
pref = m.group(1) if m else name.split("_")[0]
pref = pref.strip()
if not pref:
continue
counts[pref] = counts.get(pref, 0) + 1
# Sort by frequency desc, then alpha
items = sorted(counts.items(), key=lambda kv: (-kv[1], kv[0]))
return [k for k, _ in items[:limit]]
def suggested_prefixes_for_location(self, limit: int = 200) -> tuple[list[str], list[str]]:
seg_seen: set[str] = set()
segpos_seen: set[str] = set()
for s in self.samples:
loc = s.loc_str() # "-" or like "B3-10"
if loc and loc != "-" and isinstance(loc, str) and "-" in loc:
left = loc.split("-", 1)[0] # e.g., "B3"
if left:
segpos_seen.add(left)
seg = left[0]
if seg:
seg_seen.add(seg)
segs = sorted(seg_seen)
segpos = sorted(segpos_seen, key=lambda x: (x[0], int(x[1:]) if x[1:].isdigit() else 0))
return (segs[:limit], segpos[:limit])
+81 -1
View File
@@ -9,11 +9,14 @@ from PySide6.QtWidgets import (
QPushButton,
QAbstractItemView,
)
from aaredaqlib.logger_config import setup_logger
from aaredaqlib.models import BeamlineStateEnum, SampleShortInfo, SampleShortInfoList, DAQStatusModel
from aaregui.models.user_sample_model import UserSampleSpreadsheet
from aaregui.widgets.title_label import TitleLabel
logger = setup_logger("aareGUI")
class TellSamplePanel(QFrame):
mount = Signal(SampleShortInfo)
@@ -70,9 +73,12 @@ class TellSamplePanel(QFrame):
self.table_view.setDragEnabled(True)
header = self.table_view.horizontalHeader()
header.setSectionResizeMode(QHeaderView.ResizeMode.Interactive)
header.setStretchLastSection(True) # Optional: Make last column stretch to fit
header.setStretchLastSection(True)
self.table_view.verticalHeader().setVisible(True)
header.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
header.customContextMenuRequested.connect(self.header_context_menu)
def unmount_button_clicked(self):
self.unmount.emit()
@@ -103,6 +109,76 @@ class TellSamplePanel(QFrame):
if action == mount_action:
self.mount.emit(sample)
def header_context_menu(self, pos):
header = self.table_view.horizontalHeader()
logical_index = header.logicalIndexAt(pos)
if logical_index < 0:
return
col_name = self.table_model.header[logical_index]
menu = QMenu(self)
# Column-specific preset submenus (existing logic) ...
if logical_index == 0:
presets = self.table_model.suggested_prefixes_for_sample_name()
if presets:
prefix_menu = menu.addMenu("Filter by name prefix")
for p in presets:
act = prefix_menu.addAction(p)
act.triggered.connect(
lambda checked=False, vv=p: self.table_model.set_column_filter(logical_index, vv))
elif logical_index == 3:
segs, segpos = self.table_model.suggested_prefixes_for_location()
if segs:
seg_menu = menu.addMenu("Filter by segment (A..F,X,R)")
for s in segs:
act = seg_menu.addAction(s)
act.triggered.connect(
lambda checked=False, vv=s: self.table_model.set_column_filter(logical_index, vv))
if segpos:
sp_menu = menu.addMenu("Filter by segment+position (e.g. B3)")
for sp in segpos:
act = sp_menu.addAction(sp)
act.triggered.connect(
lambda checked=False, vv=sp: self.table_model.set_column_filter(logical_index, vv))
else:
values = self.table_model.unique_values_for_column(logical_index)
if values:
choose_menu = menu.addMenu(f"Choose {col_name}")
for v in values:
act = choose_menu.addAction(v)
act.triggered.connect(
lambda checked=False, vv=v: self.table_model.set_column_filter(logical_index, vv))
# Manual entry and clear options
set_filter_action = menu.addAction(f"Filter column: {col_name}...")
clear_filter_action = menu.addAction(f"Clear filter: {col_name}")
clear_all_action = menu.addAction("Clear all filters")
if logical_index == 5:
menu.addSeparator()
toggle_all = menu.addAction("Show all pgroups (ignore current p-group)")
toggle_all.setCheckable(True)
toggle_all.setChecked(self.table_model.show_all_pgroups)
def _toggle_all():
self.table_model.set_show_all_pgroups(not self.table_model.show_all_pgroups)
toggle_all.triggered.connect(_toggle_all)
action = menu.exec_(header.mapToGlobal(pos))
if action is None:
return
if action == set_filter_action:
from PySide6.QtWidgets import QInputDialog
text, ok = QInputDialog.getText(self, "Set filter", f"Filter for '{col_name}':")
if ok:
self.table_model.set_column_filter(logical_index, text)
elif action == clear_filter_action:
self.table_model.set_column_filter(logical_index, None)
elif action == clear_all_action:
self.table_model.clear_all_column_filters()
@Slot(DAQStatusModel)
def update_daq_status(self, status: DAQStatusModel):
sample = status.sample
@@ -124,3 +200,7 @@ class TellSamplePanel(QFrame):
)
except Exception as e:
self.curr_sample_label.setText(f"Confusing information :/ {e}")
if status.session.current_pgroup is not None:
self.__current_pgroup = status.session.current_pgroup
self.table_model.set_default_user_filter(self.__current_pgroup)