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