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