Skip to content

Commit 74f26d6

Browse files
committed
GWL: add notification badges to panel icons and remove "Show window count numbers" config option.
Remove 'notifications' extension role as it only allows for one extension. Increase max notifications per source from 10 to 20.
1 parent 5174d3c commit 74f26d6

File tree

12 files changed

+230
-65
lines changed

12 files changed

+230
-65
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: #ef2591;
22+
2123
$accent_color: #78aeed;
2224
$accent_bg_color: #3584e4;
2325

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

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -73,14 +73,24 @@
7373

7474
&-button-label { padding-left: 4px;}
7575

76-
&-number-label {
77-
font-size: 0.8em;
76+
&-windows-badge {
77+
border-radius: 9999px;
78+
color: $fg_color;
79+
background-color: $accent_bg_color;
80+
}
81+
82+
&-windows-badge-label {
7883
z-index: 99;
7984
}
8085

81-
&-badge {
86+
&-notifications-badge {
8287
border-radius: 9999px;
83-
background-color: $bg_color;
88+
color: $fg_color;
89+
background-color: $notification_badge_bg_color;
90+
}
91+
92+
&-notifications-badge-label {
93+
z-index: 99;
8494
}
8595
}
8696

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

Lines changed: 66 additions & 33 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({
128-
style_class: 'grouped-window-list-badge',
127+
this.windowsBadge = new St.BoxLayout({
128+
style_class: 'grouped-window-list-windows-badge',
129129
important: true,
130+
x_align: St.Align.MIDDLE,
131+
y_align: St.Align.MIDDLE,
132+
show_on_set_parent: false,
133+
});
134+
this.windowsBadgeLabel = new St.Label({
135+
style_class: 'grouped-window-list-windows-badge-label',
136+
important: true,
137+
text: ''
138+
});
139+
this.windowsBadgeLabel.clutter_text.ellipsize = false;
140+
this.windowsBadge.add(this.windowsBadgeLabel, {
130141
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,
131151
y_align: St.Align.MIDDLE,
132152
show_on_set_parent: false,
133153
});
134-
this.numberLabel = new St.Label({
135-
style_class: 'grouped-window-list-number-label',
154+
this.notificationsBadgeLabel = new St.Label({
155+
style_class: 'grouped-window-list-notifications-badge-label',
136156
important: true,
137-
text: '',
138-
anchor_x: -3 * global.ui_scale,
157+
text: ''
139158
});
140-
this.numberLabel.clutter_text.ellipsize = false;
141-
this.badge.add(this.numberLabel, {
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',
@@ -172,6 +191,7 @@ class AppGroup {
172191
this.calcWindowNumber();
173192
this.on_orientation_changed(true);
174193
this.handleFavorite();
194+
this.state.notifications.addAppGroup(this);
175195
}
176196

177197
initThumbnailMenu() {
@@ -394,15 +414,23 @@ class AppGroup {
394414

395415
this.iconBox.allocate(childBox, flags);
396416

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];
417+
this.updateBadgesTextSize();
404418

405-
this.badge.allocate(childBox, flags);
419+
// Set windows badge position
420+
childBox.x1 = box.x1;
421+
childBox.x2 = childBox.x1 + this.windowsBadgeLabel.width;
422+
childBox.y1 = box.y2 - this.windowsBadge.get_preferred_height(childBox.get_width())[1];
423+
childBox.y2 = box.y2;
424+
425+
this.windowsBadge.allocate(childBox, flags);
426+
427+
// Set notifications badge position
428+
childBox.x2 = this.iconBox.x + this.iconBox.width;
429+
childBox.x1 = childBox.x2 - this.notificationsBadgeLabel.width;
430+
childBox.y1 = box.y1;
431+
childBox.y2 = childBox.y1 + this.notificationsBadge.get_preferred_height(childBox.get_width())[1];
432+
433+
this.notificationsBadge.allocate(childBox, flags);
406434

407435
// Set label position
408436
if (this.drawLabel) {
@@ -450,6 +478,14 @@ class AppGroup {
450478
if (this.progressOverlay.visible) this.allocateProgress(childBox, flags);
451479
}
452480

481+
updateBadgesTextSize() {
482+
const badgeTextSize = Math.round(this.iconBox.width / 2.5 / global.ui_scale);
483+
const badgePadding = Math.round(badgeTextSize / 4);
484+
const sizeStyle = `font-size: ${badgeTextSize}px; padding-left: ${badgePadding}px; padding-right: ${badgePadding}px;`;
485+
this.windowsBadgeLabel.set_style(sizeStyle);
486+
this.notificationsBadgeLabel.set_style(sizeStyle);
487+
}
488+
453489
showLabel(animate = false) {
454490
if (this.labelVisiblePref
455491
|| !this.label
@@ -676,8 +712,8 @@ class AppGroup {
676712
}
677713

678714
showOrderLabel(number) {
679-
this.numberLabel.text = (number + 1).toString();
680-
this.badge.show();
715+
this.windowsBadgeLabel.text = (number + 1).toString();
716+
this.windowsBadge.show();
681717
}
682718

683719
launchNewInstance(offload=false) {
@@ -917,6 +953,7 @@ class AppGroup {
917953
this.setIcon(metaWindow)
918954

919955
this.calcWindowNumber();
956+
this.state.notifications.updateNotificationsBadge(this);
920957
this.onFocusChange();
921958
}
922959
set({
@@ -1074,20 +1111,16 @@ class AppGroup {
10741111
calcWindowNumber() {
10751112
if (this.groupState.willUnmount) return;
10761113

1077-
const windowCount = this.groupState.metaWindows ? this.groupState.metaWindows.length : 0;
1078-
this.numberLabel.text = windowCount.toString();
1079-
1080-
this.groupState.set({windowCount});
1081-
1082-
if (this.state.settings.numDisplay) {
1083-
if (windowCount <= 1) {
1084-
this.badge.hide();
1085-
} else {
1086-
this.badge.show();
1114+
this.groupState.set({windowCount: this.groupState.metaWindows ? this.groupState.metaWindows.length : 0});
1115+
this.updateWindowsBadge();
1116+
}
10871117

1088-
}
1118+
updateWindowsBadge(){
1119+
if (this.groupState.windowCount > 1) {
1120+
this.windowsBadgeLabel.text = this.groupState.windowCount.toString();
1121+
this.windowsBadge.show();
10891122
} else {
1090-
this.badge.hide();
1123+
this.windowsBadge.hide();
10911124
}
10921125
}
10931126

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

Lines changed: 123 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ 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 NotificationDestroyedReason = imports.ui.messageTray.NotificationDestroyedReason;
13+
const MessageTray = imports.ui.messageTray;
1214
const {AppletSettings} = imports.ui.settings;
1315
const {SignalManager} = imports.misc.signalManager;
1416
const {throttle, unref, trySpawnCommandLine} = imports.misc.util;
@@ -161,6 +163,124 @@ class PinnedFavs {
161163
}
162164
}
163165

166+
class Notifications {
167+
// As an app can have multiple appgroups (multiple windows, different workspaces, multiple instances), all
168+
// appGroups with the same appId should always display the same number of notifications.
169+
constructor() {
170+
this._appGroupList = []; //stores all appGroups from all workspaces
171+
Main.messageTray.connect('notify-applet-update', (mtray, notification) => this._notificationReceived(mtray, notification));
172+
}
173+
174+
_notificationReceived(mtray, notification) {
175+
const guessFlatpakAppIdFromDesktopEntryHint = (desktopEntry) => {
176+
let tryAppId = desktopEntry + '.desktop:flatpak';
177+
if (this._appGroupList.some(appGroup => appGroup.groupState?.appId === tryAppId)) {
178+
return tryAppId;
179+
}
180+
const exceptions = {
181+
"vivaldi-stable": "com.vivaldi.Vivaldi",
182+
"brave-browser": "com.brave.Browser",
183+
"google-chrome": "com.google.Chrome",
184+
"microsoft-edge": "com.microsoft.Edge",
185+
"opera": "com.opera.Opera"
186+
};
187+
if (exceptions[desktopEntry]) {
188+
tryAppId = exceptions[desktopEntry] + '.desktop:flatpak';
189+
if (this._appGroupList.some(appGroup => appGroup.groupState?.appId === tryAppId)) {
190+
return tryAppId;
191+
}
192+
}
193+
};
194+
195+
let appId = notification.source.app?.get_id();
196+
if (!appId) {
197+
appId = guessFlatpakAppIdFromDesktopEntryHint(notification.desktopEntry);
198+
}
199+
if (!appId) {
200+
global.logError('GWL: Failed to find appId for notification with desktopEntry hint: '
201+
+ notification.desktopEntry);
202+
return;
203+
}
204+
205+
// Add notification to all appgroups with appId
206+
let notificationAdded = false;
207+
this._appGroupList.forEach(appGroup => {
208+
if (!appGroup.groupState || appGroup.groupState.willUnmount) return;
209+
if (appId === appGroup.groupState.appId) {
210+
appGroup.notifications.push(notification);
211+
notificationAdded = true;
212+
this.updateNotificationsBadge(appGroup);
213+
}
214+
});
215+
if (notificationAdded) {
216+
notification.appId = appId;
217+
notification.connect('destroy', () => this._removeNotification(notification));
218+
}
219+
}
220+
221+
_removeNotification(notification) {
222+
this._appGroupList.forEach(appGroup => {
223+
if (!appGroup.groupState || appGroup.groupState.willUnmount) return;
224+
if (notification.appId === appGroup.groupState.appId) {
225+
const index = appGroup.notifications.indexOf(notification);
226+
if (index > -1) {
227+
appGroup.notifications.splice(index, 1)
228+
this.updateNotificationsBadge(appGroup);
229+
}
230+
}
231+
});
232+
}
233+
234+
// Called when an app is focused to remove all notifications from all instances of an app (with given appId)
235+
removeAllNotifications(appId) {
236+
this._appGroupList.forEach(appGroup => {
237+
if (!appGroup.groupState || appGroup.groupState.willUnmount) return;
238+
if (appId === appGroup.groupState.appId) {
239+
// iterate backwards due to in place array element deletion
240+
for (let i = appGroup.notifications.length - 1; i >= 0; i--) {
241+
if (appGroup.notifications[i] && !appGroup.notifications[i]._destroyed) {
242+
appGroup.notifications[i].destroy(NotificationDestroyedReason.DISMISSED);
243+
}
244+
}
245+
appGroup.notifications = [];
246+
this.updateNotificationsBadge(appGroup);
247+
}
248+
});
249+
}
250+
251+
updateNotificationsBadge(appGroup) {
252+
if (appGroup.notifications.length > 0) {
253+
appGroup.notificationsBadgeLabel.text = appGroup.notifications.length.toString();
254+
appGroup.notificationsBadge.show();
255+
} else {
256+
appGroup.notificationsBadge.hide();
257+
}
258+
}
259+
260+
// Called from AppGroup constructor so that we always have a list of all appgroups from all workspaces.
261+
addAppGroup(newAppGroup) {
262+
newAppGroup.notifications = [];
263+
264+
//Copy notifications from any existing appGroup with the same appId.
265+
this._appGroupList.some(appGroup => {
266+
if (!appGroup.groupState || appGroup.groupState.willUnmount) return false;
267+
if (appGroup.groupState.appId === newAppGroup.groupState.appId) {
268+
newAppGroup.notifications = appGroup.notifications.slice(); //shallow copy
269+
return true;
270+
}
271+
})
272+
273+
this._appGroupList.push(newAppGroup);
274+
275+
//remove old deleted appgroups
276+
this._appGroupList = this._appGroupList.filter(appGroup =>
277+
appGroup !== null &&
278+
appGroup !== undefined &&
279+
appGroup.groupState &&
280+
!appGroup.groupState.willUnmount);
281+
}
282+
}
283+
164284
class GroupedWindowListApplet extends Applet.Applet {
165285
constructor(metadata, orientation, panel_height, instance_id) {
166286
super(orientation, panel_height, instance_id);
@@ -191,6 +311,7 @@ class GroupedWindowListApplet extends Applet.Applet {
191311
appletReady: false,
192312
willUnmount: false,
193313
settings: {},
314+
notifications: new Notifications(),
194315
homeDir: GLib.get_home_dir(),
195316
lastOverlayPreview: null,
196317
lastCycled: -1,
@@ -307,7 +428,6 @@ class GroupedWindowListApplet extends Applet.Applet {
307428
{key: 'super-num-hotkeys', value: 'SuperNumHotkeys', cb: this.bindAppKeys},
308429
{key: 'title-display', value: 'titleDisplay', cb: this.updateTitleDisplay},
309430
{key: 'launcher-animation-effect', value: 'launcherAnimationEffect', cb: null},
310-
{key: 'number-display', value: 'numDisplay', cb: this.updateWindowNumberState},
311431
{key: 'enable-app-button-dragging', value: 'enableDragging', cb: this.draggableSettingChanged},
312432
{key: 'thumbnail-scroll-behavior', value: 'thumbnailScrollBehavior', cb: null},
313433
{key: 'show-thumbnails', value: 'showThumbs', cb: this.updateVerticalThumbnailState},
@@ -357,6 +477,7 @@ class GroupedWindowListApplet extends Applet.Applet {
357477
}
358478
this.bindAppKeys();
359479
this.state.set({appletReady: true});
480+
MessageTray.extensionsHandlingNotifications++;
360481
}
361482

362483
_updateState(initialUpdate) {
@@ -424,6 +545,7 @@ class GroupedWindowListApplet extends Applet.Applet {
424545
});
425546
this.settings.finalize();
426547
unref(this, RESERVE_KEYS);
548+
MessageTray.extensionsHandlingNotifications--;
427549
}
428550

429551
on_panel_icon_size_changed(iconSize) {
@@ -584,12 +706,6 @@ class GroupedWindowListApplet extends Applet.Applet {
584706
});
585707
}
586708

587-
updateWindowNumberState() {
588-
this.workspaces.forEach(
589-
workspace => workspace.calcAllWindowNumbers()
590-
);
591-
}
592-
593709
updateAttentionState(display, window) {
594710
this.workspaces.forEach(
595711
workspace => workspace.updateAttentionState(display, window)

0 commit comments

Comments
 (0)