source: orange/Orange/OrangeCanvas/application/widgettoolbox.py @ 11254:1097e40c8736

Revision 11254:1097e40c8736, 12.4 KB checked in by Ales Erjavec <ales.erjavec@…>, 16 months ago (diff)

Changed the WidgetToolBox save/restoreState format.

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