Listen to the events from alsa mixers in volumealsa plugin.
[lxde/lxpanel.git] / src / plugins / volumealsa / volumealsa.c
1 /**
2 * Copyright (c) 2008 LxDE Developers, see the file AUTHORS for details.
3 *
4 * This program is free software; you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License as published by
6 * the Free Software Foundation; either version 2 of the License, or
7 * (at your option) any later version.
8 *
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
13 *
14 * You should have received a copy of the GNU General Public License
15 * along with this program; if not, write to the Free Software Foundation,
16 * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
17 */
18
19 #include <gtk/gtk.h>
20 #include <stdlib.h>
21 #include <fcntl.h>
22 #include <unistd.h>
23 #include <glib.h>
24 #include <glib/gi18n.h>
25 #include <gdk-pixbuf/gdk-pixbuf.h>
26 #include <alsa/asoundlib.h>
27 #include <poll.h>
28 #include "panel.h"
29 #include "misc.h"
30 #include "plugin.h"
31 #include "dbg.h"
32
33 #define ICONS_VOLUME PACKAGE_DATA_DIR "/lxpanel/images/volume.png"
34 #define ICONS_MUTE PACKAGE_DATA_DIR "/lxpanel/images/mute.png"
35
36 typedef struct {
37 Plugin* plugin;
38 GtkWidget *mainw;
39 GtkWidget *tray_icon;
40 GtkWidget *dlg;
41 GtkTooltips* tooltips;
42 GtkWidget *vscale;
43 snd_mixer_t *mixer;
44 snd_mixer_selem_id_t *sid;
45 snd_mixer_elem_t *master_element;
46 long alsa_min_vol, alsa_max_vol;
47 int mute;
48 int show;
49 gboolean mixer_evt_idle;
50 } volume_t;
51
52
53
54 /* ALSA */
55 static gboolean find_element(volume_t *vol, const char *ename)
56 {
57 for (vol->master_element=snd_mixer_first_elem(vol->mixer);vol->master_element;vol->master_element=snd_mixer_elem_next(vol->master_element)) {
58 snd_mixer_selem_get_id(vol->master_element, vol->sid);
59 if (!snd_mixer_selem_is_active(vol->master_element))
60 continue;
61
62 if (strcmp(ename, snd_mixer_selem_id_get_name(vol->sid))==0) {
63 return TRUE;
64 }
65 }
66
67 return FALSE;
68 }
69
70 /* NOTE by PCMan:
71 * This is magic! Since ALSA uses its own machanism to handle this part.
72 * After polling of mixer fds, it requires that we should call
73 * snd_mixer_handle_events to clear all pending mixer events.
74 * However, when using the glib IO channels approach, we don't have
75 * poll() and snd_mixer_poll_descriptors_revents(). Due to the design of
76 * glib, on_mixer_event() will be called for every fd whose status was
77 * changed. So, after each poll(), it's called for several times,
78 * not just once. Therefore, we cannot call snd_mixer_handle_events()
79 * directly in the event handler. Otherwise, it will get called for
80 * several times, which might clear unprocessed pending events in the queue.
81 * So, here we call it once in the event callback for the first fd.
82 * Then, we don't call it for the following fds. After all fds with changed
83 * status are handled, we remove this restriction in an idle handler.
84 * The next time the event callback is involked for the first fs, we can
85 * call snd_mixer_handle_events() again. Racing shouldn't happen here
86 * because the idle handler has the same priority as the io channel callback.
87 * So, io callbacks for future pending events should be in the next gmain
88 * iteration, and won't be affected.
89 */
90 static gboolean reset_mixer_evt_idle( volume_t* vol )
91 {
92 vol->mixer_evt_idle = 0;
93 return FALSE;
94 }
95
96 static gboolean on_mixer_event( GIOChannel* channel, GIOCondition cond, volume_t *vol )
97 {
98 if( cond & G_IO_IN )
99 {
100 /* the status of mixer is changed. update of display is needed. */
101 }
102 if( cond & G_IO_HUP )
103 {
104 /* FIXME: This means there're some problems with alsa. */
105
106 return FALSE;
107 }
108
109 if( 0 == vol->mixer_evt_idle )
110 {
111 vol->mixer_evt_idle = g_idle_add_full( G_PRIORITY_DEFAULT, (GSourceFunc)reset_mixer_evt_idle, vol, NULL );
112 snd_mixer_handle_events( vol->mixer );
113 }
114 return TRUE;
115 }
116
117 static gboolean asound_init(volume_t *vol)
118 {
119 int i, n_fds;
120 struct pollfd *fds;
121
122 snd_mixer_selem_id_alloca(&vol->sid);
123 snd_mixer_open(&vol->mixer, 0);
124 snd_mixer_attach(vol->mixer, "default");
125 snd_mixer_selem_register(vol->mixer, NULL, NULL);
126 snd_mixer_load(vol->mixer);
127
128 /* Find Master element */
129 if (!find_element(vol, "Master"))
130 if (!find_element(vol, "Front"))
131 if (!find_element(vol, "PCM"))
132 if (!find_element(vol, "LineOut"))
133 return FALSE;
134
135
136 snd_mixer_selem_get_playback_volume_range(vol->master_element, &vol->alsa_min_vol, &vol->alsa_max_vol);
137
138 snd_mixer_selem_set_playback_volume_range(vol->master_element, 0, 100);
139
140 /* listen to events from alsa */
141 n_fds = snd_mixer_poll_descriptors_count( vol->mixer );
142 fds = g_new0( struct pollfd, n_fds );
143
144 snd_mixer_poll_descriptors( vol->mixer, fds, n_fds );
145 for( i = 0; i < n_fds; ++i )
146 {
147 /* g_debug("fd=%d", fds[i]); */
148 GIOChannel* channel = g_io_channel_unix_new( fds[i].fd );
149 g_io_add_watch( channel, G_IO_IN|G_IO_HUP, on_mixer_event, vol );
150 g_io_channel_unref( channel );
151 }
152 g_free( fds );
153 return TRUE;
154 }
155
156 static int asound_read(volume_t *vol)
157 {
158 long aleft, aright;
159 snd_mixer_handle_events(vol->mixer);
160 /* Left */
161 snd_mixer_selem_get_playback_volume(vol->master_element, SND_MIXER_SCHN_FRONT_LEFT, &aleft);
162 /* Right */
163 snd_mixer_selem_get_playback_volume(vol->master_element, SND_MIXER_SCHN_FRONT_RIGHT, &aright);
164
165 return (aleft + aright) >> 1;
166 }
167
168 static void asound_write(volume_t *vol, int volume)
169 {
170 snd_mixer_selem_set_playback_volume(vol->master_element, SND_MIXER_SCHN_FRONT_LEFT, volume);
171 snd_mixer_selem_set_playback_volume(vol->master_element, SND_MIXER_SCHN_FRONT_RIGHT, volume);
172 }
173
174 static gboolean focus_out_event(GtkWidget *widget, GdkEvent *event, volume_t *vol)
175 {
176 gtk_widget_hide(vol->dlg);
177 vol->show = 0;
178 return FALSE;
179 }
180
181 static gboolean tray_icon_press(GtkWidget *widget, GdkEventButton *event, volume_t *vol)
182 {
183 if( event->button == 3 ) /* right button */
184 {
185 GtkMenu* popup = lxpanel_get_panel_menu( vol->plugin->panel, vol->plugin, FALSE );
186 gtk_menu_popup( popup, NULL, NULL, NULL, NULL, event->button, event->time );
187 return TRUE;
188 }
189
190 if (vol->show==0) {
191 gtk_window_set_position(GTK_WINDOW(vol->dlg), GTK_WIN_POS_MOUSE);
192 gtk_scale_set_digits(GTK_SCALE(vol->vscale), asound_read(vol));
193 gtk_widget_show_all(vol->dlg);
194 vol->show = 1;
195 } else {
196 gtk_widget_hide(vol->dlg);
197 vol->show = 0;
198 }
199 return TRUE;
200 }
201
202 static void on_vscale_value_changed(GtkRange *range, volume_t *vol)
203 {
204 asound_write(vol, gtk_range_get_value(range));
205 }
206
207 static void click_mute(GtkWidget *widget, volume_t *vol)
208 {
209 int chn;
210
211 if (gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(widget))) {
212 gtk_image_set_from_file(vol->tray_icon, ICONS_MUTE);
213 for (chn = 0; chn <= SND_MIXER_SCHN_LAST; chn++) {
214 snd_mixer_selem_set_playback_switch(vol->master_element, chn, 0);
215 }
216 } else {
217 gtk_image_set_from_file(vol->tray_icon, ICONS_VOLUME);
218 for (chn = 0; chn <= SND_MIXER_SCHN_LAST; chn++) {
219 snd_mixer_selem_set_playback_switch(vol->master_element, chn, 1);
220 }
221 }
222 }
223
224 static void panel_init(Plugin *p)
225 {
226 volume_t *vol = p->priv;
227 GtkWidget *scrolledwindow;
228 GtkWidget *viewport;
229 GtkWidget *box;
230 GtkWidget *frame;
231 GtkWidget *checkbutton;
232
233 /* set show flags */
234 vol->show = 0;
235
236 /* create a new window */
237 vol->dlg = gtk_window_new(GTK_WINDOW_TOPLEVEL);
238 gtk_window_set_decorated(GTK_WINDOW(vol->dlg), FALSE);
239 gtk_container_set_border_width(GTK_CONTAINER(vol->dlg), 5);
240 gtk_window_set_default_size(GTK_WINDOW(vol->dlg), 80, 140);
241 gtk_window_set_skip_taskbar_hint(GTK_WINDOW(vol->dlg), TRUE);
242 gtk_window_set_skip_pager_hint(GTK_WINDOW(vol->dlg), TRUE);
243 gtk_window_set_type_hint(GTK_WINDOW(vol->dlg), GDK_WINDOW_TYPE_HINT_DIALOG);
244
245 /* setting background to default */
246 //gtk_widget_set_style(vol->dlg, p->panel->defstyle);
247
248 /* Focus-out signal */
249 g_signal_connect (G_OBJECT (vol->dlg), "focus_out_event",
250 G_CALLBACK (focus_out_event), vol);
251
252 scrolledwindow = gtk_scrolled_window_new(NULL, NULL);
253 gtk_container_set_border_width (GTK_CONTAINER (scrolledwindow), 0);
254 gtk_widget_show (scrolledwindow);
255 gtk_container_add (GTK_CONTAINER (vol->dlg), scrolledwindow);
256 GTK_WIDGET_UNSET_FLAGS (scrolledwindow, GTK_CAN_FOCUS);
257 gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW (scrolledwindow), GTK_POLICY_NEVER, GTK_POLICY_NEVER);
258 gtk_scrolled_window_set_shadow_type(GTK_SCROLLED_WINDOW(scrolledwindow), GTK_SHADOW_NONE);
259
260 viewport = gtk_viewport_new (NULL, NULL);
261 gtk_container_add (GTK_CONTAINER (scrolledwindow), viewport);
262 gtk_viewport_set_shadow_type (GTK_VIEWPORT (viewport), GTK_SHADOW_NONE);
263 gtk_widget_show(viewport);
264
265 /* create frame */
266 frame = gtk_frame_new(_("Volume"));
267 gtk_frame_set_shadow_type(GTK_FRAME(frame), GTK_SHADOW_IN);
268 gtk_container_add(GTK_CONTAINER(viewport), frame);
269
270 /* create box */
271 box = gtk_vbox_new(FALSE, 0);
272
273 /* create controller */
274 vol->vscale = gtk_vscale_new(GTK_ADJUSTMENT(gtk_adjustment_new(asound_read(vol), 0, 100, 0, 0, 0)));
275 gtk_scale_set_draw_value(GTK_SCALE(vol->vscale), FALSE);
276 gtk_range_set_inverted(GTK_RANGE(vol->vscale), TRUE);
277
278 g_signal_connect ((gpointer) vol->vscale, "value_changed",
279 G_CALLBACK (on_vscale_value_changed),
280 vol);
281
282 checkbutton = gtk_check_button_new_with_label(_("Mute"));
283 snd_mixer_selem_get_playback_switch(vol->master_element, 0, &vol->mute);
284
285 if (!vol->mute)
286 gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(checkbutton), TRUE);
287
288 g_signal_connect ((gpointer) checkbutton, "toggled",
289 G_CALLBACK (click_mute),
290 vol);
291
292 gtk_box_pack_start(GTK_BOX(box), vol->vscale, TRUE, TRUE, 0);
293 gtk_box_pack_end(GTK_BOX(box), checkbutton, FALSE, FALSE, 0);
294 gtk_container_add(GTK_CONTAINER(frame), box);
295
296 /* setting background to default */
297 gtk_widget_set_style(viewport, p->panel->defstyle);
298 }
299
300 static void
301 volumealsa_destructor(Plugin *p)
302 {
303 volume_t *vol = (volume_t *) p->priv;
304
305 ENTER;
306
307 if( vol->mixer_evt_idle )
308 g_source_remove( vol->mixer_evt_idle );
309
310 if (vol->dlg)
311 gtk_widget_destroy(vol->dlg);
312
313 g_free(vol);
314 RET();
315 }
316
317 static int
318 volumealsa_constructor(Plugin *p, char **fp)
319 {
320 volume_t *vol;
321 line s;
322 GdkPixbuf *icon;
323 GtkWidget *image;
324 GtkIconTheme* theme;
325 GtkIconInfo* info;
326
327 ENTER;
328 s.len = 256;
329 vol = g_new0(volume_t, 1);
330 vol->plugin = p;
331 g_return_val_if_fail(vol != NULL, 0);
332 p->priv = vol;
333
334 /* initializing */
335 if (!asound_init(vol))
336 RET(1);
337
338 panel_init(p);
339
340 /* main */
341 vol->mainw = gtk_event_box_new();
342
343 gtk_widget_add_events(vol->mainw, GDK_BUTTON_PRESS_MASK);
344 gtk_widget_set_size_request( vol->mainw, 24, 24 );
345
346 g_signal_connect(G_OBJECT(vol->mainw), "button-press-event",
347 G_CALLBACK(tray_icon_press), vol);
348
349 /* tray icon */
350 snd_mixer_selem_get_playback_switch(vol->master_element, 0, &vol->mute);
351 if (vol->mute==0)
352 vol->tray_icon = gtk_image_new_from_file(ICONS_MUTE);
353 else
354 vol->tray_icon = gtk_image_new_from_file(ICONS_VOLUME);
355
356 gtk_container_add(GTK_CONTAINER(vol->mainw), vol->tray_icon);
357
358 gtk_widget_show_all(vol->mainw);
359
360 vol->tooltips = p->panel->tooltips;;
361 #if GLIB_CHECK_VERSION( 2, 10, 0 )
362 g_object_ref_sink( vol->tooltips );
363 #else
364 g_object_ref( vol->tooltips );
365 gtk_object_sink( vol->tooltips );
366 #endif
367
368 /* FIXME: display current level in tooltip. ex: "Volume Control: 80%" */
369 gtk_tooltips_set_tip (vol->tooltips, vol->mainw, _("Volume control"), NULL);
370
371 /* store the created plugin widget in plugin->pwid */
372 p->pwid = vol->mainw;
373
374 RET(1);
375 }
376
377
378 PluginClass volumealsa_plugin_class = {
379 fname: NULL,
380 count: 0,
381
382 type : "volumealsa",
383 name : N_("Volume Control"),
384 version: "1.0",
385 description : "Display and control volume for ALSA",
386
387 constructor : volumealsa_constructor,
388 destructor : volumealsa_destructor,
389 config : NULL,
390 save : NULL
391 };