From f4799a4287fd0c0b1104c7674be482d3316b2a9f Mon Sep 17 00:00:00 2001 From: James Graham Date: Mon, 5 May 2025 16:25:40 +0100 Subject: [PATCH] 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. --- memorytests/CMakeLists.txt | 2 +- memorytests/Main.qml | 1 + memorytests/main.cpp | 6 +- memorytests/memtest-sync.json | 300 +------------ memorytests/memtesttimelinemodel.h | 42 ++ src/libneochat/delegatesizehelper.cpp | 33 +- src/libneochat/delegatesizehelper.h | 14 +- src/timeline/AvatarFlow.qml | 27 +- src/timeline/CMakeLists.txt | 1 + src/timeline/MessageDelegate.qml | 258 +++-------- src/timeline/PollComponent.qml | 2 - src/timeline/TimelineView.qml | 2 +- src/timeline/messagedelegate.cpp | 590 ++++++++++++++++++++++++++ src/timeline/messagedelegate.h | 240 +++++++++++ src/timeline/timelinedelegate.cpp | 79 +++- src/timeline/timelinedelegate.h | 29 +- 16 files changed, 1089 insertions(+), 537 deletions(-) create mode 100644 src/timeline/messagedelegate.cpp create mode 100644 src/timeline/messagedelegate.h diff --git a/memorytests/CMakeLists.txt b/memorytests/CMakeLists.txt index dc75f4fca..aadfe071b 100644 --- a/memorytests/CMakeLists.txt +++ b/memorytests/CMakeLists.txt @@ -15,7 +15,7 @@ target_link_libraries(timeline-memtest PUBLIC Qt::Gui Qt::QuickControls2 Qt::Widgets - KF6::I18n + KF6::I18nQml KF6::Kirigami QuotientQt6 LibNeoChat diff --git a/memorytests/Main.qml b/memorytests/Main.qml index a4f71869a..04e9b7e5f 100644 --- a/memorytests/Main.qml +++ b/memorytests/Main.qml @@ -23,6 +23,7 @@ QQC2.ApplicationWindow { height: root.height contentItem: ListView { + cacheBuffer: 1000000 model: messageFilterModel delegate: EventDelegate { diff --git a/memorytests/main.cpp b/memorytests/main.cpp index 3ae04007a..c1854f0a0 100644 --- a/memorytests/main.cpp +++ b/memorytests/main.cpp @@ -5,6 +5,7 @@ #include #include +#include #include #include "memtesttimelinemodel.h" @@ -19,12 +20,15 @@ int main(int argc, char **argv) KLocalizedString::setApplicationDomain(QByteArrayLiteral("neochat")); QQmlApplicationEngine engine; - engine.loadFromModule("org.kde.neochat.timeline-memtest", "Main"); + + KLocalization::setupLocalizedContext(&engine); MemTestTimelineModel *memTestTimelineModel = new MemTestTimelineModel; MessageFilterModel *messageFilterModel = new MessageFilterModel(nullptr, memTestTimelineModel); engine.rootContext()->setContextProperty(u"memTestTimelineModel"_s, memTestTimelineModel); engine.rootContext()->setContextProperty(u"messageFilterModel"_s, messageFilterModel); + engine.loadFromModule("org.kde.neochat.timeline-memtest", "Main"); + return app.exec(); } diff --git a/memorytests/memtest-sync.json b/memorytests/memtest-sync.json index c833461c8..feb944070 100644 --- a/memorytests/memtest-sync.json +++ b/memorytests/memtest-sync.json @@ -241,7 +241,7 @@ "formatted_body": "This is an example
text message", "msgtype": "m.text" }, - "event_id": "$1000000000000:example.org", + "event_id": 0, "origin_server_ts": 1000000000000, "room_id": "!jEsUZKDJdhlrceRyVU:example.org", "sender": "@example:example.org", @@ -255,7 +255,7 @@ "body": "This is a highlight @bob:example.org", "msgtype": "m.text" }, - "event_id": "$1000000000001:example.org", + "event_id": 1, "origin_server_ts": 1000000000001, "room_id": "!jEsUZKDJdhlrceRyVU:example.org", "sender": "@example:example.org", @@ -266,11 +266,11 @@ }, { "content": { - "m.relates_to": { - "event_id": "$1000000000001:example.org", + "m.relates_to": { + "event_id": 1, "key": "👍", "rel_type": "m.annotation" - } + } }, "origin_server_ts": 1000000000002, "room_id": "!jEsUZKDJdhlrceRyVU:example.org", @@ -279,7 +279,7 @@ "unsigned": { "age": 390159120 }, - "event_id": "$1000000000002:example.org", + "event_id": 2, "age": 390159120 }, { @@ -289,7 +289,7 @@ "formatted_body": "reply", "m.relates_to": { "m.in_reply_to": { - "event_id": "$1000000000000:example.org" + "event_id": 0 } }, "msgtype": "m.text" @@ -300,7 +300,7 @@ "unsigned": { "age": 98 }, - "event_id": "$1000000000003:example.org", + "event_id": 3, "room_id": "!jEsUZKDJdhlrceRyVU:example.org" }, { @@ -317,7 +317,7 @@ "uri": "geo:51.7035,-1.14394" } }, - "event_id": "$1000000000004:example.org", + "event_id": 4, "origin_server_ts": 1000000000004, "room_id": "!jEsUZKDJdhlrceRyVU:example.org", "sender": "@example:example.org", @@ -333,7 +333,7 @@ "formatted_body": "
int 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
", "msgtype": "m.text" }, - "event_id": "$1000000000005:example.org", + "event_id": 5, "origin_server_ts": 1000000000005, "room_id": "!jEsUZKDJdhlrceRyVU: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. ", "msgtype": "m.text" }, - "event_id": "$1000000000006:example.org", + "event_id": 6, "origin_server_ts": 1000000000006, "room_id": "!jEsUZKDJdhlrceRyVU:example.org", "sender": "@example:example.org", @@ -363,7 +363,7 @@ "formatted_body": "
\n

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.

\n
", "msgtype": "m.text" }, - "event_id": "$1000000000007:example.org", + "event_id": 7, "origin_server_ts": 1000000000007, "room_id": "!jEsUZKDJdhlrceRyVU:example.org", "sender": "@example:example.org", @@ -371,282 +371,6 @@ "unsigned": { "age": 1232 } - }, - { - "content": { - "body": "This is an example text message", - "format": "org.matrix.custom.html", - "formatted_body": "This is an example
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": "
int 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
", - "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": "
\n

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.

\n
", - "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
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": "
int 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
", - "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": "
\n

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.

\n
", - "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, diff --git a/memorytests/memtesttimelinemodel.h b/memorytests/memtesttimelinemodel.h index 494dd75b5..f9a7aaaa4 100644 --- a/memorytests/memtesttimelinemodel.h +++ b/memorytests/memtesttimelinemodel.h @@ -39,10 +39,52 @@ public: testSyncFile.setFileName(QStringLiteral(DATA_DIR) + u'/' + syncFileName); testSyncFile.open(QIODevice::ReadOnly); 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); 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; + } }; /** diff --git a/src/libneochat/delegatesizehelper.cpp b/src/libneochat/delegatesizehelper.cpp index 6bd1d2ff3..4be8c4f3e 100644 --- a/src/libneochat/delegatesizehelper.cpp +++ b/src/libneochat/delegatesizehelper.cpp @@ -26,14 +26,12 @@ void DelegateSizeHelper::setParentItem(QQuickItem *parentItem) if (m_parentItem) { connect(m_parentItem, &QQuickItem::widthChanged, this, [this]() { - Q_EMIT availablePercentageWidthChanged(); - Q_EMIT availableWidthChanged(); + calcWidthsChanged(); }); } Q_EMIT parentItemChanged(); - Q_EMIT availablePercentageWidthChanged(); - Q_EMIT availableWidthChanged(); + calcWidthsChanged(); } qreal DelegateSizeHelper::leftPadding() const @@ -48,8 +46,7 @@ void DelegateSizeHelper::setLeftPadding(qreal leftPadding) } m_leftPadding = leftPadding; Q_EMIT leftPaddingChanged(); - Q_EMIT availablePercentageWidthChanged(); - Q_EMIT availableWidthChanged(); + calcWidthsChanged(); } qreal DelegateSizeHelper::rightPadding() const @@ -64,8 +61,7 @@ void DelegateSizeHelper::setRightPadding(qreal rightPadding) } m_rightPadding = rightPadding; Q_EMIT rightPaddingChanged(); - Q_EMIT availablePercentageWidthChanged(); - Q_EMIT availableWidthChanged(); + calcWidthsChanged(); } qreal DelegateSizeHelper::startBreakpoint() const @@ -80,6 +76,7 @@ void DelegateSizeHelper::setStartBreakpoint(qreal startBreakpoint) } m_startBreakpoint = startBreakpoint; Q_EMIT startBreakpointChanged(); + calcWidthsChanged(); } qreal DelegateSizeHelper::endBreakpoint() const @@ -94,6 +91,7 @@ void DelegateSizeHelper::setEndBreakpoint(qreal endBreakpoint) } m_endBreakpoint = endBreakpoint; Q_EMIT endBreakpointChanged(); + calcWidthsChanged(); } int DelegateSizeHelper::startPercentWidth() const @@ -108,6 +106,7 @@ void DelegateSizeHelper::setStartPercentWidth(int startPercentWidth) } m_startPercentWidth = startPercentWidth; Q_EMIT startPercentWidthChanged(); + calcWidthsChanged(); } int DelegateSizeHelper::endPercentWidth() const @@ -122,6 +121,7 @@ void DelegateSizeHelper::setEndPercentWidth(int endPercentWidth) } m_endPercentWidth = endPercentWidth; Q_EMIT endPercentWidthChanged(); + calcWidthsChanged(); } qreal DelegateSizeHelper::maxWidth() const @@ -143,6 +143,7 @@ void DelegateSizeHelper::setMaxWidth(qreal maxWidth) } m_maxWidth = maxWidth; Q_EMIT maxWidthChanged(); + calcWidthsChanged(); } qreal DelegateSizeHelper::maxAvailableWidth() const @@ -188,4 +189,20 @@ qreal DelegateSizeHelper::availableWidth() const 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" diff --git a/src/libneochat/delegatesizehelper.h b/src/libneochat/delegatesizehelper.h index 702dc8358..79d37ba3c 100644 --- a/src/libneochat/delegatesizehelper.h +++ b/src/libneochat/delegatesizehelper.h @@ -84,7 +84,7 @@ class DelegateSizeHelper : public QObject * * @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. @@ -124,6 +124,16 @@ public: int availablePercentageWidth() 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: void parentItemChanged(); void leftPaddingChanged(); @@ -148,5 +158,7 @@ private: int m_endPercentWidth = 85; std::optional m_maxWidth = std::nullopt; + void calcWidthsChanged(); + int calculateAvailablePercentageWidth() const; }; diff --git a/src/timeline/AvatarFlow.qml b/src/timeline/AvatarFlow.qml index 6ef3cc370..c62059ed4 100644 --- a/src/timeline/AvatarFlow.qml +++ b/src/timeline/AvatarFlow.qml @@ -3,11 +3,12 @@ import QtQuick import QtQuick.Controls as QQC2 +import QtQuick.Layouts import org.kde.kirigami as Kirigami import org.kde.kirigamiaddons.labs.components as KirigamiComponents -Flow { +RowLayout { id: root property var avatarSize: Kirigami.Units.iconSizes.small @@ -33,6 +34,11 @@ Flow { } QQC2.Label { 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 !== "" color: Kirigami.Theme.textColor horizontalAlignment: Text.AlignHCenter @@ -41,18 +47,17 @@ Flow { background: Kirigami.ShadowedRectangle { color: Kirigami.Theme.backgroundColor - Kirigami.Theme.inherit: false - Kirigami.Theme.colorSet: Kirigami.Theme.View - radius: height / 2 - shadow.size: Kirigami.Units.smallSpacing - 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.width: 1 + radius: Math.ceil(height / 2) + shadow { + size: Kirigami.Units.smallSpacing + 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) + width: 1 + } } - height: Kirigami.Units.iconSizes.small + Kirigami.Units.smallSpacing - width: Math.max(excessAvatarsTextMetrics.advanceWidth + Kirigami.Units.smallSpacing * 2, height) - TextMetrics { id: excessAvatarsTextMetrics text: excessAvatarsLabel.text diff --git a/src/timeline/CMakeLists.txt b/src/timeline/CMakeLists.txt index bb056673f..6f0035f74 100644 --- a/src/timeline/CMakeLists.txt +++ b/src/timeline/CMakeLists.txt @@ -63,6 +63,7 @@ ecm_add_qml_module(Timeline GENERATE_PLUGIN_SOURCE locationhelper.cpp mediasizehelper.cpp messageattached.cpp + messagedelegate.cpp pollhandler.cpp timelinedelegate.cpp enums/delegatetype.h diff --git a/src/timeline/MessageDelegate.qml b/src/timeline/MessageDelegate.qml index 2fbfd9a56..fc7003884 100644 --- a/src/timeline/MessageDelegate.qml +++ b/src/timeline/MessageDelegate.qml @@ -26,7 +26,7 @@ import org.kde.neochat.libneochat as LibNeoChat * 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. */ -TimelineDelegate { +MessageDelegateBase { id: root /** @@ -44,20 +44,6 @@ TimelineDelegate { */ 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. */ @@ -68,26 +54,11 @@ TimelineDelegate { */ 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. */ 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. */ @@ -130,7 +101,7 @@ TimelineDelegate { * * @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. @@ -164,179 +135,83 @@ TimelineDelegate { */ 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.timeline: root.ListView.view Message.contentModel: root.contentModel Message.index: root.index - Message.maxContentWidth: contentMaxWidth + Message.maxContentWidth: maxContentWidth - bubble.leftPadding - bubble.rightPadding 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 { - spacing: Kirigami.Units.smallSpacing + contentItem: Bubble { + 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 { - id: sectionDelegate - Layout.fillWidth: true - visible: root.showSection - labelText: root.section - colorSet: NeoChatConfig.compactLayout || root.alwaysFillWidth ? Kirigami.Theme.View : Kirigami.Theme.Window + author: root.author + showAuthor: root.showAuthor + isThreaded: root.isThreaded + + contentModel: root.contentModel + + showHighlight: root.showHighlight + + isPending: root.isPending + + onSelectedTextChanged: selectedText => { + root.Message.selectedText = selectedText; } - QQC2.ItemDelegate { - id: mainContainer - - 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 + onHoveredLinkChanged: hoveredLink => { + root.Message.hoveredLink = hoveredLink; } - LibNeoChat.DelegateSizeHelper { - id: bubbleSizeHelper - parentItem: mainContainer - leftPadding: avatar.anchors.leftMargin + (NeoChatConfig.showAvatarInTimeline ? avatar.width + bubble.anchors.leftMargin : 0) - startBreakpoint: Kirigami.Units.gridUnit * 25 - endBreakpoint: Kirigami.Units.gridUnit * 40 - startPercentWidth: root.alwaysFillWidth ? 100 : 90 - endPercentWidth: root.alwaysFillWidth ? 100 : 60 + showBackground: root.cardBackground && !NeoChatConfig.compactLayout + + TapHandler { + acceptedButtons: Qt.RightButton + onTapped: _private.showMessageMenu() + } + + TapHandler { + acceptedDevices: PointerDevice.TouchScreen + acceptedButtons: Qt.LeftButton + onLongPressed: _private.showMessageMenu() } } - function isVisibleInTimeline() { - let yoff = Math.round(y - ListView.view.contentY); - return (yoff + height > 0 && yoff < ListView.view.height); + avatarComponent: KirigamiComponents.AvatarButton { + id: avatar + 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() { @@ -348,11 +223,6 @@ TimelineDelegate { QtObject { 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() { RoomManager.viewEventMenu(root.eventId, root.room, root.author, root.Message.selectedText, root.Message.hoveredLink); } diff --git a/src/timeline/PollComponent.qml b/src/timeline/PollComponent.qml index f5218aa03..b32512ac2 100644 --- a/src/timeline/PollComponent.qml +++ b/src/timeline/PollComponent.qml @@ -10,8 +10,6 @@ import org.kde.kirigami as Kirigami import org.kde.kirigamiaddons.delegates as Delegates import org.kde.kirigamiaddons.formcard as FormCard -import Quotient - import org.kde.neochat /** diff --git a/src/timeline/TimelineView.qml b/src/timeline/TimelineView.qml index 9b14e5bba..706f80d04 100644 --- a/src/timeline/TimelineView.qml +++ b/src/timeline/TimelineView.qml @@ -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 : "" colorSet: NeoChatConfig.compactLayout ? Kirigami.Theme.View : Kirigami.Theme.Window } diff --git a/src/timeline/messagedelegate.cpp b/src/timeline/messagedelegate.cpp new file mode 100644 index 000000000..897cad0ec --- /dev/null +++ b/src/timeline/messagedelegate.cpp @@ -0,0 +1,590 @@ +// SPDX-FileCopyrightText: 2025 James Graham +// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + +#include "messagedelegate.h" +#include "timelinedelegate.h" + +#include +#include + +MessageObjectIncubator::MessageObjectIncubator(std::function initialCallback, + std::function completedCallback, + std::function errorCallback) + : QQmlIncubator(QQmlIncubator::Asynchronous) + , m_initialCallback(initialCallback) + , m_completedCallback(completedCallback) + , m_errorCallback(errorCallback) +{ +} + +void MessageObjectIncubator::setInitialState(QObject *object) +{ + auto item = qobject_cast(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(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(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(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(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" diff --git a/src/timeline/messagedelegate.h b/src/timeline/messagedelegate.h new file mode 100644 index 000000000..ba14bdd96 --- /dev/null +++ b/src/timeline/messagedelegate.h @@ -0,0 +1,240 @@ +// SPDX-FileCopyrightText: 2025 James Graham +// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + +#pragma once + +#include +#include +#include + +#include "neochatroommember.h" +#include "timelinedelegate.h" + +/** + * @brief Incubator for creating instances of components as required. + */ +class MessageObjectIncubator : public QQmlIncubator +{ +public: + MessageObjectIncubator(std::function initialCallback, + std::function completedCallback, + std::function errorCallback); + +private: + void setInitialState(QObject *object) override; + std::function m_initialCallback; + void statusChanged(QQmlIncubator::Status status) override; + std::function m_completedCallback; + std::function 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 m_author; + + bool m_isThreaded = false; + + QPointer m_avatarComponent; + bool m_avatarIncubating = false; + QPointer m_avatarItem; + bool m_showAuthor = false; + bool m_enableAvatars = true; + bool leaveAvatarSpace() const; + bool showAvatar() const; + void updateAvatar(); + + QPointer m_sectionComponent; + bool m_sectionIncubating = false; + QPointer m_sectionItem; + bool m_showSection = false; + void updateSection(); + + QPointer m_readMarkerComponent; + bool m_readMarkerIncubating = false; + QPointer m_readMarkerItem; + bool m_showReadMarkers = false; + void updateReadMarker(); + + QPointer m_compactBackgroundComponent; + bool m_compactBackgroundIncubating = false; + QPointer 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 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 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 m_temporaryHighlightTimer; +}; diff --git a/src/timeline/timelinedelegate.cpp b/src/timeline/timelinedelegate.cpp index 50dc8a254..553d23a1b 100644 --- a/src/timeline/timelinedelegate.cpp +++ b/src/timeline/timelinedelegate.cpp @@ -9,13 +9,17 @@ TimelineDelegate::TimelineDelegate(QQuickItem *parent) m_sizeHelper.setParentItem(this); connect(&m_sizeHelper, &DelegateSizeHelper::leftPaddingChanged, this, [this]() { Q_EMIT leftPaddingChanged(); - resizeContent(); - updatePolish(); + Q_EMIT timelineWidthChanged(); + markAsDirty(); }); connect(&m_sizeHelper, &DelegateSizeHelper::rightPaddingChanged, this, [this]() { Q_EMIT rightPaddingChanged(); - resizeContent(); - updatePolish(); + Q_EMIT timelineWidthChanged(); + markAsDirty(); + }); + connect(&m_sizeHelper, &DelegateSizeHelper::availableWidthChanged, this, [this]() { + Q_EMIT timelineWidthChanged(); + markAsDirty(); }); } @@ -31,7 +35,7 @@ void TimelineDelegate::setContentItem(QQuickItem *item) } if (m_contentItem) { - disconnect(m_contentItem, &QQuickItem::implicitHeightChanged, this, &TimelineDelegate::updateImplicitHeight); + m_contentItem->disconnect(this); m_contentItem->setParentItem(nullptr); } @@ -39,13 +43,13 @@ void TimelineDelegate::setContentItem(QQuickItem *item) if (m_contentItem) { 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(); - - updateImplicitHeight(); - resizeContent(); } bool TimelineDelegate::alwaysFillWidth() @@ -59,10 +63,16 @@ void TimelineDelegate::setAlwaysFillWidth(bool alwaysFillWidth) return; } m_alwaysFillWidth = alwaysFillWidth; + + if (m_alwaysFillWidth) { + m_sizeHelper.setEndPercentWidth(100); + } else { + m_sizeHelper.setEndPercentWidth(85); + } + Q_EMIT alwaysFillWidthChanged(); - resizeContent(); - updatePolish(); + markAsDirty(); } qreal TimelineDelegate::leftPadding() @@ -85,14 +95,18 @@ void TimelineDelegate::setRightPadding(qreal rightPadding) m_sizeHelper.setRightPadding(rightPadding); } +qreal TimelineDelegate::timelineWidth() +{ + return m_sizeHelper.availableWidth(); +} + void TimelineDelegate::geometryChange(const QRectF &newGeometry, const QRectF &oldGeometry) { - if (newGeometry == oldGeometry) { - return; + if (newGeometry != oldGeometry) { + markAsDirty(); } QQuickItem::geometryChange(newGeometry, oldGeometry); - resizeContent(); } void TimelineDelegate::componentComplete() @@ -103,8 +117,14 @@ void TimelineDelegate::componentComplete() Q_ASSERT(engine); m_units = engine->singletonInstance("org.kde.kirigami.platform", "Units"); Q_ASSERT(m_units); - 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() @@ -115,21 +135,38 @@ void TimelineDelegate::setCurveValues() m_sizeHelper.setStartBreakpoint(qreal(m_units->gridUnit() * 46)); m_sizeHelper.setEndBreakpoint(qreal(m_units->gridUnit() * 66)); 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() { - if (m_contentItem == nullptr || !isComponentComplete()) { + if (m_contentItem == nullptr || !isComponentComplete() || m_resizingContent) { 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; - m_contentItem->setPosition(QPointF(leftPadding, 0)); - m_contentItem->setSize(QSizeF(availableWidth, m_contentItem->implicitHeight())); + updateImplicitHeight(); + + m_contentItem->setPosition(QPointF(m_sizeHelper.leftX(), 0)); + m_contentItem->setSize(QSizeF(m_sizeHelper.availableWidth(), m_contentItem->implicitHeight())); + + m_resizingContent = false; } void TimelineDelegate::updateImplicitHeight() diff --git a/src/timeline/timelinedelegate.h b/src/timeline/timelinedelegate.h index b8c6c6b01..8d76c895e 100644 --- a/src/timeline/timelinedelegate.h +++ b/src/timeline/timelinedelegate.h @@ -50,6 +50,11 @@ class TimelineDelegate : public QQuickItem */ 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: TimelineDelegate(QQuickItem *parent = nullptr); @@ -65,28 +70,34 @@ public: [[nodiscard]] qreal rightPadding(); void setRightPadding(qreal rightPadding); + [[nodiscard]] qreal timelineWidth(); + Q_SIGNALS: void contentItemChanged(); void alwaysFillWidthChanged(); void leftPaddingChanged(); void rightPaddingChanged(); + void timelineWidthChanged(); protected: - void geometryChange(const QRectF &newGeometry, const QRectF &oldGeometry) override; - void componentComplete() override; - -private: Kirigami::Platform::Units *m_units = nullptr; - void setCurveValues(); + virtual void setCurveValues(); DelegateSizeHelper m_sizeHelper; - bool m_alwaysFillWidth = false; - void resizeContent(); - void updateImplicitHeight(); - QPointer 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