~ubuntu-branches/ubuntu/trusty/gnome-shell/trusty-proposed

« back to all changes in this revision

Viewing changes to js/ui/status/volume.js

  • Committer: Package Import Robot
  • Author(s): Emilio Pozuelo Monfort, Sjoerd Simons, Emilio Pozuelo Monfort
  • Date: 2013-03-20 22:40:26 UTC
  • mfrom: (1.4.1) (67 raring-proposed)
  • mto: (18.1.33 sid)
  • mto: This revision was merged to the branch mainline in revision 81.
  • Revision ID: package-import@ubuntu.com-20130320224026-zgrrbbiezhjgc5dc
Tags: 3.7.92-1
[ Sjoerd Simons ]
* Sync from Ubuntu
* d/p/ubuntu-lightdm-user-switching.patch:
  d/p/ubuntu_lock_on_suspend.patch:
  + Dropped, Ubuntu specifc
* d/p/14_make-GLX-optional.patch: Fixed upstream
* debian/control.in: Depend on e-d-s >= 3.7.90 to fix build failures due to
  deprecated structures in headers.
* debian/patches/40_change-pam-name-to-match-gdm.patch
  + Added. Change the pam service name to match the one used by gdm in debian

[ Emilio Pozuelo Monfort ]
* New upstream release.
  + debian/control.in:
    - Update build dependencies.
    - Temporarily add an explicit build-depend on network-manager-dev
      until libnm-glib-dev gets a versioned dependency.
  + debian/patches/git_fix_too_short_apps_view.patch:
    - Removed, included upstream.

Show diffs side-by-side

added added

removed removed

Lines of Context:
2
2
 
3
3
const Clutter = imports.gi.Clutter;
4
4
const Lang = imports.lang;
 
5
const Gio = imports.gi.Gio;
5
6
const Gvc = imports.gi.Gvc;
6
7
const St = imports.gi.St;
 
8
const Signals = imports.signals;
7
9
 
8
10
const PanelMenu = imports.ui.panelMenu;
9
11
const PopupMenu = imports.ui.popupMenu;
25
27
    return _mixerControl;
26
28
}
27
29
 
 
30
const StreamSlider = new Lang.Class({
 
31
    Name: 'StreamSlider',
 
32
 
 
33
    _init: function(control, title) {
 
34
        this._control = control;
 
35
 
 
36
        this.item = new PopupMenu.PopupMenuSection();
 
37
 
 
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));
 
42
 
 
43
        this.item.addMenuItem(this._title);
 
44
        this.item.addMenuItem(this._slider);
 
45
 
 
46
        this._stream = null;
 
47
        this._shouldShow = true;
 
48
    },
 
49
 
 
50
    get stream() {
 
51
        return this._stream;
 
52
    },
 
53
 
 
54
    set stream(stream) {
 
55
        if (this._stream) {
 
56
            this._disconnectStream(this._stream);
 
57
        }
 
58
 
 
59
        this._stream = stream;
 
60
 
 
61
        if (this._stream) {
 
62
            this._connectStream(this._stream);
 
63
            this._updateVolume();
 
64
        } else {
 
65
            this.emit('stream-updated');
 
66
        }
 
67
 
 
68
        this._updateVisibility();
 
69
    },
 
70
 
 
71
    _disconnectStream: function(stream) {
 
72
        stream.disconnect(this._mutedChangedId);
 
73
        this._mutedChangedId = 0;
 
74
        stream.disconnect(this._volumeChangedId);
 
75
        this._volumeChangedId = 0;
 
76
    },
 
77
 
 
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));
 
81
    },
 
82
 
 
83
    _shouldBeVisible: function() {
 
84
        return this._stream != null;
 
85
    },
 
86
 
 
87
    _updateVisibility: function() {
 
88
        let visible = this._shouldBeVisible();
 
89
        this._title.actor.visible = visible;
 
90
        this._slider.actor.visible = visible;
 
91
    },
 
92
 
 
93
    scroll: function(event) {
 
94
        this._slider.scroll(event);
 
95
    },
 
96
 
 
97
    setValue: function(value) {
 
98
        // piggy-back off of sliderChanged
 
99
        this._slider.setValue(value);
 
100
    },
 
101
 
 
102
    _sliderChanged: function(slider, value, property) {
 
103
        if (!this._stream)
 
104
            return;
 
105
 
 
106
        let volume = value * this._control.get_vol_max_norm();
 
107
        let prevMuted = this._stream.is_muted;
 
108
        if (volume < 1) {
 
109
            this._stream.volume = 0;
 
110
            if (!prevMuted)
 
111
                this._stream.change_is_muted(true);
 
112
        } else {
 
113
            this._stream.volume = volume;
 
114
            if (prevMuted)
 
115
                this._stream.change_is_muted(false);
 
116
        }
 
117
        this._stream.push_volume();
 
118
    },
 
