diff options
Diffstat (limited to 'src/silx/gui/plot3d/_model/core.py')
-rw-r--r-- | src/silx/gui/plot3d/_model/core.py | 372 |
1 files changed, 372 insertions, 0 deletions
diff --git a/src/silx/gui/plot3d/_model/core.py b/src/silx/gui/plot3d/_model/core.py new file mode 100644 index 0000000..e8e0820 --- /dev/null +++ b/src/silx/gui/plot3d/_model/core.py @@ -0,0 +1,372 @@ +# coding: utf-8 +# /*########################################################################## +# +# Copyright (c) 2017-2018 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 base classes to implement models for 3D scene content. +""" + +from __future__ import absolute_import, division + +__authors__ = ["T. Vincent"] +__license__ = "MIT" +__date__ = "11/01/2018" + + +import collections +import weakref + +from ....utils.weakref import WeakMethodProxy +from ... import qt + + +class BaseRow(qt.QObject): + """Base class for rows of the tree model. + + The root node parent MUST be set to the QAbstractItemModel it belongs to. + By default item is enabled. + + :param children: Iterable of BaseRow to start with (not signaled) + """ + + def __init__(self, children=()): + self.__modelRef = None + self.__parentRef = None + super(BaseRow, self).__init__() + self.__children = [] + for row in children: + assert isinstance(row, BaseRow) + row.setParent(self) + self.__children.append(row) + self.__flags = collections.defaultdict(lambda: qt.Qt.ItemIsEnabled) + self.__tooltip = None + + def setParent(self, parent): + """Override :meth:`QObject.setParent` to cache model and parent""" + self.__parentRef = None if parent is None else weakref.ref(parent) + + if isinstance(parent, qt.QAbstractItemModel): + model = parent + elif isinstance(parent, BaseRow): + model = parent.model() + else: + model = None + + self._updateModel(model) + + super(BaseRow, self).setParent(parent) + + def parent(self): + """Override :meth:`QObject.setParent` to use cached parent + + :rtype: Union[QObject, None]""" + return self.__parentRef() if self.__parentRef is not None else None + + def _updateModel(self, model): + """Update the model this row belongs to""" + if model != self.model(): + self.__modelRef = weakref.ref(model) if model is not None else None + for child in self.children(): + child._updateModel(model) + + def model(self): + """Return the model this node belongs to or None if not in a model. + + :rtype: Union[QAbstractItemModel, None] + """ + return self.__modelRef() if self.__modelRef is not None else None + + def index(self, column=0): + """Return corresponding index in the model or None if not in a model. + + :param int column: The column to make the index for + :rtype: Union[QModelIndex, None] + """ + parent = self.parent() + model = self.model() + + if model is None: # Not in a model + return None + elif parent is model: # Root node + return qt.QModelIndex() + else: + index = parent.index() + row = parent.children().index(self) + return model.index(row, column, index) + + def columnCount(self): + """Returns number of columns (default: 2) + + :rtype: int + """ + return 2 + + def children(self): + """Returns the list of children nodes + + :rtype: tuple of Node + """ + return tuple(self.__children) + + def rowCount(self): + """Returns number of rows + + :rtype: int + """ + return len(self.__children) + + def addRow(self, row, index=None): + """Add a node to the children + + :param BaseRow row: The node to add + :param int index: The index at which to insert it or + None to append + """ + if index is None: + index = self.rowCount() + assert index <= self.rowCount() + + model = self.model() + + if model is not None: + parent = self.index() + model.beginInsertRows(parent, index, index) + + self.__children.insert(index, row) + row.setParent(self) + + if model is not None: + model.endInsertRows() + + def removeRow(self, row): + """Remove a row from the children list. + + It removes either a node or a row index. + + :param row: BaseRow object or index of row to remove + :type row: Union[BaseRow, int] + """ + if isinstance(row, BaseRow): + row = self.__children.index(row) + else: + row = int(row) + assert row < self.rowCount() + + model = self.model() + + if model is not None: + index = self.index() + model.beginRemoveRows(index, row, row) + + node = self.__children.pop(row) + node.setParent(None) + + if model is not None: + model.endRemoveRows() + + def data(self, column, role): + """Returns data for given column and role + + :param int column: Column index for this row + :param int role: The role to get + :return: Corresponding data (Default: None) + """ + if role == qt.Qt.ToolTipRole and self.__tooltip is not None: + return self.__tooltip + else: + return None + + def setData(self, column, value, role): + """Set data for given column and role + + :param int column: Column index for this row + :param value: The data to set + :param int role: The role to set + :return: True on success, False on failure + :rtype: bool + """ + return False + + def setToolTip(self, tooltip): + """Set the tooltip of the whole row. + + If None there is no tooltip. + + :param Union[str, None] tooltip: + """ + self.__tooltip = tooltip + + def setFlags(self, flags, column=None): + """Set the static flags to return. + + Default is ItemIsEnabled for all columns. + + :param int column: The column for which to set the flags + :param flags: Item flags + """ + if column is None: + self.__flags = collections.defaultdict(lambda: flags) + else: + self.__flags[column] = flags + + def flags(self, column): + """Returns flags for given column + + :rtype: int + """ + return self.__flags[column] + + +class StaticRow(BaseRow): + """Row with static data. + + :param tuple display: List of data for DisplayRole for each column + :param dict roles: Optional mapping of roles to list of data. + :param children: Iterable of BaseRow to start with (not signaled) + """ + + def __init__(self, display=('', None), roles=None, children=()): + super(StaticRow, self).__init__(children) + self._dataByRoles = {} if roles is None else roles + self._dataByRoles[qt.Qt.DisplayRole] = display + + def data(self, column, role): + if role in self._dataByRoles: + data = self._dataByRoles[role] + if column < len(data): + return data[column] + return super(StaticRow, self).data(column, role) + + def columnCount(self): + return len(self._dataByRoles[qt.Qt.DisplayRole]) + + +class ProxyRow(BaseRow): + """Provides a node to proxy a data accessible through functions. + + Warning: Only weak reference are kept on fget and fset. + + :param str name: The name of this node + :param callable fget: A callable returning the data + :param callable fset: + An optional callable setting the data with data as a single argument. + :param notify: + An optional signal emitted when data has changed. + :param callable toModelData: + An optional callable to convert from fget + callable to data returned by the model. + :param callable fromModelData: + An optional callable converting data provided to the model to + data for fset. + :param editorHint: Data to provide as UserRole for editor selection/setup + """ + + def __init__(self, + name='', + fget=None, + fset=None, + notify=None, + toModelData=None, + fromModelData=None, + editorHint=None): + + super(ProxyRow, self).__init__() + self.__name = name + self.__editorHint = editorHint + + assert fget is not None + self._fget = WeakMethodProxy(fget) + self._fset = WeakMethodProxy(fset) if fset is not None else None + if fset is not None: + self.setFlags(qt.Qt.ItemIsEnabled | qt.Qt.ItemIsEditable, 1) + self._toModelData = toModelData + self._fromModelData = fromModelData + + if notify is not None: + notify.connect(self._notified) # TODO support sigItemChanged flags + + def _notified(self, *args, **kwargs): + """Send update to the model upon signal notifications""" + index = self.index(column=1) + model = self.model() + if model is not None: + model.dataChanged.emit(index, index) + + def data(self, column, role): + if column == 0: + if role == qt.Qt.DisplayRole: + return self.__name + + elif column == 1: + if role == qt.Qt.UserRole: # EditorHint + return self.__editorHint + elif role == qt.Qt.DisplayRole or (role == qt.Qt.EditRole and + self._fset is not None): + data = self._fget() + if self._toModelData is not None: + data = self._toModelData(data) + return data + + return super(ProxyRow, self).data(column, role) + + def setData(self, column, value, role): + if role == qt.Qt.EditRole and self._fset is not None: + if self._fromModelData is not None: + value = self._fromModelData(value) + self._fset(value) + return True + + return super(ProxyRow, self).setData(column, value, role) + + +class ColorProxyRow(ProxyRow): + """Provides a proxy to a QColor property. + + The color is returned through the decorative role. + + See :class:`ProxyRow` + """ + + def data(self, column, role): + if column == 1: # Show color as decoration, not text + if role == qt.Qt.DisplayRole: + return None + if role == qt.Qt.DecorationRole: + role = qt.Qt.DisplayRole + return super(ColorProxyRow, self).data(column, role) + + +class AngleDegreeRow(ProxyRow): + """ProxyRow patching display of column 1 to add degree symbol + + See :class:`ProxyRow` + """ + + def __init__(self, *args, **kwargs): + super(AngleDegreeRow, self).__init__(*args, **kwargs) + + def data(self, column, role): + if column == 1 and role == qt.Qt.DisplayRole: + return u'%g°' % super(AngleDegreeRow, self).data(column, role) + else: + return super(AngleDegreeRow, self).data(column, role) |