예제 #1
0
class _BoundaryWidget(qt.QWidget):
    """Widget to edit a boundary of the colormap (vmin, vmax)"""
    sigValueChanged = qt.Signal(object)
    """Signal emitted when value is changed"""
    def __init__(self, parent=None, value=0.0):
        qt.QWidget.__init__(self, parent=None)
        self.setLayout(qt.QHBoxLayout())
        self.layout().setContentsMargins(0, 0, 0, 0)
        self._numVal = FloatEdit(parent=self, value=value)
        self.layout().addWidget(self._numVal)
        self._autoCB = qt.QCheckBox('auto', parent=self)
        self.layout().addWidget(self._autoCB)
        self._autoCB.setChecked(False)

        self._autoCB.toggled.connect(self._autoToggled)
        self.sigValueChanged = self._autoCB.toggled
        self.textEdited = self._numVal.textEdited
        self.editingFinished = self._numVal.editingFinished
        self._dataValue = None

    def isAutoChecked(self):
        return self._autoCB.isChecked()

    def getValue(self):
        return None if self._autoCB.isChecked() else self._numVal.value()

    def getFiniteValue(self):
        if not self._autoCB.isChecked():
            return self._numVal.value()
        elif self._dataValue is None:
            return self._numVal.value()
        else:
            return self._dataValue

    def _autoToggled(self, enabled):
        self._numVal.setEnabled(not enabled)
        self._updateDisplayedText()

    def _updateDisplayedText(self):
        # if dataValue is finite
        if self._autoCB.isChecked() and self._dataValue is not None:
            old = self._numVal.blockSignals(True)
            self._numVal.setValue(self._dataValue)
            self._numVal.blockSignals(old)

    def setDataValue(self, dataValue):
        self._dataValue = dataValue
        self._updateDisplayedText()

    def setFiniteValue(self, value):
        assert (value is not None)
        old = self._numVal.blockSignals(True)
        self._numVal.setValue(value)
        self._numVal.blockSignals(old)

    def setValue(self, value, isAuto=False):
        self._autoCB.setChecked(isAuto or value is None)
        if value is not None:
            self._numVal.setValue(value)
        self._updateDisplayedText()
