From 30a23fdd6de8fb46e7b1349126a9ae6921cf7555 Mon Sep 17 00:00:00 2001 From: William Wilgus Date: Thu, 11 Nov 2021 00:05:29 -0500 Subject: folder_select.c move to plugin as db_folder_select handles the folder selection for autoresume paths and database paths folder_select uses the pluginbuf to build the directory tree hopefully having it as a plugin will make it stop corrupting TSR plugins like battery_bench and announce_status I left the original include and source in the gui directory for now there are currently no other users.. Change-Id: If40ccb5a2bec4286a0616c10dfa7e1479a592808 --- apps/menus/settings_menu.c | 9 +- apps/plugin.c | 1 + apps/plugin.h | 1 + apps/plugins/CATEGORIES | 1 + apps/plugins/SOURCES | 3 + apps/plugins/db_folder_select.c | 652 ++++++++++++++++++++++++++++++++++++++++ 6 files changed, 662 insertions(+), 5 deletions(-) create mode 100644 apps/plugins/db_folder_select.c diff --git a/apps/menus/settings_menu.c b/apps/menus/settings_menu.c index a5e747651a..94b697aada 100644 --- a/apps/menus/settings_menu.c +++ b/apps/menus/settings_menu.c @@ -48,7 +48,6 @@ #ifdef HAVE_DIRCACHE #include "dircache.h" #endif -#include "folder_select.h" #ifndef HAS_BUTTON_HOLD #include "mask_select.h" #endif @@ -56,6 +55,7 @@ #include "governor-ibasso.h" #include "usb-ibasso.h" #endif +#include "plugin.h" #ifndef HAS_BUTTON_HOLD static int selectivesoftlock_callback(int action, @@ -133,8 +133,7 @@ static void tagcache_update_with_splash(void) static int dirs_to_scan(void) { - if (folder_select(global_settings.tagcache_scan_paths, - sizeof(global_settings.tagcache_scan_paths))) + if(plugin_load(VIEWERS_DIR"/db_folder_select.rock", NULL) > PLUGIN_OK) { static const char *lines[] = {ID2P(LANG_TAGCACHE_BUSY), ID2P(LANG_TAGCACHE_FORCE_UPDATE)}; @@ -650,8 +649,8 @@ static int autoresume_nexttrack_callback(int action, break; case ACTION_EXIT_MENUITEM: if (global_settings.autoresume_automatic == AUTORESUME_NEXTTRACK_CUSTOM - && !folder_select(global_settings.autoresume_paths, - MAX_PATHNAME+1)) + && plugin_load(VIEWERS_DIR"/db_folder_select.rock", + str(LANG_AUTORESUME)) == PLUGIN_OK) { global_settings.autoresume_automatic = oldval; } diff --git a/apps/plugin.c b/apps/plugin.c index c9c3d047d2..a19b3ee226 100644 --- a/apps/plugin.c +++ b/apps/plugin.c @@ -346,6 +346,7 @@ static const struct plugin_api rockbox_api = { gui_syncyesno_run, simplelist_info_init, simplelist_show_list, + yesno_pop, /* action handling */ get_custom_action, diff --git a/apps/plugin.h b/apps/plugin.h index 6ca0488e06..e1b7c69f96 100644 --- a/apps/plugin.h +++ b/apps/plugin.h @@ -393,6 +393,7 @@ struct plugin_api { void (*simplelist_info_init)(struct simplelist_info *info, char* title, int count, void* data); bool (*simplelist_show_list)(struct simplelist_info *info); + bool (*yesno_pop)(const char* text); /* action handling */ int (*get_custom_action)(int context,int timeout, diff --git a/apps/plugins/CATEGORIES b/apps/plugins/CATEGORIES index 471a32b9e1..5b672fc8ab 100644 --- a/apps/plugins/CATEGORIES +++ b/apps/plugins/CATEGORIES @@ -22,6 +22,7 @@ clock,apps codebuster,games credits,viewers cube,demos +db_folder_select,viewers demystify,demos dice,games dict,apps diff --git a/apps/plugins/SOURCES b/apps/plugins/SOURCES index ec13c17dc9..0635a62d83 100644 --- a/apps/plugins/SOURCES +++ b/apps/plugins/SOURCES @@ -2,6 +2,9 @@ #if !defined(SIMULATOR) && (CONFIG_BATTERY_MEASURE != 0) battery_bench.c #endif +#ifdef HAVE_TAGCACHE +db_folder_select.c +#endif chessclock.c credits.c cube.c diff --git a/apps/plugins/db_folder_select.c b/apps/plugins/db_folder_select.c new file mode 100644 index 0000000000..7f51e520cb --- /dev/null +++ b/apps/plugins/db_folder_select.c @@ -0,0 +1,652 @@ +/*************************************************************************** + * __________ __ ___. + * Open \______ \ ____ ____ | | _\_ |__ _______ ___ + * Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ / + * Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < < + * Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \ + * \/ \/ \/ \/ \/ + * + * Copyright (C) 2012 Jonathan Gordon + * Copyright (C) 2012 Thomas Martitz +* * Copyright (C) 2021 William Wilgus + * + * 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 "plugin.h" +#ifdef ROCKBOX_HAS_LOGF +#define logf rb->logf +#else +#define logf(...) do { } while(0) +#endif + +/* + * 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; + struct folder* previous; + uint16_t children_count; + uint16_t depth; +}; + +static char *buffer_front, *buffer_end; + +static struct +{ + int32_t len; /* keeps count versus maxlen to give buffer full notification */ + uint32_t val; /* hash of all selected items */ + char buf[3];/* address used as identifier -- only \0 written to it */ + char maxlen_exceeded; /*0,1*/ +} hashed; + +static inline void get_hash(const char *key, uint32_t *hash, int len) +{ + *hash = rb->crc_32(key, len, *hash); +} + +static char* folder_alloc(size_t size) +{ + char* retval; + /* 32-bit aligned */ + size = ALIGN_UP(size, 4); + 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; +} +#if 0 +/* returns the buffer size required to store the path + \0 */ +static int get_full_pathsz(struct folder *start) +{ + int reql = 0; + struct folder *next = start; + do + { + reql += rb->strlen(next->name) + 1; + } while ((next = next->previous)); + + if (start->name[0] != '/') reql--; + if (--reql < 0) reql = 0; + return reql; +} +#endif + +static size_t get_full_path(struct folder *start, char *dst, size_t dst_sz) +{ + size_t pos = 0; + struct folder *prev, *cur = NULL, *next = start; + dst[0] = '\0'; /* for rb->strlcat to do its thing */ + /* First traversal R->L mutate nodes->previous to point at child */ + while (next->previous != NULL) /* stop at the root */ + { +#define PATHMUTATE() \ + ({ \ + prev = cur; \ + cur = next; \ + next = cur->previous;\ + cur->previous = prev; \ + }) + PATHMUTATE(); + } + /*swap the next and cur nodes to reverse direction */ + prev = next; + next = cur; + cur = prev; + /* Second traversal L->R mutate nodes->previous to point back at parent + * copy strings to buf as they go by */ + while (next != NULL) + { + PATHMUTATE(); + pos = rb->strlcat(dst, cur->name, dst_sz); + /* do not append slash to paths starting with slash */ + if (cur->name[0] != '/') + pos = rb->strlcat(dst, "/", dst_sz); + } + logf("get_full_path: (%d)[%s]", (int)pos, dst); + return pos; +#undef PATHMUTATE +} + +/* support function for rb->qsort() */ +static int compare(const void* p1, const void* p2) +{ + struct child *left = (struct child*)p1; + struct child *right = (struct child*)p2; + return rb->strcasecmp(left->name, right->name); +} + +static struct folder* load_folder(struct folder* parent, char *folder) +{ + DIR *dir; + char fullpath[MAX_PATH]; + + struct dirent *entry; + int child_count = 0; + char *first_child = NULL; + size_t len = 0; + + struct folder* this = (struct folder*)folder_alloc(sizeof(struct folder)); + if (this == NULL) + goto fail; + + if (parent) + { + len = get_full_path(parent, fullpath, sizeof(fullpath)); + if (len >= sizeof(fullpath)) + goto fail; + } + rb->strlcpy(&fullpath[len], folder, sizeof(fullpath) - len); + logf("load_folder: [%s]", fullpath); + + dir = rb->opendir(fullpath); + if (dir == NULL) + goto fail; + this->previous = parent; + this->name = folder; + this->children = NULL; + this->children_count = 0; + if (parent) + this->depth = parent->depth + 1; + + while ((entry = rb->readdir(dir))) { + /* skip anything not a directory */ + if ((rb->dir_get_info(dir, entry).attribute & ATTR_DIRECTORY) == 0) { + continue; + } + /* skip . and .. */ + char *dn = entry->d_name; + if ((dn[0] == '.') && (dn[1] == '\0' || (dn[1] == '.' && dn[2] == '\0'))) + continue; + /* copy entry name to end of buffer, save pointer */ + int len = rb->strlen((char *)entry->d_name); + char *name = folder_alloc_from_end(len+1); /*for NULL*/ + if (name == NULL) + { + rb->closedir(dir); + goto fail; + } + memcpy(name, (char *)entry->d_name, len+1); + child_count++; + first_child = name; + } + rb->closedir(dir); + /* now put the names in the array */ + this->children = (struct child*)folder_alloc(sizeof(struct child) * child_count); + + if (this->children == NULL) + goto fail; + + while (child_count) + { + struct child *child = &this->children[this->children_count++]; + child->name = first_child; + child->folder = NULL; + child->state = COLLAPSED; + while(*first_child++ != '\0'){};/* move to next name entry */ + child_count--; + } + rb->qsort(this->children, this->children_count, sizeof(struct child), compare); + + return this; +fail: + return NULL; +} + +struct folder* load_root(void) +{ + static struct child root_child; + /* reset the root for each call */ + root_child.name = "/"; + root_child.folder = NULL; + root_child.state = COLLAPSED; + + static struct folder root = { + .name = "", + .children = &root_child, + .children_count = 1, + .depth = 0, + .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); + + char *buf = buffer; + if ((int)buffer_len > parent->depth) + { + int i = parent->depth; + while(--i > 0) /* don't indent the parent /folders */ + *buf++ = '\t'; + } + *buf = '\0'; + rb->strlcat(buffer, this->name, buffer_len); + + if (this->state == EACCESS) + { /* append error message to the entry if unaccessible */ + size_t len = rb->strlcat(buffer, " ( ", buffer_len); + if (buffer_len > len) + { + rb->snprintf(&buffer[len], buffer_len - len, rb->str(LANG_READ_FAILED), ")"); + } + } + + 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 child_set_state_expand(struct child *this, struct folder *parent) +{ + int newstate = EACCESS; + if (this->folder == NULL) + this->folder = load_folder(parent, this->name); + + if (this->folder != NULL) + { + if(this->folder->children_count == 0) + newstate = SELECTED; + else + newstate = EXPANDED; + } + this->state = newstate; + return newstate; +} + +static int folder_action_callback(int action, struct gui_synclist *list) +{ + struct folder *root = (struct folder*)list->data; + struct folder *parent; + struct child *this = find_index(root, list->selected_item, &parent), *child; + int i; + + if (action == ACTION_STD_OK) + { + switch (this->state) + { + case EXPANDED: + this->state = SELECTED; + break; + case SELECTED: + this->state = COLLAPSED; + break; + case COLLAPSED: + child_set_state_expand(this, parent); + break; + case EACCESS: + /* cannot open, do nothing */ + return action; + } + action = ACTION_REDRAW; + } + else if (action == ACTION_STD_CONTEXT) + { + switch (this->state) + { + case EXPANDED: + for (i = 0; i < this->folder->children_count; i++) + { + child = &this->folder->children[i]; + switch (child->state) + { + case SELECTED: + case EXPANDED: + child->state = COLLAPSED; + break; + case COLLAPSED: + child->state = SELECTED; + break; + case EACCESS: + break; + } + } + break; + case SELECTED: + case COLLAPSED: + if (child_set_state_expand(this, parent) != EACCESS) + { + for (i = 0; i < (this->folder->children_count); i++) + { + child = &this->folder->children[i]; + child->state = SELECTED; + } + } + break; + case EACCESS: + /* cannot open, do nothing */ + return action; + } + action = ACTION_REDRAW; + } + if (action == ACTION_REDRAW) + list->nb_items = count_items(root); + return action; +} + +static struct child* find_from_filename(const char* filename, struct folder *root) +{ + if (!root) + return NULL; + const char *slash = rb->strchr(filename, '/'); + 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 (filename[0] == '/') /* in the loop nothing starts with '/' */ + { + logf("find_from_filename [%s]", filename); + /* filename begins with /. in this case root must be the + * top level folder */ + this = &root->children[0]; + if (filename[1] == '\0') + { /* filename == "/" */ + return this; + } + else /* filename == "/XXX/YYY". cascade down */ + goto cascade; + } + + for (int i = 0; i < root->children_count; i++) + { + this = &root->children[i]; + /* when slash == NULL n will be really large but \0 stops the compare */ + if (rb->strncasecmp(this->name, filename, slash - filename) == 0) + { + if (slash == NULL) + { /* filename == XXX */ + return this; + } + else + goto cascade; + } + } + return NULL; + +cascade: + /* filename == XXX/YYY. cascade down */ + child_set_state_expand(this, root); + while (slash[0] == '/') slash++; /* eat slashes */ + return find_from_filename(slash, this->folder); +} + +static int select_paths(struct folder* root, const char* filenames) +{ + /* Takes a list of filenames in a ':' delimited string + splits filenames at the ':' character loads each into buffer + selects each file in the folder list + + if last item or only item the rest of the string is copied to the buffer + *End the last item WITHOUT the ':' character /.rockbox/eqs:/.rockbox/wps\0* + */ + char buf[MAX_PATH]; + const int buflen = sizeof(buf); + + const char *fnp = filenames; + const char *lastfnp = fnp; + const char *sstr; + off_t len; + + while (fnp) + { + fnp = rb->strchr(fnp, ':'); + if (fnp) + { + len = fnp - lastfnp; + fnp++; + } + else /* no ':' get the rest of the string */ + len = rb->strlen(lastfnp); + + sstr = lastfnp; + lastfnp = fnp; + if (len <= 0 || len > buflen) + continue; + rb->strlcpy(buf, sstr, len + 1); + 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, size_t buflen) +{ + size_t len; + struct folder *curfolder; + char* name; + + for (int i = 0; i < root->children_count; i++) + { + struct child *this = &root->children[i]; + if (this->state == SELECTED) + { + if (this->folder == NULL) + { + curfolder = root; + name = this->name; + logf("save_folders_r: this->name[%s]", name); + } + else + { + curfolder = this->folder->previous; + name = this->folder->name; + logf("save_folders_r: this->folder->name[%s]", name); + } + + len = get_full_path(curfolder, buffer_front, buflen); + + if (len + 2 >= buflen) + continue; + + len += rb->snprintf(&buffer_front[len], buflen - len, "%s:", name); + logf("save_folders_r: [%s]", buffer_front); + if (dst != hashed.buf) + { + int dlen = rb->strlen(dst); + if (dlen + len >= maxlen) + continue; + rb->strlcpy(&dst[dlen], buffer_front, maxlen - dlen); + } + else + { + if (hashed.len + len >= maxlen) + { + hashed.maxlen_exceeded = 1; + continue; + } + get_hash(buffer_front, &hashed.val, len); + hashed.len += len; + } + } + else if (this->state == EXPANDED) + save_folders_r(this->folder, dst, maxlen, buflen); + } +} + +static uint32_t save_folders(struct folder *root, char* dst, size_t maxlen) +{ + hashed.len = 0; + hashed.val = 0; + hashed.maxlen_exceeded = 0; + size_t len = buffer_end - buffer_front; + dst[0] = '\0'; + save_folders_r(root, dst, maxlen, len); + len = rb->strlen(dst); + /* fix trailing ':' */ + if (len > 1) dst[len-1] = '\0'; + /*Notify - user will probably not see save dialog if nothing new got added*/ + if (hashed.maxlen_exceeded > 0) rb->splash(HZ *2, ID2P(LANG_SHOWDIR_BUFFER_FULL)); + return hashed.val; +} + +bool folder_select(char * header_text, char* setting, int setting_len) +{ + struct folder *root; + struct simplelist_info info; + size_t buf_size; + + buffer_front = rb->plugin_get_buffer(&buf_size); + buffer_end = buffer_front + buf_size; + logf("folder_select %d bytes free", (int)(buffer_end - buffer_front)); + root = load_root(); + + logf("folders in: %s", setting); + /* Load previous selection(s) */ + select_paths(root, setting); + /* get current hash to check for changes later */ + uint32_t hash = save_folders(root, hashed.buf, setting_len); + rb->simplelist_info_init(&info, header_text, + count_items(root), root); + info.get_name = folder_get_name; + info.action_callback = folder_action_callback; + info.get_icon = folder_get_icon; + rb->simplelist_show_list(&info); + logf("folder_select %d bytes free", (int)(buffer_end - buffer_front)); + /* done editing. check for changes */ + if (hash != save_folders(root, hashed.buf, setting_len)) + { /* prompt for saving changes and commit if yes */ + if (rb->yesno_pop(ID2P(LANG_SAVE_CHANGES))) + { + save_folders(root, setting, setting_len); + rb->settings_save(); + logf("folders out: %s", setting); + return true; + } + } + return false; +} + +/* plugin entry point */ +enum plugin_status plugin_start(const void* parameter) +{ + (void) parameter; + + if(parameter) + { + + if (rb->strcmp(parameter, rb->str(LANG_AUTORESUME)) == 0) + { + if (folder_select(rb->str(LANG_AUTORESUME), + rb->global_settings->autoresume_paths, + MAX_PATHNAME+1)) + { + return 1; + } + } + } + else if (folder_select(rb->str(LANG_SELECT_FOLDER), + rb->global_settings->tagcache_scan_paths, + sizeof(rb->global_settings->tagcache_scan_paths))) + { + return 1; + } + + return PLUGIN_OK; +} -- cgit v1.2.3