Source code for orangecanvas.gui.tooltree
"""
=========
Tool Tree
=========
A ToolTree widget presenting the user with a set of actions
organized in a tree structure.
"""
import logging
from AnyQt.QtWidgets import (
QTreeView, QWidget, QVBoxLayout, QSizePolicy, QStyledItemDelegate,
QStyle, QAction,
)
from AnyQt.QtGui import QStandardItemModel
from AnyQt.QtCore import (
Qt, QEvent, QModelIndex, QAbstractItemModel, QAbstractProxyModel
)
from AnyQt.QtCore import pyqtSignal as Signal
log = logging.getLogger(__name__)
[docs]class ToolTree(QWidget):
"""
A ListView like presentation of a list of actions.
"""
triggered = Signal(QAction)
hovered = Signal(QAction)
def __init__(self, parent=None, **kwargs):
super().__init__(parent, **kwargs)
self.setSizePolicy(QSizePolicy.MinimumExpanding,
QSizePolicy.Expanding)
self.__model = QStandardItemModel()
self.__flattened = False
self.__actionRole = Qt.UserRole
self.__view = None
self.__setupUi()
def __setupUi(self):
layout = QVBoxLayout()
layout.setContentsMargins(0, 0, 0, 0)
view = QTreeView(objectName="tool-tree-view")
view.setUniformRowHeights(True)
view.setFrameStyle(QTreeView.NoFrame)
view.setModel(self.__model)
view.setRootIsDecorated(False)
view.setHeaderHidden(True)
view.setItemsExpandable(True)
view.setEditTriggers(QTreeView.NoEditTriggers)
view.setItemDelegate(ToolTreeItemDelegate(self))
view.activated.connect(self.__onActivated)
view.clicked.connect(self.__onActivated)
view.entered.connect(self.__onEntered)
view.installEventFilter(self)
self.__view = view
layout.addWidget(view)
self.setLayout(layout)
[docs] def setFlattened(self, flatten):
"""
Show the actions in a flattened view.
"""
if self.__flattened != flatten:
self.__flattened = flatten
if flatten:
model = FlattenedTreeItemModel()
model.setSourceModel(self.__model)
else:
model = self.__model
self.__view.setModel(model)
[docs] def flattened(self):
"""
Are actions shown in a flattened tree (a list).
"""
return self.__flattened
def setModel(self, model):
# type: (QAbstractItemModel) -> None
if self.__model is not model:
self.__model = model
if self.__flattened:
model = FlattenedTreeItemModel()
model.setSourceModel(self.__model)
self.__view.setModel(model)
def model(self):
# type: () -> QAbstractItemModel
return self.__model
[docs] def setActionRole(self, role):
"""Set the action role. By default this is UserRole
"""
self.__actionRole = role
def actionRole(self):
return self.__actionRole
def __actionForIndex(self, index):
val = index.data(self.__actionRole)
if isinstance(val, QAction):
return val
else:
log.debug("index does not have an QAction")
def __onActivated(self, index):
"""The item was activated, if index has an action we
need to trigger it.
"""
if index.isValid():
action = self.__actionForIndex(index)
if action is not None:
action.trigger()
self.triggered.emit(action)
def __onEntered(self, index):
if index.isValid():
action = self.__actionForIndex(index)
if action is not None:
action.hover()
self.hovered.emit(action)
[docs] def ensureCurrent(self):
"""Ensure the view has a current item if one is available.
"""
model = self.__view.model()
curr = self.__view.currentIndex()
if not curr.isValid():
for i in range(model.rowCount()):
index = model.index(i, 0)
if index.flags() & Qt.ItemIsEnabled:
self.__view.setCurrentIndex(index)
break
[docs] def eventFilter(self, obj, event):
if obj is self.__view and event.type() == QEvent.KeyPress:
key = event.key()
space_activates = \
self.style().styleHint(
QStyle.SH_Menu_SpaceActivatesItem,
None, None)
if key in [Qt.Key_Enter, Qt.Key_Return, Qt.Key_Select] or \
(key == Qt.Key_Space and space_activates):
index = self.__view.currentIndex()
if index.isValid() and index.flags() & Qt.ItemIsEnabled:
# Emit activated on behalf of QTreeView.
self.__view.activated.emit(index)
return True
return super().eventFilter(obj, event)
class ToolTreeItemDelegate(QStyledItemDelegate):
def paint(self, painter, option, index):
super().paint(painter, option, index)
class FlattenedTreeItemModel(QAbstractProxyModel):
"""An Proxy Item model containing a flattened view of a column in a tree
like item model.
"""
Default = 1
InternalNodesDisabled = 2
LeavesOnly = 4
def __init__(self, parent=None):
super().__init__(parent)
self.__sourceColumn = 0
self.__flatteningMode = 1
self.__sourceRootIndex = QModelIndex()
self._source_key = {}
self._source_offset = {}
def setSourceModel(self, model):
self.beginResetModel()
curr_model = self.sourceModel()
if curr_model is not None:
curr_model.dataChanged.disconnect(self._sourceDataChanged)
curr_model.rowsInserted.disconnect(self._sourceRowsInserted)
curr_model.rowsRemoved.disconnect(self._sourceRowsRemoved)
curr_model.rowsMoved.disconnect(self._sourceRowsMoved)
super().setSourceModel(model)
self._updateRowMapping()
model.dataChanged.connect(self._sourceDataChanged)
model.rowsInserted.connect(self._sourceRowsInserted)
model.rowsRemoved.connect(self._sourceRowsRemoved)
model.rowsMoved.connect(self._sourceRowsMoved)
self.endResetModel()
def setSourceColumn(self, column):
raise NotImplementedError
self.beginResetModel()
self.__sourceColumn = column
self._updateRowMapping()
self.endResetModel()
def sourceColumn(self):
return self.__sourceColumn
def setSourceRootIndex(self, rootIndex):
"""Set the source root index.
"""
self.beginResetModel()
self.__sourceRootIndex = rootIndex
self._updateRowMapping()
self.endResetModel()
def sourceRootIndex(self):
"""Return the source root index.
"""
return self.__sourceRootIndex
def setFlatteningMode(self, mode):
"""Set the flattening mode.
"""
if mode != self.__flatteningMode:
self.beginResetModel()
self.__flatteningMode = mode
self._updateRowMapping()
self.endResetModel()
def flatteningMode(self):
"""Return the flattening mode.
"""
return self.__flatteningMode
def mapFromSource(self, sourceIndex):
if sourceIndex.isValid():
key = self._indexKey(sourceIndex)
offset = self._source_offset[key]
row = offset + sourceIndex.row()
return self.index(row, 0)
else:
return sourceIndex
def mapToSource(self, index):
if index.isValid():
row = index.row()
source_key_path = self._source_key[row]
return self._indexFromKey(source_key_path)
else:
return index
def index(self, row, column=0, parent=QModelIndex()):
if not parent.isValid():
return self.createIndex(row, column, object=row)
else:
return QModelIndex()
def parent(self, child):
return QModelIndex()
def rowCount(self, parent=QModelIndex()):
if parent.isValid():
return 0
else:
return len(self._source_key)
def columnCount(self, parent=QModelIndex()):
if parent.isValid():
return 0
else:
return 1
def flags(self, index):
flags = super().flags(index)
if self.__flatteningMode == self.InternalNodesDisabled:
sourceIndex = self.mapToSource(index)
sourceModel = self.sourceModel()
if sourceModel.rowCount(sourceIndex) > 0 and \
flags & Qt.ItemIsEnabled:
# Internal node, enabled in the source model, disable it
flags ^= Qt.ItemIsEnabled
return flags
def _indexKey(self, index):
"""Return a key for `index` from the source model into
the _source_offset map. The key is a tuple of row indices on
the path from the top if the model to the `index`.
"""
key_path = []
parent = index
while parent.isValid():
key_path.append(parent.row())
parent = parent.parent()
return tuple(reversed(key_path))
def _indexFromKey(self, key_path):
"""Return an source QModelIndex for the given key.
"""
index = self.sourceModel().index(key_path[0], 0)
for row in key_path[1:]:
index = index.child(row, 0)
return index
def _updateRowMapping(self):
source = self.sourceModel()
source_key = []
source_offset_map = {}
def create_mapping(index, key_path):
if source.rowCount(index) > 0:
if self.__flatteningMode != self.LeavesOnly:
source_offset_map[key_path] = len(source_offset_map)
source_key.append(key_path)
for i in range(source.rowCount(index)):
create_mapping(index.child(i, 0), key_path + (i, ))
else:
source_offset_map[key_path] = len(source_offset_map)
source_key.append(key_path)
for i in range(source.rowCount()):
create_mapping(source.index(i, 0), (i,))
self._source_key = source_key
self._source_offset = source_offset_map
def _sourceDataChanged(self, top, bottom):
changed_indexes = []
for i in range(top.row(), bottom.row() + 1):
source_ind = top.sibling(i, 0)
changed_indexes.append(source_ind)
for ind in changed_indexes:
self.dataChanged.emit(ind, ind)
def _sourceRowsInserted(self, parent, start, end):
self.beginResetModel()
self._updateRowMapping()
self.endResetModel()
def _sourceRowsRemoved(self, parent, start, end):
self.beginResetModel()
self._updateRowMapping()
self.endResetModel()
def _sourceRowsMoved(self, sourceParent, sourceStart, sourceEnd,
destParent, destRow):
self.beginResetModel()
self._updateRowMapping()
self.endResetModel()