mudlet/src/TConsole.cpp
Vadim Peretokin 074c6ef08d
improve: "Copy as image" now works without selecting text first (#9738)
#### Brief overview of PR changes/additions
- "Copy as image" with nothing selected now copies the visible screen
(timestamps included) instead of doing nothing. With a selection it is
unchanged.
- Copy, Copy HTML and Search on ... genuinely need a selection, so
without one they are greyed out with a tooltip saying so rather than
silently doing nothing.
- Copy as image no longer wipes the clipboard when it runs out of its 3s
budget, crops the drawn lines instead of squashing the whole selection
to fit, and copies a run of blank lines rather than nothing.

#### Motivation for adding to Mudlet
Right-clicking the console and picking "Copy as image" left nothing on
the clipboard, with no hint that a selection was needed.

#### Other info (issues closed, discussion etc)
Fixes #9715

Also fixes two ways a selection could outlive the lines it covers:
clearing a console now clears its selection (`clearWindow()` then "Copy
as image" aborted on Qt's bounds assert), and a selection stranded by
the buffer hitting its size limit is followed down with its lines
instead of copying whatever took their place.

New `CopyAsImageTest` functional test, 15 cases. Verified on X11 with
`xclip -selection clipboard -t image/png -o`.

Assisted-by: Claude:claude-opus-5

**Test case:** Right-click the main console with nothing selected, pick
"Copy as image", and paste - you get a picture of the screen. Select
some text and repeat - you get just the selection.
2026-08-11 19:13:35 +02:00

3160 lines
121 KiB
C++

/***************************************************************************
* Copyright (C) 2008-2013 by Heiko Koehn - KoehnHeiko@googlemail.com *
* Copyright (C) 2014-2024, 2026 by Stephen Lyons *
* - slysven@virginmedia.com *
* Copyright (C) 2014 by Ahmed Charles - acharles@outlook.com *
* Copyright (C) 2016 by Ian Adkins - ieadkins@gmail.com *
* Copyright (C) 2021 by Vadim Peretokin - vperetokin@gmail.com *
* Copyright (C) 2022 by Thiago Jung Bauermann - bauermann@kolabnow.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 "ctelnet.h"
#include "Host.h"
#include "TCommandLine.h"
#include "THyperlinkCompactManager.h"
#include "TDebug.h"
#include "TDockWidget.h"
#include "TEvent.h"
#include "THyperlinkSelectionManager.h"
#include "THyperlinkVisibilityManager.h"
#include "TLabel.h"
#include "TMainConsole.h"
#include "TMap.h"
#include "TSplitter.h"
#include "TTextEdit.h"
#include "dlgMapper.h"
#include "mudlet.h"
#include <QAccessibleInterface>
#include <QAccessibleWidget>
#include <QFile>
#include <QHBoxLayout>
#include <QLabel>
#include <QLineEdit>
#include <QMessageBox>
#include <QMimeData>
#include <QPainter>
#include <QScrollBar>
#include <QSettings>
#include <QShortcut>
#include <QSplitter>
#include <QTextBoundaryFinder>
#include <QVideoWidget>
#include <chrono>
using namespace std::chrono_literals;
const QString TConsole::cmLuaLineVariable("line");
// A high-performance text widget with split screen ability for scrolling back
// Contains two TTextEdits, and is backed by a TBuffer
TConsole::TConsole(Host* pH, const QString& name, const ConsoleType type, QWidget* parent)
: QWidget(parent)
, mpHost(pH)
, mDisplayFontDetails(pH->fontsAntiAlias())
, buffer(pH, this)
, emergencyStop(new QToolButton)
, mConsoleName(name)
, mpBaseVFrame(new QWidget(this))
, mpTopToolBar(new QWidget(mpBaseVFrame))
, mpBaseHFrame(new QWidget(mpBaseVFrame))
, mpLeftToolBar(new QWidget(mpBaseHFrame))
, mpMainFrame(new QWidget(mpBaseHFrame))
, mpRightToolBar(new QWidget(mpBaseHFrame))
, mpMainDisplay(new QWidget(mpMainFrame))
, mpScrollBar(new QScrollBar)
, mpHScrollBar(new QScrollBar(Qt::Horizontal))
, mProfileName(mpHost ? mpHost->getName() : qsl("debug console"))
, mpBufferSearchBox(new QLineEdit)
, mpBufferSearchUp(new QToolButton)
, mpBufferSearchDown(new QToolButton)
, mControlCharacter(pH->getControlCharacterMode())
, mType(type)
{
mpHyperlinkCompactManager = std::make_unique<THyperlinkCompactManager>();
mpHyperlinkSelectionManager = std::make_unique<THyperlinkSelectionManager>(*this);
mpHyperlinkVisibilityManager = std::make_unique<THyperlinkVisibilityManager>(this);
initializeOSC8StyleFeature();
initializeOSC8MenuFeature();
initializeOSC8TooltipFeature();
initializeOSC8DisabledFeature();
initializeOSC8SpoilerFeature();
initializeOSC8SelectionFeature();
initializeOSC8VisibilityFeature();
initializeOSC8TitleFeature();
auto quitShortcut = new QShortcut(this);
quitShortcut->setKey(Qt::CTRL | Qt::Key_W);
quitShortcut->setContext(Qt::WidgetShortcut);
if (mType == CentralDebugConsole) {
// Probably will not show up as this is used inside a QMainWindow widget
// which has its own title and icon set.
setWindowTitle(tr("Debug Console"));
mWrapAt = 50;
mShowTimeStamps = true;
} else if (mType == MainConsole) {
mBorders = mpHost->borders();
mCommandBgColor = mpHost->mCommandBgColor;
mCommandFgColor = mpHost->mCommandFgColor;
}
QWidget::setFont(mDisplayFontDetails.makeFont());
setContentsMargins(0, 0, 0, 0);
setAttribute(Qt::WA_DeleteOnClose);
setAttribute(Qt::WA_OpaquePaintEvent, (mType == MainConsole || mType == CentralDebugConsole));
const QSizePolicy sizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
const QSizePolicy sizePolicy3(QSizePolicy::Expanding, QSizePolicy::Expanding);
const QSizePolicy sizePolicy2(QSizePolicy::Expanding, QSizePolicy::Fixed);
const QSizePolicy sizePolicy4(QSizePolicy::Fixed, QSizePolicy::Expanding);
const QSizePolicy sizePolicy5(QSizePolicy::Fixed, QSizePolicy::Fixed);
mpMainFrame->setContentsMargins(0, 0, 0, 0);
// the central debug console is a top-level window with no main console
// behind it, so it must paint its own background like the main console does
if (mType == MainConsole || mType == CentralDebugConsole) {
QPalette framePalette;
framePalette.setColor(QPalette::Text, QColor(Qt::black));
framePalette.setColor(QPalette::Highlight, QColor(55, 55, 255));
framePalette.setColor(QPalette::Window, QColor(0, 0, 0, 255));
mpMainFrame->setPalette(framePalette);
mpMainFrame->setAutoFillBackground(true);
} else {
QPalette transparentBgPalette;
transparentBgPalette.setColor(QPalette::Window, QColor(0, 0, 0, 0));
setPalette(transparentBgPalette);
mpMainFrame->setPalette(transparentBgPalette);
}
mpMainFrame->setObjectName(qsl("MainFrame"));
auto centralLayout = new QVBoxLayout;
setLayout(centralLayout);
auto baseVFrameLayout = new QVBoxLayout;
mpBaseVFrame->setLayout(baseVFrameLayout);
baseVFrameLayout->setContentsMargins(0, 0, 0, 0);
baseVFrameLayout->setSpacing(0);
centralLayout->addWidget(mpBaseVFrame);
auto baseHFrameLayout = new QHBoxLayout;
mpBaseHFrame->setLayout(baseHFrameLayout);
baseHFrameLayout->setContentsMargins(0, 0, 0, 0);
baseHFrameLayout->setSpacing(0);
layout()->setSpacing(0);
layout()->setContentsMargins(0, 0, 0, 0);
setContentsMargins(0, 0, 0, 0);
auto topBarLayout = new QHBoxLayout;
mpTopToolBar->setLayout(topBarLayout);
mpTopToolBar->setSizePolicy(QSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed));
mpTopToolBar->setContentsMargins(0, 0, 0, 0);
mpTopToolBar->setAutoFillBackground(true);
topBarLayout->setContentsMargins(0, 0, 0, 0);
topBarLayout->setSpacing(0);
auto leftBarLayout = new QVBoxLayout;
mpLeftToolBar->setLayout(leftBarLayout);
mpLeftToolBar->setSizePolicy(QSizePolicy(QSizePolicy::Fixed, QSizePolicy::Expanding));
mpLeftToolBar->setAutoFillBackground(true);
leftBarLayout->setContentsMargins(0, 0, 0, 0);
leftBarLayout->setSpacing(0);
mpLeftToolBar->setContentsMargins(0, 0, 0, 0);
auto rightBarLayout = new QVBoxLayout;
mpRightToolBar->setLayout(rightBarLayout);
mpRightToolBar->setSizePolicy(QSizePolicy(QSizePolicy::Fixed, QSizePolicy::Expanding));
mpRightToolBar->setAutoFillBackground(true);
rightBarLayout->setContentsMargins(0, 0, 0, 0);
rightBarLayout->setSpacing(0);
mpRightToolBar->setContentsMargins(0, 0, 0, 0);
mpBaseVFrame->setContentsMargins(0, 0, 0, 0);
baseVFrameLayout->setSpacing(0);
baseVFrameLayout->setContentsMargins(0, 0, 0, 0);
mpTopToolBar->setContentsMargins(0, 0, 0, 0);
baseVFrameLayout->addWidget(mpTopToolBar);
baseVFrameLayout->addWidget(mpBaseHFrame);
baseHFrameLayout->addWidget(mpLeftToolBar);
auto mpCorePane = new QWidget(mpBaseHFrame);
auto coreSpreadLayout = new QVBoxLayout;
mpCorePane->setLayout(coreSpreadLayout);
mpCorePane->setContentsMargins(0, 0, 0, 0);
coreSpreadLayout->setContentsMargins(0, 0, 0, 0);
coreSpreadLayout->setSpacing(0);
coreSpreadLayout->addWidget(mpMainFrame);
mpCorePane->setSizePolicy(sizePolicy);
baseHFrameLayout->addWidget(mpCorePane);
baseHFrameLayout->addWidget(mpRightToolBar);
mpTopToolBar->setContentsMargins(0, 0, 0, 0);
mpBaseHFrame->setAutoFillBackground(true);
baseHFrameLayout->setSpacing(0);
baseHFrameLayout->setContentsMargins(0, 0, 0, 0);
setContentsMargins(0, 0, 0, 0);
mpBaseHFrame->setContentsMargins(0, 0, 0, 0);
centralLayout->setSpacing(0);
centralLayout->setContentsMargins(0, 0, 0, 0);
mpMainDisplay->move(mBorders.left(), mBorders.top());
mpMainFrame->show();
mpMainDisplay->show();
mpMainFrame->setContentsMargins(0, 0, 0, 0);
mpMainDisplay->setContentsMargins(0, 0, 0, 0);
auto layout = new QVBoxLayout;
mpMainDisplay->setObjectName(qsl("MainDisplay"));
mpMainDisplay->setLayout(layout);
layout->setContentsMargins(0, 0, 0, 0);
layout->setSpacing(0);
if (mType == MainConsole) {
// Sits behind mpMainDisplay within mpMainFrame, which always spans the
// console's full pane regardless of mBorders - so this stays independent
// of setBorderSizes() without needing its own resize/border logic.
mpWindowBackground = new QWidget(mpMainFrame);
mpWindowBackground->setObjectName(qsl("WindowBackground"));
mpWindowBackground->setContentsMargins(0, 0, 0, 0);
mpWindowBackground->move(0, 0);
mpWindowBackground->resize(mpMainFrame->size());
mpWindowBackground->lower();
mpWindowBackground->show();
}
mpBaseVFrame->setSizePolicy(sizePolicy);
mpBaseHFrame->setSizePolicy(sizePolicy);
baseVFrameLayout->setContentsMargins(0, 0, 0, 0);
baseHFrameLayout->setContentsMargins(0, 0, 0, 0);
centralLayout->setContentsMargins(0, 0, 0, 0);
if (mType == MainConsole) {
mpCommandLine = new TCommandLine(pH, qsl("main"), TCommandLine::MainCommandLine, this, mpMainDisplay);
mpCommandLine->setContentsMargins(0, 0, 0, 0);
mpCommandLine->setSizePolicy(sizePolicy);
mpCommandLine->setFont(font());
// Setting the focusProxy cannot be done here because things have not
// been completed enough at this point - it has been defered to a
// zero-timer at the end of this constructor
// Connect user input trigger (command submission only, not typing)
connect(mpCommandLine, &TCommandLine::commandSubmitted, mpHyperlinkVisibilityManager.get(), &THyperlinkVisibilityManager::onUserInput);
// Connect GA/EOR prompt signal from telnet
connect(&(pH->mTelnet), &cTelnet::signal_promptReceived, mpHyperlinkVisibilityManager.get(), &THyperlinkVisibilityManager::onPromptReceived);
// Refresh display when hyperlink visibility changes
connect(mpHyperlinkVisibilityManager.get(), &THyperlinkVisibilityManager::visibilityChanged, this, [this]() {
if (mUpperPane) {
mUpperPane->forceUpdate();
}
if (mLowerPane) {
mLowerPane->forceUpdate();
}
});
}
layer = new QWidget(mpMainDisplay);
layer->setObjectName(qsl("layer"));
layer->setStyleSheet("QWidget#layer{background-color: rgba(0,0,0,0)}");
layer->setContentsMargins(0, 0, 0, 0);
layer->setSizePolicy(sizePolicy);
layer->setFocusPolicy(Qt::NoFocus);
auto vLayoutLayer = new QVBoxLayout;
auto layoutLayer = new QHBoxLayout;
layer->setLayout(vLayoutLayer);
layoutLayer->setContentsMargins(0, 0, 0, 0);
layoutLayer->setSpacing(0);
mpScrollBar->setFixedWidth(15);
mpHScrollBar->setFixedHeight(15);
splitter = new TSplitter(Qt::Vertical, layer);
splitter->setObjectName(qsl("splitter_%1_%2").arg(mProfileName, mConsoleName));
splitter->setContentsMargins(0, 0, 0, 0);
splitter->setSizePolicy(sizePolicy);
splitter->setHandleWidth(3);
splitter->setFocusPolicy(Qt::NoFocus);
//QSplitter covers the background if not set to transparent and a new AppStyleSheet is set for example by DarkTheme
auto styleSheet = qsl("QSplitter { background-color: rgba(0,0,0,0) }");
splitter->setStyleSheet(styleSheet);
mUpperPane = new TTextEdit(this, splitter, &buffer, mpHost, false);
mUpperPane->setObjectName(qsl("upperPane_%1_%2").arg(mProfileName, mConsoleName));
mUpperPane->setContentsMargins(0, 0, 0, 0);
mUpperPane->setSizePolicy(sizePolicy3);
mUpperPane->setAccessibleName(tr("main window"));
mUpperPane->setFont(font());
mLowerPane = new TTextEdit(this, splitter, &buffer, mpHost, true);
mLowerPane->setObjectName(qsl("lowerPane_%1_%2").arg(mProfileName, mConsoleName));
mLowerPane->setContentsMargins(0, 0, 0, 0);
mLowerPane->setSizePolicy(sizePolicy3);
mLowerPane->setFont(font());
if (mType == MainConsole) {
setFocusProxy(mpCommandLine);
mUpperPane->setFocusProxy(mpCommandLine);
mLowerPane->setFocusProxy(mpCommandLine);
} else if (mType & (UserWindow | SubConsole)) {
// These will need to be changed when the built in TCommandLine is
// enabled or an additional one is added to them:
setFocusProxy(mpHost->mpConsole->mpCommandLine);
mUpperPane->setFocusProxy(mpHost->mpConsole->mpCommandLine);
mLowerPane->setFocusProxy(mpHost->mpConsole->mpCommandLine);
}
splitter->addWidget(mUpperPane);
splitter->addWidget(mLowerPane);
splitter->setCollapsible(1, false);
splitter->setCollapsible(0, false);
splitter->setStretchFactor(0, 6);
splitter->setStretchFactor(1, 1);
layoutLayer->addWidget(splitter);
layoutLayer->addWidget(mpScrollBar);
layoutLayer->setContentsMargins(0, 0, 0, 0);
layoutLayer->setSpacing(1); // not closer, otherwise there could be performance problems when displaying
vLayoutLayer->addLayout(layoutLayer);
vLayoutLayer->addWidget(mpHScrollBar);
vLayoutLayer->setContentsMargins(0, 0, 0, 0);
vLayoutLayer->setSpacing(0);
layerCommandLine = new QWidget; //( mpMainFrame );//layer );
layerCommandLine->setContentsMargins(0, 0, 0, 0);
layerCommandLine->setSizePolicy(sizePolicy);
layerCommandLine->setFocusPolicy(Qt::NoFocus);
layerCommandLine->setMaximumHeight(31);
layerCommandLine->setMinimumHeight(31);
layerCommandLine->setMinimumWidth(300);
layoutLayer2 = new QHBoxLayout(layerCommandLine);
layoutLayer2->setContentsMargins(0, 0, 0, 0);
layoutLayer2->setSpacing(0);
mpButtonMainLayer = new QWidget;
mpButtonMainLayer->setObjectName(qsl("mpButtonMainLayer"));
mpButtonMainLayer->setSizePolicy(sizePolicy);
mpButtonMainLayer->setContentsMargins(0, 0, 0, 0);
auto layoutButtonMainLayer = new QVBoxLayout(mpButtonMainLayer);
layoutButtonMainLayer->setObjectName(qsl("layoutButtonMainLayer"));
layoutButtonMainLayer->setContentsMargins(0, 0, 0, 0);
layoutButtonMainLayer->setSpacing(0);
/*mpButtonMainLayer->setMinimumHeight(31);
mpButtonMainLayer->setMaximumHeight(31);*/
auto buttonLayer = new QWidget;
buttonLayer->setObjectName(qsl("buttonLayer"));
auto layoutButtonLayer = new QHBoxLayout(buttonLayer);
layoutButtonLayer->setObjectName(qsl("layoutButtonLayer"));
layoutButtonLayer->setContentsMargins(0, 0, 0, 0);
layoutButtonLayer->setSpacing(0);
auto buttonLayerSpacer = new QWidget(buttonLayer);
buttonLayerSpacer->setSizePolicy(sizePolicy4);
layoutButtonMainLayer->addWidget(buttonLayerSpacer);
layoutButtonMainLayer->addWidget(buttonLayer);
timeStampButton = new QToolButton;
timeStampButton->setCheckable(true);
timeStampButton->setMinimumSize(QSize(30, 30));
timeStampButton->setMaximumSize(QSize(30, 30));
timeStampButton->setSizePolicy(sizePolicy5);
timeStampButton->setFocusPolicy(Qt::NoFocus);
timeStampButton->setIcon(QIcon(qsl(":/icons/dialog-information.png")));
timeStampButton->setToolTip(utils::richText(tr("Toggle time stamps")));
// Using the QAbstractButton::clicked rather than QAbstractButton::toggled
// so that we can set the state of the button without getting the signal
// being raised:
connect(timeStampButton, &QAbstractButton::clicked, this, &TConsole::slot_toggleTimeStamps);
replayButton = new QToolButton;
replayButton->setCheckable(true);
replayButton->setMinimumSize(QSize(30, 30));
replayButton->setMaximumSize(QSize(30, 30));
replayButton->setSizePolicy(sizePolicy5);
replayButton->setFocusPolicy(Qt::NoFocus);
QIcon replayIcon;
replayIcon.addPixmap(QPixmap(qsl(":/icons/media-tape.png")), QIcon::Normal, QIcon::Off);
replayIcon.addPixmap(QPixmap(qsl(":/icons/media-tape-red-cross.png")), QIcon::Normal, QIcon::On);
replayButton->setIcon(replayIcon);
//: Button tooltip for the replay recording toggle button
replayButton->setToolTip(utils::richText(tr("Start recording of replay")));
connect(replayButton, &QAbstractButton::clicked, this, &TConsole::slot_toggleReplayRecording);
logButton = new QToolButton;
logButton->setMinimumSize(QSize(30, 30));
logButton->setMaximumSize(QSize(30, 30));
logButton->setCheckable(true);
logButton->setSizePolicy(sizePolicy5);
logButton->setFocusPolicy(Qt::NoFocus);
//: Button tooltip for the logging button
logButton->setToolTip(utils::richText(tr("Start logging game output to log file.")));
QIcon logIcon;
logIcon.addPixmap(QPixmap(qsl(":/icons/folder-downloads.png")), QIcon::Normal, QIcon::Off);
logIcon.addPixmap(QPixmap(qsl(":/icons/folder-downloads-red-cross.png")), QIcon::Normal, QIcon::On);
logButton->setIcon(logIcon);
connect(logButton, &QAbstractButton::clicked, this, &TConsole::slot_toggleLogging);
if (mType == MainConsole) {
mpLineEdit_networkLatency = new QLineEdit(this);
mpLineEdit_networkLatency->setReadOnly(true);
mpLineEdit_networkLatency->setSizePolicy(sizePolicy4);
mpLineEdit_networkLatency->setFocusPolicy(Qt::NoFocus);
//: Tooltip for N and S network latency indicators
mpLineEdit_networkLatency->setToolTip(utils::richText(tr("<i>N:</i> network latency in seconds (ping),<br>"
"<i>S:</i> system processing time (triggers).")));
mpLineEdit_networkLatency->setMaximumSize(120, 30);
mpLineEdit_networkLatency->setMinimumSize(120, 30);
mpLineEdit_networkLatency->setAutoFillBackground(true);
mpLineEdit_networkLatency->setContentsMargins(0, 0, 0, 0);
mpLineEdit_networkLatency->setAlignment(Qt::AlignHCenter | Qt::AlignVCenter);
int latencyFontPointSize = 21;
QFont latencyFont = QFont(qsl("Bitstream Vera Sans Mono"), latencyFontPointSize, QFont::Normal);
const int latencyFontSizeMargin = 10;
/*:
The first argument 'N' represents the 'N'etwork latency; the second 'S' the
'S'ystem (processing) time
*/
const QString dummyTextA = tr("N:%1 S:%2").arg(0.0, 0, 'f', 3).arg(0.0, 0, 'f', 3);
/*:
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...
*/
const QString dummyTextB = tr("<no GA> S:%1").arg(0.0, 0, 'f', 3);
do {
latencyFont.setPointSize(--latencyFontPointSize);
} while (latencyFontPointSize > 6
&& qMax(QFontMetrics(latencyFont).boundingRect(dummyTextA).width(), QFontMetrics(latencyFont).boundingRect(dummyTextB).width()) + latencyFontSizeMargin
> mpLineEdit_networkLatency->maximumWidth());
mpLineEdit_networkLatency->setFont(latencyFont);
mpLineEdit_networkLatency->setFrame(false);
}
emergencyStop->setMinimumSize(QSize(30, 30));
emergencyStop->setMaximumSize(QSize(30, 30));
QIcon emergencyIcon;
emergencyIcon.addPixmap(QPixmap(qsl(":/icons/edit-bomb.png")), QIcon::Normal, QIcon::Off);
emergencyIcon.addPixmap(QPixmap(qsl(":/icons/red-bomb.png")), QIcon::Normal, QIcon::On);
emergencyStop->setIcon(emergencyIcon);
emergencyStop->setSizePolicy(sizePolicy4);
emergencyStop->setFocusPolicy(Qt::NoFocus);
emergencyStop->setCheckable(true);
emergencyStop->setToolTip(utils::richText(tr("Emergency stop! Stop all scripts")));
connect(emergencyStop, &QAbstractButton::clicked, this, &TConsole::slot_stopAllItems);
mpBufferSearchBox->setClearButtonEnabled(true);
for (auto child : mpBufferSearchBox->children()) {
auto* pAction_clear(qobject_cast<QAction*>(child));
if (pAction_clear && pAction_clear->objectName() == QLatin1String("_q_qlineeditclearaction")) {
connect(pAction_clear, &QAction::triggered, this, &TConsole::slot_clearSearchResults, Qt::QueuedConnection);
break;
}
}
mpBufferSearchBox->setMinimumSize(QSize(100, 30));
mpBufferSearchBox->setSizePolicy(sizePolicy);
mpBufferSearchBox->setFont(font());
mpBufferSearchBox->setFocusPolicy(Qt::ClickFocus);
//: search bar placeholder text
mpBufferSearchBox->setPlaceholderText(tr("Search"));
QPalette commandLinePalette;
commandLinePalette.setColor(QPalette::Text, mpHost->mCommandLineFgColor);
commandLinePalette.setColor(QPalette::Highlight, QColor(0, 0, 192));
commandLinePalette.setColor(QPalette::HighlightedText, QColor(Qt::white));
commandLinePalette.setColor(QPalette::Base, mpHost->mCommandLineBgColor);
commandLinePalette.setColor(QPalette::Window, mpHost->mCommandLineBgColor);
mpBufferSearchBox->setToolTip(utils::richText(tr("Search buffer.")));
connect(mpBufferSearchBox, &QLineEdit::returnPressed, this, &TConsole::slot_searchBufferUp);
mpAction_searchOptions = new QAction(tr("Search Options"), this);
mpAction_searchOptions->setObjectName(qsl("mpAction_searchOptions"));
QMenu* pMenu_searchOptions = new QMenu(tr("Search Options"), this);
pMenu_searchOptions->setObjectName(qsl("pMenu_searchOptions"));
pMenu_searchOptions->setToolTipsVisible(true);
mpAction_searchCaseSensitive = new QAction(tr("Case sensitive"), this);
mpAction_searchCaseSensitive->setObjectName(qsl("mpAction_searchCaseSensitive"));
mpAction_searchCaseSensitive->setToolTip(utils::richText(tr("Match case precisely")));
mpAction_searchCaseSensitive->setCheckable(true);
pMenu_searchOptions->insertAction(nullptr, mpAction_searchCaseSensitive);
setSearchOptions(mSearchOptions);
connect(mpAction_searchCaseSensitive, &QAction::triggered, this, &TConsole::slot_toggleSearchCaseSensitivity);
mpAction_searchOptions->setMenu(pMenu_searchOptions);
mpBufferSearchBox->addAction(mpAction_searchOptions, QLineEdit::LeadingPosition);
mpBufferSearchUp->setMinimumSize(QSize(30, 30));
mpBufferSearchUp->setMaximumSize(QSize(30, 30));
mpBufferSearchUp->setSizePolicy(sizePolicy5);
mpBufferSearchUp->setToolTip(utils::richText(tr("Earlier search result.")));
mpBufferSearchUp->setFocusPolicy(Qt::NoFocus);
mpBufferSearchUp->setIcon(QIcon(qsl(":/icons/export.png")));
connect(mpBufferSearchUp, &QAbstractButton::clicked, this, &TConsole::slot_searchBufferUp);
mpBufferSearchDown->setMinimumSize(QSize(30, 30));
mpBufferSearchDown->setMaximumSize(QSize(30, 30));
mpBufferSearchDown->setSizePolicy(sizePolicy5);
mpBufferSearchDown->setFocusPolicy(Qt::NoFocus);
mpBufferSearchDown->setToolTip(utils::richText(tr("Later search result.")));
mpBufferSearchDown->setIcon(QIcon(qsl(":/icons/import.png")));
connect(mpBufferSearchDown, &QAbstractButton::clicked, this, &TConsole::slot_searchBufferDown);
if (mType == MainConsole) {
setF3SearchEnabled(mpHost->getF3SearchEnabled());
}
if (mpCommandLine) {
layoutLayer2->addWidget(mpCommandLine);
}
layoutLayer2->addWidget(mpButtonMainLayer);
layoutLayer2->setContentsMargins(0, 0, 0, 0);
layout->addWidget(layer);
layerCommandLine->setAutoFillBackground(true);
if (mType == MainConsole) {
// All console control buttons should only be on MainConsole
layoutButtonLayer->addWidget(mpBufferSearchBox);
layoutButtonLayer->addWidget(mpBufferSearchUp);
layoutButtonLayer->addWidget(mpBufferSearchDown);
layoutButtonLayer->addWidget(timeStampButton);
layoutButtonLayer->addWidget(replayButton);
layoutButtonLayer->addWidget(logButton);
layoutButtonLayer->addWidget(emergencyStop);
layoutButtonLayer->addWidget(mpLineEdit_networkLatency);
commandSplitter = new QSplitter(Qt::Horizontal, this);
commandSplitter->setFocusPolicy(Qt::NoFocus);
connect(commandSplitter, &QSplitter::splitterMoved, this, &TConsole::slot_saveCommandSearchSettings);
commandSplitter->addWidget(layerCommandLine);
commandSplitter->addWidget(mpButtonMainLayer);
commandSplitter->setStretchFactor(0, 3); // command line
commandSplitter->setStretchFactor(1, 1); // search layer
commandSplitter->setCollapsible(0, false); // command line cannot collapse
commandSplitter->setCollapsible(1, false); // search layer cannot collapse
centralLayout->addWidget(commandSplitter);
restoreCommandSearchSettings();
} else {
centralLayout->addWidget(layerCommandLine);
}
QList<int> sizeList;
sizeList << 6 << 2;
splitter->setSizes(sizeList);
mUpperPane->show();
mLowerPane->hide();
connect(mpScrollBar, &QAbstractSlider::valueChanged, mUpperPane, &TTextEdit::slot_scrollBarMoved);
connect(mpHScrollBar, &QAbstractSlider::valueChanged, mUpperPane, &TTextEdit::slot_hScrollBarMoved);
mpHScrollBar->hide();
//enable horizontal scrollbar in ErrorConsole
if (mType == ErrorConsole) {
mHScrollBarEnabled = true;
}
// a Buffer is never displayed and the three types below start with their
// scroll bar hidden, so only the main and debug consoles begin with one
mScrollBarEnabled = !(mType & (ErrorConsole | SubConsole | UserWindow | Buffer));
if (mType & (ErrorConsole | SubConsole | UserWindow)) {
mpScrollBar->hide();
mLowerPane->hide();
layerCommandLine->hide();
mpMainFrame->move(0, 0);
mpMainDisplay->move(0, 0);
}
if (mType & CentralDebugConsole) {
layerCommandLine->hide();
}
mpBaseVFrame->setContentsMargins(0, 0, 0, 0);
mpBaseHFrame->setContentsMargins(0, 0, 0, 0);
mpBaseVFrame->layout()->setSpacing(0);
mpBaseHFrame->layout()->setSpacing(0);
buttonLayerSpacer->setMinimumHeight(0);
buttonLayerSpacer->setMinimumWidth(100);
buttonLayer->setMaximumHeight(31);
buttonLayer->setMinimumWidth(400);
mpButtonMainLayer->setMinimumWidth(400);
mpButtonMainLayer->setAutoFillBackground(true);
mpButtonMainLayer->setPalette(commandLinePalette);
buttonLayer->setAutoFillBackground(true);
changeColors();
// error and debug consoles inherit font of the main console
if (mType & (ErrorConsole | CentralDebugConsole)) {
// They always use "Control Pictures" to show control characters:
mControlCharacter = ControlCharacterMode::Picture;
refreshView();
} else if (mpHost) {
connect(mpHost, &Host::signal_controlCharacterHandlingChanged, this, &TConsole::slot_changeControlCharacterHandling);
}
if (mType & (MainConsole | UserWindow)) {
setAcceptDrops(true);
setMouseTracking(true);
}
if (mType & MainConsole) {
mpButtonMainLayer->setVisible(!mpHost->getCompactInputLine());
mpCommandLine->adjustHeight();
}
connect(mudlet::self(), &mudlet::signal_adjustAccessibleNames, this, &TConsole::slot_adjustAccessibleNames);
slot_adjustAccessibleNames();
// Need to delay doing this because it uses elements that may not have
// been constructed yet:
if (mType == MainConsole) {
QTimer::singleShot(0ms, this, [this]() {
setProxyForFocus(mpCommandLine);
});
}
}
TConsole::~TConsole()
{
#if defined(DEBUG_CODEPOINT_PROBLEMS)
if (mType & ~CentralDebugConsole) {
// Codepoint issues reporting is not enabled for the CDC:
mUpperPane->reportCodepointErrors();
}
#endif
}
Host* TConsole::getHost()
{
return mpHost;
}
void TConsole::resizeConsole()
{
const QSize size = QSize(width(), height());
QResizeEvent event(size, size);
QApplication::sendEvent(this, &event);
}
void TConsole::raiseMudletSysWindowResizeEvent(const int overallWidth, const int overallHeight)
{
if (mpHost.isNull()) {
return;
}
TEvent mudletEvent{};
mudletEvent.mArgumentList.append(QLatin1String("sysWindowResizeEvent"));
mudletEvent.mArgumentList.append(QString::number(overallWidth - mBorders.left() - mBorders.right()));
mudletEvent.mArgumentList.append(QString::number(overallHeight - mBorders.top() - mBorders.bottom() - mpCommandLine->height()));
mudletEvent.mArgumentList.append(mConsoleName);
mudletEvent.mArgumentTypeList.append(ARGUMENT_TYPE_STRING);
mudletEvent.mArgumentTypeList.append(ARGUMENT_TYPE_NUMBER);
mudletEvent.mArgumentTypeList.append(ARGUMENT_TYPE_NUMBER);
mudletEvent.mArgumentTypeList.append(ARGUMENT_TYPE_STRING);
mpHost->raiseEvent(mudletEvent);
}
void TConsole::resizeEvent(QResizeEvent* event)
{
if (mType & MainConsole) {
mBorders = mpHost->borders();
}
int x = event->size().width();
int y = event->size().height();
if (mType == MainConsole && !x) {
// When multi-view is NOT active but more than one profile is loaded
// switching between tabs causes the deselected profile to resize its
// main console to a width of zero - but that is not useful from a NAWS
// or event handling system point of view - so abort doing anything
// with the event:
return;
}
// prevents the command line from being hidden
if (layoutLayer2) {
layoutLayer2->invalidate();
layoutLayer2->activate();
}
// Move before resizing: resizing first could leave the display overrunning
// its parent frame, clipping it and making TTextEdit::updateScreenView()
// undercount the visible rows:
mpMainDisplay->move(mBorders.left(), mBorders.top());
if (mType & (MainConsole | SubConsole | UserWindow) && mpCommandLine && !mpCommandLine->isHidden()) {
mpMainFrame->resize(x, y);
mpBaseVFrame->resize(x, y);
mpBaseHFrame->resize(x, y);
x -= (mpLeftToolBar->width() + mpRightToolBar->width());
y -= mpTopToolBar->height();
// The mBorders components will be all zeros for all but the MainConsole:
mpMainDisplay->resize(x - mBorders.left() - mBorders.right(), y - mBorders.top() - mBorders.bottom() - mpCommandLine->height());
} else {
mpMainFrame->resize(x, y);
mpMainDisplay->resize(x, y);
}
if (mpWindowBackground) {
mpWindowBackground->resize(mpMainFrame->size());
if (mWindowBgImageMode == 5) {
updateWindowBackgroundCoverPixmap();
}
}
if (mType & (CentralDebugConsole | ErrorConsole)) {
layerCommandLine->hide();
} else if (mType & ~(SubConsole | UserWindow)) {
// does nothing for SubConsole or UserWindows
layerCommandLine->move(0, mpBaseVFrame->height() - layerCommandLine->height());
}
// MXP frames are positioned by hand against the space the borders leave, so
// they have to be moved whenever the window or those borders change
if ((mType & MainConsole) && !mpHost.isNull()) {
mpHost->mMxpFrameManager.scheduleRelayout();
}
// Sync Host dimensions on resize so wraps and NAWS reflect the current pane width.
if ((mType & MainConsole) && !mpHost.isNull() && mUpperPane && !mUpperPane->visibleRegion().isEmpty()) {
const int paneWidthPx = mUpperPane->visibleRegion().boundingRect().width();
auto syncHost = [paneWidthPx](Host* host, const QWidget* paneForFont) {
if (!host || !paneForFont) {
return;
}
const int fontWidth = QFontMetrics(paneForFont->font()).averageCharWidth();
if (fontWidth <= 0) {
return;
}
const int cols = qMax(40, paneWidthPx / fontWidth);
if (cols > 0 && cols != host->mScreenWidth) {
host->setScreenDimensions(cols, host->mScreenHeight);
QTimer::singleShot(0ms, host, &Host::updateDisplayDimensions);
}
};
syncHost(mpHost.data(), mUpperPane);
// Detached profiles have their own pixel width; only propagate from a main-window console.
mudlet* const app = mudlet::self();
const bool inMainWindow = app && !app->getDetachedWindows().contains(mpHost->getName());
if (inMainWindow) {
for (const auto& otherHostPtr : app->getHostManager()) {
Host* otherHost = otherHostPtr.data();
if (!otherHost || otherHost == mpHost.data()) {
continue;
}
// Skip detached profiles: different container, different width.
if (app->getDetachedWindows().contains(otherHost->getName())) {
continue;
}
if (!otherHost->mpConsole || !otherHost->mpConsole->mUpperPane) {
continue;
}
// Visible siblings (multi-view) handle their own resizeEvent.
if (!otherHost->mpConsole->mUpperPane->visibleRegion().isEmpty()) {
continue;
}
syncHost(otherHost, otherHost->mpConsole->mUpperPane);
}
}
}
emit resized(event);
QWidget::resizeEvent(event);
if (mType & MainConsole) {
// don't call event in lua if size didn't change
const bool preventLuaEvent = (getMainWindowSize() == mOldSize);
mOldSize = getMainWindowSize();
if (preventLuaEvent) {
return;
}
if (!mpHost.isNull()) {
TLuaInterpreter* pLua = mpHost->getLuaInterpreter();
const QString func = "handleWindowResizeEvent";
const QString n = "WindowResizeEvent";
pLua->call(func, n);
raiseMudletSysWindowResizeEvent(x, y);
}
}
//create the sysUserWindowResize Event for automatic resizing with Geyser
if (mType & (UserWindow) && !mpHost.isNull()) {
TLuaInterpreter* pLua = mpHost->getLuaInterpreter();
const QString func = "handleWindowResizeEvent";
const QString n = "WindowResizeEvent";
pLua->call(func, n);
TEvent mudletEvent{};
mudletEvent.mArgumentList.append(QLatin1String("sysUserWindowResizeEvent"));
mudletEvent.mArgumentList.append(QString::number(x));
mudletEvent.mArgumentList.append(QString::number(y));
mudletEvent.mArgumentList.append(mConsoleName);
mudletEvent.mArgumentTypeList.append(ARGUMENT_TYPE_STRING);
mudletEvent.mArgumentTypeList.append(ARGUMENT_TYPE_NUMBER);
mudletEvent.mArgumentTypeList.append(ARGUMENT_TYPE_NUMBER);
mudletEvent.mArgumentTypeList.append(ARGUMENT_TYPE_STRING);
mpHost->raiseEvent(mudletEvent);
}
}
void TConsole::refresh()
{
if (mType == MainConsole) {
mBorders = mpHost->borders();
}
int x = width();
int y = height();
mpBaseVFrame->resize(x, y);
mpBaseHFrame->resize(x, y);
x = mpBaseVFrame->width();
if (!mpLeftToolBar->isHidden()) {
x -= mpLeftToolBar->width();
}
if (!mpRightToolBar->isHidden()) {
x -= mpRightToolBar->width();
}
y = mpBaseVFrame->height();
if (!mpTopToolBar->isHidden()) {
y -= mpTopToolBar->height();
}
// Move before resizing, see comment in resizeEvent():
mpMainDisplay->move(mBorders.left(), mBorders.top());
mpMainDisplay->resize(x - mBorders.left() - mBorders.right(), y - mBorders.top() - mBorders.bottom() - mpCommandLine->height());
if (!mpCommandLine.isNull()) {
mpCommandLine->adjustHeight();
}
x = width();
y = height();
const QSize s = QSize(x, y);
QResizeEvent event(s, s);
QApplication::sendEvent(this, &event);
}
void TConsole::clear()
{
mUpperPane->resetHScrollbar();
// before the buffer goes, or the selection is left pointing at lines that
// no longer exist and the copy actions work on out of range indices
clearSelection();
buffer.clear();
clearSplit();
mUpperPane->update();
mLowerPane->update();
}
void TConsole::clearSelection() const
{
mLowerPane->unHighlight();
mUpperPane->unHighlight();
mLowerPane->mSelectedRegion = QRegion(0, 0, 0, 0);
mUpperPane->mSelectedRegion = QRegion(0, 0, 0, 0);
mUpperPane->forceUpdate();
mLowerPane->forceUpdate();
}
void TConsole::closeEvent(QCloseEvent* event)
{
if (mType == CentralDebugConsole) {
if (mudlet::self()->isGoingDown() || mpHost->isClosingDown()) {
event->accept();
return;
}
hide();
mudlet::smpDebugArea->setVisible(false);
mudlet::smDebugMode = false;
mudlet::self()->refreshTabBar();
event->ignore();
return;
}
if (mType & (SubConsole | Buffer)) {
if (mudlet::self()->isGoingDown() || mpHost->isClosingDown()) {
auto pC = mpHost->mpConsole->mSubConsoleMap.take(mConsoleName);
if (pC) {
// As it happens pC will be identical to 'this' it is just that
// we will have removed it from the main TConsole's
// mSubConsoleMap:
mUpperPane->close();
mLowerPane->close();
}
event->accept();
return;
}
hide();
event->ignore();
return;
}
if (mType == UserWindow) {
if (mudlet::self()->isGoingDown() || mpHost->isClosingDown()) {
auto pC = mpHost->mpConsole->mSubConsoleMap.take(mConsoleName);
auto pD = mpHost->mpConsole->mDockWidgetMap.take(mConsoleName);
if (pC) {
// As it happens pC will be identical to 'this' it is just that
// we will have removed it from the main TConsole's
// mSubConsoleMap:
mUpperPane->close();
mLowerPane->close();
}
if (pD) {
pD->setAttribute(Qt::WA_DeleteOnClose);
pD->deleteLater();
} else {
qDebug() << "TConsole::closeEvent(QCloseEvent*) INFO - closing a UserWindow but the TDockWidget pointer was not found to be removed...";
}
// This will also cause the QWidget to be automatically hidden:
event->accept();
return;
}
hide();
event->ignore();
return;
}
if (mType == MainConsole) {
// The event should have been handled by the override in the TMainConsole
Q_ASSERT_X(false, "TConsole::closeEvent()", "Close event not handled by TMainConsole override.");
}
}
int TConsole::getButtonState()
{
return mButtonState;
}
// Converted into a wrapper around a separate toggleLogging() method so that
// calls to turn logging on/off via the toolbar button - which go via this
// wrapper - generate messages on the console. Requests to control logging from
// the Lua interpreter call the wrapped method directly and messages are
// generated for Lua user control by the Lua subsystem.
void TConsole::slot_toggleLogging()
{
if (mType & (CentralDebugConsole | ErrorConsole | SubConsole | UserWindow)) {
return;
// We don't support logging anything other than main console (at present?)
}
mpHost->mpConsole->toggleLogging(true);
}
// FIXME: This needs to move to the TMainConsole class but the button handling
// code is currently defined but not used for all TConsole instances - and some
// of them might be useful to have on the other ones...
void TConsole::slot_toggleReplayRecording()
{
if (mType & CentralDebugConsole) {
return;
}
mRecordReplay = !mRecordReplay;
if (mRecordReplay) {
const QString directoryLogFile = mudlet::getMudletPath(enums::profileReplayAndLogFilesPath, mProfileName);
const QString mLogFileName = qsl("%1/%2.dat").arg(directoryLogFile, QDateTime::currentDateTime().toString(qsl("yyyy-MM-dd#HH-mm-ss")));
const QDir dirLogFile;
if (!dirLogFile.exists(directoryLogFile)) {
dirLogFile.mkpath(directoryLogFile);
}
mReplayFile.setFileName(mLogFileName);
if (!mReplayFile.open(QIODevice::WriteOnly)) {
qWarning() << "TConsole: failed to open replay file for writing:" << mReplayFile.errorString();
mRecordReplay = false;
//: Informational message displayed when replay recording file could not be opened
printSystemMessage(tr("Failed to open replay recording file for writing.") % QChar::LineFeed);
return;
}
if (mudlet::scmRunTimeQtVersion >= QVersionNumber(5, 13, 0)) {
mReplayStream.setVersion(mudlet::scmQDataStreamFormat_5_12);
}
mReplayStream.setDevice(&mReplayFile);
mpHost->mTelnet.recordReplay();
printSystemMessage(tr("Replay recording has started. File: %1").arg(mReplayFile.fileName()) % QChar::LineFeed);
//: Button tooltip for the replay recording toggle button
replayButton->setToolTip(utils::richText(tr("Stop recording of replay")));
} else {
if (!mReplayFile.commit()) {
qDebug() << "TConsole::slot_toggleReplayRecording: error saving replay: " << mReplayFile.errorString();
//: Informational message displayed when replay recording is stopped but could not be saved
printSystemMessage(tr("Replay recording has been stopped, but couldn't be saved.") % QChar::LineFeed);
} else {
//: Informational message displayed when replay recording is stopped
printSystemMessage(tr("Replay recording has been stopped. File: %1").arg(mReplayFile.fileName()) % QChar::LineFeed);
}
//: Button tooltip for the replay recording toggle button
replayButton->setToolTip(utils::richText(tr("Start recording of replay")));
}
}
QString getColorCode(QColor color)
{
return qsl("%1,%2,%3,%4").arg(color.red()).arg(color.green()).arg(color.blue()).arg(color.alpha());
}
// Builds the QSS fragment shared by setConsoleBackgroundImage()/setWindowBackgroundImage()
// for modes 1-4 ('border'/'center'/'tile'/'style'). Mode 5 ('cover') is not stylesheet-based
// (Qt's border-image/background-image have no scale-to-fill option) and is handled separately.
static QString buildBackgroundImageStyleSheet(const QString& objectName, const QColor& bgColor, int mode, const QString& imgPath)
{
if (mode == 1) {
return qsl("QWidget#%1{background-color: rgba(%2); border-image: url(%3);}").arg(objectName, getColorCode(bgColor), imgPath);
} else if (mode == 2) {
return qsl("QWidget#%1{background-color: rgba(%2); background-image: url(%3); background-repeat: no-repeat; background-position: center; background-origin: margin;}")
.arg(objectName, getColorCode(bgColor), imgPath);
} else if (mode == 3) {
return qsl("QWidget#%1{background-color: rgba(%2); background-image: url(%3);}").arg(objectName, getColorCode(bgColor), imgPath);
} else if (mode == 4) {
return qsl("QWidget#%1{background-color: rgba(%2); %3}").arg(objectName, getColorCode(bgColor), imgPath);
}
return QString();
}
void TConsole::changeColors()
{
if (mType == CentralDebugConsole) {
// Font is managed via QWidget::font() inheritance, no font operations needed here
} else if (mType & (ErrorConsole | SubConsole | UserWindow | Buffer)) {
if (!mBgImageMode) {
auto styleSheet = qsl("QWidget#MainDisplay{background-color: rgba(%1);}").arg(getColorCode(mBgColor));
mpMainDisplay->setStyleSheet(styleSheet);
QPalette transparentBgPalette;
transparentBgPalette.setColor(QPalette::Window, QColor(0, 0, 0, 0));
mpBaseVFrame->setPalette(transparentBgPalette);
mpBaseHFrame->setPalette(transparentBgPalette);
mpMainFrame->setPalette(transparentBgPalette);
mpMainDisplay->setPalette(transparentBgPalette);
setPalette(transparentBgPalette);
} else {
setConsoleBackgroundImage(mBgImagePath, mBgImageMode);
}
} else if (mType == MainConsole) {
if (mpCommandLine) {
auto styleSheet = mpCommandLine->styleSheet();
mpCommandLine->setStyleSheet(QString());
// CHECK: This seems to be a, possibly iffy, attempt to combine a
// QPalette with a style-sheet - though the Qt Documentation does
// seem to say one should not mix QPalettes with styles/stylesheets!
QPalette commandLinePalette;
commandLinePalette.setColor(QPalette::Text, mpHost->mCommandLineFgColor);
commandLinePalette.setColor(QPalette::Highlight, QColor(0, 0, 192));
commandLinePalette.setColor(QPalette::HighlightedText, QColor(Qt::white));
commandLinePalette.setColor(QPalette::Base, mpHost->mCommandLineBgColor);
commandLinePalette.setColor(QPalette::Window, mpHost->mCommandLineBgColor);
mpCommandLine->setPalette(commandLinePalette);
mpButtonMainLayer->setPalette(commandLinePalette);
mpCommandLine->mRegularPalette = commandLinePalette;
mpCommandLine->setStyleSheet(styleSheet);
}
if (!mBgImageMode) {
auto styleSheet = qsl("QWidget#MainDisplay{background-color: rgba(%1);}").arg(getColorCode(mpHost->mBgColor));
mpMainDisplay->setStyleSheet(styleSheet);
} else {
setConsoleBackgroundImage(mBgImagePath, mBgImageMode);
}
mBgColor = mpHost->mBgColor;
mFgColor = mpHost->mFgColor;
mCommandFgColor = mpHost->mCommandFgColor;
mCommandBgColor = mpHost->mCommandBgColor;
mFormatCurrent.setColors(mpHost->mFgColor, mpHost->mBgColor);
updateMainFrameTransparency();
} else {
Q_ASSERT_X(false, "TConsole::changeColors()", "invalid TConsole type detected");
}
buffer.updateColors();
if (mType & (MainConsole | Buffer)) {
buffer.mWrapAt = mpHost->mWrapAt;
buffer.mWrapIndent = mpHost->mWrapIndentCount;
buffer.mWrapHangingIndent = mpHost->mWrapHangingIndentCount;
}
}
void TConsole::setConsoleBgColor(int r, int g, int b, int a)
{
mBgColor = QColor(r, g, b, a);
mUpperPane->setConsoleBgColor(r, g, b, a);
mLowerPane->setConsoleBgColor(r, g, b, a);
changeColors();
}
// Not used:
//void TConsole::setConsoleFgColor(int r, int g, int b)
//{
// mFgColor = QColor(r, g, b);
// mUpperPane->setConsoleFgColor(r, g, b);
// mLowerPane->setConsoleFgColor(r, g, b);
// changeColors();
//}
/*std::string TConsole::getCurrentTime()
{
time_t t;
time(&t);
tm lt;
ostringstream s;
s.str("");
struct timeval tv;
struct timezone tz;
gettimeofday(&tv, &tz);
localtime_r( &t, &lt );
s << "["<<lt.tm_hour<<":"<<lt.tm_min<<":"<<lt.tm_sec<<":"<<tv.tv_usec<<"]";
string time = s.str();
return time;
} */
/* ANSI color codes: sequence = "ESCAPE + [ code_1; ... ; code_n m"
-----------------------------------------
0 reset
1 intensity bold on
2 intensity faint
3 italics on
4 underline on
5 blink slow
6 blink fast
7 inverse on
9 strikethrough
22 intensity normal (not bold, not faint)
23 italics off
24 underline off
27 inverse off
29 strikethrough off
30 fg black
31 fg red
32 fg green
33 fg yellow
34 fg blue
35 fg magenta
36 fg cyan
37 fg white
39 bg default white
40 bg black
41 bg red
42 bg green
43 bg yellow
44 bg blue
45 bg magenta
46 bg cyan
47 bg white
49 bg black */
void TConsole::scrollDown(int lines)
{
if ((mType & (UserWindow | SubConsole)) && !mScrollingEnabled) {
return;
}
mUpperPane->scrollDown(lines);
if (!mUpperPane->mIsTailMode && (mUpperPane->imageTopLine() + mUpperPane->getScreenHeight() >= buffer.lineBuffer.size() - mLowerPane->getRowCount())) {
mUpperPane->scrollDown(mLowerPane->getRowCount() + 100); // Gets to the bottom
mUpperPane->scrollDown(100); // needs another scroll to force mIsTailMode
}
if (mUpperPane->mIsTailMode) {
mLowerPane->mCursorY = buffer.lineBuffer.size();
mLowerPane->hide();
mUpperPane->mCursorY = buffer.lineBuffer.size();
mUpperPane->updateScreenView();
mUpperPane->forceUpdate();
}
slot_adjustAccessibleNames();
}
void TConsole::scrollUp(int lines)
{
if ((mType & (UserWindow | SubConsole)) && !mScrollingEnabled) {
return;
}
const bool lowerAppears = mLowerPane->isHidden();
mLowerPane->mCursorY = buffer.size();
mLowerPane->show();
mLowerPane->updateScreenView();
mLowerPane->forceUpdate();
if (lowerAppears) {
QTimer::singleShot(0ms, this, [this, lines]() {
mUpperPane->scrollUp(mLowerPane->getRowCount() + lines);
});
if (mudlet::self()->showSplitscreenTutorial()) {
#if defined(Q_OS_MACOS)
const QString infoMsg = tr("[ INFO ] - Split-screen scrollback activated. Press <⌘>+<ENTER> to cancel.");
#else
const QString infoMsg = tr("[ INFO ] - Split-screen scrollback activated. Press <CTRL>+<ENTER> to cancel.");
#endif
mpHost->postMessage(infoMsg);
mudlet::self()->showedSplitscreenTutorial();
}
} else {
mUpperPane->scrollUp(lines);
}
slot_adjustAccessibleNames();
}
void TConsole::deselect()
{
P_begin = QPoint();
P_end = QPoint();
}
void TConsole::showEvent(QShowEvent* event)
{
if (mType & (MainConsole | Buffer)) {
if (mpHost) {
mAlertOnNewData = false;
}
}
QWidget::showEvent(event); //FIXME-refac: might cause problems
}
void TConsole::hideEvent(QHideEvent* event)
{
if (mType & (MainConsole | Buffer)) {
if (mpHost) {
if (mudlet::self()->mWindowMinimized) {
if (mpHost->mAlertOnNewData) {
mAlertOnNewData = true;
}
}
}
}
QWidget::hideEvent(event); //FIXME-refac: might cause problems
}
void TConsole::reset()
{
deselect();
mFormatCurrent.setColors(mFgColor, mBgColor);
mFormatCurrent.setAllDisplayAttributes(TChar::None);
}
void TConsole::insertLink(const QString& text, QStringList& func, QStringList& hint, QPoint P, bool customFormat, QVector<int> luaReference)
{
const int x = P.x();
const int y = P.y();
QPoint P2 = P;
P2.setX(x + text.size());
const TChar standardLinkFormat = TChar(Qt::blue, mBgColor, TChar::Underline);
if (mTriggerEngineMode) {
mpHost->getLuaInterpreter()->adjustCaptureGroups(x, text.size());
if (customFormat) {
buffer.insertInLine(P, text, mFormatCurrent);
} else {
buffer.insertInLine(P, text, standardLinkFormat);
}
buffer.applyLink(P, P2, func, hint, luaReference);
if (y < mEngineCursor) {
mUpperPane->needUpdate(mUserCursor.y(), mUserCursor.y() + 1);
}
return;
}
if ((buffer.buffer.empty()) || mUserCursor == buffer.getEndPos()) {
if (customFormat) {
buffer.addLink(mTriggerEngineMode, text, func, hint, mFormatCurrent, luaReference);
} else {
buffer.addLink(mTriggerEngineMode, text, func, hint, standardLinkFormat, luaReference);
}
mUpperPane->showNewLines();
mLowerPane->showNewLines();
} else {
if (customFormat) {
buffer.insertInLine(mUserCursor, text, mFormatCurrent);
} else {
buffer.insertInLine(mUserCursor, text, standardLinkFormat);
}
buffer.applyLink(P, P2, func, hint, luaReference);
if (text.indexOf("\n") != -1) {
const int y_tmp = mUserCursor.y();
const int down = buffer.wrapLine(mUserCursor.y(), mpHost->mScreenWidth, mpHost->mWrapIndentCount, mpHost->mWrapHangingIndentCount);
mUpperPane->needUpdate(y_tmp, y_tmp + down + 1);
const int y_neu = y_tmp + down;
const int x_adjust = text.lastIndexOf("\n");
int x_neu = 0;
if (x_adjust != -1) {
x_neu = text.size() - x_adjust - 1 > 0 ? text.size() - x_adjust - 1 : 0;
}
moveCursor(x_neu, y_neu);
} else {
mUpperPane->needUpdate(mUserCursor.y(), mUserCursor.y() + 1);
moveCursor(mUserCursor.x() + text.size(), mUserCursor.y());
}
}
}
void TConsole::insertText(const QString& text, QPoint P)
{
const int x = P.x();
const int y = P.y();
if (mTriggerEngineMode) {
mpHost->getLuaInterpreter()->adjustCaptureGroups(x, text.size());
buffer.insertInLine(P, text, mFormatCurrent);
if (y < mEngineCursor) {
mUpperPane->needUpdate(mUserCursor.y(), mUserCursor.y() + 1);
}
} else {
if ((buffer.buffer.empty()) || mUserCursor == buffer.getEndPos()) {
buffer.append(text, 0, text.size(), mFormatCurrent);
mUpperPane->showNewLines();
mLowerPane->showNewLines();
} else {
buffer.insertInLine(mUserCursor, text, mFormatCurrent);
const int y_tmp = mUserCursor.y();
if (text.indexOf(QChar::LineFeed) != -1) {
const int down = buffer.wrapLine(y_tmp, mpHost->mScreenWidth, mpHost->mWrapIndentCount, mpHost->mWrapHangingIndentCount);
mUpperPane->needUpdate(y_tmp, y_tmp + down + 1);
} else {
mUpperPane->needUpdate(y_tmp, y_tmp + 1);
}
}
}
}
void TConsole::replace(const QString& text)
{
const int x = P_begin.x();
const int o = P_end.x() - P_begin.x();
const int r = text.size();
if (mTriggerEngineMode) {
if (hasSelection()) {
if (r < o) {
const int a = -1 * (o - r);
mpHost->getLuaInterpreter()->adjustCaptureGroups(x, a);
}
if (r > o) {
const int a = r - o;
mpHost->getLuaInterpreter()->adjustCaptureGroups(x, a);
}
} else {
mpHost->getLuaInterpreter()->adjustCaptureGroups(x, r);
}
}
buffer.replaceInLine(P_begin, P_end, text, mFormatCurrent);
}
void TConsole::skipLine()
{
deleteLine(mUserCursor.y());
}
bool TConsole::deleteLine(int y)
{
return buffer.deleteLine(y);
}
bool TConsole::hasSelection()
{
if (P_begin != P_end) {
return true;
}
return false;
}
void TConsole::insertText(const QString& msg)
{
insertText(msg, mUserCursor);
}
void TConsole::insertLink(const QString& text, QStringList& func, QStringList& hint, bool customFormat, QVector<int> luaReference)
{
insertLink(text, func, hint, mUserCursor, customFormat, luaReference);
}
void TConsole::insertHTML(const QString& text)
{
insertText(text);
}
int TConsole::getLineNumber()
{
return mUserCursor.y();
}
int TConsole::getColumnNumber()
{
return mUserCursor.x();
}
int TConsole::getWrapAt()
{
return buffer.mWrapAt;
}
int TConsole::getLineCount()
{
return buffer.getLastLineNumber();
}
QStringList TConsole::getLines(int from, int to)
{
QStringList ret;
const int delta = abs(from - to);
for (int i = 0; i < delta; i++) {
ret << buffer.line(from + i);
}
return ret;
}
void TConsole::selectCurrentLine()
{
selectSection(0, buffer.line(mUserCursor.y()).size());
}
std::list<int> TConsole::getFgColor()
{
std::list<int> result;
const int x = P_begin.x();
const int y = P_begin.y();
if (y < 0) {
return result;
}
if (x < 0) {
return result;
}
if (y >= static_cast<int>(buffer.buffer.size())) {
return result;
}
auto line = buffer.buffer.at(y);
const int len = static_cast<int>(line.size());
if (len - 1 >= x) {
const QColor color(line.at(x).foreground());
result.push_back(color.red());
result.push_back(color.green());
result.push_back(color.blue());
}
return result;
}
std::list<int> TConsole::getBgColor()
{
std::list<int> result;
const int x = P_begin.x();
const int y = P_begin.y();
if (y < 0) {
return result;
}
if (x < 0) {
return result;
}
if (y >= static_cast<int>(buffer.buffer.size())) {
return result;
}
auto line = buffer.buffer.at(y);
const int len = static_cast<int>(line.size());
if (len - 1 >= x) {
const QColor color(line.at(x).background());
result.push_back(color.red());
result.push_back(color.green());
result.push_back(color.blue());
}
return result;
}
QPair<quint8, TChar> TConsole::getTextAttributes() const
{
// Take snapshots of cursor/selection coordinates to avoid race conditions
const QPoint beginPoint = P_begin;
const QPoint endPoint = P_end;
const QPoint userCursorPoint = mUserCursor;
int x = beginPoint.x();
int y = beginPoint.y();
// Fallback to cursor position if no selection is active
if (beginPoint == endPoint) {
x = userCursorPoint.x();
y = userCursorPoint.y();
}
// Take a snapshot of buffer size to avoid TOCTOU issues
const int bufferSize = static_cast<int>(buffer.buffer.size());
// Early bounds check
if (y < 0 || x < 0 || y >= bufferSize) {
return qMakePair(2, TChar());
}
// Get line reference and check its bounds safely
const auto& line = buffer.buffer.at(y);
const int lineSize = static_cast<int>(line.size());
if (x >= lineSize) {
return qMakePair(2, TChar());
}
// Safe access with bounds already verified
return qMakePair(0, line.at(x));
}
void TConsole::luaWrapLine(int line)
{
if (!mpHost) {
return;
}
buffer.wrapLine(line, mWrapAt, mIndentCount, mHangingIndentCount);
}
void TConsole::setFontSize(int size)
{
if (mDisplayFontDetails.mPointSize != size) {
mDisplayFontDetails.mPointSize = size;
setFont(mDisplayFontDetails.makeFont(), true);
}
}
bool TConsole::setConsoleBackgroundImage(const QString& imgPath, int mode)
{
const QColor bgColor = (mType == MainConsole) ? mpHost->mBgColor : mBgColor;
const QString styleSheet = buildBackgroundImageStyleSheet(qsl("MainDisplay"), bgColor, mode, imgPath);
if (styleSheet.isEmpty()) {
return false;
}
mpMainDisplay->setStyleSheet(styleSheet);
mBgImageMode = mode;
mBgImagePath = imgPath;
return true;
}
bool TConsole::resetConsoleBackgroundImage()
{
mBgImageMode = 0;
changeColors();
return true;
}
bool TConsole::setWindowBackgroundImage(const QString& imgPath, int mode)
{
if (!mpWindowBackground) {
return false;
}
if (mode == 5) {
QPixmap pixmap(imgPath);
if (pixmap.isNull()) {
qWarning().nospace().noquote() << "TConsole::setWindowBackgroundImage() ERROR - could not load \"" << imgPath << "\" as an image.";
return false;
}
const QPixmap previousSource = mWindowBgSourcePixmap;
const QString previousPath = mWindowBgImagePath;
const QString previousStyleSheet = mpWindowBackground->styleSheet();
mWindowBgSourcePixmap = pixmap;
mWindowBgImagePath = imgPath;
// clearing a stylesheet repolishes the widget and drops the palette brush,
// so it has to happen before the brush is installed
mpWindowBackground->setStyleSheet(QString());
if (!updateWindowBackgroundCoverPixmap()) {
mWindowBgSourcePixmap = previousSource;
mWindowBgImagePath = previousPath;
mpWindowBackground->setStyleSheet(previousStyleSheet);
// the failed attempt dropped the brush, so rebuild the one the previous
// source was showing rather than waiting for the next resize
updateWindowBackgroundCoverPixmap();
return false;
}
} else {
const QColor bgColor = mpHost ? mpHost->mBgColor : QColorConstants::Black;
const QString styleSheet = buildBackgroundImageStyleSheet(qsl("WindowBackground"), bgColor, mode, imgPath);
if (styleSheet.isEmpty()) {
return false;
}
mWindowBgSourcePixmap = QPixmap();
mpWindowBackground->setAutoFillBackground(false);
mpWindowBackground->setPalette(QPalette());
mpWindowBackground->setStyleSheet(styleSheet);
}
mWindowBgImageMode = mode;
mWindowBgImagePath = imgPath;
updateMainFrameTransparency();
return true;
}
bool TConsole::resetWindowBackgroundImage()
{
if (!mpWindowBackground) {
return false;
}
mWindowBgImageMode = 0;
mWindowBgImagePath.clear();
mWindowBgSourcePixmap = QPixmap();
mpWindowBackground->setStyleSheet(QString());
mpWindowBackground->setAutoFillBackground(false);
mpWindowBackground->setPalette(QPalette());
updateMainFrameTransparency();
return true;
}
void TConsole::updateMainFrameTransparency()
{
if (mType != MainConsole || !mpMainFrame) {
return;
}
QPalette framePalette;
framePalette.setColor(QPalette::Text, QColor(Qt::black));
framePalette.setColor(QPalette::Highlight, QColor(55, 55, 255));
framePalette.setColor(QPalette::Window, mWindowBgImageMode ? QColor(0, 0, 0, 0) : mBorderColor);
mpMainFrame->setPalette(framePalette);
mpMainFrame->setAutoFillBackground(true);
}
void TConsole::setBorderColor(const QColor& color)
{
mBorderColor = color;
updateMainFrameTransparency();
}
void TConsole::lowerMainDisplay()
{
mpMainDisplay->lower();
if (mpWindowBackground) {
mpWindowBackground->lower();
}
}
// The largest centred rectangle of the source that has the target's aspect ratio.
QRect TConsole::coverSourceRect(const QSize& sourceSize, const QSize& targetSize)
{
QSize cropSize = targetSize;
cropSize.scale(sourceSize, Qt::KeepAspectRatio);
cropSize = cropSize.boundedTo(sourceSize).expandedTo(QSize(1, 1));
return QRect(QPoint((sourceSize.width() - cropSize.width()) / 2, (sourceSize.height() - cropSize.height()) / 2), cropSize);
}
// Simulates CSS "cover" since QT stylesheets do not support it. Crop first: the
// other order multiplies the intermediate by the aspect mismatch, so a 3000x100
// image in a 1920x1080 window builds a 32400x1080 (~140MB) one on every resize.
bool TConsole::updateWindowBackgroundCoverPixmap()
{
if (!mpWindowBackground || mWindowBgSourcePixmap.isNull()) {
return true;
}
const QSize targetSize = mpWindowBackground->size();
if (targetSize.isEmpty()) {
return true;
}
const QRect sourceRect = coverSourceRect(mWindowBgSourcePixmap.size(), targetSize);
const QPixmap cropped = (sourceRect == mWindowBgSourcePixmap.rect()) ? mWindowBgSourcePixmap : mWindowBgSourcePixmap.copy(sourceRect);
const QPixmap scaled = cropped.scaled(targetSize, Qt::IgnoreAspectRatio, Qt::SmoothTransformation);
if (scaled.isNull()) {
if (!mWindowBgCoverScaleFailed) {
mWindowBgCoverScaleFailed = true;
qWarning().nospace().noquote() << "TConsole::updateWindowBackgroundCoverPixmap() ERROR - could not scale \"" << mWindowBgImagePath << "\" (source area " << sourceRect << ") to "
<< targetSize << '.';
}
// a brush smaller than the widget tiles, so drop the stale one
mpWindowBackground->setAutoFillBackground(false);
mpWindowBackground->setPalette(QPalette());
return false;
}
mWindowBgCoverScaleFailed = false;
QPalette palette;
palette.setBrush(QPalette::Window, QBrush(scaled));
mpWindowBackground->setPalette(palette);
mpWindowBackground->setAutoFillBackground(true);
return true;
}
void TConsole::setCmdVisible(bool isVisible)
{
const QSizePolicy sizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
// create MiniConsole commandline if it's not existing
if (!mpCommandLine) {
if (!isVisible) {
// If we don't have one and we are being told to hide it then
// really there is nothing to do - so lets do nothing:
return;
}
mpCommandLine = new TCommandLine(mpHost, mConsoleName, TCommandLine::ConsoleCommandLine, this, mpMainDisplay);
mpCommandLine->setContentsMargins(0, 0, 0, 0);
mpCommandLine->setSizePolicy(sizePolicy);
mpCommandLine->setFocusPolicy(Qt::StrongFocus);
mpCommandLine->setFont(font());
// put this CommandLine in the mainConsoles SubCommandLineMap
// name is the console name
mpHost->mpConsole->registerSubCommandLine(mConsoleName, mpCommandLine);
layoutLayer2->addWidget(mpCommandLine);
}
if (mType == MainConsole) {
if (mpHost) {
if (!mpHost->getCompactInputLine() && isVisible) {
mpButtonMainLayer->setVisible(true);
} else {
mpButtonMainLayer->setVisible(false);
}
}
} else {
mpButtonMainLayer->setVisible(false);
}
layerCommandLine->setVisible(isVisible);
mpCommandLine->setVisible(isVisible);
//resizes miniconsole if command line gets enabled/disabled
resizeConsole();
setProxyForFocus(isVisible ? mpCommandLine : nullptr);
// Need to remove the TCommandLine from the last used stack
// if it has been explicitly hidden:
if (!isVisible && mpHost) {
mpHost->forgetCommandLine(mpCommandLine);
}
}
void TConsole::refreshView() const
{
mUpperPane->setFont(font());
mUpperPane->updateScreenView();
mUpperPane->forceUpdate();
mLowerPane->setFont(font());
mLowerPane->updateScreenView();
mLowerPane->forceUpdate();
}
void TConsole::raiseFontChangeEvent()
{
if (!mpHost) {
return;
}
if (!(mType & (MainConsole | UserWindow | SubConsole))) {
return;
}
TEvent fontChangeEvent{};
fontChangeEvent.mArgumentList.append(QLatin1String("sysFontChangeEvent"));
fontChangeEvent.mArgumentTypeList.append(ARGUMENT_TYPE_STRING);
fontChangeEvent.mArgumentList.append(mConsoleName);
fontChangeEvent.mArgumentTypeList.append(ARGUMENT_TYPE_STRING);
fontChangeEvent.mArgumentList.append(font().family());
fontChangeEvent.mArgumentTypeList.append(ARGUMENT_TYPE_STRING);
fontChangeEvent.mArgumentList.append(QString::number(font().pointSize()));
fontChangeEvent.mArgumentTypeList.append(ARGUMENT_TYPE_NUMBER);
mpHost->raiseEvent(fontChangeEvent);
}
void TConsole::setFont(const QFont& newFont, const bool forceChange)
{
TFontAttributes newFontDetails(newFont);
if (forceChange || (mDisplayFontDetails != newFontDetails)) {
mDisplayFontDetails = newFontDetails;
QWidget::setFont(newFont);
// Update associated TCommandLine's:
if (mType & (MainConsole | SubConsole | UserWindow)) {
if (mpHost && mpHost->mpConsole) {
for (auto& commandLine : mpHost->mpConsole->mSubCommandLineMap) {
auto pConsole = commandLine->console();
if (pConsole && (pConsole == this)) {
commandLine->setFont(font());
commandLine->adjustHeight();
}
}
}
if (!mpCommandLine.isNull()) {
mpCommandLine->setFont(font());
mpCommandLine->adjustHeight();
}
}
refreshView();
raiseFontChangeEvent();
}
}
void TConsole::setFontName(const QString& fontName)
{
mDisplayFontDetails.mName = fontName;
setFont(mDisplayFontDetails.makeFont(), true);
}
QString TConsole::getCurrentLine()
{
return buffer.line(mUserCursor.y());
}
int TConsole::getLastLineNumber()
{
return buffer.getLastLineNumber();
}
void TConsole::moveCursorEnd()
{
const int y = buffer.getLastLineNumber();
int x = buffer.line(y).size() - 1;
x = x >= 0 ? x : 0;
moveCursor(x, y);
}
bool TConsole::moveCursor(int x, int y)
{
QPoint P(x, y);
if (buffer.moveCursor(P)) {
mUserCursor.setX(x);
mUserCursor.setY(y);
return true;
}
return false;
}
int TConsole::select(const QString& text, int numOfMatch)
{
if (mUserCursor.y() < 0 || mUserCursor.y() >= buffer.size()) {
deselect();
return -1;
}
if (mudlet::smDebugMode) {
TDebug(Qt::darkMagenta, Qt::black) << "line under current user cursor: " >> mpHost;
TDebug(Qt::red, Qt::black) << TDebug::csmContinue << mUserCursor.y() << "#:" >> mpHost;
TDebug(Qt::gray, Qt::black) << TDebug::csmContinue << buffer.line(mUserCursor.y()) << "\n" >> mpHost;
}
int begin = -1;
for (int i = 0; i < numOfMatch; i++) {
const QString li = buffer.line(mUserCursor.y());
if (li.isEmpty()) {
continue;
}
begin = li.indexOf(text, begin + 1);
if (begin == -1) {
deselect();
return -1;
}
}
if (begin < 0) {
deselect();
return -1;
}
const int end = begin + text.size();
P_begin = QPoint(begin, mUserCursor.y());
P_end = QPoint(end, mUserCursor.y());
if (mudlet::smDebugMode) {
TDebug(Qt::darkRed, Qt::black) << "P_begin(" << P_begin.x() << "/" << P_begin.y() << "), P_end(" << P_end.x() << "/" << P_end.y()
<< ") selectedText = " << buffer.line(mUserCursor.y()).mid(P_begin.x(), P_end.x() - P_begin.x()) << "\n"
>> mpHost;
}
return begin;
}
bool TConsole::selectSection(int from, int to)
{
if (mudlet::smDebugMode) {
TDebug(Qt::darkMagenta, Qt::black) << "selectSection(" << from << "," << to << "): line under current user cursor: " << buffer.line(mUserCursor.y()) << "\n" >> mpHost;
}
if (from < 0) {
return false;
}
if (mUserCursor.y() >= static_cast<int>(buffer.buffer.size())) {
return false;
}
const int s = buffer.buffer[mUserCursor.y()].size();
if (from > s || from + to > s) {
return false;
}
P_begin = QPoint(from, mUserCursor.y());
P_end = QPoint(from + to, mUserCursor.y());
if (mudlet::smDebugMode) {
TDebug(Qt::darkMagenta, Qt::black) << "P_begin(" << P_begin.x() << "/" << P_begin.y() << "), P_end(" << P_end.x() << "/" << P_end.y() << ") selectedText:\n\""
<< buffer.line(mUserCursor.y()).mid(P_begin.x(), P_end.x() - P_begin.x()) << "\"\n"
>> mpHost;
}
return true;
}
// returns whenever the selection is valid, the selection text,
// start position, and the length of the selection
std::tuple<bool, QString, int, int> TConsole::getSelection()
{
if (mUserCursor.y() >= static_cast<int>(buffer.buffer.size())) {
return {false, qsl("the selection is no longer valid"), 0, 0};
}
const auto start = P_begin.x();
const auto length = P_end.x() - P_begin.x();
const auto line = buffer.line(mUserCursor.y());
if (line.size() < start) {
return {false, qsl("the selection is no longer valid"), 0, 0};
}
const auto text = line.mid(start, length);
return {true, text, start, length};
}
void TConsole::setLink(const QStringList& linkFunction, const QStringList& linkHint, const QVector<int> linkReference)
{
buffer.applyLink(P_begin, P_end, linkFunction, linkHint, linkReference);
mUpperPane->forceUpdate();
mLowerPane->forceUpdate();
}
// Set or Reset ALL the specified (but not others)
void TConsole::setDisplayAttributes(const TChar::AttributeFlags attributes, const bool b)
{
mFormatCurrent.setAllDisplayAttributes((mFormatCurrent.allDisplayAttributes() & ~(attributes)) | (b ? attributes : TChar::None));
buffer.applyAttribute(P_begin, P_end, attributes, b);
mUpperPane->forceUpdate();
mLowerPane->forceUpdate();
}
void TConsole::setFgColor(int r, int g, int b)
{
setFgColor(QColor(r, g, b));
}
void TConsole::setBgColor(int r, int g, int b, int a)
{
setBgColor(QColor(r, g, b, a));
}
void TConsole::setBgColor(const QColor& newColor)
{
mFormatCurrent.setBackground(newColor);
buffer.applyBgColor(P_begin, P_end, newColor);
mUpperPane->forceUpdate();
mLowerPane->forceUpdate();
}
void TConsole::setFgColor(const QColor& newColor)
{
mFormatCurrent.setForeground(newColor);
buffer.applyFgColor(P_begin, P_end, newColor);
mUpperPane->forceUpdate();
mLowerPane->forceUpdate();
}
void TConsole::setCommandBgColor(int r, int g, int b, int a)
{
setCommandBgColor(QColor(r, g, b, a));
}
void TConsole::setCommandBgColor(const QColor& newColor)
{
mCommandBgColor = newColor;
}
void TConsole::setCommandFgColor(int r, int g, int b, int a)
{
setCommandFgColor(QColor(r, g, b, a));
}
void TConsole::setCommandFgColor(const QColor& newColor)
{
mCommandFgColor = newColor;
}
void TConsole::setScrollBarVisible(bool isVisible)
{
if (mpScrollBar) {
mScrollBarEnabled = isVisible;
mpScrollBar->setVisible(isVisible);
}
}
// Reports what enableScrollBar()/disableScrollBar() last asked for rather than
// QWidget::isVisible(): a profile that is not the front tab has its whole
// console hidden, which would otherwise make every background profile report
// its scroll bar as gone.
bool TConsole::getScrollBarVisible() const
{
return mScrollBarEnabled;
}
void TConsole::setHorizontalScrollBar(bool isEnabled)
{
if (mpHScrollBar) {
mHScrollBarEnabled = isEnabled;
mpHScrollBar->setVisible(isEnabled);
}
}
void TConsole::setScrolling(const bool state)
{
if (mType & (UserWindow | SubConsole)) {
mScrollingEnabled = state;
if (!mScrollingEnabled) {
clearSplit();
}
}
}
void TConsole::printCommand(QString& msg)
{
// Skip printing if remote echo is active (e.g., password mode)
if (mpHost && mpHost->isRemoteEchoingActive()) {
return;
}
if (mTriggerEngineMode) {
msg.append(QChar::LineFeed);
const int lineBeforeNewContent = buffer.getLastLineNumber();
if (lineBeforeNewContent >= 0 && !buffer.lineBuffer.back().isEmpty()) {
msg.prepend(QChar::LineFeed);
}
buffer.appendLine(msg, 0, msg.size() - 1, mCommandFgColor, mCommandBgColor);
} else {
const int lineBeforeNewContent = buffer.size() - 2;
if (lineBeforeNewContent >= 0) {
int promptEnd = buffer.buffer.at(lineBeforeNewContent).size();
if (promptEnd < 0) {
promptEnd = 0;
}
if (buffer.promptBuffer[lineBeforeNewContent]) {
QPoint P(promptEnd, lineBeforeNewContent);
const TChar format(mCommandFgColor, mCommandBgColor);
buffer.insertInLine(P, msg, format);
const int down = buffer.wrapLine(lineBeforeNewContent, mpHost->mScreenWidth, mpHost->mWrapIndentCount, mpHost->mWrapHangingIndentCount);
mUpperPane->needUpdate(lineBeforeNewContent, lineBeforeNewContent + 1 + down);
mLowerPane->needUpdate(lineBeforeNewContent, lineBeforeNewContent + 1 + down);
buffer.promptBuffer[lineBeforeNewContent] = false;
return;
}
}
msg.append("\n");
print(msg, mCommandFgColor, mCommandBgColor);
}
}
void TConsole::echoLink(const QString& text, QStringList& func, QStringList& hint, bool customFormat, QVector<int> luaReference)
{
if (customFormat) {
buffer.addLink(mTriggerEngineMode, text, func, hint, mFormatCurrent, luaReference);
} else {
const TChar f = TChar(Qt::blue, (mType == MainConsole ? mpHost->mBgColor : mBgColor), TChar::Underline);
buffer.addLink(mTriggerEngineMode, text, func, hint, f, luaReference);
}
mUpperPane->showNewLines();
mLowerPane->showNewLines();
}
// An overload of print(const QString& msg):
void TConsole::print(const char* txt)
{
const QString msg(txt);
print(msg);
}
// echoUserWindow(const QString& msg) was a redundant wrapper around this method:
void TConsole::print(const QString& msg)
{
buffer.append(msg, 0, msg.size(), mFormatCurrent.foreground(), mFormatCurrent.background(), mFormatCurrent.allDisplayAttributes());
mUpperPane->showNewLines();
mLowerPane->showNewLines();
if (Q_UNLIKELY(mudlet::self()->smMirrorToStdOut)) {
qDebug().nospace().noquote() << qsl("%1| %2").arg(mConsoleName, msg);
}
}
// printDebug(QColor& c, QColor& d, const QString& msg) was functionally the
// same as this method it was just that the arguments were in a different order
void TConsole::print(const QString& msg, const QColor fgColor, const QColor bgColor)
{
buffer.append(msg, 0, msg.size(), fgColor, bgColor);
mUpperPane->showNewLines();
mLowerPane->showNewLines();
if (Q_UNLIKELY(mudlet::self()->smMirrorToStdOut)) {
qDebug().nospace().noquote() << qsl("%1| %2").arg(mConsoleName, msg);
}
}
void TConsole::printFormatted(const QString& text, const std::deque<TChar>& formatting, const TLinkStore& sourceLinkStore)
{
buffer.appendFormatted(text, formatting, sourceLinkStore);
mUpperPane->showNewLines();
mLowerPane->showNewLines();
if (Q_UNLIKELY(mudlet::self()->smMirrorToStdOut)) {
qDebug().nospace().noquote() << qsl("%1| %2").arg(mConsoleName, text);
}
}
void TConsole::printSystemMessage(const QString& msg)
{
const QString txt = tr("System Message: %1").arg(msg);
print(txt, mSystemMessageFgColor, mSystemMessageBgColor);
}
void TConsole::echo(const QString& msg)
{
// Strip \r so that \r\n becomes \n and standalone \r disappears; without
// this, \r is stored literally in the buffer and rendered as a glyph.
QString normalizedMsg = msg;
normalizedMsg.remove(QChar::CarriageReturn);
if (mTriggerEngineMode) {
// Use insertInLine instead of appendLine so that newline characters
// are embedded in the trigger line rather than creating new buffer
// lines (which would cause subsequent echo/cecho calls to append to
// the wrong line). The embedded newlines are properly handled during
// wrapping by getWrapInfo.
const int y = buffer.size() - 1;
if (y >= 0) {
const int x = buffer.lineBuffer.at(y).size();
QPoint insertPoint(x, y);
buffer.insertInLine(insertPoint, normalizedMsg, mFormatCurrent);
} else {
buffer.appendLine(normalizedMsg, 0, normalizedMsg.size() - 1, mFormatCurrent.foreground(), mFormatCurrent.background(), mFormatCurrent.allDisplayAttributes());
}
} else {
print(normalizedMsg);
}
}
void TConsole::copy()
{
mpHost->mpConsole->mClipboard = buffer.copy(P_begin, P_end);
}
void TConsole::cut()
{
mpHost->mpConsole->mClipboard = buffer.cut(P_begin, P_end);
}
void TConsole::paste()
{
if (buffer.size() - 1 > mUserCursor.y()) {
buffer.paste(mUserCursor, mpHost->mpConsole->mClipboard);
mUpperPane->needUpdate(mUserCursor.y(), mUserCursor.y());
} else {
buffer.appendBuffer(mpHost->mpConsole->mClipboard);
}
mUpperPane->showNewLines();
mLowerPane->showNewLines();
}
void TConsole::pasteWindow(const TBuffer& bufferSlice)
{
mpHost->mpConsole->mClipboard = bufferSlice;
paste();
}
void TConsole::appendBuffer()
{
buffer.appendBuffer(mpHost->mpConsole->mClipboard);
mUpperPane->showNewLines();
mLowerPane->showNewLines();
}
void TConsole::appendBuffer(const TBuffer& bufferSlice)
{
buffer.appendBuffer(bufferSlice);
mUpperPane->showNewLines();
mLowerPane->showNewLines();
}
void TConsole::slot_stopAllItems(bool b)
{
if (b) {
mpHost->stopAllTriggers();
} else {
mpHost->reenableAllTriggers();
}
}
void TConsole::focusOnSearchResultAndAnnounce(int searchX, int searchY)
{
mpHost->setCaretEnabled(true);
mUpperPane->initializeCaret();
moveCursor(searchX, searchY);
mUpperPane->setCaretPosition(searchY, searchX);
mUpperPane->updateCaret();
mUpperPane->setFocusPolicy(Qt::StrongFocus);
mUpperPane->setFocusProxy(nullptr);
mUpperPane->setFocus();
mudlet::self()->announce(buffer.lineBuffer[searchY]);
}
void TConsole::slot_searchBufferUp()
{
if (mpHost->getF3SearchEnabled()) {
buffer.clearSearchHighlights();
}
// The search term entry box is one widget that does not pass a mouse press
// event up to the main TConsole and thus does not cause the focus to shift
// to the profile's tab when in multi-view mode - so add a call to make that
// happen:
mudlet::self()->activateProfile(mpHost);
if (mSearchQuery != mpBufferSearchBox->text()) {
mSearchQuery = mpBufferSearchBox->text();
buffer.clearSearchHighlights();
mCurrentSearchResult = buffer.lineBuffer.size();
} else {
// make sure the line to search from does not exceed the buffer, which can grow and shrink dynamically
mCurrentSearchResult = std::min<qsizetype>(mCurrentSearchResult, buffer.lineBuffer.size());
}
if (mSearchQuery.isEmpty() || buffer.lineBuffer.empty()) {
// Don't try and search for anything if the search term OR the console is empty:
return;
}
bool found = false;
for (int searchY = mCurrentSearchResult - 1; searchY >= 0; --searchY) {
int searchX = -1;
do {
searchX = buffer.lineBuffer[searchY].indexOf(mSearchQuery, searchX + 1, ((mSearchOptions & SearchOptionCaseSensitive) ? Qt::CaseSensitive : Qt::CaseInsensitive));
if (searchX > -1) {
buffer.applyAttribute(QPoint(searchX, searchY), QPoint(searchX + mSearchQuery.size(), searchY), TChar::Found, true);
if (mpHost->getF3SearchEnabled()) {
focusOnSearchResultAndAnnounce(searchX, searchY);
}
found = true;
}
} while (searchX > -1);
if (found) {
// Scroll to show the match
scrollUp(buffer.mCursorY - searchY - 3);
mUpperPane->forceUpdate();
mCurrentSearchResult = searchY;
return;
}
}
print(qsl("%1\n").arg(tr("No search results, sorry!")));
}
void TConsole::slot_searchBufferDown()
{
if (mpHost->getF3SearchEnabled()) {
buffer.clearSearchHighlights();
}
if (mSearchQuery != mpBufferSearchBox->text()) {
mSearchQuery = mpBufferSearchBox->text();
buffer.clearSearchHighlights();
mCurrentSearchResult = buffer.lineBuffer.size();
}
if (mSearchQuery.isEmpty() || buffer.lineBuffer.empty()) {
// Don't try and search for anything if the search term OR the console is empty:
return;
}
if (mCurrentSearchResult >= buffer.lineBuffer.size()) {
return;
}
bool found = false;
for (int searchY = mCurrentSearchResult + 1; searchY < buffer.lineBuffer.size(); ++searchY) {
int searchX = -1;
do {
searchX = buffer.lineBuffer[searchY].indexOf(mSearchQuery, searchX + 1, ((mSearchOptions & SearchOptionCaseSensitive) ? Qt::CaseSensitive : Qt::CaseInsensitive));
if (searchX > -1) {
buffer.applyAttribute(QPoint(searchX, searchY), QPoint(searchX + mSearchQuery.size(), searchY), TChar::Found, true);
if (mpHost->getF3SearchEnabled()) {
focusOnSearchResultAndAnnounce(searchX, searchY);
}
found = true;
}
} while (searchX > -1);
if (found) {
// Scroll to show the match
scrollUp(buffer.mCursorY - searchY - 3);
mUpperPane->forceUpdate();
mCurrentSearchResult = searchY;
return;
}
}
print(qsl("%1\n").arg(tr("No search results, sorry!")));
}
QSize TConsole::getMainWindowSize() const
{
if (isHidden()) {
return mOldSize;
}
const QSize consoleSize = size();
const int toolbarWidth = mpLeftToolBar->width() + mpRightToolBar->width();
const int toolbarHeight = mpTopToolBar->height();
const int commandLineHeight = mpCommandLine->height();
QSize mainWindowSize(consoleSize.width() - toolbarWidth, consoleSize.height() - (commandLineHeight + toolbarHeight));
// Reject obviously invalid or suspiciously small sizes during profile switch transitions
const int minValidWidth = 50;
if (mainWindowSize.width() < minValidWidth && mOldSize.width() >= minValidWidth) {
return mOldSize;
}
// Reject suspicious shrinkage (more than 50% reduction) - geometry may not have settled yet
if (mOldSize.width() > 0) {
const double shrinkageRatio = static_cast<double>(mainWindowSize.width()) / mOldSize.width();
if (shrinkageRatio < 0.5) {
return mOldSize;
}
}
return mainWindowSize;
}
void TConsole::setProfileName(const QString& newName)
{
mProfileName = newName;
}
void TConsole::dragEnterEvent(QDragEnterEvent* e)
{
if (e->mimeData()->hasUrls() || e->mimeData()->hasText()) {
// Use ctrl key to decide if action is link or copy
// CopyAction corresponds to installing dropped file as a package
// LinkAction corresponds to installing dropped file as a module
Qt::KeyboardModifiers modifiers = e->modifiers();
if (modifiers & Qt::ControlModifier) {
e->setDropAction(Qt::LinkAction);
} else {
e->setDropAction(Qt::CopyAction);
}
e->accept();
}
}
void TConsole::dragMoveEvent(QDragMoveEvent* e)
{
if (e->mimeData()->hasUrls() || e->mimeData()->hasText()) {
// Use ctrl key to decide if action is link or copy
// CopyAction corresponds to installing dropped file as a package
// LinkAction corresponds to installing dropped file as a module
Qt::KeyboardModifiers modifiers = e->modifiers();
if (modifiers & Qt::ControlModifier) {
e->setDropAction(Qt::LinkAction);
} else {
e->setDropAction(Qt::CopyAction);
}
e->accept();
}
}
//https://amin-ahmadi.com/2016/01/04/qt-drag-drop-files-images/
void TConsole::dropEvent(QDropEvent* e)
{
for (const auto& url : e->mimeData()->urls()) {
const QString fname = url.toLocalFile();
const QFileInfo info(fname);
if (info.exists()) {
QPoint pos = e->position().toPoint();
TEvent mudletEvent{};
mudletEvent.mArgumentList.append(QLatin1String("sysDropEvent"));
mudletEvent.mArgumentList.append(fname);
mudletEvent.mArgumentList.append(info.suffix().trimmed());
mudletEvent.mArgumentList.append(QString::number(pos.x()));
mudletEvent.mArgumentList.append(QString::number(pos.y()));
mudletEvent.mArgumentList.append(mConsoleName);
mudletEvent.mArgumentTypeList.append(ARGUMENT_TYPE_STRING);
mudletEvent.mArgumentTypeList.append(ARGUMENT_TYPE_STRING);
mudletEvent.mArgumentTypeList.append(ARGUMENT_TYPE_STRING);
mudletEvent.mArgumentTypeList.append(ARGUMENT_TYPE_NUMBER);
mudletEvent.mArgumentTypeList.append(ARGUMENT_TYPE_NUMBER);
mudletEvent.mArgumentTypeList.append(ARGUMENT_TYPE_STRING);
mpHost->raiseEvent(mudletEvent);
}
}
if (e->mimeData()->hasText()) {
if (const QUrl url(e->mimeData()->text()); url.isValid()) {
QPoint pos = e->position().toPoint();
TEvent mudletEvent{};
mudletEvent.mArgumentList.append(QLatin1String("sysDropUrlEvent"));
mudletEvent.mArgumentList.append(url.toString());
mudletEvent.mArgumentList.append(url.scheme());
mudletEvent.mArgumentList.append(QString::number(pos.x()));
mudletEvent.mArgumentList.append(QString::number(pos.y()));
mudletEvent.mArgumentList.append(mConsoleName);
mudletEvent.mArgumentTypeList.append(ARGUMENT_TYPE_STRING);
mudletEvent.mArgumentTypeList.append(ARGUMENT_TYPE_STRING);
mudletEvent.mArgumentTypeList.append(ARGUMENT_TYPE_STRING);
mudletEvent.mArgumentTypeList.append(ARGUMENT_TYPE_NUMBER);
mudletEvent.mArgumentTypeList.append(ARGUMENT_TYPE_NUMBER);
mudletEvent.mArgumentTypeList.append(ARGUMENT_TYPE_STRING);
mpHost->raiseEvent(mudletEvent);
}
}
}
// This is also called from the TTextEdit mouse(Press|Release)Event()s:
void TConsole::raiseMudletMousePressOrReleaseEvent(QMouseEvent* event, const bool isPressEvent)
{
if (mType & (CentralDebugConsole | ErrorConsole)) {
return;
}
// Else if NOT the CentralDebugConsole or the ErrorConsole then bring the
// focus to the current profile in the main application window:
TEvent mudletEvent{};
mudletEvent.mArgumentList.append(isPressEvent ? qsl("sysWindowMousePressEvent") : qsl("sysWindowMouseReleaseEvent"));
switch (event->button()) {
case Qt::LeftButton:
mudletEvent.mArgumentList.append(QString::number(1));
break;
case Qt::RightButton:
mudletEvent.mArgumentList.append(QString::number(2));
break;
case Qt::MiddleButton:
mudletEvent.mArgumentList.append(QString::number(3));
break;
case Qt::BackButton:
mudletEvent.mArgumentList.append(QString::number(4));
break;
case Qt::ForwardButton:
mudletEvent.mArgumentList.append(QString::number(5));
break;
case Qt::TaskButton:
mudletEvent.mArgumentList.append(QString::number(6));
break;
case Qt::ExtraButton4:
mudletEvent.mArgumentList.append(QString::number(7));
break;
case Qt::ExtraButton5:
mudletEvent.mArgumentList.append(QString::number(8));
break;
case Qt::ExtraButton6:
mudletEvent.mArgumentList.append(QString::number(9));
break;
case Qt::ExtraButton7:
mudletEvent.mArgumentList.append(QString::number(10));
break;
case Qt::ExtraButton8:
mudletEvent.mArgumentList.append(QString::number(11));
break;
case Qt::ExtraButton9:
mudletEvent.mArgumentList.append(QString::number(12));
break;
case Qt::ExtraButton10:
mudletEvent.mArgumentList.append(QString::number(13));
break;
case Qt::ExtraButton11:
mudletEvent.mArgumentList.append(QString::number(14));
break;
case Qt::ExtraButton12:
mudletEvent.mArgumentList.append(QString::number(15));
break;
case Qt::ExtraButton13:
mudletEvent.mArgumentList.append(QString::number(16));
break;
case Qt::ExtraButton14:
mudletEvent.mArgumentList.append(QString::number(17));
break;
case Qt::ExtraButton15:
mudletEvent.mArgumentList.append(QString::number(18));
break;
case Qt::ExtraButton16:
mudletEvent.mArgumentList.append(QString::number(19));
break;
case Qt::ExtraButton17:
mudletEvent.mArgumentList.append(QString::number(20));
break;
case Qt::ExtraButton18:
mudletEvent.mArgumentList.append(QString::number(21));
break;
case Qt::ExtraButton19:
mudletEvent.mArgumentList.append(QString::number(22));
break;
case Qt::ExtraButton20:
mudletEvent.mArgumentList.append(QString::number(23));
break;
case Qt::ExtraButton21:
mudletEvent.mArgumentList.append(QString::number(24));
break;
case Qt::ExtraButton22:
mudletEvent.mArgumentList.append(QString::number(25));
break;
case Qt::ExtraButton23:
mudletEvent.mArgumentList.append(QString::number(26));
break;
case Qt::ExtraButton24:
mudletEvent.mArgumentList.append(QString::number(27));
break;
default:
mudletEvent.mArgumentList.append(QString::number(0));
}
QPoint pos = event->position().toPoint();
mudletEvent.mArgumentList.append(QString::number(pos.x()));
mudletEvent.mArgumentList.append(QString::number(pos.y()));
mudletEvent.mArgumentList.append(mConsoleName);
mudletEvent.mArgumentTypeList.append(ARGUMENT_TYPE_STRING);
mudletEvent.mArgumentTypeList.append(ARGUMENT_TYPE_NUMBER);
mudletEvent.mArgumentTypeList.append(ARGUMENT_TYPE_NUMBER);
mudletEvent.mArgumentTypeList.append(ARGUMENT_TYPE_NUMBER);
mudletEvent.mArgumentTypeList.append(ARGUMENT_TYPE_STRING);
mpHost->raiseEvent(mudletEvent);
// Focus this console's command line, not the "active" one
// This ensures clicking on a console focuses its own command line
if (mpCommandLine && mpCommandLine->isVisible()) {
mpCommandLine->setFocus(Qt::MouseFocusReason);
mpHost->recordActiveCommandLine(mpCommandLine);
} else if (mType == MainConsole) {
// Main console always has its command line
mpHost->mpConsole->mpCommandLine->setFocus(Qt::MouseFocusReason);
mpHost->recordActiveCommandLine(mpHost->mpConsole->mpCommandLine);
} else {
// Fallback to the old behavior for other cases
mpHost->setFocusOnHostActiveCommandLine();
}
}
void TConsole::mousePressEvent(QMouseEvent* event)
{
raiseMudletMousePressOrReleaseEvent(event, true);
}
void TConsole::slot_adjustAccessibleNames()
{
const bool multipleProfilesActive = (mudlet::self()->getHostManager().getHostCount() > 1);
switch (mType) {
case CentralDebugConsole:
setAccessibleName(tr("Debug Console."));
setAccessibleDescription(tr("Debug messages from all profiles are shown here."));
if (mLowerPane->isVisible()) {
//: accessibility-friendly name to describe the upper half of the Mudlet central debug window when you've scrolled up
mUpperPane->setAccessibleName(tr("Central debug console past content."));
//: accessibility-friendly name to describe the lower half of the Mudlet central debug when you've scrolled up
mLowerPane->setAccessibleName(tr("Central debug console live content."));
} else {
//: accessibility-friendly name to describe the upper half of the Mudlet central debug window when it is not scrolled up
mUpperPane->setAccessibleName(tr("Central debug console."));
mLowerPane->setAccessibleName(QString());
}
return;
case ErrorConsole:
setAccessibleName(tr("Error Console in editor."));
if (mLowerPane->isVisible()) {
if (multipleProfilesActive) {
//: accessibility-friendly name to describe the upper half of the Mudlet profile's editor error window when you've scrolled up, %1 is the name of the profile when more than one is loaded.
mUpperPane->setAccessibleName(tr("Editor's error window for profile \"%1\", past content.").arg(mProfileName));
//: accessibility-friendly name to describe the lower half of the Mudlet profile's editor error window when you've scrolled up, %1 is the name of the profile when more than one is loaded.
mLowerPane->setAccessibleName(tr("Editor's error window for profile \"%1\", live content.").arg(mProfileName));
} else {
//: accessibility-friendly name to describe the upper half of the Mudlet profile's editor error window when you've scrolled up and only one profile is loaded.
mUpperPane->setAccessibleName(tr("Editor's error window past content."));
//: accessibility-friendly name to describe the lower half of the Mudlet profile's editor error window when you've scrolled up and only one profile is loaded.
mLowerPane->setAccessibleName(tr("Editor's error window live content."));
}
setAccessibleDescription(tr("Error messages for the \"%1\" profile are shown here in the editor.").arg(mProfileName));
} else {
if (multipleProfilesActive) {
//: accessibility-friendly name to describe the upper half of the Mudlet profile's editor error window when it is not scrolled up, %1 is the name of the profile when more than one is loaded.
mUpperPane->setAccessibleName(tr("Editor's error window for profile \"%1\".").arg(mProfileName));
} else {
//: accessibility-friendly name to describe the upper half of the Mudlet profile's editor error window when it is not scrolled up and only one profile is loaded.
mUpperPane->setAccessibleName(tr("Editor's error window"));
}
mLowerPane->setAccessibleName(QString());
setAccessibleDescription(tr("Error messages are shown here in the editor."));
}
return;
case MainConsole:
setAccessibleDescription(tr("Game content is shown here. It may contain subconsoles and a mapper window."));
if (multipleProfilesActive) {
setAccessibleName(tr("Main Window for \"%1\" profile.").arg(mProfileName));
} else {
setAccessibleName(tr("Main Window."));
}
if (mLowerPane->isVisible()) {
if (multipleProfilesActive) {
//: accessibility-friendly name to describe the upper half of a Mudlet profile's main window when you've scrolled up, %1 is the name of the profile when more than one is loaded.
mUpperPane->setAccessibleName(tr("Profile \"%1\" main window past content.").arg(mProfileName));
//: accessibility-friendly name to describe the lower half of a Mudlet profile's main window when you've scrolled up, %1 is the name of the profile when more than one is loaded.
mLowerPane->setAccessibleName(tr("Profile \"%1\" main window live content.").arg(mProfileName));
} else {
//: accessibility-friendly name to describe the upper half of a Mudlet profile's main window when you've scrolled up and only one profile is loaded.
mUpperPane->setAccessibleName(tr("Profile main window past content."));
//: accessibility-friendly name to describe the lower half of a Mudlet profile's main window when you've scrolled up and only one profile is loaded.
mLowerPane->setAccessibleName(tr("Profile main window live content."));
}
} else {
if (multipleProfilesActive) {
//: accessibility-friendly name to describe the upper half of a Mudlet profile's main window when it is not scrolled up, %1 is the name of the profile when more than one is loaded.
mUpperPane->setAccessibleName(tr("Profile \"%1\" main window.").arg(mProfileName));
} else {
//: accessibility-friendly name to describe the upper half of a Mudlet profile's main window when it is not scrolled up and only one profile is loaded.
mUpperPane->setAccessibleName(tr("Profile main window."));
}
mLowerPane->setAccessibleName(QString());
}
return;
case SubConsole:
if (multipleProfilesActive) {
setAccessibleName(tr("Embedded window \"%1\" for \"%2\" profile.").arg(mConsoleName, mProfileName));
} else {
setAccessibleName(tr("Embedded window \"%1\".").arg(mConsoleName));
}
setAccessibleDescription(tr("Game content or locally generated text may be sent here."));
if (mLowerPane->isVisible()) {
if (multipleProfilesActive) {
//: accessibility-friendly name to describe the upper half of a Mudlet profile's sub-console window when you've scrolled up, %1 is the name of the profile when more than one is loaded and %2 is the name of the window.
mUpperPane->setAccessibleName(tr("Profile \"%1\" embedded window \"%2\" past content.").arg(mProfileName, mConsoleName));
//: accessibility-friendly name to describe the lower half of a Mudlet profile's sub-console window when you've scrolled up, %1 is the name of the profile when more than one is loaded and %2 is the name of the window.
mLowerPane->setAccessibleName(tr("Profile \"%1\" embedded window \"%2\" live content.").arg(mProfileName, mConsoleName));
} else {
//: accessibility-friendly name to describe the upper half of a Mudlet profile's sub-console window when you've scrolled up, %1 is the name of the window.
mUpperPane->setAccessibleName(tr("Profile embedded window \"%1\" past content.").arg(mConsoleName));
//: accessibility-friendly name to describe the lower half of a Mudlet profile's sub-console window when you've scrolled up, %1 is the name of the window.
mLowerPane->setAccessibleName(tr("Profile embedded window \"%1\" live content.").arg(mConsoleName));
}
} else {
if (multipleProfilesActive) {
//: accessibility-friendly name to describe the upper half of a Mudlet profile's sub-console window when it is not scrolled up, %1 is the name of the profile when more than one is loaded and %2 is the name of the window.
mUpperPane->setAccessibleName(tr("Profile \"%1\" embedded window \"%2\".").arg(mProfileName, mConsoleName));
} else {
//: accessibility-friendly name to describe the upper half of a Mudlet profile's sub-console window when it is not scrolled up, %1 is the name of the window.
mUpperPane->setAccessibleName(tr("Profile embedded window \"%1\".").arg(mConsoleName));
}
mLowerPane->setAccessibleName(QString());
}
return;
case UserWindow:
if (multipleProfilesActive) {
setAccessibleName(tr("User window \"%1\" for \"%2\" profile.").arg(mConsoleName, mProfileName));
} else {
setAccessibleName(tr("User window \"%1\".").arg(mConsoleName));
}
setAccessibleDescription(
tr("Game content or locally generated text may be sent to this window that may be floated away from the Mudlet application or docked within the main application window."));
if (mLowerPane->isVisible()) {
if (multipleProfilesActive) {
//: accessibility-friendly name to describe the upper half of a Mudlet profile's floating/dockable user window when you've scrolled up, %1 is the name of the profile when more than one is loaded and %2 is the name of the window.
mUpperPane->setAccessibleName(tr("Profile \"%1\" user window \"%2\" past content.").arg(mProfileName, mConsoleName));
//: accessibility-friendly name to describe the lower half of a Mudlet profile's floating/dockable user window window when you've scrolled up, %1 is the name of the profile when more than one is loaded and %2 is the name of the window.
mLowerPane->setAccessibleName(tr("Profile \"%1\" user window \"%2\" live content.").arg(mProfileName, mConsoleName));
} else {
//: accessibility-friendly name to describe the upper half of a Mudlet profile's sub-console window when you've scrolled up, %1 is the name of the window.
mUpperPane->setAccessibleName(tr("Profile user window \"%1\" past content.").arg(mConsoleName));
//: accessibility-friendly name to describe the lower half of a Mudlet profile's sub-console window when you've scrolled up, %1 is the name of the window.
mLowerPane->setAccessibleName(tr("Profile user window \"%1\" live content.").arg(mConsoleName));
}
} else {
if (multipleProfilesActive) {
//: accessibility-friendly name to describe the upper half of a Mudlet profile's floating/dockable user window window when it is not scrolled up, %1 is the name of the profile when more than one is loaded and %2 is the name of the window.
mUpperPane->setAccessibleName(tr("Profile \"%1\" user window \"%2\".").arg(mProfileName, mConsoleName));
} else {
//: accessibility-friendly name to describe the upper half of a Mudlet profile's floating/dockable user window window when it is not scrolled up, %1 is the name of the window.
mUpperPane->setAccessibleName(tr("Profile user window \"%1\".").arg(mConsoleName));
}
mLowerPane->setAccessibleName(QString());
}
return;
case Buffer:
// This is not a visible thing so is not accessible to screen readers
return;
case UnknownType:
// Should never be used - and since we have now handled ALL enum values
// we do not need a "default:" entry
Q_UNREACHABLE();
}
}
void TConsole::mouseReleaseEvent(QMouseEvent* event)
{
raiseMudletMousePressOrReleaseEvent(event, false);
}
void TConsole::slot_changeControlCharacterHandling(const ControlCharacterMode mode)
{
if (mControlCharacter != mode) {
mControlCharacter = mode;
refreshView();
}
}
void TConsole::setProxyForFocus(TCommandLine* pCommandLine)
{
if (mType == MainConsole) {
// Update all focus proxies to the main command line
setFocusProxy(pCommandLine);
mUpperPane->setFocusProxy(pCommandLine);
mLowerPane->setFocusProxy(pCommandLine);
QAccessibleEvent event(pCommandLine, QAccessible::Focus);
QAccessible::updateAccessibility(&event);
} else if (mType == UserWindow) {
if (pCommandLine && pCommandLine->isVisible()) {
// Update all focus proxies to the UserWindow's command line
setFocusProxy(pCommandLine);
mUpperPane->setFocusProxy(pCommandLine);
mLowerPane->setFocusProxy(pCommandLine);
QAccessibleEvent event(pCommandLine, QAccessible::Focus);
QAccessible::updateAccessibility(&event);
} else {
// Revert to main console's command line
setFocusProxy(mpHost->mpConsole->mpCommandLine);
mUpperPane->setFocusProxy(mpHost->mpConsole->mpCommandLine);
mLowerPane->setFocusProxy(mpHost->mpConsole->mpCommandLine);
QAccessibleEvent event(mpHost->mpConsole->mpCommandLine, QAccessible::Focus);
QAccessible::updateAccessibility(&event);
}
} else if (mType == SubConsole) {
if (pCommandLine && pCommandLine->isVisible()) {
// Update all focus proxies to the SubConsole's command line
setFocusProxy(pCommandLine);
mUpperPane->setFocusProxy(pCommandLine);
mLowerPane->setFocusProxy(pCommandLine);
QAccessibleEvent event(pCommandLine, QAccessible::Focus);
QAccessible::updateAccessibility(&event);
} else {
// Need to search ancestors to find the TConsole that this one
// is inserted into - and if it has a TCommandLine
auto parentConsole = mpHost->parentTConsole(this);
if (!parentConsole.isNull() && parentConsole->mpCommandLine && parentConsole->mpCommandLine->isVisible()) {
// TBH We ought to also check for any added TCommandLine but
// that can wait for a future development...
setFocusProxy(parentConsole->mpCommandLine);
mUpperPane->setFocusProxy(parentConsole->mpCommandLine);
mLowerPane->setFocusProxy(parentConsole->mpCommandLine);
QAccessibleEvent event(parentConsole->mpCommandLine, QAccessible::Focus);
QAccessible::updateAccessibility(&event);
} else {
// Somehow that has failed so fall back to the main console
setFocusProxy(mpHost->mpConsole->mpCommandLine);
mUpperPane->setFocusProxy(mpHost->mpConsole->mpCommandLine);
mLowerPane->setFocusProxy(mpHost->mpConsole->mpCommandLine);
QAccessibleEvent event(mpHost->mpConsole->mpCommandLine, QAccessible::Focus);
QAccessible::updateAccessibility(&event);
}
}
}
}
// At present this only supports/works on the main console
void TConsole::setCaretMode(bool enabled)
{
mUpperPane->updateCaret();
mLowerPane->updateCaret();
if (enabled) {
mUpperPane->initializeCaret();
// Remove the focusProxy before setting the focusPolicy otherwise
// the Policy gets sent to the Proxy!
mUpperPane->setFocusProxy(nullptr);
// This adds TabFocus to the otherwise used ClickFocus:
mUpperPane->setFocusPolicy(Qt::StrongFocus);
#if defined(Q_OS_WINDOWS) || defined(Q_OS_LINUX)
// windows & linux don't move keyboard focus to the main window without this
mUpperPane->setFocus(Qt::MouseFocusReason);
mUpperPane->grabKeyboard();
QAccessibleEvent event(mUpperPane, QAccessible::Focus);
QAccessible::updateAccessibility(&event);
#endif
// The overload without an argument uses Qt::OtherFocusReason according
// to the Qt source code:
mUpperPane->setFocus();
} else {
#if defined(Q_OS_WINDOWS) || defined(Q_OS_LINUX)
// NVDA breaks focus reset, so do it on a timer
QTimer::singleShot(0ms, this, [this]() {
mUpperPane->releaseKeyboard();
});
#endif
Q_ASSERT_X(!mUpperPane->focusProxy(), "TConsole:setCaretMode(false) FAIL", "About to set a focusPolicy but there is a focusProxy in place that will get it instead!");
mUpperPane->setFocusPolicy(Qt::ClickFocus);
setProxyForFocus(mpCommandLine);
// Carefull - if there is a FocusProxy for this element then IT gets the policy
}
}
void TConsole::createSearchOptionIcon()
{
// When we add new search options we must create icons for each combination
// beforehand - which is simpler than having to do code to combine the
// QPixMaps...
QIcon newIcon;
switch (mSearchOptions) {
// Each combination must be handled here
case SearchOptionCaseSensitive:
newIcon.addPixmap(QPixmap(":/icons/searchOptions-caseSensitive.png"));
break;
case SearchOptionNone:
// Use the grey icon as that is appropriate for the "No options set" case
newIcon.addPixmap(QPixmap(":/icons/searchOptions-none.png"));
break;
default:
// Don't grey out this one - is a diagnositic for an uncoded combination
newIcon.addPixmap(QPixmap(":/icons/searchOptions-unspecified.png"));
}
mIcon_searchOptions = newIcon;
mpAction_searchOptions->setIcon(newIcon);
}
void TConsole::setSearchOptions(const SearchOptions optionsState)
{
mSearchOptions = optionsState;
mpAction_searchCaseSensitive->setChecked(optionsState & SearchOptionCaseSensitive);
createSearchOptionIcon();
}
void TConsole::slot_toggleSearchCaseSensitivity(const bool state)
{
if ((mSearchOptions & SearchOptionCaseSensitive) != state) {
mSearchOptions = (mSearchOptions & ~(SearchOptionCaseSensitive)) | (state ? SearchOptionCaseSensitive : SearchOptionNone);
createSearchOptionIcon();
mpHost->mBufferSearchOptions = mSearchOptions;
}
}
void TConsole::setF3SearchEnabled(const bool enabled)
{
if (mType != MainConsole) {
// Don't do anything if we are NOT the main console:
return;
}
if (mF3SearchEnabled == enabled) {
// Don't do anything if the stored setting already matches the wanted one
return;
}
mF3SearchEnabled = enabled;
if (mF3SearchEnabled) {
// Create F3/Shift+F3 shortcuts for search navigation if needed
if (mpSearchNextShortcut.isNull()) {
mpSearchNextShortcut = new QShortcut(QKeySequence(Qt::Key_F3), this);
}
if (mpSearchPrevShortcut.isNull()) {
mpSearchPrevShortcut = new QShortcut(QKeySequence(Qt::SHIFT | Qt::Key_F3), this);
}
connect(mpSearchNextShortcut, &QShortcut::activated, this, &TConsole::slot_searchBufferDown, Qt::UniqueConnection);
connect(mpSearchPrevShortcut, &QShortcut::activated, this, &TConsole::slot_searchBufferUp, Qt::UniqueConnection);
} else {
if (!mpSearchNextShortcut.isNull()) {
disconnect(mpSearchNextShortcut, &QShortcut::activated, this, &TConsole::slot_searchBufferDown);
mpSearchNextShortcut->deleteLater();
}
if (!mpSearchPrevShortcut.isNull()) {
disconnect(mpSearchPrevShortcut, &QShortcut::activated, this, &TConsole::slot_searchBufferUp);
mpSearchPrevShortcut->deleteLater();
}
}
}
void TConsole::slot_clearSearchResults()
{
buffer.clearSearchHighlights();
mUpperPane->forceUpdate();
mLowerPane->forceUpdate();
}
void TConsole::handleLinesOverflowEvent(const int lineCount)
{
if (mType & ~(UserWindow | SubConsole)) {
// It isn't a type that we need to worry about the number of lines of
// text in it:
return;
}
if (mScrollingEnabled) {
// It is capable of scrolling so a "text overflow" is not a concern:
return;
}
const int linesSpare = mUpperPane->getRowCount() - lineCount;
if (linesSpare >= 0) {
// There IS space for all the lines
return;
}
// Else we do have an overflow situation so let's raise an event for it:
TEvent sysWindowOverflow{};
sysWindowOverflow.mArgumentList.append(QLatin1String("sysWindowOverflowEvent"));
sysWindowOverflow.mArgumentTypeList.append(ARGUMENT_TYPE_STRING);
sysWindowOverflow.mArgumentList.append(mConsoleName);
sysWindowOverflow.mArgumentTypeList.append(ARGUMENT_TYPE_STRING);
sysWindowOverflow.mArgumentList.append(QString::number(-linesSpare));
sysWindowOverflow.mArgumentTypeList.append(ARGUMENT_TYPE_NUMBER);
mpHost->raiseEvent(sysWindowOverflow);
}
void TConsole::clearSplit()
{
mLowerPane->mCursorY = buffer.size();
mLowerPane->hide();
buffer.mCursorY = buffer.size();
mUpperPane->mCursorY = buffer.size();
mUpperPane->mCursorX = 0;
mUpperPane->mIsTailMode = true;
mUpperPane->updateScreenView();
mUpperPane->forceUpdate();
}
void TConsole::raiseMudletResizeEvent()
{
// Hiding the TConsole - particularly the main one, multiview is not active
// and the profile is being switched away from causes a zero column count
// even though the TConsole is not actually resized - so don't raise the
// Mudlet TEvent in that case:
auto characterDimensions = QSize(mUpperPane->getColumnCount(), mUpperPane->getRowCount());
if (!characterDimensions.width()) {
return;
}
// Showing, Hiding and then Showing the console will produce three resize
// events - whilst the prior step will prevent this method from generating
// and event for the hiding one the two successive showing ones will
// still get to here - so we also need to check that there HAS been an
// actual change in the dimensions - and abort if there hasn't:
if (mDimensions == characterDimensions) {
return;
}
mDimensions = characterDimensions;
TEvent mudletEvent{};
mudletEvent.mArgumentList.append(qsl("sysConsoleSizeChanged"));
mudletEvent.mArgumentTypeList.append(ARGUMENT_TYPE_STRING);
mudletEvent.mArgumentList.append(mConsoleName);
mudletEvent.mArgumentTypeList.append(ARGUMENT_TYPE_STRING);
mudletEvent.mArgumentList.append(QString::number(characterDimensions.width()));
mudletEvent.mArgumentTypeList.append(ARGUMENT_TYPE_NUMBER);
mudletEvent.mArgumentList.append(QString::number(characterDimensions.height()));
mudletEvent.mArgumentTypeList.append(ARGUMENT_TYPE_NUMBER);
mudletEvent.mArgumentList.append(QString::number(mShowTimeStamps ? mudlet::smTimeStampFormat.size() : 0));
mudletEvent.mArgumentTypeList.append(ARGUMENT_TYPE_NUMBER);
mpHost->raiseEvent(mudletEvent);
}
void TConsole::slot_toggleTimeStamps(const bool state)
{
if (mShowTimeStamps == state) {
return;
}
mShowTimeStamps = state;
if (mType == TConsole::MainConsole) {
if (timeStampButton->isChecked() != state) {
// using this will NOT cause the QAbstractButton::checked signal
// to be raised - which is why we use that rather than the
// QAbstractButton::toggled one
timeStampButton->setChecked(state);
}
const auto filePath = mudlet::getMudletPath(enums::profileDataItemPath, mpHost->getName(), qsl("autotimestamp"));
QSaveFile file(filePath);
if (state) {
if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) {
qWarning() << "TConsole: failed to open autotimestamp file for writing:" << file.errorString();
return;
}
QTextStream out(&file);
if (!file.commit()) {
qDebug() << "TConsole::slot_toggleTimeStamps: error saving timestamp state: " << file.errorString();
}
} else {
QFile::remove(filePath);
}
}
// These hardly do anything now - just forces a redraw
mUpperPane->toggleTimeStamps(state);
mLowerPane->toggleTimeStamps(state);
if (mpHost && mType == TConsole::MainConsole) {
// Update and send out the NAWS data:
mpHost->updateDisplayDimensions();
}
if (mType & (TConsole::MainConsole | TConsole::UserWindow | TConsole::SubConsole)) {
raiseMudletResizeEvent();
}
}
void TConsole::slot_saveCommandSearchSettings()
{
if (!mpHost) {
return;
}
QSettings* pQSettings = mudlet::getQSettings();
if (!pQSettings) {
return;
}
pQSettings->setValue("commandSearchSplitterState", commandSplitter->saveState());
}
void TConsole::restoreCommandSearchSettings()
{
if (!mpHost) {
return;
}
QSettings* pQSettings = mudlet::getQSettings();
if (!pQSettings) {
return;
}
commandSplitter->restoreState(pQSettings->value("commandSearchSplitterState").toByteArray());
}
void TConsole::initializeOSC8StyleFeature()
{
if (!mpHyperlinkCompactManager) {
return;
}
// Register shorthands for style property names
mpHyperlinkCompactManager->registerShorthand(qsl("s"), qsl("style"));
mpHyperlinkCompactManager->registerShorthand(qsl("c"), qsl("color"));
mpHyperlinkCompactManager->registerShorthand(qsl("bg"), qsl("bg"));
mpHyperlinkCompactManager->registerShorthand(qsl("b"), qsl("bold"));
mpHyperlinkCompactManager->registerShorthand(qsl("i"), qsl("italic"));
mpHyperlinkCompactManager->registerShorthand(qsl("u"), qsl("underline"));
mpHyperlinkCompactManager->registerShorthand(qsl("o"), qsl("overline"));
mpHyperlinkCompactManager->registerShorthand(qsl("st"), qsl("strikethrough"));
mpHyperlinkCompactManager->registerShorthand(qsl("tdc"), qsl("text-decoration-color"));
mpHyperlinkCompactManager->registerShorthand(qsl("h"), qsl("hover"));
mpHyperlinkCompactManager->registerShorthand(qsl("a"), qsl("active"));
mpHyperlinkCompactManager->registerShorthand(qsl("f"), qsl("focus"));
mpHyperlinkCompactManager->registerShorthand(qsl("fv"), qsl("focus-visible"));
mpHyperlinkCompactManager->registerShorthand(qsl("vi"), qsl("visited"));
mpHyperlinkCompactManager->registerShorthand(qsl("l"), qsl("link"));
mpHyperlinkCompactManager->registerShorthand(qsl("al"), qsl("any-link"));
mpHyperlinkCompactManager->registerShorthand(qsl("sl"), qsl("selected"));
mpHyperlinkCompactManager->registerPresetProperty(qsl("style"));
}
void TConsole::initializeOSC8MenuFeature()
{
if (!mpHyperlinkCompactManager) {
return;
}
// Register shorthand for menu property
mpHyperlinkCompactManager->registerShorthand(qsl("m"), qsl("menu"));
mpHyperlinkCompactManager->registerPresetProperty(qsl("menu"));
}
void TConsole::initializeOSC8TooltipFeature()
{
if (!mpHyperlinkCompactManager) {
return;
}
// Register shorthand for tooltip property
mpHyperlinkCompactManager->registerShorthand(qsl("t"), qsl("tooltip"));
mpHyperlinkCompactManager->registerPresetProperty(qsl("tooltip"));
}
void TConsole::initializeOSC8VisibilityFeature()
{
if (!mpHyperlinkCompactManager) {
return;
}
// Register shorthand for visibility property
mpHyperlinkCompactManager->registerShorthand(qsl("v"), qsl("visibility"));
mpHyperlinkCompactManager->registerPresetProperty(qsl("visibility"));
}
void TConsole::initializeOSC8SelectionFeature()
{
if (!mpHyperlinkCompactManager) {
return;
}
// Register shorthand for selection property
mpHyperlinkCompactManager->registerShorthand(qsl("sel"), qsl("selection"));
mpHyperlinkCompactManager->registerPresetProperty(qsl("selection"));
}
void TConsole::initializeOSC8SpoilerFeature()
{
if (!mpHyperlinkCompactManager) {
return;
}
mpHyperlinkCompactManager->registerShorthand(qsl("sp"), qsl("spoiler"));
mpHyperlinkCompactManager->registerPresetProperty(qsl("spoiler"));
}
void TConsole::initializeOSC8DisabledFeature()
{
if (!mpHyperlinkCompactManager) {
return;
}
mpHyperlinkCompactManager->registerShorthand(qsl("d"), qsl("disabled"));
mpHyperlinkCompactManager->registerPresetProperty(qsl("disabled"));
}
void TConsole::initializeOSC8TitleFeature()
{
if (!mpHyperlinkCompactManager) {
return;
}
mpHyperlinkCompactManager->registerShorthand(qsl("ti"), qsl("title"));
mpHyperlinkCompactManager->registerPresetProperty(qsl("title"));
}