jfileselect: add a new file selection widget

This commit is contained in:
Lephenixnoir 2022-06-19 22:36:39 +01:00
parent 819181d6f0
commit d0856d100b
Signed by: Lephenixnoir
GPG Key ID: 1BBA026E13FC0495
6 changed files with 437 additions and 4 deletions

View File

@ -5,6 +5,8 @@ project(JustUI VERSION 1.0 LANGUAGES C)
find_package(Gint 2.1 REQUIRED)
include(Fxconv)
set(CMAKE_INSTALL_MESSAGE LAZY)
configure_file(include/justui/config.h.in include/justui/config.h)
set(ASSETS_fx
@ -26,6 +28,7 @@ add_library(${NAME} STATIC
src/jinput.c
src/jpainted.c
src/jfkeys.c
src/jfileselect.c
src/vec.c
src/keymap.c
${ASSETS_${FXSDK_PLATFORM}}

View File

@ -13,7 +13,7 @@
#include <stddef.h>
#include <stdbool.h>
/* j_dirs_t: Quadruplet with four directions */
/* jdirs: Quadruplet with four directions */
typedef struct {
uint8_t top;
uint8_t right;
@ -21,7 +21,7 @@ typedef struct {
uint8_t left;
} jdirs;
/* j_align_t: Alignment options with both horizontal and vertical names */
/* jalign: Alignment options with both horizontal and vertical names */
typedef enum {
/* Horizontal names */
J_ALIGN_LEFT = 0,

View File

@ -16,7 +16,7 @@
occurs in the GUI. These are mostly widget signaling state changes,
validations, and other GUI specifics that might require attention. Events
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
when to produce downwards events. In a normal situation, events from

View 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 */

View File

@ -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
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
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);
/* jwidget_poly_destroy_t: Destroy a widget's specific resources

342
src/jfileselect.c Normal file
View 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();
}