Update copyrights everywhere.
[lxde/lxpanel.git] / src / plugins / volumealsa / volumealsa.c
index 269886d..ffc932e 100644 (file)
@@ -1,5 +1,5 @@
 /**
 /**
- * Copyright (c) 2008 LxDE Developers, see the file AUTHORS for details.
+ * Copyright (c) 2008-2014 LxDE Developers, see the file AUTHORS for details.
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
 #include <glib/gi18n.h>
 #include <gdk-pixbuf/gdk-pixbuf.h>
 #include <alsa/asoundlib.h>
 #include <glib/gi18n.h>
 #include <gdk-pixbuf/gdk-pixbuf.h>
 #include <alsa/asoundlib.h>
-#include "panel.h"
-#include "misc.h"
+#include <poll.h>
+#include <libfm/fm-gtk.h>
 #include "plugin.h"
 #include "dbg.h"
 
 #include "plugin.h"
 #include "dbg.h"
 
-#define ICONS_VOLUME PACKAGE_DATA_DIR "/lxpanel/images/volume.png"
-#define ICONS_MUTE PACKAGE_DATA_DIR "/lxpanel/images/mute.png"
+#define ICONS_VOLUME_HIGH   PACKAGE_DATA_DIR "/images/volume-high.png"
+#define ICONS_VOLUME_MEDIUM PACKAGE_DATA_DIR "/images/volume-medium.png"
+#define ICONS_VOLUME_LOW    PACKAGE_DATA_DIR "/images/volume-low.png"
+#define ICONS_MUTE          PACKAGE_DATA_DIR "/images/mute.png"
 
 typedef struct {
 
 typedef struct {
-    Plugin* plugin;
-    GtkWidget *mainw;
-    GtkWidget *tray_icon;
-    GtkWidget *dlg;
-    GtkTooltips* tooltips;
-    GtkWidget *vscale;
-    snd_mixer_t *mixer;
-    snd_mixer_selem_id_t *sid;
-    snd_mixer_elem_t *master_element;
-    long alsa_min_vol, alsa_max_vol;
-    int mute;
-    int show;
-} volume_t;
-
-
-
-/* ALSA */
-static gboolean find_element(volume_t *vol, const char *ename)
+
+    /* Graphics. */
+    GtkWidget * plugin;                                /* Back pointer to the widget */
+    LXPanel * panel;                           /* Back pointer to panel */
+    GtkWidget * tray_icon;                     /* Displayed image */
+    GtkWidget * popup_window;                  /* Top level window for popup */
+    GtkWidget * volume_scale;                  /* Scale for volume */
+    GtkWidget * mute_check;                    /* Checkbox for mute state */
+    gboolean show_popup;                       /* Toggle to show and hide the popup on left click */
+    guint volume_scale_handler;                        /* Handler for vscale widget */
+    guint mute_check_handler;                  /* Handler for mute_check widget */
+
+    /* ALSA interface. */
+    snd_mixer_t * mixer;                       /* The mixer */
+    snd_mixer_selem_id_t * sid;                        /* The element ID */
+    snd_mixer_elem_t * master_element;         /* The Master element */
+    guint mixer_evt_idle;                      /* Timer to handle restarting poll */
+    guint restart_idle;
+
+    /* unloading and error handling */
+    GIOChannel **channels;                      /* Channels that we listen to */
+    guint num_channels;                         /* Number of channels */
+
+    /* Icons */
+    const char* icon;
+    const char* icon_panel;
+    const char* icon_fallback;
+
+} VolumeALSAPlugin;
+
+static gboolean asound_restart(gpointer vol_gpointer);
+static gboolean asound_initialize(VolumeALSAPlugin * vol);
+static void asound_deinitialize(VolumeALSAPlugin * vol);
+static void volumealsa_update_display(VolumeALSAPlugin * vol);
+static void volumealsa_destructor(gpointer user_data);
+
+/*** ALSA ***/
+
+static gboolean asound_find_element(VolumeALSAPlugin * vol, const char * ename)
 {
 {
-    for (vol->master_element=snd_mixer_first_elem(vol->mixer);vol->master_element;vol->master_element=snd_mixer_elem_next(vol->master_element)) {
+    for (
+      vol->master_element = snd_mixer_first_elem(vol->mixer);
+      vol->master_element != NULL;
+      vol->master_element = snd_mixer_elem_next(vol->master_element))
+    {
         snd_mixer_selem_get_id(vol->master_element, vol->sid);
         snd_mixer_selem_get_id(vol->master_element, vol->sid);
-        if (!snd_mixer_selem_is_active(vol->master_element))
-            continue;
-
-        if (strcmp(ename, snd_mixer_selem_id_get_name(vol->sid))==0) {
+        if ((snd_mixer_selem_is_active(vol->master_element))
+        && (strcmp(ename, snd_mixer_selem_id_get_name(vol->sid)) == 0))
             return TRUE;
             return TRUE;
-        }
     }
     }
+    return FALSE;
+}
+
+/* NOTE by PCMan:
+ * This is magic! Since ALSA uses its own machanism to handle this part.
+ * After polling of mixer fds, it requires that we should call
+ * snd_mixer_handle_events to clear all pending mixer events.
+ * However, when using the glib IO channels approach, we don't have
+ * poll() and snd_mixer_poll_descriptors_revents(). Due to the design of
+ * glib, on_mixer_event() will be called for every fd whose status was
+ * changed. So, after each poll(), it's called for several times,
+ * not just once. Therefore, we cannot call snd_mixer_handle_events()
+ * directly in the event handler. Otherwise, it will get called for
+ * several times, which might clear unprocessed pending events in the queue.
+ * So, here we call it once in the event callback for the first fd.
+ * Then, we don't call it for the following fds. After all fds with changed
+ * status are handled, we remove this restriction in an idle handler.
+ * The next time the event callback is involked for the first fs, we can
+ * call snd_mixer_handle_events() again. Racing shouldn't happen here
+ * because the idle handler has the same priority as the io channel callback.
+ * So, io callbacks for future pending events should be in the next gmain
+ * iteration, and won't be affected.
+ */
 
 
+static gboolean asound_reset_mixer_evt_idle(VolumeALSAPlugin * vol)
+{
+    if (!g_source_is_destroyed(g_main_current_source()))
+        vol->mixer_evt_idle = 0;
     return FALSE;
 }
 
     return FALSE;
 }
 
