source: orange/Orange/OrangeCanvas/application/widgettoolbox.py @ 11177:9d471e1fca3e

Revision 11177:9d471e1fca3e, 10.8 KB checked in by Ales Erjavec <ales.erjavec@…>, 18 months ago (diff)

Cleanup of the ToolGrid code and interface.

Moved the add/remove action code to actionEvent handler, also added
more tests.

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