source: orange/Orange/OrangeCanvas/application/widgettoolbox.py @ 11309:7b2804352bbe

Revision 11309:7b2804352bbe, 12.5 KB checked in by Ales Erjavec <ales.erjavec@…>, 14 months ago (diff)

Fix a test when the WidgetsToolBox widget outlives the model's actions.

Also added a warning to setModel's docstring about the life time of the
model.

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