-static gboolean asound_init(volume_t *vol)
+/* Handler for I/O event on ALSA channel. */
+static gboolean asound_mixer_event(GIOChannel * channel, GIOCondition cond, gpointer vol_gpointer)
 {
 {
+    VolumeALSAPlugin * vol = (VolumeALSAPlugin *) vol_gpointer;
+    int res = 0;
+
+    if (vol->mixer_evt_idle == 0)
+    {
+        vol->mixer_evt_idle = g_idle_add_full(G_PRIORITY_DEFAULT, (GSourceFunc) asound_reset_mixer_evt_idle, vol, NULL);
+        res = snd_mixer_handle_events(vol->mixer);
+    }
+
+    if (cond & G_IO_IN)
+    {
+        /* the status of mixer is changed. update of display is needed. */
+        volumealsa_update_display(vol);
+    }
+
+    if ((cond & G_IO_HUP) || (res < 0))
+    {
+        /* This means there're some problems with alsa. */
+        ERR("volumealsa: ALSA (or pulseaudio) had a problem: \n"
+                "volumealsa: snd_mixer_handle_events() = %d,"
+                " cond 0x%x (IN: 0x%x, HUP: 0x%x).\n", res, cond,
+                G_IO_IN, G_IO_HUP);
+        gtk_widget_set_tooltip_text(vol->plugin, "ALSA (or pulseaudio) had a problem."
+                " Please check the lxpanel logs.");
+
+        if (vol->restart_idle == 0)
+            vol->restart_idle = g_timeout_add_seconds(1, asound_restart, vol);
+
+        return FALSE;
+    }
+
+    return TRUE;
+}
+
+static gboolean asound_restart(gpointer vol_gpointer)
+{
+    VolumeALSAPlugin * vol = vol_gpointer;
+
+    if (g_source_is_destroyed(g_main_current_source()))
+        return FALSE;
+
+    asound_deinitialize(vol);
+
+    if (!asound_initialize(vol)) {
+        ERR("volumealsa: Re-initialization failed.\n");
+        return TRUE; // try again in a second
+    }
+
+    ERR("volumealsa: Restarted ALSA interface...\n");
+
+    vol->restart_idle = 0;
+    return FALSE;
+}
+
+/* Initialize the ALSA interface. */
+static gboolean asound_initialize(VolumeALSAPlugin * vol)
+{
+    /* Access the "default" device. */
     snd_mixer_selem_id_alloca(&vol->sid);
     snd_mixer_open(&vol->mixer, 0);
     snd_mixer_attach(vol->mixer, "default");
     snd_mixer_selem_register(vol->mixer, NULL, NULL);
     snd_mixer_load(vol->mixer);
 
     snd_mixer_selem_id_alloca(&vol->sid);
     snd_mixer_open(&vol->mixer, 0);
     snd_mixer_attach(vol->mixer, "default");
     snd_mixer_selem_register(vol->mixer, NULL, NULL);
     snd_mixer_load(vol->mixer);
 
-    /* Find Master element */
-    if (!find_element(vol, "Master"))
-        if (!find_element(vol, "Front"))
-            if (!find_element(vol, "PCM"))
-                return FALSE;
+    /* Find Master element, or Front element, or PCM element, or LineOut element.
+     * If one of these succeeds, master_element is valid. */
+    if ( ! asound_find_element(vol, "Master"))
+        if ( ! asound_find_element(vol, "Front"))
+            if ( ! asound_find_element(vol, "PCM"))
+                if ( ! asound_find_element(vol, "LineOut"))
+                    return FALSE;
 
 
+    /* Set the playback volume range as we wish it. */
+    snd_mixer_selem_set_playback_volume_range(vol->master_element, 0, 100);
 
 
-    snd_mixer_selem_get_playback_volume_range(vol->master_element, &vol->alsa_min_vol, &vol->alsa_max_vol);
+    /* Listen to events from ALSA. */
+    int n_fds = snd_mixer_poll_descriptors_count(vol->mixer);
+    struct pollfd * fds = g_new0(struct pollfd, n_fds);
 
 
-    snd_mixer_selem_set_playback_volume_range(vol->master_element, 0, 100);
+    vol->channels = g_new0(GIOChannel *, n_fds);
+    vol->num_channels = n_fds;
 
 
+    snd_mixer_poll_descriptors(vol->mixer, fds, n_fds);
+    int i;
+    for (i = 0; i < n_fds; ++i)
+    {
+        GIOChannel* channel = g_io_channel_unix_new(fds[i].fd);
+        g_io_add_watch(channel, G_IO_IN | G_IO_HUP, asound_mixer_event, vol);
+        vol->channels[i] = channel;
+    }
+    g_free(fds);
     return TRUE;
 }
 
     return TRUE;
 }
 
