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()
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()
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()
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)
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)
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)
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)