diff --git a/ImguiMapEditor/Application.cpp b/ImguiMapEditor/Application.cpp index 4b14b97..b83ba12 100644 --- a/ImguiMapEditor/Application.cpp +++ b/ImguiMapEditor/Application.cpp @@ -173,7 +173,6 @@ void Application::wireCallbacks() { .edit_towns = &dialogs_.edit_towns, .map_properties = &dialogs_.map_properties, // Search components - .quick_search = ui_.search_controller->getQuickSearchPopup(), .advanced_search = ui_.search_controller->getAdvancedSearchDialog(), .search_results = ui_.search_controller->getSearchResultsWidget(), .search_controller = ui_.search_controller.get(), @@ -367,7 +366,6 @@ void Application::render() { .ribbon = ui_.ribbon_controller.get(), .file_panel = ui_.file_panel_ptr, .main_window = ui_.main_window.get(), - .quick_search_popup = ui_.search_controller->getQuickSearchPopup(), .advanced_search_dialog = ui_.search_controller->getAdvancedSearchDialog(), .search_results_widget = ui_.search_controller->getSearchResultsWidget(), diff --git a/ImguiMapEditor/Application/CallbackMediator.cpp b/ImguiMapEditor/Application/CallbackMediator.cpp index f9a067b..98ec87e 100644 --- a/ImguiMapEditor/Application/CallbackMediator.cpp +++ b/ImguiMapEditor/Application/CallbackMediator.cpp @@ -31,7 +31,6 @@ #include "UI/Map/MapPanel.h" #include "UI/PreferencesDialog.h" #include "UI/Ribbon/Panels/FilePanel.h" -#include "UI/Widgets/QuickSearchPopup.h" #include "UI/Widgets/SearchResultsWidget.h" #include "UI/Windows/BrowseTile/BrowseTileWindow.h" #include "UI/Windows/IngameBoxWindow.h" @@ -366,11 +365,8 @@ void CallbackMediator::wireMenuCallbacks(Context &ctx) { }); // Search menu callbacks - ctx.menu_bar->setQuickFindCallback( - [ctx]() { if (ctx.quick_search) ctx.quick_search->open(); }); - ctx.menu_bar->setFindItemsCallback( - [ctx]() { ctx.view_settings->show_search_results = true; }); + [ctx]() { if (ctx.advanced_search) ctx.advanced_search->open(); }); ctx.menu_bar->setFindUniqueCallback( [ctx]() { @@ -537,29 +533,16 @@ void CallbackMediator::wireCleanupCallbacks(Context &ctx) { } void CallbackMediator::wireSearchCallbacks(Context &ctx) { - // Wire Ctrl+F quick search callback - if (ctx.hotkey && ctx.quick_search) { + // Wire Ctrl+F to open Advanced Search dialog + if (ctx.hotkey && ctx.advanced_search) { ctx.hotkey->setQuickSearchCallback( - [qs = ctx.quick_search]() { qs->open(); }); + [as = ctx.advanced_search]() { as->open(); }); } - // Wire Ctrl+Shift+F to toggle Search Results Widget (not Advanced Search) + // Wire Ctrl+Shift+F to toggle Search Results Widget if (ctx.hotkey && ctx.view_settings) { ctx.hotkey->setAdvancedSearchCallback( - [vs = ctx.view_settings]() { vs->show_search_results = true; }); - } - - // Wire QuickSearch selection callback - if (ctx.quick_search) { - ctx.quick_search->setSelectCallback( - [](uint16_t server_id, bool is_creature) { - spdlog::info("QuickSearch selected: {} (ID: {})", - is_creature ? "creature" : "item", server_id); - Presentation::showInfo(std::string("Selected ") + - (is_creature ? "creature" : "item") + - " ID: " + std::to_string(server_id), - 2000); - }); + [vs = ctx.view_settings]() { vs->show_search_results = !vs->show_search_results; }); } // Wire SearchResultsWidget navigate callback diff --git a/ImguiMapEditor/Application/CallbackMediator.h b/ImguiMapEditor/Application/CallbackMediator.h index 919899b..f2497a1 100644 --- a/ImguiMapEditor/Application/CallbackMediator.h +++ b/ImguiMapEditor/Application/CallbackMediator.h @@ -25,7 +25,6 @@ class MapOperationHandler; class HotkeyController; class MapInputController; class EditorSession; -class ItemPickerService; class MapSearchService; class SearchController; } // namespace AppLogic @@ -64,7 +63,6 @@ class PreferencesDialog; class EditTownsDialog; class MapPropertiesDialog; class ConfirmationDialog; -class QuickSearchPopup; class AdvancedSearchDialog; class SearchResultsWidget; @@ -134,7 +132,6 @@ class CallbackMediator { UI::MapPropertiesDialog *map_properties = nullptr; // Search components - UI::QuickSearchPopup *quick_search = nullptr; UI::AdvancedSearchDialog *advanced_search = nullptr; UI::SearchResultsWidget *search_results = nullptr; AppLogic::SearchController *search_controller = nullptr; diff --git a/ImguiMapEditor/Application/Frame/RenderOrchestrator.cpp b/ImguiMapEditor/Application/Frame/RenderOrchestrator.cpp index a02f081..abdfb45 100644 --- a/ImguiMapEditor/Application/Frame/RenderOrchestrator.cpp +++ b/ImguiMapEditor/Application/Frame/RenderOrchestrator.cpp @@ -23,7 +23,6 @@ #include "UI/Panels/BrushSizePanel.h" #include "UI/Ribbon/Panels/FilePanel.h" #include "UI/Ribbon/RibbonController.h" -#include "UI/Widgets/QuickSearchPopup.h" #include "UI/Widgets/SearchResultsWidget.h" #include "UI/Widgets/TilesetWidget.h" #include "UI/Windows/BrowseTile/BrowseTileWindow.h" @@ -195,12 +194,7 @@ void RenderOrchestrator::renderDialogs(Context &ctx) { ctx.rendering_manager, // Pass rendering_manager instead of renderer &dialogs.cleanup_confirm}); - // Quick Search popup (Ctrl+F) - if (ctx.quick_search_popup) { - ctx.quick_search_popup->render(); - } - - // Advanced Search dialog (Ctrl+Shift+F) + // Advanced Search dialog (Ctrl+F) if (ctx.advanced_search_dialog) { ctx.advanced_search_dialog->render(); } diff --git a/ImguiMapEditor/Application/Frame/RenderOrchestrator.h b/ImguiMapEditor/Application/Frame/RenderOrchestrator.h index f93629f..94947be 100644 --- a/ImguiMapEditor/Application/Frame/RenderOrchestrator.h +++ b/ImguiMapEditor/Application/Frame/RenderOrchestrator.h @@ -33,7 +33,6 @@ class MapPanel; class MinimapWindow; class BrowseTileWindow; class TilesetWidget; -class QuickSearchPopup; class AdvancedSearchDialog; class SearchResultsWidget; class StartupDialog; @@ -99,7 +98,6 @@ class RenderOrchestrator { UI::Ribbon::RibbonController *ribbon = nullptr; UI::Ribbon::FilePanel *file_panel = nullptr; Presentation::MainWindow *main_window = nullptr; - UI::QuickSearchPopup *quick_search_popup = nullptr; UI::AdvancedSearchDialog *advanced_search_dialog = nullptr; UI::SearchResultsWidget *search_results_widget = nullptr; UI::TilesetWidget *tileset_widget = nullptr; diff --git a/ImguiMapEditor/CMakeLists.txt b/ImguiMapEditor/CMakeLists.txt index d80f1e4..2ab2da9 100644 --- a/ImguiMapEditor/CMakeLists.txt +++ b/ImguiMapEditor/CMakeLists.txt @@ -243,7 +243,6 @@ set(SERVICES_SOURCES Services/Map/MapCleanupService.cpp Services/Map/MapSearchService.cpp Services/ClipboardService.cpp - Services/ItemPickerService.cpp Services/MapMergeService.cpp Services/SessionWiringService.cpp Services/SettingsRegistry.cpp @@ -296,7 +295,6 @@ set(UI_SOURCES UI/Ribbon/RibbonController.cpp UI/Widgets/Properties/PropertyPanelRenderer.cpp UI/Widgets/Properties/PropertyWidgets.cpp - UI/Widgets/QuickSearchPopup.cpp UI/Widgets/SearchResultsWidget.cpp UI/Widgets/TilesetWidget.cpp UI/Widgets/TilesetGridWidget.cpp diff --git a/ImguiMapEditor/Controllers/SearchController.cpp b/ImguiMapEditor/Controllers/SearchController.cpp index 5f4e2dd..e62b5bc 100644 --- a/ImguiMapEditor/Controllers/SearchController.cpp +++ b/ImguiMapEditor/Controllers/SearchController.cpp @@ -1,7 +1,6 @@ #include "Controllers/SearchController.h" #include "Services/ClientDataService.h" #include "Services/SpriteManager.h" -#include "UI/Widgets/QuickSearchPopup.h" #include "UI/Dialogs/AdvancedSearchDialog.h" #include "UI/Widgets/SearchResultsWidget.h" #include "Domain/ChunkedMap.h" @@ -24,13 +23,11 @@ static void sortResultsByName(std::vector& resu } SearchController::SearchController() - : quick_search_popup_(std::make_unique()), - advanced_search_dialog_(std::make_unique()), + : advanced_search_dialog_(std::make_unique()), search_results_widget_(std::make_unique()) {} SearchController::~SearchController() = default; -UI::QuickSearchPopup* SearchController::getQuickSearchPopup() const { return quick_search_popup_.get(); } UI::AdvancedSearchDialog* SearchController::getAdvancedSearchDialog() const { return advanced_search_dialog_.get(); } UI::SearchResultsWidget* SearchController::getSearchResultsWidget() const { return search_results_widget_.get(); } @@ -61,12 +58,6 @@ void SearchController::onMapLoaded( if (!client_data) return; - if (!item_picker_service_ || current_client_data_ != client_data) { - item_picker_service_ = std::make_unique(client_data); - quick_search_popup_->setItemPickerService(item_picker_service_.get()); - advanced_search_dialog_->setItemPickerService(item_picker_service_.get()); - } - if (!map_search_service_) { map_search_service_ = std::make_unique(); search_results_widget_->setMapSearchService(map_search_service_.get()); @@ -80,9 +71,6 @@ void SearchController::onMapLoaded( search_results_widget_->setClientData(client_data); search_results_widget_->setSpriteManager(sprite_manager); - quick_search_popup_->setSpriteManager(sprite_manager); - quick_search_popup_->setClientDataService(client_data); - advanced_search_dialog_->setClientDataService(client_data); advanced_search_dialog_->setSpriteManager(sprite_manager); diff --git a/ImguiMapEditor/Controllers/SearchController.h b/ImguiMapEditor/Controllers/SearchController.h index 56624db..ffd0cbe 100644 --- a/ImguiMapEditor/Controllers/SearchController.h +++ b/ImguiMapEditor/Controllers/SearchController.h @@ -1,5 +1,4 @@ #pragma once -#include "Services/ItemPickerService.h" #include "Services/Map/MapSearchService.h" #include "Services/ViewSettings.h" #include @@ -14,7 +13,6 @@ namespace Search { struct MapSearchResult; } } namespace UI { -class QuickSearchPopup; class AdvancedSearchDialog; class SearchResultsWidget; } @@ -88,7 +86,6 @@ class SearchController { void forgetSessionMap(const Domain::ChunkedMap* map); // Accessors for UI components (needed for rendering and callbacks) - UI::QuickSearchPopup* getQuickSearchPopup() const; UI::AdvancedSearchDialog* getAdvancedSearchDialog() const; UI::SearchResultsWidget* getSearchResultsWidget() const; @@ -96,12 +93,10 @@ class SearchController { template void launchAsync(F&& searchFn); // UI Components (unique_ptr to allow forward declarations in header) - std::unique_ptr quick_search_popup_; std::unique_ptr advanced_search_dialog_; std::unique_ptr search_results_widget_; // Services - std::unique_ptr item_picker_service_; std::unique_ptr map_search_service_; // State tracking diff --git a/ImguiMapEditor/Presentation/MenuBar.cpp b/ImguiMapEditor/Presentation/MenuBar.cpp index a959d1a..736583c 100644 --- a/ImguiMapEditor/Presentation/MenuBar.cpp +++ b/ImguiMapEditor/Presentation/MenuBar.cpp @@ -224,19 +224,12 @@ void MenuBar::renderSearchMenu() { if (ImGui::BeginMenu("Search")) { bool has_session = tab_manager_ && tab_manager_->getActiveSession(); - if (ImGui::MenuItem(ICON_FA_MAGNIFYING_GLASS " Quick Find", formatShortcut(QUICK_SEARCH).c_str())) { - if (on_quick_find_) - on_quick_find_(); - } - if (ImGui::IsItemHovered()) - ImGui::SetTooltip("Quick catalog search for items and creatures"); - - if (ImGui::MenuItem(ICON_FA_MAGNIFYING_GLASS_PLUS " Find Items...", formatShortcut(ADVANCED_SEARCH).c_str())) { + if (ImGui::MenuItem(ICON_FA_MAGNIFYING_GLASS_PLUS " Find Items...", formatShortcut(QUICK_SEARCH).c_str())) { if (on_find_items_) on_find_items_(); } if (ImGui::IsItemHovered()) - ImGui::SetTooltip("Advanced search with type and property filters"); + ImGui::SetTooltip("Search for items and creatures with filters"); ImGui::Separator(); diff --git a/ImguiMapEditor/Presentation/MenuBar.h b/ImguiMapEditor/Presentation/MenuBar.h index f5a7ec5..9214582 100644 --- a/ImguiMapEditor/Presentation/MenuBar.h +++ b/ImguiMapEditor/Presentation/MenuBar.h @@ -72,7 +72,6 @@ class MenuBar { void setConvertToClientIdCallback(ActionCallback cb) { on_convert_to_client_id_ = std::move(cb); } // Search menu callbacks - void setQuickFindCallback(ActionCallback cb) { on_quick_find_ = std::move(cb); } void setFindItemsCallback(ActionCallback cb) { on_find_items_ = std::move(cb); } void setFindUniqueCallback(ActionCallback cb) { on_find_unique_ = std::move(cb); } void setFindActionCallback(ActionCallback cb) { on_find_action_ = std::move(cb); } @@ -122,7 +121,6 @@ class MenuBar { ActionCallback on_convert_to_server_id_; ActionCallback on_convert_to_client_id_; - ActionCallback on_quick_find_; ActionCallback on_find_items_; ActionCallback on_find_unique_; ActionCallback on_find_action_; diff --git a/ImguiMapEditor/Services/ItemPickerService.cpp b/ImguiMapEditor/Services/ItemPickerService.cpp deleted file mode 100644 index 1002b45..0000000 --- a/ImguiMapEditor/Services/ItemPickerService.cpp +++ /dev/null @@ -1,114 +0,0 @@ -#include "ItemPickerService.h" -#include "Services/ClientDataService.h" -#include "Domain/ItemType.h" -#include -#include - -namespace MapEditor::AppLogic { - -ItemPickerService::ItemPickerService(Services::ClientDataService* client_data) - : client_data_(client_data) { -} - -std::vector ItemPickerService::search( - const std::string& query, size_t limit) const { - - std::vector results; - if (!client_data_ || query.empty()) { - return results; - } - - std::string query_lower = toLower(query); - - // Check for "cid:" prefix (client ID search) - bool is_client_id_search = query_lower.substr(0, 4) == "cid:"; - uint16_t search_id = 0; - bool is_id_search = false; - - if (is_client_id_search && query.length() > 4) { - try { - search_id = static_cast(std::stoi(query.substr(4))); - is_id_search = true; - } catch (...) { } - } else { - // Check if query is a number (server ID search) - bool all_digits = !query.empty() && - std::all_of(query.begin(), query.end(), ::isdigit); - if (all_digits) { - try { - search_id = static_cast(std::stoi(query)); - is_id_search = true; - } catch (...) { } - } - } - - // Search items - const auto& items = client_data_->getItemTypes(); - for (const auto& item : items) { - if (results.size() >= limit) break; - if (item.server_id == 0) continue; - - bool match = false; - - if (is_id_search) { - if (is_client_id_search) { - match = (item.client_id == search_id); - } else { - match = (item.server_id == search_id); - } - } else { - // Fuzzy name search - match = matchesFuzzy(item.name, query_lower); - } - - if (match) { - Domain::Search::PickResult result; - result.server_id = item.server_id; - result.name = item.name.empty() - ? "Item " + std::to_string(item.server_id) - : item.name; - result.is_creature = false; - results.push_back(result); - } - } - - // Search creatures by name - if (!is_id_search) { // Creatures don't have server/client IDs - const auto& creature_map = client_data_->getCreatureMap(); - for (const auto& [name, creature] : creature_map) { - if (results.size() >= limit) break; - if (!creature) continue; - - if (matchesFuzzy(name, query_lower)) { - Domain::Search::PickResult result; - result.server_id = 0; // Creatures don't have server ID - result.name = creature->name; - result.is_creature = true; - results.push_back(result); - } - } - } - - // Sort by name for consistent display - std::sort(results.begin(), results.end()); - - return results; -} - -bool ItemPickerService::matchesFuzzy(const std::string& text, - const std::string& query) const { - std::string text_lower = toLower(text); - - // Simple substring match for now - // Can be upgraded to fuzzy matching algorithm later - return text_lower.find(query) != std::string::npos; -} - -std::string ItemPickerService::toLower(const std::string& str) const { - std::string result = str; - std::transform(result.begin(), result.end(), result.begin(), - [](unsigned char c) { return std::tolower(c); }); - return result; -} - -} // namespace MapEditor::AppLogic diff --git a/ImguiMapEditor/Services/ItemPickerService.h b/ImguiMapEditor/Services/ItemPickerService.h deleted file mode 100644 index 9f458a5..0000000 --- a/ImguiMapEditor/Services/ItemPickerService.h +++ /dev/null @@ -1,47 +0,0 @@ -#pragma once -#include "Domain/Search/ISearchProvider.h" -#include -#include - -namespace MapEditor { -namespace Services { class ClientDataService; } - -namespace AppLogic { - -/** - * Service for searching items/creatures available to place. - * Used by QuickSearch (Ctrl+F) to find items in the catalog. - */ -class ItemPickerService { -public: - explicit ItemPickerService(Services::ClientDataService* client_data); - ~ItemPickerService() = default; - - // Non-copyable - ItemPickerService(const ItemPickerService&) = delete; - ItemPickerService& operator=(const ItemPickerService&) = delete; - - /** - * Search items and creatures by query. - * Supports: - * - Name search (fuzzy): "dragon" - * - Server ID exact: "2492" or pure number - * - Client ID: "cid:3031" - * - * @param query Search string - * @param limit Max results - * @return Matching items/creatures - */ - std::vector search( - const std::string& query, - size_t limit = 50) const; - -private: - bool matchesFuzzy(const std::string& text, const std::string& query) const; - std::string toLower(const std::string& str) const; - - Services::ClientDataService* client_data_; // Non-owning -}; - -} // namespace AppLogic -} // namespace MapEditor diff --git a/ImguiMapEditor/UI/Dialogs/AdvancedSearchDialog.cpp b/ImguiMapEditor/UI/Dialogs/AdvancedSearchDialog.cpp index 8fdd01b..9fc6687 100644 --- a/ImguiMapEditor/UI/Dialogs/AdvancedSearchDialog.cpp +++ b/ImguiMapEditor/UI/Dialogs/AdvancedSearchDialog.cpp @@ -12,17 +12,15 @@ #include "UI/Utils/PreviewUtils.hpp" #include #include +#include namespace MapEditor::UI { -// PreviewResult helpers +static inline ImTextureID TexId(uint32_t id) { return (ImTextureID)(intptr_t)id; } + std::string PreviewResult::getDisplayName() const { - if (is_creature && creature) { - return creature->name; - } - if (item) { - return item->name.empty() ? "(unnamed)" : item->name; - } + if (is_creature && creature) return creature->name; + if (item) return item->name.empty() ? "(unnamed)" : item->name; return "(unknown)"; } @@ -37,445 +35,446 @@ AdvancedSearchDialog::AdvancedSearchDialog() { void AdvancedSearchDialog::render() { if (!is_open_) return; - - ImGui::SetNextWindowSize(ImVec2(800, 550), ImGuiCond_FirstUseEver); - - if (ImGui::Begin(ICON_FA_MAGNIFYING_GLASS_PLUS " Advanced Search###AdvancedSearch", &is_open_)) { - - // === 4 COLUMNS === - if (ImGui::BeginTable("SearchColumns", 4, ImGuiTableFlags_Resizable | ImGuiTableFlags_BordersInner)) { - ImGui::TableSetupColumn("FindBy", ImGuiTableColumnFlags_WidthStretch, 0.9f); - ImGui::TableSetupColumn("Types", ImGuiTableColumnFlags_WidthStretch, 0.9f); - ImGui::TableSetupColumn("Properties", ImGuiTableColumnFlags_WidthStretch, 1.0f); - ImGui::TableSetupColumn("Results", ImGuiTableColumnFlags_WidthStretch, 1.2f); - - ImGui::TableNextRow(); - - // Column 1: Find By - ImGui::TableSetColumnIndex(0); - renderFindByColumn(); - - // Column 2: Types - ImGui::TableSetColumnIndex(1); - renderTypesColumn(); - - // Column 3: Properties - ImGui::TableSetColumnIndex(2); - renderPropertiesColumn(); - - // Column 4: Results - ImGui::TableSetColumnIndex(3); - renderResultsColumn(); - - ImGui::EndTable(); - } - - ImGui::Separator(); - - // Bottom bar with buttons + + // Execute deferred map search from previous frame + if (pending_map_search_) { + pending_map_search_ = false; + executeMapSearch(); + } + + ImGui::SetNextWindowSize(ImVec2(850, 600), ImGuiCond_Once); + + if (ImGui::Begin(ICON_FA_MAGNIFYING_GLASS_PLUS " Advanced Search###AdvancedSearch", &is_open_, + ImGuiWindowFlags_NoCollapse)) { + + const float spacing = ImGui::GetStyle().ItemSpacing.x; + const float footer_pad = ImGui::GetStyle().ItemSpacing.y * 5.0f; + const float footer_h = ImGui::GetFrameHeightWithSpacing() + footer_pad; + const float content_h = ImGui::GetContentRegionAvail().y - footer_h; + const float filters_w = ImGui::GetContentRegionAvail().x * 0.35f; + const float results_w = ImGui::GetContentRegionAvail().x - filters_w - spacing; + + // === LEFT PANEL: Filters === + ImGui::BeginChild("##filters_panel", ImVec2(filters_w, content_h), ImGuiChildFlags_Borders); + renderFiltersPanel(); + ImGui::EndChild(); + + ImGui::SameLine(0.0f, spacing); + + // === RIGHT PANEL: Results === + ImGui::BeginChild("##results_panel", ImVec2(results_w, content_h), ImGuiChildFlags_Borders); + renderResultsColumn(); + ImGui::EndChild(); + + // === FOOTER === always visible, bordered renderBottomBar(); } ImGui::End(); - - // Close on Escape + if (is_open_ && ImGui::IsKeyPressed(ImGuiKey_Escape)) { close(); } } -void AdvancedSearchDialog::renderFindByColumn() { - ImGui::BeginChild("FindBy", ImVec2(0, -40), true); - - ImGui::Text(ICON_FA_MAGNIFYING_GLASS " Find By"); - ImGui::Separator(); +void AdvancedSearchDialog::renderFiltersPanel() { + renderSearchSection(); + ImGui::Spacing(); + renderOrSection(); ImGui::Spacing(); - - // Search input + renderAndSection(); + ImGui::Spacing(); + renderHintsSection(); +} + +void AdvancedSearchDialog::renderSearchSection() { + ImGui::SeparatorText(ICON_FA_MAGNIFYING_GLASS " Search"); + if (focus_input_) { ImGui::SetKeyboardFocusHere(); focus_input_ = false; } - + ImGui::PushItemWidth(-1); - if (ImGui::InputTextWithHint("##SearchInput", "Name or ID...", + if (ImGui::InputTextWithHint("##SearchInput", "Name or ID...", search_buffer_, sizeof(search_buffer_))) { filters_changed_ = true; } ImGui::PopItemWidth(); - - ImGui::Spacing(); - ImGui::TextWrapped(ICON_FA_CIRCLE_INFO " Searches by:"); - ImGui::BulletText("Name (fuzzy)"); - ImGui::BulletText("Server ID"); - ImGui::BulletText("Client ID"); - - ImGui::Spacing(); - ImGui::TextDisabled("Leave empty to search\nby Types/Properties only"); - - ImGui::EndChild(); } -void AdvancedSearchDialog::renderTypesColumn() { - ImGui::BeginChild("Types", ImVec2(0, -40), true); - - ImGui::Text(ICON_FA_CUBES " Types"); - ImGui::Separator(); - ImGui::Spacing(); - - ImGui::TextDisabled("(OR logic)"); - ImGui::Spacing(); - - if (ImGui::Checkbox("Depot", &type_filter_.depot)) filters_changed_ = true; - if (ImGui::Checkbox("Mailbox", &type_filter_.mailbox)) filters_changed_ = true; - if (ImGui::Checkbox("Trash Holder", &type_filter_.trash_holder)) filters_changed_ = true; - if (ImGui::Checkbox("Container", &type_filter_.container)) filters_changed_ = true; - if (ImGui::Checkbox("Door", &type_filter_.door)) filters_changed_ = true; - if (ImGui::Checkbox("Magic Field", &type_filter_.magic_field)) filters_changed_ = true; - if (ImGui::Checkbox("Teleport", &type_filter_.teleport)) filters_changed_ = true; - if (ImGui::Checkbox("Bed", &type_filter_.bed)) filters_changed_ = true; - if (ImGui::Checkbox("Key", &type_filter_.key)) filters_changed_ = true; - if (ImGui::Checkbox("Podium", &type_filter_.podium)) filters_changed_ = true; - - ImGui::Separator(); - ImGui::TextDisabled("Combat"); - if (ImGui::Checkbox("Weapon", &type_filter_.weapon)) filters_changed_ = true; - if (ImGui::Checkbox("Ammo", &type_filter_.ammo)) filters_changed_ = true; - if (ImGui::Checkbox("Armor", &type_filter_.armor)) filters_changed_ = true; - if (ImGui::Checkbox("Rune", &type_filter_.rune)) filters_changed_ = true; - - ImGui::Separator(); - if (ImGui::Checkbox("Creature", &type_filter_.creature)) filters_changed_ = true; - - ImGui::EndChild(); +void AdvancedSearchDialog::renderOrSection() { + ImGui::SeparatorText(ICON_FA_CUBES " OR (any match)"); + + if (ImGui::BeginTable("OrColumns", 2, ImGuiTableFlags_SizingStretchSame)) { + ImGui::TableNextRow(); + + ImGui::TableSetColumnIndex(0); + ImGui::PushID("OrLeft"); + if (ImGui::Checkbox("Depot", &type_filter_.depot)) filters_changed_ = true; + if (ImGui::Checkbox("Trash Holder", &type_filter_.trash_holder)) filters_changed_ = true; + if (ImGui::Checkbox("Door", &type_filter_.door)) filters_changed_ = true; + if (ImGui::Checkbox("Teleport", &type_filter_.teleport)) filters_changed_ = true; + if (ImGui::Checkbox("Key", &type_filter_.key)) filters_changed_ = true; + if (ImGui::Checkbox("Weapon", &type_filter_.weapon)) filters_changed_ = true; + if (ImGui::Checkbox("Armor", &type_filter_.armor)) filters_changed_ = true; + if (ImGui::Checkbox("Creature", &type_filter_.creature)) filters_changed_ = true; + ImGui::PopID(); + + ImGui::TableSetColumnIndex(1); + ImGui::PushID("OrRight"); + if (ImGui::Checkbox("Mailbox", &type_filter_.mailbox)) filters_changed_ = true; + if (ImGui::Checkbox("Container", &type_filter_.container)) filters_changed_ = true; + if (ImGui::Checkbox("Magic Field", &type_filter_.magic_field)) filters_changed_ = true; + if (ImGui::Checkbox("Bed", &type_filter_.bed)) filters_changed_ = true; + if (ImGui::Checkbox("Podium", &type_filter_.podium)) filters_changed_ = true; + if (ImGui::Checkbox("Ammo", &type_filter_.ammo)) filters_changed_ = true; + if (ImGui::Checkbox("Rune", &type_filter_.rune)) filters_changed_ = true; + ImGui::PopID(); + + ImGui::EndTable(); + } } -void AdvancedSearchDialog::renderPropertiesColumn() { - ImGui::BeginChild("Properties", ImVec2(0, -40), true); - - ImGui::Text(ICON_FA_SLIDERS " Properties"); - ImGui::Separator(); - ImGui::Spacing(); - - ImGui::TextDisabled("(AND logic)"); - ImGui::Spacing(); - - if (ImGui::Checkbox("Unpassable", &property_filter_.unpassable)) filters_changed_ = true; - if (ImGui::Checkbox("Unmovable", &property_filter_.unmovable)) filters_changed_ = true; - if (ImGui::Checkbox("Block Missiles", &property_filter_.block_missiles)) filters_changed_ = true; - if (ImGui::Checkbox("Block Pathfinder", &property_filter_.block_pathfinder)) filters_changed_ = true; - if (ImGui::Checkbox("Has Elevation", &property_filter_.has_elevation)) filters_changed_ = true; - if (ImGui::Checkbox("Floor Change", &property_filter_.floor_change)) filters_changed_ = true; - if (ImGui::Checkbox("Full Tile", &property_filter_.full_tile)) filters_changed_ = true; - - ImGui::Separator(); - ImGui::TextDisabled("Interaction"); - if (ImGui::Checkbox("Readable", &property_filter_.readable)) filters_changed_ = true; - if (ImGui::Checkbox("Writeable", &property_filter_.writeable)) filters_changed_ = true; - if (ImGui::Checkbox("Pickupable", &property_filter_.pickupable)) filters_changed_ = true; - if (ImGui::Checkbox("Force Use", &property_filter_.force_use)) filters_changed_ = true; - if (ImGui::Checkbox("Dist Read", &property_filter_.allow_dist_read)) filters_changed_ = true; - if (ImGui::Checkbox("Rotatable", &property_filter_.rotatable)) filters_changed_ = true; - if (ImGui::Checkbox("Hangable", &property_filter_.hangable)) filters_changed_ = true; - - ImGui::Separator(); - ImGui::TextDisabled("Visuals/Misc"); - if (ImGui::Checkbox("Has Light", &property_filter_.has_light)) filters_changed_ = true; - if (ImGui::Checkbox("Animation", &property_filter_.animation)) filters_changed_ = true; - if (ImGui::Checkbox("Always Top", &property_filter_.always_on_top)) filters_changed_ = true; - if (ImGui::Checkbox("Ignore Look", &property_filter_.ignore_look)) filters_changed_ = true; - if (ImGui::Checkbox("Has Charges", &property_filter_.has_charges)) filters_changed_ = true; - if (ImGui::Checkbox("Client Charges", &property_filter_.client_charges)) filters_changed_ = true; - if (ImGui::Checkbox("Decays", &property_filter_.decays)) filters_changed_ = true; - if (ImGui::Checkbox("Has Speed", &property_filter_.has_speed)) filters_changed_ = true; - - ImGui::EndChild(); +void AdvancedSearchDialog::renderAndSection() { + ImGui::SeparatorText(ICON_FA_SLIDERS " AND (all must match)"); + + if (ImGui::BeginTable("AndColumns", 2, ImGuiTableFlags_SizingStretchSame)) { + ImGui::TableNextRow(); + + ImGui::TableSetColumnIndex(0); + ImGui::PushID("AndLeft"); + if (ImGui::Checkbox("Unpassable", &property_filter_.unpassable)) filters_changed_ = true; + if (ImGui::Checkbox("Block Missiles", &property_filter_.block_missiles)) filters_changed_ = true; + if (ImGui::Checkbox("Has Elevation", &property_filter_.has_elevation)) filters_changed_ = true; + if (ImGui::Checkbox("Full Tile", &property_filter_.full_tile)) filters_changed_ = true; + if (ImGui::Checkbox("Writeable", &property_filter_.writeable)) filters_changed_ = true; + if (ImGui::Checkbox("Force Use", &property_filter_.force_use)) filters_changed_ = true; + if (ImGui::Checkbox("Rotatable", &property_filter_.rotatable)) filters_changed_ = true; + if (ImGui::Checkbox("Has Light", &property_filter_.has_light)) filters_changed_ = true; + if (ImGui::Checkbox("Always Top", &property_filter_.always_on_top)) filters_changed_ = true; + if (ImGui::Checkbox("Has Charges", &property_filter_.has_charges)) filters_changed_ = true; + if (ImGui::Checkbox("Decays", &property_filter_.decays)) filters_changed_ = true; + if (ImGui::Checkbox("Dist Read", &property_filter_.allow_dist_read)) filters_changed_ = true; + if (ImGui::Checkbox("Hangable", &property_filter_.hangable)) filters_changed_ = true; + if (ImGui::Checkbox("Stackable", &property_filter_.stackable)) filters_changed_ = true; + ImGui::PopID(); + + ImGui::TableSetColumnIndex(1); + ImGui::PushID("AndRight"); + if (ImGui::Checkbox("Unmovable", &property_filter_.unmovable)) filters_changed_ = true; + if (ImGui::Checkbox("Block Pathfinder", &property_filter_.block_pathfinder)) filters_changed_ = true; + if (ImGui::Checkbox("Floor Change", &property_filter_.floor_change)) filters_changed_ = true; + if (ImGui::Checkbox("Readable", &property_filter_.readable)) filters_changed_ = true; + if (ImGui::Checkbox("Pickupable", &property_filter_.pickupable)) filters_changed_ = true; + if (ImGui::Checkbox("Animation", &property_filter_.animation)) filters_changed_ = true; + if (ImGui::Checkbox("Ignore Look", &property_filter_.ignore_look)) filters_changed_ = true; + if (ImGui::Checkbox("Client Charges", &property_filter_.client_charges)) filters_changed_ = true; + if (ImGui::Checkbox("Has Speed", &property_filter_.has_speed)) filters_changed_ = true; + if (ImGui::Checkbox("Hook East", &property_filter_.hook_east)) filters_changed_ = true; + if (ImGui::Checkbox("Hook South", &property_filter_.hook_south)) filters_changed_ = true; + ImGui::PopID(); + + ImGui::EndTable(); + } +} + +void AdvancedSearchDialog::renderHintsSection() { + ImGui::SeparatorText(ICON_FA_CIRCLE_INFO " Hints"); + ImGui::TextDisabled("Double-click result to search map."); + ImGui::TextDisabled("Leave search empty to filter by types/properties only."); } void AdvancedSearchDialog::renderResultsColumn() { - // Update preview if filters changed if (filters_changed_) { updatePreviewResults(); filters_changed_ = false; } - - ImGui::BeginChild("Results", ImVec2(0, -40), true); - - ImGui::Text(ICON_FA_LIST " Result (%zu)", preview_results_.size()); - ImGui::Separator(); - ImGui::Spacing(); - + + auto result_label = std::format("Result ({})", preview_results_.size()); + ImGui::SeparatorText(result_label.c_str()); + + // State-based empty messages if (preview_results_.empty()) { - ImGui::TextDisabled("No matching items"); - ImGui::TextDisabled("Enter search term or"); - ImGui::TextDisabled("select filters"); - } else { - ImGui::TextDisabled("Double-click to search map"); - ImGui::Spacing(); - - // Scrollable list of results - ImGui::BeginChild("ResultsList", ImVec2(0, 0), false); - - constexpr float SPRITE_SIZE = 24.0f; - constexpr float ROW_HEIGHT = 28.0f; - - for (size_t i = 0; i < preview_results_.size(); ++i) { - const auto& result = preview_results_[i]; - bool is_selected = (static_cast(i) == selected_preview_index_); - - ImGui::PushID(static_cast(i)); - - // Selectable row with fixed height for sprite - if (ImGui::Selectable("##Row", is_selected, ImGuiSelectableFlags_AllowDoubleClick, - ImVec2(0, ROW_HEIGHT))) { - selected_preview_index_ = static_cast(i); - - // Double-click = auto search map - if (ImGui::IsMouseDoubleClicked(0)) { - onSearchMap(); - } - } - - // Draw content on same line (sprite + text) - ImGui::SameLine(4); - - // Sprite thumbnail - bool sprite_rendered = false; - if (sprite_manager_ && client_data_) { - if (result.is_creature && result.creature) { - // Creature sprite - auto preview = Utils::GetCreaturePreview(*client_data_, *sprite_manager_, result.creature->outfit); - if (preview && preview.texture) { - // AdvancedSearchDialog forces SPRITE_SIZE (24.0f) for list alignment, ignoring recommended size - ImGui::Image((void*)(intptr_t)preview.texture->id(), ImVec2(SPRITE_SIZE, SPRITE_SIZE)); - sprite_rendered = true; - } - } else if (result.item) { - // Item sprite - if (auto* texture = Utils::GetItemPreview(*sprite_manager_, result.item)) { - ImGui::Image((void*)(intptr_t)texture->id(), ImVec2(SPRITE_SIZE, SPRITE_SIZE)); - sprite_rendered = true; + ImVec2 wsize = ImGui::GetContentRegionAvail(); + bool has_query = strlen(search_buffer_) > 0; + bool has_filters = type_filter_.hasAnySelected() || property_filter_.hasAnySelected(); + + const char* icon; + const char* msg; + if (!has_query && !has_filters) { + icon = ICON_FA_KEYBOARD; + msg = "Type to search or select filters"; + } else { + icon = ICON_FA_CIRCLE_EXCLAMATION; + msg = "No matching items"; + } + + std::string text = std::format("{} {}", icon, msg); + ImVec2 tsize = ImGui::CalcTextSize(text.c_str()); + ImGui::SetCursorPos(ImVec2( + (ImGui::GetContentRegionAvail().x - tsize.x) * 0.5f, + wsize.y * 0.4f)); + ImGui::TextDisabled("%s", text.c_str()); + return; + } + + ImGui::TextDisabled("Hover for details. Double-click to search map."); + ImGui::Spacing(); + + constexpr float CELL_SIZE = 32.0f; + constexpr float PREVIEW_SIZE = 64.0f; + constexpr float CARD_ROUNDING = 4.0f; + constexpr float IMG_ROUNDING = 3.0f; + constexpr float IMG_PADDING = 2.0f; + const float pad = ImGui::GetStyle().WindowPadding.x; + const float avail_w = ImGui::GetContentRegionAvail().x; + const int columns = std::max(1, static_cast((avail_w - pad) / CELL_SIZE)); + const int total_rows = (static_cast(preview_results_.size()) + columns - 1) / columns; + + ImDrawList* dl = ImGui::GetWindowDrawList(); + const ImU32 col_bg = ImGui::GetColorU32(ImGuiCol_FrameBg); + const ImU32 col_border = ImGui::GetColorU32(ImGuiCol_Border); + const ImU32 col_selected = ImGui::GetColorU32(ImGuiCol_Header); + const ImU32 col_hovered = ImGui::GetColorU32(ImGuiCol_HeaderHovered); + + ImGuiListClipper clipper; + clipper.Begin(total_rows, CELL_SIZE); + while (clipper.Step()) { + for (int row = clipper.DisplayStart; row < clipper.DisplayEnd; ++row) { + for (int col = 0; col < columns; ++col) { + int idx = row * columns + col; + if (idx >= static_cast(preview_results_.size())) break; + + const auto& result = preview_results_[idx]; + bool is_selected = (idx == selected_preview_index_); + + ImGui::PushID(idx); + + // Load sprite texture + uint32_t tex_id = 0; + bool sprite_rendered = false; + if (sprite_manager_ && client_data_) { + if (result.is_creature && result.creature) { + auto preview = Utils::GetCreaturePreview(*client_data_, *sprite_manager_, result.creature->outfit); + if (preview && preview.texture) { + tex_id = preview.texture->id(); + sprite_rendered = true; + } + } else if (result.item) { + if (auto* texture = Utils::GetItemPreview(*sprite_manager_, result.item)) { + tex_id = texture->id(); + sprite_rendered = true; + } } } - } - - // Fallback icon if no sprite - if (!sprite_rendered) { - ImGui::Dummy(ImVec2(SPRITE_SIZE, SPRITE_SIZE)); - ImGui::SameLine(4); - if (result.is_creature) { - ImGui::Text(ICON_FA_DRAGON); - } else { - ImGui::Text(ICON_FA_CUBE); + + // Exact 32x32 hitbox + ImGui::InvisibleButton("##cell", ImVec2(CELL_SIZE, CELL_SIZE)); + bool hovered = ImGui::IsItemHovered(); + bool clicked = ImGui::IsItemClicked(); + bool double_clicked = hovered && ImGui::IsMouseDoubleClicked(0); + + if (clicked) selected_preview_index_ = idx; + if (double_clicked) onSearchMap(); + + ImVec2 min = ImGui::GetItemRectMin(); + ImVec2 max = ImGui::GetItemRectMax(); + + // Rounded card background + ImU32 bg_col = is_selected ? col_selected : (hovered ? col_hovered : col_bg); + dl->AddRectFilled(min, max, bg_col, CARD_ROUNDING); + dl->AddRect(min, max, col_border, CARD_ROUNDING); + + // Rounded image inside card + if (sprite_rendered) { + ImVec2 img_min(min.x + IMG_PADDING, min.y + IMG_PADDING); + ImVec2 img_max(max.x - IMG_PADDING, max.y - IMG_PADDING); + dl->AddImageRounded(TexId(tex_id), img_min, img_max, + ImVec2(0, 0), ImVec2(1, 1), IM_COL32_WHITE, IMG_ROUNDING); } - } - - ImGui::SameLine(); - - // Text label - if (result.is_creature) { - ImGui::Text("%s", result.getDisplayName().c_str()); - } else { - ImGui::Text("[%u] %s", result.getServerId(), result.getDisplayName().c_str()); - } - - // Tooltip with more details - if (ImGui::IsItemHovered()) { - ImGui::BeginTooltip(); - if (result.is_creature && result.creature) { - ImGui::Text("Creature: %s", result.creature->name.c_str()); - ImGui::Text("LookType: %u", result.creature->outfit.lookType); - } else if (result.item) { - ImGui::Text("Server ID: %u", result.item->server_id); - ImGui::Text("Client ID: %u", result.item->client_id); - if (!result.item->name.empty()) { - ImGui::Text("Name: %s", result.item->name.c_str()); + + // Tooltip on hover + if (hovered) { + ImGui::BeginTooltip(); + if (sprite_rendered) { + ImGui::Image(TexId(tex_id), ImVec2(PREVIEW_SIZE, PREVIEW_SIZE)); + ImGui::SameLine(); + } + if (result.is_creature && result.creature) { + ImGui::TextUnformatted(result.creature->name.c_str()); + ImGui::TextDisabled("Creature"); + } else if (result.item) { + ImGui::TextUnformatted(result.item->name.empty() ? "(unnamed)" : result.item->name.c_str()); + ImGui::TextDisabled("SID: %u", result.item->server_id); + ImGui::TextDisabled("CID: %u", result.item->client_id); } + ImGui::EndTooltip(); } - ImGui::EndTooltip(); - } - // Right-align the copy button - ImGui::SameLine(); - ImGui::SetCursorPosX(ImGui::GetCursorPosX() + ImGui::GetContentRegionAvail().x - 30); - - if (ImGui::Button(ICON_FA_COPY)) { - if (result.is_creature && result.creature) { - ImGui::SetClipboardText(result.creature->name.c_str()); - Presentation::showSuccess("Creature name copied"); - } else if (result.item) { - ImGui::SetClipboardText(std::to_string(result.item->server_id).c_str()); - Presentation::showSuccess("Item ID copied"); + // Grid flow + if (col < columns - 1 && idx + 1 < static_cast(preview_results_.size())) { + ImGui::SameLine(0.0f, 0.0f); } + + ImGui::PopID(); } - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Copy ID/Name to clipboard"); - } - - ImGui::PopID(); } - - ImGui::EndChild(); } - - ImGui::EndChild(); } void AdvancedSearchDialog::renderBottomBar() { - ImGui::Spacing(); - - float button_width = 120.0f; - float total_buttons_width = button_width * 3 + ImGui::GetStyle().ItemSpacing.x * 2; - float start_x = (ImGui::GetContentRegionAvail().x - total_buttons_width) * 0.5f; - - ImGui::SetCursorPosX(start_x); - - // Search Map button + ImGui::BeginChild("##footer", ImVec2(0, 0), ImGuiChildFlags_Borders); + + // Center content vertically + float child_h = ImGui::GetContentRegionAvail().y; + float frame_h = ImGui::GetFrameHeight(); + float v_pad = (child_h - frame_h) * 0.5f; + if (v_pad > 0) + ImGui::SetCursorPosY(ImGui::GetCursorPosY() + v_pad); + + // Result count on the left + auto count_label = std::format("Results: {}", preview_results_.size()); + ImGui::TextUnformatted(count_label.c_str()); + ImGui::SameLine(); + + // Buttons right-aligned with padding so they don't touch the edge + const char* search_label = ICON_FA_MAP " Search Map"; + const char* select_label = ICON_FA_HAND_POINTER " Select Item"; + const char* cancel_label = ICON_FA_XMARK " Cancel"; + float gap = ImGui::GetStyle().ItemSpacing.x; + float pad = ImGui::GetStyle().WindowPadding.x; + float total_w = ImGui::CalcTextSize(search_label).x + ImGui::CalcTextSize(select_label).x + + ImGui::CalcTextSize(cancel_label).x + gap * 6; // 3 buttons × 2 padding each + float avail = ImGui::GetContentRegionAvail().x; + float offset_x = avail - total_w - pad; + if (offset_x > 0) + ImGui::SetCursorPosX(ImGui::GetCursorPosX() + offset_x); + bool can_search = search_service_ && selected_preview_index_ >= 0; ImGui::BeginDisabled(!can_search); - if (ImGui::Button(ICON_FA_MAP " Search Map", ImVec2(button_width, 0))) { + if (ImGui::Button(search_label)) { onSearchMap(); } ImGui::EndDisabled(); - if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled) && !can_search) { ImGui::SetTooltip("Select an item from results first"); } - + ImGui::SameLine(); - - // Select Item button (placeholder) - ImGui::BeginDisabled(true); // Always disabled - placeholder - if (ImGui::Button(ICON_FA_HAND_POINTER " Select Item", ImVec2(button_width, 0))) { + + ImGui::BeginDisabled(true); + if (ImGui::Button(select_label)) { onSelectItem(); } ImGui::EndDisabled(); - if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) { ImGui::SetTooltip("Select item as brush (Coming Soon)"); } - + ImGui::SameLine(); - - // Cancel button - if (ImGui::Button(ICON_FA_XMARK " Cancel", ImVec2(button_width, 0))) { + + if (ImGui::Button(cancel_label)) { close(); } + + ImGui::EndChild(); } void AdvancedSearchDialog::updatePreviewResults() { preview_results_.clear(); selected_preview_index_ = -1; - - // Only search if we have query or filters + bool has_query = strlen(search_buffer_) > 0; bool has_type_filter = type_filter_.hasAnySelected(); bool has_property_filter = property_filter_.hasAnySelected(); - - if (!has_query && !has_type_filter && !has_property_filter) { - return; // No filters, no results - } - + + if (!has_query && !has_type_filter && !has_property_filter) return; + std::string query_lower = search_buffer_; std::transform(query_lower.begin(), query_lower.end(), query_lower.begin(), [](unsigned char c) { return std::tolower(c); }); - - // Search items (only if not creature-only filter) - bool search_items = !type_filter_.creature || type_filter_.hasAnySelected(); - if (search_items && search_service_ && !property_filter_.hasAnySelected() || !type_filter_.creature) { - // If only creature is selected, skip item search - if (!(type_filter_.creature && !type_filter_.depot && !type_filter_.mailbox && - !type_filter_.trash_holder && !type_filter_.container && !type_filter_.door && - !type_filter_.magic_field && !type_filter_.teleport && !type_filter_.bed && - !type_filter_.key && !type_filter_.podium)) { - - auto item_results = search_service_->searchItemDatabase( - search_buffer_, - type_filter_, - property_filter_, - 10000 // No practical limit - ); - - for (const auto* item : item_results) { - PreviewResult result; - result.is_creature = false; - result.item = item; - preview_results_.push_back(result); - } + + // Search items (skip if only creature/combat types selected — searchItemDatabase handles type+property filtering) + bool creature_only = type_filter_.creature && !type_filter_.depot && !type_filter_.mailbox && + !type_filter_.trash_holder && !type_filter_.container && !type_filter_.door && + !type_filter_.magic_field && !type_filter_.teleport && !type_filter_.bed && + !type_filter_.key && !type_filter_.podium && !type_filter_.weapon && + !type_filter_.armor && !type_filter_.ammo && !type_filter_.rune; + + if (search_service_ && !creature_only) { + auto item_results = search_service_->searchItemDatabase( + search_buffer_, type_filter_, property_filter_, 10000); + + for (const auto* item : item_results) { + preview_results_.push_back({false, item, nullptr}); } } - - // Search creatures (if creature type selected OR query and no type filter) + bool search_creatures = type_filter_.creature || (!has_type_filter && has_query); if (search_creatures && client_data_ && !property_filter_.hasAnySelected()) { const auto& creatures = client_data_->getCreatures(); - for (const auto& creature_ptr : creatures) { if (!creature_ptr) continue; const auto* creature = creature_ptr.get(); - - // Match by name if we have a query + if (has_query) { std::string name_lower = creature->name; std::transform(name_lower.begin(), name_lower.end(), name_lower.begin(), [](unsigned char c) { return std::tolower(c); }); - - if (name_lower.find(query_lower) == std::string::npos) { - continue; - } + if (name_lower.find(query_lower) == std::string::npos) continue; } - - PreviewResult result; - result.is_creature = true; - result.creature = creature; - preview_results_.push_back(result); + + preview_results_.push_back({true, nullptr, creature}); } } + + // Sort by server_id ascending, creatures (id=0) last + std::sort(preview_results_.begin(), preview_results_.end(), + [](const PreviewResult& a, const PreviewResult& b) { + uint16_t a_id = a.is_creature ? UINT16_MAX : (a.item ? a.item->server_id : 0); + uint16_t b_id = b.is_creature ? UINT16_MAX : (b.item ? b.item->server_id : 0); + return a_id < b_id; + }); } void AdvancedSearchDialog::onSearchMap() { - if (!search_service_ || selected_preview_index_ < 0 || + if (!search_service_ || selected_preview_index_ < 0 || + selected_preview_index_ >= static_cast(preview_results_.size())) { + return; + } + + // Defer search to next frame + pending_map_search_ = true; + + // Show SearchResultsWidget immediately in searching state + if (results_widget_) results_widget_->setSearching(true); + if (view_settings_) *view_settings_ = true; +} + +void AdvancedSearchDialog::executeMapSearch() { + if (selected_preview_index_ < 0 || selected_preview_index_ >= static_cast(preview_results_.size())) { return; } - + const auto& selected = preview_results_[selected_preview_index_]; - std::vector results; - + if (selected.is_creature && selected.creature) { - // Search map for this creature by name - results = search_service_->search( - selected.creature->name, - Services::MapSearchMode::ByName, - false, // search_items - true, // search_creatures - 1000 - ); + // Set widget search buffer so empty state shows "No results found" + if (results_widget_) results_widget_->setSearchBuffer(selected.creature->name.c_str()); + results = search_service_->search(selected.creature->name, Services::MapSearchMode::ByName, false, true, 1000); } else if (selected.item) { - // Search map for this item by server ID - results = search_service_->search( - std::to_string(selected.item->server_id), - Services::MapSearchMode::ByServerId, - true, // search_items - false, // search_creatures - 1000 - ); - } - - // Output to SearchResultsWidget - if (results_widget_) { - results_widget_->setResults(results); - } - - // Auto-show the search results widget - if (view_settings_) { - *view_settings_ = true; + auto id_str = std::to_string(selected.item->server_id); + if (results_widget_) results_widget_->setSearchBuffer(id_str.c_str()); + results = search_service_->search(id_str, Services::MapSearchMode::ByServerId, true, false, 1000); } + + if (results_widget_) results_widget_->setResults(results); } void AdvancedSearchDialog::onSelectItem() { - // Placeholder for future brush selection functionality - // Will be implemented when brush system is ready + // Placeholder } } // namespace MapEditor::UI diff --git a/ImguiMapEditor/UI/Dialogs/AdvancedSearchDialog.h b/ImguiMapEditor/UI/Dialogs/AdvancedSearchDialog.h index ca6dd45..d8c4fb9 100644 --- a/ImguiMapEditor/UI/Dialogs/AdvancedSearchDialog.h +++ b/ImguiMapEditor/UI/Dialogs/AdvancedSearchDialog.h @@ -2,9 +2,7 @@ #include #include -#include #include -#include #include "Domain/Search/MapSearchResult.h" #include "Domain/Search/SearchFilterTypes.h" namespace MapEditor { @@ -12,9 +10,6 @@ namespace Domain { class ItemType; class CreatureType; } -namespace AppLogic { - class ItemPickerService; -} namespace Services { class ClientDataService; class SpriteManager; @@ -39,15 +34,13 @@ struct PreviewResult { }; /** - * Advanced Search dialog (Ctrl+Shift+F) - RME-style item/map search. + * Advanced Search dialog - RME-style item/map search. * - * 4-column layout: - * 1. Find By - unified fuzzy search (auto-detects name vs ID) - * 2. Types - multi-select toggles (OR logic) - * 3. Properties - multi-select toggles (AND logic) - * 4. Results - preview of matching items/creatures from database + * 2-column layout: + * Left column: 4 bordered sections (Search, OR types, AND properties, Hints) + * Right column: Results preview of matching items/creatures from database * - * Bottom buttons: Search Map, Select Item (placeholder), Cancel + * Footer: result count + Search Map, Select Item (placeholder), Cancel buttons */ class AdvancedSearchDialog { public: @@ -59,7 +52,6 @@ class AdvancedSearchDialog { AdvancedSearchDialog& operator=(const AdvancedSearchDialog&) = delete; // Dependencies - void setItemPickerService(AppLogic::ItemPickerService* picker) { (void)picker; } void setMapSearchService(Services::MapSearchService* service) { search_service_ = service; } void setClientDataService(Services::ClientDataService* service) { client_data_ = service; } void setSpriteManager(Services::SpriteManager* sprites) { sprite_manager_ = sprites; } @@ -73,16 +65,19 @@ class AdvancedSearchDialog { void render(); private: - // Render helpers for 4-column layout - void renderFindByColumn(); - void renderTypesColumn(); - void renderPropertiesColumn(); + // Render helpers for 2-column layout + void renderFiltersPanel(); + void renderSearchSection(); + void renderOrSection(); + void renderAndSection(); + void renderHintsSection(); void renderResultsColumn(); void renderBottomBar(); // Actions void updatePreviewResults(); void onSearchMap(); + void executeMapSearch(); void onSelectItem(); // Dependencies @@ -95,18 +90,14 @@ class AdvancedSearchDialog { // Dialog state bool is_open_ = false; bool focus_input_ = false; - bool filters_changed_ = true; // Trigger preview update + bool filters_changed_ = false; + bool pending_map_search_ = false; - // === COLUMN 1: Find By === char search_buffer_[256] = {}; - // === COLUMN 2: Types (multi-select, OR logic) === Domain::Search::TypeFilter type_filter_; - - // === COLUMN 3: Properties (multi-select, AND logic) === Domain::Search::PropertyFilter property_filter_; - // === COLUMN 4: Results Preview === std::vector preview_results_; int selected_preview_index_ = -1; }; diff --git a/ImguiMapEditor/UI/Widgets/QuickSearchPopup.cpp b/ImguiMapEditor/UI/Widgets/QuickSearchPopup.cpp deleted file mode 100644 index 24590b5..0000000 --- a/ImguiMapEditor/UI/Widgets/QuickSearchPopup.cpp +++ /dev/null @@ -1,226 +0,0 @@ -#include "QuickSearchPopup.h" -#include -#include -#include "ext/fontawesome6/IconsFontAwesome6.h" -#include "Services/ItemPickerService.h" -#include "Services/ClientDataService.h" -#include "Services/SpriteManager.h" -#include "Rendering/Core/Texture.h" -#include "UI/Utils/UIUtils.hpp" -#include "UI/Utils/PreviewUtils.hpp" -#include "Core/Config.h" - -namespace MapEditor::UI { - -QuickSearchPopup::QuickSearchPopup() { - search_buffer_[0] = '\0'; -} - -void QuickSearchPopup::open() { - is_open_ = true; - focus_input_ = true; - search_buffer_[0] = '\0'; - last_query_.clear(); - results_.clear(); - selected_index_ = 0; -} - -void QuickSearchPopup::close() { - is_open_ = false; -} - -void QuickSearchPopup::render() { - if (!is_open_) return; - - // Center popup - ImVec2 center = ImGui::GetMainViewport()->GetCenter(); - ImGui::SetNextWindowPos(center, ImGuiCond_Appearing, ImVec2(0.5f, 0.3f)); - ImGui::SetNextWindowSize(ImVec2(500, 0), ImGuiCond_Appearing); - - ImGuiWindowFlags flags = ImGuiWindowFlags_NoTitleBar | - ImGuiWindowFlags_NoResize | - ImGuiWindowFlags_NoMove | - ImGuiWindowFlags_NoScrollbar | - ImGuiWindowFlags_AlwaysAutoResize; - - ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(8, 8)); - ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 6.0f); - - if (ImGui::Begin("##QuickSearch", &is_open_, flags)) { - // Search input - ImGui::PushItemWidth(-1); - - if (focus_input_) { - ImGui::SetKeyboardFocusHere(); - focus_input_ = false; - } - - ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(8, 8)); - bool input_changed = ImGui::InputTextWithHint( - "##SearchInput", - ICON_FA_MAGNIFYING_GLASS " Search items by name or ID...", - search_buffer_, - sizeof(search_buffer_), - ImGuiInputTextFlags_EnterReturnsTrue - ); - ImGui::PopStyleVar(); - - if (input_changed) { - selectCurrent(); - } - - // Tooltip for the input field - Utils::SetTooltipOnHover(ICON_FA_KEYBOARD " Type to search, use Up/Down arrows to navigate, Enter to select"); - - ImGui::PopItemWidth(); - - // Handle keyboard - handleKeyboardNavigation(); - - // Update search if query changed - std::string current_query(search_buffer_); - if (current_query != last_query_) { - last_query_ = current_query; - doSearch(); - } - - // Results list - if (!results_.empty()) { - ImGui::Separator(); - - for (size_t i = 0; i < std::min(results_.size(), MAX_VISIBLE_RESULTS); ++i) { - const auto& result = results_[i]; - bool is_selected = (static_cast(i) == selected_index_); - - ImGui::PushID(static_cast(i)); - - // Selectable row - if (ImGui::Selectable("##Row", is_selected, 0, ImVec2(0, 32))) { - selected_index_ = static_cast(i); - selectCurrent(); - } - - // Show preview on hover (moved to cover the entire row) - if (ImGui::IsItemHovered()) { - ImGui::BeginTooltip(); - - bool rendered = false; - if (sprite_manager_ && client_data_) { - if (result.is_creature) { - if (auto preview = Utils::GetCreaturePreview(*client_data_, *sprite_manager_, result.name)) { - ImGui::Image(reinterpret_cast(static_cast(preview.texture->id())), ImVec2(preview.size, preview.size)); - rendered = true; - } - } else if (const auto* item_type = client_data_->getItemTypeByServerId(result.server_id)) { - if (auto* texture = Utils::GetItemPreview(*sprite_manager_, item_type)) { - const float size = static_cast(std::max(item_type->width, item_type->height)) * Config::UI::PREVIEW_TILE_SIZE; - ImGui::Image(reinterpret_cast(static_cast(texture->id())), ImVec2(size, size)); - rendered = true; - } - } - } - - if (!rendered) { - ImGui::TextDisabled("No preview available"); - } - ImGui::TextDisabled(result.is_creature ? "Double-click to place creature" : "Double-click to place item"); - ImGui::EndTooltip(); - } - - // Draw content on same line - ImGui::SameLine(8); - - // Selection indicator - if (is_selected) { - ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.0f, 1.0f), ICON_FA_ARROW_POINTER); - } else { - // Reserve space to prevent jitter - ImGui::Dummy(ImVec2(ImGui::CalcTextSize(ICON_FA_ARROW_POINTER).x, 0)); - } - ImGui::SameLine(); - - // Icon placeholder (could use sprite later) - if (result.is_creature) { - ImGui::Text(ICON_FA_DRAGON); - } else { - ImGui::Text(ICON_FA_CUBE); - } - - ImGui::SameLine(); - ImGui::Text("%s", result.name.c_str()); - - ImGui::SameLine(ImGui::GetWindowWidth() - 80); - ImGui::TextDisabled("ID: %u", result.server_id); - - ImGui::PopID(); - } - - // Show count if more results - if (results_.size() > MAX_VISIBLE_RESULTS) { - ImGui::Separator(); - ImGui::TextDisabled("... and %zu more", - results_.size() - MAX_VISIBLE_RESULTS); - } - } else if (std::string_view(search_buffer_).length() >= 2) { - ImGui::Separator(); - ImGui::TextDisabled(ICON_FA_CIRCLE_EXCLAMATION " No results found"); - } else if (!std::string_view(search_buffer_).empty()) { - ImGui::Separator(); - ImGui::TextDisabled("Type at least 2 characters"); - } - - // Help text - ImGui::Separator(); - ImGui::TextDisabled(ICON_FA_KEYBOARD " Enter: select | " - ICON_FA_ARROW_UP ICON_FA_ARROW_DOWN " Navigate | Esc: close"); - } - ImGui::End(); - - ImGui::PopStyleVar(2); - - // Close on Escape - if (ImGui::IsKeyPressed(ImGuiKey_Escape)) { - close(); - } -} - -void QuickSearchPopup::doSearch() { - results_.clear(); - selected_index_ = 0; - - if (!picker_ || std::string_view(search_buffer_).length() < 2) { - return; - } - - results_ = picker_->search(search_buffer_, SEARCH_LIMIT); -} - -void QuickSearchPopup::handleKeyboardNavigation() { - if (results_.empty()) return; - - if (ImGui::IsKeyPressed(ImGuiKey_DownArrow)) { - selected_index_ = std::min(selected_index_ + 1, - static_cast(std::min(results_.size(), MAX_VISIBLE_RESULTS)) - 1); - } - - if (ImGui::IsKeyPressed(ImGuiKey_UpArrow)) { - selected_index_ = std::max(selected_index_ - 1, 0); - } -} - -void QuickSearchPopup::selectCurrent() { - if (results_.empty() || selected_index_ < 0 || - selected_index_ >= static_cast(results_.size())) { - return; - } - - const auto& result = results_[selected_index_]; - - if (on_select_) { - on_select_(result.server_id, result.is_creature); - } - - close(); -} - -} // namespace MapEditor::UI diff --git a/ImguiMapEditor/UI/Widgets/QuickSearchPopup.h b/ImguiMapEditor/UI/Widgets/QuickSearchPopup.h deleted file mode 100644 index 8d5ca17..0000000 --- a/ImguiMapEditor/UI/Widgets/QuickSearchPopup.h +++ /dev/null @@ -1,84 +0,0 @@ -#pragma once -#include "Domain/Search/ISearchProvider.h" -#include -#include -#include -#include -#include - -namespace MapEditor { -namespace AppLogic { class ItemPickerService; } -namespace Services { - class SpriteManager; - class ClientDataService; -} - -namespace UI { - -/** - * QuickSearch popup (Ctrl+F) - VS Code style item picker. - * Modal popup for quickly selecting items/creatures to place. - */ -class QuickSearchPopup { -public: - using SelectCallback = std::function; - - QuickSearchPopup(); - ~QuickSearchPopup() = default; - - // Non-copyable - QuickSearchPopup(const QuickSearchPopup&) = delete; - QuickSearchPopup& operator=(const QuickSearchPopup&) = delete; - - /** - * Set dependencies (call before first render) - */ - void setItemPickerService(AppLogic::ItemPickerService* picker) { picker_ = picker; } - void setSpriteManager(Services::SpriteManager* sprites) { sprite_manager_ = sprites; } - void setClientDataService(Services::ClientDataService* client_data) { client_data_ = client_data; } - void setSelectCallback(SelectCallback callback) { on_select_ = std::move(callback); } - - /** - * Open the popup - */ - void open(); - - /** - * Close the popup - */ - void close(); - - /** - * Check if popup is open - */ - bool isOpen() const { return is_open_; } - - /** - * Render the popup (call each frame) - */ - void render(); - -private: - void doSearch(); - void handleKeyboardNavigation(); - void selectCurrent(); - - AppLogic::ItemPickerService* picker_ = nullptr; - Services::SpriteManager* sprite_manager_ = nullptr; - Services::ClientDataService* client_data_ = nullptr; - SelectCallback on_select_; - - bool is_open_ = false; - bool focus_input_ = false; - char search_buffer_[256] = {}; - std::string last_query_; - - std::vector results_; - int selected_index_ = 0; - - static constexpr size_t MAX_VISIBLE_RESULTS = 10; - static constexpr size_t SEARCH_LIMIT = 50; -}; - -} // namespace UI -} // namespace MapEditor diff --git a/ImguiMapEditor/UI/Widgets/SearchResultsWidget.cpp b/ImguiMapEditor/UI/Widgets/SearchResultsWidget.cpp index 4a08933..8bbeb68 100644 --- a/ImguiMapEditor/UI/Widgets/SearchResultsWidget.cpp +++ b/ImguiMapEditor/UI/Widgets/SearchResultsWidget.cpp @@ -19,6 +19,7 @@ SearchResultsWidget::SearchResultsWidget() { } void SearchResultsWidget::setResults(const std::vector& results) { + is_searching_ = false; auto view = results | std::views::take(MAX_RESULTS); results_.assign(view.begin(), view.end()); total_results_ = results.size(); @@ -39,6 +40,7 @@ void SearchResultsWidget::clear() { search_buffer_[0] = '\0'; filter_buffer_[0] = '\0'; filter_dirty_ = true; + is_searching_ = false; if (active_map_) saveState(); } @@ -179,8 +181,19 @@ void SearchResultsWidget::renderResultsList(float row_height) { if (results_.empty()) { ImVec2 wsize = ImGui::GetWindowSize(); - const char* msg = search_buffer_[0] ? "No results found" : "Type to search..."; - std::string text = std::format("{} {}", search_buffer_[0] ? ICON_FA_CIRCLE_EXCLAMATION : ICON_FA_KEYBOARD, msg); + const char* icon; + const char* msg; + if (is_searching_) { + icon = ICON_FA_SPINNER; + msg = "Searching..."; + } else if (search_buffer_[0]) { + icon = ICON_FA_CIRCLE_EXCLAMATION; + msg = "No results found"; + } else { + icon = ICON_FA_KEYBOARD; + msg = "Type to search..."; + } + std::string text = std::format("{} {}", icon, msg); ImVec2 tsize = ImGui::CalcTextSize(text.c_str()); ImGui::SetCursorPos(ImVec2((wsize.x - tsize.x) * 0.5f, (wsize.y - tsize.y) * 0.5f)); ImGui::PushStyleColor(ImGuiCol_Text, ImGui::GetStyle().Colors[ImGuiCol_TextDisabled]); @@ -418,9 +431,10 @@ void SearchResultsWidget::doSearch() { if (std::string_view(search_buffer_).empty()) return; - if (on_search_async_) { - on_search_async_(search_buffer_, search_items_, search_creatures_); - } + if (!on_search_async_) return; + + is_searching_ = true; + on_search_async_(search_buffer_, search_items_, search_creatures_); } } // namespace MapEditor::UI diff --git a/ImguiMapEditor/UI/Widgets/SearchResultsWidget.h b/ImguiMapEditor/UI/Widgets/SearchResultsWidget.h index 064ca23..41c5a9f 100644 --- a/ImguiMapEditor/UI/Widgets/SearchResultsWidget.h +++ b/ImguiMapEditor/UI/Widgets/SearchResultsWidget.h @@ -48,6 +48,8 @@ class SearchResultsWidget { void forgetMap(const Domain::ChunkedMap* map); void setResults(const std::vector& results); + void setSearching(bool searching) { is_searching_ = searching; } + void setSearchBuffer(const char* query) { snprintf(search_buffer_, sizeof(search_buffer_), "%s", query); } void clear(); void invalidateFilter(); size_t getResultCount() const { return total_results_; } @@ -84,6 +86,7 @@ class SearchResultsWidget { std::vector filtered_indices_; size_t filtered_count_ = 0; bool filter_dirty_ = true; + bool is_searching_ = false; const Domain::ChunkedMap* active_map_ = nullptr;