1 /*
2  * infowin.c
3  * Copyright 2006-2013 Ariadne Conill, Tomasz Moń, Eugene Zagidullin,
4  *                     John Lindgren, and Thomas Lange
5  *
6  * Redistribution and use in source and binary forms, with or without
7  * modification, are permitted provided that the following conditions are met:
8  *
9  * 1. Redistributions of source code must retain the above copyright notice,
10  *    this list of conditions, and the following disclaimer.
11  *
12  * 2. Redistributions in binary form must reproduce the above copyright notice,
13  *    this list of conditions, and the following disclaimer in the documentation
14  *    provided with the distribution.
15  *
16  * This software is provided "as is" and without any warranty, express or
17  * implied. In no event shall the authors be liable for any damages arising from
18  * the use of this software.
19  */
20 
21 #include <gtk/gtk.h>
22 #include <stdarg.h>
23 #include <stdlib.h>
24 #include <string.h>
25 
26 #include <libaudcore/audstrings.h>
27 #include <libaudcore/hook.h>
28 #include <libaudcore/i18n.h>
29 #include <libaudcore/interface.h>
30 #include <libaudcore/mainloop.h>
31 #include <libaudcore/playlist.h>
32 #include <libaudcore/probe.h>
33 #include <libaudcore/runtime.h>
34 #include <libaudcore/tuple.h>
35 
36 #include "internal.h"
37 #include "libaudgui.h"
38 #include "libaudgui-gtk.h"
39 
40 #define AUDGUI_STATUS_TIMEOUT 3000
41 
42 enum {
43     CODEC_FORMAT,
44     CODEC_QUALITY,
45     CODEC_BITRATE,
46     CODEC_ITEMS
47 };
48 
49 static const char * codec_labels[CODEC_ITEMS] = {
50     N_("Format:"),
51     N_("Quality:"),
52     N_("Bitrate:")
53 };
54 
55 static struct {
56     GtkWidget * location;
57     GtkWidget * title;
58     GtkWidget * artist;
59     GtkWidget * album;
60     GtkWidget * album_artist;
61     GtkWidget * comment;
62     GtkWidget * year;
63     GtkWidget * track;
64     GtkWidget * genre;
65     GtkWidget * image;
66     GtkWidget * codec[3];
67     GtkWidget * apply;
68     GtkWidget * autofill;
69     GtkWidget * ministatus;
70 } widgets;
71 
72 static GtkWidget * infowin;
73 static Playlist current_playlist;
74 static int current_entry;
75 static String current_file;
76 static Tuple current_tuple;
77 static PluginHandle * current_decoder = nullptr;
78 static bool can_write = false;
79 static QueuedFunc ministatus_timer;
80 
81 /* This is by no means intended to be a complete list.  If it is not short, it
82  * is useless: scrolling through ten pages of dropdown list is more work than
83  * typing out the genre. */
84 
85 static const char * genre_table[] = {
86  N_("Acid Jazz"),
87  N_("Acid Rock"),
88  N_("Ambient"),
89  N_("Bebop"),
90  N_("Bluegrass"),
91  N_("Blues"),
92  N_("Chamber Music"),
93  N_("Classical"),
94  N_("Country"),
95  N_("Death Metal"),
96  N_("Disco"),
97  N_("Easy Listening"),
98  N_("Folk"),
99  N_("Funk"),
100  N_("Gangsta Rap"),
101  N_("Gospel"),
102  N_("Grunge"),
103  N_("Hard Rock"),
104  N_("Heavy Metal"),
105  N_("Hip-hop"),
106  N_("House"),
107  N_("Jazz"),
108  N_("Jungle"),
109  N_("Metal"),
110  N_("New Age"),
111  N_("New Wave"),
112  N_("Noise"),
113  N_("Pop"),
114  N_("Punk Rock"),
115  N_("Rap"),
116  N_("Reggae"),
117  N_("Rock"),
118  N_("Rock and Roll"),
119  N_("Rhythm and Blues"),
120  N_("Ska"),
121  N_("Soul"),
122  N_("Swing"),
123  N_("Techno"),
124  N_("Trip-hop")};
125 
small_label_new(const char * text)126 static GtkWidget * small_label_new (const char * text)
127 {
128     static PangoAttrList * attrs = nullptr;
129 
130     if (! attrs)
131     {
132         attrs = pango_attr_list_new ();
133         pango_attr_list_insert (attrs, pango_attr_scale_new (PANGO_SCALE_SMALL));
134     }
135 
136     GtkWidget * label = gtk_label_new (text);
137     gtk_label_set_attributes ((GtkLabel *) label, attrs);
138     gtk_misc_set_alignment ((GtkMisc *) label, 0, 0.5);
139 
140     return label;
141 }
142 
set_entry_str_from_field(GtkWidget * widget,const Tuple & tuple,Tuple::Field field,bool editable,bool clear,bool & changed)143 static void set_entry_str_from_field (GtkWidget * widget, const Tuple & tuple,
144  Tuple::Field field, bool editable, bool clear, bool & changed)
145 {
146     String text = tuple.get_str (field);
147 
148     if (! text && ! clear)
149     {
150         if (gtk_entry_get_text_length ((GtkEntry *) widget) > 0)
151             changed = true;
152         return;
153     }
154 
155     gtk_entry_set_text ((GtkEntry *) widget, text ? text : "");
156     gtk_editable_set_editable ((GtkEditable *) widget, editable);
157 }
158 
set_entry_int_from_field(GtkWidget * widget,const Tuple & tuple,Tuple::Field field,bool editable,bool clear,bool & changed)159 static void set_entry_int_from_field (GtkWidget * widget, const Tuple & tuple,
160  Tuple::Field field, bool editable, bool clear, bool & changed)
161 {
162     int value = tuple.get_int (field);
163 
164     if (value <= 0 && ! clear)
165     {
166         if (gtk_entry_get_text_length ((GtkEntry *) widget) > 0)
167             changed = true;
168         return;
169     }
170 
171     gtk_entry_set_text ((GtkEntry *) widget, (value > 0) ? (const char *) int_to_str (value) : "");
172     gtk_editable_set_editable ((GtkEditable *) widget, editable);
173 }
174 
set_field_str_from_entry(Tuple & tuple,Tuple::Field field,GtkWidget * widget)175 static void set_field_str_from_entry (Tuple & tuple, Tuple::Field field, GtkWidget * widget)
176 {
177     const char * text = gtk_entry_get_text ((GtkEntry *) widget);
178 
179     if (text[0])
180         tuple.set_str (field, text);
181     else
182         tuple.unset (field);
183 }
184 
set_field_int_from_entry(Tuple & tuple,Tuple::Field field,GtkWidget * widget)185 static void set_field_int_from_entry (Tuple & tuple, Tuple::Field field, GtkWidget * widget)
186 {
187     const char * text = gtk_entry_get_text ((GtkEntry *) widget);
188 
189     if (text[0])
190         tuple.set_int (field, atoi (text));
191     else
192         tuple.unset (field);
193 }
194 
entry_changed()195 static void entry_changed ()
196 {
197     if (can_write)
198         gtk_widget_set_sensitive (widgets.apply, true);
199 }
200 
ministatus_display_message(const char * text)201 static void ministatus_display_message (const char * text)
202 {
203     gtk_label_set_text ((GtkLabel *) widgets.ministatus, text);
204     gtk_widget_hide (widgets.autofill);
205     gtk_widget_show (widgets.ministatus);
206 
207     ministatus_timer.queue (AUDGUI_STATUS_TIMEOUT, [] () {
208         gtk_widget_hide (widgets.ministatus);
209         gtk_widget_show (widgets.autofill);
210     });
211 }
212 
infowin_update_tuple()213 static void infowin_update_tuple ()
214 {
215     set_field_str_from_entry (current_tuple, Tuple::Title, widgets.title);
216     set_field_str_from_entry (current_tuple, Tuple::Artist, widgets.artist);
217     set_field_str_from_entry (current_tuple, Tuple::Album, widgets.album);
218     set_field_str_from_entry (current_tuple, Tuple::AlbumArtist, widgets.album_artist);
219     set_field_str_from_entry (current_tuple, Tuple::Comment, widgets.comment);
220     set_field_str_from_entry (current_tuple, Tuple::Genre,
221      gtk_bin_get_child ((GtkBin *) widgets.genre));
222     set_field_int_from_entry (current_tuple, Tuple::Year, widgets.year);
223     set_field_int_from_entry (current_tuple, Tuple::Track, widgets.track);
224 
225     if (aud_file_write_tuple (current_file, current_decoder, current_tuple))
226     {
227         ministatus_display_message (_("Save successful"));
228         gtk_widget_set_sensitive (widgets.apply, false);
229     }
230     else
231         ministatus_display_message (_("Save error"));
232 }
233 
infowin_select_entry(int entry)234 static void infowin_select_entry (int entry)
235 {
236     if (entry >= 0 && entry < current_playlist.n_entries ())
237     {
238         current_playlist.select_all (false);
239         current_playlist.select_entry (entry, true);
240         current_playlist.set_focus (entry);
241         audgui_infowin_show (current_playlist, entry);
242     }
243     else
244         audgui_infowin_hide ();
245 }
246 
infowin_prev()247 static void infowin_prev ()
248 {
249     infowin_select_entry (current_entry - 1);
250 }
251 
infowin_next()252 static void infowin_next ()
253 {
254     infowin_select_entry (current_entry + 1);
255 }
256 
genre_fill(GtkWidget * combo)257 static void genre_fill (GtkWidget * combo)
258 {
259     GList * list = nullptr;
260     GList * node;
261 
262     for (const char * genre : genre_table)
263         list = g_list_prepend (list, _(genre));
264 
265     list = g_list_sort (list, (GCompareFunc) strcmp);
266 
267     for (node = list; node != nullptr; node = node->next)
268         gtk_combo_box_text_append_text ((GtkComboBoxText *) combo, (const char *) node->data);
269 
270     g_list_free (list);
271 }
272 
autofill_toggled(GtkToggleButton * toggle)273 static void autofill_toggled (GtkToggleButton * toggle)
274 {
275     aud_set_bool ("audgui", "clear_song_fields", ! gtk_toggle_button_get_active (toggle));
276 }
277 
infowin_display_image(const char * filename)278 static void infowin_display_image (const char * filename)
279 {
280     if (! current_file || strcmp (filename, current_file))
281         return;
282 
283     AudguiPixbuf pb = audgui_pixbuf_request (filename);
284     if (! pb)
285         pb = audgui_pixbuf_fallback ();
286 
287     if (pb)
288         audgui_scaled_image_set (widgets.image, pb.get ());
289 }
290 
infowin_destroyed()291 static void infowin_destroyed ()
292 {
293     hook_dissociate ("art ready", (HookFunction) infowin_display_image);
294 
295     ministatus_timer.stop ();
296 
297     memset (& widgets, 0, sizeof widgets);
298 
299     infowin = nullptr;
300     current_file = String ();
301     current_tuple = Tuple ();
302     current_decoder = nullptr;
303 }
304 
add_entry(GtkWidget * grid,const char * title,GtkWidget * entry,int x,int y,int span)305 static void add_entry (GtkWidget * grid, const char * title, GtkWidget * entry,
306  int x, int y, int span)
307 {
308     GtkWidget * label = small_label_new (title);
309 
310     gtk_table_attach ((GtkTable *) grid, label, x, x + span, y, y + 1,
311      GTK_FILL, GTK_FILL, 0, 0);
312     gtk_table_attach ((GtkTable *) grid, entry, x, x + span, y + 1, y + 2,
313      GTK_FILL, GTK_FILL, 0, 0);
314 
315     g_signal_connect (entry, "changed", (GCallback) entry_changed, nullptr);
316 }
317 
create_infowin()318 static void create_infowin ()
319 {
320     int dpi = audgui_get_dpi ();
321 
322     infowin = gtk_window_new (GTK_WINDOW_TOPLEVEL);
323     gtk_container_set_border_width ((GtkContainer *) infowin, 6);
324     gtk_window_set_title ((GtkWindow *) infowin, _("Song Info"));
325     gtk_window_set_type_hint ((GtkWindow *) infowin,
326      GDK_WINDOW_TYPE_HINT_DIALOG);
327 
328     GtkWidget * main_grid = gtk_table_new (0, 0, false);
329     gtk_table_set_col_spacings ((GtkTable *) main_grid, 6);
330     gtk_table_set_row_spacings ((GtkTable *) main_grid, 6);
331     gtk_container_add ((GtkContainer *) infowin, main_grid);
332 
333     widgets.image = audgui_scaled_image_new (nullptr);
334     gtk_table_attach_defaults ((GtkTable *) main_grid, widgets.image, 0, 1, 0, 1);
335 
336     widgets.location = gtk_label_new ("");
337     gtk_widget_set_size_request (widgets.location, 2 * dpi, -1);
338     gtk_label_set_line_wrap ((GtkLabel *) widgets.location, true);
339     gtk_label_set_line_wrap_mode ((GtkLabel *) widgets.location, PANGO_WRAP_WORD_CHAR);
340     gtk_label_set_selectable ((GtkLabel *) widgets.location, true);
341     gtk_table_attach ((GtkTable *) main_grid, widgets.location, 0, 1, 1, 2,
342      GTK_FILL, GTK_FILL, 0, 0);
343 
344     GtkWidget * codec_grid = gtk_table_new (0, 0, false);
345     gtk_table_set_row_spacings ((GtkTable *) codec_grid, 2);
346     gtk_table_set_col_spacings ((GtkTable *) codec_grid, 12);
347     gtk_table_attach ((GtkTable *) main_grid, codec_grid, 0, 1, 2, 3,
348      GTK_FILL, GTK_FILL, 0, 0);
349 
350     for (int row = 0; row < CODEC_ITEMS; row ++)
351     {
352         GtkWidget * label = small_label_new (_(codec_labels[row]));
353         gtk_table_attach ((GtkTable *) codec_grid, label, 0, 1, row, row + 1,
354          GTK_FILL, GTK_FILL, 0, 0);
355 
356         widgets.codec[row] = small_label_new (nullptr);
357         gtk_table_attach ((GtkTable *) codec_grid, widgets.codec[row], 1, 2, row, row + 1,
358          GTK_FILL, GTK_FILL, 0, 0);
359     }
360 
361     GtkWidget * grid = gtk_table_new (0, 0, false);
362     gtk_table_set_row_spacings ((GtkTable *) grid, 2);
363     gtk_table_set_col_spacings ((GtkTable *) grid, 6);
364     gtk_table_attach ((GtkTable *) main_grid, grid, 1, 2, 0, 3,
365      GTK_FILL, GTK_FILL, 0, 0);
366 
367     widgets.title = gtk_entry_new ();
368     gtk_widget_set_size_request (widgets.title, 3 * dpi, -1);
369     add_entry (grid, _("Title"), widgets.title, 0, 0, 2);
370 
371     widgets.artist = gtk_entry_new ();
372     add_entry (grid, _("Artist"), widgets.artist, 0, 2, 2);
373 
374     widgets.album = gtk_entry_new ();
375     add_entry (grid, _("Album"), widgets.album, 0, 4, 2);
376 
377     widgets.album_artist = gtk_entry_new ();
378     add_entry (grid, _("Album Artist"), widgets.album_artist, 0, 6, 2);
379 
380     widgets.comment = gtk_entry_new ();
381     add_entry (grid, _("Comment"), widgets.comment, 0, 8, 2);
382 
383     widgets.genre = gtk_combo_box_text_new_with_entry ();
384     genre_fill (widgets.genre);
385     add_entry (grid, _("Genre"), widgets.genre, 0, 10, 2);
386 
387     widgets.year = gtk_entry_new ();
388     add_entry (grid, _("Year"), widgets.year, 0, 12, 1);
389 
390     widgets.track = gtk_entry_new ();
391     add_entry (grid, _("Track Number"), widgets.track, 1, 12, 1);
392 
393     GtkWidget * bottom_hbox = gtk_hbox_new (false, 6);
394     gtk_table_attach ((GtkTable *) main_grid, bottom_hbox, 0, 2, 3, 4,
395      GTK_FILL, GTK_FILL, 0, 0);
396 
397     widgets.autofill = gtk_check_button_new_with_mnemonic (_("_Auto-fill empty fields"));
398 
399     gtk_toggle_button_set_active ((GtkToggleButton *) widgets.autofill,
400      ! aud_get_bool ("audgui", "clear_song_fields"));
401     g_signal_connect (widgets.autofill, "toggled", (GCallback) autofill_toggled, nullptr);
402 
403     gtk_widget_set_no_show_all (widgets.autofill, true);
404     gtk_widget_show (widgets.autofill);
405     gtk_box_pack_start ((GtkBox *) bottom_hbox, widgets.autofill, false, false, 0);
406 
407     widgets.ministatus = small_label_new (nullptr);
408     gtk_widget_set_no_show_all (widgets.ministatus, true);
409     gtk_box_pack_start ((GtkBox *) bottom_hbox, widgets.ministatus, true, true, 0);
410 
411     widgets.apply = audgui_button_new (_("_Save"), "document-save",
412      (AudguiCallback) infowin_update_tuple, nullptr);
413 
414     GtkWidget * close_button = audgui_button_new (_("_Close"), "window-close",
415      (AudguiCallback) audgui_infowin_hide, nullptr);
416 
417     GtkWidget * prev_button = audgui_button_new (_("_Previous"), "go-previous",
418      (AudguiCallback) infowin_prev, nullptr);
419 
420     GtkWidget * next_button = audgui_button_new (_("_Next"), "go-next",
421      (AudguiCallback) infowin_next, nullptr);
422 
423     gtk_box_pack_end ((GtkBox *) bottom_hbox, close_button, false, false, 0);
424     gtk_box_pack_end ((GtkBox *) bottom_hbox, widgets.apply, false, false, 0);
425     gtk_box_pack_end ((GtkBox *) bottom_hbox, next_button, false, false, 0);
426     gtk_box_pack_end ((GtkBox *) bottom_hbox, prev_button, false, false, 0);
427 
428     audgui_destroy_on_escape (infowin);
429     g_signal_connect (infowin, "destroy", (GCallback) infowin_destroyed, nullptr);
430 
431     hook_associate ("art ready", (HookFunction) infowin_display_image, nullptr);
432 }
433 
infowin_show(Playlist list,int entry,const String & filename,const Tuple & tuple,PluginHandle * decoder,bool writable)434 static void infowin_show (Playlist list, int entry, const String & filename,
435  const Tuple & tuple, PluginHandle * decoder, bool writable)
436 {
437     if (! infowin)
438         create_infowin ();
439 
440     current_playlist = list;
441     current_entry = entry;
442     current_file = filename;
443     current_tuple = tuple.ref ();
444     current_decoder = decoder;
445     can_write = writable;
446 
447     bool clear = aud_get_bool ("audgui", "clear_song_fields");
448     bool changed = false;
449 
450     set_entry_str_from_field (widgets.title, tuple, Tuple::Title, writable, clear, changed);
451     set_entry_str_from_field (widgets.artist, tuple, Tuple::Artist, writable, clear, changed);
452     set_entry_str_from_field (widgets.album, tuple, Tuple::Album, writable, clear, changed);
453     set_entry_str_from_field (widgets.album_artist, tuple, Tuple::AlbumArtist, writable, clear, changed);
454     set_entry_str_from_field (widgets.comment, tuple, Tuple::Comment, writable, clear, changed);
455     set_entry_str_from_field (gtk_bin_get_child ((GtkBin *) widgets.genre),
456      tuple, Tuple::Genre, writable, clear, changed);
457 
458     gtk_label_set_text ((GtkLabel *) widgets.location, uri_to_display (filename));
459 
460     set_entry_int_from_field (widgets.year, tuple, Tuple::Year, writable, clear, changed);
461     set_entry_int_from_field (widgets.track, tuple, Tuple::Track, writable, clear, changed);
462 
463     String codec_values[CODEC_ITEMS];
464 
465     codec_values[CODEC_FORMAT] = tuple.get_str (Tuple::Codec);
466     codec_values[CODEC_QUALITY] = tuple.get_str (Tuple::Quality);
467 
468     if (tuple.get_value_type (Tuple::Bitrate) == Tuple::Int)
469         codec_values[CODEC_BITRATE] = String (str_printf (_("%d kb/s"),
470          tuple.get_int (Tuple::Bitrate)));
471 
472     for (int row = 0; row < CODEC_ITEMS; row ++)
473     {
474         const char * text = codec_values[row] ? (const char *) codec_values[row] : _("N/A");
475         gtk_label_set_text ((GtkLabel *) widgets.codec[row], text);
476     }
477 
478     infowin_display_image (filename);
479 
480     gtk_widget_set_sensitive (widgets.apply, changed);
481     gtk_widget_grab_focus (widgets.title);
482 
483     if (! audgui_reshow_unique_window (AUDGUI_INFO_WINDOW))
484         audgui_show_unique_window (AUDGUI_INFO_WINDOW, infowin);
485 }
486 
audgui_infowin_show(Playlist playlist,int entry)487 EXPORT void audgui_infowin_show (Playlist playlist, int entry)
488 {
489     String filename = playlist.entry_filename (entry);
490     g_return_if_fail (filename != nullptr);
491 
492     String error;
493     PluginHandle * decoder = playlist.entry_decoder (entry, Playlist::Wait, & error);
494     Tuple tuple = decoder ? playlist.entry_tuple (entry, Playlist::Wait, & error) : Tuple ();
495 
496     if (decoder && tuple.valid () && ! aud_custom_infowin (filename, decoder))
497     {
498         /* cuesheet entries cannot be updated */
499         bool can_write = aud_file_can_write_tuple (filename, decoder) &&
500          ! tuple.is_set (Tuple::StartTime);
501 
502         tuple.delete_fallbacks ();
503         infowin_show (playlist, entry, filename, tuple, decoder, can_write);
504     }
505     else
506         audgui_infowin_hide ();
507 
508     if (error)
509         aud_ui_show_error (str_printf (_("Error opening %s:\n%s"),
510          (const char *) filename, (const char *) error));
511 }
512 
audgui_infowin_show_current()513 EXPORT void audgui_infowin_show_current ()
514 {
515     auto playlist = Playlist::playing_playlist ();
516     if (playlist == Playlist ())
517         playlist = Playlist::active_playlist ();
518 
519     int position = playlist.get_position ();
520     if (position < 0)
521         return;
522 
523     audgui_infowin_show (playlist, position);
524 }
525 
audgui_infowin_hide()526 EXPORT void audgui_infowin_hide ()
527 {
528     audgui_hide_unique_window (AUDGUI_INFO_WINDOW);
529 }
530