diff options
author | Picca Frédéric-Emmanuel <picca@synchrotron-soleil.fr> | 2017-08-18 14:48:52 +0200 |
---|---|---|
committer | Picca Frédéric-Emmanuel <picca@synchrotron-soleil.fr> | 2017-08-18 14:48:52 +0200 |
commit | f7bdc2acff3c13a6d632c28c4569690ab106eed7 (patch) | |
tree | 9d67cdb7152ee4e711379e03fe0546c7c3b97303 /silx/gui/plot3d/SFViewParamTree.py |
Import Upstream version 0.5.0+dfsg
Diffstat (limited to 'silx/gui/plot3d/SFViewParamTree.py')
-rw-r--r-- | silx/gui/plot3d/SFViewParamTree.py | 1467 |
1 files changed, 1467 insertions, 0 deletions
diff --git a/silx/gui/plot3d/SFViewParamTree.py b/silx/gui/plot3d/SFViewParamTree.py new file mode 100644 index 0000000..38d4e37 --- /dev/null +++ b/silx/gui/plot3d/SFViewParamTree.py @@ -0,0 +1,1467 @@ +# coding: utf-8 +# /*########################################################################## +# +# Copyright (c) 2015-2017 European Synchrotron Radiation Facility +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +# +# ###########################################################################*/ +""" +This module provides a tree widget to set/view parameters of a ScalarFieldView. +""" + +from __future__ import absolute_import + +__authors__ = ["D. N."] +__license__ = "MIT" +__date__ = "10/01/2017" + +import logging +import sys + +import numpy + +from silx.gui import qt +from silx.gui.icons import getQIcon + +from .ScalarFieldView import Isosurface + + +_logger = logging.getLogger(__name__) + + +class ModelColumns(object): + NameColumn, ValueColumn, ColumnMax = range(3) + ColumnNames = ['Name', 'Value'] + + +class SubjectItem(qt.QStandardItem): + """ + Base class for observers items. + + Subclassing: + ------------ + The following method can/should be reimplemented: + - _init + - _pullData + - _pushData + - _setModelData + - _subjectChanged + - getEditor + - getSignals + - leftClicked + - queryRemove + - setEditorData + + Also the following attributes are available: + - editable + - persistent + + :param subject: object that this item will be observing. + """ + + editable = False + """ boolean: set to True to make the item editable. """ + + persistent = False + """ + boolean: set to True to make the editor persistent. + See : Qt.QAbstractItemView.openPersistentEditor + """ + + def __init__(self, subject, *args): + + super(SubjectItem, self).__init__(*args) + + self.setEditable(self.editable) + + self.__subject = None + self.subject = subject + + def setData(self, value, role=qt.Qt.UserRole, pushData=True): + """ + Overloaded method from QStandardItem. The pushData keyword tells + the item to push data to the subject if the role is equal to EditRole. + This is useful to let this method know if the setData method was called + internaly or from the view. + + :param value: the value ti set to data + :param role: role in the item + :param pushData: if True push value in the existing data. + """ + if role == qt.Qt.EditRole and pushData: + setValue = self._pushData(value, role) + if setValue != value: + value = setValue + super(SubjectItem, self).setData(value, role) + + subject = property(lambda self: self.__subject) + + @subject.setter + def subject(self, subject): + if self.__subject is not None: + raise ValueError('Subject already set ' + ' (subject change not supported).') + self.__subject = subject + if subject is not None: + self._init() + self._connectSignals() + + def _connectSignals(self): + """ + Connects the signals. Called when the subject is set. + """ + + def gen_slot(_sigIdx): + def slotfn(*args, **kwargs): + self._subjectChanged(signalIdx=_sigIdx, + args=args, + kwargs=kwargs) + return slotfn + + if self.__subject is not None: + self.__slots = slots = [] + + signals = self.getSignals() + + if signals: + if not isinstance(signals, (list, tuple)): + signals = [signals] + for sigIdx, signal in enumerate(signals): + slot = gen_slot(sigIdx) + signal.connect(slot) + slots.append((signal, slot)) + + def _disconnectSignals(self): + """ + Disconnects all subject's signal + """ + if self.__slots: + for signal, slot in self.__slots: + try: + signal.disconnect(slot) + except TypeError: + pass + + def _enableRow(self, enable): + """ + Set the enabled state for this cell, or for the whole row + if this item has a parent. + + :param bool enable: True if we wan't to enable the cell + """ + parent = self.parent() + model = self.model() + if model is None or parent is None: + # no parent -> no siblings + self.setEnabled(enable) + return + + for col in range(model.columnCount()): + sibling = parent.child(self.row(), col) + sibling.setEnabled(enable) + + ################################################################# + # Overloadable methods + ################################################################# + + def getSignals(self): + """ + Returns the list of this items subject's signals that + this item will be listening to. + + :return: list. + """ + return None + + def _subjectChanged(self, signalIdx=None, args=None, kwargs=None): + """ + Called when one of the signals is triggered. Default implementation + just calls _pullData, compares the result to the current value stored + as Qt.EditRole, and stores the new value if it is different. It also + stores its str representation as Qt.DisplayRole + + :param signalIdx: index of the triggered signal. The value passed + is the same as the signal position in the list returned by + SubjectItem.getSignals. + :param args: arguments received from the signal + :param kwargs: keyword arguments received from the signal + """ + data = self._pullData() + if data == self.data(qt.Qt.EditRole): + return + self.setData(data, role=qt.Qt.DisplayRole, pushData=False) + self.setData(data, role=qt.Qt.EditRole, pushData=False) + + def _pullData(self): + """ + Pulls data from the subject. + + :return: subject data + """ + return None + + def _pushData(self, value, role=qt.Qt.UserRole): + """ + Pushes data to the subject and returns the actual value that was stored + + :return: the value that was stored + """ + return value + + def _init(self): + """ + Called when the subject is set. + :return: + """ + self._subjectChanged() + + def getEditor(self, parent, option, index): + """ + Returns the editor widget used to edit this item's data. The arguments + are the one passed to the QStyledItemDelegate.createEditor method. + + :param parent: the Qt parent of the editor + :param option: + :param index: + :return: + """ + return None + + def setEditorData(self, editor): + """ + This is called by the View's delegate just before the editor is shown, + its purpose it to setup the editors contents. Return False to use + the delegate's default behaviour. + + :param editor: + :return: + """ + return True + + def _setModelData(self, editor): + """ + This is called by the View's delegate just before the editor is closed, + its allows this item to update itself with data from the editor. + + :param editor: + :return: + """ + return False + + def queryRemove(self, view=None): + """ + This is called by the view to ask this items if it (the view) can + remove it. Return True to let the view know that the item can be + removed. + + :param view: + :return: + """ + return False + + def leftClicked(self): + """ + This method is called by the view when the item's cell if left clicked. + + :return: + """ + pass + + +# View settings ############################################################### + +class ColorItem(SubjectItem): + """color item.""" + editable = True + persistent = True + + def getEditor(self, parent, option, index): + editor = QColorEditor(parent) + editor.color = self.getColor() + editor.sigColorChanged.connect(self._editorSlot) + return editor + + def _editorSlot(self, color): + self.setData(color, qt.Qt.EditRole) + + def _pushData(self, value, role=qt.Qt.UserRole): + self.setColor(value) + return self.getColor() + + def _pullData(self): + self.getColor() + + def setColor(self, color): + """Override to implement actual color setter""" + pass + + +class BackgroundColorItem(ColorItem): + itemName = 'Background' + + def setColor(self, color): + self.subject.setBackgroundColor(color) + + def getColor(self): + return self.subject.getBackgroundColor() + + +class ForegroundColorItem(ColorItem): + itemName = 'Foreground' + + def setColor(self, color): + self.subject.setForegroundColor(color) + + def getColor(self): + return self.subject.getForegroundColor() + + +class HighlightColorItem(ColorItem): + itemName = 'Highlight' + + def setColor(self, color): + self.subject.setHighlightColor(color) + + def getColor(self): + return self.subject.getHighlightColor() + + +class ViewSettingsItem(qt.QStandardItem): + """Viewport settings""" + + def __init__(self, subject, *args): + + super(ViewSettingsItem, self).__init__(*args) + + self.setEditable(False) + + classes = BackgroundColorItem, ForegroundColorItem, HighlightColorItem + for cls in classes: + titleItem = qt.QStandardItem(cls.itemName) + titleItem.setEditable(False) + self.appendRow([titleItem, cls(subject)]) + + +# Data information ############################################################ + +class DataChangedItem(SubjectItem): + """ + Base class for items listening to ScalarFieldView.sigDataChanged + """ + + def getSignals(self): + subject = self.subject + if subject: + return subject.sigDataChanged + return None + + def _init(self): + self._subjectChanged() + + +class DataTypeItem(DataChangedItem): + itemName = 'dtype' + + def _pullData(self): + data = self.subject.getData(copy=False) + return ((data is not None) and str(data.dtype)) or 'N/A' + + +class DataShapeItem(DataChangedItem): + itemName = 'size' + + def _pullData(self): + data = self.subject.getData(copy=False) + if data is None: + return 'N/A' + else: + return str(list(reversed(data.shape))) + + +class OffsetItem(DataChangedItem): + itemName = 'offset' + + def _pullData(self): + offset = self.subject.getTranslation() + return ((offset is not None) and str(offset)) or 'N/A' + + +class ScaleItem(DataChangedItem): + itemName = 'scale' + + def _pullData(self): + scale = self.subject.getScale() + return ((scale is not None) and str(scale)) or 'N/A' + + +class DataSetItem(qt.QStandardItem): + + def __init__(self, subject, *args): + + super(DataSetItem, self).__init__(*args) + + self.setEditable(False) + + klasses = [DataTypeItem, DataShapeItem, OffsetItem, ScaleItem] + for klass in klasses: + titleItem = qt.QStandardItem(klass.itemName) + titleItem.setEditable(False) + self.appendRow([titleItem, klass(subject)]) + + +# Isosurface ################################################################## + +class IsoSurfaceRootItem(SubjectItem): + """ + Root (i.e : column index 0) Isosurface item. + """ + + def getSignals(self): + subject = self.subject + return [subject.sigColorChanged, + subject.sigVisibilityChanged] + + def _subjectChanged(self, signalIdx=None, args=None, kwargs=None): + if signalIdx == 0: + color = self.subject.getColor() + self.setData(color, qt.Qt.DecorationRole) + elif signalIdx == 1: + visible = args[0] + self.setCheckState((visible and qt.Qt.Checked) or qt.Qt.Unchecked) + + def _init(self): + self.setCheckable(True) + + isosurface = self.subject + color = isosurface.getColor() + visible = isosurface.isVisible() + self.setData(color, qt.Qt.DecorationRole) + self.setCheckState((visible and qt.Qt.Checked) or qt.Qt.Unchecked) + + nameItem = qt.QStandardItem('Level') + sliderItem = IsoSurfaceLevelSlider(self.subject) + self.appendRow([nameItem, sliderItem]) + + nameItem = qt.QStandardItem('Color') + nameItem.setEditable(False) + valueItem = IsoSurfaceColorItem(self.subject) + self.appendRow([nameItem, valueItem]) + + nameItem = qt.QStandardItem('Opacity') + nameItem.setTextAlignment(qt.Qt.AlignLeft | qt.Qt.AlignTop) + nameItem.setEditable(False) + valueItem = IsoSurfaceAlphaItem(self.subject) + self.appendRow([nameItem, valueItem]) + + nameItem = qt.QStandardItem() + nameItem.setEditable(False) + valueItem = IsoSurfaceAlphaLegendItem(self.subject) + valueItem.setEditable(False) + self.appendRow([nameItem, valueItem]) + + def queryRemove(self, view=None): + buttons = qt.QMessageBox.Ok | qt.QMessageBox.Cancel + ans = qt.QMessageBox.question(view, + 'Remove isosurface', + 'Remove the selected iso-surface?', + buttons=buttons) + if ans == qt.QMessageBox.Ok: + sfview = self.subject.parent() + if sfview: + sfview.removeIsosurface(self.subject) + return False + return False + + def leftClicked(self): + checked = (self.checkState() == qt.Qt.Checked) + visible = self.subject.isVisible() + if checked != visible: + self.subject.setVisible(checked) + + +class IsoSurfaceLevelItem(SubjectItem): + """ + Base class for the isosurface level items. + """ + editable = True + + def getSignals(self): + subject = self.subject + return [subject.sigLevelChanged, + subject.sigVisibilityChanged] + + def setEditorData(self, editor): + return False + + def _pullData(self): + return self.subject.getLevel() + + def _pushData(self, value, role=qt.Qt.UserRole): + self.subject.setLevel(value) + return self.subject.getLevel() + + +class _IsoLevelSlider(qt.QSlider): + """QSlider used for iso-surface level""" + + def __init__(self, parent, subject): + super(_IsoLevelSlider, self).__init__(parent=parent) + self.subject = subject + + self.sliderReleased.connect(self.__sliderReleased) + + self.subject.sigLevelChanged.connect(self.setLevel) + self.subject.parent().sigDataChanged.connect(self.__dataChanged) + + def setLevel(self, level): + """Set slider from iso-surface level""" + dataRange = self.subject.parent().getDataRange() + + if dataRange is not None and None not in dataRange: + width = dataRange[1] - dataRange[0] + if width > 0: + sliderWidth = self.maximum() - self.minimum() + sliderPosition = sliderWidth * (level - dataRange[0]) / width + self.setValue(sliderPosition) + + def __dataChanged(self): + """Handles data update to refresh slider range if needed""" + self.setLevel(self.subject.getLevel()) + + def __sliderReleased(self): + value = self.value() + dataRange = self.subject.parent().getDataRange() + width = dataRange[1] - dataRange[0] + sliderWidth = self.maximum() - self.minimum() + level = dataRange[0] + width * value / sliderWidth + self.subject.setLevel(level) + + +class IsoSurfaceLevelSlider(IsoSurfaceLevelItem): + """ + Isosurface level item with a slider editor. + """ + nTicks = 1000 + persistent = True + + def getEditor(self, parent, option, index): + editor = _IsoLevelSlider(parent, self.subject) + editor.setOrientation(qt.Qt.Horizontal) + editor.setMinimum(0) + editor.setMaximum(self.nTicks) + + editor.setSingleStep(1) + + editor.setLevel(self.subject.getLevel()) + return editor + + def setEditorData(self, editor): + return True + + def _setModelData(self, editor): + return True + + +class IsoSurfaceColorItem(SubjectItem): + """ + Isosurface color item. + """ + editable = True + persistent = True + + def getSignals(self): + return self.subject.sigColorChanged + + def getEditor(self, parent, option, index): + editor = QColorEditor(parent) + color = self.subject.getColor() + color.setAlpha(255) + editor.color = color + editor.sigColorChanged.connect(self.__editorChanged) + return editor + + def __editorChanged(self, color): + color.setAlpha(self.subject.getColor().alpha()) + self.subject.setColor(color) + + def _pushData(self, value, role=qt.Qt.UserRole): + self.subject.setColor(value) + return self.subject.getColor() + + +class QColorEditor(qt.QWidget): + """ + QColor editor. + """ + sigColorChanged = qt.Signal(object) + + color = property(lambda self: qt.QColor(self.__color)) + + @color.setter + def color(self, color): + self._setColor(color) + self.__previousColor = color + + def __init__(self, *args, **kwargs): + super(QColorEditor, self).__init__(*args, **kwargs) + layout = qt.QHBoxLayout(self) + layout.setContentsMargins(0, 0, 0, 0) + button = qt.QToolButton() + icon = qt.QIcon(qt.QPixmap(32, 32)) + button.setIcon(icon) + layout.addWidget(button) + button.clicked.connect(self.__showColorDialog) + layout.addStretch(1) + + self.__color = None + self.__previousColor = None + + def sizeHint(self): + return qt.QSize(0, 0) + + def _setColor(self, qColor): + button = self.findChild(qt.QToolButton) + pixmap = qt.QPixmap(32, 32) + pixmap.fill(qColor) + button.setIcon(qt.QIcon(pixmap)) + self.__color = qColor + + def __showColorDialog(self): + dialog = qt.QColorDialog(parent=self) + if sys.platform == 'darwin': + # Use of native color dialog on macos might cause problems + dialog.setOption(qt.QColorDialog.DontUseNativeDialog, True) + + self.__previousColor = self.__color + dialog.setAttribute(qt.Qt.WA_DeleteOnClose) + dialog.setModal(True) + dialog.currentColorChanged.connect(self.__colorChanged) + dialog.finished.connect(self.__dialogClosed) + dialog.show() + + def __colorChanged(self, color): + self.__color = color + self._setColor(color) + self.sigColorChanged.emit(color) + + def __dialogClosed(self, result): + if result == qt.QDialog.Rejected: + self.__colorChanged(self.__previousColor) + self.__previousColor = None + + +class IsoSurfaceAlphaItem(SubjectItem): + """ + Isosurface alpha item. + """ + editable = True + persistent = True + + def _init(self): + pass + + def getSignals(self): + return self.subject.sigColorChanged + + def getEditor(self, parent, option, index): + editor = qt.QSlider(parent) + editor.setOrientation(qt.Qt.Horizontal) + editor.setMinimum(0) + editor.setMaximum(255) + + color = self.subject.getColor() + editor.setValue(color.alpha()) + + editor.valueChanged.connect(self.__editorChanged) + + return editor + + def __editorChanged(self, value): + color = self.subject.getColor() + color.setAlpha(value) + self.subject.setColor(color) + + def setEditorData(self, editor): + return True + + def _setModelData(self, editor): + return True + + +class IsoSurfaceAlphaLegendItem(SubjectItem): + """Legend to place under opacity slider""" + + editable = False + persistent = True + + def getEditor(self, parent, option, index): + layout = qt.QHBoxLayout() + layout.setContentsMargins(0, 0, 0, 0) + layout.setSpacing(0) + layout.addWidget(qt.QLabel('0')) + layout.addStretch(1) + layout.addWidget(qt.QLabel('1')) + + editor = qt.QWidget(parent) + editor.setLayout(layout) + return editor + + +class IsoSurfaceCount(SubjectItem): + """ + Item displaying the number of isosurfaces. + """ + + def getSignals(self): + subject = self.subject + return [subject.sigIsosurfaceAdded, subject.sigIsosurfaceRemoved] + + def _pullData(self): + return len(self.subject.getIsosurfaces()) + + +class IsoSurfaceAddRemoveWidget(qt.QWidget): + + sigViewTask = qt.Signal(str) + """Signal for the tree view to perform some task""" + + def __init__(self, parent, item): + super(IsoSurfaceAddRemoveWidget, self).__init__(parent) + self._item = item + layout = qt.QHBoxLayout(self) + layout.setContentsMargins(0, 0, 0, 0) + layout.setSpacing(0) + + addBtn = qt.QToolButton() + addBtn.setText('+') + addBtn.setToolButtonStyle(qt.Qt.ToolButtonTextOnly) + layout.addWidget(addBtn) + addBtn.clicked.connect(self.__addClicked) + + removeBtn = qt.QToolButton() + removeBtn.setText('-') + removeBtn.setToolButtonStyle(qt.Qt.ToolButtonTextOnly) + layout.addWidget(removeBtn) + removeBtn.clicked.connect(self.__removeClicked) + + layout.addStretch(1) + + def __addClicked(self): + sfview = self._item.subject + if not sfview: + return + dataRange = sfview.getDataRange() + if dataRange is None: + dataRange = [0, 1] + + sfview.addIsosurface(numpy.mean(dataRange), '#0000FF') + + def __removeClicked(self): + self.sigViewTask.emit('remove_iso') + + +class IsoSurfaceAddRemoveItem(SubjectItem): + """ + Item displaying a simple QToolButton allowing to add an isosurface. + """ + persistent = True + + def getEditor(self, parent, option, index): + return IsoSurfaceAddRemoveWidget(parent, self) + + +class IsoSurfaceGroup(SubjectItem): + """ + Root item for the list of isosurface items. + """ + def getSignals(self): + subject = self.subject + return [subject.sigIsosurfaceAdded, subject.sigIsosurfaceRemoved] + + def _subjectChanged(self, signalIdx=None, args=None, kwargs=None): + if signalIdx == 0: + if len(args) >= 1: + isosurface = args[0] + if not isinstance(isosurface, Isosurface): + raise ValueError('Expected an isosurface instance.') + self.__addIsosurface(isosurface) + else: + raise ValueError('Expected an isosurface instance.') + elif signalIdx == 1: + if len(args) >= 1: + isosurface = args[0] + if not isinstance(isosurface, Isosurface): + raise ValueError('Expected an isosurface instance.') + self.__removeIsosurface(isosurface) + else: + raise ValueError('Expected an isosurface instance.') + + def __addIsosurface(self, isosurface): + valueItem = IsoSurfaceRootItem(subject=isosurface) + nameItem = IsoSurfaceLevelItem(subject=isosurface) + self.insertRow(max(0, self.rowCount() - 1), [valueItem, nameItem]) + + def __removeIsosurface(self, isosurface): + for row in range(self.rowCount()): + child = self.child(row) + subject = getattr(child, 'subject', None) + if subject == isosurface: + self.takeRow(row) + break + + def _init(self): + nameItem = IsoSurfaceAddRemoveItem(self.subject) + valueItem = qt.QStandardItem() + valueItem.setEditable(False) + self.appendRow([nameItem, valueItem]) + + subject = self.subject + isosurfaces = subject.getIsosurfaces() + for isosurface in isosurfaces: + self.__addIsosurface(isosurface) + + +# Cutting Plane ############################################################### + +class ColormapBase(SubjectItem): + """ + Mixin class for colormap items. + """ + + def getSignals(self): + return [self.subject.getCutPlanes()[0].sigColormapChanged] + + +class PlaneMinRangeItem(ColormapBase): + """ + colormap minVal item. + Editor is a QLineEdit with a QDoubleValidator + """ + editable = True + + def _pullData(self): + colormap = self.subject.getCutPlanes()[0].getColormap() + auto = colormap.isAutoscale() + if auto == self.isEnabled(): + self._enableRow(not auto) + return colormap.getVMin() + + def _pushData(self, value, role=qt.Qt.UserRole): + self._setVMin(value) + + def _setVMin(self, value): + cutPlane = self.subject.getCutPlanes()[0] + colormap = cutPlane.getColormap() + vMin = value + vMax = colormap.getVMax() + + if vMax is not None and value > vMax: + vMin = vMax + vMax = value + cutPlane.setColormap(name=colormap.getName(), + norm=colormap.getNorm(), + vmin=vMin, + vmax=vMax) + + def getEditor(self, parent, option, index): + editor = qt.QLineEdit(parent) + editor.setValidator(qt.QDoubleValidator()) + return editor + + def setEditorData(self, editor): + editor.setText(str(self._pullData())) + return True + + def _setModelData(self, editor): + value = float(editor.text()) + self._setVMin(value) + return True + + +class PlaneMaxRangeItem(ColormapBase): + """ + colormap maxVal item. + Editor is a QLineEdit with a QDoubleValidator + """ + editable = True + + def _pullData(self): + colormap = self.subject.getCutPlanes()[0].getColormap() + auto = colormap.isAutoscale() + if auto == self.isEnabled(): + self._enableRow(not auto) + return self.subject.getCutPlanes()[0].getColormap().getVMax() + + def _setVMax(self, value): + cutPlane = self.subject.getCutPlanes()[0] + colormap = cutPlane.getColormap() + vMin = colormap.getVMin() + vMax = value + if vMin is not None and value < vMin: + vMax = vMin + vMin = value + cutPlane.setColormap(name=colormap.getName(), + norm=colormap.getNorm(), + vmin=vMin, + vmax=vMax) + + def getEditor(self, parent, option, index): + editor = qt.QLineEdit(parent) + editor.setValidator(qt.QDoubleValidator()) + return editor + + def setEditorData(self, editor): + editor.setText(str(self._pullData())) + return True + + def _setModelData(self, editor): + value = float(editor.text()) + self._setVMax(value) + return True + + +class PlaneOrientationItem(SubjectItem): + """ + Plane orientation item. + Editor is a QComboBox. + """ + editable = True + + _PLANE_ACTIONS = ( + ('3d-plane-normal-x', 'Plane 0', + 'Set plane perpendicular to red axis', (1., 0., 0.)), + ('3d-plane-normal-y', 'Plane 1', + 'Set plane perpendicular to green axis', (0., 1., 0.)), + ('3d-plane-normal-z', 'Plane 2', + 'Set plane perpendicular to blue axis', (0., 0., 1.)), + ) + + def getSignals(self): + return [self.subject.getCutPlanes()[0].sigPlaneChanged] + + def _pullData(self): + currentNormal = self.subject.getCutPlanes()[0].getNormal() + for _, text, _, normal in self._PLANE_ACTIONS: + if numpy.array_equal(normal, currentNormal): + return text + return '' + + def getEditor(self, parent, option, index): + editor = qt.QComboBox(parent) + for iconName, text, tooltip, normal in self._PLANE_ACTIONS: + editor.addItem(getQIcon(iconName), text) + editor.currentIndexChanged[int].connect(self.__editorChanged) + return editor + + def __editorChanged(self, index): + normal = self._PLANE_ACTIONS[index][3] + plane = self.subject.getCutPlanes()[0] + plane.setNormal(normal) + plane.moveToCenter() + + def setEditorData(self, editor): + currentText = self._pullData() + index = 0 + for normIdx, (_, text, _, _) in enumerate(self._PLANE_ACTIONS): + if text == currentText: + index = normIdx + break + editor.setCurrentIndex(index) + return True + + def _setModelData(self, editor): + return True + + +class PlaneInterpolationItem(SubjectItem): + """Toggle cut plane interpolation method: nearest or linear. + + Item is checkable + """ + + def _init(self): + interpolation = self.subject.getCutPlanes()[0].getInterpolation() + self.setCheckable(True) + self.setCheckState( + qt.Qt.Checked if interpolation == 'linear' else qt.Qt.Unchecked) + self.setData(self._pullData(), role=qt.Qt.DisplayRole, pushData=False) + + def getSignals(self): + return [self.subject.getCutPlanes()[0].sigInterpolationChanged] + + def leftClicked(self): + checked = self.checkState() == qt.Qt.Checked + self._setInterpolation('linear' if checked else 'nearest') + + def _pullData(self): + interpolation = self.subject.getCutPlanes()[0].getInterpolation() + self._setInterpolation(interpolation) + return interpolation[0].upper() + interpolation[1:] + + def _setInterpolation(self, interpolation): + self.subject.getCutPlanes()[0].setInterpolation(interpolation) + + +class PlaneColormapItem(ColormapBase): + """ + colormap name item. + Editor is a QComboBox + """ + editable = True + + listValues = ['gray', 'reversed gray', + 'temperature', 'red', + 'green', 'blue'] + + def getEditor(self, parent, option, index): + editor = qt.QComboBox(parent) + editor.addItems(self.listValues) + editor.currentIndexChanged[int].connect(self.__editorChanged) + + return editor + + def __editorChanged(self, index): + colorMapName = self.listValues[index] + colorMap = self.subject.getCutPlanes()[0].getColormap() + self.subject.getCutPlanes()[0].setColormap(name=colorMapName, + norm=colorMap.getNorm(), + vmin=colorMap.getVMin(), + vmax=colorMap.getVMax()) + + def setEditorData(self, editor): + colormapName = self.subject.getCutPlanes()[0].getColormap().getName() + index = self.listValues.index(colormapName) + editor.setCurrentIndex(index) + return True + + def _setModelData(self, editor): + self.__editorChanged(editor.currentIndex()) + return True + + def _pullData(self): + return self.subject.getCutPlanes()[0].getColormap().getName() + + +class PlaneAutoScaleItem(ColormapBase): + """ + colormap autoscale item. + Item is checkable. + """ + + def _init(self): + colorMap = self.subject.getCutPlanes()[0].getColormap() + self.setCheckable(True) + self.setCheckState((colorMap.isAutoscale() and qt.Qt.Checked) + or qt.Qt.Unchecked) + self.setData(self._pullData(), role=qt.Qt.DisplayRole, pushData=False) + + def leftClicked(self): + checked = (self.checkState() == qt.Qt.Checked) + self._setAutoScale(checked) + + def _setAutoScale(self, auto): + view3d = self.subject + cutPlane = view3d.getCutPlanes()[0] + colormap = cutPlane.getColormap() + + if auto != colormap.isAutoscale(): + if auto: + vMin = vMax = None + else: + dataRange = view3d.getDataRange() + if dataRange is None or None in dataRange: + vMin = vMax = None + else: + vMin, vMax = dataRange + cutPlane.setColormap(colormap.getName(), + colormap.getNorm(), + vMin, + vMax) + + def _pullData(self): + auto = self.subject.getCutPlanes()[0].getColormap().isAutoscale() + self._setAutoScale(auto) + if auto: + data = 'Auto' + else: + data = 'User' + return data + + +class NormalizationNode(ColormapBase): + """ + colormap normalization item. + Item is a QComboBox. + """ + editable = True + listValues = ['linear', 'log'] + + def getEditor(self, parent, option, index): + editor = qt.QComboBox(parent) + editor.addItems(self.listValues) + editor.currentIndexChanged[int].connect(self.__editorChanged) + + return editor + + def __editorChanged(self, index): + colorMap = self.subject.getCutPlanes()[0].getColormap() + normalization = self.listValues[index] + self.subject.getCutPlanes()[0].setColormap(name=colorMap.getName(), + norm=normalization, + vmin=colorMap.getVMin(), + vmax=colorMap.getVMax()) + + def setEditorData(self, editor): + normalization = self.subject.getCutPlanes()[0].getColormap().getNorm() + index = self.listValues.index(normalization) + editor.setCurrentIndex(index) + return True + + def _setModelData(self, editor): + self.__editorChanged(editor.currentIndex()) + return True + + def _pullData(self): + return self.subject.getCutPlanes()[0].getColormap().getNorm() + + +class PlaneGroup(SubjectItem): + """ + Root Item for the plane items. + """ + def _init(self): + valueItem = qt.QStandardItem() + valueItem.setEditable(False) + nameItem = PlaneVisibleItem(self.subject, 'Visible') + self.appendRow([nameItem, valueItem]) + + nameItem = qt.QStandardItem('Colormap') + nameItem.setEditable(False) + valueItem = PlaneColormapItem(self.subject) + self.appendRow([nameItem, valueItem]) + + nameItem = qt.QStandardItem('Normalization') + nameItem.setEditable(False) + valueItem = NormalizationNode(self.subject) + self.appendRow([nameItem, valueItem]) + + nameItem = qt.QStandardItem('Orientation') + nameItem.setEditable(False) + valueItem = PlaneOrientationItem(self.subject) + self.appendRow([nameItem, valueItem]) + + nameItem = qt.QStandardItem('Interpolation') + nameItem.setEditable(False) + valueItem = PlaneInterpolationItem(self.subject) + self.appendRow([nameItem, valueItem]) + + nameItem = qt.QStandardItem('Autoscale') + nameItem.setEditable(False) + valueItem = PlaneAutoScaleItem(self.subject) + self.appendRow([nameItem, valueItem]) + + nameItem = qt.QStandardItem('Min') + nameItem.setEditable(False) + valueItem = PlaneMinRangeItem(self.subject) + self.appendRow([nameItem, valueItem]) + + nameItem = qt.QStandardItem('Max') + nameItem.setEditable(False) + valueItem = PlaneMaxRangeItem(self.subject) + self.appendRow([nameItem, valueItem]) + + +class PlaneVisibleItem(SubjectItem): + """ + Plane visibility item. + Item is checkable. + """ + def _init(self): + plane = self.subject.getCutPlanes()[0] + self.setCheckable(True) + self.setCheckState((plane.isVisible() and qt.Qt.Checked) + or qt.Qt.Unchecked) + + def leftClicked(self): + plane = self.subject.getCutPlanes()[0] + checked = (self.checkState() == qt.Qt.Checked) + if checked != plane.isVisible(): + plane.setVisible(checked) + if plane.isVisible(): + plane.moveToCenter() + + +# Tree ######################################################################## + +class ItemDelegate(qt.QStyledItemDelegate): + """ + Delegate for the QTreeView filled with SubjectItems. + """ + + sigDelegateEvent = qt.Signal(str) + + def __init__(self, parent=None): + super(ItemDelegate, self).__init__(parent) + + def createEditor(self, parent, option, index): + item = index.model().itemFromIndex(index) + if item: + if isinstance(item, SubjectItem): + editor = item.getEditor(parent, option, index) + if editor: + editor.setAutoFillBackground(True) + if hasattr(editor, 'sigViewTask'): + editor.sigViewTask.connect(self.__viewTask) + return editor + + editor = super(ItemDelegate, self).createEditor(parent, + option, + index) + return editor + + def updateEditorGeometry(self, editor, option, index): + editor.setGeometry(option.rect) + + def setEditorData(self, editor, index): + item = index.model().itemFromIndex(index) + if item: + if isinstance(item, SubjectItem) and item.setEditorData(editor): + return + super(ItemDelegate, self).setEditorData(editor, index) + + def setModelData(self, editor, model, index): + item = index.model().itemFromIndex(index) + if isinstance(item, SubjectItem) and item._setModelData(editor): + return + super(ItemDelegate, self).setModelData(editor, model, index) + + def __viewTask(self, task): + self.sigDelegateEvent.emit(task) + + +class TreeView(qt.QTreeView): + """ + TreeView displaying the SubjectItems for the ScalarFieldView. + """ + + def __init__(self, parent=None): + super(TreeView, self).__init__(parent) + self.__openedIndex = None + + self.setIconSize(qt.QSize(16, 16)) + + header = self.header() + if hasattr(header, 'setSectionResizeMode'): # Qt5 + header.setSectionResizeMode(qt.QHeaderView.ResizeToContents) + else: # Qt4 + header.setResizeMode(qt.QHeaderView.ResizeToContents) + + delegate = ItemDelegate() + self.setItemDelegate(delegate) + delegate.sigDelegateEvent.connect(self.__delegateEvent) + self.setSelectionBehavior(qt.QAbstractItemView.SelectRows) + self.setSelectionMode(qt.QAbstractItemView.SingleSelection) + + self.clicked.connect(self.__clicked) + + def setSfView(self, sfView): + """ + Sets the ScalarFieldView this view is controlling. + + :param sfView: A `ScalarFieldView` + """ + model = qt.QStandardItemModel() + model.setColumnCount(ModelColumns.ColumnMax) + model.setHorizontalHeaderLabels(['Name', 'Value']) + + item = qt.QStandardItem() + item.setEditable(False) + model.appendRow([ViewSettingsItem(sfView, 'Style'), item]) + + item = qt.QStandardItem() + item.setEditable(False) + model.appendRow([DataSetItem(sfView, 'Data'), item]) + + item = IsoSurfaceCount(sfView) + item.setEditable(False) + model.appendRow([IsoSurfaceGroup(sfView, 'Isosurfaces'), item]) + + item = qt.QStandardItem() + item.setEditable(False) + model.appendRow([PlaneGroup(sfView, 'Cutting Plane'), item]) + + self.setModel(model) + + def setModel(self, model): + """ + Reimplementation of the QTreeView.setModel method. It connects the + rowsRemoved signal and opens the persistent editors. + + :param qt.QStandardItemModel model: the model + """ + + prevModel = self.model() + if prevModel: + self.__openPersistentEditors(qt.QModelIndex(), False) + try: + prevModel.rowsRemoved.disconnect(self.rowsRemoved) + except TypeError: + pass + + super(TreeView, self).setModel(model) + model.rowsRemoved.connect(self.rowsRemoved) + self.__openPersistentEditors(qt.QModelIndex()) + + def __openPersistentEditors(self, parent=None, openEditor=True): + """ + Opens or closes the items persistent editors. + + :param qt.QModelIndex parent: starting index, or None if the whole tree + is to be considered. + :param bool openEditor: True to open the editors, False to close them. + """ + model = self.model() + + if not model: + return + + if not parent or not parent.isValid(): + parent = self.model().invisibleRootItem().index() + + if openEditor: + meth = self.openPersistentEditor + else: + meth = self.closePersistentEditor + + curParent = parent + children = [model.index(row, 0, curParent) + for row in range(model.rowCount(curParent))] + + columnCount = model.columnCount() + + while len(children) > 0: + curParent = children.pop(-1) + + children.extend([model.index(row, 0, curParent) + for row in range(model.rowCount(curParent))]) + + for colIdx in range(columnCount): + sibling = model.sibling(curParent.row(), + colIdx, + curParent) + item = model.itemFromIndex(sibling) + if isinstance(item, SubjectItem) and item.persistent: + meth(sibling) + + def rowsAboutToBeRemoved(self, parent, start, end): + """ + Reimplementation of the QTreeView.rowsAboutToBeRemoved. Closes all + persistent editors under parent. + + :param qt.QModelIndex parent: Parent index + :param int start: Start index from parent index (inclusive) + :param int end: End index from parent index (inclusive) + """ + self.__openPersistentEditors(parent, False) + super(TreeView, self).rowsAboutToBeRemoved(parent, start, end) + + def rowsRemoved(self, parent, start, end): + """ + Called when QTreeView.rowsRemoved is emitted. Opens all persistent + editors under parent. + + :param qt.QModelIndex parent: Parent index + :param int start: Start index from parent index (inclusive) + :param int end: End index from parent index (inclusive) + """ + super(TreeView, self).rowsRemoved(parent, start, end) + self.__openPersistentEditors(parent, True) + + def rowsInserted(self, parent, start, end): + """ + Reimplementation of the QTreeView.rowsInserted. Opens all persistent + editors under parent. + + :param qt.QModelIndex parent: Parent index + :param int start: Start index from parent index + :param int end: End index from parent index + """ + self.__openPersistentEditors(parent, False) + super(TreeView, self).rowsInserted(parent, start, end) + self.__openPersistentEditors(parent) + + def keyReleaseEvent(self, event): + """ + Reimplementation of the QTreeView.keyReleaseEvent. + At the moment only Key_Delete is handled. It calls the selected item's + queryRemove method, and deleted the item if needed. + + :param qt.QKeyEvent event: A key event + """ + + # TODO : better filtering + key = event.key() + modifiers = event.modifiers() + + if key == qt.Qt.Key_Delete and modifiers == qt.Qt.NoModifier: + self.__removeIsosurfaces() + + super(TreeView, self).keyReleaseEvent(event) + + def __removeIsosurfaces(self): + model = self.model() + selected = self.selectedIndexes() + items = [] + # WARNING : the selection mode is set to single, so we re not + # supposed to have more than one item here. + # Multiple selection deletion has not been tested. + # Watch out for index invalidation + for index in selected: + leftIndex = model.sibling(index.row(), 0, index) + leftItem = model.itemFromIndex(leftIndex) + if isinstance(leftItem, SubjectItem) and leftItem not in items: + items.append(leftItem) + + isos = [item for item in items if isinstance(item, IsoSurfaceRootItem)] + if isos: + for iso in isos: + if iso.queryRemove(self): + parentItem = iso.parent() + parentItem.removeRow(iso.row()) + else: + qt.QMessageBox.information( + self, + 'Remove isosurface', + 'Select an iso-surface to remove it') + + def __clicked(self, index): + """ + Called when the QTreeView.clicked signal is emitted. Calls the item's + leftClick method. + + :param qt.QIndex index: An index + """ + item = self.model().itemFromIndex(index) + if isinstance(item, SubjectItem): + item.leftClicked() + + def __delegateEvent(self, task): + if task == 'remove_iso': + self.__removeIsosurfaces() |