-static int asound_read(volume_t *vol)
+static void asound_deinitialize(VolumeALSAPlugin * vol)
 {
 {
-    long aleft, aright;
-    snd_mixer_handle_events(vol->mixer);
-    /* Left */
-    snd_mixer_selem_get_playback_volume(vol->master_element, SND_MIXER_SCHN_FRONT_LEFT, &aleft);
-    /* Right */
-    snd_mixer_selem_get_playback_volume(vol->master_element, SND_MIXER_SCHN_FRONT_RIGHT, &aright);
+    guint i;
 
 
+    if (vol->mixer_evt_idle != 0) {
+        g_source_remove(vol->mixer_evt_idle);
+        vol->mixer_evt_idle = 0;
+    }
+
+    for (i = 0; i < vol->num_channels; i++) {
+        g_io_channel_shutdown(vol->channels[i], FALSE, NULL);
+        g_io_channel_unref(vol->channels[i]);
+    }
+    g_free(vol->channels);
+    vol->channels = NULL;
+    vol->num_channels = 0;
+
+    snd_mixer_close(vol->mixer);
+    vol->master_element = NULL;
+    /* FIXME: unalloc vol->sid */
+}
+
+/* Get the presence of the mute control from the sound system. */
+static gboolean asound_has_mute(VolumeALSAPlugin * vol)
+{
+    return ((vol->master_element != NULL) ? snd_mixer_selem_has_playback_switch(vol->master_element) : FALSE);
+}
+
+/* Get the condition of the mute control from the sound system. */
+static gboolean asound_is_muted(VolumeALSAPlugin * vol)
+{
+    /* The switch is on if sound is not muted, and off if the sound is muted.
+     * Initialize so that the sound appears unmuted if the control does not exist. */
+    int value = 1;
+    if (vol->master_element != NULL)
+        snd_mixer_selem_get_playback_switch(vol->master_element, 0, &value);
+    return (value == 0);
+}
+
+/* Get the volume from the sound system.
+ * This implementation returns the average of the Front Left and Front Right channels. */
+static int asound_get_volume(VolumeALSAPlugin * vol)
+{
+    long aleft = 0;
+    long aright = 0;
+    if (vol->master_element != NULL)
+    {
+        snd_mixer_selem_get_playback_volume(vol->master_element, SND_MIXER_SCHN_FRONT_LEFT, &aleft);
+        snd_mixer_selem_get_playback_volume(vol->master_element, SND_MIXER_SCHN_FRONT_RIGHT, &aright);
+    }
     return (aleft + aright) >> 1;
 }
 
     return (aleft + aright) >> 1;
 }
 
-static void asound_write(volume_t *vol, int volume)
+/* Set the volume to the sound system.
+ * This implementation sets the Front Left and Front Right channels to the specified value. */
+static void asound_set_volume(VolumeALSAPlugin * vol, int volume)
 {
 {
-    snd_mixer_selem_set_playback_volume(vol->master_element, SND_MIXER_SCHN_FRONT_LEFT, volume);
-    snd_mixer_selem_set_playback_volume(vol->master_element, SND_MIXER_SCHN_FRONT_RIGHT, volume);
+    if (vol->master_element != NULL)
+    {
+        snd_mixer_selem_set_playback_volume(vol->master_element, SND_MIXER_SCHN_FRONT_LEFT, volume);
+        snd_mixer_selem_set_playback_volume(vol->master_element, SND_MIXER_SCHN_FRONT_RIGHT, volume);
+    }
 }
 
 }
 
