Now works with gnome 3.6
[AdvancedVolumeMixer.git] / extension.js
blobd1378859faa43773faa4b5877df241814dbbce85
1 // extension.js
2 // vi: et sw=2
3 //
4 // Advanced Volume Mixer
5 // Control programs' volume from gnome volume mixer applet.
6 //
7 // Author: Harry Karvonen <harry.karvonen@gmail.com>
8 //
10 const Clutter = imports.gi.Clutter;
11 const Lang = imports.lang;
12 const Gvc = imports.gi.Gvc;
13 const Signals = imports.signals;
14 const St = imports.gi.St;
16 const Main = imports.ui.main;
17 const PopupMenu = imports.ui.popupMenu;
20 let advMixer;
23 function AdvPopupSwitchMenuItem() {
24   this._init.apply(this, arguments);
28 AdvPopupSwitchMenuItem.prototype = {
29   __proto__: PopupMenu.PopupSwitchMenuItem.prototype,
31   _init: function(text, active, gicon, params) {
32     PopupMenu.PopupSwitchMenuItem.prototype._init.call(
33       this,
34       " " + text + "  ",
35       active,
36       params
37     );
39     this._icon = new St.Icon({
40       gicon:        gicon,
41       style_class: "adv-volume-icon"
42     });
44     // Rebuild switch
45     this.removeActor(this._statusBin);
46     this.removeActor(this.label)
48     // Horizontal box
49     let labelBox = new St.BoxLayout({vertical: false});
51     labelBox.add(this._icon,
52                 {expand: false, x_fill: false, x_align: St.Align.START});
53     labelBox.add(this.label,
54                  {expand: false, x_fill: false, x_align: St.Align.START});
55     labelBox.add(this._statusBin,
56                  {expand: true, x_fill: true, x_align: St.Align.END});
57             
58     this.addActor(labelBox, {span: -1, expand: true });
59   }
63 function AdvMixer(mixer) {
64   this._init(mixer);
68 AdvMixer.prototype = {
69   _init: function(mixer) {
70     this._mixer = mixer;
71     this._control = mixer._control;
72     this._separator = new PopupMenu.PopupSeparatorMenuItem();
73     this._items = {};
74     this._outputs = {};
75     this._outputMenu = new PopupMenu.PopupSubMenuMenuItem(_("Volume"));
77     this._mixer.menu.addMenuItem(this._separator, 1);
79     this._streamAddedId = this._control.connect(
80       "stream-added",
81       Lang.bind(this, this._streamAdded)
82     );
83     this._streamRemovedId = this._control.connect(
84       "stream-removed",
85       Lang.bind(this, this._streamRemoved)
86     );
87     this._defaultSinkChangedId = this._control.connect(
88       "default-sink-changed",
89       Lang.bind(this, this._defaultSinkChanged)
90     );
92     // Change Volume label
93     let label = this._mixer.menu.firstMenuItem;
94     label.destroy();
95     //delete label;
97     this._mixer.menu.addMenuItem(this._outputMenu, 0);
98     this._outputMenu.actor.show();
100     // Add streams
101     let streams = this._control.get_streams();
102     for (let i = 0; i < streams.length; i++) {
103       this._streamAdded(this._control, streams[i].id);
104     }
106     if (this._control.get_default_sink() != null) {
107       this._defaultSinkChanged(
108         this._control,
109         this._control.get_default_sink().id
110       );
111     }
112   },
115   _streamAdded: function(control, id) {
116     if (id in this._items) {
117       return;
118     }
120     if (id in this._outputs) {
121       return;
122     }
124     let stream = control.lookup_stream_id(id);
126     if (stream["is-event-stream"]) {
127       // Do nothing
128     } else if (stream instanceof Gvc.MixerSinkInput) {
129       let slider = new PopupMenu.PopupSliderMenuItem(
130         stream.volume / this._control.get_vol_max_norm()
131       );
132       let title = new AdvPopupSwitchMenuItem(
133         stream.name || stream.description,
134         !stream.is_muted,
135         stream.get_gicon(),
136         {activate: false}
137       );
139       this._items[id] = {
140         slider: slider,
141         title: title
142       };
144       slider.connect(
145         "value-changed",
146         Lang.bind(this, this._sliderValueChanged, stream.id)
147       );
149       title.actor.connect(
150         "button-release-event",
151         Lang.bind(this, this._titleToggleState, stream.id)
152       );
154       title.actor.connect(
155         "key-press-event",
156         Lang.bind(this, this._titleToggleState, stream.id)
157       );
159       stream.connect(
160         "notify::volume",
161         Lang.bind(this, this._notifyVolume, stream.id)
162       );
164       stream.connect(
165         "notify::is-muted",
166         Lang.bind(this, this._notifyIsMuted, stream.id)
167       );
169       this._mixer.menu.addMenuItem(this._items[id]["slider"], 2);
170       this._mixer.menu.addMenuItem(this._items[id]["title"], 2);
171     } else if (stream instanceof Gvc.MixerSink) {
172       let output = new PopupMenu.PopupMenuItem(stream.description);
174       output.connect(
175         "activate",
176         function (item, event) { control.set_default_sink(stream); }
177       );
179       this._outputMenu.menu.addMenuItem(output);
181       this._outputs[id] = output;
182     }
183   },
185   _streamRemoved: function(control, id) {
186     if (id in this._items) {
187       this._items[id]["slider"].destroy();
188       this._items[id]["title"].destroy();
189       delete this._items[id];
190     }
192     if (id in this._outputs) {
193       this._outputs[id].destroy();
194       delete this._outputs[id];
195     }
196   },
198   _defaultSinkChanged: function(control, id) {
199     for (let output in this._outputs) {
200       this._outputs[output].setShowDot(output == id);
201     }
202   },
204   _sliderValueChanged: function(slider, value, id) {
205     let stream = this._control.lookup_stream_id(id);
206     let volume = value * this._control.get_vol_max_norm();
208     stream.volume = volume;
209     stream.push_volume();
210   },
212   _titleToggleState: function(title, event, id) {
213     if (event.type() == Clutter.EventType.KEY_PRESS) {
214       let symbol = event.get_key_symbol();
216       if (symbol != Clutter.KEY_space && symbol != Clutter.KEY_Return) {
217         return false;
218       }
219     }
221     let stream = this._control.lookup_stream_id(id);
223     stream.change_is_muted(!stream.is_muted);
225     return true;
226   },
228   _notifyVolume: function(object, param_spec, id) {
229     let stream = this._control.lookup_stream_id(id);
231     this._items[id]["slider"].setValue(stream.volume / this._control.get_vol_max_norm());
232   },
234   _notifyIsMuted: function(object, param_spec, id) {
235     let stream = this._control.lookup_stream_id(id);
237     this._items[id]["title"].setToggleState(!stream.is_muted);
238   },
240   destroy: function() {
241     this._control.disconnect(this._streamAddedId);
242     this._control.disconnect(this._streamRemovedId);
243     this._control.disconnect(this._defaultSinkChangedId);
245     this._separator.destroy();
246     delete this._separator;
248     // Restore Volume label
249     this._outputMenu.destroy();
250     delete this._outputMenu;
252     let label = new PopupMenu.PopupMenuItem(_("Volume"), {reactive: false });
253     this._mixer.menu.addMenuItem(label, 0);
254     label.actor.show();
256     // remove application streams
257     for (let id in this._items) {
258       this._streamRemoved(this._control, id);
259     }
261     this.emit("destroy");
262   }
266 Signals.addSignalMethods(AdvMixer.prototype);
269 function main() {
270   init();
271   enable();
275 function init() {
279 function enable() {
280   if (Main.panel.statusArea['volume'] && !advMixer) {
281     advMixer = new AdvMixer(Main.panel.statusArea["volume"]);
282   }
286 function disable() {
287   if (advMixer) {
288     advMixer.destroy();
289     advMixer = null;
290   }