/*************************************************************************** * Copyright (C) 2008-2013 by Heiko Koehn - KoehnHeiko@googlemail.com * * Copyright (C) 2014-2023 by Stephen Lyons - slysven@virginmedia.com * * Copyright (C) 2014 by Ahmed Charles - acharles@outlook.com * * Copyright (C) 2016 by Ian Adkins - ieadkins@gmail.com * * * * 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., * * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. * ***************************************************************************/ #include "TConsole.h" #include "Host.h" #include "TCommandLine.h" #include "TDebug.h" #include "TDockWidget.h" #include "TEvent.h" #include "THyperlinkVisibilityManager.h" #include "TLabel.h" #include "TMap.h" #include "TMedia.h" #include "TRoomDB.h" #include "TTextBox.h" #include "TTextEdit.h" #include "dlgMapper.h" #include "mudlet.h" #include "GifTracker.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include TMainConsole::TMainConsole(Host* pH, QWidget* parent) : TConsole(pH, qsl("main"), TConsole::MainConsole, parent) , mClipboard(pH) { setFont(pH->getAndClearTempDisplayFont()); // During first use where mIsDebugConsole IS true mudlet::self() is null // then - but we rely on that flag to avoid having to also test for a // non-null mudlet::self() - the connect(...) will produce a debug // message and not make THAT connection should it indeed be null but it // is not fatal... connect(mudlet::self(), &mudlet::signal_profileMapReloadRequested, this, &TMainConsole::slot_reloadMap, Qt::UniqueConnection); connect(this, &TMainConsole::signal_newDataAlert, mudlet::self(), &mudlet::slot_newDataOnHost, Qt::UniqueConnection); // Load up the spelling dictionary from the system: setSystemSpellDictionary(mpHost->getSpellDic()); // Load up the spelling dictionary for the profile - needs to handle the // absence of files for the first run in a new profile or from an older // Mudlet version: setProfileSpellDictionary(); // Ensure the QWidget has the profile name embedded into it setProperty("HostName", pH->getName()); } TMainConsole::~TMainConsole() { // Neither is a child of this console: the map dock is reparented onto the main // window by addDockWidget(), and the unpacking dialog is parentless. So neither // dies with the console automatically. if (mpDockableMapWidget) { mpDockableMapWidget->deleteLater(); } if (mpUnpackingDialog) { mpUnpackingDialog->deleteLater(); } if (mpHunspell_system) { Hunspell_destroy(mpHunspell_system); mpHunspell_system = nullptr; } if (mpHunspell_profile) { Hunspell_destroy(mpHunspell_profile); mpHunspell_profile = nullptr; if (mudlet::self()) { // Need to commit any changes to personal dictionary qDebug() << "TCommandLine::~TConsole(...) INFO - Saving profile's own Hunspell dictionary..."; mudlet::self()->saveDictionary(mudlet::self()->getMudletPath(enums::profileDataItemPath, mProfileName, qsl("profile")), mWordSet_profile); } } } std::pair TMainConsole::setLabelStyleSheet(const QString& name, const QString& stylesheet) { if (name.isEmpty()) { return {false, qsl("a label cannot have an empty string as its name")}; } auto pL = mLabelMap.value(name); if (pL) { pL->setStyleSheet(stylesheet); return {true, QString()}; } return {false, qsl("label name '%1' not found").arg(name)}; } std::optional TMainConsole::getLabelStyleSheet(const QString& name) const { QMap::const_iterator const it = mLabelMap.constFind(name); if (it != mLabelMap.cend() && it.key() == name) { return it.value()->styleSheet(); } return {}; } std::optional TMainConsole::getLabelSizeHint(const QString& name) const { QMap::const_iterator const it = mLabelMap.constFind(name); if (it != mLabelMap.cend() && it.key() == name) { return it.value()->sizeHint(); } return {}; } std::optional TMainConsole::getLabelToolTip(const QString& name) const { auto pL = mLabelMap.value(name); if (!pL) { return {}; } return {pL->toolTip()}; } // NOLINTNEXTLINE(readability-make-member-function-const) std::pair TMainConsole::setUserWindowStyleSheet(const QString& name, const QString& userWindowStyleSheet) { if (name.isEmpty()) { return {false, qsl("a userwindow cannot have an empty string as its name")}; } auto pW = mDockWidgetMap.value(name); if (pW) { pW->setStyleSheet(userWindowStyleSheet); return {true, QString()}; } return {false, qsl("userwindow name '%1' not found").arg(name)}; } std::optional TMainConsole::getUserWindowStyleSheet(const QString& name) const { auto pW = mDockWidgetMap.value(name); if (!pW) { return {}; } return {pW->styleSheet()}; } std::pair TMainConsole::setCmdLineStyleSheet(const QString& name, const QString& styleSheet) { if (name.isEmpty() || !name.compare(qsl("main"))) { mpHost->mpConsole->mpCommandLine->setStyleSheet(styleSheet); return {true, QString()}; } auto pN = mSubCommandLineMap.value(name); if (pN) { pN->setStyleSheet(styleSheet); return {true, QString()}; } return {false, qsl("command-line name '%1' not found").arg(name)}; } std::optional TMainConsole::getCmdLineStyleSheet(const QString& name) const { if (name.isEmpty() || !name.compare(qsl("main"))) { if (auto pMain = mpHost->mpConsole->mpCommandLine) { return {pMain->styleSheet()}; } return {}; } auto pN = mSubCommandLineMap.value(name); if (!pN) { return {}; } return {pN->styleSheet()}; } void TMainConsole::toggleLogging(bool isMessageEnabled) { const auto loggingPath = mudlet::getMudletPath(enums::profileDataItemPath, mpHost->getName(), qsl("autolog")); QFile file(loggingPath); const QDateTime logDateTime = QDateTime::currentDateTime(); if (!mLogToLogFile) { if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) { qWarning() << "TMainConsole: failed to open autolog file for writing:" << file.errorString(); return; } QTextStream out(&file); file.close(); QString directoryLogFile; QString logFileName; // If no log directory is set, default to Mudlet's replay and log files path if (mpHost->mLogDir == nullptr || mpHost->mLogDir.isEmpty()) { directoryLogFile = mudlet::getMudletPath(enums::profileReplayAndLogFilesPath, mProfileName); } else { directoryLogFile = mpHost->mLogDir; } // The format being empty is a signal value that means use a specified // name: if (mpHost->mLogFileNameFormat.isEmpty()) { if (mpHost->mLogFileName.isEmpty()) { // If no log name is set, use the default placeholder //: Must be a valid default filename for a log-file and is used if the user does not enter any other value (Ensure all instances have the same translation {one of two copies}). logFileName = tr("logfile"); } else { // Otherwise a specific name as one is given logFileName = mpHost->mLogFileName; } } else { logFileName = logDateTime.toString(mpHost->mLogFileNameFormat); } // The preset file name formats are derived from date/times so that // alphabetical filename and date sort order are the same... const QDir dirLogFile; if (!dirLogFile.exists(directoryLogFile)) { dirLogFile.mkpath(directoryLogFile); } mpHost->mIsCurrentLogFileInHtmlFormat = mpHost->mIsNextLogFileInHtmlFormat; if (mpHost->mIsCurrentLogFileInHtmlFormat) { mLogFileName = qsl("%1/%2.html").arg(directoryLogFile, logFileName); } else { mLogFileName = qsl("%1/%2.txt").arg(directoryLogFile, logFileName); } mLogFile.setFileName(mLogFileName); // We do not want to use WriteOnly here: // Append = "The device is opened in append mode so that all data is // written to the end of the file." // WriteOnly = "The device is open for writing. Note that this mode // implies Truncate." if (mpHost->mIsCurrentLogFileInHtmlFormat) { if (!mLogFile.open(QIODevice::ReadWrite)) { qWarning() << "TMainConsole: failed to open log file for reading/writing:" << mLogFile.errorString(); return; } } else { if (!mLogFile.open(QIODevice::Append)) { qWarning() << "TMainConsole: failed to open log file for appending:" << mLogFile.errorString(); return; } } mLogStream.setDevice(&mLogFile); if (isMessageEnabled) { const QString message = qsl("%1\n").arg(tr("Logging has started. Log file is %1").arg(mLogFile.fileName())); printSystemMessage(message); // This puts text onto console that is IMMEDIATELY POSTED into log file so // must be done BEFORE logging starts - or actually mLogToLogFile gets set! } mLogToLogFile = true; } else { QFile::remove(loggingPath); mLogToLogFile = false; if (isMessageEnabled) { const QString message = qsl("%1\n").arg(tr("Logging has been stopped. Log file is %1").arg(mLogFile.fileName())); printSystemMessage(message); // This puts text onto console that is IMMEDIATELY POSTED into log file so // must be done AFTER logging ends - or actually mLogToLogFile gets reset! } } if (mLogToLogFile) { // Logging is being turned on if (mpHost->mIsCurrentLogFileInHtmlFormat) { QString log; QTextStream logStream(&log); // No setting a QTextCodec here, they don't work on QString based QTextStreams QStringList fontsList; // List of fonts to become the font-family entry for // the master css in the header fontsList << this->fontInfo().family(); // Seems to be the best way to get the // font in use, as different TConsole // instances within the same profile // might have different fonts fontsList << qsl("Courier New"); fontsList << qsl("Monospace"); fontsList << qsl("Courier"); fontsList.removeDuplicates(); // In case the actual one is one of the defaults here logStream << "\n"; logStream << "\n"; logStream << " \n"; logStream << " "; // put the charset as early as possible as the parser MUST restart when it // switches away from the ASCII default logStream << " \n"; // Nice to identify what made the file! logStream << " " << tr("Mudlet, log from %1 profile").arg(mProfileName) << "\n"; // Web-page title logStream << " \n"; logStream << " \n"; bool isAtBody = false; bool foundBody = false; while (!mLogStream.atEnd()) { const QString line = mLogStream.readLine(); if (line.contains("
")) { // Begin writing old log to the current log when the body is // found. isAtBody = true; foundBody = true; } else if (line.contains("
")) { // Stop writing to current log once the end of the old log's // is reached. isAtBody = false; } if (isAtBody) { logStream << line << "\n"; } } if (!foundBody) { logStream << "
\n"; } else { // Put a horizontal line between separate log sessions logStream << "