-static gboolean focus_out_event(GtkWidget *widget, GdkEvent *event, volume_t *vol)
+/*** Graphics ***/
+
+static void volumealsa_update_current_icon(VolumeALSAPlugin * vol)
 {
 {
-    gtk_widget_hide(vol->dlg);
-    vol->show = 0;
-    return FALSE;
+    /* Mute status. */
+    gboolean mute = asound_is_muted(vol);
+    int level = asound_get_volume(vol);
+
+    /* Change icon according to mute / volume */
+    const char* icon="audio-volume-muted";
+    const char* icon_panel="audio-volume-muted-panel";
+    const char* icon_fallback=ICONS_MUTE;
+    if (mute)
+    {
+         icon_panel = "audio-volume-muted-panel";
+         icon="audio-volume-muted";
+         icon_fallback=ICONS_MUTE;
+    }
+    else if (level >= 75)
+    {
+         icon_panel = "audio-volume-high-panel";
+         icon="audio-volume-high";
+         icon_fallback=ICONS_VOLUME_HIGH;
+    }
+    else if (level >= 50)
+    {
+         icon_panel = "audio-volume-medium-panel";
+         icon="audio-volume-medium";
+         icon_fallback=ICONS_VOLUME_MEDIUM;
+    }
+    else if (level > 0)
+    {
+         icon_panel = "audio-volume-low-panel";
+         icon="audio-volume-low";
+         icon_fallback=ICONS_VOLUME_LOW;
+    }
+
+    vol->icon_panel = icon_panel;
+    vol->icon = icon;
+    vol->icon_fallback= icon_fallback;
 }
 
 }
 
-static gboolean tray_icon_press(GtkWidget *widget, GdkEventButton *event, volume_t *vol)
+/* Do a full redraw of the display. */
+static void volumealsa_update_display(VolumeALSAPlugin * vol)
 {
 {
-    if( event->button == 3 )  /* right button */
+    /* Mute status. */
+    gboolean mute = asound_is_muted(vol);
+    int level = asound_get_volume(vol);
+
+    volumealsa_update_current_icon(vol);
+
+    /* Change icon, fallback to default icon if theme doesn't exsit */
+    if ( ! lxpanel_image_set_icon_theme(vol->panel, vol->tray_icon, vol->icon_panel))
     {
     {
-        GtkMenu* popup = lxpanel_get_panel_menu( vol->plugin->panel, vol->plugin, FALSE );
-        gtk_menu_popup( popup, NULL, NULL, NULL, NULL, event->button, event->time );
-        return TRUE;
+        if ( ! lxpanel_image_set_icon_theme(vol->panel, vol->tray_icon, vol->icon))
+        {
+            lxpanel_image_set_from_file(vol->panel, vol->tray_icon, vol->icon_fallback);
+        }
     }
 
     }
 
-    if (vol->show==0) {
-        gtk_window_set_position(GTK_WINDOW(vol->dlg), GTK_WIN_POS_MOUSE);
-        gtk_scale_set_digits(GTK_SCALE(vol->vscale), asound_read(vol));
-        gtk_widget_show_all(vol->dlg);
-        vol->show = 1;
-    } else {
-        gtk_widget_hide(vol->dlg);
-        vol->show = 0;
+    g_signal_handler_block(vol->mute_check, vol->mute_check_handler);
+    gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(vol->mute_check), mute);
+    gtk_widget_set_sensitive(vol->mute_check, asound_has_mute(vol));
+    g_signal_handler_unblock(vol->mute_check, vol->mute_check_handler);
+
+    /* Volume. */
+    if (vol->volume_scale != NULL)
+    {
+        g_signal_handler_block(vol->volume_scale, vol->volume_scale_handler);
+        gtk_range_set_value(GTK_RANGE(vol->volume_scale), asound_get_volume(vol));
+        g_signal_handler_unblock(vol->volume_scale, vol->volume_scale_handler);
+    }
+
+    /* Display current level in tooltip. */
+    char * tooltip = g_strdup_printf("%s %d", _("Volume control"), level);
+    gtk_widget_set_tooltip_text(vol->plugin, tooltip);
+    g_free(tooltip);
+}
+
+
+/* Handler for "button-press-event" signal on main widget. */
+static gboolean volumealsa_button_press_event(GtkWidget * widget, GdkEventButton * event, LXPanel * panel)
+{
+    VolumeALSAPlugin * vol = lxpanel_plugin_get_data(widget);
+
+    /* Left-click.  Show or hide the popup window. */
+    if (event->button == 1)
+    {
+        if (vol->show_popup)
+        {
+            gtk_widget_hide(vol->popup_window);
+            vol->show_popup = FALSE;
+        }
+        else
+        {
+            gtk_widget_show_all(vol->popup_window);
+            vol->show_popup = TRUE;
+        }
+    }
+
+    /* Middle-click.  Toggle the mute status. */
+    else if (event->button == 2)
+    {
+        gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(vol->mute_check), ! gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(vol->mute_check)));
     }
     return TRUE;
 }
 
     }
     return TRUE;
 }
 
