Skip to content

Commit a705739

Browse files
committed
Add per app notification settings
fix claude suggestions 1. app.id -> app.get_id() 2. Add gio settings cache 6. trailing whitespace. fix claude suggestions in cs_notifications.py
1 parent 7f1863a commit a705739

2 files changed

Lines changed: 187 additions & 21 deletions

File tree

files/usr/share/cinnamon/cinnamon-settings/modules/cs_notifications.py

Lines changed: 148 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22

33
import gi
44
gi.require_version('Notify', '0.7')
5-
from gi.repository import Gio, Notify
5+
from gi.repository import Gio, Notify, Gtk, Pango
6+
import re
67

78
from bin.SettingsWidgets import SidePage
89
from xapp.GSettingsWidgets import *
@@ -55,6 +56,9 @@ def on_module_selected(self):
5556
switch = GSettingsSwitch(_("Enable notifications"), "org.cinnamon.desktop.notifications", "display-notifications")
5657
settings.add_row(switch)
5758

59+
button = Button(_("Application notifications"), self.open_app_settings)
60+
settings.add_reveal_row(button, "org.cinnamon.desktop.notifications", "display-notifications")
61+
5862
switch = GSettingsSwitch(_("Remove notifications after their timeout is reached"), "org.cinnamon.desktop.notifications", "remove-old")
5963
settings.add_reveal_row(switch, "org.cinnamon.desktop.notifications", "display-notifications")
6064

