/* ** c_console.cpp ** Implements the console itself ** **--------------------------------------------------------------------------- ** Copyright 1998-2006 Randy Heit ** All rights reserved. ** ** Redistribution and use in source and binary forms, with or without ** modification, are permitted provided that the following conditions ** are met: ** ** 1. Redistributions of source code must retain the above copyright ** notice, this list of conditions and the following disclaimer. ** 2. Redistributions in binary form must reproduce the above copyright ** notice, this list of conditions and the following disclaimer in the ** documentation and/or other materials provided with the distribution. ** 3. The name of the author may not be used to endorse or promote products ** derived from this software without specific prior written permission. ** ** THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR ** IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES ** OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. ** IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, ** INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT ** NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, ** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY ** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT ** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF ** THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. **--------------------------------------------------------------------------- ** */ #include #include "templates.h" #include "version.h" #include "c_bind.h" #include "c_console.h" #include "c_cvars.h" #include "c_dispatch.h" #include "gamestate.h" #include "v_text.h" #include "filesystem.h" #include "d_gui.h" #include "cmdlib.h" #include "d_event.h" #include "c_consolebuffer.h" #include "utf8.h" #include "v_2ddrawer.h" #include "v_draw.h" #include "v_font.h" #include "printf.h" #include "inputstate.h" #include "i_time.h" #include "gamecvars.h" #include "i_system.h" #include "s_soundinternal.h" #include "engineerrors.h" #include "gamecontrol.h" #include "v_video.h" #include "v_draw.h" #include "g_input.h" #include "razemenu.h" #include "raze_music.h" #include "gstrings.h" #include "menustate.h" #define LEFTMARGIN 8 #define RIGHTMARGIN 8 #define BOTTOMARGIN 12 extern bool hud_toggled; void D_ToggleHud(); CUSTOM_CVAR(Int, con_buffersize, -1, CVAR_ARCHIVE | CVAR_GLOBALCONFIG) { // ensure a minimum size if (self >= 0 && self < 128) self = 128; } FConsoleBuffer *conbuffer; static void C_TabComplete (bool goForward); static bool C_TabCompleteList (); static bool TabbedLast; // True if last key pressed was tab static bool TabbedList; // True if tab list was shown CVAR(Bool, con_notablist, false, CVAR_ARCHIVE) static FGameTexture *conback; static uint32_t conshade; static bool conline; extern bool advancedemo; extern FBaseCVar *CVars; extern FConsoleCommand *Commands[FConsoleCommand::HASH_SIZE]; unsigned ConCols; int ConWidth; bool vidactive = false; bool cursoron = false; int ConBottom, ConScroll, RowAdjust; uint64_t CursorTicker; constate_e ConsoleState = c_up; double NotifyFontScale = 1; void C_SetNotifyFontScale(double scale) { NotifyFontScale = scale; } static int TopLine, InsertLine; static void ClearConsole (); struct GameAtExit { GameAtExit(FString str) : Command(str) {} GameAtExit *Next; FString Command; }; static GameAtExit *ExitCmdList; #define SCROLLUP 1 #define SCROLLDN 2 #define SCROLLNO 0 // Buffer for AddToConsole() static char *work = NULL; static int worklen = 0; CVAR(Float, con_notifytime, 3.f, CVAR_ARCHIVE) CUSTOM_CVAR(Float, con_notifyscale, 1, CVAR_ARCHIVE) { if (self < 0.36f) self = 0.36f; if (self > 1) self = 1; } CVAR(Bool, con_centernotify, false, CVAR_ARCHIVE) CVAR(Bool, con_notify_advanced, false, CVAR_ARCHIVE) CVAR(Bool, con_pulsetext, false, CVAR_ARCHIVE) CUSTOM_CVAR(Int, con_scale, 0, CVAR_ARCHIVE) { if (self < 0) self = 0; } CUSTOM_CVAR(Float, con_alpha, 0.75f, CVAR_ARCHIVE) { if (self < 0.f) self = 0.f; if (self > 1.f) self = 1.f; } // Command to run when Ctrl-D is pressed at start of line CVAR(String, con_ctrl_d, "", CVAR_ARCHIVE | CVAR_GLOBALCONFIG) CVAR(Int, developer, 0, CVAR_ARCHIVE | CVAR_GLOBALCONFIG) EXTERN_CVAR(Int, uiscale); CVAR(Bool, generic_ui, false, CVAR_ARCHIVE) struct History { struct History *Older; struct History *Newer; FString String; }; struct FCommandBuffer { private: std::u32string Text; unsigned CursorPos = 0; unsigned StartPos = 0; // First character to display unsigned CursorPosCells = 0; unsigned StartPosCells = 0; std::u32string YankBuffer; // Deleted text buffer public: bool AppendToYankBuffer = false; // Append consecutive deletes to buffer FCommandBuffer() = default; FCommandBuffer(const FCommandBuffer &o) { Text = o.Text; CursorPos = o.CursorPos; StartPos = o.StartPos; } FString GetText() const { FString build; for (auto chr : Text) build.AppendCharacter(chr); return build; } size_t TextLength() const { return Text.length(); } void Draw(int x, int y, int scale, bool cursor) { if (scale == 1) { DrawChar(twod, CurrentConsoleFont, CR_ORANGE, x, y, '\x1c', TAG_DONE); DrawText(twod, CurrentConsoleFont, CR_ORANGE, x + CurrentConsoleFont->GetCharWidth(0x1c), y, &Text[StartPos], TAG_DONE); if (cursor) { DrawChar(twod, CurrentConsoleFont, CR_YELLOW, x + CurrentConsoleFont->GetCharWidth(0x1c) + (CursorPosCells - StartPosCells) * CurrentConsoleFont->GetCharWidth(0xb), y, '\xb', TAG_DONE); } } else { DrawChar(twod, CurrentConsoleFont, CR_ORANGE, x, y, '\x1c', DTA_VirtualWidth, twod->GetWidth() / scale, DTA_VirtualHeight, twod->GetHeight() / scale, DTA_KeepRatio, true, TAG_DONE); DrawText(twod, CurrentConsoleFont, CR_ORANGE, x + CurrentConsoleFont->GetCharWidth(0x1c), y, &Text[StartPos], DTA_VirtualWidth, twod->GetWidth() / scale, DTA_VirtualHeight, twod->GetHeight() / scale, DTA_KeepRatio, true, TAG_DONE); if (cursor) { DrawChar(twod, CurrentConsoleFont, CR_YELLOW, x + CurrentConsoleFont->GetCharWidth(0x1c) + (CursorPosCells - StartPosCells) * CurrentConsoleFont->GetCharWidth(0xb), y, '\xb', DTA_VirtualWidth, twod->GetWidth() / scale, DTA_VirtualHeight, twod->GetHeight() / scale, DTA_KeepRatio, true, TAG_DONE); } } } unsigned CalcCellSize(unsigned length) { unsigned cellcount = 0; for (unsigned i = 0; i < length; i++) { int w; NewConsoleFont->GetChar(Text[i], CR_UNTRANSLATED, &w); cellcount += w / 9; } return cellcount; } unsigned CharsForCells(unsigned cellin, bool *overflow) { unsigned chars = 0; int cells = cellin; while (cells > 0) { int w; NewConsoleFont->GetChar(Text[chars++], CR_UNTRANSLATED, &w); cells -= w / 9; } *overflow = (cells < 0); return chars; } void MakeStartPosGood() { // Make sure both values point to something valid. if (CursorPos > Text.length()) CursorPos = (unsigned)Text.length(); if (StartPos > Text.length()) StartPos = (unsigned)Text.length(); CursorPosCells = CalcCellSize(CursorPos); StartPosCells = CalcCellSize(StartPos); unsigned LengthCells = CalcCellSize((unsigned)Text.length()); int n = StartPosCells; unsigned cols = ConCols / active_con_scale(twod); if (StartPosCells >= LengthCells) { // Start of visible line is beyond end of line n = CursorPosCells - cols + 2; } if ((CursorPosCells - StartPosCells) >= cols - 2) { // The cursor is beyond the visible part of the line n = CursorPosCells - cols + 2; } if (StartPosCells > CursorPosCells) { // The cursor is in front of the visible part of the line n = CursorPosCells; } StartPosCells = std::max(0, n); bool overflow; StartPos = CharsForCells(StartPosCells, &overflow); if (overflow) { // We ended up in the middle of a double cell character, so set the start to the following character. StartPosCells++; StartPos = CharsForCells(StartPosCells, &overflow); } } void CursorStart() { CursorPos = 0; StartPos = 0; CursorPosCells = 0; StartPosCells = 0; } void CursorEnd() { CursorPos = (unsigned)Text.length(); MakeStartPosGood(); } private: void MoveCursorLeft() { CursorPos--; } void MoveCursorRight() { CursorPos++; } public: void CursorLeft() { if (CursorPos > 0) { MoveCursorLeft(); MakeStartPosGood(); } } void CursorRight() { if (CursorPos < Text.length()) { MoveCursorRight(); MakeStartPosGood(); } } void CursorWordLeft() { if (CursorPos > 0) { do MoveCursorLeft(); while (CursorPos > 0 && Text[CursorPos - 1] != ' '); MakeStartPosGood(); } } void CursorWordRight() { if (CursorPos < Text.length()) { do MoveCursorRight(); while (CursorPos < Text.length() && Text[CursorPos] != ' '); MakeStartPosGood(); } } void DeleteLeft() { if (CursorPos > 0) { MoveCursorLeft(); Text.erase(CursorPos, 1); MakeStartPosGood(); } } void DeleteRight() { if (CursorPos < Text.length()) { Text.erase(CursorPos, 1); MakeStartPosGood(); } } void DeleteWordLeft() { if (CursorPos > 0) { auto now = CursorPos; CursorWordLeft(); if (AppendToYankBuffer) { YankBuffer = Text.substr(CursorPos, now - CursorPos) + YankBuffer; } else { YankBuffer = Text.substr(CursorPos, now - CursorPos); } Text.erase(CursorPos, now - CursorPos); MakeStartPosGood(); } } void DeleteLineLeft() { if (CursorPos > 0) { if (AppendToYankBuffer) { YankBuffer = Text.substr(0, CursorPos) + YankBuffer; } else { YankBuffer = Text.substr(0, CursorPos); } Text.erase(0, CursorPos); CursorStart(); } } void DeleteLineRight() { if (CursorPos < Text.length()) { if (AppendToYankBuffer) { YankBuffer += Text.substr(CursorPos, Text.length() - CursorPos); } else { YankBuffer = Text.substr(CursorPos, Text.length() - CursorPos); } Text.resize(CursorPos); CursorEnd(); } } void AddChar(int character) { if (Text.length() == 0) { Text += character; } else { Text.insert(CursorPos, 1, character); } CursorPos++; MakeStartPosGood(); } void AddString(FString clip) { if (clip.IsNotEmpty()) { // Only paste the first line. long brk = clip.IndexOfAny("\r\n\b"); std::u32string build; if (brk >= 0) { clip.Truncate(brk); } auto strp = (const uint8_t*)clip.GetChars(); while (auto chr = GetCharFromString(strp)) build += chr; if (Text.length() == 0) { Text = build; } else { Text.insert(CursorPos, build); } CursorPos += (unsigned)build.length(); MakeStartPosGood(); } } void SetString(const FString &str) { Text.clear(); auto strp = (const uint8_t*)str.GetChars(); while (auto chr = GetCharFromString(strp)) Text += chr; CursorEnd(); MakeStartPosGood(); } void AddYankBuffer() { if (YankBuffer.length() > 0) { if (Text.length() == 0) { Text = YankBuffer; } else { Text.insert(CursorPos, YankBuffer); } CursorPos += (unsigned)YankBuffer.length(); MakeStartPosGood(); } } }; static FCommandBuffer CmdLine; #define MAXHISTSIZE 50 static struct History *HistHead = NULL, *HistTail = NULL, *HistPos = NULL; static int HistSize; #define NUMNOTIFIES 4 #define NOTIFYFADETIME 6 struct FNotifyText { int TimeOut; int Ticker; int PrintLevel; FString Text; }; struct FNotifyBuffer { public: FNotifyBuffer(); void AddString(int printlevel, FString source); void Shift(int maxlines); void Clear() { Text.Clear(); } void Tick(); void Draw(); void DrawNative(); private: TArray Text; int Top; int TopGoal; enum { NEWLINE, APPENDLINE, REPLACELINE } AddType; }; static FNotifyBuffer NotifyStrings; CUSTOM_CVAR(Int, con_notifylines, NUMNOTIFIES, CVAR_GLOBALCONFIG | CVAR_ARCHIVE) { NotifyStrings.Shift(self); } int PrintColors[PRINTLEVELS+2] = { CR_UNTRANSLATED, CR_GOLD, CR_GRAY, CR_GREEN, CR_GREEN, CR_UNTRANSLATED }; static void setmsgcolor (int index, int color); FILE *Logfile = NULL; FIntCVar msglevel ("msg", 0, CVAR_ARCHIVE); CUSTOM_CVAR (Int, msg0color, CR_UNTRANSLATED, CVAR_ARCHIVE) { setmsgcolor (0, self); } CUSTOM_CVAR (Int, msg1color, CR_GOLD, CVAR_ARCHIVE) { setmsgcolor (1, self); } CUSTOM_CVAR (Int, msg2color, CR_GRAY, CVAR_ARCHIVE) { setmsgcolor (2, self); } CUSTOM_CVAR (Int, msg3color, CR_GREEN, CVAR_ARCHIVE) { setmsgcolor (3, self); } CUSTOM_CVAR (Int, msg4color, CR_GREEN, CVAR_ARCHIVE) { setmsgcolor (4, self); } CUSTOM_CVAR (Int, msgmidcolor, CR_UNTRANSLATED, CVAR_ARCHIVE) { setmsgcolor (PRINTLEVELS, self); } CUSTOM_CVAR (Int, msgmidcolor2, CR_BROWN, CVAR_ARCHIVE) { setmsgcolor (PRINTLEVELS+1, self); } void C_InitConback() { #if 0 conback = TexMan.CheckForTexture ("CONBACK", ETextureType::MiscPatch); if (!conback.isValid()) { conback = TexMan.GetTextureID (gameinfo.TitlePage, ETextureType::MiscPatch); conshade = MAKEARGB(175,0,0,0); conline = true; } else { conshade = 0; conline = false; } #else conshade = MAKEARGB(175, 0, 0, 0); conline = true; #endif } void C_InitConsole (int width, int height, bool ingame) { int cwidth, cheight; vidactive = ingame; if (CurrentConsoleFont != NULL) { cwidth = CurrentConsoleFont->GetCharWidth ('M'); cheight = CurrentConsoleFont->GetHeight(); } else { cwidth = cheight = 8; } ConWidth = (width - LEFTMARGIN - RIGHTMARGIN); ConCols = ConWidth / cwidth; if (conbuffer == NULL) conbuffer = new FConsoleBuffer; } //========================================================================== // // CCMD atexit // //========================================================================== UNSAFE_CCMD (atexit) { if (argv.argc() == 1) { Printf ("Registered atexit commands:\n"); GameAtExit *record = ExitCmdList; while (record != NULL) { Printf ("%s\n", record->Command.GetChars()); record = record->Next; } return; } for (int i = 1; i < argv.argc(); ++i) { GameAtExit *record = new GameAtExit(argv[i]); record->Next = ExitCmdList; ExitCmdList = record; } } //========================================================================== // // C_DeinitConsole // // Executes the contents of the atexit cvar, if any, at quit time. // Then releases all of the console's memory. // //========================================================================== void C_DeinitConsole () { GameAtExit *cmd = ExitCmdList; while (cmd != NULL) { GameAtExit *next = cmd->Next; AddCommandString (cmd->Command); delete cmd; cmd = next; } // Free command history History *hist = HistTail; while (hist != NULL) { History *next = hist->Newer; delete hist; hist = next; } HistTail = HistHead = HistPos = NULL; // Free cvars allocated at runtime FBaseCVar *var, *next, **nextp; for (var = CVars, nextp = &CVars; var != NULL; var = next) { next = var->m_Next; if (var->GetFlags() & CVAR_UNSETTABLE) { delete var; *nextp = next; } else { nextp = &var->m_Next; } } // Free alias commands. (i.e. The "commands" that can be allocated // at runtime.) for (size_t i = 0; i < countof(Commands); ++i) { FConsoleCommand *cmd = Commands[i]; while (cmd != NULL) { FConsoleCommand *next = cmd->m_Next; if (cmd->IsAlias()) { delete cmd; } cmd = next; } } // Make sure all tab commands are cleared before the memory for // their names is deallocated. C_ClearTabCommands (); C_ClearDynCCmds(); // Free AddToConsole()'s work buffer if (work != NULL) { free (work); work = NULL; worklen = 0; } if (conbuffer != NULL) { delete conbuffer; conbuffer = NULL; } } static void ClearConsole () { if (conbuffer != NULL) { conbuffer->Clear(); } TopLine = InsertLine = 0; } static void setmsgcolor (int index, int color) { if ((unsigned)color >= (unsigned)NUM_TEXT_COLORS) color = 0; PrintColors[index] = color; } FNotifyBuffer::FNotifyBuffer() { Top = TopGoal = 0; AddType = NEWLINE; } void FNotifyBuffer::Shift(int maxlines) { if (maxlines >= 0 && Text.Size() > (unsigned)maxlines) { Text.Delete(0, Text.Size() - maxlines); } } void FNotifyBuffer::AddString(int printlevel, FString source) { TArray lines; int width; if (hud_messages == 0 || screen == nullptr || source.IsEmpty() || gamestate == GS_FULLCONSOLE || gamestate == GS_MENUSCREEN || con_notifylines == 0) return; auto screenratio = ActiveRatio(screen->GetWidth(), screen->GetHeight()); FFont* font = generic_ui ? NewSmallFont : SmallFont ? SmallFont : AlternativeSmallFont; if (font == nullptr) return; // Without an initialized font we cannot handle the message (this is for those which come here before the font system is ready.) double fontscale = (generic_ui? 0.7 : NotifyFontScale) * con_notifyscale; width = int(320 * (screenratio / 1.333) / fontscale); if (AddType == APPENDLINE && Text.Size() > 0 && Text[Text.Size() - 1].PrintLevel == printlevel) { FString str = Text[Text.Size() - 1].Text + source; lines = V_BreakLines (font, width, str); } else { lines = V_BreakLines (font, width, source); if (AddType == APPENDLINE) { AddType = NEWLINE; } } if (lines.Size() == 0) return; for (auto &line : lines) { FNotifyText newline; newline.Text = line.Text; newline.TimeOut = int(con_notifytime * GameTicRate); newline.Ticker = 0; newline.PrintLevel = printlevel; if (AddType == NEWLINE || Text.Size() == 0) { if (con_notifylines > 0) { Shift(con_notifylines - 1); } Text.Push(newline); } else { Text[Text.Size() - 1] = newline; } AddType = NEWLINE; } switch (source[source.Len()-1]) { case '\r': AddType = REPLACELINE; break; case '\n': AddType = NEWLINE; break; default: AddType = APPENDLINE; break; } TopGoal = 0; } void AddToConsole (int printlevel, const char *text) { conbuffer->AddText(printlevel, MakeUTF8(text)); } //========================================================================== // // // //========================================================================== void WriteLineToLog(FILE *LogFile, const char *outline) { // Strip out any color escape sequences before writing to the log file TArray copy(strlen(outline) + 1); const char * srcp = outline; char * dstp = copy.Data(); while (*srcp != 0) { if (*srcp != TEXTCOLOR_ESCAPE) { *dstp++ = *srcp++; } else if (srcp[1] == '[') { srcp += 2; while (*srcp != ']' && *srcp != 0) srcp++; if (*srcp == ']') srcp++; } else { if (srcp[1] != 0) srcp += 2; else break; } } *dstp = 0; fputs(copy.Data(), LogFile); fflush(LogFile); } extern bool gameisdead; int PrintString (int iprintlevel, const char *outline) { if (gameisdead) return 0; if (!conbuffer) return 0; // when called too early int printlevel = iprintlevel & PRINT_TYPES; if (printlevel < msglevel || *outline == '\0') { return 0; } if (printlevel != PRINT_LOG || Logfile != nullptr) { // Convert everything coming through here to UTF-8 so that all console text is in a consistent format int count; outline = MakeUTF8(outline, &count); if (printlevel != PRINT_LOG) { I_PrintStr(outline); conbuffer->AddText(printlevel, outline); if (!(iprintlevel & PRINT_NONOTIFY)) { if (vidactive && ((iprintlevel & PRINT_NOTIFY) || con_notify_advanced)) { NotifyStrings.AddString(printlevel, outline); } } } if (Logfile != nullptr && !(iprintlevel & PRINT_NOLOG)) { WriteLineToLog(Logfile, outline); } return count; } return 0; // Don't waste time on calculating this if nothing at all was printed... } void C_ClearMessages() { NotifyStrings.Clear(); } int VPrintf (int printlevel, const char *format, va_list parms) { FString outline; outline.VFormat (format, parms); return PrintString (printlevel, outline.GetChars()); } int Printf (int printlevel, const char *format, ...) { va_list argptr; int count; va_start (argptr, format); count = VPrintf (printlevel, format, argptr); va_end (argptr); return count; } int Printf (const char *format, ...) { va_list argptr; int count; va_start (argptr, format); count = VPrintf (PRINT_HIGH, format, argptr); va_end (argptr); return count; } int DPrintf (int level, const char *format, ...) { va_list argptr; int count; if (developer >= level) { va_start (argptr, format); count = VPrintf (PRINT_HIGH, format, argptr); va_end (argptr); return count; } else { return 0; } } void C_FlushDisplay () { NotifyStrings.Clear(); } void C_AdjustBottom () { if (gamestate == GS_FULLCONSOLE || gamestate == GS_STARTUP) ConBottom = twod->GetHeight(); else if (ConBottom > twod->GetHeight() / 2 || ConsoleState == c_down) ConBottom = twod->GetHeight() / 2; } void C_NewModeAdjust () { C_InitConsole (screen->GetWidth(), screen->GetHeight(), true); C_FlushDisplay (); C_AdjustBottom (); } int consoletic = 0; void C_Ticker() { static int lasttic = 0; consoletic++; if (lasttic == 0) lasttic = consoletic - 1; if (con_buffersize > 0) { conbuffer->ResizeBuffer(con_buffersize); } if (ConsoleState != c_up) { if (ConsoleState == c_falling) { ConBottom += (consoletic - lasttic) * (twod->GetHeight() * 2 / 25); if (ConBottom >= twod->GetHeight() / 2) { ConBottom = twod->GetHeight() / 2; ConsoleState = c_down; } } else if (ConsoleState == c_rising) { ConBottom -= (consoletic - lasttic) * (twod->GetHeight() * 2 / 25); if (ConBottom <= 0) { ConsoleState = c_up; ConBottom = 0; } } } lasttic = consoletic; NotifyStrings.Tick(); } void FNotifyBuffer::Tick() { if (TopGoal > Top) { Top++; } else if (TopGoal < Top) { Top--; } // Remove lines from the beginning that have expired. unsigned i; for (i = 0; i < Text.Size(); ++i) { Text[i].Ticker++; } for (i = 0; i < Text.Size(); ++i) { if (Text[i].TimeOut != 0 && Text[i].TimeOut > Text[i].Ticker) break; } if (i > 0) { Text.Delete(0, i); FFont* font = generic_ui ? NewSmallFont : SmallFont ? SmallFont : AlternativeSmallFont; Top += font->GetHeight() / NotifyFontScale; } } void FNotifyBuffer::DrawNative() { // Native display is: // * centered at the top and pulsing for Duke // * centered shifted down and not pulsing for Shadow Warrior // * top left for Exhumed // * 4 lines with the tiny font for Blood. (same mechanic as the regular one, just a different font and scale.) bool center = g_gameType & (GAMEFLAG_DUKE | GAMEFLAG_NAM | GAMEFLAG_WW2GI | GAMEFLAG_RR | GAMEFLAG_SW); bool pulse = g_gameType & (GAMEFLAG_DUKE | GAMEFLAG_NAM | GAMEFLAG_WW2GI | GAMEFLAG_RR); unsigned topline = g_gameType & GAMEFLAG_BLOOD ? 0 : Text.Size() - 1; FFont* font = g_gameType & GAMEFLAG_BLOOD ? SmallFont2 : SmallFont; int line = (g_gameType & GAMEFLAG_BLOOD)? Top : (g_gameType & GAMEFLAG_SW) ? 40 : font->GetDisplacement(); bool canskip = (g_gameType & GAMEFLAG_BLOOD); double scale = 1 / (NotifyFontScale * con_notifyscale); int lineadv = font->GetHeight() / NotifyFontScale; for (unsigned i = topline; i < Text.Size(); ++i) { FNotifyText& notify = Text[i]; if (notify.TimeOut == 0) continue; int j = notify.TimeOut - notify.Ticker; if (j > 0) { double alpha = g_gameType & GAMEFLAG_BLOOD? ((j < NOTIFYFADETIME) ? 1. * j / NOTIFYFADETIME : 1) : 1; if (pulse) { alpha *= 0.7 + 0.3 * sin(I_msTime() / 100.); } if (!center) { DrawText(twod, font, CR_UNTRANSLATED, 0, line, notify.Text, DTA_FullscreenScale, FSMode_ScaleToHeight, DTA_VirtualWidthF, 320 * scale, DTA_VirtualHeightF, 200 * scale, DTA_KeepRatio, true, DTA_Alpha, alpha, TAG_DONE); } else { DrawText(twod, font, CR_UNTRANSLATED, 160 * scale - font->StringWidth(notify.Text) / 2, line, notify.Text, DTA_FullscreenScale, FSMode_ScaleToHeight, DTA_VirtualWidthF, 320 * scale, DTA_VirtualHeightF, 200 * scale, DTA_Alpha, alpha, TAG_DONE); } line += lineadv; canskip = false; } else { notify.TimeOut = 0; } } if (canskip) { Top = TopGoal; } } static bool printNative() { // Blood originally uses its tiny font for the notify display which does not play along well with localization because it is too small if (con_notify_advanced) return false; if (!(g_gameType & GAMEFLAG_BLOOD)) return true; auto p = GStrings["REQUIRED_CHARACTERS"]; if (p && *p) return false; return true; } void FNotifyBuffer::Draw() { if (gamestate == GS_FULLCONSOLE || gamestate == GS_MENUSCREEN) return; if (printNative()) { DrawNative(); return; } bool center = (con_centernotify != 0.f); int color; bool canskip = true; FFont* font = generic_ui ? NewSmallFont : SmallFont? SmallFont : AlternativeSmallFont; double nfscale = (generic_ui? 0.7 : NotifyFontScale); double scale = 1 / ( * con_notifyscale); int line = Top + font->GetDisplacement() / nfscale; int lineadv = font->GetHeight () / nfscale; for (unsigned i = 0; i < Text.Size(); ++ i) { FNotifyText ¬ify = Text[i]; if (notify.TimeOut == 0) continue; int j = notify.TimeOut - notify.Ticker; if (j > 0) { double alpha = (j < NOTIFYFADETIME) ? 1. * j / NOTIFYFADETIME : 1; if (con_pulsetext) { alpha *= 0.7 + 0.3 * sin(I_msTime() / 100.); } if (notify.PrintLevel >= PRINTLEVELS) color = CR_UNTRANSLATED; else color = PrintColors[notify.PrintLevel]; if (!center) DrawText(twod, font, color, 0, line * NotifyFontScale, notify.Text, DTA_FullscreenScale, FSMode_ScaleToHeight, DTA_VirtualWidthF, 320. * scale, DTA_VirtualHeightF, 200. * scale, DTA_KeepRatio, true, DTA_Alpha, alpha, TAG_DONE); else DrawText(twod, font, color, 160 * scale - font->StringWidth (notify.Text) / 2., line, notify.Text, DTA_FullscreenScale, FSMode_ScaleToHeight, DTA_VirtualWidthF, 320. * scale, DTA_VirtualHeightF, 200. * scale, DTA_Alpha, alpha, TAG_DONE); line += lineadv; canskip = false; } else { notify.TimeOut = 0; } } if (canskip) { Top = TopGoal; } } void C_DrawConsole () { static int oldbottom = 0; int lines, left, offset; int textScale = active_con_scale(twod); left = LEFTMARGIN; lines = (ConBottom/textScale-CurrentConsoleFont->GetHeight()*2)/CurrentConsoleFont->GetHeight(); if (-CurrentConsoleFont->GetHeight() + lines*CurrentConsoleFont->GetHeight() > ConBottom/textScale - CurrentConsoleFont->GetHeight()*7/2) { offset = -CurrentConsoleFont->GetHeight()/2; lines--; } else { offset = -CurrentConsoleFont->GetHeight(); } oldbottom = ConBottom; if (ConsoleState == c_up && gamestate != GS_INTRO && gamestate != GS_INTERMISSION) { NotifyStrings.Draw(); return; } else if (ConBottom) { int visheight; visheight = ConBottom; if (conback) { DrawTexture (twod, conback, 0, visheight - screen->GetHeight(), DTA_DestWidth, screen->GetWidth(), DTA_DestHeight, screen->GetHeight(), DTA_ColorOverlay, conshade, DTA_Alpha, (/*gamestate != GS_FULLCONSOLE*/true) ? (double)con_alpha : 1., DTA_Masked, false, TAG_DONE); } else { PalEntry pe((uint8_t)(con_alpha * 255), 0, 0, 0); twod->AddColorOnlyQuad(0, 0, screen->GetWidth(), visheight, pe); } if (conline && visheight < screen->GetHeight()) { twod->AddColorOnlyQuad(0, visheight, screen->GetWidth(), visheight+1, 0xff000000); } if (ConBottom >= 12) { if (textScale == 1) DrawText(twod, CurrentConsoleFont, CR_ORANGE, twod->GetWidth() - 8 - CurrentConsoleFont->StringWidth (GetVersionString()), ConBottom / textScale - CurrentConsoleFont->GetHeight() - 4, GetVersionString(), TAG_DONE); else DrawText(twod, CurrentConsoleFont, CR_ORANGE, twod->GetWidth() / textScale - 8 - CurrentConsoleFont->StringWidth(GetVersionString()), ConBottom / textScale - CurrentConsoleFont->GetHeight() - 4, GetVersionString(), DTA_VirtualWidth, twod->GetWidth() / textScale, DTA_VirtualHeight, twod->GetHeight() / textScale, DTA_KeepRatio, true, TAG_DONE); } } if (menuactive != MENU_Off) { return; } if (lines > 0) { // No more enqueuing because adding new text to the console won't touch the actual print data. conbuffer->FormatText(CurrentConsoleFont, ConWidth / textScale); unsigned int consolelines = conbuffer->GetFormattedLineCount(); FBrokenLines *blines = conbuffer->GetLines(); FBrokenLines *printline = blines + consolelines - 1 - RowAdjust; int bottomline = ConBottom / textScale - CurrentConsoleFont->GetHeight()*2 - 4; for(FBrokenLines *p = printline; p >= blines && lines > 0; p--, lines--) { if (textScale == 1) { DrawText(twod, CurrentConsoleFont, CR_TAN, LEFTMARGIN, offset + lines * CurrentConsoleFont->GetHeight(), p->Text, TAG_DONE); } else { DrawText(twod, CurrentConsoleFont, CR_TAN, LEFTMARGIN, offset + lines * CurrentConsoleFont->GetHeight(), p->Text, DTA_VirtualWidth, twod->GetWidth() / textScale, DTA_VirtualHeight, twod->GetHeight() / textScale, DTA_KeepRatio, true, TAG_DONE); } } if (ConBottom >= 20) { if (gamestate != GS_STARTUP) { auto now = I_msTime(); if (now > CursorTicker) { CursorTicker = now + 500; cursoron = !cursoron; } CmdLine.Draw(left, bottomline, textScale, cursoron); } if (RowAdjust && ConBottom >= CurrentConsoleFont->GetHeight()*7/2) { // Indicate that the view has been scrolled up (10) // and if we can scroll no further (12) if (textScale == 1) DrawChar(twod, CurrentConsoleFont, CR_GREEN, 0, bottomline, RowAdjust == conbuffer->GetFormattedLineCount() ? 12 : 10, TAG_DONE); else DrawChar(twod, CurrentConsoleFont, CR_GREEN, 0, bottomline, RowAdjust == conbuffer->GetFormattedLineCount() ? 12 : 10, DTA_VirtualWidth, twod->GetWidth() / textScale, DTA_VirtualHeight, twod->GetHeight() / textScale, DTA_KeepRatio, true, TAG_DONE); } } } } void C_FullConsole () { /* if (hud_toggled) D_ToggleHud(); if (demoplayback) G_CheckDemoStatus (); D_QuitNetGame (); advancedemo = false; */ ConsoleState = c_down; HistPos = NULL; TabbedLast = false; TabbedList = false; if (gamestate != GS_STARTUP) { gamestate = GS_FULLCONSOLE; Mus_Stop(); } C_AdjustBottom (); } void C_ToggleConsole () { if (gamestate == GS_INTRO) // blocked { return; } if (gamestate == GS_MENUSCREEN) { gamestate = GS_FULLCONSOLE; C_FullConsole(); } else if (!chatmodeon && (ConsoleState == c_up || ConsoleState == c_rising) && menuactive == MENU_Off) { ConsoleState = c_falling; HistPos = NULL; TabbedLast = false; TabbedList = false; } else if (gamestate != GS_FULLCONSOLE && gamestate != GS_STARTUP) { ConsoleState = c_rising; C_FlushDisplay (); } } void C_HideConsole () { if (gamestate != GS_FULLCONSOLE) { ConsoleState = c_up; ConBottom = 0; HistPos = NULL; } } static bool C_HandleKey (event_t *ev, FCommandBuffer &buffer) { int data1 = ev->data1; bool keepappending = false; switch (ev->subtype) { default: return false; case EV_GUI_Char: if (ev->data2) { // Bash-style shortcuts if (data1 == 'b') { buffer.CursorWordLeft(); break; } else if (data1 == 'f') { buffer.CursorWordRight(); break; } } // Add keypress to command line buffer.AddChar(data1); HistPos = NULL; TabbedLast = false; TabbedList = false; break; case EV_GUI_WheelUp: case EV_GUI_WheelDown: if (!(ev->data3 & GKM_SHIFT)) { data1 = GK_PGDN + EV_GUI_WheelDown - ev->subtype; } else { data1 = GK_DOWN + EV_GUI_WheelDown - ev->subtype; } // Intentional fallthrough case EV_GUI_KeyDown: case EV_GUI_KeyRepeat: switch (data1) { case '\t': // Try to do tab-completion C_TabComplete ((ev->data3 & GKM_SHIFT) ? false : true); break; case GK_PGUP: if (ev->data3 & (GKM_SHIFT|GKM_CTRL)) { // Scroll console buffer up one page RowAdjust += (twod->GetHeight()-4)/active_con_scale(twod) / ((gamestate == GS_FULLCONSOLE || gamestate == GS_STARTUP) ? CurrentConsoleFont->GetHeight() : CurrentConsoleFont->GetHeight()*2) - 3; } else if (RowAdjust < conbuffer->GetFormattedLineCount()) { // Scroll console buffer up if (ev->subtype == EV_GUI_WheelUp) { RowAdjust += 3; } else { RowAdjust++; } if (RowAdjust > conbuffer->GetFormattedLineCount()) { RowAdjust = conbuffer->GetFormattedLineCount(); } } break; case GK_PGDN: if (ev->data3 & (GKM_SHIFT|GKM_CTRL)) { // Scroll console buffer down one page const int scrollamt = (twod->GetHeight()-4)/active_con_scale(twod) / ((gamestate == GS_FULLCONSOLE || gamestate == GS_STARTUP) ? CurrentConsoleFont->GetHeight() : CurrentConsoleFont->GetHeight()*2) - 3; if (RowAdjust < scrollamt) { RowAdjust = 0; } else { RowAdjust -= scrollamt; } } else if (RowAdjust > 0) { // Scroll console buffer down if (ev->subtype == EV_GUI_WheelDown) { RowAdjust = std::max (0, RowAdjust - 3); } else { RowAdjust--; } } break; case GK_HOME: if (ev->data3 & GKM_CTRL) { // Move to top of console buffer RowAdjust = conbuffer->GetFormattedLineCount(); } else { // Move cursor to start of line buffer.CursorStart(); } break; case GK_END: if (ev->data3 & GKM_CTRL) { // Move to bottom of console buffer RowAdjust = 0; } else { // Move cursor to end of line buffer.CursorEnd(); } break; case GK_LEFT: // Move cursor left one character buffer.CursorLeft(); break; case GK_RIGHT: // Move cursor right one character buffer.CursorRight(); break; case '\b': // Erase character to left of cursor buffer.DeleteLeft(); TabbedLast = false; TabbedList = false; break; case GK_DEL: // Erase character under cursor buffer.DeleteRight(); TabbedLast = false; TabbedList = false; break; case GK_UP: // Move to previous entry in the command history if (HistPos == NULL) { HistPos = HistHead; } else if (HistPos->Older) { HistPos = HistPos->Older; } if (HistPos) { buffer.SetString(HistPos->String); } TabbedLast = false; TabbedList = false; break; case GK_DOWN: // Move to next entry in the command history if (HistPos && HistPos->Newer) { HistPos = HistPos->Newer; buffer.SetString(HistPos->String); } else { HistPos = NULL; buffer.SetString(""); } TabbedLast = false; TabbedList = false; break; case 'X': if (ev->data3 & GKM_CTRL) { buffer.SetString(""); TabbedLast = TabbedList = false; } break; case 'D': if (ev->data3 & GKM_CTRL && buffer.TextLength() == 0) { // Control-D pressed on an empty line if (strlen(con_ctrl_d) == 0) { break; // Replacement is empty, so do nothing } buffer.SetString(*con_ctrl_d); } else { break; } // Intentional fall-through for command(s) added with Ctrl-D case '\r': { // Execute command line (ENTER) FString bufferText = buffer.GetText(); bufferText.StripLeftRight(); Printf(127, TEXTCOLOR_WHITE "]%s\n", bufferText.GetChars()); if (bufferText.Len() == 0) { // Command line is empty, so do nothing to the history } else if (HistHead && HistHead->String.CompareNoCase(bufferText) == 0) { // Command line was the same as the previous one, // so leave the history list alone } else { // Command line is different from last command line, // or there is nothing in the history list, // so add it to the history list. History *temp = new History; temp->String = bufferText; temp->Older = HistHead; if (HistHead) { HistHead->Newer = temp; } temp->Newer = NULL; HistHead = temp; if (!HistTail) { HistTail = temp; } if (HistSize == MAXHISTSIZE) { HistTail = HistTail->Newer; delete HistTail->Older; HistTail->Older = NULL; } else { HistSize++; } } HistPos = NULL; buffer.SetString(""); AddCommandString(bufferText); TabbedLast = false; TabbedList = false; break; } case '`': // Check to see if we have ` bound to the console before accepting // it as a way to close the console. if (Bindings.GetBinding(KEY_GRAVE).CompareNoCase("toggleconsole")) { break; } case GK_ESCAPE: // Close console and clear command line. But if we're in the // fullscreen console mode, there's nothing to fall back on // if it's closed, so open the main menu instead. if (gamestate == GS_STARTUP) { return false; } else if (gamestate == GS_FULLCONSOLE) { gamestate = GS_MENUSCREEN; C_DoCommand ("menu_main"); } else { buffer.SetString(""); HistPos = NULL; C_ToggleConsole (); } break; case 'C': case 'V': TabbedLast = false; TabbedList = false; #ifdef __APPLE__ if (ev->data3 & GKM_META) #else // !__APPLE__ if (ev->data3 & GKM_CTRL) #endif // __APPLE__ { if (data1 == 'C') { // copy to clipboard if (buffer.TextLength() > 0) { I_PutInClipboard(buffer.GetText()); } } else { // paste from clipboard buffer.AddString(I_GetFromClipboard(false)); HistPos = NULL; } break; } break; // Bash-style shortcuts case 'A': if (ev->data3 & GKM_CTRL) { buffer.CursorStart(); } break; case 'E': if (ev->data3 & GKM_CTRL) { buffer.CursorEnd(); } break; case 'W': if (ev->data3 & GKM_CTRL) { buffer.DeleteWordLeft(); keepappending = true; TabbedLast = false; TabbedList = false; } break; case 'U': if (ev->data3 & GKM_CTRL) { buffer.DeleteLineLeft(); keepappending = true; TabbedLast = false; TabbedList = false; } break; case 'K': if (ev->data3 & GKM_CTRL) { buffer.DeleteLineRight(); keepappending = true; TabbedLast = false; TabbedList = false; } break; case 'Y': if (ev->data3 & GKM_CTRL) { buffer.AddYankBuffer(); TabbedLast = false; TabbedList = false; HistPos = NULL; } break; } break; #ifdef __unix__ case EV_GUI_MButtonDown: buffer.AddString(I_GetFromClipboard(true)); HistPos = NULL; break; #endif } buffer.AppendToYankBuffer = keepappending; // Ensure that the cursor is always visible while typing CursorTicker = I_msTime() + 500; cursoron = 1; return true; } bool C_Responder (event_t *ev) { if (ev->type != EV_GUI_Event || ConsoleState == c_up || ConsoleState == c_rising || menuactive != MENU_Off) { return false; } return C_HandleKey(ev, CmdLine); } CCMD (history) { struct History *hist = HistTail; while (hist) { Printf (" %s\n", hist->String.GetChars()); hist = hist->Newer; } } CCMD (clear) { C_FlushDisplay (); ClearConsole (); } CCMD (echo) { int last = argv.argc()-1; for (int i = 1; i <= last; ++i) { FString formatted = strbin1 (argv[i]); Printf ("%s%s", formatted.GetChars(), i!=last ? " " : "\n"); } } CCMD(toggleconsole) { C_ToggleConsole(); } /* Printing in the middle of the screen */ CVAR(Float, con_midtime, 3.f, CVAR_ARCHIVE) const char *console_bar = "----------------------------------------"; void C_MidPrint (FFont *font, const char *msg, bool bold) { #if 0 // The Build engine cannot do this at the moment. Q: Implement and redirect some messages here? if (StatusBar == nullptr || screen == nullptr) return; // [MK] allow the status bar to take over MidPrint IFVIRTUALPTR(StatusBar, DBaseStatusBar, ProcessMidPrint) { FString msgstr = msg; VMValue params[] = { (DObject*)StatusBar, font, &msgstr, bold }; int rv; VMReturn ret(&rv); VMCall(func, params, countof(params), &ret, 1); if (!!rv) return; } if (msg != nullptr) { auto color = (EColorRange)PrintColors[bold? PRINTLEVELS+1 : PRINTLEVELS]; Printf(PRINT_HIGH|PRINT_NONOTIFY, TEXTCOLOR_ESCAPESTR "%c%s\n%s\n%s\n", color, console_bar, msg, console_bar); StatusBar->AttachMessage (Create(font, msg, 1.5f, 0.375f, 0, 0, color, con_midtime), MAKE_ID('C','N','T','R')); } else { StatusBar->DetachMessage (MAKE_ID('C','N','T','R')); } #endif } /****** Tab completion code ******/ struct TabData { int UseCount; FName TabName; TabData() : UseCount(0), TabName(NAME_None) { } TabData(const char *name) : UseCount(1), TabName(name) { } TabData(const TabData &other) = default; }; static TArray TabCommands (TArray::NoInit); static int TabPos; // Last TabCommand tabbed to static int TabStart; // First char in CmdLine to use for tab completion static int TabSize; // Size of tab string static bool FindTabCommand (const char *name, int *stoppos, int len) { FName aname(name); unsigned int i; int cval = 1; for (i = 0; i < TabCommands.Size(); i++) { if (TabCommands[i].TabName == aname) { *stoppos = i; return true; } cval = strnicmp (TabCommands[i].TabName.GetChars(), name, len); if (cval >= 0) break; } *stoppos = i; return (cval == 0); } void C_AddTabCommand (const char *name) { int pos; if (FindTabCommand (name, &pos, INT_MAX)) { TabCommands[pos].UseCount++; } else { TabData tab(name); TabCommands.Insert (pos, tab); } } void C_RemoveTabCommand (const char *name) { if (TabCommands.Size() == 0) { // There are no tab commands that can be removed. // This is important to skip construction of aname // in case the NameManager has already been destroyed. return; } FName aname(name, true); if (aname == NAME_None) { return; } for (unsigned int i = 0; i < TabCommands.Size(); ++i) { if (TabCommands[i].TabName == aname) { if (--TabCommands[i].UseCount == 0) { TabCommands.Delete(i); } break; } } } void C_ClearTabCommands () { TabCommands.Clear(); } static int FindDiffPoint (FName name1, const char *str2) { const char *str1 = name1.GetChars(); int i; for (i = 0; tolower(str1[i]) == tolower(str2[i]); i++) if (str1[i] == 0 || str2[i] == 0) break; return i; } static void C_TabComplete (bool goForward) { unsigned i; int diffpoint; auto CmdLineText = CmdLine.GetText(); if (!TabbedLast) { bool cancomplete; // Skip any spaces at beginning of command line for (i = 0; i < CmdLineText.Len(); ++i) { if (CmdLineText[i] != ' ') break; } if (i == CmdLineText.Len()) { // Line was nothing but spaces return; } TabStart = i; TabSize = (int)CmdLineText.Len() - TabStart; if (!FindTabCommand(&CmdLineText[TabStart], &TabPos, TabSize)) return; // No initial matches // Show a list of possible completions, if more than one. if (TabbedList || con_notablist) { cancomplete = true; } else { cancomplete = C_TabCompleteList (); TabbedList = true; } if (goForward) { // Position just before the list of completions so that when TabPos // gets advanced below, it will be at the first one. --TabPos; } else { // Find the last matching tab, then go one past it. while (++TabPos < (int)TabCommands.Size()) { if (FindDiffPoint(TabCommands[TabPos].TabName, &CmdLineText[TabStart]) < TabSize) { break; } } } TabbedLast = true; if (!cancomplete) { return; } } if ((goForward && ++TabPos == (int)TabCommands.Size()) || (!goForward && --TabPos < 0)) { TabbedLast = false; CmdLineText.Truncate(TabSize); } else { diffpoint = FindDiffPoint(TabCommands[TabPos].TabName, &CmdLineText[TabStart]); if (diffpoint < TabSize) { // No more matches TabbedLast = false; CmdLineText.Truncate(TabSize - TabStart); } else { CmdLineText.Truncate(TabStart); CmdLineText << TabCommands[TabPos].TabName.GetChars() << ' '; } } CmdLine.SetString(CmdLineText); CmdLine.MakeStartPosGood(); } static bool C_TabCompleteList () { int nummatches, i; size_t maxwidth; int commonsize = INT_MAX; nummatches = 0; maxwidth = 0; auto CmdLineText = CmdLine.GetText(); for (i = TabPos; i < (int)TabCommands.Size(); ++i) { if (FindDiffPoint (TabCommands[i].TabName, &CmdLineText[TabStart]) < TabSize) { break; } else { if (i > TabPos) { // This keeps track of the longest common prefix for all the possible // completions, so we can fill in part of the command for the user if // the longest common prefix is longer than what the user already typed. int diffpt = FindDiffPoint (TabCommands[i-1].TabName, TabCommands[i].TabName.GetChars()); if (diffpt < commonsize) { commonsize = diffpt; } } nummatches++; maxwidth = std::max (maxwidth, strlen (TabCommands[i].TabName.GetChars())); } } if (nummatches > 1) { size_t x = 0; maxwidth += 3; Printf (TEXTCOLOR_BLUE "Completions for %s:\n", CmdLineText.GetChars()); for (i = TabPos; nummatches > 0; ++i, --nummatches) { // [Dusk] Print console commands blue, CVars green, aliases red. const char* colorcode = ""; FConsoleCommand* ccmd; if (FindCVar (TabCommands[i].TabName.GetChars(), NULL)) colorcode = TEXTCOLOR_GREEN; else if ((ccmd = FConsoleCommand::FindByName (TabCommands[i].TabName.GetChars())) != NULL) { if (ccmd->IsAlias()) colorcode = TEXTCOLOR_RED; else colorcode = TEXTCOLOR_LIGHTBLUE; } Printf ("%s%-*s", colorcode, int(maxwidth), TabCommands[i].TabName.GetChars()); x += maxwidth; if (x > ConCols / active_con_scale(twod) - maxwidth) { x = 0; Printf ("\n"); } } if (x != 0) { Printf ("\n"); } // Fill in the longest common prefix, if it's longer than what was typed. if (TabSize != commonsize) { TabSize = commonsize; CmdLineText.Truncate(TabStart); CmdLineText.AppendCStrPart(TabCommands[TabPos].TabName.GetChars(), commonsize); CmdLine.SetString(CmdLineText); } return false; } return true; }