Adding upstream version 0.4.1+svn20090524.
[debian/lxpanel.git] / src / plugins / volumealsa / volumealsa.c
CommitLineData
6cc5e1a6
DB
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>
96dc8a45 27#include <poll.h>
6cc5e1a6
DB
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
36typedef struct {
37 Plugin* plugin;
38 GtkWidget *mainw;
39 GtkWidget *tray_icon;
40 GtkWidget *dlg;
6cc5e1a6 41 GtkWidget *vscale;
96dc8a45
DB
42 guint vscale_handler;
43 GtkWidget* mute_check;
44 guint mute_handler;
6cc5e1a6
DB
45 snd_mixer_t *mixer;
46 snd_mixer_selem_id_t *sid;
47 snd_mixer_elem_t *master_element;
48 long alsa_min_vol, alsa_max_vol;
96dc8a45 49 long level;
6cc5e1a6
DB
50 int mute;
51 int show;
96dc8a45 52 gboolean mixer_evt_idle;
6cc5e1a6
DB
53} volume_t;
54
55
6cc5e1a6 56/* ALSA */
96dc8a45
DB
57
58static int asound_read(volume_t *vol);
59
60static void asound_write(volume_t *vol, int volume);
61
6cc5e1a6
DB
62static gboolean find_element(volume_t *vol, const char *ename)
63{
64 for (vol->master_element=snd_mixer_first_elem(vol->mixer);vol->master_element;vol->master_element=snd_mixer_elem_next(vol->master_element)) {
65 snd_mixer_selem_get_id(vol->master_element, vol->sid);
66 if (!snd_mixer_selem_is_active(vol->master_element))
67 continue;
68
69 if (strcmp(ename, snd_mixer_selem_id_get_name(vol->sid))==0) {
70 return TRUE;
71 }
72 }
73
74 return FALSE;
75}
76
96dc8a45
DB
77static void update_display(volume_t* vol)
78{
79 /* mute status */
80 snd_mixer_selem_get_playback_switch(vol->master_element, 0, &vol->mute);
81
82 if (vol->mute==0)
7486d297 83 gtk_image_set_from_file(GTK_IMAGE(vol->tray_icon), ICONS_MUTE);
96dc8a45 84 else
7486d297 85 gtk_image_set_from_file(GTK_IMAGE(vol->tray_icon), ICONS_VOLUME);
96dc8a45
DB
86
87 g_signal_handler_block( vol->mute_check, vol->mute_handler );
88 gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(vol->mute_check), !vol->mute );
89 g_signal_handler_unblock( vol->mute_check, vol->mute_handler );
90
91 /* volume */
92 vol->level = asound_read(vol);
93
94 if( vol->vscale )
95 {
96 g_signal_handler_block( vol->vscale, vol->vscale_handler );
97 gtk_range_set_value(GTK_RANGE(vol->vscale), vol->level);
98 g_signal_handler_unblock( vol->vscale, vol->vscale_handler );
99 }
100}
101
102/* NOTE by PCMan:
103 * This is magic! Since ALSA uses its own machanism to handle this part.
104 * After polling of mixer fds, it requires that we should call
105 * snd_mixer_handle_events to clear all pending mixer events.
106 * However, when using the glib IO channels approach, we don't have
107 * poll() and snd_mixer_poll_descriptors_revents(). Due to the design of
108 * glib, on_mixer_event() will be called for every fd whose status was
109 * changed. So, after each poll(), it's called for several times,
110 * not just once. Therefore, we cannot call snd_mixer_handle_events()
111 * directly in the event handler. Otherwise, it will get called for
112 * several times, which might clear unprocessed pending events in the queue.
113 * So, here we call it once in the event callback for the first fd.
114 * Then, we don't call it for the following fds. After all fds with changed
115 * status are handled, we remove this restriction in an idle handler.
116 * The next time the event callback is involked for the first fs, we can
117 * call snd_mixer_handle_events() again. Racing shouldn't happen here
118 * because the idle handler has the same priority as the io channel callback.
119 * So, io callbacks for future pending events should be in the next gmain
120 * iteration, and won't be affected.
121 */
122static gboolean reset_mixer_evt_idle( volume_t* vol )
123{
124 vol->mixer_evt_idle = 0;
125 return FALSE;
126}
127
7486d297 128static gboolean on_mixer_event( GIOChannel* channel, GIOCondition cond, gpointer vol_gpointer)
96dc8a45 129{
7486d297 130 volume_t *vol = (volume_t *)(vol_gpointer);
96dc8a45
DB
131 if( 0 == vol->mixer_evt_idle )
132 {
133 vol->mixer_evt_idle = g_idle_add_full( G_PRIORITY_DEFAULT, (GSourceFunc)reset_mixer_evt_idle, vol, NULL );
134 snd_mixer_handle_events( vol->mixer );
135 }
136
137 if( cond & G_IO_IN )
138 {
139 /* the status of mixer is changed. update of display is needed. */
140 update_display( vol );
141 }
142 if( cond & G_IO_HUP )
143 {
144 /* FIXME: This means there're some problems with alsa. */
145
146 return FALSE;
147 }
148
149 return TRUE;
150}
151
6cc5e1a6
DB
152static gboolean asound_init(volume_t *vol)
153{
96dc8a45
DB
154 int i, n_fds;
155 struct pollfd *fds;
156
6cc5e1a6
DB
157 snd_mixer_selem_id_alloca(&vol->sid);
158 snd_mixer_open(&vol->mixer, 0);
159 snd_mixer_attach(vol->mixer, "default");
160 snd_mixer_selem_register(vol->mixer, NULL, NULL);
161 snd_mixer_load(vol->mixer);
162
163 /* Find Master element */
164 if (!find_element(vol, "Master"))
165 if (!find_element(vol, "Front"))
166 if (!find_element(vol, "PCM"))
39c13576
DB
167 if (!find_element(vol, "LineOut"))
168 return FALSE;
6cc5e1a6
DB
169
170
171 snd_mixer_selem_get_playback_volume_range(vol->master_element, &vol->alsa_min_vol, &vol->alsa_max_vol);
172
173 snd_mixer_selem_set_playback_volume_range(vol->master_element, 0, 100);
174
96dc8a45
DB
175 /* listen to events from alsa */
176 n_fds = snd_mixer_poll_descriptors_count( vol->mixer );
177 fds = g_new0( struct pollfd, n_fds );
178
179 snd_mixer_poll_descriptors( vol->mixer, fds, n_fds );
180 for( i = 0; i < n_fds; ++i )
181 {
182 /* g_debug("fd=%d", fds[i]); */
183 GIOChannel* channel = g_io_channel_unix_new( fds[i].fd );
184 g_io_add_watch( channel, G_IO_IN|G_IO_HUP, on_mixer_event, vol );
185 g_io_channel_unref( channel );
186 }
187 g_free( fds );
6cc5e1a6
DB
188 return TRUE;
189}
190
96dc8a45 191int asound_read(volume_t *vol)
6cc5e1a6
DB
192{
193 long aleft, aright;
6cc5e1a6
DB
194 /* Left */
195 snd_mixer_selem_get_playback_volume(vol->master_element, SND_MIXER_SCHN_FRONT_LEFT, &aleft);
196 /* Right */
197 snd_mixer_selem_get_playback_volume(vol->master_element, SND_MIXER_SCHN_FRONT_RIGHT, &aright);
198
199 return (aleft + aright) >> 1;
200}
201
96dc8a45 202void asound_write(volume_t *vol, int volume)
6cc5e1a6
DB
203{
204 snd_mixer_selem_set_playback_volume(vol->master_element, SND_MIXER_SCHN_FRONT_LEFT, volume);
205 snd_mixer_selem_set_playback_volume(vol->master_element, SND_MIXER_SCHN_FRONT_RIGHT, volume);
206}
207
208static gboolean focus_out_event(GtkWidget *widget, GdkEvent *event, volume_t *vol)
209{
210 gtk_widget_hide(vol->dlg);
211 vol->show = 0;
212 return FALSE;
213}
214
215static gboolean tray_icon_press(GtkWidget *widget, GdkEventButton *event, volume_t *vol)
216{
217 if( event->button == 3 ) /* right button */
218 {
219 GtkMenu* popup = lxpanel_get_panel_menu( vol->plugin->panel, vol->plugin, FALSE );
220 gtk_menu_popup( popup, NULL, NULL, NULL, NULL, event->button, event->time );
221 return TRUE;
222 }
223
224 if (vol->show==0) {
225 gtk_window_set_position(GTK_WINDOW(vol->dlg), GTK_WIN_POS_MOUSE);
6cc5e1a6
DB
226 gtk_widget_show_all(vol->dlg);
227 vol->show = 1;
228 } else {
229 gtk_widget_hide(vol->dlg);
230 vol->show = 0;
231 }
232 return TRUE;
233}
234
235static void on_vscale_value_changed(GtkRange *range, volume_t *vol)
236{
237 asound_write(vol, gtk_range_get_value(range));
238}
239
96dc8a45
DB
240static void on_vscale_scrolled( GtkScale* scale, GdkEventScroll *evt, volume_t* vol )
241{
242 gdouble val = gtk_range_get_value((GtkRange*)scale);
243 switch( evt->direction )
244 {
245 case GDK_SCROLL_UP:
246 val += 2;
247 break;
248 case GDK_SCROLL_DOWN:
249 val -= 2;
250 break;
251 }
252 gtk_range_set_value((GtkRange*)scale, CLAMP((int)val, 0, 100) );
253}
254
6cc5e1a6
DB
255static void click_mute(GtkWidget *widget, volume_t *vol)
256{
257 int chn;
258
259 if (gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(widget))) {
7486d297 260 gtk_image_set_from_file(GTK_IMAGE(vol->tray_icon), ICONS_MUTE);
6cc5e1a6
DB
261 for (chn = 0; chn <= SND_MIXER_SCHN_LAST; chn++) {
262 snd_mixer_selem_set_playback_switch(vol->master_element, chn, 0);
263 }
264 } else {
7486d297 265 gtk_image_set_from_file(GTK_IMAGE(vol->tray_icon), ICONS_VOLUME);
6cc5e1a6
DB
266 for (chn = 0; chn <= SND_MIXER_SCHN_LAST; chn++) {
267 snd_mixer_selem_set_playback_switch(vol->master_element, chn, 1);
268 }
269 }
270}
271
272static void panel_init(Plugin *p)
273{
274 volume_t *vol = p->priv;
275 GtkWidget *scrolledwindow;
276 GtkWidget *viewport;
277 GtkWidget *box;
278 GtkWidget *frame;
6cc5e1a6
DB
279
280 /* set show flags */
281 vol->show = 0;
282
283 /* create a new window */
284 vol->dlg = gtk_window_new(GTK_WINDOW_TOPLEVEL);
285 gtk_window_set_decorated(GTK_WINDOW(vol->dlg), FALSE);
286 gtk_container_set_border_width(GTK_CONTAINER(vol->dlg), 5);
287 gtk_window_set_default_size(GTK_WINDOW(vol->dlg), 80, 140);
288 gtk_window_set_skip_taskbar_hint(GTK_WINDOW(vol->dlg), TRUE);
289 gtk_window_set_skip_pager_hint(GTK_WINDOW(vol->dlg), TRUE);
290 gtk_window_set_type_hint(GTK_WINDOW(vol->dlg), GDK_WINDOW_TYPE_HINT_DIALOG);
291
6cc5e1a6
DB
292 /* Focus-out signal */
293 g_signal_connect (G_OBJECT (vol->dlg), "focus_out_event",
294 G_CALLBACK (focus_out_event), vol);
295
296 scrolledwindow = gtk_scrolled_window_new(NULL, NULL);
297 gtk_container_set_border_width (GTK_CONTAINER (scrolledwindow), 0);
298 gtk_widget_show (scrolledwindow);
299 gtk_container_add (GTK_CONTAINER (vol->dlg), scrolledwindow);
300 GTK_WIDGET_UNSET_FLAGS (scrolledwindow, GTK_CAN_FOCUS);
301 gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW (scrolledwindow), GTK_POLICY_NEVER, GTK_POLICY_NEVER);
302 gtk_scrolled_window_set_shadow_type(GTK_SCROLLED_WINDOW(scrolledwindow), GTK_SHADOW_NONE);
303
304 viewport = gtk_viewport_new (NULL, NULL);
305 gtk_container_add (GTK_CONTAINER (scrolledwindow), viewport);
306 gtk_viewport_set_shadow_type (GTK_VIEWPORT (viewport), GTK_SHADOW_NONE);
307 gtk_widget_show(viewport);
308
309 /* create frame */
310 frame = gtk_frame_new(_("Volume"));
311 gtk_frame_set_shadow_type(GTK_FRAME(frame), GTK_SHADOW_IN);
312 gtk_container_add(GTK_CONTAINER(viewport), frame);
313
314 /* create box */
315 box = gtk_vbox_new(FALSE, 0);
316
317 /* create controller */
318 vol->vscale = gtk_vscale_new(GTK_ADJUSTMENT(gtk_adjustment_new(asound_read(vol), 0, 100, 0, 0, 0)));
319 gtk_scale_set_draw_value(GTK_SCALE(vol->vscale), FALSE);
320 gtk_range_set_inverted(GTK_RANGE(vol->vscale), TRUE);
321
96dc8a45
DB
322 vol->vscale_handler = g_signal_connect ((gpointer) vol->vscale, "value_changed",
323 G_CALLBACK (on_vscale_value_changed),
324 vol);
325 g_signal_connect( vol->vscale, "scroll-event", G_CALLBACK(on_vscale_scrolled), vol );
6cc5e1a6 326
96dc8a45 327 vol->mute_check = gtk_check_button_new_with_label(_("Mute"));
6cc5e1a6
DB
328 snd_mixer_selem_get_playback_switch(vol->master_element, 0, &vol->mute);
329
96dc8a45
DB
330 vol->mute_handler = g_signal_connect ((gpointer) vol->mute_check, "toggled",
331 G_CALLBACK (click_mute),
332 vol);
6cc5e1a6
DB
333
334 gtk_box_pack_start(GTK_BOX(box), vol->vscale, TRUE, TRUE, 0);
96dc8a45 335 gtk_box_pack_end(GTK_BOX(box), vol->mute_check, FALSE, FALSE, 0);
6cc5e1a6
DB
336 gtk_container_add(GTK_CONTAINER(frame), box);
337
338 /* setting background to default */
339 gtk_widget_set_style(viewport, p->panel->defstyle);
340}
341
342static void
343volumealsa_destructor(Plugin *p)
344{
345 volume_t *vol = (volume_t *) p->priv;
346
347 ENTER;
96dc8a45
DB
348
349 if( vol->mixer_evt_idle )
350 g_source_remove( vol->mixer_evt_idle );
351
6cc5e1a6
DB
352 if (vol->dlg)
353 gtk_widget_destroy(vol->dlg);
6cc5e1a6
DB
354
355 g_free(vol);
356 RET();
357}
358
359static int
360volumealsa_constructor(Plugin *p, char **fp)
361{
362 volume_t *vol;
363 line s;
364 GdkPixbuf *icon;
365 GtkWidget *image;
366 GtkIconTheme* theme;
367 GtkIconInfo* info;
368
369 ENTER;
370 s.len = 256;
371 vol = g_new0(volume_t, 1);
372 vol->plugin = p;
373 g_return_val_if_fail(vol != NULL, 0);
374 p->priv = vol;
375
376 /* initializing */
377 if (!asound_init(vol))
378 RET(1);
379
380 panel_init(p);
381
382 /* main */
383 vol->mainw = gtk_event_box_new();
384
385 gtk_widget_add_events(vol->mainw, GDK_BUTTON_PRESS_MASK);
386 gtk_widget_set_size_request( vol->mainw, 24, 24 );
387
388 g_signal_connect(G_OBJECT(vol->mainw), "button-press-event",
389 G_CALLBACK(tray_icon_press), vol);
390
391 /* tray icon */
96dc8a45
DB
392 vol->tray_icon = gtk_image_new();
393 update_display( vol );
6cc5e1a6
DB
394
395 gtk_container_add(GTK_CONTAINER(vol->mainw), vol->tray_icon);
396
397 gtk_widget_show_all(vol->mainw);
398
6cc5e1a6 399 /* FIXME: display current level in tooltip. ex: "Volume Control: 80%" */
7486d297 400 gtk_widget_set_tooltip_text( vol->mainw, _("Volume control"));
6cc5e1a6
DB
401
402 /* store the created plugin widget in plugin->pwid */
403 p->pwid = vol->mainw;
404
405 RET(1);
406}
407
408
409PluginClass volumealsa_plugin_class = {
410 fname: NULL,
411 count: 0,
412
413 type : "volumealsa",
414 name : N_("Volume Control"),
415 version: "1.0",
416 description : "Display and control volume for ALSA",
417
418 constructor : volumealsa_constructor,
419 destructor : volumealsa_destructor,
420 config : NULL,
421 save : NULL
422};