Skip to content

Commit 1c995f6

Browse files
authored
GWL: add notification badges (#12569)
GWL: add notification badges to top right of panel icons and add config option to disable notification badges Remove 'notifications' extension role as it only allows for one extension. Use extensionsHandlingNotifications variable in messageTray.js for applets to increment/decrement when added/removed from panel instead. Ensure notificationDaemon can identify source of notifications from flatpak apps. Increase max notifications per source from 10 to 20.
1 parent 0c8b652 commit 1c995f6

File tree

12 files changed

+237
-63
lines changed

12 files changed

+237
-63
lines changed

data/theme/cinnamon-sass/_colors.scss

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ $destructive_color: #ff7b63;
1818
$warning_color: #f8e45c;
1919
$warning_bg_color: #cd9309;
2020

21+
$notification_badge_bg_color: #e74b37;
22+
2123
$accent_color: #78aeed;
2224
$accent_bg_color: #3584e4;
2325

data/theme/cinnamon-sass/widgets/_windowlist.scss

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,14 +89,25 @@
8989
&-button-label { padding-left: 4px;}
9090

9191
&-number-label {
92-
font-size: 0.8em;
92+
font-size: 10px;
9393
z-index: 99;
9494
}
9595

9696
&-badge {
9797
border-radius: 9999px;
9898
background-color: $bg_color;
9999
}
100+
101+
&-notifications-badge {
102+
border-radius: 9999px;
103+
background-color: $notification_badge_bg_color;
104+
color: $fg_color;
105+
font-size: 13px;
106+
}
107+
108+
&-notifications-badge-label {
109+
z-index: 99;
110+
}
100111
}
101112

102113
// classic window list

files/usr/share/cinnamon/applets/[email protected]/appGroup.js

Lines changed: 80 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -119,31 +119,50 @@ class AppGroup {
119119
});
120120
this.actor.add_child(this.progressOverlay);
121121

122-
// Create the app button icon, number label, and text label for titleDisplay
122+
// Create the app button icon, window count and notification badges, and text label for titleDisplay
123123
this.iconBox = new Cinnamon.Slicer({name: 'appMenuIcon'});
124124
this.actor.add_child(this.iconBox);
125125
this.setActorAttributes(null, params.metaWindow);
126126

