diff --git a/debye_bec/bec_widgets/widgets/data_viewer/data_viewer.py b/debye_bec/bec_widgets/widgets/data_viewer/data_viewer.py index 9ed8458..16c6826 100644 --- a/debye_bec/bec_widgets/widgets/data_viewer/data_viewer.py +++ b/debye_bec/bec_widgets/widgets/data_viewer/data_viewer.py @@ -67,6 +67,11 @@ class DataViewer(BECWidget, QWidget): self.current_row = 0 + logger.info(self.client.acl) + logger.info(self.client.active_account) + logger.info(self.client.proc) + logger.info(self.client.username) + self.input.scan_sel.currentItemChanged_connect(self.scan_sel_changed) self.input.load_button.clicked_connect(self.load_dataset) @@ -75,11 +80,13 @@ class DataViewer(BECWidget, QWidget): self.current_row = kwargs["value"]().row() @SafeSlot() - def load_dataset(self, *_): + def load_dataset( + self, *_ + ): # TODO: Check scan file components for combined xas/xrd scans. Is the Pilatus file in there as well? scan = self.history[self.current_row] file = scan["file_components"][0] + b"_master." + scan["file_components"][1] logger.info(file.decode()) - self.viewer.load_file(file.decode()) + self.viewer.load_files([file.decode()]) @SafeSlot() def on_history_update(self, *_): @@ -89,19 +96,32 @@ class DataViewer(BECWidget, QWidget): # and no scan has finished yet. Limit the history to the latest 20 scans. max_scans = min(len(self.client.history), 20) for n in range(1, max_scans): # last scans, up to 20 + logger.info(self.client.history[-n].metadata) scan_data = self.client.history[-n].metadata["bec"] # logger.info(scan_data) + scan_number = scan_data["scan_number"] + scan_name = scan_data["scan_name"] + comment = scan_data["metadata"]["user_metadata"]["comment"] + sample_name = scan_data["metadata"]["user_metadata"]["sample_name"] + # start = scan_data[] + # end = scan_data[] self.history.append( { - "scan_number": scan_data["scan_number"], - "scan_name": scan_data["scan_name"], - "user_metadata": scan_data["user_metadata"], + "scan_number": scan_number, + "scan_name": scan_name, + "comment": comment, + "sample_name": sample_name, "file_components": scan_data["file_components"], } ) - self.input.scan_sel.addTaggedItem( - label=str(scan_data["scan_number"]), tags=[(scan_data["scan_name"], "#4A90D9")] - ) + tags = [] + tags.append((scan_name, "#4A90D9")) + if sample_name != "": + tags.append((sample_name, "#4A10D9")) + if comment != "": + tags.append((comment, "#FA10D9")) + self.input.scan_sel.addTaggedItem(label=str(scan_number), tags=tags) + # logger.info(f"Scan history: {self.history}") class InputPanel(QWidget): diff --git a/debye_bec/bec_widgets/widgets/data_viewer/qt_widgets.py b/debye_bec/bec_widgets/widgets/data_viewer/qt_widgets.py index fa9235c..572a972 100644 --- a/debye_bec/bec_widgets/widgets/data_viewer/qt_widgets.py +++ b/debye_bec/bec_widgets/widgets/data_viewer/qt_widgets.py @@ -4,46 +4,50 @@ Selection works natively; no popup, no paintEvent hacks. """ import sys + +from qtpy.QtCore import QPoint, QRect, QSize, Qt +from qtpy.QtGui import QColor, QFont, QFontMetrics, QPainter, QPen from qtpy.QtWidgets import ( - QApplication, QListWidget, QListWidgetItem, QStyledItemDelegate, - QStyleOptionViewItem, QWidget, QVBoxLayout, QLabel, QStyle, + QApplication, + QLabel, + QListWidget, + QListWidgetItem, + QStyle, + QStyledItemDelegate, + QStyleOptionViewItem, + QVBoxLayout, + QWidget, ) -from qtpy.QtGui import QPainter, QColor, QFont, QPen, QFontMetrics -from qtpy.QtCore import Qt, QRect, QSize, QPoint # ── Design tokens ────────────────────────────────────────────────────────────── -ITEM_HEIGHT = 40 -H_PAD = 12 -TAG_H_PAD = 7 -TAG_V_PAD = 3 -TAG_GAP = 5 -LABEL_TAG_GAP = 12 -CORNER_RADIUS = 4 -FONT_FAMILY = "Segoe UI" -LABEL_FONT_SIZE = 10 -TAG_FONT_SIZE = 8 -TAG_TEXT_COLOR = "#FFFFFF" +ITEM_HEIGHT = 30 +H_PAD = 12 +TAG_H_PAD = 7 +TAG_V_PAD = 3 +TAG_GAP = 5 +LABEL_TAG_GAP = 12 +CORNER_RADIUS = 4 +TAG_TEXT_COLOR = "#FFFFFF" # ── Enum compat (PySide6 nested vs PyQt5 flat) ──────────────────────────────── try: _DEMIBOLD = QFont.Weight.DemiBold - _MEDIUM = QFont.Weight.Medium + _MEDIUM = QFont.Weight.Medium except AttributeError: _DEMIBOLD = QFont.DemiBold - _MEDIUM = QFont.Medium + _MEDIUM = QFont.Medium _AlignVCenter = Qt.AlignmentFlag.AlignVCenter if hasattr(Qt, "AlignmentFlag") else Qt.AlignVCenter -_AlignCenter = Qt.AlignmentFlag.AlignCenter if hasattr(Qt, "AlignmentFlag") else Qt.AlignCenter -_UserRole = Qt.ItemDataRole.UserRole if hasattr(Qt, "ItemDataRole") else Qt.UserRole -_NoPen = Qt.PenStyle.NoPen if hasattr(Qt, "PenStyle") else Qt.NoPen -_AA = (QPainter.RenderHint.Antialiasing - if hasattr(QPainter, "RenderHint") else QPainter.Antialiasing) +_AlignCenter = Qt.AlignmentFlag.AlignCenter if hasattr(Qt, "AlignmentFlag") else Qt.AlignCenter +_UserRole = Qt.ItemDataRole.UserRole if hasattr(Qt, "ItemDataRole") else Qt.UserRole +_NoPen = Qt.PenStyle.NoPen if hasattr(Qt, "PenStyle") else Qt.NoPen +_AA = QPainter.RenderHint.Antialiasing if hasattr(QPainter, "RenderHint") else QPainter.Antialiasing try: - _State_Selected = QStyle.StateFlag.State_Selected + _State_Selected = QStyle.StateFlag.State_Selected _State_MouseOver = QStyle.StateFlag.State_MouseOver except AttributeError: - _State_Selected = QStyle.State_Selected + _State_Selected = QStyle.State_Selected _State_MouseOver = QStyle.State_MouseOver @@ -57,7 +61,7 @@ class TaggedDelegate(QStyledItemDelegate): painter.save() is_selected = bool(option.state & _State_Selected) - is_hover = bool(option.state & _State_MouseOver) + is_hover = bool(option.state & _State_MouseOver) # Background if is_selected: @@ -67,16 +71,23 @@ class TaggedDelegate(QStyledItemDelegate): else: painter.fillRect(option.rect, option.palette.base()) - label_text = index.data(Qt.ItemDataRole.DisplayRole - if hasattr(Qt, "ItemDataRole") else Qt.DisplayRole) or "" + label_text = ( + index.data( + Qt.ItemDataRole.DisplayRole if hasattr(Qt, "ItemDataRole") else Qt.DisplayRole + ) + or "" + ) tags: list = index.data(_UserRole) or [] # Label - label_font = QFont(FONT_FAMILY, LABEL_FONT_SIZE) + label_font = painter.font() # inherit default font label_font.setWeight(_DEMIBOLD) painter.setFont(label_font) - label_color = (option.palette.highlightedText().color() - if is_selected else option.palette.text().color()) + label_color = ( + option.palette.highlightedText().color() + if is_selected + else option.palette.text().color() + ) painter.setPen(QPen(label_color)) fm = QFontMetrics(label_font) @@ -85,7 +96,7 @@ class TaggedDelegate(QStyledItemDelegate): painter.drawText(QPoint(option.rect.left() + H_PAD, label_y), label_text) # Tag pills - tag_font = QFont(FONT_FAMILY, TAG_FONT_SIZE) + tag_font = painter.font() # inherit default font tag_font.setWeight(_MEDIUM) fm_tag = QFontMetrics(tag_font) @@ -122,7 +133,7 @@ class TaggedListWidget(QListWidget): def __init__(self, parent=None) -> None: super().__init__(parent) self.setItemDelegate(TaggedDelegate(self)) - self.setMouseTracking(True) # enables hover highlight + self.setMouseTracking(True) # enables hover highlight def addTaggedItem(self, label: str, tags: list | None = None) -> QListWidgetItem: """ @@ -154,11 +165,11 @@ if __name__ == "__main__": layout.setSpacing(12) lst = TaggedListWidget() - lst.addTaggedItem("NumPy", tags=[("1.26", "#2563EB"), ("stable", "#16A34A")]) - lst.addTaggedItem("Pandas", tags=[("2.2", "#7C3AED"), ("deprecated", "#DC2626")]) - lst.addTaggedItem("PyTorch", tags=[("2.3", "#2563EB"), ("cuda", "#DC2626")]) - lst.addTaggedItem("scikit-learn", tags=[("1.4", "#0891B2"), ("stable", "#16A34A")]) - lst.addTaggedItem("Matplotlib", tags=[("3.8", "#D97706")]) + lst.addTaggedItem("NumPy", tags=[("1.26", "#2563EB"), ("stable", "#16A34A")]) + lst.addTaggedItem("Pandas", tags=[("2.2", "#7C3AED"), ("deprecated", "#DC2626")]) + lst.addTaggedItem("PyTorch", tags=[("2.3", "#2563EB"), ("cuda", "#DC2626")]) + lst.addTaggedItem("scikit-learn", tags=[("1.4", "#0891B2"), ("stable", "#16A34A")]) + lst.addTaggedItem("Matplotlib", tags=[("3.8", "#D97706")]) lst.addTaggedItem("Plain item — no tags") layout.addWidget(lst) @@ -166,7 +177,7 @@ if __name__ == "__main__": def on_selection(): label = lst.currentItem().text() if lst.currentItem() else "" - tags = lst.currentTags() + tags = lst.currentTags() tag_str = " ".join(t for t, _ in tags) if tags else "none" info.setText(f"Selected: {label} tags: {tag_str}") @@ -174,4 +185,4 @@ if __name__ == "__main__": layout.addWidget(info) window.show() - sys.exit(app.exec()) \ No newline at end of file + sys.exit(app.exec()) diff --git a/debye_bec/bec_widgets/widgets/data_viewer/viewer.py b/debye_bec/bec_widgets/widgets/data_viewer/viewer.py index 52f73e3..b975214 100644 --- a/debye_bec/bec_widgets/widgets/data_viewer/viewer.py +++ b/debye_bec/bec_widgets/widgets/data_viewer/viewer.py @@ -355,15 +355,18 @@ class DataPanel(QWidget): class HDF5Viewer(QMainWindow): def __init__(self, filepath=None): super().__init__() - self.filepath = filepath - self.h5file = None + self.h5files = {} # filepath -> h5py.File self._build_ui() + if filepath: + self.load_files([filepath]) - def load_file(self, f): - self.filepath = f - self.h5file = h5py.File(f, "r") - filename = f.split("/")[-1] - self._load_tree(filename) + def load_files(self, filepaths: list[str]): + """Open one or more HDF5 files and add each as a top-level tree node.""" + for f in filepaths: + if f in self.h5files: + continue # already loaded + self.h5files[f] = h5py.File(f, "r") + self._add_file_to_tree(f) def _build_ui(self): central = QWidget() @@ -394,7 +397,6 @@ class HDF5Viewer(QMainWindow): splitter.addWidget(left_pane) splitter.addWidget(self.data_panel) - # Initial proportions: 25% / 75% splitter.setStretchFactor(0, 1) splitter.setStretchFactor(1, 3) splitter.setSizes([300, 900]) @@ -403,17 +405,20 @@ class HDF5Viewer(QMainWindow): root_layout.addWidget(splitter) - def _load_tree(self, filename): - self.tree.clear() + def _add_file_to_tree(self, filepath: str): + """Add a single file as a new top-level node in the tree.""" + h5file = self.h5files[filepath] + filename = filepath.split("/")[-1] dataset_icon = material_icon("dataset", size=(ICON_SIZE, ICON_SIZE), color="#2980b9") root_item = QTreeWidgetItem(self.tree, [filename, "Group", ""]) root_item.setIcon(0, dataset_icon) root_item.setData(0, Qt.UserRole, "/") root_item.setData(0, Qt.UserRole + 1, "group") + root_item.setData(0, Qt.UserRole + 2, filepath) # so clicks know which file root_item.setForeground(0, QColor(ACCENT)) - populate_tree(root_item, self.h5file) + populate_tree(root_item, h5file) self.tree.addTopLevelItem(root_item) # Expand first 2 levels @@ -423,21 +428,40 @@ class HDF5Viewer(QMainWindow): self.tree.setCurrentItem(root_item) + def _get_filepath_for_item(self, item: QTreeWidgetItem) -> str: + """Walk up the tree to find the filepath stored on the root node.""" + node = item + while node.parent(): + node = node.parent() + return node.data(0, Qt.UserRole + 2) + def _on_item_clicked(self, item, _col): path = item.data(0, Qt.UserRole) kind = item.data(0, Qt.UserRole + 1) - if not path: + filepath = self._get_filepath_for_item(item) + + if not path or not filepath: return - obj = self.h5file[path] if path != "/" else self.h5file + h5file = self.h5files[filepath] + obj = h5file[path] if path != "/" else h5file if kind == "dataset": - ds = self.h5file[path] - data = ds[()] + data = h5file[path][()] self.data_panel.display(data, path) else: n = len(obj) if isinstance(obj, h5py.Group) else 0 + def clear_files(self): + """Close all open HDF5 files and reset the tree.""" + for h5file in self.h5files.values(): + h5file.close() + self.h5files.clear() + self.tree.clear() + self.data_panel._show_empty() # TODO: If it works, make function public! + def closeEvent(self, event): - self.h5file.close() + for h5file in self.h5files.values(): + h5file.close() + self.h5files.clear() super().closeEvent(event)