1 /*
2     Playback Status Widget plugin for the DeaDBeeF audio player
3 
4     Copyright (C) 2015 Christian Boxdörfer <christian.boxdoerfer@posteo.de>
5 
6     This program is free software; you can redistribute it and/or
7     modify it under the terms of the GNU General Public License
8     as published by the Free Software Foundation; either version 2
9     of the License, or (at your option) any later version.
10 
11     This program is distributed in the hope that it will be useful,
12     but WITHOUT ANY WARRANTY; without even the implied warranty of
13     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14     GNU General Public License for more details.
15 
16     You should have received a copy of the GNU General Public License
17     along with this program; if not, write to the Free Software
18     Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
19 */
20 
21 #include <sys/types.h>
22 #include <sys/stat.h>
23 #include <stdlib.h>
24 #include <string.h>
25 #include <assert.h>
26 #include <math.h>
27 #include <fcntl.h>
28 #include <gtk/gtk.h>
29 
30 #include <deadbeef/deadbeef.h>
31 #include <deadbeef/gtkui_api.h>
32 
33 #include "fastftoi.h"
34 #include "support.h"
35 
36 #define MAX_LINES 10
37 
38 #define     CONFSTR_VM_REFRESH_INTERVAL       "playback_status.refresh_interval"
39 #define     CONFSTR_VM_NUM_LINES              "playback_status.num_lines"
40 #define     CONFSTR_VM_FORMAT                 "playback_status.format."
41 
42 /* Global variables */
43 static DB_misc_t            plugin;
44 static DB_functions_t *     deadbeef = NULL;
45 static ddb_gtkui_t *        gtkui_plugin = NULL;
46 
47 typedef struct {
48     ddb_gtkui_widget_t base;
49     GtkWidget *label[MAX_LINES];
50     GtkWidget *popup;
51     GtkWidget *popup_item;
52     cairo_surface_t *surf;
53     char *bytecode[MAX_LINES];
54     guint drawtimer;
55     intptr_t mutex;
56 } w_playback_status_t;
57 
58 static int CONFIG_REFRESH_INTERVAL = 100;
59 static int CONFIG_NUM_LINES = 3;
60 static const gchar *CONFIG_FORMAT[MAX_LINES];
61 
62 static void
save_config(void)63 save_config (void)
64 {
65     deadbeef->conf_set_int (CONFSTR_VM_REFRESH_INTERVAL,            CONFIG_REFRESH_INTERVAL);
66     deadbeef->conf_set_int (CONFSTR_VM_NUM_LINES,            CONFIG_NUM_LINES);
67     char conf_format_str[100];
68     for (int i = 0; i < CONFIG_NUM_LINES; i++) {
69         snprintf (conf_format_str, sizeof (conf_format_str), "%s%02d", CONFSTR_VM_FORMAT, i);
70         deadbeef->conf_set_str (conf_format_str, CONFIG_FORMAT[i]);
71     }
72     return;
73 }
74 
75 static void
load_config(gpointer user_data)76 load_config (gpointer user_data)
77 {
78     w_playback_status_t *w = user_data;
79     deadbeef->mutex_lock (w->mutex);
80     for (int i = 0; i < MAX_LINES; i++) {
81         if (CONFIG_FORMAT[i])
82             g_free ((gchar *)CONFIG_FORMAT[i]);
83     }
84     deadbeef->conf_lock ();
85     CONFIG_REFRESH_INTERVAL = deadbeef->conf_get_int (CONFSTR_VM_REFRESH_INTERVAL,          100);
86     CONFIG_NUM_LINES = deadbeef->conf_get_int (CONFSTR_VM_NUM_LINES,          3);
87 
88     char conf_format_str[1024];
89     for (int i = 0; i < CONFIG_NUM_LINES; i++) {
90         snprintf (conf_format_str, sizeof (conf_format_str), "%s%02d", CONFSTR_VM_FORMAT, i);
91         if (i == 0) {
92             CONFIG_FORMAT[i] = strdup (deadbeef->conf_get_str_fast (conf_format_str, "<span size='xx-large' weight='semibold'>%e / %l%</span>"));
93         }
94         else if (i == 1) {
95             CONFIG_FORMAT[i] = strdup (deadbeef->conf_get_str_fast (conf_format_str, "<span size='large'>%n. %t</span>"));
96         }
97         else if (i== 2) {
98             CONFIG_FORMAT[i] = strdup (deadbeef->conf_get_str_fast (conf_format_str, "<span size='large' foreground='gray31'>%a - <i>%b</i></span>"));
99         }
100         else {
101             CONFIG_FORMAT[i] = strdup (deadbeef->conf_get_str_fast (conf_format_str, ""));
102         }
103     }
104 
105     deadbeef->conf_unlock ();
106     deadbeef->mutex_unlock (w->mutex);
107 }
108 
109 static int
on_config_changed(gpointer user_data,uintptr_t ctx)110 on_config_changed (gpointer user_data, uintptr_t ctx)
111 {
112     w_playback_status_t *w = user_data;
113     load_config (user_data);
114     for (int i = 0; i < MAX_LINES; i++) {
115         if (w->bytecode[i]) {
116 #if DDB_API_LEVEL >= 8
117             deadbeef->tf_free (w->bytecode[i]);
118 #endif
119             w->bytecode[i] = NULL;
120         }
121         if (i < CONFIG_NUM_LINES) {
122             gtk_widget_show (w->label[i]);
123 #if DDB_API_LEVEL >= 8
124             w->bytecode[i] = deadbeef->tf_compile (CONFIG_FORMAT[i]);
125 #else
126             w->bytecode[i] = CONFIG_FORMAT[i];
127 #endif
128         }
129         else {
130             gtk_widget_hide (w->label[i]);
131         }
132     }
133     return 0;
134 }
135 
136 static GtkWidget *format[MAX_LINES];
137 
138 static gboolean
on_num_lines_changed(GtkSpinButton * spin,gpointer user_data)139 on_num_lines_changed (GtkSpinButton *spin, gpointer user_data)
140 {
141     w_playback_status_t *w = user_data;
142 
143     int value = gtk_spin_button_get_value_as_int (spin);
144     for (int i = 0; i < MAX_LINES; i++) {
145         if (i < value) {
146             gtk_widget_show (format[i]);
147         }
148         else {
149             gtk_widget_hide (format[i]);
150         }
151     }
152     return TRUE;
153 }
154 
155 #pragma GCC diagnostic push
156 #pragma GCC diagnostic ignored "-Wdeprecated-declarations"
157 static void
on_button_config(GtkMenuItem * menuitem,gpointer user_data)158 on_button_config (GtkMenuItem *menuitem, gpointer user_data)
159 {
160     GtkWidget *playback_status_properties;
161     GtkWidget *config_dialog;
162     GtkWidget *hbox01;
163     GtkWidget *vbox01;
164     GtkWidget *num_lines;
165     GtkWidget *dialog_action_area13;
166     GtkWidget *applybutton1;
167     GtkWidget *cancelbutton1;
168     GtkWidget *okbutton1;
169     playback_status_properties = gtk_dialog_new ();
170     gtk_window_set_title (GTK_WINDOW (playback_status_properties), "Playback Status Properties");
171     gtk_window_set_type_hint (GTK_WINDOW (playback_status_properties), GDK_WINDOW_TYPE_HINT_DIALOG);
172     gtk_window_set_resizable (GTK_WINDOW (playback_status_properties), FALSE);
173 
174     config_dialog = gtk_dialog_get_content_area (GTK_DIALOG (playback_status_properties));
175     gtk_widget_show (config_dialog);
176 
177     hbox01 = gtk_hbox_new (FALSE, 8);
178     gtk_widget_show (hbox01);
179     gtk_box_pack_start (GTK_BOX (config_dialog), hbox01, FALSE, FALSE, 0);
180     gtk_container_set_border_width (GTK_CONTAINER (hbox01), 12);
181 
182     vbox01 = gtk_vbox_new (TRUE, 8);
183     gtk_box_pack_start (GTK_BOX (hbox01), vbox01, TRUE, TRUE, 0);
184     gtk_widget_show (vbox01);
185 
186     num_lines = gtk_spin_button_new_with_range (1,MAX_LINES,1);
187     gtk_widget_show (num_lines);
188     gtk_box_pack_start (GTK_BOX (vbox01), num_lines, FALSE, FALSE, 0);
189     g_signal_connect_after ((gpointer) num_lines, "value-changed", G_CALLBACK (on_num_lines_changed), user_data);
190 
191     for (int i = 0; i < MAX_LINES; i++) {
192         format[i] = gtk_entry_new ();
193         gtk_widget_show (format[i]);
194         gtk_entry_set_invisible_char (GTK_ENTRY (format[i]), 8226);
195         gtk_entry_set_activates_default (GTK_ENTRY (format[i]), TRUE);
196         gtk_box_pack_start (GTK_BOX (vbox01), format[i], FALSE, FALSE, 0);
197         if (CONFIG_FORMAT [i]) {
198             gtk_entry_set_text (GTK_ENTRY (format[i]), CONFIG_FORMAT[i]);
199         }
200     }
201 
202     dialog_action_area13 = gtk_dialog_get_action_area (GTK_DIALOG (playback_status_properties));
203     gtk_widget_show (dialog_action_area13);
204     gtk_button_box_set_layout (GTK_BUTTON_BOX (dialog_action_area13), GTK_BUTTONBOX_END);
205 
206     applybutton1 = gtk_button_new_from_stock ("gtk-apply");
207     gtk_widget_show (applybutton1);
208     gtk_dialog_add_action_widget (GTK_DIALOG (playback_status_properties), applybutton1, GTK_RESPONSE_APPLY);
209     gtk_widget_set_can_default (applybutton1, TRUE);
210 
211     cancelbutton1 = gtk_button_new_from_stock ("gtk-cancel");
212     gtk_widget_show (cancelbutton1);
213     gtk_dialog_add_action_widget (GTK_DIALOG (playback_status_properties), cancelbutton1, GTK_RESPONSE_CANCEL);
214     gtk_widget_set_can_default (cancelbutton1, TRUE);
215 
216     okbutton1 = gtk_button_new_from_stock ("gtk-ok");
217     gtk_widget_show (okbutton1);
218     gtk_dialog_add_action_widget (GTK_DIALOG (playback_status_properties), okbutton1, GTK_RESPONSE_OK);
219     gtk_widget_set_can_default (okbutton1, TRUE);
220 
221     gtk_spin_button_set_value (GTK_SPIN_BUTTON (num_lines), CONFIG_NUM_LINES);
222     for (;;) {
223         int response = gtk_dialog_run (GTK_DIALOG (playback_status_properties));
224         if (response == GTK_RESPONSE_OK || response == GTK_RESPONSE_APPLY) {
225             CONFIG_NUM_LINES = gtk_spin_button_get_value_as_int (GTK_SPIN_BUTTON (num_lines));
226             for (int i = 0; i < CONFIG_NUM_LINES; i++) {
227                 if (CONFIG_FORMAT[i])
228                     g_free ((gchar *)CONFIG_FORMAT[i]);
229                 CONFIG_FORMAT[i] = strdup (gtk_entry_get_text (GTK_ENTRY (format[i])));
230             }
231             save_config ();
232             deadbeef->sendmessage (DB_EV_CONFIGCHANGED, 0, 0, 0);
233         }
234         if (response == GTK_RESPONSE_APPLY) {
235             continue;
236         }
237         break;
238     }
239     gtk_widget_destroy (playback_status_properties);
240     return;
241 }
242 #pragma GCC diagnostic pop
243 
244 static void
w_playback_status_destroy(ddb_gtkui_widget_t * w)245 w_playback_status_destroy (ddb_gtkui_widget_t *w) {
246     w_playback_status_t *s = (w_playback_status_t *)w;
247     deadbeef->vis_waveform_unlisten (w);
248     for (int i = 0; i < MAX_LINES; ++i) {
249         if (s->bytecode[i]) {
250 #if DDB_API_LEVEL >= 8
251             deadbeef->tf_free (s->bytecode[i]);
252 #endif
253             s->bytecode[i] = NULL;
254         }
255     }
256     if (s->drawtimer) {
257         g_source_remove (s->drawtimer);
258         s->drawtimer = 0;
259     }
260     if (s->surf) {
261         cairo_surface_destroy (s->surf);
262         s->surf = NULL;
263     }
264     if (s->mutex) {
265         deadbeef->mutex_free (s->mutex);
266         s->mutex = 0;
267     }
268 }
269 
270 static void
playback_status_set_label_text(gpointer user_data)271 playback_status_set_label_text (gpointer user_data)
272 {
273     w_playback_status_t *w = user_data;
274     deadbeef->mutex_lock (w->mutex);
275 
276     char title[1024];
277     DB_playItem_t *playing = deadbeef->streamer_get_playing_track ();
278     if (playing) {
279 #if DDB_API_LEVEL >= 8
280         ddb_tf_context_t ctx = {
281             ._size = sizeof (ddb_tf_context_t),
282             .it = playing,
283             .plt = deadbeef->plt_get_curr (),
284         };
285 
286         for (int i = 0; i < CONFIG_NUM_LINES; i++) {
287             deadbeef->tf_eval (&ctx, w->bytecode[i], title, sizeof (title));
288             gtk_label_set_markup (GTK_LABEL (w->label[i]), title);
289         }
290         if (ctx.plt) {
291             deadbeef->plt_unref (ctx.plt);
292             ctx.plt = NULL;
293         }
294 #else
295         for (int i = 0; i < CONFIG_NUM_LINES; i++) {
296             if (deadbeef->pl_format_title(playing, -1, title, sizeof(title)
297               -1, -1, w->bytecode[i]) >= 0)
298                 gtk_label_set_markup (GTK_LABEL (w->label[i]), title);
299         }
300 #endif
301         deadbeef->pl_item_unref (playing);
302     }
303     else {
304         for (int i = 0; i < CONFIG_NUM_LINES; i++) {
305             gtk_label_set_markup (GTK_LABEL (w->label[i]), i == 1 ?
306               "<span weight='semibold' size='x-large'>Stopped</span>" : "");
307         }
308     }
309     deadbeef->mutex_unlock (w->mutex);
310 }
311 
312 static gboolean
playback_status_update_cb(void * data)313 playback_status_update_cb (void *data) {
314     w_playback_status_t *w = data;
315     playback_status_set_label_text (w);
316     return TRUE;
317 }
318 
319 static gboolean
playback_status_update_single_cb(void * data)320 playback_status_update_single_cb (void *data) {
321     w_playback_status_t *w = data;
322     playback_status_set_label_text (w);
323     return FALSE;
324 }
325 
326 static gboolean
playback_status_set_refresh_interval(gpointer user_data,int interval)327 playback_status_set_refresh_interval (gpointer user_data, int interval)
328 {
329     w_playback_status_t *w = user_data;
330     if (!w || interval <= 0) {
331         return FALSE;
332     }
333     if (w->drawtimer) {
334         g_source_remove (w->drawtimer);
335         w->drawtimer = 0;
336     }
337     w->drawtimer = g_timeout_add (interval, playback_status_update_cb, w);
338     return TRUE;
339 }
340 
341 static gboolean
playback_status_button_press_event(GtkWidget * widget,GdkEventButton * event,gpointer user_data)342 playback_status_button_press_event (GtkWidget *widget, GdkEventButton *event, gpointer user_data)
343 {
344     //w_playback_status_t *w = user_data;
345     if (event->button == 3) {
346       return TRUE;
347     }
348     return TRUE;
349 }
350 
351 static gboolean
playback_status_button_release_event(GtkWidget * widget,GdkEventButton * event,gpointer user_data)352 playback_status_button_release_event (GtkWidget *widget, GdkEventButton *event, gpointer user_data)
353 {
354     w_playback_status_t *w = user_data;
355     if (event->button == 3) {
356       gtk_menu_popup (GTK_MENU (w->popup), NULL, NULL, NULL, w->base.widget, 0, gtk_get_current_event_time ());
357       return TRUE;
358     }
359     return TRUE;
360 }
361 
362 static int
playback_status_message(ddb_gtkui_widget_t * widget,uint32_t id,uintptr_t ctx,uint32_t p1,uint32_t p2)363 playback_status_message (ddb_gtkui_widget_t *widget, uint32_t id, uintptr_t ctx, uint32_t p1, uint32_t p2)
364 {
365     w_playback_status_t *w = (w_playback_status_t *)widget;
366 
367     switch (id) {
368         case DB_EV_SONGSTARTED:
369             playback_status_set_refresh_interval (w, CONFIG_REFRESH_INTERVAL);
370             break;
371         case DB_EV_PAUSED:
372             break;
373         case DB_EV_STOP:
374             break;
375         case DB_EV_CONFIGCHANGED:
376             on_config_changed (w, ctx);
377             playback_status_set_refresh_interval (w, CONFIG_REFRESH_INTERVAL);
378             break;
379     }
380     return 0;
381 }
382 
383 static void
w_playback_status_init(ddb_gtkui_widget_t * w)384 w_playback_status_init (ddb_gtkui_widget_t *w) {
385     w_playback_status_t *s = (w_playback_status_t *)w;
386     load_config (w);
387     for (int i = 0; i < MAX_LINES; i++) {
388         if (i < CONFIG_NUM_LINES) {
389             gtk_widget_show (s->label[i]);
390 #if DDB_API_LEVEL >= 8
391             s->bytecode[i] = deadbeef->tf_compile (CONFIG_FORMAT[i]);
392 #else
393             s->bytecode[i] = CONFIG_FORMAT[i];
394 #endif
395         }
396         else {
397             gtk_widget_hide (s->label[i]);
398             s->bytecode[i] = NULL;
399         }
400     }
401     deadbeef->mutex_lock (s->mutex);
402 
403     playback_status_set_refresh_interval (w, CONFIG_REFRESH_INTERVAL);
404     deadbeef->mutex_unlock (s->mutex);
405 }
406 
407 ddb_gtkui_widget_t *
w_playback_status_create(void)408 w_playback_status_create (void) {
409     w_playback_status_t *w = malloc (sizeof (w_playback_status_t));
410     memset (w, 0, sizeof (w_playback_status_t));
411 
412     w->base.widget = gtk_event_box_new ();
413     w->base.init = w_playback_status_init;
414     w->base.destroy  = w_playback_status_destroy;
415     w->base.message = playback_status_message;
416     GtkWidget *vbox = gtk_vbox_new (FALSE, 4);
417     gtk_container_set_border_width (GTK_CONTAINER (vbox), 4);
418     for (int i = 0; i < MAX_LINES; ++i) {
419         w->label[i] = gtk_label_new (NULL);
420         gtk_label_set_ellipsize (GTK_LABEL (w->label[i]), PANGO_ELLIPSIZE_END);
421         gtk_box_pack_start (GTK_BOX (vbox), w->label[i], FALSE, FALSE, 0);
422         gtk_widget_show (w->label[i]);
423     }
424     w->popup = gtk_menu_new ();
425     w->popup_item = gtk_menu_item_new_with_mnemonic ("Configure");
426     w->mutex = deadbeef->mutex_create ();
427 
428     gtk_container_add (GTK_CONTAINER (w->base.widget), vbox);
429     gtk_container_add (GTK_CONTAINER (w->popup), w->popup_item);
430     gtk_widget_show (w->popup);
431     gtk_widget_show (vbox);
432     gtk_widget_show (w->popup_item);
433 
434     gtk_widget_set_size_request (w->base.widget, 300, 16);
435     g_signal_connect_after ((gpointer) w->base.widget, "button_press_event", G_CALLBACK (playback_status_button_press_event), w);
436     g_signal_connect_after ((gpointer) w->base.widget, "button_release_event", G_CALLBACK (playback_status_button_release_event), w);
437     g_signal_connect_after ((gpointer) w->popup_item, "activate", G_CALLBACK (on_button_config), w);
438     gtkui_plugin->w_override_signals (w->base.widget, w);
439     gtk_widget_set_events (w->base.widget, GDK_EXPOSURE_MASK
440                                          | GDK_LEAVE_NOTIFY_MASK
441                                          | GDK_BUTTON_PRESS_MASK
442                                          | GDK_POINTER_MOTION_MASK
443                                          | GDK_POINTER_MOTION_HINT_MASK);
444     return (ddb_gtkui_widget_t *)w;
445 }
446 
447 int
playback_status_connect(void)448 playback_status_connect (void)
449 {
450     gtkui_plugin = (ddb_gtkui_t *) deadbeef->plug_get_for_id (DDB_GTKUI_PLUGIN_ID);
451     if (gtkui_plugin) {
452         //trace("using '%s' plugin %d.%d\n", DDB_GTKUI_PLUGIN_ID, gtkui_plugin->gui.plugin.version_major, gtkui_plugin->gui.plugin.version_minor );
453         if (gtkui_plugin->gui.plugin.version_major == 2) {
454             //printf ("fb api2\n");
455             // 0.6+, use the new widget API
456             gtkui_plugin->w_reg_widget ("Playback Status Widget", 0, w_playback_status_create, "playback_status", NULL);
457             return 0;
458         }
459     }
460     return -1;
461 }
462 
463 int
playback_status_start(void)464 playback_status_start (void)
465 {
466     return 0;
467 }
468 
469 int
playback_status_stop(void)470 playback_status_stop (void)
471 {
472     return 0;
473 }
474 
475 int
playback_status_startup(GtkWidget * cont)476 playback_status_startup (GtkWidget *cont)
477 {
478     return 0;
479 }
480 
481 int
playback_status_shutdown(GtkWidget * cont)482 playback_status_shutdown (GtkWidget *cont)
483 {
484     return 0;
485 }
486 int
playback_status_disconnect(void)487 playback_status_disconnect (void)
488 {
489     gtkui_plugin = NULL;
490     return 0;
491 }
492 
493 static const char settings_dlg[] =
494     "property \"Refresh interval (ms): \"           spinbtn[10,1000,1] "      CONFSTR_VM_REFRESH_INTERVAL         " 25 ;\n"
495 ;
496 
497 static DB_misc_t plugin = {
498     //DB_PLUGIN_SET_API_VERSION
499     .plugin.type            = DB_PLUGIN_MISC,
500     .plugin.api_vmajor      = 1,
501     .plugin.api_vminor      = 5,
502     .plugin.version_major   = 0,
503     .plugin.version_minor   = 1,
504 #if GTK_CHECK_VERSION(3,0,0)
505     .plugin.id              = "playback_status-gtk3",
506 #else
507     .plugin.id              = "playback_status",
508 #endif
509     .plugin.name            = "Playback Status Widget",
510     .plugin.descr           = "Playback Status Widget",
511     .plugin.copyright       =
512         "Copyright (C) 2015 Christian Boxdörfer <christian.boxdoerfer@posteo.de>\n"
513         "\n"
514         "This program is free software; you can redistribute it and/or\n"
515         "modify it under the terms of the GNU General Public License\n"
516         "as published by the Free Software Foundation; either version 2\n"
517         "of the License, or (at your option) any later version.\n"
518         "\n"
519         "This program is distributed in the hope that it will be useful,\n"
520         "but WITHOUT ANY WARRANTY; without even the implied warranty of\n"
521         "MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n"
522         "GNU General Public License for more details.\n"
523         "\n"
524         "You should have received a copy of the GNU General Public License\n"
525         "along with this program; if not, write to the Free Software\n"
526         "Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.\n"
527     ,
528     .plugin.website         = "https://github.com/cboxdoerfer/ddb_playback_status",
529     .plugin.start           = playback_status_start,
530     .plugin.stop            = playback_status_stop,
531     .plugin.connect         = playback_status_connect,
532     .plugin.disconnect      = playback_status_disconnect,
533     .plugin.configdialog    = settings_dlg,
534 };
535 
536 #if !GTK_CHECK_VERSION(3,0,0)
537 DB_plugin_t *
ddb_misc_playback_status_GTK2_load(DB_functions_t * ddb)538 ddb_misc_playback_status_GTK2_load (DB_functions_t *ddb) {
539     deadbeef = ddb;
540     return &plugin.plugin;
541 }
542 #else
543 DB_plugin_t *
ddb_misc_playback_status_GTK3_load(DB_functions_t * ddb)544 ddb_misc_playback_status_GTK3_load (DB_functions_t *ddb) {
545     deadbeef = ddb;
546     return &plugin.plugin;
547 }
548 #endif
549