chiark / gitweb /
a505478bb907fe45c6975fad076cf3f29e8d7529
[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  * - playing state
42  * - display length of tracks
43  */
44
45 #include "disobedience.h"
46 #include "choose.h"
47
48 /** @brief The current selection tree */
49 GtkTreeStore *choose_store;
50
51 /** @brief The view onto the selection tree */
52 GtkWidget *choose_view;
53
54 /** @brief The selection tree's selection */
55 GtkTreeSelection *choose_selection;
56
57 /** @brief Map choosedata types to names */
58 static const char *const choose_type_map[] = { "track", "dir" };
59
60 /** @brief Return the choosedata given an interator */
61 struct choosedata *choose_iter_to_data(GtkTreeIter *iter) {
62   GValue v[1];
63   memset(v, 0, sizeof v);
64   gtk_tree_model_get_value(GTK_TREE_MODEL(choose_store), iter, CHOOSEDATA_COLUMN, v);
65   assert(G_VALUE_TYPE(v) == G_TYPE_POINTER);
66   struct choosedata *const cd = g_value_get_pointer(v);
67   g_value_unset(v);
68   return cd;
69 }
70
71 /** @brief Remove node @p it and all its children
72  * @param Iterator, updated to point to next
73  * @return True if iterator remains valid
74  */
75 static gboolean choose_remove_node(GtkTreeIter *it) {
76   GtkTreeIter child[1];
77   gboolean childv = gtk_tree_model_iter_children(GTK_TREE_MODEL(choose_store),
78                                                  child,
79                                                  it);
80   while(childv)
81     childv = choose_remove_node(child);
82   struct choosedata *cd = choose_iter_to_data(it);
83   if(cd) {
84     g_free(cd->track);
85     g_free(cd->sort);
86     g_free(cd);
87   }
88   return gtk_tree_store_remove(choose_store, it);
89 }
90
91 /** @brief (Re-)populate a node
92  * @param parent_ref Node to populate or NULL to fill root
93  * @param nvec Number of children to add
94  * @param vec Children
95  * @param dirs True if children are directories
96  *
97  * Adjusts the set of files (or directories) below @p parent_ref to match those
98  * listed in @p nvec and @p vec.
99  *
100  * @parent_ref will be destroyed.
101  */
102 static void choose_populate(GtkTreeRowReference *parent_ref,
103                             int nvec, char **vec,
104                             int type) {
105   /* Compute parent_* */
106   GtkTreeIter pit[1], *parent_it;
107   GtkTreePath *parent_path;
108   if(parent_ref) {
109     parent_path = gtk_tree_row_reference_get_path(parent_ref);
110     parent_it = pit;
111     gboolean pitv = gtk_tree_model_get_iter(GTK_TREE_MODEL(choose_store),
112                                             pit, parent_path);
113     assert(pitv);
114     /*fprintf(stderr, "choose_populate %s: parent path is [%s]\n",
115             choose_type_map[type],
116             gtk_tree_path_to_string(parent_path));*/
117   } else {
118     parent_path = 0;
119     parent_it = 0;
120     /*fprintf(stderr, "choose_populate %s: populating the root\n",
121             choose_type_map[type]);*/
122   }
123   /* Remove unwanted nodes and find out which we must add */
124   //fprintf(stderr, " trimming unwanted %s nodes\n", choose_type_map[type]);
125   char *found = xmalloc(nvec);
126   GtkTreeIter it[1];
127   gboolean itv = gtk_tree_model_iter_children(GTK_TREE_MODEL(choose_store),
128                                               it,
129                                               parent_it);
130   while(itv) {
131     struct choosedata *cd = choose_iter_to_data(it);
132     int keep;
133
134     if(!cd)  {
135       /* Always kill placeholders */
136       //fprintf(stderr, "  kill a placeholder\n");
137       keep = 0;
138     } else if(cd->type == type) {
139       /* This is the type we care about */
140       //fprintf(stderr, "  %s is a %s\n", cd->track, choose_type_map[cd->type]);
141       int n;
142       for(n = 0; n < nvec && strcmp(vec[n], cd->track); ++n)
143         ;
144       if(n < nvec) {
145         //fprintf(stderr, "   ... and survives\n");
146         found[n] = 1;
147         keep = 1;
148       } else {
149         //fprintf(stderr, "   ... and is to be removed\n");
150         keep = 0;
151       }
152     } else {
153       /* Keep wrong-type entries */
154       //fprintf(stderr, "  %s is a %s\n", cd->track, choose_type_map[cd->type]);
155       keep = 1;
156     }
157     if(keep)
158       itv = gtk_tree_model_iter_next(GTK_TREE_MODEL(choose_store), it);
159     else
160       itv = choose_remove_node(it);
161   }
162   /* Add nodes we don't have */
163   int inserted = 0;
164   //fprintf(stderr, " inserting new %s nodes\n", choose_type_map[type]);
165   for(int n = 0; n < nvec; ++n) {
166     if(!found[n]) {
167       //fprintf(stderr, "  %s was not found\n", vec[n]);
168       struct choosedata *cd = g_malloc0(sizeof *cd);
169       cd->type = type;
170       cd->track = g_strdup(vec[n]);
171       cd->sort = g_strdup(trackname_transform(choose_type_map[type],
172                                               vec[n],
173                                               "sort"));
174       gtk_tree_store_append(choose_store, it, parent_it);
175       gtk_tree_store_set(choose_store, it,
176                          NAME_COLUMN, trackname_transform(choose_type_map[type],
177                                                           vec[n],
178                                                           "display"),
179                          CHOOSEDATA_COLUMN, cd,
180                          -1);
181       ++inserted;
182       /* If we inserted a directory, insert a placeholder too, so it appears to
183        * have children; it will be deleted when we expand the directory. */
184       if(type == CHOOSE_DIRECTORY) {
185         //fprintf(stderr, "  inserting a placeholder\n");
186         GtkTreeIter placeholder[1];
187
188         gtk_tree_store_append(choose_store, placeholder, it);
189         gtk_tree_store_set(choose_store, placeholder,
190                            NAME_COLUMN, "Waddling...",
191                            CHOOSEDATA_COLUMN, (void *)0,
192                            -1);
193       }
194     }
195   }
196   //fprintf(stderr, " %d nodes inserted\n", inserted);
197   if(inserted) {
198     /* TODO sort the children */
199   }
200   if(parent_ref) {
201     /* If we deleted a placeholder then we must re-expand the row */
202     gtk_tree_view_expand_row(GTK_TREE_VIEW(choose_view), parent_path, FALSE);
203     gtk_tree_row_reference_free(parent_ref);
204     gtk_tree_path_free(parent_path);
205   }
206 }
207
208 static void choose_dirs_completed(void *v,
209                                   const char *error,
210                                   int nvec, char **vec) {
211   if(error) {
212     popup_protocol_error(0, error);
213     return;
214   }
215   choose_populate(v, nvec, vec, CHOOSE_DIRECTORY);
216 }
217
218 static void choose_files_completed(void *v,
219                                    const char *error,
220                                    int nvec, char **vec) {
221   if(error) {
222     popup_protocol_error(0, error);
223     return;
224   }
225   choose_populate(v, nvec, vec, CHOOSE_FILE);
226 }
227
228 static void choose_row_expanded(GtkTreeView attribute((unused)) *treeview,
229                                 GtkTreeIter *iter,
230                                 GtkTreePath *path,
231                                 gpointer attribute((unused)) user_data) {
232   /*fprintf(stderr, "row-expanded path=[%s]\n",
233           gtk_tree_path_to_string(path));*/
234   /* We update a node's contents whenever it is expanded, even if it was
235    * already populated; the effect is that contracting and expanding a node
236    * suffices to update it to the latest state on the server. */
237   struct choosedata *cd = choose_iter_to_data(iter);
238   disorder_eclient_files(client, choose_files_completed,
239                          xstrdup(cd->track),
240                          NULL,
241                          gtk_tree_row_reference_new(GTK_TREE_MODEL(choose_store),
242                                                     path));
243   disorder_eclient_dirs(client, choose_dirs_completed,
244                         xstrdup(cd->track),
245                         NULL,
246                         gtk_tree_row_reference_new(GTK_TREE_MODEL(choose_store),
247                                                    path));
248   /* The row references are destroyed in the _completed handlers. */
249 }
250
251 /** @brief Create the choose tab */
252 GtkWidget *choose_widget(void) {
253   /* Create the tree store. */
254   choose_store = gtk_tree_store_new(1 + CHOOSEDATA_COLUMN,
255                                     G_TYPE_STRING,
256                                     G_TYPE_POINTER);
257
258   /* Create the view */
259   choose_view = gtk_tree_view_new_with_model(GTK_TREE_MODEL(choose_store));
260
261   /* Create cell renderers and columns */
262   GtkCellRenderer *r = gtk_cell_renderer_text_new();
263   GtkTreeViewColumn *c = gtk_tree_view_column_new_with_attributes
264     ("Track",
265      r,
266      "text", 0,
267      (char *)0);
268   gtk_tree_view_append_column(GTK_TREE_VIEW(choose_view), c);
269   
270   /* The selection should support multiple things being selected */
271   choose_selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(choose_view));
272   gtk_tree_selection_set_mode(choose_selection, GTK_SELECTION_MULTIPLE);
273
274   /* Catch button presses */
275   g_signal_connect(choose_view, "button-press-event",
276                    G_CALLBACK(choose_button_event), 0);
277   g_signal_connect(choose_view, "button-release-event",
278                    G_CALLBACK(choose_button_event), 0);
279   /* Catch row expansions so we can fill in placeholders */
280   g_signal_connect(choose_view, "row-expanded",
281                    G_CALLBACK(choose_row_expanded), 0);
282   
283   /* Fill the root */
284   disorder_eclient_files(client, choose_files_completed, "", NULL, NULL); 
285   disorder_eclient_dirs(client, choose_dirs_completed, "", NULL, NULL); 
286   
287   /* Make the widget scrollable */
288   GtkWidget *scrolled = scroll_widget(choose_view);
289   g_object_set_data(G_OBJECT(scrolled), "type", (void *)&choose_tabtype);
290   return scrolled;
291 }
292
293 /*
294 Local Variables:
295 c-basic-offset:2
296 comment-column:40
297 fill-column:79
298 indent-tabs-mode:nil
299 End:
300 */