X-Git-Url: http://www.chiark.greenend.org.uk/ucgi/~mdw/git/disorder/blobdiff_plain/39fe10140c119ea11d2834416d59ff0995675adc..3849817f8cbfc366a5e9979c190b2f1d604f4021:/disobedience/queue.c diff --git a/disobedience/queue.c b/disobedience/queue.c index f1df8f8..8ff6b6f 100644 --- a/disobedience/queue.c +++ b/disobedience/queue.c @@ -1,6 +1,6 @@ /* * This file is part of DisOrder - * Copyright (C) 2006 Richard Kettlewell + * Copyright (C) 2006-2008 Richard Kettlewell * * 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 @@ -17,14 +17,14 @@ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 * USA */ - -#include "disobedience.h" - -#define HCELLPADDING 4 -#define VCELLPADDING 2 - -/* A queue layout is structured as follows: +/** @file disobedience/queue.c + * @brief Queue widgets + * + * This file provides both the queue widget and the recently-played widget. * + * A queue layout is structured as follows: + * + *
  *  vbox
  *   titlescroll
  *    titlelayout
@@ -34,6 +34,7 @@
  *    mainlayout
  *     cells[row * N + c]              eventbox (made by wrap_queue_cell)
  *      cells[row * N + c]->child      label (from column constructors)
