1 /*
2     DeaDBeeF -- the music player
3     Copyright (C) 2009-2015 Alexey Yakovenko and other contributors
4 
5     This software is provided 'as-is', without any express or implied
6     warranty.  In no event will the authors be held liable for any damages
7     arising from the use of this software.
8 
9     Permission is granted to anyone to use this software for any purpose,
10     including commercial applications, and to alter it and redistribute it
11     freely, subject to the following restrictions:
12 
13     1. The origin of this software must not be misrepresented; you must not
14      claim that you wrote the original software. If you use this software
15      in a product, an acknowledgment in the product documentation would be
16      appreciated but is not required.
17 
18     2. Altered source versions must be plainly marked as such, and must not be
19      misrepresented as being the original software.
20 
21     3. This notice may not be removed or altered from any source distribution.
22 */
23 
24 
25 #include "../../deadbeef.h"
26 #include <gtk/gtk.h>
27 #ifdef HAVE_CONFIG_H
28 #include "../../config.h"
29 #endif
30 #include <string.h>
31 #include <stdlib.h>
32 #include <math.h>
33 #include <sys/time.h>
34 #include <unistd.h>
35 #include <sys/stat.h>
36 #ifdef __linux__
37 #include <sys/prctl.h>
38 #endif
39 #include "../../gettext.h"
40 #include "gtkui.h"
41 #include "ddblistview.h"
42 #include "search.h"
43 #include "progress.h"
44 #include "interface.h"
45 #include "callbacks.h"
46 #include "support.h"
47 #include "../libparser/parser.h"
48 #include "drawing.h"
49 #include "trkproperties.h"
50 #include "../artwork/artwork.h"
51 #include "coverart.h"
52 #include "plcommon.h"
53 #include "ddbtabstrip.h"
54 #include "eq.h"
55 #include "actions.h"
56 #include "pluginconf.h"
57 #include "gtkui_api.h"
58 #include "wingeom.h"
59 #include "widgets.h"
60 #ifndef __APPLE__
61 #include "X11/Xlib.h"
62 #else
63 #include "retina.h"
64 #endif
65 #include "actionhandlers.h"
66 #include "hotkeys.h"
67 #include "../hotkeys/hotkeys.h"
68 
69 #define trace(...) { fprintf(stderr, __VA_ARGS__); }
70 //#define trace(fmt,...)
71 
72 static ddb_gtkui_t plugin;
73 DB_functions_t *deadbeef;
74 
75 // main widgets
76 GtkWidget *mainwin;
77 GtkWidget *searchwin;
78 GtkStatusIcon *trayicon;
79 GtkWidget *traymenu;
80 
81 static int gtkui_accept_messages = 0;
82 
83 static gint refresh_timeout = 0;
84 
85 int fileadded_listener_id;
86 int fileadd_beginend_listener_id;
87 // overriden API methods
88 #if 0
89 int (*gtkui_original_plt_add_dir) (ddb_playlist_t *plt, const char *dirname, int (*cb)(DB_playItem_t *it, void *data), void *user_data);
90 int (*gtkui_original_plt_add_file) (ddb_playlist_t *plt, const char *fname, int (*cb)(DB_playItem_t *it, void *data), void *user_data);
91 int (*gtkui_original_pl_add_files_begin) (ddb_playlist_t *plt);
92 void (*gtkui_original_pl_add_files_end) (void);
93 #endif
94 
95 // cached config variables
96 int gtkui_embolden_current_track;
97 int gtkui_embolden_tracks;
98 int gtkui_embolden_selected_tracks;
99 int gtkui_italic_selected_tracks;
100 int gtkui_italic_tracks;
101 int gtkui_italic_current_track;
102 
103 int gtkui_tabstrip_embolden_playing;
104 int gtkui_tabstrip_italic_playing;
105 int gtkui_tabstrip_embolden_selected;
106 int gtkui_tabstrip_italic_selected;
107 
108 int gtkui_groups_pinned;
109 
110 #ifdef __APPLE__
111 int gtkui_is_retina = 0;
112 #endif
113 
114 int gtkui_unicode_playstate = 0;
115 int gtkui_disable_seekbar_overlay = 0;
116 
117 #define TRAY_ICON "deadbeef_tray_icon"
118 
119 void
gtkpl_free(DdbListview * pl)120 gtkpl_free (DdbListview *pl) {
121 #if 0
122     if (colhdr_anim.timeline) {
123         timeline_free (colhdr_anim.timeline, 1);
124         colhdr_anim.timeline = 0;
125     }
126 #endif
127 }
128 
129 struct fromto_t {
130     DB_playItem_t *from;
131     DB_playItem_t *to;
132 };
133 
134 static gboolean
135 update_win_title_idle (gpointer data);
136 
137 // update status bar and window title
138 static int sb_context_id = -1;
139 static char sb_text[512];
140 static float last_songpos = -1;
141 static char sbitrate[20] = "";
142 static struct timeval last_br_update;
143 
144 static gboolean
update_songinfo(gpointer ctx)145 update_songinfo (gpointer ctx) {
146     int iconified = gdk_window_get_state(gtk_widget_get_window(mainwin)) & GDK_WINDOW_STATE_ICONIFIED;
147     if (!gtk_widget_get_visible (mainwin) || iconified) {
148         return FALSE;
149     }
150     DB_output_t *output = deadbeef->get_output ();
151     char sbtext_new[512] = "-";
152 
153     float pl_totaltime = deadbeef->pl_get_totaltime ();
154     int daystotal = (int)pl_totaltime / (3600*24);
155     int hourtotal = ((int)pl_totaltime / 3600) % 24;
156     int mintotal = ((int)pl_totaltime/60) % 60;
157     int sectotal = ((int)pl_totaltime) % 60;
158 
159     char totaltime_str[512] = "";
160     if (daystotal == 0) {
161         snprintf (totaltime_str, sizeof (totaltime_str), "%d:%02d:%02d", hourtotal, mintotal, sectotal);
162     }
163     else if (daystotal == 1) {
164         snprintf (totaltime_str, sizeof (totaltime_str), _("1 day %d:%02d:%02d"), hourtotal, mintotal, sectotal);
165     }
166     else {
167         snprintf (totaltime_str, sizeof (totaltime_str), _("%d days %d:%02d:%02d"), daystotal, hourtotal, mintotal, sectotal);
168     }
169 
170     DB_playItem_t *track = deadbeef->streamer_get_playing_track ();
171     DB_fileinfo_t *c = deadbeef->streamer_get_current_fileinfo (); // FIXME: might crash streamer
172 
173     float duration = track ? deadbeef->pl_get_item_duration (track) : -1;
174 
175     if (!output || (output->state () == OUTPUT_STATE_STOPPED || !track || !c)) {
176         snprintf (sbtext_new, sizeof (sbtext_new), _("Stopped | %d tracks | %s total playtime"), deadbeef->pl_getcount (PL_MAIN), totaltime_str);
177     }
178     else {
179         float playpos = deadbeef->streamer_get_playpos ();
180         int minpos = playpos / 60;
181         int secpos = playpos - minpos * 60;
182         int mindur = duration / 60;
183         int secdur = duration - mindur * 60;
184 
185         const char *mode;
186         char temp[20];
187         if (c->fmt.channels <= 2) {
188             mode = c->fmt.channels == 1 ? _("Mono") : _("Stereo");
189         }
190         else {
191             snprintf (temp, sizeof (temp), "%dch Multichannel", c->fmt.channels);
192             mode = temp;
193         }
194         int samplerate = c->fmt.samplerate;
195         int bitspersample = c->fmt.bps;
196         //        codec_unlock ();
197 
198         char t[100];
199         if (duration >= 0) {
200             snprintf (t, sizeof (t), "%d:%02d", mindur, secdur);
201         }
202         else {
203             strcpy (t, "-:--");
204         }
205 
206         struct timeval tm;
207         gettimeofday (&tm, NULL);
208         if (tm.tv_sec - last_br_update.tv_sec + (tm.tv_usec - last_br_update.tv_usec) / 1000000.0 >= 0.3) {
209             memcpy (&last_br_update, &tm, sizeof (tm));
210             int bitrate = deadbeef->streamer_get_apx_bitrate ();
211             if (bitrate > 0) {
212                 snprintf (sbitrate, sizeof (sbitrate), _("| %4d kbps "), bitrate);
213             }
214             else {
215                 sbitrate[0] = 0;
216             }
217         }
218         const char *spaused = deadbeef->get_output ()->state () == OUTPUT_STATE_PAUSED ? _("Paused | ") : "";
219         char filetype[20];
220         if (!deadbeef->pl_get_meta (track, ":FILETYPE", filetype, sizeof (filetype))) {
221             strcpy (filetype, "-");
222         }
223         snprintf (sbtext_new, sizeof (sbtext_new), _("%s%s %s| %dHz | %d bit | %s | %d:%02d / %s | %d tracks | %s total playtime"), spaused, filetype, sbitrate, samplerate, bitspersample, mode, minpos, secpos, t, deadbeef->pl_getcount (PL_MAIN), totaltime_str);
224     }
225 
226     if (strcmp (sbtext_new, sb_text)) {
227         strcpy (sb_text, sbtext_new);
228 
229         // form statusline
230         GtkStatusbar *sb = GTK_STATUSBAR (lookup_widget (mainwin, "statusbar"));
231         if (sb_context_id == -1) {
232             sb_context_id = gtk_statusbar_get_context_id (sb, "msg");
233         }
234 
235         gtk_statusbar_pop (sb, sb_context_id);
236         gtk_statusbar_push (sb, sb_context_id, sb_text);
237     }
238 
239     if (track) {
240         deadbeef->pl_item_unref (track);
241     }
242     return FALSE;
243 }
244 
245 void
set_tray_tooltip(const char * text)246 set_tray_tooltip (const char *text) {
247     if (trayicon) {
248 #if !GTK_CHECK_VERSION(2,16,0)
249         gtk_status_icon_set_tooltip (trayicon, text);
250 #else
251         gtk_status_icon_set_tooltip_text (trayicon, text);
252 #endif
253     }
254 }
255 
256 gboolean
on_trayicon_scroll_event(GtkWidget * widget,GdkEventScroll * event,gpointer user_data)257 on_trayicon_scroll_event               (GtkWidget       *widget,
258                                         GdkEventScroll  *event,
259                                         gpointer         user_data)
260 {
261     float vol = deadbeef->volume_get_db ();
262     int sens = deadbeef->conf_get_int ("gtkui.tray_volume_sensitivity", 1);
263     if (event->direction == GDK_SCROLL_UP || event->direction == GDK_SCROLL_RIGHT) {
264         vol += sens;
265     }
266     else if (event->direction == GDK_SCROLL_DOWN || event->direction == GDK_SCROLL_LEFT) {
267         vol -= sens;
268     }
269     if (vol > 0) {
270         vol = 0;
271     }
272     else if (vol < deadbeef->volume_get_min_db ()) {
273         vol = deadbeef->volume_get_min_db ();
274     }
275     deadbeef->volume_set_db (vol);
276 
277 #if 0
278     char str[100];
279     if (deadbeef->conf_get_int ("gtkui.show_gain_in_db", 1)) {
280         snprintf (str, sizeof (str), "Gain: %s%d dB", vol == 0 ? "+" : "", (int)vol);
281     }
282     else {
283         snprintf (str, sizeof (str), "Gain: %d%%", (int)(deadbeef->volume_get_amp () * 100));
284     }
285     set_tray_tooltip (str);
286 #endif
287 
288     return FALSE;
289 }
290 
291 void
mainwin_toggle_visible(void)292 mainwin_toggle_visible (void) {
293     int iconified = gdk_window_get_state(gtk_widget_get_window(mainwin)) & GDK_WINDOW_STATE_ICONIFIED;
294     if (gtk_widget_get_visible (mainwin) && !iconified) {
295         gtk_widget_hide (mainwin);
296     }
297     else {
298         wingeom_restore (mainwin, "mainwin", 40, 40, 500, 300, 0);
299         if (iconified) {
300             gtk_window_deiconify (GTK_WINDOW(mainwin));
301         }
302         else {
303             gtk_window_present (GTK_WINDOW (mainwin));
304         }
305     }
306 }
307 
308 #if !GTK_CHECK_VERSION(2,14,0)
309 gboolean
on_trayicon_activate(GtkWidget * widget,gpointer user_data)310 on_trayicon_activate (GtkWidget       *widget,
311                                         gpointer         user_data)
312 {
313     mainwin_toggle_visible ();
314     return FALSE;
315 }
316 
317 #else
318 
319 gboolean
on_trayicon_button_press_event(GtkWidget * widget,GdkEventButton * event,gpointer user_data)320 on_trayicon_button_press_event (GtkWidget       *widget,
321                                         GdkEventButton  *event,
322                                         gpointer         user_data)
323 {
324     if (event->button == 1 && event->type == GDK_BUTTON_PRESS) {
325         mainwin_toggle_visible ();
326     }
327     else if (event->button == 2 && event->type == GDK_BUTTON_PRESS) {
328         deadbeef->sendmessage (DB_EV_TOGGLE_PAUSE, 0, 0, 0);
329     }
330     return FALSE;
331 }
332 #endif
333 
334 gboolean
on_trayicon_popup_menu(GtkWidget * widget,guint button,guint time,gpointer user_data)335 on_trayicon_popup_menu (GtkWidget       *widget,
336         guint button,
337         guint time,
338                                         gpointer         user_data)
339 {
340     gtk_menu_popup (GTK_MENU (traymenu), NULL, NULL, gtk_status_icon_position_menu, trayicon, button, time);
341     return FALSE;
342 }
343 
344 static gboolean
activate_cb(gpointer nothing)345 activate_cb (gpointer nothing) {
346     gtk_widget_show (mainwin);
347     gtk_window_present (GTK_WINDOW (mainwin));
348     return FALSE;
349 }
350 
351 void
redraw_queued_tracks(DdbListview * pl)352 redraw_queued_tracks (DdbListview *pl) {
353     DB_playItem_t *it;
354     int idx = 0;
355     deadbeef->pl_lock ();
356     for (it = deadbeef->pl_get_first (PL_MAIN); it; idx++) {
357         if (deadbeef->pl_playqueue_test (it) != -1) {
358             ddb_listview_draw_row (pl, idx, (DdbListviewIter)it);
359         }
360         DB_playItem_t *next = deadbeef->pl_get_next (it, PL_MAIN);
361         deadbeef->pl_item_unref (it);
362         it = next;
363     }
364     deadbeef->pl_unlock ();
365 }
366 
367 gboolean
redraw_queued_tracks_cb(gpointer plt)368 redraw_queued_tracks_cb (gpointer plt) {
369     DdbListview *list = plt;
370     int iconified = gdk_window_get_state(gtk_widget_get_window(mainwin)) & GDK_WINDOW_STATE_ICONIFIED;
371     if (!gtk_widget_get_visible (mainwin) || iconified) {
372         return FALSE;
373     }
374     redraw_queued_tracks (list);
375     return FALSE;
376 }
377 
378 void
gtkpl_songchanged_wrapper(DB_playItem_t * from,DB_playItem_t * to)379 gtkpl_songchanged_wrapper (DB_playItem_t *from, DB_playItem_t *to) {
380     struct fromto_t *ft = malloc (sizeof (struct fromto_t));
381     ft->from = from;
382     ft->to = to;
383     if (from) {
384         deadbeef->pl_item_ref (from);
385     }
386     if (to) {
387         deadbeef->pl_item_ref (to);
388     }
389     g_idle_add (update_win_title_idle, ft);
390     if (searchwin && gtk_widget_get_window (searchwin)) {
391         int iconified = gdk_window_get_state(gtk_widget_get_window (searchwin)) & GDK_WINDOW_STATE_ICONIFIED;
392         if (gtk_widget_get_visible (searchwin) && !iconified) {
393             g_idle_add (redraw_queued_tracks_cb, DDB_LISTVIEW (lookup_widget (searchwin, "searchlist")));
394         }
395     }
396 }
397 
398 const char *gtkui_default_titlebar_playing = "%artist% - %title% - DeaDBeeF-%_deadbeef_version%";
399 const char *gtkui_default_titlebar_stopped = "DeaDBeeF-%_deadbeef_version%";
400 
401 static char *titlebar_playing_bc;
402 static char *titlebar_stopped_bc;
403 
404 static void
titlebar_tf_free(void)405 titlebar_tf_free (void) {
406     if (titlebar_playing_bc) {
407         deadbeef->tf_free (titlebar_playing_bc);
408         titlebar_playing_bc = NULL;
409     }
410 
411     if (titlebar_stopped_bc) {
412         deadbeef->tf_free (titlebar_stopped_bc);
413         titlebar_stopped_bc = NULL;
414     }
415 }
416 
417 void
gtkui_titlebar_tf_init(void)418 gtkui_titlebar_tf_init (void) {
419     titlebar_tf_free ();
420 
421     char fmt[500];
422     deadbeef->conf_get_str ("gtkui.titlebar_playing_tf", gtkui_default_titlebar_playing, fmt, sizeof (fmt));
423     titlebar_playing_bc = deadbeef->tf_compile (fmt);
424     deadbeef->conf_get_str ("gtkui.titlebar_stopped_tf", gtkui_default_titlebar_stopped, fmt, sizeof (fmt));
425     titlebar_stopped_bc = deadbeef->tf_compile (fmt);
426 }
427 
428 void
gtkui_set_titlebar(DB_playItem_t * it)429 gtkui_set_titlebar (DB_playItem_t *it) {
430     if (!it) {
431         it = deadbeef->streamer_get_playing_track ();
432     }
433     else {
434         deadbeef->pl_item_ref (it);
435     }
436     char str[1024];
437     ddb_tf_context_t ctx = {
438         ._size = sizeof (ddb_tf_context_t),
439         .it = it,
440         // FIXME: current playlist is not correct here.
441         // need the playlist corresponding to the pointed track
442         .plt = deadbeef->plt_get_curr (),
443     };
444     deadbeef->tf_eval (&ctx, it ? titlebar_playing_bc : titlebar_stopped_bc, str, sizeof (str));
445     if (ctx.plt) {
446         deadbeef->plt_unref (ctx.plt);
447         ctx.plt = NULL;
448     }
449     gtk_window_set_title (GTK_WINDOW (mainwin), str);
450     if (it) {
451         deadbeef->pl_item_unref (it);
452     }
453     set_tray_tooltip (str);
454 }
455 
456 static void
trackinfochanged_wrapper(DdbListview * playlist,DB_playItem_t * track,int iter)457 trackinfochanged_wrapper (DdbListview *playlist, DB_playItem_t *track, int iter) {
458     if (track) {
459         int idx = deadbeef->pl_get_idx_of_iter (track, iter);
460         if (idx != -1) {
461             ddb_listview_draw_row (playlist, idx, (DdbListviewIter)track);
462         }
463     }
464 }
465 
466 void
gtkui_trackinfochanged(DB_playItem_t * track)467 gtkui_trackinfochanged (DB_playItem_t *track) {
468     if (searchwin && gtk_widget_get_visible (searchwin)) {
469         GtkWidget *search = lookup_widget (searchwin, "searchlist");
470         trackinfochanged_wrapper (DDB_LISTVIEW (search), track, PL_SEARCH);
471     }
472 
473     DB_playItem_t *curr = deadbeef->streamer_get_playing_track ();
474     if (track == curr) {
475         gtkui_set_titlebar (track);
476     }
477     if (curr) {
478         deadbeef->pl_item_unref (curr);
479     }
480 }
481 
482 static gboolean
trackinfochanged_cb(gpointer data)483 trackinfochanged_cb (gpointer data) {
484     gtkui_trackinfochanged (data);
485     if (data) {
486         deadbeef->pl_item_unref ((DB_playItem_t *)data);
487     }
488     return FALSE;
489 }
490 
491 void
playlist_refresh(void)492 playlist_refresh (void) {
493     search_refresh ();
494     trkproperties_fill_metadata ();
495 }
496 
497 static gboolean
playlistcontentchanged_cb(gpointer none)498 playlistcontentchanged_cb (gpointer none) {
499     playlist_refresh ();
500     return FALSE;
501 }
502 
503 static gboolean
playlistswitch_cb(gpointer none)504 playlistswitch_cb (gpointer none) {
505     search_refresh ();
506     return FALSE;
507 }
508 
509 static gboolean
gtkui_on_frameupdate(gpointer data)510 gtkui_on_frameupdate (gpointer data) {
511     update_songinfo (NULL);
512 
513     return TRUE;
514 }
515 
516 static gboolean
gtkui_update_status_icon(gpointer unused)517 gtkui_update_status_icon (gpointer unused) {
518     int hide_tray_icon = deadbeef->conf_get_int ("gtkui.hide_tray_icon", 0);
519     if (hide_tray_icon && !trayicon) {
520         return FALSE;
521     }
522     if (trayicon) {
523         if (hide_tray_icon) {
524             g_object_set (trayicon, "visible", FALSE, NULL);
525         }
526         else {
527             g_object_set (trayicon, "visible", TRUE, NULL);
528         }
529         return FALSE;
530     }
531     // system tray icon
532     traymenu = create_traymenu ();
533 
534     char tmp[1000];
535     const char *icon_name = tmp;
536     deadbeef->conf_get_str ("gtkui.custom_tray_icon", TRAY_ICON, tmp, sizeof (tmp));
537     GtkIconTheme *theme = gtk_icon_theme_get_default();
538 
539     if (!gtk_icon_theme_has_icon(theme, icon_name))
540         icon_name = "deadbeef";
541     else {
542         GtkIconInfo *icon_info = gtk_icon_theme_lookup_icon(theme, icon_name, 48, GTK_ICON_LOOKUP_USE_BUILTIN);
543         const gboolean icon_is_builtin = gtk_icon_info_get_filename(icon_info) == NULL;
544         gtk_icon_info_free(icon_info);
545         icon_name = icon_is_builtin ? "deadbeef" : icon_name;
546     }
547 
548     if (!gtk_icon_theme_has_icon(theme, icon_name)) {
549         char iconpath[1024];
550         snprintf (iconpath, sizeof (iconpath), "%s/deadbeef.png", deadbeef->get_prefix ());
551         trayicon = gtk_status_icon_new_from_file(iconpath);
552     }
553     else {
554         trayicon = gtk_status_icon_new_from_icon_name(icon_name);
555     }
556     if (hide_tray_icon) {
557         g_object_set (trayicon, "visible", FALSE, NULL);
558     }
559 
560 #if !GTK_CHECK_VERSION(2,14,0)
561     g_signal_connect ((gpointer)trayicon, "activate", G_CALLBACK (on_trayicon_activate), NULL);
562 #else
563     printf ("connecting button tray signals\n");
564     g_signal_connect ((gpointer)trayicon, "scroll_event", G_CALLBACK (on_trayicon_scroll_event), NULL);
565     g_signal_connect ((gpointer)trayicon, "button_press_event", G_CALLBACK (on_trayicon_button_press_event), NULL);
566 #endif
567     g_signal_connect ((gpointer)trayicon, "popup_menu", G_CALLBACK (on_trayicon_popup_menu), NULL);
568 
569     gtkui_set_titlebar (NULL);
570 
571     return FALSE;
572 }
573 
574 static void
gtkui_hide_status_icon()575 gtkui_hide_status_icon () {
576     if (trayicon) {
577         g_object_set (trayicon, "visible", FALSE, NULL);
578     }
579 }
580 
581 int
gtkui_get_curr_playlist_mod(void)582 gtkui_get_curr_playlist_mod (void) {
583     ddb_playlist_t *plt = deadbeef->plt_get_curr ();
584     int res = plt ? deadbeef->plt_get_modification_idx (plt) : 0;
585     if (plt) {
586         deadbeef->plt_unref (plt);
587     }
588     return res;
589 }
590 
591 void
gtkui_setup_gui_refresh(void)592 gtkui_setup_gui_refresh (void) {
593     int tm = 1000/gtkui_get_gui_refresh_rate ();
594 
595     if (refresh_timeout) {
596         g_source_remove (refresh_timeout);
597         refresh_timeout = 0;
598     }
599 
600     refresh_timeout = g_timeout_add (tm, gtkui_on_frameupdate, NULL);
601 }
602 
603 
604 static gboolean
gtkui_on_configchanged(void * data)605 gtkui_on_configchanged (void *data) {
606     // order and looping
607     const char *w;
608 
609     // order
610     const char *orderwidgets[4] = { "order_linear", "order_shuffle", "order_random", "order_shuffle_albums" };
611     w = orderwidgets[deadbeef->conf_get_int ("playback.order", PLAYBACK_ORDER_LINEAR)];
612     gtk_check_menu_item_set_active (GTK_CHECK_MENU_ITEM (lookup_widget (mainwin, w)), TRUE);
613 
614     // looping
615     const char *loopingwidgets[3] = { "loop_all", "loop_disable", "loop_single" };
616     w = loopingwidgets[deadbeef->conf_get_int ("playback.loop", PLAYBACK_MODE_LOOP_ALL)];
617     gtk_check_menu_item_set_active (GTK_CHECK_MENU_ITEM (lookup_widget (mainwin, w)), TRUE);
618 
619     // scroll follows playback
620     gtk_check_menu_item_set_active (GTK_CHECK_MENU_ITEM (lookup_widget (mainwin, "scroll_follows_playback")), deadbeef->conf_get_int ("playlist.scroll.followplayback", 1) ? TRUE : FALSE);
621 
622     // cursor follows playback
623     gtk_check_menu_item_set_active (GTK_CHECK_MENU_ITEM (lookup_widget (mainwin, "cursor_follows_playback")), deadbeef->conf_get_int ("playlist.scroll.cursorfollowplayback", 1) ? TRUE : FALSE);
624 
625     // stop after current track
626     int stop_after_current = deadbeef->conf_get_int ("playlist.stop_after_current", 0);
627     gtk_check_menu_item_set_active (GTK_CHECK_MENU_ITEM (lookup_widget (mainwin, "stop_after_current")), stop_after_current ? TRUE : FALSE);
628 
629     // stop after current album
630     int stop_after_album = deadbeef->conf_get_int ("playlist.stop_after_album", 0);
631     gtk_check_menu_item_set_active (GTK_CHECK_MENU_ITEM (lookup_widget (mainwin, "stop_after_album")), stop_after_album ? TRUE : FALSE);
632 
633     gtkui_embolden_current_track = deadbeef->conf_get_int ("gtkui.embolden_current_track", 0);
634     gtkui_embolden_tracks = deadbeef->conf_get_int ("gtkui.embolden_tracks", 0);
635     gtkui_embolden_selected_tracks = deadbeef->conf_get_int ("gtkui.embolden_selected_tracks", 0);
636     gtkui_italic_current_track = deadbeef->conf_get_int ("gtkui.italic_current_track", 0);
637     gtkui_italic_tracks = deadbeef->conf_get_int ("gtkui.italic_tracks", 0);
638     gtkui_italic_selected_tracks = deadbeef->conf_get_int ("gtkui.italic_selected_tracks", 0);
639     gtkui_tabstrip_embolden_playing = deadbeef->conf_get_int ("gtkui.tabstrip_embolden_playing", 0);
640     gtkui_tabstrip_italic_playing = deadbeef->conf_get_int ("gtkui.tabstrip_italic_playing", 0);
641     gtkui_tabstrip_embolden_selected = deadbeef->conf_get_int ("gtkui.tabstrip_embolden_selected", 0);
642     gtkui_tabstrip_italic_selected = deadbeef->conf_get_int ("gtkui.tabstrip_italic_selected", 0);
643 
644     // titlebar tf
645     gtkui_titlebar_tf_init ();
646 
647     // pin groups
648     gtkui_groups_pinned = deadbeef->conf_get_int ("playlist.pin.groups", 0);
649 
650     // play state images
651     gtkui_unicode_playstate = deadbeef->conf_get_int ("gtkui.unicode_playstate", 0);
652 
653     // seekbar overlay
654     gtkui_disable_seekbar_overlay = deadbeef->conf_get_int ("gtkui.disable_seekbar_overlay", 0);
655 
656     // tray icon
657     gtkui_update_status_icon (NULL);
658 
659     // statusbar refresh
660     gtkui_setup_gui_refresh ();
661 
662     return FALSE;
663 }
664 
665 static gboolean
outputchanged_cb(gpointer nothing)666 outputchanged_cb (gpointer nothing) {
667     preferences_fill_soundcards ();
668     return FALSE;
669 }
670 
671 void
save_playlist_as(void)672 save_playlist_as (void) {
673     gdk_threads_add_idle (action_save_playlist_handler_cb, NULL);
674 }
675 
676 void
on_playlist_save_as_activate(GtkMenuItem * menuitem,gpointer user_data)677 on_playlist_save_as_activate           (GtkMenuItem     *menuitem,
678                                         gpointer         user_data)
679 {
680     save_playlist_as ();
681 }
682 
683 void
on_playlist_load_activate(GtkMenuItem * menuitem,gpointer user_data)684 on_playlist_load_activate              (GtkMenuItem     *menuitem,
685                                         gpointer         user_data)
686 {
687     gdk_threads_add_idle (action_load_playlist_handler_cb, NULL);
688 }
689 
690 void
on_add_location_activate(GtkMenuItem * menuitem,gpointer user_data)691 on_add_location_activate               (GtkMenuItem     *menuitem,
692                                         gpointer         user_data)
693 {
694     gdk_threads_add_idle (action_add_location_handler_cb, NULL);
695 }
696 
697 static gboolean
update_win_title_idle(gpointer data)698 update_win_title_idle (gpointer data) {
699     struct fromto_t *ft = (struct fromto_t *)data;
700     DB_playItem_t *from = ft->from;
701     DB_playItem_t *to = ft->to;
702     free (ft);
703 
704     // update window title
705     if (from || to) {
706         if (to) {
707             DB_playItem_t *it = deadbeef->streamer_get_playing_track ();
708             if (it) { // it might have been deleted after event was sent
709                 gtkui_set_titlebar (it);
710                 deadbeef->pl_item_unref (it);
711             }
712             else {
713                 gtkui_set_titlebar (NULL);
714             }
715         }
716         else {
717             gtkui_set_titlebar (NULL);
718         }
719     }
720     if (from) {
721         deadbeef->pl_item_unref (from);
722     }
723     if (to) {
724         deadbeef->pl_item_unref (to);
725     }
726     return FALSE;
727 }
728 
729 int
gtkui_add_new_playlist(void)730 gtkui_add_new_playlist (void) {
731     int cnt = deadbeef->plt_get_count ();
732     int i;
733     int idx = 0;
734     for (;;) {
735         char name[100];
736         if (!idx) {
737             strcpy (name, _("New Playlist"));
738         }
739         else {
740             snprintf (name, sizeof (name), _("New Playlist (%d)"), idx);
741         }
742         deadbeef->pl_lock ();
743         for (i = 0; i < cnt; i++) {
744             char t[100];
745             ddb_playlist_t *plt = deadbeef->plt_get_for_idx (i);
746             deadbeef->plt_get_title (plt, t, sizeof (t));
747             deadbeef->plt_unref (plt);
748             if (!strcasecmp (t, name)) {
749                 break;
750             }
751         }
752         deadbeef->pl_unlock ();
753         if (i == cnt) {
754             return deadbeef->plt_add (cnt, name);
755         }
756         idx++;
757     }
758     return -1;
759 }
760 
761 int
gtkui_get_gui_refresh_rate()762 gtkui_get_gui_refresh_rate () {
763     int fps = deadbeef->conf_get_int ("gtkui.refresh_rate", 10);
764     if (fps < 1) {
765         fps = 1;
766     }
767     else if (fps > 30) {
768         fps = 30;
769     }
770     return fps;
771 }
772 
773 static void
send_messages_to_widgets(ddb_gtkui_widget_t * w,uint32_t id,uintptr_t ctx,uint32_t p1,uint32_t p2)774 send_messages_to_widgets (ddb_gtkui_widget_t *w, uint32_t id, uintptr_t ctx, uint32_t p1, uint32_t p2) {
775     for (ddb_gtkui_widget_t *c = w->children; c; c = c->next) {
776         send_messages_to_widgets (c, id, ctx, p1, p2);
777     }
778     if (w->message) {
779         w->message (w, id, ctx, p1, p2);
780     }
781 }
782 
783 gboolean
add_mainmenu_actions_cb(void * data)784 add_mainmenu_actions_cb (void *data) {
785     add_mainmenu_actions ();
786     return FALSE;
787 }
788 
789 int
790 gtkui_thread (void *ctx);
791 
792 int
793 gtkui_plt_add_dir (ddb_playlist_t *plt, const char *dirname, int (*cb)(DB_playItem_t *it, void *data), void *user_data);
794 
795 int
796 gtkui_plt_add_file (ddb_playlist_t *plt, const char *filename, int (*cb)(DB_playItem_t *it, void *data), void *user_data);
797 
798 int
799 gtkui_pl_add_files_begin (ddb_playlist_t *plt);
800 
801 void
802 gtkui_pl_add_files_end (void);
803 
804 DB_playItem_t *
805 gtkui_plt_load (ddb_playlist_t *plt, DB_playItem_t *after, const char *fname, int *pabort, int (*cb)(DB_playItem_t *it, void *data), void *user_data);
806 
807 int
gtkui_message(uint32_t id,uintptr_t ctx,uint32_t p1,uint32_t p2)808 gtkui_message (uint32_t id, uintptr_t ctx, uint32_t p1, uint32_t p2) {
809     if (!gtkui_accept_messages) {
810         return -1;
811     }
812     ddb_gtkui_widget_t *rootwidget = w_get_rootwidget ();
813     if (rootwidget) {
814         send_messages_to_widgets (rootwidget, id, ctx, p1, p2);
815     }
816 
817     switch (id) {
818     case DB_EV_ACTIVATED:
819         g_idle_add (activate_cb, NULL);
820         break;
821     case DB_EV_SONGCHANGED:
822         {
823             ddb_event_trackchange_t *ev = (ddb_event_trackchange_t *)ctx;
824             gtkpl_songchanged_wrapper (ev->from, ev->to);
825         }
826         break;
827     case DB_EV_TRACKINFOCHANGED:
828         {
829             ddb_event_track_t *ev = (ddb_event_track_t *)ctx;
830             if (ev->track) {
831                 deadbeef->pl_item_ref (ev->track);
832             }
833             g_idle_add (trackinfochanged_cb, ev->track);
834         }
835         break;
836 //    case DB_EV_PAUSED:
837 //        g_idle_add (paused_cb, NULL);
838 //        break;
839     case DB_EV_PLAYLISTCHANGED:
840         if (p1 == DDB_PLAYLIST_CHANGE_CONTENT) {
841             g_idle_add (playlistcontentchanged_cb, NULL);
842         }
843         break;
844     case DB_EV_CONFIGCHANGED:
845         g_idle_add (gtkui_on_configchanged, NULL);
846         break;
847     case DB_EV_OUTPUTCHANGED:
848         g_idle_add (outputchanged_cb, NULL);
849         break;
850     case DB_EV_PLAYLISTSWITCHED:
851         g_idle_add (playlistswitch_cb, NULL);
852         break;
853     case DB_EV_ACTIONSCHANGED:
854         g_idle_add (add_mainmenu_actions_cb, NULL);
855         break;
856     case DB_EV_DSPCHAINCHANGED:
857         eq_refresh ();
858         break;
859     }
860     return 0;
861 }
862 
863 static const char gtkui_def_layout[] = "vbox expand=\"0 1\" fill=\"1 1\" homogeneous=0 {hbox expand=\"0 1 0\" fill=\"1 1 1\" homogeneous=0 {playtb {} seekbar {} volumebar {} } tabbed_playlist hideheaders=0 {} } ";
864 
865 static void
init_widget_layout(void)866 init_widget_layout (void) {
867     w_init ();
868     ddb_gtkui_widget_t *rootwidget = w_get_rootwidget ();
869     gtk_widget_show (rootwidget->widget);
870     gtk_box_pack_start (GTK_BOX(lookup_widget(mainwin, "plugins_bottom_vbox")), rootwidget->widget, TRUE, TRUE, 0);
871 
872     // load layout
873     // config var name is defined in DDB_GTKUI_CONF_LAYOUT
874     // gtkui.layout: 0.6.0 and 0.6.1
875     // gtkui.layout.major.minor.point: later versions
876 
877     char layout[20000];
878     deadbeef->conf_get_str (DDB_GTKUI_CONF_LAYOUT, "-", layout, sizeof (layout));
879     if (!strcmp (layout, "-")) {
880         // upgrade from 0.6.0 to 0.6.2
881         char layout_060[20000];
882         deadbeef->conf_get_str ("gtkui.layout", "-", layout_060, sizeof (layout_060));
883         if (!strcmp (layout_060, "-")) {
884             // new setup
885             strcpy (layout, gtkui_def_layout);
886         }
887         else {
888             // upgrade with top bar
889             snprintf (layout, sizeof (layout), "vbox expand=\"0 1\" fill=\"1 1\" homogeneous=0 {hbox expand=\"0 1 0\" fill=\"1 1 1\" homogeneous=0 {playtb {} seekbar {} volumebar {} } %s }", layout_060);
890             deadbeef->conf_set_str (DDB_GTKUI_CONF_LAYOUT, layout);
891             deadbeef->conf_save ();
892         }
893     }
894 
895     ddb_gtkui_widget_t *w = NULL;
896     w_create_from_string (layout, &w);
897     if (!w) {
898         ddb_gtkui_widget_t *plt = w_create ("tabbed_playlist");
899         w_append (rootwidget, plt);
900         gtk_widget_show (plt->widget);
901     }
902     else {
903         w_append (rootwidget, w);
904     }
905 }
906 
907 static DB_plugin_t *supereq_plugin;
908 
909 gboolean
gtkui_connect_cb(void * none)910 gtkui_connect_cb (void *none) {
911     // equalizer
912     GtkWidget *eq_mi = lookup_widget (mainwin, "view_eq");
913     if (!supereq_plugin) {
914         gtk_widget_hide (GTK_WIDGET (eq_mi));
915     }
916     else {
917         if (deadbeef->conf_get_int ("gtkui.eq.visible", 0)) {
918             gtk_check_menu_item_set_active (GTK_CHECK_MENU_ITEM (eq_mi), TRUE);
919             eq_window_show ();
920         }
921         else {
922             gtk_check_menu_item_set_active (GTK_CHECK_MENU_ITEM (eq_mi), FALSE);
923         }
924     }
925 
926     add_mainmenu_actions ();
927     ddb_event_t *e = deadbeef->event_alloc (DB_EV_TRACKINFOCHANGED);
928     deadbeef->event_send(e, 0, 0);
929     return FALSE;
930 }
931 
932 int
gtkui_add_file_info_cb(ddb_fileadd_data_t * data,void * user_data)933 gtkui_add_file_info_cb (ddb_fileadd_data_t *data, void *user_data) {
934     if (data->visibility == 0) {
935         if (progress_is_aborted ()) {
936             return -1;
937         }
938         deadbeef->pl_lock ();
939         const char *fname = deadbeef->pl_find_meta (data->track, ":URI");
940         g_idle_add (gtkui_set_progress_text_idle, (gpointer)strdup(fname)); // slowwwww
941         deadbeef->pl_unlock ();
942     }
943     return 0;
944 }
945 
946 void
gtkui_add_file_begin_cb(ddb_fileadd_data_t * data,void * user_data)947 gtkui_add_file_begin_cb (ddb_fileadd_data_t *data, void *user_data) {
948     if (data->visibility == 0) {
949         progress_show ();
950     }
951 }
952 
953 void
gtkui_add_file_end_cb(ddb_fileadd_data_t * data,void * user_data)954 gtkui_add_file_end_cb (ddb_fileadd_data_t *data, void *user_data) {
955     if (data->visibility == 0) {
956         progress_hide ();
957     }
958 }
959 
960 typedef struct {
961     void (*callback) (void *userdata);
962     void *userdata;
963 } window_init_hook_t;
964 
965 #define WINDOW_INIT_HOOK_MAX 10
966 static window_init_hook_t window_init_hooks[WINDOW_INIT_HOOK_MAX];
967 static int window_init_hooks_count;
968 
969 static void
add_window_init_hook(void (* callback)(void * userdata),void * userdata)970 add_window_init_hook (void (*callback) (void *userdata), void *userdata) {
971     if (window_init_hooks_count >= WINDOW_INIT_HOOK_MAX) {
972         fprintf (stderr, "gtkui: add_window_init_hook can't add another hook, maximum number of hooks (%d) exceeded\n", (int)WINDOW_INIT_HOOK_MAX);
973         return;
974     }
975 
976     window_init_hooks[window_init_hooks_count].callback = callback;
977     window_init_hooks[window_init_hooks_count].userdata = userdata;
978     window_init_hooks_count++;
979 }
980 
981 int
gtkui_thread(void * ctx)982 gtkui_thread (void *ctx) {
983 #ifdef __linux__
984     prctl (PR_SET_NAME, "deadbeef-gtkui", 0, 0, 0, 0);
985 #endif
986 
987     int argc = 2;
988     const char **argv = alloca (sizeof (char *) * argc);
989     argv[0] = "deadbeef";
990     argv[1] = "--sync";
991     //argv[1] = "--g-fatal-warnings";
992     if (!deadbeef->conf_get_int ("gtkui.sync", 0)) {
993         argc = 1;
994     }
995 
996     gtk_disable_setlocale ();
997     add_pixmap_directory (deadbeef->get_pixmap_dir ());
998 
999     // let's start some gtk
1000     g_thread_init (NULL);
1001     gdk_threads_init ();
1002     gdk_threads_enter ();
1003 
1004     gtk_init (&argc, (char ***)&argv);
1005 
1006     // register widget types
1007     w_reg_widget (_("Playlist with tabs"), DDB_WF_SINGLE_INSTANCE, w_tabbed_playlist_create, "tabbed_playlist", NULL);
1008     w_reg_widget (_("Playlist"), DDB_WF_SINGLE_INSTANCE, w_playlist_create, "playlist", NULL);
1009     w_reg_widget (NULL, 0, w_box_create, "box", NULL);
1010     w_reg_widget (NULL, 0, w_dummy_create, "dummy", NULL);
1011     w_reg_widget (_("Splitter (top and bottom)"), 0, w_vsplitter_create, "vsplitter", NULL);
1012     w_reg_widget (_("Splitter (left and right)"), 0, w_hsplitter_create, "hsplitter", NULL);
1013     w_reg_widget (NULL, 0, w_placeholder_create, "placeholder", NULL);
1014     w_reg_widget (_("Tabs"), 0, w_tabs_create, "tabs", NULL);
1015     w_reg_widget (_("Playlist tabs"), 0, w_tabstrip_create, "tabstrip", NULL);
1016     w_reg_widget (_("Selection properties"), 0, w_selproperties_create, "selproperties", NULL);
1017     w_reg_widget (_("Album art display"), 0, w_coverart_create, "coverart", NULL);
1018     w_reg_widget (_("Scope"), 0, w_scope_create, "scope", NULL);
1019     w_reg_widget (_("Spectrum"), 0, w_spectrum_create, "spectrum", NULL);
1020     w_reg_widget (_("HBox"), 0, w_hbox_create, "hbox", NULL);
1021     w_reg_widget (_("VBox"), 0, w_vbox_create, "vbox", NULL);
1022     w_reg_widget (_("Button"), 0, w_button_create, "button", NULL);
1023     w_reg_widget (_("Seekbar"), 0, w_seekbar_create, "seekbar", NULL);
1024     w_reg_widget (_("Playback controls"), 0, w_playtb_create, "playtb", NULL);
1025     w_reg_widget (_("Volume bar"), 0, w_volumebar_create, "volumebar", NULL);
1026     w_reg_widget (_("Chiptune voices"), 0, w_ctvoices_create, "ctvoices", NULL);
1027 
1028     mainwin = create_mainwin ();
1029 
1030     // initialize default hotkey mapping
1031     if (!deadbeef->conf_get_int ("hotkeys_created", 0)) {
1032         // check if any hotkeys were created manually (e.g. beta versions of 0.6)
1033         if (!deadbeef->conf_find ("hotkey.key", NULL)) {
1034             gtkui_set_default_hotkeys ();
1035             gtkui_import_0_5_global_hotkeys ();
1036             DB_plugin_t *hkplug = deadbeef->plug_get_for_id ("hotkeys");
1037             if (hkplug) {
1038                 ((DB_hotkeys_plugin_t *)hkplug)->reset ();
1039             }
1040         }
1041         deadbeef->conf_set_int ("hotkeys_created", 1);
1042         deadbeef->conf_save ();
1043     }
1044 #if GTK_CHECK_VERSION(3,0,0)
1045     gtk_widget_set_events (GTK_WIDGET (mainwin), gtk_widget_get_events (GTK_WIDGET (mainwin)) | GDK_SCROLL_MASK);
1046 #endif
1047 
1048     pl_common_init();
1049 
1050     GtkIconTheme *theme = gtk_icon_theme_get_default();
1051     if (gtk_icon_theme_has_icon(theme, "deadbeef")) {
1052         gtk_window_set_icon_name (GTK_WINDOW (mainwin), "deadbeef");
1053     }
1054     else {
1055         // try loading icon from $prefix/deadbeef.png (for static build)
1056         char iconpath[1024];
1057         snprintf (iconpath, sizeof (iconpath), "%s/deadbeef.png", deadbeef->get_prefix ());
1058         gtk_window_set_icon_from_file (GTK_WINDOW (mainwin), iconpath, NULL);
1059     }
1060 
1061     wingeom_restore (mainwin, "mainwin", 40, 40, 500, 300, 0);
1062 
1063     gtkui_on_configchanged (NULL);
1064     gtkui_init_theme_colors ();
1065 
1066     // visibility of statusbar and headers
1067     GtkWidget *sb_mi = lookup_widget (mainwin, "view_status_bar");
1068     GtkWidget *sb = lookup_widget (mainwin, "statusbar");
1069     if (deadbeef->conf_get_int ("gtkui.statusbar.visible", 1)) {
1070         gtk_check_menu_item_set_active (GTK_CHECK_MENU_ITEM (sb_mi), TRUE);
1071     }
1072     else {
1073         gtk_check_menu_item_set_active (GTK_CHECK_MENU_ITEM (sb_mi), FALSE);
1074         gtk_widget_hide (sb);
1075     }
1076 
1077     GtkWidget *menu = lookup_widget (mainwin, "menubar");
1078     if (deadbeef->conf_get_int ("gtkui.show_menu", 1)) {
1079         gtk_widget_show (menu);
1080     }
1081     else {
1082         gtk_widget_hide (menu);
1083     }
1084 
1085     searchwin = create_searchwin ();
1086     gtk_window_set_transient_for (GTK_WINDOW (searchwin), GTK_WINDOW (mainwin));
1087 
1088     DdbListview *search_playlist = DDB_LISTVIEW (lookup_widget (searchwin, "searchlist"));
1089     search_playlist_init (GTK_WIDGET (search_playlist));
1090 
1091     progress_init ();
1092     cover_art_init ();
1093 
1094 #ifdef __APPLE__
1095 #if 0
1096     GtkWidget *menubar = lookup_widget (mainwin, "menubar");
1097     gtk_widget_hide (menubar);
1098     GtkosxApplication *theApp = g_object_new(GTKOSX_TYPE_APPLICATION, NULL);
1099     gtkosx_application_set_menu_bar(theApp, GTK_MENU_SHELL(menubar));
1100 #endif
1101 #endif
1102 
1103     for (int i = 0; i < window_init_hooks_count; i++) {
1104         window_init_hooks[i].callback (window_init_hooks[i].userdata);
1105     }
1106     gtk_widget_show (mainwin);
1107 
1108     init_widget_layout ();
1109 
1110     gtkui_set_titlebar (NULL);
1111 
1112     fileadded_listener_id = deadbeef->listen_file_added (gtkui_add_file_info_cb, NULL);
1113     fileadd_beginend_listener_id = deadbeef->listen_file_add_beginend (gtkui_add_file_begin_cb, gtkui_add_file_end_cb, NULL);
1114 
1115     supereq_plugin = deadbeef->plug_get_for_id ("supereq");
1116 
1117     gtkui_connect_cb (NULL);
1118 
1119     gtkui_accept_messages = 1;
1120     deadbeef->sendmessage (DB_EV_PLAYLISTCHANGED, 0, DDB_PLAYLIST_CHANGE_CONTENT, 0);
1121 
1122 #ifdef __APPLE__
1123     gtkui_is_retina = is_retina (mainwin);
1124 #endif
1125 
1126     gtk_main ();
1127 
1128     deadbeef->unlisten_file_added (fileadded_listener_id);
1129     deadbeef->unlisten_file_add_beginend (fileadd_beginend_listener_id);
1130 
1131     w_free ();
1132 
1133     if (refresh_timeout) {
1134         g_source_remove (refresh_timeout);
1135         refresh_timeout = 0;
1136     }
1137     cover_art_free ();
1138     eq_window_destroy ();
1139     trkproperties_destroy ();
1140     progress_destroy ();
1141     gtkui_hide_status_icon ();
1142     pl_common_free();
1143 //    draw_free ();
1144     titlebar_tf_free ();
1145     if (mainwin) {
1146         gtk_widget_destroy (mainwin);
1147         mainwin = NULL;
1148     }
1149     if (searchwin) {
1150         gtk_widget_destroy (searchwin);
1151         searchwin = NULL;
1152     }
1153     gdk_threads_leave ();
1154     return 0;
1155 }
1156 
1157 gboolean
gtkui_set_progress_text_idle(gpointer data)1158 gtkui_set_progress_text_idle (gpointer data) {
1159     char *text = (char *)data;
1160     if (text) {
1161         progress_settext (text);
1162         free (text);
1163     }
1164     return FALSE;
1165 }
1166 
1167 void
gtkui_playlist_set_curr(int playlist)1168 gtkui_playlist_set_curr (int playlist) {
1169     deadbeef->plt_set_curr_idx (playlist);
1170     deadbeef->conf_set_int ("playlist.current", playlist);
1171 }
1172 
1173 void
on_gtkui_info_window_delete(GtkWidget * widget,GtkTextDirection previous_direction,GtkWidget ** pwindow)1174 on_gtkui_info_window_delete (GtkWidget *widget, GtkTextDirection previous_direction, GtkWidget **pwindow) {
1175     *pwindow = NULL;
1176     gtk_widget_hide (widget);
1177     gtk_widget_destroy (widget);
1178 }
1179 
1180 void
gtkui_show_info_window(const char * fname,const char * title,GtkWidget ** pwindow)1181 gtkui_show_info_window (const char *fname, const char *title, GtkWidget **pwindow) {
1182     if (*pwindow) {
1183         return;
1184     }
1185     GtkWidget *widget = *pwindow = create_helpwindow ();
1186     g_object_set_data (G_OBJECT (widget), "pointer", pwindow);
1187     g_signal_connect (widget, "delete_event", G_CALLBACK (on_gtkui_info_window_delete), pwindow);
1188     gtk_window_set_title (GTK_WINDOW (widget), title);
1189     gtk_window_set_transient_for (GTK_WINDOW (widget), GTK_WINDOW (mainwin));
1190     GtkWidget *txt = lookup_widget (widget, "helptext");
1191     GtkTextBuffer *buffer = gtk_text_buffer_new (NULL);
1192 
1193     FILE *fp = fopen (fname, "rb");
1194     if (fp) {
1195         fseek (fp, 0, SEEK_END);
1196         size_t s = ftell (fp);
1197         rewind (fp);
1198         char buf[s+1];
1199         if (fread (buf, 1, s, fp) != s) {
1200             fprintf (stderr, "error reading help file contents\n");
1201             const char *error = _("Failed while reading help file");
1202             gtk_text_buffer_set_text (buffer, error, strlen (error));
1203         }
1204         else {
1205             buf[s] = 0;
1206             gtk_text_buffer_set_text (buffer, buf, s);
1207         }
1208         fclose (fp);
1209     }
1210     else {
1211         const char *error = _("Failed to load help file");
1212         gtk_text_buffer_set_text (buffer, error, strlen (error));
1213     }
1214     gtk_text_view_set_buffer (GTK_TEXT_VIEW (txt), buffer);
1215     g_object_unref (buffer);
1216     gtk_widget_show (widget);
1217 }
1218 
1219 gboolean
gtkui_quit_cb(void * ctx)1220 gtkui_quit_cb (void *ctx) {
1221     w_save ();
1222 
1223     if (deadbeef->have_background_jobs ()) {
1224         GtkWidget *dlg = gtk_message_dialog_new (GTK_WINDOW (mainwin), GTK_DIALOG_MODAL, GTK_MESSAGE_WARNING, GTK_BUTTONS_YES_NO, _("The player is currently running background tasks. If you quit now, the tasks will be cancelled or interrupted. This may result in data loss."));
1225         gtk_window_set_transient_for (GTK_WINDOW (dlg), GTK_WINDOW (mainwin));
1226         gtk_message_dialog_format_secondary_text (GTK_MESSAGE_DIALOG (dlg), _("Do you still want to quit?"));
1227         gtk_window_set_title (GTK_WINDOW (dlg), _("Warning"));
1228 
1229         int response = gtk_dialog_run (GTK_DIALOG (dlg));
1230         gtk_widget_destroy (dlg);
1231         if (response != GTK_RESPONSE_YES) {
1232             return FALSE;
1233         }
1234         else {
1235             exit (0);
1236         }
1237     }
1238     else {
1239         progress_abort ();
1240         deadbeef->sendmessage (DB_EV_TERMINATE, 0, 0, 0);
1241     }
1242     return FALSE;
1243 }
1244 
1245 void
gtkui_quit(void)1246 gtkui_quit (void) {
1247     gdk_threads_add_idle (gtkui_quit_cb, NULL);
1248 }
1249 
1250 static void
import_legacy_tf(const char * key_from,const char * key_to)1251 import_legacy_tf (const char *key_from, const char *key_to) {
1252     deadbeef->conf_lock ();
1253     if (!deadbeef->conf_get_str_fast (key_to, NULL)
1254             && deadbeef->conf_get_str_fast (key_from, NULL)) {
1255         char old[200], new[200];
1256         deadbeef->conf_get_str (key_from, "", old, sizeof (old));
1257         deadbeef->tf_import_legacy (old, new, sizeof (new));
1258         deadbeef->conf_set_str (key_to, new);
1259         deadbeef->conf_save ();
1260     }
1261     deadbeef->conf_unlock ();
1262 }
1263 
1264 static int
gtkui_start(void)1265 gtkui_start (void) {
1266     fprintf (stderr, "gtkui plugin compiled for gtk version: %d.%d.%d\n", GTK_MAJOR_VERSION, GTK_MINOR_VERSION, GTK_MICRO_VERSION);
1267 
1268     import_legacy_tf ("gtkui.titlebar_playing", "gtkui.titlebar_playing_tf");
1269     import_legacy_tf ("gtkui.titlebar_stopped", "gtkui.titlebar_stopped_tf");
1270 
1271     import_legacy_tf ("playlist.group_by", "gtkui.playlist.group_by_tf");
1272 
1273     gtkui_thread (NULL);
1274 
1275     return 0;
1276 }
1277 
1278 static int
gtkui_connect(void)1279 gtkui_connect (void) {
1280     return 0;
1281 }
1282 
1283 static int
gtkui_disconnect(void)1284 gtkui_disconnect (void) {
1285     supereq_plugin = NULL;
1286     return 0;
1287 }
1288 
1289 
1290 static gboolean
quit_gtk_cb(gpointer nothing)1291 quit_gtk_cb (gpointer nothing) {
1292     extern int trkproperties_modified;
1293     trkproperties_modified = 0;
1294     trkproperties_destroy ();
1295     search_destroy ();
1296     gtk_main_quit ();
1297     trace ("gtkui_stop completed\n");
1298     return FALSE;
1299 }
1300 
1301 static int
gtkui_stop(void)1302 gtkui_stop (void) {
1303     trace ("quitting gtk\n");
1304     cover_art_disconnect();
1305     g_idle_add (quit_gtk_cb, NULL);
1306     return 0;
1307 }
1308 
1309 GtkWidget *
gtkui_get_mainwin(void)1310 gtkui_get_mainwin (void) {
1311     return mainwin;
1312 }
1313 
1314 static DB_plugin_action_t action_deselect_all = {
1315     .title = "Edit/Deselect All",
1316     .name = "deselect_all",
1317     .flags = DB_ACTION_COMMON,
1318     .callback2 = action_deselect_all_handler,
1319     .next = NULL
1320 };
1321 
1322 static DB_plugin_action_t action_select_all = {
1323     .title = "Edit/Select All",
1324     .name = "select_all",
1325     .flags = DB_ACTION_COMMON,
1326     .callback2 = action_select_all_handler,
1327     .next = &action_deselect_all
1328 };
1329 
1330 static DB_plugin_action_t action_quit = {
1331     .title = "Quit",
1332     .name = "quit",
1333     .flags = DB_ACTION_COMMON,
1334     .callback2 = action_quit_handler,
1335     .next = &action_select_all
1336 };
1337 
1338 static DB_plugin_action_t action_delete_from_disk = {
1339     .title = "Remove From Disk",
1340     .name = "delete_from_disk",
1341     .flags = DB_ACTION_MULTIPLE_TRACKS,
1342     .callback2 = action_delete_from_disk_handler,
1343     .next = &action_quit
1344 };
1345 
1346 static DB_plugin_action_t action_add_location = {
1347     .title = "File/Add Location",
1348     .name = "add_location",
1349     .flags = DB_ACTION_COMMON,
1350     .callback2 = action_add_location_handler,
1351     .next = &action_delete_from_disk
1352 };
1353 
1354 static DB_plugin_action_t action_add_folders = {
1355     .title = "File/Add Folder(s)",
1356     .name = "add_folders",
1357     .flags = DB_ACTION_COMMON,
1358     .callback2 = action_add_folders_handler,
1359     .next = &action_add_location
1360 };
1361 
1362 static DB_plugin_action_t action_add_files = {
1363     .title = "File/Add File(s)",
1364     .name = "add_files",
1365     .flags = DB_ACTION_COMMON,
1366     .callback2 = action_add_files_handler,
1367     .next = &action_add_folders
1368 };
1369 
1370 static DB_plugin_action_t action_open_files = {
1371     .title = "File/Open File(s)",
1372     .name = "open_files",
1373     .flags = DB_ACTION_COMMON,
1374     .callback2 = action_open_files_handler,
1375     .next = &action_add_files
1376 };
1377 
1378 
1379 static DB_plugin_action_t action_track_properties = {
1380     .title = "Track Properties",
1381     .name = "track_properties",
1382     .flags = DB_ACTION_MULTIPLE_TRACKS,
1383     .callback2 = action_show_track_properties_handler,
1384     .next = &action_open_files
1385 };
1386 
1387 static DB_plugin_action_t action_show_help = {
1388     .title = "Help/Show Help Page",
1389     .name = "help",
1390     .flags = DB_ACTION_COMMON,
1391     .callback2 = action_show_help_handler,
1392     .next = &action_track_properties
1393 };
1394 
1395 static DB_plugin_action_t action_playback_loop_cycle = {
1396     .title = "Playback/Cycle Playback Looping Mode",
1397     .name = "loop_cycle",
1398     .flags = DB_ACTION_COMMON,
1399     .callback2 = action_playback_loop_cycle_handler,
1400     .next = &action_show_help
1401 };
1402 
1403 static DB_plugin_action_t action_playback_loop_off = {
1404     .title = "Playback/Playback Looping - Don't loop",
1405     .name = "loop_off",
1406     .flags = DB_ACTION_COMMON,
1407     .callback2 = action_playback_loop_off_handler,
1408     .next = &action_playback_loop_cycle
1409 };
1410 
1411 static DB_plugin_action_t action_playback_loop_single = {
1412     .title = "Playback/Playback Looping - Single track",
1413     .name = "loop_track",
1414     .flags = DB_ACTION_COMMON,
1415     .callback2 = action_playback_loop_single_handler,
1416     .next = &action_playback_loop_off
1417 };
1418 
1419 static DB_plugin_action_t action_playback_loop_all = {
1420     .title = "Playback/Playback Looping - All",
1421     .name = "loop_all",
1422     .flags = DB_ACTION_COMMON,
1423     .callback2 = action_playback_loop_all_handler,
1424     .next = &action_playback_loop_single
1425 };
1426 
1427 static DB_plugin_action_t action_playback_order_cycle = {
1428     .title = "Playback/Cycle Playback Order",
1429     .name = "order_cycle",
1430     .flags = DB_ACTION_COMMON,
1431     .callback2 = action_playback_order_cycle_handler,
1432     .next = &action_playback_loop_all
1433 };
1434 
1435 static DB_plugin_action_t action_playback_order_random = {
1436     .title = "Playback/Playback Order - Random",
1437     .name = "order_random",
1438     .flags = DB_ACTION_COMMON,
1439     .callback2 = action_playback_order_random_handler,
1440     .next = &action_playback_order_cycle
1441 };
1442 
1443 static DB_plugin_action_t action_playback_order_shuffle_albums = {
1444     .title = "Playback/Playback Order - Shuffle albums",
1445     .name = "order_shuffle_albums",
1446     .flags = DB_ACTION_COMMON,
1447     .callback2 = action_playback_order_shuffle_albums_handler,
1448     .next = &action_playback_order_random
1449 };
1450 
1451 static DB_plugin_action_t action_playback_order_shuffle = {
1452     .title = "Playback/Playback Order - Shuffle tracks",
1453     .name = "order_shuffle",
1454     .flags = DB_ACTION_COMMON,
1455     .callback2 = action_playback_order_shuffle_handler,
1456     .next = &action_playback_order_shuffle_albums
1457 };
1458 
1459 static DB_plugin_action_t action_playback_order_linear = {
1460     .title = "Playback/Playback Order - Linear",
1461     .name = "order_linear",
1462     .flags = DB_ACTION_COMMON,
1463     .callback2 = action_playback_order_linear_handler,
1464     .next = &action_playback_order_shuffle
1465 };
1466 
1467 
1468 static DB_plugin_action_t action_cursor_follows_playback = {
1469     .title = "Playback/Toggle Cursor Follows Playback",
1470     .name = "toggle_cursor_follows_playback",
1471     .flags = DB_ACTION_COMMON,
1472     .callback2 = action_cursor_follows_playback_handler,
1473     .next = &action_playback_order_linear
1474 };
1475 
1476 
1477 static DB_plugin_action_t action_scroll_follows_playback = {
1478     .title = "Playback/Toggle Scroll Follows Playback",
1479     .name = "toggle_scroll_follows_playback",
1480     .flags = DB_ACTION_COMMON,
1481     .callback2 = action_scroll_follows_playback_handler,
1482     .next = &action_cursor_follows_playback
1483 };
1484 
1485 static DB_plugin_action_t action_toggle_menu = {
1486     .title = "View/Show\\/Hide menu",
1487     .name = "toggle_menu",
1488     .flags = DB_ACTION_COMMON,
1489     .callback2 = action_toggle_menu_handler,
1490     .next = &action_scroll_follows_playback
1491 };
1492 
1493 static DB_plugin_action_t action_toggle_statusbar = {
1494     .title = "View/Show\\/Hide statusbar",
1495     .name = "toggle_statusbar",
1496     .flags = DB_ACTION_COMMON,
1497     .callback2 = action_toggle_statusbar_handler,
1498     .next = &action_toggle_menu
1499 };
1500 
1501 static DB_plugin_action_t action_toggle_designmode = {
1502     .title = "Edit/Toggle Design Mode",
1503     .name = "toggle_design_mode",
1504     .flags = DB_ACTION_COMMON,
1505     .callback2 = action_toggle_designmode_handler,
1506     .next = &action_toggle_statusbar
1507 };
1508 
1509 static DB_plugin_action_t action_preferences = {
1510     .title = "Edit/Preferences",
1511     .name = "preferences",
1512     .flags = DB_ACTION_COMMON,
1513     .callback2 = action_preferences_handler,
1514     .next = &action_toggle_designmode
1515 };
1516 
1517 static DB_plugin_action_t action_sort_custom = {
1518     .title = "Edit/Sort Custom",
1519     .name = "sort_custom",
1520     .flags = DB_ACTION_COMMON,
1521     .callback2 = action_sort_custom_handler,
1522     .next = &action_preferences
1523 };
1524 
1525 static DB_plugin_action_t action_crop_selected = {
1526     .title = "Edit/Crop Selected",
1527     .name = "crop_selected",
1528     .flags = DB_ACTION_COMMON,
1529     .callback2 = action_crop_selected_handler,
1530     .next = &action_sort_custom
1531 };
1532 
1533 static DB_plugin_action_t action_remove_from_playlist = {
1534     .title = "Edit/Remove Track(s) From Playlist",
1535     .name = "remove_from_playlist",
1536     .flags = DB_ACTION_MULTIPLE_TRACKS,
1537     .callback2 = action_remove_from_playlist_handler,
1538     .next = &action_crop_selected
1539 };
1540 
1541 static DB_plugin_action_t action_save_playlist = {
1542     .title = "File/Save Playlist",
1543     .name = "save_playlist",
1544     .flags = DB_ACTION_COMMON,
1545     .callback2 = action_save_playlist_handler,
1546     .next = &action_remove_from_playlist
1547 };
1548 
1549 static DB_plugin_action_t action_load_playlist = {
1550     .title = "File/Load Playlist",
1551     .name = "load_playlist",
1552     .flags = DB_ACTION_COMMON,
1553     .callback2 = action_load_playlist_handler,
1554     .next = &action_save_playlist
1555 };
1556 
1557 static DB_plugin_action_t action_remove_current_playlist = {
1558     .title = "File/Remove Current Playlist",
1559     .name = "remove_current_playlist",
1560     .flags = DB_ACTION_COMMON,
1561     .callback2 = action_remove_current_playlist_handler,
1562     .next = &action_load_playlist
1563 };
1564 
1565 
1566 static DB_plugin_action_t action_new_playlist = {
1567     .title = "File/New Playlist",
1568     .name = "new_playlist",
1569     .flags = DB_ACTION_COMMON,
1570     .callback2 = action_new_playlist_handler,
1571     .next = &action_remove_current_playlist
1572 };
1573 
1574 static DB_plugin_action_t action_toggle_eq = {
1575     .title = "View/Show\\/Hide Equalizer",
1576     .name = "toggle_eq",
1577     .flags = DB_ACTION_COMMON,
1578     .callback2 = action_toggle_eq_handler,
1579     .next = &action_new_playlist
1580 };
1581 
1582 static DB_plugin_action_t action_hide_eq = {
1583     .title = "View/Hide Equalizer",
1584     .name = "hide_eq",
1585     .flags = DB_ACTION_COMMON,
1586     .callback2 = action_hide_eq_handler,
1587     .next = &action_toggle_eq
1588 };
1589 
1590 static DB_plugin_action_t action_show_eq = {
1591     .title = "View/Show Equalizer",
1592     .name = "show_eq",
1593     .flags = DB_ACTION_COMMON,
1594     .callback2 = action_show_eq_handler,
1595     .next = &action_hide_eq
1596 };
1597 
1598 static DB_plugin_action_t action_toggle_mainwin = {
1599     .title = "View/Show\\/Hide Player Window",
1600     .name = "toggle_player_window",
1601     .flags = DB_ACTION_COMMON,
1602     .callback2 = action_toggle_mainwin_handler,
1603     .next = &action_show_eq
1604 };
1605 
1606 static DB_plugin_action_t action_hide_mainwin = {
1607     .title = "View/Hide Player Window",
1608     .name = "hide_player_window",
1609     .flags = DB_ACTION_COMMON,
1610     .callback2 = action_hide_mainwin_handler,
1611     .next = &action_toggle_mainwin
1612 };
1613 
1614 static DB_plugin_action_t action_show_mainwin = {
1615     .title = "View/Show Player Window",
1616     .name = "show_player_window",
1617     .flags = DB_ACTION_COMMON,
1618     .callback2 = action_show_mainwin_handler,
1619     .next = &action_hide_mainwin
1620 };
1621 
1622 static DB_plugin_action_t action_find = {
1623     .title = "Edit/Find",
1624     .name = "find",
1625     .flags = DB_ACTION_COMMON,
1626     .callback2 = action_find_handler,
1627     .next = &action_show_mainwin
1628 };
1629 
1630 static DB_plugin_action_t *
gtkui_get_actions(DB_playItem_t * it)1631 gtkui_get_actions (DB_playItem_t *it)
1632 {
1633     return &action_find;
1634 }
1635 
1636 #if !GTK_CHECK_VERSION(3,0,0)
1637 DB_plugin_t *
ddb_gui_GTK2_load(DB_functions_t * api)1638 ddb_gui_GTK2_load (DB_functions_t *api) {
1639     deadbeef = api;
1640     return DB_PLUGIN (&plugin);
1641 }
1642 #else
1643 DB_plugin_t *
ddb_gui_GTK3_load(DB_functions_t * api)1644 ddb_gui_GTK3_load (DB_functions_t *api) {
1645     deadbeef = api;
1646     return DB_PLUGIN (&plugin);
1647 }
1648 #endif
1649 
1650 static const char settings_dlg[] =
1651     "property \"Ask confirmation to delete files from disk\" checkbox gtkui.delete_files_ask 1;\n"
1652     "property \"Status icon volume control sensitivity\" entry gtkui.tray_volume_sensitivity 1;\n"
1653     "property \"Custom status icon\" entry gtkui.custom_tray_icon \"" TRAY_ICON "\" ;\n"
1654     "property \"Run gtk_init with --sync (debug mode)\" checkbox gtkui.sync 0;\n"
1655     "property \"Add separators between plugin context menu items\" checkbox gtkui.action_separators 0;\n"
1656     "property \"Use unicode chars instead of images for track state\" checkbox gtkui.unicode_playstate 0;\n"
1657     "property \"Disable seekbar overlay text\" checkbox gtkui.disable_seekbar_overlay 0;\n"
1658 ;
1659 
1660 // define plugin interface
1661 static ddb_gtkui_t plugin = {
1662     .gui.plugin.api_vmajor = 1,
1663     .gui.plugin.api_vminor = 6,
1664     .gui.plugin.version_major = DDB_GTKUI_API_VERSION_MAJOR,
1665     .gui.plugin.version_minor = DDB_GTKUI_API_VERSION_MINOR,
1666     .gui.plugin.type = DB_PLUGIN_GUI,
1667     .gui.plugin.id = DDB_GTKUI_PLUGIN_ID,
1668 #if GTK_CHECK_VERSION(3,0,0)
1669     .gui.plugin.name = "GTK3 user interface",
1670     .gui.plugin.descr = "User interface using GTK+ 3.x",
1671 #else
1672     .gui.plugin.name = "GTK2 user interface",
1673     .gui.plugin.descr = "User interface using GTK+ 2.x",
1674 #endif
1675     .gui.plugin.copyright =
1676         "GTK+ user interface for DeaDBeeF Player.\n"
1677         "Copyright (C) 2009-2015 Alexey Yakovenko and other contributors\n"
1678         "\n"
1679         "This software is provided 'as-is', without any express or implied\n"
1680         "warranty.  In no event will the authors be held liable for any damages\n"
1681         "arising from the use of this software.\n"
1682         "\n"
1683         "Permission is granted to anyone to use this software for any purpose,\n"
1684         "including commercial applications, and to alter it and redistribute it\n"
1685         "freely, subject to the following restrictions:\n"
1686         "\n"
1687         "1. The origin of this software must not be misrepresented; you must not\n"
1688         " claim that you wrote the original software. If you use this software\n"
1689         " in a product, an acknowledgment in the product documentation would be\n"
1690         " appreciated but is not required.\n"
1691         "\n"
1692         "2. Altered source versions must be plainly marked as such, and must not be\n"
1693         " misrepresented as being the original software.\n"
1694         "\n"
1695         "3. This notice may not be removed or altered from any source distribution.\n"
1696         "\n"
1697         "\n"
1698         "GTK - The GIMP Toolkit\n"
1699         "Copyright (C) GTK Developers\n"
1700         "\n"
1701         "This library is free software; you can redistribute it and/or\n"
1702         "modify it under the terms of the GNU Lesser General Public\n"
1703         "License as published by the Free Software Foundation; either\n"
1704         "version 2 of the License, or (at your option) any later version.\n"
1705         "\n"
1706         "This library is distributed in the hope that it will be useful,\n"
1707         "but WITHOUT ANY WARRANTY; without even the implied warranty of\n"
1708         "MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n"
1709         "Lesser General Public License for more details.\n"
1710         "\n"
1711         "You should have received a copy of the GNU Lesser General Public\n"
1712         "License along with this library. If not, see <http://www.gnu.org/licenses/>.\n"
1713     ,
1714     .gui.plugin.website = "http://deadbeef.sf.net",
1715     .gui.plugin.start = gtkui_start,
1716     .gui.plugin.stop = gtkui_stop,
1717     .gui.plugin.connect = gtkui_connect,
1718     .gui.plugin.disconnect = gtkui_disconnect,
1719     .gui.plugin.configdialog = settings_dlg,
1720     .gui.plugin.message = gtkui_message,
1721     .gui.run_dialog = gtkui_run_dialog_root,
1722     .gui.plugin.get_actions = gtkui_get_actions,
1723     .get_mainwin = gtkui_get_mainwin,
1724     .w_reg_widget = w_reg_widget,
1725     .w_unreg_widget = w_unreg_widget,
1726     .w_override_signals = w_override_signals,
1727     .w_is_registered = w_is_registered,
1728     .w_get_rootwidget = w_get_rootwidget,
1729     .w_set_design_mode = w_set_design_mode,
1730     .w_get_design_mode = w_get_design_mode,
1731     .w_create = w_create,
1732     .w_destroy = w_destroy,
1733     .w_append = w_append,
1734     .w_replace = w_replace,
1735     .w_remove = w_remove,
1736     .create_pltmenu = gtkui_create_pltmenu,
1737     .get_cover_art_pixbuf = get_cover_art_callb, // deprecated
1738     .get_cover_art_primary = get_cover_art_primary,
1739     .get_cover_art_thumb = get_cover_art_thumb,
1740     .cover_get_default_pixbuf = cover_get_default_pixbuf,
1741     .add_window_init_hook = add_window_init_hook,
1742 };
1743