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