class BaseMaskToolsWidget(qt.QWidget):
    """Base class for :class:`MaskToolsWidget` (image mask) and
    :class:`scatterMaskToolsWidget`"""

    sigMaskChanged = qt.Signal()
    _maxLevelNumber = 255

    def __init__(self, parent=None, plot=None, mask=None):
        """

        :param parent: Parent QWidget
        :param plot: Plot widget on which to operate
        :param mask: Instance of subclass of :class:`BaseMask`
            (e.g. :class:`ImageMask`)
        """
        super(BaseMaskToolsWidget, self).__init__(parent)
        # register if the user as force a color for the corresponding mask level
        self._defaultColors = numpy.ones((self._maxLevelNumber + 1),
                                         dtype=numpy.bool)
        # overlays colors set by the user
        self._overlayColors = numpy.zeros((self._maxLevelNumber + 1, 3),
                                          dtype=numpy.float32)

        # as parent have to be the first argument of the widget to fit
        # QtDesigner need but here plot can't be None by default.
        assert plot is not None
        self._plotRef = weakref.ref(plot)
        self._maskName = '__MASK_TOOLS_%d' % id(self)  # Legend of the mask

        self._colormap = Colormap(normalization='linear',
                                  vmin=0,
                                  vmax=self._maxLevelNumber)
        self._defaultOverlayColor = rgba('gray')  # Color of the mask
        self._setMaskColors(1, 0.5)  # Set the colormap LUT

        if not isinstance(mask, BaseMask):
            raise TypeError("mask is not an instance of BaseMask")
        self._mask = mask

        self._mask.sigChanged.connect(self._updatePlotMask)
        self._mask.sigChanged.connect(self._emitSigMaskChanged)

        self._drawingMode = None  # Store current drawing mode
        self._lastPencilPos = None
        self._multipleMasks = 'exclusive'

        self._maskFileDir = qt.QDir.home().absolutePath()
        self.plot.sigInteractiveModeChanged.connect(
            self._interactiveModeChanged)

        self._initWidgets()

    def _emitSigMaskChanged(self):
        """Notify mask changes"""
        self.sigMaskChanged.emit()

    def getSelectionMask(self, copy=True):
        """Get the current mask as a numpy array.

        :param bool copy: True (default) to get a copy of the mask.
                          If False, the returned array MUST not be modified.
        :return: The mask (as an array of uint8) with dimension of
                 the 'active' plot item.
                 If there is no active image or scatter, it returns None.
        :rtype: Union[numpy.ndarray,None]
        """
        mask = self._mask.getMask(copy=copy)
        return None if mask.size == 0 else mask

    def setSelectionMask(self, mask):
        """Set the mask: Must be implemented in subclass"""
        raise NotImplementedError()

    def resetSelectionMask(self):
        """Reset the mask: Must be implemented in subclass"""
        raise NotImplementedError()

    def multipleMasks(self):
        """Return the current mode of multiple masks support.

        See :meth:`setMultipleMasks`
        """
        return self._multipleMasks

    def setMultipleMasks(self, mode):
        """Set the mode of multiple masks support.

        Available modes:

        - 'single': Edit a single level of mask
        - 'exclusive': Supports to 256 levels of non overlapping masks

        :param str mode: The mode to use
        """
        assert mode in ('exclusive', 'single')
        if mode != self._multipleMasks:
            self._multipleMasks = mode
            self.levelWidget.setVisible(self._multipleMasks != 'single')
            self.clearAllBtn.setVisible(self._multipleMasks != 'single')

    @property
    def maskFileDir(self):
        """The directory from which to load/save mask from/to files."""
        if not os.path.isdir(self._maskFileDir):
            self._maskFileDir = qt.QDir.home().absolutePath()
        return self._maskFileDir

    @maskFileDir.setter
    def maskFileDir(self, maskFileDir):
        self._maskFileDir = str(maskFileDir)

    @property
    def plot(self):
        """The :class:`.PlotWindow` this widget is attached to."""
        plot = self._plotRef()
        if plot is None:
            raise RuntimeError(
                'Mask widget attached to a PlotWidget that no longer exists')
        return plot

    def setDirection(self, direction=qt.QBoxLayout.LeftToRight):
        """Set the direction of the layout of the widget

        :param direction: QBoxLayout direction
        """
        self.layout().setDirection(direction)

    def _initWidgets(self):
        """Create widgets"""
        layout = qt.QBoxLayout(qt.QBoxLayout.LeftToRight)
        layout.addWidget(self._initMaskGroupBox())
        layout.addWidget(self._initDrawGroupBox())
        layout.addWidget(self._initThresholdGroupBox())
        layout.addWidget(self._initOtherToolsGroupBox())
        layout.addStretch(1)
        self.setLayout(layout)

    @staticmethod
    def _hboxWidget(*widgets, **kwargs):
        """Place widgets in widget with horizontal layout

        :param widgets: Widgets to position horizontally
        :param bool stretch: True for trailing stretch (default),
                             False for no trailing stretch
        :return: A QWidget with a QHBoxLayout
        """
        stretch = kwargs.get('stretch', True)

        layout = qt.QHBoxLayout()
        layout.setContentsMargins(0, 0, 0, 0)
        for widget in widgets:
            layout.addWidget(widget)
        if stretch:
            layout.addStretch(1)
        widget = qt.QWidget()
        widget.setLayout(layout)
        return widget

    def _initTransparencyWidget(self):
        """ Init the mask transparency widget """
        transparencyWidget = qt.QWidget(self)
        grid = qt.QGridLayout()
        grid.setContentsMargins(0, 0, 0, 0)
        self.transparencySlider = qt.QSlider(qt.Qt.Horizontal,
                                             parent=transparencyWidget)
        self.transparencySlider.setRange(3, 10)
        self.transparencySlider.setValue(8)
        self.transparencySlider.setToolTip(
            'Set the transparency of the mask display')
        self.transparencySlider.valueChanged.connect(self._updateColors)
        grid.addWidget(qt.QLabel('Display:', parent=transparencyWidget), 0, 0)
        grid.addWidget(self.transparencySlider, 0, 1, 1, 3)
        grid.addWidget(
            qt.QLabel('<small><b>Transparent</b></small>',
                      parent=transparencyWidget), 1, 1)
        grid.addWidget(
            qt.QLabel('<small><b>Opaque</b></small>',
                      parent=transparencyWidget), 1, 3)
        transparencyWidget.setLayout(grid)
        return transparencyWidget

    def _initMaskGroupBox(self):
        """Init general mask operation widgets"""

        # Mask level
        self.levelSpinBox = qt.QSpinBox()
        self.levelSpinBox.setRange(1, self._maxLevelNumber)
        self.levelSpinBox.setToolTip(
            'Choose which mask level is edited.\n'
            'A mask can have up to 255 non-overlapping levels.')
        self.levelSpinBox.valueChanged[int].connect(self._updateColors)
        self.levelWidget = self._hboxWidget(qt.QLabel('Mask level:'),
                                            self.levelSpinBox)
        # Transparency
        self.transparencyWidget = self._initTransparencyWidget()

        # Buttons group
        invertBtn = qt.QPushButton('Invert')
        invertBtn.setShortcut(qt.Qt.CTRL + qt.Qt.Key_I)
        invertBtn.setToolTip('Invert current mask <b>%s</b>' %
                             invertBtn.shortcut().toString())
        invertBtn.clicked.connect(self._handleInvertMask)

        clearBtn = qt.QPushButton('Clear')
        clearBtn.setShortcut(qt.QKeySequence.Delete)
        clearBtn.setToolTip('Clear current mask level <b>%s</b>' %
                            clearBtn.shortcut().toString())
        clearBtn.clicked.connect(self._handleClearMask)

        invertClearWidget = self._hboxWidget(invertBtn,
                                             clearBtn,
                                             stretch=False)

        undoBtn = qt.QPushButton('Undo')
        undoBtn.setShortcut(qt.QKeySequence.Undo)
        undoBtn.setToolTip('Undo last mask change <b>%s</b>' %
                           undoBtn.shortcut().toString())
        self._mask.sigUndoable.connect(undoBtn.setEnabled)
        undoBtn.clicked.connect(self._mask.undo)

        redoBtn = qt.QPushButton('Redo')
        redoBtn.setShortcut(qt.QKeySequence.Redo)
        redoBtn.setToolTip('Redo last undone mask change <b>%s</b>' %
                           redoBtn.shortcut().toString())
        self._mask.sigRedoable.connect(redoBtn.setEnabled)
        redoBtn.clicked.connect(self._mask.redo)

        undoRedoWidget = self._hboxWidget(undoBtn, redoBtn, stretch=False)

        self.clearAllBtn = qt.QPushButton('Clear all')
        self.clearAllBtn.setToolTip('Clear all mask levels')
        self.clearAllBtn.clicked.connect(self.resetSelectionMask)

        loadBtn = qt.QPushButton('Load...')
        loadBtn.clicked.connect(self._loadMask)

        saveBtn = qt.QPushButton('Save...')
        saveBtn.clicked.connect(self._saveMask)

        self.loadSaveWidget = self._hboxWidget(loadBtn, saveBtn, stretch=False)

        layout = qt.QVBoxLayout()
        layout.addWidget(self.levelWidget)
        layout.addWidget(self.transparencyWidget)
        layout.addWidget(invertClearWidget)
        layout.addWidget(undoRedoWidget)
        layout.addWidget(self.clearAllBtn)
        layout.addWidget(self.loadSaveWidget)
        layout.addStretch(1)

        maskGroup = qt.QGroupBox('Mask')
        maskGroup.setLayout(layout)
        return maskGroup

    def isMaskInteractionActivated(self):
        """Returns true if any mask interaction is activated"""
        return self.drawActionGroup.checkedAction() is not None

    def _initDrawGroupBox(self):
        """Init drawing tools widgets"""
        layout = qt.QVBoxLayout()

        self.browseAction = PanModeAction(self.plot, self.plot)
        self.addAction(self.browseAction)

        # Draw tools
        self.rectAction = qt.QAction(icons.getQIcon('shape-rectangle'),
                                     'Rectangle selection', None)
        self.rectAction.setToolTip(
            'Rectangle selection tool: (Un)Mask a rectangular region <b>R</b>')
        self.rectAction.setShortcut(qt.QKeySequence(qt.Qt.Key_R))
        self.rectAction.setCheckable(True)
        self.rectAction.triggered.connect(self._activeRectMode)
        self.addAction(self.rectAction)

        self.ellipseAction = qt.QAction(icons.getQIcon('shape-ellipse'),
                                        'Circle selection', None)
        self.ellipseAction.setToolTip(
            'Rectangle selection tool: (Un)Mask a circle region <b>R</b>')
        self.ellipseAction.setShortcut(qt.QKeySequence(qt.Qt.Key_R))
        self.ellipseAction.setCheckable(True)
        self.ellipseAction.triggered.connect(self._activeEllipseMode)
        self.addAction(self.ellipseAction)

        self.polygonAction = qt.QAction(icons.getQIcon('shape-polygon'),
                                        'Polygon selection', None)
        self.polygonAction.setShortcut(qt.QKeySequence(qt.Qt.Key_S))
        self.polygonAction.setToolTip(
            'Polygon selection tool: (Un)Mask a polygonal region <b>S</b><br>'
            'Left-click to place new polygon corners<br>'
            'Left-click on first corner to close the polygon')
        self.polygonAction.setCheckable(True)
        self.polygonAction.triggered.connect(self._activePolygonMode)
        self.addAction(self.polygonAction)

        self.pencilAction = qt.QAction(icons.getQIcon('draw-pencil'),
                                       'Pencil tool', None)
        self.pencilAction.setShortcut(qt.QKeySequence(qt.Qt.Key_P))
        self.pencilAction.setToolTip(
            'Pencil tool: (Un)Mask using a pencil <b>P</b>')
        self.pencilAction.setCheckable(True)
        self.pencilAction.triggered.connect(self._activePencilMode)
        self.addAction(self.pencilAction)

        self.drawActionGroup = qt.QActionGroup(self)
        self.drawActionGroup.setExclusive(True)
        self.drawActionGroup.addAction(self.rectAction)
        self.drawActionGroup.addAction(self.ellipseAction)
        self.drawActionGroup.addAction(self.polygonAction)
        self.drawActionGroup.addAction(self.pencilAction)

        actions = (self.browseAction, self.rectAction, self.ellipseAction,
                   self.polygonAction, self.pencilAction)
        drawButtons = []
        for action in actions:
            btn = qt.QToolButton()
            btn.setDefaultAction(action)
            drawButtons.append(btn)
        container = self._hboxWidget(*drawButtons)
        layout.addWidget(container)

        # Mask/Unmask radio buttons
        maskRadioBtn = qt.QRadioButton('Mask')
        maskRadioBtn.setToolTip(
            'Drawing masks with current level. Press <b>Ctrl</b> to unmask')
        maskRadioBtn.setChecked(True)

        unmaskRadioBtn = qt.QRadioButton('Unmask')
        unmaskRadioBtn.setToolTip(
            'Drawing unmasks with current level. Press <b>Ctrl</b> to mask')

        self.maskStateGroup = qt.QButtonGroup()
        self.maskStateGroup.addButton(maskRadioBtn, 1)
        self.maskStateGroup.addButton(unmaskRadioBtn, 0)

        self.maskStateWidget = self._hboxWidget(maskRadioBtn, unmaskRadioBtn)
        layout.addWidget(self.maskStateWidget)

        self.maskStateWidget.setHidden(True)

        # Pencil settings
        self.pencilSetting = self._createPencilSettings(None)
        self.pencilSetting.setVisible(False)
        layout.addWidget(self.pencilSetting)

        layout.addStretch(1)

        drawGroup = qt.QGroupBox('Draw tools')
        drawGroup.setLayout(layout)
        return drawGroup

    def _createPencilSettings(self, parent=None):
        pencilSetting = qt.QWidget(parent)

        self.pencilSpinBox = qt.QSpinBox(parent=pencilSetting)
        self.pencilSpinBox.setRange(1, 1024)
        pencilToolTip = """Set pencil drawing tool size in pixels of the image
            on which to make the mask."""
        self.pencilSpinBox.setToolTip(pencilToolTip)

        self.pencilSlider = qt.QSlider(qt.Qt.Horizontal, parent=pencilSetting)
        self.pencilSlider.setRange(1, 50)
        self.pencilSlider.setToolTip(pencilToolTip)

        pencilLabel = qt.QLabel('Pencil size:', parent=pencilSetting)

        layout = qt.QGridLayout()
        layout.addWidget(pencilLabel, 0, 0)
        layout.addWidget(self.pencilSpinBox, 0, 1)
        layout.addWidget(self.pencilSlider, 1, 1)
        pencilSetting.setLayout(layout)

        self.pencilSpinBox.valueChanged.connect(self._pencilWidthChanged)
        self.pencilSlider.valueChanged.connect(self._pencilWidthChanged)

        return pencilSetting

    def _initThresholdGroupBox(self):
        """Init thresholding widgets"""

        self.belowThresholdAction = qt.QAction(
            icons.getQIcon('plot-roi-below'), 'Mask below threshold', None)
        self.belowThresholdAction.setToolTip(
            'Mask image where values are below given threshold')
        self.belowThresholdAction.setCheckable(True)
        self.belowThresholdAction.setChecked(True)

        self.betweenThresholdAction = qt.QAction(
            icons.getQIcon('plot-roi-between'), 'Mask within range', None)
        self.betweenThresholdAction.setToolTip(
            'Mask image where values are within given range')
        self.betweenThresholdAction.setCheckable(True)

        self.aboveThresholdAction = qt.QAction(
            icons.getQIcon('plot-roi-above'), 'Mask above threshold', None)
        self.aboveThresholdAction.setToolTip(
            'Mask image where values are above given threshold')
        self.aboveThresholdAction.setCheckable(True)

        self.thresholdActionGroup = qt.QActionGroup(self)
        self.thresholdActionGroup.setExclusive(True)
        self.thresholdActionGroup.addAction(self.belowThresholdAction)
        self.thresholdActionGroup.addAction(self.betweenThresholdAction)
        self.thresholdActionGroup.addAction(self.aboveThresholdAction)
        self.thresholdActionGroup.triggered.connect(
            self._thresholdActionGroupTriggered)

        self.loadColormapRangeAction = qt.QAction(
            icons.getQIcon('view-refresh'), 'Set min-max from colormap', None)
        self.loadColormapRangeAction.setToolTip(
            'Set min and max values from current colormap range')
        self.loadColormapRangeAction.setCheckable(False)
        self.loadColormapRangeAction.triggered.connect(
            self._loadRangeFromColormapTriggered)

        widgets = []
        for action in self.thresholdActionGroup.actions():
            btn = qt.QToolButton()
            btn.setDefaultAction(action)
            widgets.append(btn)

        spacer = qt.QWidget()
        spacer.setSizePolicy(qt.QSizePolicy.Expanding,
                             qt.QSizePolicy.Preferred)
        widgets.append(spacer)

        loadColormapRangeBtn = qt.QToolButton()
        loadColormapRangeBtn.setDefaultAction(self.loadColormapRangeAction)
        widgets.append(loadColormapRangeBtn)

        toolBar = self._hboxWidget(*widgets, stretch=False)

        config = qt.QGridLayout()
        config.setContentsMargins(0, 0, 0, 0)

        self.minLineLabel = qt.QLabel("Min:", self)
        self.minLineEdit = FloatEdit(self, value=0)
        config.addWidget(self.minLineLabel, 0, 0)
        config.addWidget(self.minLineEdit, 0, 1)

        self.maxLineLabel = qt.QLabel("Max:", self)
        self.maxLineEdit = FloatEdit(self, value=0)
        config.addWidget(self.maxLineLabel, 1, 0)
        config.addWidget(self.maxLineEdit, 1, 1)

        self.applyMaskBtn = qt.QPushButton('Apply mask')
        self.applyMaskBtn.clicked.connect(self._maskBtnClicked)

        layout = qt.QVBoxLayout()
        layout.addWidget(toolBar)
        layout.addLayout(config)
        layout.addWidget(self.applyMaskBtn)

        self.thresholdGroup = qt.QGroupBox('Threshold')
        self.thresholdGroup.setLayout(layout)

        # Init widget state
        self._thresholdActionGroupTriggered(self.belowThresholdAction)
        return self.thresholdGroup

        # track widget visibility and plot active image changes

    def _initOtherToolsGroupBox(self):
        layout = qt.QVBoxLayout()

        self.maskNanBtn = qt.QPushButton('Mask not finite values')
        self.maskNanBtn.setToolTip('Mask Not a Number and infinite values')
        self.maskNanBtn.clicked.connect(self._maskNotFiniteBtnClicked)
        layout.addWidget(self.maskNanBtn)

        self.otherToolGroup = qt.QGroupBox('Other tools')
        self.otherToolGroup.setLayout(layout)
        return self.otherToolGroup

    def changeEvent(self, event):
        """Reset drawing action when disabling widget"""
        if (event.type() == qt.QEvent.EnabledChange and not self.isEnabled()
                and self.drawActionGroup.checkedAction()):
            # Disable drawing tool by setting interaction to zoom
            self.browseAction.trigger()

    def save(self, filename, kind):
        """Save current mask in a file

        :param str filename: The file where to save to mask
        :param str kind: The kind of file to save in 'edf', 'tif', 'npy'
        :raise Exception: Raised if the process fails
        """
        self._mask.save(filename, kind)

    def getCurrentMaskColor(self):
        """Returns the color of the current selected level.

        :rtype: A tuple or a python array
        """
        currentLevel = self.levelSpinBox.value()
        if self._defaultColors[currentLevel]:
            return self._defaultOverlayColor
        else:
            return self._overlayColors[currentLevel].tolist()

    def _setMaskColors(self, level, alpha):
        """Set-up the mask colormap to highlight current mask level.

        :param int level: The mask level to highlight
        :param float alpha: Alpha level of mask in [0., 1.]
        """
        assert 0 < level <= self._maxLevelNumber

        colors = numpy.empty((self._maxLevelNumber + 1, 4),
                             dtype=numpy.float32)

        # Set color
        colors[:, :3] = self._defaultOverlayColor[:3]

        # check if some colors has been directly set by the user
        mask = numpy.equal(self._defaultColors, False)
        colors[mask, :3] = self._overlayColors[mask, :3]

        # Set alpha
        colors[:, -1] = alpha / 2.

        # Set highlighted level color
        colors[level, 3] = alpha

        # Set no mask level
        colors[0] = (0., 0., 0., 0.)

        self._colormap.setColormapLUT(colors)

    def resetMaskColors(self, level=None):
        """Reset the mask color at the given level to be defaultColors

        :param level:
            The index of the mask for which we want to reset the color.
            If none we will reset color for all masks.
        """
        if level is None:
            self._defaultColors[level] = True
        else:
            self._defaultColors[:] = True

        self._updateColors()

    def setMaskColors(self, rgb, level=None):
        """Set the masks color

        :param rgb: The rgb color
        :param level:
            The index of the mask for which we want to change the color.
            If none set this color for all the masks
        """
        rgb = rgba(rgb)[0:3]
        if level is None:
            self._overlayColors[:] = rgb
            self._defaultColors[:] = False
        else:
            self._overlayColors[level] = rgb
            self._defaultColors[level] = False

        self._updateColors()

    def getMaskColors(self):
        """masks colors getter"""
        return self._overlayColors

    def _updateColors(self, *args):
        """Rebuild mask colormap when selected level or transparency change"""
        self._setMaskColors(
            self.levelSpinBox.value(),
            self.transparencySlider.value() /
            self.transparencySlider.maximum())
        self._updatePlotMask()
        self._updateInteractiveMode()

    def _pencilWidthChanged(self, width):

        old = self.pencilSpinBox.blockSignals(True)
        try:
            self.pencilSpinBox.setValue(width)
        finally:
            self.pencilSpinBox.blockSignals(old)

        old = self.pencilSlider.blockSignals(True)
        try:
            self.pencilSlider.setValue(width)
        finally:
            self.pencilSlider.blockSignals(old)
        self._updateInteractiveMode()

    def _updateInteractiveMode(self):
        """Update the current mode to the same if some cached data have to be
        updated. It is the case for the color for example.
        """
        if self._drawingMode == 'rectangle':
            self._activeRectMode()
        elif self._drawingMode == 'ellipse':
            self._activeEllipseMode()
        elif self._drawingMode == 'polygon':
            self._activePolygonMode()
        elif self._drawingMode == 'pencil':
            self._activePencilMode()

    def _handleClearMask(self):
        """Handle clear button clicked: reset current level mask"""
        self._mask.clear(self.levelSpinBox.value())
        self._mask.commit()

    def _handleInvertMask(self):
        """Invert the current mask level selection."""
        self._mask.invert(self.levelSpinBox.value())
        self._mask.commit()

    # Handle drawing tools UI events

    def _interactiveModeChanged(self, source):
        """Handle plot interactive mode changed:

        If changed from elsewhere, disable drawing tool
        """
        if source is not self:
            self.pencilAction.setChecked(False)
            self.rectAction.setChecked(False)
            self.polygonAction.setChecked(False)
            self._releaseDrawingMode()
            self._updateDrawingModeWidgets()

    def _releaseDrawingMode(self):
        """Release the drawing mode if is was used"""
        if self._drawingMode is None:
            return
        self.plot.sigPlotSignal.disconnect(self._plotDrawEvent)
        self._drawingMode = None

    def _activeRectMode(self):
        """Handle rect action mode triggering"""
        self._releaseDrawingMode()
        self._drawingMode = 'rectangle'
        self.plot.sigPlotSignal.connect(self._plotDrawEvent)
        color = self.getCurrentMaskColor()
        self.plot.setInteractiveMode('draw',
                                     shape='rectangle',
                                     source=self,
                                     color=color)
        self._updateDrawingModeWidgets()

    def _activeEllipseMode(self):
        """Handle circle action mode triggering"""
        self._releaseDrawingMode()
        self._drawingMode = 'ellipse'
        self.plot.sigPlotSignal.connect(self._plotDrawEvent)
        color = self.getCurrentMaskColor()
        self.plot.setInteractiveMode('draw',
                                     shape='ellipse',
                                     source=self,
                                     color=color)
        self._updateDrawingModeWidgets()

    def _activePolygonMode(self):
        """Handle polygon action mode triggering"""
        self._releaseDrawingMode()
        self._drawingMode = 'polygon'
        self.plot.sigPlotSignal.connect(self._plotDrawEvent)
        color = self.getCurrentMaskColor()
        self.plot.setInteractiveMode('draw',
                                     shape='polygon',
                                     source=self,
                                     color=color)
        self._updateDrawingModeWidgets()

    def _getPencilWidth(self):
        """Returns the width of the pencil to use in data coordinates`

        :rtype: float
        """
        return self.pencilSpinBox.value()

    def _activePencilMode(self):
        """Handle pencil action mode triggering"""
        self._releaseDrawingMode()
        self._drawingMode = 'pencil'
        self.plot.sigPlotSignal.connect(self._plotDrawEvent)
        color = self.getCurrentMaskColor()
        width = self._getPencilWidth()
        self.plot.setInteractiveMode('draw',
                                     shape='pencil',
                                     source=self,
                                     color=color,
                                     width=width)
        self._updateDrawingModeWidgets()

    def _updateDrawingModeWidgets(self):
        self.maskStateWidget.setVisible(self._drawingMode is not None)
        self.pencilSetting.setVisible(self._drawingMode == 'pencil')

    # Handle plot drawing events

    def _isMasking(self):
        """Returns true if the tool is used for masking, else it is used for
        unmasking.

        :rtype: bool"""
        # First draw event, use current modifiers for all draw sequence
        doMask = (self.maskStateGroup.checkedId() == 1)
        if qt.QApplication.keyboardModifiers() & qt.Qt.ControlModifier:
            doMask = not doMask
        return doMask

    # Handle threshold UI events

    def _thresholdActionGroupTriggered(self, triggeredAction):
        """Threshold action group listener."""
        if triggeredAction is self.belowThresholdAction:
            self.minLineLabel.setVisible(True)
            self.maxLineLabel.setVisible(False)
            self.minLineEdit.setVisible(True)
            self.maxLineEdit.setVisible(False)
            self.applyMaskBtn.setText("Mask bellow")
        elif triggeredAction is self.betweenThresholdAction:
            self.minLineLabel.setVisible(True)
            self.maxLineLabel.setVisible(True)
            self.minLineEdit.setVisible(True)
            self.maxLineEdit.setVisible(True)
            self.applyMaskBtn.setText("Mask between")
        elif triggeredAction is self.aboveThresholdAction:
            self.minLineLabel.setVisible(False)
            self.maxLineLabel.setVisible(True)
            self.minLineEdit.setVisible(False)
            self.maxLineEdit.setVisible(True)
            self.applyMaskBtn.setText("Mask above")
        self.applyMaskBtn.setToolTip(triggeredAction.toolTip())

    def _maskBtnClicked(self):
        if self.belowThresholdAction.isChecked():
            if self.minLineEdit.text():
                self._mask.updateBelowThreshold(self.levelSpinBox.value(),
                                                self.minLineEdit.value())
                self._mask.commit()

        elif self.betweenThresholdAction.isChecked():
            if self.minLineEdit.text() and self.maxLineEdit.text():
                min_ = self.minLineEdit.value()
                max_ = self.maxLineEdit.value()
                self._mask.updateBetweenThresholds(self.levelSpinBox.value(),
                                                   min_, max_)
                self._mask.commit()

        elif self.aboveThresholdAction.isChecked():
            if self.maxLineEdit.text():
                max_ = float(self.maxLineEdit.value())
                self._mask.updateAboveThreshold(self.levelSpinBox.value(),
                                                max_)
                self._mask.commit()

    def _maskNotFiniteBtnClicked(self):
        """Handle not finite mask button clicked: mask NaNs and inf"""
        self._mask.updateNotFinite(self.levelSpinBox.value())
        self._mask.commit()
