From 70f3c3cf6f9c1563d95ffb8c25bf8cd2bb7a1ad0 Mon Sep 17 00:00:00 2001 From: hop311 Date: Tue, 30 Jul 2024 00:02:35 +0100 Subject: Search panel + text edit box UI generation --- extension/deps/openvic-simulation | 2 +- .../src/openvic-extension/classes/GUIListBox.cpp | 22 ++- .../src/openvic-extension/classes/GUINode.cpp | 3 +- .../src/openvic-extension/classes/GUINode.hpp | 3 + .../openvic-extension/singletons/GameSingleton.cpp | 2 +- .../openvic-extension/singletons/GameSingleton.hpp | 2 +- .../openvic-extension/singletons/MenuSingleton.cpp | 186 ++++++++++++++++++++- .../openvic-extension/singletons/MenuSingleton.hpp | 23 +++ .../singletons/ModelSingleton.cpp | 4 +- .../singletons/PopulationMenu.cpp | 2 +- .../src/openvic-extension/utility/UITools.cpp | 100 ++++++++--- game/src/Game/GameSession/GameSession.tscn | 10 +- .../GameSession/MapControlPanel/MapControlPanel.gd | 4 + .../MapControlPanel/MapControlPanel.tscn | 3 +- game/src/Game/GameSession/MapView.gd | 15 +- game/src/Game/GameSession/SearchPanel.gd | 143 ++++++++++++++++ 16 files changed, 477 insertions(+), 47 deletions(-) create mode 100644 game/src/Game/GameSession/SearchPanel.gd diff --git a/extension/deps/openvic-simulation b/extension/deps/openvic-simulation index 861acff..de19002 160000 --- a/extension/deps/openvic-simulation +++ b/extension/deps/openvic-simulation @@ -1 +1 @@ -Subproject commit 861acff78bd238232ed9e369ea046c2ed4a31198 +Subproject commit de190028555cb1b70ca9fbbd10af3de4440b8b89 diff --git a/extension/src/openvic-extension/classes/GUIListBox.cpp b/extension/src/openvic-extension/classes/GUIListBox.cpp index c153870..e3bea86 100644 --- a/extension/src/openvic-extension/classes/GUIListBox.cpp +++ b/extension/src/openvic-extension/classes/GUIListBox.cpp @@ -9,6 +9,7 @@ using namespace OpenVic; using namespace godot; +using namespace OpenVic::Utilities::literals; using OpenVic::Utilities::std_view_to_godot_string; @@ -24,14 +25,14 @@ Error GUIListBox::_calculate_max_scroll_index(bool signal) { if (fixed_item_count <= 0) { max_scroll_index = 0; fixed_visible_items = 0; - } else if (fixed_item_height <= 0.0f) { + } else if (fixed_item_height <= 0.0_real) { max_scroll_index = fixed_item_count - 1; fixed_visible_items = max_scroll_index; } else { const real_t max_height = get_size().height; fixed_visible_items = max_height / fixed_item_height; - max_scroll_index = fixed_item_count - std::max(fixed_visible_items, 1); + max_scroll_index = std::max(fixed_item_count - std::max(fixed_visible_items, 1), 0); } } else { const int32_t child_count = get_child_count(); @@ -41,7 +42,7 @@ Error GUIListBox::_calculate_max_scroll_index(bool signal) { } else { const real_t max_height = get_size().height; - real_t height_under_max_scroll_index = 0.0f; + real_t height_under_max_scroll_index = 0.0_real; max_scroll_index = child_count; @@ -75,8 +76,9 @@ Error GUIListBox::_calculate_max_scroll_index(bool signal) { Error GUIListBox::_update_child_positions() { const int32_t child_count = get_child_count(); const real_t max_height = get_size().height; + const Vector2 offset = gui_listbox != nullptr ? Utilities::to_godot_fvec2(gui_listbox->get_items_offset()) : Vector2 {}; - real_t height = 0.0f; + real_t height = 0.0_real; const int32_t child_scroll_index = fixed ? 0 : scroll_index; @@ -87,7 +89,7 @@ Error GUIListBox::_update_child_positions() { if (index < child_scroll_index) { child->hide(); } else { - child->set_position({ 0.0f, height }); + child->set_position(offset + Vector2 { 0.0_real, height }); height += child->get_size().height; /* Spacing is ignored */ @@ -130,7 +132,7 @@ void GUIListBox::_notification(int what) { GUIListBox::GUIListBox() : gui_listbox { nullptr }, scrollbar { nullptr }, scroll_index { 0 }, max_scroll_index { 0 }, - fixed { false }, fixed_item_count { 0 }, fixed_visible_items { 0 }, fixed_item_height { 0.0f } {} + fixed { false }, fixed_item_count { 0 }, fixed_visible_items { 0 }, fixed_item_height { 0.0_real } {} Vector2 GUIListBox::_get_minimum_size() const { if (gui_listbox != nullptr) { @@ -177,7 +179,7 @@ void GUIListBox::clear() { fixed = false; fixed_item_count = 0; fixed_visible_items = 0; - fixed_item_height = 0.0f; + fixed_item_height = 0.0_real; clear_children(); if (scrollbar != nullptr) { @@ -233,7 +235,7 @@ Error GUIListBox::unset_fixed(bool signal) { fixed = false; fixed_item_count = 0; - fixed_item_height = 0.0f; + fixed_item_height = 0.0_real; return _calculate_max_scroll_index(signal); } @@ -273,7 +275,9 @@ Error GUIListBox::set_gui_listbox(GUI::ListBox const* new_gui_listbox) { add_child(scrollbar, false, INTERNAL_MODE_FRONT); const Size2 size = Utilities::to_godot_fvec2(gui_listbox->get_size()); - scrollbar->set_position({ size.width, 0.0f }); + Vector2 position = Utilities::to_godot_fvec2(gui_listbox->get_scrollbar_offset()); + position.x += size.width; + scrollbar->set_position(position); scrollbar->set_length_override(size.height); static const StringName set_scroll_index_func_name = "set_scroll_index"; diff --git a/extension/src/openvic-extension/classes/GUINode.cpp b/extension/src/openvic-extension/classes/GUINode.cpp index d8ab6c1..bd8197b 100644 --- a/extension/src/openvic-extension/classes/GUINode.cpp +++ b/extension/src/openvic-extension/classes/GUINode.cpp @@ -46,7 +46,8 @@ using namespace OpenVic; F(TextureRect, texture_rect) \ F(GUIOverlappingElementsBox, gui_overlapping_elements_box) \ F(GUIScrollbar, gui_scrollbar) \ - F(GUIListBox, gui_listbox) + F(GUIListBox, gui_listbox) \ + F(LineEdit, line_edit) #define APPLY_TO_TEXTURE_TYPES(F) \ F(GFXSpriteTexture, gfx_sprite_texture) \ diff --git a/extension/src/openvic-extension/classes/GUINode.hpp b/extension/src/openvic-extension/classes/GUINode.hpp index 27ce780..f8eb62c 100644 --- a/extension/src/openvic-extension/classes/GUINode.hpp +++ b/extension/src/openvic-extension/classes/GUINode.hpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include #include @@ -58,6 +59,7 @@ namespace OpenVic { static GUIOverlappingElementsBox* get_gui_overlapping_elements_box_from_node(godot::Node* node); static GUIScrollbar* get_gui_scrollbar_from_node(godot::Node* node); static GUIListBox* get_gui_listbox_from_node(godot::Node* node); + static godot::LineEdit* get_line_edit_from_node(godot::Node* node); godot::Button* get_button_from_nodepath(godot::NodePath const& path) const; godot::Label* get_label_from_nodepath(godot::NodePath const& path) const; @@ -67,6 +69,7 @@ namespace OpenVic { GUIOverlappingElementsBox* get_gui_overlapping_elements_box_from_nodepath(godot::NodePath const& path) const; GUIScrollbar* get_gui_scrollbar_from_nodepath(godot::NodePath const& path) const; GUIListBox* get_gui_listbox_from_nodepath(godot::NodePath const& path) const; + godot::LineEdit* get_line_edit_from_nodepath(godot::NodePath const& path) const; /* Helper functions to get textures from TextureRects and Buttons. */ static godot::Ref get_texture_from_node(godot::Node* node); diff --git a/extension/src/openvic-extension/singletons/GameSingleton.cpp b/extension/src/openvic-extension/singletons/GameSingleton.cpp index c1a811e..33d70da 100644 --- a/extension/src/openvic-extension/singletons/GameSingleton.cpp +++ b/extension/src/openvic-extension/singletons/GameSingleton.cpp @@ -170,7 +170,7 @@ float GameSingleton::get_map_aspect_ratio() const { return static_cast(get_map_width()) / static_cast(get_map_height()); } -Vector2 GameSingleton::map_position_to_world_coords(fvec2_t const& position) const { +Vector2 GameSingleton::normalise_map_position(fvec2_t const& position) const { return Utilities::to_godot_fvec2(position) / get_map_dims(); } diff --git a/extension/src/openvic-extension/singletons/GameSingleton.hpp b/extension/src/openvic-extension/singletons/GameSingleton.hpp index b29e588..e7f12bd 100644 --- a/extension/src/openvic-extension/singletons/GameSingleton.hpp +++ b/extension/src/openvic-extension/singletons/GameSingleton.hpp @@ -86,7 +86,7 @@ namespace OpenVic { int32_t get_map_height() const; godot::Vector2i get_map_dims() const; float get_map_aspect_ratio() const; - godot::Vector2 map_position_to_world_coords(fvec2_t const& position) const; + godot::Vector2 normalise_map_position(fvec2_t const& position) const; /* The cosmetic terrain textures stored in a Texture2DArray. */ godot::Ref get_terrain_texture() const; diff --git a/extension/src/openvic-extension/singletons/MenuSingleton.cpp b/extension/src/openvic-extension/singletons/MenuSingleton.cpp index 1c3b6ec..885915c 100644 --- a/extension/src/openvic-extension/singletons/MenuSingleton.cpp +++ b/extension/src/openvic-extension/singletons/MenuSingleton.cpp @@ -28,6 +28,10 @@ StringName const& MenuSingleton::_signal_population_menu_pops_changed() { static const StringName signal_population_menu_pops_changed = "population_menu_pops_changed"; return signal_population_menu_pops_changed; } +StringName const& MenuSingleton::_signal_search_cache_changed() { + static const StringName signal_search_cache_changed = "search_cache_changed"; + return signal_search_cache_changed; +} String MenuSingleton::get_state_name(State const& state) const { StateSet const& state_set = state.get_state_set(); @@ -59,14 +63,47 @@ String MenuSingleton::get_state_name(State const& state) const { if (owned && split) { // COUNTRY STATE/CAPITAL - return tr(std_view_to_godot_string(StringUtils::append_string_views(state.get_owner()->get_identifier(), "_ADJ"))) - + " " + name; + return get_country_adjective(*state.get_owner()) + " " + name; } // STATE/CAPITAL return name; } +String MenuSingleton::get_country_name(CountryInstance const& country) const { + if (country.get_government_type() != nullptr && !country.get_government_type()->get_identifier().empty()) { + const String government_name_key = std_to_godot_string(StringUtils::append_string_views( + country.get_identifier(), "_", country.get_government_type()->get_identifier() + )); + + String government_name = tr(government_name_key); + + if (government_name != government_name_key) { + return government_name; + } + } + + return tr(std_view_to_godot_string(country.get_identifier())); +} + +String MenuSingleton::get_country_adjective(CountryInstance const& country) const { + static constexpr std::string_view adjective = "_ADJ"; + + if (country.get_government_type() != nullptr && !country.get_government_type()->get_identifier().empty()) { + const String government_adjective_key = std_to_godot_string(StringUtils::append_string_views( + country.get_identifier(), "_", country.get_government_type()->get_identifier(), adjective + )); + + String government_adjective = tr(government_adjective_key); + + if (government_adjective != government_adjective_key) { + return government_adjective; + } + } + + return tr(std_to_godot_string(StringUtils::append_string_views(country.get_identifier(), adjective))); +} + void MenuSingleton::_bind_methods() { /* PROVINCE OVERVIEW PANEL */ OV_BIND_METHOD(MenuSingleton::get_province_info_from_index, { "index" }); @@ -142,6 +179,15 @@ void MenuSingleton::_bind_methods() { BIND_ENUM_CONSTANT(SORT_REBEL_FACTION); BIND_ENUM_CONSTANT(SORT_SIZE_CHANGE); BIND_ENUM_CONSTANT(SORT_LITERACY); + + /* Find/Search Panel */ + OV_BIND_METHOD(MenuSingleton::generate_search_cache); + OV_BIND_METHOD(MenuSingleton::update_search_results, { "text" }); + OV_BIND_METHOD(MenuSingleton::get_search_result_rows, { "start", "count" }); + OV_BIND_METHOD(MenuSingleton::get_search_result_row_count); + OV_BIND_METHOD(MenuSingleton::get_search_result_position, { "result_index" }); + + ADD_SIGNAL(MethodInfo(_signal_search_cache_changed())); } MenuSingleton* MenuSingleton::get_singleton() { @@ -442,3 +488,139 @@ String MenuSingleton::get_longform_date() const { return Utilities::date_to_formatted_string(instance_manager->get_today()); } + +Error MenuSingleton::generate_search_cache() { + GameSingleton const* game_singleton = GameSingleton::get_singleton(); + ERR_FAIL_NULL_V(game_singleton, FAILED); + InstanceManager const* instance_manager = game_singleton->get_instance_manager(); + ERR_FAIL_NULL_V(instance_manager, FAILED); + + search_panel.entry_cache.clear(); + + std::vector const& provinces = instance_manager->get_map_instance().get_province_instances(); + std::vector const& state_sets = instance_manager->get_map_instance().get_state_manager().get_state_sets(); + std::vector const& countries = instance_manager->get_country_instance_manager().get_country_instances(); + + // TODO - reserve actual state count rather than state set count (maybe use a vector of pointers to all states?) + search_panel.entry_cache.reserve(provinces.size() + state_sets.size() + countries.size()); + + for (ProvinceInstance const& province : provinces) { + String identifier = std_view_to_godot_string(province.get_identifier()); + String display_name = tr(GUINode::format_province_name(identifier)); + String search_name = display_name.to_lower(); + + search_panel.entry_cache.push_back({ + &province, std::move(display_name), std::move(search_name), identifier.to_lower() + }); + } + + for (StateSet const& state_set : state_sets) { + for (State const& state : state_set.get_states()) { + String display_name = get_state_name(state); + String search_name = display_name.to_lower(); + + search_panel.entry_cache.push_back({ + // TODO - include state identifier? (region and/or split?) + &state, std::move(display_name), std::move(search_name), {} + }); + } + } + + for (CountryInstance const& country : countries) { + // TODO - replace with a proper "exists" check + if (country.get_capital() != nullptr) { + String display_name = get_country_name(country); + String search_name = display_name.to_lower(); + + search_panel.entry_cache.push_back({ + &country, std::move(display_name), std::move(search_name), + std_view_to_godot_string(country.get_identifier()).to_lower() + }); + } + } + + std::sort(search_panel.entry_cache.begin(), search_panel.entry_cache.end(), [](auto const& a, auto const& b) -> bool { + return a.search_name < b.search_name; + }); + + emit_signal(_signal_search_cache_changed()); + + return OK; +} + +void MenuSingleton::update_search_results(godot::String const& text) { + // Sanatise input + const String search_text = text.strip_edges().to_lower(); + + search_panel.result_indices.clear(); + + if (!search_text.is_empty()) { + // Search through cache + for (size_t idx = 0; idx < search_panel.entry_cache.size(); ++idx) { + search_panel_t::entry_t const& entry = search_panel.entry_cache[idx]; + + if (entry.search_name.begins_with(search_text) || entry.identifier == search_text) { + search_panel.result_indices.push_back(idx); + } + } + } +} + +PackedStringArray MenuSingleton::get_search_result_rows(int32_t start, int32_t count) const { + if (search_panel.result_indices.empty()) { + return {}; + } + + ERR_FAIL_INDEX_V_MSG( + start, search_panel.result_indices.size(), {}, + vformat("Invalid start for search panel result rows: %d", start) + ); + ERR_FAIL_COND_V_MSG(count <= 0, {}, vformat("Invalid count for search panel result rows: %d", count)); + + if (start + count > search_panel.result_indices.size()) { + UtilityFunctions::push_warning( + "Requested search result rows beyond the end of the result indices (", start, " + ", count, " > ", + static_cast(search_panel.result_indices.size()), "), limiting to ", + static_cast(search_panel.result_indices.size() - start), " rows." + ); + count = search_panel.result_indices.size() - start; + } + + PackedStringArray results; + results.resize(count); + + for (size_t idx = 0; idx < count; ++idx) { + results[idx] = search_panel.entry_cache[search_panel.result_indices[start + idx]].display_name; + } + + return results; +} + +int32_t MenuSingleton::get_search_result_row_count() const { + return search_panel.result_indices.size(); +} + +Vector2 MenuSingleton::get_search_result_position(int32_t result_index) const { + ERR_FAIL_INDEX_V(result_index, search_panel.result_indices.size(), {}); + + GameSingleton const* game_singleton = GameSingleton::get_singleton(); + ERR_FAIL_NULL_V(game_singleton, {}); + + struct entry_visitor_t { + fvec2_t operator()(ProvinceInstance const* province) { + return province->get_province_definition().get_centre(); + } + + fvec2_t operator()(State const* state) { + return (*this)(state->get_capital()); + } + + fvec2_t operator()(CountryInstance const* country) { + return (*this)(country->get_capital()); + } + } entry_visitor; + + return game_singleton->normalise_map_position( + std::visit(entry_visitor, search_panel.entry_cache[search_panel.result_indices[result_index]].target) + ); +} diff --git a/extension/src/openvic-extension/singletons/MenuSingleton.hpp b/extension/src/openvic-extension/singletons/MenuSingleton.hpp index 54891bc..190e3ea 100644 --- a/extension/src/openvic-extension/singletons/MenuSingleton.hpp +++ b/extension/src/openvic-extension/singletons/MenuSingleton.hpp @@ -73,8 +73,18 @@ namespace OpenVic { std::vector pops, filtered_pops; }; + struct search_panel_t { + struct entry_t { + std::variant target; + godot::String display_name, search_name, identifier; + }; + std::vector entry_cache; + std::vector result_indices; + }; + private: population_menu_t population_menu; + search_panel_t search_panel; /* Emitted when the number of visible province list rows changes (list generated or state entry expanded).*/ static godot::StringName const& _signal_population_menu_province_list_changed(); @@ -83,8 +93,12 @@ namespace OpenVic { static godot::StringName const& _signal_population_menu_province_list_selected_changed(); /* Emitted when the selected/filtered collection of pops changes. */ static godot::StringName const& _signal_population_menu_pops_changed(); + /* Emitted when the collection of possible search results changes. */ + static godot::StringName const& _signal_search_cache_changed(); godot::String get_state_name(State const& state) const; + godot::String get_country_name(CountryInstance const& country) const; + godot::String get_country_adjective(CountryInstance const& country) const; protected: static void _bind_methods(); @@ -145,6 +159,15 @@ namespace OpenVic { godot::PackedStringArray get_population_menu_distribution_setup_info() const; /* Array of GFXPieChartTexture::godot_pie_chart_data_t. */ godot::TypedArray get_population_menu_distribution_info() const; + + /* Find/Search Panel */ + // TODO - update on country government type change and state creation/destruction + // (which automatically includes country creation/destruction) + godot::Error generate_search_cache(); + void update_search_results(godot::String const& text); + godot::PackedStringArray get_search_result_rows(int32_t start, int32_t count) const; + int32_t get_search_result_row_count() const; + godot::Vector2 get_search_result_position(int32_t result_index) const; }; } diff --git a/extension/src/openvic-extension/singletons/ModelSingleton.cpp b/extension/src/openvic-extension/singletons/ModelSingleton.cpp index 5fb9cf8..0e90a00 100644 --- a/extension/src/openvic-extension/singletons/ModelSingleton.cpp +++ b/extension/src/openvic-extension/singletons/ModelSingleton.cpp @@ -295,7 +295,7 @@ bool ModelSingleton::add_unit_dict( } dict[position_key] = - game_singleton->map_position_to_world_coords(unit.get_position()->get_province_definition().get_unit_position()); + game_singleton->normalise_map_position(unit.get_position()->get_province_definition().get_unit_position()); if (display_unit_type->get_unit_category() != UnitType::unit_category_t::INFANTRY) { dict[rotation_key] = -0.25f * std::numbers::pi_v; @@ -433,7 +433,7 @@ bool ModelSingleton::add_building_dict( dict[model_key] = make_model_dict(*actor); - dict[position_key] = game_singleton->map_position_to_world_coords( + dict[position_key] = game_singleton->normalise_map_position( position_ptr != nullptr ? *position_ptr : province_definition.get_centre() ); diff --git a/extension/src/openvic-extension/singletons/PopulationMenu.cpp b/extension/src/openvic-extension/singletons/PopulationMenu.cpp index df5b6b1..65987ad 100644 --- a/extension/src/openvic-extension/singletons/PopulationMenu.cpp +++ b/extension/src/openvic-extension/singletons/PopulationMenu.cpp @@ -110,7 +110,7 @@ TypedArray MenuSingleton::get_population_menu_province_list_rows(int country_dict[type_key] = population_menu_t::LIST_ENTRY_COUNTRY; country_dict[index_key] = index; - country_dict[name_key] = std_view_to_godot_string(country_entry.country.get_identifier()); + country_dict[name_key] = menu_singleton.get_country_name(country_entry.country); country_dict[size_key] = country_entry.country.get_total_population(); country_dict[change_key] = 0; country_dict[selected_key] = country_entry.selected; diff --git a/extension/src/openvic-extension/utility/UITools.cpp b/extension/src/openvic-extension/utility/UITools.cpp index 3c7e04f..93fe9fe 100644 --- a/extension/src/openvic-extension/utility/UITools.cpp +++ b/extension/src/openvic-extension/utility/UITools.cpp @@ -3,7 +3,9 @@ #include #include #include +#include #include +#include #include #include #include @@ -111,7 +113,9 @@ static bool new_control(T*& node, GUI::Element const& element, String const& nam return ret; } -static bool add_theme_stylebox(Control* control, StringName const& theme_name, Ref const& texture) { +static bool add_theme_stylebox( + Control* control, StringName const& theme_name, Ref const& texture, Vector2 border = {} +) { Ref stylebox; stylebox.instantiate(); ERR_FAIL_NULL_V(stylebox, false); @@ -121,6 +125,13 @@ static bool add_theme_stylebox(Control* control, StringName const& theme_name, R static const StringName emit_changed_func = "emit_changed"; texture->connect(changed_signal, Callable { *stylebox, emit_changed_func }, Object::CONNECT_PERSIST); + if (border != Vector2 {}) { + stylebox->set_texture_margin(SIDE_LEFT, border.x); + stylebox->set_texture_margin(SIDE_RIGHT, border.x); + stylebox->set_texture_margin(SIDE_TOP, border.y); + stylebox->set_texture_margin(SIDE_BOTTOM, border.y); + } + control->add_theme_stylebox_override(theme_name, stylebox); return true; }; @@ -574,37 +585,78 @@ static bool generate_listbox(generate_gui_args_t&& args) { return ret; } -template T> -requires requires(T const& element) { - { element.get_size() } -> std::same_as; -} -static bool generate_placeholder(generate_gui_args_t&& args, Color colour) { - T const& cast_element = static_cast(args.element); +static bool generate_texteditbox(generate_gui_args_t&& args) { + using namespace OpenVic::Utilities::literals; - static const String type_name = std_view_to_godot_string(T::get_type_static()); - const String placeholder_name = std_view_to_godot_string(cast_element.get_name()); - const Vector2 godot_size = Utilities::to_godot_fvec2(cast_element.get_size()); + GUI::TextEditBox const& text_edit_box = static_cast(args.element); - UtilityFunctions::push_warning( - "Generating placeholder ColorRect for GUI ", type_name, " ", placeholder_name, " (size ", godot_size, ")" - ); + const String text_edit_box_name = std_view_to_godot_string(text_edit_box.get_name()); - ColorRect* godot_rect = nullptr; - bool ret = new_control(godot_rect, cast_element, args.name); + LineEdit* godot_line_edit = nullptr; + bool ret = new_control(godot_line_edit, text_edit_box, args.name); ERR_FAIL_NULL_V_MSG( - godot_rect, false, vformat("Failed to create placeholder ColorRect for GUI %s %s", type_name, placeholder_name) + godot_line_edit, false, vformat("Failed to create LineEdit for GUI text edit box %s", text_edit_box_name) ); - godot_rect->set_custom_minimum_size(godot_size); - godot_rect->set_color(colour); + godot_line_edit->set_context_menu_enabled(false); + godot_line_edit->set_caret_blink_enabled(true); + godot_line_edit->set_focus_mode(Control::FOCUS_CLICK); - args.result = godot_rect; - return ret; -} + godot_line_edit->set_text(std_view_to_godot_string(text_edit_box.get_text())); -static bool generate_texteditbox(generate_gui_args_t&& args) { - using namespace OpenVic::Utilities::literals; - return generate_placeholder(std::move(args), { 0.0_real, 1.0_real, 0.0_real, 0.3_real }); + static const Vector2 default_position_padding { -4.0_real, 1.0_real }; + static const Vector2 default_size_padding { 2.0_real, 2.0_real }; + const Vector2 border_size = Utilities::to_godot_fvec2(text_edit_box.get_border_size()); + const Vector2 max_size = Utilities::to_godot_fvec2(text_edit_box.get_size()); + godot_line_edit->set_position(godot_line_edit->get_position() + border_size + default_position_padding); + godot_line_edit->set_custom_minimum_size(max_size - border_size - default_size_padding); + + static const StringName caret_color_theme = "caret_color"; + static const Color caret_colour { 1.0_real, 0.0_real, 0.0_real }; + godot_line_edit->add_theme_color_override(caret_color_theme, caret_colour); + + if (text_edit_box.get_font() != nullptr) { + const StringName font_file = std_view_to_godot_string_name(text_edit_box.get_font()->get_fontname()); + const Ref font = args.asset_manager.get_font(font_file); + if (font.is_valid()) { + static const StringName font_theme = "font"; + godot_line_edit->add_theme_font_override(font_theme, font); + } else { + UtilityFunctions::push_error("Failed to load font \"", font_file, "\" for GUI text edit box", text_edit_box_name); + ret = false; + } + const Color colour = Utilities::to_godot_color(text_edit_box.get_font()->get_colour()); + static const StringName font_color_theme = "font_color"; + godot_line_edit->add_theme_color_override(font_color_theme, colour); + } + + const StringName texture_file = std_view_to_godot_string_name(text_edit_box.get_texture_file()); + if (!texture_file.is_empty()) { + Ref texture = args.asset_manager.get_texture(texture_file); + + if (texture.is_valid()) { + static const StringName normal_theme = "normal"; + ret &= add_theme_stylebox(godot_line_edit, normal_theme, texture, border_size); + } else { + UtilityFunctions::push_error( + "Failed to load texture \"", texture_file, "\" for text edit box \"", text_edit_box_name, "\"" + ); + ret = false; + } + } + + Ref stylebox_empty; + stylebox_empty.instantiate(); + if (stylebox_empty.is_valid()) { + static const StringName focus_theme = "focus"; + godot_line_edit->add_theme_stylebox_override(focus_theme, stylebox_empty); + } else { + UtilityFunctions::push_error("Failed to create empty style box for focus of GUI text edit box ", text_edit_box_name); + ret = false; + } + + args.result = godot_line_edit; + return ret; } static bool generate_scrollbar(generate_gui_args_t&& args) { diff --git a/game/src/Game/GameSession/GameSession.tscn b/game/src/Game/GameSession/GameSession.tscn index d54970f..018aad8 100644 --- a/game/src/Game/GameSession/GameSession.tscn +++ b/game/src/Game/GameSession/GameSession.tscn @@ -1,4 +1,4 @@ -[gd_scene load_steps=19 format=3 uid="uid://bgnupcshe1m7r"] +[gd_scene load_steps=20 format=3 uid="uid://bgnupcshe1m7r"] [ext_resource type="Script" path="res://src/Game/GameSession/GameSession.gd" id="1_eklvp"] [ext_resource type="PackedScene" uid="uid://cvl76duuym1wq" path="res://src/Game/MusicConductor/MusicPlayer.tscn" id="2_kt6aa"] @@ -9,6 +9,7 @@ [ext_resource type="PackedScene" uid="uid://dkehmdnuxih2r" path="res://src/Game/GameSession/MapView.tscn" id="4_xkg5j"] [ext_resource type="Script" path="res://src/Game/GameSession/NationManagementScreen/ProductionMenu.gd" id="5_16755"] [ext_resource type="Script" path="res://src/Game/GameSession/ProvinceOverviewPanel.gd" id="5_lfv8l"] +[ext_resource type="Script" path="res://src/Game/GameSession/SearchPanel.gd" id="5_t260f"] [ext_resource type="PackedScene" uid="uid://cnbfxjy1m6wja" path="res://src/Game/Menu/OptionMenu/OptionsMenu.tscn" id="6_p5mnx"] [ext_resource type="Script" path="res://src/Game/GameSession/NationManagementScreen/BudgetMenu.gd" id="6_vninv"] [ext_resource type="Script" path="res://src/Game/GameSession/NationManagementScreen/TechnologyMenu.gd" id="7_r712c"] @@ -111,6 +112,12 @@ anchor_bottom = 1.0 grow_horizontal = 0 grow_vertical = 0 +[node name="SearchPanel" type="GUINode" parent="UICanvasLayer/UI" node_paths=PackedStringArray("_map_view")] +layout_mode = 1 +anchors_preset = 15 +script = ExtResource("5_t260f") +_map_view = NodePath("../../../MapView") + [node name="GameSessionMenu" parent="UICanvasLayer/UI" instance=ExtResource("3_bvmqh")] visible = false layout_mode = 1 @@ -153,6 +160,7 @@ grow_horizontal = 0 [connection signal="map_view_camera_changed" from="MapView" to="UICanvasLayer/UI/MapControlPanel" method="_on_map_view_camera_changed"] [connection signal="game_session_menu_button_pressed" from="UICanvasLayer/UI/MapControlPanel" to="." method="_on_game_session_menu_button_pressed"] [connection signal="minimap_clicked" from="UICanvasLayer/UI/MapControlPanel" to="MapView" method="_on_minimap_clicked"] +[connection signal="search_button_pressed" from="UICanvasLayer/UI/MapControlPanel" to="UICanvasLayer/UI/SearchPanel" method="toggle_visibility"] [connection signal="zoom_in_button_pressed" from="UICanvasLayer/UI/MapControlPanel" to="MapView" method="zoom_in"] [connection signal="zoom_out_button_pressed" from="UICanvasLayer/UI/MapControlPanel" to="MapView" method="zoom_out"] [connection signal="load_button_pressed" from="UICanvasLayer/UI/GameSessionMenu" to="UICanvasLayer/UI/SaveLoadMenu" method="show_for_load"] diff --git a/game/src/Game/GameSession/MapControlPanel/MapControlPanel.gd b/game/src/Game/GameSession/MapControlPanel/MapControlPanel.gd index eb4dd9f..61de6ae 100644 --- a/game/src/Game/GameSession/MapControlPanel/MapControlPanel.gd +++ b/game/src/Game/GameSession/MapControlPanel/MapControlPanel.gd @@ -1,6 +1,7 @@ extends PanelContainer signal game_session_menu_button_pressed +signal search_button_pressed signal map_view_camera_changed(near_left : Vector2, far_left : Vector2, far_right : Vector2, near_right : Vector2) signal minimap_clicked(pos_clicked : Vector2) signal zoom_in_button_pressed @@ -35,6 +36,9 @@ func _ready() -> void: func _on_game_session_menu_button_pressed() -> void: game_session_menu_button_pressed.emit() +func _on_search_button_pressed() -> void: + search_button_pressed.emit() + # REQUIREMENTS: # * SS-76 # * UIFUN-129, UIFUN-131, UIFUN-133, UIFUN-140, UIFUN-141, UIFUN-142 diff --git a/game/src/Game/GameSession/MapControlPanel/MapControlPanel.tscn b/game/src/Game/GameSession/MapControlPanel/MapControlPanel.tscn index 7578c82..d49cf61 100644 --- a/game/src/Game/GameSession/MapControlPanel/MapControlPanel.tscn +++ b/game/src/Game/GameSession/MapControlPanel/MapControlPanel.tscn @@ -80,7 +80,7 @@ focus_mode = 0 mouse_filter = 1 text = "L" -[node name="FindButton" type="Button" parent="MapPanelMargin/MapPanelList/AuxiliaryPanel"] +[node name="SearchButton" type="Button" parent="MapPanelMargin/MapPanelList/AuxiliaryPanel"] editor_description = "UI-861" layout_mode = 2 focus_mode = 0 @@ -108,5 +108,6 @@ text = "-" [connection signal="map_view_camera_changed" from="." to="MapPanelMargin/MapPanelList/MapDisplayList/Minimap/ViewportQuad" method="_on_map_view_camera_changed"] [connection signal="minimap_clicked" from="MapPanelMargin/MapPanelList/MapDisplayList/Minimap/ViewportQuad" to="." method="_on_minimap_clicked"] [connection signal="pressed" from="MapPanelMargin/MapPanelList/AuxiliaryPanel/GameSessionMenuButton" to="." method="_on_game_session_menu_button_pressed"] +[connection signal="pressed" from="MapPanelMargin/MapPanelList/AuxiliaryPanel/SearchButton" to="." method="_on_search_button_pressed"] [connection signal="pressed" from="MapPanelMargin/MapPanelList/AuxiliaryPanel/ZoomButtonsContainer/ZoomInButton" to="." method="_on_zoom_in_button_pressed"] [connection signal="pressed" from="MapPanelMargin/MapPanelList/AuxiliaryPanel/ZoomButtonsContainer/ZoomOutButton" to="." method="_on_zoom_out_button_pressed"] diff --git a/game/src/Game/GameSession/MapView.gd b/game/src/Game/GameSession/MapView.gd index 01755ec..d77cfe5 100644 --- a/game/src/Game/GameSession/MapView.gd +++ b/game/src/Game/GameSession/MapView.gd @@ -134,18 +134,27 @@ func _map_to_world_coords(pos : Vector2) -> Vector3: pos = pos * _map_mesh_dims + _map_mesh_corner return Vector3(pos.x, 0, pos.y) -func _viewport_to_map_coords(pos_viewport : Vector2) -> Vector2: +func _viewport_to_world_coords(pos_viewport : Vector2) -> Vector3: var ray_origin := _camera.project_ray_origin(pos_viewport) var ray_normal := _camera.project_ray_normal(pos_viewport) # Plane with normal (0,1,0) facing upwards, at a distance 0 from the origin var intersection : Variant = Plane(0, 1, 0, 0).intersects_ray(ray_origin, ray_normal) if typeof(intersection) == TYPE_VECTOR3: - return _world_to_map_coords(intersection as Vector3) + return intersection else: # Normals parallel to the xz-plane could cause null intersections, # but the camera's orientation should prevent such normals push_error("Invalid intersection: ", intersection) - return Vector2(0.5, 0.5) + return _map_to_world_coords(Vector2(0.5, 0.5)) + +func _viewport_to_map_coords(pos_viewport : Vector2) -> Vector2: + return _world_to_map_coords(_viewport_to_world_coords(pos_viewport)) + +func look_at_map_position(pos : Vector2) -> void: + var viewport_centre : Vector2 = Vector2(0.5, 0.5) * _viewport_dims / GuiScale.get_current_guiscale() + var pos_delta : Vector3 = _map_to_world_coords(pos) - _viewport_to_world_coords(viewport_centre) + _camera.position.x += pos_delta.x + _camera.position.z += pos_delta.z func zoom_in() -> void: _zoom_target -= _zoom_target_step diff --git a/game/src/Game/GameSession/SearchPanel.gd b/game/src/Game/GameSession/SearchPanel.gd new file mode 100644 index 0000000..5554226 --- /dev/null +++ b/game/src/Game/GameSession/SearchPanel.gd @@ -0,0 +1,143 @@ +extends GUINode + +@export var _map_view : MapView + +var _search_panel : Panel +var _search_line_edit : LineEdit +var _results_list_box : GUIListBox +var _result_buttons : Array[Button] + +var _drag_active : bool = false +var _drag_anchor : Vector2 + +func _ready() -> void: + MenuSingleton.search_cache_changed.connect(_update_results_base) + + add_gui_element("goto", "goto_box") + + remove_node(^"./goto_box/goto") + + _search_panel = get_panel_from_nodepath(^"./goto_box") + + var close_button : Button = get_button_from_nodepath(^"./goto_box/cancel") + if close_button: + close_button.pressed.connect(hide) + + var panel_button : Button = get_button_from_nodepath(^"./goto_box/goto_box") + if panel_button: + panel_button.button_down.connect(_start_drag) + panel_button.button_up.connect(_end_drag) + if _search_panel: + # Move to back so it's not drawn over the results list + _search_panel.move_child(panel_button, 0) + + _search_line_edit = get_line_edit_from_nodepath(^"./goto_box/goto_edit") + if _search_line_edit: + _search_line_edit.text_changed.connect(_search_string_updated) + # Restrict to desired size (by default it's a bit too tall, probably due to font size) + _search_line_edit.set_size(_search_line_edit.get_minimum_size()) + + _results_list_box = get_gui_listbox_from_nodepath(^"./goto_box/provinces") + if _results_list_box: + _results_list_box.scroll_index_changed.connect(_update_results_scroll) + + _results_list_box.set_position(_results_list_box.get_position() - Vector2(4, 0)) + + hide() + + MenuSingleton.generate_search_cache() + +func toggle_visibility() -> void: + if is_visible(): + hide() + else: + show() + if _search_line_edit: + _search_line_edit.grab_focus() + +func _start_drag() -> void: + if _search_panel: + _drag_anchor = _search_panel.get_position() - get_window().get_mouse_position() + _drag_active = true + +func _end_drag() -> void: + _drag_active = false + +func _input(event : InputEvent) -> void: + if _drag_active and event is InputEventMouseMotion: + _search_panel.set_position(_drag_anchor + get_window().get_mouse_position()) + +func _notification(what : int) -> void: + match what: + NOTIFICATION_TRANSLATION_CHANGED: + MenuSingleton.generate_search_cache() + +func _search_string_updated(search_string : String) -> void: + MenuSingleton.update_search_results(search_string) + _update_results_base() + +func _update_results_base() -> void: + if not _results_list_box: + return + + var result_count : int = MenuSingleton.get_search_result_row_count() + + var result_height : float = 0.0 + if result_count > 0 and (_results_list_box.get_child_count() > 0 or _add_result_button()): + result_height = _results_list_box.get_child(0).get_size().y + + _results_list_box.set_fixed(result_count, result_height, false) + _update_results_scroll() + +func _add_result_button() -> bool: + if not _results_list_box: + return false + + var child : Panel = GUINode.generate_gui_element("menubar", "save_game_entry") + if not child: + return false + + var button : Button = GUINode.get_button_from_node(child.get_node(^"./game")) + if not button: + child.queue_free() + return false + + button.pressed.connect(_result_selected.bind(_result_buttons.size())) + + _results_list_box.add_child(child) + _result_buttons.push_back(button) + + return true + +func _update_results_scroll(scroll_index : int = -1) -> void: + if not _results_list_box: + return + + if scroll_index >= 0: + _results_list_box.set_scroll_index(scroll_index, false) + + scroll_index = _results_list_box.get_scroll_index() + + var results : PackedStringArray = MenuSingleton.get_search_result_rows(scroll_index, _results_list_box.get_fixed_visible_items()) + + if results.size() < _result_buttons.size(): + _result_buttons.resize(results.size()) + _results_list_box.clear_children(results.size()) + else: + while _result_buttons.size() < results.size() and _add_result_button(): + pass # Button is added in the loop condition + + for index : int in min(results.size(), _result_buttons.size()): + _result_buttons[index].set_text(results[index]) + +func _result_selected(index : int) -> void: + if _map_view: + _map_view.look_at_map_position(MenuSingleton.get_search_result_position(index)) + else: + push_error("SearchPanel missing MapView reference!") + + if _search_line_edit: + # This triggers a search results update, preventing further get_search_result_position(index) calls + _search_line_edit.clear() + + hide() -- cgit v1.2.3-56-ga3b1