1 /*
2  * ui_playlist_notebook.c
3  * Copyright 2010-2017 Michał Lipski and John Lindgren
4  *
5  * Redistribution and use in source and binary forms, with or without
6  * modification, are permitted provided that the following conditions are met:
7  *
8  * 1. Redistributions of source code must retain the above copyright notice,
9  *    this list of conditions, and the following disclaimer.
10  *
11  * 2. Redistributions in binary form must reproduce the above copyright notice,
12  *    this list of conditions, and the following disclaimer in the documentation
13  *    provided with the distribution.
14  *
15  * This software is provided "as is" and without any warranty, express or
16  * implied. In no event shall the authors be liable for any damages arising from
17  * the use of this software.
18  */
19 
20 #include <stdlib.h>
21 
22 #include <gdk/gdkkeysyms.h>
23 #include <gtk/gtk.h>
24 
25 #define AUD_GLIB_INTEGRATION
26 #include <libaudcore/runtime.h>
27 #include <libaudcore/playlist.h>
28 #include <libaudcore/audstrings.h>
29 #include <libaudcore/hook.h>
30 #include <libaudgui/list.h>
31 #include <libaudgui/libaudgui.h>
32 
33 #include "../ui-common/menu-ops.h"
34 
35 #include "gtkui.h"
36 #include "ui_playlist_notebook.h"
37 #include "ui_playlist_widget.h"
38 
39 GtkWidget * pl_notebook = nullptr;
40 
41 static Playlist highlighted;
42 
43 static int switch_handler = 0;
44 static int reorder_handler = 0;
45 
treeview_of(GtkWidget * page)46 static GtkWidget * treeview_of (GtkWidget * page)
47     { return (GtkWidget *) g_object_get_data ((GObject *) page, "treeview"); }
48 
treeview_at_idx(int idx)49 static GtkWidget * treeview_at_idx (int idx)
50     { return treeview_of (gtk_notebook_get_nth_page ((GtkNotebook *) pl_notebook, idx)); }
51 
list_of(GtkWidget * widget)52 static Playlist list_of (GtkWidget * widget)
53     { return aud::from_ptr<Playlist> (g_object_get_data ((GObject *) widget, "playlist")); }
54 
apply_column_widths(GtkWidget * treeview)55 void apply_column_widths (GtkWidget * treeview)
56 {
57     /* skip righthand column since it expands with the window */
58     for (int i = 0; i < pw_num_cols - 1; i ++)
59     {
60         GtkTreeViewColumn * col = gtk_tree_view_get_column ((GtkTreeView *) treeview, i);
61         gtk_tree_view_column_set_fixed_width (col, pw_col_widths[pw_cols[i]]);
62     }
63 }
64 
size_allocate_cb(GtkWidget * treeview)65 static void size_allocate_cb (GtkWidget * treeview)
66 {
67     int current = gtk_notebook_get_current_page ((GtkNotebook *) pl_notebook);
68 
69     if (current < 0 || treeview != treeview_at_idx (current))
70         return;
71 
72     bool changed = false;
73 
74     /* skip righthand column since it expands with the window */
75     for (int i = 0; i < pw_num_cols - 1; i ++)
76     {
77         GtkTreeViewColumn * col = gtk_tree_view_get_column ((GtkTreeView *) treeview, i);
78         int width = gtk_tree_view_column_get_width (col);
79 
80         if (width != pw_col_widths[pw_cols[i]])
81         {
82             pw_col_widths[pw_cols[i]] = width;
83             changed = true;
84         }
85     }
86 
87     if (changed)
88     {
89         int count = gtk_notebook_get_n_pages ((GtkNotebook *) pl_notebook);
90 
91         for (int i = 0; i < count; i ++)
92         {
93             if (i != current)
94                 apply_column_widths (treeview_at_idx (i));
95         }
96     }
97 }
98 
make_add_button(GtkWidget * notebook)99 static void make_add_button (GtkWidget * notebook)
100 {
101     GtkWidget * button = gtk_button_new ();
102     gtk_button_set_relief ((GtkButton *) button, GTK_RELIEF_NONE);
103     gtk_container_add ((GtkContainer *) button, gtk_image_new_from_icon_name
104      ("list-add", GTK_ICON_SIZE_MENU));
105     gtk_widget_set_can_focus (button, false);
106 
107     g_signal_connect (button, "clicked", pl_new, nullptr);
108     gtk_widget_show_all (button);
109 
110     gtk_notebook_set_action_widget ((GtkNotebook *) notebook, button, GTK_PACK_END);
111 }
112 
close_button_cb(GtkWidget * button,void * data)113 static void close_button_cb (GtkWidget * button, void * data)
114 {
115     audgui_confirm_playlist_delete (aud::from_ptr<Playlist> (data));
116 }
117 
close_button_style_set(GtkWidget * button)118 static void close_button_style_set (GtkWidget * button)
119 {
120     int w, h;
121     gtk_icon_size_lookup_for_settings (gtk_widget_get_settings (button),
122      GTK_ICON_SIZE_MENU, & w, & h);
123     gtk_widget_set_size_request (button, w + 2, h + 2);
124 }
125 
make_close_button(GtkWidget * ebox,Playlist list)126 static GtkWidget * make_close_button (GtkWidget * ebox, Playlist list)
127 {
128     GtkWidget * button = gtk_button_new ();
129     GtkWidget * image = gtk_image_new_from_icon_name ("window-close", GTK_ICON_SIZE_MENU);
130     gtk_button_set_image ((GtkButton *) button, image);
131     gtk_button_set_relief ((GtkButton *) button, GTK_RELIEF_NONE);
132     gtk_button_set_focus_on_click ((GtkButton *) button, false);
133     gtk_widget_set_name (button, "gtkui-tab-close-button");
134 
135     g_signal_connect (button, "clicked", (GCallback) close_button_cb, aud::to_ptr (list));
136 
137     gtk_rc_parse_string (
138      "style \"gtkui-tab-close-button-style\" {"
139      " GtkButton::default-border = {0, 0, 0, 0}"
140      " GtkButton::default-outside-border = {0, 0, 0, 0}"
141      " GtkButton::inner-border = {0, 0, 0, 0}"
142      " GtkWidget::focus-padding = 0"
143      " GtkWidget::focus-line-width = 0"
144      " xthickness = 0"
145      " ythickness = 0 }"
146      "widget \"*.gtkui-tab-close-button\" style \"gtkui-tab-close-button-style\""
147     );
148 
149     g_signal_connect (button, "style-set", (GCallback) close_button_style_set, nullptr);
150 
151     gtk_widget_show (button);
152 
153     return button;
154 }
155 
pl_notebook_grab_focus()156 void pl_notebook_grab_focus ()
157 {
158     int idx = gtk_notebook_get_current_page ((GtkNotebook *) pl_notebook);
159     gtk_widget_grab_focus (treeview_at_idx (idx));
160 }
161 
tab_title_reset(GtkWidget * ebox)162 static void tab_title_reset (GtkWidget * ebox)
163 {
164     GtkWidget * label = (GtkWidget *) g_object_get_data ((GObject *) ebox, "label");
165     GtkWidget * entry = (GtkWidget *) g_object_get_data ((GObject *) ebox, "entry");
166     gtk_widget_hide (entry);
167     gtk_widget_show (label);
168 }
169 
tab_title_save(GtkEntry * entry,GtkWidget * ebox)170 static void tab_title_save (GtkEntry * entry, GtkWidget * ebox)
171 {
172     GtkWidget * label = (GtkWidget *) g_object_get_data ((GObject *) ebox, "label");
173     list_of (ebox).set_title (gtk_entry_get_text (entry));
174     gtk_widget_hide ((GtkWidget *) entry);
175     gtk_widget_show (label);
176 }
177 
tab_key_press_cb(GtkWidget * widget,GdkEventKey * event)178 static gboolean tab_key_press_cb (GtkWidget * widget, GdkEventKey * event)
179 {
180     if (event->keyval == GDK_KEY_Escape)
181         tab_title_reset (widget);
182 
183     return false;
184 }
185 
tab_button_press_cb(GtkWidget * ebox,GdkEventButton * event)186 static gboolean tab_button_press_cb (GtkWidget * ebox, GdkEventButton * event)
187 {
188     auto list = list_of (ebox);
189 
190     if (event->type == GDK_2BUTTON_PRESS && event->button == 1)
191         list.start_playback ();
192 
193     if (event->type == GDK_BUTTON_PRESS && event->button == 2)
194         audgui_confirm_playlist_delete (list);
195 
196     if (event->type == GDK_BUTTON_PRESS && event->button == 3)
197         popup_menu_tab (event->button, event->time, list);
198 
199     return false;
200 }
201 
scroll_cb(GtkWidget * widget,GdkEventScroll * event)202 static gboolean scroll_cb (GtkWidget * widget, GdkEventScroll * event)
203 {
204     switch (event->direction)
205     {
206     case GDK_SCROLL_UP:
207     case GDK_SCROLL_LEFT:
208         pl_prev ();
209         return true;
210 
211     case GDK_SCROLL_DOWN:
212     case GDK_SCROLL_RIGHT:
213         pl_next ();
214         return true;
215 
216     default:
217         return false;
218     }
219 }
220 
scroll_ignore_cb()221 static gboolean scroll_ignore_cb ()
222 {
223     return true;
224 }
225 
tab_changed(GtkNotebook * notebook,GtkWidget * page,unsigned page_num)226 static void tab_changed (GtkNotebook * notebook, GtkWidget * page, unsigned page_num)
227 {
228     Playlist::by_index (page_num).activate ();
229 }
230 
tab_reordered(GtkNotebook * notebook,GtkWidget * page,unsigned page_num)231 static void tab_reordered (GtkNotebook * notebook, GtkWidget * page, unsigned page_num)
232 {
233     auto list = list_of (treeview_of (page));
234     Playlist::reorder_playlists (list.index (), page_num, 1);
235 }
236 
get_tab_label(int list_idx)237 static GtkLabel * get_tab_label (int list_idx)
238 {
239     GtkWidget * page = gtk_notebook_get_nth_page ((GtkNotebook *) pl_notebook, list_idx);
240     GtkWidget * ebox = gtk_notebook_get_tab_label ((GtkNotebook *) pl_notebook, page);
241     return (GtkLabel *) g_object_get_data ((GObject *) ebox, "label");
242 }
243 
update_tab_label(GtkLabel * label,Playlist list)244 static void update_tab_label (GtkLabel * label, Playlist list)
245 {
246     String title0 = list.get_title ();
247     StringBuf title = aud_get_bool ("gtkui", "entry_count_visible") ?
248      str_printf ("%s (%d)", (const char *) title0, list.n_entries ()) :
249      str_copy (title0);
250 
251     if (list == Playlist::playing_playlist ())
252     {
253         CharPtr markup (g_markup_printf_escaped ("<b>%s</b>", (const char *) title));
254         gtk_label_set_markup (label, markup);
255     }
256     else
257         gtk_label_set_text (label, title);
258 }
259 
start_rename_playlist(Playlist playlist)260 void start_rename_playlist (Playlist playlist)
261 {
262     if (! gtk_notebook_get_show_tabs ((GtkNotebook *) pl_notebook))
263     {
264         audgui_show_playlist_rename (playlist);
265         return;
266     }
267 
268     GtkWidget * page = gtk_notebook_get_nth_page ((GtkNotebook *) pl_notebook, playlist.index ());
269     GtkWidget * ebox = gtk_notebook_get_tab_label ((GtkNotebook *) pl_notebook, page);
270 
271     GtkWidget * label = (GtkWidget *) g_object_get_data ((GObject *) ebox, "label");
272     GtkWidget * entry = (GtkWidget *) g_object_get_data ((GObject *) ebox, "entry");
273     gtk_widget_hide (label);
274 
275     gtk_entry_set_text ((GtkEntry *) entry, playlist.get_title ());
276 
277     gtk_widget_grab_focus (entry);
278     gtk_editable_select_region ((GtkEditable *) entry, 0, -1);
279     gtk_widget_show (entry);
280 }
281 
create_tab(int list_idx,Playlist list)282 static void create_tab (int list_idx, Playlist list)
283 {
284     GtkWidget * scrollwin = gtk_scrolled_window_new (nullptr, nullptr);
285     GtkAdjustment * vscroll = gtk_scrolled_window_get_vadjustment ((GtkScrolledWindow *) scrollwin);
286 
287     /* do not allow scroll events to propagate up to the notebook */
288     g_signal_connect_after (scrollwin, "scroll-event", (GCallback) scroll_ignore_cb, nullptr);
289 
290     GtkWidget * treeview = ui_playlist_widget_new (list);
291 
292     apply_column_widths (treeview);
293     g_signal_connect (treeview, "size-allocate", (GCallback) size_allocate_cb, nullptr);
294 
295     g_object_set_data ((GObject *) scrollwin, "treeview", treeview);
296 
297     gtk_container_add ((GtkContainer *) scrollwin, treeview);
298     gtk_scrolled_window_set_policy ((GtkScrolledWindow *) scrollwin,
299      GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC);
300     gtk_widget_show_all (scrollwin);
301 
302     GtkWidget * ebox = gtk_event_box_new ();
303     gtk_event_box_set_visible_window ((GtkEventBox *) ebox, false);
304 
305     GtkWidget * hbox = gtk_hbox_new (false, 2);
306 
307     GtkWidget * label = gtk_label_new ("");
308     update_tab_label ((GtkLabel *) label, list);
309     gtk_box_pack_start ((GtkBox *) hbox, label, false, false, 0);
310 
311     GtkWidget * entry = gtk_entry_new ();
312     gtk_box_pack_start ((GtkBox *) hbox, entry, false, false, 0);
313     gtk_container_add ((GtkContainer *) ebox, hbox);
314     gtk_widget_show_all (ebox);
315     gtk_widget_hide (entry);
316 
317     GtkWidget * button = nullptr;
318 
319     if (aud_get_bool ("gtkui", "close_button_visible"))
320     {
321         button = make_close_button (ebox, list);
322         gtk_box_pack_end ((GtkBox *) hbox, button, false, false, 0);
323     }
324 
325     g_object_set_data ((GObject *) ebox, "label", label);
326     g_object_set_data ((GObject *) ebox, "entry", entry);
327     g_object_set_data ((GObject *) ebox, "page", scrollwin);
328 
329     gtk_notebook_insert_page ((GtkNotebook *) pl_notebook, scrollwin, ebox, list_idx);
330     gtk_notebook_set_tab_reorderable ((GtkNotebook *) pl_notebook, scrollwin, true);
331 
332     g_object_set_data ((GObject *) ebox, "playlist", aud::to_ptr (list));
333     g_object_set_data ((GObject *) treeview, "playlist", aud::to_ptr (list));
334 
335     int position = list.get_position ();
336     if (position >= 0)
337         audgui_list_set_highlight (treeview, position);
338 
339     int focus = list.get_focus ();
340     if (focus >= 0)
341         audgui_list_set_focus (treeview, position);
342 
343     g_signal_connect (ebox, "button-press-event", (GCallback) tab_button_press_cb, nullptr);
344     g_signal_connect (ebox, "key-press-event", (GCallback) tab_key_press_cb, nullptr);
345     g_signal_connect (entry, "activate", (GCallback) tab_title_save, ebox);
346     g_signal_connect_swapped (vscroll, "value-changed",
347      (GCallback) ui_playlist_widget_scroll, treeview);
348 
349     /* we have to connect to "scroll-event" on the notebook, the tabs, AND the
350      * close buttons (sigh) */
351     gtk_widget_add_events (ebox, GDK_SCROLL_MASK);
352     g_signal_connect (ebox, "scroll-event", (GCallback) scroll_cb, nullptr);
353 
354     if (button)
355     {
356         gtk_widget_add_events (button, GDK_SCROLL_MASK);
357         g_signal_connect (button, "scroll-event", (GCallback) scroll_cb, nullptr);
358     }
359 }
360 
switch_to_active()361 static void switch_to_active ()
362 {
363     int active_idx = Playlist::active_playlist ().index ();
364     gtk_notebook_set_current_page ((GtkNotebook *) pl_notebook, active_idx);
365 }
366 
pl_notebook_populate()367 void pl_notebook_populate ()
368 {
369     int n_playlists = Playlist::n_playlists ();
370     for (int idx = 0; idx < n_playlists; idx ++)
371         create_tab (idx, Playlist::by_index (idx));
372 
373     switch_to_active ();
374     highlighted = Playlist::playing_playlist ();
375 
376     if (! switch_handler)
377         switch_handler = g_signal_connect (pl_notebook, "switch-page",
378          (GCallback) tab_changed, nullptr);
379     if (! reorder_handler)
380         reorder_handler = g_signal_connect (pl_notebook, "page-reordered",
381          (GCallback) tab_reordered, nullptr);
382 
383     pl_notebook_grab_focus ();
384 }
385 
pl_notebook_purge()386 void pl_notebook_purge ()
387 {
388     if (switch_handler)
389         g_signal_handler_disconnect (pl_notebook, switch_handler);
390     switch_handler = 0;
391     if (reorder_handler)
392         g_signal_handler_disconnect (pl_notebook, reorder_handler);
393     reorder_handler = 0;
394 
395     int n_pages = gtk_notebook_get_n_pages ((GtkNotebook *) pl_notebook);
396     while (n_pages)
397         gtk_notebook_remove_page ((GtkNotebook *) pl_notebook, -- n_pages);
398 }
399 
add_remove_pages()400 static void add_remove_pages ()
401 {
402     g_signal_handlers_block_by_func (pl_notebook, (void *) tab_changed, nullptr);
403     g_signal_handlers_block_by_func (pl_notebook, (void *) tab_reordered, nullptr);
404 
405     int lists = Playlist::n_playlists ();
406     int pages = gtk_notebook_get_n_pages ((GtkNotebook *) pl_notebook);
407 
408     /* scan through existing treeviews */
409     for (int i = 0; i < pages; )
410     {
411         auto list0 = list_of (treeview_at_idx (i));
412 
413         /* do we have an orphaned treeview? */
414         if (! list0.exists ())
415         {
416             gtk_notebook_remove_page ((GtkNotebook *) pl_notebook, i);
417             pages --;
418             continue;
419         }
420 
421         /* do we have the right treeview? */
422         auto list = Playlist::by_index (i);
423 
424         if (list0 == list)
425         {
426             i ++;
427             continue;
428         }
429 
430         /* look for the right treeview */
431         int found = false;
432 
433         for (int j = i + 1; j < pages; j ++)
434         {
435             GtkWidget * page = gtk_notebook_get_nth_page ((GtkNotebook *) pl_notebook, j);
436             auto list2 = list_of (treeview_of (page));
437 
438             /* found it? move it to the right place */
439             if (list2 == list)
440             {
441                 gtk_notebook_reorder_child ((GtkNotebook *) pl_notebook, page, i);
442                 found = true;
443                 break;
444             }
445         }
446 
447         /* didn't find it? create it */
448         if (! found)
449         {
450             create_tab (i, list);
451             pages ++;
452             continue;
453         }
454     }
455 
456     /* create new treeviews */
457     while (pages < lists)
458     {
459         create_tab (pages, Playlist::by_index (pages));
460         pages ++;
461     }
462 
463     switch_to_active ();
464     show_hide_playlist_tabs ();
465 
466     g_signal_handlers_unblock_by_func (pl_notebook, (void *) tab_changed, nullptr);
467     g_signal_handlers_unblock_by_func (pl_notebook, (void *) tab_reordered, nullptr);
468 }
469 
pl_notebook_update(void * data,void * user)470 void pl_notebook_update (void * data, void * user)
471 {
472     auto global_level = aud::from_ptr<Playlist::UpdateLevel> (data);
473     if (global_level == Playlist::Structure)
474         add_remove_pages ();
475 
476     int n_pages = gtk_notebook_get_n_pages ((GtkNotebook *) pl_notebook);
477 
478     for (int i = 0; i < n_pages; i ++)
479     {
480         GtkWidget * treeview = treeview_at_idx (i);
481 
482         if (global_level >= Playlist::Metadata)
483             update_tab_label (get_tab_label (i), list_of (treeview));
484 
485         ui_playlist_widget_update (treeview);
486     }
487 
488     switch_to_active ();
489 }
490 
pl_notebook_set_position(void * data,void * user)491 void pl_notebook_set_position (void * data, void * user)
492 {
493     auto list = aud::from_ptr<Playlist> (data);
494     int row = list.get_position ();
495 
496     if (aud_get_bool ("gtkui", "autoscroll"))
497     {
498         list.select_all (false);
499         list.select_entry (row, true);
500         list.set_focus (row);
501     }
502 
503     audgui_list_set_highlight (treeview_at_idx (list.index ()), row);
504 }
505 
pl_notebook_activate(void * data,void * user)506 void pl_notebook_activate (void * data, void * user)
507 {
508     switch_to_active ();
509 }
510 
pl_notebook_set_playing(void * data,void * user)511 void pl_notebook_set_playing (void * data, void * user)
512 {
513     auto playing = Playlist::playing_playlist ();
514 
515     // if the previous playing playlist was deleted, ignore it
516     if (! highlighted.exists ())
517         highlighted = Playlist ();
518 
519     if (highlighted == playing)
520         return;
521 
522     int pages = gtk_notebook_get_n_pages ((GtkNotebook *) pl_notebook);
523 
524     for (int i = 0; i < pages; i ++)
525     {
526         auto list = list_of (treeview_at_idx (i));
527         if (list == highlighted || list == playing)
528             update_tab_label (get_tab_label (i), list);
529     }
530 
531     highlighted = playing;
532 }
533 
destroy_cb()534 static void destroy_cb ()
535 {
536     pl_notebook = nullptr;
537     switch_handler = 0;
538     reorder_handler = 0;
539 }
540 
pl_notebook_new()541 GtkWidget * pl_notebook_new ()
542 {
543     pl_notebook = gtk_notebook_new ();
544     gtk_notebook_set_scrollable ((GtkNotebook *) pl_notebook, true);
545     make_add_button (pl_notebook);
546 
547     show_hide_playlist_tabs ();
548 
549     gtk_widget_add_events (pl_notebook, GDK_SCROLL_MASK);
550     g_signal_connect (pl_notebook, "scroll-event", (GCallback) scroll_cb, nullptr);
551     g_signal_connect (pl_notebook, "destroy", (GCallback) destroy_cb, nullptr);
552 
553     return pl_notebook;
554 }
555 
show_hide_playlist_tabs()556 void show_hide_playlist_tabs ()
557 {
558     gtk_notebook_set_show_tabs ((GtkNotebook *) pl_notebook, aud_get_bool ("gtkui",
559      "playlist_tabs_visible") || Playlist::n_playlists () > 1);
560 }
561