-static void on_vscale_value_changed(GtkRange *range, volume_t *vol)
+/* Handler for "focus-out" signal on popup window. */
+static gboolean volumealsa_popup_focus_out(GtkWidget * widget, GdkEvent * event, VolumeALSAPlugin * vol)
 {
 {
-    asound_write(vol, gtk_range_get_value(range));
+    /* Hide the widget. */
+    gtk_widget_hide(vol->popup_window);
+    vol->show_popup = FALSE;
+    return FALSE;
 }
 
 }
 
-static void click_mute(GtkWidget *widget, volume_t *vol)
+/* Handler for "map" signal on popup window. */
+static void volumealsa_popup_map(GtkWidget * widget, VolumeALSAPlugin * vol)
 {
 {
-    int chn;
+    lxpanel_plugin_adjust_popup_position(widget, vol->plugin);
+}
 
 
-    if (gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(widget))) {
-        gtk_image_set_from_file(vol->tray_icon, ICONS_MUTE);
-        for (chn = 0; chn <= SND_MIXER_SCHN_LAST; chn++) {
-            snd_mixer_selem_set_playback_switch(vol->master_element, chn, 0);
-        }
-    } else {
-        gtk_image_set_from_file(vol->tray_icon, ICONS_VOLUME);
-        for (chn = 0; chn <= SND_MIXER_SCHN_LAST; chn++) {
-            snd_mixer_selem_set_playback_switch(vol->master_element, chn, 1);
+static void volumealsa_theme_change(GtkWidget * widget, VolumeALSAPlugin * vol)
+{
+    if ( ! lxpanel_image_set_icon_theme(vol->panel, vol->tray_icon, vol->icon_panel))
+    {
+        if ( ! lxpanel_image_set_icon_theme(vol->panel, vol->tray_icon, vol->icon))
+        {
+            lxpanel_image_set_from_file(vol->panel, vol->tray_icon, vol->icon_fallback);
         }
     }
 }
 
         }
     }
 }
 
