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: [PATCH] NOISSUE use model/view for Minecraft log data --- api/logic/CMakeLists.txt | 2 + api/logic/launch/LaunchTask.cpp | 12 +- api/logic/launch/LaunchTask.h | 12 +- api/logic/launch/LogModel.cpp | 135 ++++++++++++++ api/logic/launch/LogModel.h | 51 +++++ application/CMakeLists.txt | 2 + application/pages/LogPage.cpp | 321 ++++++++++++++++---------------- application/pages/LogPage.h | 14 +- application/pages/LogPage.ui | 9 +- application/widgets/LogView.cpp | 143 ++++++++++++++ application/widgets/LogView.h | 36 ++++ 11 files changed, 555 insertions(+), 182 deletions(-) create mode 100644 api/logic/launch/LogModel.cpp create mode 100644 api/logic/launch/LogModel.h create mode 100644 application/widgets/LogView.cpp create mode 100644 application/widgets/LogView.h diff --git a/api/logic/CMakeLists.txt b/api/logic/CMakeLists.txt index 415584fc..c430b53f 100644 --- a/api/logic/CMakeLists.txt +++ b/api/logic/CMakeLists.txt @@ -119,6 +119,8 @@ set(LAUNCH_SOURCES launch/LaunchTask.h launch/LoggedProcess.cpp launch/LoggedProcess.h + launch/LogModel.cpp + launch/LogModel.h launch/MessageLevel.cpp launch/MessageLevel.h ) diff --git a/api/logic/launch/LaunchTask.cpp b/api/logic/launch/LaunchTask.cpp index 5b7ff182..9c09caf4 100644 --- a/api/logic/launch/LaunchTask.cpp +++ b/api/logic/launch/LaunchTask.cpp @@ -167,6 +167,15 @@ bool LaunchTask::abort() return false; } +shared_qobject_ptr LaunchTask::getLogModel() +{ + if(!m_logModel) + { + m_logModel.reset(new LogModel()); + } + return m_logModel; +} + void LaunchTask::onLogLines(const QStringList &lines, MessageLevel::Enum defaultLevel) { for (auto & line: lines) @@ -193,7 +202,8 @@ void LaunchTask::onLogLine(QString line, MessageLevel::Enum level) // censor private user info line = censorPrivateInfo(line); - emit log(line, level); + auto &model = *getLogModel(); + model.append(level, line); } void LaunchTask::emitSucceeded() diff --git a/api/logic/launch/LaunchTask.h b/api/logic/launch/LaunchTask.h index 447445ca..f2fb3a2c 100644 --- a/api/logic/launch/LaunchTask.h +++ b/api/logic/launch/LaunchTask.h @@ -17,6 +17,8 @@ #pragma once #include +#include +#include "LogModel.h" #include "BaseInstance.h" #include "MessageLevel.h" #include "LoggedProcess.h" @@ -80,6 +82,8 @@ public: /* methods */ */ virtual bool abort() override; + shared_qobject_ptr getLogModel(); + public: QString substituteVariables(const QString &cmd) const; QString censorPrivateInfo(QString in); @@ -98,13 +102,6 @@ signals: void requestLogging(); - /** - * @brief emitted when we want to log something - * @param text the text to log - * @param level the level to log at - */ - void log(QString text, MessageLevel::Enum level = MessageLevel::MultiMC); - public slots: void onLogLines(const QStringList& lines, MessageLevel::Enum defaultLevel = MessageLevel::MultiMC); void onLogLine(QString line, MessageLevel::Enum defaultLevel = MessageLevel::MultiMC); @@ -114,6 +111,7 @@ public slots: protected: /* data */ InstancePtr m_instance; + shared_qobject_ptr m_logModel; QList > m_steps; QMap m_censorFilter; int currentStep = -1; diff --git a/api/logic/launch/LogModel.cpp b/api/logic/launch/LogModel.cpp new file mode 100644 index 00000000..c12a0488 --- /dev/null +++ b/api/logic/launch/LogModel.cpp @@ -0,0 +1,135 @@ +#include "LogModel.h" + +LogModel::LogModel(QObject *parent):QAbstractListModel(parent) +{ + m_content.resize(m_maxLines); +} + +int LogModel::rowCount(const QModelIndex &parent) const +{ + if (parent.isValid()) + return 0; + + return m_numLines; +} + +QVariant LogModel::data(const QModelIndex &index, int role) const +{ + if (index.row() < 0 || index.row() >= m_numLines) + return QVariant(); + + auto row = index.row(); + auto realRow = (row + m_firstLine) % m_maxLines; + if (role == Qt::DisplayRole || role == Qt::EditRole) + { + return m_content[realRow].line; + } + if(role == LevelRole) + { + return m_content[realRow].level; + } + + return QVariant(); +} + +void LogModel::append(MessageLevel::Enum level, QString line) +{ + int lineNum = (m_firstLine + m_numLines) % m_maxLines; + // overflow + if(m_numLines == m_maxLines) + { + if(m_stopOnOverflow) + { + // nothing more to do, the buffer is full + return; + } + beginRemoveRows(QModelIndex(), 0, 0); + m_firstLine = (m_firstLine + 1) % m_maxLines; + m_numLines --; + endRemoveRows(); + } + else if (m_numLines == m_maxLines - 1 && m_stopOnOverflow) + { + level = MessageLevel::Fatal; + line = m_overflowMessage; + } + beginInsertRows(QModelIndex(), m_numLines, m_numLines); + m_numLines ++; + m_content[lineNum].level = level; + m_content[lineNum].line = line; + endInsertRows(); +} + +void LogModel::clear() +{ + beginResetModel(); + m_firstLine = 0; + m_numLines = 0; + endResetModel(); +} + +QString LogModel::toPlainText() +{ + QString out; + out.reserve(m_numLines * 80); + for(int i = 0; i < m_numLines; i++) + { + QString & line = m_content[(m_firstLine + i) % m_maxLines].line; + out.append(line + '\n'); + } + out.squeeze(); + return out; +} + +void LogModel::setMaxLines(int maxLines) +{ + // no-op + if(maxLines == m_maxLines) + { + return; + } + // if it all still fits in the buffer, just resize it + if(m_firstLine + m_numLines < maxLines) + { + m_maxLines = maxLines; + m_content.resize(maxLines); + return; + } + // otherwise, we need to reorganize the data because it crosses the wrap boundary + QVector newContent; + newContent.resize(maxLines); + if(m_numLines <= maxLines) + { + // if it all fits in the new buffer, just copy it over + for(int i = 0; i < m_numLines; i++) + { + newContent[i] = m_content[(m_firstLine + i) % m_maxLines]; + } + m_content.swap(newContent); + } + else + { + // if it doesn't fit, part of the data needs to be thrown away (the oldest log messages) + int lead = m_numLines - maxLines; + beginRemoveRows(QModelIndex(), 0, lead - 1); + for(int i = 0; i < maxLines; i++) + { + newContent[i] = m_content[(m_firstLine + lead + i) % m_maxLines]; + } + m_numLines = m_maxLines; + m_content.swap(newContent); + endRemoveRows(); + } + m_firstLine = 0; + m_maxLines = maxLines; +} + +void LogModel::setStopOnOverflow(bool stop) +{ + m_stopOnOverflow = stop; +} + +void LogModel::setOverflowMessage(const QString& overflowMessage) +{ + m_overflowMessage = overflowMessage; +} diff --git a/api/logic/launch/LogModel.h b/api/logic/launch/LogModel.h new file mode 100644 index 00000000..87e6b583 --- /dev/null +++ b/api/logic/launch/LogModel.h @@ -0,0 +1,51 @@ +#pragma once + +#include +#include +#include "MessageLevel.h" + +#include + +class MULTIMC_LOGIC_EXPORT LogModel : public QAbstractListModel +{ + Q_OBJECT +public: + explicit LogModel(QObject *parent = 0); + + int rowCount(const QModelIndex &parent = QModelIndex()) const; + QVariant data(const QModelIndex &index, int role) const; + + void append(MessageLevel::Enum, QString line); + void clear(); + + QString toPlainText(); + + void setMaxLines(int maxLines); + void setStopOnOverflow(bool stop); + void setOverflowMessage(const QString & overflowMessage); + + enum Roles + { + LevelRole = Qt::UserRole + }; + +private /* types */: + struct entry + { + MessageLevel::Enum level; + QString line; + }; + +private: /* data */ + QVector m_content; + int m_maxLines = 1000; + // first line in the circular buffer + int m_firstLine = 0; + // number of lines occupied in the circular buffer + int m_numLines = 0; + bool m_stopOnOverflow = false; + QString m_overflowMessage = "OVERFLOW"; + +private: + Q_DISABLE_COPY(LogModel) +}; 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) { - if(m_process) - { - disconnect(m_process.get(), &LaunchTask::log, this, &LogPage::write); - } m_process = proc; if(m_process) { - ui->text->clear(); - connect(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()); + } + else + { + 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,27 +245,24 @@ 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::findNextActivated() +{ + ui->text->findNext(ui->searchBar->text(), false); +} + +void LogPage::findPreviousActivated() +{ + ui->text->findNext(ui->searchBar->text(), true); } void LogPage::findActivated() @@ -169,118 +270,12 @@ void LogPage::findActivated() // 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(); } } -void LogPage::findNextActivated() -{ - auto toSearch = ui->searchBar->text(); - if (toSearch.size()) - { - ui->text->find(toSearch); - } -} - -void LogPage::findPreviousActivated() -{ - auto toSearch = ui->searchBar->text(); - if (toSearch.size()) - { - ui->text->find(toSearch, QTextDocument::FindBackward); - } -} - 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; +};