[gnome-shell/wip/fmuellner/notification-redux+sass: 100/141] calendar: Add MessageList and Section/Message base types



commit b5e8f25d8057583be07077ca06487dd0711a9f2d
Author: Florian Müllner <fmuellner gnome org>
Date:   Fri Dec 5 16:24:35 2014 +0100

    calendar: Add MessageList and Section/Message base types
    
    The message list is a scrollable list that will hold sections of
    different types of time-related messages like notifications,
    calendar events or birthday reminders. When no section displays
    any content for the selected date, a placeholder is shown instead.
    
    https://bugzilla.gnome.org/show_bug.cgi?id=744817

 data/gnome-shell-theme.gresource.xml |    1 +
 data/theme/_common.scss              |   82 +++++++-
 data/theme/gnome-shell.css           |   72 ++++++-
 data/theme/no-events.svg             |  119 ++++++++++
 js/ui/calendar.js                    |  421 +++++++++++++++++++++++++++++++++-
 5 files changed, 687 insertions(+), 8 deletions(-)
---
diff --git a/data/gnome-shell-theme.gresource.xml b/data/gnome-shell-theme.gresource.xml
index 067d324..9e270d0 100644
--- a/data/gnome-shell-theme.gresource.xml
+++ b/data/gnome-shell-theme.gresource.xml
@@ -19,6 +19,7 @@
     <file>gnome-shell-high-contrast.css</file>
     <file>logged-in-indicator.svg</file>
     <file>more-results.svg</file>
+    <file>no-events.svg</file>
     <file>noise-texture.png</file>
     <file>page-indicator-active.svg</file>
     <file>page-indicator-inactive.svg</file>
diff --git a/data/theme/_common.scss b/data/theme/_common.scss
index ae8b208..9575e67 100644
--- a/data/theme/_common.scss
+++ b/data/theme/_common.scss
@@ -702,7 +702,8 @@ StScrollBar {
   }
 
     .calendar,