예제 #3
0
class _AmplitudeRangeDialog(qt.QDialog):
    """QDialog asking for the amplitude range to display."""

    sigRangeChanged = qt.Signal(tuple)
    """Signal emitted when the range has changed.

    It provides the new range as a 2-tuple: (max, delta)
    """

    def __init__(self,
                 parent=None,
                 amplitudeRange=None,
                 displayedRange=(None, 2)):
        super(_AmplitudeRangeDialog, self).__init__(parent)
        self.setWindowTitle('Set Displayed Amplitude Range')

        if amplitudeRange is not None:
            amplitudeRange = min(amplitudeRange), max(amplitudeRange)
        self._amplitudeRange = amplitudeRange
        self._defaultDisplayedRange = displayedRange

        layout = qt.QFormLayout()
        self.setLayout(layout)

        if self._amplitudeRange is not None:
            min_, max_ = self._amplitudeRange
            layout.addRow(
                qt.QLabel('Data Amplitude Range: [%g, %g]' % (min_, max_)))

        self._maxLineEdit = FloatEdit(parent=self)
        self._maxLineEdit.validator().setBottom(0.)
        self._maxLineEdit.setAlignment(qt.Qt.AlignRight)

        self._maxLineEdit.editingFinished.connect(self._rangeUpdated)
        layout.addRow('Displayed Max.:', self._maxLineEdit)

        self._autoscale = qt.QCheckBox('autoscale')
        self._autoscale.toggled.connect(self._autoscaleCheckBoxToggled)
        layout.addRow('', self._autoscale)

        self._deltaLineEdit = FloatEdit(parent=self)
        self._deltaLineEdit.validator().setBottom(1.)
        self._deltaLineEdit.setAlignment(qt.Qt.AlignRight)
        self._deltaLineEdit.editingFinished.connect(self._rangeUpdated)
        layout.addRow('Displayed delta (log10 unit):', self._deltaLineEdit)

        buttons = qt.QDialogButtonBox(self)
        buttons.addButton(qt.QDialogButtonBox.Ok)
        buttons.addButton(qt.QDialogButtonBox.Cancel)
        buttons.accepted.connect(self.accept)
        buttons.rejected.connect(self.reject)
        layout.addRow(buttons)

        # Set dialog from default values
        self._resetDialogToDefault()

        self.rejected.connect(self._handleRejected)

    def _resetDialogToDefault(self):
        """Set Widgets of the dialog from range information
        """
        max_, delta = self._defaultDisplayedRange

        if max_ is not None:  # Not in autoscale
            displayedMax = max_
        elif self._amplitudeRange is not None:  # Autoscale with data
            displayedMax = self._amplitudeRange[1]
        else:  # Autoscale without data
            displayedMax = ''
        if displayedMax == "":
            self._maxLineEdit.setText("")
        else:
            self._maxLineEdit.setValue(displayedMax)
        self._maxLineEdit.setEnabled(max_ is not None)

        self._deltaLineEdit.setValue(delta)

        self._autoscale.setChecked(self._defaultDisplayedRange[0] is None)

    def getRangeInfo(self):
        """Returns the current range as a 2-tuple (max, delta (in log10))"""
        if self._autoscale.isChecked():
            max_ = None
        else:
            maxStr = self._maxLineEdit.text()
            max_ = self._maxLineEdit.value() if maxStr else None
        return max_, self._deltaLineEdit.value() if self._deltaLineEdit.text() else 2

    def _handleRejected(self):
        """Reset range info to default when rejected"""
        self._resetDialogToDefault()
        self._rangeUpdated()

    def _rangeUpdated(self):
        """Handle QLineEdit editing finised"""
        self.sigRangeChanged.emit(self.getRangeInfo())

    def _autoscaleCheckBoxToggled(self, checked):
        """Handle autoscale checkbox state changes"""
        if checked:  # Use default values
            if self._amplitudeRange is None:
                max_ = ''
            else:
                max_ = self._amplitudeRange[1]
            if max_ == "":
                self._maxLineEdit.setText("")
            else:
                self._maxLineEdit.setValue(max_)
        self._maxLineEdit.setEnabled(not checked)
        self._rangeUpdated()
