From ee8fd1e6131bae3752addba0e6e2e962e6ad12be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=BCllner?= Date: Mon, 15 Feb 2016 12:02:31 +0100 Subject: [PATCH] calendar: Split out message list base classes Currently both the base classes for messages/sections and the message list itself that instantiates the available sections are located in the same module. As a result, it isn't possible to define sections in a different module without introducing circular dependencies. The Calendar module is already unwieldily large, so split it up a bit to avoid it growing even bigger in the future. https://bugzilla.gnome.org/show_bug.cgi?id=756491 --- js/js-resources.gresource.xml | 1 + js/ui/calendar.js | 761 ++-------------------------- js/ui/components/telepathyClient.js | 4 +- js/ui/dateMenu.js | 2 +- js/ui/messageList.js | 713 ++++++++++++++++++++++++++ po/POTFILES.in | 1 + 6 files changed, 747 insertions(+), 735 deletions(-) create mode 100644 js/ui/messageList.js diff --git a/js/js-resources.gresource.xml b/js/js-resources.gresource.xml index 169d952db..e0c522bb3 100644 --- a/js/js-resources.gresource.xml +++ b/js/js-resources.gresource.xml @@ -63,6 +63,7 @@ ui/magnifierDBus.js ui/main.js ui/messageTray.js + ui/messageList.js ui/modalDialog.js ui/notificationDaemon.js ui/osdWindow.js diff --git a/js/ui/calendar.js b/js/ui/calendar.js index d2a7b9ca0..c9a62227e 100644 --- a/js/ui/calendar.js +++ b/js/ui/calendar.js @@ -1,23 +1,18 @@ // -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*- -const Atk = imports.gi.Atk; const Clutter = imports.gi.Clutter; const Gio = imports.gi.Gio; const GLib = imports.gi.GLib; -const GObject = imports.gi.GObject; const Gtk = imports.gi.Gtk; const Lang = imports.lang; const St = imports.gi.St; const Signals = imports.signals; -const Pango = imports.gi.Pango; const Gettext_gtk30 = imports.gettext.domain('gtk30'); -const Mainloop = imports.mainloop; -const Meta = imports.gi.Meta; const Shell = imports.gi.Shell; const Main = imports.ui.main; +const MessageList = imports.ui.messageList; const MessageTray = imports.ui.messageTray; -const Tweener = imports.ui.tweener; const Util = imports.misc.util; const MSECS_IN_DAY = 24 * 60 * 60 * 1000; @@ -26,28 +21,24 @@ const ELLIPSIS_CHAR = '\u2026'; const MESSAGE_ICON_SIZE = 32; -const MESSAGE_ANIMATION_TIME = 0.1; - -const DEFAULT_EXPAND_LINES = 6; - // alias to prevent xgettext from picking up strings translated in GTK+ const gtk30_ = Gettext_gtk30.gettext; const NC_ = function(context, str) { return context + '\u0004' + str; }; -function _sameYear(dateA, dateB) { +function sameYear(dateA, dateB) { return (dateA.getYear() == dateB.getYear()); } -function _sameMonth(dateA, dateB) { - return _sameYear(dateA, dateB) && (dateA.getMonth() == dateB.getMonth()); +function sameMonth(dateA, dateB) { + return sameYear(dateA, dateB) && (dateA.getMonth() == dateB.getMonth()); } -function _sameDay(dateA, dateB) { - return _sameMonth(dateA, dateB) && (dateA.getDate() == dateB.getDate()); +function sameDay(dateA, dateB) { + return sameMonth(dateA, dateB) && (dateA.getDate() == dateB.getDate()); } -function _isToday(date) { - return _sameDay(new Date(), date); +function isToday(date) { + return sameDay(new Date(), date); } function _isWorkDay(date) { @@ -98,148 +89,6 @@ function _getCalendarDayAbbreviation(dayNumber) { return Shell.util_translate_time_string(abbreviations[dayNumber]); } -function _fixMarkup(text, allowMarkup) { - if (allowMarkup) { - // Support &, ", ', < and >, escape all other - // occurrences of '&'. - let _text = text.replace(/&(?!amp;|quot;|apos;|lt;|gt;)/g, '&'); - - // Support , , and , escape anything else - // so it displays as raw markup. - _text = _text.replace(/<(?!\/?[biu]>)/g, '<'); - - try { - Pango.parse_markup(_text, -1, ''); - return _text; - } catch (e) {} - } - - // !allowMarkup, or invalid markup - return GLib.markup_escape_text(text, -1); -} - -const URLHighlighter = new Lang.Class({ - Name: 'URLHighlighter', - - _init: function(text, lineWrap, allowMarkup) { - if (!text) - text = ''; - this.actor = new St.Label({ reactive: true, style_class: 'url-highlighter', - x_expand: true, x_align: Clutter.ActorAlign.START }); - this._linkColor = '#ccccff'; - this.actor.connect('style-changed', Lang.bind(this, function() { - let [hasColor, color] = this.actor.get_theme_node().lookup_color('link-color', false); - if (hasColor) { - let linkColor = color.to_string().substr(0, 7); - if (linkColor != this._linkColor) { - this._linkColor = linkColor; - this._highlightUrls(); - } - } - })); - this.actor.clutter_text.line_wrap = lineWrap; - this.actor.clutter_text.line_wrap_mode = Pango.WrapMode.WORD_CHAR; - - this.setMarkup(text, allowMarkup); - this.actor.connect('button-press-event', Lang.bind(this, function(actor, event) { - // Don't try to URL highlight when invisible. - // The MessageTray doesn't actually hide us, so - // we need to check for paint opacities as well. - if (!actor.visible || actor.get_paint_opacity() == 0) - return Clutter.EVENT_PROPAGATE; - - // Keep Notification.actor from seeing this and taking - // a pointer grab, which would block our button-release-event - // handler, if an URL is clicked - return this._findUrlAtPos(event) != -1; - })); - this.actor.connect('button-release-event', Lang.bind(this, function (actor, event) { - if (!actor.visible || actor.get_paint_opacity() == 0) - return Clutter.EVENT_PROPAGATE; - - let urlId = this._findUrlAtPos(event); - if (urlId != -1) { - let url = this._urls[urlId].url; - if (url.indexOf(':') == -1) - url = 'http://' + url; - - Gio.app_info_launch_default_for_uri(url, global.create_app_launch_context(0, -1)); - return Clutter.EVENT_STOP; - } - return Clutter.EVENT_PROPAGATE; - })); - this.actor.connect('motion-event', Lang.bind(this, function(actor, event) { - if (!actor.visible || actor.get_paint_opacity() == 0) - return Clutter.EVENT_PROPAGATE; - - let urlId = this._findUrlAtPos(event); - if (urlId != -1 && !this._cursorChanged) { - global.screen.set_cursor(Meta.Cursor.POINTING_HAND); - this._cursorChanged = true; - } else if (urlId == -1) { - global.screen.set_cursor(Meta.Cursor.DEFAULT); - this._cursorChanged = false; - } - return Clutter.EVENT_PROPAGATE; - })); - this.actor.connect('leave-event', Lang.bind(this, function() { - if (!this.actor.visible || this.actor.get_paint_opacity() == 0) - return Clutter.EVENT_PROPAGATE; - - if (this._cursorChanged) { - this._cursorChanged = false; - global.screen.set_cursor(Meta.Cursor.DEFAULT); - } - return Clutter.EVENT_PROPAGATE; - })); - }, - - setMarkup: function(text, allowMarkup) { - text = text ? _fixMarkup(text, allowMarkup) : ''; - this._text = text; - - this.actor.clutter_text.set_markup(text); - /* clutter_text.text contain text without markup */ - this._urls = Util.findUrls(this.actor.clutter_text.text); - this._highlightUrls(); - }, - - _highlightUrls: function() { - // text here contain markup - let urls = Util.findUrls(this._text); - let markup = ''; - let pos = 0; - for (let i = 0; i < urls.length; i++) { - let url = urls[i]; - let str = this._text.substr(pos, url.pos - pos); - markup += str + '' + url.url + ''; - pos = url.pos + url.url.length; - } - markup += this._text.substr(pos); - this.actor.clutter_text.set_markup(markup); - }, - - _findUrlAtPos: function(event) { - let success; - let [x, y] = event.get_coords(); - [success, x, y] = this.actor.transform_stage_point(x, y); - let find_pos = -1; - for (let i = 0; i < this.actor.clutter_text.text.length; i++) { - let [success, px, py, line_height] = this.actor.clutter_text.position_to_coords(i); - if (py > y || py + line_height < y || x < px) - continue; - find_pos = i; - } - if (find_pos != -1) { - for (let i = 0; i < this._urls.length; i++) - if (find_pos >= this._urls[i].pos && - this._urls[i].pos + this._urls[i].url.length > find_pos) - return i; - } - return -1; - } -}); - // Abstraction for an appointment/event in a calendar const CalendarEvent = new Lang.Class({ @@ -544,7 +393,7 @@ const Calendar = new Lang.Class({ // Sets the calendar to show a specific date setDate: function(date) { - if (_sameDay(date, this._selectedDate)) + if (sameDay(date, this._selectedDate)) return; this._selectedDate = date; @@ -757,7 +606,7 @@ const Calendar = new Lang.Class({ if (leftMost) styleClass = 'calendar-day-left ' + styleClass; - if (_sameDay(now, iter)) + if (sameDay(now, iter)) styleClass += ' calendar-today'; else if (iter.getMonth() != this._selectedDate.getMonth()) styleClass += ' calendar-other-month-day'; @@ -800,16 +649,16 @@ const Calendar = new Lang.Class({ _update: function() { let now = new Date(); - if (_sameYear(this._selectedDate, now)) + if (sameYear(this._selectedDate, now)) this._monthLabel.text = this._selectedDate.toLocaleFormat(this._headerFormatWithoutYear); else this._monthLabel.text = this._selectedDate.toLocaleFormat(this._headerFormat); - if (!this._calendarBegin || !_sameMonth(this._selectedDate, this._calendarBegin) || !_sameDay(now, this._markedAsToday)) + if (!this._calendarBegin || !sameMonth(this._selectedDate, this._calendarBegin) || !sameDay(now, this._markedAsToday)) this._rebuildCalendar(); this._buttons.forEach(Lang.bind(this, function(button) { - if (_sameDay(button._date, this._selectedDate)) { + if (sameDay(button._date, this._selectedDate)) { button.add_style_pseudo_class('active'); if (this._shouldDateGrabFocus) button.grab_key_focus(); @@ -821,353 +670,9 @@ const Calendar = new Lang.Class({ }); Signals.addSignalMethods(Calendar.prototype); -const ScaleLayout = new Lang.Class({ - Name: 'ScaleLayout', - Extends: Clutter.BinLayout, - - _connectContainer: function(container) { - if (this._container == container) - return; - - if (this._container) - for (let id of this._signals) - this._container.disconnect(id); - - this._container = container; - this._signals = []; - - if (this._container) - for (let signal of ['notify::scale-x', 'notify::scale-y']) { - let id = this._container.connect(signal, Lang.bind(this, - function() { - this.layout_changed(); - })); - this._signals.push(id); - } - }, - - vfunc_get_preferred_width: function(container, forHeight) { - this._connectContainer(container); - - let [min, nat] = this.parent(container, forHeight); - return [Math.floor(min * container.scale_x), - Math.floor(nat * container.scale_x)]; - }, - - vfunc_get_preferred_height: function(container, forWidth) { - this._connectContainer(container); - - let [min, nat] = this.parent(container, forWidth); - return [Math.floor(min * container.scale_y), - Math.floor(nat * container.scale_y)]; - } -}); - -const LabelExpanderLayout = new Lang.Class({ - Name: 'LabelExpanderLayout', - Extends: Clutter.LayoutManager, - Properties: { 'expansion': GObject.ParamSpec.double('expansion', - 'Expansion', - 'Expansion of the layout, between 0 (collapsed) ' + - 'and 1 (fully expanded', - GObject.ParamFlags.READABLE | GObject.ParamFlags.WRITABLE, - 0, 1, 0)}, - - _init: function(params) { - this._expansion = 0; - this._expandLines = DEFAULT_EXPAND_LINES; - - this.parent(params); - }, - - get expansion() { - return this._expansion; - }, - - set expansion(v) { - if (v == this._expansion) - return; - this._expansion = v; - this.notify('expansion'); - - let visibleIndex = this._expansion > 0 ? 1 : 0; - for (let i = 0; this._container && i < this._container.get_n_children(); i++) - this._container.get_child_at_index(i).visible = (i == visibleIndex); - - this.layout_changed(); - }, - - set expandLines(v) { - if (v == this._expandLines) - return; - this._expandLines = v; - if (this._expansion > 0) - this.layout_changed(); - }, - - vfunc_set_container: function(container) { - this._container = container; - }, - - vfunc_get_preferred_width: function(container, forHeight) { - let [min, nat] = [0, 0]; - - for (let i = 0; i < container.get_n_children(); i++) { - if (i > 1) - break; // we support one unexpanded + one expanded child - - let child = container.get_child_at_index(i); - let [childMin, childNat] = child.get_preferred_width(forHeight); - [min, nat] = [Math.max(min, childMin), Math.max(nat, childNat)]; - } - - return [min, nat]; - }, - - vfunc_get_preferred_height: function(container, forWidth) { - let [min, nat] = [0, 0]; - - let children = container.get_children(); - if (children[0]) - [min, nat] = children[0].get_preferred_height(forWidth); - - if (children[1]) { - let [min2, nat2] = children[1].get_preferred_height(forWidth); - let [expMin, expNat] = [Math.min(min2, min * this._expandLines), - Math.min(nat2, nat * this._expandLines)]; - [min, nat] = [min + this._expansion * (expMin - min), - nat + this._expansion * (expNat - nat)]; - } - - return [min, nat]; - }, - - vfunc_allocate: function(container, box, flags) { - for (let i = 0; i < container.get_n_children(); i++) { - let child = container.get_child_at_index(i); - - if (child.visible) - child.allocate(box, flags); - } - - } -}); - -const Message = new Lang.Class({ - Name: 'Message', - - _init: function(title, body) { - this.expanded = false; - - this.actor = new St.Button({ style_class: 'message', - accessible_role: Atk.Role.NOTIFICATION, - can_focus: true, - x_expand: true, x_fill: true }); - this.actor.connect('key-press-event', - Lang.bind(this, this._onKeyPressed)); - - let vbox = new St.BoxLayout({ vertical: true }); - this.actor.set_child(vbox); - - let hbox = new St.BoxLayout(); - vbox.add_actor(hbox); - - this._actionBin = new St.Widget({ layout_manager: new ScaleLayout(), - visible: false }); - vbox.add_actor(this._actionBin); - - this._iconBin = new St.Bin({ style_class: 'message-icon-bin', - y_expand: true, - visible: false }); - hbox.add_actor(this._iconBin); - - let contentBox = new St.BoxLayout({ style_class: 'message-content', - vertical: true, x_expand: true }); - hbox.add_actor(contentBox); - - let titleBox = new St.BoxLayout(); - contentBox.add_actor(titleBox); - - this.titleLabel = new St.Label({ style_class: 'message-title', - x_expand: true, - x_align: Clutter.ActorAlign.START }); - this.setTitle(title); - titleBox.add_actor(this.titleLabel); - - this._secondaryBin = new St.Bin({ style_class: 'message-secondary-bin' }); - titleBox.add_actor(this._secondaryBin); - - let closeIcon = new St.Icon({ icon_name: 'window-close-symbolic', - icon_size: 16 }); - this._closeButton = new St.Button({ child: closeIcon, visible: false }); - titleBox.add_actor(this._closeButton); - - this._bodyStack = new St.Widget({ x_expand: true }); - this._bodyStack.layout_manager = new LabelExpanderLayout(); - contentBox.add_actor(this._bodyStack); - - this.bodyLabel = new URLHighlighter('', false, this._useBodyMarkup); - this.bodyLabel.actor.add_style_class_name('message-body'); - this._bodyStack.add_actor(this.bodyLabel.actor); - this.setBody(body); - - this._closeButton.connect('clicked', Lang.bind(this, this.close)); - this.actor.connect('notify::hover', Lang.bind(this, this._sync)); - this.actor.connect('clicked', Lang.bind(this, this._onClicked)); - this.actor.connect('destroy', Lang.bind(this, this._onDestroy)); - this._sync(); - }, - - close: function() { - this.emit('close'); - }, - - setIcon: function(actor) { - this._iconBin.child = actor; - this._iconBin.visible = (actor != null); - }, - - setSecondaryActor: function(actor) { - this._secondaryBin.child = actor; - }, - - setTitle: function(text) { - let title = text ? _fixMarkup(text.replace(/\n/g, ' '), false) : ''; - this.titleLabel.clutter_text.set_markup(title); - }, - - setBody: function(text) { - this._bodyText = text; - this.bodyLabel.setMarkup(text ? text.replace(/\n/g, ' ') : '', - this._useBodyMarkup); - if (this._expandedLabel) - this._expandedLabel.setMarkup(text, this._useBodyMarkup); - }, - - setUseBodyMarkup: function(enable) { - if (this._useBodyMarkup === enable) - return; - this._useBodyMarkup = enable; - if (this.bodyLabel) - this.setBody(this._bodyText); - }, - - setActionArea: function(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_actor(actor); - this._actionBin.visible = this.expanded; - }, - - setExpandedBody: function(actor) { - if (actor == null) { - if (this._bodyStack.get_n_children() > 1) - this._bodyStack.get_child_at_index(1).destroy(); - return; - } - - if (this._bodyStack.get_n_children() > 1) - throw new Error('Message already has an expanded body actor'); - - this._bodyStack.insert_child_at_index(actor, 1); - }, - - setExpandedLines: function(nLines) { - this._bodyStack.layout_manager.expandLines = nLines; - }, - - expand: function(animate) { - this.expanded = true; - - this._actionBin.visible = (this._actionBin.get_n_children() > 0); - - if (this._bodyStack.get_n_children() < 2) { - this._expandedLabel = new URLHighlighter(this._bodyText, - true, this._useBodyMarkup); - this.setExpandedBody(this._expandedLabel.actor); - } - - if (animate) { - Tweener.addTween(this._bodyStack.layout_manager, - { expansion: 1, - time: MessageTray.ANIMATION_TIME, - transition: 'easeOutQuad' }); - this._actionBin.scale_y = 0; - Tweener.addTween(this._actionBin, - { scale_y: 1, - time: MessageTray.ANIMATION_TIME, - transition: 'easeOutQuad' }); - } else { - this._bodyStack.layout_manager.expansion = 1; - this._actionBin.scale_y = 1; - } - - this.emit('expanded'); - }, - - unexpand: function(animate) { - if (animate) { - Tweener.addTween(this._bodyStack.layout_manager, - { expansion: 0, - time: MessageTray.ANIMATION_TIME, - transition: 'easeOutQuad' }); - Tweener.addTween(this._actionBin, - { scale_y: 0, - time: MessageTray.ANIMATION_TIME, - transition: 'easeOutQuad', - onCompleteScope: this, - onComplete: function() { - this._actionBin.hide(); - this.expanded = false; - }}); - } else { - this._bodyStack.layout_manager.expansion = 0; - this._actionBin.scale_y = 0; - this.expanded = false; - } - - this.emit('unexpanded'); - }, - - canClose: function() { - return true; - }, - - _sync: function() { - let hovered = this.actor.hover; - this._closeButton.visible = hovered && this.canClose(); - this._secondaryBin.visible = !hovered; - }, - - _onClicked: function() { - }, - - _onDestroy: function() { - }, - - _onKeyPressed: function(a, event) { - let keysym = event.get_key_symbol(); - - if (keysym == Clutter.KEY_Delete || - keysym == Clutter.KEY_KP_Delete) { - this.close(); - return Clutter.EVENT_STOP; - } - return Clutter.EVENT_PROPAGATE; - } -}); -Signals.addSignalMethods(Message.prototype); - const EventMessage = new Lang.Class({ Name: 'EventMessage', - Extends: Message, + Extends: MessageList.Message, _init: function(event, date) { this._event = event; @@ -1210,13 +715,13 @@ const EventMessage = new Lang.Class({ }, canClose: function() { - return _isToday(this._date); + return isToday(this._date); } }); const NotificationMessage = new Lang.Class({ Name: 'NotificationMessage', - Extends: Message, + Extends: MessageList.Message, _init: function(notification) { this.notification = notification; @@ -1270,217 +775,9 @@ const NotificationMessage = new Lang.Class({ } }); -const MessageListSection = new Lang.Class({ - Name: 'MessageListSection', - - _init: function(title) { - this.actor = new St.BoxLayout({ style_class: 'message-list-section', - clip_to_allocation: true, - x_expand: true, vertical: true }); - let titleBox = new St.BoxLayout({ style_class: 'message-list-section-title-box' }); - this.actor.add_actor(titleBox); - - this._title = new St.Button({ style_class: 'message-list-section-title', - label: title, - can_focus: true, - x_expand: true, - x_align: St.Align.START }); - titleBox.add_actor(this._title); - - this._title.connect('clicked', Lang.bind(this, this._onTitleClicked)); - this._title.connect('key-focus-in', Lang.bind(this, this._onKeyFocusIn)); - - let closeIcon = new St.Icon({ icon_name: 'window-close-symbolic' }); - this._closeButton = new St.Button({ style_class: 'message-list-section-close', - child: closeIcon, - accessible_name: _("Clear section"), - can_focus: true }); - this._closeButton.set_x_align(Clutter.ActorAlign.END); - titleBox.add_actor(this._closeButton); - - this._closeButton.connect('clicked', Lang.bind(this, this.clear)); - - this._list = new St.BoxLayout({ style_class: 'message-list-section-list', - vertical: true }); - this.actor.add_actor(this._list); - - this._list.connect('actor-added', Lang.bind(this, this._sync)); - this._list.connect('actor-removed', Lang.bind(this, this._sync)); - - let id = Main.sessionMode.connect('updated', - Lang.bind(this, this._sync)); - this.actor.connect('destroy', function() { - Main.sessionMode.disconnect(id); - }); - - this._messages = new Map(); - this._date = new Date(); - this.empty = true; - this._sync(); - }, - - _onTitleClicked: function() { - Main.overview.hide(); - Main.panel.closeCalendar(); - }, - - _onKeyFocusIn: function(actor) { - this.emit('key-focus-in', actor); - }, - - get allowed() { - return true; - }, - - setDate: function(date) { - if (_sameDay(date, this._date)) - return; - this._date = date; - this._sync(); - }, - - addMessage: function(message, animate) { - this.addMessageAtIndex(message, -1, animate); - }, - - addMessageAtIndex: function(message, index, animate) { - let obj = { - container: null, - destroyId: 0, - keyFocusId: 0, - closeId: 0 - }; - let pivot = new Clutter.Point({ x: .5, y: .5 }); - let scale = animate ? 0 : 1; - obj.container = new St.Widget({ layout_manager: new ScaleLayout(), - pivot_point: pivot, - scale_x: scale, scale_y: scale }); - obj.keyFocusId = message.actor.connect('key-focus-in', - Lang.bind(this, this._onKeyFocusIn)); - obj.destroyId = message.actor.connect('destroy', - Lang.bind(this, function() { - this.removeMessage(message, false); - })); - obj.closeId = message.connect('close', - Lang.bind(this, function() { - this.removeMessage(message, true); - })); - - this._messages.set(message, obj); - obj.container.add_actor(message.actor); - - this._list.insert_child_at_index(obj.container, index); - - if (animate) - Tweener.addTween(obj.container, { scale_x: 1, - scale_y: 1, - time: MESSAGE_ANIMATION_TIME, - transition: 'easeOutQuad' }); - }, - - moveMessage: function(message, index, animate) { - let obj = this._messages.get(message); - - if (!animate) { - this._list.set_child_at_index(obj.container, index); - return; - } - - let onComplete = Lang.bind(this, function() { - this._list.set_child_at_index(obj.container, index); - Tweener.addTween(obj.container, { scale_x: 1, - scale_y: 1, - time: MESSAGE_ANIMATION_TIME, - transition: 'easeOutQuad' }); - }); - Tweener.addTween(obj.container, { scale_x: 0, - scale_y: 0, - time: MESSAGE_ANIMATION_TIME, - transition: 'easeOutQuad', - onComplete: onComplete }); - }, - - removeMessage: function(message, animate) { - let obj = this._messages.get(message); - - message.actor.disconnect(obj.destroyId); - message.actor.disconnect(obj.keyFocusId); - message.disconnect(obj.closeId); - - this._messages.delete(message); - - if (animate) { - Tweener.addTween(obj.container, { scale_x: 0, scale_y: 0, - time: MESSAGE_ANIMATION_TIME, - transition: 'easeOutQuad', - onComplete: function() { - obj.container.destroy(); - global.sync_pointer(); - }}); - } else { - obj.container.destroy(); - global.sync_pointer(); - } - }, - - clear: function() { - let messages = [...this._messages.keys()].filter(function(message) { - return message.canClose(); - }); - - // If there are few messages, letting them all zoom out looks OK - if (messages.length < 2) { - messages.forEach(function(message) { - message.close(); - }); - } else { - // Otherwise we slide them out one by one, and then zoom them - // out "off-screen" in the end to smoothly shrink the parent - let delay = MESSAGE_ANIMATION_TIME / Math.max(messages.length, 5); - for (let i = 0; i < messages.length; i++) { - let message = messages[i]; - let obj = this._messages.get(message); - Tweener.addTween(obj.container, - { anchor_x: this._list.width, - opacity: 0, - time: MESSAGE_ANIMATION_TIME, - delay: i * delay, - transition: 'easeOutQuad', - onComplete: function() { - message.close(); - }}); - } - } - }, - - _canClear: function() { - for (let message of this._messages.keys()) - if (message.canClose()) - return true; - return false; - }, - - _shouldShow: function() { - return !this.empty; - }, - - _sync: function() { - let empty = this._list.get_n_children() == 0; - let changed = this.empty !== empty; - this.empty = empty; - - if (changed) - this.emit('empty-changed'); - - this._closeButton.visible = this._canClear(); - this.actor.visible = this.allowed && this._shouldShow(); - } -}); -Signals.addSignalMethods(MessageListSection.prototype); - const EventsSection = new Lang.Class({ Name: 'EventsSection', - Extends: MessageListSection, + Extends: MessageList.MessageListSection, _init: function() { this._desktopSettings = new Gio.Settings({ schema_id: 'org.gnome.desktop.interface' }); @@ -1519,14 +816,14 @@ const EventsSection = new Lang.Class({ }, _updateTitle: function() { - if (_isToday(this._date)) { + if (isToday(this._date)) { this._title.label = _("Events"); return; } let dayFormat; let now = new Date(); - if (_sameYear(this._date, now)) + if (sameYear(this._date, now)) /* Translators: Shown on calendar heading when selected day occurs on current year */ dayFormat = Shell.util_translate_time_string(NC_("calendar heading", "%A, %B %d")); @@ -1602,7 +899,7 @@ const EventsSection = new Lang.Class({ }, _shouldShow: function() { - return !this.empty || !_isToday(this._date); + return !this.empty || !isToday(this._date); }, _sync: function() { @@ -1615,7 +912,7 @@ const EventsSection = new Lang.Class({ const NotificationSection = new Lang.Class({ Name: 'NotificationSection', - Extends: MessageListSection, + Extends: MessageList.MessageListSection, _init: function() { this.parent(_("Notifications")); @@ -1721,7 +1018,7 @@ const NotificationSection = new Lang.Class({ }, _shouldShow: function() { - return !this.empty && _isToday(this._date); + return !this.empty && isToday(this._date); }, _sync: function() { @@ -1754,20 +1051,20 @@ const Placeholder = new Lang.Class({ }, setDate: function(date) { - if (_sameDay(this._date, date)) + if (sameDay(this._date, date)) return; this._date = date; this._sync(); }, _sync: function() { - let isToday = _isToday(this._date); - if (isToday && this._icon.gicon == this._todayIcon) + let today = isToday(this._date); + if (today && this._icon.gicon == this._todayIcon) return; - if (!isToday && this._icon.gicon == this._otherIcon) + if (!today && this._icon.gicon == this._otherIcon) return; - if (isToday) { + if (today) { this._icon.gicon = this._todayIcon; this._label.text = _("No Notifications"); } else { @@ -1777,8 +1074,8 @@ const Placeholder = new Lang.Class({ } }); -const MessageList = new Lang.Class({ - Name: 'MessageList', +const CalendarMessageList = new Lang.Class({ + Name: 'CalendarMessageList', _init: function() { this.actor = new St.Widget({ style_class: 'message-list', diff --git a/js/ui/components/telepathyClient.js b/js/ui/components/telepathyClient.js index f57131975..bde21a8fa 100644 --- a/js/ui/components/telepathyClient.js +++ b/js/ui/components/telepathyClient.js @@ -12,9 +12,9 @@ const St = imports.gi.St; const Tpl = imports.gi.TelepathyLogger; const Tp = imports.gi.TelepathyGLib; -const Calendar = imports.ui.calendar; const History = imports.misc.history; const Main = imports.ui.main; +const MessageList = imports.ui.messageList; const MessageTray = imports.ui.messageTray; const Params = imports.misc.params; const PopupMenu = imports.ui.popupMenu; @@ -866,7 +866,7 @@ const ChatNotificationBanner = new Lang.Class({ }, _addMessage: function(message) { - let highlighter = new Calendar.URLHighlighter(message.body, true, true); + let highlighter = new MessageList.URLHighlighter(message.body, true, true); let body = highlighter.actor; let styles = message.styles; diff --git a/js/ui/dateMenu.js b/js/ui/dateMenu.js index 12d7eea15..98c4aa4c3 100644 --- a/js/ui/dateMenu.js +++ b/js/ui/dateMenu.js @@ -360,7 +360,7 @@ const DateMenuButton = new Lang.Class({ })); // Fill up the first column - this._messageList = new Calendar.MessageList(); + this._messageList = new Calendar.CalendarMessageList(); hbox.add(this._messageList.actor, { expand: true, y_fill: false, y_align: St.Align.START }); // Fill up the second column diff --git a/js/ui/messageList.js b/js/ui/messageList.js new file mode 100644 index 000000000..344eee978 --- /dev/null +++ b/js/ui/messageList.js @@ -0,0 +1,713 @@ +const Atk = imports.gi.Atk; +const Clutter = imports.gi.Clutter; +const GLib = imports.gi.GLib; +const GObject = imports.gi.GObject; +const Lang = imports.lang; +const Main = imports.ui.main; +const MessageTray = imports.ui.messageTray; +const Meta = imports.gi.Meta; +const Pango = imports.gi.Pango; +const Signals = imports.signals; +const St = imports.gi.St; + +const Calendar = imports.ui.calendar; +const Tweener = imports.ui.tweener; +const Util = imports.misc.util; + +const MESSAGE_ANIMATION_TIME = 0.1; + +const DEFAULT_EXPAND_LINES = 6; + +function _fixMarkup(text, allowMarkup) { + if (allowMarkup) { + // Support &, ", ', < and >, escape all other + // occurrences of '&'. + let _text = text.replace(/&(?!amp;|quot;|apos;|lt;|gt;)/g, '&'); + + // Support , , and , escape anything else + // so it displays as raw markup. + _text = _text.replace(/<(?!\/?[biu]>)/g, '<'); + + try { + Pango.parse_markup(_text, -1, ''); + return _text; + } catch (e) {} + } + + // !allowMarkup, or invalid markup + return GLib.markup_escape_text(text, -1); +} + +const URLHighlighter = new Lang.Class({ + Name: 'URLHighlighter', + + _init: function(text, lineWrap, allowMarkup) { + if (!text) + text = ''; + this.actor = new St.Label({ reactive: true, style_class: 'url-highlighter', + x_expand: true, x_align: Clutter.ActorAlign.START }); + this._linkColor = '#ccccff'; + this.actor.connect('style-changed', Lang.bind(this, function() { + let [hasColor, color] = this.actor.get_theme_node().lookup_color('link-color', false); + if (hasColor) { + let linkColor = color.to_string().substr(0, 7); + if (linkColor != this._linkColor) { + this._linkColor = linkColor; + this._highlightUrls(); + } + } + })); + this.actor.clutter_text.line_wrap = lineWrap; + this.actor.clutter_text.line_wrap_mode = Pango.WrapMode.WORD_CHAR; + + this.setMarkup(text, allowMarkup); + this.actor.connect('button-press-event', Lang.bind(this, function(actor, event) { + // Don't try to URL highlight when invisible. + // The MessageTray doesn't actually hide us, so + // we need to check for paint opacities as well. + if (!actor.visible || actor.get_paint_opacity() == 0) + return Clutter.EVENT_PROPAGATE; + + // Keep Notification.actor from seeing this and taking + // a pointer grab, which would block our button-release-event + // handler, if an URL is clicked + return this._findUrlAtPos(event) != -1; + })); + this.actor.connect('button-release-event', Lang.bind(this, function (actor, event) { + if (!actor.visible || actor.get_paint_opacity() == 0) + return Clutter.EVENT_PROPAGATE; + + let urlId = this._findUrlAtPos(event); + if (urlId != -1) { + let url = this._urls[urlId].url; + if (url.indexOf(':') == -1) + url = 'http://' + url; + + Gio.app_info_launch_default_for_uri(url, global.create_app_launch_context(0, -1)); + return Clutter.EVENT_STOP; + } + return Clutter.EVENT_PROPAGATE; + })); + this.actor.connect('motion-event', Lang.bind(this, function(actor, event) { + if (!actor.visible || actor.get_paint_opacity() == 0) + return Clutter.EVENT_PROPAGATE; + + let urlId = this._findUrlAtPos(event); + if (urlId != -1 && !this._cursorChanged) { + global.screen.set_cursor(Meta.Cursor.POINTING_HAND); + this._cursorChanged = true; + } else if (urlId == -1) { + global.screen.set_cursor(Meta.Cursor.DEFAULT); + this._cursorChanged = false; + } + return Clutter.EVENT_PROPAGATE; + })); + this.actor.connect('leave-event', Lang.bind(this, function() { + if (!this.actor.visible || this.actor.get_paint_opacity() == 0) + return Clutter.EVENT_PROPAGATE; + + if (this._cursorChanged) { + this._cursorChanged = false; + global.screen.set_cursor(Meta.Cursor.DEFAULT); + } + return Clutter.EVENT_PROPAGATE; + })); + }, + + setMarkup: function(text, allowMarkup) { + text = text ? _fixMarkup(text, allowMarkup) : ''; + this._text = text; + + this.actor.clutter_text.set_markup(text); + /* clutter_text.text contain text without markup */ + this._urls = Util.findUrls(this.actor.clutter_text.text); + this._highlightUrls(); + }, + + _highlightUrls: function() { + // text here contain markup + let urls = Util.findUrls(this._text); + let markup = ''; + let pos = 0; + for (let i = 0; i < urls.length; i++) { + let url = urls[i]; + let str = this._text.substr(pos, url.pos - pos); + markup += str + '' + url.url + ''; + pos = url.pos + url.url.length; + } + markup += this._text.substr(pos); + this.actor.clutter_text.set_markup(markup); + }, + + _findUrlAtPos: function(event) { + let success; + let [x, y] = event.get_coords(); + [success, x, y] = this.actor.transform_stage_point(x, y); + let find_pos = -1; + for (let i = 0; i < this.actor.clutter_text.text.length; i++) { + let [success, px, py, line_height] = this.actor.clutter_text.position_to_coords(i); + if (py > y || py + line_height < y || x < px) + continue; + find_pos = i; + } + if (find_pos != -1) { + for (let i = 0; i < this._urls.length; i++) + if (find_pos >= this._urls[i].pos && + this._urls[i].pos + this._urls[i].url.length > find_pos) + return i; + } + return -1; + } +}); + +const ScaleLayout = new Lang.Class({ + Name: 'ScaleLayout', + Extends: Clutter.BinLayout, + + _connectContainer: function(container) { + if (this._container == container) + return; + + if (this._container) + for (let id of this._signals) + this._container.disconnect(id); + + this._container = container; + this._signals = []; + + if (this._container) + for (let signal of ['notify::scale-x', 'notify::scale-y']) { + let id = this._container.connect(signal, Lang.bind(this, + function() { + this.layout_changed(); + })); + this._signals.push(id); + } + }, + + vfunc_get_preferred_width: function(container, forHeight) { + this._connectContainer(container); + + let [min, nat] = this.parent(container, forHeight); + return [Math.floor(min * container.scale_x), + Math.floor(nat * container.scale_x)]; + }, + + vfunc_get_preferred_height: function(container, forWidth) { + this._connectContainer(container); + + let [min, nat] = this.parent(container, forWidth); + return [Math.floor(min * container.scale_y), + Math.floor(nat * container.scale_y)]; + } +}); + +const LabelExpanderLayout = new Lang.Class({ + Name: 'LabelExpanderLayout', + Extends: Clutter.LayoutManager, + Properties: { 'expansion': GObject.ParamSpec.double('expansion', + 'Expansion', + 'Expansion of the layout, between 0 (collapsed) ' + + 'and 1 (fully expanded', + GObject.ParamFlags.READABLE | GObject.ParamFlags.WRITABLE, + 0, 1, 0)}, + + _init: function(params) { + this._expansion = 0; + this._expandLines = DEFAULT_EXPAND_LINES; + + this.parent(params); + }, + + get expansion() { + return this._expansion; + }, + + set expansion(v) { + if (v == this._expansion) + return; + this._expansion = v; + this.notify('expansion'); + + let visibleIndex = this._expansion > 0 ? 1 : 0; + for (let i = 0; this._container && i < this._container.get_n_children(); i++) + this._container.get_child_at_index(i).visible = (i == visibleIndex); + + this.layout_changed(); + }, + + set expandLines(v) { + if (v == this._expandLines) + return; + this._expandLines = v; + if (this._expansion > 0) + this.layout_changed(); + }, + + vfunc_set_container: function(container) { + this._container = container; + }, + + vfunc_get_preferred_width: function(container, forHeight) { + let [min, nat] = [0, 0]; + + for (let i = 0; i < container.get_n_children(); i++) { + if (i > 1) + break; // we support one unexpanded + one expanded child + + let child = container.get_child_at_index(i); + let [childMin, childNat] = child.get_preferred_width(forHeight); + [min, nat] = [Math.max(min, childMin), Math.max(nat, childNat)]; + } + + return [min, nat]; + }, + + vfunc_get_preferred_height: function(container, forWidth) { + let [min, nat] = [0, 0]; + + let children = container.get_children(); + if (children[0]) + [min, nat] = children[0].get_preferred_height(forWidth); + + if (children[1]) { + let [min2, nat2] = children[1].get_preferred_height(forWidth); + let [expMin, expNat] = [Math.min(min2, min * this._expandLines), + Math.min(nat2, nat * this._expandLines)]; + [min, nat] = [min + this._expansion * (expMin - min), + nat + this._expansion * (expNat - nat)]; + } + + return [min, nat]; + }, + + vfunc_allocate: function(container, box, flags) { + for (let i = 0; i < container.get_n_children(); i++) { + let child = container.get_child_at_index(i); + + if (child.visible) + child.allocate(box, flags); + } + + } +}); + +const Message = new Lang.Class({ + Name: 'Message', + + _init: function(title, body) { + this.expanded = false; + + this.actor = new St.Button({ style_class: 'message', + accessible_role: Atk.Role.NOTIFICATION, + can_focus: true, + x_expand: true, x_fill: true }); + this.actor.connect('key-press-event', + Lang.bind(this, this._onKeyPressed)); + + let vbox = new St.BoxLayout({ vertical: true }); + this.actor.set_child(vbox); + + let hbox = new St.BoxLayout(); + vbox.add_actor(hbox); + + this._actionBin = new St.Widget({ layout_manager: new ScaleLayout(), + visible: false }); + vbox.add_actor(this._actionBin); + + this._iconBin = new St.Bin({ style_class: 'message-icon-bin', + y_expand: true, + visible: false }); + hbox.add_actor(this._iconBin); + + let contentBox = new St.BoxLayout({ style_class: 'message-content', + vertical: true, x_expand: true }); + hbox.add_actor(contentBox); + + let titleBox = new St.BoxLayout(); + contentBox.add_actor(titleBox); + + this.titleLabel = new St.Label({ style_class: 'message-title', + x_expand: true, + x_align: Clutter.ActorAlign.START }); + this.setTitle(title); + titleBox.add_actor(this.titleLabel); + + this._secondaryBin = new St.Bin({ style_class: 'message-secondary-bin' }); + titleBox.add_actor(this._secondaryBin); + + let closeIcon = new St.Icon({ icon_name: 'window-close-symbolic', + icon_size: 16 }); + this._closeButton = new St.Button({ child: closeIcon, visible: false }); + titleBox.add_actor(this._closeButton); + + this._bodyStack = new St.Widget({ x_expand: true }); + this._bodyStack.layout_manager = new LabelExpanderLayout(); + contentBox.add_actor(this._bodyStack); + + this.bodyLabel = new URLHighlighter('', false, this._useBodyMarkup); + this.bodyLabel.actor.add_style_class_name('message-body'); + this._bodyStack.add_actor(this.bodyLabel.actor); + this.setBody(body); + + this._closeButton.connect('clicked', Lang.bind(this, this.close)); + this.actor.connect('notify::hover', Lang.bind(this, this._sync)); + this.actor.connect('clicked', Lang.bind(this, this._onClicked)); + this.actor.connect('destroy', Lang.bind(this, this._onDestroy)); + this._sync(); + }, + + close: function() { + this.emit('close'); + }, + + setIcon: function(actor) { + this._iconBin.child = actor; + this._iconBin.visible = (actor != null); + }, + + setSecondaryActor: function(actor) { + this._secondaryBin.child = actor; + }, + + setTitle: function(text) { + let title = text ? _fixMarkup(text.replace(/\n/g, ' '), false) : ''; + this.titleLabel.clutter_text.set_markup(title); + }, + + setBody: function(text) { + this._bodyText = text; + this.bodyLabel.setMarkup(text ? text.replace(/\n/g, ' ') : '', + this._useBodyMarkup); + if (this._expandedLabel) + this._expandedLabel.setMarkup(text, this._useBodyMarkup); + }, + + setUseBodyMarkup: function(enable) { + if (this._useBodyMarkup === enable) + return; + this._useBodyMarkup = enable; + if (this.bodyLabel) + this.setBody(this._bodyText); + }, + + setActionArea: function(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_actor(actor); + this._actionBin.visible = this.expanded; + }, + + setExpandedBody: function(actor) { + if (actor == null) { + if (this._bodyStack.get_n_children() > 1) + this._bodyStack.get_child_at_index(1).destroy(); + return; + } + + if (this._bodyStack.get_n_children() > 1) + throw new Error('Message already has an expanded body actor'); + + this._bodyStack.insert_child_at_index(actor, 1); + }, + + setExpandedLines: function(nLines) { + this._bodyStack.layout_manager.expandLines = nLines; + }, + + expand: function(animate) { + this.expanded = true; + + this._actionBin.visible = (this._actionBin.get_n_children() > 0); + + if (this._bodyStack.get_n_children() < 2) { + this._expandedLabel = new URLHighlighter(this._bodyText, + true, this._useBodyMarkup); + this.setExpandedBody(this._expandedLabel.actor); + } + + if (animate) { + Tweener.addTween(this._bodyStack.layout_manager, + { expansion: 1, + time: MessageTray.ANIMATION_TIME, + transition: 'easeOutQuad' }); + this._actionBin.scale_y = 0; + Tweener.addTween(this._actionBin, + { scale_y: 1, + time: MessageTray.ANIMATION_TIME, + transition: 'easeOutQuad' }); + } else { + this._bodyStack.layout_manager.expansion = 1; + this._actionBin.scale_y = 1; + } + + this.emit('expanded'); + }, + + unexpand: function(animate) { + if (animate) { + Tweener.addTween(this._bodyStack.layout_manager, + { expansion: 0, + time: MessageTray.ANIMATION_TIME, + transition: 'easeOutQuad' }); + Tweener.addTween(this._actionBin, + { scale_y: 0, + time: MessageTray.ANIMATION_TIME, + transition: 'easeOutQuad', + onCompleteScope: this, + onComplete: function() { + this._actionBin.hide(); + this.expanded = false; + }}); + } else { + this._bodyStack.layout_manager.expansion = 0; + this._actionBin.scale_y = 0; + this.expanded = false; + } + + this.emit('unexpanded'); + }, + + canClose: function() { + return true; + }, + + _sync: function() { + let hovered = this.actor.hover; + this._closeButton.visible = hovered && this.canClose(); + this._secondaryBin.visible = !hovered; + }, + + _onClicked: function() { + }, + + _onDestroy: function() { + }, + + _onKeyPressed: function(a, event) { + let keysym = event.get_key_symbol(); + + if (keysym == Clutter.KEY_Delete || + keysym == Clutter.KEY_KP_Delete) { + this.close(); + return Clutter.EVENT_STOP; + } + return Clutter.EVENT_PROPAGATE; + } +}); +Signals.addSignalMethods(Message.prototype); + +const MessageListSection = new Lang.Class({ + Name: 'MessageListSection', + + _init: function(title) { + this.actor = new St.BoxLayout({ style_class: 'message-list-section', + clip_to_allocation: true, + x_expand: true, vertical: true }); + let titleBox = new St.BoxLayout({ style_class: 'message-list-section-title-box' }); + this.actor.add_actor(titleBox); + + this._title = new St.Button({ style_class: 'message-list-section-title', + label: title, + can_focus: true, + x_expand: true, + x_align: St.Align.START }); + titleBox.add_actor(this._title); + + this._title.connect('clicked', Lang.bind(this, this._onTitleClicked)); + this._title.connect('key-focus-in', Lang.bind(this, this._onKeyFocusIn)); + + let closeIcon = new St.Icon({ icon_name: 'window-close-symbolic' }); + this._closeButton = new St.Button({ style_class: 'message-list-section-close', + child: closeIcon, + accessible_name: _("Clear section"), + can_focus: true }); + this._closeButton.set_x_align(Clutter.ActorAlign.END); + titleBox.add_actor(this._closeButton); + + this._closeButton.connect('clicked', Lang.bind(this, this.clear)); + + this._list = new St.BoxLayout({ style_class: 'message-list-section-list', + vertical: true }); + this.actor.add_actor(this._list); + + this._list.connect('actor-added', Lang.bind(this, this._sync)); + this._list.connect('actor-removed', Lang.bind(this, this._sync)); + + let id = Main.sessionMode.connect('updated', + Lang.bind(this, this._sync)); + this.actor.connect('destroy', function() { + Main.sessionMode.disconnect(id); + }); + + this._messages = new Map(); + this._date = new Date(); + this.empty = true; + this._sync(); + }, + + _onTitleClicked: function() { + Main.overview.hide(); + Main.panel.closeCalendar(); + }, + + _onKeyFocusIn: function(actor) { + this.emit('key-focus-in', actor); + }, + + get allowed() { + return true; + }, + + setDate: function(date) { + if (Calendar.sameDay(date, this._date)) + return; + this._date = date; + this._sync(); + }, + + addMessage: function(message, animate) { + this.addMessageAtIndex(message, -1, animate); + }, + + addMessageAtIndex: function(message, index, animate) { + let obj = { + container: null, + destroyId: 0, + keyFocusId: 0, + closeId: 0 + }; + let pivot = new Clutter.Point({ x: .5, y: .5 }); + let scale = animate ? 0 : 1; + obj.container = new St.Widget({ layout_manager: new ScaleLayout(), + pivot_point: pivot, + scale_x: scale, scale_y: scale }); + obj.keyFocusId = message.actor.connect('key-focus-in', + Lang.bind(this, this._onKeyFocusIn)); + obj.destroyId = message.actor.connect('destroy', + Lang.bind(this, function() { + this.removeMessage(message, false); + })); + obj.closeId = message.connect('close', + Lang.bind(this, function() { + this.removeMessage(message, true); + })); + + this._messages.set(message, obj); + obj.container.add_actor(message.actor); + + this._list.insert_child_at_index(obj.container, index); + + if (animate) + Tweener.addTween(obj.container, { scale_x: 1, + scale_y: 1, + time: MESSAGE_ANIMATION_TIME, + transition: 'easeOutQuad' }); + }, + + moveMessage: function(message, index, animate) { + let obj = this._messages.get(message); + + if (!animate) { + this._list.set_child_at_index(obj.container, index); + return; + } + + let onComplete = Lang.bind(this, function() { + this._list.set_child_at_index(obj.container, index); + Tweener.addTween(obj.container, { scale_x: 1, + scale_y: 1, + time: MESSAGE_ANIMATION_TIME, + transition: 'easeOutQuad' }); + }); + Tweener.addTween(obj.container, { scale_x: 0, + scale_y: 0, + time: MESSAGE_ANIMATION_TIME, + transition: 'easeOutQuad', + onComplete: onComplete }); + }, + + removeMessage: function(message, animate) { + let obj = this._messages.get(message); + + message.actor.disconnect(obj.destroyId); + message.actor.disconnect(obj.keyFocusId); + message.disconnect(obj.closeId); + + this._messages.delete(message); + + if (animate) { + Tweener.addTween(obj.container, { scale_x: 0, scale_y: 0, + time: MESSAGE_ANIMATION_TIME, + transition: 'easeOutQuad', + onComplete: function() { + obj.container.destroy(); + global.sync_pointer(); + }}); + } else { + obj.container.destroy(); + global.sync_pointer(); + } + }, + + clear: function() { + let messages = [...this._messages.keys()].filter(function(message) { + return message.canClose(); + }); + + // If there are few messages, letting them all zoom out looks OK + if (messages.length < 2) { + messages.forEach(function(message) { + message.close(); + }); + } else { + // Otherwise we slide them out one by one, and then zoom them + // out "off-screen" in the end to smoothly shrink the parent + let delay = MESSAGE_ANIMATION_TIME / Math.max(messages.length, 5); + for (let i = 0; i < messages.length; i++) { + let message = messages[i]; + let obj = this._messages.get(message); + Tweener.addTween(obj.container, + { anchor_x: this._list.width, + opacity: 0, + time: MESSAGE_ANIMATION_TIME, + delay: i * delay, + transition: 'easeOutQuad', + onComplete: function() { + message.close(); + }}); + } + } + }, + + _canClear: function() { + for (let message of this._messages.keys()) + if (message.canClose()) + return true; + return false; + }, + + _shouldShow: function() { + return !this.empty; + }, + + _sync: function() { + let empty = this._list.get_n_children() == 0; + let changed = this.empty !== empty; + this.empty = empty; + + if (changed) + this.emit('empty-changed'); + + this._closeButton.visible = this._canClear(); + this.actor.visible = this.allowed && this._shouldShow(); + } +}); +Signals.addSignalMethods(MessageListSection.prototype); diff --git a/po/POTFILES.in b/po/POTFILES.in index ef1296206..14a9f6301 100644 --- a/po/POTFILES.in +++ b/po/POTFILES.in @@ -33,6 +33,7 @@ js/ui/keyboard.js js/ui/legacyTray.js js/ui/lookingGlass.js js/ui/main.js +js/ui/messageList.js js/ui/messageTray.js js/ui/notificationDaemon.js js/ui/overviewControls.js