miggor-StreamGraph/classes/deck/deck_holder.gd
Lera Elvoé f720efcc72 lib group storage rework (#162)
saving decks that use lib groups will no longer save the whole file path to that library (at the expense of the structure needing to be the same)

also some ui/ux improvements:
- more menus in sidebar remember their collapsed state between deck/node switches
- adding a lib group will name the group node appropriately
- save dialog properly remembers the most recent path when invoked via ctrl+s and the deck hasn't been saved before

Reviewed-on: https://codeberg.org/StreamGraph/StreamGraph/pulls/162
Co-authored-by: Lera Elvoé <yagich@poto.cafe>
Co-committed-by: Lera Elvoé <yagich@poto.cafe>
2024-05-26 14:13:42 +00:00

406 lines
13 KiB
GDScript

# (c) 2023-present Eroax
# (c) 2023-present Yagich
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
class_name DeckHolder
## @experimental
## A static class holding references to all decks opened in the current session.
## List of decks opened this session.
#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()
static var signals := Signals.new()
enum Compat {
CONNECTIONS_IN_DECK = 1 << 0,
}
static func _static_init() -> void:
NodeDB.init()
## Returns a new empty deck and assigns a new random ID to it.
static func add_empty_deck() -> Deck:
var deck := Deck.new()
var uuid := UUID.v4()
decks[uuid] = deck
deck.id = uuid
signals.deck_added.emit(uuid)
print_verbose("DeckHolder: added empty deck %s, id %s" % [deck.id, deck.get_instance_id()])
return deck
static func get_deck_compat(data: Dictionary) -> int:
var res := 0
if not data.deck.has("connections"):
res |= Compat.CONNECTIONS_IN_DECK
return res
static func apply_compat_patches(data: Dictionary, compat: int) -> void:
if compat & Compat.CONNECTIONS_IN_DECK:
# convert pre-0.0.6 connections to be stored in the deck instead
var connections := Deck.NodeConnections.new()
for node: String in data.deck.nodes:
for from_port in data.deck.nodes[node].outgoing_connections:
for to_node: String in data.deck.nodes[node].outgoing_connections[from_port]:
for to_port: int in data.deck.nodes[node].outgoing_connections[from_port][to_node]:
connections.add_connection(node, to_node, int(from_port), to_port)
data.deck.connections = connections.to_dict()
## Opens a deck from the [param path].
static func open_deck_from_file(path: String) -> Deck:
var f := FileAccess.open(path, FileAccess.READ)
if f.get_error() != OK:
return null
var deck := open_deck_from_dict(JSON.parse_string(f.get_as_text()), path)
return deck
static func open_deck_from_dict(data: Dictionary, path := "") -> Deck:
if get_deck_compat(data) != 0:
apply_compat_patches(data, get_deck_compat(data))
var deck := Deck.from_dict(data, path)
decks[deck.id] = deck
signals.deck_added.emit(deck.id)
print_verbose("DeckHolder: opened deck %s, id %s" % [deck.id, deck.get_instance_id()])
return deck
static func add_group_from_dict(data: Dictionary, deck_id: String, instance_id: String, parent: String = "") -> Deck:
var group := Deck.from_dict(data)
group.instance_id = instance_id
group.is_group = true
group._belonging_to = parent
group.group_input_node = data.deck.group_input_node
group.group_output_node = data.deck.group_output_node
var instances: Dictionary = decks.get(deck_id, {})
instances[instance_id] = group
decks[deck_id] = instances
connect_group_signals(group)
if deck_id not in groups_emitted:
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
static func make_new_group_instance(group_id: String, parent: String = "") -> Deck:
var group := get_deck(group_id)
var data := group.to_dict()
var inst := add_group_from_dict(data, group_id, UUID.v4(), parent)
# copy connections
inst.connections = group.connections
return inst
static func make_group_instance_unique(group_id: String, instance_id: String, parent_deck_id: String, group_node_id: String) -> Deck:
if not decks.has(group_id):
return null
var group_node := get_deck(parent_deck_id).get_node(group_node_id)
var inst := get_group_instance(group_id, instance_id)
var new_id := UUID.v4()
var new_inst_id := UUID.v4()
inst.id = new_id
group_node.group_id = new_id
group_node.group_instance_id = new_inst_id
var data := inst.to_dict()
#data.instance_id = new_inst_id
(decks[group_id] as Dictionary).erase(instance_id)
var new_instance := add_group_from_dict(data, new_id, new_inst_id, parent_deck_id)
group_node.init_io()
#TODO: some weird close bug
var instances := inst.get_referenced_group_instances()
for instance: Dictionary in instances:
make_group_instance_unique(instance.group_id, instance.instance_id, instance.parent_id, instance.group_node)
return new_instance
static func add_empty_group(parent: String = "") -> Deck:
var group := Deck.new()
group.is_group = true
group.id = UUID.v4()
group._belonging_to = parent
group.instance_id = UUID.v4()
decks[group.id] = {group.instance_id: group}
connect_group_signals(group)
if group.id not in groups_emitted:
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(type):
deck_data = (lib_groups[type].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(type, {}) as Dictionary
var deck := Deck.from_dict(deck_data)
deck.id = type
deck.instance_id = instance_id
deck.is_group = true
deck.is_library = true
deck._belonging_to = to_deck
instances[instance_id] = deck
lib_groups[type] = 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)
group.node_port_value_updated.connect(DeckHolder._on_node_port_value_updated)
group.node_ports_updated.connect(DeckHolder._on_node_ports_updated)
group.node_renamed.connect(DeckHolder._on_node_renamed)
group.node_moved.connect(DeckHolder._on_node_moved)
static func get_deck(id: String) -> Deck:
if not decks.has(id):
return get_lib(id)
if not decks[id] is Dictionary:
return decks[id]
else:
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 get_lib_instance(group_id, instance_id)
if decks[group_id] is Dictionary:
return (decks[group_id] as Dictionary).get(instance_id)
else:
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 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)
from.erase(group_id)
static func close_all_group_instances(group_id: String) -> void:
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
## closing this deck.
static func close_deck(deck_id: String) -> Array:
if decks.get(deck_id) is Deck:
var deck: Deck = decks[deck_id] as Deck
var groups := deck.get_referenced_groups()
for group in groups:
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:
var deck: Deck = decks[deck_id]
if not is_instance_valid(deck):
continue
deck.send_event(event_name, event_data)
else:
for deck_instance_id: String in decks[deck_id]:
var deck: Deck = decks[deck_id][deck_instance_id]
if not is_instance_valid(deck):
continue
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]:
var deck: Deck = lib_groups[lib_group_id][lib_instance_id]
if not is_instance_valid(deck):
continue
deck.send_event(event_name, event_data)
#region group signal callbacks
static func _on_node_added_to_group(node: DeckNode, assign_id: String, assign_to_self: bool, deck: Deck) -> void:
var group_id := deck.id
for instance_id: String in decks[group_id]:
if instance_id == deck.instance_id:
continue
var instance: Deck = get_group_instance(group_id, instance_id)
instance.emit_group_signals = false
var node_duplicate := DeckNode.from_dict(node.to_dict())
instance.add_node_inst(node_duplicate, assign_id, assign_to_self)
instance.emit_group_signals = true
static func _on_node_removed_from_group(node_id: String, remove_connections: bool, deck: Deck) -> void:
var group_id := deck.id
for instance_id: String in decks[group_id]:
if instance_id == deck.instance_id:
continue
var instance: Deck = get_group_instance(group_id, instance_id)
instance.emit_group_signals = false
instance.remove_node(node_id, remove_connections)
instance.emit_group_signals = true
static func _on_node_port_value_updated(node_id: String, port_idx: int, new_value: Variant, deck: Deck) -> void:
var group_id := deck.id
for instance_id: String in decks[group_id]:
if instance_id == deck.instance_id:
continue
var instance: Deck = get_group_instance(group_id, instance_id)
instance.emit_group_signals = false
instance.get_node(node_id).get_all_ports()[port_idx].set_value_no_signal(new_value)
instance.emit_group_signals = true
static func _on_node_ports_updated(node_id: String, deck: Deck) -> void:
var group_id := deck.id
var original_node := deck.get_node(node_id)
for instance_id: String in decks[group_id]:
if instance_id == deck.instance_id:
continue
var instance: Deck = get_group_instance(group_id, instance_id)
instance.emit_group_signals = false
var node := instance.get_node(node_id)
node.ports.clear()
for port in original_node.ports:
node.add_port(
port.type,
port.label,
port.port_type,
port.index_of_type,
port.descriptor,
port.usage_type,
)
instance.emit_group_signals = true
static func _on_node_renamed(node_id: String, new_name: String, deck: Deck) -> void:
var group_id := deck.id
for instance_id: String in decks[group_id]:
if instance_id == deck.instance_id:
continue
var instance: Deck = get_group_instance(group_id, instance_id)
instance.emit_group_signals = false
instance.get_node(node_id).name = new_name
instance.emit_group_signals = true
static func _on_node_moved(node_id: String, new_position: Dictionary, deck: Deck) -> void:
var group_id := deck.id
for instance_id: String in decks[group_id]:
if instance_id == deck.instance_id:
continue
var instance: Deck = get_group_instance(group_id, instance_id)
instance.emit_group_signals = false
instance.get_node(node_id).position = new_position.duplicate()
instance.emit_group_signals = true
#endregion
class Signals:
signal deck_added(deck_id: String)
signal deck_closed(deck_id: String)