From 88746ec2ba4780093b3c0b14a9b68ef8c505d574 Mon Sep 17 00:00:00 2001 From: Scott Duensing Date: Thu, 2 Apr 2026 20:06:48 -0500 Subject: [PATCH] ProgMan now requires double clicks to launch. Enum type added to widgets. Huge amount of BASIC work. --- apps/dvxbasic/compiler/opcodes.h | 3 +- apps/dvxbasic/compiler/parser.c | 4 +- apps/dvxbasic/formrt/formrt.c | 22 ++ apps/dvxbasic/ide/ideDesigner.c | 87 +++++ apps/dvxbasic/ide/ideMain.c | 526 ++++++++++++++++++++---------- apps/dvxbasic/ide/ideProject.c | 241 ++++++++++---- apps/dvxbasic/ide/ideProject.h | 2 + apps/dvxbasic/ide/ideProperties.c | 76 +++++ apps/dvxbasic/runtime/vm.c | 10 +- apps/dvxbasic/runtime/vm.h | 1 + apps/progman/progman.c | 7 +- core/dvxApp.c | 27 +- core/dvxApp.h | 3 + core/dvxWidget.h | 10 +- listhelp/listHelp.c | 24 ++ widgets/dropdown/widgetDropdown.c | 11 +- widgets/label/widgetLabel.c | 14 +- 17 files changed, 807 insertions(+), 261 deletions(-) diff --git a/apps/dvxbasic/compiler/opcodes.h b/apps/dvxbasic/compiler/opcodes.h index 158a1e4..6715d93 100644 --- a/apps/dvxbasic/compiler/opcodes.h +++ b/apps/dvxbasic/compiler/opcodes.h @@ -313,6 +313,7 @@ // Halt // ============================================================ -#define OP_HALT 0xFF +#define OP_END 0xFE // explicit END statement -- terminates program +#define OP_HALT 0xFF // implicit end of module #endif // DVXBASIC_OPCODES_H diff --git a/apps/dvxbasic/compiler/parser.c b/apps/dvxbasic/compiler/parser.c index 9e85fa0..6bf4a5e 100644 --- a/apps/dvxbasic/compiler/parser.c +++ b/apps/dvxbasic/compiler/parser.c @@ -2613,10 +2613,10 @@ static void parseDo(BasParserT *p) { static void parseEnd(BasParserT *p) { - // END -- by itself = halt + // END -- by itself = terminate program // END IF / END SUB / END FUNCTION / END SELECT are handled by their parsers advance(p); // consume END - basEmit8(&p->cg, OP_HALT); + basEmit8(&p->cg, OP_END); } diff --git a/apps/dvxbasic/formrt/formrt.c b/apps/dvxbasic/formrt/formrt.c index 67a4ffe..a33f8ae 100644 --- a/apps/dvxbasic/formrt/formrt.c +++ b/apps/dvxbasic/formrt/formrt.c @@ -491,12 +491,15 @@ void *basFormRtLoadForm(void *ctx, const char *formName) { } } + // Forms start hidden; code must call Show to make them visible WindowT *win = dvxCreateWindowCentered(rt->ctx, formName, DEFAULT_FORM_W, DEFAULT_FORM_H, true); if (!win) { return NULL; } + win->visible = false; + WidgetT *root = wgtInitWindow(rt->ctx, win); if (!root) { @@ -898,6 +901,7 @@ void basFormRtShowForm(void *ctx, void *formRef, bool modal) { } form->window->visible = true; + dvxRaiseWindow(rt->ctx, form->window); if (form->frmAutoSize) { dvxFitWindow(rt->ctx, form->window); @@ -1836,6 +1840,24 @@ static bool setIfaceProp(const WgtIfaceT *iface, WidgetT *w, const char *propNam break; } + case WGT_IFACE_ENUM: + if (p->enumNames && value.type == BAS_TYPE_STRING && value.strVal) { + // Map name to index + int32_t enumVal = 0; + + for (int32_t en = 0; p->enumNames[en]; en++) { + if (strcasecmp(p->enumNames[en], value.strVal->data) == 0) { + enumVal = en; + break; + } + } + + ((void (*)(WidgetT *, int32_t))p->setFn)(w, enumVal); + } else { + ((void (*)(WidgetT *, int32_t))p->setFn)(w, (int32_t)basValToNumber(value)); + } + break; + case WGT_IFACE_INT: ((void (*)(WidgetT *, int32_t))p->setFn)(w, (int32_t)basValToNumber(value)); break; diff --git a/apps/dvxbasic/ide/ideDesigner.c b/apps/dvxbasic/ide/ideDesigner.c index 152b370..90f39c2 100644 --- a/apps/dvxbasic/ide/ideDesigner.c +++ b/apps/dvxbasic/ide/ideDesigner.c @@ -220,6 +220,41 @@ void dsgnCreateWidgets(DsgnStateT *ds, WidgetT *contentBox) { } w->weight = ctrl->weight; + + // Apply interface properties (Alignment, etc.) from FRM data + const char *wgtName = wgtFindByBasName(ctrl->typeName); + const WgtIfaceT *iface = wgtName ? wgtGetIface(wgtName) : NULL; + + if (iface) { + for (int32_t pi = 0; pi < iface->propCount; pi++) { + const WgtPropDescT *p = &iface->props[pi]; + + if (!p->setFn) { + continue; + } + + const char *val = getPropValue(ctrl, p->name); + + if (!val) { + continue; + } + + if (p->type == WGT_IFACE_ENUM && p->enumNames) { + for (int32_t en = 0; p->enumNames[en]; en++) { + if (strcasecmp(p->enumNames[en], val) == 0) { + ((void (*)(WidgetT *, int32_t))p->setFn)(w, en); + break; + } + } + } else if (p->type == WGT_IFACE_INT) { + ((void (*)(WidgetT *, int32_t))p->setFn)(w, atoi(val)); + } else if (p->type == WGT_IFACE_BOOL) { + ((void (*)(WidgetT *, bool))p->setFn)(w, strcasecmp(val, "True") == 0); + } else if (p->type == WGT_IFACE_STRING) { + ((void (*)(WidgetT *, const char *))p->setFn)(w, val); + } + } + } } } @@ -901,6 +936,58 @@ static int32_t saveControls(const DsgnFormT *form, char *buf, int32_t bufSize, i pos += snprintf(buf + pos, bufSize - pos, "%s %s = \"%s\"\n", pad, ctrl->props[j].name, ctrl->props[j].value); } + // Save interface properties (Alignment, etc.) read from the live widget + if (ctrl->widget) { + const char *wgtName = wgtFindByBasName(ctrl->typeName); + const WgtIfaceT *iface = wgtName ? wgtGetIface(wgtName) : NULL; + + if (iface) { + for (int32_t j = 0; j < iface->propCount; j++) { + const WgtPropDescT *p = &iface->props[j]; + + if (!p->getFn) { + continue; + } + + // Skip if already saved as a custom prop + bool already = false; + + for (int32_t k = 0; k < ctrl->propCount; k++) { + if (strcasecmp(ctrl->props[k].name, p->name) == 0) { + already = true; + break; + } + } + + if (already) { + continue; + } + + if (p->type == WGT_IFACE_ENUM && p->enumNames) { + int32_t v = ((int32_t (*)(const WidgetT *))p->getFn)(ctrl->widget); + const char *name = NULL; + + for (int32_t en = 0; p->enumNames[en]; en++) { + if (en == v) { + name = p->enumNames[en]; + break; + } + } + + if (name) { + pos += snprintf(buf + pos, bufSize - pos, "%s %s = %s\n", pad, p->name, name); + } + } else if (p->type == WGT_IFACE_INT) { + int32_t v = ((int32_t (*)(const WidgetT *))p->getFn)(ctrl->widget); + pos += snprintf(buf + pos, bufSize - pos, "%s %s = %d\n", pad, p->name, (int)v); + } else if (p->type == WGT_IFACE_BOOL) { + bool v = ((bool (*)(const WidgetT *))p->getFn)(ctrl->widget); + pos += snprintf(buf + pos, bufSize - pos, "%s %s = %s\n", pad, p->name, v ? "True" : "False"); + } + } + } + } + // Recursively output children of this container if (dsgnIsContainer(ctrl->typeName)) { pos = saveControls(form, buf, bufSize, pos, ctrl->name, indent + 1); diff --git a/apps/dvxbasic/ide/ideMain.c b/apps/dvxbasic/ide/ideMain.c index 194af82..caed4e4 100644 --- a/apps/dvxbasic/ide/ideMain.c +++ b/apps/dvxbasic/ide/ideMain.c @@ -107,7 +107,8 @@ static void loadFile(void); static void parseProcs(const char *source); static void updateProjectMenuState(void); static void saveActiveFile(void); -static void saveCurProc(void); +static bool saveCurProc(void); +static void stashFormCode(void); static void showProc(int32_t procIdx); static int32_t toolbarBottom(void); static void loadFilePath(const char *path); @@ -197,6 +198,7 @@ static int32_t sOutputLen = 0; static char *sGeneralBuf = NULL; // (General) section: module-level code static char **sProcBufs = NULL; // stb_ds array: one buffer per procedure static int32_t sCurProcIdx = -2; // which buffer is in the editor (-1=General, -2=none) +static int32_t sEditorFileIdx = -1; // which project file owns sProcBufs (-1=none) // Procedure table for Object/Event dropdowns typedef struct { @@ -209,6 +211,7 @@ static IdeProcEntryT *sProcTable = NULL; // stb_ds dynamic array static const char **sObjItems = NULL; // stb_ds dynamic array static const char **sEvtItems = NULL; // stb_ds dynamic array static bool sDropdownNavSuppressed = false; +static bool sStopRequested = false; // ============================================================ // App descriptor @@ -265,6 +268,7 @@ int32_t appMain(DxeAppContextT *ctx) { // Auto-load project for development/testing if (prjLoad(&sProject, "C:\\BIN\\APPS\\DVXBASIC\\MULTI.DBP")) { + prjLoadAllFiles(&sProject, sAc); sProjectWin = prjCreateWindow(sAc, &sProject, onPrjFileClick); if (sProjectWin) { @@ -332,9 +336,10 @@ static void buildWindow(void) { wmAddMenuItem(fileMenu, "Open Pro&ject...", CMD_PRJ_OPEN); wmAddMenuItem(fileMenu, "Save Projec&t", CMD_PRJ_SAVE); wmAddMenuItem(fileMenu, "Close Projec&t", CMD_PRJ_CLOSE); + wmAddMenuSeparator(fileMenu); wmAddMenuItem(fileMenu, "Project &Properties...", CMD_PRJ_PROPS); wmAddMenuSeparator(fileMenu); - wmAddMenuItem(fileMenu, "&Open File...\tCtrl+O", CMD_OPEN); + wmAddMenuItem(fileMenu, "&Add File...\tCtrl+O", CMD_OPEN); wmAddMenuItem(fileMenu, "&Save File\tCtrl+S", CMD_SAVE); wmAddMenuItem(fileMenu, "Save A&ll", CMD_SAVE_ALL); wmAddMenuSeparator(fileMenu); @@ -439,54 +444,73 @@ static void buildWindow(void) { // Syntax colorizer callback for BASIC source code. Scans a single // line and fills the colors array with syntax color indices. -static bool isBasicKeyword(const char *word, int32_t wordLen) { +// Hash-based keyword/type lookup using stb_ds. +// Key = uppercase word, value = syntax color (1=keyword, 6=type). +// Built once on first use, then O(1) per lookup. + +typedef struct { + char *key; + uint8_t value; +} SyntaxMapEntryT; + +static SyntaxMapEntryT *sSyntaxMap = NULL; + + +static void initSyntaxMap(void) { + if (sSyntaxMap) { + return; + } + + sh_new_arena(sSyntaxMap); + static const char *keywords[] = { - "AND", "AS", "CALL", "CASE", "CLOSE", "CONST", "DATA", "DECLARE", - "DEF", "DEFINT", "DEFLNG", "DEFSNG", "DEFDBL", "DEFSTR", - "DIM", "DO", "DOEVENTS", "ELSE", "ELSEIF", "END", "ERASE", - "EXIT", "FOR", "FUNCTION", "GET", "GOSUB", "GOTO", "HIDE", - "IF", "IMP", "INPUT", "IS", "LET", "LIBRARY", "LINE", "LOAD", - "LOOP", "MOD", "MSGBOX", "NEXT", "NOT", "ON", "OPEN", "OPTION", - "OR", "PRINT", "PUT", "RANDOMIZE", "READ", "REDIM", "RESTORE", - "RESUME", "RETURN", "SEEK", "SELECT", "SHARED", "SHELL", "SHOW", - "SLEEP", "STATIC", "STEP", "STOP", "SUB", "SWAP", "THEN", "TO", - "TYPE", "UNLOAD", "UNTIL", "WEND", "WHILE", "WRITE", "XOR", + "AND", "AS", "BYVAL", "CALL", "CASE", "CLOSE", "CONST", + "DATA", "DECLARE", "DEF", "DEFDBL", "DEFINT", "DEFLNG", + "DEFSNG", "DEFSTR", "DIM", "DO", "DOEVENTS", + "ELSE", "ELSEIF", "END", "ERASE", "EXIT", + "FOR", "FUNCTION", + "GET", "GOSUB", "GOTO", + "HIDE", + "IF", "IMP", "INPUT", "IS", + "LET", "LIBRARY", "LINE", "LOAD", "LOOP", + "ME", "MOD", "MSGBOX", + "NEXT", "NOT", + "ON", "OPEN", "OPTION", "OR", + "PRINT", "PUT", + "RANDOMIZE", "READ", "REDIM", "RESTORE", "RESUME", "RETURN", + "SEEK", "SELECT", "SHARED", "SHELL", "SHOW", "SLEEP", + "STATIC", "STEP", "STOP", "SUB", "SWAP", + "THEN", "TO", "TYPE", + "UNLOAD", "UNTIL", + "WEND", "WHILE", "WRITE", + "XOR", NULL }; - char upper[32]; - - if (wordLen <= 0 || wordLen >= 32) { - return false; - } - - for (int32_t i = 0; i < wordLen; i++) { - upper[i] = (char)toupper((unsigned char)word[i]); - } - - upper[wordLen] = '\0'; + static const char *types[] = { + "BOOLEAN", "BYTE", "DOUBLE", "FALSE", "INTEGER", + "LONG", "SINGLE", "STRING", "TRUE", + NULL + }; for (int32_t i = 0; keywords[i]; i++) { - if (strcmp(upper, keywords[i]) == 0) { - return true; - } + shput(sSyntaxMap, keywords[i], 1); } - return false; + for (int32_t i = 0; types[i]; i++) { + shput(sSyntaxMap, types[i], 6); + } } -static bool isBasicType(const char *word, int32_t wordLen) { - static const char *types[] = { - "BOOLEAN", "BYTE", "DOUBLE", "INTEGER", "LONG", "SINGLE", "STRING", - "TRUE", "FALSE", - NULL - }; +// classifyWord -- returns syntax color for an identifier. +// Converts to uppercase once, then does a single hash lookup. +static uint8_t classifyWord(const char *word, int32_t wordLen) { char upper[32]; if (wordLen <= 0 || wordLen >= 32) { - return false; + return 0; } for (int32_t i = 0; i < wordLen; i++) { @@ -495,13 +519,15 @@ static bool isBasicType(const char *word, int32_t wordLen) { upper[wordLen] = '\0'; - for (int32_t i = 0; types[i]; i++) { - if (strcmp(upper, types[i]) == 0) { - return true; - } + initSyntaxMap(); + + int32_t idx = shgeti(sSyntaxMap, upper); + + if (idx >= 0) { + return sSyntaxMap[idx].value; } - return false; + return 0; } @@ -562,13 +588,7 @@ static void basicColorize(const char *line, int32_t lineLen, uint8_t *colors, vo return; } - uint8_t c = 0; // default - - if (isBasicKeyword(line + start, wordLen)) { - c = 1; // SYNTAX_KEYWORD - } else if (isBasicType(line + start, wordLen)) { - c = 6; // SYNTAX_TYPE - } + uint8_t c = classifyWord(line + start, wordLen); for (int32_t j = start; j < i; j++) { colors[j] = c; @@ -678,23 +698,20 @@ static void compileAndRun(void) { int32_t srcLen = 0; if (sProject.projectPath[0] != '\0' && sProject.fileCount > 0) { - // Stash current editor state - if (sProject.activeFileIdx >= 0) { - PrjFileT *cur = &sProject.files[sProject.activeFileIdx]; + // Stash current editor state to the file that owns the proc buffers + if (sEditorFileIdx >= 0 && sEditorFileIdx < sProject.fileCount) { + PrjFileT *edFile = &sProject.files[sEditorFileIdx]; - if (!cur->isForm) { + if (!edFile->isForm) { + saveCurProc(); const char *fullSrc = getFullSource(); - free(cur->buffer); - cur->buffer = fullSrc ? strdup(fullSrc) : NULL; + free(edFile->buffer); + edFile->buffer = fullSrc ? strdup(fullSrc) : NULL; } } - // Stash form code if editing a form's code - if (sDesigner.form && sCurProcIdx >= -1) { - saveCurProc(); - free(sDesigner.form->code); - sDesigner.form->code = strdup(getFullSource()); - } + // Stash form code if the editor has form code loaded + stashFormCode(); // Concatenate all .bas files from buffers (or disk if not yet loaded) concatBuf = (char *)malloc(IDE_MAX_SOURCE); @@ -710,7 +727,14 @@ static void compileAndRun(void) { sProject.sourceMap = NULL; sProject.sourceMapCount = 0; + // Two passes: .bas modules first (so CONST declarations are + // available), then .frm code sections. + for (int32_t pass = 0; pass < 2; pass++) for (int32_t i = 0; i < sProject.fileCount; i++) { + // Pass 0: modules only. Pass 1: forms only. + if (pass == 0 && sProject.files[i].isForm) { continue; } + if (pass == 1 && !sProject.files[i].isForm) { continue; } + const char *fileSrc = NULL; char *diskBuf = NULL; @@ -935,6 +959,19 @@ static void runCached(void) { static void runModule(BasModuleT *mod) { setStatus("Running..."); + // Hide IDE windows while the program runs + bool hadFormWin = sFormWin && sFormWin->visible; + bool hadToolbox = sToolboxWin && sToolboxWin->visible; + bool hadProps = sPropsWin && sPropsWin->visible; + bool hadCodeWin = sCodeWin && sCodeWin->visible; + bool hadPrjWin = sProjectWin && sProjectWin->visible; + + if (sFormWin) { sFormWin->visible = false; dvxInvalidateWindow(sAc, sFormWin); } + if (sToolboxWin) { sToolboxWin->visible = false; dvxInvalidateWindow(sAc, sToolboxWin); } + if (sPropsWin) { sPropsWin->visible = false; dvxInvalidateWindow(sAc, sPropsWin); } + if (sCodeWin) { sCodeWin->visible = false; dvxInvalidateWindow(sAc, sCodeWin); } + if (sProjectWin) { sProjectWin->visible = false; dvxInvalidateWindow(sAc, sProjectWin); } + // Create VM BasVmT *vm = basVmCreate(); basVmLoadModule(vm, mod); @@ -954,9 +991,21 @@ static void runModule(BasModuleT *mod) { // Load any .frm files from the same directory as the source loadFrmFiles(formRt); - // Auto-show the first form (like VB3's startup form) + // Auto-show the startup form (or first form if none specified). + // Other forms remain hidden until code calls Show. if (formRt->formCount > 0) { - basFormRtShowForm(formRt, &formRt->forms[0], false); + BasFormT *startupForm = &formRt->forms[0]; + + if (sProject.startupForm[0]) { + for (int32_t i = 0; i < formRt->formCount; i++) { + if (strcasecmp(formRt->forms[i].name, sProject.startupForm) == 0) { + startupForm = &formRt->forms[i]; + break; + } + } + } + + basFormRtShowForm(formRt, startupForm, false); } sVm = vm; @@ -966,6 +1015,7 @@ static void runModule(BasModuleT *mod) { int32_t totalSteps = 0; BasVmResultE result; + sStopRequested = false; for (;;) { result = basVmRun(vm); @@ -975,8 +1025,8 @@ static void runModule(BasModuleT *mod) { // Yield to DVX to keep the GUI responsive dvxUpdate(sAc); - // Stop if IDE window was closed or DVX is shutting down - if (!sWin || !sAc->running) { + // Stop if IDE window was closed, DVX is shutting down, or user hit Stop + if (!sWin || !sAc->running || sStopRequested) { break; } @@ -1000,8 +1050,9 @@ static void runModule(BasModuleT *mod) { // The program ends when all forms are unloaded (closed). if (result == BAS_VM_HALTED && formRt->formCount > 0) { setStatus("Running (event loop)..."); + sStopRequested = false; - while (sWin && sAc->running && formRt->formCount > 0) { + while (sWin && sAc->running && formRt->formCount > 0 && !sStopRequested && !vm->ended) { dvxUpdate(sAc); } } @@ -1017,6 +1068,16 @@ static void runModule(BasModuleT *mod) { basFormRtDestroy(formRt); basVmDestroy(vm); + + // Restore IDE windows + if (hadFormWin && sFormWin) { sFormWin->visible = true; dvxInvalidateWindow(sAc, sFormWin); } + if (hadToolbox && sToolboxWin) { sToolboxWin->visible = true; dvxInvalidateWindow(sAc, sToolboxWin); } + if (hadProps && sPropsWin) { sPropsWin->visible = true; dvxInvalidateWindow(sAc, sPropsWin); } + if (hadCodeWin && sCodeWin) { sCodeWin->visible = true; dvxInvalidateWindow(sAc, sCodeWin); } + if (hadPrjWin && sProjectWin) { sProjectWin->visible = true; dvxInvalidateWindow(sAc, sProjectWin); } + + // Repaint to clear destroyed runtime forms and restore designer + dvxUpdate(sAc); } // ============================================================ @@ -1191,6 +1252,9 @@ static void loadFilePath(const char *path) { showCodeWindow(); } + // Stash form code before overwriting proc buffers + stashFormCode(); + // Parse into per-procedure buffers and show (General) section parseProcs(srcBuf); free(srcBuf); @@ -1303,6 +1367,8 @@ static void ensureProject(const char *filePath) { sProject.dirty = false; sProject.activeFileIdx = 0; + prjLoadAllFiles(&sProject, sAc); + char title[300]; snprintf(title, sizeof(title), "DVX BASIC - [%s]", sProject.name); @@ -1323,7 +1389,7 @@ static void loadFile(void) { char path[DVX_MAX_PATH]; - if (!dvxFileDialog(sAc, "Open BASIC File", FD_OPEN, NULL, filters, 3, path, sizeof(path))) { + if (!dvxFileDialog(sAc, "Add File", FD_OPEN, NULL, filters, 3, path, sizeof(path))) { return; } @@ -1358,6 +1424,7 @@ static void loadFile(void) { onPrjFileClick(0, true); } else { loadFilePath(path); + sEditorFileIdx = 0; } } } @@ -1381,36 +1448,52 @@ static void saveActiveFile(void) { char fullPath[DVX_MAX_PATH]; prjFullPath(&sProject, idx, fullPath, sizeof(fullPath)); - if (file->isForm && sDesigner.form) { - // Save editor code back to form->code before saving - if (sCurProcIdx >= -1) { - saveCurProc(); - free(sDesigner.form->code); - sDesigner.form->code = strdup(getFullSource()); - } + if (file->isForm) { + // Only serialize through the designer if it holds THIS form + bool isDesignerForm = (sDesigner.form && + strcasecmp(sDesigner.form->name, file->formName) == 0); - // Save form designer state to .frm file - char *frmBuf = (char *)malloc(IDE_MAX_SOURCE); + if (isDesignerForm) { + stashFormCode(); - if (frmBuf) { - int32_t frmLen = dsgnSaveFrm(&sDesigner, frmBuf, IDE_MAX_SOURCE); + char *frmBuf = (char *)malloc(IDE_MAX_SOURCE); - if (frmLen > 0) { - FILE *f = fopen(fullPath, "w"); + if (frmBuf) { + int32_t frmLen = dsgnSaveFrm(&sDesigner, frmBuf, IDE_MAX_SOURCE); - if (f) { - fwrite(frmBuf, 1, frmLen, f); - fclose(f); - sDesigner.form->dirty = false; - file->modified = false; + if (frmLen > 0) { + FILE *f = fopen(fullPath, "w"); + + if (f) { + fwrite(frmBuf, 1, frmLen, f); + fclose(f); + sDesigner.form->dirty = false; + file->modified = false; + } } - } - free(frmBuf); + free(frmBuf); + } + } else if (file->buffer) { + // Not the active designer form -- save from stashed buffer + FILE *f = fopen(fullPath, "w"); + + if (f) { + fputs(file->buffer, f); + fclose(f); + file->modified = false; + } + } + } else { + // Save .bas file -- use editor if it has this file, else use buffer + const char *src = NULL; + + if (sEditorFileIdx == idx) { + saveCurProc(); + src = getFullSource(); + } else { + src = file->buffer; } - } else if (!file->isForm) { - // Save full source (splice current proc back first) - const char *src = getFullSource(); if (src) { FILE *f = fopen(fullPath, "w"); @@ -1472,14 +1555,22 @@ static void onPrjFileClick(int32_t fileIdx, bool isForm) { } if (fileIdx == sProject.activeFileIdx) { + // Already active -- but ensure the right view is shown + if (isForm) { + switchToDesign(); + } return; } - // Stash current active file's contents into its buffer + // Stash current active file's contents into its buffer. + // This is just caching -- do not mark modified. if (sProject.activeFileIdx >= 0) { PrjFileT *cur = &sProject.files[sProject.activeFileIdx]; if (cur->isForm && sDesigner.form) { + // Save editor code back to form->code before serializing + stashFormCode(); + // Serialize form designer state to .frm text char *frmBuf = (char *)malloc(IDE_MAX_SOURCE); @@ -1495,15 +1586,13 @@ static void onPrjFileClick(int32_t fileIdx, bool isForm) { free(frmBuf); cur->buffer = NULL; } - - cur->modified = true; } - } else if (!cur->isForm) { - // Stash full source (splice current proc back first) + } else if (!cur->isForm && sEditorFileIdx == sProject.activeFileIdx) { + // Stash full source (only if editor has this file's code) + saveCurProc(); const char *src = getFullSource(); free(cur->buffer); - cur->buffer = src ? strdup(src) : NULL; - cur->modified = true; + cur->buffer = src ? strdup(src) : NULL; } } @@ -1550,11 +1639,17 @@ static void onPrjFileClick(int32_t fileIdx, bool isForm) { *dot = '\0'; } + if (sFormWin) { + dvxDestroyWindow(sAc, sFormWin); + cleanupFormWin(); + } + if (sDesigner.form) { dsgnFree(&sDesigner); } dsgnNewForm(&sDesigner, formName); + snprintf(target->formName, sizeof(target->formName), "%s", sDesigner.form->name); target->modified = true; sProject.activeFileIdx = fileIdx; switchToDesign(); @@ -1583,6 +1678,12 @@ static void onPrjFileClick(int32_t fileIdx, bool isForm) { frmSrc = diskBuf; } + // Close the old form designer window before loading a new form + if (sFormWin) { + dvxDestroyWindow(sAc, sFormWin); + cleanupFormWin(); + } + if (sDesigner.form) { dsgnFree(&sDesigner); } @@ -1590,10 +1691,16 @@ static void onPrjFileClick(int32_t fileIdx, bool isForm) { dsgnLoadFrm(&sDesigner, frmSrc, (int32_t)strlen(frmSrc)); free(diskBuf); + if (sDesigner.form) { + snprintf(target->formName, sizeof(target->formName), "%s", sDesigner.form->name); + } + sProject.activeFileIdx = fileIdx; switchToDesign(); } else { // Load .bas file from buffer or disk + stashFormCode(); + if (!sCodeWin) { showCodeWindow(); } @@ -1624,6 +1731,7 @@ static void onPrjFileClick(int32_t fileIdx, bool isForm) { sEditor->onChange = onEditorChange; } + sEditorFileIdx = fileIdx; sProject.activeFileIdx = fileIdx; } } @@ -1724,6 +1832,8 @@ static void openProject(void) { return; } + prjLoadAllFiles(&sProject, sAc); + // Create and show project window if (!sProjectWin) { sProjectWin = prjCreateWindow(sAc, &sProject, onPrjFileClick); @@ -1766,6 +1876,26 @@ static void closeProject(void) { prjSave(&sProject); } + // Close designer windows + if (sFormWin) { + dvxDestroyWindow(sAc, sFormWin); + cleanupFormWin(); + } + + dsgnFree(&sDesigner); + + // Close code editor + if (sCodeWin) { + dvxDestroyWindow(sAc, sCodeWin); + sCodeWin = NULL; + sEditor = NULL; + sObjDropdown = NULL; + sEvtDropdown = NULL; + } + + freeProcBufs(); + + // Close project window prjClose(&sProject); if (sProjectWin) { @@ -1938,13 +2068,24 @@ void ideRenameInCode(const char *oldName, const char *newName) { } } - // Update form->code from the renamed buffers - if (sDesigner.form) { + // Update form->code from the renamed buffers (only if editor has this form's code) + if (sDesigner.form && sEditorFileIdx >= 0 && sEditorFileIdx < sProject.fileCount && + sProject.files[sEditorFileIdx].isForm && + strcasecmp(sProject.files[sEditorFileIdx].formName, sDesigner.form->name) == 0) { free(sDesigner.form->code); sDesigner.form->code = strdup(getFullSource()); sDesigner.form->dirty = true; } + // Update cached formName if the active file is a form being renamed + if (sProject.activeFileIdx >= 0 && sProject.activeFileIdx < sProject.fileCount) { + PrjFileT *cur = &sProject.files[sProject.activeFileIdx]; + + if (cur->isForm && strcasecmp(cur->formName, oldName) == 0) { + snprintf(cur->formName, sizeof(cur->formName), "%s", newName); + } + } + // Rename in all project .bas file buffers (and non-active .frm code) for (int32_t i = 0; i < sProject.fileCount; i++) { // Skip the active file (already handled above) @@ -2006,13 +2147,8 @@ void ideRenameInCode(const char *oldName, const char *newName) { // ============================================================ static void onCodeWinClose(WindowT *win) { - // Stash code back to form->code before the window is destroyed. - // This is just caching -- do not mark dirty. - if (sDesigner.form && sCurProcIdx >= -1) { - saveCurProc(); - free(sDesigner.form->code); - sDesigner.form->code = strdup(getFullSource()); - } + // Stash code back before the window is destroyed. + stashFormCode(); dvxDestroyWindow(sAc, win); sCodeWin = NULL; @@ -2043,43 +2179,47 @@ static void onProjectWinClose(WindowT *win) { // Load all .frm files listed in the current project into the // form runtime for execution. -static void loadFrmFile(BasFormRtT *rt, const char *frmPath) { - FILE *f = fopen(frmPath, "r"); - - if (!f) { - return; - } - - fseek(f, 0, SEEK_END); - long size = ftell(f); - fseek(f, 0, SEEK_SET); - - if (size <= 0 || size >= IDE_MAX_SOURCE) { - fclose(f); - return; - } - - char *frmBuf = (char *)malloc(size + 1); - - if (!frmBuf) { - fclose(f); - return; - } - - int32_t bytesRead = (int32_t)fread(frmBuf, 1, size, f); - fclose(f); - frmBuf[bytesRead] = '\0'; - - basFormRtLoadFrm(rt, frmBuf, bytesRead); - free(frmBuf); -} - static void loadFrmFiles(BasFormRtT *rt) { for (int32_t i = 0; i < sProject.fileCount; i++) { - if (sProject.files[i].isForm) { - char fullPath[DVX_MAX_PATH]; - prjFullPath(&sProject, i, fullPath, sizeof(fullPath)); - loadFrmFile(rt, fullPath); + if (!sProject.files[i].isForm) { + continue; + } + + char fullPath[DVX_MAX_PATH]; + prjFullPath(&sProject, i, fullPath, sizeof(fullPath)); + + FILE *f = fopen(fullPath, "r"); + + if (!f) { + continue; + } + + fseek(f, 0, SEEK_END); + long size = ftell(f); + fseek(f, 0, SEEK_SET); + + if (size <= 0 || size >= IDE_MAX_SOURCE) { + fclose(f); + continue; + } + + char *frmBuf = (char *)malloc(size + 1); + + if (!frmBuf) { + fclose(f); + continue; + } + + int32_t bytesRead = (int32_t)fread(frmBuf, 1, size, f); + fclose(f); + frmBuf[bytesRead] = '\0'; + + BasFormT *form = basFormRtLoadFrm(rt, frmBuf, bytesRead); + free(frmBuf); + + // Cache the form object name in the project file entry + if (form && form->name[0]) { + snprintf(sProject.files[i].formName, sizeof(sProject.files[i].formName), "%s", form->name); } } } @@ -2297,10 +2437,11 @@ static void onMenu(WindowT *win, int32_t menuId) { break; case CMD_STOP: + sStopRequested = true; if (sVm) { sVm->running = false; - setStatus("Program stopped."); } + setStatus("Program stopped."); break; case CMD_CLEAR: @@ -2569,19 +2710,7 @@ static void onEvtDropdownChange(WidgetT *w) { snprintf(skeleton, sizeof(skeleton), "Sub %s ()\n\nEnd Sub\n", subName); arrput(sProcBufs, strdup(skeleton)); - - updateDropdowns(); - - // Show the new proc (last in the list) - procCount = (int32_t)arrlen(sProcTable); - - for (int32_t i = 0; i < procCount; i++) { - if (strcasecmp(sProcTable[i].objName, selObj) == 0 && - strcasecmp(sProcTable[i].evtName, evtName) == 0) { - showProc(i); - return; - } - } + showProc((int32_t)arrlen(sProcBufs) - 1); } @@ -3048,9 +3177,10 @@ static void selectDropdowns(const char *objName, const char *evtName) { } // Rebuild the event list for this object but suppress navigation + bool savedSuppress = sDropdownNavSuppressed; sDropdownNavSuppressed = true; onObjDropdownChange(sObjDropdown); - sDropdownNavSuppressed = false; + sDropdownNavSuppressed = savedSuppress; // Now select the specific event int32_t evtCount = (int32_t)arrlen(sEvtItems); @@ -3100,8 +3230,10 @@ static void navigateToEventSub(void) { char subName[128]; snprintf(subName, sizeof(subName), "%s_%s", ctrlName, eventName); - // Parse the form's code into per-procedure buffers + // Stash any existing editor code, then load this form's code + stashFormCode(); parseProcs(sDesigner.form->code ? sDesigner.form->code : ""); + sEditorFileIdx = sProject.activeFileIdx; // Ensure code window is open if (!sCodeWin) { @@ -3114,9 +3246,12 @@ static void navigateToEventSub(void) { // Populate dropdown items without triggering navigation -- // we navigate explicitly below after finding the target proc. - sDropdownNavSuppressed = true; - updateDropdowns(); - sDropdownNavSuppressed = false; + { + bool saved = sDropdownNavSuppressed; + sDropdownNavSuppressed = true; + updateDropdowns(); + sDropdownNavSuppressed = saved; + } // Search for existing procedure int32_t procCount = (int32_t)arrlen(sProcTable); @@ -3144,8 +3279,6 @@ static void navigateToEventSub(void) { arrput(sProcBufs, strdup(skeleton)); - updateDropdowns(); - // Show the new procedure (it's the last one) switchToCode(); showProc((int32_t)arrlen(sProcBufs) - 1); @@ -3327,12 +3460,7 @@ static void switchToCode(void) { // ============================================================ static void switchToDesign(void) { - // Save code back to form before switching - if (sDesigner.form && sCurProcIdx >= -1) { - saveCurProc(); - free(sDesigner.form->code); - sDesigner.form->code = strdup(getFullSource()); - } + stashFormCode(); // If already open, just bring to front if (sFormWin) { @@ -3415,6 +3543,7 @@ static void switchToDesign(void) { } } + dvxInvalidateWindow(sAc, sFormWin); setStatus("Design view open."); } @@ -3426,7 +3555,7 @@ static void switchToDesign(void) { static void onTbOpen(WidgetT *w) { (void)w; loadFile(); } static void onTbSave(WidgetT *w) { (void)w; saveFile(); } static void onTbRun(WidgetT *w) { (void)w; compileAndRun(); } -static void onTbStop(WidgetT *w) { (void)w; if (sVm) { sVm->running = false; setStatus("Program stopped."); } } +static void onTbStop(WidgetT *w) { (void)w; sStopRequested = true; if (sVm) { sVm->running = false; } setStatus("Program stopped."); } static void onTbCode(WidgetT *w) { (void)w; switchToCode(); } static void onTbDesign(WidgetT *w) { (void)w; switchToDesign(); } @@ -3683,8 +3812,9 @@ static void freeProcBufs(void) { } arrfree(sProcBufs); - sProcBufs = NULL; - sCurProcIdx = -2; + sProcBufs = NULL; + sCurProcIdx = -2; + sEditorFileIdx = -1; } @@ -3956,19 +4086,42 @@ static char *extractNewProcs(const char *buf) { } -// saveCurProc -- save editor contents back to the current buffer. -// If the user typed a new Sub/Function in the General section, -// it's automatically extracted into its own procedure buffer. +// stashFormCode -- if the proc buffers belong to the designer's form, +// save them back to form->code. Uses sEditorFileIdx to know which +// file the proc buffers actually belong to. -static void saveCurProc(void) { - if (!sEditor) { +static void stashFormCode(void) { + if (!sDesigner.form || sEditorFileIdx < 0) { return; } + if (sEditorFileIdx >= sProject.fileCount || !sProject.files[sEditorFileIdx].isForm) { + return; + } + + if (strcasecmp(sProject.files[sEditorFileIdx].formName, sDesigner.form->name) != 0) { + return; + } + + saveCurProc(); + free(sDesigner.form->code); + sDesigner.form->code = strdup(getFullSource()); +} + + +// saveCurProc -- save editor contents back to the current buffer. +// Returns true if the proc list was modified (skeleton discarded or +// new procs extracted), meaning sProcBufs indices may have shifted. + +static bool saveCurProc(void) { + if (!sEditor) { + return false; + } + const char *edText = wgtGetText(sEditor); if (!edText) { - return; + return false; } if (sCurProcIdx == -1) { @@ -3979,10 +4132,11 @@ static void saveCurProc(void) { sGeneralBuf = cleaned ? cleaned : strdup(edText); if (cleaned) { - // Update editor to show the cleaned General section wgtSetText(sEditor, sGeneralBuf); - updateDropdowns(); + return true; } + + return false; } else if (sCurProcIdx >= 0 && sCurProcIdx < (int32_t)arrlen(sProcBufs)) { // Get the name of the current proc so we can identify its block // regardless of position in the editor. @@ -4052,10 +4206,11 @@ static void saveCurProc(void) { free(sProcBufs[sCurProcIdx]); arrdel(sProcBufs, sCurProcIdx); sCurProcIdx = -2; - updateDropdowns(); + return true; } else { free(sProcBufs[sCurProcIdx]); sProcBufs[sCurProcIdx] = strdup(edText); + return false; } } else { // Multiple proc blocks in the editor. Find the one matching @@ -4150,10 +4305,12 @@ static void saveCurProc(void) { // Update editor to show only this proc wgtSetText(sEditor, sProcBufs[sCurProcIdx]); - updateDropdowns(); + return true; } } } + + return false; } @@ -4164,9 +4321,16 @@ static void showProc(int32_t procIdx) { return; } - // Save whatever is currently in the editor + // Save whatever is currently in the editor. + // If a buffer was deleted (empty skeleton discard), adjust the + // target index since arrdel shifts everything after it. if (sCurProcIdx >= -1) { - saveCurProc(); + int32_t deletedIdx = sCurProcIdx; + bool changed = saveCurProc(); + + if (changed && deletedIdx >= 0 && procIdx > deletedIdx) { + procIdx--; + } } // Suppress onChange while loading -- setting text is not a user edit diff --git a/apps/dvxbasic/ide/ideProject.c b/apps/dvxbasic/ide/ideProject.c index 7ce169c..f84f402 100644 --- a/apps/dvxbasic/ide/ideProject.c +++ b/apps/dvxbasic/ide/ideProject.c @@ -28,6 +28,7 @@ #include "dvxWm.h" #include "widgetBox.h" #include "widgetButton.h" +#include "widgetDropdown.h" #include "widgetImage.h" #include "widgetLabel.h" #include "widgetTextInput.h" @@ -64,6 +65,7 @@ static char **sLabels = NULL; // stb_ds array of strdup'd strings static void onPrjWinClose(WindowT *win); static void onTreeItemDblClick(WidgetT *w); +static bool validateIcon(const char *fullPath, bool showErrors); // ============================================================ // prjInit @@ -296,6 +298,79 @@ int32_t prjAddFile(PrjStateT *prj, const char *relativePath, bool isForm) { } +// ============================================================ +// prjLoadAllFiles -- read all project files into memory buffers +// and extract form names from .frm files. +// ============================================================ + +void prjLoadAllFiles(PrjStateT *prj, AppContextT *ctx) { + for (int32_t i = 0; i < prj->fileCount; i++) { + if (prj->files[i].buffer) { + continue; // already loaded + } + + char fullPath[DVX_MAX_PATH]; + prjFullPath(prj, i, fullPath, sizeof(fullPath)); + + FILE *f = fopen(fullPath, "r"); + + if (!f) { + continue; + } + + fseek(f, 0, SEEK_END); + long size = ftell(f); + fseek(f, 0, SEEK_SET); + + if (size <= 0) { + fclose(f); + continue; + } + + char *buf = (char *)malloc(size + 1); + + if (!buf) { + fclose(f); + continue; + } + + int32_t bytesRead = (int32_t)fread(buf, 1, size, f); + fclose(f); + buf[bytesRead] = '\0'; + + prj->files[i].buffer = buf; + + // Extract form name from .frm files + if (prj->files[i].isForm) { + const char *pos = buf; + + while (*pos) { + while (*pos == ' ' || *pos == '\t') { pos++; } + + if (strncasecmp(pos, "Begin Form ", 11) == 0) { + const char *np = pos + 11; + while (*np == ' ' || *np == '\t') { np++; } + int32_t n = 0; + while (*np && *np != ' ' && *np != '\t' && *np != '\r' && *np != '\n' && n < PRJ_MAX_NAME - 1) { + prj->files[i].formName[n++] = *np++; + } + prj->files[i].formName[n] = '\0'; + break; + } + + while (*pos && *pos != '\n') { pos++; } + if (*pos == '\n') { pos++; } + } + } + + // Yield between files to keep the UI responsive + if (ctx) { + dvxUpdate(ctx); + } + } +} + + // ============================================================ // prjRemoveFile // ============================================================ @@ -460,26 +535,13 @@ void prjRebuildTree(PrjStateT *prj) { projNode->userData = (void *)(intptr_t)-1; wgtTreeItemSetExpanded(projNode, true); - // Forms group + // Forms and Modules groups char *formsLabel = strdup("Forms"); arrput(sLabels, formsLabel); WidgetT *formsNode = wgtTreeItem(projNode, formsLabel); formsNode->userData = (void *)(intptr_t)-1; wgtTreeItemSetExpanded(formsNode, true); - for (int32_t i = 0; i < prj->fileCount; i++) { - if (prj->files[i].isForm) { - char buf[DVX_MAX_PATH + 4]; - snprintf(buf, sizeof(buf), "%s%s", prj->files[i].path, prj->files[i].modified ? " *" : ""); - char *label = strdup(buf); - arrput(sLabels, label); - WidgetT *item = wgtTreeItem(formsNode, label); - item->userData = (void *)(intptr_t)i; - item->onDblClick = onTreeItemDblClick; - } - } - - // Modules group char *modsLabel = strdup("Modules"); arrput(sLabels, modsLabel); WidgetT *modsNode = wgtTreeItem(projNode, modsLabel); @@ -487,15 +549,13 @@ void prjRebuildTree(PrjStateT *prj) { wgtTreeItemSetExpanded(modsNode, true); for (int32_t i = 0; i < prj->fileCount; i++) { - if (!prj->files[i].isForm) { - char buf[DVX_MAX_PATH + 4]; - snprintf(buf, sizeof(buf), "%s%s", prj->files[i].path, prj->files[i].modified ? " *" : ""); - char *label = strdup(buf); - arrput(sLabels, label); - WidgetT *item = wgtTreeItem(modsNode, label); - item->userData = (void *)(intptr_t)i; - item->onDblClick = onTreeItemDblClick; - } + char buf[DVX_MAX_PATH + 4]; + snprintf(buf, sizeof(buf), "%s%s", prj->files[i].path, prj->files[i].modified ? " *" : ""); + char *label = strdup(buf); + arrput(sLabels, label); + WidgetT *item = wgtTreeItem(prj->files[i].isForm ? formsNode : modsNode, label); + item->userData = (void *)(intptr_t)i; + item->onDblClick = onTreeItemDblClick; } wgtInvalidate(sTree); @@ -521,6 +581,8 @@ static struct { WidgetT *version; WidgetT *copyright; WidgetT *description; + WidgetT *startupForm; + const char **formNames; // stb_ds array of form name strings for startup dropdown WidgetT *iconPreview; char iconPath[DVX_MAX_PATH]; const char *appPath; @@ -533,22 +595,10 @@ static void ppdOnOk(WidgetT *w) { // Validate icon path if set if (sPpd.iconPath[0] && sPpd.prj) { - const char *iconText = sPpd.iconPath; char fullPath[DVX_MAX_PATH * 2]; - snprintf(fullPath, sizeof(fullPath), "%s/%s", sPpd.prj->projectDir, iconText); + snprintf(fullPath, sizeof(fullPath), "%s/%s", sPpd.prj->projectDir, sPpd.iconPath); - int32_t infoW = 0; - int32_t infoH = 0; - - if (!dvxImageInfo(fullPath, &infoW, &infoH)) { - dvxMessageBox(sPpd.ctx, "Invalid Icon", "Could not read image file.", MB_OK | MB_ICONERROR); - return; - } - - if (infoW != 32 || infoH != 32) { - char msg[128]; - snprintf(msg, sizeof(msg), "Icon must be 32x32 pixels.\nThis image is %dx%d.", (int)infoW, (int)infoH); - dvxMessageBox(sPpd.ctx, "Invalid Icon", msg, MB_OK | MB_ICONWARNING); + if (!validateIcon(fullPath, true)) { return; } } @@ -560,6 +610,33 @@ static void ppdOnCancel(WidgetT *w) { (void)w; sPpd.accepted = false; sPpd.done static void ppdOnClose(WindowT *win) { (void)win; sPpd.accepted = false; sPpd.done = true; } +// validateIcon -- check that an image file is a valid 32x32 icon. +// Returns true if valid. Shows an error dialog and returns false if not. + +static bool validateIcon(const char *fullPath, bool showErrors) { + int32_t infoW = 0; + int32_t infoH = 0; + + if (!dvxImageInfo(fullPath, &infoW, &infoH)) { + if (showErrors) { + dvxMessageBox(sPpd.ctx, "Invalid Icon", "Could not read image file.", MB_OK | MB_ICONERROR); + } + return false; + } + + if (infoW != 32 || infoH != 32) { + if (showErrors) { + char msg[128]; + snprintf(msg, sizeof(msg), "Icon must be 32x32 pixels.\nThis image is %dx%d.", (int)infoW, (int)infoH); + dvxMessageBox(sPpd.ctx, "Invalid Icon", msg, MB_OK | MB_ICONWARNING); + } + return false; + } + + return true; +} + + static void ppdLoadIconPreview(void) { if (!sPpd.iconPreview || !sPpd.ctx || !sPpd.prj) { return; @@ -574,18 +651,7 @@ static void ppdLoadIconPreview(void) { char fullPath[DVX_MAX_PATH * 2]; snprintf(fullPath, sizeof(fullPath), "%s/%s", sPpd.prj->projectDir, relPath); - // Verify the image is 32x32 before loading - int32_t infoW = 0; - int32_t infoH = 0; - - if (!dvxImageInfo(fullPath, &infoW, &infoH)) { - return; - } - - if (infoW != 32 || infoH != 32) { - char msg[128]; - snprintf(msg, sizeof(msg), "Icon must be 32x32 pixels.\nThis image is %dx%d.", (int)infoW, (int)infoH); - dvxMessageBox(sPpd.ctx, "Invalid Icon", msg, MB_OK | MB_ICONWARNING); + if (!validateIcon(fullPath, true)) { sPpd.iconPath[0] = '\0'; return; } @@ -612,19 +678,7 @@ static void ppdOnBrowseIcon(WidgetT *w) { char path[DVX_MAX_PATH]; if (dvxFileDialog(sPpd.ctx, "Select Icon", FD_OPEN, NULL, filters, 2, path, sizeof(path))) { - // Validate size using the full path before accepting - int32_t infoW = 0; - int32_t infoH = 0; - - if (!dvxImageInfo(path, &infoW, &infoH)) { - dvxMessageBox(sPpd.ctx, "Invalid Icon", "Could not read image file.", MB_OK | MB_ICONERROR); - return; - } - - if (infoW != 32 || infoH != 32) { - char msg[128]; - snprintf(msg, sizeof(msg), "Icon must be 32x32 pixels.\nThis image is %dx%d.", (int)infoW, (int)infoH); - dvxMessageBox(sPpd.ctx, "Invalid Icon", msg, MB_OK | MB_ICONWARNING); + if (!validateIcon(path, true)) { return; } @@ -761,6 +815,53 @@ bool prjPropertiesDialog(AppContextT *ctx, PrjStateT *prj, const char *appPath) sPpd.company = ppdAddRow(root, "Company:", prj->company, PRJ_MAX_STRING); sPpd.version = ppdAddRow(root, "Version:", prj->version, PRJ_MAX_NAME); sPpd.copyright = ppdAddRow(root, "Copyright:", prj->copyright, PRJ_MAX_STRING); + + // Startup form dropdown + { + WidgetT *sfRow = wgtHBox(root); + sfRow->spacing = wgtPixels(4); + + WidgetT *sfLbl = wgtLabel(sfRow, "Startup Form:"); + sfLbl->minW = wgtPixels(PPD_LABEL_W); + + sPpd.startupForm = wgtDropdown(sfRow); + sPpd.startupForm->weight = 100; + + // Populate with form names from the project + sPpd.formNames = NULL; + int32_t selectedIdx = 0; + + for (int32_t i = 0; i < prj->fileCount; i++) { + if (!prj->files[i].isForm) { + continue; + } + + // Use the cached form object name, fall back to filename + const char *name = prj->files[i].formName; + char fallback[PRJ_MAX_NAME]; + + if (!name[0]) { + snprintf(fallback, sizeof(fallback), "%s", prj->files[i].path); + char *dot = strrchr(fallback, '.'); + if (dot) { *dot = '\0'; } + name = fallback; + } + + arrput(sPpd.formNames, strdup(name)); + + if (strcasecmp(name, prj->startupForm) == 0) { + selectedIdx = (int32_t)arrlen(sPpd.formNames) - 1; + } + } + + int32_t formCount = (int32_t)arrlen(sPpd.formNames); + wgtDropdownSetItems(sPpd.startupForm, sPpd.formNames, formCount); + + if (formCount > 0) { + wgtDropdownSetSelected(sPpd.startupForm, selectedIdx); + } + } + // Icon row: label + preview + Browse button { WidgetT *iconRow = wgtHBox(root); @@ -847,9 +948,25 @@ bool prjPropertiesDialog(AppContextT *ctx, PrjStateT *prj, const char *appPath) snprintf(prj->iconPath, sizeof(prj->iconPath), "%s", sPpd.iconPath); + // Read startup form from dropdown + if (sPpd.startupForm && sPpd.formNames) { + int32_t sfIdx = wgtDropdownGetSelected(sPpd.startupForm); + + if (sfIdx >= 0 && sfIdx < (int32_t)arrlen(sPpd.formNames)) { + snprintf(prj->startupForm, sizeof(prj->startupForm), "%s", sPpd.formNames[sfIdx]); + } + } + prj->dirty = true; } + // Free form name strings + for (int32_t i = 0; i < (int32_t)arrlen(sPpd.formNames); i++) { + free((char *)sPpd.formNames[i]); + } + arrfree(sPpd.formNames); + sPpd.formNames = NULL; + ctx->modalWindow = prevModal; dvxDestroyWindow(ctx, win); diff --git a/apps/dvxbasic/ide/ideProject.h b/apps/dvxbasic/ide/ideProject.h index f64fd25..b179004 100644 --- a/apps/dvxbasic/ide/ideProject.h +++ b/apps/dvxbasic/ide/ideProject.h @@ -26,6 +26,7 @@ typedef struct { bool isForm; // true = .frm, false = .bas char *buffer; // in-memory edit buffer (malloc'd, NULL = not loaded) bool modified; // true = buffer has unsaved changes + char formName[PRJ_MAX_NAME]; // form object name (from "Begin Form ") } PrjFileT; // ============================================================ @@ -73,6 +74,7 @@ bool prjSaveAs(PrjStateT *prj, const char *dbpPath); void prjNew(PrjStateT *prj, const char *name, const char *directory); void prjClose(PrjStateT *prj); int32_t prjAddFile(PrjStateT *prj, const char *relativePath, bool isForm); +void prjLoadAllFiles(PrjStateT *prj, AppContextT *ctx); void prjRemoveFile(PrjStateT *prj, int32_t idx); void prjFullPath(const PrjStateT *prj, int32_t fileIdx, char *outPath, int32_t outSize); diff --git a/apps/dvxbasic/ide/ideProperties.c b/apps/dvxbasic/ide/ideProperties.c index 70d45be..26ce5da 100644 --- a/apps/dvxbasic/ide/ideProperties.c +++ b/apps/dvxbasic/ide/ideProperties.c @@ -102,6 +102,7 @@ static void onPrpClose(WindowT *win) { #define PROP_TYPE_STRING WGT_IFACE_STRING #define PROP_TYPE_INT WGT_IFACE_INT #define PROP_TYPE_BOOL WGT_IFACE_BOOL +#define PROP_TYPE_ENUM WGT_IFACE_ENUM #define PROP_TYPE_READONLY 255 static uint8_t getPropType(const char *propName, const char *typeName) { @@ -145,6 +146,33 @@ static uint8_t getPropType(const char *propName, const char *typeName) { } +static const WgtPropDescT *findIfaceProp(const char *typeName, const char *propName) { + if (!typeName || !typeName[0]) { + return NULL; + } + + const char *wgtName = wgtFindByBasName(typeName); + + if (!wgtName) { + return NULL; + } + + const WgtIfaceT *iface = wgtGetIface(wgtName); + + if (!iface) { + return NULL; + } + + for (int32_t i = 0; i < iface->propCount; i++) { + if (strcasecmp(iface->props[i].name, propName) == 0) { + return &iface->props[i]; + } + } + + return NULL; +} + + // ============================================================ // cascadeToChildren // ============================================================ @@ -260,6 +288,31 @@ static void onPropDblClick(WidgetT *w) { // Toggle boolean on double-click -- no input box bool cur = (strcasecmp(curValue, "True") == 0); snprintf(newValue, sizeof(newValue), "%s", cur ? "False" : "True"); + } else if (propType == PROP_TYPE_ENUM) { + // Enum: cycle to next value on double-click + const WgtPropDescT *pd = findIfaceProp(ctrlTypeName, propName); + + if (!pd || !pd->enumNames) { + return; + } + + // Find current value and advance to next + int32_t enumCount = 0; + int32_t curIdx = 0; + + while (pd->enumNames[enumCount]) { + if (strcasecmp(pd->enumNames[enumCount], curValue) == 0) { + curIdx = enumCount; + } + enumCount++; + } + + if (enumCount == 0) { + return; + } + + int32_t nextIdx = (curIdx + 1) % enumCount; + snprintf(newValue, sizeof(newValue), "%s", pd->enumNames[nextIdx]); } else if (propType == PROP_TYPE_INT) { // Spinner dialog for integers char prompt[128]; @@ -387,6 +440,17 @@ static void onPropDblClick(WidgetT *w) { ((void (*)(WidgetT *, const char *))p->setFn)(ctrl->widget, ctrl->props[ctrl->propCount].value); ctrl->propCount++; } + } else if (p->type == WGT_IFACE_ENUM && p->enumNames) { + int32_t enumVal = 0; + + for (int32_t en = 0; p->enumNames[en]; en++) { + if (strcasecmp(p->enumNames[en], newValue) == 0) { + enumVal = en; + break; + } + } + + ((void (*)(WidgetT *, int32_t))p->setFn)(ctrl->widget, enumVal); } else if (p->type == WGT_IFACE_INT) { ((void (*)(WidgetT *, int32_t))p->setFn)(ctrl->widget, atoi(newValue)); } else if (p->type == WGT_IFACE_BOOL) { @@ -885,6 +949,18 @@ void prpRefresh(DsgnStateT *ds) { if (p->type == WGT_IFACE_STRING && p->getFn) { const char *s = ((const char *(*)(WidgetT *))p->getFn)(ctrl->widget); addPropRow(p->name, s ? s : ""); + } else if (p->type == WGT_IFACE_ENUM && p->getFn && p->enumNames) { + int32_t v = ((int32_t (*)(const WidgetT *))p->getFn)(ctrl->widget); + const char *name = NULL; + + for (int32_t k = 0; p->enumNames[k]; k++) { + if (k == v) { + name = p->enumNames[k]; + break; + } + } + + addPropRow(p->name, name ? name : "?"); } else if (p->type == WGT_IFACE_INT && p->getFn) { int32_t v = ((int32_t (*)(const WidgetT *))p->getFn)(ctrl->widget); snprintf(buf, sizeof(buf), "%d", (int)v); diff --git a/apps/dvxbasic/runtime/vm.c b/apps/dvxbasic/runtime/vm.c index fbe124f..b9d014c 100644 --- a/apps/dvxbasic/runtime/vm.c +++ b/apps/dvxbasic/runtime/vm.c @@ -2534,6 +2534,11 @@ BasVmResultE basVmStep(BasVmT *vm) { // Halt // ============================================================ + case OP_END: + vm->running = false; + vm->ended = true; + return BAS_VM_HALTED; + case OP_HALT: vm->running = false; return BAS_VM_HALTED; @@ -2974,16 +2979,17 @@ static BasVmResultE execArith(BasVmT *vm, uint8_t op) { BasStringT *sa = a.strVal ? a.strVal : basStringNew("", 0); BasStringT *sb = b.strVal ? b.strVal : basStringNew("", 0); int32_t newLen = sa->len + sb->len; - BasStringT *cat = basStringNew("", 0); + BasStringT *cat; if (newLen > 0) { - basStringUnref(cat); char *buf = (char *)malloc(newLen + 1); memcpy(buf, sa->data, sa->len); memcpy(buf + sa->len, sb->data, sb->len); buf[newLen] = '\0'; cat = basStringNew(buf, newLen); free(buf); + } else { + cat = basStringNew("", 0); } basValRelease(&a); diff --git a/apps/dvxbasic/runtime/vm.h b/apps/dvxbasic/runtime/vm.h index 2f5ca5d..40ad424 100644 --- a/apps/dvxbasic/runtime/vm.h +++ b/apps/dvxbasic/runtime/vm.h @@ -248,6 +248,7 @@ typedef struct { // Execution int32_t pc; // program counter bool running; + bool ended; // END statement executed -- program should terminate bool yielded; int32_t stepLimit; // max steps per basVmRun (0 = unlimited) int32_t stepCount; // steps executed in last basVmRun diff --git a/apps/progman/progman.c b/apps/progman/progman.c index cb6fb7f..061e6a9 100644 --- a/apps/progman/progman.c +++ b/apps/progman/progman.c @@ -112,7 +112,7 @@ static int32_t sAppCount = 0; int32_t appMain(DxeAppContextT *ctx); static void buildPmWindow(void); static void desktopUpdate(void); -static void onAppButtonClick(WidgetT *w); +static void onAppButtonDblClick(WidgetT *w); static void onPmClose(WindowT *win); static void onPmMenu(WindowT *win, int32_t menuId); static void scanAppsDir(void); @@ -233,8 +233,7 @@ static void buildPmWindow(void) { } btn->userData = &sAppFiles[i]; - btn->onDblClick = onAppButtonClick; - btn->onClick = onAppButtonClick; + btn->onDblClick = onAppButtonDblClick; if (sAppFiles[i].tooltip[0]) { wgtSetTooltip(btn, sAppFiles[i].tooltip); @@ -277,7 +276,7 @@ static void desktopUpdate(void) { // Widget click handler for app grid buttons. userData was set to the // AppEntryT pointer during window construction, giving us the .app path. -static void onAppButtonClick(WidgetT *w) { +static void onAppButtonDblClick(WidgetT *w) { AppEntryT *entry = (AppEntryT *)w->userData; if (!entry) { diff --git a/core/dvxApp.c b/core/dvxApp.c index 4afbd49..050f6d7 100644 --- a/core/dvxApp.c +++ b/core/dvxApp.c @@ -1546,9 +1546,9 @@ static void drawPopupLevel(AppContextT *ctx, DisplayT *d, const BlitOpsT *ops, c } uint32_t bg = ctx->colors.menuBg; - uint32_t fg = ctx->colors.menuFg; + uint32_t fg = item->enabled ? ctx->colors.menuFg : ctx->colors.windowShadow; - if (k == hoverItem) { + if (k == hoverItem && item->enabled) { bg = ctx->colors.menuHighlightBg; fg = ctx->colors.menuHighlightFg; } @@ -1845,7 +1845,13 @@ static void handleMouseButton(AppContextT *ctx, int32_t mx, int32_t my, int32_t } else { ctx->lastTitleClickTime = now; ctx->lastTitleClickId = win->id; - wmDragBegin(&ctx->stack, hitIdx, mx, my); + + // Don't start a drag on a maximized window -- + // dragging clears the maximized flag, which + // prevents the double-click restore from working. + if (!win->maximized) { + wmDragBegin(&ctx->stack, hitIdx, mx, my); + } } } break; @@ -3942,6 +3948,21 @@ void dvxDestroyWindow(AppContextT *ctx, WindowT *win) { } +// ============================================================ +// dvxRaiseWindow +// ============================================================ + +void dvxRaiseWindow(AppContextT *ctx, WindowT *win) { + for (int32_t i = 0; i < ctx->stack.count; i++) { + if (ctx->stack.windows[i] == win) { + wmRaiseWindow(&ctx->stack, &ctx->dirty, i); + wmSetFocus(&ctx->stack, &ctx->dirty, ctx->stack.count - 1); + return; + } + } +} + + // ============================================================ // dvxFitWindow // ============================================================ diff --git a/core/dvxApp.h b/core/dvxApp.h index c761fc9..3b10f31 100644 --- a/core/dvxApp.h +++ b/core/dvxApp.h @@ -203,6 +203,9 @@ WindowT *dvxCreateWindowCentered(AppContextT *ctx, const char *title, int32_t w, // Destroy a window, free all its resources, and dirty its former region. void dvxDestroyWindow(AppContextT *ctx, WindowT *win); +// Raise a window to the top of the z-order and give it focus. +void dvxRaiseWindow(AppContextT *ctx, WindowT *win); + // Resize a window to exactly fit its widget tree's computed minimum size // (plus chrome). Used for dialog boxes and other fixed-layout windows // where the window should shrink-wrap its content. diff --git a/core/dvxWidget.h b/core/dvxWidget.h index 4e69ddb..8158572 100644 --- a/core/dvxWidget.h +++ b/core/dvxWidget.h @@ -552,6 +552,7 @@ const void *wgtGetApi(const char *name); #define WGT_IFACE_INT 1 #define WGT_IFACE_BOOL 2 #define WGT_IFACE_FLOAT 3 +#define WGT_IFACE_ENUM 4 // int32_t with named values // Method calling conventions (how the form runtime marshals args) #define WGT_SIG_VOID 0 // void fn(WidgetT *) @@ -566,10 +567,11 @@ const void *wgtGetApi(const char *name); // Property descriptor typedef struct { - const char *name; // BASIC property name (e.g. "Caption", "Value") - uint8_t type; // WGT_IFACE_* - void *getFn; // getter function pointer (NULL if write-only) - void *setFn; // setter function pointer (NULL if read-only) + const char *name; // BASIC property name (e.g. "Caption", "Value") + uint8_t type; // WGT_IFACE_* + void *getFn; // getter function pointer (NULL if write-only) + void *setFn; // setter function pointer (NULL if read-only) + const char **enumNames; // WGT_IFACE_ENUM only: NULL-terminated array of value names } WgtPropDescT; // Method descriptor diff --git a/listhelp/listHelp.c b/listhelp/listHelp.c index ad70599..475c3dd 100644 --- a/listhelp/listHelp.c +++ b/listhelp/listHelp.c @@ -143,6 +143,30 @@ void widgetPaintPopupList(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *f drawText(d, ops, font, textX, iy, items[idx], ifg, ibg, false); } + + // Draw scroll indicators if the list extends beyond visible area + if (itemCount > visibleItems) { + int32_t cx = popX + popW / 2; + uint32_t arrowC = colors->menuHighlightBg; + + // Up triangle (point at top, wide at bottom) + if (scrollPos > 0) { + int32_t ty = popY + 2; + + for (int32_t i = 0; i < 3; i++) { + drawHLine(d, ops, cx - i, ty + i, 1 + i * 2, arrowC); + } + } + + // Down triangle (wide at top, point at bottom) + if (scrollPos + visibleItems < itemCount) { + int32_t by = popY + popH - 4; + + for (int32_t i = 0; i < 3; i++) { + drawHLine(d, ops, cx - i, by - i, 1 + i * 2, arrowC); + } + } + } } diff --git a/widgets/dropdown/widgetDropdown.c b/widgets/dropdown/widgetDropdown.c index 2cef5be..6f994aa 100644 --- a/widgets/dropdown/widgetDropdown.c +++ b/widgets/dropdown/widgetDropdown.c @@ -122,7 +122,7 @@ void widgetDropdownOnKey(WidgetT *w, int32_t key, int32_t mod) { } } else { // Popup is closed - if (key == (0x50 | 0x100) || key == ' ' || key == 0x0D) { + if (key == ' ' || key == 0x0D) { d->open = true; d->hoverIdx = d->selectedIdx; sOpenPopup = w; @@ -134,6 +134,15 @@ void widgetDropdownOnKey(WidgetT *w, int32_t key, int32_t mod) { if (d->hoverIdx < d->scrollPos) { d->scrollPos = d->hoverIdx; } + } else if (key == (0x50 | 0x100)) { + // Down arrow: cycle selection forward (wheel-friendly) + if (d->selectedIdx < d->itemCount - 1) { + d->selectedIdx++; + + if (w->onChange) { + w->onChange(w); + } + } } else if (key == (0x48 | 0x100)) { if (d->selectedIdx > 0) { d->selectedIdx--; diff --git a/widgets/label/widgetLabel.c b/widgets/label/widgetLabel.c index 4a1a778..329bbea 100644 --- a/widgets/label/widgetLabel.c +++ b/widgets/label/widgetLabel.c @@ -147,6 +147,16 @@ void wgtLabelSetAlign(WidgetT *w, WidgetAlignE align) { } +int32_t wgtLabelGetAlign(const WidgetT *w) { + if (w && w->type == sTypeId) { + LabelDataT *d = (LabelDataT *)w->data; + return (int32_t)d->textAlign; + } + + return 0; +} + + // ============================================================ // DXE registration // ============================================================ @@ -160,8 +170,10 @@ static const struct { .setAlign = wgtLabelSetAlign }; +static const char *sAlignNames[] = { "Left", "Center", "Right", NULL }; + static const WgtPropDescT sProps[] = { - { "Alignment", WGT_IFACE_INT, NULL, (void *)wgtLabelSetAlign } + { "Alignment", WGT_IFACE_ENUM, (void *)wgtLabelGetAlign, (void *)wgtLabelSetAlign, sAlignNames } }; static const WgtIfaceT sIface = {