mirror of
https://github.com/bec-project/bec_widgets.git
synced 2026-07-28 06:03:02 +02:00
fix(image): custom colorbar histogram with a custom right-click menu for better UX
This commit is contained in:
@@ -2188,7 +2188,7 @@ class Heatmap(RPCBase):
|
||||
self,
|
||||
enabled: "bool",
|
||||
style: "Literal['full', 'simple']" = "full",
|
||||
vrange: "tuple[int, int] | None" = None,
|
||||
vrange: "tuple[float, float] | None" = None,
|
||||
):
|
||||
"""
|
||||
Enable the colorbar and switch types of colorbars.
|
||||
@@ -2924,7 +2924,7 @@ class Image(RPCBase):
|
||||
self,
|
||||
enabled: "bool",
|
||||
style: "Literal['full', 'simple']" = "full",
|
||||
vrange: "tuple[int, int] | None" = None,
|
||||
vrange: "tuple[float, float] | None" = None,
|
||||
):
|
||||
"""
|
||||
Enable the colorbar and switch types of colorbars.
|
||||
|
||||
@@ -0,0 +1,484 @@
|
||||
"""Colorbar items with a BEC-specific right-click context menu.
|
||||
|
||||
pyqtgraph's :class:`~pyqtgraph.HistogramLUTItem` renders the colorbar histogram
|
||||
as an ordinary plot, so right-clicking it shows the generic plot context menu
|
||||
(``View All``, ``X/Y Axis``, ``Mouse Mode`` …). That is confusing for a colorbar,
|
||||
where the only ranges that matter are the image color levels and the histogram
|
||||
display range. :class:`BECHistogramLUTItem` replaces that menu on the histogram
|
||||
view with a focused one and leaves the gradient (colormap) menu untouched.
|
||||
|
||||
:class:`BECColorBarItem` gives the simple colorbar (:class:`~pyqtgraph.ColorBarItem`)
|
||||
the same focused menu (minus the histogram-specific entries), replacing pyqtgraph's
|
||||
built-in colormap-only menu which would bypass the widget's colormap handling.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Callable
|
||||
|
||||
import pyqtgraph as pg
|
||||
from pyqtgraph.widgets.ColorMapMenu import ColorMapMenu
|
||||
from qtpy.QtCore import Qt, Signal
|
||||
from qtpy.QtWidgets import (
|
||||
QDialog,
|
||||
QDialogButtonBox,
|
||||
QFormLayout,
|
||||
QLabel,
|
||||
QLayout,
|
||||
QMenu,
|
||||
QVBoxLayout,
|
||||
QWidget,
|
||||
)
|
||||
|
||||
from bec_widgets.widgets.utility.spinbox.decimal_spinbox import BECSpinBox
|
||||
|
||||
|
||||
def _graphics_item_dialog_parent(item: pg.GraphicsObject) -> QWidget | None:
|
||||
"""Return a QWidget to parent modal dialogs to (the item's host graphics view).
|
||||
|
||||
Args:
|
||||
item (pg.GraphicsObject): The graphics item requesting a dialog.
|
||||
|
||||
Returns:
|
||||
QWidget | None: The graphics view hosting the item, or None if unavailable.
|
||||
"""
|
||||
scene = item.scene()
|
||||
if scene is not None:
|
||||
views = scene.views()
|
||||
if views:
|
||||
return views[0]
|
||||
return None
|
||||
|
||||
|
||||
def _make_bec_colormap_menu(on_triggered: Callable) -> ColorMapMenu:
|
||||
"""Create the "Select colormap" submenu used by the BEC colorbar menus.
|
||||
|
||||
Args:
|
||||
on_triggered(Callable) : Slot connected to the menu's ``sigColorMapTriggered``.
|
||||
|
||||
Returns:
|
||||
ColorMapMenu: The colormap submenu (entries are built lazily by pyqtgraph).
|
||||
"""
|
||||
colormap_menu = ColorMapMenu(showColorMapSubMenus=True)
|
||||
colormap_menu.setTitle("Select colormap")
|
||||
colormap_menu.sigColorMapTriggered.connect(on_triggered)
|
||||
# The 'None' entry clears the colormap in plain pyqtgraph; BEC always keeps a
|
||||
# valid named colormap, so hide it to avoid a no-op menu entry.
|
||||
for action in colormap_menu.actions():
|
||||
if action.text() == "None":
|
||||
action.setVisible(False)
|
||||
break
|
||||
return colormap_menu
|
||||
|
||||
|
||||
class RangeDialog(QDialog):
|
||||
"""
|
||||
Modal dialog to enter a ``(min, max)`` floating point range.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
parent: QWidget | None = None,
|
||||
*,
|
||||
title: str,
|
||||
label: str,
|
||||
vmin: float,
|
||||
vmax: float,
|
||||
decimals: int = 4,
|
||||
) -> None:
|
||||
"""
|
||||
Initialize the range dialog.
|
||||
|
||||
Args:
|
||||
parent (QWidget | None): The parent widget the dialog is modal to.
|
||||
title (str): The window title of the dialog.
|
||||
label (str): A description shown above the inputs. If empty, no label is shown.
|
||||
vmin (float): The initial value of the minimum spin box.
|
||||
vmax (float): The initial value of the maximum spin box.
|
||||
decimals (int): The number of decimals shown in both spin boxes.
|
||||
"""
|
||||
super().__init__(parent=parent)
|
||||
self.setWindowTitle(title)
|
||||
self.setModal(True)
|
||||
|
||||
layout = QVBoxLayout(self)
|
||||
# Fixed-size, non-resizable dialog that hugs its contents.
|
||||
layout.setSizeConstraint(QLayout.SizeConstraint.SetFixedSize)
|
||||
if label:
|
||||
layout.addWidget(QLabel(label))
|
||||
|
||||
form = QFormLayout()
|
||||
self.min_spinbox = BECSpinBox(parent=self)
|
||||
self.max_spinbox = BECSpinBox(parent=self)
|
||||
for spinbox, value in ((self.min_spinbox, vmin), (self.max_spinbox, vmax)):
|
||||
spinbox.setDecimals(decimals)
|
||||
spinbox.setValue(float(value))
|
||||
form.addRow("Minimum:", self.min_spinbox)
|
||||
form.addRow("Maximum:", self.max_spinbox)
|
||||
layout.addLayout(form)
|
||||
|
||||
self.button_box = QDialogButtonBox(
|
||||
QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel, parent=self
|
||||
)
|
||||
self.button_box.accepted.connect(self.accept)
|
||||
self.button_box.rejected.connect(self.reject)
|
||||
layout.addWidget(self.button_box)
|
||||
|
||||
def get_range(self) -> tuple[float, float]:
|
||||
"""
|
||||
Return the entered range, ordered so that ``min <= max``.
|
||||
|
||||
Returns:
|
||||
tuple[float, float]: The (minimum, maximum) values entered by the user.
|
||||
"""
|
||||
low, high = self.min_spinbox.value(), self.max_spinbox.value()
|
||||
return (low, high) if low <= high else (high, low)
|
||||
|
||||
@classmethod
|
||||
def get_new_range(
|
||||
cls,
|
||||
parent: QWidget | None,
|
||||
*,
|
||||
title: str,
|
||||
label: str,
|
||||
vmin: float,
|
||||
vmax: float,
|
||||
decimals: int = 4,
|
||||
) -> tuple[float, float] | None:
|
||||
"""
|
||||
Show the dialog modally and return the new range.
|
||||
|
||||
Args:
|
||||
parent (QWidget | None): The parent widget the dialog is modal to.
|
||||
title (str): The window title of the dialog.
|
||||
label (str): A description shown above the inputs.
|
||||
vmin (float): The initial value of the minimum spin box.
|
||||
vmax (float): The initial value of the maximum spin box.
|
||||
decimals (int): The number of decimals shown in both spin boxes.
|
||||
|
||||
Returns:
|
||||
tuple[float, float] | None: The new (min, max) range, or None if cancelled.
|
||||
"""
|
||||
dialog = cls(parent, title=title, label=label, vmin=vmin, vmax=vmax, decimals=decimals)
|
||||
try:
|
||||
if dialog.exec() == QDialog.DialogCode.Accepted:
|
||||
return dialog.get_range()
|
||||
return None
|
||||
finally:
|
||||
dialog.deleteLater()
|
||||
|
||||
|
||||
class BECHistogramLUTItem(pg.HistogramLUTItem):
|
||||
"""
|
||||
:class:`~pyqtgraph.HistogramLUTItem` with a BEC-specific context menu.
|
||||
|
||||
The default pyqtgraph context menu on the histogram view is replaced with one
|
||||
exposing the controls that matter for a colorbar:
|
||||
|
||||
* **Colormap** -- a submenu (pyqtgraph's :class:`~pyqtgraph.widgets.ColorMapMenu.ColorMapMenu`)
|
||||
to pick the image colormap. The choice is emitted through
|
||||
:attr:`sigColorMapChangeRequested` so the owning image widget applies it through
|
||||
its usual colormap handling (config + multi-layer sync + colorbar).
|
||||
* **Image scaling (levels)** -- the ``LinearRegionItem`` mapping data values to
|
||||
colors. Changes are emitted through :attr:`sigColorLevelsChangeRequested` and
|
||||
:attr:`sigAutoLevelsRequested` so the owning image widget keeps autorange and
|
||||
multi-layer state consistent (rather than mutating levels here directly).
|
||||
* **Histogram display range** -- the visible range of the histogram plot, applied
|
||||
directly via :meth:`~pyqtgraph.HistogramLUTItem.setHistogramRange` /
|
||||
:meth:`~pyqtgraph.HistogramLUTItem.autoHistogramRange`.
|
||||
|
||||
The gradient (colormap) context menu on the right edge is left untouched.
|
||||
"""
|
||||
|
||||
#: Emitted with the chosen colormap name when the user picks one from the menu.
|
||||
sigColorMapChangeRequested = Signal(str)
|
||||
#: Emitted with ``(vmin, vmax)`` when the user sets explicit color levels.
|
||||
sigColorLevelsChangeRequested = Signal(tuple)
|
||||
#: Emitted when the user requests autoscaling of the color levels.
|
||||
sigAutoLevelsRequested = Signal()
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self._bec_menu: QMenu | None = None
|
||||
self._colormap_menu: ColorMapMenu | None = None
|
||||
self._cleaned_up_triggered = False
|
||||
self._install_bec_context_menu()
|
||||
|
||||
################################################################################
|
||||
# Context menu
|
||||
|
||||
def _install_bec_context_menu(self) -> None:
|
||||
"""
|
||||
Build the BEC menu and route the histogram view right-clicks to it.
|
||||
"""
|
||||
menu = QMenu()
|
||||
|
||||
menu.addSection("Colormap")
|
||||
self._colormap_menu = _make_bec_colormap_menu(self._on_colormap_triggered)
|
||||
menu.addMenu(self._colormap_menu)
|
||||
|
||||
menu.addSection("Image scaling (levels)")
|
||||
set_levels = menu.addAction("Set levels…")
|
||||
set_levels.setToolTip("Set the data range mapped to the colormap (image scaling).")
|
||||
set_levels.triggered.connect(self._open_levels_dialog)
|
||||
auto_levels = menu.addAction("Autoscale levels")
|
||||
auto_levels.triggered.connect(self.sigAutoLevelsRequested)
|
||||
|
||||
menu.addSection("Histogram display range")
|
||||
set_hist = menu.addAction("Set histogram range…")
|
||||
set_hist.setToolTip("Set the visible range of the histogram plot.")
|
||||
set_hist.triggered.connect(self._open_histogram_range_dialog)
|
||||
reset_hist = menu.addAction("Reset histogram range")
|
||||
reset_hist.triggered.connect(self.autoHistogramRange)
|
||||
|
||||
self._bec_menu = menu
|
||||
|
||||
# Route right-clicks on the histogram ViewBox to the BEC menu only. The
|
||||
# original ViewBoxMenu (self.vb.menu) is left in place but never shown, so
|
||||
# internal pyqtgraph calls such as updateViewLists() keep working.
|
||||
vb = self.vb
|
||||
vb.getMenu = self._vb_get_menu
|
||||
vb.getContextMenus = self._vb_get_context_menus
|
||||
vb.raiseContextMenu = self._vb_raise_context_menu
|
||||
|
||||
def _vb_get_menu(self, ev):
|
||||
"""
|
||||
Return the BEC menu instead of the ViewBox's default menu.
|
||||
|
||||
Args:
|
||||
ev: The mouse event that requested the menu (unused).
|
||||
|
||||
Returns:
|
||||
QMenu | None: The BEC context menu.
|
||||
"""
|
||||
return self._bec_menu
|
||||
|
||||
def _vb_get_context_menus(self, event):
|
||||
"""
|
||||
Return the BEC menu actions for aggregation into child-item menus.
|
||||
|
||||
Args:
|
||||
event: The mouse event that requested the menu (unused).
|
||||
|
||||
Returns:
|
||||
list: The actions of the BEC context menu, or an empty list.
|
||||
"""
|
||||
return self._bec_menu.actions() if self._bec_menu is not None else []
|
||||
|
||||
def _vb_raise_context_menu(self, ev):
|
||||
"""
|
||||
Pop up the BEC menu at the event position.
|
||||
|
||||
Args:
|
||||
ev: The mouse event that triggered the context menu.
|
||||
"""
|
||||
if self._bec_menu is not None:
|
||||
self._bec_menu.popup(ev.screenPos().toPoint())
|
||||
|
||||
def _on_colormap_triggered(self, cmap) -> None:
|
||||
"""
|
||||
Forward a colormap chosen in the submenu as a name on the BEC signal.
|
||||
|
||||
Args:
|
||||
cmap (pyqtgraph.ColorMap): The colormap selected in the submenu.
|
||||
"""
|
||||
name = getattr(cmap, "name", None)
|
||||
if name:
|
||||
self.sigColorMapChangeRequested.emit(name)
|
||||
|
||||
################################################################################
|
||||
# Actions
|
||||
|
||||
def _open_levels_dialog(self) -> None:
|
||||
"""
|
||||
Open the dialog to set the image color levels (image scaling).
|
||||
"""
|
||||
vmin, vmax = (float(x) for x in self.getLevels())
|
||||
new_range = RangeDialog.get_new_range(
|
||||
_graphics_item_dialog_parent(self),
|
||||
title="Set color levels",
|
||||
label="Data values mapped to the colormap (image scaling).",
|
||||
vmin=vmin,
|
||||
vmax=vmax,
|
||||
)
|
||||
if new_range is not None:
|
||||
self.sigColorLevelsChangeRequested.emit(new_range)
|
||||
|
||||
def _open_histogram_range_dialog(self) -> None:
|
||||
"""
|
||||
Open the dialog to set the histogram's display range.
|
||||
"""
|
||||
vmin, vmax = (float(x) for x in self.getHistogramRange())
|
||||
new_range = RangeDialog.get_new_range(
|
||||
_graphics_item_dialog_parent(self),
|
||||
title="Set histogram range",
|
||||
label="Visible range of the histogram plot.",
|
||||
vmin=vmin,
|
||||
vmax=vmax,
|
||||
)
|
||||
if new_range is not None:
|
||||
self.setHistogramRange(*new_range)
|
||||
|
||||
################################################################################
|
||||
# Cleanup
|
||||
|
||||
def cleanup(self) -> None:
|
||||
"""
|
||||
Tear down the BEC menu and the inherited pyqtgraph menus/dialogs.
|
||||
|
||||
``HistogramLUTItem`` owns several parentless top-level widgets (the ViewBox
|
||||
menu, the gradient ColorMapMenu and its QColorDialog). They must be closed
|
||||
explicitly, otherwise they linger as leaked top-level widgets.
|
||||
"""
|
||||
# One-shot: the teardown below cannot be re-run — the inherited menus are deleted
|
||||
# without being reset to None, so a second pass would close a dead C++ object.
|
||||
if self._cleaned_up_triggered:
|
||||
return
|
||||
self._cleaned_up_triggered = True
|
||||
|
||||
# Restore the patched ViewBox context-menu methods.
|
||||
vb_dict = self.vb.__dict__
|
||||
for name in ("getMenu", "getContextMenus", "raiseContextMenu"):
|
||||
vb_dict.pop(name, None)
|
||||
|
||||
# Tear down the BEC context menu and its colormap submenu.
|
||||
if self._colormap_menu is not None:
|
||||
self._colormap_menu.close()
|
||||
self._colormap_menu.deleteLater()
|
||||
self._colormap_menu = None
|
||||
if self._bec_menu is not None:
|
||||
self._bec_menu.close()
|
||||
self._bec_menu.deleteLater()
|
||||
self._bec_menu = None
|
||||
|
||||
# Tear down the inherited pyqtgraph menus and dialogs.
|
||||
self.vb.menu.close()
|
||||
self.vb.menu.deleteLater()
|
||||
self.gradient.menu.close()
|
||||
self.gradient.menu.deleteLater()
|
||||
self.gradient.colorDialog.close()
|
||||
self.gradient.colorDialog.deleteLater()
|
||||
|
||||
|
||||
class BECColorBarItem(pg.ColorBarItem):
|
||||
"""
|
||||
:class:`~pyqtgraph.ColorBarItem` (the "simple" colorbar) with the BEC context menu.
|
||||
|
||||
pyqtgraph's built-in right-click menu on ColorBarItem only offers colormap
|
||||
selection and applies it directly to the item, bypassing the owning widget's
|
||||
colormap handling (config + multi-layer sync). This subclass replaces it with
|
||||
the same focused menu as :class:`BECHistogramLUTItem` (minus the
|
||||
histogram-specific entries) and forwards every request through signals so the
|
||||
owning image widget stays the single source of truth.
|
||||
"""
|
||||
|
||||
#: Emitted with the chosen colormap name when the user picks one from the menu.
|
||||
sigColorMapChangeRequested = Signal(str)
|
||||
#: Emitted with ``(vmin, vmax)`` when the user sets explicit color levels.
|
||||
sigColorLevelsChangeRequested = Signal(tuple)
|
||||
#: Emitted when the user requests autoscaling of the color levels.
|
||||
sigAutoLevelsRequested = Signal()
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
# Disable pyqtgraph's own colormap menu; the BEC menu replaces it.
|
||||
kwargs.setdefault("colorMapMenu", False)
|
||||
super().__init__(*args, **kwargs)
|
||||
self._bec_menu: QMenu | None = None
|
||||
self._colormap_menu: ColorMapMenu | None = None
|
||||
self._cleaned_up_triggered = False
|
||||
self._install_bec_context_menu()
|
||||
|
||||
################################################################################
|
||||
# Context menu
|
||||
|
||||
def _install_bec_context_menu(self) -> None:
|
||||
"""
|
||||
Build the BEC menu shown on right-clicks anywhere on the colorbar.
|
||||
"""
|
||||
menu = QMenu()
|
||||
|
||||
menu.addSection("Colormap")
|
||||
self._colormap_menu = _make_bec_colormap_menu(self._on_colormap_triggered)
|
||||
menu.addMenu(self._colormap_menu)
|
||||
|
||||
menu.addSection("Image scaling (levels)")
|
||||
set_levels = menu.addAction("Set levels…")
|
||||
set_levels.setToolTip("Set the data range mapped to the colormap (image scaling).")
|
||||
set_levels.triggered.connect(self._open_levels_dialog)
|
||||
auto_levels = menu.addAction("Autoscale levels")
|
||||
auto_levels.triggered.connect(self.sigAutoLevelsRequested)
|
||||
|
||||
self._bec_menu = menu
|
||||
|
||||
def mouseClickEvent(self, ev):
|
||||
"""
|
||||
Show the BEC menu on right-click; defer to pyqtgraph otherwise.
|
||||
|
||||
Args:
|
||||
ev: The mouse click event.
|
||||
"""
|
||||
if ev.button() == Qt.MouseButton.RightButton and self._bec_menu is not None:
|
||||
ev.accept()
|
||||
self._bec_menu.popup(ev.screenPos().toPoint())
|
||||
return
|
||||
super().mouseClickEvent(ev)
|
||||
|
||||
def _on_colormap_triggered(self, cmap) -> None:
|
||||
"""
|
||||
Forward a colormap chosen in the submenu as a name on the BEC signal.
|
||||
|
||||
Args:
|
||||
cmap (pyqtgraph.ColorMap): The colormap selected in the submenu.
|
||||
"""
|
||||
name = getattr(cmap, "name", None)
|
||||
if name:
|
||||
self.sigColorMapChangeRequested.emit(name)
|
||||
|
||||
def _open_levels_dialog(self) -> None:
|
||||
"""
|
||||
Open the dialog to set the image color levels (image scaling).
|
||||
"""
|
||||
vmin, vmax = (float(x) for x in self.levels())
|
||||
new_range = RangeDialog.get_new_range(
|
||||
_graphics_item_dialog_parent(self),
|
||||
title="Set color levels",
|
||||
label="Data values mapped to the colormap (image scaling).",
|
||||
vmin=vmin,
|
||||
vmax=vmax,
|
||||
)
|
||||
if new_range is not None:
|
||||
self.sigColorLevelsChangeRequested.emit(new_range)
|
||||
|
||||
################################################################################
|
||||
# Cleanup
|
||||
|
||||
def cleanup(self) -> None:
|
||||
"""
|
||||
Tear down the BEC menu and the menus inherited from :class:`~pyqtgraph.PlotItem`.
|
||||
|
||||
``ColorBarItem`` is a PlotItem, so it owns a parentless ViewBox menu and a
|
||||
plot-options (``ctrlMenu``) menu even though both are disabled; close them
|
||||
explicitly so they do not linger as leaked top-level widgets.
|
||||
"""
|
||||
# One-shot: the teardown below cannot be re-run — the inherited menus are deleted
|
||||
# without being reset to None, so a second pass would close a dead C++ object.
|
||||
if self._cleaned_up_triggered:
|
||||
return
|
||||
self._cleaned_up_triggered = True
|
||||
|
||||
if self._colormap_menu is not None:
|
||||
self._colormap_menu.close()
|
||||
self._colormap_menu.deleteLater()
|
||||
self._colormap_menu = None
|
||||
if self._bec_menu is not None:
|
||||
self._bec_menu.close()
|
||||
self._bec_menu.deleteLater()
|
||||
self._bec_menu = None
|
||||
|
||||
# ColorBarItem disables its ViewBox menu, which leaves vb.menu as None.
|
||||
if self.vb.menu is not None:
|
||||
self.vb.menu.close()
|
||||
self.vb.menu.deleteLater()
|
||||
if self.ctrlMenu is not None:
|
||||
self.ctrlMenu.close()
|
||||
self.ctrlMenu.deleteLater()
|
||||
@@ -14,6 +14,10 @@ from bec_widgets.utils.container_utils import WidgetContainerUtils
|
||||
from bec_widgets.utils.error_popups import SafeProperty, SafeSlot
|
||||
from bec_widgets.utils.side_panel import SidePanel
|
||||
from bec_widgets.utils.toolbars.actions import MaterialIconAction, SwitchableToolBarAction
|
||||
from bec_widgets.widgets.plots.image.bec_histogram_lut_item import (
|
||||
BECColorBarItem,
|
||||
BECHistogramLUTItem,
|
||||
)
|
||||
from bec_widgets.widgets.plots.image.image_item import ImageItem
|
||||
from bec_widgets.widgets.plots.image.image_roi_plot import ImageROIPlot
|
||||
from bec_widgets.widgets.plots.image.setting_widgets.image_roi_tree import ROIPropertyTree
|
||||
@@ -476,7 +480,7 @@ class ImageBase(PlotBase):
|
||||
self,
|
||||
enabled: bool,
|
||||
style: Literal["full", "simple"] = "full",
|
||||
vrange: tuple[int, int] | None = None,
|
||||
vrange: tuple[float, float] | None = None,
|
||||
):
|
||||
"""
|
||||
Enable the colorbar and switch types of colorbars.
|
||||
@@ -486,13 +490,14 @@ class ImageBase(PlotBase):
|
||||
style(Literal["full", "simple"]): The type of colorbar to enable.
|
||||
vrange(tuple): The range of values to use for the colorbar.
|
||||
"""
|
||||
autorange_state = self.layer_manager["main"].image.autorange
|
||||
if enabled and style not in ("full", "simple"):
|
||||
raise ValueError(f"Invalid colorbar style '{style}'; use 'full' or 'simple'.")
|
||||
|
||||
main_image = self.layer_manager["main"].image
|
||||
autorange_state = main_image.autorange
|
||||
saved_vrange = main_image.v_range
|
||||
if enabled:
|
||||
if self._color_bar:
|
||||
if self.config.color_bar == "full":
|
||||
self.cleanup_histogram_lut_item(self._color_bar)
|
||||
self.plot_widget.removeItem(self._color_bar)
|
||||
self._color_bar = None
|
||||
self._remove_color_bar()
|
||||
|
||||
def disable_autorange():
|
||||
logger.info("Disabling autorange")
|
||||
@@ -500,31 +505,52 @@ class ImageBase(PlotBase):
|
||||
|
||||
if style == "simple":
|
||||
cmap = Colors.get_colormap(self.config.color_map)
|
||||
self._color_bar = pg.ColorBarItem(colorMap=cmap)
|
||||
self._color_bar.setImageItem(self.layer_manager["main"].image)
|
||||
self._color_bar = BECColorBarItem(colorMap=cmap)
|
||||
self._color_bar.setImageItem(main_image)
|
||||
self._color_bar.sigLevelsChangeFinished.connect(disable_autorange)
|
||||
self.config.color_bar = "simple"
|
||||
|
||||
elif style == "full":
|
||||
self._color_bar = pg.HistogramLUTItem()
|
||||
self._color_bar.setImageItem(self.layer_manager["main"].image)
|
||||
self._color_bar = BECHistogramLUTItem()
|
||||
self._color_bar.setImageItem(main_image)
|
||||
self.config.color_bar = "full"
|
||||
self._apply_colormap_to_colorbar(self.config.color_map)
|
||||
self._color_bar.sigLevelsChanged.connect(disable_autorange)
|
||||
|
||||
# Custom colorbar context menu (replaces pyqtgraph's default menus).
|
||||
self._color_bar.sigColorMapChangeRequested.connect(self._set_colormap_from_menu)
|
||||
self._color_bar.sigColorLevelsChangeRequested.connect(self._set_vrange_from_menu)
|
||||
self._color_bar.sigAutoLevelsRequested.connect(self._autorange_from_menu)
|
||||
|
||||
self.plot_widget.addItem(self._color_bar, row=0, col=1)
|
||||
else:
|
||||
if self._color_bar:
|
||||
self.plot_widget.removeItem(self._color_bar)
|
||||
self._color_bar = None
|
||||
self._remove_color_bar()
|
||||
self.config.color_bar = None
|
||||
|
||||
self.autorange = autorange_state
|
||||
if enabled and not autorange_state:
|
||||
# Attaching a colorbar re-levels the image (HistogramLUTItem's
|
||||
# setImageItem auto-levels); restore the previous manual levels so
|
||||
# switching between colorbar styles keeps the levels in sync.
|
||||
self._set_vrange(saved_vrange, disable_autorange=False)
|
||||
self._sync_colorbar_actions()
|
||||
|
||||
if vrange: # should be at the end to disable the autorange if defined
|
||||
self.v_range = vrange
|
||||
|
||||
def _remove_color_bar(self) -> None:
|
||||
"""
|
||||
Remove the current colorbar from the plot and fully tear it down, including
|
||||
the parentless menus/dialogs it owns.
|
||||
"""
|
||||
if self._color_bar is None:
|
||||
return
|
||||
if isinstance(self._color_bar, (BECHistogramLUTItem, BECColorBarItem)):
|
||||
self._color_bar.cleanup()
|
||||
self.plot_widget.removeItem(self._color_bar)
|
||||
self._color_bar.deleteLater()
|
||||
self._color_bar = None
|
||||
|
||||
def _apply_colormap_to_colorbar(self, color_map: str) -> None:
|
||||
if not self._color_bar:
|
||||
return
|
||||
@@ -974,6 +1000,21 @@ class ImageBase(PlotBase):
|
||||
|
||||
# self.toolbar.components.get_action("image_autorange").set_state_all(False)
|
||||
|
||||
@SafeSlot(str)
|
||||
def _set_colormap_from_menu(self, color_map: str):
|
||||
"""Apply a colormap chosen from the colorbar context menu."""
|
||||
self.color_map = color_map
|
||||
|
||||
@SafeSlot(object)
|
||||
def _set_vrange_from_menu(self, vrange: tuple[float, float]):
|
||||
"""Apply explicit color levels requested from the colorbar context menu."""
|
||||
self._set_vrange(vrange, disable_autorange=True)
|
||||
|
||||
@SafeSlot()
|
||||
def _autorange_from_menu(self):
|
||||
"""Enable autorange (current mode) from the colorbar context menu."""
|
||||
self.autorange = True
|
||||
|
||||
@property
|
||||
def v_min(self) -> float:
|
||||
"""
|
||||
@@ -1171,22 +1212,6 @@ class ImageBase(PlotBase):
|
||||
else:
|
||||
colorbar_switch.set_state_all(False)
|
||||
|
||||
@staticmethod
|
||||
def cleanup_histogram_lut_item(histogram_lut_item: pg.HistogramLUTItem):
|
||||
"""
|
||||
Clean up HistogramLUTItem safely, including open ViewBox menus and child widgets.
|
||||
|
||||
Args:
|
||||
histogram_lut_item(pg.HistogramLUTItem): The HistogramLUTItem to clean up.
|
||||
"""
|
||||
histogram_lut_item.vb.menu.close()
|
||||
histogram_lut_item.vb.menu.deleteLater()
|
||||
|
||||
histogram_lut_item.gradient.menu.close()
|
||||
histogram_lut_item.gradient.menu.deleteLater()
|
||||
histogram_lut_item.gradient.colorDialog.close()
|
||||
histogram_lut_item.gradient.colorDialog.deleteLater()
|
||||
|
||||
def cleanup(self):
|
||||
"""
|
||||
Cleanup the widget.
|
||||
@@ -1199,13 +1224,7 @@ class ImageBase(PlotBase):
|
||||
roi.remove()
|
||||
|
||||
# Colorbar Cleanup
|
||||
if self._color_bar:
|
||||
if self.config.color_bar == "full":
|
||||
self.cleanup_histogram_lut_item(self._color_bar)
|
||||
if self.config.color_bar == "simple":
|
||||
self.plot_widget.removeItem(self._color_bar)
|
||||
self._color_bar.deleteLater()
|
||||
self._color_bar = None
|
||||
self._remove_color_bar()
|
||||
|
||||
# Popup cleanup
|
||||
if self.roi_manager_dialog is not None:
|
||||
|
||||
@@ -0,0 +1,240 @@
|
||||
import numpy as np
|
||||
import pyqtgraph as pg
|
||||
import pytest
|
||||
from qtpy.QtCore import QPoint, QPointF, Qt
|
||||
|
||||
from bec_widgets.widgets.plots.image.bec_histogram_lut_item import (
|
||||
BECColorBarItem,
|
||||
BECHistogramLUTItem,
|
||||
RangeDialog,
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def hist_item(qtbot):
|
||||
"""A BECHistogramLUTItem living inside a real scene, cleaned up after the test."""
|
||||
glw = pg.GraphicsLayoutWidget()
|
||||
qtbot.addWidget(glw)
|
||||
qtbot.waitExposed(glw)
|
||||
item = BECHistogramLUTItem()
|
||||
glw.addItem(item, row=0, col=0)
|
||||
item.setImageItem(pg.ImageItem(np.arange(100, dtype=float).reshape(10, 10)))
|
||||
yield item
|
||||
item.cleanup()
|
||||
|
||||
|
||||
def test_menu_actions_present(hist_item):
|
||||
labels = [a.text() for a in hist_item._bec_menu.actions() if a.text()]
|
||||
assert labels == [
|
||||
"Colormap",
|
||||
"Select colormap",
|
||||
"Image scaling (levels)",
|
||||
"Set levels…",
|
||||
"Autoscale levels",
|
||||
"Histogram display range",
|
||||
"Set histogram range…",
|
||||
"Reset histogram range",
|
||||
]
|
||||
|
||||
|
||||
def _menu_action(hist_item: BECHistogramLUTItem, text: str):
|
||||
return next(a for a in hist_item._bec_menu.actions() if a.text() == text)
|
||||
|
||||
|
||||
def test_colormap_submenu_forwards_name(hist_item):
|
||||
"""Picking a colormap in the submenu re-emits its name on the BEC signal."""
|
||||
assert hist_item._colormap_menu.actions() # the pyqtgraph colormap menu is populated
|
||||
received = []
|
||||
hist_item.sigColorMapChangeRequested.connect(received.append)
|
||||
hist_item._colormap_menu.sigColorMapTriggered.emit(pg.colormap.get("viridis"))
|
||||
assert received == ["viridis"]
|
||||
|
||||
|
||||
def test_default_viewbox_menu_bypassed(hist_item):
|
||||
"""The histogram view shows the BEC menu, not pyqtgraph's default plot menu."""
|
||||
vb = hist_item.vb
|
||||
assert vb.getMenu(None) is hist_item._bec_menu
|
||||
assert vb.getContextMenus(None) == hist_item._bec_menu.actions()
|
||||
# The original ViewBoxMenu is kept alive (not replaced) so pyqtgraph internals
|
||||
# such as updateViewLists() keep working.
|
||||
assert vb.menu.__class__.__name__ == "ViewBoxMenu"
|
||||
vb.updateViewLists() # would raise AttributeError if vb.menu were a plain QMenu
|
||||
|
||||
|
||||
def test_raise_context_menu_pops_bec_menu(hist_item, monkeypatch):
|
||||
calls = []
|
||||
monkeypatch.setattr(hist_item._bec_menu, "popup", lambda pos: calls.append(pos))
|
||||
|
||||
class _Pos:
|
||||
def toPoint(self):
|
||||
return QPoint(1, 2)
|
||||
|
||||
class _Event:
|
||||
def screenPos(self):
|
||||
return _Pos()
|
||||
|
||||
hist_item.vb.raiseContextMenu(_Event())
|
||||
assert calls == [QPoint(1, 2)]
|
||||
|
||||
|
||||
def test_set_levels_action_emits_request(hist_item, monkeypatch):
|
||||
monkeypatch.setattr(RangeDialog, "get_new_range", staticmethod(lambda *a, **k: (3.0, 9.0)))
|
||||
received = []
|
||||
hist_item.sigColorLevelsChangeRequested.connect(received.append)
|
||||
_menu_action(hist_item, "Set levels…").trigger()
|
||||
assert received == [(3.0, 9.0)]
|
||||
|
||||
|
||||
def test_set_levels_action_cancel_emits_nothing(hist_item, monkeypatch):
|
||||
monkeypatch.setattr(RangeDialog, "get_new_range", staticmethod(lambda *a, **k: None))
|
||||
received = []
|
||||
hist_item.sigColorLevelsChangeRequested.connect(received.append)
|
||||
_menu_action(hist_item, "Set levels…").trigger()
|
||||
assert received == []
|
||||
|
||||
|
||||
def test_autoscale_levels_action_emits_request(hist_item):
|
||||
received = []
|
||||
hist_item.sigAutoLevelsRequested.connect(lambda: received.append(True))
|
||||
_menu_action(hist_item, "Autoscale levels").trigger()
|
||||
assert received == [True]
|
||||
|
||||
|
||||
def test_set_histogram_range_action_applies_range(hist_item, monkeypatch):
|
||||
monkeypatch.setattr(RangeDialog, "get_new_range", staticmethod(lambda *a, **k: (5.0, 25.0)))
|
||||
recorded = []
|
||||
monkeypatch.setattr(hist_item, "setHistogramRange", lambda mn, mx: recorded.append((mn, mx)))
|
||||
_menu_action(hist_item, "Set histogram range…").trigger()
|
||||
assert recorded == [(5.0, 25.0)]
|
||||
|
||||
|
||||
def test_reset_histogram_range_action_reenables_histogram_autorange(hist_item):
|
||||
hist_item.setHistogramRange(5.0, 25.0)
|
||||
assert hist_item.vb.autoRangeEnabled()[1] is False
|
||||
|
||||
_menu_action(hist_item, "Reset histogram range").trigger()
|
||||
|
||||
assert hist_item.vb.autoRangeEnabled()[1]
|
||||
|
||||
|
||||
def test_cleanup_restores_viewbox_and_clears_menu(hist_item):
|
||||
vb = hist_item.vb
|
||||
assert {"getMenu", "getContextMenus", "raiseContextMenu"} <= set(vb.__dict__)
|
||||
hist_item.cleanup()
|
||||
assert not ({"getMenu", "getContextMenus", "raiseContextMenu"} & set(vb.__dict__))
|
||||
assert hist_item._bec_menu is None
|
||||
assert hist_item._colormap_menu is None
|
||||
# idempotent: cleaning up again (as the fixture teardown will) must not raise
|
||||
hist_item.cleanup()
|
||||
|
||||
|
||||
def test_range_dialog_orders_values(qtbot):
|
||||
dialog = RangeDialog(None, title="t", label="l", vmin=1.0, vmax=4.0)
|
||||
qtbot.addWidget(dialog)
|
||||
qtbot.waitExposed(dialog)
|
||||
dialog.min_spinbox.setValue(8.0)
|
||||
dialog.max_spinbox.setValue(2.0)
|
||||
assert dialog.get_range() == (2.0, 8.0)
|
||||
|
||||
|
||||
def test_range_dialog_returns_fractional_values(qtbot):
|
||||
"""The dialog hands fractional values through unchanged (regression context:
|
||||
they used to be truncated later by the QPoint-based tuple conversion)."""
|
||||
dialog = RangeDialog(None, title="t", label="l", vmin=0.25, vmax=10.5)
|
||||
qtbot.addWidget(dialog)
|
||||
qtbot.waitExposed(dialog)
|
||||
assert dialog.get_range() == (0.25, 10.5)
|
||||
|
||||
|
||||
def test_colormap_menu_hides_none_entry(hist_item):
|
||||
none_actions = [a for a in hist_item._colormap_menu.actions() if a.text() == "None"]
|
||||
assert none_actions and not none_actions[0].isVisible()
|
||||
|
||||
|
||||
################################################################################
|
||||
# BECColorBarItem (simple colorbar)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def color_bar_item(qtbot):
|
||||
"""A BECColorBarItem attached to an image inside a real scene."""
|
||||
glw = pg.GraphicsLayoutWidget()
|
||||
qtbot.addWidget(glw)
|
||||
qtbot.waitExposed(glw)
|
||||
image_item = pg.ImageItem(np.arange(100, dtype=float).reshape(10, 10))
|
||||
item = BECColorBarItem()
|
||||
item.setImageItem(image_item)
|
||||
glw.addItem(item, row=0, col=0)
|
||||
yield item
|
||||
item.cleanup()
|
||||
|
||||
|
||||
class _FakeRightClick:
|
||||
def __init__(self):
|
||||
self.accepted = False
|
||||
|
||||
def button(self):
|
||||
return Qt.MouseButton.RightButton
|
||||
|
||||
def accept(self):
|
||||
self.accepted = True
|
||||
|
||||
def screenPos(self):
|
||||
return QPointF(3.0, 4.0)
|
||||
|
||||
|
||||
def test_simple_colorbar_menu_actions_present(color_bar_item):
|
||||
labels = [a.text() for a in color_bar_item._bec_menu.actions() if a.text()]
|
||||
assert labels == [
|
||||
"Colormap",
|
||||
"Select colormap",
|
||||
"Image scaling (levels)",
|
||||
"Set levels…",
|
||||
"Autoscale levels",
|
||||
]
|
||||
|
||||
|
||||
def test_simple_colorbar_native_menu_disabled(color_bar_item):
|
||||
"""pyqtgraph's built-in colormap menu is off; the BEC menu replaces it."""
|
||||
assert color_bar_item.colorMapMenu is False
|
||||
|
||||
|
||||
def test_simple_colorbar_right_click_pops_bec_menu(color_bar_item, monkeypatch):
|
||||
calls = []
|
||||
monkeypatch.setattr(color_bar_item._bec_menu, "popup", lambda pos: calls.append(pos))
|
||||
ev = _FakeRightClick()
|
||||
color_bar_item.mouseClickEvent(ev)
|
||||
assert ev.accepted
|
||||
assert calls == [QPoint(3, 4)]
|
||||
|
||||
|
||||
def test_simple_colorbar_set_levels_action_emits_request(color_bar_item, monkeypatch):
|
||||
monkeypatch.setattr(RangeDialog, "get_new_range", staticmethod(lambda *a, **k: (3.0, 9.0)))
|
||||
received = []
|
||||
color_bar_item.sigColorLevelsChangeRequested.connect(received.append)
|
||||
action = next(a for a in color_bar_item._bec_menu.actions() if a.text() == "Set levels…")
|
||||
action.trigger()
|
||||
assert received == [(3.0, 9.0)]
|
||||
|
||||
|
||||
def test_simple_colorbar_autoscale_action_emits_request(color_bar_item):
|
||||
received = []
|
||||
color_bar_item.sigAutoLevelsRequested.connect(lambda: received.append(True))
|
||||
action = next(a for a in color_bar_item._bec_menu.actions() if a.text() == "Autoscale levels")
|
||||
action.trigger()
|
||||
assert received == [True]
|
||||
|
||||
|
||||
def test_simple_colorbar_colormap_submenu_forwards_name(color_bar_item):
|
||||
received = []
|
||||
color_bar_item.sigColorMapChangeRequested.connect(received.append)
|
||||
color_bar_item._colormap_menu.sigColorMapTriggered.emit(pg.colormap.get("viridis"))
|
||||
assert received == ["viridis"]
|
||||
|
||||
|
||||
def test_simple_colorbar_cleanup_idempotent(color_bar_item):
|
||||
color_bar_item.cleanup()
|
||||
assert color_bar_item._bec_menu is None
|
||||
assert color_bar_item._colormap_menu is None
|
||||
# idempotent: cleaning up again (as the fixture teardown will) must not raise
|
||||
color_bar_item.cleanup()
|
||||
@@ -4,6 +4,10 @@ import pytest
|
||||
from bec_lib.endpoints import MessageEndpoints
|
||||
from qtpy.QtCore import QPointF, Qt
|
||||
|
||||
from bec_widgets.widgets.plots.image.bec_histogram_lut_item import (
|
||||
BECColorBarItem,
|
||||
BECHistogramLUTItem,
|
||||
)
|
||||
from bec_widgets.widgets.plots.image.image import Image
|
||||
from bec_widgets.widgets.plots.image.image_processor import ImageProcessor, ProcessingConfig
|
||||
from tests.unit_tests.client_mocks import mocked_client
|
||||
@@ -124,6 +128,126 @@ def test_enable_full_colorbar(qtbot, mocked_client):
|
||||
assert bec_image_view.main_image.autorange_mode == "mean"
|
||||
|
||||
|
||||
def test_full_colorbar_uses_bec_histogram_lut_item(qtbot, mocked_client):
|
||||
bec_image_view = create_widget(qtbot, Image, client=mocked_client)
|
||||
bec_image_view.enable_full_colorbar = True
|
||||
color_bar = bec_image_view._color_bar
|
||||
assert isinstance(color_bar, BECHistogramLUTItem)
|
||||
# The confusing default pyqtgraph plot menu is replaced on the histogram view.
|
||||
assert color_bar.vb.getMenu(None) is color_bar._bec_menu
|
||||
|
||||
|
||||
def test_colorbar_menu_set_levels_updates_vrange(qtbot, mocked_client):
|
||||
bec_image_view = create_widget(qtbot, Image, client=mocked_client)
|
||||
bec_image_view.enable_full_colorbar = True
|
||||
assert bec_image_view.autorange is True
|
||||
|
||||
# Simulate the "Set levels…" context-menu action (image scaling via the region).
|
||||
bec_image_view._color_bar.sigColorLevelsChangeRequested.emit((0, 50))
|
||||
|
||||
assert bec_image_view.v_range == QPointF(0, 50)
|
||||
assert bec_image_view.main_image.levels == (0, 50)
|
||||
# Setting explicit levels disables autorange.
|
||||
assert bec_image_view.autorange is False
|
||||
|
||||
|
||||
def test_colorbar_menu_autoscale_enables_autorange(qtbot, mocked_client):
|
||||
bec_image_view = create_widget(qtbot, Image, client=mocked_client)
|
||||
bec_image_view.enable_full_colorbar = True
|
||||
bec_image_view.autorange = False
|
||||
|
||||
# Simulate the "Autoscale levels" context-menu action.
|
||||
bec_image_view._color_bar.sigAutoLevelsRequested.emit()
|
||||
|
||||
assert bec_image_view.autorange is True
|
||||
|
||||
|
||||
def test_colorbar_menu_colormap_updates_image(qtbot, mocked_client):
|
||||
bec_image_view = create_widget(qtbot, Image, client=mocked_client)
|
||||
bec_image_view.enable_full_colorbar = True
|
||||
|
||||
# Simulate picking a colormap from the colorbar context menu.
|
||||
bec_image_view._color_bar.sigColorMapChangeRequested.emit("viridis")
|
||||
|
||||
assert bec_image_view.color_map == "viridis"
|
||||
assert bec_image_view.config.color_map == "viridis"
|
||||
assert bec_image_view.main_image.color_map == "viridis"
|
||||
|
||||
|
||||
def test_simple_colorbar_uses_bec_colorbar_item(qtbot, mocked_client):
|
||||
bec_image_view = create_widget(qtbot, Image, client=mocked_client)
|
||||
bec_image_view.enable_simple_colorbar = True
|
||||
assert isinstance(bec_image_view._color_bar, BECColorBarItem)
|
||||
|
||||
|
||||
def test_simple_colorbar_menu_signals_update_image_state(qtbot, mocked_client):
|
||||
bec_image_view = create_widget(qtbot, Image, client=mocked_client)
|
||||
bec_image_view.enable_simple_colorbar = True
|
||||
|
||||
# "Set levels…" applies explicit levels and disables autorange.
|
||||
bec_image_view._color_bar.sigColorLevelsChangeRequested.emit((1.0, 42.0))
|
||||
assert bec_image_view.v_range == QPointF(1.0, 42.0)
|
||||
assert bec_image_view.autorange is False
|
||||
|
||||
# "Autoscale levels" re-enables autorange.
|
||||
bec_image_view._color_bar.sigAutoLevelsRequested.emit()
|
||||
assert bec_image_view.autorange is True
|
||||
|
||||
# Picking a colormap goes through the BEC colormap handling (config + image).
|
||||
bec_image_view._color_bar.sigColorMapChangeRequested.emit("viridis")
|
||||
assert bec_image_view.config.color_map == "viridis"
|
||||
assert bec_image_view.main_image.color_map == "viridis"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"first, second", [("full", "simple"), ("simple", "full"), ("full", "full")]
|
||||
)
|
||||
def test_switching_colorbar_styles_preserves_manual_levels(qtbot, mocked_client, first, second):
|
||||
"""Switching between colorbar styles must not reset manually set levels."""
|
||||
bec_image_view = create_widget(qtbot, Image, client=mocked_client)
|
||||
bec_image_view.main_image.set_data(np.arange(100, dtype=float).reshape(10, 10))
|
||||
bec_image_view.enable_colorbar(True, first)
|
||||
|
||||
bec_image_view.v_range = (5.0, 77.0)
|
||||
assert bec_image_view.autorange is False
|
||||
|
||||
bec_image_view.enable_colorbar(True, second)
|
||||
assert bec_image_view.autorange is False
|
||||
assert bec_image_view.v_range == QPointF(5.0, 77.0)
|
||||
assert bec_image_view.main_image.v_range == (5.0, 77.0)
|
||||
|
||||
|
||||
def test_switching_colorbar_styles_keeps_autorange(qtbot, mocked_client):
|
||||
"""With autorange on, switching colorbar styles keeps autorange enabled."""
|
||||
bec_image_view = create_widget(qtbot, Image, client=mocked_client)
|
||||
bec_image_view.main_image.set_data(np.arange(100, dtype=float).reshape(10, 10))
|
||||
bec_image_view.enable_full_colorbar = True
|
||||
assert bec_image_view.autorange is True
|
||||
|
||||
bec_image_view.enable_simple_colorbar = True
|
||||
assert bec_image_view.autorange is True
|
||||
|
||||
|
||||
@pytest.mark.parametrize("colorbar_type", ["simple", "full"])
|
||||
def test_disable_colorbar_cleans_up(qtbot, mocked_client, colorbar_type):
|
||||
"""Disabling the colorbar tears down its parentless menus (no leaked top-levels)."""
|
||||
bec_image_view = create_widget(qtbot, Image, client=mocked_client)
|
||||
bec_image_view.enable_colorbar(True, colorbar_type)
|
||||
color_bar = bec_image_view._color_bar
|
||||
|
||||
bec_image_view.enable_colorbar(False)
|
||||
|
||||
assert bec_image_view._color_bar is None
|
||||
assert bec_image_view.config.color_bar is None
|
||||
assert color_bar._cleaned_up_triggered is True
|
||||
|
||||
|
||||
def test_enable_colorbar_invalid_style_raises(qtbot, mocked_client):
|
||||
bec_image_view = create_widget(qtbot, Image, client=mocked_client)
|
||||
with pytest.raises(ValueError, match="Invalid colorbar style"):
|
||||
bec_image_view.enable_colorbar(True, "fancy")
|
||||
|
||||
|
||||
@pytest.mark.parametrize("colorbar_type", ["simple", "full"])
|
||||
def test_enable_colorbar_with_vrange(qtbot, mocked_client, colorbar_type):
|
||||
bec_image_view = create_widget(qtbot, Image, client=mocked_client)
|
||||
|
||||
Reference in New Issue
Block a user