// Copyright (c) 2020 - present, Roland Munguia // Distributed under the MIT License (http://opensource.org/licenses/MIT) //#pragma once #include #include "imgui_console.h" #include "imgui_internal.h" #include // The following three functions (InputTextCallback_UserData, InputTextCallback, InputText) are obtained from misc/cpp/imgui_stdlib.h // Which are licensed under MIT License (https://github.com/ocornut/imgui/blob/master/LICENSE.txt) namespace ImGui { struct InputTextCallback_UserData { std::string* Str; ImGuiInputTextCallback ChainCallback; void* ChainCallbackUserData; }; static int InputTextCallback(ImGuiInputTextCallbackData* data) { auto* user_data = (InputTextCallback_UserData*)data->UserData; if (data->EventFlag == ImGuiInputTextFlags_CallbackResize) { // Resize string callback // If for some reason we refuse the new length (BufTextLen) and/or capacity (BufSize) we need to set them back to what we want. std::string* str = user_data->Str; IM_ASSERT(data->Buf == str->c_str()); str->resize(data->BufTextLen); data->Buf = (char*)str->c_str(); } else if (user_data->ChainCallback) { // Forward to user callback, if any data->UserData = user_data->ChainCallbackUserData; return user_data->ChainCallback(data); } return 0; } bool InputText(const char* label, std::string* str, ImGuiInputTextFlags flags, ImGuiInputTextCallback callback, void* user_data) { IM_ASSERT((flags & ImGuiInputTextFlags_CallbackResize) == 0); flags |= ImGuiInputTextFlags_CallbackResize; InputTextCallback_UserData cb_user_data; cb_user_data.Str = str; cb_user_data.ChainCallback = callback; cb_user_data.ChainCallbackUserData = user_data; return InputText(label, (char*)str->c_str(), str->capacity() + 1, flags, InputTextCallback, &cb_user_data); } } ImGuiConsole::ImGuiConsole(std::string c_name, size_t inputBufferSize) : m_ConsoleName(std::move(c_name)) { // Set input buffer size. m_Buffer.resize(inputBufferSize); m_HistoryIndex = std::numeric_limits::min(); // Specify custom data to be store/loaded from imgui.ini InitIniSettings(); // Set Console ImGui default settings if (!m_LoadedFromIni) { DefaultSettings(); } // Custom functions. RegisterConsoleCommands(); } void ImGuiConsole::Draw() { /////////////////////////////////////////////////////////////////////////// // Window and Settings //////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////// // Begin Console Window. ImGui::PushStyleVar(ImGuiStyleVar_Alpha, m_WindowAlpha); if (!ImGui::Begin(m_ConsoleName.data(), nullptr, ImGuiWindowFlags_MenuBar)) { ImGui::PopStyleVar(); ImGui::End(); return; } ImGui::PopStyleVar(); /////////////// // Menu bar // /////////////// MenuBar(); //////////////// // Filter bar // //////////////// if (m_FilterBar) { FilterBar(); } ////////////////// // Console Logs // ////////////////// LogWindow(); // Section off. ImGui::Separator(); /////////////////////////////////////////////////////////////////////////// // Command-line /////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////// InputBar(); ImGui::End(); } csys::System& ImGuiConsole::System() { return m_ConsoleSystem; } void ImGuiConsole::InitIniSettings() { ImGuiContext& g = *ImGui::GetCurrentContext(); // Load from .ini if (g.Initialized && !g.SettingsLoaded && !m_LoadedFromIni) { ImGuiSettingsHandler console_ini_handler; console_ini_handler.TypeName = "imgui-console"; console_ini_handler.TypeHash = ImHashStr("imgui-console"); console_ini_handler.ClearAllFn = SettingsHandler_ClearALl; console_ini_handler.ApplyAllFn = SettingsHandler_ApplyAll; console_ini_handler.ReadInitFn = SettingsHandler_ReadInit; console_ini_handler.ReadOpenFn = SettingsHandler_ReadOpen; console_ini_handler.ReadLineFn = SettingsHandler_ReadLine; console_ini_handler.WriteAllFn = SettingsHandler_WriteAll; console_ini_handler.UserData = this; g.SettingsHandlers.push_back(console_ini_handler); } // else Ini settings already loaded! } void ImGuiConsole::DefaultSettings() { // Settings m_AutoScroll = true; m_ScrollToBottom = false; m_ColoredOutput = true; m_FilterBar = true; m_TimeStamps = true; // Style m_WindowAlpha = 1; m_ColorPalette[COL_COMMAND] = ImVec4(1.f, 1.f, 1.f, 1.f); m_ColorPalette[COL_LOG] = ImVec4(1.f, 1.f, 1.f, 0.5f); m_ColorPalette[COL_WARNING] = ImVec4(1.0f, 0.87f, 0.37f, 1.f); m_ColorPalette[COL_ERROR] = ImVec4(1.f, 0.365f, 0.365f, 1.f); m_ColorPalette[COL_INFO] = ImVec4(0.46f, 0.96f, 0.46f, 1.f); m_ColorPalette[COL_TIMESTAMP] = ImVec4(1.f, 1.f, 1.f, 0.5f); } void ImGuiConsole::RegisterConsoleCommands() { m_ConsoleSystem.RegisterCommand("clear", "Clear console log", [this]() { m_ConsoleSystem.Items().clear(); }); m_ConsoleSystem.RegisterCommand("filter", "Set screen filter", [this](const csys::String& filter) { // Reset filter buffer. std::memset(m_TextFilter.InputBuf, '\0', 256); // Copy filter input buffer from client. std::copy(filter.m_String.c_str(), filter.m_String.c_str() + std::min(static_cast(filter.m_String.length()), 255), m_TextFilter.InputBuf); // Build text filter. m_TextFilter.Build(); }, csys::Arg("filter_str")); m_ConsoleSystem.RegisterCommand("run", "Run given script", [this](const csys::String& filter) { // Logs command. m_ConsoleSystem.RunScript(filter.m_String); }, csys::Arg("script_name")); } void ImGuiConsole::FilterBar() { m_TextFilter.Draw("Filter", ImGui::GetWindowWidth() * 0.25f); ImGui::Separator(); } void ImGuiConsole::LogWindow() { const float footerHeightToReserve = ImGui::GetStyle().ItemSpacing.y + ImGui::GetFrameHeightWithSpacing(); if (ImGui::BeginChild("ScrollRegion##", ImVec2(0, -footerHeightToReserve), false, 0)) { // Display colored command output. static const float timestamp_width = ImGui::CalcTextSize("00:00:00:0000").x; // Timestamp. int count = 0; // Item count. // Wrap items. ImGui::PushTextWrapPos(); // Display items. for (const auto& item : m_ConsoleSystem.Items()) { // Exit if word is filtered. if (!m_TextFilter.PassFilter(item.Get().c_str())) continue; // Spacing between commands. if (item.m_Type == csys::eCOMMAND) { if (m_TimeStamps) ImGui::PushTextWrapPos(ImGui::GetColumnWidth() - timestamp_width); // Wrap before timestamps start. if (count++ != 0) ImGui::Dummy(ImVec2(-1, ImGui::GetFontSize())); // No space for the first command. } // Items. if (m_ColoredOutput) { ImGui::PushStyleColor(ImGuiCol_Text, m_ColorPalette[item.m_Type]); ImGui::TextUnformatted(item.Get().data()); ImGui::PopStyleColor(); } else { ImGui::TextUnformatted(item.Get().data()); } // Time stamp. if (item.m_Type == csys::eCOMMAND && m_TimeStamps) { // No wrap for timestamps ImGui::PopTextWrapPos(); // Right align. ImGui::SameLine(ImGui::GetColumnWidth(-1) - timestamp_width); // Draw time stamp. ImGui::PushStyleColor(ImGuiCol_Text, m_ColorPalette[COL_TIMESTAMP]); ImGui::Text("%02d:%02d:%02d:%04d", ((item.m_TimeStamp / 1000 / 3600) % 24), ((item.m_TimeStamp / 1000 / 60) % 60), ((item.m_TimeStamp / 1000) % 60), item.m_TimeStamp % 1000); ImGui::PopStyleColor(); } } // Stop wrapping since we are done displaying console items. ImGui::PopTextWrapPos(); // Auto-scroll logs. if ((m_ScrollToBottom && (ImGui::GetScrollY() >= ImGui::GetScrollMaxY() || m_AutoScroll))) ImGui::SetScrollHereY(1.0f); m_ScrollToBottom = false; // Loop through command string vector. ImGui::EndChild(); } } void ImGuiConsole::InputBar() { // Variables. ImGuiInputTextFlags inputTextFlags = ImGuiInputTextFlags_CallbackHistory | ImGuiInputTextFlags_CallbackCharFilter | ImGuiInputTextFlags_CallbackCompletion | ImGuiInputTextFlags_EnterReturnsTrue | ImGuiInputTextFlags_CallbackAlways; // Only reclaim after enter key is pressed! bool reclaimFocus = false; // Input widget. (Width an always fixed width) ImGui::PushItemWidth(-ImGui::GetStyle().ItemSpacing.x * 7); if (ImGui::InputText("Input", &m_Buffer, inputTextFlags, InputCallback, this)) { // Validate. if (!m_Buffer.empty()) { // Run command line input. m_ConsoleSystem.RunCommand(m_Buffer); // Scroll to bottom after its ran. m_ScrollToBottom = true; } // Keep focus. reclaimFocus = true; // Clear command line. m_Buffer.clear(); } ImGui::PopItemWidth(); // Reset suggestions when client provides char input. if (ImGui::IsItemEdited() && !m_WasPrevFrameTabCompletion) { m_CmdSuggestions.clear(); } m_WasPrevFrameTabCompletion = false; // Auto-focus on window apparition ImGui::SetItemDefaultFocus(); if (reclaimFocus) ImGui::SetKeyboardFocusHere(-1); // Focus on command line after clearing. } void ImGuiConsole::MenuBar() { if (ImGui::BeginMenuBar()) { // Settings menu. if (ImGui::BeginMenu("Settings")) { // Colored output ImGui::Checkbox("Colored Output", &m_ColoredOutput); ImGui::SameLine(); HelpMaker("Enable colored command output"); // Auto Scroll ImGui::Checkbox("Auto Scroll", &m_AutoScroll); ImGui::SameLine(); HelpMaker("Automatically scroll to bottom of console log"); // Filter bar ImGui::Checkbox("Filter Bar", &m_FilterBar); ImGui::SameLine(); HelpMaker("Enable console filter bar"); // Time stamp ImGui::Checkbox("Time Stamps", &m_TimeStamps); ImGui::SameLine(); HelpMaker("Display command execution timestamps"); // Reset to default settings if (ImGui::Button("Reset settings", ImVec2(ImGui::GetColumnWidth(), 0))) ImGui::OpenPopup("Reset Settings?"); // Confirmation if (ImGui::BeginPopupModal("Reset Settings?", nullptr, ImGuiWindowFlags_AlwaysAutoResize)) { ImGui::Text("All settings will be reset to default.\nThis operation cannot be undone!\n\n"); ImGui::Separator(); if (ImGui::Button("Reset", ImVec2(120, 0))) { DefaultSettings(); ImGui::CloseCurrentPopup(); } ImGui::SetItemDefaultFocus(); ImGui::SameLine(); if (ImGui::Button("Cancel", ImVec2(120, 0))) { ImGui::CloseCurrentPopup(); } ImGui::EndPopup(); } ImGui::EndMenu(); } // View settings. if (ImGui::BeginMenu("Appearance")) { // Logging Colors ImGuiColorEditFlags flags = ImGuiColorEditFlags_Float | ImGuiColorEditFlags_AlphaPreview | ImGuiColorEditFlags_NoInputs | ImGuiColorEditFlags_AlphaBar; ImGui::TextUnformatted("Color Palette"); ImGui::Indent(); ImGui::ColorEdit4("Command##", (float*)&m_ColorPalette[COL_COMMAND], flags); ImGui::ColorEdit4("Log##", (float*)&m_ColorPalette[COL_LOG], flags); ImGui::ColorEdit4("Warning##", (float*)&m_ColorPalette[COL_WARNING], flags); ImGui::ColorEdit4("Error##", (float*)&m_ColorPalette[COL_ERROR], flags); ImGui::ColorEdit4("Info##", (float*)&m_ColorPalette[COL_INFO], flags); ImGui::ColorEdit4("Time Stamp##", (float*)&m_ColorPalette[COL_TIMESTAMP], flags); ImGui::Unindent(); ImGui::Separator(); // Window transparency. ImGui::TextUnformatted("Background"); ImGui::SliderFloat("Transparency##", &m_WindowAlpha, 0.1f, 1.f); ImGui::EndMenu(); } // All scripts. if (ImGui::BeginMenu("Scripts")) { // Show registered scripts. for (const auto& scr_pair : m_ConsoleSystem.Scripts()) { if (ImGui::MenuItem(scr_pair.first.c_str())) { m_ConsoleSystem.RunScript(scr_pair.first); m_ScrollToBottom = true; } } // Reload scripts. ImGui::Separator(); if (ImGui::Button("Reload Scripts", ImVec2(ImGui::GetColumnWidth(), 0))) { for (const auto& scr_pair : m_ConsoleSystem.Scripts()) { scr_pair.second->Reload(); } } ImGui::EndMenu(); } ImGui::EndMenuBar(); } } // From imgui_demo.cpp void ImGuiConsole::HelpMaker(const char* desc) { ImGui::TextDisabled("(?)"); if (ImGui::IsItemHovered()) { ImGui::BeginTooltip(); ImGui::PushTextWrapPos(ImGui::GetFontSize() * 35.0f); ImGui::TextUnformatted(desc); ImGui::PopTextWrapPos(); ImGui::EndTooltip(); } } int ImGuiConsole::InputCallback(ImGuiInputTextCallbackData* data) { // Exit if no buffer. if (data->BufTextLen == 0 && (data->EventFlag != ImGuiInputTextFlags_CallbackHistory)) return 0; // Get input string and console. std::string input_str = data->Buf; std::string trim_str; auto console = static_cast(data->UserData); // Optimize by only using positions. // Trim start and end spaces. size_t startPos = console->m_Buffer.find_first_not_of(' '); size_t endPos = console->m_Buffer.find_last_not_of(' '); // Get trimmed string. if (startPos != std::string::npos && endPos != std::string::npos) trim_str = console->m_Buffer.substr(startPos, endPos + 1); else trim_str = console->m_Buffer; switch (data->EventFlag) { case ImGuiInputTextFlags_CallbackCompletion: { // Find last word. size_t startSubtrPos = trim_str.find_last_of(' '); csys::AutoComplete* console_autocomplete; // Command line is an entire word/string (No whitespace) // Determine which autocomplete tree to use. if (startSubtrPos == std::string::npos) { startSubtrPos = 0; console_autocomplete = &console->m_ConsoleSystem.CmdAutocomplete(); } else { startSubtrPos += 1; console_autocomplete = &console->m_ConsoleSystem.VarAutocomplete(); } // Validate str if (!trim_str.empty()) { // Display suggestions on console. if (!console->m_CmdSuggestions.empty()) { console->m_ConsoleSystem.Log(csys::eCOMMAND) << "Suggestions: " << csys::endl; for (const auto& suggestion : console->m_CmdSuggestions) console->m_ConsoleSystem.Log(csys::eLOG) << suggestion << csys::endl; console->m_CmdSuggestions.clear(); } // Get partial completion and suggestions. std::string partial = console_autocomplete->Suggestions(trim_str.substr(startSubtrPos, endPos + 1), console->m_CmdSuggestions); // Autocomplete only when one work is available. if (!console->m_CmdSuggestions.empty() && console->m_CmdSuggestions.size() == 1) { data->DeleteChars(static_cast(startSubtrPos), static_cast(data->BufTextLen - startSubtrPos)); data->InsertChars(static_cast(startSubtrPos), console->m_CmdSuggestions[0].data()); console->m_CmdSuggestions.clear(); } else { // Partially complete word. if (!partial.empty()) { data->DeleteChars(static_cast(startSubtrPos), static_cast(data->BufTextLen - startSubtrPos)); data->InsertChars(static_cast(startSubtrPos), partial.data()); } } } // We have performed the completion event. console->m_WasPrevFrameTabCompletion = true; } break; case ImGuiInputTextFlags_CallbackHistory: { // Clear buffer. data->DeleteChars(0, data->BufTextLen); // Init history index if (console->m_HistoryIndex == std::numeric_limits::min()) console->m_HistoryIndex = console->m_ConsoleSystem.History().GetNewIndex(); // Traverse history. if (data->EventKey == ImGuiKey_UpArrow) { if (console->m_HistoryIndex) --(console->m_HistoryIndex); } else { if (console->m_HistoryIndex < console->m_ConsoleSystem.History().Size()) ++(console->m_HistoryIndex); } // Get history. std::string prevCommand = console->m_ConsoleSystem.History()[console->m_HistoryIndex]; // Insert commands. data->InsertChars(data->CursorPos, prevCommand.data()); } break; case ImGuiInputTextFlags_CallbackCharFilter: case ImGuiInputTextFlags_CallbackAlways: default: break; } return 0; } void ImGuiConsole::SettingsHandler_ClearALl(ImGuiContext* ctx, ImGuiSettingsHandler* handler) { } void ImGuiConsole::SettingsHandler_ReadInit(ImGuiContext* ctx, ImGuiSettingsHandler* handler) { } void* ImGuiConsole::SettingsHandler_ReadOpen(ImGuiContext* ctx, ImGuiSettingsHandler* handler, const char* name) { if (!handler->UserData) return nullptr; auto console = static_cast(handler->UserData); if (strcmp(name, console->m_ConsoleName.c_str()) != 0) return nullptr; return (void*)1; } void ImGuiConsole::SettingsHandler_ReadLine(ImGuiContext* ctx, ImGuiSettingsHandler* handler, void* entry, const char* line) { if (!handler->UserData) return; // Get console. auto console = static_cast(handler->UserData); // Ensure console doesn't reset variables. console->m_LoadedFromIni = true; // Disable warning regarding sscanf when using MVSC #pragma warning( push ) #pragma warning( disable:4996 ) #define INI_CONSOLE_LOAD_COLOR(type) (std::sscanf(line, #type"=%i,%i,%i,%i", &r, &g, &b, &a) == 4) { console->m_ColorPalette[type] = ImColor(r, g, b, a); } #define INI_CONSOLE_LOAD_FLOAT(var) (std::sscanf(line, #var"=%f", &f) == 1) { console->var = f; } #define INI_CONSOLE_LOAD_BOOL(var) (std::sscanf(line, #var"=%i", &b) == 1) {console->var = b == 1;} float f; int r, g, b, a; // Window style/visuals if INI_CONSOLE_LOAD_COLOR(COL_COMMAND) else if INI_CONSOLE_LOAD_COLOR(COL_LOG) else if INI_CONSOLE_LOAD_COLOR(COL_WARNING) else if INI_CONSOLE_LOAD_COLOR(COL_ERROR) else if INI_CONSOLE_LOAD_COLOR(COL_INFO) else if INI_CONSOLE_LOAD_COLOR(COL_TIMESTAMP) else if INI_CONSOLE_LOAD_FLOAT(m_WindowAlpha) // Window settings else if INI_CONSOLE_LOAD_BOOL(m_AutoScroll) else if INI_CONSOLE_LOAD_BOOL(m_ScrollToBottom) else if INI_CONSOLE_LOAD_BOOL(m_ColoredOutput) else if INI_CONSOLE_LOAD_BOOL(m_FilterBar) else if INI_CONSOLE_LOAD_BOOL(m_TimeStamps) #pragma warning( pop ) } void ImGuiConsole::SettingsHandler_ApplyAll(ImGuiContext* ctx, ImGuiSettingsHandler* handler) { if (!handler->UserData) return; } void ImGuiConsole::SettingsHandler_WriteAll(ImGuiContext* ctx, ImGuiSettingsHandler* handler, ImGuiTextBuffer* buf) { if (!handler->UserData) return; // Get console. auto console = static_cast(handler->UserData); #define INI_CONSOLE_SAVE_COLOR(type) buf->appendf(#type"=%i,%i,%i,%i\n", (int)(console->m_ColorPalette[type].x * 255),\ (int)(console->m_ColorPalette[type].y * 255),\ (int)(console->m_ColorPalette[type].z * 255),\ (int)(console->m_ColorPalette[type].w * 255)) #define INI_CONSOLE_SAVE_FLOAT(var) buf->appendf(#var"=%.3f\n", console->var) #define INI_CONSOLE_SAVE_BOOL(var) buf->appendf(#var"=%i\n", console->var) // Set header for CONSOLE Console. buf->appendf("[%s][%s]\n", handler->TypeName, console->m_ConsoleName.data()); // Window settings. INI_CONSOLE_SAVE_BOOL(m_AutoScroll); INI_CONSOLE_SAVE_BOOL(m_ScrollToBottom); INI_CONSOLE_SAVE_BOOL(m_ColoredOutput); INI_CONSOLE_SAVE_BOOL(m_FilterBar); INI_CONSOLE_SAVE_BOOL(m_TimeStamps); // Window style/visuals INI_CONSOLE_SAVE_FLOAT(m_WindowAlpha); INI_CONSOLE_SAVE_COLOR(COL_COMMAND); INI_CONSOLE_SAVE_COLOR(COL_LOG); INI_CONSOLE_SAVE_COLOR(COL_WARNING); INI_CONSOLE_SAVE_COLOR(COL_ERROR); INI_CONSOLE_SAVE_COLOR(COL_INFO); INI_CONSOLE_SAVE_COLOR(COL_TIMESTAMP); // End saving. buf->append("\n"); }