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