diff --git a/data/theme/cinnamon-sass/_colors.scss b/data/theme/cinnamon-sass/_colors.scss index c9f18d40d4..b0b1063f83 100644 --- a/data/theme/cinnamon-sass/_colors.scss +++ b/data/theme/cinnamon-sass/_colors.scss @@ -18,6 +18,8 @@ $destructive_color: #ff7b63; $warning_color: #f8e45c; $warning_bg_color: #cd9309; +$notification_badge_bg_color: #e74b37; + $accent_color: #78aeed; $accent_bg_color: #3584e4; diff --git a/data/theme/cinnamon-sass/widgets/_windowlist.scss b/data/theme/cinnamon-sass/widgets/_windowlist.scss index a23da85c5c..8a0e8494d9 100644 --- a/data/theme/cinnamon-sass/widgets/_windowlist.scss +++ b/data/theme/cinnamon-sass/widgets/_windowlist.scss @@ -89,7 +89,7 @@ &-button-label { padding-left: 4px;} &-number-label { - font-size: 0.8em; + font-size: 10px; z-index: 99; } @@ -97,6 +97,17 @@ border-radius: 9999px; background-color: $bg_color; } + + &-notifications-badge { + border-radius: 9999px; + background-color: $notification_badge_bg_color; + color: $fg_color; + font-size: 13px; + } + + &-notifications-badge-label { + z-index: 99; + } } // classic window list diff --git a/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/appGroup.js b/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/appGroup.js index 6a0f0990c5..0594af49de 100644 --- a/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/appGroup.js +++ b/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/appGroup.js @@ -119,31 +119,50 @@ class AppGroup { }); this.actor.add_child(this.progressOverlay); - // Create the app button icon, number label, and text label for titleDisplay + // Create the app button icon, window count and notification badges, and text label for titleDisplay this.iconBox = new Cinnamon.Slicer({name: 'appMenuIcon'}); this.actor.add_child(this.iconBox); this.setActorAttributes(null, params.metaWindow); - this.badge = new St.BoxLayout({ + this.windowsBadge = new St.BoxLayout({ style_class: 'grouped-window-list-badge', important: true, - x_align: St.Align.START, + x_align: St.Align.MIDDLE, y_align: St.Align.MIDDLE, show_on_set_parent: false, }); - this.numberLabel = new St.Label({ + this.windowsBadgeLabel = new St.Label({ style_class: 'grouped-window-list-number-label', important: true, - text: '', - anchor_x: -3 * global.ui_scale, + text: '' + }); + this.windowsBadgeLabel.clutter_text.ellipsize = false; + this.windowsBadge.add(this.windowsBadgeLabel, { + x_align: St.Align.START, + y_align: St.Align.START, + }); + this.actor.add_child(this.windowsBadge); + this.windowsBadge.set_text_direction(St.TextDirection.LTR); + + this.notificationsBadge = new St.BoxLayout({ + style_class: 'grouped-window-list-notifications-badge', + important: true, + x_align: St.Align.MIDDLE, + y_align: St.Align.MIDDLE, + show_on_set_parent: false, }); - this.numberLabel.clutter_text.ellipsize = false; - this.badge.add(this.numberLabel, { + this.notificationsBadgeLabel = new St.Label({ + style_class: 'grouped-window-list-notifications-badge-label', + important: true, + text: '' + }); + this.notificationsBadgeLabel.clutter_text.ellipsize = false; + this.notificationsBadge.add(this.notificationsBadgeLabel, { x_align: St.Align.START, y_align: St.Align.START, }); - this.actor.add_child(this.badge); - this.badge.set_text_direction(St.TextDirection.LTR); + this.actor.add_child(this.notificationsBadge); + this.notificationsBadge.set_text_direction(St.TextDirection.LTR); this.label = new St.Label({ style_class: 'grouped-window-list-button-label', @@ -372,6 +391,8 @@ class AppGroup { const allocWidth = box.x2 - box.x1; const allocHeight = box.y2 - box.y1; const childBox = new Clutter.ActorBox(); + const windowBadgeBox = new Clutter.ActorBox(); + const notifBadgeBox = new Clutter.ActorBox(); const direction = this.actor.get_text_direction(); // Set the icon to be left-justified (or right-justified) and centered vertically @@ -394,15 +415,37 @@ class AppGroup { this.iconBox.allocate(childBox, flags); - // Set badge position - const windowCountFactor = this.groupState.windowCount > 9 ? 1.5 : 2; - const badgeOffset = 2 * global.ui_scale; - childBox.x1 = childBox.x1 - badgeOffset; - childBox.x2 = childBox.x1 + (this.numberLabel.width * windowCountFactor); - childBox.y1 = Math.max(childBox.y1 - badgeOffset, 0); - childBox.y2 = childBox.y1 + this.badge.get_preferred_height(childBox.get_width())[1]; - - this.badge.allocate(childBox, flags); + // Set windows badge position + const windowBadgeOffset = 3 * global.ui_scale; + const windowBadgeXCenter = this.iconBox.x + windowBadgeOffset; + const windowBadgeYCenter = this.iconBox.y + windowBadgeOffset; + const [wLabelMinWidth, wLabelMinHeight, wLabelNaturalWidth, wLabelNaturalHeight] = this.windowsBadgeLabel.get_preferred_size(); + const windowBadgesize = Math.max(wLabelNaturalWidth, wLabelNaturalHeight); + windowBadgeBox.x1 = Math.max(windowBadgeXCenter - Math.floor(windowBadgesize / 2), 0); + windowBadgeBox.x2 = windowBadgeBox.x1 + windowBadgesize; + windowBadgeBox.y1 = Math.max(windowBadgeYCenter - Math.floor(windowBadgesize / 2), 0); + windowBadgeBox.y2 = windowBadgeBox.y1 + windowBadgesize; + const windowLabelPosX = Math.floor((windowBadgesize - wLabelNaturalWidth) / 2); + const windowLabelPosY = Math.floor((windowBadgesize - wLabelNaturalHeight) / 2); + this.windowsBadgeLabel.set_anchor_point(-windowLabelPosX, -windowLabelPosY); + this.windowsBadge.set_size(windowBadgesize, windowBadgesize); + this.windowsBadge.allocate(windowBadgeBox, flags); + + // Set notifications badge position + const notifBadgeOffset = 3 * global.ui_scale; + const notifBadgeXCenter = this.iconBox.x + this.iconBox.width - notifBadgeOffset; + const notifBadgeYCenter = this.iconBox.y + notifBadgeOffset; + const [nLabelMinWidth, nLabelMinHeight, nLabelNaturalWidth, nLabelNaturalHeight] = this.notificationsBadgeLabel.get_preferred_size(); + const notifBadgesize = Math.max(nLabelNaturalWidth, nLabelNaturalHeight); + notifBadgeBox.x2 = Math.min(notifBadgeXCenter + Math.floor(notifBadgesize / 2), box.x2); + notifBadgeBox.x1 = notifBadgeBox.x2 - notifBadgesize; + notifBadgeBox.y1 = Math.max(notifBadgeYCenter - Math.floor(notifBadgesize / 2), 0); + notifBadgeBox.y2 = notifBadgeBox.y1 + notifBadgesize; + const notifLabelPosX = Math.floor((notifBadgesize - nLabelNaturalWidth) / 2); + const notifLabelPosY = Math.floor((notifBadgesize - nLabelNaturalHeight) / 2); + this.notificationsBadgeLabel.set_anchor_point(-notifLabelPosX, -notifLabelPosY); + this.notificationsBadge.set_size(notifBadgesize, notifBadgesize); + this.notificationsBadge.allocate(notifBadgeBox, flags); // Set label position if (this.drawLabel) { @@ -676,8 +719,8 @@ class AppGroup { } showOrderLabel(number) { - this.numberLabel.text = (number + 1).toString(); - this.badge.show(); + this.windowsBadgeLabel.text = (number + 1).toString(); + this.windowsBadge.show(); } launchNewInstance(offload=false) { @@ -917,6 +960,7 @@ class AppGroup { this.setIcon(metaWindow) this.calcWindowNumber(); + this.updateNotificationsBadge(); this.onFocusChange(); } set({ @@ -1074,20 +1118,24 @@ class AppGroup { calcWindowNumber() { if (this.groupState.willUnmount) return; - const windowCount = this.groupState.metaWindows ? this.groupState.metaWindows.length : 0; - this.numberLabel.text = windowCount.toString(); - - this.groupState.set({windowCount}); + this.groupState.set({windowCount: this.groupState.metaWindows ? this.groupState.metaWindows.length : 0}); + + if (this.groupState.windowCount > 1 && this.state.settings.enableWindowCountBadges) { + this.windowsBadgeLabel.text = this.groupState.windowCount.toString(); + this.windowsBadge.show(); + } else { + this.windowsBadge.hide(); + } + } - if (this.state.settings.numDisplay) { - if (windowCount <= 1) { - this.badge.hide(); - } else { - this.badge.show(); + updateNotificationsBadge() { + const nCount = Main.notificationDaemon.getNotificationCountForApp(this.groupState.app); - } + if (nCount > 0 && this.state.settings.enableNotificationBadges) { + this.notificationsBadgeLabel.text = nCount.toString(); + this.notificationsBadge.show(); } else { - this.badge.hide(); + this.notificationsBadge.hide(); } } diff --git a/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/applet.js b/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/applet.js index 75d1c85208..9be3d5c727 100644 --- a/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/applet.js +++ b/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/applet.js @@ -9,6 +9,7 @@ const Applet = imports.ui.applet; const Cinnamon = imports.gi.Cinnamon; const Main = imports.ui.main; const DND = imports.ui.dnd; +const MessageTray = imports.ui.messageTray; const {AppletSettings} = imports.ui.settings; const {SignalManager} = imports.misc.signalManager; const {throttle, unref, trySpawnCommandLine} = imports.misc.util; @@ -254,7 +255,7 @@ class GroupedWindowListApplet extends Applet.Applet { cycleWindows: (e, source) => this.handleScroll(e, source), openAbout: () => this.openAbout(), configureApplet: () => this.configureApplet(), - removeApplet: (event) => this.confirmRemoveApplet(event), + removeApplet: (event) => this.confirmRemoveApplet(event) }); this.settings = new AppletSettings(this.state.settings, metadata.uuid, instance_id); @@ -291,6 +292,7 @@ class GroupedWindowListApplet extends Applet.Applet { this.signals.connect(global.display, 'window-created', (...args) => this.onWindowCreated(...args)); this.signals.connect(global.settings, 'changed::panel-edit-mode', (...args) => this.on_panel_edit_mode_changed(...args)); this.signals.connect(Main.themeManager, 'theme-set', (...args) => this.refreshCurrentWorkspace(...args)); + this.signals.connect(Main.messageTray, 'notify-applet-update', this._onNotificationReceived.bind(this)); } bindSettings() { @@ -307,7 +309,8 @@ class GroupedWindowListApplet extends Applet.Applet { {key: 'super-num-hotkeys', value: 'SuperNumHotkeys', cb: this.bindAppKeys}, {key: 'title-display', value: 'titleDisplay', cb: this.updateTitleDisplay}, {key: 'launcher-animation-effect', value: 'launcherAnimationEffect', cb: null}, - {key: 'number-display', value: 'numDisplay', cb: this.updateWindowNumberState}, + {key: 'enable-window-count-badges', value: 'enableWindowCountBadges', cb: this.onEnableWindowCountBadgeChange}, + {key: 'enable-notification-badges', value: 'enableNotificationBadges', cb: this.onEnableNotificationsChange}, {key: 'enable-app-button-dragging', value: 'enableDragging', cb: this.draggableSettingChanged}, {key: 'thumbnail-scroll-behavior', value: 'thumbnailScrollBehavior', cb: null}, {key: 'show-thumbnails', value: 'showThumbs', cb: this.updateVerticalThumbnailState}, @@ -357,6 +360,7 @@ class GroupedWindowListApplet extends Applet.Applet { } this.bindAppKeys(); this.state.set({appletReady: true}); + MessageTray.extensionsHandlingNotifications++; } _updateState(initialUpdate) { @@ -424,6 +428,7 @@ class GroupedWindowListApplet extends Applet.Applet { }); this.settings.finalize(); unref(this, RESERVE_KEYS); + MessageTray.extensionsHandlingNotifications--; } on_panel_icon_size_changed(iconSize) { @@ -584,7 +589,7 @@ class GroupedWindowListApplet extends Applet.Applet { }); } - updateWindowNumberState() { + onEnableWindowCountBadgeChange() { this.workspaces.forEach( workspace => workspace.calcAllWindowNumbers() ); @@ -1022,6 +1027,57 @@ class GroupedWindowListApplet extends Applet.Applet { this.state.set({thumbnailCloseButtonOffset: global.ui_scale > 1 ? -10 : 0}); this.refreshAllWorkspaces(); } + + _onNotificationReceived(mtray, notification) { + let appId = notification.source.app?.get_id(); + + if (!appId) { + return; + } + + // Add notification to all appgroups with appId. + let notificationAdded = false; + + this.workspaces.forEach(workspace => { + if (!workspace) return; + workspace.appGroups.forEach(appGroup => { + if (!appGroup || !appGroup.groupState || appGroup.groupState.willUnmount) return; + if (appId === appGroup.groupState.appId) { + notificationAdded = true; + appGroup.updateNotificationsBadge(); + } + }); + }); + + if (notificationAdded) { + notification.appId = appId; + notification.connect('destroy', () => this._onNotificationDestroyed(notification)); + } + } + + _onNotificationDestroyed(notification) { + if (!this.workspaces) return; + + this.workspaces.forEach(workspace => { + if (!workspace) return; + workspace.appGroups.forEach(appGroup => { + if (!appGroup || !appGroup.groupState || appGroup.groupState.willUnmount) return; + if (notification.appId === appGroup.groupState.appId) { + appGroup.updateNotificationsBadge(); + } + }); + }); + } + + onEnableNotificationsChange() { + this.workspaces.forEach(workspace => { + if (!workspace) return; + workspace.appGroups.forEach(appGroup => { + if (!appGroup || !appGroup.groupState || appGroup.groupState.willUnmount) return; + appGroup.updateNotificationsBadge(); + }); + }); + } } function main(metadata, orientation, panel_height, instance_id) { diff --git a/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/settings-schema.json b/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/settings-schema.json index b86af45073..c4f1b49cc2 100644 --- a/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/settings-schema.json +++ b/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/settings-schema.json @@ -50,7 +50,8 @@ "keys": [ "title-display", "launcher-animation-effect", - "number-display", + "enable-window-count-badges", + "enable-notification-badges", "enable-app-button-dragging" ] }, @@ -184,10 +185,17 @@ "Scale": 3 } }, - "number-display": { + "enable-window-count-badges": { "type": "checkbox", "default": true, - "description": "Show window count numbers" + "description": "Show window count badges", + "tooltip": "Indicate on the panel the number of open windows an application has" + }, + "enable-notification-badges": { + "type": "checkbox", + "default": true, + "description": "Show notification badges", + "tooltip": "Indicate on the panel when an application has notifications" }, "enable-app-button-dragging": { "type": "checkbox", diff --git a/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/workspace.js b/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/workspace.js index 4329489297..4af591ee8c 100644 --- a/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/workspace.js +++ b/files/usr/share/cinnamon/applets/grouped-window-list@cinnamon.org/workspace.js @@ -333,7 +333,7 @@ class Workspace { calcAllWindowNumbers() { this.appGroups.forEach( appGroup => { - appGroup.calcWindowNumber(appGroup.groupState.metaWindows); + appGroup.calcWindowNumber(); }); } diff --git a/files/usr/share/cinnamon/applets/notifications@cinnamon.org/applet.js b/files/usr/share/cinnamon/applets/notifications@cinnamon.org/applet.js index 883b252a14..a0f45180d9 100644 --- a/files/usr/share/cinnamon/applets/notifications@cinnamon.org/applet.js +++ b/files/usr/share/cinnamon/applets/notifications@cinnamon.org/applet.js @@ -7,6 +7,7 @@ const PopupMenu = imports.ui.popupMenu; const St = imports.gi.St; const Mainloop = imports.mainloop; const Urgency = imports.ui.messageTray.Urgency; +const MessageTray = imports.ui.messageTray; const NotificationDestroyedReason = imports.ui.messageTray.NotificationDestroyedReason; const Settings = imports.ui.settings; const Gettext = imports.gettext.domain("cinnamon-applets"); @@ -55,6 +56,11 @@ class CinnamonNotificationsApplet extends Applet.TextIconApplet { Main.keybindingManager.removeXletHotKey(this, "notification-open"); Main.keybindingManager.removeXletHotKey(this, "notification-clear"); global.settings.disconnect(this.panelEditModeHandler); + + MessageTray.extensionsHandlingNotifications--; + if (MessageTray.extensionsHandlingNotifications === 0) { + this._clear_all(); + } } _openMenu() { @@ -266,6 +272,7 @@ class CinnamonNotificationsApplet extends Applet.TextIconApplet { on_applet_added_to_panel() { this.on_orientation_changed(this._orientation); + MessageTray.extensionsHandlingNotifications++; } on_orientation_changed (orientation) { diff --git a/files/usr/share/cinnamon/applets/notifications@cinnamon.org/metadata.json b/files/usr/share/cinnamon/applets/notifications@cinnamon.org/metadata.json index 6daa046102..0d9d00af31 100644 --- a/files/usr/share/cinnamon/applets/notifications@cinnamon.org/metadata.json +++ b/files/usr/share/cinnamon/applets/notifications@cinnamon.org/metadata.json @@ -2,6 +2,5 @@ "uuid": "notifications@cinnamon.org", "name": "Notifications", "description": "Click to display and manage system notifications", -"role": "notifications", "icon": "cs-notifications" } diff --git a/js/ui/appletManager.js b/js/ui/appletManager.js index b8bfc679d1..3a2ecdfb7d 100644 --- a/js/ui/appletManager.js +++ b/js/ui/appletManager.js @@ -25,7 +25,6 @@ var appletsLoaded = false; // FIXME: This role stuff is checked in extension.js, why not move checks from here to there? var Roles = { - NOTIFICATIONS: 'notifications', PANEL_LAUNCHER: 'panellauncher', WINDOW_ATTENTION_HANDLER: 'windowattentionhandler', WINDOW_LIST: 'windowlist' diff --git a/js/ui/extension.js b/js/ui/extension.js index 8c9ea22dce..8ef64bb114 100644 --- a/js/ui/extension.js +++ b/js/ui/extension.js @@ -104,7 +104,6 @@ var Type = { }), APPLET: _createExtensionType("Applet", "applets", AppletManager, { roles: { - notifications: null, windowlist: null, windowattentionhandler: null, panellauncher: null, @@ -113,7 +112,6 @@ var Type = { }), DESKLET: _createExtensionType("Desklet", "desklets", DeskletManager, { roles: { - notifications: null, windowlist: null, windowattentionhandler: null } @@ -339,7 +337,7 @@ Extension.prototype = { // If a role is set, make sure it's a valid one let meta_role_list_str = this.meta['role']; if (meta_role_list_str) { - let meta_roles = meta_role_list_str.replace(" ", "").split(","); + let meta_roles = meta_role_list_str.replaceAll(" ", "").split(","); for (let role of meta_roles) { if (!(role in Type[this.upperType].roles)) { throw logError(`Unknown role definition: ${role} in metadata.json`, this.uuid); @@ -415,7 +413,7 @@ Extension.prototype = { lockRole: function(roleProvider) { if (this.meta && this.meta.role) { let meta_role_list_str = this.meta.role; - let meta_roles = meta_role_list_str.replace(" ", "").split(","); + let meta_roles = meta_role_list_str.replaceAll(" ", "").split(","); let avail_roles = []; @@ -448,7 +446,7 @@ Extension.prototype = { unlockRoles: function() { if (this.meta.role) { let meta_role_list_str = this.meta.role; - let meta_roles = meta_role_list_str.replace(" ", "").split(","); + let meta_roles = meta_role_list_str.replaceAll(" ", "").split(","); for (let role of meta_roles) { if (Type[this.upperType].roles[role] === this.uuid) { diff --git a/js/ui/messageTray.js b/js/ui/messageTray.js index 63e46fa2af..7d224852c4 100644 --- a/js/ui/messageTray.js +++ b/js/ui/messageTray.js @@ -30,6 +30,11 @@ var LONGER_HIDE_TIMEOUT = 0.6; const NOTIFICATION_IMAGE_SIZE = 125; const NOTIFICATION_IMAGE_OPACITY = 230; // 0 - 255 +// Applets wishing to receive the "notify-applet-update" signal should increment and decrement this value when +// added and removed from the panel respectfully as when this value is zero, the signal will not be emitted and +// notifications will be automatically destroyed after being shown. +var extensionsHandlingNotifications = 0; + var State = Object.freeze({ HIDDEN: 0, SHOWING: 1, @@ -634,7 +639,7 @@ function Source(title) { Source.prototype = { ICON_SIZE: 24, - MAX_NOTIFICATIONS: 10, + MAX_NOTIFICATIONS: 20, _init: function (title) { this.title = title; @@ -1017,7 +1022,7 @@ MessageTray.prototype = { if (this._notification.urgency != Urgency.CRITICAL) { this._updateNotificationTimeout(this.notificationDuration * 1000); - } else if (AppletManager.get_role_provider_exists(AppletManager.Roles.NOTIFICATIONS)) { + } else if (extensionsHandlingNotifications > 0) { this._updateNotificationTimeout(NOTIFICATION_CRITICAL_TIMEOUT_WITH_APPLET * 1000); } }, @@ -1076,7 +1081,7 @@ MessageTray.prototype = { this._notificationBin.hide(); this._notificationBin.child = null; let notification = this._notification; - if (AppletManager.get_role_provider_exists(AppletManager.Roles.NOTIFICATIONS) && !this._notificationRemoved) { + if (extensionsHandlingNotifications > 0 && !this._notificationRemoved) { this.emit('notify-applet-update', notification); } else { if (notification.isTransient) diff --git a/js/ui/notificationDaemon.js b/js/ui/notificationDaemon.js index f14f844ab3..7c01633a5c 100644 --- a/js/ui/notificationDaemon.js +++ b/js/ui/notificationDaemon.js @@ -13,10 +13,6 @@ const MessageTray = imports.ui.messageTray; const Params = imports.misc.params; const Mainloop = imports.mainloop; -// don't automatically clear these apps' notifications on window focus -// lowercase only -const AUTOCLEAR_BLACKLIST = ['chromium', 'firefox', 'google chrome']; - let nextNotificationId = 1; // Should really be defined in Gio.js @@ -231,7 +227,8 @@ NotificationDaemon.prototype = { } } - let source = new Source(title, pid, sender, trayIcon); + const desktopEntryHint = ndata && ndata.hints['desktop-entry']; + let source = new Source(title, pid, sender, desktopEntryHint, trayIcon); source.setTransient(isForTransientNotification); if (!isForTransientNotification) { @@ -405,7 +402,7 @@ NotificationDaemon.prototype = { } let [pid] = result; - source = this._getSource(appName, pid, ndata, sender); + source = this._getSource(appName, pid, ndata, sender, null); // We only store sender-pid entries for persistent sources. // Removing the entries once the source is destroyed @@ -586,8 +583,6 @@ NotificationDaemon.prototype = { return; let name = tracker.focus_app.get_name(); - if (name && AUTOCLEAR_BLACKLIST.includes(name.toLowerCase())) - return; for (let i = 0; i < this._sources.length; i++) { let source = this._sources[i]; @@ -616,20 +611,31 @@ NotificationDaemon.prototype = { let source = this._lookupSource(null, icon.pid, true); if (source) source.destroy(); + }, + + getNotificationCountForApp(app) { + const foundSource = this._sources.find(source => source.app === app); + + if (foundSource) { + return foundSource.notifications.length; + } else { + return 0; + } } }; -function Source(title, pid, sender, trayIcon) { - this._init(title, pid, sender, trayIcon); +function Source(title, pid, sender, desktopEntryHint, trayIcon) { + this._init(title, pid, sender, desktopEntryHint, trayIcon); } Source.prototype = { __proto__: MessageTray.Source.prototype, - _init: function(title, pid, sender, trayIcon) { + _init: function(title, pid, sender, desktopEntryHint, trayIcon) { MessageTray.Source.prototype._init.call(this, title); this.initialTitle = title; + this.desktopEntryHint = desktopEntryHint; this.pid = pid; if (sender) @@ -676,8 +682,25 @@ Source.prototype = { let app; app = Cinnamon.WindowTracker.get_default().get_app_from_pid(this.pid); - if (app != null) - return app; + + // With flatpak apps, the notification's pid is that of the portal so use the desktop-entry hint instead. + if (!app && this.desktopEntryHint) { + const exceptions = { + "vivaldi-stable": "com.vivaldi.Vivaldi", + "brave-browser": "com.brave.Browser", + "google-chrome": "com.google.Chrome", + "microsoft-edge": "com.microsoft.Edge", + "opera": "com.opera.Opera" + }; + const exception = exceptions[this.desktopEntryHint]; + app = Cinnamon.AppSystem.get_default().lookup_flatpak_app_id(exception ? exception : this.desktopEntryHint); + if (!app) { + app = this._findUniqueAppByName(this.initialTitle); + } + if (!app) log('Failed to find flatpak app for notification with desktop-entry hint:', this.desktopEntryHint); + } + + if (app) return app; if (this.trayIcon) { app = Cinnamon.AppSystem.get_default().lookup_wmclass(this.trayIcon.wmclass); @@ -688,6 +711,24 @@ Source.prototype = { return null; }, + _findUniqueAppByName(appName) { + const appSystem = Cinnamon.AppSystem.get_default(); + const runningApps = appSystem.get_running(); + const matches = []; + + for (const app of runningApps) { + if (app.get_name() === appName) { + matches.push(app); + } + } + + if (matches.length === 1) { + return matches[0]; + } else { + return null; + } + }, + _setApp: function() { if (this.app) return;