summaryrefslogtreecommitdiff
path: root/src/silx/gui/plot/PlotToolButtons.py
diff options
context:
space:
mode:
authorPicca Frédéric-Emmanuel <picca@debian.org>2022-02-02 14:19:58 +0100
committerPicca Frédéric-Emmanuel <picca@debian.org>2022-02-02 14:19:58 +0100
commit4e774db12d5ebe7a20eded6dd434a289e27999e5 (patch)
treea9822974ba45196f1e3740995ab157d6eb214a04 /src/silx/gui/plot/PlotToolButtons.py
parentd3194b1a9c4404ba93afac43d97172ab24c57098 (diff)
New upstream version 1.0.0+dfsg
Diffstat (limited to 'src/silx/gui/plot/PlotToolButtons.py')
-rw-r--r--src/silx/gui/plot/PlotToolButtons.py592
1 files changed, 592 insertions, 0 deletions
diff --git a/src/silx/gui/plot/PlotToolButtons.py b/src/silx/gui/plot/PlotToolButtons.py
new file mode 100644
index 0000000..3970896
--- /dev/null
+++ b/src/silx/gui/plot/PlotToolButtons.py
@@ -0,0 +1,592 @@
+# coding: utf-8
+# /*##########################################################################
+#
+# Copyright (c) 2004-2020 European Synchrotron Radiation Facility
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+# ###########################################################################*/
+"""This module provides a set of QToolButton to use with
+:class:`~silx.gui.plot.PlotWidget`.
+
+The following QToolButton are available:
+
+- :class:`.AspectToolButton`
+- :class:`.YAxisOriginToolButton`
+- :class:`.ProfileToolButton`
+- :class:`.SymbolToolButton`
+
+"""
+
+__authors__ = ["V. Valls", "H. Payno"]
+__license__ = "MIT"
+__date__ = "27/06/2017"
+
+
+import functools
+import logging
+import weakref
+
+from .. import icons
+from .. import qt
+from ... import config
+
+from .items import SymbolMixIn, Scatter
+
+
+_logger = logging.getLogger(__name__)
+
+
+class PlotToolButton(qt.QToolButton):
+ """A QToolButton connected to a :class:`~silx.gui.plot.PlotWidget`.
+ """
+
+ def __init__(self, parent=None, plot=None):
+ super(PlotToolButton, self).__init__(parent)
+ self._plotRef = None
+ if plot is not None:
+ self.setPlot(plot)
+
+ def plot(self):
+ """
+ Returns the plot connected to the widget.
+ """
+ return None if self._plotRef is None else self._plotRef()
+
+ def setPlot(self, plot):
+ """
+ Set the plot connected to the widget
+
+ :param plot: :class:`.PlotWidget` instance on which to operate.
+ """
+ previousPlot = self.plot()
+
+ if previousPlot is plot:
+ return
+ if previousPlot is not None:
+ self._disconnectPlot(previousPlot)
+
+ if plot is None:
+ self._plotRef = None
+ else:
+ self._plotRef = weakref.ref(plot)
+ self._connectPlot(plot)
+
+ def _connectPlot(self, plot):
+ """
+ Called when the plot is connected to the widget
+
+ :param plot: :class:`.PlotWidget` instance
+ """
+ pass
+
+ def _disconnectPlot(self, plot):
+ """
+ Called when the plot is disconnected from the widget
+
+ :param plot: :class:`.PlotWidget` instance
+ """
+ pass
+
+
+class AspectToolButton(PlotToolButton):
+ """Tool button to switch keep aspect ratio of a plot"""
+
+ STATE = None
+ """Lazy loaded states used to feed AspectToolButton"""
+
+ def __init__(self, parent=None, plot=None):
+ if self.STATE is None:
+ self.STATE = {}
+ # dont keep ratio
+ self.STATE[False, "icon"] = icons.getQIcon('shape-ellipse-solid')
+ self.STATE[False, "state"] = "Aspect ratio is not kept"
+ self.STATE[False, "action"] = "Do no keep data aspect ratio"
+ # keep ratio
+ self.STATE[True, "icon"] = icons.getQIcon('shape-circle-solid')
+ self.STATE[True, "state"] = "Aspect ratio is kept"
+ self.STATE[True, "action"] = "Keep data aspect ratio"
+
+ super(AspectToolButton, self).__init__(parent=parent, plot=plot)
+
+ keepAction = self._createAction(True)
+ keepAction.triggered.connect(self.keepDataAspectRatio)
+ keepAction.setIconVisibleInMenu(True)
+
+ dontKeepAction = self._createAction(False)
+ dontKeepAction.triggered.connect(self.dontKeepDataAspectRatio)
+ dontKeepAction.setIconVisibleInMenu(True)
+
+ menu = qt.QMenu(self)
+ menu.addAction(keepAction)
+ menu.addAction(dontKeepAction)
+ self.setMenu(menu)
+ self.setPopupMode(qt.QToolButton.InstantPopup)
+
+ def _createAction(self, keepAspectRatio):
+ icon = self.STATE[keepAspectRatio, "icon"]
+ text = self.STATE[keepAspectRatio, "action"]
+ return qt.QAction(icon, text, self)
+
+ def _connectPlot(self, plot):
+ plot.sigSetKeepDataAspectRatio.connect(self._keepDataAspectRatioChanged)
+ self._keepDataAspectRatioChanged(plot.isKeepDataAspectRatio())
+
+ def _disconnectPlot(self, plot):
+ plot.sigSetKeepDataAspectRatio.disconnect(self._keepDataAspectRatioChanged)
+
+ def keepDataAspectRatio(self):
+ """Configure the plot to keep the aspect ratio"""
+ plot = self.plot()
+ if plot is not None:
+ # This will trigger _keepDataAspectRatioChanged
+ plot.setKeepDataAspectRatio(True)
+
+ def dontKeepDataAspectRatio(self):
+ """Configure the plot to not keep the aspect ratio"""
+ plot = self.plot()
+ if plot is not None:
+ # This will trigger _keepDataAspectRatioChanged
+ plot.setKeepDataAspectRatio(False)
+
+ def _keepDataAspectRatioChanged(self, aspectRatio):
+ """Handle Plot set keep aspect ratio signal"""
+ icon, toolTip = self.STATE[aspectRatio, "icon"], self.STATE[aspectRatio, "state"]
+ self.setIcon(icon)
+ self.setToolTip(toolTip)
+
+
+class YAxisOriginToolButton(PlotToolButton):
+ """Tool button to switch the Y axis orientation of a plot."""
+
+ STATE = None
+ """Lazy loaded states used to feed YAxisOriginToolButton"""
+
+ def __init__(self, parent=None, plot=None):
+ if self.STATE is None:
+ self.STATE = {}
+ # is down
+ self.STATE[False, "icon"] = icons.getQIcon('plot-ydown')
+ self.STATE[False, "state"] = "Y-axis is oriented downward"
+ self.STATE[False, "action"] = "Orient Y-axis downward"
+ # keep ration
+ self.STATE[True, "icon"] = icons.getQIcon('plot-yup')
+ self.STATE[True, "state"] = "Y-axis is oriented upward"
+ self.STATE[True, "action"] = "Orient Y-axis upward"
+
+ super(YAxisOriginToolButton, self).__init__(parent=parent, plot=plot)
+
+ upwardAction = self._createAction(True)
+ upwardAction.triggered.connect(self.setYAxisUpward)
+ upwardAction.setIconVisibleInMenu(True)
+
+ downwardAction = self._createAction(False)
+ downwardAction.triggered.connect(self.setYAxisDownward)
+ downwardAction.setIconVisibleInMenu(True)
+
+ menu = qt.QMenu(self)
+ menu.addAction(upwardAction)
+ menu.addAction(downwardAction)
+ self.setMenu(menu)
+ self.setPopupMode(qt.QToolButton.InstantPopup)
+
+ def _createAction(self, isUpward):
+ icon = self.STATE[isUpward, "icon"]
+ text = self.STATE[isUpward, "action"]
+ return qt.QAction(icon, text, self)
+
+ def _connectPlot(self, plot):
+ yAxis = plot.getYAxis()
+ yAxis.sigInvertedChanged.connect(self._yAxisInvertedChanged)
+ self._yAxisInvertedChanged(yAxis.isInverted())
+
+ def _disconnectPlot(self, plot):
+ plot.getYAxis().sigInvertedChanged.disconnect(self._yAxisInvertedChanged)
+
+ def setYAxisUpward(self):
+ """Configure the plot to use y-axis upward"""
+ plot = self.plot()
+ if plot is not None:
+ # This will trigger _yAxisInvertedChanged
+ plot.getYAxis().setInverted(False)
+
+ def setYAxisDownward(self):
+ """Configure the plot to use y-axis downward"""
+ plot = self.plot()
+ if plot is not None:
+ # This will trigger _yAxisInvertedChanged
+ plot.getYAxis().setInverted(True)
+
+ def _yAxisInvertedChanged(self, inverted):
+ """Handle Plot set y axis inverted signal"""
+ isUpward = not inverted
+ icon, toolTip = self.STATE[isUpward, "icon"], self.STATE[isUpward, "state"]
+ self.setIcon(icon)
+ self.setToolTip(toolTip)
+
+
+class ProfileOptionToolButton(PlotToolButton):
+ """Button to define option on the profile"""
+ sigMethodChanged = qt.Signal(str)
+
+ def __init__(self, parent=None, plot=None):
+ PlotToolButton.__init__(self, parent=parent, plot=plot)
+
+ self.STATE = {}
+ # is down
+ self.STATE['sum', "icon"] = icons.getQIcon('math-sigma')
+ self.STATE['sum', "state"] = "Compute profile sum"
+ self.STATE['sum', "action"] = "Compute profile sum"
+ # keep ration
+ self.STATE['mean', "icon"] = icons.getQIcon('math-mean')
+ self.STATE['mean', "state"] = "Compute profile mean"
+ self.STATE['mean', "action"] = "Compute profile mean"
+
+ self.sumAction = self._createAction('sum')
+ self.sumAction.triggered.connect(self.setSum)
+ self.sumAction.setIconVisibleInMenu(True)
+ self.sumAction.setCheckable(True)
+ self.sumAction.setChecked(True)
+
+ self.meanAction = self._createAction('mean')
+ self.meanAction.triggered.connect(self.setMean)
+ self.meanAction.setIconVisibleInMenu(True)
+ self.meanAction.setCheckable(True)
+
+ menu = qt.QMenu(self)
+ menu.addAction(self.sumAction)
+ menu.addAction(self.meanAction)
+ self.setMenu(menu)
+ self.setPopupMode(qt.QToolButton.InstantPopup)
+ self._method = 'mean'
+ self._update()
+
+ def _createAction(self, method):
+ icon = self.STATE[method, "icon"]
+ text = self.STATE[method, "action"]
+ return qt.QAction(icon, text, self)
+
+ def setSum(self):
+ self.setMethod('sum')
+
+ def _update(self):
+ icon = self.STATE[self._method, "icon"]
+ toolTip = self.STATE[self._method, "state"]
+ self.setIcon(icon)
+ self.setToolTip(toolTip)
+ self.sumAction.setChecked(self._method == "sum")
+ self.meanAction.setChecked(self._method == "mean")
+
+ def setMean(self):
+ self.setMethod('mean')
+
+ def setMethod(self, method):
+ """Set the method to use.
+
+ :param str method: Either 'sum' or 'mean'
+ """
+ if method != self._method:
+ if method in ('sum', 'mean'):
+ self._method = method
+ self.sigMethodChanged.emit(self._method)
+ self._update()
+ else:
+ _logger.warning(
+ "Unsupported method '%s'. Setting ignored.", method)
+
+ def getMethod(self):
+ """Returns the current method in use (See :meth:`setMethod`).
+
+ :rtype: str
+ """
+ return self._method
+
+
+class ProfileToolButton(PlotToolButton):
+ """Button used in Profile3DToolbar to switch between 2D profile
+ and 1D profile."""
+ STATE = None
+ """Lazy loaded states used to feed ProfileToolButton"""
+
+ sigDimensionChanged = qt.Signal(int)
+
+ def __init__(self, parent=None, plot=None):
+ if self.STATE is None:
+ self.STATE = {
+ (1, "icon"): icons.getQIcon('profile1D'),
+ (1, "state"): "1D profile is computed on visible image",
+ (1, "action"): "1D profile on visible image",
+ (2, "icon"): icons.getQIcon('profile2D'),
+ (2, "state"): "2D profile is computed, one 1D profile for each image in the stack",
+ (2, "action"): "2D profile on image stack"}
+ # Compute 1D profile
+ # Compute 2D profile
+
+ super(ProfileToolButton, self).__init__(parent=parent, plot=plot)
+
+ self._dimension = 1
+
+ profile1DAction = self._createAction(1)
+ profile1DAction.triggered.connect(self.computeProfileIn1D)
+ profile1DAction.setIconVisibleInMenu(True)
+ profile1DAction.setCheckable(True)
+ profile1DAction.setChecked(True)
+ self._profile1DAction = profile1DAction
+
+ profile2DAction = self._createAction(2)
+ profile2DAction.triggered.connect(self.computeProfileIn2D)
+ profile2DAction.setIconVisibleInMenu(True)
+ profile2DAction.setCheckable(True)
+ self._profile2DAction = profile2DAction
+
+ menu = qt.QMenu(self)
+ menu.addAction(profile1DAction)
+ menu.addAction(profile2DAction)
+ self.setMenu(menu)
+ self.setPopupMode(qt.QToolButton.InstantPopup)
+ menu.setTitle('Select profile dimension')
+ self.computeProfileIn1D()
+
+ def _createAction(self, profileDimension):
+ icon = self.STATE[profileDimension, "icon"]
+ text = self.STATE[profileDimension, "action"]
+ return qt.QAction(icon, text, self)
+
+ def _profileDimensionChanged(self, profileDimension):
+ """Update icon in toolbar, emit number of dimensions for profile"""
+ self.setIcon(self.STATE[profileDimension, "icon"])
+ self.setToolTip(self.STATE[profileDimension, "state"])
+ self._dimension = profileDimension
+ self.sigDimensionChanged.emit(profileDimension)
+ self._profile1DAction.setChecked(profileDimension == 1)
+ self._profile2DAction.setChecked(profileDimension == 2)
+
+ def computeProfileIn1D(self):
+ self._profileDimensionChanged(1)
+
+ def computeProfileIn2D(self):
+ self._profileDimensionChanged(2)
+
+ def setDimension(self, dimension):
+ """Set the selected dimension"""
+ assert dimension in [1, 2]
+ if self._dimension == dimension:
+ return
+ if dimension == 1:
+ self.computeProfileIn1D()
+ elif dimension == 2:
+ self.computeProfileIn2D()
+ else:
+ _logger.warning("Unsupported dimension '%s'. Setting ignored.", dimension)
+
+ def getDimension(self):
+ """Get the selected dimension.
+
+ :rtype: int (1 or 2)
+ """
+ return self._dimension
+
+
+class _SymbolToolButtonBase(PlotToolButton):
+ """Base class for PlotToolButton setting marker and size.
+
+ :param parent: See QWidget
+ :param plot: The `~silx.gui.plot.PlotWidget` to control
+ """
+
+ def __init__(self, parent=None, plot=None):
+ super(_SymbolToolButtonBase, self).__init__(parent=parent, plot=plot)
+
+ def _addSizeSliderToMenu(self, menu):
+ """Add a slider to set size to the given menu
+
+ :param QMenu menu:
+ """
+ slider = qt.QSlider(qt.Qt.Horizontal)
+ slider.setRange(1, 20)
+ slider.setValue(int(config.DEFAULT_PLOT_SYMBOL_SIZE))
+ slider.setTracking(False)
+ slider.valueChanged.connect(self._sizeChanged)
+ widgetAction = qt.QWidgetAction(menu)
+ widgetAction.setDefaultWidget(slider)
+ menu.addAction(widgetAction)
+
+ def _addSymbolsToMenu(self, menu):
+ """Add symbols to the given menu
+
+ :param QMenu menu:
+ """
+ for marker, name in zip(SymbolMixIn.getSupportedSymbols(),
+ SymbolMixIn.getSupportedSymbolNames()):
+ action = qt.QAction(name, menu)
+ action.setCheckable(False)
+ action.triggered.connect(
+ functools.partial(self._markerChanged, marker))
+ menu.addAction(action)
+
+ def _sizeChanged(self, value):
+ """Manage slider value changed
+
+ :param int value: Marker size
+ """
+ plot = self.plot()
+ if plot is None:
+ return
+
+ for item in plot.getItems():
+ if isinstance(item, SymbolMixIn):
+ item.setSymbolSize(value)
+
+ def _markerChanged(self, marker):
+ """Manage change of marker.
+
+ :param str marker: Letter describing the marker
+ """
+ plot = self.plot()
+ if plot is None:
+ return
+
+ for item in plot.getItems():
+ if isinstance(item, SymbolMixIn):
+ item.setSymbol(marker)
+
+
+class SymbolToolButton(_SymbolToolButtonBase):
+ """A tool button with a drop-down menu to control symbol size and marker.
+
+ :param parent: See QWidget
+ :param plot: The `~silx.gui.plot.PlotWidget` to control
+ """
+
+ def __init__(self, parent=None, plot=None):
+ super(SymbolToolButton, self).__init__(parent=parent, plot=plot)
+
+ self.setToolTip('Set symbol size and marker')
+ self.setIcon(icons.getQIcon('plot-symbols'))
+
+ menu = qt.QMenu(self)
+ self._addSizeSliderToMenu(menu)
+ menu.addSeparator()
+ self._addSymbolsToMenu(menu)
+
+ self.setMenu(menu)
+ self.setPopupMode(qt.QToolButton.InstantPopup)
+
+
+class ScatterVisualizationToolButton(_SymbolToolButtonBase):
+ """QToolButton to select the visualization mode of scatter plot
+
+ :param parent: See QWidget
+ :param plot: The `~silx.gui.plot.PlotWidget` to control
+ """
+
+ def __init__(self, parent=None, plot=None):
+ super(ScatterVisualizationToolButton, self).__init__(
+ parent=parent, plot=plot)
+
+ self.setToolTip(
+ 'Set scatter visualization mode, symbol marker and size')
+ self.setIcon(icons.getQIcon('eye'))
+
+ menu = qt.QMenu(self)
+
+ # Add visualization modes
+
+ for mode in Scatter.supportedVisualizations():
+ if mode is not Scatter.Visualization.BINNED_STATISTIC:
+ name = mode.value.capitalize()
+ action = qt.QAction(name, menu)
+ action.setCheckable(False)
+ action.triggered.connect(
+ functools.partial(self._visualizationChanged, mode, None))
+ menu.addAction(action)
+
+ if Scatter.Visualization.BINNED_STATISTIC in Scatter.supportedVisualizations():
+ reductions = Scatter.supportedVisualizationParameterValues(
+ Scatter.VisualizationParameter.BINNED_STATISTIC_FUNCTION)
+ if reductions:
+ submenu = menu.addMenu('Binned Statistic')
+ for reduction in reductions:
+ name = reduction.capitalize()
+ action = qt.QAction(name, menu)
+ action.setCheckable(False)
+ action.triggered.connect(functools.partial(
+ self._visualizationChanged,
+ Scatter.Visualization.BINNED_STATISTIC,
+ {Scatter.VisualizationParameter.BINNED_STATISTIC_FUNCTION: reduction}))
+ submenu.addAction(action)
+
+ submenu.addSeparator()
+ binsmenu = submenu.addMenu('N Bins')
+
+ slider = qt.QSlider(qt.Qt.Horizontal)
+ slider.setRange(10, 1000)
+ slider.setValue(100)
+ slider.setTracking(False)
+ slider.valueChanged.connect(self._binningChanged)
+ widgetAction = qt.QWidgetAction(binsmenu)
+ widgetAction.setDefaultWidget(slider)
+ binsmenu.addAction(widgetAction)
+
+ menu.addSeparator()
+
+ submenu = menu.addMenu(icons.getQIcon('plot-symbols'), "Symbol")
+ self._addSymbolsToMenu(submenu)
+
+ submenu = menu.addMenu(icons.getQIcon('plot-symbols'), "Symbol Size")
+ self._addSizeSliderToMenu(submenu)
+
+ self.setMenu(menu)
+ self.setPopupMode(qt.QToolButton.InstantPopup)
+
+ def _visualizationChanged(self, mode, parameters=None):
+ """Handle change of visualization mode.
+
+ :param ScatterVisualizationMixIn.Visualization mode:
+ The visualization mode to use for scatter
+ :param Union[dict,None] parameters:
+ Dict of VisualizationParameter: parameter_value to set
+ with the visualization.
+ """
+ plot = self.plot()
+ if plot is None:
+ return
+
+ for item in plot.getItems():
+ if isinstance(item, Scatter):
+ if parameters:
+ for parameter, value in parameters.items():
+ item.setVisualizationParameter(parameter, value)
+ item.setVisualization(mode)
+
+ def _binningChanged(self, value):
+ """Handle change of binning.
+
+ :param int value: The number of bin on each dimension.
+ """
+ plot = self.plot()
+ if plot is None:
+ return
+
+ for item in plot.getItems():
+ if isinstance(item, Scatter):
+ item.setVisualizationParameter(
+ Scatter.VisualizationParameter.BINNED_STATISTIC_SHAPE,
+ (value, value))
+ item.setVisualization(Scatter.Visualization.BINNED_STATISTIC)