scummvm/gui/console.cpp
Martin Gerhardy a7d4d0f232 GUI: added debug output for console prints
this is useful for situations where the game crashes or is quit, but you still need to
output of the debug commands for investigating the issue
2021-02-17 12:51:46 +00:00

749 lines
19 KiB
C++

/* ScummVM - Graphic Adventure Engine
*
* ScummVM is the legal property of its developers, whose names
* are too numerous to list here. Please refer to the COPYRIGHT
* file distributed with this source distribution.
*
* This program 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; either version 2
* of the License, or (at your option) any later version.
*
* This program 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. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*
*/
#include "gui/console.h"
#include "gui/widgets/scrollbar.h"
#include "gui/ThemeEval.h"
#include "gui/gui-manager.h"
#include "base/version.h"
#include "common/system.h"
#include "graphics/fontman.h"
namespace GUI {
#define kConsoleCharWidth (_font->getCharWidth('M'))
#define kConsoleLineHeight (_font->getFontHeight())
enum {
kConsoleSlideDownDuration = 200 // Time in milliseconds
};
#define PROMPT ") "
/* TODO:
* - it is very inefficient to redraw the full thingy when just one char is added/removed.
* Instead, we could just copy the GFX of the blank console (i.e. after the transparent
* background is drawn, before any text is drawn). Then using that, it becomes trivial
* to erase a single character, do scrolling etc.
* - a *lot* of others things, this code is in no way complete and heavily under progress
*/
ConsoleDialog::ConsoleDialog(float widthPercent, float heightPercent)
: Dialog(0, 0, 1, 1),
_widthPercent(widthPercent), _heightPercent(heightPercent) {
// Reset the line buffer
memset(_buffer, ' ', kBufferSize);
// Dummy
_scrollBar = new ScrollBarWidget(this, 0, 0, 5, 10);
_scrollBar->setTarget(this);
init();
_currentPos = 0;
_scrollLine = _linesPerPage - 1;
_firstLineInBuffer = 0;
_caretVisible = false;
_caretTime = 0;
_slideMode = kNoSlideMode;
_slideTime = 0;
_promptStartPos = _promptEndPos = -1;
// Init callback
_callbackProc = nullptr;
_callbackRefCon = nullptr;
// Init History
_historyIndex = 0;
_historyLine = 0;
_historySize = 0;
// Display greetings & prompt
print(gScummVMFullVersion);
print("\nConsole is ready\n");
}
void ConsoleDialog::init() {
const int screenW = g_system->getOverlayWidth();
const int screenH = g_system->getOverlayHeight();
_font = &g_gui.getFont(ThemeEngine::kFontStyleConsole);
_leftPadding = g_gui.xmlEval()->getVar("Globals.Console.Padding.Left", 0);
_rightPadding = g_gui.xmlEval()->getVar("Globals.Console.Padding.Right", 0);
_topPadding = g_gui.xmlEval()->getVar("Globals.Console.Padding.Top", 0);
_bottomPadding = g_gui.xmlEval()->getVar("Globals.Console.Padding.Bottom", 0);
// Calculate the real width/height (rounded to char/line multiples)
_w = (uint16)(_widthPercent * screenW);
_h = (uint16)((_heightPercent * screenH - 2) / kConsoleLineHeight);
_w = _w - _w / 20;
_h = _h * kConsoleLineHeight + 2;
_x = _w / 40;
// Set scrollbar dimensions
int scrollBarWidth = g_gui.xmlEval()->getVar("Globals.Scrollbar.Width", 0);
_scrollBar->resize(_w - scrollBarWidth - 1, 0, scrollBarWidth, _h);
_pageWidth = (_w - scrollBarWidth - 2 - _leftPadding - _topPadding - scrollBarWidth) / kConsoleCharWidth;
_linesPerPage = (_h - 2 - _topPadding - _bottomPadding) / kConsoleLineHeight;
_linesInBuffer = kBufferSize / kCharsPerLine;
}
void ConsoleDialog::slideUpAndClose() {
if (_slideMode == kNoSlideMode) {
_slideTime = g_system->getMillis();
_slideMode = kUpSlideMode;
}
}
void ConsoleDialog::open() {
// TODO: find a new way to do this
// Initiate sliding the console down. We do a very simple trick to achieve
// this effect: we simply move the console dialog just above (outside) the
// visible screen area, then shift it down in handleTickle() over a
// certain period of time.
const int screenW = g_system->getOverlayWidth();
const int screenH = g_system->getOverlayHeight();
// Calculate the real width/height (rounded to char/line multiples)
uint16 w = (uint16)(_widthPercent * screenW);
uint16 h = (uint16)((_heightPercent * screenH - 2) / kConsoleLineHeight);
h = h * kConsoleLineHeight + 2;
w = w - w / 20;
if (_w != w || _h != h)
init();
_y = -_h;
_slideTime = g_system->getMillis();
_slideMode = kDownSlideMode;
Dialog::open();
if ((_promptStartPos == -1) || (_currentPos > _promptEndPos)) {
// we print a prompt, if this is the first time we are called or if the
// engine wrote onto us since the last call
print(PROMPT);
_promptStartPos = _promptEndPos = _currentPos;
}
}
void ConsoleDialog::close() {
Dialog::close();
}
void ConsoleDialog::drawDialog(DrawLayer layerToDraw) {
Dialog::drawDialog(layerToDraw);
for (int line = 0; line < _linesPerPage; line++)
drawLine(line);
}
void ConsoleDialog::drawLine(int line) {
int x = _x + 1 + _leftPadding;
int start = _scrollLine - _linesPerPage + 1;
int y = _y + 2 + _topPadding;
int limit = MIN(_pageWidth, (int)kCharsPerLine);
y += line * kConsoleLineHeight;
for (int column = 0; column < limit; column++) {
#if 0
int l = (start + line) % _linesInBuffer;
byte c = buffer(l * kCharsPerLine + column);
#else
byte c = buffer((start + line) * kCharsPerLine + column);
#endif
g_gui.theme()->drawChar(Common::Rect(x, y, x+kConsoleCharWidth, y+kConsoleLineHeight), c, _font);
x += kConsoleCharWidth;
}
}
void ConsoleDialog::reflowLayout() {
init();
_scrollLine = _promptEndPos / kCharsPerLine;
if (_scrollLine < _linesPerPage - 1)
_scrollLine = _linesPerPage - 1;
updateScrollBuffer();
Dialog::reflowLayout();
g_gui.scheduleTopDialogRedraw();
}
void ConsoleDialog::handleTickle() {
uint32 time = g_system->getMillis();
if (_caretTime < time) {
_caretTime = time + kCaretBlinkTime;
drawCaret(_caretVisible);
}
// Perform the "slide animation".
if (_slideMode != kNoSlideMode) {
const float tmp = (float)(g_system->getMillis() - _slideTime) / kConsoleSlideDownDuration;
if (_slideMode == kUpSlideMode) {
_y = (int)(_h * (0.0 - tmp));
} else {
_y = (int)(_h * (tmp - 1.0));
}
if (_slideMode == kDownSlideMode && _y > 0) {
// End the slide
_slideMode = kNoSlideMode;
_y = 0;
g_gui.scheduleTopDialogRedraw();
} else if (_slideMode == kUpSlideMode && _y <= -_h) {
// End the slide
//_slideMode = kNoSlideMode;
close();
} else
g_gui.scheduleTopDialogRedraw();
}
_scrollBar->handleTickle();
}
void ConsoleDialog::handleMouseWheel(int x, int y, int direction) {
_scrollBar->handleMouseWheel(x, y, direction);
}
Common::String ConsoleDialog::getUserInput() {
assert(_promptEndPos >= _promptStartPos);
int len = _promptEndPos - _promptStartPos;
// Copy the user input to str
Common::String str;
for (int i = 0; i < len; i++)
str.insertChar(buffer(_promptStartPos + i), i);
return str;
}
void ConsoleDialog::handleKeyDown(Common::KeyState state) {
if (_slideMode != kNoSlideMode)
return;
switch (state.keycode) {
case Common::KEYCODE_RETURN:
case Common::KEYCODE_KP_ENTER: {
if (_caretVisible)
drawCaret(true);
nextLine();
bool keepRunning = true;
Common::String userInput = getUserInput();
if (!userInput.empty()) {
// Add the input to the history
addToHistory(userInput);
// Pass it to the input callback, if any
if (_callbackProc)
keepRunning = (*_callbackProc)(this, userInput.c_str(), _callbackRefCon);
}
print(PROMPT);
_promptStartPos = _promptEndPos = _currentPos;
g_gui.scheduleTopDialogRedraw();
if (!keepRunning)
slideUpAndClose();
break;
}
case Common::KEYCODE_ESCAPE:
slideUpAndClose();
break;
case Common::KEYCODE_BACKSPACE:
if (_caretVisible)
drawCaret(true);
if (_currentPos > _promptStartPos) {
_currentPos--;
killChar();
}
scrollToCurrent();
drawLine(pos2line(_currentPos));
break;
case Common::KEYCODE_TAB: {
if (_completionCallbackProc) {
int len = _currentPos - _promptStartPos;
assert(len >= 0);
char *str = new char[len + 1];
// Copy the user input to str
for (int i = 0; i < len; i++)
str[i] = buffer(_promptStartPos + i);
str[len] = '\0';
Common::String completion;
if ((*_completionCallbackProc)(this, str, completion, _callbackRefCon)) {
if (_caretVisible)
drawCaret(true);
insertIntoPrompt(completion.c_str());
scrollToCurrent();
drawLine(pos2line(_currentPos));
}
delete[] str;
}
break;
}
// Keypad & special keys
// - if num lock is set, we always go to the default case
// - if num lock is not set, we either fall down to the special key case
// or ignore the key press in case of 0 (INSERT) or 5
case Common::KEYCODE_KP0:
case Common::KEYCODE_KP5:
if (state.flags & Common::KBD_NUM)
defaultKeyDownHandler(state);
break;
case Common::KEYCODE_KP_PERIOD:
if (state.flags & Common::KBD_NUM) {
defaultKeyDownHandler(state);
break;
}
// fall through
case Common::KEYCODE_DELETE:
if (_currentPos < _promptEndPos) {
killChar();
drawLine(pos2line(_currentPos));
}
break;
case Common::KEYCODE_KP1:
if (state.flags & Common::KBD_NUM) {
defaultKeyDownHandler(state);
break;
}
// fall through
case Common::KEYCODE_END:
if (state.hasFlags(Common::KBD_SHIFT)) {
_scrollLine = _promptEndPos / kCharsPerLine;
if (_scrollLine < _linesPerPage - 1)
_scrollLine = _linesPerPage - 1;
updateScrollBuffer();
} else {
_currentPos = _promptEndPos;
}
g_gui.scheduleTopDialogRedraw();
break;
case Common::KEYCODE_KP2:
if (state.flags & Common::KBD_NUM) {
defaultKeyDownHandler(state);
break;
}
// fall through
case Common::KEYCODE_DOWN:
historyScroll(-1);
break;
case Common::KEYCODE_KP3:
if (state.flags & Common::KBD_NUM) {
defaultKeyDownHandler(state);
break;
}
// fall through
case Common::KEYCODE_PAGEDOWN:
if (state.hasFlags(Common::KBD_SHIFT)) {
_scrollLine += _linesPerPage - 1;
if (_scrollLine > _promptEndPos / kCharsPerLine) {
_scrollLine = _promptEndPos / kCharsPerLine;
if (_scrollLine < _firstLineInBuffer + _linesPerPage - 1)
_scrollLine = _firstLineInBuffer + _linesPerPage - 1;
}
updateScrollBuffer();
g_gui.scheduleTopDialogRedraw();
}
break;
case Common::KEYCODE_KP4:
if (state.flags & Common::KBD_NUM) {
defaultKeyDownHandler(state);
break;
}
// fall through
case Common::KEYCODE_LEFT:
if (_currentPos > _promptStartPos)
_currentPos--;
drawLine(pos2line(_currentPos));
break;
case Common::KEYCODE_KP6:
if (state.flags & Common::KBD_NUM) {
defaultKeyDownHandler(state);
break;
}
// fall through
case Common::KEYCODE_RIGHT:
if (_currentPos < _promptEndPos)
_currentPos++;
drawLine(pos2line(_currentPos));
break;
case Common::KEYCODE_KP7:
if (state.flags & Common::KBD_NUM) {
defaultKeyDownHandler(state);
break;
}
// fall through
case Common::KEYCODE_HOME:
if (state.hasFlags(Common::KBD_SHIFT)) {
_scrollLine = _firstLineInBuffer + _linesPerPage - 1;
updateScrollBuffer();
} else {
_currentPos = _promptStartPos;
}
g_gui.scheduleTopDialogRedraw();
break;
case Common::KEYCODE_KP8:
if (state.flags & Common::KBD_NUM) {
defaultKeyDownHandler(state);
break;
}
// fall through
case Common::KEYCODE_UP:
historyScroll(+1);
break;
case Common::KEYCODE_KP9:
if (state.flags & Common::KBD_NUM) {
defaultKeyDownHandler(state);
break;
}
// fall through
case Common::KEYCODE_PAGEUP:
if (state.hasFlags(Common::KBD_SHIFT)) {
_scrollLine -= _linesPerPage - 1;
if (_scrollLine < _firstLineInBuffer + _linesPerPage - 1)
_scrollLine = _firstLineInBuffer + _linesPerPage - 1;
updateScrollBuffer();
g_gui.scheduleTopDialogRedraw();
}
break;
default:
defaultKeyDownHandler(state);
}
}
void ConsoleDialog::defaultKeyDownHandler(Common::KeyState &state) {
if (state.hasFlags(Common::KBD_CTRL)) {
specialKeys(state.keycode);
} else if ((state.ascii >= 32 && state.ascii <= 127) || (state.ascii >= 160 && state.ascii <= 255)) {
for (int i = _promptEndPos - 1; i >= _currentPos; i--)
buffer(i + 1) = buffer(i);
_promptEndPos++;
printChar((byte)state.ascii);
scrollToCurrent();
}
}
void ConsoleDialog::insertIntoPrompt(const char* str) {
unsigned int l = strlen(str);
for (int i = _promptEndPos - 1; i >= _currentPos; i--)
buffer(i + l) = buffer(i);
for (unsigned int j = 0; j < l; ++j) {
_promptEndPos++;
printCharIntern(str[j]);
}
}
void ConsoleDialog::handleCommand(CommandSender *sender, uint32 cmd, uint32 data) {
switch (cmd) {
case kSetPositionCmd:
{
int newPos = (int)data + _linesPerPage - 1 + _firstLineInBuffer;
if (newPos != _scrollLine) {
_scrollLine = newPos;
g_gui.scheduleTopDialogRedraw();
}
}
break;
default:
break;
}
}
void ConsoleDialog::specialKeys(Common::KeyCode keycode) {
switch (keycode) {
case Common::KEYCODE_a:
_currentPos = _promptStartPos;
g_gui.scheduleTopDialogRedraw();
break;
case Common::KEYCODE_d:
if (_currentPos < _promptEndPos) {
killChar();
g_gui.scheduleTopDialogRedraw();
}
break;
case Common::KEYCODE_e:
_currentPos = _promptEndPos;
g_gui.scheduleTopDialogRedraw();
break;
case Common::KEYCODE_k:
killLine();
g_gui.scheduleTopDialogRedraw();
break;
case Common::KEYCODE_w:
killLastWord();
g_gui.scheduleTopDialogRedraw();
break;
case Common::KEYCODE_v:
if (g_system->hasTextInClipboard()) {
Common::U32String text = g_system->getTextFromClipboard();
insertIntoPrompt(text.encode().c_str());
scrollToCurrent();
drawLine(pos2line(_currentPos));
}
break;
case Common::KEYCODE_c:
{
Common::String userInput = getUserInput();
if (!userInput.empty())
g_system->setTextInClipboard(userInput);
}
break;
default:
break;
}
}
void ConsoleDialog::killChar() {
for (int i = _currentPos; i < _promptEndPos; i++)
buffer(i) = buffer(i + 1);
if (_promptEndPos > _promptStartPos) {
buffer(_promptEndPos) = ' ';
_promptEndPos--;
}
}
void ConsoleDialog::killLine() {
for (int i = _currentPos; i < _promptEndPos; i++)
buffer(i) = ' ';
_promptEndPos = _currentPos;
}
void ConsoleDialog::killLastWord() {
int cnt = 0;
bool space = true;
while (_currentPos > _promptStartPos) {
if (buffer(_currentPos - 1) == ' ') {
if (!space)
break;
} else
space = false;
_currentPos--;
cnt++;
}
for (int i = _currentPos; i < _promptEndPos; i++)
buffer(i) = buffer(i + cnt);
if (_promptEndPos > _promptStartPos) {
buffer(_promptEndPos) = ' ';
_promptEndPos -= cnt;
}
}
void ConsoleDialog::addToHistory(const Common::String &str) {
_history[_historyIndex] = str;
_historyIndex = (_historyIndex + 1) % kHistorySize;
_historyLine = 0;
if (_historySize < kHistorySize)
_historySize++;
}
void ConsoleDialog::historyScroll(int direction) {
if (_historySize == 0)
return;
if (_historyLine == 0 && direction > 0) {
int i;
for (i = 0; i < _promptEndPos - _promptStartPos; i++)
_history[_historyIndex].insertChar(buffer(_promptStartPos + i), i);
}
// Advance to the next line in the history
int line = _historyLine + direction;
if ((direction < 0 && line < 0) || (direction > 0 && line > _historySize))
return;
_historyLine = line;
// Hide caret if visible
if (_caretVisible)
drawCaret(true);
// Remove the current user text
_currentPos = _promptStartPos;
killLine();
// ... and ensure the prompt is visible
scrollToCurrent();
// Print the text from the history
int idx;
if (_historyLine > 0)
idx = (_historyIndex - _historyLine + _historySize) % _historySize;
else
idx = _historyIndex;
int length = _history[idx].size();
for (int i = 0; i < length; i++)
printCharIntern(_history[idx][i]);
_promptEndPos = _currentPos;
// Ensure once more the caret is visible (in case of very long history entries)
scrollToCurrent();
g_gui.scheduleTopDialogRedraw();
}
void ConsoleDialog::nextLine() {
int line = _currentPos / kCharsPerLine;
if (line == _scrollLine)
_scrollLine++;
_currentPos = (line + 1) * kCharsPerLine;
updateScrollBuffer();
}
// Call this (at least) when the current line changes or when
// a new line is added
void ConsoleDialog::updateScrollBuffer() {
int lastchar = MAX(_promptEndPos, _currentPos);
int line = lastchar / kCharsPerLine;
int numlines = (line < _linesInBuffer) ? line + 1 : _linesInBuffer;
int firstline = line - numlines + 1;
if (firstline > _firstLineInBuffer) {
// clear old line from buffer
for (int i = lastchar; i < (line+1) * kCharsPerLine; ++i)
buffer(i) = ' ';
_firstLineInBuffer = firstline;
}
_scrollBar->_numEntries = numlines;
_scrollBar->_currentPos = _scrollBar->_numEntries - (line - _scrollLine + _linesPerPage);
_scrollBar->_entriesPerPage = _linesPerPage;
_scrollBar->recalc();
}
int ConsoleDialog::printFormat(int dummy, const char *format, ...) {
va_list argptr;
va_start(argptr, format);
int count = this->vprintFormat(dummy, format, argptr);
va_end (argptr);
return count;
}
int ConsoleDialog::vprintFormat(int dummy, const char *format, va_list argptr) {
Common::String buf = Common::String::vformat(format, argptr);
const int size = buf.size();
print(buf.c_str());
buf.trim();
debug("%s", buf.c_str());
return size;
}
void ConsoleDialog::printChar(int c) {
if (_caretVisible)
drawCaret(true);
printCharIntern(c);
drawLine(pos2line(_currentPos));
}
void ConsoleDialog::printCharIntern(int c) {
if (c == '\n')
nextLine();
else {
buffer(_currentPos) = (char)c;
_currentPos++;
if ((_scrollLine + 1) * kCharsPerLine == _currentPos) {
_scrollLine++;
updateScrollBuffer();
}
}
}
void ConsoleDialog::print(const char *str) {
if (_caretVisible)
drawCaret(true);
while (*str)
printCharIntern(*str++);
g_gui.scheduleTopDialogRedraw();
}
void ConsoleDialog::drawCaret(bool erase) {
// TODO: use code from EditableWidget::drawCaret here
int line = _currentPos / kCharsPerLine;
int displayLine = line - _scrollLine + _linesPerPage - 1;
// Only draw caret if visible
if (!isVisible() || displayLine < 0 || displayLine >= _linesPerPage) {
_caretVisible = false;
return;
}
int x = _x + 1 + _leftPadding + (_currentPos % kCharsPerLine) * kConsoleCharWidth;
int y = _y + 2 + _topPadding + displayLine * kConsoleLineHeight;
_caretVisible = !erase;
g_gui.theme()->drawCaret(Common::Rect(x, y, x + 1, y + kConsoleLineHeight), erase);
}
void ConsoleDialog::scrollToCurrent() {
int line = _promptEndPos / kCharsPerLine;
if (line + _linesPerPage <= _scrollLine) {
// TODO - this should only occur for loong edit lines, though
} else if (line > _scrollLine) {
_scrollLine = line;
updateScrollBuffer();
g_gui.scheduleTopDialogRedraw();
}
}
} // End of namespace GUI