/* 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 . */ #include "LogPage.h" #include "ui_LogPage.h" #include "Application.h" #include #include #include #include "launch/LaunchTask.h" #include "settings/Setting.h" #include "ui/GuiUtil.h" #include "ui/ColorCache.h" #include 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 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 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 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(); } }