@@ -86,3 +90,146 @@ def on_module_selected(self):
8690
def send_test(self, widget):
8791
n = Notify.Notification.new(_("This is a test notification"), content, "dialog-warning")
8892
n.show()
93+
94+
def open_app_settings(self, widget):
95+
win = AppNotificationsWindow(widget.get_toplevel())
96+
97+
PER_APP_SCHEMA = "org.cinnamon.desktop.notifications.application"
98+
PER_APP_BASE_PATH = "/org/cinnamon/desktop/notifications/application/"
99+
100+
class AppNotificationRow(Gtk.ListBoxRow):
101+
def __init__(self, app_info, parent_settings):
102+
super().__init__()
103+
self.parent_settings = parent_settings
104+
self.set_activatable(True)
105+
self.set_selectable(False)
106+
self.set_can_focus(True)
107+
108+
self.app_name = app_info.get_name().lower()
109+
110+
# Sanitise app ID for GSettings path (this should remain the same as in ui/messageTray.js)
111+
# 1. Convert to lower case.
112+
# 2. Replace any one or more consecutive characters that is not a lowercase letter or a digit with a hyphen.
113+
# 3. Trim any leading or trailing hyphens.
114+
app_id = app_info.get_id().lower().replace(".desktop", "")
115+
self.settings_id = re.sub(r'[^a-z0-9]+', '-', app_id).strip('-')
116+
path = f"{PER_APP_BASE_PATH}{self.settings_id}/"
117+
118+
self.settings = Gio.Settings.new_with_path(PER_APP_SCHEMA, path)
119+
120+
hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=12)
121+
hbox.set_margin_start(8)
122+
hbox.set_margin_end(8)
123+
hbox.set_margin_top(4)
124+
hbox.set_margin_bottom(4)
125+
126+
# Icon
127+
gicon = app_info.get_icon()
128+
if not gicon:
129+
gicon = Gio.ThemedIcon.new("application-x-executable")
130+
icon = Gtk.Image.new_from_gicon(gicon, Gtk.IconSize.DND)
131+
icon.set_pixel_size(32)
132+
hbox.pack_start(icon, False, False, 0)
133+
134+
# Labels
135+
name_label = Gtk.Label(label=app_info.get_name(), xalign=0)
136+
name_label.set_ellipsize(Pango.EllipsizeMode.END)
137+
hbox.pack_start(name_label, True, True, 0)
138+
139+
# Switch
140+
self.switch = Gtk.Switch()
141+
self.switch.set_active(self.settings.get_boolean("enabled"))
142+
self.settings.bind("enabled", self.switch, "active", Gio.SettingsBindFlags.DEFAULT)
143+
self.settings.connect("changed::enabled", self.update_index)
144+
hbox.pack_start(self.switch, False, False, 0)
145+
146+
self.add(hbox)
147+
148+
def update_index(self, settings, key):
149+
current_children = list(self.parent_settings.get_strv("application-children"))
150+
151+
if self.settings.get_boolean("enabled"):
152+
# Since 'true' is the default, we can remove the custom setting from dconf
153+
if self.settings_id in current_children:
154+
current_children.remove(self.settings_id)
155+
self.parent_settings.set_strv("application-children", current_children)
156+
self.settings.reset("enabled")
157+
else:
158+
if self.settings_id not in current_children:
159+
current_children.append(self.settings_id)
160+
self.parent_settings.set_strv("application-children", current_children)
161+
162+
def toggle_switch(self):
163+
self.switch.set_active(not self.switch.get_active())
164+
165+
class AppNotificationsWindow(Gtk.Dialog):
166+
def __init__(self, parent):
167+
super().__init__(title=_("Application Notifications"), transient_for=parent)
168+
self.set_modal(True)
169+
self.set_destroy_with_parent(True)
170+
self.set_default_size(430, 480)
171+
self.set_border_width(10)
172+
173+
frame = Gtk.Frame()
174+
frame.set_border_width(6)
175+
frame.set_shadow_type(Gtk.ShadowType.IN)
176+
inner_vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=0)
177+
self.search_entry = Gtk.SearchEntry()
178+
self.search_entry.set_margin_start(16)
179+
self.search_entry.set_margin_end(16)
180+
self.search_entry.connect("search-changed", self.on_search_changed)
181+
scrolled = Gtk.ScrolledWindow()
182+
scrolled.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC)
183+
184+
self.listbox = Gtk.ListBox()
185+
self.listbox.set_selection_mode(Gtk.SelectionMode.NONE)
186+
self.listbox.connect("row-activated", self.on_row_activated)
187+
self.listbox.set_filter_func(self.filter_func)
188+
189+
self.parent_settings = Gio.Settings.new("org.cinnamon.desktop.notifications")
190+
apps = Gio.AppInfo.get_all()
191+
# Filter for unique apps that are not hidden
192+
seen_ids = set()
193+
for app in sorted(apps, key=lambda x: x.get_name()):
194+
app_id = app.get_id()
195+
if app.should_show() and app_id not in seen_ids and not app_id.startswith("cinnamon-settings-"):
196+
row = AppNotificationRow(app, self.parent_settings)
197+
self.listbox.add(row)
198+
seen_ids.add(app_id)
199+
200+
scrolled.add(self.listbox)
201+
inner_vbox.pack_start(self.search_entry, False, False, 6)
202+
inner_vbox.pack_start(scrolled, True, True, 0)
203+
frame.add(inner_vbox)
204+
content_area = self.get_content_area()
205+
content_area.pack_start(frame, True, True, 0)
206+
207+
reset_button = Gtk.Button(label=_("Reset All"))
208+
reset_button.connect("clicked", self.on_reset_all_clicked)
209+
self.add_action_widget(reset_button, Gtk.ResponseType.NONE)
210+
211+
self.show_all()
212+
213+
def on_row_activated(self, listbox, row):
214+
row.toggle_switch()
215+
216+
def filter_func(self, row):
217+
search_text = self.search_entry.get_text().lower()
218+
if not search_text:
219+
return True
220+
return search_text in row.app_name
221+
222+
def on_search_changed(self, entry):
223+
self.listbox.invalidate_filter()
224+
225+
def on_reset_all_clicked(self, button):
226+
overridden_apps = self.parent_settings.get_strv("application-children")
227+
if not overridden_apps:
228+
return
229+
230+
for app_id in overridden_apps:
231+
path = f"{PER_APP_BASE_PATH}{app_id}/"
232+
app_settings = Gio.Settings.new_with_path(PER_APP_SCHEMA, path)
233+
app_settings.reset("enabled")
234+
235+
self.parent_settings.set_strv("application-children", [])

