Fix memory leaks on gtk_list_store_new() never unref'ed.
[lxde/lxpanel.git] / plugins / volumealsa / volumealsa.c
CommitLineData
b021864b 1/*
9d4601f2
AG
2 * Copyright (C) 2006-2008 Jim Huang <jserv.tw@gmail.com>
3 * 2006 Hong Jen Yee (PCMan) <pcman.tw@gmail.com>
9fc4c110 4 * 2008 Frank ENDRES <frank_endres@yahoo.fr>
9d4601f2 5 *
b021864b
AG
6 * Copyright (C) 2008 Fred Chien <fred@lxde.org>
7 * 2008 Hong Jen Yee (PCMan) <pcman.tw@gmail.com>
8 * 2009-2010 Marty Jack <martyj19@comcast.net>
9 * 2010-2012 Julien Lavergne <julien.lavergne@gmail.com>
10 * 2012 Henry Gebhardt <hsggebhardt@gmail.com>
11 * 2014 Peter <ombalaxitabou@users.sf.net>
9d4601f2 12 * 2014-2016 Andriy Grytsenko <andrej@rep.kiev.ua>
b021864b
AG
13 *
14 * This file is a part of LXPanel project.
49db0a37
FC
15 *
16 * This program is free software; you can redistribute it and/or modify
17 * it under the terms of the GNU General Public License as published by
18 * the Free Software Foundation; either version 2 of the License, or
19 * (at your option) any later version.
20 *
21 * This program is distributed in the hope that it will be useful,
22 * but WITHOUT ANY WARRANTY; without even the implied warranty of
23 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
24 * GNU General Public License for more details.
25 *
26 * You should have received a copy of the GNU General Public License
27 * along with this program; if not, write to the Free Software Foundation,
28 * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
29 */
30
418bf2f5
P
31#define _ISOC99_SOURCE /* lrint() */
32#define _GNU_SOURCE /* exp10() */
33
9d4601f2
AG
34#ifdef HAVE_CONFIG_H
35#include <config.h>
36#endif
37
49db0a37 38#include <gtk/gtk.h>
9fc4c110 39#include <gdk/gdkkeysyms.h>
49db0a37
FC
40#include <stdlib.h>
41#include <fcntl.h>
42#include <unistd.h>
43#include <glib.h>
44#include <glib/gi18n.h>
45#include <gdk-pixbuf/gdk-pixbuf.h>
9d4601f2
AG
46#ifdef DISABLE_ALSA
47#include <sys/types.h>
48#include <sys/stat.h>
49#include <sys/ioctl.h>
50#include <string.h>
51#include <stdio.h>
52#include <fcntl.h>
53#include <errno.h>
54#ifdef HAVE_SYS_SOUNDCARD_H
55#include <sys/soundcard.h>
56#elif defined(HAVE_LINUX_SOUNDCARD_H)
57#include <linux/soundcard.h>
58#else
59#error "Not supported platform"
60#endif
589cd33a
AG
61#ifndef SOUND_MIXER_PHONEOUT
62# define SOUND_MIXER_PHONEOUT SOUND_MIXER_MONO
63#endif
64//TODO: support OSSv4
9d4601f2 65#else
49db0a37 66#include <alsa/asoundlib.h>
55ccbddf 67#include <poll.h>
9d4601f2 68#endif
418bf2f5 69#include <math.h>
b285684e 70#include <libfm/fm-gtk.h>
9fc4c110 71
b285684e 72#include "plugin.h"
6febec6d 73#include "misc.h"
9fc4c110 74#include "gtk-compat.h"
49db0a37 75
6febec6d
AG
76#define ICONS_VOLUME_HIGH "volume-high"
77#define ICONS_VOLUME_MEDIUM "volume-medium"
78#define ICONS_VOLUME_LOW "volume-low"
79#define ICONS_MUTE "mute"
49db0a37 80
418bf2f5
P
81#ifdef __UCLIBC__
82/* 10^x = 10^(log e^x) = (e^x)^log10 = e^(x * log 10) */
83# define M_LN10 2.30258509299404568402 /* log_e 10 */
84#define exp10(x) (exp((x) * log(10)))
85#endif /* __UCLIBC__ */
86
87#define MAX_LINEAR_DB_SCALE 24
88
9d4601f2 89#ifdef DISABLE_ALSA
589cd33a 90typedef union
9d4601f2 91{
589cd33a
AG
92 struct
93 {
94 unsigned char left;
95 unsigned char right;
96 };
97 int value;
9d4601f2
AG
98} StereoVolume;
99#endif
100
49db0a37 101typedef struct {
f0861dee 102
103 /* Graphics. */
b285684e 104 GtkWidget * plugin; /* Back pointer to the widget */
a7bd16a4 105 LXPanel * panel; /* Back pointer to panel */
72312227 106 config_setting_t * settings; /* Plugin settings */
f0861dee 107 GtkWidget * tray_icon; /* Displayed image */
108 GtkWidget * popup_window; /* Top level window for popup */
109 GtkWidget * volume_scale; /* Scale for volume */
110 GtkWidget * mute_check; /* Checkbox for mute state */
111 gboolean show_popup; /* Toggle to show and hide the popup on left click */
112 guint volume_scale_handler; /* Handler for vscale widget */
113 guint mute_check_handler; /* Handler for mute_check widget */
114
9d4601f2
AG
115#ifdef DISABLE_ALSA
116 int mixer_fd; /* The mixer FD */
9d4601f2 117 gdouble vol_before_mute; /* Save value when muted */
589cd33a
AG
118
119 guint master_channel;
9d4601f2 120#else
f0861dee 121 /* ALSA interface. */
122 snd_mixer_t * mixer; /* The mixer */
f0861dee 123 snd_mixer_elem_t * master_element; /* The Master element */
124 guint mixer_evt_idle; /* Timer to handle restarting poll */
b285684e 125 guint restart_idle;
418bf2f5 126 gint alsamixer_mapping;
ad987a0c
HG
127
128 /* unloading and error handling */
129 GIOChannel **channels; /* Channels that we listen to */
9973857e 130 guint *watches; /* Watcher IDs for channels */
ad987a0c 131 guint num_channels; /* Number of channels */
895885e3 132
decc23e2 133 gint used_device;
895885e3 134 char *master_channel;
9d4601f2 135#endif
7192ab49
JL
136
137 /* Icons */
7192ab49
JL
138 const char* icon_panel;
139 const char* icon_fallback;
140
895885e3
AG
141 /* Clicks */
142 int mute_click;
143 GdkModifierType mute_click_mods;
144 int mixer_click;
145 GdkModifierType mixer_click_mods;
146 int slider_click;
147 GdkModifierType slider_click_mods;
650d2ca5
AG
148
149 /* Hotkeys */
150 char * hotkey_up;
151 char * hotkey_down;
152 char * hotkey_mute;
9fc4c110
AG
153
154 GtkWidget *channel_selector; /* Used by configure dialog */
f0861dee 155} VolumeALSAPlugin;
156
9d4601f2 157#ifndef DISABLE_ALSA
ad987a0c 158static gboolean asound_restart(gpointer vol_gpointer);
9d4601f2 159#endif
f0861dee 160static gboolean asound_initialize(VolumeALSAPlugin * vol);
ad987a0c 161static void asound_deinitialize(VolumeALSAPlugin * vol);
f0861dee 162static void volumealsa_update_display(VolumeALSAPlugin * vol);
b285684e 163static void volumealsa_destructor(gpointer user_data);
f0861dee 164
165/*** ALSA ***/
166
9d4601f2 167#ifndef DISABLE_ALSA
895885e3 168static gboolean asound_find_element(VolumeALSAPlugin * vol, const char ** ename, int n)
49db0a37 169{
895885e3 170 int i;
decc23e2 171 snd_mixer_selem_id_t * sid; /* The element ID */
895885e3 172
decc23e2 173 snd_mixer_selem_id_alloca(&sid);
895885e3 174 for (i = 0; i < n; i++)
f0861dee 175 {
895885e3
AG
176 for (vol->master_element = snd_mixer_first_elem(vol->mixer);
177 vol->master_element != NULL;
178 vol->master_element = snd_mixer_elem_next(vol->master_element))
179 {
decc23e2 180 snd_mixer_selem_get_id(vol->master_element, sid);
895885e3 181 if (snd_mixer_selem_is_active(vol->master_element) &&
decc23e2 182 strcmp(ename[i], snd_mixer_selem_id_get_name(sid)) == 0)
895885e3
AG
183 return TRUE;
184 }
4542c20d 185 }
4542c20d 186 return FALSE;
49db0a37
FC
187}
188
55ccbddf
HJYP
189/* NOTE by PCMan:
190 * This is magic! Since ALSA uses its own machanism to handle this part.
191 * After polling of mixer fds, it requires that we should call
192 * snd_mixer_handle_events to clear all pending mixer events.
193 * However, when using the glib IO channels approach, we don't have
194 * poll() and snd_mixer_poll_descriptors_revents(). Due to the design of
195 * glib, on_mixer_event() will be called for every fd whose status was
196 * changed. So, after each poll(), it's called for several times,
197 * not just once. Therefore, we cannot call snd_mixer_handle_events()
198 * directly in the event handler. Otherwise, it will get called for
199 * several times, which might clear unprocessed pending events in the queue.
200 * So, here we call it once in the event callback for the first fd.
201 * Then, we don't call it for the following fds. After all fds with changed
202 * status are handled, we remove this restriction in an idle handler.
203 * The next time the event callback is involked for the first fs, we can
204 * call snd_mixer_handle_events() again. Racing shouldn't happen here
205 * because the idle handler has the same priority as the io channel callback.
206 * So, io callbacks for future pending events should be in the next gmain
207 * iteration, and won't be affected.
208 */
f0861dee 209
210static gboolean asound_reset_mixer_evt_idle(VolumeALSAPlugin * vol)
55ccbddf 211{
251cfd3e
AG
212 if (!g_source_is_destroyed(g_main_current_source()))
213 vol->mixer_evt_idle = 0;
55ccbddf
HJYP
214 return FALSE;
215}
216
f0861dee 217/* Handler for I/O event on ALSA channel. */
218static gboolean asound_mixer_event(GIOChannel * channel, GIOCondition cond, gpointer vol_gpointer)
55ccbddf 219{
f0861dee 220 VolumeALSAPlugin * vol = (VolumeALSAPlugin *) vol_gpointer;
e0eb2f82 221 int res = 0;
f0861dee 222
9973857e
AG
223 if (g_source_is_destroyed(g_main_current_source()))
224 return FALSE;
225
f0861dee 226 if (vol->mixer_evt_idle == 0)
0fdd5153 227 {
f0861dee 228 vol->mixer_evt_idle = g_idle_add_full(G_PRIORITY_DEFAULT, (GSourceFunc) asound_reset_mixer_evt_idle, vol, NULL);
e0eb2f82 229 res = snd_mixer_handle_events(vol->mixer);
0fdd5153
HJYP
230 }
231
f0861dee 232 if (cond & G_IO_IN)
55ccbddf
HJYP
233 {
234 /* the status of mixer is changed. update of display is needed. */
f0861dee 235 volumealsa_update_display(vol);
55ccbddf 236 }
55ccbddf 237
e0eb2f82 238 if ((cond & G_IO_HUP) || (res < 0))
f0861dee 239 {
240 /* This means there're some problems with alsa. */
06e29ce1 241 g_warning("volumealsa: ALSA (or pulseaudio) had a problem: "
e0eb2f82 242 "volumealsa: snd_mixer_handle_events() = %d,"
06e29ce1 243 " cond 0x%x (IN: 0x%x, HUP: 0x%x).", res, cond,
e0eb2f82 244 G_IO_IN, G_IO_HUP);
9fc4c110
AG
245 gtk_widget_set_tooltip_text(vol->plugin, _("ALSA (or pulseaudio) had a problem."
246 " Please check the lxpanel logs."));
ad987a0c 247
b285684e
AG
248 if (vol->restart_idle == 0)
249 vol->restart_idle = g_timeout_add_seconds(1, asound_restart, vol);
ad987a0c 250
55ccbddf
HJYP
251 return FALSE;
252 }
253
55ccbddf
HJYP
254 return TRUE;
255}
256
ad987a0c
HG
257static gboolean asound_restart(gpointer vol_gpointer)
258{
259 VolumeALSAPlugin * vol = vol_gpointer;
260
b285684e
AG
261 if (g_source_is_destroyed(g_main_current_source()))
262 return FALSE;
263
ad987a0c
HG
264 asound_deinitialize(vol);
265
266 if (!asound_initialize(vol)) {
06e29ce1 267 g_warning("volumealsa: Re-initialization failed.");
ad987a0c
HG
268 return TRUE; // try again in a second
269 }
270
06e29ce1 271 g_warning("volumealsa: Restarted ALSA interface...");
ad987a0c 272
b285684e 273 vol->restart_idle = 0;
ad987a0c
HG
274 return FALSE;
275}
9d4601f2 276#endif
ad987a0c 277
f0861dee 278/* Initialize the ALSA interface. */
279static gboolean asound_initialize(VolumeALSAPlugin * vol)
49db0a37 280{
9d4601f2 281#ifdef DISABLE_ALSA
9fc4c110 282 /* Access the "default" device. */
9d4601f2
AG
283 vol->mixer_fd = open ("/dev/mixer", O_RDWR, 0);
284 if (vol->mixer_fd < 0)
285 {
286 g_warning("cannot initialize OSS mixer: %s", strerror(errno));
287 return FALSE;
288 }
289
290 //FIXME: is there a way to watch volume with OSS?
291#else
4542c20d 292 snd_mixer_open(&vol->mixer, 0);
decc23e2
AG
293 if (vol->used_device < 0)
294 snd_mixer_attach(vol->mixer, "default");
295 else
296 {
297 char id[16];
298
299 snprintf(id, sizeof(id), "hw:%d", vol->used_device);
300 snd_mixer_attach(vol->mixer, id);
301 }
4542c20d
HJYP
302 snd_mixer_selem_register(vol->mixer, NULL, NULL);
303 snd_mixer_load(vol->mixer);
304
895885e3
AG
305 if (vol->master_channel)
306 {
307 /* If user defined the channel then use it */
308 if (!asound_find_element(vol, (const char **)&vol->master_channel, 1))
309 return FALSE;
310 }
311 else
312 {
313 const char * def_channels[] = { "Master", "Front", "PCM", "LineOut" };
60d8abbb
MJ
314 /* Find Master element, or Front element, or PCM element, or LineOut element.
315 * If one of these succeeds, master_element is valid. */
895885e3
AG
316 if (!asound_find_element(vol, def_channels, G_N_ELEMENTS(def_channels)))
317 return FALSE;
318 }
4542c20d 319
f0861dee 320 /* Set the playback volume range as we wish it. */
418bf2f5
P
321 if ( ! vol->alsamixer_mapping)
322 snd_mixer_selem_set_playback_volume_range(vol->master_element, 0, 100);
9864f98f 323
f0861dee 324 /* Listen to events from ALSA. */
325 int n_fds = snd_mixer_poll_descriptors_count(vol->mixer);
326 struct pollfd * fds = g_new0(struct pollfd, n_fds);
55ccbddf 327
ad987a0c 328 vol->channels = g_new0(GIOChannel *, n_fds);
9973857e 329 vol->watches = g_new0(guint, n_fds);
ad987a0c
HG
330 vol->num_channels = n_fds;
331
f0861dee 332 snd_mixer_poll_descriptors(vol->mixer, fds, n_fds);
333 int i;
334 for (i = 0; i < n_fds; ++i)
55ccbddf 335 {
f0861dee 336 GIOChannel* channel = g_io_channel_unix_new(fds[i].fd);
9973857e 337 vol->watches[i] = g_io_add_watch(channel, G_IO_IN | G_IO_HUP, asound_mixer_event, vol);
ad987a0c 338 vol->channels[i] = channel;
55ccbddf 339 }
f0861dee 340 g_free(fds);
9d4601f2 341#endif
9864f98f 342 return TRUE;
49db0a37
FC
343}
344
ad987a0c
HG
345static void asound_deinitialize(VolumeALSAPlugin * vol)
346{
9d4601f2
AG
347#ifdef DISABLE_ALSA
348 if (vol->mixer_fd >= 0)
349 close(vol->mixer_fd);
350 vol->mixer_fd = -1;
351#else
fbe0300a 352 guint i;
ad987a0c 353
08a8293e 354 if (vol->mixer_evt_idle != 0) {
ad987a0c 355 g_source_remove(vol->mixer_evt_idle);
08a8293e
HG
356 vol->mixer_evt_idle = 0;
357 }
ad987a0c
HG
358
359 for (i = 0; i < vol->num_channels; i++) {
9973857e 360 g_source_remove(vol->watches[i]);
ad987a0c
HG
361 g_io_channel_shutdown(vol->channels[i], FALSE, NULL);
362 g_io_channel_unref(vol->channels[i]);
363 }
364 g_free(vol->channels);
9973857e 365 g_free(vol->watches);
ad987a0c 366 vol->channels = NULL;
9973857e 367 vol->watches = NULL;
ad987a0c 368 vol->num_channels = 0;
08a8293e
HG
369
370 snd_mixer_close(vol->mixer);
371 vol->master_element = NULL;
9d4601f2 372#endif
ad987a0c
HG
373}
374
f0861dee 375/* Get the presence of the mute control from the sound system. */
376static gboolean asound_has_mute(VolumeALSAPlugin * vol)
377{
9d4601f2
AG
378#ifdef DISABLE_ALSA
379 /* it's emulated with OSS */
380 return TRUE;
381#else
60d8abbb 382 return ((vol->master_element != NULL) ? snd_mixer_selem_has_playback_switch(vol->master_element) : FALSE);
9d4601f2 383#endif
f0861dee 384}
385
386/* Get the condition of the mute control from the sound system. */
387static gboolean asound_is_muted(VolumeALSAPlugin * vol)
388{
389 /* The switch is on if sound is not muted, and off if the sound is muted.
390 * Initialize so that the sound appears unmuted if the control does not exist. */
391 int value = 1;
9d4601f2
AG
392#ifdef DISABLE_ALSA
393 StereoVolume levels;
394
589cd33a 395 ioctl(vol->mixer_fd, MIXER_READ(vol->master_channel), &levels.value);
9d4601f2
AG
396 value = (levels.left + levels.right) >> 1;
397#else
60d8abbb
MJ
398 if (vol->master_element != NULL)
399 snd_mixer_selem_get_playback_switch(vol->master_element, 0, &value);
9d4601f2 400#endif
f0861dee 401 return (value == 0);
402}
403
9d4601f2 404#ifndef DISABLE_ALSA
418bf2f5
P
405static long lrint_dir(double x, int dir)
406{
407 if (dir > 0)
408 return lrint(ceil(x));
409 else if (dir < 0)
410 return lrint(floor(x));
411 else
412 return lrint(x);
413}
414
415static inline gboolean use_linear_dB_scale(long dBmin, long dBmax)
416{
417 return dBmax - dBmin <= MAX_LINEAR_DB_SCALE * 100;
418}
419
420static long get_normalized_volume(snd_mixer_elem_t *elem,
421 snd_mixer_selem_channel_id_t channel)
422{
423 long min, max, value;
424 double normalized, min_norm;
425 int err;
426
427 err = snd_mixer_selem_get_playback_dB_range(elem, &min, &max);
428 if (err < 0 || min >= max) {
429 err = snd_mixer_selem_get_playback_volume_range(elem, &min, &max);
430 if (err < 0 || min == max)
431 return 0;
432
433 err = snd_mixer_selem_get_playback_volume(elem, channel, &value);
434 if (err < 0)
435 return 0;
436
437 return lrint(100.0 * (value - min) / (double)(max - min));
438 }
439
440 err = snd_mixer_selem_get_playback_dB(elem, channel, &value);
441 if (err < 0)
442 return 0;
443
444 if (use_linear_dB_scale(min, max))
445 return lrint(100.0 * (value - min) / (double)(max - min));
446
447 normalized = exp10((value - max) / 6000.0);
448 if (min != SND_CTL_TLV_DB_GAIN_MUTE) {
449 min_norm = exp10((min - max) / 6000.0);
450 normalized = (normalized - min_norm) / (1 - min_norm);
451 }
452
453 return lrint(100.0 * normalized);
454}
9d4601f2 455#endif
418bf2f5 456
f0861dee 457/* Get the volume from the sound system.
458 * This implementation returns the average of the Front Left and Front Right channels. */
459static int asound_get_volume(VolumeALSAPlugin * vol)
49db0a37 460{
9d4601f2
AG
461#ifdef DISABLE_ALSA
462 StereoVolume levels;
463
589cd33a 464 ioctl(vol->mixer_fd, MIXER_READ(vol->master_channel), &levels.value);
9d4601f2
AG
465 return (levels.left + levels.right) >> 1;
466#else
60d8abbb
MJ
467 long aleft = 0;
468 long aright = 0;
418bf2f5 469
60d8abbb
MJ
470 if (vol->master_element != NULL)
471 {
418bf2f5
P
472 if ( ! vol->alsamixer_mapping)
473 {
474 snd_mixer_selem_get_playback_volume(vol->master_element, SND_MIXER_SCHN_FRONT_LEFT, &aleft);
475 snd_mixer_selem_get_playback_volume(vol->master_element, SND_MIXER_SCHN_FRONT_RIGHT, &aright);
476 }
477 else
478 {
479 aleft = get_normalized_volume(vol->master_element, SND_MIXER_SCHN_FRONT_LEFT);
480 aright = get_normalized_volume(vol->master_element, SND_MIXER_SCHN_FRONT_RIGHT);
481 }
60d8abbb 482 }
4542c20d 483 return (aleft + aright) >> 1;
9d4601f2 484#endif
49db0a37
FC
485}
486
9d4601f2 487#ifndef DISABLE_ALSA
418bf2f5
P
488static int set_normalized_volume(snd_mixer_elem_t *elem,
489 snd_mixer_selem_channel_id_t channel,
490 int vol,
491 int dir)
492{
493 long min, max, value;
494 double min_norm, volume;
495 int err;
496
497 volume = vol / 100.0;
498
499 err = snd_mixer_selem_get_playback_dB_range(elem, &min, &max);
500 if (err < 0 || min >= max) {
501 err = snd_mixer_selem_get_playback_volume_range(elem, &min, &max);
502 if (err < 0)
503 return err;
504
505 value = lrint_dir(volume * (max - min), dir) + min;
506 return snd_mixer_selem_set_playback_volume(elem, channel, value);
507 }
508
509 if (use_linear_dB_scale(min, max)) {
510 value = lrint_dir(volume * (max - min), dir) + min;
511 return snd_mixer_selem_set_playback_dB(elem, channel, value, dir);
512 }
513
514 if (min != SND_CTL_TLV_DB_GAIN_MUTE) {
515 min_norm = exp10((min - max) / 6000.0);
516 volume = volume * (1 - min_norm) + min_norm;
517 }
518 value = lrint_dir(6000.0 * log10(volume), dir) + max;
519
520 return snd_mixer_selem_set_playback_dB(elem, channel, value, dir);
521}
9d4601f2 522#endif
418bf2f5 523
f0861dee 524/* Set the volume to the sound system.
525 * This implementation sets the Front Left and Front Right channels to the specified value. */
526static void asound_set_volume(VolumeALSAPlugin * vol, int volume)
49db0a37 527{
418bf2f5
P
528 int dir = volume - asound_get_volume(vol);
529
530 /* Volume is set to the correct value already */
531 if (dir == 0)
532 return;
533
9d4601f2
AG
534#ifdef DISABLE_ALSA
535 StereoVolume levels;
536
537 levels.left = levels.right = volume;
589cd33a 538 ioctl(vol->mixer_fd, MIXER_WRITE(vol->master_channel), &levels.value);
9d4601f2 539#else
60d8abbb
MJ
540 if (vol->master_element != NULL)
541 {
418bf2f5
P
542 if ( ! vol->alsamixer_mapping)
543 {
544 snd_mixer_selem_set_playback_volume(vol->master_element, SND_MIXER_SCHN_FRONT_LEFT, volume);
545 snd_mixer_selem_set_playback_volume(vol->master_element, SND_MIXER_SCHN_FRONT_RIGHT, volume);
546 }
547 else
548 {
549 set_normalized_volume(vol->master_element, SND_MIXER_SCHN_FRONT_LEFT, volume, dir);
550 set_normalized_volume(vol->master_element, SND_MIXER_SCHN_FRONT_RIGHT, volume, dir);
551 }
60d8abbb 552 }
9d4601f2 553#endif
49db0a37
FC
554}
555
f0861dee 556/*** Graphics ***/
557
13d93418 558static void volumealsa_lookup_current_icon(VolumeALSAPlugin * vol, gboolean mute, int level)
49db0a37 559{
4896be7f 560 /* Change icon according to mute / volume */
9a82da9a 561 const char* icon_panel="audio-volume-muted-panel";
4896be7f
JL
562 const char* icon_fallback=ICONS_MUTE;
563 if (mute)
564 {
9a82da9a 565 icon_panel = "audio-volume-muted-panel";
4896be7f
JL
566 icon_fallback=ICONS_MUTE;
567 }
418bf2f5 568 else if (level >= 66)
c14620f2 569 {
9a82da9a 570 icon_panel = "audio-volume-high-panel";
4896be7f
JL
571 icon_fallback=ICONS_VOLUME_HIGH;
572 }
418bf2f5 573 else if (level >= 33)
4896be7f 574 {
9a82da9a 575 icon_panel = "audio-volume-medium-panel";
4896be7f
JL
576 icon_fallback=ICONS_VOLUME_MEDIUM;
577 }
578 else if (level > 0)
579 {
9a82da9a 580 icon_panel = "audio-volume-low-panel";
4896be7f
JL
581 icon_fallback=ICONS_VOLUME_LOW;
582 }
583
7192ab49 584 vol->icon_panel = icon_panel;
13d93418 585 vol->icon_fallback = icon_fallback;
7192ab49
JL
586}
587
13d93418 588static void volumealsa_update_current_icon(VolumeALSAPlugin * vol, gboolean mute, int level)
7192ab49 589{
13d93418
P
590 /* Find suitable icon */
591 volumealsa_lookup_current_icon(vol, mute, level);
7192ab49 592
4896be7f 593 /* Change icon, fallback to default icon if theme doesn't exsit */
6febec6d 594 lxpanel_image_change_icon(vol->tray_icon, vol->icon_panel, vol->icon_fallback);
f0861dee 595
f0861dee 596 /* Display current level in tooltip. */
597 char * tooltip = g_strdup_printf("%s %d", _("Volume control"), level);
b285684e 598 gtk_widget_set_tooltip_text(vol->plugin, tooltip);
f0861dee 599 g_free(tooltip);
49db0a37
FC
600}
601
13d93418
P
602/*
603 * Here we just update volume's vertical scale and mute check button.
604 * The rest will be updated by signal handelrs.
605 */
606static void volumealsa_update_display(VolumeALSAPlugin * vol)
607{
608 /* Mute. */
609 gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(vol->mute_check), asound_is_muted(vol));
610 gtk_widget_set_sensitive(vol->mute_check, (asound_has_mute(vol)));
611
612 /* Volume. */
613 if (vol->volume_scale != NULL)
614 {
615 gtk_range_set_value(GTK_RANGE(vol->volume_scale), asound_get_volume(vol));
616 }
617}
7192ab49 618
9fc4c110
AG
619struct mixer_desc
620{
621 char * cmd;
622 char * exec;
623 gboolean needs_pa;
624 gboolean needs_term;
625};
626
627const struct mixer_desc mixers[] = {
628 /* those with needs_pa should be first! */
629 { "gnome-sound-applet", "gnome-sound-applet", TRUE, FALSE },
630 { "pavucontrol", "pavucontrol", TRUE, FALSE },
631 { "gnome-alsamixer", "gnome-alsamixer", FALSE, FALSE },
632 { "alsamixergui", "alsamixergui", FALSE, FALSE },
633 { "alsamixer", "alsamixer", FALSE, TRUE },
634 { NULL }
635};
636
895885e3 637static void volume_run_mixer(VolumeALSAPlugin * vol)
49db0a37 638{
895885e3
AG
639 char *path = NULL;
640 const gchar *command_line = NULL;
641 GAppInfoCreateFlags flags = G_APP_INFO_CREATE_NONE;
9fc4c110 642 int i;
b285684e 643
895885e3 644 /* check if command line was configured */
9fc4c110
AG
645 if (config_setting_lookup_string(vol->settings, "MixerCommand", &command_line))
646 if (config_setting_lookup_int(vol->settings, "MixerCommandTerm", &i) && i)
647 flags = G_APP_INFO_CREATE_NEEDS_TERMINAL;
895885e3
AG
648
649 /* if command isn't set in settings then let guess it */
895885e3
AG
650 if (command_line == NULL)
651 {
9fc4c110
AG
652 i = 0;
653 path = g_find_program_in_path("pulseaudio");
654 /* Assume that when pulseaudio is installed, it's launching every time */
655 if (path)
656 g_free(path);
657 /* Fallback to alsamixer when PA is not running, or when no PA utility is find */
658 else while (mixers[i].cmd && mixers[i].needs_pa)
659 i++;
660 for (; mixers[i].cmd; i++)
895885e3 661 {
9fc4c110
AG
662 if ((path = g_find_program_in_path(mixers[i].exec)))
663 {
664 command_line = mixers[i].cmd;
665 if (mixers[i].needs_term)
666 flags = G_APP_INFO_CREATE_NEEDS_TERMINAL;
667 g_free(path);
668 break;
669 }
895885e3
AG
670 }
671 }
895885e3
AG
672
673 if (command_line)
d220e22a 674 {
895885e3
AG
675 fm_launch_command_simple(NULL, NULL, flags, command_line, NULL);
676 }
677 else
678 {
679 fm_show_error(NULL, NULL,
680 _("Error, you need to install an application to configure"
681 " the sound (pavucontrol, alsamixer ...)"));
682 }
683}
684
685static void _check_click(VolumeALSAPlugin * vol, int button, GdkModifierType mod)
686{
687 if (vol->slider_click == button && vol->slider_click_mods == mod)
688 {
689 /* Left-click. Show or hide the popup window. */
f0861dee 690 if (vol->show_popup)
d220e22a 691 {
f0861dee 692 gtk_widget_hide(vol->popup_window);
693 vol->show_popup = FALSE;
d220e22a 694 }
695 else
696 {
f0861dee 697 gtk_widget_show_all(vol->popup_window);
698 vol->show_popup = TRUE;
d220e22a 699 }
895885e3
AG
700 }
701 if (vol->mute_click == button && vol->mute_click_mods == mod)
702 {
703 /* Middle-click. Toggle the mute status. */
704 gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(vol->mute_check), ! gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(vol->mute_check)));
705 }
706 if (vol->mixer_click == button && vol->mixer_click_mods == mod)
707 {
708 volume_run_mixer(vol);
709 }
710}
711
712/* Handler for "button-press-event" signal on main widget. */
713static gboolean volumealsa_button_press_event(GtkWidget * widget, GdkEventButton * event, LXPanel * panel)
714{
715 VolumeALSAPlugin * vol = lxpanel_plugin_get_data(widget);
716
717 if (event->button == 1)
718 {
719 _check_click(vol, 1,
720 event->state & gtk_accelerator_get_default_mod_mask());
d220e22a 721 }
f0861dee 722
9d4601f2
AG
723 return FALSE;
724}
725
726static gboolean volumealsa_button_release_event(GtkWidget * widget, GdkEventButton * event, VolumeALSAPlugin * vol)
727{
895885e3 728 if (event->button != 1)
d220e22a 729 {
895885e3
AG
730 _check_click(vol, event->button,
731 event->state & gtk_accelerator_get_default_mod_mask());
4542c20d 732 }
9d4601f2 733 return FALSE;
49db0a37
FC
734}
735
f0861dee 736/* Handler for "focus-out" signal on popup window. */
737static gboolean volumealsa_popup_focus_out(GtkWidget * widget, GdkEvent * event, VolumeALSAPlugin * vol)
49db0a37 738{
f0861dee 739 /* Hide the widget. */
740 gtk_widget_hide(vol->popup_window);
741 vol->show_popup = FALSE;
742 return FALSE;
49db0a37
FC
743}
744
4ec924bc
MJ
745/* Handler for "map" signal on popup window. */
746static void volumealsa_popup_map(GtkWidget * widget, VolumeALSAPlugin * vol)
747{
b285684e 748 lxpanel_plugin_adjust_popup_position(widget, vol->plugin);
4ec924bc
MJ
749}
750
f0861dee 751/* Handler for "value_changed" signal on popup window vertical scale. */
752static void volumealsa_popup_scale_changed(GtkRange * range, VolumeALSAPlugin * vol)
0fdd5153 753{
13d93418
P
754 int level = gtk_range_get_value(GTK_RANGE(vol->volume_scale));
755 gboolean mute = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(vol->mute_check));
756
f0861dee 757 /* Reflect the value of the control to the sound system. */
13d93418 758 asound_set_volume(vol, level);
f0861dee 759
13d93418
P
760 /*
761 * Redraw the controls.
762 * Scale and check button do not need to be updated, as these are always
763 * in sync with user's actions.
764 */
765 volumealsa_update_current_icon(vol, mute, level);
f0861dee 766}
767
768/* Handler for "scroll-event" signal on popup window vertical scale. */
769static void volumealsa_popup_scale_scrolled(GtkScale * scale, GdkEventScroll * evt, VolumeALSAPlugin * vol)
770{
771 /* Get the state of the vertical scale. */
772 gdouble val = gtk_range_get_value(GTK_RANGE(vol->volume_scale));
773
774 /* Dispatch on scroll direction to update the value. */
775 if ((evt->direction == GDK_SCROLL_UP) || (evt->direction == GDK_SCROLL_LEFT))
0fdd5153 776 val += 2;
f0861dee 777 else
0fdd5153 778 val -= 2;
f0861dee 779
780 /* Reset the state of the vertical scale. This provokes a "value_changed" event. */
781 gtk_range_set_value(GTK_RANGE(vol->volume_scale), CLAMP((int)val, 0, 100));
0fdd5153
HJYP
782}
783
f0861dee 784/* Handler for "toggled" signal on popup window mute checkbox. */
785static void volumealsa_popup_mute_toggled(GtkWidget * widget, VolumeALSAPlugin * vol)
49db0a37 786{
13d93418
P
787 int level = gtk_range_get_value(GTK_RANGE(vol->volume_scale));
788 gboolean mute = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(vol->mute_check));
f0861dee 789
790 /* Reflect the mute toggle to the sound system. */
9d4601f2
AG
791#ifdef DISABLE_ALSA
792 if (mute)
793 {
794 vol->vol_before_mute = level;
795 asound_set_volume(vol, 0);
796 }
797 else
798 {
799 asound_set_volume(vol, vol->vol_before_mute);
800 }
801#else
60d8abbb
MJ
802 if (vol->master_element != NULL)
803 {
804 int chn;
805 for (chn = 0; chn <= SND_MIXER_SCHN_LAST; chn++)
13d93418 806 snd_mixer_selem_set_playback_switch(vol->master_element, chn, ((mute) ? 0 : 1));
60d8abbb 807 }
9d4601f2 808#endif
4542c20d 809
13d93418
P
810 /*
811 * Redraw the controls.
812 * Scale and check button do not need to be updated, as these are always
813 * in sync with user's actions.
814 */
815 volumealsa_update_current_icon(vol, mute, level);
49db0a37
FC
816}
817
650d2ca5
AG
818/* Hotkeys handlers */
819static void volume_up(const char *keystring, gpointer user_data)
820{
821 VolumeALSAPlugin * vol = (VolumeALSAPlugin *)user_data;
822 int val = (int)gtk_range_get_value(GTK_RANGE(vol->volume_scale)) + 2;
823 gtk_range_set_value(GTK_RANGE(vol->volume_scale), CLAMP(val, 0, 100));
824}
825
826static void volume_down(const char *keystring, gpointer user_data)
827{
828 VolumeALSAPlugin * vol = (VolumeALSAPlugin *)user_data;
829 int val = (int)gtk_range_get_value(GTK_RANGE(vol->volume_scale)) - 2;
830 gtk_range_set_value(GTK_RANGE(vol->volume_scale), CLAMP(val, 0, 100));
831}
832
833static void volume_mute(const char *keystring, gpointer user_data)
834{
835 VolumeALSAPlugin * vol = (VolumeALSAPlugin *)user_data;
836 gboolean muted = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(vol->mute_check));
837 gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(vol->mute_check), !muted);
838}
839
f0861dee 840/* Build the window that appears when the top level widget is clicked. */
b285684e 841static void volumealsa_build_popup_window(GtkWidget *p)
49db0a37 842{
b285684e 843 VolumeALSAPlugin * vol = lxpanel_plugin_get_data(p);
f0861dee 844
845 /* Create a new window. */
1918164d 846 vol->popup_window = gtk_window_new(GTK_WINDOW_POPUP);
f0861dee 847 gtk_window_set_decorated(GTK_WINDOW(vol->popup_window), FALSE);
848 gtk_container_set_border_width(GTK_CONTAINER(vol->popup_window), 5);
849 gtk_window_set_default_size(GTK_WINDOW(vol->popup_window), 80, 140);
850 gtk_window_set_skip_taskbar_hint(GTK_WINDOW(vol->popup_window), TRUE);
851 gtk_window_set_skip_pager_hint(GTK_WINDOW(vol->popup_window), TRUE);
91f2b215 852 gtk_window_set_type_hint(GTK_WINDOW(vol->popup_window), GDK_WINDOW_TYPE_HINT_UTILITY);
f0861dee 853
4ec924bc 854 /* Connect signals. */
93a217b3 855 g_signal_connect(G_OBJECT(vol->popup_window), "focus-out-event", G_CALLBACK(volumealsa_popup_focus_out), vol);
4ec924bc 856 g_signal_connect(G_OBJECT(vol->popup_window), "map", G_CALLBACK(volumealsa_popup_map), vol);
f0861dee 857
858 /* Create a scrolled window as the child of the top level window. */
859 GtkWidget * scrolledwindow = gtk_scrolled_window_new(NULL, NULL);
860 gtk_container_set_border_width (GTK_CONTAINER(scrolledwindow), 0);
861 gtk_widget_show(scrolledwindow);
862 gtk_container_add(GTK_CONTAINER(vol->popup_window), scrolledwindow);
09fa171b 863 gtk_widget_set_can_focus(scrolledwindow, FALSE);
4542c20d 864 gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW (scrolledwindow), GTK_POLICY_NEVER, GTK_POLICY_NEVER);
63de7afd 865 gtk_scrolled_window_set_shadow_type(GTK_SCROLLED_WINDOW(scrolledwindow), GTK_SHADOW_NONE);
4542c20d 866
f0861dee 867 /* Create a viewport as the child of the scrolled window. */
868 GtkWidget * viewport = gtk_viewport_new(NULL, NULL);
869 gtk_container_add(GTK_CONTAINER(scrolledwindow), viewport);
870 gtk_viewport_set_shadow_type(GTK_VIEWPORT(viewport), GTK_SHADOW_NONE);
4542c20d
HJYP
871 gtk_widget_show(viewport);
872
f0861dee 873 /* Create a frame as the child of the viewport. */
874 GtkWidget * frame = gtk_frame_new(_("Volume"));
4542c20d
HJYP
875 gtk_frame_set_shadow_type(GTK_FRAME(frame), GTK_SHADOW_IN);
876 gtk_container_add(GTK_CONTAINER(viewport), frame);
877
f0861dee 878 /* Create a vertical box as the child of the frame. */
879 GtkWidget * box = gtk_vbox_new(FALSE, 0);
880 gtk_container_add(GTK_CONTAINER(frame), box);
4542c20d 881
f0861dee 882 /* Create a vertical scale as the child of the vertical box. */
883 vol->volume_scale = gtk_vscale_new(GTK_ADJUSTMENT(gtk_adjustment_new(100, 0, 100, 0, 0, 0)));
884 gtk_scale_set_draw_value(GTK_SCALE(vol->volume_scale), FALSE);
885 gtk_range_set_inverted(GTK_RANGE(vol->volume_scale), TRUE);
886 gtk_box_pack_start(GTK_BOX(box), vol->volume_scale, TRUE, TRUE, 0);
4542c20d 887
f0861dee 888 /* Value-changed and scroll-event signals. */
93a217b3 889 vol->volume_scale_handler = g_signal_connect(vol->volume_scale, "value-changed", G_CALLBACK(volumealsa_popup_scale_changed), vol);
f0861dee 890 g_signal_connect(vol->volume_scale, "scroll-event", G_CALLBACK(volumealsa_popup_scale_scrolled), vol);
49db0a37 891
f0861dee 892 /* Create a check button as the child of the vertical box. */
0fdd5153 893 vol->mute_check = gtk_check_button_new_with_label(_("Mute"));
0fdd5153 894 gtk_box_pack_end(GTK_BOX(box), vol->mute_check, FALSE, FALSE, 0);
f0861dee 895 vol->mute_check_handler = g_signal_connect(vol->mute_check, "toggled", G_CALLBACK(volumealsa_popup_mute_toggled), vol);
49db0a37 896
f0861dee 897 /* Set background to default. */
b285684e 898 gtk_widget_set_style(viewport, panel_get_defstyle(vol->panel));
49db0a37
FC
899}
900
f0861dee 901/* Plugin constructor. */
a7bd16a4 902static GtkWidget *volumealsa_constructor(LXPanel *panel, config_setting_t *settings)
49db0a37 903{
f0861dee 904 /* Allocate and initialize plugin context and set into Plugin private data pointer. */
905 VolumeALSAPlugin * vol = g_new0(VolumeALSAPlugin, 1);
b285684e 906 GtkWidget *p;
895885e3 907 const char *tmp_str;
4542c20d 908
9d4601f2 909#ifndef DISABLE_ALSA
418bf2f5 910 /* Read config necessary for proper initialization of ALSA. */
a46b378a 911 config_setting_lookup_int(settings, "UseAlsamixerVolumeMapping", &vol->alsamixer_mapping);
895885e3
AG
912 if (config_setting_lookup_string(settings, "MasterChannel", &tmp_str))
913 vol->master_channel = g_strdup(tmp_str);
decc23e2
AG
914 if (!config_setting_lookup_int(settings, "CardNumber", &vol->used_device))
915 vol->used_device = -1;
589cd33a
AG
916#else
917 vol->master_channel = SOUND_MIXER_VOLUME;
918 if (config_setting_lookup_string(settings, "MasterChannel", &tmp_str))
919 {
920 if (strcmp(tmp_str, "PCM") == 0)
921 vol->master_channel = SOUND_MIXER_PCM;
922 else if (strcmp(tmp_str, "Headphone") == 0)
923 vol->master_channel = SOUND_MIXER_PHONEOUT;
924 }
9d4601f2 925#endif
895885e3
AG
926 if (config_setting_lookup_string(settings, "MuteButton", &tmp_str))
927 vol->mute_click = panel_config_click_parse(tmp_str, &vol->mute_click_mods);
928 else
929 vol->mute_click = 2; /* middle-click default */
930 if (config_setting_lookup_string(settings, "SliderButton", &tmp_str))
931 vol->slider_click = panel_config_click_parse(tmp_str, &vol->slider_click_mods);
932 else
933 vol->slider_click = 1; /* left-click default */
934 if (config_setting_lookup_string(settings, "MixerButton", &tmp_str))
935 vol->mixer_click = panel_config_click_parse(tmp_str, &vol->mixer_click_mods);
650d2ca5
AG
936 if (config_setting_lookup_string(settings, "VolumeUpKey", &tmp_str))
937 lxpanel_apply_hotkey(&vol->hotkey_up, tmp_str, volume_up, vol, FALSE);
938 if (config_setting_lookup_string(settings, "VolumeDownKey", &tmp_str))
9fc4c110 939 lxpanel_apply_hotkey(&vol->hotkey_down, tmp_str, volume_down, vol, FALSE);
650d2ca5 940 if (config_setting_lookup_string(settings, "VolumeMuteKey", &tmp_str))
9fc4c110 941 lxpanel_apply_hotkey(&vol->hotkey_mute, tmp_str, volume_mute, vol, FALSE);
418bf2f5
P
942
943 /* Initialize ALSA. If that fails, present nothing. */
944 if ( ! asound_initialize(vol))
945 {
946 volumealsa_destructor(vol);
947 return NULL;
948 }
949
a46b378a
AG
950 /* Allocate top level widget and set into Plugin widget pointer. */
951 vol->panel = panel;
952 vol->plugin = p = gtk_event_box_new();
953 vol->settings = settings;
954 lxpanel_plugin_set_data(p, vol, volumealsa_destructor);
a46b378a
AG
955 gtk_widget_set_tooltip_text(p, _("Volume control"));
956
f0861dee 957 /* Allocate icon as a child of top level. */
6febec6d
AG
958 vol->tray_icon = lxpanel_image_new_for_icon(panel, "audio-volume-muted-panel",
959 -1, ICONS_MUTE);
b285684e 960 gtk_container_add(GTK_CONTAINER(p), vol->tray_icon);
4542c20d 961
f0861dee 962 /* Initialize window to appear when icon clicked. */
963 volumealsa_build_popup_window(p);
49db0a37 964
f0861dee 965 /* Connect signals. */
b285684e 966 g_signal_connect(G_OBJECT(p), "scroll-event", G_CALLBACK(volumealsa_popup_scale_scrolled), vol );
9d4601f2 967 g_signal_connect(G_OBJECT(p), "button-release-event", G_CALLBACK(volumealsa_button_release_event), vol );
49db0a37 968
f0861dee 969 /* Update the display, show the widget, and return. */
970 volumealsa_update_display(vol);
35e581ff 971 volumealsa_update_current_icon(vol, asound_is_muted(vol), asound_get_volume(vol));
b285684e
AG
972 gtk_widget_show_all(p);
973 return p;
f0861dee 974}
1abc8a6a 975
f0861dee 976/* Plugin destructor. */
b285684e 977static void volumealsa_destructor(gpointer user_data)
f0861dee 978{
b285684e 979 VolumeALSAPlugin * vol = (VolumeALSAPlugin *) user_data;
49db0a37 980
650d2ca5
AG
981 lxpanel_apply_hotkey(&vol->hotkey_up, NULL, NULL, NULL, FALSE);
982 lxpanel_apply_hotkey(&vol->hotkey_down, NULL, NULL, NULL, FALSE);
983 lxpanel_apply_hotkey(&vol->hotkey_mute, NULL, NULL, NULL, FALSE);
984
ad987a0c 985 asound_deinitialize(vol);
4542c20d 986
f0861dee 987 /* If the dialog box is open, dismiss it. */
988 if (vol->popup_window != NULL)
989 gtk_widget_destroy(vol->popup_window);
49db0a37 990
9d4601f2 991#ifndef DISABLE_ALSA
b285684e
AG
992 if (vol->restart_idle)
993 g_source_remove(vol->restart_idle);
895885e3
AG
994
995 g_free(vol->master_channel);
9d4601f2 996#endif
b285684e 997
f0861dee 998 /* Deallocate all memory. */
999 g_free(vol);
49db0a37
FC
1000}
1001
9fc4c110
AG
1002#ifndef DISABLE_ALSA
1003static GtkListStore *alsa_make_channels_list(VolumeALSAPlugin *vol, int *active)
1004{
1005 GtkListStore *list;
1006 GtkTreeIter iter;
1007 snd_mixer_selem_id_t *sid;
1008 snd_mixer_elem_t *elem;
1009 const char *name;
1010 int i;
1011
1012 snd_mixer_selem_id_alloca(&sid);
1013 list = gtk_list_store_new(2, G_TYPE_STRING, G_TYPE_STRING); /* desc, value */
1014 for (elem = snd_mixer_first_elem(vol->mixer), i = 0; elem != NULL;
1015 elem = snd_mixer_elem_next(elem), i++)
1016 {
1017 if (snd_mixer_selem_is_active(elem) &&
1018 snd_mixer_selem_has_playback_volume(elem) &&
1019 !snd_mixer_selem_has_capture_volume(elem) &&
1020 !snd_mixer_selem_has_capture_switch(elem))
1021 {
1022 snd_mixer_selem_get_id(elem, sid);
1023 name = snd_mixer_selem_id_get_name(sid);
1024 gtk_list_store_insert_with_values(list, &iter, i, 0, _(name),
1025 1, name, -1);
1026 if (elem == vol->master_element)
1027 *active = i;
1028 }
1029 }
1030 return list;
1031}
1032
1033static void card_selector_changed(GtkComboBox *card_selector, VolumeALSAPlugin *vol)
1034{
1035 GtkTreeModel *model = gtk_combo_box_get_model(card_selector);
1036 GtkTreeIter iter;
1037 int old_card = vol->used_device;
1038 int i = gtk_combo_box_get_active(card_selector);
1039 char *old_channel;
1040
1041 gtk_tree_model_iter_nth_child(model, &iter, NULL, i);
1042 gtk_tree_model_get(model, &iter, 1, &vol->used_device, -1);
1043 asound_deinitialize(vol);
1044 if (!asound_initialize(vol))
1045 {
1046 /* could not change card with the same master channel, try default */
1047 old_channel = vol->master_channel;
1048 vol->master_channel = NULL;
1049 asound_deinitialize(vol);
1050 if (!asound_initialize(vol))
1051 {
1052 g_warning("could not set card to %d", vol->used_device);
1053 vol->master_channel = old_channel;
1054 vol->used_device = old_card;
1055 /* schedule to restart with old settings */
1056 if (vol->restart_idle == 0)
1057 vol->restart_idle = g_timeout_add_seconds(1, asound_restart, vol);
1058 return;
1059 }
1060 g_free(old_channel);
1061 config_group_set_string(vol->settings, "MasterChannel", NULL);
1062 }
1063 /* remember and apply selection */
1064 volumealsa_update_display(vol);
1065 config_group_set_int(vol->settings, "CardNumber", vol->used_device);
1066 /* rebuild channel selection list */
1067 i = -1;
1068 model = GTK_TREE_MODEL(alsa_make_channels_list(vol, &i));
1069 gtk_combo_box_set_model(GTK_COMBO_BOX(vol->channel_selector), model);
1070 gtk_combo_box_set_active(GTK_COMBO_BOX(vol->channel_selector), i);
632fb2d5 1071 g_object_unref(model);
9fc4c110
AG
1072}
1073#endif
1074
1075static void channel_selector_changed(GtkComboBox *channel_selector, VolumeALSAPlugin *vol)
1076{
1077 GtkTreeModel *model = gtk_combo_box_get_model(channel_selector);
1078 GtkTreeIter iter;
1079#ifdef DISABLE_ALSA
1080 int ch; /* channel index */
1081#else
1082 char *ch; /* channel name */
1083#endif
1084 int i = gtk_combo_box_get_active(channel_selector);
1085
1086 gtk_tree_model_iter_nth_child(model, &iter, NULL, i);
1087 gtk_tree_model_get(model, &iter, 1, &ch, -1);
1088#ifdef DISABLE_ALSA
1089 config_group_set_int(vol->settings, "MasterChannel", ch);
1090#else
1091 config_group_set_string(vol->settings, "MasterChannel", ch);
1092 asound_find_element(vol, (const char **)&ch, 1); //FIXME: is error possible?
1093 /* Set the playback volume range as we wish it. */
1094 if (!vol->alsamixer_mapping)
1095 snd_mixer_selem_set_playback_volume_range(vol->master_element, 0, 100);
1096 /* g_debug("MasterChannel changed: %s", ch); */
1097 g_free(vol->master_channel);
1098#endif
1099 vol->master_channel = ch; /* just take it instead of alloc + free */
1100 volumealsa_update_display(vol);
1101}
1102
1103static void mixer_selector_changed(GtkComboBox *mixer_selector, VolumeALSAPlugin *vol)
1104{
1105 GtkWidget *mixer_entry = gtk_bin_get_child(GTK_BIN(mixer_selector));
1106 const char *cmd, *set;
1107 GtkTreeModel *model;
1108 GtkTreeIter iter;
1109 int i;
1110
1111 i = gtk_combo_box_get_active(mixer_selector);
1112 if (i < 0)
1113 /* it was just editing */
1114 return;
1115 if (!config_setting_lookup_string(vol->settings, "MixerCommand", &set))
1116 set = NULL;
1117 cmd = gtk_entry_get_text((GtkEntry *)mixer_entry);
1118 if (set)
1119 {
1120 if (strcmp(set, cmd) == 0)
1121 /* not changed */
1122 return;
1123 }
1124 else if (gtk_combo_box_get_active(mixer_selector) == 0)
1125 /* it's left at default */
1126 return;
1127 model = gtk_combo_box_get_model(mixer_selector);
1128 gtk_tree_model_iter_nth_child(model, &iter, NULL, i);
1129 gtk_tree_model_get(model, &iter, 1, &i, -1);
1130 /* g_debug("new choice: %s needs_term=%d", cmd, i); */
1131 config_group_set_string(vol->settings, "MixerCommand", cmd);
1132 config_group_set_int(vol->settings, "MixerCommandTerm", i);
1133}
1134
1135struct mixer_selector_check_data
1136{
1137 GtkComboBox *mixer_selector;
1138 const char *text;
1139 int needs_term;
1140};
1141
1142static gboolean mixer_selector_check(GtkTreeModel *model, GtkTreePath *path,
1143 GtkTreeIter *iter, gpointer user_data)
1144{
1145 struct mixer_selector_check_data *data = user_data;
1146 char *cmd;
1147
1148 gtk_tree_model_get(model, iter, 0, &cmd, 1, &data->needs_term, -1);
1149 if (cmd && strcmp(cmd, data->text) == 0)
1150 {
1151 int *indices = gtk_tree_path_get_indices(path);
1152 gtk_combo_box_set_active(data->mixer_selector, indices[0]);
1153 g_free(cmd);
1154 return TRUE;
1155 }
1156 g_free(cmd);
1157 return FALSE;
1158}
1159
1160static gboolean mixer_selector_focus_out(GtkWidget *mixer_entry,
1161 GdkEvent *evt, VolumeALSAPlugin *vol)
1162{
1163 struct mixer_selector_check_data data;
1164 GtkTreeModel *model;
1165
1166 data.mixer_selector = GTK_COMBO_BOX(gtk_widget_get_parent(mixer_entry));
1167 data.text = gtk_entry_get_text((GtkEntry *)mixer_entry);
1168 data.needs_term = 0;
1169 model = gtk_combo_box_get_model(data.mixer_selector);
1170
1171 /* check if current value is one of model choices */
1172 if (gtk_combo_box_get_active(data.mixer_selector) < 0)
1173 gtk_tree_model_foreach(model, &mixer_selector_check, &data);
1174 /* check executable and remember selection */
1175 if (gtk_combo_box_get_active(data.mixer_selector) < 0)
1176 {
1177 /* check only user input since predefined choices were tested already */
1178 char *exec, *path;
1179
1180 /* g_debug("user entered mixer: %s", data.text); */
1181 exec = strchr(data.text, ' ');
1182 if (exec)
1183 exec = g_strndup(data.text, exec - data.text);
1184 path = g_find_program_in_path(exec ? exec : data.text);
1185 g_free(exec);
1186 g_free(path);
1187 if (path == NULL)
1188 {
1189 /* invalid executable requested, ignore it then */
1190 g_warning("%s cannot be executed, ignoring it", data.text);
1191 return FALSE;
1192 }
1193 }
1194 config_group_set_string(vol->settings, "MixerCommand", data.text);
1195 config_group_set_int(vol->settings, "MixerCommandTerm", data.needs_term);
1196 return FALSE;
1197}
1198
1199static gboolean mixer_selector_key_press(GtkWidget *mixer_entry,
1200 GdkEventKey *evt, VolumeALSAPlugin *vol)
1201{
1202 if (evt->keyval == GDK_KEY_Return)
1203 /* loose focus on Enter press */
1204 gtk_window_set_focus(GTK_WINDOW(gtk_widget_get_toplevel(mixer_entry)), NULL);
1205 return FALSE;
1206}
1207
1208static gboolean mute_button_changed(GtkWidget *btn, char *click, VolumeALSAPlugin *vol)
1209{
1210 int n;
1211 GdkModifierType mods;
1212
1213 n = panel_config_click_parse(click, &mods);
1214 if (n == 0 || ((n != vol->mixer_click || mods != vol->mixer_click_mods) &&
1215 (n != vol->slider_click || mods != vol->slider_click_mods)))
1216 {
1217 config_group_set_string(vol->settings, "MuteButton", click);
1218 vol->mute_click = n;
1219 vol->mute_click_mods = mods;
1220 return TRUE;
1221 }
1222 //FIXME: show a message?
1223 return FALSE;
1224}
1225
1226static gboolean mixer_button_changed(GtkWidget *btn, char *click, VolumeALSAPlugin *vol)
1227{
1228 int n;
1229 GdkModifierType mods;
1230
1231 n = panel_config_click_parse(click, &mods);
1232 if (n == 0 || ((n != vol->mute_click || mods != vol->mute_click_mods) &&
1233 (n != vol->slider_click || mods != vol->slider_click_mods)))
1234 {
1235 config_group_set_string(vol->settings, "MixerButton", click);
1236 vol->mixer_click = n;
1237 vol->mixer_click_mods = mods;
1238 return TRUE;
1239 }
1240 //FIXME: show a message?
1241 return FALSE;
1242}
1243
1244static gboolean volume_button_changed(GtkWidget *btn, char *click, VolumeALSAPlugin *vol)
1245{
1246 int n;
1247 GdkModifierType mods;
1248
1249 n = panel_config_click_parse(click, &mods);
1250 if (n == 0 || ((n != vol->mixer_click || mods != vol->mixer_click_mods) &&
1251 (n != vol->mute_click || mods != vol->mute_click_mods)))
1252 {
1253 config_group_set_string(vol->settings, "SliderButton", click);
1254 vol->slider_click = n;
1255 vol->slider_click_mods = mods;
1256 return TRUE;
1257 }
1258 //FIXME: show a message?
1259 return FALSE;
1260}
1261
1262static gboolean up_key_changed(GtkWidget *btn, char *click, VolumeALSAPlugin *vol)
1263{
1264 gboolean res;
1265
1266 res = lxpanel_apply_hotkey(&vol->hotkey_up, click, &volume_up, vol, TRUE);
1267 if (res)
1268 config_group_set_string(vol->settings, "VolumeUpKey", click);
1269 return res;
1270}
1271
1272static gboolean down_key_changed(GtkWidget *btn, char *click, VolumeALSAPlugin *vol)
1273{
1274 gboolean res;
1275
1276 res = lxpanel_apply_hotkey(&vol->hotkey_down, click, &volume_down, vol, TRUE);
1277 if (res)
1278 config_group_set_string(vol->settings, "VolumeDownKey", click);
1279 return res;
1280}
1281
1282static gboolean mute_key_changed(GtkWidget *btn, char *click, VolumeALSAPlugin *vol)
1283{
1284 gboolean res;
1285
1286 res = lxpanel_apply_hotkey(&vol->hotkey_mute, click, &volume_mute, vol, TRUE);
1287 if (res)
1288 config_group_set_string(vol->settings, "VolumeMuteKey", click);
1289 return res;
1290}
1291
1292#if THING_THAT_NEVER_HAPPEN
1293/* Just to have these translated */
1294N_("Line"), N_("LineOut"), N_("Front"), N_("Surround"), N_("Center"), N_("Speaker+LO");
1295#endif
1296
552475ec 1297/* Callback when the configuration dialog is to be shown. */
752ee4e2 1298static GtkWidget *volumealsa_configure(LXPanel *panel, GtkWidget *p)
552475ec 1299{
9fc4c110
AG
1300 VolumeALSAPlugin *vol = lxpanel_plugin_get_data(p);
1301 const char *tmp_str;
1302 char *path;
1303 GtkListStore *list;
1304 GtkCellRenderer *column;
1305#ifndef DISABLE_ALSA
1306 snd_mixer_selem_id_t *sid;
1307 snd_mixer_elem_t *elem;
1308 snd_hctl_t *hctl;
1309 GtkWidget *card_selector;
1310#endif
1311 GtkWidget *mute_button;
1312 GtkWidget *volume_button;
1313 GtkWidget *mixer_button;
1314 GtkWidget *up_key;
1315 GtkWidget *down_key;
1316 GtkWidget *mute_key;
1317 GtkWidget *mixer_selector;
1318 GtkWidget *mixer_entry;
1319 GtkTreeIter iter;
1320 int active = 0;
1321 int i = 0;
1322#ifndef DISABLE_ALSA
1323 int j = -1;
552475ec 1324
9fc4c110
AG
1325 snd_mixer_selem_id_alloca(&sid);
1326 /* setup card selector */
1327 list = gtk_list_store_new(2, G_TYPE_STRING, G_TYPE_INT); /* desc, num */
1328 if (snd_hctl_open(&hctl, "default", 0) == 0)
1329 {
1330 /* check if "default" isn't a HW, so include it in the list */
1331 if (snd_ctl_type(snd_hctl_ctl(hctl)) != 0)
1332 {
1333 gtk_list_store_insert_with_values(list, &iter, i++, 0, _("default"),
1334 1, j, -1);
1335 if (vol->used_device < 0)
1336 active = 0;
1337 }
1338 snd_hctl_close(hctl);
1339 }
1340 while (snd_card_next(&j) == 0 && j >= 0)
1341 {
1342 char *name = NULL;
1343 snd_mixer_t *mixer;
1344 char id[16];
72312227 1345
9fc4c110
AG
1346 if (snd_card_get_name(j, &name) == 0)
1347 {
1348 /* test if there any available channel */
1349 snprintf(id, sizeof(id), "hw:%d", j);
1350 snd_mixer_open(&mixer, 0);
1351 snd_mixer_attach(mixer, id);
1352 snd_mixer_selem_register(mixer, NULL, NULL);
1353 snd_mixer_load(mixer);
1354 for (elem = snd_mixer_first_elem(mixer); elem != NULL;
1355 elem = snd_mixer_elem_next(elem))
1356 {
1357 if (snd_mixer_selem_is_active(elem) &&
1358 snd_mixer_selem_has_playback_volume(elem) &&
1359 !snd_mixer_selem_has_capture_volume(elem) &&
1360 !snd_mixer_selem_has_capture_switch(elem))
1361 break;
1362 }
1363 snd_mixer_close(mixer);
1364 if (elem != NULL)
1365 {
1366 g_debug("found soundcard: %s", name);
1367 gtk_list_store_insert_with_values(list, &iter, i++, 0, name,
1368 1, j, -1);
1369 if (vol->used_device == j)
1370 active = i;
1371 }
1372 else
1373 g_debug("no elements in soundcard %s", name);
1374 free(name);
1375 }
1376 }
1377 card_selector = gtk_combo_box_new_with_model(GTK_TREE_MODEL(list));
632fb2d5 1378 g_object_unref(list);
9fc4c110
AG
1379 /* gtk_combo_box_set_wrap_width(GTK_COMBO_BOX(card_selector), 1); */
1380 column = gtk_cell_renderer_text_new();
1381 gtk_cell_layout_pack_start(GTK_CELL_LAYOUT(card_selector), column, TRUE);
1382 gtk_cell_layout_set_attributes(GTK_CELL_LAYOUT(card_selector), column,
1383 "text", 0, NULL);
1384 gtk_combo_box_set_active(GTK_COMBO_BOX(card_selector), active);
1385 g_signal_connect(card_selector, "changed",
1386 G_CALLBACK(card_selector_changed), vol);
1387 g_signal_connect(card_selector, "scroll-event", G_CALLBACK(gtk_true), NULL);
1388#endif
552475ec 1389
9fc4c110
AG
1390 /* setup channel selector */
1391#ifdef DISABLE_ALSA
1392 list = gtk_list_store_new(2, G_TYPE_STRING, G_TYPE_INT); /* desc, index */
1393 gtk_list_store_insert_with_values(list, &iter, 0, 0, _("Master"),
1394 1, SOUND_MIXER_VOLUME, -1);
1395 if (vol->master_channel == SOUND_MIXER_VOLUME)
1396 active = 0;
1397 gtk_list_store_insert_with_values(list, &iter, 1, 0, _("PCM"),
1398 1, SOUND_MIXER_PCM, -1);
1399 if (vol->master_channel == SOUND_MIXER_VOLUME)
1400 active = 1;
1401 gtk_list_store_insert_with_values(list, &iter, 2, 0, _("Headphone"),
1402 1, SOUND_MIXER_PHONEOUT, -1);
1403 if (vol->master_channel == SOUND_MIXER_VOLUME)
1404 active = 2;
1405#else
1406 list = alsa_make_channels_list(vol, &active);
650d2ca5 1407#endif
9fc4c110 1408 vol->channel_selector = gtk_combo_box_new_with_model(GTK_TREE_MODEL(list));
632fb2d5 1409 g_object_unref(list);
9fc4c110
AG
1410 /* gtk_combo_box_set_wrap_width(GTK_COMBO_BOX(vol->channel_selector), 1); */
1411 column = gtk_cell_renderer_text_new();
1412 gtk_cell_layout_pack_start(GTK_CELL_LAYOUT(vol->channel_selector), column, TRUE);
1413 gtk_cell_layout_set_attributes(GTK_CELL_LAYOUT(vol->channel_selector), column,
1414 "text", 0, NULL);
1415 gtk_combo_box_set_active(GTK_COMBO_BOX(vol->channel_selector), active);
1416 g_signal_connect(vol->channel_selector, "changed",
1417 G_CALLBACK(channel_selector_changed), vol);
1418 g_signal_connect(vol->channel_selector, "scroll-event", G_CALLBACK(gtk_true), NULL);
1419
1420 /* setup buttons */
1421 if (!config_setting_lookup_string(vol->settings, "SliderButton", &tmp_str))
1422 tmp_str = "1";
1423 volume_button = panel_config_click_button_new(_("Click for Volume Slider"), tmp_str);
1424 g_signal_connect(volume_button, "changed", G_CALLBACK(volume_button_changed), vol);
1425 if (!config_setting_lookup_string(vol->settings, "MuteButton", &tmp_str))
1426 tmp_str = "2";
1427 mute_button = panel_config_click_button_new(_("Click for Toggle Mute"), tmp_str);
1428 g_signal_connect(mute_button, "changed", G_CALLBACK(mute_button_changed), vol);
1429 if (!config_setting_lookup_string(vol->settings, "MixerButton", &tmp_str))
1430 tmp_str = NULL;
1431 mixer_button = panel_config_click_button_new(_("Click for Open Mixer"), tmp_str);
1432 g_signal_connect(mixer_button, "changed", G_CALLBACK(mixer_button_changed), vol);
1433
1434 /* setup hotkeys */
1435 up_key = panel_config_hotkey_button_new(_("Hotkey for Volume Up"), vol->hotkey_up);
1436 g_signal_connect(up_key, "changed", G_CALLBACK(up_key_changed), vol);
1437 down_key = panel_config_hotkey_button_new(_("Hotkey for Volume Down"), vol->hotkey_down);
1438 g_signal_connect(down_key, "changed", G_CALLBACK(down_key_changed), vol);
1439 mute_key = panel_config_hotkey_button_new(_("Hotkey for Volume Mute"), vol->hotkey_mute);
1440 g_signal_connect(mute_key, "changed", G_CALLBACK(mute_key_changed), vol);
1441
1442 /* setup mixer selector */
1443 if (!config_setting_lookup_string(vol->settings, "MixerCommand", &tmp_str))
1444 tmp_str = NULL;
1445 active = -1;
1446 i = j = 0;
1447 list = gtk_list_store_new(2, G_TYPE_STRING, G_TYPE_INT); /* line, needs_term */
1448 path = g_find_program_in_path("pulseaudio");
1449 if (path)
1450 g_free(path);
1451 else while (mixers[i].cmd && mixers[i].needs_pa)
1452 i++;
1453 for (; mixers[i].cmd; i++)
1454 {
1455 path = g_find_program_in_path(mixers[i].exec);
1456 if (path)
1457 {
1458 if (tmp_str && active < 0 && strcmp(tmp_str, mixers[i].cmd) == 0)
1459 {
1460 active = j;
1461 tmp_str = NULL;
1462 }
1463 gtk_list_store_insert_with_values(list, &iter, j++, 0, mixers[i].cmd,
1464 1, (int)mixers[i].needs_term,
1465 -1);
1466 g_free(path);
1467 }
1468 }
1469 if (tmp_str)
1470 {
1471 active = j;
1472 /* FIXME: support "needs terminal" for custom MixerCommand */
1473 gtk_list_store_insert_with_values(list, &iter, j, 0, tmp_str, 1, 0, -1);
1474 }
1475 if (active < 0)
1476 active = 0;
1477#if GTK_CHECK_VERSION(2, 24, 0)
1478 mixer_selector = gtk_combo_box_new_with_model_and_entry(GTK_TREE_MODEL(list));
1479 gtk_combo_box_set_entry_text_column(GTK_COMBO_BOX(mixer_selector), 0);
1480#else
1481 mixer_selector = gtk_combo_box_entry_new_with_model(GTK_TREE_MODEL(list), 0);
1482#endif
632fb2d5 1483 g_object_unref(list);
9fc4c110
AG
1484 /* gtk_combo_box_set_wrap_width(GTK_COMBO_BOX(mixer_selector), 1); */
1485 gtk_combo_box_set_active(GTK_COMBO_BOX(mixer_selector), active);
1486 mixer_entry = gtk_bin_get_child(GTK_BIN(mixer_selector));
1487 g_signal_connect(mixer_entry, "key-press-event",
1488 G_CALLBACK(mixer_selector_key_press), vol);
1489 g_signal_connect(mixer_selector, "changed",
1490 G_CALLBACK(mixer_selector_changed), vol);
1491 g_signal_connect(mixer_entry, "focus-out-event",
1492 G_CALLBACK(mixer_selector_focus_out), vol);
1493 g_signal_connect(mixer_selector, "scroll-event", G_CALLBACK(gtk_true), NULL);
1494
1495 return lxpanel_generic_config_dlg(_("Volume Control"), panel, NULL, p,
1496#ifndef DISABLE_ALSA
1497 _("Audio Card"), NULL, CONF_TYPE_TRIM,
1498 "", card_selector, CONF_TYPE_EXTERNAL,
1499#endif
1500 _("Channel to Operate"), NULL, CONF_TYPE_TRIM,
1501 "", vol->channel_selector, CONF_TYPE_EXTERNAL,
1502 "", volume_button, CONF_TYPE_EXTERNAL,
1503 "", mute_button, CONF_TYPE_EXTERNAL,
1504 "", mixer_button, CONF_TYPE_EXTERNAL,
1505 "", up_key, CONF_TYPE_EXTERNAL,
1506 "", down_key, CONF_TYPE_EXTERNAL,
1507 "", mute_key, CONF_TYPE_EXTERNAL,
1508 _("Command to Open Mixer"), NULL, CONF_TYPE_TRIM,
1509 "", mixer_selector, CONF_TYPE_EXTERNAL,
1510 NULL);
1511}
552475ec 1512
8f9e6256 1513/* Callback when panel configuration changes. */
a7bd16a4 1514static void volumealsa_panel_configuration_changed(LXPanel *panel, GtkWidget *p)
8f9e6256 1515{
1516 /* Do a full redraw. */
b285684e 1517 volumealsa_update_display(lxpanel_plugin_get_data(p));
8f9e6256 1518}
49db0a37 1519
650d2ca5
AG
1520static gboolean volumealsa_update_context_menu(GtkWidget *plugin, GtkMenu *menu)
1521{
1522 GtkWidget *img = gtk_image_new_from_stock("gtk-directory", GTK_ICON_SIZE_MENU);
9fc4c110 1523 GtkWidget *menu_item = gtk_image_menu_item_new_with_label(_("Launch Mixer"));
650d2ca5
AG
1524 //FIXME: precheck and disable if MixerCommand not set
1525 gtk_image_menu_item_set_image((GtkImageMenuItem *)menu_item, img);
1526 g_signal_connect_swapped(menu_item, "activate", G_CALLBACK(volume_run_mixer),
1527 lxpanel_plugin_get_data(plugin));
1528 gtk_menu_shell_append(GTK_MENU_SHELL(menu), menu_item);
1529 return FALSE;
1530}
1531
9d4601f2
AG
1532#ifndef DISABLE_ALSA
1533static LXPanelPluginInit _volumealsa_init = {
1534 .name = N_("Volume Control"),
1535 .description = N_("Display and control volume"),
1536
1537 .superseded = TRUE,
1538 .new_instance = volumealsa_constructor,
9fc4c110 1539 .config = volumealsa_configure,
9d4601f2 1540 .reconfigure = volumealsa_panel_configuration_changed,
650d2ca5 1541 .update_context_menu = volumealsa_update_context_menu,
9d4601f2
AG
1542 .button_press_event = volumealsa_button_press_event
1543};
1544
1545static void volumealsa_init(void)
1546{
1547 lxpanel_register_plugin_type("volumealsa", &_volumealsa_init);
1548}
1549#endif
1550
1551FM_DEFINE_MODULE(lxpanel_gtk, volume)
2918994e 1552
b285684e
AG
1553/* Plugin descriptor. */
1554LXPanelPluginInit fm_module_init_lxpanel_gtk = {
3c3e9c9e 1555 .name = N_("Volume Control"),
9d4601f2
AG
1556 .description = N_("Display and control volume"),
1557#ifndef DISABLE_ALSA
1558 .init = volumealsa_init,
1559#endif
3c3e9c9e 1560
b285684e 1561 .new_instance = volumealsa_constructor,
9fc4c110 1562 .config = volumealsa_configure,
b285684e 1563 .reconfigure = volumealsa_panel_configuration_changed,
650d2ca5 1564 .update_context_menu = volumealsa_update_context_menu,
b285684e 1565 .button_press_event = volumealsa_button_press_event
49db0a37 1566};
e0eb2f82
HG
1567
1568/* vim: set sw=4 et sts=4 : */