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