diff --git a/addons/gaea/editor/graph_editor/graph_edit.gd b/addons/gaea/editor/graph_editor/graph_edit.gd index dcfc274d..b598dcf1 100644 --- a/addons/gaea/editor/graph_editor/graph_edit.gd +++ b/addons/gaea/editor/graph_editor/graph_edit.gd @@ -2,6 +2,9 @@ class_name GaeaGraphEdit extends GraphEdit +signal copy_to_clipboard_request() +signal paste_from_clipboard_request() + @export var main_editor: GaeaMainEditor @export var bottom_note_label: RichTextLabel @@ -53,6 +56,9 @@ func _ready() -> void: EditorInterface.get_script_editor().editor_script_changed.connect(_on_editor_script_changed) _add_toolbar_buttons() + copy_to_clipboard_request.connect(_on_copy_from_clipboard_request) + paste_from_clipboard_request.connect(_on_paste_from_clipboard_request) + #region Saving and Loading func populate(new_graph: GaeaGraph) -> void: @@ -315,10 +321,12 @@ func delete_nodes(nodes: Array[StringName]) -> void: update_connections() -func get_selected() -> Array[Node]: - return get_children().filter( - func(child: Node) -> bool: return child is GraphElement and child.selected - ) +func get_selected() -> Array[GraphElement]: + var selected: Array[GraphElement] = [] + for child: Node in get_children(): + if child is GraphElement and child.selected: + selected.append(child) + return selected func get_selected_names() -> Array[StringName]: @@ -694,7 +702,7 @@ func _paste_nodes(at_position: Vector2, data: GaeaNodesCopy = copy_buffer) -> vo _load_connections.call_deferred(new_connections) -func _get_copy_data(nodes: Array) -> GaeaNodesCopy: +func _get_copy_data(nodes: Array[GraphElement]) -> GaeaNodesCopy: var copy_data: GaeaNodesCopy = GaeaNodesCopy.new() for selected in nodes: if selected is GaeaGraphNode: @@ -814,3 +822,16 @@ func _on_main_editor_visibility_changed() -> void: set_connection_lines_thickness(GaeaEditorSettings.get_line_thickness()) set_minimap_opacity(GaeaEditorSettings.get_minimap_opacity()) #endregion + + +func _on_copy_from_clipboard_request() -> void: + var copy_data: GaeaNodesCopy = _get_copy_data(get_selected()) + DisplayServer.clipboard_set(copy_data.serialize()) + + +func _on_paste_from_clipboard_request() -> void: + var copy_data: Variant = GaeaNodesCopy.deserialize(DisplayServer.clipboard_get()) + if copy_data is GaeaNodesCopy: + _paste_nodes(local_to_grid(get_local_mouse_position()), copy_data) + elif copy_data is String: + EditorInterface.get_editor_toaster().push_toast(copy_data, EditorToaster.SEVERITY_ERROR) diff --git a/addons/gaea/editor/graph_editor/node_context_menu.gd b/addons/gaea/editor/graph_editor/node_context_menu.gd index 5f35253e..8e52e80c 100644 --- a/addons/gaea/editor/graph_editor/node_context_menu.gd +++ b/addons/gaea/editor/graph_editor/node_context_menu.gd @@ -16,7 +16,9 @@ enum Action { GROUP_IN_FRAME, DETACH, ENABLE_AUTO_SHRINK, - OPEN_IN_INSPECTOR + OPEN_IN_INSPECTOR, + COPY_TO_CLIPBOARD, + PASTE_FROM_CLIPBOARD, } @export var main_editor: GaeaMainEditor @@ -40,9 +42,14 @@ func populate(selected: Array) -> void: add_item("Delete", Action.DELETE) add_item("Clear Copy Buffer", Action.CLEAR_BUFFER) + add_separator() + add_item("Copy to Clipboard", Action.COPY_TO_CLIPBOARD) + add_item("Paste from Clipboard", Action.PASTE_FROM_CLIPBOARD) + if not is_instance_valid(graph_edit.copy_buffer): set_item_disabled(get_item_index(Action.PASTE), true) set_item_disabled(get_item_index(Action.CLEAR_BUFFER), true) + if not selected.is_empty(): add_separator() add_item("Group in New Frame", Action.GROUP_IN_FRAME) @@ -57,6 +64,7 @@ func populate(selected: Array) -> void: set_item_disabled(get_item_index(Action.COPY), true) set_item_disabled(get_item_index(Action.CUT), true) set_item_disabled(get_item_index(Action.DELETE), true) + set_item_disabled(get_item_index(Action.COPY_TO_CLIPBOARD), true) return if selected.front() is GaeaGraphFrame and selected.size() == 1: @@ -128,7 +136,10 @@ func _on_id_pressed(id: int) -> void: Action.GROUP_IN_FRAME: var selected: Array[StringName] = graph_edit.get_selected_names() _group_nodes_in_frame(selected) - + Action.COPY_TO_CLIPBOARD: + graph_edit.copy_to_clipboard_request.emit() + Action.PASTE_FROM_CLIPBOARD: + graph_edit.paste_from_clipboard_request.emit() Action.DETACH: var selected: Array = graph_edit.get_selected() for node: GraphElement in selected: diff --git a/addons/gaea/resources/gaea_nodes_copy.gd b/addons/gaea/resources/gaea_nodes_copy.gd index caaf9040..f506b7b8 100644 --- a/addons/gaea/resources/gaea_nodes_copy.gd +++ b/addons/gaea/resources/gaea_nodes_copy.gd @@ -8,6 +8,10 @@ var _connections: Array[Dictionary] : get = get_connections var _origin: Vector2 = Vector2(INF, INF) : get = get_origin +func _init(origin = Vector2(INF, INF)) -> void: + _origin = origin + + func get_nodes_info() -> Dictionary[int, Dictionary]: return _nodes @@ -44,7 +48,13 @@ func add_frame(current_id: int, position: Vector2, data: Dictionary) -> void: func add_connections(connections: Array[Dictionary]) -> void: - _connections.append_array(connections) + for connection in connections: + add_connection(connection) + + +func add_connection(connection: Dictionary) -> void: + if not _connections.has(connection): + _connections.append(connection) func get_node_type(id: int) -> GaeaGraph.NodeType: @@ -61,3 +71,79 @@ func get_node_data(id: int) -> Dictionary: func get_node_position(id: int) -> Vector2: return _nodes.get(id, {}).get(&"position", get_origin()) + + +func serialize() -> String: + var nodes_data: Dictionary[int, Array] = {} + var connections: Array[String] = [] + + for node_id: int in _nodes.keys(): + var node_properties: Dictionary = _nodes.get(node_id, {}) + nodes_data.set(node_id, [ + node_properties.get(&"type"), + node_properties.get(&"data"), + ]) + + for connection in _connections: + if nodes_data.has(connection.from_node) and nodes_data.has(connection.to_node): + connections.append("%s-%s-%s-%s" % [ + connection.get("from_node"), + connection.get("from_port"), + connection.get("to_node"), + connection.get("to_port"), + ]) + + return JSON.stringify({ + "origin": _origin, + "nodes": nodes_data, + "connections": connections + }, "", false) + + +## Deserialize a previously serialized GaeaNodesCopy, +## return a GaeaNodesCopy object or a string as error message. +static func deserialize(serialized: String) -> Variant: + var data = JSON.parse_string(serialized) + + if ( + not data is Dictionary + or not data.get("origin") is Vector2 + or not data.get("nodes") is Dictionary + or not data.get("connections") is Array + ): + return "Invalid data provided: the data could not be parsed" + + var origin: Vector2 = data[0] + var deserialized: GaeaNodesCopy = GaeaNodesCopy.new(origin) + + var nodes_data: Dictionary = data.get("nodes") + for node_id in nodes_data.keys(): + var node_data = nodes_data.get(node_id) + if typeof(node_id) != TYPE_INT or typeof(node_data) != TYPE_ARRAY or not node_data[1] is Dictionary: + return "Invalid data provided: the data could not be parsed" + match node_data[0]: + GaeaGraph.NodeType.NODE: + var uid: String = node_data[1].get(&"uid") + var resource: GaeaNodeResource + if GaeaNodeResource.is_valid_node_resource(uid).is_empty(): + resource = load(uid).new() + else: + resource = GaeaNodeInvalidScript.new() + resource.load_save_data(node_data[1]) + deserialized.add_node(node_id, resource, node_data[1].get(&"position", origin), node_data[1]) + GaeaGraph.NodeType.FRAME: + deserialized.add_frame(node_id, node_data[1].get(&"position", origin), node_data[1]) + _: + return "Invalid data provided: the data could not be parsed" + + var connections: Array = nodes_data.get("connections") + @warning_ignore("integer_division") + for connection_string: String in connections: + var split_string := connection_string.split("-") + deserialized.add_connection({ + "from_node": int(split_string[0]), + "from_port": int(split_string[1]), + "to_node": int(split_string[2]), + "to_port": int(split_string[3]) + }) + return deserialized