Add support for chatting directly from IM notifications
Also reorganizes the notification layout to use an StScrollView; very tall notifications are now scrolled instead of just taking up more and more of the screen. https://bugzilla.gnome.org/show_bug.cgi?id=608999
This commit is contained in:
parent
5bce103a40
commit
e94d54bffb
@ -723,6 +723,18 @@ StTooltip {
|
|||||||
padding-bottom: 38px;
|
padding-bottom: 38px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#notification-scrollview {
|
||||||
|
max-height: 10em;
|
||||||
|
}
|
||||||
|
|
||||||
|
#notification-scrollview > .top-shadow, #notification-scrollview > .bottom-shadow {
|
||||||
|
height: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
#notification-body {
|
||||||
|
spacing: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
#notification-actions {
|
#notification-actions {
|
||||||
spacing: 5px;
|
spacing: 5px;
|
||||||
}
|
}
|
||||||
@ -745,6 +757,24 @@ StTooltip {
|
|||||||
background: #808080;
|
background: #808080;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.chat-received {
|
||||||
|
background-gradient-direction: horizontal;
|
||||||
|
background-gradient-start: #606060;
|
||||||
|
background-gradient-end: #000000;
|
||||||
|
|
||||||
|
min-width: 20em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-sent {
|
||||||
|
background-gradient-direction: horizontal;
|
||||||
|
background-gradient-start: #000000;
|
||||||
|
background-gradient-end: #606060;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-response {
|
||||||
|
border: 1px solid white;
|
||||||
|
}
|
||||||
|
|
||||||
/* The spacing and padding on the summary is tricky; we want to keep
|
/* The spacing and padding on the summary is tricky; we want to keep
|
||||||
* the icons from touching each other or the edges of the screen, but
|
* the icons from touching each other or the edges of the screen, but
|
||||||
* we also want them to be "Fitts"-y with respect to the edges, so the
|
* we also want them to be "Fitts"-y with respect to the edges, so the
|
||||||
|
@ -215,6 +215,10 @@ const ChannelTextIface = {
|
|||||||
{ name: 'AcknowledgePendingMessages',
|
{ name: 'AcknowledgePendingMessages',
|
||||||
inSignature: 'au',
|
inSignature: 'au',
|
||||||
outSignature: ''
|
outSignature: ''
|
||||||
|
},
|
||||||
|
{ name: 'Send',
|
||||||
|
inSignature: 'us',
|
||||||
|
outSignature: ''
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
signals: [
|
signals: [
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
/* -*- mode: js2; js2-basic-offset: 4; indent-tabs-mode: nil -*- */
|
/* -*- mode: js2; js2-basic-offset: 4; indent-tabs-mode: nil -*- */
|
||||||
|
|
||||||
const Clutter = imports.gi.Clutter;
|
const Clutter = imports.gi.Clutter;
|
||||||
|
const Gtk = imports.gi.Gtk;
|
||||||
const Lang = imports.lang;
|
const Lang = imports.lang;
|
||||||
const Mainloop = imports.mainloop;
|
const Mainloop = imports.mainloop;
|
||||||
const Pango = imports.gi.Pango;
|
const Pango = imports.gi.Pango;
|
||||||
@ -46,11 +47,15 @@ function _cleanMarkup(text) {
|
|||||||
// @source's icon, @title (in bold) and @banner, all on a single line
|
// @source's icon, @title (in bold) and @banner, all on a single line
|
||||||
// (with @banner ellipsized if necessary).
|
// (with @banner ellipsized if necessary).
|
||||||
//
|
//
|
||||||
// Additional notification details can be added via addBody(),
|
// Additional notification details can be added, in which case the
|
||||||
// addAction(), and addActor(). If any of these are called, then the
|
// notification can be expanded by moving the pointer into it. In
|
||||||
// notification will expand to show the additional actors (while
|
// expanded mode, the banner text disappears, and there can be one or
|
||||||
// hiding the @banner) if the pointer is moved into it while it is
|
// more rows of additional content. This content is put inside a
|
||||||
// visible.
|
// scrollview, so if it gets too tall, the notification will scroll
|
||||||
|
// rather than continuing to grow. In addition to this main content
|
||||||
|
// area, there is also a single-row "action area", which is not
|
||||||
|
// scrolled and can contain a single actor. There are also convenience
|
||||||
|
// methods for creating a button box in the action area.
|
||||||
//
|
//
|
||||||
// If @bannerBody is %true, then @banner will also be used as the body
|
// If @bannerBody is %true, then @banner will also be used as the body
|
||||||
// of the notification (as with addBody()) when the banner is expanded.
|
// of the notification (as with addBody()) when the banner is expanded.
|
||||||
@ -71,39 +76,44 @@ Notification.prototype = {
|
|||||||
this.emit('dismissed');
|
this.emit('dismissed');
|
||||||
}));
|
}));
|
||||||
|
|
||||||
this.actor = new St.Table({ name: 'notification' });
|
this.actor = new St.Table({ name: 'notification',
|
||||||
|
reactive: true });
|
||||||
this.update(title, banner, true);
|
this.update(title, banner, true);
|
||||||
},
|
},
|
||||||
|
|
||||||
// update:
|
// update:
|
||||||
// @title: the new title
|
// @title: the new title
|
||||||
// @banner: the new banner
|
// @banner: the new banner
|
||||||
// @clear: whether or not to clear out extra actors
|
// @clear: whether or not to clear out body and action actors
|
||||||
//
|
//
|
||||||
// Updates the notification by regenerating its icon and updating
|
// Updates the notification by regenerating its icon and updating
|
||||||
// the title/banner. If @clear is %true, it will also remove any
|
// the title/banner. If @clear is %true, it will also remove any
|
||||||
// additional actors/action buttons previously added.
|
// additional actors/action buttons previously added.
|
||||||
update: function(title, banner, clear) {
|
update: function(title, banner, clear) {
|
||||||
let children = this.actor.get_children();
|
if (this._icon)
|
||||||
for (let i = 0; i < children.length; i++) {
|
this._icon.destroy();
|
||||||
let meta = this.actor.get_child_meta(children[i]);
|
if (this._bannerBox)
|
||||||
if (clear || meta.row == 0 || (this._bannerBody && meta.row == 1))
|
this._bannerBox.destroy();
|
||||||
children[i].destroy();
|
if (this._scrollArea && (this._bannerBody || clear)) {
|
||||||
|
this._scrollArea.destroy();
|
||||||
|
this._scrollArea = null;
|
||||||
|
this._contentArea = null;
|
||||||
}
|
}
|
||||||
if (clear) {
|
if (this._actionArea && clear) {
|
||||||
this.actions = {};
|
this._actionArea.destroy();
|
||||||
this._actionBox = null;
|
this._actionArea = null;
|
||||||
|
this._buttonBox = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
let icon = this.source.createIcon(ICON_SIZE);
|
this._icon = this.source.createIcon(ICON_SIZE);
|
||||||
icon.reactive = true;
|
this._icon.reactive = true;
|
||||||
this.actor.add(icon, { row: 0,
|
this.actor.add(this._icon, { row: 0,
|
||||||
col: 0,
|
col: 0,
|
||||||
x_expand: false,
|
x_expand: false,
|
||||||
y_expand: false,
|
y_expand: false,
|
||||||
y_fill: false });
|
y_fill: false });
|
||||||
|
|
||||||
icon.connect('button-release-event', Lang.bind(this,
|
this._icon.connect('button-release-event', Lang.bind(this,
|
||||||
function () {
|
function () {
|
||||||
this.source.clicked();
|
this.source.clicked();
|
||||||
}));
|
}));
|
||||||
@ -139,64 +149,32 @@ Notification.prototype = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
// addActor:
|
// addActor:
|
||||||
// @actor: actor to add to the notification
|
// @actor: actor to add to the body of the notification
|
||||||
// @props: (optional) child properties
|
|
||||||
//
|
//
|
||||||
// Adds @actor to the notification's St.Table, using @props.
|
// Appends @actor to the notification's body
|
||||||
//
|
addActor: function(actor) {
|
||||||
// If @props does not specify a %row, then @actor will be added
|
if (!this._scrollArea) {
|
||||||
// to the bottom of the notification (unless there are action
|
this._scrollArea = new St.ScrollView({ name: 'notification-scrollview',
|
||||||
// buttons present, in which case it will be added above them).
|
vscrollbar_policy: Gtk.PolicyType.AUTOMATIC,
|
||||||
//
|
hscrollbar_policy: Gtk.PolicyType.NEVER,
|
||||||
// If @props does not specify a %col, it will default to column 1.
|
vshadows: true });
|
||||||
// (Normally only the icon is in column 0.)
|
this.actor.add(this._scrollArea, { row: 1,
|
||||||
//
|
col: 1 });
|
||||||
// If @props specifies an already-occupied cell, then the existing
|
this._contentArea = new St.BoxLayout({ name: 'notification-body',
|
||||||
// contents of the table will be shifted down to make room for it.
|
vertical: true });
|
||||||
addActor: function(actor, props) {
|
this._scrollArea.add_actor(this._contentArea);
|
||||||
if (!props)
|
|
||||||
props = {};
|
|
||||||
|
|
||||||
if (!('col' in props))
|
|
||||||
props.col = 1;
|
|
||||||
|
|
||||||
if ('row' in props) {
|
|
||||||
let children = this.actor.get_children();
|
|
||||||
let i, meta, collision = false;
|
|
||||||
|
|
||||||
for (i = 0; i < children.length; i++) {
|
|
||||||
meta = this.actor.get_child_meta(children[i]);
|
|
||||||
if (meta.row == props.row && meta.col == props.col) {
|
|
||||||
collision = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (collision) {
|
|
||||||
for (i = 0; i < children.length; i++) {
|
|
||||||
meta = this.actor.get_child_meta(children[i]);
|
|
||||||
if (meta.row >= props.row)
|
|
||||||
meta.row++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (this._actionBox) {
|
|
||||||
props.row = this.actor.row_count - 1;
|
|
||||||
this.actor.get_child_meta(this._actionBox).row++;
|
|
||||||
} else {
|
|
||||||
props.row = this.actor.row_count;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.actor.add(actor, props);
|
this._contentArea.add(actor);
|
||||||
},
|
},
|
||||||
|
|
||||||
// addBody:
|
// addBody:
|
||||||
// @text: the text
|
// @text: the text
|
||||||
// @props: (optional) properties for addActor()
|
|
||||||
//
|
//
|
||||||
// Adds a multi-line label containing @text to the notification.
|
// Adds a multi-line label containing @text to the notification.
|
||||||
addBody: function(text, props) {
|
//
|
||||||
|
// Return value: the newly-added label
|
||||||
|
addBody: function(text) {
|
||||||
let body = new St.Label();
|
let body = new St.Label();
|
||||||
body.clutter_text.line_wrap = true;
|
body.clutter_text.line_wrap = true;
|
||||||
body.clutter_text.line_wrap_mode = Pango.WrapMode.WORD_CHAR;
|
body.clutter_text.line_wrap_mode = Pango.WrapMode.WORD_CHAR;
|
||||||
@ -205,15 +183,56 @@ Notification.prototype = {
|
|||||||
text = text ? _cleanMarkup(text) : '';
|
text = text ? _cleanMarkup(text) : '';
|
||||||
body.clutter_text.set_markup(text);
|
body.clutter_text.set_markup(text);
|
||||||
|
|
||||||
this.addActor(body, props);
|
this.addActor(body);
|
||||||
|
return body;
|
||||||
},
|
},
|
||||||
|
|
||||||
_addBannerBody: function() {
|
_addBannerBody: function() {
|
||||||
this.addBody(this._bannerBodyText, { row: 1 });
|
this.addBody(this._bannerBodyText);
|
||||||
this._bannerBodyText = null;
|
this._bannerBodyText = null;
|
||||||
},
|
},
|
||||||
|
|
||||||
// addAction:
|
// scrollTo:
|
||||||
|
// @side: St.Side.TOP or St.Side.BOTTOM
|
||||||
|
//
|
||||||
|
// Scrolls the content area (if scrollable) to the indicated edge
|
||||||
|
scrollTo: function(side) {
|
||||||
|
// Hack to force a relayout, since the caller probably
|
||||||
|
// just added or removed something to scrollArea, and
|
||||||
|
// the adjustment needs to reflect that.
|
||||||
|
global.stage.get_actor_at_pos(Clutter.PickMode.REACTIVE, 0, 0);
|
||||||
|
|
||||||
|
let adjustment = this._scrollArea.vscroll.adjustment;
|
||||||
|
if (side == St.Side.TOP)
|
||||||
|
adjustment.value = adjustment.lower;
|
||||||
|
else if (side == St.Side.BOTTOM)
|
||||||
|
adjustment.value = adjustment.upper;
|
||||||
|
},
|
||||||
|
|
||||||
|
// setActionArea:
|
||||||
|
// @actor: the actor
|
||||||
|
// @props: (option) St.Table child properties
|
||||||
|
//
|
||||||
|
// Puts @actor into the action area of the notification, replacing
|
||||||
|
// the previous contents
|
||||||
|
setActionArea: function(actor, props) {
|
||||||
|
if (this._actionArea) {
|
||||||
|
this._actionArea.destroy();
|
||||||
|
this._actionArea = null;
|
||||||
|
if (this._buttonBox)
|
||||||
|
this._buttonBox = null;
|
||||||
|
}
|
||||||
|
this._actionArea = actor;
|
||||||
|
|
||||||
|
if (!props)
|
||||||
|
props = {};
|
||||||
|
props.row = 2;
|
||||||
|
props.col = 1;
|
||||||
|
|
||||||
|
this.actor.add(this._actionArea, props);
|
||||||
|
},
|
||||||
|
|
||||||
|
// addButton:
|
||||||
// @id: the action ID
|
// @id: the action ID
|
||||||
// @label: the label for the action's button
|
// @label: the label for the action's button
|
||||||
//
|
//
|
||||||
@ -223,21 +242,21 @@ Notification.prototype = {
|
|||||||
//
|
//
|
||||||
// If the button is clicked, the notification will emit the
|
// If the button is clicked, the notification will emit the
|
||||||
// %action-invoked signal with @id as a parameter
|
// %action-invoked signal with @id as a parameter
|
||||||
addAction: function(id, label) {
|
addButton: function(id, label) {
|
||||||
if (!this._actionBox) {
|
if (!this._buttonBox) {
|
||||||
if (this._bannerBodyText)
|
if (this._bannerBodyText)
|
||||||
this._addBannerBody();
|
this._addBannerBody();
|
||||||
|
|
||||||
let box = new St.BoxLayout({ name: 'notification-actions' });
|
let box = new St.BoxLayout({ name: 'notification-actions' });
|
||||||
this.addActor(box, { x_expand: false,
|
this.setActionArea(box, { x_expand: false,
|
||||||
x_fill: false,
|
x_fill: false,
|
||||||
x_align: St.Align.END });
|
x_align: St.Align.END });
|
||||||
this._actionBox = box;
|
this._buttonBox = box;
|
||||||
}
|
}
|
||||||
|
|
||||||
let button = new St.Button({ style_class: 'notification-button',
|
let button = new St.Button({ style_class: 'notification-button',
|
||||||
label: label });
|
label: label });
|
||||||
this._actionBox.add(button);
|
this._buttonBox.add(button);
|
||||||
button.connect('clicked', Lang.bind(this, function() { this.emit('action-invoked', id); }));
|
button.connect('clicked', Lang.bind(this, function() { this.emit('action-invoked', id); }));
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -563,6 +582,19 @@ MessageTray.prototype = {
|
|||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
lock: function() {
|
||||||
|
this._locked = true;
|
||||||
|
},
|
||||||
|
|
||||||
|
unlock: function() {
|
||||||
|
this._locked = false;
|
||||||
|
|
||||||
|
this.actor.sync_hover();
|
||||||
|
this._summary.sync_hover();
|
||||||
|
|
||||||
|
this._updateState();
|
||||||
|
},
|
||||||
|
|
||||||
_onNotify: function(source, notification) {
|
_onNotify: function(source, notification) {
|
||||||
if (this._getNotification(notification.id, source) == null) {
|
if (this._getNotification(notification.id, source) == null) {
|
||||||
notification.connect('destroy',
|
notification.connect('destroy',
|
||||||
@ -647,7 +679,7 @@ MessageTray.prototype = {
|
|||||||
let notificationsPending = this._notificationQueue.length > 0;
|
let notificationsPending = this._notificationQueue.length > 0;
|
||||||
let notificationPinned = this._pointerInTray && !this._pointerInSummary && !this._notificationRemoved;
|
let notificationPinned = this._pointerInTray && !this._pointerInSummary && !this._notificationRemoved;
|
||||||
let notificationExpanded = this._notificationBin.y < 0;
|
let notificationExpanded = this._notificationBin.y < 0;
|
||||||
let notificationExpired = (this._notificationTimeoutId == 0 && !this._pointerInTray) || this._notificationRemoved;
|
let notificationExpired = (this._notificationTimeoutId == 0 && !this._pointerInTray && !this._locked) || this._notificationRemoved;
|
||||||
|
|
||||||
if (this._notificationState == State.HIDDEN) {
|
if (this._notificationState == State.HIDDEN) {
|
||||||
if (notificationsPending)
|
if (notificationsPending)
|
||||||
@ -778,6 +810,11 @@ MessageTray.prototype = {
|
|||||||
_hideNotification: function() {
|
_hideNotification: function() {
|
||||||
this._notification.popIn();
|
this._notification.popIn();
|
||||||
|
|
||||||
|
if (this._reExpandNotificationId) {
|
||||||
|
this._notificationBin.disconnect(this._reExpandNotificationId);
|
||||||
|
this._reExpandNotificationId = 0;
|
||||||
|
}
|
||||||
|
|
||||||
this._tween(this._notificationBin, "_notificationState", State.HIDDEN,
|
this._tween(this._notificationBin, "_notificationState", State.HIDDEN,
|
||||||
{ y: this.actor.height,
|
{ y: this.actor.height,
|
||||||
opacity: 0,
|
opacity: 0,
|
||||||
@ -802,6 +839,9 @@ MessageTray.prototype = {
|
|||||||
time: ANIMATION_TIME,
|
time: ANIMATION_TIME,
|
||||||
transition: "easeOutQuad"
|
transition: "easeOutQuad"
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (!this._reExpandNotificationId)
|
||||||
|
this._reExpandNotificationId = this._notificationBin.connect('notify::height', Lang.bind(this, this._expandNotification));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -207,7 +207,7 @@ NotificationDaemon.prototype = {
|
|||||||
|
|
||||||
if (actions.length) {
|
if (actions.length) {
|
||||||
for (let i = 0; i < actions.length - 1; i += 2)
|
for (let i = 0; i < actions.length - 1; i += 2)
|
||||||
notification.addAction(actions[i], actions[i + 1]);
|
notification.addButton(actions[i], actions[i + 1]);
|
||||||
notification.connect('action-invoked', Lang.bind(this, this._actionInvoked, source, id));
|
notification.connect('action-invoked', Lang.bind(this, this._actionInvoked, source, id));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
/* -*- mode: js2; js2-basic-offset: 4; indent-tabs-mode: nil -*- */
|
/* -*- mode: js2; js2-basic-offset: 4; indent-tabs-mode: nil -*- */
|
||||||
|
|
||||||
|
const Clutter = imports.gi.Clutter;
|
||||||
const DBus = imports.dbus;
|
const DBus = imports.dbus;
|
||||||
const Lang = imports.lang;
|
const Lang = imports.lang;
|
||||||
const Shell = imports.gi.Shell;
|
const Shell = imports.gi.Shell;
|
||||||
@ -11,6 +12,11 @@ const Telepathy = imports.misc.telepathy;
|
|||||||
|
|
||||||
let avatarManager;
|
let avatarManager;
|
||||||
|
|
||||||
|
// See Notification.appendMessage
|
||||||
|
const SCROLLBACK_RECENT_TIME = 15 * 60; // 15 minutes
|
||||||
|
const SCROLLBACK_RECENT_LENGTH = 20;
|
||||||
|
const SCROLLBACK_IDLE_LENGTH = 5;
|
||||||
|
|
||||||
// This is GNOME Shell's implementation of the Telepathy "Client"
|
// This is GNOME Shell's implementation of the Telepathy "Client"
|
||||||
// interface. Specifically, the shell is a Telepathy "Approver", which
|
// interface. Specifically, the shell is a Telepathy "Approver", which
|
||||||
// lets us control the routing of incoming messages, a "Handler",
|
// lets us control the routing of incoming messages, a "Handler",
|
||||||
@ -316,7 +322,7 @@ Source.prototype = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this._channelText = new Telepathy.ChannelText(DBus.session, connName, channelPath);
|
this._channelText = new Telepathy.ChannelText(DBus.session, connName, channelPath);
|
||||||
this._receivedId = this._channelText.connect('Received', Lang.bind(this, this._receivedMessage));
|
this._receivedId = this._channelText.connect('Received', Lang.bind(this, this._messageReceived));
|
||||||
|
|
||||||
this._channelText.ListPendingMessagesRemote(false, Lang.bind(this, this._gotPendingMessages));
|
this._channelText.ListPendingMessagesRemote(false, Lang.bind(this, this._gotPendingMessages));
|
||||||
},
|
},
|
||||||
@ -330,7 +336,7 @@ Source.prototype = {
|
|||||||
return;
|
return;
|
||||||
|
|
||||||
for (let i = 0; i < msgs.length; i++)
|
for (let i = 0; i < msgs.length; i++)
|
||||||
this._receivedMessage.apply(this, [this._channel].concat(msgs[i]));
|
this._messageReceived.apply(this, [this._channel].concat(msgs[i]));
|
||||||
},
|
},
|
||||||
|
|
||||||
_channelClosed: function() {
|
_channelClosed: function() {
|
||||||
@ -339,26 +345,131 @@ Source.prototype = {
|
|||||||
this.destroy();
|
this.destroy();
|
||||||
},
|
},
|
||||||
|
|
||||||
_receivedMessage: function(channel, id, timestamp, sender,
|
_messageReceived: function(channel, id, timestamp, sender,
|
||||||
type, flags, text) {
|
type, flags, text) {
|
||||||
if (!Main.messageTray.contains(this))
|
if (!Main.messageTray.contains(this))
|
||||||
Main.messageTray.add(this);
|
Main.messageTray.add(this);
|
||||||
|
|
||||||
let notification = new Notification(this._targetId, this, text);
|
if (!this._notification)
|
||||||
this.notify(notification);
|
this._notification = new Notification(this._targetId, this);
|
||||||
|
this._notification.appendMessage(text);
|
||||||
|
this.notify(this._notification);
|
||||||
|
|
||||||
this._channelText.AcknowledgePendingMessagesRemote([id]);
|
this._channelText.AcknowledgePendingMessagesRemote([id]);
|
||||||
|
},
|
||||||
|
|
||||||
|
respond: function(text) {
|
||||||
|
this._channelText.SendRemote(Telepathy.ChannelTextMessageType.NORMAL, text);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
function Notification(id, source, text) {
|
function Notification(id, source) {
|
||||||
this._init(id, source, text);
|
this._init(id, source);
|
||||||
}
|
}
|
||||||
|
|
||||||
Notification.prototype = {
|
Notification.prototype = {
|
||||||
__proto__: MessageTray.Notification.prototype,
|
__proto__: MessageTray.Notification.prototype,
|
||||||
|
|
||||||
_init: function(id, source, text) {
|
_init: function(id, source) {
|
||||||
MessageTray.Notification.prototype._init.call(this, id, source, source.name, text, true);
|
MessageTray.Notification.prototype._init.call(this, id, source, source.name);
|
||||||
|
this.actor.connect('button-press-event', Lang.bind(this, this._onButtonPress));
|
||||||
|
|
||||||
|
this._responseEntry = new St.Entry({ style_class: 'chat-response' });
|
||||||
|
this._responseEntry.clutter_text.connect('key-focus-in', Lang.bind(this, this._onEntryFocused));
|
||||||
|
this._responseEntry.clutter_text.connect('activate', Lang.bind(this, this._onEntryActivated));
|
||||||
|
this.setActionArea(this._responseEntry);
|
||||||
|
|
||||||
|
this._history = [];
|
||||||
|
},
|
||||||
|
|
||||||
|
appendMessage: function(text) {
|
||||||
|
this.update(this.source.name, text);
|
||||||
|
this._append(text, 'chat-received');
|
||||||
|
},
|
||||||
|
|
||||||
|
_append: function(text, style) {
|
||||||
|
let body = this.addBody(text);
|
||||||
|
body.add_style_class_name(style);
|
||||||
|
this.scrollTo(St.Side.BOTTOM);
|
||||||
|
|
||||||
|
let now = new Date().getTime() / 1000;
|
||||||
|
this._history.unshift({ actor: body, time: now });
|
||||||
|
|
||||||
|
if (this._history.length > 1) {
|
||||||
|
// Keep the scrollback from growing too long. If the most
|
||||||
|
// recent message (before the one we just added) is within
|
||||||
|
// SCROLLBACK_RECENT_TIME, we will keep
|
||||||
|
// SCROLLBACK_RECENT_LENGTH previous messages. Otherwise
|
||||||
|
// we'll keep SCROLLBACK_IDLE_LENGTH messages.
|
||||||
|
|
||||||
|
let lastMessageTime = this._history[1].time;
|
||||||
|
let maxLength = (lastMessageTime < now - SCROLLBACK_RECENT_TIME) ?
|
||||||
|
SCROLLBACK_IDLE_LENGTH : SCROLLBACK_RECENT_LENGTH;
|
||||||
|
if (this._history.length > maxLength) {
|
||||||
|
let expired = this._history.splice(maxLength);
|
||||||
|
for (let i = 0; i < expired.length; i++)
|
||||||
|
expired[i].actor.destroy();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
_onButtonPress: function(notification, event) {
|
||||||
|
if (!this._active)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
let source = event.get_source ();
|
||||||
|
while (source) {
|
||||||
|
if (source == notification)
|
||||||
|
return false;
|
||||||
|
source = source.get_parent();
|
||||||
|
}
|
||||||
|
|
||||||
|
// @source is outside @notification, which has to mean that
|
||||||
|
// we have a pointer grab, and the user clicked outside the
|
||||||
|
// notification, so we should deactivate.
|
||||||
|
this._deactivate();
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
|
||||||
|
_onEntryFocused: function() {
|
||||||
|
if (this._active)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (!Main.pushModal(this.actor))
|
||||||
|
return;
|
||||||
|
Clutter.grab_pointer(this.actor);
|
||||||
|
|
||||||
|
this._active = true;
|
||||||
|
Main.messageTray.lock();
|
||||||
|
},
|
||||||
|
|
||||||
|
_onEntryActivated: function() {
|
||||||
|
let text = this._responseEntry.get_text();
|
||||||
|
if (text == '') {
|
||||||
|
this._deactivate();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._responseEntry.set_text('');
|
||||||
|
this._append(text, 'chat-sent');
|
||||||
|
this.source.respond(text);
|
||||||
|
},
|
||||||
|
|
||||||
|
_deactivate: function() {
|
||||||
|
if (this._active) {
|
||||||
|
Clutter.ungrab_pointer(this.actor);
|
||||||
|
Main.popModal(this.actor);
|
||||||
|
global.stage.set_key_focus(null);
|
||||||
|
|
||||||
|
// We have to do this after calling popModal(), because
|
||||||
|
// that will return the keyboard focus to
|
||||||
|
// this._responseEntry (because that's where it was when
|
||||||
|
// pushModal() was called), which will cause
|
||||||
|
// _onEntryFocused() to be called again, but we don't want
|
||||||
|
// it to do anything.
|
||||||
|
this._active = false;
|
||||||
|
|
||||||
|
Main.messageTray.unlock();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
Loading…
Reference in New Issue
Block a user