Commits

Aleš Erjavec committed 28c3d24 Merge

Merge

Comments (0)

Files changed (11)

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.
 
         scheme_doc.setScheme(new_scheme)
 
-        old_scheme.save_widget_settings()
-        old_scheme.close_all_open_widgets()
-        old_scheme.signal_manager.stop()
+        # Send a close event to the Scheme, it is responsible for
+        # closing/clearing all resources (widgets).
+        QApplication.sendEvent(old_scheme, QEvent(QEvent.Close))
+
         old_scheme.deleteLater()
 
     def ask_save_changes(self):
                 event.ignore()
                 return
 
+        old_scheme = document.scheme()
+
         # Set an empty scheme to clear the document
         document.setScheme(widgetsscheme.WidgetsScheme())
 
-        scheme = document.scheme()
-        scheme.save_widget_settings()
-        scheme.close_all_open_widgets()
-        scheme.signal_manager.stop()
-        scheme.deleteLater()
+        QApplication.sendEvent(old_scheme, QEvent(QEvent.Close))
+
+        old_scheme.deleteLater()
 
         config.save_config()
 
         if dbl_click:
             triggers |= SchemeEditWidget.DoubleClicked
 
-        left_click = settings.value("trigger-on-left-click",
+        right_click = settings.value("trigger-on-right-click",
                                     defaultValue=False,
                                     type=bool)
-        if left_click:
-            triggers |= SchemeEditWidget.Clicked
+        if right_click:
+            triggers |= SchemeEditWidget.RightClicked
 
         space_press = settings.value("trigger-on-space-key",
                                      defaultValue=True,
             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:

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

Orange/OrangeCanvas/application/settings.py

                         toolTip=self.tr("Open quick menu on a double click "
                                         "on an empty spot in the canvas"))
 
-        cb2 = QCheckBox(self.tr("On left click"),
-                        toolTip=self.tr("Open quick menu on a left click "
+        cb2 = QCheckBox(self.tr("On right click"),
+                        toolTip=self.tr("Open quick menu on a right click "
                                         "on an empty spot in the canvas"))
 
         cb3 = QCheckBox(self.tr("On space key press"),
                                         "is hovering over the canvas."))
 
         self.bind(cb1, "checked", "quickmenu/trigger-on-double-click")
-        self.bind(cb2, "checked", "quickmenu/trigger-on-left-click")
+        self.bind(cb2, "checked", "quickmenu/trigger-on-right-click")
         self.bind(cb3, "checked", "quickmenu/trigger-on-space-key")
         self.bind(cb4, "checked", "quickmenu/trigger-on-any-key")
 

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"),
 
      ("quickmenu/trigger-on-double-click", bool, True,
       "Show quick menu on double click."),
 
-     ("quickmenu/trigger-on-left-click", bool, False,
-      "Show quick menu on left click."),
+     ("quickmenu/trigger-on-right-click", bool, True,
+      "Show quick menu on right click."),
 
      ("quickmenu/trigger-on-space-key", bool, True,
       "Show quick menu on space key press."),

Orange/OrangeCanvas/document/interactions.py

             self.create_new(event.screenPos())
             self.end()
 
-    def create_new(self, pos):
+    def create_new(self, pos, search_text=""):
         """
         Create a new widget with a `QuickMenu` at `pos` (in screen
         coordinates).
         menu = self.document.quickMenu()
         menu.setFilterFunc(None)
 
-        action = menu.exec_(pos)
+        action = menu.exec_(pos, search_text)
         if action:
             item = action.property("item").toPyObject()
             desc = item.data(QtWidgetRegistry.WIDGET_DESC_ROLE).toPyObject()

Orange/OrangeCanvas/document/quickmenu.py

     QButtonGroup, QStackedWidget, QHBoxLayout, QVBoxLayout, QSizePolicy,
     QStandardItemModel, QSortFilterProxyModel, QStyleOptionToolButton,
     QStylePainter, QStyle, QApplication, QStyledItemDelegate,
-    QStyleOptionViewItemV4, QSizeGrip, QKeySequence
+    QStyleOptionViewItemV4, QSizeGrip
 )
 
 from PyQt4.QtCore import pyqtSignal as Signal
 from ..gui.framelesswindow import FramelessWindow
 from ..gui.lineedit import LineEdit
 from ..gui.tooltree import ToolTree, FlattenedTreeItemModel
+from ..gui.toolgrid import ToolButtonEventListener
+from ..gui.toolbox import create_tab_gradient
 from ..gui.utils import StyledWidget_paintEvent
 
 from ..registry.qt import QtWidgetRegistry
 log = logging.getLogger(__name__)
 
 
+class _MenuItemDelegate(QStyledItemDelegate):
+    def __init__(self, parent=None):
+        QStyledItemDelegate.__init__(self, parent)
+
+    def sizeHint(self, option, index):
+        option = QStyleOptionViewItemV4(option)
+        self.initStyleOption(option, index)
+        size = QStyledItemDelegate.sizeHint(self, option, index)
+
+        # TODO: get the default QMenu item height from the current style.
+        size.setHeight(max(size.height(), 25))
+        return size
+
+
 class MenuPage(ToolTree):
     """
     A menu page in a :class:`QuickMenu` widget, showing a list of actions.
         self.__title = title
         self.__icon = icon
 
+        self.view().setItemDelegate(_MenuItemDelegate(self.view()))
         # Make sure the initial model is wrapped in a ItemDisableFilter.
         self.setModel(self.model())
 
         proxyModel = self.view().model()
         return proxyModel.mapToSource(ToolTree.rootIndex(self))
 
+    def sizeHint(self):
+        view = self.view()
+        hint = view.sizeHint()
+        model = view.model()
+
+        # This will not work for nested items (tree).
+        count = model.rowCount(view.rootIndex())
+
+        width = view.sizeHintForColumn(0)
+
+        if count:
+            height = view.sizeHintForRow(0)
+            height = height * count
+        else:
+            height = hint.height()
+        return QSize(max(width, hint.width()), max(height, hint.height()))
+
 
 class ItemDisableFilter(QSortFilterProxyModel):
     """
         default_size = QSize(200, 400)
         widget_hints = [default_size]
         for i in range(self.count()):
-            w = self.widget(i)
-            if isinstance(w, ToolTree):
-                hint = self.__sizeHintForTreeView(w.view())
-            else:
-                hint = w.sizeHint()
+            hint = self.widget(i).sizeHint()
             widget_hints.append(hint)
+
         width = max([s.width() for s in widget_hints])
         # Take the median for the height
         height = numpy.median([s.height() for s in widget_hints])
                      designable=True)
 
     def paintEvent(self, event):
+        opt = QStyleOptionToolButton()
+        self.initStyleOption(opt)
+        opt.features |= QStyleOptionToolButton.HasMenu
         if self.__flat:
             # Use default widget background/border styling.
             StyledWidget_paintEvent(self, event)
 
-            opt = QStyleOptionToolButton()
-            self.initStyleOption(opt)
             p = QStylePainter(self)
             p.drawControl(QStyle.CE_ToolButtonLabel, opt)
         else:
-            QToolButton.paintEvent(self, event)
+            p = QStylePainter(self)
+            p.drawComplexControl(QStyle.CC_ToolButton, opt)
 
+    def sizeHint(self):
+        opt = QStyleOptionToolButton()
+        self.initStyleOption(opt)
+        opt.features |= QStyleOptionToolButton.HasMenu
+        style = self.style()
+
+        hint = style.sizeFromContents(QStyle.CT_ToolButton, opt,
+                                      opt.iconSize, self)
+        return hint
 
 _Tab = \
     namedtuple(
          "palette"])
 
 
-# TODO: ..application.canvastooldock.QuickCategoryToolbar is very similar,
-#       to TobBarWidget. Maybe common functionality could factored our.
-
 class TabBarWidget(QWidget):
     """
     A tab bar widget using tool buttons as tabs.
 
     def __init__(self, parent=None, **kwargs):
         QWidget.__init__(self, parent, **kwargs)
-        layout = QHBoxLayout()
+        layout = QVBoxLayout()
         layout.setContentsMargins(0, 0, 0, 0)
         layout.setSpacing(0)
         self.setLayout(layout)
 
-        self.setSizePolicy(QSizePolicy.Expanding,
-                           QSizePolicy.Fixed)
+        self.setSizePolicy(QSizePolicy.Fixed,
+                           QSizePolicy.Expanding)
         self.__tabs = []
+
         self.__currentIndex = -1
+        self.__changeOnHover = False
+
+        self.__iconSize = QSize(26, 26)
+
         self.__group = QButtonGroup(self, exclusive=True)
         self.__group.buttonPressed[QAbstractButton].connect(
             self.__onButtonPressed
         )
 
+        self.__hoverListener = ToolButtonEventListener(self)
+
+    def setChangeOnHover(self, changeOnHover):
+        """
+        If set to ``True`` the tab widget will change the current index when
+        the mouse hovers over a tab button.
+
+        """
+        if self.__changeOnHover != changeOnHover:
+            self.__changeOnHover = changeOnHover
+
+            if changeOnHover:
+                self.__hoverListener.buttonEnter.connect(
+                    self.__onButtonEnter
+                )
+            else:
+                self.__hoverListener.buttonEnter.disconnect(
+                    self.__onButtonEnter
+                )
+
+    def changeOnHover(self):
+        """
+        Does the current tab index follow the mouse cursor.
+        """
+        return self.__changeOnHover
+
     def count(self):
         """
         Return the number of tabs in the widget.
         button = TabButton(self, objectName="tab-button")
         button.setSizePolicy(QSizePolicy.Expanding,
                              QSizePolicy.Expanding)
+        button.setIconSize(self.__iconSize)
 
         self.__group.addButton(button)
+
+        button.installEventFilter(self.__hoverListener)
+
         tab = _Tab(text, icon, toolTip, button, None, None)
         self.layout().insertWidget(index, button)
 
             self.layout().takeItem(index)
             tab = self.__tabs.pop(index)
             self.__group.removeButton(tab.button)
+
+            tab.button.removeEventFilter(self.__hoverListener)
+
             tab.button.deleteLater()
 
             if self.currentIndex() == index:
         """
         return self.__tabs[index].button
 
+    def setIconSize(self, size):
+        if self.__iconSize != size:
+            self.__iconSize = size
+            for tab in self.__tabs:
+                tab.button.setIconSize(self.__iconSize)
+
     def __updateTab(self, index):
         """
         Update the tab button.
                 self.setCurrentIndex(i)
                 break
 
+    def __onButtonEnter(self, button):
+        if self.__changeOnHover:
+            button.click()
+
 
 class PagedMenu(QWidget):
     """
         self.__pages = []
         self.__currentIndex = -1
 
-        layout = QVBoxLayout()
+        layout = QHBoxLayout()
         layout.setContentsMargins(0, 0, 0, 0)
         layout.setSpacing(0)
 
         self.__tab = TabBarWidget(self)
-        self.__tab.setFixedHeight(25)
         self.__tab.currentChanged.connect(self.setCurrentIndex)
+        self.__tab.setChangeOnHover(True)
 
         self.__stack = MenuStackWidget(self)
 
-        layout.addWidget(self.__tab)
+        layout.addWidget(self.__tab, alignment=Qt.AlignTop)
         layout.addWidget(self.__stack)
 
         self.setLayout(layout)
         self.setLayout(QVBoxLayout(self))
         self.layout().setContentsMargins(6, 6, 6, 6)
 
+        self.__search = SearchWidget(self, objectName="search-line")
+
+        self.__search.setPlaceholderText(
+            self.tr("Search for widget or select from the list.")
+        )
+
+        self.layout().addWidget(self.__search)
+
         self.__frame = QFrame(self, objectName="menu-frame")
         layout = QVBoxLayout()
-        layout.setContentsMargins(1, 1, 1, 1)
+        layout.setContentsMargins(0, 0, 0, 0)
         layout.setSpacing(2)
         self.__frame.setLayout(layout)
 
 
         self.__frame.layout().addWidget(self.__pages)
 
-        self.__search = SearchWidget(self, objectName="search-line")
-
-        self.__search.setPlaceholderText(
-            self.tr("Search for widget or select from the list.")
-        )
-
-        self.layout().addWidget(self.__search)
         self.setSizePolicy(QSizePolicy.Fixed,
                            QSizePolicy.Expanding)
 
         if sys.platform == "darwin":
             view = self.__suggestPage.view()
             view.verticalScrollBar().setAttribute(Qt.WA_MacMiniSize, True)
-            # Don't show the focus frame because it expands into the tab
-            # bar at the top.
+            # Don't show the focus frame because it expands into the tab bar.
             view.setAttribute(Qt.WA_MacShowFocusRect, False)
 
-        self.addPage(self.tr("Quick Search"), self.__suggestPage)
+        i = self.addPage(self.tr("Quick Search"), self.__suggestPage)
+        button = self.__pages.tabButton(i)
+        button.setObjectName("search-tab-button")
+        button.setStyleSheet(
+            "TabButton {\n"
+            "    qproperty-flat_: false;\n"
+            "    border: none;"
+            "}\n")
 
         self.__search.textEdited.connect(self.__on_textEdited)
 
 
         """
         page = MenuPage(self)
-        view = page.view()
-        delegate = WidgetItemDelegate(view)
-        view.setItemDelegate(delegate)
 
         page.setModel(index.model())
         page.setRootIndex(index)
 
             if brush.isValid():
                 brush = brush.toPyObject()
+                base_color = brush.color()
                 button = self.__pages.tabButton(i)
-                palette = button.palette()
                 button.setStyleSheet(
                     "TabButton {\n"
                     "    qproperty-flat_: false;\n"
-                    "    background-color: %s;\n"
+                    "    background: %s;\n"
                     "    border: none;\n"
+                    "    border-bottom: 1px solid palette(dark);\n"
                     "}\n"
                     "TabButton:checked {\n"
-                    "    border: 1px solid %s;\n"
-                    "}" % (brush.color().name(),
-                           palette.color(palette.Mid).name())
+                    "    background: %s\n"
+                    "}" % (create_css_gradient(base_color),
+                           create_css_gradient(base_color.darker(110)))
                 )
 
         self.__model = model
             for i in range(0, self.__pages.count()):
                 self.__pages.page(i).setFilterFunc(func)
 
-    def popup(self, pos=None):
+    def popup(self, pos=None, searchText=""):
         """
-        Popup the menu at `pos` (in screen coordinates).
+        Popup the menu at `pos` (in screen coordinates). 'Search' text field
+        is initialized with `searchText` if provided.
         """
         if pos is None:
             pos = QPoint()
 
-        self.__search.setText("")
-        self.__suggestPage.setFilterFixedString("")
+        self.__search.setText(searchText)
+        self.__suggestPage.setFilterFixedString(searchText)
 
         self.ensurePolished()
 
 
         self.show()
 
-    def exec_(self, pos=None):
+        if searchText:
+            self.setFocusProxy(self.__search)
+        else:
+            self.setFocusProxy(None)
+
+    def exec_(self, pos=None, searchText=""):
         """
         Execute the menu at position `pos` (in global screen coordinates).
         Return the triggered :class:`QAction` or `None` if no action was
-        triggered.
+        triggered. 'Search' text field is initialized with `searchText` if
+        provided.
 
         """
-        self.popup(pos)
+        self.popup(pos, searchText)
         self.setFocus(Qt.PopupFocusReason)
 
         self.__triggeredAction = None
         return FramelessWindow.eventFilter(self, obj, event)
 
 
-class WidgetItemDelegate(QStyledItemDelegate):
-    def __init__(self, parent=None):
-        QStyledItemDelegate.__init__(self, parent)
-
-    def sizeHint(self, option, index):
-        option = QStyleOptionViewItemV4(option)
-        self.initStyleOption(option, index)
-        size = QStyledItemDelegate.sizeHint(self, option, index)
-        size.setHeight(max(size.height(), 25))
-        return size
-
-
 class ItemViewKeyNavigator(QObject):
     """
     A event filter class listening to key press events and responding
             y = window_size.height() - size.height()
 
         self.move(x, y)
+
+
+def create_css_gradient(base_color):
+    """
+    Create a Qt css linear gradient fragment based on the `base_color`.
+    """
+    grad = create_tab_gradient(base_color)
+    stops = grad.stops()
+    stops = "\n".join("    stop: {0:f} {1}".format(stop, color.name())
+                      for stop, color in stops)
+    return ("qlineargradient(\n"
+            "    x1: 0, y1: 0, x2: 0, y2: 1,\n"
+            "{0})").format(stops)

Orange/OrangeCanvas/document/schemeedit.py

 import sys
 import logging
 import itertools
+import unicodedata
 
 from operator import attrgetter
 from contextlib import nested
 
     # Quick Menu triggers
     (NoTriggers,
-     Clicked,
+     RightClicked,
      DoubleClicked,
      SpaceKey,
      AnyKey) = [0, 1, 2, 4, 8]
         layout.setSpacing(0)
 
         scene = CanvasScene()
-        scene.set_channel_names_visible(self.__channelNamesVisible)
-        scene.set_node_animation_enabled(self.__nodeAnimationEnabled)
-        scene.setFont(self.font())
+        self.__setupScene(scene)
 
         view = CanvasView(scene)
         view.setFrameStyle(CanvasView.NoFrame)
         self.__view = view
         self.__scene = scene
 
+        layout.addWidget(view)
+        self.setLayout(layout)
+
+    def __setupScene(self, scene):
+        """
+        Set up a :class:`CanvasScene` instance for use by the editor.
+
+        .. note:: If an existing scene is in use it must be teared down using
+            __teardownScene
+
+        """
+        scene.set_channel_names_visible(self.__channelNamesVisible)
+        scene.set_node_animation_enabled(
+            self.__nodeAnimationEnabled
+        )
+
+        scene.setFont(self.font())
+
+        scene.installEventFilter(self)
+
+        scene.set_registry(self.__registry)
+
+        # Focus listener
         self.__focusListener = GraphicsSceneFocusEventListener()
-        self.__focusListener.itemFocusedIn.connect(self.__onItemFocusedIn)
-        self.__focusListener.itemFocusedOut.connect(self.__onItemFocusedOut)
-        self.__scene.addItem(self.__focusListener)
+        self.__focusListener.itemFocusedIn.connect(
+            self.__onItemFocusedIn
+        )
+        self.__focusListener.itemFocusedOut.connect(
+            self.__onItemFocusedOut
+        )
+        scene.addItem(self.__focusListener)
 
-        self.__scene.selectionChanged.connect(
+        scene.selectionChanged.connect(
             self.__onSelectionChanged
         )
 
-        layout.addWidget(view)
-        self.setLayout(layout)
+        scene.node_item_activated.connect(
+            self.__onNodeActivate
+        )
+
+        scene.annotation_added.connect(
+            self.__onAnnotationAdded
+        )
+
+        scene.annotation_removed.connect(
+            self.__onAnnotationRemoved
+        )
+
+        self.__annotationGeomChanged = QSignalMapper(self)
+
+    def __teardownScene(self, scene):
+        """
+        Tear down an instance of :class:`CanvasScene` that was used by the
+        editor.
+
+        """
+        # Clear the current item selection in the scene so edit action
+        # states are updated accordingly.
+        scene.clearSelection()
+
+        # Clear focus from any item.
+        scene.setFocusItem(None)
+
+        # Clear the annotation mapper
+        self.__annotationGeomChanged.deleteLater()
+        self.__annotationGeomChanged = None
+
+        self.__focusListener.itemFocusedIn.disconnect(
+            self.__onItemFocusedIn
+        )
+        self.__focusListener.itemFocusedOut.disconnect(
+            self.__onItemFocusedOut
+        )
+
+        scene.selectionChanged.disconnect(
+            self.__onSelectionChanged
+        )
+
+        scene.removeEventFilter(self)
+
+        # Clear all items from the scene
+        scene.blockSignals(True)
+        scene.clear_scene()
 
     def toolbarActions(self):
         """
         Flags can be a bitwise `or` of:
 
             - `SchemeEditWidget.NoTrigeres`
-            - `SchemeEditWidget.Clicked`
+            - `SchemeEditWidget.RightClicked`
             - `SchemeEditWidget.DoubleClicked`
             - `SchemeEditWidget.SpaceKey`
             - `SchemeEditWidget.AnyKey`
             else:
                 self.__cleanSettings = []
 
-            # Clear the current item selection in the scene so edit action
-            # states are updated accordingly.
-            self.__scene.clearSelection()
-
-            self.__annotationGeomChanged.deleteLater()
-            self.__annotationGeomChanged = QSignalMapper(self)
+            self.__teardownScene(self.__scene)
+            self.__scene.deleteLater()
 
             self.__undoStack.clear()
 
-            self.__focusListener.itemFocusedIn.disconnect(
-                self.__onItemFocusedIn
-            )
-            self.__focusListener.itemFocusedOut.disconnect(
-                self.__onItemFocusedOut
-            )
+            self.__scene = CanvasScene()
+            self.__setupScene(self.__scene)
 
-            self.__scene.selectionChanged.disconnect(
-                self.__onSelectionChanged
-            )
-
-            self.__scene.removeEventFilter(self)
-
-            # Clear all items from the scene
-            self.__scene.blockSignals(True)
-            self.__scene.clear_scene()
-
-            self.__scene.deleteLater()
-
-            self.__scene = CanvasScene()
             self.__view.setScene(self.__scene)
-            self.__scene.set_channel_names_visible(self.__channelNamesVisible)
-            self.__scene.set_node_animation_enabled(
-                self.__nodeAnimationEnabled
-            )
-
-            self.__scene.setFont(self.font())
-
-            self.__scene.installEventFilter(self)
-
-            self.__scene.set_registry(self.__registry)
-
-            # Focus listener
-            self.__focusListener = GraphicsSceneFocusEventListener()
-            self.__focusListener.itemFocusedIn.connect(
-                self.__onItemFocusedIn
-            )
-            self.__focusListener.itemFocusedOut.connect(
-                self.__onItemFocusedOut
-            )
-            self.__scene.addItem(self.__focusListener)
-
-            self.__scene.selectionChanged.connect(
-                self.__onSelectionChanged
-            )
-
-            self.__scene.node_item_activated.connect(
-                self.__onNodeActivate
-            )
-
-            self.__scene.annotation_added.connect(
-                self.__onAnnotationAdded
-            )
-
-            self.__scene.annotation_removed.connect(
-                self.__onAnnotationRemoved
-            )
 
             self.__scene.set_scheme(scheme)
 
             return handler.mousePressEvent(event)
 
         any_item = scene.item_at(pos)
+        if not any_item:
+            self.__emptyClickButtons |= event.button()
+
         if not any_item and event.button() == Qt.LeftButton:
-            self.__emptyClickButtons |= Qt.LeftButton
             # Create a RectangleSelectionAction but do not set in on the scene
             # just yet (instead wait for the mouse move event).
             handler = interactions.RectangleSelectionAction(self)
             # on the scene
             handler = self.__possibleSelectionHandler
             self._setUserInteractionHandler(handler)
+            self.__possibleSelectionHandler = None
             return handler.mouseMoveEvent(event)
 
         return False
 
     def sceneMouseReleaseEvent(self, event):
+        scene = self.__scene
+        if scene.user_interaction_handler:
+            return False
+
         if event.button() == Qt.LeftButton and self.__possibleMouseItemsMove:
             self.__possibleMouseItemsMove = False
             self.__scene.node_item_position_changed.disconnect(
 
                 self.__itemsMoving.clear()
                 return True
-
-        if self.__emptyClickButtons & Qt.LeftButton and \
-                event.button() & Qt.LeftButton:
-            self.__emptyClickButtons &= ~Qt.LeftButton
-
-            if self.__quickMenuTriggers & SchemeEditWidget.Clicked and \
-                    mouse_drag_distance(event, Qt.LeftButton) < 1:
-                action = interactions.NewNodeAction(self)
-
-                with nested(disabled(self.__undoAction),
-                            disabled(self.__redoAction)):
-                    action.create_new(event.screenPos())
-
-                event.accept()
-                return True
+        elif event.button() == Qt.LeftButton:
+            self.__possibleSelectionHandler = None
 
         return False
 
             return False
 
         handler = None
+        searchText = ""
         if (event.key() == Qt.Key_Space and \
                 self.__quickMenuTriggers & SchemeEditWidget.SpaceKey):
             handler = interactions.NewNodeAction(self)
 
         elif len(event.text()) and \
-                self.__quickMenuTriggers & SchemeEditWidget.AnyKey:
+                self.__quickMenuTriggers & SchemeEditWidget.AnyKey and \
+                is_printable(unicode(event.text())[0]):
             handler = interactions.NewNodeAction(self)
+            searchText = unicode(event.text())
+
             # TODO: set the search text to event.text() and set focus on the
             # search line
 
             with nested(disabled(self.__removeSelectedAction),
                         disabled(self.__undoAction),
                         disabled(self.__redoAction)):
-                handler.create_new(QCursor.pos())
+                handler.create_new(QCursor.pos(), searchText)
 
             event.accept()
             return True
             self.__linkMenu.popup(globalPos)
             return
 
+        item = self.scene().item_at(scenePos)
+        if not item and \
+                self.__quickMenuTriggers & SchemeEditWidget.RightClicked:
+            action = interactions.NewNodeAction(self)
+
+            with nested(disabled(self.__undoAction),
+                        disabled(self.__redoAction)):
+                action.create_new(globalPos)
+            return
+
     def __onRenameAction(self):
         """
         Rename was requested for the selected widget.
     """
     for obj in objects:
         obj.setEnabled(enable)
+
+
+# All control character categories.
+_control = set(["Cc", "Cf", "Cs", "Co", "Cn"])
+
+
+def is_printable(unichar):
+    """
+    Return True if the unicode character `unichar` is a printable character.
+    """
+    return unicodedata.category(unichar) not in _control

Orange/OrangeCanvas/gui/tooltree.py

         view.setItemDelegate(ToolTreeItemDelegate(self))
 
         view.activated.connect(self.__onActivated)
-        view.pressed.connect(self.__onPressed)
+        view.clicked.connect(self.__onActivated)
         view.entered.connect(self.__onEntered)
 
         view.installEventFilter(self)
                 action.trigger()
                 self.triggered.emit(action)
 
-    def __onPressed(self, index):
-        self.__onActivated(index)
-
     def __onEntered(self, index):
         if index.isValid():
             action = self.__actionForIndex(index)

Orange/OrangeCanvas/icons/arrow-right.svg

Added
New image
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Generator: Adobe Illustrator 16.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->
+<svg xmlns="http://www.w3.org/2000/svg" enable-background="new 0 0 48 48" xml:space="preserve" viewBox="0 0 48 48" version="1.1" y="0px" x="0px">
+<polygon points="24.007,37.979,42,10.021,6,10.021" transform="matrix(0,-1,1,0,0,48)" fill="#3a3a3a"/>
+</svg>

Orange/OrangeCanvas/scheme/widgetsscheme.py

         del self.node_for_widget[widget]
 
         # Save settings to user global settings.
-        widget.saveSettings()
+        if not widget._settingsFromSchema:
+            widget.saveSettings()
 
         # Notify the widget it will be deleted.
         widget.onDeleteWidget()
         help_shortcut.activated.connect(self.__on_help_request)
         return widget
 
-    def close_all_open_widgets(self):
-        for widget in self.widget_for_node.values():
-            widget.close()
-
     def widget_settings(self):
         """Return a list of dictionaries with widget settings.
         """
         return [self.widget_for_node[node].getSettings(alsoContexts=False)
                 for node in self.nodes]
 
-    def save_widget_settings(self):
-        """Save all widget settings to their global settings file.
-        """
-        for node in self.nodes:
-            widget = self.widget_for_node[node]
-            widget.saveSettings()
-
     def sync_node_properties(self):
         """Sync the widget settings/properties with the SchemeNode.properties.
         Return True if there were any changes in the properties (i.e. if the
         self.sync_node_properties()
         Scheme.save_to(self, stream, pretty, pickle_fallback)
 
+    def event(self, event):
+        """
+        Reimplemented from `QObject.event`.
+
+        Responds to QEvent.Close event by stopping signal processing and
+        closing all widgets.
+
+        """
+        if event.type() == QEvent.Close:
+            self.signal_manager.stop()
+
+            # Notify the widget instances.
+            for widget in self.widget_for_node.values():
+                if not widget._settingsFromSchema:
+                    # First save global settings if necessary.
+                    widget.saveSettings()
+
+                widget.close()
+                widget.onDeleteWidget()
+
+            event.accept()
+            return True
+        else:
+            return Scheme.event(self, event)
+
     def __on_help_request(self):
         """
         Help shortcut was pressed. We send a `QWhatsThisClickedEvent` and

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 */
 }
 
 QuickMenu ToolTree QTreeView::item {
-    height: 25px;
-    border-top: 1px solid #e9eff2;
-    border-bottom: 1px solid #e9eff2;
+	height: 25px;
+	border-bottom: 1px solid #e9eff2;
 }
 
 QuickMenu QTreeView::item:hover {
-    background-color: lightblue;
-    color: white;
+	background: qlineargradient(
+		x1: 0, y1: 0, x2: 0, y2: 1,
+		stop: 0 #688EF6,
+		stop: 0.5 #4047f4,
+		stop: 1.0 #2D68F3
+	);
+	color: white;
 }
 
 QuickMenu QTreeView::item:selected {
-    background-color: blue;
-    color: white;
+	background-color: blue;
+	color: white;
 }
 
-/* Quick Menu search line edit 
+QuickMenu TabBarWidget QToolButton {
+	width: 33px;
+	height: 25px;
+	border-bottom: 1px solid palette(dark);
+	padding-right: 5px;
+}
+
+QuickMenu TabBarWidget QToolButton#search-tab-button {
+	background-color: #9CACB4;
+}
+
+QuickMenu TabBarWidget QToolButton:menu-indicator {
+	image: url(canvas_icons:/arrow-right.svg);
+	subcontrol-position: center right;
+	height: 8px;
+	width: 8px;
+}
+
+/* Quick Menu search line edit
  */
 
 QuickMenu QLineEdit {