2 * This file is part of OpenTTD.
3 * OpenTTD is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, version 2.
4 * OpenTTD is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
5 * See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with OpenTTD. If not, see <http://www.gnu.org/licenses/>.
8 /** @file console_gui.cpp Handling the GUI of the in-game console. */
11 #include "textbuf_type.h"
12 #include "window_gui.h"
13 #include "console_gui.h"
14 #include "console_internal.h"
15 #include "window_func.h"
16 #include "string_func.h"
17 #include "strings_func.h"
19 #include "settings_type.h"
20 #include "console_func.h"
22 #include "video/video_driver.hpp"
23 #include "timer/timer.h"
24 #include "timer/timer_window.h"
26 #include "widgets/console_widget.h"
28 #include "table/strings.h"
30 #include "safeguards.h"
32 static const uint ICON_HISTORY_SIZE
= 20;
33 static const uint ICON_RIGHT_BORDERWIDTH
= 10;
34 static const uint ICON_BOTTOM_BORDERWIDTH
= 12;
37 * Container for a single line of console output
40 std::string buffer
; ///< The data to store.
41 TextColour colour
; ///< The colour of the line.
42 uint16_t time
; ///< The amount of time the line is in the backlog.
44 IConsoleLine() : buffer(), colour(TC_BEGIN
), time(0)
50 * Initialize the console line.
51 * @param buffer the data to print.
52 * @param colour the colour of the line.
54 IConsoleLine(std::string buffer
, TextColour colour
) :
55 buffer(std::move(buffer
)),
66 /** The console backlog buffer. Item index 0 is the newest line. */
67 static std::deque
<IConsoleLine
> _iconsole_buffer
;
69 static bool TruncateBuffer();
72 /* ** main console cmd buffer ** */
73 static Textbuf
_iconsole_cmdline(ICON_CMDLN_SIZE
);
74 static std::deque
<std::string
> _iconsole_history
;
75 static ptrdiff_t _iconsole_historypos
;
76 IConsoleModes _iconsole_mode
;
82 static void IConsoleClearCommand()
84 memset(_iconsole_cmdline
.buf
, 0, ICON_CMDLN_SIZE
);
85 _iconsole_cmdline
.chars
= _iconsole_cmdline
.bytes
= 1; // only terminating zero
86 _iconsole_cmdline
.pixels
= 0;
87 _iconsole_cmdline
.caretpos
= 0;
88 _iconsole_cmdline
.caretxoffs
= 0;
89 SetWindowDirty(WC_CONSOLE
, 0);
92 static inline void IConsoleResetHistoryPos()
94 _iconsole_historypos
= -1;
98 static const char *IConsoleHistoryAdd(const char *cmd
);
99 static void IConsoleHistoryNavigate(int direction
);
101 static constexpr NWidgetPart _nested_console_window_widgets
[] = {
102 NWidget(WWT_EMPTY
, INVALID_COLOUR
, WID_C_BACKGROUND
), SetResize(1, 1),
105 static WindowDesc
_console_window_desc(__FILE__
, __LINE__
,
106 WDP_MANUAL
, nullptr, 0, 0,
109 std::begin(_nested_console_window_widgets
), std::end(_nested_console_window_widgets
)
112 struct IConsoleWindow
: Window
114 static size_t scroll
;
115 int line_height
; ///< Height of one line of text in the console.
118 IConsoleWindow() : Window(&_console_window_desc
)
120 _iconsole_mode
= ICONSOLE_OPENED
;
123 ResizeWindow(this, _screen
.width
, _screen
.height
/ 3);
126 void OnInit() override
128 this->line_height
= GetCharacterHeight(FS_NORMAL
) + WidgetDimensions::scaled
.hsep_normal
;
129 this->line_offset
= GetStringBoundingBox("] ").width
+ WidgetDimensions::scaled
.frametext
.left
;
132 void Close([[maybe_unused
]] int data
= 0) override
134 _iconsole_mode
= ICONSOLE_CLOSED
;
135 VideoDriver::GetInstance()->EditBoxLostFocus();
136 this->Window::Close();
140 * Scroll the content of the console.
141 * @param amount Number of lines to scroll back.
143 void Scroll(int amount
)
146 size_t namount
= (size_t) -amount
;
147 IConsoleWindow::scroll
= (namount
> IConsoleWindow::scroll
) ? 0 : IConsoleWindow::scroll
- namount
;
149 assert(this->height
>= 0 && this->line_height
> 0);
150 size_t visible_lines
= (size_t)(this->height
/ this->line_height
);
151 size_t max_scroll
= (visible_lines
> _iconsole_buffer
.size()) ? 0 : _iconsole_buffer
.size() + 1 - visible_lines
;
152 IConsoleWindow::scroll
= std::min
<size_t>(IConsoleWindow::scroll
+ amount
, max_scroll
);
157 void OnPaint() override
159 const int right
= this->width
- WidgetDimensions::scaled
.frametext
.right
;
161 GfxFillRect(0, 0, this->width
- 1, this->height
- 1, PC_BLACK
);
162 int ypos
= this->height
- this->line_height
;
163 for (size_t line_index
= IConsoleWindow::scroll
; line_index
< _iconsole_buffer
.size(); line_index
++) {
164 const IConsoleLine
&print
= _iconsole_buffer
[line_index
];
165 SetDParamStr(0, print
.buffer
);
166 ypos
= DrawStringMultiLine(WidgetDimensions::scaled
.frametext
.left
, right
, -this->line_height
, ypos
, STR_JUST_RAW_STRING
, print
.colour
, SA_LEFT
| SA_BOTTOM
| SA_FORCE
) - WidgetDimensions::scaled
.hsep_normal
;
169 /* If the text is longer than the window, don't show the starting ']' */
170 int delta
= this->width
- this->line_offset
- _iconsole_cmdline
.pixels
- ICON_RIGHT_BORDERWIDTH
;
172 DrawString(WidgetDimensions::scaled
.frametext
.left
, right
, this->height
- this->line_height
, "]", (TextColour
)CC_COMMAND
, SA_LEFT
| SA_FORCE
);
176 /* If we have a marked area, draw a background highlight. */
177 if (_iconsole_cmdline
.marklength
!= 0) GfxFillRect(this->line_offset
+ delta
+ _iconsole_cmdline
.markxoffs
, this->height
- this->line_height
, this->line_offset
+ delta
+ _iconsole_cmdline
.markxoffs
+ _iconsole_cmdline
.marklength
, this->height
- 1, PC_DARK_RED
);
179 DrawString(this->line_offset
+ delta
, right
, this->height
- this->line_height
, _iconsole_cmdline
.buf
, (TextColour
)CC_COMMAND
, SA_LEFT
| SA_FORCE
);
181 if (_focused_window
== this && _iconsole_cmdline
.caret
) {
182 DrawString(this->line_offset
+ delta
+ _iconsole_cmdline
.caretxoffs
, right
, this->height
- this->line_height
, "_", TC_WHITE
, SA_LEFT
| SA_FORCE
);
186 /** Check on a regular interval if the console buffer needs truncating. */
187 IntervalTimer
<TimerWindow
> truncate_interval
= {std::chrono::seconds(3), [this](auto) {
188 assert(this->height
>= 0 && this->line_height
> 0);
189 size_t visible_lines
= (size_t)(this->height
/ this->line_height
);
191 if (TruncateBuffer() && IConsoleWindow::scroll
+ visible_lines
> _iconsole_buffer
.size()) {
192 size_t max_scroll
= (visible_lines
> _iconsole_buffer
.size()) ? 0 : _iconsole_buffer
.size() + 1 - visible_lines
;
193 IConsoleWindow::scroll
= std::min
<size_t>(IConsoleWindow::scroll
, max_scroll
);
198 void OnMouseLoop() override
200 if (_iconsole_cmdline
.HandleCaret()) this->SetDirty();
203 EventState
OnKeyPress([[maybe_unused
]] char32_t key
, uint16_t keycode
) override
205 if (_focused_window
!= this) return ES_NOT_HANDLED
;
207 const int scroll_height
= (this->height
/ this->line_height
) - 1;
210 IConsoleHistoryNavigate(1);
215 IConsoleHistoryNavigate(-1);
219 case WKC_SHIFT
| WKC_PAGEDOWN
:
220 this->Scroll(-scroll_height
);
223 case WKC_SHIFT
| WKC_PAGEUP
:
224 this->Scroll(scroll_height
);
227 case WKC_SHIFT
| WKC_DOWN
:
231 case WKC_SHIFT
| WKC_UP
:
239 case WKC_RETURN
: case WKC_NUM_ENTER
: {
240 /* We always want the ] at the left side; we always force these strings to be left
241 * aligned anyway. So enforce this in all cases by adding a left-to-right marker,
242 * otherwise it will be drawn at the wrong side with right-to-left texts. */
243 IConsolePrint(CC_COMMAND
, LRM
"] {}", _iconsole_cmdline
.buf
);
244 const char *cmd
= IConsoleHistoryAdd(_iconsole_cmdline
.buf
);
245 IConsoleClearCommand();
247 if (cmd
!= nullptr) IConsoleCmdExec(cmd
);
251 case WKC_CTRL
| WKC_RETURN
:
252 _iconsole_mode
= (_iconsole_mode
== ICONSOLE_FULL
) ? ICONSOLE_OPENED
: ICONSOLE_FULL
;
253 IConsoleResize(this);
254 MarkWholeScreenDirty();
257 case (WKC_CTRL
| 'L'):
258 IConsoleCmdExec("clear");
262 if (_iconsole_cmdline
.HandleKeyPress(key
, keycode
) != HKPR_NOT_HANDLED
) {
263 IConsoleWindow::scroll
= 0;
264 IConsoleResetHistoryPos();
267 return ES_NOT_HANDLED
;
274 void InsertTextString(WidgetID
, const char *str
, bool marked
, const char *caret
, const char *insert_location
, const char *replacement_end
) override
276 if (_iconsole_cmdline
.InsertString(str
, marked
, caret
, insert_location
, replacement_end
)) {
277 IConsoleWindow::scroll
= 0;
278 IConsoleResetHistoryPos();
283 Textbuf
*GetFocusedTextbuf() const override
285 return &_iconsole_cmdline
;
288 Point
GetCaretPosition() const override
290 int delta
= std::min
<int>(this->width
- this->line_offset
- _iconsole_cmdline
.pixels
- ICON_RIGHT_BORDERWIDTH
, 0);
291 Point pt
= {this->line_offset
+ delta
+ _iconsole_cmdline
.caretxoffs
, this->height
- this->line_height
};
296 Rect
GetTextBoundingRect(const char *from
, const char *to
) const override
298 int delta
= std::min
<int>(this->width
- this->line_offset
- _iconsole_cmdline
.pixels
- ICON_RIGHT_BORDERWIDTH
, 0);
300 Point p1
= GetCharPosInString(_iconsole_cmdline
.buf
, from
, FS_NORMAL
);
301 Point p2
= from
!= to
? GetCharPosInString(_iconsole_cmdline
.buf
, to
, FS_NORMAL
) : p1
;
303 Rect r
= {this->line_offset
+ delta
+ p1
.x
, this->height
- this->line_height
, this->line_offset
+ delta
+ p2
.x
, this->height
};
307 ptrdiff_t GetTextCharacterAtPosition(const Point
&pt
) const override
309 int delta
= std::min
<int>(this->width
- this->line_offset
- _iconsole_cmdline
.pixels
- ICON_RIGHT_BORDERWIDTH
, 0);
311 if (!IsInsideMM(pt
.y
, this->height
- this->line_height
, this->height
)) return -1;
313 return GetCharAtPosition(_iconsole_cmdline
.buf
, pt
.x
- delta
);
316 void OnMouseWheel(int wheel
) override
318 this->Scroll(-wheel
);
321 void OnFocus() override
323 VideoDriver::GetInstance()->EditBoxGainedFocus();
326 void OnFocusLost(bool) override
328 VideoDriver::GetInstance()->EditBoxLostFocus();
332 size_t IConsoleWindow::scroll
= 0;
334 void IConsoleGUIInit()
336 IConsoleResetHistoryPos();
337 _iconsole_mode
= ICONSOLE_CLOSED
;
339 IConsoleClearBuffer();
341 IConsolePrint(TC_LIGHT_BLUE
, "OpenTTD Game Console Revision 7 - {}", _openttd_revision
);
342 IConsolePrint(CC_WHITE
, "------------------------------------");
343 IConsolePrint(CC_WHITE
, "use \"help\" for more information.");
344 IConsolePrint(CC_WHITE
, "");
345 IConsoleClearCommand();
348 void IConsoleClearBuffer()
350 _iconsole_buffer
.clear();
353 void IConsoleGUIFree()
355 IConsoleClearBuffer();
358 /** Change the size of the in-game console window after the screen size changed, or the window state changed. */
359 void IConsoleResize(Window
*w
)
361 switch (_iconsole_mode
) {
362 case ICONSOLE_OPENED
:
363 w
->height
= _screen
.height
/ 3;
364 w
->width
= _screen
.width
;
367 w
->height
= _screen
.height
- ICON_BOTTOM_BORDERWIDTH
;
368 w
->width
= _screen
.width
;
373 MarkWholeScreenDirty();
376 /** Toggle in-game console between opened and closed. */
377 void IConsoleSwitch()
379 switch (_iconsole_mode
) {
380 case ICONSOLE_CLOSED
:
381 new IConsoleWindow();
384 case ICONSOLE_OPENED
: case ICONSOLE_FULL
:
385 CloseWindowById(WC_CONSOLE
, 0);
389 MarkWholeScreenDirty();
392 /** Close the in-game console. */
395 if (_iconsole_mode
== ICONSOLE_OPENED
) IConsoleSwitch();
399 * Add the entered line into the history so you can look it back
400 * scroll, etc. Put it to the beginning as it is the latest text
401 * @param cmd Text to be entered into the 'history'
402 * @return the command to execute
404 static const char *IConsoleHistoryAdd(const char *cmd
)
406 /* Strip all spaces at the begin */
407 while (IsWhitespace(*cmd
)) cmd
++;
409 /* Do not put empty command in history */
410 if (StrEmpty(cmd
)) return nullptr;
412 /* Do not put in history if command is same as previous */
413 if (_iconsole_history
.empty() || _iconsole_history
.front() != cmd
) {
414 _iconsole_history
.emplace_front(cmd
);
415 while (_iconsole_history
.size() > ICON_HISTORY_SIZE
) _iconsole_history
.pop_back();
418 /* Reset the history position */
419 IConsoleResetHistoryPos();
420 return _iconsole_history
.front().c_str();
424 * Navigate Up/Down in the history of typed commands
425 * @param direction Go further back in history (+1), go to recently typed commands (-1)
427 static void IConsoleHistoryNavigate(int direction
)
429 if (_iconsole_history
.empty()) return; // Empty history
430 _iconsole_historypos
= Clamp
<ptrdiff_t>(_iconsole_historypos
+ direction
, -1, _iconsole_history
.size() - 1);
432 if (_iconsole_historypos
== -1) {
433 _iconsole_cmdline
.DeleteAll();
435 _iconsole_cmdline
.Assign(_iconsole_history
[_iconsole_historypos
]);
440 * Handle the printing of text entered into the console or redirected there
441 * by any other means. Text can be redirected to other clients in a network game
442 * as well as to a logfile. If the network server is a dedicated server, all activities
443 * are also logged. All lines to print are added to a temporary buffer which can be
444 * used as a history to print them onscreen
445 * @param colour_code the colour of the command. Red in case of errors, etc.
446 * @param str the message entered or output on the console (notice, error, etc.)
448 void IConsoleGUIPrint(TextColour colour_code
, const std::string
&str
)
450 _iconsole_buffer
.push_front(IConsoleLine(str
, colour_code
));
451 SetWindowDirty(WC_CONSOLE
, 0);
455 * Remove old lines from the backlog buffer.
456 * The buffer is limited by a maximum size and a minimum age. Every time truncation runs,
457 * all lines in the buffer are aged by one. When a line exceeds both the maximum position
458 * and also the maximum age, it gets removed.
459 * @return true if any lines were removed
461 static bool TruncateBuffer()
463 bool need_truncation
= false;
465 for (IConsoleLine
&line
: _iconsole_buffer
) {
468 if (line
.time
> _settings_client
.gui
.console_backlog_timeout
&& count
> _settings_client
.gui
.console_backlog_length
) {
469 /* Any messages after this are older and need to be truncated */
470 need_truncation
= true;
475 if (need_truncation
) {
476 _iconsole_buffer
.resize(count
- 1);
479 return need_truncation
;
484 * Check whether the given TextColour is valid for console usage.
485 * @param c The text colour to compare to.
486 * @return true iff the TextColour is valid for console usage.
488 bool IsValidConsoleColour(TextColour c
)
490 /* A normal text colour is used. */
491 if (!(c
& TC_IS_PALETTE_COLOUR
)) return TC_BEGIN
<= c
&& c
< TC_END
;
493 /* A text colour from the palette is used; must be the company
494 * colour gradient, so it must be one of those. */
495 c
&= ~TC_IS_PALETTE_COLOUR
;
496 for (uint i
= COLOUR_BEGIN
; i
< COLOUR_END
; i
++) {
497 if (_colour_gradient
[i
][4] == c
) return true;