From 82939c3f279390c2b7827c2d6a50241fc30a52df Mon Sep 17 00:00:00 2001 From: Scott Duensing Date: Mon, 30 Mar 2026 22:19:38 -0500 Subject: [PATCH] Adding project support and breaking a lot of stuff. --- apps/cpanel/cpanel.c | 44 +- apps/dvxbasic/Makefile | 16 +- apps/dvxbasic/compiler/codegen.c | 66 +- apps/dvxbasic/compiler/codegen.h | 9 +- apps/dvxbasic/compiler/parser.c | 61 +- apps/dvxbasic/compiler/symtab.c | 21 +- apps/dvxbasic/compiler/symtab.h | 14 +- apps/dvxbasic/dvxbasic.res | 2 + apps/dvxbasic/formrt/formrt.c | 53 +- apps/dvxbasic/formrt/formrt.h | 6 +- apps/dvxbasic/ide/ideDesigner.c | 2 +- apps/dvxbasic/ide/ideDesigner.h | 10 + apps/dvxbasic/ide/ideMain.c | 1328 ++++++++++++++++++++++++++--- apps/dvxbasic/ide/ideProject.c | 853 ++++++++++++++++++ apps/dvxbasic/ide/ideProject.h | 105 +++ apps/dvxbasic/ide/ideProperties.c | 21 +- apps/dvxbasic/noicon.bmp | 3 + apps/dvxbasic/samples/ICON32.BMP | 3 + apps/dvxbasic/samples/MULTI.DBP | 18 + apps/dvxbasic/samples/MULTI1.FRM | 10 + apps/notepad/notepad.c | 24 +- apps/progman/progman.c | 9 +- config/dvx.ini | 4 +- core/dvxApp.c | 43 + core/dvxApp.h | 14 + core/dvxDialog.c | 20 + core/dvxDialog.h | 16 + core/dvxPrefs.c | 186 ++-- core/dvxPrefs.h | 59 +- core/thirdparty/stb_ds_impl.c | 3 + shell/shellMain.c | 26 +- tools/Makefile | 5 +- tools/mkicon.c | 36 +- 33 files changed, 2699 insertions(+), 391 deletions(-) create mode 100644 apps/dvxbasic/ide/ideProject.c create mode 100644 apps/dvxbasic/ide/ideProject.h create mode 100644 apps/dvxbasic/noicon.bmp create mode 100644 apps/dvxbasic/samples/ICON32.BMP create mode 100644 apps/dvxbasic/samples/MULTI.DBP create mode 100644 apps/dvxbasic/samples/MULTI1.FRM create mode 100644 core/thirdparty/stb_ds_impl.c diff --git a/apps/cpanel/cpanel.c b/apps/cpanel/cpanel.c index b1ae03b..a405970 100644 --- a/apps/cpanel/cpanel.c +++ b/apps/cpanel/cpanel.c @@ -84,9 +84,10 @@ typedef struct { // Module state // ============================================================ -static DxeAppContextT *sCtx = NULL; -static AppContextT *sAc = NULL; -static WindowT *sWin = NULL; +static DxeAppContextT *sCtx = NULL; +static AppContextT *sAc = NULL; +static WindowT *sWin = NULL; +static PrefsHandleT *sPrefs = NULL; // Saved state for Cancel static uint8_t sSavedColorRgb[ColorCountE][3]; @@ -338,7 +339,7 @@ static void buildMouseTab(WidgetT *page) { sDblClickSldr->weight = 100; sDblClickSldr->onChange = onDblClickSlider; - int32_t dblMs = prefsGetInt("mouse", "doubleclick", 500); + int32_t dblMs = prefsGetInt(sPrefs, "mouse", "doubleclick", 500); wgtSliderSetValue(sDblClickSldr, dblMs); wgtLabel(dblRow, "Slow "); sDblClickLbl = wgtLabel(dblRow, ""); @@ -358,7 +359,7 @@ static void buildMouseTab(WidgetT *page) { sAccelDrop->onChange = onAccelChange; wgtDropdownSetItems(sAccelDrop, accelItems, 4); - const char *accelStr = prefsGetString("mouse", "acceleration", "medium"); + const char *accelStr = prefsGetString(sPrefs, "mouse", "acceleration", "medium"); if (strcmp(accelStr, "off") == 0) { wgtDropdownSetSelected(sAccelDrop, 0); @@ -804,9 +805,9 @@ static void onOk(WidgetT *w) { // Save mouse settings int32_t wheelSel = wgtDropdownGetSelected(sWheelDrop); - prefsSetString("mouse", "wheel", wheelSel == 1 ? "reversed" : "normal"); - prefsSetInt("mouse", "doubleclick", wgtSliderGetValue(sDblClickSldr)); - prefsSetString("mouse", "acceleration", mapAccelValue(wgtDropdownGetSelected(sAccelDrop))); + prefsSetString(sPrefs, "mouse", "wheel", wheelSel == 1 ? "reversed" : "normal"); + prefsSetInt(sPrefs, "mouse", "doubleclick", wgtSliderGetValue(sDblClickSldr)); + prefsSetString(sPrefs, "mouse", "acceleration", mapAccelValue(wgtDropdownGetSelected(sAccelDrop))); // Save colors to INI for (int32_t i = 0; i < ColorCountE; i++) { @@ -817,14 +818,14 @@ static void onOk(WidgetT *w) { char val[16]; snprintf(val, sizeof(val), "%d,%d,%d", r, g, b); - prefsSetString("colors", dvxColorName((ColorIdE)i), val); + prefsSetString(sPrefs, "colors", dvxColorName((ColorIdE)i), val); } // Save desktop settings if (sWallpaperPath[0]) { - prefsSetString("desktop", "wallpaper", sWallpaperPath); + prefsSetString(sPrefs, "desktop", "wallpaper", sWallpaperPath); } else { - prefsRemove("desktop", "wallpaper"); + prefsRemove(sPrefs, "desktop", "wallpaper"); } const char *modeStr = "stretch"; @@ -835,14 +836,16 @@ static void onOk(WidgetT *w) { modeStr = "center"; } - prefsSetString("desktop", "mode", modeStr); + prefsSetString(sPrefs, "desktop", "mode", modeStr); // Save video settings - prefsSetInt("video", "width", sAc->display.width); - prefsSetInt("video", "height", sAc->display.height); - prefsSetInt("video", "bpp", sAc->display.format.bitsPerPixel); + prefsSetInt(sPrefs, "video", "width", sAc->display.width); + prefsSetInt(sPrefs, "video", "height", sAc->display.height); + prefsSetInt(sPrefs, "video", "bpp", sAc->display.format.bitsPerPixel); - prefsSave(); + prefsSave(sPrefs); + prefsClose(sPrefs); + sPrefs = NULL; dvxDestroyWindow(sAc, sWin); sWin = NULL; } @@ -851,6 +854,8 @@ static void onOk(WidgetT *w) { static void onCancel(WidgetT *w) { (void)w; restoreSnapshot(); + prefsClose(sPrefs); + sPrefs = NULL; dvxDestroyWindow(sAc, sWin); sWin = NULL; } @@ -858,6 +863,8 @@ static void onCancel(WidgetT *w) { static void onClose(WindowT *win) { restoreSnapshot(); + prefsClose(sPrefs); + sPrefs = NULL; dvxDestroyWindow(sAc, win); sWin = NULL; } @@ -1083,8 +1090,9 @@ static void updateSwatch(void) { // ============================================================ int32_t appMain(DxeAppContextT *ctx) { - sCtx = ctx; - sAc = ctx->shellCtx; + sCtx = ctx; + sAc = ctx->shellCtx; + sPrefs = prefsLoad("CONFIG/DVX.INI"); int32_t winX = (sAc->display.width - CP_WIN_W) / 2; int32_t winY = (sAc->display.height - CP_WIN_H) / 2; diff --git a/apps/dvxbasic/Makefile b/apps/dvxbasic/Makefile index 6bb6546..98b1374 100644 --- a/apps/dvxbasic/Makefile +++ b/apps/dvxbasic/Makefile @@ -31,13 +31,13 @@ COMP_OBJS = $(OBJDIR)/lexer.o $(OBJDIR)/parser.o $(OBJDIR)/codegen.o $(OBJDIR)/s FORMRT_OBJS = $(OBJDIR)/formrt.o # IDE app objects -IDE_OBJS = $(OBJDIR)/ideMain.o $(OBJDIR)/ideDesigner.o $(OBJDIR)/ideToolbox.o $(OBJDIR)/ideProperties.o +IDE_OBJS = $(OBJDIR)/ideMain.o $(OBJDIR)/ideDesigner.o $(OBJDIR)/ideProject.o $(OBJDIR)/ideToolbox.o $(OBJDIR)/ideProperties.o APP_OBJS = $(IDE_OBJS) $(FORMRT_OBJS) APP_TARGET = $(APPDIR)/dvxbasic.app # Native test programs (host gcc, not cross-compiled) HOSTCC = gcc -HOSTCFLAGS = -O2 -Wall -Wextra -Wno-type-limits -Wno-sign-compare -I. +HOSTCFLAGS = -O2 -Wall -Wextra -Wno-type-limits -Wno-sign-compare -I. -I../../core BINDIR = ../bin TEST_COMPILER = $(BINDIR)/test_compiler @@ -45,10 +45,11 @@ TEST_VM = $(BINDIR)/test_vm TEST_LEX = $(BINDIR)/test_lex TEST_QUICK = $(BINDIR)/test_quick -TEST_COMPILER_SRCS = test_compiler.c compiler/lexer.c compiler/parser.c compiler/codegen.c compiler/symtab.c runtime/vm.c runtime/values.c -TEST_VM_SRCS = test_vm.c runtime/vm.c runtime/values.c +STB_DS_IMPL = ../../core/thirdparty/stb_ds_impl.c +TEST_COMPILER_SRCS = test_compiler.c compiler/lexer.c compiler/parser.c compiler/codegen.c compiler/symtab.c runtime/vm.c runtime/values.c $(STB_DS_IMPL) +TEST_VM_SRCS = test_vm.c runtime/vm.c runtime/values.c $(STB_DS_IMPL) TEST_LEX_SRCS = test_lex.c compiler/lexer.c -TEST_QUICK_SRCS = test_quick.c compiler/lexer.c compiler/parser.c compiler/codegen.c compiler/symtab.c runtime/vm.c runtime/values.c +TEST_QUICK_SRCS = test_quick.c compiler/lexer.c compiler/parser.c compiler/codegen.c compiler/symtab.c runtime/vm.c runtime/values.c $(STB_DS_IMPL) .PHONY: all clean tests @@ -96,7 +97,10 @@ $(OBJDIR)/formrt.o: formrt/formrt.c formrt/formrt.h compiler/codegen.h runtime/v $(OBJDIR)/ideDesigner.o: ide/ideDesigner.c ide/ideDesigner.h | $(OBJDIR) $(CC) $(CFLAGS) -c -o $@ $< -$(OBJDIR)/ideMain.o: ide/ideMain.c ide/ideDesigner.h ide/ideToolbox.h ide/ideProperties.h compiler/parser.h runtime/vm.h | $(OBJDIR) +$(OBJDIR)/ideMain.o: ide/ideMain.c ide/ideDesigner.h ide/ideProject.h ide/ideToolbox.h ide/ideProperties.h compiler/parser.h runtime/vm.h | $(OBJDIR) + $(CC) $(CFLAGS) -c -o $@ $< + +$(OBJDIR)/ideProject.o: ide/ideProject.c ide/ideProject.h | $(OBJDIR) $(CC) $(CFLAGS) -c -o $@ $< $(OBJDIR)/ideProperties.o: ide/ideProperties.c ide/ideProperties.h ide/ideDesigner.h | $(OBJDIR) diff --git a/apps/dvxbasic/compiler/codegen.c b/apps/dvxbasic/compiler/codegen.c index 4d98f80..c804621 100644 --- a/apps/dvxbasic/compiler/codegen.c +++ b/apps/dvxbasic/compiler/codegen.c @@ -3,6 +3,7 @@ #include "codegen.h" #include "symtab.h" #include "opcodes.h" +#include "thirdparty/stb_ds_wrap.h" #include #include @@ -13,11 +14,9 @@ // ============================================================ bool basAddData(BasCodeGenT *cg, BasValueT val) { - if (cg->dataCount >= BAS_MAX_CONSTANTS) { - return false; - } - - cg->dataPool[cg->dataCount++] = basValCopy(val); + BasValueT copy = basValCopy(val); + arrput(cg->dataPool, copy); + cg->dataCount = (int32_t)arrlen(cg->dataPool); return true; } @@ -34,12 +33,10 @@ uint16_t basAddConstant(BasCodeGenT *cg, const char *text, int32_t len) { } } - if (cg->constCount >= BAS_MAX_CONSTANTS) { - return 0; - } - uint16_t idx = (uint16_t)cg->constCount; - cg->constants[cg->constCount++] = basStringNew(text, len); + BasStringT *s = basStringNew(text, len); + arrput(cg->constants, s); + cg->constCount = (int32_t)arrlen(cg->constants); return idx; } @@ -175,6 +172,12 @@ void basCodeGenFree(BasCodeGenT *cg) { basValRelease(&cg->dataPool[i]); } + arrfree(cg->code); + arrfree(cg->constants); + arrfree(cg->dataPool); + cg->code = NULL; + cg->constants = NULL; + cg->dataPool = NULL; cg->constCount = 0; cg->dataCount = 0; cg->codeLen = 0; @@ -204,9 +207,8 @@ int32_t basCodePos(const BasCodeGenT *cg) { // ============================================================ void basEmit8(BasCodeGenT *cg, uint8_t b) { - if (cg->codeLen < BAS_MAX_CODE) { - cg->code[cg->codeLen++] = b; - } + arrput(cg->code, b); + cg->codeLen = (int32_t)arrlen(cg->code); } @@ -215,10 +217,11 @@ void basEmit8(BasCodeGenT *cg, uint8_t b) { // ============================================================ void basEmit16(BasCodeGenT *cg, int16_t v) { - if (cg->codeLen + 2 <= BAS_MAX_CODE) { - memcpy(&cg->code[cg->codeLen], &v, 2); - cg->codeLen += 2; - } + uint8_t buf[2]; + memcpy(buf, &v, 2); + arrput(cg->code, buf[0]); + arrput(cg->code, buf[1]); + cg->codeLen = (int32_t)arrlen(cg->code); } @@ -227,10 +230,14 @@ void basEmit16(BasCodeGenT *cg, int16_t v) { // ============================================================ void basEmitDouble(BasCodeGenT *cg, double v) { - if (cg->codeLen + (int32_t)sizeof(double) <= BAS_MAX_CODE) { - memcpy(&cg->code[cg->codeLen], &v, sizeof(double)); - cg->codeLen += (int32_t)sizeof(double); + uint8_t buf[sizeof(double)]; + memcpy(buf, &v, sizeof(double)); + + for (int32_t i = 0; i < (int32_t)sizeof(double); i++) { + arrput(cg->code, buf[i]); } + + cg->codeLen = (int32_t)arrlen(cg->code); } @@ -239,10 +246,14 @@ void basEmitDouble(BasCodeGenT *cg, double v) { // ============================================================ void basEmitFloat(BasCodeGenT *cg, float v) { - if (cg->codeLen + (int32_t)sizeof(float) <= BAS_MAX_CODE) { - memcpy(&cg->code[cg->codeLen], &v, sizeof(float)); - cg->codeLen += (int32_t)sizeof(float); + uint8_t buf[sizeof(float)]; + memcpy(buf, &v, sizeof(float)); + + for (int32_t i = 0; i < (int32_t)sizeof(float); i++) { + arrput(cg->code, buf[i]); } + + cg->codeLen = (int32_t)arrlen(cg->code); } @@ -251,10 +262,11 @@ void basEmitFloat(BasCodeGenT *cg, float v) { // ============================================================ void basEmitU16(BasCodeGenT *cg, uint16_t v) { - if (cg->codeLen + 2 <= BAS_MAX_CODE) { - memcpy(&cg->code[cg->codeLen], &v, 2); - cg->codeLen += 2; - } + uint8_t buf[2]; + memcpy(buf, &v, 2); + arrput(cg->code, buf[0]); + arrput(cg->code, buf[1]); + cg->codeLen = (int32_t)arrlen(cg->code); } diff --git a/apps/dvxbasic/compiler/codegen.h b/apps/dvxbasic/compiler/codegen.h index e077cef..6efc575 100644 --- a/apps/dvxbasic/compiler/codegen.h +++ b/apps/dvxbasic/compiler/codegen.h @@ -19,16 +19,13 @@ // Code generator state // ============================================================ -#define BAS_MAX_CODE 65536 -#define BAS_MAX_CONSTANTS 1024 - typedef struct { - uint8_t code[BAS_MAX_CODE]; + uint8_t *code; // stb_ds dynamic array int32_t codeLen; - BasStringT *constants[BAS_MAX_CONSTANTS]; + BasStringT **constants; // stb_ds dynamic array int32_t constCount; int32_t globalCount; - BasValueT dataPool[BAS_MAX_CONSTANTS]; + BasValueT *dataPool; // stb_ds dynamic array int32_t dataCount; } BasCodeGenT; diff --git a/apps/dvxbasic/compiler/parser.c b/apps/dvxbasic/compiler/parser.c index 78e9ffa..6e758fc 100644 --- a/apps/dvxbasic/compiler/parser.c +++ b/apps/dvxbasic/compiler/parser.c @@ -7,6 +7,7 @@ #include "parser.h" #include "opcodes.h" +#include "thirdparty/stb_ds_wrap.h" #include #include @@ -529,8 +530,8 @@ static void emitFunctionCall(BasParserT *p, BasSymbolT *sym) { basEmit8(&p->cg, baseSlot); // If not yet defined, record the address for backpatching - if (!sym->isDefined && sym->patchCount < BAS_MAX_CALL_PATCHES) { - sym->patchAddrs[sym->patchCount++] = addrPos; + if (!sym->isDefined && true) { + arrput(sym->patchAddrs, addrPos); sym->patchCount = (int32_t)arrlen(sym->patchAddrs); } } @@ -574,9 +575,8 @@ static void emitJumpToLabel(BasParserT *p, uint8_t opcode, const char *labelName basEmit16(&p->cg, 0); // Record patch address for backpatching when label is defined - if (sym->patchCount < BAS_MAX_CALL_PATCHES) { - sym->patchAddrs[sym->patchCount++] = patchAddr; - } + arrput(sym->patchAddrs, patchAddr); + sym->patchCount = (int32_t)arrlen(sym->patchAddrs); } @@ -1616,8 +1616,8 @@ static void parseAssignOrCall(BasParserT *p) { basEmit8(&p->cg, (uint8_t)argc); basEmit8(&p->cg, baseSlot); - if (!sym->isDefined && sym->patchCount < BAS_MAX_CALL_PATCHES) { - sym->patchAddrs[sym->patchCount++] = addrPos; + if (!sym->isDefined && true) { + arrput(sym->patchAddrs, addrPos); sym->patchCount = (int32_t)arrlen(sym->patchAddrs); } } @@ -2248,12 +2248,17 @@ static void parseDimBounds(BasParserT *p, int32_t *outDims) { int32_t exprLen = basCodePos(&p->cg) - exprStart; int32_t insertLen = 3; // OP_PUSH_INT16 + 2 bytes - if (basCodePos(&p->cg) + insertLen <= BAS_MAX_CODE) { + { + // Grow the array to make room for the insertion + for (int32_t pad = 0; pad < insertLen; pad++) { + arrput(p->cg.code, 0); + } + memmove(&p->cg.code[exprStart + insertLen], &p->cg.code[exprStart], exprLen); p->cg.code[exprStart] = OP_PUSH_INT16; int16_t lbound = (int16_t)p->optionBase; memcpy(&p->cg.code[exprStart + 1], &lbound, 2); - p->cg.codeLen += insertLen; + p->cg.codeLen = (int32_t)arrlen(p->cg.code); } } @@ -3289,9 +3294,8 @@ static void parseOnError(BasParserT *p) { int32_t patchAddr = basCodePos(&p->cg); basEmit16(&p->cg, 0); - if (sym->patchCount < BAS_MAX_CALL_PATCHES) { - sym->patchAddrs[sym->patchCount++] = patchAddr; - } + arrput(sym->patchAddrs, patchAddr); + sym->patchCount = (int32_t)arrlen(sym->patchAddrs); } } @@ -4178,8 +4182,8 @@ static void parseStatement(BasParserT *p) { basEmit8(&p->cg, 0); basEmit8(&p->cg, baseSlot); - if (!sym->isDefined && sym->patchCount < BAS_MAX_CALL_PATCHES) { - sym->patchAddrs[sym->patchCount++] = addrPos; + if (!sym->isDefined && true) { + arrput(sym->patchAddrs, addrPos); sym->patchCount = (int32_t)arrlen(sym->patchAddrs); } } @@ -4513,26 +4517,23 @@ static void parseType(BasParserT *p) { return; } - if (typeSym->fieldCount >= BAS_MAX_UDT_FIELDS) { - error(p, "Too many fields in TYPE"); - return; - } - - BasFieldDefT *field = &typeSym->fields[typeSym->fieldCount]; + BasFieldDefT field; + memset(&field, 0, sizeof(field)); // Truncation is intentional -- field names are clamped to BAS_MAX_SYMBOL_NAME. #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wformat-truncation" - snprintf(field->name, BAS_MAX_SYMBOL_NAME, "%s", p->lex.token.text); + snprintf(field.name, BAS_MAX_SYMBOL_NAME, "%s", p->lex.token.text); #pragma GCC diagnostic pop advance(p); expect(p, TOK_AS); - field->dataType = resolveTypeName(p); - if (field->dataType == BAS_TYPE_UDT) { - field->udtTypeId = p->lastUdtTypeId; + field.dataType = resolveTypeName(p); + if (field.dataType == BAS_TYPE_UDT) { + field.udtTypeId = p->lastUdtTypeId; } - typeSym->fieldCount++; + arrput(typeSym->fields, field); + typeSym->fieldCount = (int32_t)arrlen(typeSym->fields); expectEndOfStatement(p); skipNewlines(p); @@ -4806,4 +4807,14 @@ BasModuleT *basParserBuildModule(BasParserT *p) { void basParserFree(BasParserT *p) { basCodeGenFree(&p->cg); + + // Free per-symbol dynamic arrays + for (int32_t i = 0; i < p->sym.count; i++) { + arrfree(p->sym.symbols[i].patchAddrs); + arrfree(p->sym.symbols[i].fields); + } + + arrfree(p->sym.symbols); + p->sym.symbols = NULL; + p->sym.count = 0; } diff --git a/apps/dvxbasic/compiler/symtab.c b/apps/dvxbasic/compiler/symtab.c index 5f6f29a..a0d75ed 100644 --- a/apps/dvxbasic/compiler/symtab.c +++ b/apps/dvxbasic/compiler/symtab.c @@ -1,8 +1,10 @@ // symtab.c -- DVX BASIC symbol table implementation #include "symtab.h" +#include "thirdparty/stb_ds_wrap.h" #include +#include #include // ============================================================ @@ -31,10 +33,6 @@ static bool namesEqual(const char *a, const char *b) { // ============================================================ BasSymbolT *basSymTabAdd(BasSymTabT *tab, const char *name, BasSymKindE kind, uint8_t dataType) { - if (tab->count >= BAS_MAX_SYMBOLS) { - return NULL; - } - // Check for duplicate in current scope BasScopeE scope = tab->inLocalScope ? SCOPE_LOCAL : SCOPE_GLOBAL; @@ -44,8 +42,11 @@ BasSymbolT *basSymTabAdd(BasSymTabT *tab, const char *name, BasSymKindE kind, ui } } - BasSymbolT *sym = &tab->symbols[tab->count++]; - memset(sym, 0, sizeof(*sym)); + BasSymbolT entry; + memset(&entry, 0, sizeof(entry)); + arrput(tab->symbols, entry); + tab->count = (int32_t)arrlen(tab->symbols); + BasSymbolT *sym = &tab->symbols[tab->count - 1]; strncpy(sym->name, name, BAS_MAX_SYMBOL_NAME - 1); sym->name[BAS_MAX_SYMBOL_NAME - 1] = '\0'; sym->kind = kind; @@ -128,11 +129,14 @@ void basSymTabInit(BasSymTabT *tab) { // ============================================================ void basSymTabLeaveLocal(BasSymTabT *tab) { - // Remove all local symbols + // Remove all local symbols, freeing their dynamic arrays int32_t newCount = 0; for (int32_t i = 0; i < tab->count; i++) { - if (tab->symbols[i].scope != SCOPE_LOCAL) { + if (tab->symbols[i].scope == SCOPE_LOCAL) { + arrfree(tab->symbols[i].patchAddrs); + arrfree(tab->symbols[i].fields); + } else { if (i != newCount) { tab->symbols[newCount] = tab->symbols[i]; } @@ -141,6 +145,7 @@ void basSymTabLeaveLocal(BasSymTabT *tab) { } } + arrsetlen(tab->symbols, newCount); tab->count = newCount; tab->inLocalScope = false; tab->nextLocalIdx = 0; diff --git a/apps/dvxbasic/compiler/symtab.h b/apps/dvxbasic/compiler/symtab.h index 6c6ca64..5259d28 100644 --- a/apps/dvxbasic/compiler/symtab.h +++ b/apps/dvxbasic/compiler/symtab.h @@ -42,8 +42,6 @@ typedef enum { #define BAS_MAX_SYMBOL_NAME 64 #define BAS_MAX_PARAMS 16 -#define BAS_MAX_CALL_PATCHES 32 -#define BAS_MAX_UDT_FIELDS 32 // UDT field definition typedef struct { @@ -74,7 +72,7 @@ typedef struct { bool paramByVal[BAS_MAX_PARAMS]; // Forward-reference backpatch list (code addresses to patch when defined) - int32_t patchAddrs[BAS_MAX_CALL_PATCHES]; + int32_t *patchAddrs; // stb_ds dynamic array int32_t patchCount; // For CONST: the constant value @@ -85,19 +83,17 @@ typedef struct { char constStr[256]; // For TYPE_DEF: field definitions - BasFieldDefT fields[BAS_MAX_UDT_FIELDS]; - int32_t fieldCount; + BasFieldDefT *fields; // stb_ds dynamic array + int32_t fieldCount; } BasSymbolT; // ============================================================ // Symbol table // ============================================================ -#define BAS_MAX_SYMBOLS 512 - typedef struct { - BasSymbolT symbols[BAS_MAX_SYMBOLS]; - int32_t count; + BasSymbolT *symbols; // stb_ds dynamic array + int32_t count; int32_t nextGlobalIdx; // next global variable slot int32_t nextLocalIdx; // next local variable slot (reset per SUB/FUNCTION) bool inLocalScope; // true when inside SUB/FUNCTION diff --git a/apps/dvxbasic/dvxbasic.res b/apps/dvxbasic/dvxbasic.res index 18b92da..d84f195 100644 --- a/apps/dvxbasic/dvxbasic.res +++ b/apps/dvxbasic/dvxbasic.res @@ -10,3 +10,5 @@ tb_run icon tb_run.bmp tb_stop icon tb_stop.bmp tb_code icon tb_code.bmp tb_design icon tb_design.bmp +# Placeholder icon (32x32) +noicon icon noicon.bmp diff --git a/apps/dvxbasic/formrt/formrt.c b/apps/dvxbasic/formrt/formrt.c index 73a5dfd..50743fb 100644 --- a/apps/dvxbasic/formrt/formrt.c +++ b/apps/dvxbasic/formrt/formrt.c @@ -10,6 +10,7 @@ #include "dvxDialog.h" #include "dvxWm.h" #include "widgetBox.h" +#include "thirdparty/stb_ds_wrap.h" #include #include @@ -230,7 +231,7 @@ void *basFormRtCreateCtrl(void *ctx, void *formRef, const char *typeName, const (void)ctx; BasFormT *form = (BasFormT *)formRef; - if (!form || form->controlCount >= BAS_MAX_CTRLS) { + if (!form) { return NULL; } @@ -257,8 +258,12 @@ void *basFormRtCreateCtrl(void *ctx, void *formRef, const char *typeName, const wgtSetName(widget, ctrlName); // Initialize control entry - BasControlT *ctrl = &form->controls[form->controlCount++]; - memset(ctrl, 0, sizeof(*ctrl)); + BasControlT entry; + memset(&entry, 0, sizeof(entry)); + snprintf(entry.name, BAS_MAX_CTRL_NAME, "%s", ctrlName); + arrput(form->controls, entry); + form->controlCount = (int32_t)arrlen(form->controls); + BasControlT *ctrl = &form->controls[form->controlCount - 1]; snprintf(ctrl->name, BAS_MAX_CTRL_NAME, "%s", ctrlName); ctrl->widget = widget; ctrl->form = form; @@ -296,12 +301,15 @@ void basFormRtDestroy(BasFormRtT *rt) { freeListBoxItems(&form->controls[j]); } + arrfree(form->controls); + if (form->window) { dvxDestroyWindow(rt->ctx, form->window); form->window = NULL; } } + arrfree(rt->forms); free(rt); } @@ -454,10 +462,6 @@ static BasStringT *basFormRtInputBox(void *ctx, const char *prompt, const char * void *basFormRtLoadForm(void *ctx, const char *formName) { BasFormRtT *rt = (BasFormRtT *)ctx; - if (rt->formCount >= BAS_MAX_FORMS) { - return NULL; - } - // Check if form already exists for (int32_t i = 0; i < rt->formCount; i++) { if (strcasecmp(rt->forms[i].name, formName) == 0) { @@ -478,8 +482,11 @@ void *basFormRtLoadForm(void *ctx, const char *formName) { return NULL; } - BasFormT *form = &rt->forms[rt->formCount++]; - memset(form, 0, sizeof(*form)); + BasFormT entry; + memset(&entry, 0, sizeof(entry)); + arrput(rt->forms, entry); + rt->formCount = (int32_t)arrlen(rt->forms); + BasFormT *form = &rt->forms[rt->formCount - 1]; snprintf(form->name, BAS_MAX_CTRL_NAME, "%s", formName); win->onClose = onFormClose; win->onResize = onFormResize; @@ -626,15 +633,19 @@ BasFormT *basFormRtLoadFrm(BasFormRtT *rt, const char *source, int32_t sourceLen wgtSetName(widget, ctrlName); - if (form->controlCount < BAS_MAX_CTRLS) { - current = &form->controls[form->controlCount++]; - memset(current, 0, sizeof(*current)); - snprintf(current->name, BAS_MAX_CTRL_NAME, "%s", ctrlName); - snprintf(current->typeName, BAS_MAX_CTRL_NAME, "%s", typeName); - current->widget = widget; - current->form = form; - current->iface = wgtGetIface(wgtTypeName); + { + BasControlT ctrlEntry; + memset(&ctrlEntry, 0, sizeof(ctrlEntry)); + snprintf(ctrlEntry.name, BAS_MAX_CTRL_NAME, "%s", ctrlName); + snprintf(ctrlEntry.typeName, BAS_MAX_CTRL_NAME, "%s", typeName); + ctrlEntry.widget = widget; + ctrlEntry.form = form; + ctrlEntry.iface = wgtGetIface(wgtTypeName); + arrput(form->controls, ctrlEntry); + form->controlCount = (int32_t)arrlen(form->controls); + // Re-derive pointer after arrput (may realloc) + current = &form->controls[form->controlCount - 1]; widget->userData = current; widget->onClick = onWidgetClick; widget->onDblClick = onWidgetDblClick; @@ -779,6 +790,14 @@ BasFormT *basFormRtLoadFrm(BasFormRtT *rt, const char *source, int32_t sourceLen form->window->x = form->frmLeft; form->window->y = form->frmTop; } + // Re-wire widget->userData pointers now that the controls array + // is finalized. arrput may have reallocated during loading, so + // any userData set during parsing could be stale. + for (int32_t i = 0; i < form->controlCount; i++) { + if (form->controls[i].widget) { + form->controls[i].widget->userData = &form->controls[i]; + } + } } return form; diff --git a/apps/dvxbasic/formrt/formrt.h b/apps/dvxbasic/formrt/formrt.h index 54cd389..f4d3904 100644 --- a/apps/dvxbasic/formrt/formrt.h +++ b/apps/dvxbasic/formrt/formrt.h @@ -27,8 +27,6 @@ typedef struct BasControlT BasControlT; // ============================================================ #define BAS_MAX_CTRL_NAME 32 -#define BAS_MAX_CTRLS 64 // max controls per form -#define BAS_MAX_FORMS 8 // ============================================================ // Control instance (a widget on a form) @@ -55,7 +53,7 @@ typedef struct BasFormT { WidgetT *root; // widget root (from wgtInitWindow) WidgetT *contentBox; // VBox/HBox for user controls AppContextT *ctx; // DVX app context - BasControlT controls[BAS_MAX_CTRLS]; // controls on this form + BasControlT *controls; // stb_ds dynamic array int32_t controlCount; BasVmT *vm; // VM for event dispatch BasModuleT *module; // compiled module (for SUB lookup) @@ -79,7 +77,7 @@ typedef struct { AppContextT *ctx; // DVX app context BasVmT *vm; // shared VM instance BasModuleT *module; // compiled module - BasFormT forms[BAS_MAX_FORMS]; + BasFormT *forms; // stb_ds dynamic array int32_t formCount; BasFormT *currentForm; // form currently dispatching events } BasFormRtT; diff --git a/apps/dvxbasic/ide/ideDesigner.c b/apps/dvxbasic/ide/ideDesigner.c index bf8dcdf..e6710ea 100644 --- a/apps/dvxbasic/ide/ideDesigner.c +++ b/apps/dvxbasic/ide/ideDesigner.c @@ -524,7 +524,7 @@ void dsgnNewForm(DsgnStateT *ds, const char *name) { form->top = 0; snprintf(form->layout, DSGN_MAX_NAME, "VBox"); form->centered = true; - form->autoSize = true; + form->autoSize = false; form->resizable = true; snprintf(form->name, DSGN_MAX_NAME, "%s", name); snprintf(form->caption, DSGN_MAX_TEXT, "%s", name); diff --git a/apps/dvxbasic/ide/ideDesigner.h b/apps/dvxbasic/ide/ideDesigner.h index cd7d2a3..c826517 100644 --- a/apps/dvxbasic/ide/ideDesigner.h +++ b/apps/dvxbasic/ide/ideDesigner.h @@ -175,4 +175,14 @@ bool dsgnIsContainer(const char *typeName); // Free designer resources. void dsgnFree(DsgnStateT *ds); +// ============================================================ +// Code rename support (implemented in ideMain.c) +// ============================================================ +// +// Rename all references to a form or control in project .bas files. +// Replaces OldName. -> NewName. and OldName_ -> NewName_ (case-insensitive, +// word-boundary aware). Also handles FormName.ControlName. patterns. + +void ideRenameInCode(const char *oldName, const char *newName); + #endif // IDE_DESIGNER_H diff --git a/apps/dvxbasic/ide/ideMain.c b/apps/dvxbasic/ide/ideMain.c index 6475d30..a3dc835 100644 --- a/apps/dvxbasic/ide/ideMain.c +++ b/apps/dvxbasic/ide/ideMain.c @@ -27,6 +27,7 @@ #include "widgetToolbar.h" #include "ideDesigner.h" +#include "ideProject.h" #include "ideToolbox.h" #include "ideProperties.h" @@ -74,6 +75,17 @@ #define CMD_SELECT_ALL 118 #define CMD_VIEW_TOOLBAR 119 #define CMD_VIEW_STATUS 120 +#define CMD_SAVE_ALL 129 +#define CMD_SAVE_ON_RUN 139 +#define CMD_PRJ_NEW 130 +#define CMD_PRJ_OPEN 131 +#define CMD_PRJ_SAVE 132 +#define CMD_PRJ_CLOSE 133 +#define CMD_PRJ_ADD_MOD 134 +#define CMD_PRJ_ADD_FRM 135 +#define CMD_PRJ_REMOVE 136 +#define CMD_PRJ_PROPS 138 +#define CMD_WIN_PROJECT 137 #define IDE_MAX_IMM 1024 #define IDE_DESIGN_W 400 #define IDE_DESIGN_H 300 @@ -86,12 +98,24 @@ int32_t appMain(DxeAppContextT *ctx); static void buildWindow(void); static void clearOutput(void); static void compileAndRun(void); +static void ensureProject(const char *filePath); static void loadFile(void); +static int32_t toolbarBottom(void); static void loadFilePath(const char *path); +static void newProject(void); +static void onPrjFileClick(int32_t fileIdx, bool isForm); +static void openProject(void); +static void closeProject(void); static void saveFile(void); static void onTbSave(WidgetT *w); +static bool hasUnsavedData(void); +static bool promptAndSave(void); +static void cleanupFormWin(void); static void onClose(WindowT *win); static void onContentFocus(WindowT *win); +static void onFormWinClose(WindowT *win); +static void onFormWinResize(WindowT *win, int32_t newW, int32_t newH); +static void onProjectWinClose(WindowT *win); static WindowT *getLastFocusWin(void); static void onMenu(WindowT *win, int32_t menuId); static void basicColorize(const char *line, int32_t lineLen, uint8_t *colors, void *ctx); @@ -105,7 +129,6 @@ static bool inputCallback(void *ctx, const char *prompt, char *buf, int32_t bufS static bool doEventsCallback(void *ctx); static void runCached(void); static void runModule(BasModuleT *mod); -static void onFormWinClose(WindowT *win); static void setStatus(const char *text); static void switchToCode(void); static void switchToDesign(void); @@ -129,6 +152,7 @@ static void updateDropdowns(void); static DxeAppContextT *sCtx = NULL; static AppContextT *sAc = NULL; +static PrefsHandleT *sPrefs = NULL; static WindowT *sWin = NULL; // Main toolbar window static WindowT *sCodeWin = NULL; // Code editor window static WindowT *sOutWin = NULL; // Output window @@ -147,6 +171,8 @@ static DsgnStateT sDesigner; static WindowT *sFormWin = NULL; // Form designer window (separate) static WindowT *sToolboxWin = NULL; static WindowT *sPropsWin = NULL; +static WindowT *sProjectWin = NULL; +static PrjStateT sProject; static WindowT *sLastFocusWin = NULL; // last focused non-toolbar window static char sSourceBuf[IDE_MAX_SOURCE]; @@ -186,25 +212,31 @@ int32_t appMain(DxeAppContextT *ctx) { sAc = ctx->shellCtx; basStringSystemInit(); + prjInit(&sProject); buildWindow(); // Load persisted settings char prefsPath[DVX_MAX_PATH]; shellConfigPath(sCtx, "dvxbasic.ini", prefsPath, sizeof(prefsPath)); - prefsLoad(prefsPath); + sPrefs = prefsLoad(prefsPath); if (sToolbar && sWin && sWin->menuBar) { - bool showTb = prefsGetBool("view", "toolbar", true); + bool showTb = prefsGetBool(sPrefs, "view", "toolbar", true); sToolbar->visible = showTb; wmMenuItemSetChecked(sWin->menuBar, CMD_VIEW_TOOLBAR, showTb); } if (sStatusBar && sWin && sWin->menuBar) { - bool showSb = prefsGetBool("view", "statusbar", true); + bool showSb = prefsGetBool(sPrefs, "view", "statusbar", true); sStatusBar->visible = showSb; wmMenuItemSetChecked(sWin->menuBar, CMD_VIEW_STATUS, showSb); } + if (sWin && sWin->menuBar) { + bool saveOnRun = prefsGetBool(sPrefs, "run", "saveOnRun", true); + wmMenuItemSetChecked(sWin->menuBar, CMD_SAVE_ON_RUN, saveOnRun); + } + if (sWin) { dvxFitWindowH(sAc, sWin); } @@ -214,13 +246,33 @@ int32_t appMain(DxeAppContextT *ctx) { sOutputBuf[0] = '\0'; sOutputLen = 0; - // Auto-load clickme.bas for development/testing - loadFilePath("C:\\BIN\\APPS\\DVXBASIC\\CLICKME.BAS"); + // Auto-load project for development/testing + if (prjLoad(&sProject, "C:\\BIN\\APPS\\DVXBASIC\\MULTI.DBP")) { + sProjectWin = prjCreateWindow(sAc, &sProject, onPrjFileClick); - setStatus("Ready. Open a .BAS file or type code and press Run."); + if (sProjectWin) { + sProjectWin->y = toolbarBottom() + 25; + sProjectWin->onClose = onProjectWinClose; + } + + char title[300]; + snprintf(title, sizeof(title), "DVX BASIC - [%s]", sProject.name); + dvxSetTitle(sAc, sWin, title); + } + + setStatus("Ready."); return 0; } +// ============================================================ +// toolbarBottom -- Y position just below the toolbar window +// ============================================================ + +static int32_t toolbarBottom(void) { + return sWin ? sWin->y + sWin->h + 2 : 60; +} + + // ============================================================ // loadTbIcon -- load a toolbar icon from the app's resources // ============================================================ @@ -258,8 +310,17 @@ static void buildWindow(void) { // Menu bar MenuBarT *menuBar = wmAddMenuBar(sWin); MenuT *fileMenu = wmAddMenu(menuBar, "&File"); - wmAddMenuItem(fileMenu, "&Open...\tCtrl+O", CMD_OPEN); - wmAddMenuItem(fileMenu, "&Save\tCtrl+S", CMD_SAVE); + wmAddMenuItem(fileMenu, "New &Project...", CMD_PRJ_NEW); + wmAddMenuItem(fileMenu, "Open Pro&ject...", CMD_PRJ_OPEN); + wmAddMenuItem(fileMenu, "Save Projec&t", CMD_PRJ_SAVE); + wmAddMenuItem(fileMenu, "Close Projec&t", CMD_PRJ_CLOSE); + wmAddMenuItem(fileMenu, "Project &Properties...", CMD_PRJ_PROPS); + wmAddMenuSeparator(fileMenu); + wmAddMenuItem(fileMenu, "&Open File...\tCtrl+O", CMD_OPEN); + wmAddMenuItem(fileMenu, "&Save File\tCtrl+S", CMD_SAVE); + wmAddMenuItem(fileMenu, "Save A&ll", CMD_SAVE_ALL); + wmAddMenuSeparator(fileMenu); + wmAddMenuItem(fileMenu, "&Remove File", CMD_PRJ_REMOVE); wmAddMenuSeparator(fileMenu); wmAddMenuItem(fileMenu, "E&xit", CMD_EXIT); @@ -278,6 +339,8 @@ static void buildWindow(void) { wmAddMenuItem(runMenu, "&Stop\tEsc", CMD_STOP); wmAddMenuSeparator(runMenu); wmAddMenuItem(runMenu, "&Clear Output", CMD_CLEAR); + wmAddMenuSeparator(runMenu); + wmAddMenuCheckItem(runMenu, "Save on &Run", CMD_SAVE_ON_RUN, true); MenuT *viewMenu = wmAddMenu(menuBar, "&View"); wmAddMenuItem(viewMenu, "&Code\tF7", CMD_VIEW_CODE); @@ -291,6 +354,7 @@ static void buildWindow(void) { wmAddMenuItem(winMenu, "&Output", CMD_WIN_OUTPUT); wmAddMenuItem(winMenu, "&Immediate", CMD_WIN_IMM); wmAddMenuSeparator(winMenu); + wmAddMenuItem(winMenu, "&Project Explorer", CMD_WIN_PROJECT); wmAddMenuItem(winMenu, "&Toolbox", CMD_WIN_TOOLBOX); wmAddMenuItem(winMenu, "&Properties", CMD_WIN_PROPS); @@ -343,8 +407,6 @@ static void buildWindow(void) { // Initialize designer (form window created on demand) dsgnInit(&sDesigner, sAc); - // Create child windows - showCodeWindow(); showOutputWindow(); showImmediateWindow(); } @@ -510,10 +572,16 @@ static void basicColorize(const char *line, int32_t lineLen, uint8_t *colors, vo // clearOutput // ============================================================ +static void setOutputText(const char *text) { + if (sOutput) { + wgtSetText(sOutput, text); + } +} + static void clearOutput(void) { sOutputBuf[0] = '\0'; sOutputLen = 0; - wgtSetText(sOutput, ""); + setOutputText(""); } // ============================================================ @@ -521,12 +589,9 @@ static void clearOutput(void) { // ============================================================ static void compileAndRun(void) { - // Get source from editor - const char *src = wgtGetText(sEditor); - - if (!src || *src == '\0') { - setStatus("No source code to run."); - return; + // Save all files before compiling if Save on Run is enabled + if (sWin && sWin->menuBar && wmMenuItemIsChecked(sWin->menuBar, CMD_SAVE_ON_RUN)) { + saveFile(); } clearOutput(); @@ -535,12 +600,130 @@ static void compileAndRun(void) { // Force a display update so the status is visible dvxInvalidateWindow(sAc, sWin); - int32_t srcLen = (int32_t)strlen(src); + // Build source: either concatenate project files or use editor contents + char *concatBuf = NULL; + const char *src = NULL; + int32_t srcLen = 0; + + if (sProject.projectPath[0] != '\0' && sProject.fileCount > 0) { + // Stash current editor contents into the active file's buffer + if (sProject.activeFileIdx >= 0 && sEditor) { + PrjFileT *cur = &sProject.files[sProject.activeFileIdx]; + const char *edSrc = wgtGetText(sEditor); + free(cur->buffer); + cur->buffer = edSrc ? strdup(edSrc) : NULL; + } + + // Concatenate all .bas files from buffers (or disk if not yet loaded) + concatBuf = (char *)malloc(IDE_MAX_SOURCE); + + if (!concatBuf) { + setStatus("Out of memory."); + return; + } + + int32_t pos = 0; + int32_t line = 1; + arrfree(sProject.sourceMap); + sProject.sourceMap = NULL; + sProject.sourceMapCount = 0; + + for (int32_t i = 0; i < sProject.fileCount; i++) { + if (sProject.files[i].isForm) { + continue; + } + + const char *fileSrc = sProject.files[i].buffer; + char *diskBuf = NULL; + + if (!fileSrc) { + // Not yet loaded into memory -- read from disk + 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) { + diskBuf = (char *)malloc(size + 1); + + if (diskBuf) { + int32_t br = (int32_t)fread(diskBuf, 1, size, f); + diskBuf[br] = '\0'; + fileSrc = diskBuf; + } + } + + fclose(f); + } + + if (!fileSrc) { + continue; + } + + int32_t startLine = line; + int32_t fileLen = (int32_t)strlen(fileSrc); + int32_t copyLen = fileLen; + + if (pos + copyLen >= IDE_MAX_SOURCE - 1) { + copyLen = IDE_MAX_SOURCE - 1 - pos; + } + + memcpy(concatBuf + pos, fileSrc, copyLen); + pos += copyLen; + + // Count lines + for (int32_t j = 0; j < copyLen; j++) { + if (fileSrc[j] == '\n') { + line++; + } + } + + free(diskBuf); + + // Ensure a trailing newline between files + if (copyLen > 0 && concatBuf[pos - 1] != '\n' && pos < IDE_MAX_SOURCE - 1) { + concatBuf[pos++] = '\n'; + line++; + } + + { + PrjSourceMapT mapEntry; + mapEntry.fileIdx = i; + mapEntry.startLine = startLine; + mapEntry.lineCount = line - startLine; + arrput(sProject.sourceMap, mapEntry); + sProject.sourceMapCount = (int32_t)arrlen(sProject.sourceMap); + } + } + + concatBuf[pos] = '\0'; + src = concatBuf; + srcLen = pos; + } else { + // No project files -- compile whatever is in the editor + src = wgtGetText(sEditor); + + if (!src || *src == '\0') { + setStatus("No source code to run."); + return; + } + + srcLen = (int32_t)strlen(src); + } // Compile (heap-allocated -- BasParserT is ~300KB, too large for stack) BasParserT *parser = (BasParserT *)malloc(sizeof(BasParserT)); if (!parser) { + free(concatBuf); setStatus("Out of memory."); return; } @@ -550,19 +733,32 @@ static void compileAndRun(void) { if (!basParse(parser)) { int32_t n = snprintf(sOutputBuf, IDE_MAX_OUTPUT, "COMPILE ERROR:\n%s\n", parser->error); sOutputLen = n; - wgtSetText(sOutput, sOutputBuf); + setOutputText(sOutputBuf); - // Jump to error line in editor + // Jump to error line -- translate through source map if project if (parser->errorLine > 0 && sEditor) { - wgtTextAreaGoToLine(sEditor, parser->errorLine); + int32_t fileIdx = -1; + int32_t localLine = parser->errorLine; + + if (sProject.fileCount > 0 && prjMapLine(&sProject, parser->errorLine, &fileIdx, &localLine)) { + // Open the offending file if it's not already active + if (fileIdx != sProject.activeFileIdx) { + onPrjFileClick(fileIdx, false); + } + } + + wgtTextAreaGoToLine(sEditor, localLine); } setStatus("Compilation failed."); basParserFree(parser); free(parser); + free(concatBuf); return; } + free(concatBuf); + BasModuleT *mod = basParserBuildModule(parser); basParserFree(parser); free(parser); @@ -659,14 +855,14 @@ static void runModule(BasModuleT *mod) { int32_t pos = sOutputLen; int32_t n = snprintf(sOutputBuf + pos, IDE_MAX_OUTPUT - pos, "\n[Runtime error: %s]\n", basVmGetError(vm)); sOutputLen += n; - wgtSetText(sOutput, sOutputBuf); + setOutputText(sOutputBuf); break; } sVm = NULL; // Update output display - wgtSetText(sOutput, sOutputBuf); + setOutputText(sOutputBuf); static char statusBuf[128]; snprintf(statusBuf, sizeof(statusBuf), "Done. %ld instructions executed.", (long)totalSteps); @@ -807,7 +1003,7 @@ static bool inputCallback(void *ctx, const char *prompt, char *buf, int32_t bufS if (prompt && sOutputLen < IDE_MAX_OUTPUT - 1) { int32_t n = snprintf(sOutputBuf + sOutputLen, IDE_MAX_OUTPUT - sOutputLen, "%s", prompt); sOutputLen += n; - wgtSetText(sOutput, sOutputBuf); + setOutputText(sOutputBuf); } return dvxInputBox(sAc, "DVX BASIC", prompt ? prompt : "Enter value:", NULL, buf, bufSize); @@ -837,25 +1033,132 @@ static void loadFilePath(const char *path) { fclose(f); sSourceBuf[bytesRead] = '\0'; - wgtSetText(sEditor, sSourceBuf); + if (!sCodeWin) { + showCodeWindow(); + } + + if (sEditor) { + wgtSetText(sEditor, sSourceBuf); + } + snprintf(sFilePath, sizeof(sFilePath), "%s", path); - char title[300]; - snprintf(title, sizeof(title), "DVX BASIC - %s", path); - dvxSetTitle(sAc, sWin, title); - if (sFormWin) { - onFormWinClose(sFormWin); + dvxDestroyWindow(sAc, sFormWin); + cleanupFormWin(); } dsgnFree(&sDesigner); - updateDropdowns(); setStatus("File loaded."); } +// Auto-create an implicit project when opening a file without one. +// Derives project name and directory from the file path. Also adds +// a matching .frm file if one exists alongside the .bas. + +static void ensureProject(const char *filePath) { + if (sProject.projectPath[0] != '\0') { + return; + } + + // Derive directory and base name from path + char dir[DVX_MAX_PATH]; + char baseName[PRJ_MAX_NAME]; + snprintf(dir, sizeof(dir), "%s", filePath); + + char *sep = strrchr(dir, '/'); + char *sep2 = strrchr(dir, '\\'); + + if (sep2 > sep) { + sep = sep2; + } + + const char *fileName = filePath; + + if (sep) { + fileName = sep + 1; + *sep = '\0'; + } else { + dir[0] = '.'; + dir[1] = '\0'; + } + + // Strip extension for project name + // Length-clamped memcpy instead of strncpy/snprintf because + // GCC warns about both when source (DVX_MAX_PATH) exceeds + // the buffer (PRJ_MAX_NAME), even though truncation is safe. + int32_t nl = (int32_t)strlen(fileName); + + if (nl >= PRJ_MAX_NAME) { + nl = PRJ_MAX_NAME - 1; + } + + memcpy(baseName, fileName, nl); + baseName[nl] = '\0'; + + char *dot = strrchr(baseName, '.'); + + if (dot) { + *dot = '\0'; + } + + prjNew(&sProject, baseName, dir); + + // Determine if this is a .bas or .frm + const char *ext = strrchr(filePath, '.'); + bool isForm = (ext && strcasecmp(ext, ".frm") == 0); + + prjAddFile(&sProject, fileName, isForm); + + // If it's a .bas, check for a matching .frm and add it too + if (!isForm) { + char frmPath[DVX_MAX_PATH]; + snprintf(frmPath, sizeof(frmPath), "%s", filePath); + char *frmDot = strrchr(frmPath, '.'); + + if (frmDot) { + strcpy(frmDot, ".frm"); + } else { + strcat(frmPath, ".frm"); + } + + FILE *frmFile = fopen(frmPath, "r"); + + if (frmFile) { + fclose(frmFile); + + // Get just the filename portion + const char *frmName = strrchr(frmPath, '/'); + const char *frmName2 = strrchr(frmPath, '\\'); + + if (frmName2 > frmName) { + frmName = frmName2; + } + + frmName = frmName ? frmName + 1 : frmPath; + prjAddFile(&sProject, frmName, true); + } + } + + sProject.dirty = false; + sProject.activeFileIdx = 0; + + char title[300]; + snprintf(title, sizeof(title), "DVX BASIC - [%s]", sProject.name); + + if (sWin) { + dvxSetTitle(sAc, sWin, title); + } +} + + static void loadFile(void) { + if (!promptAndSave()) { + return; + } + FileFilterT filters[] = { { "BASIC Files (*.bas)", "*.bas" }, { "Form Files (*.frm)", "*.frm" }, @@ -865,7 +1168,17 @@ static void loadFile(void) { char path[DVX_MAX_PATH]; if (dvxFileDialog(sAc, "Open BASIC File", FD_OPEN, NULL, filters, 3, path, sizeof(path))) { - loadFilePath(path); + closeProject(); + ensureProject(path); + + const char *ext = strrchr(path, '.'); + bool isForm = (ext && strcasecmp(ext, ".frm") == 0); + + if (isForm) { + onPrjFileClick(0, true); + } else { + loadFilePath(path); + } } } @@ -873,56 +1186,55 @@ static void loadFile(void) { // saveFile // ============================================================ -static void saveFile(void) { - if (sFilePath[0] == '\0') { - // No file loaded -- use Save As dialog - FileFilterT filters[] = { - { "BASIC Files (*.bas)", "*.bas" }, - { "All Files (*.*)", "*.*" } - }; - - if (!dvxFileDialog(sAc, "Save BASIC File", FD_SAVE, NULL, filters, 2, sFilePath, sizeof(sFilePath))) { - return; - } +static void saveActiveFile(void) { + if (sProject.projectPath[0] == '\0') { + return; } - // Save the .bas source - const char *src = wgtGetText(sEditor); + int32_t idx = sProject.activeFileIdx; - if (src) { - FILE *f = fopen(sFilePath, "w"); - - if (f) { - fputs(src, f); - fclose(f); - } else { - dvxMessageBox(sAc, "Error", "Could not write file.", MB_OK | MB_ICONERROR); - return; - } + if (idx < 0 || idx >= sProject.fileCount) { + return; } - // Save the .frm if the designer has form data - if (sDesigner.form && sDesigner.form->dirty) { - char frmPath[DVX_MAX_PATH]; - snprintf(frmPath, sizeof(frmPath), "%s", sFilePath); - char *dot = strrchr(frmPath, '.'); + PrjFileT *file = &sProject.files[idx]; + char fullPath[DVX_MAX_PATH]; + prjFullPath(&sProject, idx, fullPath, sizeof(fullPath)); - if (dot) { - strcpy(dot, ".frm"); - } else { - strcat(frmPath, ".frm"); + if (file->isForm && sDesigner.form) { + // Save form designer state to .frm file + char *frmBuf = (char *)malloc(IDE_MAX_SOURCE); + + if (frmBuf) { + int32_t frmLen = dsgnSaveFrm(&sDesigner, frmBuf, IDE_MAX_SOURCE); + + 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); } + } else if (!file->isForm && sEditor) { + // Save code editor contents to .bas file + const char *src = wgtGetText(sEditor); - char frmBuf[IDE_MAX_SOURCE]; - int32_t frmLen = dsgnSaveFrm(&sDesigner, frmBuf, sizeof(frmBuf)); - - if (frmLen > 0) { - FILE *f = fopen(frmPath, "w"); + if (src) { + FILE *f = fopen(fullPath, "w"); if (f) { - fwrite(frmBuf, 1, frmLen, f); + fputs(src, f); fclose(f); - sDesigner.form->dirty = false; + file->modified = false; + } else { + dvxMessageBox(sAc, "Error", "Could not write file.", MB_OK | MB_ICONERROR); + return; } } } @@ -930,6 +1242,580 @@ static void saveFile(void) { setStatus("Saved."); } +static void saveFile(void) { + // If no project or no active file, prompt for a path + if (sProject.projectPath[0] == '\0' || sProject.activeFileIdx < 0) { + if (sFilePath[0] == '\0') { + FileFilterT filters[] = { + { "BASIC Files (*.bas)", "*.bas" }, + { "All Files (*.*)", "*.*" } + }; + + if (!dvxFileDialog(sAc, "Save BASIC File", FD_SAVE, NULL, filters, 2, sFilePath, sizeof(sFilePath))) { + return; + } + + ensureProject(sFilePath); + } + + // Save editor to sFilePath directly + const char *src = sEditor ? wgtGetText(sEditor) : NULL; + + if (src) { + FILE *f = fopen(sFilePath, "w"); + + if (f) { + fputs(src, f); + fclose(f); + } + } + + setStatus("Saved."); + return; + } + + // Save the active project file + saveActiveFile(); + + // Also save any other dirty forms + for (int32_t i = 0; i < sProject.fileCount; i++) { + if (i == sProject.activeFileIdx) { + continue; + } + + if (sProject.files[i].isForm && sProject.files[i].modified && sProject.files[i].buffer) { + char fullPath[DVX_MAX_PATH]; + prjFullPath(&sProject, i, fullPath, sizeof(fullPath)); + + FILE *f = fopen(fullPath, "w"); + + if (f) { + fputs(sProject.files[i].buffer, f); + fclose(f); + sProject.files[i].modified = false; + } + } + } +} + + +// ============================================================ +// onPrjFileClick -- called when a file is clicked in the project tree +// ============================================================ + +static void onPrjFileClick(int32_t fileIdx, bool isForm) { + if (fileIdx < 0 || fileIdx >= sProject.fileCount) { + return; + } + + if (fileIdx == sProject.activeFileIdx) { + return; + } + + // Stash current active file's contents into its buffer + if (sProject.activeFileIdx >= 0) { + PrjFileT *cur = &sProject.files[sProject.activeFileIdx]; + + if (cur->isForm && sDesigner.form) { + // Serialize form designer state to .frm text + char *frmBuf = (char *)malloc(IDE_MAX_SOURCE); + + if (frmBuf) { + int32_t frmLen = dsgnSaveFrm(&sDesigner, frmBuf, IDE_MAX_SOURCE); + + free(cur->buffer); + + if (frmLen > 0) { + frmBuf[frmLen] = '\0'; + cur->buffer = frmBuf; + } else { + free(frmBuf); + cur->buffer = NULL; + } + + cur->modified = true; + } + } else if (!cur->isForm && sEditor) { + // Stash code editor text + const char *src = wgtGetText(sEditor); + free(cur->buffer); + cur->buffer = src ? strdup(src) : NULL; + cur->modified = true; + } + } + + PrjFileT *target = &sProject.files[fileIdx]; + + if (isForm) { + // Load form from buffer or disk, or create a new blank form + const char *frmSrc = target->buffer; + char *diskBuf = NULL; + + if (!frmSrc) { + char fullPath[DVX_MAX_PATH]; + prjFullPath(&sProject, fileIdx, fullPath, sizeof(fullPath)); + + FILE *f = fopen(fullPath, "r"); + + if (!f) { + // File doesn't exist yet -- create a new blank form. + // Derive form name from the filename (strip path and extension). + char formName[PRJ_MAX_NAME]; + const char *base = strrchr(target->path, '/'); + const char *base2 = strrchr(target->path, '\\'); + + if (base2 > base) { + base = base2; + } + + base = base ? base + 1 : target->path; + + // Length-clamped memcpy instead of strncpy/snprintf because + // GCC warns about both when source (DVX_MAX_PATH) exceeds + // the buffer (PRJ_MAX_NAME), even though truncation is safe. + int32_t bl = (int32_t)strlen(base); + + if (bl >= PRJ_MAX_NAME) { + bl = PRJ_MAX_NAME - 1; + } + + memcpy(formName, base, bl); + formName[bl] = '\0'; + char *dot = strrchr(formName, '.'); + + if (dot) { + *dot = '\0'; + } + + if (sDesigner.form) { + dsgnFree(&sDesigner); + } + + dsgnNewForm(&sDesigner, formName); + target->modified = true; + sProject.activeFileIdx = fileIdx; + switchToDesign(); + 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; + } + + diskBuf = (char *)malloc(size + 1); + + if (!diskBuf) { + fclose(f); + return; + } + + int32_t bytesRead = (int32_t)fread(diskBuf, 1, size, f); + fclose(f); + diskBuf[bytesRead] = '\0'; + frmSrc = diskBuf; + } + + if (sDesigner.form) { + dsgnFree(&sDesigner); + } + + dsgnLoadFrm(&sDesigner, frmSrc, (int32_t)strlen(frmSrc)); + free(diskBuf); + + sProject.activeFileIdx = fileIdx; + switchToDesign(); + } else { + // Load .bas file from buffer or disk + if (!sCodeWin) { + showCodeWindow(); + } + + if (target->buffer) { + if (sEditor) { + wgtSetText(sEditor, target->buffer); + } + } else { + char fullPath[DVX_MAX_PATH]; + prjFullPath(&sProject, fileIdx, fullPath, sizeof(fullPath)); + + FILE *f = fopen(fullPath, "r"); + + if (f) { + fclose(f); + loadFilePath(fullPath); + } else if (sEditor) { + // File doesn't exist yet -- start with empty editor + wgtSetText(sEditor, ""); + target->modified = true; + } + } + + sProject.activeFileIdx = fileIdx; + } +} + + +// ============================================================ +// newProject +// ============================================================ + +static void newProject(void) { + char name[PRJ_MAX_NAME]; + + if (!dvxInputBox(sAc, "New Project", "Project name:", "", name, sizeof(name))) { + return; + } + + if (name[0] == '\0') { + return; + } + + // Ask for directory via save dialog (file = name.dbp) + FileFilterT filters[] = { + { "Project Files (*.dbp)", "*.dbp" } + }; + + char dbpPath[DVX_MAX_PATH]; + snprintf(dbpPath, sizeof(dbpPath), "%s.dbp", name); + + if (!dvxFileDialog(sAc, "Save New Project", FD_SAVE, NULL, filters, 1, dbpPath, sizeof(dbpPath))) { + return; + } + + closeProject(); + + // Derive directory from chosen path + char dir[DVX_MAX_PATH]; + snprintf(dir, sizeof(dir), "%s", dbpPath); + char *sep = strrchr(dir, '/'); + char *sep2 = strrchr(dir, '\\'); + + if (sep2 > sep) { + sep = sep2; + } + + if (sep) { + *sep = '\0'; + } else { + dir[0] = '.'; + dir[1] = '\0'; + } + + prjNew(&sProject, name, dir); + snprintf(sProject.projectPath, sizeof(sProject.projectPath), "%s", dbpPath); + prjSave(&sProject); + sProject.dirty = false; + + // Create and show project window + if (!sProjectWin) { + sProjectWin = prjCreateWindow(sAc, &sProject, onPrjFileClick); + + if (sProjectWin) { + sProjectWin->y = toolbarBottom() + 25; + sProjectWin->onClose = onProjectWinClose; + } + } else { + prjRebuildTree(&sProject); + } + + char title[300]; + snprintf(title, sizeof(title), "DVX BASIC - [%s]", sProject.name); + dvxSetTitle(sAc, sWin, title); + + setStatus("New project created."); +} + + +// ============================================================ +// openProject +// ============================================================ + +static void openProject(void) { + FileFilterT filters[] = { + { "Project Files (*.dbp)", "*.dbp" }, + { "All Files (*.*)", "*.*" } + }; + + char path[DVX_MAX_PATH]; + + if (!dvxFileDialog(sAc, "Open Project", FD_OPEN, NULL, filters, 2, path, sizeof(path))) { + return; + } + + closeProject(); + + if (!prjLoad(&sProject, path)) { + dvxMessageBox(sAc, "Error", "Could not open project file.", MB_OK | MB_ICONERROR); + return; + } + + // Create and show project window + if (!sProjectWin) { + sProjectWin = prjCreateWindow(sAc, &sProject, onPrjFileClick); + + if (sProjectWin) { + sProjectWin->y = toolbarBottom() + 25; + sProjectWin->onClose = onProjectWinClose; + } + } else { + prjRebuildTree(&sProject); + } + + // Open the first .bas file in the editor + for (int32_t i = 0; i < sProject.fileCount; i++) { + if (!sProject.files[i].isForm) { + onPrjFileClick(i, false); + break; + } + } + + char title[300]; + snprintf(title, sizeof(title), "DVX BASIC - [%s]", sProject.name); + dvxSetTitle(sAc, sWin, title); + + setStatus("Project loaded."); +} + + +// ============================================================ +// closeProject +// ============================================================ + +static void closeProject(void) { + if (sProject.projectPath[0] == '\0') { + return; + } + + if (sProject.dirty) { + prjSave(&sProject); + } + + prjClose(&sProject); + + if (sProjectWin) { + prjDestroyWindow(sAc, sProjectWin); + sProjectWin = NULL; + } + + if (sWin) { + dvxSetTitle(sAc, sWin, "DVX BASIC"); + } + + if (sStatus) { + setStatus("Project closed."); + } +} + + +// ============================================================ +// ideRenameInCode -- rename form/control references in all .bas files +// ============================================================ +// +// Case-insensitive replacement of OldName followed by '.' or '_' with +// NewName followed by the same delimiter. This handles: +// ControlName.Property -> NewName.Property +// ControlName_Click -> NewName_Click (event handlers) +// FormName.ControlName.X -> NewFormName.ControlName.X +// Sub FormName_Load -> Sub NewFormName_Load +// +// Word-boundary check on the left: the character before the match must +// be a non-identifier character (space, tab, newline, '.', '(', start +// of string) to avoid replacing "Command1" inside "MyCommand1". + +static bool isIdentChar(char c) { + return (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || + (c >= '0' && c <= '9') || c == '_'; +} + + +// Check if position i is inside a string literal or comment. +// Scans from the start of the line containing i. +static bool isInStringOrComment(const char *src, int32_t i) { + // Find start of line + int32_t lineStart = i; + + while (lineStart > 0 && src[lineStart - 1] != '\n') { + lineStart--; + } + + bool inString = false; + + for (int32_t j = lineStart; j < i; j++) { + if (src[j] == '"') { + inString = !inString; + } else if (src[j] == '\'' && !inString) { + // Rest of line is a comment + return true; + } + } + + return inString; +} + + +static char *renameInBuffer(const char *src, const char *oldName, const char *newName) { + if (!src || !oldName || !newName || !oldName[0]) { + return NULL; + } + + int32_t oldLen = (int32_t)strlen(oldName); + int32_t newLen = (int32_t)strlen(newName); + int32_t srcLen = (int32_t)strlen(src); + + // First pass: count replacements to compute output size + int32_t count = 0; + + for (int32_t i = 0; i <= srcLen - oldLen; i++) { + if (strncasecmp(&src[i], oldName, oldLen) != 0) { + continue; + } + + char after = src[i + oldLen]; + + if (after != '.' && after != '_') { + continue; + } + + if (i > 0 && isIdentChar(src[i - 1])) { + continue; + } + + if (isInStringOrComment(src, i)) { + continue; + } + + count++; + } + + if (count == 0) { + return NULL; + } + + // Allocate output + int32_t outLen = srcLen + count * (newLen - oldLen); + char *out = (char *)malloc(outLen + 1); + + if (!out) { + return NULL; + } + + // Second pass: build output + int32_t op = 0; + + for (int32_t i = 0; i < srcLen; ) { + if (i <= srcLen - oldLen && + strncasecmp(&src[i], oldName, oldLen) == 0) { + char after = src[i + oldLen]; + + if ((after == '.' || after == '_') && + (i == 0 || !isIdentChar(src[i - 1])) && + !isInStringOrComment(src, i)) { + memcpy(out + op, newName, newLen); + op += newLen; + i += oldLen; + continue; + } + } + + out[op++] = src[i++]; + } + + out[op] = '\0'; + return out; +} + + +void ideRenameInCode(const char *oldName, const char *newName) { + if (!oldName || !newName || strcasecmp(oldName, newName) == 0) { + return; + } + + // Rename in the active editor + if (sEditor && sProject.activeFileIdx >= 0 && + !sProject.files[sProject.activeFileIdx].isForm) { + const char *edText = wgtGetText(sEditor); + + if (edText) { + char *replaced = renameInBuffer(edText, oldName, newName); + + if (replaced) { + wgtSetText(sEditor, replaced); + free(replaced); + } + } + } + + // Rename in all project .bas file buffers + for (int32_t i = 0; i < sProject.fileCount; i++) { + if (sProject.files[i].isForm) { + continue; + } + + // Skip the active file (already handled via editor) + if (i == sProject.activeFileIdx) { + continue; + } + + char *buf = sProject.files[i].buffer; + + if (!buf) { + // Load from disk + 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; + } + + buf = (char *)malloc(size + 1); + + if (!buf) { + fclose(f); + continue; + } + + int32_t br = (int32_t)fread(buf, 1, size, f); + fclose(f); + buf[br] = '\0'; + sProject.files[i].buffer = buf; + } + + char *replaced = renameInBuffer(buf, oldName, newName); + + if (replaced) { + free(sProject.files[i].buffer); + sProject.files[i].buffer = replaced; + sProject.files[i].modified = true; + } + } +} + + +// ============================================================ +// onProjectWinClose -- user closed the project window via X button +// ============================================================ + +static void onProjectWinClose(WindowT *win) { + prjDestroyWindow(sAc, win); + sProjectWin = NULL; +} + // ============================================================ // loadFrmFiles @@ -939,25 +1825,7 @@ static void saveFile(void) { // .bas source file. For example, if the user loaded "clickme.bas", // this looks for "clickme.frm" in the same directory. -static void loadFrmFiles(BasFormRtT *rt) { - if (sFilePath[0] == '\0') { - return; - } - - // Build .frm path from .bas path - char frmPath[DVX_MAX_PATH]; - snprintf(frmPath, sizeof(frmPath), "%s", sFilePath); - - // Find the extension and replace with .frm - char *dot = strrchr(frmPath, '.'); - - if (dot) { - strcpy(dot, ".frm"); - } else { - strcat(frmPath, ".frm"); - } - - // Try to open the .frm file +static void loadFrmFile(BasFormRtT *rt, const char *frmPath) { FILE *f = fopen(frmPath, "r"); if (!f) { @@ -995,6 +1863,16 @@ static void loadFrmFiles(BasFormRtT *rt) { 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); + } + } +} + @@ -1018,39 +1896,118 @@ static WindowT *getLastFocusWin(void) { } +// ============================================================ +// hasUnsavedData -- check if any project files have unsaved changes +// ============================================================ + +static bool hasUnsavedData(void) { + // Check the active editor/designer + if (sDesigner.form && sDesigner.form->dirty) { + return true; + } + + // Check all project files + for (int32_t i = 0; i < sProject.fileCount; i++) { + if (sProject.files[i].modified) { + return true; + } + } + + return sProject.dirty; +} + + +// ============================================================ +// promptAndSave -- ask user to save, discard, or cancel +// ============================================================ +// +// Returns true if the caller should proceed (user saved or discarded). +// Returns false if the user cancelled. + +static bool promptAndSave(void) { + if (!hasUnsavedData()) { + return true; + } + + int32_t result = dvxPromptSave(sAc, "DVX BASIC"); + + if (result == DVX_SAVE_YES) { + saveFile(); + return true; + } + + return result == DVX_SAVE_NO; +} + + // ============================================================ // onClose // ============================================================ static void onClose(WindowT *win) { + if (!promptAndSave()) { + return; + } + + // Prevent stale focus tracking during shutdown + sLastFocusWin = NULL; + + // Null widget pointers first so nothing references destroyed widgets + sEditor = NULL; + sOutput = NULL; + sImmediate = NULL; + sObjDropdown = NULL; + sEvtDropdown = NULL; + sStatus = NULL; + sToolbar = NULL; + sStatusBar = NULL; + + // Close all child windows // Close all child windows if (sCodeWin && sCodeWin != win) { dvxDestroyWindow(sAc, sCodeWin); } + sCodeWin = NULL; + if (sOutWin && sOutWin != win) { dvxDestroyWindow(sAc, sOutWin); } + sOutWin = NULL; + if (sImmWin && sImmWin != win) { dvxDestroyWindow(sAc, sImmWin); } + sImmWin = NULL; + if (sFormWin) { - onFormWinClose(sFormWin); + dvxDestroyWindow(sAc, sFormWin); + cleanupFormWin(); } - sWin = NULL; - sCodeWin = NULL; - sOutWin = NULL; - sImmWin = NULL; - sLastFocusWin = NULL; - sEditor = NULL; - sOutput = NULL; - sImmediate = NULL; - sObjDropdown = NULL; - sEvtDropdown = NULL; - sStatus = NULL; + if (sToolboxWin) { + tbxDestroy(sAc, sToolboxWin); + sToolboxWin = NULL; + } + + if (sPropsWin) { + prpDestroy(sAc, sPropsWin); + sPropsWin = NULL; + } + + if (sProjectWin) { + prjDestroyWindow(sAc, sProjectWin); + sProjectWin = NULL; + } + + closeProject(); + + // Don't destroy win here -- the shell manages it. Destroying + // it from inside onClose crashes because the calling code in + // dvxApp.c still references the window after the callback returns. + sWin = NULL; if (sCachedModule) { basModuleFree(sCachedModule); @@ -1085,6 +2042,38 @@ static void onMenu(WindowT *win, int32_t menuId) { saveFile(); break; + case CMD_SAVE_ALL: + saveFile(); + + // Save all non-active files from their buffers + for (int32_t i = 0; i < sProject.fileCount; i++) { + if (i == sProject.activeFileIdx) { + continue; + } + + if (sProject.files[i].modified && sProject.files[i].buffer) { + char fullPath[DVX_MAX_PATH]; + prjFullPath(&sProject, i, fullPath, sizeof(fullPath)); + + FILE *f = fopen(fullPath, "w"); + + if (f) { + fputs(sProject.files[i].buffer, f); + fclose(f); + sProject.files[i].modified = false; + } + } + } + + // Save the project file + if (sProject.projectPath[0] != '\0') { + prjSave(&sProject); + sProject.dirty = false; + } + + setStatus("All files saved."); + break; + case CMD_RUN: compileAndRun(); break; @@ -1127,12 +2116,20 @@ static void onMenu(WindowT *win, int32_t menuId) { case CMD_WIN_TOOLBOX: if (!sToolboxWin) { sToolboxWin = tbxCreate(sAc, &sDesigner); + + if (sToolboxWin) { + sToolboxWin->y = toolbarBottom(); + } } break; case CMD_WIN_PROPS: if (!sPropsWin) { sPropsWin = prpCreate(sAc, &sDesigner); + + if (sPropsWin) { + sPropsWin->y = toolbarBottom(); + } } break; @@ -1172,8 +2169,8 @@ static void onMenu(WindowT *win, int32_t menuId) { bool show = wmMenuItemIsChecked(sWin->menuBar, CMD_VIEW_TOOLBAR); sToolbar->visible = show; dvxFitWindowH(sAc, sWin); - prefsSetBool("view", "toolbar", show); - prefsSave(); + prefsSetBool(sPrefs, "view", "toolbar", show); + prefsSave(sPrefs); } break; @@ -1182,8 +2179,84 @@ static void onMenu(WindowT *win, int32_t menuId) { bool show = wmMenuItemIsChecked(sWin->menuBar, CMD_VIEW_STATUS); sStatusBar->visible = show; dvxFitWindowH(sAc, sWin); - prefsSetBool("view", "statusbar", show); - prefsSave(); + prefsSetBool(sPrefs, "view", "statusbar", show); + prefsSave(sPrefs); + } + break; + + case CMD_SAVE_ON_RUN: + if (sWin && sWin->menuBar) { + bool save = wmMenuItemIsChecked(sWin->menuBar, CMD_SAVE_ON_RUN); + prefsSetBool(sPrefs, "run", "saveOnRun", save); + prefsSave(sPrefs); + } + break; + + case CMD_PRJ_NEW: + newProject(); + break; + + case CMD_PRJ_OPEN: + openProject(); + break; + + case CMD_PRJ_SAVE: + if (sProject.projectPath[0] != '\0') { + prjSave(&sProject); + sProject.dirty = false; + setStatus("Project saved."); + } + break; + + case CMD_PRJ_CLOSE: + if (promptAndSave()) { + closeProject(); + } + break; + + case CMD_PRJ_PROPS: + if (sProject.projectPath[0] != '\0') { + if (prjPropertiesDialog(sAc, &sProject, sCtx->appPath)) { + char title[300]; + snprintf(title, sizeof(title), "DVX BASIC - [%s]", sProject.name); + dvxSetTitle(sAc, sWin, title); + + if (sProjectWin) { + prjRebuildTree(&sProject); + } + } + } + break; + + case CMD_PRJ_REMOVE: + if (sProject.activeFileIdx >= 0) { + PrjFileT *rmFile = &sProject.files[sProject.activeFileIdx]; + + if (rmFile->modified) { + int32_t result = dvxPromptSave(sAc, "DVX BASIC"); + + if (result == DVX_SAVE_CANCEL) { + break; + } + + if (result == DVX_SAVE_YES) { + saveActiveFile(); + } + } + + prjRemoveFile(&sProject, sProject.activeFileIdx); + prjRebuildTree(&sProject); + } + break; + + case CMD_WIN_PROJECT: + if (!sProjectWin) { + sProjectWin = prjCreateWindow(sAc, &sProject, onPrjFileClick); + + if (sProjectWin) { + sProjectWin->y = toolbarBottom() + 25; + sProjectWin->onClose = onProjectWinClose; + } } break; @@ -1358,7 +2431,7 @@ static void printCallback(void *ctx, const char *text, bool newline) { // Update the output textarea immediately so PRINT is visible if (sOutput) { - wgtSetText(sOutput, sOutputBuf); + setOutputText(sOutputBuf); } } @@ -1531,8 +2604,9 @@ static void onFormWinPaint(WindowT *win, RectT *dirtyArea) { // onFormWinClose // ============================================================ -static void onFormWinClose(WindowT *win) { - dvxDestroyWindow(sAc, win); +// cleanupFormWin -- release designer-related state without destroying +// the form window itself (the caller handles that). +static void cleanupFormWin(void) { sFormWin = NULL; sDesigner.formWin = NULL; @@ -1547,6 +2621,26 @@ static void onFormWinClose(WindowT *win) { } } +// onFormWinResize -- update form dimensions when the design window is resized +static void onFormWinResize(WindowT *win, int32_t newW, int32_t newH) { + // Let the widget system handle the layout recalculation + widgetOnResize(win, newW, newH); + + if (sDesigner.form) { + sDesigner.form->width = newW; + sDesigner.form->height = newH; + sDesigner.form->dirty = true; + prpRefresh(&sDesigner); + } +} + + +// onFormWinClose -- shell callback when user clicks X on the form window. +static void onFormWinClose(WindowT *win) { + dvxDestroyWindow(sAc, win); + cleanupFormWin(); +} + // ============================================================ // switchToCode @@ -1554,7 +2648,8 @@ static void onFormWinClose(WindowT *win) { static void switchToCode(void) { if (sFormWin) { - onFormWinClose(sFormWin); + dvxDestroyWindow(sAc, sFormWin); + cleanupFormWin(); } setStatus("Code view."); @@ -1641,6 +2736,7 @@ static void switchToDesign(void) { sFormWin->onPaint = onFormWinPaint; sFormWin->onMouse = onFormWinMouse; sFormWin->onKey = onFormWinKey; + sFormWin->onResize = onFormWinResize; sFormWin->onCursorQuery = onFormWinCursorQuery; // Create live widgets for each control @@ -1665,10 +2761,18 @@ static void switchToDesign(void) { // Create toolbox and properties windows if (!sToolboxWin) { sToolboxWin = tbxCreate(sAc, &sDesigner); + + if (sToolboxWin) { + sToolboxWin->y = toolbarBottom(); + } } if (!sPropsWin) { sPropsWin = prpCreate(sAc, &sDesigner); + + if (sPropsWin) { + sPropsWin->y = toolbarBottom(); + } } setStatus("Design view open."); @@ -1761,7 +2865,7 @@ static void showOutputWindow(void) { sOutput->readOnly = true; if (sOutputLen > 0) { - wgtSetText(sOutput, sOutputBuf); + setOutputText(sOutputBuf); } } } diff --git a/apps/dvxbasic/ide/ideProject.c b/apps/dvxbasic/ide/ideProject.c new file mode 100644 index 0000000..02f2bed --- /dev/null +++ b/apps/dvxbasic/ide/ideProject.c @@ -0,0 +1,853 @@ +// ideProject.c -- DVX BASIC project file management and project window +// +// The .dbp (DVX BASIC Project) file is INI-format: +// +// [Project] +// Name = MyProject +// +// [Modules] +// Count = 2 +// File0 = MAIN.BAS +// File1 = UTILS.BAS +// +// [Forms] +// Count = 1 +// File0 = FORM1.FRM +// +// [Settings] +// StartupForm = Form1 +// +// All file paths are relative to the directory containing the .dbp file. +// Uses the handle-based dvxPrefs API with a dedicated handle per load/save +// so project files don't interfere with the IDE's own preferences. + +#include "ideProject.h" +#include "dvxApp.h" +#include "dvxDialog.h" +#include "dvxPrefs.h" +#include "dvxWm.h" +#include "widgetBox.h" +#include "widgetButton.h" +#include "widgetImage.h" +#include "widgetLabel.h" +#include "widgetTextInput.h" +#include "widgetTreeView.h" + +#include "thirdparty/stb_ds_wrap.h" + +#include +#include +#include +#include + +// ============================================================ +// Constants +// ============================================================ + +#define PRJ_WIN_W 180 +#define PRJ_WIN_H 300 +#define PRJ_MAX_FILES 256 + +// ============================================================ +// Module state +// ============================================================ + +static PrjStateT *sPrj = NULL; +static WindowT *sPrjWin = NULL; +static WidgetT *sTree = NULL; +static PrjFileClickFnT sOnClick = NULL; +static char **sLabels = NULL; // stb_ds array of strdup'd strings + +// ============================================================ +// Prototypes +// ============================================================ + +static void onPrjWinClose(WindowT *win); +static void onTreeItemClick(WidgetT *w); + +// ============================================================ +// prjInit +// ============================================================ + +void prjInit(PrjStateT *prj) { + memset(prj, 0, sizeof(*prj)); + prj->activeFileIdx = -1; +} + + +// ============================================================ +// prjClose +// ============================================================ + +void prjClose(PrjStateT *prj) { + for (int32_t i = 0; i < prj->fileCount; i++) { + free(prj->files[i].buffer); + } + + arrfree(prj->files); + arrfree(prj->sourceMap); + prjInit(prj); +} + + +// ============================================================ +// prjLoad +// ============================================================ + +bool prjLoad(PrjStateT *prj, const char *dbpPath) { + PrefsHandleT *h = prefsLoad(dbpPath); + + if (!h) { + return false; + } + + prjInit(prj); + snprintf(prj->projectPath, sizeof(prj->projectPath), "%s", dbpPath); + + // Derive project directory + snprintf(prj->projectDir, sizeof(prj->projectDir), "%s", dbpPath); + char *sep = strrchr(prj->projectDir, '/'); + char *sep2 = strrchr(prj->projectDir, '\\'); + + if (sep2 > sep) { + sep = sep2; + } + + if (sep) { + *sep = '\0'; + } else { + prj->projectDir[0] = '.'; + prj->projectDir[1] = '\0'; + } + + // [Project] section + const char *val; + + val = prefsGetString(h, "Project", "Name", NULL); + if (val) { snprintf(prj->name, sizeof(prj->name), "%s", val); } + + val = prefsGetString(h, "Project", "Author", NULL); + if (val) { snprintf(prj->author, sizeof(prj->author), "%s", val); } + + val = prefsGetString(h, "Project", "Company", NULL); + if (val) { snprintf(prj->company, sizeof(prj->company), "%s", val); } + + val = prefsGetString(h, "Project", "Version", NULL); + if (val) { snprintf(prj->version, sizeof(prj->version), "%s", val); } + + val = prefsGetString(h, "Project", "Copyright", NULL); + if (val) { snprintf(prj->copyright, sizeof(prj->copyright), "%s", val); } + + val = prefsGetString(h, "Project", "Description", NULL); + if (val) { snprintf(prj->description, sizeof(prj->description), "%s", val); } + + val = prefsGetString(h, "Project", "Icon", NULL); + if (val) { snprintf(prj->iconPath, sizeof(prj->iconPath), "%s", val); } + + // [Modules] section -- File0, File1, ... + for (int32_t i = 0; i < PRJ_MAX_FILES; i++) { + char key[16]; + snprintf(key, sizeof(key), "File%d", (int)i); + val = prefsGetString(h, "Modules", key, NULL); + + if (!val) { + break; + } + + prjAddFile(prj, val, false); + } + + // [Forms] section -- File0, File1, ... + for (int32_t i = 0; i < PRJ_MAX_FILES; i++) { + char key[16]; + snprintf(key, sizeof(key), "File%d", (int)i); + val = prefsGetString(h, "Forms", key, NULL); + + if (!val) { + break; + } + + prjAddFile(prj, val, true); + } + + // [Settings] section + val = prefsGetString(h, "Settings", "StartupForm", NULL); + if (val) { snprintf(prj->startupForm, sizeof(prj->startupForm), "%s", val); } + + prefsClose(h); + prj->dirty = false; + return true; +} + + +// ============================================================ +// prjSave +// ============================================================ + +bool prjSave(const PrjStateT *prj) { + if (prj->projectPath[0] == '\0') { + return false; + } + + PrefsHandleT *h = prefsCreate(); + + if (!h) { + return false; + } + + // [Project] section + prefsSetString(h, "Project", "Name", prj->name); + + if (prj->author[0]) { prefsSetString(h, "Project", "Author", prj->author); } + if (prj->company[0]) { prefsSetString(h, "Project", "Company", prj->company); } + if (prj->version[0]) { prefsSetString(h, "Project", "Version", prj->version); } + if (prj->copyright[0]) { prefsSetString(h, "Project", "Copyright", prj->copyright); } + if (prj->description[0]) { prefsSetString(h, "Project", "Description", prj->description); } + if (prj->iconPath[0]) { prefsSetString(h, "Project", "Icon", prj->iconPath); } + + // [Modules] section + int32_t modIdx = 0; + + for (int32_t i = 0; i < prj->fileCount; i++) { + if (!prj->files[i].isForm) { + char key[16]; + snprintf(key, sizeof(key), "File%d", (int)modIdx++); + prefsSetString(h, "Modules", key, prj->files[i].path); + } + } + + prefsSetInt(h, "Modules", "Count", modIdx); + + // [Forms] section + int32_t frmIdx = 0; + + for (int32_t i = 0; i < prj->fileCount; i++) { + if (prj->files[i].isForm) { + char key[16]; + snprintf(key, sizeof(key), "File%d", (int)frmIdx++); + prefsSetString(h, "Forms", key, prj->files[i].path); + } + } + + prefsSetInt(h, "Forms", "Count", frmIdx); + + // [Settings] section + prefsSetString(h, "Settings", "StartupForm", prj->startupForm); + + bool ok = prefsSaveAs(h, prj->projectPath); + prefsClose(h); + return ok; +} + + +// ============================================================ +// prjSaveAs +// ============================================================ + +bool prjSaveAs(PrjStateT *prj, const char *dbpPath) { + snprintf(prj->projectPath, sizeof(prj->projectPath), "%s", dbpPath); + + // Update project directory + snprintf(prj->projectDir, sizeof(prj->projectDir), "%s", dbpPath); + char *sep = strrchr(prj->projectDir, '/'); + char *sep2 = strrchr(prj->projectDir, '\\'); + + if (sep2 > sep) { + sep = sep2; + } + + if (sep) { + *sep = '\0'; + } else { + prj->projectDir[0] = '.'; + prj->projectDir[1] = '\0'; + } + + return prjSave(prj); +} + + +// ============================================================ +// prjNew +// ============================================================ + +void prjNew(PrjStateT *prj, const char *name, const char *directory) { + prjInit(prj); + snprintf(prj->name, sizeof(prj->name), "%s", name); + snprintf(prj->projectDir, sizeof(prj->projectDir), "%s", directory); + snprintf(prj->projectPath, sizeof(prj->projectPath), "%s/%s.dbp", directory, name); + prj->dirty = true; +} + + +// ============================================================ +// prjAddFile +// ============================================================ + +int32_t prjAddFile(PrjStateT *prj, const char *relativePath, bool isForm) { + PrjFileT entry; + memset(&entry, 0, sizeof(entry)); + snprintf(entry.path, sizeof(entry.path), "%s", relativePath); + entry.isForm = isForm; + arrput(prj->files, entry); + prj->fileCount = (int32_t)arrlen(prj->files); + prj->dirty = true; + return prj->fileCount - 1; +} + + +// ============================================================ +// prjRemoveFile +// ============================================================ + +void prjRemoveFile(PrjStateT *prj, int32_t idx) { + if (idx < 0 || idx >= prj->fileCount) { + return; + } + + free(prj->files[idx].buffer); + arrdel(prj->files, idx); + prj->fileCount = (int32_t)arrlen(prj->files); + + // Adjust active file index + if (prj->activeFileIdx == idx) { + prj->activeFileIdx = -1; + } else if (prj->activeFileIdx > idx) { + prj->activeFileIdx--; + } + + prj->dirty = true; +} + + +// ============================================================ +// prjFullPath +// ============================================================ + +void prjFullPath(const PrjStateT *prj, int32_t fileIdx, char *outPath, int32_t outSize) { + if (fileIdx < 0 || fileIdx >= prj->fileCount) { + outPath[0] = '\0'; + return; + } + + snprintf(outPath, outSize, "%s/%s", prj->projectDir, prj->files[fileIdx].path); +} + + +// ============================================================ +// prjMapLine +// ============================================================ + +bool prjMapLine(const PrjStateT *prj, int32_t concatLine, int32_t *outFileIdx, int32_t *outLocalLine) { + for (int32_t i = 0; i < prj->sourceMapCount; i++) { + const PrjSourceMapT *m = &prj->sourceMap[i]; + + if (concatLine >= m->startLine && concatLine < m->startLine + m->lineCount) { + *outFileIdx = m->fileIdx; + *outLocalLine = concatLine - m->startLine + 1; + return true; + } + } + + return false; +} + + +// ============================================================ +// Project window callbacks +// ============================================================ + +static void onPrjWinClose(WindowT *win) { + (void)win; +} + + +static void onTreeItemClick(WidgetT *w) { + if (!sPrj || !sOnClick) { + return; + } + + int32_t fileIdx = (int32_t)(intptr_t)w->userData; + + if (fileIdx >= 0 && fileIdx < sPrj->fileCount) { + sOnClick(fileIdx, sPrj->files[fileIdx].isForm); + } +} + + +// ============================================================ +// prjCreateWindow +// ============================================================ + +WindowT *prjCreateWindow(AppContextT *ctx, PrjStateT *prj, PrjFileClickFnT onClick) { + sPrj = prj; + sOnClick = onClick; + + sPrjWin = dvxCreateWindow(ctx, "Project", 0, 250, PRJ_WIN_W, PRJ_WIN_H, true); + + if (!sPrjWin) { + return NULL; + } + + sPrjWin->onClose = onPrjWinClose; + + WidgetT *root = wgtInitWindow(ctx, sPrjWin); + sTree = wgtTreeView(root); + sTree->weight = 100; + + prjRebuildTree(prj); + return sPrjWin; +} + + +// ============================================================ +// prjDestroyWindow +// ============================================================ + +void prjDestroyWindow(AppContextT *ctx, WindowT *win) { + if (win) { + dvxDestroyWindow(ctx, win); + } + + // Free label strings + if (sLabels) { + for (int32_t i = 0; i < (int32_t)arrlen(sLabels); i++) { + free(sLabels[i]); + } + + arrfree(sLabels); + sLabels = NULL; + } + + sPrjWin = NULL; + sTree = NULL; + sPrj = NULL; + sOnClick = NULL; +} + + +// ============================================================ +// prjRebuildTree +// ============================================================ + +void prjRebuildTree(PrjStateT *prj) { + if (!sTree) { + return; + } + + // Clear existing items by removing all children + sTree->firstChild = NULL; + sTree->lastChild = NULL; + + // Free old labels + if (sLabels) { + for (int32_t i = 0; i < (int32_t)arrlen(sLabels); i++) { + free(sLabels[i]); + } + + arrfree(sLabels); + sLabels = NULL; + } + + if (!prj || prj->fileCount == 0) { + return; + } + + // Project name as root + char *projLabel = strdup(prj->name[0] ? prj->name : "Project"); + arrput(sLabels, projLabel); + WidgetT *projNode = wgtTreeItem(sTree, projLabel); + projNode->userData = (void *)(intptr_t)-1; + wgtTreeItemSetExpanded(projNode, true); + + // Forms group + 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 *label = strdup(prj->files[i].path); + arrput(sLabels, label); + WidgetT *item = wgtTreeItem(formsNode, label); + item->userData = (void *)(intptr_t)i; + item->onClick = onTreeItemClick; + } + } + + // Modules group + char *modsLabel = strdup("Modules"); + arrput(sLabels, modsLabel); + WidgetT *modsNode = wgtTreeItem(projNode, modsLabel); + modsNode->userData = (void *)(intptr_t)-1; + wgtTreeItemSetExpanded(modsNode, true); + + for (int32_t i = 0; i < prj->fileCount; i++) { + if (!prj->files[i].isForm) { + char *label = strdup(prj->files[i].path); + arrput(sLabels, label); + WidgetT *item = wgtTreeItem(modsNode, label); + item->userData = (void *)(intptr_t)i; + item->onClick = onTreeItemClick; + } + } + + wgtInvalidate(sTree); +} + + +// ============================================================ +// Project properties dialog +// ============================================================ + +#define PPD_WIDTH 380 +#define PPD_LABEL_W 96 +#define PPD_BTN_W 70 +#define PPD_BTN_H 24 +#define PPD_DESC_H 60 + +static struct { + bool done; + bool accepted; + WidgetT *name; + WidgetT *author; + WidgetT *company; + WidgetT *version; + WidgetT *copyright; + WidgetT *description; + WidgetT *iconPreview; + char iconPath[DVX_MAX_PATH]; + const char *appPath; + AppContextT *ctx; + PrjStateT *prj; +} sPpd; + +static void ppdOnOk(WidgetT *w) { + (void)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); + + 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); + return; + } + } + + sPpd.accepted = true; + sPpd.done = true; +} +static void ppdOnCancel(WidgetT *w) { (void)w; sPpd.accepted = false; sPpd.done = true; } +static void ppdOnClose(WindowT *win) { (void)win; sPpd.accepted = false; sPpd.done = true; } + + +static void ppdLoadIconPreview(void) { + if (!sPpd.iconPreview || !sPpd.ctx || !sPpd.prj) { + return; + } + + if (!sPpd.iconPath[0]) { + return; + } + + const char *relPath = sPpd.iconPath; + + 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); + sPpd.iconPath[0] = '\0'; + return; + } + + int32_t w = 0; + int32_t h = 0; + int32_t pitch = 0; + uint8_t *data = dvxLoadImage(sPpd.ctx, fullPath, &w, &h, &pitch); + + if (data) { + wgtImageSetData(sPpd.iconPreview, data, w, h, pitch); + } +} + + +static void ppdOnBrowseIcon(WidgetT *w) { + (void)w; + + FileFilterT filters[] = { + { "Images (*.bmp;*.png;*.jpg;*.gif)", "*.bmp;*.png;*.jpg;*.gif" }, + { "All Files (*.*)", "*.*" } + }; + + 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); + return; + } + + // The icon must be in the project directory so the relative + // path works when the project is reloaded. + const char *relPath = NULL; + int32_t dirLen = (int32_t)strlen(sPpd.prj->projectDir); + + if (strncasecmp(path, sPpd.prj->projectDir, dirLen) == 0 && + (path[dirLen] == '/' || path[dirLen] == '\\')) { + relPath = path + dirLen + 1; + } + + if (!relPath) { + int32_t result = dvxMessageBox(sPpd.ctx, "Copy Icon", + "The icon is outside the project directory.\nCopy it to the project?", + MB_YESNO | MB_ICONQUESTION); + + if (result != ID_YES) { + return; + } + + // Get just the filename + const char *fname = strrchr(path, '/'); + const char *fname2 = strrchr(path, '\\'); + + if (fname2 > fname) { + fname = fname2; + } + + fname = fname ? fname + 1 : path; + + // Check if destination already exists + char destPath[DVX_MAX_PATH * 2]; + snprintf(destPath, sizeof(destPath), "%s/%s", sPpd.prj->projectDir, fname); + + FILE *existing = fopen(destPath, "rb"); + + if (existing) { + fclose(existing); + + char msg[DVX_MAX_PATH + 32]; + snprintf(msg, sizeof(msg), "%s already exists.\nOverwrite it?", fname); + int32_t ow = dvxMessageBox(sPpd.ctx, "Overwrite", msg, MB_YESNO | MB_ICONQUESTION); + + if (ow != ID_YES) { + return; + } + } + + // Copy the file + FILE *src = fopen(path, "rb"); + + if (!src) { + dvxMessageBox(sPpd.ctx, "Error", "Could not read source file.", MB_OK | MB_ICONERROR); + return; + } + + FILE *dst = fopen(destPath, "wb"); + + if (!dst) { + fclose(src); + dvxMessageBox(sPpd.ctx, "Error", "Could not write to project directory.", MB_OK | MB_ICONERROR); + return; + } + + char buf[4096]; + size_t n; + + while ((n = fread(buf, 1, sizeof(buf), src)) > 0) { + fwrite(buf, 1, n, dst); + } + + fclose(src); + fclose(dst); + + relPath = fname; + } + + snprintf(sPpd.iconPath, sizeof(sPpd.iconPath), "%s", relPath); + ppdLoadIconPreview(); + } +} + + +static WidgetT *ppdAddRow(WidgetT *parent, const char *labelText, const char *value, int32_t maxLen) { + WidgetT *row = wgtHBox(parent); + row->spacing = wgtPixels(4); + + WidgetT *lbl = wgtLabel(row, labelText); + lbl->minW = wgtPixels(PPD_LABEL_W); + + WidgetT *input = wgtTextInput(row, maxLen); + input->weight = 100; + wgtSetText(input, value); + + return input; +} + + +bool prjPropertiesDialog(AppContextT *ctx, PrjStateT *prj, const char *appPath) { + if (!ctx || !prj) { + return false; + } + + WindowT *win = dvxCreateWindowCentered(ctx, "Project Properties", PPD_WIDTH, 380, false); + + if (!win) { + return false; + } + + win->modal = true; + win->onClose = ppdOnClose; + win->maxW = win->w; + win->maxH = win->h; + + sPpd.done = false; + sPpd.accepted = false; + sPpd.ctx = ctx; + sPpd.prj = prj; + sPpd.appPath = appPath; + + WidgetT *root = wgtInitWindow(ctx, win); + + if (!root) { + dvxDestroyWindow(ctx, win); + return false; + } + + root->spacing = wgtPixels(2); + + sPpd.name = ppdAddRow(root, "Name:", prj->name, PRJ_MAX_NAME); + sPpd.author = ppdAddRow(root, "Author:", prj->author, PRJ_MAX_STRING); + 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); + // Icon row: label + preview + Browse button + { + WidgetT *iconRow = wgtHBox(root); + iconRow->spacing = wgtPixels(4); + + WidgetT *iconLbl = wgtLabel(iconRow, "Icon:"); + iconLbl->minW = wgtPixels(PPD_LABEL_W); + + // Load "noicon" placeholder from app resources + int32_t niW = 0; + int32_t niH = 0; + int32_t niP = 0; + uint8_t *noIconData = appPath ? dvxResLoadIcon(ctx, appPath, "noicon", &niW, &niH, &niP) : NULL; + + if (noIconData) { + sPpd.iconPreview = wgtImage(iconRow, noIconData, niW, niH, niP); + } else { + uint8_t *placeholder = (uint8_t *)calloc(4, 1); + sPpd.iconPreview = wgtImage(iconRow, placeholder, 1, 1, 4); + } + + WidgetT *browseBtn = wgtButton(iconRow, "Browse..."); + browseBtn->onClick = ppdOnBrowseIcon; + + snprintf(sPpd.iconPath, sizeof(sPpd.iconPath), "%s", prj->iconPath); + ppdLoadIconPreview(); + } + + // Description gets a taller text area + WidgetT *descRow = wgtHBox(root); + descRow->spacing = wgtPixels(4); + + WidgetT *descLbl = wgtLabel(descRow, "Description:"); + descLbl->minW = wgtPixels(PPD_LABEL_W); + + sPpd.description = wgtTextArea(descRow, PRJ_MAX_DESC); + sPpd.description->weight = 100; + sPpd.description->minH = wgtPixels(PPD_DESC_H); + wgtSetText(sPpd.description, prj->description); + + // OK / Cancel buttons + WidgetT *btnRow = wgtHBox(root); + btnRow->align = AlignCenterE; + + WidgetT *okBtn = wgtButton(btnRow, "&OK"); + okBtn->minW = wgtPixels(PPD_BTN_W); + okBtn->minH = wgtPixels(PPD_BTN_H); + okBtn->onClick = ppdOnOk; + + WidgetT *cancelBtn = wgtButton(btnRow, "&Cancel"); + cancelBtn->minW = wgtPixels(PPD_BTN_W); + cancelBtn->minH = wgtPixels(PPD_BTN_H); + cancelBtn->onClick = ppdOnCancel; + + dvxFitWindow(ctx, win); + + WindowT *prevModal = ctx->modalWindow; + ctx->modalWindow = win; + + while (!sPpd.done && ctx->running) { + dvxUpdate(ctx); + } + + if (sPpd.accepted) { + const char *s; + + s = wgtGetText(sPpd.name); + if (s) { snprintf(prj->name, sizeof(prj->name), "%s", s); } + + s = wgtGetText(sPpd.author); + if (s) { snprintf(prj->author, sizeof(prj->author), "%s", s); } + + s = wgtGetText(sPpd.company); + if (s) { snprintf(prj->company, sizeof(prj->company), "%s", s); } + + s = wgtGetText(sPpd.version); + if (s) { snprintf(prj->version, sizeof(prj->version), "%s", s); } + + s = wgtGetText(sPpd.copyright); + if (s) { snprintf(prj->copyright, sizeof(prj->copyright), "%s", s); } + + s = wgtGetText(sPpd.description); + if (s) { snprintf(prj->description, sizeof(prj->description), "%s", s); } + + snprintf(prj->iconPath, sizeof(prj->iconPath), "%s", sPpd.iconPath); + + prj->dirty = true; + } + + ctx->modalWindow = prevModal; + dvxDestroyWindow(ctx, win); + + return sPpd.accepted; +} diff --git a/apps/dvxbasic/ide/ideProject.h b/apps/dvxbasic/ide/ideProject.h new file mode 100644 index 0000000..f64fd25 --- /dev/null +++ b/apps/dvxbasic/ide/ideProject.h @@ -0,0 +1,105 @@ +// ideProject.h -- DVX BASIC project file management and project window + +#ifndef IDE_PROJECT_H +#define IDE_PROJECT_H + +#include "dvxApp.h" +#include "dvxTypes.h" + +#include +#include + +// ============================================================ +// Constants +// ============================================================ + +#define PRJ_MAX_NAME 32 +#define PRJ_MAX_STRING 128 +#define PRJ_MAX_DESC 512 + +// ============================================================ +// Project file entry +// ============================================================ + +typedef struct { + char path[DVX_MAX_PATH]; // relative path (8.3 DOS name) + bool isForm; // true = .frm, false = .bas + char *buffer; // in-memory edit buffer (malloc'd, NULL = not loaded) + bool modified; // true = buffer has unsaved changes +} PrjFileT; + +// ============================================================ +// Source map entry (for multi-file error reporting) +// ============================================================ + +typedef struct { + int32_t startLine; // 1-based line in concatenated source + int32_t lineCount; // lines contributed by this file + int32_t fileIdx; // index in PrjStateT.files[] +} PrjSourceMapT; + +// ============================================================ +// Project state +// ============================================================ + +typedef struct { + char name[PRJ_MAX_NAME]; + char projectPath[DVX_MAX_PATH]; // full path to .dbp file + char projectDir[DVX_MAX_PATH]; // directory containing .dbp + char startupForm[PRJ_MAX_NAME]; + // Project metadata (for binary generation) + char author[PRJ_MAX_STRING]; + char company[PRJ_MAX_STRING]; + char version[PRJ_MAX_NAME]; + char copyright[PRJ_MAX_STRING]; + char description[PRJ_MAX_DESC]; + char iconPath[DVX_MAX_PATH]; // relative path to icon BMP + PrjFileT *files; // stb_ds dynamic array + int32_t fileCount; + PrjSourceMapT *sourceMap; // stb_ds dynamic array + int32_t sourceMapCount; + bool dirty; + int32_t activeFileIdx; // index of file open in editor (-1 = none) +} PrjStateT; + +// ============================================================ +// Project management +// ============================================================ + +void prjInit(PrjStateT *prj); +bool prjLoad(PrjStateT *prj, const char *dbpPath); +bool prjSave(const PrjStateT *prj); +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 prjRemoveFile(PrjStateT *prj, int32_t idx); +void prjFullPath(const PrjStateT *prj, int32_t fileIdx, char *outPath, int32_t outSize); + +// ============================================================ +// Source map -- translate concatenated line to file + local line +// ============================================================ + +// Returns true if the line was found in the map. Sets outFileIdx and +// outLocalLine to the originating file and line within that file. +bool prjMapLine(const PrjStateT *prj, int32_t concatLine, int32_t *outFileIdx, int32_t *outLocalLine); + +// ============================================================ +// Project window UI +// ============================================================ + +typedef void (*PrjFileClickFnT)(int32_t fileIdx, bool isForm); + +WindowT *prjCreateWindow(AppContextT *ctx, PrjStateT *prj, PrjFileClickFnT onClick); +void prjDestroyWindow(AppContextT *ctx, WindowT *win); +void prjRebuildTree(PrjStateT *prj); + +// ============================================================ +// Project properties dialog +// ============================================================ + +// Show a modal dialog for editing project metadata. Returns true if +// the user clicked OK (fields in prj are updated), false if cancelled. +bool prjPropertiesDialog(AppContextT *ctx, PrjStateT *prj, const char *appPath); + +#endif // IDE_PROJECT_H diff --git a/apps/dvxbasic/ide/ideProperties.c b/apps/dvxbasic/ide/ideProperties.c index 79e9ce5..70d45be 100644 --- a/apps/dvxbasic/ide/ideProperties.c +++ b/apps/dvxbasic/ide/ideProperties.c @@ -288,12 +288,15 @@ static void onPropDblClick(WidgetT *w) { DsgnControlT *ctrl = &sDs->form->controls[sDs->selectedIdx]; if (strcasecmp(propName, "Name") == 0) { + char oldName[DSGN_MAX_NAME]; + snprintf(oldName, sizeof(oldName), "%s", ctrl->name); snprintf(ctrl->name, DSGN_MAX_NAME, "%.31s", newValue); if (ctrl->widget) { wgtSetName(ctrl->widget, ctrl->name); } + ideRenameInCode(oldName, ctrl->name); prpRebuildTree(sDs); } else if (strcasecmp(propName, "MinWidth") == 0) { ctrl->width = atoi(newValue); @@ -433,7 +436,23 @@ static void onPropDblClick(WidgetT *w) { dvxInvalidateWindow(sPrpCtx, sDs->formWin); } } else { - if (strcasecmp(propName, "Caption") == 0) { + if (strcasecmp(propName, "Name") == 0) { + char oldName[DSGN_MAX_NAME]; + snprintf(oldName, sizeof(oldName), "%s", sDs->form->name); + + // Length-clamped memcpy instead of strncpy/snprintf because + // GCC warns about both when source exceeds the buffer. + int32_t nl = (int32_t)strlen(newValue); + + if (nl >= DSGN_MAX_NAME) { + nl = DSGN_MAX_NAME - 1; + } + + memcpy(sDs->form->name, newValue, nl); + sDs->form->name[nl] = '\0'; + ideRenameInCode(oldName, sDs->form->name); + prpRebuildTree(sDs); + } else if (strcasecmp(propName, "Caption") == 0) { snprintf(sDs->form->caption, DSGN_MAX_TEXT, "%s", newValue); if (sDs->formWin) { diff --git a/apps/dvxbasic/noicon.bmp b/apps/dvxbasic/noicon.bmp new file mode 100644 index 0000000..2ff3d44 --- /dev/null +++ b/apps/dvxbasic/noicon.bmp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e80c12d82bec065b279d219f657e0475ebbcf55dce2bb3b916aa202671b37032 +size 3126 diff --git a/apps/dvxbasic/samples/ICON32.BMP b/apps/dvxbasic/samples/ICON32.BMP new file mode 100644 index 0000000..7fe939d --- /dev/null +++ b/apps/dvxbasic/samples/ICON32.BMP @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3358a4f7b545b0eb14a8c3371a5d2777fc4ad59b0b7a0956e0376364c1b20d23 +size 3126 diff --git a/apps/dvxbasic/samples/MULTI.DBP b/apps/dvxbasic/samples/MULTI.DBP new file mode 100644 index 0000000..62cee7b --- /dev/null +++ b/apps/dvxbasic/samples/MULTI.DBP @@ -0,0 +1,18 @@ +[Project] +Name = Multi Form Test 1 +Author = Scott Duensing +Company = Kangaroo Punch Studios +Version = 1.00 +Copyright = Copyright 2026 Scott Duensing +Description = Testing properties. +Icon = icon32.bmp + +[Modules] +Count = 0 + +[Forms] +File0 = multi1.frm +Count = 1 + +[Settings] +StartupForm = diff --git a/apps/dvxbasic/samples/MULTI1.FRM b/apps/dvxbasic/samples/MULTI1.FRM new file mode 100644 index 0000000..a385540 --- /dev/null +++ b/apps/dvxbasic/samples/MULTI1.FRM @@ -0,0 +1,10 @@ +VERSION 1.00 +Begin Form multi1 + Caption = "Welcome to DVX BASIC!" + Layout = VBox + AutoSize = False + Resizable = True + Centered = True + Width = 400 + Height = 300 +End diff --git a/apps/notepad/notepad.c b/apps/notepad/notepad.c index 7faefd0..e374e26 100644 --- a/apps/notepad/notepad.c +++ b/apps/notepad/notepad.c @@ -69,7 +69,6 @@ static void doNew(void); static void doOpen(void); static void doSave(void); static void doSaveAs(void); -static uint32_t hashText(const char *text); static bool isDirty(void); static void markClean(void); static void onClose(WindowT *win); @@ -93,35 +92,18 @@ AppDescriptorT appDescriptor = { // Dirty tracking // ============================================================ -// djb2-xor hash for dirty detection. Not cryptographic -- just a fast way -// to detect changes without storing a full copy of the last-saved text. -// False negatives are theoretically possible but vanishingly unlikely for -// text edits. This avoids the memory cost of keeping a shadow buffer. -static uint32_t hashText(const char *text) { - if (!text) { - return 0; - } - - uint32_t h = 5381; - - while (*text) { - h = ((h << 5) + h) ^ (uint8_t)*text; - text++; - } - - return h; -} +// Dirty tracking uses dvxTextHash from dvxApp.h. static bool isDirty(void) { const char *text = wgtGetText(sTextArea); - return hashText(text) != sCleanHash; + return dvxTextHash(text) != sCleanHash; } static void markClean(void) { const char *text = wgtGetText(sTextArea); - sCleanHash = hashText(text); + sCleanHash = dvxTextHash(text); } diff --git a/apps/progman/progman.c b/apps/progman/progman.c index aabdb26..b2d5c23 100644 --- a/apps/progman/progman.c +++ b/apps/progman/progman.c @@ -98,6 +98,7 @@ static DxeAppContextT *sCtx = NULL; static AppContextT *sAc = NULL; static WindowT *sPmWindow = NULL; static WidgetT *sStatusLabel = NULL; +static PrefsHandleT *sPrefs = NULL; static bool sMinOnRun = false; static AppEntryT sAppFiles[MAX_APP_FILES]; static int32_t sAppCount = 0; @@ -341,8 +342,8 @@ static void onPmMenu(WindowT *win, int32_t menuId) { case CMD_MIN_ON_RUN: sMinOnRun = !sMinOnRun; shellEnsureConfigDir(sCtx); - prefsSetBool("options", "minimizeOnRun", sMinOnRun); - prefsSave(); + prefsSetBool(sPrefs, "options", "minimizeOnRun", sMinOnRun); + prefsSave(sPrefs); break; case CMD_ABOUT: @@ -535,8 +536,8 @@ int32_t appMain(DxeAppContextT *ctx) { // Load saved preferences char prefsPath[DVX_MAX_PATH]; shellConfigPath(sCtx, "progman.ini", prefsPath, sizeof(prefsPath)); - prefsLoad(prefsPath); - sMinOnRun = prefsGetBool("options", "minimizeOnRun", false); + sPrefs = prefsLoad(prefsPath); + sMinOnRun = prefsGetBool(sPrefs, "options", "minimizeOnRun", false); scanAppsDir(); buildPmWindow(); diff --git a/config/dvx.ini b/config/dvx.ini index e674901..09cbcd7 100644 --- a/config/dvx.ini +++ b/config/dvx.ini @@ -5,8 +5,8 @@ ; Supported color depths: 8, 15, 16, 24, 32 [video] -width = 640 -height = 480 +width = 1024 +height = 768 bpp = 16 ; Mouse settings. diff --git a/core/dvxApp.c b/core/dvxApp.c index 696bee4..8770c09 100644 --- a/core/dvxApp.c +++ b/core/dvxApp.c @@ -3783,6 +3783,26 @@ void *dvxResLoadData(const char *dxePath, const char *resName, uint32_t *outSize } +// ============================================================ +// dvxTextHash +// ============================================================ + +uint32_t dvxTextHash(const char *text) { + if (!text) { + return 0; + } + + uint32_t h = 5381; + + while (*text) { + h = ((h << 5) + h) ^ (uint8_t)*text; + text++; + } + + return h; +} + + // ============================================================ // dvxColorLabel // ============================================================ @@ -4057,6 +4077,29 @@ void dvxFreeImage(uint8_t *data) { } +// ============================================================ +// dvxImageInfo +// ============================================================ + +bool dvxImageInfo(const char *path, int32_t *outW, int32_t *outH) { + if (!path) { + return false; + } + + int w = 0; + int h = 0; + int comp = 0; + + if (stbi_info(path, &w, &h, &comp)) { + if (outW) { *outW = w; } + if (outH) { *outH = h; } + return true; + } + + return false; +} + + // ============================================================ // dvxGetBlitOps // ============================================================ diff --git a/core/dvxApp.h b/core/dvxApp.h index ec5f3d8..c761fc9 100644 --- a/core/dvxApp.h +++ b/core/dvxApp.h @@ -300,6 +300,10 @@ uint8_t *dvxLoadImageFromMemory(const AppContextT *ctx, const uint8_t *data, int // Free a pixel buffer returned by dvxLoadImage. void dvxFreeImage(uint8_t *data); +// Query image dimensions without decoding the full file. +// Returns true on success, false if the file can't be read. +bool dvxImageInfo(const char *path, int32_t *outW, int32_t *outH); + // Save native-format pixel data to a PNG file. The pixel data must be // in the display's native format (as returned by dvxLoadImage or // captured from a content buffer). Returns 0 on success, -1 on failure. @@ -338,4 +342,14 @@ bool dvxResLoadText(const char *dxePath, const char *resName, char *buf, int32_t // data size in bytes. void *dvxResLoadData(const char *dxePath, const char *resName, uint32_t *outSize); +// ============================================================ +// Text hash for dirty tracking +// ============================================================ +// +// djb2-xor hash for cheap dirty detection. Compare the hash at save +// time with the current hash to detect changes without keeping a +// shadow copy of the text. Not cryptographic. + +uint32_t dvxTextHash(const char *text); + #endif // DVX_APP_H diff --git a/core/dvxDialog.c b/core/dvxDialog.c index 93b866b..cf92649 100644 --- a/core/dvxDialog.c +++ b/core/dvxDialog.c @@ -1609,3 +1609,23 @@ bool dvxFileDialog(AppContextT *ctx, const char *title, int32_t flags, const cha return result; } + + +// ============================================================ +// dvxPromptSave +// ============================================================ + +int32_t dvxPromptSave(AppContextT *ctx, const char *title) { + int32_t result = dvxMessageBox(ctx, title ? title : "Save", + "Save changes?", MB_YESNOCANCEL | MB_ICONQUESTION); + + if (result == ID_YES) { + return DVX_SAVE_YES; + } + + if (result == ID_NO) { + return DVX_SAVE_NO; + } + + return DVX_SAVE_CANCEL; +} diff --git a/core/dvxDialog.h b/core/dvxDialog.h index c507253..31f41ee 100644 --- a/core/dvxDialog.h +++ b/core/dvxDialog.h @@ -87,4 +87,20 @@ bool dvxInputBox(AppContextT *ctx, const char *title, const char *prompt, const // Returns true if the user clicked OK, false if cancelled. bool dvxIntInputBox(AppContextT *ctx, const char *title, const char *prompt, int32_t defaultVal, int32_t minVal, int32_t maxVal, int32_t step, int32_t *outVal); +// ============================================================ +// Save prompt helper +// ============================================================ +// +// Common "Save changes?" dialog for apps with unsaved data. +// Returns: +// DVX_SAVE_YES -- user wants to save (caller should save, then proceed) +// DVX_SAVE_NO -- user wants to discard (caller should proceed without saving) +// DVX_SAVE_CANCEL -- user cancelled (caller should abort the operation) + +#define DVX_SAVE_YES 1 +#define DVX_SAVE_NO 2 +#define DVX_SAVE_CANCEL 3 + +int32_t dvxPromptSave(AppContextT *ctx, const char *title); + #endif // DVX_DIALOG_H diff --git a/core/dvxPrefs.c b/core/dvxPrefs.c index 25c7ce4..916971a 100644 --- a/core/dvxPrefs.c +++ b/core/dvxPrefs.c @@ -1,8 +1,7 @@ // dvxPrefs.c -- INI-based preferences system (read/write) // -// Custom INI parser and writer. Stores entries as a dynamic array of -// section/key/value triples using stb_ds. Preserves insertion order -// on save so the file remains human-readable. +// Handle-based: each PrefsHandleT holds its own entry array and file +// path. Multiple INI files can be open simultaneously. #include "dvxPrefs.h" @@ -12,7 +11,6 @@ #include #include "dvxMem.h" -// stb_ds dynamic arrays (implementation lives in libtasks.a) #include "thirdparty/stb_ds_wrap.h" @@ -26,12 +24,14 @@ typedef struct { char *value; } PrefsEntryT; -// Comment lines are stored to preserve them on save. A comment has +// Comment lines are stored to preserve them on save. A comment has // key=NULL and value=the full line text (including the ; prefix). // Section headers have key=NULL and value=NULL. -static PrefsEntryT *sEntries = NULL; // stb_ds dynamic array -static char *sFilePath = NULL; // path used by prefsLoad (for prefsSave) +struct PrefsHandleT { + PrefsEntryT *entries; // stb_ds dynamic array + char *filePath; // path used by prefsSave +}; // ============================================================ @@ -64,7 +64,6 @@ static void freeEntry(PrefsEntryT *e) { } -// Case-insensitive string compare static int strcmpci(const char *a, const char *b) { for (;;) { int d = tolower((unsigned char)*a) - tolower((unsigned char)*b); @@ -79,10 +78,9 @@ static int strcmpci(const char *a, const char *b) { } -// Find an entry by section+key (case-insensitive). Returns index or -1. -static int32_t findEntry(const char *section, const char *key) { - for (int32_t i = 0; i < arrlen(sEntries); i++) { - PrefsEntryT *e = &sEntries[i]; +static int32_t findEntry(PrefsHandleT *h, const char *section, const char *key) { + for (int32_t i = 0; i < arrlen(h->entries); i++) { + PrefsEntryT *e = &h->entries[i]; if (e->key && e->section && strcmpci(e->section, section) == 0 && @@ -95,10 +93,9 @@ static int32_t findEntry(const char *section, const char *key) { } -// Find the index of a section header entry. Returns -1 if not found. -static int32_t findSection(const char *section) { - for (int32_t i = 0; i < arrlen(sEntries); i++) { - PrefsEntryT *e = &sEntries[i]; +static int32_t findSection(PrefsHandleT *h, const char *section) { + for (int32_t i = 0; i < arrlen(h->entries); i++) { + PrefsEntryT *e = &h->entries[i]; if (!e->key && !e->value && e->section && strcmpci(e->section, section) == 0) { @@ -110,7 +107,6 @@ static int32_t findSection(const char *section) { } -// Trim leading/trailing whitespace in place. Returns pointer into buf. static char *trimInPlace(char *buf) { while (*buf == ' ' || *buf == '\t') { buf++; @@ -127,18 +123,31 @@ static char *trimInPlace(char *buf) { // ============================================================ -// prefsFree +// prefsClose // ============================================================ -void prefsFree(void) { - for (int32_t i = 0; i < arrlen(sEntries); i++) { - freeEntry(&sEntries[i]); +void prefsClose(PrefsHandleT *h) { + if (!h) { + return; } - arrfree(sEntries); - sEntries = NULL; - free(sFilePath); - sFilePath = NULL; + for (int32_t i = 0; i < arrlen(h->entries); i++) { + freeEntry(&h->entries[i]); + } + + arrfree(h->entries); + free(h->filePath); + free(h); +} + + +// ============================================================ +// prefsCreate +// ============================================================ + +PrefsHandleT *prefsCreate(void) { + PrefsHandleT *h = (PrefsHandleT *)calloc(1, sizeof(PrefsHandleT)); + return h; } @@ -146,8 +155,8 @@ void prefsFree(void) { // prefsGetBool // ============================================================ -bool prefsGetBool(const char *section, const char *key, bool defaultVal) { - const char *val = prefsGetString(section, key, NULL); +bool prefsGetBool(PrefsHandleT *h, const char *section, const char *key, bool defaultVal) { + const char *val = prefsGetString(h, section, key, NULL); if (!val) { return defaultVal; @@ -171,8 +180,8 @@ bool prefsGetBool(const char *section, const char *key, bool defaultVal) { // prefsGetInt // ============================================================ -int32_t prefsGetInt(const char *section, const char *key, int32_t defaultVal) { - const char *val = prefsGetString(section, key, NULL); +int32_t prefsGetInt(PrefsHandleT *h, const char *section, const char *key, int32_t defaultVal) { + const char *val = prefsGetString(h, section, key, NULL); if (!val) { return defaultVal; @@ -193,14 +202,18 @@ int32_t prefsGetInt(const char *section, const char *key, int32_t defaultVal) { // prefsGetString // ============================================================ -const char *prefsGetString(const char *section, const char *key, const char *defaultVal) { - int32_t idx = findEntry(section, key); +const char *prefsGetString(PrefsHandleT *h, const char *section, const char *key, const char *defaultVal) { + if (!h) { + return defaultVal; + } + + int32_t idx = findEntry(h, section, key); if (idx < 0) { return defaultVal; } - return sEntries[idx].value; + return h->entries[idx].value; } @@ -208,24 +221,25 @@ const char *prefsGetString(const char *section, const char *key, const char *def // prefsLoad // ============================================================ -bool prefsLoad(const char *filename) { - prefsFree(); +PrefsHandleT *prefsLoad(const char *filename) { + PrefsHandleT *h = prefsCreate(); - // Always store the path so prefsSave can create the file - // even if it doesn't exist yet. - sFilePath = dupStr(filename); + if (!h) { + return NULL; + } + + h->filePath = dupStr(filename); FILE *fp = fopen(filename, "rb"); if (!fp) { - return false; + return h; } char line[512]; char *currentSection = dupStr(""); while (fgets(line, sizeof(line), fp)) { - // Strip trailing whitespace/newline char *end = line + strlen(line) - 1; while (end >= line && (*end == '\r' || *end == '\n' || *end == ' ' || *end == '\t')) { @@ -234,30 +248,26 @@ bool prefsLoad(const char *filename) { char *p = line; - // Skip leading whitespace while (*p == ' ' || *p == '\t') { p++; } - // Blank line -- store as comment to preserve formatting if (*p == '\0') { PrefsEntryT e = {0}; e.section = dupStr(currentSection); e.value = dupStr(""); - arrput(sEntries, e); + arrput(h->entries, e); continue; } - // Comment line if (*p == ';' || *p == '#') { PrefsEntryT e = {0}; e.section = dupStr(currentSection); e.value = dupStr(line); - arrput(sEntries, e); + arrput(h->entries, e); continue; } - // Section header if (*p == '[') { char *close = strchr(p, ']'); @@ -268,13 +278,12 @@ bool prefsLoad(const char *filename) { PrefsEntryT e = {0}; e.section = dupStr(currentSection); - arrput(sEntries, e); + arrput(h->entries, e); } continue; } - // Key=value char *eq = strchr(p, '='); if (eq) { @@ -284,13 +293,13 @@ bool prefsLoad(const char *filename) { e.section = dupStr(currentSection); e.key = dupStr(trimInPlace(p)); e.value = dupStr(trimInPlace(eq + 1)); - arrput(sEntries, e); + arrput(h->entries, e); } } free(currentSection); fclose(fp); - return true; + return h; } @@ -298,12 +307,16 @@ bool prefsLoad(const char *filename) { // prefsRemove // ============================================================ -void prefsRemove(const char *section, const char *key) { - int32_t idx = findEntry(section, key); +void prefsRemove(PrefsHandleT *h, const char *section, const char *key) { + if (!h) { + return; + } + + int32_t idx = findEntry(h, section, key); if (idx >= 0) { - freeEntry(&sEntries[idx]); - arrdel(sEntries, idx); + freeEntry(&h->entries[idx]); + arrdel(h->entries, idx); } } @@ -312,12 +325,12 @@ void prefsRemove(const char *section, const char *key) { // prefsSave // ============================================================ -bool prefsSave(void) { - if (!sFilePath) { +bool prefsSave(PrefsHandleT *h) { + if (!h || !h->filePath) { return false; } - return prefsSaveAs(sFilePath); + return prefsSaveAs(h, h->filePath); } @@ -325,29 +338,30 @@ bool prefsSave(void) { // prefsSaveAs // ============================================================ -bool prefsSaveAs(const char *filename) { +bool prefsSaveAs(PrefsHandleT *h, const char *filename) { + if (!h) { + return false; + } + FILE *fp = fopen(filename, "wb"); if (!fp) { return false; } - for (int32_t i = 0; i < arrlen(sEntries); i++) { - PrefsEntryT *e = &sEntries[i]; + for (int32_t i = 0; i < arrlen(h->entries); i++) { + PrefsEntryT *e = &h->entries[i]; - // Comment or blank line (key=NULL, value=text or empty) if (!e->key && e->value) { fprintf(fp, "%s\r\n", e->value); continue; } - // Section header (key=NULL, value=NULL) if (!e->key && !e->value) { fprintf(fp, "[%s]\r\n", e->section); continue; } - // Key=value if (e->key && e->value) { fprintf(fp, "%s = %s\r\n", e->key, e->value); } @@ -362,8 +376,8 @@ bool prefsSaveAs(const char *filename) { // prefsSetBool // ============================================================ -void prefsSetBool(const char *section, const char *key, bool value) { - prefsSetString(section, key, value ? "true" : "false"); +void prefsSetBool(PrefsHandleT *h, const char *section, const char *key, bool value) { + prefsSetString(h, section, key, value ? "true" : "false"); } @@ -371,10 +385,10 @@ void prefsSetBool(const char *section, const char *key, bool value) { // prefsSetInt // ============================================================ -void prefsSetInt(const char *section, const char *key, int32_t value) { +void prefsSetInt(PrefsHandleT *h, const char *section, const char *key, int32_t value) { char buf[32]; snprintf(buf, sizeof(buf), "%ld", (long)value); - prefsSetString(section, key, buf); + prefsSetString(h, section, key, buf); } @@ -382,48 +396,45 @@ void prefsSetInt(const char *section, const char *key, int32_t value) { // prefsSetString // ============================================================ -void prefsSetString(const char *section, const char *key, const char *value) { - int32_t idx = findEntry(section, key); - - if (idx >= 0) { - // Update existing entry - free(sEntries[idx].value); - sEntries[idx].value = dupStr(value); +void prefsSetString(PrefsHandleT *h, const char *section, const char *key, const char *value) { + if (!h) { return; } - // Find or create section header - int32_t secIdx = findSection(section); + int32_t idx = findEntry(h, section, key); + + if (idx >= 0) { + free(h->entries[idx].value); + h->entries[idx].value = dupStr(value); + return; + } + + int32_t secIdx = findSection(h, section); if (secIdx < 0) { - // Add blank line before new section (unless file is empty) - if (arrlen(sEntries) > 0) { + if (arrlen(h->entries) > 0) { PrefsEntryT blank = {0}; blank.section = dupStr(section); blank.value = dupStr(""); - arrput(sEntries, blank); + arrput(h->entries, blank); } - // Add section header PrefsEntryT secEntry = {0}; secEntry.section = dupStr(section); - arrput(sEntries, secEntry); - secIdx = arrlen(sEntries) - 1; + arrput(h->entries, secEntry); + secIdx = arrlen(h->entries) - 1; } - // Find insertion point: after last entry in this section int32_t insertAt = secIdx + 1; - while (insertAt < arrlen(sEntries)) { - PrefsEntryT *e = &sEntries[insertAt]; + while (insertAt < arrlen(h->entries)) { + PrefsEntryT *e = &h->entries[insertAt]; - // Stop if we've hit a different section header if (!e->key && !e->value && e->section && strcmpci(e->section, section) != 0) { break; } - // Stop if we've hit an entry from a different section if (e->section && strcmpci(e->section, section) != 0) { break; } @@ -431,10 +442,9 @@ void prefsSetString(const char *section, const char *key, const char *value) { insertAt++; } - // Insert new entry PrefsEntryT newEntry = {0}; newEntry.section = dupStr(section); newEntry.key = dupStr(key); newEntry.value = dupStr(value); - arrins(sEntries, insertAt, newEntry); + arrins(h->entries, insertAt, newEntry); } diff --git a/core/dvxPrefs.h b/core/dvxPrefs.h index 96dfe37..71ecd6d 100644 --- a/core/dvxPrefs.h +++ b/core/dvxPrefs.h @@ -1,9 +1,8 @@ // dvxPrefs.h -- INI-based preferences system (read/write) // -// Loads a configuration file at startup and provides typed accessors -// with caller-supplied defaults. Values can be modified at runtime -// and saved back to disk. If the file is missing or a key is absent, -// getters return the default silently. +// Handle-based API: multiple INI files can be open simultaneously. +// Each prefsOpen/prefsLoad returns a handle that must be passed to +// all subsequent calls and freed with prefsClose when done. #ifndef DVX_PREFS_H #define DVX_PREFS_H @@ -11,41 +10,47 @@ #include #include -// Load an INI file into memory. Returns true on success, false if the -// file could not be opened (all getters will return their defaults). -// Only one file may be loaded at a time; calling again frees the previous. -bool prefsLoad(const char *filename); +// Opaque handle to a loaded preferences file. +typedef struct PrefsHandleT PrefsHandleT; -// Save the current in-memory state back to the file that was loaded. -// Returns true on success. -bool prefsSave(void); +// Create an empty preferences handle (no file loaded). Useful for +// building a new INI from scratch before saving. +PrefsHandleT *prefsCreate(void); -// Save the current in-memory state to a specific file. -bool prefsSaveAs(const char *filename); +// Load an INI file into a new handle. Returns NULL on allocation +// failure. If the file doesn't exist, returns a valid empty handle +// (all getters return defaults) with the path stored for prefsSave. +PrefsHandleT *prefsLoad(const char *filename); -// Release all memory held by the preferences. -void prefsFree(void); +// Save the in-memory state back to the file that was loaded. +bool prefsSave(PrefsHandleT *h); -// Retrieve a string value. Returns defaultVal if the key is not present. -// The returned pointer is valid until the key is modified or prefsFree(). -const char *prefsGetString(const char *section, const char *key, const char *defaultVal); +// Save the in-memory state to a specific file. +bool prefsSaveAs(PrefsHandleT *h, const char *filename); + +// Release all memory held by the handle. +void prefsClose(PrefsHandleT *h); + +// Retrieve a string value. Returns defaultVal if the key is not present. +// The returned pointer is valid until the key is modified or prefsClose. +const char *prefsGetString(PrefsHandleT *h, const char *section, const char *key, const char *defaultVal); // Retrieve an integer value. -int32_t prefsGetInt(const char *section, const char *key, int32_t defaultVal); +int32_t prefsGetInt(PrefsHandleT *h, const char *section, const char *key, int32_t defaultVal); -// Retrieve a boolean value. Recognises "true"/"yes"/"1" and "false"/"no"/"0". -bool prefsGetBool(const char *section, const char *key, bool defaultVal); +// Retrieve a boolean value. Recognises "true"/"yes"/"1" and "false"/"no"/"0". +bool prefsGetBool(PrefsHandleT *h, const char *section, const char *key, bool defaultVal); -// Set a string value. Creates the section and key if they don't exist. -void prefsSetString(const char *section, const char *key, const char *value); +// Set a string value. Creates the section and key if they don't exist. +void prefsSetString(PrefsHandleT *h, const char *section, const char *key, const char *value); // Set an integer value. -void prefsSetInt(const char *section, const char *key, int32_t value); +void prefsSetInt(PrefsHandleT *h, const char *section, const char *key, int32_t value); // Set a boolean value (stored as "true"/"false"). -void prefsSetBool(const char *section, const char *key, bool value); +void prefsSetBool(PrefsHandleT *h, const char *section, const char *key, bool value); -// Remove a key from a section. No-op if not found. -void prefsRemove(const char *section, const char *key); +// Remove a key from a section. No-op if not found. +void prefsRemove(PrefsHandleT *h, const char *section, const char *key); #endif diff --git a/core/thirdparty/stb_ds_impl.c b/core/thirdparty/stb_ds_impl.c new file mode 100644 index 0000000..7d05d64 --- /dev/null +++ b/core/thirdparty/stb_ds_impl.c @@ -0,0 +1,3 @@ +// stb_ds_impl.c -- stb_ds implementation for host test builds +#define STB_DS_IMPLEMENTATION +#include "stb_ds.h" diff --git a/shell/shellMain.c b/shell/shellMain.c index 6327861..404a714 100644 --- a/shell/shellMain.c +++ b/shell/shellMain.c @@ -47,6 +47,7 @@ // ============================================================ static AppContextT sCtx; +static PrefsHandleT *sPrefs = NULL; // setjmp buffer for crash recovery. The crash handler longjmps here to // return control to the shell's main loop after an app crashes. static jmp_buf sCrashJmp; @@ -211,11 +212,11 @@ int shellMain(int argc, char *argv[]) { dvxLog("DVX Shell starting..."); // Load preferences (missing file or keys silently use defaults) - prefsLoad("CONFIG/DVX.INI"); + sPrefs = prefsLoad("CONFIG/DVX.INI"); - int32_t videoW = prefsGetInt("video", "width", 640); - int32_t videoH = prefsGetInt("video", "height", 480); - int32_t videoBpp = prefsGetInt("video", "bpp", 16); + int32_t videoW = prefsGetInt(sPrefs, "video", "width", 640); + int32_t videoH = prefsGetInt(sPrefs, "video", "height", 480); + int32_t videoBpp = prefsGetInt(sPrefs, "video", "bpp", 16); dvxLog("Preferences: video %ldx%ld %ldbpp", (long)videoW, (long)videoH, (long)videoBpp); // Initialize GUI @@ -223,13 +224,13 @@ int shellMain(int argc, char *argv[]) { if (result == 0) { // Apply mouse preferences - const char *wheelStr = prefsGetString("mouse", "wheel", "normal"); + const char *wheelStr = prefsGetString(sPrefs, "mouse", "wheel", "normal"); int32_t wheelDir = (strcmp(wheelStr, "reversed") == 0) ? -1 : 1; - int32_t dblClick = prefsGetInt("mouse", "doubleclick", 500); + int32_t dblClick = prefsGetInt(sPrefs, "mouse", "doubleclick", 500); // Map acceleration name to double-speed threshold (mickeys/sec). // "off" sets a very high threshold so acceleration never triggers. - const char *accelStr = prefsGetString("mouse", "acceleration", "medium"); + const char *accelStr = prefsGetString(sPrefs, "mouse", "acceleration", "medium"); int32_t accelVal = 0; if (strcmp(accelStr, "off") == 0) { @@ -249,7 +250,7 @@ int shellMain(int argc, char *argv[]) { bool colorsLoaded = false; for (int32_t i = 0; i < ColorCountE; i++) { - const char *val = prefsGetString("colors", dvxColorName((ColorIdE)i), NULL); + const char *val = prefsGetString(sPrefs, "colors", dvxColorName((ColorIdE)i), NULL); if (val) { int r; @@ -271,7 +272,7 @@ int shellMain(int argc, char *argv[]) { } // Apply saved wallpaper mode and image - const char *wpMode = prefsGetString("desktop", "mode", "stretch"); + const char *wpMode = prefsGetString(sPrefs, "desktop", "mode", "stretch"); if (strcmp(wpMode, "tile") == 0) { sCtx.wallpaperMode = WallpaperTileE; @@ -281,7 +282,7 @@ int shellMain(int argc, char *argv[]) { sCtx.wallpaperMode = WallpaperStretchE; } - const char *wpPath = prefsGetString("desktop", "wallpaper", NULL); + const char *wpPath = prefsGetString(sPrefs, "desktop", "wallpaper", NULL); if (wpPath) { if (dvxSetWallpaper(&sCtx, wpPath)) { @@ -343,7 +344,7 @@ int shellMain(int argc, char *argv[]) { platformInstallCrashHandler(&sCrashJmp, &sCrashSignal, dvxLog); // Load the desktop app (configurable via [shell] desktop= in dvx.ini) - const char *desktopApp = prefsGetString("shell", "desktop", SHELL_DESKTOP_APP); + const char *desktopApp = prefsGetString(sPrefs, "shell", "desktop", SHELL_DESKTOP_APP); int32_t desktopId = shellLoadApp(&sCtx, desktopApp); if (desktopId < 0) { @@ -439,7 +440,8 @@ int shellMain(int argc, char *argv[]) { tsShutdown(); dvxShutdown(&sCtx); - prefsFree(); + prefsClose(sPrefs); + sPrefs = NULL; dvxLog("DVX Shell exited."); return 0; diff --git a/tools/Makefile b/tools/Makefile index 81710a5..8c5e5ec 100644 --- a/tools/Makefile +++ b/tools/Makefile @@ -10,11 +10,14 @@ BINDIR = ../bin .PHONY: all clean -all: $(BINDIR)/dvxres $(BINDIR)/mktbicon +all: $(BINDIR)/dvxres $(BINDIR)/mkicon $(BINDIR)/mktbicon $(BINDIR)/dvxres: dvxres.c ../core/dvxResource.c ../core/dvxResource.h | $(BINDIR) $(CC) $(CFLAGS) -o $@ dvxres.c ../core/dvxResource.c +$(BINDIR)/mkicon: mkicon.c | $(BINDIR) + $(CC) $(CFLAGS) -o $@ mkicon.c -lm + $(BINDIR)/mktbicon: mktbicon.c | $(BINDIR) $(CC) $(CFLAGS) -o $@ mktbicon.c diff --git a/tools/mkicon.c b/tools/mkicon.c index 320de3a..3353d82 100644 --- a/tools/mkicon.c +++ b/tools/mkicon.c @@ -62,6 +62,36 @@ static void vline(int x, int y0, int h, uint8_t r, uint8_t g, uint8_t b) { } } +// No-icon placeholder: grey square with red diagonal X +static void iconNoIcon(void) { + clear(192, 192, 192); + + // Border + rect(0, 0, 32, 1, 128, 128, 128); + rect(0, 31, 32, 1, 128, 128, 128); + rect(0, 0, 1, 32, 128, 128, 128); + rect(31, 0, 1, 32, 128, 128, 128); + + // Red X (two diagonal lines, 2px thick) + for (int i = 4; i < 28; i++) { + pixel(i, i, 200, 40, 40); + pixel(i + 1, i, 200, 40, 40); + pixel(31 - i, i, 200, 40, 40); + pixel(30 - i, i, 200, 40, 40); + } + + // "?" in the center + for (int x = 13; x <= 18; x++) { pixel(x, 8, 80, 80, 80); } + pixel(19, 9, 80, 80, 80); + pixel(19, 10, 80, 80, 80); + for (int x = 15; x <= 18; x++) { pixel(x, 11, 80, 80, 80); } + pixel(15, 12, 80, 80, 80); + pixel(15, 13, 80, 80, 80); + pixel(15, 15, 80, 80, 80); + pixel(16, 15, 80, 80, 80); +} + + // Clock icon: circle with hands static void iconClock(void) { clear(192, 192, 192); @@ -294,14 +324,16 @@ static void writeBmp(const char *path) { int main(int argc, char **argv) { if (argc < 3) { fprintf(stderr, "Usage: mkicon \n"); - fprintf(stderr, "Types: clock, notepad, cpanel, dvxdemo, imgview, basic\n"); + fprintf(stderr, "Types: noicon, clock, notepad, cpanel, dvxdemo, imgview, basic\n"); return 1; } const char *path = argv[1]; const char *type = argv[2]; - if (strcmp(type, "clock") == 0) { + if (strcmp(type, "noicon") == 0) { + iconNoIcon(); + } else if (strcmp(type, "clock") == 0) { iconClock(); } else if (strcmp(type, "notepad") == 0) { iconNotepad();