From 8c655cfdc09b0be326e7d9f190ae728d4e2bdc87 Mon Sep 17 00:00:00 2001 From: Thomas Martitz Date: Wed, 18 Jul 2012 23:26:21 +0200 Subject: [PATCH] New GUI browser to select one (or more) folders. The browser lets the user pick one or more directories in a convinient GUI browser. The initial directory list is read from a string (separated by colons) and the resulting list is written back to the same string (again separated by colons). Note: The work was initially done by Jonathan Gordon, however I changed it substantially so I claim autorship. This selector is going to be used for autoresume and database scan folders. Change-Id: Id1d3186dad783411eb5c6056ce93f5b6123c7aa0 --- apps/SOURCES | 1 + apps/gui/folder_select.c | 474 +++++++++++++++++++++++++++++++++++++++ apps/gui/folder_select.h | 35 +++ apps/lang/english.lang | 14 ++ apps/misc.c | 31 +++ apps/misc.h | 1 + 6 files changed, 556 insertions(+) create mode 100644 apps/gui/folder_select.c create mode 100644 apps/gui/folder_select.h diff --git a/apps/SOURCES b/apps/SOURCES index 8749c36c87..6005460a5a 100644 --- a/apps/SOURCES +++ b/apps/SOURCES @@ -86,6 +86,7 @@ gui/pitchscreen.c #ifdef HAVE_QUICKSCREEN gui/quickscreen.c #endif +gui/folder_select.c gui/wps.c gui/scrollbar.c diff --git a/apps/gui/folder_select.c b/apps/gui/folder_select.c new file mode 100644 index 0000000000..512b73f588 --- /dev/null +++ b/apps/gui/folder_select.c @@ -0,0 +1,474 @@ +/*************************************************************************** + * __________ __ ___. + * Open \______ \ ____ ____ | | _\_ |__ _______ ___ + * Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ / + * Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < < + * Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \ + * \/ \/ \/ \/ \/ + * + * Copyright (C) 2012 Jonathan Gordon + * Copyright (C) 2012 Thomas Martitz + * + * 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 software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY + * KIND, either express or implied. + * + ****************************************************************************/ + +#include +#include +#include +#include "inttypes.h" +#include "config.h" +#include "core_alloc.h" +#include "filetypes.h" +#include "lang.h" +#include "language.h" +#include "list.h" +#include "plugin.h" + + +/* + * Order for changing child states: + * 1) expand folder (skip to 3 if empty, skip to 4 if cannot be opened) + * 2) collapse and select + * 3) unselect (skip to 1) + * 4) do nothing + */ + +enum child_state { + EXPANDED, + SELECTED, + COLLAPSED, + EACCESS, +}; + +struct child { + char* name; + struct folder *folder; + enum child_state state; +}; + +struct folder { + char *name; + struct child *children; + int children_count; + int depth; + + struct folder* previous; +}; + +static char *buffer_front, *buffer_end; +static char* folder_alloc(size_t size) +{ + char* retval; + /* 32-bit aligned */ + size = (size + 3) & ~3; + if (buffer_front + size > buffer_end) + { + return NULL; + } + retval = buffer_front; + buffer_front += size; + return retval; +} + +static char* folder_alloc_from_end(size_t size) +{ + if (buffer_end - size < buffer_front) + { + return NULL; + } + buffer_end -= size; + return buffer_end; +} + +static void get_full_path_r(struct folder *start, char* dst) +{ + if (start->previous) + get_full_path_r(start->previous, dst); + + if (start->name && start->name[0] && strcmp(start->name, "/")) + { + strlcat(dst, "/", MAX_PATH); + strlcat(dst, start->name, MAX_PATH); + } +} + +static char* get_full_path(struct folder *start) +{ + static char buffer[MAX_PATH]; + + buffer[0] = '\0'; + + get_full_path_r(start, buffer); + + return buffer; +} + +/* support function for qsort() */ +static int compare(const void* p1, const void* p2) +{ + struct child *left = (struct child*)p1; + struct child *right = (struct child*)p2; + return strcasecmp(left->name, right->name); +} + +static struct folder* load_folder(struct folder* parent, char *folder) +{ + DIR *dir; + char* path = get_full_path(parent); + char fullpath[MAX_PATH]; + struct dirent *entry; + struct folder* this = (struct folder*)folder_alloc(sizeof(struct folder)); + int child_count = 0; + char *first_child = NULL; + + if (!strcmp(folder,"/")) + strlcpy(fullpath, folder, 2); + else + snprintf(fullpath, MAX_PATH, "%s/%s", parent ? path : "", folder); + + if (!this) + return NULL; + dir = opendir(fullpath); + if (!dir) + return NULL; + this->previous = parent; + this->name = folder; + this->children = NULL; + this->children_count = 0; + this->depth = parent ? parent->depth + 1 : 0; + + while ((entry = readdir(dir))) { + int len = strlen((char *)entry->d_name); + struct dirinfo info; + + info = dir_get_info(dir, entry); + + /* skip anything not a directory */ + if ((info.attribute & ATTR_DIRECTORY) == 0) { + continue; + } + /* skip directories . and .. */ + if ((!strcmp((char *)entry->d_name, ".")) || + (!strcmp((char *)entry->d_name, ".."))) { + continue; + } + char *name = folder_alloc_from_end(len+1); + if (!name) + return NULL; + memcpy(name, (char *)entry->d_name, len+1); + child_count++; + first_child = name; + } + closedir(dir); + /* now put the names in the array */ + this->children = (struct child*)folder_alloc(sizeof(struct child) * child_count); + + if (!this->children) + return NULL; + while (child_count) + { + this->children[this->children_count].name = first_child; + this->children[this->children_count].folder = NULL; + this->children[this->children_count].state = COLLAPSED; + this->children_count++; + first_child += strlen(first_child) + 1; + child_count--; + } + qsort(this->children, this->children_count, sizeof(struct child), compare); + + return this; +} + +struct folder* load_root(void) +{ + static struct child root_child; + + root_child.name = "/"; + root_child.folder = NULL; + root_child.state = COLLAPSED; + + static struct folder root = { + .name = "", + .children = &root_child, + .children_count = 1, + .depth = -1, + .previous = NULL, + }; + + return &root; +} + +static int count_items(struct folder *start) +{ + int count = 0; + int i; + + for (i=0; ichildren_count; i++) + { + struct child *foo = &start->children[i]; + if (foo->state == EXPANDED) + count += count_items(foo->folder); + count++; + } + return count; +} + +static struct child* find_index(struct folder *start, int index, struct folder **parent) +{ + int i = 0; + + *parent = NULL; + + while (i < start->children_count) + { + struct child *foo = &start->children[i]; + if (i == index) + { + *parent = start; + return foo; + } + i++; + if (foo->state == EXPANDED) + { + struct child *bar = find_index(foo->folder, index - i, parent); + if (bar) + { + return bar; + } + index -= count_items(foo->folder); + } + } + return NULL; +} + +static const char * folder_get_name(int selected_item, void * data, + char * buffer, size_t buffer_len) +{ + struct folder *root = (struct folder*)data; + struct folder *parent; + struct child *this = find_index(root, selected_item , &parent); + + buffer[0] = '\0'; + + if (parent->depth >= 0) + for(int i = 0; i <= parent->depth; i++) + strcat(buffer, "\t"); + + strlcat(buffer, this->name, buffer_len); + + if (this->state == EACCESS) + { /* append error message to the entry if unaccessible */ + size_t len = strlcat(buffer, " (", buffer_len); + if (buffer_len > len) + { + snprintf(&buffer[len], buffer_len - len, str(LANG_READ_FAILED), + this->name); + strlcat(buffer, ")", buffer_len); + } + } + + return buffer; +} + +static enum themable_icons folder_get_icon(int selected_item, void * data) +{ + struct folder *root = (struct folder*)data; + struct folder *parent; + struct child *this = find_index(root, selected_item, &parent); + + switch (this->state) + { + case SELECTED: + return Icon_Cursor; + case COLLAPSED: + return Icon_Folder; + case EXPANDED: + return Icon_Submenu; + case EACCESS: + return Icon_Questionmark; + } + return Icon_NOICON; +} + +static int folder_action_callback(int action, struct gui_synclist *list) +{ + struct folder *root = (struct folder*)list->data; + + if (action == ACTION_STD_OK) + { + struct folder *parent; + struct child *this = find_index(root, list->selected_item, &parent); + switch (this->state) + { + case EXPANDED: + this->state = SELECTED; + break; + case SELECTED: + this->state = COLLAPSED; + break; + case COLLAPSED: + if (this->folder == NULL) + this->folder = load_folder(parent, this->name); + this->state = this->folder ? (this->folder->children_count == 0 ? + SELECTED : EXPANDED) : EACCESS; + break; + case EACCESS: + /* cannot open, do nothing */ + break; + } + list->nb_items = count_items(root); + return ACTION_REDRAW; + } + return action; +} + +static struct child* find_from_filename(char* filename, struct folder *root) +{ + char *slash = strchr(filename, '/'); + int i = 0; + if (slash) + *slash = '\0'; + if (!root) + return NULL; + + struct child *this; + + /* filenames beginning with a / are specially treated as the + * loop below can't handle them. they can only occur on the first, + * and not recursive, calls to this function.*/ + if (slash == filename) + { + /* filename begins with /. in this case root must be the + * top level folder */ + this = &root->children[0]; + if (!slash[1]) + { /* filename == "/" */ + return this; + } + else + { + /* filename == "/XXX/YYY". cascade down */ + if (!this->folder) + this->folder = load_folder(root, this->name); + this->state = EXPANDED; + /* recurse with XXX/YYY */ + return find_from_filename(slash+1, this->folder); + } + } + + while (i < root->children_count) + { + this = &root->children[i]; + if (!strcasecmp(this->name, filename)) + { + if (!slash) + { /* filename == XXX */ + return this; + } + else + { + /* filename == XXX/YYY. cascade down */ + if (!this->folder) + this->folder = load_folder(root, this->name); + this->state = EXPANDED; + return find_from_filename(slash+1, this->folder); + } + } + i++; + } + return NULL; +} + +/* _modifies_ buf */ +int select_paths(struct folder* root, char* buf) +{ + struct child *item = find_from_filename(buf, root); + if (item) + item->state = SELECTED; + return 0; +} + +static void save_folders_r(struct folder *root, char* dst, size_t maxlen) +{ + int i = 0; + + while (i < root->children_count) + { + struct child *this = &root->children[i]; + if (this->state == SELECTED) + { + if (this->folder) + snprintf(buffer_front, buffer_end - buffer_front, + "%s:", get_full_path(this->folder)); + else + snprintf(buffer_front, buffer_end - buffer_front, + "%s/%s:", get_full_path(root), this->name); + strlcat(dst, buffer_front, maxlen); + } + else if (this->state == EXPANDED) + save_folders_r(this->folder, dst, maxlen); + i++; + } +} + +static void save_folders(struct folder *root, char* dst, size_t maxlen) +{ + int len; + dst[0] = '\0'; + save_folders_r(root, dst, maxlen); + len = strlen(dst); + /* fix trailing ':' */ + if (len > 1) dst[len-1] = '\0'; +} + +bool folder_select(char* setting, int setting_len) +{ + struct folder *root; + struct simplelist_info info; + size_t buf_size; + /* 32 separate folders should be Enough For Everybody(TM) */ + char *vect[32]; + char copy[setting_len]; + int nb_items; + + /* copy onto stack as split_string() modifies it */ + strlcpy(copy, setting, setting_len); + nb_items = split_string(copy, ':', vect, ARRAYLEN(vect)); + + buffer_front = plugin_get_buffer(&buf_size); + buffer_end = buffer_front + buf_size; + root = load_root(); + + if (nb_items > 0) + { + for(int i = 0; i < nb_items; i++) + select_paths(root, vect[i]); + } + + simplelist_info_init(&info, str(LANG_SELECT_FOLDER), + count_items(root), root); + info.get_name = folder_get_name; + info.action_callback = folder_action_callback; + info.get_icon = folder_get_icon; + simplelist_show_list(&info); + + /* done editing. check for changes */ + save_folders(root, copy, setting_len); + if (strcmp(copy, setting)) + { /* prompt for saving changes and commit if yes */ + if (yesno_pop(ID2P(LANG_SAVE_CHANGES))) + { + strcpy(setting, copy); + settings_save(); + return true; + } + } + return false; +} diff --git a/apps/gui/folder_select.h b/apps/gui/folder_select.h new file mode 100644 index 0000000000..1c7e559532 --- /dev/null +++ b/apps/gui/folder_select.h @@ -0,0 +1,35 @@ +/*************************************************************************** + * __________ __ ___. + * Open \______ \ ____ ____ | | _\_ |__ _______ ___ + * Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ / + * Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < < + * Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \ + * \/ \/ \/ \/ \/ + * + * Copyright (C) 2011 Jonathan Gordon + * + * 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 software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY + * KIND, either express or implied. + * + ****************************************************************************/ + +#ifndef __FOLDER_SELECT_H__ +#define __FOLDER_SELECT_H__ + +/** + * A GUI browser to select folders from the file system + * + * It reads a list of folders, separated by colons (:) from setting + * and pre-selects them in the UI. If the user is done it writes the new + * list back to setting (again separated by colons), assuming the + * user confirms the yesno dialog. + * + * Returns true if the the folder list has changed, otherwise false */ +bool folder_select(char* setting, int setting_len); + +#endif /* __FOLDER_SELECT_H__ */ diff --git a/apps/lang/english.lang b/apps/lang/english.lang index 7366a80030..42acad2609 100644 --- a/apps/lang/english.lang +++ b/apps/lang/english.lang @@ -13086,3 +13086,17 @@ swcodec: "Custom" + + id: LANG_SELECT_FOLDER + desc: in settings_menu + user: core + + *: "Select one or more directories" + + + *: "Select one or more directories" + + + *: "Select one or more directories" + + diff --git a/apps/misc.c b/apps/misc.c index 5b20b35c17..d7d4bdd2f9 100644 --- a/apps/misc.c +++ b/apps/misc.c @@ -1075,6 +1075,37 @@ void format_time(char* buf, int buf_size, long t) } } +/** + * Splits str at each occurence of split_char and puts the substrings into vector, + * but at most vector_lenght items. Empty substrings are ignored. + * + * Modifies str by replacing each split_char following a substring with nul + * + * Returns the number of substrings found, i.e. the number of valid strings + * in vector + */ +int split_string(char *str, const char split_char, char *vector[], const int vector_length) +{ + int i; + char *p = str; + + /* skip leading splitters */ + while(*p == split_char) p++; + + /* *p in the condition takes care of trailing splitters */ + for(i = 0; p && *p && i < vector_length; i++) + { + vector[i] = p; + if ((p = strchr(p, split_char))) + { + *p++ = '\0'; + while(*p == split_char) p++; /* skip successive splitters */ + } + } + + return i; +} + /** Open a UTF-8 file and set file descriptor to first byte after BOM. * If no BOM is present this behaves like open(). diff --git a/apps/misc.h b/apps/misc.h index 7ca8d75930..994e7775d2 100644 --- a/apps/misc.h +++ b/apps/misc.h @@ -73,6 +73,7 @@ extern int show_logo(void); #define BOM_UTF_16_BE "\xfe\xff" #define BOM_UTF_16_SIZE 2 +int split_string(char *str, const char needle, char *vector[], int vector_length); int open_utf8(const char* pathname, int flags); #ifdef BOOTFILE