GUI:" tell_sample_panel.py added filters to sample spreadsheet
This commit is contained in:
@@ -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])
|
||||
@@ -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)
|
||||
Reference in New Issue
Block a user