You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
449 lines
19 KiB
C++
449 lines
19 KiB
C++
/*
|
|
* Copyright (c) Contributors to the Open 3D Engine Project.
|
|
* For complete copyright and license terms please see the LICENSE at the root of this distribution.
|
|
*
|
|
* SPDX-License-Identifier: Apache-2.0 OR MIT
|
|
*
|
|
*/
|
|
|
|
#include <DebugConsole.h>
|
|
|
|
#if defined(IMGUI_ENABLED)
|
|
|
|
#include <AzFramework/Input/Devices/Gamepad/InputDeviceGamepad.h>
|
|
#include <AzFramework/Input/Devices/Keyboard/InputDeviceKeyboard.h>
|
|
#include <AzFramework/Input/Devices/Mouse/InputDeviceMouse.h>
|
|
#include <AzFramework/Input/Devices/Touch/InputDeviceTouch.h>
|
|
#include <AzFramework/Input/Mappings/InputMappingAnd.h>
|
|
#include <AzFramework/Input/Mappings/InputMappingOr.h>
|
|
|
|
#include <AzCore/Console/IConsole.h>
|
|
#include <AzCore/Interface/Interface.h>
|
|
|
|
#include <Atom/Feature/ImGui/SystemBus.h>
|
|
#include <ImGuiContextScope.h>
|
|
#include <ImGui/ImGuiPass.h>
|
|
#include <imgui/imgui.h>
|
|
|
|
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<InputMappingAnd> inputMappingL3AndR3 = AZStd::make_shared<InputMappingAnd>(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<InputMappingOr> inputMappingToggleConsole = AZStd::make_shared<InputMappingOr>(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<InputChannelEventFilterInclusionList> inputFilter = AZStd::make_shared<InputChannelEventFilterInclusionList>();
|
|
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<InputChannelEventFilterInclusionList>();
|
|
inputFilter->IncludeChannelName(inputMappingToggleConsole->GetInputChannelId().GetNameCrc32());
|
|
SetFilter(inputFilter);
|
|
|
|
// Bind our custom log handler.
|
|
AZ::ILogger* loggerInstance = AZ::Interface<AZ::ILogger>::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<AZ::RPI::ViewportContextRequestsInterface>::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<AZStd::string, AZ::Color>(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<AZStd::string> matchingCommands;
|
|
const AZStd::string longestMatchingSubstring = AZ::Interface<AZ::IConsole>::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<AZ::ILogger>::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 = static_cast<int>(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<AZ::IConsole>::Get()->PerformCommand(inputTextString.c_str());
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////
|
|
int InputTextCallback(ImGuiInputTextCallbackData* data)
|
|
{
|
|
DebugConsole* debugConsole = static_cast<DebugConsole*>(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<int>(AZ::Interface<AZ::ILogger>::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<int>(AZ::LogLevel::Trace), static_cast<int>(AZ::LogLevel::Fatal));
|
|
AZ::Interface<AZ::ILogger>::Get()->SetLogLevel(static_cast<AZ::LogLevel>(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)
|