diff --git a/ryven/ironflow/__init__.py b/ryven/ironflow/__init__.py index 490564e3..873a4993 100644 --- a/ryven/ironflow/__init__.py +++ b/ryven/ironflow/__init__.py @@ -1,4 +1,4 @@ -__all__ = ['gui', 'flow_canvas', 'canvas_widgets', 'node_interface.py'] +__all__ = ['gui', 'flow_canvas', 'canvas_widgets', 'layouts', 'models', 'node_interface'] -from .gui import GUI +from ryven.ironflow.gui import GUI from ryven.NENV import Node, NodeInputBP, NodeOutputBP, dtypes diff --git a/ryven/ironflow/canvas_widgets.py b/ryven/ironflow/canvas_widgets.py index 257d65ce..4336c3ef 100644 --- a/ryven/ironflow/canvas_widgets.py +++ b/ryven/ironflow/canvas_widgets.py @@ -6,13 +6,13 @@ import numpy as np from IPython.display import display -from .layouts import Layout, NodeLayout, PortLayout, DataPortLayout, ExecPortLayout, ButtonLayout +from ryven.ironflow.layouts import Layout, NodeLayout, PortLayout, DataPortLayout, ExecPortLayout, ButtonLayout from abc import ABC, abstractmethod from ryvencore.NodePort import NodeInput, NodeOutput -from typing import TYPE_CHECKING, Optional, Union, List, Callable +from typing import TYPE_CHECKING, Optional, Union if TYPE_CHECKING: - from .flow_canvas import FlowCanvas + from ryven.ironflow.flow_canvas import FlowCanvas from ryven.ironflow.gui import GUI from ipycanvas import Canvas from ryven.NENV import Node, NodeInputBP, NodeOutputBP @@ -21,7 +21,7 @@ from ryvencore.Flow import Flow -__author__ = "Joerg Neugebauer" +__author__ = "Joerg Neugebauer, Liam Huber" __copyright__ = ( "Copyright 2020, Max-Planck-Institut für Eisenforschung GmbH - " "Computational Materials Design (CM) Department" @@ -42,7 +42,7 @@ def __init__( self, x: Number, y: Number, - parent: Union[FlowCanvas, CanvasWidget], + parent: FlowCanvas | CanvasWidget, layout: Layout, selected: bool = False, title: Optional[str] = None, @@ -60,15 +60,12 @@ def __init__( self._height = self.layout.height @abstractmethod - def on_click(self, last_selected_object: Optional[CanvasWidget]) -> Optional[CanvasWidget]: + def on_click(self, last_selected_object: Optional[CanvasWidget]) -> CanvasWidget | None: pass - def on_double_click(self) -> Optional[CanvasWidget]: + def on_double_click(self) -> CanvasWidget | None: return self - def _init_after_parent_assignment(self): - pass - @property def width(self) -> int: return self.layout.width @@ -79,11 +76,11 @@ def height(self) -> int: @property def x(self) -> Number: - return self.parent.x + self._x # - self.parent.width//2 + return self.parent.x + self._x @property def y(self) -> Number: - return self.parent.y + self._y # - self.parent.height//2 + return self.parent.y + self._y @property def canvas(self) -> Canvas: @@ -114,8 +111,8 @@ def add_x_y(self, dx_in: Number, dy_in: Number) -> None: def draw_shape(self) -> None: self.canvas.fill_style = self.layout.selected_color if self.selected else self.layout.background_color self.canvas.fill_rect( - self.x, # - (self.width * 0.5), - self.y, # - (self.height * 0.5), + self.x, + self.y, self.width, self.height, ) @@ -132,11 +129,11 @@ def draw(self) -> None: o.draw() def _is_at_xy(self, x_in: Number, y_in: Number) -> bool: - x_coord = self.x # - (self.width * 0.5) - y_coord = self.y # - (self.height * 0.5) + x_coord = self.x + y_coord = self.y return x_coord < x_in < (x_coord + self.width) and y_coord < y_in < (y_coord + self.height) - def get_element_at_xy(self, x_in: Number, y_in: Number) -> Union[CanvasWidget, None]: + def get_element_at_xy(self, x_in: Number, y_in: Number) -> CanvasWidget | None: if self.is_here(x_in, y_in): for o in self.objects_to_draw: if o.is_here(x_in, y_in): @@ -165,7 +162,7 @@ def __init__( self, x: Number, y: Number, - parent: Union[FlowCanvas, CanvasWidget], + parent: FlowCanvas | CanvasWidget, layout: Layout, selected: bool = False, title: Optional[str] = None, @@ -222,7 +219,7 @@ def __init__( self, x: Number, y: Number, - parent: Union[FlowCanvas, CanvasWidget], + parent: FlowCanvas | CanvasWidget, layout: PortLayout, port: NodePort, selected: bool = False, @@ -245,7 +242,7 @@ def __init__( self.radius = radius self.port = port - def on_click(self, last_selected_object: Optional[CanvasWidget]) -> Optional[CanvasWidget]: + def on_click(self, last_selected_object: Optional[CanvasWidget]) -> PortWidget | None: if last_selected_object == self: self.deselect() return None @@ -277,7 +274,7 @@ def __init__( self, x: Number, y: Number, - parent: Union[FlowCanvas, CanvasWidget], + parent: FlowCanvas | CanvasWidget, layout: NodeLayout, node: Node, selected: bool = False, @@ -303,26 +300,25 @@ def __init__( 'exec': ExecPortLayout() } - self._title_box_height = 30 - - n_ports_max = max(len(self.node.inputs), len(self.node.outputs)) - n_ports_min = len([p for p in self.node.inputs if p.type_ == "exec"]) + n_ports_max = max(len(self.node.inputs), len(self.node.outputs)) + 1 # Includes the expand/collapse button + exec_port_i = np.where([p.type_ == "exec" for p in self.node.inputs])[0] + n_ports_min = exec_port_i[-1] + 1 if len(exec_port_i) > 0 else 1 subwidget_size_and_buffer = 1.33 * 2 * self.port_radius - self._io_height = subwidget_size_and_buffer * n_ports_max - self._exec_height = subwidget_size_and_buffer * n_ports_min - # TODO: Right now we're hard-coding in that all the exec ports (which come with buttons) appear first in input - # This isn't necessarily so, nor checked for anywhere. Do better. - self._expand_collapse_height = subwidget_size_and_buffer + self._title_box_height = self.layout.title_box_height + self._max_body_height = subwidget_size_and_buffer * n_ports_max + self._min_body_height = subwidget_size_and_buffer * n_ports_min + self._expanded_height = self._title_box_height + self._max_body_height + self._collapsed_height = self._title_box_height + self._min_body_height self._height = self._expanded_height - y_step = (self._io_height + self._expand_collapse_height) / (n_ports_max + 1) - self._port_y_locs = (np.arange(n_ports_max + 1) + 0.5) * y_step + self._title_box_height + y_step = self._max_body_height / n_ports_max + self._subwidget_y_locs = (np.arange(n_ports_max) + 0.5) * y_step + self._title_box_height self.add_inputs() self.add_outputs() self.expand_button = ExpandButtonWidget( x=0.5 * self.width - self.port_radius, - y=self._port_y_locs[0] - self.port_radius, + y=self._subwidget_y_locs[0] - self.port_radius, parent=self, layout=ButtonLayout(), pressed=True, @@ -332,7 +328,7 @@ def __init__( self.add_widget(self.expand_button) self.collapse_button = CollapseButtonWidget( x=0.5 * self.width - self.port_radius, - y=self._port_y_locs[-1] - self.port_radius, + y=self._subwidget_y_locs[-1] - self.port_radius, parent=self, layout=ButtonLayout(), pressed=False, @@ -341,7 +337,7 @@ def __init__( ) self.add_widget(self.collapse_button) - def on_click(self, last_selected_object: Optional[CanvasWidget]) -> Optional[CanvasWidget]: + def on_click(self, last_selected_object: Optional[CanvasWidget]) -> NodeWidget | None: if last_selected_object == self: return self else: @@ -357,7 +353,7 @@ def on_click(self, last_selected_object: Optional[CanvasWidget]) -> Optional[Can self.deselect() return None - def on_double_click(self) -> Optional[CanvasWidget]: + def on_double_click(self) -> None: self.delete() return None @@ -373,8 +369,8 @@ def draw_title(self) -> None: def _add_ports( self, radius: Number, - inputs: Optional[List[NodeInputBP]] = None, - outputs: Optional[List[NodeOutputBP]] = None, + inputs: Optional[list[NodeInputBP]] = None, + outputs: Optional[list[NodeOutputBP]] = None, border: Number = 1.4, ) -> None: if inputs is not None: @@ -393,12 +389,12 @@ def _add_ports( self.add_widget( PortWidget( x=x, - y=self._port_y_locs[i_port], + y=self._subwidget_y_locs[i_port], parent=self, layout=self.port_layouts[data_or_exec], port=port, hidden_x=x, - hidden_y=self._port_y_locs[0], + hidden_y=self._subwidget_y_locs[0], radius=radius, ) ) @@ -406,8 +402,8 @@ def _add_ports( button_layout = ButtonLayout() self.add_widget( ExecButtonWidget( - x=x + 0.3 * button_layout.width, - y=self._port_y_locs[i_port] - 0.5 * button_layout.height, + x=x + radius, + y=self._subwidget_y_locs[i_port] - 0.5 * button_layout.height, parent=self, layout=button_layout, port=port @@ -434,29 +430,17 @@ def delete(self) -> None: def port_widgets(self) -> list[PortWidget]: return [o for o in self.objects_to_draw if isinstance(o, PortWidget)] - @property - def _expanded_height(self) -> Number: - return self._title_box_height + self._io_height + self._expand_collapse_height - - @property - def _collapsed_height(self) -> Number: - return self._title_box_height + max(self._expand_collapse_height, self._exec_height) - def expand_io(self): self._height = self._expanded_height for o in self.port_widgets: o.show() - # self.collapse_button.on_click() # Why doesn't this do the same as the next two lines?? - self.collapse_button.on_unpressed() - self.collapse_button.pressed = False + self.collapse_button.unpress() def collapse_io(self): self._height = self._collapsed_height for o in self.port_widgets: o.hide() - # TODO: The expand and collapse buttons are effectively an XOR toggle...improve this awkward implementation - self.expand_button.on_unpressed() - self.expand_button.pressed = False + self.expand_button.unpress() class ButtonNodeWidget(NodeWidget): @@ -464,7 +448,7 @@ def __init__( self, x: Number, y: Number, - parent: Union[FlowCanvas, CanvasWidget], + parent: FlowCanvas | CanvasWidget, layout: NodeLayout, node: Node, selected: bool = False, @@ -478,7 +462,7 @@ def __init__( button_layout = ButtonLayout() self.exec_button = ExecButtonWidget( x=0.8 * (self.width - button_layout.width), - y=self._port_y_locs[0] - 0.5 * button_layout.height, + y=self._subwidget_y_locs[0] - 0.5 * button_layout.height, parent=self, layout=button_layout, port=self.node.outputs[0], @@ -501,7 +485,7 @@ def __init__( self.title = title self.pressed = pressed - def on_click(self, last_selected_object: Optional[CanvasWidget]) -> Optional[CanvasWidget]: + def on_click(self, last_selected_object: Optional[CanvasWidget]) -> CanvasWidget | None: if self.pressed: self.unpress() else: @@ -550,7 +534,7 @@ def __init__( parent: DisplayableNodeWidget, layout: ButtonLayout, selected: bool = False, - title="PLOT", + title="SHOW", ): super().__init__(x, y, parent, layout, selected, title=title) @@ -572,7 +556,7 @@ def __init__( self, x: Number, y: Number, - parent: Union[FlowCanvas, CanvasWidget], + parent: FlowCanvas | CanvasWidget, layout: NodeLayout, node: Node, selected: bool = False, @@ -647,9 +631,8 @@ def __init__( if size is not None: layout.width = size layout.height = size - dpl = DataPortLayout() - layout.background_color = dpl.background_color - layout.pressed_color = dpl.background_color + layout.background_color = parent.node.color + layout.pressed_color = parent.node.color ButtonWidget.__init__(self, x=x, y=y, parent=parent, layout=layout, selected=selected, title=title, pressed=pressed) @@ -718,7 +701,7 @@ def __init__( parent=parent, layout=layout, selected=selected, - title=port.label_str if port.label_str is not None else title, + title=port.label_str if port.label_str != '' else title, pressed=pressed ) self.port = port diff --git a/ryven/ironflow/flow_canvas.py b/ryven/ironflow/flow_canvas.py index b0d8220c..6e738cf4 100644 --- a/ryven/ironflow/flow_canvas.py +++ b/ryven/ironflow/flow_canvas.py @@ -7,12 +7,12 @@ from ipycanvas import Canvas, hold_canvas from time import time -from .canvas_widgets import ( +from ryven.ironflow.canvas_widgets import ( NodeWidget, PortWidget, CanvasWidget, ButtonNodeWidget, DisplayableNodeWidget, DisplayButtonWidget ) -from .layouts import NodeLayout +from ryven.ironflow.layouts import NodeLayout -from typing import TYPE_CHECKING, Optional, Union, List +from typing import TYPE_CHECKING, Optional, Union if TYPE_CHECKING: from gui import GUI from ryven.NENV import Node @@ -55,28 +55,24 @@ def __init__(self, gui: GUI, flow: Optional[Flow] = None, width: int = 2000, hei self.flow = flow if flow is not None else gui.flow self._width, self._height = width, height - self._col_background = "black" # "#584f4e" - self._col_node_header = "blue" # "#38a8a4" - self._col_node_selected = "#9dcea6" - self._col_node_unselected = "#dee7bc" - - self._font_size = 30 - self._node_box_size = 160, 70 + self._canvas_color = "black" # "#584f4e" + self._connection_style = "white" + self._connection_width = 3 self._canvas = Canvas(width=width, height=height) - self._canvas.fill_style = self._col_background + self._canvas.fill_style = self._canvas_color self._canvas.fill_rect(0, 0, width, height) self._canvas.layout.width = "100%" self._canvas.layout.height = "auto" - self.objects_to_draw = [] - self.connections = [] - self._canvas.on_mouse_down(self.handle_mouse_down) self._canvas.on_mouse_up(self.handle_mouse_up) self._canvas.on_mouse_move(self.handle_mouse_move) self._canvas.on_key_down(self.handle_keyboard_event) + self.objects_to_draw = [] + self.connections = [] + self.x = 0 self.y = 0 self._x_move_anchor = None @@ -88,9 +84,6 @@ def __init__(self, gui: GUI, flow: Optional[Flow] = None, width: int = 2000, hei self._last_mouse_down = time() self._double_click_speed = 0.25 # In seconds. TODO: Put this in a config somewhere - self._connection_in = None - self._node_widget = None - self._object_to_gui_dict = {} @property @@ -102,15 +95,12 @@ def gui(self): return self._gui def draw_connection(self, port_1: int, port_2: int) -> None: - # i_out, i_in = path - # out = self.objects_to_draw[i_out] - # inp = self.objects_to_draw[i_in] out = self._object_to_gui_dict[port_1] inp = self._object_to_gui_dict[port_2] canvas = self._canvas - canvas.stroke_style = "white" - canvas.line_width = 3 + canvas.stroke_style = self._connection_style + canvas.line_width = self._connection_width canvas.move_to(out.x, out.y) canvas.line_to(inp.x, inp.y) canvas.stroke() @@ -125,27 +115,12 @@ def _built_object_to_gui_dict(self) -> None: def canvas_restart(self) -> None: self._canvas.clear() - self._canvas.fill_style = self._col_background + self._canvas.fill_style = self._canvas_color self._canvas.fill_rect(0, 0, self._width, self._height) def handle_keyboard_event(self, key: str, shift_key, ctrl_key, meta_key) -> None: pass # TODO - def set_connection(self, ind_node: int) -> None: - if self._connection_in is None: - self._connection_in = ind_node - else: - out = self.objects_to_draw[self._connection_in].node.outputs[0] - inp = self.objects_to_draw[ind_node].node.inputs[-1] - if self.flow.connect_nodes(inp, out) is None: - i_con = self.connections.index([self._connection_in, ind_node]) - del self.connections[i_con] - else: - self.connections.append([self._connection_in, ind_node]) - - self._connection_in = None - self.deselect_all() - def deselect_all(self) -> None: [o.deselect() for o in self.objects_to_draw] self.redraw() @@ -181,13 +156,13 @@ def handle_mouse_down(self, x: Number, y: Number): def handle_mouse_up(self, x: Number, y: Number): self._mouse_is_down = False - def get_element_at_xy(self, x_in: Number, y_in: Number) -> Union[CanvasWidget, None]: + def get_element_at_xy(self, x_in: Number, y_in: Number) -> CanvasWidget | None: for o in self.objects_to_draw: if o.is_here(x_in, y_in): return o.get_element_at_xy(x_in, y_in) return None - def get_selected_objects(self) -> List[CanvasWidget]: + def get_selected_objects(self) -> list[CanvasWidget]: return [o for o in self.objects_to_draw if o.selected] def handle_mouse_move(self, x: Number, y: Number) -> None: @@ -212,20 +187,18 @@ def redraw(self) -> None: self.draw_connection(c.inp, c.out) def load_node(self, x: Number, y: Number, node: Node) -> NodeWidget: - # print ('node: ', node.identifier, node.GLOBAL_ID) - layout = NodeLayout() - if hasattr(node, "main_widget_class"): - if node.main_widget_class is not None: - # node.title = str(node.main_widget_class) - f = eval(node.main_widget_class) - s = f(x, y, parent=self, layout=layout, node=node) + if hasattr(node, "main_widget_class") and node.main_widget_class is not None: + if isinstance(node.main_widget_class, str): + node_class = eval(node.main_widget_class) + elif issubclass(node.main_widget_class, NodeWidget): + node_class = node.main_widget_class else: - s = NodeWidget(x, y, parent=self, layout=layout, node=node) - # print ('s: ', s) + raise TypeError(f"main_widget_class {node.main_widget_class} not recognized") else: - s = NodeWidget(x, y, parent=self, layout=layout, node=node) + node_class = NodeWidget + s = node_class(x=x, y=y, parent=self, layout=layout, node=node) self.objects_to_draw.append(s) return s diff --git a/ryven/ironflow/gui.py b/ryven/ironflow/gui.py index e3226b1c..5774489a 100644 --- a/ryven/ironflow/gui.py +++ b/ryven/ironflow/gui.py @@ -21,7 +21,7 @@ from ryven.ironflow.node_interface import NodeInterface from ryven.ironflow.flow_canvas import FlowCanvas -from typing import Optional, Dict +from typing import Optional from ryvencore import Session alg_modes = ["data", "exec"] @@ -49,7 +49,7 @@ def create_script( self, title: Optional[str] = None, create_default_logs: bool = True, - data: Optional[Dict] = None + data: Optional[dict] = None ) -> None: super().create_script(title=title, create_default_logs=create_default_logs, data=data) self._flow_canvases.append(FlowCanvas(gui=self)) @@ -59,14 +59,26 @@ def delete_script(self) -> None: super().delete_script() @property - def flow_canvas_widget(self): + def flow_canvas(self): return self._flow_canvases[self.active_script_index] @property def new_node_class(self): return self._nodes_dict[self.modules_dropdown.value][self.node_selector.value] - def load_from_data(self, data: Dict) -> None: + def serialize(self) -> dict: + data = super().serialize() + currently_active = self.active_script_index + for i_script, script in enumerate(self.session.scripts): + all_data = data["scripts"][i_script]["flow"]["nodes"] + self.active_script_index = i_script + for i, node_widget in enumerate(self.flow_canvas.objects_to_draw): + all_data[i]["pos x"] = node_widget.x + all_data[i]["pos y"] = node_widget.y + self.active_script_index = currently_active + return data + + def load_from_data(self, data: dict) -> None: super().load_from_data(data) self._flow_canvases = [] for i_script, script in enumerate(self.session.scripts): @@ -126,6 +138,17 @@ def draw(self) -> widgets.VBox: icon="map-marker", # TODO: Use location-dot once this is available layout=button_layout ) + buttons = [ + self.btn_save, + self.btn_load, + self.btn_help_node, + self.btn_add_node, + self.btn_delete_node, + self.btn_create_script, + self.btn_rename_script, + self.btn_delete_script, + self.btn_zero_location, + ] self.text_input_panel = widgets.HBox([]) self.text_input_field = widgets.Text(value="INIT VALUE", description="DESCRIPTION") @@ -144,8 +167,6 @@ def draw(self) -> widgets.VBox: self.node_selector = widgets.RadioButtons( options=nodes_options, value=list(nodes_options)[0], - # layout={'width': 'max-content'}, # If the items' names are long - # description='Nodes:', disabled=False, ) @@ -162,7 +183,7 @@ def draw(self) -> widgets.VBox: self.btn_rename_script.on_click(self.click_rename_script) self.btn_input_text_ok.on_click(self.click_input_text_ok) self.text_input_field.on_submit(self.click_input_text_ok) - # ^ Ignore the deprecation warning, 'observe' does function the way we actually want + # ^ Ignore the deprecation warning, 'observe' doesn't function the way we actually want # https://github.com/jupyter-widgets/ipywidgets/issues/2446 self.btn_input_text_cancel.on_click(self.click_input_text_cancel) self.btn_delete_script.on_click(self.click_delete_script) @@ -175,15 +196,7 @@ def draw(self) -> widgets.VBox: [ self.modules_dropdown, self.alg_mode_dropdown, - self.btn_save, - self.btn_load, - self.btn_help_node, - self.btn_add_node, - self.btn_delete_node, - self.btn_create_script, - self.btn_rename_script, - self.btn_delete_script, - self.btn_zero_location, + *buttons, ] ), self.text_input_panel, @@ -198,73 +211,16 @@ def draw(self) -> widgets.VBox: # Type hinting for unused `change` argument in callbacks taken from ipywidgets docs: # https://ipywidgets.readthedocs.io/en/latest/examples/Widget%20Events.html#Traitlet-events - def click_add_node(self, change: dict) -> None: - self.flow_canvas_widget.add_node(10, 10, self.new_node_class) - - def click_delete_node(self, change: Dict) -> None: - self.flow_canvas_widget.delete_selected() - - def change_modules_dropdown(self, change: Dict) -> None: + def change_modules_dropdown(self, change: dict) -> None: self.node_selector.options = sorted(self._nodes_dict[self.modules_dropdown.value].keys()) - def change_alg_mode_dropdown(self, change: Dict) -> None: + def change_alg_mode_dropdown(self, change: dict) -> None: # Current behaviour: Updates the flow mode for all scripts # TODO: Change only for the active script, and update the dropdown on tab (script) switching for script in self.session.scripts: script.flow.set_algorithm_mode(self.alg_mode_dropdown.value) - def change_script_tabs(self, change: Dict): - if change['name'] == 'selected_index' and change['new'] is not None: - self._depopulate_text_input_panel() - if self.script_tabs.selected_index == self.n_scripts: - self.create_script() - self._update_tabs_from_model() - else: - self.active_script_index = self.script_tabs.selected_index - self.flow_canvas_widget.redraw() - - def _populate_text_input_panel(self, description, initial_value, description_tooltip=None): - self.text_input_panel.children = [ - self.text_input_field, - self.btn_input_text_ok, - self.btn_input_text_cancel - ] - self.text_input_field.description = description - description_tooltip = description_tooltip if description_tooltip is not None else description - self.text_input_field.description_tooltip = description_tooltip - self.text_input_field.value = initial_value - - def _depopulate_text_input_panel(self) -> None: - self.text_input_panel.children = [] - - def click_input_text_ok(self, change: Dict) -> None: - self._context_actions[self._context](self.text_input_field.value) - self._depopulate_text_input_panel() - - def click_input_text_cancel(self, change: Dict) -> None: - self._depopulate_text_input_panel() - self._print("") - - def _set_context(self, context): - if context not in self._context_actions.keys(): - raise KeyError(f"Expected a context action among {list(self._context_actions.keys())} but got {context}.") - self._context = context - - def click_node_help(self, change: dict) -> None: - def _pretty_docstring(node_class): - """ - If we just pass a string, `display` doesn't resolve newlines. - If we pass a `print`ed string, `display` also shows the `None` value returned by `print` - So we use this ugly hack. - """ - string = f"{node_class.__name__.replace('_Node', '')}:\n{node_class.__doc__}" - return HTML(string.replace("\n", "
").replace("\t", " ").replace(" ", " ")) - - self.out_log.clear_output() - with self.out_log: - display(_pretty_docstring(self.new_node_class)) - - def click_save(self, change: Dict) -> None: + def click_save(self, change: dict) -> None: self._depopulate_text_input_panel() self._populate_text_input_panel( "Save file", @@ -278,7 +234,7 @@ def _save_context_action(self, file_name): self.save(f"{file_name}.json") self._print(f"Session saved to {file_name}.json") - def click_load(self, change: Dict) -> None: + def click_load(self, change: dict) -> None: self._depopulate_text_input_panel() self._populate_text_input_panel( "Load file", @@ -295,14 +251,34 @@ def _load_context_action(self, file_name): self.out_log.clear_output() self._print(f"Session loaded from {file_name}.json") + def click_node_help(self, change: dict) -> None: + def _pretty_docstring(node_class): + """ + If we just pass a string, `display` doesn't resolve newlines. + If we pass a `print`ed string, `display` also shows the `None` value returned by `print` + So we use this ugly hack. + """ + string = f"{node_class.__name__.replace('_Node', '')}:\n{node_class.__doc__}" + return HTML(string.replace("\n", "
").replace("\t", " ").replace(" ", " ")) + + self.out_log.clear_output() + with self.out_log: + display(_pretty_docstring(self.new_node_class)) + + def click_add_node(self, change: dict) -> None: + self.flow_canvas.add_node(10, 10, self.new_node_class) + + def click_delete_node(self, change: dict) -> None: + self.flow_canvas.delete_selected() + def click_create_script(self, change: dict) -> None: self.create_script() self._update_tabs_from_model() self.script_tabs.selected_index = self.n_scripts - 1 self.active_script_index = self.script_tabs.selected_index - self.flow_canvas_widget.redraw() + self.flow_canvas.redraw() - def click_rename_script(self, change: Dict) -> None: + def click_rename_script(self, change: dict) -> None: self._depopulate_text_input_panel() self._populate_text_input_panel( "New name", @@ -321,14 +297,51 @@ def _rename_context_action(self, new_name): else: self._print(f"INVALID NAME: Failed to rename script '{self.script.title}' to '{new_name}'.") - def click_delete_script(self, change: Dict) -> None: + def click_delete_script(self, change: dict) -> None: self.delete_script() self._update_tabs_from_model() def click_zero_location(self, change: dict) -> None: - self.flow_canvas_widget.x = 0 - self.flow_canvas_widget.y = 0 - self.flow_canvas_widget.redraw() + self.flow_canvas.x = 0 + self.flow_canvas.y = 0 + self.flow_canvas.redraw() + + def _populate_text_input_panel(self, description, initial_value, description_tooltip=None): + self.text_input_panel.children = [ + self.text_input_field, + self.btn_input_text_ok, + self.btn_input_text_cancel + ] + self.text_input_field.description = description + description_tooltip = description_tooltip if description_tooltip is not None else description + self.text_input_field.description_tooltip = description_tooltip + self.text_input_field.value = initial_value + + def _depopulate_text_input_panel(self) -> None: + self.text_input_panel.children = [] + + def click_input_text_ok(self, change: dict) -> None: + self._context_actions[self._context](self.text_input_field.value) + self._depopulate_text_input_panel() + + def click_input_text_cancel(self, change: dict) -> None: + self._depopulate_text_input_panel() + self._print("") + + def _set_context(self, context): + if context not in self._context_actions.keys(): + raise KeyError(f"Expected a context action among {list(self._context_actions.keys())} but got {context}.") + self._context = context + + def change_script_tabs(self, change: dict): + if change['name'] == 'selected_index' and change['new'] is not None: + self._depopulate_text_input_panel() + if self.script_tabs.selected_index == self.n_scripts: + self.create_script() + self._update_tabs_from_model() + else: + self.active_script_index = self.script_tabs.selected_index + self.flow_canvas.redraw() def _update_tabs_from_model(self): self.script_tabs.selected_index = None @@ -352,5 +365,4 @@ def _add_new_script_tab(self): def _print(self, text: str) -> None: with self.out_log: self.out_log.clear_output() - print(text) diff --git a/ryven/ironflow/layouts.py b/ryven/ironflow/layouts.py index b1224bc2..37d8d02f 100644 --- a/ryven/ironflow/layouts.py +++ b/ryven/ironflow/layouts.py @@ -38,6 +38,7 @@ class NodeLayout(Layout): width: int = 200 height: int = 100 font_size: int = 22 + title_box_height: int = 30 @dataclass @@ -61,7 +62,7 @@ class ExecPortLayout(PortLayout): @dataclass class ButtonLayout(Layout): font_size: int = 16 - width: int = 50 + width: int = 60 height: int = 20 background_color: str = "darkgray" pressed_color: str = "dimgray" diff --git a/ryven/ironflow/models.py b/ryven/ironflow/models.py index e8797cad..3bfdff61 100644 --- a/ryven/ironflow/models.py +++ b/ryven/ironflow/models.py @@ -22,7 +22,7 @@ from ryvencore import Session, Script, Flow from ryven.main.utils import import_nodes_package, NodesPackage -from typing import Optional, Dict, Type +from typing import Optional, Type import ryven.NENV as NENV @@ -34,7 +34,7 @@ ("built_in",), ("std",), ("pyiron",), -]] # , ("mynodes",) +]] class HasSession(ABC): @@ -94,7 +94,7 @@ def create_script( self, title: Optional[str] = None, create_default_logs: bool = True, - data: Optional[Dict] = None + data: Optional[dict] = None ) -> None: self.session.create_script( title=title if title is not None else self.next_auto_script_name, @@ -120,17 +120,8 @@ def save(self, file_path: str) -> None: with open(file_path, "w") as f: f.write(json.dumps(data, indent=4)) - def serialize(self) -> Dict: - currently_active = self.active_script_index - data = self.session.serialize() - for i_script, script in enumerate(self.session.scripts): - all_data = data["scripts"][i_script]["flow"]["nodes"] - self.active_script_index = i_script - for i, node_widget in enumerate(self.flow_canvas_widget.objects_to_draw): - all_data[i]["pos x"] = node_widget.x - all_data[i]["pos y"] = node_widget.y - self.active_script_index = currently_active - return data + def serialize(self) -> dict: + return self.session.serialize() def load(self, file_path: str) -> None: with open(file_path, "r") as f: @@ -138,7 +129,7 @@ def load(self, file_path: str) -> None: self.load_from_data(data) - def load_from_data(self, data: Dict) -> None: + def load_from_data(self, data: dict) -> None: for script in self.session.scripts[::-1]: self.session.delete_script(script) self.session.load(data) diff --git a/ryven/ironflow/node_interface.py b/ryven/ironflow/node_interface.py index a80984bd..848b9508 100644 --- a/ryven/ironflow/node_interface.py +++ b/ryven/ironflow/node_interface.py @@ -11,7 +11,7 @@ import pickle import base64 -from typing import TYPE_CHECKING, Dict, Union, Callable +from typing import TYPE_CHECKING, Callable if TYPE_CHECKING: from gui import GUI from ryven.NENV import Node @@ -42,7 +42,7 @@ def __init__(self, central_gui: GUI): self._central_gui = central_gui # self.input = [] - def gui_object(self) -> Union[widgets.FloatSlider, widgets.Box]: + def gui_object(self) -> widgets.FloatSlider | widgets.Box: if "slider" in self.node.title.lower(): self.gui = widgets.FloatSlider( value=self.node.val, min=0, max=10, continuous_update=False @@ -53,10 +53,10 @@ def gui_object(self) -> Union[widgets.FloatSlider, widgets.Box]: self.gui = widgets.Box() return self.gui - def gui_object_change(self, change: Dict) -> None: + def gui_object_change(self, change: dict) -> None: self.node.set_state({"val": change["new"]}, 0) self.node.update_event() - self._central_gui.flow_canvas_widget.redraw() + self._central_gui.flow_canvas.redraw() def input_widgets(self) -> None: self._input = [] @@ -112,10 +112,10 @@ def input_widgets(self) -> None: # inp_widget.value = dtype_state['default'] def input_change_i(self, i_c) -> Callable: - def input_change(change: Dict) -> None: + def input_change(change: dict) -> None: self.node.inputs[i_c].val = change["new"] self.node.update_event() - self._central_gui.flow_canvas_widget.redraw() + self._central_gui.flow_canvas.redraw() return input_change def draw(self) -> widgets.HBox: diff --git a/ryven/nodes/pyiron/atomistics_nodes.py b/ryven/nodes/pyiron/atomistics_nodes.py index 5650964e..fa2b97f1 100644 --- a/ryven/nodes/pyiron/atomistics_nodes.py +++ b/ryven/nodes/pyiron/atomistics_nodes.py @@ -6,12 +6,13 @@ import numpy as np from pyiron_atomistics import Project from ryven.NENV import Node, NodeInputBP, NodeOutputBP, dtypes +from ryven.ironflow.canvas_widgets import DisplayableNodeWidget, ButtonNodeWidget from abc import ABC, abstractmethod from ryven.nodes.std.special_nodes import DualNodeBase -__author__ = "Joerg Neugebauer" +__author__ = "Joerg Neugebauer, Liam Huber" __copyright__ = ( "Copyright 2020, Max-Planck-Institut für Eisenforschung GmbH - " "Computational Materials Design (CM) Department" @@ -33,7 +34,7 @@ def __init__(self, params): class NodeWithDisplay(NodeBase, ABC): - main_widget_class = "DisplayableNodeWidget" + main_widget_class = DisplayableNodeWidget def __init__(self, params): super().__init__(params) @@ -220,7 +221,6 @@ class GenericOutput_Node(NodeWithDisplay): """Select Generic Output item""" version = "v0.1" - title = "GenericOutput" init_inputs = [ NodeInputBP(dtype=dtypes.Data(size="m"), label="job"), @@ -234,9 +234,6 @@ class GenericOutput_Node(NodeWithDisplay): init_outputs = [ NodeOutputBP(), ] - - # main_widget_class = widgets.Result_Node_MainWidget - # main_widget_pos = 'between ports' color = "#c69a15" def __init__(self, params): @@ -390,8 +387,6 @@ class Result_Node(NodeBase): init_inputs = [ NodeInputBP(type_="data"), ] - # main_widget_class = widgets.Result_Node_MainWidget - # main_widget_pos = 'between ports' color = "#c69a15" def __init__(self, params): @@ -438,12 +433,7 @@ def update_event(self, inp=-1): self.exec_output(2) elif inp > 0: self._count = 0 - self.val = self._count - # for e in self.input(0): - # self.set_output_val(1, e) - # self.exec_output(0) - - # self.exec_output(2) + self.val = self._count class ExecCounter_Node(DualNodeBase): @@ -472,8 +462,7 @@ def update_event(self, inp=-1): class Click_Node(NodeBase): title = "Click" version = "v0.1" - main_widget_class = "ButtonNodeWidget" - main_widget_pos = "between ports" + main_widget_class = ButtonNodeWidget init_inputs = [] init_outputs = [NodeOutputBP(type_="exec")] color = "#99dd55" diff --git a/tests/unit/test_flow_canvas.py b/tests/unit/test_flow_canvas.py index 92d1006c..674600fb 100644 --- a/tests/unit/test_flow_canvas.py +++ b/tests/unit/test_flow_canvas.py @@ -11,7 +11,7 @@ class TestCanvasObect(TestCase): def setUp(self): self.gui = GUI('gui') - self.canvas = self.gui.flow_canvas_widget + self.canvas = self.gui.flow_canvas @classmethod def tearDownClass(cls): diff --git a/tests/unit/test_gui.py b/tests/unit/test_gui.py index 1b8ee0fd..59a67704 100644 --- a/tests/unit/test_gui.py +++ b/tests/unit/test_gui.py @@ -17,9 +17,9 @@ def tearDown(self) -> None: def test_multiple_scripts(self): gui = GUI('foo') - gui.flow_canvas_widget.add_node(0, 0, gui._nodes_dict['nodes']['val']) + gui.flow_canvas.add_node(0, 0, gui._nodes_dict['nodes']['val']) gui.create_script() - gui.flow_canvas_widget.add_node(1, 1, gui._nodes_dict['nodes']['result']) + gui.flow_canvas.add_node(1, 1, gui._nodes_dict['nodes']['result']) canonical_file_name = f"{gui.session_title}.json" gui.save(canonical_file_name) new_gui = GUI('something_random') @@ -45,7 +45,7 @@ def test_multiple_scripts(self): def test_saving_and_loading(self): title = 'foo' gui = GUI(title) - canvas = gui.flow_canvas_widget + canvas = gui.flow_canvas flow = gui._session.scripts[0].flow canvas.add_node(0, 0, gui._nodes_dict['nodes']['val']) # Need to create with canvas instead of flow @@ -99,7 +99,7 @@ def update_event(self, inp=-1): gui.register_user_node(MyNode) self.assertIn(MyNode, gui.session.nodes) - gui.flow_canvas_widget.add_node(0, 0, gui._nodes_dict["user"][MyNode.title]) + gui.flow_canvas.add_node(0, 0, gui._nodes_dict["user"][MyNode.title]) gui.flow.nodes[0].inputs[0].update(1) self.assertEqual(gui.flow.nodes[0].outputs[0].val, 43) @@ -120,7 +120,7 @@ def update_event(self, inp=-1): gui.flow.nodes[0].inputs[0].update(2) self.assertEqual(gui.flow.nodes[0].outputs[0].val, 44, msg="Expected to be using instance of old class") - gui.flow_canvas_widget.add_node(1, 1, gui._nodes_dict["user"][MyNode.title]) + gui.flow_canvas.add_node(1, 1, gui._nodes_dict["user"][MyNode.title]) gui.flow.nodes[1].inputs[0].update(2) self.assertEqual(gui.flow.nodes[1].outputs[0].val, -40, msg="New node instances should reflect updated class.")