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