119
 
 
120
    _notifyVolumeChange: function() {
 
121
        global.cancel_theme_sound(VOLUME_NOTIFY_ID);
 
122
        global.play_theme_sound(VOLUME_NOTIFY_ID,
 
123
                                'audio-volume-change',
 
124
                                _("Volume changed"),
 
125
                                Clutter.get_current_event ());
 
126
    },
 
127
 
 
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');
 
132
    },
 
133
 
 
134
    getIcon: function() {
 
135
        if (!this._stream)
 
136
            return null;
 
137
 
 
138
        let volume = this._stream.volume;
 
139
        if (this._stream.is_muted || volume <= 0) {
 
140
            return 'audio-volume-muted-symbolic';
 
141
        } else {
 
142
            let n = Math.floor(3 * volume / this._control.get_vol_max_norm()) + 1;
 
143
            if (n < 2)
 
144
                return 'audio-volume-low-symbolic';
 
145
            if (n >= 3)
 
146
                return 'audio-volume-high-symbolic';
 
147
            return 'audio-volume-medium-symbolic';
 
148
        }
 
149
    }
 
150
});
 
151
Signals.addSignalMethods(StreamSlider.prototype);
 
152
 
 
153
const OutputStreamSlider = new Lang.Class({
 
154
    Name: 'OutputStreamSlider',
 
155
    Extends: StreamSlider,
 
156
 
 
157
    _connectStream: function(stream) {
 
158
        this.parent(stream);
 
159
        this._portChangedId = stream.connect('notify::port', Lang.bind(this, this._portChanged));
 
160
        this._portChanged();
 
161
    },
 
162
 
 
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')
 
167
            return true;
 
168
 
 
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;
 
174
 
 
175
        return false;
 
176
    },
 
177
 
 
178
    _disconnectStream: function(stream) {
 
179
        this.parent(stream);
 
180
        stream.disconnect(this._portChangedId);
 
181
        this._portChangedId = 0;
 
182
    },
 
183
 
 
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);
 
189
        }
 
190
    }
 
191
});
 
192
 
 
193
const InputStreamSlider = new Lang.Class({
 
194
    Name: 'InputStreamSlider',
 
195
    Extends: StreamSlider,
 
196
 
 
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));
 
201
    },
 
202
 
 
203
    _connectStream: function(stream) {
 
204
        this.parent(stream);
 
205
        this._maybeShowInput();
 
206
    },
 
207
 
 
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')) {
 
219
                    showInput = true;
 
220
                    break;
 
221
                }
 
222
            }
 
223
        }
 
224
 
 
225
        this._showInput = showInput;
 
226
        this._updateVisibility();
 
227
    },
 
228
 
 
229
    _shouldBeVisible: function() {
 
230
        return this.parent() && this._showInput;
 
231
    }
 
232
});
 
233
 
28
234
const VolumeMenu = new Lang.Class({
29
235
    Name: 'VolumeMenu',
30
236
    Extends: PopupMenu.PopupMenuSection,
32
238
    _init: function(control) {
33
239
        this.parent();
34
240
 
 
241
        this.hasHeadphones = false;
 
242
 
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();
42
247
 
43
 
        this._output = null;
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');
 
252
        }));
 
253
        this._output.connect('headphones-changed', Lang.bind(this, function(stream, value) {
 
254
            this.emit('headphones-changed', value);
 
255
        }));
 
256
        this.addMenuItem(this._output.item);
 
257
 
 
258
        this._input = new InputStreamSlider(this._control, _("Microphone"));
 
259
        this.addMenuItem(this._input.item);
53
260
 
54
261
        this.addMenuItem(new PopupMenu.PopupSeparatorMenuItem());
55
262
 
56
 
        this._input = null;
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);
65
 
 
66
263
        this._onControlStateChanged();
67
264
    },
68
265
 
69
 
    scroll: function(direction) {
70
 
        let currentVolume = this._output.volume;
71
 
 
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;
77
 
                if (!prev_muted)
78
 
                    this._output.change_is_muted(true);
79
 
            }
80
 
            this._output.push_volume();
81
 
        }
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();
86
 
        }
87
 
 
88
 
        this._notifyVolumeChange();
 
266
    scroll: function(event) {
 
267
        this._output.scroll(event);
89
268
    },
90
269
 
