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 #ifdef HAVE_CONFIG_H
25 #  include <config.h>
26 #endif
27 #include <gtk/gtk.h>
28 #include <gdk/gdkkeysyms.h>
29 #include <string.h>
30 #include <math.h>
31 #include <assert.h>
32 #include "../../gettext.h"
33 #include "ddblistview.h"
34 #include "trkproperties.h"
35 #include "interface.h"
36 #include "support.h"
37 #include "../../deadbeef.h"
38 #include "gtkui.h"
39 #include "mainplaylist.h"
40 #include "search.h"
41 #include "ddbcellrenderertextmultiline.h"
42 #include "tagwritersettings.h"
43 #include "wingeom.h"
44 
45 //#define trace(...) { fprintf(stderr, __VA_ARGS__); }
46 #define trace(fmt,...)
47 
48 #define min(x,y) ((x)<(y)?(x):(y))
49 
50 // some versions of cairo crash when fields are too long
51 // this is the workaround
52 // the fields are limited to be no more than 1000 bytes
53 // if they are larger - they will be treated as "multiple values".
54 #define MAX_GUI_FIELD_LEN 5000
55 
56 static GtkWidget *trackproperties;
57 static GtkCellRenderer *rend_text2;
58 static GtkListStore *store;
59 static GtkListStore *propstore;
60 int trkproperties_modified;
61 static DB_playItem_t **tracks;
62 static int numtracks;
63 static GtkWidget *progressdlg;
64 static int progress_aborted;
65 static int last_ctx;
66 static ddb_playlist_t *last_plt;
67 
68 int
build_key_list(const char *** pkeys,int props,DB_playItem_t ** tracks,int numtracks)69 build_key_list (const char ***pkeys, int props, DB_playItem_t **tracks, int numtracks) {
70     int sz = 20;
71     const char **keys = malloc (sizeof (const char *) * sz);
72     if (!keys) {
73         fprintf (stderr, "fatal: out of memory allocating key list\n");
74         assert (0);
75         return 0;
76     }
77 
78     int n = 0;
79 
80     for (int i = 0; i < numtracks; i++) {
81         DB_metaInfo_t *meta = deadbeef->pl_get_metadata_head (tracks[i]);
82         while (meta) {
83             if (meta->key[0] != '!' && ((props && meta->key[0] == ':') || (!props && meta->key[0] != ':'))) {
84                 int k = 0;
85                 for (; k < n; k++) {
86                     if (meta->key == keys[k]) {
87                         break;
88                     }
89                 }
90                 if (k == n) {
91                     if (n >= sz) {
92                         sz *= 2;
93                         keys = realloc (keys, sizeof (const char *) * sz);
94                         if (!keys) {
95                             fprintf (stderr, "fatal: out of memory reallocating key list (%d keys)\n", sz);
96                             assert (0);
97                         }
98                     }
99                     keys[n++] = meta->key;
100                 }
101             }
102             meta = meta->next;
103         }
104     }
105 
106     *pkeys = keys;
107     return n;
108 }
109 
110 static int
equals_ptr(const char * a,const char * b)111 equals_ptr (const char *a, const char *b) {
112     return a == b;
113 }
114 
115 static int
get_field_value(char * out,int size,const char * key,const char * (* getter)(DB_playItem_t * it,const char * key),int (* equals)(const char * a,const char * b),DB_playItem_t ** tracks,int numtracks)116 get_field_value (char *out, int size, const char *key, const char *(*getter)(DB_playItem_t *it, const char *key), int (*equals)(const char *a, const char *b), DB_playItem_t **tracks, int numtracks) {
117     int multiple = 0;
118     *out = 0;
119     if (numtracks == 0) {
120         return 0;
121     }
122     char *p = out;
123     deadbeef->pl_lock ();
124     const char **prev = malloc (sizeof (const char *) * numtracks);
125     memset (prev, 0, sizeof (const char *) * numtracks);
126     for (int i = 0; i < numtracks; i++) {
127         const char *val = getter (tracks[i], key);
128         if (val && val[0] == 0) {
129             val = NULL;
130         }
131         if (i > 0 || (val && strlen (val) >= MAX_GUI_FIELD_LEN)) {
132             int n = 0;
133             for (; n < i; n++) {
134                 if (equals (prev[n], val)) {
135                     break;
136                 }
137             }
138             if (n == i || (val && strlen (val) >= MAX_GUI_FIELD_LEN)) {
139                 multiple = 1;
140                 if (val) {
141                     size_t l = snprintf (out, size, out == p ? "%s" : "; %s", val ? val : "");
142                     l = min (l, size);
143                     out += l;
144                     size -= l;
145                 }
146             }
147         }
148         else if (val) {
149             size_t l = snprintf (out, size, "%s", val ? val : "");
150             l = min (l, size);
151             out += l;
152             size -= l;
153         }
154         prev[i] = val;
155         if (size <= 1) {
156             break;
157         }
158     }
159     deadbeef->pl_unlock ();
160     if (size <= 1) {
161         gchar *prev = g_utf8_prev_char (out-4);
162         strcpy (prev, "...");
163     }
164     free (prev);
165     return multiple;
166 }
167 
168 gboolean
on_trackproperties_delete_event(GtkWidget * widget,GdkEvent * event,gpointer user_data)169 on_trackproperties_delete_event        (GtkWidget       *widget,
170                                         GdkEvent        *event,
171                                         gpointer         user_data)
172 {
173     if (trkproperties_modified) {
174         GtkWidget *dlg = gtk_message_dialog_new (GTK_WINDOW (trackproperties), GTK_DIALOG_MODAL, GTK_MESSAGE_WARNING, GTK_BUTTONS_YES_NO, _("You've modified data for this track."));
175         gtk_window_set_transient_for (GTK_WINDOW (dlg), GTK_WINDOW (trackproperties));
176         gtk_message_dialog_format_secondary_text (GTK_MESSAGE_DIALOG (dlg), _("Really close the window?"));
177         gtk_window_set_title (GTK_WINDOW (dlg), _("Warning"));
178 
179         int response = gtk_dialog_run (GTK_DIALOG (dlg));
180         gtk_widget_destroy (dlg);
181         if (response != GTK_RESPONSE_YES) {
182             return TRUE;
183         }
184     }
185     gtk_widget_destroy (widget);
186     rend_text2 = NULL;
187     trackproperties = NULL;
188     if (tracks) {
189         for (int i = 0; i < numtracks; i++) {
190             deadbeef->pl_item_unref (tracks[i]);
191         }
192         free (tracks);
193         tracks = NULL;
194         numtracks = 0;
195     }
196     return TRUE;
197 }
198 
199 void
200 on_remove_field_activate                 (GtkMenuItem     *menuitem,
201                                         gpointer         user_data);
202 
203 void
204 on_add_field_activate                 (GtkMenuItem     *menuitem,
205                                         gpointer         user_data);
206 
207 int trkproperties_block_keyhandler = 0;
208 
209 
210 gboolean
on_trackproperties_key_press_event(GtkWidget * widget,GdkEventKey * event,gpointer user_data)211 on_trackproperties_key_press_event     (GtkWidget       *widget,
212                                         GdkEventKey     *event,
213                                         gpointer         user_data)
214 {
215     if (trkproperties_block_keyhandler) {
216         return FALSE;
217     }
218     if (event->keyval == GDK_Escape) {
219         on_trackproperties_delete_event (trackproperties, NULL, NULL);
220         return TRUE;
221     }
222     else if (event->keyval == GDK_Delete) {
223         on_remove_field_activate (NULL, NULL);
224         return TRUE;
225     }
226     else if (event->keyval == GDK_Insert) {
227         on_add_field_activate (NULL, NULL);
228         return TRUE;
229     }
230     return FALSE;
231 }
232 
233 void
trkproperties_destroy(void)234 trkproperties_destroy (void) {
235     if (trackproperties) {
236         on_trackproperties_delete_event (trackproperties, NULL, NULL);
237     }
238     if (last_plt) {
239         deadbeef->plt_unref (last_plt);
240         last_plt = NULL;
241     }
242     last_ctx = -1;
243 }
244 
245 void
on_closebtn_clicked(GtkButton * button,gpointer user_data)246 on_closebtn_clicked                    (GtkButton       *button,
247                                         gpointer         user_data)
248 {
249     trkproperties_destroy ();
250 }
251 
252 void
on_metadata_edited(GtkCellRendererText * renderer,gchar * path,gchar * new_text,gpointer user_data)253 on_metadata_edited (GtkCellRendererText *renderer, gchar *path, gchar *new_text, gpointer user_data) {
254     GtkListStore *store = GTK_LIST_STORE (user_data);
255     GtkTreePath *treepath = gtk_tree_path_new_from_string (path);
256     GtkTreeIter iter;
257 
258     if (!treepath) {
259         return;
260     }
261 
262     gboolean valid = gtk_tree_model_get_iter (GTK_TREE_MODEL (store), &iter, treepath);
263     gtk_tree_path_free (treepath);
264 
265     if (!valid) {
266         return;
267     }
268 
269     GValue value = {0,};
270     GValue mult = {0,};
271     gtk_tree_model_get_value (GTK_TREE_MODEL (store), &iter, 1, &value);
272     gtk_tree_model_get_value (GTK_TREE_MODEL (store), &iter, 3, &mult);
273     const char *svalue = g_value_get_string (&value);
274     int imult = g_value_get_int (&mult);
275     if (strcmp (svalue, new_text) && (!imult || strlen (new_text))) {
276         gtk_list_store_set (store, &iter, 1, new_text, 3, 0, -1);
277         trkproperties_modified = 1;
278     }
279     trkproperties_block_keyhandler = 0;
280 }
281 
282 // full metadata
283 static const char *types[] = {
284     "artist", "Artist",
285     "title", "Track Title",
286     "album", "Album",
287     "year", "Date",
288     "track", "Track Number",
289     "numtracks", "Total Tracks",
290     "genre", "Genre",
291     "composer", "Composer",
292     "disc", "Disc Number",
293     "numdiscs", "Total Discs",
294     "comment", "Comment",
295     NULL
296 };
297 
298 static const char *hc_props[] = {
299     ":URI", "Location",
300     ":TRACKNUM", "Subtrack Index",
301     ":DURATION", "Duration",
302     ":TAGS", "Tag Type(s)",
303     ":HAS_EMBEDDED_CUESHEET", "Embedded Cuesheet",
304     ":DECODER", "Codec",
305     NULL
306 };
307 
308 void
add_field(GtkListStore * store,const char * key,const char * title,int is_prop,DB_playItem_t ** tracks,int numtracks)309 add_field (GtkListStore *store, const char *key, const char *title, int is_prop, DB_playItem_t **tracks, int numtracks) {
310     // get value to edit
311     const char *mult = is_prop ? "" : _("[Multiple values] ");
312     char val[MAX_GUI_FIELD_LEN];
313     size_t ml = strlen (mult);
314     memcpy (val, mult, ml+1);
315     int n = get_field_value (val + ml, sizeof (val) - ml, key, deadbeef->pl_find_meta_raw, equals_ptr, tracks, numtracks);
316 
317     GtkTreeIter iter;
318     gtk_list_store_append (store, &iter);
319     if (!is_prop) {
320         if (n) {
321             gtk_list_store_set (store, &iter, 0, title, 1, val, 2, key, 3, n ? 1 : 0, -1);
322         }
323         else {
324             deadbeef->pl_lock ();
325             const char *val = deadbeef->pl_find_meta_raw (tracks[0], key);
326             if (!val) {
327                 val = "";
328             }
329             gtk_list_store_set (store, &iter, 0, title, 1, val, 2, key, 3, n ? 1 : 0, -1);
330             deadbeef->pl_unlock ();
331         }
332     }
333     else {
334         gtk_list_store_set (store, &iter, 0, title, 1, n ? val : val + ml, -1);
335     }
336 }
337 
338 void
trkproperties_fill_meta(GtkListStore * store,DB_playItem_t ** tracks,int numtracks)339 trkproperties_fill_meta (GtkListStore *store, DB_playItem_t **tracks, int numtracks) {
340     gtk_list_store_clear (store);
341     if (!tracks) {
342         return;
343     }
344 
345     const char **keys = NULL;
346     int nkeys = build_key_list (&keys, 0, tracks, numtracks);
347 
348     int k;
349 
350     // add "standard" fields
351     for (int i = 0; types[i]; i += 2) {
352         add_field (store, types[i], _(types[i+1]), 0, tracks, numtracks);
353     }
354 
355     // add all other fields
356     for (int k = 0; k < nkeys; k++) {
357         int i;
358         for (i = 0; types[i]; i += 2) {
359             if (!strcasecmp (keys[k], types[i])) {
360                 break;
361             }
362         }
363         if (types[i]) {
364             continue;
365         }
366 
367         char title[MAX_GUI_FIELD_LEN];
368         if (!types[i]) {
369             snprintf (title, sizeof (title), "<%s>", keys[k]);
370         }
371         add_field (store, keys[k], title, 0, tracks, numtracks);
372     }
373     if (keys) {
374         free (keys);
375     }
376 }
377 
378 void
trkproperties_fill_metadata(void)379 trkproperties_fill_metadata (void) {
380     if (!trackproperties) {
381         return;
382     }
383     trkproperties_modified = 0;
384     deadbeef->pl_lock ();
385 
386     trkproperties_fill_meta (store, tracks, numtracks);
387     gtk_list_store_clear (propstore);
388 
389     // hardcoded properties
390     for (int i = 0; hc_props[i]; i += 2) {
391         add_field (propstore, hc_props[i], _(hc_props[i+1]), 1, tracks, numtracks);
392     }
393     // properties
394     const char **keys = NULL;
395     int nkeys = build_key_list (&keys, 1, tracks, numtracks);
396     for (int k = 0; k < nkeys; k++) {
397         int i;
398         for (i = 0; hc_props[i]; i += 2) {
399             if (!strcasecmp (keys[k], hc_props[i])) {
400                 break;
401             }
402         }
403         if (hc_props[i]) {
404             continue;
405         }
406         char title[MAX_GUI_FIELD_LEN];
407         snprintf (title, sizeof (title), "<%s>", keys[k]+1);
408         add_field (propstore, keys[k], title, 1, tracks, numtracks);
409     }
410     if (keys) {
411         free (keys);
412     }
413 
414     deadbeef->pl_unlock ();
415 }
416 
417 void
show_track_properties_dlg(int ctx,ddb_playlist_t * plt)418 show_track_properties_dlg (int ctx, ddb_playlist_t *plt) {
419     last_ctx = ctx;
420     deadbeef->plt_ref (plt);
421     if (last_plt) {
422         deadbeef->plt_unref (last_plt);
423     }
424     last_plt = plt;
425 
426     if (tracks) {
427         for (int i = 0; i < numtracks; i++) {
428             deadbeef->pl_item_unref (tracks[i]);
429         }
430         free (tracks);
431         tracks = NULL;
432         numtracks = 0;
433     }
434 
435     deadbeef->pl_lock ();
436     int num = 0;
437     if (ctx == DDB_ACTION_CTX_SELECTION) {
438         num = deadbeef->plt_getselcount (plt);
439     }
440     else if (ctx == DDB_ACTION_CTX_PLAYLIST) {
441         num = deadbeef->plt_get_item_count (plt, PL_MAIN);
442     }
443     else if (ctx == DDB_ACTION_CTX_NOWPLAYING) {
444         num = 1;
445     }
446     if (num <= 0) {
447         deadbeef->pl_unlock ();
448         return;
449     }
450 
451     tracks = malloc (sizeof (DB_playItem_t *) * num);
452     if (!tracks) {
453         fprintf (stderr, "gtkui: failed to alloc %d bytes to store selected tracks\n", (int)(num * sizeof (void *)));
454         deadbeef->pl_unlock ();
455         return;
456     }
457 
458     if (ctx == DDB_ACTION_CTX_NOWPLAYING) {
459         DB_playItem_t *it = deadbeef->streamer_get_playing_track ();
460         if (!it) {
461             free (tracks);
462             tracks = NULL;
463             deadbeef->pl_unlock ();
464             return;
465         }
466         tracks[0] = it;
467     }
468     else {
469         int n = 0;
470         DB_playItem_t *it = deadbeef->plt_get_first (plt, PL_MAIN);
471         while (it) {
472             if (ctx == DDB_ACTION_CTX_PLAYLIST || deadbeef->pl_is_selected (it)) {
473                 assert (n < num);
474                 deadbeef->pl_item_ref (it);
475                 tracks[n++] = it;
476             }
477             DB_playItem_t *next = deadbeef->pl_get_next (it, PL_MAIN);
478             deadbeef->pl_item_unref (it);
479             it = next;
480         }
481     }
482     numtracks = num;
483 
484     deadbeef->pl_unlock ();
485 
486     GtkTreeView *tree;
487     GtkTreeView *proptree;
488     if (!trackproperties) {
489         trackproperties = create_trackproperties ();
490         gtk_window_set_transient_for (GTK_WINDOW (trackproperties), GTK_WINDOW (mainwin));
491         wingeom_restore (trackproperties, "trkproperties", -1, -1, 300, 400, 0);
492 
493         // metadata tree
494         tree = GTK_TREE_VIEW (lookup_widget (trackproperties, "metalist"));
495         store = gtk_list_store_new (4, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_INT);
496         gtk_tree_view_set_model (tree, GTK_TREE_MODEL (store));
497         GtkCellRenderer *rend_text = gtk_cell_renderer_text_new ();
498         rend_text2 = GTK_CELL_RENDERER (ddb_cell_renderer_text_multiline_new ());
499         g_signal_connect ((gpointer)rend_text2, "edited",
500                 G_CALLBACK (on_metadata_edited),
501                 store);
502         GtkTreeViewColumn *col1 = gtk_tree_view_column_new_with_attributes (_("Key"), rend_text, "text", 0, NULL);
503         GtkTreeViewColumn *col2 = gtk_tree_view_column_new_with_attributes (_("Value"), rend_text2, "text", 1, NULL);
504         gtk_tree_view_append_column (tree, col1);
505         gtk_tree_view_append_column (tree, col2);
506 
507         // properties tree
508         proptree = GTK_TREE_VIEW (lookup_widget (trackproperties, "properties"));
509         propstore = gtk_list_store_new (2, G_TYPE_STRING, G_TYPE_STRING);
510         gtk_tree_view_set_model (proptree, GTK_TREE_MODEL (propstore));
511         GtkCellRenderer *rend_propkey = gtk_cell_renderer_text_new ();
512         GtkCellRenderer *rend_propvalue = gtk_cell_renderer_text_new ();
513         g_object_set (G_OBJECT (rend_propvalue), "editable", TRUE, NULL);
514         col1 = gtk_tree_view_column_new_with_attributes (_("Key"), rend_propkey, "text", 0, NULL);
515         col2 = gtk_tree_view_column_new_with_attributes (_("Value"), rend_propvalue, "text", 1, NULL);
516         gtk_tree_view_append_column (proptree, col1);
517         gtk_tree_view_append_column (proptree, col2);
518     }
519     else {
520         tree = GTK_TREE_VIEW (lookup_widget (trackproperties, "metalist"));
521         store = GTK_LIST_STORE (gtk_tree_view_get_model (tree));
522         gtk_list_store_clear (store);
523         proptree = GTK_TREE_VIEW (lookup_widget (trackproperties, "properties"));
524         propstore = GTK_LIST_STORE (gtk_tree_view_get_model (proptree));
525         gtk_list_store_clear (propstore);
526     }
527 
528     if (numtracks == 1) {
529         deadbeef->pl_lock ();
530         gtk_entry_set_text (GTK_ENTRY (lookup_widget (trackproperties, "filename")), deadbeef->pl_find_meta_raw (tracks[0], ":URI"));
531         deadbeef->pl_unlock ();
532     }
533     else {
534         gtk_entry_set_text (GTK_ENTRY (lookup_widget (trackproperties, "filename")), _("[Multiple values]"));
535     }
536 
537     g_object_set (G_OBJECT (rend_text2), "editable", TRUE, NULL);
538 
539     GtkWidget *widget = trackproperties;
540     GtkWidget *w;
541     const char *meta;
542 
543     trkproperties_fill_metadata ();
544 
545     gtk_widget_set_sensitive (lookup_widget (widget, "write_tags"), TRUE);
546 
547     gtk_widget_show (widget);
548     gtk_window_present (GTK_WINDOW (widget));
549 }
550 
551 static gboolean
set_metadata_cb(GtkTreeModel * model,GtkTreePath * path,GtkTreeIter * iter,gpointer data)552 set_metadata_cb (GtkTreeModel *model, GtkTreePath *path, GtkTreeIter *iter, gpointer data) {
553     GValue mult = {0,};
554     gtk_tree_model_get_value (model, iter, 3, &mult);
555     int smult = g_value_get_int (&mult);
556     if (!smult) {
557         GValue key = {0,}, value = {0,};
558         gtk_tree_model_get_value (model, iter, 2, &key);
559         gtk_tree_model_get_value (model, iter, 1, &value);
560         const char *skey = g_value_get_string (&key);
561         const char *svalue = g_value_get_string (&value);
562 
563         for (int i = 0; i < numtracks; i++) {
564             const char *oldvalue= deadbeef->pl_find_meta_raw (tracks[i], skey);
565             if (oldvalue && strlen (oldvalue) > MAX_GUI_FIELD_LEN) {
566                 fprintf (stderr, "trkproperties: value is too long, ignored\n");
567                 continue;
568             }
569 
570             if (*svalue) {
571                 deadbeef->pl_replace_meta (tracks[i], skey, svalue);
572             }
573             else {
574                 deadbeef->pl_delete_meta (tracks[i], skey);
575             }
576         }
577     }
578 
579     return FALSE;
580 }
581 
582 static gboolean
write_finished_cb(void * ctx)583 write_finished_cb (void *ctx) {
584     gtk_widget_destroy (progressdlg);
585     progressdlg = NULL;
586     trkproperties_modified = 0;
587     if (last_plt) {
588         deadbeef->plt_modified (last_plt);
589         show_track_properties_dlg (last_ctx, last_plt);
590     }
591     main_refresh ();
592     search_refresh ();
593 
594     return FALSE;
595 }
596 
597 static gboolean
set_progress_cb(void * ctx)598 set_progress_cb (void *ctx) {
599     DB_playItem_t *track = ctx;
600     GtkWidget *progressitem = lookup_widget (progressdlg, "progresstitle");
601     deadbeef->pl_lock ();
602     gtk_entry_set_text (GTK_ENTRY (progressitem), deadbeef->pl_find_meta_raw (track, ":URI"));
603     deadbeef->pl_unlock ();
604     deadbeef->pl_item_unref (track);
605     return FALSE;
606 }
607 
608 static void
write_meta_worker(void * ctx)609 write_meta_worker (void *ctx) {
610     for (int t = 0; t < numtracks; t++) {
611         if (progress_aborted) {
612             break;
613         }
614         DB_playItem_t *track = tracks[t];
615         deadbeef->pl_lock ();
616         const char *dec = deadbeef->pl_find_meta_raw (track, ":DECODER");
617         char decoder_id[100];
618         if (dec) {
619             strncpy (decoder_id, dec, sizeof (decoder_id));
620         }
621         int match = track && dec;
622         deadbeef->pl_unlock ();
623         if (match) {
624             int is_subtrack = deadbeef->pl_get_item_flags (track) & DDB_IS_SUBTRACK;
625             if (is_subtrack) {
626                 continue;
627             }
628             deadbeef->pl_item_ref (track);
629             g_idle_add (set_progress_cb, track);
630             // find decoder
631             DB_decoder_t *dec = NULL;
632             DB_decoder_t **decoders = deadbeef->plug_get_decoder_list ();
633             for (int i = 0; decoders[i]; i++) {
634                 if (!strcmp (decoders[i]->plugin.id, decoder_id)) {
635                     dec = decoders[i];
636                     if (dec->write_metadata) {
637                         dec->write_metadata (track);
638                     }
639                     break;
640                 }
641             }
642         }
643     }
644     g_idle_add (write_finished_cb, ctx);
645 }
646 
647 static gboolean
on_progress_delete_event(GtkWidget * widget,GdkEvent * event,gpointer user_data)648 on_progress_delete_event            (GtkWidget       *widget,
649                                         GdkEvent        *event,
650                                         gpointer         user_data)
651 {
652     progress_aborted = 1;
653     return gtk_widget_hide_on_delete (widget);
654 }
655 
656 static void
on_progress_abort(GtkButton * button,gpointer user_data)657 on_progress_abort                      (GtkButton       *button,
658                                         gpointer         user_data)
659 {
660     progress_aborted = 1;
661 }
662 
663 void
on_write_tags_clicked(GtkButton * button,gpointer user_data)664 on_write_tags_clicked                  (GtkButton       *button,
665                                         gpointer         user_data)
666 {
667     deadbeef->pl_lock ();
668     GtkTreeView *tree = GTK_TREE_VIEW (lookup_widget (trackproperties, "metalist"));
669     GtkTreeModel *model = GTK_TREE_MODEL (gtk_tree_view_get_model (tree));
670 
671     // delete all metadata properties that are not in the listview
672     for (int i = 0; i < numtracks; i++) {
673         DB_metaInfo_t *meta = deadbeef->pl_get_metadata_head (tracks[i]);
674         while (meta) {
675             DB_metaInfo_t *next = meta->next;
676             if (meta->key[0] != ':' && meta->key[0] != '!' && meta->key[0] != '_') {
677                 GtkTreeIter iter;
678                 gboolean res = gtk_tree_model_get_iter_first (model, &iter);
679                 while (res) {
680                     GValue key = {0,};
681                     gtk_tree_model_get_value (model, &iter, 2, &key);
682                     const char *skey = g_value_get_string (&key);
683 
684                     if (!strcasecmp (skey, meta->key)) {
685                         // field found, don't delete
686                         break;
687                     }
688                     res = gtk_tree_model_iter_next (GTK_TREE_MODEL (store), &iter);
689                 }
690                 if (!res) {
691                     // field not found, delete
692                     deadbeef->pl_delete_metadata (tracks[i], meta);
693                 }
694             }
695             meta = next;
696         }
697     }
698     // put all metainfo into track
699     gtk_tree_model_foreach (model, set_metadata_cb, NULL);
700     deadbeef->pl_unlock ();
701 
702     for (int i = 0; i < numtracks; i++) {
703         ddb_event_track_t *ev = (ddb_event_track_t *)deadbeef->event_alloc (DB_EV_TRACKINFOCHANGED);
704         ev->track = tracks[i];
705         deadbeef->pl_item_ref (ev->track);
706         deadbeef->event_send ((ddb_event_t*)ev, 0, 0);
707     }
708 
709     progress_aborted = 0;
710     progressdlg = create_progressdlg ();
711     gtk_window_set_title (GTK_WINDOW (progressdlg), _("Writing tags..."));
712 
713     g_signal_connect ((gpointer) progressdlg, "delete_event",
714             G_CALLBACK (on_progress_delete_event),
715             NULL);
716     GtkWidget *cancelbtn = lookup_widget (progressdlg, "cancelbtn");
717     g_signal_connect ((gpointer) cancelbtn, "clicked",
718             G_CALLBACK (on_progress_abort),
719             NULL);
720 
721     gtk_widget_show_all (progressdlg);
722     gtk_window_present (GTK_WINDOW (progressdlg));
723     gtk_window_set_transient_for (GTK_WINDOW (progressdlg), GTK_WINDOW (trackproperties));
724 
725     // start new thread for writing metadata
726     intptr_t tid = deadbeef->thread_start (write_meta_worker, NULL);
727     deadbeef->thread_detach (tid);
728 }
729 
730 void
on_add_field_activate(GtkMenuItem * menuitem,gpointer user_data)731 on_add_field_activate                 (GtkMenuItem     *menuitem,
732                                         gpointer         user_data) {
733     GtkTreeView *treeview = GTK_TREE_VIEW (lookup_widget (trackproperties, "metalist"));
734     if (!gtk_widget_is_focus(GTK_WIDGET (treeview))) {
735         return; // do not add field if Metadata tab is not focused
736     }
737     GtkWidget *dlg = create_entrydialog ();
738     gtk_window_set_transient_for (GTK_WINDOW (dlg), GTK_WINDOW (trackproperties));
739     gtk_dialog_set_default_response (GTK_DIALOG (dlg), GTK_RESPONSE_OK);
740     gtk_window_set_title (GTK_WINDOW (dlg), _("Field name"));
741     GtkWidget *e;
742     e = lookup_widget (dlg, "title_label");
743     gtk_label_set_text (GTK_LABEL(e), _("Name:"));
744     for (;;) {
745         int res = gtk_dialog_run (GTK_DIALOG (dlg));
746         if (res == GTK_RESPONSE_OK) {
747             e = lookup_widget (dlg, "title");
748 
749             const char *text = gtk_entry_get_text (GTK_ENTRY(e));
750 
751             GtkTreeIter iter;
752 
753             // check for _ and :
754             if (text[0] == '_' || text[0] == ':' || text[0] == '!') {
755                 GtkWidget *d = gtk_message_dialog_new (GTK_WINDOW (dlg), GTK_DIALOG_MODAL, GTK_MESSAGE_ERROR, GTK_BUTTONS_OK, _("Field names must not start with : or _"));
756                 gtk_window_set_title (GTK_WINDOW (d), _("Cannot add field"));
757 
758                 gtk_dialog_run (GTK_DIALOG (d));
759                 gtk_widget_destroy (d);
760                 continue;
761             }
762 
763             // check if a field with the same name already exists
764             int dup = 0;
765             gboolean res = gtk_tree_model_get_iter_first (GTK_TREE_MODEL (store), &iter);
766             while (res) {
767                 GValue value = {0,};
768                 gtk_tree_model_get_value (GTK_TREE_MODEL (store), &iter, 2, &value);
769                 const char *svalue = g_value_get_string (&value);
770                 if (!strcasecmp (svalue, text)) {
771                     dup = 1;
772                     break;
773                 }
774                 res = gtk_tree_model_iter_next (GTK_TREE_MODEL (store), &iter);
775             }
776 
777             if (!dup) {
778                 int l = strlen (text);
779                 char title[l+3];
780                 snprintf (title, sizeof (title), "<%s>", text);
781                 const char *value = "";
782                 const char *key = text;
783 
784                 gtk_list_store_append (store, &iter);
785                 gtk_list_store_set (store, &iter, 0, title, 1, value, 2, key, -1);
786                 GtkTreePath *path;
787                 gint rows = gtk_tree_model_iter_n_children (GTK_TREE_MODEL (store), NULL);
788                 path = gtk_tree_path_new_from_indices (rows - 1, -1);
789                 gtk_tree_view_set_cursor (treeview, path, NULL, TRUE); // set cursor onto new field
790                 gtk_tree_path_free(path);
791                 trkproperties_modified = 1;
792             }
793             else {
794                 GtkWidget *d = gtk_message_dialog_new (GTK_WINDOW (dlg), GTK_DIALOG_MODAL, GTK_MESSAGE_ERROR, GTK_BUTTONS_OK, _("Field with such name already exists, please try different name."));
795                 gtk_window_set_title (GTK_WINDOW (d), _("Cannot add field"));
796 
797                 gtk_dialog_run (GTK_DIALOG (d));
798                 gtk_widget_destroy (d);
799                 continue;
800             }
801         }
802         break;
803     }
804     gtk_widget_destroy (dlg);
805     gtk_window_present (GTK_WINDOW (trackproperties));
806 }
807 
808 void
on_remove_field_activate(GtkMenuItem * menuitem,gpointer user_data)809 on_remove_field_activate                 (GtkMenuItem     *menuitem,
810                                         gpointer         user_data) {
811 
812     GtkTreeView *treeview = GTK_TREE_VIEW (lookup_widget (trackproperties, "metalist"));
813     if (!gtk_widget_is_focus(GTK_WIDGET (treeview))) {
814         return; // do not remove field if Metadata tab is not focused
815     }
816     GtkTreePath *path;
817     GtkTreeViewColumn *col;
818     gtk_tree_view_get_cursor (treeview, &path, &col);
819     if (!path || !col) {
820         return;
821     }
822 
823     GtkTreeIter iter;
824     gtk_tree_model_get_iter (GTK_TREE_MODEL (store), &iter, path);
825     GValue value = {0,};
826     gtk_tree_model_get_value (GTK_TREE_MODEL (store), &iter, 2, &value);
827     const char *svalue = g_value_get_string (&value);
828 
829     // delete unknown fields completely; otherwise just clear
830     int i = 0;
831     for (; types[i]; i += 2) {
832         if (!strcasecmp (svalue, types[i])) {
833             break;
834         }
835     }
836     if (types[i]) { // known val, clear
837         gtk_list_store_set (store, &iter, 1, "", 3, 0, -1);
838     }
839     else {
840         gtk_list_store_remove (store, &iter);
841     }
842     gtk_tree_view_set_cursor (treeview, path, NULL, FALSE); // restore cursor after deletion
843     gtk_tree_path_free (path);
844     trkproperties_modified = 1;
845 }
846 
847 gboolean
on_metalist_button_press_event(GtkWidget * widget,GdkEventButton * event,gpointer user_data)848 on_metalist_button_press_event         (GtkWidget       *widget,
849                                         GdkEventButton  *event,
850                                         gpointer         user_data)
851 {
852     if (event->button == 3) {
853         GtkWidget *menu;
854         GtkWidget *add;
855         GtkWidget *remove;
856         menu = gtk_menu_new ();
857         add = gtk_menu_item_new_with_mnemonic (_("Add field"));
858         gtk_widget_show (add);
859         gtk_container_add (GTK_CONTAINER (menu), add);
860         remove = gtk_menu_item_new_with_mnemonic (_("Remove field"));
861         gtk_widget_show (remove);
862         gtk_container_add (GTK_CONTAINER (menu), remove);
863 
864         g_signal_connect ((gpointer) add, "activate",
865                 G_CALLBACK (on_add_field_activate),
866                 NULL);
867 
868         g_signal_connect ((gpointer) remove, "activate",
869                 G_CALLBACK (on_remove_field_activate),
870                 NULL);
871 
872         gtk_menu_popup (GTK_MENU (menu), NULL, NULL, NULL, widget, event->button, gtk_get_current_event_time());
873     }
874   return FALSE;
875 }
876 
877 void
on_tagwriter_settings_clicked(GtkButton * button,gpointer user_data)878 on_tagwriter_settings_clicked          (GtkButton       *button,
879                                         gpointer         user_data)
880 {
881     run_tagwriter_settings (trackproperties);
882 }
883 
884 gboolean
on_trackproperties_configure_event(GtkWidget * widget,GdkEventConfigure * event,gpointer user_data)885 on_trackproperties_configure_event     (GtkWidget       *widget,
886                                         GdkEventConfigure *event,
887                                         gpointer         user_data)
888 {
889     wingeom_save (widget, "trkproperties");
890     return FALSE;
891 }
892 
893 
894 gboolean
on_trackproperties_window_state_event(GtkWidget * widget,GdkEventWindowState * event,gpointer user_data)895 on_trackproperties_window_state_event  (GtkWidget       *widget,
896                                         GdkEventWindowState *event,
897                                         gpointer         user_data)
898 {
899     wingeom_save_max (event, widget, "trkproperties");
900     return FALSE;
901 }
902 
903