Fixed few errors.
[AdvancedVolumeMixer.git] / extension.js
blobf565059c811e9725297f2549a8fa3e76cffed38c
1 // extension.js
2 // vi: et sw=2
3 //
4 // Advanced Volume Mixer
5 // Control programs' volume from gnome volume mixer applet.
6 //
7 // Idea from: https://extensions.gnome.org/extension/142/output-device-chooser-on-volume-menu/
8 //
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;
22 let advMixer;
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(
35       this,
36       " " + text + "  ",
37       active,
38       params
39     );
41     this._icon = new St.Icon({
42       gicon:        gicon,
43       style_class: "adv-volume-icon"
44     });
46     // Rebuild switch
47     this.removeActor(this._statusBin);
48     this.removeActor(this.label)
50     // Horizontal box
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});
59             
60     this.addActor(labelBox, {span: -1, expand: true });
61   }
65 function AdvMixer(mixer) {
66   this._init(mixer);
70 AdvMixer.prototype = {
71   _init: function(mixer) {
72     this._mixer = mixer;
73     this._control = mixer._control;
74     this._items = {};
75     this._outputs = {};
76     this._outputMenu = new PopupMenu.PopupSubMenuMenuItem(_("Volume"));
78     this._streamAddedId = this._control.connect(
79       "stream-added",
80       Lang.bind(this, this._streamAdded)
81     );
82     this._streamRemovedId = this._control.connect(
83       "stream-removed",
84       Lang.bind(this, this._streamRemoved)
85     );
86     this._defaultSinkChangedId = this._control.connect(
87       "default-sink-changed",
88       Lang.bind(this, this._defaultSinkChanged)
89     );
91     // Change Volume label
92     let label = this._mixer.menu.firstMenuItem;
93     label.destroy();
94     //delete label;
96     this._mixer.menu.addMenuItem(this._outputMenu, 0);
97     this._outputMenu.actor.show();
99     // Add streams
100     let streams = this._control.get_streams();
101     for (let i = 0; i < streams.length; i++) {
102       this._streamAdded(this._control, streams[i].id);
103     }
105     if (this._control.get_default_sink() != null) {
106       this._defaultSinkChanged(
107         this._control,
108         this._control.get_default_sink().id
109       );
110     }
111   },
114   _streamAdded: function(control, id) {
115     if (id in this._items) {
116       return;
117     }
119     if (id in this._outputs) {
120       return;
121     }
123     let stream = control.lookup_stream_id(id);
125     if (stream["is-event-stream"]) {
126       // Do nothing
127     } else if (stream instanceof Gvc.MixerSinkInput) {
128       let slider = new PopupMenu.PopupSliderMenuItem(
129         stream.volume / this._control.get_vol_max_norm()
130       );
131       let title = new AdvPopupSwitchMenuItem(
132         stream.name,
133         !stream.is_muted,
134         stream.get_gicon(),
135         {activate: false}
136       );
138       this._items[id] = {
139         slider: slider,
140         title: title
141       };
143       slider.connect(
144         "value-changed",
145         Lang.bind(this, this._sliderValueChanged, stream.id)
146       );
148       title.actor.connect(
149         "button-release-event",
150         Lang.bind(this, this._titleToggleState, stream.id)
151       );
153       title.actor.connect(
154         "key-press-event",
155         Lang.bind(this, this._titleToggleState, stream.id)
156       );
158       stream.connect(
159         "notify::volume",
160         Lang.bind(this, this._notifyVolume, stream.id)
161       );
163       stream.connect(
164         "notify::is-muted",
165         Lang.bind(this, this._notifyIsMuted, stream.id)
166       );
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);
173       output.connect(
174         "activate",
175         function (item, event) { control.set_default_sink(stream); }
176       );
178       this._outputMenu.menu.addMenuItem(output);
180       this._outputs[id] = output;
181     }
182   },
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];
189     }
191     if (id in this._outputs) {
192       this._outputs[id].destroy();
193       delete this._outputs[id];
194     }
195   },
197   _defaultSinkChanged: function(control, id) {
198     for (let output in this._outputs) {
199       this._outputs[output].setShowDot(output == id);
200     }
201   },
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();
209   },
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) {
216         return false;
217       }
218     }
220     let stream = this._control.lookup_stream_id(id);
222     stream.change_is_muted(!stream.is_muted);
224     return true;
225   },
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());
231   },
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);
237   },
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);
250     label.actor.show();
252     // remove application streams
253     for (let id in this._items) {
254       this._streamRemoved(this._control, id);
255     }
257     this.emit("destroy");
258   }
262 Signals.addSignalMethods(AdvMixer.prototype);
265 function main() {
266   init();
267   enable();
271 function init() {
275 function enable() {
276   if (Main.panel._statusArea['volume'] && !advMixer) {
277     advMixer = new AdvMixer(Main.panel._statusArea["volume"]);
278   }
282 function disable() {
283   if (advMixer) {
284     advMixer.destroy();
285     advMixer = null;
286   }