Adding upstream version 0.5.4.1.
[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 {
2ba86315
DB
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
55static gboolean asound_find_element(VolumeALSAPlugin * vol, const char * ename);
56static gboolean asound_reset_mixer_evt_idle(VolumeALSAPlugin * vol);
57static gboolean asound_mixer_event(GIOChannel * channel, GIOCondition cond, gpointer vol_gpointer);
58static gboolean asound_initialize(VolumeALSAPlugin * vol);
59static gboolean asound_has_mute(VolumeALSAPlugin * vol);
60static gboolean asound_is_muted(VolumeALSAPlugin * vol);
61static int asound_get_volume(VolumeALSAPlugin * vol);
62static void asound_set_volume(VolumeALSAPlugin * vol, int volume);
63static void volumealsa_update_display(VolumeALSAPlugin * vol);
64static gboolean volumealsa_button_press_event(GtkWidget * widget, GdkEventButton * event, VolumeALSAPlugin * vol);
65static gboolean volumealsa_popup_focus_out(GtkWidget * widget, GdkEvent * event, VolumeALSAPlugin * vol);
66static void volumealsa_popup_scale_changed(GtkRange * range, VolumeALSAPlugin * vol);
67static void volumealsa_popup_scale_scrolled(GtkScale * scale, GdkEventScroll * evt, VolumeALSAPlugin * vol);
68static void volumealsa_popup_mute_toggled(GtkWidget * widget, VolumeALSAPlugin * vol);
69static void volumealsa_build_popup_window(Plugin * p);
70static int volumealsa_constructor(Plugin * p, char ** fp);
71static void volumealsa_destructor(Plugin * p);
72static void volumealsa_panel_configuration_changed(Plugin * p);
73
74/*** ALSA ***/
75
76static gboolean asound_find_element(VolumeALSAPlugin * vol, const char * ename)
6cc5e1a6 77{
2ba86315
DB
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 {
6cc5e1a6 83 snd_mixer_selem_get_id(vol->master_element, vol->sid);
2ba86315
DB
84 if ((snd_mixer_selem_is_active(vol->master_element))
85 && (strcmp(ename, snd_mixer_selem_id_get_name(vol->sid)) == 0))
6cc5e1a6 86 return TRUE;
6cc5e1a6 87 }
6cc5e1a6
DB
88 return FALSE;
89}
90
96dc8a45
DB
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 */
2ba86315
DB
111
112static gboolean asound_reset_mixer_evt_idle(VolumeALSAPlugin * vol)
96dc8a45
DB
113{
114 vol->mixer_evt_idle = 0;
115 return FALSE;
116}
117
2ba86315
DB
118/* Handler for I/O event on ALSA channel. */
119static gboolean asound_mixer_event(GIOChannel * channel, GIOCondition cond, gpointer vol_gpointer)
96dc8a45 120{
2ba86315
DB
121 VolumeALSAPlugin * vol = (VolumeALSAPlugin *) vol_gpointer;
122
123 if (vol->mixer_evt_idle == 0)
96dc8a45 124 {
2ba86315
DB
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);
96dc8a45
DB
127 }
128
2ba86315 129 if (cond & G_IO_IN)
96dc8a45
DB
130 {
131 /* the status of mixer is changed. update of display is needed. */
2ba86315 132 volumealsa_update_display(vol);
96dc8a45 133 }
96dc8a45 134
2ba86315
DB
135 if (cond & G_IO_HUP)
136 {
137 /* This means there're some problems with alsa. */
96dc8a45
DB
138 return FALSE;
139 }
140
141 return TRUE;
142}
143
2ba86315
DB
144/* Initialize the ALSA interface. */
145static gboolean asound_initialize(VolumeALSAPlugin * vol)
6cc5e1a6 146{
2ba86315 147 /* Access the "default" device. */
6cc5e1a6
DB
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
2ba86315
DB
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"))
39c13576 159 return FALSE;
6cc5e1a6 160
2ba86315 161 /* Set the playback volume range as we wish it. */
6cc5e1a6
DB
162 snd_mixer_selem_set_playback_volume_range(vol->master_element, 0, 100);
163
2ba86315
DB
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);
96dc8a45 167
2ba86315
DB
168 snd_mixer_poll_descriptors(vol->mixer, fds, n_fds);
169 int i;
170 for (i = 0; i < n_fds; ++i)
96dc8a45 171 {
2ba86315
DB
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);
96dc8a45 175 }
2ba86315 176 g_free(fds);
6cc5e1a6
DB
177 return TRUE;
178}
179
2ba86315
DB
180/* Get the presence of the mute control from the sound system. */
181static 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. */
187static gboolean asound_is_muted(VolumeALSAPlugin * vol)
6cc5e1a6 188{
2ba86315
DB
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. */
198static int asound_get_volume(VolumeALSAPlugin * vol)
199{
200 long aleft;
201 long aright;
6cc5e1a6 202 snd_mixer_selem_get_playback_volume(vol->master_element, SND_MIXER_SCHN_FRONT_LEFT, &aleft);
6cc5e1a6 203 snd_mixer_selem_get_playback_volume(vol->master_element, SND_MIXER_SCHN_FRONT_RIGHT, &aright);
6cc5e1a6
DB
204 return (aleft + aright) >> 1;
205}
206
2ba86315
DB
207/* Set the volume to the sound system.
208 * This implementation sets the Front Left and Front Right channels to the specified value. */
209static void asound_set_volume(VolumeALSAPlugin * vol, int volume)
6cc5e1a6
DB
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
2ba86315
DB
215/*** Graphics ***/
216
217/* Do a full redraw of the display. */
218static void volumealsa_update_display(VolumeALSAPlugin * vol)
6cc5e1a6 219{
2ba86315
DB
220 /* Mute status. */
221 gboolean mute = asound_is_muted(vol);
222 panel_image_set_from_file(vol->plugin->panel, vol->tray_icon, ((mute) ? ICONS_MUTE : ICONS_VOLUME));
223
224 g_signal_handler_block(vol->mute_check, vol->mute_check_handler);
225 gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(vol->mute_check), mute);
226 gtk_widget_set_sensitive(vol->mute_check, asound_has_mute(vol));
227 g_signal_handler_unblock(vol->mute_check, vol->mute_check_handler);
228
229 /* Volume. */
230 int level = asound_get_volume(vol);
231 if (vol->volume_scale != NULL)
232 {
233 g_signal_handler_block(vol->volume_scale, vol->volume_scale_handler);
234 gtk_range_set_value(GTK_RANGE(vol->volume_scale), asound_get_volume(vol));
235 g_signal_handler_unblock(vol->volume_scale, vol->volume_scale_handler);
236 }
237
238 /* Display current level in tooltip. */
239 char * tooltip = g_strdup_printf("%s %d", _("Volume control"), level);
240 gtk_widget_set_tooltip_text(vol->plugin->pwid, tooltip);
241 g_free(tooltip);
6cc5e1a6
DB
242}
243
2ba86315
DB
244/* Handler for "button-press-event" signal on main widget. */
245static gboolean volumealsa_button_press_event(GtkWidget * widget, GdkEventButton * event, VolumeALSAPlugin * vol)
6cc5e1a6 246{
2ba86315
DB
247 /* Standard right-click handling. */
248 if (plugin_button_press_event(widget, event, vol->plugin))
6cc5e1a6 249 return TRUE;
2ba86315
DB
250
251 /* Left-click. Show or hide the popup window. */
252 if (event->button == 1)
253 {
254 if (vol->show_popup)
255 {
256 gtk_widget_hide(vol->popup_window);
257 vol->show_popup = FALSE;
258 }
259 else
260 {
261 gtk_window_set_position(GTK_WINDOW(vol->popup_window), GTK_WIN_POS_MOUSE);
262 gtk_widget_show_all(vol->popup_window);
263 vol->show_popup = TRUE;
264 }
6cc5e1a6
DB
265 }
266
2ba86315
DB
267 /* Middle-click. Toggle the mute status. */
268 else if (event->button == 2)
269 {
270 gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(vol->mute_check), ! gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(vol->mute_check)));
6cc5e1a6
DB
271 }
272 return TRUE;
273}
274
2ba86315
DB
275/* Handler for "focus-out" signal on popup window. */
276static gboolean volumealsa_popup_focus_out(GtkWidget * widget, GdkEvent * event, VolumeALSAPlugin * vol)
6cc5e1a6 277{
2ba86315
DB
278 /* Hide the widget. */
279 gtk_widget_hide(vol->popup_window);
280 vol->show_popup = FALSE;
281 return FALSE;
6cc5e1a6
DB
282}
283
2ba86315
DB
284/* Handler for "value_changed" signal on popup window vertical scale. */
285static void volumealsa_popup_scale_changed(GtkRange * range, VolumeALSAPlugin * vol)
96dc8a45 286{
2ba86315
DB
287 /* Reflect the value of the control to the sound system. */
288 asound_set_volume(vol, gtk_range_get_value(range));
289
290 /* Redraw the controls. */
291 volumealsa_update_display(vol);
292}
293
294/* Handler for "scroll-event" signal on popup window vertical scale. */
295static void volumealsa_popup_scale_scrolled(GtkScale * scale, GdkEventScroll * evt, VolumeALSAPlugin * vol)
296{
297 /* Get the state of the vertical scale. */
298 gdouble val = gtk_range_get_value(GTK_RANGE(vol->volume_scale));
299
300 /* Dispatch on scroll direction to update the value. */
301 if ((evt->direction == GDK_SCROLL_UP) || (evt->direction == GDK_SCROLL_LEFT))
96dc8a45 302 val += 2;
2ba86315 303 else
96dc8a45 304 val -= 2;
2ba86315
DB
305
306 /* Reset the state of the vertical scale. This provokes a "value_changed" event. */
307 gtk_range_set_value(GTK_RANGE(vol->volume_scale), CLAMP((int)val, 0, 100));
96dc8a45
DB
308}
309
2ba86315
DB
310/* Handler for "toggled" signal on popup window mute checkbox. */
311static void volumealsa_popup_mute_toggled(GtkWidget * widget, VolumeALSAPlugin * vol)
6cc5e1a6 312{
2ba86315
DB
313 /* Get the state of the mute toggle. */
314 gboolean active = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(widget));
315
316 /* Reflect the mute toggle to the sound system. */
6cc5e1a6 317 int chn;
2ba86315
DB
318 for (chn = 0; chn <= SND_MIXER_SCHN_LAST; chn++)
319 snd_mixer_selem_set_playback_switch(vol->master_element, chn, ((active) ? 0 : 1));
6cc5e1a6 320
2ba86315
DB
321 /* Redraw the controls. */
322 volumealsa_update_display(vol);
6cc5e1a6
DB
323}
324
2ba86315
DB
325/* Build the window that appears when the top level widget is clicked. */
326static void volumealsa_build_popup_window(Plugin * p)
6cc5e1a6 327{
2ba86315
DB
328 VolumeALSAPlugin * vol = p->priv;
329
330 /* Create a new window. */
331 vol->popup_window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
332 gtk_window_set_decorated(GTK_WINDOW(vol->popup_window), FALSE);
333 gtk_container_set_border_width(GTK_CONTAINER(vol->popup_window), 5);
334 gtk_window_set_default_size(GTK_WINDOW(vol->popup_window), 80, 140);
335 gtk_window_set_skip_taskbar_hint(GTK_WINDOW(vol->popup_window), TRUE);
336 gtk_window_set_skip_pager_hint(GTK_WINDOW(vol->popup_window), TRUE);
337 gtk_window_set_type_hint(GTK_WINDOW(vol->popup_window), GDK_WINDOW_TYPE_HINT_DIALOG);
338
339 /* Focus-out signal. */
340 g_signal_connect(G_OBJECT(vol->popup_window), "focus_out_event", G_CALLBACK(volumealsa_popup_focus_out), vol);
341
342 /* Create a scrolled window as the child of the top level window. */
343 GtkWidget * scrolledwindow = gtk_scrolled_window_new(NULL, NULL);
344 gtk_container_set_border_width (GTK_CONTAINER(scrolledwindow), 0);
345 gtk_widget_show(scrolledwindow);
346 gtk_container_add(GTK_CONTAINER(vol->popup_window), scrolledwindow);
347 GTK_WIDGET_UNSET_FLAGS(scrolledwindow, GTK_CAN_FOCUS);
6cc5e1a6
DB
348 gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW (scrolledwindow), GTK_POLICY_NEVER, GTK_POLICY_NEVER);
349 gtk_scrolled_window_set_shadow_type(GTK_SCROLLED_WINDOW(scrolledwindow), GTK_SHADOW_NONE);
350
2ba86315
DB
351 /* Create a viewport as the child of the scrolled window. */
352 GtkWidget * viewport = gtk_viewport_new(NULL, NULL);
353 gtk_container_add(GTK_CONTAINER(scrolledwindow), viewport);
354 gtk_viewport_set_shadow_type(GTK_VIEWPORT(viewport), GTK_SHADOW_NONE);
6cc5e1a6
DB
355 gtk_widget_show(viewport);
356
2ba86315
DB
357 /* Create a frame as the child of the viewport. */
358 GtkWidget * frame = gtk_frame_new(_("Volume"));
6cc5e1a6
DB
359 gtk_frame_set_shadow_type(GTK_FRAME(frame), GTK_SHADOW_IN);
360 gtk_container_add(GTK_CONTAINER(viewport), frame);
361
2ba86315
DB
362 /* Create a vertical box as the child of the frame. */
363 GtkWidget * box = gtk_vbox_new(FALSE, 0);
364 gtk_container_add(GTK_CONTAINER(frame), box);
6cc5e1a6 365
2ba86315
DB
366 /* Create a vertical scale as the child of the vertical box. */
367 vol->volume_scale = gtk_vscale_new(GTK_ADJUSTMENT(gtk_adjustment_new(100, 0, 100, 0, 0, 0)));
368 gtk_scale_set_draw_value(GTK_SCALE(vol->volume_scale), FALSE);
369 gtk_range_set_inverted(GTK_RANGE(vol->volume_scale), TRUE);
370 gtk_box_pack_start(GTK_BOX(box), vol->volume_scale, TRUE, TRUE, 0);
6cc5e1a6 371
2ba86315
DB
372 /* Value-changed and scroll-event signals. */
373 vol->volume_scale_handler = g_signal_connect(vol->volume_scale, "value_changed", G_CALLBACK(volumealsa_popup_scale_changed), vol);
374 g_signal_connect(vol->volume_scale, "scroll-event", G_CALLBACK(volumealsa_popup_scale_scrolled), vol);
6cc5e1a6 375
2ba86315 376 /* Create a check button as the child of the vertical box. */
96dc8a45 377 vol->mute_check = gtk_check_button_new_with_label(_("Mute"));
96dc8a45 378 gtk_box_pack_end(GTK_BOX(box), vol->mute_check, FALSE, FALSE, 0);
2ba86315 379 vol->mute_check_handler = g_signal_connect(vol->mute_check, "toggled", G_CALLBACK(volumealsa_popup_mute_toggled), vol);
6cc5e1a6 380
2ba86315 381 /* Set background to default. */
6cc5e1a6
DB
382 gtk_widget_set_style(viewport, p->panel->defstyle);
383}
384
2ba86315
DB
385/* Plugin constructor. */
386static int volumealsa_constructor(Plugin * p, char ** fp)
6cc5e1a6 387{
2ba86315
DB
388 /* Allocate and initialize plugin context and set into Plugin private data pointer. */
389 VolumeALSAPlugin * vol = g_new0(VolumeALSAPlugin, 1);
6cc5e1a6 390 vol->plugin = p;
6cc5e1a6
DB
391 p->priv = vol;
392
2ba86315
DB
393 /* Initialize ALSA. If that fails, present nothing. */
394 if ( ! asound_initialize(vol))
395 return 1;
6cc5e1a6 396
2ba86315
DB
397 /* Allocate top level widget and set into Plugin widget pointer. */
398 p->pwid = gtk_event_box_new();
399 gtk_widget_add_events(p->pwid, GDK_BUTTON_PRESS_MASK);
400 gtk_widget_set_tooltip_text(p->pwid, _("Volume control"));
6cc5e1a6 401
2ba86315
DB
402 /* Allocate icon as a child of top level. */
403 vol->tray_icon = gtk_image_new();
404 gtk_container_add(GTK_CONTAINER(p->pwid), vol->tray_icon);
6cc5e1a6 405
2ba86315
DB
406 /* Initialize window to appear when icon clicked. */
407 volumealsa_build_popup_window(p);
6cc5e1a6 408
2ba86315
DB
409 /* Connect signals. */
410 g_signal_connect(G_OBJECT(p->pwid), "button-press-event", G_CALLBACK(volumealsa_button_press_event), vol);
411 g_signal_connect(G_OBJECT(p->pwid), "scroll-event", G_CALLBACK(volumealsa_popup_scale_scrolled), vol );
6cc5e1a6 412
2ba86315
DB
413 /* Update the display, show the widget, and return. */
414 volumealsa_update_display(vol);
415 gtk_widget_show_all(p->pwid);
416 return 1;
417}
6cc5e1a6 418
2ba86315
DB
419/* Plugin destructor. */
420static void volumealsa_destructor(Plugin * p)
421{
422 VolumeALSAPlugin * vol = (VolumeALSAPlugin *) p->priv;
6cc5e1a6 423
2ba86315
DB
424 /* Remove the periodic timer. */
425 if (vol->mixer_evt_idle != 0)
426 g_source_remove(vol->mixer_evt_idle);
6cc5e1a6 427
2ba86315
DB
428 /* If the dialog box is open, dismiss it. */
429 if (vol->popup_window != NULL)
430 gtk_widget_destroy(vol->popup_window);
6cc5e1a6 431
2ba86315
DB
432 /* Deallocate all memory. */
433 g_free(vol);
6cc5e1a6
DB
434}
435
2ba86315
DB
436/* Callback when panel configuration changes. */
437static void volumealsa_panel_configuration_changed(Plugin * p)
438{
439 /* Do a full redraw. */
440 volumealsa_update_display((VolumeALSAPlugin *) p->priv);
441}
6cc5e1a6 442
2ba86315 443/* Plugin descriptor. */
6cc5e1a6 444PluginClass volumealsa_plugin_class = {
2ba86315
DB
445
446 PLUGINCLASS_VERSIONING,
6cc5e1a6
DB
447
448 type : "volumealsa",
449 name : N_("Volume Control"),
450 version: "1.0",
451 description : "Display and control volume for ALSA",
452
453 constructor : volumealsa_constructor,
454 destructor : volumealsa_destructor,
455 config : NULL,
2ba86315
DB
456 save : NULL,
457 panel_configuration_changed : volumealsa_panel_configuration_changed
458
6cc5e1a6 459};