Skip to content

Commit 9f7763d

Browse files
authored
Add copy/pasting of nodes to/from clipboard in JSON (#560)
1 parent 59c78ae commit 9f7763d

File tree

3 files changed

+126
-8
lines changed

3 files changed

+126
-8
lines changed

addons/gaea/editor/graph_editor/graph_edit.gd

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22
class_name GaeaGraphEdit
33
extends GraphEdit
44

5+
signal copy_to_clipboard_request()
6+
signal paste_from_clipboard_request()
7+
58
@export var main_editor: GaeaMainEditor
69
@export var bottom_note_label: RichTextLabel
710

@@ -53,6 +56,9 @@ func _ready() -> void:
5356
EditorInterface.get_script_editor().editor_script_changed.connect(_on_editor_script_changed)
5457
_add_toolbar_buttons()
5558

59+
copy_to_clipboard_request.connect(_on_copy_from_clipboard_request)
60+
paste_from_clipboard_request.connect(_on_paste_from_clipboard_request)
61+
5662

5763
#region Saving and Loading
5864
func populate(new_graph: GaeaGraph) -> void:
@@ -315,10 +321,12 @@ func delete_nodes(nodes: Array[StringName]) -> void:
315321
update_connections()
316322

317323

318-
func get_selected() -> Array[Node]:
319-
return get_children().filter(
320-
func(child: Node) -> bool: return child is GraphElement and child.selected
321-
)
324+
func get_selected() -> Array[GraphElement]:
325+
var selected: Array[GraphElement] = []
326+
for child: Node in get_children():
327+
if child is GraphElement and child.selected:
328+
selected.append(child)
329+
return selected
322330

323331

324332
func get_selected_names() -> Array[StringName]:
@@ -694,7 +702,7 @@ func _paste_nodes(at_position: Vector2, data: GaeaNodesCopy = copy_buffer) -> vo
694702
_load_connections.call_deferred(new_connections)
695703

696704

697-
func _get_copy_data(nodes: Array) -> GaeaNodesCopy:
705+
func _get_copy_data(nodes: Array[GraphElement]) -> GaeaNodesCopy:
698706
var copy_data: GaeaNodesCopy = GaeaNodesCopy.new()
699707
for selected in nodes:
700708
if selected is GaeaGraphNode:
@@ -814,3 +822,16 @@ func _on_main_editor_visibility_changed() -> void:
814822
set_connection_lines_thickness(GaeaEditorSettings.get_line_thickness())
815823
set_minimap_opacity(GaeaEditorSettings.get_minimap_opacity())
816824
#endregion
825+
826+
827+
func _on_copy_from_clipboard_request() -> void:
828+
var copy_data: GaeaNodesCopy = _get_copy_data(get_selected())
829+
DisplayServer.clipboard_set(copy_data.serialize())
830+
831+
832+
func _on_paste_from_clipboard_request() -> void:
833+
var copy_data: Variant = GaeaNodesCopy.deserialize(DisplayServer.clipboard_get())
834+
if copy_data is GaeaNodesCopy:
835+
_paste_nodes(local_to_grid(get_local_mouse_position()), copy_data)
836+
elif copy_data is String:
837+
EditorInterface.get_editor_toaster().push_toast(copy_data, EditorToaster.SEVERITY_ERROR)

addons/gaea/editor/graph_editor/node_context_menu.gd

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,9 @@ enum Action {
1616
GROUP_IN_FRAME,
1717
DETACH,
1818
ENABLE_AUTO_SHRINK,
19-
OPEN_IN_INSPECTOR
19+
OPEN_IN_INSPECTOR,
20+
COPY_TO_CLIPBOARD,
21+
PASTE_FROM_CLIPBOARD,
2022
}
2123

2224
@export var main_editor: GaeaMainEditor
@@ -40,9 +42,14 @@ func populate(selected: Array) -> void:
4042
add_item("Delete", Action.DELETE)
4143
add_item("Clear Copy Buffer", Action.CLEAR_BUFFER)
4244

45+
add_separator()
46+
add_item("Copy to Clipboard", Action.COPY_TO_CLIPBOARD)
47+
add_item("Paste from Clipboard", Action.PASTE_FROM_CLIPBOARD)
48+
4349
if not is_instance_valid(graph_edit.copy_buffer):
4450
set_item_disabled(get_item_index(Action.PASTE), true)
4551
set_item_disabled(get_item_index(Action.CLEAR_BUFFER), true)
52+
4653
if not selected.is_empty():
4754
add_separator()
4855
add_item("Group in New Frame", Action.GROUP_IN_FRAME)
@@ -57,6 +64,7 @@ func populate(selected: Array) -> void:
5764
set_item_disabled(get_item_index(Action.COPY), true)
5865
set_item_disabled(get_item_index(Action.CUT), true)
5966
set_item_disabled(get_item_index(Action.DELETE), true)
67+
set_item_disabled(get_item_index(Action.COPY_TO_CLIPBOARD), true)
6068
return
6169

6270
if selected.front() is GaeaGraphFrame and selected.size() == 1:
@@ -128,7 +136,10 @@ func _on_id_pressed(id: int) -> void:
128136
Action.GROUP_IN_FRAME:
129137
var selected: Array[StringName] = graph_edit.get_selected_names()
130138
_group_nodes_in_frame(selected)
131-
139+
Action.COPY_TO_CLIPBOARD:
140+
graph_edit.copy_to_clipboard_request.emit()
141+
Action.PASTE_FROM_CLIPBOARD:
142+
graph_edit.paste_from_clipboard_request.emit()
132143
Action.DETACH:
133144
var selected: Array = graph_edit.get_selected()
134145
for node: GraphElement in selected:

addons/gaea/resources/gaea_nodes_copy.gd

Lines changed: 87 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@ var _connections: Array[Dictionary] : get = get_connections
88
var _origin: Vector2 = Vector2(INF, INF) : get = get_origin
99

1010

11+
func _init(origin = Vector2(INF, INF)) -> void:
12+
_origin = origin
13+
14+
1115
func get_nodes_info() -> Dictionary[int, Dictionary]:
1216
return _nodes
1317

@@ -44,7 +48,13 @@ func add_frame(current_id: int, position: Vector2, data: Dictionary) -> void:
4448

4549

4650
func add_connections(connections: Array[Dictionary]) -> void:
47-
_connections.append_array(connections)
51+
for connection in connections:
52+
add_connection(connection)
53+
54+
55+
func add_connection(connection: Dictionary) -> void:
56+
if not _connections.has(connection):
57+
_connections.append(connection)
4858

4959

5060
func get_node_type(id: int) -> GaeaGraph.NodeType:
@@ -61,3 +71,79 @@ func get_node_data(id: int) -> Dictionary:
6171

6272
func get_node_position(id: int) -> Vector2:
6373
return _nodes.get(id, {}).get(&"position", get_origin())
74+
75+
76+
func serialize() -> String:
77+
var nodes_data: Dictionary[int, Array] = {}
78+
var connections: Array[String] = []
79+
80+
for node_id: int in _nodes.keys():
81+
var node_properties: Dictionary = _nodes.get(node_id, {})
82+
nodes_data.set(node_id, [
83+
node_properties.get(&"type"),
84+
node_properties.get(&"data"),
85+
])
86+
87+
for connection in _connections:
88+
if nodes_data.has(connection.from_node) and nodes_data.has(connection.to_node):
89+
connections.append("%s-%s-%s-%s" % [
90+
connection.get("from_node"),
91+
connection.get("from_port"),
92+
connection.get("to_node"),
93+
connection.get("to_port"),
94+
])
95+
96+
return JSON.stringify({
97+
"origin": _origin,
98+
"nodes": nodes_data,
99+
"connections": connections
100+
}, "", false)
101+
102+
103+
## Deserialize a previously serialized GaeaNodesCopy,
104+
## return a GaeaNodesCopy object or a string as error message.
105+
static func deserialize(serialized: String) -> Variant:
106+
var data = JSON.parse_string(serialized)
107+
108+
if (
109+
not data is Dictionary
110+
or not data.get("origin") is Vector2
111+
or not data.get("nodes") is Dictionary
112+
or not data.get("connections") is Array
113+
):
114+
return "Invalid data provided: the data could not be parsed"
115+
116+
var origin: Vector2 = data[0]
117+
var deserialized: GaeaNodesCopy = GaeaNodesCopy.new(origin)
118+
119+
var nodes_data: Dictionary = data.get("nodes")
120+
for node_id in nodes_data.keys():
121+
var node_data = nodes_data.get(node_id)
122+
if typeof(node_id) != TYPE_INT or typeof(node_data) != TYPE_ARRAY or not node_data[1] is Dictionary:
123+
return "Invalid data provided: the data could not be parsed"
124+
match node_data[0]:
125+
GaeaGraph.NodeType.NODE:
126+
var uid: String = node_data[1].get(&"uid")
127+
var resource: GaeaNodeResource
128+
if GaeaNodeResource.is_valid_node_resource(uid).is_empty():
129+
resource = load(uid).new()
130+
else:
131+
resource = GaeaNodeInvalidScript.new()
132+
resource.load_save_data(node_data[1])
133+
deserialized.add_node(node_id, resource, node_data[1].get(&"position", origin), node_data[1])
134+
GaeaGraph.NodeType.FRAME:
135+
deserialized.add_frame(node_id, node_data[1].get(&"position", origin), node_data[1])
136+
_:
137+
return "Invalid data provided: the data could not be parsed"
138+
139+
var connections: Array = nodes_data.get("connections")
140+
@warning_ignore("integer_division")
141+
for connection_string: String in connections:
142+
var split_string := connection_string.split("-")
143+
deserialized.add_connection({
144+
"from_node": int(split_string[0]),
145+
"from_port": int(split_string[1]),
146+
"to_node": int(split_string[2]),
147+
"to_port": int(split_string[3])
148+
})
149+
return deserialized

0 commit comments

Comments
 (0)