mirror of
https://git.planet-casio.com/Lephenixnoir/JustUI.git
synced 2024-12-28 20:43:40 +01:00
jfileselect: add a new file selection widget
This commit is contained in:
parent
819181d6f0
commit
d0856d100b
6 changed files with 437 additions and 4 deletions
|
@ -5,6 +5,8 @@ project(JustUI VERSION 1.0 LANGUAGES C)
|
||||||
find_package(Gint 2.1 REQUIRED)
|
find_package(Gint 2.1 REQUIRED)
|
||||||
include(Fxconv)
|
include(Fxconv)
|
||||||
|
|
||||||
|
set(CMAKE_INSTALL_MESSAGE LAZY)
|
||||||
|
|
||||||
configure_file(include/justui/config.h.in include/justui/config.h)
|
configure_file(include/justui/config.h.in include/justui/config.h)
|
||||||
|
|
||||||
set(ASSETS_fx
|
set(ASSETS_fx
|
||||||
|
@ -26,6 +28,7 @@ add_library(${NAME} STATIC
|
||||||
src/jinput.c
|
src/jinput.c
|
||||||
src/jpainted.c
|
src/jpainted.c
|
||||||
src/jfkeys.c
|
src/jfkeys.c
|
||||||
|
src/jfileselect.c
|
||||||
src/vec.c
|
src/vec.c
|
||||||
src/keymap.c
|
src/keymap.c
|
||||||
${ASSETS_${FXSDK_PLATFORM}}
|
${ASSETS_${FXSDK_PLATFORM}}
|
||||||
|
|
|
@ -13,7 +13,7 @@
|
||||||
#include <stddef.h>
|
#include <stddef.h>
|
||||||
#include <stdbool.h>
|
#include <stdbool.h>
|
||||||
|
|
||||||
/* j_dirs_t: Quadruplet with four directions */
|
/* jdirs: Quadruplet with four directions */
|
||||||
typedef struct {
|
typedef struct {
|
||||||
uint8_t top;
|
uint8_t top;
|
||||||
uint8_t right;
|
uint8_t right;
|
||||||
|
@ -21,7 +21,7 @@ typedef struct {
|
||||||
uint8_t left;
|
uint8_t left;
|
||||||
} jdirs;
|
} jdirs;
|
||||||
|
|
||||||
/* j_align_t: Alignment options with both horizontal and vertical names */
|
/* jalign: Alignment options with both horizontal and vertical names */
|
||||||
typedef enum {
|
typedef enum {
|
||||||
/* Horizontal names */
|
/* Horizontal names */
|
||||||
J_ALIGN_LEFT = 0,
|
J_ALIGN_LEFT = 0,
|
||||||
|
|
|
@ -16,7 +16,7 @@
|
||||||
occurs in the GUI. These are mostly widget signaling state changes,
|
occurs in the GUI. These are mostly widget signaling state changes,
|
||||||
validations, and other GUI specifics that might require attention. Events
|
validations, and other GUI specifics that might require attention. Events
|
||||||
can either be reported to the user by the scene (upwards event) or notify
|
can either be reported to the user by the scene (upwards event) or notify
|
||||||
widgets of something occuring to them (downwards event).
|
widgets of something occurring to them (downwards event).
|
||||||
|
|
||||||
JustUI tries hard to not invert flow control and leave the user to decide
|
JustUI tries hard to not invert flow control and leave the user to decide
|
||||||
when to produce downwards events. In a normal situation, events from
|
when to produce downwards events. In a normal situation, events from
|
||||||
|
|
88
include/justui/jfileselect.h
Normal file
88
include/justui/jfileselect.h
Normal file
|
@ -0,0 +1,88 @@
|
||||||
|
//---
|
||||||
|
// JustUI.jfileselect: Basic file selector
|
||||||
|
//---
|
||||||
|
|
||||||
|
#ifndef _J_JFILESELECT
|
||||||
|
#define _J_JFILESELECT
|
||||||
|
|
||||||
|
#include <justui/defs.h>
|
||||||
|
#include <justui/jwidget.h>
|
||||||
|
#include <gint/display.h>
|
||||||
|
#include <dirent.h>
|
||||||
|
|
||||||
|
/* jfileselect: Basic file selector
|
||||||
|
|
||||||
|
This widget is used to browse the filesystem and select a file. Visually, it
|
||||||
|
only consists of a scrolling list of names showing a section of a folder's
|
||||||
|
entries.
|
||||||
|
|
||||||
|
TODO: jfileselect: Select a new file to write to
|
||||||
|
|
||||||
|
Events:
|
||||||
|
* JFILESELECT_LOADED when a folder is loaded into the view
|
||||||
|
* JFILESELECT_VALIDATED when a file has been selected
|
||||||
|
* JFILESELECT_CANCELED when if the user exits from the top-level folder */
|
||||||
|
typedef struct {
|
||||||
|
jwidget widget;
|
||||||
|
|
||||||
|
/* Folder currently being browsed */
|
||||||
|
char *path;
|
||||||
|
/* Corresponding directory stream */
|
||||||
|
DIR *dp;
|
||||||
|
/* Entry previously validated (with EXE) */
|
||||||
|
char *selected_file;
|
||||||
|
|
||||||
|
/* Number of entries in the current folder */
|
||||||
|
int folder_entries;
|
||||||
|
/* Current cursor position (0 .. folder_entries-1) */
|
||||||
|
int16_t cursor;
|
||||||
|
/* Current scroll position */
|
||||||
|
int16_t scroll;
|
||||||
|
/* Number of visible lines */
|
||||||
|
int8_t visible_lines;
|
||||||
|
|
||||||
|
/* Additional pixels of spacing per line (base is font->height) */
|
||||||
|
int8_t line_spacing;
|
||||||
|
/* Rendering font */
|
||||||
|
font_t const *font;
|
||||||
|
|
||||||
|
} jfileselect;
|
||||||
|
|
||||||
|
/* Type IDs */
|
||||||
|
extern uint16_t JFILESELECT_LOADED;
|
||||||
|
extern uint16_t JFILESELECT_VALIDATED;
|
||||||
|
extern uint16_t JFILESELECT_CANCELED;
|
||||||
|
|
||||||
|
/* jfileselect_create(): Create a file selection interface
|
||||||
|
|
||||||
|
There is no initial folder. The widget will not handle any events nor emit
|
||||||
|
any events in this state; a path must first be set before use. */
|
||||||
|
jfileselect *jfileselect_create(void *parent);
|
||||||
|
|
||||||
|
/* jfileselect_browse(): Browse a folder
|
||||||
|
|
||||||
|
This function loads the specified folder and allows the user to select a
|
||||||
|
file. (Remember to give the widget focus.) A JFILESELECT_LOADED event is
|
||||||
|
emitted immediately, and further events are emitted based on user inputs.
|
||||||
|
|
||||||
|
This function resets the selected file to NULL.
|
||||||
|
|
||||||
|
Returns true on success, false if the path does not exist or cannot be
|
||||||
|
browsed (in that case, check errno). */
|
||||||
|
bool jfileselect_browse(jfileselect *fs, char const *path);
|
||||||
|
|
||||||
|
/* jfileselect_selected_file(): Get the path to the selected file
|
||||||
|
|
||||||
|
The selected file is NULL until jfileselect_browse() is called and the user
|
||||||
|
selects a file in the interface. The returned pointer is owned by the
|
||||||
|
widget. */
|
||||||
|
char const *jfileselect_selected_file(jfileselect *fs);
|
||||||
|
|
||||||
|
/* jfileselect_current_folder(): Get the path to the current folder */
|
||||||
|
char const *jfileselect_current_folder(jfileselect *fs);
|
||||||
|
|
||||||
|
/* Trivial properties */
|
||||||
|
void jfileselect_set_font(jfileselect *fs, font_t const *font);
|
||||||
|
void jfileselect_set_line_spacing(jfileselect *fs, int line_spacing);
|
||||||
|
|
||||||
|
#endif /* _J_JFILESELECT */
|
|
@ -63,7 +63,7 @@ typedef void jwidget_poly_render_t(void *w, int x, int y);
|
||||||
key events. This function is somewhat of a catch-all function for dynamic
|
key events. This function is somewhat of a catch-all function for dynamic
|
||||||
occurrences. The widget should either accept the event, do something with
|
occurrences. The widget should either accept the event, do something with
|
||||||
it, and return true, or refuse the event, do nothing and return false. This
|
it, and return true, or refuse the event, do nothing and return false. This
|
||||||
is influences events that propagate, such as key events. */
|
influences events that propagate, such as key events. */
|
||||||
typedef bool jwidget_poly_event_t(void *w, jevent e);
|
typedef bool jwidget_poly_event_t(void *w, jevent e);
|
||||||
|
|
||||||
/* jwidget_poly_destroy_t: Destroy a widget's specific resources
|
/* jwidget_poly_destroy_t: Destroy a widget's specific resources
|
||||||
|
|
342
src/jfileselect.c
Normal file
342
src/jfileselect.c
Normal file
|
@ -0,0 +1,342 @@
|
||||||
|
#include <justui/jwidget.h>
|
||||||
|
#include <justui/jwidget-api.h>
|
||||||
|
#include <justui/jfileselect.h>
|
||||||
|
|
||||||
|
#include <gint/display.h>
|
||||||
|
#include <gint/gint.h>
|
||||||
|
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
/* Type identifier for jfileselect */
|
||||||
|
static int jfileselect_type_id = -1;
|
||||||
|
|
||||||
|
/* Events */
|
||||||
|
uint16_t JFILESELECT_LOADED;
|
||||||
|
uint16_t JFILESELECT_VALIDATED;
|
||||||
|
uint16_t JFILESELECT_CANCELED;
|
||||||
|
|
||||||
|
jfileselect *jfileselect_create(void *parent)
|
||||||
|
{
|
||||||
|
if(jfileselect_type_id < 0) return NULL;
|
||||||
|
|
||||||
|
jfileselect *fs = malloc(sizeof *fs);
|
||||||
|
if(!fs) return NULL;
|
||||||
|
|
||||||
|
jwidget_init(&fs->widget, jfileselect_type_id, parent);
|
||||||
|
|
||||||
|
fs->path = NULL;
|
||||||
|
fs->dp = NULL;
|
||||||
|
fs->selected_file = NULL;
|
||||||
|
|
||||||
|
fs->folder_entries = -1;
|
||||||
|
fs->cursor = -1;
|
||||||
|
fs->scroll = 0;
|
||||||
|
fs->visible_lines = 0;
|
||||||
|
|
||||||
|
fs->line_spacing = 4;
|
||||||
|
fs->font = dfont_default();
|
||||||
|
|
||||||
|
return fs;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void count_visible_lines(jfileselect *fs)
|
||||||
|
{
|
||||||
|
int ch = jwidget_content_height(fs);
|
||||||
|
int line_height = fs->font->line_height + fs->line_spacing;
|
||||||
|
fs->visible_lines = ch / line_height;
|
||||||
|
}
|
||||||
|
|
||||||
|
//---
|
||||||
|
// Getters and setters
|
||||||
|
//---
|
||||||
|
|
||||||
|
void jfileselect_set_font(jfileselect *fs, font_t const *font)
|
||||||
|
{
|
||||||
|
fs->font = font ? font : dfont_default();
|
||||||
|
count_visible_lines(fs);
|
||||||
|
}
|
||||||
|
|
||||||
|
void jfileselect_set_line_spacing(jfileselect *fs, int line_spacing)
|
||||||
|
{
|
||||||
|
fs->line_spacing = line_spacing;
|
||||||
|
count_visible_lines(fs);
|
||||||
|
}
|
||||||
|
|
||||||
|
//---
|
||||||
|
// Path and folder manipulation
|
||||||
|
//---
|
||||||
|
|
||||||
|
static char *path_down(char const *path, char const *name)
|
||||||
|
{
|
||||||
|
char *child = malloc(strlen(path) + strlen(name) + 2);
|
||||||
|
if(!child)
|
||||||
|
return NULL;
|
||||||
|
|
||||||
|
strcpy(child, path);
|
||||||
|
if(strcmp(path, "/") != 0)
|
||||||
|
strcat(child, "/");
|
||||||
|
strcat(child, name);
|
||||||
|
|
||||||
|
return child;
|
||||||
|
}
|
||||||
|
|
||||||
|
static char *path_up(char const *path)
|
||||||
|
{
|
||||||
|
char *parent = strdup(path);
|
||||||
|
if(!parent)
|
||||||
|
return NULL;
|
||||||
|
|
||||||
|
char *p = strrchr(parent, '/');
|
||||||
|
if(p == parent)
|
||||||
|
*(p+1) = 0;
|
||||||
|
else if(p)
|
||||||
|
*p = 0;
|
||||||
|
|
||||||
|
return parent;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int accept_entry(struct dirent *ent)
|
||||||
|
{
|
||||||
|
/* TODO: jfileselect: Programmable filter */
|
||||||
|
if(!strcmp(ent->d_name, "@MainMem"))
|
||||||
|
return 0;
|
||||||
|
if(!strcmp(ent->d_name, "SAVE-F"))
|
||||||
|
return 0;
|
||||||
|
if(!strcmp(ent->d_name, "."))
|
||||||
|
return 0;
|
||||||
|
if(!strcmp(ent->d_name, ".."))
|
||||||
|
return 0;
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct dirent *read_nth_dir_entry(DIR *dp, int n)
|
||||||
|
{
|
||||||
|
struct dirent *entry = NULL;
|
||||||
|
rewinddir(dp);
|
||||||
|
|
||||||
|
for(int i = 0; i <= n;) {
|
||||||
|
entry = readdir(dp);
|
||||||
|
if(!entry)
|
||||||
|
return NULL;
|
||||||
|
i += accept_entry(entry);
|
||||||
|
}
|
||||||
|
|
||||||
|
return entry;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool load_folder(jfileselect *fs, char *path)
|
||||||
|
{
|
||||||
|
if(fs->dp)
|
||||||
|
closedir(fs->dp);
|
||||||
|
|
||||||
|
fs->dp = (DIR *)gint_world_switch(GINT_CALL(opendir, path));
|
||||||
|
if(!fs->dp)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
free(fs->path);
|
||||||
|
fs->path = path;
|
||||||
|
|
||||||
|
fs->folder_entries = 0;
|
||||||
|
struct dirent *ent;
|
||||||
|
while ((ent = readdir(fs->dp)))
|
||||||
|
fs->folder_entries += accept_entry(ent);
|
||||||
|
|
||||||
|
fs->widget.update = true;
|
||||||
|
|
||||||
|
jwidget_emit(fs, (jevent){ .type = JFILESELECT_LOADED });
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool jfileselect_browse(jfileselect *fs, char const *path)
|
||||||
|
{
|
||||||
|
char *path_copy = strdup(path);
|
||||||
|
if(!path_copy)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if(!load_folder(fs, path_copy))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
free(fs->selected_file);
|
||||||
|
fs->selected_file = NULL;
|
||||||
|
|
||||||
|
fs->cursor = 0;
|
||||||
|
fs->scroll = 0;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
char const *jfileselect_selected_file(jfileselect *fs)
|
||||||
|
{
|
||||||
|
return fs->selected_file;
|
||||||
|
}
|
||||||
|
|
||||||
|
char const *jfileselect_current_folder(jfileselect *fs)
|
||||||
|
{
|
||||||
|
return fs->path;
|
||||||
|
}
|
||||||
|
|
||||||
|
//---
|
||||||
|
// Polymorphic widget operations
|
||||||
|
//---
|
||||||
|
|
||||||
|
static void jfileselect_poly_csize(void *fs0)
|
||||||
|
{
|
||||||
|
jfileselect *fs = fs0;
|
||||||
|
jwidget *w = &fs->widget;
|
||||||
|
|
||||||
|
w->w = 128;
|
||||||
|
w->h = 6 * max(fs->font->line_height + fs->line_spacing, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void jfileselect_poly_layout(void *fs0)
|
||||||
|
{
|
||||||
|
jfileselect *fs = fs0;
|
||||||
|
count_visible_lines(fs);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void jfileselect_poly_render(void *fs0, int x, int y)
|
||||||
|
{
|
||||||
|
jfileselect *fs = fs0;
|
||||||
|
if(!fs->path || !fs->dp)
|
||||||
|
return;
|
||||||
|
|
||||||
|
font_t const *old_font = dfont(fs->font);
|
||||||
|
int line_height = fs->font->line_height + fs->line_spacing;
|
||||||
|
int cw = jwidget_content_width(fs);
|
||||||
|
|
||||||
|
rewinddir(fs->dp);
|
||||||
|
struct dirent *ent;
|
||||||
|
char const *entry_name;
|
||||||
|
bool isfolder;
|
||||||
|
|
||||||
|
for(int i = -fs->scroll; i < fs->visible_lines;) {
|
||||||
|
bool selected = (fs->cursor == fs->scroll + i);
|
||||||
|
|
||||||
|
ent = readdir(fs->dp);
|
||||||
|
if(!ent) break;
|
||||||
|
if(!accept_entry(ent)) continue;
|
||||||
|
|
||||||
|
entry_name = ent->d_name;
|
||||||
|
isfolder = (ent->d_type == DT_DIR);
|
||||||
|
|
||||||
|
if(i < 0) {
|
||||||
|
i++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
int line_y = y + line_height * i;
|
||||||
|
if(selected)
|
||||||
|
drect(x, line_y, x + cw - 1, line_y + line_height - 1, C_BLACK);
|
||||||
|
|
||||||
|
/* Round `line_spacing / 2` down so there is more spacing below */
|
||||||
|
dprint(x+2, line_y + (fs->line_spacing + 0) / 2,
|
||||||
|
selected ? C_WHITE : C_BLACK,
|
||||||
|
"%s%s", entry_name, isfolder ? "/" : "");
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
|
||||||
|
dfont(old_font);
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool jfileselect_poly_event(void *fs0, jevent e)
|
||||||
|
{
|
||||||
|
jfileselect *fs = fs0;
|
||||||
|
if(!fs->path || !fs->dp)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if(e.type == JWIDGET_KEY) {
|
||||||
|
key_event_t ev = e.key;
|
||||||
|
if(ev.type != KEYEV_DOWN && ev.type != KEYEV_HOLD)
|
||||||
|
return false;
|
||||||
|
int key = ev.key;
|
||||||
|
|
||||||
|
bool moved = false;
|
||||||
|
|
||||||
|
if(key == KEY_UP && fs->cursor > 0) {
|
||||||
|
fs->cursor = ev.shift ? 0 : fs->cursor - 1;
|
||||||
|
moved = true;
|
||||||
|
}
|
||||||
|
if(key == KEY_DOWN && fs->cursor < fs->folder_entries - 1) {
|
||||||
|
fs->cursor = ev.shift ? fs->folder_entries - 1 : fs->cursor + 1;
|
||||||
|
moved = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(fs->scroll > 0 && fs->cursor <= fs->scroll)
|
||||||
|
fs->scroll = max(fs->cursor - 1, 0);
|
||||||
|
if(fs->scroll + fs->visible_lines < fs->folder_entries
|
||||||
|
&& fs->cursor >= fs->scroll + fs->visible_lines - 2) {
|
||||||
|
fs->scroll = min(fs->cursor - fs->visible_lines + 2,
|
||||||
|
fs->folder_entries - fs->visible_lines);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(moved) {
|
||||||
|
fs->widget.update = true;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(key == KEY_EXIT) {
|
||||||
|
if(!strcmp(fs->path, "/")) {
|
||||||
|
jwidget_emit(fs, (jevent){ .type = JFILESELECT_CANCELED });
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
char *parent = path_up(fs->path);
|
||||||
|
if(parent) {
|
||||||
|
load_folder(fs, parent);
|
||||||
|
fs->cursor = 0;
|
||||||
|
fs->scroll = 0;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if(key == KEY_EXE) {
|
||||||
|
struct dirent *ent = read_nth_dir_entry(fs->dp, fs->cursor);
|
||||||
|
if(ent->d_type == DT_DIR) {
|
||||||
|
char *child = path_down(fs->path, ent->d_name);
|
||||||
|
if(child) {
|
||||||
|
load_folder(fs, child);
|
||||||
|
fs->cursor = 0;
|
||||||
|
fs->scroll = 0;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
fs->selected_file = path_down(fs->path, ent->d_name);
|
||||||
|
if(fs->selected_file) {
|
||||||
|
jwidget_emit(fs,(jevent){ .type = JFILESELECT_VALIDATED });
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void jfileselect_poly_destroy(void *fs0)
|
||||||
|
{
|
||||||
|
jfileselect *fs = fs0;
|
||||||
|
|
||||||
|
free(fs->path);
|
||||||
|
if(fs->dp)
|
||||||
|
closedir(fs->dp);
|
||||||
|
free(fs->selected_file);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* jfileselect type definition */
|
||||||
|
static jwidget_poly type_jfileselect = {
|
||||||
|
.name = "jfileselect",
|
||||||
|
.csize = jfileselect_poly_csize,
|
||||||
|
.layout = jfileselect_poly_layout,
|
||||||
|
.render = jfileselect_poly_render,
|
||||||
|
.event = jfileselect_poly_event,
|
||||||
|
.destroy = jfileselect_poly_destroy,
|
||||||
|
};
|
||||||
|
|
||||||
|
__attribute__((constructor(1003)))
|
||||||
|
static void j_register_jfileselect(void)
|
||||||
|
{
|
||||||
|
jfileselect_type_id = j_register_widget(&type_jfileselect, "jwidget");
|
||||||
|
JFILESELECT_LOADED = j_register_event();
|
||||||
|
JFILESELECT_VALIDATED = j_register_event();
|
||||||
|
JFILESELECT_CANCELED = j_register_event();
|
||||||
|
}
|
Loading…
Reference in a new issue