mirror of
https://github.com/edu4rdshl/gnome-shell.git
synced 2026-07-17 23:24:51 +00:00
Merge branch 'extend_notification_api' into 'main'
Draft: GtkNotificationDaemon: Extend API to include new portal features Closes #6983 See merge request GNOME/gnome-shell!3389
This commit is contained in:
commit
8e482af5d2
14 changed files with 264 additions and 154 deletions
|
|
@ -9,5 +9,12 @@
|
||||||
<arg name="app_id" type="s" direction="in"/>
|
<arg name="app_id" type="s" direction="in"/>
|
||||||
<arg name="id" type="s" direction="in"/>
|
<arg name="id" type="s" direction="in"/>
|
||||||
</method>
|
</method>
|
||||||
|
<signal name="ActionInvoked">
|
||||||
|
<arg name="app_id" type="s"/>
|
||||||
|
<arg name="id" type="s"/>
|
||||||
|
<arg name="action" type="s"/>
|
||||||
|
<arg name="parameter" type="av"/>
|
||||||
|
<arg name="platform_data" type="a{sv}"/>
|
||||||
|
</signal>
|
||||||
</interface>
|
</interface>
|
||||||
</node>
|
</node>
|
||||||
|
|
|
||||||
|
|
@ -1081,7 +1081,7 @@ export const LoginDialog = GObject.registerClass({
|
||||||
title: _('Login Attempt Timed Out'),
|
title: _('Login Attempt Timed Out'),
|
||||||
body: _('Login took too long, please try again'),
|
body: _('Login took too long, please try again'),
|
||||||
urgency: MessageTray.Urgency.CRITICAL,
|
urgency: MessageTray.Urgency.CRITICAL,
|
||||||
isTransient: true,
|
displayHint: MessageTray.DisplayHint.TRANSIENT,
|
||||||
});
|
});
|
||||||
this._conflictingSessionNotification.connect('destroy', () => {
|
this._conflictingSessionNotification.connect('destroy', () => {
|
||||||
this._conflictingSessionNotification = null;
|
this._conflictingSessionNotification = null;
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,10 @@
|
||||||
// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
|
// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
|
||||||
|
|
||||||
|
import * as MessageTray from './messageTray.js';
|
||||||
import Shell from 'gi://Shell';
|
import Shell from 'gi://Shell';
|
||||||
import * as ParentalControlsManager from '../misc/parentalControlsManager.js';
|
import * as ParentalControlsManager from '../misc/parentalControlsManager.js';
|
||||||
import * as Signals from '../misc/signals.js';
|
import * as Signals from '../misc/signals.js';
|
||||||
|
|
||||||
import * as Main from './main.js';
|
|
||||||
|
|
||||||
// In alphabetical order
|
// In alphabetical order
|
||||||
const RENAMED_DESKTOP_IDS = {
|
const RENAMED_DESKTOP_IDS = {
|
||||||
'baobab.desktop': 'org.gnome.baobab.desktop',
|
'baobab.desktop': 'org.gnome.baobab.desktop',
|
||||||
|
|
@ -161,13 +160,11 @@ class AppFavorites extends Signals.EventEmitter {
|
||||||
if (!this._addFavorite(appId, pos))
|
if (!this._addFavorite(appId, pos))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
let app = Shell.AppSystem.get_default().lookup_app(appId);
|
const app = Shell.AppSystem.get_default().lookup_app(appId);
|
||||||
|
|
||||||
let msg = _('%s has been pinned to the dash.').format(app.get_name());
|
this.showNotification(_('%s has been pinned to the dash.').format(app.get_name()),
|
||||||
Main.overview.setMessage(msg, {
|
null,
|
||||||
forFeedback: true,
|
() => this._removeFavorite(appId));
|
||||||
undoCallback: () => this._removeFavorite(appId),
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
addFavorite(appId) {
|
addFavorite(appId) {
|
||||||
|
|
@ -196,11 +193,21 @@ class AppFavorites extends Signals.EventEmitter {
|
||||||
if (!this._removeFavorite(appId))
|
if (!this._removeFavorite(appId))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
let msg = _('%s has been unpinned from the dash.').format(app.get_name());
|
this.showNotification(_('%s has been unpinned from the dash.').format(app.get_name()),
|
||||||
Main.overview.setMessage(msg, {
|
null,
|
||||||
forFeedback: true,
|
() => this._addFavorite(appId, pos));
|
||||||
undoCallback: () => this._addFavorite(appId, pos),
|
}
|
||||||
|
|
||||||
|
showNotification(title, body, undoCallback) {
|
||||||
|
const source = MessageTray.getSystemSource();
|
||||||
|
const notification = new MessageTray.Notification({
|
||||||
|
source,
|
||||||
|
title,
|
||||||
|
body,
|
||||||
|
displayHint: MessageTray.DisplayHint.TRANSIENT | MessageTray.DisplayHint.FORCE_IMMEDIATELY,
|
||||||
});
|
});
|
||||||
|
notification.addAction(_('Undo'), () => undoCallback());
|
||||||
|
source.addNotification(notification);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -801,6 +801,11 @@ class NotificationMessage extends MessageList.Message {
|
||||||
notification.bind_property('gicon',
|
notification.bind_property('gicon',
|
||||||
this, 'icon',
|
this, 'icon',
|
||||||
GObject.BindingFlags.SYNC_CREATE);
|
GObject.BindingFlags.SYNC_CREATE);
|
||||||
|
notification.bind_property_full('display-hint',
|
||||||
|
this, 'can-close',
|
||||||
|
GObject.BindingFlags.SYNC_CREATE,
|
||||||
|
(bind, value) => [true, !(value & MessageTray.DisplayHint.PERSISTENT)],
|
||||||
|
null);
|
||||||
|
|
||||||
this._actions = new Map();
|
this._actions = new Map();
|
||||||
this.notification.actions.forEach(action => {
|
this.notification.actions.forEach(action => {
|
||||||
|
|
@ -812,10 +817,6 @@ class NotificationMessage extends MessageList.Message {
|
||||||
this.notification.activate();
|
this.notification.activate();
|
||||||
}
|
}
|
||||||
|
|
||||||
canClose() {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
_addAction(action) {
|
_addAction(action) {
|
||||||
if (!this._buttonBox) {
|
if (!this._buttonBox) {
|
||||||
this._buttonBox = new St.BoxLayout({
|
this._buttonBox = new St.BoxLayout({
|
||||||
|
|
|
||||||
|
|
@ -278,7 +278,7 @@ async function _initializeUI() {
|
||||||
source,
|
source,
|
||||||
title: _('System was put in unsafe mode'),
|
title: _('System was put in unsafe mode'),
|
||||||
body: _('Apps now have unrestricted access'),
|
body: _('Apps now have unrestricted access'),
|
||||||
isTransient: true,
|
displayHint: MessageTray.DisplayHint.TRANSIENT,
|
||||||
});
|
});
|
||||||
notification.addAction(_('Undo'),
|
notification.addAction(_('Undo'),
|
||||||
() => (global.context.unsafe_mode = false));
|
() => (global.context.unsafe_mode = false));
|
||||||
|
|
@ -620,7 +620,7 @@ export function notify(msg, details) {
|
||||||
source,
|
source,
|
||||||
title: msg,
|
title: msg,
|
||||||
body: details,
|
body: details,
|
||||||
isTransient: true,
|
displayHint: MessageTray.DisplayHint.TRANSIENT,
|
||||||
});
|
});
|
||||||
source.addNotification(notification);
|
source.addNotification(notification);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -405,6 +405,10 @@ export const Message = GObject.registerClass({
|
||||||
'datetime', 'datetime', 'datetime',
|
'datetime', 'datetime', 'datetime',
|
||||||
GObject.ParamFlags.READWRITE,
|
GObject.ParamFlags.READWRITE,
|
||||||
GLib.DateTime),
|
GLib.DateTime),
|
||||||
|
'can-close': GObject.ParamSpec.boolean(
|
||||||
|
'can-close', 'can-close', 'can-close',
|
||||||
|
GObject.ParamFlags.READWRITE,
|
||||||
|
false),
|
||||||
},
|
},
|
||||||
Signals: {
|
Signals: {
|
||||||
'close': {},
|
'close': {},
|
||||||
|
|
@ -487,7 +491,8 @@ export const Message = GObject.registerClass({
|
||||||
this.connect('destroy', this._onDestroy.bind(this));
|
this.connect('destroy', this._onDestroy.bind(this));
|
||||||
|
|
||||||
this._header.closeButton.connect('clicked', this.close.bind(this));
|
this._header.closeButton.connect('clicked', this.close.bind(this));
|
||||||
this._header.closeButton.visible = this.canClose();
|
|
||||||
|
this.bind_property('can-close', this._header.closeButton, 'visible', GObject.BindingFlags.SYNC_CREATE);
|
||||||
|
|
||||||
this._header.expandButton.connect('clicked', () => {
|
this._header.expandButton.connect('clicked', () => {
|
||||||
if (this.expanded)
|
if (this.expanded)
|
||||||
|
|
@ -633,10 +638,6 @@ export const Message = GObject.registerClass({
|
||||||
this.emit('unexpanded');
|
this.emit('unexpanded');
|
||||||
}
|
}
|
||||||
|
|
||||||
canClose() {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
_onDestroy() {
|
_onDestroy() {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -646,7 +647,7 @@ export const Message = GObject.registerClass({
|
||||||
if (keysym === Clutter.KEY_Delete ||
|
if (keysym === Clutter.KEY_Delete ||
|
||||||
keysym === Clutter.KEY_KP_Delete ||
|
keysym === Clutter.KEY_KP_Delete ||
|
||||||
keysym === Clutter.KEY_BackSpace) {
|
keysym === Clutter.KEY_BackSpace) {
|
||||||
if (this.canClose()) {
|
if (this.canClose) {
|
||||||
this.close();
|
this.close();
|
||||||
return Clutter.EVENT_STOP;
|
return Clutter.EVENT_STOP;
|
||||||
}
|
}
|
||||||
|
|
@ -816,7 +817,7 @@ export const MessageListSection = GObject.registerClass({
|
||||||
}
|
}
|
||||||
|
|
||||||
clear() {
|
clear() {
|
||||||
let messages = this._messages.filter(msg => msg.canClose());
|
let messages = this._messages.filter(msg => msg.canClose);
|
||||||
|
|
||||||
// If there are few messages, letting them all zoom out looks OK
|
// If there are few messages, letting them all zoom out looks OK
|
||||||
if (messages.length < 2) {
|
if (messages.length < 2) {
|
||||||
|
|
@ -854,7 +855,7 @@ export const MessageListSection = GObject.registerClass({
|
||||||
this.notify('empty');
|
this.notify('empty');
|
||||||
}
|
}
|
||||||
|
|
||||||
let canClear = messages.some(m => m.canClose());
|
let canClear = messages.some(m => m.canClose);
|
||||||
if (this._canClear !== canClear) {
|
if (this._canClear !== canClear) {
|
||||||
this._canClear = canClear;
|
this._canClear = canClear;
|
||||||
this.notify('can-clear');
|
this.notify('can-clear');
|
||||||
|
|
|
||||||
|
|
@ -78,6 +78,25 @@ export const PrivacyScope = {
|
||||||
SYSTEM: 1,
|
SYSTEM: 1,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** @enum {number} */
|
||||||
|
export const DisplayHint = {
|
||||||
|
NONE: 0,
|
||||||
|
// Only show a banner for the message.
|
||||||
|
TRANSIENT: 1 << 0,
|
||||||
|
// Don't show a banner but keep the message till dismissed.
|
||||||
|
NO_BANNER: 1 << 1,
|
||||||
|
// Show a banner immediately.
|
||||||
|
FORCE_IMMEDIATELY: 1 << 2,
|
||||||
|
// Message that can't be dismissed by the user.
|
||||||
|
PERSISTENT: 1 << 3,
|
||||||
|
// Message that won't automatically dismissed when an action is invoked.
|
||||||
|
RESIDENT: 1 << 4,
|
||||||
|
// Show on the lock screen that a source as a message, but without any content.
|
||||||
|
SHOW_ON_LOCKSCREEN: 1 << 5,
|
||||||
|
// Show the message on the lock screen.
|
||||||
|
SHOW_CONTENT_ON_LOCKSCREEN: 1 << 6,
|
||||||
|
};
|
||||||
|
|
||||||
class FocusGrabber {
|
class FocusGrabber {
|
||||||
constructor(actor) {
|
constructor(actor) {
|
||||||
this._actor = actor;
|
this._actor = actor;
|
||||||
|
|
@ -314,11 +333,43 @@ export const NotificationApplicationPolicy = GObject.registerClass({
|
||||||
|
|
||||||
export const Sound = GObject.registerClass(
|
export const Sound = GObject.registerClass(
|
||||||
class Sound extends GObject.Object {
|
class Sound extends GObject.Object {
|
||||||
constructor(file, themedName) {
|
static newForFile(file) {
|
||||||
super();
|
const sound = new Sound();
|
||||||
|
sound._soundFile = file;
|
||||||
|
return sound;
|
||||||
|
}
|
||||||
|
|
||||||
this._soundFile = file;
|
static newForName(name) {
|
||||||
this._soundName = themedName;
|
const sound = new Sound();
|
||||||
|
sound._soundName = name;
|
||||||
|
return sound;
|
||||||
|
}
|
||||||
|
|
||||||
|
static newForBytes(bytes) {
|
||||||
|
const sound = new Sound();
|
||||||
|
sound._soundBytes = bytes;
|
||||||
|
return sound;
|
||||||
|
}
|
||||||
|
|
||||||
|
static deserialize(serializedSound) {
|
||||||
|
const sound = serializedSound?.unpack();
|
||||||
|
|
||||||
|
if (!sound || sound === 'silent')
|
||||||
|
return null;
|
||||||
|
|
||||||
|
if (sound === 'default')
|
||||||
|
return Sound.newForName('message');
|
||||||
|
|
||||||
|
if (sound.length !== 2)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
const [type, data] = sound.map(v => v.unpack());
|
||||||
|
if (type === 'file')
|
||||||
|
return Sound.newForFile(Gio.File.new_for_commandline_arg(data.unpack()));
|
||||||
|
if (type === 'bytes')
|
||||||
|
return Sound.newForBytes(data.unpack());
|
||||||
|
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
play() {
|
play() {
|
||||||
|
|
@ -328,6 +379,22 @@ class Sound extends GObject.Object {
|
||||||
player.play_from_theme(this._soundName, _('Notification sound'), null);
|
player.play_from_theme(this._soundName, _('Notification sound'), null);
|
||||||
else if (this._soundFile)
|
else if (this._soundFile)
|
||||||
player.play_from_file(this._soundFile, _('Notification sound'), null);
|
player.play_from_file(this._soundFile, _('Notification sound'), null);
|
||||||
|
else if (this._soundBytes)
|
||||||
|
this._playSoundBytes();
|
||||||
|
}
|
||||||
|
|
||||||
|
async _playSoundBytes() {
|
||||||
|
const player = global.display.get_sound_player();
|
||||||
|
const [file, stream] = Gio.File.new_tmp('XXXXXX-notification-sound');
|
||||||
|
await stream.output_stream.write_bytes_async(this._soundBytes,
|
||||||
|
GLib.PRIORITY_DEFAULT, null);
|
||||||
|
stream.close_async(GLib.PRIORITY_DEFAULT, null);
|
||||||
|
player.play_from_file(file, _('Notification sound'), null);
|
||||||
|
this._tempFile = file;
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanupTempFile() {
|
||||||
|
this._tempFile?.delete(null);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -408,6 +475,19 @@ export class Notification extends GObject.Object {
|
||||||
this.notify('privacy-scope');
|
this.notify('privacy-scope');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get displayHint() {
|
||||||
|
return this._displayHint;
|
||||||
|
}
|
||||||
|
|
||||||
|
set displayHint(displayHint) {
|
||||||
|
// TODO: check if the value is in range
|
||||||
|
if (this._displayHint === displayHint)
|
||||||
|
return;
|
||||||
|
|
||||||
|
this._displayHint = displayHint;
|
||||||
|
this.notify('display-hint');
|
||||||
|
}
|
||||||
|
|
||||||
get urgency() {
|
get urgency() {
|
||||||
return this._urgency;
|
return this._urgency;
|
||||||
}
|
}
|
||||||
|
|
@ -434,7 +514,7 @@ export class Notification extends GObject.Object {
|
||||||
// because it is common for such notifications to update themselves with new
|
// 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
|
// information based on the action. We'd like to display the updated information
|
||||||
// in place, rather than pop-up a new notification.
|
// in place, rather than pop-up a new notification.
|
||||||
if (this.resident)
|
if (this.displayHint & DisplayHint.RESIDENT)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
this.destroy();
|
this.destroy();
|
||||||
|
|
@ -467,13 +547,14 @@ export class Notification extends GObject.Object {
|
||||||
// because it is common for such notifications to update themselves with new
|
// 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
|
// information based on the action. We'd like to display the updated information
|
||||||
// in place, rather than pop-up a new notification.
|
// in place, rather than pop-up a new notification.
|
||||||
if (this.resident)
|
if (this.displayHint & DisplayHint.RESIDENT)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
this.destroy();
|
this.destroy();
|
||||||
}
|
}
|
||||||
|
|
||||||
destroy(reason = NotificationDestroyedReason.DISMISSED) {
|
destroy(reason = NotificationDestroyedReason.DISMISSED) {
|
||||||
|
this.sound?.cleanupTempFile();
|
||||||
this.emit('destroy', reason);
|
this.emit('destroy', reason);
|
||||||
|
|
||||||
if (this._updateDatetimeId)
|
if (this._updateDatetimeId)
|
||||||
|
|
@ -570,13 +651,16 @@ export const Source = GObject.registerClass({
|
||||||
this.countUpdated();
|
this.countUpdated();
|
||||||
|
|
||||||
// If acknowledged was set to false try to show the notification again
|
// If acknowledged was set to false try to show the notification again
|
||||||
if (!notification.acknowledged)
|
if (!notification.acknowledged && !(notification.displayHint & DisplayHint.NO_BANNER))
|
||||||
this.emit('notification-request-banner', notification);
|
this.emit('notification-request-banner', notification);
|
||||||
});
|
});
|
||||||
this.notifications.push(notification);
|
this.notifications.push(notification);
|
||||||
|
|
||||||
this.emit('notification-added', notification);
|
this.emit('notification-added', notification);
|
||||||
this.emit('notification-request-banner', notification);
|
|
||||||
|
if (!(notification.displayHint & DisplayHint.NO_BANNER))
|
||||||
|
this.emit('notification-request-banner', notification);
|
||||||
|
|
||||||
this.countUpdated();
|
this.countUpdated();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -597,13 +681,6 @@ export const Source = GObject.registerClass({
|
||||||
// To be overridden by subclasses
|
// To be overridden by subclasses
|
||||||
open() {
|
open() {
|
||||||
}
|
}
|
||||||
|
|
||||||
destroyNonResidentNotifications() {
|
|
||||||
for (let i = this.notifications.length - 1; i >= 0; i--) {
|
|
||||||
if (!this.notifications[i].resident)
|
|
||||||
this.notifications[i].destroy();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
SignalTracker.registerDestroyableType(Source);
|
SignalTracker.registerDestroyableType(Source);
|
||||||
|
|
||||||
|
|
@ -669,6 +746,11 @@ GObject.registerClass({
|
||||||
'is-transient', 'is-transient', 'is-transient',
|
'is-transient', 'is-transient', 'is-transient',
|
||||||
GObject.ParamFlags.READWRITE,
|
GObject.ParamFlags.READWRITE,
|
||||||
false),
|
false),
|
||||||
|
'display-hint': GObject.ParamSpec.int(
|
||||||
|
'display-hint', 'display-hint', 'display-hint',
|
||||||
|
GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT,
|
||||||
|
0, GLib.MAXINT32,
|
||||||
|
DisplayHint.NONE),
|
||||||
},
|
},
|
||||||
Signals: {
|
Signals: {
|
||||||
'action-added': {param_types: [Action]},
|
'action-added': {param_types: [Action]},
|
||||||
|
|
@ -1063,7 +1145,9 @@ export const MessageTray = GObject.registerClass({
|
||||||
let nextNotification = this._notificationQueue[0] || null;
|
let nextNotification = this._notificationQueue[0] || null;
|
||||||
if (hasNotifications && nextNotification) {
|
if (hasNotifications && nextNotification) {
|
||||||
let limited = this._busy || Main.layoutManager.primaryMonitor.inFullscreen;
|
let limited = this._busy || Main.layoutManager.primaryMonitor.inFullscreen;
|
||||||
let showNextNotification = !limited || nextNotification.forFeedback || nextNotification.urgency === Urgency.CRITICAL;
|
let showNextNotification = !limited ||
|
||||||
|
nextNotification.displayHint & DisplayHint.FORCE_IMMEDIATELY ||
|
||||||
|
nextNotification.urgency === Urgency.CRITICAL;
|
||||||
if (showNextNotification)
|
if (showNextNotification)
|
||||||
this._showNotification();
|
this._showNotification();
|
||||||
}
|
}
|
||||||
|
|
@ -1254,7 +1338,7 @@ export const MessageTray = GObject.registerClass({
|
||||||
_hideNotificationCompleted() {
|
_hideNotificationCompleted() {
|
||||||
let notification = this._notification;
|
let notification = this._notification;
|
||||||
this._notification = null;
|
this._notification = null;
|
||||||
if (!this._notificationRemoved && notification.isTransient)
|
if (!this._notificationRemoved && notification.displayHint & DisplayHint.TRANSIENT)
|
||||||
notification.destroy(NotificationDestroyedReason.EXPIRED);
|
notification.destroy(NotificationDestroyedReason.EXPIRED);
|
||||||
|
|
||||||
this._pointerInNotification = false;
|
this._pointerInNotification = false;
|
||||||
|
|
|
||||||
|
|
@ -197,16 +197,18 @@ class FdoNotificationDaemon {
|
||||||
|
|
||||||
const gicon = this._imageForNotificationData(hints);
|
const gicon = this._imageForNotificationData(hints);
|
||||||
|
|
||||||
const soundFile = 'sound-file' in hints
|
let sound;
|
||||||
? Gio.File.new_for_path(hints['sound-file']) : null;
|
if ('sound-file' in hints)
|
||||||
|
sound = MessageTray.Sound.newForFile(Gio.File.new_for_path(hints['sound-file']));
|
||||||
|
else if ('sound-name' in hints)
|
||||||
|
sound = MessageTray.Sound.newForName(hints['sound-name']);
|
||||||
|
|
||||||
notification.set({
|
notification.set({
|
||||||
title: summary,
|
title: summary,
|
||||||
body,
|
body,
|
||||||
gicon,
|
gicon,
|
||||||
useBodyMarkup: true,
|
useBodyMarkup: true,
|
||||||
sound: new MessageTray.Sound(soundFile, hints['sound-name']),
|
sound,
|
||||||
acknowledged: false,
|
|
||||||
});
|
});
|
||||||
notification.clearActions();
|
notification.clearActions();
|
||||||
|
|
||||||
|
|
@ -248,15 +250,20 @@ class FdoNotificationDaemon {
|
||||||
notification.urgency = MessageTray.Urgency.CRITICAL;
|
notification.urgency = MessageTray.Urgency.CRITICAL;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
notification.resident = !!hints.resident;
|
|
||||||
|
if (hints.resident)
|
||||||
|
notification.displayHint |= MessageTray.DisplayHint.RESIDENT;
|
||||||
|
|
||||||
// 'transient' is a reserved keyword in JS, so we have to retrieve the value
|
// 'transient' is a reserved keyword in JS, so we have to retrieve the value
|
||||||
// of the 'transient' hint with hints['transient'] rather than hints.transient
|
// of the 'transient' hint with hints['transient'] rather than hints.transient
|
||||||
notification.isTransient = !!hints['transient'];
|
if (hints['transient'])
|
||||||
|
notification.displayHint |= MessageTray.DisplayHint.TRANSIENT;
|
||||||
|
|
||||||
let privacyScope = hints['x-gnome-privacy-scope'] || 'user';
|
if (hints['x-gnome-privacy-scope'] === 'system')
|
||||||
notification.privacyScope = privacyScope === 'system'
|
notification.displayHint |= MessageTray.DisplayHint.SHOW_CONTENT_ON_LOCKSCREEN;
|
||||||
? MessageTray.PrivacyScope.SYSTEM
|
else
|
||||||
: MessageTray.PrivacyScope.USER;
|
// Always show the notification on the lockscreen but without any content
|
||||||
|
notification.displayHint |= MessageTray.DisplayHint.SHOW_ON_LOCKSCREEN;
|
||||||
|
|
||||||
// Only fallback to 'app-icon' when the source doesn't have a valid app
|
// Only fallback to 'app-icon' when the source doesn't have a valid app
|
||||||
const sourceGIcon = source.app ? null : this._iconForNotificationData(appIcon);
|
const sourceGIcon = source.app ? null : this._iconForNotificationData(appIcon);
|
||||||
|
|
@ -357,18 +364,11 @@ class FdoNotificationDaemonSource extends MessageTray.Source {
|
||||||
this.notify('icon');
|
this.notify('icon');
|
||||||
}
|
}
|
||||||
|
|
||||||
let tracker = Shell.WindowTracker.get_default();
|
|
||||||
// Acknowledge notifications that are resident and their app has the
|
|
||||||
// current focus so that we don't show a banner.
|
|
||||||
if (notification.resident && this.app && tracker.focus_app === this.app)
|
|
||||||
notification.acknowledged = true;
|
|
||||||
|
|
||||||
this.addNotification(notification);
|
this.addNotification(notification);
|
||||||
}
|
}
|
||||||
|
|
||||||
open() {
|
open() {
|
||||||
this.openApp();
|
this.openApp();
|
||||||
this.destroyNonResidentNotifications();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
openApp() {
|
openApp() {
|
||||||
|
|
@ -409,31 +409,41 @@ const GtkNotificationDaemonNotification = GObject.registerClass(
|
||||||
class GtkNotificationDaemonNotification extends MessageTray.Notification {
|
class GtkNotificationDaemonNotification extends MessageTray.Notification {
|
||||||
constructor(source, id, notification) {
|
constructor(source, id, notification) {
|
||||||
super({source});
|
super({source});
|
||||||
this._serialized = GLib.Variant.new('a{sv}', notification);
|
|
||||||
this.id = id;
|
|
||||||
|
|
||||||
|
this.id = id;
|
||||||
|
this.update(notification);
|
||||||
|
}
|
||||||
|
|
||||||
|
update(notification) {
|
||||||
const {
|
const {
|
||||||
title,
|
title,
|
||||||
body,
|
body,
|
||||||
|
'markup-body': markupBody,
|
||||||
icon: gicon,
|
icon: gicon,
|
||||||
urgent,
|
urgent,
|
||||||
priority,
|
priority,
|
||||||
buttons,
|
buttons,
|
||||||
|
sound,
|
||||||
'default-action': defaultAction,
|
'default-action': defaultAction,
|
||||||
'default-action-target': defaultActionTarget,
|
'default-action-target': defaultActionTarget,
|
||||||
|
'display-hint': displayHint,
|
||||||
timestamp: time,
|
timestamp: time,
|
||||||
} = notification;
|
} = notification;
|
||||||
|
|
||||||
if (priority) {
|
const urgency = PRIORITY_URGENCY_MAP[priority?.unpack()] ??
|
||||||
let urgency = PRIORITY_URGENCY_MAP[priority.unpack()];
|
urgent?.unpack() ? MessageTray.Urgency.CRITICAL : MessageTray.Urgency.NORMAL;
|
||||||
this.urgency = urgency !== undefined ? urgency : MessageTray.Urgency.NORMAL;
|
|
||||||
} else if (urgent) {
|
this.set({
|
||||||
this.urgency = urgent.unpack()
|
title: title.unpack(),
|
||||||
? MessageTray.Urgency.CRITICAL
|
body: markupBody?.unpack() ?? body?.unpack() ?? null,
|
||||||
: MessageTray.Urgency.NORMAL;
|
useBodyMarkup: !!markupBody,
|
||||||
} else {
|
gicon: gicon
|
||||||
this.urgency = MessageTray.Urgency.NORMAL;
|
? Gio.icon_deserialize(gicon) : null,
|
||||||
}
|
sound: sound ? MessageTray.Sound.deserialize(sound) : null,
|
||||||
|
datetime: time
|
||||||
|
? GLib.DateTime.new_from_unix_local(time.unpack()) : null,
|
||||||
|
urgency,
|
||||||
|
});
|
||||||
|
|
||||||
if (buttons) {
|
if (buttons) {
|
||||||
buttons.deepUnpack().forEach(button => {
|
buttons.deepUnpack().forEach(button => {
|
||||||
|
|
@ -443,37 +453,55 @@ class GtkNotificationDaemonNotification extends MessageTray.Notification {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let showOnLockscreen = true;
|
||||||
|
let showContentOnLockscreen = true;
|
||||||
|
if (displayHint) {
|
||||||
|
for (const hint of displayHint.deepUnpack()) {
|
||||||
|
if (hint === 'transient')
|
||||||
|
this.displayHint |= MessageTray.DisplayHint.TRANSIENT;
|
||||||
|
else if (hint === 'tray')
|
||||||
|
this.displayHint |= MessageTray.DisplayHint.NO_BANNER;
|
||||||
|
else if (hint === 'persistent')
|
||||||
|
this.displayHint |= MessageTray.DisplayHint.PERSISTENT;
|
||||||
|
else if (hint === 'resident')
|
||||||
|
this.displayHint |= MessageTray.DisplayHint.RESIDENT;
|
||||||
|
else if (hint === 'hide-on-lockscreen')
|
||||||
|
showOnLockscreen = false;
|
||||||
|
else if (hint === 'hide-content-on-lockscreen')
|
||||||
|
showContentOnLockscreen = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showOnLockscreen) {
|
||||||
|
if (showContentOnLockscreen)
|
||||||
|
this.displayHint |= MessageTray.DisplayHint.SHOW_CONTENT_ON_LOCKSCREEN;
|
||||||
|
else
|
||||||
|
this.displayHint |= MessageTray.DisplayHint.SHOW_ON_LOCKSCREEN;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._serialized = GLib.Variant.new('a{sv}', notification);
|
||||||
this._defaultAction = defaultAction?.unpack();
|
this._defaultAction = defaultAction?.unpack();
|
||||||
this._defaultActionTarget = defaultActionTarget;
|
this._defaultActionTarget = defaultActionTarget;
|
||||||
|
|
||||||
this.set({
|
|
||||||
title: title.unpack(),
|
|
||||||
body: body?.unpack(),
|
|
||||||
gicon: gicon
|
|
||||||
? Gio.icon_deserialize(gicon) : null,
|
|
||||||
datetime: time
|
|
||||||
? GLib.DateTime.new_from_unix_local(time.unpack()) : null,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_activateAction(namespacedActionId, target) {
|
_activateAction(actionId, target) {
|
||||||
if (namespacedActionId) {
|
if (actionId.startsWith('app.'))
|
||||||
if (namespacedActionId.startsWith('app.')) {
|
this.source.activateAction(actionId.slice('app.'.length), target);
|
||||||
let actionId = namespacedActionId.slice('app.'.length);
|
else
|
||||||
this.source.activateAction(actionId, target);
|
this.source.emitActionInvoked(this.id, actionId, target);
|
||||||
}
|
|
||||||
} else {
|
|
||||||
this.source.open();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_onButtonClicked(button) {
|
_onButtonClicked(button) {
|
||||||
let {action, target} = button;
|
const {action, target} = button;
|
||||||
this._activateAction(action.unpack(), target);
|
this._activateAction(action.unpack(), target);
|
||||||
}
|
}
|
||||||
|
|
||||||
activate() {
|
activate() {
|
||||||
this._activateAction(this._defaultAction, this._defaultActionTarget);
|
if (this._defaultAction)
|
||||||
|
this._activateAction(this._defaultAction, this._defaultActionTarget);
|
||||||
|
else
|
||||||
|
this.source.open();
|
||||||
|
|
||||||
super.activate();
|
super.activate();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -486,7 +514,7 @@ function InvalidAppError() {}
|
||||||
|
|
||||||
export const GtkNotificationDaemonAppSource = GObject.registerClass(
|
export const GtkNotificationDaemonAppSource = GObject.registerClass(
|
||||||
class GtkNotificationDaemonAppSource extends MessageTray.Source {
|
class GtkNotificationDaemonAppSource extends MessageTray.Source {
|
||||||
constructor(appId) {
|
constructor(appId, dbusImpl) {
|
||||||
if (!Gio.Application.id_is_valid(appId))
|
if (!Gio.Application.id_is_valid(appId))
|
||||||
throw new InvalidAppError();
|
throw new InvalidAppError();
|
||||||
|
|
||||||
|
|
@ -502,6 +530,7 @@ class GtkNotificationDaemonAppSource extends MessageTray.Source {
|
||||||
|
|
||||||
this._appId = appId;
|
this._appId = appId;
|
||||||
this._app = app;
|
this._app = app;
|
||||||
|
this._dbusImpl = dbusImpl;
|
||||||
|
|
||||||
this._notifications = {};
|
this._notifications = {};
|
||||||
this._notificationPending = false;
|
this._notificationPending = false;
|
||||||
|
|
@ -517,6 +546,22 @@ class GtkNotificationDaemonAppSource extends MessageTray.Source {
|
||||||
Main.panel.closeCalendar();
|
Main.panel.closeCalendar();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
emitActionInvoked(notificationId, actionId, target) {
|
||||||
|
const context = global.create_app_launch_context(0, -1);
|
||||||
|
const info = this._app.get_app_info();
|
||||||
|
const token = context.get_startup_notify_id(info, []);
|
||||||
|
|
||||||
|
this._dbusImpl.emit_signal('ActionInvoked',
|
||||||
|
GLib.Variant.new('(sssava{sv})', [
|
||||||
|
this._appId,
|
||||||
|
notificationId,
|
||||||
|
actionId,
|
||||||
|
target ? [target] : [],
|
||||||
|
{'activation-token': GLib.Variant.new_string(token)},
|
||||||
|
])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
open() {
|
open() {
|
||||||
this._app.activate();
|
this._app.activate();
|
||||||
Main.overview.hide();
|
Main.overview.hide();
|
||||||
|
|
@ -566,11 +611,11 @@ class GtkNotificationDaemon {
|
||||||
constructor() {
|
constructor() {
|
||||||
this._sources = {};
|
this._sources = {};
|
||||||
|
|
||||||
this._loadNotifications();
|
|
||||||
|
|
||||||
this._dbusImpl = Gio.DBusExportedObject.wrapJSObject(GtkNotificationsIface, this);
|
this._dbusImpl = Gio.DBusExportedObject.wrapJSObject(GtkNotificationsIface, this);
|
||||||
this._dbusImpl.export(Gio.DBus.session, '/org/gtk/Notifications');
|
this._dbusImpl.export(Gio.DBus.session, '/org/gtk/Notifications');
|
||||||
|
|
||||||
|
this._loadNotifications();
|
||||||
|
|
||||||
Gio.DBus.session.own_name('org.gtk.Notifications', Gio.BusNameOwnerFlags.REPLACE, null, null);
|
Gio.DBus.session.own_name('org.gtk.Notifications', Gio.BusNameOwnerFlags.REPLACE, null, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -578,7 +623,7 @@ class GtkNotificationDaemon {
|
||||||
if (this._sources[appId])
|
if (this._sources[appId])
|
||||||
return this._sources[appId];
|
return this._sources[appId];
|
||||||
|
|
||||||
let source = new GtkNotificationDaemonAppSource(appId);
|
const source = new GtkNotificationDaemonAppSource(appId, this._dbusImpl);
|
||||||
|
|
||||||
source.connect('destroy', () => {
|
source.connect('destroy', () => {
|
||||||
delete this._sources[appId];
|
delete this._sources[appId];
|
||||||
|
|
@ -656,13 +701,22 @@ class GtkNotificationDaemon {
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
|
|
||||||
let timestamp = GLib.DateTime.new_now_local().to_unix();
|
const timestamp = GLib.DateTime.new_now_local().to_unix();
|
||||||
notificationSerialized['timestamp'] = new GLib.Variant('x', timestamp);
|
notificationSerialized['timestamp'] = new GLib.Variant('x', timestamp);
|
||||||
|
const showAsNew = notificationSerialized['display-hint']?.deepUnpack().some(hint => hint === 'show-as-new');
|
||||||
|
const oldNotification = source._notifications[notificationId];
|
||||||
|
|
||||||
const notification = new GtkNotificationDaemonNotification(source,
|
if (showAsNew || !oldNotification) {
|
||||||
notificationId,
|
const notification = new GtkNotificationDaemonNotification(
|
||||||
notificationSerialized);
|
source,
|
||||||
source.addNotification(notification);
|
notificationId,
|
||||||
|
notificationSerialized
|
||||||
|
);
|
||||||
|
|
||||||
|
source.addNotification(notification);
|
||||||
|
} else {
|
||||||
|
oldNotification.update(notificationSerialized);
|
||||||
|
}
|
||||||
|
|
||||||
invocation.return_value(null);
|
invocation.return_value(null);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,9 +16,7 @@ export const ANIMATION_TIME = 250;
|
||||||
import * as DND from './dnd.js';
|
import * as DND from './dnd.js';
|
||||||
import * as LayoutManager from './layout.js';
|
import * as LayoutManager from './layout.js';
|
||||||
import * as Main from './main.js';
|
import * as Main from './main.js';
|
||||||
import * as MessageTray from './messageTray.js';
|
|
||||||
import * as OverviewControls from './overviewControls.js';
|
import * as OverviewControls from './overviewControls.js';
|
||||||
import * as Params from '../misc/params.js';
|
|
||||||
import * as SwipeTracker from './swipeTracker.js';
|
import * as SwipeTracker from './swipeTracker.js';
|
||||||
import * as WindowManager from './windowManager.js';
|
import * as WindowManager from './windowManager.js';
|
||||||
import * as WorkspaceThumbnail from './workspaceThumbnail.js';
|
import * as WorkspaceThumbnail from './workspaceThumbnail.js';
|
||||||
|
|
@ -27,36 +25,6 @@ const DND_WINDOW_SWITCH_TIMEOUT = 750;
|
||||||
|
|
||||||
const OVERVIEW_ACTIVATION_TIMEOUT = 0.5;
|
const OVERVIEW_ACTIVATION_TIMEOUT = 0.5;
|
||||||
|
|
||||||
class ShellInfo {
|
|
||||||
setMessage(title, options) {
|
|
||||||
options = Params.parse(options, {
|
|
||||||
undoCallback: null,
|
|
||||||
forFeedback: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
const source = MessageTray.getSystemSource();
|
|
||||||
let undoCallback = options.undoCallback;
|
|
||||||
let forFeedback = options.forFeedback;
|
|
||||||
|
|
||||||
if (!this._notification) {
|
|
||||||
this._notification = new MessageTray.Notification({
|
|
||||||
source,
|
|
||||||
isTransient: true,
|
|
||||||
forFeedback,
|
|
||||||
});
|
|
||||||
this._notification.connect('destroy', () => delete this._notification);
|
|
||||||
}
|
|
||||||
this._notification.set({title});
|
|
||||||
|
|
||||||
this._notification.clearActions();
|
|
||||||
|
|
||||||
if (undoCallback)
|
|
||||||
this._notification.addAction(_('Undo'), () => undoCallback());
|
|
||||||
|
|
||||||
source.addNotification(this._notification);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const OverviewActor = GObject.registerClass(
|
const OverviewActor = GObject.registerClass(
|
||||||
class OverviewActor extends St.BoxLayout {
|
class OverviewActor extends St.BoxLayout {
|
||||||
_init() {
|
_init() {
|
||||||
|
|
@ -252,8 +220,6 @@ export class Overview extends Signals.EventEmitter {
|
||||||
this._overview._delegate = this;
|
this._overview._delegate = this;
|
||||||
Main.layoutManager.overviewGroup.add_child(this._overview);
|
Main.layoutManager.overviewGroup.add_child(this._overview);
|
||||||
|
|
||||||
this._shellInfo = new ShellInfo();
|
|
||||||
|
|
||||||
Main.layoutManager.connect('monitors-changed', this._relayout.bind(this));
|
Main.layoutManager.connect('monitors-changed', this._relayout.bind(this));
|
||||||
this._relayout();
|
this._relayout();
|
||||||
|
|
||||||
|
|
@ -275,18 +241,6 @@ export class Overview extends Signals.EventEmitter {
|
||||||
this._swipeTracker = swipeTracker;
|
this._swipeTracker = swipeTracker;
|
||||||
}
|
}
|
||||||
|
|
||||||
//
|
|
||||||
// options:
|
|
||||||
// - undoCallback (function): the callback to be called if undo support is needed
|
|
||||||
// - forFeedback (boolean): whether the message is for direct feedback of a user action
|
|
||||||
//
|
|
||||||
setMessage(text, options) {
|
|
||||||
if (this.isDummy)
|
|
||||||
return;
|
|
||||||
|
|
||||||
this._shellInfo.setMessage(text, options);
|
|
||||||
}
|
|
||||||
|
|
||||||
_changeShownState(state) {
|
_changeShownState(state) {
|
||||||
const {allowedTransitions} =
|
const {allowedTransitions} =
|
||||||
OVERVIEW_SHOWN_TRANSITIONS[this._shownState];
|
OVERVIEW_SHOWN_TRANSITIONS[this._shownState];
|
||||||
|
|
|
||||||
|
|
@ -2092,7 +2092,7 @@ export const ScreenshotUI = GObject.registerClass({
|
||||||
title,
|
title,
|
||||||
// Translators: notification body when a screencast was recorded.
|
// Translators: notification body when a screencast was recorded.
|
||||||
body: this._screencastPath ? _('Click here to view the video.') : '',
|
body: this._screencastPath ? _('Click here to view the video.') : '',
|
||||||
isTransient: true,
|
displayHint: MessageTray.DisplayHint.TRANSIENT,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (this._screencastPath) {
|
if (this._screencastPath) {
|
||||||
|
|
@ -2337,7 +2337,7 @@ function _storeScreenshot(bytes, pixbuf) {
|
||||||
body: _('You can paste the image from the clipboard.'),
|
body: _('You can paste the image from the clipboard.'),
|
||||||
datetime: time,
|
datetime: time,
|
||||||
gicon: content,
|
gicon: content,
|
||||||
isTransient: true,
|
displayHint: MessageTray.DisplayHint.TRANSIENT,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!disableSaveToDisk) {
|
if (!disableSaveToDisk) {
|
||||||
|
|
|
||||||
|
|
@ -199,7 +199,7 @@ export class ShellMountOperation {
|
||||||
source,
|
source,
|
||||||
title,
|
title,
|
||||||
body,
|
body,
|
||||||
isTransient: true,
|
displayHint: MessageTray.DisplayHint.TRANSIENT,
|
||||||
iconName: 'media-removable-symbolic',
|
iconName: 'media-removable-symbolic',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2036,7 +2036,7 @@ class Indicator extends SystemIndicator {
|
||||||
body: _('Activation of network connection failed'),
|
body: _('Activation of network connection failed'),
|
||||||
iconName: 'network-error-symbolic',
|
iconName: 'network-error-symbolic',
|
||||||
urgency: MessageTray.Urgency.HIGH,
|
urgency: MessageTray.Urgency.HIGH,
|
||||||
isTransient: true,
|
displayHint: MessageTray.DisplayHint.TRANSIENT,
|
||||||
});
|
});
|
||||||
this._notification.connect('destroy',
|
this._notification.connect('destroy',
|
||||||
() => (this._notification = null));
|
() => (this._notification = null));
|
||||||
|
|
|
||||||
|
|
@ -166,7 +166,9 @@ const NotificationsBox = GObject.registerClass({
|
||||||
|
|
||||||
_shouldShowDetails(source) {
|
_shouldShowDetails(source) {
|
||||||
return source.policy.detailsInLockScreen ||
|
return source.policy.detailsInLockScreen ||
|
||||||
source.narrowestPrivacyScope === MessageTray.PrivacyScope.SYSTEM;
|
source.notifications.every(n =>
|
||||||
|
n.displayHint & MessageTray.DisplayHint.SHOW_CONTENT_ON_LOCKSCREEN
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
_updateSourceBoxStyle(source, obj, box) {
|
_updateSourceBoxStyle(source, obj, box) {
|
||||||
|
|
|
||||||
|
|
@ -43,7 +43,7 @@ export class WindowAttentionHandler {
|
||||||
source,
|
source,
|
||||||
title,
|
title,
|
||||||
body,
|
body,
|
||||||
forFeedback: true,
|
displayHint: MessageTray.DisplayHint.FORCE_IMMEDIATELY,
|
||||||
});
|
});
|
||||||
notification.connect('activated', () => {
|
notification.connect('activated', () => {
|
||||||
source.open();
|
source.open();
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue