*
* 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
* TODO:
* - sweep up contracted nodes
* - update when content may have changed (e.g. after a rescan)
- * - playing state
- * - display length of tracks
+ * - searching!
+ * - proper sorting
*/
#include "disobedience.h"
/** @brief The selection tree's selection */
GtkTreeSelection *choose_selection;
-/** @brief Map choosedata types to names */
-static const char *const choose_type_map[] = { "track", "dir" };
+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);
+}
-/** @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;
+int choose_is_placeholder(GtkTreeIter *iter) {
+ return choose_get_string(iter, TRACK_COLUMN)[0] == 0;
}
/** @brief Remove node @p it and all its children
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.
*/
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;
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");
}
} 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)
/* 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);
}
}
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,
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,
/* 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));
/** @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,
+ 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));
/* 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;
}
/*