Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[ENH] Widget Insertion #3179

Merged
merged 5 commits into from
Aug 8, 2018
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions Orange/canvas/canvas/items/linkitem.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ class LinkCurveItem(QGraphicsPathItem):
"""
def __init__(self, parent):
super().__init__(parent)
self.__parent = parent

self.setAcceptedMouseButtons(Qt.NoButton)
self.setAcceptHoverEvents(True)

Expand Down Expand Up @@ -91,6 +93,9 @@ def setPath(self, path):
self.__shape = None
super().setPath(path)

def parent(self):
return self.__parent
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the same as parentItem()


def __update(self):
shadow_enabled = self.__hover
if self.shadow.isEnabled() != shadow_enabled:
Expand Down
34 changes: 34 additions & 0 deletions Orange/canvas/document/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Change the command name/text to 'Insert widget on link' or something similar.

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))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This logic in __init__ should be moved out of the command class. Parameterize the command with the new node, old link and new link pairs.


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)
Expand Down
89 changes: 85 additions & 4 deletions Orange/canvas/document/schemeedit.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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",
Expand All @@ -346,6 +356,7 @@ def color_icon(color):
self.__newArrowAnnotationAction,
self.__linkEnableAction,
self.__linkRemoveAction,
self.__nodeInsertAction,
self.__linkResetAction,
self.__duplicateSelectedAction])

Expand Down Expand Up @@ -867,6 +878,13 @@ def removeLink(self, link):
command = commands.RemoveLinkCommand(self.__scheme, 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)

def onNewLink(self, func):
"""
Runs function when new link is added to current scheme.
Expand Down Expand Up @@ -1029,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):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test here should also be constrained on the existing link.source_channel and link.sink_channel.

E.g. If I use:

screen shot 2018-08-02 at 11 21 41

and drop an Impute widget on the link, I get a

------------------- IncompatibleChannelTypeError Exception --------------------
Traceback (most recent call last):
  File "/Users/aleserjavec/workspace/orange3/Orange/canvas/document/schemeedit.py", line 1102, in eventFilter
    self.tryInsertNode(link, desc, pos)
  File "/Users/aleserjavec/workspace/orange3/Orange/canvas/document/schemeedit.py", line 1066, in tryInsertNode
    new_node, possible_links[0][0][1]),  # first link, first entry, output
  File "/Users/aleserjavec/workspace/orange3/Orange/canvas/scheme/link.py", line 125, in __init__
    % (source_channel.type, sink_channel.type)
Orange.canvas.scheme.errors.IncompatibleChannelTypeError: Cannot connect 'Orange.base.Model' to 'Orange.base.Learner'
-------------------------------------------------------------------------------

The set of possible links below should also be filtered correspondingly.

P.S.
The same also applies to the 'Insert widget` context menu action.

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():
Expand All @@ -1054,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())
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can use self.__scene.item_at(event.scenePos(), items.LinkItem) for better and easier hit detection (you also would not need the new LinkCurveItem.parent method).

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:
Expand Down Expand Up @@ -1594,6 +1628,47 @@ 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 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)
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.
Expand Down Expand Up @@ -1936,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))
Expand Down