source: orange/Orange/OrangeCanvas/gui/tooltree.py @ 11131:984471b6fa6e

Revision 11131:984471b6fa6e, 10.8 KB checked in by Ales Erjavec <ales.erjavec@…>, 18 months ago (diff)

Improved keyboard control and styling in quick menu.

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 = 0
235        self.sourceRootIndex = QModelIndex()
236
237    def setSourceModel(self, model):
238        curr_model = self.sourceModel()
239        if curr_model is not None:
240            curr_model.dataChanged.disconnect(self._sourceDataChanged)
241        QAbstractProxyModel.setSourceModel(self, model)
242        self._updateRowMapping()
243        self.reset()
244        model.dataChanged.connect(self._sourceDataChanged)
245        model.rowsInserted.connect(self._sourceRowsInserted)
246        model.rowsRemoved.connect(self._sourceRowsRemoved)
247
248    def setSourceColumn(self, column):
249        raise NotImplementedError
250
251        self.beginResetModel()
252        self.sourceColumn = column
253        self._updateRowMapping()
254        self.endResetModel()
255
256    def setSourceRootIndex(self, rootIndex):
257        self.beginResetModel()
258        self.sourceRootIndex = rootIndex
259        self._updateRowMapping()
260        self.endResetModel()
261
262    def setFlatteningMode(self, mode):
263        if mode != self.flatteningMode:
264            self.beginResetModel()
265            self.flatteningMode = mode
266            self._updateRowMapping()
267            self.endResetModel()
268
269    def mapFromSource(self, sourceIndex):
270        if sourceIndex.isValid():
271            key = self._indexKey(sourceIndex)
272            offset = self._source_offset[key]
273            row = offset + sourceIndex.row()
274            return self.index(row, 0)
275        else:
276            return sourceIndex
277
278    def mapToSource(self, index):
279        if index.isValid():
280            row = index.row()
281            source_key_path = self._source_key[row]
282            return self._indexFromKey(source_key_path)
283        else:
284            return index
285
286    def index(self, row, column=0, parent=QModelIndex()):
287        if not parent.isValid():
288            return self.createIndex(row, column, object=row)
289        else:
290            return QModelIndex()
291
292    def parent(self, child):
293        return QModelIndex()
294
295    def rowCount(self, parent=QModelIndex()):
296        if parent.isValid():
297            return 0
298        else:
299            return len(self._source_key)
300
301    def columnCount(self, parent=QModelIndex()):
302        if parent.isValid():
303            return 0
304        else:
305            return 1
306
307    def flags(self, index):
308        flags = QAbstractProxyModel.flags(self, index)
309        if self.flatteningMode & self.InternalNodesDisabled:
310            sourceIndex = self.mapToSource(index)
311            sourceModel = self.sourceModel()
312            if sourceModel.rowCount(sourceIndex) > 0 and \
313                    flags & Qt.ItemIsEnabled:
314                # Internal node, enabled in the source model, disable it
315                flags ^= Qt.ItemIsEnabled
316        return flags
317
318    def _indexKey(self, index):
319        key_path = []
320        parent = index
321        while parent.isValid():
322            key_path.append(parent.row())
323            parent = parent.parent()
324        return tuple(reversed(key_path))
325
326    def _indexFromKey(self, key_path):
327        index = self.sourceModel().index(key_path[0], 0)
328        for row in key_path[1:]:
329            index = index.child(row, 0)
330        return index
331
332    def _updateRowMapping(self):
333        source = self.sourceModel()
334        source_key_map = {}
335        source_key = []
336        source_offset_map = {}
337
338        def create_mapping(index, key_path):
339            source_offset_map[key_path] = len(source_key_map)
340            source_key_map[key_path] = len(source_key_map)
341            source_key.append(key_path)
342            for i in range(source.rowCount(index)):
343                source_offset_map[key_path + (i, )] = len(source_key_map)
344                source_key_map[key_path + (i,)] = len(source_key_map)
345                source_key.append(key_path + (i,))
346
347        for i in range(source.rowCount()):
348            create_mapping(source.index(i, 0), (i,))
349
350        self._source_map = source_key_map
351        self._source_key = source_key
352        self._source_offset = source_offset_map
353
354    def _sourceDataChanged(self, top, bottom):
355        parent = top.parent()
356        changed_indexes = []
357        for i in range(top.row(), bottom.row() + 1):
358            source_ind = parent.row(i)
359            changed_indexes.append(source_ind)
360
361        for ind in changed_indexes:
362            self.dataChanged.emit(ind, ind)
363
364    def _sourceRowsInserted(self, parent, start, end):
365        pass
366
367    def _sourceRowsRemoved(self, parent, start, end):
368        pass
Note: See TracBrowser for help on using the repository browser.