chiark / gitweb /
Fix playlist deletion and some update logic
[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
459 if(!playlists_list)
460 playlists_list = gtk_list_store_new(1, G_TYPE_STRING);
461 gtk_list_store_clear(playlists_list);
462 for(int n = 0; n < nplaylists; ++n)
463 gtk_list_store_insert_with_values(playlists_list, iter, n/*position*/,
464 0, playlists[n], /* column 0 */
465 -1); /* no more cols */
466 // TODO reselect whatever was formerly selected if possible, if not then
467 // zap the contents view
468}
469
470/** @brief Called when the selection might have changed */
471static void playlists_selection_changed(GtkTreeSelection attribute((unused)) *treeselection,
472 gpointer attribute((unused)) user_data) {
473 GtkTreeIter iter;
474 char *gselected, *selected;
475
476 /* Identify the current selection */
477 if(gtk_tree_selection_get_selected(playlists_selection, 0, &iter)) {
478 gtk_tree_model_get(GTK_TREE_MODEL(playlists_list), &iter,
479 0, &gselected, -1);
480 selected = xstrdup(gselected);
481 g_free(gselected);
482 } else
483 selected = 0;
d9b141cc
RK
484 /* Set button sensitivity according to the new state */
485 if(selected)
7c12e4bd
RK
486 gtk_widget_set_sensitive(playlists_delete_button, 1);
487 else
488 gtk_widget_set_sensitive(playlists_delete_button, 0);
f0bd437a
RK
489 /* Eliminate no-change cases */
490 if(!selected && !playlists_selected)
491 return;
492 if(selected && playlists_selected && !strcmp(selected, playlists_selected))
493 return;
d9b141cc
RK
494 /* Record the new state */
495 playlists_selected = selected;
f0bd437a
RK
496}
497
498/** @brief Called when the 'add' button is pressed */
499static void playlists_add(GtkButton attribute((unused)) *button,
500 gpointer attribute((unused)) userdata) {
7c12e4bd 501 /* Unselect whatever is selected TODO why?? */
f0bd437a 502 gtk_tree_selection_unselect_all(playlists_selection);
7f7c3819 503 playlist_new();
f0bd437a
RK
504}
505
d9b141cc
RK
506/** @brief Called when playlist deletion completes */
507static void playlists_delete_completed(void attribute((unused)) *v,
508 const char *err) {
509 if(err)
510 popup_protocol_error(0, err);
511}
512
f0bd437a
RK
513/** @brief Called when the 'Delete' button is pressed */
514static void playlists_delete(GtkButton attribute((unused)) *button,
c5782050 515 gpointer attribute((unused)) userdata) {
f0bd437a
RK
516 GtkWidget *yesno;
517 int res;
518
7c12e4bd 519 fprintf(stderr, "playlists_delete\n");
f0bd437a
RK
520 if(!playlists_selected)
521 return; /* shouldn't happen */
522 yesno = gtk_message_dialog_new(GTK_WINDOW(playlists_window),
523 GTK_DIALOG_MODAL,
524 GTK_MESSAGE_QUESTION,
525 GTK_BUTTONS_YES_NO,
c5782050 526 "Do you really want to delete playlist %s?"
f0bd437a
RK
527 " This action cannot be undone.",
528 playlists_selected);
529 res = gtk_dialog_run(GTK_DIALOG(yesno));
530 gtk_widget_destroy(yesno);
531 if(res == GTK_RESPONSE_YES) {
532 disorder_eclient_playlist_delete(client,
d9b141cc 533 playlists_delete_completed,
f0bd437a
RK
534 playlists_selected,
535 NULL);
536 }
537}
538
539/** @brief Table of buttons below the playlist list */
540static struct button playlists_buttons[] = {
541 {
542 GTK_STOCK_ADD,
543 playlists_add,
544 "Create a new playlist",
545 0
546 },
547 {
548 GTK_STOCK_REMOVE,
549 playlists_delete,
550 "Delete a playlist",
551 0
552 },
553};
554#define NPLAYLISTS_BUTTONS (sizeof playlists_buttons / sizeof *playlists_buttons)
555
506e02d8
RK
556/** @brief Create the list of playlists for the edit playlists window */
557static GtkWidget *playlists_window_list(void) {
558 /* Create the list of playlist and populate it */
559 playlists_fill(NULL, NULL, NULL);
560 /* Create the tree view */
561 GtkWidget *tree = gtk_tree_view_new_with_model(GTK_TREE_MODEL(playlists_list));
562 /* ...and the renderers for it */
563 GtkCellRenderer *cr = gtk_cell_renderer_text_new();
564 GtkTreeViewColumn *col = gtk_tree_view_column_new_with_attributes("Playlist",
565 cr,
566 "text", 0,
567 NULL);
568 gtk_tree_view_append_column(GTK_TREE_VIEW(tree), col);
569 /* Get the selection for the view; set its mode; arrange for a callback when
570 * it changes */
571 playlists_selected = NULL;
572 playlists_selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(tree));
573 gtk_tree_selection_set_mode(playlists_selection, GTK_SELECTION_BROWSE);
574 g_signal_connect(playlists_selection, "changed",
575 G_CALLBACK(playlists_selection_changed), NULL);
576
577 /* Create the control buttons */
578 GtkWidget *buttons = create_buttons_box(playlists_buttons,
579 NPLAYLISTS_BUTTONS,
580 gtk_hbox_new(FALSE, 1));
581 playlists_delete_button = playlists_buttons[1].widget;
582
7c12e4bd
RK
583 playlists_selection_changed(NULL, NULL);
584
506e02d8
RK
585 /* Buttons live below the list */
586 GtkWidget *vbox = gtk_vbox_new(FALSE, 0);
587 gtk_box_pack_start(GTK_BOX(vbox), scroll_widget(tree), TRUE/*expand*/, TRUE/*fill*/, 0);
588 gtk_box_pack_start(GTK_BOX(vbox), buttons, FALSE/*expand*/, FALSE, 0);
589
590 return vbox;
591}
592
593/* Playlists window (edit current playlist) --------------------------------- */
594
595static GtkWidget *playlists_window_edit(void) {
6bfaa2a1
RK
596 assert(ql_playlist.view == NULL); /* better not be set up already */
597 GtkWidget *w = init_queuelike(&ql_playlist);
598 return w;
506e02d8
RK
599}
600
601/* Playlists window --------------------------------------------------------- */
602
f0bd437a
RK
603/** @brief Keypress handler */
604static gboolean playlists_keypress(GtkWidget attribute((unused)) *widget,
605 GdkEventKey *event,
606 gpointer attribute((unused)) user_data) {
607 if(event->state)
608 return FALSE;
609 switch(event->keyval) {
610 case GDK_Escape:
611 gtk_widget_destroy(playlists_window);
612 return TRUE;
613 default:
614 return FALSE;
615 }
616}
617
6bfaa2a1
RK
618/** @brief Called when the playlist window is destroyed */
619static void playlists_window_destroyed(GtkWidget attribute((unused)) *widget,
620 GtkWidget **widget_pointer) {
6bfaa2a1
RK
621 destroy_queuelike(&ql_playlist);
622 *widget_pointer = NULL;
623}
624
7de5fbb9 625/** @brief Pop up the playlists window
121944d1
RK
626 *
627 * Called when the playlists menu item is selected
628 */
f0bd437a 629void edit_playlists(gpointer attribute((unused)) callback_data,
7c12e4bd
RK
630 guint attribute((unused)) callback_action,
631 GtkWidget attribute((unused)) *menu_item) {
f0bd437a
RK
632 /* If the window already exists, raise it */
633 if(playlists_window) {
634 gtk_window_present(GTK_WINDOW(playlists_window));
635 return;
636 }
637 /* Create the window */
638 playlists_window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
639 gtk_widget_set_style(playlists_window, tool_style);
640 g_signal_connect(playlists_window, "destroy",
6bfaa2a1 641 G_CALLBACK(playlists_window_destroyed), &playlists_window);
f0bd437a
RK
642 gtk_window_set_title(GTK_WINDOW(playlists_window), "Playlists Management");
643 /* TODO loads of this is very similar to (copied from!) users.c - can we
644 * de-dupe? */
645 /* Keyboard shortcuts */
646 g_signal_connect(playlists_window, "key-press-event",
647 G_CALLBACK(playlists_keypress), 0);
648 /* default size is too small */
7f7c3819 649 gtk_window_set_default_size(GTK_WINDOW(playlists_window), 512, 240);
f0bd437a 650
506e02d8
RK
651 GtkWidget *hbox = gtk_hbox_new(FALSE, 0);
652 gtk_box_pack_start(GTK_BOX(hbox), playlists_window_list(),
653 FALSE/*expand*/, FALSE, 0);
654 gtk_box_pack_start(GTK_BOX(hbox), gtk_event_box_new(),
655 FALSE/*expand*/, FALSE, 2);
656 gtk_box_pack_start(GTK_BOX(hbox), playlists_window_edit(),
657 TRUE/*expand*/, TRUE/*fill*/, 0);
f0bd437a 658
f0bd437a
RK
659 gtk_container_add(GTK_CONTAINER(playlists_window), frame_widget(hbox, NULL));
660 gtk_widget_show_all(playlists_window);
f9b20469
RK
661}
662
fc36ecb7
RK
663/** @brief Initialize playlist support */
664void playlists_init(void) {
665 /* We re-get all playlists upon any change... */
666 event_register("playlist-created", playlists_update, 0);
667 event_register("playlist-modified", playlists_update, 0);
668 event_register("playlist-deleted", playlists_update, 0);
669 /* ...and on reconnection */
670 event_register("log-connected", playlists_update, 0);
671 /* ...and from time to time */
672 event_register("periodic-slow", playlists_update, 0);
673 /* ...and at startup */
7c12e4bd
RK
674
675 /* Update the playlists menu when the set of playlists changes */
f9b20469 676 event_register("playlists-updated", menu_playlists_changed, 0);
7c12e4bd
RK
677 /* Update the new-playlist OK button when the set of playlists changes */
678 event_register("playlists-updated", playlist_new_changed, 0);
d9b141cc
RK
679 /* Update the list of playlists in the edit window when the set changes */
680 event_register("playlists-updated", playlists_fill, 0);
fc36ecb7
RK
681 playlists_update(0, 0, 0);
682}
683
0b0fb26b
RK
684#endif
685
fc36ecb7
RK
686/*
687Local Variables:
688c-basic-offset:2
689comment-column:40
690fill-column:79
691indent-tabs-mode:nil
692End:
693*/