예제 #4
0
class BaseMaskToolsWidget(qt.QWidget):
    """Base class for :class:`MaskToolsWidget` (image mask) and
    :class:`scatterMaskToolsWidget`"""

    sigMaskChanged = qt.Signal()
    _maxLevelNumber = 255

    def __init__(self, parent=None, plot=None, mask=None):
        """

        :param parent: Parent QWidget
        :param plot: Plot widget on which to operate
        :param mask: Instance of subclass of :class:`BaseMask`
            (e.g. :class:`ImageMask`)
        """
        super(BaseMaskToolsWidget, self).__init__(parent)
        # register if the user as force a color for the corresponding mask level
        self._defaultColors = numpy.ones((self._maxLevelNumber + 1), dtype=numpy.bool)
        # overlays colors set by the user
        self._overlayColors = numpy.zeros((self._maxLevelNumber + 1, 3), dtype=numpy.float32)

        # as parent have to be the first argument of the widget to fit
        # QtDesigner need but here plot can't be None by default.
        assert plot is not None
        self._plotRef = weakref.ref(plot)
        self._maskName = '__MASK_TOOLS_%d' % id(self)  # Legend of the mask

        self._colormap = Colormap(normalization='linear',
                                  vmin=0,
                                  vmax=self._maxLevelNumber)
        self._defaultOverlayColor = rgba('gray')  # Color of the mask
        self._setMaskColors(1, 0.5)  # Set the colormap LUT

        if not isinstance(mask, BaseMask):
            raise TypeError("mask is not an instance of BaseMask")
        self._mask = mask

        self._mask.sigChanged.connect(self._updatePlotMask)
        self._mask.sigChanged.connect(self._emitSigMaskChanged)

        self._drawingMode = None  # Store current drawing mode
        self._lastPencilPos = None
        self._multipleMasks = 'exclusive'

        self._maskFileDir = qt.QDir.home().absolutePath()
        self.plot.sigInteractiveModeChanged.connect(
            self._interactiveModeChanged)

        self._initWidgets()

    def _emitSigMaskChanged(self):
        """Notify mask changes"""
        self.sigMaskChanged.emit()

    def getSelectionMask(self, copy=True):
        """Get the current mask as a numpy array.

        :param bool copy: True (default) to get a copy of the mask.
                          If False, the returned array MUST not be modified.
        :return: The mask (as an array of uint8) with dimension of
                 the 'active' plot item.
                 If there is no active image or scatter, it returns None.
        :rtype: Union[numpy.ndarray,None]
        """
        mask = self._mask.getMask(copy=copy)
        return None if mask.size == 0 else mask

    def setSelectionMask(self, mask):
        """Set the mask: Must be implemented in subclass"""
        raise NotImplementedError()

    def resetSelectionMask(self):
        """Reset the mask: Must be implemented in subclass"""
        raise NotImplementedError()

    def multipleMasks(self):
        """Return the current mode of multiple masks support.

        See :meth:`setMultipleMasks`
        """
        return self._multipleMasks

    def setMultipleMasks(self, mode):
        """Set the mode of multiple masks support.

        Available modes:

        - 'single': Edit a single level of mask
        - 'exclusive': Supports to 256 levels of non overlapping masks

        :param str mode: The mode to use
        """
        assert mode in ('exclusive', 'single')
        if mode != self._multipleMasks:
            self._multipleMasks = mode
            self.levelWidget.setVisible(self._multipleMasks != 'single')
            self.clearAllBtn.setVisible(self._multipleMasks != 'single')

    @property
    def maskFileDir(self):
        """The directory from which to load/save mask from/to files."""
        if not os.path.isdir(self._maskFileDir):
            self._maskFileDir = qt.QDir.home().absolutePath()
        return self._maskFileDir

    @maskFileDir.setter
    def maskFileDir(self, maskFileDir):
        self._maskFileDir = str(maskFileDir)

    @property
    def plot(self):
        """The :class:`.PlotWindow` this widget is attached to."""
        plot = self._plotRef()
        if plot is None:
            raise RuntimeError(
                'Mask widget attached to a PlotWidget that no longer exists')
        return plot

    def setDirection(self, direction=qt.QBoxLayout.LeftToRight):
        """Set the direction of the layout of the widget

        :param direction: QBoxLayout direction
        """
        self.layout().setDirection(direction)

    def _initWidgets(self):
        """Create widgets"""
        layout = qt.QBoxLayout(qt.QBoxLayout.LeftToRight)
        layout.addWidget(self._initMaskGroupBox())
        layout.addWidget(self._initDrawGroupBox())
        layout.addWidget(self._initThresholdGroupBox())
        layout.addWidget(self._initOtherToolsGroupBox())
        layout.addStretch(1)
        self.setLayout(layout)

    @staticmethod
    def _hboxWidget(*widgets, **kwargs):
        """Place widgets in widget with horizontal layout

        :param widgets: Widgets to position horizontally
        :param bool stretch: True for trailing stretch (default),
                             False for no trailing stretch
        :return: A QWidget with a QHBoxLayout
        """
        stretch = kwargs.get('stretch', True)

        layout = qt.QHBoxLayout()
        layout.setContentsMargins(0, 0, 0, 0)
        for widget in widgets:
            layout.addWidget(widget)
        if stretch:
            layout.addStretch(1)
        widget = qt.QWidget()
        widget.setLayout(layout)
        return widget

    def _initTransparencyWidget(self):
        """ Init the mask transparency widget """
        transparencyWidget = qt.QWidget(self)
        grid = qt.QGridLayout()
        grid.setContentsMargins(0, 0, 0, 0)
        self.transparencySlider = qt.QSlider(qt.Qt.Horizontal, parent=transparencyWidget)
        self.transparencySlider.setRange(3, 10)
        self.transparencySlider.setValue(8)
        self.transparencySlider.setToolTip(
                'Set the transparency of the mask display')
        self.transparencySlider.valueChanged.connect(self._updateColors)
        grid.addWidget(qt.QLabel('Display:', parent=transparencyWidget), 0, 0)
        grid.addWidget(self.transparencySlider, 0, 1, 1, 3)
        grid.addWidget(qt.QLabel('<small><b>Transparent</b></small>', parent=transparencyWidget), 1, 1)
        grid.addWidget(qt.QLabel('<small><b>Opaque</b></small>', parent=transparencyWidget), 1, 3)
        transparencyWidget.setLayout(grid)
        return transparencyWidget

    def _initMaskGroupBox(self):
        """Init general mask operation widgets"""

        # Mask level
        self.levelSpinBox = qt.QSpinBox()
        self.levelSpinBox.setRange(1, self._maxLevelNumber)
        self.levelSpinBox.setToolTip(
                'Choose which mask level is edited.\n'
                'A mask can have up to 255 non-overlapping levels.')
        self.levelSpinBox.valueChanged[int].connect(self._updateColors)
        self.levelWidget = self._hboxWidget(qt.QLabel('Mask level:'),
                                            self.levelSpinBox)
        # Transparency
        self.transparencyWidget = self._initTransparencyWidget()

        # Buttons group
        invertBtn = qt.QPushButton('Invert')
        invertBtn.setShortcut(qt.Qt.CTRL + qt.Qt.Key_I)
        invertBtn.setToolTip('Invert current mask <b>%s</b>' %
                             invertBtn.shortcut().toString())
        invertBtn.clicked.connect(self._handleInvertMask)

        clearBtn = qt.QPushButton('Clear')
        clearBtn.setShortcut(qt.QKeySequence.Delete)
        clearBtn.setToolTip('Clear current mask level <b>%s</b>' %
                            clearBtn.shortcut().toString())
        clearBtn.clicked.connect(self._handleClearMask)

        invertClearWidget = self._hboxWidget(
                invertBtn, clearBtn, stretch=False)

        undoBtn = qt.QPushButton('Undo')
        undoBtn.setShortcut(qt.QKeySequence.Undo)
        undoBtn.setToolTip('Undo last mask change <b>%s</b>' %
                           undoBtn.shortcut().toString())
        self._mask.sigUndoable.connect(undoBtn.setEnabled)
        undoBtn.clicked.connect(self._mask.undo)

        redoBtn = qt.QPushButton('Redo')
        redoBtn.setShortcut(qt.QKeySequence.Redo)
        redoBtn.setToolTip('Redo last undone mask change <b>%s</b>' %
                           redoBtn.shortcut().toString())
        self._mask.sigRedoable.connect(redoBtn.setEnabled)
        redoBtn.clicked.connect(self._mask.redo)

        undoRedoWidget = self._hboxWidget(undoBtn, redoBtn, stretch=False)

        self.clearAllBtn = qt.QPushButton('Clear all')
        self.clearAllBtn.setToolTip('Clear all mask levels')
        self.clearAllBtn.clicked.connect(self.resetSelectionMask)

        loadBtn = qt.QPushButton('Load...')
        loadBtn.clicked.connect(self._loadMask)

        saveBtn = qt.QPushButton('Save...')
        saveBtn.clicked.connect(self._saveMask)

        self.loadSaveWidget = self._hboxWidget(loadBtn, saveBtn, stretch=False)

        layout = qt.QVBoxLayout()
        layout.addWidget(self.levelWidget)
        layout.addWidget(self.transparencyWidget)
        layout.addWidget(invertClearWidget)
        layout.addWidget(undoRedoWidget)
        layout.addWidget(self.clearAllBtn)
        layout.addWidget(self.loadSaveWidget)
        layout.addStretch(1)

        maskGroup = qt.QGroupBox('Mask')
        maskGroup.setLayout(layout)
        return maskGroup

    def isMaskInteractionActivated(self):
        """Returns true if any mask interaction is activated"""
        return self.drawActionGroup.checkedAction() is not None

    def _initDrawGroupBox(self):
        """Init drawing tools widgets"""
        layout = qt.QVBoxLayout()

        self.browseAction = PanModeAction(self.plot, self.plot)
        self.addAction(self.browseAction)

        # Draw tools
        self.rectAction = qt.QAction(
                icons.getQIcon('shape-rectangle'), 'Rectangle selection', None)
        self.rectAction.setToolTip(
                'Rectangle selection tool: (Un)Mask a rectangular region <b>R</b>')
        self.rectAction.setShortcut(qt.QKeySequence(qt.Qt.Key_R))
        self.rectAction.setCheckable(True)
        self.rectAction.triggered.connect(self._activeRectMode)
        self.addAction(self.rectAction)

        self.ellipseAction = qt.QAction(
                icons.getQIcon('shape-ellipse'), 'Circle selection', None)
        self.ellipseAction.setToolTip(
                'Rectangle selection tool: (Un)Mask a circle region <b>R</b>')
        self.ellipseAction.setShortcut(qt.QKeySequence(qt.Qt.Key_R))
        self.ellipseAction.setCheckable(True)
        self.ellipseAction.triggered.connect(self._activeEllipseMode)
        self.addAction(self.ellipseAction)

        self.polygonAction = qt.QAction(
                icons.getQIcon('shape-polygon'), 'Polygon selection', None)
        self.polygonAction.setShortcut(qt.QKeySequence(qt.Qt.Key_S))
        self.polygonAction.setToolTip(
                'Polygon selection tool: (Un)Mask a polygonal region <b>S</b><br>'
                'Left-click to place new polygon corners<br>'
                'Left-click on first corner to close the polygon')
        self.polygonAction.setCheckable(True)
        self.polygonAction.triggered.connect(self._activePolygonMode)
        self.addAction(self.polygonAction)

        self.pencilAction = qt.QAction(
                icons.getQIcon('draw-pencil'), 'Pencil tool', None)
        self.pencilAction.setShortcut(qt.QKeySequence(qt.Qt.Key_P))
        self.pencilAction.setToolTip(
                'Pencil tool: (Un)Mask using a pencil <b>P</b>')
        self.pencilAction.setCheckable(True)
        self.pencilAction.triggered.connect(self._activePencilMode)
        self.addAction(self.pencilAction)

        self.drawActionGroup = qt.QActionGroup(self)
        self.drawActionGroup.setExclusive(True)
        self.drawActionGroup.addAction(self.rectAction)
        self.drawActionGroup.addAction(self.ellipseAction)
        self.drawActionGroup.addAction(self.polygonAction)
        self.drawActionGroup.addAction(self.pencilAction)

        actions = (self.browseAction, self.rectAction, self.ellipseAction,
                   self.polygonAction, self.pencilAction)
        drawButtons = []
        for action in actions:
            btn = qt.QToolButton()
            btn.setDefaultAction(action)
            drawButtons.append(btn)
        container = self._hboxWidget(*drawButtons)
        layout.addWidget(container)

        # Mask/Unmask radio buttons
        maskRadioBtn = qt.QRadioButton('Mask')
        maskRadioBtn.setToolTip(
                'Drawing masks with current level. Press <b>Ctrl</b> to unmask')
        maskRadioBtn.setChecked(True)

        unmaskRadioBtn = qt.QRadioButton('Unmask')
        unmaskRadioBtn.setToolTip(
                'Drawing unmasks with current level. Press <b>Ctrl</b> to mask')

        self.maskStateGroup = qt.QButtonGroup()
        self.maskStateGroup.addButton(maskRadioBtn, 1)
        self.maskStateGroup.addButton(unmaskRadioBtn, 0)

        self.maskStateWidget = self._hboxWidget(maskRadioBtn, unmaskRadioBtn)
        layout.addWidget(self.maskStateWidget)

        self.maskStateWidget.setHidden(True)

        # Pencil settings
        self.pencilSetting = self._createPencilSettings(None)
        self.pencilSetting.setVisible(False)
        layout.addWidget(self.pencilSetting)

        layout.addStretch(1)

        drawGroup = qt.QGroupBox('Draw tools')
        drawGroup.setLayout(layout)
        return drawGroup

    def _createPencilSettings(self, parent=None):
        pencilSetting = qt.QWidget(parent)

        self.pencilSpinBox = qt.QSpinBox(parent=pencilSetting)
        self.pencilSpinBox.setRange(1, 1024)
        pencilToolTip = """Set pencil drawing tool size in pixels of the image
            on which to make the mask."""
        self.pencilSpinBox.setToolTip(pencilToolTip)

        self.pencilSlider = qt.QSlider(qt.Qt.Horizontal, parent=pencilSetting)
        self.pencilSlider.setRange(1, 50)
        self.pencilSlider.setToolTip(pencilToolTip)

        pencilLabel = qt.QLabel('Pencil size:', parent=pencilSetting)

        layout = qt.QGridLayout()
        layout.addWidget(pencilLabel, 0, 0)
        layout.addWidget(self.pencilSpinBox, 0, 1)
        layout.addWidget(self.pencilSlider, 1, 1)
        pencilSetting.setLayout(layout)

        self.pencilSpinBox.valueChanged.connect(self._pencilWidthChanged)
        self.pencilSlider.valueChanged.connect(self._pencilWidthChanged)

        return pencilSetting

    def _initThresholdGroupBox(self):
        """Init thresholding widgets"""

        self.belowThresholdAction = qt.QAction(
                icons.getQIcon('plot-roi-below'), 'Mask below threshold', None)
        self.belowThresholdAction.setToolTip(
                'Mask image where values are below given threshold')
        self.belowThresholdAction.setCheckable(True)
        self.belowThresholdAction.setChecked(True)

        self.betweenThresholdAction = qt.QAction(
                icons.getQIcon('plot-roi-between'), 'Mask within range', None)
        self.betweenThresholdAction.setToolTip(
                'Mask image where values are within given range')
        self.betweenThresholdAction.setCheckable(True)

        self.aboveThresholdAction = qt.QAction(
                icons.getQIcon('plot-roi-above'), 'Mask above threshold', None)
        self.aboveThresholdAction.setToolTip(
                'Mask image where values are above given threshold')
        self.aboveThresholdAction.setCheckable(True)

        self.thresholdActionGroup = qt.QActionGroup(self)
        self.thresholdActionGroup.setExclusive(True)
        self.thresholdActionGroup.addAction(self.belowThresholdAction)
        self.thresholdActionGroup.addAction(self.betweenThresholdAction)
        self.thresholdActionGroup.addAction(self.aboveThresholdAction)
        self.thresholdActionGroup.triggered.connect(
                self._thresholdActionGroupTriggered)

        self.loadColormapRangeAction = qt.QAction(
                icons.getQIcon('view-refresh'), 'Set min-max from colormap', None)
        self.loadColormapRangeAction.setToolTip(
                'Set min and max values from current colormap range')
        self.loadColormapRangeAction.setCheckable(False)
        self.loadColormapRangeAction.triggered.connect(
                self._loadRangeFromColormapTriggered)

        widgets = []
        for action in self.thresholdActionGroup.actions():
            btn = qt.QToolButton()
            btn.setDefaultAction(action)
            widgets.append(btn)

        spacer = qt.QWidget()
        spacer.setSizePolicy(qt.QSizePolicy.Expanding,
                             qt.QSizePolicy.Preferred)
        widgets.append(spacer)

        loadColormapRangeBtn = qt.QToolButton()
        loadColormapRangeBtn.setDefaultAction(self.loadColormapRangeAction)
        widgets.append(loadColormapRangeBtn)

        toolBar = self._hboxWidget(*widgets, stretch=False)

        config = qt.QGridLayout()
        config.setContentsMargins(0, 0, 0, 0)

        self.minLineLabel = qt.QLabel("Min:", self)
        self.minLineEdit = FloatEdit(self, value=0)
        config.addWidget(self.minLineLabel, 0, 0)
        config.addWidget(self.minLineEdit, 0, 1)

        self.maxLineLabel = qt.QLabel("Max:", self)
        self.maxLineEdit = FloatEdit(self, value=0)
        config.addWidget(self.maxLineLabel, 1, 0)
        config.addWidget(self.maxLineEdit, 1, 1)

        self.applyMaskBtn = qt.QPushButton('Apply mask')
        self.applyMaskBtn.clicked.connect(self._maskBtnClicked)

        layout = qt.QVBoxLayout()
        layout.addWidget(toolBar)
        layout.addLayout(config)
        layout.addWidget(self.applyMaskBtn)

        self.thresholdGroup = qt.QGroupBox('Threshold')
        self.thresholdGroup.setLayout(layout)

        # Init widget state
        self._thresholdActionGroupTriggered(self.belowThresholdAction)
        return self.thresholdGroup

        # track widget visibility and plot active image changes

    def _initOtherToolsGroupBox(self):
        layout = qt.QVBoxLayout()

        self.maskNanBtn = qt.QPushButton('Mask not finite values')
        self.maskNanBtn.setToolTip('Mask Not a Number and infinite values')
        self.maskNanBtn.clicked.connect(self._maskNotFiniteBtnClicked)
        layout.addWidget(self.maskNanBtn)

        self.otherToolGroup = qt.QGroupBox('Other tools')
        self.otherToolGroup.setLayout(layout)
        return self.otherToolGroup

    def changeEvent(self, event):
        """Reset drawing action when disabling widget"""
        if (event.type() == qt.QEvent.EnabledChange and
                not self.isEnabled() and
                self.drawActionGroup.checkedAction()):
            # Disable drawing tool by setting interaction to zoom
            self.browseAction.trigger()

    def save(self, filename, kind):
        """Save current mask in a file

        :param str filename: The file where to save to mask
        :param str kind: The kind of file to save in 'edf', 'tif', 'npy'
        :raise Exception: Raised if the process fails
        """
        self._mask.save(filename, kind)

    def getCurrentMaskColor(self):
        """Returns the color of the current selected level.

        :rtype: A tuple or a python array
        """
        currentLevel = self.levelSpinBox.value()
        if self._defaultColors[currentLevel]:
            return self._defaultOverlayColor
        else:
            return self._overlayColors[currentLevel].tolist()

    def _setMaskColors(self, level, alpha):
        """Set-up the mask colormap to highlight current mask level.

        :param int level: The mask level to highlight
        :param float alpha: Alpha level of mask in [0., 1.]
        """
        assert 0 < level <= self._maxLevelNumber

        colors = numpy.empty((self._maxLevelNumber + 1, 4), dtype=numpy.float32)

        # Set color
        colors[:, :3] = self._defaultOverlayColor[:3]

        # check if some colors has been directly set by the user
        mask = numpy.equal(self._defaultColors, False)
        colors[mask, :3] = self._overlayColors[mask, :3]

        # Set alpha
        colors[:, -1] = alpha / 2.

        # Set highlighted level color
        colors[level, 3] = alpha

        # Set no mask level
        colors[0] = (0., 0., 0., 0.)

        self._colormap.setColormapLUT(colors)

    def resetMaskColors(self, level=None):
        """Reset the mask color at the given level to be defaultColors

        :param level:
            The index of the mask for which we want to reset the color.
            If none we will reset color for all masks.
        """
        if level is None:
            self._defaultColors[level] = True
        else:
            self._defaultColors[:] = True

        self._updateColors()

    def setMaskColors(self, rgb, level=None):
        """Set the masks color

        :param rgb: The rgb color
        :param level:
            The index of the mask for which we want to change the color.
            If none set this color for all the masks
        """
        rgb = rgba(rgb)[0:3]
        if level is None:
            self._overlayColors[:] = rgb
            self._defaultColors[:] = False
        else:
            self._overlayColors[level] = rgb
            self._defaultColors[level] = False

        self._updateColors()

    def getMaskColors(self):
        """masks colors getter"""
        return self._overlayColors

    def _updateColors(self, *args):
        """Rebuild mask colormap when selected level or transparency change"""
        self._setMaskColors(self.levelSpinBox.value(),
                            self.transparencySlider.value() /
                            self.transparencySlider.maximum())
        self._updatePlotMask()
        self._updateInteractiveMode()

    def _pencilWidthChanged(self, width):

        old = self.pencilSpinBox.blockSignals(True)
        try:
            self.pencilSpinBox.setValue(width)
        finally:
            self.pencilSpinBox.blockSignals(old)

        old = self.pencilSlider.blockSignals(True)
        try:
            self.pencilSlider.setValue(width)
        finally:
            self.pencilSlider.blockSignals(old)
        self._updateInteractiveMode()

    def _updateInteractiveMode(self):
        """Update the current mode to the same if some cached data have to be
        updated. It is the case for the color for example.
        """
        if self._drawingMode == 'rectangle':
            self._activeRectMode()
        elif self._drawingMode == 'ellipse':
            self._activeEllipseMode()
        elif self._drawingMode == 'polygon':
            self._activePolygonMode()
        elif self._drawingMode == 'pencil':
            self._activePencilMode()

    def _handleClearMask(self):
        """Handle clear button clicked: reset current level mask"""
        self._mask.clear(self.levelSpinBox.value())
        self._mask.commit()

    def _handleInvertMask(self):
        """Invert the current mask level selection."""
        self._mask.invert(self.levelSpinBox.value())
        self._mask.commit()

    # Handle drawing tools UI events

    def _interactiveModeChanged(self, source):
        """Handle plot interactive mode changed:

        If changed from elsewhere, disable drawing tool
        """
        if source is not self:
            self.pencilAction.setChecked(False)
            self.rectAction.setChecked(False)
            self.polygonAction.setChecked(False)
            self._releaseDrawingMode()
            self._updateDrawingModeWidgets()

    def _releaseDrawingMode(self):
        """Release the drawing mode if is was used"""
        if self._drawingMode is None:
            return
        self.plot.sigPlotSignal.disconnect(self._plotDrawEvent)
        self._drawingMode = None

    def _activeRectMode(self):
        """Handle rect action mode triggering"""
        self._releaseDrawingMode()
        self._drawingMode = 'rectangle'
        self.plot.sigPlotSignal.connect(self._plotDrawEvent)
        color = self.getCurrentMaskColor()
        self.plot.setInteractiveMode(
            'draw', shape='rectangle', source=self, color=color)
        self._updateDrawingModeWidgets()

    def _activeEllipseMode(self):
        """Handle circle action mode triggering"""
        self._releaseDrawingMode()
        self._drawingMode = 'ellipse'
        self.plot.sigPlotSignal.connect(self._plotDrawEvent)
        color = self.getCurrentMaskColor()
        self.plot.setInteractiveMode(
            'draw', shape='ellipse', source=self, color=color)
        self._updateDrawingModeWidgets()

    def _activePolygonMode(self):
        """Handle polygon action mode triggering"""
        self._releaseDrawingMode()
        self._drawingMode = 'polygon'
        self.plot.sigPlotSignal.connect(self._plotDrawEvent)
        color = self.getCurrentMaskColor()
        self.plot.setInteractiveMode('draw', shape='polygon', source=self, color=color)
        self._updateDrawingModeWidgets()

    def _getPencilWidth(self):
        """Returns the width of the pencil to use in data coordinates`

        :rtype: float
        """
        return self.pencilSpinBox.value()

    def _activePencilMode(self):
        """Handle pencil action mode triggering"""
        self._releaseDrawingMode()
        self._drawingMode = 'pencil'
        self.plot.sigPlotSignal.connect(self._plotDrawEvent)
        color = self.getCurrentMaskColor()
        width = self._getPencilWidth()
        self.plot.setInteractiveMode(
            'draw', shape='pencil', source=self, color=color, width=width)
        self._updateDrawingModeWidgets()

    def _updateDrawingModeWidgets(self):
        self.maskStateWidget.setVisible(self._drawingMode is not None)
        self.pencilSetting.setVisible(self._drawingMode == 'pencil')

    # Handle plot drawing events

    def _isMasking(self):
        """Returns true if the tool is used for masking, else it is used for
        unmasking.

        :rtype: bool"""
        # First draw event, use current modifiers for all draw sequence
        doMask = (self.maskStateGroup.checkedId() == 1)
        if qt.QApplication.keyboardModifiers() & qt.Qt.ControlModifier:
            doMask = not doMask
        return doMask

    # Handle threshold UI events

    def _thresholdActionGroupTriggered(self, triggeredAction):
        """Threshold action group listener."""
        if triggeredAction is self.belowThresholdAction:
            self.minLineLabel.setVisible(True)
            self.maxLineLabel.setVisible(False)
            self.minLineEdit.setVisible(True)
            self.maxLineEdit.setVisible(False)
            self.applyMaskBtn.setText("Mask bellow")
        elif triggeredAction is self.betweenThresholdAction:
            self.minLineLabel.setVisible(True)
            self.maxLineLabel.setVisible(True)
            self.minLineEdit.setVisible(True)
            self.maxLineEdit.setVisible(True)
            self.applyMaskBtn.setText("Mask between")
        elif triggeredAction is self.aboveThresholdAction:
            self.minLineLabel.setVisible(False)
            self.maxLineLabel.setVisible(True)
            self.minLineEdit.setVisible(False)
            self.maxLineEdit.setVisible(True)
            self.applyMaskBtn.setText("Mask above")
        self.applyMaskBtn.setToolTip(triggeredAction.toolTip())

    def _maskBtnClicked(self):
        if self.belowThresholdAction.isChecked():
            if self.minLineEdit.text():
                self._mask.updateBelowThreshold(self.levelSpinBox.value(),
                                                self.minLineEdit.value())
                self._mask.commit()

        elif self.betweenThresholdAction.isChecked():
            if self.minLineEdit.text() and self.maxLineEdit.text():
                min_ = self.minLineEdit.value()
                max_ = self.maxLineEdit.value()
                self._mask.updateBetweenThresholds(self.levelSpinBox.value(),
                                                   min_, max_)
                self._mask.commit()

        elif self.aboveThresholdAction.isChecked():
            if self.maxLineEdit.text():
                max_ = float(self.maxLineEdit.value())
                self._mask.updateAboveThreshold(self.levelSpinBox.value(),
                                                max_)
                self._mask.commit()

    def _maskNotFiniteBtnClicked(self):
        """Handle not finite mask button clicked: mask NaNs and inf"""
        self._mask.updateNotFinite(
            self.levelSpinBox.value())
        self._mask.commit()
