NOISSUE use model/view for Minecraft log data

This commit is contained in:
Petr Mrázek
2016-08-18 21:31:37 +02:00
parent 9aff21c181
commit 67eca08b22
11 changed files with 555 additions and 182 deletions

View File

@ -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

View File

@ -12,15 +12,118 @@
#include "GuiUtil.h"
#include <ColorCache.h>
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<LogColorCache> 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<LaunchTask> 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 &paragraph : paragraphs)
{
//TODO: implement filtering here.
filtered.append(paragraph);
}
QListIterator<QString> 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);
}

View File

@ -21,13 +21,13 @@
#include "launch/LaunchTask.h"
#include "BasePage.h"
#include <MultiMC.h>
#include <ColorCache.h>
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<LogColorCache> m_colors;
LogFormatProxyModel * m_proxy;
shared_qobject_ptr <LogModel> m_model;
};

View File

@ -34,7 +34,7 @@
</attribute>
<layout class="QGridLayout" name="gridLayout">
<item row="1" column="0" colspan="5">
<widget class="QPlainTextEdit" name="text">
<widget class="LogView" name="text">
<property name="undoRedoEnabled">
<bool>false</bool>
</property>
@ -159,6 +159,13 @@
</item>
</layout>
</widget>
<customwidgets>
<customwidget>
<class>LogView</class>
<extends>QPlainTextEdit</extends>
<header>widgets/LogView.h</header>
</customwidget>
</customwidgets>
<tabstops>
<tabstop>tabWidget</tabstop>
<tabstop>trackLogCheckbox</tabstop>

View File

@ -0,0 +1,143 @@
#include "LogView.h"
#include <QTextBlock>
#include <QScrollBar>
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<QFont>());
}
auto fg = m_model->data(idx, Qt::TextColorRole);
if(fg.isValid())
{
format.setForeground(fg.value<QColor>());
}
auto bg = m_model->data(idx, Qt::BackgroundRole);
if(bg.isValid())
{
format.setBackground(bg.value<QColor>());
}
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));
}

View File

@ -0,0 +1,36 @@
#pragma once
#include <QPlainTextEdit>
#include <QAbstractItemView>
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;
};