source: orange/Orange/OrangeCanvas/application/widgettoolbox.py @ 11243:e788addef69c

Revision 11243:e788addef69c, 12.1 KB checked in by Ales Erjavec <ales.erjavec@…>, 16 months ago (diff)

Implemented a different way to handle toolbox/canvas hover/selection help text.

Now using a QStatusTip subclass to notify the top level window.

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