metaforce/hecl-gui/MainWindow.cpp

561 lines
18 KiB
C++
Raw Normal View History

2017-12-06 03:31:43 +00:00
#include "MainWindow.hpp"
#include "ui_MainWindow.h"
#include <QFontDatabase>
2018-01-03 23:53:01 +00:00
#include <QMessageBox>
2018-01-03 00:24:45 +00:00
#include "EscapeSequenceParser.hpp"
2017-12-06 03:31:43 +00:00
#include "FileDirDialog.hpp"
2018-01-10 06:19:48 +00:00
#include "ExtractZip.hpp"
2017-12-06 03:31:43 +00:00
2018-01-02 05:56:25 +00:00
#if _WIN32
#include <Windows.h>
#include <shellapi.h>
2018-01-10 06:19:48 +00:00
#include <TlHelp32.h>
static void KillProcessTree(QProcess& proc)
{
Q_PID pid = proc.pid();
if (!pid)
return;
DWORD myprocID = pid->dwProcessId;
PROCESSENTRY32 pe = {};
pe.dwSize = sizeof(PROCESSENTRY32);
HANDLE hSnap = ::CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if (::Process32First(hSnap, &pe))
{
BOOL bContinue = TRUE;
// kill child processes
while (bContinue)
{
// only kill child processes
if (pe.th32ParentProcessID == myprocID)
{
HANDLE hChildProc = ::OpenProcess(PROCESS_ALL_ACCESS, FALSE, pe.th32ProcessID);
if (hChildProc)
{
::TerminateProcess(hChildProc, 1);
::CloseHandle(hChildProc);
}
}
bContinue = ::Process32Next(hSnap, &pe);
}
}
proc.close();
proc.terminate();
}
#else
static void KillProcessTree(QProcess& proc)
{
proc.close();
proc.terminate();
}
2018-01-02 05:56:25 +00:00
#endif
2017-12-06 03:31:43 +00:00
MainWindow::MainWindow(QWidget *parent) :
2018-01-10 06:19:48 +00:00
QMainWindow(parent)
, m_fileMgr(_S("urde"))
, m_cvarManager(m_fileMgr)
, m_cvarCommons(m_cvarManager)
, m_ui(new Ui::MainWindow)
2017-12-06 03:31:43 +00:00
, m_heclProc(this)
, m_dlManager(this)
2018-01-10 06:19:48 +00:00
, m_launchMenu(m_cvarCommons, this)
, m_settings("AxioDL", "HECL", this)
2017-12-06 03:31:43 +00:00
{
m_ui->setupUi(this);
m_ui->heclTabs->setCurrentIndex(0);
m_ui->aboutIcon->setPixmap(QApplication::windowIcon().pixmap(256, 256));
QFont mFont = QFontDatabase::systemFont(QFontDatabase::FixedFont);
mFont.setPointSize(m_ui->currentBinaryLabel->font().pointSize());
m_ui->currentBinaryLabel->setFont(mFont);
m_ui->recommendedBinaryLabel->setFont(mFont);
mFont.setPointSize(10);
m_ui->processOutput->setFont(mFont);
2018-01-03 23:53:01 +00:00
m_cursor = QTextCursor(m_ui->processOutput->document());
m_updateURDEButton = new QPushButton(QStringLiteral("Update URDE"), m_ui->centralwidget);
m_ui->gridLayout->addWidget(m_updateURDEButton, 2, 3, 1, 1);
m_updateURDEButton->hide();
QPalette pal = m_updateURDEButton->palette();
pal.setColor(QPalette::Button, QColor(53,53,72));
m_updateURDEButton->setPalette(pal);
connect(m_updateURDEButton, SIGNAL(clicked()), this, SLOT(onUpdateURDEPressed()));
m_dlManager.connectWidgets(m_ui->downloadProgressBar, m_ui->downloadErrorLabel,
std::bind(&MainWindow::onIndexDownloaded, this, std::placeholders::_1),
std::bind(&MainWindow::onBinaryDownloaded, this, std::placeholders::_1),
2018-01-10 06:19:48 +00:00
std::bind(&MainWindow::onBinaryFailed, this));
2017-12-06 03:31:43 +00:00
initSlots();
m_dlManager.fetchIndex();
2018-01-02 04:21:45 +00:00
setPath(m_settings.value(QStringLiteral("working_dir")).toString());
2017-12-06 03:31:43 +00:00
}
MainWindow::~MainWindow()
{
2018-01-10 06:19:48 +00:00
KillProcessTree(m_heclProc);
2017-12-06 03:31:43 +00:00
delete m_ui;
}
void MainWindow::onExtract()
{
if (m_path.isEmpty())
return;
2018-01-03 23:53:01 +00:00
QString imgPath = QFileDialog::getOpenFileName(this, QStringLiteral("Extract Image"), m_path,
QStringLiteral("Images (*.iso *.wbfs *.gcm)"));
2018-01-03 23:53:01 +00:00
if (imgPath.isEmpty())
return;
m_ui->processOutput->clear();
2018-01-10 06:19:48 +00:00
KillProcessTree(m_heclProc);
m_heclProc.setProcessChannelMode(QProcess::ProcessChannelMode::MergedChannels);
m_heclProc.setWorkingDirectory(m_path);
2018-01-03 23:53:01 +00:00
QProcessEnvironment env = QProcessEnvironment::systemEnvironment();
env.insert("TERM", "xterm-color");
2018-01-10 06:19:48 +00:00
env.insert("ConEmuANSI", "ON");
2018-01-03 23:53:01 +00:00
m_heclProc.setProcessEnvironment(env);
2018-01-10 06:19:48 +00:00
disconnect(&m_heclProc, SIGNAL(finished(int)), nullptr, nullptr);
connect(&m_heclProc, SIGNAL(finished(int)), this, SLOT(onExtractFinished(int)));
m_heclProc.start(m_heclPath, {"extract", "-y", "-g", "-o", m_path, imgPath},
QIODevice::ReadOnly | QIODevice::Unbuffered);
m_ui->heclTabs->setCurrentIndex(0);
disableOperations();
m_ui->extractBtn->setText(QStringLiteral("Cancel"));
m_ui->extractBtn->setEnabled(true);
disconnect(m_ui->extractBtn, SIGNAL(clicked()), nullptr, nullptr);
connect(m_ui->extractBtn, SIGNAL(clicked()), this, SLOT(doHECLTerminate()));
}
void MainWindow::onExtractFinished(int returnCode)
{
2018-01-10 06:19:48 +00:00
m_cursor.movePosition(QTextCursor::End);
m_cursor.insertBlock();
disconnect(m_ui->extractBtn, SIGNAL(clicked()), nullptr, nullptr);
connect(m_ui->extractBtn, SIGNAL(clicked()), this, SLOT(onExtract()));
checkDownloadedBinary();
}
void MainWindow::onPackage()
{
if (m_path.isEmpty())
return;
m_ui->processOutput->clear();
2018-01-10 06:19:48 +00:00
KillProcessTree(m_heclProc);
m_heclProc.setProcessChannelMode(QProcess::ProcessChannelMode::MergedChannels);
m_heclProc.setWorkingDirectory(m_path);
2018-01-03 23:53:01 +00:00
QProcessEnvironment env = QProcessEnvironment::systemEnvironment();
env.insert("TERM", "xterm-color");
2018-01-10 06:19:48 +00:00
env.insert("ConEmuANSI", "ON");
2018-01-03 23:53:01 +00:00
m_heclProc.setProcessEnvironment(env);
2018-01-10 06:19:48 +00:00
disconnect(&m_heclProc, SIGNAL(finished(int)), nullptr, nullptr);
connect(&m_heclProc, SIGNAL(finished(int)), this, SLOT(onPackageFinished(int)));
m_heclProc.start(m_heclPath, {"package", "-y", "-g"},
QIODevice::ReadOnly | QIODevice::Unbuffered);
2017-12-06 03:31:43 +00:00
m_ui->heclTabs->setCurrentIndex(0);
disableOperations();
m_ui->packageBtn->setText(QStringLiteral("Cancel"));
m_ui->packageBtn->setEnabled(true);
disconnect(m_ui->packageBtn, SIGNAL(clicked()), nullptr, nullptr);
connect(m_ui->packageBtn, SIGNAL(clicked()), this, SLOT(doHECLTerminate()));
}
void MainWindow::onPackageFinished(int returnCode)
{
2018-01-10 06:19:48 +00:00
m_cursor.movePosition(QTextCursor::End);
m_cursor.insertBlock();
disconnect(m_ui->packageBtn, SIGNAL(clicked()), nullptr, nullptr);
connect(m_ui->packageBtn, SIGNAL(clicked()), this, SLOT(onPackage()));
checkDownloadedBinary();
}
void MainWindow::onLaunch()
{
if (m_path.isEmpty())
return;
m_ui->processOutput->clear();
2018-01-10 06:19:48 +00:00
KillProcessTree(m_heclProc);
m_heclProc.setProcessChannelMode(QProcess::ProcessChannelMode::MergedChannels);
m_heclProc.setWorkingDirectory(m_path);
2018-01-03 23:53:01 +00:00
QProcessEnvironment env = QProcessEnvironment::systemEnvironment();
env.insert("TERM", "xterm-color");
2018-01-10 06:19:48 +00:00
env.insert("ConEmuANSI", "ON");
2018-01-03 23:53:01 +00:00
m_heclProc.setProcessEnvironment(env);
2018-01-10 06:19:48 +00:00
disconnect(&m_heclProc, SIGNAL(finished(int)), nullptr, nullptr);
connect(&m_heclProc, SIGNAL(finished(int)), this, SLOT(onLaunchFinished(int)));
m_heclProc.start(m_urdePath, {"--no-shader-warmup", m_path + "/out"},
QIODevice::ReadOnly | QIODevice::Unbuffered);
m_ui->heclTabs->setCurrentIndex(0);
disableOperations();
}
void MainWindow::onLaunchFinished(int returnCode)
{
2018-01-10 06:19:48 +00:00
m_cursor.movePosition(QTextCursor::End);
m_cursor.insertBlock();
checkDownloadedBinary();
}
void MainWindow::doHECLTerminate()
{
2018-01-10 06:19:48 +00:00
KillProcessTree(m_heclProc);
2017-12-06 03:31:43 +00:00
}
void MainWindow::onReturnPressed()
{
2018-01-03 00:24:45 +00:00
if (sender() == m_ui->pathEdit)
setPath(m_ui->pathEdit->text());
2017-12-06 03:31:43 +00:00
}
void MainWindow::onIndexDownloaded(const QStringList& index)
{
int bestVersion = 0;
m_ui->binaryComboBox->clear();
for (const QString& str : index)
{
URDEVersion version(str);
if (m_ui->sysReqTable->willRun(version))
bestVersion = m_ui->binaryComboBox->count();
m_ui->binaryComboBox->addItem(version.fileString(false), QVariant::fromValue(version));
}
m_ui->binaryComboBox->setCurrentIndex(bestVersion);
m_recommendedVersion = m_ui->binaryComboBox->itemData(bestVersion).value<URDEVersion>();
m_ui->recommendedBinaryLabel->setText(m_recommendedVersion.fileString(false));
m_ui->binaryComboBox->setEnabled(true);
if (!m_path.isEmpty())
{
checkDownloadedBinary();
m_ui->downloadButton->setEnabled(true);
}
}
void MainWindow::onDownloadPressed()
{
m_updateURDEButton->hide();
QString filename = m_ui->binaryComboBox->currentData().value<URDEVersion>().fileString(true);
disableOperations();
m_ui->downloadButton->setEnabled(false);
m_dlManager.fetchBinary(filename, m_path + '/' + filename);
}
void MainWindow::onUpdateURDEPressed()
{
m_ui->heclTabs->setCurrentIndex(1);
onDownloadPressed();
}
2018-01-10 06:19:48 +00:00
void MainWindow::onBinaryDownloaded(QuaZip& file)
{
2018-01-10 06:19:48 +00:00
bool err = !ExtractZip::extractDir(file, m_path);
if (err)
2018-01-10 06:19:48 +00:00
m_ui->downloadErrorLabel->setText(QStringLiteral("Error extracting zip"));
else
m_ui->downloadErrorLabel->setText(QStringLiteral("Download successful"), true);
2018-01-10 06:19:48 +00:00
m_ui->downloadButton->setEnabled(true);
checkDownloadedBinary();
2018-01-10 06:19:48 +00:00
if (!err && m_ui->extractBtn->isEnabled())
m_ui->downloadErrorLabel->setText(QStringLiteral("Download successful - Press 'Extract' to continue."), true);
2018-01-10 06:19:48 +00:00
if (!err && !m_ui->sysReqTable->isBlenderVersionOk())
m_ui->downloadErrorLabel->setText(
QStringLiteral("Blender 2.78+ must be installed. Please download via Steam or blender.org."));
}
2018-01-10 06:19:48 +00:00
void MainWindow::onBinaryFailed()
{
m_ui->downloadButton->setEnabled(true);
checkDownloadedBinary();
}
void MainWindow::disableOperations()
{
m_ui->extractBtn->setEnabled(false);
m_ui->packageBtn->setEnabled(false);
m_ui->launchBtn->setEnabled(false);
2018-01-03 23:53:01 +00:00
m_ui->pathEdit->setEnabled(false);
m_ui->browseBtn->setEnabled(false);
m_ui->downloadButton->setEnabled(false);
}
void MainWindow::enableOperations()
{
disableOperations();
2018-01-03 23:53:01 +00:00
m_ui->pathEdit->setEnabled(true);
m_ui->browseBtn->setEnabled(true);
if (m_path.isEmpty())
return;
2018-01-03 23:53:01 +00:00
m_ui->downloadButton->setEnabled(true);
2018-01-10 06:19:48 +00:00
if (m_heclPath.isEmpty())
return;
m_ui->extractBtn->setText(QStringLiteral("Extract"));
m_ui->packageBtn->setText(QStringLiteral("Package"));
m_ui->launchBtn->setText(QStringLiteral("Launch"));
m_ui->extractBtn->setEnabled(true);
if (QFile::exists(m_path + "/MP1/!original_ids.yaml"))
{
m_ui->packageBtn->setEnabled(true);
if (isPackageComplete())
m_ui->launchBtn->setEnabled(true);
}
2018-01-10 06:19:48 +00:00
if (!m_ui->sysReqTable->isBlenderVersionOk())
insertContinueNote("Blender 2.78+ must be installed. Please download via Steam or blender.org.");
else if (m_ui->launchBtn->isEnabled())
insertContinueNote("Package complete - Press 'Launch' to start URDE.");
else if (m_ui->packageBtn->isEnabled())
insertContinueNote("Extract complete - Press 'Package' to continue.");
else if (m_ui->extractBtn->isEnabled())
insertContinueNote("Press 'Extract' to begin.");
}
bool MainWindow::isPackageComplete() const
{
return
QFile::exists(m_path + "/out/MP1/!original_ids.upak") &&
QFile::exists(m_path + "/out/MP1/AudioGrp.upak") &&
QFile::exists(m_path + "/out/MP1/GGuiSys.upak") &&
QFile::exists(m_path + "/out/MP1/Metroid1.upak") &&
QFile::exists(m_path + "/out/MP1/Metroid2.upak") &&
QFile::exists(m_path + "/out/MP1/Metroid3.upak") &&
QFile::exists(m_path + "/out/MP1/Metroid4.upak") &&
QFile::exists(m_path + "/out/MP1/metroid5.upak") &&
QFile::exists(m_path + "/out/MP1/Metroid6.upak") &&
QFile::exists(m_path + "/out/MP1/Metroid7.upak") &&
QFile::exists(m_path + "/out/MP1/Metroid8.upak") &&
QFile::exists(m_path + "/out/MP1/MidiData.upak") &&
QFile::exists(m_path + "/out/MP1/MiscData.upak") &&
QFile::exists(m_path + "/out/MP1/NoARAM.upak") &&
QFile::exists(m_path + "/out/MP1/SamGunFx.upak") &&
QFile::exists(m_path + "/out/MP1/SamusGun.upak") &&
QFile::exists(m_path + "/out/MP1/SlideShow.upak") &&
QFile::exists(m_path + "/out/MP1/TestAnim.upak") &&
QFile::exists(m_path + "/out/MP1/Tweaks.upak");
}
static bool GetDLPackage(const QString& path, QString& dlPackage)
{
QProcess proc;
proc.start(path, {"--dlpackage"}, QIODevice::ReadOnly);
if (proc.waitForStarted())
{
proc.waitForFinished();
if (proc.exitCode() == 100)
dlPackage = QString::fromUtf8(proc.readLine()).trimmed();
return true;
}
return false;
}
bool MainWindow::checkDownloadedBinary()
{
m_updateURDEButton->hide();
m_urdePath = QString();
m_heclPath = QString();
if (m_path.isEmpty())
2018-01-02 04:21:45 +00:00
{
m_ui->heclTabs->setCurrentIndex(1);
m_ui->downloadErrorLabel->setText(QStringLiteral("Set working directory to continue."), true);
2018-01-10 06:19:48 +00:00
enableOperations();
return false;
2018-01-02 04:21:45 +00:00
}
#if __APPLE__
QString urdePath = m_path + "/URDE.app/Contents/MacOS/urde";
QString heclPath = m_path + "/URDE.app/Contents/MacOS/hecl";
QString visigenPath = m_path + "/URDE.app/Contents/MacOS/visigen";
#elif _WIN32
QString urdePath = m_path + "/urde.exe";
QString heclPath = m_path + "/hecl.exe";
QString visigenPath = m_path + "/visigen.exe";
#else
QString urdePath = m_path + "/urde";
QString heclPath = m_path + "/hecl";
QString visigenPath = m_path + "/visigen";
#endif
urdePath = QFileInfo(urdePath).absoluteFilePath();
heclPath = QFileInfo(heclPath).absoluteFilePath();
2018-01-02 01:12:45 +00:00
visigenPath = QFileInfo(visigenPath).absoluteFilePath();
QString urdeDlPackage, heclDlPackage, visigenDlPackage;
if (GetDLPackage(urdePath, urdeDlPackage) &&
GetDLPackage(heclPath, heclDlPackage) &&
GetDLPackage(visigenPath, visigenDlPackage))
{
if (!urdeDlPackage.isEmpty() &&
urdeDlPackage == heclDlPackage &&
urdeDlPackage == visigenDlPackage)
{
URDEVersion v(urdeDlPackage);
m_ui->currentBinaryLabel->setText(v.fileString(false));
if (m_recommendedVersion.isValid() && v.isValid() &&
m_recommendedVersion.getVersion() > v.getVersion())
{
m_updateURDEButton->show();
}
}
else
{
2018-01-05 03:02:07 +00:00
m_ui->currentBinaryLabel->setText(QStringLiteral("unknown -- re-download recommended"));
}
m_urdePath = urdePath;
m_heclPath = heclPath;
2018-01-10 07:39:48 +00:00
m_ui->downloadErrorLabel->setText({}, true);
2018-01-10 06:19:48 +00:00
enableOperations();
return true;
}
else
{
m_ui->currentBinaryLabel->setText(QStringLiteral("none"));
m_ui->heclTabs->setCurrentIndex(1);
m_ui->downloadErrorLabel->setText(QStringLiteral("Press 'Download' to fetch latest URDE binary."), true);
2018-01-10 06:19:48 +00:00
enableOperations();
}
return false;
}
void MainWindow::setPath(const QString& path)
{
2018-01-03 23:53:01 +00:00
QFileInfo finfo(path);
2018-01-10 07:39:48 +00:00
QString usePath;
if (!path.isEmpty())
usePath = finfo.absoluteFilePath();
2018-01-10 06:19:48 +00:00
if (!usePath.isEmpty() && !finfo.exists())
2018-01-03 23:53:01 +00:00
{
if (QMessageBox::question(this, QStringLiteral("Make Directory"),
QStringLiteral("%1 does not exist. Create it now?").arg(usePath)) == QMessageBox::Yes)
QDir().mkpath(usePath);
else
usePath = QString();
}
if (!usePath.isEmpty() && !finfo.isDir())
{
QMessageBox::warning(this, QStringLiteral("Directory Error"),
QStringLiteral("%1 is not a directory").arg(usePath),
QMessageBox::Ok, QMessageBox::NoButton);
usePath = QString();
}
m_path = usePath;
m_settings.setValue(QStringLiteral("working_dir"), m_path);
2018-01-03 23:53:01 +00:00
if (!m_path.isEmpty())
{
m_ui->pathEdit->setText(m_path);
m_ui->downloadButton->setToolTip(QString());
m_ui->downloadButton->setEnabled(m_ui->binaryComboBox->isEnabled());
}
else
{
m_ui->downloadButton->setToolTip(QStringLiteral("Working directory must be set"));
m_ui->downloadButton->setEnabled(false);
m_ui->currentBinaryLabel->setText(QStringLiteral("none"));
}
2018-01-10 06:19:48 +00:00
m_ui->sysReqTable->updateFreeDiskSpace(m_path);
checkDownloadedBinary();
}
2017-12-06 03:31:43 +00:00
void MainWindow::initSlots()
{
connect(&m_heclProc, &QProcess::readyRead, [=](){
2018-01-10 06:19:48 +00:00
QByteArray bytes = m_heclProc.readAll();
setTextTermFormatting(bytes);
2017-12-06 03:31:43 +00:00
});
connect(m_ui->extractBtn, SIGNAL(clicked()), this, SLOT(onExtract()));
connect(m_ui->packageBtn, SIGNAL(clicked()), this, SLOT(onPackage()));
connect(m_ui->launchBtn, SIGNAL(clicked()), this, SLOT(onLaunch()));
2018-01-10 06:19:48 +00:00
m_ui->launchMenuBtn->setMenu(&m_launchMenu);
2017-12-06 03:31:43 +00:00
connect(m_ui->browseBtn, &QPushButton::clicked, [=]() {
FileDirDialog dialog(this);
2018-01-03 23:53:01 +00:00
dialog.setDirectory(m_path);
2017-12-27 04:10:44 +00:00
dialog.setWindowTitle("Select Working Directory");
2018-01-03 23:53:01 +00:00
int res = dialog.exec();
2017-12-06 03:31:43 +00:00
if (res == QFileDialog::Rejected)
return;
if (dialog.selectedFiles().size() <= 0)
return;
setPath(dialog.selectedFiles().at(0));
2017-12-06 03:31:43 +00:00
});
connect(m_ui->downloadButton, SIGNAL(clicked()), this, SLOT(onDownloadPressed()));
2017-12-06 03:31:43 +00:00
}
void MainWindow::setTextTermFormatting(const QString& text)
{
m_inContinueNote = false;
2018-01-03 23:53:01 +00:00
QRegExp const escapeSequenceExpression(R"(\x1B\[([\d;\?]+)([mlh]))");
QTextCharFormat defaultTextCharFormat = m_cursor.charFormat();
2017-12-06 03:31:43 +00:00
int offset = escapeSequenceExpression.indexIn(text);
2018-01-03 23:53:01 +00:00
ReturnInsert(m_cursor, text.mid(0, offset));
2017-12-06 03:31:43 +00:00
QTextCharFormat textCharFormat = defaultTextCharFormat;
2018-01-03 23:53:01 +00:00
while (offset >= 0) {
2017-12-06 03:31:43 +00:00
int previousOffset = offset + escapeSequenceExpression.matchedLength();
2018-01-03 23:53:01 +00:00
if (escapeSequenceExpression.capturedTexts()[2] == "m")
{
QStringList capturedTexts = escapeSequenceExpression.capturedTexts()[1].split(';');
QListIterator<QString> i(capturedTexts);
while (i.hasNext()) {
bool ok = false;
int attribute = i.next().toInt(&ok);
Q_ASSERT(ok);
ParseEscapeSequence(attribute, i, textCharFormat, defaultTextCharFormat);
}
2017-12-06 03:31:43 +00:00
}
offset = escapeSequenceExpression.indexIn(text, previousOffset);
if (offset < 0) {
2018-01-03 23:53:01 +00:00
ReturnInsert(m_cursor, text.mid(previousOffset), textCharFormat);
2017-12-06 03:31:43 +00:00
} else {
2018-01-03 23:53:01 +00:00
ReturnInsert(m_cursor, text.mid(previousOffset, offset - previousOffset), textCharFormat);
2017-12-06 03:31:43 +00:00
}
}
2018-01-03 23:53:01 +00:00
m_cursor.setCharFormat(defaultTextCharFormat);
m_ui->processOutput->ensureCursorVisible();
}
void MainWindow::insertContinueNote(const QString& text)
{
if (m_inContinueNote)
return;
m_inContinueNote = true;
2018-01-03 23:53:01 +00:00
m_cursor.movePosition(QTextCursor::End);
QTextCharFormat textCharFormat = m_cursor.charFormat();
textCharFormat.setForeground(QColor(0,255,0));
2018-01-10 06:19:48 +00:00
m_cursor.insertText(text, textCharFormat);
m_cursor.insertBlock();
m_ui->processOutput->ensureCursorVisible();
2017-12-06 03:31:43 +00:00
}