2023-12-15 22:44:25 +01:00
|
|
|
# (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)
|
2023-06-12 17:32:36 +02:00
|
|
|
extends GraphEdit
|
2023-11-22 05:26:11 +01:00
|
|
|
class_name DeckRendererGraphEdit
|
2023-06-12 17:32:36 +02:00
|
|
|
|
2023-11-25 11:40:43 +01:00
|
|
|
## Reference to the [DeckNodeRendererGraphNode] used for later instantiation
|
2024-02-21 02:11:26 +01:00
|
|
|
@export var NODE_SCENE: PackedScene
|
2023-11-25 11:40:43 +01:00
|
|
|
## Reference to the [AddNodeMenu] used for later instantiation
|
2024-02-21 02:11:26 +01:00
|
|
|
@export var ADD_NODE_MENU_SCENE: PackedScene
|
2023-11-23 07:38:10 +01:00
|
|
|
|
2023-11-25 11:40:43 +01:00
|
|
|
## The [PopupPanel] that holds the [AddNodeMenu] scene.
|
2023-11-23 07:38:10 +01:00
|
|
|
var search_popup_panel: PopupPanel
|
2023-11-25 11:40:43 +01:00
|
|
|
## Stores instance of [AddNodeMenu] that is used under [member search_popup_panel]
|
2023-11-23 07:38:10 +01:00
|
|
|
var add_node_menu: AddNodeMenu
|
|
|
|
|
2023-11-25 11:40:43 +01:00
|
|
|
## Used to specify the size of [member search_popup_panel].
|
|
|
|
@export var search_popup_size: Vector2i = Vector2i(500, 300)
|
2023-11-25 12:01:17 +01:00
|
|
|
## Stores the position of the [member search_popup_panel] for use when adding
|
2023-11-25 11:40:43 +01:00
|
|
|
## nodes in [method _on_add_node_menu_node_selected]
|
2023-11-23 07:38:10 +01:00
|
|
|
var popup_position: Vector2
|
2023-06-12 17:32:36 +02:00
|
|
|
|
2023-12-15 22:44:25 +01:00
|
|
|
var rename_popup := RenamePopup.new()
|
|
|
|
@export var rename_popup_size: Vector2i = Vector2i(200, 0)
|
|
|
|
|
2023-11-25 11:40:43 +01:00
|
|
|
## References the [Deck] that holds all the functional properties of this [DeckRendererGraphEdit]
|
2023-11-22 05:26:11 +01:00
|
|
|
var deck: Deck:
|
|
|
|
set(v):
|
|
|
|
deck = v
|
|
|
|
deck.node_added.connect(_on_deck_node_added)
|
|
|
|
deck.node_removed.connect(_on_deck_node_removed)
|
2023-12-18 22:10:58 +01:00
|
|
|
deck.nodes_disconnected.connect(_on_deck_nodes_disconnected)
|
2023-11-22 05:26:11 +01:00
|
|
|
|
2023-11-25 11:40:43 +01:00
|
|
|
## Emits when Group creation is requested. Ex. Hitting the "group_nodes" Hotkey.
|
2023-11-22 05:26:11 +01:00
|
|
|
signal group_enter_requested(group_id: String)
|
|
|
|
|
2023-12-15 22:44:25 +01:00
|
|
|
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
|
|
|
|
|
2023-11-25 11:40:43 +01:00
|
|
|
## Sets up the [member search_popup_panel] with an instance of [member ADD_NODE_SCENE]
|
2023-11-25 12:01:17 +01:00
|
|
|
## stored in [member add_node_menu]. And sets its size of [member search_popup_panel] to
|
2023-11-25 11:40:43 +01:00
|
|
|
## [member add_node_popup_size]
|
2023-06-12 17:32:36 +02:00
|
|
|
func _ready() -> void:
|
2023-11-23 07:38:10 +01:00
|
|
|
add_node_menu = ADD_NODE_MENU_SCENE.instantiate()
|
|
|
|
search_popup_panel = PopupPanel.new()
|
|
|
|
search_popup_panel.add_child(add_node_menu)
|
2023-11-25 11:40:43 +01:00
|
|
|
search_popup_panel.size = search_popup_size
|
2023-11-23 07:38:10 +01:00
|
|
|
add_child(search_popup_panel, false, Node.INTERNAL_MODE_BACK)
|
2023-06-12 17:32:36 +02:00
|
|
|
|
2023-12-15 22:44:25 +01:00
|
|
|
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)
|
|
|
|
|
2023-11-26 23:07:15 +01:00
|
|
|
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)
|
|
|
|
|
2023-11-23 07:38:10 +01:00
|
|
|
add_node_menu.node_selected.connect(_on_add_node_menu_node_selected)
|
2023-06-13 17:23:10 +02:00
|
|
|
|
2023-06-12 17:59:30 +02:00
|
|
|
connection_request.connect(attempt_connection)
|
2023-06-13 15:06:17 +02:00
|
|
|
disconnection_request.connect(attempt_disconnect)
|
2023-06-12 17:59:30 +02:00
|
|
|
|
2023-11-25 12:01:17 +01:00
|
|
|
## Receives [signal GraphEdit.connection_request] and attempts to create a
|
2023-11-25 11:40:43 +01:00
|
|
|
## connection between the two [DeckNode]s involved, utilizes [NodeDB] for accessing them.
|
2023-06-12 17:59:30 +02:00
|
|
|
func attempt_connection(from_node_name: StringName, from_port: int, to_node_name: StringName, to_port: int) -> void:
|
2023-11-22 05:26:11 +01:00
|
|
|
var from_node_renderer: DeckNodeRendererGraphNode = get_node(NodePath(from_node_name))
|
|
|
|
var to_node_renderer: DeckNodeRendererGraphNode = get_node(NodePath(to_node_name))
|
2023-06-12 17:59:30 +02:00
|
|
|
|
2023-11-22 05:26:11 +01:00
|
|
|
#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)
|
2023-06-12 17:59:30 +02:00
|
|
|
|
2023-12-15 22:44:25 +01:00
|
|
|
if deck.connect_nodes(from_node_renderer.node._id, to_node_renderer.node._id, from_port, to_port):
|
2023-11-22 05:26:11 +01:00
|
|
|
connect_node(
|
|
|
|
from_node_renderer.name,
|
|
|
|
from_port,
|
|
|
|
to_node_renderer.name,
|
|
|
|
to_port
|
|
|
|
)
|
2023-12-15 22:44:25 +01:00
|
|
|
dirty = true
|
2023-06-13 15:06:17 +02:00
|
|
|
|
2024-02-21 05:08:36 +01:00
|
|
|
|
|
|
|
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)
|
|
|
|
|
2023-11-25 12:01:17 +01:00
|
|
|
## Receives [signal GraphEdit.disconnection_request] and attempts to disconnect the two [DeckNode]s
|
2023-11-25 11:40:43 +01:00
|
|
|
## involved, utilizes [NodeDB] for accessing them.
|
2023-06-13 15:06:17 +02:00
|
|
|
func attempt_disconnect(from_node_name: StringName, from_port: int, to_node_name: StringName, to_port: int) -> void:
|
2023-11-22 05:26:11 +01:00
|
|
|
var from_node_renderer: DeckNodeRendererGraphNode = get_node(NodePath(from_node_name))
|
|
|
|
var to_node_renderer: DeckNodeRendererGraphNode = get_node(NodePath(to_node_name))
|
2023-06-13 15:06:17 +02:00
|
|
|
|
2023-11-22 05:26:11 +01:00
|
|
|
#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)
|
2023-06-13 15:06:17 +02:00
|
|
|
|
2023-12-15 22:44:25 +01:00
|
|
|
deck.disconnect_nodes(from_node_renderer.node._id, to_node_renderer.node._id, from_port, to_port)
|
2023-06-13 15:06:17 +02:00
|
|
|
|
2023-11-22 05:26:11 +01:00
|
|
|
disconnect_node(
|
|
|
|
from_node_renderer.name,
|
|
|
|
from_port,
|
|
|
|
to_node_renderer.name,
|
|
|
|
to_port
|
|
|
|
)
|
2023-12-15 22:44:25 +01:00
|
|
|
dirty = true
|
2023-06-24 05:39:50 +02:00
|
|
|
|
2023-11-25 12:01:17 +01:00
|
|
|
## Returns the associated [DeckNodeRendererGraphNode] for the supplied [DeckNode].
|
2023-11-25 11:40:43 +01:00
|
|
|
## Or [code]null[/code] if none is found.
|
2023-11-23 07:38:10 +01:00
|
|
|
func get_node_renderer(node: DeckNode) -> DeckNodeRendererGraphNode:
|
2023-11-26 22:39:53 +01:00
|
|
|
for i: DeckNodeRendererGraphNode in get_children():
|
2023-11-23 07:38:10 +01:00
|
|
|
if i.node == node:
|
|
|
|
return i
|
|
|
|
|
|
|
|
return null
|
|
|
|
|
2023-11-25 11:40:43 +01:00
|
|
|
## Updates [member Deck]s meta property "offset" whenever [member GraphEdit.scroll_offset]
|
2023-06-24 05:39:50 +02:00
|
|
|
func _on_scroll_offset_changed(offset: Vector2) -> void:
|
|
|
|
deck.set_meta("offset", offset)
|
2023-07-21 10:10:24 +02:00
|
|
|
|
2023-11-25 11:40:43 +01:00
|
|
|
## Setups all the data from the set [member deck] in this [DeckRendererGraphEdit]
|
2023-11-22 05:26:11 +01:00
|
|
|
func initialize_from_deck() -> void:
|
2023-12-15 22:44:25 +01:00
|
|
|
change_dirty = false
|
2023-11-26 22:39:53 +01:00
|
|
|
for i in get_children():
|
2023-11-22 05:26:11 +01:00
|
|
|
i.queue_free()
|
|
|
|
|
|
|
|
scroll_offset = deck.get_meta("offset", Vector2())
|
2023-12-15 22:44:25 +01:00
|
|
|
is_group = deck.is_group
|
2023-11-22 05:26:11 +01:00
|
|
|
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()
|
2023-12-15 22:44:25 +01:00
|
|
|
change_dirty = true
|
|
|
|
dirty = false
|
2023-11-22 05:26:11 +01:00
|
|
|
|
|
|
|
refresh_connections()
|
|
|
|
|
2023-11-25 12:01:17 +01:00
|
|
|
## Loops through all [DeckNode]s in [member Deck.nodes] and calls
|
2023-11-25 11:40:43 +01:00
|
|
|
## [method GraphEdit.connect_node] for all the connections that exist in each
|
2023-11-22 05:26:11 +01:00
|
|
|
func refresh_connections() -> void:
|
|
|
|
for node_id in deck.nodes:
|
|
|
|
var node: DeckNode = deck.nodes[node_id]
|
2023-11-26 22:39:53 +01:00
|
|
|
var from_node: DeckNodeRendererGraphNode = get_children().filter(
|
2023-11-22 05:26:11 +01:00
|
|
|
func(c: DeckNodeRendererGraphNode):
|
|
|
|
return c.node._id == node_id
|
|
|
|
)[0]
|
|
|
|
|
|
|
|
for from_port in node.outgoing_connections:
|
2023-12-15 22:44:25 +01:00
|
|
|
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(
|
2023-11-22 05:26:11 +01:00
|
|
|
func(c: DeckNodeRendererGraphNode):
|
|
|
|
return c.node._id == to_node_id
|
|
|
|
)
|
2023-12-15 22:44:25 +01:00
|
|
|
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
|
|
|
|
)
|
2023-11-22 05:26:11 +01:00
|
|
|
|
2023-11-25 12:01:17 +01:00
|
|
|
## Connected to [signal Deck.node_added], used to instance the required
|
2023-11-25 11:40:43 +01:00
|
|
|
## [DeckNodeRendererGraphNode] and set it's [member DeckNodeRenderGraphNode.position_offset]
|
2023-11-22 05:26:11 +01:00
|
|
|
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()
|
2023-12-15 22:44:25 +01:00
|
|
|
dirty = true
|
|
|
|
|
2023-11-22 05:26:11 +01:00
|
|
|
|
2023-11-25 12:01:17 +01:00
|
|
|
## Connected to [signal Deck.node_added], used to remove the specified
|
2023-11-25 11:40:43 +01:00
|
|
|
## [DeckNodeRendererGraphNode] and queue_free it.
|
2023-11-22 05:26:11 +01:00
|
|
|
func _on_deck_node_removed(node: DeckNode) -> void:
|
2023-11-26 22:39:53 +01:00
|
|
|
for renderer: DeckNodeRendererGraphNode in get_children():
|
2023-11-22 05:26:11 +01:00
|
|
|
if renderer.node != node:
|
|
|
|
continue
|
2024-03-15 07:02:04 +01:00
|
|
|
# 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.
|
2023-11-22 05:26:11 +01:00
|
|
|
renderer.queue_free()
|
|
|
|
break
|
2023-12-15 22:44:25 +01:00
|
|
|
dirty = true
|
2023-11-22 05:26:11 +01:00
|
|
|
|
2023-11-25 11:40:43 +01:00
|
|
|
## Utility function that gets all [DeckNodeRenderGraphNode]s that are selected
|
|
|
|
## See [member GraphNode.selected]
|
2023-11-22 05:26:11 +01:00
|
|
|
func get_selected_nodes() -> Array:
|
2023-11-26 22:39:53 +01:00
|
|
|
return get_children().filter(
|
2023-11-22 05:26:11 +01:00
|
|
|
func(x: DeckNodeRendererGraphNode):
|
|
|
|
return x.selected
|
|
|
|
)
|
|
|
|
|
2023-11-25 11:40:43 +01:00
|
|
|
## Executes functionality based off hotkey inputs. Specifically handles creating groups
|
|
|
|
## based off the action "group_nodes".
|
2023-11-22 05:26:11 +01:00
|
|
|
func _gui_input(event: InputEvent) -> void:
|
2024-02-21 07:11:29 +01:00
|
|
|
if event.is_action_pressed("group_nodes") and get_selected_nodes().size() > 0:
|
2023-11-22 05:26:11 +01:00
|
|
|
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()
|
|
|
|
|
2024-02-21 07:11:29 +01:00
|
|
|
if event.is_action_pressed("rename_node") and get_selected_nodes().size() == 1:
|
2023-12-15 22:44:25 +01:00
|
|
|
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)
|
|
|
|
|
|
|
|
|
2023-11-25 12:01:17 +01:00
|
|
|
## Handles entering groups with action "enter_group". Done here to bypass neighbor
|
2023-11-25 11:40:43 +01:00
|
|
|
## functionality.
|
2023-11-22 05:26:11 +01:00
|
|
|
func _input(event: InputEvent) -> void:
|
2024-02-21 07:11:29 +01:00
|
|
|
if not has_focus():
|
2023-11-22 05:26:11 +01:00
|
|
|
return
|
|
|
|
|
2024-02-21 07:11:29 +01:00
|
|
|
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":
|
2023-11-22 05:26:11 +01:00
|
|
|
return
|
|
|
|
group_enter_requested.emit((get_selected_nodes()[0] as DeckNodeRendererGraphNode).node.group_id)
|
|
|
|
get_viewport().set_input_as_handled()
|
2023-11-23 07:38:10 +01:00
|
|
|
|
2023-12-15 22:44:25 +01:00
|
|
|
|
|
|
|
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
|
|
|
|
|
|
|
|
|
2023-11-25 11:40:43 +01:00
|
|
|
## Opens [member search_popup_panel] at the mouse position. Connected to [signal GraphEdit.popup_request]
|
2023-11-23 07:38:10 +01:00
|
|
|
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
|
|
|
|
|
2023-11-25 12:01:17 +01:00
|
|
|
## Connected to [signal AddNodeMenu.node_selected] and creates a [DeckNode] using
|
|
|
|
## [method NodeDB.instance_node]. Then placing it at the [member scroll_offset] +
|
2023-11-25 11:40:43 +01:00
|
|
|
## [member popup_position] / [member zoom]
|
2023-11-23 07:38:10 +01:00
|
|
|
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()
|
2023-12-15 22:44:25 +01:00
|
|
|
|
|
|
|
|
2023-12-18 22:10:58 +01:00
|
|
|
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)
|
|
|
|
|
|
|
|
|
2023-12-15 22:44:25 +01:00
|
|
|
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()
|