2 * This file is part of DisOrder
3 * Copyright (C) 2008, 2009 Richard Kettlewell
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.
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.
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
20 /** @file disobedience/playlists.c
21 * @brief Playlist support for Disobedience
23 * The playlists management window contains:
24 * - the playlist picker (a list of all playlists) TODO should be a tree!
27 * - the playlist editor (a d+d-capable view of the currently picked playlist)
28 * - a close button TODO
30 * This file also maintains the playlist menu, allowing playlists to be
31 * activated from the main window's menu.
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
38 #include "disobedience.h"
39 #include "queue-generic.h"
45 static void playlist_list_received_playlists(void *v
,
47 int nvec
, char **vec
);
48 static void playlist_editor_fill(const char *event
,
51 static int playlist_playall_sensitive(void *extra
);
52 static void playlist_playall_activate(GtkMenuItem
*menuitem
,
54 static int playlist_remove_sensitive(void *extra
) ;
55 static void playlist_remove_activate(GtkMenuItem
*menuitem
,
57 static void playlist_new_locked(void *v
, const char *err
);
58 static void playlist_new_retrieved(void *v
, const char *err
,
61 static void playlist_new_created(void *v
, const char *err
);
62 static void playlist_new_unlocked(void *v
, const char *err
);
63 static void playlist_new_entry_edited(GtkEditable
*editable
,
65 static void playlist_new_button_toggled(GtkToggleButton
*tb
,
67 static void playlist_new_changed(const char *event
,
70 static const char *playlist_new_valid(void);
71 static void playlist_new_details(char **namep
,
76 static void playlist_new_ok(GtkButton
*button
,
78 static void playlist_new_cancel(GtkButton
*button
,
80 static void playlists_editor_received_tracks(void *v
,
82 int nvec
, char **vec
);
83 static void playlist_window_destroyed(GtkWidget
*widget
,
84 GtkWidget
**widget_pointer
);
85 static gboolean
playlist_window_keypress(GtkWidget
*widget
,
88 static int playlistcmp(const void *ap
, const void *bp
);
89 static void playlist_modify_locked(void *v
, const char *err
);
90 void playlist_modify_retrieved(void *v
, const char *err
,
93 static void playlist_modify_updated(void *v
, const char *err
);
94 static void playlist_modify_unlocked(void *v
, const char *err
);
95 static void playlist_drop(struct queuelike
*ql
,
97 char **tracks
, char **ids
,
98 struct queue_entry
*after_me
);
99 struct playlist_modify_data
;
100 static void playlist_drop_modify(struct playlist_modify_data
*mod
,
101 int nvec
, char **vec
);
102 static void playlist_remove_modify(struct playlist_modify_data
*mod
,
103 int nvec
, char **vec
);
105 /** @brief Playlist editing window */
106 static GtkWidget
*playlist_window
;
108 /** @brief Columns for the playlist editor */
109 static const struct queue_column playlist_columns
[] = {
110 { "Artist", column_namepart
, "artist", COL_EXPAND
|COL_ELLIPSIZE
},
111 { "Album", column_namepart
, "album", COL_EXPAND
|COL_ELLIPSIZE
},
112 { "Title", column_namepart
, "title", COL_EXPAND
|COL_ELLIPSIZE
},
113 { "Length", column_length
, 0, COL_RIGHT
}
116 /** @brief Pop-up menu for playlist editor
119 * - track properties works but, bizarrely, raises the main window
121 * - play playlist works
122 * - select/deselect all work
124 static struct menuitem playlist_menuitems
[] = {
125 { "Track properties", ql_properties_activate
, ql_properties_sensitive
, 0, 0 },
126 { "Play track", ql_play_activate
, ql_play_sensitive
, 0, 0 },
127 { "Play playlist", playlist_playall_activate
, playlist_playall_sensitive
, 0, 0 },
128 { "Remove track from queue", playlist_remove_activate
, playlist_remove_sensitive
, 0, 0 },
129 { "Select all tracks", ql_selectall_activate
, ql_selectall_sensitive
, 0, 0 },
130 { "Deselect all tracks", ql_selectnone_activate
, ql_selectnone_sensitive
, 0, 0 },
133 static const GtkTargetEntry playlist_targets
[] = {
135 PLAYLIST_TRACKS
, /* drag type */
136 GTK_TARGET_SAME_WIDGET
, /* rearrangement within a widget */
137 PLAYLIST_TRACKS_ID
/* ID value */
140 PLAYABLE_TRACKS
, /* drag type */
141 GTK_TARGET_SAME_APP
|GTK_TARGET_OTHER_WIDGET
, /* copying between widgets */
142 PLAYABLE_TRACKS_ID
, /* ID value */
149 /** @brief Queuelike for editing a playlist */
150 static struct queuelike ql_playlist
= {
152 .columns
= playlist_columns
,
153 .ncolumns
= sizeof playlist_columns
/ sizeof *playlist_columns
,
154 .menuitems
= playlist_menuitems
,
155 .nmenuitems
= sizeof playlist_menuitems
/ sizeof *playlist_menuitems
,
156 .drop
= playlist_drop
,
157 .drag_source_targets
= playlist_targets
,
158 .drag_source_actions
= GDK_ACTION_MOVE
|GDK_ACTION_COPY
,
159 .drag_dest_targets
= playlist_targets
,
160 .drag_dest_actions
= GDK_ACTION_MOVE
|GDK_ACTION_COPY
,
163 /* Maintaining the list of playlists ---------------------------------------- */
165 /** @brief Current list of playlists or NULL */
168 /** @brief Count of playlists */
171 /** @brief Schedule an update to the list of playlists
173 * Called periodically and when a playlist is created or deleted.
175 static void playlist_list_update(const char attribute((unused
)) *event
,
176 void attribute((unused
)) *eventdata
,
177 void attribute((unused
)) *callbackdata
) {
178 disorder_eclient_playlists(client
, playlist_list_received_playlists
, 0);
181 /** @brief Called with a new list of playlists */
182 static void playlist_list_received_playlists(void attribute((unused
)) *v
,
184 int nvec
, char **vec
) {
188 /* Probably means server does not support playlists */
192 qsort(playlists
, nplaylists
, sizeof (char *), playlistcmp
);
194 /* Tell our consumers */
195 event_raise("playlists-updated", 0);
198 /** @brief qsort() callback for playlist name comparison */
199 static int playlistcmp(const void *ap
, const void *bp
) {
200 const char *a
= *(char **)ap
, *b
= *(char **)bp
;
201 const char *ad
= strchr(a
, '.'), *bd
= strchr(b
, '.');
204 /* Group owned playlists by owner */
206 const int adn
= ad
- a
, bdn
= bd
- b
;
207 if((c
= strncmp(a
, b
, adn
< bdn ? adn
: bdn
)))
209 /* Lexical order within playlists of a single owner */
210 return strcmp(ad
+ 1, bd
+ 1);
213 /* Owned playlists after shared ones */
220 /* Lexical order of shared playlists */
224 /* Playlists menu ----------------------------------------------------------- */
226 static void playlist_menu_playing(void attribute((unused
)) *v
,
229 popup_protocol_error(0, err
);
232 /** @brief Play received playlist contents
234 * Passed as a completion callback by menu_activate_playlist().
236 static void playlist_menu_received_content(void attribute((unused
)) *v
,
238 int nvec
, char **vec
) {
240 popup_protocol_error(0, err
);
243 for(int n
= 0; n
< nvec
; ++n
)
244 disorder_eclient_play(client
, vec
[n
], playlist_menu_playing
, NULL
);
247 /** @brief Called to activate a playlist
249 * Called when the menu item for a playlist is clicked.
251 static void playlist_menu_activate(GtkMenuItem
*menuitem
,
252 gpointer
attribute((unused
)) user_data
) {
253 GtkLabel
*label
= GTK_LABEL(GTK_BIN(menuitem
)->child
);
254 const char *playlist
= gtk_label_get_text(label
);
256 disorder_eclient_playlist_get(client
, playlist_menu_received_content
,
260 /** @brief Called when the playlists change
262 * Naively refills the menu. The results might be unsettling if the menu is
263 * currently open, but this is hopefuly fairly rare.
265 static void playlist_menu_changed(const char attribute((unused
)) *event
,
266 void attribute((unused
)) *eventdata
,
267 void attribute((unused
)) *callbackdata
) {
269 return; /* OMG too soon */
270 GtkMenuShell
*menu
= GTK_MENU_SHELL(playlists_menu
);
271 while(menu
->children
)
272 gtk_container_remove(GTK_CONTAINER(menu
), GTK_WIDGET(menu
->children
->data
));
273 /* NB nplaylists can be -1 as well as 0 */
274 for(int n
= 0; n
< nplaylists
; ++n
) {
275 GtkWidget
*w
= gtk_menu_item_new_with_label(playlists
[n
]);
276 g_signal_connect(w
, "activate", G_CALLBACK(playlist_menu_activate
), 0);
278 gtk_menu_shell_append(menu
, w
);
280 gtk_widget_set_sensitive(menu_playlists_widget
,
282 gtk_widget_set_sensitive(menu_editplaylists_widget
,
286 /* Popup to create a new playlist ------------------------------------------- */
288 /** @brief New-playlist popup */
289 static GtkWidget
*playlist_new_window
;
291 /** @brief Text entry in new-playlist popup */
292 static GtkWidget
*playlist_new_entry
;
294 /** @brief Label for displaying feedback on what's wrong */
295 static GtkWidget
*playlist_new_info
;
297 /** @brief "Shared" radio button */
298 static GtkWidget
*playlist_new_shared
;
300 /** @brief "Public" radio button */
301 static GtkWidget
*playlist_new_public
;
303 /** @brief "Private" radio button */
304 static GtkWidget
*playlist_new_private
;
306 /** @brief Buttons for new-playlist popup */
307 static struct button playlist_new_buttons
[] = {
309 .stock
= GTK_STOCK_OK
,
310 .clicked
= playlist_new_ok
,
311 .tip
= "Create new playlist"
314 .stock
= GTK_STOCK_CANCEL
,
315 .clicked
= playlist_new_cancel
,
316 .tip
= "Do not create new playlist"
319 #define NPLAYLIST_NEW_BUTTONS (sizeof playlist_new_buttons / sizeof *playlist_new_buttons)
321 /** @brief Pop up a new window to enter the playlist name and details */
322 static void playlist_new_playlist(void) {
323 assert(playlist_new_window
== NULL
);
324 playlist_new_window
= gtk_window_new(GTK_WINDOW_TOPLEVEL
);
325 g_signal_connect(playlist_new_window
, "destroy",
326 G_CALLBACK(gtk_widget_destroyed
), &playlist_new_window
);
327 gtk_window_set_title(GTK_WINDOW(playlist_new_window
), "Create new playlist");
328 /* Window will be modal, suppressing access to other windows */
329 gtk_window_set_modal(GTK_WINDOW(playlist_new_window
), TRUE
);
330 gtk_window_set_transient_for(GTK_WINDOW(playlist_new_window
),
331 GTK_WINDOW(playlist_window
));
333 /* Window contents will use a table (grid) layout */
334 GtkWidget
*table
= gtk_table_new(3, 3, FALSE
/*!homogeneous*/);
336 /* First row: playlist name */
337 gtk_table_attach_defaults(GTK_TABLE(table
),
338 gtk_label_new("Playlist name"),
340 playlist_new_entry
= gtk_entry_new();
341 g_signal_connect(playlist_new_entry
, "changed",
342 G_CALLBACK(playlist_new_entry_edited
), NULL
);
343 gtk_table_attach_defaults(GTK_TABLE(table
),
347 /* Second row: radio buttons to choose type */
348 playlist_new_shared
= gtk_radio_button_new_with_label(NULL
, "shared");
350 = gtk_radio_button_new_with_label_from_widget(GTK_RADIO_BUTTON(playlist_new_shared
),
353 = gtk_radio_button_new_with_label_from_widget(GTK_RADIO_BUTTON(playlist_new_shared
),
355 g_signal_connect(playlist_new_shared
, "toggled",
356 G_CALLBACK(playlist_new_button_toggled
), NULL
);
357 g_signal_connect(playlist_new_public
, "toggled",
358 G_CALLBACK(playlist_new_button_toggled
), NULL
);
359 g_signal_connect(playlist_new_private
, "toggled",
360 G_CALLBACK(playlist_new_button_toggled
), NULL
);
361 gtk_table_attach_defaults(GTK_TABLE(table
), playlist_new_shared
, 0, 1, 1, 2);
362 gtk_table_attach_defaults(GTK_TABLE(table
), playlist_new_public
, 1, 2, 1, 2);
363 gtk_table_attach_defaults(GTK_TABLE(table
), playlist_new_private
, 2, 3, 1, 2);
365 /* Third row: info bar saying why not */
366 playlist_new_info
= gtk_label_new("");
367 gtk_table_attach_defaults(GTK_TABLE(table
), playlist_new_info
,
370 /* Fourth row: ok/cancel buttons */
371 GtkWidget
*hbox
= create_buttons_box(playlist_new_buttons
,
372 NPLAYLIST_NEW_BUTTONS
,
373 gtk_hbox_new(FALSE
, 0));
374 gtk_table_attach_defaults(GTK_TABLE(table
), hbox
, 0, 3, 3, 4);
376 gtk_container_add(GTK_CONTAINER(playlist_new_window
),
377 frame_widget(table
, NULL
));
379 /* Set initial state of OK button */
380 playlist_new_changed(0,0,0);
382 /* TODO: return should = OK, escape should = cancel */
384 /* Display the window */
385 gtk_widget_show_all(playlist_new_window
);
388 /** @brief Called when 'ok' is clicked in new-playlist popup */
389 static void playlist_new_ok(GtkButton
attribute((unused
)) *button
,
390 gpointer
attribute((unused
)) userdata
) {
391 gboolean shared
, public, private;
392 char *name
, *fullname
;
393 playlist_new_details(&name
, &fullname
, &shared
, &public, &private);
396 * - lock the playlist
397 * - check it doesn't exist
398 * - set sharing (which will create it empty
401 * TODO we should freeze the window while this is going on to stop a second
404 disorder_eclient_playlist_lock(client
, playlist_new_locked
, fullname
,
408 /** @brief Called when the proposed new playlist has been locked */
409 static void playlist_new_locked(void *v
, const char *err
) {
412 popup_protocol_error(0, err
);
415 disorder_eclient_playlist_get(client
, playlist_new_retrieved
,
419 /** @brief Called when the proposed new playlist's contents have been retrieved
421 * ...or rather, normally, when it's been reported that it does not exist.
423 static void playlist_new_retrieved(void *v
, const char *err
,
425 char attribute((unused
)) **vec
) {
427 if(!err
&& nvec
!= -1)
428 /* A rare case but not in principle impossible */
429 err
= "A playlist with that name already exists.";
431 popup_protocol_error(0, err
);
432 disorder_eclient_playlist_unlock(client
, playlist_new_unlocked
, fullname
);
435 gboolean shared
, public, private;
436 playlist_new_details(0, 0, &shared
, &public, &private);
437 disorder_eclient_playlist_set_share(client
, playlist_new_created
, fullname
,
439 : private ?
"private"
444 /** @brief Called when the new playlist has been created */
445 static void playlist_new_created(void attribute((unused
)) *v
, const char *err
) {
447 popup_protocol_error(0, err
);
450 disorder_eclient_playlist_unlock(client
, playlist_new_unlocked
, NULL
);
451 // TODO arrange for the new playlist to be selected
454 /** @brief Called when the newly created playlist has unlocked */
455 static void playlist_new_unlocked(void attribute((unused
)) *v
, const char *err
) {
457 popup_protocol_error(0, err
);
458 /* Pop down the creation window */
459 gtk_widget_destroy(playlist_new_window
);
462 /** @brief Called when 'cancel' is clicked in new-playlist popup */
463 static void playlist_new_cancel(GtkButton
attribute((unused
)) *button
,
464 gpointer
attribute((unused
)) userdata
) {
465 gtk_widget_destroy(playlist_new_window
);
468 /** @brief Called when some radio button in the new-playlist popup changes */
469 static void playlist_new_button_toggled(GtkToggleButton
attribute((unused
)) *tb
,
470 gpointer
attribute((unused
)) userdata
) {
471 playlist_new_changed(0,0,0);
474 /** @brief Called when the text entry field in the new-playlist popup changes */
475 static void playlist_new_entry_edited(GtkEditable
attribute((unused
)) *editable
,
476 gpointer
attribute((unused
)) user_data
) {
477 playlist_new_changed(0,0,0);
480 /** @brief Called to update new playlist window state
482 * This is called whenever one the text entry or radio buttons changed, and
483 * also when the set of known playlists changes. It determines whether the new
484 * playlist would be creatable and sets the sensitivity of the OK button
485 * and info display accordingly.
487 static void playlist_new_changed(const char attribute((unused
)) *event
,
488 void attribute((unused
)) *eventdata
,
489 void attribute((unused
)) *callbackdata
) {
490 if(!playlist_new_window
)
492 const char *reason
= playlist_new_valid();
493 gtk_widget_set_sensitive(playlist_new_buttons
[0].widget
,
495 gtk_label_set_text(GTK_LABEL(playlist_new_info
), reason
);
498 /** @brief Test whether the new-playlist window settings are valid
499 * @return NULL on success or an error string if not
501 static const char *playlist_new_valid(void) {
502 gboolean shared
, public, private;
503 char *name
, *fullname
;
504 playlist_new_details(&name
, &fullname
, &shared
, &public, &private);
505 if(!(shared
|| public || private))
506 return "No type set.";
509 /* See if the result is valid */
510 if(!valid_username(name
)
511 || playlist_parse_name(fullname
, NULL
, NULL
))
512 return "Not a valid playlist name.";
513 /* See if the result clashes with an existing name. This is not a perfect
514 * check, the playlist might be created after this point but before we get a
515 * chance to disable the "OK" button. However when we try to create the
516 * playlist we will first try to retrieve it, with a lock held, so we
517 * shouldn't end up overwriting anything. */
518 for(int n
= 0; n
< nplaylists
; ++n
)
519 if(!strcmp(playlists
[n
], fullname
)) {
521 return "A shared playlist with that name already exists.";
523 return "You already have a playlist with that name.";
525 /* As far as we can tell creation would work */
529 /** @brief Get entered new-playlist details
530 * @param namep Where to store entered name (or NULL)
531 * @param fullnamep Where to store computed full name (or NULL)
532 * @param sharep Where to store 'shared' flag (or NULL)
533 * @param publicp Where to store 'public' flag (or NULL)
534 * @param privatep Where to store 'private' flag (or NULL)
536 static void playlist_new_details(char **namep
,
540 gboolean
*privatep
) {
541 gboolean shared
, public, private;
542 g_object_get(playlist_new_shared
, "active", &shared
, (char *)NULL
);
543 g_object_get(playlist_new_public
, "active", &public, (char *)NULL
);
544 g_object_get(playlist_new_private
, "active", &private, (char *)NULL
);
545 char *gname
= gtk_editable_get_chars(GTK_EDITABLE(playlist_new_entry
),
546 0, -1); /* name owned by calle */
547 char *name
= xstrdup(gname
);
549 if(sharedp
) *sharedp
= shared
;
550 if(publicp
) *publicp
= public;
551 if(privatep
) *privatep
= private;
552 if(namep
) *namep
= name
;
554 if(*sharedp
) *fullnamep
= *namep
;
555 else byte_xasprintf(fullnamep
, "%s.%s", config
->username
, name
);
559 /* Playlist picker ---------------------------------------------------------- */
561 /** @brief Delete button */
562 static GtkWidget
*playlist_picker_delete_button
;
564 /** @brief Tree model for list of playlists */
565 static GtkListStore
*playlist_picker_list
;
567 /** @brief Selection for list of playlists */
568 static GtkTreeSelection
*playlist_picker_selection
;
570 /** @brief Currently selected playlist */
571 static const char *playlist_picker_selected
;
573 /** @brief (Re-)populate the playlist picker tree model */
574 static void playlist_picker_fill(const char attribute((unused
)) *event
,
575 void attribute((unused
)) *eventdata
,
576 void attribute((unused
)) *callbackdata
) {
581 if(!playlist_picker_list
)
582 playlist_picker_list
= gtk_list_store_new(1, G_TYPE_STRING
);
583 const char *was_selected
= playlist_picker_selected
;
584 gtk_list_store_clear(playlist_picker_list
); /* clears playlists_selected */
585 for(int n
= 0; n
< nplaylists
; ++n
) {
586 gtk_list_store_insert_with_values(playlist_picker_list
, iter
,
588 0, playlists
[n
], /* column 0 */
589 -1); /* no more cols */
590 /* Reselect the selected playlist */
591 if(was_selected
&& !strcmp(was_selected
, playlists
[n
]))
592 gtk_tree_selection_select_iter(playlist_picker_selection
, iter
);
594 /* TODO deselecting then reselecting the current playlist resets the playlist
595 * editor, which trashes the user's selection. */
598 /** @brief Called when the selection might have changed */
599 static void playlist_picker_selection_changed(GtkTreeSelection
attribute((unused
)) *treeselection
,
600 gpointer
attribute((unused
)) user_data
) {
602 char *gselected
, *selected
;
604 /* Identify the current selection */
605 if(gtk_tree_selection_get_selected(playlist_picker_selection
, 0, &iter
)) {
606 gtk_tree_model_get(GTK_TREE_MODEL(playlist_picker_list
), &iter
,
608 selected
= xstrdup(gselected
);
612 /* Set button sensitivity according to the new state */
614 gtk_widget_set_sensitive(playlist_picker_delete_button
, 1);
616 gtk_widget_set_sensitive(playlist_picker_delete_button
, 0);
617 /* Eliminate no-change cases */
618 if(!selected
&& !playlist_picker_selected
)
621 && playlist_picker_selected
622 && !strcmp(selected
, playlist_picker_selected
))
624 /* Record the new state */
625 playlist_picker_selected
= selected
;
626 /* Re-initalize the queue */
627 ql_new_queue(&ql_playlist
, NULL
);
628 playlist_editor_fill(NULL
, (void *)playlist_picker_selected
, NULL
);
631 /** @brief Called when the 'add' button is pressed */
632 static void playlist_picker_add(GtkButton
attribute((unused
)) *button
,
633 gpointer
attribute((unused
)) userdata
) {
634 /* Unselect whatever is selected TODO why?? */
635 gtk_tree_selection_unselect_all(playlist_picker_selection
);
636 playlist_new_playlist();
639 /** @brief Called when playlist deletion completes */
640 static void playlists_picker_delete_completed(void attribute((unused
)) *v
,
643 popup_protocol_error(0, err
);
646 /** @brief Called when the 'Delete' button is pressed */
647 static void playlist_picker_delete(GtkButton
attribute((unused
)) *button
,
648 gpointer
attribute((unused
)) userdata
) {
652 if(!playlist_picker_selected
)
653 return; /* shouldn't happen */
654 yesno
= gtk_message_dialog_new(GTK_WINDOW(playlist_window
),
656 GTK_MESSAGE_QUESTION
,
658 "Do you really want to delete playlist %s?"
659 " This action cannot be undone.",
660 playlist_picker_selected
);
661 res
= gtk_dialog_run(GTK_DIALOG(yesno
));
662 gtk_widget_destroy(yesno
);
663 if(res
== GTK_RESPONSE_YES
) {
664 disorder_eclient_playlist_delete(client
,
665 playlists_picker_delete_completed
,
666 playlist_picker_selected
,
671 /** @brief Table of buttons below the playlist list */
672 static struct button playlist_picker_buttons
[] = {
676 "Create a new playlist",
681 playlist_picker_delete
,
686 #define NPLAYLIST_PICKER_BUTTONS (sizeof playlist_picker_buttons / sizeof *playlist_picker_buttons)
688 /** @brief Create the list of playlists for the edit playlists window */
689 static GtkWidget
*playlist_picker_create(void) {
690 /* Create the list of playlist and populate it */
691 playlist_picker_fill(NULL
, NULL
, NULL
);
692 /* Create the tree view */
693 GtkWidget
*tree
= gtk_tree_view_new_with_model(GTK_TREE_MODEL(playlist_picker_list
));
694 /* ...and the renderers for it */
695 GtkCellRenderer
*cr
= gtk_cell_renderer_text_new();
696 GtkTreeViewColumn
*col
= gtk_tree_view_column_new_with_attributes("Playlist",
700 gtk_tree_view_append_column(GTK_TREE_VIEW(tree
), col
);
701 /* Get the selection for the view; set its mode; arrange for a callback when
703 playlist_picker_selected
= NULL
;
704 playlist_picker_selection
= gtk_tree_view_get_selection(GTK_TREE_VIEW(tree
));
705 gtk_tree_selection_set_mode(playlist_picker_selection
, GTK_SELECTION_BROWSE
);
706 g_signal_connect(playlist_picker_selection
, "changed",
707 G_CALLBACK(playlist_picker_selection_changed
), NULL
);
709 /* Create the control buttons */
710 GtkWidget
*buttons
= create_buttons_box(playlist_picker_buttons
,
711 NPLAYLIST_PICKER_BUTTONS
,
712 gtk_hbox_new(FALSE
, 1));
713 playlist_picker_delete_button
= playlist_picker_buttons
[1].widget
;
715 playlist_picker_selection_changed(NULL
, NULL
);
717 /* Buttons live below the list */
718 GtkWidget
*vbox
= gtk_vbox_new(FALSE
, 0);
719 gtk_box_pack_start(GTK_BOX(vbox
), scroll_widget(tree
), TRUE
/*expand*/, TRUE
/*fill*/, 0);
720 gtk_box_pack_start(GTK_BOX(vbox
), buttons
, FALSE
/*expand*/, FALSE
, 0);
725 static void playlist_picker_destroy(void) {
726 playlist_picker_delete_button
= NULL
;
727 g_object_unref(playlist_picker_list
);
728 playlist_picker_list
= NULL
;
729 playlist_picker_selection
= NULL
;
730 playlist_picker_selected
= NULL
;
733 /* Playlist editor ---------------------------------------------------------- */
735 static GtkWidget
*playlists_editor_create(void) {
736 assert(ql_playlist
.view
== NULL
); /* better not be set up already */
737 GtkWidget
*w
= init_queuelike(&ql_playlist
);
738 /* Initially empty */
742 /** @brief (Re-)populate the playlist tree model */
743 static void playlist_editor_fill(const char attribute((unused
)) *event
,
745 void attribute((unused
)) *callbackdata
) {
746 const char *modified_playlist
= eventdata
;
749 if(!playlist_picker_selected
)
751 if(!strcmp(playlist_picker_selected
, modified_playlist
))
752 disorder_eclient_playlist_get(client
, playlists_editor_received_tracks
,
753 playlist_picker_selected
,
754 (void *)playlist_picker_selected
);
757 /** @brief Called with new tracks for the playlist */
758 static void playlists_editor_received_tracks(void *v
,
760 int nvec
, char **vec
) {
761 const char *playlist
= v
;
763 popup_protocol_error(0, err
);
766 if(!playlist_picker_selected
767 || strcmp(playlist
, playlist_picker_selected
)) {
768 /* The tracks are for the wrong playlist - something must have changed
769 * while the fetch command was in flight. We just ignore this callback,
770 * the right answer will be requested and arrive in due course. */
774 /* No such playlist, presumably we'll get a deleted event shortly */
776 /* Translate the list of tracks into queue entries */
777 struct queue_entry
*newq
, **qq
= &newq
, *qprev
= NULL
;
778 hash
*h
= hash_new(sizeof(int));
779 for(int n
= 0; n
< nvec
; ++n
) {
780 struct queue_entry
*q
= xmalloc(sizeof *q
);
783 /* Synthesize a unique ID so that the selection survives updates. Tracks
784 * can appear more than once in the queue so we can't use raw track names,
785 * so we add a serial number to the start. */
786 int *serialp
= hash_find(h
, vec
[n
]), serial
= serialp ?
*serialp
: 0;
787 byte_xasprintf((char **)&q
->id
, "%d-%s", serial
++, vec
[n
]);
788 hash_add(h
, vec
[n
], &serial
, HASH_INSERT_OR_REPLACE
);
794 ql_new_queue(&ql_playlist
, newq
);
797 /* Playlist mutation -------------------------------------------------------- */
799 /** @brief State structure for guarded playlist modification
801 * To safely move, insert or delete rows we must:
803 * - fetch the playlist
804 * - verify it's not changed
805 * - update the playlist contents
806 * - store the playlist
809 * The playlist_modify_ functions do just that.
811 * To kick things off create one of these and disorder_eclient_playlist_lock()
812 * with playlist_modify_locked() as its callback. @c modify will be called; it
813 * should disorder_eclient_playlist_set() to set the new state with
814 * playlist_modify_updated() as its callback.
816 struct playlist_modify_data
{
817 /** @brief Affected playlist */
818 const char *playlist
;
819 /** @brief Modification function
820 * @param mod Pointer back to state structure
821 * @param ntracks Length of playlist
822 * @param tracks Tracks in playlist
824 void (*modify
)(struct playlist_modify_data
*mod
,
825 int ntracks
, char **tracks
);
827 /** @brief Number of tracks dropped */
829 /** @brief Track names dropped */
831 /** @brief Track IDs dropped */
833 /** @brief Drop after this point */
834 struct queue_entry
*after_me
;
837 /** @brief Called with playlist locked
839 * This is the entry point for guarded modification ising @ref
840 * playlist_modify_data.
842 static void playlist_modify_locked(void *v
, const char *err
) {
843 struct playlist_modify_data
*mod
= v
;
845 popup_protocol_error(0, err
);
848 disorder_eclient_playlist_get(client
, playlist_modify_retrieved
,
852 /** @brief Called with current playlist contents
853 * Checks that the playlist is still current and has not changed.
855 void playlist_modify_retrieved(void *v
, const char *err
,
858 struct playlist_modify_data
*mod
= v
;
860 popup_protocol_error(0, err
);
861 disorder_eclient_playlist_unlock(client
, playlist_modify_unlocked
, NULL
);
865 || !playlist_picker_selected
866 || strcmp(mod
->playlist
, playlist_picker_selected
)) {
867 disorder_eclient_playlist_unlock(client
, playlist_modify_unlocked
, NULL
);
870 /* We check that the contents haven't changed. If they have we just abandon
871 * the operation. The user will have to try again. */
872 struct queue_entry
*q
;
874 for(n
= 0, q
= ql_playlist
.q
; q
&& n
< nvec
; ++n
, q
= q
->next
)
875 if(strcmp(q
->track
, vec
[n
]))
877 if(n
!= nvec
|| q
!= NULL
) {
878 disorder_eclient_playlist_unlock(client
, playlist_modify_unlocked
, NULL
);
881 mod
->modify(mod
, nvec
, vec
);
884 /** @brief Called when the playlist has been updated */
885 static void playlist_modify_updated(void attribute((unused
)) *v
,
888 popup_protocol_error(0, err
);
889 disorder_eclient_playlist_unlock(client
, playlist_modify_unlocked
, NULL
);
892 /** @brief Called when the playlist has been unlocked */
893 static void playlist_modify_unlocked(void attribute((unused
)) *v
,
896 popup_protocol_error(0, err
);
899 /* Drop tracks into a playlist ---------------------------------------------- */
901 static void playlist_drop(struct queuelike
attribute((unused
)) *ql
,
903 char **tracks
, char **ids
,
904 struct queue_entry
*after_me
) {
905 struct playlist_modify_data
*mod
= xmalloc(sizeof *mod
);
907 mod
->playlist
= playlist_picker_selected
;
908 mod
->modify
= playlist_drop_modify
;
909 mod
->ntracks
= ntracks
;
910 mod
->tracks
= tracks
;
912 mod
->after_me
= after_me
;
913 disorder_eclient_playlist_lock(client
, playlist_modify_locked
,
917 /** @brief Return true if track @p i is in the moved set */
918 static int playlist_drop_is_moved(struct playlist_modify_data
*mod
,
920 struct queue_entry
*q
;
922 /* Find the q corresponding to i, so we can get the ID */
923 for(q
= ql_playlist
.q
; i
; q
= q
->next
, --i
)
925 /* See if track i matches any of the moved set by ID */
926 for(int n
= 0; n
< mod
->ntracks
; ++n
)
927 if(!strcmp(q
->id
, mod
->ids
[n
]))
932 static void playlist_drop_modify(struct playlist_modify_data
*mod
,
933 int nvec
, char **vec
) {
937 //fprintf(stderr, "\nplaylist_drop_modify\n");
938 /* after_me is the queue_entry to insert after, or NULL to insert at the
939 * beginning (including the case when the playlist is empty) */
940 //fprintf(stderr, "after_me = %s\n",
941 // mod->after_me ? mod->after_me->track : "NULL");
942 struct queue_entry
*q
= ql_playlist
.q
;
946 while(q
&& q
!= mod
->after_me
) {
951 /* Now ins is the index to insert at; equivalently, the row to insert before,
952 * and so equal to nvec to append. */
954 fprintf(stderr
, "ins = %d = %s\n",
955 ins
, ins
< nvec ? vec
[ins
] : "NULL");
956 for(int n
= 0; n
< nvec
; ++n
)
957 fprintf(stderr
, "%d: %s %s\n", n
, n
== ins ?
"->" : " ", vec
[n
]);
958 fprintf(stderr
, "nvec = %d\n", nvec
);
961 /* This is a rearrangement */
963 * - vec[], the current layout
964 * - ins, pointing into vec
965 * - mod->tracks[], a subset of vec[] which is to be moved
967 * ins is the insertion point BUT it is in terms of the whole
968 * array, i.e. before mod->tracks[] have been removed. The first
969 * step then is to remove everything in mod->tracks[] and adjust
970 * ins downwards as necessary.
972 /* First zero out anything that's moved */
974 for(int n
= 0; n
< nvec
; ++n
) {
975 if(playlist_drop_is_moved(mod
, n
)) {
981 /* Now collapse down the array */
983 for(int n
= 0; n
< nvec
; ++n
) {
987 assert(i
+ mod
->ntracks
== nvec
);
989 /* Adjust the insertion point to take account of things moved from before
992 /* The effect is now the same as an insertion */
994 /* This is (now) an insertion */
995 nnewvec
= nvec
+ mod
->ntracks
;
996 newvec
= xcalloc(nnewvec
, sizeof (char *));
998 ins
* sizeof (char *));
999 memcpy(newvec
+ ins
, mod
->tracks
,
1000 mod
->ntracks
* sizeof (char *));
1001 memcpy(newvec
+ ins
+ mod
->ntracks
, vec
+ ins
,
1002 (nvec
- ins
) * sizeof (char *));
1003 disorder_eclient_playlist_set(client
, playlist_modify_updated
, mod
->playlist
,
1004 newvec
, nnewvec
, mod
);
1007 /* Playlist editor right-click menu ---------------------------------------- */
1009 /** @brief Called to determine whether the playlist is playable */
1010 static int playlist_playall_sensitive(void attribute((unused
)) *extra
) {
1011 /* If there's no playlist obviously we can't play it */
1012 if(!playlist_picker_selected
)
1014 /* If it's empty we can't play it */
1017 /* Otherwise we can */
1021 /** @brief Called to play the selected playlist */
1022 static void playlist_playall_activate(GtkMenuItem
attribute((unused
)) *menuitem
,
1023 gpointer
attribute((unused
)) user_data
) {
1024 if(!playlist_picker_selected
)
1026 /* Re-use the menu-based activation callback */
1027 disorder_eclient_playlist_get(client
, playlist_menu_received_content
,
1028 playlist_picker_selected
, NULL
);
1031 /** @brief Called to determine whether the playlist is playable */
1032 static int playlist_remove_sensitive(void attribute((unused
)) *extra
) {
1033 /* If there's no playlist obviously we can't remove from it */
1034 if(!playlist_picker_selected
)
1036 /* If no tracks are selected we cannot remove them */
1037 if(!gtk_tree_selection_count_selected_rows(ql_playlist
.selection
))
1039 /* We're good to go */
1043 /** @brief Called to play the selected playlist */
1044 static void playlist_remove_activate(GtkMenuItem
attribute((unused
)) *menuitem
,
1045 gpointer
attribute((unused
)) user_data
) {
1046 /* TODO backspace should work too */
1047 if(!playlist_picker_selected
)
1049 struct playlist_modify_data
*mod
= xmalloc(sizeof *mod
);
1051 mod
->playlist
= playlist_picker_selected
;
1052 mod
->modify
= playlist_remove_modify
;
1053 disorder_eclient_playlist_lock(client
, playlist_modify_locked
,
1054 mod
->playlist
, mod
);
1057 static void playlist_remove_modify(struct playlist_modify_data
*mod
,
1058 int attribute((unused
)) nvec
, char **vec
) {
1059 GtkTreeIter iter
[1];
1060 gboolean it
= gtk_tree_model_get_iter_first(GTK_TREE_MODEL(ql_playlist
.store
),
1064 if(!gtk_tree_selection_iter_is_selected(ql_playlist
.selection
, iter
))
1065 vec
[m
++] = vec
[n
++];
1068 it
= gtk_tree_model_iter_next(GTK_TREE_MODEL(ql_playlist
.store
), iter
);
1070 disorder_eclient_playlist_set(client
, playlist_modify_updated
, mod
->playlist
,
1074 /* Playlists window --------------------------------------------------------- */
1076 /** @brief Pop up the playlists window
1078 * Called when the playlists menu item is selected
1080 void playlist_window_create(gpointer
attribute((unused
)) callback_data
,
1081 guint
attribute((unused
)) callback_action
,
1082 GtkWidget
attribute((unused
)) *menu_item
) {
1083 /* If the window already exists, raise it */
1084 if(playlist_window
) {
1085 gtk_window_present(GTK_WINDOW(playlist_window
));
1088 /* Create the window */
1089 playlist_window
= gtk_window_new(GTK_WINDOW_TOPLEVEL
);
1090 gtk_widget_set_style(playlist_window
, tool_style
);
1091 g_signal_connect(playlist_window
, "destroy",
1092 G_CALLBACK(playlist_window_destroyed
), &playlist_window
);
1093 gtk_window_set_title(GTK_WINDOW(playlist_window
), "Playlists Management");
1094 /* TODO loads of this is very similar to (copied from!) users.c - can we
1096 /* Keyboard shortcuts */
1097 g_signal_connect(playlist_window
, "key-press-event",
1098 G_CALLBACK(playlist_window_keypress
), 0);
1099 /* default size is too small */
1100 gtk_window_set_default_size(GTK_WINDOW(playlist_window
), 512, 240);
1102 GtkWidget
*hbox
= gtk_hbox_new(FALSE
, 0);
1103 gtk_box_pack_start(GTK_BOX(hbox
), playlist_picker_create(),
1104 FALSE
/*expand*/, FALSE
, 0);
1105 gtk_box_pack_start(GTK_BOX(hbox
), gtk_event_box_new(),
1106 FALSE
/*expand*/, FALSE
, 2);
1107 gtk_box_pack_start(GTK_BOX(hbox
), playlists_editor_create(),
1108 TRUE
/*expand*/, TRUE
/*fill*/, 0);
1110 gtk_container_add(GTK_CONTAINER(playlist_window
), frame_widget(hbox
, NULL
));
1111 gtk_widget_show_all(playlist_window
);
1114 /** @brief Keypress handler */
1115 static gboolean
playlist_window_keypress(GtkWidget
attribute((unused
)) *widget
,
1117 gpointer
attribute((unused
)) user_data
) {
1120 switch(event
->keyval
) {
1122 gtk_widget_destroy(playlist_window
);
1129 /** @brief Called when the playlist window is destroyed */
1130 static void playlist_window_destroyed(GtkWidget
attribute((unused
)) *widget
,
1131 GtkWidget
**widget_pointer
) {
1132 destroy_queuelike(&ql_playlist
);
1133 playlist_picker_destroy();
1134 *widget_pointer
= NULL
;
1137 /** @brief Initialize playlist support */
1138 void playlists_init(void) {
1139 /* We re-get all playlists upon any change... */
1140 event_register("playlist-created", playlist_list_update
, 0);
1141 event_register("playlist-deleted", playlist_list_update
, 0);
1142 /* ...and on reconnection */
1143 event_register("log-connected", playlist_list_update
, 0);
1144 /* ...and from time to time */
1145 event_register("periodic-slow", playlist_list_update
, 0);
1146 /* ...and at startup */
1147 playlist_list_update(0, 0, 0);
1149 /* Update the playlists menu when the set of playlists changes */
1150 event_register("playlists-updated", playlist_menu_changed
, 0);
1151 /* Update the new-playlist OK button when the set of playlists changes */
1152 event_register("playlists-updated", playlist_new_changed
, 0);
1153 /* Update the list of playlists in the edit window when the set changes */
1154 event_register("playlists-updated", playlist_picker_fill
, 0);
1155 /* Update the displayed playlist when it is modified */
1156 event_register("playlist-modified", playlist_editor_fill
, 0);
1166 indent-tabs-mode:nil