Adding upstream version 0.5.5.
[debian/lxpanel.git] / src / plugins / volumealsa / volumealsa.c
CommitLineData
6cc5e1a6
DB
1/**
2 * Copyright (c) 2008 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>
96dc8a45 27#include <poll.h>
6cc5e1a6
DB
28#include "panel.h"
29#include "misc.h"
30#include "plugin.h"
31#include "dbg.h"
32
33#define ICONS_VOLUME PACKAGE_DATA_DIR "/lxpanel/images/volume.png"
34#define ICONS_MUTE PACKAGE_DATA_DIR "/lxpanel/images/mute.png"
35
36typedef struct {
2ba86315
DB
37
38 /* Graphics. */
39 Plugin * plugin; /* Back pointer to plugin */
40 GtkWidget * tray_icon; /* Displayed image */
41 GtkWidget * popup_window; /* Top level window for popup */
42 GtkWidget * volume_scale; /* Scale for volume */
43 GtkWidget * mute_check; /* Checkbox for mute state */
44 gboolean show_popup; /* Toggle to show and hide the popup on left click */
45 guint volume_scale_handler; /* Handler for vscale widget */
46 guint mute_check_handler; /* Handler for mute_check widget */
47
48 /* ALSA interface. */
49 snd_mixer_t * mixer; /* The mixer */
50 snd_mixer_selem_id_t * sid; /* The element ID */
51 snd_mixer_elem_t * master_element; /* The Master element */
52 guint mixer_evt_idle; /* Timer to handle restarting poll */
53} VolumeALSAPlugin;
54
55static gboolean asound_find_element(VolumeALSAPlugin * vol, const char * ename);
56static gboolean asound_reset_mixer_evt_idle(VolumeALSAPlugin * vol);
57static gboolean asound_mixer_event(GIOChannel * channel, GIOCondition cond, gpointer vol_gpointer);
58static gboolean asound_initialize(VolumeALSAPlugin * vol);
59static gboolean asound_has_mute(VolumeALSAPlugin * vol);
60static gboolean asound_is_muted(VolumeALSAPlugin * vol);
61static int asound_get_volume(VolumeALSAPlugin * vol);
62static void asound_set_volume(VolumeALSAPlugin * vol, int volume);
63static void volumealsa_update_display(VolumeALSAPlugin * vol);
64static gboolean volumealsa_button_press_event(GtkWidget * widget, GdkEventButton * event, VolumeALSAPlugin * vol);
65static gboolean volumealsa_popup_focus_out(GtkWidget * widget, GdkEvent * event, VolumeALSAPlugin * vol);
32a67dc7 66static void volumealsa_popup_map(GtkWidget * widget, VolumeALSAPlugin * vol);
2ba86315
DB
67static void volumealsa_popup_scale_changed(GtkRange * range, VolumeALSAPlugin * vol);
68static void volumealsa_popup_scale_scrolled(GtkScale * scale, GdkEventScroll * evt, VolumeALSAPlugin * vol);
69static void volumealsa_popup_mute_toggled(GtkWidget * widget, VolumeALSAPlugin * vol);
70static void volumealsa_build_popup_window(Plugin * p);
71static int volumealsa_constructor(Plugin * p, char ** fp);
72static void volumealsa_destructor(Plugin * p);
73static void volumealsa_panel_configuration_changed(Plugin * p);
74
75/*** ALSA ***/
76
77static gboolean asound_find_element(VolumeALSAPlugin * vol, const char * ename)
6cc5e1a6 78{
2ba86315
DB
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 {
6cc5e1a6 84 snd_mixer_selem_get_id(vol->master_element, vol->sid);
2ba86315
DB
85 if ((snd_mixer_selem_is_active(vol->master_element))
86 && (strcmp(ename, snd_mixer_selem_id_get_name(vol->sid)) == 0))
6cc5e1a6 87 return TRUE;
6cc5e1a6 88 }
6cc5e1a6
DB
89 return FALSE;
90}
91
96dc8a45
DB
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 */
2ba86315
DB
112
113static gboolean asound_reset_mixer_evt_idle(VolumeALSAPlugin * vol)
96dc8a45
DB
114{
115 vol->mixer_evt_idle = 0;
116 return FALSE;
117}
118
2ba86315
DB
119/* Handler for I/O event on ALSA channel. */
120static gboolean asound_mixer_event(GIOChannel * channel, GIOCondition cond, gpointer vol_gpointer)
96dc8a45 121{
2ba86315
DB
122 VolumeALSAPlugin * vol = (VolumeALSAPlugin *) vol_gpointer;
123
124 if (vol->mixer_evt_idle == 0)
96dc8a45 125 {
2ba86315
DB
126 vol->mixer_evt_idle = g_idle_add_full(G_PRIORITY_DEFAULT, (GSourceFunc) asound_reset_mixer_evt_idle, vol, NULL);
127 snd_mixer_handle_events(vol->mixer);
96dc8a45
DB
128 }
129
2ba86315 130 if (cond & G_IO_IN)
96dc8a45
DB
131 {
132 /* the status of mixer is changed. update of display is needed. */
2ba86315 133 volumealsa_update_display(vol);
96dc8a45 134 }
96dc8a45 135
2ba86315
DB
136 if (cond & G_IO_HUP)
137 {
138 /* This means there're some problems with alsa. */
96dc8a45
DB
139 return FALSE;
140 }
141
142 return TRUE;
143}
144
2ba86315
DB
145/* Initialize the ALSA interface. */
146static gboolean asound_initialize(VolumeALSAPlugin * vol)
6cc5e1a6 147{
2ba86315 148 /* Access the "default" device. */
6cc5e1a6
DB
149 snd_mixer_selem_id_alloca(&vol->sid);
150 snd_mixer_open(&vol->mixer, 0);
151 snd_mixer_attach(vol->mixer, "default");
152 snd_mixer_selem_register(vol->mixer, NULL, NULL);
153 snd_mixer_load(vol->mixer);
154
2ba86315
DB
155 /* Find Master element, or Front element, or PCM element, or LineOut element. */
156 if ( ! asound_find_element(vol, "Master"))
157 if ( ! asound_find_element(vol, "Front"))
158 if ( ! asound_find_element(vol, "PCM"))
159 if ( ! asound_find_element(vol, "LineOut"))
39c13576 160 return FALSE;
6cc5e1a6 161
2ba86315 162 /* Set the playback volume range as we wish it. */
6cc5e1a6
DB
163 snd_mixer_selem_set_playback_volume_range(vol->master_element, 0, 100);
164
2ba86315
DB
165 /* Listen to events from ALSA. */
166 int n_fds = snd_mixer_poll_descriptors_count(vol->mixer);
167 struct pollfd * fds = g_new0(struct pollfd, n_fds);
96dc8a45 168
2ba86315
DB
169 snd_mixer_poll_descriptors(vol->mixer, fds, n_fds);
170 int i;
171 for (i = 0; i < n_fds; ++i)
96dc8a45 172 {
2ba86315
DB
173 GIOChannel* channel = g_io_channel_unix_new(fds[i].fd);
174 g_io_add_watch(channel, G_IO_IN | G_IO_HUP, asound_mixer_event, vol);
175 g_io_channel_unref(channel);
96dc8a45 176 }
2ba86315 177 g_free(fds);
6cc5e1a6
DB
178 return TRUE;
179}
180
2ba86315
DB
181/* Get the presence of the mute control from the sound system. */
182static gboolean asound_has_mute(VolumeALSAPlugin * vol)
183{
184 return snd_mixer_selem_has_playback_switch(vol->master_element);
185}
186
187/* Get the condition of the mute control from the sound system. */
188static gboolean asound_is_muted(VolumeALSAPlugin * vol)
6cc5e1a6 189{
2ba86315
DB
190 /* The switch is on if sound is not muted, and off if the sound is muted.
191 * Initialize so that the sound appears unmuted if the control does not exist. */
192 int value = 1;
193 snd_mixer_selem_get_playback_switch(vol->master_element, 0, &value);
194 return (value == 0);
195}
196
197/* Get the volume from the sound system.
198 * This implementation returns the average of the Front Left and Front Right channels. */
199static int asound_get_volume(VolumeALSAPlugin * vol)
200{
201 long aleft;
202 long aright;
6cc5e1a6 203 snd_mixer_selem_get_playback_volume(vol->master_element, SND_MIXER_SCHN_FRONT_LEFT, &aleft);
6cc5e1a6 204 snd_mixer_selem_get_playback_volume(vol->master_element, SND_MIXER_SCHN_FRONT_RIGHT, &aright);
6cc5e1a6
DB
205 return (aleft + aright) >> 1;
206}
207
2ba86315
DB
208/* Set the volume to the sound system.
209 * This implementation sets the Front Left and Front Right channels to the specified value. */
210static void asound_set_volume(VolumeALSAPlugin * vol, int volume)
6cc5e1a6
DB
211{
212 snd_mixer_selem_set_playback_volume(vol->master_element, SND_MIXER_SCHN_FRONT_LEFT, volume);
213 snd_mixer_selem_set_playback_volume(vol->master_element, SND_MIXER_SCHN_FRONT_RIGHT, volume);
214}
215
2ba86315
DB
216/*** Graphics ***/
217
218/* Do a full redraw of the display. */
219static void volumealsa_update_display(VolumeALSAPlugin * vol)
6cc5e1a6 220{
2ba86315
DB
221 /* Mute status. */
222 gboolean mute = asound_is_muted(vol);
32a67dc7
DB
223
224 if ( ! panel_image_set_icon_theme(vol->plugin->panel, vol->tray_icon, ((mute) ? "audio-volume-muted" : "audio-volume-high")))
225 {
226 panel_image_set_from_file(vol->plugin->panel, vol->tray_icon, ((mute) ? ICONS_MUTE : ICONS_VOLUME));
227 }
2ba86315
DB
228
229 g_signal_handler_block(vol->mute_check, vol->mute_check_handler);
230 gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(vol->mute_check), mute);
231 gtk_widget_set_sensitive(vol->mute_check, asound_has_mute(vol));
232 g_signal_handler_unblock(vol->mute_check, vol->mute_check_handler);
233
234 /* Volume. */
235 int level = asound_get_volume(vol);
236 if (vol->volume_scale != NULL)
237 {
238 g_signal_handler_block(vol->volume_scale, vol->volume_scale_handler);
239 gtk_range_set_value(GTK_RANGE(vol->volume_scale), asound_get_volume(vol));
240 g_signal_handler_unblock(vol->volume_scale, vol->volume_scale_handler);
241 }
242
243 /* Display current level in tooltip. */
244 char * tooltip = g_strdup_printf("%s %d", _("Volume control"), level);
245 gtk_widget_set_tooltip_text(vol->plugin->pwid, tooltip);
246 g_free(tooltip);
6cc5e1a6
DB
247}
248
2ba86315
DB
249/* Handler for "button-press-event" signal on main widget. */
250static gboolean volumealsa_button_press_event(GtkWidget * widget, GdkEventButton * event, VolumeALSAPlugin * vol)
6cc5e1a6 251{
2ba86315
DB
252 /* Standard right-click handling. */
253 if (plugin_button_press_event(widget, event, vol->plugin))
6cc5e1a6 254 return TRUE;
2ba86315
DB
255
256 /* Left-click. Show or hide the popup window. */
257 if (event->button == 1)
258 {
259 if (vol->show_popup)
260 {
261 gtk_widget_hide(vol->popup_window);
262 vol->show_popup = FALSE;
263 }
264 else
265 {
2ba86315
DB
266 gtk_widget_show_all(vol->popup_window);
267 vol->show_popup = TRUE;
268 }
6cc5e1a6
DB
269 }
270
2ba86315
DB
271 /* Middle-click. Toggle the mute status. */
272 else if (event->button == 2)
273 {
274 gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(vol->mute_check), ! gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(vol->mute_check)));
6cc5e1a6
DB
275 }
276 return TRUE;
277}
278
2ba86315
DB
279/* Handler for "focus-out" signal on popup window. */
280static gboolean volumealsa_popup_focus_out(GtkWidget * widget, GdkEvent * event, VolumeALSAPlugin * vol)
6cc5e1a6 281{
2ba86315
DB
282 /* Hide the widget. */
283 gtk_widget_hide(vol->popup_window);
284 vol->show_popup = FALSE;
285 return FALSE;
6cc5e1a6
DB
286}
287
32a67dc7
DB
288/* Handler for "map" signal on popup window. */
289static void volumealsa_popup_map(GtkWidget * widget, VolumeALSAPlugin * vol)
290{
291 plugin_adjust_popup_position(widget, vol->plugin);
292}
293
2ba86315
DB
294/* Handler for "value_changed" signal on popup window vertical scale. */
295static void volumealsa_popup_scale_changed(GtkRange * range, VolumeALSAPlugin * vol)
96dc8a45 296{
2ba86315
DB
297 /* Reflect the value of the control to the sound system. */
298 asound_set_volume(vol, gtk_range_get_value(range));
299
300 /* Redraw the controls. */
301 volumealsa_update_display(vol);
302}
303
304/* Handler for "scroll-event" signal on popup window vertical scale. */
305static void volumealsa_popup_scale_scrolled(GtkScale * scale, GdkEventScroll * evt, VolumeALSAPlugin * vol)
306{
307 /* Get the state of the vertical scale. */
308 gdouble val = gtk_range_get_value(GTK_RANGE(vol->volume_scale));
309
310 /* Dispatch on scroll direction to update the value. */
311 if ((evt->direction == GDK_SCROLL_UP) || (evt->direction == GDK_SCROLL_LEFT))
96dc8a45 312 val += 2;
2ba86315 313 else
96dc8a45 314 val -= 2;
2ba86315
DB
315
316 /* Reset the state of the vertical scale. This provokes a "value_changed" event. */
317 gtk_range_set_value(GTK_RANGE(vol->volume_scale), CLAMP((int)val, 0, 100));
96dc8a45
DB
318}
319
2ba86315
DB
320/* Handler for "toggled" signal on popup window mute checkbox. */
321static void volumealsa_popup_mute_toggled(GtkWidget * widget, VolumeALSAPlugin * vol)
6cc5e1a6 322{
2ba86315
DB
323 /* Get the state of the mute toggle. */
324 gboolean active = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(widget));
325
326 /* Reflect the mute toggle to the sound system. */
6cc5e1a6 327 int chn;
2ba86315
DB
328 for (chn = 0; chn <= SND_MIXER_SCHN_LAST; chn++)
329 snd_mixer_selem_set_playback_switch(vol->master_element, chn, ((active) ? 0 : 1));
6cc5e1a6 330
2ba86315
DB
331 /* Redraw the controls. */
332 volumealsa_update_display(vol);
6cc5e1a6
DB
333}
334
2ba86315
DB
335/* Build the window that appears when the top level widget is clicked. */
336static void volumealsa_build_popup_window(Plugin * p)
6cc5e1a6 337{
2ba86315
DB
338 VolumeALSAPlugin * vol = p->priv;
339
340 /* Create a new window. */
341 vol->popup_window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
342 gtk_window_set_decorated(GTK_WINDOW(vol->popup_window), FALSE);
343 gtk_container_set_border_width(GTK_CONTAINER(vol->popup_window), 5);
344 gtk_window_set_default_size(GTK_WINDOW(vol->popup_window), 80, 140);
345 gtk_window_set_skip_taskbar_hint(GTK_WINDOW(vol->popup_window), TRUE);
346 gtk_window_set_skip_pager_hint(GTK_WINDOW(vol->popup_window), TRUE);
347 gtk_window_set_type_hint(GTK_WINDOW(vol->popup_window), GDK_WINDOW_TYPE_HINT_DIALOG);
348
32a67dc7 349 /* Connect signals. */
2ba86315 350 g_signal_connect(G_OBJECT(vol->popup_window), "focus_out_event", G_CALLBACK(volumealsa_popup_focus_out), vol);
32a67dc7 351 g_signal_connect(G_OBJECT(vol->popup_window), "map", G_CALLBACK(volumealsa_popup_map), vol);
2ba86315
DB
352
353 /* Create a scrolled window as the child of the top level window. */
354 GtkWidget * scrolledwindow = gtk_scrolled_window_new(NULL, NULL);
355 gtk_container_set_border_width (GTK_CONTAINER(scrolledwindow), 0);
356 gtk_widget_show(scrolledwindow);
357 gtk_container_add(GTK_CONTAINER(vol->popup_window), scrolledwindow);
358 GTK_WIDGET_UNSET_FLAGS(scrolledwindow, GTK_CAN_FOCUS);
6cc5e1a6
DB
359 gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW (scrolledwindow), GTK_POLICY_NEVER, GTK_POLICY_NEVER);
360 gtk_scrolled_window_set_shadow_type(GTK_SCROLLED_WINDOW(scrolledwindow), GTK_SHADOW_NONE);
361
2ba86315
DB
362 /* Create a viewport as the child of the scrolled window. */
363 GtkWidget * viewport = gtk_viewport_new(NULL, NULL);
364 gtk_container_add(GTK_CONTAINER(scrolledwindow), viewport);
365 gtk_viewport_set_shadow_type(GTK_VIEWPORT(viewport), GTK_SHADOW_NONE);
6cc5e1a6
DB
366 gtk_widget_show(viewport);
367
2ba86315
DB
368 /* Create a frame as the child of the viewport. */
369 GtkWidget * frame = gtk_frame_new(_("Volume"));
6cc5e1a6
DB
370 gtk_frame_set_shadow_type(GTK_FRAME(frame), GTK_SHADOW_IN);
371 gtk_container_add(GTK_CONTAINER(viewport), frame);
372
2ba86315
DB
373 /* Create a vertical box as the child of the frame. */
374 GtkWidget * box = gtk_vbox_new(FALSE, 0);
375 gtk_container_add(GTK_CONTAINER(frame), box);
6cc5e1a6 376
2ba86315
DB
377 /* Create a vertical scale as the child of the vertical box. */
378 vol->volume_scale = gtk_vscale_new(GTK_ADJUSTMENT(gtk_adjustment_new(100, 0, 100, 0, 0, 0)));
379 gtk_scale_set_draw_value(GTK_SCALE(vol->volume_scale), FALSE);
380 gtk_range_set_inverted(GTK_RANGE(vol->volume_scale), TRUE);
381 gtk_box_pack_start(GTK_BOX(box), vol->volume_scale, TRUE, TRUE, 0);
6cc5e1a6 382
2ba86315
DB
383 /* Value-changed and scroll-event signals. */
384 vol->volume_scale_handler = g_signal_connect(vol->volume_scale, "value_changed", G_CALLBACK(volumealsa_popup_scale_changed), vol);
385 g_signal_connect(vol->volume_scale, "scroll-event", G_CALLBACK(volumealsa_popup_scale_scrolled), vol);
6cc5e1a6 386
2ba86315 387 /* Create a check button as the child of the vertical box. */
96dc8a45 388 vol->mute_check = gtk_check_button_new_with_label(_("Mute"));
96dc8a45 389 gtk_box_pack_end(GTK_BOX(box), vol->mute_check, FALSE, FALSE, 0);
2ba86315 390 vol->mute_check_handler = g_signal_connect(vol->mute_check, "toggled", G_CALLBACK(volumealsa_popup_mute_toggled), vol);
6cc5e1a6 391
2ba86315 392 /* Set background to default. */
6cc5e1a6
DB
393 gtk_widget_set_style(viewport, p->panel->defstyle);
394}
395
2ba86315
DB
396/* Plugin constructor. */
397static int volumealsa_constructor(Plugin * p, char ** fp)
6cc5e1a6 398{
2ba86315
DB
399 /* Allocate and initialize plugin context and set into Plugin private data pointer. */
400 VolumeALSAPlugin * vol = g_new0(VolumeALSAPlugin, 1);
6cc5e1a6 401 vol->plugin = p;
6cc5e1a6
DB
402 p->priv = vol;
403
2ba86315
DB
404 /* Initialize ALSA. If that fails, present nothing. */
405 if ( ! asound_initialize(vol))
406 return 1;
6cc5e1a6 407
2ba86315
DB
408 /* Allocate top level widget and set into Plugin widget pointer. */
409 p->pwid = gtk_event_box_new();
410 gtk_widget_add_events(p->pwid, GDK_BUTTON_PRESS_MASK);
411 gtk_widget_set_tooltip_text(p->pwid, _("Volume control"));
6cc5e1a6 412
2ba86315
DB
413 /* Allocate icon as a child of top level. */
414 vol->tray_icon = gtk_image_new();
415 gtk_container_add(GTK_CONTAINER(p->pwid), vol->tray_icon);
6cc5e1a6 416
2ba86315
DB
417 /* Initialize window to appear when icon clicked. */
418 volumealsa_build_popup_window(p);
6cc5e1a6 419
2ba86315
DB
420 /* Connect signals. */
421 g_signal_connect(G_OBJECT(p->pwid), "button-press-event", G_CALLBACK(volumealsa_button_press_event), vol);
422 g_signal_connect(G_OBJECT(p->pwid), "scroll-event", G_CALLBACK(volumealsa_popup_scale_scrolled), vol );
6cc5e1a6 423
2ba86315
DB
424 /* Update the display, show the widget, and return. */
425 volumealsa_update_display(vol);
426 gtk_widget_show_all(p->pwid);
427 return 1;
428}
6cc5e1a6 429
2ba86315
DB
430/* Plugin destructor. */
431static void volumealsa_destructor(Plugin * p)
432{
433 VolumeALSAPlugin * vol = (VolumeALSAPlugin *) p->priv;
6cc5e1a6 434
2ba86315
DB
435 /* Remove the periodic timer. */
436 if (vol->mixer_evt_idle != 0)
437 g_source_remove(vol->mixer_evt_idle);
6cc5e1a6 438
2ba86315
DB
439 /* If the dialog box is open, dismiss it. */
440 if (vol->popup_window != NULL)
441 gtk_widget_destroy(vol->popup_window);
6cc5e1a6 442
2ba86315
DB
443 /* Deallocate all memory. */
444 g_free(vol);
6cc5e1a6
DB
445}
446
2ba86315
DB
447/* Callback when panel configuration changes. */
448static void volumealsa_panel_configuration_changed(Plugin * p)
449{
450 /* Do a full redraw. */
451 volumealsa_update_display((VolumeALSAPlugin *) p->priv);
452}
6cc5e1a6 453
2ba86315 454/* Plugin descriptor. */
6cc5e1a6 455PluginClass volumealsa_plugin_class = {
2ba86315
DB
456
457 PLUGINCLASS_VERSIONING,
6cc5e1a6
DB
458
459 type : "volumealsa",
460 name : N_("Volume Control"),
461 version: "1.0",
462 description : "Display and control volume for ALSA",
463
464 constructor : volumealsa_constructor,
465 destructor : volumealsa_destructor,
466 config : NULL,
2ba86315
DB
467 save : NULL,
468 panel_configuration_changed : volumealsa_panel_configuration_changed
469
6cc5e1a6 470};