diff options
Diffstat (limited to 'silx/gui/plot/tools/roi.py')
-rw-r--r-- | silx/gui/plot/tools/roi.py | 1417 |
1 files changed, 0 insertions, 1417 deletions
diff --git a/silx/gui/plot/tools/roi.py b/silx/gui/plot/tools/roi.py deleted file mode 100644 index 4e2d6db..0000000 --- a/silx/gui/plot/tools/roi.py +++ /dev/null @@ -1,1417 +0,0 @@ -# coding: utf-8 -# /*########################################################################## -# -# Copyright (c) 2018-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 ROI interaction for :class:`~silx.gui.plot.PlotWidget`. -""" - -__authors__ = ["T. Vincent"] -__license__ = "MIT" -__date__ = "28/06/2018" - - -import enum -import logging -import time -import weakref -import functools - -import numpy - -from ... import qt, icons -from ...utils import blockSignals -from ...utils import LockReentrant -from .. import PlotWidget -from ..items import roi as roi_items - -from ...colors import rgba - - -logger = logging.getLogger(__name__) - - -class CreateRoiModeAction(qt.QAction): - """ - This action is a plot mode which allows to create new ROIs using a ROI - manager. - - A ROI is created using a specific `roiClass`. `initRoi` and `finalizeRoi` - can be inherited to custom the ROI initialization. - - :param class roiClass: The ROI class which will be created by this action. - :param qt.QObject parent: The action parent - :param RegionOfInterestManager roiManager: The ROI manager - """ - - def __init__(self, parent, roiManager, roiClass): - assert roiManager is not None - assert roiClass is not None - qt.QAction.__init__(self, parent=parent) - self._roiManager = weakref.ref(roiManager) - self._roiClass = roiClass - self._singleShot = False - self._initAction() - self.triggered[bool].connect(self._actionTriggered) - - def _initAction(self): - """Default initialization of the action""" - roiClass = self._roiClass - - name = None - iconName = None - if hasattr(roiClass, "NAME"): - name = roiClass.NAME - if hasattr(roiClass, "ICON"): - iconName = roiClass.ICON - - if iconName is None: - iconName = "add-shape-unknown" - if name is None: - name = roiClass.__name__ - text = 'Add %s' % name - self.setIcon(icons.getQIcon(iconName)) - self.setText(text) - self.setCheckable(True) - self.setToolTip(text) - - def getRoiClass(self): - """Return the ROI class used by this action to create ROIs""" - return self._roiClass - - def getRoiManager(self): - return self._roiManager() - - def setSingleShot(self, singleShot): - """Set it to True to deactivate the action after the first creation - of a ROI. - - :param bool singleShot: New single short state - """ - self._singleShot = singleShot - - def getSingleShot(self): - """If True, after the first creation of a ROI with this mode, - the mode is deactivated. - - :rtype: bool - """ - return self._singleShot - - def _actionTriggered(self, checked): - """Handle mode actions being checked by the user - - :param bool checked: - :param str kind: Corresponding shape kind - """ - roiManager = self.getRoiManager() - if roiManager is None: - return - - if checked: - roiManager.start(self._roiClass, self) - self.__interactiveModeStarted(roiManager) - else: - source = roiManager.getInteractionSource() - if source is self: - roiManager.stop() - - def __interactiveModeStarted(self, roiManager): - roiManager.sigInteractiveRoiCreated.connect(self.initRoi) - roiManager.sigInteractiveRoiFinalized.connect(self.__finalizeRoi) - roiManager.sigInteractiveModeFinished.connect(self.__interactiveModeFinished) - - def __interactiveModeFinished(self): - roiManager = self.getRoiManager() - if roiManager is not None: - roiManager.sigInteractiveRoiCreated.disconnect(self.initRoi) - roiManager.sigInteractiveRoiFinalized.disconnect(self.__finalizeRoi) - roiManager.sigInteractiveModeFinished.disconnect(self.__interactiveModeFinished) - self.setChecked(False) - - def initRoi(self, roi): - """Inherit it to custom the new ROI at it's creation during the - interaction.""" - pass - - def __finalizeRoi(self, roi): - self.finalizeRoi(roi) - if self._singleShot: - roiManager = self.getRoiManager() - if roiManager is not None: - roiManager.stop() - - def finalizeRoi(self, roi): - """Inherit it to custom the new ROI after it's creation when the - interaction is finalized.""" - pass - - -class RoiModeSelector(qt.QWidget): - def __init__(self, parent=None): - super(RoiModeSelector, self).__init__(parent=parent) - self.__roi = None - self.__reentrant = LockReentrant() - - layout = qt.QHBoxLayout(self) - if isinstance(parent, qt.QMenu): - margins = layout.contentsMargins() - layout.setContentsMargins(margins.left(), 0, margins.right(), 0) - else: - layout.setContentsMargins(0, 0, 0, 0) - - self._label = qt.QLabel(self) - self._label.setText("Mode:") - self._label.setToolTip("Select a specific interaction to edit the ROI") - self._combo = qt.QComboBox(self) - self._combo.currentIndexChanged.connect(self._modeSelected) - layout.addWidget(self._label) - layout.addWidget(self._combo) - self._updateAvailableModes() - - def getRoi(self): - """Returns the edited ROI. - - :rtype: roi_items.RegionOfInterest - """ - return self.__roi - - def setRoi(self, roi): - """Returns the edited ROI. - - :rtype: roi_items.RegionOfInterest - """ - if self.__roi is roi: - return - if not isinstance(roi, roi_items.InteractionModeMixIn): - self.__roi = None - self._updateAvailableModes() - return - - if self.__roi is not None: - self.__roi.sigInteractionModeChanged.disconnect(self._modeChanged) - self.__roi = roi - if self.__roi is not None: - self.__roi.sigInteractionModeChanged.connect(self._modeChanged) - self._updateAvailableModes() - - def isEmpty(self): - return not self._label.isVisibleTo(self) - - def _updateAvailableModes(self): - roi = self.getRoi() - if isinstance(roi, roi_items.InteractionModeMixIn): - modes = roi.availableInteractionModes() - else: - modes = [] - if len(modes) <= 1: - self._label.setVisible(False) - self._combo.setVisible(False) - else: - self._label.setVisible(True) - self._combo.setVisible(True) - with blockSignals(self._combo): - self._combo.clear() - for im, m in enumerate(modes): - self._combo.addItem(m.label, m) - self._combo.setItemData(im, m.description, qt.Qt.ToolTipRole) - mode = roi.getInteractionMode() - self._modeChanged(mode) - index = modes.index(mode) - self._combo.setCurrentIndex(index) - - def _modeChanged(self, mode): - """Triggered when the ROI interaction mode was changed externally""" - if self.__reentrant.locked(): - # This event was initialised by the widget - return - roi = self.__roi - modes = roi.availableInteractionModes() - index = modes.index(mode) - with blockSignals(self._combo): - self._combo.setCurrentIndex(index) - - def _modeSelected(self): - """Triggered when the ROI interaction mode was selected in the widget""" - index = self._combo.currentIndex() - if index == -1: - return - roi = self.getRoi() - if roi is not None: - mode = self._combo.itemData(index, qt.Qt.UserRole) - with self.__reentrant: - roi.setInteractionMode(mode) - - -class RoiModeSelectorAction(qt.QWidgetAction): - """Display the selected mode of a ROI and allow to change it""" - - def __init__(self, parent=None): - super(RoiModeSelectorAction, self).__init__(parent) - self.__roiManager = None - - def createWidget(self, parent): - """Inherit the method to create a new widget""" - widget = RoiModeSelector(parent) - manager = self.__roiManager - if manager is not None: - roi = manager.getCurrentRoi() - widget.setRoi(roi) - self.setVisible(not widget.isEmpty()) - return widget - - def deleteWidget(self, widget): - """Inherit the method to delete a widget""" - widget.setRoi(None) - return qt.QWidgetAction.deleteWidget(self, widget) - - def setRoiManager(self, roiManager): - """ - Connect this action to a ROI manager. - - :param RegionOfInterestManager roiManager: A ROI manager - """ - if self.__roiManager is roiManager: - return - if self.__roiManager is not None: - self.__roiManager.sigCurrentRoiChanged.disconnect(self.__currentRoiChanged) - self.__roiManager = roiManager - if self.__roiManager is not None: - self.__roiManager.sigCurrentRoiChanged.connect(self.__currentRoiChanged) - self.__currentRoiChanged(roiManager.getCurrentRoi()) - - def __currentRoiChanged(self, roi): - """Handle changes of the selected ROI""" - self.setRoi(roi) - - def setRoi(self, roi): - """Set a profile ROI to edit. - - :param ProfileRoiMixIn roi: A profile ROI - """ - widget = None - for widget in self.createdWidgets(): - widget.setRoi(roi) - if widget is not None: - self.setVisible(not widget.isEmpty()) - - -class RegionOfInterestManager(qt.QObject): - """Class handling ROI interaction on a PlotWidget. - - It supports the multiple ROIs: points, rectangles, polygons, - lines, horizontal and vertical lines. - - See ``plotInteractiveImageROI.py`` sample code (:ref:`sample-code`). - - :param silx.gui.plot.PlotWidget parent: - The plot widget in which to control the ROIs. - """ - - sigRoiAdded = qt.Signal(roi_items.RegionOfInterest) - """Signal emitted when a new ROI has been added. - - It provides the newly add :class:`RegionOfInterest` object. - """ - - sigRoiAboutToBeRemoved = qt.Signal(roi_items.RegionOfInterest) - """Signal emitted just before a ROI is removed. - - It provides the :class:`RegionOfInterest` object that is about to be removed. - """ - - sigRoiChanged = qt.Signal() - """Signal emitted whenever the ROIs have changed.""" - - sigCurrentRoiChanged = qt.Signal(object) - """Signal emitted whenever a ROI is selected.""" - - sigInteractiveModeStarted = qt.Signal(object) - """Signal emitted when switching to ROI drawing interactive mode. - - It provides the class of the ROI which will be created by the interactive - mode. - """ - - sigInteractiveRoiCreated = qt.Signal(object) - """Signal emitted when a ROI is created during the interaction. - The interaction is still incomplete and can be aborted. - - It provides the ROI object which was just been created. - """ - - sigInteractiveRoiFinalized = qt.Signal(object) - """Signal emitted when a ROI creation is complet. - - It provides the ROI object which was just been created. - """ - - sigInteractiveModeFinished = qt.Signal() - """Signal emitted when leaving interactive ROI drawing mode. - """ - - ROI_CLASSES = ( - roi_items.PointROI, - roi_items.CrossROI, - roi_items.RectangleROI, - roi_items.CircleROI, - roi_items.EllipseROI, - roi_items.PolygonROI, - roi_items.LineROI, - roi_items.HorizontalLineROI, - roi_items.VerticalLineROI, - roi_items.ArcROI, - roi_items.HorizontalRangeROI, - ) - - def __init__(self, parent): - assert isinstance(parent, PlotWidget) - super(RegionOfInterestManager, self).__init__(parent) - self._rois = [] # List of ROIs - self._drawnROI = None # New ROI being currently drawn - - self._roiClass = None - self._source = None - self._color = rgba('red') - - self._label = "__RegionOfInterestManager__%d" % id(self) - - self._currentRoi = None - """Hold currently selected ROI""" - - self._eventLoop = None - - self._modeActions = {} - - parent.sigPlotSignal.connect(self._plotSignals) - - parent.sigInteractiveModeChanged.connect( - self._plotInteractiveModeChanged) - - parent.sigItemRemoved.connect(self._itemRemoved) - - parent._sigDefaultContextMenu.connect(self._feedContextMenu) - - @classmethod - def getSupportedRoiClasses(cls): - """Returns the default available ROI classes - - :rtype: List[class] - """ - return tuple(cls.ROI_CLASSES) - - # Associated QActions - - def getInteractionModeAction(self, roiClass): - """Returns the QAction corresponding to a kind of ROI - - The QAction allows to enable the corresponding drawing - interactive mode. - - :param class roiClass: The ROI class which will be created by this action. - :rtype: QAction - :raise ValueError: If kind is not supported - """ - if not issubclass(roiClass, roi_items.RegionOfInterest): - raise ValueError('Unsupported ROI class %s' % roiClass) - - action = self._modeActions.get(roiClass, None) - if action is None: # Lazy-loading - action = CreateRoiModeAction(self, self, roiClass) - self._modeActions[roiClass] = action - return action - - # PlotWidget eventFilter and listeners - - def _plotInteractiveModeChanged(self, source): - """Handle change of interactive mode in the plot""" - if source is not self: - self.__roiInteractiveModeEnded() - - def _getRoiFromItem(self, item): - """Returns the ROI which own this item, else None - if this manager do not have knowledge of this ROI.""" - for roi in self._rois: - if isinstance(roi, roi_items.RegionOfInterest): - for child in roi.getItems(): - if child is item: - return roi - return None - - def _itemRemoved(self, item): - """Called after an item was removed from the plot.""" - if not hasattr(item, "_roiGroup"): - # Early break to avoid to use _getRoiFromItem - # And to avoid reentrant signal when the ROI remove the item itself - return - roi = self._getRoiFromItem(item) - if roi is not None: - self.removeRoi(roi) - - # Handle ROI interaction - - def _handleInteraction(self, event): - """Handle mouse interaction for ROI addition""" - roiClass = self.getCurrentInteractionModeRoiClass() - if roiClass is None: - return # Should not happen - - kind = roiClass.getFirstInteractionShape() - if kind == 'point': - if event['event'] == 'mouseClicked' and event['button'] == 'left': - points = numpy.array([(event['x'], event['y'])], - dtype=numpy.float64) - # Not an interactive creation - roi = self._createInteractiveRoi(roiClass, points=points) - roi.creationFinalized() - self.sigInteractiveRoiFinalized.emit(roi) - else: # other shapes - if (event['event'] in ('drawingProgress', 'drawingFinished') and - event['parameters']['label'] == self._label): - points = numpy.array((event['xdata'], event['ydata']), - dtype=numpy.float64).T - - if self._drawnROI is None: # Create new ROI - # NOTE: Set something before createRoi, so isDrawing is True - self._drawnROI = object() - self._drawnROI = self._createInteractiveRoi(roiClass, points=points) - else: - self._drawnROI.setFirstShapePoints(points) - - if event['event'] == 'drawingFinished': - if kind == 'polygon' and len(points) > 1: - self._drawnROI.setFirstShapePoints(points[:-1]) - roi = self._drawnROI - self._drawnROI = None # Stop drawing - roi.creationFinalized() - self.sigInteractiveRoiFinalized.emit(roi) - - # RegionOfInterest selection - - def __getRoiFromMarker(self, marker): - """Returns a ROI from a marker, else None""" - # This should be speed up - for roi in self._rois: - if isinstance(roi, roi_items.HandleBasedROI): - for m in roi.getHandles(): - if m is marker: - return roi - else: - for m in roi.getItems(): - if m is marker: - return roi - return None - - def setCurrentRoi(self, roi): - """Set the currently selected ROI, and emit a signal. - - :param Union[RegionOfInterest,None] roi: The ROI to select - """ - if self._currentRoi is roi: - return - if roi is not None: - # Note: Fixed range to avoid infinite loops - for _ in range(10): - target = roi.getFocusProxy() - if target is None: - break - roi = target - else: - raise RuntimeError("Max selection proxy depth (10) reached.") - - if self._currentRoi is not None: - self._currentRoi.setHighlighted(False) - self._currentRoi = roi - if self._currentRoi is not None: - self._currentRoi.setHighlighted(True) - self.sigCurrentRoiChanged.emit(roi) - - def getCurrentRoi(self): - """Returns the currently selected ROI, else None. - - :rtype: Union[RegionOfInterest,None] - """ - return self._currentRoi - - def _plotSignals(self, event): - """Handle mouse interaction for ROI addition""" - clicked = False - roi = None - if event["event"] in ("markerClicked", "markerMoving"): - plot = self.parent() - legend = event["label"] - marker = plot._getMarker(legend=legend) - roi = self.__getRoiFromMarker(marker) - elif event["event"] == "mouseClicked" and event["button"] == "left": - # Marker click is only for dnd - # This also can click on a marker - clicked = True - plot = self.parent() - marker = plot._getMarkerAt(event["xpixel"], event["ypixel"]) - roi = self.__getRoiFromMarker(marker) - else: - return - - if roi not in self._rois: - # The ROI is not own by this manager - return - - if roi is not None: - currentRoi = self.getCurrentRoi() - if currentRoi is roi: - if clicked: - self.__updateMode(roi) - elif roi.isSelectable(): - self.setCurrentRoi(roi) - else: - self.setCurrentRoi(None) - - def __updateMode(self, roi): - if isinstance(roi, roi_items.InteractionModeMixIn): - available = roi.availableInteractionModes() - mode = roi.getInteractionMode() - imode = available.index(mode) - mode = available[(imode + 1) % len(available)] - roi.setInteractionMode(mode) - - def _feedContextMenu(self, menu): - """Called wen the default plot context menu is about to be displayed""" - roi = self.getCurrentRoi() - if roi is not None: - if roi.isEditable(): - # Filter by data position - # FIXME: It would be better to use GUI coords for it - plot = self.parent() - pos = plot.getWidgetHandle().mapFromGlobal(qt.QCursor.pos()) - data = plot.pixelToData(pos.x(), pos.y()) - if roi.contains(data): - if isinstance(roi, roi_items.InteractionModeMixIn): - self._contextMenuForInteractionMode(menu, roi) - - removeAction = qt.QAction(menu) - removeAction.setText("Remove %s" % roi.getName()) - callback = functools.partial(self.removeRoi, roi) - removeAction.triggered.connect(callback) - menu.addAction(removeAction) - - def _contextMenuForInteractionMode(self, menu, roi): - availableModes = roi.availableInteractionModes() - currentMode = roi.getInteractionMode() - submenu = qt.QMenu(menu) - modeGroup = qt.QActionGroup(menu) - modeGroup.setExclusive(True) - for mode in availableModes: - action = qt.QAction(menu) - action.setText(mode.label) - action.setToolTip(mode.description) - action.setCheckable(True) - if mode is currentMode: - action.setChecked(True) - else: - callback = functools.partial(roi.setInteractionMode, mode) - action.triggered.connect(callback) - modeGroup.addAction(action) - submenu.addAction(action) - action = qt.QAction(menu) - action.setMenu(submenu) - action.setText("%s interaction mode" % roi.getName()) - menu.addAction(action) - - # RegionOfInterest API - - def getRois(self): - """Returns the list of ROIs. - - It returns an empty tuple if there is currently no ROI. - - :return: Tuple of arrays of objects describing the ROIs - :rtype: List[RegionOfInterest] - """ - return tuple(self._rois) - - def clear(self): - """Reset current ROIs - - :return: True if ROIs were reset. - :rtype: bool - """ - if self.getRois(): # Something to reset - for roi in self._rois: - roi.sigRegionChanged.disconnect( - self._regionOfInterestChanged) - roi.setParent(None) - self._rois = [] - self._roisUpdated() - return True - - else: - return False - - def _regionOfInterestChanged(self, event=None): - """Handle ROI object changed""" - self.sigRoiChanged.emit() - - def _createInteractiveRoi(self, roiClass, points, label=None, index=None): - """Create a new ROI with interactive creation. - - :param class roiClass: The class of the ROI to create - :param numpy.ndarray points: The first shape used to create the ROI - :param str label: The label to display along with the ROI. - :param int index: The position where to insert the ROI. - By default it is appended to the end of the list. - :return: The created ROI object - :rtype: roi_items.RegionOfInterest - :raise RuntimeError: When ROI cannot be added because the maximum - number of ROIs has been reached. - """ - roi = roiClass(parent=None) - if label is not None: - roi.setName(str(label)) - roi.creationStarted() - roi.setFirstShapePoints(points) - - self.addRoi(roi, index) - if roi.isSelectable(): - self.setCurrentRoi(roi) - self.sigInteractiveRoiCreated.emit(roi) - return roi - - def containsRoi(self, roi): - """Returns true if the ROI is part of this manager. - - :param roi_items.RegionOfInterest roi: The ROI to add - :rtype: bool - """ - return roi in self._rois - - def addRoi(self, roi, index=None, useManagerColor=True): - """Add the ROI to the list of ROIs. - - :param roi_items.RegionOfInterest roi: The ROI to add - :param int index: The position where to insert the ROI, - By default it is appended to the end of the list of ROIs - :param bool useManagerColor: - Whether to set the ROI color to the default one of the manager or not. - (Default: True). - :raise RuntimeError: When ROI cannot be added because the maximum - number of ROIs has been reached. - """ - plot = self.parent() - if plot is None: - raise RuntimeError( - 'Cannot add ROI: PlotWidget no more available') - - roi.setParent(self) - - if useManagerColor: - roi.setColor(self.getColor()) - - roi.sigRegionChanged.connect(self._regionOfInterestChanged) - roi.sigItemChanged.connect(self._regionOfInterestChanged) - - if index is None: - self._rois.append(roi) - else: - self._rois.insert(index, roi) - self.sigRoiAdded.emit(roi) - self._roisUpdated() - - def removeRoi(self, roi): - """Remove a ROI from the list of ROIs. - - :param roi_items.RegionOfInterest roi: The ROI to remove - :raise ValueError: When ROI does not belong to this object - """ - if not (isinstance(roi, roi_items.RegionOfInterest) and - roi.parent() is self and - roi in self._rois): - raise ValueError( - 'RegionOfInterest does not belong to this instance') - - roi.sigAboutToBeRemoved.emit() - self.sigRoiAboutToBeRemoved.emit(roi) - - if roi is self._currentRoi: - self.setCurrentRoi(None) - - mustRestart = False - if roi is self._drawnROI: - self._drawnROI = None - mustRestart = True - self._rois.remove(roi) - roi.sigRegionChanged.disconnect(self._regionOfInterestChanged) - roi.sigItemChanged.disconnect(self._regionOfInterestChanged) - roi.setParent(None) - self._roisUpdated() - - if mustRestart: - self._restart() - - def _roisUpdated(self): - """Handle update of the ROI list""" - self.sigRoiChanged.emit() - - # RegionOfInterest parameters - - def getColor(self): - """Return the default color of created ROIs - - :rtype: QColor - """ - return qt.QColor.fromRgbF(*self._color) - - def setColor(self, color): - """Set the default color to use when creating ROIs. - - Existing ROIs are not affected. - - :param color: The color to use for displaying ROIs as - either a color name, a QColor, a list of uint8 or float in [0, 1]. - """ - self._color = rgba(color) - - # Control ROI - - def getCurrentInteractionModeRoiClass(self): - """Returns the current ROI class used by the interactive drawing mode. - - Returns None if the ROI manager is not in an interactive mode. - - :rtype: Union[class,None] - """ - return self._roiClass - - def getInteractionSource(self): - """Returns the object which have requested the ROI creation. - - Returns None if the ROI manager is not in an interactive mode. - - :rtype: Union[object,None] - """ - return self._source - - def isStarted(self): - """Returns True if an interactive ROI drawing mode is active. - - :rtype: bool - """ - return self._roiClass is not None - - def isDrawing(self): - """Returns True if an interactive ROI is drawing. - - :rtype: bool - """ - return self._drawnROI is not None - - def start(self, roiClass, source=None): - """Start an interactive ROI drawing mode. - - :param class roiClass: The ROI class to create. It have to inherite from - `roi_items.RegionOfInterest`. - :param object source: SOurce of the ROI interaction. - :return: True if interactive ROI drawing was started, False otherwise - :rtype: bool - :raise ValueError: If roiClass is not supported - """ - self.stop() - - if not issubclass(roiClass, roi_items.RegionOfInterest): - raise ValueError('Unsupported ROI class %s' % roiClass) - - plot = self.parent() - if plot is None: - return False - - self._roiClass = roiClass - self._source = source - - self._restart() - - plot.sigPlotSignal.connect(self._handleInteraction) - - self.sigInteractiveModeStarted.emit(roiClass) - - return True - - def _restart(self): - """Restart the plot interaction without changing the - source or the ROI class. - """ - roiClass = self._roiClass - plot = self.parent() - firstInteractionShapeKind = roiClass.getFirstInteractionShape() - - if firstInteractionShapeKind == 'point': - plot.setInteractiveMode(mode='select', source=self) - else: - if roiClass.showFirstInteractionShape(): - color = rgba(self.getColor()) - else: - color = None - plot.setInteractiveMode(mode='select-draw', - source=self, - shape=firstInteractionShapeKind, - color=color, - label=self._label) - - def __roiInteractiveModeEnded(self): - """Handle end of ROI draw interactive mode""" - if self.isStarted(): - self._roiClass = None - self._source = None - - if self._drawnROI is not None: - # Cancel ROI create - roi = self._drawnROI - self._drawnROI = None - self.removeRoi(roi) - - plot = self.parent() - if plot is not None: - plot.sigPlotSignal.disconnect(self._handleInteraction) - - self.sigInteractiveModeFinished.emit() - - def stop(self): - """Stop interactive ROI drawing mode. - - :return: True if an interactive ROI drawing mode was actually stopped - :rtype: bool - """ - if not self.isStarted(): - return False - - plot = self.parent() - if plot is not None: - # This leads to call __roiInteractiveModeEnded through - # interactive mode changed signal - plot.resetInteractiveMode() - else: # Fallback - self.__roiInteractiveModeEnded() - - return True - - def exec_(self, roiClass): - """Block until :meth:`quit` is called. - - :param class kind: The class of the ROI which have to be created. - See `silx.gui.plot.items.roi`. - :return: The list of ROIs - :rtype: tuple - """ - self.start(roiClass) - - plot = self.parent() - plot.show() - plot.raise_() - - self._eventLoop = qt.QEventLoop() - self._eventLoop.exec_() - self._eventLoop = None - - self.stop() - - rois = self.getRois() - self.clear() - return rois - - def quit(self): - """Stop a blocking :meth:`exec_` and call :meth:`stop`""" - if self._eventLoop is not None: - self._eventLoop.quit() - self._eventLoop = None - self.stop() - - -class InteractiveRegionOfInterestManager(RegionOfInterestManager): - """RegionOfInterestManager with features for use from interpreter. - - It is meant to be used through the :meth:`exec_`. - It provides some messages to display in a status bar and - different modes to end blocking calls to :meth:`exec_`. - - :param parent: See QObject - """ - - sigMessageChanged = qt.Signal(str) - """Signal emitted when a new message should be displayed to the user - - It provides the message as a str. - """ - - def __init__(self, parent): - super(InteractiveRegionOfInterestManager, self).__init__(parent) - self._maxROI = None - self.__timeoutEndTime = None - self.__message = '' - self.__validationMode = self.ValidationMode.ENTER - self.__execClass = None - - self.sigRoiAdded.connect(self.__added) - self.sigRoiAboutToBeRemoved.connect(self.__aboutToBeRemoved) - self.sigInteractiveModeStarted.connect(self.__started) - self.sigInteractiveModeFinished.connect(self.__finished) - - # Max ROI - - def getMaxRois(self): - """Returns the maximum number of ROIs or None if no limit. - - :rtype: Union[int,None] - """ - return self._maxROI - - def setMaxRois(self, max_): - """Set the maximum number of ROIs. - - :param Union[int,None] max_: The max limit or None for no limit. - :raise ValueError: If there is more ROIs than max value - """ - if max_ is not None: - max_ = int(max_) - if max_ <= 0: - raise ValueError('Max limit must be strictly positive') - - if len(self.getRois()) > max_: - raise ValueError( - 'Cannot set max limit: Already too many ROIs') - - self._maxROI = max_ - - def isMaxRois(self): - """Returns True if the maximum number of ROIs is reached. - - :rtype: bool - """ - max_ = self.getMaxRois() - return max_ is not None and len(self.getRois()) >= max_ - - # Validation mode - - @enum.unique - class ValidationMode(enum.Enum): - """Mode of validation to leave blocking :meth:`exec_`""" - - AUTO = 'auto' - """Automatically ends the interactive mode once - the user terminates the last ROI shape.""" - - ENTER = 'enter' - """Ends the interactive mode when the *Enter* key is pressed.""" - - AUTO_ENTER = 'auto_enter' - """Ends the interactive mode when reaching max ROIs or - when the *Enter* key is pressed. - """ - - NONE = 'none' - """Do not provide the user a way to end the interactive mode. - - The end of :meth:`exec_` is done through :meth:`quit` or timeout. - """ - - def getValidationMode(self): - """Returns the interactive mode validation in use. - - :rtype: ValidationMode - """ - return self.__validationMode - - def setValidationMode(self, mode): - """Set the way to perform interactive mode validation. - - See :class:`ValidationMode` enumeration for the supported - validation modes. - - :param ValidationMode mode: The interactive mode validation to use. - """ - assert isinstance(mode, self.ValidationMode) - if mode != self.__validationMode: - self.__validationMode = mode - - if self.isExec(): - if (self.isMaxRois() and self.getValidationMode() in - (self.ValidationMode.AUTO, - self.ValidationMode.AUTO_ENTER)): - self.quit() - - self.__updateMessage() - - def eventFilter(self, obj, event): - if event.type() == qt.QEvent.Hide: - self.quit() - - if event.type() == qt.QEvent.KeyPress: - key = event.key() - if (key in (qt.Qt.Key_Return, qt.Qt.Key_Enter) and - self.getValidationMode() in ( - self.ValidationMode.ENTER, - self.ValidationMode.AUTO_ENTER)): - # Stop on return key pressed - self.quit() - return True # Stop further handling of this keys - - if (key in (qt.Qt.Key_Delete, qt.Qt.Key_Backspace) or ( - key == qt.Qt.Key_Z and - event.modifiers() & qt.Qt.ControlModifier)): - rois = self.getRois() - if rois: # Something to undo - self.removeRoi(rois[-1]) - # Stop further handling of keys if something was undone - return True - - return super(InteractiveRegionOfInterestManager, self).eventFilter(obj, event) - - # Message API - - def getMessage(self): - """Returns the current status message. - - This message is meant to be displayed in a status bar. - - :rtype: str - """ - if self.__timeoutEndTime is None: - return self.__message - else: - remaining = self.__timeoutEndTime - time.time() - return self.__message + (' - %d seconds remaining' % - max(1, int(remaining))) - - # Listen to ROI updates - - def __added(self, *args, **kwargs): - """Handle new ROI added""" - max_ = self.getMaxRois() - if max_ is not None: - # When reaching max number of ROIs, redo last one - while len(self.getRois()) > max_: - self.removeRoi(self.getRois()[-2]) - - self.__updateMessage() - if (self.isMaxRois() and - self.getValidationMode() in (self.ValidationMode.AUTO, - self.ValidationMode.AUTO_ENTER)): - self.quit() - - def __aboutToBeRemoved(self, *args, **kwargs): - """Handle removal of a ROI""" - # RegionOfInterest not removed yet - self.__updateMessage(nbrois=len(self.getRois()) - 1) - - def __started(self, roiKind): - """Handle interactive mode started""" - self.__updateMessage() - - def __finished(self): - """Handle interactive mode finished""" - self.__updateMessage() - - def __updateMessage(self, nbrois=None): - """Update message""" - if not self.isExec(): - message = 'Done' - - elif not self.isStarted(): - message = 'Use %s ROI edition mode' % self.__execClass - - else: - if nbrois is None: - nbrois = len(self.getRois()) - - name = self.__execClass._getShortName() - - max_ = self.getMaxRois() - if max_ is None: - message = 'Select %ss (%d selected)' % (name, nbrois) - - elif max_ <= 1: - message = 'Select a %s' % name - else: - message = 'Select %d/%d %ss' % (nbrois, max_, name) - - if (self.getValidationMode() == self.ValidationMode.ENTER and - self.isMaxRois()): - message += ' - Press Enter to confirm' - - if message != self.__message: - self.__message = message - # Use getMessage to add timeout message - self.sigMessageChanged.emit(self.getMessage()) - - # Handle blocking call - - def __timeoutUpdate(self): - """Handle update of timeout""" - if (self.__timeoutEndTime is not None and - (self.__timeoutEndTime - time.time()) > 0): - self.sigMessageChanged.emit(self.getMessage()) - else: # Stop interactive mode and message timer - timer = self.sender() - if timer is not None: - timer.stop() - self.__timeoutEndTime = None - self.quit() - - def isExec(self): - """Returns True if :meth:`exec_` is currently running. - - :rtype: bool""" - return self.__execClass is not None - - def exec_(self, roiClass, timeout=0): - """Block until ROI selection is done or timeout is elapsed. - - :meth:`quit` also ends this blocking call. - - :param class roiClass: The class of the ROI which have to be created. - See `silx.gui.plot.items.roi`. - :param int timeout: Maximum duration in seconds to block. - Default: No timeout - :return: The list of ROIs - :rtype: List[RegionOfInterest] - """ - plot = self.parent() - if plot is None: - return - - self.__execClass = roiClass - - plot.installEventFilter(self) - - if timeout > 0: - self.__timeoutEndTime = time.time() + timeout - timer = qt.QTimer(self) - timer.timeout.connect(self.__timeoutUpdate) - timer.start(1000) - - rois = super(InteractiveRegionOfInterestManager, self).exec_(roiClass) - - timer.stop() - self.__timeoutEndTime = None - - else: - rois = super(InteractiveRegionOfInterestManager, self).exec_(roiClass) - - plot.removeEventFilter(self) - - self.__execClass = None - self.__updateMessage() - - return rois - - -class _DeleteRegionOfInterestToolButton(qt.QToolButton): - """Tool button deleting a ROI object - - :param parent: See QWidget - :param RegionOfInterest roi: The ROI to delete - """ - - def __init__(self, parent, roi): - super(_DeleteRegionOfInterestToolButton, self).__init__(parent) - self.setIcon(icons.getQIcon('remove')) - self.setToolTip("Remove this ROI") - self.__roiRef = roi if roi is None else weakref.ref(roi) - self.clicked.connect(self.__clicked) - - def __clicked(self, checked): - """Handle button clicked""" - roi = None if self.__roiRef is None else self.__roiRef() - if roi is not None: - manager = roi.parent() - if manager is not None: - manager.removeRoi(roi) - self.__roiRef = None - - -class RegionOfInterestTableWidget(qt.QTableWidget): - """Widget displaying the ROIs of a :class:`RegionOfInterestManager`""" - - def __init__(self, parent=None): - super(RegionOfInterestTableWidget, self).__init__(parent) - self._roiManagerRef = None - - headers = ['Label', 'Edit', 'Kind', 'Coordinates', ''] - self.setColumnCount(len(headers)) - self.setHorizontalHeaderLabels(headers) - - horizontalHeader = self.horizontalHeader() - horizontalHeader.setDefaultAlignment(qt.Qt.AlignLeft) - if hasattr(horizontalHeader, 'setResizeMode'): # Qt 4 - setSectionResizeMode = horizontalHeader.setResizeMode - else: # Qt5 - setSectionResizeMode = horizontalHeader.setSectionResizeMode - - setSectionResizeMode(0, qt.QHeaderView.Interactive) - setSectionResizeMode(1, qt.QHeaderView.ResizeToContents) - setSectionResizeMode(2, qt.QHeaderView.ResizeToContents) - setSectionResizeMode(3, qt.QHeaderView.Stretch) - setSectionResizeMode(4, qt.QHeaderView.ResizeToContents) - - verticalHeader = self.verticalHeader() - verticalHeader.setVisible(False) - - self.setSelectionMode(qt.QAbstractItemView.NoSelection) - self.setFocusPolicy(qt.Qt.NoFocus) - - self.itemChanged.connect(self.__itemChanged) - - def __itemChanged(self, item): - """Handle item updates""" - column = item.column() - index = item.data(qt.Qt.UserRole) - - if index is not None: - manager = self.getRegionOfInterestManager() - roi = manager.getRois()[index] - else: - return - - if column == 0: - # First collect information from item, then update ROI - # Otherwise, this causes issues issues - checked = item.checkState() == qt.Qt.Checked - text= item.text() - roi.setVisible(checked) - roi.setName(text) - elif column == 1: - roi.setEditable(item.checkState() == qt.Qt.Checked) - elif column in (2, 3, 4): - pass # TODO - else: - logger.error('Unhandled column %d', column) - - def setRegionOfInterestManager(self, manager): - """Set the :class:`RegionOfInterestManager` object to sync with - - :param RegionOfInterestManager manager: - """ - assert manager is None or isinstance(manager, RegionOfInterestManager) - - previousManager = self.getRegionOfInterestManager() - - if previousManager is not None: - previousManager.sigRoiChanged.disconnect(self._sync) - self.setRowCount(0) - - self._roiManagerRef = weakref.ref(manager) - - self._sync() - - if manager is not None: - manager.sigRoiChanged.connect(self._sync) - - def _getReadableRoiDescription(self, roi): - """Returns modelisation of a ROI as a readable sequence of values. - - :rtype: str - """ - text = str(roi) - try: - # Extract the params from syntax "CLASSNAME(PARAMS)" - elements = text.split("(", 1) - if len(elements) != 2: - return text - result = elements[1] - result = result.strip() - if not result.endswith(")"): - return text - result = result[0:-1] - # Capitalize each words - result = result.title() - return result - except Exception: - logger.debug("Backtrace", exc_info=True) - return text - - def _sync(self): - """Update widget content according to ROI manger""" - manager = self.getRegionOfInterestManager() - - if manager is None: - self.setRowCount(0) - return - - rois = manager.getRois() - - self.setRowCount(len(rois)) - for index, roi in enumerate(rois): - baseFlags = qt.Qt.ItemIsSelectable | qt.Qt.ItemIsEnabled - - # Label and visible - label = roi.getName() - item = qt.QTableWidgetItem(label) - item.setFlags(baseFlags | qt.Qt.ItemIsEditable | qt.Qt.ItemIsUserCheckable) - item.setData(qt.Qt.UserRole, index) - item.setCheckState( - qt.Qt.Checked if roi.isVisible() else qt.Qt.Unchecked) - self.setItem(index, 0, item) - - # Editable - item = qt.QTableWidgetItem() - item.setFlags(baseFlags | qt.Qt.ItemIsUserCheckable) - item.setData(qt.Qt.UserRole, index) - item.setCheckState( - qt.Qt.Checked if roi.isEditable() else qt.Qt.Unchecked) - self.setItem(index, 1, item) - item.setTextAlignment(qt.Qt.AlignCenter) - item.setText(None) - - # Kind - label = roi._getShortName() - if label is None: - # Default value if kind is not overrided - label = roi.__class__.__name__ - item = qt.QTableWidgetItem(label.capitalize()) - item.setFlags(baseFlags) - self.setItem(index, 2, item) - - item = qt.QTableWidgetItem() - item.setFlags(baseFlags) - - # Coordinates - text = self._getReadableRoiDescription(roi) - item.setText(text) - self.setItem(index, 3, item) - - # Delete - delBtn = _DeleteRegionOfInterestToolButton(None, roi) - widget = qt.QWidget(self) - layout = qt.QHBoxLayout() - layout.setContentsMargins(2, 2, 2, 2) - layout.setSpacing(0) - widget.setLayout(layout) - layout.addStretch(1) - layout.addWidget(delBtn) - layout.addStretch(1) - self.setCellWidget(index, 4, widget) - - def getRegionOfInterestManager(self): - """Returns the :class:`RegionOfInterestManager` this widget supervise. - - It returns None if not sync with an :class:`RegionOfInterestManager`. - - :rtype: RegionOfInterestManager - """ - return None if self._roiManagerRef is None else self._roiManagerRef() |