예제 #5
0
class ColormapDialog(qt.QDialog):
    """A QDialog widget to set the colormap.

    :param parent: See :class:`QDialog`
    :param str title: The QDialog title
    """

    sigColormapChanged = qt.Signal(Colormap)
    """Signal triggered when the colormap is changed.

    It provides a dict describing the colormap to the slot.
    This dict can be used with :class:`Plot`.
    """
    def __init__(self, parent=None, title="Colormap Dialog"):
        qt.QDialog.__init__(self, parent)
        self.setWindowTitle(title)

        self._histogramData = None
        self._dataRange = None
        self._minMaxWasEdited = False

        colormaps = [
            'gray', 'reversed gray', 'temperature', 'red', 'green', 'blue',
            'jet', 'viridis', 'magma', 'inferno', 'plasma'
        ]
        if 'hsv' in Colormap.getSupportedColormaps():
            colormaps.append('hsv')
        self._colormapList = tuple(colormaps)

        # Make the GUI
        vLayout = qt.QVBoxLayout(self)

        formWidget = qt.QWidget()
        vLayout.addWidget(formWidget)
        formLayout = qt.QFormLayout(formWidget)
        formLayout.setContentsMargins(10, 10, 10, 10)
        formLayout.setSpacing(0)

        # Colormap row
        self._comboBoxColormap = qt.QComboBox()
        for cmap in self._colormapList:
            # Capitalize first letters
            cmap = ' '.join(w[0].upper() + w[1:] for w in cmap.split())
            self._comboBoxColormap.addItem(cmap)
        self._comboBoxColormap.activated[int].connect(self._notify)
        formLayout.addRow('Colormap:', self._comboBoxColormap)

        # Normalization row
        self._normButtonLinear = qt.QRadioButton('Linear')
        self._normButtonLinear.setChecked(True)
        self._normButtonLog = qt.QRadioButton('Log')

        normButtonGroup = qt.QButtonGroup(self)
        normButtonGroup.setExclusive(True)
        normButtonGroup.addButton(self._normButtonLinear)
        normButtonGroup.addButton(self._normButtonLog)
        normButtonGroup.buttonClicked[int].connect(self._notify)

        normLayout = qt.QHBoxLayout()
        normLayout.setContentsMargins(0, 0, 0, 0)
        normLayout.setSpacing(10)
        normLayout.addWidget(self._normButtonLinear)
        normLayout.addWidget(self._normButtonLog)

        formLayout.addRow('Normalization:', normLayout)

        # Range row
        self._rangeAutoscaleButton = qt.QCheckBox('Autoscale')
        self._rangeAutoscaleButton.setChecked(True)
        self._rangeAutoscaleButton.toggled.connect(self._autoscaleToggled)
        self._rangeAutoscaleButton.clicked.connect(self._notify)
        formLayout.addRow('Range:', self._rangeAutoscaleButton)

        # Min row
        self._minValue = FloatEdit(parent=self, value=1.)
        self._minValue.setEnabled(False)
        self._minValue.textEdited.connect(self._minMaxTextEdited)
        self._minValue.editingFinished.connect(self._minEditingFinished)
        formLayout.addRow('\tMin:', self._minValue)

        # Max row
        self._maxValue = FloatEdit(parent=self, value=10.)
        self._maxValue.setEnabled(False)
        self._maxValue.textEdited.connect(self._minMaxTextEdited)
        self._maxValue.editingFinished.connect(self._maxEditingFinished)
        formLayout.addRow('\tMax:', self._maxValue)

        # Add plot for histogram
        self._plotInit()
        vLayout.addWidget(self._plot)

        # Close button
        buttonsWidget = qt.QWidget()
        vLayout.addWidget(buttonsWidget)

        buttonsLayout = qt.QHBoxLayout(buttonsWidget)

        okButton = qt.QPushButton('OK')
        okButton.clicked.connect(self.accept)
        buttonsLayout.addWidget(okButton)

        cancelButton = qt.QPushButton('Cancel')
        cancelButton.clicked.connect(self.reject)
        buttonsLayout.addWidget(cancelButton)

        # colormap window can not be resized
        self.setFixedSize(vLayout.minimumSize())

        # Set the colormap to default values
        self.setColormap(name='gray',
                         normalization='linear',
                         autoscale=True,
                         vmin=1.,
                         vmax=10.)

    def _plotInit(self):
        """Init the plot to display the range and the values"""
        self._plot = PlotWidget()
        self._plot.setDataMargins(yMinMargin=0.125, yMaxMargin=0.125)
        self._plot.getXAxis().setLabel("Data Values")
        self._plot.getYAxis().setLabel("")
        self._plot.setInteractiveMode('select', zoomOnWheel=False)
        self._plot.setActiveCurveHandling(False)
        self._plot.setMinimumSize(qt.QSize(250, 200))
        self._plot.sigPlotSignal.connect(self._plotSlot)
        self._plot.hide()

        self._plotUpdate()

    def _plotUpdate(self, updateMarkers=True):
        """Update the plot content

        :param bool updateMarkers: True to update markers, False otherwith
        """
        dataRange = self.getDataRange()

        if dataRange is None:
            if self._plot.isVisibleTo(self):
                self._plot.setVisible(False)
                self.setFixedSize(self.layout().minimumSize())
            return

        if not self._plot.isVisibleTo(self):
            self._plot.setVisible(True)
            self.setFixedSize(self.layout().minimumSize())

        dataMin, dataMax = dataRange
        marge = (abs(dataMax) + abs(dataMin)) / 6.0
        minmd = dataMin - marge
        maxpd = dataMax + marge

        start, end = self._minValue.value(), self._maxValue.value()

        if start <= end:
            x = [minmd, start, end, maxpd]
            y = [0, 0, 1, 1]

        else:
            x = [minmd, end, start, maxpd]
            y = [1, 1, 0, 0]

        # Display the colormap on the side
        # colormap = {'name': self.getColormap()['name'],
        #             'normalization': self.getColormap()['normalization'],
        #             'autoscale': True, 'vmin': 1., 'vmax': 256.}
        # self._plot.addImage((1 + numpy.arange(256)).reshape(256, -1),
        #                     xScale=(minmd - marge, marge),
        #                     yScale=(1., 2./256.),
        #                     legend='colormap',
        #                     colormap=colormap)

        self._plot.addCurve(x,
                            y,
                            legend="ConstrainedCurve",
                            color='black',
                            symbol='o',
                            linestyle='-',
                            resetzoom=False)

        draggable = not self._rangeAutoscaleButton.isChecked()

        if updateMarkers:
            self._plot.addXMarker(self._minValue.value(),
                                  legend='Min',
                                  text='Min',
                                  draggable=draggable,
                                  color='blue',
                                  constraint=self._plotMinMarkerConstraint)

            self._plot.addXMarker(self._maxValue.value(),
                                  legend='Max',
                                  text='Max',
                                  draggable=draggable,
                                  color='blue',
                                  constraint=self._plotMaxMarkerConstraint)

        self._plot.resetZoom()

    def _plotMinMarkerConstraint(self, x, y):
        """Constraint of the min marker"""
        return min(x, self._maxValue.value()), y

    def _plotMaxMarkerConstraint(self, x, y):
        """Constraint of the max marker"""
        return max(x, self._minValue.value()), y

    def _plotSlot(self, event):
        """Handle events from the plot"""
        if event['event'] in ('markerMoving', 'markerMoved'):
            value = float(str(event['xdata']))
            if event['label'] == 'Min':
                self._minValue.setValue(value)
            elif event['label'] == 'Max':
                self._maxValue.setValue(value)

            # This will recreate the markers while interacting...
            # It might break if marker interaction is changed
            if event['event'] == 'markerMoved':
                self._notify()
            else:
                self._plotUpdate(updateMarkers=False)

    def getHistogram(self):
        """Returns the counts and bin edges of the displayed histogram.

        :return: (hist, bin_edges)
        :rtype: 2-tuple of numpy arrays"""
        if self._histogramData is None:
            return None
        else:
            bins, counts = self._histogramData
            return numpy.array(bins, copy=True), numpy.array(counts, copy=True)

    def setHistogram(self, hist=None, bin_edges=None):
        """Set the histogram to display.

        This update the data range with the bounds of the bins.
        See :meth:`setDataRange`.

        :param hist: array-like of counts or None to hide histogram
        :param bin_edges: array-like of bins edges or None to hide histogram
        """
        if hist is None or bin_edges is None:
            self._histogramData = None
            self._plot.remove(legend='Histogram', kind='curve')
            self.setDataRange()  # Remove data range

        else:
            hist = numpy.array(hist, copy=True)
            bin_edges = numpy.array(bin_edges, copy=True)
            self._histogramData = hist, bin_edges

            # For now, draw the histogram as a curve
            # using bin centers and normalised counts
            bins_center = 0.5 * (bin_edges[:-1] + bin_edges[1:])
            norm_hist = hist / max(hist)
            self._plot.addCurve(bins_center,
                                norm_hist,
                                legend="Histogram",
                                color='gray',
                                symbol='',
                                linestyle='-',
                                fill=True)

            # Update the data range
            self.setDataRange(bin_edges[0], bin_edges[-1])

    def getDataRange(self):
        """Returns the data range used for the histogram area.

        :return: (dataMin, dataMax) or None if no data range is set
        :rtype: 2-tuple of float
        """
        return self._dataRange

    def setDataRange(self, min_=None, max_=None):
        """Set the range of data to use for the range of the histogram area.

        :param float min_: The min of the data or None to disable range.
        :param float max_: The max of the data or None to disable range.
        """
        if min_ is None or max_ is None:
            self._dataRange = None
            self._plotUpdate()

        else:
            min_, max_ = float(min_), float(max_)
            assert min_ <= max_
            self._dataRange = min_, max_
            if self._rangeAutoscaleButton.isChecked():
                self._minValue.setValue(min_)
                self._maxValue.setValue(max_)
                self._notify()
            else:
                self._plotUpdate()

    def getColormap(self):
        """Return the colormap description as a :class:`.Colormap`.

        """
        isNormLinear = self._normButtonLinear.isChecked()
        if self._rangeAutoscaleButton.isChecked():
            vmin = None
            vmax = None
        else:
            vmin = self._minValue.value()
            vmax = self._maxValue.value()
        norm = Colormap.LINEAR if isNormLinear else Colormap.LOGARITHM
        colormap = Colormap(name=str(
            self._comboBoxColormap.currentText()).lower(),
                            normalization=norm,
                            vmin=vmin,
                            vmax=vmax)
        return colormap

    def setColormap(self,
                    name=None,
                    normalization=None,
                    autoscale=None,
                    vmin=None,
                    vmax=None,
                    colors=None):
        """Set the colormap description

        If some arguments are not provided, the current values are used.

        :param str name: The name of the colormap
        :param str normalization: 'linear' or 'log'
        :param bool autoscale: Toggle colormap range autoscale
        :param float vmin: The min value, ignored if autoscale is True
        :param float vmax: The max value, ignored if autoscale is True
        """
        if name is not None:
            assert name in self._colormapList
            index = self._colormapList.index(name)
            self._comboBoxColormap.setCurrentIndex(index)

        if normalization is not None:
            assert normalization in Colormap.NORMALIZATIONS
            self._normButtonLinear.setChecked(normalization == Colormap.LINEAR)
            self._normButtonLog.setChecked(normalization == Colormap.LOGARITHM)

        if vmin is not None:
            self._minValue.setValue(vmin)

        if vmax is not None:
            self._maxValue.setValue(vmax)

        if autoscale is not None:
            self._rangeAutoscaleButton.setChecked(autoscale)
            if autoscale:
                dataRange = self.getDataRange()
                if dataRange is not None:
                    self._minValue.setValue(dataRange[0])
                    self._maxValue.setValue(dataRange[1])

        # Do it once for all the changes
        self._notify()

    def _notify(self, *args, **kwargs):
        """Emit the signal for colormap change"""
        self._plotUpdate()
        self.sigColormapChanged.emit(self.getColormap())

    def _autoscaleToggled(self, checked):
        """Handle autoscale changes by enabling/disabling min/max fields"""
        self._minValue.setEnabled(not checked)
        self._maxValue.setEnabled(not checked)
        if checked:
            dataRange = self.getDataRange()
            if dataRange is not None:
                self._minValue.setValue(dataRange[0])
                self._maxValue.setValue(dataRange[1])

    def _minMaxTextEdited(self, text):
        """Handle _minValue and _maxValue textEdited signal"""
        self._minMaxWasEdited = True

    def _minEditingFinished(self):
        """Handle _minValue editingFinished signal

        Together with :meth:`_minMaxTextEdited`, this avoids to notify
        colormap change when the min and max value where not edited.
        """
        if self._minMaxWasEdited:
            self._minMaxWasEdited = False

            # Fix start value
            if self._minValue.value() > self._maxValue.value():
                self._minValue.setValue(self._maxValue.value())
            self._notify()

    def _maxEditingFinished(self):
        """Handle _maxValue editingFinished signal

        Together with :meth:`_minMaxTextEdited`, this avoids to notify
        colormap change when the min and max value where not edited.
        """
        if self._minMaxWasEdited:
            self._minMaxWasEdited = False

            # Fix end value
            if self._minValue.value() > self._maxValue.value():
                self._maxValue.setValue(self._minValue.value())
            self._notify()

    def keyPressEvent(self, event):
        """Override key handling.

        It disables leaving the dialog when editing a text field.
        """
        if event.key() == qt.Qt.Key_Enter and (self._minValue.hasFocus()
                                               or self._maxValue.hasFocus()):
            # Bypass QDialog keyPressEvent
            # To avoid leaving the dialog when pressing enter on a text field
            super(qt.QDialog, self).keyPressEvent(event)
        else:
            # Use QDialog keyPressEvent
            super(ColormapDialog, self).keyPressEvent(event)
