From eeb509cae07616521813c5ebb041ce80fb198366 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lera=20Elvo=C3=A9?= Date: Thu, 23 Nov 2023 06:38:10 +0000 Subject: [PATCH] add node menu (#3) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit we finally have a way to add nodes from a menu!! lets goooooooo Reviewed-on: https://codeberg.org/Eroax/Re-DotDeck/pulls/3 Co-authored-by: Lera ElvoƩ Co-committed-by: Lera ElvoƩ --- classes/deck/deck_node.gd | 2 + classes/deck/node_db.gd | 75 ++++- classes/deck/nodes/button.gd | 1 + classes/deck/nodes/get_deck_var.gd | 1 + classes/deck/nodes/group_input_node.gd | 1 + classes/deck/nodes/group_node.gd | 1 + classes/deck/nodes/group_output_node.gd | 1 + classes/deck/nodes/print.gd | 1 + classes/deck/nodes/set_deck_var.gd | 1 + classes/deck/nodes/string_constant.gd | 2 + classes/deck/nodes/test_interleaved_node.gd | 1 + classes/deck/search_provider.gd | 99 ++++++ graph_node_renderer/add_node_menu.gd | 306 ++++++++++++++++++ graph_node_renderer/add_node_menu.tscn | 44 +++ .../deck_renderer_graph_edit.gd | 95 +++--- .../deck_renderer_graph_edit.tscn | 1 + .../textures/collapse-icon-collapsed.svg | 85 +++++ .../collapse-icon-collapsed.svg.import | 37 +++ .../textures/collapse-icon.svg | 85 +++++ .../textures/collapse-icon.svg.import | 37 +++ .../textures/favorite-icon.svg | 1 + .../textures/favorite-icon.svg.import | 37 +++ .../textures/non-favorite-icon.svg | 1 + .../textures/non-favorite-icon.svg.import | 37 +++ 24 files changed, 891 insertions(+), 61 deletions(-) create mode 100644 classes/deck/search_provider.gd create mode 100644 graph_node_renderer/add_node_menu.gd create mode 100644 graph_node_renderer/add_node_menu.tscn create mode 100644 graph_node_renderer/textures/collapse-icon-collapsed.svg create mode 100644 graph_node_renderer/textures/collapse-icon-collapsed.svg.import create mode 100644 graph_node_renderer/textures/collapse-icon.svg create mode 100644 graph_node_renderer/textures/collapse-icon.svg.import create mode 100644 graph_node_renderer/textures/favorite-icon.svg create mode 100644 graph_node_renderer/textures/favorite-icon.svg.import create mode 100644 graph_node_renderer/textures/non-favorite-icon.svg create mode 100644 graph_node_renderer/textures/non-favorite-icon.svg.import diff --git a/classes/deck/deck_node.gd b/classes/deck/deck_node.gd index ff7b95e..8e64178 100644 --- a/classes/deck/deck_node.gd +++ b/classes/deck/deck_node.gd @@ -15,6 +15,8 @@ var node_type: String var description: String var aliases: Array[String] +var category: String +var appears_in_search: bool = true var props_to_serialize: Array[StringName] diff --git a/classes/deck/node_db.gd b/classes/deck/node_db.gd index 908e02f..d6d5314 100644 --- a/classes/deck/node_db.gd +++ b/classes/deck/node_db.gd @@ -2,12 +2,16 @@ extends Node const BASE_NODE_PATH := "res://classes/deck/nodes/" const NODE_INDEX_CACHE_PATH := "user://nodes_index.json" +const FAVORITE_NODES_PATH := "user://favorite_nodes.json" + +var favorite_nodes: Array[String] # Dictionary[node_type, NodeDescriptor] var nodes: Dictionary = {} func _init() -> void: + load_favorites() #if load_node_index(): #return @@ -23,7 +27,15 @@ func _init() -> void: func(accum, el) -> void: accum += el , "") - var descriptor := NodeDescriptor.new(script_path, node.name, node.description, aliases) + var descriptor := NodeDescriptor.new( + script_path, + node.name, + node.node_type, + node.description, + aliases, + node.category, + node.appears_in_search, + ) nodes[node.node_type] = descriptor current_file = dir.get_next() @@ -68,28 +80,71 @@ func load_node_index() -> bool: return true +func set_node_favorite(node_type: String, favorite: bool) -> void: + if (favorite && node_type in favorite_nodes) || (!favorite && !(node_type in favorite_nodes)): + return + + if favorite: + favorite_nodes.append(node_type) + else: + favorite_nodes.erase(node_type) + + var f := FileAccess.open(FAVORITE_NODES_PATH, FileAccess.WRITE) + f.store_string(JSON.stringify(favorite_nodes, "\t")) + + +func load_favorites() -> void: + var f := FileAccess.open(FAVORITE_NODES_PATH, FileAccess.READ) + if !f: + return + var data: Array = JSON.parse_string(f.get_as_text()) + favorite_nodes.clear() + favorite_nodes.assign(data) + + +func is_node_favorite(node_type: String) -> bool: + return node_type in favorite_nodes + + class NodeDescriptor: var name: String + var type: String var description: String var aliases: String + var category: String + var appears_in_search: bool var script_path: String - func _init(p_script_path: String, p_name: String, p_description: String, p_aliases: String) -> void: - script_path = p_script_path + func _init( + p_script_path: String, + p_name: String, + p_type: String, + p_description: String, + p_aliases: String, + p_category: String, + p_appears_in_search: bool, + ) -> void: + script_path = p_script_path - name = p_name - description = p_description - aliases = p_aliases + name = p_name + type = p_type + description = p_description + aliases = p_aliases + category = p_category + appears_in_search = p_appears_in_search func to_dictionary() -> Dictionary: var d := { "name": name, + "type": type, "description": description, "aliases": aliases, - "script_path": script_path + "script_path": script_path, + "category": category, + "appears_in_search": appears_in_search, } return d @@ -98,7 +153,11 @@ class NodeDescriptor: var nd := NodeDescriptor.new( data.get("script_path", ""), data.get("name", ""), + data.get("type", ""), data.get("description", ""), - data.get("aliases", "")) + data.get("aliases", ""), + data.get("category", ""), + data.get("appears_in_search", false), + ) return nd diff --git a/classes/deck/nodes/button.gd b/classes/deck/nodes/button.gd index 3bb3b15..ef3c9b5 100644 --- a/classes/deck/nodes/button.gd +++ b/classes/deck/nodes/button.gd @@ -5,6 +5,7 @@ func _init() -> void: name = "Button" node_type = "button" description = "a button" + category = "general" add_output_port( Deck.Types.BOOL, diff --git a/classes/deck/nodes/get_deck_var.gd b/classes/deck/nodes/get_deck_var.gd index b37a4ab..63301d9 100644 --- a/classes/deck/nodes/get_deck_var.gd +++ b/classes/deck/nodes/get_deck_var.gd @@ -5,6 +5,7 @@ func _init() -> void: name = "Get Deck Var" node_type = name.to_snake_case() description = "retrieve a deck variable" + category = "general" add_output_port( Deck.Types.STRING, diff --git a/classes/deck/nodes/group_input_node.gd b/classes/deck/nodes/group_input_node.gd index 5881f42..249e668 100644 --- a/classes/deck/nodes/group_input_node.gd +++ b/classes/deck/nodes/group_input_node.gd @@ -9,6 +9,7 @@ func _init() -> void: name = "Group input" node_type = "group_input" props_to_serialize = [&"output_count"] + appears_in_search = false add_output_port( Deck.Types.STRING, diff --git a/classes/deck/nodes/group_node.gd b/classes/deck/nodes/group_node.gd index 2942e93..eb1c7f9 100644 --- a/classes/deck/nodes/group_node.gd +++ b/classes/deck/nodes/group_node.gd @@ -11,6 +11,7 @@ func _init() -> void: name = "Group" node_type = "group_node" props_to_serialize = [&"group_id", &"extra_ports"] + appears_in_search = false func _pre_connection() -> void: diff --git a/classes/deck/nodes/group_output_node.gd b/classes/deck/nodes/group_output_node.gd index efcf7d8..5eee3d9 100644 --- a/classes/deck/nodes/group_output_node.gd +++ b/classes/deck/nodes/group_output_node.gd @@ -9,6 +9,7 @@ func _init() -> void: name = "Group output" node_type = "group_output" props_to_serialize = [&"input_count"] + appears_in_search = false add_input_port( Deck.Types.STRING, diff --git a/classes/deck/nodes/print.gd b/classes/deck/nodes/print.gd index 1e447b6..d70a660 100644 --- a/classes/deck/nodes/print.gd +++ b/classes/deck/nodes/print.gd @@ -9,6 +9,7 @@ func _init() -> void: description = "print a value" props_to_serialize = [&"times_activated"] + category = "general" add_input_port( Deck.Types.STRING, diff --git a/classes/deck/nodes/set_deck_var.gd b/classes/deck/nodes/set_deck_var.gd index a8e43e2..6d5b1cb 100644 --- a/classes/deck/nodes/set_deck_var.gd +++ b/classes/deck/nodes/set_deck_var.gd @@ -5,6 +5,7 @@ func _init() -> void: name = "Set Deck Var" node_type = name.to_snake_case() description = "set deck variable" + category = "general" add_input_port( Deck.Types.STRING, diff --git a/classes/deck/nodes/string_constant.gd b/classes/deck/nodes/string_constant.gd index c58f41c..530de33 100644 --- a/classes/deck/nodes/string_constant.gd +++ b/classes/deck/nodes/string_constant.gd @@ -4,6 +4,8 @@ extends DeckNode func _init() -> void: node_type = "string_constant" name = "String Constant" + category = "general" + add_output_port( Deck.Types.STRING, "Text", diff --git a/classes/deck/nodes/test_interleaved_node.gd b/classes/deck/nodes/test_interleaved_node.gd index 1970b2d..f2cfc8d 100644 --- a/classes/deck/nodes/test_interleaved_node.gd +++ b/classes/deck/nodes/test_interleaved_node.gd @@ -4,6 +4,7 @@ extends DeckNode func _init() -> void: node_type = "test_interleaved" name = "Test Interleaved" + category = "test" for i in 4: add_output_port( diff --git a/classes/deck/search_provider.gd b/classes/deck/search_provider.gd new file mode 100644 index 0000000..cf8eeab --- /dev/null +++ b/classes/deck/search_provider.gd @@ -0,0 +1,99 @@ +class_name SearchProvider + +static var filters: Array[Filter] = [ + # favorites filter. will only show nodes marked as favorite. syntax: "#f" + Filter.new( + func(search_string: String) -> bool: + return "#f" in search_string, + + func(element: NodeDB.NodeDescriptor, _search_string: String, _pre_strip_string: String) -> bool: + return NodeDB.is_node_favorite(element.type), + + func(search_string: String) -> String: + return search_string.replace("#f", "") + ), + + # category filter. will only match nodes that are in a certain category. syntax: "#c category_name" + Filter.new( + func(search_string: String) -> bool: + const p := r"#c\s[\w]+" + var r := RegEx.create_from_string(p) + var c := r.search(search_string) + + return c != null, + + func(element: NodeDB.NodeDescriptor, _search_string: String, pre_strip_string: String) -> bool: + const p := r"#c\s[\w]+" + var r := RegEx.create_from_string(p) + print("pre: ", pre_strip_string) + var c := r.search(pre_strip_string).get_string().split("#c ", false)[0] + + return c.is_subsequence_ofn(element.category), + + func(search_string: String) -> String: + const p := r"#c\s[\w]+" + var r := RegEx.create_from_string(p) + var c := r.search(search_string).get_string() + prints("c:", c, "r:", search_string.replace(c, "")) + return search_string.replace(c, "") + ), +] + + +static func search(term: String) -> Array[NodeDB.NodeDescriptor]: + var res: Array[NodeDB.NodeDescriptor] = [] + + var filters_to_apply := filters.filter( + func(f: Filter): + return f.should_apply.call(term) + ) + var cleaned_search_string := term + # strip string of filter-specific substrings + for f: Filter in filters_to_apply: + f.pre_strip_string = cleaned_search_string + cleaned_search_string = f.strip_string.call(cleaned_search_string) + cleaned_search_string = cleaned_search_string.strip_edges() + + for node_type: String in NodeDB.nodes: + var nd: NodeDB.NodeDescriptor = NodeDB.nodes[node_type] + if !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 + + # apply filters + var filtered_res: Array[NodeDB.NodeDescriptor] = res.duplicate() + for f: Filter in filters_to_apply: + filtered_res = filtered_res.filter(f.match_term.bind(cleaned_search_string, f.pre_strip_string)) + + return filtered_res + + +class Filter: + ## Return [code]true[/code] if this filter should be applied to the search.[br] + ## [code]Callable(search_string: String) -> bool[/code] + var should_apply: Callable + + ## Return a [code]bool[/code] if the provided [code]NodeDescriptor[/code] + ## should be included in the search results array.[br] + ## [code]Callable(element: NodeDB.NodeDescriptor, search_string: String, pre_strip_string: String) -> bool[/code] + var match_term: Callable + + ## Return a string that's stripped of this filter's shorthand. + ## [code]Callable(search_string: String) -> String[/code] + var strip_string: Callable + + ## The search string as it was before [member strip_string] was called. Useful for filters that use arguments, like the category filter. + var pre_strip_string: String + + + func _init(p_should_apply: Callable, p_match_term: Callable, p_strip_string: Callable) -> void: + should_apply = p_should_apply + match_term = p_match_term + strip_string = p_strip_string diff --git a/graph_node_renderer/add_node_menu.gd b/graph_node_renderer/add_node_menu.gd new file mode 100644 index 0000000..3c5e74f --- /dev/null +++ b/graph_node_renderer/add_node_menu.gd @@ -0,0 +1,306 @@ +extends MarginContainer +class_name AddNodeMenu + +@onready var search_line_edit: LineEdit = %SearchLineEdit +@onready var scroll_content_container: VBoxContainer = %ScrollContentContainer +@onready var scroll_container: ScrollContainer = $VBoxContainer/ScrollContainer + +var categories: Dictionary = {} # Dictionary[String, Category] +var collapsed_categories: Array[String] + +signal node_selected(type: String) + + +func _ready() -> void: + search("") + + +func add_category(category_name: String) -> void: + var c := Category.new(category_name.capitalize()) + categories[category_name] = c + scroll_content_container.add_child(c) + c.collapse_toggled.connect(_on_category_collapse_toggled.bind(category_name)) + c.item_pressed.connect( + func(item: int): + node_selected.emit(c.get_item_metadata(item, "type")) + ) + c.item_favorite_button_toggled.connect( + func(item: int, toggled: bool): + NodeDB.set_node_favorite(c.get_item_metadata(item, "type"), toggled) + ) + + +func add_category_item(category: String, item: String, tooltip: String = "", favorite: bool = false) -> void: + var c: Category = categories[category] + c.add_item(item, tooltip, favorite) + + +func add_item(category: String, item: String, tooltip: String = "", favorite: bool = false) -> void: + if !categories.has(category): + add_category(category) + + add_category_item(category, item, tooltip, favorite) + + +func get_category(category: String) -> Category: + return categories[category] + + +func focus_search_bar() -> void: + search_line_edit.select_all() + search_line_edit.grab_focus() + + +func search(text: String) -> void: + scroll_content_container.get_children().map(func(c: Node): c.queue_free()) + categories.clear() + + var search_results := SearchProvider.search(text) + if search_results.is_empty(): + return + + for nd in search_results: + add_item(nd.category, nd.name, nd.description, NodeDB.is_node_favorite(nd.type)) + 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)) + c.set_collapsed(nd.category in collapsed_categories) + + get_category(categories.keys()[0]).highlight_item(0) + + +func _on_search_line_edit_gui_input(event: InputEvent) -> void: + if event.is_action_pressed("ui_down"): + var category: Category + for i: String in categories: + var c: Category = categories[i] + if c.get_highlighted_item() != -1: + category = c + break + var item := category.get_highlighted_item() + if item + 1 == category.get_item_count(): + # reached the end of items in the current category + category.unhighlight_all() + + var nc: Category + if category.get_index() + 1 == scroll_content_container.get_child_count(): + # reached the end, get the first category + nc = scroll_content_container.get_child(0) + else: + # there is another category after this + nc = scroll_content_container.get_child(category.get_index() + 1) + nc.highlight_item(0) + scroll_container.ensure_control_visible(nc.get_child(0)) + return + + category.highlight_item(item + 1) + scroll_container.ensure_control_visible(category.get_child(item + 1)) + + if event.is_action_pressed("ui_up"): + var category: Category + for i: String in categories: + var c: Category = categories[i] + if c.get_highlighted_item() != -1: + category = c + break + var item := category.get_highlighted_item() + if item - 1 == -1: + # reached the beginning of items in the current category + category.unhighlight_all() + + var nc: Category + if category.get_index() - 1 == -1: + # reached the beginning, get the last category + nc = scroll_content_container.get_child(scroll_content_container.get_child_count() - 1) + else: + # there is another category before this + nc = scroll_content_container.get_child(category.get_index() - 1) + nc.highlight_item(nc.get_item_count() - 1) + scroll_container.ensure_control_visible(nc.get_child(nc.get_item_count() - 1)) + return + + category.highlight_item(item - 1) + scroll_container.ensure_control_visible(category.get_child(item - 1)) + + +func _on_search_line_edit_text_submitted(_new_text: String) -> void: + var category: Category + for i: String in categories: + var c: Category = categories[i] + if c.get_highlighted_item() != -1: + category = c + break + node_selected.emit(category.get_item_metadata(category.get_highlighted_item(), "type")) + + +func _on_category_collapse_toggled(collapsed: bool, category: String) -> void: + if collapsed: + collapsed_categories.append(category) + else: + collapsed_categories.erase(category) + + +class Category extends VBoxContainer: + const COLLAPSE_ICON := preload("res://graph_node_renderer/textures/collapse-icon.svg") + const COLLAPSE_ICON_COLLAPSED := preload("res://graph_node_renderer/textures/collapse-icon-collapsed.svg") + + var collapse_button: Button + + signal item_pressed(item: int) + signal item_favorite_button_toggled(item: int, toggled: bool) + signal collapse_toggled(collapsed: bool) + + + func _init(p_name: String) -> void: + collapse_button = Button.new() + collapse_button.alignment = HORIZONTAL_ALIGNMENT_LEFT + collapse_button.icon = COLLAPSE_ICON + collapse_button.toggle_mode = true + collapse_button.flat = true + collapse_button.size_flags_vertical = Control.SIZE_EXPAND_FILL + collapse_button.text = p_name + collapse_button.toggled.connect( + func(toggled: bool): + collapse_toggled.emit(toggled) + ) + add_child(collapse_button, false, Node.INTERNAL_MODE_FRONT) + + renamed.connect(func(): + collapse_button.name = name + ) + collapse_button.toggled.connect(set_collapsed) + + + func set_collapsed(collapsed: bool) -> void: + collapse_button.icon = COLLAPSE_ICON_COLLAPSED if collapsed else COLLAPSE_ICON + collapse_button.set_pressed_no_signal(collapsed) + for c: CategoryItem in get_children(): + c.visible = !collapsed + + + func add_item(p_name: String, p_tooltip: String, p_favorite: bool = false) -> void: + var item := CategoryItem.new(p_name, p_tooltip, p_favorite) + item.favorite_toggled.connect( + func(toggled: bool): + item_favorite_button_toggled.emit(item.get_index(), toggled) + ) + item.pressed.connect( + func(): + item_pressed.emit(item.get_index()) + ) + add_child(item) + + + func set_item_metadata(item: int, key: StringName, metadata: Variant) -> void: + get_child(item).set_meta(key, metadata) + + + func get_item_metadata(item: int, key: StringName) -> Variant: + return get_child(item).get_meta(key) + + + func get_item_count() -> int: + return get_child_count() + + + func set_item_favorite(item:int, favorite: bool) -> void: + var _item := get_child(item) as CategoryItem + _item.set_favorite(favorite) + + + func is_item_favorite(item: int) -> bool: + var _item := get_child(item) as CategoryItem + return _item.is_favorite() + + + func highlight_item(item: int) -> void: + for c: CategoryItem in get_children(): + c.set_highlighted(c.get_index() == item) + + + func unhighlight_all() -> void: + for c: CategoryItem in get_children(): + c.set_highlighted(false) + + + func get_highlighted_item() -> int: + for c: CategoryItem in get_children(): + if c.is_highlighted: + return c.get_index() + + return -1 + + +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 ITEM_MARGIN := 16 + + var highlighted_stylebox := StyleBoxFlat.new() + + var is_highlighted: bool + + var fav_button: Button + var name_button: Button + var panel: PanelContainer + + signal pressed + signal favorite_toggled(toggled: bool) + + + func _init(p_name: String, p_tooltip: String, p_favorite: bool) -> void: + fav_button = Button.new() + fav_button.icon = FAVORITE_ICON if p_favorite else NON_FAVORITE_ICON + fav_button.toggle_mode = true + fav_button.set_pressed_no_signal(p_favorite) + fav_button.flat = true + fav_button.toggled.connect( + func(toggled: bool): + favorite_toggled.emit(toggled) + ) + fav_button.toggled.connect(set_favorite) + + name_button = Button.new() + name_button.size_flags_horizontal = Control.SIZE_EXPAND_FILL + name_button.text = p_name + name_button.flat = true + name_button.alignment = HORIZONTAL_ALIGNMENT_LEFT + name_button.tooltip_text = p_tooltip + name_button.pressed.connect( + func(): + pressed.emit() + ) + + var mc := MarginContainer.new() + mc.add_theme_constant_override(&"margin_left", ITEM_MARGIN) + + panel = PanelContainer.new() + panel.size_flags_horizontal = Control.SIZE_EXPAND_FILL + panel.add_theme_stylebox_override(&"panel", highlighted_stylebox) + highlighted_stylebox.bg_color = Color(0.0, 0.0, 0.0, 0.15) + panel.self_modulate = Color.TRANSPARENT + + var inner_hb := HBoxContainer.new() + inner_hb.add_child(fav_button) + inner_hb.add_child(name_button) + panel.add_child(inner_hb) + + add_child(mc) + add_child(panel) + + + func set_favorite(favorite: bool) -> void: + fav_button.icon = FAVORITE_ICON if favorite else NON_FAVORITE_ICON + fav_button.set_pressed_no_signal(favorite) + + + func is_favorite() -> bool: + return fav_button.icon == FAVORITE_ICON + + + func set_highlighted(highlighted: bool) -> void: + is_highlighted = highlighted + if highlighted: + panel.self_modulate = Color.WHITE + else: + panel.self_modulate = Color.TRANSPARENT diff --git a/graph_node_renderer/add_node_menu.tscn b/graph_node_renderer/add_node_menu.tscn new file mode 100644 index 0000000..eeb0c6e --- /dev/null +++ b/graph_node_renderer/add_node_menu.tscn @@ -0,0 +1,44 @@ +[gd_scene load_steps=2 format=3 uid="uid://dqp08ahaho0q2"] + +[ext_resource type="Script" path="res://graph_node_renderer/add_node_menu.gd" id="1_m7f7p"] + +[node name="AddNodeMenu" type="MarginContainer"] +offset_right = 436.0 +offset_bottom = 57.0 +theme_override_constants/margin_left = 3 +theme_override_constants/margin_top = 3 +theme_override_constants/margin_right = 3 +theme_override_constants/margin_bottom = 3 +script = ExtResource("1_m7f7p") + +[node name="VBoxContainer" type="VBoxContainer" parent="."] +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 + +[node name="SearchLineEdit" type="LineEdit" parent="VBoxContainer"] +unique_name_in_owner = true +layout_mode = 2 +placeholder_text = "Search" +clear_button_enabled = true + +[node name="HSeparator" type="HSeparator" parent="VBoxContainer"] +layout_mode = 2 + +[node name="ScrollContainer" type="ScrollContainer" parent="VBoxContainer"] +layout_mode = 2 +size_flags_vertical = 3 + +[node name="MarginContainer" type="MarginContainer" parent="VBoxContainer/ScrollContainer"] +layout_mode = 2 +size_flags_horizontal = 3 +theme_override_constants/margin_right = 6 + +[node name="ScrollContentContainer" type="VBoxContainer" parent="VBoxContainer/ScrollContainer/MarginContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 + +[connection signal="gui_input" from="VBoxContainer/SearchLineEdit" to="." method="_on_search_line_edit_gui_input"] +[connection signal="text_changed" from="VBoxContainer/SearchLineEdit" to="." method="search"] +[connection signal="text_submitted" from="VBoxContainer/SearchLineEdit" to="." method="_on_search_line_edit_text_submitted"] diff --git a/graph_node_renderer/deck_renderer_graph_edit.gd b/graph_node_renderer/deck_renderer_graph_edit.gd index 116ce3c..af5d733 100644 --- a/graph_node_renderer/deck_renderer_graph_edit.gd +++ b/graph_node_renderer/deck_renderer_graph_edit.gd @@ -2,6 +2,13 @@ extends GraphEdit class_name DeckRendererGraphEdit const NODE_SCENE := preload("res://graph_node_renderer/deck_node_renderer_graph_node.tscn") +const ADD_NODE_MENU_SCENE := preload("res://graph_node_renderer/add_node_menu.tscn") + +var search_popup_panel: PopupPanel +var add_node_menu: AddNodeMenu + +@export var add_node_popup_size: Vector2i = Vector2i(500, 300) +var popup_position: Vector2 var deck: Deck: set(v): @@ -13,60 +20,13 @@ signal group_enter_requested(group_id: String) func _ready() -> void: - var add_button := Button.new() - add_button.text = "Button" - var add_print := Button.new() - add_print.text = "Print" - var get_var := Button.new() - get_var.text = "Get Var" - var set_var := Button.new() - set_var.text = "Set Var" - var test := Button.new() - test.text = "Interleaved" - var str_const := Button.new() - str_const.text = "String" - get_menu_hbox().add_child(add_button) - get_menu_hbox().add_child(add_print) - get_menu_hbox().add_child(get_var) - get_menu_hbox().add_child(set_var) - get_menu_hbox().add_child(test) - get_menu_hbox().add_child(str_const) + add_node_menu = ADD_NODE_MENU_SCENE.instantiate() + search_popup_panel = PopupPanel.new() + search_popup_panel.add_child(add_node_menu) + search_popup_panel.size = add_node_popup_size + add_child(search_popup_panel, false, Node.INTERNAL_MODE_BACK) - add_button.pressed.connect( - func(): - var node := NodeDB.instance_node("button") - deck.add_node_inst(node) - ) - - add_print.pressed.connect( - func(): - var node := NodeDB.instance_node("print") - deck.add_node_inst(node) - ) - - get_var.pressed.connect( - func(): - var node := NodeDB.instance_node("get_deck_var") - deck.add_node_inst(node) - ) - - set_var.pressed.connect( - func(): - var node := NodeDB.instance_node("set_deck_var") - deck.add_node_inst(node) - ) - - test.pressed.connect( - func(): - var node := NodeDB.instance_node("test_interleaved") - deck.add_node_inst(node) - ) - - str_const.pressed.connect( - func(): - var node := NodeDB.instance_node("string_constant") - deck.add_node_inst(node) - ) + add_node_menu.node_selected.connect(_on_add_node_menu_node_selected) connection_request.connect(attempt_connection) disconnection_request.connect(attempt_disconnect) @@ -105,6 +65,14 @@ func attempt_disconnect(from_node_name: StringName, from_port: int, to_node_name ) +func get_node_renderer(node: DeckNode) -> DeckNodeRendererGraphNode: + for i: DeckNodeRendererGraphNode in get_children().slice(1): + if i.node == node: + return i + + return null + + func _on_scroll_offset_changed(offset: Vector2) -> void: deck.set_meta("offset", offset) @@ -210,3 +178,24 @@ func _input(event: InputEvent) -> void: print("tried to enter group") group_enter_requested.emit((get_selected_nodes()[0] as DeckNodeRendererGraphNode).node.group_id) get_viewport().set_input_as_handled() + + +func _on_popup_request(p_popup_position: Vector2) -> void: + var p := get_viewport_rect().position + get_global_mouse_position() + p += Vector2(10, 10) + var r := Rect2i(p, search_popup_panel.size) + search_popup_panel.popup_on_parent(r) + add_node_menu.focus_search_bar() + popup_position = p_popup_position + + +func _on_add_node_menu_node_selected(type: String) -> void: + var node := NodeDB.instance_node(type) as DeckNode + deck.add_node_inst(node) + var node_pos := ((scroll_offset + popup_position) / zoom) + if snapping_enabled: + node_pos = node_pos.snapped(Vector2(snapping_distance, snapping_distance)) + + get_node_renderer(node).position_offset = node_pos + + search_popup_panel.hide() diff --git a/graph_node_renderer/deck_renderer_graph_edit.tscn b/graph_node_renderer/deck_renderer_graph_edit.tscn index a7720e3..e2e70c7 100644 --- a/graph_node_renderer/deck_renderer_graph_edit.tscn +++ b/graph_node_renderer/deck_renderer_graph_edit.tscn @@ -11,4 +11,5 @@ grow_vertical = 2 right_disconnects = true script = ExtResource("1_pojfs") +[connection signal="popup_request" from="." to="." method="_on_popup_request"] [connection signal="scroll_offset_changed" from="." to="." method="_on_scroll_offset_changed"] diff --git a/graph_node_renderer/textures/collapse-icon-collapsed.svg b/graph_node_renderer/textures/collapse-icon-collapsed.svg new file mode 100644 index 0000000..928b1f8 --- /dev/null +++ b/graph_node_renderer/textures/collapse-icon-collapsed.svg @@ -0,0 +1,85 @@ + + + + + + + + + + + + + + + + diff --git a/graph_node_renderer/textures/collapse-icon-collapsed.svg.import b/graph_node_renderer/textures/collapse-icon-collapsed.svg.import new file mode 100644 index 0000000..7b9bc25 --- /dev/null +++ b/graph_node_renderer/textures/collapse-icon-collapsed.svg.import @@ -0,0 +1,37 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://5gwj26ab5t6s" +path="res://.godot/imported/collapse-icon-collapsed.svg-811c248a92d92ca04d781df393765972.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://graph_node_renderer/textures/collapse-icon-collapsed.svg" +dest_files=["res://.godot/imported/collapse-icon-collapsed.svg-811c248a92d92ca04d781df393765972.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/collapse-icon.svg b/graph_node_renderer/textures/collapse-icon.svg new file mode 100644 index 0000000..73fd541 --- /dev/null +++ b/graph_node_renderer/textures/collapse-icon.svg @@ -0,0 +1,85 @@ + + + + + + + + + + + + + + + + diff --git a/graph_node_renderer/textures/collapse-icon.svg.import b/graph_node_renderer/textures/collapse-icon.svg.import new file mode 100644 index 0000000..ad3cf4f --- /dev/null +++ b/graph_node_renderer/textures/collapse-icon.svg.import @@ -0,0 +1,37 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://yrjhesfwqp6o" +path="res://.godot/imported/collapse-icon.svg-9bebc4e2d470036ab276875c18b86afd.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://graph_node_renderer/textures/collapse-icon.svg" +dest_files=["res://.godot/imported/collapse-icon.svg-9bebc4e2d470036ab276875c18b86afd.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/favorite-icon.svg b/graph_node_renderer/textures/favorite-icon.svg new file mode 100644 index 0000000..a57c3da --- /dev/null +++ b/graph_node_renderer/textures/favorite-icon.svg @@ -0,0 +1 @@ + diff --git a/graph_node_renderer/textures/favorite-icon.svg.import b/graph_node_renderer/textures/favorite-icon.svg.import new file mode 100644 index 0000000..353cd63 --- /dev/null +++ b/graph_node_renderer/textures/favorite-icon.svg.import @@ -0,0 +1,37 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://b4qr820crboug" +path="res://.godot/imported/favorite-icon.svg-9ed8d8558aa33c9d7305523c2c289a43.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://graph_node_renderer/textures/favorite-icon.svg" +dest_files=["res://.godot/imported/favorite-icon.svg-9ed8d8558aa33c9d7305523c2c289a43.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/non-favorite-icon.svg b/graph_node_renderer/textures/non-favorite-icon.svg new file mode 100644 index 0000000..dc52338 --- /dev/null +++ b/graph_node_renderer/textures/non-favorite-icon.svg @@ -0,0 +1 @@ + diff --git a/graph_node_renderer/textures/non-favorite-icon.svg.import b/graph_node_renderer/textures/non-favorite-icon.svg.import new file mode 100644 index 0000000..ed4a9ee --- /dev/null +++ b/graph_node_renderer/textures/non-favorite-icon.svg.import @@ -0,0 +1,37 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://c3sndmgw0mfpb" +path="res://.godot/imported/non-favorite-icon.svg-7d5ddab226c43aa43af383d3db45c122.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://graph_node_renderer/textures/non-favorite-icon.svg" +dest_files=["res://.godot/imported/non-favorite-icon.svg-7d5ddab226c43aa43af383d3db45c122.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