source: orange/Orange/OrangeCanvas/application/canvastooldock.py @ 11516:8ea3fb26ee98

Revision 11516:8ea3fb26ee98, 17.4 KB checked in by Ales Erjavec <ales.erjavec@…>, 11 months ago (diff)

Use absolute screen geometry to position the popup.

Line 
1"""
2Orange Canvas Tool Dock widget
3
4"""
5import sys
6
7from PyQt4.QtGui import (
8    QWidget, QSplitter, QVBoxLayout, QTextEdit, QAction, QPalette,
9    QSizePolicy, QApplication, QDrag
10)
11
12from PyQt4.QtCore import (
13    Qt, QSize, QObject, QPropertyAnimation, QEvent, QRect, QPoint,
14    QModelIndex, QPersistentModelIndex, QEventLoop, QMimeData
15)
16
17from PyQt4.QtCore import pyqtProperty as Property, pyqtSignal as Signal
18
19from ..gui.toolgrid import ToolGrid
20from ..gui.toolbar import DynamicResizeToolBar
21from ..gui.quickhelp import QuickHelp
22from ..gui.framelesswindow import FramelessWindow
23from ..document.quickmenu import MenuPage
24from ..document.quickmenu import create_css_gradient
25from .widgettoolbox import WidgetToolBox, iter_item
26
27from ..registry.qt import QtWidgetRegistry
28from ..utils.qtcompat import toPyObject
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
108        self.__action.setChecked(True)
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
133        self.__action.setChecked(False)
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
188class QuickHelpWidget(QuickHelp):
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()
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
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"
319                       "    background: %s;\n"
320                       "    border: none;\n"
321                       "    border-bottom: 1px solid palette(mid);\n"
322                       "}")
323        button.setStyleSheet(style_sheet % create_css_gradient(brush.color()))
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)
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
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.right():
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.bottom()
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.right()
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.bottom():
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.