From 98c5556a14454fa988b0b81ffb22cef9a67011e9 Mon Sep 17 00:00:00 2001 From: Lars Uebernickel Date: Fri, 2 Aug 2013 12:06:55 +0200 Subject: Use bus_watch_namespace() to monitor mpris players This function is more robust than the current code and uses glib's G_DBUS_SIGNAL_FLAGS_MATCH_ARG0_NAMESPACE instead of creating match rules itself. bus-watch-namespace.[ch] is shared with gnome-settings-daemon. --- src/Makefile.am | 6 +- src/bus-watch-namespace.c | 347 +++++++++++++++++++++++++++++++++++++++++++++ src/bus-watch-namespace.h | 34 +++++ src/media-player-list.vala | 22 +-- src/mpris2-watcher.vala | 200 -------------------------- 5 files changed, 398 insertions(+), 211 deletions(-) create mode 100644 src/bus-watch-namespace.c create mode 100644 src/bus-watch-namespace.h delete mode 100644 src/mpris2-watcher.vala (limited to 'src') diff --git a/src/Makefile.am b/src/Makefile.am index 8af02ce..1a82a18 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -7,9 +7,10 @@ indicator_sound_service_SOURCES = \ media-player.vala \ media-player-list.vala \ mpris2-interfaces.vala \ - mpris2-watcher.vala \ freedesktop-interfaces.vala \ - sound-menu.vala + sound-menu.vala \ + bus-watch-namespace.c \ + bus-watch-namespace.h indicator_sound_service_VALAFLAGS = \ --ccode \ @@ -22,6 +23,7 @@ indicator_sound_service_VALAFLAGS = \ --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 diff --git a/src/bus-watch-namespace.c b/src/bus-watch-namespace.c new file mode 100644 index 0000000..1ffdff4 --- /dev/null +++ b/src/bus-watch-namespace.c @@ -0,0 +1,347 @@ +/* + * 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 . + * + * Author: Lars Uebernickel + */ + +#include +#include +#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); +} + +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 . + * + * Author: Lars Uebernickel + */ + +#ifndef __BUS_WATCH_NAMESPACE_H__ +#define __BUS_WATCH_NAMESPACE_H__ + +#include + +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/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 (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 _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 - -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 . -*/ - -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; - } -} -- cgit v1.2.3 From 2ee354228a76d7c8b70536ca3d439e5025fbfd4b Mon Sep 17 00:00:00 2001 From: Lars Uebernickel Date: Mon, 26 Aug 2013 15:45:44 +0200 Subject: bus-watch-namespace: free name_space --- src/bus-watch-namespace.c | 2 ++ 1 file changed, 2 insertions(+) (limited to 'src') diff --git a/src/bus-watch-namespace.c b/src/bus-watch-namespace.c index 1ffdff4..dab6d23 100644 --- a/src/bus-watch-namespace.c +++ b/src/bus-watch-namespace.c @@ -81,6 +81,8 @@ namespace_watcher_stop (gpointer data) 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); } -- cgit v1.2.3 From 2094cc8a534baf610f028676092deee6e29e8beb Mon Sep 17 00:00:00 2001 From: Lars Uebernickel Date: Mon, 26 Aug 2013 15:46:14 +0200 Subject: bus-watch-namespace: remove stray semicolon --- src/bus-watch-namespace.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src') diff --git a/src/bus-watch-namespace.c b/src/bus-watch-namespace.c index dab6d23..f20905c 100644 --- a/src/bus-watch-namespace.c +++ b/src/bus-watch-namespace.c @@ -313,7 +313,7 @@ bus_watch_namespace (GBusType bus_type, watcher->vanished_handler = vanished_handler; watcher->user_data = user_data; watcher->user_data_destroy = user_data_destroy; - watcher->cancellable = g_cancellable_new ();; + 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) -- cgit v1.2.3 From 5d5353c5b761b0adc272e4826690d90984b9413f Mon Sep 17 00:00:00 2001 From: Lars Uebernickel Date: Mon, 9 Sep 2013 13:34:02 +0200 Subject: Change volume when scrolling on the indicator This depends on a new root menu property: x-canonical-scroll-action. It points to an action that gets activated whenever the user scrolls the mouse over the indicator. The parameter of that action signifies the magnitude and direction of the scroll. --- src/service.vala | 14 ++++++++++++++ src/sound-menu.vala | 1 + 2 files changed, 15 insertions(+) (limited to 'src') diff --git a/src/service.vala b/src/service.vala index 4d64502..0651c0e 100644 --- a/src/service.vala +++ b/src/service.vala @@ -64,6 +64,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 }, }; @@ -76,6 +77,19 @@ public class IndicatorSound.Service { MediaPlayerList players; uint player_action_update_id; + void activate_scroll_action (SimpleAction action, Variant? param) { + const double volume_step_percentage = 0.06; + int delta = param.get_int32(); /* positive for up, negative for down */ + + double v = this.volume_control.get_volume () + volume_step_percentage * delta; + if (v > 1.0) + v = 1.0; + else if (v < 0.0) + v = 0.0; + + this.volume_control.set_volume (v); + } + void activate_desktop_settings (SimpleAction action, Variant? param) { var env = Environment.get_variable ("DESKTOP_SESSION"); string cmd; diff --git a/src/sound-menu.vala b/src/sound-menu.vala index 415a5be..7ac8930 100644 --- a/src/sound-menu.vala +++ b/src/sound-menu.vala @@ -40,6 +40,7 @@ class SoundMenu: Object 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_submenu (this.menu); this.root = new Menu (); -- cgit v1.2.3 From 8188fd2fac8516203b90e9da4872c0d8055cb310 Mon Sep 17 00:00:00 2001 From: Lars Uebernickel Date: Mon, 9 Sep 2013 14:11:47 +0200 Subject: Toggle mute on secondary action This depends on the panel supporting x-canonical-secondary-action on the root menu item. --- src/sound-menu.vala | 1 + 1 file changed, 1 insertion(+) (limited to 'src') diff --git a/src/sound-menu.vala b/src/sound-menu.vala index 7ac8930..e46f098 100644 --- a/src/sound-menu.vala +++ b/src/sound-menu.vala @@ -41,6 +41,7 @@ class SoundMenu: Object 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 (); -- cgit v1.2.3 From eb032fc5be2d9f02f52c71b612c0aa971a2f1c96 Mon Sep 17 00:00:00 2001 From: Lars Uebernickel Date: Wed, 11 Sep 2013 11:32:31 +0200 Subject: Mark remaining user-visible strings as translatable --- src/service.vala | 4 ++-- src/sound-menu.vala | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) (limited to 'src') diff --git a/src/service.vala b/src/service.vala index 0651c0e..7d59fe5 100644 --- a/src/service.vala +++ b/src/service.vala @@ -138,10 +138,10 @@ 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 = _("Volume (%d%%)").printf (volume_int); } var root_action = actions.lookup_action ("root") as SimpleAction; diff --git a/src/sound-menu.vala b/src/sound-menu.vala index e46f098..b08db93 100644 --- a/src/sound-menu.vala +++ b/src/sound-menu.vala @@ -149,7 +149,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) { -- cgit v1.2.3 From 5423d97e09eb9e21f513e6c09fbc61fc30c6a38e Mon Sep 17 00:00:00 2001 From: Lars Uebernickel Date: Wed, 11 Sep 2013 12:31:10 +0200 Subject: Remove volume percentage from translatable part of the accessible title --- src/service.vala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src') diff --git a/src/service.vala b/src/service.vala index 7d59fe5..5cf90b6 100644 --- a/src/service.vala +++ b/src/service.vala @@ -141,7 +141,7 @@ public class IndicatorSound.Service { accessible_name = _("Volume (muted)"); } else { int volume_int = (int)(volume * 100); - accessible_name = _("Volume (%d%%)").printf (volume_int); + accessible_name = "%s (%d%%)".printf (_("Volume"), volume_int); } var root_action = actions.lookup_action ("root") as SimpleAction; -- cgit v1.2.3 From babdfeea38806f55722eec0f457f0846d7fb7013 Mon Sep 17 00:00:00 2001 From: Lars Uebernickel Date: Thu, 12 Sep 2013 13:35:53 +0200 Subject: Add "title" to the root action state dictionary --- src/service.vala | 1 + 1 file changed, 1 insertion(+) (limited to 'src') diff --git a/src/service.vala b/src/service.vala index 5cf90b6..1b3a81a 100644 --- a/src/service.vala +++ b/src/service.vala @@ -146,6 +146,7 @@ public class IndicatorSound.Service { 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)); -- cgit v1.2.3 From f0b5cc08bb303864981d8c011faab495aa4e6781 Mon Sep 17 00:00:00 2001 From: Lars Uebernickel Date: Sun, 15 Sep 2013 20:31:36 +0200 Subject: Don't show the "Mute" menu item in the phone profile --- src/service.vala | 4 ++-- src/sound-menu.vala | 5 +++-- 2 files changed, 5 insertions(+), 4 deletions(-) (limited to 'src') diff --git a/src/service.vala b/src/service.vala index 1b3a81a..f43a1a1 100644 --- a/src/service.vala +++ b/src/service.vala @@ -34,8 +34,8 @@ public class IndicatorSound.Service { this.actions.add_action (this.create_mic_volume_action ()); this.menus = new HashTable (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", new SoundMenu (true, "indicator.desktop-settings")); + this.menus.insert ("phone", new SoundMenu (false, "indicator.phone-settings")); this.menus.@foreach ( (profile, menu) => { this.volume_control.bind_property ("active-mic", menu, "show-mic-volume", BindingFlags.SYNC_CREATE); diff --git a/src/sound-menu.vala b/src/sound-menu.vala index b08db93..251f2f5 100644 --- a/src/sound-menu.vala +++ b/src/sound-menu.vala @@ -22,14 +22,15 @@ extern Variant? g_icon_serialize (Icon icon); class SoundMenu: Object { - public SoundMenu (string settings_action) { + public SoundMenu (bool show_mute, string settings_action) { /* 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"); + if (show_mute) + volume_section.append (_("Mute"), "indicator.mute"); volume_section.append_item (this.create_slider_menu_item ("indicator.volume", 0.0, 1.0, 0.01, "audio-volume-low-zero-panel", "audio-volume-high-panel")); -- cgit v1.2.3 From f23473220d279171d4659fe5400fc95982de3e86 Mon Sep 17 00:00:00 2001 From: Lars Uebernickel Date: Sun, 15 Sep 2013 20:39:49 +0200 Subject: sound-menu.vala: make the logic for showing the mic volume more explicit Before, this was done by counting the items that are currently in the volume section. This broke with the last commit, because with that, the section might not contain a mute menu item. --- src/sound-menu.vala | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) (limited to 'src') diff --git a/src/sound-menu.vala b/src/sound-menu.vala index 251f2f5..fe61cdf 100644 --- a/src/sound-menu.vala +++ b/src/sound-menu.vala @@ -59,17 +59,19 @@ 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; } } } @@ -106,6 +108,7 @@ class SoundMenu: Object Menu root; Menu menu; Menu volume_section; + bool mic_volume_shown; /* returns the position in this.menu of the section that's associated with @player */ int find_player_section (MediaPlayer player) { -- cgit v1.2.3 From 6621a3d848fd50062f25e95f1787c5084aed0671 Mon Sep 17 00:00:00 2001 From: Charles Kerr Date: Wed, 25 Sep 2013 16:07:05 -0500 Subject: Use url-dispatcher instead of invoking system-settings directly. --- src/CMakeLists.txt | 1 + src/service.vala | 6 +----- 2 files changed, 2 insertions(+), 5 deletions(-) (limited to 'src') diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index a28147d..a61fb72 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -20,6 +20,7 @@ vala_init(indicator-sound-service --vapidir=${CMAKE_SOURCE_DIR}/vapi/ --vapidir=. --target-glib=2.36 + --pkg=url-dispatcher --pkg=bus-watcher ) diff --git a/src/service.vala b/src/service.vala index f43a1a1..c7d4809 100644 --- a/src/service.vala +++ b/src/service.vala @@ -108,11 +108,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 */ -- cgit v1.2.3 From c97dcb08b160876be3057b88cd368fc7a2e3cab9 Mon Sep 17 00:00:00 2001 From: Ted Gould Date: Thu, 26 Sep 2013 21:30:30 -0500 Subject: Update settings URL to settings:///system --- src/service.vala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src') diff --git a/src/service.vala b/src/service.vala index c7d4809..9c7bcc9 100644 --- a/src/service.vala +++ b/src/service.vala @@ -108,7 +108,7 @@ public class IndicatorSound.Service { } void activate_phone_settings (SimpleAction action, Variant? param) { - UrlDispatch.send ("settings://system/sound"); + UrlDispatch.send ("settings:///system/sound"); } /* Returns a serialized version of @icon_name suited for the panel */ -- cgit v1.2.3 From 86719275d89bb2c48d1cc9451f6add1918ee0f75 Mon Sep 17 00:00:00 2001 From: Lars Uebernickel Date: Mon, 30 Sep 2013 14:13:32 +0200 Subject: Reconnect when pulseaudio terminates (or crashes) --- src/volume-control.vala | 65 ++++++++++++++++++++++++++++++------------------- 1 file changed, 40 insertions(+), 25 deletions(-) (limited to 'src') diff --git a/src/volume-control.vala b/src/volume-control.vala index 9475f53..18c407f 100644 --- a/src/volume-control.vala +++ b/src/volume-control.vala @@ -46,21 +46,7 @@ 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); - - if (context.connect(null, Context.Flags.NOFAIL, null) < 0) - { - warning( "pa_context_connect() failed: %s\n", PulseAudio.strerror(context.errno())); - return; - } + this.reconnect_to_pulse (); } /* PulseAudio logic*/ @@ -153,18 +139,47 @@ 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: + this.reconnect_to_pulse (); + break; + + default: + this.ready = false; + break; } - else + } + + 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())); } /* Mute operations */ -- cgit v1.2.3 From 4600123677c7897e76eb8b666c2b3211ebcd19d4 Mon Sep 17 00:00:00 2001 From: Lars Uebernickel Date: Tue, 1 Oct 2013 14:02:12 +0200 Subject: service: use vala's double.clamp() --- src/service.vala | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) (limited to 'src') diff --git a/src/service.vala b/src/service.vala index 9c7bcc9..9041097 100644 --- a/src/service.vala +++ b/src/service.vala @@ -82,12 +82,7 @@ public class IndicatorSound.Service { int delta = param.get_int32(); /* positive for up, negative for down */ double v = this.volume_control.get_volume () + volume_step_percentage * delta; - if (v > 1.0) - v = 1.0; - else if (v < 0.0) - v = 0.0; - - this.volume_control.set_volume (v); + this.volume_control.set_volume (v.clamp (0.0, 1.0)); } void activate_desktop_settings (SimpleAction action, Variant? param) { -- cgit v1.2.3 From d31be181a2c69c167bb61dbb9f1007f346920ac8 Mon Sep 17 00:00:00 2001 From: Lars Uebernickel Date: Tue, 1 Oct 2013 14:11:04 +0200 Subject: Show synchronous notification when changing the volume by scrolling over the indicator --- src/CMakeLists.txt | 1 + src/main.vala | 2 ++ src/service.vala | 30 ++++++++++++++++++++++++++++++ 3 files changed, 33 insertions(+) (limited to 'src') diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index a61fb72..572befd 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -14,6 +14,7 @@ vala_init(indicator-sound-service libxml-2.0 libpulse libpulse-mainloop-glib + libnotify OPTIONS --ccode --thread 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/service.vala b/src/service.vala index 9041097..0d028ac 100644 --- a/src/service.vala +++ b/src/service.vala @@ -45,6 +45,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 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 () { @@ -76,6 +84,7 @@ public class IndicatorSound.Service { VolumeControl volume_control; MediaPlayerList players; uint player_action_update_id; + Notify.Notification notification; void activate_scroll_action (SimpleAction action, Variant? param) { const double volume_step_percentage = 0.06; @@ -83,6 +92,27 @@ public class IndicatorSound.Service { 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) { -- cgit v1.2.3 From f488f4b9b87701a65433218d2c7d4bcf219f98bd Mon Sep 17 00:00:00 2001 From: Ted Gould Date: Wed, 2 Oct 2013 14:13:49 -0500 Subject: Making the settings action optional --- src/service.vala | 1 + src/sound-menu.vala | 15 ++++++++++++--- 2 files changed, 13 insertions(+), 3 deletions(-) (limited to 'src') diff --git a/src/service.vala b/src/service.vala index 0d028ac..1611608 100644 --- a/src/service.vala +++ b/src/service.vala @@ -34,6 +34,7 @@ public class IndicatorSound.Service { this.actions.add_action (this.create_mic_volume_action ()); this.menus = new HashTable (str_hash, str_equal); + this.menus.insert ("desktop_greeter", new SoundMenu (true, null)); this.menus.insert ("desktop", new SoundMenu (true, "indicator.desktop-settings")); this.menus.insert ("phone", new SoundMenu (false, "indicator.phone-settings")); diff --git a/src/sound-menu.vala b/src/sound-menu.vala index fe61cdf..6c6771a 100644 --- a/src/sound-menu.vala +++ b/src/sound-menu.vala @@ -22,7 +22,7 @@ extern Variant? g_icon_serialize (Icon icon); class SoundMenu: Object { - public SoundMenu (bool show_mute, string settings_action) { + public SoundMenu (bool show_mute, string? settings_action) { /* 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. @@ -37,7 +37,11 @@ class SoundMenu: Object 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"); @@ -96,7 +100,11 @@ class SoundMenu: Object player.notify["is-running"].connect ( () => this.update_playlists (player) ); update_playlists (player); - this.menu.insert_section (this.menu.get_n_items () -1, null, section); + if (settings_shown) { + this.menu.insert_section (this.menu.get_n_items () -1, null, section); + } else { + this.menu.append_section (null, section); + } } public void remove_player (MediaPlayer player) { @@ -109,6 +117,7 @@ class SoundMenu: Object Menu menu; Menu volume_section; bool mic_volume_shown; + bool settings_shown; /* returns the position in this.menu of the section that's associated with @player */ int find_player_section (MediaPlayer player) { -- cgit v1.2.3 From ef6e7a7243e9d86087f4357eb9522feb9667a308 Mon Sep 17 00:00:00 2001 From: Ted Gould Date: Wed, 2 Oct 2013 14:40:00 -0500 Subject: Make sure to set the initial value of settings shown --- src/sound-menu.vala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src') diff --git a/src/sound-menu.vala b/src/sound-menu.vala index 6c6771a..9d0c521 100644 --- a/src/sound-menu.vala +++ b/src/sound-menu.vala @@ -117,7 +117,7 @@ class SoundMenu: Object Menu menu; Menu volume_section; bool mic_volume_shown; - bool settings_shown; + bool settings_shown = false; /* returns the position in this.menu of the section that's associated with @player */ int find_player_section (MediaPlayer player) { -- cgit v1.2.3 From 5aa7613689ff9bb097e655334cc829a7c65e16df Mon Sep 17 00:00:00 2001 From: Lars Uebernickel Date: Wed, 9 Oct 2013 15:01:14 +0200 Subject: Allow activating the 'volume' action It does the same as the 'scroll' action except showing a notification. --- src/service.vala | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) (limited to 'src') diff --git a/src/service.vala b/src/service.vala index 1611608..e69bca8 100644 --- a/src/service.vala +++ b/src/service.vala @@ -87,8 +87,9 @@ public class IndicatorSound.Service { uint player_action_update_id; Notify.Notification notification; + const double volume_step_percentage = 0.06; + void activate_scroll_action (SimpleAction action, Variant? param) { - const double volume_step_percentage = 0.06; int delta = param.get_int32(); /* positive for up, negative for down */ double v = this.volume_control.get_volume () + volume_step_percentage * delta; @@ -202,12 +203,18 @@ public class IndicatorSound.Service { } 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, 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); -- cgit v1.2.3 From 81c82dca758bf30134e2483dfa897de0baecf647 Mon Sep 17 00:00:00 2001 From: Lars Uebernickel Date: Thu, 10 Oct 2013 05:32:49 +0200 Subject: Give the volume menu item a target, because the 'volume' action now has a parameter --- src/sound-menu.vala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src') diff --git a/src/sound-menu.vala b/src/sound-menu.vala index 9d0c521..1641219 100644 --- a/src/sound-menu.vala +++ b/src/sound-menu.vala @@ -31,7 +31,7 @@ class SoundMenu: Object this.volume_section = new Menu (); if (show_mute) volume_section.append (_("Mute"), "indicator.mute"); - volume_section.append_item (this.create_slider_menu_item ("indicator.volume", 0.0, 1.0, 0.01, + 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")); -- cgit v1.2.3 From 071576b6e7b3d979c1fcefd611b67bac171f2877 Mon Sep 17 00:00:00 2001 From: Lars Uebernickel Date: Thu, 10 Oct 2013 15:46:54 +0200 Subject: Replace 'mute' boolean by a flag in the SoundMenu constructor This makes the code calling the constructor more readable and allows for extensibility. --- src/service.vala | 6 +++--- src/sound-menu.vala | 9 +++++++-- 2 files changed, 10 insertions(+), 5 deletions(-) (limited to 'src') diff --git a/src/service.vala b/src/service.vala index 1611608..9c75044 100644 --- a/src/service.vala +++ b/src/service.vala @@ -34,9 +34,9 @@ public class IndicatorSound.Service { this.actions.add_action (this.create_mic_volume_action ()); this.menus = new HashTable (str_hash, str_equal); - this.menus.insert ("desktop_greeter", new SoundMenu (true, null)); - this.menus.insert ("desktop", new SoundMenu (true, "indicator.desktop-settings")); - this.menus.insert ("phone", new SoundMenu (false, "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.NONE)); this.menus.@foreach ( (profile, menu) => { this.volume_control.bind_property ("active-mic", menu, "show-mic-volume", BindingFlags.SYNC_CREATE); diff --git a/src/sound-menu.vala b/src/sound-menu.vala index 9d0c521..25234d9 100644 --- a/src/sound-menu.vala +++ b/src/sound-menu.vala @@ -22,14 +22,19 @@ extern Variant? g_icon_serialize (Icon icon); class SoundMenu: Object { - public SoundMenu (bool show_mute, string? settings_action) { + public enum DisplayFlags { + NONE = 0, + SHOW_MUTE = 1 + } + + 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 (); - if (show_mute) + 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, 1.0, 0.01, "audio-volume-low-zero-panel", -- cgit v1.2.3 From a66beb0805404bdf8f138fd3a683372487e6e35b Mon Sep 17 00:00:00 2001 From: Lars Uebernickel Date: Thu, 10 Oct 2013 16:42:49 +0200 Subject: Only display running players in the phone's sound menu --- src/service.vala | 2 +- src/sound-menu.vala | 77 ++++++++++++++++++++++++++++++++++++----------------- 2 files changed, 54 insertions(+), 25 deletions(-) (limited to 'src') diff --git a/src/service.vala b/src/service.vala index 9c75044..98a0bec 100644 --- a/src/service.vala +++ b/src/service.vala @@ -36,7 +36,7 @@ public class IndicatorSound.Service { this.menus = new HashTable (str_hash, str_equal); 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.NONE)); + 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); diff --git a/src/sound-menu.vala b/src/sound-menu.vala index 25234d9..0168f28 100644 --- a/src/sound-menu.vala +++ b/src/sound-menu.vala @@ -24,7 +24,8 @@ class SoundMenu: Object { public enum DisplayFlags { NONE = 0, - SHOW_MUTE = 1 + SHOW_MUTE = 1, + HIDE_INACTIVE_PLAYERS = 2 } public SoundMenu (string? settings_action, DisplayFlags flags) { @@ -56,6 +57,9 @@ class SoundMenu: Object this.root = new Menu (); root.append_item (root_item); + + this.hide_inactive = (flags & DisplayFlags.HIDE_INACTIVE_PLAYERS) != 0; + this.notify_handlers = new HashTable (direct_hash, direct_equal); } public void export (DBusConnection connection, string object_path) { @@ -86,36 +90,30 @@ class SoundMenu: Object } 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); - - if (settings_shown) { - this.menu.insert_section (this.menu.get_n_items () -1, null, section); - } else { - this.menu.append_section (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; @@ -123,6 +121,8 @@ class SoundMenu: Object Menu volume_section; bool mic_volume_shown; bool settings_shown = false; + bool hide_inactive; + HashTable notify_handlers; /* returns the position in this.menu of the section that's associated with @player */ int find_player_section (MediaPlayer player) { @@ -139,6 +139,35 @@ class SoundMenu: Object return -1; } + void insert_player_section (MediaPlayer player) { + var section = new Menu (); + + 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)); + 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) -- cgit v1.2.3 From 91a91a9bda0df197f04d0d59ee2fbf0da225dd42 Mon Sep 17 00:00:00 2001 From: Charles Kerr Date: Wed, 23 Oct 2013 21:55:56 -0500 Subject: when we fail to connect to pulse, wait a moment before retrying. --- src/volume-control.vala | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) (limited to 'src') diff --git a/src/volume-control.vala b/src/volume-control.vala index 18c407f..4ca9537 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; @@ -49,6 +51,13 @@ public class VolumeControl : Object this.reconnect_to_pulse (); } + ~VolumeControl () + { + if (_reconnect_timer != 0) { + Source.remove (_reconnect_timer); + } + } + /* PulseAudio logic*/ private void context_events_cb (Context c, Context.SubscriptionEventType t, uint32 index) { @@ -152,7 +161,8 @@ public class VolumeControl : Object case Context.State.FAILED: case Context.State.TERMINATED: - this.reconnect_to_pulse (); + if (_reconnect_timer == 0) + _reconnect_timer = Timeout.add_seconds (2, reconnect_timeout); break; default: @@ -161,6 +171,13 @@ public class VolumeControl : Object } } + bool reconnect_timeout () + { + _reconnect_timer = 0; + reconnect_to_pulse (); + return false; // G_SOURCE_REMOVE + } + void reconnect_to_pulse () { if (this.ready) { -- cgit v1.2.3 From ac9786b5400419b8360e2de42e5df0c53d3fd5f9 Mon Sep 17 00:00:00 2001 From: Sebastien Bacher Date: Thu, 24 Oct 2013 15:48:08 -0400 Subject: Don't special case Unity sessions, that's not needed since gnome-control-center is doing the right thing, the session name used was also wrong and couldn't work (lp: #1239545) --- src/service.vala | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) (limited to 'src') diff --git a/src/service.vala b/src/service.vala index 393a1ea..0b1ec77 100644 --- a/src/service.vala +++ b/src/service.vala @@ -120,9 +120,7 @@ public class IndicatorSound.Service { 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"; -- cgit v1.2.3 From c936addd06129c46196d487ad37b149b2fbc3a9d Mon Sep 17 00:00:00 2001 From: Charles Kerr Date: Thu, 24 Oct 2013 18:17:35 -0500 Subject: in Service's volume_changed(), explicitly instantiate the GVariant to avoid leaking it --- src/service.vala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src') diff --git a/src/service.vala b/src/service.vala index 0b1ec77..c9c25c6 100644 --- a/src/service.vala +++ b/src/service.vala @@ -195,7 +195,7 @@ 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 (); } -- cgit v1.2.3 From 6f243f49bb29f00211c892eee5e696d6ef50db2d Mon Sep 17 00:00:00 2001 From: Charles Kerr Date: Thu, 24 Oct 2013 19:22:52 -0500 Subject: fix similar variant leaks --- src/service.vala | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) (limited to 'src') diff --git a/src/service.vala b/src/service.vala index c9c25c6..aa992ff 100644 --- a/src/service.vala +++ b/src/service.vala @@ -175,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) => { @@ -186,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 (); }); @@ -201,7 +201,7 @@ public class IndicatorSound.Service { } Action create_volume_action () { - var volume_action = new SimpleAction.stateful ("volume", VariantType.INT32, 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 ()); @@ -221,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); -- cgit v1.2.3 From d1c735417327b2914536eecdce06365076e12fe7 Mon Sep 17 00:00:00 2001 From: Charles Kerr Date: Fri, 25 Oct 2013 11:08:20 -0500 Subject: test that player.icon isn't null before we serialize it. --- src/sound-menu.vala | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'src') diff --git a/src/sound-menu.vala b/src/sound-menu.vala index e1c5c1f..af808b5 100644 --- a/src/sound-menu.vala +++ b/src/sound-menu.vala @@ -144,7 +144,8 @@ class SoundMenu: Object 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 (player.icon != null) + player_item.set_attribute_value ("icon", g_icon_serialize (player.icon)); section.append_item (player_item); var playback_item = new MenuItem (null, null); -- cgit v1.2.3 From f20f87c277dc55a99cace2a3e80797d796265f51 Mon Sep 17 00:00:00 2001 From: Lars Uebernickel Date: Sat, 26 Oct 2013 20:40:57 -0400 Subject: VolumeControl: don't pass vala closures into libpulse Libpulse's vala bindings don't specify that callbacks passed into some of its functions (e.g., get_server_info) can be called after the function returns. Vala thus frees closure data after these functions return. This can't easily be fixed in the bindings, because libpulse doesn't provide variants of these functions with destroy_notifies. This patch works around this problem by only passing non-closure functions into libpulse. --- src/volume-control.vala | 44 +++++++++++++++++++++++++++++--------------- 1 file changed, 29 insertions(+), 15 deletions(-) (limited to 'src') diff --git a/src/volume-control.vala b/src/volume-control.vala index 18c407f..e50e8e6 100644 --- a/src/volume-control.vala +++ b/src/volume-control.vala @@ -119,12 +119,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) @@ -182,15 +184,25 @@ public class VolumeControl : Object 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 */ public void set_mute (bool mute) { 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 () @@ -260,19 +272,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 () -- cgit v1.2.3 From 05d587408fa3b64ff569329d334c82c46eb64829 Mon Sep 17 00:00:00 2001 From: Charles Kerr Date: Mon, 28 Oct 2013 11:43:22 -0700 Subject: use 'application-default-icon' as the fallback media player icon --- src/sound-menu.vala | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) (limited to 'src') diff --git a/src/sound-menu.vala b/src/sound-menu.vala index af808b5..4c62b38 100644 --- a/src/sound-menu.vala +++ b/src/sound-menu.vala @@ -141,11 +141,15 @@ class SoundMenu: Object 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 (player.icon != null) - player_item.set_attribute_value ("icon", g_icon_serialize (player.icon)); + player_item.set_attribute_value ("icon", g_icon_serialize (player.icon)); section.append_item (player_item); var playback_item = new MenuItem (null, null); -- cgit v1.2.3 From 9ebfd4a45a13d55f8f926f3a97a6479cd8e4671c Mon Sep 17 00:00:00 2001 From: Charles Kerr Date: Mon, 28 Oct 2013 12:08:05 -0700 Subject: add the null safeguard back for the pathological case of 'application-default-icon' also failing --- src/sound-menu.vala | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'src') diff --git a/src/sound-menu.vala b/src/sound-menu.vala index 4c62b38..f3f4fd9 100644 --- a/src/sound-menu.vala +++ b/src/sound-menu.vala @@ -149,7 +149,8 @@ class SoundMenu: Object 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 (icon != null) + player_item.set_attribute_value ("icon", g_icon_serialize (icon)); section.append_item (player_item); var playback_item = new MenuItem (null, null); -- cgit v1.2.3