js/ui/messageTray.js

Lines changed: 39 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -765,6 +765,7 @@ MessageTray.prototype = {
765765
this._notificationTimeoutId = 0;
766766
this._notificationExpandedId = 0;
767767
this._notificationRemoved = false;
768+
this._appSettingsCache = {};
768769

769770
this._sources = [];
770771
Main.layoutManager.addChrome(this._notificationBin);
@@ -859,7 +860,38 @@ MessageTray.prototype = {
859860
this._updateState();
860861
},
861862

863+
_isAppEnabled: function(source) {
864+
if (!source.app) return true;
865+
866+
let appId = source.app.get_id();
867+
if (appId.endsWith(":flatpak")) appId = appId.slice(0, -8);
868+
if (appId.endsWith(".desktop")) appId = appId.slice(0, -8);
869+
// Sanitise ID for GSettings path. (this should remain the same as in cs_notifications.py)
870+
// 1. Convert to lower case.
871+
// 2. Replace any one or more consecutive characters that is not a lowercase letter or a digit with a hyphen.
872+
// 3. Trim any leading or trailing hyphens.
873+
appId = appId.toLowerCase();
874+
const settingsId = appId.replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');
875+
876+
if (!this._appSettingsCache[settingsId]) {
877+
const path = `/org/cinnamon/desktop/notifications/application/${settingsId}/`;
878+
879+
this._appSettingsCache[settingsId] = new Gio.Settings({
880+
schema_id: "org.cinnamon.desktop.notifications.application",
881+
path: path
882+
});
883+
}
884+
885+
// The default for "enabled" key is true so this returns true if the path doesn't exist.
886+
return this._appSettingsCache[settingsId].get_boolean("enabled");
887+
},
888+
862889
_onNotify: function (source, notification) {
890+
if (!this._notificationsEnabled || !this._isAppEnabled(source)) {
891+
notification.destroy(NotificationDestroyedReason.DISMISSED);
892+
return;
893+
}
894+
863895
if (this._notification == notification) {
864896
// If a notification that is being shown is updated, we update
865897
// how it is shown and extend the time until it auto-hides.
@@ -900,28 +932,15 @@ MessageTray.prototype = {
900932
// _updateState() figures out what (if anything) needs to be done
901933
// at the present time.
902934
_updateState: function () {
903-
// Notifications
904-
let notificationUrgent = this._notificationQueue.length > 0 && this._notificationQueue[0].urgency == Urgency.CRITICAL;
905-
let notificationsPending = this._notificationQueue.length > 0 && (!this._busy || notificationUrgent);
906-
907-
let notificationExpired = (this._notificationTimeoutId == 0 &&
908-
!(this._notification && this._notification.urgency == Urgency.CRITICAL) &&
909-
!this._locked
910-
) || this._notificationRemoved;
911-
let canShowNotification = notificationsPending && this._notificationsEnabled;
912-
913-
if (this._notificationState == State.HIDDEN) {
914-
if (canShowNotification) {
935+
if (this._notificationState === State.HIDDEN && this._notificationQueue.length > 0) {
936+
if (!this._busy || this._notificationQueue[0].urgency === Urgency.CRITICAL) {
915937
this._showNotification();
916938
}
917-
else if (!this._notificationsEnabled) {
918-
if (notificationsPending) {
919-
this._notification = this._notificationQueue.shift();
920-
this._notification.destroy(NotificationDestroyedReason.DISMISSED);
921-
this._notification = null;
922-
}
923-
}
924-
} else if (this._notificationState == State.SHOWN) {
939+
} else if (this._notificationState === State.SHOWN) {
940+
const isCritical = this._notification && this._notification.urgency === Urgency.CRITICAL;
941+
const notificationExpired = (this._notificationTimeoutId === 0 &&
942+
!isCritical && !this._locked) || this._notificationRemoved;
943+
925944
if (notificationExpired)
926945
this._hideNotification();
927946
}

0 commit comments

Comments
 (0)