-static void panel_init(Plugin *p)
+/* Handler for "value_changed" signal on popup window vertical scale. */
+static void volumealsa_popup_scale_changed(GtkRange * range, VolumeALSAPlugin * vol)
+{
+    /* Reflect the value of the control to the sound system. */
+    asound_set_volume(vol, gtk_range_get_value(range));
+
+    /* Redraw the controls. */
+    volumealsa_update_display(vol);
+}
+
+/* Handler for "scroll-event" signal on popup window vertical scale. */
+static void volumealsa_popup_scale_scrolled(GtkScale * scale, GdkEventScroll * evt, VolumeALSAPlugin * vol)
+{
+    /* Get the state of the vertical scale. */
+    gdouble val = gtk_range_get_value(GTK_RANGE(vol->volume_scale));
+
+    /* Dispatch on scroll direction to update the value. */
+    if ((evt->direction == GDK_SCROLL_UP) || (evt->direction == GDK_SCROLL_LEFT))
+        val += 2;
+    else
+        val -= 2;
+
+    /* Reset the state of the vertical scale.  This provokes a "value_changed" event. */
+    gtk_range_set_value(GTK_RANGE(vol->volume_scale), CLAMP((int)val, 0, 100));
+}
+
+/* Handler for "toggled" signal on popup window mute checkbox. */
+static void volumealsa_popup_mute_toggled(GtkWidget * widget, VolumeALSAPlugin * vol)
+{
+    /* Get the state of the mute toggle. */
+    gboolean active = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(widget));
+
+    /* Reflect the mute toggle to the sound system. */
+    if (vol->master_element != NULL)
+    {
+        int chn;
+        for (chn = 0; chn <= SND_MIXER_SCHN_LAST; chn++)
+            snd_mixer_selem_set_playback_switch(vol->master_element, chn, ((active) ? 0 : 1));
+    }
+
+    /* Redraw the controls. */
+    volumealsa_update_display(vol);
+}
+
+/* Build the window that appears when the top level widget is clicked. */
+static void volumealsa_build_popup_window(GtkWidget *p)
 {
 {
-    volume_t *vol = p->priv;
-    GtkWidget *scrolledwindow;
-    GtkWidget *viewport;
-    GtkWidget *box;
-    GtkWidget *frame;
-    GtkWidget *checkbutton;
-
-    /* set show flags */
-    vol->show = 0;
-
-    /* create a new window */
-    vol->dlg = gtk_window_new(GTK_WINDOW_TOPLEVEL);
-    gtk_window_set_decorated(GTK_WINDOW(vol->dlg), FALSE);
-    gtk_container_set_border_width(GTK_CONTAINER(vol->dlg), 5);
-    gtk_window_set_default_size(GTK_WINDOW(vol->dlg), 80, 140);
-    gtk_window_set_skip_taskbar_hint(GTK_WINDOW(vol->dlg), TRUE);
-    gtk_window_set_skip_pager_hint(GTK_WINDOW(vol->dlg), TRUE);
-    gtk_window_set_type_hint(GTK_WINDOW(vol->dlg), GDK_WINDOW_TYPE_HINT_DIALOG);
-
-    /* setting background to default */
-    //gtk_widget_set_style(vol->dlg, p->panel->defstyle);
-
-    /* Focus-out signal */
-    g_signal_connect (G_OBJECT (vol->dlg), "focus_out_event",
-              G_CALLBACK (focus_out_event), vol);
-
-    scrolledwindow = gtk_scrolled_window_new(NULL, NULL);
-    gtk_container_set_border_width (GTK_CONTAINER (scrolledwindow), 0);
-    gtk_widget_show (scrolledwindow);
-    gtk_container_add (GTK_CONTAINER (vol->dlg), scrolledwindow);
-    GTK_WIDGET_UNSET_FLAGS (scrolledwindow, GTK_CAN_FOCUS);
+    VolumeALSAPlugin * vol = lxpanel_plugin_get_data(p);
+
+    /* Create a new window. */
+    vol->popup_window = gtk_window_new(GTK_WINDOW_POPUP);
+    gtk_window_set_decorated(GTK_WINDOW(vol->popup_window), FALSE);
+    gtk_container_set_border_width(GTK_CONTAINER(vol->popup_window), 5);
+    gtk_window_set_default_size(GTK_WINDOW(vol->popup_window), 80, 140);
+    gtk_window_set_skip_taskbar_hint(GTK_WINDOW(vol->popup_window), TRUE);
+    gtk_window_set_skip_pager_hint(GTK_WINDOW(vol->popup_window), TRUE);
+    gtk_window_set_type_hint(GTK_WINDOW(vol->popup_window), GDK_WINDOW_TYPE_HINT_UTILITY);
+
+    /* Connect signals. */
+    g_signal_connect(G_OBJECT(vol->popup_window), "focus-out-event", G_CALLBACK(volumealsa_popup_focus_out), vol);
+    g_signal_connect(G_OBJECT(vol->popup_window), "map", G_CALLBACK(volumealsa_popup_map), vol);
+
+    /* Create a scrolled window as the child of the top level window. */
+    GtkWidget * scrolledwindow = gtk_scrolled_window_new(NULL, NULL);
+    gtk_container_set_border_width (GTK_CONTAINER(scrolledwindow), 0);
+    gtk_widget_show(scrolledwindow);
+    gtk_container_add(GTK_CONTAINER(vol->popup_window), scrolledwindow);
+    gtk_widget_set_can_focus(scrolledwindow, FALSE);
     gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW (scrolledwindow), GTK_POLICY_NEVER, GTK_POLICY_NEVER);
     gtk_scrolled_window_set_shadow_type(GTK_SCROLLED_WINDOW(scrolledwindow), GTK_SHADOW_NONE);
 
     gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW (scrolledwindow), GTK_POLICY_NEVER, GTK_POLICY_NEVER);
     gtk_scrolled_window_set_shadow_type(GTK_SCROLLED_WINDOW(scrolledwindow), GTK_SHADOW_NONE);
 
-    viewport = gtk_viewport_new (NULL, NULL);
-    gtk_container_add (GTK_CONTAINER (scrolledwindow), viewport);
-    gtk_viewport_set_shadow_type (GTK_VIEWPORT (viewport), GTK_SHADOW_NONE);
+    /* Create a viewport as the child of the scrolled window. */
+    GtkWidget * viewport = gtk_viewport_new(NULL, NULL);
+    gtk_container_add(GTK_CONTAINER(scrolledwindow), viewport);
+    gtk_viewport_set_shadow_type(GTK_VIEWPORT(viewport), GTK_SHADOW_NONE);
     gtk_widget_show(viewport);
 
     gtk_widget_show(viewport);
 
-    /* create frame */
-    frame = gtk_frame_new(_("Volume"));
+    /* Create a frame as the child of the viewport. */
+    GtkWidget * frame = gtk_frame_new(_("Volume"));
     gtk_frame_set_shadow_type(GTK_FRAME(frame), GTK_SHADOW_IN);
     gtk_container_add(GTK_CONTAINER(viewport), frame);
 
     gtk_frame_set_shadow_type(GTK_FRAME(frame), GTK_SHADOW_IN);
     gtk_container_add(GTK_CONTAINER(viewport), frame);
 
