msspec_python3/msspec/msspecgui/datafloweditor/dataflowview.py

343 lines
14 KiB
Python
Raw Normal View History

2019-11-14 15:16:51 +01:00
"""
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