Rework MessageDelegate in cpp

Rework MessageDelegate to be mostly a cpp class. This allows us to only load the components that are actually needed saving memory.

In testing using memtest it saved ~30% versus the current implementation.
This commit is contained in:
James Graham
2025-05-05 16:25:40 +01:00
parent 97d5be9d81
commit f4799a4287
16 changed files with 1089 additions and 537 deletions

View File

@@ -15,7 +15,7 @@ target_link_libraries(timeline-memtest PUBLIC
Qt::Gui Qt::Gui
Qt::QuickControls2 Qt::QuickControls2
Qt::Widgets Qt::Widgets
KF6::I18n KF6::I18nQml
KF6::Kirigami KF6::Kirigami
QuotientQt6 QuotientQt6
LibNeoChat LibNeoChat

View File

@@ -23,6 +23,7 @@ QQC2.ApplicationWindow {
height: root.height height: root.height
contentItem: ListView { contentItem: ListView {
cacheBuffer: 1000000
model: messageFilterModel model: messageFilterModel
delegate: EventDelegate { delegate: EventDelegate {

View File

@@ -5,6 +5,7 @@
#include <QQmlApplicationEngine> #include <QQmlApplicationEngine>
#include <QQmlContext> #include <QQmlContext>
#include <KLocalizedQmlContext>
#include <KLocalizedString> #include <KLocalizedString>
#include "memtesttimelinemodel.h" #include "memtesttimelinemodel.h"
@@ -19,12 +20,15 @@ int main(int argc, char **argv)
KLocalizedString::setApplicationDomain(QByteArrayLiteral("neochat")); KLocalizedString::setApplicationDomain(QByteArrayLiteral("neochat"));
QQmlApplicationEngine engine; QQmlApplicationEngine engine;
engine.loadFromModule("org.kde.neochat.timeline-memtest", "Main");
KLocalization::setupLocalizedContext(&engine);
MemTestTimelineModel *memTestTimelineModel = new MemTestTimelineModel; MemTestTimelineModel *memTestTimelineModel = new MemTestTimelineModel;
MessageFilterModel *messageFilterModel = new MessageFilterModel(nullptr, memTestTimelineModel); MessageFilterModel *messageFilterModel = new MessageFilterModel(nullptr, memTestTimelineModel);
engine.rootContext()->setContextProperty(u"memTestTimelineModel"_s, memTestTimelineModel); engine.rootContext()->setContextProperty(u"memTestTimelineModel"_s, memTestTimelineModel);
engine.rootContext()->setContextProperty(u"messageFilterModel"_s, messageFilterModel); engine.rootContext()->setContextProperty(u"messageFilterModel"_s, messageFilterModel);
engine.loadFromModule("org.kde.neochat.timeline-memtest", "Main");
return app.exec(); return app.exec();
} }

View File

@@ -241,7 +241,7 @@
"formatted_body": "This is an example<br>text message", "formatted_body": "This is an example<br>text message",
"msgtype": "m.text" "msgtype": "m.text"
}, },
"event_id": "$1000000000000:example.org", "event_id": 0,
"origin_server_ts": 1000000000000, "origin_server_ts": 1000000000000,
"room_id": "!jEsUZKDJdhlrceRyVU:example.org", "room_id": "!jEsUZKDJdhlrceRyVU:example.org",
"sender": "@example:example.org", "sender": "@example:example.org",
@@ -255,7 +255,7 @@
"body": "This is a highlight @bob:example.org", "body": "This is a highlight @bob:example.org",
"msgtype": "m.text" "msgtype": "m.text"
}, },
"event_id": "$1000000000001:example.org", "event_id": 1,
"origin_server_ts": 1000000000001, "origin_server_ts": 1000000000001,
"room_id": "!jEsUZKDJdhlrceRyVU:example.org", "room_id": "!jEsUZKDJdhlrceRyVU:example.org",
"sender": "@example:example.org", "sender": "@example:example.org",
@@ -266,11 +266,11 @@
}, },
{ {
"content": { "content": {
"m.relates_to": { "m.relates_to": {
"event_id": "$1000000000001:example.org", "event_id": 1,
"key": "👍", "key": "👍",
"rel_type": "m.annotation" "rel_type": "m.annotation"
} }
}, },
"origin_server_ts": 1000000000002, "origin_server_ts": 1000000000002,
"room_id": "!jEsUZKDJdhlrceRyVU:example.org", "room_id": "!jEsUZKDJdhlrceRyVU:example.org",
@@ -279,7 +279,7 @@
"unsigned": { "unsigned": {
"age": 390159120 "age": 390159120
}, },
"event_id": "$1000000000002:example.org", "event_id": 2,
"age": 390159120 "age": 390159120
}, },
{ {
@@ -289,7 +289,7 @@
"formatted_body": "reply", "formatted_body": "reply",
"m.relates_to": { "m.relates_to": {
"m.in_reply_to": { "m.in_reply_to": {
"event_id": "$1000000000000:example.org" "event_id": 0
} }
}, },
"msgtype": "m.text" "msgtype": "m.text"
@@ -300,7 +300,7 @@
"unsigned": { "unsigned": {
"age": 98 "age": 98
}, },
"event_id": "$1000000000003:example.org", "event_id": 3,
"room_id": "!jEsUZKDJdhlrceRyVU:example.org" "room_id": "!jEsUZKDJdhlrceRyVU:example.org"
}, },
{ {
@@ -317,7 +317,7 @@
"uri": "geo:51.7035,-1.14394" "uri": "geo:51.7035,-1.14394"
} }
}, },
"event_id": "$1000000000004:example.org", "event_id": 4,
"origin_server_ts": 1000000000004, "origin_server_ts": 1000000000004,
"room_id": "!jEsUZKDJdhlrceRyVU:example.org", "room_id": "!jEsUZKDJdhlrceRyVU:example.org",
"sender": "@example:example.org", "sender": "@example:example.org",
@@ -333,7 +333,7 @@
"formatted_body": "<pre><code class=\"language-cpp\">int main(int argc, char **argv)\n{\n QApplication app(argc, argv);\n\n KLocalizedString::setApplicationDomain(QByteArrayLiteral(&quot;neochat&quot;));\n\n QQmlApplicationEngine engine;\n engine.loadFromModule(&quot;org.kde.neochat.timeline-memtest&quot;, &quot;Main&quot;);\n\n return app.exec();\n}\n</code></pre>", "formatted_body": "<pre><code class=\"language-cpp\">int main(int argc, char **argv)\n{\n QApplication app(argc, argv);\n\n KLocalizedString::setApplicationDomain(QByteArrayLiteral(&quot;neochat&quot;));\n\n QQmlApplicationEngine engine;\n engine.loadFromModule(&quot;org.kde.neochat.timeline-memtest&quot;, &quot;Main&quot;);\n\n return app.exec();\n}\n</code></pre>",
"msgtype": "m.text" "msgtype": "m.text"
}, },
"event_id": "$1000000000005:example.org", "event_id": 5,
"origin_server_ts": 1000000000005, "origin_server_ts": 1000000000005,
"room_id": "!jEsUZKDJdhlrceRyVU:example.org", "room_id": "!jEsUZKDJdhlrceRyVU:example.org",
"sender": "@bob:example.org", "sender": "@bob:example.org",
@@ -347,7 +347,7 @@
"body": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam sed fringilla risus, eget lacinia risus. Suspendisse at magna id justo sagittis suscipit. Maecenas eros quam, pulvinar a consequat sed, varius vitae risus. Cras congue est eget felis porttitor lobortis. Nam cursus, nulla ut finibus suscipit, tellus eros tincidunt ante, a volutpat velit lectus sit amet turpis. Morbi leo justo, fringilla sed rutrum a, suscipit a quam. Proin rhoncus neque eget ligula ullamcorper pellentesque. Mauris volutpat malesuada nunc. Nullam finibus enim eu nibh placerat imperdiet. Nullam in mi in diam luctus scelerisque dignissim non erat. ", "body": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam sed fringilla risus, eget lacinia risus. Suspendisse at magna id justo sagittis suscipit. Maecenas eros quam, pulvinar a consequat sed, varius vitae risus. Cras congue est eget felis porttitor lobortis. Nam cursus, nulla ut finibus suscipit, tellus eros tincidunt ante, a volutpat velit lectus sit amet turpis. Morbi leo justo, fringilla sed rutrum a, suscipit a quam. Proin rhoncus neque eget ligula ullamcorper pellentesque. Mauris volutpat malesuada nunc. Nullam finibus enim eu nibh placerat imperdiet. Nullam in mi in diam luctus scelerisque dignissim non erat. ",
"msgtype": "m.text" "msgtype": "m.text"
}, },
"event_id": "$1000000000006:example.org", "event_id": 6,
"origin_server_ts": 1000000000006, "origin_server_ts": 1000000000006,
"room_id": "!jEsUZKDJdhlrceRyVU:example.org", "room_id": "!jEsUZKDJdhlrceRyVU:example.org",
"sender": "@example:example.org", "sender": "@example:example.org",
@@ -363,7 +363,7 @@
"formatted_body": "<blockquote>\n<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam sed fringilla risus, eget lacinia risus. Suspendisse at magna id justo sagittis suscipit. Maecenas eros quam, pulvinar a consequat sed, varius vitae risus. Cras congue est eget felis porttitor lobortis. Nam cursus, nulla ut finibus suscipit, tellus eros tincidunt ante, a volutpat velit lectus sit amet turpis. Morbi leo justo, fringilla sed rutrum a, suscipit a quam. Proin rhoncus neque eget ligula ullamcorper pellentesque. Mauris volutpat malesuada nunc. Nullam finibus enim eu nibh placerat imperdiet. Nullam in mi in diam luctus scelerisque dignissim non erat.</p>\n</blockquote>", "formatted_body": "<blockquote>\n<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam sed fringilla risus, eget lacinia risus. Suspendisse at magna id justo sagittis suscipit. Maecenas eros quam, pulvinar a consequat sed, varius vitae risus. Cras congue est eget felis porttitor lobortis. Nam cursus, nulla ut finibus suscipit, tellus eros tincidunt ante, a volutpat velit lectus sit amet turpis. Morbi leo justo, fringilla sed rutrum a, suscipit a quam. Proin rhoncus neque eget ligula ullamcorper pellentesque. Mauris volutpat malesuada nunc. Nullam finibus enim eu nibh placerat imperdiet. Nullam in mi in diam luctus scelerisque dignissim non erat.</p>\n</blockquote>",
"msgtype": "m.text" "msgtype": "m.text"
}, },
"event_id": "$1000000000007:example.org", "event_id": 7,
"origin_server_ts": 1000000000007, "origin_server_ts": 1000000000007,
"room_id": "!jEsUZKDJdhlrceRyVU:example.org", "room_id": "!jEsUZKDJdhlrceRyVU:example.org",
"sender": "@example:example.org", "sender": "@example:example.org",
@@ -371,282 +371,6 @@
"unsigned": { "unsigned": {
"age": 1232 "age": 1232
} }
},
{
"content": {
"body": "This is an example text message",
"format": "org.matrix.custom.html",
"formatted_body": "This is an example<br>text message",
"msgtype": "m.text"
},
"event_id": "$1000000000008:example.org",
"origin_server_ts": 1000000000008,
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
"sender": "@example:example.org",
"type": "m.room.message",
"unsigned": {
"age": 1232
}
},
{
"content": {
"body": "This is a highlight @bob:example.org",
"msgtype": "m.text"
},
"event_id": "$1000000000009:example.org",
"origin_server_ts": 1000000000009,
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
"sender": "@example:example.org",
"type": "m.room.message",
"unsigned": {
"age": 1233
}
},
{
"content": {
"m.relates_to": {
"event_id": "$1000000000009:example.org",
"key": "👍",
"rel_type": "m.annotation"
}
},
"origin_server_ts": 1000000000010,
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
"sender": "@alice:example.org",
"type": "m.reaction",
"unsigned": {
"age": 390159120
},
"event_id": "$1000000000010:example.org",
"age": 390159120
},
{
"content": {
"body": "reply",
"format": "org.matrix.custom.html",
"formatted_body": "reply",
"m.relates_to": {
"m.in_reply_to": {
"event_id": "$1000000000008:example.org"
}
},
"msgtype": "m.text"
},
"origin_server_ts": 1000000000011,
"sender": "@alice:example.org",
"type": "m.room.message",
"unsigned": {
"age": 98
},
"event_id": "$1000000000011:example.org",
"room_id": "!jEsUZKDJdhlrceRyVU:example.org"
},
{
"age": 96845207,
"content": {
"body": "Lat: 51.7035, Lon: -1.14394",
"geo_uri": "geo:51.7035,-1.14394",
"msgtype": "m.location",
"org.matrix.msc1767.text": "Lat: 51.7035, Lon: -1.14394",
"org.matrix.msc3488.asset": {
"type": "m.pin"
},
"org.matrix.msc3488.location": {
"uri": "geo:51.7035,-1.14394"
}
},
"event_id": "$1000000000012:example.org",
"origin_server_ts": 1000000000012,
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
"sender": "@example:example.org",
"type": "m.room.message",
"unsigned": {
"age": 96845207
}
},
{
"content": {
"body": "```cpp\nint main(int argc, char **argv)\n{\n QApplication app(argc, argv);\n\n KLocalizedString::setApplicationDomain(QByteArrayLiteral(\"neochat\"));\n\n QQmlApplicationEngine engine;\n engine.loadFromModule(\"org.kde.neochat.timeline-memtest\", \"Main\");\n\n return app.exec();\n}\n```",
"format": "org.matrix.custom.html",
"formatted_body": "<pre><code class=\"language-cpp\">int main(int argc, char **argv)\n{\n QApplication app(argc, argv);\n\n KLocalizedString::setApplicationDomain(QByteArrayLiteral(&quot;neochat&quot;));\n\n QQmlApplicationEngine engine;\n engine.loadFromModule(&quot;org.kde.neochat.timeline-memtest&quot;, &quot;Main&quot;);\n\n return app.exec();\n}\n</code></pre>",
"msgtype": "m.text"
},
"event_id": "$1000000000013:example.org",
"origin_server_ts": 1000000000013,
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
"sender": "@bob:example.org",
"type": "m.room.message",
"unsigned": {
"age": 1233
}
},
{
"content": {
"body": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam sed fringilla risus, eget lacinia risus. Suspendisse at magna id justo sagittis suscipit. Maecenas eros quam, pulvinar a consequat sed, varius vitae risus. Cras congue est eget felis porttitor lobortis. Nam cursus, nulla ut finibus suscipit, tellus eros tincidunt ante, a volutpat velit lectus sit amet turpis. Morbi leo justo, fringilla sed rutrum a, suscipit a quam. Proin rhoncus neque eget ligula ullamcorper pellentesque. Mauris volutpat malesuada nunc. Nullam finibus enim eu nibh placerat imperdiet. Nullam in mi in diam luctus scelerisque dignissim non erat. ",
"msgtype": "m.text"
},
"event_id": "$1000000000014:example.org",
"origin_server_ts": 1000000000014,
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
"sender": "@example:example.org",
"type": "m.room.message",
"unsigned": {
"age": 1232
}
},
{
"content": {
"body": "> Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam sed fringilla risus, eget lacinia risus. Suspendisse at magna id justo sagittis suscipit. Maecenas eros quam, pulvinar a consequat sed, varius vitae risus. Cras congue est eget felis porttitor lobortis. Nam cursus, nulla ut finibus suscipit, tellus eros tincidunt ante, a volutpat velit lectus sit amet turpis. Morbi leo justo, fringilla sed rutrum a, suscipit a quam. Proin rhoncus neque eget ligula ullamcorper pellentesque. Mauris volutpat malesuada nunc. Nullam finibus enim eu nibh placerat imperdiet. Nullam in mi in diam luctus scelerisque dignissim non erat. ",
"format": "org.matrix.custom.html",
"formatted_body": "<blockquote>\n<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam sed fringilla risus, eget lacinia risus. Suspendisse at magna id justo sagittis suscipit. Maecenas eros quam, pulvinar a consequat sed, varius vitae risus. Cras congue est eget felis porttitor lobortis. Nam cursus, nulla ut finibus suscipit, tellus eros tincidunt ante, a volutpat velit lectus sit amet turpis. Morbi leo justo, fringilla sed rutrum a, suscipit a quam. Proin rhoncus neque eget ligula ullamcorper pellentesque. Mauris volutpat malesuada nunc. Nullam finibus enim eu nibh placerat imperdiet. Nullam in mi in diam luctus scelerisque dignissim non erat.</p>\n</blockquote>",
"msgtype": "m.text"
},
"event_id": "$1000000000015:example.org",
"origin_server_ts": 1000000000015,
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
"sender": "@example:example.org",
"type": "m.room.message",
"unsigned": {
"age": 1232
}
},
{
"content": {
"body": "This is an example text message",
"format": "org.matrix.custom.html",
"formatted_body": "This is an example<br>text message",
"msgtype": "m.text"
},
"event_id": "$1000000000016:example.org",
"origin_server_ts": 1000000000016,
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
"sender": "@example:example.org",
"type": "m.room.message",
"unsigned": {
"age": 1232
}
},
{
"content": {
"body": "This is a highlight @bob:example.org",
"msgtype": "m.text"
},
"event_id": "$1000000000017:example.org",
"origin_server_ts": 1000000000017,
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
"sender": "@example:example.org",
"type": "m.room.message",
"unsigned": {
"age": 1233
}
},
{
"content": {
"m.relates_to": {
"event_id": "$1000000000017:example.org",
"key": "👍",
"rel_type": "m.annotation"
}
},
"origin_server_ts": 1000000000018,
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
"sender": "@alice:example.org",
"type": "m.reaction",
"unsigned": {
"age": 390159120
},
"event_id": "$1000000000018:example.org",
"age": 390159120
},
{
"content": {
"body": "reply",
"format": "org.matrix.custom.html",
"formatted_body": "reply",
"m.relates_to": {
"m.in_reply_to": {
"event_id": "$1000000000016:example.org"
}
},
"msgtype": "m.text"
},
"origin_server_ts": 1000000000019,
"sender": "@alice:example.org",
"type": "m.room.message",
"unsigned": {
"age": 98
},
"event_id": "$1000000000019:example.org",
"room_id": "!jEsUZKDJdhlrceRyVU:example.org"
},
{
"age": 96845207,
"content": {
"body": "Lat: 51.7035, Lon: -1.14394",
"geo_uri": "geo:51.7035,-1.14394",
"msgtype": "m.location",
"org.matrix.msc1767.text": "Lat: 51.7035, Lon: -1.14394",
"org.matrix.msc3488.asset": {
"type": "m.pin"
},
"org.matrix.msc3488.location": {
"uri": "geo:51.7035,-1.14394"
}
},
"event_id": "$1000000000020:example.org",
"origin_server_ts": 1000000000020,
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
"sender": "@example:example.org",
"type": "m.room.message",
"unsigned": {
"age": 96845207
}
},
{
"content": {
"body": "```cpp\nint main(int argc, char **argv)\n{\n QApplication app(argc, argv);\n\n KLocalizedString::setApplicationDomain(QByteArrayLiteral(\"neochat\"));\n\n QQmlApplicationEngine engine;\n engine.loadFromModule(\"org.kde.neochat.timeline-memtest\", \"Main\");\n\n return app.exec();\n}\n```",
"format": "org.matrix.custom.html",
"formatted_body": "<pre><code class=\"language-cpp\">int main(int argc, char **argv)\n{\n QApplication app(argc, argv);\n\n KLocalizedString::setApplicationDomain(QByteArrayLiteral(&quot;neochat&quot;));\n\n QQmlApplicationEngine engine;\n engine.loadFromModule(&quot;org.kde.neochat.timeline-memtest&quot;, &quot;Main&quot;);\n\n return app.exec();\n}\n</code></pre>",
"msgtype": "m.text"
},
"event_id": "$1000000000021:example.org",
"origin_server_ts": 1000000000021,
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
"sender": "@bob:example.org",
"type": "m.room.message",
"unsigned": {
"age": 1233
}
},
{
"content": {
"body": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam sed fringilla risus, eget lacinia risus. Suspendisse at magna id justo sagittis suscipit. Maecenas eros quam, pulvinar a consequat sed, varius vitae risus. Cras congue est eget felis porttitor lobortis. Nam cursus, nulla ut finibus suscipit, tellus eros tincidunt ante, a volutpat velit lectus sit amet turpis. Morbi leo justo, fringilla sed rutrum a, suscipit a quam. Proin rhoncus neque eget ligula ullamcorper pellentesque. Mauris volutpat malesuada nunc. Nullam finibus enim eu nibh placerat imperdiet. Nullam in mi in diam luctus scelerisque dignissim non erat. ",
"msgtype": "m.text"
},
"event_id": "$1000000000022:example.org",
"origin_server_ts": 1000000000022,
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
"sender": "@example:example.org",
"type": "m.room.message",
"unsigned": {
"age": 1232
}
},
{
"content": {
"body": "> Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam sed fringilla risus, eget lacinia risus. Suspendisse at magna id justo sagittis suscipit. Maecenas eros quam, pulvinar a consequat sed, varius vitae risus. Cras congue est eget felis porttitor lobortis. Nam cursus, nulla ut finibus suscipit, tellus eros tincidunt ante, a volutpat velit lectus sit amet turpis. Morbi leo justo, fringilla sed rutrum a, suscipit a quam. Proin rhoncus neque eget ligula ullamcorper pellentesque. Mauris volutpat malesuada nunc. Nullam finibus enim eu nibh placerat imperdiet. Nullam in mi in diam luctus scelerisque dignissim non erat. ",
"format": "org.matrix.custom.html",
"formatted_body": "<blockquote>\n<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam sed fringilla risus, eget lacinia risus. Suspendisse at magna id justo sagittis suscipit. Maecenas eros quam, pulvinar a consequat sed, varius vitae risus. Cras congue est eget felis porttitor lobortis. Nam cursus, nulla ut finibus suscipit, tellus eros tincidunt ante, a volutpat velit lectus sit amet turpis. Morbi leo justo, fringilla sed rutrum a, suscipit a quam. Proin rhoncus neque eget ligula ullamcorper pellentesque. Mauris volutpat malesuada nunc. Nullam finibus enim eu nibh placerat imperdiet. Nullam in mi in diam luctus scelerisque dignissim non erat.</p>\n</blockquote>",
"msgtype": "m.text"
},
"event_id": "$1000000000023:example.org",
"origin_server_ts": 1000000000023,
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
"sender": "@example:example.org",
"type": "m.room.message",
"unsigned": {
"age": 1232
}
} }
], ],
"limited": true, "limited": true,

