From 7fa7840be1093f194031d2d5d9b45ada8613e72e Mon Sep 17 00:00:00 2001 From: rtk0c Date: Mon, 25 Apr 2022 20:32:25 -0700 Subject: Changeset: 17 Replace EditorCommandPalette.hpp/cpp with the copy from P6503 --- source/AppConfig.hpp | 5 + source/EditorCommandPalette.cpp | 828 +++++++++++++--------------------------- source/EditorCommandPalette.hpp | 109 ++++-- source/EditorCore.cpp | 2 +- source/EditorCore.hpp | 2 + 5 files changed, 350 insertions(+), 596 deletions(-) diff --git a/source/AppConfig.hpp b/source/AppConfig.hpp index b6c1fab..d797708 100644 --- a/source/AppConfig.hpp +++ b/source/AppConfig.hpp @@ -1,5 +1,6 @@ #pragma once +#include #include #include @@ -12,6 +13,10 @@ inline float mainWidnowWidth; inline float mainWindowHeight; inline float mainWindowAspectRatio; +// TODO add a bold font +inline ImFont* fontRegular = nullptr; +inline ImFont* fontBold = nullptr; + // Duplicate each as path and string so that on non-UTF-8 platforms (e.g. Windows) we can easily do string manipulation on the paths // NOTE: even though non-const, these should not be modified outside of main() inline std::filesystem::path dataDirPath; diff --git a/source/EditorCommandPalette.cpp b/source/EditorCommandPalette.cpp index 02ff65a..0e7b894 100644 --- a/source/EditorCommandPalette.cpp +++ b/source/EditorCommandPalette.cpp @@ -1,608 +1,281 @@ #include "EditorCommandPalette.hpp" +#include "AppConfig.hpp" +#include "EditorUtils.hpp" #include "FuzzyMatch.hpp" +#include "Utils.hpp" +#include #include +#include #include -#include #include #include #define IMGUI_DEFINE_MATH_OPERATORS #include -namespace ImCmd { -// ================================================================= -// Private forward decls -// ================================================================= +using namespace std::literals; -struct StackFrame; -class ExecutionManager; - -struct SearchResult; -class SearchManager; - -struct CommandOperationRegister; -struct CommandOperationUnregister; -struct CommandOperation; -struct Context; - -struct ItemExtraData; -struct Instance; - -// ================================================================= -// Private interface -// ================================================================= - -struct StackFrame { - std::vector Options; - int SelectedOption = -1; -}; - -class ExecutionManager { -private: - Instance* m_Instance; - Command* m_ExecutingCommand = nullptr; - std::vector m_CallStack; - -public: - ExecutionManager(Instance& instance) - : m_Instance{ &instance } {} - - int GetItemCount() const; - const char* GetItem(int idx) const; - void SelectItem(int idx); - - void PushOptions(std::vector options); -}; - -struct SearchResult { - int ItemIndex; - int Score; - int MatchCount; - uint8_t Matches[32]; -}; - -class SearchManager { -private: - Instance* m_Instance; - -public: - std::vector SearchResults; - char SearchText[std::numeric_limits::max() + 1]; - -public: - SearchManager(Instance& instance) - : m_Instance{ &instance } { - std::memset(SearchText, 0, sizeof(SearchText)); - } - - int GetItemCount() const; - const char* GetItem(int idx) const; - - bool IsActive() const; - - void SetSearchText(const char* text); - void ClearSearchText(); - void RefreshSearchResults(); -}; - -struct CommandOperationRegister { - Command Candidate; -}; - -struct CommandOperationUnregister { - const char* Name; -}; - -struct CommandOperation { - enum OpType { - OpType_Register, - OpType_Unregister, - }; - - OpType Type; - int Index; -}; - -struct Context { - ImGuiStorage Instances; - Instance* CurrentCommandPalette = nullptr; - std::vector Commands; - std::vector PendingRegisterOps; - std::vector PendingUnregisterOps; - std::vector PendingOps; - ImFont* Fonts[ImCmdTextType_COUNT] = {}; - ImU32 FontColors[ImCmdTextType_COUNT] = {}; - int CommandStorageLocks = 0; - bool HasFontColorOverride[ImCmdTextType_COUNT] = {}; - bool IsExecuting = false; - bool IsTerminating = false; - - struct - { - bool ItemSelected = false; - } LastCommandPaletteStatus; - - struct - { - const char* NewSearchText = nullptr; - bool FocusSearchBox = false; - } NextCommandPaletteActions; - - void RegisterCommand(Command command) { - auto location = std::lower_bound( - Commands.begin(), - Commands.end(), - command, - [](const Command& a, const Command& b) -> bool { - return strcmp(a.Name, b.Name) < 0; - }); - Commands.insert(location, std::move(command)); - } - - bool UnregisterCommand(const char* name) { - struct Comparator { - bool operator()(const Command& command, const char* str) const { - return strcmp(command.Name, str) < 0; - } - - bool operator()(const char* str, const Command& command) const { - return strcmp(str, command.Name) < 0; - } - }; - - auto range = std::equal_range(Commands.begin(), Commands.end(), name, Comparator{}); - Commands.erase(range.first, range.second); - - return range.first != range.second; - } - - bool CommitOps() { - if (IsCommandStorageLocked()) { - return false; - } - - for (auto& operation : PendingOps) { - switch (operation.Type) { - case CommandOperation::OpType_Register: { - auto& op = PendingRegisterOps[operation.Index]; - RegisterCommand(std::move(op.Candidate)); - } break; - - case CommandOperation::OpType_Unregister: { - auto& op = PendingUnregisterOps[operation.Index]; - UnregisterCommand(op.Name); - } break; - } - } - - bool had_action = !PendingOps.empty(); - PendingRegisterOps.clear(); - PendingUnregisterOps.clear(); - PendingOps.clear(); - - return had_action; - } - - bool IsCommandStorageLocked() const { - return CommandStorageLocks > 0; - } -}; - -struct ItemExtraData { - bool Hovered = false; - bool Held = false; -}; - -struct Instance { - ExecutionManager Session; - SearchManager Search; - std::vector ExtraData; - - int CurrentSelectedItem = 0; - - struct - { - bool RefreshSearch = false; - bool ClearSearch = false; - } PendingActions; - - Instance() - : Session(*this) - , Search(*this) {} -}; - -static Context gContext; - -// ================================================================= -// Private implementation -// ================================================================= - -int ExecutionManager::GetItemCount() const { - if (m_ExecutingCommand) { - return static_cast(m_CallStack.back().Options.size()); - } else { - return static_cast(gContext.Commands.size()); - } +bool EditorCommandExecuteContext::IsInitiated() const { + return mCommand != nullptr; } -const char* ExecutionManager::GetItem(int idx) const { - if (m_ExecutingCommand) { - return m_CallStack.back().Options[idx].c_str(); - } else { - return gContext.Commands[idx].Name; - } -} - -template -static void InvokeSafe(const std::function& func, Ts... args) { - if (func) { - func(std::forward(args)...); - } +const EditorCommand* EditorCommandExecuteContext::GetCurrentCommand() const { + return mCommand; } -void ExecutionManager::SelectItem(int idx) { - auto cmd = m_ExecutingCommand; - size_t initial_call_stack_height = m_CallStack.size(); - if (cmd == nullptr) { - cmd = m_ExecutingCommand = &gContext.Commands[idx]; - ++gContext.CommandStorageLocks; - - gContext.IsExecuting = true; - InvokeSafe(m_ExecutingCommand->InitialCallback); // Calls ::Prompt() - gContext.IsExecuting = false; - } else { - m_CallStack.back().SelectedOption = idx; - - gContext.IsExecuting = true; - InvokeSafe(cmd->SubsequentCallback, idx); // Calls ::Prompt() - gContext.IsExecuting = false; - } - - size_t final_call_stack_height = m_CallStack.size(); - if (initial_call_stack_height == final_call_stack_height) { - - gContext.IsTerminating = true; - InvokeSafe(m_ExecutingCommand->TerminatingCallback); // Shouldn't call ::Prompt() - gContext.IsTerminating = false; - - m_ExecutingCommand = nullptr; - m_CallStack.clear(); - --gContext.CommandStorageLocks; - - // If the executed command involved subcommands... - if (final_call_stack_height > 0) { - m_Instance->PendingActions.ClearSearch = true; - m_Instance->CurrentSelectedItem = 0; - } - - gContext.LastCommandPaletteStatus.ItemSelected = true; - } else { - // Something new is prompted - // It doesn't make sense for "current selected item" to persists through completely different set of options - m_Instance->PendingActions.ClearSearch = true; - m_Instance->CurrentSelectedItem = 0; +void EditorCommandExecuteContext::Initiate(const EditorCommand& command) { + if (mCommand == nullptr) { + mCommand = &command; } } -void ExecutionManager::PushOptions(std::vector options) { - m_CallStack.push_back({}); - auto& frame = m_CallStack.back(); - - frame.Options = std::move(options); - - m_Instance->PendingActions.ClearSearch = true; +void EditorCommandExecuteContext::Prompt(std::vector options) { + assert(mCommand != nullptr); + mCurrentOptions = std::move(options); + ++mDepth; } -int SearchManager::GetItemCount() const { - return static_cast(SearchResults.size()); +void EditorCommandExecuteContext::Finish() { + assert(mCommand != nullptr); + mCommand = nullptr; + mCurrentOptions.clear(); + mDepth = 0; } -const char* SearchManager::GetItem(int idx) const { - int actualIdx = SearchResults[idx].ItemIndex; - return m_Instance->Session.GetItem(actualIdx); +int EditorCommandExecuteContext::GetExecutionDepth() const { + return mDepth; } -bool SearchManager::IsActive() const { - return SearchText[0] != '\0'; -} +struct EditorCommandPalette::SearchResult { + int itemIndex; + int score; + int matchCount; + uint8_t matches[32]; +}; -void SearchManager::SetSearchText(const char* text) { - // Note: must detect clang first because clang-cl.exe defines both _MSC_VER and __clang__, but only accepts #pragma clang -#if defined(__GNUC__) -# pragma GCC diagnostic push -# pragma GCC diagnostic ignored "-Wdeprecated-declarations" -#elif defined(__clang__) -# pragma clang diagnostic push -# pragma clang diagnostic ignored "-Wdeprecated-declarations" -#elif defined(_MSC_VER) -# pragma warning(push) -# pragma warning(disable : 4996) -#endif - // Copy at most IM_ARRAYSIZE(SearchText) chars from `text` to `SearchText` - std::strncpy(SearchText, text, IM_ARRAYSIZE(SearchText)); -#if defined(__GNUC__) -# pragma GCC diagnostic pop -#elif defined(__clang__) -# pragma clang diagnostic pop -#elif defined(_MSC_VER) -# pragma warning(pop) -#endif - RefreshSearchResults(); -} +struct EditorCommandPalette::Item { + bool hovered = false; + bool held = false; +}; -void SearchManager::ClearSearchText() { - std::memset(SearchText, 0, IM_ARRAYSIZE(SearchText)); - SearchResults.clear(); +EditorCommandPalette::EditorCommandPalette() = default; +EditorCommandPalette::~EditorCommandPalette() = default; + +namespace P6503_UNITY_ID { +std::string MakeCommandName(std::string_view category, std::string_view name) { + std::string result; + constexpr auto infix = ": "sv; + result.reserve(category.size() + infix.size() + name.size()); + result.append(category); + result.append(infix); + result.append(name); + return result; } +} // namespace P6503_UNITY_ID -void SearchManager::RefreshSearchResults() { - m_Instance->CurrentSelectedItem = 0; - SearchResults.clear(); - - int item_count = m_Instance->Session.GetItemCount(); - for (int i = 0; i < item_count; ++i) { - const char* text = m_Instance->Session.GetItem(i); - SearchResult result; - if (FuzzyMatch::Search(SearchText, text, result.Score, result.Matches, IM_ARRAYSIZE(result.Matches), result.MatchCount)) { - result.ItemIndex = i; - SearchResults.push_back(result); - } - } +void EditorCommandPalette::AddCommand(std::string_view category, std::string_view name, EditorCommand command) { + command.name = P6503_UNITY_ID::MakeCommandName(category, name); - std::sort( - SearchResults.begin(), - SearchResults.end(), - [](const SearchResult& a, const SearchResult& b) -> bool { - // We want the biggest element first - return a.Score > b.Score; + auto location = std::lower_bound( + mCommands.begin(), + mCommands.end(), + command, + [](const EditorCommand& a, const EditorCommand& b) -> bool { + return a.name < b.name; }); -} + auto iter = mCommands.insert(location, std::move(command)); -// ================================================================= -// API implementation -// ================================================================= - -void AddCommand(Command command) { - if (gContext.IsCommandStorageLocked()) { - gContext.PendingRegisterOps.push_back(CommandOperationRegister{ std::move(command) }); - CommandOperation op; - op.Type = CommandOperation::OpType_Register; - op.Index = static_cast(gContext.PendingRegisterOps.size()) - 1; - gContext.PendingOps.push_back(op); - } else { - gContext.RegisterCommand(std::move(command)); - } - - if (auto current = gContext.CurrentCommandPalette) { - current->PendingActions.RefreshSearch = true; - } + InvalidateSearchResults(); } -void RemoveCommand(const char* name) { - if (gContext.IsCommandStorageLocked()) { - gContext.PendingUnregisterOps.push_back(CommandOperationUnregister{ name }); - CommandOperation op; - op.Type = CommandOperation::OpType_Unregister; - op.Index = static_cast(gContext.PendingUnregisterOps.size()) - 1; - gContext.PendingOps.push_back(op); - } else { - gContext.UnregisterCommand(name); - } - - if (auto current = gContext.CurrentCommandPalette) { - current->PendingActions.RefreshSearch = true; - } +void EditorCommandPalette::RemoveCommand(std::string_view category, std::string_view name) { + auto commandName = P6503_UNITY_ID::MakeCommandName(category, name); + RemoveCommand(commandName); } -void SetStyleFont(ImCmdTextType type, ImFont* font) { - gContext.Fonts[type] = font; -} - -void SetStyleColor(ImCmdTextType type, ImU32 color) { - gContext.FontColors[type] = color; - gContext.HasFontColorOverride[type] = true; -} +void EditorCommandPalette::RemoveCommand(const std::string& commandName) { + struct Comparator { + bool operator()(const EditorCommand& command, const std::string& str) const { + return command.name < str; + } -void ClearStyleColor(ImCmdTextType type) { - gContext.HasFontColorOverride[type] = false; -} + bool operator()(const std::string& str, const EditorCommand& command) const { + return str < command.name; + } + }; -void SetNextCommandPaletteSearch(const char* text) { - IM_ASSERT(text != nullptr); - gContext.NextCommandPaletteActions.NewSearchText = text; -} + auto range = std::equal_range(mCommands.begin(), mCommands.end(), commandName, Comparator{}); + mCommands.erase(range.first, range.second); -void SetNextCommandPaletteSearchBoxFocused() { - gContext.NextCommandPaletteActions.FocusSearchBox = true; + InvalidateSearchResults(); } -void ShowCommandPalette(const char* name) { - auto& gi = *[&]() { - auto id = ImHashStr(name); - if (auto ptr = gContext.Instances.GetVoidPtr(id)) { - return reinterpret_cast(ptr); - } else { - auto instance = new Instance(); - gContext.Instances.SetVoidPtr(id, instance); - return instance; - } - }(); +void EditorCommandPalette::Show(bool* open) { + // Center window horizontally, align top vertically + ImGui::SetNextWindowPos(ImVec2(ImGui::GetMainViewport()->Size.x / 2, 0), ImGuiCond_Always, ImVec2(0.5f, 0.0f)); + ImGui::SetNextWindowSizeRelScreen(0.3f, 0.0f); + ImGui::Begin("Command Palette", open, ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoTitleBar); float width = ImGui::GetWindowContentRegionMax().x - ImGui::GetWindowContentRegionMin().x; - float search_result_window_height = 400.0f; // TODO config - - // BEGIN this command palette - gContext.CurrentCommandPalette = &gi; - ImGui::PushID(name); - - gContext.LastCommandPaletteStatus = {}; - // BEGIN processing PendingActions - bool refresh_search = gi.PendingActions.RefreshSearch; - refresh_search |= gContext.CommitOps(); - - if (auto text = gContext.NextCommandPaletteActions.NewSearchText) { - refresh_search = false; - if (text[0] == '\0') { - gi.Search.ClearSearchText(); - } else { - gi.Search.SetSearchText(text); + if (!ImGui::IsWindowFocused(ImGuiFocusedFlags_ChildWindows) || mShouldCloseNextFrame) { + // Close popup when user unfocused the command palette window (clicking elsewhere) + // or some action requested closing this window + mShouldCloseNextFrame = false; + if (open) { + *open = false; } - } else if (gi.PendingActions.ClearSearch) { - refresh_search = false; - gi.Search.ClearSearchText(); - } - - if (refresh_search) { - gi.Search.RefreshSearchResults(); } - gi.PendingActions = {}; - // END procesisng PendingActions + if (ImGui::IsWindowAppearing() || mFocusSearchBox) { + mFocusSearchBox = false; - if (gContext.NextCommandPaletteActions.FocusSearchBox) { // Focus the search box when user first brings command palette window up // Note: this only affects the next frame ImGui::SetKeyboardFocusHere(0); } ImGui::SetNextItemWidth(width); - if (ImGui::InputText("##SearchBox", gi.Search.SearchText, IM_ARRAYSIZE(gi.Search.SearchText))) { + if (ImGui::InputText("##", &mSearchText)) { // Search string updated, update search results - gi.Search.RefreshSearchResults(); - } - ImGui::BeginChild("SearchResults", ImVec2(width, search_result_window_height)); + mFocusedItemId = 0; + mSearchResults.clear(); - auto window = ImGui::GetCurrentWindow(); - auto draw_list = window->DrawList; + size_t itemCount; + if (mExeCtx.GetExecutionDepth() == 0) { + itemCount = mCommands.size(); + } else { + itemCount = mExeCtx.mCurrentOptions.size(); + } - auto font_regular = gContext.Fonts[ImCmdTextType_Regular]; - if (!font_regular) { - font_regular = ImGui::GetDrawListSharedData()->Font; - } - auto font_highlight = gContext.Fonts[ImCmdTextType_Highlight]; - if (!font_highlight) { - font_highlight = ImGui::GetDrawListSharedData()->Font; - } + for (size_t i = 0; i < itemCount; ++i) { + const char* text; + if (mExeCtx.GetExecutionDepth() == 0) { + text = mCommands[i].name.c_str(); + } else { + text = mExeCtx.mCurrentOptions[i].c_str(); + } - ImU32 text_color_regular; - ImU32 text_color_highlight; - if (gContext.HasFontColorOverride[ImCmdTextType_Regular]) { - text_color_regular = gContext.FontColors[ImCmdTextType_Regular]; - } else { - text_color_regular = ImGui::GetColorU32(ImGuiCol_Text); - } - if (gContext.HasFontColorOverride[ImCmdTextType_Highlight]) { - text_color_highlight = gContext.FontColors[ImCmdTextType_Highlight]; - } else { - text_color_highlight = ImGui::GetColorU32(ImGuiCol_Text); + SearchResult result{ + .itemIndex = (int)i, + }; + if (FuzzyMatch::Search(mSearchText.c_str(), text, result.score, result.matches, std::size(result.matches), result.matchCount)) { + mSearchResults.push_back(result); + } + } + + std::sort( + mSearchResults.begin(), + mSearchResults.end(), + [](const SearchResult& a, const SearchResult& b) -> bool { + // We want the biggest element first + return a.score > b.score; + }); } - auto item_hovered_color = ImGui::GetColorU32(ImGuiCol_HeaderHovered); - auto item_active_color = ImGui::GetColorU32(ImGuiCol_HeaderActive); - auto item_selected_color = ImGui::GetColorU32(ImGuiCol_Header); + ImGui::BeginChild("SearchResults", ImVec2(width, 300), false, ImGuiWindowFlags_AlwaysAutoResize); + auto window = ImGui::GetCurrentWindow(); - int item_count; - if (gi.Search.IsActive()) { - item_count = gi.Search.GetItemCount(); - } else { - item_count = gi.Session.GetItemCount(); - } + auto& io = ImGui::GetIO(); + auto dlSharedData = ImGui::GetDrawListSharedData(); + + auto textColor = ImGui::GetColorU32(ImGuiCol_Text); + auto itemHoveredColor = ImGui::GetColorU32(ImGuiCol_HeaderHovered); + auto itemActiveColor = ImGui::GetColorU32(ImGuiCol_HeaderActive); + auto itemSelectedColor = ImGui::GetColorU32(ImGuiCol_Header); - if (gi.ExtraData.size() < item_count) { - gi.ExtraData.resize(item_count); + int itemCount = GetItemCount(); + if (mItems.size() < itemCount) { + mItems.resize(itemCount); } // Flag used to delay item selection until after the loop ends - bool select_focused_item = false; - for (int i = 0; i < item_count; ++i) { + bool selectFocusedItem = false; + for (size_t i = 0; i < itemCount; ++i) { auto id = window->GetID(static_cast(i)); ImVec2 size{ ImGui::GetContentRegionAvail().x, - ImMax(font_regular->FontSize, font_highlight->FontSize), + dlSharedData->Font->FontSize, }; ImRect rect{ window->DC.CursorPos, window->DC.CursorPos + ImGui::CalcItemSize(size, 0.0f, 0.0f), }; - bool& hovered = gi.ExtraData[i].Hovered; - bool& held = gi.ExtraData[i].Held; + bool& hovered = mItems[i].hovered; + bool& held = mItems[i].held; if (held && hovered) { - draw_list->AddRectFilled(rect.Min, rect.Max, item_active_color); + window->DrawList->AddRectFilled(rect.Min, rect.Max, itemActiveColor); } else if (hovered) { - draw_list->AddRectFilled(rect.Min, rect.Max, item_hovered_color); - } else if (gi.CurrentSelectedItem == i) { - draw_list->AddRectFilled(rect.Min, rect.Max, item_selected_color); + window->DrawList->AddRectFilled(rect.Min, rect.Max, itemHoveredColor); + } else if (mFocusedItemId == i) { + window->DrawList->AddRectFilled(rect.Min, rect.Max, itemSelectedColor); } - if (gi.Search.IsActive()) { + auto item = GetItem(i); + if (item.indexType == SearchResultIndex) { // Iterating search results: draw text with highlights at matched chars - auto& search_result = gi.Search.SearchResults[i]; - auto text = gi.Search.GetItem(i); + auto& searchResult = mSearchResults[i]; + auto textPos = window->DC.CursorPos; + int rangeBegin; + int rangeEnd; + int lastRangeEnd = 0; - auto text_pos = window->DC.CursorPos; - int range_begin; - int range_end; - int last_range_end = 0; - - auto DrawCurrentRange = [&]() { - if (range_begin != last_range_end) { + auto DrawCurrentRange = [&]() -> void { + if (rangeBegin != lastRangeEnd) { // Draw normal text between last highlighted range end and current highlighted range start - auto begin = text + last_range_end; - auto end = text + range_begin; - draw_list->AddText(text_pos, text_color_regular, begin, end); + auto begin = item.text + lastRangeEnd; + auto end = item.text + rangeBegin; + window->DrawList->AddText(textPos, textColor, begin, end); - auto segment_size = font_regular->CalcTextSizeA(font_regular->FontSize, std::numeric_limits::max(), 0.0f, begin, end); - text_pos.x += segment_size.x; + auto segmentSize = dlSharedData->Font->CalcTextSizeA(dlSharedData->Font->FontSize, std::numeric_limits::max(), 0.0f, begin, end); + textPos.x += segmentSize.x; } - auto begin = text + range_begin; - auto end = text + range_end; - draw_list->AddText(font_highlight, font_highlight->FontSize, text_pos, text_color_highlight, begin, end); + auto begin = item.text + rangeBegin; + auto end = item.text + rangeEnd; + window->DrawList->AddText(AppConfig::fontBold, AppConfig::fontBold->FontSize, textPos, textColor, begin, end); - auto segment_size = font_highlight->CalcTextSizeA(font_highlight->FontSize, std::numeric_limits::max(), 0.0f, begin, end); - text_pos.x += segment_size.x; + auto segmentSize = AppConfig::fontBold->CalcTextSizeA(AppConfig::fontBold->FontSize, std::numeric_limits::max(), 0.0f, begin, end); + textPos.x += segmentSize.x; }; - IM_ASSERT(search_result.MatchCount >= 1); - range_begin = search_result.Matches[0]; - range_end = range_begin; + assert(searchResult.matchCount >= 1); + rangeBegin = searchResult.matches[0]; + rangeEnd = rangeBegin; - int last_char_idx = -1; - for (int j = 0; j < search_result.MatchCount; ++j) { - int char_idx = search_result.Matches[j]; + int lastCharIdx = -1; + for (int j = 0; j < searchResult.matchCount; ++j) { + int charIdx = searchResult.matches[j]; - if (char_idx == last_char_idx + 1) { + if (charIdx == lastCharIdx + 1) { // These 2 indices are equal, extend our current range by 1 - ++range_end; + ++rangeEnd; } else { DrawCurrentRange(); - last_range_end = range_end; - range_begin = char_idx; - range_end = char_idx + 1; + lastRangeEnd = rangeEnd; + rangeBegin = charIdx; + rangeEnd = charIdx + 1; } - last_char_idx = char_idx; + lastCharIdx = charIdx; } // Draw the remaining range (if any) - if (range_begin != range_end) { + if (rangeBegin != rangeEnd) { DrawCurrentRange(); } // Draw the text after the last range (if any) - draw_list->AddText(text_pos, text_color_regular, text + range_end); // Draw until \0 + window->DrawList->AddText(textPos, textColor, item.text + rangeEnd); // Draw until \0 } else { // Iterating everything else: draw text as-is, there is no highlights - auto text = gi.Session.GetItem(i); - auto text_pos = window->DC.CursorPos; - draw_list->AddText(text_pos, text_color_regular, text); + window->DrawList->AddText(window->DC.CursorPos, textColor, item.text); } ImGui::ItemSize(rect); @@ -610,95 +283,124 @@ void ShowCommandPalette(const char* name) { continue; } if (ImGui::ButtonBehavior(rect, id, &hovered, &held)) { - gi.CurrentSelectedItem = i; - select_focused_item = true; + mFocusedItemId = i; + selectFocusedItem = true; } } - if (ImGui::IsKeyPressed(ImGui::GetKeyIndex(ImGuiKey_UpArrow))) { - gi.CurrentSelectedItem = ImMax(gi.CurrentSelectedItem - 1, 0); - } else if (ImGui::IsKeyPressed(ImGui::GetKeyIndex(ImGuiKey_DownArrow))) { - gi.CurrentSelectedItem = ImMin(gi.CurrentSelectedItem + 1, item_count - 1); + if (ImGui::IsKeyPressed(GLFW_KEY_UP)) { + mFocusedItemId = std::max(mFocusedItemId - 1, 0); + } else if (ImGui::IsKeyPressed(GLFW_KEY_DOWN)) { + mFocusedItemId = std::min(mFocusedItemId + 1, itemCount - 1); } - if (ImGui::IsKeyPressed(ImGui::GetKeyIndex(ImGuiKey_Enter)) || select_focused_item) { - if (gi.Search.IsActive() && !gi.Search.SearchResults.empty()) { - auto idx = gi.Search.SearchResults[gi.CurrentSelectedItem].ItemIndex; - gi.Session.SelectItem(idx); - } else { - gi.Session.SelectItem(gi.CurrentSelectedItem); - } + if (ImGui::IsKeyPressed(GLFW_KEY_ENTER) || selectFocusedItem) { + SelectFocusedItem(); } ImGui::EndChild(); - gContext.NextCommandPaletteActions = {}; - - ImGui::PopID(); - gContext.CurrentCommandPalette = nullptr; - // END this command palette + ImGui::End(); } -bool IsAnyItemSelected() { - return gContext.LastCommandPaletteStatus.ItemSelected; +size_t EditorCommandPalette::GetItemCount() const { + int depth = mExeCtx.GetExecutionDepth(); + if (depth == 0) { + if (mSearchText.empty()) { + return mCommands.size(); + } else { + return mSearchResults.size(); + } + } else { + if (mSearchText.empty()) { + return mExeCtx.mCurrentOptions.size(); + } else { + return mSearchResults.size(); + } + } } -void RemoveCache(const char* name) { - auto& instances = gContext.Instances; - auto id = ImHashStr(name); - if (auto ptr = instances.GetVoidPtr(id)) { - auto instance = reinterpret_cast(ptr); - instances.SetVoidPtr(id, nullptr); - delete instance; +EditorCommandPalette::ItemInfo EditorCommandPalette::GetItem(size_t idx) const { + ItemInfo option; + + int depth = mExeCtx.GetExecutionDepth(); + if (depth == 0) { + if (mSearchText.empty()) { + option.text = mCommands[idx].name.c_str(); + option.command = &mCommands[idx]; + option.itemId = idx; + option.indexType = DirectIndex; + } else { + auto id = mSearchResults[idx].itemIndex; + option.text = mCommands[id].name.c_str(); + option.command = &mCommands[id]; + option.itemId = id; + option.indexType = SearchResultIndex; + } + option.itemType = CommandItem; + } else { + assert(mExeCtx.GetCurrentCommand() != nullptr); + if (mSearchText.empty()) { + option.text = mExeCtx.mCurrentOptions[idx].c_str(); + option.command = mExeCtx.GetCurrentCommand(); + option.itemId = idx; + option.indexType = DirectIndex; + } else { + auto id = mSearchResults[idx].itemIndex; + option.text = mExeCtx.mCurrentOptions[id].c_str(); + option.command = mExeCtx.GetCurrentCommand(); + option.itemId = id; + option.indexType = SearchResultIndex; + } + option.itemType = CommandOptionItem; } + + return option; } -void RemoveAllCaches() { - auto& instances = gContext.Instances; - for (auto& entry : instances.Data) { - auto instance = reinterpret_cast(entry.val_p); - entry.val_p = nullptr; - delete instance; +void EditorCommandPalette::SelectFocusedItem() { + if (mFocusedItemId < 0 || mFocusedItemId >= GetItemCount()) { + return; } - instances = {}; -} -void SetNextWindowAffixedTop(ImGuiCond cond) { - auto viewport = ImGui::GetMainViewport()->Size; + auto selectedItem = GetItem(mFocusedItemId); + auto& command = *selectedItem.command; - // Center window horizontally, align top vertically - ImGui::SetNextWindowPos(ImVec2(viewport.x / 2, 0), cond, ImVec2(0.5f, 0.0f)); -} + int depth = mExeCtx.GetExecutionDepth(); + if (depth == 0) { + assert(!mExeCtx.IsInitiated()); -void ShowCommandPaletteWindow(const char* name, bool* p_open) { - auto viewport = ImGui::GetMainViewport()->Size; + mExeCtx.Initiate(*selectedItem.command); + if (command.callback) { + command.callback(mExeCtx); - SetNextWindowAffixedTop(); - ImGui::SetNextWindowSize(ImVec2(viewport.x * 0.3f, 0.0f)); - ImGui::Begin(name, nullptr, ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoTitleBar); + mFocusSearchBox = true; + // Don't invalidate search results if no further actions have been requested (returning to global list of commands) + if (mExeCtx.IsInitiated()) { + InvalidateSearchResults(); + } + } else { + mExeCtx.Finish(); + } + } else { + assert(mExeCtx.IsInitiated()); + assert(command.subsequentCallback); + command.subsequentCallback(mExeCtx, selectedItem.itemId); - if (ImGui::IsWindowAppearing()) { - SetNextCommandPaletteSearchBoxFocused(); + mFocusSearchBox = true; + InvalidateSearchResults(); } - ShowCommandPalette(name); - - if (IsAnyItemSelected()) { - *p_open = false; - } - if (!ImGui::IsWindowFocused(ImGuiFocusedFlags_ChildWindows)) { - // Close popup when user unfocused the command palette window (clicking elsewhere) - *p_open = false; + // This action terminated execution, close command palette window + if (!mExeCtx.IsInitiated()) { + if (command.terminate) { + command.terminate(); + } + mShouldCloseNextFrame = true; } - - ImGui::End(); } -void Prompt(std::vector options) { - IM_ASSERT(gContext.CurrentCommandPalette != nullptr); - IM_ASSERT(gContext.IsExecuting); - IM_ASSERT(!gContext.IsTerminating); - - auto& gi = *gContext.CurrentCommandPalette; - gi.Session.PushOptions(std::move(options)); +void EditorCommandPalette::InvalidateSearchResults() { + mSearchText.clear(); + mSearchResults.clear(); + mFocusedItemId = 0; } -} // namespace ImCmd diff --git a/source/EditorCommandPalette.hpp b/source/EditorCommandPalette.hpp index 1603f41..101344d 100644 --- a/source/EditorCommandPalette.hpp +++ b/source/EditorCommandPalette.hpp @@ -2,48 +2,93 @@ #include #include -#include #include #include -#include +#include -enum ImCmdTextType { - ImCmdTextType_Regular, - ImCmdTextType_Highlight, - ImCmdTextType_COUNT, +class EditorCommandExecuteContext; +class EditorCommand { +public: + std::string name; + std::function callback; + std::function subsequentCallback; + std::function terminate; }; -namespace ImCmd { -struct Command { - const char* Name; - std::function InitialCallback; - std::function SubsequentCallback; - std::function TerminatingCallback; +class EditorCommandExecuteContext { + friend class EditorCommandPalette; + +private: + const EditorCommand* mCommand = nullptr; + std::vector mCurrentOptions; + int mDepth = 0; + +public: + bool IsInitiated() const; + const EditorCommand* GetCurrentCommand() const; + void Initiate(const EditorCommand& command); + + void Prompt(std::vector options); + void Finish(); + + /// Return the number of prompts that the user is currently completing. For example, when the user opens command + /// palette fresh and selects a command, 0 is returned. If the command asks some prompt, and then the user selects + /// again, 1 is returned. + int GetExecutionDepth() const; }; -// Command management -void AddCommand(Command command); -void RemoveCommand(const char* name); +class EditorCommandPalette { +private: + struct SearchResult; + struct Item; + + std::vector mCommands; + std::vector mItems; + std::vector mSearchResults; + std::string mSearchText; + EditorCommandExecuteContext mExeCtx; + int mFocusedItemId = 0; + bool mFocusSearchBox = false; + bool mShouldCloseNextFrame = false; -// Styling -void SetStyleFont(ImCmdTextType type, ImFont* font); -void SetStyleColor(ImCmdTextType type, ImU32 color); -void ClearStyleColor(ImCmdTextType type); //< Clear the style color for the given type, defaulting to ImGuiCol_Text +public: + EditorCommandPalette(); + ~EditorCommandPalette(); -// Command palette widget -void SetNextCommandPaletteSearch(const char* text); -void SetNextCommandPaletteSearchBoxFocused(); -void ShowCommandPalette(const char* name); -bool IsAnyItemSelected(); + EditorCommandPalette(const EditorCommandPalette&) = delete; + EditorCommandPalette& operator=(const EditorCommandPalette&) = delete; + EditorCommandPalette(EditorCommandPalette&&) = default; + EditorCommandPalette& operator=(EditorCommandPalette&&) = default; -void RemoveCache(const char* name); -void RemoveAllCaches(); + void AddCommand(std::string_view category, std::string_view name, EditorCommand command); + void RemoveCommand(std::string_view category, std::string_view name); + void RemoveCommand(const std::string& commandName); -// Command palette widget in a window helper -void SetNextWindowAffixedTop(ImGuiCond cond = 0); -void ShowCommandPaletteWindow(const char* name, bool* p_open); + void Show(bool* open = nullptr); -// Command responses, only call these in command callbacks (except TerminatingCallback) -void Prompt(std::vector options); + enum ItemType { + CommandItem, + CommandOptionItem, + }; -} // namespace ImCmd + enum IndexType { + DirectIndex, + SearchResultIndex, + }; + + struct ItemInfo { + const char* text; + const EditorCommand* command; + int itemId; + ItemType itemType; + IndexType indexType; + }; + + size_t GetItemCount() const; + ItemInfo GetItem(size_t idx) const; + + void SelectFocusedItem(); + +private: + void InvalidateSearchResults(); +}; diff --git a/source/EditorCore.cpp b/source/EditorCore.cpp index 601a850..8288df3 100644 --- a/source/EditorCore.cpp +++ b/source/EditorCore.cpp @@ -338,7 +338,7 @@ void EditorInstance::Show() { mWindowVisible_CommandPalette = !mWindowVisible_CommandPalette; } if (mWindowVisible_CommandPalette) { - ImCmd::ShowCommandPaletteWindow("Command Palette", &mWindowVisible_CommandPalette); + mEdCommandPalette.Show(&mWindowVisible_CommandPalette); } if (io.KeyCtrl && ImGui::IsKeyPressed(GLFW_KEY_SPACE, false)) { diff --git a/source/EditorCore.hpp b/source/EditorCore.hpp index b473b2e..af35616 100644 --- a/source/EditorCore.hpp +++ b/source/EditorCore.hpp @@ -1,6 +1,7 @@ #pragma once #include "EditorAttachment.hpp" +#include "EditorCommandPalette.hpp" #include "GameObject.hpp" #include "Ires.hpp" #include "RcPtr.hpp" @@ -61,6 +62,7 @@ private: GameWorld* mWorld; GameObject* mPopupCurrent_GameObject = nullptr; RcPtr mSpriteView_Instance; + EditorCommandPalette mEdCommandPalette; EditorInspector mEdInspector; EditorContentBrowser mEdContentBrowser; int mSpriteView_Frame; -- cgit v1.2.3-70-g09d2