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