chiark / gitweb /
Synchronize with DisOrder 4.1
[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 #include "disobedience.h"
24 #include "choose.h"
25
26 int choose_auto_expanding;
27
28 GtkWidget *choose_search_entry;
29 static GtkWidget *choose_next;
30 static GtkWidget *choose_prev;
31 static GtkWidget *choose_clear;
32
33 /** @brief True if a search command is in flight */
34 static int choose_searching;
35
36 /** @brief True if in-flight search is now known to be obsolete */
37 static int choose_search_obsolete;
38
39 /** @brief Current search terms */
40 static char *choose_search_terms;
41
42 /** @brief Hash of all search result */
43 static hash *choose_search_hash;
44
45 /** @brief List of invisible search results
46  *
47  * This only lists search results not yet known to be visible, and is
48  * gradually depleted.
49  */
50 static char **choose_search_results;
51
52 /** @brief Length of @ref choose_search_results */
53 static int choose_n_search_results;
54
55 /** @brief Row references for search results */
56 static GtkTreeRowReference **choose_search_references;
57
58 /** @brief Length of @ref choose_search_references */
59 static int choose_n_search_references;
60
61 /** @brief Event handle for monitoring newly inserted tracks */
62 static event_handle choose_inserted_handle;
63
64 /** @brief Time of last search entry keypress (or 0.0) */
65 static struct timeval choose_search_last_keypress;
66
67 /** @brief Timeout ID for search delay */
68 static guint choose_search_timeout_id;
69
70 static void choose_search_entry_changed(GtkEditable *editable,
71                                         gpointer user_data);
72
73 int choose_is_search_result(const char *track) {
74   return choose_search_hash && hash_find(choose_search_hash, track);
75 }
76
77 static int is_prefix(const char *dir, const char *track) {
78   size_t nd = strlen(dir);
79
80   if(nd < strlen(track)
81      && track[nd] == '/'
82      && !strncmp(track, dir, nd))
83     return 1;
84   else
85     return 0;
86 }
87
88 /** @brief Do some work towards making @p track visible
89  * @return True if we made it visible or it was missing
90  */
91 static int choose_make_one_visible(const char *track) {
92   //fprintf(stderr, " choose_make_one_visible %s\n", track);
93   /* We walk through nodes at the top level looking for directories that are
94    * prefixes of the target track.
95    *
96    * - if we find one and it's expanded we walk through its children
97    * - if we find one and it's NOT expanded then we expand it, and arrange
98    *   to be revisited
99    * - if we don't find one then we're probably out of date
100    */
101   GtkTreeIter it[1];
102   gboolean itv = gtk_tree_model_get_iter_first(GTK_TREE_MODEL(choose_store),
103                                                it);
104   while(itv) {
105     const char *dir = choose_get_track(it);
106
107     //fprintf(stderr, "  %s\n", dir);
108     if(!dir) {
109       /* Placeholder */
110       itv = gtk_tree_model_iter_next(GTK_TREE_MODEL(choose_store), it);
111       continue;
112     }
113     GtkTreePath *path = gtk_tree_model_get_path(GTK_TREE_MODEL(choose_store),
114                                                 it);
115     if(!strcmp(dir, track)) {
116       /* We found the track.  If everything above it was expanded, it will be
117        * too.  So we can report it as visible. */
118       //fprintf(stderr, "   found %s\n", track);
119       choose_search_references[choose_n_search_references++]
120         = gtk_tree_row_reference_new(GTK_TREE_MODEL(choose_store), path);
121       gtk_tree_path_free(path);
122       return 1;
123     }
124     if(is_prefix(dir, track)) {
125       /* We found a prefix of the target track. */
126       //fprintf(stderr, "   %s is a prefix\n", dir);
127       const gboolean expanded
128         = gtk_tree_view_row_expanded(GTK_TREE_VIEW(choose_view), path);
129       if(expanded) {
130         //fprintf(stderr, "   is apparently expanded\n");
131         /* This directory is expanded, let's make like Augustus Gibbons and
132          * take it to the next level. */
133         GtkTreeIter child[1];           /* don't know if parent==iter allowed */
134         itv = gtk_tree_model_iter_children(GTK_TREE_MODEL(choose_store),
135                                            child,
136                                            it);
137         *it = *child;
138         if(choose_is_placeholder(it)) {
139           //fprintf(stderr, "   %s is expanded, has a placeholder child\n", dir);
140           /* We assume that placeholder children of expanded rows are about to
141            * be replaced */
142           gtk_tree_path_free(path);
143           return 0;
144         }
145       } else {
146         //fprintf(stderr, "   requesting expansion of %s\n", dir);
147         /* Track is below a non-expanded directory.  So let's expand it.
148          * choose_make_visible() will arrange a revisit in due course.
149          *
150          * We mark the row as auto-expanded.
151          */
152         ++choose_auto_expanding;
153         gtk_tree_view_expand_row(GTK_TREE_VIEW(choose_view),
154                                  path,
155                                  FALSE/*open_all*/);
156         gtk_tree_path_free(path);
157         --choose_auto_expanding;
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-more-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 *err,
268                                     int nvec, char **vec) {
269   //fprintf(stderr, "choose_search_completed\n");
270   if(err) {
271     popup_protocol_error(0, err);
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   /* We're actually going to use these search results.  Autocollapse anything
283    * left over from the old search. */
284   choose_auto_collapse();
285   choose_search_hash = hash_new(1);
286   if(nvec) {
287     for(int n = 0; n < nvec; ++n)
288       hash_add(choose_search_hash, vec[n], "", HASH_INSERT);
289     /* Stash results for choose_make_visible */
290     choose_n_search_results = nvec;
291     choose_search_results = vec;
292     /* Make a big-enough buffer for the results row reference list */
293     choose_n_search_references = 0;
294     choose_search_references = xcalloc(nvec, sizeof (GtkTreeRowReference *));
295     /* Start making rows visible */
296     choose_make_visible(0, 0, 0);
297     gtk_widget_set_sensitive(choose_next, TRUE);
298     gtk_widget_set_sensitive(choose_prev, TRUE);
299   } else {
300     gtk_widget_set_sensitive(choose_next, FALSE);
301     gtk_widget_set_sensitive(choose_prev, FALSE);
302     choose_n_search_results = 0;
303     choose_search_results = 0;
304     choose_n_search_references = 0;
305     choose_search_references = 0;
306   }
307   event_raise("search-results-changed", 0);
308 }
309
310 /** @brief Actually initiate a search */
311 static void initiate_search(void) {
312   //fprintf(stderr, "initiate_search\n");
313   /* If a search is in flight don't initiate a new one until it comes back */
314   if(choose_searching) {
315     choose_search_obsolete = 1;
316     return;
317   }
318   char *terms = xstrdup(gtk_entry_get_text(GTK_ENTRY(choose_search_entry)));
319   /* Strip leading and trailing space */
320   while(*terms == ' ')
321     ++terms;
322   char *e = terms + strlen(terms);
323   while(e > terms && e[-1] == ' ')
324     --e;
325   *e = 0;
326   if(choose_search_terms && !strcmp(terms, choose_search_terms)) {
327     /* Search terms have not actually changed in any way that matters */
328     return;
329   }
330   /* Remember the current terms */
331   choose_search_terms = terms;
332   if(!*terms) {
333     /* Nothing to search for.  Fake a completion call. */
334     choose_search_completed(0, 0, 0, 0);
335     return;
336   }
337   if(disorder_eclient_search(client, choose_search_completed, terms, 0)) {
338     /* Bad search terms.  Fake a completion call. */
339     choose_search_completed(0, 0, 0, 0);
340     return;
341   }
342   choose_searching = 1;
343 }
344
345 static gboolean choose_search_timeout(gpointer attribute((unused)) data) {
346   struct timeval now;
347   xgettimeofday(&now, NULL);
348   /*fprintf(stderr, "%ld.%06d choose_search_timeout\n",
349           now.tv_sec, now.tv_usec);*/
350   if(tvdouble(now) - tvdouble(choose_search_last_keypress)
351          < SEARCH_DELAY_MS / 1000.0) {
352     //fprintf(stderr, " ... too soon\n");
353     return TRUE;                        /* Call me again later */
354   }
355   //fprintf(stderr, " ... let's go\n");
356   choose_search_last_keypress.tv_sec = 0;
357   choose_search_last_keypress.tv_usec = 0;
358   choose_search_timeout_id = 0;
359   initiate_search();
360   return FALSE;
361 }
362
363 /** @brief Called when the search entry changes */
364 static void choose_search_entry_changed
365     (GtkEditable attribute((unused)) *editable,
366      gpointer attribute((unused)) user_data) {
367   xgettimeofday(&choose_search_last_keypress, NULL);
368   /*fprintf(stderr, "%ld.%06d choose_search_entry_changed\n",
369           choose_search_last_keypress.tv_sec,
370           choose_search_last_keypress.tv_usec);*/
371   /* If there's already a timeout, remove it */
372   if(choose_search_timeout_id) {
373     g_source_remove(choose_search_timeout_id);
374     choose_search_timeout_id = 0;
375   }
376   /* Add a new timeout */
377   choose_search_timeout_id = g_timeout_add(SEARCH_DELAY_MS / 10,
378                                            choose_search_timeout,
379                                            0);
380   /* We really wanted to tell Glib what time we wanted the callback at rather
381    * than asking for calls at given intervals.  But there's no interface for
382    * that, and defining a new source for it seems like overkill if we can
383    * reasonably avoid it. */
384 }
385
386 /** @brief Identify first and last visible paths
387  *
388  * We'd like to use gtk_tree_view_get_visible_range() for this, but that was
389  * introduced in GTK+ 2.8, and Fink only has 2.6 (which is around three years
390  * out of date at time of writing), and I'm not yet prepared to rule out Fink
391  * support.
392  */
393 static gboolean choose_get_visible_range(GtkTreeView *tree_view,
394                                          GtkTreePath **startpathp,
395                                          GtkTreePath **endpathp) {
396   GdkRectangle visible_tc[1];
397
398   /* Get the visible rectangle in tree coordinates */
399   gtk_tree_view_get_visible_rect(tree_view, visible_tc);
400   /*fprintf(stderr, "visible: %dx%x at %dx%d\n",
401           visible_tc->width, visible_tc->height,
402           visible_tc->x, visible_tc->y);*/
403   if(startpathp) {
404     /* Convert top-left visible point to widget coordinates */
405     int x_wc, y_wc;
406     gtk_tree_view_tree_to_widget_coords(tree_view,
407                                         visible_tc->x, visible_tc->y,
408                                         &x_wc, &y_wc);
409     //fprintf(stderr, " start widget coords: %dx%d\n", x_wc, y_wc);
410     gtk_tree_view_get_path_at_pos(tree_view,
411                                   x_wc, y_wc,
412                                   startpathp,
413                                   NULL,
414                                   NULL, NULL);
415   }
416   if(endpathp) {
417     /* Convert bottom-left visible point to widget coordinates */
418     /* Convert top-left visible point to widget coordinates */
419     int x_wc, y_wc;
420     gtk_tree_view_tree_to_widget_coords(tree_view,
421                                         visible_tc->x,
422                                         visible_tc->y + visible_tc->height - 1,
423                                         &x_wc, &y_wc);
424     //fprintf(stderr, " end widget coords: %dx%d\n", x_wc, y_wc);
425     gtk_tree_view_get_path_at_pos(tree_view,
426                                   x_wc, y_wc,
427                                   endpathp,
428                                   NULL,
429                                   NULL, NULL);
430   }
431   return TRUE;
432 }
433
434 /** @brief Move to the next/prev match
435  * @param direction -1 for prev, +1 for next
436  */
437 static void choose_move(int direction) {
438   /* Refocus the main view so typahead find continues to work */
439   gtk_widget_grab_focus(choose_view);
440   /* If there's no results we have nothing to do */
441   if(!choose_n_search_results)
442     return;
443   /* Compute bounds for searching over the array in the right direction */
444   const int first = direction > 0 ? 0 : choose_n_search_references - 1;
445   const int limit = direction > 0 ? choose_n_search_references : -1;
446   /* Find the first/last currently visible row */
447   GtkTreePath *limitpath;
448   if(!choose_get_visible_range(GTK_TREE_VIEW(choose_view),
449                                direction < 0 ? &limitpath : 0,
450                                direction > 0 ? &limitpath : 0))
451     return;
452   /* Find a the first search result later/earlier than it.  They're sorted so
453    * we could actually do much better than this if necessary. */
454   for(int n = first; n != limit; n += direction) {
455     GtkTreePath *path
456       = gtk_tree_row_reference_get_path(choose_search_references[n]);
457     if(!path)
458       continue;
459     /* gtk_tree_path_compare returns -1, 0 or 1 so we compare naively with
460      * direction */
461     if(gtk_tree_path_compare(limitpath, path) + direction == 0) {
462       choose_make_path_visible(path, 0.5);
463       gtk_tree_path_free(path);
464       return;
465     }
466     gtk_tree_path_free(path);
467   }
468   /* We didn't find one.  Loop back to the first/las. */
469   for(int n = first; n != limit; n += direction) {
470     GtkTreePath *path
471       = gtk_tree_row_reference_get_path(choose_search_references[n]);
472     if(!path)
473       continue;
474     choose_make_path_visible(path, 0.5);
475     gtk_tree_path_free(path);
476     return;
477   }
478 }
479
480 void choose_next_clicked(GtkButton attribute((unused)) *button,
481                          gpointer attribute((unused)) userdata) {
482   choose_move(1);
483 }
484
485 void choose_prev_clicked(GtkButton attribute((unused)) *button,
486                          gpointer attribute((unused)) userdata) {
487   choose_move(-1);
488 }
489
490 /** @brief Called when the cancel search button is clicked */
491 static void choose_clear_clicked(GtkButton attribute((unused)) *button,
492                                  gpointer attribute((unused)) userdata) {
493   gtk_entry_set_text(GTK_ENTRY(choose_search_entry), "");
494   gtk_widget_grab_focus(choose_view);
495   /* We start things off straight away in this case */
496   initiate_search();
497 }
498
499 /** @brief Called when the user hits ^F to start a new search */
500 void choose_search_new(void) {
501   gtk_editable_select_region(GTK_EDITABLE(choose_search_entry), 0, -1);
502 }
503
504 /** @brief Create the search widget */
505 GtkWidget *choose_search_widget(void) {
506
507   /* Text entry box for search terms */
508   choose_search_entry = gtk_entry_new();
509   gtk_widget_set_style(choose_search_entry, tool_style);
510   g_signal_connect(choose_search_entry, "changed",
511                    G_CALLBACK(choose_search_entry_changed), 0);
512   gtk_tooltips_set_tip(tips, choose_search_entry,
513                        "Enter search terms here; search is automatic", "");
514
515   /* Cancel button to clear the search */
516   choose_clear = gtk_button_new_from_stock(GTK_STOCK_CANCEL);
517   gtk_widget_set_style(choose_clear, tool_style);
518   g_signal_connect(G_OBJECT(choose_clear), "clicked",
519                    G_CALLBACK(choose_clear_clicked), 0);
520   gtk_tooltips_set_tip(tips, choose_clear, "Clear search terms", "");
521
522   /* Up and down buttons to find previous/next results; initially they are not
523    * usable as there are no search results. */
524   choose_prev = iconbutton("up.png", "Previous search result");
525   g_signal_connect(G_OBJECT(choose_prev), "clicked",
526                    G_CALLBACK(choose_prev_clicked), 0);
527   gtk_widget_set_style(choose_prev, tool_style);
528   gtk_widget_set_sensitive(choose_prev, 0);
529   choose_next = iconbutton("down.png", "Next search result");
530   g_signal_connect(G_OBJECT(choose_next), "clicked",
531                    G_CALLBACK(choose_next_clicked), 0);
532   gtk_widget_set_style(choose_next, tool_style);
533   gtk_widget_set_sensitive(choose_next, 0);
534   
535   /* Pack the search tools button together on a line */
536   GtkWidget *hbox = gtk_hbox_new(FALSE/*homogeneous*/, 1/*spacing*/);
537   gtk_box_pack_start(GTK_BOX(hbox), choose_search_entry,
538                      TRUE/*expand*/, TRUE/*fill*/, 0/*padding*/);
539   gtk_box_pack_start(GTK_BOX(hbox), choose_prev,
540                      FALSE/*expand*/, FALSE/*fill*/, 0/*padding*/);
541   gtk_box_pack_start(GTK_BOX(hbox), choose_next,
542                      FALSE/*expand*/, FALSE/*fill*/, 0/*padding*/);
543   gtk_box_pack_start(GTK_BOX(hbox), choose_clear,
544                      FALSE/*expand*/, FALSE/*fill*/, 0/*padding*/);
545
546   return hbox;
547 }
548
549 /*
550 Local Variables:
551 c-basic-offset:2
552 comment-column:40
553 fill-column:79
554 indent-tabs-mode:nil
555 End:
556 */