diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/CMakeLists.txt | 13 | ||||
-rw-r--r-- | src/Makefile.am.THIS | 39 | ||||
-rw-r--r-- | src/bus-watch-namespace.c | 349 | ||||
-rw-r--r-- | src/bus-watch-namespace.h | 34 | ||||
-rw-r--r-- | src/main.vala | 2 | ||||
-rw-r--r-- | src/media-player-list.vala | 22 | ||||
-rw-r--r-- | src/mpris2-watcher.vala | 200 | ||||
-rw-r--r-- | src/service.vala | 80 | ||||
-rw-r--r-- | src/sound-menu.vala | 111 | ||||
-rw-r--r-- | src/volume-control.vala | 122 |
10 files changed, 670 insertions, 302 deletions
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index ec43b8f..572befd 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -14,12 +14,15 @@ vala_init(indicator-sound-service libxml-2.0 libpulse libpulse-mainloop-glib + libnotify OPTIONS --ccode --thread --vapidir=${CMAKE_SOURCE_DIR}/vapi/ --vapidir=. --target-glib=2.36 + --pkg=url-dispatcher + --pkg=bus-watcher ) vala_add(indicator-sound-service @@ -47,19 +50,12 @@ vala_add(indicator-sound-service media-player-list.vala DEPENDS media-player - mpris2-watcher + mpris2-interfaces ) vala_add(indicator-sound-service mpris2-interfaces.vala ) vala_add(indicator-sound-service - mpris2-watcher.vala - DEPENDS - media-player - mpris2-interfaces - freedesktop-interfaces -) -vala_add(indicator-sound-service freedesktop-interfaces.vala ) vala_add(indicator-sound-service @@ -89,6 +85,7 @@ set( INDICATOR_SOUND_SOURCES ${project_VALA_SOURCES} ${project_VALA_C} + bus-watch-namespace.c ${SYMBOLS_PATH} ) diff --git a/src/Makefile.am.THIS b/src/Makefile.am.THIS new file mode 100644 index 0000000..1a82a18 --- /dev/null +++ b/src/Makefile.am.THIS @@ -0,0 +1,39 @@ +pkglibexec_PROGRAMS = indicator-sound-service + +indicator_sound_service_SOURCES = \ + service.vala \ + main.vala \ + volume-control.vala \ + media-player.vala \ + media-player-list.vala \ + mpris2-interfaces.vala \ + freedesktop-interfaces.vala \ + sound-menu.vala \ + bus-watch-namespace.c \ + bus-watch-namespace.h + +indicator_sound_service_VALAFLAGS = \ + --ccode \ + --vapidir=$(top_srcdir)/vapi/ \ + --vapidir=./ \ + --thread \ + --pkg config \ + --pkg gio-2.0 \ + --pkg gio-unix-2.0 \ + --pkg libxml-2.0 \ + --pkg libpulse \ + --pkg libpulse-mainloop-glib \ + --pkg bus-watcher \ + --target-glib=2.36 + +# -w to disable warnings for vala-generated code +indicator_sound_service_CFLAGS = $(PULSEAUDIO_CFLAGS) \ + $(SOUNDSERVICE_CFLAGS) \ + $(GCONF_CFLAGS) \ + $(COVERAGE_CFLAGS) \ + -DLIBEXECDIR=\"$(libexecdir)\" \ + -w \ + -DGETTEXT_PACKAGE=\"$(GETTEXT_PACKAGE)\" + +indicator_sound_service_LDADD = $(PULSEAUDIO_LIBS) $(SOUNDSERVICE_LIBS) $(GCONF_LIBS) +indicator_sound_service_LDFLAGS = $(COVERAGE_LDFLAGS) diff --git a/src/bus-watch-namespace.c b/src/bus-watch-namespace.c new file mode 100644 index 0000000..f20905c --- /dev/null +++ b/src/bus-watch-namespace.c @@ -0,0 +1,349 @@ +/* + * Copyright 2013 Canonical Ltd. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; either version 2 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * Author: Lars Uebernickel <lars.uebernickel@canonical.com> + */ + +#include <gio/gio.h> +#include <string.h> +#include "bus-watch-namespace.h" + +typedef struct +{ + guint id; + gchar *name_space; + GBusNameAppearedCallback appeared_handler; + GBusNameVanishedCallback vanished_handler; + gpointer user_data; + GDestroyNotify user_data_destroy; + + GDBusConnection *connection; + GCancellable *cancellable; + GHashTable *names; + guint subscription_id; +} NamespaceWatcher; + +typedef struct +{ + NamespaceWatcher *watcher; + gchar *name; +} GetNameOwnerData; + +static guint namespace_watcher_next_id; +static GHashTable *namespace_watcher_watchers; + +static void +namespace_watcher_stop (gpointer data) +{ + NamespaceWatcher *watcher = data; + + g_cancellable_cancel (watcher->cancellable); + g_object_unref (watcher->cancellable); + + if (watcher->subscription_id) + g_dbus_connection_signal_unsubscribe (watcher->connection, watcher->subscription_id); + + if (watcher->vanished_handler) + { + GHashTableIter it; + const gchar *name; + + g_hash_table_iter_init (&it, watcher->names); + while (g_hash_table_iter_next (&it, (gpointer *) &name, NULL)) + watcher->vanished_handler (watcher->connection, name, watcher->user_data); + } + + if (watcher->user_data_destroy) + watcher->user_data_destroy (watcher->user_data); + + if (watcher->connection) + { + g_signal_handlers_disconnect_by_func (watcher->connection, namespace_watcher_stop, watcher); + g_object_unref (watcher->connection); + } + + g_hash_table_unref (watcher->names); + + g_hash_table_remove (namespace_watcher_watchers, GUINT_TO_POINTER (watcher->id)); + if (g_hash_table_size (namespace_watcher_watchers) == 0) + g_clear_pointer (&namespace_watcher_watchers, g_hash_table_destroy); + + g_free (watcher->name_space); + + g_free (watcher); +} + +static void +namespace_watcher_name_appeared (NamespaceWatcher *watcher, + const gchar *name, + const gchar *owner) +{ + /* There's a race between NameOwnerChanged signals arriving and the + * ListNames/GetNameOwner sequence returning, so this function might + * be called more than once for the same name. To ensure that + * appeared_handler is only called once for each name, it is only + * called when inserting the name into watcher->names (each name is + * only inserted once there). + */ + if (g_hash_table_contains (watcher->names, name)) + return; + + g_hash_table_add (watcher->names, g_strdup (name)); + + if (watcher->appeared_handler) + watcher->appeared_handler (watcher->connection, name, owner, watcher->user_data); +} + +static void +namespace_watcher_name_vanished (NamespaceWatcher *watcher, + const gchar *name) +{ + if (g_hash_table_remove (watcher->names, name) && watcher->vanished_handler) + watcher->vanished_handler (watcher->connection, name, watcher->user_data); +} + +static gboolean +dbus_name_has_namespace (const gchar *name, + const gchar *name_space) +{ + gint len_name; + gint len_namespace; + + len_name = strlen (name); + len_namespace = strlen (name_space); + + if (len_name < len_namespace) + return FALSE; + + if (memcmp (name_space, name, len_namespace) != 0) + return FALSE; + + return len_namespace == len_name || name[len_namespace] == '.'; +} + +static void +name_owner_changed (GDBusConnection *connection, + const gchar *sender_name, + const gchar *object_path, + const gchar *interface_name, + const gchar *signal_name, + GVariant *parameters, + gpointer user_data) +{ + NamespaceWatcher *watcher = user_data; + const gchar *name; + const gchar *old_owner; + const gchar *new_owner; + + g_variant_get (parameters, "(&s&s&s)", &name, &old_owner, &new_owner); + + if (old_owner[0] != '\0') + namespace_watcher_name_vanished (watcher, name); + + if (new_owner[0] != '\0') + namespace_watcher_name_appeared (watcher, name, new_owner); +} + +static void +got_name_owner (GObject *object, + GAsyncResult *result, + gpointer user_data) +{ + GetNameOwnerData *data = user_data; + GError *error = NULL; + GVariant *reply; + const gchar *owner; + + reply = g_dbus_connection_call_finish (G_DBUS_CONNECTION (object), result, &error); + + if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) + { + g_error_free (error); + goto out; + } + + if (reply == NULL) + { + if (!g_error_matches (error, G_DBUS_ERROR, G_DBUS_ERROR_NAME_HAS_NO_OWNER)) + g_warning ("bus_watch_namespace: error calling org.freedesktop.DBus.GetNameOwner: %s", error->message); + g_error_free (error); + goto out; + } + + g_variant_get (reply, "(&s)", &owner); + namespace_watcher_name_appeared (data->watcher, data->name, owner); + + g_variant_unref (reply); + +out: + g_free (data->name); + g_slice_free (GetNameOwnerData, data); +} + +static void +names_listed (GObject *object, + GAsyncResult *result, + gpointer user_data) +{ + NamespaceWatcher *watcher; + GError *error = NULL; + GVariant *reply; + GVariantIter *iter; + const gchar *name; + + reply = g_dbus_connection_call_finish (G_DBUS_CONNECTION (object), result, &error); + + if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) + { + g_error_free (error); + return; + } + + watcher = user_data; + + if (reply == NULL) + { + g_warning ("bus_watch_namespace: error calling org.freedesktop.DBus.ListNames: %s", error->message); + g_error_free (error); + return; + } + + g_variant_get (reply, "(as)", &iter); + while (g_variant_iter_next (iter, "&s", &name)) + { + if (dbus_name_has_namespace (name, watcher->name_space)) + { + GetNameOwnerData *data = g_slice_new (GetNameOwnerData); + data->watcher = watcher; + data->name = g_strdup (name); + g_dbus_connection_call (watcher->connection, "org.freedesktop.DBus", "/", + "org.freedesktop.DBus", "GetNameOwner", + g_variant_new ("(s)", name), G_VARIANT_TYPE ("(s)"), + G_DBUS_CALL_FLAGS_NONE, -1, watcher->cancellable, + got_name_owner, data); + } + } + + g_variant_iter_free (iter); + g_variant_unref (reply); +} + +static void +connection_closed (GDBusConnection *connection, + gboolean remote_peer_vanished, + GError *error, + gpointer user_data) +{ + NamespaceWatcher *watcher = user_data; + + namespace_watcher_stop (watcher); +} + +static void +got_bus (GObject *object, + GAsyncResult *result, + gpointer user_data) +{ + GDBusConnection *connection; + NamespaceWatcher *watcher; + GError *error = NULL; + + connection = g_bus_get_finish (result, &error); + + if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) + { + g_error_free (error); + return; + } + + watcher = user_data; + + if (connection == NULL) + { + namespace_watcher_stop (watcher); + return; + } + + watcher->connection = connection; + g_signal_connect (watcher->connection, "closed", G_CALLBACK (connection_closed), watcher); + + watcher->subscription_id = + g_dbus_connection_signal_subscribe (watcher->connection, "org.freedesktop.DBus", + "org.freedesktop.DBus", "NameOwnerChanged", "/org/freedesktop/DBus", + watcher->name_space, G_DBUS_SIGNAL_FLAGS_MATCH_ARG0_NAMESPACE, + name_owner_changed, watcher, NULL); + + g_dbus_connection_call (watcher->connection, "org.freedesktop.DBus", "/", + "org.freedesktop.DBus", "ListNames", NULL, G_VARIANT_TYPE ("(as)"), + G_DBUS_CALL_FLAGS_NONE, -1, watcher->cancellable, + names_listed, watcher); +} + +guint +bus_watch_namespace (GBusType bus_type, + const gchar *name_space, + GBusNameAppearedCallback appeared_handler, + GBusNameVanishedCallback vanished_handler, + gpointer user_data, + GDestroyNotify user_data_destroy) +{ + NamespaceWatcher *watcher; + + /* same rules for interfaces and well-known names */ + g_return_val_if_fail (name_space != NULL && g_dbus_is_interface_name (name_space), 0); + g_return_val_if_fail (appeared_handler || vanished_handler, 0); + + watcher = g_new0 (NamespaceWatcher, 1); + watcher->id = namespace_watcher_next_id++; + watcher->name_space = g_strdup (name_space); + watcher->appeared_handler = appeared_handler; + watcher->vanished_handler = vanished_handler; + watcher->user_data = user_data; + watcher->user_data_destroy = user_data_destroy; + watcher->cancellable = g_cancellable_new (); + watcher->names = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL); + + if (namespace_watcher_watchers == NULL) + namespace_watcher_watchers = g_hash_table_new (g_direct_hash, g_direct_equal); + g_hash_table_insert (namespace_watcher_watchers, GUINT_TO_POINTER (watcher->id), watcher); + + g_bus_get (bus_type, watcher->cancellable, got_bus, watcher); + + return watcher->id; +} + +void +bus_unwatch_namespace (guint id) +{ + /* namespace_watcher_stop() might have already removed the watcher + * with @id in the case of a connection error. Thus, this function + * doesn't warn when @id is absent from the hash table. + */ + + if (namespace_watcher_watchers) + { + NamespaceWatcher *watcher; + + watcher = g_hash_table_lookup (namespace_watcher_watchers, GUINT_TO_POINTER (id)); + if (watcher) + { + /* make sure vanished() is not called as a result of this function */ + g_hash_table_remove_all (watcher->names); + + namespace_watcher_stop (watcher); + } + } +} diff --git a/src/bus-watch-namespace.h b/src/bus-watch-namespace.h new file mode 100644 index 0000000..215f6be --- /dev/null +++ b/src/bus-watch-namespace.h @@ -0,0 +1,34 @@ +/* + * Copyright 2013 Canonical Ltd. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; either version 2 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * Author: Lars Uebernickel <lars.uebernickel@canonical.com> + */ + +#ifndef __BUS_WATCH_NAMESPACE_H__ +#define __BUS_WATCH_NAMESPACE_H__ + +#include <gio/gio.h> + +guint bus_watch_namespace (GBusType bus_type, + const gchar *name_space, + GBusNameAppearedCallback appeared_handler, + GBusNameVanishedCallback vanished_handler, + gpointer user_data, + GDestroyNotify user_data_destroy); + +void bus_unwatch_namespace (guint id); + +#endif diff --git a/src/main.vala b/src/main.vala index 97f311f..4da9e58 100644 --- a/src/main.vala +++ b/src/main.vala @@ -7,6 +7,8 @@ static int main (string[] args) { Intl.setlocale (LocaleCategory.ALL, ""); Intl.bindtextdomain (Config.GETTEXT_PACKAGE, Config.GNOMELOCALEDIR); + Notify.init ("indicator-sound"); + var service = new IndicatorSound.Service (); return service.run (); } diff --git a/src/media-player-list.vala b/src/media-player-list.vala index 6eb5fc9..62badc2 100644 --- a/src/media-player-list.vala +++ b/src/media-player-list.vala @@ -26,9 +26,7 @@ public class MediaPlayerList { public MediaPlayerList () { this._players = new HashTable<string, MediaPlayer> (str_hash, str_equal); - this.mpris_watcher = new Mpris2Watcher (); - this.mpris_watcher.client_appeared.connect (this.player_appeared); - this.mpris_watcher.client_disappeared.connect (this.player_disappeared); + BusWatcher.watch_namespace (BusType.SESSION, "org.mpris.MediaPlayer2", this.player_appeared, this.player_disappeared); } /* only valid while the list is not changed */ @@ -113,15 +111,21 @@ public class MediaPlayerList { public signal void player_removed (MediaPlayer player); HashTable<string, MediaPlayer> _players; - Mpris2Watcher mpris_watcher; - void player_appeared (string desktop_id, string dbus_name, bool use_playlists) { - var player = this.insert (desktop_id); - if (player != null) - player.attach (dbus_name); + void player_appeared (DBusConnection connection, string name, string owner) { + try { + MprisRoot mpris2_root = Bus.get_proxy_sync (BusType.SESSION, name, MPRIS_MEDIA_PLAYER_PATH); + + var player = this.insert (mpris2_root.DesktopEntry); + if (player != null) + player.attach (name); + } + catch (Error e) { + warning ("unable to create mpris proxy for '%s': %s", name, e.message); + } } - void player_disappeared (string dbus_name) { + void player_disappeared (DBusConnection connection, string dbus_name) { MediaPlayer? player = this._players.find ( (name, player) => { return player.dbus_name == dbus_name; }); diff --git a/src/mpris2-watcher.vala b/src/mpris2-watcher.vala deleted file mode 100644 index 4a1ab6e..0000000 --- a/src/mpris2-watcher.vala +++ /dev/null @@ -1,200 +0,0 @@ -/* -Copyright 2010 Canonical Ltd. - -Authors: - Conor Curran <conor.curran@canonical.com> - -This program is free software: you can redistribute it and/or modify it -under the terms of the GNU General Public License version 3, as published -by the Free Software Foundation. - -This program is distributed in the hope that it will be useful, but -WITHOUT ANY WARRANTY; without even the implied warranties of -MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR -PURPOSE. See the GNU General Public License for more details. - -You should have received a copy of the GNU General Public License along -with this program. If not, see <http://www.gnu.org/licenses/>. -*/ - -using Xml; - -public class Mpris2Watcher : GLib.Object -{ - DBusConnection session_bus; - - public signal void client_appeared ( string desktop_file_name, - string dbus_name, - bool use_playlists ); - public signal void client_disappeared ( string dbus_name ); - - public Mpris2Watcher () - { - } - - construct - { - const string match_rule = "type='signal'," + - "sender='org.freedesktop.DBus'," + - "interface='org.freedesktop.DBus'," + - "member='NameOwnerChanged'," + - "path='/org/freedesktop/DBus'," + - "arg0namespace='org.mpris.MediaPlayer2'"; - try { - this.session_bus = Bus.get_sync (BusType.SESSION); - - this.session_bus.call_sync ("org.freedesktop.DBus", - "/", - "org.freedesktop.DBus", - "AddMatch", - new Variant ("(s)", match_rule), - VariantType.TUPLE, - DBusCallFlags.NONE, - -1); - - this.session_bus.signal_subscribe ("org.freedesktop.DBus", - "org.freedesktop.DBus", - "NameOwnerChanged", - "/org/freedesktop/DBus", - null, - DBusSignalFlags.NO_MATCH_RULE, - this.name_owner_changed); - - this.check_for_active_clients.begin(); - } - catch (GLib.Error e) { - warning ("unable to set up name watch for mrpis clients: %s", e.message); - } - } - - // At startup check to see if there are clients up that we are interested in - // More relevant for development and daemon's like mpd. - async void check_for_active_clients() - { - Variant interfaces; - - try { - interfaces = yield this.session_bus.call ("org.freedesktop.DBus", - "/", - "org.freedesktop.DBus", - "ListNames", - null, - new VariantType ("(as)"), - DBusCallFlags.NONE, - -1); - } - catch (GLib.Error e) { - warning ("unable to search for existing mpris clients: %s ", e.message); - return; - } - - foreach (var val in interfaces.get_child_value (0)) { - var address = (string) val; - if (address.has_prefix (MPRIS_PREFIX)){ - MprisRoot? mpris2_root = this.create_mpris_root(address); - if (mpris2_root == null) return; - bool use_playlists = this.supports_playlists ( address ); - client_appeared (mpris2_root.DesktopEntry + ".desktop", address, use_playlists); - } - } - } - - void name_owner_changed (DBusConnection con, string sender, string object_path, - string interface_name, string signal_name, Variant parameters) - { - string name, previous_owner, current_owner; - - parameters.get ("(sss)", out name, out previous_owner, out current_owner); - - MprisRoot? mpris2_root = this.create_mpris_root (name); - if (mpris2_root == null) return; - - if (previous_owner != "" && current_owner == "") { - debug ("Client '%s' gone down", name); - client_disappeared (name); - } - else if (previous_owner == "" && current_owner != "") { - debug ("Client '%s' has appeared", name); - bool use_playlists = this.supports_playlists ( name ); - client_appeared (mpris2_root.DesktopEntry + ".desktop", name, use_playlists); - } - } - - private MprisRoot? create_mpris_root ( string name ){ - MprisRoot mpris2_root = null; - if ( name.has_prefix (MPRIS_PREFIX) ){ - try { - mpris2_root = Bus.get_proxy_sync ( BusType.SESSION, - name, - MPRIS_MEDIA_PLAYER_PATH ); - } - catch (IOError e){ - warning( "Mpris2watcher could not create a root interface: %s", - e.message ); - } - } - return mpris2_root; - } - - private bool supports_playlists ( string name ) - { - FreeDesktopIntrospectable introspectable; - - try { - /* The dbusproxy flag parameter is needed to ensure Banshee does not - blow up. I suspect the issue is that if you - try to instantiate a dbus object which does not have any properties - associated with it, gdbus will attempt to fetch the properties (this is - in the documentation) but the banshee mpris dbus object more than likely - causes a crash because it doesn't check for the presence of properties - before attempting to access them. - */ - introspectable = Bus.get_proxy_sync ( BusType.SESSION, - name, - MPRIS_MEDIA_PLAYER_PATH, - GLib.DBusProxyFlags.DO_NOT_LOAD_PROPERTIES); - var results = introspectable.Introspect(); - return this.parse_interfaces (results); - } - catch (IOError e){ - warning( "Could not create an introspectable object: %s", - e.message ); - } - return false; - } - - private bool parse_interfaces( string interface_info ) - { - //parse the document from path - bool result = false; - Xml.Doc* xml_doc = Parser.parse_doc (interface_info); - if (xml_doc == null) { - warning ("Mpris2Watcher - parse-interfaces - failed to instantiate xml doc"); - return false; - } - //get the root node. notice the dereferencing operator -> instead of . - Xml.Node* root_node = xml_doc->get_root_element (); - if (root_node == null) { - //free the document manually before throwing because the garbage collector can't work on pointers - delete xml_doc; - warning ("Mpris2Watcher - the interface info xml is empty"); - return false; - } - - //let's parse those nodes - for (Xml.Node* iter = root_node->children; iter != null; iter = iter->next) { - //spaces btw. tags are also nodes, discard them - if (iter->type != ElementType.ELEMENT_NODE){ - continue; - } - Xml.Attr* attributes = iter->properties; //get the node's name - string interface_name = attributes->children->content; - debug ( "this dbus object has interface %s ", interface_name ); - if ( interface_name == MPRIS_PREFIX.concat("Playlists")){ - result = true; - } - } - delete xml_doc; - return result; - } -} diff --git a/src/service.vala b/src/service.vala index 4d64502..aa992ff 100644 --- a/src/service.vala +++ b/src/service.vala @@ -34,8 +34,9 @@ public class IndicatorSound.Service { this.actions.add_action (this.create_mic_volume_action ()); this.menus = new HashTable<string, SoundMenu> (str_hash, str_equal); - this.menus.insert ("desktop", new SoundMenu ("indicator.desktop-settings")); - this.menus.insert ("phone", new SoundMenu ("indicator.phone-settings")); + this.menus.insert ("desktop_greeter", new SoundMenu (null, SoundMenu.DisplayFlags.SHOW_MUTE)); + this.menus.insert ("desktop", new SoundMenu ("indicator.desktop-settings", SoundMenu.DisplayFlags.SHOW_MUTE)); + this.menus.insert ("phone", new SoundMenu ("indicator.phone-settings", SoundMenu.DisplayFlags.HIDE_INACTIVE_PLAYERS)); this.menus.@foreach ( (profile, menu) => { this.volume_control.bind_property ("active-mic", menu, "show-mic-volume", BindingFlags.SYNC_CREATE); @@ -45,6 +46,14 @@ public class IndicatorSound.Service { this.settings.changed["interested-media-players"].connect ( () => { this.players.sync (settings.get_strv ("interested-media-players")); }); + + if (settings.get_boolean ("show-notify-osd-on-scroll")) { + unowned List<string> caps = Notify.get_server_caps (); + if (caps.find_custom ("x-canonical-private-synchronous", strcmp) != null) { + this.notification = new Notify.Notification ("indicator-sound", "", ""); + this.notification.set_hint_string ("x-canonical-private-synchronous", "indicator-sound"); + } + } } public int run () { @@ -64,6 +73,7 @@ public class IndicatorSound.Service { const ActionEntry[] action_entries = { { "root", null, null, "@a{sv} {}", null }, + { "scroll", activate_scroll_action, "i", null, null }, { "desktop-settings", activate_desktop_settings, null, null, null }, { "phone-settings", activate_phone_settings, null, null, null }, }; @@ -75,13 +85,42 @@ public class IndicatorSound.Service { VolumeControl volume_control; MediaPlayerList players; uint player_action_update_id; + Notify.Notification notification; + + const double volume_step_percentage = 0.06; + + void activate_scroll_action (SimpleAction action, Variant? param) { + int delta = param.get_int32(); /* positive for up, negative for down */ + + double v = this.volume_control.get_volume () + volume_step_percentage * delta; + this.volume_control.set_volume (v.clamp (0.0, 1.0)); + + if (this.notification != null) { + string icon; + if (v <= 0.0) + icon = "notification-audio-volume-off"; + else if (v <= 0.3) + icon = "notification-audio-volume-low"; + else if (v <= 0.7) + icon = "notification-audio-volume-medium"; + else + icon = "notification-audio-volume-high"; + + this.notification.update ("indicator-sound", "", icon); + this.notification.set_hint_int32 ("value", ((int32) (100 * v)).clamp (-1, 101)); + try { + this.notification.show (); + } + catch (Error e) { + warning ("unable to show notification: %s", e.message); + } + } + } void activate_desktop_settings (SimpleAction action, Variant? param) { var env = Environment.get_variable ("DESKTOP_SESSION"); string cmd; - if (env == "unity") - cmd = "gnome-control-center sound-nua"; - else if (env == "xubuntu" || env == "ubuntustudio") + if (env == "xubuntu" || env == "ubuntustudio") cmd = "pavucontrol"; else cmd = "gnome-control-center sound"; @@ -94,11 +133,7 @@ public class IndicatorSound.Service { } void activate_phone_settings (SimpleAction action, Variant? param) { - try { - Process.spawn_command_line_async ("system-settings sound"); - } catch (Error e) { - warning ("unable to launch sound settings: %s", e.message); - } + UrlDispatch.send ("settings:///system/sound"); } /* Returns a serialized version of @icon_name suited for the panel */ @@ -124,14 +159,15 @@ public class IndicatorSound.Service { string accessible_name; if (this.volume_control.mute) { - accessible_name = "Volume (muted)"; + accessible_name = _("Volume (muted)"); } else { int volume_int = (int)(volume * 100); - accessible_name = @"Volume ($volume_int%)"; + accessible_name = "%s (%d%%)".printf (_("Volume"), volume_int); } var root_action = actions.lookup_action ("root") as SimpleAction; var builder = new VariantBuilder (new VariantType ("a{sv}")); + builder.add ("{sv}", "title", new Variant.string (_("Sound"))); builder.add ("{sv}", "accessible-desc", new Variant.string (accessible_name)); builder.add ("{sv}", "icon", serialize_themed_icon (icon)); builder.add ("{sv}", "visible", new Variant.boolean (true)); @@ -139,10 +175,10 @@ public class IndicatorSound.Service { } Action create_mute_action () { - var mute_action = new SimpleAction.stateful ("mute", null, this.volume_control.mute); + var mute_action = new SimpleAction.stateful ("mute", null, new Variant.boolean (this.volume_control.mute)); mute_action.activate.connect ( (action, param) => { - action.change_state (!action.get_state ().get_boolean ()); + action.change_state (new Variant.boolean (!action.get_state ().get_boolean ())); }); mute_action.change_state.connect ( (action, val) => { @@ -150,7 +186,7 @@ public class IndicatorSound.Service { }); this.volume_control.notify["mute"].connect ( () => { - mute_action.set_state (this.volume_control.mute); + mute_action.set_state (new Variant.boolean (this.volume_control.mute)); this.update_root_icon (); }); @@ -159,18 +195,24 @@ public class IndicatorSound.Service { void volume_changed (double volume) { var volume_action = this.actions.lookup_action ("volume") as SimpleAction; - volume_action.set_state (volume); + volume_action.set_state (new Variant.double (volume)); this.update_root_icon (); } Action create_volume_action () { - var volume_action = new SimpleAction.stateful ("volume", null, this.volume_control.get_volume ()); + var volume_action = new SimpleAction.stateful ("volume", VariantType.INT32, new Variant.double (this.volume_control.get_volume ())); volume_action.change_state.connect ( (action, val) => { volume_control.set_volume (val.get_double ()); }); + /* activating this action changes the volume by the amount given in the parameter */ + volume_action.activate.connect ( (action, param) => { + double v = volume_control.get_volume () + volume_step_percentage * param.get_int32 (); + volume_control.set_volume (v.clamp (0.0, 1.0)); + }); + this.volume_control.volume_changed.connect (volume_changed); this.volume_control.bind_property ("ready", volume_action, "enabled", BindingFlags.SYNC_CREATE); @@ -179,14 +221,14 @@ public class IndicatorSound.Service { } Action create_mic_volume_action () { - var volume_action = new SimpleAction.stateful ("mic-volume", null, this.volume_control.get_mic_volume ()); + var volume_action = new SimpleAction.stateful ("mic-volume", null, new Variant.double (this.volume_control.get_mic_volume ())); volume_action.change_state.connect ( (action, val) => { volume_control.set_mic_volume (val.get_double ()); }); this.volume_control.mic_volume_changed.connect ( (volume) => { - volume_action.set_state (volume); + volume_action.set_state (new Variant.double (volume)); }); this.volume_control.bind_property ("ready", volume_action, "enabled", BindingFlags.SYNC_CREATE); diff --git a/src/sound-menu.vala b/src/sound-menu.vala index 415a5be..f3f4fd9 100644 --- a/src/sound-menu.vala +++ b/src/sound-menu.vala @@ -22,28 +22,44 @@ extern Variant? g_icon_serialize (Icon icon); class SoundMenu: Object { - public SoundMenu (string settings_action) { + public enum DisplayFlags { + NONE = 0, + SHOW_MUTE = 1, + HIDE_INACTIVE_PLAYERS = 2 + } + + public SoundMenu (string? settings_action, DisplayFlags flags) { /* A sound menu always has at least two sections: the volume section (this.volume_section) * at the start of the menu, and the settings section at the end. Between those two, * it has a dynamic amount of player sections, one for each registered player. */ this.volume_section = new Menu (); - volume_section.append (_("Mute"), "indicator.mute"); - volume_section.append_item (this.create_slider_menu_item ("indicator.volume", 0.0, 1.0, 0.01, + if ((flags & DisplayFlags.SHOW_MUTE) != 0) + volume_section.append (_("Mute"), "indicator.mute"); + volume_section.append_item (this.create_slider_menu_item ("indicator.volume(0)", 0.0, 1.0, 0.01, "audio-volume-low-zero-panel", "audio-volume-high-panel")); this.menu = new Menu (); this.menu.append_section (null, volume_section); - this.menu.append (_("Sound Settingsā¦"), settings_action); + + if (settings_action != null) { + settings_shown = true; + this.menu.append (_("Sound Settingsā¦"), settings_action); + } var root_item = new MenuItem (null, "indicator.root"); root_item.set_attribute ("x-canonical-type", "s", "com.canonical.indicator.root"); + root_item.set_attribute ("x-canonical-scroll-action", "s", "indicator.scroll"); + root_item.set_attribute ("x-canonical-secondary-action", "s", "indicator.mute"); root_item.set_submenu (this.menu); this.root = new Menu (); root.append_item (root_item); + + this.hide_inactive = (flags & DisplayFlags.HIDE_INACTIVE_PLAYERS) != 0; + this.notify_handlers = new HashTable<MediaPlayer, ulong> (direct_hash, direct_equal); } public void export (DBusConnection connection, string object_path) { @@ -56,53 +72,57 @@ class SoundMenu: Object public bool show_mic_volume { get { - return this.volume_section.get_n_items () == 3; + return this.mic_volume_shown; } set { - if (value && this.volume_section.get_n_items () < 3) { + if (value && !this.mic_volume_shown) { var slider = this.create_slider_menu_item ("indicator.mic-volume", 0.0, 1.0, 0.01, "audio-input-microphone-low-zero-panel", "audio-input-microphone-high-panel"); volume_section.append_item (slider); + this.mic_volume_shown = true; } - else if (!value && this.volume_section.get_n_items () > 2) { - this.volume_section.remove (2); + else if (!value && this.mic_volume_shown) { + this.volume_section.remove (this.volume_section.get_n_items () -1); + this.mic_volume_shown = false; } } } public void add_player (MediaPlayer player) { - /* Add new players to the end of the player sections, just before the settings */ - var player_item = new MenuItem (player.name, "indicator." + player.id); - player_item.set_attribute ("x-canonical-type", "s", "com.canonical.unity.media-player"); - player_item.set_attribute_value ("icon", g_icon_serialize (player.icon)); + if (this.notify_handlers.contains (player)) + return; - var playback_item = new MenuItem (null, null); - playback_item.set_attribute ("x-canonical-type", "s", "com.canonical.unity.playback-item"); - playback_item.set_attribute ("x-canonical-play-action", "s", "indicator.play." + player.id); - playback_item.set_attribute ("x-canonical-next-action", "s", "indicator.next." + player.id); - playback_item.set_attribute ("x-canonical-previous-action", "s", "indicator.previous." + player.id); + if (player.is_running || !this.hide_inactive) + this.insert_player_section (player); + this.update_playlists (player); - var section = new Menu (); - section.append_item (player_item); - section.append_item (playback_item); + var handler_id = player.notify["is-running"].connect ( () => { + if (this.hide_inactive) { + if (player.is_running) + this.insert_player_section (player); + else + this.remove_player_section (player); + } + this.update_playlists (player); + }); + this.notify_handlers.insert (player, handler_id); player.playlists_changed.connect (this.update_playlists); - player.notify["is-running"].connect ( () => this.update_playlists (player) ); - update_playlists (player); - - this.menu.insert_section (this.menu.get_n_items () -1, null, section); } public void remove_player (MediaPlayer player) { - int index = this.find_player_section (player); - if (index >= 0) - this.menu.remove (index); + this.remove_player_section (player); + this.notify_handlers.remove (player); } Menu root; Menu menu; Menu volume_section; + bool mic_volume_shown; + bool settings_shown = false; + bool hide_inactive; + HashTable<MediaPlayer, ulong> notify_handlers; /* returns the position in this.menu of the section that's associated with @player */ int find_player_section (MediaPlayer player) { @@ -119,6 +139,41 @@ class SoundMenu: Object return -1; } + void insert_player_section (MediaPlayer player) { + var section = new Menu (); + Icon icon; + + icon = player.icon; + if (icon == null) + icon = new ThemedIcon.with_default_fallbacks ("application-default-icon"); + + var player_item = new MenuItem (player.name, "indicator." + player.id); + player_item.set_attribute ("x-canonical-type", "s", "com.canonical.unity.media-player"); + if (icon != null) + player_item.set_attribute_value ("icon", g_icon_serialize (icon)); + section.append_item (player_item); + + var playback_item = new MenuItem (null, null); + playback_item.set_attribute ("x-canonical-type", "s", "com.canonical.unity.playback-item"); + playback_item.set_attribute ("x-canonical-play-action", "s", "indicator.play." + player.id); + playback_item.set_attribute ("x-canonical-next-action", "s", "indicator.next." + player.id); + playback_item.set_attribute ("x-canonical-previous-action", "s", "indicator.previous." + player.id); + section.append_item (playback_item); + + /* Add new players to the end of the player sections, just before the settings */ + if (settings_shown) { + this.menu.insert_section (this.menu.get_n_items () -1, null, section); + } else { + this.menu.append_section (null, section); + } + } + + void remove_player_section (MediaPlayer player) { + int index = this.find_player_section (player); + if (index >= 0) + this.menu.remove (index); + } + void update_playlists (MediaPlayer player) { int index = find_player_section (player); if (index < 0) @@ -147,7 +202,7 @@ class SoundMenu: Object var submenu = new Menu (); submenu.append_section (null, playlists_section); - player_section.append_submenu ("Choose Playlist", submenu); + player_section.append_submenu (_("Choose Playlist"), submenu); } MenuItem create_slider_menu_item (string action, double min, double max, double step, string min_icon_name, string max_icon_name) { diff --git a/src/volume-control.vala b/src/volume-control.vala index 9475f53..c6db38c 100644 --- a/src/volume-control.vala +++ b/src/volume-control.vala @@ -27,6 +27,8 @@ public class VolumeControl : Object /* this is static to ensure it being freed after @context (loop does not have ref counting) */ private static PulseAudio.GLibMainLoop loop; + private uint _reconnect_timer = 0; + private PulseAudio.Context context; private bool _mute = true; private double _volume = 0.0; @@ -46,20 +48,13 @@ public class VolumeControl : Object if (loop == null) loop = new PulseAudio.GLibMainLoop (); - var props = new Proplist (); - props.sets (Proplist.PROP_APPLICATION_NAME, "Ubuntu Audio Settings"); - props.sets (Proplist.PROP_APPLICATION_ID, "com.canonical.settings.sound"); - props.sets (Proplist.PROP_APPLICATION_ICON_NAME, "multimedia-volume-control"); - props.sets (Proplist.PROP_APPLICATION_VERSION, "0.1"); - - context = new PulseAudio.Context (loop.get_api(), null, props); - - context.set_state_callback (context_state_callback); + this.reconnect_to_pulse (); + } - if (context.connect(null, Context.Flags.NOFAIL, null) < 0) - { - warning( "pa_context_connect() failed: %s\n", PulseAudio.strerror(context.errno())); - return; + ~VolumeControl () + { + if (_reconnect_timer != 0) { + Source.remove (_reconnect_timer); } } @@ -133,12 +128,14 @@ public class VolumeControl : Object context.get_server_info (server_info_cb_for_props); } + private void update_source_get_server_info_cb (PulseAudio.Context c, PulseAudio.ServerInfo? i) { + if (i != null) + context.get_source_info_by_name (i.default_source_name, source_info_cb); + } + private void update_source () { - context.get_server_info ( (c, i) => { - if (i != null) - context.get_source_info_by_name (i.default_source_name, source_info_cb); - }); + context.get_server_info (update_source_get_server_info_cb); } private void source_output_info_cb (Context c, SourceOutputInfo? i, int eol) @@ -153,18 +150,65 @@ public class VolumeControl : Object private void context_state_callback (Context c) { - if (c.get_state () == Context.State.READY) - { - c.subscribe (PulseAudio.Context.SubscriptionMask.SINK | - PulseAudio.Context.SubscriptionMask.SOURCE | - PulseAudio.Context.SubscriptionMask.SOURCE_OUTPUT); - c.set_subscribe_callback (context_events_cb); - update_sink (); - update_source (); - this.ready = true; + switch (c.get_state ()) { + case Context.State.READY: + c.subscribe (PulseAudio.Context.SubscriptionMask.SINK | + PulseAudio.Context.SubscriptionMask.SOURCE | + PulseAudio.Context.SubscriptionMask.SOURCE_OUTPUT); + c.set_subscribe_callback (context_events_cb); + update_sink (); + update_source (); + this.ready = true; + break; + + case Context.State.FAILED: + case Context.State.TERMINATED: + if (_reconnect_timer == 0) + _reconnect_timer = Timeout.add_seconds (2, reconnect_timeout); + break; + + default: + this.ready = false; + break; } - else + } + + bool reconnect_timeout () + { + _reconnect_timer = 0; + reconnect_to_pulse (); + return false; // G_SOURCE_REMOVE + } + + void reconnect_to_pulse () + { + if (this.ready) { + this.context.disconnect (); + this.context = null; this.ready = false; + } + + var props = new Proplist (); + props.sets (Proplist.PROP_APPLICATION_NAME, "Ubuntu Audio Settings"); + props.sets (Proplist.PROP_APPLICATION_ID, "com.canonical.settings.sound"); + props.sets (Proplist.PROP_APPLICATION_ICON_NAME, "multimedia-volume-control"); + props.sets (Proplist.PROP_APPLICATION_VERSION, "0.1"); + + this.context = new PulseAudio.Context (loop.get_api(), null, props); + this.context.set_state_callback (context_state_callback); + + if (context.connect(null, Context.Flags.NOFAIL, null) < 0) + warning( "pa_context_connect() failed: %s\n", PulseAudio.strerror(context.errno())); + } + + void sink_info_list_callback_set_mute (PulseAudio.Context context, PulseAudio.SinkInfo? sink, int eol) { + if (sink != null) + context.set_sink_mute_by_index (sink.index, true, null); + } + + void sink_info_list_callback_unset_mute (PulseAudio.Context context, PulseAudio.SinkInfo? sink, int eol) { + if (sink != null) + context.set_sink_mute_by_index (sink.index, false, null); } /* Mute operations */ @@ -172,10 +216,10 @@ public class VolumeControl : Object { return_if_fail (context.get_state () == Context.State.READY); - context.get_sink_info_list ((context, sink, eol) => { - if (sink != null) - context.set_sink_mute_by_index (sink.index, mute, null); - }); + if (mute) + context.get_sink_info_list (sink_info_list_callback_set_mute); + else + context.get_sink_info_list (sink_info_list_callback_unset_mute); } public void toggle_mute () @@ -245,19 +289,21 @@ public class VolumeControl : Object mic_volume_changed (_mic_volume); } + void set_mic_volume_get_server_info_cb (PulseAudio.Context c, PulseAudio.ServerInfo? i) { + if (i != null) { + unowned CVolume cvol = CVolume (); + cvol = vol_set (cvol, 1, double_to_volume (_mic_volume)); + c.set_source_volume_by_name (i.default_source_name, cvol, set_mic_volume_success_cb); + } + } + public void set_mic_volume (double volume) { return_if_fail (context.get_state () == Context.State.READY); _mic_volume = volume; - context.get_server_info ( (c, i) => { - if (i != null) { - unowned CVolume cvol = CVolume (); - cvol = vol_set (cvol, 1, double_to_volume (_mic_volume)); - c.set_source_volume_by_name (i.default_source_name, cvol, set_mic_volume_success_cb); - } - }); + context.get_server_info (set_mic_volume_get_server_info_cb); } public double get_volume () |