chiark / gitweb /
Delay search initiation for a bit after the last keypress, to avoid
[disorder] / disobedience / choose.c
index 27ba940b25464b14eb6bcbc0d78ca0555a7bac7b..a275fac1c49c25dbc1879c2b8402f9ff3bccb0dd 100644 (file)
  *
  * 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,8 +35,7 @@
  * TODO:
  * - sweep up contracted nodes
  * - update when content may have changed (e.g. after a rescan)
- * - playing state
- * - display length of tracks
+ * - proper sorting
  */
 
 #include "disobedience.h"
@@ -54,26 +50,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;
 
-/** @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;
+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;
 }
 
-struct choosedata *choose_path_to_data(GtkTreePath *path) {
-  GtkTreeIter it[1];
-  gboolean itv = gtk_tree_model_get_iter(GTK_TREE_MODEL(choose_store),
-                                         it, path);
-  assert(itv);
-  return choose_iter_to_data(it);
+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
@@ -87,20 +108,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, SEARCH_RESULT_BG,
+                         FG_COLUMN, SEARCH_RESULT_FG,
+                         -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.
@@ -109,7 +164,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;
@@ -136,18 +191,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");
@@ -159,7 +214,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)
@@ -169,34 +224,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);
       }
     }
@@ -211,6 +269,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,
@@ -220,7 +290,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,
@@ -230,7 +300,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,
@@ -242,38 +339,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));
@@ -287,15 +426,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;
 }
 
 /*