-    /* create box */
-    box = gtk_vbox_new(FALSE, 0);
+    /* Create a vertical box as the child of the frame. */
+    GtkWidget * box = gtk_vbox_new(FALSE, 0);
+    gtk_container_add(GTK_CONTAINER(frame), box);
 
 
-    /* create controller */
-    vol->vscale = gtk_vscale_new(GTK_ADJUSTMENT(gtk_adjustment_new(asound_read(vol), 0, 100, 0, 0, 0)));
-    gtk_scale_set_draw_value(GTK_SCALE(vol->vscale), FALSE);
-    gtk_range_set_inverted(GTK_RANGE(vol->vscale), TRUE);
+    /* Create a vertical scale as the child of the vertical box. */
+    vol->volume_scale = gtk_vscale_new(GTK_ADJUSTMENT(gtk_adjustment_new(100, 0, 100, 0, 0, 0)));
+    gtk_scale_set_draw_value(GTK_SCALE(vol->volume_scale), FALSE);
+    gtk_range_set_inverted(GTK_RANGE(vol->volume_scale), TRUE);
+    gtk_box_pack_start(GTK_BOX(box), vol->volume_scale, TRUE, TRUE, 0);
 
 
-    g_signal_connect ((gpointer) vol->vscale, "value_changed",
-                      G_CALLBACK (on_vscale_value_changed),
-                      vol);
+    /* Value-changed and scroll-event signals. */
+    vol->volume_scale_handler = g_signal_connect(vol->volume_scale, "value-changed", G_CALLBACK(volumealsa_popup_scale_changed), vol);
+    g_signal_connect(vol->volume_scale, "scroll-event", G_CALLBACK(volumealsa_popup_scale_scrolled), vol);
 
 
-    checkbutton = gtk_check_button_new_with_label(_("Mute"));
-    snd_mixer_selem_get_playback_switch(vol->master_element, 0, &vol->mute);
+    /* Create a check button as the child of the vertical box. */
+    vol->mute_check = gtk_check_button_new_with_label(_("Mute"));
+    gtk_box_pack_end(GTK_BOX(box), vol->mute_check, FALSE, FALSE, 0);
+    vol->mute_check_handler = g_signal_connect(vol->mute_check, "toggled", G_CALLBACK(volumealsa_popup_mute_toggled), vol);
 
 
-    if (!vol->mute)
-        gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(checkbutton), TRUE);
+    /* Set background to default. */
+    gtk_widget_set_style(viewport, panel_get_defstyle(vol->panel));
+}
 
 
-    g_signal_connect ((gpointer) checkbutton, "toggled",
-                      G_CALLBACK (click_mute),
-                      vol);
+/* Plugin constructor. */
+static GtkWidget *volumealsa_constructor(LXPanel *panel, config_setting_t *settings)
+{
+    /* Allocate and initialize plugin context and set into Plugin private data pointer. */
+    VolumeALSAPlugin * vol = g_new0(VolumeALSAPlugin, 1);
+    GtkWidget *p;
 
 
-    gtk_box_pack_start(GTK_BOX(box), vol->vscale, TRUE, TRUE, 0);
-    gtk_box_pack_end(GTK_BOX(box), checkbutton, FALSE, FALSE, 0);
-    gtk_container_add(GTK_CONTAINER(frame), box);
+    /* Initialize ALSA.  If that fails, present nothing. */
+    if ( ! asound_initialize(vol))
+    {
+        volumealsa_destructor(vol);
+        return NULL;
+    }
+
+    /* Allocate top level widget and set into Plugin widget pointer. */
+    vol->panel = panel;
+    vol->plugin = p = gtk_event_box_new();
+    lxpanel_plugin_set_data(p, vol, volumealsa_destructor);
+    gtk_widget_add_events(p, GDK_BUTTON_PRESS_MASK);
+    gtk_widget_set_tooltip_text(p, _("Volume control"));
 
 
-    /* setting background to default */
-    gtk_widget_set_style(viewport, p->panel->defstyle);
+    /* Allocate icon as a child of top level. */
+    vol->tray_icon = gtk_image_new();
+    gtk_container_add(GTK_CONTAINER(p), vol->tray_icon);
+
+    /* Initialize window to appear when icon clicked. */
+    volumealsa_build_popup_window(p);
+
+    /* Connect signals. */
+    g_signal_connect(G_OBJECT(p), "scroll-event", G_CALLBACK(volumealsa_popup_scale_scrolled), vol );
+    g_signal_connect(panel_get_icon_theme(panel), "changed", G_CALLBACK(volumealsa_theme_change), vol );
+
+    /* Update the display, show the widget, and return. */
+    volumealsa_update_display(vol);
+    gtk_widget_show_all(p);
+    return p;
 }
 
 }
 
-static void
-volumealsa_destructor(Plugin *p)
+/* Plugin destructor. */
+static void volumealsa_destructor(gpointer user_data)
 {
 {
-    volume_t *vol = (volume_t *) p->priv;
+    VolumeALSAPlugin * vol = (VolumeALSAPlugin *) user_data;
+
+    asound_deinitialize(vol);
 
 
-    ENTER;
-    if (vol->dlg)
-        gtk_widget_destroy(vol->dlg);
-    g_object_unref( vol->tooltips );
+    /* If the dialog box is open, dismiss it. */
+    if (vol->popup_window != NULL)
+        gtk_widget_destroy(vol->popup_window);
 
 
+    if (vol->restart_idle)
+        g_source_remove(vol->restart_idle);
+
+    /* Deallocate all memory. */
     g_free(vol);
     g_free(vol);
-    RET();
 }
 
 }
 
