From 8989b6be65b3abbe0029b5eb7b9e2d1570bebf04 Mon Sep 17 00:00:00 2001 From: Rafael Irgolic Date: Wed, 1 Aug 2018 11:48:22 +0200 Subject: [PATCH 1/5] Right-click on a link to insert a widget --- Orange/canvas/document/commands.py | 34 ++++++++++++++ Orange/canvas/document/schemeedit.py | 67 ++++++++++++++++++++++++++-- 2 files changed, 98 insertions(+), 3 deletions(-) diff --git a/Orange/canvas/document/commands.py b/Orange/canvas/document/commands.py index 113cb56f3a8..53138554944 100644 --- a/Orange/canvas/document/commands.py +++ b/Orange/canvas/document/commands.py @@ -6,6 +6,8 @@ from AnyQt.QtWidgets import QUndoCommand +from Orange.canvas.scheme import SchemeLink + class AddNodeCommand(QUndoCommand): def __init__(self, scheme, node, parent=None): @@ -69,6 +71,38 @@ def undo(self): self.scheme.add_link(self.link) +class InsertNodeCommand(QUndoCommand): + def __init__(self, scheme, link, new_node, parent=None): + QUndoCommand.__init__(self, "Remove link", parent) + self.scheme = scheme + self.original_link = link + self.inserted_widget = new_node + + possible_links = (self.scheme.propose_links(link.source_node, new_node), + self.scheme.propose_links(new_node, link.sink_node)) + + if not possible_links[0] or not possible_links[1]: + raise ValueError("Cannot insert widget: links not possible") + + self.new_links = ( + SchemeLink(link.source_node, link.source_channel, + new_node, possible_links[0][0][1]), # first link, first entry, output (1) + SchemeLink(new_node, possible_links[1][0][0], # second link, first entry, input (0) + link.sink_node, link.sink_channel)) + + def redo(self): + self.scheme.add_node(self.inserted_widget) + self.scheme.remove_link(self.original_link) + self.scheme.add_link(self.new_links[0]) + self.scheme.add_link(self.new_links[1]) + + def undo(self): + self.scheme.remove_link(self.new_links[0]) + self.scheme.remove_link(self.new_links[1]) + self.scheme.add_link(self.original_link) + self.scheme.remove_node(self.inserted_widget) + + class AddAnnotationCommand(QUndoCommand): def __init__(self, scheme, annotation, parent=None): QUndoCommand.__init__(self, "Add annotation", parent) diff --git a/Orange/canvas/document/schemeedit.py b/Orange/canvas/document/schemeedit.py index 6190bfb1254..88359df2ec3 100644 --- a/Orange/canvas/document/schemeedit.py +++ b/Orange/canvas/document/schemeedit.py @@ -29,13 +29,14 @@ ) from AnyQt.QtCore import ( - Qt, QObject, QEvent, QSignalMapper, QRectF, QCoreApplication -) + Qt, QObject, QEvent, QSignalMapper, QRectF, QCoreApplication, + QPoint) from AnyQt.QtCore import pyqtProperty as Property, pyqtSignal as Signal +from Orange.canvas.registry import WidgetDescription from .suggestions import Suggestions -from ..registry.qt import whats_this_helper +from ..registry.qt import whats_this_helper, QtWidgetRegistry from ..gui.quickhelp import QuickHelpTipEvent from ..gui.utils import message_information, disabled from ..scheme import ( @@ -171,6 +172,8 @@ def __init__(self, parent=None, ): self.__linkMenu = QMenu(self.tr("Link"), self) self.__linkMenu.addAction(self.__linkEnableAction) self.__linkMenu.addSeparator() + self.__linkMenu.addAction(self.__nodeInsertAction) + self.__linkMenu.addSeparator() self.__linkMenu.addAction(self.__linkRemoveAction) self.__linkMenu.addAction(self.__linkResetAction) @@ -328,6 +331,13 @@ def color_icon(color): toolTip=self.tr("Remove link."), ) + self.__nodeInsertAction = \ + QAction(self.tr("Insert Widget"), self, + objectName="node-insert-action", + triggered=self.__nodeInsert, + toolTip=self.tr("Insert widget."), + ) + self.__linkResetAction = \ QAction(self.tr("Reset Signals"), self, objectName="link-reset-action", @@ -346,6 +356,7 @@ def color_icon(color): self.__newArrowAnnotationAction, self.__linkEnableAction, self.__linkRemoveAction, + self.__nodeInsertAction, self.__linkResetAction, self.__duplicateSelectedAction]) @@ -867,6 +878,10 @@ def removeLink(self, link): command = commands.RemoveLinkCommand(self.__scheme, link) self.__undoStack.push(command) + def insertNode(self, link, new_node): + command = commands.InsertNodeCommand(self.__scheme, link, new_node) + self.__undoStack.push(command) + def onNewLink(self, func): """ Runs function when new link is added to current scheme. @@ -1594,6 +1609,52 @@ def __linkReset(self): ) action.edit_links() + def __nodeInsert(self): + """ + Node insert was requested from the context menu. + """ + if not self.__contextMenuTarget: + return + + original_link = self.__contextMenuTarget + source_node = original_link.source_node + sink_node = original_link.sink_node + + def is_compatible(source, sink): + return any(scheme.compatible_channels(output, input) \ + for output in source.outputs \ + for input in sink.inputs) + + def filterFunc(index): + new_node_desc = index.data(QtWidgetRegistry.WIDGET_DESC_ROLE) + if isinstance(new_node_desc, WidgetDescription): + return is_compatible(source_node.description, new_node_desc) and\ + is_compatible(new_node_desc, sink_node.description) + else: + return False + + x = (source_node.position[0] + sink_node.position[0]) / 2 + y = (source_node.position[1] + sink_node.position[1]) / 2 + + menu = self.quickMenu() + menu.setFilterFunc(filterFunc) + menu.setSortingFunc(None) + + view = self.view() + try: + action = menu.exec_(view.mapToGlobal(view.mapFromScene(QPoint(x, y)))) + finally: + menu.setFilterFunc(None) + + if action: + item = action.property("item") + desc = item.data(QtWidgetRegistry.WIDGET_DESC_ROLE) + new_node = self.newNodeHelper(desc, position=(x, y)) + else: + return + + self.insertNode(original_link, new_node) + def __duplicateSelected(self): """ Duplicate currently selected nodes. From 57380e0eb1b31bd8f48f527b330b8aae72852727 Mon Sep 17 00:00:00 2001 From: Rafael Irgolic Date: Wed, 1 Aug 2018 15:15:46 +0200 Subject: [PATCH 2/5] Drag onto link to insert widget --- Orange/canvas/canvas/items/linkitem.py | 5 ++++ Orange/canvas/document/schemeedit.py | 36 ++++++++++++++++++++------ 2 files changed, 33 insertions(+), 8 deletions(-) diff --git a/Orange/canvas/canvas/items/linkitem.py b/Orange/canvas/canvas/items/linkitem.py index 79b1cf94ba1..7e35c534c53 100644 --- a/Orange/canvas/canvas/items/linkitem.py +++ b/Orange/canvas/canvas/items/linkitem.py @@ -28,6 +28,8 @@ class LinkCurveItem(QGraphicsPathItem): """ def __init__(self, parent): super().__init__(parent) + self.__parent = parent + self.setAcceptedMouseButtons(Qt.NoButton) self.setAcceptHoverEvents(True) @@ -91,6 +93,9 @@ def setPath(self, path): self.__shape = None super().setPath(path) + def parent(self): + return self.__parent + def __update(self): shadow_enabled = self.__hover if self.shadow.isEnabled() != shadow_enabled: diff --git a/Orange/canvas/document/schemeedit.py b/Orange/canvas/document/schemeedit.py index 88359df2ec3..d0a7b0f765d 100644 --- a/Orange/canvas/document/schemeedit.py +++ b/Orange/canvas/document/schemeedit.py @@ -879,6 +879,9 @@ def removeLink(self, link): self.__undoStack.push(command) def insertNode(self, link, new_node): + """ + Insert a node in-between two linked nodes. + """ command = commands.InsertNodeCommand(self.__scheme, link, new_node) self.__undoStack.push(command) @@ -1044,6 +1047,17 @@ def changeEvent(self, event): QWidget.changeEvent(self, event) + def tryInsertNode(self, link, new_node_desc, pos): + source_node = link.source_node + sink_node = link.sink_node + + if nodes_are_compatible(source_node.description, new_node_desc) and \ + nodes_are_compatible(new_node_desc, sink_node.description): + new_node = self.newNodeHelper(new_node_desc, position=(pos.x(), pos.y())) + self.insertNode(link, new_node) + else: + self.createNewNode(new_node_desc, position=(pos.x(), pos.y())) + def eventFilter(self, obj, event): # Filter the scene's drag/drop events. if obj is self.scene(): @@ -1069,7 +1083,12 @@ def eventFilter(self, obj, event): log.error("Unknown qualified name '%s'", qname) else: pos = event.scenePos() - self.createNewNode(desc, position=(pos.x(), pos.y())) + item = self.__scene.item_at(event.scenePos()) + if item and isinstance(item, items.LinkCurveItem): + link = self.__scene.link_for_item(item.parent()) + self.tryInsertNode(link, desc, pos) + else: + self.createNewNode(desc, position=(pos.x(), pos.y())) return True elif etype == QEvent.GraphicsSceneMousePress: @@ -1620,16 +1639,11 @@ def __nodeInsert(self): source_node = original_link.source_node sink_node = original_link.sink_node - def is_compatible(source, sink): - return any(scheme.compatible_channels(output, input) \ - for output in source.outputs \ - for input in sink.inputs) - def filterFunc(index): new_node_desc = index.data(QtWidgetRegistry.WIDGET_DESC_ROLE) if isinstance(new_node_desc, WidgetDescription): - return is_compatible(source_node.description, new_node_desc) and\ - is_compatible(new_node_desc, sink_node.description) + return nodes_are_compatible(source_node.description, new_node_desc) and \ + nodes_are_compatible(new_node_desc, sink_node.description) else: return False @@ -1997,6 +2011,12 @@ def node_properties(scheme): return [dict(node.properties) for node in scheme.nodes] +def nodes_are_compatible(source, sink): + return any(scheme.compatible_channels(output, input) \ + for output in source.outputs \ + for input in sink.inputs) + + def uniquify(item, names, pattern="{item}-{_}", start=0): candidates = (pattern.format(item=item, _=i) for i in itertools.count(start)) From 28fe8211e975736ea53078ed464ba66642ed766c Mon Sep 17 00:00:00 2001 From: Rafael Irgolic Date: Thu, 2 Aug 2018 11:35:58 +0200 Subject: [PATCH 3/5] Addressing review -- Code quality --- Orange/canvas/canvas/items/linkitem.py | 5 ---- Orange/canvas/document/commands.py | 21 +++----------- Orange/canvas/document/schemeedit.py | 39 +++++++++++++++++++++----- 3 files changed, 36 insertions(+), 29 deletions(-) diff --git a/Orange/canvas/canvas/items/linkitem.py b/Orange/canvas/canvas/items/linkitem.py index 7e35c534c53..79b1cf94ba1 100644 --- a/Orange/canvas/canvas/items/linkitem.py +++ b/Orange/canvas/canvas/items/linkitem.py @@ -28,8 +28,6 @@ class LinkCurveItem(QGraphicsPathItem): """ def __init__(self, parent): super().__init__(parent) - self.__parent = parent - self.setAcceptedMouseButtons(Qt.NoButton) self.setAcceptHoverEvents(True) @@ -93,9 +91,6 @@ def setPath(self, path): self.__shape = None super().setPath(path) - def parent(self): - return self.__parent - def __update(self): shadow_enabled = self.__hover if self.shadow.isEnabled() != shadow_enabled: diff --git a/Orange/canvas/document/commands.py b/Orange/canvas/document/commands.py index 53138554944..a686ddb4484 100644 --- a/Orange/canvas/document/commands.py +++ b/Orange/canvas/document/commands.py @@ -6,8 +6,6 @@ from AnyQt.QtWidgets import QUndoCommand -from Orange.canvas.scheme import SchemeLink - class AddNodeCommand(QUndoCommand): def __init__(self, scheme, node, parent=None): @@ -72,23 +70,12 @@ def undo(self): class InsertNodeCommand(QUndoCommand): - def __init__(self, scheme, link, new_node, parent=None): - QUndoCommand.__init__(self, "Remove link", parent) + def __init__(self, scheme, new_node, old_link, new_links, parent=None): + QUndoCommand.__init__(self, "Insert widget into link", parent) self.scheme = scheme - self.original_link = link self.inserted_widget = new_node - - possible_links = (self.scheme.propose_links(link.source_node, new_node), - self.scheme.propose_links(new_node, link.sink_node)) - - if not possible_links[0] or not possible_links[1]: - raise ValueError("Cannot insert widget: links not possible") - - self.new_links = ( - SchemeLink(link.source_node, link.source_channel, - new_node, possible_links[0][0][1]), # first link, first entry, output (1) - SchemeLink(new_node, possible_links[1][0][0], # second link, first entry, input (0) - link.sink_node, link.sink_channel)) + self.original_link = old_link + self.new_links = new_links def redo(self): self.scheme.add_node(self.inserted_widget) diff --git a/Orange/canvas/document/schemeedit.py b/Orange/canvas/document/schemeedit.py index d0a7b0f765d..449858b376a 100644 --- a/Orange/canvas/document/schemeedit.py +++ b/Orange/canvas/document/schemeedit.py @@ -878,11 +878,11 @@ def removeLink(self, link): command = commands.RemoveLinkCommand(self.__scheme, link) self.__undoStack.push(command) - def insertNode(self, link, new_node): + def insertNode(self, new_node, old_link, new_links): """ Insert a node in-between two linked nodes. """ - command = commands.InsertNodeCommand(self.__scheme, link, new_node) + command = commands.InsertNodeCommand(self.__scheme, new_node, old_link, new_links) self.__undoStack.push(command) def onNewLink(self, func): @@ -1054,7 +1054,20 @@ def tryInsertNode(self, link, new_node_desc, pos): if nodes_are_compatible(source_node.description, new_node_desc) and \ nodes_are_compatible(new_node_desc, sink_node.description): new_node = self.newNodeHelper(new_node_desc, position=(pos.x(), pos.y())) - self.insertNode(link, new_node) + + possible_links = (self.scheme().propose_links(source_node, new_node), + self.scheme().propose_links(new_node, sink_node)) + + if not possible_links[0] or not possible_links[1]: + raise ValueError("Cannot insert widget: links not possible") + + new_links = ( + SchemeLink(source_node, link.source_channel, + new_node, possible_links[0][0][1]), # first link, first entry, output + SchemeLink(new_node, possible_links[1][0][0], # second link, first entry, input + sink_node, link.sink_channel)) + + self.insertNode(new_node, link, new_links) else: self.createNewNode(new_node_desc, position=(pos.x(), pos.y())) @@ -1083,9 +1096,9 @@ def eventFilter(self, obj, event): log.error("Unknown qualified name '%s'", qname) else: pos = event.scenePos() - item = self.__scene.item_at(event.scenePos()) - if item and isinstance(item, items.LinkCurveItem): - link = self.__scene.link_for_item(item.parent()) + item = self.__scene.item_at(event.scenePos(), items.LinkItem) + if item: + link = self.__scene.link_for_item(item) self.tryInsertNode(link, desc, pos) else: self.createNewNode(desc, position=(pos.x(), pos.y())) @@ -1667,7 +1680,19 @@ def filterFunc(index): else: return - self.insertNode(original_link, new_node) + possible_links = (self.scheme().propose_links(source_node, new_node), + self.scheme().propose_links(new_node, sink_node)) + + if not possible_links[0] or not possible_links[1]: + raise ValueError("Cannot insert widget: links not possible") + + new_links = ( + SchemeLink(source_node, original_link.source_channel, + new_node, possible_links[0][0][1]), # first link, first entry, output + SchemeLink(new_node, possible_links[1][0][0], # second link, first entry, input + sink_node, original_link.sink_channel)) + + self.insertNode(new_node, original_link, new_links) def __duplicateSelected(self): """ From 976f9ea013348f6885114f0dae092e03dd0d1022 Mon Sep 17 00:00:00 2001 From: Rafael Irgolic Date: Thu, 2 Aug 2018 12:52:38 +0200 Subject: [PATCH 4/5] schemeedit: Inserted widgets are connected with previously existing channels --- Orange/canvas/document/schemeedit.py | 84 ++++++++++++---------------- 1 file changed, 35 insertions(+), 49 deletions(-) diff --git a/Orange/canvas/document/schemeedit.py b/Orange/canvas/document/schemeedit.py index 449858b376a..840ece304a1 100644 --- a/Orange/canvas/document/schemeedit.py +++ b/Orange/canvas/document/schemeedit.py @@ -878,10 +878,27 @@ def removeLink(self, link): command = commands.RemoveLinkCommand(self.__scheme, link) self.__undoStack.push(command) - def insertNode(self, new_node, old_link, new_links): + def insertNode(self, new_node, old_link): """ Insert a node in-between two linked nodes. """ + source_node = old_link.source_node + sink_node = old_link.sink_node + + possible_links = (self.scheme().propose_links(source_node, new_node), + self.scheme().propose_links(new_node, sink_node)) + + first_link_sink_channel = [l[1] for l in possible_links[0] + if l[0] == old_link.source_channel][0] + second_link_source_channel = [l[0] for l in possible_links[1] + if l[1] == old_link.sink_channel][0] + + new_links = ( + SchemeLink(source_node, old_link.source_channel, + new_node, first_link_sink_channel), + SchemeLink(new_node, second_link_source_channel, + sink_node, old_link.sink_channel)) + command = commands.InsertNodeCommand(self.__scheme, new_node, old_link, new_links) self.__undoStack.push(command) @@ -1047,30 +1064,6 @@ def changeEvent(self, event): QWidget.changeEvent(self, event) - def tryInsertNode(self, link, new_node_desc, pos): - source_node = link.source_node - sink_node = link.sink_node - - if nodes_are_compatible(source_node.description, new_node_desc) and \ - nodes_are_compatible(new_node_desc, sink_node.description): - new_node = self.newNodeHelper(new_node_desc, position=(pos.x(), pos.y())) - - possible_links = (self.scheme().propose_links(source_node, new_node), - self.scheme().propose_links(new_node, sink_node)) - - if not possible_links[0] or not possible_links[1]: - raise ValueError("Cannot insert widget: links not possible") - - new_links = ( - SchemeLink(source_node, link.source_channel, - new_node, possible_links[0][0][1]), # first link, first entry, output - SchemeLink(new_node, possible_links[1][0][0], # second link, first entry, input - sink_node, link.sink_channel)) - - self.insertNode(new_node, link, new_links) - else: - self.createNewNode(new_node_desc, position=(pos.x(), pos.y())) - def eventFilter(self, obj, event): # Filter the scene's drag/drop events. if obj is self.scene(): @@ -1097,9 +1090,10 @@ def eventFilter(self, obj, event): else: pos = event.scenePos() item = self.__scene.item_at(event.scenePos(), items.LinkItem) - if item: - link = self.__scene.link_for_item(item) - self.tryInsertNode(link, desc, pos) + link = self.scene().link_for_item(item) if item else None + if link and can_insert_node(desc, link): + node = self.newNodeHelper(desc, position=(pos.x(), pos.y())) + self.insertNode(node, link) else: self.createNewNode(desc, position=(pos.x(), pos.y())) return True @@ -1653,10 +1647,9 @@ def __nodeInsert(self): sink_node = original_link.sink_node def filterFunc(index): - new_node_desc = index.data(QtWidgetRegistry.WIDGET_DESC_ROLE) - if isinstance(new_node_desc, WidgetDescription): - return nodes_are_compatible(source_node.description, new_node_desc) and \ - nodes_are_compatible(new_node_desc, sink_node.description) + desc = index.data(QtWidgetRegistry.WIDGET_DESC_ROLE) + if isinstance(desc, WidgetDescription): + return can_insert_node(desc, original_link) else: return False @@ -1676,23 +1669,15 @@ def filterFunc(index): if action: item = action.property("item") desc = item.data(QtWidgetRegistry.WIDGET_DESC_ROLE) - new_node = self.newNodeHelper(desc, position=(x, y)) else: return - possible_links = (self.scheme().propose_links(source_node, new_node), - self.scheme().propose_links(new_node, sink_node)) - - if not possible_links[0] or not possible_links[1]: - raise ValueError("Cannot insert widget: links not possible") - - new_links = ( - SchemeLink(source_node, original_link.source_channel, - new_node, possible_links[0][0][1]), # first link, first entry, output - SchemeLink(new_node, possible_links[1][0][0], # second link, first entry, input - sink_node, original_link.sink_channel)) + if can_insert_node(desc, original_link): + new_node = self.newNodeHelper(desc, position=(x, y)) + self.insertNode(new_node, original_link) + else: + log.info("Cannot insert node: links not possible.") - self.insertNode(new_node, original_link, new_links) def __duplicateSelected(self): """ @@ -2036,10 +2021,11 @@ def node_properties(scheme): return [dict(node.properties) for node in scheme.nodes] -def nodes_are_compatible(source, sink): - return any(scheme.compatible_channels(output, input) \ - for output in source.outputs \ - for input in sink.inputs) +def can_insert_node(new_node_desc, original_link): + return any(scheme.compatible_channels(original_link.source_channel, input) + for input in new_node_desc.inputs) and \ + any(scheme.compatible_channels(output, original_link.sink_channel) + for output in new_node_desc.outputs) def uniquify(item, names, pattern="{item}-{_}", start=0): From cdffe818312dcc148f5d6a04d270edfa5e25aa6e Mon Sep 17 00:00:00 2001 From: Rafael Irgolic Date: Tue, 7 Aug 2018 11:26:03 +0200 Subject: [PATCH 5/5] schemeedit: hover over link when inserting --- Orange/canvas/document/schemeedit.py | 33 +++++++++++++++++++++++----- 1 file changed, 27 insertions(+), 6 deletions(-) diff --git a/Orange/canvas/document/schemeedit.py b/Orange/canvas/document/schemeedit.py index 840ece304a1..73391af3257 100644 --- a/Orange/canvas/document/schemeedit.py +++ b/Orange/canvas/document/schemeedit.py @@ -132,6 +132,7 @@ def __init__(self, parent=None, ): self.__possibleMouseItemsMove = False self.__itemsMoving = {} self.__contextMenuTarget = None + self.__dropTarget = None self.__quickMenu = None self.__quickTip = "" @@ -1066,23 +1067,43 @@ def changeEvent(self, event): def eventFilter(self, obj, event): # Filter the scene's drag/drop events. + MIME_TYPE = "application/vnv.orange-canvas.registry.qualified-name" if obj is self.scene(): etype = event.type() if etype == QEvent.GraphicsSceneDragEnter or \ etype == QEvent.GraphicsSceneDragMove: mime_data = event.mimeData() - if mime_data.hasFormat( - "application/vnv.orange-canvas.registry.qualified-name" - ): + drop_target = None + if mime_data.hasFormat(MIME_TYPE): + qname = bytes(mime_data.data(MIME_TYPE)).decode("ascii") + try: + desc = self.__registry.widget(qname) + except KeyError: + pass + else: + item = self.__scene.item_at(event.scenePos(), items.LinkItem) + link = self.scene().link_for_item(item) if item else None + if link is not None and can_insert_node(desc, link): + drop_target = item + drop_target.setHoverState(True) event.acceptProposedAction() else: event.ignore() + + if self.__dropTarget is not None and \ + self.__dropTarget is not drop_target: + self.__dropTarget.setHoverState(False) + # self.__dropTarget = None + + self.__dropTarget = drop_target return True + elif etype == QEvent.GraphicsSceneDragLeave: + if self.__dropTarget is not None: + self.__dropTarget.setHoverState(False) + self.__dropTarget = None elif etype == QEvent.GraphicsSceneDrop: data = event.mimeData() - qname = data.data( - "application/vnv.orange-canvas.registry.qualified-name" - ) + qname = data.data(MIME_TYPE) try: desc = self.__registry.widget(bytes(qname).decode()) except KeyError: