source: orange/Orange/OrangeCanvas/gui/tooltree.py @ 11493:1afbda175fac

Revision 11493:1afbda175fac, 11.3 KB checked in by Ales Erjavec <ales.erjavec@…>, 12 months ago (diff)

Activate an item on a click (an not just a press).

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.clicked.connect(self.__onActivated)
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 __onEntered(self, index):
148        if index.isValid():
149            action = self.__actionForIndex(index)
150            if action is not None:
151                action.hover()
152                self.hovered.emit(action)
153
154    def ensureCurrent(self):
155        """Ensure the view has a current item if one is available.
156        """
157        model = self.__view.model()
158        curr = self.__view.currentIndex()
159        if not curr.isValid():
160            for i in range(model.rowCount()):
161                index = model.index(i, 0)
162                if index.flags() & Qt.ItemIsEnabled:
163                    self.__view.setCurrentIndex(index)
164                    break
165
166    def eventFilter(self, obj, event):
167        if obj is self.__view and event.type() == QEvent.KeyPress:
168            key = event.key()
169
170            space_activates = \
171                self.style().styleHint(
172                        QStyle.SH_Menu_SpaceActivatesItem,
173                        None, None)
174
175            if key in [Qt.Key_Enter, Qt.Key_Return, Qt.Key_Select] or \
176                    (key == Qt.Key_Space and space_activates):
177                index = self.__view.currentIndex()
178                if index.isValid() and index.flags() & Qt.ItemIsEnabled:
179                    # Emit activated on behalf of QTreeView.
180                    self.__view.activated.emit(index)
181                return True
182
183        return QWidget.eventFilter(self, obj, event)
184
185
186class ToolTreeItemDelegate(QStyledItemDelegate):
187    def paint(self, painter, option, index):
188        QStyledItemDelegate.paint(self, painter, option, index)
189
190
191class FlattenedTreeItemModel(QAbstractProxyModel):
192    """An Proxy Item model containing a flattened view of a column in a tree
193    like item model.
194
195    """
196    Default = 1
197    InternalNodesDisabled = 2
198    LeavesOnly = 4
199
200    def __init__(self, parent=None):
201        QAbstractProxyModel.__init__(self, parent)
202        self.__sourceColumn = 0
203        self.__flatteningMode = 1
204        self.__sourceRootIndex = QModelIndex()
205
206    def setSourceModel(self, model):
207        self.beginResetModel()
208
209        curr_model = self.sourceModel()
210
211        if curr_model is not None:
212            curr_model.dataChanged.disconnect(self._sourceDataChanged)
213            curr_model.rowsInserted.disconnect(self._sourceRowsInserted)
214            curr_model.rowsRemoved.disconnect(self._sourceRowsRemoved)
215            curr_model.rowsMoved.disconnect(self._sourceRowsMoved)
216
217        QAbstractProxyModel.setSourceModel(self, model)
218        self._updateRowMapping()
219
220        model.dataChanged.connect(self._sourceDataChanged)
221        model.rowsInserted.connect(self._sourceRowsInserted)
222        model.rowsRemoved.connect(self._sourceRowsRemoved)
223        model.rowsMoved.connect(self._sourceRowsMoved)
224
225        self.endResetModel()
226
227    def setSourceColumn(self, column):
228        raise NotImplementedError
229
230        self.beginResetModel()
231        self.__sourceColumn = column
232        self._updateRowMapping()
233        self.endResetModel()
234
235    def sourceColumn(self):
236        return self.__sourceColumn
237
238    def setSourceRootIndex(self, rootIndex):
239        """Set the source root index.
240        """
241        self.beginResetModel()
242        self.__sourceRootIndex = rootIndex
243        self._updateRowMapping()
244        self.endResetModel()
245
246    def sourceRootIndex(self):
247        """Return the source root index.
248        """
249        return self.__sourceRootIndex
250
251    def setFlatteningMode(self, mode):
252        """Set the flattening mode.
253        """
254        if mode != self.__flatteningMode:
255            self.beginResetModel()
256            self.__flatteningMode = mode
257            self._updateRowMapping()
258            self.endResetModel()
259
260    def flatteningMode(self):
261        """Return the flattening mode.
262        """
263        return self.__flatteningMode
264
265    def mapFromSource(self, sourceIndex):
266        if sourceIndex.isValid():
267            key = self._indexKey(sourceIndex)
268            offset = self._source_offset[key]
269            row = offset + sourceIndex.row()
270            return self.index(row, 0)
271        else:
272            return sourceIndex
273
274    def mapToSource(self, index):
275        if index.isValid():
276            row = index.row()
277            source_key_path = self._source_key[row]
278            return self._indexFromKey(source_key_path)
279        else:
280            return index
281
282    def index(self, row, column=0, parent=QModelIndex()):
283        if not parent.isValid():
284            return self.createIndex(row, column, object=row)
285        else:
286            return QModelIndex()
287
288    def parent(self, child):
289        return QModelIndex()
290
291    def rowCount(self, parent=QModelIndex()):
292        if parent.isValid():
293            return 0
294        else:
295            return len(self._source_key)
296
297    def columnCount(self, parent=QModelIndex()):
298        if parent.isValid():
299            return 0
300        else:
301            return 1
302
303    def flags(self, index):
304        flags = QAbstractProxyModel.flags(self, index)
305        if self.__flatteningMode == self.InternalNodesDisabled:
306            sourceIndex = self.mapToSource(index)
307            sourceModel = self.sourceModel()
308            if sourceModel.rowCount(sourceIndex) > 0 and \
309                    flags & Qt.ItemIsEnabled:
310                # Internal node, enabled in the source model, disable it
311                flags ^= Qt.ItemIsEnabled
312        return flags
313
314    def _indexKey(self, index):
315        """Return a key for `index` from the source model into
316        the _source_offset map. The key is a tuple of row indices on
317        the path from the top if the model to the `index`.
318
319        """
320        key_path = []
321        parent = index
322        while parent.isValid():
323            key_path.append(parent.row())
324            parent = parent.parent()
325        return tuple(reversed(key_path))
326
327    def _indexFromKey(self, key_path):
328        """Return an source QModelIndex for the given key.
329        """
330        index = self.sourceModel().index(key_path[0], 0)
331        for row in key_path[1:]:
332            index = index.child(row, 0)
333        return index
334
335    def _updateRowMapping(self):
336        source = self.sourceModel()
337
338        source_key = []
339        source_offset_map = {}
340
341        def create_mapping(index, key_path):
342            if source.rowCount(index) > 0:
343                if self.__flatteningMode != self.LeavesOnly:
344                    source_offset_map[key_path] = len(source_offset_map)
345                    source_key.append(key_path)
346
347                for i in range(source.rowCount(index)):
348                    create_mapping(index.child(i, 0), key_path + (i, ))
349
350            else:
351                source_offset_map[key_path] = len(source_offset_map)
352                source_key.append(key_path)
353
354        for i in range(source.rowCount()):
355            create_mapping(source.index(i, 0), (i,))
356
357        self._source_key = source_key
358        self._source_offset = source_offset_map
359
360    def _sourceDataChanged(self, top, bottom):
361        changed_indexes = []
362        for i in range(top.row(), bottom.row() + 1):
363            source_ind = top.sibling(i, 0)
364            changed_indexes.append(source_ind)
365
366        for ind in changed_indexes:
367            self.dataChanged.emit(ind, ind)
368
369    def _sourceRowsInserted(self, parent, start, end):
370        self.beginResetModel()
371        self._updateRowMapping()
372        self.endResetModel()
373
374    def _sourceRowsRemoved(self, parent, start, end):
375        self.beginResetModel()
376        self._updateRowMapping()
377        self.endResetModel()
378
379    def _sourceRowsMoved(self, sourceParent, sourceStart, sourceEnd,
380                         destParent, destRow):
381        self.beginResetModel()
382        self._updateRowMapping()
383        self.endResetModel()
Note: See TracBrowser for help on using the repository browser.