chiark / gitweb /
suppress mention of tkdisorder
[disorder] / disobedience / choose.c
CommitLineData
460b9539 1/*
2 * This file is part of DisOrder
eb525fcd 3 * Copyright (C) 2006, 2007 Richard Kettlewell
460b9539 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 */
b9bdafc7
RK
20/** @file disobedience/choose.c
21 * @brief Hierarchical track selection and search
22 *
23 * We don't use the built-in tree widgets because they require that you know
24 * the children of a node on demand, and we have to wait for the server to tell
25 * us.
26 */
460b9539 27
28#include "disobedience.h"
29
30/* Choose track ------------------------------------------------------------ */
31
e9b70a84 32WT(label);
33WT(event_box);
34WT(menu);
35WT(menu_item);
36WT(layout);
37WT(vbox);
38WT(arrow);
39WT(hbox);
40WT(button);
41WT(image);
42WT(entry);
43
460b9539 44/* Types */
45
46struct choosenode;
47
b9bdafc7 48/** @brief Accumulated information about the tree widget */
460b9539 49struct displaydata {
b9bdafc7
RK
50 /** @brief Maximum width required */
51 guint width;
52 /** @brief Maximum height required */
53 guint height;
460b9539 54};
55
56/* instantiate the node vector type */
b8956e9e
RK
57
58VECTOR_TYPE(nodevector, struct choosenode *, xrealloc);
460b9539 59
16e145a5
RK
60/** @brief Signature of function called when a choosenode is filled */
61typedef void (when_filled_callback)(struct choosenode *cn,
62 void *wfu);
63
b9bdafc7 64/** @brief One node in the virtual filesystem */
460b9539 65struct choosenode {
b9bdafc7
RK
66 struct choosenode *parent; /**< @brief parent node */
67 const char *path; /**< @brief full path or 0 */
68 const char *sort; /**< @brief sort key */
69 const char *display; /**< @brief display name */
70 int pending; /**< @brief pending resolve queries */
460b9539 71 unsigned flags;
b9bdafc7
RK
72#define CN_EXPANDABLE 0x0001 /**< @brief node is expandable */
73#define CN_EXPANDED 0x0002 /**< @brief node is expanded
74 *
75 * Expandable items are directories;
76 * non-expandable ones are files. */
77#define CN_DISPLAYED 0x0004 /**< @brief widget is displayed in layout */
78#define CN_SELECTED 0x0008 /**< @brief node is selected */
16e145a5
RK
79#define CN_GETTING_FILES 0x0010 /**< @brief files inbound */
80#define CN_RESOLVING_FILES 0x0020 /**< @brief resolved files inbound */
81#define CN_GETTING_DIRS 0x0040 /**< @brief directories inbound */
82#define CN_GETTING_ANY 0x0070 /**< @brief getting something */
b9bdafc7
RK
83 struct nodevector children; /**< @brief vector of children */
84 void (*fill)(struct choosenode *); /**< @brief request child fill or 0 for leaf */
85 GtkWidget *container; /**< @brief the container for this row */
86 GtkWidget *hbox; /**< @brief the hbox for this row */
87 GtkWidget *arrow; /**< @brief arrow widget or 0 */
88 GtkWidget *label; /**< @brief text label for this node */
89 GtkWidget *marker; /**< @brief queued marker */
16e145a5
RK
90
91 when_filled_callback *whenfilled; /**< @brief called when filled or 0 */
92 void *wfu; /**< @brief passed to @c whenfilled */
460b9539 93};
94
b9bdafc7 95/** @brief One item in the popup menu */
16e145a5 96struct choose_menuitem {
460b9539 97 /* Parameters */
b9bdafc7 98 const char *name; /**< @brief name */
460b9539 99
100 /* Callbacks */
101 void (*activate)(GtkMenuItem *menuitem, gpointer user_data);
b9bdafc7
RK
102 /**< @brief Called to activate the menu item.
103 *
104 * @p user_data is the choosenode the mouse pointer is over. */
460b9539 105
106 gboolean (*sensitive)(struct choosenode *cn);
b9bdafc7
RK
107 /* @brief Called to determine whether the menu item should be sensitive.
108 *
109 * TODO? */
460b9539 110
111 /* State */
b9bdafc7
RK
112 gulong handlerid; /**< @brief signal handler ID */
113 GtkWidget *w; /**< @brief menu item widget */
460b9539 114};
115
116/* Variables */
117
118static GtkWidget *chooselayout;
b9bdafc7 119static GtkWidget *searchentry; /**< @brief search terms */
460b9539 120static struct choosenode *root;
121static struct choosenode *realroot;
16e145a5
RK
122static GtkWidget *track_menu; /**< @brief track popup menu */
123static GtkWidget *dir_menu; /**< @brief directory popup menu */
b9bdafc7
RK
124static struct choosenode *last_click; /**< @brief last clicked node for selection */
125static int files_visible; /**< @brief total files visible */
126static int files_selected; /**< @brief total files selected */
127static int search_in_flight; /**< @brief a search is underway */
128static int search_obsolete; /**< @brief the current search is void */
129static char **searchresults; /**< @brief search results */
130static int nsearchresults; /**< @brief number of results */
460b9539 131
132/* Forward Declarations */
133
77d95881 134static void clear_children(struct choosenode *cn);
460b9539 135static struct choosenode *newnode(struct choosenode *parent,
136 const char *path,
137 const char *display,
138 const char *sort,
139 unsigned flags,
140 void (*fill)(struct choosenode *));
141static void fill_root_node(struct choosenode *cn);
142static void fill_letter_node(struct choosenode *cn);
143static void fill_directory_node(struct choosenode *cn);
144static void got_files(void *v, int nvec, char **vec);
145static void got_resolved_file(void *v, const char *track);
146static void got_dirs(void *v, int nvec, char **vec);
147
148static void expand_node(struct choosenode *cn);
149static void contract_node(struct choosenode *cn);
150static void updated_node(struct choosenode *cn, int redisplay);
151
152static void display_selection(struct choosenode *cn);
153static void clear_selection(struct choosenode *cn);
154
155static void redisplay_tree(void);
156static struct displaydata display_tree(struct choosenode *cn, int x, int y);
157static void undisplay_tree(struct choosenode *cn);
158static void initiate_search(void);
159static void delete_widgets(struct choosenode *cn);
160
161static void clicked_choosenode(GtkWidget attribute((unused)) *widget,
162 GdkEventButton *event,
163 gpointer user_data);
164
16e145a5
RK
165static void activate_track_play(GtkMenuItem *menuitem, gpointer user_data);
166static void activate_track_properties(GtkMenuItem *menuitem, gpointer user_data);
167
168static gboolean sensitive_track_play(struct choosenode *cn);
169static gboolean sensitive_track_properties(struct choosenode *cn);
170
171static void activate_dir_play(GtkMenuItem *menuitem, gpointer user_data);
172static void activate_dir_properties(GtkMenuItem *menuitem, gpointer user_data);
173static void activate_dir_select(GtkMenuItem *menuitem, gpointer user_data);
174
175static gboolean sensitive_dir_play(struct choosenode *cn);
176static gboolean sensitive_dir_properties(struct choosenode *cn);
177static gboolean sensitive_dir_select(struct choosenode *cn);
178
179/** @brief Track menu items */
180static struct choose_menuitem track_menuitems[] = {
181 { "Play track", activate_track_play, sensitive_track_play, 0, 0 },
182 { "Track properties", activate_track_properties, sensitive_track_properties, 0, 0 },
183 { 0, 0, 0, 0, 0 }
184};
185
186/** @brief Directory menu items */
187static struct choose_menuitem dir_menuitems[] = {
188 { "Play all tracks", activate_dir_play, sensitive_dir_play, 0, 0 },
189 { "Track properties", activate_dir_properties, sensitive_dir_properties, 0, 0 },
190 { "Select all tracks", activate_dir_select, sensitive_dir_select, 0, 0 },
191 { 0, 0, 0, 0, 0 }
460b9539 192};
193
460b9539 194
195/* Maintaining the data structure ------------------------------------------ */
196
b9bdafc7 197/** @brief Create a new node */
460b9539 198static struct choosenode *newnode(struct choosenode *parent,
199 const char *path,
200 const char *display,
201 const char *sort,
202 unsigned flags,
203 void (*fill)(struct choosenode *)) {
204 struct choosenode *const n = xmalloc(sizeof *n);
205
206 D(("newnode %s %s", path, display));
207 if(flags & CN_EXPANDABLE)
208 assert(fill);
209 else
210 assert(!fill);
211 n->parent = parent;
212 n->path = path;
213 n->display = display;
214 n->sort = sort;
215 n->flags = flags;
216 nodevector_init(&n->children);
217 n->fill = fill;
218 if(parent)
219 nodevector_append(&parent->children, n);
220 return n;
221}
222
16e145a5
RK
223/** @brief Called when a node has been filled
224 *
225 * Response for calling @c whenfilled.
226 */
227static void filled(struct choosenode *cn) {
228 when_filled_callback *const whenfilled = cn->whenfilled;
229
230 if(whenfilled) {
231 cn->whenfilled = 0;
232 whenfilled(cn, cn->wfu);
233 }
234}
235
b9bdafc7 236/** @brief Fill the root */
460b9539 237static void fill_root_node(struct choosenode *cn) {
238 int ch;
239 char *name;
240 struct callbackdata *cbd;
241
242 D(("fill_root_node"));
77d95881 243 clear_children(cn);
460b9539 244 if(choosealpha) {
245 if(!cn->children.nvec) { /* Only need to do this once */
246 for(ch = 'A'; ch <= 'Z'; ++ch) {
247 byte_xasprintf(&name, "%c", ch);
248 newnode(cn, "<letter>", name, name, CN_EXPANDABLE, fill_letter_node);
249 }
250 newnode(cn, "<letter>", "*", "~", CN_EXPANDABLE, fill_letter_node);
251 }
252 updated_node(cn, 1);
16e145a5 253 filled(cn);
460b9539 254 } else {
255 /* More de-duping possible here */
16e145a5
RK
256 if(cn->flags & CN_GETTING_ANY)
257 return;
460b9539 258 gtk_label_set_text(GTK_LABEL(report_label), "getting files");
259 cbd = xmalloc(sizeof *cbd);
260 cbd->u.choosenode = cn;
261 disorder_eclient_dirs(client, got_dirs, "", 0, cbd);
262 cbd = xmalloc(sizeof *cbd);
263 cbd->u.choosenode = cn;
264 disorder_eclient_files(client, got_files, "", 0, cbd);
16e145a5 265 cn->flags |= CN_GETTING_FILES|CN_GETTING_DIRS;
460b9539 266 }
267}
268
b9bdafc7 269/** @brief Delete all the widgets owned by @p cn */
e9b70a84 270static void delete_cn_widgets(struct choosenode *cn) {
271 if(cn->arrow) {
272 DW(arrow);
273 gtk_widget_destroy(cn->arrow);
274 cn->arrow = 0;
275 }
276 if(cn->label) {
277 DW(label);
278 gtk_widget_destroy(cn->label);
279 cn->label = 0;
280 }
281 if(cn->marker) {
282 DW(image);
283 gtk_widget_destroy(cn->marker);
284 cn->marker = 0;
285 }
286 if(cn->hbox) {
287 DW(hbox);
288 gtk_widget_destroy(cn->hbox);
289 cn->hbox = 0;
290 }
291 if(cn->container) {
292 DW(event_box);
293 gtk_widget_destroy(cn->container);
294 cn->container = 0;
295 }
296}
297
b9bdafc7
RK
298/** @brief Recursively clear all the children of @p cn
299 *
300 * All the widgets at or below @p cn are deleted. All choosenodes below
301 * it are emptied. i.e. we prune the tree at @p cn.
302 */
460b9539 303static void clear_children(struct choosenode *cn) {
304 int n;
305
306 D(("clear_children %s", cn->path));
307 /* Recursively clear subtrees */
308 for(n = 0; n < cn->children.nvec; ++n) {
309 clear_children(cn->children.vec[n]);
e9b70a84 310 delete_cn_widgets(cn->children.vec[n]);
460b9539 311 }
312 cn->children.nvec = 0;
313}
314
b9bdafc7 315/** @brief Fill a letter node */
460b9539 316static void fill_letter_node(struct choosenode *cn) {
317 const char *regexp;
318 struct callbackdata *cbd;
319
320 D(("fill_letter_node %s", cn->display));
16e145a5
RK
321 if(cn->flags & CN_GETTING_ANY)
322 return;
460b9539 323 switch(cn->display[0]) {
324 default:
325 byte_xasprintf((char **)&regexp, "^(the )?%c", tolower(cn->display[0]));
326 break;
327 case 'T':
328 regexp = "^(?!the [^t])t";
329 break;
330 case '*':
331 regexp = "^[^a-z]";
332 break;
333 }
334 /* TODO: caching */
335 /* TODO: de-dupe against fill_directory_node */
336 gtk_label_set_text(GTK_LABEL(report_label), "getting files");
337 clear_children(cn);
338 cbd = xmalloc(sizeof *cbd);
339 cbd->u.choosenode = cn;
340 disorder_eclient_dirs(client, got_dirs, "", regexp, cbd);
341 cbd = xmalloc(sizeof *cbd);
342 cbd->u.choosenode = cn;
343 disorder_eclient_files(client, got_files, "", regexp, cbd);
16e145a5 344 cn->flags |= CN_GETTING_FILES|CN_GETTING_DIRS;
460b9539 345}
346
b9bdafc7 347/** @brief Called with a list of files just below some node */
460b9539 348static void got_files(void *v, int nvec, char **vec) {
349 struct callbackdata *cbd = v;
350 struct choosenode *cn = cbd->u.choosenode;
351 int n;
352
353 D(("got_files %d files for %s", nvec, cn->path));
354 /* Complicated by the need to resolve aliases. We can save a bit of effort
355 * by re-using cbd though. */
16e145a5 356 cn->flags &= ~CN_GETTING_FILES;
5459347d
RK
357 if((cn->pending = nvec)) {
358 cn->flags |= CN_RESOLVING_FILES;
359 for(n = 0; n < nvec; ++n)
360 disorder_eclient_resolve(client, got_resolved_file, vec[n], cbd);
361 }
460b9539 362}
363
b9bdafc7 364/** @brief Called with an alias resolved filename */
460b9539 365static void got_resolved_file(void *v, const char *track) {
366 struct callbackdata *cbd = v;
367 struct choosenode *cn = cbd->u.choosenode, *file_cn;
368
b9bdafc7 369 /* TODO as below */
460b9539 370 file_cn = newnode(cn, track,
371 trackname_transform("track", track, "display"),
372 trackname_transform("track", track, "sort"),
373 0/*flags*/, 0/*fill*/);
374 /* Only bother updating when we've got the lot */
16e145a5
RK
375 if(--cn->pending == 0) {
376 cn->flags &= ~CN_RESOLVING_FILES;
460b9539 377 updated_node(cn, 1);
16e145a5
RK
378 if(!(cn->flags & CN_GETTING_ANY))
379 filled(cn);
380 }
460b9539 381}
382
b9bdafc7 383/** @brief Called with a list of directories just below some node */
460b9539 384static void got_dirs(void *v, int nvec, char **vec) {
385 struct callbackdata *cbd = v;
386 struct choosenode *cn = cbd->u.choosenode;
387 int n;
388
389 D(("got_dirs %d dirs for %s", nvec, cn->path));
b9bdafc7
RK
390 /* TODO this depends on local configuration for trackname_transform().
391 * This will work, since the defaults are now built-in, but it'll be
392 * (potentially) different to the server's configured settings.
393 *
394 * Really we want a variant of files/dirs that produces both the
395 * raw filename and the transformed name for a chosen context.
396 */
460b9539 397 for(n = 0; n < nvec; ++n)
398 newnode(cn, vec[n],
399 trackname_transform("dir", vec[n], "display"),
400 trackname_transform("dir", vec[n], "sort"),
401 CN_EXPANDABLE, fill_directory_node);
402 updated_node(cn, 1);
16e145a5
RK
403 cn->flags &= ~CN_GETTING_DIRS;
404 if(!(cn->flags & CN_GETTING_ANY))
405 filled(cn);
460b9539 406}
407
b9bdafc7 408/** @brief Fill a child node */
460b9539 409static void fill_directory_node(struct choosenode *cn) {
410 struct callbackdata *cbd;
411
412 D(("fill_directory_node %s", cn->path));
413 /* TODO: caching */
414 /* TODO: de-dupe against fill_letter_node */
16e145a5
RK
415 if(cn->flags & CN_GETTING_ANY)
416 return;
460b9539 417 assert(report_label != 0);
418 gtk_label_set_text(GTK_LABEL(report_label), "getting files");
1d0c28c7 419 clear_children(cn);
460b9539 420 cbd = xmalloc(sizeof *cbd);
421 cbd->u.choosenode = cn;
422 disorder_eclient_dirs(client, got_dirs, cn->path, 0, cbd);
423 cbd = xmalloc(sizeof *cbd);
424 cbd->u.choosenode = cn;
425 disorder_eclient_files(client, got_files, cn->path, 0, cbd);
16e145a5 426 cn->flags |= CN_GETTING_FILES|CN_GETTING_DIRS;
460b9539 427}
428
b9bdafc7 429/** @brief Expand a node */
460b9539 430static void expand_node(struct choosenode *cn) {
431 D(("expand_node %s", cn->path));
432 assert(cn->flags & CN_EXPANDABLE);
433 /* If node is already expanded do nothing. */
434 if(cn->flags & CN_EXPANDED) return;
435 /* We mark the node as expanded and request that it fill itself. When it has
436 * completed it will called updated_node() and we can redraw at that
437 * point. */
438 cn->flags |= CN_EXPANDED;
439 /* TODO: visual feedback */
440 cn->fill(cn);
441}
442
b9bdafc7 443/** @brief Contract a node */
460b9539 444static void contract_node(struct choosenode *cn) {
445 D(("contract_node %s", cn->path));
446 assert(cn->flags & CN_EXPANDABLE);
447 /* If node is already contracted do nothing. */
448 if(!(cn->flags & CN_EXPANDED)) return;
449 cn->flags &= ~CN_EXPANDED;
450 /* Clear selection below this node */
451 clear_selection(cn);
e9b70a84 452 /* Zot children. We never used to do this but the result would be that over
453 * time you'd end up with the entire tree pulled into memory. If the server
454 * is over a slow network it will make interactivity slightly worse; if
455 * anyone complains we can make it an option. */
456 clear_children(cn);
460b9539 457 /* We can contract a node immediately. */
458 redisplay_tree();
459}
460
b9bdafc7 461/** @brief qsort() callback for ordering choosenodes */
460b9539 462static int compare_choosenode(const void *av, const void *bv) {
463 const struct choosenode *const *aa = av, *const *bb = bv;
464 const struct choosenode *a = *aa, *b = *bb;
465
466 return compare_tracks(a->sort, b->sort,
467 a->display, b->display,
468 a->path, b->path);
469}
470
b9bdafc7 471/** @brief Called when an expandable node is updated. */
460b9539 472static void updated_node(struct choosenode *cn, int redisplay) {
473 D(("updated_node %s", cn->path));
474 assert(cn->flags & CN_EXPANDABLE);
475 /* It might be that the node has been de-expanded since we requested the
476 * update. In that case we ignore this notification. */
477 if(!(cn->flags & CN_EXPANDED)) return;
478 /* Sort children */
479 qsort(cn->children.vec, cn->children.nvec, sizeof (struct choosenode *),
480 compare_choosenode);
481 if(redisplay)
482 redisplay_tree();
483}
484
485/* Searching --------------------------------------------------------------- */
486
b9bdafc7 487/** @brief qsort() callback for ordering tracks */
460b9539 488static int compare_track_for_qsort(const void *a, const void *b) {
489 return compare_path(*(char **)a, *(char **)b);
490}
491
b9bdafc7 492/** @brief Return true iff @p file is a child of @p dir */
460b9539 493static int is_child(const char *dir, const char *file) {
494 const size_t dlen = strlen(dir);
495
496 return (!strncmp(file, dir, dlen)
497 && file[dlen] == '/'
498 && strchr(file + dlen + 1, '/') == 0);
499}
500
b9bdafc7 501/** @brief Return true iff @p file is a descendant of @p dir */
460b9539 502static int is_descendant(const char *dir, const char *file) {
503 const size_t dlen = strlen(dir);
504
505 return !strncmp(file, dir, dlen) && file[dlen] == '/';
506}
507
b9bdafc7 508/** @brief Called to fill a node in the search results tree */
460b9539 509static void fill_search_node(struct choosenode *cn) {
510 int n;
511 const size_t plen = strlen(cn->path);
512 const char *s;
513 char *dir, *last = 0;
514
515 D(("fill_search_node %s", cn->path));
516 /* We depend on the search results being sorted as by compare_path(). */
9e9fc4ae 517 clear_children(cn);
460b9539 518 for(n = 0; n < nsearchresults; ++n) {
519 /* We only care about descendants of CN */
520 if(!is_descendant(cn->path, searchresults[n]))
521 continue;
522 s = strchr(searchresults[n] + plen + 1, '/');
523 if(s) {
524 /* We've identified a subdirectory of CN. */
525 dir = xstrndup(searchresults[n], s - searchresults[n]);
526 if(!last || strcmp(dir, last)) {
527 /* Not a duplicate */
528 last = dir;
529 newnode(cn, dir,
530 trackname_transform("dir", dir, "display"),
531 trackname_transform("dir", dir, "sort"),
532 CN_EXPANDABLE, fill_search_node);
533 }
534 } else {
535 /* We've identified a file in CN */
536 newnode(cn, searchresults[n],
537 trackname_transform("track", searchresults[n], "display"),
538 trackname_transform("track", searchresults[n], "sort"),
539 0/*flags*/, 0/*fill*/);
540 }
541 }
542 updated_node(cn, 1);
543}
544
b9bdafc7
RK
545/** @brief Called with a list of search results
546 *
547 * This is called from eclient with a (possibly empty) list of search results,
460b9539 548 * and also from initiate_seatch with an always empty list to indicate that
549 * we're not searching for anything in particular. */
550static void search_completed(void attribute((unused)) *v,
551 int nvec, char **vec) {
552 struct choosenode *cn;
553 int n;
554 const char *dir;
555
556 search_in_flight = 0;
557 if(search_obsolete) {
558 /* This search has been obsoleted by user input since it started.
559 * Therefore we throw away the result and search again. */
560 search_obsolete = 0;
561 initiate_search();
562 } else {
563 if(nvec) {
564 /* We will replace the choose tree with a tree structured view of search
565 * results. First we must disabled the choose tree's widgets. */
566 delete_widgets(root);
567 /* Put the tracks into order, grouped by directory. They'll probably
568 * come back this way anyway in current versions of the server, but it's
569 * cheap not to rely on it (compared with the massive effort we expend
570 * later on) */
571 qsort(vec, nvec, sizeof(char *), compare_track_for_qsort);
572 searchresults = vec;
573 nsearchresults = nvec;
574 cn = root = newnode(0/*parent*/, "", "Search results", "",
575 CN_EXPANDABLE|CN_EXPANDED, fill_search_node);
576 /* Construct the initial tree. We do this in a single pass and expand
577 * everything, so you can actually see your search results. */
578 for(n = 0; n < nsearchresults; ++n) {
579 /* Firstly we might need to go up a few directories to each an ancestor
580 * of this track */
581 while(!is_descendant(cn->path, searchresults[n])) {
582 /* We report the update on each node the last time we see it (With
583 * display=0, the main purpose of this is to get the order of the
584 * children right.) */
585 updated_node(cn, 0);
586 cn = cn->parent;
587 }
588 /* Secondly we might need to insert some new directories */
589 while(!is_child(cn->path, searchresults[n])) {
590 /* Figure out the subdirectory */
591 dir = xstrndup(searchresults[n],
592 strchr(searchresults[n] + strlen(cn->path) + 1,
593 '/') - searchresults[n]);
594 cn = newnode(cn, dir,
595 trackname_transform("dir", dir, "display"),
596 trackname_transform("dir", dir, "sort"),
597 CN_EXPANDABLE|CN_EXPANDED, fill_search_node);
598 }
599 /* Finally we can insert the track as a child of the current
600 * directory */
601 newnode(cn, searchresults[n],
602 trackname_transform("track", searchresults[n], "display"),
603 trackname_transform("track", searchresults[n], "sort"),
604 0/*flags*/, 0/*fill*/);
605 }
606 while(cn) {
607 /* Update all the nodes back up to the root */
608 updated_node(cn, 0);
609 cn = cn->parent;
610 }
611 /* Now it's worth displaying the tree */
612 redisplay_tree();
613 } else if(root != realroot) {
614 delete_widgets(root);
615 root = realroot;
616 redisplay_tree();
617 }
618 }
619}
620
b9bdafc7
RK
621/** @brief Initiate a search
622 *
623 * If a search is underway we set @ref search_obsolete and restart the search
624 * in search_completed() above.
625 */
460b9539 626static void initiate_search(void) {
627 char *terms, *e;
628
629 /* Find out what the user is after */
630 terms = xstrdup(gtk_entry_get_text(GTK_ENTRY(searchentry)));
631 /* Strip leading and trailing space */
632 while(*terms == ' ') ++terms;
633 e = terms + strlen(terms);
634 while(e > terms && e[-1] == ' ') --e;
635 *e = 0;
636 /* If a search is already underway then mark it as obsolete. We'll revisit
637 * when it returns. */
638 if(search_in_flight) {
639 search_obsolete = 1;
640 return;
641 }
642 if(*terms) {
643 /* There's still something left. Initiate the search. */
644 if(disorder_eclient_search(client, search_completed, terms, 0)) {
645 /* The search terms are bad! We treat this as if there were no search
646 * terms at all. Some kind of feedback would be handy. */
647 fprintf(stderr, "bad terms [%s]\n", terms); /* TODO */
648 search_completed(0, 0, 0);
649 } else {
650 search_in_flight = 1;
651 }
652 } else {
653 /* No search terms - we want to see all tracks */
654 search_completed(0, 0, 0);
655 }
656}
657
b9bdafc7 658/** @brief Called when the cancel search button is clicked */
460b9539 659static void clearsearch_clicked(GtkButton attribute((unused)) *button,
660 gpointer attribute((unused)) userdata) {
661 gtk_entry_set_text(GTK_ENTRY(searchentry), "");
662}
663
664/* Display functions ------------------------------------------------------- */
665
b9bdafc7 666/** @brief Delete all the widgets in the tree */
460b9539 667static void delete_widgets(struct choosenode *cn) {
668 int n;
669
e9b70a84 670 delete_cn_widgets(cn);
460b9539 671 for(n = 0; n < cn->children.nvec; ++n)
672 delete_widgets(cn->children.vec[n]);
673 cn->flags &= ~(CN_DISPLAYED|CN_SELECTED);
674 files_selected = 0;
675}
676
b9bdafc7 677/** @brief Update the display */
460b9539 678static void redisplay_tree(void) {
679 struct displaydata d;
680 guint oldwidth, oldheight;
681
682 D(("redisplay_tree"));
683 /* We'll count these up empirically each time */
684 files_selected = 0;
685 files_visible = 0;
686 /* Correct the layout and find out how much space it uses */
e1d4a32b 687 MTAG_PUSH("display_tree");
460b9539 688 d = display_tree(root, 0, 0);
e1d4a32b 689 MTAG_POP();
460b9539 690 /* We must set the total size or scrolling will not work (it wouldn't be hard
691 * for GtkLayout to figure it out for itself but presumably you're supposed
692 * to be able to have widgets off the edge of the layuot.)
693 *
694 * There is a problem: if we shrink the size then the part of the screen that
695 * is outside the new size but inside the old one is not updated. I think
696 * this is arguably bug in GTK+ but it's easy to force a redraw if this
697 * region is nonempty.
698 */
699 gtk_layout_get_size(GTK_LAYOUT(chooselayout), &oldwidth, &oldheight);
700 if(oldwidth > d.width || oldheight > d.height)
701 gtk_widget_queue_draw(chooselayout);
702 gtk_layout_set_size(GTK_LAYOUT(chooselayout), d.width, d.height);
703 /* Notify the main menu of any recent changes */
704 menu_update(-1);
705}
706
b9bdafc7
RK
707/** @brief Recursive step for redisplay_tree()
708 *
709 * Makes sure all displayed widgets from CN down exist and are in their proper
710 * place and return the maximum space used.
711 */
460b9539 712static struct displaydata display_tree(struct choosenode *cn, int x, int y) {
713 int n, aw;
714 GtkRequisition req;
715 struct displaydata d, cd;
716 GdkPixbuf *pb;
717
718 D(("display_tree %s %d,%d", cn->path, x, y));
719
720 /* An expandable item contains an arrow and a text label. When you press the
721 * button it flips its expand state.
722 *
723 * A non-expandable item has just a text label and no arrow.
724 */
725 if(!cn->container) {
521b8841 726 MTAG_PUSH("make_widgets_1");
460b9539 727 /* Widgets need to be created */
e9b70a84 728 NW(hbox);
460b9539 729 cn->hbox = gtk_hbox_new(FALSE, 1);
730 if(cn->flags & CN_EXPANDABLE) {
e9b70a84 731 NW(arrow);
460b9539 732 cn->arrow = gtk_arrow_new(cn->flags & CN_EXPANDED ? GTK_ARROW_DOWN
733 : GTK_ARROW_RIGHT,
734 GTK_SHADOW_NONE);
735 cn->marker = 0;
736 } else {
737 cn->arrow = 0;
e9b70a84 738 if((pb = find_image("notes.png"))) {
739 NW(image);
460b9539 740 cn->marker = gtk_image_new_from_pixbuf(pb);
e9b70a84 741 }
460b9539 742 }
521b8841 743 MTAG_POP();
744 MTAG_PUSH("make_widgets_2");
e9b70a84 745 NW(label);
460b9539 746 cn->label = gtk_label_new(cn->display);
747 if(cn->arrow)
748 gtk_container_add(GTK_CONTAINER(cn->hbox), cn->arrow);
749 gtk_container_add(GTK_CONTAINER(cn->hbox), cn->label);
750 if(cn->marker)
751 gtk_container_add(GTK_CONTAINER(cn->hbox), cn->marker);
521b8841 752 MTAG_POP();
753 MTAG_PUSH("make_widgets_3");
e9b70a84 754 NW(event_box);
460b9539 755 cn->container = gtk_event_box_new();
756 gtk_container_add(GTK_CONTAINER(cn->container), cn->hbox);
757 g_signal_connect(cn->container, "button-release-event",
758 G_CALLBACK(clicked_choosenode), cn);
759 g_signal_connect(cn->container, "button-press-event",
760 G_CALLBACK(clicked_choosenode), cn);
761 g_object_ref(cn->container);
762 gtk_widget_set_name(cn->label, "choose");
763 gtk_widget_set_name(cn->container, "choose");
764 /* Show everything by default */
765 gtk_widget_show_all(cn->container);
e1d4a32b 766 MTAG_POP();
460b9539 767 }
768 assert(cn->container);
769 /* Make sure the icon is right */
770 if(cn->flags & CN_EXPANDABLE)
771 gtk_arrow_set(GTK_ARROW(cn->arrow),
772 cn->flags & CN_EXPANDED ? GTK_ARROW_DOWN : GTK_ARROW_RIGHT,
773 GTK_SHADOW_NONE);
774 else if(cn->marker)
775 /* Make sure the queued marker is right */
776 /* TODO: doesn't always work */
777 (queued(cn->path) ? gtk_widget_show : gtk_widget_hide)(cn->marker);
778 /* Put the widget in the right place */
779 if(cn->flags & CN_DISPLAYED)
780 gtk_layout_move(GTK_LAYOUT(chooselayout), cn->container, x, y);
781 else {
782 gtk_layout_put(GTK_LAYOUT(chooselayout), cn->container, x, y);
783 cn->flags |= CN_DISPLAYED;
521b8841 784 /* Now chooselayout has a ref to the container */
785 g_object_unref(cn->container);
460b9539 786 }
787 /* Set the widget's selection status */
788 if(!(cn->flags & CN_EXPANDABLE))
789 display_selection(cn);
790 /* Find the size used so we can get vertical positioning right. */
791 gtk_widget_size_request(cn->container, &req);
792 d.width = x + req.width;
793 d.height = y + req.height;
794 if(cn->flags & CN_EXPANDED) {
795 /* We'll offset children by the size of the arrow whatever it might be. */
796 assert(cn->arrow);
797 gtk_widget_size_request(cn->arrow, &req);
798 aw = req.width;
799 for(n = 0; n < cn->children.nvec; ++n) {
800 cd = display_tree(cn->children.vec[n], x + aw, d.height);
801 if(cd.width > d.width)
802 d.width = cd.width;
803 d.height = cd.height;
804 }
805 } else {
806 for(n = 0; n < cn->children.nvec; ++n)
807 undisplay_tree(cn->children.vec[n]);
808 }
809 if(!(cn->flags & CN_EXPANDABLE)) {
810 ++files_visible;
811 if(cn->flags & CN_SELECTED)
812 ++files_selected;
813 }
814 /* report back how much space we used */
815 D(("display_tree %s %d,%d total size %dx%d", cn->path, x, y,
816 d.width, d.height));
817 return d;
818}
819
b9bdafc7 820/** @brief Remove widgets for newly hidden nodes */
460b9539 821static void undisplay_tree(struct choosenode *cn) {
822 int n;
823
824 D(("undisplay_tree %s", cn->path));
825 /* Remove this widget from the display */
826 if(cn->flags & CN_DISPLAYED) {
827 gtk_container_remove(GTK_CONTAINER(chooselayout), cn->container);
828 cn->flags ^= CN_DISPLAYED;
829 }
830 /* Remove children too */
831 for(n = 0; n < cn->children.nvec; ++n)
832 undisplay_tree(cn->children.vec[n]);
833}
834
835/* Selection --------------------------------------------------------------- */
836
b9bdafc7 837/** @brief Mark the widget @p cn according to its selection state */
460b9539 838static void display_selection(struct choosenode *cn) {
839 /* Need foreground and background colors */
840 gtk_widget_set_state(cn->label, (cn->flags & CN_SELECTED
841 ? GTK_STATE_SELECTED : GTK_STATE_NORMAL));
842 gtk_widget_set_state(cn->container, (cn->flags & CN_SELECTED
843 ? GTK_STATE_SELECTED : GTK_STATE_NORMAL));
844}
845
b9bdafc7
RK
846/** @brief Set the selection state of a widget
847 *
848 * Directories can never be selected, we just ignore attempts to do so. */
460b9539 849static void set_selection(struct choosenode *cn, int selected) {
850 unsigned f = selected ? CN_SELECTED : 0;
851
852 D(("set_selection %d %s", selected, cn->path));
853 if(!(cn->flags & CN_EXPANDABLE) && (cn->flags & CN_SELECTED) != f) {
854 cn->flags ^= CN_SELECTED;
855 /* Maintain selection count */
856 if(selected)
857 ++files_selected;
858 else
859 --files_selected;
860 display_selection(cn);
861 /* Update main menu sensitivity */
862 menu_update(-1);
863 }
864}
865
b9bdafc7 866/** @brief Recursively clear all selection bits from CN down */
460b9539 867static void clear_selection(struct choosenode *cn) {
868 int n;
869
870 set_selection(cn, 0);
871 for(n = 0; n < cn->children.nvec; ++n)
872 clear_selection(cn->children.vec[n]);
873}
874
875/* User actions ------------------------------------------------------------ */
876
b9bdafc7
RK
877/** @brief Clicked on something
878 *
879 * This implements playing, all the modifiers for selection, etc.
880 */
460b9539 881static void clicked_choosenode(GtkWidget attribute((unused)) *widget,
882 GdkEventButton *event,
883 gpointer user_data) {
884 struct choosenode *cn = user_data;
885 int ind, last_ind, n;
886
887 D(("clicked_choosenode %s", cn->path));
888 if(event->type == GDK_BUTTON_RELEASE
889 && event->button == 1) {
890 /* Left click */
891 if(cn->flags & CN_EXPANDABLE) {
892 /* This is a directory. Flip its expansion status. */
893 if(cn->flags & CN_EXPANDED)
894 contract_node(cn);
895 else
896 expand_node(cn);
897 last_click = 0;
898 } else {
899 /* This is a file. Adjust selection status */
900 /* TODO the basic logic here is essentially the same as that in queue.c.
901 * Can we share code at all? */
902 switch(event->state & (GDK_SHIFT_MASK|GDK_CONTROL_MASK)) {
903 case 0:
904 clear_selection(root);
905 set_selection(cn, 1);
906 last_click = cn;
907 break;
908 case GDK_CONTROL_MASK:
909 set_selection(cn, !(cn->flags & CN_SELECTED));
910 last_click = cn;
911 break;
912 case GDK_SHIFT_MASK:
913 case GDK_SHIFT_MASK|GDK_CONTROL_MASK:
914 if(last_click && last_click->parent == cn->parent) {
915 /* Figure out where the current and last clicks are in the list */
916 ind = last_ind = -1;
917 for(n = 0; n < cn->parent->children.nvec; ++n) {
918 if(cn->parent->children.vec[n] == cn)
919 ind = n;
920 if(cn->parent->children.vec[n] == last_click)
921 last_ind = n;
922 }
923 /* Test shouldn't ever fail, but still */
924 if(ind >= 0 && last_ind >= 0) {
925 if(!(event->state & GDK_CONTROL_MASK)) {
926 for(n = 0; n < cn->parent->children.nvec; ++n)
927 set_selection(cn->parent->children.vec[n], 0);
928 }
929 if(ind > last_ind)
930 for(n = last_ind; n <= ind; ++n)
931 set_selection(cn->parent->children.vec[n], 1);
932 else
933 for(n = ind; n <= last_ind; ++n)
934 set_selection(cn->parent->children.vec[n], 1);
935 if(event->state & GDK_CONTROL_MASK)
936 last_click = cn;
937 }
938 }
b9bdafc7
RK
939 /* TODO trying to select a range that doesn't share a single parent
940 * currently does not work, but it ought to. */
460b9539 941 break;
942 }
943 }
944 } else if(event->type == GDK_BUTTON_RELEASE
945 && event->button == 2) {
946 /* Middle click - play the pointed track */
947 if(!(cn->flags & CN_EXPANDABLE)) {
948 clear_selection(root);
949 set_selection(cn, 1);
950 gtk_label_set_text(GTK_LABEL(report_label), "adding track to queue");
951 disorder_eclient_play(client, cn->path, 0, 0);
952 last_click = 0;
953 }
954 } else if(event->type == GDK_BUTTON_PRESS
955 && event->button == 3) {
16e145a5
RK
956 struct choose_menuitem *const menuitems =
957 (cn->flags & CN_EXPANDABLE ? dir_menuitems : track_menuitems);
958 GtkWidget *const menu =
959 (cn->flags & CN_EXPANDABLE ? dir_menu : track_menu);
460b9539 960 /* Right click. Pop up a menu. */
961 /* If the current file isn't selected, switch the selection to just that.
962 * (If we're looking at a directory then leave the selection alone.) */
963 if(!(cn->flags & CN_EXPANDABLE) && !(cn->flags & CN_SELECTED)) {
964 clear_selection(root);
965 set_selection(cn, 1);
966 last_click = cn;
967 }
968 /* Set the item sensitivity and callbacks */
16e145a5 969 for(n = 0; menuitems[n].name; ++n) {
460b9539 970 if(menuitems[n].handlerid)
971 g_signal_handler_disconnect(menuitems[n].w,
972 menuitems[n].handlerid);
973 gtk_widget_set_sensitive(menuitems[n].w,
974 menuitems[n].sensitive(cn));
975 menuitems[n].handlerid = g_signal_connect
976 (menuitems[n].w, "activate", G_CALLBACK(menuitems[n].activate), cn);
977 }
978 /* Pop up the menu */
979 gtk_widget_show_all(menu);
980 gtk_menu_popup(GTK_MENU(menu), 0, 0, 0, 0,
981 event->button, event->time);
982 }
983}
984
b9bdafc7 985/** @brief Called BY GTK+ to tell us the search entry box has changed */
460b9539 986static void searchentry_changed(GtkEditable attribute((unused)) *editable,
987 gpointer attribute((unused)) user_data) {
988 initiate_search();
989}
990
16e145a5 991/* Track menu items -------------------------------------------------------- */
460b9539 992
b9bdafc7 993/** @brief Recursive step for gather_selected() */
460b9539 994static void recurse_selected(struct choosenode *cn, struct vector *v) {
995 int n;
996
997 if(cn->flags & CN_EXPANDABLE) {
998 if(cn->flags & CN_EXPANDED)
999 for(n = 0; n < cn->children.nvec; ++n)
1000 recurse_selected(cn->children.vec[n], v);
1001 } else {
1002 if((cn->flags & CN_SELECTED) && cn->path)
1003 vector_append(v, (char *)cn->path);
1004 }
1005}
1006
b9bdafc7 1007/*** @brief Get a list of all the selected tracks */
16e145a5 1008static const char **gather_selected(int *ntracks) {
460b9539 1009 struct vector v;
1010
1011 vector_init(&v);
1012 recurse_selected(root, &v);
1013 vector_terminate(&v);
1014 if(ntracks) *ntracks = v.nvec;
16e145a5 1015 return (const char **)v.vec;
460b9539 1016}
1017
16e145a5
RK
1018/** @brief Called when the track menu's play option is activated */
1019static void activate_track_play(GtkMenuItem attribute((unused)) *menuitem,
1020 gpointer attribute((unused)) user_data) {
1021 const char **tracks = gather_selected(0);
460b9539 1022 int n;
1023
1024 gtk_label_set_text(GTK_LABEL(report_label), "adding track to queue");
1025 for(n = 0; tracks[n]; ++n)
1026 disorder_eclient_play(client, tracks[n], 0, 0);
1027}
1028
b9bdafc7 1029/** @brief Called when the menu's properties option is activated */
16e145a5
RK
1030static void activate_track_properties(GtkMenuItem attribute((unused)) *menuitem,
1031 gpointer attribute((unused)) user_data) {
460b9539 1032 int ntracks;
16e145a5 1033 const char **tracks = gather_selected(&ntracks);
460b9539 1034
1035 properties(ntracks, tracks);
1036}
1037
b9bdafc7 1038/** @brief Determine whether the menu's play option should be sensitive */
16e145a5 1039static gboolean sensitive_track_play(struct choosenode attribute((unused)) *cn) {
8f763f1b
RK
1040 return (!!files_selected
1041 && (disorder_eclient_state(client) & DISORDER_CONNECTED));
460b9539 1042}
1043
b9bdafc7 1044/** @brief Determine whether the menu's properties option should be sensitive */
16e145a5 1045static gboolean sensitive_track_properties(struct choosenode attribute((unused)) *cn) {
8f763f1b 1046 return !!files_selected && (disorder_eclient_state(client) & DISORDER_CONNECTED);
460b9539 1047}
1048
16e145a5
RK
1049/* Directory menu items ---------------------------------------------------- */
1050
1051/** @brief Return the file children of @p cn
1052 *
1053 * The list is terminated by a null pointer.
1054 */
1055static const char **dir_files(struct choosenode *cn, int *nfiles) {
1056 const char **files = xcalloc(cn->children.nvec + 1, sizeof (char *));
1057 int n, m;
1058
1059 for(n = m = 0; n < cn->children.nvec; ++n)
1060 if(!(cn->children.vec[n]->flags & CN_EXPANDABLE))
1061 files[m++] = cn->children.vec[n]->path;
1062 files[m] = 0;
1063 if(nfiles) *nfiles = m;
1064 return files;
1065}
1066
1067static void play_dir(struct choosenode *cn,
1068 void attribute((unused)) *wfu) {
1069 int ntracks, n;
1070 const char **tracks = dir_files(cn, &ntracks);
1071
1072 gtk_label_set_text(GTK_LABEL(report_label), "adding track to queue");
1073 for(n = 0; n < ntracks; ++n)
1074 disorder_eclient_play(client, tracks[n], 0, 0);
1075}
1076
1077static void properties_dir(struct choosenode *cn,
1078 void attribute((unused)) *wfu) {
1079 int ntracks;
1080 const char **tracks = dir_files(cn, &ntracks);
1081
1082 properties(ntracks, tracks);
1083}
1084
1085static void select_dir(struct choosenode *cn,
1086 void attribute((unused)) *wfu) {
1087 int n;
1088
1089 clear_selection(root);
1090 for(n = 0; n < cn->children.nvec; ++n)
1091 set_selection(cn->children.vec[n], 1);
1092}
1093
1094/** @brief Ensure @p cn is expanded and then call @p callback */
1095static void call_with_dir(struct choosenode *cn,
1096 when_filled_callback *whenfilled,
1097 void *wfu) {
1098 if(!(cn->flags & CN_EXPANDABLE))
1099 return; /* something went wrong */
1100 if(cn->flags & CN_EXPANDED)
1101 /* @p cn is already open */
1102 whenfilled(cn, wfu);
1103 else {
1104 /* @p cn is not open, arrange for the callback to go off when it is
1105 * opened */
1106 cn->whenfilled = whenfilled;
1107 cn->wfu = wfu;
1108 expand_node(cn);
1109 }
1110}
1111
1112/** @brief Called when the directory menu's play option is activated */
1113static void activate_dir_play(GtkMenuItem attribute((unused)) *menuitem,
1114 gpointer user_data) {
1115 struct choosenode *const cn = (struct choosenode *)user_data;
1116
1117 call_with_dir(cn, play_dir, 0);
1118}
1119
1120/** @brief Called when the directory menu's properties option is activated */
1121static void activate_dir_properties(GtkMenuItem attribute((unused)) *menuitem,
1122 gpointer user_data) {
1123 struct choosenode *const cn = (struct choosenode *)user_data;
1124
1125 call_with_dir(cn, properties_dir, 0);
1126}
1127
1128/** @brief Called when the directory menu's select option is activated */
1129static void activate_dir_select(GtkMenuItem attribute((unused)) *menuitem,
1130 gpointer user_data) {
1131 struct choosenode *const cn = (struct choosenode *)user_data;
1132
1133 call_with_dir(cn, select_dir, 0);
1134}
1135
1136/** @brief Determine whether the directory menu's play option should be sensitive */
1137static gboolean sensitive_dir_play(struct choosenode attribute((unused)) *cn) {
1138 return !!(disorder_eclient_state(client) & DISORDER_CONNECTED);
1139}
1140
1141/** @brief Determine whether the directory menu's properties option should be sensitive */
1142static gboolean sensitive_dir_properties(struct choosenode attribute((unused)) *cn) {
1143 return !!(disorder_eclient_state(client) & DISORDER_CONNECTED);
1144}
1145
1146/** @brief Determine whether the directory menu's select option should be sensitive */
1147static gboolean sensitive_dir_select(struct choosenode attribute((unused)) *cn) {
1148 return TRUE;
1149}
1150
1151
1152
460b9539 1153/* Main menu plumbing ------------------------------------------------------ */
1154
b9bdafc7 1155/** @brief Determine whether the edit menu's properties option should be sensitive */
460b9539 1156static int choose_properties_sensitive(GtkWidget attribute((unused)) *w) {
8f763f1b 1157 return !!files_selected && (disorder_eclient_state(client) & DISORDER_CONNECTED);
460b9539 1158}
1159
b9bdafc7
RK
1160/** @brief Determine whether the edit menu's select all option should be sensitive
1161 *
1162 * TODO not implemented, see also choose_selectall_activate()
1163 */
460b9539 1164static int choose_selectall_sensitive(GtkWidget attribute((unused)) *w) {
b9bdafc7 1165 return FALSE;
460b9539 1166}
1167
b9bdafc7 1168/** @brief Called when the edit menu's properties option is activated */
460b9539 1169static void choose_properties_activate(GtkWidget attribute((unused)) *w) {
16e145a5 1170 activate_track_properties(0, 0);
460b9539 1171}
1172
b9bdafc7
RK
1173/** @brief Called when the edit menu's select all option is activated
1174 *
1175 * TODO not implemented, see choose_selectall_sensitive() */
460b9539 1176static void choose_selectall_activate(GtkWidget attribute((unused)) *w) {
460b9539 1177}
1178
b9bdafc7 1179/** @brief Main menu callbacks for Choose screen */
460b9539 1180static const struct tabtype tabtype_choose = {
1181 choose_properties_sensitive,
1182 choose_selectall_sensitive,
1183 choose_properties_activate,
1184 choose_selectall_activate,
1185};
1186
1187/* Public entry points ----------------------------------------------------- */
1188
b9bdafc7 1189/** @brief Create a track choice widget */
460b9539 1190GtkWidget *choose_widget(void) {
1191 int n;
1192 GtkWidget *scrolled;
1193 GtkWidget *vbox, *hbox, *clearsearch;
1194
1195 /*
1196 * +--vbox-------------------------------------------------------+
1197 * | +-hbox----------------------------------------------------+ |
1198 * | | searchentry | clearsearch | |
1199 * | +---------------------------------------------------------+ |
1200 * | +-scrolled------------------------------------------------+ |
1201 * | | +-chooselayout------------------------------------++--+ | |
1202 * | | | Tree structure is manually layed out in here ||^^| | |
1203 * | | | || | | |
1204 * | | | || | | |
1205 * | | | || | | |
1206 * | | | ||vv| | |
1207 * | | +-------------------------------------------------++--+ | |
1208 * | | +-------------------------------------------------+ | |
1209 * | | |< >| | |
1210 * | | +-------------------------------------------------+ | |
1211 * | +---------------------------------------------------------+ |
1212 * +-------------------------------------------------------------+
1213 */
1214
1215 /* Text entry box for search terms */
e9b70a84 1216 NW(entry);
460b9539 1217 searchentry = gtk_entry_new();
1218 g_signal_connect(searchentry, "changed", G_CALLBACK(searchentry_changed), 0);
a8c9507f 1219 gtk_tooltips_set_tip(tips, searchentry, "Enter search terms here; search is automatic", "");
460b9539 1220
1221 /* Cancel button to clear the search */
e9b70a84 1222 NW(button);
460b9539 1223 clearsearch = gtk_button_new_from_stock(GTK_STOCK_CANCEL);
1224 g_signal_connect(G_OBJECT(clearsearch), "clicked",
1225 G_CALLBACK(clearsearch_clicked), 0);
a8c9507f
RK
1226 gtk_tooltips_set_tip(tips, clearsearch, "Clear search terms", "");
1227
460b9539 1228
1229 /* hbox packs the search box and the cancel button together on a line */
e9b70a84 1230 NW(hbox);
460b9539 1231 hbox = gtk_hbox_new(FALSE/*homogeneous*/, 1/*spacing*/);
1232 gtk_box_pack_start(GTK_BOX(hbox), searchentry,
1233 TRUE/*expand*/, TRUE/*fill*/, 0/*padding*/);
1234 gtk_box_pack_end(GTK_BOX(hbox), clearsearch,
1235 FALSE/*expand*/, FALSE/*fill*/, 0/*padding*/);
1236
1237 /* chooselayout contains the currently visible subset of the track
1238 * namespace */
e9b70a84 1239 NW(layout);
460b9539 1240 chooselayout = gtk_layout_new(0, 0);
1241 root = newnode(0/*parent*/, "<root>", "All files", "",
1242 CN_EXPANDABLE, fill_root_node);
1243 realroot = root;
1244 expand_node(root); /* will call redisplay_tree */
16e145a5
RK
1245 /* Create the popup menus */
1246 NW(menu);
1247 track_menu = gtk_menu_new();
1248 g_signal_connect(track_menu, "destroy", G_CALLBACK(gtk_widget_destroyed),
1249 &track_menu);
1250 for(n = 0; track_menuitems[n].name; ++n) {
1251 NW(menu_item);
1252 track_menuitems[n].w =
1253 gtk_menu_item_new_with_label(track_menuitems[n].name);
1254 gtk_menu_attach(GTK_MENU(track_menu), track_menuitems[n].w,
1255 0, 1, n, n + 1);
1256 }
e9b70a84 1257 NW(menu);
16e145a5
RK
1258 dir_menu = gtk_menu_new();
1259 g_signal_connect(dir_menu, "destroy", G_CALLBACK(gtk_widget_destroyed),
1260 &dir_menu);
1261 for(n = 0; dir_menuitems[n].name; ++n) {
e9b70a84 1262 NW(menu_item);
16e145a5
RK
1263 dir_menuitems[n].w =
1264 gtk_menu_item_new_with_label(dir_menuitems[n].name);
1265 gtk_menu_attach(GTK_MENU(dir_menu), dir_menuitems[n].w,
1266 0, 1, n, n + 1);
460b9539 1267 }
1268 /* The layout is scrollable */
1269 scrolled = scroll_widget(GTK_WIDGET(chooselayout), "choose");
1270
1271 /* The scrollable layout and the search hbox go together in a vbox */
e9b70a84 1272 NW(vbox);
460b9539 1273 vbox = gtk_vbox_new(FALSE/*homogenous*/, 1/*spacing*/);
1274 gtk_box_pack_start(GTK_BOX(vbox), hbox,
1275 FALSE/*expand*/, FALSE/*fill*/, 0/*padding*/);
1276 gtk_box_pack_end(GTK_BOX(vbox), scrolled,
1277 TRUE/*expand*/, TRUE/*fill*/, 0/*padding*/);
1278
1279 g_object_set_data(G_OBJECT(vbox), "type", (void *)&tabtype_choose);
1280 return vbox;
1281}
1282
b9bdafc7 1283/** @brief Called when something we care about here might have changed */
460b9539 1284void choose_update(void) {
1285 redisplay_tree();
1286}
1287
1288/*
1289Local Variables:
1290c-basic-offset:2
1291comment-column:40
1292fill-column:79
1293indent-tabs-mode:nil
1294End:
1295*/