/* * CInGameConsole.cpp, part of VCMI engine * * Authors: listed in file AUTHORS in main folder * * License: GNU General Public License v2.0 or later * Full text of license available in license.txt file, in main folder * */ #include "StdInc.h" #include "CInGameConsole.h" #include "../CGameInfo.h" #include "../CMusicHandler.h" #include "../CPlayerInterface.h" #include "../CServerHandler.h" #include "../GameChatHandler.h" #include "../ClientCommandManager.h" #include "../gui/CGuiHandler.h" #include "../gui/WindowHandler.h" #include "../gui/Shortcut.h" #include "../gui/TextAlignment.h" #include "../render/Colors.h" #include "../render/Canvas.h" #include "../render/IScreenHandler.h" #include "../adventureMap/AdventureMapInterface.h" #include "../windows/CMessage.h" #include "../../CCallback.h" #include "../../lib/CConfigHandler.h" #include "../../lib/CThreadHelper.h" #include "../../lib/TextOperations.h" #include "../../lib/mapObjects/CArmedInstance.h" #include "../../lib/MetaString.h" CInGameConsole::CInGameConsole() : CIntObject(KEYBOARD | TIME | TEXTINPUT) , prevEntDisp(-1) { setRedrawParent(true); } void CInGameConsole::showAll(Canvas & to) { show(to); } void CInGameConsole::show(Canvas & to) { if (LOCPLINT->cingconsole != this) return; int number = 0; for(auto & text : texts) { Point leftBottomCorner(0, pos.h); Point textPosition(leftBottomCorner.x + 50, leftBottomCorner.y - texts.size() * 20 - 80 + number * 20); to.drawText(pos.topLeft() + textPosition, FONT_MEDIUM, Colors::GREEN, ETextAlignment::TOPLEFT, text.text); number++; } } void CInGameConsole::tick(uint32_t msPassed) { // Check whether text input is active - we want to keep recent messages visible during this period if(isEnteringText()) return; size_t sizeBefore = texts.size(); for(auto & text : texts) text.timeOnScreen += msPassed; vstd::erase_if( texts, [&](const auto & value) { return value.timeOnScreen > defaultTimeout; } ); if(sizeBefore != texts.size()) GH.windows().totalRedraw(); // FIXME: ingame console has no parent widget set } void CInGameConsole::addMessageSilent(const std::string & timeFormatted, const std::string & senderName, const std::string & messageText) { MetaString formatted = MetaString::createFromRawString("[%s] %s: %s"); formatted.replaceRawString(timeFormatted); formatted.replaceRawString(senderName); formatted.replaceRawString(messageText); // Maximum width for a text line is limited by: // 1) width of adventure map terrain area, for when in-game console is on top of advmap // 2) width of castle/battle window (fixed to 800) when this window is open // 3) arbitrary selected left and right margins int maxWidth = std::min( 800, adventureInt->terrainAreaPixels().w) - 100; auto splitText = CMessage::breakText(formatted.toString(), maxWidth, FONT_MEDIUM); for(const auto & entry : splitText) texts.push_back({entry, 0}); while(texts.size() > maxDisplayedTexts) texts.erase(texts.begin()); } void CInGameConsole::addMessage(const std::string & timeFormatted, const std::string & senderName, const std::string & messageText) { addMessageSilent(timeFormatted, senderName, messageText); GH.windows().totalRedraw(); // FIXME: ingame console has no parent widget set int volume = CCS->soundh->getVolume(); if(volume == 0) CCS->soundh->setVolume(settings["general"]["sound"].Integer()); int handle = CCS->soundh->playSound(AudioPath::builtin("CHAT")); if(volume == 0) CCS->soundh->setCallback(handle, [&]() { if(!GH.screenHandler().hasFocus()) CCS->soundh->setVolume(0); }); } bool CInGameConsole::captureThisKey(EShortcut key) { if (!isEnteringText()) return false; switch (key) { case EShortcut::GLOBAL_ACCEPT: case EShortcut::GLOBAL_CANCEL: case EShortcut::GAME_ACTIVATE_CONSOLE: case EShortcut::GLOBAL_BACKSPACE: case EShortcut::MOVE_UP: case EShortcut::MOVE_DOWN: return true; default: return false; } } void CInGameConsole::keyPressed (EShortcut key) { if (LOCPLINT->cingconsole != this) return; if(!isEnteringText() && key != EShortcut::GAME_ACTIVATE_CONSOLE) return; //because user is not entering any text switch(key) { case EShortcut::GLOBAL_CANCEL: if(!enteredText.empty()) endEnteringText(false); break; case EShortcut::GAME_ACTIVATE_CONSOLE: if(GH.isKeyboardAltDown()) return; //QoL for alt-tab operating system shortcut if(!enteredText.empty()) endEnteringText(false); else startEnteringText(); break; case EShortcut::GLOBAL_ACCEPT: { if(!enteredText.empty()) { bool anyTextExceptCaret = enteredText.size() > 1; endEnteringText(anyTextExceptCaret); } break; } case EShortcut::GLOBAL_BACKSPACE: { if(enteredText.size() > 1) { TextOperations::trimRightUnicode(enteredText,2); enteredText += '_'; refreshEnteredText(); } break; } case EShortcut::MOVE_UP: { if(previouslyEntered.empty()) break; if(prevEntDisp == -1) { prevEntDisp = static_cast(previouslyEntered.size() - 1); enteredText = previouslyEntered[prevEntDisp] + "_"; refreshEnteredText(); } else if( prevEntDisp > 0) { --prevEntDisp; enteredText = previouslyEntered[prevEntDisp] + "_"; refreshEnteredText(); } break; } case EShortcut::MOVE_DOWN: { if(prevEntDisp != -1 && prevEntDisp+1 < previouslyEntered.size()) { ++prevEntDisp; enteredText = previouslyEntered[prevEntDisp] + "_"; refreshEnteredText(); } else if(prevEntDisp+1 == previouslyEntered.size()) //useful feature { prevEntDisp = -1; enteredText = "_"; refreshEnteredText(); } break; } } } void CInGameConsole::textInputed(const std::string & inputtedText) { if (LOCPLINT->cingconsole != this) return; if(!isEnteringText()) return; enteredText.resize(enteredText.size()-1); enteredText += inputtedText; enteredText += "_"; refreshEnteredText(); } void CInGameConsole::textEdited(const std::string & inputtedText) { //do nothing here } void CInGameConsole::showRecentChatHistory() { auto const & history = CSH->getGameChat().getChatHistory(); texts.clear(); int entriesToShow = std::min(maxDisplayedTexts, history.size()); int firstEntryToShow = history.size() - entriesToShow; for (int i = firstEntryToShow; i < history.size(); ++i) addMessageSilent(history[i].dateFormatted, history[i].senderName, history[i].messageText); GH.windows().totalRedraw(); } void CInGameConsole::startEnteringText() { if (!isActive()) return; if(isEnteringText()) { // force-reset text input to re-show on-screen keyboard GH.statusbar()->setEnteringMode(false); GH.statusbar()->setEnteringMode(true); GH.statusbar()->setEnteredText(enteredText); return; } assert(currentStatusBar.expired());//effectively, nullptr check currentStatusBar = GH.statusbar(); enteredText = "_"; GH.statusbar()->setEnteringMode(true); GH.statusbar()->setEnteredText(enteredText); showRecentChatHistory(); } void CInGameConsole::endEnteringText(bool processEnteredText) { prevEntDisp = -1; if(processEnteredText) { std::string txt = enteredText.substr(0, enteredText.size()-1); previouslyEntered.push_back(txt); if(txt.at(0) == '/') { //some commands like gosolo don't work when executed from GUI thread auto threadFunction = [=]() { setThreadName("processCommand"); ClientCommandManager commandController; commandController.processCommand(txt.substr(1), true); }; boost::thread clientCommandThread(threadFunction); clientCommandThread.detach(); } else CSH->getGameChat().sendMessageGameplay(txt); } enteredText.clear(); auto statusbar = currentStatusBar.lock(); assert(statusbar); if (statusbar) statusbar->setEnteringMode(false); currentStatusBar.reset(); } void CInGameConsole::refreshEnteredText() { auto statusbar = currentStatusBar.lock(); assert(statusbar); if (statusbar) statusbar->setEnteredText(enteredText); } bool CInGameConsole::isEnteringText() const { return !enteredText.empty(); }