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