diff --git a/src/irc/messagemodel.cpp b/src/irc/messagemodel.cpp index 80682135..a35a18f1 100644 --- a/src/irc/messagemodel.cpp +++ b/src/irc/messagemodel.cpp @@ -1,150 +1,173 @@ /* 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 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; + + if (precedingMessageRow < rowCount()) { + const QModelIndex &precedingMessage = QSortFilterProxyModel::index(precedingMessageRow, 0); + return (index.data(MessageModel::Author) == precedingMessage.data(MessageModel::Author)); + } + } + + if (role == MessageModel::TimeStampMatchesPrecedingMessage) { + const int precedingMessageRow = index.row() + 1; + + if (precedingMessageRow < rowCount()) { + const QModelIndex &precedingMessage = QSortFilterProxyModel::index(precedingMessageRow, 0); + return (index.data(MessageModel::TimeStamp) == precedingMessage.data(MessageModel::TimeStamp)); + } + } + + return QSortFilterProxyModel::data(index, role); +} + MessageModel::MessageModel(QObject *parent) : QAbstractListModel(parent) { } 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 == Nick) { + } else if (role == Author) { return msg.nick; } else if (role == NickColor) { return msg.nickColor; } return QVariant(); } int MessageModel::rowCount(const QModelIndex &parent) const { return parent.isValid() ? 0 : 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); 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); endInsertRows(); } 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 aabc7b74..721b7102 100644 --- a/src/irc/messagemodel.h +++ b/src/irc/messagemodel.h @@ -1,90 +1,94 @@ /* 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, - Nick, - NickColor + 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; }; diff --git a/src/qtquick/uipackages/default/contents/Message.qml b/src/qtquick/uipackages/default/contents/Message.qml index 54df50c5..4bf29763 100644 --- a/src/qtquick/uipackages/default/contents/Message.qml +++ b/src/qtquick/uipackages/default/contents/Message.qml @@ -1,224 +1,200 @@ /* 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 org.kde.kirigami 2.1 as Kirigami import org.kde.konversation 1.0 as Konversation Item { id: msg width: ListView.view.width height: (metabitsLoader.active ? (konvApp.largerFontSize + messageText.height + Kirigami.Units.gridUnit) : messageText.height) - property string user: model.Nick - property int row: index - property int avatarSize: nick.height * 2 + readonly property int avatarSize: nick.height * 2 property bool selectable: false property Item selectableText: null - property bool linkHovered: selectableText && selectableText.hoveredLink != "" - - - - onRowChanged: metabitsLoader.active = showMetabits() + readonly property bool linkHovered: selectableText && selectableText.hoveredLink != "" onSelectableChanged: { if (selectable) { selectableText = selectableTextComponent.createObject(msg); selectableText.forceActiveFocus(); } else if (selectableText) { selectableText.destroy(); } } - function showMetabits() { - if (row == (messageModel.rowCount() - 1)) { - return true; - } - - var prevNick = messageModel.data(messageModel.index(row + 1, 0), - Konversation.MessageModel.Nick); - - return (prevNick != model.Nick); - } - Text { // WIPQTQUICK TODO Only outside loader to set avatar height. id: nick visible: metabitsLoader.active y: Kirigami.Units.gridUnit / 2 anchors.left: parent.left anchors.leftMargin: avatarSize + Kirigami.Units.gridUnit renderType: Text.NativeRendering color: model.NickColor font.weight: Font.Bold font.pixelSize: konvApp.largerFontSize - text: model.Nick + text: model.Author } Loader { id: metabitsLoader - active: false + active: !model.AuthorMatchesPrecedingMessage anchors.fill: parent sourceComponent: metabitsComponent } Component { id: metabitsComponent Item { anchors.fill: parent Rectangle { id: avatar x: Kirigami.Units.gridUnit / 2 y: Kirigami.Units.gridUnit / 2 width: avatarSize height: avatarSize color: model.NickColor radius: width * 0.5 Text { anchors.fill: parent anchors.margins: Kirigami.Units.smallSpacing renderType: Text.QtRendering color: "white" font.weight: Font.Bold font.pointSize: 100 minimumPointSize: theme.defaultFont.pointSize fontSizeMode: Text.Fit horizontalAlignment: Text.AlignHCenter verticalAlignment: Text.AlignVCenter text: { // WIPQTQUICK HACK TODO Probably doesn't work with non-latin1. - var match = model.Nick.match(/([a-zA-Z])([a-zA-Z])/); + var match = model.Author.match(/([a-zA-Z])([a-zA-Z])/); var abbrev = match[1].toUpperCase(); if (match.length > 2) { abbrev += match[2].toLowerCase(); } return abbrev; } } } Text { id: timeStamp y: Kirigami.Units.gridUnit / 2 height: nick.height anchors.left: parent.left anchors.leftMargin: (avatarSize + Kirigami.Units.gridUnit + nick.width + (Kirigami.Units.gridUnit / 2)) renderType: Text.NativeRendering color: "grey" text: model.TimeStamp verticalAlignment: Text.AlignVCenter } } } Text { id: messageText visible: !selectable anchors.left: parent.left anchors.leftMargin: avatarSize + Kirigami.Units.gridUnit anchors.right: parent.right anchors.bottom: parent.bottom renderType: Text.NativeRendering textFormat: Text.RichText font.pixelSize: konvApp.largerFontSize wrapMode: Text.WordWrap text: { var t = model.display; if (model.Type == Konversation.MessageModel.ActionMessage) { t = "" + model.Nick + " " + t + ""; } if (metabitsLoader.active) { return t; - } else { - var prevTimeStamp = messageModel.data(messageModel.index(row + 1, 0), - Konversation.MessageModel.TimeStamp); - - if (model.TimeStamp != prevTimeStamp) { - return t + "  " + model.TimeStamp + ""; - } + } else if (!model.TimeStampMatchesPrecedingMessage) { + return t + "  " + model.TimeStamp + ""; } return t; } onLinkActivated: Qt.openUrlExternally(link) } Component { id: selectableTextComponent TextEdit { anchors.fill: messageText readOnly: true selectByMouse: true persistentSelection: true renderType: Text.NativeRendering textFormat: Text.RichText font.pixelSize: konvApp.largerFontSize wrapMode: Text.WordWrap text: messageText.text onLinkActivated: Qt.openUrlExternally(link) } } - - Component.onCompleted: metabitsLoader.active = showMetabits() }