summaryrefslogtreecommitdiff
path: root/apps/gui/folder_select.c
diff options
context:
space:
mode:
authorThomas Martitz <kugel@rockbox.org>2012-07-18 23:26:21 +0200
committerThomas Martitz <kugel@rockbox.org>2012-07-30 21:20:51 +0200
commit8c655cfdc09b0be326e7d9f190ae728d4e2bdc87 (patch)
tree668b5e8506dd8eea506067535f593a4c2524d021 /apps/gui/folder_select.c
parent9dd2eb49bec19de06c5cfd168a0e4cd4dc44c867 (diff)
downloadrockbox-8c655cfdc09b0be326e7d9f190ae728d4e2bdc87.zip
rockbox-8c655cfdc09b0be326e7d9f190ae728d4e2bdc87.tar.gz
rockbox-8c655cfdc09b0be326e7d9f190ae728d4e2bdc87.tar.bz2
rockbox-8c655cfdc09b0be326e7d9f190ae728d4e2bdc87.tar.xz
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
Diffstat (limited to 'apps/gui/folder_select.c')
-rw-r--r--apps/gui/folder_select.c474
1 files changed, 474 insertions, 0 deletions
diff --git a/apps/gui/folder_select.c b/apps/gui/folder_select.c
new file mode 100644
index 0000000..512b73f
--- /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 <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#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; i<start->children_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;
+}