source: orange/Orange/OrangeCanvas/application/canvastooldock.py @ 11506:31e40306cb73

Revision 11506:31e40306cb73, 17.4 KB checked in by Ales Erjavec <ales.erjavec@…>, 11 months ago (diff)

Added a linear gradient to the category buttons in the toolbar.

RevLine 
[11120]1"""
2Orange Canvas Tool Dock widget
3
4"""
[11495]5import sys
6
[11120]7from PyQt4.QtGui import (
8    QWidget, QSplitter, QVBoxLayout, QTextEdit, QAction, QPalette,
[11495]9    QSizePolicy, QApplication, QDrag
[11120]10)
11
[11495]12from PyQt4.QtCore import (
[11502]13    Qt, QSize, QObject, QPropertyAnimation, QEvent, QRect, QPoint,
[11495]14    QModelIndex, QPersistentModelIndex, QEventLoop, QMimeData
15)
16
17from PyQt4.QtCore import pyqtProperty as Property, pyqtSignal as Signal
[11120]18
19from ..gui.toolgrid import ToolGrid
20from ..gui.toolbar import DynamicResizeToolBar
[11243]21from ..gui.quickhelp import QuickHelp
[11495]22from ..gui.framelesswindow import FramelessWindow
23from ..document.quickmenu import MenuPage
[11506]24from ..document.quickmenu import create_css_gradient
[11120]25from .widgettoolbox import WidgetToolBox, iter_item
[11243]26
[11133]27from ..registry.qt import QtWidgetRegistry
[11495]28from ..utils.qtcompat import toPyObject
[11120]29
30
31class SplitterResizer(QObject):
32    """An object able to control the size of a widget in a
33    QSpliter instance.
34
35    """
36
37    def __init__(self, parent=None):
38        QObject.__init__(self, parent)
39        self.__splitter = None
40        self.__widget = None
41        self.__animationEnabled = True
42        self.__size = -1
43        self.__expanded = False
44        self.__animation = QPropertyAnimation(self, "size_", self)
45
46        self.__action = QAction("toogle-expanded", self, checkable=True)
47        self.__action.triggered[bool].connect(self.setExpanded)
48
49    def setSize(self, size):
50        """Set the size of the controlled widget (either width or height
51        depending on the orientation).
52
53        """
54        if self.__size != size:
55            self.__size = size
56            self.__update()
57
58    def size(self):
59        """Return the size of the widget in the splitter (either height of
60        width) depending on the splitter orientation.
61
62        """
63        if self.__splitter and self.__widget:
64            index = self.__splitter.indexOf(self.__widget)
65            sizes = self.__splitter.sizes()
66            return sizes[index]
67        else:
68            return -1
69
70    size_ = Property(int, fget=size, fset=setSize)
71
72    def setAnimationEnabled(self, enable):
73        """Enable/disable animation.
74        """
75        self.__animation.setDuration(0 if enable else 200)
76
77    def animationEnabled(self):
78        return self.__animation.duration() == 0
79
80    def setSplitterAndWidget(self, splitter, widget):
81        """Set the QSplitter and QWidget instance the resizer should control.
82
83        .. note:: the widget must be in the splitter.
84
85        """
86        if splitter and widget and not splitter.indexOf(widget) > 0:
87            raise ValueError("Widget must be in a spliter.")
88
89        if self.__widget:
90            self.__widget.removeEventFilter()
91        self.__splitter = splitter
92        self.__widget = widget
93
94        if widget:
95            widget.installEventFilter(self)
96
97        self.__update()
98
99    def toogleExpandedAction(self):
100        """Return a QAction that can be used to toggle expanded state.
101        """
102        return self.__action
103
104    def open(self):
105        """Open the controlled widget (expand it to it sizeHint).
106        """
107        self.__expanded = True
[11242]108        self.__action.setChecked(True)
[11120]109
110        if not (self.__splitter and self.__widget):
111            return
112
113        size = self.size()
114        if size > 0:
115            # Already has non zero size.
116            return
117
118        hint = self.__widget.sizeHint()
119
120        if self.__splitter.orientation() == Qt.Vertical:
121            end = hint.height()
122        else:
123            end = hint.width()
124
125        self.__animation.setStartValue(0)
126        self.__animation.setEndValue(end)
127        self.__animation.start()
128
129    def close(self):
130        """Close the controlled widget (shrink to size 0).
131        """
132        self.__expanded = False
[11242]133        self.__action.setChecked(False)
[11120]134
135        if not (self.__splitter and self.__widget):
136            return
137
138        self.__animation.setStartValue(self.size())
139        self.__animation.setEndValue(0)
140        self.__animation.start()
141
142    def setExpanded(self, expanded):
143        """Set the expanded state.
144
145        """
146        if self.__expanded != expanded:
147            if expanded:
148                self.open()
149            else:
150                self.close()
151
152    def expanded(self):
153        """Return the expanded state.
154        """
155        return self.__expanded
156
157    def __update(self):
158        """Update the splitter sizes.
159        """
160        if self.__splitter and self.__widget:
161            splitter = self.__splitter
162            index = splitter.indexOf(self.__widget)
163            sizes = splitter.sizes()
164            current = sizes[index]
165            diff = current - self.__size
166            sizes[index] = self.__size
167            sizes[index - 1] = sizes[index - 1] + diff
168
169            self.__splitter.setSizes(sizes)
170
171    def eventFilter(self, obj, event):
172        if event.type() == QEvent.Resize:
173            if self.__splitter.orientation() == Qt.Vertical:
174                size = event.size().height()
175            else:
176                size = event.size().width()
177
178            if self.__expanded and size == 0:
179                self.__action.setChecked(False)
180                self.__expanded = False
181            elif not self.__expanded and size > 0:
182                self.__action.setChecked(True)
183                self.__expanded = True
184
185        return QObject.eventFilter(self, obj, event)
186
187
[11243]188class QuickHelpWidget(QuickHelp):
[11120]189    def minimumSizeHint(self):
190        """Reimplemented to allow the Splitter to resize the widget
191        with a continuous animation.
192
193        """
194        hint = QTextEdit.minimumSizeHint(self)
195        return QSize(hint.width(), 0)
196
197
198class CanvasToolDock(QWidget):
199    """Canvas dock widget with widget toolbox, quick help and
200    canvas actions.
201
202    """
203    def __init__(self, parent=None, **kwargs):
204        QWidget.__init__(self, parent, **kwargs)
205
206        self.__setupUi()
207
208    def __setupUi(self):
209        layout = QVBoxLayout()
210        layout.setContentsMargins(0, 0, 0, 0)
211        layout.setSpacing(0)
212
213        self.toolbox = WidgetToolBox()
214
215        self.help = QuickHelpWidget(objectName="quick-help")
216
217        self.__splitter = QSplitter()
218        self.__splitter.setOrientation(Qt.Vertical)
219
220        self.__splitter.addWidget(self.toolbox)
221        self.__splitter.addWidget(self.help)
222
223        self.toolbar = DynamicResizeToolBar()
224        self.toolbar.setMovable(False)
225        self.toolbar.setFloatable(False)
226
227        self.toolbar.setSizePolicy(QSizePolicy.Ignored,
228                                   QSizePolicy.Preferred)
229
230        layout.addWidget(self.__splitter, 10)
231        layout.addWidget(self.toolbar)
232
233        self.setLayout(layout)
234        self.__splitterResizer = SplitterResizer()
235        self.__splitterResizer.setSplitterAndWidget(self.__splitter, self.help)
236
237    def setQuickHelpVisible(self, state):
238        """Set the quick help box visibility status.
239        """
240        self.__splitterResizer.setExpanded(state)
241
242    def quickHelpVisible(self):
243        return self.__splitterResizer.expanded()
244
245    def setQuickHelpAnimationEnabled(self, enabled):
246        """Enable/disable the quick help animation.
247        """
248        self.__splitterResizer.setAnimationEnabled(enabled)
249
250    def toogleQuickHelpAction(self):
251        """Return a checkable QAction for help show/hide.
252        """
253        return self.__splitterResizer.toogleExpandedAction()
254
255
256class QuickCategoryToolbar(ToolGrid):
257    """A toolbar with category buttons.
258    """
259    def __init__(self, parent=None, buttonSize=None, iconSize=None):
260        ToolGrid.__init__(self, parent, 1, buttonSize, iconSize,
261                          Qt.ToolButtonIconOnly)
262        self.__model = None
263
264    def setColumnCount(self, count):
265        raise Exception("Cannot set the column count on a Toolbar")
266
267    def setModel(self, model):
268        """Set the registry model.
269        """
270        if self.__model is not None:
271            self.__model.itemChanged.disconnect(self.__on_itemChanged)
272            self.__model.rowsInserted.disconnect(self.__on_rowsInserted)
273            self.__model.rowsRemoved.disconnect(self.__on_rowsRemoved)
274            self.clear()
275
276        self.__model = model
277        if self.__model is not None:
278            self.__model.itemChanged.connect(self.__on_itemChanged)
279            self.__model.rowsInserted.connect(self.__on_rowsInserted)
280            self.__model.rowsRemoved.connect(self.__on_rowsRemoved)
281            self.__initFromModel(model)
282
283    def __initFromModel(self, model):
284        """Initialize the toolbar from the model.
285        """
286        root = model.invisibleRootItem()
287        for item in iter_item(root):
288            action = self.createActionForItem(item)
289            self.addAction(action)
290
291    def createActionForItem(self, item):
292        """Create the QAction instance for item.
293        """
294        action = QAction(item.icon(), item.text(), self,
295                         toolTip=item.toolTip())
296        action.setData(item)
297        return action
298
299    def createButtonForAction(self, action):
300        """Create a button for the action.
301        """
302        button = ToolGrid.createButtonForAction(self, action)
303
304        item = action.data().toPyObject()
[11133]305        if item.data(Qt.BackgroundRole).isValid():
306            brush = item.background()
307        elif item.data(QtWidgetRegistry.BACKGROUND_ROLE).isValid():
308            brush = item.data(QtWidgetRegistry.BACKGROUND_ROLE).toPyObject()
309        else:
310            brush = self.palette().brush(QPalette.Button)
311
[11120]312        palette = button.palette()
313        palette.setColor(QPalette.Button, brush.color())
314        palette.setColor(QPalette.Window, brush.color())
315        button.setPalette(palette)
316        button.setProperty("quick-category-toolbutton", True)
317
318        style_sheet = ("QToolButton {\n"
[11506]319                       "    background: %s;\n"
[11120]320                       "    border: none;\n"
[11506]321                       "    border-bottom: 1px solid palette(mid);\n"
[11120]322                       "}")
[11506]323        button.setStyleSheet(style_sheet % create_css_gradient(brush.color()))
[11120]324
325        return button
326
327    def __on_itemChanged(self, item):
328        root = self.__model.invisibleRootItem()
329        if item.parentItem() == root:
330            row = item.row()
331            action = self._gridSlots[row].action
332            action.setText(item.text())
333            action.setIcon(item.icon())
334            action.setToolTip(item.toolTip())
335
336    def __on_rowsInserted(self, parent, start, end):
337        root = self.__model.invisibleRootItem()
338        if root == parent:
339            for index in range(start, end + 1):
340                item = parent.child(index)
341                self.addAction(self.createActionForItem(item))
342
343    def __on_rowsRemoved(self, parent, start, end):
344        root = self.__model.invisibleRootItem()
345        if root == parent:
346            for index in range(end, start - 1, -1):
347                action = self._gridSlots[index].action
348                self.removeAction(action)
[11495]349
350
351class CategoryPopupMenu(FramelessWindow):
352    triggered = Signal(QAction)
353    hovered = Signal(QAction)
354
355    def __init__(self, parent=None, **kwargs):
356        FramelessWindow.__init__(self, parent, **kwargs)
357        self.setWindowFlags(self.windowFlags() | Qt.Popup)
358
359        layout = QVBoxLayout()
360        layout.setContentsMargins(6, 6, 6, 6)
361
362        self.__menu = MenuPage()
363        self.__menu.setActionRole(QtWidgetRegistry.WIDGET_ACTION_ROLE)
364
365        if sys.platform == "darwin":
366            self.__menu.view().setAttribute(Qt.WA_MacShowFocusRect, False)
367
368        self.__menu.triggered.connect(self.__onTriggered)
369        self.__menu.hovered.connect(self.hovered)
370
371        self.__dragListener = ItemViewDragStartEventListener(self)
372        self.__dragListener.dragStarted.connect(self.__onDragStarted)
373
374        self.__menu.view().viewport().installEventFilter(self.__dragListener)
375
376        layout.addWidget(self.__menu)
377
378        self.setLayout(layout)
379
380        self.__action = None
381        self.__loop = None
382        self.__item = None
383
384    def setCategoryItem(self, item):
385        """
386        Set the category root item (:class:`QStandardItem`).
387        """
388        self.__item = item
389        model = item.model()
390        self.__menu.setModel(model)
391        self.__menu.setRootIndex(item.index())
392
393    def popup(self, pos=None):
394        if pos is None:
395            pos = self.pos()
396        geom = widget_popup_geometry(pos, self)
397        self.setGeometry(geom)
398        self.show()
399
400    def exec_(self, pos=None):
401        self.popup(pos)
402        self.__loop = QEventLoop()
403
404        self.__action = None
405        self.__loop.exec_()
406        self.__loop = None
407
408        if self.__action is not None:
409            action = self.__action
410        else:
411            action = None
412        return action
413
414    def hideEvent(self, event):
415        if self.__loop is not None:
416            self.__loop.exit(0)
417
418        return FramelessWindow.hideEvent(self, event)
419
420    def __onTriggered(self, action):
421        self.__action = action
422        self.triggered.emit(action)
423        self.hide()
424
425        if self.__loop:
426            self.__loop.exit(0)
427
428    def __onDragStarted(self, index):
429        desc = toPyObject(index.data(QtWidgetRegistry.WIDGET_DESC_ROLE))
430        icon = toPyObject(index.data(Qt.DecorationRole))
431
432        drag_data = QMimeData()
433        drag_data.setData(
434            "application/vnv.orange-canvas.registry.qualified-name",
435            desc.qualified_name
436        )
437        drag = QDrag(self)
438        drag.setPixmap(icon.pixmap(38))
439        drag.setMimeData(drag_data)
440
441        # TODO: Should animate (accept) hide.
442        self.hide()
443
444        # When a drag is started and the menu hidden the item's tool tip
445        # can still show for a short time UNDER the cursor preventing a
446        # drop.
447        viewport = self.__menu.view().viewport()
448        filter = ToolTipEventFilter()
449        viewport.installEventFilter(filter)
450
451        drag.exec_(Qt.CopyAction)
452
453        viewport.removeEventFilter(filter)
454
455
456class ItemViewDragStartEventListener(QObject):
457    dragStarted = Signal(QModelIndex)
458
459    def __init__(self, parent=None):
460        QObject.__init__(self, parent)
461        self._pos = None
462        self._index = None
463
464    def eventFilter(self, viewport, event):
465        view = viewport.parent()
466
467        if event.type() == QEvent.MouseButtonPress and \
468                event.button() == Qt.LeftButton:
469
470            index = view.indexAt(event.pos())
471
472            if index is not None:
473                self._pos = event.pos()
474                self._index = QPersistentModelIndex(index)
475
476        elif event.type() == QEvent.MouseMove and self._pos is not None and \
477                ((self._pos - event.pos()).manhattanLength() >=
478                 QApplication.startDragDistance()):
479
480            if self._index.isValid():
481                # Map to a QModelIndex in the model.
482                index = self._index
483                index = index.model().index(index.row(), index.column(),
484                                            index.parent())
485                self._pos = None
486                self._index = None
487
488                self.dragStarted.emit(index)
489
490        return QObject.eventFilter(self, view, event)
491
492
493class ToolTipEventFilter(QObject):
494    def eventFilter(self, receiver, event):
495        if event.type() == QEvent.ToolTip:
496            return True
497
498        return QObject.eventFilter(self, receiver, event)
499
500
501def widget_popup_geometry(pos, widget):
502    widget.ensurePolished()
503
504    if widget.testAttribute(Qt.WA_Resized):
505        size = widget.size()
506    else:
507        size = widget.sizeHint()
508
509    desktop = QApplication.desktop()
510    screen_geom = desktop.availableGeometry(pos)
511
512    # Adjust the size to fit inside the screen.
513    if size.height() > screen_geom.height():
514        size.setHeight(screen_geom.height())
515    if size.width() > screen_geom.width():
516        size.setWidth(screen_geom.width())
517
518    geom = QRect(pos, size)
519
520    if geom.top() < screen_geom.top():
521        geom.setTop(screen_geom.top())
522
523    if geom.left() < screen_geom.left():
524        geom.setLeft(screen_geom.left())
525
526    bottom_margin = screen_geom.bottom() - geom.bottom()
527    right_margin = screen_geom.right() - geom.right()
528    if bottom_margin < 0:
529        # Falls over the bottom of the screen, move it up.
530        geom.translate(0, bottom_margin)
531
532    # TODO: right to left locale
533    if right_margin < 0:
534        # Falls over the right screen edge, move the menu to the
535        # other side of pos.
536        geom.translate(-size.width(), 0)
537
538    return geom
[11502]539
540
541def popup_position_from_source(popup, source, orientation=Qt.Vertical):
542    popup.ensurePolished()
543    source.ensurePolished()
544
545    if popup.testAttribute(Qt.WA_Resized):
546        size = popup.size()
547    else:
548        size = popup.sizeHint()
549
550    desktop = QApplication.desktop()
551    screen_geom = desktop.availableGeometry(source)
552    source_rect = QRect(source.mapToGlobal(QPoint(0, 0)), source.size())
553
554    if orientation == Qt.Vertical:
555        if source_rect.right() + size.width() < screen_geom.width():
556            x = source_rect.right()
557        else:
558            x = source_rect.left() - size.width()
559
560        # bottom overflow
561        dy = source_rect.top() + size.height() - screen_geom.height()
562        if dy < 0:
563            y = source_rect.top()
564        else:
565            y = source_rect.top() - dy
566    else:
567        # right overflow
568        dx = source_rect.left() + size.width() - screen_geom.width()
569        if dx < 0:
570            x = source_rect.left()
571        else:
572            x = source_rect.left() - dx
573
574        if source_rect.bottom() + size.height() < screen_geom.height():
575            y = source_rect.bottom()
576        else:
577            y = source_rect.top() - size.height()
578
579    return QPoint(x, y)
Note: See TracBrowser for help on using the repository browser.