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