summaryrefslogtreecommitdiffstats
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
parent9dd2eb49bec19de06c5cfd168a0e4cd4dc44c867 (diff)
downloadrockbox-8c655cfdc09b0be326e7d9f190ae728d4e2bdc87.tar.gz
rockbox-8c655cfdc09b0be326e7d9f190ae728d4e2bdc87.zip
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
-rw-r--r--apps/SOURCES1
-rw-r--r--apps/gui/folder_select.c474
-rw-r--r--apps/gui/folder_select.h35
-rw-r--r--apps/lang/english.lang14
-rw-r--r--apps/misc.c31
-rw-r--r--apps/misc.h1
6 files changed, 556 insertions, 0 deletions
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 <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;
+}
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"
</voice>
</phrase>
+<phrase>
+ id: LANG_SELECT_FOLDER
+ desc: in settings_menu
+ user: core
+ <source>
+ *: "Select one or more directories"
+ </source>
+ <dest>
+ *: "Select one or more directories"
+ </dest>
+ <voice>
+ *: "Select one or more directories"
+ </voice>
+</phrase>
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