91
270
    _onControlStateChanged: function() {
92
271
        if (this._control.get_state() == Gvc.MixerControlState.READY) {
 
272
            this._readInput();
93
273
            this._readOutput();
94
 
            this._readInput();
95
 
            this._maybeShowInput();
96
274
        } else {
97
 
            this.emit('icon-changed', null);
 
275
            this.emit('icon-changed');
98
276
        }
99
277
    },
100
278
 
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;
107
 
        }
108
 
        this._output = this._control.get_default_sink();
109
 
        if (this._output) {
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');
114
 
        } else {
115
 
            this._outputSlider.setValue(0);
116
 
            this.emit('icon-changed', 'audio-volume-muted-symbolic');
117
 
        }
 
280
        this._output.stream = this._control.get_default_sink();
118
281
    },
119
282
 
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;
126
 
        }
127
 
        this._input = this._control.get_default_source();
128
 
        if (this._input) {
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');
133
 
        } else {
134
 
            this._inputTitle.actor.hide();
135
 
            this._inputSlider.actor.hide();
136
 
        }
137
 
    },
138
 
 
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')) {
150
 
                    showInput = true;
151
 
                    break;
152
 
                }
153
 
            }
154
 
        }
155
 
 
156
 
        this._inputTitle.actor.visible = showInput;
157
 
        this._inputSlider.actor.visible = showInput;
158
 
    },
159
 
 
160
 
    _volumeToIcon: function(volume) {
161
 
        if (volume <= 0) {
162
 
            return 'audio-volume-muted-symbolic';
163
 
        } else {
164
 
            let n = Math.floor(3 * volume / this._volumeMax) + 1;
165
 
            if (n < 2)
166
 
                return 'audio-volume-low-symbolic';
167
 
            if (n >= 3)
168
 
                return 'audio-volume-high-symbolic';
169
 
            return 'audio-volume-medium-symbolic';
170
 
        }
171
 
    },
172
 
 
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));
176
 
            return;
177
 
        }
178
 
        let volume = value * this._volumeMax;
179
 
        let prev_muted = this[property].is_muted;
180
 
        if (volume < 1) {
181
 
            this[property].volume = 0;
182
 
            if (!prev_muted)
183
 
                this[property].change_is_muted(true);
184
 
        } else {
185
 
            this[property].volume = volume;
186
 
            if (prev_muted)
187
 
                this[property].change_is_muted(false);
188
 
        }
189
 
        this[property].push_volume();
190
 
    },
191
 
 
192
 
    _notifyVolumeChange: function() {
193
 
        global.cancel_theme_sound(VOLUME_NOTIFY_ID);
194
 
        global.play_theme_sound(VOLUME_NOTIFY_ID, 'audio-volume-change');
195
 
    },
196
 
 
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') {
202
 
            if (muted)
203
 
                this.emit('icon-changed', 'audio-volume-muted-symbolic');
204
 
            else
205
 
                this.emit('icon-changed', this._volumeToIcon(this._output.volume));
206
 
        }
207
 
    },
208
 
 
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();
 
285
    },
 
286
 
 
287
    getIcon: function() {
 
288
        return this._output.getIcon();
213
289
    }
214
290
});
215
291
 
222
298
 
223
299
        this._control = getMixerControl();
224
300
        this._volumeMenu = new VolumeMenu(this._control);
225
 
        this._volumeMenu.connect('icon-changed', Lang.bind(this, function(menu, icon) {
226
 
            this._hasPulseAudio = (icon != null);
 
301
        this._volumeMenu.connect('icon-changed', Lang.bind(this, function(menu) {
 
302
            let icon = this._volumeMenu.getIcon();
 
303
            this.actor.visible = (icon != null);
227
304
            this.setIcon(icon);
228
 
            this._syncVisibility();
229
 
        }));
 
305
        }));
 
306
        this._volumeMenu.connect('headphones-changed', Lang.bind(this, function(menu, value) {
 
307
            this._headphoneIcon.visible = value;
 
308
        }));
 
309
 
 
310
        this._headphoneIcon = this.addIcon(new Gio.ThemedIcon({ name: 'headphones-symbolic' }));
 
311
        this._headphoneIcon.visible = false;
230
312
 
231
313
        this.menu.addMenuItem(this._volumeMenu);
232
314
 
236
318
        this.actor.connect('scroll-event', Lang.bind(this, this._onScrollEvent));
237
319
    },
238
320
 
239
 
    _syncVisibility: function() {
240
 
        this.actor.visible = this._hasPulseAudio;
241
 
        this.mainIcon.visible = this._hasPulseAudio;
242
 
    },
243
 
 
244
321
    _onScrollEvent: function(actor, event) {
245
 
        this._volumeMenu.scroll(event.get_scroll_direction());
 
322
        this._volumeMenu.scroll(event);
246
323
    }
247
324
});