feat: overhaul editor console panel and diagnostics
This commit is contained in:
@@ -9,15 +9,33 @@ namespace XCEngine {
|
||||
namespace Editor {
|
||||
namespace Actions {
|
||||
|
||||
inline ActionBinding MakeConsoleCollapseAction(bool active, bool enabled = true) {
|
||||
return MakeAction("Collapse", nullptr, active, enabled);
|
||||
}
|
||||
|
||||
inline ActionBinding MakeConsoleClearOnPlayAction(bool active, bool enabled = true) {
|
||||
return MakeAction("Clear on Play", nullptr, active, enabled);
|
||||
}
|
||||
|
||||
inline ActionBinding MakeConsoleErrorPauseAction(bool active, bool enabled = true) {
|
||||
return MakeAction("Error Pause", nullptr, active, enabled);
|
||||
}
|
||||
|
||||
inline void DrawConsoleToolbarActions(Debug::EditorConsoleSink& sink, UI::ConsoleFilterState& filterState) {
|
||||
if (DrawToolbarAction(MakeClearConsoleAction())) {
|
||||
sink.Clear();
|
||||
}
|
||||
|
||||
ImGui::SameLine();
|
||||
DrawToolbarToggleAction(MakeConsoleCollapseAction(filterState.Collapse()), filterState.Collapse());
|
||||
ImGui::SameLine();
|
||||
DrawToolbarToggleAction(MakeConsoleClearOnPlayAction(filterState.ClearOnPlay()), filterState.ClearOnPlay());
|
||||
ImGui::SameLine();
|
||||
DrawToolbarToggleAction(MakeConsoleErrorPauseAction(filterState.ErrorPause()), filterState.ErrorPause());
|
||||
ImGui::SameLine();
|
||||
UI::DrawToolbarLabel("Filter");
|
||||
ImGui::SameLine();
|
||||
DrawToolbarToggleAction(MakeConsoleInfoFilterAction(filterState.ShowInfo()), filterState.ShowInfo());
|
||||
DrawToolbarToggleAction(MakeConsoleInfoFilterAction(filterState.ShowLog()), filterState.ShowLog());
|
||||
ImGui::SameLine();
|
||||
DrawToolbarToggleAction(MakeConsoleWarningFilterAction(filterState.ShowWarning()), filterState.ShowWarning());
|
||||
ImGui::SameLine();
|
||||
|
||||
@@ -21,13 +21,18 @@ EditorConsoleSink::~EditorConsoleSink() {
|
||||
}
|
||||
|
||||
void EditorConsoleSink::Log(const LogEntry& entry) {
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
if (m_logs.size() >= MAX_LOGS) {
|
||||
m_logs.erase(m_logs.begin());
|
||||
std::function<void()> callback;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
if (m_logs.size() >= MAX_LOGS) {
|
||||
m_logs.erase(m_logs.begin());
|
||||
}
|
||||
m_logs.push_back(EditorConsoleRecord{ m_nextSerial++, entry });
|
||||
++m_revision;
|
||||
callback = m_callback;
|
||||
}
|
||||
m_logs.push_back(entry);
|
||||
if (m_callback) {
|
||||
m_callback();
|
||||
if (callback) {
|
||||
callback();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,16 +40,43 @@ void EditorConsoleSink::Flush() {
|
||||
}
|
||||
|
||||
std::vector<LogEntry> EditorConsoleSink::GetLogs() const {
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
std::vector<LogEntry> logs;
|
||||
logs.reserve(m_logs.size());
|
||||
for (const EditorConsoleRecord& record : m_logs) {
|
||||
logs.push_back(record.entry);
|
||||
}
|
||||
return logs;
|
||||
}
|
||||
|
||||
std::vector<EditorConsoleRecord> EditorConsoleSink::GetRecords() const {
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
return m_logs;
|
||||
}
|
||||
|
||||
void EditorConsoleSink::Clear() {
|
||||
uint64_t EditorConsoleSink::GetRevision() const {
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
m_logs.clear();
|
||||
return m_revision;
|
||||
}
|
||||
|
||||
void EditorConsoleSink::Clear() {
|
||||
std::function<void()> callback;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
if (m_logs.empty()) {
|
||||
return;
|
||||
}
|
||||
m_logs.clear();
|
||||
++m_revision;
|
||||
callback = m_callback;
|
||||
}
|
||||
if (callback) {
|
||||
callback();
|
||||
}
|
||||
}
|
||||
|
||||
void EditorConsoleSink::SetCallback(std::function<void()> callback) {
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
m_callback = std::move(callback);
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,11 @@
|
||||
namespace XCEngine {
|
||||
namespace Debug {
|
||||
|
||||
struct EditorConsoleRecord {
|
||||
uint64_t serial = 0;
|
||||
LogEntry entry = {};
|
||||
};
|
||||
|
||||
class EditorConsoleSink : public ILogSink {
|
||||
public:
|
||||
static EditorConsoleSink* GetInstance();
|
||||
@@ -20,13 +25,17 @@ public:
|
||||
void Flush() override;
|
||||
|
||||
std::vector<LogEntry> GetLogs() const;
|
||||
std::vector<EditorConsoleRecord> GetRecords() const;
|
||||
uint64_t GetRevision() const;
|
||||
void Clear();
|
||||
void SetCallback(std::function<void()> callback);
|
||||
|
||||
private:
|
||||
mutable std::mutex m_mutex;
|
||||
std::vector<LogEntry> m_logs;
|
||||
std::vector<EditorConsoleRecord> m_logs;
|
||||
std::function<void()> m_callback;
|
||||
uint64_t m_nextSerial = 1;
|
||||
uint64_t m_revision = 0;
|
||||
static EditorConsoleSink* s_instance;
|
||||
static constexpr size_t MAX_LOGS = 1000;
|
||||
};
|
||||
|
||||
@@ -2,6 +2,9 @@
|
||||
|
||||
#include "Platform/Win32Utf8.h"
|
||||
|
||||
#ifdef _MSC_VER
|
||||
#include <crtdbg.h>
|
||||
#endif
|
||||
#include <dbghelp.h>
|
||||
#include <stdio.h>
|
||||
#include <string>
|
||||
@@ -15,6 +18,46 @@ inline std::string GetExecutableLogPath(const char* fileName) {
|
||||
return GetExecutableDirectoryUtf8() + "\\" + fileName;
|
||||
}
|
||||
|
||||
#ifdef _MSC_VER
|
||||
inline void WriteInvalidParameterReport(
|
||||
const wchar_t* expression,
|
||||
const wchar_t* function,
|
||||
const wchar_t* file,
|
||||
unsigned int line) {
|
||||
const std::string logPath = GetExecutableLogPath("crash.log");
|
||||
FILE* logFile = nullptr;
|
||||
fopen_s(&logFile, logPath.c_str(), "a");
|
||||
if (logFile != nullptr) {
|
||||
fwprintf(
|
||||
logFile,
|
||||
L"[CRT] Invalid parameter. function=%s file=%s line=%u expression=%s\n",
|
||||
function != nullptr ? function : L"(null)",
|
||||
file != nullptr ? file : L"(null)",
|
||||
line,
|
||||
expression != nullptr ? expression : L"(null)");
|
||||
fclose(logFile);
|
||||
}
|
||||
|
||||
fwprintf(
|
||||
stderr,
|
||||
L"[CRT] Invalid parameter. function=%s file=%s line=%u expression=%s\n",
|
||||
function != nullptr ? function : L"(null)",
|
||||
file != nullptr ? file : L"(null)",
|
||||
line,
|
||||
expression != nullptr ? expression : L"(null)");
|
||||
fflush(stderr);
|
||||
}
|
||||
|
||||
inline void InvalidParameterHandler(
|
||||
const wchar_t* expression,
|
||||
const wchar_t* function,
|
||||
const wchar_t* file,
|
||||
unsigned int line,
|
||||
uintptr_t) {
|
||||
WriteInvalidParameterReport(expression, function, file, line);
|
||||
}
|
||||
#endif
|
||||
|
||||
inline void WriteCrashStackTrace(FILE* file) {
|
||||
if (file == nullptr) {
|
||||
return;
|
||||
@@ -84,11 +127,24 @@ inline LONG WINAPI CrashExceptionFilter(EXCEPTION_POINTERS* exceptionPointers) {
|
||||
|
||||
inline void InstallCrashExceptionFilter() {
|
||||
SetUnhandledExceptionFilter(CrashExceptionFilter);
|
||||
|
||||
#ifdef _MSC_VER
|
||||
_set_invalid_parameter_handler(InvalidParameterHandler);
|
||||
#endif
|
||||
}
|
||||
|
||||
inline void RedirectStderrToExecutableLog() {
|
||||
const std::string stderrPath = GetExecutableLogPath("stderr.log");
|
||||
freopen(stderrPath.c_str(), "w", stderr);
|
||||
|
||||
#ifdef _MSC_VER
|
||||
_CrtSetReportMode(_CRT_WARN, _CRTDBG_MODE_FILE);
|
||||
_CrtSetReportFile(_CRT_WARN, _CRTDBG_FILE_STDERR);
|
||||
_CrtSetReportMode(_CRT_ERROR, _CRTDBG_MODE_FILE);
|
||||
_CrtSetReportFile(_CRT_ERROR, _CRTDBG_FILE_STDERR);
|
||||
_CrtSetReportMode(_CRT_ASSERT, _CRTDBG_MODE_FILE);
|
||||
_CrtSetReportFile(_CRT_ASSERT, _CRTDBG_FILE_STDERR);
|
||||
#endif
|
||||
}
|
||||
|
||||
} // namespace Platform
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include <XCEngine/Debug/LogEntry.h>
|
||||
#include <XCEngine/Debug/LogLevel.h>
|
||||
|
||||
#include <filesystem>
|
||||
#include <string>
|
||||
|
||||
namespace XCEngine {
|
||||
@@ -26,11 +27,77 @@ inline const char* ConsoleLogPrefix(::XCEngine::Debug::LogLevel level) {
|
||||
return "[LOG] ";
|
||||
}
|
||||
|
||||
inline const char* ConsoleSeverityLabel(::XCEngine::Debug::LogLevel level) {
|
||||
switch (level) {
|
||||
case ::XCEngine::Debug::LogLevel::Verbose:
|
||||
case ::XCEngine::Debug::LogLevel::Debug:
|
||||
case ::XCEngine::Debug::LogLevel::Info:
|
||||
return "Log";
|
||||
case ::XCEngine::Debug::LogLevel::Warning:
|
||||
return "Warning";
|
||||
case ::XCEngine::Debug::LogLevel::Error:
|
||||
case ::XCEngine::Debug::LogLevel::Fatal:
|
||||
return "Error";
|
||||
}
|
||||
|
||||
return "Log";
|
||||
}
|
||||
|
||||
inline std::string BuildConsoleSummaryText(const ::XCEngine::Debug::LogEntry& log) {
|
||||
return log.message.CStr();
|
||||
}
|
||||
|
||||
inline std::string BuildConsoleSourceText(const ::XCEngine::Debug::LogEntry& log) {
|
||||
std::string source;
|
||||
const char* category = ::XCEngine::Debug::LogCategoryToString(log.category);
|
||||
if (category && category[0] != '\0') {
|
||||
source += "[";
|
||||
source += category;
|
||||
source += "]";
|
||||
}
|
||||
|
||||
const std::string filePath = log.file.CStr();
|
||||
if (!filePath.empty()) {
|
||||
if (!source.empty()) {
|
||||
source += " ";
|
||||
}
|
||||
|
||||
source += std::filesystem::path(filePath).filename().string();
|
||||
if (log.line > 0) {
|
||||
source += ":";
|
||||
source += std::to_string(log.line);
|
||||
}
|
||||
}
|
||||
|
||||
return source;
|
||||
}
|
||||
|
||||
inline std::string BuildConsoleLogText(const ::XCEngine::Debug::LogEntry& log) {
|
||||
const char* category = ::XCEngine::Debug::LogCategoryToString(log.category);
|
||||
return std::string(ConsoleLogPrefix(log.level)) + "[" + category + "] " + log.message.CStr();
|
||||
}
|
||||
|
||||
inline std::string BuildConsoleCopyText(const ::XCEngine::Debug::LogEntry& log) {
|
||||
std::string text = BuildConsoleLogText(log);
|
||||
if (log.file.Length() > 0) {
|
||||
text += "\nFile: ";
|
||||
text += log.file.CStr();
|
||||
if (log.line > 0) {
|
||||
text += ":";
|
||||
text += std::to_string(log.line);
|
||||
}
|
||||
}
|
||||
if (log.function.Length() > 0) {
|
||||
text += "\nFunction: ";
|
||||
text += log.function.CStr();
|
||||
}
|
||||
text += "\nThread: ";
|
||||
text += std::to_string(log.threadId);
|
||||
text += "\nTimestamp: ";
|
||||
text += std::to_string(log.timestamp);
|
||||
return text;
|
||||
}
|
||||
|
||||
} // namespace UI
|
||||
} // namespace Editor
|
||||
} // namespace XCEngine
|
||||
|
||||
@@ -39,24 +39,24 @@ inline void DrawDisclosureArrow(ImDrawList* drawList, const ImVec2& min, const I
|
||||
return;
|
||||
}
|
||||
|
||||
constexpr float kSqrt3 = 1.7320508f;
|
||||
const ImVec2 center(
|
||||
static_cast<float>(std::floor((min.x + max.x) * 0.5f)) + 0.5f,
|
||||
static_cast<float>(std::floor((min.y + max.y) * 0.5f)) + 0.5f);
|
||||
const float width = max.x - min.x;
|
||||
const float height = max.y - min.y;
|
||||
const float radius = (std::max)(
|
||||
const float halfBaseExtent = (std::max)(
|
||||
3.0f,
|
||||
static_cast<float>(std::floor((width < height ? width : height) * DisclosureArrowScale())));
|
||||
const float baseHalfExtent = radius;
|
||||
const float tipExtent = (std::max)(2.0f, static_cast<float>(std::floor(radius * 0.70f)));
|
||||
if (baseHalfExtent < 1.0f || tipExtent < 1.0f) {
|
||||
const float triangleHeight = kSqrt3 * halfBaseExtent;
|
||||
if (halfBaseExtent < 1.0f || triangleHeight < 1.0f) {
|
||||
return;
|
||||
}
|
||||
|
||||
ImVec2 points[3] = {
|
||||
ImVec2(-baseHalfExtent, -tipExtent),
|
||||
ImVec2(baseHalfExtent, -tipExtent),
|
||||
ImVec2(0.0f, tipExtent)
|
||||
ImVec2(-halfBaseExtent, -triangleHeight / 3.0f),
|
||||
ImVec2(halfBaseExtent, -triangleHeight / 3.0f),
|
||||
ImVec2(0.0f, triangleHeight * 2.0f / 3.0f)
|
||||
};
|
||||
|
||||
if (!open) {
|
||||
|
||||
@@ -269,7 +269,7 @@ inline float NavigationTreePrefixLabelGap() {
|
||||
}
|
||||
|
||||
inline float DisclosureArrowScale() {
|
||||
return 0.28f;
|
||||
return 0.18f;
|
||||
}
|
||||
|
||||
inline ImVec4 NavigationTreePrefixColor(bool selected = false, bool hovered = false) {
|
||||
@@ -704,6 +704,74 @@ inline ImVec4 ConsoleRowHoverFillColor() {
|
||||
return ImVec4(1.0f, 1.0f, 1.0f, 0.03f);
|
||||
}
|
||||
|
||||
inline ImVec4 ConsoleRowSelectedFillColor() {
|
||||
return ImVec4(0.26f, 0.33f, 0.43f, 0.95f);
|
||||
}
|
||||
|
||||
inline ImVec4 ConsoleListBackgroundColor() {
|
||||
return ImVec4(0.205f, 0.205f, 0.205f, 1.0f);
|
||||
}
|
||||
|
||||
inline ImVec4 ConsoleDetailsBackgroundColor() {
|
||||
return ImVec4(0.185f, 0.185f, 0.185f, 1.0f);
|
||||
}
|
||||
|
||||
inline ImVec4 ConsoleDetailsHeaderBackgroundColor() {
|
||||
return ImVec4(0.19f, 0.19f, 0.19f, 1.0f);
|
||||
}
|
||||
|
||||
inline ImVec4 ConsoleSecondaryTextColor() {
|
||||
return ImVec4(0.57f, 0.57f, 0.57f, 1.0f);
|
||||
}
|
||||
|
||||
inline ImVec4 ConsoleCountBadgeBackgroundColor() {
|
||||
return ImVec4(0.29f, 0.29f, 0.29f, 1.0f);
|
||||
}
|
||||
|
||||
inline ImVec4 ConsoleCountBadgeTextColor() {
|
||||
return ImVec4(0.88f, 0.88f, 0.88f, 1.0f);
|
||||
}
|
||||
|
||||
inline ImVec4 ConsoleLogColor() {
|
||||
return ImVec4(0.77f, 0.77f, 0.77f, 1.0f);
|
||||
}
|
||||
|
||||
inline ImVec4 ConsoleWarningColor() {
|
||||
return ImVec4(0.93f, 0.72f, 0.26f, 1.0f);
|
||||
}
|
||||
|
||||
inline ImVec4 ConsoleErrorColor() {
|
||||
return ImVec4(0.92f, 0.37f, 0.33f, 1.0f);
|
||||
}
|
||||
|
||||
inline float ConsoleRowHeight() {
|
||||
return 22.0f;
|
||||
}
|
||||
|
||||
inline float ConsoleSeverityButtonMinWidth() {
|
||||
return 54.0f;
|
||||
}
|
||||
|
||||
inline ImVec2 ConsoleSeverityButtonPadding() {
|
||||
return ImVec2(8.0f, 4.0f);
|
||||
}
|
||||
|
||||
inline float ConsoleBadgeRounding() {
|
||||
return 3.0f;
|
||||
}
|
||||
|
||||
inline float ConsoleDetailsDefaultHeight() {
|
||||
return 168.0f;
|
||||
}
|
||||
|
||||
inline float ConsoleDetailsMinHeight() {
|
||||
return 92.0f;
|
||||
}
|
||||
|
||||
inline float ConsoleListMinHeight() {
|
||||
return 96.0f;
|
||||
}
|
||||
|
||||
inline float MenuBarStatusRightPadding() {
|
||||
return 20.0f;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,481 @@
|
||||
#include "Actions/ActionRouting.h"
|
||||
#include "Actions/ConsoleActionRouter.h"
|
||||
#include "ConsolePanel.h"
|
||||
#include "Core/EditorConsoleSink.h"
|
||||
#include "Core/EditorEvents.h"
|
||||
#include "Core/EventBus.h"
|
||||
#include "Core/IEditorContext.h"
|
||||
#include "Platform/Win32Utf8.h"
|
||||
#include "UI/UI.h"
|
||||
|
||||
#include <imgui.h>
|
||||
#include <shellapi.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <cmath>
|
||||
#include <ctime>
|
||||
#include <filesystem>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
namespace {
|
||||
|
||||
using XCEngine::Debug::EditorConsoleRecord;
|
||||
using XCEngine::Debug::EditorConsoleSink;
|
||||
using XCEngine::Debug::LogEntry;
|
||||
using XCEngine::Debug::LogLevel;
|
||||
|
||||
enum class ConsoleSeverityVisual {
|
||||
Log,
|
||||
Warning,
|
||||
Error
|
||||
};
|
||||
|
||||
struct ConsoleSeverityCounts {
|
||||
size_t logCount = 0;
|
||||
size_t warningCount = 0;
|
||||
size_t errorCount = 0;
|
||||
};
|
||||
|
||||
struct ConsoleRowData {
|
||||
uint64_t serial = 0;
|
||||
LogEntry entry = {};
|
||||
std::string entryKey;
|
||||
size_t count = 1;
|
||||
};
|
||||
|
||||
struct ConsoleRowInteraction {
|
||||
bool clicked = false;
|
||||
bool rightClicked = false;
|
||||
bool doubleClicked = false;
|
||||
};
|
||||
|
||||
ConsoleSeverityVisual ResolveSeverity(LogLevel level) {
|
||||
switch (level) {
|
||||
case LogLevel::Warning:
|
||||
return ConsoleSeverityVisual::Warning;
|
||||
case LogLevel::Error:
|
||||
case LogLevel::Fatal:
|
||||
return ConsoleSeverityVisual::Error;
|
||||
case LogLevel::Verbose:
|
||||
case LogLevel::Debug:
|
||||
case LogLevel::Info:
|
||||
default:
|
||||
return ConsoleSeverityVisual::Log;
|
||||
}
|
||||
}
|
||||
|
||||
ImVec4 SeverityColor(ConsoleSeverityVisual severity) {
|
||||
switch (severity) {
|
||||
case ConsoleSeverityVisual::Warning:
|
||||
return XCEngine::Editor::UI::ConsoleWarningColor();
|
||||
case ConsoleSeverityVisual::Error:
|
||||
return XCEngine::Editor::UI::ConsoleErrorColor();
|
||||
case ConsoleSeverityVisual::Log:
|
||||
default:
|
||||
return XCEngine::Editor::UI::ConsoleLogColor();
|
||||
}
|
||||
}
|
||||
|
||||
bool IsErrorLevel(LogLevel level) {
|
||||
return level == LogLevel::Error || level == LogLevel::Fatal;
|
||||
}
|
||||
|
||||
std::string ToLowerCopy(std::string text) {
|
||||
std::transform(text.begin(), text.end(), text.begin(), [](unsigned char ch) {
|
||||
return static_cast<char>(std::tolower(ch));
|
||||
});
|
||||
return text;
|
||||
}
|
||||
|
||||
std::string BuildEntryKey(const LogEntry& entry) {
|
||||
std::string key;
|
||||
key.reserve(128 + entry.message.Length() + entry.file.Length() + entry.function.Length());
|
||||
key += std::to_string(static_cast<int>(entry.level));
|
||||
key.push_back('\x1f');
|
||||
key += std::to_string(static_cast<int>(entry.category));
|
||||
key.push_back('\x1f');
|
||||
key += entry.message.CStr();
|
||||
key.push_back('\x1f');
|
||||
key += entry.file.CStr();
|
||||
key.push_back('\x1f');
|
||||
key += std::to_string(entry.line);
|
||||
key.push_back('\x1f');
|
||||
key += entry.function.CStr();
|
||||
return key;
|
||||
}
|
||||
|
||||
std::string BuildSearchHaystack(const LogEntry& entry) {
|
||||
std::string haystack = XCEngine::Editor::UI::BuildConsoleLogText(entry);
|
||||
haystack.push_back('\n');
|
||||
haystack += entry.file.CStr();
|
||||
haystack.push_back('\n');
|
||||
haystack += entry.function.CStr();
|
||||
return ToLowerCopy(std::move(haystack));
|
||||
}
|
||||
|
||||
bool MatchesSearch(const LogEntry& entry, const std::string& searchText) {
|
||||
if (searchText.empty()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return BuildSearchHaystack(entry).find(searchText) != std::string::npos;
|
||||
}
|
||||
|
||||
void CountSeverity(ConsoleSeverityCounts& counts, LogLevel level) {
|
||||
switch (ResolveSeverity(level)) {
|
||||
case ConsoleSeverityVisual::Warning:
|
||||
++counts.warningCount;
|
||||
break;
|
||||
case ConsoleSeverityVisual::Error:
|
||||
++counts.errorCount;
|
||||
break;
|
||||
case ConsoleSeverityVisual::Log:
|
||||
default:
|
||||
++counts.logCount;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
uint64_t FindLatestSerial(const std::vector<EditorConsoleRecord>& records) {
|
||||
uint64_t latestSerial = 0;
|
||||
for (const EditorConsoleRecord& record : records) {
|
||||
latestSerial = (std::max)(latestSerial, record.serial);
|
||||
}
|
||||
return latestSerial;
|
||||
}
|
||||
|
||||
std::vector<ConsoleRowData> BuildVisibleRows(
|
||||
const std::vector<EditorConsoleRecord>& records,
|
||||
const XCEngine::Editor::UI::ConsoleFilterState& filterState,
|
||||
const std::string& searchText,
|
||||
ConsoleSeverityCounts& counts) {
|
||||
std::vector<ConsoleRowData> rows;
|
||||
rows.reserve(records.size());
|
||||
|
||||
if (!filterState.Collapse()) {
|
||||
for (const EditorConsoleRecord& record : records) {
|
||||
CountSeverity(counts, record.entry.level);
|
||||
if (!filterState.Allows(record.entry.level) || !MatchesSearch(record.entry, searchText)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
rows.push_back(ConsoleRowData{
|
||||
record.serial,
|
||||
record.entry,
|
||||
BuildEntryKey(record.entry),
|
||||
1u
|
||||
});
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
|
||||
std::unordered_map<std::string, size_t> rowIndicesByKey;
|
||||
rowIndicesByKey.reserve(records.size());
|
||||
for (const EditorConsoleRecord& record : records) {
|
||||
CountSeverity(counts, record.entry.level);
|
||||
if (!filterState.Allows(record.entry.level) || !MatchesSearch(record.entry, searchText)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const std::string entryKey = BuildEntryKey(record.entry);
|
||||
const auto it = rowIndicesByKey.find(entryKey);
|
||||
if (it == rowIndicesByKey.end()) {
|
||||
rowIndicesByKey.emplace(entryKey, rows.size());
|
||||
rows.push_back(ConsoleRowData{
|
||||
record.serial,
|
||||
record.entry,
|
||||
entryKey,
|
||||
1u
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
ConsoleRowData& row = rows[it->second];
|
||||
row.serial = record.serial;
|
||||
row.entry = record.entry;
|
||||
++row.count;
|
||||
}
|
||||
|
||||
return rows;
|
||||
}
|
||||
|
||||
const ConsoleRowData* ResolveSelectedRow(
|
||||
const std::vector<ConsoleRowData>& rows,
|
||||
uint64_t& selectedSerial,
|
||||
const std::string& selectedEntryKey) {
|
||||
if (selectedSerial != 0) {
|
||||
for (const ConsoleRowData& row : rows) {
|
||||
if (row.serial == selectedSerial) {
|
||||
return &row;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!selectedEntryKey.empty()) {
|
||||
for (const ConsoleRowData& row : rows) {
|
||||
if (row.entryKey == selectedEntryKey) {
|
||||
selectedSerial = row.serial;
|
||||
return &row;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
selectedSerial = 0;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
bool SelectRelativeRow(const std::vector<ConsoleRowData>& rows, uint64_t& selectedSerial, int delta) {
|
||||
if (rows.empty() || delta == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
int currentIndex = -1;
|
||||
for (int i = 0; i < static_cast<int>(rows.size()); ++i) {
|
||||
if (rows[i].serial == selectedSerial) {
|
||||
currentIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
int nextIndex = 0;
|
||||
if (currentIndex >= 0) {
|
||||
nextIndex = std::clamp(currentIndex + delta, 0, static_cast<int>(rows.size()) - 1);
|
||||
} else if (delta > 0) {
|
||||
nextIndex = 0;
|
||||
} else {
|
||||
nextIndex = static_cast<int>(rows.size()) - 1;
|
||||
}
|
||||
|
||||
if (rows[nextIndex].serial == selectedSerial) {
|
||||
return false;
|
||||
}
|
||||
|
||||
selectedSerial = rows[nextIndex].serial;
|
||||
return true;
|
||||
}
|
||||
|
||||
void DrawSeverityIcon(ImDrawList* drawList, const ImVec2& center, float radius, ConsoleSeverityVisual severity) {
|
||||
const ImU32 color = ImGui::GetColorU32(SeverityColor(severity));
|
||||
switch (severity) {
|
||||
case ConsoleSeverityVisual::Warning:
|
||||
drawList->AddTriangleFilled(
|
||||
ImVec2(center.x, center.y - radius),
|
||||
ImVec2(center.x - radius * 0.90f, center.y + radius * 0.78f),
|
||||
ImVec2(center.x + radius * 0.90f, center.y + radius * 0.78f),
|
||||
color);
|
||||
break;
|
||||
case ConsoleSeverityVisual::Error: {
|
||||
ImVec2 points[8];
|
||||
for (int i = 0; i < 8; ++i) {
|
||||
const float angle = (45.0f * static_cast<float>(i) + 22.5f) * 3.14159265f / 180.0f;
|
||||
points[i] = ImVec2(center.x + std::cos(angle) * radius, center.y + std::sin(angle) * radius);
|
||||
}
|
||||
drawList->AddConvexPolyFilled(points, 8, color);
|
||||
break;
|
||||
}
|
||||
case ConsoleSeverityVisual::Log:
|
||||
default:
|
||||
drawList->AddCircleFilled(center, radius, color, 16);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
bool DrawSeverityToggleButton(
|
||||
const char* id,
|
||||
ConsoleSeverityVisual severity,
|
||||
size_t count,
|
||||
bool& active,
|
||||
const char* tooltip) {
|
||||
const std::string countText = std::to_string(count);
|
||||
const ImVec2 countSize = ImGui::CalcTextSize(countText.c_str());
|
||||
const ImVec2 padding = XCEngine::Editor::UI::ConsoleSeverityButtonPadding();
|
||||
const float iconRadius = 5.0f;
|
||||
const float gap = 8.0f;
|
||||
const ImVec2 size(
|
||||
(std::max)(
|
||||
XCEngine::Editor::UI::ConsoleSeverityButtonMinWidth(),
|
||||
padding.x * 2.0f + iconRadius * 2.0f + gap + countSize.x),
|
||||
ImGui::GetFrameHeight());
|
||||
|
||||
ImGui::InvisibleButton(id, size);
|
||||
const bool hovered = ImGui::IsItemHovered();
|
||||
const bool held = ImGui::IsItemActive();
|
||||
const bool pressed = ImGui::IsItemClicked(ImGuiMouseButton_Left);
|
||||
if (pressed) {
|
||||
active = !active;
|
||||
}
|
||||
|
||||
const ImVec2 min = ImGui::GetItemRectMin();
|
||||
const ImVec2 max = ImGui::GetItemRectMax();
|
||||
const ImU32 backgroundColor = ImGui::GetColorU32(
|
||||
held ? XCEngine::Editor::UI::ToolbarButtonActiveColor()
|
||||
: hovered ? XCEngine::Editor::UI::ToolbarButtonHoveredColor(active)
|
||||
: XCEngine::Editor::UI::ToolbarButtonColor(active));
|
||||
ImDrawList* drawList = ImGui::GetWindowDrawList();
|
||||
drawList->AddRectFilled(min, max, backgroundColor, 2.0f);
|
||||
|
||||
const ImVec2 iconCenter(min.x + padding.x + iconRadius, min.y + size.y * 0.5f);
|
||||
DrawSeverityIcon(drawList, iconCenter, iconRadius, severity);
|
||||
drawList->AddText(
|
||||
ImVec2(iconCenter.x + iconRadius + gap, min.y + (size.y - countSize.y) * 0.5f),
|
||||
ImGui::GetColorU32(ImGuiCol_Text),
|
||||
countText.c_str());
|
||||
|
||||
if (hovered && tooltip && tooltip[0] != '\0') {
|
||||
ImGui::SetTooltip("%s", tooltip);
|
||||
}
|
||||
|
||||
return pressed;
|
||||
}
|
||||
|
||||
ConsoleRowInteraction DrawConsoleRow(const ConsoleRowData& row, bool selected) {
|
||||
ConsoleRowInteraction interaction;
|
||||
|
||||
const float rowHeight = XCEngine::Editor::UI::ConsoleRowHeight();
|
||||
const float availableWidth = (std::max)(ImGui::GetContentRegionAvail().x, 1.0f);
|
||||
ImGui::InvisibleButton("##ConsoleRow", ImVec2(availableWidth, rowHeight));
|
||||
interaction.clicked = ImGui::IsItemClicked(ImGuiMouseButton_Left);
|
||||
interaction.rightClicked = ImGui::IsItemClicked(ImGuiMouseButton_Right);
|
||||
interaction.doubleClicked = ImGui::IsItemHovered() && ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left);
|
||||
|
||||
const bool hovered = ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenBlockedByActiveItem);
|
||||
const ImVec2 min = ImGui::GetItemRectMin();
|
||||
const ImVec2 max = ImGui::GetItemRectMax();
|
||||
ImDrawList* drawList = ImGui::GetWindowDrawList();
|
||||
if (selected) {
|
||||
drawList->AddRectFilled(min, max, ImGui::GetColorU32(XCEngine::Editor::UI::ConsoleRowSelectedFillColor()));
|
||||
} else if (hovered) {
|
||||
drawList->AddRectFilled(min, max, ImGui::GetColorU32(XCEngine::Editor::UI::ConsoleRowHoverFillColor()));
|
||||
}
|
||||
|
||||
const ConsoleSeverityVisual severity = ResolveSeverity(row.entry.level);
|
||||
const ImVec2 iconCenter(min.x + 12.0f, min.y + rowHeight * 0.5f);
|
||||
DrawSeverityIcon(drawList, iconCenter, 5.0f, severity);
|
||||
|
||||
float rightEdge = max.x - 8.0f;
|
||||
if (row.count > 1) {
|
||||
const std::string countText = std::to_string(row.count);
|
||||
const ImVec2 countSize = ImGui::CalcTextSize(countText.c_str());
|
||||
const ImVec2 badgeMin(rightEdge - countSize.x - 10.0f, min.y + 3.0f);
|
||||
const ImVec2 badgeMax(rightEdge, max.y - 3.0f);
|
||||
drawList->AddRectFilled(
|
||||
badgeMin,
|
||||
badgeMax,
|
||||
ImGui::GetColorU32(XCEngine::Editor::UI::ConsoleCountBadgeBackgroundColor()),
|
||||
XCEngine::Editor::UI::ConsoleBadgeRounding());
|
||||
drawList->AddText(
|
||||
ImVec2(
|
||||
badgeMin.x + (badgeMax.x - badgeMin.x - countSize.x) * 0.5f,
|
||||
badgeMin.y + (badgeMax.y - badgeMin.y - countSize.y) * 0.5f),
|
||||
ImGui::GetColorU32(XCEngine::Editor::UI::ConsoleCountBadgeTextColor()),
|
||||
countText.c_str());
|
||||
rightEdge = badgeMin.x - 8.0f;
|
||||
}
|
||||
|
||||
const std::string sourceText = XCEngine::Editor::UI::BuildConsoleSourceText(row.entry);
|
||||
if (!sourceText.empty()) {
|
||||
const ImVec2 sourceSize = ImGui::CalcTextSize(sourceText.c_str());
|
||||
const float sourceWidth = (std::min)(sourceSize.x, availableWidth * 0.32f);
|
||||
const ImVec2 sourceMin(rightEdge - sourceWidth, min.y);
|
||||
const ImVec2 sourceMax(rightEdge, max.y);
|
||||
drawList->PushClipRect(sourceMin, sourceMax, true);
|
||||
drawList->AddText(
|
||||
ImVec2(sourceMin.x, min.y + (rowHeight - sourceSize.y) * 0.5f),
|
||||
ImGui::GetColorU32(XCEngine::Editor::UI::ConsoleSecondaryTextColor()),
|
||||
sourceText.c_str());
|
||||
drawList->PopClipRect();
|
||||
rightEdge = sourceMin.x - 10.0f;
|
||||
}
|
||||
|
||||
const std::string summary = XCEngine::Editor::UI::BuildConsoleSummaryText(row.entry);
|
||||
const ImVec2 textMin(min.x + 24.0f, min.y);
|
||||
const ImVec2 textMax((std::max)(textMin.x, rightEdge), max.y);
|
||||
drawList->PushClipRect(textMin, textMax, true);
|
||||
drawList->AddText(
|
||||
ImVec2(textMin.x, min.y + (rowHeight - ImGui::GetTextLineHeight()) * 0.5f),
|
||||
ImGui::GetColorU32(ImGuiCol_Text),
|
||||
summary.c_str());
|
||||
drawList->PopClipRect();
|
||||
|
||||
return interaction;
|
||||
}
|
||||
|
||||
std::string FormatTimestamp(uint64_t timestamp) {
|
||||
if (timestamp == 0) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const std::time_t seconds = static_cast<std::time_t>(timestamp);
|
||||
std::tm localTime = {};
|
||||
localtime_s(&localTime, &seconds);
|
||||
char buffer[32] = {};
|
||||
if (std::strftime(buffer, sizeof(buffer), "%Y-%m-%d %H:%M:%S", &localTime) == 0) {
|
||||
return std::to_string(timestamp);
|
||||
}
|
||||
return buffer;
|
||||
}
|
||||
|
||||
bool TryResolveSourcePath(const LogEntry& entry, std::filesystem::path& outPath) {
|
||||
const std::string filePath = entry.file.CStr();
|
||||
if (filePath.empty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
std::error_code ec;
|
||||
std::filesystem::path path(XCEngine::Editor::Platform::Utf8ToWide(filePath));
|
||||
if (path.is_relative()) {
|
||||
path = std::filesystem::current_path(ec) / path;
|
||||
ec.clear();
|
||||
}
|
||||
|
||||
path = std::filesystem::weakly_canonical(path, ec);
|
||||
if (ec) {
|
||||
path = std::filesystem::path(XCEngine::Editor::Platform::Utf8ToWide(filePath)).lexically_normal();
|
||||
}
|
||||
|
||||
if (!std::filesystem::exists(path)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
outPath = path;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool CanOpenSourceLocation(const LogEntry& entry) {
|
||||
std::filesystem::path resolvedPath;
|
||||
return TryResolveSourcePath(entry, resolvedPath);
|
||||
}
|
||||
|
||||
bool OpenSourceLocation(const LogEntry& entry) {
|
||||
std::filesystem::path resolvedPath;
|
||||
if (!TryResolveSourcePath(entry, resolvedPath)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const HINSTANCE result = ShellExecuteW(
|
||||
nullptr,
|
||||
L"open",
|
||||
resolvedPath.c_str(),
|
||||
nullptr,
|
||||
resolvedPath.parent_path().c_str(),
|
||||
SW_SHOWNORMAL);
|
||||
return reinterpret_cast<INT_PTR>(result) > 32;
|
||||
}
|
||||
|
||||
void CopySummaryToClipboard(const ConsoleRowData& row) {
|
||||
const std::string copyText = XCEngine::Editor::UI::BuildConsoleLogText(row.entry);
|
||||
ImGui::SetClipboardText(copyText.c_str());
|
||||
}
|
||||
|
||||
void CopyToClipboard(const ConsoleRowData& row) {
|
||||
const std::string copyText = XCEngine::Editor::UI::BuildConsoleCopyText(row.entry);
|
||||
ImGui::SetClipboardText(copyText.c_str());
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
namespace XCEngine {
|
||||
namespace Editor {
|
||||
@@ -9,14 +483,421 @@ namespace Editor {
|
||||
ConsolePanel::ConsolePanel() : Panel("Console") {
|
||||
}
|
||||
|
||||
void ConsolePanel::OnAttach() {
|
||||
if (!m_context) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_detailsHeight <= 0.0f) {
|
||||
m_detailsHeight = UI::ConsoleDetailsDefaultHeight();
|
||||
}
|
||||
|
||||
EditorConsoleSink* sink = EditorConsoleSink::GetInstance();
|
||||
m_lastSeenRevision = sink ? sink->GetRevision() : 0;
|
||||
m_lastErrorPauseScanSerial = sink ? FindLatestSerial(sink->GetRecords()) : 0;
|
||||
|
||||
if (!m_playModeStartedHandlerId) {
|
||||
m_playModeStartedHandlerId = m_context->GetEventBus().Subscribe<PlayModeStartedEvent>(
|
||||
[this](const PlayModeStartedEvent&) {
|
||||
HandlePlayModeStarted();
|
||||
});
|
||||
}
|
||||
if (!m_playModeStoppedHandlerId) {
|
||||
m_playModeStoppedHandlerId = m_context->GetEventBus().Subscribe<PlayModeStoppedEvent>(
|
||||
[this](const PlayModeStoppedEvent&) {
|
||||
HandlePlayModeStopped();
|
||||
});
|
||||
}
|
||||
if (!m_playModePausedHandlerId) {
|
||||
m_playModePausedHandlerId = m_context->GetEventBus().Subscribe<PlayModePausedEvent>(
|
||||
[this](const PlayModePausedEvent&) {
|
||||
HandlePlayModePaused();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void ConsolePanel::OnDetach() {
|
||||
if (!m_context) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_playModeStartedHandlerId) {
|
||||
m_context->GetEventBus().Unsubscribe<PlayModeStartedEvent>(m_playModeStartedHandlerId);
|
||||
m_playModeStartedHandlerId = 0;
|
||||
}
|
||||
if (m_playModeStoppedHandlerId) {
|
||||
m_context->GetEventBus().Unsubscribe<PlayModeStoppedEvent>(m_playModeStoppedHandlerId);
|
||||
m_playModeStoppedHandlerId = 0;
|
||||
}
|
||||
if (m_playModePausedHandlerId) {
|
||||
m_context->GetEventBus().Unsubscribe<PlayModePausedEvent>(m_playModePausedHandlerId);
|
||||
m_playModePausedHandlerId = 0;
|
||||
}
|
||||
}
|
||||
|
||||
void ConsolePanel::HandlePlayModeStarted() {
|
||||
m_playModeActive = true;
|
||||
m_playModePaused = false;
|
||||
|
||||
EditorConsoleSink* sink = EditorConsoleSink::GetInstance();
|
||||
if (!sink) {
|
||||
m_lastErrorPauseScanSerial = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_filterState.ClearOnPlay()) {
|
||||
sink->Clear();
|
||||
m_selectedSerial = 0;
|
||||
m_selectedEntryKey.clear();
|
||||
m_lastErrorPauseScanSerial = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
m_lastErrorPauseScanSerial = FindLatestSerial(sink->GetRecords());
|
||||
}
|
||||
|
||||
void ConsolePanel::HandlePlayModeStopped() {
|
||||
m_playModeActive = false;
|
||||
m_playModePaused = false;
|
||||
}
|
||||
|
||||
void ConsolePanel::HandlePlayModePaused() {
|
||||
m_playModeActive = true;
|
||||
m_playModePaused = true;
|
||||
}
|
||||
|
||||
void ConsolePanel::Render() {
|
||||
UI::PanelWindowScope panel(m_name.c_str());
|
||||
if (!panel.IsOpen()) {
|
||||
return;
|
||||
}
|
||||
|
||||
EditorConsoleSink* sink = EditorConsoleSink::GetInstance();
|
||||
if (!sink) {
|
||||
UI::PanelContentScope content("ConsoleUnavailable", UI::DefaultPanelContentPadding());
|
||||
if (content.IsOpen()) {
|
||||
UI::DrawEmptyState("Console unavailable");
|
||||
}
|
||||
Actions::ObserveInactiveActionRoute(*m_context);
|
||||
return;
|
||||
}
|
||||
|
||||
const std::vector<EditorConsoleRecord> records = sink->GetRecords();
|
||||
const uint64_t revision = sink->GetRevision();
|
||||
const bool hasNewRevision = revision != m_lastSeenRevision;
|
||||
m_lastSeenRevision = revision;
|
||||
|
||||
if (m_playModeActive && m_filterState.ErrorPause() && !m_playModePaused) {
|
||||
bool shouldPause = false;
|
||||
uint64_t latestSerial = m_lastErrorPauseScanSerial;
|
||||
for (const EditorConsoleRecord& record : records) {
|
||||
latestSerial = (std::max)(latestSerial, record.serial);
|
||||
if (record.serial > m_lastErrorPauseScanSerial && IsErrorLevel(record.entry.level)) {
|
||||
shouldPause = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (shouldPause) {
|
||||
m_context->GetEventBus().Publish(PlayModePausedEvent{});
|
||||
m_playModePaused = true;
|
||||
}
|
||||
m_lastErrorPauseScanSerial = latestSerial;
|
||||
} else {
|
||||
m_lastErrorPauseScanSerial = FindLatestSerial(records);
|
||||
}
|
||||
|
||||
const std::string searchText = ToLowerCopy(std::string(m_searchBuffer));
|
||||
ConsoleSeverityCounts counts;
|
||||
std::vector<ConsoleRowData> rows = BuildVisibleRows(records, m_filterState, searchText, counts);
|
||||
const ConsoleRowData* selectedRow = ResolveSelectedRow(rows, m_selectedSerial, m_selectedEntryKey);
|
||||
if (selectedRow) {
|
||||
m_selectedEntryKey = selectedRow->entryKey;
|
||||
} else if (m_selectedSerial == 0) {
|
||||
m_selectedEntryKey.clear();
|
||||
}
|
||||
|
||||
const bool panelFocused = ImGui::IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows);
|
||||
bool scrollSelectionIntoView = false;
|
||||
if (panelFocused && !ImGui::GetIO().WantTextInput) {
|
||||
if (ImGui::GetIO().KeyCtrl && ImGui::IsKeyPressed(ImGuiKey_F, false)) {
|
||||
m_requestSearchFocus = true;
|
||||
}
|
||||
if (ImGui::GetIO().KeyCtrl && ImGui::IsKeyPressed(ImGuiKey_C, false) && selectedRow) {
|
||||
CopyToClipboard(*selectedRow);
|
||||
}
|
||||
if (ImGui::IsKeyPressed(ImGuiKey_UpArrow, false) && SelectRelativeRow(rows, m_selectedSerial, -1)) {
|
||||
scrollSelectionIntoView = true;
|
||||
}
|
||||
if (ImGui::IsKeyPressed(ImGuiKey_DownArrow, false) && SelectRelativeRow(rows, m_selectedSerial, 1)) {
|
||||
scrollSelectionIntoView = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (scrollSelectionIntoView) {
|
||||
selectedRow = ResolveSelectedRow(rows, m_selectedSerial, m_selectedEntryKey);
|
||||
if (selectedRow) {
|
||||
m_selectedEntryKey = selectedRow->entryKey;
|
||||
}
|
||||
}
|
||||
|
||||
UI::PanelToolbarScope toolbar(
|
||||
"ConsoleToolbar",
|
||||
UI::StandardPanelToolbarHeight(),
|
||||
ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse,
|
||||
true,
|
||||
UI::ToolbarPadding(),
|
||||
UI::ToolbarItemSpacing(),
|
||||
UI::ToolbarBackgroundColor());
|
||||
if (toolbar.IsOpen() &&
|
||||
ImGui::BeginTable(
|
||||
"##ConsoleToolbarLayout",
|
||||
3,
|
||||
ImGuiTableFlags_NoSavedSettings | ImGuiTableFlags_SizingStretchProp))
|
||||
{
|
||||
ImGui::TableSetupColumn("##Primary", ImGuiTableColumnFlags_WidthStretch);
|
||||
ImGui::TableSetupColumn("##Search", ImGuiTableColumnFlags_WidthFixed, 240.0f);
|
||||
ImGui::TableSetupColumn("##Severity", ImGuiTableColumnFlags_WidthFixed, 250.0f);
|
||||
ImGui::TableNextRow();
|
||||
|
||||
ImGui::TableNextColumn();
|
||||
if (Actions::DrawToolbarAction(Actions::MakeClearConsoleAction())) {
|
||||
sink->Clear();
|
||||
m_selectedSerial = 0;
|
||||
m_selectedEntryKey.clear();
|
||||
}
|
||||
ImGui::SameLine();
|
||||
Actions::DrawToolbarToggleAction(
|
||||
Actions::MakeConsoleCollapseAction(m_filterState.Collapse()),
|
||||
m_filterState.Collapse());
|
||||
ImGui::SameLine();
|
||||
Actions::DrawToolbarToggleAction(
|
||||
Actions::MakeConsoleClearOnPlayAction(m_filterState.ClearOnPlay()),
|
||||
m_filterState.ClearOnPlay());
|
||||
ImGui::SameLine();
|
||||
Actions::DrawToolbarToggleAction(
|
||||
Actions::MakeConsoleErrorPauseAction(m_filterState.ErrorPause()),
|
||||
m_filterState.ErrorPause());
|
||||
|
||||
ImGui::TableNextColumn();
|
||||
if (m_requestSearchFocus) {
|
||||
ImGui::SetKeyboardFocusHere();
|
||||
m_requestSearchFocus = false;
|
||||
}
|
||||
UI::ToolbarSearchField("##ConsoleSearch", "Search", m_searchBuffer, sizeof(m_searchBuffer));
|
||||
|
||||
ImGui::TableNextColumn();
|
||||
DrawSeverityToggleButton("##ConsoleLogFilter", ConsoleSeverityVisual::Log, counts.logCount, m_filterState.ShowLog(), "Log");
|
||||
ImGui::SameLine(0.0f, 6.0f);
|
||||
DrawSeverityToggleButton("##ConsoleWarningFilter", ConsoleSeverityVisual::Warning, counts.warningCount, m_filterState.ShowWarning(), "Warnings");
|
||||
ImGui::SameLine(0.0f, 6.0f);
|
||||
DrawSeverityToggleButton("##ConsoleErrorFilter", ConsoleSeverityVisual::Error, counts.errorCount, m_filterState.ShowError(), "Errors");
|
||||
|
||||
ImGui::EndTable();
|
||||
}
|
||||
|
||||
UI::PanelContentScope content("ConsoleRoot", ImVec2(0.0f, 0.0f));
|
||||
if (!content.IsOpen()) {
|
||||
Actions::ObserveInactiveActionRoute(*m_context);
|
||||
return;
|
||||
}
|
||||
|
||||
const float splitterThickness = UI::PanelSplitterHitThickness();
|
||||
const float totalHeight = ImGui::GetContentRegionAvail().y;
|
||||
const float minDetailsHeight = UI::ConsoleDetailsMinHeight();
|
||||
const float minListHeight = UI::ConsoleListMinHeight();
|
||||
const float maxDetailsHeight = (std::max)(minDetailsHeight, totalHeight - minListHeight - splitterThickness);
|
||||
m_detailsHeight = std::clamp(m_detailsHeight, minDetailsHeight, maxDetailsHeight);
|
||||
const float listHeight = (std::max)(minListHeight, totalHeight - m_detailsHeight - splitterThickness);
|
||||
|
||||
ImGui::PushStyleColor(ImGuiCol_ChildBg, UI::ConsoleListBackgroundColor());
|
||||
const bool listOpen = ImGui::BeginChild(
|
||||
"ConsoleLogList",
|
||||
ImVec2(0.0f, listHeight),
|
||||
false,
|
||||
ImGuiWindowFlags_HorizontalScrollbar);
|
||||
ImGui::PopStyleColor();
|
||||
if (listOpen) {
|
||||
const bool wasAtBottom = ImGui::GetScrollY() >= ImGui::GetScrollMaxY() - 4.0f;
|
||||
bool openSelectedSource = false;
|
||||
ImGuiListClipper clipper;
|
||||
clipper.Begin(static_cast<int>(rows.size()), UI::ConsoleRowHeight());
|
||||
while (clipper.Step()) {
|
||||
for (int i = clipper.DisplayStart; i < clipper.DisplayEnd; ++i) {
|
||||
ConsoleRowData& row = rows[static_cast<size_t>(i)];
|
||||
ImGui::PushID(static_cast<int>(row.serial));
|
||||
const bool selected = row.serial == m_selectedSerial;
|
||||
const ConsoleRowInteraction interaction = DrawConsoleRow(row, selected);
|
||||
if (interaction.clicked || interaction.rightClicked) {
|
||||
m_selectedSerial = row.serial;
|
||||
m_selectedEntryKey = row.entryKey;
|
||||
selectedRow = &row;
|
||||
}
|
||||
if (interaction.doubleClicked) {
|
||||
openSelectedSource = true;
|
||||
}
|
||||
if (scrollSelectionIntoView && row.serial == m_selectedSerial) {
|
||||
ImGui::SetScrollHereY(0.5f);
|
||||
}
|
||||
|
||||
if (UI::BeginContextMenuForLastItem("##ConsoleRowContext")) {
|
||||
Actions::DrawMenuAction(Actions::MakeAction("Copy", "Ctrl+C", false, true), [&]() {
|
||||
CopySummaryToClipboard(row);
|
||||
});
|
||||
Actions::DrawMenuAction(Actions::MakeAction("Copy Full", nullptr, false, true), [&]() {
|
||||
CopyToClipboard(row);
|
||||
});
|
||||
Actions::DrawMenuAction(
|
||||
Actions::MakeAction("Open Source", nullptr, false, CanOpenSourceLocation(row.entry)),
|
||||
[&]() {
|
||||
OpenSourceLocation(row.entry);
|
||||
});
|
||||
Actions::DrawMenuSeparator();
|
||||
Actions::DrawMenuAction(Actions::MakeClearConsoleAction(), [&]() {
|
||||
sink->Clear();
|
||||
m_selectedSerial = 0;
|
||||
m_selectedEntryKey.clear();
|
||||
});
|
||||
UI::EndContextMenu();
|
||||
}
|
||||
|
||||
ImGui::PopID();
|
||||
}
|
||||
}
|
||||
|
||||
if (rows.empty()) {
|
||||
UI::DrawEmptyState(
|
||||
searchText.empty() ? "No messages" : "No search results",
|
||||
searchText.empty() ? nullptr : "No console entries match the current search",
|
||||
ImVec2(12.0f, 12.0f));
|
||||
}
|
||||
|
||||
if (openSelectedSource && selectedRow != nullptr && !OpenSourceLocation(selectedRow->entry)) {
|
||||
CopyToClipboard(*selectedRow);
|
||||
}
|
||||
|
||||
if (UI::BeginContextMenuForWindow("##ConsoleBackgroundContext")) {
|
||||
Actions::DrawMenuAction(Actions::MakeClearConsoleAction(), [&]() {
|
||||
sink->Clear();
|
||||
m_selectedSerial = 0;
|
||||
m_selectedEntryKey.clear();
|
||||
});
|
||||
UI::EndContextMenu();
|
||||
}
|
||||
|
||||
if (hasNewRevision && wasAtBottom) {
|
||||
ImGui::SetScrollHereY(1.0f);
|
||||
}
|
||||
}
|
||||
ImGui::EndChild();
|
||||
|
||||
const UI::SplitterResult splitter = UI::DrawSplitter("##ConsoleDetailsSplitter", UI::SplitterAxis::Horizontal, splitterThickness);
|
||||
if (splitter.active) {
|
||||
m_detailsHeight = std::clamp(m_detailsHeight - splitter.delta, minDetailsHeight, maxDetailsHeight);
|
||||
}
|
||||
|
||||
selectedRow = ResolveSelectedRow(rows, m_selectedSerial, m_selectedEntryKey);
|
||||
if (selectedRow) {
|
||||
m_selectedEntryKey = selectedRow->entryKey;
|
||||
}
|
||||
|
||||
ImGui::PushStyleColor(ImGuiCol_ChildBg, UI::ConsoleDetailsBackgroundColor());
|
||||
const bool detailsOpen = ImGui::BeginChild("ConsoleDetails", ImVec2(0.0f, 0.0f), false);
|
||||
ImGui::PopStyleColor();
|
||||
if (detailsOpen) {
|
||||
ImGui::PushStyleColor(ImGuiCol_ChildBg, UI::ConsoleDetailsHeaderBackgroundColor());
|
||||
const bool headerOpen = ImGui::BeginChild(
|
||||
"ConsoleDetailsHeader",
|
||||
ImVec2(0.0f, 28.0f),
|
||||
false,
|
||||
ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse);
|
||||
ImGui::PopStyleColor();
|
||||
if (headerOpen) {
|
||||
if (selectedRow) {
|
||||
const char* severityLabel = UI::ConsoleSeverityLabel(selectedRow->entry.level);
|
||||
const std::string sourceText = UI::BuildConsoleSourceText(selectedRow->entry);
|
||||
ImGui::Text("%s", severityLabel);
|
||||
if (!sourceText.empty()) {
|
||||
ImGui::SameLine();
|
||||
ImGui::TextColored(UI::ConsoleSecondaryTextColor(), "%s", sourceText.c_str());
|
||||
}
|
||||
if (selectedRow->count > 1) {
|
||||
const std::string countLabel = "x" + std::to_string(selectedRow->count);
|
||||
ImGui::SameLine();
|
||||
UI::DrawRightAlignedText(countLabel.c_str(), UI::ConsoleSecondaryTextColor(), 8.0f);
|
||||
}
|
||||
} else {
|
||||
ImGui::TextColored(UI::ConsoleSecondaryTextColor(), "Details");
|
||||
}
|
||||
}
|
||||
ImGui::EndChild();
|
||||
|
||||
if (selectedRow) {
|
||||
const std::string message = selectedRow->entry.message.CStr();
|
||||
const std::string fileText = selectedRow->entry.file.CStr();
|
||||
const std::string functionText = selectedRow->entry.function.CStr();
|
||||
const std::string timeText = FormatTimestamp(selectedRow->entry.timestamp);
|
||||
const std::string sourceText = UI::BuildConsoleSourceText(selectedRow->entry);
|
||||
const std::string threadText = std::to_string(selectedRow->entry.threadId);
|
||||
const std::string lineText =
|
||||
selectedRow->entry.line > 0 ? std::to_string(selectedRow->entry.line) : std::string();
|
||||
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(6.0f, 6.0f));
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(10.0f, 8.0f));
|
||||
const bool bodyOpen = ImGui::BeginChild("ConsoleDetailsBody", ImVec2(0.0f, 0.0f), false);
|
||||
ImGui::PopStyleVar(2);
|
||||
if (bodyOpen) {
|
||||
ImGui::TextWrapped("%s", message.c_str());
|
||||
ImGui::Spacing();
|
||||
ImGui::Separator();
|
||||
ImGui::Spacing();
|
||||
|
||||
if (ImGui::BeginTable("##ConsoleDetailsMeta", 2, ImGuiTableFlags_SizingFixedFit | ImGuiTableFlags_NoSavedSettings)) {
|
||||
auto drawMetaRow = [](const char* label, const std::string& value) {
|
||||
if (value.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
ImGui::TableNextRow();
|
||||
ImGui::TableNextColumn();
|
||||
ImGui::TextColored(XCEngine::Editor::UI::ConsoleSecondaryTextColor(), "%s", label);
|
||||
ImGui::TableNextColumn();
|
||||
ImGui::TextWrapped("%s", value.c_str());
|
||||
};
|
||||
|
||||
drawMetaRow("Type", UI::ConsoleSeverityLabel(selectedRow->entry.level));
|
||||
drawMetaRow("Category", XCEngine::Debug::LogCategoryToString(selectedRow->entry.category));
|
||||
drawMetaRow("Source", sourceText);
|
||||
drawMetaRow("File", fileText);
|
||||
drawMetaRow("Line", lineText);
|
||||
drawMetaRow("Function", functionText);
|
||||
drawMetaRow("Time", timeText);
|
||||
drawMetaRow("Thread", threadText);
|
||||
ImGui::EndTable();
|
||||
}
|
||||
|
||||
ImGui::Spacing();
|
||||
if (ImGui::Button("Copy", ImVec2(72.0f, 0.0f))) {
|
||||
CopyToClipboard(*selectedRow);
|
||||
}
|
||||
ImGui::SameLine();
|
||||
ImGui::BeginDisabled(!CanOpenSourceLocation(selectedRow->entry));
|
||||
if (ImGui::Button("Open Source", ImVec2(96.0f, 0.0f))) {
|
||||
OpenSourceLocation(selectedRow->entry);
|
||||
}
|
||||
ImGui::EndDisabled();
|
||||
}
|
||||
ImGui::EndChild();
|
||||
} else {
|
||||
UI::PanelContentScope detailsContent("ConsoleDetailsEmpty", ImVec2(10.0f, 8.0f));
|
||||
if (detailsContent.IsOpen()) {
|
||||
UI::DrawEmptyState("No message selected");
|
||||
}
|
||||
}
|
||||
}
|
||||
ImGui::EndChild();
|
||||
|
||||
Actions::ObserveInactiveActionRoute(*m_context);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
} // namespace Editor
|
||||
} // namespace XCEngine
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
#pragma once
|
||||
|
||||
#include "Panel.h"
|
||||
#include "UI/ConsoleFilterState.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
|
||||
namespace XCEngine {
|
||||
namespace Editor {
|
||||
@@ -8,7 +12,28 @@ namespace Editor {
|
||||
class ConsolePanel : public Panel {
|
||||
public:
|
||||
ConsolePanel();
|
||||
void OnAttach() override;
|
||||
void OnDetach() override;
|
||||
void Render() override;
|
||||
|
||||
private:
|
||||
void HandlePlayModeStarted();
|
||||
void HandlePlayModeStopped();
|
||||
void HandlePlayModePaused();
|
||||
|
||||
UI::ConsoleFilterState m_filterState;
|
||||
char m_searchBuffer[256] = "";
|
||||
uint64_t m_selectedSerial = 0;
|
||||
uint64_t m_lastSeenRevision = 0;
|
||||
uint64_t m_lastErrorPauseScanSerial = 0;
|
||||
uint64_t m_playModeStartedHandlerId = 0;
|
||||
uint64_t m_playModeStoppedHandlerId = 0;
|
||||
uint64_t m_playModePausedHandlerId = 0;
|
||||
std::string m_selectedEntryKey;
|
||||
float m_detailsHeight = 0.0f;
|
||||
bool m_playModeActive = false;
|
||||
bool m_playModePaused = false;
|
||||
bool m_requestSearchFocus = false;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user