source: orange/Orange/OrangeCanvas/gui/tooltree.py @ 11366:7f9332b11252

Revision 11366:7f9332b11252, 12.3 KB checked in by Ales Erjavec <ales.erjavec@…>, 14 months ago (diff)

Added rst documentation for the canvas gui package.

Fixing docstrings in the process.

Line 
1"""
2A ToolTree widget presenting the user with a set of actions
3organized in a tree structure.
4
5"""
6
7import logging
8
9from PyQt4.QtGui import (
10    QTreeView, QWidget, QVBoxLayout, QSizePolicy, QStandardItemModel,
11    QAbstractProxyModel, QStyledItemDelegate, QStyle, QAction, QIcon
12)
13
14from PyQt4.QtCore import Qt, QEvent, QModelIndex
15from PyQt4.QtCore import pyqtSignal as Signal, pyqtProperty as Property
16
17log = logging.getLogger(__name__)
18
19
20class ToolTree(QWidget):
21    """
22    A ListView like presentation of a list of actions.
23
24    """
25    triggered = Signal(QAction)
26    hovered = Signal(QAction)
27
28    def __init__(self, parent=None, title=None, icon=None, **kwargs):
29        QTreeView.__init__(self, parent, **kwargs)
30        self.setSizePolicy(QSizePolicy.MinimumExpanding,
31                           QSizePolicy.Expanding)
32
33        if title is None:
34            title = ""
35
36        if icon is None:
37            icon = QIcon()
38
39        self.__title = title
40        self.__icon = icon
41
42        self.__model = QStandardItemModel()
43        self.__flattened = False
44        self.__actionRole = Qt.UserRole
45        self.__view = None
46
47        self.__setupUi()
48
49    def __setupUi(self):
50        layout = QVBoxLayout()
51        layout.setContentsMargins(0, 0, 0, 0)
52
53        view = QTreeView(objectName="tool-tree-view")
54        view.setUniformRowHeights(True)
55        view.setFrameStyle(QTreeView.NoFrame)
56        view.setModel(self.__model)
57        view.setRootIsDecorated(False)
58        view.setHeaderHidden(True)
59        view.setItemsExpandable(True)
60        view.setEditTriggers(QTreeView.NoEditTriggers)
61        view.setItemDelegate(ToolTreeItemDelegate(self))
62
63        view.activated.connect(self.__onActivated)
64        view.pressed.connect(self.__onPressed)
65        view.entered.connect(self.__onEntered)
66
67        view.installEventFilter(self)
68
69        self.__view = view
70
71        layout.addWidget(view)
72
73        self.setLayout(layout)
74
75    def setTitle(self, title):
76        """
77        Set the title
78        """
79        if self.__title != title:
80            self.__title = title
81            self.update()
82
83    def title(self):
84        """
85        Return the title of this tool tree.
86        """
87        return self.__title
88
89    title_ = Property(unicode, fget=title, fset=setTitle)
90
91    def setIcon(self, icon):
92        """
93        Set icon for this tool tree.
94        """
95        if self.__icon != icon:
96            self.__icon = icon
97            self.update()
98
99    def icon(self):
100        """
101        Return the icon of this tool tree.
102        """
103        return self.__icon
104
105    icon_ = Property(QIcon, fget=icon, fset=setIcon)
106
107    def setFlattened(self, flatten):
108        """
109        Show the actions in a flattened view.
110        """
111        if self.__flattened != flatten:
112            self.__flattened = flatten
113            if flatten:
114                model = FlattenedTreeItemModel()
115                model.setSourceModel(self.__model)
116            else:
117                model = self.__model
118
119            self.__view.setModel(model)
120
121    def flattened(self):
122        """
123        Are actions shown in a flattened tree (a list).
124        """
125        return self.__flattened
126
127    def setModel(self, model):
128        if self.__model is not model:
129            self.__model = model
130
131            if self.__flattened:
132                model = FlattenedTreeItemModel()
133                model.setSourceModel(self.__model)
134
135            self.__view.setModel(model)
136
137    def model(self):
138        return self.__model
139
140    def setRootIndex(self, index):
141        """Set the root index
142        """
143        self.__view.setRootIndex(index)
144
145    def rootIndex(self):
146        """Return the root index.
147        """
148        return self.__view.rootIndex()
149
150    def view(self):
151        """Return the QTreeView instance used.
152        """
153        return self.__view
154
155    def setActionRole(self, role):
156        """Set the action role. By default this is UserRole
157        """
158        self.__actionRole = role
159
160    def actionRole(self):
161        return self.__actionRole
162
163    def __actionForIndex(self, index):
164        val = index.data(self.__actionRole)
165        if val.isValid():
166            action = val.toPyObject()
167            if isinstance(action, QAction):
168                return action
169            else:
170                log.debug("index does not have an QAction")
171        else:
172            log.debug("index does not have a value for action role")
173
174    def __onActivated(self, index):
175        """The item was activated, if index has an action we
176        need to trigger it.
177
178        """
179        if index.isValid():
180            action = self.__actionForIndex(index)
181            if action is not None:
182                action.trigger()
183                self.triggered.emit(action)
184
185    def __onPressed(self, index):
186        self.__onActivated(index)
187
188    def __onEntered(self, index):
189        if index.isValid():
190            action = self.__actionForIndex(index)
191            if action is not None:
192                action.hover()
193                self.hovered.emit(action)
194
195    def ensureCurrent(self):
196        """Ensure the view has a current item if one is available.
197        """
198        model = self.__view.model()
199        curr = self.__view.currentIndex()
200        if not curr.isValid():
201            for i in range(model.rowCount()):
202                index = model.index(i, 0)
203                if index.flags() & Qt.ItemIsEnabled:
204                    self.__view.setCurrentIndex(index)
205                    break
206
207    def eventFilter(self, obj, event):
208        if obj is self.__view and event.type() == QEvent.KeyPress:
209            key = event.key()
210
211            space_activates = \
212                self.style().styleHint(
213                        QStyle.SH_Menu_SpaceActivatesItem,
214                        None, None)
215
216            if key in [Qt.Key_Enter, Qt.Key_Return, Qt.Key_Select] or \
217                    (key == Qt.Key_Space and space_activates):
218                index = self.__view.currentIndex()
219                if index.isValid() and index.flags() & Qt.ItemIsEnabled:
220                    # Emit activated on behalf of QTreeView.
221                    self.__view.activated.emit(index)
222                return True
223
224        return QWidget.eventFilter(self, obj, event)
225
226
227class ToolTreeItemDelegate(QStyledItemDelegate):
228    def paint(self, painter, option, index):
229        QStyledItemDelegate.paint(self, painter, option, index)
230
231
232class FlattenedTreeItemModel(QAbstractProxyModel):
233    """An Proxy Item model containing a flattened view of a column in a tree
234    like item model.
235
236    """
237    Default = 1
238    InternalNodesDisabled = 2
239    LeavesOnly = 4
240
241    def __init__(self, parent=None):
242        QAbstractProxyModel.__init__(self, parent)
243        self.__sourceColumn = 0
244        self.__flatteningMode = 1
245        self.__sourceRootIndex = QModelIndex()
246
247    def setSourceModel(self, model):
248        self.beginResetModel()
249
250        curr_model = self.sourceModel()
251
252        if curr_model is not None:
253            curr_model.dataChanged.disconnect(self._sourceDataChanged)
254            curr_model.rowsInserted.disconnect(self._sourceRowsInserted)
255            curr_model.rowsRemoved.disconnect(self._sourceRowsRemoved)
256            curr_model.rowsMoved.disconnect(self._sourceRowsMoved)
257
258        QAbstractProxyModel.setSourceModel(self, model)
259        self._updateRowMapping()
260
261        model.dataChanged.connect(self._sourceDataChanged)
262        model.rowsInserted.connect(self._sourceRowsInserted)
263        model.rowsRemoved.connect(self._sourceRowsRemoved)
264        model.rowsMoved.connect(self._sourceRowsMoved)
265
266        self.endResetModel()
267
268    def setSourceColumn(self, column):
269        raise NotImplementedError
270
271        self.beginResetModel()
272        self.__sourceColumn = column
273        self._updateRowMapping()
274        self.endResetModel()
275
276    def sourceColumn(self):
277        return self.__sourceColumn
278
279    def setSourceRootIndex(self, rootIndex):
280        """Set the source root index.
281        """
282        self.beginResetModel()
283        self.__sourceRootIndex = rootIndex
284        self._updateRowMapping()
285        self.endResetModel()
286
287    def sourceRootIndex(self):
288        """Return the source root index.
289        """
290        return self.__sourceRootIndex
291
292    def setFlatteningMode(self, mode):
293        """Set the flattening mode.
294        """
295        if mode != self.__flatteningMode:
296            self.beginResetModel()
297            self.__flatteningMode = mode
298            self._updateRowMapping()
299            self.endResetModel()
300
301    def flatteningMode(self):
302        """Return the flattening mode.
303        """
304        return self.__flatteningMode
305
306    def mapFromSource(self, sourceIndex):
307        if sourceIndex.isValid():
308            key = self._indexKey(sourceIndex)
309            offset = self._source_offset[key]
310            row = offset + sourceIndex.row()
311            return self.index(row, 0)
312        else:
313            return sourceIndex
314
315    def mapToSource(self, index):
316        if index.isValid():
317            row = index.row()
318            source_key_path = self._source_key[row]
319            return self._indexFromKey(source_key_path)
320        else:
321            return index
322
323    def index(self, row, column=0, parent=QModelIndex()):
324        if not parent.isValid():
325            return self.createIndex(row, column, object=row)
326        else:
327            return QModelIndex()
328
329    def parent(self, child):
330        return QModelIndex()
331
332    def rowCount(self, parent=QModelIndex()):
333        if parent.isValid():
334            return 0
335        else:
336            return len(self._source_key)
337
338    def columnCount(self, parent=QModelIndex()):
339        if parent.isValid():
340            return 0
341        else:
342            return 1
343
344    def flags(self, index):
345        flags = QAbstractProxyModel.flags(self, index)
346        if self.__flatteningMode == self.InternalNodesDisabled:
347            sourceIndex = self.mapToSource(index)
348            sourceModel = self.sourceModel()
349            if sourceModel.rowCount(sourceIndex) > 0 and \
350                    flags & Qt.ItemIsEnabled:
351                # Internal node, enabled in the source model, disable it
352                flags ^= Qt.ItemIsEnabled
353        return flags
354
355    def _indexKey(self, index):
356        """Return a key for `index` from the source model into
357        the _source_offset map. The key is a tuple of row indices on
358        the path from the top if the model to the `index`.
359
360        """
361        key_path = []
362        parent = index
363        while parent.isValid():
364            key_path.append(parent.row())
365            parent = parent.parent()
366        return tuple(reversed(key_path))
367
368    def _indexFromKey(self, key_path):
369        """Return an source QModelIndex for the given key.
370        """
371        index = self.sourceModel().index(key_path[0], 0)
372        for row in key_path[1:]:
373            index = index.child(row, 0)
374        return index
375
376    def _updateRowMapping(self):
377        source = self.sourceModel()
378
379        source_key = []
380        source_offset_map = {}
381
382        def create_mapping(index, key_path):
383            if source.rowCount(index) > 0:
384                if self.__flatteningMode != self.LeavesOnly:
385                    source_offset_map[key_path] = len(source_offset_map)
386                    source_key.append(key_path)
387
388                for i in range(source.rowCount(index)):
389                    create_mapping(index.child(i, 0), key_path + (i, ))
390
391            else:
392                source_offset_map[key_path] = len(source_offset_map)
393                source_key.append(key_path)
394
395        for i in range(source.rowCount()):
396            create_mapping(source.index(i, 0), (i,))
397
398        self._source_key = source_key
399        self._source_offset = source_offset_map
400
401    def _sourceDataChanged(self, top, bottom):
402        changed_indexes = []
403        for i in range(top.row(), bottom.row() + 1):
404            source_ind = top.sibling(i, 0)
405            changed_indexes.append(source_ind)
406
407        for ind in changed_indexes:
408            self.dataChanged.emit(ind, ind)
409
410    def _sourceRowsInserted(self, parent, start, end):
411        self.beginResetModel()
412        self._updateRowMapping()
413        self.endResetModel()
414
415    def _sourceRowsRemoved(self, parent, start, end):
416        self.beginResetModel()
417        self._updateRowMapping()
418        self.endResetModel()
419
420    def _sourceRowsMoved(self, sourceParent, sourceStart, sourceEnd,
421                         destParent, destRow):
422        self.beginResetModel()
423        self._updateRowMapping()
424        self.endResetModel()
Note: See TracBrowser for help on using the repository browser.