miggor-StreamGraph/graph_node_renderer/deck_renderer_graph_edit.gd
Lera Elvoé a518e46b0f add a method to make group instances unique, making them independent (#143)
closes #97

when copying group nodes across decks (including in and out of groups), they become unique and completely independent copies of the original. this is done recursively, so in the case of copying:

- group X
	- contained in Deck A
	- has another group Z

into Deck B, group X will become group Y, group Z will become group W.

there is a rare bug that will sometimes cause the deck to save with no groups at all, which i haven't been able to hunt down and don't know how to replicate at the moment.

Reviewed-on: https://codeberg.org/StreamGraph/StreamGraph/pulls/143
Co-authored-by: Lera Elvoé <yagich@poto.cafe>
Co-committed-by: Lera Elvoé <yagich@poto.cafe>
2024-04-11 14:56:33 +00:00

391 lines
13 KiB
GDScript

# (c) 2023-present Eroax
# (c) 2023-present Yagich
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
extends GraphEdit
class_name DeckRendererGraphEdit
## Reference to the [DeckNodeRendererGraphNode] used for later instantiation
@export var NODE_SCENE: PackedScene
## Reference to the [AddNodeMenu] used for later instantiation
@export var ADD_NODE_MENU_SCENE: PackedScene
## The [PopupPanel] that holds the [AddNodeMenu] scene.
var search_popup_panel: PopupPanel
## Stores instance of [AddNodeMenu] that is used under [member search_popup_panel]
var add_node_menu: AddNodeMenu
## Used to specify the size of [member search_popup_panel].
@export var search_popup_size: Vector2i = Vector2i(500, 300)
## Stores the position of the [member search_popup_panel] for use when adding
## nodes in [method _on_add_node_menu_node_selected]
var popup_position: Vector2
var rename_popup := RenamePopup.new()
@export var rename_popup_size: Vector2i = Vector2i(200, 0)
## References the [Deck] that holds all the functional properties of this [DeckRendererGraphEdit]
var deck: Deck:
set(v):
deck = v
deck.node_added.connect(_on_deck_node_added)
deck.node_removed.connect(_on_deck_node_removed)
deck.nodes_disconnected.connect(_on_deck_nodes_disconnected)
## Emits when Group creation is requested. Ex. Hitting the "group_nodes" Hotkey.
signal group_enter_requested(group_id: String)
var dirty: bool = false:
set(v):
if change_dirty:
dirty = v
dirty_state_changed.emit()
var is_group: bool = false
var change_dirty: bool = true
signal dirty_state_changed
## Sets up the [member search_popup_panel] with an instance of [member ADD_NODE_SCENE]
## stored in [member add_node_menu]. And sets its size of [member search_popup_panel] to
## [member add_node_popup_size]
func _ready() -> void:
add_node_menu = ADD_NODE_MENU_SCENE.instantiate()
search_popup_panel = PopupPanel.new()
search_popup_panel.add_child(add_node_menu)
search_popup_panel.size = search_popup_size
add_child(search_popup_panel, false, Node.INTERNAL_MODE_BACK)
rename_popup = RenamePopup.new()
rename_popup.rename_confirmed.connect(_on_rename_popup_rename_confirmed)
rename_popup.close_requested.connect(_on_rename_popup_closed)
add_child(rename_popup, false, Node.INTERNAL_MODE_FRONT)
for t: DeckType.Types in DeckType.CONVERSION_MAP:
for out_type: DeckType.Types in DeckType.CONVERSION_MAP[t]:
add_valid_connection_type(t, out_type)
add_node_menu.node_selected.connect(_on_add_node_menu_node_selected)
connection_request.connect(attempt_connection)
disconnection_request.connect(attempt_disconnect)
## Receives [signal GraphEdit.connection_request] and attempts to create a
## connection between the two [DeckNode]s involved, utilizes [NodeDB] for accessing them.
func attempt_connection(from_node_name: StringName, from_port: int, to_node_name: StringName, to_port: int) -> void:
var from_node_renderer: DeckNodeRendererGraphNode = get_node(NodePath(from_node_name))
var to_node_renderer: DeckNodeRendererGraphNode = get_node(NodePath(to_node_name))
#var from_output := from_node_renderer.node.get_global_port_idx_from_output(from_port)
#var to_input := to_node_renderer.node.get_global_port_idx_from_input(to_port)
if deck.connect_nodes(from_node_renderer.node._id, to_node_renderer.node._id, from_port, to_port):
connect_node(
from_node_renderer.name,
from_port,
to_node_renderer.name,
to_port
)
dirty = true
func _is_node_hover_valid(from_node: StringName, from_port: int, to_node: StringName, to_port: int) -> bool:
var from_node_renderer: DeckNodeRendererGraphNode = get_node(NodePath(from_node))
var to_node_renderer: DeckNodeRendererGraphNode = get_node(NodePath(to_node))
return deck.is_valid_connection(from_node_renderer.node._id, to_node_renderer.node._id, from_port, to_port)
## Receives [signal GraphEdit.disconnection_request] and attempts to disconnect the two [DeckNode]s
## involved, utilizes [NodeDB] for accessing them.
func attempt_disconnect(from_node_name: StringName, from_port: int, to_node_name: StringName, to_port: int) -> void:
var from_node_renderer: DeckNodeRendererGraphNode = get_node(NodePath(from_node_name))
var to_node_renderer: DeckNodeRendererGraphNode = get_node(NodePath(to_node_name))
#var from_output := from_node_renderer.node.get_global_port_idx_from_output(from_port)
#var to_input := to_node_renderer.node.get_global_port_idx_from_input(to_port)
deck.disconnect_nodes(from_node_renderer.node._id, to_node_renderer.node._id, from_port, to_port)
disconnect_node(
from_node_renderer.name,
from_port,
to_node_renderer.name,
to_port
)
dirty = true
## Returns the associated [DeckNodeRendererGraphNode] for the supplied [DeckNode].
## Or [code]null[/code] if none is found.
func get_node_renderer(node: DeckNode) -> DeckNodeRendererGraphNode:
for i: DeckNodeRendererGraphNode in get_children():
if i.node == node:
return i
return null
## Updates [member Deck]s meta property "offset" whenever [member GraphEdit.scroll_offset]
func _on_scroll_offset_changed(offset: Vector2) -> void:
deck.set_meta("offset", offset)
## Setups all the data from the set [member deck] in this [DeckRendererGraphEdit]
func initialize_from_deck() -> void:
change_dirty = false
for i in get_children():
i.queue_free()
is_group = deck.is_group
for node_id in deck.nodes:
var node_renderer: DeckNodeRendererGraphNode = NODE_SCENE.instantiate()
node_renderer.node = deck.nodes[node_id]
add_child(node_renderer)
node_renderer.position_offset = node_renderer.node.position_as_vector2()
change_dirty = true
dirty = false
refresh_connections()
var ofs = deck.get_meta("offset", Vector2())
set_scroll_offset.call_deferred(ofs)
## Loops through all [DeckNode]s in [member Deck.nodes] and calls
## [method GraphEdit.connect_node] for all the connections that exist in each
func refresh_connections() -> void:
for node_id in deck.nodes:
var node: DeckNode = deck.nodes[node_id]
var from_node: DeckNodeRendererGraphNode = get_children().filter(
func(c: DeckNodeRendererGraphNode):
return c.node._id == node_id
)[0]
for from_port in node.outgoing_connections:
for to_node_id: String in node.outgoing_connections[from_port]:
var to_node_ports = node.outgoing_connections[from_port][to_node_id]
var renderer: Array = get_children().filter(
func(c: DeckNodeRendererGraphNode):
return c.node._id == to_node_id
)
if renderer.is_empty():
break
var to_node: DeckNodeRendererGraphNode = renderer[0]
for to_node_port: int in to_node_ports:
connect_node(
from_node.name,
from_port,
to_node.name,
to_node_port
)
## Connected to [signal Deck.node_added], used to instance the required
## [DeckNodeRendererGraphNode] and set it's [member DeckNodeRenderGraphNode.position_offset]
func _on_deck_node_added(node: DeckNode) -> void:
var inst: DeckNodeRendererGraphNode = NODE_SCENE.instantiate()
inst.node = node
add_child(inst)
inst.position_offset = inst.node.position_as_vector2()
dirty = true
## Connected to [signal Deck.node_added], used to remove the specified
## [DeckNodeRendererGraphNode] and queue_free it.
func _on_deck_node_removed(node: DeckNode) -> void:
for renderer: DeckNodeRendererGraphNode in get_children():
if renderer.node != node:
continue
# TODO: when multiple nodes are removed and they are connected, the renderer will break
# trying to get an invalid node. (GraphEdit, this is not on us.)
# consider a batch removed signal for Deck or a separate signal for grouping nodes in 0.0.6.
renderer.queue_free()
break
dirty = true
## Utility function that gets all [DeckNodeRenderGraphNode]s that are selected
## See [member GraphNode.selected]
func get_selected_nodes() -> Array:
return get_children().filter(
func(x: DeckNodeRendererGraphNode):
return x.selected
)
## Executes functionality based off hotkey inputs. Specifically handles creating groups
## based off the action "group_nodes".
func _gui_input(event: InputEvent) -> void:
if event.is_action_pressed("group_nodes") and get_selected_nodes().size() > 0:
clear_connections()
var nodes = get_selected_nodes().map(
func(x: DeckNodeRendererGraphNode):
return x.node
)
deck.group_nodes(nodes)
refresh_connections()
get_viewport().set_input_as_handled()
if event.is_action_pressed("debug_make_unique") and get_selected_nodes().size() == 1:
var node: DeckNode = get_selected_nodes().map(
func(x: DeckNodeRendererGraphNode):
return x.node
)[0]
if node.node_type != "group_node":
return
node.make_unique()
if event.is_action_pressed("rename_node") and get_selected_nodes().size() == 1:
var node: DeckNodeRendererGraphNode = get_selected_nodes()[0]
var pos := get_viewport_rect().position + get_global_mouse_position()
rename_popup.popup_on_parent(Rect2i(pos, rename_popup_size))
rename_popup.le.size.x = rename_popup_size.x
rename_popup.set_text(node.title)
## Handles entering groups with action "enter_group". Done here to bypass neighbor
## functionality.
func _input(event: InputEvent) -> void:
if not has_focus():
return
if event.is_action_pressed("enter_group") and get_selected_nodes().size() == 1:
if (get_selected_nodes()[0] as DeckNodeRendererGraphNode).node.node_type != "group_node":
return
group_enter_requested.emit((get_selected_nodes()[0] as DeckNodeRendererGraphNode).node.group_id)
get_viewport().set_input_as_handled()
func _on_rename_popup_rename_confirmed(new_name: String) -> void:
var node: DeckNodeRendererGraphNode = get_selected_nodes()[0]
node.title = new_name
node.node.rename(new_name)
dirty = true
func _on_rename_popup_closed() -> void:
pass
## Opens [member search_popup_panel] at the mouse position. Connected to [signal GraphEdit.popup_request]
func _on_popup_request(p_popup_position: Vector2) -> void:
var p := get_viewport_rect().position + get_global_mouse_position()
p += Vector2(10, 10)
var r := Rect2i(p, search_popup_panel.size)
search_popup_panel.popup_on_parent(r)
add_node_menu.focus_search_bar()
popup_position = p_popup_position
## Connected to [signal AddNodeMenu.node_selected] and creates a [DeckNode] using
## [method NodeDB.instance_node]. Then placing it at the [member scroll_offset] +
## [member popup_position] / [member zoom]
func _on_add_node_menu_node_selected(type: String) -> void:
var node := NodeDB.instance_node(type) as DeckNode
deck.add_node_inst(node)
var node_pos := ((scroll_offset + popup_position) / zoom)
if snapping_enabled:
node_pos = node_pos.snapped(Vector2(snapping_distance, snapping_distance))
get_node_renderer(node).position_offset = node_pos
search_popup_panel.hide()
func _on_deck_nodes_disconnected(from_node_id: String, to_node_id: String, from_output_port: int, to_input_port: int) -> void:
print("1")
var from_node: DeckNodeRendererGraphNode = get_children().filter(
func(x: DeckNodeRendererGraphNode):
return x.node._id == from_node_id
)[0]
var to_node: DeckNodeRendererGraphNode = get_children().filter(
func(x: DeckNodeRendererGraphNode):
return x.node._id == to_node_id
)[0]
print(is_node_connected(from_node.name, from_output_port, to_node.name, to_input_port))
disconnect_node(from_node.name, from_output_port, to_node.name, to_input_port)
func _on_delete_nodes_request(nodes: Array[StringName]) -> void:
var node_ids := nodes.map(
func(n: StringName):
return (get_node(NodePath(n)) as DeckNodeRendererGraphNode).node._id
)
clear_connections()
if node_ids.is_empty():
return
for node_id in node_ids:
deck.remove_node(node_id, true)
refresh_connections()
dirty = true
func _on_copy_nodes_request() -> void:
var selected := get_selected_nodes()
if selected.is_empty():
return
var selected_ids: Array[String] = []
selected_ids.assign(selected.map(
func(x: DeckNodeRendererGraphNode):
return x.node._id
))
DisplayServer.clipboard_set(deck.copy_nodes_json(selected_ids))
func _on_paste_nodes_request() -> void:
var clip := DisplayServer.clipboard_get()
var node_pos := (get_local_mouse_position() + scroll_offset - (Vector2(75.0, 0.0) * zoom)) / zoom
if snapping_enabled:
node_pos = node_pos.snapped(Vector2(snapping_distance, snapping_distance))
clear_connections()
deck.paste_nodes_from_json(clip, node_pos)
refresh_connections()
dirty = true
func _on_duplicate_nodes_request() -> void:
var selected := get_selected_nodes()
if selected.is_empty():
return
var selected_ids: Array[String] = []
selected_ids.assign(selected.map(
func(x: DeckNodeRendererGraphNode):
return x.node._id
))
clear_connections()
deck.duplicate_nodes(selected_ids)
refresh_connections()
dirty = true
class RenamePopup extends Popup:
var le := LineEdit.new()
signal rename_confirmed(new_text: String)
func _ready() -> void:
add_child(le)
le.placeholder_text = "New Name"
le.text_submitted.connect(
func(new_text: String):
if new_text.is_empty():
return
rename_confirmed.emit(new_text)
hide()
)
func set_text(text: String) -> void:
le.text = text
le.select_all()
le.grab_focus()