chiark / gitweb /
Start rewriting Disobedience choose tab using native tree. Much
[disorder] / disobedience / choose.c
1 /*
2  * This file is part of DisOrder
3  * Copyright (C) 2008 Richard Kettlewell
4  *
5  * This program is free software; you can redistribute it and/or modify
6  * it under the terms of the GNU General Public License as published by
7  * the Free Software Foundation; either version 2 of the License, or
8  * (at your option) any later version.
9  *
10  * This program is distributed in the hope that it will be useful, but
11  * WITHOUT ANY WARRANTY; without even the implied warranty of
12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
13  * General Public License for more details.
14  *
15  * You should have received a copy of the GNU General Public License
16  * along with this program; if not, write to the Free Software
17  * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
18  * USA
19  */
20 /** @file disobedience/choose.c
21  * @brief Hierarchical track selection and search
22  *
23  * We now use an ordinary GtkTreeStore/GtkTreeView.
24  *
25  * We have an extra column with per-row data.  This isn't referenced from
26  * anywhere the GC can see so explicit memory management is required.
27  * (TODO perhaps we could fix this using a gobject?)
28  *
29  * We don't want to pull the entire tree in memory, but we want directories to
30  * show up as having children.  Therefore we give directories a placeholder
31  * child and replace their children when they are opened.  Placeholders have a
32  * null choosedata pointer.
33  *
34  * TODO We do a period sweep which kills contracted nodes, putting back
35  * placeholders, and updating expanded nodes to keep up with server-side
36  * changes.  (We could trigger the latter off rescan complete notifications?)
37  * 
38  * TODO:
39  * - sweep up contracted nodes
40  * - update when content may have changed (e.g. after a rescan)
41  * - popup menu
42  * - playing state
43  * - display length of tracks
44  */
45
46 #include "disobedience.h"
47
48 /** @brief Extra data at each node */
49 struct choosedata {
50   /** @brief Node type */
51   int type;
52
53   /** @brief Full track or directory name */
54   gchar *track;
55
56   /** @brief Sort key */
57   gchar *sort;
58 };
59
60 /** @brief Track name column number */
61 #define NAME_COLUMN 0
62
63 /** @brief Hidden column number */
64 #define CHOOSEDATA_COLUMN 1
65
66 /** @brief @ref choosedata node is a file */
67 #define CHOOSE_FILE 0
68
69 /** @brief @ref choosedata node is a directory */
70 #define CHOOSE_DIRECTORY 1
71
72 /** @brief The current selection tree */
73 static GtkTreeStore *choose_store;
74
75 /** @brief The view onto the selection tree */
76 static GtkWidget *choose_view;
77
78 /** @brief The selection tree's selection */
79 static GtkTreeSelection *choose_selection;
80
81 /** @brief Popup menu */
82 //static GtkWidget *choose_menu;
83
84 /** @brief Map choosedata types to names */
85 static const char *const choose_type_map[] = { "track", "dir" };
86
87 /** @brief Return the choosedata given an interator */
88 static struct choosedata *choose_iter_to_data(GtkTreeIter *iter) {
89   GValue v[1];
90   memset(v, 0, sizeof v);
91   gtk_tree_model_get_value(GTK_TREE_MODEL(choose_store), iter, CHOOSEDATA_COLUMN, v);
92   assert(G_VALUE_TYPE(v) == G_TYPE_POINTER);
93   struct choosedata *const cd = g_value_get_pointer(v);
94   g_value_unset(v);
95   return cd;
96 }
97
98 /** @brief Remove node @p it and all its children
99  * @param Iterator, updated to point to next
100  * @return True if iterator remains valid
101  */
102 static gboolean choose_remove_node(GtkTreeIter *it) {
103   GtkTreeIter child[1];
104   gboolean childv = gtk_tree_model_iter_children(GTK_TREE_MODEL(choose_store),
105                                                  child,
106                                                  it);
107   while(childv)
108     childv = choose_remove_node(child);
109   struct choosedata *cd = choose_iter_to_data(it);
110   if(cd) {
111     g_free(cd->track);
112     g_free(cd->sort);
113     g_free(cd);
114   }
115   return gtk_tree_store_remove(choose_store, it);
116 }
117
118 /** @brief (Re-)populate a node
119  * @param parent_ref Node to populate or NULL to fill root
120  * @param nvec Number of children to add
121  * @param vec Children
122  * @param dirs True if children are directories
123  *
124  * Adjusts the set of files (or directories) below @p parent_ref to match those
125  * listed in @p nvec and @p vec.
126  *
127  * @parent_ref will be destroyed.
128  */
129 static void choose_populate(GtkTreeRowReference *parent_ref,
130                             int nvec, char **vec,
131                             int type) {
132   /* Compute parent_* */
133   GtkTreeIter pit[1], *parent_it;
134   GtkTreePath *parent_path;
135   if(parent_ref) {
136     parent_path = gtk_tree_row_reference_get_path(parent_ref);
137     parent_it = pit;
138     gboolean pitv = gtk_tree_model_get_iter(GTK_TREE_MODEL(choose_store),
139                                             pit, parent_path);
140     assert(pitv);
141     /*fprintf(stderr, "choose_populate %s: parent path is [%s]\n",
142             choose_type_map[type],
143             gtk_tree_path_to_string(parent_path));*/
144   } else {
145     parent_path = 0;
146     parent_it = 0;
147     /*fprintf(stderr, "choose_populate %s: populating the root\n",
148             choose_type_map[type]);*/
149   }
150   /* Remove unwanted nodes and find out which we must add */
151   //fprintf(stderr, " trimming unwanted %s nodes\n", choose_type_map[type]);
152   char *found = xmalloc(nvec);
153   GtkTreeIter it[1];
154   gboolean itv = gtk_tree_model_iter_children(GTK_TREE_MODEL(choose_store),
155                                               it,
156                                               parent_it);
157   while(itv) {
158     struct choosedata *cd = choose_iter_to_data(it);
159     int keep;
160
161     if(!cd)  {
162       /* Always kill placeholders */
163       //fprintf(stderr, "  kill a placeholder\n");
164       keep = 0;
165     } else if(cd->type == type) {
166       /* This is the type we care about */
167       //fprintf(stderr, "  %s is a %s\n", cd->track, choose_type_map[cd->type]);
168       int n;
169       for(n = 0; n < nvec && strcmp(vec[n], cd->track); ++n)
170         ;
171       if(n < nvec) {
172         //fprintf(stderr, "   ... and survives\n");
173         found[n] = 1;
174         keep = 1;
175       } else {
176         //fprintf(stderr, "   ... and is to be removed\n");
177         keep = 0;
178       }
179     } else {
180       /* Keep wrong-type entries */
181       //fprintf(stderr, "  %s is a %s\n", cd->track, choose_type_map[cd->type]);
182       keep = 1;
183     }
184     if(keep)
185       itv = gtk_tree_model_iter_next(GTK_TREE_MODEL(choose_store), it);
186     else
187       itv = choose_remove_node(it);
188   }
189   /* Add nodes we don't have */
190   int inserted = 0;
191   //fprintf(stderr, " inserting new %s nodes\n", choose_type_map[type]);
192   for(int n = 0; n < nvec; ++n) {
193     if(!found[n]) {
194       //fprintf(stderr, "  %s was not found\n", vec[n]);
195       struct choosedata *cd = g_malloc0(sizeof *cd);
196       cd->type = type;
197       cd->track = g_strdup(vec[n]);
198       cd->sort = g_strdup(trackname_transform(choose_type_map[type],
199                                               vec[n],
200                                               "sort"));
201       gtk_tree_store_append(choose_store, it, parent_it);
202       gtk_tree_store_set(choose_store, it,
203                          NAME_COLUMN, trackname_transform(choose_type_map[type],
204                                                           vec[n],
205                                                           "display"),
206                          CHOOSEDATA_COLUMN, cd,
207                          -1);
208       ++inserted;
209       /* If we inserted a directory, insert a placeholder too, so it appears to
210        * have children; it will be deleted when we expand the directory. */
211       if(type == CHOOSE_DIRECTORY) {
212         //fprintf(stderr, "  inserting a placeholder\n");
213         GtkTreeIter placeholder[1];
214
215         gtk_tree_store_append(choose_store, placeholder, it);
216         gtk_tree_store_set(choose_store, placeholder,
217                            NAME_COLUMN, "Waddling...",
218                            CHOOSEDATA_COLUMN, (void *)0,
219                            -1);
220       }
221     }
222   }
223   //fprintf(stderr, " %d nodes inserted\n", inserted);
224   if(inserted) {
225     /* TODO sort the children */
226   }
227   if(parent_ref) {
228     /* If we deleted a placeholder then we must re-expand the row */
229     gtk_tree_view_expand_row(GTK_TREE_VIEW(choose_view), parent_path, FALSE);
230     gtk_tree_row_reference_free(parent_ref);
231     gtk_tree_path_free(parent_path);
232   }
233 }
234
235 static void choose_dirs_completed(void *v,
236                                   const char *error,
237                                   int nvec, char **vec) {
238   if(error) {
239     popup_protocol_error(0, error);
240     return;
241   }
242   choose_populate(v, nvec, vec, CHOOSE_DIRECTORY);
243 }
244
245 static void choose_files_completed(void *v,
246                                    const char *error,
247                                    int nvec, char **vec) {
248   if(error) {
249     popup_protocol_error(0, error);
250     return;
251   }
252   choose_populate(v, nvec, vec, CHOOSE_FILE);
253 }
254
255 static void choose_row_expanded(GtkTreeView attribute((unused)) *treeview,
256                                 GtkTreeIter *iter,
257                                 GtkTreePath *path,
258                                 gpointer attribute((unused)) user_data) {
259   /*fprintf(stderr, "row-expanded path=[%s]\n",
260           gtk_tree_path_to_string(path));*/
261   /* We update a node's contents whenever it is expanded, even if it was
262    * already populated; the effect is that contracting and expanding a node
263    * suffices to update it to the latest state on the server. */
264   struct choosedata *cd = choose_iter_to_data(iter);
265   disorder_eclient_files(client, choose_files_completed,
266                          xstrdup(cd->track),
267                          NULL,
268                          gtk_tree_row_reference_new(GTK_TREE_MODEL(choose_store),
269                                                     path));
270   disorder_eclient_dirs(client, choose_dirs_completed,
271                         xstrdup(cd->track),
272                         NULL,
273                         gtk_tree_row_reference_new(GTK_TREE_MODEL(choose_store),
274                                                    path));
275   /* The row references are destroyed in the _completed handlers. */
276 }
277
278 static int choose_tab_selectall_sensitive(void attribute((unused)) *extra) {
279   return TRUE;
280 }
281   
282 static void choose_tab_selectall_activate(void attribute((unused)) *extra) {
283   gtk_tree_selection_select_all(choose_selection);
284 }
285   
286 static int choose_tab_selectnone_sensitive(void attribute((unused)) *extra) {
287   return gtk_tree_selection_count_selected_rows(choose_selection) > 0;
288 }
289   
290 static void choose_tab_selectnone_activate(void attribute((unused)) *extra) {
291   gtk_tree_selection_unselect_all(choose_selection);
292 }
293   
294 static int choose_tab_properties_sensitive(void attribute((unused)) *extra) {
295   return TRUE;
296 }
297   
298 static void choose_tab_properties_activate(void attribute((unused)) *extra) {
299   fprintf(stderr, "TODO choose_tab_properties_activate\n");
300 }
301
302 static const struct tabtype choose_tabtype = {
303   choose_tab_properties_sensitive,
304   choose_tab_selectall_sensitive,
305   choose_tab_selectnone_sensitive,
306   choose_tab_properties_activate,
307   choose_tab_selectall_activate,
308   choose_tab_selectnone_activate,
309   0,
310   0
311 };
312
313 /** @brief Create the choose tab */
314 GtkWidget *choose_widget(void) {
315   /* Create the tree store. */
316   choose_store = gtk_tree_store_new(1 + CHOOSEDATA_COLUMN,
317                                     G_TYPE_STRING,
318                                     G_TYPE_POINTER);
319
320   /* Create the view */
321   choose_view = gtk_tree_view_new_with_model(GTK_TREE_MODEL(choose_store));
322
323   /* Create cell renderers and columns */
324   GtkCellRenderer *r = gtk_cell_renderer_text_new();
325   GtkTreeViewColumn *c = gtk_tree_view_column_new_with_attributes
326     ("Track",
327      r,
328      "text", 0,
329      (char *)0);
330   gtk_tree_view_append_column(GTK_TREE_VIEW(choose_view), c);
331   
332   /* The selection should support multiple things being selected */
333   choose_selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(choose_view));
334   gtk_tree_selection_set_mode(choose_selection, GTK_SELECTION_MULTIPLE);
335
336   /* Catch button presses */
337   /*g_signal_connect(choose_view, "button-press-event",
338     G_CALLBACK(choose_button_release), 0);*/
339   /* Catch row expansions so we can fill in placeholders */
340   g_signal_connect(choose_view, "row-expanded",
341                    G_CALLBACK(choose_row_expanded), 0);
342   
343   /* Fill the root */
344   disorder_eclient_files(client, choose_files_completed, "", NULL, NULL); 
345   disorder_eclient_dirs(client, choose_dirs_completed, "", NULL, NULL); 
346   
347   /* Make the widget scrollable */
348   GtkWidget *scrolled = scroll_widget(choose_view);
349   g_object_set_data(G_OBJECT(scrolled), "type", (void *)&choose_tabtype);
350   return scrolled;
351 }
352
353 /*
354 Local Variables:
355 c-basic-offset:2
356 comment-column:40
357 fill-column:79
358 indent-tabs-mode:nil
359 End:
360 */