From 3ecdfaffd278dbd1115dea0eb86069b3cf0ae680 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=BCllner?= Date: Mon, 15 Feb 2016 12:13:22 +0100 Subject: [PATCH] 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(-) create mode 100644 js/ui/mpris.js diff --git a/data/theme/gnome-shell-high-contrast.css b/data/theme/gnome-shell-high-contrast.css index c27717d88..292d0bca4 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 9fb391883..7e13533ab 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 2e09c1c65..d3955e841 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 e0c522bb3..0746d8b91 100644 --- a/js/js-resources.gresource.xml +++ b/js/js-resources.gresource.xml @@ -65,6 +65,7 @@ ui/messageTray.js ui/messageList.js ui/modalDialog.js + ui/mpris.js ui/notificationDaemon.js ui/osdWindow.js ui/osdMonitorLabeler.js diff --git a/js/ui/calendar.js b/js/ui/calendar.js index c9a62227e..e327c1dc0 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 344eee978..57c829aea 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 000000000..825a00e42 --- /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 = ' \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ +'; +const DBusProxy = Gio.DBusProxy.makeProxyWrapper(DBusIface); + +const MprisIface = ' \ + \ + \ + \ + \ + \ +'; +const MprisProxy = Gio.DBusProxy.makeProxyWrapper(MprisIface); + +const MprisPlayerIface = ' \ + \ + \ + \ + \ + \ + \ + \ + \ +'; +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 14a9f6301..cc4c5e5da 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