chiark / gitweb /
5759915c62412c25569ef978b7f4fe117ebaa6af
[disorder] / disobedience / queue-generic.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-generic.c
21  * @brief Queue widgets
22  *
23  * This file provides contains code shared between all the queue-like
24  * widgets - the queue, the recent list and the added tracks list.
25  *
26  * This code is in the process of being rewritten to use the native list
27  * widget.
28  *
29  * There are three @ref queuelike objects: @ref ql_queue, @ref
30  * ql_recent and @ref ql_added.  Each has an associated queue linked
31  * list and a list store containing the contents.
32  *
33  * When new contents turn up we rearrange the list store accordingly.
34  *
35  * NB that while in the server the playing track is not in the queue, in
36  * Disobedience, the playing does live in @c ql_queue.q, despite its different
37  * status to everything else found in that list.
38  *
39  * To do:
40  * - random play icon sensitivity is wrong (onl) if changed from edit menu
41  * - drag and drop queue rearrangement
42  * - display playing row in a different color?
43  */
44 #include "disobedience.h"
45 #include "queue-generic.h"
46
47 static struct queuelike *const queuelikes[] = {
48   &ql_queue, &ql_recent, &ql_added
49 };
50 #define NQUEUELIKES (sizeof queuelikes / sizeof *queuelikes)
51
52 /* Track detail lookup ----------------------------------------------------- */
53
54 static int namepart_lookups_outstanding;
55 static const struct cache_type cachetype_string = { 3600 };
56 static const struct cache_type cachetype_integer = { 3600 };
57
58 /** @brief Called when a namepart lookup has completed or failed
59  *
60  * When there are no lookups in flight a redraw is provoked.  This might well
61  * provoke further lookups.
62  */
63 static void namepart_completed_or_failed(void) {
64   --namepart_lookups_outstanding;
65   if(!namepart_lookups_outstanding) {
66     /* There are no more lookups outstanding, so we update the display */
67     for(unsigned n = 0; n < NQUEUELIKES; ++n)
68       ql_update_list_store(queuelikes[n]);
69   }
70 }
71
72 /** @brief Called when a namepart lookup has completed */
73 static void namepart_completed(void *v, const char *error, const char *value) {
74   D(("namepart_completed"));
75   if(error) {
76     gtk_label_set_text(GTK_LABEL(report_label), error);
77     value = "?";
78   }
79   const char *key = v;
80   
81   cache_put(&cachetype_string, key, value);
82   namepart_completed_or_failed();
83 }
84
85 /** @brief Called when a length lookup has completed */
86 static void length_completed(void *v, const char *error, long l) {
87   D(("length_completed"));
88   if(error) {
89     gtk_label_set_text(GTK_LABEL(report_label), error);
90     l = -1;
91   }
92   const char *key = v;
93   long *value;
94   
95   D(("namepart_completed"));
96   value = xmalloc(sizeof *value);
97   *value = l;
98   cache_put(&cachetype_integer, key, value);
99   namepart_completed_or_failed();
100 }
101
102 /** @brief Arrange to fill in a namepart cache entry */
103 static void namepart_fill(const char *track,
104                           const char *context,
105                           const char *part,
106                           const char *key) {
107   D(("namepart_fill %s %s %s %s", track, context, part, key));
108   /* We limit the total number of lookups in flight */
109   ++namepart_lookups_outstanding;
110   D(("namepart_lookups_outstanding -> %d\n", namepart_lookups_outstanding));
111   disorder_eclient_namepart(client, namepart_completed,
112                             track, context, part, (void *)key);
113 }
114
115 /** @brief Look up a namepart
116  * @param track Track name
117  * @param context Context
118  * @param part Name part
119  * @param lookup If nonzero, will schedule a lookup for unknown values
120  *
121  * If it is in the cache then just return its value.  If not then look it up
122  * and arrange for the queues to be updated when its value is available. */
123 static const char *namepart(const char *track,
124                             const char *context,
125                             const char *part) {
126   char *key;
127   const char *value;
128
129   D(("namepart %s %s %s", track, context, part));
130   byte_xasprintf(&key, "namepart context=%s part=%s track=%s",
131                  context, part, track);
132   value = cache_get(&cachetype_string, key);
133   if(!value) {
134     D(("deferring..."));
135     namepart_fill(track, context, part, key);
136     value = "?";
137   }
138   return value;
139 }
140
141 /** @brief Called from @ref disobedience/properties.c when we know a name part has changed */
142 void namepart_update(const char *track,
143                      const char *context,
144                      const char *part) {
145   char *key;
146
147   byte_xasprintf(&key, "namepart context=%s part=%s track=%s",
148                  context, part, track);
149   /* Only refetch if it's actually in the cache. */
150   if(cache_get(&cachetype_string, key))
151     namepart_fill(track, context, part, key);
152 }
153
154 /** @brief Look up a track length
155  *
156  * If it is in the cache then just return its value.  If not then look it up
157  * and arrange for the queues to be updated when its value is available. */
158 static long getlength(const char *track) {
159   char *key;
160   const long *value;
161
162   D(("getlength %s", track));
163   byte_xasprintf(&key, "length track=%s", track);
164   value = cache_get(&cachetype_integer, key);
165   if(value)
166     return *value;
167   D(("deferring..."));;
168   ++namepart_lookups_outstanding;
169   D(("namepart_lookups_outstanding -> %d\n", namepart_lookups_outstanding));
170   disorder_eclient_length(client, length_completed, track, key);
171   return -1;
172 }
173
174 /* Column formatting -------------------------------------------------------- */
175
176 /** @brief Format the 'when' column */
177 const char *column_when(const struct queue_entry *q,
178                         const char attribute((unused)) *data) {
179   char when[64];
180   struct tm tm;
181   time_t t;
182
183   D(("column_when"));
184   switch(q->state) {
185   case playing_isscratch:
186   case playing_unplayed:
187   case playing_random:
188     t = q->expected;
189     break;
190   case playing_failed:
191   case playing_no_player:
192   case playing_ok:
193   case playing_scratched:
194   case playing_started:
195   case playing_paused:
196   case playing_quitting:
197     t = q->played;
198     break;
199   default:
200     t = 0;
201     break;
202   }
203   if(t)
204     strftime(when, sizeof when, "%H:%M", localtime_r(&t, &tm));
205   else
206     when[0] = 0;
207   return xstrdup(when);
208 }
209
210 /** @brief Format the 'who' column */
211 const char *column_who(const struct queue_entry *q,
212                        const char attribute((unused)) *data) {
213   D(("column_who"));
214   return q->submitter ? q->submitter : "";
215 }
216
217 /** @brief Format one of the track name columns */
218 const char *column_namepart(const struct queue_entry *q,
219                       const char *data) {
220   D(("column_namepart"));
221   return namepart(q->track, "display", data);
222 }
223
224 /** @brief Format the length column */
225 const char *column_length(const struct queue_entry *q,
226                           const char attribute((unused)) *data) {
227   D(("column_length"));
228   long l;
229   time_t now;
230   char *played = 0, *length = 0;
231
232   /* Work out what to say for the length */
233   l = getlength(q->track);
234   if(l > 0)
235     byte_xasprintf(&length, "%ld:%02ld", l / 60, l % 60);
236   else
237     byte_xasprintf(&length, "?:??");
238   /* For the currently playing track we want to report how much of the track
239    * has been played */
240   if(q == playing_track) {
241     /* log_state() arranges that we re-get the playing data whenever the
242      * pause/resume state changes */
243     if(last_state & DISORDER_TRACK_PAUSED)
244       l = playing_track->sofar;
245     else {
246       time(&now);
247       l = playing_track->sofar + (now - last_playing);
248     }
249     byte_xasprintf(&played, "%ld:%02ld/%s", l / 60, l % 60, length);
250     return played;
251   } else
252     return length;
253 }
254
255 /* Selection processing ---------------------------------------------------- */
256
257 /** @brief Stash the selection of @c ql->view
258  * @param ql Queuelike of interest
259  * @return Hash representing current selection
260  */
261 static hash *save_selection(struct queuelike *ql) {
262   hash *h = hash_new(1);
263   GtkTreeIter iter[1];
264   gtk_tree_model_get_iter_first(GTK_TREE_MODEL(ql->store), iter);
265   for(const struct queue_entry *q = ql->q; q; q = q->next) {
266     if(gtk_tree_selection_iter_is_selected(ql->selection, iter))
267       hash_add(h, q->id, "", HASH_INSERT);
268     gtk_tree_model_iter_next(GTK_TREE_MODEL(ql->store), iter);
269   }
270   return h;
271 }
272
273 /** @brief Called from restore_selection() */
274 static int restore_selection_callback(const char *key,
275                                       void attribute((unused)) *value,
276                                       void *u) {
277   const struct queuelike *const ql = u;
278   GtkTreeIter iter[1];
279   const struct queue_entry *q;
280   
281   gtk_tree_model_get_iter_first(GTK_TREE_MODEL(ql->store), iter);
282   for(q = ql->q; q && strcmp(key, q->id); q = q->next)
283     gtk_tree_model_iter_next(GTK_TREE_MODEL(ql->store), iter);
284   if(q) 
285     gtk_tree_selection_select_iter(ql->selection, iter);
286   /* There might be gaps if things have disappeared */
287   return 0;
288 }
289
290 /** @brief Restore selection of @c ql->view
291  * @param ql Queuelike of interest
292  * @param h Hash representing former selection
293  */
294 static void restore_selection(struct queuelike *ql, hash *h) {
295   hash_foreach(h, restore_selection_callback, ql);
296 }
297
298 /* List store maintenance -------------------------------------------------- */
299
300 /** @brief Update one row of a list store
301  * @param q Queue entry
302  * @param iter Iterator referring to row or NULL to work it out
303  */
304 void ql_update_row(struct queue_entry *q,
305                    GtkTreeIter *iter) { 
306   const struct queuelike *const ql = q->ql; 
307
308   D(("ql_update_row"));
309   /* If no iter was supplied, work it out */
310   GtkTreeIter my_iter[1];
311   if(!iter) {
312     gtk_tree_model_get_iter_first(GTK_TREE_MODEL(ql->store), my_iter);
313     struct queue_entry *qq;
314     for(qq = ql->q; qq && q != qq; qq = qq->next)
315       gtk_tree_model_iter_next(GTK_TREE_MODEL(ql->store), my_iter);
316     if(!qq)
317       return;
318     iter = my_iter;
319   }
320   /* Update all the columns */
321   for(int col = 0; col < ql->ncolumns; ++col)
322     gtk_list_store_set(ql->store, iter,
323                        col, ql->columns[col].value(q,
324                                                    ql->columns[col].data),
325                        -1);
326 }
327
328 /** @brief Update the list store
329  * @param ql Queuelike to update
330  *
331  * Called when new namepart data is available (and initially).  Doesn't change
332  * the rows, just updates the cell values.
333  */
334 void ql_update_list_store(struct queuelike *ql) {
335   D(("ql_update_list_store"));
336   GtkTreeIter iter[1];
337   gtk_tree_model_get_iter_first(GTK_TREE_MODEL(ql->store), iter);
338   for(struct queue_entry *q = ql->q; q; q = q->next) {
339     ql_update_row(q, iter);
340     gtk_tree_model_iter_next(GTK_TREE_MODEL(ql->store), iter);
341   }
342 }
343
344 /** @brief Reset the list store
345  * @param ql Queuelike to reset
346  *
347  * Throws away all rows and starts again.  Used when new queue contents arrives
348  * from the server.
349  */
350 void ql_new_queue(struct queuelike *ql,
351                   struct queue_entry *newq) {
352   D(("ql_new_queue"));
353   hash *h = save_selection(ql);
354   /* Clear out old contents */
355   gtk_list_store_clear(ql->store);
356   /* Put in new rows */
357   ql->q = newq;
358   for(struct queue_entry *q = ql->q; q; q = q->next) {
359     /* Tell every queue entry which queue owns it */
360     q->ql = ql;
361     /* Add a row */
362     GtkTreeIter iter[1];
363     gtk_list_store_append(ql->store, iter);
364     /* Update the row contents */
365     ql_update_row(q, iter);
366   }
367   restore_selection(ql, h);
368   /* Update menu sensitivity */
369   menu_update(-1);
370 }
371
372 /** @brief Initialize a @ref queuelike */
373 GtkWidget *init_queuelike(struct queuelike *ql) {
374   D(("init_queuelike"));
375   /* Create the list store */
376   GType *types = xcalloc(ql->ncolumns, sizeof (GType));
377   for(int n = 0; n < ql->ncolumns; ++n)
378     types[n] = G_TYPE_STRING;
379   ql->store = gtk_list_store_newv(ql->ncolumns, types);
380
381   /* Create the view */
382   ql->view = gtk_tree_view_new_with_model(GTK_TREE_MODEL(ql->store));
383   gtk_tree_view_set_rules_hint(GTK_TREE_VIEW(ql->view), TRUE);
384
385   /* Create cell renderers and label columns */
386   for(int n = 0; n < ql->ncolumns; ++n) {
387     GtkCellRenderer *r = gtk_cell_renderer_text_new();
388     if(ql->columns[n].flags & COL_ELLIPSIZE)
389       g_object_set(r, "ellipsize", PANGO_ELLIPSIZE_END, (char *)0);
390     if(ql->columns[n].flags & COL_RIGHT)
391       g_object_set(r, "xalign", (gfloat)1.0, (char *)0);
392     GtkTreeViewColumn *c = gtk_tree_view_column_new_with_attributes
393       (ql->columns[n].name,
394        r,
395        "text", n,
396        (char *)0);
397     g_object_set(c, "resizable", TRUE, (char *)0);
398     if(ql->columns[n].flags & COL_EXPAND)
399       g_object_set(c, "expand", TRUE, (char *)0);
400     gtk_tree_view_append_column(GTK_TREE_VIEW(ql->view), c);
401   }
402
403   /* The selection should support multiple things being selected */
404   ql->selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(ql->view));
405   gtk_tree_selection_set_mode(ql->selection, GTK_SELECTION_MULTIPLE);
406
407   /* Catch button presses */
408   g_signal_connect(ql->view, "button-press-event",
409                    G_CALLBACK(ql_button_release), ql);
410
411   /* TODO style? */
412   /* TODO drag+drop */
413
414   ql->init();
415
416   GtkWidget *scrolled = scroll_widget(ql->view);
417   g_object_set_data(G_OBJECT(scrolled), "type", (void *)ql_tabtype(ql));
418   return scrolled;
419 }
420
421 /*
422 Local Variables:
423 c-basic-offset:2
424 comment-column:40
425 fill-column:79
426 indent-tabs-mode:nil
427 End:
428 */