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