/* * Copyright (c) Contributors to the Open 3D Engine Project * * SPDX-License-Identifier: Apache-2.0 OR MIT * */ #include #if defined(IMGUI_ENABLED) #include #include #include #include #include #include #include #include #include #include #include #include using namespace AzFramework; //////////////////////////////////////////////////////////////////////////////////////////////////// namespace AZ { //////////////////////////////////////////////////////////////////////////////////////////////// constexpr const char* DebugConsoleInputContext = "DebugConsoleInputContext"; const InputChannelId ToggleDebugConsoleInputChannelId("ToggleDebugConsole"); const InputChannelId ThumbstickL3AndR3InputChannelId("ThumbstickL3AndR3"); //////////////////////////////////////////////////////////////////////////////////////////////// AZ::Color GetColorForLogLevel(const AZ::LogLevel& logLevel) { switch (logLevel) { case AZ::LogLevel::Fatal: case AZ::LogLevel::Error: { return AZ::Colors::Red; } break; case AZ::LogLevel::Warn: { return AZ::Colors::Yellow; } break; case AZ::LogLevel::Notice: case AZ::LogLevel::Info: case AZ::LogLevel::Debug: case AZ::LogLevel::Trace: default: { return AZ::Colors::White; } break; } } //////////////////////////////////////////////////////////////////////////////////////////////// DebugConsole::DebugConsole(int maxEntriesToDisplay, int maxInputHistorySize) : InputChannelEventListener(InputChannelEventListener::GetPriorityFirst(), true) , m_logHandler ( [this](AZ::LogLevel logLevel, const char* message, [[maybe_unused]]const char* file, [[maybe_unused]]const char* function, [[maybe_unused]]int32_t line) { AddDebugLog(message, GetColorForLogLevel(logLevel)); } ) , m_inputContext(DebugConsoleInputContext, {nullptr, InputChannelEventListener::GetPriorityFirst(), true, true}) , m_maxEntriesToDisplay(maxEntriesToDisplay) , m_maxInputHistorySize(maxInputHistorySize) { // The debug console is currently only supported when running the standalone launcher. // It does function correctly when running the editor if you remove this check, but it // conflicts with the legacy debug console that also shows at the bottom of the editor. AZ::ApplicationTypeQuery applicationType; AZ::ComponentApplicationBus::Broadcast(&AZ::ComponentApplicationRequests::QueryApplicationType, applicationType); if (!applicationType.IsGame()) { return; } // Create an input mapping so that the debug console can be toggled by 'L3+R3' on a gamepad. AZStd::shared_ptr inputMappingL3AndR3 = AZStd::make_shared(ThumbstickL3AndR3InputChannelId, m_inputContext); inputMappingL3AndR3->AddSourceInput(InputDeviceGamepad::Button::L3); inputMappingL3AndR3->AddSourceInput(InputDeviceGamepad::Button::R3); m_inputContext.AddInputMapping(inputMappingL3AndR3); // Create an input mapping so that the debug console can be toggled by any of: // - The '~' key on a keyboard. // - Both the 'L3+R3' buttons on a gamepad. // - The fourth finger press on a touch screen. AZStd::shared_ptr inputMappingToggleConsole = AZStd::make_shared(ToggleDebugConsoleInputChannelId, m_inputContext); inputMappingToggleConsole->AddSourceInput(InputDeviceKeyboard::Key::PunctuationTilde); inputMappingToggleConsole->AddSourceInput(inputMappingL3AndR3->GetInputChannelId()); inputMappingToggleConsole->AddSourceInput(InputDeviceTouch::Touch::Index4); m_inputContext.AddInputMapping(inputMappingToggleConsole); // Filter so the input context only listens for input events that can map to // 'ToggleDebugConsoleInputChannelId', and so the debug console only listens // for the custom 'ToggleDebugConsoleInputChannelId' id (optimization only). AZStd::shared_ptr inputFilter = AZStd::make_shared(); inputFilter->IncludeChannelName(InputDeviceGamepad::Button::L3.GetNameCrc32()); inputFilter->IncludeChannelName(InputDeviceGamepad::Button::R3.GetNameCrc32()); inputFilter->IncludeChannelName(inputMappingL3AndR3->GetInputChannelId().GetNameCrc32()); inputFilter->IncludeChannelName(InputDeviceKeyboard::Key::PunctuationTilde.GetNameCrc32()); inputFilter->IncludeChannelName(InputDeviceTouch::Touch::Index4.GetNameCrc32()); m_inputContext.SetFilter(inputFilter); inputFilter = AZStd::make_shared(); inputFilter->IncludeChannelName(inputMappingToggleConsole->GetInputChannelId().GetNameCrc32()); SetFilter(inputFilter); // Bind our custom log handler. AZ::ILogger* loggerInstance = AZ::Interface::Get(); AZ_Assert(loggerInstance, "Failed to get ILogger instance. Log handler not bound.") if (loggerInstance) { loggerInstance->BindLogHandler(m_logHandler); } // Connect to receive render tick events. auto atomViewportRequests = AZ::Interface::Get(); const AZ::Name contextName = atomViewportRequests->GetDefaultViewportContextName(); AZ::RPI::ViewportContextNotificationBus::Handler::BusConnect(contextName); } //////////////////////////////////////////////////////////////////////////////////////////////// DebugConsole::~DebugConsole() { // Disconnect to stop receiving render tick events. AZ::RPI::ViewportContextNotificationBus::Handler::BusDisconnect(); // Disconnect our custom log handler. m_logHandler.Disconnect(); } //////////////////////////////////////////////////////////////////////////////////////////////// void DebugConsole::OnRenderTick() { if (!m_isShowing) { return; } const bool continueShowing = DrawDebugConsole(); if (!continueShowing) { ToggleIsShowing(); } } //////////////////////////////////////////////////////////////////////////////////////////////// bool DebugConsole::OnInputChannelEventFiltered(const AzFramework::InputChannel& inputChannel) { if (inputChannel.GetInputChannelId() == ToggleDebugConsoleInputChannelId && inputChannel.IsStateBegan()) { ToggleIsShowing(); return true; } return false; } //////////////////////////////////////////////////////////////////////////////////////////////// void DebugConsole::AddDebugLog(const AZStd::string& debugLogString, const AZ::Color& color) { // Add the debug to our display, removing the oldest entry if we exceed the maximum. m_debugLogEntires.push_back(AZStd::make_pair(debugLogString, color)); if (m_debugLogEntires.size() > m_maxEntriesToDisplay) { m_debugLogEntires.pop_front(); } } //////////////////////////////////////////////////////////////////////////////////////////////// void DebugConsole::ClearDebugLog() { m_debugLogEntires.clear(); } //////////////////////////////////////////////////////////////////////////////////////////////// void ResetTextInputField(ImGuiInputTextCallbackData* data, const AZStd::string& newText) { data->DeleteChars(0, data->BufTextLen); data->InsertChars(0, newText.c_str()); } //////////////////////////////////////////////////////////////////////////////////////////////// void DebugConsole::AutoCompleteCommand(ImGuiInputTextCallbackData* data) { AZStd::vector matchingCommands; const AZStd::string longestMatchingSubstring = AZ::Interface::Get()->AutoCompleteCommand(data->Buf, &matchingCommands); ResetTextInputField(data, longestMatchingSubstring); // Auto complete options are logged in AutoCompleteCommand using AZLOG_INFO, // so if the log level is set higher we display auto complete options here. if (AZ::Interface::Get()->GetLogLevel() > AZ::LogLevel::Info) { if (matchingCommands.empty()) { AZStd::string noAutoCompletionResults("No auto completion options: "); noAutoCompletionResults += data->Buf; AddDebugLog(noAutoCompletionResults, AZ::Colors::Gray); } else if (matchingCommands.size() > 1) { AZStd::string autoCompletionResults("Auto completion options: "); autoCompletionResults += data->Buf; AddDebugLog(autoCompletionResults, AZ::Colors::Green); for (const AZStd::string& matchingCommand : matchingCommands) { AddDebugLog(matchingCommand, AZ::Colors::Green); } } } } //////////////////////////////////////////////////////////////////////////////////////////////// void DebugConsole::BrowseInputHistory(ImGuiInputTextCallbackData* data) { const int previousHistoryIndex = m_currentHistoryIndex; const int maxHistoryIndex = m_textInputHistory.size() - 1; switch (data->EventKey) { // Browse backwards through the history. case ImGuiKey_UpArrow: { if (m_currentHistoryIndex < 0) { // Go to the last history entry. m_currentHistoryIndex = maxHistoryIndex; } else if (m_currentHistoryIndex > 0) { // Go to the previous history entry. --m_currentHistoryIndex; } } break; // Browse forwards through the history. case ImGuiKey_DownArrow: { if (m_currentHistoryIndex >= 0 && m_currentHistoryIndex < maxHistoryIndex) { ++m_currentHistoryIndex; } } break; } if (previousHistoryIndex != m_currentHistoryIndex) { ResetTextInputField(data, m_textInputHistory[m_currentHistoryIndex]); } } //////////////////////////////////////////////////////////////////////////////////////////////// void DebugConsole::OnTextInputEntered(const char* inputText) { // Add the input text to our history, removing the oldest entry if we exceed the maximum. const AZStd::string inputTextString(inputText); m_textInputHistory.push_back(inputTextString); if (m_textInputHistory.size() > m_maxInputHistorySize) { m_textInputHistory.pop_front(); } // Clear the current history index; m_currentHistoryIndex = -1; // Attempt to perform a console command. AZ::Interface::Get()->PerformCommand(inputTextString.c_str()); } //////////////////////////////////////////////////////////////////////////////////////////////// int InputTextCallback(ImGuiInputTextCallbackData* data) { DebugConsole* debugConsole = static_cast(data->UserData); if (!debugConsole) { return 0; } switch (data->EventFlag) { case ImGuiInputTextFlags_CallbackCompletion: { debugConsole->AutoCompleteCommand(data); } break; case ImGuiInputTextFlags_CallbackHistory: { debugConsole->BrowseInputHistory(data); } break; } return 0; } //////////////////////////////////////////////////////////////////////////////////////////////// bool DebugConsole::DrawDebugConsole() { // Get the default ImGui pass. AZ::Render::ImGuiPass* defaultImGuiPass = nullptr; AZ::Render::ImGuiSystemRequestBus::BroadcastResult(defaultImGuiPass, &AZ::Render::ImGuiSystemRequestBus::Events::GetDefaultImGuiPass); if (!defaultImGuiPass) { return false; } // Create an ImGui context scope using the default ImGui pass context. ImGui::ImGuiContextScope contextScope(defaultImGuiPass->GetContext()); // Draw the debug console in a closeable, moveable, and resizeable IMGUI window. bool continueShowing = true; ImGui::SetNextWindowSize(ImVec2(640, 480), ImGuiCond_Once); if (!ImGui::Begin("Debug Console", &continueShowing, ImGuiWindowFlags_NoCollapse)) { ImGui::End(); return false; } // Show a scrolling child region in which to display all debug log entires. const float footerHeightToReserve = ImGui::GetStyle().ItemSpacing.y + ImGui::GetStyle().FramePadding.y + ImGui::GetFrameHeightWithSpacing(); ImGui::BeginChild("DebugLogEntriesScrollBox", ImVec2(0, -footerHeightToReserve), false, ImGuiWindowFlags_HorizontalScrollbar); { // Display each debug log entry individually so they can be colored. for (const auto& debugLogEntry : m_debugLogEntires) { const ImVec4 color(debugLogEntry.second.GetR(), debugLogEntry.second.GetG(), debugLogEntry.second.GetB(), debugLogEntry.second.GetA()); ImGui::PushStyleColor(ImGuiCol_Text, color); ImGui::TextUnformatted(debugLogEntry.first.c_str()); ImGui::PopStyleColor(); } // Scroll to the last debug log entry if needed. if (m_forceScroll || (m_autoScroll && ImGui::GetScrollY() >= ImGui::GetScrollMaxY())) { ImGui::SetScrollHereY(1.0f); m_forceScroll = false; } } ImGui::EndChild(); // Show a text input field. ImGui::Separator(); const ImGuiInputTextFlags inputTextFlags = ImGuiInputTextFlags_EnterReturnsTrue | ImGuiInputTextFlags_CallbackCompletion | ImGuiInputTextFlags_CallbackHistory; const bool textWasInput = ImGui::InputText("", m_inputBuffer, IM_ARRAYSIZE(m_inputBuffer), inputTextFlags, &InputTextCallback, (void*)this); if (textWasInput) { OnTextInputEntered(m_inputBuffer); azstrncpy(m_inputBuffer, IM_ARRAYSIZE(m_inputBuffer), "", 1); ImGui::SetKeyboardFocusHere(-1); m_forceScroll = true; } // Focus on the text input field. if (ImGui::IsWindowAppearing()) { ImGui::SetKeyboardFocusHere(-1); } ImGui::SetItemDefaultFocus(); // Show a button to clear the debug log. ImGui::SameLine(); if (ImGui::Button("Clear")) { ClearDebugLog(); } // Show an options menu. if (ImGui::BeginPopup("Options")) { // Show a combo box that controls the minimum log level (options correspond to AZ::LogLevel). ImGui::SetNextItemWidth((ImGui::CalcTextSize("WWWWWW").x + ImGui::GetStyle().FramePadding.x) * 2.0f); int logLevel = static_cast(AZ::Interface::Get()->GetLogLevel()); if (ImGui::Combo("Minimum Log Level", &logLevel, "All\0Trace\0Debug\0Info\0Notice\0Warn\0Error\0Fatal\0\0")) { logLevel = AZStd::clamp(logLevel, static_cast(AZ::LogLevel::Trace), static_cast(AZ::LogLevel::Fatal)); AZ::Interface::Get()->SetLogLevel(static_cast(logLevel)); } // Show a checkbox that controls whether to auto scroll when new debug log entires are added. ImGui::Checkbox("Auto Scroll New Log Entries", &m_autoScroll); ImGui::EndPopup(); } // Show a button to open the options menu. ImGui::SameLine(); if (ImGui::Button("Options")) { ImGui::OpenPopup("Options"); } ImGui::End(); return continueShowing; } //////////////////////////////////////////////////////////////////////////////////////////////// AzFramework::SystemCursorState GetDesiredSystemCursorState() { AZ::ApplicationTypeQuery applicationType; AZ::ComponentApplicationBus::Broadcast(&AZ::ComponentApplicationRequests::QueryApplicationType, applicationType); return applicationType.IsEditor() ? AzFramework::SystemCursorState::ConstrainedAndVisible : AzFramework::SystemCursorState::UnconstrainedAndVisible; } //////////////////////////////////////////////////////////////////////////////////////////////// void DebugConsole::ToggleIsShowing() { m_isShowing = !m_isShowing; if (m_isShowing) { AzFramework::InputSystemCursorRequestBus::EventResult(m_previousSystemCursorState, AzFramework::InputDeviceMouse::Id, &AzFramework::InputSystemCursorRequests::GetSystemCursorState); AzFramework::InputSystemCursorRequestBus::Event(AzFramework::InputDeviceMouse::Id, &AzFramework::InputSystemCursorRequests::SetSystemCursorState, GetDesiredSystemCursorState()); } else { AzFramework::InputSystemCursorRequestBus::Event(AzFramework::InputDeviceMouse::Id, &AzFramework::InputSystemCursorRequests::SetSystemCursorState, m_previousSystemCursorState); } } } #endif // defined(IMGUI_ENABLED)