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