chiark / gitweb /
Send clients a rights-changed message when their rights change.
[disorder] / disobedience / choose-search.c
CommitLineData
cfa78eaa
RK
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 */
bb9bcc90
RK
20/** @file disobedience/search.c
21 * @brief Search support
bb9bcc90 22 */
cfa78eaa
RK
23#include "disobedience.h"
24#include "choose.h"
25
bd802f22
RK
26int choose_auto_expanding;
27
a59663d4 28GtkWidget *choose_search_entry;
cfa78eaa
RK
29static GtkWidget *choose_next;
30static GtkWidget *choose_prev;
31static GtkWidget *choose_clear;
32
33/** @brief True if a search command is in flight */
34static int choose_searching;
35
36/** @brief True if in-flight search is now known to be obsolete */
37static int choose_search_obsolete;
38
9eeb9f12
RK
39/** @brief Current search terms */
40static char *choose_search_terms;
41
cfa78eaa
RK
42/** @brief Hash of all search result */
43static hash *choose_search_hash;
44
2a9a65e4
RK
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 */
50static char **choose_search_results;
51
52/** @brief Length of @ref choose_search_results */
53static int choose_n_search_results;
54
55/** @brief Row references for search results */
56static GtkTreeRowReference **choose_search_references;
57
58/** @brief Length of @ref choose_search_references */
59static int choose_n_search_references;
60
61/** @brief Event handle for monitoring newly inserted tracks */
62static event_handle choose_inserted_handle;
63
9eeb9f12
RK
64/** @brief Time of last search entry keypress (or 0.0) */
65static struct timeval choose_search_last_keypress;
66
67/** @brief Timeout ID for search delay */
68static guint choose_search_timeout_id;
69
cfa78eaa
RK
70static void choose_search_entry_changed(GtkEditable *editable,
71 gpointer user_data);
72
73int choose_is_search_result(const char *track) {
74 return choose_search_hash && hash_find(choose_search_hash, track);
75}
76
2a9a65e4
RK
77static 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 */
91static 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. */
cab9a17c 126 //fprintf(stderr, " %s is a prefix\n", dir);
2a9a65e4
RK
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.
bd802f22
RK
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;
2a9a65e4
RK
153 gtk_tree_view_expand_row(GTK_TREE_VIEW(choose_view),
154 path,
155 FALSE/*open_all*/);
156 gtk_tree_path_free(path);
bd802f22 157 --choose_auto_expanding;
2a9a65e4
RK
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 */
175static 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
b96f65cf
RK
186/** @brief Make @p path visible
187 * @param path Row reference to make visible
188 * @param row_align Row alignment (or -ve)
2a9a65e4 189 * @return 0 on success, nonzero if @p ref has gone stale
b96f65cf
RK
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).
e9a72659
RK
193 *
194 * TODO: if the row is already visible do nothing.
b96f65cf
RK
195 */
196static 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).
2a9a65e4 215 */
b96f65cf
RK
216static int choose_make_ref_visible(GtkTreeRowReference *ref,
217 gfloat row_align) {
2a9a65e4
RK
218 GtkTreePath *path = gtk_tree_row_reference_get_path(ref);
219 if(!path)
220 return -1;
b96f65cf 221 choose_make_path_visible(path, row_align);
2a9a65e4
RK
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 */
230static 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)
cab9a17c 249 choose_inserted_handle = event_register("choose-more-tracks",
2a9a65e4
RK
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);
b96f65cf 261 choose_make_ref_visible(choose_search_references[0], 0.5);
2a9a65e4
RK
262 }
263}
264
cfa78eaa
RK
265/** @brief Called with search results */
266static void choose_search_completed(void attribute((unused)) *v,
267 const char *error,
268 int nvec, char **vec) {
2a9a65e4 269 //fprintf(stderr, "choose_search_completed\n");
cfa78eaa
RK
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) {
2a9a65e4 277 choose_search_obsolete = 0;
cfa78eaa
RK
278 choose_search_entry_changed(0, 0);
279 return;
280 }
2a9a65e4 281 //fprintf(stderr, "*** %d search results\n", nvec);
bd802f22
RK
282 /* We're actually going to use these search results. Autocollapse anything
283 * left over from the old search. */
284 choose_auto_collapse();
cfa78eaa 285 choose_search_hash = hash_new(1);
2a9a65e4
RK
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);
b96f65cf
RK
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);
a59663d4
RK
302 choose_n_search_results = 0;
303 choose_search_results = 0;
304 choose_n_search_references = 0;
305 choose_search_references = 0;
2a9a65e4 306 }
cfa78eaa
RK
307 event_raise("search-results-changed", 0);
308}
309
9eeb9f12
RK
310/** @brief Actually initiate a search */
311static void initiate_search(void) {
312 //fprintf(stderr, "initiate_search\n");
cfa78eaa
RK
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 */
9eeb9f12
RK
320 while(*terms == ' ')
321 ++terms;
cfa78eaa 322 char *e = terms + strlen(terms);
9eeb9f12
RK
323 while(e > terms && e[-1] == ' ')
324 --e;
cfa78eaa 325 *e = 0;
9eeb9f12
RK
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;
cfa78eaa
RK
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
9eeb9f12
RK
345static 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 */
364static 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
b96f65cf
RK
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 */
393static 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
a59663d4
RK
434void choose_next_clicked(GtkButton attribute((unused)) *button,
435 gpointer attribute((unused)) userdata) {
62dcb54f 436 gtk_widget_grab_focus(choose_view);
a59663d4
RK
437 if(!choose_n_search_results)
438 return;
b96f65cf
RK
439 /* Find the last visible row */
440 GtkTreePath *endpath;
441 gboolean endvalid = choose_get_visible_range(GTK_TREE_VIEW(choose_view),
442 NULL,
443 &endpath);
444 if(!endvalid)
445 return;
446 /* Find a the first search result later than it. They're sorted so we could
447 * actually do much better than this if necessary. */
448 for(int n = 0; n < choose_n_search_references; ++n) {
449 GtkTreePath *path
450 = gtk_tree_row_reference_get_path(choose_search_references[n]);
451 if(!path)
452 continue;
453 if(gtk_tree_path_compare(endpath, path) < 0) {
454 choose_make_path_visible(path, 0.5);
455 gtk_tree_path_free(path);
456 return;
457 }
458 gtk_tree_path_free(path);
459 }
a59663d4
RK
460 /* We didn't find one. Loop back to the first. */
461 for(int n = 0; n < choose_n_search_references; ++n) {
462 GtkTreePath *path
463 = gtk_tree_row_reference_get_path(choose_search_references[n]);
464 if(!path)
465 continue;
466 choose_make_path_visible(path, 0.5);
467 gtk_tree_path_free(path);
468 return;
469 }
cfa78eaa
RK
470}
471
a59663d4
RK
472void choose_prev_clicked(GtkButton attribute((unused)) *button,
473 gpointer attribute((unused)) userdata) {
62dcb54f 474 gtk_widget_grab_focus(choose_view);
b96f65cf 475 /* TODO can we de-dupe with choose_next_clicked? Probably yes. */
a59663d4
RK
476 if(!choose_n_search_results)
477 return;
b96f65cf
RK
478 /* Find the first visible row */
479 GtkTreePath *startpath;
480 gboolean startvalid = choose_get_visible_range(GTK_TREE_VIEW(choose_view),
481 &startpath,
482 NULL);
483 if(!startvalid)
484 return;
485 /* Find a the last search result earlier than it. They're sorted so we could
486 * actually do much better than this if necessary. */
487 for(int n = choose_n_search_references - 1; n >= 0; --n) {
488 GtkTreePath *path
489 = gtk_tree_row_reference_get_path(choose_search_references[n]);
490 if(!path)
491 continue;
492 if(gtk_tree_path_compare(startpath, path) > 0) {
493 choose_make_path_visible(path, 0.5);
494 gtk_tree_path_free(path);
495 return;
496 }
497 gtk_tree_path_free(path);
498 }
a59663d4
RK
499 /* We didn't find one. Loop down to the last. */
500 for(int n = choose_n_search_references - 1; n >= 0; --n) {
501 GtkTreePath *path
502 = gtk_tree_row_reference_get_path(choose_search_references[n]);
503 if(!path)
504 continue;
505 choose_make_path_visible(path, 0.5);
506 gtk_tree_path_free(path);
507 return;
508 }
cfa78eaa
RK
509}
510
9eeb9f12
RK
511/** @brief Called when the cancel search button is clicked */
512static void choose_clear_clicked(GtkButton attribute((unused)) *button,
513 gpointer attribute((unused)) userdata) {
514 gtk_entry_set_text(GTK_ENTRY(choose_search_entry), "");
62dcb54f 515 gtk_widget_grab_focus(choose_view);
9eeb9f12
RK
516 /* We start things off straight away in this case */
517 initiate_search();
518}
519
4d6a5c4e
RK
520/** @brief Called when the user hits ^F to start a new search */
521void choose_search_new(void) {
522 gtk_editable_select_region(GTK_EDITABLE(choose_search_entry), 0, -1);
523}
524
cfa78eaa
RK
525/** @brief Create the search widget */
526GtkWidget *choose_search_widget(void) {
527
528 /* Text entry box for search terms */
529 choose_search_entry = gtk_entry_new();
530 gtk_widget_set_style(choose_search_entry, tool_style);
531 g_signal_connect(choose_search_entry, "changed",
532 G_CALLBACK(choose_search_entry_changed), 0);
533 gtk_tooltips_set_tip(tips, choose_search_entry,
534 "Enter search terms here; search is automatic", "");
535
536 /* Cancel button to clear the search */
537 choose_clear = gtk_button_new_from_stock(GTK_STOCK_CANCEL);
538 gtk_widget_set_style(choose_clear, tool_style);
539 g_signal_connect(G_OBJECT(choose_clear), "clicked",
540 G_CALLBACK(choose_clear_clicked), 0);
541 gtk_tooltips_set_tip(tips, choose_clear, "Clear search terms", "");
542
543 /* Up and down buttons to find previous/next results; initially they are not
544 * usable as there are no search results. */
545 choose_prev = iconbutton("up.png", "Previous search result");
546 g_signal_connect(G_OBJECT(choose_prev), "clicked",
547 G_CALLBACK(choose_prev_clicked), 0);
548 gtk_widget_set_style(choose_prev, tool_style);
549 gtk_widget_set_sensitive(choose_prev, 0);
550 choose_next = iconbutton("down.png", "Next search result");
551 g_signal_connect(G_OBJECT(choose_next), "clicked",
552 G_CALLBACK(choose_next_clicked), 0);
553 gtk_widget_set_style(choose_next, tool_style);
554 gtk_widget_set_sensitive(choose_next, 0);
555
556 /* Pack the search tools button together on a line */
557 GtkWidget *hbox = gtk_hbox_new(FALSE/*homogeneous*/, 1/*spacing*/);
558 gtk_box_pack_start(GTK_BOX(hbox), choose_search_entry,
559 TRUE/*expand*/, TRUE/*fill*/, 0/*padding*/);
560 gtk_box_pack_start(GTK_BOX(hbox), choose_prev,
561 FALSE/*expand*/, FALSE/*fill*/, 0/*padding*/);
562 gtk_box_pack_start(GTK_BOX(hbox), choose_next,
563 FALSE/*expand*/, FALSE/*fill*/, 0/*padding*/);
564 gtk_box_pack_start(GTK_BOX(hbox), choose_clear,
565 FALSE/*expand*/, FALSE/*fill*/, 0/*padding*/);
566
567 return hbox;
568}
569
570/*
571Local Variables:
572c-basic-offset:2
573comment-column:40
574fill-column:79
575indent-tabs-mode:nil
576End:
577*/