343 lines
14 KiB
Python
343 lines
14 KiB
Python
|
"""
|
||
|
a graphical editor for dataflow
|
||
|
"""
|
||
|
from __future__ import print_function
|
||
|
# import wx
|
||
|
import wx.lib.wxcairo
|
||
|
import cairo
|
||
|
# from wx.lib.scrolledpanel import ScrolledPanel
|
||
|
# import scenegraph2d.xml
|
||
|
import msspecgui.scenegraph2d.cairo
|
||
|
|
||
|
# import dataflow
|
||
|
import msspecgui.dataflow as dataflow
|
||
|
# import msspecgui.datafloweditor as datafloweditor
|
||
|
|
||
|
from .plugwidget import PlugWidget
|
||
|
from .wirewidget import WireWidget
|
||
|
from msspecgui.datafloweditor.dotlayoutmanager import DotLayoutManager
|
||
|
# class DataflowView(wx.ScrolledWindow):
|
||
|
|
||
|
|
||
|
class DataflowView(wx.Panel):
|
||
|
'''
|
||
|
a dataflow Graphical User Interface
|
||
|
|
||
|
:type self.wire_being_created_widget: WireWidget
|
||
|
'''
|
||
|
|
||
|
# see https://wiki.wxwidgets.org/Scrolling
|
||
|
|
||
|
class DataflowEventsHandler(dataflow.DataFlow.IDataFlowEventsHandler):
|
||
|
"""
|
||
|
handles dataflow events
|
||
|
"""
|
||
|
def __init__(self, data_flow_view):
|
||
|
"""
|
||
|
:param msspec.datafloweditor.DataflowView data_flow_view:
|
||
|
"""
|
||
|
super(DataflowView.DataflowEventsHandler, self).__init__()
|
||
|
self.data_flow_view = data_flow_view
|
||
|
|
||
|
def on_added_operator(self, operator):
|
||
|
super(DataflowView.DataflowEventsHandler, self).on_added_operator(operator)
|
||
|
# a new operator has just been added to the dataflow
|
||
|
# create a new widget to graphically manipulate the added operator
|
||
|
self.data_flow_view.add_operator_widget(msspecgui.datafloweditor.OperatorWidget(operator, self.data_flow_view))
|
||
|
|
||
|
def on_deleted_operator(self, operator):
|
||
|
super(DataflowView.DataflowEventsHandler, self).on_deleted_operator(operator)
|
||
|
self.data_flow_view.delete_widget_for_operator(operator)
|
||
|
self.data_flow_view.update_appearance() # possibly update the appearance of the plugs that have now become available
|
||
|
|
||
|
def on_added_wire(self, wire):
|
||
|
super(DataflowView.DataflowEventsHandler, self).on_added_wire(wire)
|
||
|
# create a new widget to graphically represent and manipulate the added wire
|
||
|
wire_widget = self.data_flow_view.add_wire_widget(wire) # @UnusedVariable
|
||
|
self.data_flow_view.update_appearance() # possibly update the appearance of the plugs that have now become available
|
||
|
|
||
|
def on_deleted_wire(self, wire):
|
||
|
super(DataflowView.DataflowEventsHandler, self).on_deleted_wire(wire)
|
||
|
self.data_flow_view.delete_widget_for_wire(wire)
|
||
|
self.data_flow_view.update_appearance() # possibly update the appearance of the plugs that have now become available
|
||
|
|
||
|
def __init__(self, parent, data_flow, log):
|
||
|
"""
|
||
|
:type parent: wx.Window
|
||
|
:type data_flow: DataFlow
|
||
|
"""
|
||
|
wx.Panel.__init__(self, parent, -1)
|
||
|
self.scenegraph_group_to_widget = {} # this array associates a datafloweditor.Widget instance to a scenegraph group that represents this widget
|
||
|
self.layout_is_enabled = True
|
||
|
|
||
|
self.log = log
|
||
|
self.layout_manager = DotLayoutManager()
|
||
|
self.scene = None # the 2d scenegraph that describes the graphical aspect of the dataflow
|
||
|
self.scene = msspecgui.scenegraph2d.Group()
|
||
|
# with open('/Users/graffy/ownCloud/ipr/msspec/rectangle.svg') as f:
|
||
|
# self.scene = scenegraph2d.xml.parse(f.read())
|
||
|
self.operator_to_widget = {}
|
||
|
self.wire_to_widget = {}
|
||
|
|
||
|
self.dataflow = data_flow
|
||
|
|
||
|
# self.selected_widget = None
|
||
|
self.hovered_widget = None # the widget hovered on by the mouse pointer
|
||
|
self.plug_being_connected = None # when the user creates a wire, memorizes the first selected plug
|
||
|
self.wire_being_created_widget = None # while the use creates a wire, the widget that is used to represent it
|
||
|
self.is_left_down = False
|
||
|
|
||
|
self.Bind(wx.EVT_PAINT, self.on_paint)
|
||
|
self.Bind(wx.EVT_LEFT_DOWN, self.on_left_down)
|
||
|
self.Bind(wx.EVT_LEFT_UP, self.on_left_up)
|
||
|
self.Bind(wx.EVT_MOTION, self.on_move)
|
||
|
self.Bind(wx.EVT_CONTEXT_MENU, self.on_context_menu)
|
||
|
|
||
|
self.data_flow_view_updater = DataflowView.DataflowEventsHandler(self)
|
||
|
self.layout_is_enabled = False # temporarily disable the layout manager for efficiency (prevent complete layout computation for each operator in the data_flow)
|
||
|
data_flow.add_dataflow_events_handler(self.data_flow_view_updater)
|
||
|
# initialize the widgets to reflect the state of the cluster flow
|
||
|
for node in self.dataflow.operators:
|
||
|
self.data_flow_view_updater.on_added_operator(node)
|
||
|
|
||
|
for wire in self.dataflow.wires:
|
||
|
self.data_flow_view_updater.on_added_wire(wire)
|
||
|
self.layout_is_enabled = True
|
||
|
self.update_operators_position()
|
||
|
|
||
|
def on_paint(self, evt): # IGNORE:unused-argument
|
||
|
"""handler for the wx.EVT_PAINT event
|
||
|
"""
|
||
|
if self.IsDoubleBuffered():
|
||
|
display_context = wx.PaintDC(self)
|
||
|
else:
|
||
|
display_context = wx.BufferedPaintDC(self)
|
||
|
display_context.SetBackground(wx.Brush('white'))
|
||
|
display_context.Clear()
|
||
|
|
||
|
self.render(display_context)
|
||
|
|
||
|
def update_appearance(self):
|
||
|
for operator_widget in self.operator_to_widget.itervalues():
|
||
|
operator_widget.update_appearance()
|
||
|
|
||
|
def update_operators_position(self):
|
||
|
if self.layout_is_enabled:
|
||
|
op_pos = self.layout_manager.compute_operators_position(self.dataflow)
|
||
|
dodgy_offset = (20.0, 20.0) # TODO: make it better
|
||
|
dodgy_scale = 1.5
|
||
|
for op, pos in op_pos.iteritems():
|
||
|
x, y = pos
|
||
|
print("update_operators_position : %f %f" % (x, y))
|
||
|
if op in self.operator_to_widget: # while loading the dataflow, there are operators that don't have widgets yet
|
||
|
op_widget = self.operator_to_widget[op]
|
||
|
op_widget.set_position(x * dodgy_scale + dodgy_offset[0], y * dodgy_scale + dodgy_offset[1])
|
||
|
|
||
|
def add_operator_widget(self, operator_widget):
|
||
|
"""
|
||
|
:type operator_widget: an instance of OperatorWidget
|
||
|
"""
|
||
|
self.operator_to_widget[operator_widget.operator] = operator_widget
|
||
|
widget_root = msspecgui.scenegraph2d.Group()
|
||
|
self.scene.add_child(widget_root)
|
||
|
# widget_root.transform = [msspecgui.scenegraph2d.Translate(operator_widget.get_id() * 100.0 + 50.0, 60.0)]
|
||
|
operator_widget.render_to_scene_graph(widget_root)
|
||
|
self.update_operators_position()
|
||
|
# self.UpdateWindowUI(wx.UPDATE_UI_RECURSE)
|
||
|
# print("self.UpdateWindowUI has executed")
|
||
|
# self.Refresh()
|
||
|
|
||
|
def delete_widget_for_operator(self, operator):
|
||
|
"""
|
||
|
:param Operator operator: the operator for which we want to delete the widget
|
||
|
"""
|
||
|
op_widget = self.get_operator_widget(operator)
|
||
|
op_widget.remove_from_scene_graph()
|
||
|
|
||
|
self.operator_to_widget.pop(operator)
|
||
|
|
||
|
def add_wire_widget(self, wire):
|
||
|
"""
|
||
|
creates a wire widget and adds it to this dataflow view
|
||
|
:param wire: the wire represented by the widget to create
|
||
|
:type wire: Wire
|
||
|
:rtype: WireWidget
|
||
|
"""
|
||
|
wire_widget = WireWidget(self)
|
||
|
wire_widget.source_plug_widget = self.get_plug_widget(wire.input_plug)
|
||
|
wire_widget.dest_plug_widget = self.get_plug_widget(wire.output_plug)
|
||
|
self.wire_to_widget[wire] = wire_widget
|
||
|
widget_root = msspecgui.scenegraph2d.Group()
|
||
|
self.scene.add_child(widget_root)
|
||
|
wire_widget.render_to_scene_graph(widget_root)
|
||
|
self.update_operators_position()
|
||
|
return wire_widget
|
||
|
|
||
|
def delete_widget_for_wire(self, wire):
|
||
|
"""
|
||
|
:param Wire wire: the wire for which we want to delete the widget
|
||
|
"""
|
||
|
wire_widget = self.get_wire_widget(wire)
|
||
|
wire_widget.remove_from_scene_graph()
|
||
|
# widget_root = wire_widget.parent()
|
||
|
# self.scene.remove_child(widget_root)
|
||
|
|
||
|
self.wire_to_widget.pop(wire)
|
||
|
self.update_operators_position()
|
||
|
|
||
|
def get_wire_widget(self, wire):
|
||
|
"""
|
||
|
Returns the widget associated with the given wire
|
||
|
:param Wire wire:
|
||
|
:return WireWidget:
|
||
|
"""
|
||
|
return self.wire_to_widget[wire]
|
||
|
|
||
|
def get_operator_widget(self, operator):
|
||
|
"""
|
||
|
Returns the operator widget associated with the given operator
|
||
|
:param Operator operator:
|
||
|
:return OperatorWidget:
|
||
|
"""
|
||
|
return self.operator_to_widget[operator]
|
||
|
|
||
|
def get_plug_widget(self, plug):
|
||
|
"""
|
||
|
Returns the plug widget associated with the given plug
|
||
|
:type plug: Plug
|
||
|
:rtype: PlugWidget
|
||
|
"""
|
||
|
operator_widget = self.get_operator_widget(plug.operator)
|
||
|
plug_widget = operator_widget.get_plug_widget(plug)
|
||
|
return plug_widget
|
||
|
|
||
|
def render(self, display_context):
|
||
|
"""
|
||
|
renders this dataflow view in the given drawing context
|
||
|
|
||
|
:param display_context: the drawing context in which to render the dataflow
|
||
|
:type display_context: a wx context
|
||
|
"""
|
||
|
#cairo_context = wx.lib.wxcairo.ContextFromDC(display_context)
|
||
|
w, h = self.GetClientSize()
|
||
|
surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, w, h)
|
||
|
cairo_context = cairo.Context(surface)
|
||
|
|
||
|
msspecgui.scenegraph2d.cairo.render_scene(self.scene, cairo_context)
|
||
|
|
||
|
bitmap = wx.lib.wxcairo.BitmapFromImageSurface(surface)
|
||
|
display_context.DrawBitmap(bitmap, 0, 0)
|
||
|
|
||
|
def get_pointed_widget(self, pointer_position):
|
||
|
"""
|
||
|
returns the widget that is at the position pointer_position
|
||
|
"""
|
||
|
hits = self.scene.pick(pointer_position[0], pointer_position[1])
|
||
|
if len(hits) > 0:
|
||
|
svg_path, local_pos = hits[-1] # @UnusedVariable
|
||
|
# print("hit svg node stack : %s" % str(svg_path))
|
||
|
for svg_node in reversed(svg_path):
|
||
|
if svg_node in self.scenegraph_group_to_widget:
|
||
|
return self.scenegraph_group_to_widget[svg_node]
|
||
|
|
||
|
# for widget in self.operator_to_widget.itervalues():
|
||
|
# if widget.get_bounding_box().Contains(pointer_position):
|
||
|
# return widget
|
||
|
return None
|
||
|
|
||
|
# def select_widget(self, widget):
|
||
|
# if self.selected_widget is not None:
|
||
|
# self.selected_widget.set_selected_state(False)
|
||
|
# self.selected_widget = widget
|
||
|
# self.selected_widget.set_selected_state(True)
|
||
|
|
||
|
def on_left_down(self, event):
|
||
|
pos = event.GetPositionTuple()
|
||
|
display_context = wx.ClientDC(self)
|
||
|
display_context.DrawCircle(pos[0], pos[1], 5)
|
||
|
widget = self.get_pointed_widget(pos)
|
||
|
if widget is not None:
|
||
|
# self.select_widget(widget)
|
||
|
if isinstance(widget, PlugWidget):
|
||
|
plug_widget = widget
|
||
|
if plug_widget.is_connectable():
|
||
|
self.plug_being_connected = plug_widget
|
||
|
wire_widget = WireWidget(self)
|
||
|
if plug_widget.plug.is_source():
|
||
|
wire_widget.source_plug_widget = plug_widget
|
||
|
else:
|
||
|
wire_widget.dest_plug_widget = plug_widget
|
||
|
self.wire_being_created_widget = wire_widget
|
||
|
self.wire_being_created_widget.render_to_scene_graph(self.scene)
|
||
|
self.is_left_down = True
|
||
|
self.Refresh()
|
||
|
|
||
|
def on_left_up(self, event):
|
||
|
self.is_left_down = False
|
||
|
self.plug_being_connected = None
|
||
|
|
||
|
if self.wire_being_created_widget is not None:
|
||
|
pos = event.GetPositionTuple()
|
||
|
pointed_widget = self.get_pointed_widget(pos)
|
||
|
if pointed_widget is not None and isinstance(pointed_widget, PlugWidget):
|
||
|
if self.wire_being_created_widget.is_valid_final_plug_widget(pointed_widget):
|
||
|
self.wire_being_created_widget.set_final_plug_widget(pointed_widget)
|
||
|
self.dataflow.create_wire(self.wire_being_created_widget.source_plug_widget.plug, self.wire_being_created_widget.dest_plug_widget.plug)
|
||
|
self.wire_being_created_widget.remove_from_scene_graph()
|
||
|
self.wire_being_created_widget = None
|
||
|
self.Refresh()
|
||
|
|
||
|
def on_move(self, event):
|
||
|
pos = event.GetPositionTuple()
|
||
|
if self.wire_being_created_widget is not None:
|
||
|
self.wire_being_created_widget.set_pointer_pos(pos)
|
||
|
widget = self.get_pointed_widget(pos)
|
||
|
# print('widget at (%d,%d) : %s\n' % (pos[0], pos[1], widget))
|
||
|
if self.hovered_widget is not None:
|
||
|
if self.hovered_widget != widget:
|
||
|
self.hovered_widget.on_hover(False)
|
||
|
self.hovered_widget = None
|
||
|
if widget is not None:
|
||
|
if self.hovered_widget is None:
|
||
|
widget.on_hover(True)
|
||
|
self.hovered_widget = widget
|
||
|
|
||
|
if self.is_left_down:
|
||
|
display_context = wx.ClientDC(self)
|
||
|
display_context.DrawCircle(pos[0], pos[1], 3)
|
||
|
self.Refresh()
|
||
|
|
||
|
def on_context_menu(self, event):
|
||
|
pos = self.ScreenToClient(event.GetPosition())
|
||
|
|
||
|
for wire_widget in self.wire_to_widget.itervalues():
|
||
|
if wire_widget.get_bounding_box(border=5).Contains(pos):
|
||
|
self.on_wire_context_menu(event, wire_widget.wire)
|
||
|
return
|
||
|
|
||
|
for operator_widget in self.operator_to_widget.itervalues():
|
||
|
if operator_widget.get_bounding_box().Contains(pos):
|
||
|
self.on_operator_context_menu(event, operator_widget.operator)
|
||
|
return
|
||
|
|
||
|
self.on_background_context_menu(event)
|
||
|
|
||
|
def on_operator_context_menu(self, event, operator):
|
||
|
'''
|
||
|
called whenever the user right-clicks in an operator of the dataflow
|
||
|
'''
|
||
|
pass # possibly implemented in derived classes
|
||
|
|
||
|
def on_wire_context_menu(self, event, wire):
|
||
|
'''
|
||
|
called whenever the user right-clicks in an operator of the dataflow
|
||
|
:param msspec.dataflow.Wire wire: the wire on which the context menu is supposed to act
|
||
|
'''
|
||
|
pass # possibly implemented in derived classes
|
||
|
|
||
|
def on_background_context_menu(self, event):
|
||
|
'''
|
||
|
called whenever the user right-clicks in the background of the dataflow
|
||
|
'''
|
||
|
pass # possibly implemented in derived classes
|