diff --git a/Bar/Modules/Applauncher.qml b/Bar/Modules/Applauncher.qml index a02ee37..f98509c 100644 --- a/Bar/Modules/Applauncher.qml +++ b/Bar/Modules/Applauncher.qml @@ -296,9 +296,9 @@ PanelWithOverlay { const searchTerm = query.slice(5).trim(); clipboardHistory.forEach(function(clip, index) { - let searchContent = clip.type === 'image' ? - clip.mimeType : - clip.content || clip; // Support both new object format and old string format + let searchContent = clip.type === 'image' ? + clip.mimeType : + clip.content || clip; // Support both new object format and old string format if (!searchTerm || searchContent.toLowerCase().includes(searchTerm)) { let entry; diff --git a/Helpers/IPCHandlers.qml b/Helpers/IPCHandlers.qml index 3da1fca..aa8ef69 100644 --- a/Helpers/IPCHandlers.qml +++ b/Helpers/IPCHandlers.qml @@ -6,7 +6,7 @@ IpcHandler { property var appLauncherPanel property var lockScreen property IdleInhibitor idleInhibitor - property var notificationPopupVariants + property var notificationPopup target: "globalIPC" @@ -17,18 +17,11 @@ IpcHandler { function toggleNotificationPopup(): void { console.log("[IPC] NotificationPopup toggle() called") - - if (notificationPopupVariants) { - for (let i = 0; i < notificationPopupVariants.count; i++) { - let popup = notificationPopupVariants.objectAt(i); - if (popup) { - popup.togglePopup(); - } - } - } + // Use the global toggle function from the notification manager + notificationPopup.togglePopup(); } - + // Toggle Applauncher visibility function toggleLauncher(): void { if (!appLauncherPanel) { console.warn("AppLauncherIpcHandler: appLauncherPanel not set!"); @@ -42,7 +35,7 @@ IpcHandler { } } - + // Toggle LockScreen function toggleLock(): void { if (!lockScreen) { console.warn("LockScreenIpcHandler: lockScreen not set!"); @@ -51,4 +44,4 @@ IpcHandler { console.log("[IPC] LockScreen show() called"); lockScreen.locked = true; } -} +} \ No newline at end of file diff --git a/Settings/Settings.qml b/Settings/Settings.qml index 23bb5f2..0a09b94 100644 --- a/Settings/Settings.qml +++ b/Settings/Settings.qml @@ -81,7 +81,7 @@ Singleton { // Monitor/Display Settings property var barMonitors: [] // Array of monitor names to show the bar on property var dockMonitors: [] // Array of monitor names to show the dock on - property var notificationMonitors: [] // Array of monitor names to show notifications on + property var notificationMonitors: [] // Array of monitor names to show notifications on, "*" means all monitors } } @@ -90,5 +90,7 @@ Singleton { function onRandomWallpaperChanged() { WallpaperManager.toggleRandomWallpaper() } function onWallpaperIntervalChanged() { WallpaperManager.restartRandomWallpaperTimer() } function onWallpaperFolderChanged() { WallpaperManager.loadWallpapers() } + function onNotificationMonitorsChanged() { + } } } \ No newline at end of file diff --git a/Widgets/Notification/NotificationPopup.qml b/Widgets/Notification/NotificationPopup.qml index 5b4fd5d..afaddd7 100644 --- a/Widgets/Notification/NotificationPopup.qml +++ b/Widgets/Notification/NotificationPopup.qml @@ -4,313 +4,356 @@ import Quickshell import Quickshell.Widgets import qs.Settings -PanelWindow { - id: window - implicitWidth: 350 - implicitHeight: notificationColumn.implicitHeight - color: "transparent" - visible: notificationsVisible && notificationModel.count > 0 - screen: (typeof modelData !== 'undefined' ? modelData : Quickshell.primaryScreen) - focusable: false +// Main container that manages multiple notification popups for different monitors +Item { + id: notificationManager + anchors.fill: parent - property bool barVisible: true + // Get list of available monitors/screens + property var monitors: Quickshell.screens || [] + + // Global visibility state for all notification popups property bool notificationsVisible: true - - anchors.top: true - anchors.right: true - margins.top: 6 - margins.right: 6 - - ListModel { - id: notificationModel - } - - property int maxVisible: 5 - property int spacing: 5 - + function togglePopup(): void { - console.log("[NotificationPopup] Current state: " + notificationsVisible); + console.log("[NotificationManager] Current state: " + notificationsVisible); notificationsVisible = !notificationsVisible; - console.log("[NotificationPopup] New state: " + notificationsVisible); + console.log("[NotificationManager] New state: " + notificationsVisible); } - function addNotification(notification) { - notificationModel.insert(0, { - id: notification.id, - appName: notification.appName || "Notification", - summary: notification.summary || "", - body: notification.body || "", - urgency: notification.urgency || 0, - rawNotification: notification, - appeared: false, - dismissed: false - }); - - while (notificationModel.count > maxVisible) { - notificationModel.remove(notificationModel.count - 1); - } - } - - function dismissNotificationById(id) { - for (var i = 0; i < notificationModel.count; i++) { - if (notificationModel.get(i).id === id) { - dismissNotificationByIndex(i); - break; + // Create a notification popup for each monitor + Repeater { + model: notificationManager.monitors + delegate: Item { + id: delegateItem + + // Make addNotification accessible from the Item level + function addNotification(notification) { + if (panelWindow) { + panelWindow.addNotification(notification); + } } - } - } + + PanelWindow { + id: panelWindow + implicitWidth: 350 + implicitHeight: Math.max(notificationColumn.height, 0) + color: "transparent" + visible: notificationManager.notificationsVisible && notificationModel.count > 0 && shouldShowOnThisMonitor + screen: modelData + focusable: false - function dismissNotificationByIndex(index) { - if (index >= 0 && index < notificationModel.count) { - var notif = notificationModel.get(index); - if (!notif.dismissed) { - notificationModel.set(index, { - id: notif.id, - appName: notif.appName, - summary: notif.summary, - body: notif.body, - rawNotification: notif.rawNotification, - appeared: notif.appeared, - dismissed: true - }); - } - } - } + property bool barVisible: true + property bool notificationsVisible: notificationManager.notificationsVisible + + // Check if this monitor should show notifications - make it reactive to settings changes + property bool shouldShowOnThisMonitor: { + let notificationMonitors = Settings.settings.notificationMonitors || []; + let currentScreenName = modelData ? modelData.name : ""; + return notificationMonitors.includes("*") || + notificationMonitors.includes(currentScreenName); + } - Column { - id: notificationColumn - anchors.right: parent.right - spacing: window.spacing - width: parent.width - clip: false + // Watch for changes in notification monitors setting + Connections { + target: Settings.settings + function onNotificationMonitorsChanged() { + // Settings changed, visibility will update automatically + } + } - Repeater { - id: notificationRepeater - model: notificationModel + anchors.top: true + anchors.right: true + margins.top: 6 + margins.right: 6 - delegate: Rectangle { - id: notificationDelegate - width: parent.width - color: Theme.backgroundPrimary - radius: 20 - border.color: model.urgency == 2 ? Theme.warning : Theme.outline - border.width: 1 + ListModel { + id: notificationModel + } - property bool appeared: model.appeared - property bool dismissed: model.dismissed - property var rawNotification: model.rawNotification + property int maxVisible: 5 + property int spacing: 5 - x: appeared ? 0 : width - opacity: dismissed ? 0 : 1 - height: dismissed ? 0 : contentRow.height + 20 + function addNotification(notification) { + notificationModel.insert(0, { + id: notification.id, + appName: notification.appName || "Notification", + summary: notification.summary || "", + body: notification.body || "", + urgency: notification.urgency || 0, + rawNotification: notification, + appeared: false, + dismissed: false + }); - Row { - id: contentRow - anchors.centerIn: parent - spacing: 10 - width: parent.width - 20 + while (notificationModel.count > maxVisible) { + notificationModel.remove(notificationModel.count - 1); + } + } - // Circular Icon container with border - Rectangle { - id: iconBackground - width: 36 - height: 36 - radius: width / 2 - color: Theme.accentPrimary - anchors.verticalCenter: parent.verticalCenter - border.color: Qt.darker(Theme.accentPrimary, 1.2) - border.width: 1.5 - - // Priority order for notification icons: image > appIcon > icon - property var iconSources: [rawNotification?.image || "", rawNotification?.appIcon || "", rawNotification?.icon || ""] - - // Load notification icon with fallback handling - IconImage { - id: iconImage - anchors.fill: parent - anchors.margins: 4 - asynchronous: true - backer.fillMode: Image.PreserveAspectFit - source: { - // Try each icon source in priority order - for (var i = 0; i < iconBackground.iconSources.length; i++) { - var icon = iconBackground.iconSources[i]; - if (!icon) - continue; - - // Handle special path format from some notifications - if (icon.includes("?path=")) { - const [name, path] = icon.split("?path="); - const fileName = name.substring(name.lastIndexOf("/") + 1); - return `file://${path}/${fileName}`; - } - - // Handle absolute file paths - if (icon.startsWith('/')) { - return "file://" + icon; - } - - return icon; - } - return ""; - } - visible: status === Image.Ready && source.toString() !== "" + function dismissNotificationById(id) { + for (var i = 0; i < notificationModel.count; i++) { + if (notificationModel.get(i).id === id) { + dismissNotificationByIndex(i); + break; } + } + } - // Fallback: show first letter of app name when no icon available - Text { - anchors.centerIn: parent - visible: !iconImage.visible - text: model.appName ? model.appName.charAt(0).toUpperCase() : "?" - font.family: Theme.fontFamily - font.pixelSize: Theme.fontSizeBody - font.bold: true + function dismissNotificationByIndex(index) { + if (index >= 0 && index < notificationModel.count) { + var notif = notificationModel.get(index); + if (!notif.dismissed) { + notificationModel.set(index, { + id: notif.id, + appName: notif.appName, + summary: notif.summary, + body: notif.body, + rawNotification: notif.rawNotification, + appeared: notif.appeared, + dismissed: true + }); + } + } + } + + Column { + id: notificationColumn + anchors.right: parent.right + spacing: window.spacing + width: parent.width + clip: false + + Repeater { + id: notificationRepeater + model: notificationModel + + delegate: Rectangle { + id: notificationDelegate + width: parent.width color: Theme.backgroundPrimary - } - } + radius: 20 + border.color: model.urgency == 2 ? Theme.warning : Theme.outline + border.width: 1 - Column { - width: contentRow.width - iconBackground.width - 10 - spacing: 5 + property bool appeared: model.appeared + property bool dismissed: model.dismissed + property var rawNotification: model.rawNotification - Text { - text: model.appName - width: parent.width - color: Theme.textPrimary - font.family: Theme.fontFamily - font.bold: true - font.pixelSize: Theme.fontSizeSmall - elide: Text.ElideRight - } - Text { - text: model.summary - width: parent.width - color: "#eeeeee" - font.family: Theme.fontFamily - font.pixelSize: Theme.fontSizeSmall - wrapMode: Text.Wrap - visible: text !== "" - } - Text { - text: model.body - width: parent.width - color: "#cccccc" - font.family: Theme.fontFamily - font.pixelSize: Theme.fontSizeCaption - wrapMode: Text.Wrap - visible: text !== "" - } - } - } + x: appeared ? 0 : width + opacity: dismissed ? 0 : 1 + height: dismissed ? 0 : contentRow.height + 20 - Timer { - interval: 4000 - running: !dismissed - repeat: false - onTriggered: { - dismissAnimation.start(); - if (rawNotification) - rawNotification.expire(); - } - } + Row { + id: contentRow + anchors.centerIn: parent + spacing: 10 + width: parent.width - 20 - MouseArea { - anchors.fill: parent - onClicked: { - dismissAnimation.start(); - if (rawNotification) - rawNotification.dismiss(); - } - } + // Circular Icon container with border + Rectangle { + id: iconBackground + width: 36 + height: 36 + radius: width / 2 + color: Theme.accentPrimary + anchors.verticalCenter: parent.verticalCenter + border.color: Qt.darker(Theme.accentPrimary, 1.2) + border.width: 1.5 - ParallelAnimation { - id: dismissAnimation - NumberAnimation { - target: notificationDelegate - property: "opacity" - to: 0 - duration: 150 - } - NumberAnimation { - target: notificationDelegate - property: "height" - to: 0 - duration: 150 - } - NumberAnimation { - target: notificationDelegate - property: "x" - to: width - duration: 150 - easing.type: Easing.InQuad - } - onFinished: { - for (let i = 0; i < notificationModel.count; i++) { - if (notificationModel.get(i).id === notificationDelegate.id) { - notificationModel.remove(i); - break; + // Priority order for notification icons: image > appIcon > icon + property var iconSources: [rawNotification?.image || "", rawNotification?.appIcon || "", rawNotification?.icon || ""] + + // Load notification icon with fallback handling + IconImage { + id: iconImage + anchors.fill: parent + anchors.margins: 4 + asynchronous: true + backer.fillMode: Image.PreserveAspectFit + source: { + // Try each icon source in priority order + for (var i = 0; i < iconBackground.iconSources.length; i++) { + var icon = iconBackground.iconSources[i]; + if (!icon) + continue; + + // Handle special path format from some notifications + if (icon.includes("?path=")) { + const [name, path] = icon.split("?path="); + const fileName = name.substring(name.lastIndexOf("/") + 1); + return `file://${path}/${fileName}`; + } + + // Handle absolute file paths + if (icon.startsWith('/')) { + return "file://" + icon; + } + + return icon; + } + return ""; + } + visible: status === Image.Ready && source.toString() !== "" + } + + // Fallback: show first letter of app name when no icon available + Text { + anchors.centerIn: parent + visible: !iconImage.visible + text: model.appName ? model.appName.charAt(0).toUpperCase() : "?" + font.family: Theme.fontFamily + font.pixelSize: Theme.fontSizeBody + font.bold: true + color: Theme.backgroundPrimary + } + } + + Column { + width: contentRow.width - iconBackground.width - 10 + spacing: 5 + + Text { + text: model.appName + width: parent.width + color: Theme.textPrimary + font.family: Theme.fontFamily + font.bold: true + font.pixelSize: Theme.fontSizeSmall + elide: Text.ElideRight + } + Text { + text: model.summary + width: parent.width + color: "#eeeeee" + font.family: Theme.fontFamily + font.pixelSize: Theme.fontSizeSmall + wrapMode: Text.Wrap + visible: text !== "" + } + Text { + text: model.body + width: parent.width + color: "#cccccc" + font.family: Theme.fontFamily + font.pixelSize: Theme.fontSizeCaption + wrapMode: Text.Wrap + visible: text !== "" + } + } + } + + Timer { + interval: 4000 + running: !dismissed + repeat: false + onTriggered: { + dismissAnimation.start(); + if (rawNotification) + rawNotification.expire(); + } + } + + MouseArea { + anchors.fill: parent + onClicked: { + dismissAnimation.start(); + if (rawNotification) + rawNotification.dismiss(); + } + } + + ParallelAnimation { + id: dismissAnimation + NumberAnimation { + target: notificationDelegate + property: "opacity" + to: 0 + duration: 150 + } + NumberAnimation { + target: notificationDelegate + property: "height" + to: 0 + duration: 150 + } + NumberAnimation { + target: notificationDelegate + property: "x" + to: width + duration: 150 + easing.type: Easing.InQuad + } + onFinished: { + for (let i = 0; i < notificationModel.count; i++) { + if (notificationModel.get(i).id === notificationDelegate.id) { + notificationModel.remove(i); + break; + } + } + } + } + + ParallelAnimation { + id: appearAnimation + NumberAnimation { + target: notificationDelegate + property: "opacity" + to: 1 + duration: 150 + } + NumberAnimation { + target: notificationDelegate + property: "height" + to: contentRow.height + 20 + duration: 150 + } + NumberAnimation { + target: notificationDelegate + property: "x" + to: 0 + duration: 150 + easing.type: Easing.OutQuad + } + } + + Component.onCompleted: { + if (!appeared) { + opacity = 0; + height = 0; + x = width; + appearAnimation.start(); + for (let i = 0; i < notificationModel.count; i++) { + if (notificationModel.get(i).id === notificationDelegate.id) { + var oldItem = notificationModel.get(i); + notificationModel.set(i, { + id: oldItem.id, + appName: oldItem.appName, + summary: oldItem.summary, + body: oldItem.body, + rawNotification: oldItem.rawNotification, + appeared: true, + read: oldItem.read, + dismissed: oldItem.dismissed + }); + break; + } + } + } } } } } - ParallelAnimation { - id: appearAnimation - NumberAnimation { - target: notificationDelegate - property: "opacity" - to: 1 - duration: 150 - } - NumberAnimation { - target: notificationDelegate - property: "height" - to: contentRow.height + 20 - duration: 150 - } - NumberAnimation { - target: notificationDelegate - property: "x" - to: 0 - duration: 150 - easing.type: Easing.OutQuad - } - } - - Component.onCompleted: { - if (!appeared) { - opacity = 0; - height = 0; - x = width; - appearAnimation.start(); - for (let i = 0; i < notificationModel.count; i++) { - if (notificationModel.get(i).id === notificationDelegate.id) { - var oldItem = notificationModel.get(i); - notificationModel.set(i, { - id: oldItem.id, - appName: oldItem.appName, - summary: oldItem.summary, - body: oldItem.body, - rawNotification: oldItem.rawNotification, - appeared: true, - read: oldItem.read, - dismissed: oldItem.dismissed - }); - break; - } + Connections { + target: Quickshell + function onScreensChanged() { + if (panelWindow.screen) { + x = panelWindow.screen.width - panelWindow.width - 20; } } } } } } - - Connections { - target: Quickshell - function onScreensChanged() { - if (window.screen) { - x = window.screen.width - width - 20; - } - } - } } diff --git a/Widgets/SettingsWindow/Tabs/About.qml b/Widgets/SettingsWindow/Tabs/About.qml index 3ee1942..14d3bc0 100644 --- a/Widgets/SettingsWindow/Tabs/About.qml +++ b/Widgets/SettingsWindow/Tabs/About.qml @@ -255,7 +255,7 @@ Item { font.pixelSize: 14 color: Theme.textSecondary Layout.alignment: Qt.AlignCenter - Layout.topMargin: 16 + Layout.topMargin: 24 } diff --git a/Widgets/SettingsWindow/Tabs/Display.qml b/Widgets/SettingsWindow/Tabs/Display.qml index ee4a159..bf450a3 100644 --- a/Widgets/SettingsWindow/Tabs/Display.qml +++ b/Widgets/SettingsWindow/Tabs/Display.qml @@ -13,6 +13,17 @@ ColumnLayout { // Get list of available monitors/screens property var monitors: Quickshell.screens || [] + + // Sorted monitors by name + property var sortedMonitors: { + let sorted = [...monitors]; + sorted.sort((a, b) => { + let nameA = a.name || "Unknown"; + let nameB = b.name || "Unknown"; + return nameA.localeCompare(nameB); + }); + return sorted; + } Item { Layout.fillWidth: true @@ -68,7 +79,7 @@ ColumnLayout { spacing: 8 Repeater { - model: root.monitors + model: root.sortedMonitors delegate: Rectangle { id: barCheckbox property bool isChecked: false @@ -171,7 +182,7 @@ ColumnLayout { spacing: 8 Repeater { - model: root.monitors + model: root.sortedMonitors delegate: Rectangle { id: dockCheckbox property bool isChecked: false @@ -277,7 +288,7 @@ ColumnLayout { spacing: 8 Repeater { - model: root.monitors + model: root.sortedMonitors delegate: Rectangle { id: notificationCheckbox property bool isChecked: false diff --git a/Widgets/SettingsWindow/Tabs/General.qml b/Widgets/SettingsWindow/Tabs/General.qml index 4d0ffda..0f3d5b0 100644 --- a/Widgets/SettingsWindow/Tabs/General.qml +++ b/Widgets/SettingsWindow/Tabs/General.qml @@ -107,15 +107,11 @@ ColumnLayout { } } - Item { - Layout.fillWidth: true - Layout.preferredHeight: 16 - } - ColumnLayout { spacing: 4 Layout.fillWidth: true + Layout.topMargin: 58 Text { text: "User Interface" diff --git a/Widgets/SettingsWindow/Tabs/Network.qml b/Widgets/SettingsWindow/Tabs/Network.qml index 3f79e32..0223f56 100644 --- a/Widgets/SettingsWindow/Tabs/Network.qml +++ b/Widgets/SettingsWindow/Tabs/Network.qml @@ -101,7 +101,7 @@ ColumnLayout { ColumnLayout { spacing: 16 Layout.fillWidth: true - Layout.topMargin: 16 + Layout.topMargin: 58 Text { text: "Bluetooth" diff --git a/Widgets/SettingsWindow/Tabs/TimeWeather.qml b/Widgets/SettingsWindow/Tabs/TimeWeather.qml index 7ee1c75..72aa261 100644 --- a/Widgets/SettingsWindow/Tabs/TimeWeather.qml +++ b/Widgets/SettingsWindow/Tabs/TimeWeather.qml @@ -172,7 +172,7 @@ ColumnLayout { ColumnLayout { spacing: 4 Layout.fillWidth: true - Layout.topMargin: 16 + Layout.topMargin: 58 Text { text: "Weather" diff --git a/shell.qml b/shell.qml index 5bd4e33..a06df9c 100644 --- a/shell.qml +++ b/shell.qml @@ -22,17 +22,18 @@ Scope { property var notificationHistoryWin: notificationHistoryWin property bool pendingReload: false - // Round volume to nearest 5% increment for consistent control + // Helper function to round value to nearest step function roundToStep(value, step) { return Math.round(value / step) * step; } - // Current audio volume (0-100), synced with system + // Volume property reflecting current audio volume in 0-100 + // Will be kept in sync dynamically below property int volume: (defaultAudioSink && defaultAudioSink.audio && !defaultAudioSink.audio.muted) ? Math.round(defaultAudioSink.audio.volume * 100) : 0 - // Update volume with 5-step increments and apply to audio sink + // Function to update volume with clamping, stepping, and applying to audio sink function updateVolume(vol) { var clamped = Math.max(0, Math.min(100, vol)); var stepped = roundToStep(clamped, 5); @@ -52,13 +53,8 @@ Scope { property var notificationHistoryWin: notificationHistoryWin } - // Create dock for each monitor (respects dockMonitors setting) - Variants { - model: Quickshell.screens - - Dock { - property var modelData - } + Dock { + id: dock } Applauncher { @@ -83,17 +79,16 @@ Scope { NotificationServer { id: notificationServer onNotification: function (notification) { - console.log("Notification received:", notification.appName); notification.tracked = true; - - // Distribute notification to all visible notification popups - for (let i = 0; i < notificationPopupVariants.count; i++) { - let popup = notificationPopupVariants.objectAt(i); - if (popup && popup.notificationsVisible) { - popup.addNotification(notification); + if (notificationPopup.notificationsVisible) { + // Add notification to all popup instances + for (let i = 0; i < notificationPopup.children.length; i++) { + let child = notificationPopup.children[i]; + if (child.addNotification) { + child.addNotification(notification); + } } } - if (notificationHistoryWin) { notificationHistoryWin.addToHistory({ id: notification.id, @@ -107,19 +102,8 @@ Scope { } } - // Create notification popups for each selected monitor - Variants { - id: notificationPopupVariants - model: Quickshell.screens - - NotificationPopup { - property var modelData - barVisible: bar.visible - screen: modelData - visible: notificationsVisible && notificationModel.count > 0 && - (Settings.settings.notificationMonitors.includes(modelData.name) || - (Settings.settings.notificationMonitors.length === 0)) // Show on all if none selected - } + NotificationPopup { + id: notificationPopup } NotificationHistory { @@ -137,7 +121,7 @@ Scope { appLauncherPanel: appLauncherPanel lockScreen: lockScreen idleInhibitor: idleInhibitor - notificationPopupVariants: notificationPopupVariants + notificationPopup: notificationPopup } Connections { @@ -154,12 +138,11 @@ Scope { Timer { id: reloadTimer - interval: 500 + interval: 500 // ms repeat: false onTriggered: Quickshell.reload(true) } - // Handle screen configuration changes (delay reload if locked) Connections { target: Quickshell function onScreensChanged() { @@ -191,4 +174,4 @@ Scope { } } } -} +} \ No newline at end of file