source: orange/Orange/OrangeCanvas/gui/tooltree.py @ 11370:03af193a01d3

Revision 11370:03af193a01d3, 11.4 KB checked in by Ales Erjavec <ales.erjavec@…>, 14 months ago (diff)

Moved 'icon' and 'title' property from ToolTree widget, to quickmenu.MenuPage.

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