예제 #6
0
class PrintGeometryWidget(qt.QWidget):
    """Widget to specify the size and aspect ratio of an item
    before sending it to the print preview dialog.

    Use methods :meth:`setPrintGeometry` and :meth:`getPrintGeometry`
    to interact with the widget.
    """
    def __init__(self, parent=None):
        super(PrintGeometryWidget, self).__init__(parent)
        self.mainLayout = qt.QGridLayout(self)
        self.mainLayout.setContentsMargins(0, 0, 0, 0)
        self.mainLayout.setSpacing(2)
        hbox = qt.QWidget(self)
        hboxLayout = qt.QHBoxLayout(hbox)
        hboxLayout.setContentsMargins(0, 0, 0, 0)
        hboxLayout.setSpacing(2)
        label = qt.QLabel(self)
        label.setText("Units")
        label.setAlignment(qt.Qt.AlignCenter)
        self._pageButton = qt.QRadioButton()
        self._pageButton.setText("Page")
        self._inchButton = qt.QRadioButton()
        self._inchButton.setText("Inches")
        self._cmButton = qt.QRadioButton()
        self._cmButton.setText("Centimeters")
        self._buttonGroup = qt.QButtonGroup(self)
        self._buttonGroup.addButton(self._pageButton)
        self._buttonGroup.addButton(self._inchButton)
        self._buttonGroup.addButton(self._cmButton)
        self._buttonGroup.setExclusive(True)

        # units
        self.mainLayout.addWidget(label, 0, 0, 1, 4)
        hboxLayout.addWidget(self._pageButton)
        hboxLayout.addWidget(self._inchButton)
        hboxLayout.addWidget(self._cmButton)
        self.mainLayout.addWidget(hbox, 1, 0, 1, 4)
        self._pageButton.setChecked(True)

        # xOffset
        label = qt.QLabel(self)
        label.setText("X Offset:")
        self.mainLayout.addWidget(label, 2, 0)
        self._xOffset = FloatEdit(self, 0.1)
        self.mainLayout.addWidget(self._xOffset, 2, 1)

        # yOffset
        label = qt.QLabel(self)
        label.setText("Y Offset:")
        self.mainLayout.addWidget(label, 2, 2)
        self._yOffset = FloatEdit(self, 0.1)
        self.mainLayout.addWidget(self._yOffset, 2, 3)

        # width
        label = qt.QLabel(self)
        label.setText("Width:")
        self.mainLayout.addWidget(label, 3, 0)
        self._width = FloatEdit(self, 0.9)
        self.mainLayout.addWidget(self._width, 3, 1)

        # height
        label = qt.QLabel(self)
        label.setText("Height:")
        self.mainLayout.addWidget(label, 3, 2)
        self._height = FloatEdit(self, 0.9)
        self.mainLayout.addWidget(self._height, 3, 3)

        # aspect ratio
        self._aspect = qt.QCheckBox(self)
        self._aspect.setText("Keep screen aspect ratio")
        self._aspect.setChecked(True)
        self.mainLayout.addWidget(self._aspect, 4, 1, 1, 2)

    def getPrintGeometry(self):
        """Return the print geometry dictionary.

        See :meth:`setPrintGeometry` for documentation about the
        print geometry dictionary."""
        ddict = {}
        if self._inchButton.isChecked():
            ddict['units'] = "inches"
        elif self._cmButton.isChecked():
            ddict['units'] = "centimeters"
        else:
            ddict['units'] = "page"

        ddict['xOffset'] = self._xOffset.value()
        ddict['yOffset'] = self._yOffset.value()
        ddict['width'] = self._width.value()
        ddict['height'] = self._height.value()

        if self._aspect.isChecked():
            ddict['keepAspectRatio'] = True
        else:
            ddict['keepAspectRatio'] = False
        return ddict

    def setPrintGeometry(self, geometry=None):
        """Set the print geometry.

        The geometry parameters must be provided as a dictionary with
        the following keys:

         - *"xOffset"* (float)
         - *"yOffset"* (float)
         - *"width"* (float)
         - *"height"* (float)
         - *"units"*: possible values *"page", "inch", "cm"*
         - *"keepAspectRatio"*: *True* or *False*

        If *units* is *"page"*, the values should be floats in [0, 1.]
        and are interpreted as a fraction of the page width or height.

        :param dict geometry: Geometry parameters, as a dictionary."""
        if geometry is None:
            geometry = {}
        oldDict = self.getPrintGeometry()
        for key in ["units", "xOffset", "yOffset",
                    "width", "height", "keepAspectRatio"]:
            geometry[key] = geometry.get(key, oldDict[key])

        if geometry['units'].lower().startswith("inc"):
            self._inchButton.setChecked(True)
        elif geometry['units'].lower().startswith("c"):
            self._cmButton.setChecked(True)
        else:
            self._pageButton.setChecked(True)

        self._xOffset.setText("%s" % float(geometry['xOffset']))
        self._yOffset.setText("%s" % float(geometry['yOffset']))
        self._width.setText("%s" % float(geometry['width']))
        self._height.setText("%s" % float(geometry['height']))
        if geometry['keepAspectRatio']:
            self._aspect.setChecked(True)
        else:
            self._aspect.setChecked(False)
