chiark / gitweb /
Complete Disobedience transition to event_ from _monitor.
[disorder] / disobedience / queue.c
1 /*
2  * This file is part of DisOrder
3  * Copyright (C) 2006-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/queue.c
21  * @brief Queue widgets
22  *
23  * This file provides both the queue widget and the recently-played widget.
24  *
25  * A queue layout is structured as follows:
26  *
27  * <pre>
28  *  vbox
29  *   titlescroll
30  *    titlelayout
31  *     titlecells[col]                 eventbox (made by wrap_queue_cell)
32  *      titlecells[col]->child         label (from columns[])
33  *   mainscroll
34  *    mainlayout
35  *     cells[row * N + c]              eventbox (made by wrap_queue_cell)
36  *      cells[row * N + c]->child      label (from column constructors)
37  * </pre>
38  *
39  * titlescroll never has any scrollbars.  Instead whenever mainscroll's
40  * horizontal adjustment is changed, queue_scrolled adjusts titlescroll to
41  * match, forcing the title and the queue to pan in sync but allowing the queue
42  * to scroll independently.
43  *
44  * Whenever the queue changes everything below mainlayout is thrown away and
45  * reconstructed from scratch.  Name lookups are cached, so this doesn't imply
46  * lots of disorder protocol traffic.
47  *
48  * The last cell on each row is the padding cell, and this extends ridiculously
49  * far to the right.  (Can we do better?)
50  *
51  * When drag and drop is active we create extra eventboxes to act as dropzones.
52  * These only exist while the drag proceeds, as otherwise they steal events
53  * from more deserving widgets.  (It might work to hide them when not in use
54  * too but this way around the d+d code is a bit more self-contained.)
55  *
56  * NB that while in the server the playing track is not in the queue, in
57  * Disobedience, the playing does live in @c ql_queue.q, despite its different
58  * status to everything else found in that list.
59  */
60
61 #include "disobedience.h"
62 #include "charset.h"
63
64 /** @brief Horizontal padding for queue cells */
65 #define HCELLPADDING 4
66
67 /** @brief Vertical padding for queue cells */
68 #define VCELLPADDING 2
69
70 /* Queue management -------------------------------------------------------- */
71
72 WT(label);
73 WT(event_box);
74 WT(menu);
75 WT(menu_item);
76 WT(layout);
77 WT(vbox);
78
79 struct queuelike;
80
81 static void add_drag_targets(struct queuelike *ql);
82 static void remove_drag_targets(struct queuelike *ql);
83 static void redisplay_queue(struct queuelike *ql);
84 static GtkWidget *column_when(const struct queuelike *ql,
85                               const struct queue_entry *q,
86                               const char *data);
87 static GtkWidget *column_who(const struct queuelike *ql,
88                              const struct queue_entry *q,
89                              const char *data);
90 static GtkWidget *column_namepart(const struct queuelike *ql,
91                                   const struct queue_entry *q,
92                                   const char *data);
93 static GtkWidget *column_length(const struct queuelike *ql,
94                                 const struct queue_entry *q,
95                                 const char *data);
96 static int draggable_row(const struct queue_entry *q);
97 static void recent_changed(const char *event,
98                            void *eventdata,
99                            void *callbackdata);
100 static void added_changed(const char *event,
101                           void *eventdata,
102                           void *callbackdata);
103 static void queue_changed(const char *event,
104                           void *eventdata,
105                           void *callbackdata);
106
107 static const struct tabtype tabtype_queue; /* forward */
108
109 static const GtkTargetEntry dragtargets[] = {
110   { (char *)"disobedience-queue", GTK_TARGET_SAME_APP, 0 }
111 };
112 #define NDRAGTARGETS (int)(sizeof dragtargets / sizeof *dragtargets)
113
114 /** @brief Definition of a column */
115 struct column {
116   const char *name;                     /* Column name */
117   GtkWidget *(*widget)(const struct queuelike *ql,
118                        const struct queue_entry *q,
119                        const char *data); /* Make a label for this column */
120   const char *data;                     /* Data to pass to widget() */
121   gfloat xalign;                        /* Alignment of the label */
122 };
123
124 /** @brief Table of columns for queue and recently played list */
125 static const struct column maincolumns[] = {
126   { "When",   column_when,     0,        1 },
127   { "Who",    column_who,      0,        0 },
128   { "Artist", column_namepart, "artist", 0 },
129   { "Album",  column_namepart, "album",  0 },
130   { "Title",  column_namepart, "title",  0 },
131   { "Length", column_length,   0,        1 }
132 };
133
134 /** @brief Number of columns in queue and recnetly played list */
135 #define NMAINCOLUMNS (int)(sizeof maincolumns / sizeof *maincolumns)
136
137 /** @brief Table of columns for recently added tracks */
138 static const struct column addedcolumns[] = {
139   { "Artist", column_namepart, "artist", 0 },
140   { "Album",  column_namepart, "album",  0 },
141   { "Title",  column_namepart, "title",  0 },
142   { "Length", column_length,   0,        1 }
143 };
144
145 /** @brief Number of columns in recently added list */
146 #define NADDEDCOLUMNS (int)(sizeof addedcolumns / sizeof *addedcolumns)
147
148 /** @brief Maximum number of column in any @ref queuelike */
149 #define MAXCOLUMNS (NMAINCOLUMNS > NADDEDCOLUMNS ? NMAINCOLUMNS : NADDEDCOLUMNS)
150
151 /** @brief Data passed to menu item activation handlers */
152 struct menuiteminfo {
153   struct queuelike *ql;                 /**< @brief which queue we're dealing with */
154   struct queue_entry *q;                /**< @brief hovered entry or 0 */
155 };
156
157 /** @brief An item in the queue's popup menu */
158 struct queue_menuitem {
159   /** @brief Menu item name */
160   const char *name;
161
162   /** @brief Called to activate the menu item
163    *
164    * The user data is the queue entry that the pointer was over when the menu
165    * popped up. */
166   void (*activate)(GtkMenuItem *menuitem,
167                    gpointer user_data);
168   
169   /** @brief Called to determine whether the menu item is usable.
170    *
171    * Returns @c TRUE if it should be sensitive and @c FALSE otherwise.  @p q
172    * points to the queue entry the pointer is over.
173    */
174   int (*sensitive)(struct queuelike *ql,
175                    struct queue_menuitem *m,
176                    struct queue_entry *q);
177
178   /** @brief Signal handler ID */
179   gulong handlerid;
180
181   /** @brief Widget for menu item */
182   GtkWidget *w;
183 };
184
185 /** @brief A queue-like object
186  *
187  * There are (currently) three of these: @ref ql_queue, @ref ql_recent and @ref
188  * ql_added.
189  */
190 struct queuelike {
191   /** @brief Called when an update completes */
192   void (*notify)(void);
193
194   /** @brief Called to fix up the queue after update
195    * @param q The list passed back from the server
196    * @return Assigned to @c ql->q
197    */
198   struct queue_entry *(*fixup)(struct queue_entry *q);
199
200   /* Widgets */
201   GtkWidget *mainlayout;                /**< @brief main layout */
202   GtkWidget *mainscroll;                /**< @brief scroller for main layout */
203   GtkWidget *titlelayout;               /**< @brief title layout */
204   GtkWidget *titlecells[MAXCOLUMNS + 1]; /**< @brief title cells */
205   GtkWidget **cells;                    /**< @brief all the cells */
206   GtkWidget *menu;                      /**< @brief popup menu */
207   struct queue_menuitem *menuitems;     /**< @brief menu items */
208   GtkWidget *dragmark;                  /**< @brief drag destination marker */
209   GtkWidget **dropzones;                /**< @brief drag targets */
210   int ndropzones;                       /**< @brief number of drag targets */
211
212   /* State */
213   struct queue_entry *q;                /**< @brief head of queue */
214   struct queue_entry *last_click;       /**< @brief last click */
215   int nrows;                            /**< @brief number of rows */
216   int mainrowheight;                    /**< @brief height of one row */
217   hash *selection;                      /**< @brief currently selected items */
218   int swallow_release;                  /**< @brief swallow button release from drag */
219
220   const struct column *columns;         /**< @brief Table of columns */
221   int ncolumns;                         /**< @brief Number of columns */
222 };
223
224 static struct queuelike ql_queue; /**< @brief The main queue */
225 static struct queuelike ql_recent; /*< @brief Recently-played tracks */
226 static struct queuelike ql_added; /*< @brief Newly added tracks */
227 static struct queue_entry *actual_queue; /**< @brief actual queue */
228 static struct queue_entry *playing_track;     /**< @brief currenty playing */
229 static time_t last_playing = (time_t)-1; /**< @brief when last got playing */
230 static int namepart_lookups_outstanding;
231 static int  namepart_completions_deferred; /* # of completions not processed */
232 static const struct cache_type cachetype_string = { 3600 };
233 static const struct cache_type cachetype_integer = { 3600 };
234 static GtkWidget *playing_length_label;
235
236 /* Debugging --------------------------------------------------------------- */
237
238 #if 0
239 static void describe_widget(const char *name, GtkWidget *w, int indent) {
240   int ww, wh, wx, wy;
241
242   if(name)
243     fprintf(stderr, "%*s[%s]: '%s'\n", indent, "",
244             name, gtk_widget_get_name(w));
245   gdk_window_get_position(w->window, &wx, &wy);
246   gdk_drawable_get_size(GDK_DRAWABLE(w->window), &ww, &wh);
247   fprintf(stderr, "%*s window %p: %dx%d at %dx%d\n",
248           indent, "", w->window, ww, wh, wx, wy);
249 }
250
251 static void dump_layout(const struct queuelike *ql) {
252   GtkWidget *w;
253   char s[20];
254   int row, col;
255   const struct queue_entry *q;
256   
257   describe_widget("mainscroll", ql->mainscroll, 0);
258   describe_widget("mainlayout", ql->mainlayout, 1);
259   for(q = ql->q, row = 0; q; q = q->next, ++row)
260     for(col = 0; col < ql->ncolumns + 1; ++col)
261       if((w = ql->cells[row * (ql->ncolumns + 1) + col])) {
262         sprintf(s, "%dx%d", row, col);
263         describe_widget(s, w, 2);
264         if(GTK_BIN(w)->child)
265           describe_widget(0, w, 3);
266       }
267 }
268 #endif
269
270 /* Track detail lookup ----------------------------------------------------- */
271
272 /** @brief Called when a namepart lookup has completed or failed */
273 static void namepart_completed_or_failed(void) {
274   D(("namepart_completed_or_failed"));
275   --namepart_lookups_outstanding;
276   if(!namepart_lookups_outstanding) {
277     redisplay_queue(&ql_queue);
278     redisplay_queue(&ql_recent);
279     redisplay_queue(&ql_added);
280     namepart_completions_deferred = 0;
281   }
282 }
283
284 /** @brief Called when a namepart lookup has completed */
285 static void namepart_completed(void *v, const char *error, const char *value) {
286   if(error) {
287     gtk_label_set_text(GTK_LABEL(report_label), error);
288   } else {
289     const char *key = v;
290
291     cache_put(&cachetype_string, key, value);
292     ++namepart_completions_deferred;
293   }
294   namepart_completed_or_failed();
295 }
296
297 /** @brief Called when a length lookup has completed */
298 static void length_completed(void *v, const char *error, long l) {
299   if(error)
300     gtk_label_set_text(GTK_LABEL(report_label), error);
301   else {
302     const char *key = v;
303     long *value;
304     
305     D(("namepart_completed"));
306     value = xmalloc(sizeof *value);
307     *value = l;
308     cache_put(&cachetype_integer, key, value);
309     ++namepart_completions_deferred;
310   }
311   namepart_completed_or_failed();
312 }
313
314 /** @brief Arrange to fill in a namepart cache entry */
315 static void namepart_fill(const char *track,
316                           const char *context,
317                           const char *part,
318                           const char *key) {
319   ++namepart_lookups_outstanding;
320   disorder_eclient_namepart(client, namepart_completed,
321                             track, context, part, (void *)key);
322 }
323
324 /** @brief Look up a namepart
325  *
326  * If it is in the cache then just return its value.  If not then look it up
327  * and arrange for the queues to be updated when its value is available. */
328 static const char *namepart(const char *track,
329                             const char *context,
330                             const char *part) {
331   char *key;
332   const char *value;
333
334   D(("namepart %s %s %s", track, context, part));
335   byte_xasprintf(&key, "namepart context=%s part=%s track=%s",
336                  context, part, track);
337   value = cache_get(&cachetype_string, key);
338   if(!value) {
339     D(("deferring..."));
340     /* stick a value in the cache so we don't issue another lookup if we
341      * revisit */
342     cache_put(&cachetype_string, key, value = "?");
343     namepart_fill(track, context, part, key);
344   }
345   return value;
346 }
347
348 /** @brief Called from @ref disobedience/properties.c when we know a name part has changed */
349 void namepart_update(const char *track,
350                      const char *context,
351                      const char *part) {
352   char *key;
353
354   byte_xasprintf(&key, "namepart context=%s part=%s track=%s",
355                  context, part, track);
356   /* Only refetch if it's actually in the cache */
357   if(cache_get(&cachetype_string, key))
358     namepart_fill(track, context, part, key);
359 }
360
361 /** @brief Look up a track length
362  *
363  * If it is in the cache then just return its value.  If not then look it up
364  * and arrange for the queues to be updated when its value is available. */
365 static long getlength(const char *track) {
366   char *key;
367   const long *value;
368   static const long bogus = -1;
369
370   D(("getlength %s", track));
371   byte_xasprintf(&key, "length track=%s", track);
372   value = cache_get(&cachetype_integer, key);
373   if(!value) {
374     D(("deferring..."));;
375     cache_put(&cachetype_integer, key, value = &bogus);
376     ++namepart_lookups_outstanding;
377     disorder_eclient_length(client, length_completed, track, key);
378   }
379   return *value;
380 }
381
382 /* Column constructors ----------------------------------------------------- */
383
384 /** @brief Format the 'when' column */
385 static GtkWidget *column_when(const struct queuelike attribute((unused)) *ql,
386                               const struct queue_entry *q,
387                               const char attribute((unused)) *data) {
388   char when[64];
389   struct tm tm;
390   time_t t;
391
392   D(("column_when"));
393   switch(q->state) {
394   case playing_isscratch:
395   case playing_unplayed:
396   case playing_random:
397     t = q->expected;
398     break;
399   case playing_failed:
400   case playing_no_player:
401   case playing_ok:
402   case playing_scratched:
403   case playing_started:
404   case playing_paused:
405   case playing_quitting:
406     t = q->played;
407     break;
408   default:
409     t = 0;
410     break;
411   }
412   if(t)
413     strftime(when, sizeof when, "%H:%M", localtime_r(&t, &tm));
414   else
415     when[0] = 0;
416   NW(label);
417   return gtk_label_new(when);
418 }
419
420 /** @brief Format the 'who' column */
421 static GtkWidget *column_who(const struct queuelike attribute((unused)) *ql,
422                              const struct queue_entry *q,
423                              const char attribute((unused)) *data) {
424   D(("column_who"));
425   NW(label);
426   return gtk_label_new(q->submitter ? q->submitter : "");
427 }
428
429 /** @brief Format one of the track name columns */
430 static GtkWidget *column_namepart(const struct queuelike
431                                                attribute((unused)) *ql,
432                                   const struct queue_entry *q,
433                                   const char *data) {
434   D(("column_namepart"));
435   NW(label);
436   return gtk_label_new(truncate_for_display(namepart(q->track, "display", data),
437                                             config->short_display));
438 }
439
440 /** @brief Compute the length field */
441 static const char *text_length(const struct queue_entry *q) {
442   long l;
443   time_t now;
444   char *played = 0, *length = 0;
445
446   /* Work out what to say for the length */
447   l = getlength(q->track);
448   if(l > 0)
449     byte_xasprintf(&length, "%ld:%02ld", l / 60, l % 60);
450   else
451     byte_xasprintf(&length, "?:??");
452   /* For the currently playing track we want to report how much of the track
453    * has been played */
454   if(q == playing_track) {
455     /* log_state() arranges that we re-get the playing data whenever the
456      * pause/resume state changes */
457     if(last_state & DISORDER_TRACK_PAUSED)
458       l = playing_track->sofar;
459     else {
460       time(&now);
461       l = playing_track->sofar + (now - last_playing);
462     }
463     byte_xasprintf(&played, "%ld:%02ld/%s", l / 60, l % 60, length);
464     return played;
465   } else
466     return length;
467 }
468
469 /** @brief Format the length column */
470 static GtkWidget *column_length(const struct queuelike attribute((unused)) *ql,
471                                 const struct queue_entry *q,
472                                 const char attribute((unused)) *data) {
473   D(("column_length"));
474   if(q == playing_track) {
475     assert(!playing_length_label);
476     NW(label);
477     playing_length_label = gtk_label_new(text_length(q));
478     /* Zot playing_length_label when it is destroyed */
479     g_signal_connect(playing_length_label, "destroy",
480                      G_CALLBACK(gtk_widget_destroyed), &playing_length_label);
481     return playing_length_label;
482   } else {
483     NW(label);
484     return gtk_label_new(text_length(q));
485   }
486   
487 }
488
489 /** @brief Apply a new queue contents, transferring the selection from the old value */
490 static void update_queue(struct queuelike *ql, struct queue_entry *newq) {
491   struct queue_entry *q;
492
493   D(("update_queue"));
494   /* Propagate last_click across the change */
495   if(ql->last_click) {
496     for(q = newq; q; q = q->next) {
497       if(!strcmp(q->id, ql->last_click->id)) 
498         break;
499       ql->last_click = q;
500     }
501   }
502   /* Tell every queue entry which queue owns it */
503   for(q = newq; q; q = q->next)
504     q->ql = ql;
505   /* Switch to the new queue */
506   ql->q = newq;
507   /* Clean up any selected items that have fallen off */
508   for(q = ql->q; q; q = q->next)
509     selection_live(ql->selection, q->id);
510   selection_cleanup(ql->selection);
511 }
512
513 /** @brief Wrap up a widget for putting into the queue or title
514  * @param label Label to contain
515  * @param style Pointer to style to use
516  * @param wp Updated with maximum width (or NULL)
517  * @return New widget
518  */
519 static GtkWidget *wrap_queue_cell(GtkWidget *label,
520                                   GtkStyle *style,
521                                   int *wp) {
522   GtkRequisition req;
523   GtkWidget *bg;
524
525   D(("wrap_queue_cell"));
526   /* Padding should be in the label so there are no gaps in the
527    * background */
528   gtk_misc_set_padding(GTK_MISC(label), HCELLPADDING, VCELLPADDING);
529   /* Event box is just to hold a background color */
530   NW(event_box);
531   bg = gtk_event_box_new();
532   gtk_container_add(GTK_CONTAINER(bg), label);
533   if(wp) {
534     /* Update maximum width */
535     gtk_widget_size_request(label, &req);
536     if(req.width > *wp) *wp = req.width;
537   }
538   /* Set colors */
539   gtk_widget_set_style(bg, style);
540   gtk_widget_set_style(label, style);
541   return bg;
542 }
543
544 /** @brief Create the wrapped widget for a cell in the queue display */
545 static GtkWidget *get_queue_cell(struct queuelike *ql,
546                                  const struct queue_entry *q,
547                                  int row,
548                                  int col,
549                                  GtkStyle *style,
550                                  int *wp) {
551   GtkWidget *label;
552   D(("get_queue_cell %d %d", row, col));
553   label = ql->columns[col].widget(ql, q, ql->columns[col].data);
554   gtk_misc_set_alignment(GTK_MISC(label), ql->columns[col].xalign, 0);
555   return wrap_queue_cell(label, style, wp);
556 }
557
558 /** @brief Add a padding cell to the end of a row */
559 static GtkWidget *get_padding_cell(GtkStyle *style) {
560   D(("get_padding_cell"));
561   NW(label);
562   return wrap_queue_cell(gtk_label_new(""), style, 0);
563 }
564
565 /* User button press and menu ---------------------------------------------- */
566
567 /** @brief Update widget states in order to reflect the selection status */
568 static void set_widget_states(struct queuelike *ql) {
569   struct queue_entry *q;
570   int row, col;
571
572   for(q = ql->q, row = 0; q; q = q->next, ++row) {
573     for(col = 0; col < ql->ncolumns + 1; ++col)
574       gtk_widget_set_state(ql->cells[row * (ql->ncolumns + 1) + col],
575                            selection_selected(ql->selection, q->id) ?
576                            GTK_STATE_SELECTED : GTK_STATE_NORMAL);
577   }
578   /* Might need to change sensitivity of 'Properties' in main menu */
579   menu_update(-1);
580 }
581
582 /** @brief Ordering function for queue entries */
583 static int queue_before(const struct queue_entry *a,
584                         const struct queue_entry *b) {
585   while(a && a != b)
586     a = a->next;
587   return !!a;
588 }
589
590 /** @brief A button was pressed and released */
591 static gboolean queuelike_button_released(GtkWidget attribute((unused)) *widget,
592                                           GdkEventButton *event,
593                                           gpointer user_data) {
594   struct queue_entry *q = user_data, *qq;
595   struct queuelike *ql = q->ql;
596   struct menuiteminfo *mii;
597   int n;
598   
599   /* Might be a release left over from a drag */
600   if(ql->swallow_release) {
601     ql->swallow_release = 0;
602     return FALSE;                       /* propagate */
603   }
604
605   if(event->type == GDK_BUTTON_PRESS
606      && event->button == 3) {
607     /* Right button click.
608      * If the current item is not selected then switch the selection to just
609      * this item */
610     if(q && !selection_selected(ql->selection, q->id)) {
611       selection_empty(ql->selection);
612       selection_set(ql->selection, q->id, 1);
613       ql->last_click = q;
614       set_widget_states(ql);
615     }
616     /* Set the sensitivity of each menu item and (re-)establish the signal
617      * handlers */
618     for(n = 0; ql->menuitems[n].name; ++n) {
619       if(ql->menuitems[n].handlerid)
620         g_signal_handler_disconnect(ql->menuitems[n].w,
621                                     ql->menuitems[n].handlerid);
622       gtk_widget_set_sensitive(ql->menuitems[n].w,
623                                ql->menuitems[n].sensitive(ql,
624                                                           &ql->menuitems[n],
625                                                           q));
626       mii = xmalloc(sizeof *mii);
627       mii->ql = ql;
628       mii->q = q;
629       ql->menuitems[n].handlerid = g_signal_connect
630         (ql->menuitems[n].w, "activate",
631          G_CALLBACK(ql->menuitems[n].activate), mii);
632     }
633     /* Update the menu according to context */
634     gtk_widget_show_all(ql->menu);
635     gtk_menu_popup(GTK_MENU(ql->menu), 0, 0, 0, 0,
636                    event->button, event->time);
637     return TRUE;                        /* hide the click from other widgets */
638   }
639   if(event->type == GDK_BUTTON_RELEASE
640      && event->button == 1) {
641     /* no modifiers: select this, unselect everything else, set last click
642      * +ctrl: flip selection of this, set last click
643      * +shift: select from last click to here, don't set last click
644      * +ctrl+shift: select from last click to here, set last click
645      */
646     switch(event->state & (GDK_SHIFT_MASK|GDK_CONTROL_MASK)) {
647     case 0:
648       selection_empty(ql->selection);
649       selection_set(ql->selection, q->id, 1);
650       ql->last_click = q;
651       break;
652     case GDK_CONTROL_MASK:
653       selection_flip(ql->selection, q->id);
654       ql->last_click = q;
655       break;
656     case GDK_SHIFT_MASK:
657     case GDK_SHIFT_MASK|GDK_CONTROL_MASK:
658       if(ql->last_click) {
659         if(!(event->state & GDK_CONTROL_MASK))
660           selection_empty(ql->selection);
661         selection_set(ql->selection, q->id, 1);
662         qq = q;
663         if(queue_before(ql->last_click, q))
664           while(qq != ql->last_click) {
665             qq = qq->prev;
666             selection_set(ql->selection, qq->id, 1);
667           }
668         else
669           while(qq != ql->last_click) {
670             qq = qq->next;
671             selection_set(ql->selection, qq->id, 1);
672           }
673         if(event->state & GDK_CONTROL_MASK)
674           ql->last_click = q;
675       }
676       break;
677     }
678     set_widget_states(ql);
679     gtk_widget_queue_draw(ql->mainlayout);
680   }
681   return FALSE;                         /* propagate */
682 }
683
684 /** @brief A button was pressed or released on the mainlayout
685  *
686  * For debugging only at the moment. */
687 static gboolean mainlayout_button(GtkWidget attribute((unused)) *widget,
688                                   GdkEventButton attribute((unused)) *event,
689                                   gpointer attribute((unused)) user_data) {
690   return FALSE;                         /* propagate */
691 }
692
693 /** @brief Select all entries in a queue */
694 void queue_select_all(struct queuelike *ql) {
695   struct queue_entry *qq;
696
697   for(qq = ql->q; qq; qq = qq->next)
698     selection_set(ql->selection, qq->id, 1);
699   ql->last_click = 0;
700   set_widget_states(ql);
701 }
702
703 /** @brief Deselect all entries in a queue */
704 void queue_select_none(struct queuelike *ql) {
705   struct queue_entry *qq;
706
707   for(qq = ql->q; qq; qq = qq->next)
708     selection_set(ql->selection, qq->id, 0);
709   ql->last_click = 0;
710   set_widget_states(ql);
711 }
712
713 /** @brief Pop up properties for selected tracks */
714 void queue_properties(struct queuelike *ql) {
715   struct vector v;
716   const struct queue_entry *qq;
717
718   vector_init(&v);
719   for(qq = ql->q; qq; qq = qq->next)
720     if(selection_selected(ql->selection, qq->id))
721       vector_append(&v, (char *)qq->track);
722   if(v.nvec)
723     properties(v.nvec, (const char **)v.vec);
724 }
725
726 /* Drag and drop rearrangement --------------------------------------------- */
727
728 /** @brief Return nonzero if @p is a draggable row
729  *
730  * Only tracks in the main queue are draggable (and the currently playing track
731  * is not draggable).
732  */
733 static int draggable_row(const struct queue_entry *q) {
734   return q->ql == &ql_queue && q != playing_track;
735 }
736
737 /** @brief Called when a drag begings */
738 static void queue_drag_begin(GtkWidget attribute((unused)) *widget, 
739                              GdkDragContext attribute((unused)) *dc,
740                              gpointer data) {
741   struct queue_entry *q = data;
742   struct queuelike *ql = q->ql;
743
744   /* Make sure the playing track is not selected, since it cannot be dragged */
745   if(playing_track)
746     selection_set(ql->selection, playing_track->id, 0);
747   /* If the dragged item is not in the selection then change the selection to
748    * just that */
749   if(!selection_selected(ql->selection, q->id)) {
750     selection_empty(ql->selection);
751     selection_set(ql->selection, q->id, 1);
752     set_widget_states(ql);
753   }
754   /* Ignore the eventual button release */
755   ql->swallow_release = 1;
756   /* Create dropzones */
757   add_drag_targets(ql);
758 }
759
760 /** @brief Convert @p id back into a queue entry and a screen row number */
761 static struct queue_entry *findentry(struct queuelike *ql,
762                                      const char *id,
763                                      int *rowp) {
764   int row;
765   struct queue_entry *q;
766
767   if(id) {
768     for(q = ql->q, row = 0; q && strcmp(q->id, id); q = q->next, ++row)
769       ;
770   } else {
771     q = 0;
772     row = playing_track ? 0 : -1;
773   }
774   if(rowp) *rowp = row;
775   return q;
776 }
777
778 static void move_completed(void attribute((unused)) *v,
779                            const char *error) {
780   if(error)
781     popup_protocol_error(0, error);
782 }
783
784 /** @brief Called when data is dropped */
785 static gboolean queue_drag_drop(GtkWidget attribute((unused)) *widget,
786                                 GdkDragContext *drag_context,
787                                 gint attribute((unused)) x,
788                                 gint attribute((unused)) y,
789                                 guint when,
790                                 gpointer user_data) {
791   struct queuelike *ql = &ql_queue;
792   const char *id = user_data;
793   struct vector vec;
794   struct queue_entry *q;
795
796   if(!id || (playing_track && !strcmp(id, playing_track->id)))
797     id = "";
798   vector_init(&vec);
799   for(q = ql->q; q; q = q->next)
800     if(q != playing_track && selection_selected(ql->selection, q->id))
801       vector_append(&vec, (char *)q->id);
802   disorder_eclient_moveafter(client, id, vec.nvec, (const char **)vec.vec,
803                              move_completed, 0/*v*/);
804   gtk_drag_finish(drag_context, TRUE, TRUE, when);
805   /* Destroy dropzones */
806   remove_drag_targets(ql);
807   return TRUE;
808 }
809
810 /** @brief Called when we enter, or move within, a drop zone */
811 static gboolean queue_drag_motion(GtkWidget attribute((unused)) *widget,
812                                   GdkDragContext *drag_context,
813                                   gint attribute((unused)) x,
814                                   gint attribute((unused)) y,
815                                   guint when,
816                                   gpointer user_data) {
817   struct queuelike *ql = &ql_queue;
818   const char *id = user_data;
819   int row;
820   struct queue_entry *q = findentry(ql, id, &row);
821
822   if(!id || q) {
823     if(!ql->dragmark) {
824       NW(event_box);
825       ql->dragmark = gtk_event_box_new();
826       g_signal_connect(ql->dragmark, "destroy",
827                        G_CALLBACK(gtk_widget_destroyed), &ql->dragmark);
828       gtk_widget_set_size_request(ql->dragmark, 10240, row ? 4 : 2);
829       gtk_widget_set_style(ql->dragmark, drag_style);
830       gtk_layout_put(GTK_LAYOUT(ql->mainlayout), ql->dragmark, 0, 
831                      (row + 1) * ql->mainrowheight - !!row);
832     } else
833       gtk_layout_move(GTK_LAYOUT(ql->mainlayout), ql->dragmark, 0, 
834                       (row + 1) * ql->mainrowheight - !!row);
835     gtk_widget_show(ql->dragmark);
836     gdk_drag_status(drag_context, GDK_ACTION_MOVE, when);
837     return TRUE;
838   } else
839     /* ID has gone AWOL */
840     return FALSE;
841 }                              
842
843 /** @brief Called when we leave a drop zone */
844 static void queue_drag_leave(GtkWidget attribute((unused)) *widget,
845                              GdkDragContext attribute((unused)) *drag_context,
846                              guint attribute((unused)) when,
847                              gpointer attribute((unused)) user_data) {
848   struct queuelike *ql = &ql_queue;
849   
850   if(ql->dragmark)
851     gtk_widget_hide(ql->dragmark);
852 }
853
854 /** @brief Add a drag target
855  * @param ql The queue-like (in practice this is always @ref ql_queue)
856  * @param y The Y coordinate to place the drag target
857  * @param id Track to insert moved tracks after, or NULL
858  *
859  * Adds a drop zone at Y coordinate @p y, which is assumed to lie between two
860  * tracks (or before the start of the queue or after the end of the queue).  If
861  * tracks are dragged into this dropzone then they will be moved @em after
862  * track @p id, or to the start of the queue if @p id is NULL.
863  *
864  * We remember all the dropzones in @c ql->dropzones so they can be destroyed
865  * later.
866  */
867 static void add_drag_target(struct queuelike *ql, int y,
868                             const char *id) {
869   GtkWidget *eventbox;
870
871   NW(event_box);
872   eventbox = gtk_event_box_new();
873   /* Make the target zone invisible */
874   gtk_event_box_set_visible_window(GTK_EVENT_BOX(eventbox), FALSE);
875   /* Make it large enough */
876   gtk_widget_set_size_request(eventbox, 10240, 
877                               y ? ql->mainrowheight : ql->mainrowheight / 2);
878   /* Position it */
879   gtk_layout_put(GTK_LAYOUT(ql->mainlayout), eventbox, 0,
880                  y ? y - ql->mainrowheight / 2 : 0);
881   /* Mark it as capable of receiving drops */
882   gtk_drag_dest_set(eventbox,
883                     0,
884                     dragtargets, NDRAGTARGETS, GDK_ACTION_MOVE);
885   g_signal_connect(eventbox, "drag-drop",
886                    G_CALLBACK(queue_drag_drop), (char *)id);
887   /* Monitor drag motion */
888   g_signal_connect(eventbox, "drag-motion",
889                    G_CALLBACK(queue_drag_motion), (char *)id);
890   g_signal_connect(eventbox, "drag-leave",
891                    G_CALLBACK(queue_drag_leave), (char *)id);
892   /* The widget needs to be shown to receive drags */
893   gtk_widget_show(eventbox);
894   /* Remember the drag targets */
895   ql->dropzones[ql->ndropzones] = eventbox;
896   g_signal_connect(eventbox, "destroy",
897                    G_CALLBACK(gtk_widget_destroyed),
898                    &ql->dropzones[ql->ndropzones]);
899   ++ql->ndropzones;
900 }
901
902 /** @brief Create dropzones for dragging into */
903 static void add_drag_targets(struct queuelike *ql) {
904   int y;
905   struct queue_entry *q;
906
907   /* Create an array to store the widgets */
908   ql->dropzones = xcalloc(ql->nrows, sizeof (GtkWidget *));
909   ql->ndropzones = 0;
910   y = 0;
911   /* Add a drag target before the first row provided it's not the playing
912    * track */
913   if(!playing_track || ql->q != playing_track)
914     add_drag_target(ql, 0, 0);
915   /* Put a drag target at the bottom of every row */
916   for(q = ql->q; q; q = q->next) {
917     y += ql->mainrowheight;
918     add_drag_target(ql, y, q->id);
919   }
920 }
921
922 /** @brief Remove the dropzones */
923 static void remove_drag_targets(struct queuelike *ql) {
924   int n;
925
926   for(n = 0; n < ql->ndropzones; ++n) {
927     if(ql->dropzones[n]) {
928       DW(event_box);
929       gtk_widget_destroy(ql->dropzones[n]);
930     }
931     assert(ql->dropzones[n] == 0);
932   }
933 }
934
935 /* Layout ------------------------------------------------------------------ */
936
937 /** @brief Redisplay a queue */
938 static void redisplay_queue(struct queuelike *ql) {
939   struct queue_entry *q;
940   int row, col;
941   GList *c, *children;
942   GtkStyle *style;
943   GtkRequisition req;  
944   GtkWidget *w;
945   int maxwidths[MAXCOLUMNS], x, y, titlerowheight;
946   int totalwidth = 10240;               /* TODO: can we be less blunt */
947
948   D(("redisplay_queue"));
949   /* Eliminate all the existing widgets and start from scratch */
950   for(c = children = gtk_container_get_children(GTK_CONTAINER(ql->mainlayout));
951       c;
952       c = c->next) {
953     /* Destroy both the label and the eventbox */
954     if(GTK_BIN(c->data)->child) {
955       DW(label);
956       gtk_widget_destroy(GTK_BIN(c->data)->child);
957     }
958     DW(event_box);
959     gtk_widget_destroy(GTK_WIDGET(c->data));
960   }
961   g_list_free(children);
962   /* Adjust the row count */
963   for(q = ql->q, ql->nrows = 0; q; q = q->next)
964     ++ql->nrows;
965   /* We need to create all the widgets before we can position them */
966   ql->cells = xcalloc(ql->nrows * (ql->ncolumns + 1), sizeof *ql->cells);
967   /* Minimum width is given by the column headings */
968   for(col = 0; col < ql->ncolumns; ++col) {
969     /* Reset size so we don't inherit last iteration's maximum size */
970     gtk_widget_set_size_request(GTK_BIN(ql->titlecells[col])->child, -1, -1);
971     gtk_widget_size_request(GTK_BIN(ql->titlecells[col])->child, &req);
972     maxwidths[col] = req.width;
973   }
974   /* Find the vertical size of the title bar */
975   gtk_widget_size_request(GTK_BIN(ql->titlecells[0])->child, &req);
976   titlerowheight = req.height;
977   y = 0;
978   if(ql->nrows) {
979     /* Construct the widgets */
980     for(q = ql->q, row = 0; q; q = q->next, ++row) {
981       /* Figure out the widget name for this row */
982       if(q == playing_track) style = active_style;
983       else style = row % 2 ? even_style : odd_style;
984       /* Make the widget for each column */
985       for(col = 0; col <= ql->ncolumns; ++col) {
986         /* Create and store the widget */
987         if(col < ql->ncolumns)
988           w = get_queue_cell(ql, q, row, col, style, &maxwidths[col]);
989         else
990           w = get_padding_cell(style);
991         ql->cells[row * (ql->ncolumns + 1) + col] = w;
992         /* Maybe mark it draggable */
993         if(draggable_row(q)) {
994           gtk_drag_source_set(w, GDK_BUTTON1_MASK,
995                               dragtargets, NDRAGTARGETS, GDK_ACTION_MOVE);
996           g_signal_connect(w, "drag-begin", G_CALLBACK(queue_drag_begin), q);
997         }
998         /* Catch button presses */
999         g_signal_connect(w, "button-release-event",
1000                          G_CALLBACK(queuelike_button_released), q);
1001         g_signal_connect(w, "button-press-event",
1002                          G_CALLBACK(queuelike_button_released), q);
1003       }
1004     }
1005     /* ...and of each row in the main layout */
1006     gtk_widget_size_request(GTK_BIN(ql->cells[0])->child, &req);
1007     ql->mainrowheight = req.height;
1008     /* Now we know the maximum width of each column we can set the size of
1009      * everything and position it */
1010     for(row = 0, q = ql->q; row < ql->nrows; ++row, q = q->next) {
1011       x = 0;
1012       for(col = 0; col < ql->ncolumns; ++col) {
1013         w = ql->cells[row * (ql->ncolumns + 1) + col];
1014         gtk_widget_set_size_request(GTK_BIN(w)->child,
1015                                     maxwidths[col], -1);
1016         gtk_layout_put(GTK_LAYOUT(ql->mainlayout), w, x, y);
1017         x += maxwidths[col];
1018       }
1019       w = ql->cells[row * (ql->ncolumns + 1) + col];
1020       gtk_widget_set_size_request(GTK_BIN(w)->child,
1021                                   totalwidth - x, -1);
1022       gtk_layout_put(GTK_LAYOUT(ql->mainlayout), w, x, y);
1023       y += ql->mainrowheight;
1024     }
1025   }
1026   /* Titles */
1027   x = 0;
1028   for(col = 0; col < ql->ncolumns; ++col) {
1029     gtk_widget_set_size_request(GTK_BIN(ql->titlecells[col])->child,
1030                                 maxwidths[col], -1);
1031     gtk_layout_move(GTK_LAYOUT(ql->titlelayout), ql->titlecells[col], x, 0);
1032     x += maxwidths[col];
1033   }
1034   gtk_widget_set_size_request(GTK_BIN(ql->titlecells[col])->child,
1035                               totalwidth - x, -1);
1036   gtk_layout_move(GTK_LAYOUT(ql->titlelayout), ql->titlecells[col], x, 0);
1037   /* Set the states */
1038   set_widget_states(ql);
1039   /* Make sure it's all visible */
1040   gtk_widget_show_all(ql->mainlayout);
1041   gtk_widget_show_all(ql->titlelayout);
1042   /* Layouts might shrink to arrange for the area they shrink out of to be
1043    * redrawn */
1044   gtk_widget_queue_draw(ql->mainlayout);
1045   gtk_widget_queue_draw(ql->titlelayout);
1046   /* Adjust the size of the layout */
1047   gtk_layout_set_size(GTK_LAYOUT(ql->mainlayout), x, y);
1048   gtk_layout_set_size(GTK_LAYOUT(ql->titlelayout), x, titlerowheight);
1049   gtk_widget_set_size_request(ql->titlelayout, -1, titlerowheight);
1050 }
1051
1052 /** @brief Called with new queue/recent contents */ 
1053 static void queuelike_completed(void *v,
1054                                 const char *error,
1055                                 struct queue_entry *q) {
1056   if(error)
1057     popup_protocol_error(0, error);
1058   else {
1059     struct queuelike *const ql = v;
1060     
1061     D(("queuelike_complete"));
1062     /* Install the new queue */
1063     update_queue(ql, ql->fixup ? ql->fixup(q) : q);
1064     /* Update the display */
1065     redisplay_queue(ql);
1066     if(ql->notify)
1067       ql->notify();
1068     /* Update sensitivity of main menu items */
1069     menu_update(-1);
1070   }
1071 }
1072
1073 /** @brief Called with a new currently playing track */
1074 static void playing_completed(void attribute((unused)) *v,
1075                               const char *error,
1076                               struct queue_entry *q) {
1077   if(error)
1078     popup_protocol_error(0, error);
1079   else {
1080     D(("playing_completed"));
1081     playing_track = q;
1082     /* Record when we got the playing track data so we know how old the 'sofar'
1083      * field is */
1084     time(&last_playing);
1085     queuelike_completed(&ql_queue, 0, actual_queue);
1086   }
1087 }
1088
1089 /** @brief Called when the queue is scrolled */
1090 static void queue_scrolled(GtkAdjustment *adjustment,
1091                            gpointer user_data) {
1092   GtkAdjustment *titleadj = user_data;
1093
1094   D(("queue_scrolled"));
1095   gtk_adjustment_set_value(titleadj, adjustment->value);
1096 }
1097
1098 /** @brief Create a queuelike thing (queue/recent) */
1099 static GtkWidget *queuelike(struct queuelike *ql,
1100                             struct queue_entry *(*fixup)(struct queue_entry *),
1101                             void (*notify)(void),
1102                             struct queue_menuitem *menuitems,
1103                             const struct column *columns,
1104                             int ncolumns) {
1105   GtkWidget *vbox, *mainscroll, *titlescroll, *label;
1106   GtkAdjustment *mainadj, *titleadj;
1107   int col, n;
1108
1109   D(("queuelike"));
1110   ql->fixup = fixup;
1111   ql->notify = notify;
1112   ql->menuitems = menuitems;
1113   ql->mainrowheight = !0;                /* else division by 0 */
1114   ql->selection = selection_new();
1115   ql->columns = columns;
1116   ql->ncolumns = ncolumns;
1117   /* Create the layouts */
1118   NW(layout);
1119   ql->mainlayout = gtk_layout_new(0, 0);
1120   gtk_widget_set_style(ql->mainlayout, layout_style);
1121   NW(layout);
1122   ql->titlelayout = gtk_layout_new(0, 0);
1123   gtk_widget_set_style(ql->titlelayout, title_style);
1124   /* Scroll the layouts */
1125   ql->mainscroll = mainscroll = scroll_widget(ql->mainlayout);
1126   titlescroll = scroll_widget(ql->titlelayout);
1127   gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(titlescroll),
1128                                  GTK_POLICY_NEVER, GTK_POLICY_NEVER);
1129   mainadj = gtk_scrolled_window_get_hadjustment(GTK_SCROLLED_WINDOW(mainscroll));
1130   titleadj = gtk_scrolled_window_get_hadjustment(GTK_SCROLLED_WINDOW(titlescroll));
1131   g_signal_connect(mainadj, "changed", G_CALLBACK(queue_scrolled), titleadj);
1132   g_signal_connect(mainadj, "value-changed", G_CALLBACK(queue_scrolled), titleadj);
1133   /* Fill the titles and put them anywhere */
1134   for(col = 0; col < ql->ncolumns; ++col) {
1135     NW(label);
1136     label = gtk_label_new(ql->columns[col].name);
1137     gtk_misc_set_alignment(GTK_MISC(label), ql->columns[col].xalign, 0);
1138     ql->titlecells[col] = wrap_queue_cell(label, title_style, 0);
1139     gtk_layout_put(GTK_LAYOUT(ql->titlelayout), ql->titlecells[col], 0, 0);
1140   }
1141   ql->titlecells[col] = get_padding_cell(title_style);
1142   gtk_layout_put(GTK_LAYOUT(ql->titlelayout), ql->titlecells[col], 0, 0);
1143   /* Pack the lot together in a vbox */
1144   NW(vbox);
1145   vbox = gtk_vbox_new(0, 0);
1146   gtk_box_pack_start(GTK_BOX(vbox), titlescroll, 0, 0, 0);
1147   gtk_box_pack_start(GTK_BOX(vbox), mainscroll, 1, 1, 0);
1148   /* Create the popup menu */
1149   NW(menu);
1150   ql->menu = gtk_menu_new();
1151   g_signal_connect(ql->menu, "destroy",
1152                    G_CALLBACK(gtk_widget_destroyed), &ql->menu);
1153   for(n = 0; menuitems[n].name; ++n) {
1154     NW(menu_item);
1155     menuitems[n].w = gtk_menu_item_new_with_label(menuitems[n].name);
1156     gtk_menu_attach(GTK_MENU(ql->menu), menuitems[n].w, 0, 1, n, n + 1);
1157   }
1158   g_object_set_data(G_OBJECT(vbox), "type", (void *)&tabtype_queue);
1159   g_object_set_data(G_OBJECT(vbox), "queue", ql);
1160   /* Catch button presses */
1161   g_signal_connect(ql->mainlayout, "button-release-event",
1162                    G_CALLBACK(mainlayout_button), ql);
1163 #if 0
1164   g_signal_connect(ql->mainlayout, "button-press-event",
1165                    G_CALLBACK(mainlayout_button), ql);
1166 #endif
1167   set_tool_colors(ql->menu);
1168   return vbox;
1169 }
1170
1171 /* Popup menu items -------------------------------------------------------- */
1172
1173 /** @brief Count the number of items selected */
1174 static int queue_count_selected(const struct queuelike *ql) {
1175   return hash_count(ql->selection);
1176 }
1177
1178 /** @brief Count the number of items selected */
1179 static int queue_count_entries(const struct queuelike *ql) {
1180   int nitems = 0;
1181   const struct queue_entry *q;
1182
1183   for(q = ql->q; q; q = q->next)
1184     ++nitems;
1185   return nitems;
1186 }
1187
1188 /** @brief Count the number of items selected, excluding the playing track if
1189  * there is one */
1190 static int count_selected_nonplaying(const struct queuelike *ql) {
1191   int nselected = queue_count_selected(ql);
1192
1193   if(ql->q == playing_track && selection_selected(ql->selection, ql->q->id))
1194     --nselected;
1195   return nselected;
1196 }
1197
1198 /** @brief Determine whether the scratch option should be sensitive */
1199 static int scratch_sensitive(struct queuelike attribute((unused)) *ql,
1200                              struct queue_menuitem attribute((unused)) *m,
1201                              struct queue_entry attribute((unused)) *q) {
1202   /* We can scratch if the playing track is selected */
1203   return (playing_track
1204           && (disorder_eclient_state(client) & DISORDER_CONNECTED)
1205           && selection_selected(ql->selection, playing_track->id));
1206 }
1207
1208 /** @brief Called when disorder_eclient_scratch completes */
1209 static void scratch_completed(void attribute((unused)) *v,
1210                               const char *error) {
1211   if(error)
1212     popup_protocol_error(0, error);
1213 }
1214
1215 /** @brief Scratch the playing track */
1216 static void scratch_activate(GtkMenuItem attribute((unused)) *menuitem,
1217                              gpointer attribute((unused)) user_data) {
1218   if(playing_track)
1219     disorder_eclient_scratch(client, playing_track->id, scratch_completed, 0);
1220 }
1221
1222 /** @brief Determine whether the remove option should be sensitive */
1223 static int remove_sensitive(struct queuelike *ql,
1224                             struct queue_menuitem attribute((unused)) *m,
1225                             struct queue_entry *q) {
1226   /* We can remove if we're hovering over a particular track or any non-playing
1227    * tracks are selected */
1228   return ((disorder_eclient_state(client) & DISORDER_CONNECTED)
1229           && ((q
1230                && q != playing_track)
1231               || count_selected_nonplaying(ql)));
1232 }
1233
1234 static void remove_completed(void attribute((unused)) *v,
1235                              const char *error) {
1236   if(error)
1237     popup_protocol_error(0, error);
1238 }
1239
1240 /** @brief Remove selected track(s) */
1241 static void remove_activate(GtkMenuItem attribute((unused)) *menuitem,
1242                             gpointer user_data) {
1243   const struct menuiteminfo *mii = user_data;
1244   struct queue_entry *q = mii->q;
1245   struct queuelike *ql = mii->ql;
1246
1247   if(count_selected_nonplaying(mii->ql)) {
1248     /* Remove selected tracks */
1249     for(q = ql->q; q; q = q->next)
1250       if(selection_selected(ql->selection, q->id) && q != playing_track)
1251         disorder_eclient_remove(client, q->id, move_completed, 0);
1252   } else if(q)
1253     /* Remove just the hovered track */
1254     disorder_eclient_remove(client, q->id, remove_completed, 0);
1255 }
1256
1257 /** @brief Determine whether the properties menu option should be sensitive */
1258 static int properties_sensitive(struct queuelike *ql,
1259                                 struct queue_menuitem attribute((unused)) *m,
1260                                 struct queue_entry attribute((unused)) *q) {
1261   /* "Properties" is sensitive if at least something is selected */
1262   return (hash_count(ql->selection) > 0
1263           && (disorder_eclient_state(client) & DISORDER_CONNECTED));
1264 }
1265
1266 /** @brief Pop up properties for the selected tracks */
1267 static void properties_activate(GtkMenuItem attribute((unused)) *menuitem,
1268                                 gpointer user_data) {
1269   const struct menuiteminfo *mii = user_data;
1270   
1271   queue_properties(mii->ql);
1272 }
1273
1274 /** @brief Determine whether the select all menu option should be sensitive */
1275 static int selectall_sensitive(struct queuelike *ql,
1276                                struct queue_menuitem attribute((unused)) *m,
1277                                struct queue_entry attribute((unused)) *q) {
1278   /* Sensitive if there is anything to select */
1279   return !!ql->q;
1280 }
1281
1282 /** @brief Select all tracks */
1283 static void selectall_activate(GtkMenuItem attribute((unused)) *menuitem,
1284                                gpointer user_data) {
1285   const struct menuiteminfo *mii = user_data;
1286   queue_select_all(mii->ql);
1287 }
1288
1289 /** @brief Determine whether the select none menu option should be sensitive */
1290 static int selectnone_sensitive(struct queuelike *ql,
1291                                 struct queue_menuitem attribute((unused)) *m,
1292                                 struct queue_entry attribute((unused)) *q) {
1293   /* Sensitive if there is anything selected */
1294   return hash_count(ql->selection) != 0;
1295 }
1296
1297 /** @brief Select no tracks */
1298 static void selectnone_activate(GtkMenuItem attribute((unused)) *menuitem,
1299                                gpointer user_data) {
1300   const struct menuiteminfo *mii = user_data;
1301   queue_select_none(mii->ql);
1302 }
1303
1304 /** @brief Determine whether the play menu option should be sensitive */
1305 static int play_sensitive(struct queuelike *ql,
1306                           struct queue_menuitem attribute((unused)) *m,
1307                           struct queue_entry attribute((unused)) *q) {
1308   /* "Play" is sensitive if at least something is selected */
1309   return (hash_count(ql->selection) > 0
1310           && (disorder_eclient_state(client) & DISORDER_CONNECTED));
1311 }
1312
1313 /** @brief Play the selected tracks */
1314 static void play_activate(GtkMenuItem attribute((unused)) *menuitem,
1315                           gpointer user_data) {
1316   const struct menuiteminfo *mii = user_data;
1317   struct queue_entry *q = mii->q;
1318   struct queuelike *ql = mii->ql;
1319
1320   if(queue_count_selected(ql)) {
1321     /* Play selected tracks */
1322     for(q = ql->q; q; q = q->next)
1323       if(selection_selected(ql->selection, q->id))
1324         disorder_eclient_play(client, q->track, play_completed, 0);
1325   } else if(q)
1326     /* Nothing is selected, so play the hovered track */
1327     disorder_eclient_play(client, q->track, play_completed, 0);
1328 }
1329
1330 /* The queue --------------------------------------------------------------- */
1331
1332 /** @brief Fix up the queue by sticking the currently playing track on the front */
1333 static struct queue_entry *fixup_queue(struct queue_entry *q) {
1334   D(("fixup_queue"));
1335   actual_queue = q;
1336   if(playing_track) {
1337     if(actual_queue)
1338       actual_queue->prev = playing_track;
1339     playing_track->next = actual_queue;
1340     return playing_track;
1341   } else
1342     return actual_queue;
1343 }
1344
1345 /** @brief Adjust track played label
1346  *
1347  *  Called regularly to adjust the so-far played label (redrawing the whole
1348  * queue once a second makes disobedience occupy >10% of the CPU on my Athlon
1349  * which is ureasonable expensive) */
1350 static gboolean adjust_sofar(gpointer attribute((unused)) data) {
1351   if(playing_length_label && playing_track)
1352     gtk_label_set_text(GTK_LABEL(playing_length_label),
1353                        text_length(playing_track));
1354   return TRUE;
1355 }
1356
1357 /** @brief Popup menu for the queue
1358  *
1359  * Properties first so that finger trouble is less dangerous. */
1360 static struct queue_menuitem queue_menu[] = {
1361   { "Track properties", properties_activate, properties_sensitive, 0, 0 },
1362   { "Select all tracks", selectall_activate, selectall_sensitive, 0, 0 },
1363   { "Deselect all tracks", selectnone_activate, selectnone_sensitive, 0, 0 },
1364   { "Scratch track", scratch_activate, scratch_sensitive, 0, 0 },
1365   { "Remove track from queue", remove_activate, remove_sensitive, 0, 0 },
1366   { 0, 0, 0, 0, 0 }
1367 };
1368
1369 /** @brief Called whenever @ref DISORDER_PLAYING or @ref DISORDER_TRACK_PAUSED changes
1370  *
1371  * We monitor pause/resume as well as whether the track is playing in order to
1372  * keep the time played so far up to date correctly.  See playing_completed().
1373  */
1374 static void playing_changed(const char attribute((unused)) *event,
1375                             void attribute((unused)) *evendata,
1376                             void attribute((unused)) *callbackdata) {
1377   D(("playing_changed"));
1378   gtk_label_set_text(GTK_LABEL(report_label), "updating playing track");
1379   disorder_eclient_playing(client, playing_completed, 0);
1380 }
1381
1382 /** @brief Create the queue widget */
1383 GtkWidget *queue_widget(void) {
1384   D(("queue_widget"));
1385   /* Arrange periodic update of the so-far played field */
1386   g_timeout_add(1000/*ms*/, adjust_sofar, 0);
1387   /* Arrange a callback whenever the playing state changes */ 
1388   event_register("playing-changed", playing_changed, 0);
1389   event_register("pause-changed", playing_changed, 0);
1390   event_register("queue-changed", queue_changed, 0);
1391   /* We pass choose_update() as our notify function since the choose screen
1392    * marks tracks that are playing/in the queue. */
1393   return queuelike(&ql_queue, fixup_queue, choose_update, queue_menu,
1394                    maincolumns, NMAINCOLUMNS);
1395 }
1396
1397 /** @brief Arrange an update of the queue widget
1398  *
1399  * Called when a track is added to the queue, removed from the queue (by user
1400  * cmmand or because it is to be played) or moved within the queue
1401  */
1402 void queue_changed(const char attribute((unused)) *event,
1403                    void attribute((unused)) *eventdata,
1404                    void attribute((unused)) *callbackdata) {
1405   D(("queue_changed"));
1406   gtk_label_set_text(GTK_LABEL(report_label), "updating queue");
1407   disorder_eclient_queue(client, queuelike_completed, &ql_queue);
1408 }
1409
1410 /* Recently played tracks -------------------------------------------------- */
1411
1412 /** @brief Fix up the recently played list
1413  *
1414  * It's in the wrong order!  TODO fix this globally */
1415 static struct queue_entry *fixup_recent(struct queue_entry *q) {
1416   struct queue_entry *qr = 0,  *qn;
1417
1418   D(("fixup_recent"));
1419   while(q) {
1420     qn = q->next;
1421     /* Swap next/prev pointers */
1422     q->next = q->prev;
1423     q->prev = qn;
1424     /* Remember last node for new head */
1425     qr = q;
1426     /* Next node */
1427     q = qn;
1428   }
1429   return qr;
1430 }
1431
1432 /** @brief Pop-up menu for recently played list */
1433 static struct queue_menuitem recent_menu[] = {
1434   { "Track properties", properties_activate, properties_sensitive,0, 0 },
1435   { "Select all tracks", selectall_activate, selectall_sensitive, 0, 0 },
1436   { "Deselect all tracks", selectnone_activate, selectnone_sensitive, 0, 0 },
1437   { 0, 0, 0, 0, 0 }
1438 };
1439
1440 /** @brief Create the recently-played list */
1441 GtkWidget *recent_widget(void) {
1442   D(("recent_widget"));
1443   event_register("recent-changed",
1444                  recent_changed,
1445                  0);
1446   return queuelike(&ql_recent, fixup_recent, 0, recent_menu,
1447                    maincolumns, NMAINCOLUMNS);
1448 }
1449
1450 /** @brief Update the recently played list
1451  *
1452  * Called whenever a track is added to it or removed from it.
1453  */
1454 static void recent_changed(const char attribute((unused)) *event,
1455                            void attribute((unused)) *eventdata,
1456                            void attribute((unused)) *callbackdata) {
1457   D(("recent_changed"));
1458   gtk_label_set_text(GTK_LABEL(report_label), "updating recently played list");
1459   disorder_eclient_recent(client, queuelike_completed, &ql_recent);
1460 }
1461
1462 /* Newly added tracks ------------------------------------------------------ */
1463
1464 /** @brief Pop-up menu for recently played list */
1465 static struct queue_menuitem added_menu[] = {
1466   { "Track properties", properties_activate, properties_sensitive, 0, 0 },
1467   { "Play track", play_activate, play_sensitive, 0, 0 },
1468   { "Select all tracks", selectall_activate, selectall_sensitive, 0, 0 },
1469   { "Deselect all tracks", selectnone_activate, selectnone_sensitive, 0, 0 },
1470   { 0, 0, 0, 0, 0 }
1471 };
1472
1473 /** @brief Create the newly-added list */
1474 GtkWidget *added_widget(void) {
1475   D(("added_widget"));
1476   event_register("added-changed", added_changed, 0);
1477   return queuelike(&ql_added, 0/*fixup*/, 0/*notify*/, added_menu,
1478                    addedcolumns, NADDEDCOLUMNS);
1479 }
1480
1481 /** @brief Called with an updated list of newly-added tracks
1482  *
1483  * This is called with a raw list of track names but the rest of @ref
1484  * disobedience/queue.c requires @ref queue_entry structures with a valid and
1485  * unique @c id field.  This function fakes it.
1486  */
1487 static void new_completed(void *v,
1488                           const char *error,
1489                           int nvec, char **vec) {
1490   if(error)
1491     popup_protocol_error(0, error);
1492   else {
1493     struct queuelist *ql = v;
1494     /* Convert the vector result to a queue linked list */
1495     struct queue_entry *q, *qh, *qlast = 0, **qq = &qh;
1496     int n;
1497     
1498     for(n = 0; n < nvec; ++n) {
1499       q = xmalloc(sizeof *q);
1500       q->prev = qlast;
1501       q->track = vec[n];
1502       q->id = vec[n];
1503       *qq = q;
1504       qq = &q->next;
1505       qlast = q;
1506     }
1507     *qq = 0;
1508     queuelike_completed(ql, 0, qh);
1509   }
1510 }
1511
1512 /** @brief Update the newly-added list */
1513 static void added_changed(const char attribute((unused)) *event,
1514                           void attribute((unused)) *eventdata,
1515                           void attribute((unused)) *callbackdata) {
1516   D(("added_changed"));
1517
1518   gtk_label_set_text(GTK_LABEL(report_label),
1519                      "updating newly added track list");
1520   disorder_eclient_new_tracks(client, new_completed, 0/*all*/, &ql_added);
1521 }
1522
1523 /* Main menu plumbing ------------------------------------------------------ */
1524
1525 static int queue_properties_sensitive(GtkWidget *w) {
1526   return (!!queue_count_selected(g_object_get_data(G_OBJECT(w), "queue"))
1527           && (disorder_eclient_state(client) & DISORDER_CONNECTED));
1528 }
1529
1530 static int queue_selectall_sensitive(GtkWidget *w) {
1531   return !!queue_count_entries(g_object_get_data(G_OBJECT(w), "queue"));
1532 }
1533
1534 static int queue_selectnone_sensitive(GtkWidget *w) {
1535   struct queuelike *const ql = g_object_get_data(G_OBJECT(w), "queue");
1536
1537   return hash_count(ql->selection) != 0;
1538 }
1539
1540 static void queue_properties_activate(GtkWidget *w) {
1541   queue_properties(g_object_get_data(G_OBJECT(w), "queue"));
1542 }
1543
1544 static void queue_selectall_activate(GtkWidget *w) {
1545   queue_select_all(g_object_get_data(G_OBJECT(w), "queue"));
1546 }
1547
1548 static void queue_selectnone_activate(GtkWidget *w) {
1549   queue_select_none(g_object_get_data(G_OBJECT(w), "queue"));
1550 }
1551
1552 static const struct tabtype tabtype_queue = {
1553   queue_properties_sensitive,
1554   queue_selectall_sensitive,
1555   queue_selectnone_sensitive,
1556   queue_properties_activate,
1557   queue_selectall_activate,
1558   queue_selectnone_activate,
1559 };
1560
1561 /* Other entry points ------------------------------------------------------ */
1562
1563 /** @brief Return nonzero if @p track is in the queue */
1564 int queued(const char *track) {
1565   struct queue_entry *q;
1566
1567   D(("queued %s", track));
1568   for(q = ql_queue.q; q; q = q->next)
1569     if(!strcmp(q->track, track))
1570       return 1;
1571   return 0;
1572 }
1573
1574 /*
1575 Local Variables:
1576 c-basic-offset:2
1577 comment-column:40
1578 fill-column:79
1579 indent-tabs-mode:nil
1580 End:
1581 */