diff --git a/.gitignore b/.gitignore
index c62d003..313e7a2 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,3 +3,6 @@
# distribution folder
dist/*/
+
+# vscode folder
+.vscode/
\ No newline at end of file
diff --git a/classes/deck/deck.gd b/classes/deck/deck.gd
index 9a322b2..461a0af 100644
--- a/classes/deck/deck.gd
+++ b/classes/deck/deck.gd
@@ -1,6 +1,7 @@
# (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 Object
class_name Deck
## A deck/graph with nodes.
##
@@ -19,7 +20,20 @@ var save_path: String = ""
## The connections graph.
var connections := NodeConnections.new()
+## Whether this deck is a group instance.
var is_group: bool = false
+## Whether this deck is a library. Implies [member is_group].
+var is_library: bool = false
+
+#region library group props
+## The initial name of the library. Displayed by the group node and [SearchProvider].
+var lib_name: String
+## The description of this library, shown to the user by a renderer.
+var lib_description: String
+## A list of aliases for this library, used by search.
+var lib_aliases: Array[String]
+#endregion
+
## List of groups belonging to this deck, in the format of[br]
## [code]Dictionary[String -> Deck.id, Deck][/code]
#var groups: Dictionary = {}
@@ -52,6 +66,8 @@ signal nodes_connected(from_node_id: String, to_node_id: String, from_output_por
signal nodes_disconnected(from_node_id: String, to_node_id: String, from_output_port: int, to_input_port: int)
## Emitted when the [member variable_stack] has been modified.
signal variables_updated()
+## Emitted when a node has been moved to a different deck.
+signal node_moved_to_deck(node: DeckNode, other: Deck)
#region group signals
signal node_added_to_group(node: DeckNode, assign_id: String, assign_to_self: bool, deck: Deck)
@@ -88,6 +104,7 @@ func add_node_type(type: String, assign_id: String = "", assign_to_self: bool =
func add_node_inst(node: DeckNode, assign_id: String = "", assign_to_self: bool = true) -> DeckNode:
if assign_to_self:
node._belonging_to = self
+ #node._belonging_to_instance = instance_id
if assign_id == "":
var uuid := UUID.v4()
@@ -122,10 +139,37 @@ func add_node_inst(node: DeckNode, assign_id: String = "", assign_to_self: bool
)
node.connect_rpc_signals()
+ print_verbose("Deck %s::%s: added node %s, id %s" % [id, instance_id, node.node_type, node.get_instance_id()])
return node
+func add_lib_group_node(type: String) -> DeckNode:
+ var group_node := add_node_type("group_node")
+ var lib := DeckHolder.add_lib_instance(type, id)
+ group_node.is_library = true
+ group_node.group_id = lib.id
+ group_node.group_instance_id = lib.instance_id
+
+ for node_id: String in lib.nodes:
+ var node := lib.get_node(node_id)
+ if node.node_type == "group_input":
+ group_node.input_node = node
+ group_node.input_node_id = node._id
+ node.group_node = group_node
+ lib.group_input_node = node._id
+ continue
+
+ if node.node_type == "group_output":
+ group_node.output_node = node
+ group_node.output_node_id = node._id
+ node.group_node = group_node
+ lib.group_output_node = node._id
+ continue
+ group_node.init_io()
+ return group_node
+
+
## Get a node belonging to this deck by its' ID.
func get_node(uuid: String) -> DeckNode:
return nodes.get(uuid)
@@ -194,6 +238,21 @@ func disconnect_nodes(from_node_id: String, to_node_id: String, from_output_port
to_node.incoming_connection_removed.emit(to_input_port)
+func disconnect_pair(pair: ConnectionPair) -> void:
+ connections.remove_pair(pair)
+ var from_node_id := pair.incoming.from_node
+ var to_node_id := pair.outgoing.to_node
+ var from_output_port := pair.incoming.from_port
+ var to_input_port := pair.outgoing.to_port
+ connections.remove_connection(from_node_id, to_node_id, from_output_port, to_input_port)
+ nodes_disconnected.emit(from_node_id, to_node_id, from_output_port, to_input_port)
+
+ var from_node := get_node(from_node_id)
+ var to_node := get_node(to_node_id)
+ from_node.outgoing_connection_removed.emit(from_output_port)
+ to_node.incoming_connection_removed.emit(to_input_port)
+
+
## Returns true if this deck has no nodes and no variables.
func is_empty() -> bool:
return nodes.is_empty() and variable_stack.is_empty()
@@ -212,16 +271,19 @@ func remove_node(uuid: String, remove_connections: bool = false, force: bool = f
DeckHolder.close_group_instance(node.group_id, node.group_instance_id)
if remove_connections:
- var outgoing_connections := connections.get_all_outgoing_connections(uuid)
- for from_port: int in outgoing_connections:
- for outgoing: OutgoingConnection in outgoing_connections[from_port]:
- var incoming := outgoing.counterpart.get_ref() as IncomingConnection
- disconnect_nodes(uuid, outgoing.to_node, incoming.from_port, outgoing.to_port)
+ # var outgoing_connections := connections.get_all_outgoing_connections(uuid)
+ # for from_port: int in outgoing_connections.keys():
+ # for outgoing: OutgoingConnection in outgoing_connections[from_port]:
+ # var incoming := outgoing.counterpart.get_ref() as IncomingConnection
+ # disconnect_nodes(uuid, outgoing.to_node, incoming.from_port, outgoing.to_port)
- var incoming_connections := connections.get_all_incoming_connections(uuid)
- for to_port: int in incoming_connections:
- var incoming := connections.get_incoming_connection(uuid, to_port)
- disconnect_nodes(incoming.from_node, uuid, incoming.from_port, to_port)
+ # var incoming_connections := connections.get_all_incoming_connections(uuid)
+ # for to_port: int in incoming_connections.keys():
+ # var incoming := connections.get_incoming_connection(uuid, to_port)
+ # disconnect_nodes(incoming.from_node, uuid, incoming.from_port, to_port)
+ var pairs := connections.get_node_pairs(uuid)
+ for pair in pairs:
+ disconnect_pair(pair)
nodes.erase(uuid)
@@ -229,6 +291,16 @@ func remove_node(uuid: String, remove_connections: bool = false, force: bool = f
if is_group and emit_group_signals:
node_removed_from_group.emit(uuid, remove_connections, self)
+
+ print_verbose("Deck %s::%s: freeing node %s, id %s" % [id, instance_id, node.node_type, node.get_instance_id()])
+ node.free()
+
+
+func pre_exit_cleanup() -> void:
+ for node_id: String in nodes:
+ var node := get_node(node_id)
+ print_verbose("Deck %s::%s: freeing node %s, id %s" % [id, instance_id, node.node_type, node.get_instance_id()])
+ node.free()
## Set a variable on this deck.
@@ -275,47 +347,20 @@ func group_nodes(nodes_to_group: Array) -> Deck:
var rightmost := -INF
var leftmost := INF
for node: DeckNode in nodes_to_group:
- #if node.node_type == "group_node":
- #var _group_id: String = node.group_id
- #var _group: Deck = groups[_group_id]
- #groups.erase(_group)
- #group.groups[_group_id] = _group
- #_group._belonging_to = group
-
+
if node.position.x > rightmost:
rightmost = node.position.x
if node.position.x < leftmost:
leftmost = node.position.x
- #var outgoing_connections := node.outgoing_connections.duplicate(true)
-#
- #for from_port: int in outgoing_connections:
- #for to_node: String in outgoing_connections[from_port]:
- #for to_port: int in outgoing_connections[from_port][to_node]:
- #if to_node not in node_ids_to_keep:
- #disconnect_nodes(node._id, to_node, from_port, to_port)
-
- var outgoing_connections := connections.get_all_outgoing_connections(node._id)
- for from_port: int in outgoing_connections:
- for outgoing: OutgoingConnection in outgoing_connections[from_port]:
- var incoming := outgoing.counterpart.get_ref() as IncomingConnection
- disconnect_nodes(node._id, outgoing.to_node, incoming.from_port, outgoing.to_port)
-
- #var incoming_connections := node.incoming_connections.duplicate(true)
-#
- #for to_port: int in incoming_connections:
- #for from_node: String in incoming_connections[to_port]:
- #if from_node not in node_ids_to_keep:
- #disconnect_nodes(from_node, node._id, incoming_connections[to_port][from_node], to_port)
-
- var incoming_connections := connections.get_all_incoming_connections(node._id)
- for to_port: int in incoming_connections:
- var incoming := connections.get_incoming_connection(node._id, to_port)
- disconnect_nodes(incoming.from_node, node._id, incoming.from_port, to_port)
+ var pairs := connections.get_node_pairs(node._id)
+ for pair in pairs:
+ disconnect_pair(pair)
midpoint += node.position_as_vector2()
- remove_node(node._id, false, true)
- group.add_node_inst(node, node._id)
+ # remove_node(node._id, false, true)
+ # group.add_node_inst(node, node._id)
+ move_node_to_deck(node, group)
midpoint /= nodes_to_group.size()
@@ -353,9 +398,23 @@ func group_nodes(nodes_to_group: Array) -> Deck:
return group
-## Get a group belonging to this deck by its ID.
-#func get_group(uuid: String) -> Deck:
- #return groups.get(uuid)
+func move_node_to_deck(node: DeckNode, other: Deck) -> void:
+ if not node._id in nodes:
+ return
+
+ nodes.erase(node._id)
+ other.nodes[node._id] = node
+ # node_moved_to_deck.emit(node, other)
+
+ node._belonging_to = other
+
+ node_removed.emit(node)
+ other.node_added.emit(node)
+
+ if is_group and emit_group_signals:
+ node_removed_from_group.emit(node._id, false, self)
+ if other.is_group and other.emit_group_signals:
+ node_added_to_group.emit(node, node._id, true, other)
func copy_nodes(nodes_to_copy: Array[String]) -> Dictionary:
@@ -367,38 +426,6 @@ func copy_nodes(nodes_to_copy: Array[String]) -> Dictionary:
for node_id: String in nodes_to_copy:
d.nodes[node_id] = get_node(node_id).to_dict()
- #for node: String in d.nodes:
- #var outgoing_connections: Dictionary = d.nodes[node].outgoing_connections.duplicate(true)
-#
- #for from_port: int in outgoing_connections:
- #for to_node: String in outgoing_connections[from_port]:
- #if to_node not in nodes_to_copy:
- #(d.nodes[node].outgoing_connections[from_port] as Dictionary).erase(to_node)
-#
- #var keys_to_erase := []
- #for from_port: int in d.nodes[node].outgoing_connections:
- #if (d.nodes[node].outgoing_connections[from_port] as Dictionary).is_empty():
- #keys_to_erase.append(from_port)
-#
- #for key in keys_to_erase:
- #(d.nodes[node].outgoing_connections as Dictionary).erase(key)
-#
- #var incoming_connections: Dictionary = d.nodes[node].incoming_connections.duplicate(true)
-#
- #for to_port: int in incoming_connections:
- #for from_node: String in incoming_connections[to_port]:
- #if from_node not in nodes_to_copy:
- #(d.nodes[node].incoming_connections[to_port] as Dictionary).erase(from_node)
- #
- #keys_to_erase.clear()
- #for to_port: int in d.nodes[node].incoming_connections:
- #if (d.nodes[node].incoming_connections[to_port] as Dictionary).is_empty():
- #keys_to_erase.append(to_port)
-#
- #for key in keys_to_erase:
- #(d.nodes[node].incoming_connections as Dictionary).erase(key)
- #keys_to_erase.clear()
-
for node: Dictionary in d.nodes.values().slice(1):
node.position.x = node.position.x - d.nodes.values()[0].position.x
node.position.y = node.position.y - d.nodes.values()[0].position.y
@@ -456,15 +483,37 @@ func paste_nodes_from_dict(nodes_to_paste: Dictionary, position: Vector2 = Vecto
var node := DeckNode.from_dict(nodes_to_paste.nodes[node_id])
var group_needs_unique := false
if node.node_type == "group_node":
- var group := DeckHolder.make_new_group_instance(node.group_id, id)
- var old_group := DeckHolder.get_group_instance(node.group_id, node.group_instance_id)
- group_needs_unique = old_group._belonging_to != id
- node.group_instance_id = group.instance_id
- group.get_node(group.group_input_node).group_node = node
- group.get_node(group.group_output_node).group_node = node
- node.input_node = group.get_node(group.group_input_node)
- node.output_node = group.get_node(group.group_output_node)
- node.init_io()
+ if not node.is_library:
+ var group := DeckHolder.make_new_group_instance(node.group_id, id)
+ var old_group := DeckHolder.get_group_instance(node.group_id, node.group_instance_id)
+ group_needs_unique = old_group._belonging_to != id
+ node.group_instance_id = group.instance_id
+ group.get_node(group.group_input_node).group_node = node
+ group.get_node(group.group_output_node).group_node = node
+ node.input_node = group.get_node(group.group_input_node)
+ node.output_node = group.get_node(group.group_output_node)
+ node.init_io()
+ else:
+ var type: String = node.group_id.get_file().trim_suffix(".deck")
+ var lib := DeckHolder.add_lib_instance(type, id)
+ node.group_instance_id = lib.instance_id
+ for l_node_id: String in lib.nodes:
+ var l_node := lib.get_node(l_node_id)
+ if l_node.node_type == "group_input":
+ node.input_node = l_node
+ node.input_node_id = l_node._id
+ l_node.group_node = node
+ lib.group_input_node = l_node_id
+ continue
+
+ if l_node.node_type == "group_output":
+ node.output_node = l_node
+ node.output_node_id = l_node._id
+ l_node.group_node = node
+ lib.group_output_node = l_node_id
+ continue
+ node.init_io()
+
add_node_inst(node, ids_map[node_id])
if group_needs_unique:
node.make_unique()
@@ -522,10 +571,9 @@ func get_referenced_groups() -> Array[String]:
var res: Array[String] = []
for node_id: String in nodes:
var node := get_node(node_id)
- if node.node_type != "group_node":
- continue
- res.append(node.group_id)
- res.append_array(DeckHolder.get_deck(node.group_id).get_referenced_groups())
+ if node.node_type == "group_node" and not node.is_library:
+ res.append(node.group_id)
+ res.append_array(DeckHolder.get_deck(node.group_id).get_referenced_groups())
return res
@@ -533,15 +581,14 @@ func get_referenced_group_instances() -> Array[Dictionary]:
var res: Array[Dictionary] = []
for node_id: String in nodes:
var node := get_node(node_id)
- if node.node_type != "group_node":
- continue
- res.append({
- "group_id": node.group_id,
- "instance_id": node.group_instance_id,
- "parent_id": node._belonging_to.id,
- "group_node": node_id,
- })
- res.append_array(DeckHolder.get_group_instance(node.group_id, node.group_instance_id).get_referenced_group_instances())
+ if node.node_type == "group_node" and not node.is_library:
+ res.append({
+ "group_id": node.group_id,
+ "instance_id": node.group_instance_id,
+ "parent_id": node._belonging_to,
+ "group_node": node_id,
+ })
+ res.append_array(DeckHolder.get_group_instance(node.group_id, node.group_instance_id).get_referenced_group_instances())
return res
@@ -564,7 +611,7 @@ func to_dict(with_meta: bool = true, group_ids: Array = []) -> Dictionary:
for node_id: String in nodes.keys():
inner["nodes"][node_id] = nodes[node_id].to_dict(with_meta)
- if (nodes[node_id] as DeckNode).node_type == "group_node":
+ if get_node(node_id).node_type == "group_node" and not get_node(node_id).is_library:
if nodes[node_id].group_id not in group_ids:
inner["groups"][nodes[node_id].group_id] = DeckHolder.get_deck(nodes[node_id].group_id).to_dict(with_meta, group_ids)
group_ids.append(nodes[node_id].group_id)
@@ -577,6 +624,15 @@ func to_dict(with_meta: bool = true, group_ids: Array = []) -> Dictionary:
inner["group_input_node"] = group_input_node
inner["group_output_node"] = group_output_node
+ # don't save library stuff if this is not a library
+ if not lib_name.is_empty():
+ var lib := {
+ "lib_name": lib_name,
+ "lib_description": lib_description,
+ "lib_aliases": lib_aliases,
+ }
+ inner["library"] = lib
+
var d := {"deck": inner}
if with_meta:
@@ -594,6 +650,12 @@ static func from_dict(data: Dictionary, path: String = "") -> Deck:
deck.id = data.deck.id
deck.connections = NodeConnections.from_dict(data.deck.connections)
+ if data.deck.has("library"):
+ var lib_data: Dictionary = data.deck.library
+ deck.lib_name = lib_data.get("lib_name", "")
+ deck.lib_description = lib_data.get("lib_description", "")
+ # deck.lib_aliases = lib_data.get("lib_aliases", [])
+
for key in data.meta:
deck.set_meta(key, str_to_var(data.meta[key]))
@@ -611,23 +673,30 @@ static func from_dict(data: Dictionary, path: String = "") -> Deck:
continue
var group_id: String = node.group_id
var group_instance_id: String = node.group_instance_id
- var group_data: Dictionary = groups_data[group_id]
- var group := DeckHolder.add_group_from_dict(group_data, group_id, group_instance_id, deck.id)
- group.get_node(group.group_input_node).group_node = node
- group.get_node(group.group_output_node).group_node = node
+ var group: Deck
+ if not node.is_library:
+ var group_data: Dictionary = groups_data[group_id]
+ group = DeckHolder.add_group_from_dict(group_data, group_id, group_instance_id, deck.id)
+ group.get_node(node.input_node_id).group_node = node
+ group.get_node(node.output_node_id).group_node = node
+ else:
+ group = DeckHolder.add_lib_instance(
+ node.group_id.get_file().trim_suffix(".deck"),
+ deck.id,
+ node.group_instance_id
+ )
+ for io_node_id: String in deck.nodes:
+ var io_node := deck.get_node(io_node_id)
+ if io_node.node_type == "group_input":
+ node.input_node_id = io_node._id
+ io_node.group_node = node
+ continue
+ if io_node.node_type == "group_output":
+ node.output_node_id = io_node._id
+ io_node.group_node = node
+ continue
node.init_io()
- #for group_id: String in groups_data:
- #var group := Deck.from_dict(groups_data[group_id])
- #group._belonging_to = deck
- #group.is_group = true
- #deck.groups[group_id] = group
- #group.group_node = groups_data[group_id]["deck"]["group_node"]
- #group.group_input_node = groups_data[group_id]["deck"]["group_input_node"]
- #group.group_output_node = groups_data[group_id]["deck"]["group_output_node"]
- #deck.get_node(group.group_node).init_io()
-
-
return deck
@@ -670,6 +739,16 @@ class NodeConnections:
to_connection.erase(to_input_port)
out_list.erase(out)
break
+
+ # remove if empty
+ if data[from_node_id] == _create_empty_connection():
+ data.erase(from_node_id)
+
+ if data[to_node_id] == _create_empty_connection():
+ data.erase(to_node_id)
+
+ if (data[from_node_id].outgoing[from_output_port] as Array).is_empty():
+ (data[from_node_id].outgoing as Dictionary).erase(from_output_port)
func has_incoming_connection(node_id: String, on_port: int) -> bool:
@@ -699,6 +778,9 @@ class NodeConnections:
if data.get(node_id, _create_empty_connection()).outgoing.is_empty():
return []
+ if not (data[node_id].outgoing as Dictionary).has(from_port):
+ return []
+
var res: Array[OutgoingConnection] = []
res.assign(data[node_id].outgoing[from_port])
@@ -727,12 +809,32 @@ class NodeConnections:
return res
+ func get_node_pairs(node: String) -> Array[ConnectionPair]:
+ var res: Array[ConnectionPair] = []
+
+ var connections: Dictionary = data.get(node, _create_empty_connection())
+ for to_port: int in connections.outgoing:
+ for outgoing: OutgoingConnection in connections.outgoing[to_port]:
+ res.append(ConnectionPair.from_outgoing(outgoing))
+ for from_port: int in connections.incoming:
+ res.append(ConnectionPair.from_incoming(connections.incoming[from_port]))
+
+ return res
+
+
+
func add_pair(pair: ConnectionPair) -> void:
var outgoing := pair.outgoing
var incoming := pair.incoming
add_connection(incoming.from_node, outgoing.to_node, incoming.from_port, outgoing.to_port)
+ func remove_pair(pair: ConnectionPair) -> void:
+ var outgoing := pair.outgoing
+ var incoming := pair.incoming
+ remove_connection(incoming.from_node, outgoing.to_node, incoming.from_port, outgoing.to_port)
+
+
func _create_empty_connection() -> Dictionary:
return {
"incoming": {},
diff --git a/classes/deck/deck_holder.gd b/classes/deck/deck_holder.gd
index e94f5fa..f1ac35c 100644
--- a/classes/deck/deck_holder.gd
+++ b/classes/deck/deck_holder.gd
@@ -9,6 +9,9 @@ class_name DeckHolder
#static var decks: Array[Deck]
static var decks: Dictionary # Dictionary[String -> id, (Deck|Dictionary[String -> instance_id, Deck])]
+## List of library groups open this session.
+static var lib_groups: Dictionary # Dictionary[String -> path, Dictionary[String -> instance_id, Deck]]
+
static var groups_emitted: Array[String]
static var logger := Logger.new()
@@ -22,6 +25,8 @@ enum Compat {
static func _static_init() -> void:
signals.deck_added.connect(RPCSignalLayer._on_deck_added)
signals.deck_closed.connect(RPCSignalLayer._on_deck_closed)
+
+ NodeDB.init()
## Returns a new empty deck and assigns a new random ID to it.
@@ -32,6 +37,7 @@ static func add_empty_deck() -> Deck:
deck.id = uuid
deck.connect_rpc_signals()
signals.deck_added.emit(uuid)
+ print_verbose("DeckHolder: added empty deck %s, id %s" % [deck.id, deck.get_instance_id()])
return deck
@@ -74,6 +80,7 @@ static func open_deck_from_dict(data: Dictionary, path := "") -> Deck:
decks[deck.id] = deck
deck.connect_rpc_signals()
signals.deck_added.emit(deck.id)
+ print_verbose("DeckHolder: opened deck %s, id %s" % [deck.id, deck.get_instance_id()])
return deck
@@ -94,6 +101,7 @@ static func add_group_from_dict(data: Dictionary, deck_id: String, instance_id:
signals.deck_added.emit(deck_id)
groups_emitted.append(deck_id)
#print(decks)
+ print_verbose("DeckHolder: added group instance %s::%s, id %s" % [group.id, group.instance_id, group.get_instance_id()])
return group
@@ -144,9 +152,42 @@ static func add_empty_group(parent: String = "") -> Deck:
group.connect_rpc_signals()
signals.deck_added.emit(group.id)
groups_emitted.append(group.id)
+ print_verbose("DeckHolder: added empty group %s::%s, id %s" % [group.id, group.instance_id, group.get_instance_id()])
return group
+static func add_lib_instance(type: String, to_deck: String, instance_id: String = UUID.v4()) -> Deck:
+ var nd: NodeDB.NodeDescriptor = NodeDB.libraries[type]
+ var path := nd.script_path
+ var deck_data: Dictionary
+ if lib_groups.has(path):
+ deck_data = (lib_groups[path].values()[0] as Deck).to_dict()
+ else:
+ if not NodeDB.libraries.has(type):
+ return null
+
+ var f := FileAccess.open(path, FileAccess.READ)
+ if not f:
+ return null
+
+ deck_data = JSON.parse_string(f.get_as_text())
+
+ var instances := lib_groups.get(path, {}) as Dictionary
+
+ var deck := Deck.from_dict(deck_data)
+ deck.id = path
+ deck.instance_id = instance_id
+ deck.is_group = true
+ deck.is_library = true
+ deck._belonging_to = to_deck
+
+ instances[instance_id] = deck
+ lib_groups[path] = instances
+
+ print_verbose("DeckHolder: added lib group %s::%s, id %s" % [deck.id, deck.instance_id, deck.get_instance_id()])
+ return deck
+
+
static func connect_group_signals(group: Deck) -> void:
group.node_added_to_group.connect(DeckHolder._on_node_added_to_group)
group.node_removed_from_group.connect(DeckHolder._on_node_removed_from_group)
@@ -157,7 +198,7 @@ static func connect_group_signals(group: Deck) -> void:
static func get_deck(id: String) -> Deck:
if not decks.has(id):
- return null
+ return get_lib(id)
if not decks[id] is Dictionary:
return decks[id]
@@ -165,9 +206,16 @@ static func get_deck(id: String) -> Deck:
return (decks[id] as Dictionary).values()[0]
+static func get_lib(path: String) -> Deck:
+ if not lib_groups.has(path):
+ return null
+
+ return (lib_groups[path] as Dictionary).values()[0]
+
+
static func get_group_instance(group_id: String, instance_id: String) -> Deck:
if not decks.has(group_id):
- return null
+ return get_lib_instance(group_id, instance_id)
if decks[group_id] is Dictionary:
return (decks[group_id] as Dictionary).get(instance_id)
@@ -175,25 +223,41 @@ static func get_group_instance(group_id: String, instance_id: String) -> Deck:
return null
+static func get_lib_instance(path: String, instance_id: String) -> Deck:
+ if not lib_groups.has(path):
+ return null
+
+ return (lib_groups[path] as Dictionary).get(instance_id, null)
+
+
static func close_group_instance(group_id: String, instance_id: String) -> void:
# this is kinda dumb, but to close groups that may be dangling
# when all instances are closed, we have to get that list
# *before* we close the instance
var dangling_groups := get_deck(group_id).get_referenced_groups()
-
- var group_instances: Dictionary = decks.get(group_id, {}) as Dictionary
+ var from := lib_groups if group_id.is_absolute_path() else decks
+ var group_instances: Dictionary = from.get(group_id, {}) as Dictionary
+ var group_inst: Deck = group_instances[instance_id]
+ print_verbose("DeckHolder: freeing group instance %s::%s, id %s" % [group_inst.id, group_inst.instance_id, group_inst.get_instance_id()])
+ group_inst.pre_exit_cleanup()
+ group_inst.free()
group_instances.erase(instance_id)
if group_instances.is_empty():
for group in dangling_groups:
close_all_group_instances(group)
signals.deck_closed.emit(group_id)
groups_emitted.erase(group_id)
- decks.erase(group_id)
+ from.erase(group_id)
static func close_all_group_instances(group_id: String) -> void:
- if decks.get(group_id) is Dictionary:
- decks.erase(group_id)
+ var from := lib_groups if group_id.is_absolute_path() else decks
+ if from.get(group_id) is Dictionary:
+ for instance_id: String in from[group_id]:
+ print_verbose("DeckHolder: freeing group instance %s::%s, id %s" % [group_id, instance_id, from[group_id][instance_id].get_instance_id()])
+ from[group_id][instance_id].pre_exit_cleanup()
+ from[group_id][instance_id].free()
+ from.erase(group_id)
## Unloads a deck. Returns a list of groups that are closed as a result of
@@ -206,10 +270,35 @@ static func close_deck(deck_id: String) -> Array:
close_all_group_instances(group)
signals.deck_closed.emit(deck_id)
decks.erase(deck_id)
+ print_verbose("DeckHolder: freeing deck %s, id %s" % [deck.id, deck.get_instance_id()])
+ deck.pre_exit_cleanup()
+ deck.free()
return groups
return []
+static func pre_exit_cleanup() -> void:
+ for deck_id: String in decks:
+ if decks[deck_id] is Deck:
+ var deck: Deck = decks[deck_id]
+ print_verbose("DeckHolder: freeing deck %s, id %s" % [deck.id, deck.get_instance_id()])
+ deck.pre_exit_cleanup()
+ deck.free()
+ else:
+ for instance_id: String in decks[deck_id]:
+ var deck: Deck = decks[deck_id][instance_id]
+ print_verbose("DeckHolder: freeing group %s::%s, id %s" % [deck_id, instance_id, deck.get_instance_id()])
+ deck.pre_exit_cleanup()
+ deck.free()
+
+ for lib_id: String in lib_groups:
+ for instance_id: String in lib_groups[lib_id]:
+ var deck: Deck = lib_groups[lib_id][instance_id]
+ print_verbose("DeckHolder: freeing lib group %s::%s, id %s" % [lib_id, instance_id, deck.get_instance_id()])
+ deck.pre_exit_cleanup()
+ deck.free()
+
+
static func send_event(event_name: StringName, event_data: Dictionary = {}) -> void:
for deck_id: String in decks:
if decks[deck_id] is Deck:
@@ -217,6 +306,10 @@ static func send_event(event_name: StringName, event_data: Dictionary = {}) -> v
else:
for deck_instance_id: String in decks[deck_id]:
(decks[deck_id][deck_instance_id] as Deck).send_event(event_name, event_data)
+
+ for lib_group_id: String in lib_groups:
+ for lib_instance_id: String in lib_groups[lib_group_id]:
+ (lib_groups[lib_group_id][lib_instance_id] as Deck).send_event(event_name, event_data)
#region group signal callbacks
diff --git a/classes/deck/deck_node.gd b/classes/deck/deck_node.gd
index 1070b6d..cf56827 100644
--- a/classes/deck/deck_node.gd
+++ b/classes/deck/deck_node.gd
@@ -1,6 +1,7 @@
# (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 Object
class_name DeckNode
## A node in a [Deck].
##
@@ -10,14 +11,16 @@ class_name DeckNode
## The name initially shown to a renderer.
var name: String
-
## A list of [Port]s on this node.
var ports: Array[Port]
var _last_send_id: String
-## The deck this node belongs to.
+## The [Deck] this node belongs to.
var _belonging_to: Deck
+
+## The instance ID of the group this node belongs to, if it belongs to a group.
+#var _belonging_to_instance: String
## A unique identifier for this node.
var _id: String
## The type of this node, used for instantiation.
@@ -63,6 +66,8 @@ signal outgoing_connection_removed(from_port: int)
signal incoming_connection_added(from_port: int)
## Emitted when a connection to this node has been removed.
signal incoming_connection_removed(from_port: int)
+## Emitted when the node is about to be freed.
+signal about_to_free()
signal port_value_updated(port_idx: int, new_value: Variant)
@@ -233,12 +238,21 @@ func get_node(uuid: String) -> DeckNode:
return _belonging_to.get_node(uuid)
+## Get the deck this node belongs to.
+#func get_deck() -> Deck:
+ #if not DeckHolder.get_deck(_belonging_to).is_group:
+ #return DeckHolder.get_deck(_belonging_to)
+ #else:
+ #return DeckHolder.get_group_instance(_belonging_to, _belonging_to_instance)
+
+
## Virtual function that's called during deserialization before connections are loaded in.[br]
func _pre_port_load() -> void:
pass
## Virtual function that's called after the node has been deserialized.
+@warning_ignore("unused_parameter")
func _post_load(connections: Deck.NodeConnections) -> void:
pass
@@ -334,3 +348,8 @@ static func from_dict(data: Dictionary, connections: Deck.NodeConnections = null
## Returns the node's [member position] as a [Vector2].
func position_as_vector2() -> Vector2:
return Vector2(position.x, position.y)
+
+
+func _notification(what: int) -> void:
+ if what == NOTIFICATION_PREDELETE:
+ about_to_free.emit()
diff --git a/classes/deck/logger.gd b/classes/deck/logger.gd
index c219650..313c655 100644
--- a/classes/deck/logger.gd
+++ b/classes/deck/logger.gd
@@ -16,6 +16,9 @@ enum LogCategory {
RENDERER,
}
+# Dictionary["text": String, "type": LogType]
+var toast_history: Array[Dictionary]
+
signal log_message(text: String, type: LogType, category: LogCategory)
signal log_toast(text: String, type: LogType)
@@ -57,6 +60,11 @@ func toast_error(text: String) -> void:
func toast(text: String, type: LogType) -> void:
log_toast.emit(text, type)
-
+ toast_history.append(
+ {
+ "text": text,
+ "type": type,
+ }
+ )
if OS.has_feature("editor"):
prints("(t)", LogType.keys()[type].capitalize(), text)
diff --git a/classes/deck/node_db.gd b/classes/deck/node_db.gd
index 73d6bb3..6f0bc50 100644
--- a/classes/deck/node_db.gd
+++ b/classes/deck/node_db.gd
@@ -24,13 +24,17 @@ static var favorite_nodes: Array[String]
## The node index. Maps [member DeckNode.node_type]s to [NodeDB.NodeDescriptor].
static var nodes: Dictionary = {}
+static var libraries: Dictionary = {}
-static func _static_init() -> void:
+
+static func init() -> void:
load_favorites()
#if load_node_index():
#return
create_descriptors(BASE_NODE_PATH)
+
+ reload_libraries()
save_node_index()
@@ -38,32 +42,87 @@ static func _static_init() -> void:
## Fills the [member nodes] index.
static func create_descriptors(path: String) -> void:
var dir := DirAccess.open(path)
+ if not dir:
+ return
dir.list_dir_begin()
var current_file := dir.get_next()
while current_file != "":
if dir.current_is_dir():
create_descriptors(path.path_join(current_file))
- else:
- if current_file.ends_with(".gd"):
- var script_path := path.path_join(current_file)
- var node: DeckNode = load(script_path).new() as DeckNode
- var aliases: String = node.aliases.reduce(
- func(accum, el):
- return accum + el
- , "")
- var descriptor := NodeDescriptor.new(
- script_path,
- node.name,
- node.node_type,
- node.description,
- aliases,
- path.get_slice("/", path.get_slice_count("/") - 1),
- node.appears_in_search,
- )
- nodes[node.node_type] = descriptor
+ elif current_file.ends_with(".gd"):
+ var script_path := path.path_join(current_file)
+ var node: DeckNode = load(script_path).new() as DeckNode
+ var aliases: String = node.aliases.reduce(
+ func(accum, el):
+ return accum + el
+ , "")
+ var descriptor := NodeDescriptor.new(
+ script_path,
+ node.name,
+ node.node_type,
+ node.description,
+ aliases,
+ path.get_slice("/", path.get_slice_count("/") - 1),
+ node.appears_in_search,
+ false,
+ )
+ nodes[node.node_type] = descriptor
+ print_verbose("NodeDB: freeing node %s, id %s" % [node.node_type, node.get_instance_id()])
+ node.free()
current_file = dir.get_next()
+## Fills the [member libraries] index.
+static func create_lib_descriptors(path: String) -> void:
+ var dir := DirAccess.open(path)
+ if not dir:
+ return
+ dir.list_dir_begin()
+ var current_file := dir.get_next()
+ while current_file != "":
+ if dir.current_is_dir():
+ create_lib_descriptors(path.path_join(current_file))
+ elif current_file.ends_with(".deck"):
+ var load_path := path.path_join(current_file)
+ var f := FileAccess.open(load_path, FileAccess.READ)
+ var deck: Dictionary = JSON.parse_string(f.get_as_text())
+ if not deck.deck.has("library"):
+ current_file = dir.get_next()
+ continue
+ var type := current_file.trim_suffix(".deck")
+ if nodes.has(type):
+ DeckHolder.logger.toast_error("Library group '%s' collides with a node with the same type." % type)
+ current_file = dir.get_next()
+ continue
+ if libraries.has(type):
+ DeckHolder.logger.toast_error("Library group '%s' collides with a library group with the same type." % type)
+ current_file = dir.get_next()
+ continue
+ var lib = deck.deck.library
+ var aliases: String = lib.lib_aliases.reduce(
+ func(accum, el):
+ return accum + el
+ , "")
+ var descriptor := NodeDescriptor.new(
+ load_path,
+ lib.lib_name,
+ type,
+ lib.lib_description,
+ aliases,
+ path.get_slice("/", path.get_slice_count("/") - 1),
+ true,
+ true,
+ )
+ libraries[descriptor.type] = descriptor
+ current_file = dir.get_next()
+
+
+static func reload_libraries() -> void:
+ libraries.clear()
+ for path in StreamGraphConfig.get_library_search_paths():
+ create_lib_descriptors(path)
+
+
## Instantiates a [DeckNode] from a given [param node_type]. See [member DeckNode.node_type].
static func instance_node(node_type: String) -> DeckNode:
if not nodes.has(node_type):
@@ -137,6 +196,10 @@ static func is_node_favorite(node_type: String) -> bool:
return node_type in favorite_nodes
+static func is_library(type: String) -> bool:
+ return libraries.has(type)
+
+
## Returns a capitalized category string.
static func get_category_capitalization(category: String) -> String:
return CATEGORY_CAPITALIZATION.get(category, category.capitalize())
@@ -157,6 +220,8 @@ class NodeDescriptor:
var category: String
## Whether this [DeckNode] reference will appear when searching. See [member DeckNode.appears_in_search].
var appears_in_search: bool
+
+ var is_library: bool
## Stores the path to this node's script for later instantiation.
var script_path: String
@@ -169,6 +234,7 @@ class NodeDescriptor:
p_aliases: String,
p_category: String,
p_appears_in_search: bool,
+ p_is_library: bool,
) -> void:
script_path = p_script_path
@@ -178,6 +244,7 @@ class NodeDescriptor:
aliases = p_aliases
category = p_category
appears_in_search = p_appears_in_search
+ is_library = p_is_library
## Returns a [Dictionary] representation of this node descriptor.
@@ -190,6 +257,7 @@ class NodeDescriptor:
"script_path": script_path,
"category": category,
"appears_in_search": appears_in_search,
+ "is_library": is_library,
}
return d
@@ -204,6 +272,7 @@ class NodeDescriptor:
data.get("aliases", ""),
data.get("category", ""),
data.get("appears_in_search", false),
+ data.get("is_library", false)
)
return nd
diff --git a/classes/deck/nodes/general/print.gd b/classes/deck/nodes/general/print.gd
index 7ca7b7a..7ad57e0 100644
--- a/classes/deck/nodes/general/print.gd
+++ b/classes/deck/nodes/general/print.gd
@@ -24,7 +24,6 @@ func _init() -> void:
func _receive(to_input_port: int, data: Variant) -> void:
- print(_belonging_to.get_reference_count())
var data_to_print := ""
if to_input_port == 1:
data = await resolve_input_port_value_async(0)
diff --git a/classes/deck/nodes/group/group_input_node.gd b/classes/deck/nodes/group/group_input_node.gd
index 221e317..4a72e04 100644
--- a/classes/deck/nodes/group/group_input_node.gd
+++ b/classes/deck/nodes/group/group_input_node.gd
@@ -5,7 +5,10 @@ extends DeckNode
var output_count: int:
get:
- return get_all_ports().size() - 1
+ if output_count == 0:
+ return get_all_ports().size()
+ else:
+ return output_count
var group_node: DeckNode
@@ -14,8 +17,8 @@ func _init() -> void:
name = "Group input"
node_type = "group_input"
props_to_serialize = [&"output_count"]
- appears_in_search = false
- user_can_delete = false
+ # appears_in_search = false
+ # user_can_delete = false
add_output_port(
DeckType.Types.ANY,
@@ -52,11 +55,12 @@ func _on_outgoing_connection_removed(port_idx: int) -> void:
func _pre_port_load() -> void:
- for i in output_count + 1:
+ for i in output_count:
add_output_port(
DeckType.Types.ANY,
"Input %s" % (i + 1)
)
+ output_count = 0
func _post_load(connections: Deck.NodeConnections) -> void:
@@ -75,4 +79,7 @@ func _post_load(connections: Deck.NodeConnections) -> void:
func _value_request(from_port: int) -> Variant:
- return await group_node.request_value_async(group_node.get_input_ports()[from_port].index_of_type)
+ if group_node:
+ return await group_node.request_value_async(group_node.get_input_ports()[from_port].index_of_type)
+ else:
+ return null
diff --git a/classes/deck/nodes/group/group_node.gd b/classes/deck/nodes/group/group_node.gd
index 5538840..b6cda5d 100644
--- a/classes/deck/nodes/group/group_node.gd
+++ b/classes/deck/nodes/group/group_node.gd
@@ -8,6 +8,8 @@ var group_instance_id: String
var input_node: DeckNode
var output_node: DeckNode
+var is_library: bool
+
var input_node_id: String
var output_node_id: String
@@ -17,7 +19,7 @@ var extra_ports: Array
func _init() -> void:
name = "Group"
node_type = "group_node"
- props_to_serialize = [&"group_id", &"group_instance_id", &"extra_ports", &"input_node_id", &"output_node_id"]
+ props_to_serialize = [&"group_id", &"group_instance_id", &"extra_ports", &"input_node_id", &"output_node_id", &"is_library"]
appears_in_search = false
@@ -41,8 +43,8 @@ func init_io() -> void:
if output_node and output_node.ports_updated.is_connected(recalculate_ports):
output_node.ports_updated.disconnect(recalculate_ports)
- input_node = group.get_node(group.group_input_node)
- output_node = group.get_node(group.group_output_node)
+ input_node = group.get_node(input_node_id)
+ output_node = group.get_node(output_node_id)
recalculate_ports()
setup_connections()
@@ -78,7 +80,7 @@ func recalculate_ports() -> void:
func _receive(to_input_port: int, data: Variant):
- var i = DeckHolder.get_group_instance(group_id, group_instance_id).get_node(input_node_id)
+ # var i = DeckHolder.get_group_instance(group_id, group_instance_id).get_node(input_node_id)
#i.send(get_input_ports()[to_input_port].index_of_type, data)
input_node.send(get_input_ports()[to_input_port].index_of_type, data)
diff --git a/classes/deck/nodes/group/group_output_node.gd b/classes/deck/nodes/group/group_output_node.gd
index e04606d..48749a7 100644
--- a/classes/deck/nodes/group/group_output_node.gd
+++ b/classes/deck/nodes/group/group_output_node.gd
@@ -5,7 +5,10 @@ extends DeckNode
var input_count: int:
get:
- return get_all_ports().size() - 1
+ if input_count == 0:
+ return get_all_ports().size()
+ else:
+ return input_count
var group_node: DeckNode
@@ -14,8 +17,8 @@ func _init() -> void:
name = "Group output"
node_type = "group_output"
props_to_serialize = [&"input_count"]
- appears_in_search = false
- user_can_delete = false
+ # appears_in_search = false
+ # user_can_delete = false
add_input_port(
DeckType.Types.ANY,
@@ -53,11 +56,12 @@ func _on_incoming_connection_removed(port_idx: int) -> void:
func _pre_port_load() -> void:
- for i in input_count + 1:
+ for i in input_count:
add_input_port(
DeckType.Types.ANY,
"Output %s" % (i + 1)
)
+ input_count = 0
func _post_load(connections: Deck.NodeConnections) -> void:
@@ -76,4 +80,5 @@ func _post_load(connections: Deck.NodeConnections) -> void:
func _receive(to_input_port: int, data: Variant) -> void:
- group_node.send(group_node.get_output_ports()[to_input_port].index_of_type, data)
+ if group_node:
+ group_node.send(group_node.get_output_ports()[to_input_port].index_of_type, data)
diff --git a/classes/deck/search_provider.gd b/classes/deck/search_provider.gd
index 6567fdd..9e84d2b 100644
--- a/classes/deck/search_provider.gd
+++ b/classes/deck/search_provider.gd
@@ -45,6 +45,18 @@ static var filters: Array[Filter] = [
#prints("c:", c, "r:", search_string.replace(c, ""))
return search_string.replace(c, "")
),
+
+ # library filter. will only match library nodes. syntax: "#l"
+ Filter.new(
+ func(search_string: String) -> bool:
+ return "#l" in search_string,
+
+ func(element: NodeDB.NodeDescriptor, _search_string: String, _pre_strip_string: String) -> bool:
+ return element.is_library,
+
+ func(search_string: String) -> String:
+ return search_string.replace("#l", "")
+ ),
]
@@ -72,6 +84,16 @@ static func search(term: String) -> Array[NodeDB.NodeDescriptor]:
if cleaned_search_string.is_subsequence_ofn(full_search_string):
res.append(nd)
+ # same thing but libraries
+ for node_type: String in NodeDB.libraries:
+ var nd: NodeDB.NodeDescriptor = NodeDB.libraries[node_type]
+ if not nd.appears_in_search:
+ continue
+
+ var full_search_string := nd.name + nd.aliases
+ if cleaned_search_string.is_subsequence_ofn(full_search_string):
+ res.append(nd)
+
# no filters apply, just return the results straight
if filters_to_apply.is_empty():
return res
diff --git a/classes/stream_graph_config.gd b/classes/stream_graph_config.gd
new file mode 100644
index 0000000..9509fd6
--- /dev/null
+++ b/classes/stream_graph_config.gd
@@ -0,0 +1,110 @@
+# (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)
+class_name StreamGraphConfig
+
+
+static var config := {
+ &"library_search_paths": [
+ ProjectSettings.globalize_path("user://library_groups"),
+ ],
+}
+
+const SAVE_PATH := "user://config.json"
+
+
+static func _static_init() -> void:
+ var f := FileAccess.open(SAVE_PATH, FileAccess.READ)
+ if not f:
+ return
+
+ var d = JSON.parse_string(f.get_as_text())
+ config.merge(d, true)
+
+
+static func get_p(property: StringName) -> Variant:
+ return config.get(property)
+
+
+static func has(property: StringName) -> bool:
+ return config.has(property)
+
+
+static func set_p(property: StringName, value: Variant) -> void:
+ config[property] = value
+ save()
+
+
+static func get_dict() -> Dictionary:
+ return config
+
+
+static func merge(with: Dictionary) -> void:
+ config.merge(with, true)
+ save()
+
+
+static func add_library_search_path(path: String) -> void:
+ var arr: Array = config[&"library_search_paths"]
+ arr.append(path)
+ NodeDB.reload_libraries()
+ save()
+
+
+static func remove_library_search_path(path: String) -> void:
+ var arr: Array = config[&"library_search_paths"]
+ if arr.find(path) < 1:
+ return
+ arr.erase(path)
+ NodeDB.reload_libraries()
+ save()
+
+
+static func rename_library_search_path(old_path: String, new_path: String) -> void:
+ var arr: Array = config[&"library_search_paths"]
+ var idx := arr.find(old_path)
+ if idx < 1:
+ return
+
+ arr[idx] = new_path
+ NodeDB.reload_libraries()
+ save()
+
+
+static func move_library_path_up(path: String) -> void:
+ var arr: Array = config[&"library_search_paths"]
+ var idx := arr.find(path)
+ if idx < 1:
+ return
+
+ var old_path = arr[idx]
+ arr[idx] = arr[idx - 1]
+ arr[idx - 1] = old_path
+ NodeDB.reload_libraries()
+ save()
+
+
+static func move_library_path_down(path: String) -> void:
+ var arr: Array = config[&"library_search_paths"]
+ var idx := arr.find(path)
+ if idx < 1 or idx == arr.size() - 1:
+ return
+
+ var old_path = arr[idx]
+ arr[idx] = arr[idx + 1]
+ arr[idx + 1] = old_path
+ NodeDB.reload_libraries()
+ save()
+
+
+static func get_library_search_paths() -> Array:
+ return config[&"library_search_paths"]
+
+
+static func save() -> void:
+ var f := FileAccess.open(SAVE_PATH, FileAccess.WRITE)
+ if not f:
+ DeckHolder.logger.log_system("Could not open config file for writing", Logger.LogType.ERROR)
+ return
+
+ f.store_string(JSON.stringify(config))
diff --git a/classes/util.gd b/classes/util.gd
index fb44b45..db22793 100644
--- a/classes/util.gd
+++ b/classes/util.gd
@@ -9,13 +9,13 @@ static var _batches: Dictionary # Dictionary[StringName, BatchConnection]
## Connects the [param p_func] to [param p_signal] if that connection doesn't already exist.
static func safe_connect(p_signal: Signal, p_func: Callable) -> void:
- if not p_signal.is_connected(p_func) and not p_signal.is_null() and p_func.is_valid():
+ if not p_signal.is_null() and p_func.is_valid() and not p_signal.is_connected(p_func):
p_signal.connect(p_func)
## Disconnects the [param p_func] from [param p_signal] if that connection exists.
static func safe_disconnect(p_signal: Signal, p_func: Callable) -> void:
- if p_signal.is_connected(p_func) and not p_signal.is_null() and p_func.is_valid():
+ if not p_signal.is_null() and p_func.is_valid() and p_signal.is_connected(p_func):
p_signal.disconnect(p_func)
diff --git a/graph_node_renderer/add_node_menu.gd b/graph_node_renderer/add_node_menu.gd
index c3517fc..0ce3fb1 100644
--- a/graph_node_renderer/add_node_menu.gd
+++ b/graph_node_renderer/add_node_menu.gd
@@ -40,17 +40,17 @@ func add_category(category_name: String) -> void:
## Add an item to a category.
-func add_category_item(category: String, item: String, tooltip: String = "", favorite: bool = false) -> void:
+func add_category_item(category: String, item: String, tooltip: String = "", favorite: bool = false, library: bool = false) -> void:
var c: Category = categories[category]
- c.add_item(item, tooltip, favorite)
+ c.add_item(item, tooltip, favorite, library)
## Wrapper around [method add_category_item] and [method add_category]. Adds an item to a [param category], creating the category if it doesn't exist yet.
-func add_item(category: String, item: String, tooltip: String = "", favorite: bool = false) -> void:
+func add_item(category: String, item: String, tooltip: String = "", favorite: bool = false, library: bool = false) -> void:
if not categories.has(category):
add_category(category)
- add_category_item(category, item, tooltip, favorite)
+ add_category_item(category, item, tooltip, favorite, library)
## Get a [AddNodeMenu.Category] node by its' identifier.
@@ -74,7 +74,7 @@ func search(text: String) -> void:
return
for nd in search_results:
- add_item(nd.category, nd.name, nd.description, NodeDB.is_node_favorite(nd.type))
+ add_item(nd.category, nd.name, nd.description, NodeDB.is_node_favorite(nd.type), nd.is_library)
var c := get_category(nd.category)
c.set_item_metadata(c.get_item_count() - 1, "type", nd.type)
c.set_item_metadata(c.get_item_count() - 1, "node_descriptor", weakref(nd))
@@ -231,8 +231,8 @@ class Category extends VBoxContainer:
## Add an item to the category.
- func add_item(p_name: String, p_tooltip: String, p_favorite: bool = false) -> void:
- var item := CategoryItem.new(p_name, p_tooltip, p_favorite)
+ func add_item(p_name: String, p_tooltip: String, p_favorite: bool = false, p_library: bool = false) -> void:
+ var item := CategoryItem.new(p_name, p_tooltip, p_favorite, p_library)
item.favorite_toggled.connect(
func(toggled: bool):
item_favorite_button_toggled.emit(item.get_index(), toggled)
@@ -302,6 +302,7 @@ class Category extends VBoxContainer:
class CategoryItem extends HBoxContainer:
const FAVORITE_ICON := preload("res://graph_node_renderer/textures/favorite-icon.svg")
const NON_FAVORITE_ICON := preload("res://graph_node_renderer/textures/non-favorite-icon.svg")
+ const GROUP_ICON = preload("res://graph_node_renderer/textures/group_icon.svg")
const ITEM_MARGIN := 16
## The stylebox to use if this item is highlighted.
@@ -309,6 +310,7 @@ class CategoryItem extends HBoxContainer:
var is_highlighted: bool
+ var group_texture_rect: TextureRect
var fav_button: Button
var name_button: Button
var panel: PanelContainer
@@ -319,7 +321,7 @@ class CategoryItem extends HBoxContainer:
signal favorite_toggled(toggled: bool)
- func _init(p_name: String, p_tooltip: String, p_favorite: bool) -> void:
+ func _init(p_name: String, p_tooltip: String, p_favorite: bool, p_library: bool) -> void:
fav_button = Button.new()
fav_button.icon = FAVORITE_ICON if p_favorite else NON_FAVORITE_ICON
fav_button.toggle_mode = true
@@ -354,6 +356,18 @@ class CategoryItem extends HBoxContainer:
var inner_hb := HBoxContainer.new()
inner_hb.add_child(fav_button)
inner_hb.add_child(name_button)
+
+ if p_library:
+ group_texture_rect = TextureRect.new()
+ group_texture_rect.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
+ group_texture_rect.custom_minimum_size = Vector2(12, 12)
+ group_texture_rect.texture = GROUP_ICON
+ group_texture_rect.tooltip_text = "This is a library group. It will create a group node."
+ inner_hb.add_child(group_texture_rect)
+ var m := MarginContainer.new()
+ m.add_theme_constant_override(&"margin_right", 6)
+ inner_hb.add_child(m)
+
panel.add_child(inner_hb)
add_child(mc)
diff --git a/graph_node_renderer/deck_holder_renderer.gd b/graph_node_renderer/deck_holder_renderer.gd
index 1754ee5..9ebb43f 100644
--- a/graph_node_renderer/deck_holder_renderer.gd
+++ b/graph_node_renderer/deck_holder_renderer.gd
@@ -209,6 +209,9 @@ func _notification(what: int) -> void:
func _on_tab_container_tab_about_to_change(previous_tab: int) -> void:
var deck := get_deck_at_tab(previous_tab)
+ if deck == null:
+ return
+
Util.safe_disconnect(deck.variables_updated, bottom_dock.rebuild_variable_tree)
var deck_renderer := get_deck_renderer_at_tab(previous_tab)
@@ -219,21 +222,25 @@ func _on_tab_container_tab_about_to_change(previous_tab: int) -> void:
func _on_tab_container_tab_changed(tab: int) -> void:
+ var deck := get_active_deck()
+ if deck == null:
+ return
var is_group = tab_container.get_tab_metadata(tab, "group", false)
file_popup_menu.set_item_disabled(FileMenuId.SAVE, is_group)
file_popup_menu.set_item_disabled(FileMenuId.SAVE_AS, is_group)
bottom_dock.rebuild_variable_tree(get_active_deck().variable_stack)
- var deck := get_active_deck()
deck.variables_updated.connect(bottom_dock.rebuild_variable_tree.bind(deck.variable_stack))
sidebar.set_edited_deck(deck.id)
get_active_deck_renderer().node_selected.connect(set_sidebar_node)
get_active_deck_renderer().node_deselected.connect(set_sidebar_node)
+ #get_active_deck_renderer().nodes_about_to_delete.connect(sidebar.set_edited_node)
sidebar.refresh_node_list()
var batch := Util.batch_begin()
batch.add(deck.node_added, refresh_sidebar_node_list)
batch.add(deck.node_removed, refresh_sidebar_node_list)
+ #batch.add(get_active_deck_renderer().nodes_about_to_delete, sidebar.set_edited_node)
Util.push_batch(batch, &"sidebar_signals")
@@ -430,7 +437,11 @@ func get_active_deck() -> Deck:
## Returns the deck at [param tab] in the [member tab_container].
func get_deck_at_tab(tab: int) -> Deck:
- return get_deck_renderer_at_tab(tab).deck
+ var r := get_deck_renderer_at_tab(tab)
+ if is_instance_valid(r.deck):
+ return get_deck_renderer_at_tab(tab).deck
+ else:
+ return null
## Returns the current deck renderer in the [member tab_container].
diff --git a/graph_node_renderer/deck_holder_renderer.tscn b/graph_node_renderer/deck_holder_renderer.tscn
index c072686..bf9e6bf 100644
--- a/graph_node_renderer/deck_holder_renderer.tscn
+++ b/graph_node_renderer/deck_holder_renderer.tscn
@@ -16,7 +16,7 @@
[ext_resource type="PackedScene" uid="uid://cvvkj138fg8jg" path="res://graph_node_renderer/unsaved_changes_dialog.tscn" id="9_4n0q6"]
[ext_resource type="PackedScene" uid="uid://bu466w2w3q08c" path="res://graph_node_renderer/about_dialog.tscn" id="11_6ln7n"]
[ext_resource type="PackedScene" uid="uid://brfrufvkjwcor" path="res://graph_node_renderer/rpc_setup_dialog.tscn" id="12_1xrfk"]
-[ext_resource type="PackedScene" uid="uid://dodqetbke5wji" path="res://graph_node_renderer/settings_dialog.tscn" id="16_rktri"]
+[ext_resource type="PackedScene" uid="uid://dodqetbke5wji" path="res://graph_node_renderer/settings/settings_dialog.tscn" id="16_rktri"]
[ext_resource type="PackedScene" uid="uid://cd1t0gvi022gx" path="res://graph_node_renderer/compat_dialog.tscn" id="17_2ndnq"]
[node name="DeckHolderRenderer" type="Control"]
diff --git a/graph_node_renderer/deck_renderer_graph_edit.gd b/graph_node_renderer/deck_renderer_graph_edit.gd
index 927710b..27f751d 100644
--- a/graph_node_renderer/deck_renderer_graph_edit.gd
+++ b/graph_node_renderer/deck_renderer_graph_edit.gd
@@ -43,7 +43,9 @@ var is_group: bool = false
var change_dirty: bool = true
-signal dirty_state_changed
+signal dirty_state_changed()
+signal nodes_about_to_delete()
+
## 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
@@ -90,6 +92,9 @@ func attempt_connection(from_node_name: StringName, from_port: int, to_node_name
func _is_node_hover_valid(from_node: StringName, from_port: int, to_node: StringName, to_port: int) -> bool:
+ if deck.is_library:
+ return false
+
var from_node_renderer: DeckNodeRendererGraphNode = get_node(NodePath(from_node))
var to_node_renderer: DeckNodeRendererGraphNode = get_node(NodePath(to_node))
@@ -99,6 +104,9 @@ func _is_node_hover_valid(from_node: StringName, from_port: int, to_node: String
## 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:
+ if deck.is_library:
+ return
+
var from_node_renderer: DeckNodeRendererGraphNode = get_node(NodePath(from_node_name))
var to_node_renderer: DeckNodeRendererGraphNode = get_node(NodePath(to_node_name))
@@ -146,7 +154,8 @@ func focus_selection() -> void:
## 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)
+ if is_instance_valid(deck):
+ deck.set_meta("offset", offset)
## Setups all the data from the set [member deck] in this [DeckRendererGraphEdit]
@@ -160,10 +169,19 @@ func initialize_from_deck() -> void:
var node_renderer: DeckNodeRendererGraphNode = NODE_SCENE.instantiate()
node_renderer.node = deck.get_node(node_id)
node_renderer.name = node_id
+ node_renderer.draggable = not deck.is_library
add_child(node_renderer)
node_renderer.position_offset = node_renderer.node.position_as_vector2()
change_dirty = true
dirty = false
+
+ right_disconnects = not deck.is_library
+
+ if deck.is_library:
+ connection_drag_started.connect(
+ func(from_node: StringName, from_port: int, is_output: bool):
+ force_connection_drag_end()
+ )
refresh_connections()
@@ -201,14 +219,11 @@ func _on_deck_node_added(node: DeckNode) -> void:
## 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
+ # 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.
+ refresh_connections()
+ get_node_renderer(node).queue_free()
dirty = true
@@ -222,55 +237,57 @@ func get_selected_nodes() -> Array:
func _gui_input(event: InputEvent) -> void:
- if RendererShortcuts.check_shortcut("group_nodes", event) 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()
- accept_event()
-
- if RendererShortcuts.check_shortcut("add_node", event):
- 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 = r.position
- accept_event()
-
- 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
+ if not deck.is_library:
+ if RendererShortcuts.check_shortcut("group_nodes", event) 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()
+ accept_event()
- node.make_unique()
+ if RendererShortcuts.check_shortcut("add_node", event):
+ 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 = r.position
+ accept_event()
+
+ 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 RendererShortcuts.check_shortcut("rename_node", event) 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)
- accept_event()
+ if RendererShortcuts.check_shortcut("rename_node", event) 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)
+ accept_event()
+
+ if RendererShortcuts.check_shortcut("duplicate_nodes", event) and get_selected_nodes().size() > 0:
+ _on_duplicate_nodes_request()
+ accept_event()
+
+ if RendererShortcuts.check_shortcut("paste_nodes", event):
+ _on_paste_nodes_request()
+ accept_event()
# copy/paste/duplicate/etc
if RendererShortcuts.check_shortcut("copy_nodes", event) and get_selected_nodes().size() > 0:
_on_copy_nodes_request()
accept_event()
- if RendererShortcuts.check_shortcut("duplicate_nodes", event) and get_selected_nodes().size() > 0:
- _on_duplicate_nodes_request()
- accept_event()
-
- if RendererShortcuts.check_shortcut("paste_nodes", event):
- _on_paste_nodes_request()
- accept_event()
if RendererShortcuts.check_shortcut("focus_nodes", event):
focus_selection()
@@ -300,6 +317,8 @@ func _on_rename_popup_closed() -> void:
## Opens [member search_popup_panel] at the mouse position. Connected to [signal GraphEdit.popup_request]
func _on_popup_request(p_popup_position: Vector2) -> void:
+ if deck.is_library:
+ return
var p := get_viewport_rect().position + get_global_mouse_position()
p += Vector2(10, 10)
var r := Rect2i(p, search_popup_panel.size)
@@ -312,13 +331,19 @@ func _on_popup_request(p_popup_position: Vector2) -> void:
## [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))
+ if not NodeDB.is_library(type):
+ # var node := NodeDB.instance_node(type) as DeckNode
+ # deck.add_node_inst(node)
+ var node := deck.add_node_type(type)
- get_node_renderer(node).position_offset = node_pos
+ get_node_renderer(node).position_offset = node_pos
+ else:
+ var node := deck.add_lib_group_node(type)
+
+ get_node_renderer(node).position_offset = node_pos
search_popup_panel.hide()
@@ -347,6 +372,8 @@ func _on_delete_nodes_request(nodes: Array[StringName]) -> void:
if node_ids.is_empty():
return
+ nodes_about_to_delete.emit()
+
for node_id in node_ids:
deck.remove_node(node_id, true)
diff --git a/graph_node_renderer/descriptors/button_descriptor.gd b/graph_node_renderer/descriptors/button_descriptor.gd
index c67d7d2..7096459 100644
--- a/graph_node_renderer/descriptors/button_descriptor.gd
+++ b/graph_node_renderer/descriptors/button_descriptor.gd
@@ -9,3 +9,4 @@ extends DescriptorContainer
func _setup(port: Port, node: DeckNode) -> void:
button.text = port.label
button.pressed.connect(node.press_button.bind(port.index))
+ button.disabled = node._belonging_to.is_library
diff --git a/graph_node_renderer/descriptors/check_box_descriptor.gd b/graph_node_renderer/descriptors/check_box_descriptor.gd
index ae20862..215afe0 100644
--- a/graph_node_renderer/descriptors/check_box_descriptor.gd
+++ b/graph_node_renderer/descriptors/check_box_descriptor.gd
@@ -6,7 +6,7 @@ extends DescriptorContainer
@onready var check_box: CheckBox = %CheckBox
-func _setup(port: Port, _node: DeckNode) -> void:
+func _setup(port: Port, node: DeckNode) -> void:
if descriptor.size() > 1:
check_box.button_pressed = true
if port.value is bool:
@@ -14,6 +14,7 @@ func _setup(port: Port, _node: DeckNode) -> void:
check_box.text = port.label
port.value_callback = check_box.is_pressed
check_box.toggled.connect(port.set_value)
+ check_box.disabled = node._belonging_to.is_library
func set_value(new_value: Variant) -> void:
diff --git a/graph_node_renderer/descriptors/codeblock_descriptor.gd b/graph_node_renderer/descriptors/codeblock_descriptor.gd
index 4983432..52bebd1 100644
--- a/graph_node_renderer/descriptors/codeblock_descriptor.gd
+++ b/graph_node_renderer/descriptors/codeblock_descriptor.gd
@@ -6,7 +6,7 @@ extends DescriptorContainer
@onready var code_edit: CodeEdit = %CodeEdit
-func _setup(port: Port, _node: DeckNode) -> void:
+func _setup(port: Port, node: DeckNode) -> void:
if port.value:
code_edit.text = str(port.value)
code_edit.placeholder_text = port.label
@@ -14,6 +14,7 @@ func _setup(port: Port, _node: DeckNode) -> void:
code_edit.text_changed.connect(port.set_value.bind(code_edit.get_text))
code_edit.custom_minimum_size = Vector2(200, 100)
code_edit.size_flags_vertical = SIZE_EXPAND_FILL
+ code_edit.editable = not node._belonging_to.is_library
func set_value(new_value: Variant) -> void:
diff --git a/graph_node_renderer/descriptors/field_descriptor.gd b/graph_node_renderer/descriptors/field_descriptor.gd
index 9b078ab..32f807b 100644
--- a/graph_node_renderer/descriptors/field_descriptor.gd
+++ b/graph_node_renderer/descriptors/field_descriptor.gd
@@ -6,12 +6,13 @@ extends DescriptorContainer
@onready var line_edit: LineEdit = %LineEdit
-func _setup(port: Port, _node: DeckNode) -> void:
+func _setup(port: Port, node: DeckNode) -> void:
if port.value:
line_edit.text = str(port.value)
line_edit.placeholder_text = port.label
port.value_callback = line_edit.get_text
line_edit.text_changed.connect(port.set_value)
+ line_edit.editable = not node._belonging_to.is_library
func set_value(new_value: Variant) -> void:
diff --git a/graph_node_renderer/descriptors/single_choice_descriptor.gd b/graph_node_renderer/descriptors/single_choice_descriptor.gd
index 82a0e36..d494bee 100644
--- a/graph_node_renderer/descriptors/single_choice_descriptor.gd
+++ b/graph_node_renderer/descriptors/single_choice_descriptor.gd
@@ -6,7 +6,7 @@ extends DescriptorContainer
@onready var box: OptionButton = %Box
-func _setup(port: Port, _node: DeckNode) -> void:
+func _setup(port: Port, node: DeckNode) -> void:
if descriptor.slice(1).is_empty():
if port.value:
box.add_item(port.value)
@@ -27,7 +27,7 @@ func _setup(port: Port, _node: DeckNode) -> void:
if box.get_item_text(i) == port.value:
box.select(i)
break
-
+ box.disabled = node._belonging_to.is_library
func set_value(new_value: Variant) -> void:
if box.has_focus():
diff --git a/graph_node_renderer/descriptors/spin_box_descriptor.gd b/graph_node_renderer/descriptors/spin_box_descriptor.gd
index 0c748be..f86caf1 100644
--- a/graph_node_renderer/descriptors/spin_box_descriptor.gd
+++ b/graph_node_renderer/descriptors/spin_box_descriptor.gd
@@ -6,7 +6,7 @@ extends DescriptorContainer
@onready var spin_box: SpinBox = %SpinBox
-func _setup(port: Port, _node: DeckNode) -> void:
+func _setup(port: Port, node: DeckNode) -> void:
spin_box.tooltip_text = port.label
if port.value != null:
spin_box.value = float(port.value)
@@ -26,6 +26,7 @@ func _setup(port: Port, _node: DeckNode) -> void:
spin_box.step = float(descriptor[4])
port.value_callback = spin_box.get_value
spin_box.value_changed.connect(port.set_value)
+ spin_box.editable = not node._belonging_to.is_library
func set_value(new_value: Variant) -> void:
diff --git a/graph_node_renderer/settings/library_group_paths_editor.gd b/graph_node_renderer/settings/library_group_paths_editor.gd
new file mode 100644
index 0000000..c66f81c
--- /dev/null
+++ b/graph_node_renderer/settings/library_group_paths_editor.gd
@@ -0,0 +1,176 @@
+# (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 PanelContainer
+
+@onready var paths_container: VBoxContainer = %PathsContainer
+@onready var folder_dialog: FileDialog = %FolderDialog
+@onready var add_button: Button = %AddButton
+
+var _edited: FolderView
+
+
+func _ready() -> void:
+ for i in StreamGraphConfig.get_library_search_paths().size():
+ var path: String = StreamGraphConfig.get_library_search_paths()[i]
+ add_folder_view(path, i == 0)
+
+ # calculate_move_buttons()
+ folder_dialog.canceled.connect(_on_folder_dialog_canceled)
+
+ add_button.pressed.connect(
+ func():
+ folder_dialog.dir_selected.connect(_on_folder_dialog_dir_selected_new, CONNECT_ONE_SHOT)
+ folder_dialog.popup_centered()
+ )
+
+
+func calculate_move_buttons() -> void:
+ await get_tree().process_frame
+ for fv: FolderView in paths_container.get_children():
+ fv.calculate_move_buttons()
+
+
+func _on_folder_dialog_dir_selected_new(path: String) -> void:
+ add_folder_view(path)
+ StreamGraphConfig.add_library_search_path(path)
+
+
+func add_folder_view(path: String, default: bool = false) -> void:
+ var fv := FolderView.new(path, default)
+ paths_container.add_child(fv)
+ fv.open_dialog.connect(_on_folder_view_open_dialog.bind(fv))
+ fv.deleted.connect(calculate_move_buttons)
+ fv.moved.connect(calculate_move_buttons)
+ calculate_move_buttons()
+
+
+func _on_folder_view_open_dialog(path: String, who: FolderView) -> void:
+ folder_dialog.current_dir = path
+ folder_dialog.dir_selected.connect(_on_folder_dialog_dir_selected, CONNECT_ONE_SHOT)
+ _edited = who
+ folder_dialog.popup_centered()
+
+
+func _on_folder_dialog_dir_selected(path: String) -> void:
+ if not _edited:
+ return
+
+ StreamGraphConfig.rename_library_search_path(_edited.path, path)
+ _edited.set_path(path)
+ _edited = null
+
+
+func _on_folder_dialog_canceled() -> void:
+ Util.safe_disconnect(folder_dialog.dir_selected, _on_folder_dialog_dir_selected)
+ Util.safe_disconnect(folder_dialog.dir_selected, _on_folder_dialog_dir_selected_new)
+ _edited = null
+
+
+class FolderView extends HBoxContainer:
+ const REMOVE_ICON := preload("res://graph_node_renderer/textures/remove-icon.svg")
+ const LOAD_ICON := preload("res://graph_node_renderer/textures/load-icon.svg")
+
+ const ARROW_DOWN_ICON := preload("res://graph_node_renderer/textures/arrow-down-icon.svg")
+ const ARROW_UP_ICON := preload("res://graph_node_renderer/textures/arrow-up-icon.svg")
+
+ var label: Label
+ var delete_button: Button
+ var open_path_button: Button
+
+ var move_down_button: Button
+ var move_up_button: Button
+
+ var path: String
+ var is_default: bool = false
+
+ signal open_dialog(path: String)
+ signal deleted()
+ signal moved()
+
+
+ func _init(p_path: String = "", p_default: bool = false) -> void:
+ path = p_path
+ is_default = p_default
+
+ label = Label.new()
+ label.text = path
+ label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
+ label.clip_text = true
+ label.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS
+ label.tooltip_text = path
+
+ delete_button = Button.new()
+ delete_button.flat = true
+ delete_button.icon = REMOVE_ICON
+ delete_button.disabled = p_default
+ delete_button.tooltip_text = "Delete"
+ delete_button.pressed.connect(
+ func():
+ deleted.emit()
+ StreamGraphConfig.remove_library_search_path(path)
+ queue_free()
+ )
+
+ open_path_button = Button.new()
+ open_path_button.flat = true
+ open_path_button.icon = LOAD_ICON
+ open_path_button.disabled = p_default
+ open_path_button.tooltip_text = "Select path"
+ open_path_button.pressed.connect(
+ func():
+ open_dialog.emit(path)
+ )
+
+ move_up_button = Button.new()
+ move_up_button.flat = true
+ move_up_button.icon = ARROW_UP_ICON
+ move_up_button.tooltip_text = "Move up"
+ move_up_button.pressed.connect(
+ func():
+ StreamGraphConfig.move_library_path_up(path)
+ get_parent().move_child(self, get_index() - 1)
+ moved.emit()
+ )
+
+ move_down_button = Button.new()
+ move_down_button.flat = true
+ move_down_button.icon = ARROW_DOWN_ICON
+ move_down_button.tooltip_text = "Move down"
+ move_down_button.pressed.connect(
+ func():
+ StreamGraphConfig.move_library_path_down(path)
+ get_parent().move_child(self, get_index() + 1)
+ moved.emit()
+ )
+
+ if p_default:
+ label.tooltip_text += "\nThis is a default path. It cannot be changed or removed."
+ delete_button.tooltip_text = "This is a default path. It cannot be changed or removed."
+ move_up_button.tooltip_text = "This is a default path. It cannot be changed or removed."
+ move_down_button.tooltip_text = "This is a default path. It cannot be changed or removed."
+ open_path_button.tooltip_text = "This is a default path. It cannot be changed or removed."
+ move_up_button.disabled = p_default
+ move_down_button.disabled = p_default
+
+ add_child(label)
+ add_child(delete_button)
+ add_child(open_path_button)
+ add_child(move_down_button)
+ add_child(move_up_button)
+
+
+ func set_path(p_path: String) -> void:
+ path = p_path
+ label.text = path
+
+
+ func calculate_move_buttons() -> void:
+ if is_default:
+ move_up_button.disabled = true
+ move_down_button.disabled = true
+ return
+
+ move_up_button.disabled = get_index() == 1
+ move_down_button.disabled = get_index() == get_parent().get_child_count() - 1
+
diff --git a/graph_node_renderer/settings/library_group_paths_editor.tscn b/graph_node_renderer/settings/library_group_paths_editor.tscn
new file mode 100644
index 0000000..55ff48c
--- /dev/null
+++ b/graph_node_renderer/settings/library_group_paths_editor.tscn
@@ -0,0 +1,60 @@
+[gd_scene load_steps=3 format=3 uid="uid://dq6hrbd6ev4ls"]
+
+[ext_resource type="Script" path="res://graph_node_renderer/settings/library_group_paths_editor.gd" id="1_swcyp"]
+[ext_resource type="Texture2D" uid="uid://drxi5ks3mqbnk" path="res://graph_node_renderer/textures/add_icon.svg" id="2_v1g1h"]
+
+[node name="LibraryGroupPathsEditor" type="PanelContainer"]
+offset_right = 272.0
+offset_bottom = 179.0
+size_flags_vertical = 3
+script = ExtResource("1_swcyp")
+
+[node name="MarginContainer" type="MarginContainer" parent="."]
+layout_mode = 2
+theme_override_constants/margin_left = 4
+theme_override_constants/margin_top = 4
+theme_override_constants/margin_right = 4
+theme_override_constants/margin_bottom = 4
+
+[node name="VBoxContainer" type="VBoxContainer" parent="MarginContainer"]
+layout_mode = 2
+
+[node name="PanelContainer" type="PanelContainer" parent="MarginContainer/VBoxContainer"]
+layout_mode = 2
+size_flags_vertical = 3
+
+[node name="MarginContainer" type="MarginContainer" parent="MarginContainer/VBoxContainer/PanelContainer"]
+layout_mode = 2
+theme_override_constants/margin_left = 8
+theme_override_constants/margin_right = 8
+
+[node name="ScrollContainer" type="ScrollContainer" parent="MarginContainer/VBoxContainer/PanelContainer/MarginContainer"]
+layout_mode = 2
+horizontal_scroll_mode = 0
+
+[node name="PathsContainer" type="VBoxContainer" parent="MarginContainer/VBoxContainer/PanelContainer/MarginContainer/ScrollContainer"]
+unique_name_in_owner = true
+layout_mode = 2
+size_flags_horizontal = 3
+size_flags_vertical = 3
+
+[node name="CenterContainer" type="CenterContainer" parent="MarginContainer/VBoxContainer"]
+layout_mode = 2
+
+[node name="AddButton" type="Button" parent="MarginContainer/VBoxContainer/CenterContainer"]
+unique_name_in_owner = true
+layout_mode = 2
+text = "Add"
+icon = ExtResource("2_v1g1h")
+
+[node name="Label" type="Label" parent="."]
+layout_mode = 2
+
+[node name="FolderDialog" type="FileDialog" parent="."]
+unique_name_in_owner = true
+title = "Open a Directory"
+initial_position = 2
+size = Vector2i(760, 500)
+ok_button_text = "Select Current Folder"
+file_mode = 2
+access = 2
diff --git a/graph_node_renderer/settings_dialog.gd b/graph_node_renderer/settings/settings_dialog.gd
similarity index 100%
rename from graph_node_renderer/settings_dialog.gd
rename to graph_node_renderer/settings/settings_dialog.gd
diff --git a/graph_node_renderer/settings_dialog.tscn b/graph_node_renderer/settings/settings_dialog.tscn
similarity index 74%
rename from graph_node_renderer/settings_dialog.tscn
rename to graph_node_renderer/settings/settings_dialog.tscn
index 79e3cb3..6c0bc40 100644
--- a/graph_node_renderer/settings_dialog.tscn
+++ b/graph_node_renderer/settings/settings_dialog.tscn
@@ -1,7 +1,8 @@
-[gd_scene load_steps=3 format=3 uid="uid://dodqetbke5wji"]
+[gd_scene load_steps=4 format=3 uid="uid://dodqetbke5wji"]
-[ext_resource type="Script" path="res://graph_node_renderer/settings_dialog.gd" id="1_lh25g"]
-[ext_resource type="PackedScene" uid="uid://bvjxc2vyx35b1" path="res://graph_node_renderer/shortcuts/shortcuts_editor.tscn" id="2_5tyfb"]
+[ext_resource type="Script" path="res://graph_node_renderer/settings/settings_dialog.gd" id="1_l08va"]
+[ext_resource type="PackedScene" uid="uid://dq6hrbd6ev4ls" path="res://graph_node_renderer/settings/library_group_paths_editor.tscn" id="2_f0uh5"]
+[ext_resource type="PackedScene" uid="uid://bvjxc2vyx35b1" path="res://graph_node_renderer/shortcuts/shortcuts_editor.tscn" id="2_pet1f"]
[node name="SettingsDialog" type="AcceptDialog"]
title = "Settings"
@@ -10,13 +11,13 @@ size = Vector2i(705, 370)
min_size = Vector2i(500, 250)
ok_button_text = "Close"
dialog_close_on_escape = false
-script = ExtResource("1_lh25g")
+script = ExtResource("1_l08va")
[node name="HSplitContainer" type="HSplitContainer" parent="."]
offset_left = 8.0
offset_top = 8.0
offset_right = 697.0
-offset_bottom = 321.0
+offset_bottom = 324.0
[node name="CategoryTree" type="Tree" parent="HSplitContainer"]
unique_name_in_owner = true
@@ -29,22 +30,26 @@ unique_name_in_owner = true
layout_mode = 2
size_flags_horizontal = 3
-[node name="General" type="VBoxContainer" parent="HSplitContainer/CategoryContent"]
-visible = false
+[node name="General" type="ScrollContainer" parent="HSplitContainer/CategoryContent"]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
+horizontal_scroll_mode = 0
-[node name="Label" type="Label" parent="HSplitContainer/CategoryContent/General"]
+[node name="VBoxContainer" type="VBoxContainer" parent="HSplitContainer/CategoryContent/General"]
+layout_mode = 2
+size_flags_horizontal = 3
+
+[node name="Label" type="Label" parent="HSplitContainer/CategoryContent/General/VBoxContainer"]
layout_mode = 2
text = "Library Group Search Paths"
-[node name="LibraryGroupPaths" type="PanelContainer" parent="HSplitContainer/CategoryContent/General"]
+[node name="LibraryGroupPathsEditor" parent="HSplitContainer/CategoryContent/General/VBoxContainer" instance=ExtResource("2_f0uh5")]
+custom_minimum_size = Vector2(0, 290)
layout_mode = 2
-size_flags_vertical = 3
[node name="Shortcuts" type="VBoxContainer" parent="HSplitContainer/CategoryContent"]
visible = false
@@ -54,6 +59,7 @@ anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
+size_flags_horizontal = 3
[node name="Label" type="Label" parent="HSplitContainer/CategoryContent/Shortcuts"]
layout_mode = 2
@@ -76,7 +82,7 @@ theme_override_constants/margin_top = 4
theme_override_constants/margin_right = 4
theme_override_constants/margin_bottom = 4
-[node name="ShortcutsEditor" parent="HSplitContainer/CategoryContent/Shortcuts/PanelContainer/ScrollContainer/MarginContainer" instance=ExtResource("2_5tyfb")]
+[node name="ShortcutsEditor" parent="HSplitContainer/CategoryContent/Shortcuts/PanelContainer/ScrollContainer/MarginContainer" instance=ExtResource("2_pet1f")]
unique_name_in_owner = true
layout_mode = 2
diff --git a/graph_node_renderer/shortcuts/shortcuts_editor.gd b/graph_node_renderer/shortcuts/shortcuts_editor.gd
index 32d4e45..528d61d 100644
--- a/graph_node_renderer/shortcuts/shortcuts_editor.gd
+++ b/graph_node_renderer/shortcuts/shortcuts_editor.gd
@@ -107,6 +107,16 @@ class ShortcutButton extends Button:
func _init(p_event: InputEvent) -> void:
toggle_mode = true
set_event(p_event)
+ button_down.connect(
+ func():
+ text += "..."
+ )
+
+ toggled.connect(
+ func(toggled_on: bool):
+ if not toggled_on:
+ text = _event.as_text()
+ )
## Sets the event this button is storing and updates its text.
diff --git a/graph_node_renderer/sidebar/accordion_menu.gd b/graph_node_renderer/sidebar/accordion_menu.gd
index 10f94c7..5988503 100644
--- a/graph_node_renderer/sidebar/accordion_menu.gd
+++ b/graph_node_renderer/sidebar/accordion_menu.gd
@@ -34,6 +34,11 @@ const COLLAPSE_ICON = preload("res://graph_node_renderer/textures/collapse-icon.
const COLLAPSE_ICON_COLLAPSED = preload("res://graph_node_renderer/textures/collapse-icon-collapsed.svg")
const BASE_MARGIN := 12
var _indent_level := 1
+var _ignore_child_lines: Array[int]
+var _title: String
+
+var _renamed_lambda = func():
+ _set_title(name)
func _enter_tree() -> void:
@@ -43,10 +48,10 @@ func _enter_tree() -> void:
_collapse_button = CollapseButton.new(collapsed, name)
_collapse_button.toggled.connect(set_collapsed)
- renamed.connect(
- func():
- _collapse_button.text = name
- )
+ if _title.is_empty():
+ renamed.connect(_renamed_lambda)
+ else:
+ _collapse_button.text = _title
add_child(_collapse_button, false, Node.INTERNAL_MODE_FRONT)
@@ -55,6 +60,20 @@ func _exit_tree() -> void:
_collapse_button.queue_free()
+func _set_title(title: String) -> void:
+ _title = title
+
+ if _collapse_button:
+ _collapse_button.text = title
+
+
+func set_title(title: String) -> void:
+ if renamed.is_connected(_renamed_lambda):
+ renamed.disconnect(_renamed_lambda)
+
+ _set_title(title)
+
+
func set_collapsed(v: bool) -> void:
collapsed = v
if _collapse_button:
@@ -88,6 +107,16 @@ func uncollapse() -> void:
update_minimum_size()
+func set_ignore_child_lines(idx: int, ignore: bool = true) -> void:
+ if idx not in _ignore_child_lines and ignore:
+ _ignore_child_lines.append(idx)
+ return
+
+ if idx in _ignore_child_lines and not ignore:
+ _ignore_child_lines.erase(idx)
+ return
+
+
func _notification(what: int) -> void:
if what == NOTIFICATION_PRE_SORT_CHILDREN:
for i in get_children(false):
@@ -122,6 +151,9 @@ func _draw() -> void:
# draw lines going out to each child
for child in get_children(false):
+ # skip children marked as ignored
+ if child.get_index(false) in _ignore_child_lines:
+ continue
# special case for if the child is also an accordion:
# draw the line pointing to the header
if child is AccordionMenu:
diff --git a/graph_node_renderer/sidebar/sidebar.gd b/graph_node_renderer/sidebar/sidebar.gd
index 7c95382..9674603 100644
--- a/graph_node_renderer/sidebar/sidebar.gd
+++ b/graph_node_renderer/sidebar/sidebar.gd
@@ -98,17 +98,20 @@ func set_edited_node(id: String = "") -> void:
edited_node_id = id
for i in node_menu.get_children():
- i.queue_free()
+ #i.queue_free()
+ i.free()
if id.is_empty():
var label := Label.new()
label.autowrap_mode = TextServer.AUTOWRAP_WORD
label.text = "There is no Node selected (or multiple are selected). Select a single Node to edit its properties."
node_menu.add_child(label)
+ node_menu.draw_tree = false
node_inspector = null
return
- await get_tree().process_frame
+ #await get_tree().process_frame
+ node_menu.draw_tree = true
node_inspector = NodeInspector.new(edited_deck_id, id)
node_inspector.go_to_node_requested.connect(
@@ -118,11 +121,16 @@ func set_edited_node(id: String = "") -> void:
for i in node_inspector.nodes:
node_menu.add_child(i)
- DeckHolder.get_deck(edited_deck_id).node_removed.connect(
- func(node: DeckNode):
- if node._id == edited_node_id:
- set_edited_node()
+ DeckHolder.get_deck(edited_deck_id).get_node(id).about_to_free.connect(
+ func():
+ set_edited_node()
)
+
+ #DeckHolder.get_deck(edited_deck_id).node_removed.connect(
+ #func(node: DeckNode):
+ #if node._id == edited_node_id:
+ #set_edited_node()
+ #)
class Inspector extends HBoxContainer:
@@ -144,19 +152,78 @@ class DeckInspector:
func add_inspector(text: String, control: Control = null) -> void:
nodes.append(Inspector.new(text, control))
+
+
+ func create_label(text: String) -> Label:
+ var l := Label.new()
+ l.text = text
+ l.size_flags_horizontal = Control.SIZE_EXPAND_FILL
+ return l
+
+
+ func create_hb_label(text: String, control: Control) -> HBoxContainer:
+ var hb := HBoxContainer.new()
+
+ control.size_flags_horizontal = Control.SIZE_EXPAND_FILL
+ hb.add_child(create_label(text))
+ hb.add_child(control)
+
+ return hb
func _init(id: String) -> void:
ref = weakref(DeckHolder.get_deck(id))
var deck: Deck = ref.get_ref() as Deck
- if deck.is_group:
- add_inspector("This deck is a group.")
+
+ var lib_menu := AccordionMenu.new()
+ lib_menu.set_title("Library Group")
+
+ var lib_group_text: String
+ if deck.is_library:
+ lib_group_text = "This deck is open as a library group. You may not edit it here.\nTo edit it, you have to open the file it's in."
+ elif not deck.is_group:
+ lib_group_text = "You may save this deck as a Library Group to reuse it in future decks.\nYou may edit how it will appear."
+ else:
+ lib_group_text = "This deck is a group."
+
+ var l := create_label(lib_group_text)
+ l.autowrap_mode = TextServer.AUTOWRAP_WORD
+ l.custom_minimum_size.x = 40
+ lib_menu.add_child(l)
+ lib_menu.set_ignore_child_lines(0)
+
+ var name_field := LineEdit.new()
+ name_field.placeholder_text = "Name"
+ name_field.text = deck.lib_name
+ name_field.text_changed.connect(
+ func(new_text: String):
+ deck.lib_name = new_text
+ )
+ name_field.editable = not deck.is_group
+ lib_menu.add_child(create_hb_label("Library name:", name_field))
+
+ var desc_field := TextEdit.new()
+ desc_field.wrap_mode = TextEdit.LINE_WRAPPING_BOUNDARY
+ desc_field.placeholder_text = "Description"
+ desc_field.text = deck.lib_description
+ desc_field.text_changed.connect(
+ func():
+ deck.lib_description = desc_field.text
+ )
+ desc_field.editable = not deck.is_group
+ desc_field.custom_minimum_size.y = 100
+ desc_field.custom_minimum_size.x = 100
+ lib_menu.add_child(create_hb_label("Library description:", desc_field))
+
+ nodes.append(lib_menu)
class NodeInspector:
var ref: WeakRef
var nodes: Array[Control]
+ var _name_field: LineEdit
+
signal go_to_node_requested()
var DESCRIPTOR_SCENES := {
@@ -181,6 +248,13 @@ class NodeInspector:
return l
+ func _name_field_rename(new_name: String) -> void:
+ if _name_field.has_focus():
+ return
+
+ _name_field.text = new_name
+
+
func _init(deck_id: String, id: String) -> void:
ref = weakref(DeckHolder.get_deck(deck_id).get_node(id))
var node: DeckNode = ref.get_ref() as DeckNode
@@ -201,18 +275,15 @@ class NodeInspector:
hb.add_child(copy_button)
add_inspector("Node Type:", hb)
- var name_field := LineEdit.new()
- name_field.placeholder_text = "Node name"
- name_field.text = node.name
- name_field.text_changed.connect(node.rename)
- node.renamed.connect(
- func(new_name: String) -> void:
- if name_field.has_focus():
- return
-
- name_field.text = new_name
- )
- add_inspector("Node name:", name_field)
+ _name_field = LineEdit.new()
+ _name_field.placeholder_text = "Node name"
+ _name_field.text = node.name
+ _name_field.text_changed.connect(node.rename)
+ _name_field.editable = not node._belonging_to.is_library
+
+ node.renamed.connect(_name_field_rename)
+
+ add_inspector("Node name:", _name_field)
var focus_button := Button.new()
focus_button.text = "Focus node"
@@ -233,11 +304,11 @@ class NodeInspector:
var ports_menu := AccordionMenu.new()
ports_menu.draw_background = true
var is_output := ports[0].port_type == DeckNode.PortType.OUTPUT
- ports_menu.set_name.call_deferred("Output Ports" if is_output else "Input Ports")
+ ports_menu.set_title("Output Ports" if is_output else "Input Ports")
for port in ports:
var acc := AccordionMenu.new()
acc.draw_background = true
- acc.name = "Port %s" % port.index_of_type
+ acc.set_title("Port %s" % port.index_of_type)
var label_label := create_label("Name: %s" % port.label)
acc.add_child(label_label)
diff --git a/graph_node_renderer/sidebar/sidebar.tscn b/graph_node_renderer/sidebar/sidebar.tscn
index 7d2ca47..3adf16c 100644
--- a/graph_node_renderer/sidebar/sidebar.tscn
+++ b/graph_node_renderer/sidebar/sidebar.tscn
@@ -4,6 +4,7 @@
[ext_resource type="Script" path="res://graph_node_renderer/sidebar/accordion_menu.gd" id="1_q1gqb"]
[node name="Sidebar" type="PanelContainer"]
+custom_minimum_size = Vector2(340, 0)
offset_right = 439.0
offset_bottom = 266.0
script = ExtResource("1_bcym7")
diff --git a/graph_node_renderer/textures/arrow-down-icon.svg b/graph_node_renderer/textures/arrow-down-icon.svg
new file mode 100644
index 0000000..23b7af8
--- /dev/null
+++ b/graph_node_renderer/textures/arrow-down-icon.svg
@@ -0,0 +1 @@
+
diff --git a/graph_node_renderer/textures/arrow-down-icon.svg.import b/graph_node_renderer/textures/arrow-down-icon.svg.import
new file mode 100644
index 0000000..db72c7a
--- /dev/null
+++ b/graph_node_renderer/textures/arrow-down-icon.svg.import
@@ -0,0 +1,37 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://cl32inr5ja2kd"
+path="res://.godot/imported/arrow-down-icon.svg-744ecac080abb04bb3e26e5dc62154de.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://graph_node_renderer/textures/arrow-down-icon.svg"
+dest_files=["res://.godot/imported/arrow-down-icon.svg-744ecac080abb04bb3e26e5dc62154de.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
+svg/scale=1.0
+editor/scale_with_editor_scale=false
+editor/convert_colors_with_editor_theme=false
diff --git a/graph_node_renderer/textures/arrow-up-icon.svg b/graph_node_renderer/textures/arrow-up-icon.svg
new file mode 100644
index 0000000..377d275
--- /dev/null
+++ b/graph_node_renderer/textures/arrow-up-icon.svg
@@ -0,0 +1 @@
+
diff --git a/graph_node_renderer/textures/arrow-up-icon.svg.import b/graph_node_renderer/textures/arrow-up-icon.svg.import
new file mode 100644
index 0000000..d51bb60
--- /dev/null
+++ b/graph_node_renderer/textures/arrow-up-icon.svg.import
@@ -0,0 +1,37 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://bepbho6of3lad"
+path="res://.godot/imported/arrow-up-icon.svg-e0c45dce0c11c3066f4517489a55f98f.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://graph_node_renderer/textures/arrow-up-icon.svg"
+dest_files=["res://.godot/imported/arrow-up-icon.svg-e0c45dce0c11c3066f4517489a55f98f.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
+svg/scale=1.0
+editor/scale_with_editor_scale=false
+editor/convert_colors_with_editor_theme=false
diff --git a/graph_node_renderer/textures/group_icon.svg b/graph_node_renderer/textures/group_icon.svg
new file mode 100644
index 0000000..133695e
--- /dev/null
+++ b/graph_node_renderer/textures/group_icon.svg
@@ -0,0 +1 @@
+
diff --git a/graph_node_renderer/textures/group_icon.svg.import b/graph_node_renderer/textures/group_icon.svg.import
new file mode 100644
index 0000000..f1b596f
--- /dev/null
+++ b/graph_node_renderer/textures/group_icon.svg.import
@@ -0,0 +1,37 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://ysy33f5r2ji3"
+path="res://.godot/imported/group_icon.svg-553babd1069b83fbb64a3e7775cbe41e.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://graph_node_renderer/textures/group_icon.svg"
+dest_files=["res://.godot/imported/group_icon.svg-553babd1069b83fbb64a3e7775cbe41e.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
+svg/scale=1.0
+editor/scale_with_editor_scale=false
+editor/convert_colors_with_editor_theme=false
diff --git a/graph_node_renderer/textures/load-icon.svg b/graph_node_renderer/textures/load-icon.svg
new file mode 100644
index 0000000..a4a6b30
--- /dev/null
+++ b/graph_node_renderer/textures/load-icon.svg
@@ -0,0 +1 @@
+
diff --git a/graph_node_renderer/textures/load-icon.svg.import b/graph_node_renderer/textures/load-icon.svg.import
new file mode 100644
index 0000000..ace90ab
--- /dev/null
+++ b/graph_node_renderer/textures/load-icon.svg.import
@@ -0,0 +1,37 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://bbyje126vhdbe"
+path="res://.godot/imported/load-icon.svg-fefb135ef5d746efaeb8c59f181e864e.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://graph_node_renderer/textures/load-icon.svg"
+dest_files=["res://.godot/imported/load-icon.svg-fefb135ef5d746efaeb8c59f181e864e.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
+svg/scale=1.0
+editor/scale_with_editor_scale=false
+editor/convert_colors_with_editor_theme=false
diff --git a/graph_node_renderer/toast_renderer.gd b/graph_node_renderer/toast_renderer.gd
index 3c1b8ef..1fafecc 100644
--- a/graph_node_renderer/toast_renderer.gd
+++ b/graph_node_renderer/toast_renderer.gd
@@ -16,15 +16,21 @@ const INFO_ICON = preload("res://graph_node_renderer/textures/info_icon.svg")
func _ready() -> void:
+ for i in DeckHolder.logger.toast_history.duplicate():
+ _on_logger_toast(i.text, i.type)
DeckHolder.logger.log_toast.connect(_on_logger_toast)
func _on_logger_toast(text: String, type: Logger.LogType) -> void:
+ DeckHolder.logger.toast_history.remove_at(0)
var panel := PanelContainer.new()
+ panel.mouse_filter = Control.MOUSE_FILTER_IGNORE
panel.theme_type_variation = &"ToastPanel"
var hb := HBoxContainer.new()
+ hb.mouse_filter = Control.MOUSE_FILTER_IGNORE
var icon := TextureRect.new()
icon.expand_mode = TextureRect.EXPAND_FIT_WIDTH_PROPORTIONAL
+ icon.mouse_filter = Control.MOUSE_FILTER_IGNORE
match type:
Logger.LogType.INFO:
@@ -36,6 +42,7 @@ func _on_logger_toast(text: String, type: Logger.LogType) -> void:
var label := Label.new()
label.text = text
+ label.mouse_filter = Control.MOUSE_FILTER_IGNORE
hb.add_child(icon)
hb.add_child(label)
diff --git a/graph_node_renderer/toast_renderer.tscn b/graph_node_renderer/toast_renderer.tscn
index 969d00e..e6ab20a 100644
--- a/graph_node_renderer/toast_renderer.tscn
+++ b/graph_node_renderer/toast_renderer.tscn
@@ -5,6 +5,7 @@
[node name="ToastRenderer" type="Control"]
layout_mode = 3
anchors_preset = 0
+mouse_filter = 2
script = ExtResource("1_446pb")
[node name="VBoxContainer" type="VBoxContainer" parent="."]
@@ -21,7 +22,5 @@ offset_right = -32.0
offset_bottom = -32.0
grow_horizontal = 0
grow_vertical = 0
+mouse_filter = 2
alignment = 2
-
-[node name="PanelContainer" type="PanelContainer" parent="VBoxContainer"]
-layout_mode = 2
diff --git a/main.gd b/main.gd
index ebd7594..6327dce 100644
--- a/main.gd
+++ b/main.gd
@@ -27,6 +27,7 @@ func _on_deck_holder_renderer_quit_completed() -> void:
# will be used later to make sure both default and rpc are finished processing
deck_holder_renderer_finished = true
+ DeckHolder.pre_exit_cleanup()
get_tree().quit()