127-
this.badge = new St.BoxLayout({
127+
this.windowsBadge = new St.BoxLayout({
128128
style_class: 'grouped-window-list-badge',
129129
important: true,
130-
x_align: St.Align.START,
130+
x_align: St.Align.MIDDLE,
131131
y_align: St.Align.MIDDLE,
132132
show_on_set_parent: false,
133133
});
134-
this.numberLabel = new St.Label({
134+
this.windowsBadgeLabel = new St.Label({
135135
style_class: 'grouped-window-list-number-label',
136136
important: true,
137-
text: '',
138-
anchor_x: -3 * global.ui_scale,
137+
text: ''
138+
});
139+
this.windowsBadgeLabel.clutter_text.ellipsize = false;
140+
this.windowsBadge.add(this.windowsBadgeLabel, {
141+
x_align: St.Align.START,
142+
y_align: St.Align.START,
143+
});
144+
this.actor.add_child(this.windowsBadge);
145+
this.windowsBadge.set_text_direction(St.TextDirection.LTR);
146+
147+
this.notificationsBadge = new St.BoxLayout({
148+
style_class: 'grouped-window-list-notifications-badge',
149+
important: true,
150+
x_align: St.Align.MIDDLE,
151+
y_align: St.Align.MIDDLE,
152+
show_on_set_parent: false,
139153
});
140-
this.numberLabel.clutter_text.ellipsize = false;
141-
this.badge.add(this.numberLabel, {
154+
this.notificationsBadgeLabel = new St.Label({
155+
style_class: 'grouped-window-list-notifications-badge-label',
156+
important: true,
157+
text: ''
158+
});
159+
this.notificationsBadgeLabel.clutter_text.ellipsize = false;
160+
this.notificationsBadge.add(this.notificationsBadgeLabel, {
142161
x_align: St.Align.START,
143162
y_align: St.Align.START,
144163
});
145-
this.actor.add_child(this.badge);
146-
this.badge.set_text_direction(St.TextDirection.LTR);
164+
this.actor.add_child(this.notificationsBadge);
165+
this.notificationsBadge.set_text_direction(St.TextDirection.LTR);
147166

148167
this.label = new St.Label({
149168
style_class: 'grouped-window-list-button-label',
@@ -372,6 +391,8 @@ class AppGroup {
372391
const allocWidth = box.x2 - box.x1;
373392
const allocHeight = box.y2 - box.y1;
374393
const childBox = new Clutter.ActorBox();
394+
const windowBadgeBox = new Clutter.ActorBox();
395+
const notifBadgeBox = new Clutter.ActorBox();
375396
const direction = this.actor.get_text_direction();
376397

377398
// Set the icon to be left-justified (or right-justified) and centered vertically
@@ -394,15 +415,37 @@ class AppGroup {
394415

395416
this.iconBox.allocate(childBox, flags);
396417

397-
// Set badge position
398-
const windowCountFactor = this.groupState.windowCount > 9 ? 1.5 : 2;
399-
const badgeOffset = 2 * global.ui_scale;
400-
childBox.x1 = childBox.x1 - badgeOffset;
401-
childBox.x2 = childBox.x1 + (this.numberLabel.width * windowCountFactor);
402-
childBox.y1 = Math.max(childBox.y1 - badgeOffset, 0);
403-
childBox.y2 = childBox.y1 + this.badge.get_preferred_height(childBox.get_width())[1];
404-
405-
this.badge.allocate(childBox, flags);
418+
// Set windows badge position
419+
const windowBadgeOffset = 3 * global.ui_scale;
420+
const windowBadgeXCenter = this.iconBox.x + windowBadgeOffset;
421+
const windowBadgeYCenter = this.iconBox.y + windowBadgeOffset;
422+
const [wLabelMinWidth, wLabelMinHeight, wLabelNaturalWidth, wLabelNaturalHeight] = this.windowsBadgeLabel.get_preferred_size();
423+
const windowBadgesize = Math.max(wLabelNaturalWidth, wLabelNaturalHeight);
424+
windowBadgeBox.x1 = Math.max(windowBadgeXCenter - Math.floor(windowBadgesize / 2), 0);
425+
windowBadgeBox.x2 = windowBadgeBox.x1 + windowBadgesize;
426+
windowBadgeBox.y1 = Math.max(windowBadgeYCenter - Math.floor(windowBadgesize / 2), 0);
427+
windowBadgeBox.y2 = windowBadgeBox.y1 + windowBadgesize;
428+
const windowLabelPosX = Math.floor((windowBadgesize - wLabelNaturalWidth) / 2);
429+
const windowLabelPosY = Math.floor((windowBadgesize - wLabelNaturalHeight) / 2);
430+
this.windowsBadgeLabel.set_anchor_point(-windowLabelPosX, -windowLabelPosY);
431+
this.windowsBadge.set_size(windowBadgesize, windowBadgesize);
432+
this.windowsBadge.allocate(windowBadgeBox, flags);
433+
434+
// Set notifications badge position
435+
const notifBadgeOffset = 3 * global.ui_scale;
436+
const notifBadgeXCenter = this.iconBox.x + this.iconBox.width - notifBadgeOffset;
437+
const notifBadgeYCenter = this.iconBox.y + notifBadgeOffset;
438+
const [nLabelMinWidth, nLabelMinHeight, nLabelNaturalWidth, nLabelNaturalHeight] = this.notificationsBadgeLabel.get_preferred_size();
439+
const notifBadgesize = Math.max(nLabelNaturalWidth, nLabelNaturalHeight);
440+
notifBadgeBox.x2 = Math.min(notifBadgeXCenter + Math.floor(notifBadgesize / 2), box.x2);
441+
notifBadgeBox.x1 = notifBadgeBox.x2 - notifBadgesize;
442+
notifBadgeBox.y1 = Math.max(notifBadgeYCenter - Math.floor(notifBadgesize / 2), 0);
443+
notifBadgeBox.y2 = notifBadgeBox.y1 + notifBadgesize;
444+
const notifLabelPosX = Math.floor((notifBadgesize - nLabelNaturalWidth) / 2);
445+
const notifLabelPosY = Math.floor((notifBadgesize - nLabelNaturalHeight) / 2);
446+
this.notificationsBadgeLabel.set_anchor_point(-notifLabelPosX, -notifLabelPosY);
447+
this.notificationsBadge.set_size(notifBadgesize, notifBadgesize);
448+
this.notificationsBadge.allocate(notifBadgeBox, flags);
406449

407450
// Set label position
408451
if (this.drawLabel) {
@@ -676,8 +719,8 @@ class AppGroup {
676719
}
677720

678721
showOrderLabel(number) {
679-
this.numberLabel.text = (number + 1).toString();
680-
this.badge.show();
722+
this.windowsBadgeLabel.text = (number + 1).toString();
723+
this.windowsBadge.show();
681724
}
682725

683726
launchNewInstance(offload=false) {
@@ -917,6 +960,7 @@ class AppGroup {
917960
this.setIcon(metaWindow)
918961

919962
this.calcWindowNumber();
963+
this.updateNotificationsBadge();
920964
this.onFocusChange();
921965
}
922966
set({
@@ -1074,20 +1118,24 @@ class AppGroup {
10741118
calcWindowNumber() {
10751119
if (this.groupState.willUnmount) return;
10761120

1077-
const windowCount = this.groupState.metaWindows ? this.groupState.metaWindows.length : 0;
1078-
this.numberLabel.text = windowCount.toString();
1079-
1080-
this.groupState.set({windowCount});
1121+
this.groupState.set({windowCount: this.groupState.metaWindows ? this.groupState.metaWindows.length : 0});
1122+
1123+
if (this.groupState.windowCount > 1 && this.state.settings.enableWindowCountBadges) {
1124+
this.windowsBadgeLabel.text = this.groupState.windowCount.toString();
1125+
this.windowsBadge.show();
1126+
} else {
1127+
this.windowsBadge.hide();
1128+
}
1129+
}
10811130

1082-
if (this.state.settings.numDisplay) {
1083-
if (windowCount <= 1) {
1084-
this.badge.hide();
1085-
} else {
1086-
this.badge.show();
1131+
updateNotificationsBadge() {
1132+
const nCount = Main.notificationDaemon.getNotificationCountForApp(this.groupState.app);
10871133

1088-
}
1134+
if (nCount > 0 && this.state.settings.enableNotificationBadges) {
1135+
this.notificationsBadgeLabel.text = nCount.toString();
1136+
this.notificationsBadge.show();
10891137
} else {
1090-
this.badge.hide();
1138+
this.notificationsBadge.hide();
10911139
}
10921140
}
10931141

files/usr/share/cinnamon/applets/[email protected]/applet.js

Lines changed: 59 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ const Applet = imports.ui.applet;
99
const Cinnamon = imports.gi.Cinnamon;
1010
const Main = imports.ui.main;
1111
const DND = imports.ui.dnd;
12+
const MessageTray = imports.ui.messageTray;
1213
const {AppletSettings} = imports.ui.settings;
1314
const {SignalManager} = imports.misc.signalManager;
1415
const {throttle, unref, trySpawnCommandLine} = imports.misc.util;
@@ -254,7 +255,7 @@ class GroupedWindowListApplet extends Applet.Applet {
254255
cycleWindows: (e, source) => this.handleScroll(e, source),
255256
openAbout: () => this.openAbout(),
256257
configureApplet: () => this.configureApplet(),
257-
removeApplet: (event) => this.confirmRemoveApplet(event),
258+
removeApplet: (event) => this.confirmRemoveApplet(event)
258259
});
259260

260261
this.settings = new AppletSettings(this.state.settings, metadata.uuid, instance_id);
@@ -291,6 +292,7 @@ class GroupedWindowListApplet extends Applet.Applet {
291292
this.signals.connect(global.display, 'window-created', (...args) => this.onWindowCreated(...args));
292293
this.signals.connect(global.settings, 'changed::panel-edit-mode', (...args) => this.on_panel_edit_mode_changed(...args));
293294
this.signals.connect(Main.themeManager, 'theme-set', (...args) => this.refreshCurrentWorkspace(...args));
295+
this.signals.connect(Main.messageTray, 'notify-applet-update', this._onNotificationReceived.bind(this));
294296
}
295297

296298
bindSettings() {
@@ -307,7 +309,8 @@ class GroupedWindowListApplet extends Applet.Applet {
307309
{key: 'super-num-hotkeys', value: 'SuperNumHotkeys', cb: this.bindAppKeys},
308310
{key: 'title-display', value: 'titleDisplay', cb: this.updateTitleDisplay},
309311
{key: 'launcher-animation-effect', value: 'launcherAnimationEffect', cb: null},
310-
{key: 'number-display', value: 'numDisplay', cb: this.updateWindowNumberState},
312+
{key: 'enable-window-count-badges', value: 'enableWindowCountBadges', cb: this.onEnableWindowCountBadgeChange},
313+
{key: 'enable-notification-badges', value: 'enableNotificationBadges', cb: this.onEnableNotificationsChange},
311314
{key: 'enable-app-button-dragging', value: 'enableDragging', cb: this.draggableSettingChanged},
312315
{key: 'thumbnail-scroll-behavior', value: 'thumbnailScrollBehavior', cb: null},
313316
{key: 'show-thumbnails', value: 'showThumbs', cb: this.updateVerticalThumbnailState},
@@ -357,6 +360,7 @@ class GroupedWindowListApplet extends Applet.Applet {
357360
}
358361
this.bindAppKeys();
359362
this.state.set({appletReady: true});
363+
MessageTray.extensionsHandlingNotifications++;
360364
}
361365

362366
_updateState(initialUpdate) {
@@ -424,6 +428,7 @@ class GroupedWindowListApplet extends Applet.Applet {
424428
});
425429
this.settings.finalize();
426430
unref(this, RESERVE_KEYS);
431+
MessageTray.extensionsHandlingNotifications--;
427432
}
428433

429434
on_panel_icon_size_changed(iconSize) {
@@ -584,7 +589,7 @@ class GroupedWindowListApplet extends Applet.Applet {
584589
});
585590
}
586591

587-
updateWindowNumberState() {
592+
onEnableWindowCountBadgeChange() {
588593
this.workspaces.forEach(
589594
workspace => workspace.calcAllWindowNumbers()
590595
);
@@ -1022,6 +1027,57 @@ class GroupedWindowListApplet extends Applet.Applet {
10221027
this.state.set({thumbnailCloseButtonOffset: global.ui_scale > 1 ? -10 : 0});
10231028
this.refreshAllWorkspaces();
10241029
}
1030+
1031+
_onNotificationReceived(mtray, notification) {
1032+
let appId = notification.source.app?.get_id();
1033+
1034+
if (!appId) {
1035+
return;
1036+
}
1037+
1038+
// Add notification to all appgroups with appId.
1039+
let notificationAdded = false;
1040+
1041+
this.workspaces.forEach(workspace => {
1042+
if (!workspace) return;
1043+
workspace.appGroups.forEach(appGroup => {
1044+
if (!appGroup || !appGroup.groupState || appGroup.groupState.willUnmount) return;
1045+
if (appId === appGroup.groupState.appId) {
1046+
notificationAdded = true;
1047+
appGroup.updateNotificationsBadge();
1048+
}
1049+
});
1050+
});
1051+
1052+
if (notificationAdded) {
1053+
notification.appId = appId;
1054+
notification.connect('destroy', () => this._onNotificationDestroyed(notification));
1055+
}
1056+
}
1057+
1058+
_onNotificationDestroyed(notification) {
1059+
if (!this.workspaces) return;
1060+
1061+
this.workspaces.forEach(workspace => {
1062+
if (!workspace) return;
1063+
workspace.appGroups.forEach(appGroup => {
1064+
if (!appGroup || !appGroup.groupState || appGroup.groupState.willUnmount) return;
1065+
if (notification.appId === appGroup.groupState.appId) {
1066+
appGroup.updateNotificationsBadge();
1067+
}
1068+
});
1069+
});
1070+
}
1071+
1072+
onEnableNotificationsChange() {
1073+
this.workspaces.forEach(workspace => {
1074+
if (!workspace) return;
1075+
workspace.appGroups.forEach(appGroup => {
1076+
if (!appGroup || !appGroup.groupState || appGroup.groupState.willUnmount) return;
1077+
appGroup.updateNotificationsBadge();
1078+
});
1079+
});
1080+
}
10251081
}
10261082

10271083
function main(metadata, orientation, panel_height, instance_id) {

files/usr/share/cinnamon/applets/[email protected]/settings-schema.json

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,8 @@
5050
"keys": [
5151
"title-display",
5252
"launcher-animation-effect",
53-
"number-display",
53+
"enable-window-count-badges",
54+
"enable-notification-badges",
5455
"enable-app-button-dragging"
5556
]
5657
},
@@ -184,10 +185,17 @@
184185
"Scale": 3
185186
}
186187
},
187-
"number-display": {
188+
"enable-window-count-badges": {
188189
"type": "checkbox",
189190
"default": true,
190-
"description": "Show window count numbers"
191+
"description": "Show window count badges",
192+
"tooltip": "Indicate on the panel the number of open windows an application has"
193+
},
194+
"enable-notification-badges": {
195+
"type": "checkbox",
196+
"default": true,
197+
"description": "Show notification badges",
198+
"tooltip": "Indicate on the panel when an application has notifications"
191199
},
192200
"enable-app-button-dragging": {
193201
"type": "checkbox",

files/usr/share/cinnamon/applets/[email protected]/workspace.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -333,7 +333,7 @@ class Workspace {
333333

334334
calcAllWindowNumbers() {
335335
this.appGroups.forEach( appGroup => {
336-
appGroup.calcWindowNumber(appGroup.groupState.metaWindows);
336+
appGroup.calcWindowNumber();
337337
});
338338
}
339339

files/usr/share/cinnamon/applets/[email protected]/applet.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ const PopupMenu = imports.ui.popupMenu;
77
const St = imports.gi.St;
88
const Mainloop = imports.mainloop;
99
const Urgency = imports.ui.messageTray.Urgency;
10+
const MessageTray = imports.ui.messageTray;
1011
const NotificationDestroyedReason = imports.ui.messageTray.NotificationDestroyedReason;
1112
const Settings = imports.ui.settings;
1213
const Gettext = imports.gettext.domain("cinnamon-applets");
@@ -55,6 +56,11 @@ class CinnamonNotificationsApplet extends Applet.TextIconApplet {
5556
Main.keybindingManager.removeXletHotKey(this, "notification-open");
5657
Main.keybindingManager.removeXletHotKey(this, "notification-clear");
5758
global.settings.disconnect(this.panelEditModeHandler);
59+
60+
MessageTray.extensionsHandlingNotifications--;
61+
if (MessageTray.extensionsHandlingNotifications === 0) {
62+
this._clear_all();
63+
}
5864
}
5965

6066
_openMenu() {
@@ -266,6 +272,7 @@ class CinnamonNotificationsApplet extends Applet.TextIconApplet {
266272

267273
on_applet_added_to_panel() {
268274
this.on_orientation_changed(this._orientation);
275+
MessageTray.extensionsHandlingNotifications++;
269276
}
270277

271278
on_orientation_changed (orientation) {

files/usr/share/cinnamon/applets/[email protected]/metadata.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,5 @@
22
33
"name": "Notifications",
44
"description": "Click to display and manage system notifications",
5-
"role": "notifications",
65
"icon": "cs-notifications"
76
}

0 commit comments

Comments
 (0)