Merging upstream version 0.8.0 (Closes: #639729, #761971).
[debian/lxpanel.git] / plugins / volumealsa / volumealsa.c
CommitLineData
6cc5e1a6 1/**
00916e98 2 * Copyright (c) 2008-2014 LxDE Developers, see the file AUTHORS for details.
6cc5e1a6
DB
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
89173f95
AG
19#define _ISOC99_SOURCE /* lrint() */
20#define _GNU_SOURCE /* exp10() */
21
6cc5e1a6
DB
22#include <gtk/gtk.h>
23#include <stdlib.h>
24#include <fcntl.h>
25#include <unistd.h>
26#include <glib.h>
27#include <glib/gi18n.h>
28#include <gdk-pixbuf/gdk-pixbuf.h>
29#include <alsa/asoundlib.h>
67aeed17 30#include <poll.h>
89173f95 31#include <math.h>
00916e98 32#include <libfm/fm-gtk.h>
6cc5e1a6 33#include "plugin.h"
89173f95
AG
34#include "misc.h"
35
36#define ICONS_VOLUME_HIGH "volume-high"
37#define ICONS_VOLUME_MEDIUM "volume-medium"
38#define ICONS_VOLUME_LOW "volume-low"
39#define ICONS_MUTE "mute"
40
41#ifdef __UCLIBC__
42/* 10^x = 10^(log e^x) = (e^x)^log10 = e^(x * log 10) */
43# define M_LN10 2.30258509299404568402 /* log_e 10 */
44#define exp10(x) (exp((x) * log(10)))
45#endif /* __UCLIBC__ */
6cc5e1a6 46
89173f95 47#define MAX_LINEAR_DB_SCALE 24
6cc5e1a6
DB
48
49typedef struct {
1ea75322
DB
50
51 /* Graphics. */
00916e98
AG
52 GtkWidget * plugin; /* Back pointer to the widget */
53 LXPanel * panel; /* Back pointer to panel */
64ea8d44 54 config_setting_t * settings; /* Plugin settings */
1ea75322
DB
55 GtkWidget * tray_icon; /* Displayed image */
56 GtkWidget * popup_window; /* Top level window for popup */
57 GtkWidget * volume_scale; /* Scale for volume */
58 GtkWidget * mute_check; /* Checkbox for mute state */
59 gboolean show_popup; /* Toggle to show and hide the popup on left click */
60 guint volume_scale_handler; /* Handler for vscale widget */
61 guint mute_check_handler; /* Handler for mute_check widget */
62
63 /* ALSA interface. */
64 snd_mixer_t * mixer; /* The mixer */
65 snd_mixer_selem_id_t * sid; /* The element ID */
66 snd_mixer_elem_t * master_element; /* The Master element */
67 guint mixer_evt_idle; /* Timer to handle restarting poll */
00916e98 68 guint restart_idle;
89173f95 69 gint alsamixer_mapping;
5d26221e
DB
70
71 /* unloading and error handling */
72 GIOChannel **channels; /* Channels that we listen to */
64ea8d44 73 guint *watches; /* Watcher IDs for channels */
5d26221e 74 guint num_channels; /* Number of channels */
0f7f2ef3
AL
75
76 /* Icons */
0f7f2ef3
AL
77 const char* icon_panel;
78 const char* icon_fallback;
79
1ea75322
DB
80} VolumeALSAPlugin;
81
5d26221e 82static gboolean asound_restart(gpointer vol_gpointer);
1ea75322 83static gboolean asound_initialize(VolumeALSAPlugin * vol);
5d26221e 84static void asound_deinitialize(VolumeALSAPlugin * vol);
1ea75322 85static void volumealsa_update_display(VolumeALSAPlugin * vol);
00916e98 86static void volumealsa_destructor(gpointer user_data);
1ea75322
DB
87
88/*** ALSA ***/
89
90static gboolean asound_find_element(VolumeALSAPlugin * vol, const char * ename)
6cc5e1a6 91{
1ea75322
DB
92 for (
93 vol->master_element = snd_mixer_first_elem(vol->mixer);
94 vol->master_element != NULL;
95 vol->master_element = snd_mixer_elem_next(vol->master_element))
96 {
6cc5e1a6 97 snd_mixer_selem_get_id(vol->master_element, vol->sid);
1ea75322
DB
98 if ((snd_mixer_selem_is_active(vol->master_element))
99 && (strcmp(ename, snd_mixer_selem_id_get_name(vol->sid)) == 0))
6cc5e1a6 100 return TRUE;
6cc5e1a6 101 }
6cc5e1a6
DB
102 return FALSE;
103}
104
67aeed17
DB
105/* NOTE by PCMan:
106 * This is magic! Since ALSA uses its own machanism to handle this part.
107 * After polling of mixer fds, it requires that we should call
108 * snd_mixer_handle_events to clear all pending mixer events.
109 * However, when using the glib IO channels approach, we don't have
110 * poll() and snd_mixer_poll_descriptors_revents(). Due to the design of
111 * glib, on_mixer_event() will be called for every fd whose status was
112 * changed. So, after each poll(), it's called for several times,
113 * not just once. Therefore, we cannot call snd_mixer_handle_events()
114 * directly in the event handler. Otherwise, it will get called for
115 * several times, which might clear unprocessed pending events in the queue.
116 * So, here we call it once in the event callback for the first fd.
117 * Then, we don't call it for the following fds. After all fds with changed
118 * status are handled, we remove this restriction in an idle handler.
119 * The next time the event callback is involked for the first fs, we can
120 * call snd_mixer_handle_events() again. Racing shouldn't happen here
121 * because the idle handler has the same priority as the io channel callback.
122 * So, io callbacks for future pending events should be in the next gmain
123 * iteration, and won't be affected.
124 */
1ea75322
DB
125
126static gboolean asound_reset_mixer_evt_idle(VolumeALSAPlugin * vol)
67aeed17 127{
00916e98
AG
128 if (!g_source_is_destroyed(g_main_current_source()))
129 vol->mixer_evt_idle = 0;
67aeed17
DB
130 return FALSE;
131}
132
1ea75322
DB
133/* Handler for I/O event on ALSA channel. */
134static gboolean asound_mixer_event(GIOChannel * channel, GIOCondition cond, gpointer vol_gpointer)
67aeed17 135{
1ea75322 136 VolumeALSAPlugin * vol = (VolumeALSAPlugin *) vol_gpointer;
5d26221e 137 int res = 0;
1ea75322 138
64ea8d44
AG
139 if (g_source_is_destroyed(g_main_current_source()))
140 return FALSE;
141
1ea75322 142 if (vol->mixer_evt_idle == 0)
67aeed17 143 {
1ea75322 144 vol->mixer_evt_idle = g_idle_add_full(G_PRIORITY_DEFAULT, (GSourceFunc) asound_reset_mixer_evt_idle, vol, NULL);
5d26221e 145 res = snd_mixer_handle_events(vol->mixer);
67aeed17
DB
146 }
147
1ea75322 148 if (cond & G_IO_IN)
67aeed17
DB
149 {
150 /* the status of mixer is changed. update of display is needed. */
1ea75322 151 volumealsa_update_display(vol);
67aeed17 152 }
67aeed17 153
5d26221e 154 if ((cond & G_IO_HUP) || (res < 0))
1ea75322
DB
155 {
156 /* This means there're some problems with alsa. */
00916e98 157 g_warning("volumealsa: ALSA (or pulseaudio) had a problem: "
5d26221e 158 "volumealsa: snd_mixer_handle_events() = %d,"
00916e98 159 " cond 0x%x (IN: 0x%x, HUP: 0x%x).", res, cond,
5d26221e 160 G_IO_IN, G_IO_HUP);
00916e98 161 gtk_widget_set_tooltip_text(vol->plugin, "ALSA (or pulseaudio) had a problem."
5d26221e
DB
162 " Please check the lxpanel logs.");
163
00916e98
AG
164 if (vol->restart_idle == 0)
165 vol->restart_idle = g_timeout_add_seconds(1, asound_restart, vol);
5d26221e 166
67aeed17
DB
167 return FALSE;
168 }
169
170 return TRUE;
171}
172
5d26221e
DB
173static gboolean asound_restart(gpointer vol_gpointer)
174{
175 VolumeALSAPlugin * vol = vol_gpointer;
176
00916e98
AG
177 if (g_source_is_destroyed(g_main_current_source()))
178 return FALSE;
179
5d26221e
DB
180 asound_deinitialize(vol);
181
182 if (!asound_initialize(vol)) {
00916e98 183 g_warning("volumealsa: Re-initialization failed.");
5d26221e
DB
184 return TRUE; // try again in a second
185 }
186
00916e98 187 g_warning("volumealsa: Restarted ALSA interface...");
5d26221e 188
00916e98 189 vol->restart_idle = 0;
5d26221e
DB
190 return FALSE;
191}
192
1ea75322
DB
193/* Initialize the ALSA interface. */
194static gboolean asound_initialize(VolumeALSAPlugin * vol)
6cc5e1a6 195{
1ea75322 196 /* Access the "default" device. */
6cc5e1a6
DB
197 snd_mixer_selem_id_alloca(&vol->sid);
198 snd_mixer_open(&vol->mixer, 0);
199 snd_mixer_attach(vol->mixer, "default");
200 snd_mixer_selem_register(vol->mixer, NULL, NULL);
201 snd_mixer_load(vol->mixer);
202
24d886e1
DB
203 /* Find Master element, or Front element, or PCM element, or LineOut element.
204 * If one of these succeeds, master_element is valid. */
1ea75322
DB
205 if ( ! asound_find_element(vol, "Master"))
206 if ( ! asound_find_element(vol, "Front"))
207 if ( ! asound_find_element(vol, "PCM"))
00916e98 208 if ( ! asound_find_element(vol, "LineOut"))
b3df3353 209 return FALSE;
6cc5e1a6 210
1ea75322 211 /* Set the playback volume range as we wish it. */
89173f95
AG
212 if ( ! vol->alsamixer_mapping)
213 snd_mixer_selem_set_playback_volume_range(vol->master_element, 0, 100);
6cc5e1a6 214
1ea75322
DB
215 /* Listen to events from ALSA. */
216 int n_fds = snd_mixer_poll_descriptors_count(vol->mixer);
217 struct pollfd * fds = g_new0(struct pollfd, n_fds);
67aeed17 218
5d26221e 219 vol->channels = g_new0(GIOChannel *, n_fds);
64ea8d44 220 vol->watches = g_new0(guint, n_fds);
5d26221e
DB
221 vol->num_channels = n_fds;
222
1ea75322
DB
223 snd_mixer_poll_descriptors(vol->mixer, fds, n_fds);
224 int i;
225 for (i = 0; i < n_fds; ++i)
67aeed17 226 {
1ea75322 227 GIOChannel* channel = g_io_channel_unix_new(fds[i].fd);
64ea8d44 228 vol->watches[i] = g_io_add_watch(channel, G_IO_IN | G_IO_HUP, asound_mixer_event, vol);
5d26221e 229 vol->channels[i] = channel;
67aeed17 230 }
1ea75322 231 g_free(fds);
6cc5e1a6
DB
232 return TRUE;
233}
234
5d26221e
DB
235static void asound_deinitialize(VolumeALSAPlugin * vol)
236{
00916e98 237 guint i;
5d26221e 238
0f7f2ef3 239 if (vol->mixer_evt_idle != 0) {
5d26221e 240 g_source_remove(vol->mixer_evt_idle);
0f7f2ef3
AL
241 vol->mixer_evt_idle = 0;
242 }
5d26221e
DB
243
244 for (i = 0; i < vol->num_channels; i++) {
64ea8d44 245 g_source_remove(vol->watches[i]);
5d26221e
DB
246 g_io_channel_shutdown(vol->channels[i], FALSE, NULL);
247 g_io_channel_unref(vol->channels[i]);
248 }
249 g_free(vol->channels);
64ea8d44 250 g_free(vol->watches);
5d26221e 251 vol->channels = NULL;
64ea8d44 252 vol->watches = NULL;
5d26221e 253 vol->num_channels = 0;
0f7f2ef3
AL
254
255 snd_mixer_close(vol->mixer);
256 vol->master_element = NULL;
00916e98 257 /* FIXME: unalloc vol->sid */
5d26221e
DB
258}
259
1ea75322
DB
260/* Get the presence of the mute control from the sound system. */
261static gboolean asound_has_mute(VolumeALSAPlugin * vol)
262{
24d886e1 263 return ((vol->master_element != NULL) ? snd_mixer_selem_has_playback_switch(vol->master_element) : FALSE);
1ea75322
DB
264}
265
266/* Get the condition of the mute control from the sound system. */
267static gboolean asound_is_muted(VolumeALSAPlugin * vol)
6cc5e1a6 268{
1ea75322
DB
269 /* The switch is on if sound is not muted, and off if the sound is muted.
270 * Initialize so that the sound appears unmuted if the control does not exist. */
271 int value = 1;
24d886e1
DB
272 if (vol->master_element != NULL)
273 snd_mixer_selem_get_playback_switch(vol->master_element, 0, &value);
1ea75322
DB
274 return (value == 0);
275}
276
89173f95
AG
277static long lrint_dir(double x, int dir)
278{
279 if (dir > 0)
280 return lrint(ceil(x));
281 else if (dir < 0)
282 return lrint(floor(x));
283 else
284 return lrint(x);
285}
286
287static inline gboolean use_linear_dB_scale(long dBmin, long dBmax)
288{
289 return dBmax - dBmin <= MAX_LINEAR_DB_SCALE * 100;
290}
291
292static long get_normalized_volume(snd_mixer_elem_t *elem,
293 snd_mixer_selem_channel_id_t channel)
294{
295 long min, max, value;
296 double normalized, min_norm;
297 int err;
298
299 err = snd_mixer_selem_get_playback_dB_range(elem, &min, &max);
300 if (err < 0 || min >= max) {
301 err = snd_mixer_selem_get_playback_volume_range(elem, &min, &max);
302 if (err < 0 || min == max)
303 return 0;
304
305 err = snd_mixer_selem_get_playback_volume(elem, channel, &value);
306 if (err < 0)
307 return 0;
308
309 return lrint(100.0 * (value - min) / (double)(max - min));
310 }
311
312 err = snd_mixer_selem_get_playback_dB(elem, channel, &value);
313 if (err < 0)
314 return 0;
315
316 if (use_linear_dB_scale(min, max))
317 return lrint(100.0 * (value - min) / (double)(max - min));
318
319 normalized = exp10((value - max) / 6000.0);
320 if (min != SND_CTL_TLV_DB_GAIN_MUTE) {
321 min_norm = exp10((min - max) / 6000.0);
322 normalized = (normalized - min_norm) / (1 - min_norm);
323 }
324
325 return lrint(100.0 * normalized);
326}
327
1ea75322
DB
328/* Get the volume from the sound system.
329 * This implementation returns the average of the Front Left and Front Right channels. */
330static int asound_get_volume(VolumeALSAPlugin * vol)
331{
24d886e1
DB
332 long aleft = 0;
333 long aright = 0;
89173f95 334
24d886e1
DB
335 if (vol->master_element != NULL)
336 {
89173f95
AG
337 if ( ! vol->alsamixer_mapping)
338 {
339 snd_mixer_selem_get_playback_volume(vol->master_element, SND_MIXER_SCHN_FRONT_LEFT, &aleft);
340 snd_mixer_selem_get_playback_volume(vol->master_element, SND_MIXER_SCHN_FRONT_RIGHT, &aright);
341 }
342 else
343 {
344 aleft = get_normalized_volume(vol->master_element, SND_MIXER_SCHN_FRONT_LEFT);
345 aright = get_normalized_volume(vol->master_element, SND_MIXER_SCHN_FRONT_RIGHT);
346 }
24d886e1 347 }
6cc5e1a6
DB
348 return (aleft + aright) >> 1;
349}
350
89173f95
AG
351static int set_normalized_volume(snd_mixer_elem_t *elem,
352 snd_mixer_selem_channel_id_t channel,
353 int vol,
354 int dir)
355{
356 long min, max, value;
357 double min_norm, volume;
358 int err;
359
360 volume = vol / 100.0;
361
362 err = snd_mixer_selem_get_playback_dB_range(elem, &min, &max);
363 if (err < 0 || min >= max) {
364 err = snd_mixer_selem_get_playback_volume_range(elem, &min, &max);
365 if (err < 0)
366 return err;
367
368 value = lrint_dir(volume * (max - min), dir) + min;
369 return snd_mixer_selem_set_playback_volume(elem, channel, value);
370 }
371
372 if (use_linear_dB_scale(min, max)) {
373 value = lrint_dir(volume * (max - min), dir) + min;
374 return snd_mixer_selem_set_playback_dB(elem, channel, value, dir);
375 }
376
377 if (min != SND_CTL_TLV_DB_GAIN_MUTE) {
378 min_norm = exp10((min - max) / 6000.0);
379 volume = volume * (1 - min_norm) + min_norm;
380 }
381 value = lrint_dir(6000.0 * log10(volume), dir) + max;
382
383 return snd_mixer_selem_set_playback_dB(elem, channel, value, dir);
384}
385
1ea75322
DB
386/* Set the volume to the sound system.
387 * This implementation sets the Front Left and Front Right channels to the specified value. */
388static void asound_set_volume(VolumeALSAPlugin * vol, int volume)
6cc5e1a6 389{
89173f95
AG
390 int dir = volume - asound_get_volume(vol);
391
392 /* Volume is set to the correct value already */
393 if (dir == 0)
394 return;
395
24d886e1
DB
396 if (vol->master_element != NULL)
397 {
89173f95
AG
398 if ( ! vol->alsamixer_mapping)
399 {
400 snd_mixer_selem_set_playback_volume(vol->master_element, SND_MIXER_SCHN_FRONT_LEFT, volume);
401 snd_mixer_selem_set_playback_volume(vol->master_element, SND_MIXER_SCHN_FRONT_RIGHT, volume);
402 }
403 else
404 {
405 set_normalized_volume(vol->master_element, SND_MIXER_SCHN_FRONT_LEFT, volume, dir);
406 set_normalized_volume(vol->master_element, SND_MIXER_SCHN_FRONT_RIGHT, volume, dir);
407 }
24d886e1 408 }
6cc5e1a6
DB
409}
410
1ea75322
DB
411/*** Graphics ***/
412
89173f95 413static void volumealsa_lookup_current_icon(VolumeALSAPlugin * vol, gboolean mute, int level)
6cc5e1a6 414{
f8c25730 415 /* Change icon according to mute / volume */
f8c25730
DB
416 const char* icon_panel="audio-volume-muted-panel";
417 const char* icon_fallback=ICONS_MUTE;
418 if (mute)
419 {
420 icon_panel = "audio-volume-muted-panel";
f8c25730
DB
421 icon_fallback=ICONS_MUTE;
422 }
89173f95 423 else if (level >= 66)
f8c25730
DB
424 {
425 icon_panel = "audio-volume-high-panel";
f8c25730
DB
426 icon_fallback=ICONS_VOLUME_HIGH;
427 }
89173f95 428 else if (level >= 33)
f8c25730
DB
429 {
430 icon_panel = "audio-volume-medium-panel";
f8c25730
DB
431 icon_fallback=ICONS_VOLUME_MEDIUM;
432 }
433 else if (level > 0)
434 {
435 icon_panel = "audio-volume-low-panel";
f8c25730
DB
436 icon_fallback=ICONS_VOLUME_LOW;
437 }
438
0f7f2ef3 439 vol->icon_panel = icon_panel;
89173f95 440 vol->icon_fallback = icon_fallback;
0f7f2ef3
AL
441}
442
89173f95 443static void volumealsa_update_current_icon(VolumeALSAPlugin * vol, gboolean mute, int level)
0f7f2ef3 444{
89173f95
AG
445 /* Find suitable icon */
446 volumealsa_lookup_current_icon(vol, mute, level);
0f7f2ef3 447
f8c25730 448 /* Change icon, fallback to default icon if theme doesn't exsit */
89173f95 449 lxpanel_image_change_icon(vol->tray_icon, vol->icon_panel, vol->icon_fallback);
1ea75322 450
89173f95
AG
451 /* Display current level in tooltip. */
452 char * tooltip = g_strdup_printf("%s %d", _("Volume control"), level);
453 gtk_widget_set_tooltip_text(vol->plugin, tooltip);
454 g_free(tooltip);
455}
456
457/*
458 * Here we just update volume's vertical scale and mute check button.
459 * The rest will be updated by signal handelrs.
460 */
461static void volumealsa_update_display(VolumeALSAPlugin * vol)
462{
463 /* Mute. */
464 gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(vol->mute_check), asound_is_muted(vol));
465 gtk_widget_set_sensitive(vol->mute_check, (asound_has_mute(vol)));
1ea75322
DB
466
467 /* Volume. */
1ea75322
DB
468 if (vol->volume_scale != NULL)
469 {
1ea75322 470 gtk_range_set_value(GTK_RANGE(vol->volume_scale), asound_get_volume(vol));
1ea75322 471 }
6cc5e1a6
DB
472}
473
1ea75322 474/* Handler for "button-press-event" signal on main widget. */
00916e98 475static gboolean volumealsa_button_press_event(GtkWidget * widget, GdkEventButton * event, LXPanel * panel)
6cc5e1a6 476{
00916e98 477 VolumeALSAPlugin * vol = lxpanel_plugin_get_data(widget);
1ea75322
DB
478
479 /* Left-click. Show or hide the popup window. */
480 if (event->button == 1)
481 {
482 if (vol->show_popup)
483 {
484 gtk_widget_hide(vol->popup_window);
485 vol->show_popup = FALSE;
486 }
487 else
488 {
1ea75322
DB
489 gtk_widget_show_all(vol->popup_window);
490 vol->show_popup = TRUE;
491 }
6cc5e1a6
DB
492 }
493
1ea75322
DB
494 /* Middle-click. Toggle the mute status. */
495 else if (event->button == 2)
496 {
497 gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(vol->mute_check), ! gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(vol->mute_check)));
6cc5e1a6
DB
498 }
499 return TRUE;
500}
501
1ea75322
DB
502/* Handler for "focus-out" signal on popup window. */
503static gboolean volumealsa_popup_focus_out(GtkWidget * widget, GdkEvent * event, VolumeALSAPlugin * vol)
6cc5e1a6 504{
1ea75322
DB
505 /* Hide the widget. */
506 gtk_widget_hide(vol->popup_window);
507 vol->show_popup = FALSE;
508 return FALSE;
6cc5e1a6
DB
509}
510
934ecce5
DB
511/* Handler for "map" signal on popup window. */
512static void volumealsa_popup_map(GtkWidget * widget, VolumeALSAPlugin * vol)
513{
00916e98 514 lxpanel_plugin_adjust_popup_position(widget, vol->plugin);
934ecce5
DB
515}
516
1ea75322
DB
517/* Handler for "value_changed" signal on popup window vertical scale. */
518static void volumealsa_popup_scale_changed(GtkRange * range, VolumeALSAPlugin * vol)
67aeed17 519{
89173f95
AG
520 int level = gtk_range_get_value(GTK_RANGE(vol->volume_scale));
521 gboolean mute = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(vol->mute_check));
1ea75322 522
89173f95
AG
523 /* Reflect the value of the control to the sound system. */
524 asound_set_volume(vol, level);
525
526 /*
527 * Redraw the controls.
528 * Scale and check button do not need to be updated, as these are always
529 * in sync with user's actions.
530 */
531 volumealsa_update_current_icon(vol, mute, level);
1ea75322
DB
532}
533
534/* Handler for "scroll-event" signal on popup window vertical scale. */
535static void volumealsa_popup_scale_scrolled(GtkScale * scale, GdkEventScroll * evt, VolumeALSAPlugin * vol)
536{
537 /* Get the state of the vertical scale. */
538 gdouble val = gtk_range_get_value(GTK_RANGE(vol->volume_scale));
539
540 /* Dispatch on scroll direction to update the value. */
541 if ((evt->direction == GDK_SCROLL_UP) || (evt->direction == GDK_SCROLL_LEFT))
67aeed17 542 val += 2;
1ea75322 543 else
67aeed17 544 val -= 2;
1ea75322
DB
545
546 /* Reset the state of the vertical scale. This provokes a "value_changed" event. */
547 gtk_range_set_value(GTK_RANGE(vol->volume_scale), CLAMP((int)val, 0, 100));
67aeed17
DB
548}
549
1ea75322
DB
550/* Handler for "toggled" signal on popup window mute checkbox. */
551static void volumealsa_popup_mute_toggled(GtkWidget * widget, VolumeALSAPlugin * vol)
6cc5e1a6 552{
89173f95
AG
553 int level = gtk_range_get_value(GTK_RANGE(vol->volume_scale));
554 gboolean mute = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(vol->mute_check));
1ea75322
DB
555
556 /* Reflect the mute toggle to the sound system. */
24d886e1
DB
557 if (vol->master_element != NULL)
558 {
559 int chn;
560 for (chn = 0; chn <= SND_MIXER_SCHN_LAST; chn++)
89173f95 561 snd_mixer_selem_set_playback_switch(vol->master_element, chn, ((mute) ? 0 : 1));
24d886e1 562 }
6cc5e1a6 563
89173f95
AG
564 /*
565 * Redraw the controls.
566 * Scale and check button do not need to be updated, as these are always
567 * in sync with user's actions.
568 */
569 volumealsa_update_current_icon(vol, mute, level);
6cc5e1a6
DB
570}
571
1ea75322 572/* Build the window that appears when the top level widget is clicked. */
00916e98 573static void volumealsa_build_popup_window(GtkWidget *p)
6cc5e1a6 574{
00916e98 575 VolumeALSAPlugin * vol = lxpanel_plugin_get_data(p);
1ea75322
DB
576
577 /* Create a new window. */
00916e98 578 vol->popup_window = gtk_window_new(GTK_WINDOW_POPUP);
1ea75322
DB
579 gtk_window_set_decorated(GTK_WINDOW(vol->popup_window), FALSE);
580 gtk_container_set_border_width(GTK_CONTAINER(vol->popup_window), 5);
581 gtk_window_set_default_size(GTK_WINDOW(vol->popup_window), 80, 140);
582 gtk_window_set_skip_taskbar_hint(GTK_WINDOW(vol->popup_window), TRUE);
583 gtk_window_set_skip_pager_hint(GTK_WINDOW(vol->popup_window), TRUE);
00916e98 584 gtk_window_set_type_hint(GTK_WINDOW(vol->popup_window), GDK_WINDOW_TYPE_HINT_UTILITY);
1ea75322 585
934ecce5 586 /* Connect signals. */
00916e98 587 g_signal_connect(G_OBJECT(vol->popup_window), "focus-out-event", G_CALLBACK(volumealsa_popup_focus_out), vol);
934ecce5 588 g_signal_connect(G_OBJECT(vol->popup_window), "map", G_CALLBACK(volumealsa_popup_map), vol);
1ea75322
DB
589
590 /* Create a scrolled window as the child of the top level window. */
591 GtkWidget * scrolledwindow = gtk_scrolled_window_new(NULL, NULL);
592 gtk_container_set_border_width (GTK_CONTAINER(scrolledwindow), 0);
593 gtk_widget_show(scrolledwindow);
594 gtk_container_add(GTK_CONTAINER(vol->popup_window), scrolledwindow);
00916e98 595 gtk_widget_set_can_focus(scrolledwindow, FALSE);
6cc5e1a6
DB
596 gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW (scrolledwindow), GTK_POLICY_NEVER, GTK_POLICY_NEVER);
597 gtk_scrolled_window_set_shadow_type(GTK_SCROLLED_WINDOW(scrolledwindow), GTK_SHADOW_NONE);
598
1ea75322
DB
599 /* Create a viewport as the child of the scrolled window. */
600 GtkWidget * viewport = gtk_viewport_new(NULL, NULL);
601 gtk_container_add(GTK_CONTAINER(scrolledwindow), viewport);
602 gtk_viewport_set_shadow_type(GTK_VIEWPORT(viewport), GTK_SHADOW_NONE);
6cc5e1a6
DB
603 gtk_widget_show(viewport);
604
1ea75322
DB
605 /* Create a frame as the child of the viewport. */
606 GtkWidget * frame = gtk_frame_new(_("Volume"));
6cc5e1a6
DB
607 gtk_frame_set_shadow_type(GTK_FRAME(frame), GTK_SHADOW_IN);
608 gtk_container_add(GTK_CONTAINER(viewport), frame);
609
1ea75322
DB
610 /* Create a vertical box as the child of the frame. */
611 GtkWidget * box = gtk_vbox_new(FALSE, 0);
612 gtk_container_add(GTK_CONTAINER(frame), box);
6cc5e1a6 613
1ea75322
DB
614 /* Create a vertical scale as the child of the vertical box. */
615 vol->volume_scale = gtk_vscale_new(GTK_ADJUSTMENT(gtk_adjustment_new(100, 0, 100, 0, 0, 0)));
616 gtk_scale_set_draw_value(GTK_SCALE(vol->volume_scale), FALSE);
617 gtk_range_set_inverted(GTK_RANGE(vol->volume_scale), TRUE);
618 gtk_box_pack_start(GTK_BOX(box), vol->volume_scale, TRUE, TRUE, 0);
6cc5e1a6 619
1ea75322 620 /* Value-changed and scroll-event signals. */
00916e98 621 vol->volume_scale_handler = g_signal_connect(vol->volume_scale, "value-changed", G_CALLBACK(volumealsa_popup_scale_changed), vol);
1ea75322 622 g_signal_connect(vol->volume_scale, "scroll-event", G_CALLBACK(volumealsa_popup_scale_scrolled), vol);
6cc5e1a6 623
1ea75322 624 /* Create a check button as the child of the vertical box. */
67aeed17 625 vol->mute_check = gtk_check_button_new_with_label(_("Mute"));
67aeed17 626 gtk_box_pack_end(GTK_BOX(box), vol->mute_check, FALSE, FALSE, 0);
1ea75322 627 vol->mute_check_handler = g_signal_connect(vol->mute_check, "toggled", G_CALLBACK(volumealsa_popup_mute_toggled), vol);
6cc5e1a6 628
1ea75322 629 /* Set background to default. */
00916e98 630 gtk_widget_set_style(viewport, panel_get_defstyle(vol->panel));
6cc5e1a6
DB
631}
632
1ea75322 633/* Plugin constructor. */
00916e98 634static GtkWidget *volumealsa_constructor(LXPanel *panel, config_setting_t *settings)
6cc5e1a6 635{
1ea75322
DB
636 /* Allocate and initialize plugin context and set into Plugin private data pointer. */
637 VolumeALSAPlugin * vol = g_new0(VolumeALSAPlugin, 1);
00916e98 638 GtkWidget *p;
6cc5e1a6 639
89173f95
AG
640 /* Read config necessary for proper initialization of ALSA. */
641 config_setting_lookup_int(settings, "UseAlsamixerVolumeMapping", &vol->alsamixer_mapping);
642
1ea75322
DB
643 /* Initialize ALSA. If that fails, present nothing. */
644 if ( ! asound_initialize(vol))
00916e98
AG
645 {
646 volumealsa_destructor(vol);
647 return NULL;
648 }
6cc5e1a6 649
1ea75322 650 /* Allocate top level widget and set into Plugin widget pointer. */
00916e98
AG
651 vol->panel = panel;
652 vol->plugin = p = gtk_event_box_new();
64ea8d44 653 vol->settings = settings;
00916e98 654 lxpanel_plugin_set_data(p, vol, volumealsa_destructor);
00916e98 655 gtk_widget_set_tooltip_text(p, _("Volume control"));
6cc5e1a6 656
1ea75322 657 /* Allocate icon as a child of top level. */
89173f95
AG
658 vol->tray_icon = lxpanel_image_new_for_icon(panel, "audio-volume-muted-panel",
659 -1, ICONS_MUTE);
00916e98 660 gtk_container_add(GTK_CONTAINER(p), vol->tray_icon);
6cc5e1a6 661
1ea75322
DB
662 /* Initialize window to appear when icon clicked. */
663 volumealsa_build_popup_window(p);
6cc5e1a6 664
1ea75322 665 /* Connect signals. */
00916e98 666 g_signal_connect(G_OBJECT(p), "scroll-event", G_CALLBACK(volumealsa_popup_scale_scrolled), vol );
6cc5e1a6 667
1ea75322
DB
668 /* Update the display, show the widget, and return. */
669 volumealsa_update_display(vol);
00916e98
AG
670 gtk_widget_show_all(p);
671 return p;
1ea75322 672}
6cc5e1a6 673
1ea75322 674/* Plugin destructor. */
00916e98 675static void volumealsa_destructor(gpointer user_data)
1ea75322 676{
00916e98 677 VolumeALSAPlugin * vol = (VolumeALSAPlugin *) user_data;
6cc5e1a6 678
5d26221e 679 asound_deinitialize(vol);
6cc5e1a6 680
1ea75322
DB
681 /* If the dialog box is open, dismiss it. */
682 if (vol->popup_window != NULL)
683 gtk_widget_destroy(vol->popup_window);
6cc5e1a6 684
00916e98
AG
685 if (vol->restart_idle)
686 g_source_remove(vol->restart_idle);
687
1ea75322
DB
688 /* Deallocate all memory. */
689 g_free(vol);
6cc5e1a6
DB
690}
691
f8c25730
DB
692/* Callback when the configuration dialog is to be shown. */
693
00916e98 694static GtkWidget *volumealsa_configure(LXPanel *panel, GtkWidget *p)
f8c25730 695{
64ea8d44
AG
696 VolumeALSAPlugin * vol = lxpanel_plugin_get_data(p);
697 char *path = NULL;
f8c25730 698 const gchar *command_line = NULL;
dcd17043 699 GAppInfoCreateFlags flags = G_APP_INFO_CREATE_NONE;
f8c25730 700
64ea8d44
AG
701 /* FIXME: configure settings! */
702 /* check if command line was configured */
703 config_setting_lookup_string(vol->settings, "MixerCommand", &command_line);
dcd17043
AG
704 /* FIXME: support "needs terminal" for MixerCommand */
705 /* FIXME: selection for master channel! */
706 /* FIXME: configure buttons for each action (toggle volume/mixer/mute)! */
89173f95 707 /* FIXME: allow bind multimedia keys to volume using libkeybinder */
64ea8d44
AG
708
709 /* if command isn't set in settings then let guess it */
710 if (command_line == NULL && (path = g_find_program_in_path("pulseaudio")))
f8c25730 711 {
64ea8d44 712 g_free(path);
f8c25730 713 /* Assume that when pulseaudio is installed, it's launching every time */
64ea8d44 714 if ((path = g_find_program_in_path("gnome-sound-applet")))
f8c25730
DB
715 {
716 command_line = "gnome-sound-applet";
717 }
64ea8d44 718 else if ((path = g_find_program_in_path("pavucontrol")))
f8c25730 719 {
64ea8d44 720 command_line = "pavucontrol";
f8c25730
DB
721 }
722 }
723
724 /* Fallback to alsamixer when PA is not running, or when no PA utility is find */
725 if (command_line == NULL)
726 {
64ea8d44 727 if ((path = g_find_program_in_path("gnome-alsamixer")))
f8c25730
DB
728 {
729 command_line = "gnome-alsamixer";
730 }
64ea8d44
AG
731 else if ((path = g_find_program_in_path("alsamixergui")))
732 {
733 command_line = "alsamixergui";
734 }
735 else if ((path = g_find_program_in_path("alsamixer")))
f8c25730 736 {
dcd17043
AG
737 command_line = "alsamixer";
738 flags = G_APP_INFO_CREATE_NEEDS_TERMINAL;
f8c25730
DB
739 }
740 }
64ea8d44 741 g_free(path);
f8c25730
DB
742
743 if (command_line)
744 {
dcd17043 745 fm_launch_command_simple(NULL, NULL, flags, command_line, NULL);
f8c25730
DB
746 }
747 else
748 {
00916e98
AG
749 fm_show_error(NULL, NULL,
750 _("Error, you need to install an application to configure"
751 " the sound (pavucontrol, alsamixer ...)"));
f8c25730
DB
752 }
753
00916e98 754 return NULL;
f8c25730
DB
755}
756
1ea75322 757/* Callback when panel configuration changes. */
00916e98 758static void volumealsa_panel_configuration_changed(LXPanel *panel, GtkWidget *p)
1ea75322
DB
759{
760 /* Do a full redraw. */
00916e98 761 volumealsa_update_display(lxpanel_plugin_get_data(p));
1ea75322 762}
6cc5e1a6 763
00916e98 764FM_DEFINE_MODULE(lxpanel_gtk, volumealsa)
1ea75322 765
00916e98
AG
766/* Plugin descriptor. */
767LXPanelPluginInit fm_module_init_lxpanel_gtk = {
768 .name = N_("Volume Control"),
769 .description = N_("Display and control volume for ALSA"),
770
771 .new_instance = volumealsa_constructor,
772 .config = volumealsa_configure,
773 .reconfigure = volumealsa_panel_configuration_changed,
774 .button_press_event = volumealsa_button_press_event
6cc5e1a6 775};
5d26221e
DB
776
777/* vim: set sw=4 et sts=4 : */