[gnome-shell] Add volume indicator



commit 0547a582d1a6f0a2dc8a4014632daf5407f8c67a
Author: Giovanni Campagna <scampa giovanni gmail com>
Date:   Fri Jul 23 02:39:44 2010 +0200

    Add volume indicator
    
    Add volume control indicator which uses API from gnome-volume-control
    to interact with PulseAudio and shows both input and output volumes.
    Also adds a small wrapper around libcanberra in ShellGlobal, used by the
    volume indicator to provide auditive feedback.
    
    https://bugzilla.gnome.org/show_bug.cgi?id=629455

 configure.ac           |    3 +-
 js/Makefile.am         |    1 +
 js/ui/panel.js         |    3 +-
 js/ui/popupMenu.js     |   15 ++--
 js/ui/status/volume.js |  206 ++++++++++++++++++++++++++++++++++++++++++++++++
 src/shell-global.c     |   23 ++++++
 src/shell-global.h     |    3 +
 7 files changed, 245 insertions(+), 9 deletions(-)
---
diff --git a/configure.ac b/configure.ac
index ad6a1f8..a9533aa 100644
--- a/configure.ac
+++ b/configure.ac
@@ -75,7 +75,8 @@ PKG_CHECK_MODULES(MUTTER_PLUGIN, gio-2.0 >= $GIO_MIN_VERSION
 				 clutter-x11-1.0 >= $CLUTTER_MIN_VERSION
 				 clutter-glx-1.0 >= $CLUTTER_MIN_VERSION
                                  libstartup-notification-1.0
-                                 gobject-introspection-1.0 >= $GOBJECT_INTROSPECTION_MIN_VERSION)
+                                 gobject-introspection-1.0 >= $GOBJECT_INTROSPECTION_MIN_VERSION
+				 libcanberra)
 
 saved_CFLAGS=$CFLAGS
 saved_LIBS=$LIBS
diff --git a/js/Makefile.am b/js/Makefile.am
index f35026e..eca1726 100644
--- a/js/Makefile.am
+++ b/js/Makefile.am
@@ -43,6 +43,7 @@ nobase_dist_js_DATA = 	\
 	ui/statusIconDispatcher.js	\
 	ui/statusMenu.js	\
 	ui/status/accessibility.js	\
+	ui/status/volume.js	\
 	ui/telepathyClient.js	\
 	ui/tweener.js		\
 	ui/windowAttentionHandler.js	\
diff --git a/js/ui/panel.js b/js/ui/panel.js
index 26c3a03..d683a18 100644
--- a/js/ui/panel.js
+++ b/js/ui/panel.js
@@ -31,7 +31,8 @@ const SPINNER_SPEED = 0.02;
 
 const STANDARD_TRAY_ICON_ORDER = ['a11y', 'display', 'keyboard', 'volume', 'bluetooth', 'network', 'battery'];
 const STANDARD_TRAY_ICON_SHELL_IMPLEMENTATION = {
-    'a11y': imports.ui.status.accessibility.ATIndicator
+    'a11y': imports.ui.status.accessibility.ATIndicator,
+    'volume': imports.ui.status.volume.Indicator,
 };
 
 const CLOCK_FORMAT_KEY        = 'format';
