522 lines
18 KiB
C
522 lines
18 KiB
C
/* src/psp/filesel.c: Simple file selector for use with PSP menu
|
|
Copyright 2009 Andrew Church
|
|
|
|
This file is part of Yabause.
|
|
|
|
Yabause 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.
|
|
|
|
Yabause 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 Yabause; if not, write to the Free Software
|
|
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
|
*/
|
|
|
|
#include "common.h"
|
|
|
|
#include "display.h"
|
|
#include "filesel.h"
|
|
#include "font.h"
|
|
#include "sys.h"
|
|
|
|
/*************************************************************************/
|
|
/****************************** Local data *******************************/
|
|
/*************************************************************************/
|
|
|
|
/* File selector data structure (exported as opaque type FileSelector) */
|
|
|
|
struct FileSelector_ {
|
|
char *title; // Window title
|
|
|
|
char *dir; // Current directory (note that we don't support
|
|
// changing directories at present)
|
|
SceUID scandir_thread; // If nonzero, thread ID of directory scan thread;
|
|
// if zero, indicates that scan is complete.
|
|
/* The file list MUST NOT be accessed by the main thread while
|
|
* scandir_thread is nonzero */
|
|
char **file_list; // List of files in the current directory (each
|
|
// entry is malloc'd)
|
|
int num_files; // Number of files in file_list[]; negative
|
|
// indicates an error scanning the directory
|
|
|
|
int top_file; // Index of top file shown in window
|
|
int selected_file; // Selected file index (negative = cancelled)
|
|
int num_lines; // Number of lines visible in file selector window
|
|
// (set after first filesel_draw() call)
|
|
uint8_t cursor_timer; // Frame counter for cursor flashing
|
|
|
|
uint8_t done; // Nonzero when user selects a file or cancels
|
|
};
|
|
|
|
/*************************************************************************/
|
|
|
|
/* Display parameters */
|
|
|
|
/* Window size */
|
|
#define WINDOW_WIDTH 360
|
|
#define WINDOW_HEIGHT 208
|
|
|
|
/* Window colors */
|
|
#define BGCOLOR_TITLE 0xFF804060 // Title bar
|
|
#define BGCOLOR_LIST 0xFF604040 // File list area
|
|
#define BORDER_COLOR_HI 0xFFFFECEC // Border lines (bright)
|
|
#define BORDER_COLOR_LO 0xFF000000 // Border lines (shadow)
|
|
|
|
/* Cursor parameters */
|
|
#define CURSOR_COLOR 0x80FFECEC
|
|
#define CURSOR_PERIOD 60 // frames
|
|
|
|
/* Text colors */
|
|
#define TEXTCOLOR_TITLE 0xFFFFECEC
|
|
#define TEXTCOLOR_FILE 0xFFFFECEC
|
|
#define TEXTCOLOR_DISABLED 0xFF807474 // "Scanning directory...", etc.
|
|
#define TEXTCOLOR_ERROR 0xFF5540FF
|
|
|
|
/*************************************************************************/
|
|
|
|
/* Local function declarations */
|
|
|
|
static void scandir_thread(SceSize args, void *argp);
|
|
|
|
/*************************************************************************/
|
|
/************************** Interface functions **************************/
|
|
/*************************************************************************/
|
|
|
|
/**
|
|
* filesel_create: Create a new file selector.
|
|
*
|
|
* [Parameters]
|
|
* title: File selector window title
|
|
* dir: Directory to select file from
|
|
* [Return value]
|
|
* Newly-created file selector, or NULL on error
|
|
*/
|
|
FileSelector *filesel_create(const char *title, const char *dir)
|
|
{
|
|
PRECOND(title != NULL, goto error_return);
|
|
PRECOND(dir != NULL, goto error_return);
|
|
|
|
FileSelector *filesel = malloc(sizeof(*filesel));
|
|
if (UNLIKELY(!filesel)) {
|
|
DMSG("No memory for FileSelector");
|
|
goto error_return;
|
|
}
|
|
filesel->title = strdup(title);
|
|
if (UNLIKELY(!filesel->title)) {
|
|
DMSG("No memory for title");
|
|
goto error_free_filesel;
|
|
}
|
|
filesel->dir = strdup(dir);
|
|
if (UNLIKELY(!filesel->dir)) {
|
|
DMSG("No memory for current directory");
|
|
goto error_free_title;
|
|
}
|
|
filesel->scandir_thread = 0;
|
|
filesel->file_list = NULL;
|
|
filesel->num_files = 0;
|
|
filesel->top_file = 0;
|
|
filesel->selected_file = 0;
|
|
filesel->num_lines = 0;
|
|
filesel->cursor_timer = 0;
|
|
filesel->done = 0;
|
|
|
|
/* We only ever have one file selector open at once, so we just use a
|
|
* static thread name */
|
|
int32_t thread = sys_start_thread("YabauseScanDirThread", scandir_thread,
|
|
THREADPRI_MAIN+1, 0x1000,
|
|
sizeof(filesel), &filesel);
|
|
if (thread & 0x80000000) {
|
|
DMSG("Failed to start scandir thread: %s", psp_strerror(thread));
|
|
goto error_free_dir;
|
|
}
|
|
|
|
filesel->scandir_thread = thread;
|
|
return filesel;
|
|
|
|
error_free_dir:
|
|
free(filesel->dir);
|
|
error_free_title:
|
|
free(filesel->title);
|
|
error_free_filesel:
|
|
free(filesel);
|
|
error_return:
|
|
return NULL;
|
|
}
|
|
|
|
/*************************************************************************/
|
|
|
|
/**
|
|
* filesel_process: Process input for a file selector.
|
|
*
|
|
* [Parameters]
|
|
* filesel: File selector
|
|
* buttons: Newly-pressed (or repeating) buttons (PSP_CTRL_* bitmask)
|
|
* [Return value]
|
|
* None
|
|
*/
|
|
void filesel_process(FileSelector *filesel, uint32_t buttons)
|
|
{
|
|
PRECOND(filesel != NULL, return);
|
|
|
|
/* If the user wants to cancel, give that top priority */
|
|
if (buttons & PSP_CTRL_CROSS) {
|
|
filesel->selected_file = -1;
|
|
filesel->done = 1;
|
|
return;
|
|
}
|
|
|
|
/* If the scanning thread is still running, there's nothing to do */
|
|
if (filesel->scandir_thread) {
|
|
return;
|
|
}
|
|
|
|
/* Update the cursor timer (assume we're called once per frame) */
|
|
filesel->cursor_timer = (filesel->cursor_timer + 1) % CURSOR_PERIOD;
|
|
|
|
/* If we have a file list, let the user move the cursor around or
|
|
* select a file; note that scrolling is handled at drawing time, when
|
|
* we know how many lines will fit in the file list display area */
|
|
if (filesel->num_files > 0) {
|
|
if (buttons & PSP_CTRL_UP) {
|
|
if (filesel->selected_file > 0) {
|
|
filesel->selected_file--;
|
|
filesel->cursor_timer = 0;
|
|
}
|
|
} else if (buttons & PSP_CTRL_DOWN) {
|
|
if (filesel->selected_file < filesel->num_files - 1) {
|
|
filesel->selected_file++;
|
|
filesel->cursor_timer = 0;
|
|
}
|
|
} else if (buttons & PSP_CTRL_LEFT) {
|
|
/* Use the number of visible lines calculated in filesel_draw().
|
|
* If we get here on the very first filesel_process() call,
|
|
* filesel->num_lines will be zero, so this becomes a no-op. */
|
|
int num_to_scroll = filesel->num_lines;
|
|
if (num_to_scroll > filesel->top_file) {
|
|
num_to_scroll = filesel->top_file;
|
|
}
|
|
filesel->top_file -= num_to_scroll;
|
|
filesel->selected_file -= num_to_scroll;
|
|
filesel->cursor_timer = 0;
|
|
} else if (buttons & PSP_CTRL_RIGHT) {
|
|
int max_top = filesel->num_files - filesel->num_lines;
|
|
if (max_top < 0) {
|
|
max_top = 0;
|
|
}
|
|
int num_to_scroll = filesel->num_lines;
|
|
if (num_to_scroll > max_top - filesel->top_file) {
|
|
num_to_scroll = max_top - filesel->top_file;
|
|
}
|
|
filesel->top_file += num_to_scroll;
|
|
filesel->selected_file += num_to_scroll;
|
|
filesel->cursor_timer = 0;
|
|
} else if (buttons & PSP_CTRL_CIRCLE) {
|
|
filesel->done = 1;
|
|
}
|
|
}
|
|
}
|
|
|
|
/*************************************************************************/
|
|
|
|
/**
|
|
* filesel_draw: Draw a file selector.
|
|
*
|
|
* [Parameters]
|
|
* filesel: File selector
|
|
* [Return value]
|
|
* None
|
|
*/
|
|
void filesel_draw(FileSelector *filesel)
|
|
{
|
|
PRECOND(filesel != NULL, return);
|
|
|
|
const int x1 = DISPLAY_WIDTH/2 - WINDOW_WIDTH/2;
|
|
const int y1 = DISPLAY_HEIGHT/2 - WINDOW_HEIGHT/2;
|
|
const int x2 = x1 + (WINDOW_WIDTH - 1);
|
|
const int y2 = y1 + (WINDOW_HEIGHT - 1);
|
|
|
|
/* Draw the outside border */
|
|
|
|
display_fill_box(x1, y1, x2-1, y1, BORDER_COLOR_HI);
|
|
display_fill_box(x1+1, y1+1, x2, y1+1, BORDER_COLOR_LO);
|
|
display_fill_box(x1, y1+1, x1, y2-1, BORDER_COLOR_HI);
|
|
display_fill_box(x1+1, y1+2, x1+1, y2, BORDER_COLOR_LO);
|
|
display_fill_box(x2-1, y1+1, x2-1, y2-1, BORDER_COLOR_HI);
|
|
display_fill_box(x2, y2+2, x2, y2, BORDER_COLOR_LO);
|
|
display_fill_box(x1+1, y2-1, x2-2, y2-1, BORDER_COLOR_HI);
|
|
display_fill_box(x1, y2, x2-1, y2, BORDER_COLOR_LO);
|
|
|
|
/* Draw the title bar */
|
|
|
|
const int title_text_y = y1+4;
|
|
const int title_y2 = title_text_y + FONT_HEIGHT + 2;
|
|
display_fill_box(x1+1, title_y2, x2-2, title_y2, BORDER_COLOR_HI);
|
|
display_fill_box(x1+2, title_y2+1, x2-1, title_y2+1, BORDER_COLOR_LO);
|
|
display_fill_box(x1+2, y1+2, x2-2, title_y2-2, BGCOLOR_TITLE);
|
|
font_printf((x1+x2)/2, title_text_y, 0, TEXTCOLOR_TITLE,
|
|
"%s", filesel->title);
|
|
|
|
/* Draw the file list */
|
|
|
|
display_fill_box(x1+2, title_y2+2, x2-2, y2-2, BGCOLOR_LIST);
|
|
const int list_x1 = x1 + 4;
|
|
const int list_y1 = title_y2 + 4;
|
|
const int list_x2 = x2 - 4;
|
|
const int list_y2 = y2 - 4;
|
|
const int line_height = FONT_HEIGHT + 2;
|
|
const int num_lines = ((list_y2+1) - list_y1) / line_height;
|
|
filesel->num_lines = num_lines; // Save for reference in filesel_process()
|
|
int y = list_y1 + (((list_y2+1)-list_y1) - (num_lines*line_height)) / 2;
|
|
if (filesel->scandir_thread) {
|
|
font_printf(list_x1, y, -1, TEXTCOLOR_DISABLED,
|
|
"(Scanning directory...)");
|
|
} else if (filesel->num_files < 0) {
|
|
font_printf(list_x1, y, -1, TEXTCOLOR_ERROR,
|
|
"(Error scanning directory!)");
|
|
} else if (filesel->num_files == 0) {
|
|
font_printf(list_x1, y, -1, TEXTCOLOR_DISABLED,
|
|
"(No files found.)");
|
|
} else {
|
|
/* Scroll the list if needed */
|
|
if (filesel->selected_file < filesel->top_file) {
|
|
filesel->top_file = filesel->selected_file;
|
|
} else if (filesel->selected_file >= filesel->top_file + num_lines) {
|
|
filesel->top_file = filesel->selected_file - (num_lines-1);
|
|
}
|
|
/* List as many files as will fit in the window */
|
|
int i;
|
|
for (i = filesel->top_file;
|
|
i < filesel->num_files && i < filesel->top_file + num_lines;
|
|
i++, y += line_height
|
|
) {
|
|
// FIXME: handle overlength filenames in a more general way
|
|
const int maxlen = ((list_x2+1) - list_x1) / 6;
|
|
if (strlen(filesel->file_list[i]) > maxlen) {
|
|
font_printf(list_x1, y, -1, TEXTCOLOR_FILE,
|
|
"%.*s...", maxlen-3, filesel->file_list[i]);
|
|
} else {
|
|
font_printf(list_x1, y, -1, TEXTCOLOR_FILE,
|
|
"%s", filesel->file_list[i]);
|
|
}
|
|
if (filesel->selected_file == i) {
|
|
const float cursor_alpha =
|
|
(sinf((filesel->cursor_timer / (float)CURSOR_PERIOD)
|
|
* (float)M_TWOPI) + 1) / 2;
|
|
const uint32_t cursor_alpha_byte =
|
|
floorf((CURSOR_COLOR>>24 & 0xFF) * cursor_alpha + 0.5f);
|
|
display_fill_box(list_x1-1, y-1, list_x2+1, y+FONT_HEIGHT,
|
|
cursor_alpha_byte<<24
|
|
| (CURSOR_COLOR & 0x00FFFFFF));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/*************************************************************************/
|
|
|
|
/**
|
|
* filesel_done: Return whether a file selector's work is done (i.e.,
|
|
* whether the user has either selected a file or cancelled the selector).
|
|
*
|
|
* [Parameters]
|
|
* filesel: File selector
|
|
* [Return value]
|
|
* True (nonzero) if any of the following are true:
|
|
* - The user selected a file
|
|
* - The user cancelled the file selector
|
|
* - An error occurred which prevents the file selector's
|
|
* processing from continuing normally
|
|
* False (zero) if none of the above are true
|
|
*/
|
|
int filesel_done(FileSelector *filesel)
|
|
{
|
|
PRECOND(filesel != NULL, return 1);
|
|
|
|
return filesel->done;
|
|
}
|
|
|
|
/*-----------------------------------------------------------------------*/
|
|
|
|
/**
|
|
* filesel_selected_file: Return the name of the file selected by the
|
|
* user, if any.
|
|
*
|
|
* [Parameters]
|
|
* filesel: File selector
|
|
* [Return value]
|
|
* The name of the file selected by the user, or NULL if the user has
|
|
* not selected a file (including if the user has cancelled the file
|
|
* selector)
|
|
*/
|
|
const char *filesel_selected_file(FileSelector *filesel)
|
|
{
|
|
PRECOND(filesel != NULL, return NULL);
|
|
|
|
if (filesel->done && filesel->selected_file >= 0) {
|
|
return filesel->file_list[filesel->selected_file];
|
|
} else {
|
|
return NULL;
|
|
}
|
|
}
|
|
|
|
/*************************************************************************/
|
|
|
|
/**
|
|
* filesel_destroy: Destroy a file selector. Does nothing if filesel==NULL.
|
|
*
|
|
* [Parameters]
|
|
* filesel: File selector to destroy
|
|
* [Return value]
|
|
* None
|
|
*/
|
|
void filesel_destroy(FileSelector *filesel)
|
|
{
|
|
if (filesel) {
|
|
if (filesel->scandir_thread) {
|
|
sceKernelTerminateDeleteThread(filesel->scandir_thread);
|
|
}
|
|
int i;
|
|
for (i = 0; i < filesel->num_files; i++) {
|
|
free(filesel->file_list[i]);
|
|
}
|
|
free(filesel->file_list);
|
|
free(filesel->dir);
|
|
free(filesel->title);
|
|
free(filesel);
|
|
}
|
|
}
|
|
|
|
/*************************************************************************/
|
|
/**************************** Local routines *****************************/
|
|
/*************************************************************************/
|
|
|
|
/**
|
|
* scandir_thread: Thread used to scan a directory for files. Updates the
|
|
* file_list[] and num_files fields of the passed-in FileSelector structure.
|
|
*
|
|
* [Parameters]
|
|
* args: Parameter size (must be sizeof(FileSelector *))
|
|
* argp: Parameter pointer (must point to a valid FileSelector pointer)
|
|
* [Return value]
|
|
* Does not return (terminates and deletes thread when finished)
|
|
*/
|
|
static void scandir_thread(SceSize args, void *argp)
|
|
{
|
|
PRECOND(args == sizeof(FileSelector *), goto exit_thread);
|
|
PRECOND(argp != NULL, goto exit_thread);
|
|
|
|
FileSelector * const filesel = *(FileSelector **)argp;
|
|
|
|
|
|
int dirfd = sceIoDopen(filesel->dir);
|
|
if (dirfd < 0) {
|
|
DMSG("sceIoDopen(%s): %s", filesel->dir, psp_strerror(dirfd));
|
|
goto signal_error;
|
|
}
|
|
|
|
SceIoDirent dirent;
|
|
memset(&dirent, 0, sizeof(dirent));
|
|
int res;
|
|
while ((res = sceIoDread(dirfd, &dirent)) > 0) {
|
|
|
|
/* Ignore . (current directory) and .. (parent directory) entries */
|
|
if (strcmp(dirent.d_name,".") == 0 || strcmp(dirent.d_name,"..") == 0){
|
|
continue;
|
|
}
|
|
|
|
/* Ignore all directories, since we don't support changing directory
|
|
* (we could subsume the above test into this one, but we leave them
|
|
* separate for clarity) */
|
|
char pathbuf[1000];
|
|
if (UNLIKELY(snprintf(pathbuf, sizeof(pathbuf), "%s/%s", filesel->dir,
|
|
dirent.d_name) >= sizeof(pathbuf))) {
|
|
DMSG("Pathname buffer overflow: %s/%s", filesel->dir,
|
|
dirent.d_name);
|
|
continue;
|
|
}
|
|
struct SceIoStat st;
|
|
memset(&st, 0, sizeof(st));
|
|
res = sceIoGetstat(pathbuf, &st);
|
|
if (UNLIKELY(res < 0)) {
|
|
DMSG("sceIoGetstat(%s): %s", pathbuf, psp_strerror(res));
|
|
continue;
|
|
}
|
|
if (FIO_S_ISDIR(st.st_mode)) {
|
|
continue;
|
|
}
|
|
|
|
/* Make room for the new file in the array */
|
|
char **new_file_list =
|
|
realloc(filesel->file_list,
|
|
sizeof(*filesel->file_list) * (filesel->num_files + 1));
|
|
if (UNLIKELY(!new_file_list)) {
|
|
DMSG("No memory to expand file list to %d files",
|
|
filesel->num_files + 1);
|
|
goto clear_file_list;
|
|
}
|
|
filesel->file_list = new_file_list;
|
|
char *new_file = strdup(dirent.d_name);
|
|
if (UNLIKELY(!new_file)) {
|
|
DMSG("No memory to copy filename: %s", dirent.d_name);
|
|
goto clear_file_list;
|
|
}
|
|
|
|
/* Insert the new file into the file list, keeping the list sorted.
|
|
* We don't expect to see very many files in a directory, so we
|
|
* don't bother with anything more complex than a linear search. */
|
|
int i;
|
|
for (i = 0; i < filesel->num_files; i++) {
|
|
if (stricmp(new_file, filesel->file_list[i]) < 0) {
|
|
break;
|
|
}
|
|
}
|
|
if (i < filesel->num_files) {
|
|
memmove(&filesel->file_list[i+1], &filesel->file_list[i],
|
|
sizeof(*filesel->file_list) * (filesel->num_files - i));
|
|
}
|
|
filesel->file_list[i] = new_file;
|
|
filesel->num_files++;
|
|
}
|
|
|
|
res = sceIoDclose(dirfd);
|
|
if (res != 0) {
|
|
DMSG("sceIoDclose(%s): %s", filesel->dir, psp_strerror(dirfd));
|
|
/* Not a critical error, so just let it slide */
|
|
}
|
|
|
|
filesel->scandir_thread = 0;
|
|
goto exit_thread;
|
|
|
|
clear_file_list:;
|
|
int i;
|
|
for (i = 0; i < filesel->num_files; i++) {
|
|
free(filesel->file_list[i]);
|
|
}
|
|
free(filesel->file_list);
|
|
filesel->file_list = NULL;
|
|
signal_error:
|
|
filesel->num_files = -1;
|
|
filesel->scandir_thread = 0;
|
|
exit_thread:
|
|
sceKernelExitDeleteThread(0);
|
|
}
|
|
|
|
/*************************************************************************/
|
|
/*************************************************************************/
|
|
|
|
/*
|
|
* Local variables:
|
|
* c-file-style: "stroustrup"
|
|
* c-file-offsets: ((case-label . *) (statement-case-intro . *))
|
|
* indent-tabs-mode: nil
|
|
* End:
|
|
*
|
|
* vim: expandtab shiftwidth=4:
|
|
*/
|