From 67eca08b2260f19ff296c0b6cb73eb3b0479e4b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petr=20Mr=C3=A1zek?= Date: Thu, 18 Aug 2016 21:31:37 +0200 Subject: NOISSUE use model/view for Minecraft log data --- application/CMakeLists.txt | 2 + application/pages/LogPage.cpp | 317 ++++++++++++++++++++-------------------- application/pages/LogPage.h | 14 +- application/pages/LogPage.ui | 9 +- application/widgets/LogView.cpp | 143 ++++++++++++++++++ application/widgets/LogView.h | 36 +++++ 6 files changed, 349 insertions(+), 172 deletions(-) create mode 100644 application/widgets/LogView.cpp create mode 100644 application/widgets/LogView.h (limited to 'application') diff --git a/application/CMakeLists.txt b/application/CMakeLists.txt index 0bc103bf..b681f3fd 100644 --- a/application/CMakeLists.txt +++ b/application/CMakeLists.txt @@ -210,6 +210,8 @@ SET(MULTIMC_SOURCES widgets/LabeledToolButton.h widgets/LineSeparator.cpp widgets/LineSeparator.h + widgets/LogView.cpp + widgets/LogView.h widgets/MCModInfoFrame.cpp widgets/MCModInfoFrame.h widgets/ModListView.cpp diff --git a/application/pages/LogPage.cpp b/application/pages/LogPage.cpp index de4ed4f3..e7b670ea 100644 --- a/application/pages/LogPage.cpp +++ b/application/pages/LogPage.cpp @@ -12,15 +12,118 @@ #include "GuiUtil.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::TextColorRole: + { + 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(); - // create the format and set its font + 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 { - defaultFormat = new QTextCharFormat(ui->text->currentCharFormat()); QString fontFamily = MMC->settings()->get("ConsoleFont").toString(); bool conversionOk = false; int fontSize = MMC->settings()->get("ConsoleFontSize").toInt(&conversionOk); @@ -28,23 +131,10 @@ LogPage::LogPage(InstancePtr instance, QWidget *parent) { fontSize = 11; } - defaultFormat->setFont(QFont(fontFamily, fontSize)); + m_proxy->setFont(QFont(fontFamily, fontSize)); } - // ensure we don't eat all the RAM - { - auto lineSetting = MMC->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; - } - ui->text->setMaximumBlockCount(maxLines); - - m_stopOnOverflow = MMC->settings()->get("ConsoleOverflowStop").toBool(); - } + ui->text->setModel(m_proxy); // set up instance and launch process recognition { @@ -53,16 +143,10 @@ LogPage::LogPage(InstancePtr instance, QWidget *parent) { on_InstanceLaunchTask_changed(launchTask); } - connect(m_instance.get(), &BaseInstance::launchTaskChanged, - this, &LogPage::on_InstanceLaunchTask_changed); + connect(m_instance.get(), &BaseInstance::launchTaskChanged, this, &LogPage::on_InstanceLaunchTask_changed); } - // set up text colors 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_colors.reset(new LogColorCache(origForeground, origBackground)); - } + ui->text->setWordWrap(true); auto findShortcut = new QShortcut(QKeySequence(QKeySequence::Find), this); connect(findShortcut, SIGNAL(activated()), SLOT(findActivated())); @@ -76,20 +160,33 @@ LogPage::LogPage(InstancePtr instance, QWidget *parent) LogPage::~LogPage() { delete ui; - delete defaultFormat; } void LogPage::on_InstanceLaunchTask_changed(std::shared_ptr proc) { + m_process = proc; if(m_process) { - disconnect(m_process.get(), &LaunchTask::log, this, &LogPage::write); + m_model = proc->getLogModel(); + auto lineSetting = MMC->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; + } + m_model->setMaxLines(maxLines); + m_model->setStopOnOverflow(MMC->settings()->get("ConsoleOverflowStop").toBool()); + m_model->setOverflowMessage(tr("MultiMC 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 loggging to files and" + " likely wasting harddrive space at an alarming rate!").arg(maxLines)); + m_proxy->setSourceModel(m_model.get()); } - m_process = proc; - if(m_process) + else { - ui->text->clear(); - connect(m_process.get(), &LaunchTask::log, this, &LogPage::write); + m_proxy->setSourceModel(nullptr); + m_model.reset(); } } @@ -100,38 +197,45 @@ bool LogPage::apply() bool LogPage::shouldDisplay() const { - return m_instance->isRunning() || ui->text->blockCount() > 1; + 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! - write(tr("MultiMC: Log upload triggered at: %1").arg(QDateTime::currentDateTime().toString(Qt::RFC2822Date)), MessageLevel::MultiMC); - auto url = GuiUtil::uploadPaste(ui->text->toPlainText(), this); + m_model->append(MessageLevel::MultiMC, tr("MultiMC: Log upload triggered at: %1").arg(QDateTime::currentDateTime().toString(Qt::RFC2822Date))); + auto url = GuiUtil::uploadPaste(m_model->toPlainText(), this); if(!url.isEmpty()) { - write(tr("MultiMC: Log uploaded to: %1").arg(url), MessageLevel::MultiMC); + m_model->append(MessageLevel::MultiMC, tr("MultiMC: Log uploaded to: %1").arg(url)); } else { - write(tr("MultiMC: Log upload failed!"), MessageLevel::Error); + m_model->append(MessageLevel::Error, tr("MultiMC: Log upload failed!")); } } void LogPage::on_btnCopy_clicked() { - write(QString("Clipboard copy at: %1").arg(QDateTime::currentDateTime().toString(Qt::RFC2822Date)), MessageLevel::MultiMC); - GuiUtil::setClipboardText(ui->text->toPlainText()); + if(!m_model) + return; + m_model->append(MessageLevel::MultiMC, QString("Clipboard copy at: %1").arg(QDateTime::currentDateTime().toString(Qt::RFC2822Date))); + GuiUtil::setClipboardText(m_model->toPlainText()); } void LogPage::on_btnClear_clicked() { - ui->text->clear(); + if(!m_model) + return; + m_model->clear(); } void LogPage::on_btnBottom_clicked() { - ui->text->verticalScrollBar()->setSliderPosition(ui->text->verticalScrollBar()->maximum()); + ui->text->scrollToBottom(); } void LogPage::on_trackLogCheckbox_clicked(bool checked) @@ -141,60 +245,33 @@ void LogPage::on_trackLogCheckbox_clicked(bool checked) void LogPage::on_wrapCheckbox_clicked(bool checked) { - if(checked) - { - ui->text->setWordWrapMode(QTextOption::WrapAtWordBoundaryOrAnywhere); - } - else - { - ui->text->setWordWrapMode(QTextOption::WrapMode::NoWrap); - } + ui->text->setWordWrap(checked); } void LogPage::on_findButton_clicked() { auto modifiers = QApplication::keyboardModifiers(); - if (modifiers & Qt::ShiftModifier) - { - findPreviousActivated(); - } - else - { - findNextActivated(); - } + bool reverse = modifiers & Qt::ShiftModifier; + ui->text->findNext(ui->searchBar->text(), reverse); } -void LogPage::findActivated() +void LogPage::findNextActivated() { - // focus the search bar if it doesn't have focus - if (!ui->searchBar->hasFocus()) - { - auto searchForCursor = ui->text->textCursor(); - auto searchForString = searchForCursor.selectedText(); - if (searchForString.size()) - { - ui->searchBar->setText(searchForString); - } - ui->searchBar->setFocus(); - ui->searchBar->selectAll(); - } + ui->text->findNext(ui->searchBar->text(), false); } -void LogPage::findNextActivated() +void LogPage::findPreviousActivated() { - auto toSearch = ui->searchBar->text(); - if (toSearch.size()) - { - ui->text->find(toSearch); - } + ui->text->findNext(ui->searchBar->text(), true); } -void LogPage::findPreviousActivated() +void LogPage::findActivated() { - auto toSearch = ui->searchBar->text(); - if (toSearch.size()) + // focus the search bar if it doesn't have focus + if (!ui->searchBar->hasFocus()) { - ui->text->find(toSearch, QTextDocument::FindBackward); + ui->searchBar->setFocus(); + ui->searchBar->selectAll(); } } @@ -202,85 +279,3 @@ void LogPage::setParentContainer(BasePageContainer * container) { m_parentContainer = container; } - -void LogPage::write(QString data, MessageLevel::Enum mode) -{ - if (!m_write_active) - { - if (mode != MessageLevel::MultiMC) - { - return; - } - } - if(m_stopOnOverflow && m_write_active) - { - if(mode != MessageLevel::MultiMC) - { - if(ui->text->blockCount() >= ui->text->maximumBlockCount()) - { - m_write_active = false; - data = tr("MultiMC 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 loggging to files and" - " likely wasting harddrive space at an alarming rate!") - .arg(ui->text->maximumBlockCount()); - mode = MessageLevel::Fatal; - ui->trackLogCheckbox->setCheckState(Qt::Unchecked); - if(!isVisible()) - { - m_parentContainer->selectPage(id()); - } - } - } - } - - // save the cursor so it can be restored. - auto savedCursor = ui->text->cursor(); - - QScrollBar *bar = ui->text->verticalScrollBar(); - int max_bar = bar->maximum(); - int val_bar = bar->value(); - if (isVisible()) - { - if (m_scroll_active) - { - m_scroll_active = (max_bar - val_bar) <= 1; - } - else - { - m_scroll_active = val_bar == max_bar; - } - } - if (data.endsWith('\n')) - data = data.left(data.length() - 1); - QStringList paragraphs = data.split('\n'); - QStringList filtered; - for (QString ¶graph : paragraphs) - { - //TODO: implement filtering here. - filtered.append(paragraph); - } - QListIterator iter(filtered); - QTextCharFormat format(*defaultFormat); - - format.setForeground(m_colors->getFront(mode)); - format.setBackground(m_colors->getBack(mode)); - - while (iter.hasNext()) - { - // append a paragraph/line - auto workCursor = ui->text->textCursor(); - workCursor.movePosition(QTextCursor::End); - workCursor.insertText(iter.next(), format); - workCursor.insertBlock(); - } - - if (isVisible()) - { - if (m_scroll_active) - { - bar->setValue(bar->maximum()); - } - m_last_scroll_value = bar->value(); - } - ui->text->setCursor(savedCursor); -} diff --git a/application/pages/LogPage.h b/application/pages/LogPage.h index e902ad13..f98b5ecf 100644 --- a/application/pages/LogPage.h +++ b/application/pages/LogPage.h @@ -21,13 +21,13 @@ #include "launch/LaunchTask.h" #include "BasePage.h" #include -#include namespace Ui { class LogPage; } class QTextCharFormat; +class LogFormatProxyModel; class LogPage : public QWidget, public BasePage { @@ -57,13 +57,6 @@ public: virtual void setParentContainer(BasePageContainer *) override; private slots: - /** - * @brief write a string - * @param data the string - * @param level the @MessageLevel the string should be written under - * lines have to be put through this as a whole! - */ - void write(QString data, MessageLevel::Enum level = MessageLevel::MultiMC); void on_btnPaste_clicked(); void on_btnCopy_clicked(); void on_btnClear_clicked(); @@ -88,8 +81,9 @@ private: int m_saved_offset = 0; bool m_write_active = true; bool m_stopOnOverflow = true; + bool m_autoScroll = false; - QTextCharFormat * defaultFormat; BasePageContainer * m_parentContainer; - std::unique_ptr m_colors; + LogFormatProxyModel * m_proxy; + shared_qobject_ptr m_model; }; diff --git a/application/pages/LogPage.ui b/application/pages/LogPage.ui index bf5ec004..4843d7c3 100644 --- a/application/pages/LogPage.ui +++ b/application/pages/LogPage.ui @@ -34,7 +34,7 @@ - + false @@ -159,6 +159,13 @@ + + + LogView + QPlainTextEdit +
widgets/LogView.h
+
+
tabWidget trackLogCheckbox diff --git a/application/widgets/LogView.cpp b/application/widgets/LogView.cpp new file mode 100644 index 00000000..9cd91b2e --- /dev/null +++ b/application/widgets/LogView.cpp @@ -0,0 +1,143 @@ +#include "LogView.h" +#include +#include + +LogView::LogView(QWidget* parent) : QPlainTextEdit(parent) +{ + setWordWrapMode(QTextOption::WrapAtWordBoundaryOrAnywhere); + m_defaultFormat = new QTextCharFormat(currentCharFormat()); +} + +LogView::~LogView() +{ +} + +void LogView::setWordWrap(bool wrapping) +{ + if(wrapping) + { + setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + setLineWrapMode(QPlainTextEdit::WidgetWidth); + } + else + { + setHorizontalScrollBarPolicy(Qt::ScrollBarAsNeeded); + setLineWrapMode(QPlainTextEdit::NoWrap); + } +} + +void LogView::setModel(QAbstractItemModel* model) +{ + if(m_model) + { + disconnect(m_model, &QAbstractItemModel::modelReset, this, &LogView::repopulate); + disconnect(m_model, &QAbstractItemModel::rowsInserted, this, &LogView::rowsInserted); + disconnect(m_model, &QAbstractItemModel::rowsAboutToBeInserted, this, &LogView::rowsAboutToBeInserted); + disconnect(m_model, &QAbstractItemModel::rowsRemoved, this, &LogView::rowsRemoved); + } + m_model = model; + if(m_model) + { + connect(m_model, &QAbstractItemModel::modelReset, this, &LogView::repopulate); + connect(m_model, &QAbstractItemModel::rowsInserted, this, &LogView::rowsInserted); + connect(m_model, &QAbstractItemModel::rowsAboutToBeInserted, this, &LogView::rowsAboutToBeInserted); + connect(m_model, &QAbstractItemModel::rowsRemoved, this, &LogView::rowsRemoved); + connect(m_model, &QAbstractItemModel::destroyed, this, &LogView::modelDestroyed); + } + repopulate(); +} + +QAbstractItemModel * LogView::model() const +{ + return m_model; +} + +void LogView::modelDestroyed(QObject* model) +{ + if(m_model == model) + { + setModel(nullptr); + } +} + +void LogView::repopulate() +{ + auto doc = document(); + doc->clear(); + if(!m_model) + { + return; + } + rowsInserted(QModelIndex(), 0, m_model->rowCount() - 1); +} + +void LogView::rowsAboutToBeInserted(const QModelIndex& parent, int first, int last) +{ + Q_UNUSED(parent) + Q_UNUSED(first) + Q_UNUSED(last) + QScrollBar *bar = verticalScrollBar(); + int max_bar = bar->maximum(); + int val_bar = bar->value(); + if (m_scroll) + { + m_scroll = (max_bar - val_bar) <= 1; + } + else + { + m_scroll = val_bar == max_bar; + } +} + +void LogView::rowsInserted(const QModelIndex& parent, int first, int last) +{ + for(int i = first; i <= last; i++) + { + auto idx = m_model->index(i, 0, parent); + auto text = m_model->data(idx, Qt::DisplayRole).toString(); + QTextCharFormat format(*m_defaultFormat); + auto font = m_model->data(idx, Qt::FontRole); + if(font.isValid()) + { + format.setFont(font.value()); + } + auto fg = m_model->data(idx, Qt::TextColorRole); + if(fg.isValid()) + { + format.setForeground(fg.value()); + } + auto bg = m_model->data(idx, Qt::BackgroundRole); + if(bg.isValid()) + { + format.setBackground(bg.value()); + } + auto workCursor = textCursor(); + workCursor.movePosition(QTextCursor::End); + workCursor.insertText(text, format); + workCursor.insertBlock(); + } + if(m_scroll && !m_scrolling) + { + m_scrolling = true; + QMetaObject::invokeMethod( this, "scrollToBottom", Qt::QueuedConnection); + } +} + +void LogView::rowsRemoved(const QModelIndex& parent, int first, int last) +{ + // TODO: some day... maybe + Q_UNUSED(parent) + Q_UNUSED(first) + Q_UNUSED(last) +} + +void LogView::scrollToBottom() +{ + m_scrolling = false; + verticalScrollBar()->setSliderPosition(verticalScrollBar()->maximum()); +} + +void LogView::findNext(const QString& what, bool reverse) +{ + find(what, reverse ? QTextDocument::FindFlag::FindBackward : QTextDocument::FindFlag(0)); +} diff --git a/application/widgets/LogView.h b/application/widgets/LogView.h new file mode 100644 index 00000000..bb6747cd --- /dev/null +++ b/application/widgets/LogView.h @@ -0,0 +1,36 @@ +#pragma once +#include +#include + +class QAbstractItemModel; + +class LogView: public QPlainTextEdit +{ + Q_OBJECT +public: + explicit LogView(QWidget *parent = nullptr); + virtual ~LogView(); + + virtual void setModel(QAbstractItemModel *model); + QAbstractItemModel *model() const; + +public slots: + void setWordWrap(bool wrapping); + void findNext(const QString & what, bool reverse); + void scrollToBottom(); + +protected slots: + void repopulate(); + // note: this supports only appending + void rowsInserted(const QModelIndex &parent, int first, int last); + void rowsAboutToBeInserted(const QModelIndex &parent, int first, int last); + // note: this supports only removing from front + void rowsRemoved(const QModelIndex &parent, int first, int last); + void modelDestroyed(QObject * model); + +protected: + QAbstractItemModel *m_model = nullptr; + QTextCharFormat *m_defaultFormat = nullptr; + bool m_scroll = false; + bool m_scrolling = false; +}; -- cgit v1.2.3