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

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

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

397 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, type: String, tooltip: String = "", favorite: bool = false, library: bool = false) -> void:
var c: Category = categories[category]
c.add_item(item, tooltip, type, 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, type: String, tooltip: String = "", favorite: bool = false, library: bool = false) -> void:
if not categories.has(category):
add_category(category)
add_category_item(category, item, type, 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.type, 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_type: String, p_favorite: bool = false, p_library: bool = false) -> void:
var item := CategoryItem.new(p_name, p_tooltip, p_favorite, p_library, p_type)
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, p_type: String) -> 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:
var nd := NodeDB.get_library_descriptor(p_type)
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."
group_texture_rect.tooltip_text += "\nAdded by %s" % nd.added_by_library
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