+ * 
* * titlescroll never has any scrollbars. Instead whenever mainscroll's * horizontal adjustment is changed, queue_scrolled adjusts titlescroll to @@ -51,8 +52,21 @@ * These only exist while the drag proceeds, as otherwise they steal events * from more deserving widgets. (It might work to hide them when not in use * too but this way around the d+d code is a bit more self-contained.) + * + * NB that while in the server the playing track is not in the queue, in + * Disobedience, the playing does live in @c ql_queue.q, despite its different + * status to everything else found in that list. */ +#include "disobedience.h" +#include "charset.h" + +/** @brief Horizontal padding for queue cells */ +#define HCELLPADDING 4 + +/** @brief Vertical padding for queue cells */ +#define VCELLPADDING 2 + /* Queue management -------------------------------------------------------- */ WT(label); @@ -80,6 +94,15 @@ static GtkWidget *column_length(const struct queuelike *ql, const struct queue_entry *q, const char *data); static int draggable_row(const struct queue_entry *q); +static void recent_changed(const char *event, + void *eventdata, + void *callbackdata); +static void added_changed(const char *event, + void *eventdata, + void *callbackdata); +static void queue_changed(const char *event, + void *eventdata, + void *callbackdata); static const struct tabtype tabtype_queue; /* forward */ @@ -88,7 +111,7 @@ static const GtkTargetEntry dragtargets[] = { }; #define NDRAGTARGETS (int)(sizeof dragtargets / sizeof *dragtargets) -/* Definition of a column */ +/** @brief Definition of a column */ struct column { const char *name; /* Column name */ GtkWidget *(*widget)(const struct queuelike *ql, @@ -98,8 +121,8 @@ struct column { gfloat xalign; /* Alignment of the label */ }; -/* Need this in the middle of the types for NCOLUMNS */ -static const struct column columns[] = { +/** @brief Table of columns for queue and recently played list */ +static const struct column maincolumns[] = { { "When", column_when, 0, 1 }, { "Who", column_who, 0, 0 }, { "Artist", column_namepart, "artist", 0 }, @@ -107,72 +130,103 @@ static const struct column columns[] = { { "Title", column_namepart, "title", 0 }, { "Length", column_length, 0, 1 } }; -#define NCOLUMNS (int)(sizeof columns / sizeof *columns) -/* Data passed to menu item activation handlers */ +/** @brief Number of columns in queue and recnetly played list */ +#define NMAINCOLUMNS (int)(sizeof maincolumns / sizeof *maincolumns) + +/** @brief Table of columns for recently added tracks */ +static const struct column addedcolumns[] = { + { "Artist", column_namepart, "artist", 0 }, + { "Album", column_namepart, "album", 0 }, + { "Title", column_namepart, "title", 0 }, + { "Length", column_length, 0, 1 } +}; + +/** @brief Number of columns in recently added list */ +#define NADDEDCOLUMNS (int)(sizeof addedcolumns / sizeof *addedcolumns) + +/** @brief Maximum number of column in any @ref queuelike */ +#define MAXCOLUMNS (NMAINCOLUMNS > NADDEDCOLUMNS ? NMAINCOLUMNS : NADDEDCOLUMNS) + +/** @brief Data passed to menu item activation handlers */ struct menuiteminfo { - struct queuelike *ql; /* which queue we're dealing with */ - struct queue_entry *q; /* hovered entry or 0 */ + struct queuelike *ql; /**< @brief which queue we're dealing with */ + struct queue_entry *q; /**< @brief hovered entry or 0 */ }; -struct menuitem { - /* Parameters */ - const char *name; /* name */ +/** @brief An item in the queue's popup menu */ +struct queue_menuitem { + /** @brief Menu item name */ + const char *name; - /* Callbacks */ + /** @brief Called to activate the menu item + * + * The user data is the queue entry that the pointer was over when the menu + * popped up. */ void (*activate)(GtkMenuItem *menuitem, gpointer user_data); - /* Called to activate the menu item. The user data is the queue entry that - * the pointer was over when the menu popped up. */ + /** @brief Called to determine whether the menu item is usable. + * + * Returns @c TRUE if it should be sensitive and @c FALSE otherwise. @p q + * points to the queue entry the pointer is over. + */ int (*sensitive)(struct queuelike *ql, - struct menuitem *m, + struct queue_menuitem *m, struct queue_entry *q); - /* Called to determine whether the menu item is usable. Returns TRUE if it - * should be sensitive and FALSE otherwise. Q points to the queue entry the - * pointer is over. */ - /* State */ - gulong handlerid; /* signal handler ID */ - GtkWidget *w; /* menu item widget */ + /** @brief Signal handler ID */ + gulong handlerid; + + /** @brief Widget for menu item */ + GtkWidget *w; }; +/** @brief A queue-like object + * + * There are (currently) three of these: @ref ql_queue, @ref ql_recent and @ref + * ql_added. + */ struct queuelike { - /* Parameters */ - const char *name; /* queue or recent */ - - /* Callbacks */ + /** @brief Called when an update completes */ void (*notify)(void); - /* Called when an update completes. */ - + + /** @brief Called to fix up the queue after update + * @param q The list passed back from the server + * @return Assigned to @c ql->q + */ struct queue_entry *(*fixup)(struct queue_entry *q); - /* Fix up the queue after update, or 0. Q is the list passed back from the - * server, the return value is assigned to ql->q. */ /* Widgets */ - GtkWidget *mainlayout; /* main layout */ - GtkWidget *mainscroll; /* scroller for main layout */ - GtkWidget *titlelayout; /* title layout */ - GtkWidget *titlecells[NCOLUMNS + 1]; /* title cells */ - GtkWidget **cells; /* all the cells */ - GtkWidget *menu; /* popup menu */ - struct menuitem *menuitems; /* menu items */ - GtkWidget *dragmark; /* drag destination marker */ - GtkWidget **dropzones; /* drag targets */ + GtkWidget *mainlayout; /**< @brief main layout */ + GtkWidget *mainscroll; /**< @brief scroller for main layout */ + GtkWidget *titlelayout; /**< @brief title layout */ + GtkWidget *titlecells[MAXCOLUMNS + 1]; /**< @brief title cells */ + GtkWidget **cells; /**< @brief all the cells */ + GtkWidget *menu; /**< @brief popup menu */ + struct queue_menuitem *menuitems; /**< @brief menu items */ + GtkWidget *dragmark; /**< @brief drag destination marker */ + GtkWidget **dropzones; /**< @brief drag targets */ + int ndropzones; /**< @brief number of drag targets */ /* State */ - struct queue_entry *q; /* head of queue */ - struct queue_entry *last_click; /* last click */ - int nrows; /* number of rows */ - int mainrowheight; /* height of one row */ - hash *selection; /* currently selected items */ - int swallow_release; /* swallow button release from drag */ + struct queue_entry *q; /**< @brief head of queue */ + struct queue_entry *last_click; /**< @brief last click */ + int nrows; /**< @brief number of rows */ + int mainrowheight; /**< @brief height of one row */ + hash *selection; /**< @brief currently selected items */ + int swallow_release; /**< @brief swallow button release from drag */ + + const struct column *columns; /**< @brief Table of columns */ + int ncolumns; /**< @brief Number of columns */ }; -static struct queuelike ql_queue, ql_recent; /* queue and recently played */ -static struct queue_entry *actual_queue; /* actual queue */ -static struct queue_entry *playing_track; /* currenty playing */ -static time_t last_playing = (time_t)-1; /* when last got playing */ +static struct queuelike ql_queue; /**< @brief The main queue */ +static struct queuelike ql_recent; /*< @brief Recently-played tracks */ +static struct queuelike ql_added; /*< @brief Newly added tracks */ +static struct queue_entry *actual_queue; /**< @brief actual queue */ +static struct queue_entry *playing_track; /**< @brief currenty playing */ +static time_t last_playing = (time_t)-1; /**< @brief when last got playing */ static int namepart_lookups_outstanding; static int namepart_completions_deferred; /* # of completions not processed */ static const struct cache_type cachetype_string = { 3600 }; @@ -203,8 +257,8 @@ static void dump_layout(const struct queuelike *ql) { describe_widget("mainscroll", ql->mainscroll, 0); describe_widget("mainlayout", ql->mainlayout, 1); for(q = ql->q, row = 0; q; q = q->next, ++row) - for(col = 0; col < NCOLUMNS + 1; ++col) - if((w = ql->cells[row * (NCOLUMNS + 1) + col])) { + for(col = 0; col < ql->ncolumns + 1; ++col) + if((w = ql->cells[row * (ql->ncolumns + 1) + col])) { sprintf(s, "%dx%d", row, col); describe_widget(s, w, 2); if(GTK_BIN(w)->child) @@ -215,68 +269,62 @@ static void dump_layout(const struct queuelike *ql) { /* Track detail lookup ----------------------------------------------------- */ -/* A namepart lookup has completed or failed. */ +/** @brief Called when a namepart lookup has completed or failed */ static void namepart_completed_or_failed(void) { D(("namepart_completed_or_failed")); --namepart_lookups_outstanding; - if(!namepart_lookups_outstanding || namepart_completions_deferred > 24) { + if(!namepart_lookups_outstanding) { redisplay_queue(&ql_queue); redisplay_queue(&ql_recent); + redisplay_queue(&ql_added); namepart_completions_deferred = 0; } } -/* A namepart lookup has completed. */ -static void namepart_completed(void *v, const char *value) { - struct callbackdata *cbd = v; - - D(("namepart_completed")); - cache_put(&cachetype_string, cbd->u.key, value); - ++namepart_completions_deferred; - namepart_completed_or_failed(); -} - -/* A length lookup has completed. */ -static void length_completed(void *v, long l) { - struct callbackdata *cbd = v; - long *value; +/** @brief Called when a namepart lookup has completed */ +static void namepart_completed(void *v, const char *error, const char *value) { + if(error) { + gtk_label_set_text(GTK_LABEL(report_label), error); + } else { + const char *key = v; - D(("namepart_completed")); - value = xmalloc(sizeof *value); - *value = l; - cache_put(&cachetype_integer, cbd->u.key, value); - ++namepart_completions_deferred; + cache_put(&cachetype_string, key, value); + ++namepart_completions_deferred; + } namepart_completed_or_failed(); } -/* A length or namepart lookup has failed. */ -static void namepart_protocol_error( - struct callbackdata attribute((unused)) *cbd, - int attribute((unused)) code, - const char *msg) { - D(("namepart_protocol_error")); - gtk_label_set_text(GTK_LABEL(report_label), msg); +/** @brief Called when a length lookup has completed */ +static void length_completed(void *v, const char *error, long l) { + if(error) + gtk_label_set_text(GTK_LABEL(report_label), error); + else { + const char *key = v; + long *value; + + D(("namepart_completed")); + value = xmalloc(sizeof *value); + *value = l; + cache_put(&cachetype_integer, key, value); + ++namepart_completions_deferred; + } namepart_completed_or_failed(); } -/* Arrange to fill in a namepart cache entry */ +/** @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) { - struct callbackdata *cbd; - ++namepart_lookups_outstanding; - cbd = xmalloc(sizeof *cbd); - cbd->onerror = namepart_protocol_error; - cbd->u.key = key; disorder_eclient_namepart(client, namepart_completed, - track, context, part, cbd); + track, context, part, (void *)key); } -/* Look up a namepart. 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. */ +/** @brief Look up a namepart + * + * 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) { @@ -297,7 +345,7 @@ static const char *namepart(const char *track, return value; } -/* Called from properties.c when we know a name part has changed */ +/** @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) { @@ -310,13 +358,13 @@ void namepart_update(const char *track, namepart_fill(track, context, part, key); } -/* 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. */ +/** @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; - struct callbackdata *cbd; static const long bogus = -1; D(("getlength %s", track)); @@ -326,17 +374,14 @@ static long getlength(const char *track) { D(("deferring..."));; cache_put(&cachetype_integer, key, value = &bogus); ++namepart_lookups_outstanding; - cbd = xmalloc(sizeof *cbd); - cbd->onerror = namepart_protocol_error; - cbd->u.key = key; - disorder_eclient_length(client, length_completed, track, cbd); + disorder_eclient_length(client, length_completed, track, key); } return *value; } /* Column constructors ----------------------------------------------------- */ -/* Format the 'when' column */ +/** @brief Format the 'when' column */ static GtkWidget *column_when(const struct queuelike attribute((unused)) *ql, const struct queue_entry *q, const char attribute((unused)) *data) { @@ -372,7 +417,7 @@ static GtkWidget *column_when(const struct queuelike attribute((unused)) *ql, return gtk_label_new(when); } -/* Format the 'who' column */ +/** @brief Format the 'who' column */ static GtkWidget *column_who(const struct queuelike attribute((unused)) *ql, const struct queue_entry *q, const char attribute((unused)) *data) { @@ -381,17 +426,18 @@ static GtkWidget *column_who(const struct queuelike attribute((unused)) *ql, return gtk_label_new(q->submitter ? q->submitter : ""); } -/* Format one of the track name columns */ +/** @brief Format one of the track name columns */ static GtkWidget *column_namepart(const struct queuelike attribute((unused)) *ql, const struct queue_entry *q, const char *data) { D(("column_namepart")); NW(label); - return gtk_label_new(namepart(q->track, "display", data)); + return gtk_label_new(truncate_for_display(namepart(q->track, "display", data), + config->short_display)); } -/* Compute the length field */ +/** @brief Compute the length field */ static const char *text_length(const struct queue_entry *q) { long l; time_t now; @@ -420,7 +466,7 @@ static const char *text_length(const struct queue_entry *q) { return length; } -/* Format the length column */ +/** @brief Format the length column */ static GtkWidget *column_length(const struct queuelike attribute((unused)) *ql, const struct queue_entry *q, const char attribute((unused)) *data) { @@ -440,7 +486,7 @@ static GtkWidget *column_length(const struct queuelike attribute((unused)) *ql, } -/* Apply a new queue contents, transferring the selection from the old value */ +/** @brief Apply a new queue contents, transferring the selection from the old value */ static void update_queue(struct queuelike *ql, struct queue_entry *newq) { struct queue_entry *q; @@ -464,9 +510,14 @@ static void update_queue(struct queuelike *ql, struct queue_entry *newq) { selection_cleanup(ql->selection); } -/* Wrap up a widget for putting into the queue or title */ +/** @brief Wrap up a widget for putting into the queue or title + * @param label Label to contain + * @param style Pointer to style to use + * @param wp Updated with maximum width (or NULL) + * @return New widget + */ static GtkWidget *wrap_queue_cell(GtkWidget *label, - const char *name, + GtkStyle *style, int *wp) { GtkRequisition req; GtkWidget *bg; @@ -484,43 +535,43 @@ static GtkWidget *wrap_queue_cell(GtkWidget *label, gtk_widget_size_request(label, &req); if(req.width > *wp) *wp = req.width; } - /* Set widget names */ - gtk_widget_set_name(bg, name); - gtk_widget_set_name(label, name); + /* Set colors */ + gtk_widget_set_style(bg, style); + gtk_widget_set_style(label, style); return bg; } -/* Create the wrapped widget for a cell in the queue display */ +/** @brief Create the wrapped widget for a cell in the queue display */ static GtkWidget *get_queue_cell(struct queuelike *ql, const struct queue_entry *q, int row, int col, - const char *name, + GtkStyle *style, int *wp) { GtkWidget *label; D(("get_queue_cell %d %d", row, col)); - label = columns[col].widget(ql, q, columns[col].data); - gtk_misc_set_alignment(GTK_MISC(label), columns[col].xalign, 0); - return wrap_queue_cell(label, name, wp); + label = ql->columns[col].widget(ql, q, ql->columns[col].data); + gtk_misc_set_alignment(GTK_MISC(label), ql->columns[col].xalign, 0); + return wrap_queue_cell(label, style, wp); } -/* Add a padding cell to the end of a row */ -static GtkWidget *get_padding_cell(const char *name) { +/** @brief Add a padding cell to the end of a row */ +static GtkWidget *get_padding_cell(GtkStyle *style) { D(("get_padding_cell")); NW(label); - return wrap_queue_cell(gtk_label_new(""), name, 0); + return wrap_queue_cell(gtk_label_new(""), style, 0); } /* User button press and menu ---------------------------------------------- */ -/* Update widget states in order to reflect the selection status */ +/** @brief Update widget states in order to reflect the selection status */ static void set_widget_states(struct queuelike *ql) { struct queue_entry *q; int row, col; for(q = ql->q, row = 0; q; q = q->next, ++row) { - for(col = 0; col < NCOLUMNS + 1; ++col) - gtk_widget_set_state(ql->cells[row * (NCOLUMNS + 1) + col], + for(col = 0; col < ql->ncolumns + 1; ++col) + gtk_widget_set_state(ql->cells[row * (ql->ncolumns + 1) + col], selection_selected(ql->selection, q->id) ? GTK_STATE_SELECTED : GTK_STATE_NORMAL); } @@ -528,6 +579,7 @@ static void set_widget_states(struct queuelike *ql) { menu_update(-1); } +/** @brief Ordering function for queue entries */ static int queue_before(const struct queue_entry *a, const struct queue_entry *b) { while(a && a != b) @@ -535,7 +587,7 @@ static int queue_before(const struct queue_entry *a, return !!a; } -/* A button was pressed and released */ +/** @brief A button was pressed and released */ static gboolean queuelike_button_released(GtkWidget attribute((unused)) *widget, GdkEventButton *event, gpointer user_data) { @@ -629,14 +681,16 @@ static gboolean queuelike_button_released(GtkWidget attribute((unused)) *widget, return FALSE; /* propagate */ } -/* A button was pressed or released on the mainlayout. For debugging only at - * the moment. */ +/** @brief A button was pressed or released on the mainlayout + * + * For debugging only at the moment. */ static gboolean mainlayout_button(GtkWidget attribute((unused)) *widget, GdkEventButton attribute((unused)) *event, gpointer attribute((unused)) user_data) { return FALSE; /* propagate */ } +/** @brief Select all entries in a queue */ void queue_select_all(struct queuelike *ql) { struct queue_entry *qq; @@ -646,6 +700,17 @@ void queue_select_all(struct queuelike *ql) { set_widget_states(ql); } +/** @brief Deselect all entries in a queue */ +void queue_select_none(struct queuelike *ql) { + struct queue_entry *qq; + + for(qq = ql->q; qq; qq = qq->next) + selection_set(ql->selection, qq->id, 0); + ql->last_click = 0; + set_widget_states(ql); +} + +/** @brief Pop up properties for selected tracks */ void queue_properties(struct queuelike *ql) { struct vector v; const struct queue_entry *qq; @@ -655,16 +720,21 @@ void queue_properties(struct queuelike *ql) { if(selection_selected(ql->selection, qq->id)) vector_append(&v, (char *)qq->track); if(v.nvec) - properties(v.nvec, v.vec); + properties(v.nvec, (const char **)v.vec); } /* Drag and drop rearrangement --------------------------------------------- */ +/** @brief Return nonzero if @p is a draggable row + * + * Only tracks in the main queue are draggable (and the currently playing track + * is not draggable). + */ static int draggable_row(const struct queue_entry *q) { return q->ql == &ql_queue && q != playing_track; } -/* Called when a drag begings */ +/** @brief Called when a drag begings */ static void queue_drag_begin(GtkWidget attribute((unused)) *widget, GdkDragContext attribute((unused)) *dc, gpointer data) { @@ -687,7 +757,7 @@ static void queue_drag_begin(GtkWidget attribute((unused)) *widget, add_drag_targets(ql); } -/* Convert an ID back into a queue entry and a screen row number */ +/** @brief Convert @p id back into a queue entry and a screen row number */ static struct queue_entry *findentry(struct queuelike *ql, const char *id, int *rowp) { @@ -705,7 +775,13 @@ static struct queue_entry *findentry(struct queuelike *ql, return q; } -/* Called when data is dropped */ +static void move_completed(void attribute((unused)) *v, + const char *error) { + if(error) + popup_protocol_error(0, error); +} + +/** @brief Called when data is dropped */ static gboolean queue_drag_drop(GtkWidget attribute((unused)) *widget, GdkDragContext *drag_context, gint attribute((unused)) x, @@ -724,14 +800,14 @@ static gboolean queue_drag_drop(GtkWidget attribute((unused)) *widget, if(q != playing_track && selection_selected(ql->selection, q->id)) vector_append(&vec, (char *)q->id); disorder_eclient_moveafter(client, id, vec.nvec, (const char **)vec.vec, - 0/*completed*/, 0/*v*/); + move_completed, 0/*v*/); gtk_drag_finish(drag_context, TRUE, TRUE, when); /* Destroy dropzones */ remove_drag_targets(ql); return TRUE; } -/* Called when we enter, or move within, a drop zone */ +/** @brief Called when we enter, or move within, a drop zone */ static gboolean queue_drag_motion(GtkWidget attribute((unused)) *widget, GdkDragContext *drag_context, gint attribute((unused)) x, @@ -750,7 +826,7 @@ static gboolean queue_drag_motion(GtkWidget attribute((unused)) *widget, g_signal_connect(ql->dragmark, "destroy", G_CALLBACK(gtk_widget_destroyed), &ql->dragmark); gtk_widget_set_size_request(ql->dragmark, 10240, row ? 4 : 2); - gtk_widget_set_name(ql->dragmark, "queue-drag"); + gtk_widget_set_style(ql->dragmark, drag_style); gtk_layout_put(GTK_LAYOUT(ql->mainlayout), ql->dragmark, 0, (row + 1) * ql->mainrowheight - !!row); } else @@ -764,7 +840,7 @@ static gboolean queue_drag_motion(GtkWidget attribute((unused)) *widget, return FALSE; } -/* Called when we leave a drop zone */ +/** @brief Called when we leave a drop zone */ static void queue_drag_leave(GtkWidget attribute((unused)) *widget, GdkDragContext attribute((unused)) *drag_context, guint attribute((unused)) when, @@ -775,13 +851,23 @@ static void queue_drag_leave(GtkWidget attribute((unused)) *widget, gtk_widget_hide(ql->dragmark); } -/* Add a drag target at position Y. ID is the track to insert the moved tracks - * after, and might be 0 to insert before the start. */ -static void add_drag_target(struct queuelike *ql, int y, int row, +/** @brief Add a drag target + * @param ql The queue-like (in practice this is always @ref ql_queue) + * @param y The Y coordinate to place the drag target + * @param id Track to insert moved tracks after, or NULL + * + * Adds a drop zone at Y coordinate @p y, which is assumed to lie between two + * tracks (or before the start of the queue or after the end of the queue). If + * tracks are dragged into this dropzone then they will be moved @em after + * track @p id, or to the start of the queue if @p id is NULL. + * + * We remember all the dropzones in @c ql->dropzones so they can be destroyed + * later. + */ +static void add_drag_target(struct queuelike *ql, int y, const char *id) { GtkWidget *eventbox; - assert(ql->dropzones[row] == 0); NW(event_box); eventbox = gtk_event_box_new(); /* Make the target zone invisible */ @@ -806,59 +892,62 @@ static void add_drag_target(struct queuelike *ql, int y, int row, /* The widget needs to be shown to receive drags */ gtk_widget_show(eventbox); /* Remember the drag targets */ - ql->dropzones[row] = eventbox; + ql->dropzones[ql->ndropzones] = eventbox; g_signal_connect(eventbox, "destroy", - G_CALLBACK(gtk_widget_destroyed), &ql->dropzones[row]); + G_CALLBACK(gtk_widget_destroyed), + &ql->dropzones[ql->ndropzones]); + ++ql->ndropzones; } -/* Create dropzones for dragging into */ +/** @brief Create dropzones for dragging into */ static void add_drag_targets(struct queuelike *ql) { - int row, y; + int y; struct queue_entry *q; /* Create an array to store the widgets */ ql->dropzones = xcalloc(ql->nrows, sizeof (GtkWidget *)); + ql->ndropzones = 0; y = 0; /* Add a drag target before the first row provided it's not the playing * track */ if(!playing_track || ql->q != playing_track) - add_drag_target(ql, 0, 0, 0); + add_drag_target(ql, 0, 0); /* Put a drag target at the bottom of every row */ - for(q = ql->q, row = 0; q; q = q->next, ++row) { + for(q = ql->q; q; q = q->next) { y += ql->mainrowheight; - add_drag_target(ql, y, row, q->id); + add_drag_target(ql, y, q->id); } } -/* Remove the dropzones */ +/** @brief Remove the dropzones */ static void remove_drag_targets(struct queuelike *ql) { - int row; + int n; - for(row = 0; row < ql->nrows; ++row) { - if(ql->dropzones[row]) { + for(n = 0; n < ql->ndropzones; ++n) { + if(ql->dropzones[n]) { DW(event_box); - gtk_widget_destroy(ql->dropzones[row]); + gtk_widget_destroy(ql->dropzones[n]); } - assert(ql->dropzones[row] == 0); + assert(ql->dropzones[n] == 0); } } /* Layout ------------------------------------------------------------------ */ -/* Redisplay the queue. */ +/** @brief Redisplay a queue */ static void redisplay_queue(struct queuelike *ql) { struct queue_entry *q; int row, col; - GList *c; - const char *name; + GList *c, *children; + GtkStyle *style; GtkRequisition req; GtkWidget *w; - int maxwidths[NCOLUMNS], x, y, titlerowheight; + int maxwidths[MAXCOLUMNS], x, y, titlerowheight; int totalwidth = 10240; /* TODO: can we be less blunt */ D(("redisplay_queue")); /* Eliminate all the existing widgets and start from scratch */ - for(c = gtk_container_get_children(GTK_CONTAINER(ql->mainlayout)); + for(c = children = gtk_container_get_children(GTK_CONTAINER(ql->mainlayout)); c; c = c->next) { /* Destroy both the label and the eventbox */ @@ -869,13 +958,14 @@ static void redisplay_queue(struct queuelike *ql) { DW(event_box); gtk_widget_destroy(GTK_WIDGET(c->data)); } + g_list_free(children); /* Adjust the row count */ for(q = ql->q, ql->nrows = 0; q; q = q->next) ++ql->nrows; /* We need to create all the widgets before we can position them */ - ql->cells = xcalloc(ql->nrows * (NCOLUMNS + 1), sizeof *ql->cells); + ql->cells = xcalloc(ql->nrows * (ql->ncolumns + 1), sizeof *ql->cells); /* Minimum width is given by the column headings */ - for(col = 0; col < NCOLUMNS; ++col) { + for(col = 0; col < ql->ncolumns; ++col) { /* Reset size so we don't inherit last iteration's maximum size */ gtk_widget_set_size_request(GTK_BIN(ql->titlecells[col])->child, -1, -1); gtk_widget_size_request(GTK_BIN(ql->titlecells[col])->child, &req); @@ -889,16 +979,16 @@ static void redisplay_queue(struct queuelike *ql) { /* Construct the widgets */ for(q = ql->q, row = 0; q; q = q->next, ++row) { /* Figure out the widget name for this row */ - if(q == playing_track) name = "row-playing"; - else name = row % 2 ? "row-even" : "row-odd"; + if(q == playing_track) style = active_style; + else style = row % 2 ? even_style : odd_style; /* Make the widget for each column */ - for(col = 0; col <= NCOLUMNS; ++col) { + for(col = 0; col <= ql->ncolumns; ++col) { /* Create and store the widget */ - if(col < NCOLUMNS) - w = get_queue_cell(ql, q, row, col, name, &maxwidths[col]); + if(col < ql->ncolumns) + w = get_queue_cell(ql, q, row, col, style, &maxwidths[col]); else - w = get_padding_cell(name); - ql->cells[row * (NCOLUMNS + 1) + col] = w; + w = get_padding_cell(style); + ql->cells[row * (ql->ncolumns + 1) + col] = w; /* Maybe mark it draggable */ if(draggable_row(q)) { gtk_drag_source_set(w, GDK_BUTTON1_MASK, @@ -919,14 +1009,14 @@ static void redisplay_queue(struct queuelike *ql) { * everything and position it */ for(row = 0, q = ql->q; row < ql->nrows; ++row, q = q->next) { x = 0; - for(col = 0; col < NCOLUMNS; ++col) { - w = ql->cells[row * (NCOLUMNS + 1) + col]; + for(col = 0; col < ql->ncolumns; ++col) { + w = ql->cells[row * (ql->ncolumns + 1) + col]; gtk_widget_set_size_request(GTK_BIN(w)->child, maxwidths[col], -1); gtk_layout_put(GTK_LAYOUT(ql->mainlayout), w, x, y); x += maxwidths[col]; } - w = ql->cells[row * (NCOLUMNS + 1) + col]; + w = ql->cells[row * (ql->ncolumns + 1) + col]; gtk_widget_set_size_request(GTK_BIN(w)->child, totalwidth - x, -1); gtk_layout_put(GTK_LAYOUT(ql->mainlayout), w, x, y); @@ -935,7 +1025,7 @@ static void redisplay_queue(struct queuelike *ql) { } /* Titles */ x = 0; - for(col = 0; col < NCOLUMNS; ++col) { + for(col = 0; col < ql->ncolumns; ++col) { gtk_widget_set_size_request(GTK_BIN(ql->titlecells[col])->child, maxwidths[col], -1); gtk_layout_move(GTK_LAYOUT(ql->titlelayout), ql->titlecells[col], x, 0); @@ -959,35 +1049,44 @@ static void redisplay_queue(struct queuelike *ql) { gtk_widget_set_size_request(ql->titlelayout, -1, titlerowheight); } -/* Called with new queue/recent contents */ -static void queuelike_completed(void *v, struct queue_entry *q) { - struct callbackdata *cbd = v; - struct queuelike *ql = cbd->u.ql; - - D(("queuelike_complete")); - /* Install the new queue */ - update_queue(ql, ql->fixup(q)); - /* Update the display */ - redisplay_queue(ql); - if(ql->notify) - ql->notify(); - /* Update sensitivity of main menu items */ - menu_update(-1); +/** @brief Called with new queue/recent contents */ +static void queuelike_completed(void *v, + const char *error, + struct queue_entry *q) { + if(error) + popup_protocol_error(0, error); + else { + struct queuelike *const ql = v; + + D(("queuelike_complete")); + /* Install the new queue */ + update_queue(ql, ql->fixup ? ql->fixup(q) : q); + /* Update the display */ + redisplay_queue(ql); + if(ql->notify) + ql->notify(); + /* Update sensitivity of main menu items */ + menu_update(-1); + } } -/* Called with a new currently playing track */ +/** @brief Called with a new currently playing track */ static void playing_completed(void attribute((unused)) *v, + const char *error, struct queue_entry *q) { - struct callbackdata cbd; - D(("playing_completed")); - playing_track = q; - /* Record when we got the playing track data so we know how old the 'sofar' - * field is */ - time(&last_playing); - cbd.u.ql = &ql_queue; - queuelike_completed(&cbd, actual_queue); + if(error) + popup_protocol_error(0, error); + else { + D(("playing_completed")); + playing_track = q; + /* Record when we got the playing track data so we know how old the 'sofar' + * field is */ + time(&last_playing); + queuelike_completed(&ql_queue, 0, actual_queue); + } } +/** @brief Called when the queue is scrolled */ static void queue_scrolled(GtkAdjustment *adjustment, gpointer user_data) { GtkAdjustment *titleadj = user_data; @@ -996,12 +1095,13 @@ static void queue_scrolled(GtkAdjustment *adjustment, gtk_adjustment_set_value(titleadj, adjustment->value); } -/* Create a queuelike thing (queue/recent) */ +/** @brief Create a queuelike thing (queue/recent) */ static GtkWidget *queuelike(struct queuelike *ql, struct queue_entry *(*fixup)(struct queue_entry *), void (*notify)(void), - struct menuitem *menuitems, - const char *name) { + struct queue_menuitem *menuitems, + const struct column *columns, + int ncolumns) { GtkWidget *vbox, *mainscroll, *titlescroll, *label; GtkAdjustment *mainadj, *titleadj; int col, n; @@ -1010,17 +1110,20 @@ static GtkWidget *queuelike(struct queuelike *ql, ql->fixup = fixup; ql->notify = notify; ql->menuitems = menuitems; - ql->name = name; ql->mainrowheight = !0; /* else division by 0 */ ql->selection = selection_new(); + ql->columns = columns; + ql->ncolumns = ncolumns; /* Create the layouts */ NW(layout); ql->mainlayout = gtk_layout_new(0, 0); + gtk_widget_set_style(ql->mainlayout, layout_style); NW(layout); ql->titlelayout = gtk_layout_new(0, 0); + gtk_widget_set_style(ql->titlelayout, title_style); /* Scroll the layouts */ - ql->mainscroll = mainscroll = scroll_widget(ql->mainlayout, name); - titlescroll = scroll_widget(ql->titlelayout, name); + ql->mainscroll = mainscroll = scroll_widget(ql->mainlayout); + titlescroll = scroll_widget(ql->titlelayout); gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(titlescroll), GTK_POLICY_NEVER, GTK_POLICY_NEVER); mainadj = gtk_scrolled_window_get_hadjustment(GTK_SCROLLED_WINDOW(mainscroll)); @@ -1028,14 +1131,14 @@ static GtkWidget *queuelike(struct queuelike *ql, g_signal_connect(mainadj, "changed", G_CALLBACK(queue_scrolled), titleadj); g_signal_connect(mainadj, "value-changed", G_CALLBACK(queue_scrolled), titleadj); /* Fill the titles and put them anywhere */ - for(col = 0; col < NCOLUMNS; ++col) { + for(col = 0; col < ql->ncolumns; ++col) { NW(label); - label = gtk_label_new(columns[col].name); - gtk_misc_set_alignment(GTK_MISC(label), columns[col].xalign, 0); - ql->titlecells[col] = wrap_queue_cell(label, "row-title", 0); + label = gtk_label_new(ql->columns[col].name); + gtk_misc_set_alignment(GTK_MISC(label), ql->columns[col].xalign, 0); + ql->titlecells[col] = wrap_queue_cell(label, title_style, 0); gtk_layout_put(GTK_LAYOUT(ql->titlelayout), ql->titlecells[col], 0, 0); } - ql->titlecells[col] = get_padding_cell("row-title"); + ql->titlecells[col] = get_padding_cell(title_style); gtk_layout_put(GTK_LAYOUT(ql->titlelayout), ql->titlecells[col], 0, 0); /* Pack the lot together in a vbox */ NW(vbox); @@ -1061,17 +1164,18 @@ static GtkWidget *queuelike(struct queuelike *ql, g_signal_connect(ql->mainlayout, "button-press-event", G_CALLBACK(mainlayout_button), ql); #endif + set_tool_colors(ql->menu); return vbox; } /* Popup menu items -------------------------------------------------------- */ -/* Count the number of items selected */ +/** @brief Count the number of items selected */ static int queue_count_selected(const struct queuelike *ql) { return hash_count(ql->selection); } -/* Count the number of items selected */ +/** @brief Count the number of items selected */ static int queue_count_entries(const struct queuelike *ql) { int nitems = 0; const struct queue_entry *q; @@ -1081,8 +1185,8 @@ static int queue_count_entries(const struct queuelike *ql) { return nitems; } -/* Count the number of items selected, excluding the playing track if there is - * one */ +/** @brief Count the number of items selected, excluding the playing track if + * there is one */ static int count_selected_nonplaying(const struct queuelike *ql) { int nselected = queue_count_selected(ql); @@ -1091,27 +1195,49 @@ static int count_selected_nonplaying(const struct queuelike *ql) { return nselected; } +/** @brief Determine whether the scratch option should be sensitive */ static int scratch_sensitive(struct queuelike attribute((unused)) *ql, - struct menuitem attribute((unused)) *m, + struct queue_menuitem attribute((unused)) *m, struct queue_entry attribute((unused)) *q) { /* We can scratch if the playing track is selected */ - return playing_track && selection_selected(ql->selection, playing_track->id); + return (playing_track + && (disorder_eclient_state(client) & DISORDER_CONNECTED) + && selection_selected(ql->selection, playing_track->id)); +} + +/** @brief Called when disorder_eclient_scratch completes */ +static void scratch_completed(void attribute((unused)) *v, + const char *error) { + if(error) + popup_protocol_error(0, error); } +/** @brief Scratch the playing track */ static void scratch_activate(GtkMenuItem attribute((unused)) *menuitem, gpointer attribute((unused)) user_data) { if(playing_track) - disorder_eclient_scratch(client, playing_track->id, 0, 0); + disorder_eclient_scratch(client, playing_track->id, scratch_completed, 0); } +/** @brief Determine whether the remove option should be sensitive */ static int remove_sensitive(struct queuelike *ql, - struct menuitem attribute((unused)) *m, + struct queue_menuitem attribute((unused)) *m, struct queue_entry *q) { /* We can remove if we're hovering over a particular track or any non-playing * tracks are selected */ - return (q && q != playing_track) || count_selected_nonplaying(ql); + return ((disorder_eclient_state(client) & DISORDER_CONNECTED) + && ((q + && q != playing_track) + || count_selected_nonplaying(ql))); } +static void remove_completed(void attribute((unused)) *v, + const char *error) { + if(error) + popup_protocol_error(0, error); +} + +/** @brief Remove selected track(s) */ static void remove_activate(GtkMenuItem attribute((unused)) *menuitem, gpointer user_data) { const struct menuiteminfo *mii = user_data; @@ -1122,19 +1248,22 @@ static void remove_activate(GtkMenuItem attribute((unused)) *menuitem, /* Remove selected tracks */ for(q = ql->q; q; q = q->next) if(selection_selected(ql->selection, q->id) && q != playing_track) - disorder_eclient_remove(client, q->id, 0, 0); + disorder_eclient_remove(client, q->id, move_completed, 0); } else if(q) /* Remove just the hovered track */ - disorder_eclient_remove(client, q->id, 0, 0); + disorder_eclient_remove(client, q->id, remove_completed, 0); } +/** @brief Determine whether the properties menu option should be sensitive */ static int properties_sensitive(struct queuelike *ql, - struct menuitem attribute((unused)) *m, + struct queue_menuitem attribute((unused)) *m, struct queue_entry attribute((unused)) *q) { /* "Properties" is sensitive if at least something is selected */ - return hash_count(ql->selection) > 0; + return (hash_count(ql->selection) > 0 + && (disorder_eclient_state(client) & DISORDER_CONNECTED)); } +/** @brief Pop up properties for the selected tracks */ static void properties_activate(GtkMenuItem attribute((unused)) *menuitem, gpointer user_data) { const struct menuiteminfo *mii = user_data; @@ -1142,22 +1271,65 @@ static void properties_activate(GtkMenuItem attribute((unused)) *menuitem, queue_properties(mii->ql); } +/** @brief Determine whether the select all menu option should be sensitive */ static int selectall_sensitive(struct queuelike *ql, - struct menuitem attribute((unused)) *m, + struct queue_menuitem attribute((unused)) *m, struct queue_entry attribute((unused)) *q) { /* Sensitive if there is anything to select */ return !!ql->q; } +/** @brief Select all tracks */ static void selectall_activate(GtkMenuItem attribute((unused)) *menuitem, gpointer user_data) { const struct menuiteminfo *mii = user_data; queue_select_all(mii->ql); } +/** @brief Determine whether the select none menu option should be sensitive */ +static int selectnone_sensitive(struct queuelike *ql, + struct queue_menuitem attribute((unused)) *m, + struct queue_entry attribute((unused)) *q) { + /* Sensitive if there is anything selected */ + return hash_count(ql->selection) != 0; +} + +/** @brief Select no tracks */ +static void selectnone_activate(GtkMenuItem attribute((unused)) *menuitem, + gpointer user_data) { + const struct menuiteminfo *mii = user_data; + queue_select_none(mii->ql); +} + +/** @brief Determine whether the play menu option should be sensitive */ +static int play_sensitive(struct queuelike *ql, + struct queue_menuitem attribute((unused)) *m, + struct queue_entry attribute((unused)) *q) { + /* "Play" is sensitive if at least something is selected */ + return (hash_count(ql->selection) > 0 + && (disorder_eclient_state(client) & DISORDER_CONNECTED)); +} + +/** @brief Play the selected tracks */ +static void play_activate(GtkMenuItem attribute((unused)) *menuitem, + gpointer user_data) { + const struct menuiteminfo *mii = user_data; + struct queue_entry *q = mii->q; + struct queuelike *ql = mii->ql; + + if(queue_count_selected(ql)) { + /* Play selected tracks */ + for(q = ql->q; q; q = q->next) + if(selection_selected(ql->selection, q->id)) + disorder_eclient_play(client, q->track, play_completed, 0); + } else if(q) + /* Nothing is selected, so play the hovered track */ + disorder_eclient_play(client, q->track, play_completed, 0); +} + /* The queue --------------------------------------------------------------- */ -/* Fix up the queue by sticking the currently playing track on the front */ +/** @brief Fix up the queue by sticking the currently playing track on the front */ static struct queue_entry *fixup_queue(struct queue_entry *q) { D(("fixup_queue")); actual_queue = q; @@ -1170,7 +1342,9 @@ static struct queue_entry *fixup_queue(struct queue_entry *q) { return actual_queue; } -/* Called regularly to adjust the so-far played label (redrawing the whole +/** @brief Adjust track played label + * + * Called regularly to adjust the so-far played label (redrawing the whole * queue once a second makes disobedience occupy >10% of the CPU on my Athlon * which is ureasonable expensive) */ static gboolean adjust_sofar(gpointer attribute((unused)) data) { @@ -1180,47 +1354,65 @@ static gboolean adjust_sofar(gpointer attribute((unused)) data) { return TRUE; } -/* Popup menu for the queue. Put the properties first so that finger trouble - * is less dangerous. */ -static struct menuitem queue_menu[] = { +/** @brief Popup menu for the queue + * + * Properties first so that finger trouble is less dangerous. */ +static struct queue_menuitem queue_menu[] = { { "Track properties", properties_activate, properties_sensitive, 0, 0 }, { "Select all tracks", selectall_activate, selectall_sensitive, 0, 0 }, + { "Deselect all tracks", selectnone_activate, selectnone_sensitive, 0, 0 }, { "Scratch track", scratch_activate, scratch_sensitive, 0, 0 }, { "Remove track from queue", remove_activate, remove_sensitive, 0, 0 }, { 0, 0, 0, 0, 0 } }; +/** @brief Called whenever @ref DISORDER_PLAYING or @ref DISORDER_TRACK_PAUSED changes + * + * We monitor pause/resume as well as whether the track is playing in order to + * keep the time played so far up to date correctly. See playing_completed(). + */ +static void playing_changed(const char attribute((unused)) *event, + void attribute((unused)) *evendata, + void attribute((unused)) *callbackdata) { + D(("playing_changed")); + gtk_label_set_text(GTK_LABEL(report_label), "updating playing track"); + disorder_eclient_playing(client, playing_completed, 0); +} + +/** @brief Create the queue widget */ GtkWidget *queue_widget(void) { D(("queue_widget")); /* Arrange periodic update of the so-far played field */ g_timeout_add(1000/*ms*/, adjust_sofar, 0); + /* Arrange a callback whenever the playing state changes */ + event_register("playing-changed", playing_changed, 0); + event_register("pause-changed", playing_changed, 0); + event_register("queue-changed", queue_changed, 0); /* We pass choose_update() as our notify function since the choose screen * marks tracks that are playing/in the queue. */ return queuelike(&ql_queue, fixup_queue, choose_update, queue_menu, - "queue"); + maincolumns, NMAINCOLUMNS); } -void queue_update(void) { - struct callbackdata *cbd; - - D(("queue_update")); - cbd = xmalloc(sizeof *cbd); - cbd->onerror = 0; - cbd->u.ql = &ql_queue; +/** @brief Arrange an update of the queue widget + * + * Called when a track is added to the queue, removed from the queue (by user + * cmmand or because it is to be played) or moved within the queue + */ +void queue_changed(const char attribute((unused)) *event, + void attribute((unused)) *eventdata, + void attribute((unused)) *callbackdata) { + D(("queue_changed")); gtk_label_set_text(GTK_LABEL(report_label), "updating queue"); - disorder_eclient_queue(client, queuelike_completed, cbd); -} - -void playing_update(void) { - D(("playing_update")); - gtk_label_set_text(GTK_LABEL(report_label), "updating playing track"); - disorder_eclient_playing(client, playing_completed, 0); + disorder_eclient_queue(client, queuelike_completed, &ql_queue); } /* Recently played tracks -------------------------------------------------- */ +/** @brief Fix up the recently played list + * + * It's in the wrong order! TODO fix this globally */ static struct queue_entry *fixup_recent(struct queue_entry *q) { - /* 'recent' is in the wrong order. TODO: globally fix this! */ struct queue_entry *qr = 0, *qn; D(("fixup_recent")); @@ -1237,38 +1429,114 @@ static struct queue_entry *fixup_recent(struct queue_entry *q) { return qr; } -static struct menuitem recent_menu[] = { +/** @brief Pop-up menu for recently played list */ +static struct queue_menuitem recent_menu[] = { { "Track properties", properties_activate, properties_sensitive,0, 0 }, { "Select all tracks", selectall_activate, selectall_sensitive, 0, 0 }, + { "Deselect all tracks", selectnone_activate, selectnone_sensitive, 0, 0 }, { 0, 0, 0, 0, 0 } }; +/** @brief Create the recently-played list */ GtkWidget *recent_widget(void) { D(("recent_widget")); - return queuelike(&ql_recent, fixup_recent, 0, recent_menu, "recent"); + event_register("recent-changed", + recent_changed, + 0); + return queuelike(&ql_recent, fixup_recent, 0, recent_menu, + maincolumns, NMAINCOLUMNS); } -void recent_update(void) { - struct callbackdata *cbd; - - D(("recent_update")); - cbd = xmalloc(sizeof *cbd); - cbd->onerror = 0; - cbd->u.ql = &ql_recent; +/** @brief Update the recently played list + * + * Called whenever a track is added to it or removed from it. + */ +static void recent_changed(const char attribute((unused)) *event, + void attribute((unused)) *eventdata, + void attribute((unused)) *callbackdata) { + D(("recent_changed")); gtk_label_set_text(GTK_LABEL(report_label), "updating recently played list"); - disorder_eclient_recent(client, queuelike_completed, cbd); + disorder_eclient_recent(client, queuelike_completed, &ql_recent); +} + +/* Newly added tracks ------------------------------------------------------ */ + +/** @brief Pop-up menu for recently played list */ +static struct queue_menuitem added_menu[] = { + { "Track properties", properties_activate, properties_sensitive, 0, 0 }, + { "Play track", play_activate, play_sensitive, 0, 0 }, + { "Select all tracks", selectall_activate, selectall_sensitive, 0, 0 }, + { "Deselect all tracks", selectnone_activate, selectnone_sensitive, 0, 0 }, + { 0, 0, 0, 0, 0 } +}; + +/** @brief Create the newly-added list */ +GtkWidget *added_widget(void) { + D(("added_widget")); + event_register("added-changed", added_changed, 0); + return queuelike(&ql_added, 0/*fixup*/, 0/*notify*/, added_menu, + addedcolumns, NADDEDCOLUMNS); +} + +/** @brief Called with an updated list of newly-added tracks + * + * This is called with a raw list of track names but the rest of @ref + * disobedience/queue.c requires @ref queue_entry structures with a valid and + * unique @c id field. This function fakes it. + */ +static void new_completed(void *v, + const char *error, + int nvec, char **vec) { + if(error) + popup_protocol_error(0, error); + else { + struct queuelist *ql = v; + /* Convert the vector result to a queue linked list */ + struct queue_entry *q, *qh, *qlast = 0, **qq = &qh; + int n; + + for(n = 0; n < nvec; ++n) { + q = xmalloc(sizeof *q); + q->prev = qlast; + q->track = vec[n]; + q->id = vec[n]; + *qq = q; + qq = &q->next; + qlast = q; + } + *qq = 0; + queuelike_completed(ql, 0, qh); + } +} + +/** @brief Update the newly-added list */ +static void added_changed(const char attribute((unused)) *event, + void attribute((unused)) *eventdata, + void attribute((unused)) *callbackdata) { + D(("added_changed")); + + gtk_label_set_text(GTK_LABEL(report_label), + "updating newly added track list"); + disorder_eclient_new_tracks(client, new_completed, 0/*all*/, &ql_added); } /* Main menu plumbing ------------------------------------------------------ */ static int queue_properties_sensitive(GtkWidget *w) { - return !!queue_count_selected(g_object_get_data(G_OBJECT(w), "queue")); + return (!!queue_count_selected(g_object_get_data(G_OBJECT(w), "queue")) + && (disorder_eclient_state(client) & DISORDER_CONNECTED)); } static int queue_selectall_sensitive(GtkWidget *w) { return !!queue_count_entries(g_object_get_data(G_OBJECT(w), "queue")); } +static int queue_selectnone_sensitive(GtkWidget *w) { + struct queuelike *const ql = g_object_get_data(G_OBJECT(w), "queue"); + + return hash_count(ql->selection) != 0; +} + static void queue_properties_activate(GtkWidget *w) { queue_properties(g_object_get_data(G_OBJECT(w), "queue")); } @@ -1277,15 +1545,22 @@ static void queue_selectall_activate(GtkWidget *w) { queue_select_all(g_object_get_data(G_OBJECT(w), "queue")); } +static void queue_selectnone_activate(GtkWidget *w) { + queue_select_none(g_object_get_data(G_OBJECT(w), "queue")); +} + static const struct tabtype tabtype_queue = { queue_properties_sensitive, queue_selectall_sensitive, + queue_selectnone_sensitive, queue_properties_activate, queue_selectall_activate, + queue_selectnone_activate, }; /* Other entry points ------------------------------------------------------ */ +/** @brief Return nonzero if @p track is in the queue */ int queued(const char *track) { struct queue_entry *q;