25
27
return _mixerControl;
30
const StreamSlider = new Lang.Class({
33
_init: function(control, title) {
34
this._control = control;
36
this.item = new PopupMenu.PopupMenuSection();
38
this._title = new PopupMenu.PopupMenuItem(title, { reactive: false });
39
this._slider = new PopupMenu.PopupSliderMenuItem(0);
40
this._slider.connect('value-changed', Lang.bind(this, this._sliderChanged));
41
this._slider.connect('drag-end', Lang.bind(this, this._notifyVolumeChange));
43
this.item.addMenuItem(this._title);
44
this.item.addMenuItem(this._slider);
47
this._shouldShow = true;
56
this._disconnectStream(this._stream);
59
this._stream = stream;
62
this._connectStream(this._stream);
65
this.emit('stream-updated');
68
this._updateVisibility();
71
_disconnectStream: function(stream) {
72
stream.disconnect(this._mutedChangedId);
73
this._mutedChangedId = 0;
74
stream.disconnect(this._volumeChangedId);
75
this._volumeChangedId = 0;
78
_connectStream: function(stream) {
79
this._mutedChangedId = stream.connect('notify::is-muted', Lang.bind(this, this._updateVolume));
80
this._volumeChangedId = stream.connect('notify::volume', Lang.bind(this, this._updateVolume));
83
_shouldBeVisible: function() {
84
return this._stream != null;
87
_updateVisibility: function() {
88
let visible = this._shouldBeVisible();
89
this._title.actor.visible = visible;
90
this._slider.actor.visible = visible;
93
scroll: function(event) {
94
this._slider.scroll(event);
97
setValue: function(value) {
98
// piggy-back off of sliderChanged
99
this._slider.setValue(value);
102
_sliderChanged: function(slider, value, property) {
106
let volume = value * this._control.get_vol_max_norm();
107
let prevMuted = this._stream.is_muted;
109
this._stream.volume = 0;
111
this._stream.change_is_muted(true);
113
this._stream.volume = volume;
115
this._stream.change_is_muted(false);
117
this._stream.push_volume();
120
_notifyVolumeChange: function() {
121
global.cancel_theme_sound(VOLUME_NOTIFY_ID);
122
global.play_theme_sound(VOLUME_NOTIFY_ID,
123
'audio-volume-change',
125
Clutter.get_current_event ());
128
_updateVolume: function() {
129
let muted = this._stream.is_muted;
130
this._slider.setValue(muted ? 0 : (this._stream.volume / this._control.get_vol_max_norm()));
131
this.emit('stream-updated');
134
getIcon: function() {
138
let volume = this._stream.volume;
139
if (this._stream.is_muted || volume <= 0) {
140
return 'audio-volume-muted-symbolic';
142
let n = Math.floor(3 * volume / this._control.get_vol_max_norm()) + 1;
144
return 'audio-volume-low-symbolic';
146
return 'audio-volume-high-symbolic';
147
return 'audio-volume-medium-symbolic';
151
Signals.addSignalMethods(StreamSlider.prototype);
153
const OutputStreamSlider = new Lang.Class({
154
Name: 'OutputStreamSlider',
155
Extends: StreamSlider,
157
_connectStream: function(stream) {
159
this._portChangedId = stream.connect('notify::port', Lang.bind(this, this._portChanged));
163
_findHeadphones: function(sink) {
164
// This only works for external headphones (e.g. bluetooth)
165
if (sink.get_form_factor() == 'headset' ||
166
sink.get_form_factor() == 'headphone')
169
// a bit hackish, but ALSA/PulseAudio have a number
170
// of different identifiers for headphones, and I could
171
// not find the complete list
172
if (sink.get_ports().length > 0)
173
return sink.get_port().port.indexOf('headphone') >= 0;
178
_disconnectStream: function(stream) {
180
stream.disconnect(this._portChangedId);
181
this._portChangedId = 0;
184
_portChanged: function() {
185
let hasHeadphones = this._findHeadphones(this._stream);
186
if (hasHeadphones != this._hasHeadphones) {
187
this._hasHeadphones = hasHeadphones;
188
this.emit('headphones-changed', this._hasHeadphones);
193
const InputStreamSlider = new Lang.Class({
194
Name: 'InputStreamSlider',
195
Extends: StreamSlider,
197
_init: function(control, title) {
198
this.parent(control, title);
199
this._control.connect('stream-added', Lang.bind(this, this._maybeShowInput));
200
this._control.connect('stream-removed', Lang.bind(this, this._maybeShowInput));
203
_connectStream: function(stream) {
205
this._maybeShowInput();
208
_maybeShowInput: function() {
209
// only show input widgets if any application is recording audio
210
let showInput = false;
211
let recordingApps = this._control.get_source_outputs();
212
if (this._stream && recordingApps) {
213
for (let i = 0; i < recordingApps.length; i++) {
214
let outputStream = recordingApps[i];
215
let id = outputStream.get_application_id();
216
// but skip gnome-volume-control and pavucontrol
217
// (that appear as recording because they show the input level)
218
if (!id || (id != 'org.gnome.VolumeControl' && id != 'org.PulseAudio.pavucontrol')) {
225
this._showInput = showInput;
226
this._updateVisibility();
229
_shouldBeVisible: function() {
230
return this.parent() && this._showInput;
28
234
const VolumeMenu = new Lang.Class({
29
235
Name: 'VolumeMenu',
30
236
Extends: PopupMenu.PopupMenuSection,
32
238
_init: function(control) {
241
this.hasHeadphones = false;
35
243
this._control = control;
36
244
this._control.connect('state-changed', Lang.bind(this, this._onControlStateChanged));
37
245
this._control.connect('default-sink-changed', Lang.bind(this, this._readOutput));
38
246
this._control.connect('default-source-changed', Lang.bind(this, this._readInput));
39
this._control.connect('stream-added', Lang.bind(this, this._maybeShowInput));
40
this._control.connect('stream-removed', Lang.bind(this, this._maybeShowInput));
41
this._volumeMax = this._control.get_vol_max_norm();
44
this._outputVolumeId = 0;
45
this._outputMutedId = 0;
46
248
/* Translators: This is the label for audio volume */
47
this._outputTitle = new PopupMenu.PopupMenuItem(_("Volume"), { reactive: false });
48
this._outputSlider = new PopupMenu.PopupSliderMenuItem(0);
49
this._outputSlider.connect('value-changed', Lang.bind(this, this._sliderChanged, '_output'));
50
this._outputSlider.connect('drag-end', Lang.bind(this, this._notifyVolumeChange));
51
this.addMenuItem(this._outputTitle);
52
this.addMenuItem(this._outputSlider);
249
this._output = new OutputStreamSlider(this._control, _("Volume"));
250
this._output.connect('stream-updated', Lang.bind(this, function() {
251
this.emit('icon-changed');
253
this._output.connect('headphones-changed', Lang.bind(this, function(stream, value) {
254
this.emit('headphones-changed', value);
256
this.addMenuItem(this._output.item);
258
this._input = new InputStreamSlider(this._control, _("Microphone"));
259
this.addMenuItem(this._input.item);
54
261
this.addMenuItem(new PopupMenu.PopupSeparatorMenuItem());
57
this._inputVolumeId = 0;
58
this._inputMutedId = 0;
59
this._inputTitle = new PopupMenu.PopupMenuItem(_("Microphone"), { reactive: false });
60
this._inputSlider = new PopupMenu.PopupSliderMenuItem(0);
61
this._inputSlider.connect('value-changed', Lang.bind(this, this._sliderChanged, '_input'));
62
this._inputSlider.connect('drag-end', Lang.bind(this, this._notifyVolumeChange));
63
this.addMenuItem(this._inputTitle);
64
this.addMenuItem(this._inputSlider);
66
263
this._onControlStateChanged();
69
scroll: function(direction) {
70
let currentVolume = this._output.volume;
72
if (direction == Clutter.ScrollDirection.DOWN) {
73
let prev_muted = this._output.is_muted;
74
this._output.volume = Math.max(0, currentVolume - this._volumeMax * VOLUME_ADJUSTMENT_STEP);
75
if (this._output.volume < 1) {
76
this._output.volume = 0;
78
this._output.change_is_muted(true);
80
this._output.push_volume();
82
else if (direction == Clutter.ScrollDirection.UP) {
83
this._output.volume = Math.min(this._volumeMax, currentVolume + this._volumeMax * VOLUME_ADJUSTMENT_STEP);
84
this._output.change_is_muted(false);
85
this._output.push_volume();
88
this._notifyVolumeChange();
266
scroll: function(event) {
267
this._output.scroll(event);
91
270
_onControlStateChanged: function() {
92
271
if (this._control.get_state() == Gvc.MixerControlState.READY) {
93
273
this._readOutput();
95
this._maybeShowInput();
97
this.emit('icon-changed', null);
275
this.emit('icon-changed');
101
279
_readOutput: function() {
102
if (this._outputVolumeId) {
103
this._output.disconnect(this._outputVolumeId);
104
this._output.disconnect(this._outputMutedId);
105
this._outputVolumeId = 0;
106
this._outputMutedId = 0;
108
this._output = this._control.get_default_sink();
110
this._outputMutedId = this._output.connect('notify::is-muted', Lang.bind(this, this._mutedChanged, '_output'));
111
this._outputVolumeId = this._output.connect('notify::volume', Lang.bind(this, this._volumeChanged, '_output'));
112
this._mutedChanged (null, null, '_output');
113
this._volumeChanged (null, null, '_output');
115
this._outputSlider.setValue(0);
116
this.emit('icon-changed', 'audio-volume-muted-symbolic');
280
this._output.stream = this._control.get_default_sink();
120
283
_readInput: function() {
121
if (this._inputVolumeId) {
122
this._input.disconnect(this._inputVolumeId);
123
this._input.disconnect(this._inputMutedId);
124
this._inputVolumeId = 0;
125
this._inputMutedId = 0;
127
this._input = this._control.get_default_source();
129
this._inputMutedId = this._input.connect('notify::is-muted', Lang.bind(this, this._mutedChanged, '_input'));
130
this._inputVolumeId = this._input.connect('notify::volume', Lang.bind(this, this._volumeChanged, '_input'));
131
this._mutedChanged (null, null, '_input');
132
this._volumeChanged (null, null, '_input');
134
this._inputTitle.actor.hide();
135
this._inputSlider.actor.hide();
139
_maybeShowInput: function() {
140
// only show input widgets if any application is recording audio
141
let showInput = false;
142
let recordingApps = this._control.get_source_outputs();
143
if (this._input && recordingApps) {
144
for (let i = 0; i < recordingApps.length; i++) {
145
let outputStream = recordingApps[i];
146
let id = outputStream.get_application_id();
147
// but skip gnome-volume-control and pavucontrol
148
// (that appear as recording because they show the input level)
149
if (!id || (id != 'org.gnome.VolumeControl' && id != 'org.PulseAudio.pavucontrol')) {
156
this._inputTitle.actor.visible = showInput;
157
this._inputSlider.actor.visible = showInput;
160
_volumeToIcon: function(volume) {
162
return 'audio-volume-muted-symbolic';
164
let n = Math.floor(3 * volume / this._volumeMax) + 1;
166
return 'audio-volume-low-symbolic';
168
return 'audio-volume-high-symbolic';
169
return 'audio-volume-medium-symbolic';
173
_sliderChanged: function(slider, value, property) {
174
if (this[property] == null) {
175
log ('Volume slider changed for %s, but %s does not exist'.format(property, property));
178
let volume = value * this._volumeMax;
179
let prev_muted = this[property].is_muted;
181
this[property].volume = 0;
183
this[property].change_is_muted(true);
185
this[property].volume = volume;
187
this[property].change_is_muted(false);
189
this[property].push_volume();
192
_notifyVolumeChange: function() {
193
global.cancel_theme_sound(VOLUME_NOTIFY_ID);
194
global.play_theme_sound(VOLUME_NOTIFY_ID, 'audio-volume-change');
197
_mutedChanged: function(object, param_spec, property) {
198
let muted = this[property].is_muted;
199
let slider = this[property+'Slider'];
200
slider.setValue(muted ? 0 : (this[property].volume / this._volumeMax));
201
if (property == '_output') {
203
this.emit('icon-changed', 'audio-volume-muted-symbolic');
205
this.emit('icon-changed', this._volumeToIcon(this._output.volume));
209
_volumeChanged: function(object, param_spec, property) {
210
this[property+'Slider'].setValue(this[property].volume / this._volumeMax);
211
if (property == '_output' && !this._output.is_muted)
212
this.emit('icon-changed', this._volumeToIcon(this._output.volume));
284
this._input.stream = this._control.get_default_source();
287
getIcon: function() {
288
return this._output.getIcon();