예제 #7
0
파일: PlotTools.py 프로젝트: fangohr/silx
class LimitsToolBar(qt.QToolBar):
    """QToolBar displaying and controlling the limits of a :class:`PlotWidget`.

    To run the following sample code, a QApplication must be initialized.
    First, create a PlotWindow:

    >>> from silx.gui.plot import PlotWindow
    >>> plot = PlotWindow()  # Create a PlotWindow to add the toolbar to

    Then, create the LimitsToolBar and add it to the PlotWindow.

    >>> from silx.gui import qt
    >>> from silx.gui.plot.PlotTools import LimitsToolBar

    >>> toolbar = LimitsToolBar(plot=plot)  # Create the toolbar
    >>> plot.addToolBar(qt.Qt.BottomToolBarArea, toolbar)  # Add it to the plot
    >>> plot.show()  # To display the PlotWindow with the limits toolbar

    :param parent: See :class:`QToolBar`.
    :param plot: :class:`PlotWidget` instance on which to operate.
    :param str title: See :class:`QToolBar`.
    """
    def __init__(self, parent=None, plot=None, title='Limits'):
        super(LimitsToolBar, self).__init__(title, parent)
        assert plot is not None
        self._plot = plot
        self._plot.sigPlotSignal.connect(self._plotWidgetSlot)

        self._initWidgets()

    @property
    def plot(self):
        """The :class:`PlotWidget` the toolbar is attached to."""
        return self._plot

    def _initWidgets(self):
        """Create and init Toolbar widgets."""
        xMin, xMax = self.plot.getXAxis().getLimits()
        yMin, yMax = self.plot.getYAxis().getLimits()

        self.addWidget(qt.QLabel('Limits: '))
        self.addWidget(qt.QLabel(' X: '))
        self._xMinFloatEdit = FloatEdit(self, xMin)
        self._xMinFloatEdit.editingFinished[()].connect(
            self._xFloatEditChanged)
        self.addWidget(self._xMinFloatEdit)

        self._xMaxFloatEdit = FloatEdit(self, xMax)
        self._xMaxFloatEdit.editingFinished[()].connect(
            self._xFloatEditChanged)
        self.addWidget(self._xMaxFloatEdit)

        self.addWidget(qt.QLabel(' Y: '))
        self._yMinFloatEdit = FloatEdit(self, yMin)
        self._yMinFloatEdit.editingFinished[()].connect(
            self._yFloatEditChanged)
        self.addWidget(self._yMinFloatEdit)

        self._yMaxFloatEdit = FloatEdit(self, yMax)
        self._yMaxFloatEdit.editingFinished[()].connect(
            self._yFloatEditChanged)
        self.addWidget(self._yMaxFloatEdit)

    def _plotWidgetSlot(self, event):
        """Listen to :class:`PlotWidget` events."""
        if event['event'] not in ('limitsChanged', ):
            return

        xMin, xMax = self.plot.getXAxis().getLimits()
        yMin, yMax = self.plot.getYAxis().getLimits()

        self._xMinFloatEdit.setValue(xMin)
        self._xMaxFloatEdit.setValue(xMax)
        self._yMinFloatEdit.setValue(yMin)
        self._yMaxFloatEdit.setValue(yMax)

    def _xFloatEditChanged(self):
        """Handle X limits changed from the GUI."""
        xMin, xMax = self._xMinFloatEdit.value(), self._xMaxFloatEdit.value()
        if xMax < xMin:
            xMin, xMax = xMax, xMin

        self.plot.getXAxis().setLimits(xMin, xMax)

    def _yFloatEditChanged(self):
        """Handle Y limits changed from the GUI."""
        yMin, yMax = self._yMinFloatEdit.value(), self._yMaxFloatEdit.value()
        if yMax < yMin:
            yMin, yMax = yMax, yMin

        self.plot.getYAxis().setLimits(yMin, yMax)