-    .datemenu-today-button {
+    .datemenu-today-button,
+    .message-list-sections {
       margin: 0 1.5em;
     }
 
@@ -711,12 +712,22 @@ StScrollBar {
       padding-bottom: 3em;
     }
 
-    .datemenu-today-button {
+    .datemenu-today-button,
+    .message-list-section-title {
       border-radius: 4px;
       padding: .4em;
     }
 
-    .datemenu-today-button {
+    .message-list-section-list:ltr {
+      padding-left: .4em;
+    }
+
+    .message-list-section-list:rtl {
+      padding-right: .4em;
+    }
+
+    .datemenu-today-button,
+    .message-list-section-title {
       &:hover,&:focus { background-color: lighten($bg_color,5%); }
       &:active {
         color: lighten($selected_fg_color,5%);
@@ -731,6 +742,11 @@ StScrollBar {
       font-size: 1.5em;
     }
 
+    .message-list-section-title {
+      color: darken($fg_color,40%);
+      font-weight: bold;
+    }
+
     .calendar-month-label {
       color: darken($fg_color,5%);
       font-weight: bold;
@@ -802,6 +818,66 @@ StScrollBar {
         opacity: 0.5;
       }
 
+      /* Message list */
+      .message-list {
+        width: 340px;
+      }
+
+        .message-list-sections {
+          spacing: 1.5em;
+        }
+
+        .message-list-section,
+        .message-list-section-list {
+          spacing: 0.7em;
+        }
+
+        .message-list-section-title-box {
+          spacing: 0.4em;
+        }
+
+        .message-list-section-close > StIcon {
+          icon-size: 16px;
+          border-radius: 8px;
+          color: $bg_color;
+          background-color: darken($fg_color,60%);
+        }
+
+        /* FIXME: how do you do this in sass? */
+        .message-list-section-close:hover > StIcon,
+        .message-list-section-close:focus > StIcon {
+          background-color: darken($fg_color,40%);
+        }
+
+        .message {
+          background-color: lighten($bg_color,5%);
+          &:hover,&:focus { background-color: lighten($bg_color,15%); }
+        }
+
+          .message-icon-bin {
+            padding: 5px;
+          }
+
+          .message-icon-bin > StIcon {
+            icon-size: 48px;
+          }
+
+          .message-secondary-bin {
+            color: darken($fg_color,40%);
+          }
+
+          .message-secondary-bin > StIcon {
+            icon-size: 16px;
+          }
+
+          .message-title {
+            font-weight: bold;
+          }
+          .message-content {
+            padding: 5px;
+            spacing: 5px;
+          }
+
     .events-table { //right hand side
       width: 15em;
       spacing-columns: 1em;
diff --git a/data/theme/gnome-shell.css b/data/theme/gnome-shell.css
index 45d73b2..94160f7 100644
--- a/data/theme/gnome-shell.css
+++ b/data/theme/gnome-shell.css
@@ -592,26 +592,41 @@ StScrollBar {
   margin-bottom: 1em; }
 
 .calendar,
-.datemenu-today-button {
+.datemenu-today-button,
+.message-list-sections {
   margin: 0 1.5em; }
 
 .datemenu-calendar-column {
   spacing: 0.5em;
   padding-bottom: 3em; }
 
-.datemenu-today-button {
+.datemenu-today-button,
+.message-list-section-title {
   border-radius: 4px;
   padding: .4em; }
 
-.datemenu-today-button:hover, .datemenu-today-button:focus {
+.message-list-section-list:ltr {
+  padding-left: .4em; }
+
+.message-list-section-list:rtl {
+  padding-right: .4em; }
+
+.datemenu-today-button:hover, .datemenu-today-button:focus,
+.message-list-section-title:hover,
+.message-list-section-title:focus {
   background-color: #454c4c; }
-.datemenu-today-button:active {
+.datemenu-today-button:active,
+.message-list-section-title:active {
   color: white;
   background-color: #215d9c; }
 
 .datemenu-today-button .date-label {
   font-size: 1.5em; }
 
+.message-list-section-title {
+  color: #8e8e80;
+  font-weight: bold; }
+
 .calendar-month-label {
   color: #e2e2df;
   font-weight: bold;
@@ -681,6 +696,55 @@ StScrollBar {
   color: rgba(238, 238, 236, 0.15);
   opacity: 0.5; }
 
+/* Message list */
+.message-list {
+  width: 340px; }
+
+.message-list-sections {
+  spacing: 1.5em; }
+
+.message-list-section,
+.message-list-section-list {
+  spacing: 0.7em; }
+
+.message-list-section-title-box {
+  spacing: 0.4em; }
+
+.message-list-section-close > StIcon {
+  icon-size: 16px;
+  border-radius: 8px;
+  color: #393f3f;
+  background-color: #59594f; }
+
+/* FIXME: how do you do this in sass? */
+.message-list-section-close:hover > StIcon,
+.message-list-section-close:focus > StIcon {
+  background-color: #8e8e80; }
+
+.message {
+  background-color: #454c4c; }
+  .message:hover, .message:focus {
+    background-color: #5d6767; }
+
+.message-icon-bin {
+  padding: 5px; }
+
+.message-icon-bin > StIcon {
+  icon-size: 48px; }
+
+.message-secondary-bin {
+  color: #8e8e80; }
+
+.message-secondary-bin > StIcon {
+  icon-size: 16px; }
+
+.message-title {
+  font-weight: bold; }
+
+.message-content {
+  padding: 5px;
+  spacing: 5px; }
+
 .events-table {
   width: 15em;
   spacing-columns: 1em;
diff --git a/data/theme/no-events.svg b/data/theme/no-events.svg
new file mode 100644
index 0000000..8ab08a9
--- /dev/null
+++ b/data/theme/no-events.svg
@@ -0,0 +1,119 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/";
+   xmlns:cc="http://creativecommons.org/ns#";
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#";
+   xmlns:svg="http://www.w3.org/2000/svg";
+   xmlns="http://www.w3.org/2000/svg";
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd";
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape";
+   width="64px"
+   height="64px"
+   id="svg3471"
+   version="1.1"
+   inkscape:version="0.48.5 r10040"
+   sodipodi:docname="New document 5">
+  <defs
+     id="defs3473" />
+  <sodipodi:namedview
+     id="base"
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1.0"
+     inkscape:pageopacity="0.0"
+     inkscape:pageshadow="2"
+     inkscape:zoom="5.5"
+     inkscape:cx="32"
+     inkscape:cy="32"
+     inkscape:current-layer="layer1"
+     showgrid="true"
+     inkscape:document-units="px"
+     inkscape:grid-bbox="true"
+     inkscape:window-width="1461"
+     inkscape:window-height="772"
+     inkscape:window-x="37"
+     inkscape:window-y="64"
+     inkscape:window-maximized="0" />
+  <metadata
+     id="metadata3476">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage"; />
+        <dc:title></dc:title>
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <g
+     id="layer1"
+     inkscape:label="Layer 1"
+     inkscape:groupmode="layer">
+    <g
+       transform="matrix(4,0,0,4,1.9999997,2.3636364)"
+       id="g19145"
+       style="fill:#bebebe;fill-opacity:1;display:inline">
+      <g
+         id="g19147"
+         inkscape:label="status"
+         style="fill:#bebebe;fill-opacity:1;display:inline"
+         transform="translate(-541.0002,-301)" />
+      <g
+         style="fill:#bebebe;fill-opacity:1"
+         id="g19149"
+         inkscape:label="devices"
+         transform="translate(-541.0002,-301)" />
+      <g
+         style="fill:#bebebe;fill-opacity:1"
+         id="g19151"
+         inkscape:label="apps"
+         transform="translate(-541.0002,-301)" />
+      <g
+         style="fill:#bebebe;fill-opacity:1"
+         id="g19153"
+         inkscape:label="places"
+         transform="translate(-541.0002,-301)" />
+      <g
+         style="fill:#bebebe;fill-opacity:1"
+         id="g19155"
+         inkscape:label="mimetypes"
+         transform="translate(-541.0002,-301)">
+        <path
+           inkscape:connector-curvature="0"
+           d="m 543.0002,301 c -1.05237,0 -2,0.84508 -2,1.9375 l 0,11.125 c 0,1.09242 0.94763,1.9375 
2,1.9375 l 11,0 c 1.05237,0 2,-0.84508 2,-1.9375 l 0,-11.125 c 0,-1.09242 -0.94763,-1.9375 -2,-1.9375 l -11,0 
z m 0,5 3.03125,0 0,2 -3.03125,0 0,-2 z m 4.03125,0 2.96875,0 0,2 -2.96875,0 0,-2 z m 3.96875,0 3,0 0,2 -3,0 
0,-2 z m -8,3 3.03125,0 0,2 -3.03125,0 0,-2 z m 4.03125,0 2.96875,0 0,2 -2.96875,0 0,-2 z m 3.96875,0 3,0 0,2 
-3,0 0,-2 z m -8,3 3.03125,0 0,2 -3.03125,0 0,-2 z m 4.03125,0 2.96875,0 0,2 -2.96875,0 0,-2 z m 3.96875,0 
3,0 0,2 -3,0 0,-2 z"
+           id="path19157"
+           
style="font-size:medium;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-indent:0;text-align:start;text-decoration:none;line-height:normal;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;text-anchor:start;baseline-shift:baseline;color:#000000;fill:#bebebe;fill-opacity:1;stroke:none;stroke-width:2;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:new;font-family:Sans;-inkscape-font-specification:Sans"
 />
+        <rect
+           height="1.9999993"
+           id="rect19159"
+           
style="opacity:0.35;color:#000000;fill:#bebebe;fill-opacity:1;stroke:none;stroke-width:1;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
+           width="2.9999993"
+           x="551.00018"
+           y="309" />
+      </g>
+      <g
+         id="g19161"
+         inkscape:label="emblems"
+         style="fill:#bebebe;fill-opacity:1;display:inline"
+         transform="translate(-541.0002,-301)" />
+      <g
+         id="g19163"
+         inkscape:label="emotes"
+         style="fill:#bebebe;fill-opacity:1;display:inline"
+         transform="translate(-541.0002,-301)" />
+      <g
+         id="g19165"
+         inkscape:label="categories"
+         style="fill:#bebebe;fill-opacity:1;display:inline"
+         transform="translate(-541.0002,-301)" />
+      <g
+         id="g19167"
+         inkscape:label="actions"
+         style="fill:#bebebe;fill-opacity:1;display:inline"
+         transform="translate(-541.0002,-301)" />
+    </g>
+  </g>
+</svg>
diff --git a/js/ui/calendar.js b/js/ui/calendar.js
index a09350a..d4046cf 100644
--- a/js/ui/calendar.js
+++ b/js/ui/calendar.js
@@ -1,8 +1,10 @@
 // -*- 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 Gtk = imports.gi.Gtk;
 const Lang = imports.lang;
 const St = imports.gi.St;
 const Signals = imports.signals;
@@ -12,12 +14,16 @@ const Mainloop = imports.mainloop;
 const Meta = imports.gi.Meta;
 const Shell = imports.gi.Shell;
 
+const Main = imports.ui.main;
+const Tweener = imports.ui.tweener;
 const Util = imports.misc.util;
 
 const MSECS_IN_DAY = 24 * 60 * 60 * 1000;
 const SHOW_WEEKDATE_KEY = 'show-weekdate';
 const ELLIPSIS_CHAR = '\u2026';
 
+const MESSAGE_ANIMATION_TIME = 0.1;
+
 // alias to prevent xgettext from picking up strings translated in GTK+
 const gtk30_ = Gettext_gtk30.gettext;
 const NC_ = function(context, str) { return str; };
@@ -852,9 +858,324 @@ 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 Message = new Lang.Class({
+    Name: 'Message',
+
+    _init: function(title, body) {
+        this.actor = new St.Button({ style_class: 'message',
+                                     accessible_role: Atk.Role.NOTIFICATION,
+                                     can_focus: true,
+                                     x_expand: true, x_fill: true });
+
+        let hbox = new St.BoxLayout();
+        this.actor.set_child(hbox);
+
+        this._iconBin = new St.Bin({ style_class: 'message-icon-bin',
+                                     y_expand: true,
+                                     visible: false });
+        this._iconBin.set_y_align(Clutter.ActorAlign.START);
+        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.bodyLabel = new URLHighlighter(body, false, this._useBodyMarkup);
+        this.bodyLabel.actor.add_style_class_name('message-body');
+        contentBox.add_actor(this.bodyLabel.actor);
+
+        this._closeButton.connect('clicked', Lang.bind(this,
+            function() {
+                this.emit('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();
+    },
+
+    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.text = title;
+    },
+
+    setBody: function(text) {
+        this.bodyLabel.setMarkup(text, this._useBodyMarkup);
+    },
+
+    setUseBodyMarkup: function(enable) {
+        if (this._useBodyMarkup === enable)
+            return;
+        this._useBodyMarkup = enable;
+        if (this.bodyLabel)
+            this.setBody(this.bodyLabel.actor.text);
+    },
+
+    canClear: function() {
+        return true;
+    },
+
+    _sync: function() {
+        let hovered = this.actor.hover;
+        this._closeButton.visible = hovered;
+        this._secondaryBin.visible = !hovered;
+    },
+
+    _onClicked: function() {
+    },
+
+    _onDestroy: function() {
+    }
+});
+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));
+
+        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);
+    },
+
+    setDate: function(date) {
+        if (_sameDay(date, this._date))
+            return;
+        this._date = date;
+        this._sync();
+    },
+
+    addMessage: function(message, animate) {
+        this.addMessageAtIndex(message, 0, 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' });
+    },
+
+    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();
+                                              }});
+        else
+            obj.container.destroy();
+    },
+
+    clear: function() {
+        let messages = [...this._messages.keys()].filter(function(message) {
+            return message.canClear();
+        });
+
+        // If there are few messages, letting them all zoom out looks OK
+        if (messages.length < 2) {
+            messages.forEach(Lang.bind(this, function(message) {
+                this.removeMessage(message, true); }));
+        } 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: Lang.bind(this, function() {
+                                       this.removeMessage(message, true);
+                                   })});
+            }
+        }
+    },
+
+    _canClear: function() {
+        for (let message of this._messages.keys())
+            if (message.canClear())
+                return true;
+        return false;
+    },
+
+    _isToday: function() {
+        let today = new Date();
+        return _sameDay(this._date, today);
+    },
+
+    _syncVisible: function() {
+        this.actor.visible = !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._syncVisible();
+    }
+});
+Signals.addSignalMethods(MessageListSection.prototype);
+
 const EventsList = new Lang.Class({
     Name: 'EventsList',
 
@@ -1030,3 +1351,101 @@ const EventsList = new Lang.Class({
         }
     }
 });
+
+const Placeholder = new Lang.Class({
+    Name: 'Placeholder',
+
+    _init: function() {
+        this.actor = new St.BoxLayout({ style_class: 'message-list-placeholder',
+                                        vertical: true });
+
+        this._date = new Date();
+
+        let file = Gio.File.new_for_uri('resource:///org/gnome/shell/theme/no-events.svg');
+        let gicon = new Gio.FileIcon({ file: file });
+
+        this._icon = new St.Icon({ gicon: gicon });
+        this.actor.add_actor(this._icon);
+
+        this._label = new St.Label({ text: _("No Events") });
+        this.actor.add_actor(this._label);
+    }
+});
+
+const MessageList = new Lang.Class({
+    Name: 'MessageList',
+
+    _init: function() {
+        this.actor = new St.Widget({ style_class: 'message-list',
+                                     layout_manager: new Clutter.BinLayout(),
+                                     x_expand: true, y_expand: true });
+
+        this._placeholder = new Placeholder();
+        this.actor.add_actor(this._placeholder.actor);
+
+        this._scrollView = new St.ScrollView({ style_class: 'vfade',
+                                               overlay_scrollbars: true,
+                                               x_expand: true, y_expand: true,
+                                               x_fill: true, y_fill: true });
+        this._scrollView.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC);
+        this.actor.add_actor(this._scrollView);
+
+        this._sectionList = new St.BoxLayout({ style_class: 'message-list-sections',
+                                               vertical: true,
+                                               y_expand: true,
+                                               y_align: Clutter.ActorAlign.START });
+        this._scrollView.add_actor(this._sectionList);
+        this._sections = new Map();
+    },
+
+    _addSection: function(section) {
+        let obj = {
+            destroyId: 0,
+            visibleId:  0,
+            emptyChangedId: 0,
+            keyFocusId: 0
+        };
+        obj.destroyId = section.actor.connect('destroy', Lang.bind(this,
+            function() {
+                this._removeSection(section);
+            }));
+        obj.visibleId = section.actor.connect('notify::visible',
+                                              Lang.bind(this, this._sync));
+        obj.emptyChangedId = section.connect('empty-changed',
+                                             Lang.bind(this, this._sync));
+        obj.keyFocusId = section.connect('key-focus-in',
+                                         Lang.bind(this, this._onKeyFocusIn));
+
+        this._sections.set(section, obj);
+        this._sectionList.add_actor(section.actor);
+        this._sync();
+    },
+
+    _removeSection: function(section) {
+        let obj = this._sections.get(section);
+        section.actor.disconnect(obj.destroyId);
+        section.actor.disconnect(obj.visibleId);
+        section.disconnect(obj.emptyChangedId);
+        section.disconnect(obj.keyFocusId);
+
+        this._sections.delete(section);
+        this._sectionList.remove_actor(section.actor);
+        this._sync();
+    },
+
+    _onKeyFocusIn: function(section, actor) {
+        Util.ensureActorVisibleInScrollView(this._scrollView, actor);
+    },
+
+    _sync: function() {
+        let showPlaceholder = [...this._sections.keys()].every(function(s) {
+            return s.empty || !s.actor.visible;
+        });
+        this._placeholder.actor.visible = showPlaceholder;
+    },
+
+    setDate: function(date) {
+        for (let section of this._sections.keys())
+            section.setDate(date);
+    }
+});


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]