chiark / gitweb /
Account choose_list_in_flight correctly.
[disorder] / disobedience / choose-search.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/search.c
21  * @brief Search support
22  *
23  * TODO:
24  * - cleverer focus to implement typeahead find
25  * - don't steal ^A
26  */
27 #include "disobedience.h"
28 #include "choose.h"
29
30 static GtkWidget *choose_search_entry;
31 static GtkWidget *choose_next;
32 static GtkWidget *choose_prev;
33 static GtkWidget *choose_clear;
34
35 /** @brief True if a search command is in flight */
36 static int choose_searching;
37
38 /** @brief True if in-flight search is now known to be obsolete */
39 static int choose_search_obsolete;
40
41 /** @brief Current search terms */
42 static char *choose_search_terms;
43
44 /** @brief Hash of all search result */
45 static hash *choose_search_hash;
46
47 /** @brief List of invisible search results
48  *
49  * This only lists search results not yet known to be visible, and is
50  * gradually depleted.
51  */
52 static char **choose_search_results;
53
54 /** @brief Length of @ref choose_search_results */
55 static int choose_n_search_results;
56
57 /** @brief Row references for search results */
58 static GtkTreeRowReference **choose_search_references;
59
60 /** @brief Length of @ref choose_search_references */
61 static int choose_n_search_references;
62
63 /** @brief Event handle for monitoring newly inserted tracks */
64 static event_handle choose_inserted_handle;
65
66 /** @brief Time of last search entry keypress (or 0.0) */
67 static struct timeval choose_search_last_keypress;
68
69 /** @brief Timeout ID for search delay */
70 static guint choose_search_timeout_id;
71
72 static void choose_search_entry_changed(GtkEditable *editable,
73                                         gpointer user_data);
74
75 int choose_is_search_result(const char *track) {
76   return choose_search_hash && hash_find(choose_search_hash, track);
77 }
78
79 static int is_prefix(const char *dir, const char *track) {
80   size_t nd = strlen(dir);
81
82   if(nd < strlen(track)
83      && track[nd] == '/'
84      && !strncmp(track, dir, nd))
85     return 1;
86   else
87     return 0;
88 }
89
90 /** @brief Do some work towards making @p track visible
91  * @return True if we made it visible or it was missing
92  */
93 static int choose_make_one_visible(const char *track) {
94   //fprintf(stderr, " choose_make_one_visible %s\n", track);
95   /* We walk through nodes at the top level looking for directories that are
96    * prefixes of the target track.
97    *
98    * - if we find one and it's expanded we walk through its children
99    * - if we find one and it's NOT expanded then we expand it, and arrange
100    *   to be revisited
101    * - if we don't find one then we're probably out of date
102    */
103   GtkTreeIter it[1];
104   gboolean itv = gtk_tree_model_get_iter_first(GTK_TREE_MODEL(choose_store),
105                                                it);
106   while(itv) {
107     const char *dir = choose_get_track(it);
108
109     //fprintf(stderr, "  %s\n", dir);
110     if(!dir) {
111       /* Placeholder */
112       itv = gtk_tree_model_iter_next(GTK_TREE_MODEL(choose_store), it);
113       continue;
114     }
115     GtkTreePath *path = gtk_tree_model_get_path(GTK_TREE_MODEL(choose_store),
116                                                 it);
117     if(!strcmp(dir, track)) {
118       /* We found the track.  If everything above it was expanded, it will be
119        * too.  So we can report it as visible. */
120       //fprintf(stderr, "   found %s\n", track);
121       choose_search_references[choose_n_search_references++]
122         = gtk_tree_row_reference_new(GTK_TREE_MODEL(choose_store), path);
123       gtk_tree_path_free(path);
124       return 1;
125     }
126     if(is_prefix(dir, track)) {
127       /* We found a prefix of the target track. */
128       //fprintf(stderr, "   is a prefix\n");
129       const gboolean expanded
130         = gtk_tree_view_row_expanded(GTK_TREE_VIEW(choose_view), path);
131       if(expanded) {
132         //fprintf(stderr, "   is apparently expanded\n");
133         /* This directory is expanded, let's make like Augustus Gibbons and
134          * take it to the next level. */
135         GtkTreeIter child[1];           /* don't know if parent==iter allowed */
136         itv = gtk_tree_model_iter_children(GTK_TREE_MODEL(choose_store),
137                                            child,
138                                            it);
139         *it = *child;
140         if(choose_is_placeholder(it)) {
141           //fprintf(stderr, "   %s is expanded, has a placeholder child\n", dir);
142           /* We assume that placeholder children of expanded rows are about to
143            * be replaced */
144           gtk_tree_path_free(path);
145           return 0;
146         }
147       } else {
148         //fprintf(stderr, "   requesting expansion of %s\n", dir);
149         /* Track is below a non-expanded directory.  So let's expand it.
150          * choose_make_visible() will arrange a revisit in due course. */
151         gtk_tree_view_expand_row(GTK_TREE_VIEW(choose_view),
152                                  path,
153                                  FALSE/*open_all*/);
154         gtk_tree_path_free(path);
155         /* TODO: the old version would remember which rows had been expanded
156          * just to show search results and collapse them again.  We should
157          * probably do that. */
158         return 0;
159       }
160     } else
161       itv = gtk_tree_model_iter_next(GTK_TREE_MODEL(choose_store), it);
162     gtk_tree_path_free(path);
163   }
164   /* If we reach the end then we didn't find the track at all. */
165   fprintf(stderr, "choose_make_one_visible: could not find %s\n",
166           track);
167   return 1;
168 }
169
170 /** @brief Compare two GtkTreeRowReferences
171  *
172  * Not very efficient since it does multiple memory operations per
173  * comparison!
174  */
175 static int choose_compare_references(const void *av, const void *bv) {
176   GtkTreeRowReference *a = *(GtkTreeRowReference **)av;
177   GtkTreeRowReference *b = *(GtkTreeRowReference **)bv;
178   GtkTreePath *pa = gtk_tree_row_reference_get_path(a);
179   GtkTreePath *pb = gtk_tree_row_reference_get_path(b);
180   const int rc = gtk_tree_path_compare(pa, pb);
181   gtk_tree_path_free(pa);
182   gtk_tree_path_free(pb);
183   return rc;
184 }
185
186 /** @brief Make @p path visible
187  * @param path Row reference to make visible
188  * @param row_align Row alignment (or -ve)
189  * @return 0 on success, nonzero if @p ref has gone stale
190  *
191  * If @p row_align is negative no row alignemt is performed.  Otherwise
192  * it must be between 0 (the top) and 1 (the bottom).
193  *
194  * TODO: if the row is already visible do nothing.
195  */
196 static int choose_make_path_visible(GtkTreePath *path,
197                                     gfloat row_align) {
198   /* Make sure that the target's parents are all expanded */
199   gtk_tree_view_expand_to_path(GTK_TREE_VIEW(choose_view), path);
200   /* Make sure the target is visible */
201   gtk_tree_view_scroll_to_cell(GTK_TREE_VIEW(choose_view), path, NULL,
202                                row_align >= 0.0,
203                                row_align,
204                                0);
205   return 0;
206 }
207
208 /** @brief Make @p ref visible
209  * @param ref Row reference to make visible
210  * @param row_align Row alignment (or -ve)
211  * @return 0 on success, nonzero if @p ref has gone stale
212  *
213  * If @p row_align is negative no row alignemt is performed.  Otherwise
214  * it must be between 0 (the top) and 1 (the bottom).
215  */
216 static int choose_make_ref_visible(GtkTreeRowReference *ref,
217                                    gfloat row_align) {
218   GtkTreePath *path = gtk_tree_row_reference_get_path(ref);
219   if(!path)
220     return -1;
221   choose_make_path_visible(path, row_align);
222   gtk_tree_path_free(path);
223   return 0;
224 }
225
226 /** @brief Do some work towards ensuring that all search results are visible
227  *
228  * Assumes there's at least one results!
229  */
230 static void choose_make_visible(const char attribute((unused)) *event,
231                                 void attribute((unused)) *eventdata,
232                                 void attribute((unused)) *callbackdata) {
233   //fprintf(stderr, "choose_make_visible\n");
234   int remaining = 0;
235
236   for(int n = 0; n < choose_n_search_results; ++n) {
237     if(!choose_search_results[n])
238       continue;
239     if(choose_make_one_visible(choose_search_results[n]))
240       choose_search_results[n] = 0;
241     else
242       ++remaining;
243   }
244   //fprintf(stderr, "remaining=%d\n", remaining);
245   if(remaining) {
246     /* If there's work left to be done make sure we get a callback when
247      * something changes */
248     if(!choose_inserted_handle)
249       choose_inserted_handle = event_register("choose-inserted-tracks",
250                                               choose_make_visible, 0);
251   } else {
252     /* Suppress callbacks if there's nothing more to do */
253     event_cancel(choose_inserted_handle);
254     choose_inserted_handle = 0;
255     /* We've expanded everything, now we can mess with the cursor */
256     //fprintf(stderr, "sort %d references\n", choose_n_search_references);
257     qsort(choose_search_references,
258           choose_n_search_references,
259           sizeof (GtkTreeRowReference *),
260           choose_compare_references);
261     choose_make_ref_visible(choose_search_references[0], 0.5);
262   }
263 }
264
265 /** @brief Called with search results */
266 static void choose_search_completed(void attribute((unused)) *v,
267                                     const char *error,
268                                     int nvec, char **vec) {
269   //fprintf(stderr, "choose_search_completed\n");
270   if(error) {
271     popup_protocol_error(0, error);
272     return;
273   }
274   choose_searching = 0;
275   /* If the search was obsoleted initiate another one */
276   if(choose_search_obsolete) {
277     choose_search_obsolete = 0;
278     choose_search_entry_changed(0, 0);
279     return;
280   }
281   //fprintf(stderr, "*** %d search results\n", nvec);
282   choose_search_hash = hash_new(1);
283   if(nvec) {
284     for(int n = 0; n < nvec; ++n)
285       hash_add(choose_search_hash, vec[n], "", HASH_INSERT);
286     /* Stash results for choose_make_visible */
287     choose_n_search_results = nvec;
288     choose_search_results = vec;
289     /* Make a big-enough buffer for the results row reference list */
290     choose_n_search_references = 0;
291     choose_search_references = xcalloc(nvec, sizeof (GtkTreeRowReference *));
292     /* Start making rows visible */
293     choose_make_visible(0, 0, 0);
294     gtk_widget_set_sensitive(choose_next, TRUE);
295     gtk_widget_set_sensitive(choose_prev, TRUE);
296   } else {
297     gtk_widget_set_sensitive(choose_next, FALSE);
298     gtk_widget_set_sensitive(choose_prev, FALSE);
299   }
300   event_raise("search-results-changed", 0);
301 }
302
303 /** @brief Actually initiate a search */
304 static void initiate_search(void) {
305   //fprintf(stderr, "initiate_search\n");
306   /* If a search is in flight don't initiate a new one until it comes back */
307   if(choose_searching) {
308     choose_search_obsolete = 1;
309     return;
310   }
311   char *terms = xstrdup(gtk_entry_get_text(GTK_ENTRY(choose_search_entry)));
312   /* Strip leading and trailing space */
313   while(*terms == ' ')
314     ++terms;
315   char *e = terms + strlen(terms);
316   while(e > terms && e[-1] == ' ')
317     --e;
318   *e = 0;
319   if(choose_search_terms && !strcmp(terms, choose_search_terms)) {
320     /* Search terms have not actually changed in any way that matters */
321     return;
322   }
323   /* Remember the current terms */
324   choose_search_terms = terms;
325   if(!*terms) {
326     /* Nothing to search for.  Fake a completion call. */
327     choose_search_completed(0, 0, 0, 0);
328     return;
329   }
330   if(disorder_eclient_search(client, choose_search_completed, terms, 0)) {
331     /* Bad search terms.  Fake a completion call. */
332     choose_search_completed(0, 0, 0, 0);
333     return;
334   }
335   choose_searching = 1;
336 }
337
338 static gboolean choose_search_timeout(gpointer attribute((unused)) data) {
339   struct timeval now;
340   xgettimeofday(&now, NULL);
341   /*fprintf(stderr, "%ld.%06d choose_search_timeout\n",
342           now.tv_sec, now.tv_usec);*/
343   if(tvdouble(now) - tvdouble(choose_search_last_keypress)
344          < SEARCH_DELAY_MS / 1000.0) {
345     //fprintf(stderr, " ... too soon\n");
346     return TRUE;                        /* Call me again later */
347   }
348   //fprintf(stderr, " ... let's go\n");
349   choose_search_last_keypress.tv_sec = 0;
350   choose_search_last_keypress.tv_usec = 0;
351   choose_search_timeout_id = 0;
352   initiate_search();
353   return FALSE;
354 }
355
356 /** @brief Called when the search entry changes */
357 static void choose_search_entry_changed
358     (GtkEditable attribute((unused)) *editable,
359      gpointer attribute((unused)) user_data) {
360   xgettimeofday(&choose_search_last_keypress, NULL);
361   /*fprintf(stderr, "%ld.%06d choose_search_entry_changed\n",
362           choose_search_last_keypress.tv_sec,
363           choose_search_last_keypress.tv_usec);*/
364   /* If there's already a timeout, remove it */
365   if(choose_search_timeout_id) {
366     g_source_remove(choose_search_timeout_id);
367     choose_search_timeout_id = 0;
368   }
369   /* Add a new timeout */
370   choose_search_timeout_id = g_timeout_add(SEARCH_DELAY_MS / 10,
371                                            choose_search_timeout,
372                                            0);
373   /* We really wanted to tell Glib what time we wanted the callback at rather
374    * than asking for calls at given intervals.  But there's no interface for
375    * that, and defining a new source for it seems like overkill if we can
376    * reasonably avoid it. */
377 }
378
379 /** @brief Identify first and last visible paths
380  *
381  * We'd like to use gtk_tree_view_get_visible_range() for this, but that was
382  * introduced in GTK+ 2.8, and Fink only has 2.6 (which is around three years
383  * out of date at time of writing), and I'm not yet prepared to rule out Fink
384  * support.
385  */
386 static gboolean choose_get_visible_range(GtkTreeView *tree_view,
387                                          GtkTreePath **startpathp,
388                                          GtkTreePath **endpathp) {
389   GdkRectangle visible_tc[1];
390
391   /* Get the visible rectangle in tree coordinates */
392   gtk_tree_view_get_visible_rect(tree_view, visible_tc);
393   /*fprintf(stderr, "visible: %dx%x at %dx%d\n",
394           visible_tc->width, visible_tc->height,
395           visible_tc->x, visible_tc->y);*/
396   if(startpathp) {
397     /* Convert top-left visible point to widget coordinates */
398     int x_wc, y_wc;
399     gtk_tree_view_tree_to_widget_coords(tree_view,
400                                         visible_tc->x, visible_tc->y,
401                                         &x_wc, &y_wc);
402     //fprintf(stderr, " start widget coords: %dx%d\n", x_wc, y_wc);
403     gtk_tree_view_get_path_at_pos(tree_view,
404                                   x_wc, y_wc,
405                                   startpathp,
406                                   NULL,
407                                   NULL, NULL);
408   }
409   if(endpathp) {
410     /* Convert bottom-left visible point to widget coordinates */
411     /* Convert top-left visible point to widget coordinates */
412     int x_wc, y_wc;
413     gtk_tree_view_tree_to_widget_coords(tree_view,
414                                         visible_tc->x,
415                                         visible_tc->y + visible_tc->height - 1,
416                                         &x_wc, &y_wc);
417     //fprintf(stderr, " end widget coords: %dx%d\n", x_wc, y_wc);
418     gtk_tree_view_get_path_at_pos(tree_view,
419                                   x_wc, y_wc,
420                                   endpathp,
421                                   NULL,
422                                   NULL, NULL);
423   }
424   return TRUE;
425 }
426
427 static void choose_next_clicked(GtkButton attribute((unused)) *button,
428                                 gpointer attribute((unused)) userdata) {
429   /* Find the last visible row */
430   GtkTreePath *endpath;
431   gboolean endvalid = choose_get_visible_range(GTK_TREE_VIEW(choose_view),
432                                                NULL,
433                                                &endpath);
434   if(!endvalid)
435     return;
436   /* Find a the first search result later than it.  They're sorted so we could
437    * actually do much better than this if necessary. */
438   for(int n = 0; n < choose_n_search_references; ++n) {
439     GtkTreePath *path
440       = gtk_tree_row_reference_get_path(choose_search_references[n]);
441     if(!path)
442       continue;
443     if(gtk_tree_path_compare(endpath, path) < 0) {
444       choose_make_path_visible(path, 0.5);
445       gtk_tree_path_free(path);
446       return;
447     }
448     gtk_tree_path_free(path);
449   }
450 }
451
452 static void choose_prev_clicked(GtkButton attribute((unused)) *button,
453                                 gpointer attribute((unused)) userdata) {
454   /* TODO can we de-dupe with choose_next_clicked?  Probably yes. */
455   /* Find the first visible row */
456   GtkTreePath *startpath;
457   gboolean startvalid = choose_get_visible_range(GTK_TREE_VIEW(choose_view),
458                                                  &startpath,
459                                                  NULL);
460   if(!startvalid)
461     return;
462   /* Find a the last search result earlier than it.  They're sorted so we could
463    * actually do much better than this if necessary. */
464   for(int n = choose_n_search_references - 1; n >= 0; --n) {
465     GtkTreePath *path
466       = gtk_tree_row_reference_get_path(choose_search_references[n]);
467     if(!path)
468       continue;
469     if(gtk_tree_path_compare(startpath, path) > 0) {
470       choose_make_path_visible(path, 0.5);
471       gtk_tree_path_free(path);
472       return;
473     }
474     gtk_tree_path_free(path);
475   }
476 }
477
478 /** @brief Called when the cancel search button is clicked */
479 static void choose_clear_clicked(GtkButton attribute((unused)) *button,
480                                  gpointer attribute((unused)) userdata) {
481   gtk_entry_set_text(GTK_ENTRY(choose_search_entry), "");
482   /* We start things off straight away in this case */
483   initiate_search();
484 }
485
486 /** @brief Create the search widget */
487 GtkWidget *choose_search_widget(void) {
488
489   /* Text entry box for search terms */
490   choose_search_entry = gtk_entry_new();
491   gtk_widget_set_style(choose_search_entry, tool_style);
492   g_signal_connect(choose_search_entry, "changed",
493                    G_CALLBACK(choose_search_entry_changed), 0);
494   gtk_tooltips_set_tip(tips, choose_search_entry,
495                        "Enter search terms here; search is automatic", "");
496
497   /* Cancel button to clear the search */
498   choose_clear = gtk_button_new_from_stock(GTK_STOCK_CANCEL);
499   gtk_widget_set_style(choose_clear, tool_style);
500   g_signal_connect(G_OBJECT(choose_clear), "clicked",
501                    G_CALLBACK(choose_clear_clicked), 0);
502   gtk_tooltips_set_tip(tips, choose_clear, "Clear search terms", "");
503
504   /* Up and down buttons to find previous/next results; initially they are not
505    * usable as there are no search results. */
506   choose_prev = iconbutton("up.png", "Previous search result");
507   g_signal_connect(G_OBJECT(choose_prev), "clicked",
508                    G_CALLBACK(choose_prev_clicked), 0);
509   gtk_widget_set_style(choose_prev, tool_style);
510   gtk_widget_set_sensitive(choose_prev, 0);
511   choose_next = iconbutton("down.png", "Next search result");
512   g_signal_connect(G_OBJECT(choose_next), "clicked",
513                    G_CALLBACK(choose_next_clicked), 0);
514   gtk_widget_set_style(choose_next, tool_style);
515   gtk_widget_set_sensitive(choose_next, 0);
516   
517   /* Pack the search tools button together on a line */
518   GtkWidget *hbox = gtk_hbox_new(FALSE/*homogeneous*/, 1/*spacing*/);
519   gtk_box_pack_start(GTK_BOX(hbox), choose_search_entry,
520                      TRUE/*expand*/, TRUE/*fill*/, 0/*padding*/);
521   gtk_box_pack_start(GTK_BOX(hbox), choose_prev,
522                      FALSE/*expand*/, FALSE/*fill*/, 0/*padding*/);
523   gtk_box_pack_start(GTK_BOX(hbox), choose_next,
524                      FALSE/*expand*/, FALSE/*fill*/, 0/*padding*/);
525   gtk_box_pack_start(GTK_BOX(hbox), choose_clear,
526                      FALSE/*expand*/, FALSE/*fill*/, 0/*padding*/);
527
528   return hbox;
529 }
530
531 /*
532 Local Variables:
533 c-basic-offset:2
534 comment-column:40
535 fill-column:79
536 indent-tabs-mode:nil
537 End:
538 */