View File

@@ -39,10 +39,52 @@ public:
testSyncFile.setFileName(QStringLiteral(DATA_DIR) + u'/' + syncFileName); testSyncFile.setFileName(QStringLiteral(DATA_DIR) + u'/' + syncFileName);
testSyncFile.open(QIODevice::ReadOnly); testSyncFile.open(QIODevice::ReadOnly);
auto testSyncJson = QJsonDocument::fromJson(testSyncFile.readAll()).object(); auto testSyncJson = QJsonDocument::fromJson(testSyncFile.readAll()).object();
auto timelineJson = testSyncJson["timeline"_L1].toObject();
timelineJson["events"_L1] = multiplyEvents(timelineJson["events"_L1].toArray(), 100);
testSyncJson["timeline"_L1] = timelineJson;
Quotient::SyncRoomData roomData(id(), Quotient::JoinState::Join, testSyncJson); Quotient::SyncRoomData roomData(id(), Quotient::JoinState::Join, testSyncJson);
update(std::move(roomData)); update(std::move(roomData));
} }
} }
QJsonArray multiplyEvents(QJsonArray events, int factor)
{
QJsonArray newArray;
int eventNum = 0;
int ts = 0;
for (int i = 0; i < factor; ++i) {
for (const auto &event : events) {
auto eventObject = event.toObject();
auto contentJson = eventObject["content"_L1].toObject();
if (contentJson.contains("m.relates_to"_L1)) {
auto relatesToJson = contentJson["m.relates_to"_L1].toObject();
if (relatesToJson.contains("m.in_reply_to"_L1)) {
auto replyJson = relatesToJson["m.in_reply_to"_L1].toObject();
const auto currentId = eventObject["event_id"_L1].toInt();
const auto currentReplyId = replyJson["event_id"_L1].toInt();
replyJson["event_id"_L1] = "$%1:example.org"_L1.arg(QString::number(eventNum - (currentId - currentReplyId)));
relatesToJson["m.in_reply_to"_L1] = replyJson;
} else if (relatesToJson.contains("event_id"_L1)) {
const auto currentId = eventObject["event_id"_L1].toInt();
const auto currentRelationId = relatesToJson["event_id"_L1].toInt();
relatesToJson["event_id"_L1] = "$%1:example.org"_L1.arg(QString::number(eventNum - (currentId - currentRelationId)));
}
contentJson["m.relates_to"_L1] = relatesToJson;
eventObject["content"_L1] = contentJson;
}
eventObject["event_id"_L1] = "$%1:example.org"_L1.arg(QString::number(eventNum));
eventObject["origin_server_ts"_L1] = ts;
auto unsignedJson = eventObject["unsigned"_L1].toObject();
unsignedJson["age"_L1] = ts;
eventObject["unsigned"_L1] = unsignedJson;
newArray.append(eventObject);
++eventNum;
++ts;
}
}
return newArray;
}
}; };
/** /**

View File

@@ -26,14 +26,12 @@ void DelegateSizeHelper::setParentItem(QQuickItem *parentItem)
if (m_parentItem) { if (m_parentItem) {
connect(m_parentItem, &QQuickItem::widthChanged, this, [this]() { connect(m_parentItem, &QQuickItem::widthChanged, this, [this]() {
Q_EMIT availablePercentageWidthChanged(); calcWidthsChanged();
Q_EMIT availableWidthChanged();
}); });
} }
Q_EMIT parentItemChanged(); Q_EMIT parentItemChanged();
Q_EMIT availablePercentageWidthChanged(); calcWidthsChanged();
Q_EMIT availableWidthChanged();
} }
qreal DelegateSizeHelper::leftPadding() const qreal DelegateSizeHelper::leftPadding() const
@@ -48,8 +46,7 @@ void DelegateSizeHelper::setLeftPadding(qreal leftPadding)
} }
m_leftPadding = leftPadding; m_leftPadding = leftPadding;
Q_EMIT leftPaddingChanged(); Q_EMIT leftPaddingChanged();
Q_EMIT availablePercentageWidthChanged(); calcWidthsChanged();
Q_EMIT availableWidthChanged();
} }
qreal DelegateSizeHelper::rightPadding() const qreal DelegateSizeHelper::rightPadding() const
@@ -64,8 +61,7 @@ void DelegateSizeHelper::setRightPadding(qreal rightPadding)
} }
m_rightPadding = rightPadding; m_rightPadding = rightPadding;
Q_EMIT rightPaddingChanged(); Q_EMIT rightPaddingChanged();
Q_EMIT availablePercentageWidthChanged(); calcWidthsChanged();
Q_EMIT availableWidthChanged();
} }
qreal DelegateSizeHelper::startBreakpoint() const qreal DelegateSizeHelper::startBreakpoint() const
@@ -80,6 +76,7 @@ void DelegateSizeHelper::setStartBreakpoint(qreal startBreakpoint)
} }
m_startBreakpoint = startBreakpoint; m_startBreakpoint = startBreakpoint;
Q_EMIT startBreakpointChanged(); Q_EMIT startBreakpointChanged();
calcWidthsChanged();
} }
qreal DelegateSizeHelper::endBreakpoint() const qreal DelegateSizeHelper::endBreakpoint() const
@@ -94,6 +91,7 @@ void DelegateSizeHelper::setEndBreakpoint(qreal endBreakpoint)
} }
m_endBreakpoint = endBreakpoint; m_endBreakpoint = endBreakpoint;
Q_EMIT endBreakpointChanged(); Q_EMIT endBreakpointChanged();
calcWidthsChanged();
} }
int DelegateSizeHelper::startPercentWidth() const int DelegateSizeHelper::startPercentWidth() const
@@ -108,6 +106,7 @@ void DelegateSizeHelper::setStartPercentWidth(int startPercentWidth)
} }
m_startPercentWidth = startPercentWidth; m_startPercentWidth = startPercentWidth;
Q_EMIT startPercentWidthChanged(); Q_EMIT startPercentWidthChanged();
calcWidthsChanged();
} }
int DelegateSizeHelper::endPercentWidth() const int DelegateSizeHelper::endPercentWidth() const
@@ -122,6 +121,7 @@ void DelegateSizeHelper::setEndPercentWidth(int endPercentWidth)
} }
m_endPercentWidth = endPercentWidth; m_endPercentWidth = endPercentWidth;
Q_EMIT endPercentWidthChanged(); Q_EMIT endPercentWidthChanged();
calcWidthsChanged();
} }
qreal DelegateSizeHelper::maxWidth() const qreal DelegateSizeHelper::maxWidth() const
@@ -143,6 +143,7 @@ void DelegateSizeHelper::setMaxWidth(qreal maxWidth)
} }
m_maxWidth = maxWidth; m_maxWidth = maxWidth;
Q_EMIT maxWidthChanged(); Q_EMIT maxWidthChanged();
calcWidthsChanged();
} }
qreal DelegateSizeHelper::maxAvailableWidth() const qreal DelegateSizeHelper::maxAvailableWidth() const
@@ -188,4 +189,20 @@ qreal DelegateSizeHelper::availableWidth() const
return std::round(std::min(absoluteWidth, maxWidth())); return std::round(std::min(absoluteWidth, maxWidth()));
} }
qreal DelegateSizeHelper::leftX() const
{
return m_leftPadding + (maxAvailableWidth() - availableWidth()) / 2;
}
qreal DelegateSizeHelper::rightX() const
{
return m_leftPadding + maxAvailableWidth() - (maxAvailableWidth() - availableWidth()) / 2;
}
void DelegateSizeHelper::calcWidthsChanged()
{
Q_EMIT availablePercentageWidthChanged();
Q_EMIT availableWidthChanged();
}
#include "moc_delegatesizehelper.cpp" #include "moc_delegatesizehelper.cpp"

View File

@@ -84,7 +84,7 @@ class DelegateSizeHelper : public QObject
* *
* @sa parentWidth, startBreakpoint, endBreakpoint * @sa parentWidth, startBreakpoint, endBreakpoint
*/ */
Q_PROPERTY(int availablePercentageWidth READ availablePercentageWidth NOTIFY availablePercentageWidthChanged) Q_PROPERTY(int availablePercentageWidth READ availablePercentageWidth NOTIFY availableWidthChanged)
/** /**
* @brief The width (in px) of the component at the current parentWidth. * @brief The width (in px) of the component at the current parentWidth.
@@ -124,6 +124,16 @@ public:
int availablePercentageWidth() const; int availablePercentageWidth() const;
qreal availableWidth() const; qreal availableWidth() const;
/**
* @brief The left x position of the content column.
*/
qreal leftX() const;
/**
* @brief The right x position of the content column.
*/
qreal rightX() const;
Q_SIGNALS: Q_SIGNALS:
void parentItemChanged(); void parentItemChanged();
void leftPaddingChanged(); void leftPaddingChanged();
@@ -148,5 +158,7 @@ private:
int m_endPercentWidth = 85; int m_endPercentWidth = 85;
std::optional<qreal> m_maxWidth = std::nullopt; std::optional<qreal> m_maxWidth = std::nullopt;
void calcWidthsChanged();
int calculateAvailablePercentageWidth() const; int calculateAvailablePercentageWidth() const;
}; };