\n"; } logStream << qsl("

%1

\n") //: This is the format argument to QDateTime::toString(...) and needs to follow the rules for that function {literal text must be single quoted} as well as being suitable for the translation locale .arg(logDateTime.toString(tr("'Log session starting at 'hh:mm:ss' on 'dddd', 'd' 'MMMM' 'yyyy'."))); //
tags required around outside of the body for // strict HTML 4 as we do not use

s or anything else if (!mLogFile.resize(0)) { qWarning() << "TConsole::toggleLogging(...) ERROR - Failed to resize HTML Logfile - it may now be corrupted...!"; } mLogStream << log; mLogFile.flush(); } else { // File is NOT an HTML one but pure text: // Put a horizontal line between separate log sessions // Unfortunately QLatin1String does not have a repeated() method, // but it does mean we can use non-ASCII/Latin1 characters: // Using 10x U+23AF Horizontal line extension from "Box drawing characters": if (mLogFile.size() > 5) { // Allow a few junk characters ("BOM"???) at the very start of // file to not trigger the insertion of this line: mLogStream << qsl("⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯").repeated(8).append(QChar::LineFeed); } mLogStream << qsl("%1\n") //: This is the format argument to QDateTime::toString(...) and needs to follow the rules for that function {literal text must be single quoted} as well as being suitable for the translation locale .arg(logDateTime.toString(tr("'Log session starting at 'hh:mm:ss' on 'dddd', 'd' 'MMMM' 'yyyy'."))); } logButton->setToolTip(utils::richText(tr("Stop logging game output to log file."))); } else { // Logging is being turned off buffer.logRemainingOutput(); //: This is the format argument to QDateTime::toString(...) and needs to follow the rules for that function {literal text must be single quoted} as well as being suitable for the translation locale const QString endDateTimeLine = logDateTime.toString(tr("'Log session ending at 'hh:mm:ss' on 'dddd', 'd' 'MMMM' 'yyyy'.")); if (mpHost->mIsCurrentLogFileInHtmlFormat) { mLogStream << qsl("

%1

\n").arg(endDateTimeLine); mLogStream << "
\n"; mLogStream << "\n"; } else { // File is NOT an HTML one but pure text: mLogStream << endDateTimeLine << "\n"; } mLogFile.flush(); mLogFile.close(); logButton->setToolTip(utils::richText(tr("Start logging game output to log file."))); } } void TMainConsole::selectCurrentLine(std::string& buf) { const QString key = buf.c_str(); if (key.isEmpty() || key == QLatin1String("main")) { TConsole::selectCurrentLine(); return; } auto pC = mSubConsoleMap.value(key); if (pC) { pC->selectCurrentLine(); } } std::list TMainConsole::getFgColor(QString& buf) { if (buf.isEmpty() || buf == QLatin1String("main")) { return TConsole::getFgColor(); } auto pC = mSubConsoleMap.value(buf); if (pC) { return pC->getFgColor(); } return {}; } std::list TMainConsole::getBgColor(QString& buf) { if (buf.isEmpty() || buf == QLatin1String("main")) { return TConsole::getBgColor(); } auto pC = mSubConsoleMap.value(buf); if (pC) { return pC->getBgColor(); } return {}; } QPair TMainConsole::getTextAttributes(const QString& name) const { if (name.isEmpty() || name == QLatin1String("main")) { return TConsole::getTextAttributes(); } auto pC = mSubConsoleMap.value(name); if (pC) { return pC->getTextAttributes(); } return qMakePair(1, TChar()); } void TMainConsole::luaWrapLine(QString& buf, int line) { if (buf.isEmpty() || buf == QLatin1String("main")) { TConsole::luaWrapLine(line); return; } auto pC = mSubConsoleMap.value(buf); if (pC) { pC->luaWrapLine(line); } } QString TMainConsole::getCurrentLine(const std::string& buf) { const QString key = buf.c_str(); if (key.isEmpty() || key == QLatin1String("main")) { return TConsole::getCurrentLine(); } auto pC = mSubConsoleMap.value(key); if (pC) { return pC->getCurrentLine(); } return qsl("ERROR: mini console does not exist"); } TConsole* TMainConsole::createBuffer(const QString& name) { if (!mSubConsoleMap.contains(name)) { auto pC = new TConsole(mpHost, name, Buffer); mSubConsoleMap[name] = pC; pC->setContentsMargins(0, 0, 0, 0); pC->hide(); pC->layerCommandLine->hide(); return pC; } return nullptr; } void TMainConsole::resetMainConsole() { // Delete DockWidgets first — their child UserWindow TConsoles will be // cascade-deleted by Qt's parent-child ownership. Remove the corresponding // TConsole entries from mSubConsoleMap to avoid dangling pointers. QMutableMapIterator itDockWidget(mDockWidgetMap); while (itDockWidget.hasNext()) { itDockWidget.next(); mSubConsoleMap.remove(itDockWidget.key()); itDockWidget.value()->deleteLater(); itDockWidget.remove(); } QMutableMapIterator itCommandLine(mSubCommandLineMap); while (itCommandLine.hasNext()) { itCommandLine.next(); itCommandLine.value()->deleteLater(); itCommandLine.remove(); } // Remaining SubConsole/Buffer entries (UserWindow ones were already removed above) QMutableMapIterator itSubConsole(mSubConsoleMap); while (itSubConsole.hasNext()) { itSubConsole.next(); itSubConsole.value()->deleteLater(); itSubConsole.remove(); } QMutableMapIterator itLabel(mLabelMap); while (itLabel.hasNext()) { itLabel.next(); if (itLabel.value()->mpMovie) { mpHost->getGifTracker()->unregisterGif(itLabel.value()->mpMovie); } itLabel.value()->deleteLater(); itLabel.remove(); } QMutableMapIterator itScrollBox(mScrollBoxMap); while (itScrollBox.hasNext()) { itScrollBox.next(); itScrollBox.value()->deleteLater(); itScrollBox.remove(); } QMutableMapIterator itTextBox(mTextBoxMap); while (itTextBox.hasNext()) { itTextBox.next(); itTextBox.value()->deleteLater(); itTextBox.remove(); } } // This is a sub-console overlaid on to the main or other console TConsole* TMainConsole::createMiniConsole(const QString& windowname, const QString& name, int x, int y, int width, int height) { //if pW then add Console as Overlay to the Userwindow auto pW = mDockWidgetMap.value(windowname); auto pC = mSubConsoleMap.value(name); auto pS = mScrollBoxMap.value(windowname); if (!pC) { if (pS) { pC = new TConsole(mpHost, name, SubConsole, pS->widget()); } else if (pW) { pC = new TConsole(mpHost, name, SubConsole, pW->widget()); } else { pC = new TConsole(mpHost, name, SubConsole, mpMainFrame); } if (!pC) { return nullptr; } mSubConsoleMap[name] = pC; pC->setObjectName(name); const auto& hostCommandLine = mpHost->mpConsole->mpCommandLine; pC->setFocusProxy(hostCommandLine); pC->mUpperPane->setFocusProxy(hostCommandLine); pC->mLowerPane->setFocusProxy(hostCommandLine); pC->resize(width, height); pC->mOldX = x; pC->mOldY = y; pC->setContentsMargins(0, 0, 0, 0); pC->move(x, y); pC->setFontSize(12); pC->show(); return pC; } return nullptr; } // This is a scrollBox overlaid on to the main console TScrollBox* TMainConsole::createScrollBox(const QString& windowname, const QString& name, int x, int y, int width, int height) { //if pW then add ScrollBox as Overlay to the Userwindow auto pW = mDockWidgetMap.value(windowname); auto pSW = mScrollBoxMap.value(windowname); auto pS = mScrollBoxMap.value(name); if (!pS) { if (pW) { pS = new TScrollBox(mpHost, pW->widget()); } else if (pSW) { pS = new TScrollBox(mpHost, pSW->widget()); } else { pS = new TScrollBox(mpHost, mpMainFrame); } mScrollBoxMap[name] = pS; pS->setObjectName(name); pS->resize(width, height); pS->setContentsMargins(0, 0, 0, 0); pS->move(x, y); pS->show(); return pS; } return nullptr; } TLabel* TMainConsole::createLabel(const QString& windowname, const QString& name, int x, int y, int width, int height, bool fillBackground, bool clickThrough) { //if pW put Label in Userwindow auto pL = mLabelMap.value(name); auto pW = mDockWidgetMap.value(windowname); auto pS = mScrollBoxMap.value(windowname); if (!pL) { if (pW) { pL = new TLabel(mpHost, name, pW->widget()); } else if (pS) { pL = new TLabel(mpHost, name, pS->widget()); } else { pL = new TLabel(mpHost, name, mpMainFrame); } mLabelMap[name] = pL; pL->setAutoFillBackground(fillBackground); pL->setClickThrough(clickThrough); pL->resize(width, height); pL->setContentsMargins(0, 0, 0, 0); pL->move(x, y); pL->show(); mpHost->setBackgroundColor(name, 32, 32, 32, 255); return pL; } return nullptr; } std::pair TMainConsole::deleteLabel(const QString& name) { if (name.isEmpty()) { return {false, QLatin1String("a label cannot have an empty string as its name")}; } auto pL = mLabelMap.take(name); if (pL) { if (pL->mpMovie) { mpHost->getGifTracker()->unregisterGif(pL->mpMovie); } // Using deleteLater() rather than delete as it seems a safer option // given that this item is likely to be linked to some events and // suchlike: pL->deleteLater(); // It remains to be seen if the label has "gone" as a result of the // above by the time the Lua subsystem processes the following: TEvent mudletEvent{}; mudletEvent.mArgumentList.append(QLatin1String("sysLabelDeleted")); mudletEvent.mArgumentTypeList.append(ARGUMENT_TYPE_STRING); mudletEvent.mArgumentList.append(name); mudletEvent.mArgumentTypeList.append(ARGUMENT_TYPE_STRING); mpHost->raiseEvent(mudletEvent); return {true, QString()}; } // Message is of the form needed for a Lua API function call run-time error return {false, qsl("label name '%1' not found").arg(name)}; } std::pair TMainConsole::deleteMiniConsole(const QString& name) { if (name.isEmpty()) { return {false, QLatin1String("a miniconsole cannot have an empty string as its name")}; } auto pConsole = mSubConsoleMap.take(name); if (pConsole) { mCachedWindowSizes.remove(name); // A UserWindow's TConsole lives *inside* a TDockWidget. Deleting only the // console (as for an ordinary miniconsole) leaves the dock orphaned in // mDockWidgetMap with a now-null widget(), which later crashes - e.g. in // getUserWindowSize() - when a window of the same name is recreated. Tear // the dock down too; it owns the console as its child widget and deletes // it along with itself (mirrors the shutdown path in TConsole::closeEvent). if (pConsole->getType() == TConsole::UserWindow) { if (auto pDock = mDockWidgetMap.take(name)) { // deleteLater() alone is sufficient: the console is the dock's // child widget, so destroying the dock destroys the console with // it. (No WA_DeleteOnClose - we delete programmatically here, not // in response to a close event.) pDock->deleteLater(); } else { pConsole->deleteLater(); } } else { // Using deleteLater() rather than delete as it seems a safer option // given that this item is likely to be linked to some events and // suchlike: pConsole->deleteLater(); } // It remains to be seen if the miniconsole has "gone" as a result of the // above by the time the Lua subsystem processes the following: TEvent mudletEvent{}; mudletEvent.mArgumentList.append(QLatin1String("sysMiniConsoleDeleted")); mudletEvent.mArgumentTypeList.append(ARGUMENT_TYPE_STRING); mudletEvent.mArgumentList.append(name); mudletEvent.mArgumentTypeList.append(ARGUMENT_TYPE_STRING); mpHost->raiseEvent(mudletEvent); return {true, QString()}; } // Message is of the form needed for a Lua API function call run-time error return {false, qsl("miniconsole name '%1' not found").arg(name)}; } std::pair TMainConsole::deleteCommandLine(const QString& name) { if (name.isEmpty()) { return {false, QLatin1String("a command line cannot have an empty string as its name")}; } auto pCmdLine = mSubCommandLineMap.take(name); if (pCmdLine) { // Using deleteLater() rather than delete as it seems a safer option // given that this item is likely to be linked to some events and // suchlike: pCmdLine->deleteLater(); // It remains to be seen if the command line has "gone" as a result of the // above by the time the Lua subsystem processes the following: TEvent mudletEvent{}; mudletEvent.mArgumentList.append(QLatin1String("sysCommandLineDeleted")); mudletEvent.mArgumentTypeList.append(ARGUMENT_TYPE_STRING); mudletEvent.mArgumentList.append(name); mudletEvent.mArgumentTypeList.append(ARGUMENT_TYPE_STRING); mpHost->raiseEvent(mudletEvent); return {true, QString()}; } // Message is of the form needed for a Lua API function call run-time error return {false, qsl("command line name '%1' not found").arg(name)}; } std::pair TMainConsole::deleteTextBox(const QString& name) { if (name.isEmpty()) { return {false, QLatin1String("a text edit cannot have an empty string as its name")}; } auto pTextBox = mTextBoxMap.take(name); if (pTextBox) { pTextBox->deleteLater(); TEvent mudletEvent{}; mudletEvent.mArgumentList.append(QLatin1String("sysTextEditDeleted")); mudletEvent.mArgumentTypeList.append(ARGUMENT_TYPE_STRING); mudletEvent.mArgumentList.append(name); mudletEvent.mArgumentTypeList.append(ARGUMENT_TYPE_STRING); mpHost->raiseEvent(mudletEvent); return {true, QString()}; } return {false, qsl("text edit name '%1' not found").arg(name)}; } std::pair TMainConsole::deleteScrollBox(const QString& name) { if (name.isEmpty()) { return {false, QLatin1String("a scrollbox cannot have an empty string as its name")}; } auto pScrollBox = mScrollBoxMap.take(name); if (pScrollBox) { // Using deleteLater() rather than delete as it seems a safer option // given that this item is likely to be linked to some events and // suchlike: pScrollBox->deleteLater(); // It remains to be seen if the scrollbox has "gone" as a result of the // above by the time the Lua subsystem processes the following: TEvent mudletEvent{}; mudletEvent.mArgumentList.append(QLatin1String("sysScrollBoxDeleted")); mudletEvent.mArgumentTypeList.append(ARGUMENT_TYPE_STRING); mudletEvent.mArgumentList.append(name); mudletEvent.mArgumentTypeList.append(ARGUMENT_TYPE_STRING); mpHost->raiseEvent(mudletEvent); return {true, QString()}; } // Message is of the form needed for a Lua API function call run-time error return {false, qsl("scrollbox name '%1' not found").arg(name)}; } std::pair TMainConsole::setLabelToolTip(const QString& name, const QString& text, double duration) { if (name.isEmpty()) { return {false, qsl("a label cannot have an empty string as its name")}; } auto pL = mLabelMap.value(name); if (pL) { duration = duration * 1000; pL->setToolTip(text); pL->setToolTipDuration(duration); return {true, QString()}; } // Message is of the form needed for a Lua API function call run-time error return {false, qsl("label name '%1' not found").arg(name)}; } std::pair TMainConsole::setLabelCursor(const QString& name, int shape) { if (name.isEmpty()) { return {false, qsl("a label cannot have an empty string as its name")}; } auto pL = mLabelMap.value(name); if (pL) { if (shape > -1 && shape < 22) { pL->setCursor(static_cast(shape)); } else if (shape == -1) { pL->unsetCursor(); } else { return {false, qsl("cursor shape '%1' not found. see https://doc.qt.io/qt-5/qt.html#CursorShape-enum").arg(shape)}; } return {true, QString()}; } return {false, qsl("label name '%1' not found").arg(name)}; } std::pair TMainConsole::setLabelCustomCursor(const QString& name, const QString& pixMapLocation, int hotX, int hotY) { if (name.isEmpty()) { return {false, qsl("a label cannot have an empty string as its name")}; } if (pixMapLocation.isEmpty()) { return {false, qsl("custom cursor location cannot be an empty string")}; } auto pL = mLabelMap.value(name); if (pL) { const QPixmap cursor_pixmap = QPixmap(pixMapLocation); if (cursor_pixmap.isNull()) { return {false, qsl("couldn't find custom cursor, is the location \"%1\" correct?").arg(pixMapLocation)}; } const QCursor custom_cursor = QCursor(cursor_pixmap, hotX, hotY); pL->setCursor(custom_cursor); return {true, QString()}; } return {false, qsl("label name '%1' not found").arg(name)}; } // Called from TLuaInterpreter::createMapper(...) to create a map in a TConsole, // Host::showHideOrCreateMapper(...) {formerly also called // createMapper(...)} is used in other cases to make a map in a QDockWidget: std::pair TMainConsole::createMapper(const QString& windowname, int x, int y, int width, int height) { auto pW = mDockWidgetMap.value(windowname); auto pM = mpDockableMapWidget; if (pM) { return {false, qsl("cannot create mapper. Do you already use a map window?")}; } if (!mpMapper) { // Arrange for TMap member values to be copied from the Host masters so they // are in place when the 2D mapper is created: mpHost->getPlayerRoomStyleDetails(mpHost->mpMap->mPlayerRoomStyle, mpHost->mpMap->mPlayerRoomOuterDiameterPercentage, mpHost->mpMap->mPlayerRoomInnerDiameterPercentage, mpHost->mpMap->mPlayerRoomOuterColor, mpHost->mpMap->mPlayerRoomInnerColor); if (!pW) { mpMapper = new dlgMapper(mpMainFrame, mpHost, mpHost->mpMap.data()); } else { mpMapper = new dlgMapper(pW->widget(), mpHost, mpHost->mpMap.data()); } mpHost->mpMap->mpHost = mpHost; mpHost->mpMap->mpMapper = mpMapper; if (mpHost->mpMap->mpRoomDB->isEmpty()) { // Don't load a map if we already have one around! qDebug() << "TConsole::createMapper() - restore map case 2."; mpHost->mpMap->pushErrorMessagesToFile(tr("Pre-Map loading(2) report"), true); const QDateTime now(QDateTime::currentDateTime()); if (mpHost->mpMap->restore(QString())) { mpHost->mpMap->audit(); mpMapper->mp2dMap->init(); mpMapper->updateAreaComboBox(); mpMapper->resetAreaComboBoxToPlayerRoomArea(); mpMapper->show(); } mpHost->mpMap->pushErrorMessagesToFile(tr("Loading map(2) at %1 report").arg(now.toString(Qt::ISODate)), true); TEvent mapOpenEvent{}; mapOpenEvent.mArgumentList.append(QLatin1String("mapOpenEvent")); mapOpenEvent.mArgumentTypeList.append(ARGUMENT_TYPE_STRING); mpHost->raiseEvent(mapOpenEvent); } } mpMapper->resize(width, height); mpMapper->move(x, y); // Qt bug workaround: on Windows and during profile load only, if the mapper widget is created // it gives a height and width to mpLeftToolBar, mpRightToolBar, and mpTopToolBar for // some reason. Those widgets size back down immediately after on their own (?!), however if // getMainWindowSize() is called right after map create, the sizes reported will be wrong #if defined(Q_OS_WINDOWS) mpLeftToolBar->setHidden(true); mpRightToolBar->setHidden(true); mpTopToolBar->setHidden(true); mpMapper->show(); mpLeftToolBar->setVisible(true); mpRightToolBar->setVisible(true); mpTopToolBar->setVisible(true); #else mpMapper->show(); #endif return {true, QString()}; } std::pair TMainConsole::createCommandLine(const QString& windowname, const QString& name, int x, int y, int width, int height) { if (name.isEmpty()) { return {false, QLatin1String("a commandLine cannot have an empty string as its name")}; } auto pN = mSubCommandLineMap.value(name); auto pW = mDockWidgetMap.value(windowname); auto pS = mScrollBoxMap.value(windowname); if (!pN) { if (pS) { pN = new TCommandLine(mpHost, name, TCommandLine::SubCommandLine, this, pS->widget()); } else if (pW) { pN = new TCommandLine(mpHost, name, TCommandLine::SubCommandLine, this, pW->widget()); } else { pN = new TCommandLine(mpHost, name, TCommandLine::SubCommandLine, this, mpMainFrame); } mSubCommandLineMap[name] = pN; pN->resize(width, height); pN->move(x, y); pN->show(); return {true, QString()}; } return {false, QLatin1String("couldn't create commandLine")}; } std::pair TMainConsole::createTextBox(const QString& windowname, const QString& name, int x, int y, int width, int height) { if (name.isEmpty()) { return {false, QLatin1String("a text edit cannot have an empty string as its name")}; } auto pT = mTextBoxMap.value(name); auto pW = mDockWidgetMap.value(windowname); auto pS = mScrollBoxMap.value(windowname); if (!pT) { if (pS) { pT = new TTextBox(mpHost, name, pS->widget()); } else if (pW) { pT = new TTextBox(mpHost, name, pW->widget()); } else { pT = new TTextBox(mpHost, name, mpMainFrame); } mTextBoxMap[name] = pT; pT->resize(width, height); pT->move(x, y); pT->show(); return {true, QString()}; } return {false, QLatin1String("couldn't create text edit")}; } bool TMainConsole::setBackgroundImage(const QString& name, const QString& path) { auto pL = mLabelMap.value(name); if (pL) { const QPixmap bgPixmap(path); pL->setPixmap(bgPixmap); return true; } return false; } // Does NOT act on the TMainConsole itself: bool TMainConsole::setBackgroundColor(const QString& name, int r, int g, int b, int alpha) { auto pC = mSubConsoleMap.value(name); auto pL = mLabelMap.value(name); if (pC) { QPalette mainPalette; mainPalette.setColor(QPalette::Window, QColor(r, g, b, alpha)); pC->setPalette(mainPalette); pC->mUpperPane->mBgColor = QColor(r, g, b, alpha); pC->mLowerPane->mBgColor = QColor(r, g, b, alpha); // update the display properly when color selections change. pC->mUpperPane->updateScreenView(); pC->mUpperPane->forceUpdate(); if (!pC->mUpperPane->mIsTailMode) { // The upper pane having mIsTailMode true means lower pane is hidden pC->mLowerPane->updateScreenView(); pC->mLowerPane->forceUpdate(); } return true; } if (pL) { QPalette mainPalette; mainPalette.setColor(QPalette::Window, QColor(r, g, b, alpha)); pL->setPalette(mainPalette); return true; } return false; } bool TMainConsole::raiseWindow(const QString& name) { auto pC = mSubConsoleMap.value(name); auto pL = mLabelMap.value(name); auto pM = mpMapper; auto pN = mSubCommandLineMap.value(name); auto pS = mScrollBoxMap.value(name); auto pT = mTextBoxMap.value(name); if (pC) { pC->raise(); return true; } if (pL) { pL->raise(); return true; } if (pM && !name.compare(QLatin1String("mapper"), Qt::CaseInsensitive)) { pM->raise(); return true; } if (pS) { pS->raise(); return true; } if (pN) { pN->raise(); return true; } if (pT) { pT->raise(); return true; } return false; } bool TMainConsole::lowerWindow(const QString& name) { auto pC = mSubConsoleMap.value(name); auto pL = mLabelMap.value(name); auto pM = mpMapper; auto pN = mSubCommandLineMap.value(name); auto pS = mScrollBoxMap.value(name); auto pT = mTextBoxMap.value(name); if (pC) { pC->lower(); mpMainDisplay->lower(); return true; } if (pL) { pL->lower(); mpMainDisplay->lower(); return true; } if (pM && !name.compare(QLatin1String("mapper"), Qt::CaseInsensitive)) { pM->lower(); mpMainDisplay->lower(); return true; } if (pS) { pS->lower(); mpMainDisplay->lower(); return true; } if (pN) { pN->lower(); mpMainDisplay->lower(); return true; } if (pT) { pT->lower(); mpMainDisplay->lower(); return true; } return false; } bool TMainConsole::showWindow(const QString& name) { auto pC = mSubConsoleMap.value(name); auto pL = mLabelMap.value(name); if (pC) { pC->mUpperPane->updateScreenView(); pC->mUpperPane->forceUpdate(); pC->show(); pC->mLowerPane->updateScreenView(); pC->mLowerPane->forceUpdate(); return true; } if (pL) { pL->show(); return true; } return false; } bool TMainConsole::hideWindow(const QString& name) { auto pC = mSubConsoleMap.value(name); auto pL = mLabelMap.value(name); if (pC) { pC->hide(); return true; } if (pL) { pL->hide(); return true; } return false; } bool TMainConsole::printWindow(const QString& name, const QString& text) { auto pC = mSubConsoleMap.value(name); auto pL = mLabelMap.value(name); if (pC) { pC->print(text); return true; } if (pL) { pL->setText(text); return true; } return false; } //getUserWindowSize for resizing in Geyser QSize TMainConsole::getUserWindowSize(const QString& windowname) const { auto pW = mDockWidgetMap.value(windowname); // Guard pW->widget(): a dock can briefly outlive its console (the console is // deleted via deleteLater()), during which widget() is null - dereferencing // it segfaults. Fall back to the main window size until the dock is gone. if (pW && pW->widget()) { const QSize windowSize = pW->widget()->size(); const int minValidWidth = 50; // Reject obviously invalid sizes if (windowSize.width() < minValidWidth) { if (mCachedWindowSizes.contains(windowname)) { return mCachedWindowSizes.value(windowname); } return windowSize; } // Reject suspicious shrinkage (more than 50% reduction suggests profile // is transitioning and geometry isn't settled yet) if (mCachedWindowSizes.contains(windowname)) { const QSize cachedSize = mCachedWindowSizes.value(windowname); const double shrinkageRatio = static_cast(windowSize.width()) / cachedSize.width(); if (shrinkageRatio < 0.5) { return cachedSize; } } // Size looks valid, cache and return it mCachedWindowSizes[windowname] = windowSize; return windowSize; } return getMainWindowSize(); } QPair TMainConsole::addWordToSet(const QString& word) { const QString errMsg = qsl("the word \"%1\" already seems to be in the user dictionary"); QPair result{}; if (!mEnableUserDictionary) { return qMakePair(false, QLatin1String("a user dictionary is not enable for this profile")); } if (!mUseSharedDictionary) { // The return value from this function is unclear - it does not seems to // indicate anything useful Hunspell_add(mpHunspell_profile, word.toUtf8().constData()); if (!mWordSet_profile.contains(word)) { mWordSet_profile.insert(word); qDebug().noquote().nospace() << "TConsole::addWordToSet(\"" << word << "\") INFO - word added to profile mWordSet."; result.first = true; } else { result.second = errMsg.arg(word); } } else { auto pMudlet = mudlet::self(); QPair sharedDictionaryResult = pMudlet->addWordToSet(word); while (!sharedDictionaryResult.first) { qDebug() << "TConsole::addWordToSet(...) ALERT - failed to get a write lock to access mWordSet_shared and loaded shared hunspell dictionary, retrying..."; sharedDictionaryResult = pMudlet->addWordToSet(word); } if (sharedDictionaryResult.second) { // Successfully added word: result.first = true; } else { // Word already present result.second = errMsg.arg(word); } } return result; } QPair TMainConsole::removeWordFromSet(const QString& word) { const QString errMsg = qsl("the word \"%1\" does not seem to be in the user dictionary"); QPair result{}; if (!mEnableUserDictionary) { return qMakePair(false, QLatin1String("a user dictionary is not enable for this profile")); } if (!mUseSharedDictionary) { // The return value from this function is unclear - it does not seems to // indicate anything useful Hunspell_remove(mpHunspell_profile, word.toUtf8().constData()); if (mWordSet_profile.remove(word)) { qDebug().noquote().nospace() << "TConsole::removeWordFromSet(\"" << word << "\") INFO - word removed from profile mWordSet."; result.first = true; } else { result.second = errMsg.arg(word); } } else { auto pMudlet = mudlet::self(); QPair sharedDictionaryResult = pMudlet->removeWordFromSet(word); while (!sharedDictionaryResult.first) { qDebug() << "TConsole::removeWordFromSet(...) ALERT - failed to get a write lock to access mWordSet_shared and loaded shared hunspell dictionary, retrying..."; sharedDictionaryResult = pMudlet->removeWordFromSet(word); } if (sharedDictionaryResult.second) { // Successfully added word: result.first = true; } else { // Word already present result.second = errMsg.arg(word); } } return result; } void TMainConsole::setSystemSpellDictionary(const QString& newDict) { if (newDict.isEmpty() || mSpellDic == newDict) { return; } mSpellDic = newDict; const QString path = mudlet::getMudletPath(enums::hunspellDictionaryPath, mpHost->getSpellDic()); QString spell_aff = qsl("%1%2.aff").arg(path, newDict); QString spell_dic = qsl("%1%2.dic").arg(path, newDict); if (mpHunspell_system) { Hunspell_destroy(mpHunspell_system); } #if defined(Q_OS_WINDOWS) // strip non-ASCII characters from the path because hunspell can't handle them // when compiled with MinGW 7.3.0 mudlet::self()->sanitizeUtf8Path(spell_aff, qsl("%1.aff").arg(newDict)); mudlet::self()->sanitizeUtf8Path(spell_dic, qsl("%1.dic").arg(newDict)); #endif mpHunspell_system = Hunspell_create(spell_aff.toUtf8().constData(), spell_dic.toUtf8().constData()); if (mpHunspell_system) { mHunspellCodecName_system = QByteArray(Hunspell_get_dic_encoding(mpHunspell_system)); qDebug().noquote().nospace() << "TMainConsole::setSystemSpellDictionary(\"" << newDict << "\") INFO - System Hunspell dictionary loaded for profile, it uses a \"" << Hunspell_get_dic_encoding(mpHunspell_system) << "\" encoding..."; } } // NOTE: mEnabledUserDictionary has been wedged on (it will never be false) void TMainConsole::setProfileSpellDictionary() { // Determine and copy the configuration settings from the Host instance: mpHost->getUserDictionaryOptions(mEnableUserDictionary, mUseSharedDictionary); if (!mEnableUserDictionary) { if (mpHunspell_profile) { Hunspell_destroy(mpHunspell_profile); mpHunspell_profile = nullptr; // Need to commit any changes to personal dictionary qDebug() << "TMainConsole::setProfileSpellDictionary() INFO - Saving profile's own Hunspell dictionary..."; mudlet::self()->saveDictionary(mudlet::self()->getMudletPath(enums::profileDataItemPath, mProfileName, qsl("profile")), mWordSet_profile); } // Nothing else to do if not using the shared one } else { if (!mUseSharedDictionary) { // Want to use per profile dictionary, is it loaded? if (!mpHunspell_profile) { // No - so load it qDebug() << "TMainConsole::setProfileSpellDictionary() INFO - Preparing profile's own Hunspell dictionary..."; mpHunspell_profile = mudlet::self()->prepareProfileDictionary(mpHost->getName(), mWordSet_profile); } // Else no need to load it } else { // Want to use the shared dictionary - this will open it if needed: mpHunspell_shared = mudlet::self()->prepareSharedDictionary(); } } } QSet TMainConsole::getWordSet() const { if (!mEnableUserDictionary) { return QSet(); } if (!mUseSharedDictionary) { return mWordSet_profile; } return mudlet::self()->getWordSet(); } void TMainConsole::setProfileName(const QString& newName) { TConsole::setProfileName(newName); for (const auto pC : std::as_const(mSubConsoleMap)) { pC->setProfileName(newName); } } void TMainConsole::refreshSubconsoles() { for (const auto pC : std::as_const(mSubConsoleMap)) { if (pC) { pC->refreshView(); } } } std::pair TMainConsole::setUserWindowTitle(const QString& name, const QString& text) { if (name.isEmpty()) { return {false, qsl("a user window cannot have an empty string as its name")}; } auto pC = mSubConsoleMap.value(name); if (!pC) { return {false, qsl("user window name '%1' not found").arg(name)}; } // If it does not have an mType of UserWindow then it does not in a // floatable/dockable widget - so it can't have a titlebar...! if (pC->getType() != UserWindow) { return {false, qsl("\"%1\" is not a user window").arg(name)}; } auto pD = mDockWidgetMap.value(name); if (Q_LIKELY(pD)) { if (text.isEmpty()) { // Reset to default text: pD->setWindowTitle(tr("User window - %1 - %2").arg(mpHost->getName(), name)); return {true, QString()}; } pD->setWindowTitle(text); return {true, QString()}; } // This should be: Q_UNREACHABLE(); // as it means that the TConsole is flagged as being a user window yet // it does not have a TDockWidget to hold it... return {false, qsl("internal error: TConsole \"%1\" is marked as a user window but does not have a TDockWidget to contain it").arg(name)}; } // The title is in .second when .first is true, otherwise .second is why there // is none. Mirrors setUserWindowTitle's checks in the same order and words, so // that a miniconsole sharing the name is not reported as a missing window. std::pair TMainConsole::getUserWindowTitle(const QString& name) const { if (name.isEmpty()) { return {false, qsl("a user window cannot have an empty string as its name")}; } auto pC = mSubConsoleMap.value(name); if (!pC) { return {false, qsl("user window name '%1' not found").arg(name)}; } if (pC->getType() != UserWindow) { return {false, qsl("\"%1\" is not a user window").arg(name)}; } auto pD = mDockWidgetMap.value(name); if (!pD) { return {false, qsl("internal error: TConsole \"%1\" is marked as a user window but does not have a TDockWidget to contain it").arg(name)}; } return {true, pD->windowTitle()}; } bool TMainConsole::setTextFormat(const QString& name, const QColor& fgColor, const QColor& bgColor, const TChar::AttributeFlags& flags) { if (name.isEmpty() || name.compare(qsl("main"), Qt::CaseSensitive) == 0) { mFormatCurrent.setTextFormat(fgColor, bgColor, flags); return true; } auto pC = mSubConsoleMap.value(name); if (pC) { pC->mFormatCurrent.setTextFormat(fgColor, bgColor, flags); return true; } return false; } void TMainConsole::printOnDisplay(std::string& incomingSocketData, const bool isFromServer) { Q_ASSERT_X(mpLineEdit_networkLatency, "TMainConsole::printOnDisplay(...)", "mpLineEdit_networkLatency does not point to a valid QLineEdit"); mProcessingTimer.restart(); // Notify visibility manager of incoming data (for output gap detection) if (isFromServer) { getHyperlinkVisibilityManager().onDataReceived(); } mTriggerEngineMode = true; const int beforeTranslateLastLineNumber = buffer.getLastLineNumber(); const auto beforeTranslateLastLine = buffer.line(beforeTranslateLastLineNumber - 1); buffer.translateToPlainText(incomingSocketData, isFromServer); mTriggerEngineMode = false; const int lastLineNumber = buffer.getLastLineNumber(); const bool bufferChanged = lastLineNumber != beforeTranslateLastLineNumber || buffer.line(lastLineNumber - 1) != beforeTranslateLastLine; if (mAlertOnNewData && isFromServer && bufferChanged) { QApplication::alert(mudlet::self(), 0); } // dequeues MXP events and raise them through the LuaInterpreter // TODO: move this somewhere else more appropriate auto& mxpEventQueue = mpHost->mMxpClient.mMxpEvents; while (!mxpEventQueue.isEmpty()) { const auto& event = mxpEventQueue.dequeue(); mpHost->mLuaInterpreter.signalMXPEvent(event.name, event.attrs, event.actions, event.caption); } const double processT = mProcessingTimer.elapsed() / 1000.0; if (mpHost->mTelnet.mGA_Driver) { /*: The first argument 'N' represents the 'N'etwork latency; the second 'S' the 'S'ystem (processing) time */ mpLineEdit_networkLatency->setText(tr("N:%1 S:%2").arg(mpHost->mTelnet.networkLatencyTime, 0, 'f', 3).arg(processT, 0, 'f', 3)); } else { /*: The argument 'S' represents the 'S'ystem (processing) time, in this situation the Game Server is not sending \"GoAhead\" signals so we cannot deduce the network latency... */ mpLineEdit_networkLatency->setText(tr(" S:%1").arg(processT, 0, 'f', 3)); } // Modify the tab text if this is not the currently active host - this // method is only used on the "main" console so no need to filter depending // on TConsole types: emit signal_newDataAlert(mProfileName); } void TMainConsole::runTriggers(int line) { mUserCursor.setY(line); mIsPromptLine = buffer.promptBuffer.at(line); mEngineCursor = line; mUserCursor.setX(0); mCurrentLine = buffer.line(line); mpHost->getLuaInterpreter()->set_lua_string(cmLuaLineVariable, mCurrentLine); mCurrentLine.append('\n'); if (mudlet::smDebugMode) { TDebug(Qt::darkGreen, Qt::black) << "new line arrived:" >> mpHost; TDebug(Qt::lightGray, Qt::black) << TDebug::csmContinue << mCurrentLine << "\n" >> mpHost; } mpHost->incomingStreamProcessor(mCurrentLine, line); mIsPromptLine = false; //FIXME: rewrite: if lines above the current line get deleted -> redraw clean slice // otherwise just delete } void TMainConsole::finalize() { if (mUpperPane) { mUpperPane->showNewLines(); } if (mLowerPane) { mLowerPane->showNewLines(); } } // TODO: It may be worth considering moving the (now) three following methods // to the TMap class...? bool TMainConsole::saveMap(const QString& location, int saveVersion) { const QString filename_map = location.isEmpty() ? mudlet::getMudletPath(enums::profileDateTimeStampedMapPathFileName, mProfileName, QDateTime::currentDateTime().toString(qsl("yyyy-MM-dd#HH-mm-ss"))) : location; const QDir dir_map(mudlet::getMudletPath(enums::profileMapsPath, mProfileName)); if (!dir_map.exists() && !dir_map.mkpath(dir_map.path())) { return false; } QSaveFile file_map(filename_map); if (!file_map.open(QIODevice::WriteOnly)) { return false; } QDataStream out(&file_map); if (mudlet::scmRunTimeQtVersion >= QVersionNumber(5, 13, 0)) { out.setVersion(mudlet::scmQDataStreamFormat_5_12); } bool saved = mpHost->mpMap->serialize(out, saveVersion); if (saved && !file_map.commit()) { qDebug() << "Error saving map: " << (file_map.error() == QFile::NoError ? "issue with serializing" : file_map.errorString()); saved = false; } if (saved) { mpHost->mpMap->resetUnsaved(); mpHost->mpMap->setSaveError(false); } else { mpHost->mpMap->setSaveError(true); } return saved; } bool TMainConsole::loadMap(const QString& location) { Host* pHost = mpHost; if (!pHost) { // Check for valid mpHost pointer (mpHost was/is/will be a QPoint // in later software versions and is a weak pointer until used // (I think - Slysven ?) return false; } if (!pHost->mpMap || !pHost->mpMap->mpMapper) { // No map or map currently loaded - so try and created mapper // but don't load a map here by default, we do that below and it may not // be the default map anyhow pHost->showHideOrCreateMapper(false); } if (!pHost->mpMap || !pHost->mpMap->mpMapper) { // And that failed so give up return false; } pHost->mpMap->mapClear(); qDebug() << "TMainConsole::loadMap() - restore map case 1."; pHost->mpMap->pushErrorMessagesToFile(tr("Pre-Map loading(1) report"), true); const QDateTime now(QDateTime::currentDateTime()); bool result = false; if (pHost->mpMap->restore(location)) { pHost->mpMap->audit(); pHost->mpMap->mpMapper->mp2dMap->init(); pHost->mpMap->mpMapper->updateAreaComboBox(); pHost->mpMap->mpMapper->resetAreaComboBoxToPlayerRoomArea(); pHost->mpMap->mpMapper->show(); result = true; } else { pHost->mpMap->mpMapper->mp2dMap->init(); pHost->mpMap->mpMapper->updateAreaComboBox(); pHost->mpMap->mpMapper->show(); } if (location.isEmpty()) { pHost->mpMap->pushErrorMessagesToFile(tr("Loading map(1) at %1 report").arg(now.toString(Qt::ISODate)), true); } else { pHost->mpMap->pushErrorMessagesToFile(tr(R"(Loading map(1) "%1" at %2 report)").arg(location, now.toString(Qt::ISODate)), true); } pHost->mpMap->updateArea(-1); return result; } // Used by TLuaInterpreter::loadMap() and dlgProfilePreferences for import/load // of files ending in ".xml" // The TLuaInterpreter::loadMap() supplies a pointer to an error Message which // it requires in the event of an error (it should be written in a structure // to match "loadMap: XXXXX." format) - the presence of a non-null pointer here // should be used to suppress the writing of error messages direct to the // console - if possible! bool TMainConsole::importMap(const QString& location, QString* errMsg) { Host* pHost = mpHost; if (!pHost) { // Check for valid mpHost pointer (mpHost was/is/will be a QPoint // in later software versions and is a weak pointer until used // (I think - Slysven ?) if (errMsg) { *errMsg = qsl("loadMap: NULL Host pointer {in TConsole::importMap(...)} - something is wrong!"); } return false; } if (!pHost->mpMap || !pHost->mpMap->mpMapper) { // No map or mapper currently loaded/present - so try and create mapper pHost->showHideOrCreateMapper(false); } if (!pHost->mpMap || !pHost->mpMap->mpMapper) { // And that failed so give up if (errMsg) { *errMsg = qsl("loadMap: unable to initialise mapper {in TConsole::importMap(...)} - something is wrong!"); } return false; } // Dump any outstanding map errors from past activities that had not yet // been logged... qDebug() << "TMainConsole::importingMap() - importing map case 1."; pHost->mpMap->pushErrorMessagesToFile(tr("Pre-Map importing(1) report"), true); const QDateTime now(QDateTime::currentDateTime()); bool result = false; const QFileInfo fileInfo(location); QString filePathNameString; if (!fileInfo.filePath().isEmpty()) { if (fileInfo.isRelative()) { // Resolve the name relative to the profile home directory: filePathNameString = QDir::cleanPath(mudlet::getMudletPath(enums::profileDataItemPath, mProfileName, fileInfo.filePath())); } else { if (fileInfo.exists()) { filePathNameString = fileInfo.canonicalFilePath(); // Cannot use canonical path if file doesn't exist! } else { filePathNameString = fileInfo.absoluteFilePath(); } } } QFile file(filePathNameString); if (!file.exists()) { if (!errMsg) { const QString infoMsg = tr("[ ERROR ] - Map file not found, path and name used was:\n" "%1.") .arg(filePathNameString); pHost->postMessage(infoMsg); } else { // error message for lua loadMap() *errMsg = tr("loadMap: bad argument #1 value (filename used: \n" "\"%1\" was not found).") .arg(filePathNameString); } return false; } if (file.open(QFile::ReadOnly | QFile::Text)) { if (!errMsg) { const QString infoMsg = tr("[ INFO ] - Map file located and opened, now parsing it..."); pHost->postMessage(infoMsg); } result = pHost->mpMap->importMap(file, errMsg); file.close(); pHost->mpMap->pushErrorMessagesToFile(tr(R"(Importing map(1) "%1" at %2 report)").arg(location, now.toString(Qt::ISODate))); } else { if (!errMsg) { const QString infoMsg = tr(R"([ INFO ] - Map file located but it could not opened, please check permissions on:"%1".)").arg(filePathNameString); pHost->postMessage(infoMsg); } else { *errMsg = tr("loadMap: bad argument #1 value (filename used: \n" "\"%1\" could not be opened for reading).") .arg(filePathNameString); } return false; } pHost->mpMap->updateArea(-1); return result; } void TMainConsole::slot_reloadMap(QList profilesList) { Host* pHost = getHost(); if (!pHost) { return; } if (!profilesList.contains(mProfileName)) { qDebug() << "TMainConsole::slot_reloadMap(" << profilesList << ") request received but we:" << mProfileName << "are not mentioned - so we are ignoring it...!"; return; } const QString infoMsg = tr("[ INFO ] - Map reload request received from system..."); pHost->postMessage(infoMsg); QString outcomeMsg; if (loadMap(QString())) { outcomeMsg = tr("[ OK ] - ... System Map reload request completed."); } else { outcomeMsg = tr("[ WARN ] - ... System Map reload request failed."); } pHost->postMessage(outcomeMsg); } void TMainConsole::resizeEvent(QResizeEvent* event) { auto pHost = getHost(); if (!pHost) { return; } // Process the event like other TConsoles TConsole::resizeEvent(event); // Update the record of the text area size for NAWS purposes: pHost->updateDisplayDimensions(); } void TMainConsole::showPackageDownloadProgress(const QString& title, const QString& cancelText) { auto pHost = getHost(); if (!pHost) { qWarning() << "TMainConsole::showPackageDownloadProgress() WARNING - called with no host; ignoring the download-progress request."; return; } // A second server-triggered download can arrive mid-download (e.g. a // reconnect re-sends Client.GUI). QProgressDialog::close() emits canceled(), // so closing the superseded dialog while it is still wired to // slot_cancelPackageDownload() would abort the download this new dialog is // about to track; detach it before closing. if (mpPackageDownloadProgressDialog) { mpPackageDownloadProgressDialog->disconnect(); mpPackageDownloadProgressDialog->close(); } // placeholder range; reset by the first download-progress update mpPackageDownloadProgressDialog = new QProgressDialog(title, cancelText, 0, 4000000, this); connect(mpPackageDownloadProgressDialog, &QProgressDialog::canceled, &pHost->mTelnet, &cTelnet::slot_cancelPackageDownload); mpPackageDownloadProgressDialog->setAttribute(Qt::WA_DeleteOnClose); mpPackageDownloadProgressDialog->show(); } void TMainConsole::updatePackageDownloadProgress(qint64 got, qint64 total) { if (mpPackageDownloadProgressDialog) { // total is -1 for chunked HTTP responses of unknown length; a (0, 0) // range turns the dialog into a busy indicator mpPackageDownloadProgressDialog->setRange(0, total > 0 ? static_cast(total) : 0); mpPackageDownloadProgressDialog->setValue(static_cast(got)); } } void TMainConsole::closePackageDownloadProgress() { if (mpPackageDownloadProgressDialog) { mpPackageDownloadProgressDialog->close(); } } void TMainConsole::createMapProgressDialog(const QString& title, const QString& label, const QString& cancelButtonText, int minimum, int maximum) { if (mpMapProgressDialog) { mpMapProgressDialog->hide(); mpMapProgressDialog->deleteLater(); } auto pHost = getHost(); // If canceled() cannot be wired to the map, omit the cancel button rather // than show one that does nothing. const bool cancelWirable = pHost && !pHost->mpMap.isNull(); // Deliberately not WA_DeleteOnClose: the JSON import keeps updating this // dialog from a processEvents loop, so it must outlive a mid-operation // dismissal; we delete it explicitly instead. mpMapProgressDialog = new QProgressDialog(label, cancelWirable ? cancelButtonText : QString(), minimum, maximum, this); mpMapProgressDialog->setWindowTitle(title); mpMapProgressDialog->setWindowIcon(QIcon(qsl(":/icons/mudlet_map_download.png"))); mpMapProgressDialog->setAutoClose(false); mpMapProgressDialog->setAutoReset(false); // QProgressDialog still emits canceled() on Escape or window-close even with // no cancel button, so only connect it when the operation is cancelable; // otherwise a non-cancelable import could be aborted by a spurious cancel. if (cancelWirable && !cancelButtonText.isEmpty()) { connect(mpMapProgressDialog, &QProgressDialog::canceled, pHost->mpMap.data(), &TMap::slot_mapProgressDialogCancelled); } } void TMainConsole::showMapTransferProgress(const QString& title, const QString& label, const QString& cancelButtonText) { createMapProgressDialog(title, label, cancelButtonText, 0, 0); mpMapProgressDialog->setMinimumWidth(300); mpMapProgressDialog->setMinimumDuration(0); mpMapProgressDialog->show(); } void TMainConsole::showMapJsonProgress(const QString& title, const QString& label, const QString& cancelButtonText, int maximum) { createMapProgressDialog(title, label, cancelButtonText, 0, maximum); mpMapProgressDialog->setWindowModality(Qt::NonModal); mpMapProgressDialog->setMinimumWidth(500); mpMapProgressDialog->setMinimumDuration(1); } void TMainConsole::setMapProgressDialogLabel(const QString& text) { if (mpMapProgressDialog) { mpMapProgressDialog->setLabelText(text); } } void TMainConsole::setMapProgressDialogRange(int minimum, int maximum) { if (mpMapProgressDialog) { mpMapProgressDialog->setRange(minimum, maximum); } } void TMainConsole::setMapProgressDialogValue(int value) { if (mpMapProgressDialog) { mpMapProgressDialog->setValue(value); } } void TMainConsole::disableMapProgressDialogCancel() { if (mpMapProgressDialog) { // Taking the button away does not stop a window-close from emitting // canceled(), so drop the connection as well - by this point the // operation can no longer be stopped. Only ours goes, leaving // QProgressDialog's own canceled() -> cancel() wiring intact. if (auto pHost = getHost(); pHost && !pHost->mpMap.isNull()) { disconnect(mpMapProgressDialog, &QProgressDialog::canceled, pHost->mpMap.data(), &TMap::slot_mapProgressDialogCancelled); } mpMapProgressDialog->setCancelButton(nullptr); } } void TMainConsole::closeMapProgressDialog() { if (mpMapProgressDialog) { // hide() rather than close() so we don't re-enter QProgressDialog's // closeEvent -> cancel() while a cancel is already being handled. mpMapProgressDialog->hide(); mpMapProgressDialog->deleteLater(); // deleteLater() leaves the QPointer set until the event loop gets to // run, which a synchronous JSON operation will not let it do, so forget // the dialog now and make any late writes to it no-ops. mpMapProgressDialog = nullptr; } } void TMainConsole::createMapperDock(const QString& title, const QString& objectName) { mpDockableMapWidget = new QDockWidget(title); mpDockableMapWidget->setObjectName(objectName); } void TMainConsole::showMapperScriptReminder() { QUiLoader loader; QFile file(qsl(":/ui/lacking_mapper_script.ui")); if (!file.open(QFile::ReadOnly)) { qWarning() << "TMainConsole::showMapperScriptReminder() WARNING - failed to open lacking_mapper_script.ui for reading:" << file.errorString(); return; } auto dialog = qobject_cast(loader.load(&file, mudlet::self())); file.close(); if (!dialog) { qWarning() << "TMainConsole::showMapperScriptReminder() WARNING - could not load the mapping-script reminder dialog."; return; } connect(dialog, &QDialog::accepted, mudlet::self(), &mudlet::slot_openMappingScriptsPage); dialog->show(); dialog->raise(); dialog->activateWindow(); } void TMainConsole::showUnpackingProgress(const QString& message, const QString& title) { // deleteLater() not close(): the dialog is parentless with no WA_DeleteOnClose, // so closing it would leak it once we overwrite the pointer below. if (mpUnpackingDialog) { mpUnpackingDialog->deleteLater(); } QUiLoader loader; QFile uiFile(qsl(":/ui/package_manager_unpack.ui")); if (!uiFile.open(QFile::ReadOnly)) { qWarning() << "TMainConsole::showUnpackingProgress() WARNING - failed to open package_manager_unpack.ui for reading:" << uiFile.errorString(); return; } auto* pDialog = qobject_cast(loader.load(&uiFile, nullptr)); uiFile.close(); if (!pDialog) { qWarning() << "TMainConsole::showUnpackingProgress() WARNING - could not load the unpacking progress dialog."; return; } mpUnpackingDialog = pDialog; // Trap: processEvents() below can deliver a re-entrant install (or its // matching hide) that replaces or clears mpUnpackingDialog and disposes of // this frame's dialog. Drive a local pointer, never the member, and bail if // our dialog is taken out from under us. QPointer dialog = pDialog; if (auto* pLabel = dialog->findChild(qsl("label"))) { pLabel->setText(message); } dialog->hide(); // Must hide to change WindowModality dialog->setWindowTitle(title); dialog->setWindowModality(Qt::ApplicationModal); dialog->show(); QCoreApplication::processEvents(); if (!dialog) { return; } dialog->raise(); dialog->repaint(); // Force a redraw QCoreApplication::processEvents(); // Try to ensure we are on top of any other dialogs and freshly drawn } void TMainConsole::closeUnpackingProgress() { if (mpUnpackingDialog) { mpUnpackingDialog->deleteLater(); mpUnpackingDialog = nullptr; } } void TMainConsole::setupVideoOutput(TMediaPlayer* player, bool& setupSucceeded) { setupSucceeded = false; if (!player || !player->mediaPlayer()) { return; } const QString target = player->mediaData().mediaKey(); if (target.isEmpty()) { qWarning() << qsl("TMainConsole::setupVideoOutput() ERROR - 'key' not specified for video."); return; } QString widgetType = TMediaData::MediaWidgetLabel; QWidget* targetWidget = mLabelMap.value(target); if (!targetWidget) { targetWidget = mSubConsoleMap.value(target); if (targetWidget) { widgetType = TMediaData::MediaWidgetWindow; } } if (!targetWidget) { qWarning() << qsl("TMainConsole::setupVideoOutput() ERROR - No matching widget for 'key' = %1 to present video.").arg(target); return; } player->mediaData().setMediaWidget(widgetType); QVideoWidget* myVideoWidget = nullptr; if (widgetType == TMediaData::MediaWidgetLabel) { myVideoWidget = qobject_cast(targetWidget)->mpVideoWidget; } else if (widgetType == TMediaData::MediaWidgetWindow) { myVideoWidget = qobject_cast(targetWidget)->mpVideoWidget; } if (!myVideoWidget) { myVideoWidget = new QVideoWidget(); myVideoWidget->setParent(targetWidget); myVideoWidget->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); if (widgetType == TMediaData::MediaWidgetLabel) { QObject::connect(qobject_cast(targetWidget), &TLabel::resized, myVideoWidget, [targetWidget, myVideoWidget]() { myVideoWidget->resize(targetWidget->size()); }); } else if (widgetType == TMediaData::MediaWidgetWindow) { QObject::connect(qobject_cast(targetWidget), &TConsole::resized, myVideoWidget, [targetWidget, myVideoWidget]() { myVideoWidget->resize(targetWidget->size()); }); } } if (targetWidget->isHidden()) { targetWidget->show(); } myVideoWidget->resize(targetWidget->size()); player->mediaPlayer()->setVideoOutput(myVideoWidget); myVideoWidget->show(); setupSucceeded = true; } void TMainConsole::hideVideoOutput(TMediaPlayer* player) { if (!player || !player->mediaPlayer()) { return; } auto* videoOutput = qobject_cast(player->mediaPlayer()->videoOutput()); if (!videoOutput) { return; } QWidget* parent = videoOutput->parentWidget(); if (parent && parent->isVisible()) { parent->hide(); } } void TMainConsole::showStatistics() { auto pHost = getHost(); if (!pHost) { return; } const QString header = qsl("%1\n").arg(tr("+--------------------------------------------------------------+\n" "| system statistics |\n" "+--------------------------------------------------------------+", "Header for the system's statistics information displayed in the console, it is 64 'narrow' characters wide")); print(header, QColor(150, 120, 0), Qt::black); QStringList subjects; QStringList tables; if (pHost->mTelnet.isGMCPEnabled()) { //: Heading for the system's statistics information displayed in the console subjects << tr("GMCP events:"); tables << QLatin1String("gmcp"); } if (pHost->mTelnet.isATCPEnabled()) { //: Heading for the system's statistics information displayed in the console subjects << tr("ATCP events:"); tables << QLatin1String("atcp"); } if (pHost->mTelnet.isChannel102Enabled()) { //: Heading for the system's statistics information displayed in the console subjects << tr("Channel102 events:"); tables << QLatin1String("channel102"); } if (pHost->mTelnet.isMXPEnabled()) { //: Heading for the system's statistics information displayed in the console subjects << tr("MXP events:"); tables << QLatin1String("mxp"); } if (pHost->mTelnet.isMSSPEnabled()) { //: Heading for the system's statistics information displayed in the console subjects << tr("MSSP events:"); tables << QLatin1String("mssp"); } if (pHost->mTelnet.isMSDPEnabled()) { // This might be a nil rather than an empty table if not present: //: Heading for the system's statistics information displayed in the console subjects << tr("MSDP events:"); tables << QLatin1String("msdp"); } Q_ASSERT_X(subjects.count() == tables.count(), "TMainConsole::showStatistics()", "mismatch in titles and built-in tables to show"); for (int i = 0, total = subjects.count(); i < total; ++i) { mpHost->mLuaInterpreter.compileAndExecuteScript( qsl("setFgColor(190,150,0); setUnderline(true); echo([[\n\n%1\n]]);setUnderline(false);setFgColor(150,120,0);display( %2 );").arg(subjects.at(i), tables.at(i))); } const QString itemScript = "setFgColor(190,150,0); setUnderline(true); echo([[\n\n%1\n]]); setBold(false);setUnderline(false);setFgColor(150,120,0)"; //: Heading for the system's statistics information displayed in the console mpHost->mLuaInterpreter.compileAndExecuteScript(itemScript.arg(tr("Telnet Options:"))); print(pHost->mTelnet.assembleTelnetOptionsReport(), QColor(150, 120, 0), Qt::black); //: Heading for the system's statistics information displayed in the console mpHost->mLuaInterpreter.compileAndExecuteScript(itemScript.arg(tr("Trigger Report:"))); QString itemMsg = std::get<0>(mpHost->getTriggerUnit()->assembleReport()); print(itemMsg, QColor(150, 120, 0), Qt::black); //: Heading for the system's statistics information displayed in the console mpHost->mLuaInterpreter.compileAndExecuteScript(itemScript.arg(tr("Timer Report:"))); itemMsg = std::get<0>(mpHost->getTimerUnit()->assembleReport()); ; print(itemMsg, QColor(150, 120, 0), Qt::black); //: Heading for the system's statistics information displayed in the console mpHost->mLuaInterpreter.compileAndExecuteScript(itemScript.arg(tr("Alias Report:"))); itemMsg = std::get<0>(mpHost->getAliasUnit()->assembleReport()); print(itemMsg, QColor(150, 120, 0), Qt::black); //: Heading for the system's statistics information displayed in the console mpHost->mLuaInterpreter.compileAndExecuteScript(itemScript.arg(tr("Keybinding Report:"))); itemMsg = std::get<0>(mpHost->getKeyUnit()->assembleReport()); print(itemMsg, QColor(150, 120, 0), Qt::black); //: Heading for the system's statistics information displayed in the console mpHost->mLuaInterpreter.compileAndExecuteScript(itemScript.arg(tr("Script Report:"))); itemMsg = std::get<0>(mpHost->getScriptUnit()->assembleReport()); print(itemMsg, QColor(150, 120, 0), Qt::black); //: Heading for the system's statistics information displayed in the console mpHost->mLuaInterpreter.compileAndExecuteScript(itemScript.arg(tr("Gif Report:"))); itemMsg = std::get<0>(mpHost->getGifTracker()->assembleReport()); print(itemMsg, QColor(150, 120, 0), Qt::black); // Footer for the system's statistics information displayed in the console, it should be 64 'narrow' characters wide const QString footer = qsl("\n+--------------------------------------------------------------+\n"); mpHost->mpConsole->print(footer, QColor(150, 120, 0), Qt::black); mpHost->mLuaInterpreter.compileAndExecuteScript(QLatin1String("resetFormat();")); mpHost->mpConsole->raise(); } void TMainConsole::closeEvent(QCloseEvent* event) { // Guard against duplicate close events during widget destruction. // The first close comes from explicit close(), the second can come // from Qt during widget deletion. Processing the event twice can // cause crashes in TBuffer destruction. if (mEnableClose) { event->accept(); return; } qDebug().nospace().noquote() << "TMainConsole::closeEvent(...) INFO - received by \"" << mpHost->getName() << "\"."; TEvent conCloseEvent{}; conCloseEvent.mArgumentList.append(qsl("sysExitEvent")); conCloseEvent.mArgumentTypeList.append(ARGUMENT_TYPE_STRING); mpHost->raiseEvent(conCloseEvent); if (mpHost->mFORCE_SAVE_ON_EXIT || mpHost->isClosingForced()) { mudlet::self()->saveWindowLayout(); mpHost->modulesToWrite.clear(); // We are not checking the status result from here! mpHost->saveProfile(); if (mpHost->mpMap && mpHost->mpMap->mpRoomDB) { // There is a map loaded - but it *could* have no rooms at all! if (!saveMap(QString())) { qWarning() << "TMainConsole::closeEvent(...) WARNING - forced close map save failed"; } } mpHost->waitForProfileSave(); mEnableClose = true; event->accept(); return; } if (!mEnableClose) { ASK_PROFILE: const int choice = QMessageBox::question(this, tr("Save profile?"), tr("Do you want to save the profile %1?").arg(mProfileName), QMessageBox::Yes | QMessageBox::No | QMessageBox::Cancel); if (choice == QMessageBox::Cancel) { event->ignore(); return; } if (choice == QMessageBox::Yes) { mudlet::self()->saveWindowLayout(); mpHost->modulesToWrite.clear(); auto [ok, filename, error] = mpHost->saveProfile(); if (!ok) { QMessageBox::critical( this, tr("Could not save profile"), tr("Sorry, could not save your profile as \"%1\" - got the following error: \"%2\".").arg(filename, error), QMessageBox::Retry); goto ASK_PROFILE; } if (mpHost->mpMap && mpHost->mpMap->mpRoomDB) { // There is a map loaded - but it *could* have no rooms at all! ASK_MAP: if (!saveMap(QString())) { const int mapChoice = QMessageBox::warning(this, tr("Could not save map"), tr("Sorry, could not save the map. Would you like to retry or close without saving the map?"), QMessageBox::Retry | QMessageBox::Ignore | QMessageBox::Cancel); if (mapChoice == QMessageBox::Retry) { goto ASK_MAP; } if (mapChoice == QMessageBox::Cancel) { event->ignore(); return; } // QMessageBox::Ignore - continue without saving map } } mpHost->waitForProfileSave(); mEnableClose = true; event->accept(); return; } if (choice == QMessageBox::No) { mudlet::self()->saveWindowLayout(); mEnableClose = true; event->accept(); return; } if (!mudlet::self()->isGoingDown()) { QMessageBox::warning(this, "Aborting exit", "Session exit aborted on user request."); event->ignore(); return; } mEnableClose = true; event->accept(); } } bool TMainConsole::clear(const QString& name) { if (name.isEmpty() || !name.compare(QLatin1String("main"))) { TConsole::clear(); mUpperPane->showNewLines(); mUpperPane->forceUpdate(); mLowerPane->forceUpdate(); return true; } auto pC = mSubConsoleMap.value(name); if (pC) { pC->clear(); return true; } return false; }