/** * task_palette.cpp * ============================================================================= * Copyright (c) 2023-present Serhii Snitsaruk and the LimboAI contributors. * * Use of this source code is governed by an MIT-style * license that can be found in the LICENSE file or at * https://opensource.org/licenses/MIT. * ============================================================================= */ #ifdef TOOLS_ENABLED #include "task_palette.h" #include "../util/limbo_compat.h" #include "../util/limbo_string_names.h" #include "../util/limbo_task_db.h" #include "../util/limbo_utility.h" #ifdef LIMBOAI_MODULE #include "core/config/project_settings.h" #include "core/error/error_macros.h" #include "editor/editor_help.h" #include "editor/editor_node.h" #include "editor/editor_paths.h" #include "editor/plugins/script_editor_plugin.h" #include "editor/themes/editor_scale.h" #include "scene/gui/check_box.h" #endif // LIMBO_MODULE #ifdef LIMBOAI_GDEXTENSION #include <godot_cpp/classes/button_group.hpp> #include <godot_cpp/classes/check_box.hpp> #include <godot_cpp/classes/config_file.hpp> #include <godot_cpp/classes/control.hpp> #include <godot_cpp/classes/editor_interface.hpp> #include <godot_cpp/classes/editor_paths.hpp> #include <godot_cpp/classes/font.hpp> #include <godot_cpp/classes/global_constants.hpp> #include <godot_cpp/classes/h_box_container.hpp> #include <godot_cpp/classes/h_flow_container.hpp> #include <godot_cpp/classes/input_event_mouse_button.hpp> #include <godot_cpp/classes/label.hpp> #include <godot_cpp/classes/popup_menu.hpp> #include <godot_cpp/classes/project_settings.hpp> #include <godot_cpp/classes/resource_loader.hpp> #include <godot_cpp/classes/script_editor.hpp> #include <godot_cpp/classes/script_editor_base.hpp> #include <godot_cpp/classes/style_box.hpp> #include <godot_cpp/core/error_macros.hpp> using namespace godot; #endif // LIMBOAI_GDEXTENSION //**** TaskButton void TaskButton::_bind_methods() { } Control *TaskButton::_do_make_tooltip() const { #ifdef LIMBOAI_MODULE String help_symbol; bool is_resource = task_meta.begins_with("res://"); if (is_resource) { help_symbol = "class|\"" + task_meta.lstrip("res://") + "\"|"; } else { help_symbol = "class|" + task_meta + "|"; } String desc = _module_get_help_description(task_meta); if (desc.is_empty() && is_resource) { // ! HACK: Force documentation parsing. Ref<Script> s = ResourceLoader::load(task_meta); if (s.is_valid()) { Vector<DocData::ClassDoc> docs = s->get_documentation(); for (int i = 0; i < docs.size(); i++) { const DocData::ClassDoc &doc = docs.get(i); EditorHelp::get_doc_data()->add_doc(doc); } desc = _module_get_help_description(task_meta); } } if (desc.is_empty()) { desc = "[i]" + TTR("No description.") + "[/i]"; } EditorHelpBitTooltip::show_tooltip(const_cast<TaskButton *>(this), help_symbol, desc); #endif // LIMBOAI_MODULE #ifdef LIMBOAI_GDEXTENSION // TODO: When we figure out how to retrieve documentation in GDEXTENSION, should add a tooltip control here. #endif // LIMBOAI_GDEXTENSION return memnew(Control); // Make the standard tooltip invisible. } #ifdef LIMBOAI_MODULE String TaskButton::_module_get_help_description(const String &p_class_or_script_path) const { String descr; DocTools *dd = EditorHelp::get_doc_data(); HashMap<String, DocData::ClassDoc>::Iterator E; if (p_class_or_script_path.begins_with("res://")) { // Try to find by script path. E = dd->class_list.find(vformat("\"%s\"", p_class_or_script_path.trim_prefix("res://"))); if (!E) { // Try to guess global script class from filename. String maybe_class_name = p_class_or_script_path.get_file().get_basename().to_pascal_case(); E = dd->class_list.find(maybe_class_name); } } else { // Try to find core class or global class. E = dd->class_list.find(p_class_or_script_path); } if (E) { if (E->value.description.is_empty()) { descr = DTR(E->value.brief_description); } else { descr = DTR(E->value.description); } } // TODO: Documentation tooltips are only available in the module variant. Find a way to show em in GDExtension. return descr; } #endif // LIMBOAI_MODULE TaskButton::TaskButton() { set_focus_mode(FOCUS_NONE); } //**** TaskButton ^ //**** TaskPaletteSection void TaskPaletteSection::_on_task_button_pressed(const String &p_task) { emit_signal(LW_NAME(task_button_pressed), p_task); } void TaskPaletteSection::_on_task_button_gui_input(const Ref<InputEvent> &p_event, const String &p_task) { if (!p_event->is_pressed()) { return; } Ref<InputEventMouseButton> mb = p_event; if (mb.is_valid() && mb->get_button_index() == LW_MBTN(RIGHT)) { emit_signal(LW_NAME(task_button_rmb), p_task); } } void TaskPaletteSection::_on_header_pressed() { set_collapsed(!is_collapsed()); } void TaskPaletteSection::set_filter(String p_filter_text) { int num_hidden = 0; if (p_filter_text.is_empty()) { for (int i = 0; i < tasks_container->get_child_count(); i++) { Object::cast_to<Button>(tasks_container->get_child(i))->show(); } set_visible(tasks_container->get_child_count() > 0); } else { for (int i = 0; i < tasks_container->get_child_count(); i++) { Button *btn = Object::cast_to<Button>(tasks_container->get_child(i)); btn->set_visible(btn->get_text().findn(p_filter_text) != -1); num_hidden += !btn->is_visible(); } set_visible(num_hidden < tasks_container->get_child_count()); } } void TaskPaletteSection::add_task_button(const String &p_name, const Ref<Texture> &icon, const String &p_meta) { TaskButton *btn = memnew(TaskButton); btn->set_text(p_name); btn->set_button_icon(icon); btn->set_tooltip_text("dummy_text"); // Force tooltip to be shown. btn->set_task_meta(p_meta); btn->add_theme_constant_override(LW_NAME(icon_max_width), 16 * EDSCALE); // Force user icons to be of the proper size. btn->connect(LW_NAME(pressed), callable_mp(this, &TaskPaletteSection::_on_task_button_pressed).bind(p_meta)); btn->connect(LW_NAME(gui_input), callable_mp(this, &TaskPaletteSection::_on_task_button_gui_input).bind(p_meta)); tasks_container->add_child(btn); } void TaskPaletteSection::set_collapsed(bool p_collapsed) { tasks_container->set_visible(!p_collapsed); section_header->set_button_icon((p_collapsed ? theme_cache.arrow_right_icon : theme_cache.arrow_down_icon)); } bool TaskPaletteSection::is_collapsed() const { return !tasks_container->is_visible(); } void TaskPaletteSection::_do_update_theme_item_cache() { theme_cache.arrow_down_icon = get_theme_icon(LW_NAME(GuiTreeArrowDown), LW_NAME(EditorIcons)); theme_cache.arrow_right_icon = get_theme_icon(LW_NAME(GuiTreeArrowRight), LW_NAME(EditorIcons)); } void TaskPaletteSection::_notification(int p_what) { switch (p_what) { case NOTIFICATION_READY: { section_header->connect(LW_NAME(pressed), callable_mp(this, &TaskPaletteSection::_on_header_pressed)); } break; case NOTIFICATION_THEME_CHANGED: { _do_update_theme_item_cache(); section_header->set_button_icon((is_collapsed() ? theme_cache.arrow_right_icon : theme_cache.arrow_down_icon)); section_header->add_theme_font_override(LW_NAME(font), get_theme_font(LW_NAME(bold), LW_NAME(EditorFonts))); } break; } } void TaskPaletteSection::_bind_methods() { ADD_SIGNAL(MethodInfo("task_button_pressed")); ADD_SIGNAL(MethodInfo("task_button_rmb")); } TaskPaletteSection::TaskPaletteSection() { section_header = memnew(Button); add_child(section_header); section_header->set_focus_mode(FOCUS_NONE); tasks_container = memnew(HFlowContainer); add_child(tasks_container); } TaskPaletteSection::~TaskPaletteSection() { } //**** TaskPaletteSection ^ //**** TaskPalette void TaskPalette::_menu_action_selected(int p_id) { ERR_FAIL_COND(context_task.is_empty()); switch (p_id) { case MENU_OPEN_DOC: { LimboUtility::get_singleton()->open_doc_class(context_task); } break; case MENU_EDIT_SCRIPT: { ERR_FAIL_COND(!context_task.begins_with("res://")); EDIT_SCRIPT(context_task); } break; case MENU_FAVORITE: { PackedStringArray favorite_tasks = GLOBAL_GET("limbo_ai/behavior_tree/favorite_tasks"); if (favorite_tasks.has(context_task)) { int idx = favorite_tasks.find(context_task); if (idx >= 0) { favorite_tasks.remove_at(idx); } } else { favorite_tasks.append(context_task); } ProjectSettings::get_singleton()->set_setting("limbo_ai/behavior_tree/favorite_tasks", favorite_tasks); ProjectSettings::get_singleton()->save(); emit_signal(LW_NAME(favorite_tasks_changed)); } break; } } void TaskPalette::_on_task_button_pressed(const String &p_task) { emit_signal(LW_NAME(task_selected), p_task); } void TaskPalette::_on_task_button_rmb(const String &p_task) { if (dialog_mode) { return; } ERR_FAIL_COND(p_task.is_empty()); context_task = p_task; menu->clear(); menu->add_icon_item(theme_cache.edit_script_icon, TTR("Edit Script"), MENU_EDIT_SCRIPT); menu->set_item_disabled(MENU_EDIT_SCRIPT, !context_task.begins_with("res://")); menu->add_icon_item(theme_cache.open_doc_icon, TTR("Open Documentation"), MENU_OPEN_DOC); menu->add_separator(); Array favorite_tasks = GLOBAL_GET("limbo_ai/behavior_tree/favorite_tasks"); if (favorite_tasks.has(context_task)) { menu->add_icon_item(theme_cache.remove_from_favorites_icon, TTR("Remove from Favorites"), MENU_FAVORITE); } else { menu->add_icon_item(theme_cache.add_to_favorites_icon, TTR("Add to Favorites"), MENU_FAVORITE); } menu->reset_size(); menu->set_position(get_screen_position() + get_local_mouse_position()); menu->popup(); } void TaskPalette::_apply_filter(const String &p_text) { for (int i = 0; i < sections->get_child_count(); i++) { TaskPaletteSection *sec = Object::cast_to<TaskPaletteSection>(sections->get_child(i)); ERR_FAIL_NULL(sec); sec->set_filter(p_text); } } void TaskPalette::_update_filter_popup() { switch (filter_settings.type_filter) { case FilterSettings::TypeFilter::TYPE_ALL: { type_all->set_pressed(true); } break; case FilterSettings::TypeFilter::TYPE_CORE: { type_core->set_pressed(true); } break; case FilterSettings::TypeFilter::TYPE_USER: { type_user->set_pressed(true); } break; } switch (filter_settings.category_filter) { case FilterSettings::CategoryFilter::CATEGORY_ALL: { category_all->set_pressed(true); } break; case FilterSettings::CategoryFilter::CATEGORY_INCLUDE: { category_include->set_pressed(true); } break; case FilterSettings::CategoryFilter::CATEGORY_EXCLUDE: { category_exclude->set_pressed(true); } break; } while (category_list->get_child_count() > 0) { Node *item = category_list->get_child(0); category_list->remove_child(item); item->queue_free(); } for (String &cat : LimboTaskDB::get_categories()) { CheckBox *category_item = memnew(CheckBox); category_item->set_text(cat); category_item->set_focus_mode(FocusMode::FOCUS_NONE); category_item->set_pressed_no_signal(LOGICAL_XOR( filter_settings.excluded_categories.has(cat), filter_settings.category_filter == FilterSettings::CategoryFilter::CATEGORY_INCLUDE)); category_item->connect(LW_NAME(toggled), callable_mp(this, &TaskPalette::_category_item_toggled).bind(cat)); category_list->add_child(category_item); } category_list->reset_size(); Size2 size = category_list->get_size() + Size2(8, 8); size.width = MIN(size.width, 400 * EDSCALE); size.height = MIN(size.height, 600 * EDSCALE); category_scroll->set_custom_minimum_size(size); category_choice->set_visible(filter_settings.category_filter != FilterSettings::CATEGORY_ALL); } void TaskPalette::_show_filter_popup() { _update_filter_popup(); tool_filters->set_pressed_no_signal(true); Transform2D xform = tool_filters->get_screen_transform(); Rect2i rect = Rect2(xform.get_origin(), xform.get_scale() * tool_filters->get_size()); rect.position.y += rect.size.height; rect.size.height = 0; filter_popup->reset_size(); filter_popup->popup(rect); } void TaskPalette::_category_filter_changed() { if (category_all->is_pressed()) { filter_settings.category_filter = FilterSettings::CategoryFilter::CATEGORY_ALL; } else if (category_include->is_pressed()) { filter_settings.category_filter = FilterSettings::CategoryFilter::CATEGORY_INCLUDE; } else if (category_exclude->is_pressed()) { filter_settings.category_filter = FilterSettings::CategoryFilter::CATEGORY_EXCLUDE; } for (int i = 0; i < category_list->get_child_count(); i++) { CheckBox *item = Object::cast_to<CheckBox>(category_list->get_child(i)); item->set_pressed_no_signal(LOGICAL_XOR( filter_settings.excluded_categories.has(item->get_text()), filter_settings.category_filter == FilterSettings::CATEGORY_INCLUDE)); } category_choice->set_visible(filter_settings.category_filter != FilterSettings::CATEGORY_ALL); filter_popup->reset_size(); _filter_data_changed(); } void TaskPalette::_set_all_filter_categories(bool p_selected) { for (int i = 0; i < category_list->get_child_count(); i++) { CheckBox *item = Object::cast_to<CheckBox>(category_list->get_child(i)); item->set_pressed_no_signal(p_selected); bool excluded = LOGICAL_XOR(p_selected, filter_settings.category_filter == FilterSettings::CATEGORY_INCLUDE); _set_category_excluded(item->get_text(), excluded); } _filter_data_changed(); } void TaskPalette::_type_filter_changed() { if (type_all->is_pressed()) { filter_settings.type_filter = FilterSettings::TypeFilter::TYPE_ALL; } else if (type_core->is_pressed()) { filter_settings.type_filter = FilterSettings::TypeFilter::TYPE_CORE; } else if (type_user->is_pressed()) { filter_settings.type_filter = FilterSettings::TypeFilter::TYPE_USER; } _filter_data_changed(); } void TaskPalette::_category_item_toggled(bool p_pressed, const String &p_category) { bool excluded = LOGICAL_XOR(p_pressed, filter_settings.category_filter == FilterSettings::CATEGORY_INCLUDE); _set_category_excluded(p_category, excluded); _filter_data_changed(); } void TaskPalette::_filter_data_changed() { callable_mp(this, &TaskPalette::refresh).call_deferred(); _update_filter_button(); } void TaskPalette::_draw_filter_popup_background() { theme_cache.category_choice_background->draw(category_choice->get_canvas_item(), Rect2(Point2(), category_choice->get_size())); } void TaskPalette::_update_filter_button() { tool_filters->set_pressed_no_signal(filter_popup->is_visible() || filter_settings.type_filter != FilterSettings::TYPE_ALL || (filter_settings.category_filter != FilterSettings::CATEGORY_ALL && !filter_settings.excluded_categories.is_empty())); } void TaskPalette::refresh() { HashSet<String> collapsed_sections; if (sections->get_child_count() == 0) { // Restore collapsed state from config. Ref<ConfigFile> cf; cf.instantiate(); String conf_path = LAYOUT_CONFIG_FILE(); if (cf->load(conf_path) == OK) { Variant value = cf->get_value("LimboAI", "task_palette_collapsed_sections", Array()); if (VARIANT_IS_ARRAY(value)) { Array arr = value; for (int i = 0; i < arr.size(); i++) { if (arr[i].get_type() == Variant::STRING) { collapsed_sections.insert(arr[i]); } } } } } else { for (int i = 0; i < sections->get_child_count(); i++) { TaskPaletteSection *sec = Object::cast_to<TaskPaletteSection>(sections->get_child(i)); ERR_FAIL_NULL(sec); if (sec->is_collapsed()) { collapsed_sections.insert(sec->get_category_name()); } sections->get_child(i)->queue_free(); } } LimboTaskDB::scan_user_tasks(); List<String> categories = LimboTaskDB::get_categories(); categories.sort(); for (String cat : categories) { if (filter_settings.category_filter != FilterSettings::CATEGORY_ALL && filter_settings.excluded_categories.has(cat)) { continue; } List<String> tasks = LimboTaskDB::get_tasks_in_category(cat); if (tasks.size() == 0) { continue; } TaskPaletteSection *sec = memnew(TaskPaletteSection()); sec->set_category_name(cat); for (const String &task_meta : tasks) { Ref<Texture2D> icon = LimboUtility::get_singleton()->get_task_icon(task_meta); String tname; if (task_meta.begins_with("res:")) { if (filter_settings.type_filter == FilterSettings::TYPE_CORE) { continue; } tname = task_meta.get_file().get_basename().trim_prefix("BT").to_pascal_case(); } else { if (filter_settings.type_filter == FilterSettings::TYPE_USER) { continue; } tname = task_meta.trim_prefix("BT"); } sec->add_task_button(tname, icon, task_meta); } sec->set_filter(""); sec->connect(LW_NAME(task_button_pressed), callable_mp(this, &TaskPalette::_on_task_button_pressed)); sec->connect(LW_NAME(task_button_rmb), callable_mp(this, &TaskPalette::_on_task_button_rmb)); sections->add_child(sec); sec->set_collapsed(!dialog_mode && collapsed_sections.has(cat)); } if (!dialog_mode && !filter_edit->get_text().is_empty()) { _apply_filter(filter_edit->get_text()); } } void TaskPalette::use_dialog_mode() { tool_filters->hide(); tool_refresh->hide(); dialog_mode = true; } void TaskPalette::_do_update_theme_item_cache() { theme_cache.add_to_favorites_icon = get_theme_icon(LW_NAME(Favorites), LW_NAME(EditorIcons)); theme_cache.edit_script_icon = get_theme_icon(LW_NAME(Script), LW_NAME(EditorIcons)); theme_cache.open_doc_icon = get_theme_icon(LW_NAME(Help), LW_NAME(EditorIcons)); theme_cache.remove_from_favorites_icon = get_theme_icon(LW_NAME(NonFavorite), LW_NAME(EditorIcons)); theme_cache.category_choice_background = get_theme_stylebox(LW_NAME(normal), LW_NAME(LineEdit)); } void TaskPalette::_notification(int p_what) { switch (p_what) { case NOTIFICATION_READY: { // **** Signals tool_filters->connect(LW_NAME(pressed), callable_mp(this, &TaskPalette::_show_filter_popup)); filter_edit->connect(LW_NAME(text_changed), callable_mp(this, &TaskPalette::_apply_filter)); tool_refresh->connect(LW_NAME(pressed), callable_mp(this, &TaskPalette::refresh)); menu->connect(LW_NAME(id_pressed), callable_mp(this, &TaskPalette::_menu_action_selected)); type_all->connect(LW_NAME(pressed), callable_mp(this, &TaskPalette::_type_filter_changed)); type_core->connect(LW_NAME(pressed), callable_mp(this, &TaskPalette::_type_filter_changed)); type_user->connect(LW_NAME(pressed), callable_mp(this, &TaskPalette::_type_filter_changed)); category_all->connect(LW_NAME(pressed), callable_mp(this, &TaskPalette::_category_filter_changed)); category_include->connect(LW_NAME(pressed), callable_mp(this, &TaskPalette::_category_filter_changed)); category_exclude->connect(LW_NAME(pressed), callable_mp(this, &TaskPalette::_category_filter_changed)); category_choice->connect(LW_NAME(draw), callable_mp(this, &TaskPalette::_draw_filter_popup_background)); select_all->connect(LW_NAME(pressed), callable_mp(this, &TaskPalette::_set_all_filter_categories).bind(true)); deselect_all->connect(LW_NAME(pressed), callable_mp(this, &TaskPalette::_set_all_filter_categories).bind(false)); filter_popup->connect(LW_NAME(popup_hide), callable_mp(this, &TaskPalette::_update_filter_button)); } break; case NOTIFICATION_ENTER_TREE: { Ref<ConfigFile> cf; cf.instantiate(); String conf_path = LAYOUT_CONFIG_FILE(); if (cf->load(conf_path) == OK) { Variant value = cf->get_value("LimboAI", "task_palette_type_filter", FilterSettings::TypeFilter(0)); if (VARIANT_IS_NUM(value)) { filter_settings.type_filter = (FilterSettings::TypeFilter)(int)value; } value = cf->get_value("LimboAI", "task_palette_category_filter", FilterSettings::CategoryFilter(0)); if (VARIANT_IS_NUM(value)) { filter_settings.category_filter = (FilterSettings::CategoryFilter)(int)value; } value = cf->get_value("LimboAI", "task_palette_excluded_categories", Array()); if (VARIANT_IS_ARRAY(value)) { Array arr = value; for (int i = 0; i < arr.size(); i++) { if (arr[i].get_type() == Variant::STRING) { filter_settings.excluded_categories.insert(arr[i]); } } } } _update_filter_button(); } break; case NOTIFICATION_EXIT_TREE: { Ref<ConfigFile> cf; cf.instantiate(); String conf_path = LAYOUT_CONFIG_FILE(); cf->load(conf_path); Array collapsed_sections; for (int i = 0; i < sections->get_child_count(); i++) { TaskPaletteSection *sec = Object::cast_to<TaskPaletteSection>(sections->get_child(i)); if (sec->is_collapsed()) { collapsed_sections.push_back(sec->get_category_name()); } } cf->set_value("LimboAI", "task_palette_collapsed_sections", collapsed_sections); cf->set_value("LimboAI", "task_palette_type_filter", filter_settings.type_filter); cf->set_value("LimboAI", "task_palette_category_filter", filter_settings.category_filter); Array excluded_categories; for (const String &cat : filter_settings.excluded_categories) { excluded_categories.append(cat); } cf->set_value("LimboAI", "task_palette_excluded_categories", excluded_categories); cf->save(conf_path); } break; case NOTIFICATION_THEME_CHANGED: { _do_update_theme_item_cache(); tool_filters->set_button_icon(get_theme_icon(LW_NAME(AnimationFilter), LW_NAME(EditorIcons))); tool_refresh->set_button_icon(get_theme_icon(LW_NAME(Reload), LW_NAME(EditorIcons))); filter_edit->set_right_icon(get_theme_icon(LW_NAME(Search), LW_NAME(EditorIcons))); select_all->set_button_icon(LimboUtility::get_singleton()->get_task_icon("LimboSelectAll")); deselect_all->set_button_icon(LimboUtility::get_singleton()->get_task_icon("LimboDeselectAll")); category_choice->queue_redraw(); if (is_visible_in_tree()) { refresh(); } } break; } } void TaskPalette::_bind_methods() { ClassDB::bind_method(D_METHOD("refresh"), &TaskPalette::refresh); ADD_SIGNAL(MethodInfo("task_selected")); ADD_SIGNAL(MethodInfo("favorite_tasks_changed")); } TaskPalette::TaskPalette() { VBoxContainer *vb = memnew(VBoxContainer); add_child(vb); HBoxContainer *hb = memnew(HBoxContainer); vb->add_child(hb); tool_filters = memnew(Button); tool_filters->set_tooltip_text(TTR("Show filters")); tool_filters->set_flat(true); tool_filters->set_toggle_mode(true); tool_filters->set_focus_mode(FocusMode::FOCUS_NONE); hb->add_child(tool_filters); filter_edit = memnew(LineEdit); filter_edit->set_clear_button_enabled(true); filter_edit->set_placeholder(TTR("Filter tasks")); filter_edit->set_h_size_flags(SIZE_EXPAND_FILL); hb->add_child(filter_edit); tool_refresh = memnew(Button); tool_refresh->set_tooltip_text(TTR("Refresh tasks")); tool_refresh->set_flat(true); tool_refresh->set_focus_mode(FocusMode::FOCUS_NONE); hb->add_child(tool_refresh); ScrollContainer *sc = memnew(ScrollContainer); sc->set_h_size_flags(SIZE_EXPAND_FILL); sc->set_v_size_flags(SIZE_EXPAND_FILL); vb->add_child(sc); sections = memnew(VBoxContainer); sections->set_h_size_flags(SIZE_EXPAND_FILL); sections->set_v_size_flags(SIZE_EXPAND_FILL); sc->add_child(sections); menu = memnew(PopupMenu); add_child(menu); filter_popup = memnew(PopupPanel); { VBoxContainer *vbox = memnew(VBoxContainer); filter_popup->add_child(vbox); Label *type_header = memnew(Label); type_header->set_text(TTR("Type")); type_header->set_theme_type_variation("HeaderSmall"); vbox->add_child(type_header); HBoxContainer *type_filter = memnew(HBoxContainer); vbox->add_child(type_filter); Ref<ButtonGroup> type_filter_group; type_filter_group.instantiate(); type_all = memnew(Button); type_all->set_text(TTR("All")); type_all->set_tooltip_text(TTR("Show tasks of all types")); type_all->set_toggle_mode(true); type_all->set_focus_mode(FocusMode::FOCUS_NONE); type_all->set_button_group(type_filter_group); type_all->set_pressed(true); type_filter->add_child(type_all); type_core = memnew(Button); type_core->set_text(TTR("Core")); type_core->set_tooltip_text(TTR("Show only core tasks")); type_core->set_toggle_mode(true); type_core->set_focus_mode(FocusMode::FOCUS_NONE); type_core->set_button_group(type_filter_group); type_filter->add_child(type_core); type_user = memnew(Button); type_user->set_text(TTR("User")); type_user->set_tooltip_text(TTR("Show only user-implemented tasks (aka scripts)")); type_user->set_toggle_mode(true); type_user->set_focus_mode(FocusMode::FOCUS_NONE); type_user->set_button_group(type_filter_group); type_filter->add_child(type_user); Control *space1 = memnew(Control); space1->set_custom_minimum_size(Size2(0, 4)); vbox->add_child(space1); Label *category_header = memnew(Label); category_header->set_text(TTR("Categories")); category_header->set_theme_type_variation("HeaderSmall"); vbox->add_child(category_header); HBoxContainer *category_filter = memnew(HBoxContainer); vbox->add_child(category_filter); Ref<ButtonGroup> category_filter_group; category_filter_group.instantiate(); category_all = memnew(Button); category_all->set_text(TTR("All")); category_all->set_tooltip_text(TTR("Show tasks of all categories")); category_all->set_toggle_mode(true); category_all->set_focus_mode(FocusMode::FOCUS_NONE); category_all->set_pressed(true); category_all->set_button_group(category_filter_group); category_filter->add_child(category_all); category_include = memnew(Button); category_include->set_text(TTR("Include")); category_include->set_tooltip_text(TTR("Show tasks from selected categories")); category_include->set_toggle_mode(true); category_include->set_focus_mode(FocusMode::FOCUS_NONE); category_include->set_button_group(category_filter_group); category_filter->add_child(category_include); category_exclude = memnew(Button); category_exclude->set_text(TTR("Exclude")); category_exclude->set_tooltip_text(TTR("Don't show tasks from selected categories")); category_exclude->set_toggle_mode(true); category_exclude->set_focus_mode(FocusMode::FOCUS_NONE); category_exclude->set_button_group(category_filter_group); category_filter->add_child(category_exclude); category_choice = memnew(VBoxContainer); vbox->add_child(category_choice); HBoxContainer *selection_controls = memnew(HBoxContainer); selection_controls->set_alignment(BoxContainer::ALIGNMENT_CENTER); category_choice->add_child(selection_controls); select_all = memnew(Button); select_all->set_tooltip_text(TTR("Select all categories")); select_all->set_focus_mode(FocusMode::FOCUS_NONE); selection_controls->add_child(select_all); deselect_all = memnew(Button); deselect_all->set_tooltip_text(TTR("Deselect all categories")); deselect_all->set_focus_mode(FocusMode::FOCUS_NONE); selection_controls->add_child(deselect_all); category_scroll = memnew(ScrollContainer); category_choice->add_child(category_scroll); category_list = memnew(VBoxContainer); category_scroll->add_child(category_list); } add_child(filter_popup); } TaskPalette::~TaskPalette() { } #endif // ! TOOLS_ENABLED