From 697058146ed309de44a54f188c33c432d6932312 Mon Sep 17 00:00:00 2001 From: zach Date: Fri, 19 Jun 2026 22:50:52 +0200 Subject: [PATCH] notif actions rework --- Components/TextButton.qml | 39 ++++ Daemons/NotifServer.qml | 6 + Modules/Notifications/Content.qml | 5 +- Modules/Notifications/Notification.qml | 274 +++++++++++++---------- Plugins/ZShell/Components/CMakeLists.txt | 1 + Plugins/ZShell/Components/buttonrow.cpp | 132 +++++++++++ Plugins/ZShell/Components/buttonrow.hpp | 37 +++ 7 files changed, 374 insertions(+), 120 deletions(-) create mode 100644 Components/TextButton.qml create mode 100644 Plugins/ZShell/Components/buttonrow.cpp create mode 100644 Plugins/ZShell/Components/buttonrow.hpp diff --git a/Components/TextButton.qml b/Components/TextButton.qml new file mode 100644 index 0000000..78e3f08 --- /dev/null +++ b/Components/TextButton.qml @@ -0,0 +1,39 @@ +import QtQuick +import qs.Config + +ButtonBase { + id: root + + readonly property alias label: label + property alias text: label.text + + activeColor: type === TextButton.Filled ? DynamicColors.palette.m3primary : DynamicColors.palette.m3secondary + activeOnColor: { + if (type === TextButton.Text) + return DynamicColors.palette.m3primary; + return type === TextButton.Filled ? DynamicColors.palette.m3onPrimary : DynamicColors.palette.m3onSecondary; + } + horizontalPadding: Appearance.padding.normal + implicitHeight: label.implicitHeight + verticalPadding * 2 + implicitWidth: label.implicitWidth + horizontalPadding * 2 + inactiveColor: { + if (!isToggle && type === TextButton.Filled) + return DynamicColors.palette.m3primary; + return type === TextButton.Filled ? DynamicColors.tPalette.m3surfaceContainer : DynamicColors.palette.m3secondaryContainer; + } + inactiveOnColor: { + if (!isToggle && type === TextButton.Filled) + return DynamicColors.palette.m3onPrimary; + if (type === TextButton.Text) + return DynamicColors.palette.m3primary; + return type === TextButton.Filled ? DynamicColors.palette.m3onSurface : DynamicColors.palette.m3onSecondaryContainer; + } + verticalPadding: Appearance.padding.small + + CustomText { + id: label + + anchors.centerIn: parent + color: root.onColor + } +} diff --git a/Daemons/NotifServer.qml b/Daemons/NotifServer.qml index 00fa168..34b81ec 100644 --- a/Daemons/NotifServer.qml +++ b/Daemons/NotifServer.qml @@ -215,6 +215,10 @@ Singleton { notif.hasActionIcons = notif.notification.hasActionIcons; } + function onHintsChanged(): void { + notif.hints = notif.notification.hints; + } + function onImageChanged(): void { notif.imageSource = notif.notification.image || ""; notif.image = notif.imageSource; @@ -237,6 +241,7 @@ Singleton { } property real expireTimeout: 5 property bool hasActionIcons + property var hints property string image property string imageSource property var locks: new Set() @@ -333,6 +338,7 @@ Singleton { appName = notification.appName; imageSource = notification.image || ""; image = imageSource; + hints = notification.hints; expireTimeout = notification.expireTimeout; urgency = notification.urgency; resident = notification.resident; diff --git a/Modules/Notifications/Content.qml b/Modules/Notifications/Content.qml index 1b2f747..71ddb2d 100644 --- a/Modules/Notifications/Content.qml +++ b/Modules/Notifications/Content.qml @@ -1,3 +1,5 @@ +pragma ComponentBehavior: Bound + import qs.Components import qs.Config import qs.Daemons @@ -149,11 +151,12 @@ Item { color: "transparent" implicitHeight: notif.implicitHeight implicitWidth: notif.implicitWidth - radius: Appearance.rounding.smallest / 2 + radius: Appearance.rounding.small Notification { id: notif + implicitWidth: root.implicitWidth - root.padding * 2 modelData: wrapper.modelData } } diff --git a/Modules/Notifications/Notification.qml b/Modules/Notifications/Notification.qml index 9876288..3667947 100644 --- a/Modules/Notifications/Notification.qml +++ b/Modules/Notifications/Notification.qml @@ -1,35 +1,33 @@ pragma ComponentBehavior: Bound -import qs.Components -import qs.Config -import qs.Modules +import QtQuick +import QtQuick.Shapes +import Quickshell +import Quickshell.Services.Notifications +import ZShell.Components import qs.Daemons import qs.Helpers -import Quickshell -import Quickshell.Widgets -import Quickshell.Services.Notifications -import QtQuick -import QtQuick.Layouts +import qs.Config +import qs.Components CustomRect { id: root + readonly property int bodyTextFormat: /[<*_`#\[\]]/.test(modelData.body) ? Text.MarkdownText : Text.PlainText property bool expanded: Config.notifs.openExpanded readonly property bool hasAppIcon: modelData.appIcon.length > 0 readonly property bool hasImage: modelData.image.length > 0 required property NotifServer.Notif modelData - readonly property int nonAnimHeight: summary.implicitHeight + (root.expanded ? appName.height + body.height + actions.height + actions.anchors.topMargin : bodyPreview.height) + inner.anchors.margins * 2 + readonly property int nonAnimHeight: summary.implicitHeight + (root.expanded ? Appearance.spacing.extraSmall * 2 + appName.height + body.height + actions.height + actions.anchors.topMargin : bodyPreview.height) + inner.anchors.margins * 2 - clip: true color: root.modelData.urgency === NotificationUrgency.Critical ? DynamicColors.palette.m3secondaryContainer : DynamicColors.tPalette.m3surfaceContainer implicitHeight: inner.implicitHeight - implicitWidth: Config.notifs.sizes.width - radius: 6 - x: Config.notifs.sizes.width + radius: Appearance.rounding.small + x: implicitWidth Behavior on x { Anim { - easing.bezierCurve: MaterialEasing.expressiveEffects + easing.bezierCurve: Appearance.anim.curves.emphasizedDecel } } @@ -55,7 +53,7 @@ CustomRect { return; const actions = root.modelData.actions; - if (actions?.length === 1) + if (actions.length === 1) actions[0].invoke(); } onEntered: root.modelData.timer.stop() @@ -80,7 +78,7 @@ CustomRect { if (!containsMouse) root.modelData.timer.start(); - if (Math.abs(root.x) < Config.notifs.sizes.width * Config.notifs.clearThreshold) + if (Math.abs(root.x) < root.implicitWidth * Config.notifs.clearThreshold) root.x = 0; else root.modelData.popup = false; @@ -90,7 +88,7 @@ CustomRect { id: inner anchors.left: parent.left - anchors.margins: 8 + anchors.margins: Appearance.padding.normal anchors.right: parent.right anchors.top: parent.top implicitHeight: root.nonAnimHeight @@ -111,7 +109,8 @@ CustomRect { visible: root.hasImage || root.hasAppIcon width: Config.notifs.sizes.image - sourceComponent: ClippingRectangle { + sourceComponent: CustomClippingRect { + color: root.modelData.urgency === NotificationUrgency.Critical ? DynamicColors.palette.m3error : root.modelData.urgency === NotificationUrgency.Low ? DynamicColors.layer(DynamicColors.palette.m3surfaceContainerHighest, 2) : DynamicColors.palette.m3secondaryContainer implicitHeight: Config.notifs.sizes.image implicitWidth: Config.notifs.sizes.image radius: Appearance.rounding.full @@ -121,8 +120,11 @@ CustomRect { asynchronous: true cache: false fillMode: Image.PreserveAspectCrop - mipmap: true source: Qt.resolvedUrl(root.modelData.image) + sourceSize: { + const size = Config.notifs.sizes.image * ((QsWindow.window as QsWindow)?.devicePixelRatio ?? 1); + return Qt.size(size, size); + } } } } @@ -152,8 +154,9 @@ CustomRect { height: Math.round(parent.width * 0.6) width: Math.round(parent.width * 0.6) - sourceComponent: CustomIcon { + sourceComponent: ColoredIcon { anchors.fill: parent + color: root.modelData.urgency === NotificationUrgency.Critical ? DynamicColors.palette.m3onError : root.modelData.urgency === NotificationUrgency.Low ? DynamicColors.palette.m3onSurface : DynamicColors.palette.m3onSecondaryContainer layer.enabled: root.modelData.appIcon.endsWith("symbolic") source: Quickshell.iconPath(root.modelData.appIcon) } @@ -162,34 +165,67 @@ CustomRect { Loader { active: !root.hasAppIcon anchors.centerIn: parent - anchors.horizontalCenterOffset: -18 * 0.02 - anchors.verticalCenterOffset: 18 * 0.02 + anchors.verticalCenterOffset: 1 asynchronous: true sourceComponent: MaterialIcon { color: root.modelData.urgency === NotificationUrgency.Critical ? DynamicColors.palette.m3onError : root.modelData.urgency === NotificationUrgency.Low ? DynamicColors.palette.m3onSurface : DynamicColors.palette.m3onSecondaryContainer - font.pointSize: 18 text: Icons.getNotifIcon(root.modelData.summary, root.modelData.urgency) } } } } + Shape { + id: progressIndicator + + anchors.centerIn: appIcon + height: appIcon.implicitHeight + progressShape.strokeWidth * 2 + preferredRendererType: Shape.CurveRenderer + width: appIcon.implicitWidth + progressShape.strokeWidth * 2 + + ShapePath { + id: progressShape + + capStyle: ShapePath.RoundCap + fillColor: "transparent" + strokeColor: DynamicColors.palette.m3primary + strokeWidth: 2 + + PathAngleArc { + id: progressArc + + centerX: progressIndicator.width / 2 + centerY: progressIndicator.height / 2 + radiusX: progressIndicator.width / 2 - Appearance.padding.extraSmall / 2 + radiusY: progressIndicator.height / 2 - Appearance.padding.extraSmall / 2 + startAngle: -90 + sweepAngle: ((root.modelData.hints.value ?? 0) / 100) * 360 + + Behavior on sweepAngle { + Anim { + easing.bezierCurve: Appearance.anim.curves.emphasizedDecel + } + } + } + } + } + CustomText { id: appName anchors.left: image.right - anchors.leftMargin: 10 + anchors.leftMargin: Appearance.spacing.small anchors.top: parent.top animate: true color: DynamicColors.palette.m3onSurfaceVariant - font.pointSize: 10 maximumLineCount: 1 opacity: root.expanded ? 1 : 0 text: appNameMetrics.elidedText Behavior on opacity { Anim { + type: Anim.DefaultEffects } } } @@ -198,9 +234,8 @@ CustomRect { id: appNameMetrics elide: Text.ElideRight - elideWidth: expandBtn.x - time.width - timeSep.width - summary.x - 7 * 3 - font.family: appName.font.family - font.pointSize: appName.font.pointSize + elideWidth: expandBtn.x - time.width - timeSep.width - summary.x - Appearance.spacing.small * 3 + font: appName.font text: root.modelData.appName } @@ -208,7 +243,7 @@ CustomRect { id: summary anchors.left: image.right - anchors.leftMargin: 10 + anchors.leftMargin: Appearance.spacing.small anchors.top: parent.top animate: true height: implicitHeight @@ -224,6 +259,9 @@ CustomRect { when: root.expanded PropertyChanges { + body.anchors.topMargin: Appearance.spacing.extraSmall + bodyPreview.anchors.topMargin: Appearance.spacing.extraSmall + summary.anchors.topMargin: Appearance.spacing.extraSmall summary.maximumLineCount: undefined } @@ -238,10 +276,11 @@ CustomRect { target: summary } - AnchorAnimation { - duration: MaterialEasing.expressiveEffectsTime - easing.bezierCurve: MaterialEasing.expressiveEffects - easing.type: Easing.BezierSpline + Anim { + property: "topMargin" + } + + AnchorAnim { } } } @@ -250,9 +289,8 @@ CustomRect { id: summaryMetrics elide: Text.ElideRight - elideWidth: expandBtn.x - time.width - timeSep.width - summary.x - 7 * 3 - font.family: summary.font.family - font.pointSize: summary.font.pointSize + elideWidth: expandBtn.x - time.width - timeSep.width - summary.x - Appearance.spacing.small * 3 + font: summary.font text: root.modelData.summary } @@ -260,10 +298,9 @@ CustomRect { id: timeSep anchors.left: summary.right - anchors.leftMargin: 7 + anchors.leftMargin: Appearance.spacing.small anchors.top: parent.top color: DynamicColors.palette.m3onSurfaceVariant - font.pointSize: 10 text: "•" states: State { @@ -276,10 +313,7 @@ CustomRect { } } transitions: Transition { - AnchorAnimation { - duration: MaterialEasing.expressiveEffectsTime - easing.bezierCurve: MaterialEasing.expressiveEffects - easing.type: Easing.BezierSpline + AnchorAnim { } } } @@ -288,11 +322,10 @@ CustomRect { id: time anchors.left: timeSep.right - anchors.leftMargin: 7 + anchors.leftMargin: Appearance.spacing.small anchors.top: parent.top animate: true color: DynamicColors.palette.m3onSurfaceVariant - font.pointSize: 10 horizontalAlignment: Text.AlignLeft text: root.modelData.timeStr } @@ -302,25 +335,33 @@ CustomRect { anchors.right: parent.right anchors.top: parent.top - implicitHeight: expandIcon.height - implicitWidth: expandIcon.height + anchors.topMargin: -Appearance.padding.extraSmall + implicitHeight: expandIcon.implicitHeight + implicitWidth: expandIcon.implicitHeight StateLayer { color: root.modelData.urgency === NotificationUrgency.Critical ? DynamicColors.palette.m3onSecondaryContainer : DynamicColors.palette.m3onSurface radius: Appearance.rounding.full - onClicked: { - root.expanded = !root.expanded; - } + onClicked: root.expanded = !root.expanded } MaterialIcon { id: expandIcon anchors.centerIn: parent - animate: true - font.pointSize: 13 - text: root.expanded ? "expand_less" : "expand_more" + anchors.verticalCenterOffset: root.expanded ? -1 : 1 + rotation: root.expanded ? 180 : 0 + text: "expand_more" + + Behavior on anchors.verticalCenterOffset { + Anim { + } + } + Behavior on rotation { + Anim { + } + } } } @@ -329,17 +370,17 @@ CustomRect { anchors.left: summary.left anchors.right: expandBtn.left - anchors.rightMargin: 7 + anchors.rightMargin: Appearance.spacing.small anchors.top: summary.bottom animate: true color: DynamicColors.palette.m3onSurfaceVariant - font.pointSize: 10 opacity: root.expanded ? 0 : 1 text: bodyPreviewMetrics.elidedText - textFormat: Text.MarkdownText + textFormat: root.bodyTextFormat Behavior on opacity { Anim { + type: Anim.DefaultEffects } } } @@ -349,8 +390,7 @@ CustomRect { elide: Text.ElideRight elideWidth: bodyPreview.width - font.family: bodyPreview.font.family - font.pointSize: bodyPreview.font.pointSize + font: bodyPreview.font text: root.modelData.body } @@ -359,19 +399,19 @@ CustomRect { anchors.left: summary.left anchors.right: expandBtn.left - anchors.rightMargin: 7 + anchors.rightMargin: Appearance.spacing.small anchors.top: summary.bottom animate: true color: DynamicColors.palette.m3onSurfaceVariant - font.pointSize: 10 height: text ? implicitHeight : 0 opacity: root.expanded ? 1 : 0 text: root.modelData.body - textFormat: Text.MarkdownText + textFormat: root.bodyTextFormat wrapMode: Text.WrapAtWordBoundaryOrAnywhere Behavior on opacity { Anim { + type: Anim.DefaultEffects } } @@ -384,84 +424,80 @@ CustomRect { } } - RowLayout { + ButtonRow { id: actions - anchors.horizontalCenter: parent.horizontalCenter + anchors.left: body.left + anchors.right: body.right anchors.top: body.bottom - anchors.topMargin: 7 + anchors.topMargin: Appearance.spacing.small opacity: root.expanded ? 1 : 0 - spacing: 10 + spacing: Appearance.spacing.extraSmall Behavior on opacity { Anim { + type: Anim.DefaultEffects } } - Action { - modelData: QtObject { - readonly property string text: qsTr("Close") + IconButton { + fillWidth: root.modelData.actions.length === 0 + icon: "close" + inactiveColor: root.modelData.urgency === NotificationUrgency.Critical ? DynamicColors.palette.m3secondary : DynamicColors.layer(DynamicColors.palette.m3surfaceContainerHighest, 2) + inactiveOnColor: root.modelData.urgency === NotificationUrgency.Critical ? DynamicColors.palette.m3onSecondary : DynamicColors.palette.m3onSurfaceVariant + isRound: true + padding: Appearance.padding.extraSmall + shapeMorph: true - function invoke(): void { - root.modelData.close(); - } - } + onClicked: root.modelData.close() } Repeater { model: root.modelData.actions - delegate: Component { - Action { - } + TextButton { + required property var modelData + + fillWidth: true + inactiveColor: root.modelData.urgency === NotificationUrgency.Critical ? DynamicColors.palette.m3secondary : DynamicColors.layer(DynamicColors.palette.m3surfaceContainerHighest, 2) + inactiveOnColor: root.modelData.urgency === NotificationUrgency.Critical ? DynamicColors.palette.m3onSecondary : DynamicColors.palette.m3onSurfaceVariant + isRound: true + label.anchors.centerIn: undefined + label.anchors.left: left + label.anchors.margins: Appearance.padding.normal + label.anchors.right: right + label.anchors.verticalCenter: verticalCenter + label.elide: Text.ElideRight + label.horizontalAlignment: Text.AlignHCenter + shapeMorph: true + text: modelData.text + + onClicked: modelData.invoke() + } + } + + IconButton { + fillWidth: root.modelData.actions.length === 0 + icon: copyTimer.running ? "inventory" : "content_copy" + inactiveColor: root.modelData.urgency === NotificationUrgency.Critical ? DynamicColors.palette.m3secondary : DynamicColors.layer(DynamicColors.palette.m3surfaceContainerHighest, 2) + inactiveOnColor: root.modelData.urgency === NotificationUrgency.Critical ? DynamicColors.palette.m3onSecondary : DynamicColors.palette.m3onSurfaceVariant + isRound: true + label.animate: true + padding: Appearance.padding.extraSmall + shapeMorph: true + + onClicked: { + Quickshell.clipboardText = root.modelData.body; + copyTimer.restart(); + } + + Timer { + id: copyTimer + + interval: 3000 } } } } } - - component Action: CustomRect { - id: action - - required property var modelData - - Layout.preferredHeight: actionText.height + 4 * 2 - Layout.preferredWidth: actionText.width + 8 * 2 - color: root.modelData.urgency === NotificationUrgency.Critical ? DynamicColors.palette.m3secondary : DynamicColors.layer(DynamicColors.palette.m3surfaceContainerHigh, 2) - implicitHeight: actionText.height + 4 * 2 - implicitWidth: actionText.width + 8 * 2 - radius: Appearance.rounding.full - - StateLayer { - color: root.modelData.urgency === NotificationUrgency.Critical ? DynamicColors.palette.m3onSecondary : DynamicColors.palette.m3onSurface - enabled: root.expanded - radius: Appearance.rounding.full - - onClicked: { - action.modelData.invoke(); - } - } - - CustomText { - id: actionText - - anchors.centerIn: parent - color: root.modelData.urgency === NotificationUrgency.Critical ? DynamicColors.palette.m3onSecondary : DynamicColors.palette.m3onSurfaceVariant - font.pointSize: 10 - text: actionTextMetrics.elidedText - } - - TextMetrics { - id: actionTextMetrics - - elide: Text.ElideRight - elideWidth: { - const numActions = root.modelData.actions.length + 1; - return (inner.width - actions.spacing * (numActions - 1)) / numActions - 8 * 2; - } - font.family: actionText.font.family - font.pointSize: actionText.font.pointSize - text: action.modelData.text - } - } } diff --git a/Plugins/ZShell/Components/CMakeLists.txt b/Plugins/ZShell/Components/CMakeLists.txt index 16d7aa3..3edbffc 100644 --- a/Plugins/ZShell/Components/CMakeLists.txt +++ b/Plugins/ZShell/Components/CMakeLists.txt @@ -3,6 +3,7 @@ qml_module(ZShell-components SOURCES lazylistview.hpp lazylistview.cpp wavyline.hpp wavyline.cpp + buttonrow.hpp buttonrow.cpp LIBRARIES Qt::Quick ) diff --git a/Plugins/ZShell/Components/buttonrow.cpp b/Plugins/ZShell/Components/buttonrow.cpp new file mode 100644 index 0000000..a56ad33 --- /dev/null +++ b/Plugins/ZShell/Components/buttonrow.cpp @@ -0,0 +1,132 @@ +#include "buttonrow.hpp" + +namespace ZShell::components { + +ButtonRow::ButtonRow(QQuickItem* parent) + : QQuickItem(parent) + , m_dirty(false) + , m_spacing(0.0) { + setFlag(QQuickItem::ItemHasContents, true); + QObject::connect(this, &ButtonRow::widthChanged, this, &ButtonRow::invalidate); +} + +qreal ButtonRow::spacing() const { + return m_spacing; +} + +void ButtonRow::setSpacing(qreal spacing) { + if (qFuzzyCompare(m_spacing + 1.0, spacing + 1.0)) + return; + + m_spacing = spacing; + emit spacingChanged(); + + invalidate(); +} + +void ButtonRow::itemChange(QQuickItem::ItemChange change, const QQuickItem::ItemChangeData& data) { + if (change == QQuickItem::ItemChildAddedChange) { + auto* const child = data.item; + QObject::connect(child, &QQuickItem::implicitWidthChanged, this, &ButtonRow::invalidate); + QObject::connect(child, &QQuickItem::implicitHeightChanged, this, &ButtonRow::invalidate); + QObject::connect(child, &QQuickItem::visibleChanged, this, &ButtonRow::invalidate); + + const auto* childMeta = child->metaObject(); + const auto morphSignalIdx = childMeta->indexOfSignal("shapeMorphExpansionChanged()"); + if (morphSignalIdx != -1) + QObject::connect(child, childMeta->method(morphSignalIdx), this, + metaObject()->method(metaObject()->indexOfSlot("invalidate()"))); + + invalidate(); + } else if (change == QQuickItem::ItemChildRemovedChange) { + QObject::disconnect(data.item, nullptr, this, nullptr); + invalidate(); + } + + QQuickItem::itemChange(change, data); +} + +void ButtonRow::updatePolish() { + if (m_dirty) { + relayout(); + m_dirty = false; + } +} + +void ButtonRow::invalidate() { + m_dirty = true; + polish(); +} + +void ButtonRow::relayout() { + const auto allChildren = childItems(); + + QList validChildren; + for (auto* const child : allChildren) { + if (child->isVisible() && !child->inherits("QQuickRepeater")) + validChildren.append(child); + } + const auto nChildren = validChildren.size(); + const auto totalSpacing = static_cast(nChildren - 1) * m_spacing; + + qreal reservedWidth = 0; + qreal unreservedWidth = 0; + int fillWidthCount = 0; + qreal maxHeight = 0; + for (auto* const child : validChildren) { + maxHeight = qMax(maxHeight, child->implicitHeight()); + + const auto prop = child->property("fillWidth"); + if (!prop.isValid()) + continue; + + if (prop.toBool()) { + fillWidthCount++; + unreservedWidth += child->implicitWidth(); + } else { + reservedWidth += child->implicitWidth(); + } + } + + if (fillWidthCount == 0) + fillWidthCount = 1; // Avoid divide by 0 + + const auto widthPerItem = (width() - totalSpacing - reservedWidth) / static_cast(fillWidthCount); + + QList baseWidths; + baseWidths.reserve(nChildren); + for (auto* const child : validChildren) + baseWidths.append(child->property("fillWidth").toBool() ? widthPerItem : child->implicitWidth()); + + qreal accX = 0; + for (int i = 0; i < nChildren; ++i) { + auto* const child = validChildren[i]; + + // clang-format off + auto prevExtraWidth = i > 0 ? getMorphExpansion(validChildren[i - 1]) : 0.0; + auto nextExtraWidth = i < nChildren - 1 ? getMorphExpansion(validChildren[i + 1]) : 0.0; + // clang-format on + + // Items at edges push by full amount, items in middle push by half + if (i > 1) + prevExtraWidth /= 2; + if (i < nChildren - 2) + nextExtraWidth /= 2; + + child->setWidth(baseWidths[i] + getMorphExpansion(child) - prevExtraWidth - nextExtraWidth); + child->setHeight(maxHeight); + + child->setX(accX); + child->setY(0); + accX += child->width() + m_spacing; + } + + setImplicitWidth(reservedWidth + unreservedWidth + totalSpacing); + setImplicitHeight(maxHeight); +} + +qreal ButtonRow::getMorphExpansion(const QQuickItem* item) { + return item->property("shapeMorphExpansion").toReal(); +} + +} // namespace ZShell::components diff --git a/Plugins/ZShell/Components/buttonrow.hpp b/Plugins/ZShell/Components/buttonrow.hpp new file mode 100644 index 0000000..4c5f13d --- /dev/null +++ b/Plugins/ZShell/Components/buttonrow.hpp @@ -0,0 +1,37 @@ +#pragma once + +#include + +namespace ZShell::components { + +class ButtonRow : public QQuickItem { +Q_OBJECT +QML_ELEMENT + +Q_PROPERTY(qreal spacing READ spacing WRITE setSpacing NOTIFY spacingChanged) + +public: +explicit ButtonRow(QQuickItem* parent = nullptr); + +[[nodiscard]] qreal spacing() const; +void setSpacing(qreal spacing); + +signals: +void spacingChanged(); + +protected: +void itemChange(QQuickItem::ItemChange change, const QQuickItem::ItemChangeData& data) override; +void updatePolish() override; + +private slots: +void invalidate(); + +private: +void relayout(); +static qreal getMorphExpansion(const QQuickItem* item); + +bool m_dirty; +qreal m_spacing; +}; + +} // namespace ZShell::components