From a1aabd02f5098fd5bc86ba4d22b5f85a0088ad0c Mon Sep 17 00:00:00 2001 From: LemmyCook Date: Thu, 18 Sep 2025 10:10:40 -0400 Subject: [PATCH] Toast: reworked the display and logic to make it more robust. + some bluetooth logic debouncing to avoid extra toast when adapter comes back to life after suspend. --- Modules/Toast/SimpleToast.qml | 37 +++++------ Modules/Toast/ToastScreen.qml | 105 +++++++++++++++++++----------- Services/BluetoothService.qml | 51 +++++++++++---- Services/IdleInhibitorService.qml | 4 +- 4 files changed, 123 insertions(+), 74 deletions(-) diff --git a/Modules/Toast/SimpleToast.qml b/Modules/Toast/SimpleToast.qml index 90a8416ab..2d8bdf70e 100644 --- a/Modules/Toast/SimpleToast.qml +++ b/Modules/Toast/SimpleToast.qml @@ -15,14 +15,12 @@ Rectangle { signal hidden - width: Math.min(500 * scaling, parent.width * 0.8) - height: Math.max(60 * scaling, contentLayout.implicitHeight + Style.marginL * 2 * scaling) + width: parent.width + height: Math.round(contentLayout.implicitHeight + Style.marginL * 2 * scaling) radius: Style.radiusL * scaling visible: false opacity: 0 scale: initialScale - - // Clean surface background like NToast color: Color.mSurface // Colored border based on type @@ -67,6 +65,12 @@ Rectangle { } } + // Cleanup on destruction + Component.onDestruction: { + hideTimer.stop() + hideAnimation.stop() + } + RowLayout { id: contentLayout anchors.fill: parent @@ -125,24 +129,9 @@ Rectangle { visible: text.length > 0 } } - - // Close button - NIconButton { - id: closeButton - icon: "close" - - colorBg: Color.mSurfaceVariant - colorFg: Color.mOnSurface - colorBorder: Color.transparent - colorBorderHover: Color.mOutline - - baseSize: Style.baseWidgetSize * 0.8 - Layout.alignment: Qt.AlignTop - - onClicked: root.hide() - } } + // Click anywhere dismiss the toast MouseArea { anchors.fill: parent acceptedButtons: Qt.LeftButton @@ -151,6 +140,10 @@ Rectangle { } function show(msg, desc, msgType, msgDuration) { + // Stop all timers first + hideTimer.stop() + hideAnimation.stop() + message = msg description = desc || "" type = msgType || "notice" @@ -167,10 +160,12 @@ Rectangle { hideTimer.stop() opacity = 0 scale = initialScale - hideAnimation.start() + hideAnimation.restart() } function hideImmediately() { + hideTimer.stop() + hideAnimation.stop() opacity = 0 scale = initialScale root.visible = false diff --git a/Modules/Toast/ToastScreen.qml b/Modules/Toast/ToastScreen.qml index a2e0eca13..4f3ba1cb3 100644 --- a/Modules/Toast/ToastScreen.qml +++ b/Modules/Toast/ToastScreen.qml @@ -6,12 +6,12 @@ import qs.Commons import qs.Services import qs.Widgets -Loader { +Item { id: root required property ShellScreen screen required property real scaling - required property bool active + property bool active: false // Local queue for this screen only property var messageQueue: [] @@ -44,16 +44,26 @@ Loader { } } + // Clear queue on component destruction to prevent orphaned toasts + Component.onDestruction: { + messageQueue = [] + isShowingToast = false + hideTimer.stop() + quickSwitchTimer.stop() + } + function enqueueToast(toastData) { + Logger.log("ToastScreen", "Queuing:", toastData.message, toastData.description, toastData.type) + if (replaceOnNew && isShowingToast) { // Cancel current toast and clear queue for latest toast messageQueue = [] // Clear existing queue messageQueue.push(toastData) // Hide current toast immediately - if (item) { + if (windowLoader.item) { hideTimer.stop() - item.hideToast() // Need to add this method to PanelWindow + windowLoader.item.hideToast() } // Process new toast after a brief delay @@ -73,20 +83,30 @@ Loader { } function processQueue() { - if (!active || !item || messageQueue.length === 0 || isShowingToast) { + if (!active || messageQueue.length === 0 || isShowingToast) { return } var data = messageQueue.shift() isShowingToast = true - // Show the toast - item.showToast(data.message, data.description, data.type, data.duration) + // Activate the loader and show toast + windowLoader.active = true + // Need a small delay to ensure the window is created + Qt.callLater(function () { + if (windowLoader.item) { + windowLoader.item.showToast(data.message, data.description, data.type, data.duration) + } + }) } function onToastHidden() { isShowingToast = false - // Small delay before next toast + + // Deactivate the loader to completely remove the window + windowLoader.active = false + + // Small delay before processing next toast hideTimer.restart() } @@ -96,48 +116,55 @@ Loader { onTriggered: root.processQueue() } - sourceComponent: PanelWindow { - id: panel + // The loader that creates/destroys the PanelWindow as needed + Loader { + id: windowLoader + active: false // Only active when showing a toast - screen: root.screen + sourceComponent: PanelWindow { + id: panel - anchors { - top: true - } + property alias toastItem: toastItem - implicitWidth: 500 * root.scaling - implicitHeight: Math.round(toastItem.visible ? toastItem.height + Style.marginM * root.scaling : 1) + screen: root.screen - // Set margins based on bar position - margins.top: { - switch (Settings.data.bar.position) { - case "top": - return (Style.barHeight + Style.marginS) * scaling + (Settings.data.bar.floating ? Settings.data.bar.marginVertical * Style.marginXL * scaling : 0) - default: - return Style.marginL * scaling + anchors { + top: true } - } - color: Color.transparent + implicitWidth: 420 * root.scaling + implicitHeight: toastItem.height - WlrLayershell.layer: WlrLayer.Overlay - WlrLayershell.keyboardFocus: WlrKeyboardFocus.None - exclusionMode: PanelWindow.ExclusionMode.Ignore + // Set margins based on bar position + margins.top: { + switch (Settings.data.bar.position) { + case "top": + return (Style.barHeight + Style.marginS) * scaling + (Settings.data.bar.floating ? Settings.data.bar.marginVertical * Style.marginXL * scaling : 0) + default: + return Style.marginL * scaling + } + } - function showToast(message, description, type, duration) { - toastItem.show(message, description, type, duration) - } + color: Color.transparent - // Add method to immediately hide toast - function hideToast() { - toastItem.hideImmediately() - } + WlrLayershell.layer: WlrLayer.Overlay + WlrLayershell.keyboardFocus: WlrKeyboardFocus.None + exclusionMode: PanelWindow.ExclusionMode.Ignore - SimpleToast { - id: toastItem + function showToast(message, description, type, duration) { + toastItem.show(message, description, type, duration) + } - anchors.horizontalCenter: parent.horizontalCenter - onHidden: root.onToastHidden() + function hideToast() { + toastItem.hideImmediately() + } + + SimpleToast { + id: toastItem + + anchors.horizontalCenter: parent.horizontalCenter + onHidden: root.onToastHidden() + } } } } diff --git a/Services/BluetoothService.qml b/Services/BluetoothService.qml index 1f773eb35..c704f5e97 100644 --- a/Services/BluetoothService.qml +++ b/Services/BluetoothService.qml @@ -9,8 +9,7 @@ Singleton { id: root readonly property BluetoothAdapter adapter: Bluetooth.defaultAdapter - readonly property bool available: adapter !== null - readonly property bool enabled: (adapter && adapter.enabled) ?? false + readonly property bool available: (adapter !== null) readonly property bool discovering: (adapter && adapter.discovering) ?? false readonly property var devices: adapter ? adapter.devices : null readonly property var pairedDevices: { @@ -30,37 +29,64 @@ Singleton { }) } + property bool lastAdapterState: false + function init() { Logger.log("Bluetooth", "Service initialized") - delaySyncState.running = true + syncStateTimer.running = true } Timer { - id: delaySyncState + id: syncStateTimer interval: 1000 repeat: false onTriggered: { - Settings.data.network.bluetoothEnabled = adapter.enabled + lastAdapterState = Settings.data.network.bluetoothEnabled = adapter.enabled } } Timer { - id: delayDiscovery + id: discoveryTimer interval: 1000 repeat: false onTriggered: adapter.discovering = true } + Timer { + id: stateDebounceTimer + interval: 200 + repeat: false + onTriggered: { + if (!adapter) { + Logger.warn("Bluetooth", "State debouncer", "No adapter available") + return + } + if (lastAdapterState === adapter.enabled) { + return + } + lastAdapterState = adapter.enabled + if (adapter.enabled) { + ToastService.showNotice("Bluetooth", "Enabled") + } else { + ToastService.showNotice("Bluetooth", "Disabled") + } + } + } + Connections { target: adapter function onEnabledChanged() { + if (!adapter) { + Logger.warn("Bluetooth", "onEnabledChanged", "No adapter available") + return + } + + Logger.log("Bluetooth", "onEnableChanged", adapter.enabled) Settings.data.network.bluetoothEnabled = adapter.enabled + stateDebounceTimer.restart() if (adapter.enabled) { - ToastService.showNotice("Bluetooth", "Enabled") // Using a timer to give a little time so the adapter is really enabled - delayDiscovery.running = true - } else { - ToastService.showNotice("Bluetooth", "Disabled") + discoveryTimer.running = true } } } @@ -231,12 +257,13 @@ Singleton { device.forget() } - function setBluetoothEnabled(enabled) { + function setBluetoothEnabled(state) { if (!adapter) { Logger.warn("Bluetooth", "No adapter available") return } - adapter.enabled = enabled + Logger.log("Bluetooth", "SetBluetoothEnabled", state) + adapter.enabled = state } } diff --git a/Services/IdleInhibitorService.qml b/Services/IdleInhibitorService.qml index f0c842072..d6014d52b 100644 --- a/Services/IdleInhibitorService.qml +++ b/Services/IdleInhibitorService.qml @@ -163,13 +163,13 @@ Singleton { if (activeInhibitors.includes("manual")) { removeInhibitor("manual") Settings.data.ui.idleInhibitorEnabled = false - ToastService.showNotice("Keep Awake", "Disabled", false, 3000) + ToastService.showNotice("Keep Awake", "Disabled") Logger.log("IdleInhibitor", "Manual inhibition disabled and saved to settings") return false } else { addInhibitor("manual", "Manually activated by user") Settings.data.ui.idleInhibitorEnabled = true - ToastService.showNotice("Keep Awake", "Enabled", false, 3000) + ToastService.showNotice("Keep Awake", "Enabled") Logger.log("IdleInhibitor", "Manual inhibition enabled and saved to settings") return true }