source: orange/Orange/OrangeCanvas/application/widgettoolbox.py @ 11372:3c196fa6a03a

Revision 11372:3c196fa6a03a, 13.2 KB checked in by Ales Erjavec <ales.erjavec@…>, 14 months ago (diff)

Fixed docstrings for widget toolbox.

Line 
1"""
2Widget Tool Box
3===============
4
5
6A tool box with a tool grid for each category.
7
8"""
9
10import logging
11
12from PyQt4.QtGui import (
13    QAbstractButton, QSizePolicy, QAction, QApplication, QDrag, QPalette,
14    QBrush
15)
16
17from PyQt4.QtCore import (
18    Qt, QObject, QModelIndex, QSize, QEvent, QMimeData, QByteArray,
19    QDataStream, QIODevice
20)
21
22from PyQt4.QtCore import  pyqtSignal as Signal, pyqtProperty as Property
23
24from ..gui.toolbox import ToolBox, create_tab_gradient
25from ..gui.toolgrid import ToolGrid
26from ..gui.quickhelp import StatusTipPromoter
27from ..registry.qt import QtWidgetRegistry
28
29
30log = logging.getLogger(__name__)
31
32
33def iter_item(item):
34    """
35    Iterate over child items of a `QStandardItem`.
36    """
37    for i in range(item.rowCount()):
38        yield item.child(i)
39
40
41class WidgetToolGrid(ToolGrid):
42    """
43    A Tool Grid with widget buttons. Populates the widget buttons
44    from a item model. Also adds support for drag operations.
45
46    """
47    def __init__(self, *args, **kwargs):
48        ToolGrid.__init__(self, *args, **kwargs)
49
50        self.__model = None
51        self.__rootIndex = None
52        self.__rootItem = None
53        self.__rootItem = None
54        self.__actionRole = QtWidgetRegistry.WIDGET_ACTION_ROLE
55
56        self.__dragListener = DragStartEventListener(self)
57        self.__dragListener.dragStartOperationRequested.connect(
58            self.__startDrag
59        )
60        self.__statusTipPromoter = StatusTipPromoter(self)
61
62    def setModel(self, model, rootIndex=QModelIndex()):
63        """
64        Set a model (`QStandardItemModel`) for the tool grid. The
65        widget actions are children of the rootIndex.
66
67        .. warning:: The model should not be deleted before the
68                     `WidgetToolGrid` instance.
69
70        """
71        if self.__model is not None:
72            self.__model.rowsInserted.disconnect(self.__on_rowsInserted)
73            self.__model.rowsRemoved.disconnect(self.__on_rowsRemoved)
74            self.__model = None
75
76        self.__model = model
77        self.__rootIndex = rootIndex
78
79        if self.__model is not None:
80            self.__model.rowsInserted.connect(self.__on_rowsInserted)
81            self.__model.rowsRemoved.connect(self.__on_rowsRemoved)
82
83        self.__initFromModel(model, rootIndex)
84
85    def model(self):
86        """
87        Return the model for the tool grid.
88        """
89        return self.__model
90
91    def rootIndex(self):
92        """
93        Return the root index of the model.
94        """
95        return self.__rootIndex
96
97    def setActionRole(self, role):
98        """
99        Set the action role. This is the model role containing a
100        `QAction` instance.
101
102        """
103        if self.__actionRole != role:
104            self.__actionRole = role
105            if self.__model:
106                self.__update()
107
108    def actionRole(self):
109        """
110        Return the action role.
111        """
112        return self.__actionRole
113
114    def actionEvent(self, event):
115        if event.type() == QEvent.ActionAdded:
116            # Creates and inserts the button instance.
117            ToolGrid.actionEvent(self, event)
118
119            button = self.buttonForAction(event.action())
120            button.installEventFilter(self.__dragListener)
121            button.installEventFilter(self.__statusTipPromoter)
122            return
123        elif event.type() == QEvent.ActionRemoved:
124            button = self.buttonForAction(event.action())
125            button.removeEventFilter(self.__dragListener)
126            button.removeEventFilter(self.__statusTipPromoter)
127
128            # Removes the button
129            ToolGrid.actionEvent(self, event)
130            return
131        else:
132            ToolGrid.actionEvent(self, event)
133
134    def __initFromModel(self, model, rootIndex):
135        """
136        Initialize the grid from the model with rootIndex as the root.
137        """
138        if not rootIndex.isValid():
139            rootItem = model.invisibleRootItem()
140        else:
141            rootItem = model.itemFromIndex(rootIndex)
142
143        self.__rootItem = rootItem
144
145        for i, item in enumerate(iter_item(rootItem)):
146            self.__insertItem(i, item)
147
148    def __insertItem(self, index, item):
149        """
150        Insert a widget action (from a `QStandardItem`) at index.
151        """
152        value = item.data(self.__actionRole)
153        if value.isValid():
154            action = value.toPyObject()
155        else:
156            action = QAction(item.text(), self)
157            action.setIcon(item.icon())
158
159        self.insertAction(index, action)
160
161    def __update(self):
162        self.clear()
163        self.__initFromModel(self.__model, self.__rootIndex)
164
165    def __on_rowsInserted(self, parent, start, end):
166        """
167        Insert items from range start:end into the grid.
168        """
169        item = self.__model.itemForIndex(parent)
170        if item == self.__rootItem:
171            for i in range(start, end + 1):
172                item = self.__rootItem.child(i)
173                self._insertItem(i, item)
174
175    def __on_rowsRemoved(self, parent, start, end):
176        """
177        Remove items from range start:end from the grid.
178        """
179        item = self.__model.itemForIndex(parent)
180        if item == self.__rootItem:
181            for i in reversed(range(start - 1, end)):
182                action = self.actions()[i]
183                self.removeAction(action)
184
185    def __startDrag(self, button):
186        """
187        Start a drag from button
188        """
189        action = button.defaultAction()
190        desc = action.data().toPyObject()  # Widget Description
191        icon = action.icon()
192        drag_data = QMimeData()
193        drag_data.setData(
194            "application/vnv.orange-canvas.registry.qualified-name",
195            desc.qualified_name
196        )
197        drag = QDrag(button)
198        drag.setPixmap(icon.pixmap(self.iconSize()))
199        drag.setMimeData(drag_data)
200        drag.exec_(Qt.CopyAction)
201
202
203class DragStartEventListener(QObject):
204    """
205    An event filter object that can be used to detect drag start
206    operation on buttons which otherwise do not support it.
207
208    """
209    dragStartOperationRequested = Signal(QAbstractButton)
210    """A drag operation started on a button."""
211
212    def __init__(self, parent=None, **kwargs):
213        QObject.__init__(self, parent, **kwargs)
214        self.button = None
215        self.buttonDownObj = None
216        self.buttonDownPos = None
217
218    def eventFilter(self, obj, event):
219        if event.type() == QEvent.MouseButtonPress:
220            self.buttonDownPos = event.pos()
221            self.buttonDownObj = obj
222            self.button = event.button()
223
224        elif event.type() == QEvent.MouseMove and obj is self.buttonDownObj:
225            if (self.buttonDownPos - event.pos()).manhattanLength() > \
226                    QApplication.startDragDistance() and \
227                    not self.buttonDownObj.hitButton(event.pos()):
228                # Process the widget's mouse event, before starting the
229                # drag operation, so the widget can update its state.
230                obj.mouseMoveEvent(event)
231
232                self.dragStartOperationRequested.emit(obj)
233
234                self.buttonDownObj.setDown(False)
235                self.button = None
236                self.buttonDownPos = None
237                self.buttonDownObj = None
238                return True  # Already handled
239
240        return QObject.eventFilter(self, obj, event)
241
242
243class WidgetToolBox(ToolBox):
244    """
245    `WidgetToolBox` widget shows a tool box containing button grids of
246    actions for a :class:`QtWidgetRegistry` item model.
247
248    """
249
250    triggered = Signal(QAction)
251    hovered = Signal(QAction)
252
253    def __init__(self, parent=None):
254        ToolBox.__init__(self, parent)
255        self.__model = None
256        self.__iconSize = QSize(25, 25)
257        self.__buttonSize = QSize(50, 50)
258        self.setSizePolicy(QSizePolicy.Fixed,
259                           QSizePolicy.Expanding)
260
261    def setIconSize(self, size):
262        """
263        Set the widget icon size (icons in the button grid).
264        """
265        self.__iconSize = size
266        for widget in  map(self.widget, range(self.count())):
267            widget.setIconSize(size)
268
269    def iconSize(self):
270        """
271        Return the widget buttons icon size.
272        """
273        return self.__iconSize
274
275    iconSize_ = Property(QSize, fget=iconSize, fset=setIconSize,
276                         designable=True)
277
278    def setButtonSize(self, size):
279        """
280        Set fixed widget button size.
281        """
282        self.__buttonSize = size
283        for widget in map(self.widget, range(self.count())):
284            widget.setButtonSize(size)
285
286    def buttonSize(self):
287        """Return the widget button size
288        """
289        return self.__buttonSize
290
291    buttonSize_ = Property(QSize, fget=buttonSize, fset=setButtonSize,
292                           designable=True)
293
294    def saveState(self):
295        """
296        Return the toolbox state (as a `QByteArray`).
297
298        .. note:: Individual tabs are stored by their action's text.
299
300        """
301        version = 2
302
303        actions = map(self.tabAction, range(self.count()))
304        expanded = [action for action in actions if action.isChecked()]
305        expanded = [action.text() for action in expanded]
306
307        byte_array = QByteArray()
308        stream = QDataStream(byte_array, QIODevice.WriteOnly)
309        stream.writeInt(version)
310        stream.writeQStringList(expanded)
311
312        return byte_array
313
314    def restoreState(self, state):
315        """
316        Restore the toolbox from a :class:`QByteArray` `state`.
317
318        .. note:: The toolbox should already be populated for the state
319                  changes to take effect.
320
321        """
322        # In version 1 of saved state the state was saved in
323        # a simple dict repr string.
324        if isinstance(state, QByteArray):
325            stream = QDataStream(state, QIODevice.ReadOnly)
326            version = stream.readInt()
327            if version == 2:
328                expanded = stream.readQStringList()
329                for action in map(self.tabAction, range(self.count())):
330                    if (action.text() in expanded) != action.isChecked():
331                        action.trigger()
332
333                return True
334        return False
335
336    def setModel(self, model):
337        """
338        Set the widget registry model (:class:`QStandardItemModel`) for
339        this toolbox.
340
341        """
342        if self.__model is not None:
343            self.__model.itemChanged.disconnect(self.__on_itemChanged)
344            self.__model.rowsInserted.disconnect(self.__on_rowsInserted)
345            self.__model.rowsRemoved.disconnect(self.__on_rowsRemoved)
346
347        self.__model = model
348        if self.__model is not None:
349            self.__model.itemChanged.connect(self.__on_itemChanged)
350            self.__model.rowsInserted.connect(self.__on_rowsInserted)
351            self.__model.rowsRemoved.connect(self.__on_rowsRemoved)
352
353        self.__initFromModel(self.__model)
354
355    def __initFromModel(self, model):
356        for cat_item in iter_item(model.invisibleRootItem()):
357            self.__insertItem(cat_item, self.count())
358
359    def __insertItem(self, item, index):
360        """
361        Insert category item at index.
362        """
363        grid = WidgetToolGrid()
364        grid.setModel(item.model(), item.index())
365
366        grid.actionTriggered.connect(self.triggered)
367        grid.actionHovered.connect(self.hovered)
368
369        grid.setIconSize(self.__iconSize)
370        grid.setButtonSize(self.__buttonSize)
371
372        text = item.text()
373        icon = item.icon()
374        tooltip = item.toolTip()
375
376        # Set the 'tab-title' property to text.
377        grid.setProperty("tab-title", text)
378        grid.setObjectName("widgets-toolbox-grid")
379
380        self.insertItem(index, grid, text, icon, tooltip)
381        button = self.tabButton(index)
382
383        # Set the 'highlight' color
384        if item.data(Qt.BackgroundRole).isValid():
385            brush = item.background()
386        elif item.data(QtWidgetRegistry.BACKGROUND_ROLE).isValid():
387            brush = item.data(QtWidgetRegistry.BACKGROUND_ROLE).toPyObject()
388        else:
389            brush = self.palette().brush(QPalette.Button)
390
391        if not brush.gradient():
392            gradient = create_tab_gradient(brush.color())
393            brush = QBrush(gradient)
394
395        palette = button.palette()
396        palette.setBrush(QPalette.Highlight, brush)
397        button.setPalette(palette)
398
399    def __on_itemChanged(self, item):
400        """
401        Item contents have changed.
402        """
403        parent = item.parent()
404        if parent is self.__model.invisibleRootItem():
405            button = self.tabButton(item.row())
406            button.setIcon(item.icon())
407            button.setText(item.text())
408            button.setToolTip(item.toolTip())
409
410    def __on_rowsInserted(self, parent, start, end):
411        """
412        Items have been inserted in the model.
413        """
414        # Only the top level items (categories) are handled here.
415        if not parent.isValid():
416            root = self.__model.invisibleRootItem()
417            for i in range(start, end + 1):
418                item = root.child(i)
419                self.__insertItem(item, i)
420
421    def __on_rowsRemoved(self, parent, start, end):
422        """
423        Rows have been removed from the model.
424        """
425        # Only the top level items (categories) are handled here.
426        if not parent.isValid():
427            for i in range(end, start - 1, -1):
428                self.removeItem(i)
Note: See TracBrowser for help on using the repository browser.