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::QuickControls2
Qt::Widgets
KF6::I18n
KF6::I18nQml
KF6::Kirigami
QuotientQt6
LibNeoChat

View File

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

View File

@@ -5,6 +5,7 @@
#include <QQmlApplicationEngine>
#include <QQmlContext>
#include <KLocalizedQmlContext>
#include <KLocalizedString>
#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();
}

View File

@@ -241,7 +241,7 @@
"formatted_body": "This is an example<br>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": "<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": "$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": "<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": "$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<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,

View File

@@ -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;
}
};
/**

View File

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

View File

@@ -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<qreal> m_maxWidth = std::nullopt;
void calcWidthsChanged();
int calculateAvailablePercentageWidth() const;
};

View File

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

View File

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

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
* 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);
}

View File

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

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 : ""
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);
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<Kirigami::Platform::Units *>("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()

View File

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