mirror of
https://codeberg.org/StreamGraph/StreamGraph.git
synced 2024-11-13 19:49:55 +01:00
f720efcc72
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>
397 lines
13 KiB
GDScript
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
|