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