diff --git a/Commons/IconsSets/TablerIcons.qml b/Commons/IconsSets/TablerIcons.qml index 823fe87..d787ddf 100644 --- a/Commons/IconsSets/TablerIcons.qml +++ b/Commons/IconsSets/TablerIcons.qml @@ -20,7 +20,8 @@ Singleton { "folder-open": "folder-open", "download": "download", "toast-notice": "circle-check", - "toast-warning": "exclamation-circle", + "toast-warning": "alert-circle", + "toast-error": "circle-x", "question-mark": "question-mark", "search": "search", "warning": "exclamation-circle", diff --git a/Modules/Toast/SimpleToast.qml b/Modules/Toast/SimpleToast.qml new file mode 100644 index 0000000..bc51858 --- /dev/null +++ b/Modules/Toast/SimpleToast.qml @@ -0,0 +1,179 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import qs.Commons +import qs.Widgets + +Rectangle { + id: root + + property string message: "" + property string description: "" + property string type: "notice" + property int duration: 3000 + readonly property real initialScale: 0.7 + + signal hidden + + width: Math.min(500 * scaling, parent.width * 0.8) + height: Math.max(60 * scaling, 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 + border.color: { + switch (type) { + case "warning": + return Color.mPrimary + case "error": + return Color.mError + default: + return Color.mOutline + } + } + border.width: Math.max(2, Style.borderM * scaling) + + Behavior on opacity { + NumberAnimation { + duration: Style.animationNormal + easing.type: Easing.OutCubic + } + } + + Behavior on scale { + NumberAnimation { + duration: Style.animationNormal + easing.type: Easing.OutCubic + } + } + + Timer { + id: hideTimer + interval: root.duration + onTriggered: root.hide() + } + + Timer { + id: hideAnimation + interval: Style.animationFast + onTriggered: { + root.visible = false + root.hidden() + } + } + + RowLayout { + id: contentLayout + anchors.fill: parent + anchors.margins: Style.marginL * scaling + spacing: Style.marginL * scaling + + // Icon + NIcon { + id: icon + icon: { + switch (type) { + case "warning": + return "toast-warning" + case "error": + return "toast-error" + default: + return "toast-notice" + } + } + color: { + switch (type) { + case "warning": + return Color.mPrimary + case "error": + return Color.mError + default: + return Color.mOnSurface + } + } + font.pointSize: Style.fontSizeXXL * 1.5 * scaling + Layout.alignment: Qt.AlignVCenter + } + + // Label and description + ColumnLayout { + spacing: Style.marginXXS * scaling + Layout.fillWidth: true + Layout.alignment: Qt.AlignVCenter + + NText { + Layout.fillWidth: true + text: root.message + color: Color.mOnSurface + font.pointSize: Style.fontSizeL * scaling + font.weight: Style.fontWeightBold + wrapMode: Text.WordWrap + visible: text.length > 0 + } + + NText { + Layout.fillWidth: true + text: root.description + color: Color.mOnSurface + font.pointSize: Style.fontSizeM * scaling + wrapMode: Text.WordWrap + visible: text.length > 0 + } + } + + // Close button + NIconButton { + id: closeButton + icon: "close" + + colorBg: Color.mSurfaceVariant + colorFg: Color.mOnSurface + colorBorder: Color.transparent + colorBorderHover: Color.mOutline + + sizeRatio: 0.8 + Layout.alignment: Qt.AlignTop + + onClicked: root.hide() + } + } + + MouseArea { + anchors.fill: parent + acceptedButtons: Qt.LeftButton + onClicked: root.hide() + cursorShape: Qt.PointingHandCursor + } + + function show(msg, desc, msgType, msgDuration) { + message = msg + description = desc || "" + type = msgType || "notice" + duration = msgDuration || 3000 + + visible = true + opacity = 1 + scale = 1.0 + + hideTimer.restart() + } + + function hide() { + hideTimer.stop() + opacity = 0 + scale = initialScale + hideAnimation.start() + } + + function hideImmediately() { + opacity = 0 + scale = initialScale + root.visible = false + root.hidden() + } +} diff --git a/Modules/Toast/ToastOverlay.qml b/Modules/Toast/ToastOverlay.qml index 05cf809..78cb670 100644 --- a/Modules/Toast/ToastOverlay.qml +++ b/Modules/Toast/ToastOverlay.qml @@ -9,105 +9,13 @@ import qs.Widgets Variants { model: Quickshell.screens - delegate: Loader { + delegate: ToastScreen { required property ShellScreen modelData - property real scaling: ScalingService.getScreenScale(modelData) - Connections { - target: ScalingService - function onScaleChanged(screenName, scale) { - if (screenName === modelData.name) { - scaling = scale - } - } - } + screen: modelData + scaling: ScalingService.getScreenScale(modelData) - // Only show on screens that have notifications enabled - active: Settings.isLoaded && modelData ? (Settings.data.notifications.monitors.includes(modelData.name) || (Settings.data.notifications.monitors.length === 0)) : false - - sourceComponent: PanelWindow { - id: root - - screen: modelData - - // Position at top of screen, always allow horizontal centering - anchors { - top: true - left: true - right: true - } - - // Set a width instead of anchoring left/right so we can click on the side of the toast - implicitWidth: 500 * scaling - - // Small height when hidden, appropriate height when visible - implicitHeight: Math.round(toast.visible ? toast.height + Style.marginM * scaling : 1) - - // 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 0 - } - } - - margins.bottom: { - switch (Settings.data.bar.position) { - case "bottom": - return (Style.barHeight + Style.marginS) * scaling + (Settings.data.bar.floating ? Settings.data.bar.marginVertical * Style.marginXL * scaling : 0) - default: - return 0 - } - } - - margins.right: { - switch (Settings.data.bar.position) { - case "left": - case "top": - case "bottom": - return Style.marginM * scaling - default: - return 0 - } - } - - margins.left: { - switch (Settings.data.bar.position) { - case "right": - return Style.marginM * scaling - default: - return 0 - } - } - - // Transparent background - color: Color.transparent - - // Overlay layer to appear above other panels - WlrLayershell.layer: WlrLayer.Overlay - WlrLayershell.keyboardFocus: WlrKeyboardFocus.None - exclusionMode: PanelWindow.ExclusionMode.Ignore - - NToast { - id: toast - screen: modelData - - // Simple positioning - margins already account for bar - targetY: Style.marginS * scaling - - // Hidden position - always start from above the screen - hiddenY: -toast.height - 20 - - Component.onCompleted: { - // Register this toast with the service - ToastService.allToasts.push(toast) - - // Connect dismissal signal - toast.dismissed.connect(ToastService.onToastDismissed) - } - } - } + // Only activate on enabled screens + active: Settings.isLoaded && modelData && (Settings.data.notifications.monitors.includes(modelData.name) || Settings.data.notifications.monitors.length === 0) } } diff --git a/Modules/Toast/ToastScreen.qml b/Modules/Toast/ToastScreen.qml new file mode 100644 index 0000000..1d665e1 --- /dev/null +++ b/Modules/Toast/ToastScreen.qml @@ -0,0 +1,145 @@ +import QtQuick +import QtQuick.Controls +import Quickshell +import Quickshell.Wayland +import qs.Commons +import qs.Services +import qs.Widgets + +Loader { + id: root + + required property ShellScreen screen + required property real scaling + required property bool active + + // Local queue for this screen only + property var messageQueue: [] + property bool isShowingToast: false + + // If true, immediately show new toasts + property bool replaceOnNew: true + + Connections { + target: ScalingService + function onScaleChanged(screenName, scale) { + if (screenName === root.screen.name) { + root.scaling = scale + } + } + } + + Connections { + target: ToastService + enabled: root.active + + function onNotify(message, description, type, duration) { + root.enqueueToast({ + "message": message, + "description": description, + "type": type, + "duration": duration, + "timestamp": Date.now() + }) + } + } + + function enqueueToast(toastData) { + 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) { + hideTimer.stop() + item.hideToast() // Need to add this method to PanelWindow + } + + // Process new toast after a brief delay + isShowingToast = false + quickSwitchTimer.restart() + } else { + // Original behavior - queue the toast + messageQueue.push(toastData) + processQueue() + } + } + + Timer { + id: quickSwitchTimer + interval: 50 // Brief delay for smooth transition + onTriggered: root.processQueue() + } + + function processQueue() { + if (!active || !item || messageQueue.length === 0 || isShowingToast) { + return + } + + var data = messageQueue.shift() + isShowingToast = true + + // Show the toast + item.showToast(data.message, data.description, data.type, data.duration) + } + + function onToastHidden() { + isShowingToast = false + // Small delay before next toast + hideTimer.restart() + } + + Timer { + id: hideTimer + interval: 200 + onTriggered: root.processQueue() + } + + sourceComponent: PanelWindow { + id: panel + + screen: root.screen + + anchors { + top: true + left: true + right: true + } + + implicitWidth: 500 * root.scaling + implicitHeight: Math.round(toastItem.visible ? toastItem.height + Style.marginM * root.scaling : 1) + + // 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 + } + } + + color: Color.transparent + + WlrLayershell.layer: WlrLayer.Overlay + WlrLayershell.keyboardFocus: WlrKeyboardFocus.None + exclusionMode: PanelWindow.ExclusionMode.Ignore + + function showToast(message, description, type, duration) { + toastItem.show(message, description, type, duration) + } + + // Add method to immediately hide toast + function hideToast() { + toastItem.hideImmediately() + } + + SimpleToast { + id: toastItem + + anchors.horizontalCenter: parent.horizontalCenter + onHidden: root.onToastHidden() + } + } +} \ No newline at end of file diff --git a/Services/ToastService.qml b/Services/ToastService.qml index ee9fb24..df4cc25 100644 --- a/Services/ToastService.qml +++ b/Services/ToastService.qml @@ -2,247 +2,23 @@ pragma Singleton import QtQuick import Quickshell -import Quickshell.Io -import qs.Commons Singleton { id: root - // Queue of pending toast messages - property var messageQueue: [] - property bool isShowingToast: false + // Simple signal-based notification system + signal notify(string message, string description, string type, int duration) - // Reference to all toast instances (set by ToastOverlay) - property var allToasts: [] - - // Properties for command checking - property var commandCheckCallback: null - property string commandCheckSuccessMessage: "" - property string commandCheckFailMessage: "" - - // Properties for command running - property var commandRunCallback: null - property string commandRunSuccessMessage: "" - property string commandRunFailMessage: "" - - // Properties for delayed toast - property string delayedToastMessage: "" - property string delayedToastType: "notice" - - // Process for command checking - Process { - id: commandCheckProcess - command: ["which", "test"] - onExited: function (exitCode) { - if (exitCode === 0) { - showNotice(commandCheckSuccessMessage) - if (commandCheckCallback) - commandCheckCallback() - } else { - showWarning(commandCheckFailMessage) - } - } - stdout: StdioCollector {} - stderr: StdioCollector {} + // Convenience methods + function showNotice(message, description = "", duration = 3000) { + notify(message, description, "notice", duration) } - // Process for command running - Process { - id: commandRunProcess - command: ["echo", "test"] - onExited: function (exitCode) { - if (exitCode === 0) { - showNotice(commandRunSuccessMessage) - if (commandRunCallback) - commandRunCallback() - } else { - showWarning(commandRunFailMessage) - } - } - stdout: StdioCollector {} - stderr: StdioCollector {} + function showWarning(message, description = "", duration = 4000) { + notify(message, description, "warning", duration) } - // Timer for delayed toast - Timer { - id: delayedToastTimer - interval: 1000 - repeat: false - onTriggered: { - showToast(delayedToastMessage, delayedToastType) - } - } - - // Methods to show different types of messages - function showNotice(label, description = "", persistent = false, duration = 3000) { - showToast(label, description, "notice", persistent, duration) - } - - function showWarning(label, description = "", persistent = false, duration = 4000) { - showToast(label, description, "warning", persistent, duration) - } - - // Utility function to check if a command exists and show appropriate toast - function checkCommandAndToast(command, successMessage, failMessage, onSuccess = null) { - // Store callback for use in the process - commandCheckCallback = onSuccess - commandCheckSuccessMessage = successMessage - commandCheckFailMessage = failMessage - - // Start the command check process - commandCheckProcess.command = ["which", command] - commandCheckProcess.running = true - } - - // 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) { - // Store callback for use in the process - commandRunCallback = onSuccess - commandRunSuccessMessage = successMessage - commandRunFailMessage = failMessage - - // Start the command run process - commandRunProcess.command = [command].concat(args || []) - commandRunProcess.running = true - } - - // 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) { - delayedToastMessage = message - delayedToastType = type - delayedToastTimer.interval = delayMs - delayedToastTimer.restart() - } - - // Generic method to show a toast - function showToast(label, description = "", type = "notice", persistent = false, duration = 3000) { - var toastData = { - "label": label, - "description": description, - "type": type, - "persistent": persistent, - "duration": duration, - "timestamp": Date.now() - } - - // If there's already a toast showing, instantly start hide animation and show new one - if (isShowingToast) { - // Instantly start hide animation of current toast - for (var i = 0; i < allToasts.length; i++) { - allToasts[i].hide() - } - // Clear the queue since we're showing the new toast immediately - messageQueue = [] - } - - // Add to queue - messageQueue.push(toastData) - - // Always process immediately for instant display - processQueue() - } - - // Process the message queue - function processQueue() { - if (messageQueue.length === 0 || allToasts.length === 0) { - // Added this so we don't accidentally get duplicate toasts - // if it causes issues, remove it and we'll find a different solution - if (allToasts.length === 0 && messageQueue.length > 0) { - messageQueue = [] - } - isShowingToast = false - return - } - - var toastData = messageQueue.shift() - isShowingToast = true - - // Configure and show toast on all screens - for (var i = 0; i < allToasts.length; i++) { - var toast = allToasts[i] - toast.label = toastData.label - toast.description = toastData.description - toast.type = toastData.type - toast.persistent = toastData.persistent - toast.duration = toastData.duration - - toast.show() - } - } - - // Called when a toast is dismissed - function onToastDismissed() { - // Check if all toasts are dismissed - var allDismissed = true - for (var i = 0; i < allToasts.length; i++) { - if (allToasts[i].visible) { - allDismissed = false - break - } - } - - if (allDismissed) { - 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 (isShowingToast) { - for (var i = 0; i < allToasts.length; i++) { - allToasts[i].hide() - } - } + function showError(message, description = "", duration = 5000) { + notify(message, description, "error", duration) } } diff --git a/Widgets/NSlider.qml b/Widgets/NSlider.qml index 44d8f2c..b48eb17 100644 --- a/Widgets/NSlider.qml +++ b/Widgets/NSlider.qml @@ -81,7 +81,6 @@ Slider { duration: Style.animationFast } } - } } } diff --git a/Widgets/NToast.qml b/Widgets/NToast.qml deleted file mode 100644 index 3c559cb..0000000 --- a/Widgets/NToast.qml +++ /dev/null @@ -1,193 +0,0 @@ -import QtQuick -import QtQuick.Controls -import QtQuick.Layouts -import QtQuick.Effects -import Quickshell -import qs.Commons -import qs.Widgets -import qs.Services - -Item { - id: root - - property string label: "" - property string description: "" - 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 - - required property ShellScreen screen - property real scaling: 1.0 - - // 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() { - // NToast updates its scaling when showing. - scaling = ScalingService.getScreenScale(screen) - - // Stop any running animations and reset state - showAnimation.stop() - hideAnimation.stop() - autoHideTimer.stop() - - // Ensure we start from the hidden position - y = hiddenY - visible = true - - // Start the show animation - 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 { - 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) - - RowLayout { - id: contentLayout - anchors.fill: parent - anchors.margins: Style.marginL * scaling - spacing: Style.marginL * scaling - - // Icon - NIcon { - id: icon - icon: (root.type == "warning") ? "toast-warning" : "toast-notice" - 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 - } - - // Label and description - ColumnLayout { - spacing: Style.marginXXS * scaling - Layout.fillWidth: true - Layout.alignment: Qt.AlignVCenter - - NText { - Layout.fillWidth: true - text: root.label - color: Color.mOnSurface - font.pointSize: Style.fontSizeM * scaling - font.weight: Style.fontWeightBold - wrapMode: Text.WordWrap - visible: text.length > 0 - } - - NText { - Layout.fillWidth: true - text: root.description - color: Color.mOnSurface - font.pointSize: Style.fontSizeM * scaling - wrapMode: Text.WordWrap - visible: text.length > 0 - } - } - - // Close button (only if persistent or manual dismiss needed) - NIconButton { - icon: "close" - visible: root.persistent || root.duration === 0 - - colorBg: Color.mSurfaceVariant - colorFg: Color.mOnSurface - colorBorder: Color.transparent - colorBorderHover: Color.mOutline - - sizeRatio: 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 - } -} diff --git a/Widgets/NToggle.qml b/Widgets/NToggle.qml index f283e18..36e1678 100644 --- a/Widgets/NToggle.qml +++ b/Widgets/NToggle.qml @@ -32,7 +32,7 @@ RowLayout { radius: height * 0.5 color: root.checked ? Color.mPrimary : Color.mSurface border.color: Color.mOutline - border.width: Math.max(1, Style.borderS* scaling) + border.width: Math.max(1, Style.borderS * scaling) Behavior on color { ColorAnimation {