[gnome-shell] calendar: Add Media section



commit 3ecdfaffd278dbd1115dea0eb86069b3cf0ae680
Author: Florian Müllner <fmuellner gnome org>
Date:   Mon Feb 15 12:13:22 2016 +0100

    calendar: Add Media section
    
    We lost media controls outside of notification banners when
    implementing the new notification designs. Reimplement this
    functionality as a dedicated "Media" section in the message
    list based on MPRIS.
    
    https://bugzilla.gnome.org/show_bug.cgi?id=756491

 data/theme/gnome-shell-high-contrast.css |   14 ++
 data/theme/gnome-shell-sass              |    2 +-
 data/theme/gnome-shell.css               |   14 ++
 js/js-resources.gresource.xml            |    1 +
 js/ui/calendar.js                        |    4 +
 js/ui/messageList.js                     |   14 ++-
 js/ui/mpris.js                           |  270 ++++++++++++++++++++++++++++++
 po/POTFILES.in                           |    1 +
 8 files changed, 318 insertions(+), 2 deletions(-)
---
diff --git a/data/theme/gnome-shell-high-contrast.css b/data/theme/gnome-shell-high-contrast.css
index c27717d..292d0bc 100644
--- a/data/theme/gnome-shell-high-contrast.css
+++ b/data/theme/gnome-shell-high-contrast.css
@@ -849,6 +849,20 @@ StScrollBar {
   padding: 8px;
   font-size: .9em; }
 
+.message-media-control {
+  padding: 6px; }
+  .message-media-control:last-child:ltr {
+    padding-right: 18px; }
+  .message-media-control:last-child:rtl {
+    padding-left: 18px; }
+
+.media-message-cover-icon {
+  icon-size: 32px; }
+  .media-message-cover-icon.fallback {
+    icon-size: 16px;
+    padding: 8px;
+    border: 1px solid black; }
+
 .system-switch-user-submenu-icon.user-icon {
   icon-size: 20px;
   padding: 0 2px; }
diff --git a/data/theme/gnome-shell-sass b/data/theme/gnome-shell-sass
index 9fb3918..7e13533 160000
--- a/data/theme/gnome-shell-sass
+++ b/data/theme/gnome-shell-sass
@@ -1 +1 @@
-Subproject commit 9fb3918831459cd002f3d621494cf5eac70fe46a
+Subproject commit 7e13533ab5280fd1370b3e9a7b8ba57a049cfb29
diff --git a/data/theme/gnome-shell.css b/data/theme/gnome-shell.css
index 2e09c1c..d3955e8 100644
--- a/data/theme/gnome-shell.css
+++ b/data/theme/gnome-shell.css
@@ -849,6 +849,20 @@ StScrollBar {
   padding: 8px;
   font-size: .9em; }
 
