DVX_GUI/dvx/dvxDialog.c

1163 lines
33 KiB
C

// dvxDialog.c — Modal dialogs for DV/X GUI
#include "dvxDialog.h"
#include "dvxWidget.h"
#include "widgets/widgetInternal.h"
#include <ctype.h>
#include <dirent.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <unistd.h>
// ============================================================
// Constants
// ============================================================
#define MSG_MAX_WIDTH 320
#define MSG_PADDING 8
#define ICON_AREA_WIDTH 40
#define BUTTON_WIDTH 80
#define BUTTON_HEIGHT 24
#define BUTTON_GAP 8
// ============================================================
// Prototypes
// ============================================================
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 onMsgBoxClose(WindowT *win);
static void onMsgBoxPaint(WindowT *win, RectT *dirtyArea);
static int32_t wordWrapHeight(const BitmapFontT *font, const char *text, int32_t maxW);
static void wordWrapDraw(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, int32_t x, int32_t y, const char *text, int32_t maxW, uint32_t fg, uint32_t bg);
// ============================================================
// Message box state (one active at a time)
// ============================================================
typedef struct {
AppContextT *ctx;
int32_t result;
bool done;
const char *message;
int32_t iconType;
int32_t textX;
int32_t textY;
int32_t textMaxW;
int32_t msgAreaH;
} MsgBoxStateT;
static MsgBoxStateT sMsgBox;
// ============================================================
// drawIconGlyph — draw a simple icon shape
// ============================================================
static void drawIconGlyph(DisplayT *d, const BlitOpsT *ops, int32_t x, int32_t y, int32_t iconType, uint32_t color) {
if (iconType == MB_ICONINFO) {
// Circle outline with 'i'
for (int32_t row = 0; row < 24; row++) {
for (int32_t col = 0; col < 24; col++) {
int32_t dx = col - 12;
int32_t dy = row - 12;
int32_t d2 = dx * dx + dy * dy;
if (d2 <= 144 && d2 >= 100) {
rectFill(d, ops, x + col, y + row, 1, 1, color);
}
}
}
rectFill(d, ops, x + 11, y + 6, 2, 2, color);
rectFill(d, ops, x + 11, y + 10, 2, 8, color);
} else if (iconType == MB_ICONWARNING) {
// Triangle outline with '!'
for (int32_t row = 0; row < 24; row++) {
int32_t halfW = row / 2;
int32_t lx = 12 - halfW;
int32_t rx = 12 + halfW;
rectFill(d, ops, x + lx, y + row, 1, 1, color);
rectFill(d, ops, x + rx, y + row, 1, 1, color);
if (row == 23) {
drawHLine(d, ops, x + lx, y + row, rx - lx + 1, color);
}
}
rectFill(d, ops, x + 11, y + 8, 2, 9, color);
rectFill(d, ops, x + 11, y + 19, 2, 2, color);
} else if (iconType == MB_ICONERROR) {
// Circle outline with X
for (int32_t row = 0; row < 24; row++) {
for (int32_t col = 0; col < 24; col++) {
int32_t dx = col - 12;
int32_t dy = row - 12;
int32_t d2 = dx * dx + dy * dy;
if (d2 <= 144 && d2 >= 100) {
rectFill(d, ops, x + col, y + row, 1, 1, color);
}
}
}
for (int32_t i = 0; i < 12; i++) {
rectFill(d, ops, x + 6 + i, y + 6 + i, 2, 2, color);
rectFill(d, ops, x + 18 - i, y + 6 + i, 2, 2, color);
}
} else if (iconType == MB_ICONQUESTION) {
// Circle outline with '?'
for (int32_t row = 0; row < 24; row++) {
for (int32_t col = 0; col < 24; col++) {
int32_t dx = col - 12;
int32_t dy = row - 12;
int32_t d2 = dx * dx + dy * dy;
if (d2 <= 144 && d2 >= 100) {
rectFill(d, ops, x + col, y + row, 1, 1, color);
}
}
}
rectFill(d, ops, x + 9, y + 6, 6, 2, color);
rectFill(d, ops, x + 13, y + 8, 2, 4, color);
rectFill(d, ops, x + 11, y + 12, 2, 3, color);
rectFill(d, ops, x + 11, y + 17, 2, 2, color);
}
}
// ============================================================
// dvxMessageBox
// ============================================================
int32_t dvxMessageBox(AppContextT *ctx, const char *title, const char *message, int32_t flags) {
int32_t btnFlags = flags & 0x000F;
int32_t iconFlags = flags & 0x00F0;
// Determine button labels and IDs
const char *btnLabels[3];
int32_t btnIds[3];
int32_t btnCount = 0;
switch (btnFlags) {
case MB_OK:
btnLabels[0] = "&OK";
btnIds[0] = ID_OK;
btnCount = 1;
break;
case MB_OKCANCEL:
btnLabels[0] = "&OK";
btnIds[0] = ID_OK;
btnLabels[1] = "&Cancel";
btnIds[1] = ID_CANCEL;
btnCount = 2;
break;
case MB_YESNO:
btnLabels[0] = "&Yes";
btnIds[0] = ID_YES;
btnLabels[1] = "&No";
btnIds[1] = ID_NO;
btnCount = 2;
break;
case MB_YESNOCANCEL:
btnLabels[0] = "&Yes";
btnIds[0] = ID_YES;
btnLabels[1] = "&No";
btnIds[1] = ID_NO;
btnLabels[2] = "&Cancel";
btnIds[2] = ID_CANCEL;
btnCount = 3;
break;
case MB_RETRYCANCEL:
btnLabels[0] = "&Retry";
btnIds[0] = ID_RETRY;
btnLabels[1] = "&Cancel";
btnIds[1] = ID_CANCEL;
btnCount = 2;
break;
default:
btnLabels[0] = "&OK";
btnIds[0] = ID_OK;
btnCount = 1;
break;
}
// Calculate message text dimensions
bool hasIcon = (iconFlags != 0);
int32_t textMaxW = MSG_MAX_WIDTH - MSG_PADDING * 2 - (hasIcon ? ICON_AREA_WIDTH : 0);
int32_t textH = wordWrapHeight(&ctx->font, message, textMaxW);
// Calculate content area sizes
int32_t msgAreaH = textH + MSG_PADDING * 2;
int32_t iconAreaH = hasIcon ? (24 + MSG_PADDING * 2) : 0;
if (msgAreaH < iconAreaH) {
msgAreaH = iconAreaH;
}
int32_t buttonsW = btnCount * BUTTON_WIDTH + (btnCount - 1) * BUTTON_GAP;
int32_t contentW = MSG_MAX_WIDTH;
if (buttonsW + MSG_PADDING * 2 > contentW) {
contentW = buttonsW + MSG_PADDING * 2;
}
int32_t contentH = msgAreaH + BUTTON_HEIGHT + MSG_PADDING * 3;
// Create the dialog window (non-resizable)
int32_t winX = (ctx->display.width - contentW) / 2 - CHROME_TOTAL_SIDE;
int32_t winY = (ctx->display.height - contentH) / 2 - CHROME_TOTAL_TOP;
WindowT *win = dvxCreateWindow(ctx, title, winX, winY,
contentW + CHROME_TOTAL_SIDE * 2,
contentH + CHROME_TOTAL_TOP + CHROME_TOTAL_BOTTOM,
false);
if (!win) {
return ID_CANCEL;
}
win->modal = true;
// Set up state
sMsgBox.ctx = ctx;
sMsgBox.result = ID_CANCEL;
sMsgBox.done = false;
sMsgBox.message = message;
sMsgBox.iconType = iconFlags;
sMsgBox.textX = MSG_PADDING + (hasIcon ? ICON_AREA_WIDTH : 0);
sMsgBox.textY = MSG_PADDING;
sMsgBox.textMaxW = textMaxW;
sMsgBox.msgAreaH = msgAreaH;
// Create button widgets using wgtInitWindow for proper root setup
// (sets onPaint, onMouse, onKey, onResize, userData on root)
WidgetT *root = wgtInitWindow(ctx, win);
// Override onPaint with our custom handler, set window-level state
win->userData = &sMsgBox;
win->onPaint = onMsgBoxPaint;
win->onClose = onMsgBoxClose;
win->maxW = win->w;
win->maxH = win->h;
if (root) {
// Spacer for message area (text/icon drawn by onPaint)
WidgetT *msgSpacer = wgtSpacer(root);
if (msgSpacer) {
msgSpacer->minH = wgtPixels(msgAreaH);
}
// Button row centered
WidgetT *btnRow = wgtHBox(root);
if (btnRow) {
btnRow->align = AlignCenterE;
for (int32_t i = 0; i < btnCount; i++) {
WidgetT *btn = wgtButton(btnRow, btnLabels[i]);
if (btn) {
btn->minW = wgtPixels(BUTTON_WIDTH);
btn->minH = wgtPixels(BUTTON_HEIGHT);
btn->userData = (void *)(intptr_t)btnIds[i];
btn->onClick = onButtonClick;
}
}
}
// Bottom padding
WidgetT *bottomSpacer = wgtSpacer(root);
if (bottomSpacer) {
bottomSpacer->minH = wgtPixels(MSG_PADDING);
}
}
// Initial paint (window is already correctly sized, don't call dvxFitWindow)
RectT fullRect = { 0, 0, win->contentW, win->contentH };
win->onPaint(win, &fullRect);
// Set as modal (save previous in case we're stacking modals)
WindowT *prevModal = ctx->modalWindow;
ctx->modalWindow = win;
// Nested event loop
while (!sMsgBox.done && ctx->running) {
dvxUpdate(ctx);
}
// Clean up — restore previous modal
ctx->modalWindow = prevModal;
dvxDestroyWindow(ctx, win);
return sMsgBox.result;
}
// ============================================================
// onButtonClick
// ============================================================
static void onButtonClick(WidgetT *w) {
sMsgBox.result = (int32_t)(intptr_t)w->userData;
sMsgBox.done = true;
}
// ============================================================
// onMsgBoxClose
// ============================================================
static void onMsgBoxClose(WindowT *win) {
(void)win;
sMsgBox.result = ID_CANCEL;
sMsgBox.done = true;
}
// ============================================================
// onMsgBoxPaint — custom paint: background + text/icon + widgets
// ============================================================
static void onMsgBoxPaint(WindowT *win, RectT *dirtyArea) {
(void)dirtyArea;
MsgBoxStateT *state = (MsgBoxStateT *)win->userData;
AppContextT *ctx = state->ctx;
// Set up display context pointing at content buffer
DisplayT cd = ctx->display;
cd.lfb = win->contentBuf;
cd.backBuf = win->contentBuf;
cd.width = win->contentW;
cd.height = win->contentH;
cd.pitch = win->contentPitch;
cd.clipX = 0;
cd.clipY = 0;
cd.clipW = win->contentW;
cd.clipH = win->contentH;
// Fill background with window face color (not content bg — dialog style)
rectFill(&cd, &ctx->blitOps, 0, 0, win->contentW, win->contentH, ctx->colors.windowFace);
// Draw word-wrapped message text
wordWrapDraw(&cd, &ctx->blitOps, &ctx->font, state->textX, state->textY, state->message, state->textMaxW, ctx->colors.contentFg, ctx->colors.windowFace);
// Draw icon
if (state->iconType != 0) {
drawIconGlyph(&cd, &ctx->blitOps, MSG_PADDING, MSG_PADDING, state->iconType, ctx->colors.contentFg);
}
// Draw separator line above buttons
drawHLine(&cd, &ctx->blitOps, 0, state->msgAreaH, win->contentW, ctx->colors.windowShadow);
drawHLine(&cd, &ctx->blitOps, 0, state->msgAreaH + 1, win->contentW, ctx->colors.windowHighlight);
// Paint widget tree (buttons) on top
if (win->widgetRoot) {
WidgetT *root = win->widgetRoot;
// Layout widgets
widgetCalcMinSizeTree(root, &ctx->font);
root->x = 0;
root->y = 0;
root->w = win->contentW;
root->h = win->contentH;
widgetLayoutChildren(root, &ctx->font);
// Paint widgets
wgtPaint(root, &cd, &ctx->blitOps, &ctx->font, &ctx->colors);
}
}
// ============================================================
// wordWrapDraw — draw word-wrapped text
// ============================================================
static void wordWrapDraw(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, int32_t x, int32_t y, const char *text, int32_t maxW, uint32_t fg, uint32_t bg) {
int32_t charW = font->charWidth;
int32_t lineH = font->charHeight;
int32_t maxChars = maxW / charW;
int32_t curY = y;
if (maxChars < 1) {
maxChars = 1;
}
while (*text) {
// Skip leading spaces
while (*text == ' ') {
text++;
}
if (*text == '\0') {
break;
}
// Handle explicit newlines
if (*text == '\n') {
curY += lineH;
text++;
continue;
}
// Find how many characters fit on this line
int32_t lineLen = 0;
int32_t lastSpace = -1;
while (text[lineLen] && text[lineLen] != '\n' && lineLen < maxChars) {
if (text[lineLen] == ' ') {
lastSpace = lineLen;
}
lineLen++;
}
// If we didn't reach end and didn't hit newline, wrap at word boundary
if (text[lineLen] && text[lineLen] != '\n' && lastSpace > 0) {
lineLen = lastSpace;
}
drawTextN(d, ops, font, x, curY, text, lineLen, fg, bg, true);
curY += lineH;
text += lineLen;
}
}
// ============================================================
// wordWrapHeight — compute height of word-wrapped text
// ============================================================
static int32_t wordWrapHeight(const BitmapFontT *font, const char *text, int32_t maxW) {
int32_t charW = font->charWidth;
int32_t lineH = font->charHeight;
int32_t maxChars = maxW / charW;
int32_t lines = 0;
if (maxChars < 1) {
maxChars = 1;
}
while (*text) {
while (*text == ' ') {
text++;
}
if (*text == '\0') {
break;
}
if (*text == '\n') {
lines++;
text++;
continue;
}
int32_t lineLen = 0;
int32_t lastSpace = -1;
while (text[lineLen] && text[lineLen] != '\n' && lineLen < maxChars) {
if (text[lineLen] == ' ') {
lastSpace = lineLen;
}
lineLen++;
}
if (text[lineLen] && text[lineLen] != '\n' && lastSpace > 0) {
lineLen = lastSpace;
}
lines++;
text += lineLen;
}
if (lines == 0) {
lines = 1;
}
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;
}