X-Git-Url: https://git.distorted.org.uk/~mdw/disorder/blobdiff_plain/6982880f199dda54b194408f5b4fb3c42c734e79..2a9a65e46ee57ffa44bfc796c967b72be39ff85f:/disobedience/choose.c diff --git a/disobedience/choose.c b/disobedience/choose.c index 1a51a06..7c96293 100644 --- a/disobedience/choose.c +++ b/disobedience/choose.c @@ -22,14 +22,11 @@ * * We now use an ordinary GtkTreeStore/GtkTreeView. * - * We have an extra column with per-row data. This isn't referenced from - * anywhere the GC can see so explicit memory management is required. - * (TODO perhaps we could fix this using a gobject?) - * * We don't want to pull the entire tree in memory, but we want directories to * show up as having children. Therefore we give directories a placeholder - * child and replace their children when they are opened. Placeholders have a - * null choosedata pointer. + * child and replace their children when they are opened. Placeholders have + * TRACK_COLUMN="" and ISFILE_COLUMN=FALSE (so that they don't get check boxes, + * lengths, etc). * * TODO We do a period sweep which kills contracted nodes, putting back * placeholders, and updating expanded nodes to keep up with server-side @@ -38,9 +35,8 @@ * TODO: * - sweep up contracted nodes * - update when content may have changed (e.g. after a rescan) - * - popup menu (partially implemented now) - * - playing state - * - display length of tracks + * - searching! + * - proper sorting */ #include "disobedience.h" @@ -55,18 +51,51 @@ GtkWidget *choose_view; /** @brief The selection tree's selection */ GtkTreeSelection *choose_selection; -/** @brief Map choosedata types to names */ -static const char *const choose_type_map[] = { "track", "dir" }; +/** @brief Count of file listing operations in flight */ +static int choose_list_in_flight; + +/** @brief Count of files inserted in current batch of listing operations */ +static int choose_inserted; + +static char *choose_get_string(GtkTreeIter *iter, int column) { + gchar *gs; + gtk_tree_model_get(GTK_TREE_MODEL(choose_store), iter, + column, &gs, + -1); + char *s = xstrdup(gs); + g_free(gs); + return s; +} -/** @brief Return the choosedata given an interator */ -struct choosedata *choose_iter_to_data(GtkTreeIter *iter) { - GValue v[1]; - memset(v, 0, sizeof v); - gtk_tree_model_get_value(GTK_TREE_MODEL(choose_store), iter, CHOOSEDATA_COLUMN, v); - assert(G_VALUE_TYPE(v) == G_TYPE_POINTER); - struct choosedata *const cd = g_value_get_pointer(v); - g_value_unset(v); - return cd; +char *choose_get_track(GtkTreeIter *iter) { + char *s = choose_get_string(iter, TRACK_COLUMN); + return *s ? s : 0; /* Placeholder -> NULL */ +} + +char *choose_get_sort(GtkTreeIter *iter) { + return choose_get_string(iter, SORT_COLUMN); +} + +int choose_is_file(GtkTreeIter *iter) { + gboolean isfile; + gtk_tree_model_get(GTK_TREE_MODEL(choose_store), iter, + ISFILE_COLUMN, &isfile, + -1); + return isfile; +} + +int choose_is_dir(GtkTreeIter *iter) { + gboolean isfile; + gtk_tree_model_get(GTK_TREE_MODEL(choose_store), iter, + ISFILE_COLUMN, &isfile, + -1); + if(isfile) + return FALSE; + return !choose_is_placeholder(iter); +} + +int choose_is_placeholder(GtkTreeIter *iter) { + return choose_get_string(iter, TRACK_COLUMN)[0] == 0; } /** @brief Remove node @p it and all its children @@ -80,20 +109,54 @@ static gboolean choose_remove_node(GtkTreeIter *it) { it); while(childv) childv = choose_remove_node(child); - struct choosedata *cd = choose_iter_to_data(it); - if(cd) { - g_free(cd->track); - g_free(cd->sort); - g_free(cd); - } return gtk_tree_store_remove(choose_store, it); } +/** @brief Update length and state fields */ +static gboolean choose_set_state_callback(GtkTreeModel attribute((unused)) *model, + GtkTreePath attribute((unused)) *path, + GtkTreeIter *it, + gpointer attribute((unused)) data) { + if(choose_is_file(it)) { + const char *track = choose_get_track(it); + const long l = namepart_length(track); + char length[64]; + if(l > 0) + byte_snprintf(length, sizeof length, "%ld:%02ld", l / 60, l % 60); + else + length[0] = 0; + gtk_tree_store_set(choose_store, it, + LENGTH_COLUMN, length, + STATE_COLUMN, queued(track), + -1); + if(choose_is_search_result(track)) + gtk_tree_store_set(choose_store, it, + BG_COLUMN, "yellow", + FG_COLUMN, "black", + -1); + else + gtk_tree_store_set(choose_store, it, + BG_COLUMN, (char *)0, + FG_COLUMN, (char *)0, + -1); + } + return FALSE; /* continue walking */ +} + +/** @brief Called when the queue or playing track change */ +static void choose_set_state(const char attribute((unused)) *event, + void attribute((unused)) *eventdata, + void attribute((unused)) *callbackdata) { + gtk_tree_model_foreach(GTK_TREE_MODEL(choose_store), + choose_set_state_callback, + NULL); +} + /** @brief (Re-)populate a node * @param parent_ref Node to populate or NULL to fill root * @param nvec Number of children to add * @param vec Children - * @param dirs True if children are directories + * @param files 1 if children are files, 0 if directories * * Adjusts the set of files (or directories) below @p parent_ref to match those * listed in @p nvec and @p vec. @@ -102,7 +165,7 @@ static gboolean choose_remove_node(GtkTreeIter *it) { */ static void choose_populate(GtkTreeRowReference *parent_ref, int nvec, char **vec, - int type) { + int isfile) { /* Compute parent_* */ GtkTreeIter pit[1], *parent_it; GtkTreePath *parent_path; @@ -129,18 +192,18 @@ static void choose_populate(GtkTreeRowReference *parent_ref, it, parent_it); while(itv) { - struct choosedata *cd = choose_iter_to_data(it); + const char *track = choose_get_track(it); int keep; - if(!cd) { + if(!track) { /* Always kill placeholders */ //fprintf(stderr, " kill a placeholder\n"); keep = 0; - } else if(cd->type == type) { + } else if(choose_is_file(it) == isfile) { /* This is the type we care about */ - //fprintf(stderr, " %s is a %s\n", cd->track, choose_type_map[cd->type]); + //fprintf(stderr, " %s is a %s\n", track, isfile ? "file" : "dir"); int n; - for(n = 0; n < nvec && strcmp(vec[n], cd->track); ++n) + for(n = 0; n < nvec && strcmp(vec[n], track); ++n) ; if(n < nvec) { //fprintf(stderr, " ... and survives\n"); @@ -152,7 +215,7 @@ static void choose_populate(GtkTreeRowReference *parent_ref, } } else { /* Keep wrong-type entries */ - //fprintf(stderr, " %s is a %s\n", cd->track, choose_type_map[cd->type]); + //fprintf(stderr, " %s has wrong type\n", track); keep = 1; } if(keep) @@ -162,34 +225,37 @@ static void choose_populate(GtkTreeRowReference *parent_ref, } /* Add nodes we don't have */ int inserted = 0; - //fprintf(stderr, " inserting new %s nodes\n", choose_type_map[type]); + //fprintf(stderr, " inserting new %s nodes\n", isfile ? "track" : "dir"); + const char *typename = isfile ? "track" : "dir"; for(int n = 0; n < nvec; ++n) { if(!found[n]) { //fprintf(stderr, " %s was not found\n", vec[n]); - struct choosedata *cd = g_malloc0(sizeof *cd); - cd->type = type; - cd->track = g_strdup(vec[n]); - cd->sort = g_strdup(trackname_transform(choose_type_map[type], - vec[n], - "sort")); gtk_tree_store_append(choose_store, it, parent_it); gtk_tree_store_set(choose_store, it, - NAME_COLUMN, trackname_transform(choose_type_map[type], + NAME_COLUMN, trackname_transform(typename, vec[n], "display"), - CHOOSEDATA_COLUMN, cd, + ISFILE_COLUMN, isfile, + TRACK_COLUMN, vec[n], + SORT_COLUMN, trackname_transform(typename, + vec[n], + "sort"), -1); + /* Update length and state; we expect this to kick off length lookups + * rather than necessarily get the right value the first time round. */ + choose_set_state_callback(0, 0, it, 0); ++inserted; /* If we inserted a directory, insert a placeholder too, so it appears to * have children; it will be deleted when we expand the directory. */ - if(type == CHOOSE_DIRECTORY) { + if(!isfile) { //fprintf(stderr, " inserting a placeholder\n"); GtkTreeIter placeholder[1]; gtk_tree_store_append(choose_store, placeholder, it); gtk_tree_store_set(choose_store, placeholder, NAME_COLUMN, "Waddling...", - CHOOSEDATA_COLUMN, (void *)0, + TRACK_COLUMN, "", + ISFILE_COLUMN, FALSE, -1); } } @@ -204,6 +270,18 @@ static void choose_populate(GtkTreeRowReference *parent_ref, gtk_tree_row_reference_free(parent_ref); gtk_tree_path_free(parent_path); } + /* We only notify others that we've inserted tracks when there are no more + * insertions pending, so that they don't have to keep track of how many + * requests they've made. */ + choose_inserted += inserted; + if(--choose_list_in_flight == 0) { + /* Notify interested parties that we inserted some tracks, AFTER making + * sure that the row is properly expanded */ + if(choose_inserted) { + event_raise("choose-inserted-tracks", parent_it); + choose_inserted = 0; + } + } } static void choose_dirs_completed(void *v, @@ -213,7 +291,7 @@ static void choose_dirs_completed(void *v, popup_protocol_error(0, error); return; } - choose_populate(v, nvec, vec, CHOOSE_DIRECTORY); + choose_populate(v, nvec, vec, 0/*!isfile*/); } static void choose_files_completed(void *v, @@ -223,7 +301,34 @@ static void choose_files_completed(void *v, popup_protocol_error(0, error); return; } - choose_populate(v, nvec, vec, CHOOSE_FILE); + choose_populate(v, nvec, vec, 1/*isfile*/); +} + +void choose_play_completed(void attribute((unused)) *v, + const char *error) { + if(error) + popup_protocol_error(0, error); +} + +static void choose_state_toggled + (GtkCellRendererToggle attribute((unused)) *cell_renderer, + gchar *path_str, + gpointer attribute((unused)) user_data) { + GtkTreeIter it[1]; + /* Identify the track */ + gboolean itv = + gtk_tree_model_get_iter_from_string(GTK_TREE_MODEL(choose_store), + it, + path_str); + if(!itv) + return; + if(!choose_is_file(it)) + return; + const char *track = choose_get_track(it); + if(queued(track)) + return; + disorder_eclient_play(client, track, choose_play_completed, 0); + } static void choose_row_expanded(GtkTreeView attribute((unused)) *treeview, @@ -235,38 +340,80 @@ static void choose_row_expanded(GtkTreeView attribute((unused)) *treeview, /* We update a node's contents whenever it is expanded, even if it was * already populated; the effect is that contracting and expanding a node * suffices to update it to the latest state on the server. */ - struct choosedata *cd = choose_iter_to_data(iter); + const char *track = choose_get_track(iter); disorder_eclient_files(client, choose_files_completed, - xstrdup(cd->track), + track, NULL, gtk_tree_row_reference_new(GTK_TREE_MODEL(choose_store), path)); disorder_eclient_dirs(client, choose_dirs_completed, - xstrdup(cd->track), + track, NULL, gtk_tree_row_reference_new(GTK_TREE_MODEL(choose_store), path)); /* The row references are destroyed in the _completed handlers. */ + choose_list_in_flight += 2; } /** @brief Create the choose tab */ GtkWidget *choose_widget(void) { /* Create the tree store. */ - choose_store = gtk_tree_store_new(1 + CHOOSEDATA_COLUMN, + choose_store = gtk_tree_store_new(CHOOSE_COLUMNS, + G_TYPE_BOOLEAN, G_TYPE_STRING, - G_TYPE_POINTER); + G_TYPE_STRING, + G_TYPE_BOOLEAN, + G_TYPE_STRING, + G_TYPE_STRING, + G_TYPE_STRING, + G_TYPE_STRING); /* Create the view */ choose_view = gtk_tree_view_new_with_model(GTK_TREE_MODEL(choose_store)); + gtk_tree_view_set_rules_hint(GTK_TREE_VIEW(choose_view), TRUE); /* Create cell renderers and columns */ - GtkCellRenderer *r = gtk_cell_renderer_text_new(); - GtkTreeViewColumn *c = gtk_tree_view_column_new_with_attributes - ("Track", - r, - "text", 0, - (char *)0); - gtk_tree_view_append_column(GTK_TREE_VIEW(choose_view), c); + /* TODO use a table */ + { + GtkCellRenderer *r = gtk_cell_renderer_text_new(); + GtkTreeViewColumn *c = gtk_tree_view_column_new_with_attributes + ("Track", + r, + "text", NAME_COLUMN, + "background", BG_COLUMN, + "foreground", FG_COLUMN, + (char *)0); + gtk_tree_view_column_set_resizable(c, TRUE); + gtk_tree_view_column_set_reorderable(c, TRUE); + g_object_set(c, "expand", TRUE, (char *)0); + gtk_tree_view_append_column(GTK_TREE_VIEW(choose_view), c); + } + { + GtkCellRenderer *r = gtk_cell_renderer_text_new(); + GtkTreeViewColumn *c = gtk_tree_view_column_new_with_attributes + ("Length", + r, + "text", LENGTH_COLUMN, + (char *)0); + gtk_tree_view_column_set_resizable(c, TRUE); + gtk_tree_view_column_set_reorderable(c, TRUE); + g_object_set(r, "xalign", (gfloat)1.0, (char *)0); + gtk_tree_view_append_column(GTK_TREE_VIEW(choose_view), c); + } + { + GtkCellRenderer *r = gtk_cell_renderer_toggle_new(); + GtkTreeViewColumn *c = gtk_tree_view_column_new_with_attributes + ("Queued", + r, + "active", STATE_COLUMN, + "visible", ISFILE_COLUMN, + (char *)0); + gtk_tree_view_column_set_resizable(c, TRUE); + gtk_tree_view_column_set_reorderable(c, TRUE); + gtk_tree_view_append_column(GTK_TREE_VIEW(choose_view), c); + g_signal_connect(r, "toggled", + G_CALLBACK(choose_state_toggled), 0); + } /* The selection should support multiple things being selected */ choose_selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(choose_view)); @@ -280,15 +427,27 @@ GtkWidget *choose_widget(void) { /* Catch row expansions so we can fill in placeholders */ g_signal_connect(choose_view, "row-expanded", G_CALLBACK(choose_row_expanded), 0); + + event_register("queue-list-changed", choose_set_state, 0); + event_register("playing-track-changed", choose_set_state, 0); + event_register("search-results-changed", choose_set_state, 0); /* Fill the root */ disorder_eclient_files(client, choose_files_completed, "", NULL, NULL); disorder_eclient_dirs(client, choose_dirs_completed, "", NULL, NULL); - + /* Make the widget scrollable */ GtkWidget *scrolled = scroll_widget(choose_view); - g_object_set_data(G_OBJECT(scrolled), "type", (void *)&choose_tabtype); - return scrolled; + + /* Pack vertically with the search widget */ + GtkWidget *vbox = gtk_vbox_new(FALSE/*homogenous*/, 1/*spacing*/); + gtk_box_pack_start(GTK_BOX(vbox), scrolled, + TRUE/*expand*/, TRUE/*fill*/, 0/*padding*/); + gtk_box_pack_end(GTK_BOX(vbox), choose_search_widget(), + FALSE/*expand*/, FALSE/*fill*/, 0/*padding*/); + + g_object_set_data(G_OBJECT(vbox), "type", (void *)&choose_tabtype); + return vbox; } /*