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