diff options
Diffstat (limited to 'meshmc/launcher/ui/pages/instance/LogPage.cpp')
| -rw-r--r-- | meshmc/launcher/ui/pages/instance/LogPage.cpp | 337 |
1 files changed, 337 insertions, 0 deletions
diff --git a/meshmc/launcher/ui/pages/instance/LogPage.cpp b/meshmc/launcher/ui/pages/instance/LogPage.cpp new file mode 100644 index 0000000000..8f4f4c11a4 --- /dev/null +++ b/meshmc/launcher/ui/pages/instance/LogPage.cpp @@ -0,0 +1,337 @@ +/* SPDX-FileCopyrightText: 2026 Project Tick + * SPDX-FileContributor: Project Tick + * SPDX-License-Identifier: GPL-3.0-or-later + * + * MeshMC - A Custom Launcher for Minecraft + * Copyright (C) 2026 Project Tick + * + * 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 3 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, see <https://www.gnu.org/licenses/>. + */ + +#include "LogPage.h" +#include "ui_LogPage.h" + +#include "Application.h" + +#include <QIcon> +#include <QScrollBar> +#include <QShortcut> + +#include "launch/LaunchTask.h" +#include "settings/Setting.h" + +#include "ui/GuiUtil.h" +#include "ui/ColorCache.h" + +#include <BuildConfig.h> + +class LogFormatProxyModel : public QIdentityProxyModel +{ + public: + LogFormatProxyModel(QObject* parent = nullptr) : QIdentityProxyModel(parent) + { + } + QVariant data(const QModelIndex& index, int role) const override + { + switch (role) { + case Qt::FontRole: + return m_font; + case Qt::ForegroundRole: { + MessageLevel::Enum level = + (MessageLevel::Enum)QIdentityProxyModel::data( + index, LogModel::LevelRole) + .toInt(); + return m_colors->getFront(level); + } + case Qt::BackgroundRole: { + MessageLevel::Enum level = + (MessageLevel::Enum)QIdentityProxyModel::data( + index, LogModel::LevelRole) + .toInt(); + return m_colors->getBack(level); + } + default: + return QIdentityProxyModel::data(index, role); + } + } + + void setFont(QFont font) + { + m_font = font; + } + + void setColors(LogColorCache* colors) + { + m_colors.reset(colors); + } + + QModelIndex find(const QModelIndex& start, const QString& value, + bool reverse) const + { + QModelIndex parentIndex = parent(start); + auto compare = [&](int r) -> QModelIndex { + QModelIndex idx = index(r, start.column(), parentIndex); + if (!idx.isValid() || idx == start) { + return QModelIndex(); + } + QVariant v = data(idx, Qt::DisplayRole); + QString t = v.toString(); + if (t.contains(value, Qt::CaseInsensitive)) + return idx; + return QModelIndex(); + }; + if (reverse) { + int from = start.row(); + int to = 0; + + for (int i = 0; i < 2; ++i) { + for (int r = from; (r >= to); --r) { + auto idx = compare(r); + if (idx.isValid()) + return idx; + } + // prepare for the next iteration + from = rowCount() - 1; + to = start.row(); + } + } else { + int from = start.row(); + int to = rowCount(parentIndex); + + for (int i = 0; i < 2; ++i) { + for (int r = from; (r < to); ++r) { + auto idx = compare(r); + if (idx.isValid()) + return idx; + } + // prepare for the next iteration + from = 0; + to = start.row(); + } + } + return QModelIndex(); + } + + private: + QFont m_font; + std::unique_ptr<LogColorCache> m_colors; +}; + +LogPage::LogPage(InstancePtr instance, QWidget* parent) + : QWidget(parent), ui(new Ui::LogPage), m_instance(instance) +{ + ui->setupUi(this); + ui->tabWidget->tabBar()->hide(); + + m_proxy = new LogFormatProxyModel(this); + // set up text colors in the log proxy and adapt them to the current theme + // foreground and background + { + auto origForeground = + ui->text->palette().color(ui->text->foregroundRole()); + auto origBackground = + ui->text->palette().color(ui->text->backgroundRole()); + m_proxy->setColors(new LogColorCache(origForeground, origBackground)); + } + + // set up fonts in the log proxy + { + QString fontFamily = + APPLICATION->settings()->get("ConsoleFont").toString(); + bool conversionOk = false; + int fontSize = APPLICATION->settings() + ->get("ConsoleFontSize") + .toInt(&conversionOk); + if (!conversionOk) { + fontSize = 11; + } + m_proxy->setFont(QFont(fontFamily, fontSize)); + } + + ui->text->setModel(m_proxy); + + // set up instance and launch process recognition + { + auto launchTask = m_instance->getLaunchTask(); + if (launchTask) { + setInstanceLaunchTaskChanged(launchTask, true); + } + connect(m_instance.get(), &BaseInstance::launchTaskChanged, this, + &LogPage::onInstanceLaunchTaskChanged); + } + + auto findShortcut = new QShortcut(QKeySequence(QKeySequence::Find), this); + connect(findShortcut, SIGNAL(activated()), SLOT(findActivated())); + auto findNextShortcut = + new QShortcut(QKeySequence(QKeySequence::FindNext), this); + connect(findNextShortcut, SIGNAL(activated()), SLOT(findNextActivated())); + connect(ui->searchBar, SIGNAL(returnPressed()), + SLOT(on_findButton_clicked())); + auto findPreviousShortcut = + new QShortcut(QKeySequence(QKeySequence::FindPrevious), this); + connect(findPreviousShortcut, SIGNAL(activated()), + SLOT(findPreviousActivated())); +} + +LogPage::~LogPage() +{ + delete ui; +} + +void LogPage::modelStateToUI() +{ + if (m_model->wrapLines()) { + ui->text->setWordWrap(true); + ui->wrapCheckbox->setCheckState(Qt::Checked); + } else { + ui->text->setWordWrap(false); + ui->wrapCheckbox->setCheckState(Qt::Unchecked); + } + if (m_model->suspended()) { + ui->trackLogCheckbox->setCheckState(Qt::Unchecked); + } else { + ui->trackLogCheckbox->setCheckState(Qt::Checked); + } +} + +void LogPage::UIToModelState() +{ + if (!m_model) { + return; + } + m_model->setLineWrap(ui->wrapCheckbox->checkState() == Qt::Checked); + m_model->suspend(ui->trackLogCheckbox->checkState() != Qt::Checked); +} + +void LogPage::setInstanceLaunchTaskChanged(shared_qobject_ptr<LaunchTask> proc, + bool initial) +{ + m_process = proc; + if (m_process) { + m_model = proc->getLogModel(); + m_proxy->setSourceModel(m_model.get()); + if (initial) { + modelStateToUI(); + } else { + UIToModelState(); + } + } else { + m_proxy->setSourceModel(nullptr); + m_model.reset(); + } +} + +void LogPage::onInstanceLaunchTaskChanged(shared_qobject_ptr<LaunchTask> proc) +{ + setInstanceLaunchTaskChanged(proc, false); +} + +bool LogPage::apply() +{ + return true; +} + +bool LogPage::shouldDisplay() const +{ + return m_instance->isRunning() || m_proxy->rowCount() > 0; +} + +void LogPage::on_btnPaste_clicked() +{ + if (!m_model) + return; + + // FIXME: turn this into a proper task and move the upload logic out of + // GuiUtil! + m_model->append( + MessageLevel::MeshMC, + QString("%2: Log upload triggered at: %1") + .arg(QDateTime::currentDateTime().toString(Qt::RFC2822Date), + BuildConfig.MESHMC_NAME)); + auto url = GuiUtil::uploadPaste(m_model->toPlainText(), this); + if (!url.isEmpty()) { + m_model->append(MessageLevel::MeshMC, + QString("%2: Log uploaded to: %1") + .arg(url, BuildConfig.MESHMC_NAME)); + } else { + m_model->append( + MessageLevel::Error, + QString("%1: Log upload failed!").arg(BuildConfig.MESHMC_NAME)); + } +} + +void LogPage::on_btnCopy_clicked() +{ + if (!m_model) + return; + m_model->append( + MessageLevel::MeshMC, + QString("Clipboard copy at: %1") + .arg(QDateTime::currentDateTime().toString(Qt::RFC2822Date))); + GuiUtil::setClipboardText(m_model->toPlainText()); +} + +void LogPage::on_btnClear_clicked() +{ + if (!m_model) + return; + m_model->clear(); + m_container->refreshContainer(); +} + +void LogPage::on_btnBottom_clicked() +{ + ui->text->scrollToBottom(); +} + +void LogPage::on_trackLogCheckbox_clicked(bool checked) +{ + if (!m_model) + return; + m_model->suspend(!checked); +} + +void LogPage::on_wrapCheckbox_clicked(bool checked) +{ + ui->text->setWordWrap(checked); + if (!m_model) + return; + m_model->setLineWrap(checked); +} + +void LogPage::on_findButton_clicked() +{ + auto modifiers = QApplication::keyboardModifiers(); + bool reverse = modifiers & Qt::ShiftModifier; + ui->text->findNext(ui->searchBar->text(), reverse); +} + +void LogPage::findNextActivated() +{ + ui->text->findNext(ui->searchBar->text(), false); +} + +void LogPage::findPreviousActivated() +{ + ui->text->findNext(ui->searchBar->text(), true); +} + +void LogPage::findActivated() +{ + // focus the search bar if it doesn't have focus + if (!ui->searchBar->hasFocus()) { + ui->searchBar->setFocus(); + ui->searchBar->selectAll(); + } +} |
