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 9f6ea43..ae9bebe 100644 --- a/debye_bec/bec_widgets/widgets/data_viewer/data_viewer.py +++ b/debye_bec/bec_widgets/widgets/data_viewer/data_viewer.py @@ -2,9 +2,12 @@ Data Viewer: Custom BEC widget to view data from scans. """ +import os +import subprocess import sys from datetime import datetime from functools import partial +from typing import Literal, Optional, cast from bec_lib import bec_logger from bec_lib.endpoints import MessageEndpoints @@ -36,6 +39,8 @@ from debye_bec.bec_widgets.widgets.data_viewer.viewer import HDF5Viewer logger = bec_logger.logger +MAX_HIST_LEN = 100 + class DataViewer(BECWidget, QWidget): """ @@ -43,7 +48,7 @@ class DataViewer(BECWidget, QWidget): """ PLUGIN = True - ICON_NAME = "lightbulb" + ICON_NAME = "database_search" # TODO: This does not work, choose correct icon def __init__(self, *arg, parent=None, **kwargs): super().__init__(parent=parent, theme_update=True, *arg, **kwargs) @@ -76,11 +81,33 @@ class DataViewer(BECWidget, QWidget): self.input.scan_sel.currentItemChanged_connect(self.scan_sel_changed) self.input.load_button.clicked_connect(self.load_dataset) self.input.unload_button.clicked_connect(self.unload_all_datasets) + self.input.open_fm_button.clicked_connect(self.open_in_file_manager) + + def apply_theme(self, theme: Literal["dark", "light"]): + """ + Apply the theme + + Args: + theme (str): Theme, either "dark" or "light" + """ + self.viewer.apply_theme(theme) @SafeSlot() def scan_sel_changed(self, *_, **kwargs): self.current_row = kwargs["value"]().row() + @SafeSlot() + def open_in_file_manager(self, *_): + scan = self.history[self.current_row] + filepath = scan["file_components"][0].decode().rsplit("/", 1)[0] + subprocess.Popen( + ["xdg-open", filepath], + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + stdin=subprocess.DEVNULL, + start_new_session=True, + ) + @SafeSlot() def load_dataset( self, *_ @@ -121,12 +148,12 @@ class DataViewer(BECWidget, QWidget): self.input.scan_sel.clear() # Get the length of the scan history, which is 0 when the bec server was started # 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 + max_scans = min(len(self.client.history), MAX_HIST_LEN) + for n in range(1, max_scans): # last scans, limited by MAX_HIST_LEN # logger.info(self.client.history[-n].metadata["bec"]["status"]) start_time = self.client.history[-n].metadata["start_time"] end_time = self.client.history[-n].metadata["end_time"] - logger.info(type(start_time)) + # logger.info(type(start_time)) scan_data = self.client.history[-n].metadata["bec"] # logger.info(scan_data) scan_number = scan_data["scan_number"] @@ -160,7 +187,7 @@ class DataViewer(BECWidget, QWidget): tags.append((status, "#656365")) tags.append((self.duration_string(start_time, end_time), "#656365")) self.input.scan_sel.addTaggedItem(label=str(scan_number), tags=tags) - logger.info(f"Scan history: {self.history}") + # logger.info(f"Scan history: {self.history}") class InputPanel(QWidget): @@ -168,29 +195,44 @@ class InputPanel(QWidget): def __init__(self, parent=None): super().__init__(parent) - self._layout = QVBoxLayout(self) - self._layout.setSizeConstraint(QLayout.SetFixedSize) # type: ignore + self._layout = QHBoxLayout(self) + # self._layout.setSizeConstraint(QLayout.SetFixedSize) # type: ignore # Scan selection self.scan_sel = ListWidget("scan_sel", "Scan", ["Si", "Rh", "Pt"]) self.load_button = Button(label_button="Load Dataset", enabled=True) self.unload_button = Button(label_button="Unload all", enabled=True) + self.open_fm_button = Button(label_button="Open in File Manager", enabled=True) + + self._button_layout = QVBoxLayout() + self._button_layout.addWidget(self.load_button) + self._button_layout.addWidget(self.unload_button) + self._button_layout.addWidget(self.open_fm_button) + self._button_layout.addStretch() # Assemble complete scan selection group self.input_group = Group( - "Scan selection", [self.scan_sel, self.load_button, self.unload_button] + "Scan selection", [self._button_layout, self.scan_sel], orientation="horizontal" ) self._layout.addWidget(self.input_group) - self._layout.addStretch() + # self._layout.addStretch() class Group(QGroupBox): - def __init__(self, label, widgets): + def __init__(self, label, objs, orientation="vertical"): super().__init__(label) - self.layout = QVBoxLayout(self) # type: ignore - for widget in widgets: - self.layout.addWidget(widget) # type: ignore + if orientation == "vertical": + self._layout = QVBoxLayout(self) # type: ignore + elif orientation == "horizontal": # assume horizontal + self._layout = QHBoxLayout(self) # type: ignore + else: + raise ValueError(f"Orientation {orientation} is not supported!") + for obj in objs: + if isinstance(obj, QWidget): + self._layout.addWidget(obj) # type: ignore + elif isinstance(obj, QLayout): + self._layout.addLayout(obj) class ListWidget(QWidget): @@ -206,7 +248,7 @@ class ListWidget(QWidget): # self.label.setWordWrap(True) # layout.addWidget(self.label) self.value = TaggedListWidget() - self.value.setFixedWidth(400) + # self.value.setFixedWidth(400) # for entry in enums: # self.value.addItem(entry) layout.addWidget(self.value) diff --git a/debye_bec/bec_widgets/widgets/data_viewer/viewer.py b/debye_bec/bec_widgets/widgets/data_viewer/viewer.py index 4a99291..c10e989 100644 --- a/debye_bec/bec_widgets/widgets/data_viewer/viewer.py +++ b/debye_bec/bec_widgets/widgets/data_viewer/viewer.py @@ -2,15 +2,19 @@ HDF5 Viewer — qtpy + pyqtgraph + h5py """ +from typing import Literal, Optional, cast + import h5py import numpy as np import pyqtgraph as pg from bec_lib import bec_logger from bec_qthemes import material_icon +from bec_widgets.utils.colors import Colors from qtpy.QtCore import Qt from qtpy.QtGui import QColor, QFont from qtpy.QtWidgets import ( QAbstractItemView, + QApplication, QGroupBox, QHBoxLayout, QHeaderView, @@ -58,9 +62,9 @@ def _styled_plot_widget(**kwargs) -> pg.PlotWidget: pw = pg.PlotWidget(**kwargs) # pw.setBackground(pg.mkColor(*PG_PLOT_BG)) ax = pw.getPlotItem() - for side in ("left", "bottom", "right", "top"): - ax.getAxis(side).setPen(pg.mkPen(color=PG_GRID, width=1)) - ax.getAxis(side).setTextPen(pg.mkPen(color=PG_TEXT)) + # for side in ("left", "bottom", "right", "top"): + # ax.getAxis(side).setPen(pg.mkPen(color=PG_GRID, width=1)) + # ax.getAxis(side).setTextPen(pg.mkPen(color=PG_TEXT)) ax.showGrid(x=True, y=True, alpha=0.25) return pw @@ -171,6 +175,9 @@ class DataPanel(QWidget): self.stack_layout.setContentsMargins(0, 0, 0, 0) self._layout.addWidget(self.stack, 1) + self.plot = None + self.images = [] + self._current_data = None self._show_empty() @@ -246,6 +253,8 @@ class DataPanel(QWidget): x = np.arange(row_data.size, dtype=np.float32) pw = _styled_plot_widget() + self.plot = pw + self.apply_theme() pw.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) plot_item = pw.getPlotItem() @@ -308,10 +317,49 @@ class DataPanel(QWidget): else: self.stack_layout.addWidget(pw) + def apply_theme(self, theme: Optional[Literal["dark", "light"]] = None): + """ + Apply the theme + + Args: + theme (Optional[str]): Theme, either "dark", "light", or None. Defaults to None. + """ + if theme is None: + app = QApplication.instance() + theme = app.theme.theme # type: ignore + + bg_color = pg.getConfigOption("background") + fg_color = pg.getConfigOption("foreground") + try: # TODO: Remove try/except (plot or image might already be deleted!) + n_curves = len(self.plot.listDataItems()) + colors = Colors.golden_angle_color( + colormap="plasma", num=max(10, n_curves + 1), format="HEX" + ) + for idx, curve in enumerate(self.plot.listDataItems()): + curve.setPen(pg.mkPen(color=colors[idx])) + # Background + self.plot.setBackground(bg_color) + # Axes (tick marks, tick labels, axis line) + for axis in ["left", "bottom", "right", "top"]: + ax = self.plot.getAxis(axis) + ax.setPen(pg.mkPen(color=fg_color)) + ax.setTextPen(pg.mkPen(color=fg_color)) + except: + pass + + for image in self.images: + try: + image.getView().setBackgroundColor(bg_color) + image.ui.histogram.setBackground(bg_color) + except: + pass + # ── 2-D image ────────────────────────────────────────────────────────── def _show_image_2d(self, data): self._clear_stack() + self.images = [] + squeezed = np.squeeze(data) if squeezed.ndim > 2: squeezed = squeezed.reshape(-1, squeezed.shape[-1]) @@ -321,8 +369,10 @@ class DataPanel(QWidget): # ImageView gives us colorbar + histogram + zoom for free iv = pg.ImageView() - iv.getView().setBackgroundColor(pg.mkColor(*PG_BG)) - iv.ui.histogram.setBackground(pg.mkColor(*PG_PLOT_BG)) + self.images.append(iv) + self.apply_theme() + # iv.getView().setBackgroundColor(pg.mkColor(*PG_BG)) + # iv.ui.histogram.setBackground(pg.mkColor(*PG_PLOT_BG)) iv.ui.roiBtn.hide() iv.ui.menuBtn.hide() @@ -396,6 +446,15 @@ class HDF5Viewer(QMainWindow): if filepath: self.load_files([filepath]) + def apply_theme(self, theme: Literal["dark", "light"]): + """ + Apply the theme + + Args: + theme (str): Theme, either "dark" or "light" + """ + self.data_panel.apply_theme(theme) + 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: