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
This commit is contained in:
parent
a1389a0730
commit
0547a582d1
@ -75,7 +75,8 @@ PKG_CHECK_MODULES(MUTTER_PLUGIN, gio-2.0 >= $GIO_MIN_VERSION
|
|||||||
clutter-x11-1.0 >= $CLUTTER_MIN_VERSION
|
clutter-x11-1.0 >= $CLUTTER_MIN_VERSION
|
||||||
clutter-glx-1.0 >= $CLUTTER_MIN_VERSION
|
clutter-glx-1.0 >= $CLUTTER_MIN_VERSION
|
||||||
libstartup-notification-1.0
|
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_CFLAGS=$CFLAGS
|
||||||
saved_LIBS=$LIBS
|
saved_LIBS=$LIBS
|
||||||
|
@ -43,6 +43,7 @@ nobase_dist_js_DATA = \
|
|||||||
ui/statusIconDispatcher.js \
|
ui/statusIconDispatcher.js \
|
||||||
ui/statusMenu.js \
|
ui/statusMenu.js \
|
||||||
ui/status/accessibility.js \
|
ui/status/accessibility.js \
|
||||||
|
ui/status/volume.js \
|
||||||
ui/telepathyClient.js \
|
ui/telepathyClient.js \
|
||||||
ui/tweener.js \
|
ui/tweener.js \
|
||||||
ui/windowAttentionHandler.js \
|
ui/windowAttentionHandler.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_ORDER = ['a11y', 'display', 'keyboard', 'volume', 'bluetooth', 'network', 'battery'];
|
||||||
const STANDARD_TRAY_ICON_SHELL_IMPLEMENTATION = {
|
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';
|
const CLOCK_FORMAT_KEY = 'format';
|
||||||
|
@ -176,7 +176,7 @@ PopupSliderMenuItem.prototype = {
|
|||||||
if (isNaN(value))
|
if (isNaN(value))
|
||||||
// Avoid spreading NaNs around
|
// Avoid spreading NaNs around
|
||||||
throw TypeError('The slider value must be a number');
|
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._slider = new St.DrawingArea({ style_class: 'popup-slider-menu-item', reactive: true });
|
||||||
this.actor.set_child(this._slider);
|
this.actor.set_child(this._slider);
|
||||||
@ -191,7 +191,7 @@ PopupSliderMenuItem.prototype = {
|
|||||||
if (isNaN(value))
|
if (isNaN(value))
|
||||||
throw TypeError('The slider value must be a number');
|
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();
|
this._slider.queue_repaint();
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -231,7 +231,7 @@ PopupSliderMenuItem.prototype = {
|
|||||||
cr.stroke();
|
cr.stroke();
|
||||||
|
|
||||||
let handleY = height / 2;
|
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();
|
let color = new Clutter.Color();
|
||||||
themeNode.get_foreground_color(color);
|
themeNode.get_foreground_color(color);
|
||||||
@ -269,8 +269,7 @@ PopupSliderMenuItem.prototype = {
|
|||||||
Clutter.ungrab_pointer();
|
Clutter.ungrab_pointer();
|
||||||
this._dragging = false;
|
this._dragging = false;
|
||||||
|
|
||||||
this._value = this._displayValue;
|
this.emit('drag-end');
|
||||||
this.emit('value-changed', this._value);
|
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
@ -299,8 +298,9 @@ PopupSliderMenuItem.prototype = {
|
|||||||
newvalue = 1;
|
newvalue = 1;
|
||||||
else
|
else
|
||||||
newvalue = (relX - handleRadius) / (width - 2 * handleRadius);
|
newvalue = (relX - handleRadius) / (width - 2 * handleRadius);
|
||||||
this._displayValue = newvalue;
|
this._value = newvalue;
|
||||||
this._slider.queue_repaint();
|
this._slider.queue_repaint();
|
||||||
|
this.emit('value-changed', this._value);
|
||||||
},
|
},
|
||||||
|
|
||||||
get value() {
|
get value() {
|
||||||
@ -311,9 +311,10 @@ PopupSliderMenuItem.prototype = {
|
|||||||
let key = event.get_key_symbol();
|
let key = event.get_key_symbol();
|
||||||
if (key == Clutter.Right || key == Clutter.Left) {
|
if (key == Clutter.Right || key == Clutter.Left) {
|
||||||
let delta = key == Clutter.Right ? 0.1 : -0.1;
|
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._slider.queue_repaint();
|
||||||
this.emit('value-changed', this._value);
|
this.emit('value-changed', this._value);
|
||||||
|
this.emit('drag-end');
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
|
206
js/ui/status/volume.js
Normal file
206
js/ui/status/volume.js
Normal file
@ -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;
|
||||||
|
}
|
||||||
|
};
|
@ -24,6 +24,7 @@
|
|||||||
#include <math.h>
|
#include <math.h>
|
||||||
#include <X11/extensions/Xfixes.h>
|
#include <X11/extensions/Xfixes.h>
|
||||||
#include <gjs/gjs.h>
|
#include <gjs/gjs.h>
|
||||||
|
#include <canberra.h>
|
||||||
#ifdef HAVE_SYS_RESOURCE_H
|
#ifdef HAVE_SYS_RESOURCE_H
|
||||||
#include <sys/resource.h>
|
#include <sys/resource.h>
|
||||||
#endif
|
#endif
|
||||||
@ -67,6 +68,9 @@ struct _ShellGlobal {
|
|||||||
guint work_count;
|
guint work_count;
|
||||||
GSList *leisure_closures;
|
GSList *leisure_closures;
|
||||||
guint leisure_function_id;
|
guint leisure_function_id;
|
||||||
|
|
||||||
|
/* For sound notifications */
|
||||||
|
ca_context *sound_context;
|
||||||
};
|
};
|
||||||
|
|
||||||
enum {
|
enum {
|
||||||
@ -214,6 +218,10 @@ shell_global_init (ShellGlobal *global)
|
|||||||
|
|
||||||
global->last_change_screen_width = 0;
|
global->last_change_screen_width = 0;
|
||||||
global->last_change_screen_height = 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
|
static void
|
||||||
@ -1788,3 +1796,18 @@ shell_global_run_at_leisure (ShellGlobal *global,
|
|||||||
if (global->work_count == 0)
|
if (global->work_count == 0)
|
||||||
schedule_leisure_functions (global);
|
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);
|
||||||
|
}
|
||||||
|
@ -130,6 +130,9 @@ void shell_global_run_at_leisure (ShellGlobal *global,
|
|||||||
gpointer user_data,
|
gpointer user_data,
|
||||||
GDestroyNotify notify);
|
GDestroyNotify notify);
|
||||||
|
|
||||||
|
void shell_global_play_theme_sound (ShellGlobal *global,
|
||||||
|
const char *name);
|
||||||
|
|
||||||
G_END_DECLS
|
G_END_DECLS
|
||||||
|
|
||||||
#endif /* __SHELL_GLOBAL_H__ */
|
#endif /* __SHELL_GLOBAL_H__ */
|
||||||
|
Loading…
Reference in New Issue
Block a user