View File

@@ -3,11 +3,12 @@
import QtQuick import QtQuick
import QtQuick.Controls as QQC2 import QtQuick.Controls as QQC2
import QtQuick.Layouts
import org.kde.kirigami as Kirigami import org.kde.kirigami as Kirigami
import org.kde.kirigamiaddons.labs.components as KirigamiComponents import org.kde.kirigamiaddons.labs.components as KirigamiComponents
Flow { RowLayout {
id: root id: root
property var avatarSize: Kirigami.Units.iconSizes.small property var avatarSize: Kirigami.Units.iconSizes.small
@@ -33,6 +34,11 @@ Flow {
} }
QQC2.Label { QQC2.Label {
id: excessAvatarsLabel id: excessAvatarsLabel
Layout.preferredWidth: Math.max(excessAvatarsTextMetrics.advanceWidth + Kirigami.Units.largeSpacing * 2, height)
Layout.preferredHeight: Kirigami.Units.iconSizes.small + Kirigami.Units.smallSpacing
Layout.fillHeight: true
visible: text !== "" visible: text !== ""
color: Kirigami.Theme.textColor color: Kirigami.Theme.textColor
horizontalAlignment: Text.AlignHCenter horizontalAlignment: Text.AlignHCenter
@@ -41,18 +47,17 @@ Flow {
background: Kirigami.ShadowedRectangle { background: Kirigami.ShadowedRectangle {
color: Kirigami.Theme.backgroundColor color: Kirigami.Theme.backgroundColor
Kirigami.Theme.inherit: false radius: Math.ceil(height / 2)
Kirigami.Theme.colorSet: Kirigami.Theme.View shadow {
radius: height / 2 size: Kirigami.Units.smallSpacing
shadow.size: Kirigami.Units.smallSpacing color: Qt.rgba(Kirigami.Theme.textColor.r, Kirigami.Theme.textColor.g, Kirigami.Theme.textColor.b, 0.10)
shadow.color: Qt.rgba(Kirigami.Theme.textColor.r, Kirigami.Theme.textColor.g, Kirigami.Theme.textColor.b, 0.10) }
border.color: Kirigami.ColorUtils.tintWithAlpha(color, Kirigami.Theme.textColor, 0.15) border {
border.width: 1 color: Kirigami.ColorUtils.tintWithAlpha(color, Kirigami.Theme.textColor, 0.15)
width: 1
}
} }
height: Kirigami.Units.iconSizes.small + Kirigami.Units.smallSpacing
width: Math.max(excessAvatarsTextMetrics.advanceWidth + Kirigami.Units.smallSpacing * 2, height)
TextMetrics { TextMetrics {
id: excessAvatarsTextMetrics id: excessAvatarsTextMetrics
text: excessAvatarsLabel.text text: excessAvatarsLabel.text

View File

@@ -63,6 +63,7 @@ ecm_add_qml_module(Timeline GENERATE_PLUGIN_SOURCE
locationhelper.cpp locationhelper.cpp
mediasizehelper.cpp mediasizehelper.cpp
messageattached.cpp messageattached.cpp
messagedelegate.cpp
pollhandler.cpp pollhandler.cpp
timelinedelegate.cpp timelinedelegate.cpp
enums/delegatetype.h enums/delegatetype.h

View File

@@ -26,7 +26,7 @@ import org.kde.neochat.libneochat as LibNeoChat
* This component also supports a compact mode where the padding is adjusted, the * This component also supports a compact mode where the padding is adjusted, the
* background is hidden and the delegate spans the full width of the timeline. * background is hidden and the delegate spans the full width of the timeline.
*/ */
TimelineDelegate { MessageDelegateBase {
id: root id: root
/** /**
@@ -44,20 +44,6 @@ TimelineDelegate {
*/ */
required property string eventId required property string eventId
/**
* @brief The message author.
*
* A Quotient::RoomMember object.
*
* @sa Quotient::RoomMember
*/
required property NeochatRoomMember author
/**
* @brief Whether the message author should be shown.
*/
required property bool showAuthor
/** /**
* @brief The model to visualise the content of the message. * @brief The model to visualise the content of the message.
*/ */
@@ -68,26 +54,11 @@ TimelineDelegate {
*/ */
required property string section required property string section
/**
* @brief Whether the section header should be shown.
*/
required property bool showSection
/** /**
* @brief A model with the first 5 other user read markers for this message. * @brief A model with the first 5 other user read markers for this message.
*/ */
required property var readMarkers required property var readMarkers
/**
* @brief Whether the other user read marker component should be shown.
*/
required property bool showReadMarkers
/**
* @brief Whether the message in a thread.
*/
required property bool isThreaded
/** /**
* @brief The Matrix ID of the root message in the thread, if any. * @brief The Matrix ID of the root message in the thread, if any.
*/ */
@@ -130,7 +101,7 @@ TimelineDelegate {
* *
* @note Used for positioning the hover actions. * @note Used for positioning the hover actions.
*/ */
readonly property alias bubbleY: mainContainer.y readonly property alias bubbleY: bubble.y
/** /**
* @brief The width of the message bubble. * @brief The width of the message bubble.
@@ -164,179 +135,83 @@ TimelineDelegate {
*/ */
property bool showHighlight: root.isHighlighted || isTemporaryHighlighted property bool showHighlight: root.isHighlighted || isTemporaryHighlighted
/**
* @brief Whether the message should temporarily be highlighted.
*
* Normally triggered when jumping to the event in the timeline, e.g. when a reply
* is clicked.
*/
property bool isTemporaryHighlighted: false
onIsTemporaryHighlightedChanged: if (isTemporaryHighlighted) {
temporaryHighlightTimer.start();
}
Timer {
id: temporaryHighlightTimer
interval: 1500
onTriggered: isTemporaryHighlighted = false
}
/**
* @brief The width available to the bubble content.
*/
property real contentMaxWidth: (root.isThreaded ? bubbleSizeHelper.parentWidth : bubbleSizeHelper.availableWidth) - bubble.leftPadding - bubble.rightPadding
Message.room: root.room Message.room: root.room
Message.timeline: root.ListView.view Message.timeline: root.ListView.view
Message.contentModel: root.contentModel Message.contentModel: root.contentModel
Message.index: root.index Message.index: root.index
Message.maxContentWidth: contentMaxWidth Message.maxContentWidth: maxContentWidth - bubble.leftPadding - bubble.rightPadding
width: parent?.width width: parent?.width
rightPadding: NeoChatConfig.compactLayout && root.ListView.view.width >= Kirigami.Units.gridUnit * 20 ? Kirigami.Units.gridUnit * 2 + Kirigami.Units.largeSpacing : Kirigami.Units.largeSpacing
alwaysFillWidth: NeoChatConfig.compactLayout enableAvatars: NeoChatConfig?.showAvatarInTimeline ?? false
compactMode: NeoChatConfig?.compactLayout ?? false
showLocalMessagesOnRight: NeoChatConfig.showLocalMessagesOnRight
contentItem: ColumnLayout { contentItem: Bubble {
spacing: Kirigami.Units.smallSpacing id: bubble
topPadding: NeoChatConfig.compactLayout ? Kirigami.Units.smallSpacing / 2 : Kirigami.Units.largeSpacing
bottomPadding: NeoChatConfig.compactLayout ? Kirigami.Units.mediumSpacing / 2 : Kirigami.Units.largeSpacing
leftPadding: NeoChatConfig.compactLayout ? 0 : Kirigami.Units.largeSpacing + Kirigami.Units.smallSpacing
rightPadding: Kirigami.Units.largeSpacing + Kirigami.Units.smallSpacing
SectionDelegate { author: root.author
id: sectionDelegate showAuthor: root.showAuthor
Layout.fillWidth: true isThreaded: root.isThreaded
visible: root.showSection
labelText: root.section contentModel: root.contentModel
colorSet: NeoChatConfig.compactLayout || root.alwaysFillWidth ? Kirigami.Theme.View : Kirigami.Theme.Window
showHighlight: root.showHighlight
isPending: root.isPending
onSelectedTextChanged: selectedText => {
root.Message.selectedText = selectedText;
} }
QQC2.ItemDelegate { onHoveredLinkChanged: hoveredLink => {
id: mainContainer root.Message.hoveredLink = hoveredLink;
Layout.fillWidth: true
Layout.topMargin: root.showAuthor ? Kirigami.Units.largeSpacing : (NeoChatConfig.compactLayout ? 1 : Kirigami.Units.smallSpacing)
Layout.leftMargin: Kirigami.Units.smallSpacing
Layout.rightMargin: Kirigami.Units.smallSpacing
implicitHeight: Math.max(root.showAuthor ? avatar.implicitHeight : 0, bubble.height)
// show hover actions
onHoveredChanged: {
if (hovered && !Kirigami.Settings.isMobile) {
root.setHoverActionsToDelegate();
}
}
KirigamiComponents.AvatarButton {
id: avatar
width: visible || NeoChatConfig.showAvatarInTimeline ? Kirigami.Units.gridUnit + Kirigami.Units.largeSpacing * 2 : 0
height: width
anchors {
left: parent.left
leftMargin: Kirigami.Units.smallSpacing
top: parent.top
topMargin: Kirigami.Units.smallSpacing
}
visible: ((root.showAuthor ?? false) || root.isThreaded) && NeoChatConfig.showAvatarInTimeline && (NeoChatConfig.compactLayout || !_private.showUserMessageOnRight)
name: root.author.displayName
source: root.author.avatarUrl
color: root.author.color
asynchronous: true
QQC2.ToolTip.text: root.author.htmlSafeDisambiguatedName
onClicked: RoomManager.resolveResource(root.author.uri)
}
Bubble {
id: bubble
anchors.left: avatar.right
anchors.leftMargin: Kirigami.Units.largeSpacing
anchors.rightMargin: rightPadding
topPadding: NeoChatConfig.compactLayout ? Kirigami.Units.smallSpacing / 2 : Kirigami.Units.largeSpacing
bottomPadding: NeoChatConfig.compactLayout ? Kirigami.Units.mediumSpacing / 2 : Kirigami.Units.largeSpacing
leftPadding: NeoChatConfig.compactLayout ? 0 : Kirigami.Units.largeSpacing + Kirigami.Units.smallSpacing
rightPadding: Kirigami.Units.largeSpacing + Kirigami.Units.smallSpacing
state: _private.showUserMessageOnRight ? "userMessageOnRight" : "userMessageOnLeft"
// states for anchor animations on window resize
// as setting anchors to undefined did not work reliably
states: [
State {
name: "userMessageOnRight"
AnchorChanges {
target: bubble
anchors.left: undefined
anchors.right: parent.right
}
},
State {
name: "userMessageOnLeft"
AnchorChanges {
target: bubble
anchors.left: avatar.right
anchors.right: root.isThreaded ? parent.right : undefined
}
}
]
author: root.author
showAuthor: root.showAuthor
isThreaded: root.isThreaded
contentModel: root.contentModel
showHighlight: root.showHighlight
isPending: root.isPending
onSelectedTextChanged: selectedText => {
root.Message.selectedText = selectedText;
}
onHoveredLinkChanged: hoveredLink => {
root.Message.hoveredLink = hoveredLink;
}
showBackground: root.cardBackground && !NeoChatConfig.compactLayout
}
background: Rectangle {
visible: mainContainer.hovered && (NeoChatConfig.compactLayout || root.alwaysFillWidth)
color: Kirigami.ColorUtils.tintWithAlpha(Kirigami.Theme.backgroundColor, Kirigami.Theme.highlightColor, 0.15)
radius: Kirigami.Units.cornerRadius
}
TapHandler {
acceptedButtons: Qt.RightButton
onTapped: _private.showMessageMenu()
}
TapHandler {
acceptedDevices: PointerDevice.TouchScreen
acceptedButtons: Qt.LeftButton
onLongPressed: _private.showMessageMenu()
}
}
AvatarFlow {
Layout.alignment: Qt.AlignRight
Layout.rightMargin: Kirigami.Units.largeSpacing
visible: root.showReadMarkers
model: root.readMarkers
} }
LibNeoChat.DelegateSizeHelper { showBackground: root.cardBackground && !NeoChatConfig.compactLayout
id: bubbleSizeHelper
parentItem: mainContainer TapHandler {
leftPadding: avatar.anchors.leftMargin + (NeoChatConfig.showAvatarInTimeline ? avatar.width + bubble.anchors.leftMargin : 0) acceptedButtons: Qt.RightButton
startBreakpoint: Kirigami.Units.gridUnit * 25 onTapped: _private.showMessageMenu()
endBreakpoint: Kirigami.Units.gridUnit * 40 }
startPercentWidth: root.alwaysFillWidth ? 100 : 90
endPercentWidth: root.alwaysFillWidth ? 100 : 60 TapHandler {
acceptedDevices: PointerDevice.TouchScreen
acceptedButtons: Qt.LeftButton
onLongPressed: _private.showMessageMenu()
} }
} }
function isVisibleInTimeline() { avatarComponent: KirigamiComponents.AvatarButton {
let yoff = Math.round(y - ListView.view.contentY); id: avatar
return (yoff + height > 0 && yoff < ListView.view.height); implicitWidth: Kirigami.Units.gridUnit + Kirigami.Units.largeSpacing * 2
implicitHeight: width
name: root.author.displayName
source: root.author.avatarUrl
color: root.author.color
asynchronous: true
QQC2.ToolTip.text: root.author.htmlSafeDisambiguatedName
onClicked: RoomManager.resolveResource(root.author.uri)
}
sectionComponent: SectionDelegate {
id: sectionDelegate
labelText: root.section
colorSet: root.compactMode ? Kirigami.Theme.View : Kirigami.Theme.Window
}
readMarkerComponent: AvatarFlow {
model: root.readMarkers
}
compactBackgroundComponent: Rectangle {
color: Kirigami.ColorUtils.tintWithAlpha(Kirigami.Theme.backgroundColor, Kirigami.Theme.highlightColor, 0.15)
radius: Kirigami.Units.cornerRadius
} }
function setHoverActionsToDelegate() { function setHoverActionsToDelegate() {
@@ -348,11 +223,6 @@ TimelineDelegate {
QtObject { QtObject {
id: _private id: _private
/**
* @brief Whether local user messages should be aligned right.
*/
property bool showUserMessageOnRight: NeoChatConfig.showLocalMessagesOnRight && root.author.isLocalMember && !NeoChatConfig.compactLayout && !root.alwaysFillWidth && !root.isThreaded
function showMessageMenu() { function showMessageMenu() {
RoomManager.viewEventMenu(root.eventId, root.room, root.author, root.Message.selectedText, root.Message.hoveredLink); RoomManager.viewEventMenu(root.eventId, root.room, root.author, root.Message.selectedText, root.Message.hoveredLink);
} }

View File

@@ -10,8 +10,6 @@ import org.kde.kirigami as Kirigami
import org.kde.kirigamiaddons.delegates as Delegates import org.kde.kirigamiaddons.delegates as Delegates
import org.kde.kirigamiaddons.formcard as FormCard import org.kde.kirigamiaddons.formcard as FormCard
import Quotient
import org.kde.neochat import org.kde.neochat
/** /**

View File

@@ -159,7 +159,7 @@ QQC2.ScrollView {
} }
] ]
width: messageListView.sectionBannerItem ? messageListView.sectionBannerItem.contentItem.width : 0 width: messageListView.sectionBannerItem ? messageListView.sectionBannerItem.timelineWidth : 0
labelText: messageListView.sectionBannerItem ? messageListView.sectionBannerItem.ListView.section : "" labelText: messageListView.sectionBannerItem ? messageListView.sectionBannerItem.ListView.section : ""
colorSet: NeoChatConfig.compactLayout ? Kirigami.Theme.View : Kirigami.Theme.Window colorSet: NeoChatConfig.compactLayout ? Kirigami.Theme.View : Kirigami.Theme.Window
} }

View File

@@ -0,0 +1,590 @@
// SPDX-FileCopyrightText: 2025 James Graham <james.h.graham@protonmail.com>
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
#include "messagedelegate.h"
#include "timelinedelegate.h"
#include <algorithm>
#include <cmath>
MessageObjectIncubator::MessageObjectIncubator(std::function<void(QQuickItem *)> initialCallback,
std::function<void(MessageObjectIncubator *)> completedCallback,
std::function<void(MessageObjectIncubator *)> errorCallback)
: QQmlIncubator(QQmlIncubator::Asynchronous)
, m_initialCallback(initialCallback)
, m_completedCallback(completedCallback)
, m_errorCallback(errorCallback)
{
}
void MessageObjectIncubator::setInitialState(QObject *object)
{
auto item = qobject_cast<QQuickItem *>(object);
if (item) {
m_initialCallback(item);
}
}
void MessageObjectIncubator::statusChanged(QQmlIncubator::Status status)
{
if (status == QQmlIncubator::Error && m_errorCallback) {
m_errorCallback(this);
}
if (status == QQmlIncubator::Ready && m_completedCallback) {
m_completedCallback(this);
}
}
MessageDelegateBase::MessageDelegateBase(QQuickItem *parent)
: TimelineDelegate(parent)
{
m_contentSizeHelper.setParentItem(this);
setPercentageValues();
connect(this, &MessageDelegateBase::leftPaddingChanged, this, &MessageDelegateBase::setContentPadding);
connect(this, &MessageDelegateBase::rightPaddingChanged, this, &MessageDelegateBase::setContentPadding);
connect(&m_contentSizeHelper, &DelegateSizeHelper::availableWidthChanged, this, &MessageDelegateBase::setContentPadding);
connect(&m_contentSizeHelper, &DelegateSizeHelper::availableWidthChanged, this, &MessageDelegateBase::maxContentWidthChanged);
connect(&m_contentSizeHelper, &DelegateSizeHelper::availableWidthChanged, this, &MessageDelegateBase::markAsDirty);
}
NeochatRoomMember *MessageDelegateBase::author() const
{
return m_author;
}
void MessageDelegateBase::setAuthor(NeochatRoomMember *author)
{
if (author == m_author) {
return;
}
m_author = author;
Q_EMIT authorChanged();
setContentPadding();
markAsDirty();
}
bool MessageDelegateBase::isThreaded() const
{
return m_isThreaded;
}
void MessageDelegateBase::setIsThreaded(bool isThreaded)
{
if (isThreaded == m_isThreaded) {
return;
}
m_isThreaded = isThreaded;
setAlwaysFillWidth(m_isThreaded || m_compactMode);
setPercentageValues(m_isThreaded || m_compactMode);
Q_EMIT isThreadedChanged();
}
void MessageDelegateBase::setCurveValues()
{
m_spacing = qreal(m_units->smallSpacing());
m_avatarSize = qreal(m_units->gridUnit() + m_units->largeSpacing() * 2);
m_sizeHelper.setLeftPadding(qreal(m_units->largeSpacing()));
setBaseRightPadding();
m_sizeHelper.setStartBreakpoint(qreal(m_units->gridUnit() * 46));
m_sizeHelper.setEndBreakpoint(qreal(m_units->gridUnit() * 66));
m_sizeHelper.setMaxWidth(qreal(m_units->gridUnit() * 60));
m_contentSizeHelper.setStartBreakpoint(qreal(m_units->gridUnit() * 25));
m_contentSizeHelper.setEndBreakpoint(qreal(m_units->gridUnit() * 40));
m_contentSizeHelper.setMaxWidth(qreal(m_units->gridUnit() * 60));
setContentPadding();
}
void MessageDelegateBase::setBaseRightPadding()
{
if (!m_units) {
return;
}
if (m_compactMode && width() > m_units->gridUnit() * 30) {
m_sizeHelper.setRightPadding(qreal(m_units->gridUnit() * 2 + m_units->largeSpacing()));
} else {
m_sizeHelper.setRightPadding(qreal(m_units->largeSpacing()));
}
}
void MessageDelegateBase::setPercentageValues(bool fillWidth)
{
if (fillWidth) {
m_contentSizeHelper.setStartPercentWidth(100);
m_contentSizeHelper.setEndPercentWidth(100);
} else {
m_contentSizeHelper.setStartPercentWidth(90);
m_contentSizeHelper.setEndPercentWidth(60);
}
}
void MessageDelegateBase::setContentPadding()
{
m_contentSizeHelper.setLeftPadding(m_sizeHelper.leftX() + (leaveAvatarSpace() ? m_avatarSize + m_spacing : 0));
m_contentSizeHelper.setRightPadding(m_sizeHelper.rightPadding());
}
qreal MessageDelegateBase::maxContentWidth() const
{
return m_isThreaded || m_alwaysFillWidth ? m_contentSizeHelper.maxAvailableWidth() : m_contentSizeHelper.availableWidth();
}
void MessageDelegateBase::cleanupIncubator(MessageObjectIncubator *incubator)
{
incubator->clear();
delete incubator;
}
void MessageDelegateBase::cleanupItem(QQuickItem *item)
{
if (!item) {
return;
}
item->setParentItem(nullptr);
item->disconnect(this);
item->deleteLater();
}
QQmlComponent *MessageDelegateBase::avatarComponent() const
{
return m_avatarComponent;
}
void MessageDelegateBase::setAvatarComponent(QQmlComponent *avatarComponent)
{
if (avatarComponent == m_avatarComponent) {
return;
}
m_avatarComponent = avatarComponent;
Q_EMIT avatarComponentChanged();
updateAvatar();
}
bool MessageDelegateBase::showAuthor() const
{
return m_showAuthor;
}
void MessageDelegateBase::setShowAuthor(bool showAuthor)
{
if (showAuthor == m_showAuthor) {
return;
}
m_showAuthor = showAuthor;
Q_EMIT showAuthorChanged();
updateAvatar();
}
bool MessageDelegateBase::enableAvatars() const
{
return m_enableAvatars;
}
void MessageDelegateBase::setEnableAvatars(bool enableAvatars)
{
if (enableAvatars == m_enableAvatars) {
return;
}
m_enableAvatars = enableAvatars;
Q_EMIT enableAvatarsChanged();
updateAvatar();
}
bool MessageDelegateBase::leaveAvatarSpace() const
{
return m_enableAvatars && !showMessageOnRight();
}
bool MessageDelegateBase::showAvatar() const
{
return m_enableAvatars && m_showAuthor && !showMessageOnRight();
}
void MessageDelegateBase::updateAvatar()
{
if (m_avatarComponent && showAvatar() && !m_avatarItem && !m_avatarIncubating) {
const auto avatarIncubator = new MessageObjectIncubator(
m_objectInitialCallback,
[this](MessageObjectIncubator *incubator) {
if (!incubator) {
return;
}
const auto avatarObject = qobject_cast<QQuickItem *>(incubator->object());
if (avatarObject) {
// The setting may have changed during the incubation period.
if (showAvatar()) {
m_avatarItem = avatarObject;
} else {
cleanupItem(avatarObject);
}
markAsDirty();
}
cleanupIncubator(incubator);
m_avatarIncubating = false;
},
m_errorCallback);
m_avatarComponent->create(*avatarIncubator, qmlContext(m_avatarComponent));
m_avatarIncubating = true;
} else if (!showAvatar() && m_avatarItem) {
cleanupItem(m_avatarItem);
markAsDirty();
}
}
QQmlComponent *MessageDelegateBase::sectionComponent() const
{
return m_sectionComponent;
}
void MessageDelegateBase::setSectionComponent(QQmlComponent *sectionComponent)
{
if (sectionComponent == m_sectionComponent) {
return;
}
m_sectionComponent = sectionComponent;
Q_EMIT sectionComponentChanged();
updateSection();
}
bool MessageDelegateBase::showSection() const
{
return m_showSection;
}
void MessageDelegateBase::setShowSection(bool showSection)
{
if (showSection == m_showSection) {
return;
}
m_showSection = showSection;
Q_EMIT showSectionChanged();
updateSection();
}
void MessageDelegateBase::updateSection()
{
if (m_sectionComponent && m_showSection && !m_sectionItem && !m_sectionIncubating) {
const auto sectionIncubator = new MessageObjectIncubator(
m_objectInitialCallback,
[this](MessageObjectIncubator *incubator) {
if (!incubator) {
return;
}
const auto sectionObject = qobject_cast<QQuickItem *>(incubator->object());
if (sectionObject) {
// The setting may have changed during the incubation period.
if (m_showSection) {
m_sectionItem = sectionObject;
} else {
cleanupItem(sectionObject);
}
markAsDirty();
}
cleanupIncubator(incubator);
m_sectionIncubating = false;
},
m_errorCallback);
m_sectionComponent->create(*sectionIncubator, qmlContext(m_sectionComponent));
m_sectionIncubating = true;
} else if (!m_showSection && m_sectionItem) {
cleanupItem(m_sectionItem);
markAsDirty();
}
}
QQmlComponent *MessageDelegateBase::readMarkerComponent() const
{
return m_readMarkerComponent;
}
void MessageDelegateBase::setReadMarkerComponent(QQmlComponent *readMarkerComponent)
{
if (readMarkerComponent == m_readMarkerComponent) {
return;
}
m_readMarkerComponent = readMarkerComponent;
Q_EMIT readMarkerComponentChanged();
updateReadMarker();
}
bool MessageDelegateBase::showReadMarkers() const
{
return m_showReadMarkers;
}
void MessageDelegateBase::setShowReadMarkers(bool showReadMarkers)
{
if (showReadMarkers == m_showReadMarkers) {
return;
}
m_showReadMarkers = showReadMarkers;
Q_EMIT showReadMarkersChanged();
updateReadMarker();
}
void MessageDelegateBase::updateReadMarker()
{
if (m_readMarkerComponent && m_showReadMarkers && !m_readMarkerItem && !m_readMarkerIncubating) {
const auto readMarkerIncubator = new MessageObjectIncubator(
m_objectInitialCallback,
[this](MessageObjectIncubator *incubator) {
if (!incubator) {
return;
}
const auto readMarkerObject = qobject_cast<QQuickItem *>(incubator->object());
if (readMarkerObject) {
if (m_showReadMarkers) {
m_readMarkerItem = readMarkerObject;
} else {
cleanupItem(readMarkerObject);
}
markAsDirty();
}
cleanupIncubator(incubator);
m_readMarkerIncubating = false;
},
m_errorCallback);
m_readMarkerComponent->create(*readMarkerIncubator, qmlContext(m_readMarkerComponent));
m_readMarkerIncubating = true;
} else if (!m_showReadMarkers && m_readMarkerItem) {
cleanupItem(m_readMarkerItem);
markAsDirty();
}
}
QQmlComponent *MessageDelegateBase::compactBackgroundComponent() const
{
return m_compactBackgroundComponent;
}
void MessageDelegateBase::setCompactBackgroundComponentt(QQmlComponent *compactBackgroundComponent)
{
if (compactBackgroundComponent == m_compactBackgroundComponent) {
return;
}
m_compactBackgroundComponent = compactBackgroundComponent;
Q_EMIT compactBackgroundComponentChanged();
updateBackground();
}
bool MessageDelegateBase::compactMode() const
{
return m_compactMode;
}
void MessageDelegateBase::setCompactMode(bool compactMode)
{
if (compactMode == m_compactMode) {
return;
}
m_compactMode = compactMode;
setAlwaysFillWidth(m_isThreaded || m_compactMode);
setPercentageValues(m_isThreaded || m_compactMode);
setAcceptHoverEvents(m_compactMode);
setBaseRightPadding();
Q_EMIT compactModeChanged();
Q_EMIT maxContentWidthChanged();
updateBackground();
}
void MessageDelegateBase::updateBackground()
{
if (m_compactBackgroundComponent && m_compactMode && m_hovered && !m_compactBackgroundItem && !m_compactBackgroundIncubating) {
const auto compactBackgroundIncubator = new MessageObjectIncubator(
m_objectInitialCallback,
[this](MessageObjectIncubator *incubator) {
if (!incubator) {
return;
}
const auto compactBackgroundObject = qobject_cast<QQuickItem *>(incubator->object());
if (compactBackgroundObject) {
if (m_compactMode) {
m_compactBackgroundItem = compactBackgroundObject;
} else {
cleanupItem(compactBackgroundObject);
}
markAsDirty();
}
cleanupIncubator(incubator);
m_compactBackgroundIncubating = false;
},
m_errorCallback);
m_compactBackgroundComponent->create(*compactBackgroundIncubator, qmlContext(m_compactBackgroundComponent));
m_compactBackgroundIncubating = true;
} else if (m_compactBackgroundItem && !m_hovered) {
cleanupItem(m_compactBackgroundItem);
markAsDirty();
}
}
bool MessageDelegateBase::showLocalMessagesOnRight() const
{
return m_showLocalMessagesOnRight;
}
void MessageDelegateBase::setShowLocalMessagesOnRight(bool showLocalMessagesOnRight)
{
if (showLocalMessagesOnRight == m_showLocalMessagesOnRight) {
return;
}
m_showLocalMessagesOnRight = showLocalMessagesOnRight;
Q_EMIT showLocalMessagesOnRightChanged();
setContentPadding();
updateAvatar();
}
void MessageDelegateBase::updateImplicitHeight()
{
qreal implicitHeight = 0.0;
int numObj = 0;
if (m_showSection && m_sectionItem) {
implicitHeight += m_sectionItem->implicitHeight();
numObj++;
}
qreal avatarHeight = 0.0;
qreal contentHeight = 0.0;
if (showAvatar() && m_avatarItem) {
m_avatarItem->setImplicitWidth(m_avatarSize);
m_avatarItem->setImplicitHeight(m_avatarSize);
avatarHeight = m_avatarItem->implicitHeight();
}
if (m_contentItem) {
contentHeight = m_contentItem->implicitHeight();
}
implicitHeight += std::max(avatarHeight, contentHeight);
if (avatarHeight > 0 || contentHeight > 0) {
numObj++;
}
if (m_showReadMarkers && m_readMarkerItem) {
implicitHeight += m_readMarkerItem->implicitHeight();
numObj++;
}
implicitHeight += (numObj - 1) * m_spacing;
implicitHeight += m_showAuthor ? m_spacing * 2 : m_spacing;
implicitHeight = std::ceil(implicitHeight);
setImplicitWidth(m_alwaysFillWidth ? m_sizeHelper.maxAvailableWidth() : m_sizeHelper.availableWidth());
setImplicitHeight(implicitHeight);
}
bool MessageDelegateBase::showMessageOnRight() const
{
return m_showLocalMessagesOnRight && !m_alwaysFillWidth && m_author && m_author->isLocalMember();
}
void MessageDelegateBase::resizeContent()
{
if (!isComponentComplete() || m_resizingContent) {
return;
}
m_isDirty = false;
m_resizingContent = true;
updateImplicitHeight();
qreal nextY = m_showAuthor ? m_spacing * 2 : m_spacing;
if (m_compactMode && m_compactBackgroundItem) {
m_compactBackgroundItem->setPosition(QPointF(std::ceil(m_sizeHelper.leftX() / 2), std::ceil(nextY / 2)));
m_compactBackgroundItem->setSize(
QSizeF(m_sizeHelper.availableWidth() + m_sizeHelper.rightPadding() - std::ceil(m_sizeHelper.leftPadding() / 2), implicitHeight()));
m_compactBackgroundItem->setZ(-1);
}
if (m_showSection && m_sectionItem) {
m_sectionItem->setPosition(QPointF(m_sizeHelper.leftX(), nextY));
m_sectionItem->setSize(QSizeF(m_sizeHelper.availableWidth(), m_sectionItem->implicitHeight()));
nextY += m_sectionItem->implicitHeight() + m_spacing;
}
qreal yAdd = 0.0;
if (showAvatar() && m_avatarItem) {
m_avatarItem->setPosition(QPointF(m_sizeHelper.leftX(), nextY));
m_avatarItem->setSize(QSizeF(m_avatarItem->implicitWidth(), m_avatarItem->implicitHeight()));
yAdd = m_avatarItem->implicitWidth();
}
if (m_contentItem) {
const auto contentItemWidth =
m_alwaysFillWidth ? m_contentSizeHelper.availableWidth() : std::min(m_contentItem->implicitWidth(), m_contentSizeHelper.availableWidth());
const auto contentX = showMessageOnRight() ? m_sizeHelper.rightX() - contentItemWidth - 1 : m_contentSizeHelper.leftPadding();
m_contentItem->setPosition(QPointF(contentX, nextY));
m_contentItem->setSize(QSizeF(contentItemWidth, m_contentItem->implicitHeight()));
yAdd = std::max(yAdd, m_contentItem->implicitHeight());
}
nextY += yAdd + m_spacing;
if (m_showReadMarkers && m_readMarkerItem) {
qreal extraSpacing = m_readMarkerItem->implicitWidth() < m_sizeHelper.availableWidth() - m_spacing ? m_spacing : 0;
qreal objectWidth = std::min(m_sizeHelper.availableWidth(), m_readMarkerItem->implicitWidth());
m_readMarkerItem->setPosition(QPointF(m_sizeHelper.rightX() - objectWidth - extraSpacing, nextY));
m_readMarkerItem->setSize(QSizeF(objectWidth, m_readMarkerItem->implicitHeight()));
}
m_resizingContent = false;
}
void MessageDelegateBase::hoverEnterEvent(QHoverEvent *event)
{
m_hovered = true;
event->setAccepted(true);
updateBackground();
}
void MessageDelegateBase::hoverMoveEvent(QHoverEvent *event)
{
m_hovered = contains(event->pos());
event->setAccepted(true);
updateBackground();
}
void MessageDelegateBase::hoverLeaveEvent(QHoverEvent *event)
{
m_hovered = false;
event->setAccepted(true);
updateBackground();
}
bool MessageDelegateBase::isTemporaryHighlighted() const
{
return m_temporaryHighlightTimer && m_temporaryHighlightTimer->isActive();
}
void MessageDelegateBase::setIsTemporaryHighlighted(bool isTemporaryHighlighted)
{
if (!isTemporaryHighlighted) {
if (m_temporaryHighlightTimer) {
m_temporaryHighlightTimer->stop();
m_temporaryHighlightTimer->deleteLater();
Q_EMIT isTemporaryHighlightedChanged();
}
return;
}
if (!m_temporaryHighlightTimer) {
m_temporaryHighlightTimer = new QTimer(this);
}
m_temporaryHighlightTimer->start(1500);
connect(m_temporaryHighlightTimer, &QTimer::timeout, this, [this]() {
m_temporaryHighlightTimer->deleteLater();
Q_EMIT isTemporaryHighlightedChanged();
});
Q_EMIT isTemporaryHighlightedChanged();
}
#include "moc_messagedelegate.cpp"

View File

@@ -0,0 +1,240 @@
// SPDX-FileCopyrightText: 2025 James Graham <james.h.graham@protonmail.com>
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
#pragma once
#include <QQmlIncubator>
#include <QQuickItem>
#include <QTimer>
#include "neochatroommember.h"
#include "timelinedelegate.h"
/**
* @brief Incubator for creating instances of components as required.
*/
class MessageObjectIncubator : public QQmlIncubator
{
public:
MessageObjectIncubator(std::function<void(QQuickItem *)> initialCallback,
std::function<void(MessageObjectIncubator *)> completedCallback,
std::function<void(MessageObjectIncubator *)> errorCallback);
private:
void setInitialState(QObject *object) override;
std::function<void(QQuickItem *)> m_initialCallback;
void statusChanged(QQmlIncubator::Status status) override;
std::function<void(MessageObjectIncubator *)> m_completedCallback;
std::function<void(MessageObjectIncubator *)> m_errorCallback;
};
/**
* @brief Delegate Item for all messages in the timeline.
*/
class MessageDelegateBase : public TimelineDelegate
{
Q_OBJECT
QML_ELEMENT
/**
* @brief The message author.
*
* @sa NeochatRoomMember
*/
Q_PROPERTY(NeochatRoomMember *author READ author WRITE setAuthor NOTIFY authorChanged FINAL REQUIRED)
/**
* @brief Whether the message is threaded.
*/
Q_PROPERTY(bool isThreaded READ isThreaded WRITE setIsThreaded NOTIFY isThreadedChanged FINAL REQUIRED)
/**
* @brief The maximum width available to the content item.
*/
Q_PROPERTY(qreal maxContentWidth READ maxContentWidth NOTIFY maxContentWidthChanged FINAL)
/**
* @brief The component to use to visualize a user avatar.
*/
Q_PROPERTY(QQmlComponent *avatarComponent READ avatarComponent WRITE setAvatarComponent NOTIFY avatarComponentChanged FINAL)
/**
* @brief Whether the user avatar should be shown.
*
* @note An avatar is only shown if enabled.
*/
Q_PROPERTY(bool showAuthor READ showAuthor WRITE setShowAuthor NOTIFY showAuthorChanged FINAL REQUIRED)
/**
* @brief Whether user avatars are enabled.
*/
Q_PROPERTY(bool enableAvatars READ enableAvatars WRITE setEnableAvatars NOTIFY enableAvatarsChanged FINAL)
/**
* @brief The component to use to visualize a section.
*/
Q_PROPERTY(QQmlComponent *sectionComponent READ sectionComponent WRITE setSectionComponent NOTIFY sectionComponentChanged FINAL)
/**
* @brief Whether to show the section component.
*/
Q_PROPERTY(bool showSection READ showSection WRITE setShowSection NOTIFY showSectionChanged FINAL REQUIRED)
/**
* @brief The component to use to visualize other user read markers.
*/
Q_PROPERTY(QQmlComponent *readMarkerComponent READ readMarkerComponent WRITE setReadMarkerComponent NOTIFY readMarkerComponentChanged FINAL)
/**
* @brief Whether to show the other user read markers.
*/
Q_PROPERTY(bool showReadMarkers READ showReadMarkers WRITE setShowReadMarkers NOTIFY showReadMarkersChanged FINAL REQUIRED)
/**
* @brief The component to use to visualize the hover state in compact mode.
*/
Q_PROPERTY(QQmlComponent *compactBackgroundComponent READ compactBackgroundComponent WRITE setCompactBackgroundComponentt NOTIFY
compactBackgroundComponentChanged FINAL)
/**
* @brief Whether to use the compact mode appearance.
*/
Q_PROPERTY(bool compactMode READ compactMode WRITE setCompactMode NOTIFY compactModeChanged FINAL)
/**
* @brief Whether to show messages by the local user on the right in bubble mode.
*/
Q_PROPERTY(bool showLocalMessagesOnRight READ showLocalMessagesOnRight WRITE setShowLocalMessagesOnRight NOTIFY showLocalMessagesOnRightChanged FINAL)
/**
* @brief Whether the message should be highlighted temporarily.
*
* Normally triggered when jumping to the event in the timeline, e.g. when a reply
* is clicked.
*/
Q_PROPERTY(bool isTemporaryHighlighted READ isTemporaryHighlighted WRITE setIsTemporaryHighlighted NOTIFY isTemporaryHighlightedChanged FINAL)
public:
MessageDelegateBase(QQuickItem *parent = nullptr);
NeochatRoomMember *author() const;
void setAuthor(NeochatRoomMember *author);
bool isThreaded() const;
void setIsThreaded(bool isThreaded);
qreal maxContentWidth() const;
QQmlComponent *avatarComponent() const;
void setAvatarComponent(QQmlComponent *avatarComponent);
bool showAuthor() const;
void setShowAuthor(bool showAuthor);
bool enableAvatars() const;
void setEnableAvatars(bool enableAvatars);
QQmlComponent *sectionComponent() const;
void setSectionComponent(QQmlComponent *sectionComponent);
bool showSection() const;
void setShowSection(bool showSection);
QQmlComponent *readMarkerComponent() const;
void setReadMarkerComponent(QQmlComponent *readMarkerComponent);
bool showReadMarkers() const;
void setShowReadMarkers(bool showReadMarkers);
QQmlComponent *compactBackgroundComponent() const;
void setCompactBackgroundComponentt(QQmlComponent *compactBackgroundComponent);
bool compactMode() const;
void setCompactMode(bool compactMode);
bool showLocalMessagesOnRight() const;
void setShowLocalMessagesOnRight(bool showLocalMessagesOnRight);
bool isTemporaryHighlighted() const;
void setIsTemporaryHighlighted(bool isTemporaryHighlighted);
Q_SIGNALS:
void authorChanged();
void isThreadedChanged();
void maxContentWidthChanged();
void avatarComponentChanged();
void showAuthorChanged();
void enableAvatarsChanged();
void sectionComponentChanged();
void showSectionChanged();
void readMarkerComponentChanged();
void showReadMarkersChanged();
void compactBackgroundComponentChanged();
void compactModeChanged();
void showLocalMessagesOnRightChanged();
void isTemporaryHighlightedChanged();
private:
DelegateSizeHelper m_contentSizeHelper;
QPointer<NeochatRoomMember> m_author;
bool m_isThreaded = false;
QPointer<QQmlComponent> m_avatarComponent;
bool m_avatarIncubating = false;
QPointer<QQuickItem> m_avatarItem;
bool m_showAuthor = false;
bool m_enableAvatars = true;
bool leaveAvatarSpace() const;
bool showAvatar() const;
void updateAvatar();
QPointer<QQmlComponent> m_sectionComponent;
bool m_sectionIncubating = false;
QPointer<QQuickItem> m_sectionItem;
bool m_showSection = false;
void updateSection();
QPointer<QQmlComponent> m_readMarkerComponent;
bool m_readMarkerIncubating = false;
QPointer<QQuickItem> m_readMarkerItem;
bool m_showReadMarkers = false;
void updateReadMarker();
QPointer<QQmlComponent> m_compactBackgroundComponent;
bool m_compactBackgroundIncubating = false;
QPointer<QQuickItem> m_compactBackgroundItem;
bool m_compactMode = false;
void updateBackground();
bool m_showLocalMessagesOnRight = true;
bool m_hovered = false;
void hoverEnterEvent(QHoverEvent *event) override;
void hoverMoveEvent(QHoverEvent *event) override;
void hoverLeaveEvent(QHoverEvent *event) override;
std::function<void(QQuickItem *)> m_objectInitialCallback = [this](QQuickItem *object) {
object->setParentItem(this);
connect(object, &QQuickItem::implicitWidthChanged, this, &MessageDelegateBase::markAsDirty);
connect(object, &QQuickItem::implicitHeightChanged, this, &MessageDelegateBase::markAsDirty);
connect(object, &QQuickItem::visibleChanged, this, &MessageDelegateBase::markAsDirty);
};
std::function<void(MessageObjectIncubator *)> m_errorCallback = [this](MessageObjectIncubator *incubator) {
if (incubator->object()) {
incubator->object()->deleteLater();
}
cleanupIncubator(incubator);
};
void cleanupIncubator(MessageObjectIncubator *incubator);
void cleanupItem(QQuickItem *item);
qreal m_spacing = 0.0;
qreal m_avatarSize = 0.0;
void setCurveValues() override;
void setBaseRightPadding();
void setPercentageValues(bool fillWidth = false);
void setContentPadding();
void updateImplicitHeight() override;
bool showMessageOnRight() const;
void resizeContent() override;
QPointer<QTimer> m_temporaryHighlightTimer;
};

View File

@@ -9,13 +9,17 @@ TimelineDelegate::TimelineDelegate(QQuickItem *parent)
m_sizeHelper.setParentItem(this); m_sizeHelper.setParentItem(this);
connect(&m_sizeHelper, &DelegateSizeHelper::leftPaddingChanged, this, [this]() { connect(&m_sizeHelper, &DelegateSizeHelper::leftPaddingChanged, this, [this]() {
Q_EMIT leftPaddingChanged(); Q_EMIT leftPaddingChanged();
resizeContent(); Q_EMIT timelineWidthChanged();
updatePolish(); markAsDirty();
}); });
connect(&m_sizeHelper, &DelegateSizeHelper::rightPaddingChanged, this, [this]() { connect(&m_sizeHelper, &DelegateSizeHelper::rightPaddingChanged, this, [this]() {
Q_EMIT rightPaddingChanged(); Q_EMIT rightPaddingChanged();
resizeContent(); Q_EMIT timelineWidthChanged();
updatePolish(); markAsDirty();
});
connect(&m_sizeHelper, &DelegateSizeHelper::availableWidthChanged, this, [this]() {
Q_EMIT timelineWidthChanged();
markAsDirty();
}); });
} }
@@ -31,7 +35,7 @@ void TimelineDelegate::setContentItem(QQuickItem *item)
} }
if (m_contentItem) { if (m_contentItem) {
disconnect(m_contentItem, &QQuickItem::implicitHeightChanged, this, &TimelineDelegate::updateImplicitHeight); m_contentItem->disconnect(this);
m_contentItem->setParentItem(nullptr); m_contentItem->setParentItem(nullptr);
} }
@@ -39,13 +43,13 @@ void TimelineDelegate::setContentItem(QQuickItem *item)
if (m_contentItem) { if (m_contentItem) {
m_contentItem->setParentItem(this); m_contentItem->setParentItem(this);
connect(m_contentItem, &QQuickItem::implicitHeightChanged, this, &TimelineDelegate::updateImplicitHeight); connect(m_contentItem, &QQuickItem::implicitWidthChanged, this, &TimelineDelegate::markAsDirty);
connect(m_contentItem, &QQuickItem::implicitHeightChanged, this, &TimelineDelegate::markAsDirty);
connect(m_contentItem, &QQuickItem::visibleChanged, this, &TimelineDelegate::markAsDirty);
} }
markAsDirty();
Q_EMIT contentItemChanged(); Q_EMIT contentItemChanged();
updateImplicitHeight();
resizeContent();
} }
bool TimelineDelegate::alwaysFillWidth() bool TimelineDelegate::alwaysFillWidth()
@@ -59,10 +63,16 @@ void TimelineDelegate::setAlwaysFillWidth(bool alwaysFillWidth)
return; return;
} }
m_alwaysFillWidth = alwaysFillWidth; m_alwaysFillWidth = alwaysFillWidth;
if (m_alwaysFillWidth) {
m_sizeHelper.setEndPercentWidth(100);
} else {
m_sizeHelper.setEndPercentWidth(85);
}
Q_EMIT alwaysFillWidthChanged(); Q_EMIT alwaysFillWidthChanged();
resizeContent(); markAsDirty();
updatePolish();
} }
qreal TimelineDelegate::leftPadding() qreal TimelineDelegate::leftPadding()
@@ -85,14 +95,18 @@ void TimelineDelegate::setRightPadding(qreal rightPadding)
m_sizeHelper.setRightPadding(rightPadding); m_sizeHelper.setRightPadding(rightPadding);
} }
qreal TimelineDelegate::timelineWidth()
{
return m_sizeHelper.availableWidth();
}
void TimelineDelegate::geometryChange(const QRectF &newGeometry, const QRectF &oldGeometry) void TimelineDelegate::geometryChange(const QRectF &newGeometry, const QRectF &oldGeometry)
{ {
if (newGeometry == oldGeometry) { if (newGeometry != oldGeometry) {
return; markAsDirty();
} }
QQuickItem::geometryChange(newGeometry, oldGeometry); QQuickItem::geometryChange(newGeometry, oldGeometry);
resizeContent();
} }
void TimelineDelegate::componentComplete() void TimelineDelegate::componentComplete()
@@ -103,8 +117,14 @@ void TimelineDelegate::componentComplete()
Q_ASSERT(engine); Q_ASSERT(engine);
m_units = engine->singletonInstance<Kirigami::Platform::Units *>("org.kde.kirigami.platform", "Units"); m_units = engine->singletonInstance<Kirigami::Platform::Units *>("org.kde.kirigami.platform", "Units");
Q_ASSERT(m_units); Q_ASSERT(m_units);
setCurveValues();
connect(m_units, &Kirigami::Platform::Units::gridUnitChanged, this, &TimelineDelegate::setCurveValues); connect(m_units, &Kirigami::Platform::Units::gridUnitChanged, this, &TimelineDelegate::setCurveValues);
connect(m_units, &Kirigami::Platform::Units::largeSpacingChanged, this, &TimelineDelegate::setCurveValues);
connect(m_units, &Kirigami::Platform::Units::smallSpacingChanged, this, &TimelineDelegate::setCurveValues);
setCurveValues();
if (m_isDirty) {
resizeContent();
}
} }
void TimelineDelegate::setCurveValues() void TimelineDelegate::setCurveValues()
@@ -115,21 +135,38 @@ void TimelineDelegate::setCurveValues()
m_sizeHelper.setStartBreakpoint(qreal(m_units->gridUnit() * 46)); m_sizeHelper.setStartBreakpoint(qreal(m_units->gridUnit() * 46));
m_sizeHelper.setEndBreakpoint(qreal(m_units->gridUnit() * 66)); m_sizeHelper.setEndBreakpoint(qreal(m_units->gridUnit() * 66));
m_sizeHelper.setMaxWidth(qreal(m_units->gridUnit() * 60)); m_sizeHelper.setMaxWidth(qreal(m_units->gridUnit() * 60));
}
resizeContent(); void TimelineDelegate::markAsDirty()
{
if (!m_isDirty) {
m_isDirty = true;
polish();
}
}
void TimelineDelegate::updatePolish()
{
if (m_isDirty) {
resizeContent();
}
} }
void TimelineDelegate::resizeContent() void TimelineDelegate::resizeContent()
{ {
if (m_contentItem == nullptr || !isComponentComplete()) { if (m_contentItem == nullptr || !isComponentComplete() || m_resizingContent) {
return; return;
} }
auto availableWidth = m_alwaysFillWidth ? m_sizeHelper.maxAvailableWidth() : m_sizeHelper.availableWidth(); m_isDirty = false;
m_resizingContent = true;
const auto leftPadding = m_sizeHelper.leftPadding() + (m_sizeHelper.maxAvailableWidth() - availableWidth) / 2; updateImplicitHeight();
m_contentItem->setPosition(QPointF(leftPadding, 0));
m_contentItem->setSize(QSizeF(availableWidth, m_contentItem->implicitHeight())); m_contentItem->setPosition(QPointF(m_sizeHelper.leftX(), 0));
m_contentItem->setSize(QSizeF(m_sizeHelper.availableWidth(), m_contentItem->implicitHeight()));
m_resizingContent = false;
} }
void TimelineDelegate::updateImplicitHeight() void TimelineDelegate::updateImplicitHeight()

