Files
DoomRTX/neo/engine/tools/radiant/ConsoleDlg.cpp
T
2026-05-14 19:16:41 -07:00

701 lines
14 KiB
C++

/*
===========================================================================
IceTech GPL Source Code
Copyright (C) 2026 Justin Marshall
This file is part of the IceTech GPL Source Code (?IceTech Source Code?).
===========================================================================
*/
#include "precompiled.h"
#pragma hdrstop
#include "qe3.h"
#include "Radiant.h"
#include "ConsoleDlg.h"
// CConsoleDlg dialog
IMPLEMENT_DYNCREATE(CConsoleDlg, CDialog)
CConsoleDlg::CConsoleDlg(CWnd* pParent /*=NULL*/)
: CDialog(CConsoleDlg::IDD, pParent)
{
currentHistoryPosition = -1;
currentCommand = "";
saveCurrentCommand = true;
}
CConsoleDlg::~CConsoleDlg()
{
if (blackBrush.GetSafeHandle() != NULL) {
blackBrush.DeleteObject();
}
}
void CConsoleDlg::DoDataExchange(CDataExchange* pDX)
{
CDialog::DoDataExchange(pDX);
// Do not DDX IDC_EDIT_CONSOLE here.
// The resource control can stay as a normal EDIT placeholder.
// OnInitDialog destroys it and creates the RichEdit in its place.
DDX_Control(pDX, IDC_EDIT_INPUT, editInput);
}
BEGIN_MESSAGE_MAP(CConsoleDlg, CDialog)
ON_WM_SIZE()
ON_WM_SETFOCUS()
ON_WM_ACTIVATE()
ON_WM_CTLCOLOR()
END_MESSAGE_MAP()
BOOL CConsoleDlg::OnInitDialog()
{
CDialog::OnInitDialog();
// Needed before creating CRichEditCtrl.
// Safe if already initialized elsewhere.
AfxInitRichEdit2();
if (blackBrush.GetSafeHandle() == NULL) {
blackBrush.CreateSolidBrush(RGB(0, 0, 0));
}
CreateRichConsoleFromPlaceholder();
SetupRichConsoleDefaults();
if (editInput.GetSafeHwnd() != NULL) {
editInput.SetLimitText(1024);
editInput.SetWindowText("");
}
return TRUE;
}
void CConsoleDlg::CreateRichConsoleFromPlaceholder()
{
CWnd* oldConsole = GetDlgItem(IDC_EDIT_CONSOLE);
if (oldConsole == NULL || oldConsole->GetSafeHwnd() == NULL) {
return;
}
CRect rc;
oldConsole->GetWindowRect(&rc);
ScreenToClient(&rc);
oldConsole->DestroyWindow();
DWORD style =
WS_CHILD |
WS_VISIBLE |
WS_TABSTOP |
WS_VSCROLL |
ES_MULTILINE |
ES_AUTOVSCROLL |
ES_READONLY |
ES_NOHIDESEL |
ES_SAVESEL;
// Older MFC CRichEditCtrl::Create only takes 4 args.
// Do not use CreateEx here.
editConsole.Create(
style,
rc,
this,
IDC_EDIT_CONSOLE
);
}
void CConsoleDlg::SetupRichConsoleDefaults()
{
if (editConsole.GetSafeHwnd() == NULL) {
return;
}
editConsole.SetEventMask(0);
editConsole.SetOptions(ECOOP_OR, ECO_AUTOVSCROLL);
editConsole.SetReadOnly(TRUE);
// CRichEditCtrl uses LimitText(), not SetLimitText().
editConsole.LimitText(1024 * 1024);
editConsole.SetBackgroundColor(FALSE, RGB(0, 0, 0));
CHARFORMAT cf;
memset(&cf, 0, sizeof(cf));
cf.cbSize = sizeof(cf);
cf.dwMask = CFM_COLOR | CFM_FACE | CFM_SIZE;
cf.crTextColor = ICE_CONSOLE_DEFAULT_COLOR;
cf.yHeight = 180; // 9pt
lstrcpyn(cf.szFaceName, "Consolas", LF_FACESIZE);
editConsole.SetDefaultCharFormat(cf);
editConsole.SetSel(0, -1);
editConsole.SetSelectionCharFormat(cf);
editConsole.SetSel(-1, -1);
}
CString CConsoleDlg::NormalizeNewlines(const CString& in) const
{
CString out;
out.Preallocate(in.GetLength() + 16);
for (int i = 0; i < in.GetLength(); i++) {
TCHAR c = in[i];
if (c == '\r') {
if (i + 1 < in.GetLength() && in[i + 1] == '\n') {
out += "\r\n";
i++;
}
else {
out += "\r\n";
}
}
else if (c == '\n') {
out += "\r\n";
}
else {
out += c;
}
}
return out;
}
COLORREF CConsoleDlg::DoomColorForIndex(char c) const
{
switch (c) {
case '0':
return RGB(0, 0, 0); // black
case '1':
return RGB(255, 64, 64); // red
case '2':
return RGB(64, 255, 64); // green
case '3':
return RGB(255, 255, 64); // yellow
case '4':
return RGB(80, 140, 255); // blue
case '5':
return RGB(64, 255, 255); // cyan
case '6':
return RGB(255, 64, 255); // magenta
case '7':
return RGB(230, 230, 230); // white
case '8':
return RGB(160, 160, 160); // gray
case '9':
return RGB(255, 150, 32); // orange
case 'r':
case 'R':
return RGB(255, 64, 64);
case 'g':
case 'G':
return RGB(64, 255, 64);
case 'y':
case 'Y':
return RGB(255, 255, 64);
case 'b':
case 'B':
return RGB(80, 140, 255);
case 'c':
case 'C':
return RGB(64, 255, 255);
case 'm':
case 'M':
return RGB(255, 64, 255);
case 'w':
case 'W':
return RGB(230, 230, 230);
default:
return ICE_CONSOLE_DEFAULT_COLOR;
}
}
static bool ConsoleStartsWithNoCase(const char* s, const char* prefix)
{
if (s == NULL || prefix == NULL) {
return false;
}
while (*prefix != '\0') {
char a = *s;
char b = *prefix;
if (a >= 'A' && a <= 'Z') {
a = a - 'A' + 'a';
}
if (b >= 'A' && b <= 'Z') {
b = b - 'A' + 'a';
}
if (a != b) {
return false;
}
s++;
prefix++;
}
return true;
}
COLORREF CConsoleDlg::DoomColorForName(const char* name, int& consumed) const
{
consumed = 0;
struct colorName_t {
const char* name;
COLORREF color;
};
static const colorName_t names[] = {
{ "S_COLOR_BLACK", RGB(0, 0, 0) },
{ "S_COLOR_RED", RGB(255, 64, 64) },
{ "S_COLOR_GREEN", RGB(64, 255, 64) },
{ "S_COLOR_YELLOW", RGB(255, 255, 64) },
{ "S_COLOR_BLUE", RGB(80, 140, 255) },
{ "S_COLOR_CYAN", RGB(64, 255, 255) },
{ "S_COLOR_MAGENTA", RGB(255, 64, 255) },
{ "S_COLOR_WHITE", RGB(230, 230, 230) },
{ "S_COLOR_GRAY", RGB(160, 160, 160) },
{ "S_COLOR_GREY", RGB(160, 160, 160) },
{ "S_COLOR_ORANGE", RGB(255, 150, 32) },
{ "COLOR_BLACK", RGB(0, 0, 0) },
{ "COLOR_RED", RGB(255, 64, 64) },
{ "COLOR_GREEN", RGB(64, 255, 64) },
{ "COLOR_YELLOW", RGB(255, 255, 64) },
{ "COLOR_BLUE", RGB(80, 140, 255) },
{ "COLOR_CYAN", RGB(64, 255, 255) },
{ "COLOR_MAGENTA", RGB(255, 64, 255) },
{ "COLOR_WHITE", RGB(230, 230, 230) },
{ "COLOR_GRAY", RGB(160, 160, 160) },
{ "COLOR_GREY", RGB(160, 160, 160) },
{ "COLOR_ORANGE", RGB(255, 150, 32) }
};
for (int i = 0; i < sizeof(names) / sizeof(names[0]); i++) {
if (ConsoleStartsWithNoCase(name, names[i].name)) {
consumed = (int)strlen(names[i].name);
return names[i].color;
}
}
return ICE_CONSOLE_DEFAULT_COLOR;
}
bool CConsoleDlg::TryConsumeColorToken(const char* s, int& consumed, COLORREF& color) const
{
consumed = 0;
color = ICE_CONSOLE_DEFAULT_COLOR;
if (s == NULL || s[0] == '\0') {
return false;
}
// Doom/idTech style color escape: ^1, ^2, ^7, etc.
if (s[0] == '^' && s[1] != '\0') {
// ^^ means literal caret, not a color.
if (s[1] == '^') {
return false;
}
color = DoomColorForIndex(s[1]);
consumed = 2;
return true;
}
// Optional support if some code emits literal token names.
int namedConsumed = 0;
COLORREF namedColor = DoomColorForName(s, namedConsumed);
if (namedConsumed > 0) {
color = namedColor;
consumed = namedConsumed;
return true;
}
return false;
}
void CConsoleDlg::AppendConsoleRun(const CString& text, COLORREF color)
{
if (text.IsEmpty() || editConsole.GetSafeHwnd() == NULL) {
return;
}
CString normalized = NormalizeNewlines(text);
editConsole.SetReadOnly(FALSE);
int len = editConsole.GetTextLength();
editConsole.SetSel(len, len);
CHARFORMAT cf;
memset(&cf, 0, sizeof(cf));
cf.cbSize = sizeof(cf);
cf.dwMask = CFM_COLOR | CFM_FACE | CFM_SIZE;
cf.crTextColor = color;
cf.yHeight = 180;
lstrcpyn(cf.szFaceName, "Consolas", LF_FACESIZE);
editConsole.SetSelectionCharFormat(cf);
editConsole.ReplaceSel(normalized, FALSE);
editConsole.SetReadOnly(TRUE);
}
void CConsoleDlg::TrimConsoleIfNeeded()
{
if (editConsole.GetSafeHwnd() == NULL) {
return;
}
const int maxChars = 512 * 1024;
const int trimToChars = 384 * 1024;
int len = editConsole.GetTextLength();
if (len <= maxChars) {
return;
}
int removeCount = len - trimToChars;
editConsole.SetReadOnly(FALSE);
editConsole.SetSel(0, removeCount);
editConsole.ReplaceSel("", FALSE);
editConsole.SetReadOnly(TRUE);
}
void CConsoleDlg::AppendConsoleTextColored(const char* text)
{
if (text == NULL || editConsole.GetSafeHwnd() == NULL) {
return;
}
COLORREF currentColor = ICE_CONSOLE_DEFAULT_COLOR;
CString run;
const char* p = text;
while (*p != '\0') {
int consumed = 0;
COLORREF newColor = ICE_CONSOLE_DEFAULT_COLOR;
if (TryConsumeColorToken(p, consumed, newColor)) {
if (!run.IsEmpty()) {
AppendConsoleRun(run, currentColor);
run.Empty();
}
currentColor = newColor;
p += consumed;
continue;
}
if (p[0] == '^' && p[1] == '^') {
run += '^';
p += 2;
continue;
}
run += *p;
p++;
}
if (!run.IsEmpty()) {
AppendConsoleRun(run, currentColor);
}
TrimConsoleIfNeeded();
int len = editConsole.GetTextLength();
editConsole.SetSel(len, len);
editConsole.SendMessage(EM_SCROLLCARET, 0, 0);
}
void CConsoleDlg::AddText(const char* msg)
{
if (msg == NULL || editConsole.GetSafeHwnd() == NULL) {
return;
}
idStr work;
work = msg;
// DO NOT call RemoveColors() here.
// The RichEdit append path consumes Doom color codes and applies colors.
work = CEntityDlg::TranslateString(work.c_str());
AppendConsoleTextColored(work.c_str());
}
void CConsoleDlg::OnSize(UINT nType, int cx, int cy)
{
CDialog::OnSize(nType, cx, cy);
if (editInput.GetSafeHwnd() == NULL) {
return;
}
CRect clientRect;
GetClientRect(&clientRect);
CRect inputRect;
editInput.GetWindowRect(&inputRect);
const int margin = 4;
const int inputHeight = inputRect.Height();
editInput.SetWindowPos(
NULL,
margin,
clientRect.Height() - margin - inputHeight,
clientRect.Width() - margin * 2,
inputHeight,
SWP_SHOWWINDOW
);
if (editConsole.GetSafeHwnd() != NULL) {
editConsole.SetWindowPos(
NULL,
margin,
margin,
clientRect.Width() - margin * 2,
clientRect.Height() - inputHeight - margin * 3,
SWP_SHOWWINDOW
);
}
}
BOOL CConsoleDlg::PreTranslateMessage(MSG* pMsg)
{
if (pMsg->hwnd == editInput.GetSafeHwnd()) {
if (pMsg->message == WM_KEYDOWN && pMsg->wParam == VK_ESCAPE) {
Select_Deselect();
g_pParentWnd->SetFocus();
return TRUE;
}
if (pMsg->message == WM_KEYDOWN && pMsg->wParam == VK_RETURN) {
ExecuteCommand();
return TRUE;
}
if (pMsg->message == WM_KEYDOWN && pMsg->wParam == VK_UP) {
// Save off the current in-progress command so we can get back to it.
if (saveCurrentCommand == true) {
CString str;
editInput.GetWindowText(str);
currentCommand = str.GetBuffer(0);
saveCurrentCommand = false;
}
if (consoleHistory.Num() > 0) {
if (currentHistoryPosition < 0) {
currentHistoryPosition = consoleHistory.Num() - 1;
}
if (currentHistoryPosition >= consoleHistory.Num()) {
currentHistoryPosition = consoleHistory.Num() - 1;
}
editInput.SetWindowText(consoleHistory[currentHistoryPosition]);
int selLocation = consoleHistory[currentHistoryPosition].Length();
editInput.SetSel(selLocation, selLocation);
if (currentHistoryPosition > 0) {
--currentHistoryPosition;
}
}
return TRUE;
}
if (pMsg->message == WM_KEYDOWN && pMsg->wParam == VK_DOWN) {
int selLocation = 0;
if (currentHistoryPosition < consoleHistory.Num() - 1) {
++currentHistoryPosition;
editInput.SetWindowText(consoleHistory[currentHistoryPosition]);
selLocation = consoleHistory[currentHistoryPosition].Length();
}
else {
editInput.SetWindowText(currentCommand);
selLocation = currentCommand.Length();
currentCommand.Clear();
saveCurrentCommand = true;
}
editInput.SetSel(selLocation, selLocation);
return TRUE;
}
if (pMsg->message == WM_KEYDOWN && pMsg->wParam == VK_TAB) {
common->Printf("Command History\n----------------\n");
for (int i = 0; i < consoleHistory.Num(); i++) {
common->Printf("[cmd %d]: %s\n", i, consoleHistory[i].c_str());
}
common->Printf("----------------\n");
return TRUE;
}
if (pMsg->message == WM_KEYDOWN && pMsg->wParam == VK_NEXT) {
if (editConsole.GetSafeHwnd() != NULL) {
editConsole.LineScroll(10);
}
return TRUE;
}
if (pMsg->message == WM_KEYDOWN && pMsg->wParam == VK_PRIOR) {
if (editConsole.GetSafeHwnd() != NULL) {
editConsole.LineScroll(-10);
}
return TRUE;
}
if (pMsg->message == WM_KEYDOWN && pMsg->wParam == VK_HOME) {
if (editConsole.GetSafeHwnd() != NULL) {
editConsole.LineScroll(-editConsole.GetLineCount());
}
return TRUE;
}
if (pMsg->message == WM_KEYDOWN && pMsg->wParam == VK_END) {
if (editConsole.GetSafeHwnd() != NULL) {
editConsole.LineScroll(editConsole.GetLineCount());
}
return TRUE;
}
}
return CDialog::PreTranslateMessage(pMsg);
}
void CConsoleDlg::OnSetFocus(CWnd* pOldWnd)
{
CDialog::OnSetFocus(pOldWnd);
if (editInput.GetSafeHwnd() != NULL) {
editInput.SetFocus();
}
}
void CConsoleDlg::SetConsoleText(const idStr& text)
{
if (editInput.GetSafeHwnd() != NULL) {
editInput.Clear();
editInput.SetWindowText(text.c_str());
}
}
void CConsoleDlg::ExecuteCommand(const idStr& cmd)
{
CString str;
if (cmd.Length() > 0) {
str = cmd;
}
else {
editInput.GetWindowText(str);
}
if (str != "") {
editInput.SetWindowText("");
common->Printf("%s\n", str.GetBuffer(0));
// Avoid adding multiple identical commands in a row.
int index = consoleHistory.Num();
if (index == 0 || str.GetBuffer(0) != consoleHistory[index - 1]) {
// Keep the history to 16 commands, removing the oldest command.
if (consoleHistory.Num() > 16) {
consoleHistory.RemoveIndex(0);
}
currentHistoryPosition = consoleHistory.Append(str.GetBuffer(0));
}
else {
currentHistoryPosition = consoleHistory.Num() - 1;
}
currentCommand.Clear();
saveCurrentCommand = true;
bool propogateCommand = true;
// Process some of our own special commands.
if (str.CompareNoCase("clear") == 0) {
if (editConsole.GetSafeHwnd() != NULL) {
editConsole.SetReadOnly(FALSE);
editConsole.SetSel(0, -1);
editConsole.ReplaceSel("", FALSE);
editConsole.SetReadOnly(TRUE);
}
propogateCommand = false;
}
else if (str.CompareNoCase("edit") == 0) {
propogateCommand = false;
}
if (propogateCommand) {
cmdSystem->BufferCommandText(CMD_EXEC_NOW, str);
}
Sys_UpdateWindows(W_ALL);
}
}
void CConsoleDlg::OnActivate(UINT nState, CWnd* pWndOther, BOOL bMinimized)
{
CDialog::OnActivate(nState, pWndOther, bMinimized);
if (nState == WA_ACTIVE || nState == WA_CLICKACTIVE) {
if (editInput.GetSafeHwnd() != NULL) {
editInput.SetFocus();
}
}
}
HBRUSH CConsoleDlg::OnCtlColor(CDC* pDC, CWnd* pWnd, UINT nCtlColor)
{
HBRUSH hbr = CDialog::OnCtlColor(pDC, pWnd, nCtlColor);
if (pWnd != NULL && pWnd->GetSafeHwnd() == editInput.GetSafeHwnd()) {
pDC->SetBkColor(RGB(0, 0, 0));
pDC->SetTextColor(ICE_CONSOLE_DEFAULT_COLOR);
return (HBRUSH)blackBrush.GetSafeHandle();
}
return hbr;
}