Commits

Aleš Erjavec  committed 12e60f5

Added a popup menu class with widgets displayed in a list.

  • Participants
  • Parent commits a7627a1

Comments (0)

Files changed (4)

File Orange/OrangeCanvas/application/canvasmain.py

 from PyQt4.QtGui import (
     QMainWindow, QWidget, QAction, QActionGroup, QMenu, QMenuBar, QDialog,
     QFileDialog, QMessageBox, QVBoxLayout, QSizePolicy, QColor, QKeySequence,
-    QIcon, QToolBar, QToolButton, QDockWidget, QDesktopServices, QApplication
+    QIcon, QToolBar, QToolButton, QDockWidget, QDesktopServices, QApplication,
+    QCursor
 )
 
 from PyQt4.QtCore import (
 
 from ..help import HelpManager
 
-from .canvastooldock import CanvasToolDock, QuickCategoryToolbar
+from .canvastooldock import CanvasToolDock, QuickCategoryToolbar, \
+                            CategoryPopupMenu
 from .aboutdialog import AboutDialog
 from .schemeinfo import SchemeInfoDialog
 from .outputview import OutputView
         """The quick category menu action triggered.
         """
         category = action.text()
-        for i in range(self.widgets_tool_box.count()):
-            cat_act = self.widgets_tool_box.tabAction(i)
-            if cat_act.text() == category:
-                if not cat_act.isChecked():
-                    # Trigger the action to expand the tool grid contained
-                    # within.
-                    cat_act.trigger()
+        if self.use_popover:
+            # Show a popup menu with the widgets in the category
+            m = CategoryPopupMenu(self.quick_category)
+            reg = self.widget_registry.model()
+            i = index(self.widget_registry.categories(), category,
+                      predicate=lambda name, cat: cat.name == name)
+            if i != -1:
+                m.setCategoryItem(reg.item(i))
+                action = m.exec_(QCursor.pos())
+                if action is not None:
+                    self.on_tool_box_widget_activated(action)
 
-            else:
-                if cat_act.isChecked():
-                    # Trigger the action to hide the tool grid contained
-                    # within.
-                    cat_act.trigger()
+        else:
+            for i in range(self.widgets_tool_box.count()):
+                cat_act = self.widgets_tool_box.tabAction(i)
+                cat_act.setChecked(cat_act.text() == category)
 
-        self.dock_widget.expand()
+            self.dock_widget.expand()
 
     def set_scheme_margins_enabled(self, enabled):
         """Enable/disable the margins around the scheme document.
             settings.value("open-in-external-browser", defaultValue=False,
                            type=bool)
 
+        self.use_popover = \
+            settings.value("toolbox-dock-use-popover-menu", defaultValue=True,
+                           type=bool)
+
 
 def updated_flags(flags, mask, state):
     if state:

File Orange/OrangeCanvas/application/canvastooldock.py

 Orange Canvas Tool Dock widget
 
 """
+import sys
+
 from PyQt4.QtGui import (
     QWidget, QSplitter, QVBoxLayout, QTextEdit, QAction, QPalette,
-    QSizePolicy
+    QSizePolicy, QApplication, QDrag
 )
 
-from PyQt4.QtCore import Qt, QSize, QObject, QPropertyAnimation, QEvent
-from PyQt4.QtCore import pyqtProperty as Property
+from PyQt4.QtCore import (
+    Qt, QSize, QObject, QPropertyAnimation, QEvent, QRect,
+    QModelIndex, QPersistentModelIndex, QEventLoop, QMimeData
+)
+
+from PyQt4.QtCore import pyqtProperty as Property, pyqtSignal as Signal
 
 from ..gui.toolgrid import ToolGrid
 from ..gui.toolbar import DynamicResizeToolBar
 from ..gui.quickhelp import QuickHelp
+from ..gui.framelesswindow import FramelessWindow
+from ..document.quickmenu import MenuPage
 from .widgettoolbox import WidgetToolBox, iter_item
 
 from ..registry.qt import QtWidgetRegistry
+from ..utils.qtcompat import toPyObject
 
 
 class SplitterResizer(QObject):
             for index in range(end, start - 1, -1):
                 action = self._gridSlots[index].action
                 self.removeAction(action)
+
+
+class CategoryPopupMenu(FramelessWindow):
+    triggered = Signal(QAction)
+    hovered = Signal(QAction)
+
+    def __init__(self, parent=None, **kwargs):
+        FramelessWindow.__init__(self, parent, **kwargs)
+        self.setWindowFlags(self.windowFlags() | Qt.Popup)
+
+        layout = QVBoxLayout()
+        layout.setContentsMargins(6, 6, 6, 6)
+
+        self.__menu = MenuPage()
+        self.__menu.setActionRole(QtWidgetRegistry.WIDGET_ACTION_ROLE)
+
+        if sys.platform == "darwin":
+            self.__menu.view().setAttribute(Qt.WA_MacShowFocusRect, False)
+
+        self.__menu.triggered.connect(self.__onTriggered)
+        self.__menu.hovered.connect(self.hovered)
+
+        self.__dragListener = ItemViewDragStartEventListener(self)
+        self.__dragListener.dragStarted.connect(self.__onDragStarted)
+
+        self.__menu.view().viewport().installEventFilter(self.__dragListener)
+
+        layout.addWidget(self.__menu)
+
+        self.setLayout(layout)
+
+        self.__action = None
+        self.__loop = None
+        self.__item = None
+
+    def setCategoryItem(self, item):
+        """
+        Set the category root item (:class:`QStandardItem`).
+        """
+        self.__item = item
+        model = item.model()
+        self.__menu.setModel(model)
+        self.__menu.setRootIndex(item.index())
+
+    def popup(self, pos=None):
+        if pos is None:
+            pos = self.pos()
+        geom = widget_popup_geometry(pos, self)
+        self.setGeometry(geom)
+        self.show()
+
+    def exec_(self, pos=None):
+        self.popup(pos)
+        self.__loop = QEventLoop()
+
+        self.__action = None
+        self.__loop.exec_()
+        self.__loop = None
+
+        if self.__action is not None:
+            action = self.__action
+        else:
+            action = None
+        return action
+
+    def hideEvent(self, event):
+        if self.__loop is not None:
+            self.__loop.exit(0)
+
+        return FramelessWindow.hideEvent(self, event)
+
+    def __onTriggered(self, action):
+        self.__action = action
+        self.triggered.emit(action)
+        self.hide()
+
+        if self.__loop:
+            self.__loop.exit(0)
+
+    def __onDragStarted(self, index):
+        desc = toPyObject(index.data(QtWidgetRegistry.WIDGET_DESC_ROLE))
+        icon = toPyObject(index.data(Qt.DecorationRole))
+
+        drag_data = QMimeData()
+        drag_data.setData(
+            "application/vnv.orange-canvas.registry.qualified-name",
+            desc.qualified_name
+        )
+        drag = QDrag(self)
+        drag.setPixmap(icon.pixmap(38))
+        drag.setMimeData(drag_data)
+
+        # TODO: Should animate (accept) hide.
+        self.hide()
+
+        # When a drag is started and the menu hidden the item's tool tip
+        # can still show for a short time UNDER the cursor preventing a
+        # drop.
+        viewport = self.__menu.view().viewport()
+        filter = ToolTipEventFilter()
+        viewport.installEventFilter(filter)
+
+        drag.exec_(Qt.CopyAction)
+
+        viewport.removeEventFilter(filter)
+
+
+class ItemViewDragStartEventListener(QObject):
+    dragStarted = Signal(QModelIndex)
+
+    def __init__(self, parent=None):
+        QObject.__init__(self, parent)
+        self._pos = None
+        self._index = None
+
+    def eventFilter(self, viewport, event):
+        view = viewport.parent()
+
+        if event.type() == QEvent.MouseButtonPress and \
+                event.button() == Qt.LeftButton:
+
+            index = view.indexAt(event.pos())
+
+            if index is not None:
+                self._pos = event.pos()
+                self._index = QPersistentModelIndex(index)
+
+        elif event.type() == QEvent.MouseMove and self._pos is not None and \
+                ((self._pos - event.pos()).manhattanLength() >=
+                 QApplication.startDragDistance()):
+
+            if self._index.isValid():
+                # Map to a QModelIndex in the model.
+                index = self._index
+                index = index.model().index(index.row(), index.column(),
+                                            index.parent())
+                self._pos = None
+                self._index = None
+
+                self.dragStarted.emit(index)
+
+        return QObject.eventFilter(self, view, event)
+
+
+class ToolTipEventFilter(QObject):
+    def eventFilter(self, receiver, event):
+        if event.type() == QEvent.ToolTip:
+            return True
+
+        return QObject.eventFilter(self, receiver, event)
+
+
+def widget_popup_geometry(pos, widget):
+    widget.ensurePolished()
+
+    if widget.testAttribute(Qt.WA_Resized):
+        size = widget.size()
+    else:
+        size = widget.sizeHint()
+
+    desktop = QApplication.desktop()
+    screen_geom = desktop.availableGeometry(pos)
+
+    # Adjust the size to fit inside the screen.
+    if size.height() > screen_geom.height():
+        size.setHeight(screen_geom.height())
+    if size.width() > screen_geom.width():
+        size.setWidth(screen_geom.width())
+
+    geom = QRect(pos, size)
+
+    if geom.top() < screen_geom.top():
+        geom.setTop(screen_geom.top())
+
+    if geom.left() < screen_geom.left():
+        geom.setLeft(screen_geom.left())
+
+    bottom_margin = screen_geom.bottom() - geom.bottom()
+    right_margin = screen_geom.right() - geom.right()
+    if bottom_margin < 0:
+        # Falls over the bottom of the screen, move it up.
+        geom.translate(0, bottom_margin)
+
+    # TODO: right to left locale
+    if right_margin < 0:
+        # Falls over the right screen edge, move the menu to the
+        # other side of pos.
+        geom.translate(-size.width(), 0)
+
+    return geom

File Orange/OrangeCanvas/config.py

      ("mainwindow/toolbox-dock-movable", bool, True,
       "Is the canvas toolbox movable (between left and right edge)"),
 
+     ("mainwindow/toolbox-dock-use-popover-menu", bool, True,
+      "Use a popover menu to select a widget when clicking on a category "
+      "button"),
+
      ("mainwindow/number-of-recent-schemes", int, 7,
       "Number of recent schemes to keep in history"),
 

File Orange/OrangeCanvas/styles/orange.qss

 }
 
 
+/* QuickCategoryToolbar popup menus */
 
-/*
- *QuickCategoryToolbar _QuickCategoryButton {
- *    qproperty-nativeStyling_: "true";
- *    background-color: palette(button);
- *    border: none;
- *    border-bottom: 1px solid palette(dark);
- *}
- */
+CategoryPopupMenu {
+	background-color: #E9EFF2;
+}
+
+CategoryPopupMenu ToolTree QTreeView::item {
+	height: 25px;
+	border-bottom: 1px solid #e9eff2;
+}
+
+CategoryPopupMenu QTreeView::item:hover {
+	background: qlineargradient(
+		x1: 0, y1: 0, x2: 0, y2: 1,
+		stop: 0 #688EF6,
+		stop: 0.5 #4047f4,
+		stop: 1.0 #2D68F3
+	);
+	color: white;
+}
+
+CategoryPopupMenu QTreeView::item:selected {
+	background-color: blue;
+	color: white;
+}
 
 
 /* Canvas Dock Header */