chiark / gitweb /
Work on playlist right-click menu
[disorder] / disobedience / playlists.c
1 /*
2  * This file is part of DisOrder
3  * Copyright (C) 2008, 2009 Richard Kettlewell
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 support for Disobedience
22  *
23  * The playlists management window contains:
24  * - the playlist picker (a list of all playlists) TODO should be a tree!
25  * - an add button
26  * - a delete button
27  * - the playlist editor (a d+d-capable view of the currently picked playlist)
28  * - a close button   TODO
29  *
30  * This file also maintains the playlist menu, allowing playlists to be
31  * activated from the main window's menu.
32  *
33  * Internally we maintain the playlist list, which is just the current list of
34  * playlists.  Changes to this are reflected in the playlist menu and the
35  * playlist picker.
36  *
37  */
38 #include "disobedience.h"
39 #include "queue-generic.h"
40 #include "popup.h"
41 #include "validity.h"
42
43 #if PLAYLISTS
44
45 static void playlist_list_received_playlists(void *v,
46                                              const char *err,
47                                              int nvec, char **vec);
48 static void playlist_editor_fill(const char *event,
49                                  void *eventdata,
50                                  void *callbackdata);
51 static int playlist_playall_sensitive(void *extra);
52 static void playlist_playall_activate(GtkMenuItem *menuitem,
53                                       gpointer user_data);
54 static int playlist_remove_sensitive(void *extra) ;
55 static void playlist_remove_activate(GtkMenuItem *menuitem,
56                                      gpointer user_data);
57
58 /** @brief Playlist editing window */
59 static GtkWidget *playlist_window;
60
61 /** @brief Columns for the playlist editor */
62 static const struct queue_column playlist_columns[] = {
63   { "Artist", column_namepart, "artist", COL_EXPAND|COL_ELLIPSIZE },
64   { "Album",  column_namepart, "album",  COL_EXPAND|COL_ELLIPSIZE },
65   { "Title",  column_namepart, "title",  COL_EXPAND|COL_ELLIPSIZE },
66   { "Length", column_length,   0,        COL_RIGHT }
67 };
68
69 /** @brief Pop-up menu for playlist editor
70  *
71  * Status:
72  * - track properties works but, bizarrely, raises the main window
73  * - play track works
74  * - play playlist works
75  * - select/deselect all work
76  */
77 static struct menuitem playlist_menuitems[] = {
78   { "Track properties", ql_properties_activate, ql_properties_sensitive, 0, 0 },
79   { "Play track", ql_play_activate, ql_play_sensitive, 0, 0 },
80   { "Play playlist", playlist_playall_activate, playlist_playall_sensitive, 0, 0 },
81   { "Remove track from queue", playlist_remove_activate, playlist_remove_sensitive, 0, 0 },
82   { "Select all tracks", ql_selectall_activate, ql_selectall_sensitive, 0, 0 },
83   { "Deselect all tracks", ql_selectnone_activate, ql_selectnone_sensitive, 0, 0 },
84 };
85
86 /** @brief Queuelike for editing a playlist */
87 static struct queuelike ql_playlist = {
88   .name = "playlist",
89   .columns = playlist_columns,
90   .ncolumns = sizeof playlist_columns / sizeof *playlist_columns,
91   .menuitems = playlist_menuitems,
92   .nmenuitems = sizeof playlist_menuitems / sizeof *playlist_menuitems,
93   //.drop = playlist_drop  //TODO
94 };
95
96 /* Maintaining the list of playlists ---------------------------------------- */
97
98 /** @brief Current list of playlists or NULL */
99 char **playlists;
100
101 /** @brief Count of playlists */
102 int nplaylists;
103
104 /** @brief Schedule an update to the list of playlists
105  *
106  * Called periodically and when a playlist is created or deleted.
107  */
108 static void playlist_list_update(const char attribute((unused)) *event,
109                                  void attribute((unused)) *eventdata,
110                                  void attribute((unused)) *callbackdata) {
111   disorder_eclient_playlists(client, playlist_list_received_playlists, 0);
112 }
113
114 /** @brief qsort() callback for playlist name comparison */
115 static int playlistcmp(const void *ap, const void *bp) {
116   const char *a = *(char **)ap, *b = *(char **)bp;
117   const char *ad = strchr(a, '.'), *bd = strchr(b, '.');
118   int c;
119
120   /* Group owned playlists by owner */
121   if(ad && bd) {
122     const int adn = ad - a, bdn = bd - b;
123     if((c = strncmp(a, b, adn < bdn ? adn : bdn)))
124       return c;
125     /* Lexical order within playlists of a single owner */
126     return strcmp(ad + 1, bd + 1);
127   }
128
129   /* Owned playlists after shared ones */
130   if(ad) {
131     return 1;
132   } else if(bd) {
133     return -1;
134   }
135
136   /* Lexical order of shared playlists */
137   return strcmp(a, b);
138 }
139
140 /** @brief Called with a new list of playlists */
141 static void playlist_list_received_playlists(void attribute((unused)) *v,
142                                              const char *err,
143                                              int nvec, char **vec) {
144   if(err) {
145     playlists = 0;
146     nplaylists = -1;
147     /* Probably means server does not support playlists */
148   } else {
149     playlists = vec;
150     nplaylists = nvec;
151     qsort(playlists, nplaylists, sizeof (char *), playlistcmp);
152   }
153   /* Tell our consumers */
154   event_raise("playlists-updated", 0);
155 }
156
157 /* Playlists menu ----------------------------------------------------------- */
158
159 static void playlist_menu_playing(void attribute((unused)) *v,
160                                   const char *err) {
161   if(err)
162     popup_protocol_error(0, err);
163 }
164
165 /** @brief Play received playlist contents
166  *
167  * Passed as a completion callback by menu_activate_playlist().
168  */
169 static void playlist_menu_received_content(void attribute((unused)) *v,
170                                            const char *err,
171                                            int nvec, char **vec) {
172   if(err) {
173     popup_protocol_error(0, err);
174     return;
175   }
176   for(int n = 0; n < nvec; ++n)
177     disorder_eclient_play(client, vec[n], playlist_menu_playing, NULL);
178 }
179
180 /** @brief Called to activate a playlist
181  *
182  * Called when the menu item for a playlist is clicked.
183  */
184 static void playlist_menu_activate(GtkMenuItem *menuitem,
185                                    gpointer attribute((unused)) user_data) {
186   GtkLabel *label = GTK_LABEL(GTK_BIN(menuitem)->child);
187   const char *playlist = gtk_label_get_text(label);
188
189   disorder_eclient_playlist_get(client, playlist_menu_received_content,
190                                 playlist, NULL);
191 }
192
193 /** @brief Called when the playlists change
194  *
195  * Naively refills the menu.  The results might be unsettling if the menu is
196  * currently open, but this is hopefuly fairly rare.
197  */
198 static void playlist_menu_changed(const char attribute((unused)) *event,
199                                   void attribute((unused)) *eventdata,
200                                   void attribute((unused)) *callbackdata) {
201   if(!playlists_menu)
202     return;                             /* OMG too soon */
203   GtkMenuShell *menu = GTK_MENU_SHELL(playlists_menu);
204   while(menu->children)
205     gtk_container_remove(GTK_CONTAINER(menu), GTK_WIDGET(menu->children->data));
206   /* NB nplaylists can be -1 as well as 0 */
207   for(int n = 0; n < nplaylists; ++n) {
208     GtkWidget *w = gtk_menu_item_new_with_label(playlists[n]);
209     g_signal_connect(w, "activate", G_CALLBACK(playlist_menu_activate), 0);
210     gtk_widget_show(w);
211     gtk_menu_shell_append(menu, w);
212   }
213   gtk_widget_set_sensitive(menu_playlists_widget,
214                            nplaylists > 0);
215   gtk_widget_set_sensitive(menu_editplaylists_widget,
216                            nplaylists >= 0);
217 }
218
219 /* Popup to create a new playlist ------------------------------------------- */
220
221 /** @brief New-playlist popup */
222 static GtkWidget *playlist_new_window;
223
224 /** @brief Text entry in new-playlist popup */
225 static GtkWidget *playlist_new_entry;
226
227 /** @brief Label for displaying feedback on what's wrong */
228 static GtkWidget *playlist_new_info;
229
230 /** @brief "Shared" radio button */
231 static GtkWidget *playlist_new_shared;
232
233 /** @brief "Public" radio button */
234 static GtkWidget *playlist_new_public;
235
236 /** @brief "Private" radio button */
237 static GtkWidget *playlist_new_private;
238
239 /** @brief Get entered new-playlist details
240  * @param namep Where to store entered name (or NULL)
241  * @param fullnamep Where to store computed full name (or NULL)
242  * @param sharep Where to store 'shared' flag (or NULL)
243  * @param publicp Where to store 'public' flag (or NULL)
244  * @param privatep Where to store 'private' flag (or NULL)
245  */
246 static void playlist_new_details(char **namep,
247                                  char **fullnamep,
248                                  gboolean *sharedp,
249                                  gboolean *publicp,
250                                  gboolean *privatep) {
251   gboolean shared, public, private;
252   g_object_get(playlist_new_shared, "active", &shared, (char *)NULL);
253   g_object_get(playlist_new_public, "active", &public, (char *)NULL);
254   g_object_get(playlist_new_private, "active", &private, (char *)NULL);
255   char *gname = gtk_editable_get_chars(GTK_EDITABLE(playlist_new_entry),
256                                        0, -1); /* name owned by calle */
257   char *name = xstrdup(gname);
258   g_free(gname);
259   if(sharedp) *sharedp = shared;
260   if(publicp) *publicp = public;
261   if(privatep) *privatep = private;
262   if(namep) *namep = name;
263   if(fullnamep) {
264     if(*sharedp) *fullnamep = *namep;
265     else byte_xasprintf(fullnamep, "%s.%s", config->username, name);
266   }
267 }
268
269 /** @brief Called when the newly created playlist has unlocked */
270 static void playlist_new_unlocked(void attribute((unused)) *v, const char *err) {
271   if(err)
272     popup_protocol_error(0, err);
273   /* Pop down the creation window */
274   gtk_widget_destroy(playlist_new_window);
275 }
276
277 /** @brief Called when the new playlist has been created */
278 static void playlist_new_created(void attribute((unused)) *v, const char *err) {
279   if(err) {
280     popup_protocol_error(0, err);
281     return;
282   }
283   disorder_eclient_playlist_unlock(client, playlist_new_unlocked, NULL);
284   // TODO arrange for the new playlist to be selected
285 }
286
287 /** @brief Called when the proposed new playlist's contents have been retrieved
288  *
289  * ...or rather, normally, when it's been reported that it does not exist.
290  */
291 static void playlist_new_retrieved(void *v, const char *err,
292                                    int nvec,
293                                    char attribute((unused)) **vec) {
294   char *fullname = v;
295   if(!err && nvec != -1)
296     /* A rare case but not in principle impossible */
297     err = "A playlist with that name already exists.";
298   if(err) {
299     popup_protocol_error(0, err);
300     disorder_eclient_playlist_unlock(client, playlist_new_unlocked, fullname);
301     return;
302   }
303   gboolean shared, public, private;
304   playlist_new_details(0, 0, &shared, &public, &private);
305   disorder_eclient_playlist_set_share(client, playlist_new_created, fullname,
306                                       public ? "public"
307                                       : private ? "private"
308                                       : "shared",
309                                       fullname);
310 }
311
312 /** @brief Called when the proposed new playlist has been locked */
313 static void playlist_new_locked(void *v, const char *err) {
314   char *fullname = v;
315   if(err) {
316     popup_protocol_error(0, err);
317     return;
318   }
319   disorder_eclient_playlist_get(client, playlist_new_retrieved,
320                                 fullname, fullname);
321 }
322
323 /** @brief Called when 'ok' is clicked in new-playlist popup */
324 static void playlist_new_ok(GtkButton attribute((unused)) *button,
325                             gpointer attribute((unused)) userdata) {
326   gboolean shared, public, private;
327   char *name, *fullname;
328   playlist_new_details(&name, &fullname, &shared, &public, &private);
329
330   /* We need to:
331    * - lock the playlist
332    * - check it doesn't exist
333    * - set sharing (which will create it empty
334    * - unlock it
335    *
336    * TODO we should freeze the window while this is going on to stop a second
337    * click.
338    */
339   disorder_eclient_playlist_lock(client, playlist_new_locked, fullname,
340                                  fullname);
341 }
342
343 /** @brief Called when 'cancel' is clicked in new-playlist popup */
344 static void playlist_new_cancel(GtkButton attribute((unused)) *button,
345                                 gpointer attribute((unused)) userdata) {
346   gtk_widget_destroy(playlist_new_window);
347 }
348
349 /** @brief Buttons for new-playlist popup */
350 static struct button playlist_new_buttons[] = {
351   {
352     .stock = GTK_STOCK_OK,
353     .clicked = playlist_new_ok,
354     .tip = "Create new playlist"
355   },
356   {
357     .stock = GTK_STOCK_CANCEL,
358     .clicked = playlist_new_cancel,
359     .tip = "Do not create new playlist"
360   }
361 };
362 #define NPLAYLIST_NEW_BUTTONS (sizeof playlist_new_buttons / sizeof *playlist_new_buttons)
363
364 /** @brief Test whether the new-playlist window settings are valid
365  * @return NULL on success or an error string if not
366  */
367 static const char *playlist_new_valid(void) {
368   gboolean shared, public, private;
369   char *name, *fullname;
370   playlist_new_details(&name, &fullname, &shared, &public, &private);
371   if(!(shared || public || private))
372     return "No type set.";
373   if(!*name)
374     return "";
375   /* See if the result is valid */
376   if(!valid_username(name)
377      || playlist_parse_name(fullname, NULL, NULL))
378     return "Not a valid playlist name.";
379   /* See if the result clashes with an existing name.  This is not a perfect
380    * check, the playlist might be created after this point but before we get a
381    * chance to disable the "OK" button.  However when we try to create the
382    * playlist we will first try to retrieve it, with a lock held, so we
383    * shouldn't end up overwriting anything. */
384   for(int n = 0; n < nplaylists; ++n)
385     if(!strcmp(playlists[n], fullname)) {
386       if(shared)
387         return "A shared playlist with that name already exists.";
388       else
389         return "You already have a playlist with that name.";
390     }
391   /* As far as we can tell creation would work */
392   return NULL;
393 }
394
395 /** @brief Called to update new playlist window state
396  *
397  * This is called whenever one the text entry or radio buttons changed, and
398  * also when the set of known playlists changes.  It determines whether the new
399  * playlist would be creatable and sets the sensitivity of the OK button
400  * and info display accordingly.
401  */
402 static void playlist_new_changed(const char attribute((unused)) *event,
403                                  void attribute((unused)) *eventdata,
404                                  void attribute((unused)) *callbackdata) {
405   if(!playlist_new_window)
406     return;
407   const char *reason = playlist_new_valid();
408   gtk_widget_set_sensitive(playlist_new_buttons[0].widget,
409                            !reason);
410   gtk_label_set_text(GTK_LABEL(playlist_new_info), reason);
411 }
412
413 /** @brief Called when some radio button in the new-playlist popup changes */
414 static void playlist_new_button_toggled(GtkToggleButton attribute((unused)) tb,
415                                         gpointer attribute((unused)) userdata) {
416   playlist_new_changed(0,0,0);
417 }
418
419 /** @brief Called when the text entry field in the new-playlist popup changes */
420 static void playlist_new_entry_edited(GtkEditable attribute((unused)) *editable,
421                                       gpointer attribute((unused)) user_data) {
422   playlist_new_changed(0,0,0);
423 }
424
425 /** @brief Pop up a new window to enter the playlist name and details */
426 static void playlist_new_playlist(void) {
427   assert(playlist_new_window == NULL);
428   playlist_new_window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
429   g_signal_connect(playlist_new_window, "destroy",
430                    G_CALLBACK(gtk_widget_destroyed), &playlist_new_window);
431   gtk_window_set_title(GTK_WINDOW(playlist_new_window), "Create new playlist");
432   /* Window will be modal, suppressing access to other windows */
433   gtk_window_set_modal(GTK_WINDOW(playlist_new_window), TRUE);
434   gtk_window_set_transient_for(GTK_WINDOW(playlist_new_window),
435                                GTK_WINDOW(playlist_window));
436
437   /* Window contents will use a table (grid) layout */
438   GtkWidget *table = gtk_table_new(3, 3, FALSE/*!homogeneous*/);
439
440   /* First row: playlist name */
441   gtk_table_attach_defaults(GTK_TABLE(table),
442                             gtk_label_new("Playlist name"),
443                             0, 1, 0, 1);
444   playlist_new_entry = gtk_entry_new();
445   g_signal_connect(playlist_new_entry, "changed",
446                    G_CALLBACK(playlist_new_entry_edited), NULL);
447   gtk_table_attach_defaults(GTK_TABLE(table),
448                             playlist_new_entry,
449                             1, 3, 0, 1);
450
451   /* Second row: radio buttons to choose type */
452   playlist_new_shared = gtk_radio_button_new_with_label(NULL, "shared");
453   playlist_new_public
454     = gtk_radio_button_new_with_label_from_widget(GTK_RADIO_BUTTON(playlist_new_shared),
455                                                   "public");
456   playlist_new_private
457     = gtk_radio_button_new_with_label_from_widget(GTK_RADIO_BUTTON(playlist_new_shared),
458                                                   "private");
459   g_signal_connect(playlist_new_shared, "toggled",
460                    G_CALLBACK(playlist_new_button_toggled), NULL);
461   g_signal_connect(playlist_new_public, "toggled",
462                    G_CALLBACK(playlist_new_button_toggled), NULL);
463   g_signal_connect(playlist_new_private, "toggled",
464                    G_CALLBACK(playlist_new_button_toggled), NULL);
465   gtk_table_attach_defaults(GTK_TABLE(table), playlist_new_shared, 0, 1, 1, 2);
466   gtk_table_attach_defaults(GTK_TABLE(table), playlist_new_public, 1, 2, 1, 2);
467   gtk_table_attach_defaults(GTK_TABLE(table), playlist_new_private, 2, 3, 1, 2);
468
469   /* Third row: info bar saying why not */
470   playlist_new_info = gtk_label_new("");
471   gtk_table_attach_defaults(GTK_TABLE(table), playlist_new_info,
472                             0, 3, 2, 3);
473
474   /* Fourth row: ok/cancel buttons */
475   GtkWidget *hbox = create_buttons_box(playlist_new_buttons,
476                                        NPLAYLIST_NEW_BUTTONS,
477                                        gtk_hbox_new(FALSE, 0));
478   gtk_table_attach_defaults(GTK_TABLE(table), hbox, 0, 3, 3, 4);
479
480   gtk_container_add(GTK_CONTAINER(playlist_new_window),
481                     frame_widget(table, NULL));
482
483   /* Set initial state of OK button */
484   playlist_new_changed(0,0,0);
485
486   /* TODO: return should = OK, escape should = cancel */
487   
488   /* Display the window */
489   gtk_widget_show_all(playlist_new_window);
490 }
491
492 /* Playlist picker ---------------------------------------------------------- */
493
494 /** @brief Delete button */
495 static GtkWidget *playlist_picker_delete_button;
496
497 /** @brief Tree model for list of playlists */
498 static GtkListStore *playlist_picker_list;
499
500 /** @brief Selection for list of playlists */
501 static GtkTreeSelection *playlist_picker_selection;
502
503 /** @brief Currently selected playlist */
504 static const char *playlist_picker_selected;
505
506 /** @brief (Re-)populate the playlist picker tree model */
507 static void playlist_picker_fill(const char attribute((unused)) *event,
508                                  void attribute((unused)) *eventdata,
509                                  void attribute((unused)) *callbackdata) {
510   GtkTreeIter iter[1];
511
512   if(!playlist_window)
513     return;
514   if(!playlist_picker_list)
515     playlist_picker_list = gtk_list_store_new(1, G_TYPE_STRING);
516   const char *was_selected = playlist_picker_selected;
517   gtk_list_store_clear(playlist_picker_list); /* clears playlists_selected */
518   for(int n = 0; n < nplaylists; ++n) {
519     gtk_list_store_insert_with_values(playlist_picker_list, iter,
520                                       n                /*position*/,
521                                       0, playlists[n], /* column 0 */
522                                       -1);             /* no more cols */
523     /* Reselect the selected playlist */
524     if(was_selected && !strcmp(was_selected, playlists[n]))
525       gtk_tree_selection_select_iter(playlist_picker_selection, iter);
526   }
527   /* TODO deselecting then reselecting the current playlist resets the playlist
528    * editor, which trashes the user's selection. */
529 }
530
531 /** @brief Called when the selection might have changed */
532 static void playlist_picker_selection_changed(GtkTreeSelection attribute((unused)) *treeselection,
533                                               gpointer attribute((unused)) user_data) {
534   GtkTreeIter iter;
535   char *gselected, *selected;
536   
537   /* Identify the current selection */
538   if(gtk_tree_selection_get_selected(playlist_picker_selection, 0, &iter)) {
539     gtk_tree_model_get(GTK_TREE_MODEL(playlist_picker_list), &iter,
540                        0, &gselected, -1);
541     selected = xstrdup(gselected);
542     g_free(gselected);
543   } else
544     selected = 0;
545   /* Set button sensitivity according to the new state */
546   if(selected)
547     gtk_widget_set_sensitive(playlist_picker_delete_button, 1);
548   else
549     gtk_widget_set_sensitive(playlist_picker_delete_button, 0);
550   /* Eliminate no-change cases */
551   if(!selected && !playlist_picker_selected)
552     return;
553   if(selected
554      && playlist_picker_selected
555      && !strcmp(selected, playlist_picker_selected))
556     return;
557   /* Record the new state */
558   playlist_picker_selected = selected;
559   /* Re-initalize the queue */
560   ql_new_queue(&ql_playlist, NULL);
561   playlist_editor_fill(NULL, (void *)playlist_picker_selected, NULL);
562 }
563
564 /** @brief Called when the 'add' button is pressed */
565 static void playlist_picker_add(GtkButton attribute((unused)) *button,
566                                 gpointer attribute((unused)) userdata) {
567   /* Unselect whatever is selected TODO why?? */
568   gtk_tree_selection_unselect_all(playlist_picker_selection);
569   playlist_new_playlist();
570 }
571
572 /** @brief Called when playlist deletion completes */
573 static void playlists_picker_delete_completed(void attribute((unused)) *v,
574                                               const char *err) {
575   if(err)
576     popup_protocol_error(0, err);
577 }
578
579 /** @brief Called when the 'Delete' button is pressed */
580 static void playlist_picker_delete(GtkButton attribute((unused)) *button,
581                                    gpointer attribute((unused)) userdata) {
582   GtkWidget *yesno;
583   int res;
584
585   if(!playlist_picker_selected)
586     return;                             /* shouldn't happen */
587   yesno = gtk_message_dialog_new(GTK_WINDOW(playlist_window),
588                                  GTK_DIALOG_MODAL,
589                                  GTK_MESSAGE_QUESTION,
590                                  GTK_BUTTONS_YES_NO,
591                                  "Do you really want to delete playlist %s?"
592                                  " This action cannot be undone.",
593                                  playlist_picker_selected);
594   res = gtk_dialog_run(GTK_DIALOG(yesno));
595   gtk_widget_destroy(yesno);
596   if(res == GTK_RESPONSE_YES) {
597     disorder_eclient_playlist_delete(client,
598                                      playlists_picker_delete_completed,
599                                      playlist_picker_selected,
600                                      NULL);
601   }
602 }
603
604 /** @brief Table of buttons below the playlist list */
605 static struct button playlist_picker_buttons[] = {
606   {
607     GTK_STOCK_ADD,
608     playlist_picker_add,
609     "Create a new playlist",
610     0
611   },
612   {
613     GTK_STOCK_REMOVE,
614     playlist_picker_delete,
615     "Delete a playlist",
616     0
617   },
618 };
619 #define NPLAYLIST_PICKER_BUTTONS (sizeof playlist_picker_buttons / sizeof *playlist_picker_buttons)
620
621 /** @brief Create the list of playlists for the edit playlists window */
622 static GtkWidget *playlist_picker_create(void) {
623   /* Create the list of playlist and populate it */
624   playlist_picker_fill(NULL, NULL, NULL);
625   /* Create the tree view */
626   GtkWidget *tree = gtk_tree_view_new_with_model(GTK_TREE_MODEL(playlist_picker_list));
627   /* ...and the renderers for it */
628   GtkCellRenderer *cr = gtk_cell_renderer_text_new();
629   GtkTreeViewColumn *col = gtk_tree_view_column_new_with_attributes("Playlist",
630                                                                     cr,
631                                                                     "text", 0,
632                                                                     NULL);
633   gtk_tree_view_append_column(GTK_TREE_VIEW(tree), col);
634   /* Get the selection for the view; set its mode; arrange for a callback when
635    * it changes */
636   playlist_picker_selected = NULL;
637   playlist_picker_selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(tree));
638   gtk_tree_selection_set_mode(playlist_picker_selection, GTK_SELECTION_BROWSE);
639   g_signal_connect(playlist_picker_selection, "changed",
640                    G_CALLBACK(playlist_picker_selection_changed), NULL);
641
642   /* Create the control buttons */
643   GtkWidget *buttons = create_buttons_box(playlist_picker_buttons,
644                                           NPLAYLIST_PICKER_BUTTONS,
645                                           gtk_hbox_new(FALSE, 1));
646   playlist_picker_delete_button = playlist_picker_buttons[1].widget;
647
648   playlist_picker_selection_changed(NULL, NULL);
649
650   /* Buttons live below the list */
651   GtkWidget *vbox = gtk_vbox_new(FALSE, 0);
652   gtk_box_pack_start(GTK_BOX(vbox), scroll_widget(tree), TRUE/*expand*/, TRUE/*fill*/, 0);
653   gtk_box_pack_start(GTK_BOX(vbox), buttons, FALSE/*expand*/, FALSE, 0);
654
655   return vbox;
656 }
657
658 /* Playlist editor ---------------------------------------------------------- */
659
660 /** @brief Called with new tracks for the playlist */
661 static void playlists_editor_received_tracks(void *v,
662                                              const char *err,
663                                              int nvec, char **vec) {
664   const char *playlist = v;
665   if(err) {
666     popup_protocol_error(0, err);
667     return;
668   }
669   if(!playlist_picker_selected
670      || strcmp(playlist, playlist_picker_selected)) {
671     /* The tracks are for the wrong playlist - something must have changed
672      * while the fetch command was in flight.  We just ignore this callback,
673      * the right answer will be requested and arrive in due course. */
674     return;
675   }
676   if(nvec == -1)
677     /* No such playlist, presumably we'll get a deleted event shortly */
678     return;
679   /* Translate the list of tracks into queue entries */
680   struct queue_entry *newq, **qq = &newq;
681   hash *h = hash_new(sizeof(int));
682   for(int n = 0; n < nvec; ++n) {
683     struct queue_entry *q = xmalloc(sizeof *q);
684     q->track = vec[n];
685     /* Synthesize a unique ID so that the selection survives updates.  Tracks
686      * can appear more than once in the queue so we can't use raw track names,
687      * so we add a serial number to the start. */
688     int *serialp = hash_find(h, vec[n]), serial = serialp ? *serialp : 0;
689     byte_xasprintf((char **)&q->id, "%d-%s", serial++, vec[n]);
690     hash_add(h, vec[0], &serial, HASH_INSERT_OR_REPLACE);
691     *qq = q;
692     qq = &q->next;
693   }
694   *qq = NULL;
695   ql_new_queue(&ql_playlist, newq);
696 }
697
698 /** @brief (Re-)populate the playlist tree model */
699 static void playlist_editor_fill(const char attribute((unused)) *event,
700                                  void *eventdata,
701                                  void attribute((unused)) *callbackdata) {
702   const char *modified_playlist = eventdata;
703   if(!playlist_window)
704     return;
705   if(!playlist_picker_selected)
706     return;
707   if(!strcmp(playlist_picker_selected, modified_playlist))
708     disorder_eclient_playlist_get(client, playlists_editor_received_tracks,
709                                   playlist_picker_selected,
710                                   (void *)playlist_picker_selected);
711 }
712
713 static GtkWidget *playlists_editor_create(void) {
714   assert(ql_playlist.view == NULL);     /* better not be set up already */
715   GtkWidget *w = init_queuelike(&ql_playlist);
716   /* Initially empty */
717   return w;
718 }
719
720 /* Playlist editor right-click menu ---------------------------------------- */
721
722 /** @brief Called to determine whether the playlist is playable */
723 static int playlist_playall_sensitive(void attribute((unused)) *extra) {
724   /* If there's no playlist obviously we can't play it */
725   if(!playlist_picker_selected)
726     return FALSE;
727   /* If it's empty we can't play it */
728   if(!ql_playlist.q)
729     return FALSE;
730   /* Otherwise we can */
731   return TRUE;
732 }
733
734 /** @brief Called to play the selected playlist */
735 static void playlist_playall_activate(GtkMenuItem attribute((unused)) *menuitem,
736                                       gpointer attribute((unused)) user_data) {
737   if(!playlist_picker_selected)
738     return;
739   /* Re-use the menu-based activation callback */
740   disorder_eclient_playlist_get(client, playlist_menu_received_content,
741                                 playlist_picker_selected, NULL);
742 }
743
744 /** @brief Called to determine whether the playlist is playable */
745 static int playlist_remove_sensitive(void attribute((unused)) *extra) {
746   /* If there's no playlist obviously we can't remove from it */
747   if(!playlist_picker_selected)
748     return FALSE;
749   /* If no tracks are selected we cannot remove them */
750   if(!gtk_tree_selection_count_selected_rows(ql_playlist.selection))
751     return FALSE;
752   /* We're good to go */
753   return TRUE;
754 }
755
756 /** @brief Called to play the selected playlist */
757 static void playlist_remove_activate(GtkMenuItem attribute((unused)) *menuitem,
758                                      gpointer attribute((unused)) user_data) {
759   if(!playlist_picker_selected)
760     return;
761   /* To safely remove rows we must:
762    * - take a lock
763    * - fetch the playlist
764    * - delete the selected rows
765    * - store the playlist
766    * - release the lock
767    */
768   fprintf(stderr, "remove tracks\n");   /* TODO */
769 }
770
771 /* Playlists window --------------------------------------------------------- */
772
773 /** @brief Keypress handler */
774 static gboolean playlist_window_keypress(GtkWidget attribute((unused)) *widget,
775                                          GdkEventKey *event,
776                                          gpointer attribute((unused)) user_data) {
777   if(event->state)
778     return FALSE;
779   switch(event->keyval) {
780   case GDK_Escape:
781     gtk_widget_destroy(playlist_window);
782     return TRUE;
783   default:
784     return FALSE;
785   }
786 }
787
788 /** @brief Called when the playlist window is destroyed */
789 static void playlist_window_destroyed(GtkWidget attribute((unused)) *widget,
790                                       GtkWidget **widget_pointer) {
791   destroy_queuelike(&ql_playlist);
792   *widget_pointer = NULL;
793 }
794
795 /** @brief Pop up the playlists window
796  *
797  * Called when the playlists menu item is selected
798  */
799 void playlist_window_create(gpointer attribute((unused)) callback_data,
800                             guint attribute((unused)) callback_action,
801                             GtkWidget attribute((unused)) *menu_item) {
802   /* If the window already exists, raise it */
803   if(playlist_window) {
804     gtk_window_present(GTK_WINDOW(playlist_window));
805     return;
806   }
807   /* Create the window */
808   playlist_window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
809   gtk_widget_set_style(playlist_window, tool_style);
810   g_signal_connect(playlist_window, "destroy",
811                    G_CALLBACK(playlist_window_destroyed), &playlist_window);
812   gtk_window_set_title(GTK_WINDOW(playlist_window), "Playlists Management");
813   /* TODO loads of this is very similar to (copied from!) users.c - can we
814    * de-dupe? */
815   /* Keyboard shortcuts */
816   g_signal_connect(playlist_window, "key-press-event",
817                    G_CALLBACK(playlist_window_keypress), 0);
818   /* default size is too small */
819   gtk_window_set_default_size(GTK_WINDOW(playlist_window), 512, 240);
820
821   GtkWidget *hbox = gtk_hbox_new(FALSE, 0);
822   gtk_box_pack_start(GTK_BOX(hbox), playlist_picker_create(),
823                      FALSE/*expand*/, FALSE, 0);
824   gtk_box_pack_start(GTK_BOX(hbox), gtk_event_box_new(),
825                      FALSE/*expand*/, FALSE, 2);
826   gtk_box_pack_start(GTK_BOX(hbox), playlists_editor_create(),
827                      TRUE/*expand*/, TRUE/*fill*/, 0);
828
829   gtk_container_add(GTK_CONTAINER(playlist_window), frame_widget(hbox, NULL));
830   gtk_widget_show_all(playlist_window);
831 }
832
833 /** @brief Initialize playlist support */
834 void playlists_init(void) {
835   /* We re-get all playlists upon any change... */
836   event_register("playlist-created", playlist_list_update, 0);
837   event_register("playlist-deleted", playlist_list_update, 0);
838   /* ...and on reconnection */
839   event_register("log-connected", playlist_list_update, 0);
840   /* ...and from time to time */
841   event_register("periodic-slow", playlist_list_update, 0);
842   /* ...and at startup */
843   playlist_list_update(0, 0, 0);
844
845   /* Update the playlists menu when the set of playlists changes */
846   event_register("playlists-updated", playlist_menu_changed, 0);
847   /* Update the new-playlist OK button when the set of playlists changes */
848   event_register("playlists-updated", playlist_new_changed, 0);
849   /* Update the list of playlists in the edit window when the set changes */
850   event_register("playlists-updated", playlist_picker_fill, 0);
851   /* Update the displayed playlist when it is modified */
852   event_register("playlist-modified", playlist_editor_fill, 0);
853 }
854
855 #endif
856
857 /*
858 Local Variables:
859 c-basic-offset:2
860 comment-column:40
861 fill-column:79
862 indent-tabs-mode:nil
863 End:
864 */