diff --git a/data/theme/gnome-shell.css b/data/theme/gnome-shell.css index f73abb401..070d082c8 100644 --- a/data/theme/gnome-shell.css +++ b/data/theme/gnome-shell.css @@ -1070,6 +1070,14 @@ StTooltip StLabel { padding-bottom: 8px; } +/* We use row-span = 2 for the image cell, which prevents its height preferences to be + taken into account during allocation, so its height ends up being limited by the height + of the content in the other rows. To avoid showing a stretched image, we set the minimum + height of the table to be ICON_SIZE + IMAGE_SIZE + spacing-rows = 24 + 125 + 10 = 159 */ +.notification-with-image { + min-height: 159px; +} + .summary-boxpointer { -arrow-border-radius: 8px; -arrow-background-color: rgba(0,0,0,0.9); diff --git a/js/ui/messageTray.js b/js/ui/messageTray.js index 5be5fd66a..89d3aca4f 100644 --- a/js/ui/messageTray.js +++ b/js/ui/messageTray.js @@ -396,6 +396,8 @@ function Notification(source, title, banner, params) { } Notification.prototype = { + IMAGE_SIZE: 125, + _init: function(source, title, banner, params) { this.source = source; this.urgency = Urgency.NORMAL; @@ -412,6 +414,7 @@ Notification.prototype = { this._titleDirection = St.TextDirection.NONE; this._spacing = 0; this._scrollPolicy = Gtk.PolicyType.AUTOMATIC; + this._imageBin = null; source.connect('destroy', Lang.bind(this, function (source, reason) { @@ -441,9 +444,19 @@ Notification.prototype = { this._bannerBox.connect('allocate', Lang.bind(this, this._bannerBoxAllocate)); this._table.add(this._bannerBox, { row: 0, col: 1, + col_span: 2, + x_expand: false, y_expand: false, y_fill: false }); + // This is an empty cell that overlaps with this._bannerBox cell to ensure + // that this._bannerBox cell expands horizontally, while not forcing the + // this._imageBin that is also in col: 2 to expand horizontally. + this._table.add(new St.Bin(), { row: 0, + col: 2, + y_expand: false, + y_fill: false }); + this._titleLabel = new St.Label(); this._bannerBox.add_actor(this._titleLabel); this._bannerUrlHighlighter = new URLHighlighter(); @@ -495,7 +508,10 @@ Notification.prototype = { this._actionArea = null; this._buttonBox = null; } - if (!this._scrollArea && !this._actionArea) + if (this._imageBin && params.clear) + this.unsetImage(); + + if (!this._scrollArea && !this._actionArea && !this._imageBin) this._table.remove_style_class_name('multi-line-notification'); this._icon = params.icon || this.source.createNotificationIcon(); @@ -559,7 +575,9 @@ Notification.prototype = { vscrollbar_policy: this._scrollPolicy, hscrollbar_policy: Gtk.PolicyType.NEVER, style_class: 'vfade' }); - this._table.add(this._scrollArea, { row: 1, col: 1 }); + this._table.add(this._scrollArea, { row: 1, + col: 2 }); + this._updateLastColumnSettings(); this._contentArea = new St.BoxLayout({ name: 'notification-body', vertical: true }); this._scrollArea.add_actor(this._contentArea); @@ -636,13 +654,49 @@ Notification.prototype = { if (!props) props = {}; props.row = 2; - props.col = 1; + props.col = 2; this._table.add_style_class_name('multi-line-notification'); this._table.add(this._actionArea, props); + this._updateLastColumnSettings(); this._updated(); }, + _updateLastColumnSettings: function() { + if (this._scrollArea) + this._table.child_set(this._scrollArea, { col: this._imageBin ? 2 : 1, + col_span: this._imageBin ? 1 : 2 }); + if (this._actionArea) + this._table.child_set(this._actionArea, { col: this._imageBin ? 2 : 1, + col_span: this._imageBin ? 1 : 2 }); + }, + + setImage: function(image) { + if (this._imageBin) + this.unsetImage(); + this._imageBin = new St.Bin(); + this._imageBin.child = image; + this._imageBin.opacity = 230; + this._table.add_style_class_name('notification-with-image'); + this._updateLastColumnSettings(); + this._table.add(this._imageBin, { row: 1, + col: 1, + row_span: 2, + x_expand: false, + y_expand: false, + x_fill: false, + y_fill: false }); + }, + + unsetImage: function() { + if (this._imageBin) { + this._table.remove_style_class_name('notification-with-image'); + this._table.remove_actor(this._imageBin); + this._imageBin = null; + this._updateLastColumnSettings(); + } + }, + // addButton: // @id: the action ID // @label: the label for the action's button @@ -658,7 +712,9 @@ Notification.prototype = { let box = new St.BoxLayout({ name: 'notification-actions' }); this.setActionArea(box, { x_expand: false, + y_expand: false, x_fill: false, + y_fill: false, x_align: St.Align.END }); this._buttonBox = box; } diff --git a/js/ui/notificationDaemon.js b/js/ui/notificationDaemon.js index 361c585d7..d3321d710 100644 --- a/js/ui/notificationDaemon.js +++ b/js/ui/notificationDaemon.js @@ -119,11 +119,6 @@ NotificationDaemon.prototype = { return new St.Icon({ icon_name: icon, icon_type: St.IconType.FULLCOLOR, icon_size: size }); - } else if (hints['image-data']) { - let [width, height, rowStride, hasAlpha, - bitsPerSample, nChannels, data] = hints['image-data']; - return textureCache.load_from_raw(data, hasAlpha, - width, height, rowStride, size); } else { let stockIcon; switch (hints.urgency) { @@ -220,14 +215,18 @@ NotificationDaemon.prototype = { hints = Params.parse(hints, { urgency: Urgency.NORMAL }, true); - // Be compatible with the various hints for image data - // 'image-data' is the latest name of this hint, introduced in 1.2 - if (!hints['image-data']) { + // Be compatible with the various hints for image data and image path + // 'image-data' and 'image-path' are the latest name of these hints, introduced in 1.2 + + if (!hints['image-path'] && hints['image_path']) + hints['image-path'] = hints['image_path']; // version 1.1 of the spec + + if (!hints['image-data']) if (hints['image_data']) hints['image-data'] = hints['image_data']; // version 1.1 of the spec - else if (hints['icon_data']) - hints['image-data'] = hints['icon_data']; // previous versions of the spec - } + else if (hints['icon_data'] && !hints['image-path']) + // early versions of the spec; 'icon_data' should only be used if 'image-path' is not available + hints['image-data'] = hints['icon_data']; let ndata = { appName: appName, icon: icon, @@ -331,6 +330,23 @@ NotificationDaemon.prototype = { clear: true }); } + if (hints['image-data'] || hints['image-path']) { + let image = null; + if (hints['image-data']) { + let [width, height, rowStride, hasAlpha, + bitsPerSample, nChannels, data] = hints['image-data']; + image = St.TextureCache.get_default().load_from_raw(data, hasAlpha, + width, height, rowStride, notification.IMAGE_SIZE); + } else if (hints['image-path']) { + image = St.TextureCache.get_default().load_uri_async(GLib.filename_to_uri(hints['image-path'], null), + notification.IMAGE_SIZE, + notification.IMAGE_SIZE); + } + notification.setImage(image); + } else { + notification.unsetImage(); + } + if (actions.length) { notification.setUseActionIcons(hints['action-icons'] == true); for (let i = 0; i < actions.length - 1; i += 2) {