diff --git a/src/gui/gui2_entrylist.h b/src/gui/gui2_entrylist.h index ea6ee964a1..67346290bd 100644 --- a/src/gui/gui2_entrylist.h +++ b/src/gui/gui2_entrylist.h @@ -10,7 +10,7 @@ class GuiEntryList : public GuiElement public: typedef std::function func_t; -private: +protected: class GuiEntry { public: @@ -18,9 +18,9 @@ class GuiEntryList : public GuiElement string value; string icon_name = ""; GuiEntry(string name, string value) : name(name), value(value) {} + GuiEntry(string name, string value, string icon_name) : name(name), value(value), icon_name(icon_name) {} }; -protected: std::vector entries; int selection_index; func_t func; @@ -30,15 +30,15 @@ class GuiEntryList : public GuiElement GuiEntryList* setOptions(const std::vector& options); GuiEntryList* setOptions(const std::vector& options, const std::vector& values); - void setEntryName(int index, string name); - void setEntryValue(int index, string value); - void setEntryIcon(int index, string icon_name); - void setEntry(int index, string name, string value); + virtual void setEntryName(int index, string name); + virtual void setEntryValue(int index, string value); + virtual void setEntryIcon(int index, string icon_name); + virtual void setEntry(int index, string name, string value); - int addEntry(string name, string value); + virtual int addEntry(string name, string value); int indexByValue(string value) const; - void removeEntry(int index); - void clear(); + virtual void removeEntry(int index); + virtual void clear(); int entryCount() const; string getEntryName(int index) const; string getEntryValue(int index) const; diff --git a/src/gui/gui2_listbox.cpp b/src/gui/gui2_listbox.cpp index 77dd3933c7..64b9fa2b61 100644 --- a/src/gui/gui2_listbox.cpp +++ b/src/gui/gui2_listbox.cpp @@ -3,6 +3,7 @@ #include "theme.h" #include "gui2_scrollbar.h" +#include "gui2_textentry.h" GuiListbox::GuiListbox(GuiContainer* owner, string id, func_t func) : GuiEntryList(owner, id, func), text_size(30), button_height(50), text_alignment(sp::Alignment::Center), mouse_scroll_steps(25) @@ -41,6 +42,186 @@ GuiListbox* GuiListbox::scrollTo(int index) return this; } +GuiListbox* GuiListbox::addSearch(search_func_t callback) +{ + if (search_entry) return this; + + // Set the search callback. + search_callback = callback; + if (!search_callback) + { + all_entries = {}; + for (int i = 0; i < entryCount(); i++) + all_entries.emplace_back(getEntryName(i), getEntryValue(i), getEntryIcon(i)); + } + + // Build search entry field. + search_entry = new GuiTextEntry(this, id + "_SEARCH", ""); + search_entry + ->setTextSize(20.0f) + ->setSize(GuiElement::GuiSizeMax, search_bar_height) + ->setPosition(0.0f, 0.0f, sp::Alignment::TopLeft); + search_entry->callback( + [this](string value) + { + search_text = value.lower(); + if (search_callback) search_callback(search_text); + else applyFilter(); + } + ); + + // Handle listbox scrollbar. + scroll->setPosition(0.0f, search_bar_height, sp::Alignment::TopRight); + return this; +} + +GuiListbox* GuiListbox::clearSearch() +{ + if (!search_entry) return this; + search_text = ""; + search_entry->setText(""); + if (!search_callback) applyFilter(); + return this; +} + +int GuiListbox::addEntry(string name, string value) +{ + if (search_entry && !search_filtering && !search_callback) + { + all_entries.emplace_back(name, value, ""); + if (name.lower().find(search_text) >= 0) + return GuiEntryList::addEntry(name, value); + return -1; + } + return GuiEntryList::addEntry(name, value); +} + +void GuiListbox::clear() +{ + if (search_entry && !search_filtering && !search_callback) + { + all_entries.clear(); + search_text = ""; + search_entry->setText(""); + } + GuiEntryList::clear(); +} + +void GuiListbox::setEntryIcon(int index, string icon_name) +{ + if (search_entry && !search_filtering && !search_callback && index >= 0) + { + string val = getEntryValue(index); + for (auto& e : all_entries) + { + if (e.value == val) + { + e.icon_name = icon_name; + break; + } + } + } + GuiEntryList::setEntryIcon(index, icon_name); +} + +void GuiListbox::setEntryName(int index, string name) +{ + if (search_entry && !search_filtering && !search_callback && index >= 0) + { + string val = getEntryValue(index); + for (auto& e : all_entries) + { + if (e.value == val) + { + e.name = name; + break; + } + } + } + GuiEntryList::setEntryName(index, name); +} + +void GuiListbox::setEntryValue(int index, string value) +{ + if (search_entry && !search_filtering && !search_callback && index >= 0) + { + string old_val = getEntryValue(index); + for (auto& e : all_entries) + { + if (e.value == old_val) + { + e.value = value; + break; + } + } + } + GuiEntryList::setEntryValue(index, value); +} + +void GuiListbox::setEntry(int index, string name, string value) +{ + if (search_entry && !search_filtering && !search_callback && index >= 0) + { + string old_val = getEntryValue(index); + for (auto& e : all_entries) + { + if (e.value == old_val) + { + e.name = name; + e.value = value; + break; + } + } + } + GuiEntryList::setEntry(index, name, value); +} + +void GuiListbox::removeEntry(int index) +{ + if (search_entry && !search_filtering && !search_callback && index >= 0) + { + string val = getEntryValue(index); + for (auto it = all_entries.begin(); it != all_entries.end(); ++it) + { + if (it->value == val) + { + all_entries.erase(it); + break; + } + } + } + GuiEntryList::removeEntry(index); +} + +string GuiListbox::getSearchText() const +{ + return search_text; +} + +void GuiListbox::applyFilter() +{ + search_filtering = true; + // Capture previous selection. + int prev_index = getSelectionIndex(); + string prev_value = getSelectionValue(); + + // Clear the list and rebuild it with matches. + GuiEntryList::clear(); + for (const auto& e : all_entries) + { + if (e.name.lower().find(search_text) >= 0) + { + int idx = GuiEntryList::addEntry(e.name, e.value); + GuiEntryList::setEntryIcon(idx, e.icon_name); + } + } + + // Select the previous value, if present. + int restored = indexByValue(prev_value); + if (prev_index >= 0 && restored >= 0) setSelectionIndex(restored); + search_filtering = false; +} + void GuiListbox::onDraw(sp::RenderTarget& renderer) { hover = false; @@ -51,15 +232,21 @@ void GuiListbox::onDraw(sp::RenderTarget& renderer) const auto& back_selected_hover = back_selected_style->get(State::Hover); const auto& front_selected = front_selected_style->get(getState()); + // Reserve the top of the rect for the search entry if present. + float search_offset = search_entry ? search_bar_height : 0.0f; + sp::Rect clip_rect = rect; + clip_rect.position.y += search_offset; + clip_rect.size.y -= search_offset; + scroll - ->setValueSize(rect.size.y) + ->setValueSize(clip_rect.size.y) ->setRange(0, entries.size() * button_height) // Determine whether to show the scrollbar based on the total height of // all items in the list. - ->setVisible(static_cast(entries.size()) > rect.size.y / button_height); - + ->setVisible(static_cast(entries.size()) > clip_rect.size.y / button_height); + // Draw the button. If the scrollbar is visible, make room. - sp::Rect button_rect{rect.position, {rect.size.x, button_height}}; + sp::Rect button_rect{clip_rect.position, {clip_rect.size.x, button_height}}; if (scroll->isVisible()) button_rect.size.x -= scroll->getRect().size.x; @@ -70,9 +257,9 @@ void GuiListbox::onDraw(sp::RenderTarget& renderer) int index = 0; for(auto& e : entries) { - // Draw the button only if it will visible within the container. - if (button_rect.position.y + button_rect.size.y >= rect.position.y - && button_rect.position.y <= rect.position.y + rect.size.y) + // Draw the button only if it will be visible within the clip area. + if (button_rect.position.y + button_rect.size.y >= clip_rect.position.y + && button_rect.position.y <= clip_rect.position.y + clip_rect.size.y) { auto* b = button_rect.contains(hover_coordinates) ? &back_hover : &back; auto* f = &front; @@ -85,7 +272,7 @@ void GuiListbox::onDraw(sp::RenderTarget& renderer) } // Draw the background texture. - renderer.drawStretchedHVClipped(button_rect, rect, button_height * 0.5f, b->texture, b->color); + renderer.drawStretchedHVClipped(button_rect, clip_rect, button_height * 0.5f, b->texture, b->color); // Draw the icon, if one's defined. // 60% button height and aligned left. @@ -98,7 +285,7 @@ void GuiListbox::onDraw(sp::RenderTarget& renderer) button_rect.position.y + button_rect.size.y * 0.5f ), button_rect.size.y * 0.6f, // size - rect, // clipping rectangle + clip_rect, // clipping rectangle f->color // color ); } @@ -106,10 +293,10 @@ void GuiListbox::onDraw(sp::RenderTarget& renderer) // Prepare the foreground text style. auto prepared = f->font->prepare(e.name, 32, text_size, f->color, button_rect.size, sp::Alignment::Center, sp::Font::FlagClip); for(auto& c : prepared.data) - c.position.y -= rect.position.y - button_rect.position.y; + c.position.y -= clip_rect.position.y - button_rect.position.y; // Draw the text. - renderer.drawText(rect, prepared, sp::Font::FlagClip); + renderer.drawText(clip_rect, prepared, sp::Font::FlagClip); } // Prepare to draw the next button below this one. @@ -120,15 +307,30 @@ void GuiListbox::onDraw(sp::RenderTarget& renderer) bool GuiListbox::onMouseDown(sp::io::Pointer::Button button, glm::vec2 position, sp::io::Pointer::ID id) { - int offset = (position.y - rect.position.y + scroll->getValue()) / button_height; + float search_offset = search_entry ? search_bar_height : 0.0f; + if (position.y < rect.position.y + search_offset) return false; + int offset = (position.y - rect.position.y - search_offset + scroll->getValue()) / button_height; return offset >= 0 && offset < int(entries.size()); } void GuiListbox::onMouseUp(glm::vec2 position, sp::io::Pointer::ID id) { - int offset = (position.y - rect.position.y + scroll->getValue()) / button_height; - if (offset >= 0 && offset < int(entries.size())) { + float search_offset = search_entry ? search_bar_height : 0.0f; + int offset = (position.y - rect.position.y - search_offset + scroll->getValue()) / button_height; + if (offset >= 0 && offset < static_cast(entries.size())) + { soundManager->playSound("sfx/button.wav"); + string selected_value = getEntryValue(offset); + // Selecting an item clears the search so the full list is visible + // afterwards, consistent with treating the search as a navigation aid + // rather than a persistent filter. + if (search_entry && !search_callback) + { + search_text = ""; + search_entry->setText(""); + applyFilter(); + offset = indexByValue(selected_value); + } setSelectionIndex(offset); callback(); } diff --git a/src/gui/gui2_listbox.h b/src/gui/gui2_listbox.h index 59998de4ab..dea6263096 100644 --- a/src/gui/gui2_listbox.h +++ b/src/gui/gui2_listbox.h @@ -3,9 +3,13 @@ #include "gui2_entrylist.h" class GuiScrollbar; +class GuiTextEntry; class GuiListbox : public GuiEntryList { +public: + // Text filter search callback function. + using search_func_t = std::function; protected: float text_size; float button_height; @@ -18,6 +22,17 @@ class GuiListbox : public GuiEntryList const GuiThemeStyle* front_style; const GuiThemeStyle* back_selected_style; const GuiThemeStyle* front_selected_style; + + // Text entry field for text filter search. + GuiTextEntry* search_entry = nullptr; + // Callback run when text is entered in the text filter search. + search_func_t search_callback; + // Master list of all entries, used to rebuild the visible list on each filter change. + std::vector all_entries; + // Boolean indicator of whether filtering is active. + bool search_filtering = false; + // Text entered into the text filter search, to be matched against entries. + string search_text; public: GuiListbox(GuiContainer* owner, string id, func_t func); @@ -25,9 +40,33 @@ class GuiListbox : public GuiEntryList GuiListbox* setButtonHeight(float height); GuiListbox* scrollTo(int index); + // Adds the text filter search field. Takes an optional callback that + // receives the lowercased search text and runs each time the field is + // edited. When no callback is provided, the listbox filters itself. + // Has no effect if called more than once. + GuiListbox* addSearch(search_func_t search_callback = nullptr); + // Clears the search field. For the built-in filter, also resets the + // visible list to all entries. + GuiListbox* clearSearch(); + + // Returns the current text in the search field, or "" if no search field exists. + string getSearchText() const; + + virtual int addEntry(string name, string value) override; + virtual void clear() override; + virtual void setEntryName(int index, string name) override; + virtual void setEntryValue(int index, string value) override; + virtual void setEntryIcon(int index, string icon_name) override; + virtual void setEntry(int index, string name, string value) override; + virtual void removeEntry(int index) override; virtual void onDraw(sp::RenderTarget& renderer) override; virtual bool onMouseDown(sp::io::Pointer::Button button, glm::vec2 position, sp::io::Pointer::ID id) override; virtual void onMouseUp(glm::vec2 position, sp::io::Pointer::ID id) override; virtual bool onMouseWheelScroll(glm::vec2 position, float value) override; +private: + // Default search filter callback. Builds an entry list from matches. + void applyFilter(); + + static constexpr float search_bar_height = 30.0f; }; diff --git a/src/screenComponents/databaseView.cpp b/src/screenComponents/databaseView.cpp index 9df8b9b689..b12f82aef2 100644 --- a/src/screenComponents/databaseView.cpp +++ b/src/screenComponents/databaseView.cpp @@ -42,10 +42,12 @@ DatabaseViewComponent::DatabaseViewComponent(GuiContainer* owner) [this](int index, string value) { selected_entry = sp::ecs::Entity::fromString(value); + item_list->clearSearch(); display(); } ); item_list->setSize(GuiElement::GuiSizeMax, GuiElement::GuiSizeMax); + item_list->addSearch([this](string) { fillListBox(); }); display(); } @@ -80,6 +82,29 @@ void DatabaseViewComponent::fillListBox() item_list->setOptions({}); item_list->setSelectionIndex(-1); + string search_text = item_list->getSearchText(); + if (!search_text.empty()) + { + back_button->hide(); + + std::vector> matches; + for (auto [entity, database] : sp::ecs::Query()) + if (database.name.lower().find(search_text) >= 0) + matches.push_back({entity, &database}); + + sort(matches.begin(), matches.end(), [](const auto& A, const auto& B) { + return A.second->name.lower() < B.second->name.lower(); + }); + + for (auto [entity, database] : matches) + { + int idx = item_list->addEntry(database->name, entity.toString()); + if (selected_entry && selected_entry.getComponent() == database) + item_list->setSelectionIndex(idx); + } + return; + } + // indices of child or sibling pages in the science_databases vector std::vector> children; std::vector> siblings; diff --git a/src/screens/gm/objectCreationView.cpp b/src/screens/gm/objectCreationView.cpp index c4aab747a0..689100ca79 100644 --- a/src/screens/gm/objectCreationView.cpp +++ b/src/screens/gm/objectCreationView.cpp @@ -66,37 +66,28 @@ GuiObjectCreationView::GuiObjectCreationView(GuiContainer* owner) { last_selection_index = -1; object_list->clear(); - object_filter->setText(""); - for(const auto& info : spawn_list) { - if (info.category == category_selector->getSelectionValue()) { + for (const auto& info : spawn_list) + { + if (info.category == category_selector->getSelectionValue()) + { object_list->addEntry(info.label, info.label); object_list->setEntryIcon(object_list->indexByValue(info.label), info.icon); } } }); std::unordered_set categories_added; - for(const auto& info : spawn_list) { - if (categories_added.find(info.category) == categories_added.end()) { + for (const auto& info : spawn_list) + { + if (categories_added.find(info.category) == categories_added.end()) + { categories_added.insert(info.category); category_selector->addEntry(info.category, info.category); } } - category_selector->setSelectionIndex(0); - category_selector->setAttribute("stretch", "true"); + category_selector + ->setSelectionIndex(0) + ->setAttribute("stretch", "true"); - object_filter = new GuiTextEntry(col2, "OBJECT_FILTER", ""); - object_filter->setTextSize(20)->setSize(GuiElement::GuiSizeMax, 30)->setAttribute("fill_width", "true"); - object_filter->callback([this](string value) { - value = value.lower(); - last_selection_index = -1; - object_list->clear(); - for(const auto& info : spawn_list) { - if (info.category == category_selector->getSelectionValue() && info.label.lower().find(value) >= 0) { - object_list->addEntry(info.label, info.label); - object_list->setEntryIcon(object_list->indexByValue(info.label), info.icon); - } - } - }); object_list = new GuiListbox(col2, "OBJECT_LIST", [this](int index, string value) { for(auto& info : spawn_list) { if (info.category == category_selector->getSelectionValue() && info.label == value) { @@ -153,14 +144,23 @@ GuiObjectCreationView::GuiObjectCreationView(GuiContainer* owner) } last_selection_index = index; }); - object_list->setTextSize(20)->setButtonHeight(30)->setAttribute("stretch", "true"); - for(const auto& info : spawn_list) { - if (info.category == category_selector->getSelectionValue()) { + object_list + ->addSearch() + ->setTextSize(20.0f) + ->setButtonHeight(30.0f) + ->setAttribute("stretch", "true"); + + // Handle category navigation. + for (const auto& info : spawn_list) + { + if (info.category == category_selector->getSelectionValue()) + { object_list->addEntry(info.label, info.label); object_list->setEntryIcon(object_list->indexByValue(info.label), info.icon); } } + // Display description. description = new GuiScrollText(col3, "DESCRIPTION", ""); description->setAttribute("stretch", "true"); diff --git a/src/screens/gm/objectCreationView.h b/src/screens/gm/objectCreationView.h index bc5207159d..f4de0f965b 100644 --- a/src/screens/gm/objectCreationView.h +++ b/src/screens/gm/objectCreationView.h @@ -8,14 +8,12 @@ class GuiSelector; class GuiListbox; class GuiContainer; class GuiScrollText; -class GuiTextEntry; class GuiObjectCreationView : public GuiOverlay { private: GuiSelector* faction_selector = nullptr; GuiListbox* category_selector = nullptr; - GuiTextEntry* object_filter = nullptr; GuiListbox* object_list = nullptr; GuiScrollText* description = nullptr; std::vector spawn_list;