source: orange/Orange/OrangeCanvas/gui/tooltree.py @ 11106:89363c45cb3d

Revision 11106:89363c45cb3d, 10.1 KB checked in by Ales Erjavec <ales.erjavec@…>, 18 months ago (diff)

Added a ToolTree class (a list of actions).

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