From 7f7c38193f1f45a70710bc922d110bf08022c175 Mon Sep 17 00:00:00 2001 Message-Id: <7f7c38193f1f45a70710bc922d110bf08022c175.1714841541.git.mdw@distorted.org.uk> From: Mark Wooding Date: Sun, 22 Nov 2009 17:53:31 +0000 Subject: [PATCH] (Slightly scrappy) new playlist box Organization: Straylight/Edgeware From: Richard Kettlewell --- disobedience/playlists.c | 172 +++++++++++++++++++++++++++++++++++++-- lib/Makefile.am | 1 + lib/trackdb-int.h | 1 - lib/trackdb-playlists.c | 38 +-------- lib/trackdb.c | 27 +----- lib/trackdb.h | 3 - lib/validity.c | 98 ++++++++++++++++++++++ lib/validity.h | 43 ++++++++++ 8 files changed, 308 insertions(+), 75 deletions(-) create mode 100644 lib/validity.c create mode 100644 lib/validity.h diff --git a/disobedience/playlists.c b/disobedience/playlists.c index bf6d185..1a3e74f 100644 --- a/disobedience/playlists.c +++ b/disobedience/playlists.c @@ -30,6 +30,7 @@ #include "disobedience.h" #include "queue-generic.h" #include "popup.h" +#include "validity.h" #if PLAYLISTS @@ -194,6 +195,167 @@ static void menu_playlists_changed(const char attribute((unused)) *event, nplaylists >= 0); } +/* Popup to create a new playlist ------------------------------------------- */ + +/** @brief New-playlist popup */ +static GtkWidget *playlist_new_window; + +/** @brief Text entry in new-playlist popup */ +static GtkWidget *playlist_new_entry; + +static GtkWidget *playlist_new_info; + +static GtkWidget *playlist_new_shared; +static GtkWidget *playlist_new_public; +static GtkWidget *playlist_new_private; + +/** @brief Called when 'ok' is clicked in new-playlist popup */ +static void playlist_new_ok(GtkButton attribute((unused)) *button, + gpointer attribute((unused)) userdata) { +} + +/** @brief Called when 'cancel' is clicked in new-playlist popup */ +static void playlist_new_cancel(GtkButton attribute((unused)) *button, + gpointer attribute((unused)) userdata) { + gtk_widget_destroy(playlist_new_window); +} + +/** @brief Buttons for new-playlist popup */ +static struct button playlist_new_buttons[] = { + { + .stock = GTK_STOCK_OK, + .clicked = playlist_new_ok, + .tip = "Create new playlist" + }, + { + .stock = GTK_STOCK_CANCEL, + .clicked = playlist_new_cancel, + .tip = "Do not create new plalist" + } +}; +#define NPLAYLIST_NEW_BUTTONS (sizeof playlist_new_buttons / sizeof *playlist_new_buttons) + +/** @brief Test whether the new-playlist window settings are valid */ +static const char *playlist_new_valid(void) { + gboolean shared, public, private; + g_object_get(playlist_new_shared, "active", &shared, (char *)NULL); + g_object_get(playlist_new_public, "active", &public, (char *)NULL); + g_object_get(playlist_new_private, "active", &private, (char *)NULL); + if(!(shared || public || private)) + return "No type set."; + char *name = gtk_editable_get_chars(GTK_EDITABLE(playlist_new_entry), + 0, -1); + if(!*name) + return ""; + if(!valid_username(name)) + return "Not a valid playlist name."; + /* Construct the full form of the name */ + char *fullname; + if(shared) + fullname = name; + else + byte_xasprintf(&fullname, "%s.%s", config->username, name); + /* See if the result is valid */ + if(playlist_parse_name(fullname, NULL, NULL)) + return "Not a valid playlist name."; + /* See if the result clashes with an existing name */ + for(int n = 0; n < nplaylists; ++n) + if(!strcmp(playlists[n], fullname)) { + if(shared) + return "A shared playlist with that name already exists."; + else + return "You already have a playlist with that name."; + } + /* As far as we can tell creation would work */ + return NULL; +} + +/** @brief Called when anything in the new-playlist popup changes + * + * We use this to set the activation state of the OK button. + */ +static void playlist_new_changed(void) { + const char *reason = playlist_new_valid(); + gtk_widget_set_sensitive(playlist_new_buttons[0].widget, + !reason); + gtk_label_set_text(GTK_LABEL(playlist_new_info), reason); +} + +/** @brief Called when some radio button in the new-playlist popup changes */ +static void playlist_new_button_toggled(GtkToggleButton attribute((unused)) tb, + gpointer attribute((unused)) userdata) { + playlist_new_changed(); +} + +/** @brief Called when the text entry field in the new-playlist popup changes */ +static void playlist_new_entry_edited(GtkEditable attribute((unused)) *editable, + gpointer attribute((unused)) user_data) { + playlist_new_changed(); +} + +/** @brief Pop up a new window to enter the playlist name and details */ +static void playlist_new(void) { + assert(playlist_new_window == NULL); + playlist_new_window = gtk_window_new(GTK_WINDOW_TOPLEVEL); + g_signal_connect(playlist_new_window, "destroy", + G_CALLBACK(gtk_widget_destroyed), &playlist_new_window); + gtk_window_set_title(GTK_WINDOW(playlist_new_window), "Create new playlist"); + /* Window will be modal, suppressing access to other windows */ + gtk_window_set_modal(GTK_WINDOW(playlist_new_window), TRUE); + + /* Window contents will use a table (grid) layout */ + GtkWidget *table = gtk_table_new(3, 3, FALSE/*!homogeneous*/); + + /* First row: playlist name */ + gtk_table_attach_defaults(GTK_TABLE(table), + gtk_label_new("Playlist name"), + 0, 1, 0, 1); + playlist_new_entry = gtk_entry_new(); + g_signal_connect(playlist_new_entry, "changed", + G_CALLBACK(playlist_new_entry_edited), NULL); + gtk_table_attach_defaults(GTK_TABLE(table), + playlist_new_entry, + 1, 3, 0, 1); + + /* Second row: radio buttons to choose type */ + playlist_new_shared = gtk_radio_button_new_with_label(NULL, "shared"); + playlist_new_public + = gtk_radio_button_new_with_label_from_widget(GTK_RADIO_BUTTON(playlist_new_shared), + "public"); + playlist_new_private + = gtk_radio_button_new_with_label_from_widget(GTK_RADIO_BUTTON(playlist_new_shared), + "private"); + g_signal_connect(playlist_new_shared, "toggled", + G_CALLBACK(playlist_new_button_toggled), NULL); + g_signal_connect(playlist_new_public, "toggled", + G_CALLBACK(playlist_new_button_toggled), NULL); + g_signal_connect(playlist_new_private, "toggled", + G_CALLBACK(playlist_new_button_toggled), NULL); + gtk_table_attach_defaults(GTK_TABLE(table), playlist_new_shared, 0, 1, 1, 2); + gtk_table_attach_defaults(GTK_TABLE(table), playlist_new_public, 1, 2, 1, 2); + gtk_table_attach_defaults(GTK_TABLE(table), playlist_new_private, 2, 3, 1, 2); + + /* Third row: info bar saying why not */ + playlist_new_info = gtk_label_new(""); + gtk_table_attach_defaults(GTK_TABLE(table), playlist_new_info, + 0, 3, 2, 3); + + /* Fourth row: ok/cancel buttons */ + GtkWidget *hbox = create_buttons_box(playlist_new_buttons, + NPLAYLIST_NEW_BUTTONS, + gtk_hbox_new(FALSE, 0)); + gtk_table_attach_defaults(GTK_TABLE(table), hbox, 0, 3, 3, 4); + + gtk_container_add(GTK_CONTAINER(playlist_new_window), + frame_widget(table, NULL)); + + /* Set initial state of OK button */ + playlist_new_changed(); + + /* Display the window */ + gtk_widget_show_all(playlist_new_window); +} + /* Playlists window (list of playlists) ------------------------------------- */ /** @brief (Re-)populate the playlist tree model */ @@ -247,12 +409,7 @@ static void playlists_add(GtkButton attribute((unused)) *button, /* Unselect whatever is selected */ gtk_tree_selection_unselect_all(playlists_selection); fprintf(stderr, "playlists_add\n");/* TODO */ - /* We need to pop up a window asking for: - * - the name for the playlist - * - whether it is to be a public, private or shared playlist - * Moreover we should keep track of the known playlists and grey out OK - * if the name is a clash (as well as if it's actually invalid). - */ + playlist_new(); } /** @brief Called when the 'Delete' button is pressed */ @@ -360,7 +517,6 @@ static gboolean playlists_keypress(GtkWidget attribute((unused)) *widget, /** @brief Called when the playlist window is destroyed */ static void playlists_window_destroyed(GtkWidget attribute((unused)) *widget, GtkWidget **widget_pointer) { - fprintf(stderr, "playlists_window_destroy\n"); destroy_queuelike(&ql_playlist); *widget_pointer = NULL; } @@ -389,7 +545,7 @@ void edit_playlists(gpointer attribute((unused)) callback_data, g_signal_connect(playlists_window, "key-press-event", G_CALLBACK(playlists_keypress), 0); /* default size is too small */ - gtk_window_set_default_size(GTK_WINDOW(playlists_window), 240, 240); + gtk_window_set_default_size(GTK_WINDOW(playlists_window), 512, 240); GtkWidget *hbox = gtk_hbox_new(FALSE, 0); gtk_box_pack_start(GTK_BOX(hbox), playlists_window_list(), diff --git a/lib/Makefile.am b/lib/Makefile.am index 3c1e2f1..3505eb9 100644 --- a/lib/Makefile.am +++ b/lib/Makefile.am @@ -91,6 +91,7 @@ libdisorder_a_SOURCES=charset.c charset.h \ unicode.h unicode.c \ unidata.h unidata.c \ vacopy.h \ + validity.c validity.h \ vector.c vector.h \ version.c version.h \ wav.h wav.c \ diff --git a/lib/trackdb-int.h b/lib/trackdb-int.h index b21b5d9..3be725e 100644 --- a/lib/trackdb-int.h +++ b/lib/trackdb-int.h @@ -153,7 +153,6 @@ int trackdb_get_global_tid(const char *name, char **parsetags(const char *s); int tag_intersection(char **a, char **b); -int valid_username(const char *user); #endif /* TRACKDB_INT_H */ diff --git a/lib/trackdb-playlists.c b/lib/trackdb-playlists.c index fd33393..27d3319 100644 --- a/lib/trackdb-playlists.c +++ b/lib/trackdb-playlists.c @@ -33,6 +33,7 @@ #include "configuration.h" #include "vector.h" #include "eventlog.h" +#include "validity.h" static int trackdb_playlist_get_tid(const char *name, const char *who, @@ -54,43 +55,6 @@ static int trackdb_playlist_delete_tid(const char *name, const char *who, DB_TXN *tid); -/** @brief Parse a playlist name - * @param name Playlist name - * @param ownerp Where to put owner, or NULL - * @param sharep Where to put default sharing, or NULL - * @return 0 on success, -1 on error - * - * Playlists take the form USER.PLAYLIST or just PLAYLIST. The PLAYLIST part - * is alphanumeric and nonempty. USER is a username (see valid_username()). - */ -int playlist_parse_name(const char *name, - char **ownerp, - char **sharep) { - const char *dot = strchr(name, '.'), *share; - char *owner; - - if(dot) { - /* Owned playlist */ - owner = xstrndup(name, dot - name); - if(!valid_username(owner)) - return -1; - if(!valid_username(dot + 1)) - return -1; - share = "private"; - } else { - /* Shared playlist */ - if(!valid_username(name)) - return -1; - owner = 0; - share = "shared"; - } - if(ownerp) - *ownerp = owner; - if(sharep) - *sharep = xstrdup(share); - return 0; -} - /** @brief Check read access rights * @param name Playlist name * @param who Who wants to read diff --git a/lib/trackdb.c b/lib/trackdb.c index d2ebe07..628498a 100644 --- a/lib/trackdb.c +++ b/lib/trackdb.c @@ -59,6 +59,7 @@ #include "unidata.h" #include "base64.h" #include "sendmail.h" +#include "validity.h" #define RESCAN "disorder-rescan" #define DEADLOCK "disorder-deadlock" @@ -3013,32 +3014,6 @@ static int trusted(const char *user) { return n < config->trust.n; } -/** @brief Return non-zero for a valid username - * @param user Candidate username - * @return Nonzero if it's valid - * - * Currently we only allow the letters and digits in ASCII. We could be more - * liberal than this but it is a nice simple test. It is critical that - * semicolons are never allowed. - * - * NB also used by playlist_parse_name() to validate playlist names! - */ -int valid_username(const char *user) { - if(!*user) - return 0; - while(*user) { - const uint8_t c = *user++; - /* For now we are very strict */ - if((c >= 'a' && c <= 'z') - || (c >= 'A' && c <= 'Z') - || (c >= '0' && c <= '9')) - /* ok */; - else - return 0; - } - return 1; -} - /** @brief Add a user * @param user Username * @param password Initial password or NULL diff --git a/lib/trackdb.h b/lib/trackdb.h index 901a74a..1d7c8e9 100644 --- a/lib/trackdb.h +++ b/lib/trackdb.h @@ -184,9 +184,6 @@ void trackdb_add_rescanned(void (*rescanned)(void *ru), void *ru); int trackdb_rescan_underway(void); -int playlist_parse_name(const char *name, - char **ownerp, - char **sharep); int trackdb_playlist_get(const char *name, const char *who, char ***tracksp, diff --git a/lib/validity.c b/lib/validity.c new file mode 100644 index 0000000..408c4d5 --- /dev/null +++ b/lib/validity.c @@ -0,0 +1,98 @@ +/* + * This file is part of DisOrder + * Copyright (C) 2009 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 lib/validity.c + * @brief Various validity checks + */ +#include "common.h" +#include "validity.h" + +#include "mem.h" + +/** @brief Parse a playlist name + * @param name Playlist name + * @param ownerp Where to put owner, or NULL + * @param sharep Where to put default sharing, or NULL + * @return 0 on success, -1 on error + * + * Playlists take the form USER.PLAYLIST or just PLAYLIST. The PLAYLIST part + * is alphanumeric and nonempty. USER is a username (see valid_username()). + */ +int playlist_parse_name(const char *name, + char **ownerp, + char **sharep) { + const char *dot = strchr(name, '.'), *share; + char *owner; + + if(dot) { + /* Owned playlist */ + owner = xstrndup(name, dot - name); + if(!valid_username(owner)) + return -1; + if(!valid_username(dot + 1)) + return -1; + share = "private"; + } else { + /* Shared playlist */ + if(!valid_username(name)) + return -1; + owner = 0; + share = "shared"; + } + if(ownerp) + *ownerp = owner; + if(sharep) + *sharep = xstrdup(share); + return 0; +} + +/** @brief Return non-zero for a valid username + * @param user Candidate username + * @return Nonzero if it's valid + * + * Currently we only allow the letters and digits in ASCII. We could be more + * liberal than this but it is a nice simple test. It is critical that + * semicolons are never allowed. + * + * NB also used by playlist_parse_name() to validate playlist names! + */ +int valid_username(const char *user) { + if(!*user) + return 0; + while(*user) { + const uint8_t c = *user++; + /* For now we are very strict */ + if((c >= 'a' && c <= 'z') + || (c >= 'A' && c <= 'Z') + || (c >= '0' && c <= '9')) + /* ok */; + else + return 0; + } + return 1; +} + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +fill-column:79 +indent-tabs-mode:nil +End: +*/ diff --git a/lib/validity.h b/lib/validity.h new file mode 100644 index 0000000..8ce2505 --- /dev/null +++ b/lib/validity.h @@ -0,0 +1,43 @@ +/* + * This file is part of DisOrder + * Copyright (C) 2009 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 lib/validity.c + * @brief Various validity checks + */ +#ifndef VALIDITY_H +#define VALIDITY_H + +#include "common.h" +#include "validity.h" + +int playlist_parse_name(const char *name, + char **ownerp, + char **sharep); +int valid_username(const char *user); + +#endif /* VALIDITY_H */ + +/* +Local Variables: +c-basic-offset:2 +comment-column:40 +fill-column:79 +indent-tabs-mode:nil +End: +*/ -- [mdw]