Fix notification and other small fixes

This commit is contained in:
Ly-sec 2025-08-05 20:15:11 +02:00
parent fb68300746
commit 69d84752f3
10 changed files with 366 additions and 338 deletions

View file

@ -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;

View file

@ -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;
}
}
}

View file

@ -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() {
}
}
}

View file

@ -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;
}
}
}
}

View file

@ -255,7 +255,7 @@ Item {
font.pixelSize: 14
color: Theme.textSecondary
Layout.alignment: Qt.AlignCenter
Layout.topMargin: 16
Layout.topMargin: 24
}

View file

@ -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

View file

@ -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"

View file

@ -101,7 +101,7 @@ ColumnLayout {
ColumnLayout {
spacing: 16
Layout.fillWidth: true
Layout.topMargin: 16
Layout.topMargin: 58
Text {
text: "Bluetooth"

View file

@ -172,7 +172,7 @@ ColumnLayout {
ColumnLayout {
spacing: 4
Layout.fillWidth: true
Layout.topMargin: 16
Layout.topMargin: 58
Text {
text: "Weather"

View file

@ -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 {
}
}
}
}
}