diff --git a/dvx/dvxDialog.c b/dvx/dvxDialog.c index 9f4d61d..2528683 100644 --- a/dvx/dvxDialog.c +++ b/dvx/dvxDialog.c @@ -4,7 +4,13 @@ #include "dvxWidget.h" #include "widgets/widgetInternal.h" +#include +#include +#include +#include #include +#include +#include // ============================================================ // 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 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); @@ -283,16 +303,17 @@ int32_t dvxMessageBox(AppContextT *ctx, const char *title, const char *message, RectT fullRect = { 0, 0, win->contentW, win->contentH }; win->onPaint(win, &fullRect); - // Set as modal - ctx->modalWindow = win; + // 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 - ctx->modalWindow = NULL; + // Clean up — restore previous modal + ctx->modalWindow = prevModal; dvxDestroyWindow(ctx, win); return sMsgBox.result; @@ -484,3 +505,659 @@ static int32_t wordWrapHeight(const BitmapFontT *font, const char *text, int32_t 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; +} diff --git a/dvx/dvxDialog.h b/dvx/dvxDialog.h index 72acea9..d84d38c 100644 --- a/dvx/dvxDialog.h +++ b/dvx/dvxDialog.h @@ -37,4 +37,27 @@ // flags = MB_xxx button flag | MB_ICONxxx icon flag 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 diff --git a/dvx/dvxWidget.h b/dvx/dvxWidget.h index 645b5c3..ab946b2 100644 --- a/dvx/dvxWidget.h +++ b/dvx/dvxWidget.h @@ -187,6 +187,7 @@ typedef struct WidgetT { void *userData; void (*onClick)(struct WidgetT *w); void (*onChange)(struct WidgetT *w); + void (*onDblClick)(struct WidgetT *w); // Type-specific data union { diff --git a/dvx/widgets/widgetListBox.c b/dvx/widgets/widgetListBox.c index 8321294..183e0eb 100644 --- a/dvx/widgets/widgetListBox.c +++ b/dvx/widgets/widgetListBox.c @@ -522,6 +522,10 @@ void widgetListBoxOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy) { if (hit->onChange) { hit->onChange(hit); } + + if (hit->onDblClick && multiClickDetect(vx, vy) >= 2) { + hit->onDblClick(hit); + } } diff --git a/dvx/widgets/widgetListView.c b/dvx/widgets/widgetListView.c index 2625b57..0163e55 100644 --- a/dvx/widgets/widgetListView.c +++ b/dvx/widgets/widgetListView.c @@ -1020,6 +1020,10 @@ void widgetListViewOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy) if (hit->onChange) { hit->onChange(hit); } + + if (hit->onDblClick && multiClickDetect(vx, vy) >= 2) { + hit->onDblClick(hit); + } } wgtInvalidatePaint(hit); diff --git a/dvxdemo/demo.c b/dvxdemo/demo.c index 7b888be..cd06743 100644 --- a/dvxdemo/demo.c +++ b/dvxdemo/demo.c @@ -143,10 +143,42 @@ static void onCloseMainCb(WindowT *win) { // 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) { AppContextT *ctx = (AppContextT *)win->userData; 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: if (ctx) { int32_t result = dvxMessageBox(ctx, "Exit",