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