Fix build for mismatched \n in po file
[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
38 /* Graphics. */
39 Plugin * plugin; /* Back pointer to plugin */
40 GtkWidget * tray_icon; /* Displayed image */
41 GtkWidget * popup_window; /* Top level window for popup */
42 GtkWidget * volume_scale; /* Scale for volume */
43 GtkWidget * mute_check; /* Checkbox for mute state */
44 gboolean show_popup; /* Toggle to show and hide the popup on left click */
45 guint volume_scale_handler; /* Handler for vscale widget */
46 guint mute_check_handler; /* Handler for mute_check widget */
47
48 /* ALSA interface. */
49 snd_mixer_t * mixer; /* The mixer */
50 snd_mixer_selem_id_t * sid; /* The element ID */
51 snd_mixer_elem_t * master_element; /* The Master element */
52 guint mixer_evt_idle; /* Timer to handle restarting poll */
53 } VolumeALSAPlugin;
54
55 static gboolean asound_find_element(VolumeALSAPlugin * vol, const char * ename);
56 static gboolean asound_reset_mixer_evt_idle(VolumeALSAPlugin * vol);
57 static gboolean asound_mixer_event(GIOChannel * channel, GIOCondition cond, gpointer vol_gpointer);
58 static gboolean asound_initialize(VolumeALSAPlugin * vol);
59 static gboolean asound_has_mute(VolumeALSAPlugin * vol);
60 static gboolean asound_is_muted(VolumeALSAPlugin * vol);
61 static int asound_get_volume(VolumeALSAPlugin * vol);
62 static void asound_set_volume(VolumeALSAPlugin * vol, int volume);
63 static void volumealsa_update_display(VolumeALSAPlugin * vol);
64 static gboolean volumealsa_button_press_event(GtkWidget * widget, GdkEventButton * event, VolumeALSAPlugin * vol);
65 static gboolean volumealsa_popup_focus_out(GtkWidget * widget, GdkEvent * event, VolumeALSAPlugin * vol);
66 static void volumealsa_popup_scale_changed(GtkRange * range, VolumeALSAPlugin * vol);
67 static void volumealsa_popup_scale_scrolled(GtkScale * scale, GdkEventScroll * evt, VolumeALSAPlugin * vol);
68 static void volumealsa_popup_mute_toggled(GtkWidget * widget, VolumeALSAPlugin * vol);
69 static void volumealsa_build_popup_window(Plugin * p);
70 static int volumealsa_constructor(Plugin * p, char ** fp);
71 static void volumealsa_destructor(Plugin * p);
72 static void volumealsa_panel_configuration_changed(Plugin * p);
73
74 /*** ALSA ***/
75
76 static gboolean asound_find_element(VolumeALSAPlugin * vol, const char * ename)
77 {
78 for (
79 vol->master_element = snd_mixer_first_elem(vol->mixer);
80 vol->master_element != NULL;
81 vol->master_element = snd_mixer_elem_next(vol->master_element))
82 {
83 snd_mixer_selem_get_id(vol->master_element, vol->sid);
84 if ((snd_mixer_selem_is_active(vol->master_element))
85 && (strcmp(ename, snd_mixer_selem_id_get_name(vol->sid)) == 0))
86 return TRUE;
87 }
88 return FALSE;
89 }
90
91 /* NOTE by PCMan:
92 * This is magic! Since ALSA uses its own machanism to handle this part.
93 * After polling of mixer fds, it requires that we should call
94 * snd_mixer_handle_events to clear all pending mixer events.
95 * However, when using the glib IO channels approach, we don't have
96 * poll() and snd_mixer_poll_descriptors_revents(). Due to the design of
97 * glib, on_mixer_event() will be called for every fd whose status was
98 * changed. So, after each poll(), it's called for several times,
99 * not just once. Therefore, we cannot call snd_mixer_handle_events()
100 * directly in the event handler. Otherwise, it will get called for
101 * several times, which might clear unprocessed pending events in the queue.
102 * So, here we call it once in the event callback for the first fd.
103 * Then, we don't call it for the following fds. After all fds with changed
104 * status are handled, we remove this restriction in an idle handler.
105 * The next time the event callback is involked for the first fs, we can
106 * call snd_mixer_handle_events() again. Racing shouldn't happen here
107 * because the idle handler has the same priority as the io channel callback.
108 * So, io callbacks for future pending events should be in the next gmain
109 * iteration, and won't be affected.
110 */
111
112 static gboolean asound_reset_mixer_evt_idle(VolumeALSAPlugin * vol)
113 {
114 vol->mixer_evt_idle = 0;
115 return FALSE;
116 }
117
118 /* Handler for I/O event on ALSA channel. */
119 static gboolean asound_mixer_event(GIOChannel * channel, GIOCondition cond, gpointer vol_gpointer)
120 {
121 VolumeALSAPlugin * vol = (VolumeALSAPlugin *) vol_gpointer;
122
123 if (vol->mixer_evt_idle == 0)
124 {
125 vol->mixer_evt_idle = g_idle_add_full(G_PRIORITY_DEFAULT, (GSourceFunc) asound_reset_mixer_evt_idle, vol, NULL);
126 snd_mixer_handle_events(vol->mixer);
127 }
128
129 if (cond & G_IO_IN)
130 {
131 /* the status of mixer is changed. update of display is needed. */
132 volumealsa_update_display(vol);
133 }
134
135 if (cond & G_IO_HUP)
136 {
137 /* This means there're some problems with alsa. */
138 return FALSE;
139 }
140
141 return TRUE;
142 }
143
144 /* Initialize the ALSA interface. */
145 static gboolean asound_initialize(VolumeALSAPlugin * vol)
146 {
147 /* Access the "default" device. */
148 snd_mixer_selem_id_alloca(&vol->sid);
149 snd_mixer_open(&vol->mixer, 0);
150 snd_mixer_attach(vol->mixer, "default");
151 snd_mixer_selem_register(vol->mixer, NULL, NULL);
152 snd_mixer_load(vol->mixer);
153
154 /* Find Master element, or Front element, or PCM element, or LineOut element. */
155 if ( ! asound_find_element(vol, "Master"))
156 if ( ! asound_find_element(vol, "Front"))
157 if ( ! asound_find_element(vol, "PCM"))
158 if ( ! asound_find_element(vol, "LineOut"))
159 return FALSE;
160
161 /* Set the playback volume range as we wish it. */
162 snd_mixer_selem_set_playback_volume_range(vol->master_element, 0, 100);
163
164 /* Listen to events from ALSA. */
165 int n_fds = snd_mixer_poll_descriptors_count(vol->mixer);
166 struct pollfd * fds = g_new0(struct pollfd, n_fds);
167
168 snd_mixer_poll_descriptors(vol->mixer, fds, n_fds);
169 int i;
170 for (i = 0; i < n_fds; ++i)
171 {
172 GIOChannel* channel = g_io_channel_unix_new(fds[i].fd);
173 g_io_add_watch(channel, G_IO_IN | G_IO_HUP, asound_mixer_event, vol);
174 g_io_channel_unref(channel);
175 }
176 g_free(fds);
177 return TRUE;
178 }
179
180 /* Get the presence of the mute control from the sound system. */
181 static gboolean asound_has_mute(VolumeALSAPlugin * vol)
182 {
183 return snd_mixer_selem_has_playback_switch(vol->master_element);
184 }
185
186 /* Get the condition of the mute control from the sound system. */
187 static gboolean asound_is_muted(VolumeALSAPlugin * vol)
188 {
189 /* The switch is on if sound is not muted, and off if the sound is muted.
190 * Initialize so that the sound appears unmuted if the control does not exist. */
191 int value = 1;
192 snd_mixer_selem_get_playback_switch(vol->master_element, 0, &value);
193 return (value == 0);
194 }
195
196 /* Get the volume from the sound system.
197 * This implementation returns the average of the Front Left and Front Right channels. */
198 static int asound_get_volume(VolumeALSAPlugin * vol)
199 {
200 long aleft;
201 long aright;
202 snd_mixer_selem_get_playback_volume(vol->master_element, SND_MIXER_SCHN_FRONT_LEFT, &aleft);
203 snd_mixer_selem_get_playback_volume(vol->master_element, SND_MIXER_SCHN_FRONT_RIGHT, &aright);
204 return (aleft + aright) >> 1;
205 }
206
207 /* Set the volume to the sound system.
208 * This implementation sets the Front Left and Front Right channels to the specified value. */
209 static void asound_set_volume(VolumeALSAPlugin * vol, int volume)
210 {
211 snd_mixer_selem_set_playback_volume(vol->master_element, SND_MIXER_SCHN_FRONT_LEFT, volume);
212 snd_mixer_selem_set_playback_volume(vol->master_element, SND_MIXER_SCHN_FRONT_RIGHT, volume);
213 }
214
215 /*** Graphics ***/
216
217 /* Do a full redraw of the display. */
218 static void volumealsa_update_display(VolumeALSAPlugin * vol)
219 {
220 /* Mute status. */
221 gboolean mute = asound_is_muted(vol);
222
223 if ( ! panel_image_set_icon_theme(vol->plugin->panel, vol->tray_icon, ((mute) ? "audio-volume-muted" : "audio-volume-high")))
224 {
225 panel_image_set_from_file(vol->plugin->panel, vol->tray_icon, ((mute) ? ICONS_MUTE : ICONS_VOLUME));
226 }
227
228 g_signal_handler_block(vol->mute_check, vol->mute_check_handler);
229 gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(vol->mute_check), mute);
230 gtk_widget_set_sensitive(vol->mute_check, asound_has_mute(vol));
231 g_signal_handler_unblock(vol->mute_check, vol->mute_check_handler);
232
233 /* Volume. */
234 int level = asound_get_volume(vol);
235 if (vol->volume_scale != NULL)
236 {
237 g_signal_handler_block(vol->volume_scale, vol->volume_scale_handler);
238 gtk_range_set_value(GTK_RANGE(vol->volume_scale), asound_get_volume(vol));
239 g_signal_handler_unblock(vol->volume_scale, vol->volume_scale_handler);
240 }
241
242 /* Display current level in tooltip. */
243 char * tooltip = g_strdup_printf("%s %d", _("Volume control"), level);
244 gtk_widget_set_tooltip_text(vol->plugin->pwid, tooltip);
245 g_free(tooltip);
246 }
247
248 /* Handler for "button-press-event" signal on main widget. */
249 static gboolean volumealsa_button_press_event(GtkWidget * widget, GdkEventButton * event, VolumeALSAPlugin * vol)
250 {
251 /* Standard right-click handling. */
252 if (plugin_button_press_event(widget, event, vol->plugin))
253 return TRUE;
254
255 /* Left-click. Show or hide the popup window. */
256 if (event->button == 1)
257 {
258 if (vol->show_popup)
259 {
260 gtk_widget_hide(vol->popup_window);
261 vol->show_popup = FALSE;
262 }
263 else
264 {
265 gtk_window_set_position(GTK_WINDOW(vol->popup_window), GTK_WIN_POS_MOUSE);
266 gtk_widget_show_all(vol->popup_window);
267 vol->show_popup = TRUE;
268 }
269 }
270
271 /* Middle-click. Toggle the mute status. */
272 else if (event->button == 2)
273 {
274 gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(vol->mute_check), ! gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(vol->mute_check)));
275 }
276 return TRUE;
277 }
278
279 /* Handler for "focus-out" signal on popup window. */
280 static gboolean volumealsa_popup_focus_out(GtkWidget * widget, GdkEvent * event, VolumeALSAPlugin * vol)
281 {
282 /* Hide the widget. */
283 gtk_widget_hide(vol->popup_window);
284 vol->show_popup = FALSE;
285 return FALSE;
286 }
287
288 /* Handler for "value_changed" signal on popup window vertical scale. */
289 static void volumealsa_popup_scale_changed(GtkRange * range, VolumeALSAPlugin * vol)
290 {
291 /* Reflect the value of the control to the sound system. */
292 asound_set_volume(vol, gtk_range_get_value(range));
293
294 /* Redraw the controls. */
295 volumealsa_update_display(vol);
296 }
297
298 /* Handler for "scroll-event" signal on popup window vertical scale. */
299 static void volumealsa_popup_scale_scrolled(GtkScale * scale, GdkEventScroll * evt, VolumeALSAPlugin * vol)
300 {
301 /* Get the state of the vertical scale. */
302 gdouble val = gtk_range_get_value(GTK_RANGE(vol->volume_scale));
303
304 /* Dispatch on scroll direction to update the value. */
305 if ((evt->direction == GDK_SCROLL_UP) || (evt->direction == GDK_SCROLL_LEFT))
306 val += 2;
307 else
308 val -= 2;
309
310 /* Reset the state of the vertical scale. This provokes a "value_changed" event. */
311 gtk_range_set_value(GTK_RANGE(vol->volume_scale), CLAMP((int)val, 0, 100));
312 }
313
314 /* Handler for "toggled" signal on popup window mute checkbox. */
315 static void volumealsa_popup_mute_toggled(GtkWidget * widget, VolumeALSAPlugin * vol)
316 {
317 /* Get the state of the mute toggle. */
318 gboolean active = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(widget));
319
320 /* Reflect the mute toggle to the sound system. */
321 int chn;
322 for (chn = 0; chn <= SND_MIXER_SCHN_LAST; chn++)
323 snd_mixer_selem_set_playback_switch(vol->master_element, chn, ((active) ? 0 : 1));
324
325 /* Redraw the controls. */
326 volumealsa_update_display(vol);
327 }
328
329 /* Build the window that appears when the top level widget is clicked. */
330 static void volumealsa_build_popup_window(Plugin * p)
331 {
332 VolumeALSAPlugin * vol = p->priv;
333
334 /* Create a new window. */
335 vol->popup_window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
336 gtk_window_set_decorated(GTK_WINDOW(vol->popup_window), FALSE);
337 gtk_container_set_border_width(GTK_CONTAINER(vol->popup_window), 5);
338 gtk_window_set_default_size(GTK_WINDOW(vol->popup_window), 80, 140);
339 gtk_window_set_skip_taskbar_hint(GTK_WINDOW(vol->popup_window), TRUE);
340 gtk_window_set_skip_pager_hint(GTK_WINDOW(vol->popup_window), TRUE);
341 gtk_window_set_type_hint(GTK_WINDOW(vol->popup_window), GDK_WINDOW_TYPE_HINT_DIALOG);
342
343 /* Focus-out signal. */
344 g_signal_connect(G_OBJECT(vol->popup_window), "focus_out_event", G_CALLBACK(volumealsa_popup_focus_out), vol);
345
346 /* Create a scrolled window as the child of the top level window. */
347 GtkWidget * scrolledwindow = gtk_scrolled_window_new(NULL, NULL);
348 gtk_container_set_border_width (GTK_CONTAINER(scrolledwindow), 0);
349 gtk_widget_show(scrolledwindow);
350 gtk_container_add(GTK_CONTAINER(vol->popup_window), scrolledwindow);
351 GTK_WIDGET_UNSET_FLAGS(scrolledwindow, GTK_CAN_FOCUS);
352 gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW (scrolledwindow), GTK_POLICY_NEVER, GTK_POLICY_NEVER);
353 gtk_scrolled_window_set_shadow_type(GTK_SCROLLED_WINDOW(scrolledwindow), GTK_SHADOW_NONE);
354
355 /* Create a viewport as the child of the scrolled window. */
356 GtkWidget * viewport = gtk_viewport_new(NULL, NULL);
357 gtk_container_add(GTK_CONTAINER(scrolledwindow), viewport);
358 gtk_viewport_set_shadow_type(GTK_VIEWPORT(viewport), GTK_SHADOW_NONE);
359 gtk_widget_show(viewport);
360
361 /* Create a frame as the child of the viewport. */
362 GtkWidget * frame = gtk_frame_new(_("Volume"));
363 gtk_frame_set_shadow_type(GTK_FRAME(frame), GTK_SHADOW_IN);
364 gtk_container_add(GTK_CONTAINER(viewport), frame);
365
366 /* Create a vertical box as the child of the frame. */
367 GtkWidget * box = gtk_vbox_new(FALSE, 0);
368 gtk_container_add(GTK_CONTAINER(frame), box);
369
370 /* Create a vertical scale as the child of the vertical box. */
371 vol->volume_scale = gtk_vscale_new(GTK_ADJUSTMENT(gtk_adjustment_new(100, 0, 100, 0, 0, 0)));
372 gtk_scale_set_draw_value(GTK_SCALE(vol->volume_scale), FALSE);
373 gtk_range_set_inverted(GTK_RANGE(vol->volume_scale), TRUE);
374 gtk_box_pack_start(GTK_BOX(box), vol->volume_scale, TRUE, TRUE, 0);
375
376 /* Value-changed and scroll-event signals. */
377 vol->volume_scale_handler = g_signal_connect(vol->volume_scale, "value_changed", G_CALLBACK(volumealsa_popup_scale_changed), vol);
378 g_signal_connect(vol->volume_scale, "scroll-event", G_CALLBACK(volumealsa_popup_scale_scrolled), vol);
379
380 /* Create a check button as the child of the vertical box. */
381 vol->mute_check = gtk_check_button_new_with_label(_("Mute"));
382 gtk_box_pack_end(GTK_BOX(box), vol->mute_check, FALSE, FALSE, 0);
383 vol->mute_check_handler = g_signal_connect(vol->mute_check, "toggled", G_CALLBACK(volumealsa_popup_mute_toggled), vol);
384
385 /* Set background to default. */
386 gtk_widget_set_style(viewport, p->panel->defstyle);
387 }
388
389 /* Plugin constructor. */
390 static int volumealsa_constructor(Plugin * p, char ** fp)
391 {
392 /* Allocate and initialize plugin context and set into Plugin private data pointer. */
393 VolumeALSAPlugin * vol = g_new0(VolumeALSAPlugin, 1);
394 vol->plugin = p;
395 p->priv = vol;
396
397 /* Initialize ALSA. If that fails, present nothing. */
398 if ( ! asound_initialize(vol))
399 return 1;
400
401 /* Allocate top level widget and set into Plugin widget pointer. */
402 p->pwid = gtk_event_box_new();
403 gtk_widget_add_events(p->pwid, GDK_BUTTON_PRESS_MASK);
404 gtk_widget_set_tooltip_text(p->pwid, _("Volume control"));
405
406 /* Allocate icon as a child of top level. */
407 vol->tray_icon = gtk_image_new();
408 gtk_container_add(GTK_CONTAINER(p->pwid), vol->tray_icon);
409
410 /* Initialize window to appear when icon clicked. */
411 volumealsa_build_popup_window(p);
412
413 /* Connect signals. */
414 g_signal_connect(G_OBJECT(p->pwid), "button-press-event", G_CALLBACK(volumealsa_button_press_event), vol);
415 g_signal_connect(G_OBJECT(p->pwid), "scroll-event", G_CALLBACK(volumealsa_popup_scale_scrolled), vol );
416
417 /* Update the display, show the widget, and return. */
418 volumealsa_update_display(vol);
419 gtk_widget_show_all(p->pwid);
420 return 1;
421 }
422
423 /* Plugin destructor. */
424 static void volumealsa_destructor(Plugin * p)
425 {
426 VolumeALSAPlugin * vol = (VolumeALSAPlugin *) p->priv;
427
428 /* Remove the periodic timer. */
429 if (vol->mixer_evt_idle != 0)
430 g_source_remove(vol->mixer_evt_idle);
431
432 /* If the dialog box is open, dismiss it. */
433 if (vol->popup_window != NULL)
434 gtk_widget_destroy(vol->popup_window);
435
436 /* Deallocate all memory. */
437 g_free(vol);
438 }
439
440 /* Callback when panel configuration changes. */
441 static void volumealsa_panel_configuration_changed(Plugin * p)
442 {
443 /* Do a full redraw. */
444 volumealsa_update_display((VolumeALSAPlugin *) p->priv);
445 }
446
447 /* Plugin descriptor. */
448 PluginClass volumealsa_plugin_class = {
449
450 PLUGINCLASS_VERSIONING,
451
452 type : "volumealsa",
453 name : N_("Volume Control"),
454 version: "1.0",
455 description : "Display and control volume for ALSA",
456
457 constructor : volumealsa_constructor,
458 destructor : volumealsa_destructor,
459 config : NULL,
460 save : NULL,
461 panel_configuration_changed : volumealsa_panel_configuration_changed
462
463 };