diff --git a/data/theme/gnome-shell-sass/widgets/_notifications.scss b/data/theme/gnome-shell-sass/widgets/_notifications.scss index a5497e20f..0601425ba 100644 --- a/data/theme/gnome-shell-sass/widgets/_notifications.scss +++ b/data/theme/gnome-shell-sass/widgets/_notifications.scss @@ -14,12 +14,12 @@ $notification_banner_width: 34em; @if $contrast == 'high' { @include draw_hc_inset(); } - - .notification-actions { - spacing: 0; - } - - .notification-button { - @extend %bubble_button; - } +} + +.notification-buttons-bin { + spacing: 0; +} + +.notification-button { + @extend %bubble_button; } diff --git a/js/ui/calendar.js b/js/ui/calendar.js index c0b62d540..dcf860651 100644 --- a/js/ui/calendar.js +++ b/js/ui/calendar.js @@ -18,6 +18,7 @@ import {formatDateWithCFormatString} from '../misc/dateUtils.js'; import {loadInterfaceXML} from '../misc/fileUtils.js'; const SHOW_WEEKDATE_KEY = 'show-weekdate'; +const MAX_NOTIFICATION_BUTTONS = 3; const NC_ = (context, str) => `${context}\u0004${str}`; @@ -782,11 +783,19 @@ class NotificationMessage extends MessageList.Message { }); notification.connectObject( 'updated', this._onUpdated.bind(this), + 'action-added', (_, action) => this._addAction(action), + 'action-removed', (_, action) => this._removeAction(action), + 'destroy', () => { this.notification = null; if (!this._closed) this.close(); }, this); + + this._actions = new Map(); + this.notification.actions.forEach(action => { + this._addAction(action); + }); } _getIcon() { @@ -815,6 +824,36 @@ class NotificationMessage extends MessageList.Message { canClose() { return true; } + + _addAction(action) { + if (!this._buttonBox) { + this._buttonBox = new St.BoxLayout({ + x_expand: true, + style_class: 'notification-buttons-bin', + }); + this.setActionArea(this._buttonBox); + global.focus_manager.add_group(this._buttonBox); + } + + if (this._buttonBox.get_n_children() >= MAX_NOTIFICATION_BUTTONS) + return; + + const button = new St.Button({ + style_class: 'notification-button', + x_expand: true, + label: action.label, + }); + + button.connect('clicked', () => action.activate()); + + this._actions.set(action, button); + this._buttonBox.add_child(button); + } + + _removeAction(action) { + this._actions.get(action)?.destroy(); + this._actions.delete(action); + } }); const NotificationSection = GObject.registerClass( diff --git a/js/ui/messageList.js b/js/ui/messageList.js index b5ee41cd7..37254f72e 100644 --- a/js/ui/messageList.js +++ b/js/ui/messageList.js @@ -469,7 +469,7 @@ export const Message = GObject.registerClass({ }); vbox.add_child(hbox); - this._actionBin = new St.Widget({ + this._actionBin = new St.Bin({ layout_manager: new ScaleLayout(), visible: false, }); @@ -557,17 +557,8 @@ export const Message = GObject.registerClass({ } setActionArea(actor) { - if (actor == null) { - if (this._actionBin.get_n_children() > 0) - this._actionBin.get_child_at_index(0).destroy(); - return; - } - - if (this._actionBin.get_n_children() > 0) - throw new Error('Message already has an action area'); - - this._actionBin.add_child(actor); - this._actionBin.visible = this.expanded; + this._actionBin.child = actor; + this._actionBin.visible = actor && this.expanded; } addMediaControl(iconName, callback) { @@ -583,7 +574,7 @@ export const Message = GObject.registerClass({ expand(animate) { this.expanded = true; - this._actionBin.visible = this._actionBin.get_n_children() > 0; + this._actionBin.visible = !!this._actionBin.child; const duration = animate ? MessageTray.ANIMATION_TIME : 0; this._bodyStack.ease_property('@layout.expansion', 1, { diff --git a/js/ui/messageTray.js b/js/ui/messageTray.js index 72db9579e..48731367a 100644 --- a/js/ui/messageTray.js +++ b/js/ui/messageTray.js @@ -27,7 +27,6 @@ const LONGER_HIDE_TIMEOUT = 600; const MAX_NOTIFICATIONS_IN_QUEUE = 3; const MAX_NOTIFICATIONS_PER_SOURCE = 3; -const MAX_NOTIFICATION_BUTTONS = 3; // We delay hiding of the tray if the mouse is within MOUSE_LEFT_ACTOR_THRESHOLD // range from the point where it left the tray. @@ -498,7 +497,18 @@ export const Notification = GObject.registerClass({ // @label: the label for the action's button // @callback: the callback for the action addAction(label, callback) { - const action = new Action(label, callback); + const action = new Action(label, () => { + callback(); + + // We don't hide a resident notification when the user invokes one of its actions, + // because it is common for such notifications to update themselves with new + // information based on the action. We'd like to display the updated information + // in place, rather than pop-up a new notification. + if (this.resident) + return; + + this.destroy(); + }); this.actions.push(action); this.emit('action-added', action); } @@ -557,8 +567,14 @@ export const Notification = GObject.registerClass({ activate() { this.emit('activated'); - if (!this.resident) - this.destroy(); + // We don't hide a resident notification when the user invokes one of its actions, + // because it is common for such notifications to update themselves with new + // information based on the action. We'd like to display the updated information + // in place, rather than pop-up a new notification. + if (this.resident) + return; + + this.destroy(); } destroy(reason = NotificationDestroyedReason.DISMISSED) { @@ -579,10 +595,6 @@ export const NotificationBanner = GObject.registerClass({ this.can_focus = false; this.add_style_class_name('notification-banner'); - this._buttonBox = null; - - this._addActions(); - this.notification.connectObject('activated', () => { // We hide all types of notifications once the user clicks on // them because the common outcome of clicking should be the @@ -591,64 +603,6 @@ export const NotificationBanner = GObject.registerClass({ this.emit('done-displaying'); }, this); } - - _onUpdated(n, clear) { - super._onUpdated(n, clear); - - if (clear) { - this.setActionArea(null); - this._buttonBox = null; - } - - this._addActions(); - } - - _addActions() { - this.notification.actions.forEach(action => { - this.addAction(action.label, () => action.activate()); - }); - } - - addButton(button, callback) { - if (!this._buttonBox) { - this._buttonBox = new St.BoxLayout({ - style_class: 'notification-actions', - x_expand: true, - }); - this.setActionArea(this._buttonBox); - global.focus_manager.add_group(this._buttonBox); - } - - if (this._buttonBox.get_n_children() >= MAX_NOTIFICATION_BUTTONS) - return null; - - this._buttonBox.add_child(button); - button.connect('clicked', () => { - callback(); - - if (!this.notification.resident) { - // We don't hide a resident notification when the user invokes one of its actions, - // because it is common for such notifications to update themselves with new - // information based on the action. We'd like to display the updated information - // in place, rather than pop-up a new notification. - this.emit('done-displaying'); - this.notification.destroy(); - } - }); - - return button; - } - - addAction(label, callback) { - const button = new St.Button({ - style_class: 'notification-button', - label, - x_expand: true, - can_focus: true, - }); - - return this.addButton(button, callback); - } }); export const Source = GObject.registerClass({