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