[MessageTray] do pop-out on hover for long notifications
https://bugzilla.gnome.org/show_bug.cgi?id=606755
This commit is contained in:
parent
3c9d0fbca6
commit
fa60165764
@ -556,8 +556,9 @@ StTooltip {
|
|||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
background: rgba(0,0,0,0.9);
|
background: rgba(0,0,0,0.9);
|
||||||
color: white;
|
color: white;
|
||||||
padding: 2px 10px;
|
padding: 2px 10px 10px 10px;
|
||||||
spacing: 10px;
|
spacing-rows: 5px;
|
||||||
|
spacing-columns: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#summary-mode {
|
#summary-mode {
|
||||||
|
@ -3,6 +3,7 @@
|
|||||||
const Clutter = imports.gi.Clutter;
|
const Clutter = imports.gi.Clutter;
|
||||||
const Lang = imports.lang;
|
const Lang = imports.lang;
|
||||||
const Mainloop = imports.mainloop;
|
const Mainloop = imports.mainloop;
|
||||||
|
const Pango = imports.gi.Pango;
|
||||||
const Shell = imports.gi.Shell;
|
const Shell = imports.gi.Shell;
|
||||||
const Signals = imports.signals;
|
const Signals = imports.signals;
|
||||||
const St = imports.gi.St;
|
const St = imports.gi.St;
|
||||||
@ -25,17 +26,38 @@ const MessageTrayState = {
|
|||||||
TRAY_ONLY: 3 // neither notifiations nor summary are visible, only tray
|
TRAY_ONLY: 3 // neither notifiations nor summary are visible, only tray
|
||||||
};
|
};
|
||||||
|
|
||||||
function Notification(icon, text, source) {
|
// Notification:
|
||||||
this._init(icon, text, source);
|
// @source: the notification's Source
|
||||||
|
// @icon: a ClutterActor for the notification's icon
|
||||||
|
// @title: the title
|
||||||
|
// @banner: the banner text
|
||||||
|
// @body: the body text, or %null
|
||||||
|
//
|
||||||
|
// Creates a notification. In banner mode, it will show
|
||||||
|
// @icon, @title (in bold) and @banner, all on a single line
|
||||||
|
// (with @banner ellipsized if necessary). If @body is not %null, then
|
||||||
|
// the notification will be expandable. In expanded mode, it will show
|
||||||
|
// just @icon and @title (in bold) on the first line, and @body on
|
||||||
|
// multiple lines underneath.
|
||||||
|
function Notification(source, icon, title, banner, body) {
|
||||||
|
this._init(source, icon, title, banner, body);
|
||||||
}
|
}
|
||||||
|
|
||||||
Notification.prototype = {
|
Notification.prototype = {
|
||||||
_init: function(icon, text, source) {
|
_init: function(source, icon, title, banner, body) {
|
||||||
this.icon = icon;
|
|
||||||
this.text = text;
|
|
||||||
this.source = source;
|
this.source = source;
|
||||||
|
this.icon = icon;
|
||||||
|
this.title = title ? this._cleanMarkup(title.replace('\n', ' ')) : '';
|
||||||
|
this.banner = banner ? this._cleanMarkup(banner.replace('\n', ' ')) : '';
|
||||||
|
this.body = body ? this._cleanMarkup(body) : null;
|
||||||
|
},
|
||||||
|
|
||||||
|
_cleanMarkup: function(text) {
|
||||||
|
// Support <b>, <i>, and <u>, escape anything else
|
||||||
|
// so it displays as raw markup.
|
||||||
|
return text.replace(/<(\/?[^biu]>|[^>\/][^>])/g, "<$1");
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
function NotificationBox() {
|
function NotificationBox() {
|
||||||
this._init();
|
this._init();
|
||||||
@ -43,29 +65,122 @@ function NotificationBox() {
|
|||||||
|
|
||||||
NotificationBox.prototype = {
|
NotificationBox.prototype = {
|
||||||
_init: function() {
|
_init: function() {
|
||||||
this.actor = new St.BoxLayout({ name: 'notification' });
|
this.actor = new St.Table({ name: 'notification' });
|
||||||
|
|
||||||
this._iconBox = new St.Bin();
|
this._iconBox = new St.Bin();
|
||||||
this.actor.add(this._iconBox);
|
this.actor.add(this._iconBox, { row: 0,
|
||||||
|
col: 0,
|
||||||
|
x_expand: false,
|
||||||
|
y_expand: false,
|
||||||
|
y_fill: false });
|
||||||
|
|
||||||
this._text = new St.Label();
|
// The first line should have the title, followed by the
|
||||||
this.actor.add(this._text, { expand: true, x_fill: false, y_fill: false, y_align: St.Align.MIDDLE });
|
// banner text, but ellipsized if they won't both fit. We can't
|
||||||
|
// make St.Table or St.BoxLayout do this the way we want (don't
|
||||||
|
// show banner at all if title needs to be ellipsized), so we
|
||||||
|
// use Shell.GenericContainer.
|
||||||
|
this._bannerBox = new Shell.GenericContainer();
|
||||||
|
this._bannerBox.connect('get-preferred-width', Lang.bind(this, this._bannerBoxGetPreferredWidth));
|
||||||
|
this._bannerBox.connect('get-preferred-height', Lang.bind(this, this._bannerBoxGetPreferredHeight));
|
||||||
|
this._bannerBox.connect('allocate', Lang.bind(this, this._bannerBoxAllocate));
|
||||||
|
this.actor.add(this._bannerBox, { row: 0,
|
||||||
|
col: 1,
|
||||||
|
y_expand: false,
|
||||||
|
y_fill: false });
|
||||||
|
|
||||||
this.notification = null;
|
this._titleText = new St.Label();
|
||||||
|
this._bannerBox.add_actor(this._titleText);
|
||||||
|
|
||||||
|
this._bannerText = new St.Label();
|
||||||
|
this._bannerBox.add_actor(this._bannerText);
|
||||||
|
|
||||||
|
this._bodyText = new St.Label();
|
||||||
|
this._bodyText.clutter_text.line_wrap = true;
|
||||||
|
this._bodyText.clutter_text.ellipsize = Pango.EllipsizeMode.NONE;
|
||||||
|
this.actor.add(this._bodyText, { row: 1,
|
||||||
|
col: 1 });
|
||||||
},
|
},
|
||||||
|
|
||||||
setContent: function(notification) {
|
setContent: function(notification) {
|
||||||
this.notification = notification;
|
this.notification = notification;
|
||||||
|
|
||||||
this._iconBox.child = notification.icon;
|
this._iconBox.child = notification.icon;
|
||||||
|
this._titleText.clutter_text.set_markup('<b>' + notification.title + '</b>');
|
||||||
|
this._bannerText.clutter_text.set_markup(notification.banner);
|
||||||
|
|
||||||
// Support <b>, <i>, and <u>, escape anything else
|
if (notification.body) {
|
||||||
// so it displays as raw markup.
|
this._bodyText.clutter_text.set_markup(notification.body);
|
||||||
let markup = notification.text.replace(/<(\/?[^biu]>|[^>\/][^>])/g, "<$1");
|
this._canPopOut = true;
|
||||||
this._text.clutter_text.set_markup(markup);
|
} else {
|
||||||
|
// If there's no body, then normally we wouldn't do pop-out.
|
||||||
|
// But if title+banner is too wide for the notification, then
|
||||||
|
// we'd need to pop out to show the full banner. So we set up
|
||||||
|
// bodyText with that now.
|
||||||
|
this._bodyText.clutter_text.set_markup(notification.banner);
|
||||||
|
this._canPopOut = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
let primary = global.get_primary_monitor();
|
_bannerBoxGetPreferredWidth: function(actor, forHeight, alloc) {
|
||||||
this.actor.x = Math.round((primary.width - this.actor.width) / 2);
|
let [titleMin, titleNat] = this._titleText.get_preferred_width(forHeight);
|
||||||
|
let [bannerMin, bannerNat] = this._bannerText.get_preferred_width(forHeight);
|
||||||
|
let [has_spacing, spacing] = this.actor.get_theme_node().get_length('spacing-columns', false);
|
||||||
|
|
||||||
|
alloc.min_size = titleMin;
|
||||||
|
alloc.natural_size = titleNat + (has_spacing ? spacing : 0) + bannerNat;
|
||||||
|
},
|
||||||
|
|
||||||
|
_bannerBoxGetPreferredHeight: function(actor, forWidth, alloc) {
|
||||||
|
[alloc.min_size, alloc.natural_size] =
|
||||||
|
this._titleText.get_preferred_height(forWidth);
|
||||||
|
},
|
||||||
|
|
||||||
|
_bannerBoxAllocate: function(actor, box, flags) {
|
||||||
|
let [titleMinW, titleNatW] = this._titleText.get_preferred_width(-1);
|
||||||
|
let [titleMinH, titleNatH] = this._titleText.get_preferred_height(-1);
|
||||||
|
let [bannerMinW, bannerNatW] = this._bannerText.get_preferred_width(-1);
|
||||||
|
let [has_spacing, spacing] = this.actor.get_theme_node().get_length('spacing-columns', false);
|
||||||
|
if (!has_spacing)
|
||||||
|
spacing = 0;
|
||||||
|
let availWidth = box.x2 - box.x1;
|
||||||
|
|
||||||
|
let titleBox = new Clutter.ActorBox();
|
||||||
|
titleBox.x1 = titleBox.y1 = 0;
|
||||||
|
titleBox.x2 = Math.min(titleNatW, availWidth);
|
||||||
|
titleBox.y2 = titleNatH;
|
||||||
|
this._titleText.allocate(titleBox, flags);
|
||||||
|
|
||||||
|
let bannerBox = new Clutter.ActorBox();
|
||||||
|
bannerBox.x1 = Math.min(titleBox.x2 + spacing, availWidth);
|
||||||
|
bannerBox.y1 = 0;
|
||||||
|
bannerBox.x2 = Math.min(bannerBox.x1 + bannerNatW, availWidth);
|
||||||
|
bannerBox.y2 = titleNatH;
|
||||||
|
this._bannerText.allocate(bannerBox, flags);
|
||||||
|
|
||||||
|
if (bannerBox.x2 < bannerBox.x1 + bannerNatW)
|
||||||
|
this._canPopOut = true;
|
||||||
|
},
|
||||||
|
|
||||||
|
popOut: function() {
|
||||||
|
if (!this._canPopOut)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
Tweener.addTween(this._bannerText,
|
||||||
|
{ opacity: 0,
|
||||||
|
time: ANIMATION_TIME,
|
||||||
|
transition: "easeOutQuad" });
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
|
||||||
|
popIn: function() {
|
||||||
|
if (!this._canPopOut)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
Tweener.addTween(this._bannerText,
|
||||||
|
{ opacity: 255,
|
||||||
|
time: ANIMATION_TIME,
|
||||||
|
transition: "easeOutQuad" });
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -87,8 +202,9 @@ Source.prototype = {
|
|||||||
throw new Error('no implementation of createIcon in ' + this);
|
throw new Error('no implementation of createIcon in ' + this);
|
||||||
},
|
},
|
||||||
|
|
||||||
notify: function(text) {
|
notify: function(title, banner, body) {
|
||||||
this.text = text;
|
this.notification = new Notification(this, this.createIcon(ICON_SIZE),
|
||||||
|
title, banner, body);
|
||||||
this.emit('notify');
|
this.emit('notify');
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -111,7 +227,7 @@ MessageTray.prototype = {
|
|||||||
this.actor = new St.BoxLayout({ name: 'message-tray',
|
this.actor = new St.BoxLayout({ name: 'message-tray',
|
||||||
reactive: true });
|
reactive: true });
|
||||||
|
|
||||||
this._notificationBin = new St.Bin();
|
this._notificationBin = new St.Bin({ reactive: true });
|
||||||
this.actor.add(this._notificationBin);
|
this.actor.add(this._notificationBin);
|
||||||
this._notificationBin.hide();
|
this._notificationBin.hide();
|
||||||
this._notificationBox = new NotificationBox();
|
this._notificationBox = new NotificationBox();
|
||||||
@ -133,6 +249,7 @@ MessageTray.prototype = {
|
|||||||
this._state = MessageTrayState.HIDDEN;
|
this._state = MessageTrayState.HIDDEN;
|
||||||
this.actor.show();
|
this.actor.show();
|
||||||
Main.chrome.addActor(this.actor, { affectsStruts: false });
|
Main.chrome.addActor(this.actor, { affectsStruts: false });
|
||||||
|
Main.chrome.trackActor(this._notificationBin, { affectsStruts: false });
|
||||||
|
|
||||||
global.connect('screen-size-changed',
|
global.connect('screen-size-changed',
|
||||||
Lang.bind(this, this._setSizePosition));
|
Lang.bind(this, this._setSizePosition));
|
||||||
@ -211,8 +328,7 @@ MessageTray.prototype = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
_onNotify: function(source) {
|
_onNotify: function(source) {
|
||||||
let notification = new Notification(source.createIcon(ICON_SIZE), source.text, source)
|
this._notificationQueue.push(source.notification);
|
||||||
this._notificationQueue.push(notification);
|
|
||||||
|
|
||||||
if (this._state == MessageTrayState.HIDDEN)
|
if (this._state == MessageTrayState.HIDDEN)
|
||||||
this._updateState();
|
this._updateState();
|
||||||
@ -229,6 +345,15 @@ MessageTray.prototype = {
|
|||||||
|
|
||||||
if (this._state == MessageTrayState.HIDDEN)
|
if (this._state == MessageTrayState.HIDDEN)
|
||||||
this._updateState();
|
this._updateState();
|
||||||
|
else if (this._state == MessageTrayState.NOTIFICATION) {
|
||||||
|
if (this._notificationBox.popOut()) {
|
||||||
|
Tweener.addTween(this._notificationBin,
|
||||||
|
{ y: this.actor.height - this._notificationBin.height,
|
||||||
|
time: ANIMATION_TIME,
|
||||||
|
transition: "easeOutQuad"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
_onMessageTrayLeft: function() {
|
_onMessageTrayLeft: function() {
|
||||||
@ -345,6 +470,8 @@ MessageTray.prototype = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
_hideNotification: function() {
|
_hideNotification: function() {
|
||||||
|
this._notificationBox.popIn();
|
||||||
|
|
||||||
Tweener.addTween(this._notificationBin,
|
Tweener.addTween(this._notificationBin,
|
||||||
{ y: this.actor.height,
|
{ y: this.actor.height,
|
||||||
opacity: 0,
|
opacity: 0,
|
||||||
|
@ -122,10 +122,7 @@ NotificationDaemon.prototype = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
summary = GLib.markup_escape_text(summary, -1);
|
summary = GLib.markup_escape_text(summary, -1);
|
||||||
if (body)
|
source.notify(summary, body);
|
||||||
source.notify('<b>' + summary + '</b>: ' + body);
|
|
||||||
else
|
|
||||||
source.notify('<b>' + summary + '</b>');
|
|
||||||
return id;
|
return id;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user