diff --git a/editor/FBD_library.py b/editor/FBD_library.py deleted file mode 100644 index d3db464b..00000000 --- a/editor/FBD_library.py +++ /dev/null @@ -1,125 +0,0 @@ - -import FBD_view -import FBD_model -import remi -import remi.gui as gui -import time -import inspect -import types - - -class PRINT(FBD_view.FunctionBlockView): - @FBD_model.FunctionBlock.decorate_process([]) - def do(self, IN, EN = True): - if not EN: - return - print(IN) - -class STRING(FBD_view.FunctionBlockView): - @property - @gui.editor_attribute_decorator("WidgetSpecific",'''Defines the actual value''', str, {}) - def value(self): - if len(self.outputs) < 1: - return "" - return self.outputs['OUT'].get_value() - @value.setter - def value(self, value): self.outputs['OUT'].set_value(value) - - def __init__(self, name, *args, **kwargs): - FBD_view.FunctionBlockView.__init__(self, name, *args, **kwargs) - self.outputs['OUT'].set_value("A STRING VALUE") - - @FBD_model.FunctionBlock.decorate_process(['OUT']) - def do(self): - OUT = self.outputs['OUT'].get_value() - return OUT - -class STRING_SWAP_CASE(FBD_view.FunctionBlockView): - @FBD_model.FunctionBlock.decorate_process(['OUT']) - def do(self, IN, EN = True): - if not EN: - return - OUT = IN.swapcase() - return OUT - -class BOOL(FBD_view.FunctionBlockView): - @property - @gui.editor_attribute_decorator("WidgetSpecific",'''Defines the actual value''', bool, {}) - def value(self): - if len(self.outputs) < 1: - return False - return self.outputs['OUT'].get_value() - @value.setter - def value(self, value): self.outputs['OUT'].set_value(value) - - def __init__(self, name, *args, **kwargs): - FBD_view.FunctionBlockView.__init__(self, name, *args, **kwargs) - self.outputs['OUT'].set_value(False) - - @FBD_model.FunctionBlock.decorate_process(['OUT']) - def do(self): - OUT = self.outputs['OUT'].get_value() - return OUT - -class RISING_EDGE(FBD_view.FunctionBlockView): - previous_value = None - - @FBD_model.FunctionBlock.decorate_process(['OUT']) - def do(self, IN): - OUT = (self.previous_value != IN) and IN - self.previous_value = IN - return OUT - -class NOT(FBD_view.FunctionBlockView): - @FBD_model.FunctionBlock.decorate_process(['OUT']) - def do(self, IN): - OUT = not IN - return OUT - -class AND(FBD_view.FunctionBlockView): - @FBD_model.FunctionBlock.decorate_process(['OUT']) - def do(self, IN1, IN2): - OUT = IN1 and IN2 - return OUT - -class OR(FBD_view.FunctionBlockView): - @FBD_model.FunctionBlock.decorate_process(['OUT']) - def do(self, IN1, IN2): - OUT = IN1 or IN2 - return OUT - -class XOR(FBD_view.FunctionBlockView): - @FBD_model.FunctionBlock.decorate_process(['OUT']) - def do(self, IN1, IN2): - OUT = IN1 != IN2 - return OUT - -class PULSAR(FBD_view.FunctionBlockView): - _ton = 1000 - _toff = 1000 - - @property - @gui.editor_attribute_decorator("WidgetSpecific",'''Defines the actual TON value''', int, {'possible_values': '', 'min': 0, 'max': 65535, 'default': 0, 'step': 1}) - def ton(self): - return self._ton - @ton.setter - def ton(self, value): self._ton = value - - @property - @gui.editor_attribute_decorator("WidgetSpecific",'''Defines the actual TOFF value''', int, {'possible_values': '', 'min': 0, 'max': 65535, 'default': 0, 'step': 1}) - def toff(self): - return self._toff - @toff.setter - def toff(self, value): self._toff = value - - tstart = 0 - def __init__(self, name, *args, **kwargs): - FBD_view.FunctionBlockView.__init__(self, name, *args, **kwargs) - self.outputs['OUT'].set_value(False) - self.tstart = time.time() - - @FBD_model.FunctionBlock.decorate_process(['OUT']) - def do(self): - OUT = (int((time.time() - self.tstart)*1000) % (self.ton + self.toff)) < self.ton - return OUT - diff --git a/editor/FBD_model.py b/editor/FBD_model.py deleted file mode 100644 index 3d62eb76..00000000 --- a/editor/FBD_model.py +++ /dev/null @@ -1,172 +0,0 @@ -import inspect - -class Input(): - name = None - default = None - typ = None - source = None #has to be an Output - - def __init__(self, name, default = inspect.Parameter.empty, typ = None): - self.name = name - self.default = default - self.typ = typ - - def get_value(self): - if not self.is_linked(): - return self.default - return self.source.get_value() - - def has_default(self): - return not (self.default == inspect.Parameter.empty) - - def link(self, output): - if not issubclass(type(output), Output): - return - self.source = output - - def is_linked(self): - return self.source != None - - def unlink(self): - Input.link(self, None) - - -class Output(): - name = None - typ = None - destinations = None #has to be an Input - value = None - - def __init__(self, name, typ = None): - self.name = name - self.typ = typ - self.destinations = [] - - def get_value(self): - return self.value - - def set_value(self, value): - self.value = value - - def link(self, destination): - if not issubclass(type(destination), Input): - return - self.destinations.append(destination) - - def is_linked(self): - return len(self.destinations) > 0 - - def unlink(self, destination = None): - if not destination is None: - self.destinations.remove(destination) - return - self.destinations = [] - - -class ObjectBlock(): - name = None - FBs = None #this is the list of member functions - - inputs = None - outputs = None - - def __init__(self, name): - self.name = name - self.FBs = {} - self.inputs = {} - self.outputs = {} - - def add_io(self, io): - if issubclass(type(io), Input): - self.inputs[io.name] = io - else: - self.outputs[io.name] = io - - -class FunctionBlock(): - name = None - inputs = None - outputs = None - - def decorate_process(output_list): - """ setup a method as a process FunctionBlock """ - """ - input parameters can be obtained by introspection - outputs values (return values) are to be described with decorator - """ - def add_annotation(method): - setattr(method, "_outputs", output_list) - return method - return add_annotation - - def __init__(self, name): - self.name = name - self.inputs = {} - self.outputs = {} - - def add_io(self, io): - if issubclass(type(io), Input): - self.inputs[io.name] = io - else: - self.outputs[io.name] = io - - @decorate_process([]) - def do(self): - return None - - -class Link(): - source = None - destination = None - def __init__(self, source_widget, destination_widget): - self.source = source_widget - self.destination = destination_widget - - def unlink(self): - self.source.unlink(self.destination) - self.destination.unlink() - - -class Process(): - function_blocks = None - object_blocks = None - - def __init__(self): - self.function_blocks = {} - self.object_blocks = {} - - def add_function_block(self, function_block): - self.function_blocks[function_block.name] = function_block - - def add_object_block(self, object_block): - self.object_blocks[object_block.name] = object_block - - def do(self): - sub_function_blocks = [] - for object_block in self.object_blocks.values(): - for function_block in object_block.FBs.values(): - sub_function_blocks.append(function_block) - - for function_block in (*self.function_blocks.values(), *sub_function_blocks): - parameters = {} - all_inputs_connected = True - - for IN in function_block.inputs.values(): - if (not IN.is_linked()) and (not IN.has_default()): - all_inputs_connected = False - continue - parameters[IN.name] = IN.get_value() - - if not all_inputs_connected: - continue - output_results = function_block.do(**parameters) - if output_results is None: - continue - i = 0 - for OUT in function_block.outputs.values(): - if type(output_results) in (tuple, list): - OUT.set_value(output_results[i]) - else: - OUT.set_value(output_results) - i += 1 - diff --git a/editor/editor.py b/editor/editor.py index ddb9b8fb..78e32e75 100644 --- a/editor/editor.py +++ b/editor/editor.py @@ -78,7 +78,8 @@ def setup(self, refWidget, newParent): def start_drag(self, emitter, x, y): self.active = True - self.app_instance.mainContainer.onmousemove.do(self.on_drag, js_prevent_default=True, js_stop_propagation=False) + self.app_instance.mainContainer.onmousemove.do(self.on_drag, js_prevent_default=True, js_stop_propagation=True) + self.onmousemove.do(None, js_prevent_default=True, js_stop_propagation=False) self.app_instance.mainContainer.onmouseup.do(self.stop_drag, js_prevent_default=True, js_stop_propagation=False) self.app_instance.mainContainer.onmouseleave.do(self.stop_drag, 0, 0, js_prevent_default=True, js_stop_propagation=False) self.origin_x = -1 @@ -127,8 +128,8 @@ def setup(self, refWidget, newParent): def on_drag(self, emitter, x, y): if self.active: if self.origin_x == -1: - self.origin_x = float(x) - self.origin_y = float(y) + self.origin_x = float(self.app_instance.mouse_position_x) + self.origin_y = float(self.app_instance.mouse_position_y) self.refWidget_origin_x = float( self.refWidget.attributes[self.name_coord_x]) self.refWidget_origin_y = float( @@ -166,8 +167,8 @@ def setup(self, refWidget, newParent): def on_drag(self, emitter, x, y): if self.active: if self.origin_x == -1: - self.origin_x = float(x) - self.origin_y = float(y) + self.origin_x = float(self.app_instance.mouse_position_x) + self.origin_y = float(self.app_instance.mouse_position_y) self.refWidget_origin_w = float( self.refWidget.attributes['width']) self.refWidget_origin_h = float( @@ -205,8 +206,8 @@ def setup(self, refWidget, newParent): def on_drag(self, emitter, x, y): if self.active: if self.origin_x == -1: - self.origin_x = float(x) - self.origin_y = float(y) + self.origin_x = float(self.app_instance.mouse_position_x) + self.origin_y = float(self.app_instance.mouse_position_y) self.refWidget_origin_r = float(self.refWidget.attributes['r']) else: r = self.round_grid( @@ -237,16 +238,20 @@ def setup(self, refWidget, newParent): self.style['display'] = 'none' if issubclass(newParent.__class__, gui.TabBox): return - if issubclass(refWidget.__class__, gui.Widget) and 'left' in refWidget.style and 'top' in refWidget.style and \ - 'width' in refWidget.style and 'height' in refWidget.style and refWidget.css_position=='absolute': + if issubclass(refWidget.__class__, gui.Widget) and 'width' in refWidget.style \ + and 'height' in refWidget.style and refWidget.css_position=='absolute': + if not 'left' in refWidget.style: + refWidget.css_left = '0px' + if not 'top' in refWidget.style: + refWidget.css_top = '0px' DraggableItem.setup(self, refWidget, newParent) self.style['display'] = 'block' def on_drag(self, emitter, x, y): if self.active: if self.origin_x == -1: - self.origin_x = float(x) - self.origin_y = float(y) + self.origin_x = float(self.app_instance.mouse_position_x) + self.origin_y = float(self.app_instance.mouse_position_y) self.refWidget_origin_w = gui.from_pix( self.refWidget.style['width']) self.refWidget_origin_h = gui.from_pix( @@ -292,8 +297,8 @@ def setup(self, refWidget, newParent): def on_drag(self, emitter, x, y): if self.active: if self.origin_x == -1: - self.origin_x = float(x) - self.origin_y = float(y) + self.origin_x = float(self.app_instance.mouse_position_x) + self.origin_y = float(self.app_instance.mouse_position_y) self.refWidget_origin_x = gui.from_pix( self.refWidget.style['left']) self.refWidget_origin_y = gui.from_pix( @@ -382,6 +387,12 @@ def check_pending_listeners(self, widget, widgetVarName, force=False): source_filtered_path.remove(v) listener_filtered_path.remove(v) + #if the source is a children of the actual widget + """ + if len(source_filtered_path) == 1: + if source_filtered_path[0] in widget.children.keys() and event['eventsource'].attr_editor_newclass == False: + sourcename = widget.variable_name + """ if len(source_filtered_path) == 0 and event['eventsource'].attr_editor_newclass == False: sourcename = event['eventsource'].variable_name @@ -401,6 +412,11 @@ def check_pending_listeners(self, widget, widgetVarName, force=False): if force or (self.children['root'] == widget and not (widget.attr_editor_newclass == True)): if event['eventlistener'] != self: listenername = self.children['root'].variable_name + """ + if len(listener_filtered_path) == 1: + if listener_filtered_path[0] in widget.children.keys() and event['eventlistener'].attr_editor_newclass == False: + listenername = widget.variable_name + """ if len(listener_filtered_path) == 0 and event['eventlistener'].attr_editor_newclass == False: listenername = event['eventlistener'].variable_name if len(listener_filtered_path) > 0: @@ -449,9 +465,6 @@ def repr_widget_for_editor(self, widget, first_node=False): code_nested = prototypes.proto_widget_allocation % { 'varname': widgetVarName, 'classname': classname} - #code_nested += prototypes.proto_attribute_setup%{'varname': widgetVarName, 'attr_dict': ','.join('"%s":"%s"'%(key,widget.attributes[key]) for key in widget.attributes.keys() if key not in html_helper.htmlInternallyUsedTags)} - #code_nested += prototypes.proto_style_setup%{'varname': widgetVarName, 'style_dict': ','.join('"%s":"%s"'%(key,widget.style[key]) for key in widget.style.keys())} - for x, y in inspect.getmembers(widget.__class__): if type(y) == property and not getattr(widget, x) is None: if hasattr(y.fget, "editor_attributes"): #if this property is visible for the editor @@ -461,6 +474,7 @@ def repr_widget_for_editor(self, widget, first_node=False): code_nested += prototypes.proto_property_setup % { 'varname': widgetVarName, 'property': x, 'value': _value} + # for all the methods of this widget for (setOnEventListenerFuncname, setOnEventListenerFunc) in inspect.getmembers(widget): # if the member is decorated by decorate_set_on_listener @@ -526,6 +540,21 @@ def repr_widget_for_editor(self, widget, first_node=False): children_code_nested += self.check_pending_listeners( widget, widgetVarName) + + try: + from widgets import FBD_model + except Exception: + from .widgets import FBD_model + + if issubclass(type(widget), FBD_model.Process): + for FB in widget.children.values(): + if issubclass(type(FB), FBD_model.FunctionBlock): + for OUT in FB.outputs.values(): + if OUT.is_linked(): + for IN in OUT.linked_nodes: + children_code_nested += prototypes.proto_link_FB_out_to % {'process_name': widgetVarName, 'fb_source': FB.variable_name, 'outname': OUT.name, 'fb_desitionation': IN.get_parent().variable_name, 'inputname': IN.name} + + # and not (classname in self.code_declared_classes.keys()): if widget.attr_editor_newclass: if not widget.identifier in self.code_declared_classes: @@ -726,6 +755,9 @@ class Editor(App): projectPathFilename = None + mouse_position_x = 0 + mouse_position_y = 0 + def __init__(self, *args): editor_res_path = os.path.join(os.path.dirname(__file__), 'res') super(Editor, self).__init__( @@ -773,6 +805,8 @@ def main(self): | toolbox | signals | properties | """, 0, 0) + self.mainContainer.onmousedown.do(self.mouseclicked) + menubar = gui.MenuBar(width='100%', height='100%') menu = gui.Menu(width='100%', height='100%') menu.style['z-index'] = '1' @@ -893,6 +927,10 @@ def main(self): # returning the root widget return self.mainContainer + def mouseclicked(self, emitter, x, y): + self.mouse_position_x = x + self.mouse_position_y = y + def on_snap_grid_size_change(self, emitter, value): value = float(value) for drag_helper in self.drag_helpers: @@ -973,13 +1011,15 @@ def on_widget_selection(self, widget): import time t = time.time() + self.widgetsCollection.widget_selected(widget.__class__ if widget != self.project else gui.Widget) + """ if issubclass(widget.__class__, gui.Container) or widget == None: self.mainContainer.append( self.widgetsCollection, 'toolbox') else: self.mainContainer.append(gui.Label("Cannot append widgets to %s class. It is not a container. Select a container" % widget.__class__.__name__), 'toolbox') - + """ if self.selectedWidget == widget or widget == self.project: self.selectedWidget = widget return diff --git a/editor/editor_widgets.py b/editor/editor_widgets.py index 3897eb84..898da417 100644 --- a/editor/editor_widgets.py +++ b/editor/editor_widgets.py @@ -21,6 +21,62 @@ import os +class ProcessAddFBWrapper(gui.GenericDialog): + KEY_WIDGET_INSTANCE = 'widget_instance' + KEY_METHOD_NAME = 'method_name' + + process_view_instance = None + + def __init__(self, *args, **kwargs): + gui.GenericDialog.__init__(self, 'Function Block Wrapper', + 'Here you can create a Function Block that wraps a widget method.', width=500, *args, **kwargs) + + self.add_field_with_label(self.KEY_WIDGET_INSTANCE, 'Widget instance', gui.DropDown()) + self.get_field(self.KEY_WIDGET_INSTANCE).onchange.do(self.widget_instance_selected) + self.add_field_with_label(self.KEY_METHOD_NAME, 'Method name', gui.DropDown()) + + def widget_instance_selected(self, emitter, drop_down_item): + """ A widget has been selected. + The method names dropdown must be populated + """ + self.get_field(self.KEY_METHOD_NAME).empty() + # for all the methods of this widget + for (funcname, function) in inspect.getmembers(drop_down_item): + di = gui.DropDownItem(funcname) + self.get_field(self.KEY_METHOD_NAME).append(di) + + @gui.decorate_event + def confirm_dialog(self, emitter): + """event called pressing on OK button. + """ + widget_name = self.get_field(self.KEY_WIDGET_INSTANCE).get_value() + widget_instance = self.get_field(self.KEY_WIDGET_INSTANCE).children[widget_name] + function_name = self.get_field(self.KEY_METHOD_NAME).get_value() + + try: + import widgets + except Exception: + from . import widgets + fb = widgets.FBD_library.FBWrapObjectMethod(getattr(widget_instance, function_name)) + fb.variable_name = "wrapped:" + widget_name + "." + function_name + #fb.add_io_widget(OutputView("IMAGE")) + self.process_view_instance.add_function_block(fb) + return super(ProcessAddFBWrapper, self).confirm_dialog(self) + + def show(self, baseAppInstance, process_view_instance, widgets_instance_list): + """Allows to show the widget as root window""" + + self.process_view_instance = process_view_instance + + self.get_field(self.KEY_WIDGET_INSTANCE).empty() + for w in widgets_instance_list: + di = gui.DropDownItem(w.variable_name) + di.widget_instance = w + self.get_field(self.KEY_WIDGET_INSTANCE).append(w) + + super(ProcessAddFBWrapper, self).show(baseAppInstance) + + class InstancesTree(gui.TreeView): def __init__(self, **kwargs): super(InstancesTree, self).__init__(**kwargs) @@ -474,8 +530,8 @@ def __init__(self, appInstance, widgetClass, **kwargs_to_widget): self.appInstance = appInstance self.widgetClass = widgetClass super(WidgetHelper, self).__init__() - self.style.update({'background-color': 'rgb(250,250,250)', 'width': "auto", 'margin':"2px", - "height": "60px", "justify-content": "center", "align-items": "center", + self.style.update({'background-color': 'rgb(250,250,250)', 'width': "100%", 'margin':"2px", + "height": "20px", "justify-content": "flex-start", "align-items": "center", 'font-size': '12px'}) if hasattr(widgetClass, "icon"): if type(widgetClass.icon) == gui.Svg: @@ -492,6 +548,7 @@ def __init__(self, appInstance, widgetClass, **kwargs_to_widget): else: self.icon = default_icon(self.widgetClass.__name__) + self.icon.style['height'] = '100%' self.icon.style['max-width'] = '100%' self.icon.style['image-rendering'] = 'auto' self.icon.attributes['draggable'] = 'false' @@ -569,6 +626,9 @@ def create_instance(self, widget): self.appInstance.add_widget_to_editor(widget) + def show(self, value): + self.css_display = 'flex' if value else 'none' + class WidgetCollection(gui.Container): def __init__(self, appInstance, **kwargs): @@ -588,66 +648,98 @@ def __init__(self, appInstance, **kwargs): # load all widgets self.add_widget_to_collection(gui.HBox, width='250px', height='250px', style={ 'top': '20px', 'left': '20px', 'position': 'absolute'}) + gui.HBox.valid_parent_types = [gui.Widget, gui.Container, gui.HBox, gui.VBox] self.add_widget_to_collection(gui.VBox, width='250px', height='250px', style={ 'top': '20px', 'left': '20px', 'position': 'absolute'}) + gui.VBox.valid_parent_types = [gui.Widget, gui.Container, gui.HBox, gui.VBox] self.add_widget_to_collection(gui.Container, width='250px', height='250px', style={ 'top': '20px', 'left': '20px', 'position': 'absolute'}) + gui.Container.valid_parent_types = [gui.Widget, gui.Container, gui.HBox, gui.VBox] #self.add_widget_to_collection(gui.GridBox, width='250px', height='250px', style={'top':'20px', 'left':'20px', 'position':'absolute'}) self.add_widget_to_collection(gui.Button, text="button", width='100px', height='30px', style={ 'top': '20px', 'left': '20px', 'position': 'absolute'}) + gui.Button.valid_parent_types = [gui.Container, gui.HBox, gui.VBox] self.add_widget_to_collection(gui.TextInput, width='100px', height='30px', style={ 'top': '20px', 'left': '20px', 'position': 'absolute'}) + gui.TextInput.valid_parent_types = [gui.Container, gui.HBox, gui.VBox] self.add_widget_to_collection(gui.Label, text="label", width='100px', height='30px', style={ 'top': '20px', 'left': '20px', 'position': 'absolute'}) + gui.Label.valid_parent_types = [gui.Container, gui.HBox, gui.VBox] self.add_widget_to_collection(gui.ListView, width='100px', height='100px', style={ 'top': '20px', 'left': '20px', 'position': 'absolute', 'border': '1px solid lightgray'}) + gui.ListView.valid_parent_types = [gui.Container, gui.HBox, gui.VBox] self.add_widget_to_collection(gui.ListItem, text='list item') + gui.ListItem.valid_parent_types = [gui.ListView] self.add_widget_to_collection(gui.DropDown, width='100px', height='30px', style={ 'top': '20px', 'left': '20px', 'position': 'absolute'}) + gui.DropDown.valid_parent_types = [gui.Container, gui.HBox, gui.VBox] self.add_widget_to_collection(gui.DropDownItem, text='drop down item') + gui.DropDownItem.valid_parent_types = [gui.DropDown] self.add_widget_to_collection(gui.Image, width='100px', height='100px', style={ 'top': '20px', 'left': '20px', 'position': 'absolute'}) + gui.Image.valid_parent_types = [gui.Container, gui.HBox, gui.VBox] self.add_widget_to_collection(gui.CheckBoxLabel, text='check box label', width='100px', height='30px', style={ 'top': '20px', 'left': '20px', 'position': 'absolute'}) + gui.CheckBoxLabel.valid_parent_types = [gui.Container, gui.HBox, gui.VBox] self.add_widget_to_collection(gui.CheckBox, width='30px', height='30px', style={ 'top': '20px', 'left': '20px', 'position': 'absolute'}) + gui.CheckBox.valid_parent_types = [gui.Container, gui.HBox, gui.VBox] self.add_widget_to_collection(gui.SpinBox, width='100px', height='30px', style={ 'top': '20px', 'left': '20px', 'position': 'absolute'}) + gui.SpinBox.valid_parent_types = [gui.Container, gui.HBox, gui.VBox] self.add_widget_to_collection(gui.Slider, width='100px', height='30px', style={ 'top': '20px', 'left': '20px', 'position': 'absolute'}) + gui.Slider.valid_parent_types = [gui.Container, gui.HBox, gui.VBox] self.add_widget_to_collection(gui.ColorPicker, width='100px', height='30px', style={ 'top': '20px', 'left': '20px', 'position': 'absolute'}) + gui.ColorPicker.valid_parent_types = [gui.Container, gui.HBox, gui.VBox] self.add_widget_to_collection(gui.Date, width='100px', height='30px', style={ 'top': '20px', 'left': '20px', 'position': 'absolute'}) + gui.Date.valid_parent_types = [gui.Container, gui.HBox, gui.VBox] self.add_widget_to_collection(gui.Link, text='link', url='', width='100px', height='30px', style={ 'top': '20px', 'left': '20px', 'position': 'absolute'}) + gui.Link.valid_parent_types = [gui.Container, gui.HBox, gui.VBox] self.add_widget_to_collection(gui.Progress, value=0, _max=100, width='130px', height='30px', style={ 'top': '20px', 'left': '20px', 'position': 'absolute'}) + gui.Progress.valid_parent_types = [gui.Container, gui.HBox, gui.VBox] self.add_widget_to_collection(gui.FileFolderNavigator, width=100, height=100, style = { 'top': '20px', 'left': '20px', 'position': 'absolute'}) + gui.FileFolderNavigator.valid_parent_types = [gui.Container, gui.HBox, gui.VBox] #self.add_widget_to_collection(gui.VideoPlayer, width='100px', height='100px', style={ # 'top': '20px', 'left': '20px', 'position': 'absolute'}) self.add_widget_to_collection(gui.TableWidget, width='100px', height='100px', style={ 'top': '20px', 'left': '20px', 'position': 'absolute'}) + gui.TableWidget.valid_parent_types = [gui.Container, gui.HBox, gui.VBox] self.add_widget_to_collection(gui.TabBox, width='200px', height='200px', style={ 'top': '20px', 'left': '20px', 'position': 'absolute'}) + gui.TabBox.valid_parent_types = [gui.Container, gui.HBox, gui.VBox] self.add_widget_to_collection(gui.FileUploader, width='150px', height='30px', style={ 'top': '20px', 'left': '20px', 'position': 'absolute'}) + gui.FileUploader.valid_parent_types = [gui.Container, gui.HBox, gui.VBox] self.add_widget_to_collection( gui.Svg, style={'top': '20px', 'left': '20px', 'width':'150px', 'height':'150px', 'position': 'absolute', 'border':'1px solid gray'}) + gui.Svg.valid_parent_types = [gui.Container, gui.HBox, gui.VBox] self.add_widget_to_collection( gui.SvgSubcontainer, x = '10', y = '10', width = '80', height = '80') + gui.SvgSubcontainer.valid_parent_types = [gui.Svg, gui.SvgSubcontainer] self.add_widget_to_collection(gui.SvgLine, attributes={ 'stroke': 'black', 'stroke-width': '1'}) + gui.SvgLine.valid_parent_types = [gui.Svg, gui.SvgSubcontainer] self.add_widget_to_collection(gui.SvgCircle) + gui.SvgCircle.valid_parent_types = [gui.Svg, gui.SvgSubcontainer] self.add_widget_to_collection(gui.SvgEllipse) + gui.SvgEllipse.valid_parent_types = [gui.Svg, gui.SvgSubcontainer] self.add_widget_to_collection(gui.SvgRectangle) + gui.SvgRectangle.valid_parent_types = [gui.Svg, gui.SvgSubcontainer] self.add_widget_to_collection(gui.SvgText) + gui.SvgText.valid_parent_types = [gui.Svg, gui.SvgSubcontainer] self.add_widget_to_collection(gui.SvgPath, attributes={ 'stroke': 'black', 'stroke-width': '1'}) + gui.SvgPath.valid_parent_types = [gui.Svg, gui.SvgSubcontainer] self.add_widget_to_collection(gui.SvgImage) + gui.SvgImage.valid_parent_types = [gui.Svg, gui.SvgSubcontainer] self.add_widget_to_collection(gui.SvgGroup) - + gui.SvgGroup.valid_parent_types = [gui.Svg, gui.SvgSubcontainer] #self.load_additional_widgets() def load_additional_widgets(self): @@ -679,6 +771,24 @@ def add_widget_to_collection(self, widgetClass, group='standard_tools', **kwargs #self.widgetsContainer.append( helper ) self.widgetsContainer.children[group].append(helper) + def widget_selected(self, widgetClass): + for group in self.widgetsContainer.children.values(): + visible_children_count = 0 + for w_helper in group.container.children.values(): + visible = False + if hasattr(w_helper.widgetClass, 'valid_parent_types'): + for valid_type in w_helper.widgetClass.valid_parent_types: + if widgetClass == valid_type: + visible = True + break + else: + visible = True + if visible: + visible_children_count += 1 + w_helper.show(visible) + + group.show(visible_children_count > 0) + class EditorAttributesGroup(gui.VBox): """ Contains a title and widgets. When the title is clicked, the contained widgets are hidden. @@ -716,6 +826,9 @@ def append(self, widget, key=''): def remove_child(self, widget): return self.container.remove_child(widget) + def show(self, value): + self.css_display = 'flex' if value else 'none' + class EditorAttributes(gui.VBox): """ Contains EditorAttributeInput each one of which notify a new value with an event diff --git a/editor/prototypes.py b/editor/prototypes.py index b2622dc8..5807d941 100644 --- a/editor/prototypes.py +++ b/editor/prototypes.py @@ -91,3 +91,5 @@ def updateView(self): proto_layout_append = "%(parentname)s.append(%(varname)s)\n " proto_set_listener = "%(sourcename)s.%(register_function)s.do(%(listenername)s.%(listener_function)s)\n " + +proto_link_FB_out_to = "%(process_name)s.children['%(fb_source)s'].outputs['%(outname)s'].link(%(process_name)s.children['%(fb_desitionation)s'].inputs['%(inputname)s'], %(process_name)s)\n " diff --git a/editor/widgets/FBD_library.py b/editor/widgets/FBD_library.py new file mode 100644 index 00000000..7d9e011d --- /dev/null +++ b/editor/widgets/FBD_library.py @@ -0,0 +1,347 @@ + +from . import FBD_view +from . import FBD_model +import remi +import remi.gui as gui +import time +import inspect +import types + + +class FBWrapper(FBD_view.FunctionBlockView): + @property + @gui.editor_attribute_decorator("WidgetSpecific",'''Variable name of the object to be wrapped''', str, {}) + def obj_variable_name(self): + return self._obj_variable_name + @obj_variable_name.setter + def obj_variable_name(self, value): + self.update_io_interface() + self._obj_variable_name = value + + @property + @gui.editor_attribute_decorator("WidgetSpecific",'''Method name of the object to be wrapped''', str, {}) + def method_name(self): + return self._method_name + @method_name.setter + def method_name(self, value): + self.update_io_interface() + self._method_name = value + + _obj_variable_name = None + _method_name = None + method_bound = None + + def __init__(self, *args, **kwargs): + FBD_view.FunctionBlockView.__init__(self, *args, **kwargs) + + @FBD_model.FunctionBlock.decorate_process([]) + def do(self, *args, **kwargs): + if self.method_bound == None: + self.update_io_interface() + if self.method_bound == None: + return + + #populate outputs + results = self.method_bound(*args, **kwargs) + results_count = 0 + if not results is None: + if type(results) in [tuple,]: + results_count = len(results) + else: + results_count = 1 + + if results_count != len(self.outputs): + for k in list(self.outputs.keys()): + self.remove_output_widget(k) + if results_count == 1: + self.add_io_widget(FBD_view.OutputView(f"out:{str(type(results))}")) + if results_count > 1: + i = 0 + for res in results: + self.add_io_widget(FBD_view.OutputView(f"out:{str(type(res))}{i}")) + i += 1 + for OUT in self.outputs.values(): + OUT.onmousedown.do(self.get_parent().onselection_start, js_stop_propagation=True, js_prevent_default=True) + OUT.onmouseup.do(self.get_parent().onselection_end, js_stop_propagation=True, js_prevent_default=True) + + return results + + def update_io_interface(self): + for k in list(self.inputs.keys()): + self.remove_input_widget(k) + for k in list(self.outputs.keys()): + self.remove_output_widget(k) + + if self._obj_variable_name == None: + return + if self._method_name == None: + return + app_instance = self.search_app_instance(self.get_parent()) + obj = self.select_instance(app_instance.root, self._obj_variable_name) + self.method_bound = getattr(obj, self._method_name) + signature = inspect.signature(self.method_bound) + for arg in signature.parameters: + self.add_io_widget(FBD_view.InputView(arg, default = signature.parameters[arg].default)) + + for IN in self.inputs.values(): + IN.onmousedown.do(self.get_parent().onselection_start, js_stop_propagation=True, js_prevent_default=True) + IN.onmouseup.do(self.get_parent().onselection_end, js_stop_propagation=True, js_prevent_default=True) + + def search_app_instance(self, node): + if issubclass(node.__class__, remi.server.App): + return node + if not hasattr(node, "get_parent"): + return None + return self.search_app_instance(node.get_parent()) + + def select_instance(self, node, variable_name): + if not hasattr(node, 'attributes'): + return None + + if node.variable_name == variable_name: + return node + + for item in node.children.values(): + res = self.select_instance(item, variable_name) + if not res is None: + return res + + +def FBWrapObjectMethod(obj_name, method_bound): + fb = FBD_view.FunctionBlockView() + #if hasattr(self.do, "_outputs"): + #for o in self.do._outputs: + # self.add_io_widget(OutputView(o)) + + signature = inspect.signature(method_bound) + for arg in signature.parameters: + fb.add_io_widget(FBD_view.InputView(arg, default = signature.parameters[arg].default)) + + def do(*args, **kwargs): + return method_bound(*args, **kwargs) + + def pre_do(*args, **kwargs): + #populate outputs + results = method_bound(*args, **kwargs) + for k in list(fb.outputs.keys()): + fb.remove_output_widget(k) + if not results is None: + if type(results) in [tuple,]: + i = 0 + for res in results: + fb.add_io_widget(FBD_view.OutputView(f"out{i}")) + i += 1 + else: + fb.add_io_widget(FBD_view.OutputView("out")) + fb.do = do + + fb.do = pre_do + return fb + + +class SUM(FBD_view.FunctionBlockView): + @FBD_model.FunctionBlock.decorate_process(['RESULT',]) + def do(self, IN1, IN2): + return IN1 + IN2 + +class MUL(FBD_view.FunctionBlockView): + @FBD_model.FunctionBlock.decorate_process(['RESULT',]) + def do(self, IN1, IN2): + return IN1 * IN2 + +class DIV(FBD_view.FunctionBlockView): + @FBD_model.FunctionBlock.decorate_process(['RESULT',]) + def do(self, IN1, IN2): + return IN1 / IN2 + +class DIFFERENCE(FBD_view.FunctionBlockView): + @FBD_model.FunctionBlock.decorate_process(['RESULT',]) + def do(self, IN1, IN2): + return IN1 - IN2 + +class COUNTER(FBD_view.FunctionBlockView): + @property + @gui.editor_attribute_decorator("WidgetSpecific",'''Defines the actual value''', int, {'possible_values': '', 'min': 0, 'max': 0xffffffff, 'default': 1, 'step': 1}) + def value(self): + if len(self.outputs) < 1: + return 0 + return self.outputs['OUT'].get_value() + @value.setter + def value(self, value): self.outputs['OUT'].set_value(value) + + def __init__(self, *args, **kwargs): + FBD_view.FunctionBlockView.__init__(self, *args, **kwargs) + self.outputs['OUT'].set_value(0) + + @FBD_model.FunctionBlock.decorate_process(['OUT']) + def do(self, reset=False): + if reset: + self.value = 0 + return 0 + self.value += 1 + return self.value + +class INT(FBD_view.FunctionBlockView): + @property + @gui.editor_attribute_decorator("WidgetSpecific",'''Defines the actual value''', int, {'possible_values': '', 'min': 0, 'max': 0xffffffff, 'default': 1, 'step': 1}) + def value(self): + if len(self.outputs) < 1: + return False + return self.outputs['OUT'].get_value() + @value.setter + def value(self, value): self.outputs['OUT'].set_value(value) + + def __init__(self, *args, **kwargs): + FBD_view.FunctionBlockView.__init__(self, *args, **kwargs) + self.outputs['OUT'].set_value(False) + + @FBD_model.FunctionBlock.decorate_process(['OUT']) + def do(self): + OUT = self.outputs['OUT'].get_value() + return OUT + +class GREATER_THAN(FBD_view.FunctionBlockView): + @FBD_model.FunctionBlock.decorate_process(['RESULT',]) + def do(self, IN1, IN2): + return IN1 > IN2 + +class LESS_THAN(FBD_view.FunctionBlockView): + @FBD_model.FunctionBlock.decorate_process(['RESULT',]) + def do(self, IN1, IN2): + return IN1 < IN2 + +class EQUAL_TO(FBD_view.FunctionBlockView): + @FBD_model.FunctionBlock.decorate_process(['RESULT',]) + def do(self, IN1, IN2): + return IN1 == IN2 + +class PRINT(FBD_view.FunctionBlockView): + @FBD_model.FunctionBlock.decorate_process([]) + def do(self, IN): + print(IN) + +class NONE(FBD_view.FunctionBlockView): + def __init__(self, *args, **kwargs): + FBD_view.FunctionBlockView.__init__(self, *args, **kwargs) + + @FBD_model.FunctionBlock.decorate_process(['OUT']) + def do(self): + return None + +class STRING(FBD_view.FunctionBlockView): + @property + @gui.editor_attribute_decorator("WidgetSpecific",'''Defines the actual value''', str, {}) + def value(self): + if len(self.outputs) < 1: + return "" + return self.outputs['OUT'].get_value() + @value.setter + def value(self, value): self.outputs['OUT'].set_value(value) + + def __init__(self, *args, **kwargs): + FBD_view.FunctionBlockView.__init__(self, *args, **kwargs) + self.outputs['OUT'].set_value("A STRING VALUE") + + @FBD_model.FunctionBlock.decorate_process(['OUT']) + def do(self): + OUT = self.outputs['OUT'].get_value() + return OUT + +class STRING_SWAP_CASE(FBD_view.FunctionBlockView): + @FBD_model.FunctionBlock.decorate_process(['OUT']) + def do(self, IN, EN = True): + if not EN: + return + OUT = IN.swapcase() + return OUT + +class BOOL(FBD_view.FunctionBlockView): + @property + @gui.editor_attribute_decorator("WidgetSpecific",'''Defines the actual value''', bool, {}) + def value(self): + if len(self.outputs) < 1: + return False + return self.outputs['OUT'].get_value() + @value.setter + def value(self, value): self.outputs['OUT'].set_value(value) + + def __init__(self, *args, **kwargs): + FBD_view.FunctionBlockView.__init__(self, *args, **kwargs) + self.outputs['OUT'].set_value(False) + + @FBD_model.FunctionBlock.decorate_process(['OUT']) + def do(self): + OUT = self.outputs['OUT'].get_value() + return OUT + +class RISING_EDGE(FBD_view.FunctionBlockView): + previous_value = None + + @FBD_model.FunctionBlock.decorate_process(['OUT']) + def do(self, IN): + OUT = (self.previous_value != IN) and IN + self.previous_value = IN + return OUT + +class FALLING_EDGE(FBD_view.FunctionBlockView): + previous_value = None + + @FBD_model.FunctionBlock.decorate_process(['OUT']) + def do(self, IN): + OUT = (self.previous_value != IN) and not IN + self.previous_value = IN + return OUT + +class NOT(FBD_view.FunctionBlockView): + @FBD_model.FunctionBlock.decorate_process(['OUT']) + def do(self, IN): + OUT = not IN + return OUT + +class AND(FBD_view.FunctionBlockView): + @FBD_model.FunctionBlock.decorate_process(['OUT']) + def do(self, IN1, IN2): + OUT = IN1 and IN2 + return OUT + +class OR(FBD_view.FunctionBlockView): + @FBD_model.FunctionBlock.decorate_process(['OUT']) + def do(self, IN1, IN2): + OUT = IN1 or IN2 + return OUT + +class XOR(FBD_view.FunctionBlockView): + @FBD_model.FunctionBlock.decorate_process(['OUT']) + def do(self, IN1, IN2): + OUT = IN1 != IN2 + return OUT + +class PULSAR(FBD_view.FunctionBlockView): + _ton = 1000 + _toff = 1000 + + @property + @gui.editor_attribute_decorator("WidgetSpecific",'''Defines the actual TON value''', int, {'possible_values': '', 'min': 0, 'max': 65535, 'default': 0, 'step': 1}) + def ton(self): + return self._ton + @ton.setter + def ton(self, value): self._ton = value + + @property + @gui.editor_attribute_decorator("WidgetSpecific",'''Defines the actual TOFF value''', int, {'possible_values': '', 'min': 0, 'max': 65535, 'default': 0, 'step': 1}) + def toff(self): + return self._toff + @toff.setter + def toff(self, value): self._toff = value + + tstart = 0 + def __init__(self, *args, **kwargs): + FBD_view.FunctionBlockView.__init__(self, *args, **kwargs) + self.outputs['OUT'].set_value(False) + self.tstart = time.time() + + @FBD_model.FunctionBlock.decorate_process(['OUT']) + def do(self): + OUT = (int((time.time() - self.tstart)*1000) % (self.ton + self.toff)) < self.ton + return OUT + diff --git a/editor/widgets/FBD_model.py b/editor/widgets/FBD_model.py new file mode 100644 index 00000000..7d277014 --- /dev/null +++ b/editor/widgets/FBD_model.py @@ -0,0 +1,301 @@ +import inspect + + +class Linkable(): + """ This class represents a Linkable object + an object that can be connected to 'linked_nodes_max_count' other elements + """ + name = None + linked_nodes = None + linked_nodes_max_count = 0 + + def __init__(self, name, linked_nodes_max_count = 1, *args, **kwargs): + """Args: + name (str): the object string identifier. + linked_nodes_max_count: the maximum number of nodes that can link to. + """ + + self.name = name + self.linked_nodes_max_count = linked_nodes_max_count + self.linked_nodes = [] + + def link(self, node): + """Creates a new link. + + Args: + node (Linkable): The node to link to. + """ + if not self.is_linked_to(node): + if self.linked_nodes_max_count > len(self.linked_nodes): + self.linked_nodes.append(node) + return True + + return False + + def is_linked(self): + """Check whether this object is linked to something. + + Returns: + result (bool): If the node is linked to something. + """ + return len(self.linked_nodes) > 0 + + def is_linked_to(self, node): + """Check whether this object is linked to the given node. + + Args: + node (Linkable): The node to be checked if linked to. + + Returns: + result (bool): If this node is linked to the given node. + """ + return node in self.linked_nodes + + def unlink(self, node = None): + """Removes a link to a specific node. + If the given node is null, removes all the links. + + Args: + node (Linkable): The node to be unlinked. None to remove all links. + """ + if node is None: + self.linked_nodes = [] + return + self.linked_nodes.remove(node) + + +class Input(Linkable): + """An input element of a FunctionBlock. + """ + default = None + typ = None + + def __init__(self, name, default = inspect.Parameter.empty, typ = None): + """Args: + name (str): The name of the input. + default (any): The default value of the input. + type (type): The input type. + """ + Linkable.__init__(self, name, linked_nodes_max_count=1) + self.default = default + self.typ = typ + + def get_value(self): + """Returns the input value. + The value is the default one if the Input is not linked, + or the value given from the link. + + Returns: + result (any): The input value. + """ + if not self.is_linked(): + return self.default + return self.linked_nodes[0].get_value() + + def has_default(self): + """Returns whether there is a default value. + + Returns: + result (bool): True if this input has a default value. + """ + return not (self.default == inspect.Parameter.empty) + + +class Output(Linkable): + """An output value of a FunctionBlock + """ + name = None + typ = None + value = None + + def __init__(self, name, typ = None): + """Args: + name (str): The name of the output. + type (type): The output type. + """ + Linkable.__init__(self, name, linked_nodes_max_count=0xff) + self.name = name + self.typ = typ + + def get_value(self): + """Returns the output value. + The value given from the FunctionBlock processing. + + Returns: + result (any): The output value. + """ + return self.value + + def set_value(self, value): + """Sets the output value. + + Args: + value (any): the value to be assigned. + """ + self.value = value + + +class FunctionBlock(): + """A FunctionBlock represents a processing unit with its own logic. + The function block must implement the 'do' method that executes the algorithm. + A function block has N inputs, that are the parameters of the 'do' method, + and M outputs that are the result values of the 'do' method. + A function block can have an enabling input to enable or disable the logic exection. + A function block gets executed among the others, in the order given by the execution_priority member. + """ + inputs = None + outputs = None + + execution_priority = 0 + + has_enabling_input = False #This property gets overloaded in FBD_view.FunctionBlockView + + def decorate_process(output_list): + """ setup a method as a process FunctionBlock """ + """ + input parameters can be obtained by introspection + outputs values (return values) are to be described with decorator + """ + def add_annotation(method): + setattr(method, "_outputs", output_list) + return method + return add_annotation + + def __init__(self, execution_priority = 0): + """Args: + execution_priority (int): the order number. + """ + self.set_execution_priority(execution_priority) + self.inputs = {} + self.outputs = {} + + def set_execution_priority(self, execution_priority): + """Sets the execution order. + + Args: + execution_priority (int): The priority value. + """ + self.execution_priority = execution_priority + + def add_io(self, io): + """Adds an Input or Output to the function block. + + Args: + io (Input/Output): The io instance. + """ + if issubclass(type(io), Input): + self.inputs[io.name] = io + else: + self.outputs[io.name] = io + + def add_enabling_input(self): + """Adds an enabling input. + """ + self.add_io(Input('EN', False)) + + def remove_enabling_input(self): + """Removes the enabling input and the eventual link. + """ + self.inputs['EN'].unlink() + del self.inputs['EN'] + + @decorate_process([]) + def do(self): + """Implements the FunctionBlock logic. + Inputs are passed to this function as parameters by the Process. + Results are assigned to the Outputs by the Process. + + Returns: + result (any): The result value. + """ + return None + + +class Link(): + """Represents the link between two Linkable nodes. + """ + source = None + destination = None + def __init__(self, source_widget, destination_widget): + """Args: + source_widget (Linkable): a node participating to the link. + destination_widget (Linkable): a node participating to the link. + """ + self.source = source_widget + self.destination = destination_widget + + def unlink(self): + """Deletes the link. + """ + self.source.unlink(self.destination) + self.destination.unlink() + + +class Process(): + """A Process is a collection of FunctionBlocks in which I/O are linked. + FunctionBlocks in a Process gets executed sequentially in the order given by the FunctionBlock priority. + The priority takes places by the order of function_blocks in the dictionary. + Such order can be adjusted by reordering elements in the dictionary. + """ + function_blocks = None + + def __init__(self): + self.function_blocks = {} + + def add_function_block(self, function_block): + """Adds a function block to the process. + + Args: + function_block (FunctionBlock): the function block to be added. + """ + self.function_blocks[function_block.identifier] = function_block + + def remove_function_block(self, function_block): + """Removes a function block from the process. + + Args: + function_block (FunctionBlock): the function block to be removed. + """ + del self.function_blocks[function_block.identifier] + + def do(self): + """Executed the FunctionBlocks. + Before to call a function block, all its Inputs are gethered by the linked outputs. + If the input is not linked, the corresponding parameter takes the default value, whether available. + The function block gets executed if all the parameters are available. + Once the function block is executed, the results are assigned to its Outputs. + """ + execution_priority = 0 + for function_block in self.function_blocks.values(): + parameters = {} + all_inputs_connected = True + + function_block.set_execution_priority(execution_priority) + execution_priority += 1 + + for IN in function_block.inputs.values(): + if (not IN.is_linked()) and (not IN.has_default()): + all_inputs_connected = False + continue + parameters[IN.name] = IN.get_value() + + if not all_inputs_connected: + continue + + if function_block.has_enabling_input: + if not parameters['EN']: + continue + else: + del parameters['EN'] + + output_results = function_block.do(**parameters) + if output_results is None: + continue + i = 0 + for OUT in function_block.outputs.values(): + if type(output_results) in (tuple, list): + OUT.set_value(output_results[i]) + else: + OUT.set_value(output_results) + i += 1 + diff --git a/editor/FBD_view.py b/editor/widgets/FBD_view.py similarity index 52% rename from editor/FBD_view.py rename to editor/widgets/FBD_view.py index 34202307..cfdd3639 100644 --- a/editor/FBD_view.py +++ b/editor/widgets/FBD_view.py @@ -19,8 +19,10 @@ import inspect import time from editor_widgets import * -import FBD_model +from . import FBD_model import types +import threading +import widgets.toolbox_opencv class MixinPositionSize(): def get_position(self): @@ -30,31 +32,160 @@ def get_size(self): return float(self.attr_width), float(self.attr_height) +class NavigableArea(gui.Svg): + zoom = 1.0 + dragging_active = False + dragging_start_x = 0 + dragging_start_y = 0 + + view_width = 0 + view_height = 0 + def __init__(self, view_width, view_height, *args, **kwargs): + gui.Svg.__init__(self, width=view_width, height=view_height, *args, **kwargs) + self.view_width = view_width + self.view_height = view_height + self.set_viewbox(0, 0, self.view_width, self.view_height) + self.attr_preserveAspectRatio = "XMidYMid meet" + self.onmousedown.do(None) + self.onmouseup.do(None) + self.onmousemove.do(None) + + @gui.decorate_event_js("var params={};" \ + "var boundingBox = this.getBoundingClientRect();" \ + "params['x']=event.clientX-boundingBox.left;" \ + "params['y']=event.clientY-boundingBox.top;" \ + "params['buttons']=event.buttons;" \ + "remi.sendCallbackParam('%(emitter_identifier)s','%(event_name)s',params);") + def onmousedown(self, x, y, buttons): + """Called when the user presses left or right mouse button over a Widget. + + Args: + x (float): position of the mouse inside the widget + y (float): position of the mouse inside the widget + buttons (int): the mouse button code + + 0: No button or un-initialized + 1: Primary button (usually the left button) + 2: Secondary button (usually the right button) + 4: Auxiliary button (usually the mouse wheel button or middle button) + 8: 4th button (typically the "Browser Back" button) + 16 : 5th button (typically the "Browser Forward" button) + """ + buttons = int(buttons) + if buttons & 4: + self.dragging_active = True + self.dragging_start_x = float(x) + self.dragging_start_y = float(y) + return (x, y) + + @gui.decorate_event_js("var params={};" \ + "var boundingBox = this.getBoundingClientRect();" \ + "params['x']=event.clientX-boundingBox.left;" \ + "params['y']=event.clientY-boundingBox.top;" \ + "remi.sendCallbackParam('%(emitter_identifier)s','%(event_name)s',params);") + def onmouseup(self, x, y): + """Called when the user releases left or right mouse button over a Widget. + + Args: + x (float): position of the mouse inside the widget + y (float): position of the mouse inside the widget + """ + self.dragging_active = False + return (x, y) + + @gui.decorate_event_js("var params={};" \ + "var boundingBox = this.getBoundingClientRect();" \ + "params['x']=event.clientX-boundingBox.left;" \ + "params['y']=event.clientY-boundingBox.top;" \ + "params['buttons']=event.buttons;" \ + "remi.sendCallbackParam('%(emitter_identifier)s','%(event_name)s',params);") + def onmousemove(self, x, y, buttons): + """Called when the mouse cursor moves inside the Widget. + + Args: + x (float): position of the mouse inside the widget + y (float): position of the mouse inside the widget + buttons (int): the mouse button code + + 0: No button or un-initialized + 1: Primary button (usually the left button) + 2: Secondary button (usually the right button) + 4: Auxiliary button (usually the mouse wheel button or middle button) + 8: 4th button (typically the "Browser Back" button) + 16 : 5th button (typically the "Browser Forward" button) + """ + if self.dragging_active: + vx, vy, vw, vh = self.get_actual_viewbox_values() + self.set_viewbox(vx + self.dragging_start_x - float(x), vy + self.dragging_start_y - float(y),vw, vh) + self.dragging_start_x = float(x) + self.dragging_start_y = float(y) + self.attr_preserveAspectRatio = "XMidYMid meet" + return (x, y) + + @gui.decorate_event_js("var params={};" \ + "params['value']=event.deltaY;" \ + "remi.sendCallbackParam('%(emitter_identifier)s','%(event_name)s',params);") + def onwheel(self, value): + """Called when the user rotate the mouse wheel. + + Args: + value (float): the scroll amount + """ + self.zoom += float(value)*0.0005 + self.zoom = max(0.25, self.zoom) + self.zoom = min(4.0, self.zoom) + vx, vy, vw, vh = self.get_actual_viewbox_values() + self.set_viewbox(vx, vy, self.view_width*self.zoom, self.view_height*self.zoom) + self.attr_preserveAspectRatio = "XMidYMid meet" + #self.style['zoom'] = str(self.zoom) + return (value,) + + def get_actual_viewbox_values(self): + """Returns floating point values of the actual viewbox. + + Returns: + x, y, w, h (float): values + """ + viewbox = [float(v) for v in self.attr_viewBox.split(' ')] + return (*viewbox,) + + class MoveableWidget(gui.EventSource, MixinPositionSize): container = None - def __init__(self, container, *args, **kwargs): + x_start = 0 + y_start = 0 + start_pos_acquired = False + def __init__(self, *args, **kwargs): gui.EventSource.__init__(self) - self.container = container self.active = False self.onmousedown.do(self.start_drag, js_stop_propagation=True, js_prevent_default=True) - + def start_drag(self, emitter, x, y): self.active = True + self.container = self.get_parent() self.container.onmousemove.do(self.on_drag, js_stop_propagation=True, js_prevent_default=True) self.onmousemove.do(None, js_stop_propagation=False, js_prevent_default=True) self.container.onmouseup.do(self.stop_drag) self.container.onmouseleave.do(self.stop_drag, 0, 0) + self.start_pos_acquired = False @gui.decorate_event def stop_drag(self, emitter, x, y): self.active = False - return (x, y) + return (float(x), float(y)) @gui.decorate_event def on_drag(self, emitter, x, y): + if not self.start_pos_acquired: + self_x, self_y = self.get_position() + self.x_start = (float(x) - self_x) + self.y_start = (float(y) - self_y) + self.start_pos_acquired = True + if self.active: - self.set_position(float(x) - float(self.attr_width)/2.0, float(y) - float(self.attr_height)/2.0) - return (x, y) + #self.set_position(float(x) - float(self.attr_width)/2.0, float(y) - float(self.attr_height)/2.0) + self.set_position((float(x) - self.x_start), (float(y) - self.y_start)) + return (float(x), float(y)) class SvgTitle(gui.Widget, gui._MixinTextualWidget): @@ -85,7 +216,7 @@ def set_default_look(self): self.placeholder.set_fill("lightgray") self.placeholder.style['cursor'] = 'pointer' - self.label.attr_dominant_baseline = 'middle' + self.label.attr_dominant_baseline = 'central' self.label.attr_text_anchor = "start" self.label.style['cursor'] = 'pointer' self.label.set_fill('black') @@ -102,12 +233,17 @@ def get_value(self): v = FBD_model.Input.get_value(self) if self.is_linked() or self.has_default(): - if self.previous_value != v: - if type(v) == bool: - self.label.set_fill('white') - self.placeholder.set_fill('blue' if v else 'BLACK') - self.append(SvgTitle(str(v)), "title") - self.previous_value = v + try: + if self.previous_value == v: + return v + except: + pass + + if type(v) == bool: + self.label.set_fill('white') + self.placeholder.set_fill('blue' if v else 'BLACK') + self.append(SvgTitle(str(v)), "title") + self.previous_value = v return v @@ -135,7 +271,7 @@ def __init__(self, name, *args, **kwargs): self.append(self.placeholder) self.label = gui.SvgText("100%", "50%", name) - self.label.attr_dominant_baseline = 'middle' + self.label.attr_dominant_baseline = 'central' self.label.attr_text_anchor = "end" self.label.style['cursor'] = 'pointer' self.append(self.label) @@ -145,7 +281,7 @@ def __init__(self, name, *args, **kwargs): def link(self, destination, container): link_view = LinkView(self, destination, container) container.append(link_view) - bt_unlink = Unlink() + bt_unlink = DeleteButton() container.append(bt_unlink) link_view.set_unlink_button(bt_unlink) FBD_model.Output.link(self, destination) @@ -162,8 +298,11 @@ def set_size(self, width, height): return gui._MixinSvgSize.set_size(self, width, height) def set_value(self, value): - if value == self.value: - return + try: + if value == self.value: + return + except: + pass if type(value) == bool: self.label.set_fill('white') self.placeholder.set_fill('blue' if value else 'BLACK') @@ -176,95 +315,24 @@ def set_value(self, value): @gui.decorate_event def onpositionchanged(self): - for destination in self.destinations: - destination.link_view.update_path() + for node in self.linked_nodes: + node.link_view.update_path() return () -class InputEvent(InputView): - placeholder = None - label = None - event_callback = None - def __init__(self, name, event_callback, *args, **kwargs): - self.event_callback = event_callback - gui.SvgSubcontainer.__init__(self, 0, 0, 0, 0, *args, **kwargs) - - self.placeholder = gui.SvgRectangle(0, 0, 0, 0) - self.append(self.placeholder) - - self.label = gui.SvgText("0%", "50%", name) - self.append(self.label) - - FBD_model.Input.__init__(self, name, *args, **kwargs) - self.set_default_look() - - def set_default_look(self): - self.placeholder.set_stroke(1, 'black') - self.placeholder.set_fill("orange") - self.placeholder.style['cursor'] = 'pointer' - - self.label.attr_dominant_baseline = 'middle' - self.label.attr_text_anchor = "start" - self.label.style['cursor'] = 'pointer' - - def link(self, source, link_view): - if not issubclass(type(source), OutputEvent): - return - self.placeholder.set_fill('green') - InputView.link(self, source, link_view) - - def unlink(self, destination = None): - self.placeholder.set_fill('orange') - InputView.unlink(self) - - -class OutputEvent(OutputView): - placeholder = None - label = None - event_connector = None - def __init__(self, name, event_connector, *args, **kwargs): - self.event_connector = event_connector - gui.SvgSubcontainer.__init__(self, 0, 0, 0, 0, *args, **kwargs) - self.placeholder = gui.SvgRectangle(0, 0, 0, 0) - self.placeholder.set_stroke(1, 'black') - self.placeholder.set_fill("orange") - self.placeholder.style['cursor'] = 'pointer' - self.append(self.placeholder) - - self.label = gui.SvgText("100%", "50%", name) - self.label.attr_dominant_baseline = 'middle' - self.label.attr_text_anchor = "end" - self.label.style['cursor'] = 'pointer' - self.append(self.label) - - FBD_model.Output.__init__(self, name, *args, **kwargs) - - def link(self, destination, container): - if not issubclass(type(destination), InputEvent): - return - self.placeholder.set_fill('green') - gui.ClassEventConnector.do(self.event_connector, destination.event_callback) - OutputView.link(self, destination, container) - - def unlink(self, destination = None): - self.placeholder.set_fill('orange') - gui.ClassEventConnector.do(self.event_connector, None) - FBD_model.Output.unlink(self, destination) - - -class Unlink(gui.SvgSubcontainer): - def __init__(self, x=0, y=0, w=15, h=15, *args, **kwargs): +class DeleteButton(gui.SvgSubcontainer): + def __init__(self, x=0, y=0, w=12, h=12, *args, **kwargs): gui.SvgSubcontainer.__init__(self, x, y, w, h, *args, **kwargs) self.outline = gui.SvgRectangle(0, 0, "100%", "100%") - self.outline.set_fill('white') - self.outline.set_stroke(1, 'black') + self.outline.set_fill('red') + self.outline.set_stroke(1, 'darkred') self.append(self.outline) - line = gui.SvgLine(0,0,"100%","100%") - line.set_stroke(2, 'red') + line = gui.SvgLine("20%","20%","80%","80%") + line.set_stroke(2, 'white') self.append(line) - line = gui.SvgLine("100%", 0, 0, "100%") - line.set_stroke(2, 'red') + line = gui.SvgLine("80%", "20%", "20%", "80%") + line.set_stroke(2, 'white') self.append(line) def get_size(self): @@ -280,7 +348,7 @@ def __init__(self, source_widget, destination_widget, container, *args, **kwargs self.container = container gui.SvgPolyline.__init__(self, 2, *args, **kwargs) FBD_model.Link.__init__(self, source_widget, destination_widget) - self.set_stroke(1, 'black') + self.set_stroke(1, 'rgba(0,0,0,0.5)') self.set_fill('transparent') self.attributes['stroke-dasharray'] = "4 2" @@ -296,7 +364,7 @@ def set_unlink_button(self, bt_unlink): self.bt_unlink.onclick.do(self.unlink) self.update_path() - def unlink(self, emitter): + def unlink(self, emitter = None): self.get_parent().remove_child(self.bt_unlink) self.get_parent().remove_child(self) FBD_model.Link.unlink(self) @@ -310,6 +378,9 @@ def get_absolute_node_position(self, node): xs, ys = self.get_absolute_node_position(np) return x+xs, y+ys + def highlight(self, enb): + self.set_stroke(1, 'rgba(0,0,0,1.0)' if enb else 'rgba(0,0,0,0.5)') + def update_path(self, emitter=None): self.attributes['points'] = '' @@ -373,201 +444,292 @@ def update_path(self, emitter=None): self.bt_unlink.set_position(xdestination - offset / 2.0 - w/2, ydestination -h/2) -class ObjectBlockView(FBD_model.ObjectBlock, gui.SvgSubcontainer, MoveableWidget): +class ProcessView(NavigableArea, FBD_model.Process): - label = None - label_font_size = 12 + valid_parent_types = [gui.Container, gui.HBox, gui.VBox] - outline = None + @property + @gui.editor_attribute_decorator("WidgetSpecific",'''Defines the viewved name''', str, {}) + def variable_name(self): + if self.label == None: + return "" + return self.label.get_text() + @variable_name.setter + def variable_name(self, value): self.label.set_text(value) - reference_object = None + selected_input = None + selected_output = None - io_font_size = 12 - io_left_right_offset = 10 + selected_function_block = None - def __init__(self, obj, container, x = 10, y = 10, *args, **kwargs): - name = obj.__class__.__name__ - self.reference_object = obj - FBD_model.ObjectBlock.__init__(self, name) - gui.SvgSubcontainer.__init__(self, x, y, self.calc_width(), self.calc_height(), *args, **kwargs) - MoveableWidget.__init__(self, container, *args, **kwargs) + label = None + label_font_size = 18 - self.outline = gui.SvgRectangle(0, 0, "100%", "100%") - self.outline.set_fill('white') - self.outline.set_stroke(2, 'orange') - self.append(self.outline) + def __init__(self, name = "Process", *args, **kwargs): + NavigableArea.__init__(self, 200, 200, *args, **kwargs) + FBD_model.Process.__init__(self) + + self.css_position = 'absolute' + self.css_border_color = 'black' + self.css_border_width = '0px' + self.css_border_style = 'solid' + #self.style['background-color'] = 'lightyellow' + self.css_background_color = 'rgb(250,248,240)' + self.css_background_image = "url('/editor_resources:background.png')" - self.label = gui.SvgText("50%", 0, self.name) + self.label = gui.SvgText("50%", "50%", name) self.label.attr_text_anchor = "middle" - self.label.attr_dominant_baseline = 'hanging' + self.label.attr_dominant_baseline = 'middle' + #self.label.set_stroke(1, "gray") + self.label.set_fill("darkgray") + self.label.style['pointer-events'] = 'none' self.label.css_font_size = gui.to_pix(self.label_font_size) self.append(self.label) - self.onselection_start = self.container.onselection_start - self.onselection_end = self.container.onselection_end - - """ - for (method_name, method) in inspect.getmembers(self.reference_object, inspect.ismethod): - #try: - #c = types.new_class(method_name, (FunctionBlockView,)) - #setattr(c, "do", types.MethodType(getattr(self.reference_object, method_name), c)) - #c.do.__dict__['_outputs'] = [] - #FBD_model.FunctionBlock.decorate_process(['OUT'])(c.do) - #self.add_fb_view(c(method_name, container)) - self.add_fb_view(ObjectFunctionBlockView(self.reference_object, method, method_name, method_name, self)) - #except: - # pass - - for (class_name, _class) in inspect.getmembers(self.reference_object): - evt = getattr(self.reference_object, class_name) - if issubclass(type(_class), gui.ClassEventConnector): - #self.append(ObjectBlockView(evt, self)) - self.add_fb_view(ObjectFunctionBlockView(evt, evt, "do", evt.event_method_bound.__name__ + ".do", self)) - """ - self.stop_drag.do(lambda emitter, x, y:self.adjust_geometry()) - - def calc_height(self): - xmax = ymax = 0 - if not self.FBs is None: - for fb in self.FBs.values(): - x, y = fb.get_position() - w, h = fb.get_size() - xmax = max(xmax, x+w) - ymax = max(ymax, y+h) - - return self.label_font_size + ymax - - def calc_width(self): - xmax = ymax = 0 - if not self.FBs is None: - for fb in self.FBs.values(): - x, y = fb.get_position() - w, h = fb.get_size() - xmax = max(xmax, x+w) - ymax = max(ymax, y+h) - - return max((len(self.name) * self.label_font_size), xmax) - - def add_fb_view(self, fb_view_instance): - self.FBs[fb_view_instance.name] = fb_view_instance - - self.append(fb_view_instance) - for fb in self.FBs.values(): - fb.adjust_geometry() - fb.on_drag.do(self.onfunction_block_position_changed) - self.adjust_geometry() - - def add_io_widget(self, widget): - widget.label.css_font_size = gui.to_pix(self.io_font_size) - widget.set_size(len(widget.name) * self.io_font_size, self.io_font_size) - - FBD_model.FunctionBlock.add_io(self, widget) - self.append(widget) - widget.onmousedown.do(self.container.onselection_start, js_stop_propagation=True, js_prevent_default=True) - widget.onmouseup.do(self.container.onselection_end, js_stop_propagation=True, js_prevent_default=True) - - self.adjust_geometry() - - def onfunction_block_position_changed(self, emitter, x, y): - emitter.adjust_geometry() - self.adjust_geometry() - - def adjust_geometry(self): - w, h = self.get_size() + self.onwheel.do(None) - i = 1 - for inp in self.inputs.values(): - inp.set_position(0, self.label_font_size + self.io_font_size*i) - inp.onpositionchanged() - i += 1 + def do(self, *args, **kwargs): + return FBD_model.Process.do(self) + + def on_execution_priority_changed(self, emitter, function_block, value): + del self.function_blocks[function_block.identifier] + d = dict(self.function_blocks) + self.function_blocks = {} + for k,v in d.items(): + if len(self.function_blocks.keys()) == value: + self.function_blocks[function_block.identifier] = function_block + self.function_blocks[k] = v + if not function_block.identifier in self.function_blocks.keys(): + self.function_blocks[function_block.identifier] = function_block - i = 1 - for o in self.outputs.values(): - ow, oh = o.get_size() - o.set_position(w - ow, self.label_font_size + self.io_font_size*i) - o.onpositionchanged() - i += 1 + def onselection_start(self, emitter, x, y): + self.selected_input = self.selected_output = None + print("selection start: ", type(emitter)) + if issubclass(type(emitter), FBD_model.Input): + self.selected_input = emitter + else: + self.selected_output = emitter - gui._MixinSvgSize.set_size(self, self.calc_width(), self.calc_height()) + def onselection_end(self, emitter, x, y): + print("selection end: ", type(emitter)) + if issubclass(type(emitter), FBD_model.Input): + self.selected_input = emitter + else: + self.selected_output = emitter - def set_position(self, x, y): - for fb in self.FBs.values(): - fb.onposition_changed() + if self.selected_input != None and self.selected_output != None: + if self.selected_input.is_linked(): + return + self.selected_output.link(self.selected_input, self) - for inp in self.inputs.values(): - inp.onpositionchanged() + def append(self, value, key=''): + if issubclass(type(value), FunctionBlockView): + self.add_function_block(value) + else: + gui.Svg.append(self, value, key) - for o in self.outputs.values(): - o.onpositionchanged() - #w, h = self.get_size() - #self.attr_viewBox = "%s %s %s %s"%(x, y, x+w, y+h) - return gui.SvgSubcontainer.set_position(self, x, y) + def add_function_block(self, function_block): + function_block.onclick.do(self.onfunction_block_clicked, js_stop_propagation=True, js_prevent_default=True) + function_block.on_execution_priority_changed.do(self.on_execution_priority_changed) + function_block.on_delete_button_pressed.do(self.remove_function_block) + gui.Svg.append(self, function_block, function_block.variable_name) + FBD_model.Process.add_function_block(self, function_block) + function_block.adjust_geometry() + for IN in function_block.inputs.values(): + IN.onmousedown.do(self.onselection_start, js_stop_propagation=True, js_prevent_default=True) + IN.onmouseup.do(self.onselection_end, js_stop_propagation=True, js_prevent_default=True) + + for OUT in function_block.outputs.values(): + OUT.onmousedown.do(self.onselection_start, js_stop_propagation=True, js_prevent_default=True) + OUT.onmouseup.do(self.onselection_end, js_stop_propagation=True, js_prevent_default=True) + + def remove_function_block(self, emitter): + if issubclass(type(emitter), FBD_model.FunctionBlock): + self.remove_child(emitter) + for IN in emitter.inputs.values(): + if IN.is_linked(): + IN.link_view.unlink() + + for OUT in emitter.outputs.values(): + if OUT.is_linked(): + for IN in list(OUT.linked_nodes): + IN.link_view.unlink() + FBD_model.Process.remove_function_block(self, emitter) - def set_name(self, name): - self.name = name - self.label.set_text(name) - self.adjust_geometry() + @gui.decorate_event + def onfunction_block_clicked(self, function_block): + if not self.selected_function_block is None: + self.selected_function_block.label.css_font_weight = "normal" + for IN in self.selected_function_block.inputs.values(): + if IN.is_linked(): + IN.link_view.highlight(False) + + self.selected_function_block = function_block + self.selected_function_block.label.css_font_weight = "bolder" + for IN in self.selected_function_block.inputs.values(): + if IN.is_linked(): + IN.link_view.highlight(True) + return (function_block,) -class TextInputAdapter(ObjectBlockView): - def __init__(self, obj, container, x = 10, y = 10, *args, **kwargs): - ObjectBlockView.__init__(self, obj, container, x = 10, y = 10, *args, **kwargs) +class ProcessViewThreaded(ProcessView): + @property + @gui.editor_attribute_decorator("WidgetSpecific",'''Defines the sleep time in seconds''', float, {'possible_values': '', 'min': 0, 'max': 65535, 'default': 0, 'step': 1}) + def sleep_time(self): + return self._sleep_time + @sleep_time.setter + def sleep_time(self, value): self._sleep_time = value + + @property + @gui.editor_attribute_decorator("WidgetSpecific",'''Stop''', bool, {}) + def stop(self): + return self._stop + @stop.setter + def stop(self, value): + self._stop = value + if not self._stop and self._stopped: + self.start_thread() + + _sleep_time = 0.1 + _stop = False + _stopped = True + thread = None + + def __init__(self, name = "Process", *args, **kwargs): + ProcessView.__init__(self, name, *args, **kwargs) + self.start_thread() + + def start_thread(self): + self._stop = False + self.thread = threading.Thread(target=self.threaded_function) + self.thread.start() - txt = gui.TextInput() - ofbv = ObjectFunctionBlockView(self.reference_object, txt.get_value, "get_value", "get_value", self) - ofbv.add_io_widget(OutputView("Value")) - self.add_fb_view(ofbv) - - ofbv = ObjectFunctionBlockView(self.reference_object, txt.set_value, "set_value", "set_value", self) - self.add_fb_view(ofbv) - - ie = InputEvent("onclicked", self.callback_test) - self.add_io_widget(ie) - - oe = OutputEvent("onclick", self.onclick) - self.add_io_widget(oe) - - def callback_test(self, emitter): - self.outline.set_stroke(2, 'red') + def search_app_instance(self, node): + if issubclass(node.__class__, remi.server.App): + return node + if not hasattr(node, "get_parent"): + return None + return self.search_app_instance(node.get_parent()) + + def threaded_function(self): + self._stopped = False + app_instance = self.search_app_instance(self) + while not self._stop: + time.sleep(self._sleep_time) + if app_instance is None: + app_instance = self.search_app_instance(self) + continue + + with app_instance.update_lock: + self.do() + self._stopped = True class FunctionBlockView(FBD_model.FunctionBlock, gui.SvgSubcontainer, MoveableWidget): + valid_parent_types = [ProcessView, ProcessViewThreaded] + + @property + @gui.editor_attribute_decorator("WidgetSpecific",'''Defines the viewved name''', str, {}) + def variable_name(self): + if self.label == None: + return "" + return self.label.get_text() + @variable_name.setter + def variable_name(self, value): self.label.set_text(value) + + @property + @gui.editor_attribute_decorator("WidgetSpecific",'''Defines if the function block has to be enabled''', bool, {}) + def has_enabling_input(self): return 'EN' in self.inputs.keys() + @has_enabling_input.setter + def has_enabling_input(self, value): + if value: + self.add_enabling_input_widget() + else: + self.remove_enabling_input_widget() + label = None + label_priority = None label_font_size = 12 outline = None + delete_button = None + io_font_size = 12 io_left_right_offset = 10 - input_event = None + execution_priority = 0 + bt_increase_priority = None + bt_decrease_priority = None - def __init__(self, name, container, x = 10, y = 10, *args, **kwargs): - FBD_model.FunctionBlock.__init__(self, name) + def __init__(self, name = "FunctionBlock", x = 10, y = 10, execution_priority = 0, *args, **kwargs): + FBD_model.FunctionBlock.__init__(self, execution_priority) gui.SvgSubcontainer.__init__(self, x, y, self.calc_width(), self.calc_height(), *args, **kwargs) - MoveableWidget.__init__(self, container, *args, **kwargs) + MoveableWidget.__init__(self, *args, **kwargs) self.outline = gui.SvgRectangle(0, 0, "100%", "100%") self.outline.set_fill('white') self.outline.set_stroke(2, 'black') self.append(self.outline) - self.input_event = InputEvent(self.name, self.do) - self.input_event.label.attr_text_anchor = "middle" - #self.input_event.label.attr_dominant_baseline = 'hanging' - self.input_event.label.css_font_size = gui.to_pix(self.io_font_size) - self.input_event.label.attr_x = "50%" - self.input_event.label.attr_y = "50%" - self.input_event.set_size(len(self.input_event.name) * self.io_font_size, self.io_font_size) - self.input_event.onmousedown.do(self.container.onselection_start, js_stop_propagation=True, js_prevent_default=True) - self.input_event.onmouseup.do(self.container.onselection_end, js_stop_propagation=True, js_prevent_default=True) - self.append(self.input_event) + self.label = gui.SvgText("50%", 0, name) + self.label.attr_text_anchor = "middle" + self.label.attr_dominant_baseline = 'text-before-edge' + self.label.style['pointer-events'] = 'none' + self.label.css_font_size = gui.to_pix(self.label_font_size) + self.append(self.label) + + self.label_priority = gui.SvgText("50%", self.label_font_size, str(self.execution_priority)) + self.label_priority.attr_text_anchor = "middle" + self.label_priority.attr_dominant_baseline = 'text-before-edge' + self.label_priority.style['pointer-events'] = 'none' + self.label_priority.set_fill("gray") + self.label_priority.css_font_size = gui.to_pix(self.label_font_size) + self.append(self.label_priority) + + self.delete_button = DeleteButton() + self.delete_button.onclick.do(self.on_delete_button_pressed, js_stop_propagation=True, js_prevent_default=True) + self.append(self.delete_button) + + self.bt_increase_priority = gui.SvgText("0%", self.label_font_size, u"▲") + self.bt_increase_priority.attr_text_anchor = "start" + self.bt_increase_priority.attr_dominant_baseline = 'text-before-edge' + self.bt_increase_priority.set_fill("gray") + self.bt_increase_priority.css_font_size = gui.to_pix(self.label_font_size) + self.bt_increase_priority.style["cursor"] = "pointer" + self.bt_increase_priority.onclick.do(self.on_execution_priority_changed, -1, js_stop_propagation=False, js_prevent_default=True) + self.bt_increase_priority.attr_title = "Increase priority" + self.append(self.bt_increase_priority) + + self.bt_decrease_priority = gui.SvgText("100%", self.label_font_size, u"▼") + self.bt_decrease_priority.attr_text_anchor = "end" + self.bt_decrease_priority.attr_dominant_baseline = 'text-before-edge' + self.bt_decrease_priority.set_fill("gray") + self.bt_decrease_priority.css_font_size = gui.to_pix(self.label_font_size) + self.bt_decrease_priority.style["cursor"] = "pointer" + self.bt_decrease_priority.onclick.do(self.on_execution_priority_changed, +1, js_stop_propagation=False, js_prevent_default=True) + self.bt_decrease_priority.attr_title = "Decrease priority" + self.append(self.bt_decrease_priority) self.populate_io() self.stop_drag.do(lambda emitter, x, y:self.adjust_geometry()) + @gui.decorate_event + def on_execution_priority_changed(self, emitter, direction): + actual_priority = int(self.label_priority.get_text()) + return (self, actual_priority + direction) + + @gui.decorate_event + def on_delete_button_pressed(self, emitter): + return () + + def set_execution_priority(self, execution_priority): + if not self.label_priority is None: + self.label_priority.set_text(str(self.execution_priority)) + FBD_model.FunctionBlock.set_execution_priority(self, execution_priority) + def populate_io(self): #for all the outputs defined by decorator on FunctionBlock.do # add the related Outputs @@ -595,19 +757,47 @@ def calc_width(self): for o in self.outputs.values(): max_name_len_output = max(max_name_len_output, len(o.name)) - return max((len(self.name) * self.label_font_size), (max(max_name_len_input, max_name_len_output)*self.io_font_size) * 2) + self.io_left_right_offset + return max((len(self.variable_name) * self.label_font_size*0.6), (max(max_name_len_input, max_name_len_output)*self.io_font_size*0.6) * 2) + self.io_left_right_offset def add_io_widget(self, widget): widget.label.css_font_size = gui.to_pix(self.io_font_size) - widget.set_size(len(widget.name) * self.io_font_size, self.io_font_size) + widget.set_size(len(widget.name) * self.io_font_size*0.6, self.io_font_size) FBD_model.FunctionBlock.add_io(self, widget) self.append(widget) - widget.onmousedown.do(self.container.onselection_start, js_stop_propagation=True, js_prevent_default=True) - widget.onmouseup.do(self.container.onselection_end, js_stop_propagation=True, js_prevent_default=True) self.adjust_geometry() + def remove_input_widget(self, name): + io = self.inputs[name] + if io.is_linked(): + io.link_view.unlink() + self.remove_child(io) + del self.inputs[name] + + def remove_output_widget(self, name): + io = self.outputs[name] + if io.is_linked(): + for dest in io.linked_nodes: + if dest.name == name: + dest.link_view.unlink() + break + self.remove_child(io) + del self.outputs[name] + + def add_enabling_input_widget(self): + self.add_io_widget(InputView('EN', default = False)) + #get_parent is used to get the processview instance + parent = self.get_parent() + if parent is None: + return + self.inputs['EN'].onmousedown.do(parent.onselection_start, js_stop_propagation=True, js_prevent_default=True) + self.inputs['EN'].onmouseup.do(parent.onselection_end, js_stop_propagation=True, js_prevent_default=True) + + def remove_enabling_input_widget(self): + if 'EN' in self.inputs.keys(): + self.remove_input_widget('EN') + def onposition_changed(self): for inp in self.inputs.values(): inp.onpositionchanged() @@ -619,20 +809,17 @@ def adjust_geometry(self): gui._MixinSvgSize.set_size(self, self.calc_width(), self.calc_height()) w, h = self.get_size() - if not self.input_event is None: - iew, ieh = self.input_event.get_size() - self.input_event.set_position((w-iew)/2, 0) - - i = 1 + margin = 1 + i = 2 for inp in self.inputs.values(): - inp.set_position(0, self.label_font_size + self.io_font_size*i) + inp.set_position(margin, self.label_font_size + self.io_font_size*i - margin) inp.onpositionchanged() i += 1 - i = 1 + i = 2 for o in self.outputs.values(): ow, oh = o.get_size() - o.set_position(w - ow, self.label_font_size + self.io_font_size*i) + o.set_position(w - ow - margin, self.label_font_size + self.io_font_size*i - margin) o.onpositionchanged() i += 1 @@ -646,105 +833,10 @@ def set_position(self, x, y): return super().set_position(x, y) def set_name(self, name): - self.name = name - self.label.set_text(name) + self.variable_name = name self.adjust_geometry() -class ObjectFunctionBlockView(FunctionBlockView): - reference_object = None - method = None - method_name = None - - def __init__(self, obj, method, method_name, name, container, x = 10, y = 10, *args, **kwargs): - self.reference_object = obj - self.method = method - self.method_name = method_name - FunctionBlockView.__init__(self, name, container, x, y, *args, **kwargs) - - #for all the outputs defined by decorator on FunctionBlock.do - # add the related Outputs - #if hasattr(self.do, "_outputs"): - # for o in self.do._outputs: - # self.add_io_widget(OutputView(o)) - - def populate_io(self): - signature = inspect.signature(getattr(self.reference_object, self.method_name)) - for arg in signature.parameters: - self.add_io_widget(InputView(arg, default=signature.parameters[arg].default)) - self.add_io_widget(InputView('EN', default=False)) - - def do(self, *args, **kwargs): - if kwargs.get('EN') != None: - if kwargs['EN'] == False: - return - if 'EN' in kwargs: - del kwargs['EN'] - - output = getattr(self.reference_object, self.method_name)(*args, **kwargs) - """ #this is to populate outputs automatically - if self.processed_outputs == False: - if not output is None: - self.add_io_widget(OutputView('OUT' + str(0))) - if type(output) in (tuple,): - if len(output) > 1: - i = 1 - for o in output: - self.add_io_widget(OutputView('OUT' + str(i))) - i += 1 - - self.processed_outputs = True - """ - return output - - -class ProcessView(gui.Svg, FBD_model.Process): - selected_input = None - selected_output = None - - def __init__(self, *args, **kwargs): - gui.Svg.__init__(self, *args, **kwargs) - FBD_model.Process.__init__(self) - self.css_border_color = 'black' - self.css_border_width = '1' - self.css_border_style = 'solid' - self.style['background-color'] = 'lightyellow' - - def onselection_start(self, emitter, x, y): - self.selected_input = self.selected_output = None - print("selection start: ", type(emitter)) - if issubclass(type(emitter), FBD_model.Input): - self.selected_input = emitter - else: - self.selected_output = emitter - - def onselection_end(self, emitter, x, y): - print("selection end: ", type(emitter)) - if issubclass(type(emitter), FBD_model.Input): - self.selected_input = emitter - else: - self.selected_output = emitter - - if self.selected_input != None and self.selected_output != None: - if self.selected_input.is_linked(): - return - self.selected_output.link(self.selected_input, self) - - def add_function_block(self, function_block): - function_block.onclick.do(self.onfunction_block_clicked) - self.append(function_block, function_block.name) - FBD_model.Process.add_function_block(self, function_block) - - def add_object_block(self, object_block): - object_block.onclick.do(self.onfunction_block_clicked) - self.append(object_block, object_block.name) - FBD_model.Process.add_object_block(self, object_block) - - @gui.decorate_event - def onfunction_block_clicked(self, function_block): - return (function_block,) - - class FBToolbox(gui.Container): def __init__(self, appInstance, **kwargs): self.appInstance = appInstance @@ -760,18 +852,29 @@ def __init__(self, appInstance, **kwargs): self.append([self.lblTitle, self.widgetsContainer]) - import FBD_library + from . import FBD_library # load all widgets - self.add_widget_to_collection(FBD_library.BOOL) - self.add_widget_to_collection(FBD_library.NOT) - self.add_widget_to_collection(FBD_library.AND) - self.add_widget_to_collection(FBD_library.OR) - self.add_widget_to_collection(FBD_library.XOR) - self.add_widget_to_collection(FBD_library.PULSAR) - self.add_widget_to_collection(FBD_library.STRING) - self.add_widget_to_collection(FBD_library.STRING_SWAP_CASE) - self.add_widget_to_collection(FBD_library.RISING_EDGE) - self.add_widget_to_collection(FBD_library.PRINT) + self.add_widget_to_collection(FBD_library.NONE, "Logic") + self.add_widget_to_collection(FBD_library.BOOL, "Logic") + self.add_widget_to_collection(FBD_library.NOT, "Logic") + self.add_widget_to_collection(FBD_library.AND, "Logic") + self.add_widget_to_collection(FBD_library.OR, "Logic") + self.add_widget_to_collection(FBD_library.XOR, "Logic") + self.add_widget_to_collection(FBD_library.PULSAR, "Event") + self.add_widget_to_collection(FBD_library.RISING_EDGE, "Event") + self.add_widget_to_collection(FBD_library.FALLING_EDGE, "Event") + self.add_widget_to_collection(FBD_library.STRING, "String") + self.add_widget_to_collection(FBD_library.STRING_SWAP_CASE, "String") + self.add_widget_to_collection(FBD_library.PRINT, "String") + self.add_widget_to_collection(FBD_library.SUM, "Math") + self.add_widget_to_collection(FBD_library.MUL, "Math") + self.add_widget_to_collection(FBD_library.DIV, "Math") + self.add_widget_to_collection(FBD_library.DIFFERENCE, "Math") + self.add_widget_to_collection(FBD_library.COUNTER, "Math") + self.add_widget_to_collection(FBD_library.GREATER_THAN, "Math") + self.add_widget_to_collection(FBD_library.LESS_THAN, "Math") + self.add_widget_to_collection(FBD_library.EQUAL_TO, "Math") + self.add_widget_to_collection(FBD_library.INT, "Math") def add_widget_to_collection(self, functionBlockClass, group='standard_tools', **kwargs_to_widget): # create an helper that will be created on click @@ -839,7 +942,7 @@ def build_widget_name_list_from_tree(self, node): if not issubclass(type(node), FBD_model.FunctionBlock) and not issubclass(type(node), ProcessView): return if issubclass(type(node), FBD_model.FunctionBlock): - self.varname_list.append(node.name) + self.varname_list.append(node.identifier) for child in node.children.values(): self.build_widget_name_list_from_tree(child) @@ -881,7 +984,7 @@ def create_instance(self, widget): return """ # here we create and decorate the widget - function_block = self.functionBlockClass(variableName, self.appInstance.process, **self.kwargs_to_widget) + function_block = self.functionBlockClass(**self.kwargs_to_widget) function_block.attr_editor_newclass = False for key in self.optional_style_dict: @@ -910,17 +1013,42 @@ def main(self): self.main_container = gui.AsciiContainer(width="100%", height="100%", margin='0px auto') self.main_container.set_from_asciiart( """ - |toolbox|process_view |attributes| + |toolbox |process_view |attributes| + |container|process_view |attributes| """, 0, 0 ) + self.main_container.css_overflow = 'hidden' - self.process = ProcessView(width=600, height=600) + self.process = ProcessView("Process", width=600, height=600) self.process.onfunction_block_clicked.do(self.onprocessview_function_block_clicked) self.attributes_editor = EditorAttributes(self) self.toolbox = FBToolbox(self) - - self.process.add_object_block(TextInputAdapter(gui.TextInput(), self.process)) + #a container to put some widgets for debugging inside + self.container = gui.VBox() + self.main_container.append(self.container, "container") + + from . import FBD_library + imread = widgets.toolbox_opencv.OpencvImRead(r"C:\Users\progr\OneDrive\Software e progetti\remi\editor\res\widget_Image.png") + fb = FBD_library.FBWrapObjectMethod(imread.get_image_data) + fb.variable_name = "imread" + #fb.add_io_widget(OutputView("IMAGE")) + self.process.add_function_block(fb) + self.container.append(imread) + + imthreshold = widgets.toolbox_opencv.OpencvThreshold() + fb = FBD_library.FBWrapObjectMethod(imthreshold.set_image_data) + fb.variable_name = "imthreshold" + self.process.add_function_block(fb) + self.container.append(imthreshold) + + def set_threshold(value): + imthreshold.threshold = value + fb = FBD_library.FBWrapObjectMethod(set_threshold) + fb.variable_name = "imthreshold" + self.process.add_function_block(fb) + self.container.append(imthreshold) + self.main_container.append(self.toolbox, 'toolbox') self.main_container.append(self.process, 'process_view') self.main_container.append(self.attributes_editor, 'attributes') diff --git a/editor/widgets/__init__.py b/editor/widgets/__init__.py index f57657cf..34aa2b2c 100644 --- a/editor/widgets/__init__.py +++ b/editor/widgets/__init__.py @@ -49,3 +49,9 @@ class SIEMENSPlaceholder(gui.Label): def __init__(self, msg="In order to use Siemens widgets install python-snap7 \n" + traceback.format_exc()): super(SIEMENSPlaceholder, self).__init__(msg) self.css_white_space = 'pre' + +try: + from .FBD_view import ProcessView, ProcessViewThreaded + from .FBD_library import * +except Exception: + print("ERROR: Failed to load Function Blocks: %s" % traceback.format_exc()) diff --git a/editor/widgets/toolbox_opencv.py b/editor/widgets/toolbox_opencv.py index 79b67a14..033fb103 100644 --- a/editor/widgets/toolbox_opencv.py +++ b/editor/widgets/toolbox_opencv.py @@ -63,7 +63,7 @@ def __init__(self, filename='', *args, **kwargs): kwargs['style'] = self.default_style kwargs['width'] = kwargs['style'].get('width', kwargs.get('width','200px')) kwargs['height'] = kwargs['style'].get('height', kwargs.get('height','180px')) - super(OpencvImage, self).__init__(filename, *args, **kwargs) + gui.Image.__init__(self, filename, *args, **kwargs) OpencvWidget._setup(self) def on_new_image_listener(self, emitter): @@ -79,6 +79,9 @@ def set_image_data(self, img): self.update() self.on_new_image() + def get_image_data(self): + return self.img + def search_app_instance(self, node): if issubclass(node.__class__, remi.server.App): return node @@ -90,10 +93,10 @@ def update(self, *args): if self.app_instance==None: self.app_instance = self.search_app_instance(self) if self.app_instance==None: - self.attributes['src'] = "/%s/get_image_data?index=00"%self.identifier #gui.load_resource(self.filename) + self.attributes['src'] = "/%s/get_image_encoded?index=00"%self.identifier #gui.load_resource(self.filename) return self.app_instance.execute_javascript(""" - url = '/%(id)s/get_image_data?index=%(frame_index)s'; + url = '/%(id)s/get_image_encoded?index=%(frame_index)s'; xhr = null; xhr = new XMLHttpRequest(); @@ -108,8 +111,8 @@ def update(self, *args): xhr.send(); """ % {'id': self.identifier, 'frame_index':str(time.time())}) - def get_image_data(self, index=0): - gui.Image.set_image(self, '/%(id)s/get_image_data?index=%(frame_index)s'% {'id': self.identifier, 'frame_index':str(time.time())}) + def get_image_encoded(self, index=0): + gui.Image.set_image(self, '/%(id)s/get_image_encoded?index=%(frame_index)s'% {'id': self.identifier, 'frame_index':str(time.time())}) self._set_updated() try: ret, png = cv2.imencode('.png', self.img) @@ -202,7 +205,7 @@ def update(self, *args): with self.app_instance.update_lock: self.app_instance.execute_javascript(""" - var url = '/%(id)s/get_image_data?index=%(frame_index)s'; + var url = '/%(id)s/get_image_encoded?index=%(frame_index)s'; var xhr = new XMLHttpRequest(); xhr.open('GET', url, true); xhr.responseType = 'blob' @@ -216,8 +219,8 @@ def update(self, *args): """ % {'id': self.identifier, 'frame_index':str(time.time())}) - def get_image_data(self, index=0): - gui.Image.set_image(self, '/%(id)s/get_image_data?index=%(frame_index)s'% {'id': self.identifier, 'frame_index':str(time.time())}) + def get_image_encoded(self, index=0): + gui.Image.set_image(self, '/%(id)s/get_image_encoded?index=%(frame_index)s'% {'id': self.identifier, 'frame_index':str(time.time())}) self._set_updated() try: ret, frame = self.capture.read() @@ -298,15 +301,17 @@ def __init__(self, *args, **kwargs): super(OpencvThreshold, self).__init__("", *args, **kwargs) self.threshold = 125 + def set_image_data(self, img): + OpencvImage.set_image_data(self, img) + if len(img.shape)>2: + img = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) + res, self.img = cv2.threshold(img,self.threshold,255,cv2.THRESH_BINARY) + def on_new_image_listener(self, emitter): #THRESHOLD if emitter is None or emitter.img is None: return self.image_source = emitter - img = emitter.img - if len(img.shape)>2: - img = cv2.cvtColor(emitter.img, cv2.COLOR_BGR2GRAY) - res, self.img = cv2.threshold(img,self.threshold,255,cv2.THRESH_BINARY) - self.set_image_data(self.img) + self.set_image_data(emitter.img) ''' class OpencvSimpleBlobDetector(OpencvImage):