BizHawk/yabause/src/psp/filesel.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:
*/