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