4 // Advanced Volume Mixer
5 // Control programs' volume from gnome volume mixer applet.
7 // Idea from: https://extensions.gnome.org/extension/142/output-device-chooser-on-volume-menu/
9 // Author: Harry Karvonen <harry.karvonen@gmail.com>
12 const Clutter = imports.gi.Clutter;
13 const Lang = imports.lang;
14 const Gvc = imports.gi.Gvc;
15 const Signals = imports.signals;
16 const St = imports.gi.St;
18 const Main = imports.ui.main;
19 const PopupMenu = imports.ui.popupMenu;
25 function AdvPopupSwitchMenuItem() {
26 this._init.apply(this, arguments);
30 AdvPopupSwitchMenuItem.prototype = {
31 __proto__: PopupMenu.PopupSwitchMenuItem.prototype,
33 _init: function(text, active, gicon, params) {
34 PopupMenu.PopupSwitchMenuItem.prototype._init.call(
41 this._icon = new St.Icon({
43 style_class: "adv-volume-icon"
47 this.removeActor(this._statusBin);
48 this.removeActor(this.label)
51 let labelBox = new St.BoxLayout({vertical: false});
53 labelBox.add(this._icon,
54 {expand: false, x_fill: false, x_align: St.Align.START});
55 labelBox.add(this.label,
56 {expand: false, x_fill: false, x_align: St.Align.START});
57 labelBox.add(this._statusBin,
58 {expand: true, x_fill: true, x_align: St.Align.END});
60 this.addActor(labelBox, {span: -1, expand: true });
65 function AdvMixer(mixer) {
70 AdvMixer.prototype = {
71 _init: function(mixer) {
73 this._control = mixer._control;
76 this._outputMenu = new PopupMenu.PopupSubMenuMenuItem(_("Volume"));
78 this._streamAddedId = this._control.connect(
80 Lang.bind(this, this._streamAdded)
82 this._streamRemovedId = this._control.connect(
84 Lang.bind(this, this._streamRemoved)
86 this._defaultSinkChangedId = this._control.connect(
87 "default-sink-changed",
88 Lang.bind(this, this._defaultSinkChanged)
91 // Change Volume label
92 let label = this._mixer.menu.firstMenuItem;
96 this._mixer.menu.addMenuItem(this._outputMenu, 0);
97 this._outputMenu.actor.show();
100 let streams = this._control.get_streams();
101 for (let i = 0; i < streams.length; i++) {
102 this._streamAdded(this._control, streams[i].id);
105 if (this._control.get_default_sink() != null) {
106 this._defaultSinkChanged(
108 this._control.get_default_sink().id
114 _streamAdded: function(control, id) {
115 if (id in this._items) {
119 if (id in this._outputs) {
123 let stream = control.lookup_stream_id(id);
125 if (stream["is-event-stream"]) {
127 } else if (stream instanceof Gvc.MixerSinkInput) {
128 let slider = new PopupMenu.PopupSliderMenuItem(
129 stream.volume / this._control.get_vol_max_norm()
131 let title = new AdvPopupSwitchMenuItem(
145 Lang.bind(this, this._sliderValueChanged, stream.id)
149 "button-release-event",
150 Lang.bind(this, this._titleToggleState, stream.id)
155 Lang.bind(this, this._titleToggleState, stream.id)
160 Lang.bind(this, this._notifyVolume, stream.id)
165 Lang.bind(this, this._notifyIsMuted, stream.id)
168 this._mixer.menu.addMenuItem(this._items[id]["slider"], 3);
169 this._mixer.menu.addMenuItem(this._items[id]["title"], 3);
170 } else if (stream instanceof Gvc.MixerSink) {
171 let output = new PopupMenu.PopupMenuItem(stream.description);
175 function (item, event) { control.set_default_sink(stream); }
178 this._outputMenu.menu.addMenuItem(output);
180 this._outputs[id] = output;
184 _streamRemoved: function(control, id) {
185 if (id in this._items) {
186 this._items[id]["slider"].destroy();
187 this._items[id]["title"].destroy();
188 delete this._items[id];
191 if (id in this._outputs) {
192 this._outputs[id].destroy();
193 delete this._outputs[id];
197 _defaultSinkChanged: function(control, id) {
198 for (let output in this._outputs) {
199 this._outputs[output].setShowDot(output == id);
203 _sliderValueChanged: function(slider, value, id) {
204 let stream = this._control.lookup_stream_id(id);
205 let volume = value * this._control.get_vol_max_norm();
207 stream.volume = volume;
208 stream.push_volume();
211 _titleToggleState: function(title, event, id) {
212 if (event.type() == Clutter.EventType.KEY_PRESS) {
213 let symbol = event.get_key_symbol();
215 if (symbol != Clutter.KEY_space && symbol != Clutter.KEY_Return) {
220 let stream = this._control.lookup_stream_id(id);
222 stream.change_is_muted(!stream.is_muted);
227 _notifyVolume: function(object, param_spec, id) {
228 let stream = this._control.lookup_stream_id(id);
230 this._items[id]["slider"].setValue(stream.volume / this._control.get_vol_max_norm());
233 _notifyIsMuted: function(object, param_spec, id) {
234 let stream = this._control.lookup_stream_id(id);
236 this._items[id]["title"].setToggleState(!stream.is_muted);
239 destroy: function() {
240 this._control.disconnect(this._streamAddedId);
241 this._control.disconnect(this._streamRemovedId);
242 this._control.disconnect(this._defaultSinkChangedId);
244 // Restore Volume label
245 this._outputMenu.destroy();
246 delete this._outputMenu;
248 let label = new PopupMenu.PopupMenuItem(_("Volume"), {reactive: false });
249 this._mixer.menu.addMenuItem(label, 0);
252 // remove application streams
253 for (let id in this._items) {
254 this._streamRemoved(this._control, id);
257 this.emit("destroy");
262 Signals.addSignalMethods(AdvMixer.prototype);
276 if (Main.panel._statusArea['volume'] && !advMixer) {
277 advMixer = new AdvMixer(Main.panel._statusArea["volume"]);