From c133bd3c7f6d982e5447acf61e0b32853e34459b Mon Sep 17 00:00:00 2001 Message-Id: From: Mark Wooding Date: Tue, 10 Jun 2008 21:13:25 +0100 Subject: [PATCH 1/1] Rewrite queue/recent/added to use native list widget. Organization: Straylight/Edgeware From: Richard Kettlewell What works: - the tracks are displayed correctly - keyboard scrolling now works properly (at last!) What doesn't work: - popup menu is broken - edit menu is unimplemented - drag+drop is unimplemented - columns are not resizable - ellipsization is off - no horizontal banding - probably lots of other things --- disobedience/Makefile.am | 6 +- disobedience/added.c | 106 +++ disobedience/disobedience.c | 1 + disobedience/menu.c | 1 + disobedience/queue-generic.c | 418 +++++++++ disobedience/queue-generic.h | 152 ++++ disobedience/queue-menu.c | 164 ++++ disobedience/queue.c | 1591 ++-------------------------------- disobedience/recent.c | 103 +++ 9 files changed, 1032 insertions(+), 1510 deletions(-) create mode 100644 disobedience/added.c create mode 100644 disobedience/queue-generic.c create mode 100644 disobedience/queue-generic.h create mode 100644 disobedience/queue-menu.c create mode 100644 disobedience/recent.c diff --git a/disobedience/Makefile.am b/disobedience/Makefile.am index 5b0c03a..93d7096 100644 --- a/disobedience/Makefile.am +++ b/disobedience/Makefile.am @@ -26,9 +26,9 @@ AM_CFLAGS=$(GLIB_CFLAGS) $(GTK_CFLAGS) PNGS:=$(shell export LC_COLLATE=C;echo ${top_srcdir}/images/*.png) disobedience_SOURCES=disobedience.h disobedience.c client.c queue.c \ - choose.c misc.c control.c properties.c menu.c \ - log.c progress.c login.c rtp.c help.c \ - ../lib/memgc.c settings.c users.c + recent.c added.c queue-generic.c queue-generic.h queue-menu.c \ + choose.c misc.c control.c properties.c menu.c log.c progress.c \ + login.c rtp.c help.c ../lib/memgc.c settings.c users.c disobedience_LDADD=../lib/libdisorder.a $(LIBPCRE) $(LIBGC) $(LIBGCRYPT) \ $(LIBASOUND) $(COREAUDIO) $(LIBDB) disobedience_LDFLAGS=$(GTK_LIBS) diff --git a/disobedience/added.c b/disobedience/added.c new file mode 100644 index 0000000..9373af3 --- /dev/null +++ b/disobedience/added.c @@ -0,0 +1,106 @@ +/* + * This file is part of DisOrder + * 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 + * the Free Software Foundation; either version 2 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. + * + * 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 + */ +#include "disobedience.h" +#include "queue-generic.h" + +/** @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-generic.c requires @ref queue_entry structures + * with a valid and unique @c id field. This function fakes it. + */ +static void added_completed(void attribute((unused)) *v, + const char *error, + int nvec, char **vec) { + if(error) { + popup_protocol_error(0, error); + return; + } + /* 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]; /* unique because a track is only added once */ + *qq = q; + qq = &q->next; + qlast = q; + } + *qq = 0; + ql_new_queue(&ql_added, qh); + /* Tell anyone who cares */ + event_raise("added-list-changed", 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, added_completed, 0/*all*/, 0); +} + +/** @brief Called at startup */ +static void added_init(void) { + event_register("added-changed", added_changed, 0); +} + +/** @brief Columns for the new tracks list */ +static const struct queue_column added_columns[] = { + { "Artist", column_namepart, "artist", 0 }, + { "Album", column_namepart, "album", 0 }, + { "Title", column_namepart, "title", 0 }, + { "Length", column_length, 0, 1 } +}; + +/** @brief Pop-up menu for new tracks list */ +static struct queue_menuitem added_menuitems[] = { + { "Track properties", ql_properties_activate, ql_properties_sensitive, 0, 0 }, + { "Play track", ql_play_activate, ql_play_sensitive, 0, 0 }, + { "Select all tracks", ql_selectall_activate, ql_selectall_sensitive, 0, 0 }, + { "Deselect all tracks", ql_selectnone_activate, ql_selectnone_sensitive, 0, 0 }, +}; + +struct queuelike ql_added = { + .init = added_init, + .columns = added_columns, + .ncolumns = sizeof added_columns / sizeof *added_columns, + .menuitems = added_menuitems, + .nmenuitems = sizeof added_menuitems / sizeof *added_menuitems, +}; + +GtkWidget *added_widget(void) { + return init_queuelike(&ql_added); +} + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +fill-column:79 +indent-tabs-mode:nil +End: +*/ diff --git a/disobedience/disobedience.c b/disobedience/disobedience.c index b1c801f..b9866f7 100644 --- a/disobedience/disobedience.c +++ b/disobedience/disobedience.c @@ -125,6 +125,7 @@ static void tab_switched(GtkNotebook *notebook, menu_update(page_num); GtkWidget *const tab = gtk_notebook_get_nth_page(notebook, page_num); const struct tabtype *const t = g_object_get_data(G_OBJECT(tab), "type"); + if(!t) return; /* TODO */ if(t->selected) t->selected(); } diff --git a/disobedience/menu.c b/disobedience/menu.c index b2106ef..eb20988 100644 --- a/disobedience/menu.c +++ b/disobedience/menu.c @@ -125,6 +125,7 @@ void menu_update(int page) { page < 0 ? gtk_notebook_current_page(GTK_NOTEBOOK(tabs)) : page); const struct tabtype *t = g_object_get_data(G_OBJECT(tab), "type"); + if(!t) return; /* TODO */ assert(t != 0); gtk_widget_set_sensitive(properties_widget, (t->properties_sensitive(tab) diff --git a/disobedience/queue-generic.c b/disobedience/queue-generic.c new file mode 100644 index 0000000..37e0c30 --- /dev/null +++ b/disobedience/queue-generic.c @@ -0,0 +1,418 @@ +/* + * This file is part of DisOrder + * 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 + * the Free Software Foundation; either version 2 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. + * + * 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 + */ +/** @file disobedience/queue-generic.c + * @brief Queue widgets + * + * This file provides contains code shared between all the queue-like + * widgets - the queue, the recent list and the added tracks list. + * + * This code is in the process of being rewritten to use the native list + * widget. + * + * There are three @ref queuelike objects: @ref ql_queue, @ref + * ql_recent and @ref ql_added. Each has an associated queue linked + * list and a list store containing the contents. + * + * When new contents turn up we rearrange the list store accordingly. + * + * 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 "queue-generic.h" + +static struct queuelike *const queuelikes[] = { + &ql_queue, &ql_recent, &ql_added +}; +#define NQUEUELIKES (sizeof queuelikes / sizeof *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; + fprintf(stderr, "namepart_lookups_outstanding -> %d\n", 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; +} + +/* Column formatting -------------------------------------------------------- */ + +/** @brief Format the 'when' column */ +const char *column_when(const struct queue_entry *q, + const char attribute((unused)) *data) { + char when[64]; + struct tm tm; + time_t t; + + D(("column_when")); + switch(q->state) { + case playing_isscratch: + case playing_unplayed: + case playing_random: + t = q->expected; + break; + case playing_failed: + case playing_no_player: + case playing_ok: + case playing_scratched: + case playing_started: + case playing_paused: + case playing_quitting: + t = q->played; + break; + default: + t = 0; + break; + } + if(t) + strftime(when, sizeof when, "%H:%M", localtime_r(&t, &tm)); + else + when[0] = 0; + return xstrdup(when); +} + +/** @brief Format the 'who' column */ +const char *column_who(const struct queue_entry *q, + const char attribute((unused)) *data) { + D(("column_who")); + return q->submitter ? q->submitter : ""; +} + +/** @brief Format one of the track name columns */ +const char *column_namepart(const struct queue_entry *q, + const char *data) { + D(("column_namepart")); + return namepart(q->track, "display", data); +} + +/** @brief Format the length column */ +const char *column_length(const struct queue_entry *q, + const char attribute((unused)) *data) { + D(("column_length")); + long l; + time_t now; + char *played = 0, *length = 0; + + /* Work out what to say for the length */ + l = getlength(q->track); + if(l > 0) + byte_xasprintf(&length, "%ld:%02ld", l / 60, l % 60); + else + byte_xasprintf(&length, "?:??"); + /* For the currently playing track we want to report how much of the track + * has been played */ + if(q == playing_track) { + /* log_state() arranges that we re-get the playing data whenever the + * pause/resume state changes */ + if(last_state & DISORDER_TRACK_PAUSED) + l = playing_track->sofar; + else { + time(&now); + l = playing_track->sofar + (now - last_playing); + } + byte_xasprintf(&played, "%ld:%02ld/%s", l / 60, l % 60, length); + return played; + } else + return length; +} + +/* Selection processing ---------------------------------------------------- */ + +/** @brief Stash the selection of @c ql->view + * @param ql Queuelike of interest + * @return Hash representing current selection + */ +static hash *save_selection(struct queuelike *ql) { + hash *h = hash_new(1); + GtkTreeIter iter[1]; + gtk_tree_model_get_iter_first(GTK_TREE_MODEL(ql->store), iter); + for(const struct queue_entry *q = ql->q; q; q = q->next) { + if(gtk_tree_selection_iter_is_selected(ql->selection, iter)) + hash_add(h, q->id, "", HASH_INSERT); + gtk_tree_model_iter_next(GTK_TREE_MODEL(ql->store), iter); + } + return h; +} + +/** @brief Called from restore_selection() */ +static int restore_selection_callback(const char *key, + void attribute((unused)) *value, + void *u) { + const struct queuelike *const ql = u; + GtkTreeIter iter[1]; + const struct queue_entry *q; + + gtk_tree_model_get_iter_first(GTK_TREE_MODEL(ql->store), iter); + for(q = ql->q; q && strcmp(key, q->id); q = q->next) + gtk_tree_model_iter_next(GTK_TREE_MODEL(ql->store), iter); + if(q) + gtk_tree_selection_select_iter(ql->selection, iter); + /* There might be gaps if things have disappeared */ + return 0; +} + +/** @brief Restore selection of @c ql->view + * @param ql Queuelike of interest + * @param h Hash representing former selection + */ +static void restore_selection(struct queuelike *ql, hash *h) { + hash_foreach(h, restore_selection_callback, ql); +} + +/* List store maintenance -------------------------------------------------- */ + +/** @brief Update one row of a list store + * @param q Queue entry + * @param iter Iterator referring to row or NULL to work it out + */ +void ql_update_row(struct queue_entry *q, + GtkTreeIter *iter) { + const struct queuelike *const ql = q->ql; + + D(("ql_update_row")); + /* If no iter was supplied, work it out */ + GtkTreeIter my_iter[1]; + if(!iter) { + gtk_tree_model_get_iter_first(GTK_TREE_MODEL(ql->store), my_iter); + struct queue_entry *qq; + for(qq = ql->q; qq && q != qq; qq = qq->next) + gtk_tree_model_iter_next(GTK_TREE_MODEL(ql->store), my_iter); + if(!qq) + return; + iter = my_iter; + } + /* Update all the columns */ + for(int col = 0; col < ql->ncolumns; ++col) + gtk_list_store_set(ql->store, iter, + col, ql->columns[col].value(q, + ql->columns[col].data), + -1); +} + +/** @brief Update the list store + * @param ql Queuelike to update + * + * Called when new namepart data is available (and initially). Doesn't change + * the rows, just updates the cell values. + */ +void ql_update_list_store(struct queuelike *ql) { + D(("ql_update_list_store")); + GtkTreeIter iter[1]; + gtk_tree_model_get_iter_first(GTK_TREE_MODEL(ql->store), iter); + for(struct queue_entry *q = ql->q; q; q = q->next) { + ql_update_row(q, iter); + gtk_tree_model_iter_next(GTK_TREE_MODEL(ql->store), iter); + } +} + +/** @brief Reset the list store + * @param ql Queuelike to reset + * + * Throws away all rows and starts again. Used when new queue contents arrives + * from the server. + */ +void ql_new_queue(struct queuelike *ql, + struct queue_entry *newq) { + D(("ql_new_queue")); + hash *h = save_selection(ql); + /* Clear out old contents */ + gtk_list_store_clear(ql->store); + /* Put in new rows */ + ql->q = newq; + for(struct queue_entry *q = ql->q; q; q = q->next) { + /* Tell every queue entry which queue owns it */ + q->ql = ql; + /* Add a row */ + GtkTreeIter iter[1]; + gtk_list_store_append(ql->store, iter); + /* Update the row contents */ + ql_update_row(q, iter); + } + restore_selection(ql, h); + /* Update menu sensitivity */ + menu_update(-1); +} + +/** @brief Initialize a @ref queuelike */ +GtkWidget *init_queuelike(struct queuelike *ql) { + D(("init_queuelike")); + /* Create the list store */ + GType *types = xcalloc(ql->ncolumns, sizeof (GType)); + for(int n = 0; n < ql->ncolumns; ++n) + types[n] = G_TYPE_STRING; + ql->store = gtk_list_store_newv(ql->ncolumns, types); + + /* Create the view */ + ql->view = gtk_tree_view_new_with_model(GTK_TREE_MODEL(ql->store)); + + /* Create cell renderers and label columns */ + for(int n = 0; n < ql->ncolumns; ++n) { + GtkCellRenderer *r = gtk_cell_renderer_text_new(); + GtkTreeViewColumn *c = gtk_tree_view_column_new_with_attributes + (ql->columns[n].name, + r, + "text", n, + (char *)0); + gtk_tree_view_append_column(GTK_TREE_VIEW(ql->view), c); + } + + /* The selection should support multiple things being selected */ + ql->selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(ql->view)); + gtk_tree_selection_set_mode(ql->selection, GTK_SELECTION_MULTIPLE); + + /* Remember what the view belongs to */ + //g_object_set_data(G_OBJECT(ql->view), "type", (void *)&tabtype_queue); + /* TODO tabtype */ + g_object_set_data(G_OBJECT(ql->view), "queue", ql); + /* Catch button presses */ + g_signal_connect(ql->view, "button-release-event", + G_CALLBACK(ql_button_release), ql); + + /* TODO style? */ + /* TODO drag+drop */ + + ql->init(); + + return scroll_widget(ql->view); +} + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +fill-column:79 +indent-tabs-mode:nil +End: +*/ diff --git a/disobedience/queue-generic.h b/disobedience/queue-generic.h new file mode 100644 index 0000000..7ee93b6 --- /dev/null +++ b/disobedience/queue-generic.h @@ -0,0 +1,152 @@ +/* + * This file is part of DisOrder + * 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 + * the Free Software Foundation; either version 2 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. + * + * 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 + */ +#ifndef QUEUE_GENERIC_H +#define QUEUE_GENERIC_H + +/** @brief Definition of a column */ +struct queue_column { + /** @brief Column name */ + const char *name; + + /** @brief Compute value for this column */ + const char *(*value)(const struct queue_entry *q, + const char *data); + + /** @brief Passed to value() */ + const char *data; + + /** @brief TODO */ + gfloat xalign; +}; + +/** @brief An item in the queue's popup menu */ +struct queue_menuitem { + /** @brief Menu item name */ + const char *name; + + /** @brief Called to activate the menu item */ + void (*activate)(GtkMenuItem *menuitem, + gpointer user_data); + + /** @brief Called to determine whether the menu item is usable. + * + * Returns @c TRUE if it should be sensitive and @c FALSE otherwise. + */ + int (*sensitive)(struct queuelike *ql); + + /** @brief Signal handler ID */ + gulong handlerid; + + /** @brief Widget for menu item */ + GtkWidget *w; +}; + +/** @brief Definition of a queue-like window */ +struct queuelike { + + /* Things filled in by the caller: */ + + /** @brief Initialization function */ + void (*init)(void); + + /** @brief Columns */ + const struct queue_column *columns; + + /** @brief Number of columns in this queuelike */ + int ncolumns; + + /** @brief Items for popup menu */ + struct queue_menuitem *menuitems; + + /** @brief Number of menu items */ + int nmenuitems; + + /* Dynamic state: */ + + /** @brief The head of the queue */ + struct queue_entry *q; + + /* Things created by the implementation: */ + + /** @brief The list store */ + GtkListStore *store; + + /** @brief The tree view */ + GtkWidget *view; + + /** @brief The selection */ + GtkTreeSelection *selection; + + /** @brief The popup menu */ + GtkWidget *menu; +}; + +extern struct queuelike ql_queue; +extern struct queuelike ql_recent; +extern struct queuelike ql_added; + +extern time_t last_playing; + +int ql_selectall_sensitive(struct queuelike *ql); +void ql_selectall_activate(GtkMenuItem *menuitem, + gpointer user_data); +int ql_selectnone_sensitive(struct queuelike *ql); +void ql_selectnone_activate(GtkMenuItem *menuitem, + gpointer user_data); +int ql_properties_sensitive(struct queuelike *ql); +void ql_properties_activate(GtkMenuItem *menuitem, + gpointer user_data); +int ql_scratch_sensitive(struct queuelike *ql); +void ql_scratch_activate(GtkMenuItem *menuitem, + gpointer user_data); +int ql_remove_sensitive(struct queuelike *ql); +void ql_remove_activate(GtkMenuItem *menuitem, + gpointer user_data); +int ql_play_sensitive(struct queuelike *ql); +void ql_play_activate(GtkMenuItem *menuitem, + gpointer user_data); +gboolean ql_button_release(GtkWidget *widget, + GdkEventButton *event, + gpointer user_data); +GtkWidget *init_queuelike(struct queuelike *ql); +void ql_update_list_store(struct queuelike *ql) ; +void ql_update_row(struct queue_entry *q, + GtkTreeIter *iter); +void ql_new_queue(struct queuelike *ql, + struct queue_entry *newq); +const char *column_when(const struct queue_entry *q, + const char *data); +const char *column_who(const struct queue_entry *q, + const char *data); +const char *column_namepart(const struct queue_entry *q, + const char *data); +const char *column_length(const struct queue_entry *q, + const char *data); + +#endif /* QUEUE_GENERIC_H */ + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +fill-column:79 +indent-tabs-mode:nil +End: +*/ diff --git a/disobedience/queue-menu.c b/disobedience/queue-menu.c new file mode 100644 index 0000000..8734cf6 --- /dev/null +++ b/disobedience/queue-menu.c @@ -0,0 +1,164 @@ +/* + * This file is part of DisOrder + * 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 + * the Free Software Foundation; either version 2 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. + * + * 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 + */ +#include "disobedience.h" +#include "queue-generic.h" + +/* Select All */ + +int ql_selectall_sensitive(struct queuelike *ql) { + return !!ql->q; +} + +void ql_selectall_activate(GtkMenuItem attribute((unused)) *menuitem, + gpointer user_data) { + struct queuelike *ql = user_data; + + gtk_tree_selection_select_all(ql->selection); +} + +/* Select None */ + +int ql_selectnone_sensitive(struct queuelike *ql) { + return gtk_tree_selection_count_selected_rows(ql->selection) > 0; +} + +void ql_selectnone_activate(GtkMenuItem attribute((unused)) *menuitem, + gpointer user_data) { + struct queuelike *ql = user_data; + + gtk_tree_selection_unselect_all(ql->selection); +} + +/* Properties */ + +int ql_properties_sensitive(struct queuelike *ql) { + return gtk_tree_selection_count_selected_rows(ql->selection) > 0; +} + +void ql_properties_activate(GtkMenuItem attribute((unused)) *menuitem, + gpointer user_data) { + struct queuelike *ql = user_data; + struct vector v[1]; + GtkTreeIter iter[1]; + + vector_init(v); + gtk_tree_model_get_iter_first(GTK_TREE_MODEL(ql->store), iter); + for(struct queue_entry *q = ql->q; q; q = q->next) { + if(gtk_tree_selection_iter_is_selected(ql->selection, iter)) + vector_append(v, (char *)q->track); + gtk_tree_model_iter_next(GTK_TREE_MODEL(ql->store), iter); + } + if(v->nvec) + properties(v->nvec, (const char **)v->vec); +} + +/* Scratch */ + +int ql_scratch_sensitive(struct queuelike attribute((unused)) *ql) { + return !!playing_track; +} + +void ql_scratch_activate(GtkMenuItem attribute((unused)) *menuitem, + gpointer attribute((unused)) user_data) { + /* TODO */ +} + +/* Remove */ + +int ql_remove_sensitive(struct queuelike *ql) { + return gtk_tree_selection_count_selected_rows(ql->selection) > 0; + /* TODO ... but not if only selected track is playing track */ +} + +void ql_remove_activate(GtkMenuItem attribute((unused)) *menuitem, + gpointer attribute((unused)) user_data) { + /* TODO */ +} + +/* Play */ + +int ql_play_sensitive(struct queuelike *ql) { + return gtk_tree_selection_count_selected_rows(ql->selection) > 0; +} + +void ql_play_activate(GtkMenuItem attribute((unused)) *menuitem, + gpointer attribute((unused)) user_data) { + /* TODO */ +} + + +/** @brief Create @c ql->menu if it does not already exist */ +static void ql_create_menu(struct queuelike *ql) { + if(ql->menu) + return; + ql->menu = gtk_menu_new(); + g_signal_connect(ql->menu, "destroy", + G_CALLBACK(gtk_widget_destroyed), &ql->menu); + for(int n = 0; n < ql->nmenuitems; ++n) { + ql->menuitems[n].w = gtk_menu_item_new_with_label(ql->menuitems[n].name); + gtk_menu_attach(GTK_MENU(ql->menu), ql->menuitems[n].w, 0, 1, n, n + 1); + } + set_tool_colors(ql->menu); +} + +/** @brief Configure @c ql->menu */ +static void ql_configure_menu(struct queuelike *ql) { + /* Set the sensitivity of each menu item and (re-)establish the signal + * handlers */ + for(int n = 0; n < ql->nmenuitems; ++n) { + if(ql->menuitems[n].handlerid) + g_signal_handler_disconnect(ql->menuitems[n].w, + ql->menuitems[n].handlerid); + gtk_widget_set_sensitive(ql->menuitems[n].w, + ql->menuitems[n].sensitive(ql)); + ql->menuitems[n].handlerid = g_signal_connect + (ql->menuitems[n].w, "activate", + G_CALLBACK(ql->menuitems[n].activate), ql); + } +} + +/** @brief Called when a button is released over a queuelike */ +gboolean ql_button_release(GtkWidget attribute((unused)) *widget, + GdkEventButton *event, + gpointer user_data) { + struct queuelike *ql = user_data; + + if(event->type == GDK_BUTTON_PRESS + && event->button == 3) { + /* Right button click. */ + ql_create_menu(ql); + ql_configure_menu(ql); + gtk_widget_show_all(ql->menu); + gtk_menu_popup(GTK_MENU(ql->menu), 0, 0, 0, 0, + event->button, event->time); + return TRUE; /* hide the click from other widgets */ + } + + return FALSE; +} + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +fill-column:79 +indent-tabs-mode:nil +End: +*/ diff --git a/disobedience/queue.c b/disobedience/queue.c index 026c492..18d9d8b 100644 --- a/disobedience/queue.c +++ b/disobedience/queue.c @@ -17,1556 +17,133 @@ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 * USA */ -/** @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
- *     titlecells[col]                 eventbox (made by wrap_queue_cell)
- *      titlecells[col]->child         label (from columns[])
- *   mainscroll
- *    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 - * match, forcing the title and the queue to pan in sync but allowing the queue - * to scroll independently. - * - * Whenever the queue changes everything below mainlayout is thrown away and - * reconstructed from scratch. Name lookups are cached, so this doesn't imply - * lots of disorder protocol traffic. - * - * The last cell on each row is the padding cell, and this extends ridiculously - * far to the right. (Can we do better?) - * - * When drag and drop is active we create extra eventboxes to act as dropzones. - * 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); -WT(event_box); -WT(menu); -WT(menu_item); -WT(layout); -WT(vbox); - -struct queuelike; - -static void add_drag_targets(struct queuelike *ql); -static void remove_drag_targets(struct queuelike *ql); -static void redisplay_queue(struct queuelike *ql); -static GtkWidget *column_when(const struct queuelike *ql, - const struct queue_entry *q, - const char *data); -static GtkWidget *column_who(const struct queuelike *ql, - const struct queue_entry *q, - const char *data); -static GtkWidget *column_namepart(const struct queuelike *ql, - const struct queue_entry *q, - const char *data); -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 */ - -static const GtkTargetEntry dragtargets[] = { - { (char *)"disobedience-queue", GTK_TARGET_SAME_APP, 0 } -}; -#define NDRAGTARGETS (int)(sizeof dragtargets / sizeof *dragtargets) - -/** @brief Definition of a column */ -struct column { - const char *name; /* Column name */ - GtkWidget *(*widget)(const struct queuelike *ql, - const struct queue_entry *q, - const char *data); /* Make a label for this column */ - const char *data; /* Data to pass to widget() */ - gfloat xalign; /* Alignment of the label */ -}; - -/** @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 }, - { "Album", column_namepart, "album", 0 }, - { "Title", column_namepart, "title", 0 }, - { "Length", column_length, 0, 1 } -}; - -/** @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; /**< @brief which queue we're dealing with */ - struct queue_entry *q; /**< @brief hovered entry or 0 */ -}; - -/** @brief An item in the queue's popup menu */ -struct queue_menuitem { - /** @brief Menu item name */ - const char *name; - - /** @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); - - /** @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 queue_menuitem *m, - struct queue_entry *q); - - /** @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 { - /** @brief Called when an update completes */ - void (*notify)(void); - - /** @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); +#include "queue-generic.h" - /* Widgets */ - 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 */ +/** @brief The actual queue */ +static struct queue_entry *actual_queue; +static struct queue_entry *actual_playing_track; - /* State */ - 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 */ +/** @brief The playing track */ +struct queue_entry *playing_track; - const struct column *columns; /**< @brief Table of columns */ - int ncolumns; /**< @brief Number of columns */ -}; - -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 */ -struct queue_entry *playing_track; /**< @brief currently 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 }; -static const struct cache_type cachetype_integer = { 3600 }; -static GtkWidget *playing_length_label; - -/* Debugging --------------------------------------------------------------- */ - -#if 0 -static void describe_widget(const char *name, GtkWidget *w, int indent) { - int ww, wh, wx, wy; - - if(name) - fprintf(stderr, "%*s[%s]: '%s'\n", indent, "", - name, gtk_widget_get_name(w)); - gdk_window_get_position(w->window, &wx, &wy); - gdk_drawable_get_size(GDK_DRAWABLE(w->window), &ww, &wh); - fprintf(stderr, "%*s window %p: %dx%d at %dx%d\n", - indent, "", w->window, ww, wh, wx, wy); -} - -static void dump_layout(const struct queuelike *ql) { - GtkWidget *w; - char s[20]; - int row, col; - const struct queue_entry *q; - - 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 < 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) - describe_widget(0, w, 3); - } -} -#endif +/** @brief When we last got the playing track */ +time_t last_playing; -/* Track detail lookup ----------------------------------------------------- */ +/** @brief Called when either the actual queue or the playing track change */ +static void queue_playing_changed(void) { + struct queue_entry *q = xmalloc(sizeof *q); -/** @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) { - redisplay_queue(&ql_queue); - redisplay_queue(&ql_recent); - redisplay_queue(&ql_added); - namepart_completions_deferred = 0; - } + *q = *actual_playing_track; + q->next = actual_queue; + playing_track = q; + time(&last_playing); /* for column_length() */ + ql_new_queue(&ql_queue, q); + /* Tell anyone who cares */ + event_raise("queue-list-changed", q); + event_raise("playing-track-changed", q); } -/** @brief Called when a namepart lookup has completed */ -static void namepart_completed(void *v, const char *error, const char *value) { +/** @brief Update the queue itself */ +static void queue_completed(void attribute((unused)) *v, + const char *error, + struct queue_entry *q) { if(error) { - gtk_label_set_text(GTK_LABEL(report_label), error); - } else { - const char *key = v; - - cache_put(&cachetype_string, key, value); - ++namepart_completions_deferred; - } - namepart_completed_or_failed(); -} - -/** @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(); -} - -/** @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) { - ++namepart_lookups_outstanding; - disorder_eclient_namepart(client, namepart_completed, - track, context, part, (void *)key); -} - -/** @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) { - 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...")); - /* stick a value in the cache so we don't issue another lookup if we - * revisit */ - cache_put(&cachetype_string, key, value = "?"); - namepart_fill(track, context, part, key); - } - 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; - static const long bogus = -1; - - D(("getlength %s", track)); - byte_xasprintf(&key, "length track=%s", track); - value = cache_get(&cachetype_integer, key); - if(!value) { - D(("deferring..."));; - cache_put(&cachetype_integer, key, value = &bogus); - ++namepart_lookups_outstanding; - disorder_eclient_length(client, length_completed, track, key); - } - return *value; -} - -/* Column constructors ----------------------------------------------------- */ - -/** @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) { - char when[64]; - struct tm tm; - time_t t; - - D(("column_when")); - switch(q->state) { - case playing_isscratch: - case playing_unplayed: - case playing_random: - t = q->expected; - break; - case playing_failed: - case playing_no_player: - case playing_ok: - case playing_scratched: - case playing_started: - case playing_paused: - case playing_quitting: - t = q->played; - break; - default: - t = 0; - break; - } - if(t) - strftime(when, sizeof when, "%H:%M", localtime_r(&t, &tm)); - else - when[0] = 0; - NW(label); - return gtk_label_new(when); -} - -/** @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) { - D(("column_who")); - NW(label); - return gtk_label_new(q->submitter ? q->submitter : ""); -} - -/** @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(truncate_for_display(namepart(q->track, "display", data), - config->short_display)); -} - -/** @brief Compute the length field */ -static const char *text_length(const struct queue_entry *q) { - long l; - time_t now; - char *played = 0, *length = 0; - - /* Work out what to say for the length */ - l = getlength(q->track); - if(l > 0) - byte_xasprintf(&length, "%ld:%02ld", l / 60, l % 60); - else - byte_xasprintf(&length, "?:??"); - /* For the currently playing track we want to report how much of the track - * has been played */ - if(q == playing_track) { - /* log_state() arranges that we re-get the playing data whenever the - * pause/resume state changes */ - if(last_state & DISORDER_TRACK_PAUSED) - l = playing_track->sofar; - else { - time(&now); - l = playing_track->sofar + (now - last_playing); - } - byte_xasprintf(&played, "%ld:%02ld/%s", l / 60, l % 60, length); - return played; - } else - return length; -} - -/** @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) { - D(("column_length")); - if(q == playing_track) { - assert(!playing_length_label); - NW(label); - playing_length_label = gtk_label_new(text_length(q)); - /* Zot playing_length_label when it is destroyed */ - g_signal_connect(playing_length_label, "destroy", - G_CALLBACK(gtk_widget_destroyed), &playing_length_label); - return playing_length_label; - } else { - NW(label); - return gtk_label_new(text_length(q)); - } - -} - -/** @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; - - D(("update_queue")); - /* Propagate last_click across the change */ - if(ql->last_click) { - for(q = newq; q; q = q->next) { - if(!strcmp(q->id, ql->last_click->id)) - break; - ql->last_click = q; - } - } - /* Tell every queue entry which queue owns it */ - for(q = newq; q; q = q->next) - q->ql = ql; - /* Switch to the new queue */ - ql->q = newq; - /* Clean up any selected items that have fallen off */ - for(q = ql->q; q; q = q->next) - selection_live(ql->selection, q->id); - selection_cleanup(ql->selection); -} - -/** @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, - GtkStyle *style, - int *wp) { - GtkRequisition req; - GtkWidget *bg; - - D(("wrap_queue_cell")); - /* Padding should be in the label so there are no gaps in the - * background */ - gtk_misc_set_padding(GTK_MISC(label), HCELLPADDING, VCELLPADDING); - /* Event box is just to hold a background color */ - NW(event_box); - bg = gtk_event_box_new(); - gtk_container_add(GTK_CONTAINER(bg), label); - if(wp) { - /* Update maximum width */ - gtk_widget_size_request(label, &req); - if(req.width > *wp) *wp = req.width; - } - /* Set colors */ - gtk_widget_set_style(bg, style); - gtk_widget_set_style(label, style); - return bg; -} - -/** @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, - GtkStyle *style, - int *wp) { - GtkWidget *label; - D(("get_queue_cell %d %d", row, col)); - 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); -} - -/** @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(""), style, 0); -} - -/* User button press and menu ---------------------------------------------- */ - -/** @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 < 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); - } - /* Might need to change sensitivity of 'Properties' in main menu */ - 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) - a = a->next; - return !!a; -} - -/** @brief A button was pressed and released */ -static gboolean queuelike_button_released(GtkWidget attribute((unused)) *widget, - GdkEventButton *event, - gpointer user_data) { - struct queue_entry *q = user_data, *qq; - struct queuelike *ql = q->ql; - struct menuiteminfo *mii; - int n; - - /* Might be a release left over from a drag */ - if(ql->swallow_release) { - ql->swallow_release = 0; - return FALSE; /* propagate */ - } - - if(event->type == GDK_BUTTON_PRESS - && event->button == 3) { - /* Right button click. - * If the current item is not selected then switch the selection to just - * this item */ - if(q && !selection_selected(ql->selection, q->id)) { - selection_empty(ql->selection); - selection_set(ql->selection, q->id, 1); - ql->last_click = q; - set_widget_states(ql); - } - /* Set the sensitivity of each menu item and (re-)establish the signal - * handlers */ - for(n = 0; ql->menuitems[n].name; ++n) { - if(ql->menuitems[n].handlerid) - g_signal_handler_disconnect(ql->menuitems[n].w, - ql->menuitems[n].handlerid); - gtk_widget_set_sensitive(ql->menuitems[n].w, - ql->menuitems[n].sensitive(ql, - &ql->menuitems[n], - q)); - mii = xmalloc(sizeof *mii); - mii->ql = ql; - mii->q = q; - ql->menuitems[n].handlerid = g_signal_connect - (ql->menuitems[n].w, "activate", - G_CALLBACK(ql->menuitems[n].activate), mii); - } - /* Update the menu according to context */ - gtk_widget_show_all(ql->menu); - gtk_menu_popup(GTK_MENU(ql->menu), 0, 0, 0, 0, - event->button, event->time); - return TRUE; /* hide the click from other widgets */ - } - if(event->type == GDK_BUTTON_RELEASE - && event->button == 1) { - /* no modifiers: select this, unselect everything else, set last click - * +ctrl: flip selection of this, set last click - * +shift: select from last click to here, don't set last click - * +ctrl+shift: select from last click to here, set last click - */ - switch(event->state & (GDK_SHIFT_MASK|GDK_CONTROL_MASK)) { - case 0: - selection_empty(ql->selection); - selection_set(ql->selection, q->id, 1); - ql->last_click = q; - break; - case GDK_CONTROL_MASK: - selection_flip(ql->selection, q->id); - ql->last_click = q; - break; - case GDK_SHIFT_MASK: - case GDK_SHIFT_MASK|GDK_CONTROL_MASK: - if(ql->last_click) { - if(!(event->state & GDK_CONTROL_MASK)) - selection_empty(ql->selection); - selection_set(ql->selection, q->id, 1); - qq = q; - if(queue_before(ql->last_click, q)) - while(qq != ql->last_click) { - qq = qq->prev; - selection_set(ql->selection, qq->id, 1); - } - else - while(qq != ql->last_click) { - qq = qq->next; - selection_set(ql->selection, qq->id, 1); - } - if(event->state & GDK_CONTROL_MASK) - ql->last_click = q; - } - break; - } - set_widget_states(ql); - gtk_widget_queue_draw(ql->mainlayout); - } - return FALSE; /* propagate */ -} - -/** @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; - - for(qq = ql->q; qq; qq = qq->next) - selection_set(ql->selection, qq->id, 1); - ql->last_click = 0; - 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; - - vector_init(&v); - for(qq = ql->q; qq; qq = qq->next) - if(selection_selected(ql->selection, qq->id)) - vector_append(&v, (char *)qq->track); - if(v.nvec) - 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; -} - -/** @brief Called when a drag begings */ -static void queue_drag_begin(GtkWidget attribute((unused)) *widget, - GdkDragContext attribute((unused)) *dc, - gpointer data) { - struct queue_entry *q = data; - struct queuelike *ql = q->ql; - - /* Make sure the playing track is not selected, since it cannot be dragged */ - if(playing_track) - selection_set(ql->selection, playing_track->id, 0); - /* If the dragged item is not in the selection then change the selection to - * just that */ - if(!selection_selected(ql->selection, q->id)) { - selection_empty(ql->selection); - selection_set(ql->selection, q->id, 1); - set_widget_states(ql); - } - /* Ignore the eventual button release */ - ql->swallow_release = 1; - /* Create dropzones */ - add_drag_targets(ql); -} - -/** @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) { - int row; - struct queue_entry *q; - - if(id) { - for(q = ql->q, row = 0; q && strcmp(q->id, id); q = q->next, ++row) - ; - } else { - q = 0; - row = playing_track ? 0 : -1; - } - if(rowp) *rowp = row; - return q; -} - -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, - gint attribute((unused)) y, - guint when, - gpointer user_data) { - struct queuelike *ql = &ql_queue; - const char *id = user_data; - struct vector vec; - struct queue_entry *q; - - if(!id || (playing_track && !strcmp(id, playing_track->id))) - id = ""; - vector_init(&vec); - for(q = ql->q; q; q = q->next) - 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, - move_completed, 0/*v*/); - gtk_drag_finish(drag_context, TRUE, TRUE, when); - /* Destroy dropzones */ - remove_drag_targets(ql); - return TRUE; -} - -/** @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, - gint attribute((unused)) y, - guint when, - gpointer user_data) { - struct queuelike *ql = &ql_queue; - const char *id = user_data; - int row; - struct queue_entry *q = findentry(ql, id, &row); - - if(!id || q) { - if(!ql->dragmark) { - NW(event_box); - ql->dragmark = gtk_event_box_new(); - 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_style(ql->dragmark, drag_style); - gtk_layout_put(GTK_LAYOUT(ql->mainlayout), ql->dragmark, 0, - (row + 1) * ql->mainrowheight - !!row); - } else - gtk_layout_move(GTK_LAYOUT(ql->mainlayout), ql->dragmark, 0, - (row + 1) * ql->mainrowheight - !!row); - gtk_widget_show(ql->dragmark); - gdk_drag_status(drag_context, GDK_ACTION_MOVE, when); - return TRUE; - } else - /* ID has gone AWOL */ - return FALSE; -} - -/** @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, - gpointer attribute((unused)) user_data) { - struct queuelike *ql = &ql_queue; - - if(ql->dragmark) - gtk_widget_hide(ql->dragmark); -} - -/** @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; - - NW(event_box); - eventbox = gtk_event_box_new(); - /* Make the target zone invisible */ - gtk_event_box_set_visible_window(GTK_EVENT_BOX(eventbox), FALSE); - /* Make it large enough */ - gtk_widget_set_size_request(eventbox, 10240, - y ? ql->mainrowheight : ql->mainrowheight / 2); - /* Position it */ - gtk_layout_put(GTK_LAYOUT(ql->mainlayout), eventbox, 0, - y ? y - ql->mainrowheight / 2 : 0); - /* Mark it as capable of receiving drops */ - gtk_drag_dest_set(eventbox, - 0, - dragtargets, NDRAGTARGETS, GDK_ACTION_MOVE); - g_signal_connect(eventbox, "drag-drop", - G_CALLBACK(queue_drag_drop), (char *)id); - /* Monitor drag motion */ - g_signal_connect(eventbox, "drag-motion", - G_CALLBACK(queue_drag_motion), (char *)id); - g_signal_connect(eventbox, "drag-leave", - G_CALLBACK(queue_drag_leave), (char *)id); - /* The widget needs to be shown to receive drags */ - gtk_widget_show(eventbox); - /* Remember the drag targets */ - ql->dropzones[ql->ndropzones] = eventbox; - g_signal_connect(eventbox, "destroy", - G_CALLBACK(gtk_widget_destroyed), - &ql->dropzones[ql->ndropzones]); - ++ql->ndropzones; -} - -/** @brief Create dropzones for dragging into */ -static void add_drag_targets(struct queuelike *ql) { - 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); - /* Put a drag target at the bottom of every row */ - for(q = ql->q; q; q = q->next) { - y += ql->mainrowheight; - add_drag_target(ql, y, q->id); - } -} - -/** @brief Remove the dropzones */ -static void remove_drag_targets(struct queuelike *ql) { - int n; - - for(n = 0; n < ql->ndropzones; ++n) { - if(ql->dropzones[n]) { - DW(event_box); - gtk_widget_destroy(ql->dropzones[n]); - } - assert(ql->dropzones[n] == 0); - } -} - -/* Layout ------------------------------------------------------------------ */ - -/** @brief Redisplay a queue */ -static void redisplay_queue(struct queuelike *ql) { - struct queue_entry *q; - int row, col; - GList *c, *children; - GtkStyle *style; - GtkRequisition req; - GtkWidget *w; - 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 = children = gtk_container_get_children(GTK_CONTAINER(ql->mainlayout)); - c; - c = c->next) { - /* Destroy both the label and the eventbox */ - if(GTK_BIN(c->data)->child) { - DW(label); - gtk_widget_destroy(GTK_BIN(c->data)->child); - } - 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 * (ql->ncolumns + 1), sizeof *ql->cells); - /* Minimum width is given by the column headings */ - 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); - maxwidths[col] = req.width; - } - /* Find the vertical size of the title bar */ - gtk_widget_size_request(GTK_BIN(ql->titlecells[0])->child, &req); - titlerowheight = req.height; - y = 0; - if(ql->nrows) { - /* 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) style = active_style; - else style = row % 2 ? even_style : odd_style; - /* Make the widget for each column */ - for(col = 0; col <= ql->ncolumns; ++col) { - /* Create and store the widget */ - if(col < ql->ncolumns) - w = get_queue_cell(ql, q, row, col, style, &maxwidths[col]); - else - 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, - dragtargets, NDRAGTARGETS, GDK_ACTION_MOVE); - g_signal_connect(w, "drag-begin", G_CALLBACK(queue_drag_begin), q); - } - /* Catch button presses */ - g_signal_connect(w, "button-release-event", - G_CALLBACK(queuelike_button_released), q); - g_signal_connect(w, "button-press-event", - G_CALLBACK(queuelike_button_released), q); - } - } - /* ...and of each row in the main layout */ - gtk_widget_size_request(GTK_BIN(ql->cells[0])->child, &req); - ql->mainrowheight = req.height; - /* Now we know the maximum width of each column we can set the size of - * everything and position it */ - for(row = 0, q = ql->q; row < ql->nrows; ++row, q = q->next) { - x = 0; - 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 * (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); - y += ql->mainrowheight; - } - } - /* Titles */ - x = 0; - 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); - x += maxwidths[col]; - } - gtk_widget_set_size_request(GTK_BIN(ql->titlecells[col])->child, - totalwidth - x, -1); - gtk_layout_move(GTK_LAYOUT(ql->titlelayout), ql->titlecells[col], x, 0); - /* Set the states */ - set_widget_states(ql); - /* Make sure it's all visible */ - gtk_widget_show_all(ql->mainlayout); - gtk_widget_show_all(ql->titlelayout); - /* Layouts might shrink to arrange for the area they shrink out of to be - * redrawn */ - gtk_widget_queue_draw(ql->mainlayout); - gtk_widget_queue_draw(ql->titlelayout); - /* Adjust the size of the layout */ - gtk_layout_set_size(GTK_LAYOUT(ql->mainlayout), x, y); - gtk_layout_set_size(GTK_LAYOUT(ql->titlelayout), x, titlerowheight); - gtk_widget_set_size_request(ql->titlelayout, -1, titlerowheight); -} - -/** @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); + return; } + actual_queue = q; + queue_playing_changed(); } -/** @brief Called with a new currently playing track */ +/** @brief Update the playing track */ static void playing_completed(void attribute((unused)) *v, const char *error, struct queue_entry *q) { - if(error) + 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); - /* Notify any interested parties */ - event_raise("playing-track-changed", q); - } -} - -/** @brief Called when the queue is scrolled */ -static void queue_scrolled(GtkAdjustment *adjustment, - gpointer user_data) { - GtkAdjustment *titleadj = user_data; - - D(("queue_scrolled")); - gtk_adjustment_set_value(titleadj, adjustment->value); -} - -/** @brief Create a queuelike thing (queue/recent) */ -static GtkWidget *queuelike(struct queuelike *ql, - struct queue_entry *(*fixup)(struct queue_entry *), - void (*notify)(void), - struct queue_menuitem *menuitems, - const struct column *columns, - int ncolumns) { - GtkWidget *vbox, *mainscroll, *titlescroll, *label; - GtkAdjustment *mainadj, *titleadj; - int col, n; - - D(("queuelike")); - ql->fixup = fixup; - ql->notify = notify; - ql->menuitems = menuitems; - 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); - 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)); - titleadj = gtk_scrolled_window_get_hadjustment(GTK_SCROLLED_WINDOW(titlescroll)); - 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 < ql->ncolumns; ++col) { - NW(label); - 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(title_style); - gtk_layout_put(GTK_LAYOUT(ql->titlelayout), ql->titlecells[col], 0, 0); - /* Pack the lot together in a vbox */ - NW(vbox); - vbox = gtk_vbox_new(0, 0); - gtk_box_pack_start(GTK_BOX(vbox), titlescroll, 0, 0, 0); - gtk_box_pack_start(GTK_BOX(vbox), mainscroll, 1, 1, 0); - /* Create the popup menu */ - NW(menu); - ql->menu = gtk_menu_new(); - g_signal_connect(ql->menu, "destroy", - G_CALLBACK(gtk_widget_destroyed), &ql->menu); - for(n = 0; menuitems[n].name; ++n) { - NW(menu_item); - menuitems[n].w = gtk_menu_item_new_with_label(menuitems[n].name); - gtk_menu_attach(GTK_MENU(ql->menu), menuitems[n].w, 0, 1, n, n + 1); + return; } - g_object_set_data(G_OBJECT(vbox), "type", (void *)&tabtype_queue); - g_object_set_data(G_OBJECT(vbox), "queue", ql); - /* Catch button presses */ - g_signal_connect(ql->mainlayout, "button-release-event", - G_CALLBACK(mainlayout_button), ql); -#if 0 - g_signal_connect(ql->mainlayout, "button-press-event", - G_CALLBACK(mainlayout_button), ql); -#endif - set_tool_colors(ql->menu); - return vbox; -} - -/* Popup menu items -------------------------------------------------------- */ - -/** @brief Count the number of items selected */ -static int queue_count_selected(const struct queuelike *ql) { - return hash_count(ql->selection); -} - -/** @brief Count the number of items selected */ -static int queue_count_entries(const struct queuelike *ql) { - int nitems = 0; - const struct queue_entry *q; - - for(q = ql->q; q; q = q->next) - ++nitems; - return nitems; -} - -/** @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); - - if(ql->q == playing_track && selection_selected(ql->selection, ql->q->id)) - --nselected; - return nselected; -} - -/** @brief Determine whether the scratch option should be sensitive */ -static int scratch_sensitive(struct queuelike attribute((unused)) *ql, - struct queue_menuitem attribute((unused)) *m, - struct queue_entry attribute((unused)) *q) { - /* We can scratch if the playing track is selected */ - return (playing_track - && (disorder_eclient_state(client) & DISORDER_CONNECTED) - && selection_selected(ql->selection, playing_track->id) - && right_scratchable(last_rights, config->username, playing_track)); -} - -/** @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, scratch_completed, 0); -} - -/** @brief Determine whether the remove option should be sensitive */ -static int remove_sensitive(struct queuelike *ql, - 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 ((disorder_eclient_state(client) & DISORDER_CONNECTED) - && ((q - && q != playing_track) - || count_selected_nonplaying(ql)) - && right_removable(last_rights, config->username, q)); -} - -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; - struct queue_entry *q = mii->q; - struct queuelike *ql = mii->ql; - - if(count_selected_nonplaying(mii->ql)) { - /* 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, move_completed, 0); - } else if(q) - /* Remove just the hovered track */ - 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 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 - && (disorder_eclient_state(client) & DISORDER_CONNECTED) - && (last_rights & RIGHT_PREFS)); -} - -/** @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; - - queue_properties(mii->ql); -} - -/** @brief Determine whether the select all menu option should be sensitive */ -static int selectall_sensitive(struct queuelike *ql, - 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) - && (last_rights & RIGHT_PLAY)); -} - -/** @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 --------------------------------------------------------------- */ - -/** @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; - if(playing_track) { - if(actual_queue) - actual_queue->prev = playing_track; - playing_track->next = actual_queue; - return playing_track; - } else - return actual_queue; + actual_playing_track = q; + queue_playing_changed(); } -/** @brief Adjust track played label +/** @brief Schedule an update to the queue * - * 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) { - if(playing_length_label && playing_track) - gtk_label_set_text(GTK_LABEL(playing_length_label), - text_length(playing_track)); - return TRUE; + * Called whenever a track is added to it or removed from it. + */ +static 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, queue_completed, 0); } -/** @brief Popup menu for the queue +/** @brief Schedule an update to the playing track * - * 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(). + * Called whenever it changes */ static void playing_changed(const char attribute((unused)) *event, - void attribute((unused)) *evendata, - void attribute((unused)) *callbackdata) { + void attribute((unused)) *eventdata, + 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, - maincolumns, NMAINCOLUMNS); -} - -/** @brief Arrange an update of the queue widget +/** @brief Called regularly * - * 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 + * Updates the played-so-far field */ -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, &ql_queue); +static gboolean playing_periodic(gpointer attribute((unused)) data) { + /* If there's a track playing, update its row */ + if(playing_track) + ql_update_row(playing_track, 0); + return TRUE; } -/* 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) { - struct queue_entry *qr = 0, *qn; - - D(("fixup_recent")); - while(q) { - qn = q->next; - /* Swap next/prev pointers */ - q->next = q->prev; - q->prev = qn; - /* Remember last node for new head */ - qr = q; - /* Next node */ - q = qn; - } - return qr; +/** @brief Called at startup */ +static void queue_init(void) { + /* Arrange a callback whenever the playing state changes */ + event_register("playing-changed", playing_changed, 0); + event_register("pause-changed", playing_changed, 0); + /* ...and when the queue changes */ + event_register("queue-changed", queue_changed, 0); + /* ...and once a second anyway */ + g_timeout_add(1000/*ms*/, playing_periodic, 0); } -/** @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 Columns for the queue */ +static const struct queue_column queue_columns[] = { + { "When", column_when, 0, 1 }, + { "Who", column_who, 0, 0 }, + { "Artist", column_namepart, "artist", 0 }, + { "Album", column_namepart, "album", 0 }, + { "Title", column_namepart, "title", 0 }, + { "Length", column_length, 0, 1 } }; -/** @brief Create the recently-played list */ -GtkWidget *recent_widget(void) { - D(("recent_widget")); - event_register("recent-changed", - recent_changed, - 0); - return queuelike(&ql_recent, fixup_recent, 0, recent_menu, - maincolumns, NMAINCOLUMNS); -} - -/** @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, &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 Pop-up menu for queue */ +static struct queue_menuitem queue_menuitems[] = { + { "Track properties", ql_properties_activate, ql_properties_sensitive, 0, 0 }, + { "Select all tracks", ql_selectall_activate, ql_selectall_sensitive, 0, 0 }, + { "Deselect all tracks", ql_selectnone_activate, ql_selectnone_sensitive, 0, 0 }, + { "Scratch track", ql_scratch_activate, ql_scratch_sensitive, 0, 0 }, + { "Remove track from queue", ql_remove_activate, ql_remove_sensitive, 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")) - && (disorder_eclient_state(client) & DISORDER_CONNECTED) - && (last_rights & RIGHT_PREFS)); -} - -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")); -} - -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, - 0 +struct queuelike ql_queue = { + .init = queue_init, + .columns = queue_columns, + .ncolumns = sizeof queue_columns / sizeof *queue_columns, + .menuitems = queue_menuitems, + .nmenuitems = sizeof queue_menuitems / sizeof *queue_menuitems, }; -/* Other entry points ------------------------------------------------------ */ +GtkWidget *queue_widget(void) { + return init_queuelike(&ql_queue); +} /** @brief Return nonzero if @p track is in the queue */ int queued(const char *track) { diff --git a/disobedience/recent.c b/disobedience/recent.c new file mode 100644 index 0000000..1f3e667 --- /dev/null +++ b/disobedience/recent.c @@ -0,0 +1,103 @@ +/* + * This file is part of DisOrder + * 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 + * the Free Software Foundation; either version 2 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. + * + * 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 + */ +#include "disobedience.h" +#include "queue-generic.h" + +/** @brief Update the recently played list */ +static void recent_completed(void attribute((unused)) *v, + const char *error, + struct queue_entry *q) { + if(error) { + popup_protocol_error(0, error); + return; + } + /* The recent list is backwards compared to what we wanted */ + struct queue_entry *qr = 0, *qn; + while(q) { + qn = q->next; + /* Swap next/prev pointers */ + q->next = q->prev; + q->prev = qn; + /* Remember last node for new head */ + qr = q; + /* Next node */ + q = qn; + } + /* Update the display */ + ql_new_queue(&ql_recent, qr); + /* Tell anyone who cares */ + event_raise("recent-list-changed", qr); +} + +/** @brief Schedule an update to 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, recent_completed, 0); +} + +/** @brief Called at startup */ +static void recent_init(void) { + /* Whenever the recent list changes on the server, re-fetch it */ + event_register("recent-changed", recent_changed, 0); +} + +/** @brief Columns for the recently-played list */ +static const struct queue_column recent_columns[] = { + { "When", column_when, 0, 1 }, + { "Who", column_who, 0, 0 }, + { "Artist", column_namepart, "artist", 0 }, + { "Album", column_namepart, "album", 0 }, + { "Title", column_namepart, "title", 0 }, + { "Length", column_length, 0, 1 } +}; + +/** @brief Pop-up menu for recently played list */ +static struct queue_menuitem recent_menuitems[] = { + { "Track properties", ql_properties_activate, ql_properties_sensitive,0, 0 }, + { "Select all tracks", ql_selectall_activate, ql_selectall_sensitive, 0, 0 }, + { "Deselect all tracks", ql_selectnone_activate, ql_selectnone_sensitive, 0, 0 }, +}; + +struct queuelike ql_recent = { + .init = recent_init, + .columns = recent_columns, + .ncolumns = sizeof recent_columns / sizeof *recent_columns, + .menuitems = recent_menuitems, + .nmenuitems = sizeof recent_menuitems / sizeof *recent_menuitems, +}; + +GtkWidget *recent_widget(void) { + return init_queuelike(&ql_recent); +} + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +fill-column:79 +indent-tabs-mode:nil +End: +*/ -- [mdw]