diff --git a/src/irc/messagemodel.cpp b/src/irc/messagemodel.cpp index 07774952..9ba05464 100644 --- a/src/irc/messagemodel.cpp +++ b/src/irc/messagemodel.cpp @@ -1,197 +1,201 @@ /* 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; either version 2 of the License, or (at your option) any later version. */ /* Copyright (C) 2017 Eike Hein */ #include "messagemodel.h" #include #define MAX_MESSAGES 500000 #define MAX_MESSAGES_TOLERANCE 501000 #define ALLOCATION_BATCH_SIZE 500 FilteredMessageModel::FilteredMessageModel(QObject *parent) : QSortFilterProxyModel(parent) , m_filterView(nullptr) { } FilteredMessageModel::~FilteredMessageModel() { } QObject *FilteredMessageModel::filterView() const { return m_filterView; } void FilteredMessageModel::setFilterView(QObject *view) { if (m_filterView != view) { m_filterView = view; invalidateFilter(); emit filterViewChanged(); } } bool FilteredMessageModel::filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const { Q_UNUSED(sourceParent) if (!m_filterView) { return false; } const QModelIndex &sourceIdx = sourceModel()->index(sourceRow, 0); const QObject *view = qvariant_cast(sourceIdx.data(MessageModel::View)); if (view && view == m_filterView) { return true; } return false; } QVariant FilteredMessageModel::data(const QModelIndex &index, int role) const { if (role == MessageModel::AuthorMatchesPrecedingMessage) { - const int precedingMessageRow = index.row() + 1; + const int precedingMessageRow = index.row() - 1; - if (precedingMessageRow < rowCount()) { + if (precedingMessageRow >= 0) { const QModelIndex &precedingMessage = QSortFilterProxyModel::index(precedingMessageRow, 0); return (index.data(MessageModel::Author) == precedingMessage.data(MessageModel::Author)); } + + return false; } if (role == MessageModel::TimeStampMatchesPrecedingMessage) { const int precedingMessageRow = index.row() + 1; - if (precedingMessageRow < rowCount()) { + if (precedingMessageRow >= 0) { const QModelIndex &precedingMessage = QSortFilterProxyModel::index(precedingMessageRow, 0); return (index.data(MessageModel::TimeStamp) == precedingMessage.data(MessageModel::TimeStamp)); } + + return false; } return QSortFilterProxyModel::data(index, role); } MessageModel::MessageModel(QObject *parent) : QAbstractListModel(parent) , m_allocCount(0) { // Pre-allocate batch. m_messages.reserve(ALLOCATION_BATCH_SIZE); } MessageModel::~MessageModel() { } QHash MessageModel::roleNames() const { QHash roles = QAbstractItemModel::roleNames(); QMetaEnum e = metaObject()->enumerator(metaObject()->indexOfEnumerator("AdditionalRoles")); for (int i = 0; i < e.keyCount(); ++i) { roles.insert(e.value(i), e.key(i)); } return roles; } QVariant MessageModel::data(const QModelIndex &index, int role) const { if (!index.isValid() || index.row() >= m_messages.count()) { return QVariant(); } const Message &msg = m_messages.at(index.row()); if (role == Qt::DisplayRole) { return msg.text; } else if (role == Type) { return (msg.action ? ActionMessage : NormalMessage); } else if (role == View) { return qVariantFromValue(msg.view); } else if (role == TimeStamp) { return msg.timeStamp; } else if (role == Author) { return msg.nick; } else if (role == NickColor) { return msg.nickColor; } return QVariant(); } int MessageModel::rowCount(const QModelIndex &parent) const { // Limit model to MAX_MESSAGES. return parent.isValid() ? 0 : qMax(MAX_MESSAGES, m_messages.count()); } void MessageModel::appendMessage(QObject *view, const QString &timeStamp, const QString &nick, const QColor &nickColor, const QString &text, const MessageType type) { - beginInsertRows(QModelIndex(), 0, 0); + beginInsertRows(QModelIndex(), m_messages.count(), m_messages.count()); Message msg; msg.view = view; msg.timeStamp = timeStamp; msg.nick = nick; msg.nickColor = nickColor; msg.text = text; msg.action = (type == ActionMessage); - m_messages.prepend(msg); + m_messages.append(msg); endInsertRows(); ++m_allocCount; // Grow the list in batches to make the insertion a little // faster. if (m_allocCount == ALLOCATION_BATCH_SIZE) { m_allocCount = 0; m_messages.reserve(m_messages.count() + ALLOCATION_BATCH_SIZE); } // Whenever we grow above MAX_MESSAGES_TOLERANCE, cull to // MAX_MESSAGES. I.e. we cull in batches, not on every new // message. if (m_messages.count() > MAX_MESSAGES_TOLERANCE) { - m_messages = m_messages.mid(0, MAX_MESSAGES); + m_messages = m_messages.mid(MAX_MESSAGES_TOLERANCE, MAX_MESSAGES); } } void MessageModel::cullMessages(const QObject *view) { int i = 0; while (i < m_messages.count()) { const Message &msg = m_messages.at(i); if (msg.view == view) { beginRemoveRows(QModelIndex(), i, i); m_messages.removeAt(i); endRemoveRows(); } else { ++i; } } } diff --git a/src/irc/messagemodel.h b/src/irc/messagemodel.h index 488e8573..940e03cb 100644 --- a/src/irc/messagemodel.h +++ b/src/irc/messagemodel.h @@ -1,95 +1,95 @@ /* 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; either version 2 of the License, or (at your option) any later version. */ /* Copyright (C) 2017 Eike Hein */ #include "chatwindow.h" #include #include #include struct Message { QObject *view; QString timeStamp; QString nick; QColor nickColor; QString text; bool action; }; class FilteredMessageModel : public QSortFilterProxyModel { Q_OBJECT Q_PROPERTY(QObject* filterView READ filterView WRITE setFilterView NOTIFY filterViewChanged) public: explicit FilteredMessageModel(QObject *parent = 0); virtual ~FilteredMessageModel(); QObject *filterView() const; void setFilterView(QObject *view); virtual QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; signals: void filterViewChanged() const; protected: bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const override; private: QObject *m_filterView; }; class MessageModel : public QAbstractListModel { Q_OBJECT public: enum AdditionalRoles { Type = Qt::UserRole + 1, View, TimeStamp, TimeStampMatchesPrecedingMessage, // Implemented in FilteredMessageModel for search efficiency. Author, AuthorMatchesPrecedingMessage, // Implemented in FilteredMessageModel for search efficiency. NickColor, }; Q_ENUM(AdditionalRoles) enum MessageType { NormalMessage = 0, ActionMessage }; Q_ENUM(MessageType) explicit MessageModel(QObject *parent = 0); virtual ~MessageModel(); QHash roleNames() const override; virtual QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; int rowCount(const QModelIndex &parent = QModelIndex()) const override; Q_INVOKABLE void appendMessage(QObject *view, const QString &timeStamp, const QString &nick, const QColor &nickColor, const QString &text, const MessageType type = NormalMessage); void cullMessages(const QObject *view); private: - QList m_messages; + QVector m_messages; int m_allocCount; }; diff --git a/src/qtquick/uipackages/default/contents/TextView.qml b/src/qtquick/uipackages/default/contents/TextView.qml index 3c387e95..e009b8ed 100644 --- a/src/qtquick/uipackages/default/contents/TextView.qml +++ b/src/qtquick/uipackages/default/contents/TextView.qml @@ -1,196 +1,204 @@ /* 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; either version 2 of the License, or (at your option) any later version. */ /* Copyright (C) 2017 Eike Hein */ import QtQuick 2.7 import QtQuick.Controls 2.2 as QQC2 import org.kde.kirigami 2.1 as Kirigami import org.kde.konversation 1.0 as Konversation import org.kde.konversation.uicomponents 1.0 as KUIC -KUIC.ListView { - id: textViewList +Item { + id: textView - visible: !konvUi.settingsMode + KUIC.ListView { + id: textViewList - QQC2.ScrollBar.vertical: QQC2.ScrollBar {} + anchors.bottom: parent.bottom - onHeightChanged: positionViewAtBeginning() + width: parent.width + height: Math.min(contentItem.height, parent.height) - readonly property int msgWidth: width - QQC2.ScrollBar.vertical.width + visible: !konvUi.settingsMode - verticalLayoutDirection: ListView.BottomToTop + QQC2.ScrollBar.vertical: QQC2.ScrollBar {} - model: messageModel - delegate: msgComponent + readonly property int msgWidth: width - QQC2.ScrollBar.vertical.width - ListView.onAdd: { - currentIndex = 0; - positionViewAtIndex(0, ListView.Contain); - } + model: messageModel + delegate: msgComponent + + onHeightChanged: positionViewAtEnd() + + onCountChanged: { + currentIndex = (count - 1); + positionViewAtEnd(); + } - Component { - id: msgComponent + Component { + id: msgComponent - Loader { - id: msg + Loader { + id: msg - width: ListView.view.msgWidth - height: (active ? (konvUi.largerFontSize + messageText.height + Kirigami.Units.gridUnit) - : messageText.height) + width: ListView.view.msgWidth + height: (active ? (konvUi.largerFontSize + messageText.height + Kirigami.Units.gridUnit) + : messageText.height) - readonly property int avatarSize: konvUi.largerFontSize * 3.6 - property var authorSize: Qt.point(0, 0) + readonly property int avatarSize: konvUi.largerFontSize * 3.6 + property var authorSize: Qt.point(0, 0) - readonly property bool showTimeStamp: !model.TimeStampMatchesPrecedingMessage - property Item timeStamp: null + readonly property bool showTimeStamp: !model.TimeStampMatchesPrecedingMessage + property Item timeStamp: null - active: !model.AuthorMatchesPrecedingMessage - sourceComponent: metabitsComponent + active: !model.AuthorMatchesPrecedingMessage + sourceComponent: metabitsComponent - onShowTimeStampChanged: { - if (!showTimeStamp) { - if (timeStamp) { - timeStamp.destroy(); + onShowTimeStampChanged: { + if (!showTimeStamp) { + if (timeStamp) { + timeStamp.destroy(); + } + } else { + timeStamp = timeStampComponent.createObject(msg); } - } else { - timeStamp = timeStampComponent.createObject(msg); } - } - Component { - id: timeStampComponent + Component { + id: timeStampComponent - Text { - id: timeStamp + Text { + id: timeStamp - readonly property bool collides: (messageText.x - + messageText.implicitWidth - + Kirigami.Units.smallSpacing + width > parent.width) - readonly property int margin: Kirigami.Units.gridUnit / 2 + readonly property bool collides: (messageText.x + + messageText.implicitWidth + + Kirigami.Units.smallSpacing + width > parent.width) + readonly property int margin: Kirigami.Units.gridUnit / 2 - x: messageText.x + margin + (active ? authorSize.x : messageText.contentWidth) + x: messageText.x + margin + (active ? authorSize.x : messageText.contentWidth) - y: { - if (!active) { - return messageText.y + ((largerFontMetrics.height / 2) - (height / 2)); - } else { - return (Kirigami.Units.gridUnit / 2) + ((authorSize.y / 2) - (height / 2)); + y: { + if (!active) { + return messageText.y + ((largerFontMetrics.height / 2) - (height / 2)); + } else { + return (Kirigami.Units.gridUnit / 2) + ((authorSize.y / 2) - (height / 2)); + } } - } - renderType: Text.NativeRendering - color: "grey" + renderType: Text.NativeRendering + color: "grey" - text: model.TimeStamp + text: model.TimeStamp + } } - } - Component { - id: metabitsComponent + Component { + id: metabitsComponent - Item { - anchors.fill: parent + Item { + anchors.fill: parent - Rectangle { - id: avatar + Rectangle { + id: avatar - x: Kirigami.Units.gridUnit / 2 - y: Kirigami.Units.gridUnit / 2 + x: Kirigami.Units.gridUnit / 2 + y: Kirigami.Units.gridUnit / 2 - width: avatarSize - height: avatarSize + width: avatarSize + height: avatarSize - color: model.NickColor + color: model.NickColor - radius: width * 0.5 + radius: width * 0.5 - Text { - anchors.fill: parent - anchors.margins: Kirigami.Units.smallSpacing + Text { + anchors.fill: parent + anchors.margins: Kirigami.Units.smallSpacing - renderType: Text.QtRendering - color: "white" + renderType: Text.QtRendering + color: "white" - font.weight: Font.Bold - font.pointSize: 100 - minimumPointSize: theme.defaultFont.pointSize - fontSizeMode: Text.Fit + font.weight: Font.Bold + font.pointSize: 100 + minimumPointSize: theme.defaultFont.pointSize + fontSizeMode: Text.Fit - horizontalAlignment: Text.AlignHCenter - verticalAlignment: Text.AlignVCenter + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter - text: { - // WIPQTQUICK HACK TODO Probably doesn't work with non-latin1. - var match = model.Author.match(/([a-zA-Z])([a-zA-Z])/); - var abbrev = match[1].toUpperCase(); + text: { + // WIPQTQUICK HACK TODO Probably doesn't work with non-latin1. + var match = model.Author.match(/([a-zA-Z])([a-zA-Z])/); + var abbrev = match[1].toUpperCase(); - if (match.length > 2) { - abbrev += match[2].toLowerCase(); - } + if (match.length > 2) { + abbrev += match[2].toLowerCase(); + } - return abbrev; + return abbrev; + } } } - } - Text { - id: author + Text { + id: author - y: Kirigami.Units.gridUnit / 2 + y: Kirigami.Units.gridUnit / 2 - anchors.left: parent.left - anchors.leftMargin: avatarSize + Kirigami.Units.gridUnit + anchors.left: parent.left + anchors.leftMargin: avatarSize + Kirigami.Units.gridUnit - renderType: Text.NativeRendering - color: model.NickColor + renderType: Text.NativeRendering + color: model.NickColor - font.weight: Font.Bold - font.pixelSize: konvUi.largerFontSize + font.weight: Font.Bold + font.pixelSize: konvUi.largerFontSize - text: model.Author + text: model.Author - onWidthChanged: msg.authorSize = Qt.point(width, height) + onWidthChanged: msg.authorSize = Qt.point(width, height) + } } } - } - Text { - id: messageText + Text { + id: messageText - anchors.left: parent.left - anchors.leftMargin: avatarSize + Kirigami.Units.gridUnit - anchors.right: parent.right - anchors.rightMargin: (timeStamp && timeStamp.collides - ? timeStamp.margin + timeStamp.width : 0) - anchors.bottom: parent.bottom + anchors.left: parent.left + anchors.leftMargin: avatarSize + Kirigami.Units.gridUnit + anchors.right: parent.right + anchors.rightMargin: (timeStamp && timeStamp.collides + ? timeStamp.margin + timeStamp.width : 0) + anchors.bottom: parent.bottom - renderType: Text.NativeRendering - textFormat: Text.StyledText + renderType: Text.NativeRendering + textFormat: Text.StyledText - font.pixelSize: konvUi.largerFontSize + font.pixelSize: konvUi.largerFontSize - wrapMode: Text.Wrap + wrapMode: Text.Wrap - text: (model.Type == Konversation.MessageModel.ActionMessage - ? actionWrap(model.display) : model.display) + text: (model.Type == Konversation.MessageModel.ActionMessage + ? actionWrap(model.display) : model.display) - function actionWrap(text) { - return "" + model.Author + " " + text + ""; - } + function actionWrap(text) { + return "" + model.Author + " " + text + ""; + } - onLinkActivated: konvApp.openUrl(link) + onLinkActivated: konvApp.openUrl(link) + } } } } + }