From 28f921fa029fb76ac14e866d5df6bdc15e191ddc Mon Sep 17 00:00:00 2001 From: bbedward Date: Wed, 21 Oct 2026 13:15:53 -0400 Subject: [PATCH] dash: hold panel height while dragging cards unless hung from a top bar lyrics: ignore floored position samples and shell refresh emits --- quickshell/Modules/DDash/DDashPopout.qml | 5 ++++- quickshell/Modules/DDash/DashMetrics.qml | 1 + quickshell/Modules/DDash/DashWidgetGrid.qml | 1 + quickshell/Modules/DDash/MediaPlayerTab.qml | 2 +- .../Modules/DDash/Overview/MediaOverviewCard.qml | 2 +- quickshell/Modules/DDash/OverviewTab.qml | 1 + quickshell/Modules/DDash/WeatherTab.qml | 1 + quickshell/Services/LyricsController.qml | 14 +++++++++++++- quickshell/Services/MprisController.qml | 11 ++++++++++- quickshell/Widgets/DSeekbar.qml | 2 +- 10 files changed, 34 insertions(+), 6 deletions(-) diff --git a/quickshell/Modules/DDash/DDashPopout.qml b/quickshell/Modules/DDash/DDashPopout.qml index 9d84b8547..81874bbe6 100644 --- a/quickshell/Modules/DDash/DDashPopout.qml +++ b/quickshell/Modules/DDash/DDashPopout.qml @@ -680,6 +680,8 @@ DPopout { readonly property bool ready: targetHeight >= 0 && settledHeight === targetHeight readonly property real availableHeight: root.screen ? root.screen.height - mainContainer.horizontalChromeHeight - DashMetrics.contentPadding * 2 - Theme.barHeight - Theme.spacingL * 2 : contentHeight readonly property int rowBudget: DashMetrics.rowCapFor(availableHeight) + // Growing a panel not hung from a top bar moves the grid under a held card, which retargets the drop. + readonly property bool heightHeld: (currentItem?.cardInteracting ?? false) && root.effectiveBarPosition % 4 !== SettingsData.Top x: (root.verticalNavigation && root.navigationEdge === SettingsData.Left ? root.navigationWidth : 0) - root.editGutter y: (!root.verticalNavigation && root.navigationEdge !== SettingsData.Bottom ? mainContainer.horizontalChromeHeight : 0) - root.editGutter @@ -691,12 +693,13 @@ DPopout { clip: contentHeight > height function updateContentHeight() { - if (targetHeight < 0) + if (targetHeight < 0 || heightHeld) return; settledHeight = targetHeight; } onTargetHeightChanged: Qt.callLater(updateContentHeight) + onHeightHeldChanged: Qt.callLater(updateContentHeight) Column { id: emptyContent diff --git a/quickshell/Modules/DDash/DashMetrics.qml b/quickshell/Modules/DDash/DashMetrics.qml index db8c85550..c94cde521 100644 --- a/quickshell/Modules/DDash/DashMetrics.qml +++ b/quickshell/Modules/DDash/DashMetrics.qml @@ -214,6 +214,7 @@ Singleton { readonly property int mediaLyricsRequestTimeout: 16000 readonly property int mediaLyricsLoadingDelay: 300 readonly property real mediaLyricsPositionTolerance: 0.05 + readonly property real mediaLyricsWholeSecondSlack: 0.005 readonly property int mediaLyricsPositionProbeInterval: 5000 readonly property real mediaLyricsPositionProbeDrift: 1.5 readonly property real mediaLyricsPositionProbeThreshold: 1 diff --git a/quickshell/Modules/DDash/DashWidgetGrid.qml b/quickshell/Modules/DDash/DashWidgetGrid.qml index 07fc21394..93d3bd4fa 100644 --- a/quickshell/Modules/DDash/DashWidgetGrid.qml +++ b/quickshell/Modules/DDash/DashWidgetGrid.qml @@ -25,6 +25,7 @@ Item { readonly property Item focusTarget: grid readonly property bool blocksTabNavigation: editMode || addMenu.open || optionsSheet.shown readonly property bool cardResizing: grid.sizePreview !== null + readonly property bool cardInteracting: grid.interacting implicitHeight: Math.max(DashMetrics.tabMinHeight, grid.implicitHeight) LayoutMirroring.enabled: I18n.isRtl diff --git a/quickshell/Modules/DDash/MediaPlayerTab.qml b/quickshell/Modules/DDash/MediaPlayerTab.qml index 2d1ce7ddf..c98f29a52 100644 --- a/quickshell/Modules/DDash/MediaPlayerTab.qml +++ b/quickshell/Modules/DDash/MediaPlayerTab.qml @@ -123,7 +123,7 @@ Item { interval: DashMetrics.mediaPositionPollInterval running: root.live && root.activePlayer?.playbackState === MprisPlaybackState.Playing && !root.isSeeking repeat: true - onTriggered: root.activePlayer?.positionChanged() + onTriggered: MprisController.refreshPosition(root.activePlayer) } function syncAudioRef(wanted) { diff --git a/quickshell/Modules/DDash/Overview/MediaOverviewCard.qml b/quickshell/Modules/DDash/Overview/MediaOverviewCard.qml index da4a66589..7b886db7e 100644 --- a/quickshell/Modules/DDash/Overview/MediaOverviewCard.qml +++ b/quickshell/Modules/DDash/Overview/MediaOverviewCard.qml @@ -53,7 +53,7 @@ Card { interval: DashMetrics.mediaPositionPollInterval running: root.live && root.visible && root.interactive && root.showSeekbar && root.playing && !root.isSeeking repeat: true - onTriggered: root.activePlayer?.positionSupported && root.activePlayer.positionChanged() + onTriggered: MprisController.refreshPosition(root.activePlayer) } Loader { diff --git a/quickshell/Modules/DDash/OverviewTab.qml b/quickshell/Modules/DDash/OverviewTab.qml index 66f069781..8f54ed466 100644 --- a/quickshell/Modules/DDash/OverviewTab.qml +++ b/quickshell/Modules/DDash/OverviewTab.qml @@ -26,6 +26,7 @@ FocusScope { readonly property var addable: grid.addableEntries readonly property int usedColumns: grid.usedColumns readonly property bool cardResizing: grid.sizePreview !== null + readonly property bool cardInteracting: grid.interacting readonly property bool blocksTabNavigation: grid.blocksTabNavigation || detailSheet.shown || editorSheet.shown || optionsSheet.shown || addMenu.open implicitWidth: DashMetrics.contentWidthFor(SettingsData.showWeekNumber) diff --git a/quickshell/Modules/DDash/WeatherTab.qml b/quickshell/Modules/DDash/WeatherTab.qml index 5f59ae3aa..634769bb8 100644 --- a/quickshell/Modules/DDash/WeatherTab.qml +++ b/quickshell/Modules/DDash/WeatherTab.qml @@ -36,6 +36,7 @@ Item { } ] readonly property bool blocksTabNavigation: widgets.blocksTabNavigation + readonly property bool cardInteracting: widgets.cardInteracting readonly property var forecastOptions: [DashRegistry.choice("forecast", I18n.tr("Forecast", "weather widget option label for forecast display type"), "chart", [ { "value": "chart", diff --git a/quickshell/Services/LyricsController.qml b/quickshell/Services/LyricsController.qml index b43b7952a..7860442c3 100644 --- a/quickshell/Services/LyricsController.qml +++ b/quickshell/Services/LyricsController.qml @@ -90,7 +90,11 @@ QtObject { if (enabled && (state === "none" || state === "error")) useEmbeddedText(state); } - onPositionChanged: scheduleAnchor() + onPositionChanged: { + if (MprisController.refreshingPosition || flooredEcho()) + return; + scheduleAnchor(); + } onRateChanged: updatePlaybackClock() onPlayingChanged: { probe = null; @@ -379,6 +383,7 @@ QtObject { if (!line || !Number.isFinite(line.t)) return false; player.position = Math.max(0.1, Math.min(line.t, duration * 0.99)); + reanchor(); return true; } @@ -434,6 +439,13 @@ QtObject { resync(); } + // pear-desktop floors Position to whole seconds on pause, resume and track events. + function flooredEcho() { + const lead = currentTime() - position; + const whole = Math.abs(position - Math.round(position)) < DashMetrics.mediaLyricsWholeSecondSlack; + return whole && lead >= 0 && lead < 1 + DashMetrics.mediaLyricsPositionTolerance; + } + function scheduleAnchor() { positionWall = Date.now(); if (enabled && synced && !stopped && !anchorUpdate.running) diff --git a/quickshell/Services/MprisController.qml b/quickshell/Services/MprisController.qml index faa9148bc..c5e6860a2 100644 --- a/quickshell/Services/MprisController.qml +++ b/quickshell/Services/MprisController.qml @@ -34,6 +34,15 @@ Singleton { property bool _mprisPublishDirty: false property int _mprisConnectionEpoch: 0 property int positionConsumers: 0 + property bool refreshingPosition: false + + function refreshPosition(player) { + if (!player?.positionSupported) + return; + refreshingPosition = true; + player.positionChanged(); + refreshingPosition = false; + } function addPositionRef() { positionConsumers++; @@ -398,7 +407,7 @@ Singleton { interval: 1000 running: root.positionConsumers > 0 && root.activePlayer?.playbackState === MprisPlaybackState.Playing repeat: true - onTriggered: root.activePlayer?.positionChanged() + onTriggered: root.refreshPosition(root.activePlayer) } function _hasMPRISSubscription(): bool { diff --git a/quickshell/Widgets/DSeekbar.qml b/quickshell/Widgets/DSeekbar.qml index 65517a173..1fb5c1240 100644 --- a/quickshell/Widgets/DSeekbar.qml +++ b/quickshell/Widgets/DSeekbar.qml @@ -227,7 +227,7 @@ Item { isPlaying: activePlayer && activePlayer.playbackState === MprisPlaybackState.Playing onFrameTicked: { if (!root.isSeeking) - activePlayer.positionChanged(); + MprisController.refreshPosition(activePlayer); } MouseArea {