#include "popup.h"
#include "validity.h"
-#if PLAYLISTS
-
static void playlist_list_received_playlists(void *v,
const char *err,
int nvec, char **vec);
int nvec, char **vec);
static void playlist_remove_modify(struct playlist_modify_data *mod,
int nvec, char **vec);
+static gboolean playlist_new_keypress(GtkWidget *widget,
+ GdkEventKey *event,
+ gpointer user_data);
+static gboolean playlist_picker_keypress(GtkWidget *widget,
+ GdkEventKey *event,
+ gpointer user_data);
+static void playlist_editor_button_toggled(GtkToggleButton *tb,
+ gpointer userdata);
+static void playlist_editor_set_buttons(const char *event,
+ void *eventdata,
+ void *callbackdata);
+static void playlist_editor_got_share(void *v,
+ const char *err,
+ const char *value);
+static void playlist_editor_share_set(void *v, const char *err);
/** @brief Playlist editing window */
static GtkWidget *playlist_window;
{ "Artist", column_namepart, "artist", COL_EXPAND|COL_ELLIPSIZE },
{ "Album", column_namepart, "album", COL_EXPAND|COL_ELLIPSIZE },
{ "Title", column_namepart, "title", COL_EXPAND|COL_ELLIPSIZE },
- { "Length", column_length, 0, COL_RIGHT }
};
/** @brief Pop-up menu for playlist editor
{ "Deselect all tracks", ql_selectnone_activate, ql_selectnone_sensitive, 0, 0 },
};
+static const GtkTargetEntry playlist_targets[] = {
+ {
+ PLAYLIST_TRACKS, /* drag type */
+ GTK_TARGET_SAME_WIDGET, /* rearrangement within a widget */
+ PLAYLIST_TRACKS_ID /* ID value */
+ },
+ {
+ PLAYABLE_TRACKS, /* drag type */
+ GTK_TARGET_SAME_APP|GTK_TARGET_OTHER_WIDGET, /* copying between widgets */
+ PLAYABLE_TRACKS_ID, /* ID value */
+ },
+ {
+ .target = NULL
+ }
+};
+
/** @brief Queuelike for editing a playlist */
static struct queuelike ql_playlist = {
.name = "playlist",
.ncolumns = sizeof playlist_columns / sizeof *playlist_columns,
.menuitems = playlist_menuitems,
.nmenuitems = sizeof playlist_menuitems / sizeof *playlist_menuitems,
- .drop = playlist_drop
+ .drop = playlist_drop,
+ .drag_source_targets = playlist_targets,
+ .drag_source_actions = GDK_ACTION_MOVE|GDK_ACTION_COPY,
+ .drag_dest_targets = playlist_targets,
+ .drag_dest_actions = GDK_ACTION_MOVE|GDK_ACTION_COPY,
};
/* Maintaining the list of playlists ---------------------------------------- */
/* Set initial state of OK button */
playlist_new_changed(0,0,0);
- /* TODO: return should = OK, escape should = cancel */
+ g_signal_connect(playlist_new_window, "key-press-event",
+ G_CALLBACK(playlist_new_keypress), 0);
/* Display the window */
gtk_widget_show_all(playlist_new_window);
}
+/** @brief Keypress handler */
+static gboolean playlist_new_keypress(GtkWidget attribute((unused)) *widget,
+ GdkEventKey *event,
+ gpointer attribute((unused)) user_data) {
+ if(event->state)
+ return FALSE;
+ switch(event->keyval) {
+ case GDK_Return:
+ playlist_new_ok(NULL, NULL);
+ return TRUE;
+ case GDK_Escape:
+ gtk_widget_destroy(playlist_new_window);
+ return TRUE;
+ default:
+ return FALSE;
+ }
+}
+
/** @brief Called when 'ok' is clicked in new-playlist popup */
static void playlist_new_ok(GtkButton attribute((unused)) *button,
gpointer attribute((unused)) userdata) {
+ if(playlist_new_valid())
+ return;
gboolean shared, public, private;
char *name, *fullname;
playlist_new_details(&name, &fullname, &shared, &public, &private);
gtk_widget_set_sensitive(playlist_picker_delete_button, 1);
else
gtk_widget_set_sensitive(playlist_picker_delete_button, 0);
+ /* TODO delete should not be sensitive for public playlists owned by other
+ * users */
/* Eliminate no-change cases */
if(!selected && !playlist_picker_selected)
return;
playlist_picker_selected = selected;
/* Re-initalize the queue */
ql_new_queue(&ql_playlist, NULL);
- playlist_editor_fill(NULL, (void *)playlist_picker_selected, NULL);
+ /* Synthesize a playlist-modified to re-initialize the editor etc */
+ event_raise("playlist-modified", (void *)playlist_picker_selected);
}
/** @brief Called when the 'add' button is pressed */
int res;
if(!playlist_picker_selected)
- return; /* shouldn't happen */
+ return;
yesno = gtk_message_dialog_new(GTK_WINDOW(playlist_window),
GTK_DIALOG_MODAL,
GTK_MESSAGE_QUESTION,
gtk_box_pack_start(GTK_BOX(vbox), scroll_widget(tree), TRUE/*expand*/, TRUE/*fill*/, 0);
gtk_box_pack_start(GTK_BOX(vbox), buttons, FALSE/*expand*/, FALSE, 0);
+ g_signal_connect(tree, "key-press-event",
+ G_CALLBACK(playlist_picker_keypress), 0);
+
return vbox;
}
+static gboolean playlist_picker_keypress(GtkWidget attribute((unused)) *widget,
+ GdkEventKey *event,
+ gpointer attribute((unused)) user_data) {
+ if(event->state)
+ return FALSE;
+ switch(event->keyval) {
+ case GDK_BackSpace:
+ case GDK_Delete:
+ playlist_picker_delete(NULL, NULL);
+ return TRUE;
+ default:
+ return FALSE;
+ }
+}
+
+static void playlist_picker_destroy(void) {
+ playlist_picker_delete_button = NULL;
+ g_object_unref(playlist_picker_list);
+ playlist_picker_list = NULL;
+ playlist_picker_selection = NULL;
+ playlist_picker_selected = NULL;
+}
+
/* Playlist editor ---------------------------------------------------------- */
+static GtkWidget *playlist_editor_shared;
+static GtkWidget *playlist_editor_public;
+static GtkWidget *playlist_editor_private;
+static int playlist_editor_setting_buttons;
+
static GtkWidget *playlists_editor_create(void) {
assert(ql_playlist.view == NULL); /* better not be set up already */
- GtkWidget *w = init_queuelike(&ql_playlist);
- /* Initially empty */
- return w;
+
+ GtkWidget *hbox = gtk_hbox_new(FALSE, 0);
+ playlist_editor_shared = gtk_radio_button_new_with_label(NULL, "shared");
+ playlist_editor_public
+ = gtk_radio_button_new_with_label_from_widget(GTK_RADIO_BUTTON(playlist_editor_shared),
+ "public");
+ playlist_editor_private
+ = gtk_radio_button_new_with_label_from_widget(GTK_RADIO_BUTTON(playlist_editor_shared),
+ "private");
+ g_signal_connect(playlist_editor_public, "toggled",
+ G_CALLBACK(playlist_editor_button_toggled),
+ (void *)"public");
+ g_signal_connect(playlist_editor_private, "toggled",
+ G_CALLBACK(playlist_editor_button_toggled),
+ (void *)"private");
+ gtk_box_pack_start(GTK_BOX(hbox), playlist_editor_shared,
+ FALSE/*expand*/, FALSE/*fill*/, 0);
+ gtk_box_pack_start(GTK_BOX(hbox), playlist_editor_public,
+ FALSE/*expand*/, FALSE/*fill*/, 0);
+ gtk_box_pack_start(GTK_BOX(hbox), playlist_editor_private,
+ FALSE/*expand*/, FALSE/*fill*/, 0);
+ playlist_editor_set_buttons(0,0,0);
+
+ GtkWidget *vbox = gtk_vbox_new(FALSE, 0);
+ gtk_box_pack_start(GTK_BOX(vbox), init_queuelike(&ql_playlist),
+ TRUE/*expand*/, TRUE/*fill*/, 0);
+ gtk_box_pack_start(GTK_BOX(vbox), hbox,
+ FALSE/*expand*/, FALSE/*fill*/, 0);
+ return vbox;
+}
+
+/** @brief Called when the public/private buttons are set */
+static void playlist_editor_button_toggled(GtkToggleButton *tb,
+ gpointer userdata) {
+ const char *state = userdata;
+ if(!gtk_toggle_button_get_active(tb)
+ || !playlist_picker_selected
+ || playlist_editor_setting_buttons)
+ return;
+ disorder_eclient_playlist_set_share(client, playlist_editor_share_set,
+ playlist_picker_selected, state, NULL);
+}
+
+static void playlist_editor_share_set(void attribute((unused)) *v,
+ const attribute((unused)) char *err) {
+ if(err)
+ popup_protocol_error(0, err);
+}
+
+/** @brief Set the editor button state and sensitivity */
+static void playlist_editor_set_buttons(const char attribute((unused)) *event,
+ void *eventdata,
+ void attribute((unused)) *callbackdata) {
+ /* If this event is for a non-selected playlist do nothing */
+ if(eventdata
+ && playlist_picker_selected
+ && strcmp(eventdata, playlist_picker_selected))
+ return;
+ if(playlist_picker_selected) {
+ if(strchr(playlist_picker_selected, '.'))
+ disorder_eclient_playlist_get_share(client,
+ playlist_editor_got_share,
+ playlist_picker_selected,
+ (void *)playlist_picker_selected);
+ else
+ playlist_editor_got_share((void *)playlist_picker_selected, NULL,
+ "shared");
+ } else
+ playlist_editor_got_share(NULL, NULL, NULL);
+}
+
+/** @brief Called with playlist sharing details */
+static void playlist_editor_got_share(void *v,
+ const char *err,
+ const char *value) {
+ const char *playlist = v;
+ if(err) {
+ popup_protocol_error(0, err);
+ value = NULL;
+ }
+ /* Set the currently active button */
+ ++playlist_editor_setting_buttons;
+ gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(playlist_editor_shared),
+ value && !strcmp(value, "shared"));
+ gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(playlist_editor_public),
+ value && !strcmp(value, "public"));
+ gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(playlist_editor_private),
+ value && !strcmp(value, "private"));
+ /* Set button sensitivity */
+ gtk_widget_set_sensitive(playlist_editor_shared, FALSE);
+ int sensitive = (playlist
+ && strchr(playlist, '.')
+ && !strncmp(playlist, config->username,
+ strlen(config->username)));
+ gtk_widget_set_sensitive(playlist_editor_public, sensitive);
+ gtk_widget_set_sensitive(playlist_editor_private, sensitive);
+ --playlist_editor_setting_buttons;
}
/** @brief (Re-)populate the playlist tree model */
/* No such playlist, presumably we'll get a deleted event shortly */
return;
/* Translate the list of tracks into queue entries */
- struct queue_entry *newq, **qq = &newq;
+ struct queue_entry *newq, **qq = &newq, *qprev = NULL;
hash *h = hash_new(sizeof(int));
for(int n = 0; n < nvec; ++n) {
struct queue_entry *q = xmalloc(sizeof *q);
+ q->prev = qprev;
q->track = vec[n];
/* Synthesize a unique ID so that the selection survives updates. Tracks
* can appear more than once in the queue so we can't use raw track names,
hash_add(h, vec[n], &serial, HASH_INSERT_OR_REPLACE);
*qq = q;
qq = &q->next;
+ qprev = q;
}
*qq = NULL;
ql_new_queue(&ql_playlist, newq);
mod->playlist, mod);
}
+/** @brief Return true if track @p i is in the moved set */
+static int playlist_drop_is_moved(struct playlist_modify_data *mod,
+ int i) {
+ struct queue_entry *q;
+
+ /* Find the q corresponding to i, so we can get the ID */
+ for(q = ql_playlist.q; i; q = q->next, --i)
+ ;
+ /* See if track i matches any of the moved set by ID */
+ for(int n = 0; n < mod->ntracks; ++n)
+ if(!strcmp(q->id, mod->ids[n]))
+ return 1;
+ return 0;
+}
+
static void playlist_drop_modify(struct playlist_modify_data *mod,
int nvec, char **vec) {
char **newvec;
int nnewvec;
- fprintf(stderr, "after_me = %s\n",
- mod->after_me ? mod->after_me->track : "NULL");
+ //fprintf(stderr, "\nplaylist_drop_modify\n");
+ /* after_me is the queue_entry to insert after, or NULL to insert at the
+ * beginning (including the case when the playlist is empty) */
+ //fprintf(stderr, "after_me = %s\n",
+ // mod->after_me ? mod->after_me->track : "NULL");
+ struct queue_entry *q = ql_playlist.q;
+ int ins = 0;
+ if(mod->after_me) {
+ ++ins;
+ while(q && q != mod->after_me) {
+ q = q->next;
+ ++ins;
+ }
+ }
+ /* Now ins is the index to insert at; equivalently, the row to insert before,
+ * and so equal to nvec to append. */
+#if 0
+ fprintf(stderr, "ins = %d = %s\n",
+ ins, ins < nvec ? vec[ins] : "NULL");
+ for(int n = 0; n < nvec; ++n)
+ fprintf(stderr, "%d: %s %s\n", n, n == ins ? "->" : " ", vec[n]);
+ fprintf(stderr, "nvec = %d\n", nvec);
+#endif
if(mod->ids) {
/* This is a rearrangement */
- /* TODO what if it's a drag from the queue? */
- abort();
- } else {
- /* This is an insertion */
- struct queue_entry *q = ql_playlist.q;
- int ins = 0;
- if(mod->after_me) {
- ++ins;
- while(q && q != mod->after_me) {
- q = q->next;
- ++ins;
+ /* We have:
+ * - vec[], the current layout
+ * - ins, pointing into vec
+ * - mod->tracks[], a subset of vec[] which is to be moved
+ *
+ * ins is the insertion point BUT it is in terms of the whole
+ * array, i.e. before mod->tracks[] have been removed. The first
+ * step then is to remove everything in mod->tracks[] and adjust
+ * ins downwards as necessary.
+ */
+ /* First zero out anything that's moved */
+ int before_ins = 0;
+ for(int n = 0; n < nvec; ++n) {
+ if(playlist_drop_is_moved(mod, n)) {
+ vec[n] = NULL;
+ if(n < ins)
+ ++before_ins;
}
}
- fprintf(stderr, "ins = %d = %s\n",
- ins, ins < nvec ? vec[ins] : "NULL");
- nnewvec = nvec + mod->ntracks;
- newvec = xcalloc(nnewvec, sizeof (char *));
- memcpy(newvec, vec,
- ins * sizeof (char *));
- memcpy(newvec + ins, mod->tracks,
- mod->ntracks * sizeof (char *));
- memcpy(newvec + ins + mod->ntracks, vec + ins,
- (nvec - ins) * sizeof (char *));
+ /* Now collapse down the array */
+ int i = 0;
+ for(int n = 0; n < nvec; ++n) {
+ if(vec[n])
+ vec[i++] = vec[n];
+ }
+ assert(i + mod->ntracks == nvec);
+ nvec = i;
+ /* Adjust the insertion point to take account of things moved from before
+ * it */
+ ins -= before_ins;
+ /* The effect is now the same as an insertion */
}
+ /* This is (now) an insertion */
+ nnewvec = nvec + mod->ntracks;
+ newvec = xcalloc(nnewvec, sizeof (char *));
+ memcpy(newvec, vec,
+ ins * sizeof (char *));
+ memcpy(newvec + ins, mod->tracks,
+ mod->ntracks * sizeof (char *));
+ memcpy(newvec + ins + mod->ntracks, vec + ins,
+ (nvec - ins) * sizeof (char *));
disorder_eclient_playlist_set(client, playlist_modify_updated, mod->playlist,
newvec, nnewvec, mod);
}
return TRUE;
}
-/** @brief Called to play the selected playlist */
+/** @brief Called to remove the selected playlist */
static void playlist_remove_activate(GtkMenuItem attribute((unused)) *menuitem,
gpointer attribute((unused)) user_data) {
- /* TODO backspace should work too */
if(!playlist_picker_selected)
return;
struct playlist_modify_data *mod = xmalloc(sizeof *mod);
g_signal_connect(playlist_window, "key-press-event",
G_CALLBACK(playlist_window_keypress), 0);
/* default size is too small */
- gtk_window_set_default_size(GTK_WINDOW(playlist_window), 512, 240);
+ gtk_window_set_default_size(GTK_WINDOW(playlist_window), 640, 320);
GtkWidget *hbox = gtk_hbox_new(FALSE, 0);
gtk_box_pack_start(GTK_BOX(hbox), playlist_picker_create(),
static void playlist_window_destroyed(GtkWidget attribute((unused)) *widget,
GtkWidget **widget_pointer) {
destroy_queuelike(&ql_playlist);
+ playlist_picker_destroy();
*widget_pointer = NULL;
}
event_register("playlists-updated", playlist_picker_fill, 0);
/* Update the displayed playlist when it is modified */
event_register("playlist-modified", playlist_editor_fill, 0);
+ /* Update the shared/public/etc buttons when a playlist is modified */
+ event_register("playlist-modified", playlist_editor_set_buttons, 0);
}
-#endif
-
/*
Local Variables:
c-basic-offset:2