source: orange/Orange/OrangeCanvas/application/widgettoolbox.py @ 11508:5f54d6c68d9e

Revision 11508:5f54d6c68d9e, 13.2 KB checked in by Ales Erjavec <ales.erjavec@…>, 11 months ago (diff)

Moved gradient functions into gui.utils.

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