Basic file open/save dialogs added.
This commit is contained in:
parent
b8eb83d63f
commit
705fa1e99a
6 changed files with 745 additions and 4 deletions
683
dvx/dvxDialog.c
683
dvx/dvxDialog.c
|
|
@ -4,7 +4,13 @@
|
||||||
#include "dvxWidget.h"
|
#include "dvxWidget.h"
|
||||||
#include "widgets/widgetInternal.h"
|
#include "widgets/widgetInternal.h"
|
||||||
|
|
||||||
|
#include <ctype.h>
|
||||||
|
#include <dirent.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
|
#include <sys/stat.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// Constants
|
// Constants
|
||||||
|
|
@ -22,6 +28,20 @@
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
static void drawIconGlyph(DisplayT *d, const BlitOpsT *ops, int32_t x, int32_t y, int32_t iconType, uint32_t color);
|
static void drawIconGlyph(DisplayT *d, const BlitOpsT *ops, int32_t x, int32_t y, int32_t iconType, uint32_t color);
|
||||||
|
static bool fdAcceptFile(const char *name);
|
||||||
|
static int fdEntryCompare(const void *a, const void *b);
|
||||||
|
static bool fdFilterMatch(const char *name, const char *pattern);
|
||||||
|
static void fdFreeEntries(void);
|
||||||
|
static void fdLoadDir(void);
|
||||||
|
static void fdNavigate(const char *path);
|
||||||
|
static void fdOnCancel(WidgetT *w);
|
||||||
|
static void fdOnClose(WindowT *win);
|
||||||
|
static void fdOnFilterChange(WidgetT *w);
|
||||||
|
static void fdOnListClick(WidgetT *w);
|
||||||
|
static void fdOnListDblClick(WidgetT *w);
|
||||||
|
static void fdOnOk(WidgetT *w);
|
||||||
|
static void fdOnPathSubmit(WidgetT *w);
|
||||||
|
static bool fdValidateFilename(const char *name);
|
||||||
static void onButtonClick(WidgetT *w);
|
static void onButtonClick(WidgetT *w);
|
||||||
static void onMsgBoxClose(WindowT *win);
|
static void onMsgBoxClose(WindowT *win);
|
||||||
static void onMsgBoxPaint(WindowT *win, RectT *dirtyArea);
|
static void onMsgBoxPaint(WindowT *win, RectT *dirtyArea);
|
||||||
|
|
@ -283,7 +303,8 @@ int32_t dvxMessageBox(AppContextT *ctx, const char *title, const char *message,
|
||||||
RectT fullRect = { 0, 0, win->contentW, win->contentH };
|
RectT fullRect = { 0, 0, win->contentW, win->contentH };
|
||||||
win->onPaint(win, &fullRect);
|
win->onPaint(win, &fullRect);
|
||||||
|
|
||||||
// Set as modal
|
// Set as modal (save previous in case we're stacking modals)
|
||||||
|
WindowT *prevModal = ctx->modalWindow;
|
||||||
ctx->modalWindow = win;
|
ctx->modalWindow = win;
|
||||||
|
|
||||||
// Nested event loop
|
// Nested event loop
|
||||||
|
|
@ -291,8 +312,8 @@ int32_t dvxMessageBox(AppContextT *ctx, const char *title, const char *message,
|
||||||
dvxUpdate(ctx);
|
dvxUpdate(ctx);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clean up
|
// Clean up — restore previous modal
|
||||||
ctx->modalWindow = NULL;
|
ctx->modalWindow = prevModal;
|
||||||
dvxDestroyWindow(ctx, win);
|
dvxDestroyWindow(ctx, win);
|
||||||
|
|
||||||
return sMsgBox.result;
|
return sMsgBox.result;
|
||||||
|
|
@ -484,3 +505,659 @@ static int32_t wordWrapHeight(const BitmapFontT *font, const char *text, int32_t
|
||||||
|
|
||||||
return lines * lineH;
|
return lines * lineH;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// File dialog
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
#define FD_MAX_ENTRIES 512
|
||||||
|
#define FD_MAX_PATH 260
|
||||||
|
#define FD_NAME_LEN 64
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
AppContextT *ctx;
|
||||||
|
bool done;
|
||||||
|
bool accepted;
|
||||||
|
int32_t flags;
|
||||||
|
char curDir[FD_MAX_PATH];
|
||||||
|
const FileFilterT *filters;
|
||||||
|
int32_t filterCount;
|
||||||
|
int32_t activeFilter;
|
||||||
|
char *entryNames[FD_MAX_ENTRIES];
|
||||||
|
bool entryIsDir[FD_MAX_ENTRIES];
|
||||||
|
int32_t entryCount;
|
||||||
|
const char *listItems[FD_MAX_ENTRIES];
|
||||||
|
WidgetT *fileList;
|
||||||
|
WidgetT *pathInput;
|
||||||
|
WidgetT *nameInput;
|
||||||
|
WidgetT *filterDd;
|
||||||
|
char outPath[FD_MAX_PATH];
|
||||||
|
} FileDialogStateT;
|
||||||
|
|
||||||
|
static FileDialogStateT sFd;
|
||||||
|
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// fdFilterMatch — check if filename matches a glob pattern
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
static bool fdFilterMatch(const char *name, const char *pattern) {
|
||||||
|
if (!pattern || pattern[0] == '\0') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (strcmp(pattern, "*.*") == 0 || strcmp(pattern, "*") == 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Simple *.ext matching
|
||||||
|
if (pattern[0] == '*' && pattern[1] == '.') {
|
||||||
|
const char *ext = strrchr(name, '.');
|
||||||
|
|
||||||
|
if (!ext) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
ext++;
|
||||||
|
const char *patExt = pattern + 2;
|
||||||
|
|
||||||
|
// Case-insensitive extension compare
|
||||||
|
while (*patExt && *ext) {
|
||||||
|
if (tolower((unsigned char)*patExt) != tolower((unsigned char)*ext)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
patExt++;
|
||||||
|
ext++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (*patExt == '\0' && *ext == '\0');
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// fdFreeEntries — free allocated entry name strings
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
static void fdFreeEntries(void) {
|
||||||
|
for (int32_t i = 0; i < sFd.entryCount; i++) {
|
||||||
|
free(sFd.entryNames[i]);
|
||||||
|
sFd.entryNames[i] = NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
sFd.entryCount = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// fdEntryCompare — sort: dirs first, then alphabetical
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
static int fdEntryCompare(const void *a, const void *b) {
|
||||||
|
int32_t ia = *(const int32_t *)a;
|
||||||
|
int32_t ib = *(const int32_t *)b;
|
||||||
|
|
||||||
|
// Dirs before files
|
||||||
|
if (sFd.entryIsDir[ia] != sFd.entryIsDir[ib]) {
|
||||||
|
return sFd.entryIsDir[ia] ? -1 : 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return stricmp(sFd.entryNames[ia], sFd.entryNames[ib]);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// fdLoadDir — read directory contents into state
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
static void fdLoadDir(void) {
|
||||||
|
fdFreeEntries();
|
||||||
|
|
||||||
|
const char *pattern = NULL;
|
||||||
|
|
||||||
|
if (sFd.filters && sFd.activeFilter >= 0 && sFd.activeFilter < sFd.filterCount) {
|
||||||
|
pattern = sFd.filters[sFd.activeFilter].pattern;
|
||||||
|
}
|
||||||
|
|
||||||
|
DIR *dir = opendir(sFd.curDir);
|
||||||
|
|
||||||
|
if (!dir) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct dirent *ent;
|
||||||
|
|
||||||
|
while ((ent = readdir(dir)) != NULL && sFd.entryCount < FD_MAX_ENTRIES) {
|
||||||
|
// Skip "."
|
||||||
|
if (strcmp(ent->d_name, ".") == 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build full path to stat
|
||||||
|
char fullPath[FD_MAX_PATH * 2];
|
||||||
|
snprintf(fullPath, sizeof(fullPath), "%s/%s", sFd.curDir, ent->d_name);
|
||||||
|
|
||||||
|
struct stat st;
|
||||||
|
|
||||||
|
if (stat(fullPath, &st) != 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isDir = S_ISDIR(st.st_mode);
|
||||||
|
|
||||||
|
// Apply filter to files only
|
||||||
|
if (!isDir && !fdFilterMatch(ent->d_name, pattern)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
int32_t idx = sFd.entryCount;
|
||||||
|
sFd.entryIsDir[idx] = isDir;
|
||||||
|
|
||||||
|
if (isDir) {
|
||||||
|
// Prefix dirs with brackets
|
||||||
|
char buf[FD_MAX_PATH];
|
||||||
|
snprintf(buf, sizeof(buf), "[%s]", ent->d_name);
|
||||||
|
sFd.entryNames[idx] = strdup(buf);
|
||||||
|
} else {
|
||||||
|
sFd.entryNames[idx] = strdup(ent->d_name);
|
||||||
|
}
|
||||||
|
|
||||||
|
sFd.entryCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
closedir(dir);
|
||||||
|
|
||||||
|
// Sort: build index array, sort, reorder
|
||||||
|
int32_t sortIdx[FD_MAX_ENTRIES];
|
||||||
|
|
||||||
|
for (int32_t i = 0; i < sFd.entryCount; i++) {
|
||||||
|
sortIdx[i] = i;
|
||||||
|
}
|
||||||
|
|
||||||
|
qsort(sortIdx, sFd.entryCount, sizeof(int32_t), fdEntryCompare);
|
||||||
|
|
||||||
|
// Rebuild arrays in sorted order
|
||||||
|
char *tmpNames[FD_MAX_ENTRIES];
|
||||||
|
bool tmpIsDir[FD_MAX_ENTRIES];
|
||||||
|
|
||||||
|
for (int32_t i = 0; i < sFd.entryCount; i++) {
|
||||||
|
tmpNames[i] = sFd.entryNames[sortIdx[i]];
|
||||||
|
tmpIsDir[i] = sFd.entryIsDir[sortIdx[i]];
|
||||||
|
}
|
||||||
|
|
||||||
|
memcpy(sFd.entryNames, tmpNames, sizeof(char *) * sFd.entryCount);
|
||||||
|
memcpy(sFd.entryIsDir, tmpIsDir, sizeof(bool) * sFd.entryCount);
|
||||||
|
|
||||||
|
// Build listItems pointer array for the listbox
|
||||||
|
for (int32_t i = 0; i < sFd.entryCount; i++) {
|
||||||
|
sFd.listItems[i] = sFd.entryNames[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
wgtListBoxSetItems(sFd.fileList, sFd.listItems, sFd.entryCount);
|
||||||
|
wgtListBoxSetSelected(sFd.fileList, 0);
|
||||||
|
sFd.fileList->as.listBox.scrollPos = 0;
|
||||||
|
|
||||||
|
// Update path display
|
||||||
|
wgtSetText(sFd.pathInput, sFd.curDir);
|
||||||
|
wgtInvalidate(sFd.fileList);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// fdNavigate — change to a new directory
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
static void fdNavigate(const char *path) {
|
||||||
|
// Resolve relative paths
|
||||||
|
char resolved[FD_MAX_PATH];
|
||||||
|
|
||||||
|
if (path[0] == '/' || path[0] == '\\' ||
|
||||||
|
(path[1] == ':' && (path[2] == '/' || path[2] == '\\'))) {
|
||||||
|
strncpy(resolved, path, FD_MAX_PATH - 1);
|
||||||
|
resolved[FD_MAX_PATH - 1] = '\0';
|
||||||
|
} else {
|
||||||
|
char tmp[FD_MAX_PATH * 2];
|
||||||
|
snprintf(tmp, sizeof(tmp), "%s/%s", sFd.curDir, path);
|
||||||
|
strncpy(resolved, tmp, FD_MAX_PATH - 1);
|
||||||
|
resolved[FD_MAX_PATH - 1] = '\0';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify it's a directory
|
||||||
|
struct stat st;
|
||||||
|
|
||||||
|
if (stat(resolved, &st) != 0 || !S_ISDIR(st.st_mode)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Canonicalize the path
|
||||||
|
char canon[FD_MAX_PATH];
|
||||||
|
|
||||||
|
if (realpath(resolved, canon) != NULL) {
|
||||||
|
strncpy(sFd.curDir, canon, FD_MAX_PATH - 1);
|
||||||
|
sFd.curDir[FD_MAX_PATH - 1] = '\0';
|
||||||
|
} else {
|
||||||
|
strncpy(sFd.curDir, resolved, FD_MAX_PATH - 1);
|
||||||
|
sFd.curDir[FD_MAX_PATH - 1] = '\0';
|
||||||
|
}
|
||||||
|
|
||||||
|
fdLoadDir();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// fdValidateFilename — check filename is valid for target OS
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
static bool fdValidateFilename(const char *name) {
|
||||||
|
// DOS 8.3 validation
|
||||||
|
static const char *reserved[] = {
|
||||||
|
"CON", "PRN", "AUX", "NUL",
|
||||||
|
"COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7", "COM8", "COM9",
|
||||||
|
"LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", "LPT9",
|
||||||
|
NULL
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!name || name[0] == '\0') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Split into base and extension
|
||||||
|
const char *dot = strrchr(name, '.');
|
||||||
|
int32_t baseLen;
|
||||||
|
int32_t extLen;
|
||||||
|
|
||||||
|
if (dot) {
|
||||||
|
baseLen = (int32_t)(dot - name);
|
||||||
|
extLen = (int32_t)strlen(dot + 1);
|
||||||
|
} else {
|
||||||
|
baseLen = (int32_t)strlen(name);
|
||||||
|
extLen = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (baseLen < 1 || baseLen > 8) {
|
||||||
|
dvxMessageBox(sFd.ctx, "Invalid Filename", "Filename must be 1-8 characters before the extension.", MB_OK | MB_ICONERROR);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (extLen > 3) {
|
||||||
|
dvxMessageBox(sFd.ctx, "Invalid Filename", "Extension must be 3 characters or fewer.", MB_OK | MB_ICONERROR);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for invalid characters
|
||||||
|
for (const char *p = name; *p; p++) {
|
||||||
|
if (*p == '.') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (*p < '!' || *p > '~') {
|
||||||
|
dvxMessageBox(sFd.ctx, "Invalid Filename", "Filename contains invalid characters.", MB_OK | MB_ICONERROR);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (strchr(" \"*+,/:;<=>?[\\]|", *p)) {
|
||||||
|
dvxMessageBox(sFd.ctx, "Invalid Filename", "Filename contains invalid characters.", MB_OK | MB_ICONERROR);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for multiple dots
|
||||||
|
if (dot && strchr(name, '.') != dot) {
|
||||||
|
dvxMessageBox(sFd.ctx, "Invalid Filename", "Filename may contain only one dot.", MB_OK | MB_ICONERROR);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check reserved device names (compare base only, case-insensitive)
|
||||||
|
char base[9];
|
||||||
|
int32_t copyLen = baseLen < 8 ? baseLen : 8;
|
||||||
|
|
||||||
|
for (int32_t i = 0; i < copyLen; i++) {
|
||||||
|
base[i] = toupper((unsigned char)name[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
base[copyLen] = '\0';
|
||||||
|
|
||||||
|
for (const char **r = reserved; *r; r++) {
|
||||||
|
if (strcmp(base, *r) == 0) {
|
||||||
|
dvxMessageBox(sFd.ctx, "Invalid Filename", "That name is a reserved device name.", MB_OK | MB_ICONERROR);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// fdAcceptFile — confirm and accept the selected filename
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
static bool fdAcceptFile(const char *name) {
|
||||||
|
if (!fdValidateFilename(name)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
char fullPath[FD_MAX_PATH * 2];
|
||||||
|
snprintf(fullPath, sizeof(fullPath), "%s/%s", sFd.curDir, name);
|
||||||
|
|
||||||
|
struct stat st;
|
||||||
|
bool exists = (stat(fullPath, &st) == 0 && S_ISREG(st.st_mode));
|
||||||
|
|
||||||
|
if ((sFd.flags & FD_SAVE) && exists) {
|
||||||
|
// Save dialog: confirm overwrite of existing file
|
||||||
|
char msg[FD_MAX_PATH * 2 + 64];
|
||||||
|
snprintf(msg, sizeof(msg), "%s already exists.\nDo you want to replace it?", name);
|
||||||
|
|
||||||
|
if (dvxMessageBox(sFd.ctx, "Confirm Save", msg, MB_YESNO | MB_ICONQUESTION) != ID_YES) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} else if (!(sFd.flags & FD_SAVE) && !exists) {
|
||||||
|
// Open dialog: confirm creation of non-existent file
|
||||||
|
char msg[FD_MAX_PATH * 2 + 64];
|
||||||
|
snprintf(msg, sizeof(msg), "%s does not exist.\nDo you want to create it?", name);
|
||||||
|
|
||||||
|
if (dvxMessageBox(sFd.ctx, "Confirm Create", msg, MB_YESNO | MB_ICONQUESTION) != ID_YES) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sFd.accepted = true;
|
||||||
|
sFd.done = true;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// fdOnListClick — file list selection changed
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
static void fdOnListClick(WidgetT *w) {
|
||||||
|
int32_t sel = wgtListBoxGetSelected(w);
|
||||||
|
|
||||||
|
if (sel < 0 || sel >= sFd.entryCount) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!sFd.entryIsDir[sel]) {
|
||||||
|
wgtSetText(sFd.nameInput, sFd.entryNames[sel]);
|
||||||
|
wgtInvalidatePaint(sFd.nameInput);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// fdOnListDblClick — file list double-click
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
static void fdOnListDblClick(WidgetT *w) {
|
||||||
|
int32_t sel = wgtListBoxGetSelected(w);
|
||||||
|
|
||||||
|
if (sel < 0 || sel >= sFd.entryCount) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sFd.entryIsDir[sel]) {
|
||||||
|
// Double-click on directory — navigate into it
|
||||||
|
const char *display = sFd.entryNames[sel];
|
||||||
|
char dirName[FD_NAME_LEN];
|
||||||
|
|
||||||
|
if (display[0] == '[') {
|
||||||
|
strncpy(dirName, display + 1, sizeof(dirName) - 1);
|
||||||
|
dirName[sizeof(dirName) - 1] = '\0';
|
||||||
|
char *bracket = strchr(dirName, ']');
|
||||||
|
|
||||||
|
if (bracket) {
|
||||||
|
*bracket = '\0';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
strncpy(dirName, display, sizeof(dirName) - 1);
|
||||||
|
dirName[sizeof(dirName) - 1] = '\0';
|
||||||
|
}
|
||||||
|
|
||||||
|
fdNavigate(dirName);
|
||||||
|
wgtSetText(sFd.nameInput, "");
|
||||||
|
} else {
|
||||||
|
// Double-click on file — accept it (with confirmation if needed)
|
||||||
|
wgtSetText(sFd.nameInput, sFd.entryNames[sel]);
|
||||||
|
fdAcceptFile(sFd.entryNames[sel]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// fdOnOk — OK button clicked
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
static void fdOnOk(WidgetT *w) {
|
||||||
|
(void)w;
|
||||||
|
|
||||||
|
// Check if a directory is selected in the list
|
||||||
|
int32_t sel = wgtListBoxGetSelected(sFd.fileList);
|
||||||
|
|
||||||
|
if (sel >= 0 && sel < sFd.entryCount && sFd.entryIsDir[sel]) {
|
||||||
|
// Extract directory name from "[name]"
|
||||||
|
const char *display = sFd.entryNames[sel];
|
||||||
|
char dirName[FD_NAME_LEN];
|
||||||
|
|
||||||
|
if (display[0] == '[') {
|
||||||
|
strncpy(dirName, display + 1, sizeof(dirName) - 1);
|
||||||
|
dirName[sizeof(dirName) - 1] = '\0';
|
||||||
|
char *bracket = strchr(dirName, ']');
|
||||||
|
|
||||||
|
if (bracket) {
|
||||||
|
*bracket = '\0';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
strncpy(dirName, display, sizeof(dirName) - 1);
|
||||||
|
dirName[sizeof(dirName) - 1] = '\0';
|
||||||
|
}
|
||||||
|
|
||||||
|
fdNavigate(dirName);
|
||||||
|
wgtSetText(sFd.nameInput, "");
|
||||||
|
wgtInvalidatePaint(sFd.nameInput);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const char *name = wgtGetText(sFd.nameInput);
|
||||||
|
|
||||||
|
if (!name || name[0] == '\0') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If user typed a directory path, navigate there
|
||||||
|
char testPath[FD_MAX_PATH * 2];
|
||||||
|
snprintf(testPath, sizeof(testPath), "%s/%s", sFd.curDir, name);
|
||||||
|
|
||||||
|
struct stat st;
|
||||||
|
|
||||||
|
if (stat(testPath, &st) == 0 && S_ISDIR(st.st_mode)) {
|
||||||
|
fdNavigate(testPath);
|
||||||
|
wgtSetText(sFd.nameInput, "");
|
||||||
|
wgtInvalidatePaint(sFd.nameInput);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Accept the file (with confirmation if needed)
|
||||||
|
fdAcceptFile(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// fdOnCancel — Cancel button clicked
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
static void fdOnCancel(WidgetT *w) {
|
||||||
|
(void)w;
|
||||||
|
sFd.accepted = false;
|
||||||
|
sFd.done = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// fdOnClose — window close button
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
static void fdOnClose(WindowT *win) {
|
||||||
|
(void)win;
|
||||||
|
sFd.accepted = false;
|
||||||
|
sFd.done = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// fdOnFilterChange — filter dropdown changed
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
static void fdOnFilterChange(WidgetT *w) {
|
||||||
|
sFd.activeFilter = wgtDropdownGetSelected(w);
|
||||||
|
fdLoadDir();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// fdOnPathSubmit — enter pressed in path input
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
static void fdOnPathSubmit(WidgetT *w) {
|
||||||
|
const char *path = wgtGetText(w);
|
||||||
|
|
||||||
|
if (path && path[0]) {
|
||||||
|
fdNavigate(path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// dvxFileDialog
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
bool dvxFileDialog(AppContextT *ctx, const char *title, int32_t flags, const char *initialDir, const FileFilterT *filters, int32_t filterCount, char *outPath, int32_t outPathSize) {
|
||||||
|
memset(&sFd, 0, sizeof(sFd));
|
||||||
|
sFd.ctx = ctx;
|
||||||
|
sFd.flags = flags;
|
||||||
|
sFd.filters = filters;
|
||||||
|
sFd.filterCount = filterCount;
|
||||||
|
sFd.activeFilter = 0;
|
||||||
|
|
||||||
|
// Set initial directory
|
||||||
|
if (initialDir && initialDir[0]) {
|
||||||
|
strncpy(sFd.curDir, initialDir, FD_MAX_PATH - 1);
|
||||||
|
} else {
|
||||||
|
getcwd(sFd.curDir, FD_MAX_PATH);
|
||||||
|
}
|
||||||
|
|
||||||
|
sFd.curDir[FD_MAX_PATH - 1] = '\0';
|
||||||
|
|
||||||
|
// Create dialog window
|
||||||
|
int32_t dlgW = 360;
|
||||||
|
int32_t dlgH = 340;
|
||||||
|
int32_t winX = (ctx->display.width - dlgW) / 2;
|
||||||
|
int32_t winY = (ctx->display.height - dlgH) / 2;
|
||||||
|
|
||||||
|
WindowT *win = dvxCreateWindow(ctx, title ? title : ((flags & FD_SAVE) ? "Save As" : "Open"), winX, winY, dlgW, dlgH, false);
|
||||||
|
|
||||||
|
if (!win) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
win->modal = true;
|
||||||
|
win->onClose = fdOnClose;
|
||||||
|
win->userData = ctx;
|
||||||
|
|
||||||
|
WidgetT *root = wgtInitWindow(ctx, win);
|
||||||
|
|
||||||
|
if (!root) {
|
||||||
|
dvxDestroyWindow(ctx, win);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Path row
|
||||||
|
WidgetT *pathRow = wgtHBox(root);
|
||||||
|
wgtLabel(pathRow, "&Path:");
|
||||||
|
sFd.pathInput = wgtTextInput(pathRow, FD_MAX_PATH);
|
||||||
|
sFd.pathInput->onChange = fdOnPathSubmit;
|
||||||
|
|
||||||
|
// File list
|
||||||
|
sFd.fileList = wgtListBox(root);
|
||||||
|
sFd.fileList->weight = 100;
|
||||||
|
sFd.fileList->onChange = fdOnListClick;
|
||||||
|
sFd.fileList->onDblClick = fdOnListDblClick;
|
||||||
|
|
||||||
|
// Filter row (if filters provided)
|
||||||
|
if (filters && filterCount > 0) {
|
||||||
|
WidgetT *filterRow = wgtHBox(root);
|
||||||
|
wgtLabel(filterRow, "F&ilter:");
|
||||||
|
sFd.filterDd = wgtDropdown(filterRow);
|
||||||
|
|
||||||
|
// Build filter label array (static — lives for dialog lifetime)
|
||||||
|
static const char *filterLabels[16];
|
||||||
|
int32_t fc = filterCount < 16 ? filterCount : 16;
|
||||||
|
|
||||||
|
for (int32_t i = 0; i < fc; i++) {
|
||||||
|
filterLabels[i] = filters[i].label;
|
||||||
|
}
|
||||||
|
|
||||||
|
wgtDropdownSetItems(sFd.filterDd, filterLabels, fc);
|
||||||
|
wgtDropdownSetSelected(sFd.filterDd, 0);
|
||||||
|
sFd.filterDd->onChange = fdOnFilterChange;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filename row
|
||||||
|
WidgetT *nameRow = wgtHBox(root);
|
||||||
|
wgtLabel(nameRow, "File &name:");
|
||||||
|
sFd.nameInput = wgtTextInput(nameRow, FD_MAX_PATH);
|
||||||
|
|
||||||
|
// Button row
|
||||||
|
WidgetT *btnRow = wgtHBox(root);
|
||||||
|
btnRow->align = AlignEndE;
|
||||||
|
WidgetT *okBtn = wgtButton(btnRow, (flags & FD_SAVE) ? "&Save" : "&Open");
|
||||||
|
okBtn->onClick = fdOnOk;
|
||||||
|
okBtn->minW = wgtPixels(80);
|
||||||
|
WidgetT *cancelBtn = wgtButton(btnRow, "&Cancel");
|
||||||
|
cancelBtn->onClick = fdOnCancel;
|
||||||
|
cancelBtn->minW = wgtPixels(80);
|
||||||
|
|
||||||
|
// Load initial directory
|
||||||
|
fdLoadDir();
|
||||||
|
wgtInvalidate(root);
|
||||||
|
|
||||||
|
// Modal loop
|
||||||
|
ctx->modalWindow = win;
|
||||||
|
|
||||||
|
while (!sFd.done && ctx->running) {
|
||||||
|
dvxUpdate(ctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx->modalWindow = NULL;
|
||||||
|
|
||||||
|
// Build result path
|
||||||
|
bool result = false;
|
||||||
|
|
||||||
|
if (sFd.accepted) {
|
||||||
|
const char *name = wgtGetText(sFd.nameInput);
|
||||||
|
|
||||||
|
if (name && name[0]) {
|
||||||
|
char tmp[FD_MAX_PATH * 2];
|
||||||
|
snprintf(tmp, sizeof(tmp), "%s/%s", sFd.curDir, name);
|
||||||
|
strncpy(sFd.outPath, tmp, FD_MAX_PATH - 1);
|
||||||
|
sFd.outPath[FD_MAX_PATH - 1] = '\0';
|
||||||
|
|
||||||
|
if (outPath && outPathSize > 0) {
|
||||||
|
strncpy(outPath, sFd.outPath, outPathSize - 1);
|
||||||
|
outPath[outPathSize - 1] = '\0';
|
||||||
|
}
|
||||||
|
|
||||||
|
result = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cleanup
|
||||||
|
dvxDestroyWindow(ctx, win);
|
||||||
|
fdFreeEntries();
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -37,4 +37,27 @@
|
||||||
// flags = MB_xxx button flag | MB_ICONxxx icon flag
|
// flags = MB_xxx button flag | MB_ICONxxx icon flag
|
||||||
int32_t dvxMessageBox(AppContextT *ctx, const char *title, const char *message, int32_t flags);
|
int32_t dvxMessageBox(AppContextT *ctx, const char *title, const char *message, int32_t flags);
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// File dialog flags
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
#define FD_OPEN 0x0000 // Open file (default)
|
||||||
|
#define FD_SAVE 0x0001 // Save file
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// File dialog filter
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
const char *label; // e.g. "Text Files (*.txt)"
|
||||||
|
const char *pattern; // e.g. "*.txt" (case-insensitive, single pattern)
|
||||||
|
} FileFilterT;
|
||||||
|
|
||||||
|
// Show a modal file open/save dialog. Returns true if the user selected
|
||||||
|
// a file, false if cancelled. The selected path is written to outPath
|
||||||
|
// (buffer must be at least outPathSize bytes).
|
||||||
|
// initialDir may be NULL (defaults to current directory).
|
||||||
|
// filters/filterCount may be NULL/0 for no filter dropdown.
|
||||||
|
bool dvxFileDialog(AppContextT *ctx, const char *title, int32_t flags, const char *initialDir, const FileFilterT *filters, int32_t filterCount, char *outPath, int32_t outPathSize);
|
||||||
|
|
||||||
#endif // DVX_DIALOG_H
|
#endif // DVX_DIALOG_H
|
||||||
|
|
|
||||||
|
|
@ -187,6 +187,7 @@ typedef struct WidgetT {
|
||||||
void *userData;
|
void *userData;
|
||||||
void (*onClick)(struct WidgetT *w);
|
void (*onClick)(struct WidgetT *w);
|
||||||
void (*onChange)(struct WidgetT *w);
|
void (*onChange)(struct WidgetT *w);
|
||||||
|
void (*onDblClick)(struct WidgetT *w);
|
||||||
|
|
||||||
// Type-specific data
|
// Type-specific data
|
||||||
union {
|
union {
|
||||||
|
|
|
||||||
|
|
@ -522,6 +522,10 @@ void widgetListBoxOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy) {
|
||||||
if (hit->onChange) {
|
if (hit->onChange) {
|
||||||
hit->onChange(hit);
|
hit->onChange(hit);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (hit->onDblClick && multiClickDetect(vx, vy) >= 2) {
|
||||||
|
hit->onDblClick(hit);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1020,6 +1020,10 @@ void widgetListViewOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy)
|
||||||
if (hit->onChange) {
|
if (hit->onChange) {
|
||||||
hit->onChange(hit);
|
hit->onChange(hit);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (hit->onDblClick && multiClickDetect(vx, vy) >= 2) {
|
||||||
|
hit->onDblClick(hit);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
wgtInvalidatePaint(hit);
|
wgtInvalidatePaint(hit);
|
||||||
|
|
|
||||||
|
|
@ -143,10 +143,42 @@ static void onCloseMainCb(WindowT *win) {
|
||||||
// onMenuCb
|
// onMenuCb
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
|
static const FileFilterT sFileFilters[] = {
|
||||||
|
{"All Files (*.*)", "*.*"},
|
||||||
|
{"Text Files (*.txt)", "*.txt"},
|
||||||
|
{"Batch Files (*.bat)", "*.bat"},
|
||||||
|
{"Executables (*.exe)", "*.exe"},
|
||||||
|
{"Bitmap Files (*.bmp)", "*.bmp"}
|
||||||
|
};
|
||||||
|
|
||||||
static void onMenuCb(WindowT *win, int32_t menuId) {
|
static void onMenuCb(WindowT *win, int32_t menuId) {
|
||||||
AppContextT *ctx = (AppContextT *)win->userData;
|
AppContextT *ctx = (AppContextT *)win->userData;
|
||||||
|
|
||||||
switch (menuId) {
|
switch (menuId) {
|
||||||
|
case CMD_FILE_OPEN: {
|
||||||
|
char path[260];
|
||||||
|
|
||||||
|
if (dvxFileDialog(ctx, "Open File", FD_OPEN, NULL, sFileFilters, 5, path, sizeof(path))) {
|
||||||
|
char msg[300];
|
||||||
|
snprintf(msg, sizeof(msg), "Selected: %s", path);
|
||||||
|
dvxMessageBox(ctx, "Open", msg, MB_OK | MB_ICONINFO);
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case CMD_FILE_SAVE: {
|
||||||
|
char path[260];
|
||||||
|
|
||||||
|
if (dvxFileDialog(ctx, "Save As", FD_SAVE, NULL, sFileFilters, 5, path, sizeof(path))) {
|
||||||
|
char msg[300];
|
||||||
|
snprintf(msg, sizeof(msg), "Save to: %s", path);
|
||||||
|
dvxMessageBox(ctx, "Save", msg, MB_OK | MB_ICONINFO);
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
case CMD_FILE_EXIT:
|
case CMD_FILE_EXIT:
|
||||||
if (ctx) {
|
if (ctx) {
|
||||||
int32_t result = dvxMessageBox(ctx, "Exit",
|
int32_t result = dvxMessageBox(ctx, "Exit",
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue