X-Git-Url: http://www.chiark.greenend.org.uk/ucgi/~mdw/git/disorder/blobdiff_plain/83fb99f9459f4f6a270d2b75c8dd0137d2f2ccde..812b526d127c6657e571db8b33a58137af6709cd:/disobedience/queue-generic.c diff --git a/disobedience/queue-generic.c b/disobedience/queue-generic.c index c4b5b32..60ff794 100644 --- a/disobedience/queue-generic.c +++ b/disobedience/queue-generic.c @@ -2,23 +2,21 @@ * This file is part of DisOrder * Copyright (C) 2006-2008 Richard Kettlewell * - * This program is free software; you can redistribute it and/or modify + * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 2 of the License, or + * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * - * This program is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * General Public License for more details. - * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * * You should have received a copy of the GNU General Public License - * along with this program; if not, write to the Free Software - * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 - * USA + * along with this program. If not, see . */ /** @file disobedience/queue-generic.c - * @brief Queue widgets + * @brief Disobedience queue widgets * * This file provides contains code shared between all the queue-like * widgets - the queue, the recent list and the added tracks list. @@ -37,10 +35,10 @@ * status to everything else found in that list. * * To do: - * - drag and drop queue rearrangement * - display playing row in a different color? */ #include "disobedience.h" +#include "popup.h" #include "queue-generic.h" static struct queuelike *const queuelikes[] = { @@ -50,124 +48,11 @@ static struct queuelike *const queuelikes[] = { /* Track detail lookup ----------------------------------------------------- */ -static int namepart_lookups_outstanding; -static const struct cache_type cachetype_string = { 3600 }; -static const struct cache_type cachetype_integer = { 3600 }; - -/** @brief Called when a namepart lookup has completed or failed - * - * When there are no lookups in flight a redraw is provoked. This might well - * provoke further lookups. - */ -static void namepart_completed_or_failed(void) { - --namepart_lookups_outstanding; - if(!namepart_lookups_outstanding) { - /* There are no more lookups outstanding, so we update the display */ - for(unsigned n = 0; n < NQUEUELIKES; ++n) - ql_update_list_store(queuelikes[n]); - } -} - -/** @brief Called when a namepart lookup has completed */ -static void namepart_completed(void *v, const char *error, const char *value) { - D(("namepart_completed")); - if(error) { - gtk_label_set_text(GTK_LABEL(report_label), error); - value = "?"; - } - const char *key = v; - - cache_put(&cachetype_string, key, value); - namepart_completed_or_failed(); -} - -/** @brief Called when a length lookup has completed */ -static void length_completed(void *v, const char *error, long l) { - D(("length_completed")); - if(error) { - gtk_label_set_text(GTK_LABEL(report_label), error); - l = -1; - } - const char *key = v; - long *value; - - D(("namepart_completed")); - value = xmalloc(sizeof *value); - *value = l; - cache_put(&cachetype_integer, key, value); - namepart_completed_or_failed(); -} - -/** @brief Arrange to fill in a namepart cache entry */ -static void namepart_fill(const char *track, - const char *context, - const char *part, - const char *key) { - D(("namepart_fill %s %s %s %s", track, context, part, key)); - /* We limit the total number of lookups in flight */ - ++namepart_lookups_outstanding; - D(("namepart_lookups_outstanding -> %d\n", namepart_lookups_outstanding)); - disorder_eclient_namepart(client, namepart_completed, - track, context, part, (void *)key); -} - -/** @brief Look up a namepart - * @param track Track name - * @param context Context - * @param part Name part - * @param lookup If nonzero, will schedule a lookup for unknown values - * - * If it is in the cache then just return its value. If not then look it up - * and arrange for the queues to be updated when its value is available. */ -static const char *namepart(const char *track, - const char *context, - const char *part) { - char *key; - const char *value; - - D(("namepart %s %s %s", track, context, part)); - byte_xasprintf(&key, "namepart context=%s part=%s track=%s", - context, part, track); - value = cache_get(&cachetype_string, key); - if(!value) { - D(("deferring...")); - namepart_fill(track, context, part, key); - value = "?"; - } - return value; -} - -/** @brief Called from @ref disobedience/properties.c when we know a name part has changed */ -void namepart_update(const char *track, - const char *context, - const char *part) { - char *key; - - byte_xasprintf(&key, "namepart context=%s part=%s track=%s", - context, part, track); - /* Only refetch if it's actually in the cache. */ - if(cache_get(&cachetype_string, key)) - namepart_fill(track, context, part, key); -} - -/** @brief Look up a track length - * - * If it is in the cache then just return its value. If not then look it up - * and arrange for the queues to be updated when its value is available. */ -static long getlength(const char *track) { - char *key; - const long *value; - - D(("getlength %s", track)); - byte_xasprintf(&key, "length track=%s", track); - value = cache_get(&cachetype_integer, key); - if(value) - return *value; - D(("deferring..."));; - ++namepart_lookups_outstanding; - D(("namepart_lookups_outstanding -> %d\n", namepart_lookups_outstanding)); - disorder_eclient_length(client, length_completed, track, key); - return -1; +static void queue_lookups_completed(const char attribute((unused)) *event, + void attribute((unused)) *eventdata, + void *callbackdata) { + struct queuelike *ql = callbackdata; + ql_update_list_store(ql); } /* Column formatting -------------------------------------------------------- */ @@ -229,7 +114,7 @@ const char *column_length(const struct queue_entry *q, char *played = 0, *length = 0; /* Work out what to say for the length */ - l = getlength(q->track); + l = namepart_length(q->track); if(l > 0) byte_xasprintf(&length, "%ld:%02ld", l / 60, l % 60); else @@ -242,6 +127,8 @@ const char *column_length(const struct queue_entry *q, if(last_state & DISORDER_TRACK_PAUSED) l = playing_track->sofar; else { + if(!last_playing) + return NULL; time(&now); l = playing_track->sofar + (now - last_playing); } @@ -256,14 +143,14 @@ const char *column_length(const struct queue_entry *q, /** @brief Return the @ref queue_entry corresponding to @p iter * @param model Model that owns @p iter * @param iter Tree iterator - * @return ID string + * @return Pointer to queue entry */ struct queue_entry *ql_iter_to_q(GtkTreeModel *model, GtkTreeIter *iter) { struct queuelike *ql = g_object_get_data(G_OBJECT(model), "ql"); GValue v[1]; memset(v, 0, sizeof v); - gtk_tree_model_get_value(model, iter, ql->ncolumns, v); + gtk_tree_model_get_value(model, iter, ql->ncolumns + QUEUEPOINTER_COLUMN, v); assert(G_VALUE_TYPE(v) == G_TYPE_POINTER); struct queue_entry *const q = g_value_get_pointer(v); g_value_unset(v); @@ -291,13 +178,27 @@ void ql_update_row(struct queue_entry *q, iter = my_iter; } /* Update all the columns */ - for(int col = 0; col < ql->ncolumns; ++col) + for(int col = 0; col < ql->ncolumns; ++col) { + const char *const v = ql->columns[col].value(q, + ql->columns[col].data); + if(v) + gtk_list_store_set(ql->store, iter, + col, v, + -1); + } + gtk_list_store_set(ql->store, iter, + ql->ncolumns + QUEUEPOINTER_COLUMN, q, + -1); + if(q == playing_track) + gtk_list_store_set(ql->store, iter, + ql->ncolumns + BACKGROUND_COLUMN, BG_PLAYING, + ql->ncolumns + FOREGROUND_COLUMN, FG_PLAYING, + -1); + else gtk_list_store_set(ql->store, iter, - col, ql->columns[col].value(q, - ql->columns[col].data), + ql->ncolumns + BACKGROUND_COLUMN, (char *)0, + ql->ncolumns + FOREGROUND_COLUMN, (char *)0, -1); - /* The hidden extra column is the queue entry */ - gtk_list_store_set(ql->store, iter, ql->ncolumns, q, -1); } /** @brief Update the list store @@ -400,7 +301,9 @@ void ql_new_queue(struct queuelike *ql, const struct newqueue_data *nqd = hash_find(h, q->id); if(nqd->new) { /* Tell this row that it belongs to the new version of the queue */ - gtk_list_store_set(ql->store, iter, ql->ncolumns, nqd->new, -1); + gtk_list_store_set(ql->store, iter, + ql->ncolumns + QUEUEPOINTER_COLUMN, nqd->new, + -1); it = gtk_tree_model_iter_next(GTK_TREE_MODEL(ql->store), iter); ++kept; } else { @@ -421,7 +324,6 @@ void ql_new_queue(struct queuelike *ql, for(struct queue_entry *q = newq; q; q = q->next) { const struct newqueue_data *nqd = hash_find(h, q->id); if(!nqd->old) { - GtkTreeIter iter[1]; if(after) { /* Try to insert at the right sort of place */ GtkTreeIter where[1]; @@ -435,7 +337,9 @@ void ql_new_queue(struct queuelike *ql, gtk_list_store_append(ql->store, iter); } else gtk_list_store_prepend(ql->store, iter); - gtk_list_store_set(ql->store, iter, ql->ncolumns, q, -1); + gtk_list_store_set(ql->store, iter, + ql->ncolumns + QUEUEPOINTER_COLUMN, q, + -1); //fprintf(stderr, " add %s", q->id); ++inserted; } @@ -495,20 +399,133 @@ void ql_new_queue(struct queuelike *ql, ql->q = newq; /* Set the rest of the columns in new rows */ ql_update_list_store(ql); - /* Update menu sensitivity */ - menu_update(-1); --suppress_actions; } +/* Drag and drop has to be figured out experimentally, because it is not well + * documented. + * + * First you get a row-inserted. The path argument points to the destination + * row but this will not yet have had its values set. The source row is still + * present. AFAICT the iter argument points to the same place. + * + * Then you get a row-deleted. The path argument identifies the row that was + * deleted. By this stage the row inserted above has acquired its values. + * + * A complication is that the deletion will move the inserted row. For + * instance, if you do a drag that moves row 1 down to after the track that was + * formerly on row 9, in the row-inserted call it will show up as row 10, but + * in the row-deleted call, row 1 will have been deleted thus making the + * inserted row be row 9. + * + * So when we see the row-inserted we have no idea what track to move. + * Therefore we stash it until we see a row-deleted. + */ + +/** @brief row-inserted callback */ +static void ql_row_inserted(GtkTreeModel attribute((unused)) *treemodel, + GtkTreePath *path, + GtkTreeIter attribute((unused)) *iter, + gpointer user_data) { + struct queuelike *const ql = user_data; + if(!suppress_actions) { +#if 0 + char *ps = gtk_tree_path_to_string(path); + GtkTreeIter piter[1]; + gboolean pi = gtk_tree_model_get_iter(treemodel, piter, path); + struct queue_entry *pq = pi ? ql_iter_to_q(treemodel, piter) : 0; + struct queue_entry *iq = ql_iter_to_q(treemodel, iter); + + fprintf(stderr, "row-inserted %s path=%s pi=%d pq=%p path=%s iq=%p iter=%s\n", + ql->name, + ps, + pi, + pq, + (pi + ? (pq ? pq->track : "(pq=0)") + : "(pi=FALSE)"), + iq, + iq ? iq->track : "(iq=0)"); + + GtkTreeIter j[1]; + gboolean jt = gtk_tree_model_get_iter_first(treemodel, j); + int row = 0; + while(jt) { + struct queue_entry *q = ql_iter_to_q(treemodel, j); + fprintf(stderr, " %2d %s\n", row++, q ? q->track : "(no q)"); + jt = gtk_tree_model_iter_next(GTK_TREE_MODEL(ql->store), j); + } + g_free(ps); +#endif + /* Remember an iterator pointing at the insertion target */ + if(ql->drag_target) + gtk_tree_path_free(ql->drag_target); + ql->drag_target = gtk_tree_path_copy(path); + } +} + +/** @brief row-deleted callback */ +static void ql_row_deleted(GtkTreeModel attribute((unused)) *treemodel, + GtkTreePath *path, + gpointer user_data) { + struct queuelike *const ql = user_data; + + if(!suppress_actions) { +#if 0 + char *ps = gtk_tree_path_to_string(path); + fprintf(stderr, "row-deleted %s path=%s ql->drag_target=%s\n", + ql->name, ps, gtk_tree_path_to_string(ql->drag_target)); + GtkTreeIter j[1]; + gboolean jt = gtk_tree_model_get_iter_first(treemodel, j); + int row = 0; + while(jt) { + struct queue_entry *q = ql_iter_to_q(treemodel, j); + fprintf(stderr, " %2d %s\n", row++, q ? q->track : "(no q)"); + jt = gtk_tree_model_iter_next(GTK_TREE_MODEL(ql->store), j); + } + g_free(ps); +#endif + if(!ql->drag_target) { + error(0, "%s: unsuppressed row-deleted with no row-inserted", + ql->name); + return; + } + + /* Get the source and destination row numbers. */ + int srcrow = gtk_tree_path_get_indices(path)[0]; + int dstrow = gtk_tree_path_get_indices(ql->drag_target)[0]; + //fprintf(stderr, "srcrow=%d dstrow=%d\n", srcrow, dstrow); + + /* Note that the source row is computed AFTER the destination has been + * inserted, since GTK+ does the insert before the delete. Therefore if + * the source row is south (higher row number) of the destination, it will + * be one higher than expected. + * + * For instance if we drag row 1 to before row 0 we will see row-inserted + * for row 0 but then a row-deleted for row 2. + */ + if(srcrow > dstrow) + --srcrow; + + /* Tell the queue implementation */ + ql->drop(srcrow, dstrow); + + /* Dispose of stashed data */ + gtk_tree_path_free(ql->drag_target); + ql->drag_target = 0; + } +} + /** @brief Initialize a @ref queuelike */ GtkWidget *init_queuelike(struct queuelike *ql) { D(("init_queuelike")); - /* Create the list store. We add an extra column to hold the ID. */ - GType *types = xcalloc(ql->ncolumns + 1, sizeof (GType)); - for(int n = 0; n < ql->ncolumns; ++n) + /* Create the list store. We add an extra column to hold a pointer to the + * queue_entry. */ + GType *types = xcalloc(ql->ncolumns + EXTRA_COLUMNS, sizeof (GType)); + for(int n = 0; n < ql->ncolumns + EXTRA_COLUMNS; ++n) types[n] = G_TYPE_STRING; - types[ql->ncolumns] = G_TYPE_POINTER; - ql->store = gtk_list_store_newv(ql->ncolumns + 1, types); + types[ql->ncolumns + QUEUEPOINTER_COLUMN] = G_TYPE_POINTER; + ql->store = gtk_list_store_newv(ql->ncolumns + EXTRA_COLUMNS, types); g_object_set_data(G_OBJECT(ql->store), "ql", (void *)ql); /* Create the view */ @@ -526,6 +543,8 @@ GtkWidget *init_queuelike(struct queuelike *ql) { (ql->columns[n].name, r, "text", n, + "background", ql->ncolumns + BACKGROUND_COLUMN, + "foreground", ql->ncolumns + FOREGROUND_COLUMN, (char *)0); gtk_tree_view_column_set_resizable(c, TRUE); gtk_tree_view_column_set_reorderable(c, TRUE); @@ -542,11 +561,24 @@ GtkWidget *init_queuelike(struct queuelike *ql) { g_signal_connect(ql->view, "button-press-event", G_CALLBACK(ql_button_release), ql); + /* Drag+drop*/ + if(ql->drop) { + gtk_tree_view_set_reorderable(GTK_TREE_VIEW(ql->view), TRUE); + g_signal_connect(ql->store, + "row-inserted", + G_CALLBACK(ql_row_inserted), ql); + g_signal_connect(ql->store, + "row-deleted", + G_CALLBACK(ql_row_deleted), ql); + } + /* TODO style? */ - /* TODO drag+drop */ ql->init(); + /* Update display text when lookups complete */ + event_register("lookups-completed", queue_lookups_completed, ql); + GtkWidget *scrolled = scroll_widget(ql->view); g_object_set_data(G_OBJECT(scrolled), "type", (void *)ql_tabtype(ql)); return scrolled;