-static int
-volumealsa_constructor(Plugin *p, char **fp)
-{
-    volume_t *vol;
-    line s;
-    GdkPixbuf *icon;
-    GtkWidget *image;
-    GtkIconTheme* theme;
-    GtkIconInfo* info;
-
-    ENTER;
-    s.len = 256;
-    vol = g_new0(volume_t, 1);
-    vol->plugin = p;
-    g_return_val_if_fail(vol != NULL, 0);
-    p->priv = vol;
-
-    /* initializing */
-    if (!asound_init(vol))
-        RET(1);
-
-    panel_init(p);
-
-    /* main */
-    vol->mainw = gtk_event_box_new();
-
-    gtk_widget_add_events(vol->mainw, GDK_BUTTON_PRESS_MASK);
-    gtk_widget_set_size_request( vol->mainw, 24, 24 );
-
-    g_signal_connect(G_OBJECT(vol->mainw), "button-press-event",
-                         G_CALLBACK(tray_icon_press), vol);
-
-    /* tray icon */
-    snd_mixer_selem_get_playback_switch(vol->master_element, 0, &vol->mute);
-    if (vol->mute==0)
-        vol->tray_icon = gtk_image_new_from_file(ICONS_MUTE);
-    else
-        vol->tray_icon = gtk_image_new_from_file(ICONS_VOLUME);
-
-    gtk_container_add(GTK_CONTAINER(vol->mainw), vol->tray_icon);
+/* Callback when the configuration dialog is to be shown. */
 
 
-    gtk_widget_show_all(vol->mainw);
+static GtkWidget *volumealsa_configure(LXPanel *panel, GtkWidget *p)
+{
+    const gchar *command_line = NULL;
 
 
-    vol->tooltips = gtk_tooltips_new ();
-#if GLIB_CHECK_VERSION( 2, 10, 0 )
-    g_object_ref_sink( vol->tooltips );
-#else
-    g_object_ref( vol->tooltips );
-    gtk_object_sink( vol->tooltips );
-#endif
+    if (g_find_program_in_path("pulseaudio"))
+    {
+     /* Assume that when pulseaudio is installed, it's launching every time */
+        if (g_find_program_in_path("gnome-sound-applet"))
+        {
+            command_line = "gnome-sound-applet";
+        }
+        else
+        {
+            if (g_find_program_in_path("pavucontrol"))
+            {
+                command_line = "pavucontrol";
+            }
+        }
+    }
 
 
-    /* FIXME: display current level in tooltip. ex: "Volume Control: 80%"  */
-    gtk_tooltips_set_tip (vol->tooltips, vol->mainw, _("Volume control"), NULL);
+    /* Fallback to alsamixer when PA is not running, or when no PA utility is find */
+    if (command_line == NULL)
+    {
+        if (g_find_program_in_path("gnome-alsamixer"))
+        {
+            command_line = "gnome-alsamixer";
+        }
+        else
+        {
+            if (g_find_program_in_path("alsamixer"))
+            {
+                if (g_find_program_in_path("xterm"))
+                {
+                    command_line = "xterm -e alsamixer";
+                }
+            }
+        }
+    }
 
 
-    /* store the created plugin widget in plugin->pwid */
-    p->pwid = vol->mainw;
+    if (command_line)
+    {
+        fm_launch_command_simple(NULL, NULL, G_APP_INFO_CREATE_NONE,
+                                 command_line, NULL);
+    }
+    else
+    {
+        fm_show_error(NULL,
+                      _("Error, you need to install an application to configure the sound (pavucontrol, alsamixer ...)"),
+                      NULL);
+    }
 
 
-    RET(1);
+    return NULL;
 }
 
 }
 
+/* Callback when panel configuration changes. */
+static void volumealsa_panel_configuration_changed(LXPanel *panel, GtkWidget *p)
+{
+    /* Do a full redraw. */
+    volumealsa_update_display(lxpanel_plugin_get_data(p));
+}
 
 
-PluginClass volumealsa_plugin_class = {
-    fname: NULL,
-    count: 0,
+FM_DEFINE_MODULE(lxpanel_gtk, volumealsa)
 
 
-    type : "volumealsa",
-    name : N_("Volume Control"),
-    version: "1.0",
-    description : "Display and control volume for ALSA",
+/* Plugin descriptor. */
+LXPanelPluginInit fm_module_init_lxpanel_gtk = {
+    .name = N_("Volume Control"),
+    .description = N_("Display and control volume for ALSA"),
 
 
-    constructor : volumealsa_constructor,
-    destructor  : volumealsa_destructor,
-    config : NULL,
-    save : NULL
+    .new_instance = volumealsa_constructor,
+    .config = volumealsa_configure,
+    .reconfigure = volumealsa_panel_configuration_changed,
+    .button_press_event = volumealsa_button_press_event
 };
 };
+
+/* vim: set sw=4 et sts=4 : */