chiark / gitweb /
Preserve playlist selection when list changes
[disorder] / disobedience / playlists.c
CommitLineData
fc36ecb7
RK
1/*
2 * This file is part of DisOrder
121944d1 3 * Copyright (C) 2008, 2009 Richard Kettlewell
fc36ecb7
RK
4 *
5 * This program is free software; you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation; either version 2 of the License, or
8 * (at your option) any later version.
9 *
10 * This program is distributed in the hope that it will be useful, but
11 * WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13 * General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License
16 * along with this program; if not, write to the Free Software
17 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
18 * USA
19 */
20/** @file disobedience/playlists.c
21 * @brief Playlist for Disobedience
22 *
f0bd437a
RK
23 * The playlists management window contains:
24 * - a list of all playlists
25 * - an add button
26 * - a delete button
27 * - a drag+drop capable view of the playlist
28 * - a close button
fc36ecb7
RK
29 */
30#include "disobedience.h"
6bfaa2a1
RK
31#include "queue-generic.h"
32#include "popup.h"
7f7c3819 33#include "validity.h"
fc36ecb7 34
0b0fb26b
RK
35#if PLAYLISTS
36
fc36ecb7
RK
37static void playlists_updated(void *v,
38 const char *err,
39 int nvec, char **vec);
40
f0bd437a
RK
41/** @brief Playlist editing window */
42static GtkWidget *playlists_window;
43
44/** @brief Tree model for list of playlists */
45static GtkListStore *playlists_list;
46
47/** @brief Selection for list of playlists */
48static GtkTreeSelection *playlists_selection;
49
50/** @brief Currently selected playlist */
51static const char *playlists_selected;
52
53/** @brief Delete button */
54static GtkWidget *playlists_delete_button;
55
fc36ecb7
RK
56/** @brief Current list of playlists or NULL */
57char **playlists;
58
59/** @brief Count of playlists */
60int nplaylists;
61
6bfaa2a1
RK
62/** @brief Columns for the playlist editor */
63static const struct queue_column playlist_columns[] = {
64 { "Artist", column_namepart, "artist", COL_EXPAND|COL_ELLIPSIZE },
65 { "Album", column_namepart, "album", COL_EXPAND|COL_ELLIPSIZE },
66 { "Title", column_namepart, "title", COL_EXPAND|COL_ELLIPSIZE },
67 { "Length", column_length, 0, COL_RIGHT }
68};
69
70/** @brief Pop-up menu for playlist editor */
71// TODO some of these may not be generic enough yet - check!
72static struct menuitem playlist_menuitems[] = {
73 { "Track properties", ql_properties_activate, ql_properties_sensitive, 0, 0 },
74 { "Play track", ql_play_activate, ql_play_sensitive, 0, 0 },
75 //{ "Play playlist", ql_playall_activate, ql_playall_sensitive, 0, 0 },
76 { "Remove track from queue", ql_remove_activate, ql_remove_sensitive, 0, 0 },
77 { "Select all tracks", ql_selectall_activate, ql_selectall_sensitive, 0, 0 },
78 { "Deselect all tracks", ql_selectnone_activate, ql_selectnone_sensitive, 0, 0 },
79};
80
81/** @brief Queuelike for editing a playlist */
82static struct queuelike ql_playlist = {
83 .name = "playlist",
84 .columns = playlist_columns,
85 .ncolumns = sizeof playlist_columns / sizeof *playlist_columns,
86 .menuitems = playlist_menuitems,
87 .nmenuitems = sizeof playlist_menuitems / sizeof *playlist_menuitems,
88 //.drop = playlist_drop //TODO
89};
90
3c1a4e96 91/* Maintaining the list of playlists ---------------------------------------- */
121944d1 92
fc36ecb7
RK
93/** @brief Schedule an update to the list of playlists */
94static void playlists_update(const char attribute((unused)) *event,
95 void attribute((unused)) *eventdata,
96 void attribute((unused)) *callbackdata) {
97 disorder_eclient_playlists(client, playlists_updated, 0);
98}
99
100/** @brief qsort() callback for playlist name comparison */
101static int playlistcmp(const void *ap, const void *bp) {
102 const char *a = *(char **)ap, *b = *(char **)bp;
103 const char *ad = strchr(a, '.'), *bd = strchr(b, '.');
104 int c;
105
106 /* Group owned playlists by owner */
107 if(ad && bd) {
108 const int adn = ad - a, bdn = bd - b;
109 if((c = strncmp(a, b, adn < bdn ? adn : bdn)))
110 return c;
111 /* Lexical order within playlists of a single owner */
112 return strcmp(ad + 1, bd + 1);
113 }
114
115 /* Owned playlists after shared ones */
116 if(ad) {
117 return 1;
118 } else if(bd) {
119 return -1;
120 }
121
122 /* Lexical order of shared playlists */
123 return strcmp(a, b);
124}
125
126/** @brief Called with a new list of playlists */
127static void playlists_updated(void attribute((unused)) *v,
128 const char *err,
129 int nvec, char **vec) {
130 if(err) {
131 playlists = 0;
132 nplaylists = -1;
133 /* Probably means server does not support playlists */
134 } else {
135 playlists = vec;
136 nplaylists = nvec;
137 qsort(playlists, nplaylists, sizeof (char *), playlistcmp);
138 }
139 /* Tell our consumers */
140 event_raise("playlists-updated", 0);
141}
142
121944d1
RK
143/* Playlists menu ----------------------------------------------------------- */
144
145/** @brief Play received playlist contents
146 *
147 * Passed as a completion callback by menu_activate_playlist().
148 */
149static void playlist_play_content(void attribute((unused)) *v,
150 const char *err,
151 int nvec, char **vec) {
152 if(err) {
153 popup_protocol_error(0, err);
154 return;
155 }
156 for(int n = 0; n < nvec; ++n)
157 disorder_eclient_play(client, vec[n], NULL, NULL);
158}
159
160/** @brief Called to activate a playlist
161 *
162 * Called when the menu item for a playlist is clicked.
163 */
f9b20469
RK
164static void menu_activate_playlist(GtkMenuItem *menuitem,
165 gpointer attribute((unused)) user_data) {
166 GtkLabel *label = GTK_LABEL(GTK_BIN(menuitem)->child);
167 const char *playlist = gtk_label_get_text(label);
168
121944d1 169 disorder_eclient_playlist_get(client, playlist_play_content, playlist, NULL);
f9b20469
RK
170}
171
172/** @brief Called when the playlists change */
173static void menu_playlists_changed(const char attribute((unused)) *event,
174 void attribute((unused)) *eventdata,
175 void attribute((unused)) *callbackdata) {
176 if(!playlists_menu)
177 return; /* OMG too soon */
178 GtkMenuShell *menu = GTK_MENU_SHELL(playlists_menu);
179 /* TODO: we could be more sophisticated and only insert/remove widgets as
c5782050
RK
180 * needed. The current logic trashes the selection which is not acceptable
181 * and interacts badly with one playlist being currently locked and
182 * edited. */
f9b20469
RK
183 while(menu->children)
184 gtk_container_remove(GTK_CONTAINER(menu), GTK_WIDGET(menu->children->data));
185 /* NB nplaylists can be -1 as well as 0 */
186 for(int n = 0; n < nplaylists; ++n) {
187 GtkWidget *w = gtk_menu_item_new_with_label(playlists[n]);
188 g_signal_connect(w, "activate", G_CALLBACK(menu_activate_playlist), 0);
189 gtk_widget_show(w);
190 gtk_menu_shell_append(menu, w);
191 }
fdea9f40 192 gtk_widget_set_sensitive(menu_playlists_widget,
f9b20469 193 nplaylists > 0);
fdea9f40 194 gtk_widget_set_sensitive(menu_editplaylists_widget,
6acdbba4 195 nplaylists >= 0);
f9b20469
RK
196}
197
7f7c3819
RK
198/* Popup to create a new playlist ------------------------------------------- */
199
200/** @brief New-playlist popup */
201static GtkWidget *playlist_new_window;
202
203/** @brief Text entry in new-playlist popup */
204static GtkWidget *playlist_new_entry;
205
206static GtkWidget *playlist_new_info;
207
208static GtkWidget *playlist_new_shared;
209static GtkWidget *playlist_new_public;
210static GtkWidget *playlist_new_private;
211
7c12e4bd
RK
212/** @brief Get entered new-playlist details */
213static void playlist_new_details(char **namep,
214 char **fullnamep,
215 gboolean *sharedp,
216 gboolean *publicp,
217 gboolean *privatep) {
218 gboolean shared, public, private;
219 g_object_get(playlist_new_shared, "active", &shared, (char *)NULL);
220 g_object_get(playlist_new_public, "active", &public, (char *)NULL);
221 g_object_get(playlist_new_private, "active", &private, (char *)NULL);
222 char *gname = gtk_editable_get_chars(GTK_EDITABLE(playlist_new_entry),
223 0, -1); /* name owned by calle */
224 char *name = xstrdup(gname);
225 g_free(gname);
226 if(sharedp) *sharedp = shared;
227 if(publicp) *publicp = public;
228 if(privatep) *privatep = private;
229 if(namep) *namep = name;
230 if(fullnamep) {
231 if(*sharedp) *fullnamep = *namep;
232 else byte_xasprintf(fullnamep, "%s.%s", config->username, name);
233 }
234}
235
236/** @brief Called when the newly created playlist has unlocked */
237static void playlist_new_unlocked(void attribute((unused)) *v, const char *err) {
238 if(err)
239 popup_protocol_error(0, err);
240 gtk_widget_destroy(playlist_new_window);
241}
242
243/** @brief Called when the new playlist has been created */
244static void playlist_new_created(void attribute((unused)) *v, const char *err) {
245 if(err) {
246 popup_protocol_error(0, err);
247 return;
248 }
249 disorder_eclient_playlist_unlock(client, playlist_new_unlocked, NULL);
250 // TODO arrange for the new playlist to be selected
251}
252
253/** @brief Called when the proposed new playlist's contents have been retrieved
254 *
255 * ...or rather, normally, when it's been reported that it does not exist.
256 */
257static void playlist_new_retrieved(void *v, const char *err,
258 int nvec,
259 char attribute((unused)) **vec) {
260 char *fullname = v;
261 if(!err && nvec != -1)
262 /* A rare case but not in principle impossible */
263 err = "A playlist with that name already exists.";
264 if(err) {
265 popup_protocol_error(0, err);
266 disorder_eclient_playlist_unlock(client, playlist_new_unlocked, fullname);
267 return;
268 }
269 gboolean shared, public, private;
270 playlist_new_details(0, 0, &shared, &public, &private);
271 disorder_eclient_playlist_set_share(client, playlist_new_created, fullname,
272 public ? "public"
273 : private ? "private"
274 : "shared",
275 fullname);
276}
277
278/** @brief Called when the proposed new playlist has been locked */
279static void playlist_new_locked(void *v, const char *err) {
280 char *fullname = v;
281 if(err) {
282 popup_protocol_error(0, err);
283 return;
284 }
285 disorder_eclient_playlist_get(client, playlist_new_retrieved,
286 fullname, fullname);
287}
288
7f7c3819
RK
289/** @brief Called when 'ok' is clicked in new-playlist popup */
290static void playlist_new_ok(GtkButton attribute((unused)) *button,
291 gpointer attribute((unused)) userdata) {
7c12e4bd
RK
292 gboolean shared, public, private;
293 char *name, *fullname;
294 playlist_new_details(&name, &fullname, &shared, &public, &private);
295
296 /* We need to:
297 * - lock the playlist
298 * - check it doesn't exist
299 * - set sharing (which will create it empty
300 * - unlock it
301 *
302 * TODO we should freeze the window while this is going on
303 */
304 disorder_eclient_playlist_lock(client, playlist_new_locked, fullname,
305 fullname);
7f7c3819
RK
306}
307
308/** @brief Called when 'cancel' is clicked in new-playlist popup */
309static void playlist_new_cancel(GtkButton attribute((unused)) *button,
310 gpointer attribute((unused)) userdata) {
311 gtk_widget_destroy(playlist_new_window);
312}
313
314/** @brief Buttons for new-playlist popup */
315static struct button playlist_new_buttons[] = {
316 {
317 .stock = GTK_STOCK_OK,
318 .clicked = playlist_new_ok,
319 .tip = "Create new playlist"
320 },
321 {
322 .stock = GTK_STOCK_CANCEL,
323 .clicked = playlist_new_cancel,
7c12e4bd 324 .tip = "Do not create new playlist"
7f7c3819
RK
325 }
326};
327#define NPLAYLIST_NEW_BUTTONS (sizeof playlist_new_buttons / sizeof *playlist_new_buttons)
328
7c12e4bd
RK
329/** @brief Test whether the new-playlist window settings are valid
330 * @return NULL on success or an error string if not
331 */
7f7c3819
RK
332static const char *playlist_new_valid(void) {
333 gboolean shared, public, private;
7c12e4bd
RK
334 char *name, *fullname;
335 playlist_new_details(&name, &fullname, &shared, &public, &private);
7f7c3819
RK
336 if(!(shared || public || private))
337 return "No type set.";
7f7c3819
RK
338 if(!*name)
339 return "";
7f7c3819 340 /* See if the result is valid */
7c12e4bd
RK
341 if(!valid_username(name)
342 || playlist_parse_name(fullname, NULL, NULL))
7f7c3819
RK
343 return "Not a valid playlist name.";
344 /* See if the result clashes with an existing name */
345 for(int n = 0; n < nplaylists; ++n)
346 if(!strcmp(playlists[n], fullname)) {
347 if(shared)
348 return "A shared playlist with that name already exists.";
349 else
350 return "You already have a playlist with that name.";
351 }
352 /* As far as we can tell creation would work */
353 return NULL;
354}
355
7c12e4bd 356/** @brief Called to update new playlist window state
7f7c3819 357 *
7c12e4bd
RK
358 * This is called whenever one the text entry or radio buttons changed, and
359 * also when the set of known playlists changes. It determines whether the new
360 * playlist would be creatable and sets the sensitivity of the OK button
361 * and info display accordingly.
7f7c3819 362 */
7c12e4bd
RK
363static void playlist_new_changed(const char attribute((unused)) *event,
364 void attribute((unused)) *eventdata,
365 void attribute((unused)) *callbackdata) {
366 if(!playlist_new_window)
367 return;
7f7c3819
RK
368 const char *reason = playlist_new_valid();
369 gtk_widget_set_sensitive(playlist_new_buttons[0].widget,
370 !reason);
371 gtk_label_set_text(GTK_LABEL(playlist_new_info), reason);
372}
373
374/** @brief Called when some radio button in the new-playlist popup changes */
375static void playlist_new_button_toggled(GtkToggleButton attribute((unused)) tb,
376 gpointer attribute((unused)) userdata) {
7c12e4bd 377 playlist_new_changed(0,0,0);
7f7c3819
RK
378}
379
380/** @brief Called when the text entry field in the new-playlist popup changes */
381static void playlist_new_entry_edited(GtkEditable attribute((unused)) *editable,
382 gpointer attribute((unused)) user_data) {
7c12e4bd 383 playlist_new_changed(0,0,0);
7f7c3819
RK
384}
385
386/** @brief Pop up a new window to enter the playlist name and details */
387static void playlist_new(void) {
388 assert(playlist_new_window == NULL);
389 playlist_new_window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
390 g_signal_connect(playlist_new_window, "destroy",
391 G_CALLBACK(gtk_widget_destroyed), &playlist_new_window);
392 gtk_window_set_title(GTK_WINDOW(playlist_new_window), "Create new playlist");
393 /* Window will be modal, suppressing access to other windows */
394 gtk_window_set_modal(GTK_WINDOW(playlist_new_window), TRUE);
7c12e4bd
RK
395 gtk_window_set_transient_for(GTK_WINDOW(playlist_new_window),
396 GTK_WINDOW(playlists_window));
7f7c3819
RK
397
398 /* Window contents will use a table (grid) layout */
399 GtkWidget *table = gtk_table_new(3, 3, FALSE/*!homogeneous*/);
400
401 /* First row: playlist name */
402 gtk_table_attach_defaults(GTK_TABLE(table),
403 gtk_label_new("Playlist name"),
404 0, 1, 0, 1);
405 playlist_new_entry = gtk_entry_new();
406 g_signal_connect(playlist_new_entry, "changed",
407 G_CALLBACK(playlist_new_entry_edited), NULL);
408 gtk_table_attach_defaults(GTK_TABLE(table),
409 playlist_new_entry,
410 1, 3, 0, 1);
411
412 /* Second row: radio buttons to choose type */
413 playlist_new_shared = gtk_radio_button_new_with_label(NULL, "shared");
414 playlist_new_public
415 = gtk_radio_button_new_with_label_from_widget(GTK_RADIO_BUTTON(playlist_new_shared),
416 "public");
417 playlist_new_private
418 = gtk_radio_button_new_with_label_from_widget(GTK_RADIO_BUTTON(playlist_new_shared),
419 "private");
420 g_signal_connect(playlist_new_shared, "toggled",
421 G_CALLBACK(playlist_new_button_toggled), NULL);
422 g_signal_connect(playlist_new_public, "toggled",
423 G_CALLBACK(playlist_new_button_toggled), NULL);
424 g_signal_connect(playlist_new_private, "toggled",
425 G_CALLBACK(playlist_new_button_toggled), NULL);
426 gtk_table_attach_defaults(GTK_TABLE(table), playlist_new_shared, 0, 1, 1, 2);
427 gtk_table_attach_defaults(GTK_TABLE(table), playlist_new_public, 1, 2, 1, 2);
428 gtk_table_attach_defaults(GTK_TABLE(table), playlist_new_private, 2, 3, 1, 2);
429
430 /* Third row: info bar saying why not */
431 playlist_new_info = gtk_label_new("");
432 gtk_table_attach_defaults(GTK_TABLE(table), playlist_new_info,
433 0, 3, 2, 3);
434
435 /* Fourth row: ok/cancel buttons */
436 GtkWidget *hbox = create_buttons_box(playlist_new_buttons,
437 NPLAYLIST_NEW_BUTTONS,
438 gtk_hbox_new(FALSE, 0));
439 gtk_table_attach_defaults(GTK_TABLE(table), hbox, 0, 3, 3, 4);
440
441 gtk_container_add(GTK_CONTAINER(playlist_new_window),
442 frame_widget(table, NULL));
443
444 /* Set initial state of OK button */
7c12e4bd 445 playlist_new_changed(0,0,0);
7f7c3819
RK
446
447 /* Display the window */
448 gtk_widget_show_all(playlist_new_window);
449}
450
121944d1
RK
451/* Playlists window (list of playlists) ------------------------------------- */
452
f0bd437a 453/** @brief (Re-)populate the playlist tree model */
506e02d8
RK
454static void playlists_fill(const char attribute((unused)) *event,
455 void attribute((unused)) *eventdata,
456 void attribute((unused)) *callbackdata) {
f0bd437a
RK
457 GtkTreeIter iter[1];
458
d571e07b
RK
459 if(!playlists_window)
460 return;
f0bd437a
RK
461 if(!playlists_list)
462 playlists_list = gtk_list_store_new(1, G_TYPE_STRING);
d571e07b
RK
463 const char *was_selected = playlists_selected;
464 gtk_list_store_clear(playlists_list); /* clears playlists_selected */
465 for(int n = 0; n < nplaylists; ++n) {
f0bd437a
RK
466 gtk_list_store_insert_with_values(playlists_list, iter, n/*position*/,
467 0, playlists[n], /* column 0 */
468 -1); /* no more cols */
d571e07b
RK
469 /* Reselect the selected playlist */
470 if(was_selected && !strcmp(was_selected, playlists[n]))
471 gtk_tree_selection_select_iter(playlists_selection, iter);
472 }
f0bd437a
RK
473}
474
475/** @brief Called when the selection might have changed */
476static void playlists_selection_changed(GtkTreeSelection attribute((unused)) *treeselection,
477 gpointer attribute((unused)) user_data) {
478 GtkTreeIter iter;
479 char *gselected, *selected;
480
481 /* Identify the current selection */
482 if(gtk_tree_selection_get_selected(playlists_selection, 0, &iter)) {
483 gtk_tree_model_get(GTK_TREE_MODEL(playlists_list), &iter,
484 0, &gselected, -1);
485 selected = xstrdup(gselected);
486 g_free(gselected);
487 } else
488 selected = 0;
d9b141cc
RK
489 /* Set button sensitivity according to the new state */
490 if(selected)
7c12e4bd
RK
491 gtk_widget_set_sensitive(playlists_delete_button, 1);
492 else
493 gtk_widget_set_sensitive(playlists_delete_button, 0);
f0bd437a
RK
494 /* Eliminate no-change cases */
495 if(!selected && !playlists_selected)
496 return;
497 if(selected && playlists_selected && !strcmp(selected, playlists_selected))
498 return;
d9b141cc
RK
499 /* Record the new state */
500 playlists_selected = selected;
f0bd437a
RK
501}
502
503/** @brief Called when the 'add' button is pressed */
504static void playlists_add(GtkButton attribute((unused)) *button,
505 gpointer attribute((unused)) userdata) {
7c12e4bd 506 /* Unselect whatever is selected TODO why?? */
f0bd437a 507 gtk_tree_selection_unselect_all(playlists_selection);
7f7c3819 508 playlist_new();
f0bd437a
RK
509}
510
d9b141cc
RK
511/** @brief Called when playlist deletion completes */
512static void playlists_delete_completed(void attribute((unused)) *v,
513 const char *err) {
514 if(err)
515 popup_protocol_error(0, err);
516}
517
f0bd437a
RK
518/** @brief Called when the 'Delete' button is pressed */
519static void playlists_delete(GtkButton attribute((unused)) *button,
c5782050 520 gpointer attribute((unused)) userdata) {
f0bd437a
RK
521 GtkWidget *yesno;
522 int res;
523
7c12e4bd 524 fprintf(stderr, "playlists_delete\n");
f0bd437a
RK
525 if(!playlists_selected)
526 return; /* shouldn't happen */
527 yesno = gtk_message_dialog_new(GTK_WINDOW(playlists_window),
528 GTK_DIALOG_MODAL,
529 GTK_MESSAGE_QUESTION,
530 GTK_BUTTONS_YES_NO,
c5782050 531 "Do you really want to delete playlist %s?"
f0bd437a
RK
532 " This action cannot be undone.",
533 playlists_selected);
534 res = gtk_dialog_run(GTK_DIALOG(yesno));
535 gtk_widget_destroy(yesno);
536 if(res == GTK_RESPONSE_YES) {
537 disorder_eclient_playlist_delete(client,
d9b141cc 538 playlists_delete_completed,
f0bd437a
RK
539 playlists_selected,
540 NULL);
541 }
542}
543
544/** @brief Table of buttons below the playlist list */
545static struct button playlists_buttons[] = {
546 {
547 GTK_STOCK_ADD,
548 playlists_add,
549 "Create a new playlist",
550 0
551 },
552 {
553 GTK_STOCK_REMOVE,
554 playlists_delete,
555 "Delete a playlist",
556 0
557 },
558};
559#define NPLAYLISTS_BUTTONS (sizeof playlists_buttons / sizeof *playlists_buttons)
560
506e02d8
RK
561/** @brief Create the list of playlists for the edit playlists window */
562static GtkWidget *playlists_window_list(void) {
563 /* Create the list of playlist and populate it */
564 playlists_fill(NULL, NULL, NULL);
565 /* Create the tree view */
566 GtkWidget *tree = gtk_tree_view_new_with_model(GTK_TREE_MODEL(playlists_list));
567 /* ...and the renderers for it */
568 GtkCellRenderer *cr = gtk_cell_renderer_text_new();
569 GtkTreeViewColumn *col = gtk_tree_view_column_new_with_attributes("Playlist",
570 cr,
571 "text", 0,
572 NULL);
573 gtk_tree_view_append_column(GTK_TREE_VIEW(tree), col);
574 /* Get the selection for the view; set its mode; arrange for a callback when
575 * it changes */
576 playlists_selected = NULL;
577 playlists_selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(tree));
578 gtk_tree_selection_set_mode(playlists_selection, GTK_SELECTION_BROWSE);
579 g_signal_connect(playlists_selection, "changed",
580 G_CALLBACK(playlists_selection_changed), NULL);
581
582 /* Create the control buttons */
583 GtkWidget *buttons = create_buttons_box(playlists_buttons,
584 NPLAYLISTS_BUTTONS,
585 gtk_hbox_new(FALSE, 1));
586 playlists_delete_button = playlists_buttons[1].widget;
587
7c12e4bd
RK
588 playlists_selection_changed(NULL, NULL);
589
506e02d8
RK
590 /* Buttons live below the list */
591 GtkWidget *vbox = gtk_vbox_new(FALSE, 0);
592 gtk_box_pack_start(GTK_BOX(vbox), scroll_widget(tree), TRUE/*expand*/, TRUE/*fill*/, 0);
593 gtk_box_pack_start(GTK_BOX(vbox), buttons, FALSE/*expand*/, FALSE, 0);
594
595 return vbox;
596}
597
598/* Playlists window (edit current playlist) --------------------------------- */
599
600static GtkWidget *playlists_window_edit(void) {
6bfaa2a1
RK
601 assert(ql_playlist.view == NULL); /* better not be set up already */
602 GtkWidget *w = init_queuelike(&ql_playlist);
603 return w;
506e02d8
RK
604}
605
606/* Playlists window --------------------------------------------------------- */
607
f0bd437a
RK
608/** @brief Keypress handler */
609static gboolean playlists_keypress(GtkWidget attribute((unused)) *widget,
610 GdkEventKey *event,
611 gpointer attribute((unused)) user_data) {
612 if(event->state)
613 return FALSE;
614 switch(event->keyval) {
615 case GDK_Escape:
616 gtk_widget_destroy(playlists_window);
617 return TRUE;
618 default:
619 return FALSE;
620 }
621}
622
6bfaa2a1
RK
623/** @brief Called when the playlist window is destroyed */
624static void playlists_window_destroyed(GtkWidget attribute((unused)) *widget,
625 GtkWidget **widget_pointer) {
6bfaa2a1
RK
626 destroy_queuelike(&ql_playlist);
627 *widget_pointer = NULL;
628}
629
7de5fbb9 630/** @brief Pop up the playlists window
121944d1
RK
631 *
632 * Called when the playlists menu item is selected
633 */
f0bd437a 634void edit_playlists(gpointer attribute((unused)) callback_data,
7c12e4bd
RK
635 guint attribute((unused)) callback_action,
636 GtkWidget attribute((unused)) *menu_item) {
f0bd437a
RK
637 /* If the window already exists, raise it */
638 if(playlists_window) {
639 gtk_window_present(GTK_WINDOW(playlists_window));
640 return;
641 }
642 /* Create the window */
643 playlists_window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
644 gtk_widget_set_style(playlists_window, tool_style);
645 g_signal_connect(playlists_window, "destroy",
6bfaa2a1 646 G_CALLBACK(playlists_window_destroyed), &playlists_window);
f0bd437a
RK
647 gtk_window_set_title(GTK_WINDOW(playlists_window), "Playlists Management");
648 /* TODO loads of this is very similar to (copied from!) users.c - can we
649 * de-dupe? */
650 /* Keyboard shortcuts */
651 g_signal_connect(playlists_window, "key-press-event",
652 G_CALLBACK(playlists_keypress), 0);
653 /* default size is too small */
7f7c3819 654 gtk_window_set_default_size(GTK_WINDOW(playlists_window), 512, 240);
f0bd437a 655
506e02d8
RK
656 GtkWidget *hbox = gtk_hbox_new(FALSE, 0);
657 gtk_box_pack_start(GTK_BOX(hbox), playlists_window_list(),
658 FALSE/*expand*/, FALSE, 0);
659 gtk_box_pack_start(GTK_BOX(hbox), gtk_event_box_new(),
660 FALSE/*expand*/, FALSE, 2);
661 gtk_box_pack_start(GTK_BOX(hbox), playlists_window_edit(),
662 TRUE/*expand*/, TRUE/*fill*/, 0);
f0bd437a 663
f0bd437a
RK
664 gtk_container_add(GTK_CONTAINER(playlists_window), frame_widget(hbox, NULL));
665 gtk_widget_show_all(playlists_window);
f9b20469
RK
666}
667
fc36ecb7
RK
668/** @brief Initialize playlist support */
669void playlists_init(void) {
670 /* We re-get all playlists upon any change... */
671 event_register("playlist-created", playlists_update, 0);
672 event_register("playlist-modified", playlists_update, 0);
673 event_register("playlist-deleted", playlists_update, 0);
674 /* ...and on reconnection */
675 event_register("log-connected", playlists_update, 0);
676 /* ...and from time to time */
677 event_register("periodic-slow", playlists_update, 0);
678 /* ...and at startup */
7c12e4bd
RK
679
680 /* Update the playlists menu when the set of playlists changes */
f9b20469 681 event_register("playlists-updated", menu_playlists_changed, 0);
7c12e4bd
RK
682 /* Update the new-playlist OK button when the set of playlists changes */
683 event_register("playlists-updated", playlist_new_changed, 0);
d9b141cc
RK
684 /* Update the list of playlists in the edit window when the set changes */
685 event_register("playlists-updated", playlists_fill, 0);
fc36ecb7
RK
686 playlists_update(0, 0, 0);
687}
688
0b0fb26b
RK
689#endif
690
fc36ecb7
RK
691/*
692Local Variables:
693c-basic-offset:2
694comment-column:40
695fill-column:79
696indent-tabs-mode:nil
697End:
698*/