DebugConsole.cpp 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355
  1. /*
  2. * Copyright (c) Contributors to the Open 3D Engine Project.
  3. * For complete copyright and license terms please see the LICENSE at the root of this distribution.
  4. *
  5. * SPDX-License-Identifier: Apache-2.0 OR MIT
  6. *
  7. */
  8. #include <DebugConsole.h>
  9. #if defined(IMGUI_ENABLED)
  10. #include <AzFramework/Input/Devices/Gamepad/InputDeviceGamepad.h>
  11. #include <AzFramework/Input/Devices/Keyboard/InputDeviceKeyboard.h>
  12. #include <AzFramework/Input/Devices/Mouse/InputDeviceMouse.h>
  13. #include <AzFramework/Input/Devices/Touch/InputDeviceTouch.h>
  14. #include <AzFramework/Input/Mappings/InputMappingAnd.h>
  15. #include <AzFramework/Input/Mappings/InputMappingOr.h>
  16. #include <AzCore/Console/IConsole.h>
  17. #include <AzCore/Interface/Interface.h>
  18. #include <Atom/Feature/ImGui/SystemBus.h>
  19. #include <ImGuiContextScope.h>
  20. #include <imgui/imgui.h>
  21. using namespace AzFramework;
  22. namespace AZ
  23. {
  24. AZ_CVAR(bool, bg_showDebugConsole, true, nullptr, AZ::ConsoleFunctorFlags::DontReplicate, "Enables or disables the debug console within imGui");
  25. AZ_CVAR(float, bg_defaultDebugConsoleWidth, 960.f, nullptr, AZ::ConsoleFunctorFlags::DontReplicate, "The default width for the imGui debug console");
  26. AZ_CVAR(float, bg_defaultDebugConsoleHeight, 480.f, nullptr, AZ::ConsoleFunctorFlags::DontReplicate, "The default height for the imGui debug console");
  27. AZ::Color GetColorForLogLevel(const AZ::LogLevel& logLevel)
  28. {
  29. switch (logLevel)
  30. {
  31. case AZ::LogLevel::Fatal:
  32. case AZ::LogLevel::Error:
  33. {
  34. return AZ::Colors::Red;
  35. }
  36. break;
  37. case AZ::LogLevel::Warn:
  38. {
  39. return AZ::Colors::Yellow;
  40. }
  41. break;
  42. case AZ::LogLevel::Notice:
  43. case AZ::LogLevel::Info:
  44. case AZ::LogLevel::Debug:
  45. case AZ::LogLevel::Trace:
  46. default:
  47. {
  48. return AZ::Colors::White;
  49. }
  50. break;
  51. }
  52. }
  53. void ResetTextInputField(ImGuiInputTextCallbackData* data, const AZStd::string& newText)
  54. {
  55. data->DeleteChars(0, data->BufTextLen);
  56. data->InsertChars(0, newText.c_str());
  57. }
  58. int InputTextCallback(ImGuiInputTextCallbackData* data)
  59. {
  60. DebugConsole* debugConsole = static_cast<DebugConsole*>(data->UserData);
  61. if (!debugConsole)
  62. {
  63. return 0;
  64. }
  65. switch (data->EventFlag)
  66. {
  67. case ImGuiInputTextFlags_CallbackCompletion:
  68. {
  69. debugConsole->AutoCompleteCommand(data);
  70. }
  71. break;
  72. case ImGuiInputTextFlags_CallbackHistory:
  73. {
  74. debugConsole->BrowseInputHistory(data);
  75. }
  76. break;
  77. }
  78. return 0;
  79. }
  80. DebugConsole::DebugConsole(int maxEntriesToDisplay, int maxInputHistorySize)
  81. : m_maxEntriesToDisplay(maxEntriesToDisplay)
  82. , m_maxInputHistorySize(maxInputHistorySize)
  83. {
  84. // The debug console is currently only supported when running the standalone launcher.
  85. // It does function correctly when running the editor if you remove this check, but it
  86. // conflicts with the legacy debug console that also shows at the bottom of the editor.
  87. AZ::ApplicationTypeQuery applicationType;
  88. AZ::ComponentApplicationBus::Broadcast(&AZ::ComponentApplicationRequests::QueryApplicationType, applicationType);
  89. if (!applicationType.IsGame())
  90. {
  91. return;
  92. }
  93. ImGui::ImGuiUpdateListenerBus::Handler::BusConnect();
  94. AZ::Debug::TraceMessageBus::Handler::BusConnect();
  95. }
  96. DebugConsole::~DebugConsole()
  97. {
  98. AZ::Debug::TraceMessageBus::Handler::BusDisconnect();
  99. ImGui::ImGuiUpdateListenerBus::Handler::BusDisconnect();
  100. }
  101. void DebugConsole::OnImGuiMainMenuUpdate()
  102. {
  103. }
  104. void DebugConsole::OnImGuiUpdate()
  105. {
  106. if (!bg_showDebugConsole)
  107. {
  108. return;
  109. }
  110. // Draw the debug console in a closeable, moveable, and resizeable IMGUI window.
  111. bool continueShowing = bg_showDebugConsole;
  112. ImGui::SetNextWindowSize(ImVec2(bg_defaultDebugConsoleWidth, bg_defaultDebugConsoleHeight), ImGuiCond_Once);
  113. if (!ImGui::Begin("Debug Console", &continueShowing))
  114. {
  115. ImGui::End();
  116. return;
  117. }
  118. bg_showDebugConsole = continueShowing;
  119. // Show a scrolling child region in which to display all debug log entires.
  120. const float footerHeightToReserve = ImGui::GetStyle().ItemSpacing.y + ImGui::GetStyle().FramePadding.y + ImGui::GetFrameHeightWithSpacing();
  121. ImGui::BeginChild("DebugLogEntriesScrollBox", ImVec2(0, -footerHeightToReserve), false, ImGuiWindowFlags_HorizontalScrollbar);
  122. {
  123. // Display each debug log entry individually so they can be colored.
  124. for (const auto& debugLogEntry : m_debugLogEntires)
  125. {
  126. const ImVec4 color(debugLogEntry.second.GetR(),
  127. debugLogEntry.second.GetG(),
  128. debugLogEntry.second.GetB(),
  129. debugLogEntry.second.GetA());
  130. ImGui::PushStyleColor(ImGuiCol_Text, color);
  131. ImGui::TextUnformatted(debugLogEntry.first.c_str());
  132. ImGui::PopStyleColor();
  133. }
  134. // Scroll to the last debug log entry if needed.
  135. if (m_forceScroll || (m_autoScroll && ImGui::GetScrollY() >= ImGui::GetScrollMaxY()))
  136. {
  137. ImGui::SetScrollHereY(1.0f);
  138. m_forceScroll = false;
  139. }
  140. }
  141. ImGui::EndChild();
  142. // Show a text input field.
  143. ImGui::Separator();
  144. const ImGuiInputTextFlags inputTextFlags = ImGuiInputTextFlags_EnterReturnsTrue | ImGuiInputTextFlags_CallbackCompletion | ImGuiInputTextFlags_CallbackHistory;
  145. const bool textWasInput = ImGui::InputText("", m_inputBuffer, IM_ARRAYSIZE(m_inputBuffer), inputTextFlags, &InputTextCallback, (void*)this);
  146. if (textWasInput)
  147. {
  148. OnTextInputEntered(m_inputBuffer);
  149. azstrncpy(m_inputBuffer, IM_ARRAYSIZE(m_inputBuffer), "", 1);
  150. ImGui::SetKeyboardFocusHere(-1);
  151. m_forceScroll = true;
  152. }
  153. // Focus on the text input field.
  154. if (ImGui::IsWindowAppearing())
  155. {
  156. ImGui::SetKeyboardFocusHere(-1);
  157. }
  158. ImGui::SetItemDefaultFocus();
  159. // Show a button to clear the debug log.
  160. ImGui::SameLine();
  161. if (ImGui::Button("Clear"))
  162. {
  163. ClearDebugLog();
  164. }
  165. // Show an options menu.
  166. if (ImGui::BeginPopup("Options"))
  167. {
  168. // Show a combo box that controls the minimum log level (options correspond to AZ::LogLevel).
  169. ImGui::SetNextItemWidth((ImGui::CalcTextSize("WWWWWW").x + ImGui::GetStyle().FramePadding.x) * 2.0f);
  170. int logLevel = static_cast<int>(AZ::Interface<AZ::ILogger>::Get()->GetLogLevel());
  171. if (ImGui::Combo("Minimum Log Level", &logLevel, "All\0Trace\0Debug\0Info\0Notice\0Warn\0Error\0Fatal\0\0"))
  172. {
  173. logLevel = AZStd::clamp(logLevel, static_cast<int>(AZ::LogLevel::Trace), static_cast<int>(AZ::LogLevel::Fatal));
  174. AZ::Interface<AZ::ILogger>::Get()->SetLogLevel(static_cast<AZ::LogLevel>(logLevel));
  175. }
  176. // Show a checkbox that controls whether to auto scroll when new debug log entires are added.
  177. ImGui::Checkbox("Auto Scroll New Log Entries", &m_autoScroll);
  178. ImGui::EndPopup();
  179. }
  180. // Show a button to open the options menu.
  181. ImGui::SameLine();
  182. if (ImGui::Button("Options"))
  183. {
  184. ImGui::OpenPopup("Options");
  185. }
  186. ImGui::End();
  187. }
  188. void DebugConsole::AddDebugLog(const AZStd::string& debugLogString, const AZ::Color& color)
  189. {
  190. // Add the debug to our display, removing the oldest entry if we exceed the maximum.
  191. m_debugLogEntires.push_back(AZStd::make_pair(debugLogString, color));
  192. if (m_debugLogEntires.size() > m_maxEntriesToDisplay)
  193. {
  194. m_debugLogEntires.pop_front();
  195. }
  196. }
  197. void DebugConsole::AddDebugLog(const char* window, const char* debugLogString, AZ::LogLevel logLevel)
  198. {
  199. AZ::ILogger* logger = AZ::Interface<AZ::ILogger>::Get();
  200. if (logger == nullptr || logLevel < logger->GetLogLevel())
  201. {
  202. return;
  203. }
  204. AZ::Color color = GetColorForLogLevel(logLevel);
  205. if (strcmp(window, AZ::Debug::Trace::GetDefaultSystemWindow()) == 0)
  206. {
  207. AddDebugLog(debugLogString, color);
  208. }
  209. else
  210. {
  211. AddDebugLog(AZStd::string::format("(%s) - %s", window, debugLogString), color);
  212. }
  213. }
  214. void DebugConsole::ClearDebugLog()
  215. {
  216. m_debugLogEntires.clear();
  217. }
  218. bool DebugConsole::OnPreError(const char* window, [[maybe_unused]] const char* fileName, [[maybe_unused]] int line, [[maybe_unused]] const char* func, const char* message)
  219. {
  220. AddDebugLog(window, message, AZ::LogLevel::Error);
  221. return false;
  222. }
  223. bool DebugConsole::OnPreWarning(const char* window, [[maybe_unused]] const char* fileName, [[maybe_unused]] int line, [[maybe_unused]] const char* func, const char* message)
  224. {
  225. AddDebugLog(window, message, AZ::LogLevel::Warn);
  226. return false;
  227. }
  228. bool DebugConsole::OnPrintf(const char* window, const char* message)
  229. {
  230. AddDebugLog(window, message, AZ::LogLevel::Notice); // Notice is one level below warning
  231. return false;
  232. }
  233. void DebugConsole::AutoCompleteCommand(ImGuiInputTextCallbackData* data)
  234. {
  235. AZStd::vector<AZStd::string> matchingCommands;
  236. const AZStd::string longestMatchingSubstring = AZ::Interface<AZ::IConsole>::Get()->AutoCompleteCommand(data->Buf, &matchingCommands);
  237. ResetTextInputField(data, longestMatchingSubstring);
  238. // Auto complete options are logged in AutoCompleteCommand using AZLOG_INFO,
  239. // so if the log level is set higher we display auto complete options here.
  240. if (AZ::Interface<AZ::ILogger>::Get()->GetLogLevel() > AZ::LogLevel::Info)
  241. {
  242. if (matchingCommands.empty())
  243. {
  244. AZStd::string noAutoCompletionResults("No auto completion options: ");
  245. noAutoCompletionResults += data->Buf;
  246. AddDebugLog(noAutoCompletionResults, AZ::Colors::Gray);
  247. }
  248. else if (matchingCommands.size() > 1)
  249. {
  250. AZStd::string autoCompletionResults("Auto completion options: ");
  251. autoCompletionResults += data->Buf;
  252. AddDebugLog(autoCompletionResults, AZ::Colors::Green);
  253. for (const AZStd::string& matchingCommand : matchingCommands)
  254. {
  255. AddDebugLog(matchingCommand, AZ::Colors::Green);
  256. }
  257. }
  258. }
  259. }
  260. void DebugConsole::BrowseInputHistory(ImGuiInputTextCallbackData* data)
  261. {
  262. const int previousHistoryIndex = m_currentHistoryIndex;
  263. const int maxHistoryIndex = static_cast<int>(m_textInputHistory.size() - 1);
  264. switch (data->EventKey)
  265. {
  266. // Browse backwards through the history.
  267. case ImGuiKey_UpArrow:
  268. {
  269. if (m_currentHistoryIndex < 0)
  270. {
  271. // Go to the last history entry.
  272. m_currentHistoryIndex = maxHistoryIndex;
  273. }
  274. else if (m_currentHistoryIndex > 0)
  275. {
  276. // Go to the previous history entry.
  277. --m_currentHistoryIndex;
  278. }
  279. }
  280. break;
  281. // Browse forwards through the history.
  282. case ImGuiKey_DownArrow:
  283. {
  284. if (m_currentHistoryIndex >= 0 && m_currentHistoryIndex < maxHistoryIndex)
  285. {
  286. ++m_currentHistoryIndex;
  287. }
  288. }
  289. break;
  290. }
  291. if (previousHistoryIndex != m_currentHistoryIndex)
  292. {
  293. ResetTextInputField(data, m_textInputHistory[m_currentHistoryIndex]);
  294. }
  295. }
  296. void DebugConsole::OnTextInputEntered(const char* inputText)
  297. {
  298. // Add the input text to our history, removing the oldest entry if we exceed the maximum.
  299. const AZStd::string inputTextString(inputText);
  300. m_textInputHistory.push_back(inputTextString);
  301. if (m_textInputHistory.size() > m_maxInputHistorySize)
  302. {
  303. m_textInputHistory.pop_front();
  304. }
  305. // Clear the current history index;
  306. m_currentHistoryIndex = -1;
  307. // Attempt to perform a console command.
  308. AZ::Interface<AZ::IConsole>::Get()->PerformCommand(inputTextString.c_str());
  309. }
  310. }
  311. #endif // defined(IMGUI_ENABLED)