mudlet/src/TMainConsole.cpp
Vadim Peretokin 0f9bc0a4be
infrastructure: decouple the mapper engine from UI dialogs (#9513)
#### Brief overview of PR changes/additions
- Removes all raw Qt Widgets usage from the map engine: `TMap.{h,cpp}`
no longer owns a `QProgressDialog` (and drops a dead `QFileDialog`
include), and `XMLimport.{h,cpp}` no longer pulls in `QApplication` (the
clipboard read now uses `QGuiApplication::clipboard()`, which lives in
Qt Gui).
- The standalone map-progress dialog (shown when the mapper is not
visible, for map download / XML import and JSON export/import) is now
driven by Qt signals carrying pre-translated payloads; the frontend
(`TMainConsole`) owns the actual `QProgressDialog`, and a user cancel
returns to the engine through `TMap::slot_mapProgressDialogCancelled()`.
- Adds `MapProgressDialogSeamTest` covering the transfer-progress state
machine, a JSON export/import round trip driving the new signals, a
mid-import cancel delivered through the seam (the highest-risk change,
since the JSON reader used to poll `QProgressDialog::wasCanceled()`
synchronously), and an XML map import re-entered from inside a running
JSON operation.

#### Motivation for adding to Mudlet
Second concrete step of the re-scoped libmudlet plan (a Qt-Widgets-free
`mudlet_core` for headless use, testability and WASM). It copies the
seam template established in #9507: core emits a pre-translated payload
-> frontend owns the widget -> a callback slot returns the answer. The
Qt Widgets dependency audit (`cmake/audit-core-widgets.sh`) drops from
**151 to 147** offending files; `TMap.cpp`, `TMap.h`, `XMLimport.cpp`
and `XMLimport.h` are all now clean. The mapper-owned inline progress
path (`dlgMapper`/`T2DMap`, Mudlet's own widgets) is deliberately
untouched here - those move wholesale in the later target-split phase.

#### Other info (issues closed, discussion etc)
Part of #8681 / #9011. Existing translations are unaffected: every
progress string keeps its `TMap` `tr()` context, so current translations
carry straight over. Two new strings do arrive, both with `//:`
translator comments - the warnings shown when a map download or an XML
map import is refused because a JSON import/export is already running.
The JSON dialog stays non-modal and the download/import dialog keeps its
modeless styling, each applied by the frontend. The engine keeps its own
`mMapProgressStandalone` / `mMapProgressCancelRequested` /
`mMapProgressStandaloneMaximum` state to replace the widget read-backs
it used to do (`!= nullptr`, `wasCanceled()`, `maximum()`). If a map
operation ever reaches the engine before a console is wired (checked via
`isSignalConnected`), `TMap::warnIfMapProgressUnwired()` logs a loud
`qWarning` rather than silently running with no progress UI.

It also closes a latent null-dereference that exists on `development`
today. With the mapper visible a map download takes the inline-progress
path, leaving `mpProgressDialog` null - so a JSON export started
meanwhile sails past the `if (mpProgressDialog)` "already in progress"
check and creates a dialog of its own. When the download then finishes
inside the `processEvents()` pump the export is running,
`clearTransferProgress()` deletes and nulls *that* dialog, and the
export's next `incrementJsonProgressDialog()` dereferences null. The
engine now records whose dialog is up (`mMapProgressIsTransfer`) so a
transfer only ever closes its own, and `importMap()` refuses to start
while a JSON operation holds the progress - the mirror of the guard
`downloadMap()` has.

Two review-driven details worth flagging: the frontend only wires the
dialog's cancel to the engine when the operation is actually cancelable,
so a non-cancelable local XML import no longer turns a window-close into
a spurious "Map download was canceled" message; and the standalone
download/import dialog is now parented to the console (like the JSON one
always was, and like #9507's package-download dialog), so it centres on
and dies with the profile window. The three `#include <QApplication>`
additions to `Host.cpp` / `dlgTriggerEditor.cpp` /
`dlgConnectionProfiles.cpp` replace the transitive include they used to
get from `XMLimport.h`; all three are already Qt Widgets consumers, so
the audit count is unaffected.

Assisted-by: Claude:claude-opus-4-8
Assisted-by: Claude:claude-opus-5

**Test case:** With a mapper window open, use a game that supports map
download (or call `downloadMap()`) and confirm the progress dialog
shows, updates, and its Abort cancels the download. Then with the mapper
window closed, run `exportJsonMap()` and `importJsonMap()` on a large
map and confirm the non-modal JSON progress dialog appears, updates its
Areas/Rooms/Labels counts, and that clicking Abort during an import
stops it with an "aborted by user" result. Load a local XML map
(Settings -> Map -> load) and confirm closing its progress window does
not print a "Map download was canceled" line. Everything should behave
exactly as on `development`.

#### Demo (before & after)


https://github.com/user-attachments/assets/f1c62580-2d03-4e5b-a6ee-f6e2b78d113d
2026-08-02 15:33:07 +02:00

2217 lines
82 KiB
C++

/***************************************************************************
* 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 <QDialog>
#include <QDockWidget>
#include <QIcon>
#include <QLabel>
#include <QLineEdit>
#include <QMessageBox>
#include <QMimeData>
#include <QProgressDialog>
#include <QUiLoader>
#include <QScrollBar>
#include <QShortcut>
#include <QSizePolicy>
#include <QTextBoundaryFinder>
#include <QTextCodec>
#include <QTextStream>
#include <QPainter>
#include <QVideoWidget>
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<bool, QString> 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<QString> TMainConsole::getLabelStyleSheet(const QString& name) const
{
QMap<QString, TLabel*>::const_iterator const it = mLabelMap.constFind(name);
if (it != mLabelMap.cend() && it.key() == name) {
return it.value()->styleSheet();
}
return {};
}
std::optional<QSize> TMainConsole::getLabelSizeHint(const QString& name) const
{
QMap<QString, TLabel*>::const_iterator const it = mLabelMap.constFind(name);
if (it != mLabelMap.cend() && it.key() == name) {
return it.value()->sizeHint();
}
return {};
}
// NOLINTNEXTLINE(readability-make-member-function-const)
std::pair<bool, QString> 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::pair<bool, QString> 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)};
}
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 << "<!DOCTYPE HTML PUBLIC '-//W3C//DTD HTML 4.01//EN' 'http://www.w3.org/TR/html4/strict.dtd'>\n";
logStream << "<html>\n";
logStream << " <head>\n";
logStream << " <meta http-equiv='content-type' content='text/html; charset=utf-8'>";
// put the charset as early as possible as the parser MUST restart when it
// switches away from the ASCII default
logStream << " <meta name='generator' content='" << tr("Mudlet MUD Client version: %1%2").arg(APP_VERSION, mudlet::self()->mAppBuild) << "'>\n";
// Nice to identify what made the file!
logStream << " <title>" << tr("Mudlet, log from %1 profile").arg(mProfileName) << "</title>\n";
// Web-page title
logStream << " <style type='text/css'>\n";
logStream << " <!-- body { font-family: '" << fontsList.join("', '") << "'; font-size: 100%; line-height: 1.125em; white-space: nowrap; color:rgb(" << mpHost->mFgColor.red() << ","
<< mpHost->mFgColor.green() << "," << mpHost->mFgColor.blue() << "); background-color:rgb(" << mpHost->mBgColor.red() << "," << mpHost->mBgColor.green() << ","
<< mpHost->mBgColor.blue() << ");}\n";
logStream << " span { white-space: pre-wrap; }\n";
if (mpHost->getEnableBlinkText()) {
logStream << " @keyframes blink-slow { 0%, 100% { opacity: 0.4; } 50% { opacity: 1; } }\n";
logStream << " @keyframes blink-fast { 0%, 100% { opacity: 0.4; } 50% { opacity: 1; } }\n";
logStream << " .blink-slow { animation: blink-slow 2s ease-in-out infinite; }\n";
logStream << " .blink-fast { animation: blink-fast 1s ease-in-out infinite; }\n";
}
logStream << " -->\n";
logStream << " </style>\n";
logStream << " </head>\n";
bool isAtBody = false;
bool foundBody = false;
while (!mLogStream.atEnd()) {
const QString line = mLogStream.readLine();
if (line.contains("<body><div>")) {
// Begin writing old log to the current log when the body is
// found.
isAtBody = true;
foundBody = true;
} else if (line.contains("</div></body>")) {
// Stop writing to current log once the end of the old log's
// <body> is reached.
isAtBody = false;
}
if (isAtBody) {
logStream << line << "\n";
}
}
if (!foundBody) {
logStream << " <body><div>\n";
} else {
// Put a horizontal line between separate log sessions
logStream << " </div><hr><div>\n";
}
logStream
<< qsl("<p>%1</p>\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'.")));
// <div></div> tags required around outside of the body <span></spans> for
// strict HTML 4 as we do not use <p></p>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("<p>%1</p>\n").arg(endDateTimeLine);
mLogStream << " </div></body>\n";
mLogStream << "</html>\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<int> 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<int> 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<quint8, TChar> 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<QString, TDockWidget*> itDockWidget(mDockWidgetMap);
while (itDockWidget.hasNext()) {
itDockWidget.next();
mSubConsoleMap.remove(itDockWidget.key());
itDockWidget.value()->deleteLater();
itDockWidget.remove();
}
QMutableMapIterator<QString, TCommandLine*> itCommandLine(mSubCommandLineMap);
while (itCommandLine.hasNext()) {
itCommandLine.next();
itCommandLine.value()->deleteLater();
itCommandLine.remove();
}
// Remaining SubConsole/Buffer entries (UserWindow ones were already removed above)
QMutableMapIterator<QString, TConsole*> itSubConsole(mSubConsoleMap);
while (itSubConsole.hasNext()) {
itSubConsole.next();
itSubConsole.value()->deleteLater();
itSubConsole.remove();
}
QMutableMapIterator<QString, TLabel*> itLabel(mLabelMap);
while (itLabel.hasNext()) {
itLabel.next();
if (itLabel.value()->mpMovie) {
mpHost->getGifTracker()->unregisterGif(itLabel.value()->mpMovie);
}
itLabel.value()->deleteLater();
itLabel.remove();
}
QMutableMapIterator<QString, TScrollBox*> itScrollBox(mScrollBoxMap);
while (itScrollBox.hasNext()) {
itScrollBox.next();
itScrollBox.value()->deleteLater();
itScrollBox.remove();
}
QMutableMapIterator<QString, TTextBox*> 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<bool, QString> 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<bool, QString> 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<bool, QString> 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<bool, QString> 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<bool, QString> 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<bool, QString> 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<bool, QString> 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<Qt::CursorShape>(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<bool, QString> 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<bool, QString> 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<bool, QString> 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<bool, QString> 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<double>(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<bool, QString> TMainConsole::addWordToSet(const QString& word)
{
const QString errMsg = qsl("the word \"%1\" already seems to be in the user dictionary");
QPair<bool, QString> 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<bool, bool> 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<bool, QString> TMainConsole::removeWordFromSet(const QString& word)
{
const QString errMsg = qsl("the word \"%1\" does not seem to be in the user dictionary");
QPair<bool, QString> 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<bool, bool> 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<QString> TMainConsole::getWordSet() const
{
if (!mEnableUserDictionary) {
return QSet<QString>();
}
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<bool, QString> 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)};
}
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("<no GA> 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<Host>
// 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<Host>
// 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<QString> 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<int>(total) : 0);
mpPackageDownloadProgressDialog->setValue(static_cast<int>(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<QDialog*>(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<QDialog*>(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<QDialog> dialog = pDialog;
if (auto* pLabel = dialog->findChild<QLabel*>(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<TLabel*>(targetWidget)->mpVideoWidget;
} else if (widgetType == TMediaData::MediaWidgetWindow) {
myVideoWidget = qobject_cast<TConsole*>(targetWidget)->mpVideoWidget;
}
if (!myVideoWidget) {
myVideoWidget = new QVideoWidget();
myVideoWidget->setParent(targetWidget);
myVideoWidget->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
if (widgetType == TMediaData::MediaWidgetLabel) {
QObject::connect(qobject_cast<TLabel*>(targetWidget), &TLabel::resized, myVideoWidget, [targetWidget, myVideoWidget]() {
myVideoWidget->resize(targetWidget->size());
});
} else if (widgetType == TMediaData::MediaWidgetWindow) {
QObject::connect(qobject_cast<TConsole*>(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<QVideoWidget*>(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;
}