From c58cc3396ab5b4b6e92ab2c74774d32490544802 Mon Sep 17 00:00:00 2001 From: Yihe Li Date: Tue, 3 Jun 2025 04:41:48 +0800 Subject: [PATCH 1/7] Basic support for launcher log page Signed-off-by: Yihe Li --- launcher/Application.cpp | 20 +- launcher/Application.h | 2 + launcher/CMakeLists.txt | 3 + launcher/MessageLevel.cpp | 18 ++ launcher/MessageLevel.h | 2 + launcher/minecraft/mod/ResourcePack.h | 2 +- launcher/ui/pages/global/LauncherLogPage.cpp | 293 +++++++++++++++++++ launcher/ui/pages/global/LauncherLogPage.h | 99 +++++++ launcher/ui/pages/global/LauncherLogPage.ui | 193 ++++++++++++ launcher/ui/pages/instance/LogPage.cpp | 76 ----- launcher/ui/pages/instance/LogPage.h | 14 +- 11 files changed, 631 insertions(+), 91 deletions(-) create mode 100644 launcher/ui/pages/global/LauncherLogPage.cpp create mode 100644 launcher/ui/pages/global/LauncherLogPage.h create mode 100644 launcher/ui/pages/global/LauncherLogPage.ui diff --git a/launcher/Application.cpp b/launcher/Application.cpp index b683b8ab8..e3018db0d 100644 --- a/launcher/Application.cpp +++ b/launcher/Application.cpp @@ -63,6 +63,7 @@ #include "ui/pages/global/ExternalToolsPage.h" #include "ui/pages/global/JavaPage.h" #include "ui/pages/global/LanguagePage.h" +#include "ui/pages/global/LauncherLogPage.h" #include "ui/pages/global/LauncherPage.h" #include "ui/pages/global/MinecraftPage.h" #include "ui/pages/global/ProxyPage.h" @@ -244,8 +245,11 @@ void appDebugOutput(QtMsgType type, const QMessageLogContext& context, const QSt } QString out = qFormatLogMessage(type, context, msg); - out += QChar::LineFeed; + if (APPLICATION->logModel) { + APPLICATION->logModel->append(MessageLevel::getLevel(type), out); + } + out += QChar::LineFeed; APPLICATION->logFile->write(out.toUtf8()); APPLICATION->logFile->flush(); @@ -538,6 +542,8 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv) qInstallMessageHandler(appDebugOutput); qSetMessagePattern(defaultLogFormat); + logModel.reset(new LogModel(this)); + bool foundLoggingRules = false; auto logRulesFile = QStringLiteral("qtlogging.ini"); @@ -691,6 +697,17 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv) m_settings->registerSetting("ConsoleMaxLines", 100000); m_settings->registerSetting("ConsoleOverflowStop", true); + auto lineSetting = settings()->getSetting("ConsoleMaxLines"); + bool conversionOk = false; + int maxLines = lineSetting->get().toInt(&conversionOk); + if (!conversionOk) { + maxLines = lineSetting->defValue().toInt(); + qWarning() << "ConsoleMaxLines has nonsensical value, defaulting to" << maxLines; + } + logModel->setMaxLines(maxLines); + logModel->setStopOnOverflow(settings()->get("ConsoleOverflowStop").toBool()); + logModel->setOverflowMessage(tr("Cannot display this log since the log length surpassed %1 lines.").arg(maxLines)); + // Folders m_settings->registerSetting("InstanceDir", "instances"); m_settings->registerSetting({ "CentralModsDir", "ModsDir" }, "mods"); @@ -895,6 +912,7 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv) m_globalSettingsProvider->addPage(); m_globalSettingsProvider->addPage(); m_globalSettingsProvider->addPage(); + m_globalSettingsProvider->addPage(); } PixmapCache::setInstance(new PixmapCache(this)); diff --git a/launcher/Application.h b/launcher/Application.h index 2daf6ef35..548345c18 100644 --- a/launcher/Application.h +++ b/launcher/Application.h @@ -48,6 +48,7 @@ #include +#include "launch/LogModel.h" #include "minecraft/launch/MinecraftTarget.h" class LaunchController; @@ -307,6 +308,7 @@ class Application : public QApplication { QList m_urlsToImport; QString m_instanceIdToShowWindowOf; std::unique_ptr logFile; + shared_qobject_ptr logModel; public: void addQSavePath(QString); diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt index a7ccb809d..cd9903067 100644 --- a/launcher/CMakeLists.txt +++ b/launcher/CMakeLists.txt @@ -959,6 +959,8 @@ SET(LAUNCHER_SOURCES ui/pages/global/MinecraftPage.h ui/pages/global/LauncherPage.cpp ui/pages/global/LauncherPage.h + ui/pages/global/LauncherLogPage.cpp + ui/pages/global/LauncherLogPage.h ui/pages/global/AppearancePage.h ui/pages/global/ProxyPage.cpp ui/pages/global/ProxyPage.h @@ -1203,6 +1205,7 @@ qt_wrap_ui(LAUNCHER_UI ui/pages/global/AccountListPage.ui ui/pages/global/JavaPage.ui ui/pages/global/LauncherPage.ui + ui/pages/global/LauncherLogPage.ui ui/pages/global/APIPage.ui ui/pages/global/ProxyPage.ui ui/pages/global/ExternalToolsPage.ui diff --git a/launcher/MessageLevel.cpp b/launcher/MessageLevel.cpp index 2bd6ecc00..3516dbdd6 100644 --- a/launcher/MessageLevel.cpp +++ b/launcher/MessageLevel.cpp @@ -25,6 +25,24 @@ MessageLevel::Enum MessageLevel::getLevel(const QString& levelName) return MessageLevel::Unknown; } +MessageLevel::Enum MessageLevel::getLevel(QtMsgType type) +{ + switch (type) { + case QtDebugMsg: + return MessageLevel::Debug; + case QtInfoMsg: + return MessageLevel::Info; + case QtWarningMsg: + return MessageLevel::Warning; + case QtCriticalMsg: + return MessageLevel::Error; + case QtFatalMsg: + return MessageLevel::Fatal; + default: + return MessageLevel::Unknown; + } +} + MessageLevel::Enum MessageLevel::fromLine(QString& line) { // Level prefix diff --git a/launcher/MessageLevel.h b/launcher/MessageLevel.h index 321af9d92..794e2ac39 100644 --- a/launcher/MessageLevel.h +++ b/launcher/MessageLevel.h @@ -1,6 +1,7 @@ #pragma once #include +#include /** * @brief the MessageLevel Enum @@ -21,6 +22,7 @@ enum Enum { Fatal, /**< Fatal Errors */ }; MessageLevel::Enum getLevel(const QString& levelName); +MessageLevel::Enum getLevel(QtMsgType type); /* Get message level from a line. Line is modified if it was successful. */ MessageLevel::Enum fromLine(QString& line); diff --git a/launcher/minecraft/mod/ResourcePack.h b/launcher/minecraft/mod/ResourcePack.h index f214bedf2..bd161df87 100644 --- a/launcher/minecraft/mod/ResourcePack.h +++ b/launcher/minecraft/mod/ResourcePack.h @@ -24,5 +24,5 @@ class ResourcePack : public DataPack { /** Gets, respectively, the lower and upper versions supported by the set pack format. */ [[nodiscard]] std::pair compatibleVersions() const override; - virtual QString directory() { return "/assets"; } + QString directory() override { return "/assets"; } }; diff --git a/launcher/ui/pages/global/LauncherLogPage.cpp b/launcher/ui/pages/global/LauncherLogPage.cpp new file mode 100644 index 000000000..62c866b75 --- /dev/null +++ b/launcher/ui/pages/global/LauncherLogPage.cpp @@ -0,0 +1,293 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2022 Jamie Mansfield + * Copyright (C) 2022 Sefa Eyeoglu + * Copyright (C) 2024 TheKodeToad + * Copyright (c) 2025 Yihe Li + * + * 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, version 3. + * + * 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 . + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "LauncherLogPage.h" +#include "ui_LauncherLogPage.h" + +#include "Application.h" + +#include +#include +#include + +#include "launch/LaunchTask.h" +#include "settings/Setting.h" + +#include "ui/GuiUtil.h" +#include "ui/themes/ThemeManager.h" + +#include + +QVariant LogFormatProxyModel::data(const QModelIndex& index, int role) const +{ + const LogColors& colors = APPLICATION->themeManager()->getLogColors(); + + switch (role) { + case Qt::FontRole: + return m_font; + case Qt::ForegroundRole: { + auto level = static_cast(QIdentityProxyModel::data(index, LogModel::LevelRole).toInt()); + QColor result = colors.foreground.value(level); + + if (result.isValid()) + return result; + + break; + } + case Qt::BackgroundRole: { + auto level = static_cast(QIdentityProxyModel::data(index, LogModel::LevelRole).toInt()); + QColor result = colors.background.value(level); + + if (result.isValid()) + return result; + + break; + } + } + + return QIdentityProxyModel::data(index, role); +} + +QModelIndex LogFormatProxyModel::find(const QModelIndex& start, const QString& value, bool reverse) const +{ + QModelIndex parentIndex = parent(start); + auto compare = [this, start, parentIndex, value](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(); +} + +LauncherLogPage::LauncherLogPage(QWidget* parent) : QWidget(parent), ui(new Ui::LauncherLogPage) +{ + ui->setupUi(this); + ui->tabWidget->tabBar()->hide(); + + m_proxy = new LogFormatProxyModel(this); + + // 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); + m_proxy->setSourceModel(APPLICATION->logModel.get()); + modelStateToUI(); + + 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())); +} + +LauncherLogPage::~LauncherLogPage() +{ + delete ui; +} + +void LauncherLogPage::modelStateToUI() +{ + if (APPLICATION->logModel->wrapLines()) { + ui->text->setWordWrap(true); + ui->wrapCheckbox->setCheckState(Qt::Checked); + } else { + ui->text->setWordWrap(false); + ui->wrapCheckbox->setCheckState(Qt::Unchecked); + } + if (APPLICATION->logModel->colorLines()) { + ui->text->setColorLines(true); + ui->colorCheckbox->setCheckState(Qt::Checked); + } else { + ui->text->setColorLines(false); + ui->colorCheckbox->setCheckState(Qt::Unchecked); + } + if (APPLICATION->logModel->suspended()) { + ui->trackLogCheckbox->setCheckState(Qt::Unchecked); + } else { + ui->trackLogCheckbox->setCheckState(Qt::Checked); + } +} + +void LauncherLogPage::UIToModelState() +{ + if (!APPLICATION->logModel) { + return; + } + APPLICATION->logModel->setLineWrap(ui->wrapCheckbox->checkState() == Qt::Checked); + APPLICATION->logModel->setColorLines(ui->colorCheckbox->checkState() == Qt::Checked); + APPLICATION->logModel->suspend(ui->trackLogCheckbox->checkState() != Qt::Checked); +} + +void LauncherLogPage::on_btnPaste_clicked() +{ + if (!APPLICATION->logModel) + return; + + // FIXME: turn this into a proper task and move the upload logic out of GuiUtil! + APPLICATION->logModel->append(MessageLevel::Launcher, + QString("Log upload triggered at: %1").arg(QDateTime::currentDateTime().toString(Qt::RFC2822Date))); + auto url = GuiUtil::uploadPaste(tr("Launcher Log"), APPLICATION->logModel->toPlainText(), this); + if (!url.has_value()) { + APPLICATION->logModel->append(MessageLevel::Error, QString("Log upload canceled")); + } else if (url->isNull()) { + APPLICATION->logModel->append(MessageLevel::Error, QString("Log upload failed!")); + } else { + APPLICATION->logModel->append(MessageLevel::Launcher, QString("Log uploaded to: %1").arg(url.value())); + } +} + +void LauncherLogPage::on_btnCopy_clicked() +{ + if (!APPLICATION->logModel) + return; + APPLICATION->logModel->append(MessageLevel::Launcher, + QString("Clipboard copy at: %1").arg(QDateTime::currentDateTime().toString(Qt::RFC2822Date))); + GuiUtil::setClipboardText(APPLICATION->logModel->toPlainText()); +} + +void LauncherLogPage::on_btnClear_clicked() +{ + if (!APPLICATION->logModel) + return; + APPLICATION->logModel->clear(); + m_container->refreshContainer(); +} + +void LauncherLogPage::on_btnBottom_clicked() +{ + ui->text->scrollToBottom(); +} + +void LauncherLogPage::on_trackLogCheckbox_clicked(bool checked) +{ + if (!APPLICATION->logModel) + return; + APPLICATION->logModel->suspend(!checked); +} + +void LauncherLogPage::on_wrapCheckbox_clicked(bool checked) +{ + ui->text->setWordWrap(checked); + if (!APPLICATION->logModel) + return; + APPLICATION->logModel->setLineWrap(checked); +} + +void LauncherLogPage::on_colorCheckbox_clicked(bool checked) +{ + ui->text->setColorLines(checked); + if (!APPLICATION->logModel) + return; + APPLICATION->logModel->setColorLines(checked); +} + +void LauncherLogPage::on_findButton_clicked() +{ + auto modifiers = QApplication::keyboardModifiers(); + bool reverse = modifiers & Qt::ShiftModifier; + ui->text->findNext(ui->searchBar->text(), reverse); +} + +void LauncherLogPage::findNextActivated() +{ + ui->text->findNext(ui->searchBar->text(), false); +} + +void LauncherLogPage::findPreviousActivated() +{ + ui->text->findNext(ui->searchBar->text(), true); +} + +void LauncherLogPage::findActivated() +{ + // focus the search bar if it doesn't have focus + if (!ui->searchBar->hasFocus()) { + ui->searchBar->setFocus(); + ui->searchBar->selectAll(); + } +} + +void LauncherLogPage::retranslate() +{ + ui->retranslateUi(this); +} diff --git a/launcher/ui/pages/global/LauncherLogPage.h b/launcher/ui/pages/global/LauncherLogPage.h new file mode 100644 index 000000000..bab8a3a1a --- /dev/null +++ b/launcher/ui/pages/global/LauncherLogPage.h @@ -0,0 +1,99 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2022 Jamie Mansfield + * Copyright (c) 2025 Yihe Li + * + * 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, version 3. + * + * 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 . + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include + +#include +#include "BaseInstance.h" +#include "launch/LaunchTask.h" +#include "ui/pages/BasePage.h" + +namespace Ui { +class LauncherLogPage; +} +class QTextCharFormat; + +class LogFormatProxyModel : public QIdentityProxyModel { + public: + LogFormatProxyModel(QObject* parent = nullptr) : QIdentityProxyModel(parent) {} + QVariant data(const QModelIndex& index, int role) const override; + QFont getFont() const { return m_font; } + void setFont(QFont font) { m_font = font; } + QModelIndex find(const QModelIndex& start, const QString& value, bool reverse) const; + + private: + QFont m_font; +}; + +class LauncherLogPage : public QWidget, public BasePage { + Q_OBJECT + + public: + explicit LauncherLogPage(QWidget* parent = 0); + ~LauncherLogPage(); + + QString displayName() const override { return tr("Logs"); } + QIcon icon() const override { return APPLICATION->getThemedIcon("log"); } + QString id() const override { return "launcher-console"; } + QString helpPage() const override { return "Launcher-Logs"; } + void retranslate() override; + + private slots: + void on_btnPaste_clicked(); + void on_btnCopy_clicked(); + void on_btnClear_clicked(); + void on_btnBottom_clicked(); + + void on_trackLogCheckbox_clicked(bool checked); + void on_wrapCheckbox_clicked(bool checked); + void on_colorCheckbox_clicked(bool checked); + + void on_findButton_clicked(); + void findActivated(); + void findNextActivated(); + void findPreviousActivated(); + + private: + void modelStateToUI(); + void UIToModelState(); + + private: + Ui::LauncherLogPage* ui; + LogFormatProxyModel* m_proxy; +}; diff --git a/launcher/ui/pages/global/LauncherLogPage.ui b/launcher/ui/pages/global/LauncherLogPage.ui new file mode 100644 index 000000000..44e564f68 --- /dev/null +++ b/launcher/ui/pages/global/LauncherLogPage.ui @@ -0,0 +1,193 @@ + + + LauncherLogPage + + + + 0 + 0 + 825 + 782 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + 0 + + + + Tab 1 + + + + + + false + + + true + + + + + + Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse|Qt::TextBrowserInteraction|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse + + + false + + + + + + + + + Keep updating + + + true + + + + + + + Wrap lines + + + true + + + + + + + Color lines + + + true + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Copy the whole log into the clipboard + + + &Copy + + + + + + + Upload the log to the paste service configured in preferences + + + Upload + + + + + + + Clear the log + + + Clear + + + + + + + + + Search: + + + + + + + Find + + + + + + + + + + Scroll all the way to bottom + + + Bottom + + + + + + + Qt::Vertical + + + + + + + + + + + + LogView + QPlainTextEdit +
ui/widgets/LogView.h
+
+
+ + tabWidget + trackLogCheckbox + wrapCheckbox + colorCheckbox + btnCopy + btnPaste + btnClear + text + searchBar + findButton + + + +
diff --git a/launcher/ui/pages/instance/LogPage.cpp b/launcher/ui/pages/instance/LogPage.cpp index 7897a2932..d1691ff16 100644 --- a/launcher/ui/pages/instance/LogPage.cpp +++ b/launcher/ui/pages/instance/LogPage.cpp @@ -52,82 +52,6 @@ #include -QVariant LogFormatProxyModel::data(const QModelIndex& index, int role) const -{ - const LogColors& colors = APPLICATION->themeManager()->getLogColors(); - - switch (role) { - case Qt::FontRole: - return m_font; - case Qt::ForegroundRole: { - auto level = static_cast(QIdentityProxyModel::data(index, LogModel::LevelRole).toInt()); - QColor result = colors.foreground.value(level); - - if (result.isValid()) - return result; - - break; - } - case Qt::BackgroundRole: { - auto level = static_cast(QIdentityProxyModel::data(index, LogModel::LevelRole).toInt()); - QColor result = colors.background.value(level); - - if (result.isValid()) - return result; - - break; - } - } - - return QIdentityProxyModel::data(index, role); -} - -QModelIndex LogFormatProxyModel::find(const QModelIndex& start, const QString& value, bool reverse) const -{ - QModelIndex parentIndex = parent(start); - auto compare = [this, start, parentIndex, value](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(); -} - LogPage::LogPage(InstancePtr instance, QWidget* parent) : QWidget(parent), ui(new Ui::LogPage), m_instance(instance) { ui->setupUi(this); diff --git a/launcher/ui/pages/instance/LogPage.h b/launcher/ui/pages/instance/LogPage.h index b4d74fb9c..caa870cbc 100644 --- a/launcher/ui/pages/instance/LogPage.h +++ b/launcher/ui/pages/instance/LogPage.h @@ -42,23 +42,11 @@ #include "BaseInstance.h" #include "launch/LaunchTask.h" #include "ui/pages/BasePage.h" +#include "ui/pages/global/LauncherLogPage.h" namespace Ui { class LogPage; } -class QTextCharFormat; - -class LogFormatProxyModel : public QIdentityProxyModel { - public: - LogFormatProxyModel(QObject* parent = nullptr) : QIdentityProxyModel(parent) {} - QVariant data(const QModelIndex& index, int role) const override; - QFont getFont() const { return m_font; } - void setFont(QFont font) { m_font = font; } - QModelIndex find(const QModelIndex& start, const QString& value, bool reverse) const; - - private: - QFont m_font; -}; class LogPage : public QWidget, public BasePage { Q_OBJECT From 289645266ad72f81cdb968241adb98cd802255ef Mon Sep 17 00:00:00 2001 From: Yihe Li Date: Tue, 3 Jun 2025 06:08:24 +0800 Subject: [PATCH 2/7] Add support for view older launcher logs Signed-off-by: Yihe Li --- launcher/Application.cpp | 30 +- launcher/Application.h | 3 + launcher/MessageLevel.cpp | 15 + launcher/MessageLevel.h | 3 + launcher/ui/pages/global/LauncherLogPage.cpp | 407 ++++++++++++++++--- launcher/ui/pages/global/LauncherLogPage.h | 22 +- launcher/ui/pages/global/LauncherLogPage.ui | 260 +++++++----- launcher/ui/pages/instance/OtherLogsPage.cpp | 3 +- 8 files changed, 560 insertions(+), 183 deletions(-) diff --git a/launcher/Application.cpp b/launcher/Application.cpp index e3018db0d..ef2530e0d 100644 --- a/launcher/Application.cpp +++ b/launcher/Application.cpp @@ -697,16 +697,9 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv) m_settings->registerSetting("ConsoleMaxLines", 100000); m_settings->registerSetting("ConsoleOverflowStop", true); - auto lineSetting = settings()->getSetting("ConsoleMaxLines"); - bool conversionOk = false; - int maxLines = lineSetting->get().toInt(&conversionOk); - if (!conversionOk) { - maxLines = lineSetting->defValue().toInt(); - qWarning() << "ConsoleMaxLines has nonsensical value, defaulting to" << maxLines; - } - logModel->setMaxLines(maxLines); - logModel->setStopOnOverflow(settings()->get("ConsoleOverflowStop").toBool()); - logModel->setOverflowMessage(tr("Cannot display this log since the log length surpassed %1 lines.").arg(maxLines)); + logModel->setMaxLines(getConsoleMaxLines()); + logModel->setStopOnOverflow(shouldStopOnConsoleOverflow()); + logModel->setOverflowMessage(tr("Cannot display this log since the log length surpassed %1 lines.").arg(logModel->getMaxLines())); // Folders m_settings->registerSetting("InstanceDir", "instances"); @@ -1614,6 +1607,23 @@ void Application::updateIsRunning(bool running) m_updateRunning = running; } +int Application::getConsoleMaxLines() const +{ + auto lineSetting = settings()->getSetting("ConsoleMaxLines"); + bool conversionOk = false; + int maxLines = lineSetting->get().toInt(&conversionOk); + if (!conversionOk) { + maxLines = lineSetting->defValue().toInt(); + qWarning() << "ConsoleMaxLines has nonsensical value, defaulting to" << maxLines; + } + return maxLines; +} + +bool Application::shouldStopOnConsoleOverflow() const +{ + return settings()->get("ConsoleOverflowStop").toBool(); +} + void Application::controllerSucceeded() { auto controller = qobject_cast(QObject::sender()); diff --git a/launcher/Application.h b/launcher/Application.h index 548345c18..3c2c6e11c 100644 --- a/launcher/Application.h +++ b/launcher/Application.h @@ -162,6 +162,9 @@ class Application : public QApplication { QString getModrinthAPIToken(); QString getUserAgent(); + int getConsoleMaxLines() const; + bool shouldStopOnConsoleOverflow() const; + /// this is the root of the 'installation'. Used for automatic updates const QString& root() { return m_rootPath; } diff --git a/launcher/MessageLevel.cpp b/launcher/MessageLevel.cpp index 3516dbdd6..2440f644e 100644 --- a/launcher/MessageLevel.cpp +++ b/launcher/MessageLevel.cpp @@ -54,3 +54,18 @@ MessageLevel::Enum MessageLevel::fromLine(QString& line) } return MessageLevel::Unknown; } + +MessageLevel::Enum MessageLevel::fromLauncherLine(QString& line) +{ + // Level prefix + int startMark = 0; + while (startMark < line.size() && (line[startMark].isDigit() || line[startMark].isSpace() || line[startMark] == '.')) + ++startMark; + int endmark = line.indexOf(":"); + if (startMark < line.size() && endmark != -1) { + auto level = MessageLevel::getLevel(line.left(endmark).mid(startMark)); + line = line.mid(endmark + 2); + return level; + } + return MessageLevel::Unknown; +} diff --git a/launcher/MessageLevel.h b/launcher/MessageLevel.h index 794e2ac39..ce4e8263f 100644 --- a/launcher/MessageLevel.h +++ b/launcher/MessageLevel.h @@ -26,4 +26,7 @@ MessageLevel::Enum getLevel(QtMsgType type); /* Get message level from a line. Line is modified if it was successful. */ MessageLevel::Enum fromLine(QString& line); + +/* Get message level from a line from the launcher log. Line is modified if it was successful. */ +MessageLevel::Enum fromLauncherLine(QString& line); } // namespace MessageLevel diff --git a/launcher/ui/pages/global/LauncherLogPage.cpp b/launcher/ui/pages/global/LauncherLogPage.cpp index 62c866b75..2f8dbac53 100644 --- a/launcher/ui/pages/global/LauncherLogPage.cpp +++ b/launcher/ui/pages/global/LauncherLogPage.cpp @@ -2,7 +2,6 @@ /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 Jamie Mansfield - * Copyright (C) 2022 Sefa Eyeoglu * Copyright (C) 2024 TheKodeToad * Copyright (c) 2025 Yihe Li * @@ -39,19 +38,18 @@ #include "LauncherLogPage.h" #include "ui_LauncherLogPage.h" -#include "Application.h" - -#include -#include -#include - -#include "launch/LaunchTask.h" -#include "settings/Setting.h" +#include #include "ui/GuiUtil.h" #include "ui/themes/ThemeManager.h" -#include +#include +#include +#include +#include +#include +#include +#include QVariant LogFormatProxyModel::data(const QModelIndex& index, int role) const { @@ -129,7 +127,12 @@ QModelIndex LogFormatProxyModel::find(const QModelIndex& start, const QString& v return QModelIndex(); } -LauncherLogPage::LauncherLogPage(QWidget* parent) : QWidget(parent), ui(new Ui::LauncherLogPage) +LauncherLogPage::LauncherLogPage(QWidget* parent) + : QWidget(parent) + , ui(new Ui::LauncherLogPage) + , m_model(APPLICATION->logModel) + , m_basePath(APPLICATION->dataRoot()) + , m_logSearchPaths({ "logs" }) { ui->setupUi(this); ui->tabWidget->tabBar()->hide(); @@ -148,16 +151,21 @@ LauncherLogPage::LauncherLogPage(QWidget* parent) : QWidget(parent), ui(new Ui:: } ui->text->setModel(m_proxy); - m_proxy->setSourceModel(APPLICATION->logModel.get()); + m_proxy->setSourceModel(m_model.get()); modelStateToUI(); + connect(&m_watcher, &QFileSystemWatcher::directoryChanged, this, &LauncherLogPage::populateSelectLogBox); + auto findShortcut = new QShortcut(QKeySequence(QKeySequence::Find), this); - connect(findShortcut, SIGNAL(activated()), SLOT(findActivated())); + connect(findShortcut, &QShortcut::activated, this, &LauncherLogPage::findActivated); + auto findNextShortcut = new QShortcut(QKeySequence(QKeySequence::FindNext), this); - connect(findNextShortcut, SIGNAL(activated()), SLOT(findNextActivated())); - connect(ui->searchBar, SIGNAL(returnPressed()), SLOT(on_findButton_clicked())); + connect(findNextShortcut, &QShortcut::activated, this, &LauncherLogPage::findNextActivated); + auto findPreviousShortcut = new QShortcut(QKeySequence(QKeySequence::FindPrevious), this); - connect(findPreviousShortcut, SIGNAL(activated()), SLOT(findPreviousActivated())); + connect(findPreviousShortcut, &QShortcut::activated, this, &LauncherLogPage::findPreviousActivated); + + connect(ui->searchBar, &QLineEdit::returnPressed, this, &LauncherLogPage::on_findButton_clicked); } LauncherLogPage::~LauncherLogPage() @@ -167,21 +175,21 @@ LauncherLogPage::~LauncherLogPage() void LauncherLogPage::modelStateToUI() { - if (APPLICATION->logModel->wrapLines()) { + if (m_model->wrapLines()) { ui->text->setWordWrap(true); ui->wrapCheckbox->setCheckState(Qt::Checked); } else { ui->text->setWordWrap(false); ui->wrapCheckbox->setCheckState(Qt::Unchecked); } - if (APPLICATION->logModel->colorLines()) { + if (m_model->colorLines()) { ui->text->setColorLines(true); ui->colorCheckbox->setCheckState(Qt::Checked); } else { ui->text->setColorLines(false); ui->colorCheckbox->setCheckState(Qt::Unchecked); } - if (APPLICATION->logModel->suspended()) { + if (m_model->suspended()) { ui->trackLogCheckbox->setCheckState(Qt::Unchecked); } else { ui->trackLogCheckbox->setCheckState(Qt::Checked); @@ -190,47 +198,205 @@ void LauncherLogPage::modelStateToUI() void LauncherLogPage::UIToModelState() { - if (!APPLICATION->logModel) { + if (!m_model) { return; } - APPLICATION->logModel->setLineWrap(ui->wrapCheckbox->checkState() == Qt::Checked); - APPLICATION->logModel->setColorLines(ui->colorCheckbox->checkState() == Qt::Checked); - APPLICATION->logModel->suspend(ui->trackLogCheckbox->checkState() != Qt::Checked); + m_model->setLineWrap(ui->wrapCheckbox->checkState() == Qt::Checked); + m_model->setColorLines(ui->colorCheckbox->checkState() == Qt::Checked); + m_model->suspend(ui->trackLogCheckbox->checkState() != Qt::Checked); +} + +void LauncherLogPage::retranslate() +{ + ui->retranslateUi(this); +} + +void LauncherLogPage::openedImpl() +{ + const QStringList failedPaths = m_watcher.addPaths(m_logSearchPaths); + + for (const QString& path : m_logSearchPaths) { + if (failedPaths.contains(path)) + qDebug() << "Failed to start watching" << path; + else + qDebug() << "Started watching" << path; + } + + populateSelectLogBox(); +} + +void LauncherLogPage::closedImpl() +{ + const QStringList failedPaths = m_watcher.removePaths(m_logSearchPaths); + + for (const QString& path : m_logSearchPaths) { + if (failedPaths.contains(path)) + qDebug() << "Failed to stop watching" << path; + else + qDebug() << "Stopped watching" << path; + } +} + +void LauncherLogPage::populateSelectLogBox() +{ + const QString prevCurrentFile = m_currentFile; + + ui->selectLogBox->blockSignals(true); + ui->selectLogBox->clear(); + ui->selectLogBox->addItem("Current logs"); + ui->selectLogBox->addItems(getPaths()); + ui->selectLogBox->blockSignals(false); + + if (!prevCurrentFile.isEmpty()) { + const int index = ui->selectLogBox->findText(prevCurrentFile); + if (index != -1) { + ui->selectLogBox->blockSignals(true); + ui->selectLogBox->setCurrentIndex(index); + ui->selectLogBox->blockSignals(false); + setControlsEnabled(true); + // don't refresh file + return; + } else { + setControlsEnabled(false); + } + } else { + ui->selectLogBox->setCurrentIndex(0); + setControlsEnabled(true); + } + + on_selectLogBox_currentIndexChanged(ui->selectLogBox->currentIndex()); +} + +void LauncherLogPage::on_selectLogBox_currentIndexChanged(const int index) +{ + QString file; + if (index > 0) { + file = ui->selectLogBox->itemText(index); + } + + if (index != 0 && (file.isEmpty() || !QFile::exists(FS::PathCombine(m_basePath, file)))) { + m_currentFile = QString(); + ui->text->clear(); + setControlsEnabled(false); + } else { + m_currentFile = file; + reload(); + setControlsEnabled(true); + } +} + +void LauncherLogPage::on_btnReload_clicked() +{ + if (m_currentFile.isEmpty()) { + if (!m_model) + return; + m_model->clear(); + m_container->refreshContainer(); + } else { + reload(); + } +} + +void LauncherLogPage::reload() +{ + if (m_currentFile.isEmpty()) { + m_model = APPLICATION->logModel; + m_proxy->setSourceModel(m_model.get()); + ui->text->setModel(m_proxy); + ui->text->scrollToBottom(); + UIToModelState(); + setControlsEnabled(true); + return; + } + + QFile file(FS::PathCombine(m_basePath, m_currentFile)); + if (!file.open(QFile::ReadOnly)) { + setControlsEnabled(false); + ui->btnReload->setEnabled(true); // allow reload + m_currentFile = QString(); + QMessageBox::critical(this, tr("Error"), tr("Unable to open %1 for reading: %2").arg(m_currentFile, file.errorString())); + } else { + auto setPlainText = [this](const QString& text) { + QTextDocument* doc = ui->text->document(); + doc->setDefaultFont(m_proxy->getFont()); + ui->text->setPlainText(text); + }; + auto showTooBig = [setPlainText, &file]() { + setPlainText(tr("The file (%1) is too big. You may want to open it in a viewer optimized " + "for large files.") + .arg(file.fileName())); + }; + if (file.size() > (1024ll * 1024ll * 12ll)) { + showTooBig(); + return; + } + MessageLevel::Enum last = MessageLevel::Unknown; + + auto handleLine = [this, &last](QString line) { + if (line.isEmpty()) + return false; + if (line.back() == '\n') + line = line.remove(line.size() - 1, 1); + QString lineTemp = line; // don't edit out the time and level for clarity + MessageLevel::Enum level = MessageLevel::fromLauncherLine(lineTemp); + + last = level; + m_model->append(level, line); + return m_model->isOverFlow(); + }; + + // Try to determine a level for each line + ui->text->clear(); + ui->text->setModel(nullptr); + m_model.reset(new LogModel(this)); + m_model->setMaxLines(APPLICATION->getConsoleMaxLines()); + m_model->setStopOnOverflow(APPLICATION->shouldStopOnConsoleOverflow()); + m_model->setOverflowMessage(tr("Cannot display this log since the log length surpassed %1 lines.").arg(m_model->getMaxLines())); + m_model->clear(); + if (file.fileName().endsWith(".gz")) { + QString line; + auto error = GZip::readGzFileByBlocks(&file, [&line, handleLine](const QByteArray& d) { + auto block = d; + int newlineIndex = block.indexOf('\n'); + while (newlineIndex != -1) { + line += QString::fromUtf8(block).left(newlineIndex); + block.remove(0, newlineIndex + 1); + if (handleLine(line)) { + line.clear(); + return false; + } + line.clear(); + newlineIndex = block.indexOf('\n'); + } + line += QString::fromUtf8(block); + return true; + }); + if (!error.isEmpty()) { + setPlainText(tr("The file (%1) encountered an error when reading: %2.").arg(file.fileName(), error)); + return; + } else if (!line.isEmpty()) { + handleLine(line); + } + } else { + while (!file.atEnd() && !handleLine(QString::fromUtf8(file.readLine()))) { + } + } + m_proxy->setSourceModel(m_model.get()); + ui->text->setModel(m_proxy); + ui->text->scrollToBottom(); + UIToModelState(); + setControlsEnabled(true); + } } void LauncherLogPage::on_btnPaste_clicked() { - if (!APPLICATION->logModel) - return; - - // FIXME: turn this into a proper task and move the upload logic out of GuiUtil! - APPLICATION->logModel->append(MessageLevel::Launcher, - QString("Log upload triggered at: %1").arg(QDateTime::currentDateTime().toString(Qt::RFC2822Date))); - auto url = GuiUtil::uploadPaste(tr("Launcher Log"), APPLICATION->logModel->toPlainText(), this); - if (!url.has_value()) { - APPLICATION->logModel->append(MessageLevel::Error, QString("Log upload canceled")); - } else if (url->isNull()) { - APPLICATION->logModel->append(MessageLevel::Error, QString("Log upload failed!")); - } else { - APPLICATION->logModel->append(MessageLevel::Launcher, QString("Log uploaded to: %1").arg(url.value())); - } + GuiUtil::uploadPaste(m_currentFile, ui->text->toPlainText(), this); } void LauncherLogPage::on_btnCopy_clicked() { - if (!APPLICATION->logModel) - return; - APPLICATION->logModel->append(MessageLevel::Launcher, - QString("Clipboard copy at: %1").arg(QDateTime::currentDateTime().toString(Qt::RFC2822Date))); - GuiUtil::setClipboardText(APPLICATION->logModel->toPlainText()); -} - -void LauncherLogPage::on_btnClear_clicked() -{ - if (!APPLICATION->logModel) - return; - APPLICATION->logModel->clear(); - m_container->refreshContainer(); + GuiUtil::setClipboardText(ui->text->toPlainText()); } void LauncherLogPage::on_btnBottom_clicked() @@ -240,25 +406,149 @@ void LauncherLogPage::on_btnBottom_clicked() void LauncherLogPage::on_trackLogCheckbox_clicked(bool checked) { - if (!APPLICATION->logModel) + if (!m_model) return; - APPLICATION->logModel->suspend(!checked); + m_model->suspend(!checked); +} + +void LauncherLogPage::on_btnDelete_clicked() +{ + if (m_currentFile.isEmpty()) { + setControlsEnabled(false); + return; + } + if (QMessageBox::question(this, tr("Confirm Deletion"), + tr("You are about to delete \"%1\".\n" + "This may be permanent and it will be gone from the logs folder.\n\n" + "Are you sure?") + .arg(m_currentFile), + QMessageBox::Yes, QMessageBox::No) == QMessageBox::No) { + return; + } + QFile file(FS::PathCombine(m_basePath, m_currentFile)); + + if (FS::trash(file.fileName())) { + return; + } + + if (!file.remove()) { + QMessageBox::critical(this, tr("Error"), tr("Unable to delete %1: %2").arg(m_currentFile, file.errorString())); + } +} + +void LauncherLogPage::on_btnClean_clicked() +{ + auto toDelete = getPaths(); + if (toDelete.isEmpty()) { + return; + } + QMessageBox* messageBox = new QMessageBox(this); + messageBox->setWindowTitle(tr("Confirm Cleanup")); + if (toDelete.size() > 5) { + messageBox->setText(tr("Are you sure you want to delete all log files?")); + messageBox->setDetailedText(toDelete.join('\n')); + } else { + messageBox->setText(tr("Are you sure you want to delete all these files?\n%1").arg(toDelete.join('\n'))); + } + messageBox->setStandardButtons(QMessageBox::Ok | QMessageBox::Cancel); + messageBox->setDefaultButton(QMessageBox::Ok); + messageBox->setTextInteractionFlags(Qt::TextSelectableByMouse); + messageBox->setIcon(QMessageBox::Question); + messageBox->setTextInteractionFlags(Qt::TextBrowserInteraction); + + if (messageBox->exec() != QMessageBox::Ok) { + return; + } + QStringList failed; + for (auto item : toDelete) { + QString absolutePath = FS::PathCombine(m_basePath, item); + QFile file(absolutePath); + qDebug() << "Deleting log" << absolutePath; + if (FS::trash(file.fileName())) { + continue; + } + if (!file.remove()) { + failed.push_back(item); + } + } + if (!failed.empty()) { + QMessageBox* messageBoxFailure = new QMessageBox(this); + messageBoxFailure->setWindowTitle(tr("Error")); + if (failed.size() > 5) { + messageBoxFailure->setText(tr("Couldn't delete some files!")); + messageBoxFailure->setDetailedText(failed.join('\n')); + } else { + messageBoxFailure->setText(tr("Couldn't delete some files:\n%1").arg(failed.join('\n'))); + } + messageBoxFailure->setStandardButtons(QMessageBox::Ok); + messageBoxFailure->setDefaultButton(QMessageBox::Ok); + messageBoxFailure->setTextInteractionFlags(Qt::TextSelectableByMouse); + messageBoxFailure->setIcon(QMessageBox::Critical); + messageBoxFailure->setTextInteractionFlags(Qt::TextBrowserInteraction); + messageBoxFailure->exec(); + } } void LauncherLogPage::on_wrapCheckbox_clicked(bool checked) { ui->text->setWordWrap(checked); - if (!APPLICATION->logModel) + if (!m_model) return; - APPLICATION->logModel->setLineWrap(checked); + m_model->setLineWrap(checked); + ui->text->scrollToBottom(); } void LauncherLogPage::on_colorCheckbox_clicked(bool checked) { ui->text->setColorLines(checked); - if (!APPLICATION->logModel) + if (!m_model) return; - APPLICATION->logModel->setColorLines(checked); + m_model->setColorLines(checked); + ui->text->scrollToBottom(); +} + +void LauncherLogPage::setControlsEnabled(const bool enabled) +{ + if (!m_currentFile.isEmpty()) { + ui->btnReload->setText("&Reload"); + ui->btnReload->setToolTip("Reload the contents of the log from the disk"); + ui->btnDelete->setEnabled(enabled); + ui->btnClean->setEnabled(enabled); + ui->trackLogCheckbox->setEnabled(false); + } else { + ui->btnReload->setText("Clear"); + ui->btnReload->setToolTip("Clear the log"); + ui->btnDelete->setEnabled(false); + ui->btnClean->setEnabled(false); + ui->trackLogCheckbox->setEnabled(enabled); + } + ui->btnReload->setEnabled(enabled); + ui->btnCopy->setEnabled(enabled); + ui->btnPaste->setEnabled(enabled); + ui->text->setEnabled(enabled); +} + +QStringList LauncherLogPage::getPaths() +{ + QDir baseDir(m_basePath); + + QStringList result; + + for (QString searchPath : m_logSearchPaths) { + QDir searchDir(searchPath); + + QStringList filters{ "*.log", "*.log.gz" }; + + if (searchPath != m_basePath) + filters.append("*.txt"); + + QStringList entries = searchDir.entryList(filters, QDir::Files | QDir::Readable, QDir::SortFlag::Time); + + for (const QString& name : entries) + result.append(baseDir.relativeFilePath(searchDir.filePath(name))); + } + + return result; } void LauncherLogPage::on_findButton_clicked() @@ -286,8 +576,3 @@ void LauncherLogPage::findActivated() ui->searchBar->selectAll(); } } - -void LauncherLogPage::retranslate() -{ - ui->retranslateUi(this); -} diff --git a/launcher/ui/pages/global/LauncherLogPage.h b/launcher/ui/pages/global/LauncherLogPage.h index bab8a3a1a..4a6fb5882 100644 --- a/launcher/ui/pages/global/LauncherLogPage.h +++ b/launcher/ui/pages/global/LauncherLogPage.h @@ -36,6 +36,7 @@ #pragma once +#include #include #include @@ -48,6 +49,7 @@ namespace Ui { class LauncherLogPage; } class QTextCharFormat; +class RecursiveFileSystemWatcher; class LogFormatProxyModel : public QIdentityProxyModel { public: @@ -74,10 +76,17 @@ class LauncherLogPage : public QWidget, public BasePage { QString helpPage() const override { return "Launcher-Logs"; } void retranslate() override; + void openedImpl() override; + void closedImpl() override; + private slots: + void populateSelectLogBox(); + void on_selectLogBox_currentIndexChanged(int index); + void on_btnReload_clicked(); void on_btnPaste_clicked(); void on_btnCopy_clicked(); - void on_btnClear_clicked(); + void on_btnDelete_clicked(); + void on_btnClean_clicked(); void on_btnBottom_clicked(); void on_trackLogCheckbox_clicked(bool checked); @@ -90,10 +99,21 @@ class LauncherLogPage : public QWidget, public BasePage { void findPreviousActivated(); private: + void reload(); void modelStateToUI(); void UIToModelState(); + void setControlsEnabled(bool enabled); + + QStringList getPaths(); private: Ui::LauncherLogPage* ui; LogFormatProxyModel* m_proxy; + shared_qobject_ptr m_model; + + /** Path to display log paths relative to. */ + QString m_basePath; + QStringList m_logSearchPaths; + QString m_currentFile; + QFileSystemWatcher m_watcher; }; diff --git a/launcher/ui/pages/global/LauncherLogPage.ui b/launcher/ui/pages/global/LauncherLogPage.ui index 44e564f68..189f2fe78 100644 --- a/launcher/ui/pages/global/LauncherLogPage.ui +++ b/launcher/ui/pages/global/LauncherLogPage.ui @@ -33,6 +33,40 @@ Tab 1 + + + + Search: + + + + + + + + + + &Find + + + + + + + Qt::Vertical + + + + + + + Scroll all the way to bottom + + + &Bottom + + + @@ -53,116 +87,120 @@ - - - - - Keep updating - - - true - - + + + + + + + + 0 + 0 + + + + + + + + Delete the selected log + + + &Delete Selected + + + + + + + Delete all the logs + + + Delete &All + + + + - - - - Wrap lines - - - true - - - - - - - Color lines - - - true - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - Copy the whole log into the clipboard - - - &Copy - - - - - - - Upload the log to the paste service configured in preferences - - - Upload - - - - - - - Clear the log - - - Clear - - + + + + + + Keep updating + + + true + + + + + + + Wrap lines + + + true + + + + + + + Color lines + + + true + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Copy the whole log into the clipboard + + + &Copy + + + + + + + Upload the log to the paste service configured in preferences + + + &Upload + + + + + + + Reload the contents of the log from the disk + + + &Reload + + + + - - - - Search: - - - - - - - Find - - - - - - - - - - Scroll all the way to bottom - - - Bottom - - - - - - - Qt::Vertical - - - @@ -178,12 +216,14 @@ tabWidget - trackLogCheckbox - wrapCheckbox - colorCheckbox + selectLogBox + btnReload btnCopy btnPaste - btnClear + btnDelete + btnClean + wrapCheckbox + colorCheckbox text searchBar findButton diff --git a/launcher/ui/pages/instance/OtherLogsPage.cpp b/launcher/ui/pages/instance/OtherLogsPage.cpp index afd1ff1c1..a90969503 100644 --- a/launcher/ui/pages/instance/OtherLogsPage.cpp +++ b/launcher/ui/pages/instance/OtherLogsPage.cpp @@ -211,7 +211,8 @@ void OtherLogsPage::on_btnReload_clicked() MessageLevel::Enum level = MessageLevel::Unknown; // if the launcher part set a log level, use it - auto innerLevel = MessageLevel::fromLine(line); + QString lineTemp = line; // don't edit out the time and level for clarity + auto innerLevel = MessageLevel::fromLine(lineTemp); if (innerLevel != MessageLevel::Unknown) { level = innerLevel; } From 1aa8d7bc13bf53d00eb60a4fd439404446f53105 Mon Sep 17 00:00:00 2001 From: Yihe Li Date: Tue, 3 Jun 2025 15:44:11 +0800 Subject: [PATCH 3/7] Reuse OtherLogsPage directly Signed-off-by: Yihe Li --- launcher/Application.cpp | 4 +- launcher/CMakeLists.txt | 3 - launcher/InstancePageProvider.h | 2 +- launcher/ui/pages/global/LauncherLogPage.cpp | 578 ------------------- launcher/ui/pages/global/LauncherLogPage.h | 119 ---- launcher/ui/pages/global/LauncherLogPage.ui | 233 -------- launcher/ui/pages/instance/LogPage.cpp | 76 +++ launcher/ui/pages/instance/LogPage.h | 14 +- launcher/ui/pages/instance/OtherLogsPage.cpp | 160 ++++- launcher/ui/pages/instance/OtherLogsPage.h | 16 +- launcher/ui/pages/instance/OtherLogsPage.ui | 10 + 11 files changed, 251 insertions(+), 964 deletions(-) delete mode 100644 launcher/ui/pages/global/LauncherLogPage.cpp delete mode 100644 launcher/ui/pages/global/LauncherLogPage.h delete mode 100644 launcher/ui/pages/global/LauncherLogPage.ui diff --git a/launcher/Application.cpp b/launcher/Application.cpp index ef2530e0d..86e454802 100644 --- a/launcher/Application.cpp +++ b/launcher/Application.cpp @@ -63,10 +63,10 @@ #include "ui/pages/global/ExternalToolsPage.h" #include "ui/pages/global/JavaPage.h" #include "ui/pages/global/LanguagePage.h" -#include "ui/pages/global/LauncherLogPage.h" #include "ui/pages/global/LauncherPage.h" #include "ui/pages/global/MinecraftPage.h" #include "ui/pages/global/ProxyPage.h" +#include "ui/pages/instance/OtherLogsPage.h" #include "ui/setupwizard/AutoJavaWizardPage.h" #include "ui/setupwizard/JavaWizardPage.h" @@ -905,7 +905,7 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv) m_globalSettingsProvider->addPage(); m_globalSettingsProvider->addPage(); m_globalSettingsProvider->addPage(); - m_globalSettingsProvider->addPage(); + m_globalSettingsProvider->addPageCreator([]() { return new OtherLogsPage("launcher-logs", tr("Logs"), "Launcher-Logs"); }); } PixmapCache::setInstance(new PixmapCache(this)); diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt index cd9903067..a7ccb809d 100644 --- a/launcher/CMakeLists.txt +++ b/launcher/CMakeLists.txt @@ -959,8 +959,6 @@ SET(LAUNCHER_SOURCES ui/pages/global/MinecraftPage.h ui/pages/global/LauncherPage.cpp ui/pages/global/LauncherPage.h - ui/pages/global/LauncherLogPage.cpp - ui/pages/global/LauncherLogPage.h ui/pages/global/AppearancePage.h ui/pages/global/ProxyPage.cpp ui/pages/global/ProxyPage.h @@ -1205,7 +1203,6 @@ qt_wrap_ui(LAUNCHER_UI ui/pages/global/AccountListPage.ui ui/pages/global/JavaPage.ui ui/pages/global/LauncherPage.ui - ui/pages/global/LauncherLogPage.ui ui/pages/global/APIPage.ui ui/pages/global/ProxyPage.ui ui/pages/global/ExternalToolsPage.ui diff --git a/launcher/InstancePageProvider.h b/launcher/InstancePageProvider.h index 2c2b0b580..258ed5aa5 100644 --- a/launcher/InstancePageProvider.h +++ b/launcher/InstancePageProvider.h @@ -46,7 +46,7 @@ class InstancePageProvider : protected QObject, public BasePageProvider { // values.append(new GameOptionsPage(onesix.get())); values.append(new ScreenshotsPage(FS::PathCombine(onesix->gameRoot(), "screenshots"))); values.append(new InstanceSettingsPage(onesix)); - values.append(new OtherLogsPage(inst)); + values.append(new OtherLogsPage("logs", tr("Other logs"), "Other-Logs", inst)); return values; } diff --git a/launcher/ui/pages/global/LauncherLogPage.cpp b/launcher/ui/pages/global/LauncherLogPage.cpp deleted file mode 100644 index 2f8dbac53..000000000 --- a/launcher/ui/pages/global/LauncherLogPage.cpp +++ /dev/null @@ -1,578 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-only -/* - * Prism Launcher - Minecraft Launcher - * Copyright (c) 2022 Jamie Mansfield - * Copyright (C) 2024 TheKodeToad - * Copyright (c) 2025 Yihe Li - * - * 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, version 3. - * - * 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 . - * - * This file incorporates work covered by the following copyright and - * permission notice: - * - * Copyright 2013-2021 MultiMC Contributors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "LauncherLogPage.h" -#include "ui_LauncherLogPage.h" - -#include - -#include "ui/GuiUtil.h" -#include "ui/themes/ThemeManager.h" - -#include -#include -#include -#include -#include -#include -#include - -QVariant LogFormatProxyModel::data(const QModelIndex& index, int role) const -{ - const LogColors& colors = APPLICATION->themeManager()->getLogColors(); - - switch (role) { - case Qt::FontRole: - return m_font; - case Qt::ForegroundRole: { - auto level = static_cast(QIdentityProxyModel::data(index, LogModel::LevelRole).toInt()); - QColor result = colors.foreground.value(level); - - if (result.isValid()) - return result; - - break; - } - case Qt::BackgroundRole: { - auto level = static_cast(QIdentityProxyModel::data(index, LogModel::LevelRole).toInt()); - QColor result = colors.background.value(level); - - if (result.isValid()) - return result; - - break; - } - } - - return QIdentityProxyModel::data(index, role); -} - -QModelIndex LogFormatProxyModel::find(const QModelIndex& start, const QString& value, bool reverse) const -{ - QModelIndex parentIndex = parent(start); - auto compare = [this, start, parentIndex, value](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(); -} - -LauncherLogPage::LauncherLogPage(QWidget* parent) - : QWidget(parent) - , ui(new Ui::LauncherLogPage) - , m_model(APPLICATION->logModel) - , m_basePath(APPLICATION->dataRoot()) - , m_logSearchPaths({ "logs" }) -{ - ui->setupUi(this); - ui->tabWidget->tabBar()->hide(); - - m_proxy = new LogFormatProxyModel(this); - - // 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); - m_proxy->setSourceModel(m_model.get()); - modelStateToUI(); - - connect(&m_watcher, &QFileSystemWatcher::directoryChanged, this, &LauncherLogPage::populateSelectLogBox); - - auto findShortcut = new QShortcut(QKeySequence(QKeySequence::Find), this); - connect(findShortcut, &QShortcut::activated, this, &LauncherLogPage::findActivated); - - auto findNextShortcut = new QShortcut(QKeySequence(QKeySequence::FindNext), this); - connect(findNextShortcut, &QShortcut::activated, this, &LauncherLogPage::findNextActivated); - - auto findPreviousShortcut = new QShortcut(QKeySequence(QKeySequence::FindPrevious), this); - connect(findPreviousShortcut, &QShortcut::activated, this, &LauncherLogPage::findPreviousActivated); - - connect(ui->searchBar, &QLineEdit::returnPressed, this, &LauncherLogPage::on_findButton_clicked); -} - -LauncherLogPage::~LauncherLogPage() -{ - delete ui; -} - -void LauncherLogPage::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->colorLines()) { - ui->text->setColorLines(true); - ui->colorCheckbox->setCheckState(Qt::Checked); - } else { - ui->text->setColorLines(false); - ui->colorCheckbox->setCheckState(Qt::Unchecked); - } - if (m_model->suspended()) { - ui->trackLogCheckbox->setCheckState(Qt::Unchecked); - } else { - ui->trackLogCheckbox->setCheckState(Qt::Checked); - } -} - -void LauncherLogPage::UIToModelState() -{ - if (!m_model) { - return; - } - m_model->setLineWrap(ui->wrapCheckbox->checkState() == Qt::Checked); - m_model->setColorLines(ui->colorCheckbox->checkState() == Qt::Checked); - m_model->suspend(ui->trackLogCheckbox->checkState() != Qt::Checked); -} - -void LauncherLogPage::retranslate() -{ - ui->retranslateUi(this); -} - -void LauncherLogPage::openedImpl() -{ - const QStringList failedPaths = m_watcher.addPaths(m_logSearchPaths); - - for (const QString& path : m_logSearchPaths) { - if (failedPaths.contains(path)) - qDebug() << "Failed to start watching" << path; - else - qDebug() << "Started watching" << path; - } - - populateSelectLogBox(); -} - -void LauncherLogPage::closedImpl() -{ - const QStringList failedPaths = m_watcher.removePaths(m_logSearchPaths); - - for (const QString& path : m_logSearchPaths) { - if (failedPaths.contains(path)) - qDebug() << "Failed to stop watching" << path; - else - qDebug() << "Stopped watching" << path; - } -} - -void LauncherLogPage::populateSelectLogBox() -{ - const QString prevCurrentFile = m_currentFile; - - ui->selectLogBox->blockSignals(true); - ui->selectLogBox->clear(); - ui->selectLogBox->addItem("Current logs"); - ui->selectLogBox->addItems(getPaths()); - ui->selectLogBox->blockSignals(false); - - if (!prevCurrentFile.isEmpty()) { - const int index = ui->selectLogBox->findText(prevCurrentFile); - if (index != -1) { - ui->selectLogBox->blockSignals(true); - ui->selectLogBox->setCurrentIndex(index); - ui->selectLogBox->blockSignals(false); - setControlsEnabled(true); - // don't refresh file - return; - } else { - setControlsEnabled(false); - } - } else { - ui->selectLogBox->setCurrentIndex(0); - setControlsEnabled(true); - } - - on_selectLogBox_currentIndexChanged(ui->selectLogBox->currentIndex()); -} - -void LauncherLogPage::on_selectLogBox_currentIndexChanged(const int index) -{ - QString file; - if (index > 0) { - file = ui->selectLogBox->itemText(index); - } - - if (index != 0 && (file.isEmpty() || !QFile::exists(FS::PathCombine(m_basePath, file)))) { - m_currentFile = QString(); - ui->text->clear(); - setControlsEnabled(false); - } else { - m_currentFile = file; - reload(); - setControlsEnabled(true); - } -} - -void LauncherLogPage::on_btnReload_clicked() -{ - if (m_currentFile.isEmpty()) { - if (!m_model) - return; - m_model->clear(); - m_container->refreshContainer(); - } else { - reload(); - } -} - -void LauncherLogPage::reload() -{ - if (m_currentFile.isEmpty()) { - m_model = APPLICATION->logModel; - m_proxy->setSourceModel(m_model.get()); - ui->text->setModel(m_proxy); - ui->text->scrollToBottom(); - UIToModelState(); - setControlsEnabled(true); - return; - } - - QFile file(FS::PathCombine(m_basePath, m_currentFile)); - if (!file.open(QFile::ReadOnly)) { - setControlsEnabled(false); - ui->btnReload->setEnabled(true); // allow reload - m_currentFile = QString(); - QMessageBox::critical(this, tr("Error"), tr("Unable to open %1 for reading: %2").arg(m_currentFile, file.errorString())); - } else { - auto setPlainText = [this](const QString& text) { - QTextDocument* doc = ui->text->document(); - doc->setDefaultFont(m_proxy->getFont()); - ui->text->setPlainText(text); - }; - auto showTooBig = [setPlainText, &file]() { - setPlainText(tr("The file (%1) is too big. You may want to open it in a viewer optimized " - "for large files.") - .arg(file.fileName())); - }; - if (file.size() > (1024ll * 1024ll * 12ll)) { - showTooBig(); - return; - } - MessageLevel::Enum last = MessageLevel::Unknown; - - auto handleLine = [this, &last](QString line) { - if (line.isEmpty()) - return false; - if (line.back() == '\n') - line = line.remove(line.size() - 1, 1); - QString lineTemp = line; // don't edit out the time and level for clarity - MessageLevel::Enum level = MessageLevel::fromLauncherLine(lineTemp); - - last = level; - m_model->append(level, line); - return m_model->isOverFlow(); - }; - - // Try to determine a level for each line - ui->text->clear(); - ui->text->setModel(nullptr); - m_model.reset(new LogModel(this)); - m_model->setMaxLines(APPLICATION->getConsoleMaxLines()); - m_model->setStopOnOverflow(APPLICATION->shouldStopOnConsoleOverflow()); - m_model->setOverflowMessage(tr("Cannot display this log since the log length surpassed %1 lines.").arg(m_model->getMaxLines())); - m_model->clear(); - if (file.fileName().endsWith(".gz")) { - QString line; - auto error = GZip::readGzFileByBlocks(&file, [&line, handleLine](const QByteArray& d) { - auto block = d; - int newlineIndex = block.indexOf('\n'); - while (newlineIndex != -1) { - line += QString::fromUtf8(block).left(newlineIndex); - block.remove(0, newlineIndex + 1); - if (handleLine(line)) { - line.clear(); - return false; - } - line.clear(); - newlineIndex = block.indexOf('\n'); - } - line += QString::fromUtf8(block); - return true; - }); - if (!error.isEmpty()) { - setPlainText(tr("The file (%1) encountered an error when reading: %2.").arg(file.fileName(), error)); - return; - } else if (!line.isEmpty()) { - handleLine(line); - } - } else { - while (!file.atEnd() && !handleLine(QString::fromUtf8(file.readLine()))) { - } - } - m_proxy->setSourceModel(m_model.get()); - ui->text->setModel(m_proxy); - ui->text->scrollToBottom(); - UIToModelState(); - setControlsEnabled(true); - } -} - -void LauncherLogPage::on_btnPaste_clicked() -{ - GuiUtil::uploadPaste(m_currentFile, ui->text->toPlainText(), this); -} - -void LauncherLogPage::on_btnCopy_clicked() -{ - GuiUtil::setClipboardText(ui->text->toPlainText()); -} - -void LauncherLogPage::on_btnBottom_clicked() -{ - ui->text->scrollToBottom(); -} - -void LauncherLogPage::on_trackLogCheckbox_clicked(bool checked) -{ - if (!m_model) - return; - m_model->suspend(!checked); -} - -void LauncherLogPage::on_btnDelete_clicked() -{ - if (m_currentFile.isEmpty()) { - setControlsEnabled(false); - return; - } - if (QMessageBox::question(this, tr("Confirm Deletion"), - tr("You are about to delete \"%1\".\n" - "This may be permanent and it will be gone from the logs folder.\n\n" - "Are you sure?") - .arg(m_currentFile), - QMessageBox::Yes, QMessageBox::No) == QMessageBox::No) { - return; - } - QFile file(FS::PathCombine(m_basePath, m_currentFile)); - - if (FS::trash(file.fileName())) { - return; - } - - if (!file.remove()) { - QMessageBox::critical(this, tr("Error"), tr("Unable to delete %1: %2").arg(m_currentFile, file.errorString())); - } -} - -void LauncherLogPage::on_btnClean_clicked() -{ - auto toDelete = getPaths(); - if (toDelete.isEmpty()) { - return; - } - QMessageBox* messageBox = new QMessageBox(this); - messageBox->setWindowTitle(tr("Confirm Cleanup")); - if (toDelete.size() > 5) { - messageBox->setText(tr("Are you sure you want to delete all log files?")); - messageBox->setDetailedText(toDelete.join('\n')); - } else { - messageBox->setText(tr("Are you sure you want to delete all these files?\n%1").arg(toDelete.join('\n'))); - } - messageBox->setStandardButtons(QMessageBox::Ok | QMessageBox::Cancel); - messageBox->setDefaultButton(QMessageBox::Ok); - messageBox->setTextInteractionFlags(Qt::TextSelectableByMouse); - messageBox->setIcon(QMessageBox::Question); - messageBox->setTextInteractionFlags(Qt::TextBrowserInteraction); - - if (messageBox->exec() != QMessageBox::Ok) { - return; - } - QStringList failed; - for (auto item : toDelete) { - QString absolutePath = FS::PathCombine(m_basePath, item); - QFile file(absolutePath); - qDebug() << "Deleting log" << absolutePath; - if (FS::trash(file.fileName())) { - continue; - } - if (!file.remove()) { - failed.push_back(item); - } - } - if (!failed.empty()) { - QMessageBox* messageBoxFailure = new QMessageBox(this); - messageBoxFailure->setWindowTitle(tr("Error")); - if (failed.size() > 5) { - messageBoxFailure->setText(tr("Couldn't delete some files!")); - messageBoxFailure->setDetailedText(failed.join('\n')); - } else { - messageBoxFailure->setText(tr("Couldn't delete some files:\n%1").arg(failed.join('\n'))); - } - messageBoxFailure->setStandardButtons(QMessageBox::Ok); - messageBoxFailure->setDefaultButton(QMessageBox::Ok); - messageBoxFailure->setTextInteractionFlags(Qt::TextSelectableByMouse); - messageBoxFailure->setIcon(QMessageBox::Critical); - messageBoxFailure->setTextInteractionFlags(Qt::TextBrowserInteraction); - messageBoxFailure->exec(); - } -} - -void LauncherLogPage::on_wrapCheckbox_clicked(bool checked) -{ - ui->text->setWordWrap(checked); - if (!m_model) - return; - m_model->setLineWrap(checked); - ui->text->scrollToBottom(); -} - -void LauncherLogPage::on_colorCheckbox_clicked(bool checked) -{ - ui->text->setColorLines(checked); - if (!m_model) - return; - m_model->setColorLines(checked); - ui->text->scrollToBottom(); -} - -void LauncherLogPage::setControlsEnabled(const bool enabled) -{ - if (!m_currentFile.isEmpty()) { - ui->btnReload->setText("&Reload"); - ui->btnReload->setToolTip("Reload the contents of the log from the disk"); - ui->btnDelete->setEnabled(enabled); - ui->btnClean->setEnabled(enabled); - ui->trackLogCheckbox->setEnabled(false); - } else { - ui->btnReload->setText("Clear"); - ui->btnReload->setToolTip("Clear the log"); - ui->btnDelete->setEnabled(false); - ui->btnClean->setEnabled(false); - ui->trackLogCheckbox->setEnabled(enabled); - } - ui->btnReload->setEnabled(enabled); - ui->btnCopy->setEnabled(enabled); - ui->btnPaste->setEnabled(enabled); - ui->text->setEnabled(enabled); -} - -QStringList LauncherLogPage::getPaths() -{ - QDir baseDir(m_basePath); - - QStringList result; - - for (QString searchPath : m_logSearchPaths) { - QDir searchDir(searchPath); - - QStringList filters{ "*.log", "*.log.gz" }; - - if (searchPath != m_basePath) - filters.append("*.txt"); - - QStringList entries = searchDir.entryList(filters, QDir::Files | QDir::Readable, QDir::SortFlag::Time); - - for (const QString& name : entries) - result.append(baseDir.relativeFilePath(searchDir.filePath(name))); - } - - return result; -} - -void LauncherLogPage::on_findButton_clicked() -{ - auto modifiers = QApplication::keyboardModifiers(); - bool reverse = modifiers & Qt::ShiftModifier; - ui->text->findNext(ui->searchBar->text(), reverse); -} - -void LauncherLogPage::findNextActivated() -{ - ui->text->findNext(ui->searchBar->text(), false); -} - -void LauncherLogPage::findPreviousActivated() -{ - ui->text->findNext(ui->searchBar->text(), true); -} - -void LauncherLogPage::findActivated() -{ - // focus the search bar if it doesn't have focus - if (!ui->searchBar->hasFocus()) { - ui->searchBar->setFocus(); - ui->searchBar->selectAll(); - } -} diff --git a/launcher/ui/pages/global/LauncherLogPage.h b/launcher/ui/pages/global/LauncherLogPage.h deleted file mode 100644 index 4a6fb5882..000000000 --- a/launcher/ui/pages/global/LauncherLogPage.h +++ /dev/null @@ -1,119 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-only -/* - * Prism Launcher - Minecraft Launcher - * Copyright (c) 2022 Jamie Mansfield - * Copyright (c) 2025 Yihe Li - * - * 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, version 3. - * - * 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 . - * - * This file incorporates work covered by the following copyright and - * permission notice: - * - * Copyright 2013-2021 MultiMC Contributors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#pragma once - -#include -#include -#include - -#include -#include "BaseInstance.h" -#include "launch/LaunchTask.h" -#include "ui/pages/BasePage.h" - -namespace Ui { -class LauncherLogPage; -} -class QTextCharFormat; -class RecursiveFileSystemWatcher; - -class LogFormatProxyModel : public QIdentityProxyModel { - public: - LogFormatProxyModel(QObject* parent = nullptr) : QIdentityProxyModel(parent) {} - QVariant data(const QModelIndex& index, int role) const override; - QFont getFont() const { return m_font; } - void setFont(QFont font) { m_font = font; } - QModelIndex find(const QModelIndex& start, const QString& value, bool reverse) const; - - private: - QFont m_font; -}; - -class LauncherLogPage : public QWidget, public BasePage { - Q_OBJECT - - public: - explicit LauncherLogPage(QWidget* parent = 0); - ~LauncherLogPage(); - - QString displayName() const override { return tr("Logs"); } - QIcon icon() const override { return APPLICATION->getThemedIcon("log"); } - QString id() const override { return "launcher-console"; } - QString helpPage() const override { return "Launcher-Logs"; } - void retranslate() override; - - void openedImpl() override; - void closedImpl() override; - - private slots: - void populateSelectLogBox(); - void on_selectLogBox_currentIndexChanged(int index); - void on_btnReload_clicked(); - void on_btnPaste_clicked(); - void on_btnCopy_clicked(); - void on_btnDelete_clicked(); - void on_btnClean_clicked(); - void on_btnBottom_clicked(); - - void on_trackLogCheckbox_clicked(bool checked); - void on_wrapCheckbox_clicked(bool checked); - void on_colorCheckbox_clicked(bool checked); - - void on_findButton_clicked(); - void findActivated(); - void findNextActivated(); - void findPreviousActivated(); - - private: - void reload(); - void modelStateToUI(); - void UIToModelState(); - void setControlsEnabled(bool enabled); - - QStringList getPaths(); - - private: - Ui::LauncherLogPage* ui; - LogFormatProxyModel* m_proxy; - shared_qobject_ptr m_model; - - /** Path to display log paths relative to. */ - QString m_basePath; - QStringList m_logSearchPaths; - QString m_currentFile; - QFileSystemWatcher m_watcher; -}; diff --git a/launcher/ui/pages/global/LauncherLogPage.ui b/launcher/ui/pages/global/LauncherLogPage.ui deleted file mode 100644 index 189f2fe78..000000000 --- a/launcher/ui/pages/global/LauncherLogPage.ui +++ /dev/null @@ -1,233 +0,0 @@ - - - LauncherLogPage - - - - 0 - 0 - 825 - 782 - - - - - 0 - - - 0 - - - 0 - - - 0 - - - - - 0 - - - - Tab 1 - - - - - - Search: - - - - - - - - - - &Find - - - - - - - Qt::Vertical - - - - - - - Scroll all the way to bottom - - - &Bottom - - - - - - - false - - - true - - - - - - Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse|Qt::TextBrowserInteraction|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse - - - false - - - - - - - - - - - - 0 - 0 - - - - - - - - Delete the selected log - - - &Delete Selected - - - - - - - Delete all the logs - - - Delete &All - - - - - - - - - - - Keep updating - - - true - - - - - - - Wrap lines - - - true - - - - - - - Color lines - - - true - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - Copy the whole log into the clipboard - - - &Copy - - - - - - - Upload the log to the paste service configured in preferences - - - &Upload - - - - - - - Reload the contents of the log from the disk - - - &Reload - - - - - - - - - - - - - - - - LogView - QPlainTextEdit -
ui/widgets/LogView.h
-
-
- - tabWidget - selectLogBox - btnReload - btnCopy - btnPaste - btnDelete - btnClean - wrapCheckbox - colorCheckbox - text - searchBar - findButton - - - -
diff --git a/launcher/ui/pages/instance/LogPage.cpp b/launcher/ui/pages/instance/LogPage.cpp index d1691ff16..7897a2932 100644 --- a/launcher/ui/pages/instance/LogPage.cpp +++ b/launcher/ui/pages/instance/LogPage.cpp @@ -52,6 +52,82 @@ #include +QVariant LogFormatProxyModel::data(const QModelIndex& index, int role) const +{ + const LogColors& colors = APPLICATION->themeManager()->getLogColors(); + + switch (role) { + case Qt::FontRole: + return m_font; + case Qt::ForegroundRole: { + auto level = static_cast(QIdentityProxyModel::data(index, LogModel::LevelRole).toInt()); + QColor result = colors.foreground.value(level); + + if (result.isValid()) + return result; + + break; + } + case Qt::BackgroundRole: { + auto level = static_cast(QIdentityProxyModel::data(index, LogModel::LevelRole).toInt()); + QColor result = colors.background.value(level); + + if (result.isValid()) + return result; + + break; + } + } + + return QIdentityProxyModel::data(index, role); +} + +QModelIndex LogFormatProxyModel::find(const QModelIndex& start, const QString& value, bool reverse) const +{ + QModelIndex parentIndex = parent(start); + auto compare = [this, start, parentIndex, value](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(); +} + LogPage::LogPage(InstancePtr instance, QWidget* parent) : QWidget(parent), ui(new Ui::LogPage), m_instance(instance) { ui->setupUi(this); diff --git a/launcher/ui/pages/instance/LogPage.h b/launcher/ui/pages/instance/LogPage.h index caa870cbc..b4d74fb9c 100644 --- a/launcher/ui/pages/instance/LogPage.h +++ b/launcher/ui/pages/instance/LogPage.h @@ -42,11 +42,23 @@ #include "BaseInstance.h" #include "launch/LaunchTask.h" #include "ui/pages/BasePage.h" -#include "ui/pages/global/LauncherLogPage.h" namespace Ui { class LogPage; } +class QTextCharFormat; + +class LogFormatProxyModel : public QIdentityProxyModel { + public: + LogFormatProxyModel(QObject* parent = nullptr) : QIdentityProxyModel(parent) {} + QVariant data(const QModelIndex& index, int role) const override; + QFont getFont() const { return m_font; } + void setFont(QFont font) { m_font = font; } + QModelIndex find(const QModelIndex& start, const QString& value, bool reverse) const; + + private: + QFont m_font; +}; class LogPage : public QWidget, public BasePage { Q_OBJECT diff --git a/launcher/ui/pages/instance/OtherLogsPage.cpp b/launcher/ui/pages/instance/OtherLogsPage.cpp index a90969503..281e5be27 100644 --- a/launcher/ui/pages/instance/OtherLogsPage.cpp +++ b/launcher/ui/pages/instance/OtherLogsPage.cpp @@ -40,6 +40,7 @@ #include #include "ui/GuiUtil.h" +#include "ui/themes/ThemeManager.h" #include #include @@ -49,18 +50,26 @@ #include #include -OtherLogsPage::OtherLogsPage(InstancePtr instance, QWidget* parent) +OtherLogsPage::OtherLogsPage(QString id, QString displayName, QString helpPage, InstancePtr instance, QWidget* parent) : QWidget(parent) + , m_id(id) + , m_displayName(displayName) + , m_helpPage(helpPage) , ui(new Ui::OtherLogsPage) , m_instance(instance) - , m_basePath(instance->gameRoot()) - , m_logSearchPaths(instance->getLogFileSearchPaths()) - , m_model(new LogModel(this)) + , m_basePath(instance ? instance->gameRoot() : APPLICATION->dataRoot()) + , m_logSearchPaths(instance ? instance->getLogFileSearchPaths() : QStringList{ "logs" }) { ui->setupUi(this); ui->tabWidget->tabBar()->hide(); m_proxy = new LogFormatProxyModel(this); + if (m_instance) { + m_model.reset(new LogModel(this)); + ui->trackLogCheckbox->setVisible(false); + } else { + m_model = APPLICATION->logModel; + } // set up fonts in the log proxy { @@ -75,9 +84,13 @@ OtherLogsPage::OtherLogsPage(InstancePtr instance, QWidget* parent) ui->text->setModel(m_proxy); - m_model->setMaxLines(m_instance->getConsoleMaxLines()); - m_model->setStopOnOverflow(m_instance->shouldStopOnConsoleOverflow()); - m_model->setOverflowMessage(tr("Cannot display this log since the log length surpassed %1 lines.").arg(m_model->getMaxLines())); + if (m_instance) { + m_model->setMaxLines(m_instance->getConsoleMaxLines()); + m_model->setStopOnOverflow(m_instance->shouldStopOnConsoleOverflow()); + m_model->setOverflowMessage(tr("Cannot display this log since the log length surpassed %1 lines.").arg(m_model->getMaxLines())); + } else { + modelStateToUI(); + } m_proxy->setSourceModel(m_model.get()); connect(&m_watcher, &QFileSystemWatcher::directoryChanged, this, &OtherLogsPage::populateSelectLogBox); @@ -99,6 +112,39 @@ OtherLogsPage::~OtherLogsPage() delete ui; } +void OtherLogsPage::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->colorLines()) { + ui->text->setColorLines(true); + ui->colorCheckbox->setCheckState(Qt::Checked); + } else { + ui->text->setColorLines(false); + ui->colorCheckbox->setCheckState(Qt::Unchecked); + } + if (m_model->suspended()) { + ui->trackLogCheckbox->setCheckState(Qt::Unchecked); + } else { + ui->trackLogCheckbox->setCheckState(Qt::Checked); + } +} + +void OtherLogsPage::UIToModelState() +{ + if (!m_model) { + return; + } + m_model->setLineWrap(ui->wrapCheckbox->checkState() == Qt::Checked); + m_model->setColorLines(ui->colorCheckbox->checkState() == Qt::Checked); + m_model->suspend(ui->trackLogCheckbox->checkState() != Qt::Checked); +} + void OtherLogsPage::retranslate() { ui->retranslateUi(this); @@ -136,6 +182,8 @@ void OtherLogsPage::populateSelectLogBox() ui->selectLogBox->blockSignals(true); ui->selectLogBox->clear(); + if (!m_instance) + ui->selectLogBox->addItem("Current logs"); ui->selectLogBox->addItems(getPaths()); ui->selectLogBox->blockSignals(false); @@ -151,6 +199,9 @@ void OtherLogsPage::populateSelectLogBox() } else { setControlsEnabled(false); } + } else if (!m_instance) { + ui->selectLogBox->setCurrentIndex(0); + setControlsEnabled(true); } on_selectLogBox_currentIndexChanged(ui->selectLogBox->currentIndex()); @@ -159,27 +210,49 @@ void OtherLogsPage::populateSelectLogBox() void OtherLogsPage::on_selectLogBox_currentIndexChanged(const int index) { QString file; - if (index != -1) { + if (index > 0 || (index == 0 && m_instance)) { file = ui->selectLogBox->itemText(index); } - if (file.isEmpty() || !QFile::exists(FS::PathCombine(m_basePath, file))) { + if ((index != 0 || m_instance) && (file.isEmpty() || !QFile::exists(FS::PathCombine(m_basePath, file)))) { m_currentFile = QString(); ui->text->clear(); setControlsEnabled(false); } else { m_currentFile = file; - on_btnReload_clicked(); + reload(); setControlsEnabled(true); } } void OtherLogsPage::on_btnReload_clicked() +{ + if (!m_instance && m_currentFile.isEmpty()) { + if (!m_model) + return; + m_model->clear(); + m_container->refreshContainer(); + } else { + reload(); + } +} + +void OtherLogsPage::reload() { if (m_currentFile.isEmpty()) { - setControlsEnabled(false); + if (m_instance) { + setControlsEnabled(false); + } else { + m_model = APPLICATION->logModel; + m_proxy->setSourceModel(m_model.get()); + ui->text->setModel(m_proxy); + ui->text->scrollToBottom(); + UIToModelState(); + setControlsEnabled(true); + } return; } + QFile file(FS::PathCombine(m_basePath, m_currentFile)); if (!file.open(QFile::ReadOnly)) { setControlsEnabled(false); @@ -210,16 +283,20 @@ void OtherLogsPage::on_btnReload_clicked() line = line.remove(line.size() - 1, 1); MessageLevel::Enum level = MessageLevel::Unknown; - // if the launcher part set a log level, use it QString lineTemp = line; // don't edit out the time and level for clarity - auto innerLevel = MessageLevel::fromLine(lineTemp); - if (innerLevel != MessageLevel::Unknown) { - level = innerLevel; - } + if (!m_instance) { + level = MessageLevel::fromLauncherLine(lineTemp); + } else { + // if the launcher part set a log level, use it + auto innerLevel = MessageLevel::fromLine(lineTemp); + if (innerLevel != MessageLevel::Unknown) { + level = innerLevel; + } - // If the level is still undetermined, guess level - if (level == MessageLevel::StdErr || level == MessageLevel::StdOut || level == MessageLevel::Unknown) { - level = LogParser::guessLevel(line, last); + // If the level is still undetermined, guess level + if (level == MessageLevel::StdErr || level == MessageLevel::StdOut || level == MessageLevel::Unknown) { + level = LogParser::guessLevel(line, last); + } } last = level; @@ -230,6 +307,12 @@ void OtherLogsPage::on_btnReload_clicked() // Try to determine a level for each line ui->text->clear(); ui->text->setModel(nullptr); + if (!m_instance) { + m_model.reset(new LogModel(this)); + m_model->setMaxLines(APPLICATION->getConsoleMaxLines()); + m_model->setStopOnOverflow(APPLICATION->shouldStopOnConsoleOverflow()); + m_model->setOverflowMessage(tr("Cannot display this log since the log length surpassed %1 lines.").arg(m_model->getMaxLines())); + } m_model->clear(); if (file.fileName().endsWith(".gz")) { QString line; @@ -259,8 +342,17 @@ void OtherLogsPage::on_btnReload_clicked() while (!file.atEnd() && !handleLine(QString::fromUtf8(file.readLine()))) { } } - ui->text->setModel(m_proxy); - ui->text->scrollToBottom(); + + if (m_instance) { + ui->text->setModel(m_proxy); + ui->text->scrollToBottom(); + } else { + m_proxy->setSourceModel(m_model.get()); + ui->text->setModel(m_proxy); + ui->text->scrollToBottom(); + UIToModelState(); + setControlsEnabled(true); + } } } @@ -279,6 +371,13 @@ void OtherLogsPage::on_btnBottom_clicked() ui->text->scrollToBottom(); } +void OtherLogsPage::on_trackLogCheckbox_clicked(bool checked) +{ + if (!m_model) + return; + m_model->suspend(!checked); +} + void OtherLogsPage::on_btnDelete_clicked() { if (m_currentFile.isEmpty()) { @@ -377,12 +476,27 @@ void OtherLogsPage::on_colorCheckbox_clicked(bool checked) void OtherLogsPage::setControlsEnabled(const bool enabled) { + if (m_instance) { + ui->btnDelete->setEnabled(enabled); + ui->btnClean->setEnabled(enabled); + } else if (!m_currentFile.isEmpty()) { + ui->btnReload->setText("&Reload"); + ui->btnReload->setToolTip("Reload the contents of the log from the disk"); + ui->btnDelete->setEnabled(enabled); + ui->btnClean->setEnabled(enabled); + ui->trackLogCheckbox->setEnabled(false); + } else { + ui->btnReload->setText("Clear"); + ui->btnReload->setToolTip("Clear the log"); + ui->btnDelete->setEnabled(false); + ui->btnClean->setEnabled(false); + ui->trackLogCheckbox->setEnabled(enabled); + } + ui->btnReload->setEnabled(enabled); - ui->btnDelete->setEnabled(enabled); ui->btnCopy->setEnabled(enabled); ui->btnPaste->setEnabled(enabled); ui->text->setEnabled(enabled); - ui->btnClean->setEnabled(enabled); } QStringList OtherLogsPage::getPaths() diff --git a/launcher/ui/pages/instance/OtherLogsPage.h b/launcher/ui/pages/instance/OtherLogsPage.h index 70eb145fb..4104d8f3c 100644 --- a/launcher/ui/pages/instance/OtherLogsPage.h +++ b/launcher/ui/pages/instance/OtherLogsPage.h @@ -53,13 +53,13 @@ class OtherLogsPage : public QWidget, public BasePage { Q_OBJECT public: - explicit OtherLogsPage(InstancePtr instance, QWidget* parent = 0); + explicit OtherLogsPage(QString id, QString displayName, QString helpPage, InstancePtr instance = nullptr, QWidget* parent = 0); ~OtherLogsPage(); - QString id() const override { return "logs"; } - QString displayName() const override { return tr("Other logs"); } + QString id() const override { return m_id; } + QString displayName() const override { return m_displayName; } QIcon icon() const override { return APPLICATION->getThemedIcon("log"); } - QString helpPage() const override { return "other-Logs"; } + QString helpPage() const override { return m_helpPage; } void retranslate() override; void openedImpl() override; @@ -75,6 +75,7 @@ class OtherLogsPage : public QWidget, public BasePage { void on_btnClean_clicked(); void on_btnBottom_clicked(); + void on_trackLogCheckbox_clicked(bool checked); void on_wrapCheckbox_clicked(bool checked); void on_colorCheckbox_clicked(bool checked); @@ -84,11 +85,18 @@ class OtherLogsPage : public QWidget, public BasePage { void findPreviousActivated(); private: + void reload(); + void modelStateToUI(); + void UIToModelState(); void setControlsEnabled(bool enabled); QStringList getPaths(); private: + QString m_id; + QString m_displayName; + QString m_helpPage; + Ui::OtherLogsPage* ui; InstancePtr m_instance; /** Path to display log paths relative to. */ diff --git a/launcher/ui/pages/instance/OtherLogsPage.ui b/launcher/ui/pages/instance/OtherLogsPage.ui index 6d1a46139..7d60de5c4 100644 --- a/launcher/ui/pages/instance/OtherLogsPage.ui +++ b/launcher/ui/pages/instance/OtherLogsPage.ui @@ -127,6 +127,16 @@
+ + + + Keep updating + + + true + + + From e4a801fdf7c1a17835a5c78bc90721af66b1678c Mon Sep 17 00:00:00 2001 From: Yihe Li Date: Thu, 5 Jun 2025 07:18:07 +0800 Subject: [PATCH 4/7] Use separate window for viewing logs Signed-off-by: Yihe Li --- launcher/Application.cpp | 2 -- launcher/CMakeLists.txt | 3 ++ launcher/ui/MainWindow.cpp | 14 ++++---- launcher/ui/MainWindow.ui | 8 ++--- launcher/ui/dialogs/ViewLogDialog.cpp | 21 ++++++++++++ launcher/ui/dialogs/ViewLogDialog.h | 22 +++++++++++++ launcher/ui/dialogs/ViewLogDialog.ui | 34 ++++++++++++++++++++ launcher/ui/pages/instance/OtherLogsPage.cpp | 6 ++-- 8 files changed, 94 insertions(+), 16 deletions(-) create mode 100644 launcher/ui/dialogs/ViewLogDialog.cpp create mode 100644 launcher/ui/dialogs/ViewLogDialog.h create mode 100644 launcher/ui/dialogs/ViewLogDialog.ui diff --git a/launcher/Application.cpp b/launcher/Application.cpp index 86e454802..a641a9d8a 100644 --- a/launcher/Application.cpp +++ b/launcher/Application.cpp @@ -66,7 +66,6 @@ #include "ui/pages/global/LauncherPage.h" #include "ui/pages/global/MinecraftPage.h" #include "ui/pages/global/ProxyPage.h" -#include "ui/pages/instance/OtherLogsPage.h" #include "ui/setupwizard/AutoJavaWizardPage.h" #include "ui/setupwizard/JavaWizardPage.h" @@ -905,7 +904,6 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv) m_globalSettingsProvider->addPage(); m_globalSettingsProvider->addPage(); m_globalSettingsProvider->addPage(); - m_globalSettingsProvider->addPageCreator([]() { return new OtherLogsPage("launcher-logs", tr("Logs"), "Launcher-Logs"); }); } PixmapCache::setInstance(new PixmapCache(this)); diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt index a7ccb809d..e9e32d481 100644 --- a/launcher/CMakeLists.txt +++ b/launcher/CMakeLists.txt @@ -1098,6 +1098,8 @@ SET(LAUNCHER_SOURCES ui/dialogs/ResourceUpdateDialog.h ui/dialogs/InstallLoaderDialog.cpp ui/dialogs/InstallLoaderDialog.h + ui/dialogs/ViewLogDialog.cpp + ui/dialogs/ViewLogDialog.h ui/dialogs/skins/SkinManageDialog.cpp ui/dialogs/skins/SkinManageDialog.h @@ -1256,6 +1258,7 @@ qt_wrap_ui(LAUNCHER_UI ui/dialogs/ScrollMessageBox.ui ui/dialogs/BlockedModsDialog.ui ui/dialogs/ChooseProviderDialog.ui + ui/dialogs/ViewLogDialog.ui ui/dialogs/skins/SkinManageDialog.ui ) diff --git a/launcher/ui/MainWindow.cpp b/launcher/ui/MainWindow.cpp index 455a95837..f68b94aca 100644 --- a/launcher/ui/MainWindow.cpp +++ b/launcher/ui/MainWindow.cpp @@ -103,6 +103,7 @@ #include "ui/dialogs/NewInstanceDialog.h" #include "ui/dialogs/NewsDialog.h" #include "ui/dialogs/ProgressDialog.h" +#include "ui/dialogs/ViewLogDialog.h" #include "ui/instanceview/InstanceDelegate.h" #include "ui/instanceview/InstanceProxyModel.h" #include "ui/instanceview/InstanceView.h" @@ -238,14 +239,11 @@ MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent), ui(new Ui::MainWi ui->actionViewJavaFolder->setEnabled(BuildConfig.JAVA_DOWNLOADER_ENABLED); } - { // logs upload - - auto menu = new QMenu(this); - for (auto file : QDir("logs").entryInfoList(QDir::Files)) { - auto action = menu->addAction(file.fileName()); - connect(action, &QAction::triggered, this, [this, file] { GuiUtil::uploadPaste(file.fileName(), file, this); }); - } - ui->actionUploadLog->setMenu(menu); + { // logs viewing + connect(ui->actionViewLog, &QAction::triggered, this, [this] { + ViewLogDialog dialog(this); + dialog.exec(); + }); } // add the toolbar toggles to the view menu diff --git a/launcher/ui/MainWindow.ui b/launcher/ui/MainWindow.ui index 1499ec872..1d29ff628 100644 --- a/launcher/ui/MainWindow.ui +++ b/launcher/ui/MainWindow.ui @@ -215,7 +215,7 @@ - + @@ -663,16 +663,16 @@ Clear cached metadata - + .. - Upload logs + View logs - Upload launcher logs to the selected log provider + View current and previous launcher logs diff --git a/launcher/ui/dialogs/ViewLogDialog.cpp b/launcher/ui/dialogs/ViewLogDialog.cpp new file mode 100644 index 000000000..47c63d9cc --- /dev/null +++ b/launcher/ui/dialogs/ViewLogDialog.cpp @@ -0,0 +1,21 @@ +#include "ViewLogDialog.h" +#include "ui_ViewLogDialog.h" + +#include "ui/pages/instance/OtherLogsPage.h" + +ViewLogDialog::ViewLogDialog(QWidget* parent) + : QDialog(parent) + , ui(new Ui::ViewLogDialog) + , m_page(new OtherLogsPage("launcher-logs", tr("Launcher Logs"), "Launcher-Logs", nullptr, parent)) +{ + ui->setupUi(this); + ui->verticalLayout->insertWidget(0, m_page); + connect(ui->buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject); + m_page->opened(); +} + +ViewLogDialog::~ViewLogDialog() +{ + m_page->closed(); + delete ui; +} diff --git a/launcher/ui/dialogs/ViewLogDialog.h b/launcher/ui/dialogs/ViewLogDialog.h new file mode 100644 index 000000000..ebb9ef650 --- /dev/null +++ b/launcher/ui/dialogs/ViewLogDialog.h @@ -0,0 +1,22 @@ +#pragma once + +#include +#include + +namespace Ui { +class ViewLogDialog; +} + +class OtherLogsPage; + +class ViewLogDialog : public QDialog { + Q_OBJECT + + public: + explicit ViewLogDialog(QWidget* parent = nullptr); + ~ViewLogDialog(); + + private: + Ui::ViewLogDialog* ui; + OtherLogsPage* m_page; +}; diff --git a/launcher/ui/dialogs/ViewLogDialog.ui b/launcher/ui/dialogs/ViewLogDialog.ui new file mode 100644 index 000000000..4a7bb789e --- /dev/null +++ b/launcher/ui/dialogs/ViewLogDialog.ui @@ -0,0 +1,34 @@ + + + ViewLogDialog + + + + 0 + 0 + 825 + 782 + + + + View Launcher Logs + + + true + + + + + + Qt::Horizontal + + + QDialogButtonBox::Close + + + + + + + + diff --git a/launcher/ui/pages/instance/OtherLogsPage.cpp b/launcher/ui/pages/instance/OtherLogsPage.cpp index 281e5be27..b1f0c6507 100644 --- a/launcher/ui/pages/instance/OtherLogsPage.cpp +++ b/launcher/ui/pages/instance/OtherLogsPage.cpp @@ -231,7 +231,8 @@ void OtherLogsPage::on_btnReload_clicked() if (!m_model) return; m_model->clear(); - m_container->refreshContainer(); + if (m_container) + m_container->refreshContainer(); } else { reload(); } @@ -358,7 +359,8 @@ void OtherLogsPage::reload() void OtherLogsPage::on_btnPaste_clicked() { - GuiUtil::uploadPaste(m_currentFile, ui->text->toPlainText(), this); + QString name = m_currentFile.isEmpty() ? displayName() : m_currentFile; + GuiUtil::uploadPaste(name, ui->text->toPlainText(), this); } void OtherLogsPage::on_btnCopy_clicked() From ef3bf75715dfcdb39f38d5fb525342e37b0b213f Mon Sep 17 00:00:00 2001 From: Yihe Li Date: Thu, 5 Jun 2025 16:15:29 +0800 Subject: [PATCH 5/7] Remove some duplicate code Signed-off-by: Yihe Li --- launcher/Application.cpp | 21 ++---------- launcher/Application.h | 3 -- launcher/BaseInstance.cpp | 34 ++++++++++---------- launcher/BaseInstance.h | 7 ++-- launcher/MessageLevel.h | 2 +- launcher/launch/LaunchTask.cpp | 4 +-- launcher/ui/pages/instance/OtherLogsPage.cpp | 10 +++--- 7 files changed, 31 insertions(+), 50 deletions(-) diff --git a/launcher/Application.cpp b/launcher/Application.cpp index a641a9d8a..b0ef1405a 100644 --- a/launcher/Application.cpp +++ b/launcher/Application.cpp @@ -696,8 +696,8 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv) m_settings->registerSetting("ConsoleMaxLines", 100000); m_settings->registerSetting("ConsoleOverflowStop", true); - logModel->setMaxLines(getConsoleMaxLines()); - logModel->setStopOnOverflow(shouldStopOnConsoleOverflow()); + logModel->setMaxLines(getConsoleMaxLines(settings())); + logModel->setStopOnOverflow(shouldStopOnConsoleOverflow(settings())); logModel->setOverflowMessage(tr("Cannot display this log since the log length surpassed %1 lines.").arg(logModel->getMaxLines())); // Folders @@ -1605,23 +1605,6 @@ void Application::updateIsRunning(bool running) m_updateRunning = running; } -int Application::getConsoleMaxLines() const -{ - auto lineSetting = settings()->getSetting("ConsoleMaxLines"); - bool conversionOk = false; - int maxLines = lineSetting->get().toInt(&conversionOk); - if (!conversionOk) { - maxLines = lineSetting->defValue().toInt(); - qWarning() << "ConsoleMaxLines has nonsensical value, defaulting to" << maxLines; - } - return maxLines; -} - -bool Application::shouldStopOnConsoleOverflow() const -{ - return settings()->get("ConsoleOverflowStop").toBool(); -} - void Application::controllerSucceeded() { auto controller = qobject_cast(QObject::sender()); diff --git a/launcher/Application.h b/launcher/Application.h index 3c2c6e11c..548345c18 100644 --- a/launcher/Application.h +++ b/launcher/Application.h @@ -162,9 +162,6 @@ class Application : public QApplication { QString getModrinthAPIToken(); QString getUserAgent(); - int getConsoleMaxLines() const; - bool shouldStopOnConsoleOverflow() const; - /// this is the root of the 'installation'. Used for automatic updates const QString& root() { return m_rootPath; } diff --git a/launcher/BaseInstance.cpp b/launcher/BaseInstance.cpp index f4bc7e30b..fdbcc11fe 100644 --- a/launcher/BaseInstance.cpp +++ b/launcher/BaseInstance.cpp @@ -53,6 +53,23 @@ #include "Commandline.h" #include "FileSystem.h" +int getConsoleMaxLines(SettingsObjectPtr settings) +{ + auto lineSetting = settings->getSetting("ConsoleMaxLines"); + bool conversionOk = false; + int maxLines = lineSetting->get().toInt(&conversionOk); + if (!conversionOk) { + maxLines = lineSetting->defValue().toInt(); + qWarning() << "ConsoleMaxLines has nonsensical value, defaulting to" << maxLines; + } + return maxLines; +} + +bool shouldStopOnConsoleOverflow(SettingsObjectPtr settings) +{ + return settings->get("ConsoleOverflowStop").toBool(); +} + BaseInstance::BaseInstance(SettingsObjectPtr globalSettings, SettingsObjectPtr settings, const QString& rootDir) : QObject() { m_settings = settings; @@ -184,23 +201,6 @@ void BaseInstance::copyManagedPack(BaseInstance& other) } } -int BaseInstance::getConsoleMaxLines() const -{ - auto lineSetting = m_settings->getSetting("ConsoleMaxLines"); - bool conversionOk = false; - int maxLines = lineSetting->get().toInt(&conversionOk); - if (!conversionOk) { - maxLines = lineSetting->defValue().toInt(); - qWarning() << "ConsoleMaxLines has nonsensical value, defaulting to" << maxLines; - } - return maxLines; -} - -bool BaseInstance::shouldStopOnConsoleOverflow() const -{ - return m_settings->get("ConsoleOverflowStop").toBool(); -} - QStringList BaseInstance::getLinkedInstances() const { auto setting = m_settings->get("linkedInstances").toString(); diff --git a/launcher/BaseInstance.h b/launcher/BaseInstance.h index 3509c0155..6baac4ce8 100644 --- a/launcher/BaseInstance.h +++ b/launcher/BaseInstance.h @@ -78,6 +78,10 @@ struct ShortcutData { ShortcutTarget target = ShortcutTarget::Other; }; +/// Console settings +int getConsoleMaxLines(SettingsObjectPtr settings); +bool shouldStopOnConsoleOverflow(SettingsObjectPtr settings); + /*! * \brief Base class for instances. * This class implements many functions that are common between instances and @@ -272,9 +276,6 @@ class BaseInstance : public QObject, public std::enable_shared_from_this LaunchTask::getLogModel() { if (!m_logModel) { m_logModel.reset(new LogModel()); - m_logModel->setMaxLines(m_instance->getConsoleMaxLines()); - m_logModel->setStopOnOverflow(m_instance->shouldStopOnConsoleOverflow()); + m_logModel->setMaxLines(getConsoleMaxLines(m_instance->settings())); + m_logModel->setStopOnOverflow(shouldStopOnConsoleOverflow(m_instance->settings())); // FIXME: should this really be here? m_logModel->setOverflowMessage(tr("Stopped watching the game log because the log length surpassed %1 lines.\n" "You may have to fix your mods because the game is still logging to files and" diff --git a/launcher/ui/pages/instance/OtherLogsPage.cpp b/launcher/ui/pages/instance/OtherLogsPage.cpp index b1f0c6507..6f98db4a8 100644 --- a/launcher/ui/pages/instance/OtherLogsPage.cpp +++ b/launcher/ui/pages/instance/OtherLogsPage.cpp @@ -66,7 +66,7 @@ OtherLogsPage::OtherLogsPage(QString id, QString displayName, QString helpPage, m_proxy = new LogFormatProxyModel(this); if (m_instance) { m_model.reset(new LogModel(this)); - ui->trackLogCheckbox->setVisible(false); + ui->trackLogCheckbox->hide(); } else { m_model = APPLICATION->logModel; } @@ -85,8 +85,8 @@ OtherLogsPage::OtherLogsPage(QString id, QString displayName, QString helpPage, ui->text->setModel(m_proxy); if (m_instance) { - m_model->setMaxLines(m_instance->getConsoleMaxLines()); - m_model->setStopOnOverflow(m_instance->shouldStopOnConsoleOverflow()); + m_model->setMaxLines(getConsoleMaxLines(m_instance->settings())); + m_model->setStopOnOverflow(shouldStopOnConsoleOverflow(m_instance->settings())); m_model->setOverflowMessage(tr("Cannot display this log since the log length surpassed %1 lines.").arg(m_model->getMaxLines())); } else { modelStateToUI(); @@ -310,8 +310,8 @@ void OtherLogsPage::reload() ui->text->setModel(nullptr); if (!m_instance) { m_model.reset(new LogModel(this)); - m_model->setMaxLines(APPLICATION->getConsoleMaxLines()); - m_model->setStopOnOverflow(APPLICATION->shouldStopOnConsoleOverflow()); + m_model->setMaxLines(getConsoleMaxLines(APPLICATION->settings())); + m_model->setStopOnOverflow(shouldStopOnConsoleOverflow(APPLICATION->settings())); m_model->setOverflowMessage(tr("Cannot display this log since the log length surpassed %1 lines.").arg(m_model->getMaxLines())); } m_model->clear(); From d77889f26d06574d2e635045d236a7e4fec89b4e Mon Sep 17 00:00:00 2001 From: Yihe Li Date: Thu, 5 Jun 2025 23:12:20 +0800 Subject: [PATCH 6/7] Change log to be a QMainWindow Signed-off-by: Yihe Li --- launcher/Application.cpp | 15 ++++++++++++ launcher/Application.h | 5 ++++ launcher/CMakeLists.txt | 5 ++-- launcher/MessageLevel.cpp | 2 +- launcher/ui/MainWindow.cpp | 7 ++---- launcher/ui/ViewLogWindow.cpp | 25 ++++++++++++++++++++ launcher/ui/ViewLogWindow.h | 23 ++++++++++++++++++ launcher/ui/dialogs/ViewLogDialog.cpp | 21 ----------------- launcher/ui/dialogs/ViewLogDialog.h | 22 ----------------- launcher/ui/dialogs/ViewLogDialog.ui | 34 --------------------------- 10 files changed, 73 insertions(+), 86 deletions(-) create mode 100644 launcher/ui/ViewLogWindow.cpp create mode 100644 launcher/ui/ViewLogWindow.h delete mode 100644 launcher/ui/dialogs/ViewLogDialog.cpp delete mode 100644 launcher/ui/dialogs/ViewLogDialog.h delete mode 100644 launcher/ui/dialogs/ViewLogDialog.ui diff --git a/launcher/Application.cpp b/launcher/Application.cpp index b0ef1405a..fd76d4671 100644 --- a/launcher/Application.cpp +++ b/launcher/Application.cpp @@ -52,6 +52,7 @@ #include "tools/GenericProfiler.h" #include "ui/InstanceWindow.h" #include "ui/MainWindow.h" +#include "ui/ViewLogWindow.h" #include "ui/dialogs/ProgressDialog.h" #include "ui/instanceview/AccessibleInstanceView.h" @@ -1691,6 +1692,20 @@ MainWindow* Application::showMainWindow(bool minimized) return m_mainWindow; } +ViewLogWindow* Application::showLogWindow() +{ + if (m_viewLogWindow) { + m_viewLogWindow->setWindowState(m_viewLogWindow->windowState() & ~Qt::WindowMinimized); + m_viewLogWindow->raise(); + m_viewLogWindow->activateWindow(); + } else { + m_viewLogWindow = new ViewLogWindow(); + connect(m_viewLogWindow, &ViewLogWindow::isClosing, this, &Application::on_windowClose); + m_openWindows++; + } + return m_viewLogWindow; +} + InstanceWindow* Application::showInstanceWindow(InstancePtr instance, QString page) { if (!instance) diff --git a/launcher/Application.h b/launcher/Application.h index 548345c18..52a84b461 100644 --- a/launcher/Application.h +++ b/launcher/Application.h @@ -55,6 +55,7 @@ class LaunchController; class LocalPeer; class InstanceWindow; class MainWindow; +class ViewLogWindow; class SetupWizard; class GenericPageProvider; class QFile; @@ -183,6 +184,7 @@ class Application : public QApplication { InstanceWindow* showInstanceWindow(InstancePtr instance, QString page = QString()); MainWindow* showMainWindow(bool minimized = false); + ViewLogWindow* showLogWindow(); void updateIsRunning(bool running); bool updatesAreAllowed(); @@ -290,6 +292,9 @@ class Application : public QApplication { // main window, if any MainWindow* m_mainWindow = nullptr; + // log window, if any + ViewLogWindow* m_viewLogWindow = nullptr; + // peer launcher instance connector - used to implement single instance launcher and signalling LocalPeer* m_peerInstance = nullptr; diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt index e9e32d481..ada0af75f 100644 --- a/launcher/CMakeLists.txt +++ b/launcher/CMakeLists.txt @@ -847,6 +847,8 @@ SET(LAUNCHER_SOURCES ui/MainWindow.cpp ui/InstanceWindow.h ui/InstanceWindow.cpp + ui/ViewLogWindow.h + ui/ViewLogWindow.cpp # FIXME: maybe find a better home for this. FileIgnoreProxy.cpp @@ -1098,8 +1100,6 @@ SET(LAUNCHER_SOURCES ui/dialogs/ResourceUpdateDialog.h ui/dialogs/InstallLoaderDialog.cpp ui/dialogs/InstallLoaderDialog.h - ui/dialogs/ViewLogDialog.cpp - ui/dialogs/ViewLogDialog.h ui/dialogs/skins/SkinManageDialog.cpp ui/dialogs/skins/SkinManageDialog.h @@ -1258,7 +1258,6 @@ qt_wrap_ui(LAUNCHER_UI ui/dialogs/ScrollMessageBox.ui ui/dialogs/BlockedModsDialog.ui ui/dialogs/ChooseProviderDialog.ui - ui/dialogs/ViewLogDialog.ui ui/dialogs/skins/SkinManageDialog.ui ) diff --git a/launcher/MessageLevel.cpp b/launcher/MessageLevel.cpp index 2440f644e..c1c190c72 100644 --- a/launcher/MessageLevel.cpp +++ b/launcher/MessageLevel.cpp @@ -15,7 +15,7 @@ MessageLevel::Enum MessageLevel::getLevel(const QString& levelName) return MessageLevel::Message; else if (name == "WARNING" || name == "WARN") return MessageLevel::Warning; - else if (name == "ERROR") + else if (name == "ERROR" || name == "CRITICAL") return MessageLevel::Error; else if (name == "FATAL") return MessageLevel::Fatal; diff --git a/launcher/ui/MainWindow.cpp b/launcher/ui/MainWindow.cpp index f68b94aca..4fc4044cf 100644 --- a/launcher/ui/MainWindow.cpp +++ b/launcher/ui/MainWindow.cpp @@ -92,6 +92,7 @@ #include "InstanceWindow.h" #include "ui/GuiUtil.h" +#include "ui/ViewLogWindow.h" #include "ui/dialogs/AboutDialog.h" #include "ui/dialogs/CopyInstanceDialog.h" #include "ui/dialogs/CreateShortcutDialog.h" @@ -103,7 +104,6 @@ #include "ui/dialogs/NewInstanceDialog.h" #include "ui/dialogs/NewsDialog.h" #include "ui/dialogs/ProgressDialog.h" -#include "ui/dialogs/ViewLogDialog.h" #include "ui/instanceview/InstanceDelegate.h" #include "ui/instanceview/InstanceProxyModel.h" #include "ui/instanceview/InstanceView.h" @@ -240,10 +240,7 @@ MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent), ui(new Ui::MainWi } { // logs viewing - connect(ui->actionViewLog, &QAction::triggered, this, [this] { - ViewLogDialog dialog(this); - dialog.exec(); - }); + connect(ui->actionViewLog, &QAction::triggered, this, [] { APPLICATION->showLogWindow(); }); } // add the toolbar toggles to the view menu diff --git a/launcher/ui/ViewLogWindow.cpp b/launcher/ui/ViewLogWindow.cpp new file mode 100644 index 000000000..c0c56f3ee --- /dev/null +++ b/launcher/ui/ViewLogWindow.cpp @@ -0,0 +1,25 @@ +#include + +#include "ViewLogWindow.h" + +#include "ui/pages/instance/OtherLogsPage.h" + +ViewLogWindow::ViewLogWindow(QWidget* parent) + : QMainWindow(parent), m_page(new OtherLogsPage("launcher-logs", tr("Launcher Logs"), "Launcher-Logs", nullptr, parent)) +{ + setAttribute(Qt::WA_DeleteOnClose); + setWindowIcon(APPLICATION->getThemedIcon("log")); + setWindowTitle(tr("View Launcher Logs")); + setCentralWidget(m_page); + setMinimumSize(m_page->size()); + setContentsMargins(0, 0, 0, 0); + m_page->opened(); + show(); +} + +void ViewLogWindow::closeEvent(QCloseEvent* event) +{ + m_page->closed(); + emit isClosing(); + event->accept(); +} diff --git a/launcher/ui/ViewLogWindow.h b/launcher/ui/ViewLogWindow.h new file mode 100644 index 000000000..bb10683aa --- /dev/null +++ b/launcher/ui/ViewLogWindow.h @@ -0,0 +1,23 @@ +#pragma once + +#include + +#include "Application.h" + +class OtherLogsPage; + +class ViewLogWindow : public QMainWindow { + Q_OBJECT + + public: + explicit ViewLogWindow(QWidget* parent = nullptr); + + signals: + void isClosing(); + + protected: + void closeEvent(QCloseEvent*) override; + + private: + OtherLogsPage* m_page; +}; diff --git a/launcher/ui/dialogs/ViewLogDialog.cpp b/launcher/ui/dialogs/ViewLogDialog.cpp deleted file mode 100644 index 47c63d9cc..000000000 --- a/launcher/ui/dialogs/ViewLogDialog.cpp +++ /dev/null @@ -1,21 +0,0 @@ -#include "ViewLogDialog.h" -#include "ui_ViewLogDialog.h" - -#include "ui/pages/instance/OtherLogsPage.h" - -ViewLogDialog::ViewLogDialog(QWidget* parent) - : QDialog(parent) - , ui(new Ui::ViewLogDialog) - , m_page(new OtherLogsPage("launcher-logs", tr("Launcher Logs"), "Launcher-Logs", nullptr, parent)) -{ - ui->setupUi(this); - ui->verticalLayout->insertWidget(0, m_page); - connect(ui->buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject); - m_page->opened(); -} - -ViewLogDialog::~ViewLogDialog() -{ - m_page->closed(); - delete ui; -} diff --git a/launcher/ui/dialogs/ViewLogDialog.h b/launcher/ui/dialogs/ViewLogDialog.h deleted file mode 100644 index ebb9ef650..000000000 --- a/launcher/ui/dialogs/ViewLogDialog.h +++ /dev/null @@ -1,22 +0,0 @@ -#pragma once - -#include -#include - -namespace Ui { -class ViewLogDialog; -} - -class OtherLogsPage; - -class ViewLogDialog : public QDialog { - Q_OBJECT - - public: - explicit ViewLogDialog(QWidget* parent = nullptr); - ~ViewLogDialog(); - - private: - Ui::ViewLogDialog* ui; - OtherLogsPage* m_page; -}; diff --git a/launcher/ui/dialogs/ViewLogDialog.ui b/launcher/ui/dialogs/ViewLogDialog.ui deleted file mode 100644 index 4a7bb789e..000000000 --- a/launcher/ui/dialogs/ViewLogDialog.ui +++ /dev/null @@ -1,34 +0,0 @@ - - - ViewLogDialog - - - - 0 - 0 - 825 - 782 - - - - View Launcher Logs - - - true - - - - - - Qt::Horizontal - - - QDialogButtonBox::Close - - - - - - - - From 25907ea8c652cd37d5d4907e6822cf99bc88743a Mon Sep 17 00:00:00 2001 From: Yihe Li Date: Thu, 5 Jun 2025 23:22:49 +0800 Subject: [PATCH 7/7] Forgot to reset pointer to nullptr Signed-off-by: Yihe Li --- launcher/Application.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/launcher/Application.cpp b/launcher/Application.cpp index 5e70286ac..a92c6b1e7 100644 --- a/launcher/Application.cpp +++ b/launcher/Application.cpp @@ -1760,6 +1760,10 @@ void Application::on_windowClose() if (mainWindow) { m_mainWindow = nullptr; } + auto logWindow = qobject_cast(sender()); + if (logWindow) { + m_viewLogWindow = nullptr; + } // quit when there are no more windows. if (shouldExitNow()) { exit(0);