From 1d860da42e4df9c74acfbe3d55c8628bb23a8556 Mon Sep 17 00:00:00 2001 From: Ly-sec Date: Tue, 19 Aug 2025 14:14:00 +0200 Subject: [PATCH] Add ToastService, NToast etc --- Commons/Settings.qml | 2 +- Modules/Background/ScreenCorners.qml | 4 +- Modules/Bar/Bar.qml | 4 +- Modules/Bar/BluetoothMenu.qml | 8 +- Modules/Bar/Clock.qml | 2 +- Modules/Bar/Tray.qml | 2 +- Modules/Bar/WiFiMenu.qml | 8 +- Modules/SettingsPanel/Tabs/BarTab.qml | 4 +- Modules/SettingsPanel/Tabs/ColorSchemeTab.qml | 33 ++- Modules/SettingsPanel/Tabs/NetworkTab.qml | 10 + Modules/SettingsPanel/Tabs/WallpaperTab.qml | 31 ++- Modules/SidePanel/SidePanel.qml | 6 +- Modules/Toast/ToastManager.qml | 54 +++++ Services/ToastService.qml | 214 ++++++++++++++++++ Widgets/NIconButton.qml | 4 +- Widgets/NPill.qml | 4 +- Widgets/NToast.qml | 172 ++++++++++++++ shell.qml | 3 + 18 files changed, 534 insertions(+), 31 deletions(-) create mode 100644 Modules/Toast/ToastManager.qml create mode 100644 Services/ToastService.qml create mode 100644 Widgets/NToast.qml diff --git a/Commons/Settings.qml b/Commons/Settings.qml index 6302567..2f2c0eb 100644 --- a/Commons/Settings.qml +++ b/Commons/Settings.qml @@ -108,7 +108,7 @@ Singleton { property JsonObject bar bar: JsonObject { - property string barPosition: "top" // Possible values: "top", "bottom", "left", "right" + property string position: "top" // Possible values: "top", "bottom", "left", "right" property bool showActiveWindow: true property bool showSystemInfo: false property bool showMedia: false diff --git a/Modules/Background/ScreenCorners.qml b/Modules/Background/ScreenCorners.qml index f8a58d7..ba04642 100644 --- a/Modules/Background/ScreenCorners.qml +++ b/Modules/Background/ScreenCorners.qml @@ -44,10 +44,10 @@ NLoader { margins { top: (Settings.data.bar.monitors.includes(modelData.name) - || (Settings.data.bar.monitors.length === 0)) && Settings.data.bar.barPosition === "top" + || (Settings.data.bar.monitors.length === 0)) && Settings.data.bar.position === "top" ? Math.floor(Style.barHeight * scaling) : 0 bottom: (Settings.data.bar.monitors.includes(modelData.name) - || (Settings.data.bar.monitors.length === 0)) && Settings.data.bar.barPosition === "bottom" + || (Settings.data.bar.monitors.length === 0)) && Settings.data.bar.position === "bottom" ? Math.floor(Style.barHeight * scaling) : 0 } diff --git a/Modules/Bar/Bar.qml b/Modules/Bar/Bar.qml index 55e8933..e2e4beb 100644 --- a/Modules/Bar/Bar.qml +++ b/Modules/Bar/Bar.qml @@ -25,8 +25,8 @@ Variants { || (Settings.data.bar.monitors.length === 0)) : false anchors { - top: Settings.data.bar.barPosition === "top" - bottom: Settings.data.bar.barPosition === "bottom" + top: Settings.data.bar.position === "top" + bottom: Settings.data.bar.position === "bottom" left: true right: true } diff --git a/Modules/Bar/BluetoothMenu.qml b/Modules/Bar/BluetoothMenu.qml index 6a7d40e..7198f40 100644 --- a/Modules/Bar/BluetoothMenu.qml +++ b/Modules/Bar/BluetoothMenu.qml @@ -76,10 +76,10 @@ NLoader { anchors { right: parent.right rightMargin: Style.marginXS * scaling - top: Settings.data.bar.barPosition === "top" ? parent.top : undefined - bottom: Settings.data.bar.barPosition === "bottom" ? parent.bottom : undefined - topMargin: Settings.data.bar.barPosition === "top" ? Style.marginXS * scaling : undefined - bottomMargin: Settings.data.bar.barPosition === "bottom" ? Style.barHeight * scaling + Style.marginXS * scaling : undefined + top: Settings.data.bar.position === "top" ? parent.top : undefined + bottom: Settings.data.bar.position === "bottom" ? parent.bottom : undefined + topMargin: Settings.data.bar.position === "top" ? Style.marginXS * scaling : undefined + bottomMargin: Settings.data.bar.position === "bottom" ? Style.barHeight * scaling + Style.marginXS * scaling : undefined } // Animation properties diff --git a/Modules/Bar/Clock.qml b/Modules/Bar/Clock.qml index a7cf60f..10cd1d6 100644 --- a/Modules/Bar/Clock.qml +++ b/Modules/Bar/Clock.qml @@ -20,7 +20,7 @@ Rectangle { id: tooltip text: Time.dateString target: clock - positionAbove: Settings.data.bar.barPosition === "bottom" + positionAbove: Settings.data.bar.position === "bottom" } onEntered: { diff --git a/Modules/Bar/Tray.qml b/Modules/Bar/Tray.qml index 4476f8d..ed79672 100644 --- a/Modules/Bar/Tray.qml +++ b/Modules/Bar/Tray.qml @@ -119,7 +119,7 @@ Rectangle { id: trayTooltip target: trayIcon text: modelData.tooltipTitle || modelData.name || modelData.id || "Tray Item" - positionAbove: Settings.data.bar.barPosition === "bottom" + positionAbove: Settings.data.bar.position === "bottom" } } } diff --git a/Modules/Bar/WiFiMenu.qml b/Modules/Bar/WiFiMenu.qml index bd35c23..94c8998 100644 --- a/Modules/Bar/WiFiMenu.qml +++ b/Modules/Bar/WiFiMenu.qml @@ -91,10 +91,10 @@ NLoader { anchors { right: parent.right rightMargin: Style.marginXS * scaling - top: Settings.data.bar.barPosition === "top" ? parent.top : undefined - bottom: Settings.data.bar.barPosition === "bottom" ? parent.bottom : undefined - topMargin: Settings.data.bar.barPosition === "top" ? Style.marginXS * scaling : undefined - bottomMargin: Settings.data.bar.barPosition === "bottom" ? Style.barHeight * scaling + Style.marginXS * scaling : undefined + top: Settings.data.bar.position === "top" ? parent.top : undefined + bottom: Settings.data.bar.position === "bottom" ? parent.bottom : undefined + topMargin: Settings.data.bar.position === "top" ? Style.marginXS * scaling : undefined + bottomMargin: Settings.data.bar.position === "bottom" ? Style.barHeight * scaling + Style.marginXS * scaling : undefined } // Animation properties diff --git a/Modules/SettingsPanel/Tabs/BarTab.qml b/Modules/SettingsPanel/Tabs/BarTab.qml index f8b7481..f3f26da 100644 --- a/Modules/SettingsPanel/Tabs/BarTab.qml +++ b/Modules/SettingsPanel/Tabs/BarTab.qml @@ -71,9 +71,9 @@ ColumnLayout { name: "Bottom" } } - currentKey: Settings.data.bar.barPosition + currentKey: Settings.data.bar.position onSelected: function (key) { - Settings.data.bar.barPosition = key + Settings.data.bar.position = key } } } diff --git a/Modules/SettingsPanel/Tabs/ColorSchemeTab.qml b/Modules/SettingsPanel/Tabs/ColorSchemeTab.qml index c417acb..d8ceb66 100644 --- a/Modules/SettingsPanel/Tabs/ColorSchemeTab.qml +++ b/Modules/SettingsPanel/Tabs/ColorSchemeTab.qml @@ -1,10 +1,10 @@ import QtQuick import QtQuick.Controls import QtQuick.Layouts +import Quickshell.Io import qs.Commons import qs.Services import qs.Widgets -import Quickshell.Io ColumnLayout { id: root @@ -147,9 +147,12 @@ ColumnLayout { description: "Automatically generate colors based on your active wallpaper." checked: Settings.data.colorSchemes.useWallpaperColors onToggled: checked => { - Settings.data.colorSchemes.useWallpaperColors = checked - if (Settings.data.colorSchemes.useWallpaperColors) { - ColorSchemeService.changedWallpaper() + if (checked) { + // Check if matugen is installed + matugenCheck.running = true + } else { + Settings.data.colorSchemes.useWallpaperColors = false + ToastService.showNotice("Matugen:\nDisabled") } } } @@ -337,4 +340,26 @@ ColumnLayout { } } } + + // Simple process to check if matugen exists + Process { + id: matugenCheck + command: ["which", "matugen"] + running: false + + onExited: function(exitCode) { + if (exitCode === 0) { + // Matugen exists, enable it + Settings.data.colorSchemes.useWallpaperColors = true + ColorSchemeService.changedWallpaper() + ToastService.showNotice("Matugen:\nEnabled!") + } else { + // Matugen not found + ToastService.showWarning("Matugen:\nNot installed!") + } + } + + stdout: StdioCollector {} + stderr: StdioCollector {} + } } diff --git a/Modules/SettingsPanel/Tabs/NetworkTab.qml b/Modules/SettingsPanel/Tabs/NetworkTab.qml index 698a263..783556b 100644 --- a/Modules/SettingsPanel/Tabs/NetworkTab.qml +++ b/Modules/SettingsPanel/Tabs/NetworkTab.qml @@ -49,6 +49,11 @@ ColumnLayout { onToggled: checked => { Settings.data.network.wifiEnabled = checked NetworkService.setWifiEnabled(checked) + if (checked) { + ToastService.showNotice("WiFi:\nEnabled") + } else { + ToastService.showNotice("WiFi:\nDisabled") + } } } @@ -59,6 +64,11 @@ ColumnLayout { onToggled: checked => { Settings.data.network.bluetoothEnabled = checked BluetoothService.setBluetoothEnabled(checked) + if (checked) { + ToastService.showNotice("Bluetooth:\nEnabled") + } else { + ToastService.showNotice("Bluetooth:\nDisabled") + } } } } diff --git a/Modules/SettingsPanel/Tabs/WallpaperTab.qml b/Modules/SettingsPanel/Tabs/WallpaperTab.qml index aed421c..7fc4787 100644 --- a/Modules/SettingsPanel/Tabs/WallpaperTab.qml +++ b/Modules/SettingsPanel/Tabs/WallpaperTab.qml @@ -1,6 +1,7 @@ import QtQuick import QtQuick.Controls import QtQuick.Layouts +import Quickshell.Io import qs.Commons import qs.Services import qs.Widgets @@ -147,7 +148,13 @@ ColumnLayout { description: "Use SWWW daemon for advanced wallpaper management." checked: Settings.data.wallpaper.swww.enabled onToggled: checked => { - Settings.data.wallpaper.swww.enabled = checked + if (checked) { + // Check if swww is installed + swwwCheck.running = true + } else { + Settings.data.wallpaper.swww.enabled = false + ToastService.showNotice("SWWW:\nDisabled") + } } } @@ -335,4 +342,26 @@ ColumnLayout { } } } + + // Process to check if swww is installed + Process { + id: swwwCheck + command: ["which", "swww"] + running: false + + onExited: function(exitCode) { + if (exitCode === 0) { + // SWWW exists, enable it + Settings.data.wallpaper.swww.enabled = true + WallpaperService.startSWWWDaemon() + ToastService.showNotice("SWWW:\nEnabled!") + } else { + // SWWW not found + ToastService.showWarning("SWWW:\nNot installed!") + } + } + + stdout: StdioCollector {} + stderr: StdioCollector {} + } } diff --git a/Modules/SidePanel/SidePanel.qml b/Modules/SidePanel/SidePanel.qml index f5e9f48..04bfa3f 100644 --- a/Modules/SidePanel/SidePanel.qml +++ b/Modules/SidePanel/SidePanel.qml @@ -91,10 +91,10 @@ NLoader { // Height scales to content plus vertical padding height: content.implicitHeight + innerMargin * 2 // Place the panel relative to the bar based on its position - y: Settings.data.bar.barPosition === "top" ? Style.marginS * scaling : undefined + y: Settings.data.bar.position === "top" ? Style.marginS * scaling : undefined anchors { - bottom: Settings.data.bar.barPosition === "bottom" ? parent.bottom : undefined - bottomMargin: Settings.data.bar.barPosition === "bottom" ? Style.barHeight * scaling + Style.marginS * scaling : undefined + bottom: Settings.data.bar.position === "bottom" ? parent.bottom : undefined + bottomMargin: Settings.data.bar.position === "bottom" ? Style.barHeight * scaling + Style.marginS * scaling : undefined } // Center horizontally under the anchorX, clamped to the screen bounds x: Math.max(Style.marginS * scaling, Math.min(parent.width - width - Style.marginS * scaling, diff --git a/Modules/Toast/ToastManager.qml b/Modules/Toast/ToastManager.qml new file mode 100644 index 0000000..74d494a --- /dev/null +++ b/Modules/Toast/ToastManager.qml @@ -0,0 +1,54 @@ +import QtQuick +import QtQuick.Controls +import Quickshell +import Quickshell.Wayland +import qs.Commons +import qs.Services +import qs.Widgets + +// ToastManager creates toast overlays on each screen +Variants { + model: Quickshell.screens + + delegate: PanelWindow { + id: root + + required property ShellScreen modelData + readonly property real scaling: ScalingService.scale(screen) + screen: modelData + + // Position at top, centered horizontally + anchors { + top: true + left: true + right: true + } + + // Small height when hidden, appropriate height when visible + implicitHeight: toast.visible ? toast.height + Style.barHeight * scaling + Style.marginS * scaling : 1 + + // Transparent background + color: Color.transparent + + // High layer to appear above other panels + //WlrLayershell.layer: WlrLayer.Overlay + WlrLayershell.keyboardFocus: WlrKeyboardFocus.None + exclusionMode: PanelWindow.ExclusionMode.Ignore + + NToast { + id: toast + scaling: root.scaling + + // Position just below where the bar would be + targetY: Style.barHeight * scaling + Style.marginS * scaling + + Component.onCompleted: { + // Register this toast with the service + ToastService.currentToast = toast + + // Connect dismissal signal + toast.dismissed.connect(ToastService.onToastDismissed) + } + } + } +} \ No newline at end of file diff --git a/Services/ToastService.qml b/Services/ToastService.qml new file mode 100644 index 0000000..674ce8e --- /dev/null +++ b/Services/ToastService.qml @@ -0,0 +1,214 @@ +pragma Singleton + +import QtQuick +import Quickshell.Io +import qs.Commons + +QtObject { + id: root + + // Queue of pending toast messages + property var messageQueue: [] + property bool isShowingToast: false + + // Reference to the current toast instance (set by ToastManager) + property var currentToast: null + + // Methods to show different types of messages + function showNotice(message, persistent = false, duration = 3000) { + showToast(message, "notice", persistent, duration) + } + + function showWarning(message, persistent = false, duration = 4000) { + showToast(message, "warning", persistent, duration) + } + + // Utility function to check if a command exists and show appropriate toast + function checkCommandAndToast(command, successMessage, failMessage, onSuccess = null) { + var checkProcess = Qt.createQmlObject(` + import QtQuick + import Quickshell.Io + Process { + id: checkProc + command: ["which", "${command}"] + running: true + + property var onSuccessCallback: null + property bool hasFinished: false + + onExited: { + if (!hasFinished) { + hasFinished = true + if (exitCode === 0) { + ToastService.showNotice("${successMessage}") + if (onSuccessCallback) onSuccessCallback() + } else { + ToastService.showWarning("${failMessage}") + } + checkProc.destroy() + } + } + + // Fallback collectors to prevent issues + stdout: StdioCollector {} + stderr: StdioCollector {} + } + `, root) + + checkProcess.onSuccessCallback = onSuccess + } + + // Simple function to show a random toast (useful for testing or fun messages) + function showRandomToast() { + var messages = [ + { type: "notice", text: "Everything is working smoothly!" }, + { type: "notice", text: "Noctalia is looking great today!" }, + { type: "notice", text: "Your desktop setup is amazing!" }, + { type: "warning", text: "Don't forget to take a break!" }, + { type: "notice", text: "Configuration saved successfully!" }, + { type: "warning", text: "Remember to backup your settings!" } + ] + + var randomMessage = messages[Math.floor(Math.random() * messages.length)] + showToast(randomMessage.text, randomMessage.type) + } + + // Convenience function for quick notifications + function quickNotice(message) { + showNotice(message, false, 2000) // Short duration + } + + function quickWarning(message) { + showWarning(message, false, 3000) // Medium duration + } + + // Generic command runner with toast feedback + function runCommandWithToast(command, args, successMessage, failMessage, onSuccess = null) { + var fullCommand = [command].concat(args || []) + var runProcess = Qt.createQmlObject(` + import QtQuick + import Quickshell.Io + Process { + id: runProc + command: ${JSON.stringify(fullCommand)} + running: true + + property var onSuccessCallback: null + property bool hasFinished: false + + onExited: { + if (!hasFinished) { + hasFinished = true + if (exitCode === 0) { + ToastService.showNotice("${successMessage}") + if (onSuccessCallback) onSuccessCallback() + } else { + ToastService.showWarning("${failMessage}") + } + runProc.destroy() + } + } + + stdout: StdioCollector {} + stderr: StdioCollector {} + } + `, root) + + runProcess.onSuccessCallback = onSuccess + } + + // Check if a file/directory exists + function checkPathAndToast(path, successMessage, failMessage, onSuccess = null) { + runCommandWithToast("test", ["-e", path], successMessage, failMessage, onSuccess) + } + + // Show toast after a delay (useful for delayed feedback) + function delayedToast(message, type = "notice", delayMs = 1000) { + var timer = Qt.createQmlObject(` + import QtQuick + Timer { + interval: ${delayMs} + repeat: false + running: true + onTriggered: { + ToastService.showToast("${message}", "${type}") + destroy() + } + } + `, root) + } + + // Generic method to show a toast + function showToast(message, type = "notice", persistent = false, duration = 3000) { + var toastData = { + message: message, + type: type, + persistent: persistent, + duration: duration, + timestamp: Date.now() + } + + + + // Add to queue + messageQueue.push(toastData) + + // Process queue if not currently showing a toast + if (!isShowingToast) { + processQueue() + } + } + + // Process the message queue + function processQueue() { + if (messageQueue.length === 0 || !currentToast) { + isShowingToast = false + return + } + + if (isShowingToast) { + // Wait for current toast to finish + return + } + + var toastData = messageQueue.shift() + isShowingToast = true + + + + // Configure and show toast + currentToast.message = toastData.message + currentToast.type = toastData.type + currentToast.persistent = toastData.persistent + currentToast.duration = toastData.duration + currentToast.show() + } + + // Called when a toast is dismissed + function onToastDismissed() { + + isShowingToast = false + + // Small delay before showing next toast + Qt.callLater(function() { + processQueue() + }) + } + + // Clear all pending messages + function clearQueue() { + + messageQueue = [] + } + + // Hide current toast + function hideCurrentToast() { + if (currentToast && isShowingToast) { + currentToast.hide() + } + } + + Component.onCompleted: { + + } +} \ No newline at end of file diff --git a/Widgets/NIconButton.qml b/Widgets/NIconButton.qml index 351126c..a502c7d 100644 --- a/Widgets/NIconButton.qml +++ b/Widgets/NIconButton.qml @@ -51,9 +51,7 @@ Rectangle { NTooltip { id: tooltip target: root - positionAbove: Settings.data.bar.barPosition === "bottom" - positionLeft: Settings.data.bar.barPosition === "right" - positionRight: Settings.data.bar.barPosition === "left" + positionAbove: Settings.data.bar.position === "bottom" text: root.tooltipText } diff --git a/Widgets/NPill.qml b/Widgets/NPill.qml index a05b222..62bc36b 100644 --- a/Widgets/NPill.qml +++ b/Widgets/NPill.qml @@ -168,9 +168,7 @@ Item { NTooltip { id: tooltip - positionAbove: Settings.data.bar.barPosition === "bottom" - positionLeft: Settings.data.bar.barPosition === "right" - positionRight: Settings.data.bar.barPosition === "left" + positionAbove: Settings.data.bar.position === "bottom" target: pill delay: Style.tooltipDelayLong text: root.tooltipText diff --git a/Widgets/NToast.qml b/Widgets/NToast.qml new file mode 100644 index 0000000..2a394a0 --- /dev/null +++ b/Widgets/NToast.qml @@ -0,0 +1,172 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import QtQuick.Effects +import qs.Commons +import qs.Widgets + +Item { + id: root + + property string message: "" + property string type: "notice" // "notice", "warning" + property int duration: 5000 // Auto-hide after 5 seconds, 0 = no auto-hide + property bool persistent: false // If true, requires manual dismiss + + property real scaling: 1.0 // Will be set by parent + + // Animation properties + property real targetY: 0 + property real hiddenY: -height - 20 + + signal dismissed() + + width: Math.min(500 * scaling, parent.width * 0.8) + height: Math.max(60 * scaling, contentLayout.implicitHeight + Style.marginL * 2 * scaling) + + // Position at top center of parent + anchors.horizontalCenter: parent.horizontalCenter + y: hiddenY + z: 1000 // High z-index to appear above everything + + function show() { + visible = true + showAnimation.start() + if (duration > 0 && !persistent) { + autoHideTimer.start() + } + } + + function hide() { + hideAnimation.start() + } + + // Auto-hide timer + Timer { + id: autoHideTimer + interval: root.duration + onTriggered: hide() + } + + // Show animation + PropertyAnimation { + id: showAnimation + target: root + property: "y" + to: targetY + duration: Style.animationNormal + easing.type: Easing.OutCubic + } + + // Hide animation + PropertyAnimation { + id: hideAnimation + target: root + property: "y" + to: hiddenY + duration: Style.animationNormal + easing.type: Easing.InCubic + onFinished: { + root.visible = false + root.dismissed() + } + } + + // Main toast container + Rectangle { + id: container + anchors.fill: parent + radius: Style.radiusL * scaling + + // Clean surface background + color: Color.mSurface + + // Simple colored border all around + border.color: { + switch (root.type) { + case "warning": return Color.mError + case "notice": return Color.mPrimary + default: return Color.mOutline + } + } + border.width: Math.max(2, Style.borderM * scaling) + + // Drop shadow effect + layer.enabled: true + layer.effect: MultiEffect { + shadowEnabled: true + shadowColor: Qt.rgba(0, 0, 0, 0.3) + shadowBlur: 20 * scaling + shadowVerticalOffset: 4 * scaling + } + + RowLayout { + id: contentLayout + anchors.fill: parent + anchors.margins: Style.marginM * scaling + spacing: Style.marginS * scaling + + // Icon + NIcon { + id: icon + text: { + switch (root.type) { + case "warning": return "warning" + case "notice": return "info" + default: return "info" + } + } + + color: { + switch (root.type) { + case "warning": return Color.mError + case "notice": return Color.mPrimary + default: return Color.mPrimary + } + } + + font.pointSize: Style.fontSizeXXL * 1.5 * scaling // 150% size to cover two lines + Layout.alignment: Qt.AlignVCenter + } + + // Message text + NText { + id: messageText + text: root.message + color: Color.mOnSurface + font.pointSize: Style.fontSize * scaling + wrapMode: Text.WordWrap + Layout.fillWidth: true + Layout.alignment: Qt.AlignVCenter + } + + // Close button (only if persistent or manual dismiss needed) + NIconButton { + id: closeButton + icon: "close" + visible: root.persistent || root.duration === 0 + + color: Color.mOnSurface + + fontPointSize: Style.fontSize * scaling + sizeMultiplier: 0.8 + Layout.alignment: Qt.AlignTop + + onClicked: hide() + } + } + + // Click to dismiss (if not persistent) + MouseArea { + anchors.fill: parent + enabled: !root.persistent + onClicked: hide() + cursorShape: Qt.PointingHandCursor + } + } + + // Initial state + Component.onCompleted: { + visible = false + } +} \ No newline at end of file diff --git a/shell.qml b/shell.qml index 6621d0e..de85aee 100644 --- a/shell.qml +++ b/shell.qml @@ -24,6 +24,7 @@ import qs.Modules.LockScreen import qs.Modules.Notification import qs.Modules.SettingsPanel import qs.Modules.SidePanel +import qs.Modules.Toast import qs.Services import qs.Widgets @@ -68,6 +69,8 @@ ShellRoot { id: lockScreen } + ToastManager {} + IPCManager {} Component.onCompleted: {