@@ -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):
|
||||
|
||||
@@ -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: <b>{label}</b> tags: {tag_str}")
|
||||
|
||||
@@ -174,4 +185,4 @@ if __name__ == "__main__":
|
||||
layout.addWidget(info)
|
||||
|
||||
window.show()
|
||||
sys.exit(app.exec())
|
||||
sys.exit(app.exec())
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user