library groups (#151)

~~cl0ses #51~~
~~cl0ses #93~~
~~cl0ses #98~~
~~cl0ses #150~~

another change in this PR: the Deck and DeckNode classes now use manual memory management.

Reviewed-on: https://codeberg.org/StreamGraph/StreamGraph/pulls/151
Co-authored-by: Lera Elvoé <yagich@poto.cafe>
Co-committed-by: Lera Elvoé <yagich@poto.cafe>
This commit is contained in:
Lera Elvoé 2024-05-08 07:43:45 +00:00 committed by yagich
parent a80af97377
commit 240750c48e
42 changed files with 1284 additions and 273 deletions

3
.gitignore vendored
View file

@ -3,3 +3,6 @@
# distribution folder
dist/*/
# vscode folder
.vscode/

View file

@ -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": {},

View file

@ -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

View file

@ -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()

View file

@ -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)

View file

@ -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

View file

@ -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)

View file

@ -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

View file

@ -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)

View file

@ -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)

View file

@ -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

View file

@ -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))

View file

@ -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)

View file

@ -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)

View file

@ -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].

View file

@ -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"]

View file

@ -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)

View file

@ -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

View file

@ -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:

View file

@ -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:

View file

@ -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:

View file

@ -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():

View file

@ -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:

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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.

View file

@ -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:

View file

@ -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)

View file

@ -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")

View file

@ -0,0 +1 @@
<svg height="16" viewBox="0 0 16 16" width="16" xmlns="http://www.w3.org/2000/svg"><path d="M8 5v7l4-4m-4 4L4 8" fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" stroke="#e0e0e0"/></svg>

After

Width:  |  Height:  |  Size: 214 B

View file

@ -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

View file

@ -0,0 +1 @@
<svg height="16" viewBox="0 0 16 16" width="16" xmlns="http://www.w3.org/2000/svg"><path d="M8 11V4L4 8m4-4 4 4" fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" stroke="#e0e0e0"/></svg>

After

Width:  |  Height:  |  Size: 214 B

View file

@ -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

View file

@ -0,0 +1 @@
<svg height="16" viewBox="0 0 16 16" width="16" xmlns="http://www.w3.org/2000/svg"><path d="M7 1v6H1v8h8V9h6V1zm2 2h4v4H9z" fill="#e0e0e0" fill-opacity=".4"/><path d="M1 1v2h2V1H1zm12 0v2h2V1h-2zM1 13v2h2v-2H1zm12 0v2h2v-2h-2z" fill="#fff"/></svg>

After

Width:  |  Height:  |  Size: 248 B

View file

@ -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

View file

@ -0,0 +1 @@
<svg height="16" viewBox="0 0 16 16" width="16" xmlns="http://www.w3.org/2000/svg"><path d="M3 2a2 2 0 0 0-2 2v9a2 2 0 0 0 2 2h9c1.105 0 1.818-.91 2-2l1-6a1 1 0 0 0-1-1H6c-.552 0-.909.455-1 1l-1 6c-.091.545-.448 1-1 1a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1 1 1 0 0 0 1 1h5a1 1 0 0 0-1-1H7a2 2 0 0 0-2-2H3z" fill="#e0e0e0"/></svg>

After

Width:  |  Height:  |  Size: 337 B

View file

@ -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

View file

@ -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)

View file

@ -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

View file

@ -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()