diff --git a/js/ui/popupMenu.js b/js/ui/popupMenu.js
index 889f296..8694a60 100644
--- a/js/ui/popupMenu.js
+++ b/js/ui/popupMenu.js
@@ -176,7 +176,7 @@ PopupSliderMenuItem.prototype = {
         if (isNaN(value))
             // Avoid spreading NaNs around
             throw TypeError('The slider value must be a number');
-        this._displayValue = this._value = Math.max(Math.min(value, 1), 0);
+        this._value = Math.max(Math.min(value, 1), 0);
 
         this._slider = new St.DrawingArea({ style_class: 'popup-slider-menu-item', reactive: true });
         this.actor.set_child(this._slider);
@@ -191,7 +191,7 @@ PopupSliderMenuItem.prototype = {
         if (isNaN(value))
             throw TypeError('The slider value must be a number');
 
-        this._displayValue = this._value = Math.max(Math.min(value, 1), 0);
+        this._value = Math.max(Math.min(value, 1), 0);
         this._slider.queue_repaint();
     },
 
@@ -231,7 +231,7 @@ PopupSliderMenuItem.prototype = {
         cr.stroke();
 
         let handleY = height / 2;
-        let handleX = handleRadius + (width - 2 * handleRadius) * this._displayValue;
+        let handleX = handleRadius + (width - 2 * handleRadius) * this._value;
 
         let color = new Clutter.Color();
         themeNode.get_foreground_color(color);
@@ -269,8 +269,7 @@ PopupSliderMenuItem.prototype = {
             Clutter.ungrab_pointer();
             this._dragging = false;
 
-            this._value = this._displayValue;
-            this.emit('value-changed', this._value);
+            this.emit('drag-end');
         }
         return true;
     },
@@ -299,8 +298,9 @@ PopupSliderMenuItem.prototype = {
             newvalue = 1;
         else
             newvalue = (relX - handleRadius) / (width - 2 * handleRadius);
-        this._displayValue = newvalue;
+        this._value = newvalue;
         this._slider.queue_repaint();
+        this.emit('value-changed', this._value);
     },
 
     get value() {
@@ -311,9 +311,10 @@ PopupSliderMenuItem.prototype = {
         let key = event.get_key_symbol();
         if (key == Clutter.Right || key == Clutter.Left) {
             let delta = key == Clutter.Right ? 0.1 : -0.1;
-            this._value = this._displayValue = Math.max(0, Math.min(this._value + delta, 1));
+            this._value = Math.max(0, Math.min(this._value + delta, 1));
             this._slider.queue_repaint();
             this.emit('value-changed', this._value);
+            this.emit('drag-end');
             return true;
         }
         return false;
diff --git a/js/ui/status/volume.js b/js/ui/status/volume.js
new file mode 100644
index 0000000..92f17de
--- /dev/null
+++ b/js/ui/status/volume.js
@@ -0,0 +1,206 @@
+/* -*- mode: js2; js2-basic-offset: 4; indent-tabs-mode: nil -*- */
+
+const DBus = imports.dbus;
+const Lang = imports.lang;
+const Mainloop = imports.mainloop;
+const Shell = imports.gi.Shell;
+const Gvc = imports.gi.Gvc;
+const Signals = imports.signals;
+const St = imports.gi.St;
+
+const PanelMenu = imports.ui.panelMenu;
+const PopupMenu = imports.ui.popupMenu;
+
+const Gettext = imports.gettext.domain('gnome-shell');
+const _ = Gettext.gettext;
+
+const VOLUME_MAX = 65536.0; /* PA_VOLUME_NORM */
+
+function Indicator() {
+    this._init.apply(this, arguments);
+}
+
+Indicator.prototype = {
+    __proto__: PanelMenu.SystemStatusButton.prototype,
+
+    _init: function() {
+        PanelMenu.SystemStatusButton.prototype._init.call(this, 'audio-volume-muted', null);
+
+        this._control = new Gvc.MixerControl({ name: 'GNOME Shell Volume Control' });
+        this._control.connect('ready', Lang.bind(this, this._onControlReady));
+        this._control.connect('default-sink-changed', Lang.bind(this, this._readOutput));
+        this._control.connect('default-source-changed', Lang.bind(this, this._readInput));
+        this._control.connect('stream-added', Lang.bind(this, this._maybeShowInput));
+        this._control.connect('stream-removed', Lang.bind(this, this._maybeShowInput));
+
+        this._output = null;
+        this._outputVolumeId = 0;
+        this._outputMutedId = 0;
+        this._outputSwitch = new PopupMenu.PopupSwitchMenuItem(_("Output: Muted"), false);
+        this._outputSwitch.connect('toggled', Lang.bind(this, this._switchToggled, '_output'));
+        this._outputSlider = new PopupMenu.PopupSliderMenuItem(0);
+        this._outputSlider.connect('value-changed', Lang.bind(this, this._sliderChanged, '_output'));
+        this._outputSlider.connect('drag-end', Lang.bind(this, this._notifyVolumeChange));
+        this.menu.addMenuItem(this._outputSwitch);
+        this.menu.addMenuItem(this._outputSlider);
+
+        this._separator = new PopupMenu.PopupSeparatorMenuItem();
+        this.menu.addMenuItem(this._separator);
+
+        this._input = null;
+        this._inputVolumeId = 0;
+        this._inputMutedId = 0;
+        this._inputSwitch = new PopupMenu.PopupSwitchMenuItem(_("Input: Muted"), false);
+        this._inputSwitch.connect('toggled', Lang.bind(this, this._switchToggled, '_input'));
+        this._inputSlider = new PopupMenu.PopupSliderMenuItem(0);
+        this._inputSlider.connect('value-changed', Lang.bind(this, this._sliderChanged, '_input'));
+        this._inputSlider.connect('drag-end', Lang.bind(this, this._notifyVolumeChange));
+        this.menu.addMenuItem(this._inputSwitch);
+        this.menu.addMenuItem(this._inputSlider);
+
+        this.menu.addMenuItem(new PopupMenu.PopupSeparatorMenuItem());
+        this.menu.addAction(_("Sound Preferences"), function() {
+            let p = new Shell.Process({ args: ['gnome-control-center', 'volume'] });
+            p.run();
+        });
+
+        this._control.open();
+    },
+
+    _onControlReady: function() {
+        this._readOutput();
+        this._readInput();
+    },
+
+    _readOutput: function() {
+        if (this._outputVolumeId) {
+            this._output.disconnect(this._outputVolumeId);
+            this._output.disconnect(this._outputMutedId);
+            this._outputVolumeId = 0;
+            this._outputMutedId = 0;
+        }
+        this._output = this._control.get_default_sink();
+        if (this._output) {
+            this._outputMutedId = this._output.connect('notify::is-muted', Lang.bind(this, this._mutedChanged, '_output'));
+            this._outputVolumeId = this._output.connect('notify::volume', Lang.bind(this, this._volumeChanged, '_output'));
+            this._mutedChanged (null, null, '_output');
+            this._volumeChanged (null, null, '_output');
+            this.setIcon(this._volumeToIcon(this._output.volume));
+        } else {
+            this._outputSwitch.label.text = _("Output: Muted");
+            this._outputSwitch.setToggleState(false);
+            this.setIcon('audio-volume-muted-symbolic');
+        }
+    },
+
+    _readInput: function() {
+        if (this._inputVolumeId) {
+            this._input.disconnect(this._inputVolumeId);
+            this._input.disconnect(this._inputMutedId);
+            this._inputVolumeId = 0;
+            this._inputMutedId = 0;
+        }
+        this._input = this._control.get_default_source();
+        if (this._input) {
+            this._inputMutedId = this._input.connect('notify::is-muted', Lang.bind(this, this._mutedChanged, '_input'));
+            this._inputVolumeId = this._input.connect('notify::volume', Lang.bind(this, this._volumeChanged, '_input'));
+            this._mutedChanged (null, null, '_input');
+            this._volumeChanged (null, null, '_input');
+        } else {
+            this._separator.actor.hide();
+            this._inputSwitch.actor.hide();
+            this._inputSlider.actor.hide();
+        }
+    },
+
+    _maybeShowInput: function() {
+        // only show input widgets if any application is recording audio
+        let showInput = false;
+        let recordingApps = this._control.get_source_outputs();
+        if (this._source && recordingApps) {
+            for (let i = 0; i < recordingApp.length; i++) {
+                let outputStream = recordingApp[i];
+                let id = outputStream.get_application_id();
+                // but skip gnome-volume-control and pavucontrol
+                // (that appear as recording because they show the input level)
+                if (!id || (id != 'org.gnome.VolumeControl' && id != 'org.PulseAudio.pavucontrol')) {
+                    showInput = true;
+                    break;
+                }
+            }
+        }
+        if (showInput) {
+            this._separator.actor.show();
+            this._inputSwitch.actor.show();
+            this._inputSlider.actor.show();
+        } else {
+            this._separator.actor.hide();
+            this._inputSwitch.actor.hide();
+            this._inputSlider.actor.hide();
+        }
+    },
+
+    _volumeToIcon: function(volume) {
+        if (volume <= 0) {
+            return 'audio-volume-muted';
+        } else {
+            let v = volume / VOLUME_MAX;
+            if (v < 0.33)
+                return 'audio-volume-low';
+            if (v > 0.8)
+                return 'audio-volume-high';
+            return 'audio-volume-medium';
+        }
+    },
+
+    _sliderChanged: function(slider, value, property) {
+        if (this[property] == null) {
+            log ('Volume slider changed for %s, but %s does not exist'.format(property, property));
+            return;
+        }
+        this[property].volume = value * VOLUME_MAX;
+        this[property].push_volume();
+    },
+
+    _notifyVolumeChange: function() {
+        global.play_theme_sound('audio-volume-change');
+    },
+
+    _switchToggled: function(switchItem, state, property) {
+        if (this[property] == null) {
+            log ('Volume mute switch toggled for %s, but %s does not exist'.format(property, property));
+            return;
+        }
+        this[property].change_is_muted(!state);
+        this._notifyVolumeChange();
+    },
+
+    _mutedChanged: function(object, param_spec, property) {
+        let muted = this[property].is_muted;
+        let toggleSwitch = this[property+'Switch'];
+        toggleSwitch.setToggleState(!muted);
+        this._updateLabel(property);
+        if (property == '_output') {
+            if (muted)
+                this.setIcon('audio-volume-muted');
+            else
+                this.setIcon(this._volumeToIcon(this._output.volume));
+        }
+    },
+
+    _volumeChanged: function(object, param_spec, property) {
+        this[property+'Slider'].setValue(this[property].volume / VOLUME_MAX);
+        this._updateLabel(property);
+        if (property == '_output')
+            this.setIcon(this._volumeToIcon(this._output.volume));
+    },
+
+    _updateLabel: function(property) {
+        let label;
+        if (this[property].is_muted)
+            label = (property == '_output' ? _("Output: Muted") : _("Input: Muted"));
+        else
+            label = (property == '_output' ? _("Output: %3.0f%%") : _("Input: %3.0f%%")).format(this[property].volume / VOLUME_MAX * 100);
+        this[property+'Switch'].label.text = label;
+    }
+};
diff --git a/src/shell-global.c b/src/shell-global.c
index 5251380..1e42fc9 100644
--- a/src/shell-global.c
+++ b/src/shell-global.c
@@ -24,6 +24,7 @@
 #include <math.h>
 #include <X11/extensions/Xfixes.h>
 #include <gjs/gjs.h>
+#include <canberra.h>
 #ifdef HAVE_SYS_RESOURCE_H
 #include <sys/resource.h>
 #endif
@@ -67,6 +68,9 @@ struct _ShellGlobal {
   guint work_count;
   GSList *leisure_closures;
   guint leisure_function_id;
+
+  /* For sound notifications */
+  ca_context *sound_context;
 };
 
 enum {
@@ -214,6 +218,10 @@ shell_global_init (ShellGlobal *global)
 
   global->last_change_screen_width = 0;
   global->last_change_screen_height = 0;
+
+  ca_context_create (&global->sound_context);
+  ca_context_change_props (global->sound_context, CA_PROP_APPLICATION_NAME, PACKAGE_NAME, CA_PROP_APPLICATION_ID, "org.gnome.Shell", NULL);
+  ca_context_open (global->sound_context);
 }
 
 static void
@@ -1788,3 +1796,18 @@ shell_global_run_at_leisure (ShellGlobal         *global,
   if (global->work_count == 0)
     schedule_leisure_functions (global);
 }
+
+/**
+ * shell_global_play_theme_sound:
+ * @global: the #ShellGlobal
+ * @name: the sound name
+ *
+ * Plays a simple sound picked according to Freedesktop sound theme.
+ * Really just a workaround for libcanberra not being introspected.
+ */
+void
+shell_global_play_theme_sound (ShellGlobal *global,
+                               const char  *name)
+{
+  ca_context_play (global->sound_context, 0, CA_PROP_EVENT_ID, name, NULL);
+}
diff --git a/src/shell-global.h b/src/shell-global.h
index 34c5678..5cf5e13 100644
--- a/src/shell-global.h
+++ b/src/shell-global.h
@@ -130,6 +130,9 @@ void shell_global_run_at_leisure (ShellGlobal         *global,
                                   gpointer             user_data,
                                   GDestroyNotify       notify);
 
+void shell_global_play_theme_sound (ShellGlobal       *global,
+                                    const char        *name);
+
 G_END_DECLS
 
 #endif /* __SHELL_GLOBAL_H__ */



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