chiark / gitweb /
Core Audio support should now include descriptions in error strings.
[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 3 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,
11  * but WITHOUT ANY WARRANTY; without even the implied warranty of
12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13  * GNU 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, see <http://www.gnu.org/licenses/>.
17  */
18 /** @file disobedience/queue.c
19  * @brief Disobedience queue widget
20  */
21 #include "disobedience.h"
22 #include "popup.h"
23 #include "queue-generic.h"
24
25 /** @brief The actual queue */
26 static struct queue_entry *actual_queue;
27 static struct queue_entry *actual_playing_track;
28
29 /** @brief The playing track */
30 struct queue_entry *playing_track;
31
32 /** @brief When we last got the playing track
33  *
34  * Set to 0 if the timings are currently off due to having just unpaused.
35  */
36 time_t last_playing;
37
38 static void queue_completed(void *v,
39                             const char *err,
40                             struct queue_entry *q);
41 static void playing_completed(void *v,
42                               const char *err,
43                               struct queue_entry *q);
44
45 /** @brief Called when either the actual queue or the playing track change */
46 static void queue_playing_changed(void) {
47   /* Check that the playing track isn't in the queue.  There's a race here due
48    * to the fact that we issue the two commands at slightly different times.
49    * If it goes wrong we re-issue and try again, so that we never offer up an
50    * inconsistent state. */
51   if(actual_playing_track) {
52     struct queue_entry *q;
53     for(q = actual_queue; q; q = q->next)
54       if(!strcmp(q->id, actual_playing_track->id))
55         break;
56     if(q) {
57       disorder_eclient_playing(client, playing_completed, 0);
58       disorder_eclient_queue(client, queue_completed, 0);
59       return;
60     }
61   }
62   
63   struct queue_entry *q = xmalloc(sizeof *q);
64   if(actual_playing_track) {
65     *q = *actual_playing_track;
66     q->next = actual_queue;
67     playing_track = q;
68   } else {
69     playing_track = NULL;
70     q = actual_queue;
71   }
72   ql_new_queue(&ql_queue, q);
73   /* Tell anyone who cares */
74   event_raise("queue-list-changed", q);
75   event_raise("playing-track-changed", q);
76 }
77
78 /** @brief Update the queue itself */
79 static void queue_completed(void attribute((unused)) *v,
80                             const char *err,
81                             struct queue_entry *q) {
82   if(err) {
83     popup_protocol_error(0, err);
84     return;
85   }
86   actual_queue = q;
87   queue_playing_changed();
88 }
89
90 /** @brief Update the playing track */
91 static void playing_completed(void attribute((unused)) *v,
92                               const char *err,
93                               struct queue_entry *q) {
94   if(err) {
95     popup_protocol_error(0, err);
96     return;
97   }
98   actual_playing_track = q;
99   queue_playing_changed();
100   time(&last_playing);
101 }
102
103 /** @brief Schedule an update to the queue
104  *
105  * Called whenever a track is added to it or removed from it.
106  */
107 static void queue_changed(const char attribute((unused)) *event,
108                            void  attribute((unused)) *eventdata,
109                            void  attribute((unused)) *callbackdata) {
110   D(("queue_changed"));
111   gtk_label_set_text(GTK_LABEL(report_label), "updating queue");
112   disorder_eclient_queue(client, queue_completed, 0);
113 }
114
115 /** @brief Schedule an update to the playing track
116  *
117  * Called whenever it changes
118  */
119 static void playing_changed(const char attribute((unused)) *event,
120                             void  attribute((unused)) *eventdata,
121                             void  attribute((unused)) *callbackdata) {
122   D(("playing_changed"));
123   gtk_label_set_text(GTK_LABEL(report_label), "updating playing track");
124   /* Setting last_playing=0 means that we don't know what the correct value
125    * is right now, e.g. because things have been deranged by a pause. */
126   last_playing = 0;
127   disorder_eclient_playing(client, playing_completed, 0);
128 }
129
130 /** @brief Called regularly
131  *
132  * Updates the played-so-far field
133  */
134 static gboolean playing_periodic(gpointer attribute((unused)) data) {
135   /* If there's a track playing, update its row */
136   if(playing_track)
137     ql_update_row(playing_track, 0);
138   return TRUE;
139 }
140
141 /** @brief Called at startup */
142 static void queue_init(void) {
143   /* Arrange a callback whenever the playing state changes */ 
144   event_register("playing-changed", playing_changed, 0);
145   /* We reget both playing track and queue at pause/resume so that start times
146    * can be computed correctly */
147   event_register("pause-changed", playing_changed, 0);
148   event_register("pause-changed", queue_changed, 0);
149   /* Reget the queue whenever it changes */
150   event_register("queue-changed", queue_changed, 0);
151   /* ...and once a second anyway */
152   g_timeout_add(1000/*ms*/, playing_periodic, 0);
153 }
154
155 /** @brief Columns for the queue */
156 static const struct queue_column queue_columns[] = {
157   { "When",   column_when,     0,        COL_RIGHT },
158   { "Who",    column_who,      0,        0 },
159   { "Artist", column_namepart, "artist", COL_EXPAND|COL_ELLIPSIZE },
160   { "Album",  column_namepart, "album",  COL_EXPAND|COL_ELLIPSIZE },
161   { "Title",  column_namepart, "title",  COL_EXPAND|COL_ELLIPSIZE },
162   { "Length", column_length,   0,        COL_RIGHT }
163 };
164
165 /** @brief Pop-up menu for queue */
166 static struct menuitem queue_menuitems[] = {
167   { "Track properties", ql_properties_activate, ql_properties_sensitive, 0, 0 },
168   { "Select all tracks", ql_selectall_activate, ql_selectall_sensitive, 0, 0 },
169   { "Deselect all tracks", ql_selectnone_activate, ql_selectnone_sensitive, 0, 0 },
170   { "Scratch playing track", ql_scratch_activate, ql_scratch_sensitive, 0, 0 },
171   { "Remove track from queue", ql_remove_activate, ql_remove_sensitive, 0, 0 },
172   { "Adopt track", ql_adopt_activate, ql_adopt_sensitive, 0, 0 },
173 };
174
175 struct queuelike ql_queue = {
176   .name = "queue",
177   .init = queue_init,
178   .columns = queue_columns,
179   .ncolumns = sizeof queue_columns / sizeof *queue_columns,
180   .menuitems = queue_menuitems,
181   .nmenuitems = sizeof queue_menuitems / sizeof *queue_menuitems
182 };
183
184 /* Drag and drop has to be figured out experimentally, because it is not well
185  * documented.
186  *
187  * First you get a row-inserted.  The path argument points to the destination
188  * row but this will not yet have had its values set.  The source row is still
189  * present.  AFAICT the iter argument points to the same place.
190  *
191  * Then you get a row-deleted.  The path argument identifies the row that was
192  * deleted.  By this stage the row inserted above has acquired its values.
193  *
194  * A complication is that the deletion will move the inserted row.  For
195  * instance, if you do a drag that moves row 1 down to after the track that was
196  * formerly on row 9, in the row-inserted call it will show up as row 10, but
197  * in the row-deleted call, row 1 will have been deleted thus making the
198  * inserted row be row 9.
199  *
200  * So when we see the row-inserted we have no idea what track to move.
201  * Therefore we stash it until we see a row-deleted.
202  */
203
204 /** @brief Target row for drag */
205 static int queue_drag_target = -1;
206
207 static void queue_move_completed(void attribute((unused)) *v,
208                                  const char *err) {
209   if(err) {
210     popup_protocol_error(0, err);
211     return;
212   }
213   /* The log should tell us the queue changed so we do no more here */
214 }
215
216 static void queue_row_deleted(GtkTreeModel *treemodel,
217                               GtkTreePath *path,
218                               gpointer attribute((unused)) user_data) {
219   if(!suppress_actions) {
220 #if 0
221     char *ps = gtk_tree_path_to_string(path);
222     fprintf(stderr, "row-deleted path=%s queue_drag_target=%d\n",
223             ps, queue_drag_target);
224     GtkTreeIter j[1];
225     gboolean jt = gtk_tree_model_get_iter_first(treemodel, j);
226     int row = 0;
227     while(jt) {
228       struct queue_entry *q = ql_iter_to_q(treemodel, j);
229       fprintf(stderr, " %2d %s\n", row++, q ? q->track : "(no q)");
230       jt = gtk_tree_model_iter_next(GTK_TREE_MODEL(ql_queue.store), j);
231     }
232     g_free(ps);
233 #endif
234     if(queue_drag_target < 0) {
235       error(0, "unsuppressed row-deleted with no row-inserted");
236       return;
237     }
238     int drag_source = gtk_tree_path_get_indices(path)[0];
239
240     /* If the drag is downwards (=towards higher row numbers) then the target
241      * will have been moved upwards (=towards lower row numbers) by one row. */
242     if(drag_source < queue_drag_target)
243       --queue_drag_target;
244     
245     /* Find the track to move */
246     GtkTreeIter src[1];
247     gboolean srcv = gtk_tree_model_iter_nth_child(treemodel, src, NULL,
248                                                   queue_drag_target);
249     if(!srcv) {
250       error(0, "cannot get iterator to drag target %d", queue_drag_target);
251       queue_playing_changed();
252       queue_drag_target = -1;
253       return;
254     }
255     struct queue_entry *srcq = ql_iter_to_q(treemodel, src);
256     assert(srcq);
257     //fprintf(stderr, "move %s %s\n", srcq->id, srcq->track);
258     
259     /* Don't allow the currently playing track to be moved.  As above, we put
260      * the queue back into the right order straight away. */
261     if(srcq == playing_track) {
262       //fprintf(stderr, "cannot move currently playing track\n");
263       queue_playing_changed();
264       queue_drag_target = -1;
265       return;
266     }
267
268     /* Find the destination */
269     struct queue_entry *dstq;
270     if(queue_drag_target) {
271       GtkTreeIter dst[1];
272       gboolean dstv = gtk_tree_model_iter_nth_child(treemodel, dst, NULL,
273                                                     queue_drag_target - 1);
274       if(!dstv) {
275         error(0, "cannot get iterator to drag target predecessor %d",
276               queue_drag_target - 1);
277         queue_playing_changed();
278         queue_drag_target = -1;
279         return;
280       }
281       dstq = ql_iter_to_q(treemodel, dst);
282       assert(dstq);
283       if(dstq == playing_track)
284         dstq = 0;
285     } else
286       dstq = 0;
287     /* NB if the user attempts to move a queued track before the currently
288      * playing track we assume they just missed a bit, and put it after. */
289     //fprintf(stderr, " target %s %s\n", dstq ? dstq->id : "(none)", dstq ? dstq->track : "(none)");
290     /* Now we know what is to be moved.  We need to know the preceding queue
291      * entry so we can move it. */
292     disorder_eclient_moveafter(client,
293                                dstq ? dstq->id : "",
294                                1, &srcq->id,
295                                queue_move_completed, NULL);
296     queue_drag_target = -1;
297   }
298 }
299
300 static void queue_row_inserted(GtkTreeModel attribute((unused)) *treemodel,
301                                GtkTreePath *path,
302                                GtkTreeIter attribute((unused)) *iter,
303                                gpointer attribute((unused)) user_data) {
304   if(!suppress_actions) {
305 #if 0
306     char *ps = gtk_tree_path_to_string(path);
307     GtkTreeIter piter[1];
308     gboolean pi = gtk_tree_model_get_iter(treemodel, piter, path);
309     struct queue_entry *pq = pi ? ql_iter_to_q(treemodel, piter) : 0;
310     struct queue_entry *iq = ql_iter_to_q(treemodel, iter);
311
312     fprintf(stderr, "row-inserted path=%s pi=%d pq=%p path=%s iq=%p iter=%s\n",
313             ps,
314             pi,
315             pq,
316             (pi
317              ? (pq ? pq->track : "(pq=0)")
318              : "(pi=FALSE)"),
319             iq,
320             iq ? iq->track : "(iq=0)");
321
322     GtkTreeIter j[1];
323     gboolean jt = gtk_tree_model_get_iter_first(treemodel, j);
324     int row = 0;
325     while(jt) {
326       struct queue_entry *q = ql_iter_to_q(treemodel, j);
327       fprintf(stderr, " %2d %s\n", row++, q ? q->track : "(no q)");
328       jt = gtk_tree_model_iter_next(GTK_TREE_MODEL(ql_queue.store), j);
329     }
330     g_free(ps);
331 #endif
332     queue_drag_target = gtk_tree_path_get_indices(path)[0];
333   }
334 }
335
336 /** @brief Called when a key is pressed in the queue tree view */
337 static gboolean queue_key_press(GtkWidget attribute((unused)) *widget,
338                                 GdkEventKey *event,
339                                 gpointer user_data) {
340   /*fprintf(stderr, "queue_key_press type=%d state=%#x keyval=%#x\n",
341           event->type, event->state, event->keyval);*/
342   switch(event->keyval) {
343   case GDK_BackSpace:
344   case GDK_Delete:
345     if(event->state)
346       break;                            /* Only take unmodified DEL/<-- */
347     ql_remove_activate(0, user_data);
348     return TRUE;                        /* Do not propagate */
349   }
350   return FALSE;                         /* Propagate */
351 }
352
353 GtkWidget *queue_widget(void) {
354   GtkWidget *const w = init_queuelike(&ql_queue);
355
356   /* Enable drag+drop */
357   gtk_tree_view_set_reorderable(GTK_TREE_VIEW(ql_queue.view), TRUE);
358   g_signal_connect(ql_queue.store,
359                    "row-inserted",
360                    G_CALLBACK(queue_row_inserted), &ql_queue);
361   g_signal_connect(ql_queue.store,
362                    "row-deleted",
363                    G_CALLBACK(queue_row_deleted), &ql_queue);
364   /* Catch keypresses */
365   g_signal_connect(ql_queue.view, "key-press-event",
366                    G_CALLBACK(queue_key_press), &ql_queue);
367   return w;
368 }
369
370 /** @brief Return nonzero if @p track is in the queue */
371 int queued(const char *track) {
372   struct queue_entry *q;
373
374   D(("queued %s", track));
375   /* Queue will contain resolved name */
376   track = namepart_resolve(track);
377   for(q = ql_queue.q; q; q = q->next)
378     if(!strcmp(q->track, track))
379       return 1;
380   return 0;
381 }
382
383 /*
384 Local Variables:
385 c-basic-offset:2
386 comment-column:40
387 fill-column:79
388 indent-tabs-mode:nil
389 End:
390 */