mirror of
https://codeberg.org/StreamGraph/StreamGraph.git
synced 2024-11-13 19:49:55 +01:00
240750c48e
~~cl0ses #51~~ ~~cl0ses #93~~ ~~cl0ses #98~~ ~~cl0ses #150~~ another change in this PR: the Deck and DeckNode classes now use manual memory management. Reviewed-on: https://codeberg.org/StreamGraph/StreamGraph/pulls/151 Co-authored-by: Lera Elvoé <yagich@poto.cafe> Co-committed-by: Lera Elvoé <yagich@poto.cafe>
394 lines
13 KiB
GDScript
394 lines
13 KiB
GDScript
# (c) 2023-present Eroax
|
|
# (c) 2023-present Yagich
|
|
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
|
extends MarginContainer
|
|
class_name AddNodeMenu
|
|
|
|
## A menu for adding nodes with a search bar.
|
|
|
|
@onready var search_line_edit: LineEdit = %SearchLineEdit
|
|
@onready var scroll_content_container: VBoxContainer = %ScrollContentContainer
|
|
@onready var scroll_container: ScrollContainer = $VBoxContainer/ScrollContainer
|
|
|
|
## The categories currently shown in the menu.
|
|
var categories: Dictionary = {} # Dictionary[String, Category]
|
|
## A list of categories to remember the collapsed state of so they remain collapsed when the search list is rebuilt.
|
|
var collapsed_categories: Array[String]
|
|
|
|
## Emitted when a node is selected either by clicking on it or pressing [constant @GlobalScope.KEY_ENTER] while the [member search_line_edit] is focused.
|
|
signal node_selected(type: String)
|
|
|
|
|
|
func _ready() -> void:
|
|
search("")
|
|
|
|
|
|
## Add a new category to the menu.
|
|
func add_category(category_name: String) -> void:
|
|
var c := Category.new(NodeDB.get_category_capitalization(category_name))
|
|
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)
|
|
)
|
|
|
|
|
|
## Add an item to a category.
|
|
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, 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, library: bool = false) -> void:
|
|
if not categories.has(category):
|
|
add_category(category)
|
|
|
|
add_category_item(category, item, tooltip, favorite, library)
|
|
|
|
|
|
## Get a [AddNodeMenu.Category] node by its' identifier.
|
|
func get_category(category: String) -> Category:
|
|
return categories[category]
|
|
|
|
|
|
## Focus the search bar and select all its' text.
|
|
func focus_search_bar() -> void:
|
|
search_line_edit.select_all()
|
|
search_line_edit.grab_focus()
|
|
|
|
|
|
## Searches for a node using [SearchProvider] and puts the results as items.
|
|
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), 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))
|
|
c.set_collapsed(nd.category in collapsed_categories)
|
|
|
|
get_category(categories.keys()[0]).highlight_item(0)
|
|
|
|
|
|
## Callback for [member search_line_edit]'s input events. Handles highlighting items when navigating with up/down arrow keys.
|
|
func _on_search_line_edit_gui_input(event: InputEvent) -> void:
|
|
if event.is_action_pressed("ui_down"):
|
|
if scroll_content_container.get_child_count() == 0:
|
|
return
|
|
|
|
search_line_edit.accept_event()
|
|
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 := get_next_visible_category(category.get_index())
|
|
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"):
|
|
if scroll_content_container.get_child_count() == 0:
|
|
return
|
|
|
|
search_line_edit.accept_event()
|
|
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 := get_previous_visible_category(category.get_index())
|
|
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))
|
|
|
|
|
|
## Returns the next uncollapsed category, starting from index [code]at[/code], wrapping around if no other
|
|
## categories are uncollapsed.
|
|
func get_next_visible_category(at: int) -> Category:
|
|
var i := at
|
|
var s := 0
|
|
|
|
while s < scroll_content_container.get_child_count():
|
|
i = (i + 1) % scroll_content_container.get_child_count()
|
|
if not (scroll_content_container.get_child(i) as Category).is_collapsed():
|
|
return scroll_content_container.get_child(i)
|
|
s += 1
|
|
|
|
return scroll_content_container.get_child(at)
|
|
|
|
|
|
## Returns the previous uncollapsed category, starting from index [code]at[/code], wrapping around if no other
|
|
## categories are uncollapsed.
|
|
func get_previous_visible_category(at: int) -> Category:
|
|
var i := at
|
|
var s := 0
|
|
|
|
while s < scroll_content_container.get_child_count():
|
|
i = (i - 1) % scroll_content_container.get_child_count()
|
|
if not (scroll_content_container.get_child(i) as Category).is_collapsed():
|
|
return scroll_content_container.get_child(i)
|
|
s += 1
|
|
|
|
return scroll_content_container.get_child(at)
|
|
|
|
|
|
## Callback for [member search_line_edit]. Handles emitting [signal node_selected]
|
|
func _on_search_line_edit_text_submitted(_new_text: String) -> void:
|
|
if scroll_content_container.get_child_count() == 0:
|
|
return
|
|
|
|
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)
|
|
|
|
## A collapsible item menu.
|
|
##
|
|
## An analog to [Tree] and [ItemList] made with nodes. Allows collapsing its
|
|
## children [AddNodeMenu.CategoryItem] nodes and highlighting them.[br]
|
|
## [b]Note:[/b] only one [AddNodeMenu.CategoryItem] can be highlighted at any time.
|
|
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
|
|
|
|
## Emitted when a child item has been pressed.
|
|
signal item_pressed(item: int)
|
|
## Emitted when a child item's favorite button has been pressed.
|
|
signal item_favorite_button_toggled(item: int, toggled: bool)
|
|
## Emitted when the category's collapsed state has been toggled.
|
|
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)
|
|
|
|
|
|
## If [param collapsed] is [code]true[/code], collapses the category, hiding its children.
|
|
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 = not collapsed
|
|
|
|
|
|
## Add an item to the category.
|
|
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)
|
|
)
|
|
item.pressed.connect(
|
|
func():
|
|
item_pressed.emit(item.get_index())
|
|
)
|
|
add_child(item)
|
|
|
|
|
|
## Set an item's metadata at index [param item].
|
|
func set_item_metadata(item: int, key: StringName, metadata: Variant) -> void:
|
|
get_child(item).set_meta(key, metadata)
|
|
|
|
|
|
## Retrieve an item's metadata at index [param item].
|
|
func get_item_metadata(item: int, key: StringName) -> Variant:
|
|
return get_child(item).get_meta(key)
|
|
|
|
|
|
## Get the amount of items in this category.
|
|
func get_item_count() -> int:
|
|
return get_child_count()
|
|
|
|
|
|
## Toggle an item at index [param item]'s favorite state.
|
|
func set_item_favorite(item:int, favorite: bool) -> void:
|
|
var _item := get_child(item) as CategoryItem
|
|
_item.set_favorite(favorite)
|
|
|
|
|
|
## Returns [code]true[/code] if the item at [param item] is marked as favorite.
|
|
func is_item_favorite(item: int) -> bool:
|
|
var _item := get_child(item) as CategoryItem
|
|
return _item.is_favorite()
|
|
|
|
|
|
## Highlight item at index [item], and unhighlight all other items.
|
|
func highlight_item(item: int) -> void:
|
|
for c: CategoryItem in get_children():
|
|
c.set_highlighted(c.get_index() == item)
|
|
|
|
|
|
## Unhighlight all items.
|
|
func unhighlight_all() -> void:
|
|
for c: CategoryItem in get_children():
|
|
c.set_highlighted(false)
|
|
|
|
|
|
## Returns the index of the currently highlighted item. Returns [code]-1[/code] if no item is highlighted in this category.
|
|
func get_highlighted_item() -> int:
|
|
for c: CategoryItem in get_children():
|
|
if c.is_highlighted:
|
|
return c.get_index()
|
|
|
|
return -1
|
|
|
|
|
|
func is_collapsed() -> bool:
|
|
return collapse_button.button_pressed
|
|
|
|
|
|
## Represents an item in a [AddNodeMenu.Category].
|
|
##
|
|
## A selectable and highlightable category item with a favorite button.
|
|
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.
|
|
var highlighted_stylebox := StyleBoxFlat.new()
|
|
|
|
var is_highlighted: bool
|
|
|
|
var group_texture_rect: TextureRect
|
|
var fav_button: Button
|
|
var name_button: Button
|
|
var panel: PanelContainer
|
|
|
|
## Emitted when this item has been pressed.
|
|
signal pressed
|
|
## Emitted when this item's [member fav_button] has been pressed.
|
|
signal favorite_toggled(toggled: bool)
|
|
|
|
|
|
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
|
|
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)
|
|
|
|
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)
|
|
add_child(panel)
|
|
|
|
|
|
## Toggle this item's favorite state.
|
|
func set_favorite(favorite: bool) -> void:
|
|
fav_button.icon = FAVORITE_ICON if favorite else NON_FAVORITE_ICON
|
|
fav_button.set_pressed_no_signal(favorite)
|
|
|
|
|
|
## Returns [code]true[/code] if this item is marked as favorite.
|
|
func is_favorite() -> bool:
|
|
return fav_button.icon == FAVORITE_ICON
|
|
|
|
|
|
## Toggle this item's highlighted state.
|
|
func set_highlighted(highlighted: bool) -> void:
|
|
is_highlighted = highlighted
|
|
if highlighted:
|
|
panel.self_modulate = Color.WHITE
|
|
else:
|
|
panel.self_modulate = Color.TRANSPARENT
|