pnmixer
Volume mixer for the system tray
ui-tray-icon.c
Go to the documentation of this file.
1 /* ui-tray-icon.c
2  * PNmixer is written by Nick Lanham, a fork of OBmixer
3  * which was programmed by Lee Ferrett, derived
4  * from the program "AbsVolume" by Paul Sherman
5  * This program is free software; you can redistribute
6  * it and/or modify it under the terms of the GNU General
7  * Public License v3. source code is available at
8  * <http://github.com/nicklan/pnmixer>
9  */
10 
17 #ifdef HAVE_CONFIG_H
18 #include "config.h"
19 #endif
20 
21 #include <stdlib.h>
22 #include <string.h>
23 #include <math.h>
24 #include <glib.h>
25 #include <gtk/gtk.h>
26 
27 #include "audio.h"
28 #include "prefs.h"
29 #include "support-intl.h"
30 #include "support-log.h"
31 #include "support-ui.h"
32 #include "ui-tray-icon.h"
33 
34 #include "main.h"
35 
36 #define ICON_MIN_SIZE 16
37 
38 enum {
45 };
46 
47 /*
48  * Pixbuf handling
49  */
50 
57 static GdkPixbuf *
58 pixbuf_new_from_file(const gchar *filename)
59 {
60  GError *error = NULL;
61  GdkPixbuf *pixbuf;
62  gchar *pathname = NULL;
63 
64  if (!filename || !filename[0])
65  return NULL;
66 
67  pathname = get_pixmap_file(filename);
68 
69  if (!pathname) {
70  WARN("Couldn't find pixmap file '%s'", filename);
71  return NULL;
72  }
73 
74  DEBUG("Loading PNMixer icon '%s' from '%s'", filename, pathname);
75 
76  pixbuf = gdk_pixbuf_new_from_file(pathname, &error);
77  if (!pixbuf) {
78  WARN("Could not create pixbuf from file '%s': %s",
79  pathname, error->message);
80  g_error_free(error);
81  }
82 
83  g_free(pathname);
84  return pixbuf;
85 }
86 
95 static GdkPixbuf *
96 pixbuf_new_from_stock(const gchar *icon_name, gint size)
97 {
98  static GtkIconTheme *icon_theme = NULL;
99  GError *err = NULL;
100  GtkIconInfo *info = NULL;
101  GdkPixbuf *pixbuf = NULL;
102 
103  if (icon_theme == NULL)
104  icon_theme = gtk_icon_theme_get_default();
105 
106  info = gtk_icon_theme_lookup_icon(icon_theme, icon_name, size, 0);
107  if (info == NULL) {
108  WARN("Unable to lookup icon '%s'", icon_name);
109  return NULL;
110  }
111 
112  DEBUG("Loading stock icon '%s' from '%s'", icon_name,
113  gtk_icon_info_get_filename(info));
114 
115  pixbuf = gtk_icon_info_load_icon(info, &err);
116  if (pixbuf == NULL) {
117  WARN("Unable to load icon '%s': %s", icon_name, err->message);
118  g_error_free(err);
119  }
120 
121 #ifdef WITH_GTK3
122  g_object_unref(info);
123 #else
124  gtk_icon_info_free(info);
125 #endif
126 
127  return pixbuf;
128 }
129 
130 /* Frees a pixbuf array. */
131 static void
132 pixbuf_array_free(GdkPixbuf **pixbufs)
133 {
134  gsize i;
135 
136  if (!pixbufs)
137  return;
138 
139  for (i = 0; i < N_VOLUME_PIXBUFS; i++)
140  g_object_unref(pixbufs[i]);
141 
142  g_free(pixbufs);
143 }
144 
145 /* Creates a new pixbuf array, containing the icon set that must be used. */
146 static GdkPixbuf **
148 {
149  GdkPixbuf *pixbufs[N_VOLUME_PIXBUFS];
150  gboolean system_theme;
151 
152  DEBUG("Building pixbuf array (requesting size %d)", size);
153 
154  system_theme = prefs_get_boolean("SystemTheme", FALSE);
155 
156  if (system_theme) {
157  pixbufs[VOLUME_MUTED] = pixbuf_new_from_stock("audio-volume-muted", size);
158  pixbufs[VOLUME_OFF] = pixbuf_new_from_stock("audio-volume-off", size);
159  pixbufs[VOLUME_LOW] = pixbuf_new_from_stock("audio-volume-low", size);
160  pixbufs[VOLUME_MEDIUM] = pixbuf_new_from_stock("audio-volume-medium", size);
161  pixbufs[VOLUME_HIGH] = pixbuf_new_from_stock("audio-volume-high", size);
162  /* 'audio-volume-off' is not available in every icon set.
163  * Check freedesktop standard for more info:
164  * http://standards.freedesktop.org/icon-naming-spec/
165  * icon-naming-spec-latest.html
166  */
167  if (pixbufs[VOLUME_OFF] == NULL)
168  pixbufs[VOLUME_OFF] = pixbuf_new_from_stock("audio-volume-low", size);
169  } else {
170  pixbufs[VOLUME_MUTED] = pixbuf_new_from_file("pnmixer-muted.png");
171  pixbufs[VOLUME_OFF] = pixbuf_new_from_file("pnmixer-off.png");
172  pixbufs[VOLUME_LOW] = pixbuf_new_from_file("pnmixer-low.png");
173  pixbufs[VOLUME_MEDIUM] = pixbuf_new_from_file("pnmixer-medium.png");
174  pixbufs[VOLUME_HIGH] = pixbuf_new_from_file("pnmixer-high.png");
175  }
176 
177  return g_memdup(pixbufs, sizeof pixbufs);
178 }
179 
180 /* Tray icon volume meter */
181 
182 struct vol_meter {
183  /* Configuration */
184  guchar red;
185  guchar green;
186  guchar blue;
189  /* Dynamic stuff */
190  GdkPixbuf *pixbuf;
191  gint width;
192  guchar *row;
193 };
194 
195 typedef struct vol_meter VolMeter;
196 
197 /* Frees a VolMeter instance. */
198 static void
200 {
201  if (!vol_meter)
202  return;
203 
204  if (vol_meter->pixbuf)
205  g_object_unref(vol_meter->pixbuf);
206 
207  g_free(vol_meter->row);
208  g_free(vol_meter);
209 }
210 
211 /* Returns a new VolMeter instance. Returns NULL if VolMeter is disabled. */
212 static VolMeter *
214 {
216  gboolean vol_meter_enabled;
217  gdouble *vol_meter_clrs;
218 
219  vol_meter_enabled = prefs_get_boolean("DrawVolMeter", FALSE);
220  if (vol_meter_enabled == FALSE)
221  return NULL;
222 
223  vol_meter = g_new0(VolMeter, 1);
224 
225  vol_meter->x_offset_pct = prefs_get_integer("VolMeterPos", 0);
226  vol_meter->y_offset_pct = 10;
227 
228  vol_meter_clrs = prefs_get_double_list("VolMeterColor", NULL);
229  vol_meter->red = vol_meter_clrs[0] * 255;
230  vol_meter->green = vol_meter_clrs[1] * 255;
231  vol_meter->blue = vol_meter_clrs[2] * 255;
232  g_free(vol_meter_clrs);
233 
234  return vol_meter;
235 }
236 
237 /* Draws the volume meter on top of the icon. It doesn't modify the pixbuf passed
238  * in parameter. Instead, it makes a copy internally, and return a pointer toward
239  * the modified copy. There's no need to unref it.
240  */
241 static GdkPixbuf *
242 vol_meter_draw(VolMeter *vol_meter, GdkPixbuf *pixbuf, int volume)
243 {
244  int icon_width, icon_height;
245  int vm_width, vm_height;
246  int x, y;
247  int rowstride, i;
248  guchar *pixels;
249 
250  /* Ensure the pixbuf is as expected */
251  g_assert(gdk_pixbuf_get_colorspace(pixbuf) == GDK_COLORSPACE_RGB);
252  g_assert(gdk_pixbuf_get_bits_per_sample(pixbuf) == 8);
253  g_assert(gdk_pixbuf_get_has_alpha(pixbuf));
254  g_assert(gdk_pixbuf_get_n_channels(pixbuf) == 4);
255 
256  icon_width = gdk_pixbuf_get_width(pixbuf);
257  icon_height = gdk_pixbuf_get_height(pixbuf);
258 
259  /* Cache the pixbuf passed in parameter */
260  if (vol_meter->pixbuf)
261  g_object_unref(vol_meter->pixbuf);
262  vol_meter->pixbuf = pixbuf = gdk_pixbuf_copy(pixbuf);
263 
264  /* Volume meter coordinates */
265  vm_width = icon_width / 6;
266  x = vol_meter->x_offset_pct * (icon_width - vm_width) / 100;
267  g_assert(x >= 0 && x + vm_width <= icon_width);
268 
269  y = vol_meter->y_offset_pct * icon_height / 100;
270  vm_height = (icon_height - (y * 2)) * (volume / 100.0);
271  g_assert(y >= 0 && y + vm_height <= icon_height);
272 
273  /* Let's check if the icon width changed, in which case we
274  * must reinit our internal row of pixels.
275  */
276  if (vm_width != vol_meter->width) {
277  vol_meter->width = vm_width;
278  g_free(vol_meter->row);
279  vol_meter->row = NULL;
280  }
281 
282  if (vol_meter->row == NULL) {
283  DEBUG("Allocating vol meter row (width %d)", vm_width);
284  vol_meter->row = g_malloc(vm_width * sizeof(guchar) * 4);
285  for (i = 0; i < vm_width; i++) {
286  vol_meter->row[i * 4 + 0] = vol_meter->red;
287  vol_meter->row[i * 4 + 1] = vol_meter->green;
288  vol_meter->row[i * 4 + 2] = vol_meter->blue;
289  vol_meter->row[i * 4 + 3] = 255;
290  }
291  }
292 
293  /* Draw the volume meter.
294  * Rows in the image are stored top to bottom.
295  */
296  y = icon_height - y;
297  rowstride = gdk_pixbuf_get_rowstride(pixbuf);
298  pixels = gdk_pixbuf_get_pixels(pixbuf);
299 
300  for (i = 0; i < vm_height; i++) {
301  guchar *p;
302  guint row_offset, col_offset;
303 
304  row_offset = y - i;
305  col_offset = x * 4;
306  p = pixels + (row_offset * rowstride) + col_offset;
307 
308  memcpy(p, vol_meter->row, vm_width * 4);
309  }
310 
311  return pixbuf;
312 }
313 
314 /* Helpers */
315 
316 /* Update the tray icon pixbuf according to the current audio state. */
317 static void
318 update_status_icon_pixbuf(GtkStatusIcon *status_icon,
319  GdkPixbuf **pixbufs, VolMeter *vol_meter,
320  gdouble volume, gboolean muted)
321 {
322  GdkPixbuf *pixbuf;
323 
324  if (!muted) {
325  if (volume == 0)
326  pixbuf = pixbufs[VOLUME_OFF];
327  else if (volume < 33)
328  pixbuf = pixbufs[VOLUME_LOW];
329  else if (volume < 66)
330  pixbuf = pixbufs[VOLUME_MEDIUM];
331  else
332  pixbuf = pixbufs[VOLUME_HIGH];
333  } else {
334  pixbuf = pixbufs[VOLUME_MUTED];
335  }
336 
337  if (vol_meter && muted == FALSE)
338  pixbuf = vol_meter_draw(vol_meter, pixbuf, volume);
339 
340  gtk_status_icon_set_from_pixbuf(status_icon, pixbuf);
341 }
342 
343 /* Update the tray icon tooltip according to the current audio state. */
344 static void
345 update_status_icon_tooltip(GtkStatusIcon *status_icon,
346  const gchar *card, const gchar *channel,
347  gdouble volume, gboolean has_mute, gboolean muted)
348 {
349  gchar *card_info;
350  gchar *volume_info;
351  gchar *mute_info;
352  gchar *info;
353 
354  card_info = g_strdup_printf("%s (%s)", card, channel);
355  volume_info = g_strdup_printf("%s: %ld %%", _("Volume"), lround(volume));
356  if (has_mute == FALSE)
357  mute_info = g_strdup_printf(_("No mute switch"));
358  else if (muted)
359  mute_info = g_strdup_printf(_("Muted"));
360  else
361  mute_info = NULL;
362 
363  info = g_strjoin("\n", card_info, volume_info, mute_info, NULL);
364 
365  gtk_status_icon_set_tooltip_text(status_icon, info);
366 
367  g_free(info);
368  g_free(mute_info);
369  g_free(volume_info);
370  g_free(card_info);
371 }
372 
373 /* Public functions & signal handlers */
374 
375 struct tray_icon {
378  GdkPixbuf **pixbufs;
379  GtkStatusIcon *status_icon;
381 };
382 
390 static void
391 on_activate(G_GNUC_UNUSED GtkStatusIcon *status_icon,
392  G_GNUC_UNUSED TrayIcon *icon)
393 {
395 }
396 
408 static void
409 on_popup_menu(GtkStatusIcon *status_icon, guint button,
410  guint activate_time, G_GNUC_UNUSED TrayIcon *icon)
411 {
412  do_show_popup_menu(gtk_status_icon_position_menu, status_icon, button, activate_time);
413 }
414 
425 static gboolean
426 on_button_release_event(G_GNUC_UNUSED GtkStatusIcon *status_icon,
427  GdkEventButton *event, G_GNUC_UNUSED TrayIcon *icon)
428 {
429  int middle_click_action;
430 
431  if (event->button != 2)
432  return FALSE;
433 
434  middle_click_action = prefs_get_integer("MiddleClickAction", 0);
435 
436  switch (middle_click_action) {
437  case 0:
439  break;
440  case 1:
442  break;
443  case 2:
445  break;
446  case 3:
448  break;
449  default: {
450  } // nothing
451  }
452 
453  return FALSE;
454 }
455 
466 static gboolean
467 on_scroll_event(G_GNUC_UNUSED GtkStatusIcon *status_icon, GdkEventScroll *event,
468  TrayIcon *icon)
469 {
470  if (event->direction == GDK_SCROLL_UP)
472  else if (event->direction == GDK_SCROLL_DOWN)
474 
475  return FALSE;
476 }
477 
488 static gboolean
489 on_size_changed(G_GNUC_UNUSED GtkStatusIcon *status_icon, gint size, TrayIcon *icon)
490 {
491  DEBUG("Tray icon size is now %d", size);
492 
493  /* Ensure a minimum size. This is needed for Gtk2.
494  * With Gtk2, this handler is invoked with a zero size at startup,
495  * which screws up things here and there.
496  */
497  if (size < ICON_MIN_SIZE) {
498  size = ICON_MIN_SIZE;
499  DEBUG("Forcing size to the minimum value %d", size);
500  }
501 
502  /* Save new size */
503  icon->status_icon_size = size;
504 
505  /* Rebuild everything */
506  tray_icon_reload(icon);
507 
508  return FALSE;
509 }
510 
518 static void
519 on_audio_changed(G_GNUC_UNUSED Audio *audio, AudioEvent *event, gpointer data)
520 {
521  TrayIcon *icon = (TrayIcon *) data;
523  event->volume, event->muted);
524  update_status_icon_tooltip(icon->status_icon, event->card, event->channel,
525  event->volume, event->has_mute, event->muted);
526 }
527 
535 void
537 {
538  const gchar *card, *channel;
539  gdouble volume;
540  gboolean has_mute;
541  gboolean muted;
542 
543  pixbuf_array_free(icon->pixbufs);
545 
546  vol_meter_free(icon->vol_meter);
547  icon->vol_meter = vol_meter_new();
548 
549  card = audio_get_card(icon->audio);
550  channel = audio_get_channel(icon->audio);
551  volume = audio_get_volume(icon->audio);
552  has_mute = audio_has_mute(icon->audio);
553  muted = audio_is_muted(icon->audio);
555  volume, muted);
556  update_status_icon_tooltip(icon->status_icon, card, channel, volume, has_mute, muted);
557 }
558 
564 void
566 {
567  DEBUG("Destroying");
568 
570  g_object_unref(icon->status_icon);
571  pixbuf_array_free(icon->pixbufs);
572  vol_meter_free(icon->vol_meter);
573  g_free(icon);
574 }
575 
581 TrayIcon *
583 {
584  TrayIcon *icon;
585 
586  DEBUG("Creating tray icon");
587 
588  icon = g_new0(TrayIcon, 1);
589 
590  /* Create everything */
591  icon->vol_meter = vol_meter_new();
592  icon->status_icon = gtk_status_icon_new();
594 
595  /* Connect ui signal handlers */
596 
597  // Left-click
598  g_signal_connect(icon->status_icon, "activate",
599  G_CALLBACK(on_activate), icon);
600  // Right-click
601  g_signal_connect(icon->status_icon, "popup-menu",
602  G_CALLBACK(on_popup_menu), icon);
603  // Middle-click
604  g_signal_connect(icon->status_icon, "button-release-event",
605  G_CALLBACK(on_button_release_event), icon);
606  // Mouse scrolling on the icon
607  g_signal_connect(icon->status_icon, "scroll_event",
608  G_CALLBACK(on_scroll_event), icon);
609  // Change of size
610  g_signal_connect(icon->status_icon, "size-changed",
611  G_CALLBACK(on_size_changed), icon);
612 
613  /* Connect audio signals handlers */
614  icon->audio = audio;
616 
617  /* Display icon */
618  gtk_status_icon_set_visible(icon->status_icon, TRUE);
619 
620  /* Load preferences */
621  tray_icon_reload(icon);
622 
623  return icon;
624 }
#define _(String)
Definition: support-intl.h:44
Internationalization support.
Logging support.
Header for audio.c.
void audio_raise_volume(Audio *audio, AudioUser user)
Definition: audio.c:536
const gchar * card
Definition: audio.h:76
GtkStatusIcon * status_icon
Definition: ui-tray-icon.c:379
Audio * audio
Definition: ui-tray-icon.c:376
static void on_popup_menu(GtkStatusIcon *status_icon, guint button, guint activate_time, G_GNUC_UNUSED TrayIcon *icon)
Definition: ui-tray-icon.c:409
const char * audio_get_card(Audio *audio)
Definition: audio.c:353
static GdkPixbuf * pixbuf_new_from_stock(const gchar *icon_name, gint size)
Definition: ui-tray-icon.c:96
static GdkPixbuf * vol_meter_draw(VolMeter *vol_meter, GdkPixbuf *pixbuf, int volume)
Definition: ui-tray-icon.c:242
static Audio * audio
Definition: main.c:40
static void vol_meter_free(VolMeter *vol_meter)
Definition: ui-tray-icon.c:199
Header for support-ui.c.
static void on_activate(G_GNUC_UNUSED GtkStatusIcon *status_icon, G_GNUC_UNUSED TrayIcon *icon)
Definition: ui-tray-icon.c:391
void audio_signals_disconnect(Audio *audio, AudioCallback callback, gpointer data)
Definition: audio.c:318
gboolean audio_has_mute(Audio *audio)
Definition: audio.c:378
gint x_offset_pct
Definition: ui-tray-icon.c:187
void audio_toggle_mute(Audio *audio, AudioUser user)
Definition: audio.c:412
GdkPixbuf * pixbuf
Definition: ui-tray-icon.c:190
void audio_signals_connect(Audio *audio, AudioCallback callback, gpointer data)
Definition: audio.c:337
gboolean audio_is_muted(Audio *audio)
Definition: audio.c:395
Header for prefs.c.
guchar green
Definition: ui-tray-icon.c:185
static gboolean on_size_changed(G_GNUC_UNUSED GtkStatusIcon *status_icon, gint size, TrayIcon *icon)
Definition: ui-tray-icon.c:489
gboolean muted
Definition: audio.h:79
gdouble * prefs_get_double_list(const gchar *key, gsize *n)
Definition: prefs.c:206
guchar * row
Definition: ui-tray-icon.c:192
gboolean prefs_get_boolean(const gchar *key, gboolean def)
Definition: prefs.c:102
void tray_icon_destroy(TrayIcon *icon)
Definition: ui-tray-icon.c:565
gint status_icon_size
Definition: ui-tray-icon.c:380
#define DEBUG(...)
Definition: support-log.h:38
Header for main.c.
static GdkPixbuf ** pixbuf_array_new(int size)
Definition: ui-tray-icon.c:147
const char * audio_get_channel(Audio *audio)
Definition: audio.c:366
GtkWidget * system_theme
static GdkPixbuf * pixbuf_new_from_file(const gchar *filename)
Definition: ui-tray-icon.c:58
gboolean has_mute
Definition: audio.h:78
gchar * get_pixmap_file(const gchar *filename)
Definition: support-ui.c:85
void run_mixer_command(void)
Definition: main.c:79
static gboolean on_scroll_event(G_GNUC_UNUSED GtkStatusIcon *status_icon, GdkEventScroll *event, TrayIcon *icon)
Definition: ui-tray-icon.c:467
void run_custom_command(void)
Definition: main.c:99
Definition: audio.c:198
static void update_status_icon_pixbuf(GtkStatusIcon *status_icon, GdkPixbuf **pixbufs, VolMeter *vol_meter, gdouble volume, gboolean muted)
Definition: ui-tray-icon.c:318
void do_show_popup_menu(GtkMenuPositionFunc func, gpointer data, guint button, guint activate_time)
Definition: main.c:277
static void update_status_icon_tooltip(GtkStatusIcon *status_icon, const gchar *card, const gchar *channel, gdouble volume, gboolean has_mute, gboolean muted)
Definition: ui-tray-icon.c:345
gdouble audio_get_volume(Audio *audio)
Definition: audio.c:437
void run_prefs_dialog(void)
Definition: main.c:151
const gchar * channel
Definition: audio.h:77
gint prefs_get_integer(const gchar *key, gint def)
Definition: prefs.c:125
static VolMeter * vol_meter_new(void)
Definition: ui-tray-icon.c:213
static void pixbuf_array_free(GdkPixbuf **pixbufs)
Definition: ui-tray-icon.c:132
VolMeter * vol_meter
Definition: ui-tray-icon.c:377
void tray_icon_reload(TrayIcon *icon)
Definition: ui-tray-icon.c:536
static void on_audio_changed(G_GNUC_UNUSED Audio *audio, AudioEvent *event, gpointer data)
Definition: ui-tray-icon.c:519
TrayIcon * tray_icon_create(Audio *audio)
Definition: ui-tray-icon.c:582
guchar red
Definition: ui-tray-icon.c:184
void audio_lower_volume(Audio *audio, AudioUser user)
Definition: audio.c:516
GdkPixbuf ** pixbufs
Definition: ui-tray-icon.c:378
guchar blue
Definition: ui-tray-icon.c:186
#define ICON_MIN_SIZE
Definition: ui-tray-icon.c:36
static gboolean on_button_release_event(G_GNUC_UNUSED GtkStatusIcon *status_icon, GdkEventButton *event, G_GNUC_UNUSED TrayIcon *icon)
Definition: ui-tray-icon.c:426
Header for ui-tray-icon.c.
gdouble volume
Definition: audio.h:80
#define WARN(...)
Definition: support-log.h:37
gint y_offset_pct
Definition: ui-tray-icon.c:188
void do_toggle_popup_window(void)
Definition: main.c:259