X-Git-Url: http://www.chiark.greenend.org.uk/ucgi/~mdw/git/disorder/blobdiff_plain/aeec47be468c173a57710af7ae22c399e930b78e..96295b258d742325bbc9353775d26ed35e9f69dc:/disobedience/playlists.c diff --git a/disobedience/playlists.c b/disobedience/playlists.c index c10ef6f..1f836ad 100644 --- a/disobedience/playlists.c +++ b/disobedience/playlists.c @@ -114,6 +114,23 @@ static void playlist_editor_got_share(void *v, const char *err, const char *value); static void playlist_editor_share_set(void *v, const char *err); +static void playlist_picker_update_section(const char *title, const char *key, + int start, int end); +static gboolean playlist_picker_find(GtkTreeIter *parent, + const char *title, const char *key, + GtkTreeIter iter[1], + gboolean create); +static void playlist_picker_delete_obsolete(GtkTreeIter parent[1], + char **exists, + int nexists); +static gboolean playlist_picker_button(GtkWidget *widget, + GdkEventButton *event, + gpointer user_data); +static gboolean playlist_editor_keypress(GtkWidget *widget, + GdkEventKey *event, + gpointer user_data); +static void playlist_editor_ok(GtkButton *button, gpointer userdata); +static void playlist_editor_help(GtkButton *button, gpointer userdata); /** @brief Playlist editing window */ static GtkWidget *playlist_window; @@ -137,7 +154,7 @@ static struct menuitem playlist_menuitems[] = { { "Track properties", ql_properties_activate, ql_properties_sensitive, 0, 0 }, { "Play track", ql_play_activate, ql_play_sensitive, 0, 0 }, { "Play playlist", playlist_playall_activate, playlist_playall_sensitive, 0, 0 }, - { "Remove track from queue", playlist_remove_activate, playlist_remove_sensitive, 0, 0 }, + { "Remove track from playlist", playlist_remove_activate, playlist_remove_sensitive, 0, 0 }, { "Select all tracks", ql_selectall_activate, ql_selectall_sensitive, 0, 0 }, { "Deselect all tracks", ql_selectnone_activate, ql_selectnone_sensitive, 0, 0 }, }; @@ -238,7 +255,7 @@ static int playlistcmp(const void *ap, const void *bp) { static void playlist_menu_playing(void attribute((unused)) *v, const char *err) { if(err) - popup_protocol_error(0, err); + popup_submsg(playlist_window, GTK_MESSAGE_ERROR, err); } /** @brief Play received playlist contents @@ -249,7 +266,7 @@ static void playlist_menu_received_content(void attribute((unused)) *v, const char *err, int nvec, char **vec) { if(err) { - popup_protocol_error(0, err); + popup_submsg(playlist_window, GTK_MESSAGE_ERROR, err); return; } for(int n = 0; n < nvec; ++n) @@ -442,7 +459,7 @@ static void playlist_new_ok(GtkButton attribute((unused)) *button, static void playlist_new_locked(void *v, const char *err) { char *fullname = v; if(err) { - popup_protocol_error(0, err); + popup_submsg(playlist_window, GTK_MESSAGE_ERROR, err); return; } disorder_eclient_playlist_get(client, playlist_new_retrieved, @@ -461,7 +478,7 @@ static void playlist_new_retrieved(void *v, const char *err, /* A rare case but not in principle impossible */ err = "A playlist with that name already exists."; if(err) { - popup_protocol_error(0, err); + popup_submsg(playlist_window, GTK_MESSAGE_ERROR, err); disorder_eclient_playlist_unlock(client, playlist_new_unlocked, fullname); return; } @@ -477,7 +494,7 @@ static void playlist_new_retrieved(void *v, const char *err, /** @brief Called when the new playlist has been created */ static void playlist_new_created(void attribute((unused)) *v, const char *err) { if(err) { - popup_protocol_error(0, err); + popup_submsg(playlist_window, GTK_MESSAGE_ERROR, err); return; } disorder_eclient_playlist_unlock(client, playlist_new_unlocked, NULL); @@ -487,7 +504,7 @@ static void playlist_new_created(void attribute((unused)) *v, const char *err) { /** @brief Called when the newly created playlist has unlocked */ static void playlist_new_unlocked(void attribute((unused)) *v, const char *err) { if(err) - popup_protocol_error(0, err); + popup_submsg(playlist_window, GTK_MESSAGE_ERROR, err); /* Pop down the creation window */ gtk_widget_destroy(playlist_new_window); } @@ -594,8 +611,13 @@ static void playlist_new_details(char **namep, /** @brief Delete button */ static GtkWidget *playlist_picker_delete_button; -/** @brief Tree model for list of playlists */ -static GtkListStore *playlist_picker_list; +/** @brief Tree model for list of playlists + * + * This has two columns: + * - column 0 will be the display name + * - column 1 will be the sort key/playlist name (and will not be displayed) + */ +static GtkTreeStore *playlist_picker_list; /** @brief Selection for list of playlists */ static GtkTreeSelection *playlist_picker_selection; @@ -607,25 +629,169 @@ static const char *playlist_picker_selected; static void playlist_picker_fill(const char attribute((unused)) *event, void attribute((unused)) *eventdata, void attribute((unused)) *callbackdata) { - GtkTreeIter iter[1]; - if(!playlist_window) return; if(!playlist_picker_list) - playlist_picker_list = gtk_list_store_new(1, G_TYPE_STRING); - const char *was_selected = playlist_picker_selected; - gtk_list_store_clear(playlist_picker_list); /* clears playlists_selected */ - for(int n = 0; n < nplaylists; ++n) { - gtk_list_store_insert_with_values(playlist_picker_list, iter, - n /*position*/, - 0, playlists[n], /* column 0 */ - -1); /* no more cols */ - /* Reselect the selected playlist */ - if(was_selected && !strcmp(was_selected, playlists[n])) - gtk_tree_selection_select_iter(playlist_picker_selection, iter); + playlist_picker_list = gtk_tree_store_new(2, G_TYPE_STRING, G_TYPE_STRING); + /* We will accumulate a list of all the sections that exist */ + char **sections = xcalloc(nplaylists, sizeof (char *)); + int nsections = 0; + /* Make sure shared playlists are there */ + int start = 0, end; + for(end = start; end < nplaylists && !strchr(playlists[end], '.'); ++end) + ; + if(start != end) { + playlist_picker_update_section("Shared playlists", "", + start, end); + sections[nsections++] = (char *)""; + } + /* Make sure owned playlists are there */ + while((start = end) < nplaylists) { + const int nl = strchr(playlists[start], '.') - playlists[start]; + char *name = xstrndup(playlists[start], nl); + for(end = start; + end < nplaylists + && playlists[end][nl] == '.' + && !strncmp(playlists[start], playlists[end], nl); + ++end) + ; + playlist_picker_update_section(name, name, start, end); + sections[nsections++] = name; + } + /* Delete obsolete sections */ + playlist_picker_delete_obsolete(NULL, sections, nsections); +} + +/** @brief Update a section in the picker tree model + * @param section Section name + * @param start First entry in @ref playlists + * @param end Past last entry in @ref playlists + */ +static void playlist_picker_update_section(const char *title, const char *key, + int start, int end) { + /* Find the section, creating it if necessary */ + GtkTreeIter section_iter[1]; + playlist_picker_find(NULL, title, key, section_iter, TRUE); + /* Add missing rows */ + for(int n = start; n < end; ++n) { + GtkTreeIter child[1]; + char *name; + if((name = strchr(playlists[n], '.'))) + ++name; + else + name = playlists[n]; + playlist_picker_find(section_iter, + name, playlists[n], + child, + TRUE); + } + /* Delete anything that shouldn't exist. */ + playlist_picker_delete_obsolete(section_iter, playlists + start, end - start); +} + +/** @brief Find and maybe create a row in the picker tree model + * @param parent Parent iterator (or NULL for top level) + * @param title Display name of section + * @param key Key to search for + * @param iter Iterator to point at key + * @param create If TRUE, key will be created if it doesn't exist + * @param compare Row comparison function + * @return TRUE if key exists else FALSE + * + * If the @p key exists then @p iter will point to it and TRUE will be + * returned. + * + * If the @p key does not exist and @p create is TRUE then it will be created. + * @p iter wil point to it and TRUE will be returned. + * + * If the @p key does not exist and @p create is FALSE then FALSE will be + * returned. + */ +static gboolean playlist_picker_find(GtkTreeIter *parent, + const char *title, + const char *key, + GtkTreeIter iter[1], + gboolean create) { + gchar *candidate; + GtkTreeIter next[1]; + gboolean it; + int row = 0; + + it = gtk_tree_model_iter_children(GTK_TREE_MODEL(playlist_picker_list), + next, + parent); + while(it) { + /* Find the value at row 'next' */ + gtk_tree_model_get(GTK_TREE_MODEL(playlist_picker_list), + next, + 1, &candidate, + -1); + /* See how it compares with @p key */ + int c = strcmp(key, candidate); + g_free(candidate); + if(!c) { + *iter = *next; + return TRUE; /* we found our key */ + } + if(c < 0) { + /* @p key belongs before row 'next' */ + if(create) { + gtk_tree_store_insert_with_values(playlist_picker_list, + iter, + parent, + row, /* insert here */ + 0, title, 1, key, -1); + return TRUE; + } else + return FALSE; + ++row; + } + it = gtk_tree_model_iter_next(GTK_TREE_MODEL(playlist_picker_list), next); + } + /* We have reached the end and not found a row that should be later than @p + * key. */ + if(create) { + gtk_tree_store_insert_with_values(playlist_picker_list, + iter, + parent, + INT_MAX, /* insert at end */ + 0, title, 1, key, -1); + return TRUE; + } else + return FALSE; +} + +/** @brief Delete obsolete rows + * @param parent Parent or NULL + * @param exists List of rows that should exist (by key) + * @param nexists Length of @p exists + */ +static void playlist_picker_delete_obsolete(GtkTreeIter parent[1], + char **exists, + int nexists) { + /* Delete anything that shouldn't exist. */ + GtkTreeIter iter[1]; + gboolean it = gtk_tree_model_iter_children(GTK_TREE_MODEL(playlist_picker_list), + iter, + parent); + while(it) { + /* Find the value at row 'next' */ + gchar *candidate; + gtk_tree_model_get(GTK_TREE_MODEL(playlist_picker_list), + iter, + 1, &candidate, + -1); + gboolean found = FALSE; + for(int n = 0; n < nexists; ++n) + if((found = !strcmp(candidate, exists[n]))) + break; + if(!found) + it = gtk_tree_store_remove(playlist_picker_list, iter); + else + it = gtk_tree_model_iter_next(GTK_TREE_MODEL(playlist_picker_list), + iter); + g_free(candidate); } - /* TODO deselecting then reselecting the current playlist resets the playlist - * editor, which trashes the user's selection. */ } /** @brief Called when the selection might have changed */ @@ -635,20 +801,24 @@ static void playlist_picker_selection_changed(GtkTreeSelection attribute((unused char *gselected, *selected; /* Identify the current selection */ - if(gtk_tree_selection_get_selected(playlist_picker_selection, 0, &iter)) { + if(gtk_tree_selection_get_selected(playlist_picker_selection, 0, &iter) + && gtk_tree_store_iter_depth(playlist_picker_list, &iter) > 0) { gtk_tree_model_get(GTK_TREE_MODEL(playlist_picker_list), &iter, - 0, &gselected, -1); + 1, &gselected, -1); selected = xstrdup(gselected); g_free(gselected); } else selected = 0; /* Set button sensitivity according to the new state */ - if(selected) - gtk_widget_set_sensitive(playlist_picker_delete_button, 1); - else - gtk_widget_set_sensitive(playlist_picker_delete_button, 0); - /* TODO delete should not be sensitive for public playlists owned by other - * users */ + int deletable = FALSE; + if(selected) { + if(strchr(selected, '.')) { + if(!strncmp(selected, config->username, strlen(config->username))) + deletable = TRUE; + } else + deletable = TRUE; + } + gtk_widget_set_sensitive(playlist_picker_delete_button, deletable); /* Eliminate no-change cases */ if(!selected && !playlist_picker_selected) return; @@ -676,7 +846,7 @@ static void playlist_picker_add(GtkButton attribute((unused)) *button, static void playlists_picker_delete_completed(void attribute((unused)) *v, const char *err) { if(err) - popup_protocol_error(0, err); + popup_submsg(playlist_window, GTK_MESSAGE_ERROR, err); } /** @brief Called when the 'Delete' button is pressed */ @@ -710,13 +880,15 @@ static struct button playlist_picker_buttons[] = { GTK_STOCK_ADD, playlist_picker_add, "Create a new playlist", - 0 + 0, + NULL, }, { GTK_STOCK_REMOVE, playlist_picker_delete, "Delete a playlist", - 0 + 0, + NULL, }, }; #define NPLAYLIST_PICKER_BUTTONS (sizeof playlist_picker_buttons / sizeof *playlist_picker_buttons) @@ -757,6 +929,8 @@ static GtkWidget *playlist_picker_create(void) { g_signal_connect(tree, "key-press-event", G_CALLBACK(playlist_picker_keypress), 0); + g_signal_connect(tree, "button-press-event", + G_CALLBACK(playlist_picker_button), 0); return vbox; } @@ -776,6 +950,93 @@ static gboolean playlist_picker_keypress(GtkWidget attribute((unused)) *widget, } } +static void playlist_picker_select_activate(GtkMenuItem attribute((unused)) *item, + gpointer attribute((unused)) userdata) { + /* nothing */ +} + +static int playlist_picker_select_sensitive(void *extra) { + GtkTreeIter *iter = extra; + return gtk_tree_store_iter_depth(playlist_picker_list, iter) > 0; +} + +static void playlist_picker_play_activate(GtkMenuItem attribute((unused)) *item, + gpointer attribute((unused)) userdata) { + /* Re-use the menu-based activation callback */ + disorder_eclient_playlist_get(client, playlist_menu_received_content, + playlist_picker_selected, NULL); +} + +static int playlist_picker_play_sensitive(void *extra) { + GtkTreeIter *iter = extra; + return gtk_tree_store_iter_depth(playlist_picker_list, iter) > 0; +} + +static void playlist_picker_remove_activate(GtkMenuItem attribute((unused)) *item, + gpointer attribute((unused)) userdata) { + /* Re-use the 'Remove' button' */ + playlist_picker_delete(NULL, NULL); +} + +static int playlist_picker_remove_sensitive(void *extra) { + GtkTreeIter *iter = extra; + if(gtk_tree_store_iter_depth(playlist_picker_list, iter) > 0) { + if(strchr(playlist_picker_selected, '.')) { + if(!strncmp(playlist_picker_selected, config->username, + strlen(config->username))) + return TRUE; + } else + return TRUE; + } + return FALSE; +} + +/** @brief Pop-up menu for picker */ +static struct menuitem playlist_picker_menuitems[] = { + { + "Select playlist", + playlist_picker_select_activate, + playlist_picker_select_sensitive, + 0, + 0 + }, + { + "Play playlist", + playlist_picker_play_activate, + playlist_picker_play_sensitive, + 0, + 0 + }, + { + "Remove playlist", + playlist_picker_remove_activate, + playlist_picker_remove_sensitive, + 0, + 0 + }, +}; + +static gboolean playlist_picker_button(GtkWidget *widget, + GdkEventButton *event, + gpointer attribute((unused)) user_data) { + if(event->type == GDK_BUTTON_PRESS && event->button == 3) { + static GtkWidget *playlist_picker_menu; + + /* Right click press pops up a menu */ + ensure_selected(GTK_TREE_VIEW(widget), event); + /* Find the selected row */ + GtkTreeIter iter[1]; + if(!gtk_tree_selection_get_selected(playlist_picker_selection, 0, iter)) + return TRUE; + popup(&playlist_picker_menu, event, + playlist_picker_menuitems, + sizeof playlist_picker_menuitems / sizeof *playlist_picker_menuitems, + iter); + return TRUE; + } + return FALSE; +} + static void playlist_picker_destroy(void) { playlist_picker_delete_button = NULL; g_object_unref(playlist_picker_list); @@ -791,6 +1052,26 @@ static GtkWidget *playlist_editor_public; static GtkWidget *playlist_editor_private; static int playlist_editor_setting_buttons; +/** @brief Buttons for the playlist window */ +static struct button playlist_editor_buttons[] = { + { + GTK_STOCK_OK, + playlist_editor_ok, + "Close window", + 0, + gtk_box_pack_end, + }, + { + GTK_STOCK_HELP, + playlist_editor_help, + "Go to manual", + 0, + gtk_box_pack_end, + }, +}; + +#define NPLAYLIST_EDITOR_BUTTONS (int)(sizeof playlist_editor_buttons / sizeof *playlist_editor_buttons) + static GtkWidget *playlists_editor_create(void) { assert(ql_playlist.view == NULL); /* better not be set up already */ @@ -815,15 +1096,36 @@ static GtkWidget *playlists_editor_create(void) { gtk_box_pack_start(GTK_BOX(hbox), playlist_editor_private, FALSE/*expand*/, FALSE/*fill*/, 0); playlist_editor_set_buttons(0,0,0); + create_buttons_box(playlist_editor_buttons, + NPLAYLIST_EDITOR_BUTTONS, + hbox); GtkWidget *vbox = gtk_vbox_new(FALSE, 0); - gtk_box_pack_start(GTK_BOX(vbox), init_queuelike(&ql_playlist), + GtkWidget *view = init_queuelike(&ql_playlist); + gtk_box_pack_start(GTK_BOX(vbox), view, TRUE/*expand*/, TRUE/*fill*/, 0); gtk_box_pack_start(GTK_BOX(vbox), hbox, FALSE/*expand*/, FALSE/*fill*/, 0); + g_signal_connect(view, "key-press-event", + G_CALLBACK(playlist_editor_keypress), 0); return vbox; } +static gboolean playlist_editor_keypress(GtkWidget attribute((unused)) *widget, + GdkEventKey *event, + gpointer attribute((unused)) user_data) { + if(event->state) + return FALSE; + switch(event->keyval) { + case GDK_BackSpace: + case GDK_Delete: + playlist_remove_activate(NULL, NULL); + return TRUE; + default: + return FALSE; + } +} + /** @brief Called when the public/private buttons are set */ static void playlist_editor_button_toggled(GtkToggleButton *tb, gpointer userdata) { @@ -839,7 +1141,7 @@ static void playlist_editor_button_toggled(GtkToggleButton *tb, static void playlist_editor_share_set(void attribute((unused)) *v, const attribute((unused)) char *err) { if(err) - popup_protocol_error(0, err); + popup_submsg(playlist_window, GTK_MESSAGE_ERROR, err); } /** @brief Set the editor button state and sensitivity */ @@ -870,7 +1172,7 @@ static void playlist_editor_got_share(void *v, const char *value) { const char *playlist = v; if(err) { - popup_protocol_error(0, err); + popup_submsg(playlist_window, GTK_MESSAGE_ERROR, err); value = NULL; } /* Set the currently active button */ @@ -913,7 +1215,7 @@ static void playlists_editor_received_tracks(void *v, int nvec, char **vec) { const char *playlist = v; if(err) { - popup_protocol_error(0, err); + popup_submsg(playlist_window, GTK_MESSAGE_ERROR, err); return; } if(!playlist_picker_selected @@ -947,6 +1249,16 @@ static void playlists_editor_received_tracks(void *v, ql_new_queue(&ql_playlist, newq); } +static void playlist_editor_ok(GtkButton attribute((unused)) *button, + gpointer attribute((unused)) userdata) { + gtk_widget_destroy(playlist_window); +} + +static void playlist_editor_help(GtkButton attribute((unused)) *button, + gpointer attribute((unused)) userdata) { + popup_help("playlists.html"); +} + /* Playlist mutation -------------------------------------------------------- */ /** @brief State structure for guarded playlist modification @@ -995,7 +1307,7 @@ struct playlist_modify_data { static void playlist_modify_locked(void *v, const char *err) { struct playlist_modify_data *mod = v; if(err) { - popup_protocol_error(0, err); + popup_submsg(playlist_window, GTK_MESSAGE_ERROR, err); return; } disorder_eclient_playlist_get(client, playlist_modify_retrieved, @@ -1010,7 +1322,7 @@ void playlist_modify_retrieved(void *v, const char *err, char **vec) { struct playlist_modify_data *mod = v; if(err) { - popup_protocol_error(0, err); + popup_submsg(playlist_window, GTK_MESSAGE_ERROR, err); disorder_eclient_playlist_unlock(client, playlist_modify_unlocked, NULL); return; } @@ -1038,7 +1350,7 @@ void playlist_modify_retrieved(void *v, const char *err, static void playlist_modify_updated(void attribute((unused)) *v, const char *err) { if(err) - popup_protocol_error(0, err); + popup_submsg(playlist_window, GTK_MESSAGE_ERROR, err); disorder_eclient_playlist_unlock(client, playlist_modify_unlocked, NULL); } @@ -1046,7 +1358,7 @@ static void playlist_modify_updated(void attribute((unused)) *v, static void playlist_modify_unlocked(void attribute((unused)) *v, const char *err) { if(err) - popup_protocol_error(0, err); + popup_submsg(playlist_window, GTK_MESSAGE_ERROR, err); } /* Drop tracks into a playlist ---------------------------------------------- */