X-Git-Url: https://git.distorted.org.uk/~mdw/disorder/blobdiff_plain/ebcfbfefdd8ba3bae79daf5e4a2385c012aa7591..a98fd5717e59e245fbf543872cfe417b863fd6e1:/disobedience/choose.c?ds=sidebyside diff --git a/disobedience/choose.c b/disobedience/choose.c index 569b8b7..a564a11 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,61 +35,64 @@ * TODO: * - sweep up contracted nodes * - update when content may have changed (e.g. after a rescan) - * - popup menu - * - playing state - * - display length of tracks + * - searching! + * - proper sorting */ #include "disobedience.h" - -/** @brief Extra data at each node */ -struct choosedata { - /** @brief Node type */ - int type; - - /** @brief Full track or directory name */ - gchar *track; - - /** @brief Sort key */ - gchar *sort; -}; - -/** @brief Track name column number */ -#define NAME_COLUMN 0 - -/** @brief Hidden column number */ -#define CHOOSEDATA_COLUMN 1 - -/** @brief @ref choosedata node is a file */ -#define CHOOSE_FILE 0 - -/** @brief @ref choosedata node is a directory */ -#define CHOOSE_DIRECTORY 1 +#include "choose.h" /** @brief The current selection tree */ -static GtkTreeStore *choose_store; +GtkTreeStore *choose_store; /** @brief The view onto the selection tree */ -static GtkWidget *choose_view; +GtkWidget *choose_view; /** @brief The selection tree's selection */ -static GtkTreeSelection *choose_selection; - -/** @brief Popup menu */ -//static GtkWidget *choose_menu; +GtkTreeSelection *choose_selection; /** @brief Map choosedata types to names */ static const char *const choose_type_map[] = { "track", "dir" }; -/** @brief Return the choosedata given an interator */ -static 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; +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; +} + +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 @@ -106,20 +106,44 @@ 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); + } + 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. @@ -128,7 +152,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; @@ -155,18 +179,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"); @@ -178,7 +202,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) @@ -189,33 +213,36 @@ 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]); + 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); } } @@ -239,7 +266,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, @@ -249,7 +276,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, @@ -261,84 +315,91 @@ 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. */ } -static int choose_tab_selectall_sensitive(void attribute((unused)) *extra) { - return TRUE; -} - -static void choose_tab_selectall_activate(void attribute((unused)) *extra) { - gtk_tree_selection_select_all(choose_selection); -} - -static int choose_tab_selectnone_sensitive(void attribute((unused)) *extra) { - return gtk_tree_selection_count_selected_rows(choose_selection) > 0; -} - -static void choose_tab_selectnone_activate(void attribute((unused)) *extra) { - gtk_tree_selection_unselect_all(choose_selection); -} - -static int choose_tab_properties_sensitive(void attribute((unused)) *extra) { - return TRUE; -} - -static void choose_tab_properties_activate(void attribute((unused)) *extra) { - fprintf(stderr, "TODO choose_tab_properties_activate\n"); -} - -static const struct tabtype choose_tabtype = { - choose_tab_properties_sensitive, - choose_tab_selectall_sensitive, - choose_tab_selectnone_sensitive, - choose_tab_properties_activate, - choose_tab_selectall_activate, - choose_tab_selectnone_activate, - 0, - 0 -}; - /** @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_STRING, - G_TYPE_POINTER); + G_TYPE_BOOLEAN, + 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, + (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)); gtk_tree_selection_set_mode(choose_selection, GTK_SELECTION_MULTIPLE); /* Catch button presses */ - /*g_signal_connect(choose_view, "button-press-event", - G_CALLBACK(choose_button_release), 0);*/ + g_signal_connect(choose_view, "button-press-event", + G_CALLBACK(choose_button_event), 0); + g_signal_connect(choose_view, "button-release-event", + G_CALLBACK(choose_button_event), 0); /* 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); /* Fill the root */ disorder_eclient_files(client, choose_files_completed, "", NULL, NULL);