예제 #8
0
class _BoundaryWidget(qt.QWidget):
    """Widget to edit a boundary of the colormap (vmin, vmax)"""
    sigValueChanged = qt.Signal(object)
    """Signal emitted when value is changed"""

    def __init__(self, parent=None, value=0.0):
        qt.QWidget.__init__(self, parent=None)
        self.setLayout(qt.QHBoxLayout())
        self.layout().setContentsMargins(0, 0, 0, 0)
        self._numVal = FloatEdit(parent=self, value=value)
        self.layout().addWidget(self._numVal)
        self._autoCB = qt.QCheckBox('auto', parent=self)
        self.layout().addWidget(self._autoCB)
        self._autoCB.setChecked(False)

        self._autoCB.toggled.connect(self._autoToggled)
        self.sigValueChanged = self._autoCB.toggled
        self.textEdited = self._numVal.textEdited
        self.editingFinished = self._numVal.editingFinished
        self._dataValue = None

    def isAutoChecked(self):
        return self._autoCB.isChecked()

    def getValue(self):
        return None if self._autoCB.isChecked() else self._numVal.value()

    def getFiniteValue(self):
        if not self._autoCB.isChecked():
            return self._numVal.value()
        elif self._dataValue is None:
            return self._numVal.value()
        else:
            return self._dataValue

    def _autoToggled(self, enabled):
        self._numVal.setEnabled(not enabled)
        self._updateDisplayedText()

    def _updateDisplayedText(self):
        # if dataValue is finite
        if self._autoCB.isChecked() and self._dataValue is not None:
            old = self._numVal.blockSignals(True)
            self._numVal.setValue(self._dataValue)
            self._numVal.blockSignals(old)

    def setDataValue(self, dataValue):
        self._dataValue = dataValue
        self._updateDisplayedText()

    def setFiniteValue(self, value):
        assert(value is not None)
        old = self._numVal.blockSignals(True)
        self._numVal.setValue(value)
        self._numVal.blockSignals(old)

    def setValue(self, value, isAuto=False):
        self._autoCB.setChecked(isAuto or value is None)
        if value is not None:
            self._numVal.setValue(value)
        self._updateDisplayedText()
class PrintGeometryWidget(qt.QWidget):
    """Widget to specify the size and aspect ratio of an item
    before sending it to the print preview dialog.

    Use methods :meth:`setPrintGeometry` and :meth:`getPrintGeometry`
    to interact with the widget.
    """
    def __init__(self, parent=None):
        super(PrintGeometryWidget, self).__init__(parent)
        self.mainLayout = qt.QGridLayout(self)
        self.mainLayout.setContentsMargins(0, 0, 0, 0)
        self.mainLayout.setSpacing(2)
        hbox = qt.QWidget(self)
        hboxLayout = qt.QHBoxLayout(hbox)
        hboxLayout.setContentsMargins(0, 0, 0, 0)
        hboxLayout.setSpacing(2)
        label = qt.QLabel(self)
        label.setText("Units")
        label.setAlignment(qt.Qt.AlignCenter)
        self._pageButton = qt.QRadioButton()
        self._pageButton.setText("Page")
        self._inchButton = qt.QRadioButton()
        self._inchButton.setText("Inches")
        self._cmButton = qt.QRadioButton()
        self._cmButton.setText("Centimeters")
        self._buttonGroup = qt.QButtonGroup(self)
        self._buttonGroup.addButton(self._pageButton)
        self._buttonGroup.addButton(self._inchButton)
        self._buttonGroup.addButton(self._cmButton)
        self._buttonGroup.setExclusive(True)

        # units
        self.mainLayout.addWidget(label, 0, 0, 1, 4)
        hboxLayout.addWidget(self._pageButton)
        hboxLayout.addWidget(self._inchButton)
        hboxLayout.addWidget(self._cmButton)
        self.mainLayout.addWidget(hbox, 1, 0, 1, 4)
        self._pageButton.setChecked(True)

        # xOffset
        label = qt.QLabel(self)
        label.setText("X Offset:")
        self.mainLayout.addWidget(label, 2, 0)
        self._xOffset = FloatEdit(self, 0.1)
        self.mainLayout.addWidget(self._xOffset, 2, 1)

        # yOffset
        label = qt.QLabel(self)
        label.setText("Y Offset:")
        self.mainLayout.addWidget(label, 2, 2)
        self._yOffset = FloatEdit(self, 0.1)
        self.mainLayout.addWidget(self._yOffset, 2, 3)

        # width
        label = qt.QLabel(self)
        label.setText("Width:")
        self.mainLayout.addWidget(label, 3, 0)
        self._width = FloatEdit(self, 0.9)
        self.mainLayout.addWidget(self._width, 3, 1)

        # height
        label = qt.QLabel(self)
        label.setText("Height:")
        self.mainLayout.addWidget(label, 3, 2)
        self._height = FloatEdit(self, 0.9)
        self.mainLayout.addWidget(self._height, 3, 3)

        # aspect ratio
        self._aspect = qt.QCheckBox(self)
        self._aspect.setText("Keep screen aspect ratio")
        self._aspect.setChecked(True)
        self.mainLayout.addWidget(self._aspect, 4, 1, 1, 2)

    def getPrintGeometry(self):
        """Return the print geometry dictionary.

        See :meth:`setPrintGeometry` for documentation about the
        print geometry dictionary."""
        ddict = {}
        if self._inchButton.isChecked():
            ddict['units'] = "inches"
        elif self._cmButton.isChecked():
            ddict['units'] = "centimeters"
        else:
            ddict['units'] = "page"

        ddict['xOffset'] = self._xOffset.value()
        ddict['yOffset'] = self._yOffset.value()
        ddict['width'] = self._width.value()
        ddict['height'] = self._height.value()

        if self._aspect.isChecked():
            ddict['keepAspectRatio'] = True
        else:
            ddict['keepAspectRatio'] = False
        return ddict

    def setPrintGeometry(self, geometry=None):
        """Set the print geometry.

        The geometry parameters must be provided as a dictionary with
        the following keys:

         - *"xOffset"* (float)
         - *"yOffset"* (float)
         - *"width"* (float)
         - *"height"* (float)
         - *"units"*: possible values *"page", "inch", "cm"*
         - *"keepAspectRatio"*: *True* or *False*

        If *units* is *"page"*, the values should be floats in [0, 1.]
        and are interpreted as a fraction of the page width or height.

        :param dict geometry: Geometry parameters, as a dictionary."""
        if geometry is None:
            geometry = {}
        oldDict = self.getPrintGeometry()
        for key in [
                "units", "xOffset", "yOffset", "width", "height",
                "keepAspectRatio"
        ]:
            geometry[key] = geometry.get(key, oldDict[key])

        if geometry['units'].lower().startswith("inc"):
            self._inchButton.setChecked(True)
        elif geometry['units'].lower().startswith("c"):
            self._cmButton.setChecked(True)
        else:
            self._pageButton.setChecked(True)

        self._xOffset.setText("%s" % float(geometry['xOffset']))
        self._yOffset.setText("%s" % float(geometry['yOffset']))
        self._width.setText("%s" % float(geometry['width']))
        self._height.setText("%s" % float(geometry['height']))
        if geometry['keepAspectRatio']:
            self._aspect.setChecked(True)
        else:
            self._aspect.setChecked(False)
예제 #10
0
class _AmplitudeRangeDialog(qt.QDialog):
    """QDialog asking for the amplitude range to display."""

    sigRangeChanged = qt.Signal(tuple)
    """Signal emitted when the range has changed.

    It provides the new range as a 2-tuple: (max, delta)
    """

    def __init__(self,
                 parent=None,
                 amplitudeRange=None,
                 displayedRange=(None, 2)):
        super(_AmplitudeRangeDialog, self).__init__(parent)
        self.setWindowTitle('Set Displayed Amplitude Range')

        if amplitudeRange is not None:
            amplitudeRange = min(amplitudeRange), max(amplitudeRange)
        self._amplitudeRange = amplitudeRange
        self._defaultDisplayedRange = displayedRange

        layout = qt.QFormLayout()
        self.setLayout(layout)

        if self._amplitudeRange is not None:
            min_, max_ = self._amplitudeRange
            layout.addRow(
                qt.QLabel('Data Amplitude Range: [%g, %g]' % (min_, max_)))

        self._maxLineEdit = FloatEdit(parent=self)
        self._maxLineEdit.validator().setBottom(0.)
        self._maxLineEdit.setAlignment(qt.Qt.AlignRight)

        self._maxLineEdit.editingFinished.connect(self._rangeUpdated)
        layout.addRow('Displayed Max.:', self._maxLineEdit)

        self._autoscale = qt.QCheckBox('autoscale')
        self._autoscale.toggled.connect(self._autoscaleCheckBoxToggled)
        layout.addRow('', self._autoscale)

        self._deltaLineEdit = FloatEdit(parent=self)
        self._deltaLineEdit.validator().setBottom(1.)
        self._deltaLineEdit.setAlignment(qt.Qt.AlignRight)
        self._deltaLineEdit.editingFinished.connect(self._rangeUpdated)
        layout.addRow('Displayed delta (log10 unit):', self._deltaLineEdit)

        buttons = qt.QDialogButtonBox(self)
        buttons.addButton(qt.QDialogButtonBox.Ok)
        buttons.addButton(qt.QDialogButtonBox.Cancel)
        buttons.accepted.connect(self.accept)
        buttons.rejected.connect(self.reject)
        layout.addRow(buttons)

        # Set dialog from default values
        self._resetDialogToDefault()

        self.rejected.connect(self._handleRejected)

    def _resetDialogToDefault(self):
        """Set Widgets of the dialog from range information
        """
        max_, delta = self._defaultDisplayedRange

        if max_ is not None:  # Not in autoscale
            displayedMax = max_
        elif self._amplitudeRange is not None:  # Autoscale with data
            displayedMax = self._amplitudeRange[1]
        else:  # Autoscale without data
            displayedMax = ''
        if displayedMax == "":
            self._maxLineEdit.setText("")
        else:
            self._maxLineEdit.setValue(displayedMax)
        self._maxLineEdit.setEnabled(max_ is not None)

        self._deltaLineEdit.setValue(delta)

        self._autoscale.setChecked(self._defaultDisplayedRange[0] is None)

    def getRangeInfo(self):
        """Returns the current range as a 2-tuple (max, delta (in log10))"""
        if self._autoscale.isChecked():
            max_ = None
        else:
            maxStr = self._maxLineEdit.text()
            max_ = self._maxLineEdit.value() if maxStr else None
        return max_, self._deltaLineEdit.value() if self._deltaLineEdit.text() else 2

    def _handleRejected(self):
        """Reset range info to default when rejected"""
        self._resetDialogToDefault()
        self._rangeUpdated()

    def _rangeUpdated(self):
        """Handle QLineEdit editing finised"""
        self.sigRangeChanged.emit(self.getRangeInfo())

    def _autoscaleCheckBoxToggled(self, checked):
        """Handle autoscale checkbox state changes"""
        if checked:  # Use default values
            if self._amplitudeRange is None:
                max_ = ''
            else:
                max_ = self._amplitudeRange[1]
            if max_ == "":
                self._maxLineEdit.setText("")
            else:
                self._maxLineEdit.setValue(max_)
        self._maxLineEdit.setEnabled(not checked)
        self._rangeUpdated()