From 0547a582d1a6f0a2dc8a4014632daf5407f8c67a Mon Sep 17 00:00:00 2001 From: Giovanni Campagna Date: Fri, 23 Jul 2010 02:39:44 +0200 Subject: [PATCH] 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(-) create mode 100644 js/ui/status/volume.js diff --git a/configure.ac b/configure.ac index ad6a1f84b..a9533aa4d 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 f35026e81..eca172601 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 26c3a0330..d683a1813 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 889f29622..8694a601c 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 000000000..92f17def1 --- /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 5251380d1..1e42fc9f0 100644 --- a/src/shell-global.c +++ b/src/shell-global.c @@ -24,6 +24,7 @@ #include #include #include +#include #ifdef HAVE_SYS_RESOURCE_H #include #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 34c56786b..5cf5e1373 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__ */