View File

@@ -50,6 +50,11 @@ class TimelineDelegate : public QQuickItem
*/ */
Q_PROPERTY(qreal rightPadding READ rightPadding WRITE setRightPadding NOTIFY rightPaddingChanged FINAL) Q_PROPERTY(qreal rightPadding READ rightPadding WRITE setRightPadding NOTIFY rightPaddingChanged FINAL)
/**
* @brief The width of the timeline column.
*/
Q_PROPERTY(qreal timelineWidth READ timelineWidth NOTIFY timelineWidthChanged FINAL)
public: public:
TimelineDelegate(QQuickItem *parent = nullptr); TimelineDelegate(QQuickItem *parent = nullptr);
@@ -65,28 +70,34 @@ public:
[[nodiscard]] qreal rightPadding(); [[nodiscard]] qreal rightPadding();
void setRightPadding(qreal rightPadding); void setRightPadding(qreal rightPadding);
[[nodiscard]] qreal timelineWidth();
Q_SIGNALS: Q_SIGNALS:
void contentItemChanged(); void contentItemChanged();
void alwaysFillWidthChanged(); void alwaysFillWidthChanged();
void leftPaddingChanged(); void leftPaddingChanged();
void rightPaddingChanged(); void rightPaddingChanged();
void timelineWidthChanged();
protected: protected:
void geometryChange(const QRectF &newGeometry, const QRectF &oldGeometry) override;
void componentComplete() override;
private:
Kirigami::Platform::Units *m_units = nullptr; Kirigami::Platform::Units *m_units = nullptr;
void setCurveValues(); virtual void setCurveValues();
DelegateSizeHelper m_sizeHelper; DelegateSizeHelper m_sizeHelper;
bool m_alwaysFillWidth = false; bool m_alwaysFillWidth = false;
void resizeContent();
void updateImplicitHeight();
QPointer<QQuickItem> m_contentItem; QPointer<QQuickItem> m_contentItem;
void markAsDirty();
bool m_isDirty = false;
virtual void updateImplicitHeight();
virtual void resizeContent();
bool m_resizingContent = false;
private:
void componentComplete() override;
void geometryChange(const QRectF &newGeometry, const QRectF &oldGeometry) override;
void updatePolish() override;
}; };
#endif #endif