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