+.message-media-control {
+  padding: 6px; }
+  .message-media-control:last-child:ltr {
+    padding-right: 18px; }
+  .message-media-control:last-child:rtl {
+    padding-left: 18px; }
+
+.media-message-cover-icon {
+  icon-size: 32px; }
+  .media-message-cover-icon.fallback {
+    icon-size: 16px;
+    padding: 8px;
+    border: 1px solid #1c1f1f; }
+
 .system-switch-user-submenu-icon.user-icon {
   icon-size: 20px;
   padding: 0 2px; }
diff --git a/js/js-resources.gresource.xml b/js/js-resources.gresource.xml
index e0c522b..0746d8b 100644
--- a/js/js-resources.gresource.xml
+++ b/js/js-resources.gresource.xml
@@ -65,6 +65,7 @@
     <file>ui/messageTray.js</file>
     <file>ui/messageList.js</file>
     <file>ui/modalDialog.js</file>
+    <file>ui/mpris.js</file>
     <file>ui/notificationDaemon.js</file>
     <file>ui/osdWindow.js</file>
     <file>ui/osdMonitorLabeler.js</file>
diff --git a/js/ui/calendar.js b/js/ui/calendar.js
index c9a6222..e327c1d 100644
--- a/js/ui/calendar.js
+++ b/js/ui/calendar.js
@@ -13,6 +13,7 @@ const Shell = imports.gi.Shell;
 const Main = imports.ui.main;
 const MessageList = imports.ui.messageList;
 const MessageTray = imports.ui.messageTray;
+const Mpris = imports.ui.mpris;
 const Util = imports.misc.util;
 
 const MSECS_IN_DAY = 24 * 60 * 60 * 1000;
@@ -1099,6 +1100,9 @@ const CalendarMessageList = new Lang.Class({
         this._scrollView.add_actor(this._sectionList);
         this._sections = new Map();
 
+        this._mediaSection = new Mpris.MediaSection();
+        this._addSection(this._mediaSection);
+
         this._notificationSection = new NotificationSection();
         this._addSection(this._notificationSection);
 
diff --git a/js/ui/messageList.js b/js/ui/messageList.js
index 344eee9..57c829a 100644
--- a/js/ui/messageList.js
+++ b/js/ui/messageList.js
@@ -324,6 +324,9 @@ const Message = new Lang.Class({
                                             vertical: true, x_expand: true });
         hbox.add_actor(contentBox);
 
+        this._mediaControls = new St.BoxLayout();
+        hbox.add_actor(this._mediaControls);
+
         let titleBox = new St.BoxLayout();
         contentBox.add_actor(titleBox);
 
@@ -405,6 +408,15 @@ const Message = new Lang.Class({
         this._actionBin.visible = this.expanded;
     },
 
+    addMediaControl: function(iconName, callback) {
+        let icon = new St.Icon({ icon_name: iconName, icon_size: 16 });
+        let button = new St.Button({ style_class: 'message-media-control',
+                                     child: icon });
+        button.connect('clicked', callback);
+        this._mediaControls.add_actor(button);
+        return button;
+    },
+
     setExpandedBody: function(actor) {
         if (actor == null) {
             if (this._bodyStack.get_n_children() > 1)
@@ -476,7 +488,7 @@ const Message = new Lang.Class({
     },
 
     canClose: function() {
-        return true;
+        return this._mediaControls.get_n_children() == 0;
     },
 
     _sync: function() {
diff --git a/js/ui/mpris.js b/js/ui/mpris.js
new file mode 100644
index 0000000..825a00e
--- /dev/null
+++ b/js/ui/mpris.js
@@ -0,0 +1,270 @@
+const Gio = imports.gi.Gio;
+const Lang = imports.lang;
+const Signals = imports.signals;
+const Shell = imports.gi.Shell;
+const St = imports.gi.St;
+
+const Calendar = imports.ui.calendar;
+const Main = imports.ui.main;
+const MessageList = imports.ui.messageList;
+
+const DBusIface = '<node> \
+<interface name="org.freedesktop.DBus"> \
+  <method name="ListNames"> \
+    <arg type="as" direction="out" name="names" /> \
+  </method> \
+  <signal name="NameOwnerChanged"> \
+    <arg type="s" direction="out" name="name" /> \
+    <arg type="s" direction="out" name="oldOwner" /> \
+    <arg type="s" direction="out" name="newOwner" /> \
+  </signal> \
+</interface> \
+</node>';
+const DBusProxy = Gio.DBusProxy.makeProxyWrapper(DBusIface);
+
+const MprisIface = '<node> \
+<interface name="org.mpris.MediaPlayer2"> \
+  <method name="Raise" /> \
+  <property name="CanRaise" type="b" access="read" /> \
+  <property name="DesktopEntry" type="s" access="read" /> \
+</interface> \
+</node>';
+const MprisProxy = Gio.DBusProxy.makeProxyWrapper(MprisIface);
+
+const MprisPlayerIface = '<node> \
+<interface name="org.mpris.MediaPlayer2.Player"> \
+  <method name="PlayPause" /> \
+  <method name="Next" /> \
+  <method name="Previous" /> \
+  <property name="CanPlay" type="b" access="read" /> \
+  <property name="Metadata" type="a{sv}" access="read" /> \
+  <property name="PlaybackStatus" type="s" access="read" /> \
+</interface> \
+</node>';
+const MprisPlayerProxy = Gio.DBusProxy.makeProxyWrapper(MprisPlayerIface);
+
+const MPRIS_PLAYER_PREFIX = 'org.mpris.MediaPlayer2.';
+
+const MediaMessage = new Lang.Class({
+    Name: 'MediaMessage',
+    Extends: MessageList.Message,
+
+    _init: function(player) {
+        this._player = player;
+
+        this.parent('', '');
+
+        this._icon = new St.Icon({ style_class: 'media-message-cover-icon' });
+        this.setIcon(this._icon);
+
+        this.addMediaControl('media-skip-backward-symbolic',
+            Lang.bind(this, function() {
+                this._player.previous();
+            }));
+
+        this._playPauseButton = this.addMediaControl(null,
+            Lang.bind(this, function() {
+                this._player.playPause();
+            }));
+
+        this.addMediaControl('media-skip-forward-symbolic',
+            Lang.bind(this, function() {
+                this._player.next();
+            }));
+
+        this._player.connect('changed', Lang.bind(this, this._update));
+        this._player.connect('closed', Lang.bind(this, this.close));
+        this._update();
+    },
+
+    _onClicked: function() {
+        this._player.raise();
+        Main.panel.closeCalendar();
+    },
+
+    _update: function() {
+        this.setTitle(this._player.trackArtists.join(', '));
+        this.setBody(this._player.trackTitle);
+
+        if (this._player.trackCoverUrl) {
+            let file = Gio.File.new_for_uri(this._player.trackCoverUrl);
+            this._icon.gicon = new Gio.FileIcon({ file: file });
+            this._icon.remove_style_class_name('fallback');
+        } else {
+            this._icon.icon_name = 'audio-x-generic-symbolic';
+            this._icon.add_style_class_name('fallback');
+        }
+
+        let isPlaying = this._player.status == 'Playing';
+        let iconName = isPlaying ? 'media-playback-pause-symbolic'
+                                 : 'media-playback-start-symbolic';
+        this._playPauseButton.child.icon_name = iconName;
+    }
+});
+
+const MprisPlayer = new Lang.Class({
+    Name: 'MprisPlayer',
+
+    _init: function(busName) {
+        this._mprisProxy = new MprisProxy(Gio.DBus.session, busName,
+                                          '/org/mpris/MediaPlayer2',
+                                          Lang.bind(this, this._onMprisProxyReady));
+        this._playerProxy = new MprisPlayerProxy(Gio.DBus.session, busName,
+                                                 '/org/mpris/MediaPlayer2',
+                                                 Lang.bind(this, this._onPlayerProxyReady));
+
+        this._visible = false;
+        this._trackArtists = [];
+        this._trackTitle = '';
+        this._trackCoverUrl = '';
+    },
+
+    get status() {
+        return this._playerProxy.PlaybackStatus;
+    },
+
+    get trackArtists() {
+        return this._trackArtists;
+    },
+
+    get trackTitle() {
+        return this._trackTitle;
+    },
+
+    get trackCoverUrl() {
+        return this._trackCoverUrl;
+    },
+
+    playPause: function() {
+        this._playerProxy.PlayPauseRemote();
+    },
+
+    next: function() {
+        this._playerProxy.NextRemote();
+    },
+
+    previous: function() {
+        this._playerProxy.PreviousRemote();
+    },
+
+    raise: function() {
+        // The remote Raise() method may run into focus stealing prevention,
+        // so prefer activating the app via .desktop file if possible
+        let app = null;
+        if (this._mprisProxy.DesktopEntry) {
+            let desktopId = this._mprisProxy.DesktopEntry + '.desktop';
+            app = Shell.AppSystem.get_default().lookup_app(desktopId);
+        }
+
+        if (app)
+            app.activate();
+        else if (this._mprisProxy.CanRaise)
+            this._mprisProxy.RaiseRemote();
+    },
+
+    _close: function() {
+        this._mprisProxy.disconnect(this._ownerNotifyId);
+        this._mprisProxy = null;
+
+        this._playerProxy.disconnect(this._propsChangedId);
+        this._playerProxy = null;
+
+        this.emit('closed');
+    },
+
+    _onMprisProxyReady: function() {
+        this._ownerNotifyId = this._mprisProxy.connect('notify::g-name-owner',
+            Lang.bind(this, function() {
+                if (!this._mprisProxy.g_name_owner)
+                    this._close();
+            }));
+    },
+
+    _onPlayerProxyReady: function() {
+        this._propsChangedId = this._playerProxy.connect('g-properties-changed',
+                                                         Lang.bind(this, this._updateState));
+        this._updateState();
+    },
+
+    _updateState: function() {
+        let metadata = {};
+        for (let prop in this._playerProxy.Metadata)
+            metadata[prop] = this._playerProxy.Metadata[prop].deep_unpack();
+
+        this._trackArtists = metadata['xesam:artist'] || [_("Unknown artist")];
+        this._trackTitle = metadata['xesam:title'] || _("Unknown title");
+        this._trackCoverUrl = metadata['mpris:artUrl'] || '';
+        this.emit('changed');
+
+        let visible = this._playerProxy.CanPlay;
+
+        if (this._visible != visible) {
+            this._visible = visible;
+            if (visible)
+                this.emit('show');
+            else
+                this._close();
+        }
+    }
+});
+Signals.addSignalMethods(MprisPlayer.prototype);
+
+const MediaSection = new Lang.Class({
+    Name: 'MediaSection',
+    Extends: MessageList.MessageListSection,
+
+    _init: function() {
+        this.parent(_("Media"));
+
+        this._players = new Map();
+
+        this._proxy = new DBusProxy(Gio.DBus.session,
+                                    'org.freedesktop.DBus',
+                                    '/org/freedesktop/DBus',
+                                    Lang.bind(this, this._onProxyReady));
+    },
+
+    _shouldShow: function() {
+        return !this.empty && Calendar.isToday(this._date);
+    },
+
+    _addPlayer: function(busName) {
+        if (this._players.get(busName))
+            return;
+
+        let player = new MprisPlayer(busName);
+        player.connect('closed', Lang.bind(this,
+            function() {
+                this._players.delete(busName);
+            }));
+        player.connect('show', Lang.bind(this,
+            function() {
+                let message = new MediaMessage(player);
+                this.addMessage(message, true);
+            }));
+        this._players.set(busName, player);
+    },
+
+    _onProxyReady: function() {
+        this._proxy.ListNamesRemote(Lang.bind(this,
+            function([names]) {
+                names.forEach(Lang.bind(this,
+                    function(name) {
+                        if (!name.startsWith(MPRIS_PLAYER_PREFIX))
+                            return;
+
+                        this._addPlayer(name);
+                    }));
+            }));
+        this._proxy.connectSignal('NameOwnerChanged',
+                                  Lang.bind(this, this._onNameOwnerChanged));
+    },
+
+    _onNameOwnerChanged: function(proxy, sender, [name, oldOwner, newOwner]) {
+        if (!name.startsWith(MPRIS_PLAYER_PREFIX))
+            return;
+
+        if (newOwner && !oldOwner)
+            this._addPlayer(name);
+    }
+});
diff --git a/po/POTFILES.in b/po/POTFILES.in
index 14a9f63..cc4c5e5 100644
--- a/po/POTFILES.in
+++ b/po/POTFILES.in
@@ -35,6 +35,7 @@ js/ui/lookingGlass.js
 js/ui/main.js
 js/ui/messageList.js
 js/ui/messageTray.js
+js/ui/mpris.js
 js/ui/notificationDaemon.js
 js/ui/overviewControls.js
 js/ui/overview.js


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