Files
2023-04-23 23:00:13 +08:00

798 lines
20 KiB
C++

#include "console.h"
#include "imgui/ui.h"
#include "imgui/region_string.h"
using namespace engine;
using namespace engine::ui;
#pragma warning(disable: 4996)
#include <imgui/imgui.h>
#include <regex>
#include "../editor.h"
RegionStringInit Console_Title( "Console_Title", "Console", "Console");
RegionStringInit Console_Filter( "Console_Filter", "Filter", "Console");
RegionStringInit Console_LogTrace( "Console_LogTrace", "Trace", "Console");
RegionStringInit Console_LogInfo( "Console_LogInfo", "Info", "Console");
RegionStringInit Console_LogWarn( "Console_LogWarn", "Warn", "Console");
RegionStringInit Console_LogError( "Console_LogError", "Error", "Console");
RegionStringInit Console_LogOther( "Console_LogOther", "Other", "Console");
RegionStringInit Console_CVarIn( "Console_CVarIn", "CVar Input ", "Console");
RegionStringInit Console_ClearAll( "Console_ClearAll", "Clear All ", "Console");
RegionStringInit Console_CopyItem( "Console_CopyItem", " Copy Item ", "Console");
const static std::string ICON_CONSOLE_CLONE = ICON_FA_COPY;
const static std::string ICON_CONSOLE_CLEAR = ICON_NONE;
const static std::string ICON_CONSOLE_CONSOLE_TITLE = ICON_FA_COMMENT;
const static std::string ICON_CONSOLE_FILTER = ICON_FA_MAGNIFYING_GLASS;
static AutoCVarCmd cmdExportEnglishUIStrins("cmd.ui.exportStrings", "Export ui string to ini files.");
static AutoCVarCmd cmdExportCVars("cmd.exportCVars", "Export all cvars to ini files.");
void execCmdCallback()
{
CVarCmdHandle(cmdExportEnglishUIStrins, []()
{
std::filesystem::path p = Framework::get()->getConfig().configFolder;
p /= "editor-ui-string-english.ini";
RegionStringManager::get()->exportEnglishRegionConfigs(p.string());
});
CVarCmdHandle(cmdExportCVars, []()
{
std::filesystem::path p = Framework::get()->getConfig().configFolder;
p /= "cvars.ini";
CVarSystem::get()->exportAllConfig(p.string());
});
}
class Console
{
private:
DelegateHandle m_logCacheHandle;
// command input buffer.
char m_inputBuffer[256];
// precache commands on cvar system.
ImVector<const char*> m_commands;
ImVector<char*> m_historyCommands;
std::vector<const char*> m_activeCommands;
// -1 is new line, [0, m_historyCommands.size() - 1] is browsing history.
int32_t m_historyPos;
// filter for log items.
ImGuiTextFilter m_filter;
// whether auto scroll if log items full.
bool m_bAutoScroll = true;
bool m_bScrollToBottom;
bool m_bCommandSelectPop = false;
int m_selectedCommandIndex = -1;
// log items deque.
std::deque<std::pair<std::string, ELogType>> m_logItems;
static const uint32_t kMaxLogsItemNum = 200;
int32_t m_hoverItem = -1;
bool m_bLogItemMenuPopup = false;
bool m_logVisible[(size_t)ELogType::Max] =
{
true, // Trace
true, // Info
true, // Warn
true, // Error
true, // Fatal
true // Other
};
uint32_t m_logTypeCount[(size_t)ELogType::Max] =
{
0, // Trace
0, // Info
0, // Warn
0, // Error
0, // Fatal
0 // Other
};
private:
static int myStricmp(const char* s1, const char* s2)
{
int d;
while ((d = toupper(*s2) - toupper(*s1)) == 0 && *s1)
{
s1++;
s2++;
}
return d;
}
static int myStrnicmp(const char* s1, const char* s2, int n)
{
int d = 0;
while (n > 0 && (d = toupper(*s2) - toupper(*s1)) == 0 && *s1)
{
s1++;
s2++;
n--;
}
return d;
}
static char* myStrdup(const char* s)
{
IM_ASSERT(s);
size_t len = strlen(s) + 1;
void* buf = malloc(len);
IM_ASSERT(buf);
return (char*)memcpy(buf, (const void*)s, len);
}
static void myStrtrim(char* s)
{
char* str_end = s + strlen(s);
while (str_end > s && str_end[-1] == ' ')
{
str_end--;
}
*str_end = 0;
}
static int textEditCallbackStub(ImGuiInputTextCallbackData* data)
{
Console* console = (Console*)data->UserData;
return console->textEditCallback(data);
}
void clearLog()
{
m_logItems.clear();
for (auto& i : m_logTypeCount)
{
i = 0;
}
}
void addLog(std::string info, ELogType type)
{
m_logTypeCount[size_t(type)] ++;
m_logItems.push_back({ info, type });
if (static_cast<uint32_t>(m_logItems.size()) >= kMaxLogsItemNum)
{
m_logItems.pop_front();
}
}
void addLog(const char* fmt, ...)
{
char buf[1024];
va_list args;
va_start(args, fmt);
vsnprintf(buf, IM_ARRAYSIZE(buf), fmt, args);
buf[IM_ARRAYSIZE(buf) - 1] = 0;
va_end(args);
addLog(std::string(myStrdup(buf)), ELogType::Other);
}
void execCommand(const char* command)
{
addLog("# %s\n", command);
// Insert into history. First find match and delete it so it can be pushed to the back.
// This isn't trying to be smart or optimal.
m_historyPos = -1;
for (int i = m_historyCommands.Size - 1; i >= 0; i--)
{
if (myStricmp(m_historyCommands[i], command) == 0)
{
free(m_historyCommands[i]);
m_historyCommands.erase(m_historyCommands.begin() + i);
break;
}
}
m_historyCommands.push_back(myStrdup(command));
std::string cmd_info = command;
std::regex ws_re("\\s+"); // whitespace
std::vector<std::string> tokens(
std::sregex_token_iterator(cmd_info.begin(), cmd_info.end(), ws_re, -1),
std::sregex_token_iterator()
);
auto cVar = CVarSystem::get()->getCVarParameter(tokens[0].c_str());
if (cVar != nullptr)
{
// Try to get command value.
if (tokens.size() == 1)
{
static const std::string cmdbeginStr = "cmd.";
int32_t arrayIndex = cVar->arrayIndex;
if (tokens[0].rfind(cmdbeginStr, 0) == 0)
{
CVarSystem::get()->getArray<bool>().setCurrent(true, arrayIndex);
execCmdCallback();
}
else
{
std::string val;
// Load value.
if (cVar->type == CVarType::Int32)
{
val = std::to_string(CVarSystem::get()->getArray<int32_t>().getCurrent(arrayIndex));
}
else if (cVar->type == CVarType::Bool)
{
val = std::to_string(CVarSystem::get()->getArray<bool>().getCurrent(arrayIndex));
}
else if (cVar->type == CVarType::Float)
{
val = std::format("{}",CVarSystem::get()->getArray<float>().getCurrent(arrayIndex));
}
else if (cVar->type == CVarType::String)
{
val = CVarSystem::get()->getArray<std::string>().getCurrent(arrayIndex);
}
// Print value.
{
std::string outHelp = "Help: " + std::string(cVar->description);
addLog(outHelp.c_str());
addLog(("Current value: " + tokens[0] + " " + val).c_str());
}
}
}
// Try to set command value.
else if (tokens.size() == 2)
{
int32_t arrayIndex = cVar->arrayIndex;
uint32_t flag = cVar->flag;
// Readonly flag check.
const bool bReadOnly = flag & ReadOnly;
if (bReadOnly)
{
addLog("%s is a readonly value, can't change on console!\n", tokens[0].c_str());
}
else if (cVar->type == CVarType::Int32)
{
CVarSystem::get()->getArray<int32_t>().setCurrent((int32_t)std::stoi(tokens[1]), cVar->arrayIndex);
}
else if (cVar->type == CVarType::Bool)
{
const bool bNewValue = stringToBoolApprox(tokens[1]);
CVarSystem::get()->getArray<bool>().setCurrent(bNewValue, cVar->arrayIndex);
}
else if (cVar->type == CVarType::Float)
{
CVarSystem::get()->getArray<float>().setCurrent((float)std::stof(tokens[1]), cVar->arrayIndex);
}
else if (cVar->type == CVarType::String)
{
CVarSystem::get()->getArray<std::string>().setCurrent(tokens[1], cVar->arrayIndex);
}
}
else
{
addLog("Error command parameters, all CVar only keep one parameter, but '%d' paramters input this time.\n", tokens.size() - 1);
}
}
else
{
addLog("Unkonwn command: '%s'!\n", command);
}
// On command input, we scroll to bottom even if AutoScroll==false
m_bScrollToBottom = true;
}
void inputCallbackOnComplete(ImGuiInputTextCallbackData* data)
{
// locate beginning of current word
const char* word_end = data->Buf + data->CursorPos;
const char* word_start = word_end;
while (word_start > data->Buf)
{
const char c = word_start[-1];
if (c == ' ' || c == '\t' || c == ',' || c == ';')
break;
word_start--;
}
// build a list of candidates
ImVector<const char*> candidates;
for (int i = 0; i < m_commands.Size; i++)
{
if (myStrnicmp(m_commands[i], word_start, (int)(word_end - word_start)) == 0)
{
candidates.push_back(m_commands[i]);
}
else
{
std::string lowerCommand = m_commands[i];
std::transform(lowerCommand.begin(), lowerCommand.end(), lowerCommand.begin(), ::tolower);
if (myStrnicmp(lowerCommand.c_str(), word_start, (int)(word_end - word_start)) == 0)
{
candidates.push_back(m_commands[i]);
}
}
}
if (candidates.Size == 0)
{
// No match
}
else if (candidates.Size == 1)
{
// Single match. Delete the beginning of the word and replace it entirely so we've got nice casing.
data->DeleteChars((int)(word_start - data->Buf), (int)(word_end - word_start));
data->InsertChars(data->CursorPos, candidates[0]);
data->InsertChars(data->CursorPos, " ");
}
else
{
// Multiple matches. Complete as much as we can..
// So inputing "C"+Tab will complete to "CL" then display "CLEAR" and "CLASSIFY" as matches.
int match_len = (int)(word_end - word_start);
for (;;)
{
int c = 0;
bool all_candidates_matches = true;
for (int i = 0; i < candidates.Size && all_candidates_matches; i++)
if (i == 0)
c = toupper(candidates[i][match_len]);
else if (c == 0 || c != toupper(candidates[i][match_len]))
all_candidates_matches = false;
if (!all_candidates_matches)
break;
match_len++;
}
if (match_len > 0)
{
data->DeleteChars((int)(word_start - data->Buf), (int)(word_end - word_start));
data->InsertChars(data->CursorPos, candidates[0], candidates[0] + match_len);
}
}
}
void inputCallbackOnEdit(ImGuiInputTextCallbackData* data)
{
m_activeCommands.clear();
// locate beginning of current word
const char* word_end = data->Buf + data->CursorPos;
const char* word_start = word_end;
while (word_start > data->Buf)
{
const char c = word_start[-1];
if (c == ' ' || c == '\t' || c == ',' || c == ';')
break;
word_start--;
}
// build a list of candidates
ImVector<const char*> candidates;
for (int i = 0; i < m_commands.Size; i++)
{
int size = (int)(word_end - word_start);
if (size > 0 && myStrnicmp(m_commands[i], word_start, size) == 0)
{
candidates.push_back(m_commands[i]);
}
else
{
std::string lowerCommand = m_commands[i];
std::transform(lowerCommand.begin(), lowerCommand.end(), lowerCommand.begin(), ::tolower);
if (size > 0 && myStrnicmp(lowerCommand.c_str(), word_start, size) == 0)
{
candidates.push_back(m_commands[i]);
}
}
}
if (candidates.Size >= 1)
{
for (int i = 0; i < candidates.Size; i++)
{
m_activeCommands.push_back(candidates[i]);
}
}
}
void inputCallbackOnHistory(ImGuiInputTextCallbackData* data)
{
if (m_bCommandSelectPop)
{
if (data->EventKey == ImGuiKey_UpArrow)
{
if (m_selectedCommandIndex <= 0)
{
m_selectedCommandIndex = int(m_activeCommands.size()) - 1;
}
else
{
m_selectedCommandIndex--;
}
}
else if (data->EventKey == ImGuiKey_DownArrow)
{
if (++m_selectedCommandIndex >= m_activeCommands.size())
m_selectedCommandIndex = 0;
}
if (m_bCommandSelectPop && m_selectedCommandIndex >= 0)
{
data->DeleteChars(0, data->BufTextLen);
data->InsertChars(0, m_activeCommands[m_selectedCommandIndex]);
}
return;
}
const int prev_history_pos = m_historyPos;
if (data->EventKey == ImGuiKey_UpArrow)
{
if (m_historyPos == -1)
m_historyPos = m_historyCommands.Size - 1;
else if (m_historyPos > 0)
m_historyPos--;
}
else if (data->EventKey == ImGuiKey_DownArrow)
{
if (m_historyPos != -1)
if (++m_historyPos >= m_historyCommands.Size)
m_historyPos = -1;
}
// A better implementation would preserve the data on the current input line along with cursor position.
if (prev_history_pos != m_historyPos)
{
const char* history_str = (m_historyPos >= 0) ? m_historyCommands[m_historyPos] : "";
data->DeleteChars(0, data->BufTextLen);
data->InsertChars(0, history_str);
}
}
public:
Console() = default;
void init()
{
auto& int32Array = CVarSystem::get()->getArray<int32_t>();
auto& floatArray = CVarSystem::get()->getArray<float>();
auto& boolArray = CVarSystem::get()->getArray<bool>();
auto& stringArray = CVarSystem::get()->getArray<std::string>();
auto parseArray = [&](auto& cVarsArray)
{
auto typeBit = cVarsArray.lastCVar;
while (typeBit > 0)
{
typeBit--;
auto storageValue = cVarsArray.getCurrentStorage(typeBit);
m_commands.push_back(storageValue->parameter->name);
}
};
clearLog();
memset(m_inputBuffer, 0, sizeof(m_inputBuffer));
m_historyPos = -1;
parseArray(int32Array);
parseArray(floatArray);
parseArray(boolArray);
parseArray(stringArray);
m_bAutoScroll = true;
m_bScrollToBottom = false;
m_logCacheHandle = LoggerSystem::getDefaultLoggerSystem()->pushCallback([&](const std::string& info, ELogType type)
{
this->addLog(info, type);
});
}
void draw()
{
ImGui::Separator();
// Log type visibility toggle.
{
const auto buttonLogTypeVisibilityToggle = [this](ELogType index, std::string Name)
{
bool& visibility = m_logVisible[(size_t)index];
std::string numCount = (m_logTypeCount[(size_t)index] <= 99) ? std::format("{}", m_logTypeCount[(size_t)index]) : "99+";
ImGui::Checkbox(std::format(" {} [{}] ", Name, numCount).c_str(), &visibility);
};
const std::string sFilterName = combineIcon(Console_Filter, ICON_CONSOLE_FILTER);
m_filter.Draw(sFilterName.c_str(), 180);
ImGui::SameLine();
buttonLogTypeVisibilityToggle(ELogType::Trace, Console_LogTrace.imgui());
ImGui::SameLine();
buttonLogTypeVisibilityToggle(ELogType::Info, Console_LogInfo.imgui());
ImGui::SameLine();
buttonLogTypeVisibilityToggle(ELogType::Warn, Console_LogWarn.imgui());
ImGui::SameLine();
buttonLogTypeVisibilityToggle(ELogType::Error, Console_LogError.imgui());
ImGui::SameLine();
buttonLogTypeVisibilityToggle(ELogType::Other, Console_LogOther.imgui());
}
ImGui::Separator();
// Reserve enough left-over height for 1 separator + 1 input text
const float footerHeightToReserve = ImGui::GetStyle().ItemSpacing.y + ImGui::GetFrameHeightWithSpacing();
ImGui::BeginChild("ScrollingRegion", ImVec2(0, -footerHeightToReserve), false, ImGuiWindowFlags_HorizontalScrollbar);
if (!ImGui::IsItemHovered() && !m_bLogItemMenuPopup)
{
m_hoverItem = -1;
}
// Tighten spacing
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(4, 1));
// Print log items.
for (int i = 0; i < m_logItems.size(); i++)
{
const char* item = m_logItems[i].first.c_str();
if (!m_filter.PassFilter(item))
continue;
ImVec4 color;
bool bHasColor = false;
bool bOutSeparate = false;
if (m_logItems[i].second == ELogType::Error)
{
if (!m_logVisible[size_t(ELogType::Error)])
continue;
color = ImVec4(1.0f, 0.08f, 0.08f, 1.0f);
bHasColor = true;
}
else if (m_logItems[i].second == ELogType::Warn)
{
if (!m_logVisible[size_t(ELogType::Warn)])
continue;
color = ImVec4(1.0f, 1.0f, 0.1f, 1.0f);
bHasColor = true;
}
else if (m_logItems[i].second == ELogType::Trace)
{
if (!m_logVisible[size_t(ELogType::Trace)])
continue;
}
else if (m_logItems[i].second == ELogType::Info)
{
if (!m_logVisible[size_t(ELogType::Info)])
continue;
}
else if (strncmp(item, "# ", 2) == 0)
{
if (!m_logVisible[size_t(ELogType::Other)])
continue;
bOutSeparate = true;
color = ImVec4(1.0f, 0.8f, 0.6f, 1.0f);
bHasColor = true;
}
else if (strncmp(item, "Help: ", 5) == 0)
{
if (!m_logVisible[size_t(ELogType::Other)])
continue;
color = ImVec4(1.0f, 0.6f, 0.0f, 1.0f);
bHasColor = true;
}
else
{
if (!m_logVisible[size_t(ELogType::Other)])
continue;
}
if (bOutSeparate)
{
ImGui::Separator();
}
if (bHasColor)
ImGui::PushStyleColor(ImGuiCol_Text, color);
ImGui::Selectable(item, m_hoverItem == i);
if (bHasColor)
ImGui::PopStyleColor();
if (ImGui::IsItemHovered(ImGuiHoveredFlags_RectOnly) && !m_bLogItemMenuPopup)
{
m_hoverItem = i;
}
}
if (ImGui::BeginPopupContextWindow())
{
m_bLogItemMenuPopup = true;
std::string sCopyName = combineIcon(Console_CopyItem, ICON_CONSOLE_CLONE);
std::string sClearName = combineIcon(Console_ClearAll, ICON_CONSOLE_CLEAR);
if (ImGui::Selectable(sClearName.c_str()))
{
clearLog();
}
ImGui::Spacing();
if (m_hoverItem >= 0 && m_hoverItem < kMaxLogsItemNum)
{
if (ImGui::Selectable(sCopyName.c_str()))
{
ImGui::SetClipboardText(m_logItems[m_hoverItem].first.c_str());
}
}
else
{
ImGui::TextDisabled(sCopyName.c_str());
}
ImGui::EndPopup();
}
else
{
m_bLogItemMenuPopup = false;
}
if (m_bScrollToBottom || (m_bAutoScroll && ImGui::GetScrollY() >= ImGui::GetScrollMaxY()))
{
ImGui::SetScrollHereY(1.0f);
}
m_bScrollToBottom = false;
ImGui::PopStyleVar();
ImGui::EndChild();
ImGui::Separator();
// Command-line
bool reclaim_focus = false;
ImGuiInputTextFlags input_text_flags =
ImGuiInputTextFlags_EnterReturnsTrue |
ImGuiInputTextFlags_CallbackEdit |
ImGuiInputTextFlags_CallbackCompletion |
ImGuiInputTextFlags_CallbackHistory;
auto TipPos = ImGui::GetWindowPos();
TipPos.x += ImGui::GetStyle().WindowPadding.x;
const float WindowHeight = ImGui::GetWindowHeight();
const auto ItemSize = ImGui::GetTextLineHeightWithSpacing();
TipPos.y = TipPos.y + WindowHeight - (m_activeCommands.size() + 2.5f) * ItemSize;
// CVar Input.
if (ImGui::InputText(Console_CVarIn.imgui(), m_inputBuffer, IM_ARRAYSIZE(m_inputBuffer), input_text_flags, &textEditCallbackStub, (void*)this))
{
char* s = m_inputBuffer;
myStrtrim(s);
if (s[0])
{
execCommand(s);
m_activeCommands.clear();
}
strcpy(s, "");
reclaim_focus = true;
}
if (m_activeCommands.size() > 0 && ImGui::IsWindowFocused())
{
ImGui::SetNextWindowPos(TipPos);
ImGui::BeginTooltip();
for (size_t i = 0; i < m_activeCommands.size(); i++)
{
ImGui::Selectable(m_activeCommands[i], m_selectedCommandIndex == i);
}
ImGui::EndTooltip();
m_bCommandSelectPop = true;
}
else
{
m_bCommandSelectPop = false;
m_selectedCommandIndex = -1;
}
// Auto-focus on window apparition
ImGui::SetItemDefaultFocus();
if (reclaim_focus)
{
ImGui::SetKeyboardFocusHere(-1); // Auto focus previous widget
}
}
void release()
{
LoggerSystem::getDefaultLoggerSystem()->popCallback(m_logCacheHandle);
clearLog();
for (int i = 0; i < m_historyCommands.Size; i++)
{
free(m_historyCommands[i]);
}
}
int32_t textEditCallback(ImGuiInputTextCallbackData* data)
{
// do event callback.
switch (data->EventFlag)
{
case ImGuiInputTextFlags_CallbackCompletion:
{
inputCallbackOnComplete(data);
break;
}
case ImGuiInputTextFlags_CallbackEdit:
{
inputCallbackOnEdit(data);
break;
}
case ImGuiInputTextFlags_CallbackHistory:
{
inputCallbackOnHistory(data);
break;
}
}
return 0;
}
};
// Widget console.
WidgetConsole::WidgetConsole(Editor* editor) : Widget(editor, "Console")
{
m_console = std::make_unique<Console>();
}
void WidgetConsole::onInit()
{
m_console->init();
}
void WidgetConsole::onRelease()
{
m_console->release();
}
void WidgetConsole::onTick(const RuntimeModuleTickData& tickData, VulkanContext* context)
{
m_name = combineIcon(Console_Title, ICON_CONSOLE_CONSOLE_TITLE);
}
void WidgetConsole::onVisibleTick(const RuntimeModuleTickData& tickData)
{
m_console->draw();
}