From 31f0e39617338966c2d3b7d15d774fae48ea6be8 Mon Sep 17 00:00:00 2001 From: James Graham Date: Sat, 25 May 2024 17:06:13 +0000 Subject: [PATCH] Convert TimelineDelegate to cpp There are 2 main reason for doing this: 1. Because I can, I wanted to see if I could do it 2. It gets rid of the janky qml re parenting stuff so should be faster. --- src/qml/HoverActions.qml | 2 +- src/qml/RoomMedia.qml | 4 +- src/timeline/CMakeLists.txt | 9 +- src/timeline/HiddenDelegate.qml | 5 + src/timeline/LoadingDelegate.qml | 8 ++ src/timeline/MessageDelegate.qml | 20 +-- src/timeline/ReadMarkerDelegate.qml | 7 + src/timeline/StateDelegate.qml | 5 + src/timeline/TimelineDelegate.qml | 102 --------------- src/timeline/TimelineEndDelegate.qml | 5 + src/timeline/timelinedelegate.cpp | 189 +++++++++++++++++++++++++++ src/timeline/timelinedelegate.h | 99 ++++++++++++++ 12 files changed, 339 insertions(+), 116 deletions(-) delete mode 100644 src/timeline/TimelineDelegate.qml create mode 100644 src/timeline/timelinedelegate.cpp create mode 100644 src/timeline/timelinedelegate.h diff --git a/src/qml/HoverActions.qml b/src/qml/HoverActions.qml index a2445f3e6..f1f6eba20 100644 --- a/src/qml/HoverActions.qml +++ b/src/qml/HoverActions.qml @@ -44,7 +44,7 @@ QQC2.Control { leftPadding: 0 rightPadding: 0 - x: delegate ? delegate.contentX + delegate.bubbleX : 0 + x: delegate ? delegate.contentItem.x + delegate.bubbleX : 0 y: delegate ? delegate.mapToItem(parent, 0, 0).y + delegate.bubbleY - height + Kirigami.Units.smallSpacing : 0 width: delegate ? delegate.bubbleWidth : Kirigami.Units.gridUnit * 4 diff --git a/src/qml/RoomMedia.qml b/src/qml/RoomMedia.qml index 04999c489..8fd62969b 100644 --- a/src/qml/RoomMedia.qml +++ b/src/qml/RoomMedia.qml @@ -51,7 +51,7 @@ QQC2.ScrollView { roleValue: MediaMessageFilterModel.Image delegate: MessageDelegate { alwaysShowAuthor: true - alwaysMaxWidth: true + alwaysFillWidth: true cardBackground: false room: root.currentRoom } @@ -61,7 +61,7 @@ QQC2.ScrollView { roleValue: MediaMessageFilterModel.Video delegate: MessageDelegate { alwaysShowAuthor: true - alwaysMaxWidth: true + alwaysFillWidth: true cardBackground: false room: root.currentRoom } diff --git a/src/timeline/CMakeLists.txt b/src/timeline/CMakeLists.txt index c3a46c733..bea342dec 100644 --- a/src/timeline/CMakeLists.txt +++ b/src/timeline/CMakeLists.txt @@ -7,7 +7,6 @@ qt_add_qml_module(timeline OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/src/org/kde/neochat/timeline QML_FILES EventDelegate.qml - TimelineDelegate.qml HiddenDelegate.qml MessageDelegate.qml LoadingDelegate.qml @@ -45,6 +44,9 @@ qt_add_qml_module(timeline StateComponent.qml TextComponent.qml VideoComponent.qml + SOURCES + timelinedelegate.cpp + timelinedelegate.h RESOURCES images/bike.svg images/bus.svg @@ -72,3 +74,8 @@ qt_add_qml_module(timeline images/wait.svg images/walk.svg ) + +target_link_libraries(timeline PRIVATE + Qt::Quick + KF6::Kirigami +) diff --git a/src/timeline/HiddenDelegate.qml b/src/timeline/HiddenDelegate.qml index ba04ae933..7291b948a 100644 --- a/src/timeline/HiddenDelegate.qml +++ b/src/timeline/HiddenDelegate.qml @@ -41,6 +41,11 @@ TimelineDelegate { */ required property var author + width: parent?.width + rightPadding: Config.compactLayout && root.ListView.view.width >= Kirigami.Units.gridUnit * 20 ? Kirigami.Units.gridUnit * 2 + Kirigami.Units.largeSpacing : Kirigami.Units.largeSpacing + + alwaysFillWidth: Config.compactLayout + contentItem: QQC2.Control { id: contentControl diff --git a/src/timeline/LoadingDelegate.qml b/src/timeline/LoadingDelegate.qml index d8e7f8d17..539a5ffa0 100644 --- a/src/timeline/LoadingDelegate.qml +++ b/src/timeline/LoadingDelegate.qml @@ -5,8 +5,16 @@ import QtQuick import org.kde.kirigami as Kirigami +import org.kde.neochat + TimelineDelegate { id: root + + width: parent?.width + rightPadding: Config.compactLayout && root.ListView.view.width >= Kirigami.Units.gridUnit * 20 ? Kirigami.Units.gridUnit * 2 + Kirigami.Units.largeSpacing : Kirigami.Units.largeSpacing + + alwaysFillWidth: Config.compactLayout + contentItem: Kirigami.PlaceholderMessage { text: i18n("Loading…") } diff --git a/src/timeline/MessageDelegate.qml b/src/timeline/MessageDelegate.qml index 4fd7a8492..51640385e 100644 --- a/src/timeline/MessageDelegate.qml +++ b/src/timeline/MessageDelegate.qml @@ -202,11 +202,6 @@ TimelineDelegate { */ property bool cardBackground: true - /** - * @brief Whether the delegate should always stretch to the maximum availabel width. - */ - property bool alwaysMaxWidth: false - /** * @brief Whether the message should be highlighted. */ @@ -241,6 +236,11 @@ TimelineDelegate { */ property real contentMaxWidth: bubbleSizeHelper.currentWidth - bubble.leftPadding - bubble.rightPadding + width: parent?.width + rightPadding: Config.compactLayout && root.ListView.view.width >= Kirigami.Units.gridUnit * 20 ? Kirigami.Units.gridUnit * 2 + Kirigami.Units.largeSpacing : Kirigami.Units.largeSpacing + + alwaysFillWidth: Config.compactLayout + contentItem: ColumnLayout { spacing: Kirigami.Units.smallSpacing @@ -249,7 +249,7 @@ TimelineDelegate { Layout.fillWidth: true visible: root.showSection labelText: root.section - colorSet: Config.compactLayout || root.alwaysMaxWidth ? Kirigami.Theme.View : Kirigami.Theme.Window + colorSet: Config.compactLayout || root.alwaysFillWidth ? Kirigami.Theme.View : Kirigami.Theme.Window } QQC2.ItemDelegate { id: mainContainer @@ -347,7 +347,7 @@ TimelineDelegate { } background: Rectangle { - visible: mainContainer.hovered && (Config.compactLayout || root.alwaysMaxWidth) + visible: mainContainer.hovered && (Config.compactLayout || root.alwaysFillWidth) color: Kirigami.ColorUtils.tintWithAlpha(Kirigami.Theme.backgroundColor, Kirigami.Theme.highlightColor, 0.15) radius: Kirigami.Units.cornerRadius } @@ -387,8 +387,8 @@ TimelineDelegate { id: bubbleSizeHelper startBreakpoint: Kirigami.Units.gridUnit * 25 endBreakpoint: Kirigami.Units.gridUnit * 40 - startPercentWidth: Config.compactLayout || root.alwaysMaxWidth ? 100 : 90 - endPercentWidth: Config.compactLayout || root.alwaysMaxWidth ? 100 : 60 + startPercentWidth: root.alwaysFillWidth ? 100 : 90 + endPercentWidth: root.alwaysFillWidth ? 100 : 60 parentWidth: mainContainer.availableWidth - (Config.showAvatarInTimeline ? avatar.width + bubble.anchors.leftMargin : 0) } @@ -411,7 +411,7 @@ TimelineDelegate { /** * @brief Whether local user messages should be aligned right. */ - property bool showUserMessageOnRight: Config.showLocalMessagesOnRight && root.author.isLocalUser && !Config.compactLayout && !root.alwaysMaxWidth + property bool showUserMessageOnRight: Config.showLocalMessagesOnRight && root.author.isLocalUser && !Config.compactLayout && !root.alwaysFillWidth function showMessageMenu() { RoomManager.viewEventMenu(root.eventId, root.room, root.selectedText); diff --git a/src/timeline/ReadMarkerDelegate.qml b/src/timeline/ReadMarkerDelegate.qml index df4d4ad3e..54b27d400 100644 --- a/src/timeline/ReadMarkerDelegate.qml +++ b/src/timeline/ReadMarkerDelegate.qml @@ -8,6 +8,8 @@ import QtQuick.Layouts import Qt.labs.qmlmodels import org.kde.kirigami as Kirigami +import org.kde.neochat + TimelineDelegate { id: root @@ -16,6 +18,11 @@ TimelineDelegate { temporaryHighlightTimer.start(); } + width: parent?.width + rightPadding: Config.compactLayout && root.ListView.view.width >= Kirigami.Units.gridUnit * 20 ? Kirigami.Units.gridUnit * 2 + Kirigami.Units.largeSpacing : Kirigami.Units.largeSpacing + + alwaysFillWidth: Config.compactLayout + contentItem: QQC2.ItemDelegate { padding: Kirigami.Units.largeSpacing topInset: Kirigami.Units.largeSpacing diff --git a/src/timeline/StateDelegate.qml b/src/timeline/StateDelegate.qml index ae1c9d35e..7b2a90d1d 100644 --- a/src/timeline/StateDelegate.qml +++ b/src/timeline/StateDelegate.qml @@ -73,6 +73,11 @@ TimelineDelegate { */ property bool folded: true + width: parent?.width + rightPadding: Config.compactLayout && root.ListView.view.width >= Kirigami.Units.gridUnit * 20 ? Kirigami.Units.gridUnit * 2 + Kirigami.Units.largeSpacing : Kirigami.Units.largeSpacing + + alwaysFillWidth: Config.compactLayout + contentItem: ColumnLayout { SectionDelegate { Layout.fillWidth: true diff --git a/src/timeline/TimelineDelegate.qml b/src/timeline/TimelineDelegate.qml deleted file mode 100644 index b7bd7f1b6..000000000 --- a/src/timeline/TimelineDelegate.qml +++ /dev/null @@ -1,102 +0,0 @@ -// SPDX-FileCopyrightText: 2022 James Graham -// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL - -import QtQuick - -import org.kde.kirigami as Kirigami - -import org.kde.neochat - -/** - * @brief The base Item for all delegates in the timeline. - * - * This component handles the placing of the main content for a delegate in the - * timeline. The component is designed for all delegates, positioning them in the - * timeline with variable padding depending on the window width. - * - * This component also supports always setting the delegate to fill the available - * width in the timeline, e.g. in compact mode. - */ -Item { - id: root - - /** - * @brief The Item representing the delegate's main content. - */ - property Item contentItem - - /** - * @brief The x position of the content item. - * - * @note Used for positioning the hover actions. - */ - property real contentX: contentItemParent.x - - /** - * @brief Whether the delegate should always stretch to the maximum available width. - */ - property bool alwaysMaxWidth: false - - /** - * @brief The padding to the left of the content. - */ - property real leftPadding: Kirigami.Units.largeSpacing - - /** - * @brief The padding to the right of the content. - */ - property real rightPadding: Config.compactLayout && root.ListView.view.width >= Kirigami.Units.gridUnit * 20 ? Kirigami.Units.gridUnit * 2 + Kirigami.Units.largeSpacing : Kirigami.Units.largeSpacing - - width: parent?.width - implicitHeight: contentItemParent.implicitHeight - - Item { - id: contentItemParent - anchors.top: parent.top - anchors.bottom: parent.bottom - anchors.leftMargin: state === "alignLeft" ? Kirigami.Units.largeSpacing : 0 - - state: Config.compactLayout || root.alwaysMaxWidth ? "alignLeft" : "alignCenter" - // Align left when in compact mode and center when using bubbles - states: [ - State { - name: "alignLeft" - AnchorChanges { - target: contentItemParent - anchors.horizontalCenter: undefined - anchors.left: parent ? parent.left : undefined - } - }, - State { - name: "alignCenter" - AnchorChanges { - target: contentItemParent - anchors.horizontalCenter: parent ? parent.horizontalCenter : undefined - anchors.left: undefined - } - } - ] - - width: (Config.compactLayout || root.alwaysMaxWidth ? root.width : delegateSizeHelper.currentWidth) - root.leftPadding - root.rightPadding - implicitHeight: root.contentItem?.implicitHeight ?? 0 - } - - DelegateSizeHelper { - id: delegateSizeHelper - startBreakpoint: Kirigami.Units.gridUnit * 46 - endBreakpoint: Kirigami.Units.gridUnit * 66 - startPercentWidth: 100 - endPercentWidth: 85 - maxWidth: Kirigami.Units.gridUnit * 60 - - parentWidth: root.width - } - - onContentItemChanged: { - if (!contentItem) { - return; - } - contentItem.parent = contentItemParent; - contentItem.anchors.fill = contentItem.parent; - } -} diff --git a/src/timeline/TimelineEndDelegate.qml b/src/timeline/TimelineEndDelegate.qml index 9a8bf6baa..3ef484311 100644 --- a/src/timeline/TimelineEndDelegate.qml +++ b/src/timeline/TimelineEndDelegate.qml @@ -17,6 +17,11 @@ TimelineDelegate { */ required property NeoChatRoom room + width: parent?.width + rightPadding: Config.compactLayout && root.ListView.view.width >= Kirigami.Units.gridUnit * 20 ? Kirigami.Units.gridUnit * 2 + Kirigami.Units.largeSpacing : Kirigami.Units.largeSpacing + + alwaysFillWidth: Config.compactLayout + contentItem: ColumnLayout { RowLayout { Layout.topMargin: Kirigami.Units.largeSpacing diff --git a/src/timeline/timelinedelegate.cpp b/src/timeline/timelinedelegate.cpp new file mode 100644 index 000000000..6ba8ab325 --- /dev/null +++ b/src/timeline/timelinedelegate.cpp @@ -0,0 +1,189 @@ +// SPDX-FileCopyrightText: 2024 James Graham +// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + +#include "timelinedelegate.h" + +TimelineDelegate::TimelineDelegate(QQuickItem *parent) + : QQuickItem(parent) +{ +} + +QQuickItem *TimelineDelegate::contentItem() +{ + return m_contentItem; +} + +void TimelineDelegate::setContentItem(QQuickItem *item) +{ + if (m_contentItem == item) { + return; + } + + if (m_contentItem) { + disconnect(m_contentItem, &QQuickItem::implicitHeightChanged, this, &TimelineDelegate::updateImplicitHeight); + m_contentItem->setParentItem(nullptr); + } + + m_contentItem = item; + + if (m_contentItem) { + m_contentItem->setParentItem(this); + connect(m_contentItem, &QQuickItem::implicitHeightChanged, this, &TimelineDelegate::updateImplicitHeight); + } + + Q_EMIT contentItemChanged(); + + updateImplicitHeight(); + resizeContent(); +} + +bool TimelineDelegate::alwaysFillWidth() +{ + return m_alwaysFillWidth; +} + +void TimelineDelegate::setAlwaysFillWidth(bool alwaysFillWidth) +{ + if (alwaysFillWidth == m_alwaysFillWidth) { + return; + } + m_alwaysFillWidth = alwaysFillWidth; + Q_EMIT alwaysFillWidthChanged(); + + resizeContent(); + updatePolish(); +} + +qreal TimelineDelegate::leftPadding() +{ + return m_leftPadding; +} + +void TimelineDelegate::setLeftPadding(qreal leftPadding) +{ + if (qFuzzyCompare(leftPadding, m_leftPadding)) { + return; + } + + m_leftPadding = leftPadding; + Q_EMIT leftPaddingChanged(); + + resizeContent(); + updatePolish(); +} + +qreal TimelineDelegate::rightPadding() +{ + return m_rightPadding; +} + +void TimelineDelegate::setRightPadding(qreal rightPadding) +{ + if (qFuzzyCompare(rightPadding, m_rightPadding)) { + return; + } + + m_rightPadding = rightPadding; + Q_EMIT rightPaddingChanged(); + + resizeContent(); + updatePolish(); +} + +void TimelineDelegate::geometryChange(const QRectF &newGeometry, const QRectF &oldGeometry) +{ + if (newGeometry == oldGeometry) { + return; + } + + QQuickItem::geometryChange(newGeometry, oldGeometry); + resizeContent(); +} + +void TimelineDelegate::componentComplete() +{ + QQuickItem::componentComplete(); + + auto engine = qmlEngine(this); + 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); +} + +void TimelineDelegate::setCurveValues() +{ + m_leftPadding = qreal(m_units->largeSpacing()); + m_rightPadding = qreal(m_units->largeSpacing()); + + m_startBreakpoint = qreal(m_units->gridUnit() * 46); + m_endBreakpoint = qreal(m_units->gridUnit() * 66); + m_maxWidth = qreal(m_units->gridUnit() * 60); + + resizeContent(); +} + +int TimelineDelegate::availablePercentageWidth() const +{ + // Don't bother with calculations for a horizontal line. + if (m_startPercentWidth == m_endPercentWidth) { + return m_startPercentWidth; + } + // Dividing by zero is a bad idea. + if (m_startBreakpoint == m_endBreakpoint || qFuzzyCompare(width(), 0)) { + return 100; + } + + // Fit to y = mx + c + qreal m = (m_endPercentWidth - m_startPercentWidth) / (m_endBreakpoint - m_startBreakpoint); + qreal c = m_startPercentWidth - m * m_startBreakpoint; + + // This allows us to clamp correctly if the start or end width is bigger. + bool endPercentBigger = m_endPercentWidth > m_startPercentWidth; + int maxPercentWidth = endPercentBigger ? m_endPercentWidth : m_startPercentWidth; + int minPercentWidth = endPercentBigger ? m_startPercentWidth : m_endPercentWidth; + + int calcPercentWidth = std::round(m * maxAvailableWidth() + c); + return std::clamp(calcPercentWidth, minPercentWidth, maxPercentWidth); +} + +qreal TimelineDelegate::maxAvailableWidth() const +{ + if (qFuzzyCompare(width(), 0)) { + return 0; + } + + return std::max(width() - m_leftPadding - m_rightPadding, 0.0); +} + +qreal TimelineDelegate::availableWidth() const +{ + if (m_alwaysFillWidth) { + return maxAvailableWidth(); + } + + qreal absoluteWidth = maxAvailableWidth() * availablePercentageWidth() * 0.01; + return std::round(std::min(absoluteWidth, m_maxWidth)); +} + +void TimelineDelegate::resizeContent() +{ + if (m_contentItem == nullptr || !isComponentComplete()) { + return; + } + + const auto leftPadding = m_leftPadding + (maxAvailableWidth() - availableWidth()) / 2; + m_contentItem->setPosition(QPointF(leftPadding, 0)); + m_contentItem->setSize(QSizeF(availableWidth(), m_contentItem->implicitHeight())); +} + +void TimelineDelegate::updateImplicitHeight() +{ + if (m_contentItem == nullptr) { + setImplicitHeight(0); + } + setImplicitHeight(m_contentItem->implicitHeight()); +} + +#include "moc_timelinedelegate.cpp" diff --git a/src/timeline/timelinedelegate.h b/src/timeline/timelinedelegate.h new file mode 100644 index 000000000..52fc5dae2 --- /dev/null +++ b/src/timeline/timelinedelegate.h @@ -0,0 +1,99 @@ +// SPDX-FileCopyrightText: 2024 James Graham +// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + +#ifndef TIMELINEDELEGATE_H +#define TIMELINEDELEGATE_H + +#include + +#include + +/** + * @brief The base Item for all delegates in the timeline. + * + * This component handles the placing of the main content for a delegate in the + * timeline. The component is designed for all delegates, positioning them in the + * timeline with variable padding depending on the window width. + * + * This component also supports always setting the delegate to fill the available + * width in the timeline, e.g. in compact mode. + */ +class TimelineDelegate : public QQuickItem +{ + Q_OBJECT + QML_ELEMENT + + /** + * @brief This property holds the visual content Item. + * + * It will be resized both in width and height with the layout resizing. + * Its height will be resized to still have room for the heder and footer + */ + Q_PROPERTY(QQuickItem *contentItem READ contentItem WRITE setContentItem NOTIFY contentItemChanged FINAL) + + /** + * @brief Whether the contentItem should fill all available width regardless of how wide the delegate is. + * + * The left and right padding values will still be respected. + */ + Q_PROPERTY(bool alwaysFillWidth READ alwaysFillWidth WRITE setAlwaysFillWidth NOTIFY alwaysFillWidthChanged FINAL) + + /** + * @brief The minimum padding to the left of the content. + */ + Q_PROPERTY(qreal leftPadding READ leftPadding WRITE setLeftPadding NOTIFY leftPaddingChanged FINAL) + + /** + * @brief The minimum padding to the right of the content. + */ + Q_PROPERTY(qreal rightPadding READ rightPadding WRITE setRightPadding NOTIFY rightPaddingChanged FINAL) + +public: + TimelineDelegate(QQuickItem *parent = nullptr); + + [[nodiscard]] QQuickItem *contentItem(); + void setContentItem(QQuickItem *item); + + [[nodiscard]] bool alwaysFillWidth(); + void setAlwaysFillWidth(bool alwaysFillWidth); + + [[nodiscard]] qreal leftPadding(); + void setLeftPadding(qreal leftPadding); + + [[nodiscard]] qreal rightPadding(); + void setRightPadding(qreal rightPadding); + +Q_SIGNALS: + void contentItemChanged(); + void alwaysFillWidthChanged(); + void leftPaddingChanged(); + void rightPaddingChanged(); + +protected: + void geometryChange(const QRectF &newGeometry, const QRectF &oldGeometry) override; + void componentComplete() override; + +private: + Kirigami::Platform::Units *m_units = nullptr; + void setCurveValues(); + + qreal m_leftPadding; + qreal m_rightPadding; + + qreal m_startBreakpoint; + qreal m_endBreakpoint; + int m_startPercentWidth = 100; + int m_endPercentWidth = 85; + qreal m_maxWidth; + int availablePercentageWidth() const; + qreal maxAvailableWidth() const; + qreal availableWidth() const; + bool m_alwaysFillWidth = false; + + void resizeContent(); + void updateImplicitHeight(); + + QPointer m_contentItem; +}; + +#endif