source: orange/Orange/OrangeCanvas/gui/tooltree.py @ 11228:e17010e1c0a2

Revision 11228:e17010e1c0a2, 12.1 KB checked in by Ales Erjavec <ales.erjavec@…>, 17 months ago (diff)

Fixes to FlattenedTreeModel (source index mapping, 'LeavesOnly' mode).

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