mirror of
https://github.com/bec-project/bec_widgets.git
synced 2025-07-14 11:41:49 +02:00
feat(plot_base_next_gen): new type of plot base inherited from QWidget
This commit is contained in:
@ -20,6 +20,7 @@ from bec_widgets.widgets.containers.dock import BECDockArea
|
|||||||
from bec_widgets.widgets.containers.figure import BECFigure
|
from bec_widgets.widgets.containers.figure import BECFigure
|
||||||
from bec_widgets.widgets.containers.layout_manager.layout_manager import LayoutManagerWidget
|
from bec_widgets.widgets.containers.layout_manager.layout_manager import LayoutManagerWidget
|
||||||
from bec_widgets.widgets.editors.jupyter_console.jupyter_console import BECJupyterConsole
|
from bec_widgets.widgets.editors.jupyter_console.jupyter_console import BECJupyterConsole
|
||||||
|
from bec_widgets.widgets.plots_next_gen.plot_base import PlotBase
|
||||||
|
|
||||||
|
|
||||||
class JupyterConsoleWindow(QWidget): # pragma: no cover:
|
class JupyterConsoleWindow(QWidget): # pragma: no cover:
|
||||||
@ -62,6 +63,8 @@ class JupyterConsoleWindow(QWidget): # pragma: no cover:
|
|||||||
"btn4": self.btn4,
|
"btn4": self.btn4,
|
||||||
"btn5": self.btn5,
|
"btn5": self.btn5,
|
||||||
"btn6": self.btn6,
|
"btn6": self.btn6,
|
||||||
|
"pb": self.pb,
|
||||||
|
"pi": self.pi,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -92,6 +95,15 @@ class JupyterConsoleWindow(QWidget): # pragma: no cover:
|
|||||||
third_tab_layout.addWidget(self.lm)
|
third_tab_layout.addWidget(self.lm)
|
||||||
tab_widget.addTab(third_tab, "Layout Manager Widget")
|
tab_widget.addTab(third_tab, "Layout Manager Widget")
|
||||||
|
|
||||||
|
fourth_tab = QWidget()
|
||||||
|
fourth_tab_layout = QVBoxLayout(fourth_tab)
|
||||||
|
self.pb = PlotBase()
|
||||||
|
self.pi = self.pb.plot_item
|
||||||
|
fourth_tab_layout.addWidget(self.pb)
|
||||||
|
tab_widget.addTab(fourth_tab, "PltoBase")
|
||||||
|
|
||||||
|
tab_widget.setCurrentIndex(3)
|
||||||
|
|
||||||
group_box = QGroupBox("Jupyter Console", splitter)
|
group_box = QGroupBox("Jupyter Console", splitter)
|
||||||
group_box_layout = QVBoxLayout(group_box)
|
group_box_layout = QVBoxLayout(group_box)
|
||||||
self.console = BECJupyterConsole(inprocess=True)
|
self.console = BECJupyterConsole(inprocess=True)
|
||||||
|
@ -114,10 +114,12 @@ class RoundedFrame(BECWidget, QFrame):
|
|||||||
|
|
||||||
# Apply axis label and tick colors
|
# Apply axis label and tick colors
|
||||||
plot_item = self.content_widget.getPlotItem()
|
plot_item = self.content_widget.getPlotItem()
|
||||||
plot_item.getAxis("left").setPen(pg.mkPen(color=axis_color))
|
for axis in ["left", "right", "top", "bottom"]:
|
||||||
plot_item.getAxis("bottom").setPen(pg.mkPen(color=axis_color))
|
plot_item.getAxis(axis).setPen(pg.mkPen(color=axis_color))
|
||||||
plot_item.getAxis("left").setTextPen(pg.mkPen(color=label_color))
|
plot_item.getAxis(axis).setTextPen(pg.mkPen(color=label_color))
|
||||||
plot_item.getAxis("bottom").setTextPen(pg.mkPen(color=label_color))
|
|
||||||
|
# Change title color
|
||||||
|
plot_item.titleLabel.setText(plot_item.titleLabel.text, color=label_color)
|
||||||
|
|
||||||
# Apply border style via stylesheet
|
# Apply border style via stylesheet
|
||||||
self.content_widget.setStyleSheet(
|
self.content_widget.setStyleSheet(
|
||||||
|
@ -5,18 +5,18 @@ from qtpy.QtCore import Property, QEasingCurve, QPropertyAnimation
|
|||||||
from qtpy.QtGui import QAction
|
from qtpy.QtGui import QAction
|
||||||
from qtpy.QtWidgets import (
|
from qtpy.QtWidgets import (
|
||||||
QApplication,
|
QApplication,
|
||||||
|
QFrame,
|
||||||
QHBoxLayout,
|
QHBoxLayout,
|
||||||
QLabel,
|
QLabel,
|
||||||
QMainWindow,
|
QMainWindow,
|
||||||
|
QScrollArea,
|
||||||
QSizePolicy,
|
QSizePolicy,
|
||||||
QSpacerItem,
|
|
||||||
QStackedWidget,
|
QStackedWidget,
|
||||||
QVBoxLayout,
|
QVBoxLayout,
|
||||||
QWidget,
|
QWidget,
|
||||||
)
|
)
|
||||||
|
|
||||||
from bec_widgets.qt_utils.toolbar import MaterialIconAction, ModularToolBar
|
from bec_widgets.qt_utils.toolbar import MaterialIconAction, ModularToolBar
|
||||||
from bec_widgets.widgets.plots.waveform.waveform_widget import BECWaveformWidget
|
|
||||||
|
|
||||||
|
|
||||||
class SidePanel(QWidget):
|
class SidePanel(QWidget):
|
||||||
@ -41,7 +41,6 @@ class SidePanel(QWidget):
|
|||||||
self._panel_max_width = panel_max_width
|
self._panel_max_width = panel_max_width
|
||||||
self._animation_duration = animation_duration
|
self._animation_duration = animation_duration
|
||||||
self._animations_enabled = animations_enabled
|
self._animations_enabled = animations_enabled
|
||||||
self._orientation = orientation
|
|
||||||
|
|
||||||
self._panel_width = 0
|
self._panel_width = 0
|
||||||
self._panel_height = 0
|
self._panel_height = 0
|
||||||
@ -71,6 +70,7 @@ class SidePanel(QWidget):
|
|||||||
self.stack_widget = QStackedWidget()
|
self.stack_widget = QStackedWidget()
|
||||||
self.stack_widget.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Expanding)
|
self.stack_widget.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Expanding)
|
||||||
self.stack_widget.setMinimumWidth(5)
|
self.stack_widget.setMinimumWidth(5)
|
||||||
|
self.stack_widget.setMaximumWidth(self._panel_max_width)
|
||||||
|
|
||||||
if self._orientation == "left":
|
if self._orientation == "left":
|
||||||
self.main_layout.addWidget(self.toolbar)
|
self.main_layout.addWidget(self.toolbar)
|
||||||
@ -80,7 +80,10 @@ class SidePanel(QWidget):
|
|||||||
self.main_layout.addWidget(self.toolbar)
|
self.main_layout.addWidget(self.toolbar)
|
||||||
|
|
||||||
self.container.layout.addWidget(self.stack_widget)
|
self.container.layout.addWidget(self.stack_widget)
|
||||||
self.stack_widget.setMaximumWidth(self._panel_max_width)
|
|
||||||
|
self.menu_anim = QPropertyAnimation(self, b"panel_width")
|
||||||
|
self.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Expanding)
|
||||||
|
self.panel_width = 0 # start hidden
|
||||||
|
|
||||||
else:
|
else:
|
||||||
self.main_layout = QVBoxLayout(self)
|
self.main_layout = QVBoxLayout(self)
|
||||||
@ -97,6 +100,7 @@ class SidePanel(QWidget):
|
|||||||
self.stack_widget = QStackedWidget()
|
self.stack_widget = QStackedWidget()
|
||||||
self.stack_widget.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
|
self.stack_widget.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
|
||||||
self.stack_widget.setMinimumHeight(5)
|
self.stack_widget.setMinimumHeight(5)
|
||||||
|
self.stack_widget.setMaximumHeight(self._panel_max_width)
|
||||||
|
|
||||||
if self._orientation == "top":
|
if self._orientation == "top":
|
||||||
self.main_layout.addWidget(self.toolbar)
|
self.main_layout.addWidget(self.toolbar)
|
||||||
@ -106,74 +110,46 @@ class SidePanel(QWidget):
|
|||||||
self.main_layout.addWidget(self.toolbar)
|
self.main_layout.addWidget(self.toolbar)
|
||||||
|
|
||||||
self.container.layout.addWidget(self.stack_widget)
|
self.container.layout.addWidget(self.stack_widget)
|
||||||
self.stack_widget.setMaximumHeight(self._panel_max_width)
|
|
||||||
|
|
||||||
if self._orientation in ("left", "right"):
|
|
||||||
self.menu_anim = QPropertyAnimation(self, b"panel_width")
|
|
||||||
else:
|
|
||||||
self.menu_anim = QPropertyAnimation(self, b"panel_height")
|
self.menu_anim = QPropertyAnimation(self, b"panel_height")
|
||||||
|
self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
|
||||||
|
self.panel_height = 0 # start hidden
|
||||||
|
|
||||||
self.menu_anim.setDuration(self._animation_duration)
|
self.menu_anim.setDuration(self._animation_duration)
|
||||||
self.menu_anim.setEasingCurve(QEasingCurve.InOutQuad)
|
self.menu_anim.setEasingCurve(QEasingCurve.InOutQuad)
|
||||||
|
|
||||||
if self._orientation in ("left", "right"):
|
|
||||||
self.panel_width = 0
|
|
||||||
else:
|
|
||||||
self.panel_height = 0
|
|
||||||
|
|
||||||
@Property(int)
|
@Property(int)
|
||||||
def panel_width(self):
|
def panel_width(self):
|
||||||
"""
|
"""Get the panel width."""
|
||||||
Get the panel width.
|
|
||||||
"""
|
|
||||||
return self._panel_width
|
return self._panel_width
|
||||||
|
|
||||||
@panel_width.setter
|
@panel_width.setter
|
||||||
def panel_width(self, width: int):
|
def panel_width(self, width: int):
|
||||||
"""
|
"""Set the panel width."""
|
||||||
Set the panel width.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
width(int): The width of the panel.
|
|
||||||
"""
|
|
||||||
self._panel_width = width
|
self._panel_width = width
|
||||||
if self._orientation in ("left", "right"):
|
if self._orientation in ("left", "right"):
|
||||||
self.stack_widget.setFixedWidth(width)
|
self.stack_widget.setFixedWidth(width)
|
||||||
|
|
||||||
@Property(int)
|
@Property(int)
|
||||||
def panel_height(self):
|
def panel_height(self):
|
||||||
"""
|
"""Get the panel height."""
|
||||||
Get the panel height.
|
|
||||||
"""
|
|
||||||
return self._panel_height
|
return self._panel_height
|
||||||
|
|
||||||
@panel_height.setter
|
@panel_height.setter
|
||||||
def panel_height(self, height: int):
|
def panel_height(self, height: int):
|
||||||
"""
|
"""Set the panel height."""
|
||||||
Set the panel height.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
height(int): The height of the panel.
|
|
||||||
"""
|
|
||||||
self._panel_height = height
|
self._panel_height = height
|
||||||
if self._orientation in ("top", "bottom"):
|
if self._orientation in ("top", "bottom"):
|
||||||
self.stack_widget.setFixedHeight(height)
|
self.stack_widget.setFixedHeight(height)
|
||||||
|
|
||||||
@Property(int)
|
@Property(int)
|
||||||
def panel_max_width(self):
|
def panel_max_width(self):
|
||||||
"""
|
"""Get the maximum width of the panel."""
|
||||||
Get the maximum width of the panel.
|
|
||||||
"""
|
|
||||||
return self._panel_max_width
|
return self._panel_max_width
|
||||||
|
|
||||||
@panel_max_width.setter
|
@panel_max_width.setter
|
||||||
def panel_max_width(self, size: int):
|
def panel_max_width(self, size: int):
|
||||||
"""
|
"""Set the maximum width of the panel."""
|
||||||
Set the maximum width of the panel.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
size(int): The maximum width of the panel.
|
|
||||||
"""
|
|
||||||
self._panel_max_width = size
|
self._panel_max_width = size
|
||||||
if self._orientation in ("left", "right"):
|
if self._orientation in ("left", "right"):
|
||||||
self.stack_widget.setMaximumWidth(self._panel_max_width)
|
self.stack_widget.setMaximumWidth(self._panel_max_width)
|
||||||
@ -182,45 +158,28 @@ class SidePanel(QWidget):
|
|||||||
|
|
||||||
@Property(int)
|
@Property(int)
|
||||||
def animation_duration(self):
|
def animation_duration(self):
|
||||||
"""
|
"""Get the duration of the animation."""
|
||||||
Get the duration of the animation.
|
|
||||||
"""
|
|
||||||
return self._animation_duration
|
return self._animation_duration
|
||||||
|
|
||||||
@animation_duration.setter
|
@animation_duration.setter
|
||||||
def animation_duration(self, duration: int):
|
def animation_duration(self, duration: int):
|
||||||
"""
|
"""Set the duration of the animation."""
|
||||||
Set the duration of the animation.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
duration(int): The duration of the animation.
|
|
||||||
"""
|
|
||||||
self._animation_duration = duration
|
self._animation_duration = duration
|
||||||
self.menu_anim.setDuration(duration)
|
self.menu_anim.setDuration(duration)
|
||||||
|
|
||||||
@Property(bool)
|
@Property(bool)
|
||||||
def animations_enabled(self):
|
def animations_enabled(self):
|
||||||
"""
|
"""Get the status of the animations."""
|
||||||
Get the status of the animations.
|
|
||||||
"""
|
|
||||||
return self._animations_enabled
|
return self._animations_enabled
|
||||||
|
|
||||||
@animations_enabled.setter
|
@animations_enabled.setter
|
||||||
def animations_enabled(self, enabled: bool):
|
def animations_enabled(self, enabled: bool):
|
||||||
"""
|
"""Set the status of the animations."""
|
||||||
Set the status of the animations.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
enabled(bool): The status of the animations.
|
|
||||||
"""
|
|
||||||
self._animations_enabled = enabled
|
self._animations_enabled = enabled
|
||||||
|
|
||||||
def show_panel(self, idx: int):
|
def show_panel(self, idx: int):
|
||||||
"""
|
"""
|
||||||
Show the side panel with animation and switch to idx.
|
Show the side panel with animation and switch to idx.
|
||||||
|
|
||||||
Args:
|
|
||||||
idx(int): The index of the panel to show.
|
|
||||||
"""
|
"""
|
||||||
self.stack_widget.setCurrentIndex(idx)
|
self.stack_widget.setCurrentIndex(idx)
|
||||||
self.panel_visible = True
|
self.panel_visible = True
|
||||||
@ -268,9 +227,6 @@ class SidePanel(QWidget):
|
|||||||
def switch_to(self, idx: int):
|
def switch_to(self, idx: int):
|
||||||
"""
|
"""
|
||||||
Switch to the specified index without animation.
|
Switch to the specified index without animation.
|
||||||
|
|
||||||
Args:
|
|
||||||
idx(int): The index of the panel to switch to.
|
|
||||||
"""
|
"""
|
||||||
if self.current_index != idx:
|
if self.current_index != idx:
|
||||||
self.stack_widget.setCurrentIndex(idx)
|
self.stack_widget.setCurrentIndex(idx)
|
||||||
@ -287,20 +243,35 @@ class SidePanel(QWidget):
|
|||||||
widget(QWidget): The widget to add to the panel.
|
widget(QWidget): The widget to add to the panel.
|
||||||
title(str): The title of the panel.
|
title(str): The title of the panel.
|
||||||
"""
|
"""
|
||||||
|
# container_widget: top-level container for the stacked page
|
||||||
container_widget = QWidget()
|
container_widget = QWidget()
|
||||||
container_layout = QVBoxLayout(container_widget)
|
container_layout = QVBoxLayout(container_widget)
|
||||||
title_label = QLabel(f"<b>{title}</b>")
|
container_layout.setContentsMargins(0, 0, 0, 0)
|
||||||
title_label.setStyleSheet("font-size: 16px;")
|
|
||||||
spacer = QSpacerItem(0, 0, QSizePolicy.Minimum, QSizePolicy.Expanding)
|
|
||||||
container_layout.addWidget(title_label)
|
|
||||||
container_layout.addWidget(widget)
|
|
||||||
container_layout.addItem(spacer)
|
|
||||||
container_layout.setContentsMargins(5, 5, 5, 5)
|
|
||||||
container_layout.setSpacing(5)
|
container_layout.setSpacing(5)
|
||||||
|
|
||||||
|
title_label = QLabel(f"<b>{title}</b>")
|
||||||
|
title_label.setStyleSheet("font-size: 16px;")
|
||||||
|
container_layout.addWidget(title_label)
|
||||||
|
|
||||||
|
# Create a QScrollArea for the actual widget to ensure scrolling if the widget inside is too large
|
||||||
|
scroll_area = QScrollArea()
|
||||||
|
scroll_area.setFrameShape(QFrame.NoFrame)
|
||||||
|
scroll_area.setWidgetResizable(True)
|
||||||
|
# Let the scroll area expand in both directions if there's room
|
||||||
|
scroll_area.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
|
||||||
|
scroll_area.setWidget(widget)
|
||||||
|
|
||||||
|
# Put the scroll area in the container layout
|
||||||
|
container_layout.addWidget(scroll_area)
|
||||||
|
|
||||||
|
# Optionally stretch the scroll area to fill vertical space
|
||||||
|
container_layout.setStretchFactor(scroll_area, 1)
|
||||||
|
|
||||||
|
# Add container_widget to the stacked widget
|
||||||
index = self.stack_widget.count()
|
index = self.stack_widget.count()
|
||||||
self.stack_widget.addWidget(container_widget)
|
self.stack_widget.addWidget(container_widget)
|
||||||
|
|
||||||
|
# Add an action to the toolbar
|
||||||
action = MaterialIconAction(icon_name=icon_name, tooltip=tooltip, checkable=True)
|
action = MaterialIconAction(icon_name=icon_name, tooltip=tooltip, checkable=True)
|
||||||
self.toolbar.add_action(action_id, action, target_widget=self)
|
self.toolbar.add_action(action_id, action, target_widget=self)
|
||||||
|
|
||||||
@ -328,6 +299,11 @@ class SidePanel(QWidget):
|
|||||||
action.action.toggled.connect(on_action_toggled)
|
action.action.toggled.connect(on_action_toggled)
|
||||||
|
|
||||||
|
|
||||||
|
############################################
|
||||||
|
# DEMO APPLICATION
|
||||||
|
############################################
|
||||||
|
|
||||||
|
|
||||||
class ExampleApp(QMainWindow): # pragma: no cover
|
class ExampleApp(QMainWindow): # pragma: no cover
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
@ -335,20 +311,24 @@ class ExampleApp(QMainWindow): # pragma: no cover
|
|||||||
|
|
||||||
central_widget = QWidget()
|
central_widget = QWidget()
|
||||||
self.setCentralWidget(central_widget)
|
self.setCentralWidget(central_widget)
|
||||||
|
|
||||||
self.side_panel = SidePanel(self, orientation="left")
|
|
||||||
|
|
||||||
self.layout = QHBoxLayout(central_widget)
|
self.layout = QHBoxLayout(central_widget)
|
||||||
|
|
||||||
|
# Create side panel
|
||||||
|
self.side_panel = SidePanel(self, orientation="left", panel_max_width=250)
|
||||||
self.layout.addWidget(self.side_panel)
|
self.layout.addWidget(self.side_panel)
|
||||||
|
|
||||||
|
from bec_widgets.widgets.plots.waveform.waveform_widget import BECWaveformWidget
|
||||||
|
|
||||||
self.plot = BECWaveformWidget()
|
self.plot = BECWaveformWidget()
|
||||||
self.layout.addWidget(self.plot)
|
self.layout.addWidget(self.plot)
|
||||||
|
|
||||||
self.add_side_menus()
|
self.add_side_menus()
|
||||||
|
|
||||||
def add_side_menus(self):
|
def add_side_menus(self):
|
||||||
widget1 = QWidget()
|
widget1 = QWidget()
|
||||||
widget1_layout = QVBoxLayout(widget1)
|
layout1 = QVBoxLayout(widget1)
|
||||||
widget1_layout.addWidget(QLabel("This is Widget 1"))
|
for i in range(15):
|
||||||
|
layout1.addWidget(QLabel(f"Widget 1 label row {i}"))
|
||||||
self.side_panel.add_menu(
|
self.side_panel.add_menu(
|
||||||
action_id="widget1",
|
action_id="widget1",
|
||||||
icon_name="counter_1",
|
icon_name="counter_1",
|
||||||
@ -358,8 +338,8 @@ class ExampleApp(QMainWindow): # pragma: no cover
|
|||||||
)
|
)
|
||||||
|
|
||||||
widget2 = QWidget()
|
widget2 = QWidget()
|
||||||
widget2_layout = QVBoxLayout(widget2)
|
layout2 = QVBoxLayout(widget2)
|
||||||
widget2_layout.addWidget(QLabel("This is Widget 2"))
|
layout2.addWidget(QLabel("Short widget 2 content"))
|
||||||
self.side_panel.add_menu(
|
self.side_panel.add_menu(
|
||||||
action_id="widget2",
|
action_id="widget2",
|
||||||
icon_name="counter_2",
|
icon_name="counter_2",
|
||||||
@ -369,8 +349,9 @@ class ExampleApp(QMainWindow): # pragma: no cover
|
|||||||
)
|
)
|
||||||
|
|
||||||
widget3 = QWidget()
|
widget3 = QWidget()
|
||||||
widget3_layout = QVBoxLayout(widget3)
|
layout3 = QVBoxLayout(widget3)
|
||||||
widget3_layout.addWidget(QLabel("This is Widget 3"))
|
for i in range(10):
|
||||||
|
layout3.addWidget(QLabel(f"Line {i} for Widget 3"))
|
||||||
self.side_panel.add_menu(
|
self.side_panel.add_menu(
|
||||||
action_id="widget3",
|
action_id="widget3",
|
||||||
icon_name="counter_3",
|
icon_name="counter_3",
|
||||||
@ -383,6 +364,6 @@ class ExampleApp(QMainWindow): # pragma: no cover
|
|||||||
if __name__ == "__main__": # pragma: no cover
|
if __name__ == "__main__": # pragma: no cover
|
||||||
app = QApplication(sys.argv)
|
app = QApplication(sys.argv)
|
||||||
window = ExampleApp()
|
window = ExampleApp()
|
||||||
window.resize(800, 600)
|
window.resize(1000, 700)
|
||||||
window.show()
|
window.show()
|
||||||
sys.exit(app.exec())
|
sys.exit(app.exec())
|
||||||
|
571
bec_widgets/widgets/plots_next_gen/plot_base.py
Normal file
571
bec_widgets/widgets/plots_next_gen/plot_base.py
Normal file
@ -0,0 +1,571 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import pyqtgraph as pg
|
||||||
|
from bec_lib import bec_logger
|
||||||
|
from qtpy.QtCore import QPoint, QPointF, Qt, Signal
|
||||||
|
from qtpy.QtWidgets import QLabel, QVBoxLayout, QWidget
|
||||||
|
|
||||||
|
from bec_widgets.qt_utils.error_popups import SafeProperty, SafeSlot
|
||||||
|
from bec_widgets.qt_utils.round_frame import RoundedFrame
|
||||||
|
from bec_widgets.qt_utils.side_panel import SidePanel
|
||||||
|
from bec_widgets.qt_utils.toolbar import MaterialIconAction, ModularToolBar, SeparatorAction
|
||||||
|
from bec_widgets.utils import ConnectionConfig, Crosshair, EntryValidator
|
||||||
|
from bec_widgets.utils.bec_widget import BECWidget
|
||||||
|
from bec_widgets.utils.colors import set_theme
|
||||||
|
from bec_widgets.utils.fps_counter import FPSCounter
|
||||||
|
from bec_widgets.utils.widget_state_manager import WidgetStateManager
|
||||||
|
from bec_widgets.widgets.containers.layout_manager.layout_manager import LayoutManagerWidget
|
||||||
|
from bec_widgets.widgets.plots_next_gen.setting_menus.axis_settings import AxisSettings
|
||||||
|
from bec_widgets.widgets.plots_next_gen.toolbar_bundles.mouse_interactions import (
|
||||||
|
MouseInteractionToolbarBundle,
|
||||||
|
)
|
||||||
|
from bec_widgets.widgets.plots_next_gen.toolbar_bundles.plot_export import PlotExportBundle
|
||||||
|
from bec_widgets.widgets.plots_next_gen.toolbar_bundles.save_state import SaveStateBundle
|
||||||
|
from bec_widgets.widgets.utility.visual.dark_mode_button.dark_mode_button import DarkModeButton
|
||||||
|
|
||||||
|
logger = bec_logger.logger
|
||||||
|
|
||||||
|
|
||||||
|
class BECViewBox(pg.ViewBox):
|
||||||
|
sigPaint = Signal()
|
||||||
|
|
||||||
|
def paint(self, painter, opt, widget):
|
||||||
|
super().paint(painter, opt, widget)
|
||||||
|
self.sigPaint.emit()
|
||||||
|
|
||||||
|
def itemBoundsChanged(self, item):
|
||||||
|
self._itemBoundsCache.pop(item, None)
|
||||||
|
if (self.state["autoRange"][0] is not False) or (self.state["autoRange"][1] is not False):
|
||||||
|
# check if the call is coming from a mouse-move event
|
||||||
|
if hasattr(item, "skip_auto_range") and item.skip_auto_range:
|
||||||
|
return
|
||||||
|
self._autoRangeNeedsUpdate = True
|
||||||
|
self.update()
|
||||||
|
|
||||||
|
|
||||||
|
class PlotBase(BECWidget, QWidget):
|
||||||
|
PLUGIN = False
|
||||||
|
RPC = False
|
||||||
|
|
||||||
|
# Custom Signals
|
||||||
|
property_changed = Signal(str, object)
|
||||||
|
crosshair_position_changed = Signal(tuple)
|
||||||
|
crosshair_position_clicked = Signal(tuple)
|
||||||
|
crosshair_coordinates_changed = Signal(tuple)
|
||||||
|
crosshair_coordinates_clicked = Signal(tuple)
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
parent: QWidget | None = None,
|
||||||
|
config: ConnectionConfig | None = None,
|
||||||
|
client=None,
|
||||||
|
gui_id: str | None = None,
|
||||||
|
) -> None:
|
||||||
|
if config is None:
|
||||||
|
config = ConnectionConfig(widget_class=self.__class__.__name__)
|
||||||
|
super().__init__(client=client, gui_id=gui_id, config=config)
|
||||||
|
QWidget.__init__(self, parent=parent)
|
||||||
|
|
||||||
|
# For PropertyManager identification
|
||||||
|
self.setObjectName("PlotBase")
|
||||||
|
self.get_bec_shortcuts()
|
||||||
|
|
||||||
|
# Layout Management
|
||||||
|
self.layout = QVBoxLayout(self)
|
||||||
|
self.layout.setContentsMargins(0, 0, 0, 0)
|
||||||
|
self.layout.setSpacing(0)
|
||||||
|
self.layout_manager = LayoutManagerWidget(parent=self)
|
||||||
|
|
||||||
|
# Property Manager
|
||||||
|
self.state_manager = WidgetStateManager(self)
|
||||||
|
|
||||||
|
# Entry Validator
|
||||||
|
self.entry_validator = EntryValidator(self.dev)
|
||||||
|
|
||||||
|
# Base widgets elements
|
||||||
|
self.plot_item = pg.PlotItem(viewBox=BECViewBox(enableMenu=True))
|
||||||
|
self.plot_widget = pg.PlotWidget(plotItem=self.plot_item)
|
||||||
|
self.side_panel = SidePanel(self, orientation="left", panel_max_width=280)
|
||||||
|
self.toolbar = ModularToolBar(target_widget=self, orientation="horizontal")
|
||||||
|
self.init_toolbar()
|
||||||
|
|
||||||
|
# PlotItem Addons
|
||||||
|
self.plot_item.addLegend()
|
||||||
|
self.crosshair = None
|
||||||
|
self.fps_monitor = None
|
||||||
|
self.fps_label = QLabel(alignment=Qt.AlignmentFlag.AlignRight)
|
||||||
|
|
||||||
|
self._init_ui()
|
||||||
|
|
||||||
|
def _init_ui(self):
|
||||||
|
self.layout.addWidget(self.layout_manager)
|
||||||
|
self.round_plot_widget = RoundedFrame(content_widget=self.plot_widget, theme_update=True)
|
||||||
|
self.round_plot_widget.apply_theme("dark")
|
||||||
|
|
||||||
|
self.layout_manager.add_widget(self.round_plot_widget)
|
||||||
|
self.layout_manager.add_widget_relative(self.fps_label, self.round_plot_widget, "top")
|
||||||
|
self.fps_label.hide()
|
||||||
|
self.layout_manager.add_widget_relative(self.side_panel, self.round_plot_widget, "left")
|
||||||
|
self.layout_manager.add_widget_relative(self.toolbar, self.fps_label, "top")
|
||||||
|
|
||||||
|
self.add_side_menus()
|
||||||
|
|
||||||
|
# PlotItem ViewBox Signals
|
||||||
|
self.plot_item.vb.sigStateChanged.connect(self.viewbox_state_changed)
|
||||||
|
|
||||||
|
def init_toolbar(self):
|
||||||
|
|
||||||
|
self.plot_export_bundle = PlotExportBundle("plot_export", target_widget=self)
|
||||||
|
self.mouse_bundle = MouseInteractionToolbarBundle("mouse_interaction", target_widget=self)
|
||||||
|
self.state_export_bundle = SaveStateBundle("state_export", target_widget=self)
|
||||||
|
|
||||||
|
# Add elements to toolbar
|
||||||
|
self.toolbar.add_bundle(self.plot_export_bundle, target_widget=self)
|
||||||
|
self.toolbar.add_bundle(self.state_export_bundle, target_widget=self)
|
||||||
|
self.toolbar.add_bundle(self.mouse_bundle, target_widget=self)
|
||||||
|
|
||||||
|
self.toolbar.add_action("separator_0", SeparatorAction(), target_widget=self)
|
||||||
|
self.toolbar.add_action(
|
||||||
|
"crosshair",
|
||||||
|
MaterialIconAction(icon_name="point_scan", tooltip="Show Crosshair", checkable=True),
|
||||||
|
target_widget=self,
|
||||||
|
)
|
||||||
|
self.toolbar.add_action("separator_1", SeparatorAction(), target_widget=self)
|
||||||
|
self.toolbar.add_action(
|
||||||
|
"fps_monitor",
|
||||||
|
MaterialIconAction(icon_name="speed", tooltip="Show FPS Monitor", checkable=True),
|
||||||
|
target_widget=self,
|
||||||
|
)
|
||||||
|
self.toolbar.addWidget(DarkModeButton(toolbar=True))
|
||||||
|
|
||||||
|
self.toolbar.widgets["fps_monitor"].action.toggled.connect(
|
||||||
|
lambda checked: setattr(self, "enable_fps_monitor", checked)
|
||||||
|
)
|
||||||
|
self.toolbar.widgets["crosshair"].action.toggled.connect(self.toggle_crosshair)
|
||||||
|
|
||||||
|
def add_side_menus(self):
|
||||||
|
"""Adds multiple menus to the side panel."""
|
||||||
|
# Setting Axis Widget
|
||||||
|
axis_setting = AxisSettings(target_widget=self)
|
||||||
|
self.side_panel.add_menu(
|
||||||
|
action_id="axis",
|
||||||
|
icon_name="settings",
|
||||||
|
tooltip="Show Axis Settings",
|
||||||
|
widget=axis_setting,
|
||||||
|
title="Axis Settings",
|
||||||
|
)
|
||||||
|
|
||||||
|
################################################################################
|
||||||
|
# Toggle UI Elements
|
||||||
|
################################################################################
|
||||||
|
|
||||||
|
@SafeProperty(bool, doc="Show Toolbar")
|
||||||
|
def enable_toolbar(self) -> bool:
|
||||||
|
return self.toolbar.isVisible()
|
||||||
|
|
||||||
|
@enable_toolbar.setter
|
||||||
|
def enable_toolbar(self, value: bool):
|
||||||
|
self.toolbar.setVisible(value)
|
||||||
|
|
||||||
|
@SafeProperty(bool, doc="Show Side Panel")
|
||||||
|
def enable_side_panel(self) -> bool:
|
||||||
|
return self.side_panel.isVisible()
|
||||||
|
|
||||||
|
@enable_side_panel.setter
|
||||||
|
def enable_side_panel(self, value: bool):
|
||||||
|
self.side_panel.setVisible(value)
|
||||||
|
|
||||||
|
@SafeProperty(bool, doc="Enable the FPS monitor.")
|
||||||
|
def enable_fps_monitor(self) -> bool:
|
||||||
|
return self.fps_label.isVisible()
|
||||||
|
|
||||||
|
@enable_fps_monitor.setter
|
||||||
|
def enable_fps_monitor(self, value: bool):
|
||||||
|
if value and self.fps_monitor is None:
|
||||||
|
self.hook_fps_monitor()
|
||||||
|
elif not value and self.fps_monitor is not None:
|
||||||
|
self.unhook_fps_monitor()
|
||||||
|
|
||||||
|
################################################################################
|
||||||
|
# ViewBox State Signals
|
||||||
|
################################################################################
|
||||||
|
|
||||||
|
def viewbox_state_changed(self):
|
||||||
|
"""
|
||||||
|
Emit a signal when the state of the viewbox has changed.
|
||||||
|
Merges the default pyqtgraphs signal states and also CTRL menu toggles.
|
||||||
|
"""
|
||||||
|
|
||||||
|
viewbox_state = self.plot_item.vb.getState()
|
||||||
|
# Range Limits
|
||||||
|
x_min, x_max = viewbox_state["targetRange"][0]
|
||||||
|
y_min, y_max = viewbox_state["targetRange"][1]
|
||||||
|
self.property_changed.emit("x_min", x_min)
|
||||||
|
self.property_changed.emit("x_max", x_max)
|
||||||
|
self.property_changed.emit("y_min", y_min)
|
||||||
|
self.property_changed.emit("y_max", y_max)
|
||||||
|
|
||||||
|
# Grid Toggles
|
||||||
|
|
||||||
|
################################################################################
|
||||||
|
# Plot Properties
|
||||||
|
################################################################################
|
||||||
|
|
||||||
|
def set(self, **kwargs):
|
||||||
|
"""
|
||||||
|
Set the properties of the plot widget.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
**kwargs: Keyword arguments for the properties to be set.
|
||||||
|
|
||||||
|
Possible properties:
|
||||||
|
|
||||||
|
"""
|
||||||
|
property_map = {
|
||||||
|
"title": self.title,
|
||||||
|
"x_label": self.x_label,
|
||||||
|
"y_label": self.y_label,
|
||||||
|
"x_limits": self.x_limits,
|
||||||
|
"y_limits": self.y_limits,
|
||||||
|
"x_grid": self.x_grid,
|
||||||
|
"y_grid": self.y_grid,
|
||||||
|
"inner_axes": self.inner_axes,
|
||||||
|
"outer_axes": self.outer_axes,
|
||||||
|
"lock_aspect_ratio": self.lock_aspect_ratio,
|
||||||
|
"auto_range_x": self.auto_range_x,
|
||||||
|
"auto_range_y": self.auto_range_y,
|
||||||
|
"x_log": self.x_log,
|
||||||
|
"y_log": self.y_log,
|
||||||
|
"legend_label_size": self.legend_label_size,
|
||||||
|
}
|
||||||
|
|
||||||
|
for key, value in kwargs.items():
|
||||||
|
if key in property_map:
|
||||||
|
setattr(self, key, value)
|
||||||
|
else:
|
||||||
|
logger.warning(f"Property {key} not found.")
|
||||||
|
|
||||||
|
@SafeProperty(str, doc="The title of the axes.")
|
||||||
|
def title(self) -> str:
|
||||||
|
return self.plot_item.titleLabel.text
|
||||||
|
|
||||||
|
@title.setter
|
||||||
|
def title(self, value: str):
|
||||||
|
self.plot_item.setTitle(value)
|
||||||
|
self.property_changed.emit("title", value)
|
||||||
|
|
||||||
|
@SafeProperty(str, doc="The text of the x label")
|
||||||
|
def x_label(self) -> str:
|
||||||
|
return self.plot_item.getAxis("bottom").labelText
|
||||||
|
|
||||||
|
@x_label.setter
|
||||||
|
def x_label(self, value: str):
|
||||||
|
self.plot_item.setLabel("bottom", text=value)
|
||||||
|
self.property_changed.emit("x_label", value)
|
||||||
|
|
||||||
|
@SafeProperty(str, doc="The text of the y label")
|
||||||
|
def y_label(self) -> str:
|
||||||
|
return self.plot_item.getAxis("left").labelText
|
||||||
|
|
||||||
|
@y_label.setter
|
||||||
|
def y_label(self, value: str):
|
||||||
|
self.plot_item.setLabel("left", text=value)
|
||||||
|
self.property_changed.emit("y_label", value)
|
||||||
|
|
||||||
|
def _tuple_to_qpointf(self, tuple: tuple | list):
|
||||||
|
"""
|
||||||
|
Helper function to convert a tuple to a QPointF.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
tuple(tuple|list): Tuple or list of two numbers.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
QPointF: The tuple converted to a QPointF.
|
||||||
|
"""
|
||||||
|
if len(tuple) != 2:
|
||||||
|
raise ValueError("Limits must be a tuple or list of two numbers.")
|
||||||
|
min_val, max_val = tuple
|
||||||
|
if not isinstance(min_val, (int, float)) or not isinstance(max_val, (int, float)):
|
||||||
|
raise TypeError("Limits must be numbers.")
|
||||||
|
if min_val > max_val:
|
||||||
|
raise ValueError("Minimum limit cannot be greater than maximum limit.")
|
||||||
|
return QPoint(*tuple)
|
||||||
|
|
||||||
|
################################################################################
|
||||||
|
# X limits, has to be SaveProperty("QPointF") because of the tuple conversion for designer,
|
||||||
|
# the python properties are used for CLI and API for context dialog settings.
|
||||||
|
|
||||||
|
@SafeProperty("QPointF")
|
||||||
|
def x_limits(self) -> QPointF:
|
||||||
|
current_lim = self.plot_item.vb.viewRange()[0]
|
||||||
|
return QPointF(current_lim[0], current_lim[1])
|
||||||
|
|
||||||
|
@x_limits.setter
|
||||||
|
def x_limits(self, value):
|
||||||
|
if isinstance(value, (tuple, list)):
|
||||||
|
value = self._tuple_to_qpointf(value)
|
||||||
|
self.plot_item.vb.setXRange(value.x(), value.y(), padding=0)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def x_lim(self) -> tuple:
|
||||||
|
return (self.x_limits.x(), self.x_limits.y())
|
||||||
|
|
||||||
|
@x_lim.setter
|
||||||
|
def x_lim(self, value):
|
||||||
|
self.x_limits = value
|
||||||
|
|
||||||
|
@property
|
||||||
|
def x_min(self) -> float:
|
||||||
|
return self.x_limits.x()
|
||||||
|
|
||||||
|
@x_min.setter
|
||||||
|
def x_min(self, value: float):
|
||||||
|
self.x_limits = (value, self.x_lim[1])
|
||||||
|
|
||||||
|
@property
|
||||||
|
def x_max(self) -> float:
|
||||||
|
return self.x_limits.y()
|
||||||
|
|
||||||
|
@x_max.setter
|
||||||
|
def x_max(self, value: float):
|
||||||
|
self.x_limits = (self.x_lim[0], value)
|
||||||
|
|
||||||
|
################################################################################
|
||||||
|
# Y limits, has to be SaveProperty("QPointF") because of the tuple conversion for designer,
|
||||||
|
# the python properties are used for CLI and API for context dialog settings.
|
||||||
|
|
||||||
|
@SafeProperty("QPointF")
|
||||||
|
def y_limits(self) -> QPointF:
|
||||||
|
current_lim = self.plot_item.vb.viewRange()[1]
|
||||||
|
return QPointF(current_lim[0], current_lim[1])
|
||||||
|
|
||||||
|
@y_limits.setter
|
||||||
|
def y_limits(self, value):
|
||||||
|
if isinstance(value, (tuple, list)):
|
||||||
|
value = self._tuple_to_qpointf(value)
|
||||||
|
self.plot_item.vb.setYRange(value.x(), value.y(), padding=0)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def y_lim(self) -> tuple:
|
||||||
|
return (self.y_limits.x(), self.y_limits.y())
|
||||||
|
|
||||||
|
@y_lim.setter
|
||||||
|
def y_lim(self, value):
|
||||||
|
self.y_limits = value
|
||||||
|
|
||||||
|
@property
|
||||||
|
def y_min(self) -> float:
|
||||||
|
return self.y_limits.x()
|
||||||
|
|
||||||
|
@y_min.setter
|
||||||
|
def y_min(self, value: float):
|
||||||
|
self.y_limits = (value, self.y_lim[1])
|
||||||
|
|
||||||
|
@property
|
||||||
|
def y_max(self) -> float:
|
||||||
|
return self.y_limits.y()
|
||||||
|
|
||||||
|
@y_max.setter
|
||||||
|
def y_max(self, value: float):
|
||||||
|
self.y_limits = (self.y_lim[0], value)
|
||||||
|
|
||||||
|
@SafeProperty(bool, doc="Show grid on the x-axis.")
|
||||||
|
def x_grid(self) -> bool:
|
||||||
|
return self.plot_item.ctrl.xGridCheck.isChecked()
|
||||||
|
|
||||||
|
@x_grid.setter
|
||||||
|
def x_grid(self, value: bool):
|
||||||
|
self.plot_item.showGrid(x=value)
|
||||||
|
self.property_changed.emit("x_grid", value)
|
||||||
|
|
||||||
|
@SafeProperty(bool, doc="Show grid on the y-axis.")
|
||||||
|
def y_grid(self) -> bool:
|
||||||
|
return self.plot_item.ctrl.yGridCheck.isChecked()
|
||||||
|
|
||||||
|
@y_grid.setter
|
||||||
|
def y_grid(self, value: bool):
|
||||||
|
self.plot_item.showGrid(y=value)
|
||||||
|
self.property_changed.emit("y_grid", value)
|
||||||
|
|
||||||
|
@SafeProperty(bool, doc="Set X-axis to log scale if True, linear if False.")
|
||||||
|
def x_log(self) -> bool:
|
||||||
|
return bool(self.plot_item.vb.state.get("logMode", [False, False])[0])
|
||||||
|
|
||||||
|
@x_log.setter
|
||||||
|
def x_log(self, value: bool):
|
||||||
|
self.plot_item.setLogMode(x=value)
|
||||||
|
self.property_changed.emit("x_log", value)
|
||||||
|
|
||||||
|
@SafeProperty(bool, doc="Set Y-axis to log scale if True, linear if False.")
|
||||||
|
def y_log(self) -> bool:
|
||||||
|
return bool(self.plot_item.vb.state.get("logMode", [False, False])[1])
|
||||||
|
|
||||||
|
@y_log.setter
|
||||||
|
def y_log(self, value: bool):
|
||||||
|
self.plot_item.setLogMode(y=value)
|
||||||
|
self.property_changed.emit("y_log", value)
|
||||||
|
|
||||||
|
@SafeProperty(bool, doc="Show the outer axes of the plot widget.")
|
||||||
|
def outer_axes(self) -> bool:
|
||||||
|
return self.plot_item.getAxis("top").isVisible()
|
||||||
|
|
||||||
|
@outer_axes.setter
|
||||||
|
def outer_axes(self, value: bool):
|
||||||
|
self.plot_item.showAxis("top", value)
|
||||||
|
self.plot_item.showAxis("right", value)
|
||||||
|
self.property_changed.emit("outer_axes", value)
|
||||||
|
|
||||||
|
@SafeProperty(bool, doc="Show inner axes of the plot widget.")
|
||||||
|
def inner_axes(self) -> bool:
|
||||||
|
return self.plot_item.getAxis("bottom").isVisible()
|
||||||
|
|
||||||
|
@inner_axes.setter
|
||||||
|
def inner_axes(self, value: bool):
|
||||||
|
self.plot_item.showAxis("bottom", value)
|
||||||
|
self.plot_item.showAxis("left", value)
|
||||||
|
self.property_changed.emit("inner_axes", value)
|
||||||
|
|
||||||
|
@SafeProperty(bool, doc="Lock aspect ratio of the plot widget.")
|
||||||
|
def lock_aspect_ratio(self) -> bool:
|
||||||
|
return bool(self.plot_item.vb.getState()["aspectLocked"])
|
||||||
|
|
||||||
|
@lock_aspect_ratio.setter
|
||||||
|
def lock_aspect_ratio(self, value: bool):
|
||||||
|
self.plot_item.setAspectLocked(value)
|
||||||
|
|
||||||
|
@SafeProperty(bool, doc="Set auto range for the x-axis.")
|
||||||
|
def auto_range_x(self) -> bool:
|
||||||
|
return bool(self.plot_item.vb.getState()["autoRange"][0])
|
||||||
|
|
||||||
|
@auto_range_x.setter
|
||||||
|
def auto_range_x(self, value: bool):
|
||||||
|
self.plot_item.enableAutoRange(x=value)
|
||||||
|
|
||||||
|
@SafeProperty(bool, doc="Set auto range for the y-axis.")
|
||||||
|
def auto_range_y(self) -> bool:
|
||||||
|
return bool(self.plot_item.vb.getState()["autoRange"][1])
|
||||||
|
|
||||||
|
@auto_range_y.setter
|
||||||
|
def auto_range_y(self, value: bool):
|
||||||
|
self.plot_item.enableAutoRange(y=value)
|
||||||
|
|
||||||
|
@SafeProperty(int, doc="The font size of the legend font.")
|
||||||
|
def legend_label_size(self) -> int:
|
||||||
|
if not self.plot_item.legend:
|
||||||
|
return
|
||||||
|
scale = self.plot_item.legend.scale() * 9
|
||||||
|
return scale
|
||||||
|
|
||||||
|
@legend_label_size.setter
|
||||||
|
def legend_label_size(self, value: int):
|
||||||
|
if not self.plot_item.legend:
|
||||||
|
return
|
||||||
|
scale = (
|
||||||
|
value / 9
|
||||||
|
) # 9 is the default font size of the legend, so we always scale it against 9
|
||||||
|
self.plot_item.legend.setScale(scale)
|
||||||
|
|
||||||
|
################################################################################
|
||||||
|
# FPS Counter
|
||||||
|
################################################################################
|
||||||
|
|
||||||
|
def update_fps_label(self, fps: float) -> None:
|
||||||
|
"""
|
||||||
|
Update the FPS label.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
fps(float): The frames per second.
|
||||||
|
"""
|
||||||
|
if self.fps_label:
|
||||||
|
self.fps_label.setText(f"FPS: {fps:.2f}")
|
||||||
|
|
||||||
|
def hook_fps_monitor(self):
|
||||||
|
"""Hook the FPS monitor to the plot."""
|
||||||
|
if self.fps_monitor is None:
|
||||||
|
self.fps_monitor = FPSCounter(self.plot_item.vb)
|
||||||
|
self.fps_label.show()
|
||||||
|
|
||||||
|
self.fps_monitor.sigFpsUpdate.connect(self.update_fps_label)
|
||||||
|
self.update_fps_label(0)
|
||||||
|
|
||||||
|
def unhook_fps_monitor(self, delete_label=True):
|
||||||
|
"""Unhook the FPS monitor from the plot."""
|
||||||
|
if self.fps_monitor is not None and delete_label:
|
||||||
|
# Remove Monitor
|
||||||
|
self.fps_monitor.cleanup()
|
||||||
|
self.fps_monitor.deleteLater()
|
||||||
|
self.fps_monitor = None
|
||||||
|
if self.fps_label is not None:
|
||||||
|
# Hide Label
|
||||||
|
self.fps_label.hide()
|
||||||
|
|
||||||
|
################################################################################
|
||||||
|
# Crosshair
|
||||||
|
################################################################################
|
||||||
|
|
||||||
|
def hook_crosshair(self) -> None:
|
||||||
|
"""Hook the crosshair to all plots."""
|
||||||
|
if self.crosshair is None:
|
||||||
|
self.crosshair = Crosshair(self.plot_item, precision=3)
|
||||||
|
self.crosshair.crosshairChanged.connect(self.crosshair_position_changed)
|
||||||
|
self.crosshair.crosshairClicked.connect(self.crosshair_position_clicked)
|
||||||
|
self.crosshair.coordinatesChanged1D.connect(self.crosshair_coordinates_changed)
|
||||||
|
self.crosshair.coordinatesClicked1D.connect(self.crosshair_coordinates_clicked)
|
||||||
|
self.crosshair.coordinatesChanged2D.connect(self.crosshair_coordinates_changed)
|
||||||
|
self.crosshair.coordinatesClicked2D.connect(self.crosshair_coordinates_clicked)
|
||||||
|
|
||||||
|
def unhook_crosshair(self) -> None:
|
||||||
|
"""Unhook the crosshair from all plots."""
|
||||||
|
if self.crosshair is not None:
|
||||||
|
self.crosshair.crosshairChanged.disconnect(self.crosshair_position_changed)
|
||||||
|
self.crosshair.crosshairClicked.disconnect(self.crosshair_position_clicked)
|
||||||
|
self.crosshair.coordinatesChanged1D.disconnect(self.crosshair_coordinates_changed)
|
||||||
|
self.crosshair.coordinatesClicked1D.disconnect(self.crosshair_coordinates_clicked)
|
||||||
|
self.crosshair.coordinatesChanged2D.disconnect(self.crosshair_coordinates_changed)
|
||||||
|
self.crosshair.coordinatesClicked2D.disconnect(self.crosshair_coordinates_clicked)
|
||||||
|
self.crosshair.cleanup()
|
||||||
|
self.crosshair.deleteLater()
|
||||||
|
self.crosshair = None
|
||||||
|
|
||||||
|
def toggle_crosshair(self) -> None:
|
||||||
|
"""Toggle the crosshair on all plots."""
|
||||||
|
if self.crosshair is None:
|
||||||
|
return self.hook_crosshair()
|
||||||
|
|
||||||
|
self.unhook_crosshair()
|
||||||
|
|
||||||
|
@SafeSlot()
|
||||||
|
def reset(self) -> None:
|
||||||
|
"""Reset the plot widget."""
|
||||||
|
if self.crosshair is not None:
|
||||||
|
self.crosshair.clear_markers()
|
||||||
|
self.crosshair.update_markers()
|
||||||
|
|
||||||
|
def cleanup(self):
|
||||||
|
self.unhook_crosshair()
|
||||||
|
self.unhook_fps_monitor(delete_label=True)
|
||||||
|
self.cleanup_pyqtgraph()
|
||||||
|
|
||||||
|
def cleanup_pyqtgraph(self):
|
||||||
|
"""Cleanup pyqtgraph items."""
|
||||||
|
item = self.plot_item
|
||||||
|
item.vb.menu.close()
|
||||||
|
item.vb.menu.deleteLater()
|
||||||
|
item.ctrlMenu.close()
|
||||||
|
item.ctrlMenu.deleteLater()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__": # pragma: no cover:
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from qtpy.QtWidgets import QApplication
|
||||||
|
|
||||||
|
app = QApplication(sys.argv)
|
||||||
|
set_theme("dark")
|
||||||
|
widget = PlotBase()
|
||||||
|
widget.show()
|
||||||
|
# Just some example data and parameters to test
|
||||||
|
widget.y_grid = True
|
||||||
|
widget.plot_item.plot([1, 2, 3, 4, 5], [1, 2, 3, 4, 5])
|
||||||
|
|
||||||
|
sys.exit(app.exec_())
|
@ -0,0 +1,95 @@
|
|||||||
|
import os
|
||||||
|
|
||||||
|
from qtpy.QtWidgets import QFrame, QScrollArea, QVBoxLayout, QWidget
|
||||||
|
|
||||||
|
from bec_widgets.qt_utils.error_popups import SafeSlot
|
||||||
|
from bec_widgets.qt_utils.settings_dialog import SettingWidget
|
||||||
|
from bec_widgets.utils import UILoader
|
||||||
|
from bec_widgets.utils.widget_io import WidgetIO
|
||||||
|
|
||||||
|
|
||||||
|
class AxisSettings(SettingWidget):
|
||||||
|
def __init__(self, parent=None, target_widget=None, *args, **kwargs):
|
||||||
|
super().__init__(parent=parent, *args, **kwargs)
|
||||||
|
|
||||||
|
# This is a settings widget that depends on the target widget
|
||||||
|
# and should mirror what is in the target widget.
|
||||||
|
# Saving settings for this widget could result in recursively setting the target widget.
|
||||||
|
self.setProperty("skip_settings", True)
|
||||||
|
self.setObjectName("AxisSettings")
|
||||||
|
current_path = os.path.dirname(__file__)
|
||||||
|
form = UILoader().load_ui(os.path.join(current_path, "axis_settings_vertical.ui"), self)
|
||||||
|
|
||||||
|
self.target_widget = target_widget
|
||||||
|
|
||||||
|
# # Scroll area
|
||||||
|
self.scroll_area = QScrollArea(self)
|
||||||
|
self.scroll_area.setWidgetResizable(True)
|
||||||
|
self.scroll_area.setFrameShape(QFrame.NoFrame)
|
||||||
|
self.scroll_area.setWidget(form)
|
||||||
|
|
||||||
|
self.layout = QVBoxLayout(self)
|
||||||
|
self.layout.setContentsMargins(0, 0, 0, 0)
|
||||||
|
self.layout.addWidget(self.scroll_area)
|
||||||
|
# self.layout.addWidget(self.ui)
|
||||||
|
self.ui = form
|
||||||
|
|
||||||
|
self.connect_all_signals()
|
||||||
|
if self.target_widget is not None:
|
||||||
|
self.target_widget.property_changed.connect(self.update_property)
|
||||||
|
|
||||||
|
def connect_all_signals(self):
|
||||||
|
for widget in [
|
||||||
|
self.ui.title,
|
||||||
|
self.ui.inner_axes,
|
||||||
|
self.ui.outer_axes,
|
||||||
|
self.ui.x_label,
|
||||||
|
self.ui.x_min,
|
||||||
|
self.ui.x_max,
|
||||||
|
self.ui.x_log,
|
||||||
|
self.ui.x_grid,
|
||||||
|
self.ui.y_label,
|
||||||
|
self.ui.y_min,
|
||||||
|
self.ui.y_max,
|
||||||
|
self.ui.y_log,
|
||||||
|
self.ui.y_grid,
|
||||||
|
]:
|
||||||
|
WidgetIO.connect_widget_change_signal(widget, self.set_property)
|
||||||
|
|
||||||
|
@SafeSlot()
|
||||||
|
def set_property(self, widget: QWidget, value):
|
||||||
|
"""
|
||||||
|
Set property of the target widget based on the widget that emitted the signal.
|
||||||
|
The name of the property has to be the same as the objectName of the widget
|
||||||
|
and compatible with WidgetIO.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
widget(QWidget): The widget that emitted the signal.
|
||||||
|
value(): The value to set the property to.
|
||||||
|
"""
|
||||||
|
|
||||||
|
try: # to avoid crashing when the widget is not found in Designer
|
||||||
|
property_name = widget.objectName()
|
||||||
|
setattr(self.target_widget, property_name, value)
|
||||||
|
except RuntimeError:
|
||||||
|
return
|
||||||
|
|
||||||
|
@SafeSlot()
|
||||||
|
def update_property(self, property_name: str, value):
|
||||||
|
"""
|
||||||
|
Update the value of the widget based on the property name and value.
|
||||||
|
The name of the property has to be the same as the objectName of the widget
|
||||||
|
and compatible with WidgetIO.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
property_name(str): The name of the property to update.
|
||||||
|
value: The value to set the property to.
|
||||||
|
"""
|
||||||
|
try: # to avoid crashing when the widget is not found in Designer
|
||||||
|
widget_to_set = self.ui.findChild(QWidget, property_name)
|
||||||
|
except RuntimeError:
|
||||||
|
return
|
||||||
|
# Block signals to avoid triggering set_property again
|
||||||
|
was_blocked = widget_to_set.blockSignals(True)
|
||||||
|
WidgetIO.set_value(widget_to_set, value)
|
||||||
|
widget_to_set.blockSignals(was_blocked)
|
@ -0,0 +1,256 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<ui version="4.0">
|
||||||
|
<class>Form</class>
|
||||||
|
<widget class="QWidget" name="Form">
|
||||||
|
<property name="geometry">
|
||||||
|
<rect>
|
||||||
|
<x>0</x>
|
||||||
|
<y>0</y>
|
||||||
|
<width>427</width>
|
||||||
|
<height>270</height>
|
||||||
|
</rect>
|
||||||
|
</property>
|
||||||
|
<property name="minimumSize">
|
||||||
|
<size>
|
||||||
|
<width>0</width>
|
||||||
|
<height>250</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
|
<property name="maximumSize">
|
||||||
|
<size>
|
||||||
|
<width>16777215</width>
|
||||||
|
<height>278</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
|
<property name="windowTitle">
|
||||||
|
<string>Form</string>
|
||||||
|
</property>
|
||||||
|
<layout class="QGridLayout" name="gridLayout">
|
||||||
|
<item row="0" column="0" colspan="2">
|
||||||
|
<layout class="QHBoxLayout" name="horizontalLayout">
|
||||||
|
<item>
|
||||||
|
<widget class="QLabel" name="plot_title_label">
|
||||||
|
<property name="text">
|
||||||
|
<string>Plot Title</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QLineEdit" name="plot_title"/>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</item>
|
||||||
|
<item row="1" column="0">
|
||||||
|
<widget class="QLabel" name="label_outer_axes">
|
||||||
|
<property name="text">
|
||||||
|
<string>Outer Axes</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="2" column="1">
|
||||||
|
<widget class="QGroupBox" name="y_axis_box">
|
||||||
|
<property name="title">
|
||||||
|
<string>Y Axis</string>
|
||||||
|
</property>
|
||||||
|
<layout class="QGridLayout" name="gridLayout_5">
|
||||||
|
<item row="3" column="2">
|
||||||
|
<widget class="QComboBox" name="y_scale">
|
||||||
|
<item>
|
||||||
|
<property name="text">
|
||||||
|
<string>linear</string>
|
||||||
|
</property>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<property name="text">
|
||||||
|
<string>log</string>
|
||||||
|
</property>
|
||||||
|
</item>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="2" column="2">
|
||||||
|
<widget class="QDoubleSpinBox" name="y_max">
|
||||||
|
<property name="alignment">
|
||||||
|
<set>Qt::AlignmentFlag::AlignCenter</set>
|
||||||
|
</property>
|
||||||
|
<property name="minimum">
|
||||||
|
<double>-9999.000000000000000</double>
|
||||||
|
</property>
|
||||||
|
<property name="maximum">
|
||||||
|
<double>9999.000000000000000</double>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="1" column="0" colspan="2">
|
||||||
|
<widget class="QLabel" name="y_min_label">
|
||||||
|
<property name="text">
|
||||||
|
<string>Min</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="1" column="2">
|
||||||
|
<widget class="QDoubleSpinBox" name="y_min">
|
||||||
|
<property name="alignment">
|
||||||
|
<set>Qt::AlignmentFlag::AlignCenter</set>
|
||||||
|
</property>
|
||||||
|
<property name="minimum">
|
||||||
|
<double>-9999.000000000000000</double>
|
||||||
|
</property>
|
||||||
|
<property name="maximum">
|
||||||
|
<double>9999.000000000000000</double>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="0" column="2">
|
||||||
|
<widget class="QLineEdit" name="y_label"/>
|
||||||
|
</item>
|
||||||
|
<item row="3" column="0">
|
||||||
|
<widget class="QLabel" name="y_scale_label">
|
||||||
|
<property name="text">
|
||||||
|
<string>Scale</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="0" column="0">
|
||||||
|
<widget class="QLabel" name="y_label_label">
|
||||||
|
<property name="text">
|
||||||
|
<string>Label</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="2" column="0">
|
||||||
|
<widget class="QLabel" name="y_max_label">
|
||||||
|
<property name="text">
|
||||||
|
<string>Max</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="4" column="2">
|
||||||
|
<widget class="QCheckBox" name="y_grid">
|
||||||
|
<property name="text">
|
||||||
|
<string/>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="4" column="0">
|
||||||
|
<widget class="QLabel" name="y_grid_label">
|
||||||
|
<property name="text">
|
||||||
|
<string>Grid</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="2" column="0">
|
||||||
|
<widget class="QGroupBox" name="x_axis_box">
|
||||||
|
<property name="title">
|
||||||
|
<string>X Axis</string>
|
||||||
|
</property>
|
||||||
|
<layout class="QGridLayout" name="gridLayout_4">
|
||||||
|
<item row="3" column="0">
|
||||||
|
<widget class="QLabel" name="x_scale_label">
|
||||||
|
<property name="text">
|
||||||
|
<string>Scale</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="1" column="2">
|
||||||
|
<widget class="QDoubleSpinBox" name="x_min">
|
||||||
|
<property name="alignment">
|
||||||
|
<set>Qt::AlignmentFlag::AlignCenter</set>
|
||||||
|
</property>
|
||||||
|
<property name="minimum">
|
||||||
|
<double>-9999.000000000000000</double>
|
||||||
|
</property>
|
||||||
|
<property name="maximum">
|
||||||
|
<double>9999.000000000000000</double>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="1" column="0" colspan="2">
|
||||||
|
<widget class="QLabel" name="x_min_label">
|
||||||
|
<property name="text">
|
||||||
|
<string>Min</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="2" column="2">
|
||||||
|
<widget class="QDoubleSpinBox" name="x_max">
|
||||||
|
<property name="alignment">
|
||||||
|
<set>Qt::AlignmentFlag::AlignCenter</set>
|
||||||
|
</property>
|
||||||
|
<property name="minimum">
|
||||||
|
<double>-9999.000000000000000</double>
|
||||||
|
</property>
|
||||||
|
<property name="maximum">
|
||||||
|
<double>9999.000000000000000</double>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="3" column="2">
|
||||||
|
<widget class="QComboBox" name="x_scale">
|
||||||
|
<item>
|
||||||
|
<property name="text">
|
||||||
|
<string>linear</string>
|
||||||
|
</property>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<property name="text">
|
||||||
|
<string>log</string>
|
||||||
|
</property>
|
||||||
|
</item>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="2" column="0">
|
||||||
|
<widget class="QLabel" name="x_max_label">
|
||||||
|
<property name="text">
|
||||||
|
<string>Max</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="0" column="2">
|
||||||
|
<widget class="QLineEdit" name="x_label"/>
|
||||||
|
</item>
|
||||||
|
<item row="0" column="0">
|
||||||
|
<widget class="QLabel" name="x_label_label">
|
||||||
|
<property name="text">
|
||||||
|
<string>Label</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="4" column="2">
|
||||||
|
<widget class="QCheckBox" name="x_grid">
|
||||||
|
<property name="text">
|
||||||
|
<string/>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="4" column="0">
|
||||||
|
<widget class="QLabel" name="x_grid_label">
|
||||||
|
<property name="text">
|
||||||
|
<string>Grid</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="1" column="1">
|
||||||
|
<widget class="ToggleSwitch" name="switch_outer_axes">
|
||||||
|
<property name="checked" stdset="0">
|
||||||
|
<bool>false</bool>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</widget>
|
||||||
|
<customwidgets>
|
||||||
|
<customwidget>
|
||||||
|
<class>ToggleSwitch</class>
|
||||||
|
<extends>QWidget</extends>
|
||||||
|
<header>toggle_switch</header>
|
||||||
|
</customwidget>
|
||||||
|
</customwidgets>
|
||||||
|
<resources/>
|
||||||
|
<connections/>
|
||||||
|
</ui>
|
@ -0,0 +1,240 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<ui version="4.0">
|
||||||
|
<class>Form</class>
|
||||||
|
<widget class="QWidget" name="Form">
|
||||||
|
<property name="geometry">
|
||||||
|
<rect>
|
||||||
|
<x>0</x>
|
||||||
|
<y>0</y>
|
||||||
|
<width>241</width>
|
||||||
|
<height>526</height>
|
||||||
|
</rect>
|
||||||
|
</property>
|
||||||
|
<property name="windowTitle">
|
||||||
|
<string>Form</string>
|
||||||
|
</property>
|
||||||
|
<layout class="QGridLayout" name="gridLayout">
|
||||||
|
<item row="4" column="0" colspan="2">
|
||||||
|
<widget class="QGroupBox" name="x_axis_box">
|
||||||
|
<property name="title">
|
||||||
|
<string>X Axis</string>
|
||||||
|
</property>
|
||||||
|
<layout class="QGridLayout" name="gridLayout_4">
|
||||||
|
<item row="2" column="2">
|
||||||
|
<widget class="QDoubleSpinBox" name="x_max">
|
||||||
|
<property name="alignment">
|
||||||
|
<set>Qt::AlignmentFlag::AlignCenter</set>
|
||||||
|
</property>
|
||||||
|
<property name="minimum">
|
||||||
|
<double>-9999.000000000000000</double>
|
||||||
|
</property>
|
||||||
|
<property name="maximum">
|
||||||
|
<double>9999.000000000000000</double>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="3" column="0">
|
||||||
|
<widget class="QLabel" name="x_scale_label">
|
||||||
|
<property name="text">
|
||||||
|
<string>Log</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="0" column="2">
|
||||||
|
<widget class="QLineEdit" name="x_label"/>
|
||||||
|
</item>
|
||||||
|
<item row="2" column="0">
|
||||||
|
<widget class="QLabel" name="x_max_label">
|
||||||
|
<property name="text">
|
||||||
|
<string>Max</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="3" column="2">
|
||||||
|
<widget class="ToggleSwitch" name="x_log">
|
||||||
|
<property name="checked" stdset="0">
|
||||||
|
<bool>false</bool>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="5" column="0">
|
||||||
|
<widget class="QLabel" name="x_grid_label">
|
||||||
|
<property name="text">
|
||||||
|
<string>Grid</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="1" column="2">
|
||||||
|
<widget class="QDoubleSpinBox" name="x_min">
|
||||||
|
<property name="alignment">
|
||||||
|
<set>Qt::AlignmentFlag::AlignCenter</set>
|
||||||
|
</property>
|
||||||
|
<property name="minimum">
|
||||||
|
<double>-9999.000000000000000</double>
|
||||||
|
</property>
|
||||||
|
<property name="maximum">
|
||||||
|
<double>9999.000000000000000</double>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="1" column="0" colspan="2">
|
||||||
|
<widget class="QLabel" name="x_min_label">
|
||||||
|
<property name="text">
|
||||||
|
<string>Min</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="0" column="0">
|
||||||
|
<widget class="QLabel" name="x_label_label">
|
||||||
|
<property name="text">
|
||||||
|
<string>Label</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="5" column="2">
|
||||||
|
<widget class="ToggleSwitch" name="x_grid">
|
||||||
|
<property name="checked" stdset="0">
|
||||||
|
<bool>false</bool>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="0" column="0" colspan="2">
|
||||||
|
<layout class="QHBoxLayout" name="horizontalLayout">
|
||||||
|
<item>
|
||||||
|
<widget class="QLabel" name="plot_title_label">
|
||||||
|
<property name="text">
|
||||||
|
<string>Plot Title</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QLineEdit" name="title"/>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</item>
|
||||||
|
<item row="2" column="0">
|
||||||
|
<widget class="QLabel" name="label_outer_axes">
|
||||||
|
<property name="text">
|
||||||
|
<string>Outer Axes</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="5" column="0" colspan="2">
|
||||||
|
<widget class="QGroupBox" name="y_axis_box">
|
||||||
|
<property name="title">
|
||||||
|
<string>Y Axis</string>
|
||||||
|
</property>
|
||||||
|
<layout class="QGridLayout" name="gridLayout_5">
|
||||||
|
<item row="2" column="2">
|
||||||
|
<widget class="QDoubleSpinBox" name="y_max">
|
||||||
|
<property name="alignment">
|
||||||
|
<set>Qt::AlignmentFlag::AlignCenter</set>
|
||||||
|
</property>
|
||||||
|
<property name="minimum">
|
||||||
|
<double>-9999.000000000000000</double>
|
||||||
|
</property>
|
||||||
|
<property name="maximum">
|
||||||
|
<double>9999.000000000000000</double>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="1" column="0" colspan="2">
|
||||||
|
<widget class="QLabel" name="y_min_label">
|
||||||
|
<property name="text">
|
||||||
|
<string>Min</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="1" column="2">
|
||||||
|
<widget class="QDoubleSpinBox" name="y_min">
|
||||||
|
<property name="alignment">
|
||||||
|
<set>Qt::AlignmentFlag::AlignCenter</set>
|
||||||
|
</property>
|
||||||
|
<property name="minimum">
|
||||||
|
<double>-9999.000000000000000</double>
|
||||||
|
</property>
|
||||||
|
<property name="maximum">
|
||||||
|
<double>9999.000000000000000</double>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="0" column="2">
|
||||||
|
<widget class="QLineEdit" name="y_label"/>
|
||||||
|
</item>
|
||||||
|
<item row="3" column="0">
|
||||||
|
<widget class="QLabel" name="y_scale_label">
|
||||||
|
<property name="text">
|
||||||
|
<string>Log</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="0" column="0">
|
||||||
|
<widget class="QLabel" name="y_label_label">
|
||||||
|
<property name="text">
|
||||||
|
<string>Label</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="2" column="0">
|
||||||
|
<widget class="QLabel" name="y_max_label">
|
||||||
|
<property name="text">
|
||||||
|
<string>Max</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="4" column="0">
|
||||||
|
<widget class="QLabel" name="y_grid_label">
|
||||||
|
<property name="text">
|
||||||
|
<string>Grid</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="3" column="2">
|
||||||
|
<widget class="ToggleSwitch" name="y_log">
|
||||||
|
<property name="checked" stdset="0">
|
||||||
|
<bool>false</bool>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="4" column="2">
|
||||||
|
<widget class="ToggleSwitch" name="y_grid">
|
||||||
|
<property name="checked" stdset="0">
|
||||||
|
<bool>false</bool>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="2" column="1">
|
||||||
|
<widget class="ToggleSwitch" name="outer_axes">
|
||||||
|
<property name="checked" stdset="0">
|
||||||
|
<bool>false</bool>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="1" column="0">
|
||||||
|
<widget class="QLabel" name="label">
|
||||||
|
<property name="text">
|
||||||
|
<string>Inner Axes</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="1" column="1">
|
||||||
|
<widget class="ToggleSwitch" name="inner_axes"/>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</widget>
|
||||||
|
<customwidgets>
|
||||||
|
<customwidget>
|
||||||
|
<class>ToggleSwitch</class>
|
||||||
|
<extends>QWidget</extends>
|
||||||
|
<header>toggle_switch</header>
|
||||||
|
</customwidget>
|
||||||
|
</customwidgets>
|
||||||
|
<resources/>
|
||||||
|
<connections/>
|
||||||
|
</ui>
|
@ -0,0 +1,88 @@
|
|||||||
|
import pyqtgraph as pg
|
||||||
|
|
||||||
|
from bec_widgets.qt_utils.error_popups import SafeSlot
|
||||||
|
from bec_widgets.qt_utils.toolbar import MaterialIconAction, ToolbarBundle
|
||||||
|
|
||||||
|
|
||||||
|
class MouseInteractionToolbarBundle(ToolbarBundle):
|
||||||
|
"""
|
||||||
|
A bundle of actions that are hooked in this constructor itself,
|
||||||
|
so that you can immediately connect the signals and toggle states.
|
||||||
|
|
||||||
|
This bundle is for a toolbar that controls mouse interactions on a plot.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, bundle_id="mouse_interaction", target_widget=None, **kwargs):
|
||||||
|
super().__init__(bundle_id=bundle_id, actions=[], **kwargs)
|
||||||
|
self.target_widget = target_widget
|
||||||
|
|
||||||
|
# Create each MaterialIconAction with a parent
|
||||||
|
# so the signals can fire even if the toolbar isn't added yet.
|
||||||
|
drag = MaterialIconAction(
|
||||||
|
icon_name="drag_pan",
|
||||||
|
tooltip="Drag Mouse Mode",
|
||||||
|
checkable=True,
|
||||||
|
parent=self.target_widget, # or any valid parent
|
||||||
|
)
|
||||||
|
rect = MaterialIconAction(
|
||||||
|
icon_name="frame_inspect",
|
||||||
|
tooltip="Rectangle Zoom Mode",
|
||||||
|
checkable=True,
|
||||||
|
parent=self.target_widget,
|
||||||
|
)
|
||||||
|
auto = MaterialIconAction(
|
||||||
|
icon_name="open_in_full",
|
||||||
|
tooltip="Autorange Plot",
|
||||||
|
checkable=False,
|
||||||
|
parent=self.target_widget,
|
||||||
|
)
|
||||||
|
aspect_ratio = MaterialIconAction(
|
||||||
|
icon_name="aspect_ratio",
|
||||||
|
tooltip="Lock image aspect ratio",
|
||||||
|
checkable=True,
|
||||||
|
parent=self.target_widget,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Add them to the bundle
|
||||||
|
self.add_action("drag_mode", drag)
|
||||||
|
self.add_action("rectangle_mode", rect)
|
||||||
|
self.add_action("auto_range", auto)
|
||||||
|
self.add_action("aspect_ratio", aspect_ratio)
|
||||||
|
|
||||||
|
# Immediately connect signals
|
||||||
|
drag.action.toggled.connect(self.enable_mouse_pan_mode)
|
||||||
|
rect.action.toggled.connect(self.enable_mouse_rectangle_mode)
|
||||||
|
auto.action.triggered.connect(self.autorange_plot)
|
||||||
|
aspect_ratio.action.toggled.connect(self.lock_aspect_ratio)
|
||||||
|
|
||||||
|
@SafeSlot(bool)
|
||||||
|
def enable_mouse_rectangle_mode(self, checked: bool):
|
||||||
|
"""
|
||||||
|
Enable the rectangle zoom mode on the plot widget.
|
||||||
|
"""
|
||||||
|
self.actions["drag_mode"].action.setChecked(not checked)
|
||||||
|
if self.target_widget and checked:
|
||||||
|
self.target_widget.plot_item.getViewBox().setMouseMode(pg.ViewBox.RectMode)
|
||||||
|
|
||||||
|
@SafeSlot(bool)
|
||||||
|
def enable_mouse_pan_mode(self, checked: bool):
|
||||||
|
"""
|
||||||
|
Enable the pan mode on the plot widget.
|
||||||
|
"""
|
||||||
|
self.actions["rectangle_mode"].action.setChecked(not checked)
|
||||||
|
if self.target_widget and checked:
|
||||||
|
self.target_widget.plot_item.getViewBox().setMouseMode(pg.ViewBox.PanMode)
|
||||||
|
|
||||||
|
@SafeSlot()
|
||||||
|
def autorange_plot(self):
|
||||||
|
"""
|
||||||
|
Enable autorange on the plot widget.
|
||||||
|
"""
|
||||||
|
if self.target_widget:
|
||||||
|
self.target_widget.auto_range_x = True
|
||||||
|
self.target_widget.auto_range_y = True
|
||||||
|
|
||||||
|
@SafeSlot(bool)
|
||||||
|
def lock_aspect_ratio(self, checked: bool):
|
||||||
|
if self.target_widget:
|
||||||
|
self.target_widget.lock_aspect_ratio = checked
|
@ -0,0 +1,63 @@
|
|||||||
|
from pyqtgraph.exporters import MatplotlibExporter
|
||||||
|
|
||||||
|
from bec_widgets.qt_utils.error_popups import SafeSlot, WarningPopupUtility
|
||||||
|
from bec_widgets.qt_utils.toolbar import MaterialIconAction, ToolbarBundle
|
||||||
|
|
||||||
|
|
||||||
|
class PlotExportBundle(ToolbarBundle):
|
||||||
|
"""
|
||||||
|
A bundle of actions that are hooked in this constructor itself,
|
||||||
|
so that you can immediately connect the signals and toggle states.
|
||||||
|
|
||||||
|
This bundle is for a toolbar that controls exporting a plot.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, bundle_id="mouse_interaction", target_widget=None, **kwargs):
|
||||||
|
super().__init__(bundle_id=bundle_id, actions=[], **kwargs)
|
||||||
|
self.target_widget = target_widget
|
||||||
|
|
||||||
|
# Create each MaterialIconAction with a parent
|
||||||
|
# so the signals can fire even if the toolbar isn't added yet.
|
||||||
|
save = MaterialIconAction(
|
||||||
|
icon_name="save", tooltip="Open Export Dialog", parent=self.target_widget
|
||||||
|
)
|
||||||
|
matplotlib = MaterialIconAction(
|
||||||
|
icon_name="photo_library", tooltip="Open Matplotlib Dialog", parent=self.target_widget
|
||||||
|
)
|
||||||
|
|
||||||
|
# Add them to the bundle
|
||||||
|
self.add_action("save", save)
|
||||||
|
self.add_action("matplotlib", matplotlib)
|
||||||
|
|
||||||
|
# Immediately connect signals
|
||||||
|
save.action.triggered.connect(self.export_dialog)
|
||||||
|
matplotlib.action.triggered.connect(self.matplotlib_dialog)
|
||||||
|
|
||||||
|
@SafeSlot()
|
||||||
|
def export_dialog(self):
|
||||||
|
"""
|
||||||
|
Open the export dialog for the plot widget.
|
||||||
|
"""
|
||||||
|
if self.target_widget:
|
||||||
|
scene = self.target_widget.plot_item.scene()
|
||||||
|
scene.contextMenuItem = self.target_widget.plot_item
|
||||||
|
scene.showExportDialog()
|
||||||
|
|
||||||
|
@SafeSlot()
|
||||||
|
def matplotlib_dialog(self):
|
||||||
|
"""
|
||||||
|
Export the plot widget to Matplotlib.
|
||||||
|
"""
|
||||||
|
if self.target_widget:
|
||||||
|
try:
|
||||||
|
import matplotlib as mpl
|
||||||
|
|
||||||
|
MatplotlibExporter(self.target_widget.plot_item).export()
|
||||||
|
except:
|
||||||
|
warning_util = WarningPopupUtility()
|
||||||
|
warning_util.show_warning(
|
||||||
|
title="Matplotlib not installed",
|
||||||
|
message="Matplotlib is required for this feature.",
|
||||||
|
detailed_text="Please install matplotlib in your Python environment by using 'pip install matplotlib'.",
|
||||||
|
)
|
||||||
|
return
|
@ -0,0 +1,48 @@
|
|||||||
|
from bec_widgets.qt_utils.error_popups import SafeSlot
|
||||||
|
from bec_widgets.qt_utils.toolbar import MaterialIconAction, ToolbarBundle
|
||||||
|
|
||||||
|
|
||||||
|
class SaveStateBundle(ToolbarBundle):
|
||||||
|
"""
|
||||||
|
A bundle of actions that are hooked in this constructor itself,
|
||||||
|
so that you can immediately connect the signals and toggle states.
|
||||||
|
|
||||||
|
This bundle is for a toolbar that controls saving the state of the widget.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, bundle_id="mouse_interaction", target_widget=None, **kwargs):
|
||||||
|
super().__init__(bundle_id=bundle_id, actions=[], **kwargs)
|
||||||
|
self.target_widget = target_widget
|
||||||
|
|
||||||
|
# Create each MaterialIconAction with a parent
|
||||||
|
# so the signals can fire even if the toolbar isn't added yet.
|
||||||
|
save_state = MaterialIconAction(
|
||||||
|
icon_name="download", tooltip="Save Widget State", parent=self.target_widget
|
||||||
|
)
|
||||||
|
load_state = MaterialIconAction(
|
||||||
|
icon_name="upload", tooltip="Load Widget State", parent=self.target_widget
|
||||||
|
)
|
||||||
|
|
||||||
|
# Add them to the bundle
|
||||||
|
self.add_action("save", save_state)
|
||||||
|
self.add_action("matplotlib", load_state)
|
||||||
|
|
||||||
|
# Immediately connect signals
|
||||||
|
save_state.action.triggered.connect(self.save_state_dialog)
|
||||||
|
load_state.action.triggered.connect(self.load_state_dialog)
|
||||||
|
|
||||||
|
@SafeSlot()
|
||||||
|
def save_state_dialog(self):
|
||||||
|
"""
|
||||||
|
Open the export dialog to save a state of the widget.
|
||||||
|
"""
|
||||||
|
if self.target_widget:
|
||||||
|
self.target_widget.state_manager.save_state()
|
||||||
|
|
||||||
|
@SafeSlot()
|
||||||
|
def load_state_dialog(self):
|
||||||
|
"""
|
||||||
|
Load a saved state of the widget.
|
||||||
|
"""
|
||||||
|
if self.target_widget:
|
||||||
|
self.target_widget.state_manager.load_state()
|
105
tests/unit_tests/test_axis_settings.py
Normal file
105
tests/unit_tests/test_axis_settings.py
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
import pytest
|
||||||
|
from qtpy.QtWidgets import QDoubleSpinBox, QLineEdit
|
||||||
|
|
||||||
|
from bec_widgets.widgets.plots_next_gen.plot_base import PlotBase
|
||||||
|
from bec_widgets.widgets.plots_next_gen.setting_menus.axis_settings import AxisSettings
|
||||||
|
from tests.unit_tests.client_mocks import mocked_client
|
||||||
|
from tests.unit_tests.conftest import create_widget
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def axis_settings_fixture(qtbot, mocked_client):
|
||||||
|
"""
|
||||||
|
Creates an AxisSettings widget, targeting the real PlotBase widget.
|
||||||
|
"""
|
||||||
|
|
||||||
|
plot_base = create_widget(qtbot, PlotBase, client=mocked_client)
|
||||||
|
axis_settings = create_widget(qtbot, AxisSettings, parent=None, target_widget=plot_base)
|
||||||
|
return axis_settings, plot_base
|
||||||
|
|
||||||
|
|
||||||
|
def test_axis_settings_init(axis_settings_fixture):
|
||||||
|
"""
|
||||||
|
Ensure AxisSettings constructs properly with a real PlotBase target.
|
||||||
|
"""
|
||||||
|
axis_settings, plot_base = axis_settings_fixture
|
||||||
|
# Verify the UI was loaded and placed in a scroll area
|
||||||
|
assert axis_settings.ui is not None
|
||||||
|
assert axis_settings.scroll_area is not None
|
||||||
|
assert axis_settings.layout.count() == 1 # scroll area
|
||||||
|
# Check the target
|
||||||
|
assert axis_settings.target_widget == plot_base
|
||||||
|
|
||||||
|
|
||||||
|
def test_change_ui_updates_plot_base(axis_settings_fixture, qtbot):
|
||||||
|
"""
|
||||||
|
When user edits AxisSettings UI fields, verify that PlotBase's properties update.
|
||||||
|
"""
|
||||||
|
axis_settings, plot_base = axis_settings_fixture
|
||||||
|
|
||||||
|
# 1) Set the 'title'
|
||||||
|
title_edit = axis_settings.ui.title
|
||||||
|
assert isinstance(title_edit, QLineEdit)
|
||||||
|
with qtbot.waitSignal(plot_base.property_changed, timeout=500) as signal:
|
||||||
|
title_edit.setText("New Plot Title")
|
||||||
|
|
||||||
|
assert signal.args == ["title", "New Plot Title"]
|
||||||
|
assert plot_base.title == "New Plot Title"
|
||||||
|
|
||||||
|
# 2) Set x_min spinbox
|
||||||
|
x_max_spin = axis_settings.ui.x_max
|
||||||
|
assert isinstance(x_max_spin, QDoubleSpinBox)
|
||||||
|
with qtbot.waitSignal(plot_base.property_changed, timeout=500) as signal2:
|
||||||
|
x_max_spin.setValue(123)
|
||||||
|
assert plot_base.x_max == 123
|
||||||
|
|
||||||
|
# # 3) Toggle grid
|
||||||
|
x_log_toggle = axis_settings.ui.x_log
|
||||||
|
x_log_toggle.checked = True
|
||||||
|
with qtbot.waitSignal(plot_base.property_changed, timeout=500) as signal3:
|
||||||
|
x_log_toggle.checked = True
|
||||||
|
|
||||||
|
assert plot_base.x_log is True
|
||||||
|
|
||||||
|
|
||||||
|
def test_plot_base_updates_ui(axis_settings_fixture, qtbot):
|
||||||
|
"""
|
||||||
|
When PlotBase properties change (on the Python side), AxisSettings UI should update.
|
||||||
|
We do this by simulating that PlotBase sets properties and emits property_changed.
|
||||||
|
(In real usage, PlotBase calls .property_changed.emit(...) in its setters.)
|
||||||
|
"""
|
||||||
|
axis_settings, plot_base = axis_settings_fixture
|
||||||
|
|
||||||
|
# 1) Set plot_base.title
|
||||||
|
plot_base.title = "Plot Title from Code"
|
||||||
|
assert axis_settings.ui.title.text() == "Plot Title from Code"
|
||||||
|
|
||||||
|
# 2) Set x_max
|
||||||
|
plot_base.x_max = 100
|
||||||
|
qtbot.wait(50)
|
||||||
|
assert axis_settings.ui.x_max.value() == 100
|
||||||
|
|
||||||
|
# 3) Set x_log
|
||||||
|
plot_base.x_log = True
|
||||||
|
qtbot.wait(50)
|
||||||
|
assert axis_settings.ui.x_log.checked is True
|
||||||
|
|
||||||
|
|
||||||
|
def test_no_crash_no_target(qtbot):
|
||||||
|
"""
|
||||||
|
AxisSettings can be created with target_widget=None. It won't update anything,
|
||||||
|
but it shouldn't crash on UI changes.
|
||||||
|
"""
|
||||||
|
axis_settings = create_widget(qtbot, AxisSettings, parent=None, target_widget=None)
|
||||||
|
|
||||||
|
axis_settings.ui.title.setText("No target")
|
||||||
|
assert axis_settings.ui.title.text() == "No target"
|
||||||
|
|
||||||
|
|
||||||
|
def test_scroll_area_behavior(axis_settings_fixture, qtbot):
|
||||||
|
"""
|
||||||
|
Optional: Check that the QScrollArea is set up in a resizable manner.
|
||||||
|
"""
|
||||||
|
axis_settings, plot_base = axis_settings_fixture
|
||||||
|
scroll_area = axis_settings.scroll_area
|
||||||
|
assert scroll_area.widgetResizable() is True
|
249
tests/unit_tests/test_plot_base_next_gen.py
Normal file
249
tests/unit_tests/test_plot_base_next_gen.py
Normal file
@ -0,0 +1,249 @@
|
|||||||
|
from bec_widgets.widgets.plots_next_gen.plot_base import PlotBase
|
||||||
|
|
||||||
|
from .client_mocks import mocked_client
|
||||||
|
from .conftest import create_widget
|
||||||
|
|
||||||
|
|
||||||
|
def test_init_plot_base(qtbot, mocked_client):
|
||||||
|
"""
|
||||||
|
Test that PlotBase initializes without error and has expected default states.
|
||||||
|
"""
|
||||||
|
pb = create_widget(qtbot, PlotBase, client=mocked_client)
|
||||||
|
assert pb.objectName() == "PlotBase"
|
||||||
|
# The default title/labels should be empty
|
||||||
|
assert pb.title == ""
|
||||||
|
assert pb.x_label == ""
|
||||||
|
assert pb.y_label == ""
|
||||||
|
# By default, no crosshair or FPS monitor
|
||||||
|
assert pb.crosshair is None
|
||||||
|
assert pb.fps_monitor is None
|
||||||
|
# The side panel was created
|
||||||
|
assert pb.side_panel is not None
|
||||||
|
# The toolbar was created
|
||||||
|
assert pb.toolbar is not None
|
||||||
|
|
||||||
|
|
||||||
|
def test_set_title_emits_signal(qtbot, mocked_client):
|
||||||
|
"""
|
||||||
|
Test that setting the title updates the plot and emits a property_changed signal.
|
||||||
|
"""
|
||||||
|
pb = create_widget(qtbot, PlotBase, client=mocked_client)
|
||||||
|
|
||||||
|
with qtbot.waitSignal(pb.property_changed, timeout=500) as signal:
|
||||||
|
pb.title = "My Plot Title"
|
||||||
|
# The signal should carry ("title", "My Plot Title")
|
||||||
|
assert signal.args == ["title", "My Plot Title"]
|
||||||
|
assert pb.plot_item.titleLabel.text == "My Plot Title"
|
||||||
|
|
||||||
|
# Get the property back from the object
|
||||||
|
assert pb.title == "My Plot Title"
|
||||||
|
|
||||||
|
|
||||||
|
def test_set_x_label_emits_signal(qtbot, mocked_client):
|
||||||
|
"""
|
||||||
|
Test setting x_label updates the plot and emits a property_changed signal.
|
||||||
|
"""
|
||||||
|
pb = create_widget(qtbot, PlotBase, client=mocked_client)
|
||||||
|
with qtbot.waitSignal(pb.property_changed, timeout=500) as signal:
|
||||||
|
pb.x_label = "Voltage (V)"
|
||||||
|
assert signal.args == ["x_label", "Voltage (V)"]
|
||||||
|
assert pb.x_label == "Voltage (V)"
|
||||||
|
assert pb.plot_item.getAxis("bottom").labelText == "Voltage (V)"
|
||||||
|
|
||||||
|
|
||||||
|
def test_set_y_label_emits_signal(qtbot, mocked_client):
|
||||||
|
"""
|
||||||
|
Test setting y_label updates the plot and emits a property_changed signal.
|
||||||
|
"""
|
||||||
|
pb = create_widget(qtbot, PlotBase, client=mocked_client)
|
||||||
|
with qtbot.waitSignal(pb.property_changed, timeout=500) as signal:
|
||||||
|
pb.y_label = "Current (A)"
|
||||||
|
assert signal.args == ["y_label", "Current (A)"]
|
||||||
|
assert pb.y_label == "Current (A)"
|
||||||
|
assert pb.plot_item.getAxis("left").labelText == "Current (A)"
|
||||||
|
|
||||||
|
|
||||||
|
def test_set_x_min_max(qtbot, mocked_client):
|
||||||
|
"""
|
||||||
|
Test setting x_min, x_max changes the actual X-range of the plot
|
||||||
|
and emits signals.
|
||||||
|
"""
|
||||||
|
pb = create_widget(qtbot, PlotBase, client=mocked_client)
|
||||||
|
# Set x_max
|
||||||
|
with qtbot.waitSignal(pb.property_changed, timeout=500) as sig_max:
|
||||||
|
pb.x_max = 50
|
||||||
|
assert pb.x_max == 50.0
|
||||||
|
|
||||||
|
# Set x_min
|
||||||
|
with qtbot.waitSignal(pb.property_changed, timeout=500) as sig_min:
|
||||||
|
pb.x_min = 5
|
||||||
|
assert pb.x_min == 5.0
|
||||||
|
|
||||||
|
# Confirm the actual ViewBox range in pyqtgraph
|
||||||
|
assert pb.plot_item.vb.viewRange()[0] == [5.0, 50.0]
|
||||||
|
|
||||||
|
|
||||||
|
def test_set_y_min_max(qtbot, mocked_client):
|
||||||
|
"""
|
||||||
|
Test setting y_min, y_max changes the actual Y-range of the plot
|
||||||
|
"""
|
||||||
|
pb = create_widget(qtbot, PlotBase, client=mocked_client)
|
||||||
|
|
||||||
|
with qtbot.waitSignal(pb.property_changed, timeout=500) as sig_max:
|
||||||
|
pb.y_max = 100
|
||||||
|
assert pb.y_max == 100.0
|
||||||
|
|
||||||
|
with qtbot.waitSignal(pb.property_changed, timeout=500) as sig_min:
|
||||||
|
pb.y_min = 10
|
||||||
|
assert pb.y_min == 10.0
|
||||||
|
|
||||||
|
# Confirm the actual ViewBox range
|
||||||
|
assert pb.plot_item.vb.viewRange()[1] == [10.0, 100.0]
|
||||||
|
|
||||||
|
|
||||||
|
def test_auto_range_x_y(qtbot, mocked_client):
|
||||||
|
"""
|
||||||
|
Test enabling and disabling autoRange for x and y axes.
|
||||||
|
"""
|
||||||
|
pb = create_widget(qtbot, PlotBase, client=mocked_client)
|
||||||
|
# auto_range_x = True
|
||||||
|
pb.auto_range_x = True
|
||||||
|
assert pb.plot_item.vb.state["autoRange"][0] is True
|
||||||
|
pb.auto_range_y = True
|
||||||
|
assert pb.plot_item.vb.state["autoRange"][1] is True
|
||||||
|
# Turn off
|
||||||
|
pb.auto_range_x = False
|
||||||
|
assert pb.plot_item.vb.state["autoRange"][0] is False
|
||||||
|
pb.auto_range_y = False
|
||||||
|
assert pb.plot_item.vb.state["autoRange"][1] is False
|
||||||
|
|
||||||
|
|
||||||
|
def test_x_log_y_log(qtbot, mocked_client):
|
||||||
|
"""
|
||||||
|
Test toggling log scale on x and y axes.
|
||||||
|
"""
|
||||||
|
pb = create_widget(qtbot, PlotBase, client=mocked_client)
|
||||||
|
|
||||||
|
with qtbot.waitSignal(pb.property_changed, timeout=500) as sig1:
|
||||||
|
pb.x_log = True
|
||||||
|
assert pb.plot_item.vb.state["logMode"][0] is True
|
||||||
|
|
||||||
|
with qtbot.waitSignal(pb.property_changed, timeout=500) as sig2:
|
||||||
|
pb.x_log = False
|
||||||
|
assert pb.plot_item.vb.state["logMode"][0] is False
|
||||||
|
|
||||||
|
# Y log
|
||||||
|
pb.y_log = True
|
||||||
|
assert pb.plot_item.vb.state["logMode"][1] is True
|
||||||
|
pb.y_log = False
|
||||||
|
assert pb.plot_item.vb.state["logMode"][1] is False
|
||||||
|
|
||||||
|
|
||||||
|
def test_grid(qtbot, mocked_client):
|
||||||
|
"""
|
||||||
|
Test x_grid and y_grid toggles.
|
||||||
|
"""
|
||||||
|
pb = create_widget(qtbot, PlotBase, client=mocked_client)
|
||||||
|
# By default, might be off
|
||||||
|
with qtbot.waitSignal(pb.property_changed, timeout=500) as sigx:
|
||||||
|
pb.x_grid = True
|
||||||
|
assert sigx.args == ["x_grid", True]
|
||||||
|
# Confirm in pyqtgraph
|
||||||
|
assert pb.plot_item.ctrl.xGridCheck.isChecked() is True
|
||||||
|
|
||||||
|
with qtbot.waitSignal(pb.property_changed, timeout=500) as sigy:
|
||||||
|
pb.y_grid = True
|
||||||
|
assert sigy.args == ["y_grid", True]
|
||||||
|
assert pb.plot_item.ctrl.yGridCheck.isChecked() is True
|
||||||
|
|
||||||
|
|
||||||
|
def test_lock_aspect_ratio(qtbot, mocked_client):
|
||||||
|
"""
|
||||||
|
Test locking and unlocking the aspect ratio.
|
||||||
|
"""
|
||||||
|
pb = create_widget(qtbot, PlotBase, client=mocked_client)
|
||||||
|
# default is unlocked
|
||||||
|
assert bool(pb.plot_item.vb.getState()["aspectLocked"]) is False
|
||||||
|
|
||||||
|
pb.lock_aspect_ratio = True
|
||||||
|
assert bool(pb.plot_item.vb.getState()["aspectLocked"]) is True
|
||||||
|
|
||||||
|
pb.lock_aspect_ratio = False
|
||||||
|
assert bool(pb.plot_item.vb.getState()["aspectLocked"]) is False
|
||||||
|
|
||||||
|
|
||||||
|
def test_inner_axes_toggle(qtbot, mocked_client):
|
||||||
|
"""
|
||||||
|
Test the 'inner_axes' property, which shows/hides bottom and left axes.
|
||||||
|
"""
|
||||||
|
pb = create_widget(qtbot, PlotBase, client=mocked_client)
|
||||||
|
with qtbot.waitSignal(pb.property_changed, timeout=500) as sig_off:
|
||||||
|
pb.inner_axes = False
|
||||||
|
assert sig_off.args == ["inner_axes", False]
|
||||||
|
assert pb.plot_item.getAxis("bottom").isVisible() is False
|
||||||
|
assert pb.plot_item.getAxis("left").isVisible() is False
|
||||||
|
|
||||||
|
with qtbot.waitSignal(pb.property_changed, timeout=500) as sig_on:
|
||||||
|
pb.inner_axes = True
|
||||||
|
assert sig_on.args == ["inner_axes", True]
|
||||||
|
assert pb.plot_item.getAxis("bottom").isVisible() is True
|
||||||
|
assert pb.plot_item.getAxis("left").isVisible() is True
|
||||||
|
|
||||||
|
|
||||||
|
def test_outer_axes_toggle(qtbot, mocked_client):
|
||||||
|
"""
|
||||||
|
Test the 'outer_axes' property, which shows/hides top and right axes.
|
||||||
|
"""
|
||||||
|
pb = create_widget(qtbot, PlotBase, client=mocked_client)
|
||||||
|
with qtbot.waitSignal(pb.property_changed, timeout=500) as sig_on:
|
||||||
|
pb.outer_axes = True
|
||||||
|
assert sig_on.args == ["outer_axes", True]
|
||||||
|
assert pb.plot_item.getAxis("top").isVisible() is True
|
||||||
|
assert pb.plot_item.getAxis("right").isVisible() is True
|
||||||
|
|
||||||
|
with qtbot.waitSignal(pb.property_changed, timeout=500) as sig_off:
|
||||||
|
pb.outer_axes = False
|
||||||
|
assert sig_off.args == ["outer_axes", False]
|
||||||
|
assert pb.plot_item.getAxis("top").isVisible() is False
|
||||||
|
assert pb.plot_item.getAxis("right").isVisible() is False
|
||||||
|
|
||||||
|
|
||||||
|
def test_crosshair_hook_unhook(qtbot, mocked_client):
|
||||||
|
pb = create_widget(qtbot, PlotBase, client=mocked_client)
|
||||||
|
assert pb.crosshair is None
|
||||||
|
# Hook
|
||||||
|
pb.hook_crosshair()
|
||||||
|
assert pb.crosshair is not None
|
||||||
|
# Unhook
|
||||||
|
pb.unhook_crosshair()
|
||||||
|
assert pb.crosshair is None
|
||||||
|
|
||||||
|
# toggle
|
||||||
|
pb.toggle_crosshair()
|
||||||
|
assert pb.crosshair is not None
|
||||||
|
pb.toggle_crosshair()
|
||||||
|
assert pb.crosshair is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_set_method(qtbot, mocked_client):
|
||||||
|
"""
|
||||||
|
Test using the set(...) convenience method to update multiple properties at once.
|
||||||
|
"""
|
||||||
|
pb = create_widget(qtbot, PlotBase, client=mocked_client)
|
||||||
|
pb.set(
|
||||||
|
title="Multi Set Title",
|
||||||
|
x_label="Voltage",
|
||||||
|
y_label="Current",
|
||||||
|
x_grid=True,
|
||||||
|
y_grid=True,
|
||||||
|
x_log=True,
|
||||||
|
outer_axes=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert pb.title == "Multi Set Title"
|
||||||
|
assert pb.x_label == "Voltage"
|
||||||
|
assert pb.y_label == "Current"
|
||||||
|
assert pb.x_grid is True
|
||||||
|
assert pb.y_grid is True
|
||||||
|
assert pb.x_log is True
|
||||||
|
assert pb.outer_axes is True
|
Reference in New Issue
Block a user