diff options
Diffstat (limited to 'src/silx/gui/plot3d/ParamTreeView.py')
-rw-r--r-- | src/silx/gui/plot3d/ParamTreeView.py | 522 |
1 files changed, 522 insertions, 0 deletions
diff --git a/src/silx/gui/plot3d/ParamTreeView.py b/src/silx/gui/plot3d/ParamTreeView.py new file mode 100644 index 0000000..2593860 --- /dev/null +++ b/src/silx/gui/plot3d/ParamTreeView.py @@ -0,0 +1,522 @@ +# coding: utf-8 +# /*########################################################################## +# +# Copyright (c) 2017-2021 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 :class:`QTreeView` dedicated to display plot3d models. + +This module contains: +- :class:`ParamTreeView`: A QTreeView specific for plot3d parameters and scene. +- :class:`ParameterTreeDelegate`: The delegate for :class:`ParamTreeView`. +- A set of specific editors used by :class:`ParameterTreeDelegate`: + :class:`FloatEditor`, :class:`Vector3DEditor`, + :class:`Vector4DEditor`, :class:`IntSliderEditor`, :class:`BooleanEditor` +""" + +from __future__ import absolute_import + +__authors__ = ["T. Vincent"] +__license__ = "MIT" +__date__ = "05/12/2017" + + +import numbers +import sys + +from .. import qt +from ..widgets.FloatEdit import FloatEdit as _FloatEdit +from ._model import visitQAbstractItemModel + + +class FloatEditor(_FloatEdit): + """Editor widget for float. + + :param parent: The widget's parent + :param float value: The initial editor value + """ + + valueChanged = qt.Signal(float) + """Signal emitted when the float value has changed""" + + def __init__(self, parent=None, value=None): + super(FloatEditor, self).__init__(parent, value) + self.setAlignment(qt.Qt.AlignLeft) + self.editingFinished.connect(self._emit) + + def _emit(self): + self.valueChanged.emit(self.value) + + value = qt.Property(float, + fget=_FloatEdit.value, + fset=_FloatEdit.setValue, + user=True, + notify=valueChanged) + """Qt user property of the float value this widget edits""" + + +class Vector3DEditor(qt.QWidget): + """Editor widget for QVector3D. + + :param parent: The widget's parent + :param flags: The widgets's flags + """ + + valueChanged = qt.Signal(qt.QVector3D) + """Signal emitted when the QVector3D value has changed""" + + def __init__(self, parent=None, flags=qt.Qt.Widget): + super(Vector3DEditor, self).__init__(parent, flags) + layout = qt.QHBoxLayout(self) + # layout.setSpacing(0) + layout.setContentsMargins(0, 0, 0, 0) + self.setLayout(layout) + self._xEdit = _FloatEdit(parent=self, value=0.) + self._xEdit.setAlignment(qt.Qt.AlignLeft) + # self._xEdit.editingFinished.connect(self._emit) + self._yEdit = _FloatEdit(parent=self, value=0.) + self._yEdit.setAlignment(qt.Qt.AlignLeft) + # self._yEdit.editingFinished.connect(self._emit) + self._zEdit = _FloatEdit(parent=self, value=0.) + self._zEdit.setAlignment(qt.Qt.AlignLeft) + # self._zEdit.editingFinished.connect(self._emit) + layout.addWidget(qt.QLabel('x:')) + layout.addWidget(self._xEdit) + layout.addWidget(qt.QLabel('y:')) + layout.addWidget(self._yEdit) + layout.addWidget(qt.QLabel('z:')) + layout.addWidget(self._zEdit) + layout.addStretch(1) + + def _emit(self): + vector = self.value + self.valueChanged.emit(vector) + + def getValue(self): + """Returns the QVector3D value of this widget + + :rtype: QVector3D + """ + return qt.QVector3D( + self._xEdit.value(), self._yEdit.value(), self._zEdit.value()) + + def setValue(self, value): + """Set the QVector3D value + + :param QVector3D value: The new value + """ + self._xEdit.setValue(value.x()) + self._yEdit.setValue(value.y()) + self._zEdit.setValue(value.z()) + self.valueChanged.emit(value) + + value = qt.Property(qt.QVector3D, + fget=getValue, + fset=setValue, + user=True, + notify=valueChanged) + """Qt user property of the QVector3D value this widget edits""" + + +class Vector4DEditor(qt.QWidget): + """Editor widget for QVector4D. + + :param parent: The widget's parent + :param flags: The widgets's flags + """ + + valueChanged = qt.Signal(qt.QVector4D) + """Signal emitted when the QVector4D value has changed""" + + def __init__(self, parent=None, flags=qt.Qt.Widget): + super(Vector4DEditor, self).__init__(parent, flags) + layout = qt.QHBoxLayout(self) + # layout.setSpacing(0) + layout.setContentsMargins(0, 0, 0, 0) + self.setLayout(layout) + self._xEdit = _FloatEdit(parent=self, value=0.) + self._xEdit.setAlignment(qt.Qt.AlignLeft) + # self._xEdit.editingFinished.connect(self._emit) + self._yEdit = _FloatEdit(parent=self, value=0.) + self._yEdit.setAlignment(qt.Qt.AlignLeft) + # self._yEdit.editingFinished.connect(self._emit) + self._zEdit = _FloatEdit(parent=self, value=0.) + self._zEdit.setAlignment(qt.Qt.AlignLeft) + # self._zEdit.editingFinished.connect(self._emit) + self._wEdit = _FloatEdit(parent=self, value=0.) + self._wEdit.setAlignment(qt.Qt.AlignLeft) + # self._wEdit.editingFinished.connect(self._emit) + layout.addWidget(qt.QLabel('x:')) + layout.addWidget(self._xEdit) + layout.addWidget(qt.QLabel('y:')) + layout.addWidget(self._yEdit) + layout.addWidget(qt.QLabel('z:')) + layout.addWidget(self._zEdit) + layout.addWidget(qt.QLabel('w:')) + layout.addWidget(self._wEdit) + layout.addStretch(1) + + def _emit(self): + vector = self.value + self.valueChanged.emit(vector) + + def getValue(self): + """Returns the QVector4D value of this widget + + :rtype: QVector4D + """ + return qt.QVector4D(self._xEdit.value(), self._yEdit.value(), + self._zEdit.value(), self._wEdit.value()) + + def setValue(self, value): + """Set the QVector4D value + + :param QVector4D value: The new value + """ + self._xEdit.setValue(value.x()) + self._yEdit.setValue(value.y()) + self._zEdit.setValue(value.z()) + self._wEdit.setValue(value.w()) + self.valueChanged.emit(value) + + value = qt.Property(qt.QVector4D, + fget=getValue, + fset=setValue, + user=True, + notify=valueChanged) + """Qt user property of the QVector4D value this widget edits""" + + +class IntSliderEditor(qt.QSlider): + """Slider editor widget for integer. + + Note: Tracking is disabled. + + :param parent: The widget's parent + """ + + def __init__(self, parent=None): + super(IntSliderEditor, self).__init__(parent) + self.setOrientation(qt.Qt.Horizontal) + self.setSingleStep(1) + self.setRange(0, 255) + self.setValue(0) + + +class BooleanEditor(qt.QCheckBox): + """Checkbox editor for bool. + + This is a QCheckBox with white background. + + :param parent: The widget's parent + """ + + def __init__(self, parent=None): + super(BooleanEditor, self).__init__(parent) + self.setStyleSheet("background: white;") + + +class ParameterTreeDelegate(qt.QStyledItemDelegate): + """TreeView delegate specific to plot3d scene and object parameter tree. + + It provides additional editors. + + :param parent: Delegate's parent + """ + + EDITORS = { + bool: BooleanEditor, + float: FloatEditor, + qt.QVector3D: Vector3DEditor, + qt.QVector4D: Vector4DEditor, + } + """Specific editors for different type of data""" + + def __init__(self, parent=None): + super(ParameterTreeDelegate, self).__init__(parent) + + def paint(self, painter, option, index): + """See :meth:`QStyledItemDelegate.paint`""" + data = index.data(qt.Qt.DisplayRole) + + if isinstance(data, (qt.QVector3D, qt.QVector4D)): + if isinstance(data, qt.QVector3D): + text = '(x: %g; y: %g; z: %g)' % (data.x(), data.y(), data.z()) + elif isinstance(data, qt.QVector4D): + text = '(%g; %g; %g; %g)' % (data.x(), data.y(), data.z(), data.w()) + else: + text = '' + + painter.save() + painter.setRenderHint(qt.QPainter.Antialiasing, True) + + # Select palette color group + colorGroup = qt.QPalette.Inactive + if option.state & qt.QStyle.State_Active: + colorGroup = qt.QPalette.Active + if not option.state & qt.QStyle.State_Enabled: + colorGroup = qt.QPalette.Disabled + + # Draw background if selected + if option.state & qt.QStyle.State_Selected: + brush = option.palette.brush(colorGroup, + qt.QPalette.Highlight) + painter.fillRect(option.rect, brush) + + # Draw text + if option.state & qt.QStyle.State_Selected: + colorRole = qt.QPalette.HighlightedText + else: + colorRole = qt.QPalette.WindowText + color = option.palette.color(colorGroup, colorRole) + painter.setPen(qt.QPen(color)) + painter.drawText(option.rect, qt.Qt.AlignLeft, text) + + painter.restore() + + # The following commented code does the same as QPainter based code + # but it does not work with PySide + # self.initStyleOption(option, index) + # option.text = text + # widget = option.widget + # style = qt.QApplication.style() if not widget else widget.style() + # style.drawControl(qt.QStyle.CE_ItemViewItem, option, painter, widget) + + else: + super(ParameterTreeDelegate, self).paint(painter, option, index) + + def _commit(self, *args): + """Commit data to the model from editors""" + sender = self.sender() + self.commitData.emit(sender) + + def editorEvent(self, event, model, option, index): + """See :meth:`QStyledItemDelegate.editorEvent`""" + if (event.type() == qt.QEvent.MouseButtonPress and + isinstance(index.data(qt.Qt.EditRole), qt.QColor)): + initialColor = index.data(qt.Qt.EditRole) + + def callback(color): + theModel = index.model() + theModel.setData(index, color, qt.Qt.EditRole) + + dialog = qt.QColorDialog(self.parent()) + # dialog.setOption(qt.QColorDialog.ShowAlphaChannel, True) + if sys.platform == 'darwin': + # Use of native color dialog on macos might cause problems + dialog.setOption(qt.QColorDialog.DontUseNativeDialog, True) + dialog.setCurrentColor(initialColor) + dialog.currentColorChanged.connect(callback) + if dialog.exec() == qt.QDialog.Rejected: + # Reset color + dialog.setCurrentColor(initialColor) + + return True + else: + return super(ParameterTreeDelegate, self).editorEvent( + event, model, option, index) + + def createEditor(self, parent, option, index): + """See :meth:`QStyledItemDelegate.createEditor`""" + data = index.data(qt.Qt.EditRole) + editorHint = index.data(qt.Qt.UserRole) + + if callable(editorHint): + editor = editorHint() + assert isinstance(editor, qt.QWidget) + editor.setParent(parent) + + elif isinstance(data, numbers.Number) and editorHint is not None: + # Use a slider + editor = IntSliderEditor(parent) + range_ = editorHint + editor.setRange(*range_) + editor.sliderReleased.connect(self._commit) + + elif isinstance(data, str) and editorHint is not None: + # Use a combo box + editor = qt.QComboBox(parent) + if data not in editorHint: + editor.addItem(data) + editor.addItems(editorHint) + + index = editor.findText(data) + editor.setCurrentIndex(index) + + editor.currentIndexChanged.connect(self._commit) + + else: + # Handle overridden editors from Python + # Mimic Qt C++ implementation + for type_, editorClass in self.EDITORS.items(): + if isinstance(data, type_): + editor = editorClass(parent) + metaObject = editor.metaObject() + userProperty = metaObject.userProperty() + if userProperty.isValid() and userProperty.hasNotifySignal(): + notifySignal = userProperty.notifySignal() + signature = notifySignal.methodSignature() + if qt.BINDING == 'PySide2': + signature = signature.data() + else: + signature = bytes(signature) + + if hasattr(signature, 'decode'): # For PySide with python3 + signature = signature.decode('ascii') + signalName = signature.split('(')[0] + + signal = getattr(editor, signalName) + signal.connect(self._commit) + break + + else: # Default handling for default types + return super(ParameterTreeDelegate, self).createEditor( + parent, option, index) + + editor.setAutoFillBackground(True) + return editor + + def setModelData(self, editor, model, index): + """See :meth:`QStyledItemDelegate.setModelData`""" + if isinstance(editor, tuple(self.EDITORS.values())): + # Special handling of Python classes + # Translation of QStyledItemDelegate::setModelData to Python + # To make it work with Python QVariant wrapping/unwrapping + name = editor.metaObject().userProperty().name() + if not name: + pass # TODO handle the case of missing user property + if name: + if hasattr(editor, name): + value = getattr(editor, name) + else: + value = editor.property(name) + model.setData(index, value, qt.Qt.EditRole) + + else: + super(ParameterTreeDelegate, self).setModelData(editor, model, index) + + +class ParamTreeView(qt.QTreeView): + """QTreeView specific to handle plot3d scene and object parameters. + + It provides additional editors and specific creation of persistent editors. + + :param parent: The widget's parent. + """ + + def __init__(self, parent=None): + super(ParamTreeView, self).__init__(parent) + + header = self.header() + header.setMinimumSectionSize(128) # For colormap pixmaps + header.setSectionResizeMode(qt.QHeaderView.ResizeToContents) + + delegate = ParameterTreeDelegate() + self.setItemDelegate(delegate) + + self.setSelectionBehavior(qt.QAbstractItemView.SelectRows) + self.setSelectionMode(qt.QAbstractItemView.SingleSelection) + + self.expanded.connect(self._expanded) + + self.setEditTriggers(qt.QAbstractItemView.CurrentChanged | + qt.QAbstractItemView.DoubleClicked) + + self.__persistentEditors = set() + + def _openEditorForIndex(self, index): + """Check if it has to open a persistent editor for a specific cell. + + :param QModelIndex index: The cell index + """ + if index.flags() & qt.Qt.ItemIsEditable: + data = index.data(qt.Qt.EditRole) + editorHint = index.data(qt.Qt.UserRole) + if (isinstance(data, bool) or + callable(editorHint) or + (isinstance(data, numbers.Number) and editorHint)): + self.openPersistentEditor(index) + self.__persistentEditors.add(index) + + def _openEditors(self, parent=qt.QModelIndex()): + """Open persistent editors in a subtree starting at parent. + + :param QModelIndex parent: The root of the subtree to process. + """ + model = self.model() + if model is not None: + for index in visitQAbstractItemModel(model, parent): + self._openEditorForIndex(index) + + def setModel(self, model): + """Set the model this TreeView is displaying + + :param QAbstractItemModel model: + """ + super(ParamTreeView, self).setModel(model) + self._openEditors() + + def rowsInserted(self, parent, start, end): + """See :meth:`QTreeView.rowsInserted`""" + super(ParamTreeView, self).rowsInserted(parent, start, end) + model = self.model() + if model is not None: + for row in range(start, end+1): + self._openEditorForIndex(model.index(row, 1, parent)) + self._openEditors(model.index(row, 0, parent)) + + def _expanded(self, index): + """Handle QTreeView expanded signal""" + name = index.data(qt.Qt.DisplayRole) + if name == 'Transform': + rotateIndex = self.model().index(1, 0, index) + self.setExpanded(rotateIndex, True) + + def dataChanged(self, topLeft, bottomRight, roles=()): + """Handle model dataChanged signal eventually closing editors""" + if roles: # Qt 5 + super(ParamTreeView, self).dataChanged(topLeft, bottomRight, roles) + else: # Qt4 compatibility + super(ParamTreeView, self).dataChanged(topLeft, bottomRight) + if not roles or qt.Qt.UserRole in roles: # Check editorHint update + for row in range(topLeft.row(), bottomRight.row() + 1): + for column in range(topLeft.column(), bottomRight.column() + 1): + index = topLeft.sibling(row, column) + if index.isValid(): + if self._isPersistentEditorOpen(index): + self.closePersistentEditor(index) + self._openEditorForIndex(index) + + def _isPersistentEditorOpen(self, index): + """Returns True if a persistent editor is opened for index + + :param QModelIndex index: + :rtype: bool + """ + return index in self.__persistentEditors + + def selectionCommand(self, index, event=None): + """Filter out selection of not selectable items""" + if index.flags() & qt.Qt.ItemIsSelectable: + return super(ParamTreeView, self).selectionCommand(index, event) + else: + return qt.QItemSelectionModel.NoUpdate |