diff --git a/.gitignore b/.gitignore index 6301234..91323dd 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ dosbench/ bin/ obj/ lib/ +*~ *.~ .gitignore~ .gitattributes~ diff --git a/apps/dvxbasic/Makefile b/apps/dvxbasic/Makefile index ba21090..5c80484 100644 --- a/apps/dvxbasic/Makefile +++ b/apps/dvxbasic/Makefile @@ -18,7 +18,6 @@ OBJDIR = ../../obj/dvxbasic LIBSDIR = ../../bin/libs APPDIR = ../../bin/apps/kpunch/dvxbasic DVXRES = ../../bin/host/dvxres -SAMPLES = samples/hello.bas samples/formtest.bas samples/clickme.bas samples/clickme.frm samples/input.bas # Runtime library objects (VM + values) RT_OBJS = $(OBJDIR)/vm.o $(OBJDIR)/values.o @@ -54,7 +53,7 @@ TEST_QUICK_SRCS = test_quick.c compiler/lexer.c compiler/parser.c compiler/co .PHONY: all clean tests -all: $(RT_TARGET) $(RT_TARGETDIR)/basrt.dep $(APP_TARGET) install-samples +all: $(RT_TARGET) $(RT_TARGETDIR)/basrt.dep $(APP_TARGET) tests: $(TEST_COMPILER) $(TEST_VM) $(TEST_LEX) $(TEST_QUICK) @@ -85,8 +84,6 @@ $(APP_TARGET): $(COMP_OBJS) $(APP_OBJS) dvxbasic.res | $(APPDIR) $(DXE3GEN) -o $@ -E _appDescriptor -E _appMain -U $(COMP_OBJS) $(APP_OBJS) $(DVXRES) build $@ dvxbasic.res -install-samples: $(SAMPLES) | $(APPDIR) - cp $(SAMPLES) $(APPDIR)/ # Object files $(OBJDIR)/codegen.o: compiler/codegen.c compiler/codegen.h compiler/symtab.h compiler/opcodes.h runtime/values.h | $(OBJDIR) diff --git a/apps/dvxbasic/compiler/lexer.c b/apps/dvxbasic/compiler/lexer.c index 6b80f12..5e776fa 100644 --- a/apps/dvxbasic/compiler/lexer.c +++ b/apps/dvxbasic/compiler/lexer.c @@ -689,7 +689,7 @@ static BasTokenTypeE tokenizeIdentOrKeyword(BasLexerT *lex) { // If it's a keyword and has no suffix, return the keyword token. // String-returning builtins (SQLError$, SQLField$) also match with $. - if (kwType != TOK_IDENT && (baseLen == idx || kwType == TOK_SQLERROR || kwType == TOK_SQLFIELD)) { + if (kwType != TOK_IDENT && (baseLen == idx || kwType == TOK_SQLERROR || kwType == TOK_SQLFIELD || kwType == TOK_INPUTBOX)) { return kwType; } diff --git a/apps/dvxbasic/compiler/parser.c b/apps/dvxbasic/compiler/parser.c index 0505308..51ebae8 100644 --- a/apps/dvxbasic/compiler/parser.c +++ b/apps/dvxbasic/compiler/parser.c @@ -296,6 +296,7 @@ static void advance(BasParserT *p) { if (p->hasError) { return; } + p->prevLine = p->lex.token.line; basLexerNext(&p->lex); if (p->lex.token.type == TOK_ERROR) { error(p, p->lex.error); @@ -344,9 +345,17 @@ static void error(BasParserT *p, const char *msg) { if (p->hasError) { return; } - p->hasError = true; - p->errorLine = p->lex.token.line; - snprintf(p->error, sizeof(p->error), "Line %d: %s", (int)p->lex.token.line, msg); + p->hasError = true; + + // If the current token is on a later line than the previous token, + // the error is about the previous line (e.g. missing token at EOL). + int32_t line = p->lex.token.line; + if (p->prevLine > 0 && line > p->prevLine) { + line = p->prevLine; + } + + p->errorLine = line; + snprintf(p->error, sizeof(p->error), "Line %d: %s", (int)line, msg); } @@ -1739,10 +1748,10 @@ static void parsePrimary(BasParserT *p) { return; } - // Not a UDT -- treat as control property read: CtrlName.Property + // Not a UDT -- treat as control property/method: CtrlName.Member advance(p); // consume DOT - if (!check(p, TOK_IDENT)) { - errorExpected(p, "property name"); + if (!isalpha((unsigned char)p->lex.token.text[0]) && p->lex.token.text[0] != '_') { + errorExpected(p, "property or method name"); return; } char memberName[BAS_MAX_TOKEN_LEN]; @@ -1758,11 +1767,32 @@ static void parsePrimary(BasParserT *p) { basEmitU16(&p->cg, ctrlNameIdx); basEmit8(&p->cg, OP_FIND_CTRL); - // Push property name, LOAD_PROP - uint16_t propNameIdx = basAddConstant(&p->cg, memberName, (int32_t)strlen(memberName)); - basEmit8(&p->cg, OP_PUSH_STR); - basEmitU16(&p->cg, propNameIdx); - basEmit8(&p->cg, OP_LOAD_PROP); + // If followed by '(', this is a method call with args + if (check(p, TOK_LPAREN)) { + advance(p); // consume '(' + uint16_t methodNameIdx = basAddConstant(&p->cg, memberName, (int32_t)strlen(memberName)); + basEmit8(&p->cg, OP_PUSH_STR); + basEmitU16(&p->cg, methodNameIdx); + + int32_t argc = 0; + if (!check(p, TOK_RPAREN)) { + parseExpression(p); + argc++; + while (match(p, TOK_COMMA)) { + parseExpression(p); + argc++; + } + } + expect(p, TOK_RPAREN); + basEmit8(&p->cg, OP_CALL_METHOD); + basEmit8(&p->cg, (uint8_t)argc); + } else { + // Property read + uint16_t propNameIdx = basAddConstant(&p->cg, memberName, (int32_t)strlen(memberName)); + basEmit8(&p->cg, OP_PUSH_STR); + basEmitU16(&p->cg, propNameIdx); + basEmit8(&p->cg, OP_LOAD_PROP); + } return; } @@ -1895,7 +1925,9 @@ static void parseAssignOrCall(BasParserT *p) { // Emit: push current form ref, push ctrl name, FIND_CTRL advance(p); // consume DOT - if (!check(p, TOK_IDENT) && !check(p, TOK_SHOW) && !check(p, TOK_HIDE)) { + // Accept any identifier or keyword as a member name — keywords + // like Load, Show, Hide, Clear are valid method names on controls. + if (!isalpha((unsigned char)p->lex.token.text[0]) && p->lex.token.text[0] != '_') { errorExpected(p, "property or method name"); return; } @@ -2027,7 +2059,7 @@ static void parseAssignOrCall(BasParserT *p) { basEmit8(&p->cg, OP_FIND_CTRL_IDX); expect(p, TOK_DOT); - if (!check(p, TOK_IDENT) && !check(p, TOK_SHOW) && !check(p, TOK_HIDE)) { + if (!isalpha((unsigned char)p->lex.token.text[0]) && p->lex.token.text[0] != '_') { errorExpected(p, "property or method name"); return; } @@ -5023,7 +5055,7 @@ static void parseStatement(BasParserT *p) { } advance(p); // consume DOT - if (!check(p, TOK_IDENT) && !check(p, TOK_SHOW) && !check(p, TOK_HIDE)) { + if (!isalpha((unsigned char)p->lex.token.text[0]) && p->lex.token.text[0] != '_') { errorExpected(p, "method or member name after Me."); break; } diff --git a/apps/dvxbasic/compiler/parser.h b/apps/dvxbasic/compiler/parser.h index 488481d..2cd4757 100644 --- a/apps/dvxbasic/compiler/parser.h +++ b/apps/dvxbasic/compiler/parser.h @@ -28,6 +28,7 @@ typedef struct { char error[1024]; bool hasError; int32_t errorLine; + int32_t prevLine; // line of the previous token (for error reporting) int32_t lastUdtTypeId; // index of last resolved UDT type from resolveTypeName int32_t optionBase; // default array lower bound (0 or 1) bool optionCompareText; // true = case-insensitive string comparison diff --git a/apps/dvxbasic/compiler/symtab.c b/apps/dvxbasic/compiler/symtab.c index aaac913..9956923 100644 --- a/apps/dvxbasic/compiler/symtab.c +++ b/apps/dvxbasic/compiler/symtab.c @@ -118,19 +118,19 @@ BasSymbolT *basSymTabFind(BasSymTabT *tab, const char *name) { } } - // Search form scope (active form-scoped symbols shadow globals) + // Search form scope and global scope for (int32_t i = tab->count - 1; i >= 0; i--) { if (tab->symbols[i].formScopeEnded) { continue; } - if (tab->symbols[i].scope == SCOPE_FORM && namesEqual(tab->symbols[i].name, name)) { + if ((tab->symbols[i].scope == SCOPE_FORM || tab->symbols[i].scope == SCOPE_GLOBAL) && + namesEqual(tab->symbols[i].name, name)) { return &tab->symbols[i]; } } - // Search global scope - return basSymTabFindGlobal(tab, name); + return NULL; } diff --git a/apps/dvxbasic/formrt/formrt.c b/apps/dvxbasic/formrt/formrt.c index dfa76c7..e3e148e 100644 --- a/apps/dvxbasic/formrt/formrt.c +++ b/apps/dvxbasic/formrt/formrt.c @@ -5,6 +5,7 @@ // registered by .wgt DXE files. No hardcoded control types. #include "formrt.h" +#include "../ide/ideDesigner.h" #include "../compiler/codegen.h" #include "../compiler/opcodes.h" #include "dvxDlg.h" @@ -27,31 +28,7 @@ #define DEFAULT_FORM_W 400 #define DEFAULT_FORM_H 300 #define MAX_EVENT_NAME_LEN 128 -#define MAX_LISTBOX_ITEMS 256 #define MAX_FRM_LINE_LEN 512 -#define MAX_FRM_NESTING 16 -#define MAX_AUX_DATA 128 - -// ============================================================ -// Per-control listbox item storage -// ============================================================ - -typedef struct { - char *items[MAX_LISTBOX_ITEMS]; - int32_t count; -} ListBoxItemsT; - -// ============================================================ -// Auxiliary data table for listbox item storage -// ============================================================ - -typedef struct { - BasControlT *ctrl; - ListBoxItemsT *items; -} AuxDataEntryT; - -static AuxDataEntryT sAuxData[MAX_AUX_DATA]; -static int32_t sAuxDataCount = 0; // Module-level form runtime pointer for onFormClose callback static BasFormRtT *sFormRt = NULL; @@ -63,12 +40,9 @@ static BasFormRtT *sFormRt = NULL; static BasStringT *basFormRtInputBox(void *ctx, const char *prompt, const char *title, const char *defaultText); static void onFormMenu(WindowT *win, int32_t menuId); static BasValueT callCommonMethod(BasControlT *ctrl, const char *methodName, BasValueT *args, int32_t argc); -static BasValueT callListBoxMethod(BasControlT *ctrl, const char *methodName, BasValueT *args, int32_t argc); static WidgetT *createWidget(const char *wgtTypeName, WidgetT *parent); -static void freeListBoxItems(BasControlT *ctrl); static BasValueT getCommonProp(BasControlT *ctrl, const char *propName, bool *handled); static BasValueT getIfaceProp(const WgtIfaceT *iface, WidgetT *w, const char *propName, bool *handled); -static ListBoxItemsT *getListBoxItems(BasControlT *ctrl); static void onFormActivate(WindowT *win); static void onFormClose(WindowT *win); static void onFormDeactivate(WindowT *win); @@ -87,7 +61,6 @@ static void onWidgetMouseUp(WidgetT *w, int32_t button, int32_t x, int32_t static void onWidgetMouseMove(WidgetT *w, int32_t button, int32_t x, int32_t y); static void onWidgetScroll(WidgetT *w, int32_t delta); static void parseFrmLine(const char *line, char *key, char *value); -static void rebuildListBoxItems(BasControlT *ctrl); static const char *resolveTypeName(const char *typeName); static bool setCommonProp(BasControlT *ctrl, const char *propName, BasValueT value); static bool setIfaceProp(const WgtIfaceT *iface, WidgetT *w, const char *propName, BasValueT value); @@ -133,16 +106,6 @@ BasValueT basFormRtCallMethod(void *ctx, void *ctrlRef, const char *methodName, return zeroValue(); } - // ListBox-specific methods (AddItem/RemoveItem/Clear/List) - BasValueT result = callListBoxMethod(ctrl, methodName, args, argc); - - if (result.type != 0 || strcasecmp(methodName, "AddItem") == 0 || - strcasecmp(methodName, "RemoveItem") == 0 || - strcasecmp(methodName, "Clear") == 0 || - strcasecmp(methodName, "List") == 0) { - return result; - } - // Interface descriptor methods const WgtIfaceT *iface = ctrl->iface; @@ -209,6 +172,56 @@ BasValueT basFormRtCallMethod(void *ctx, void *ctrlRef, const char *methodName, } return basValBool(false); } + + case WGT_SIG_INT3: + if (argc >= 3) { + ((void (*)(WidgetT *, int32_t, int32_t, int32_t))m->fn)(w, (int32_t)basValToNumber(args[0]), (int32_t)basValToNumber(args[1]), (int32_t)basValToNumber(args[2])); + } + return zeroValue(); + + case WGT_SIG_INT4: + if (argc >= 4) { + ((void (*)(WidgetT *, int32_t, int32_t, int32_t, int32_t))m->fn)(w, (int32_t)basValToNumber(args[0]), (int32_t)basValToNumber(args[1]), (int32_t)basValToNumber(args[2]), (int32_t)basValToNumber(args[3])); + } + return zeroValue(); + + case WGT_SIG_RET_INT_INT_INT: { + if (argc >= 2) { + int32_t v = ((int32_t (*)(const WidgetT *, int32_t, int32_t))m->fn)(w, (int32_t)basValToNumber(args[0]), (int32_t)basValToNumber(args[1])); + return basValLong(v); + } + return zeroValue(); + } + + case WGT_SIG_INT5: + if (argc >= 5) { + ((void (*)(WidgetT *, int32_t, int32_t, int32_t, int32_t, int32_t))m->fn)(w, (int32_t)basValToNumber(args[0]), (int32_t)basValToNumber(args[1]), (int32_t)basValToNumber(args[2]), (int32_t)basValToNumber(args[3]), (int32_t)basValToNumber(args[4])); + } + return zeroValue(); + + case WGT_SIG_RET_STR_INT: { + if (argc >= 1) { + const char *s = ((const char *(*)(const WidgetT *, int32_t))m->fn)(w, (int32_t)basValToNumber(args[0])); + return basValStringFromC(s ? s : ""); + } + return basValStringFromC(""); + } + + case WGT_SIG_RET_STR_INT_INT: { + if (argc >= 2) { + const char *s = ((const char *(*)(const WidgetT *, int32_t, int32_t))m->fn)(w, (int32_t)basValToNumber(args[0]), (int32_t)basValToNumber(args[1])); + return basValStringFromC(s ? s : ""); + } + return basValStringFromC(""); + } + + case WGT_SIG_INT_INT_STR: + if (argc >= 3) { + BasStringT *s = basValFormatString(args[2]); + ((void (*)(WidgetT *, int32_t, int32_t, const char *))m->fn)(w, (int32_t)basValToNumber(args[0]), (int32_t)basValToNumber(args[1]), s->data); + basStringUnref(s); + } + return zeroValue(); } } } @@ -274,17 +287,19 @@ void *basFormRtCreateCtrl(void *ctx, void *formRef, const char *typeName, const wgtSetName(widget, ctrlName); // Initialize control entry - BasControlT entry; - memset(&entry, 0, sizeof(entry)); - entry.index = -1; - 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]; + BasControlT *ctrl = (BasControlT *)calloc(1, sizeof(BasControlT)); + + if (!ctrl) { + return NULL; + } + + ctrl->index = -1; snprintf(ctrl->name, BAS_MAX_CTRL_NAME, "%s", ctrlName); - ctrl->widget = widget; - ctrl->form = form; - ctrl->iface = wgtGetIface(wgtTypeName); + ctrl->widget = widget; + ctrl->form = form; + ctrl->iface = wgtGetIface(wgtTypeName); + arrput(form->controls, ctrl); + form->controlCount = (int32_t)arrlen(form->controls); // Wire up event callbacks widget->userData = ctrl; @@ -313,7 +328,7 @@ void basFormRtDestroy(BasFormRtT *rt) { } for (int32_t i = 0; i < rt->formCount; i++) { - BasFormT *form = &rt->forms[i]; + BasFormT *form = rt->forms[i]; if (form->formVars) { for (int32_t j = 0; j < form->formVarCount; j++) { @@ -324,16 +339,23 @@ void basFormRtDestroy(BasFormRtT *rt) { } for (int32_t j = 0; j < form->controlCount; j++) { - freeListBoxItems(&form->controls[j]); + free(form->controls[j]); } arrfree(form->controls); + + for (int32_t j = 0; j < form->menuIdMapCount; j++) { + free(form->menuIdMap[j].proxy); + } + arrfree(form->menuIdMap); if (form->window) { dvxDestroyWindow(rt->ctx, form->window); form->window = NULL; } + + free(form); } arrfree(rt->forms); @@ -353,16 +375,46 @@ void basFormRtDestroy(BasFormRtT *rt) { // ============================================================ void *basFormRtFindCtrl(void *ctx, void *formRef, const char *ctrlName) { - (void)ctx; + BasFormRtT *rt = (BasFormRtT *)ctx; BasFormT *form = (BasFormT *)formRef; if (!form) { return NULL; } + // Check if the name refers to the current form itself + if (strcasecmp(form->name, ctrlName) == 0) { + return &form->formCtrl; + } + + // Search controls on the current form for (int32_t i = 0; i < form->controlCount; i++) { - if (strcasecmp(form->controls[i].name, ctrlName) == 0) { - return &form->controls[i]; + if (strcasecmp(form->controls[i]->name, ctrlName) == 0) { + return form->controls[i]; + } + } + + // Search menu items on the current form + for (int32_t i = 0; i < form->menuIdMapCount; i++) { + if (strcasecmp(form->menuIdMap[i].name, ctrlName) == 0) { + // Create proxy on first access + if (!form->menuIdMap[i].proxy) { + BasControlT *proxy = (BasControlT *)calloc(1, sizeof(BasControlT)); + snprintf(proxy->name, BAS_MAX_CTRL_NAME, "%s", ctrlName); + proxy->form = form; + proxy->menuId = form->menuIdMap[i].id; + form->menuIdMap[i].proxy = proxy; + } + return form->menuIdMap[i].proxy; + } + } + + // Search other loaded forms by name (for cross-form property access) + if (rt) { + for (int32_t i = 0; i < rt->formCount; i++) { + if (strcasecmp(rt->forms[i]->name, ctrlName) == 0) { + return &rt->forms[i]->formCtrl; + } } } @@ -383,8 +435,8 @@ void *basFormRtFindCtrlIdx(void *ctx, void *formRef, const char *ctrlName, int32 } for (int32_t i = 0; i < form->controlCount; i++) { - if (form->controls[i].index == index && strcasecmp(form->controls[i].name, ctrlName) == 0) { - return &form->controls[i]; + if (form->controls[i]->index == index && strcasecmp(form->controls[i]->name, ctrlName) == 0) { + return form->controls[i]; } } @@ -511,7 +563,50 @@ BasValueT basFormRtGetProp(void *ctx, void *ctrlRef, const char *propName) { (void)ctx; BasControlT *ctrl = (BasControlT *)ctrlRef; - if (!ctrl || !ctrl->widget) { + if (!ctrl) { + return zeroValue(); + } + + // Menu item properties + if (ctrl->menuId > 0 && ctrl->form && ctrl->form->window && ctrl->form->window->menuBar) { + MenuBarT *bar = ctrl->form->window->menuBar; + + if (strcasecmp(propName, "Checked") == 0) { + return basValBool(wmMenuItemIsChecked(bar, ctrl->menuId)); + } + + if (strcasecmp(propName, "Enabled") == 0) { + return basValBool(true); // TODO: wmMenuItemIsEnabled not exposed yet + } + + if (strcasecmp(propName, "Name") == 0) { + return basValStringFromC(ctrl->name); + } + + return zeroValue(); + } + + if (!ctrl->widget) { + return zeroValue(); + } + + // Form-level properties use the window and BasFormT, not the root widget + if (ctrl->form && ctrl == &ctrl->form->formCtrl) { + WindowT *win = ctrl->form->window; + BasFormT *frm = ctrl->form; + + if (strcasecmp(propName, "Name") == 0) { return basValStringFromC(frm->name); } + if (strcasecmp(propName, "Caption") == 0) { return basValStringFromC(win ? win->title : ""); } + if (strcasecmp(propName, "Width") == 0) { return basValLong(win ? win->w : 0); } + if (strcasecmp(propName, "Height") == 0) { return basValLong(win ? win->h : 0); } + if (strcasecmp(propName, "Left") == 0) { return basValLong(win ? win->x : 0); } + if (strcasecmp(propName, "Top") == 0) { return basValLong(win ? win->y : 0); } + if (strcasecmp(propName, "Visible") == 0) { return basValBool(win && win->visible); } + if (strcasecmp(propName, "Resizable") == 0) { return basValBool(win && win->resizable); } + if (strcasecmp(propName, "AutoSize") == 0) { return basValBool(frm->frmAutoSize); } + if (strcasecmp(propName, "Centered") == 0) { return basValBool(frm->frmCentered); } + if (strcasecmp(propName, "Layout") == 0) { return basValStringFromC(frm->frmLayout); } + return zeroValue(); } @@ -540,8 +635,15 @@ BasValueT basFormRtGetProp(void *ctx, void *ctrlRef, const char *propName) { // "ListCount" for any widget with item storage if (strcasecmp(propName, "ListCount") == 0) { - ListBoxItemsT *lb = getListBoxItems(ctrl); - return basValLong(lb ? lb->count : 0); + if (ctrl->iface) { + for (int32_t m = 0; m < ctrl->iface->methodCount; m++) { + if (strcasecmp(ctrl->iface->methods[m].name, "ListCount") == 0) { + return basValLong(((int32_t (*)(const WidgetT *))ctrl->iface->methods[m].fn)(ctrl->widget)); + } + } + } + + return basValLong(0); } // Interface descriptor properties @@ -605,8 +707,8 @@ void *basFormRtLoadForm(void *ctx, const char *formName) { // Check if form already exists for (int32_t i = 0; i < rt->formCount; i++) { - if (strcasecmp(rt->forms[i].name, formName) == 0) { - return &rt->forms[i]; + if (strcasecmp(rt->forms[i]->name, formName) == 0) { + return rt->forms[i]; } } @@ -619,38 +721,36 @@ void *basFormRtLoadForm(void *ctx, const char *formName) { } // No cache entry — create a bare form (first-time load without .frm file) - WindowT *win = dvxCreateWindowCentered(rt->ctx, formName, DEFAULT_FORM_W, DEFAULT_FORM_H, true); + WidgetT *root; + WidgetT *bareContentBox; + WindowT *win = dsgnCreateFormWindow(rt->ctx, formName, "VBox", false, true, false, DEFAULT_FORM_W, DEFAULT_FORM_H, 0, 0, &root, &bareContentBox); if (!win) { return NULL; } - win->visible = false; - - WidgetT *root = wgtInitWindow(rt->ctx, win); - - if (!root) { - dvxDestroyWindow(rt->ctx, win); - return NULL; - } - - BasFormT entry; - memset(&entry, 0, sizeof(entry)); - arrput(rt->forms, entry); + BasFormT *form = (BasFormT *)calloc(1, sizeof(BasFormT)); + arrput(rt->forms, form); rt->formCount = (int32_t)arrlen(rt->forms); - BasFormT *form = &rt->forms[rt->formCount - 1]; snprintf(form->name, BAS_MAX_CTRL_NAME, "%s", formName); + snprintf(form->frmLayout, sizeof(form->frmLayout), "VBox"); win->onClose = onFormClose; win->onResize = onFormResize; win->onFocus = onFormActivate; win->onBlur = onFormDeactivate; form->window = win; form->root = root; - form->contentBox = NULL; + form->contentBox = bareContentBox; form->ctx = rt->ctx; form->vm = rt->vm; form->module = rt->module; + // Initialize synthetic control for form-level property access + memset(&form->formCtrl, 0, sizeof(form->formCtrl)); + snprintf(form->formCtrl.name, BAS_MAX_CTRL_NAME, "%s", formName); + form->formCtrl.widget = root; + form->formCtrl.form = form; + // Allocate per-form variable storage and run init code from module metadata if (rt->module && rt->module->formVarInfo) { for (int32_t j = 0; j < rt->module->formVarInfoCount; j++) { @@ -694,11 +794,11 @@ BasFormT *basFormRtLoadFrm(BasFormRtT *rt, const char *source, int32_t sourceLen BasFormT *form = NULL; BasControlT *current = NULL; - WidgetT *parentStack[MAX_FRM_NESTING]; + WidgetT *parentStack[DSGN_MAX_FRM_NESTING]; int32_t nestDepth = 0; // Track Begin/End blocks: true = container (Form/Frame), false = control - bool isContainer[MAX_FRM_NESTING]; + bool isContainer[DSGN_MAX_FRM_NESTING]; int32_t blockDepth = 0; // Temporary menu item accumulation @@ -707,6 +807,7 @@ BasFormT *basFormRtLoadFrm(BasFormRtT *rt, const char *source, int32_t sourceLen char name[BAS_MAX_CTRL_NAME]; int32_t level; bool checked; + bool radioCheck; bool enabled; } TempMenuItemT; @@ -802,12 +903,13 @@ BasFormT *basFormRtLoadFrm(BasFormRtT *rt, const char *source, int32_t sourceLen return NULL; } - // contentBox is created lazily (see below) after - // form-level properties like Layout are parsed. + // contentBox may already be set from basFormRtLoadForm. + // It gets replaced at the End block after Layout is known. nestDepth = 1; + parentStack[0] = form->contentBox; current = NULL; - if (blockDepth < MAX_FRM_NESTING) { + if (blockDepth < DSGN_MAX_FRM_NESTING) { isContainer[blockDepth++] = true; } } else if (strcasecmp(typeName, "Menu") == 0 && form) { @@ -822,7 +924,7 @@ BasFormT *basFormRtLoadFrm(BasFormRtT *rt, const char *source, int32_t sourceLen menuNestDepth++; inMenu = true; - if (blockDepth < MAX_FRM_NESTING) { + if (blockDepth < DSGN_MAX_FRM_NESTING) { isContainer[blockDepth++] = false; } @@ -830,18 +932,7 @@ BasFormT *basFormRtLoadFrm(BasFormRtT *rt, const char *source, int32_t sourceLen } else if (form && nestDepth > 0) { // Create the content box on first control if not yet done if (!form->contentBox && form->root) { - // Use the root widget directly as the content box. - // wgtInitWindow already created a VBox root. If the - // form wants HBox, create one inside. Otherwise reuse - // root to avoid double-nesting (which doubles padding - // and can trigger unwanted scrollbars). - if (form->frmHBox) { - form->contentBox = wgtHBox(form->root); - form->contentBox->weight = 100; - } else { - form->contentBox = form->root; - } - + form->contentBox = dsgnCreateContentBox(form->root, form->frmLayout); parentStack[0] = form->contentBox; } WidgetT *parent = parentStack[nestDepth - 1]; @@ -861,19 +952,22 @@ BasFormT *basFormRtLoadFrm(BasFormRtT *rt, const char *source, int32_t sourceLen wgtSetName(widget, ctrlName); { - 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.index = -1; - ctrlEntry.widget = widget; - ctrlEntry.form = form; - ctrlEntry.iface = wgtGetIface(wgtTypeName); + BasControlT *ctrlEntry = (BasControlT *)calloc(1, sizeof(BasControlT)); + + if (!ctrlEntry) { + continue; + } + + snprintf(ctrlEntry->name, BAS_MAX_CTRL_NAME, "%s", ctrlName); + snprintf(ctrlEntry->typeName, BAS_MAX_CTRL_NAME, "%s", typeName); + ctrlEntry->index = -1; + 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]; + current = ctrlEntry; widget->userData = current; widget->onClick = onWidgetClick; widget->onDblClick = onWidgetDblClick; @@ -894,14 +988,17 @@ BasFormT *basFormRtLoadFrm(BasFormRtT *rt, const char *source, int32_t sourceLen const WgtIfaceT *ctrlIface = wgtGetIface(wgtTypeName); bool isCtrlContainer = ctrlIface && ctrlIface->isContainer; - if (isCtrlContainer && nestDepth < MAX_FRM_NESTING) { + if (isCtrlContainer && nestDepth < DSGN_MAX_FRM_NESTING) { + // Push the widget for now; the Layout property hasn't + // been parsed yet. It will be applied when we see it + // via containerLayout[] below. parentStack[nestDepth++] = widget; - if (blockDepth < MAX_FRM_NESTING) { + if (blockDepth < DSGN_MAX_FRM_NESTING) { isContainer[blockDepth++] = true; } } else { - if (blockDepth < MAX_FRM_NESTING) { + if (blockDepth < DSGN_MAX_FRM_NESTING) { isContainer[blockDepth++] = false; } } @@ -964,7 +1061,8 @@ BasFormT *basFormRtLoadFrm(BasFormRtT *rt, const char *source, int32_t sourceLen } if (strcasecmp(key, "Caption") == 0) { snprintf(curMenuItem->caption, sizeof(curMenuItem->caption), "%s", text); } - else if (strcasecmp(key, "Checked") == 0) { curMenuItem->checked = (strcasecmp(text, "True") == 0 || strcasecmp(text, "-1") == 0); } + else if (strcasecmp(key, "Checked") == 0) { curMenuItem->checked = (strcasecmp(text, "True") == 0 || strcasecmp(text, "-1") == 0); } + else if (strcasecmp(key, "RadioCheck") == 0) { curMenuItem->radioCheck = (strcasecmp(text, "True") == 0 || strcasecmp(text, "-1") == 0); } else if (strcasecmp(key, "Enabled") == 0) { curMenuItem->enabled = (strcasecmp(text, "True") == 0 || strcasecmp(text, "-1") == 0 || strcasecmp(text, "False") != 0); } } else if (current) { // Control array index is stored on the struct, not as a widget property @@ -973,6 +1071,21 @@ BasFormT *basFormRtLoadFrm(BasFormRtT *rt, const char *source, int32_t sourceLen continue; } + // Layout property on a container: replace the parentStack entry + // with a layout box inside the container widget. + // NOTE: only do this for non-default layouts (HBox, WrapBox). + // VBox is the default for Frame, so no wrapper needed. + if (strcasecmp(key, "Layout") == 0 && current && current->widget && nestDepth > 0) { + char *text = value; + if (text[0] == '"') { text++; } + int32_t tlen = (int32_t)strlen(text); + if (tlen > 0 && text[tlen - 1] == '"') { text[tlen - 1] = '\0'; } + if (strcasecmp(text, "VBox") != 0) { + parentStack[nestDepth - 1] = dsgnCreateContentBox(current->widget, text); + } + continue; + } + BasValueT val; if (value[0] == '"') { @@ -1020,9 +1133,7 @@ BasFormT *basFormRtLoadFrm(BasFormRtT *rt, const char *source, int32_t sourceLen } else if (strcasecmp(key, "AutoSize") == 0) { form->frmAutoSize = (strcasecmp(text, "True") == 0); } else if (strcasecmp(key, "Layout") == 0) { - if (strcasecmp(text, "HBox") == 0) { - form->frmHBox = true; - } + snprintf(form->frmLayout, sizeof(form->frmLayout), "%s", text); } } } @@ -1031,12 +1142,7 @@ BasFormT *basFormRtLoadFrm(BasFormRtT *rt, const char *source, int32_t sourceLen if (form) { // Ensure content box exists even if form has no controls if (!form->contentBox && form->root) { - if (form->frmHBox) { - form->contentBox = wgtHBox(form->root); - form->contentBox->weight = 100; - } else { - form->contentBox = form->root; - } + form->contentBox = dsgnCreateContentBox(form->root, form->frmLayout); } // Build menu bar from accumulated menu items @@ -1068,7 +1174,9 @@ BasFormT *basFormRtLoadFrm(BasFormRtT *rt, const char *source, int32_t sourceLen // Regular menu item int32_t id = MENU_ID_BASE + i; - if (mi->checked) { + if (mi->radioCheck) { + wmAddMenuRadioItem(menuStack[mi->level - 1], mi->caption, id, mi->checked); + } else if (mi->checked) { wmAddMenuCheckItem(menuStack[mi->level - 1], mi->caption, id, true); } else { wmAddMenuItem(menuStack[mi->level - 1], mi->caption, id); @@ -1095,38 +1203,50 @@ BasFormT *basFormRtLoadFrm(BasFormRtT *rt, const char *source, int32_t sourceLen arrfree(menuItems); menuItems = NULL; - // Set resizable flag + // Call Resize on widgets that need it (e.g. PictureBox) so the + // internal bitmap matches the layout size. Width/Height aren't + // known until properties are parsed, so this must happen after + // loading but before dvxFitWindow. + for (int32_t i = 0; i < form->controlCount; i++) { + if (!form->controls[i]->widget) { + continue; + } + + const WgtIfaceT *ifc = form->controls[i]->iface; + + if (ifc) { + WidgetT *wgt = form->controls[i]->widget; + + for (int32_t m = 0; m < ifc->methodCount; m++) { + if (strcasecmp(ifc->methods[m].name, "Resize") == 0 && + ifc->methods[m].sig == WGT_SIG_INT_INT && + wgt->minW > 0 && wgt->minH > 0) { + ((void (*)(WidgetT *, int32_t, int32_t))ifc->methods[m].fn)(wgt, wgt->minW, wgt->minH); + break; + } + } + } + } + + // Apply form properties after Resize calls so that + // PictureBox bitmaps are resized before dvxFitWindow. if (form->frmHasResizable) { form->window->resizable = form->frmResizable; } - // Size and position the window if (form->frmAutoSize) { dvxFitWindow(rt->ctx, form->window); } else if (form->frmWidth > 0 && form->frmHeight > 0) { dvxResizeWindow(rt->ctx, form->window, form->frmWidth, form->frmHeight); - } else { - dvxFitWindow(rt->ctx, form->window); } - // Position: centered or explicit left/top if (form->frmCentered) { - int32_t scrW = rt->ctx->display.width; - int32_t scrH = rt->ctx->display.height; - form->window->x = (scrW - form->window->w) / 2; - form->window->y = (scrH - form->window->h) / 2; + form->window->x = (rt->ctx->display.width - form->window->w) / 2; + form->window->y = (rt->ctx->display.height - form->window->h) / 2; } else if (form->frmLeft > 0 || form->frmTop > 0) { 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]; - } - } } // Cache the .frm source for reload after unload @@ -1165,7 +1285,7 @@ BasFormT *basFormRtLoadFrm(BasFormRtT *rt, const char *source, int32_t sourceLen // Detail controls are refreshed automatically by the master-detail cascade // when the master's Reposition event fires. for (int32_t i = 0; i < form->controlCount; i++) { - BasControlT *dc = &form->controls[i]; + BasControlT *dc = form->controls[i]; if (strcasecmp(dc->typeName, "Data") != 0 || !dc->widget) { continue; @@ -1213,10 +1333,107 @@ int32_t basFormRtMsgBox(void *ctx, const char *message, int32_t flags) { // ============================================================ void basFormRtSetProp(void *ctx, void *ctrlRef, const char *propName, BasValueT value) { - (void)ctx; + BasFormRtT *rt = (BasFormRtT *)ctx; BasControlT *ctrl = (BasControlT *)ctrlRef; - if (!ctrl || !ctrl->widget) { + if (!ctrl) { + return; + } + + // Menu item properties + if (ctrl->menuId > 0 && ctrl->form && ctrl->form->window && ctrl->form->window->menuBar) { + MenuBarT *bar = ctrl->form->window->menuBar; + + if (strcasecmp(propName, "Checked") == 0) { + wmMenuItemSetChecked(bar, ctrl->menuId, basValIsTruthy(value)); + return; + } + + if (strcasecmp(propName, "Enabled") == 0) { + wmMenuItemSetEnabled(bar, ctrl->menuId, basValIsTruthy(value)); + return; + } + + return; + } + + if (!ctrl->widget) { + return; + } + + // Form-level property assignment uses the window and BasFormT + if (ctrl->form && ctrl == &ctrl->form->formCtrl) { + WindowT *win = ctrl->form->window; + BasFormT *frm = ctrl->form; + + if (strcasecmp(propName, "Caption") == 0) { + BasStringT *s = basValFormatString(value); + if (win) { dvxSetTitle(rt->ctx, win, s->data); } + basStringUnref(s); + return; + } + + if (strcasecmp(propName, "Visible") == 0) { + if (win) { + if (basValIsTruthy(value)) { + dvxShowWindow(rt->ctx, win); + } else { + dvxHideWindow(rt->ctx, win); + } + } + return; + } + + if (strcasecmp(propName, "Width") == 0 && win) { + dvxResizeWindow(rt->ctx, win, (int32_t)basValToNumber(value), win->h); + return; + } + + if (strcasecmp(propName, "Height") == 0 && win) { + dvxResizeWindow(rt->ctx, win, win->w, (int32_t)basValToNumber(value)); + return; + } + + if (strcasecmp(propName, "Left") == 0 && win) { + dirtyListAdd(&rt->ctx->dirty, win->x, win->y, win->w, win->h); + win->x = (int32_t)basValToNumber(value); + dirtyListAdd(&rt->ctx->dirty, win->x, win->y, win->w, win->h); + return; + } + + if (strcasecmp(propName, "Top") == 0 && win) { + dirtyListAdd(&rt->ctx->dirty, win->x, win->y, win->w, win->h); + win->y = (int32_t)basValToNumber(value); + dirtyListAdd(&rt->ctx->dirty, win->x, win->y, win->w, win->h); + return; + } + + if (strcasecmp(propName, "Resizable") == 0 && win) { + dirtyListAdd(&rt->ctx->dirty, win->x, win->y, win->w, win->h); + win->resizable = basValIsTruthy(value); + dirtyListAdd(&rt->ctx->dirty, win->x, win->y, win->w, win->h); + return; + } + + if (strcasecmp(propName, "AutoSize") == 0) { + frm->frmAutoSize = basValIsTruthy(value); + if (frm->frmAutoSize && win) { + dvxFitWindow(rt->ctx, win); + } + return; + } + + if (strcasecmp(propName, "Centered") == 0 && win) { + frm->frmCentered = basValIsTruthy(value); + if (frm->frmCentered) { + dirtyListAdd(&rt->ctx->dirty, win->x, win->y, win->w, win->h); + win->x = (rt->ctx->display.width - win->w) / 2; + win->y = (rt->ctx->display.height - win->h) / 2; + dirtyListAdd(&rt->ctx->dirty, win->x, win->y, win->w, win->h); + } + return; + } + return; } @@ -1225,9 +1442,9 @@ void basFormRtSetProp(void *ctx, void *ctrlRef, const char *propName, BasValueT return; } - // "Caption" and "Text" map to wgtSetText for all widgets. - // Copy to persistent buffer since some widgets (Button, Label) - // store the text pointer without copying. + // "Caption" and "Text": save to the persistent textBuf and apply + // immediately. Controls are heap-allocated so textBuf addresses + // are stable across arrput calls. if (strcasecmp(propName, "Caption") == 0 || strcasecmp(propName, "Text") == 0) { BasStringT *s = basValFormatString(value); snprintf(ctrl->textBuf, BAS_MAX_TEXT_BUF, "%s", s->data); @@ -1311,9 +1528,13 @@ void basFormRtUnloadForm(void *ctx, void *formRef) { } for (int32_t i = 0; i < form->controlCount; i++) { - freeListBoxItems(&form->controls[i]); + free(form->controls[i]); } + arrfree(form->controls); + form->controls = NULL; + form->controlCount = 0; + if (form->window) { if (rt->ctx->modalWindow == form->window) { rt->ctx->modalWindow = NULL; @@ -1325,16 +1546,24 @@ void basFormRtUnloadForm(void *ctx, void *formRef) { form->contentBox = NULL; } - int32_t idx = (int32_t)(form - rt->forms); + int32_t idx = -1; + + for (int32_t i = 0; i < rt->formCount; i++) { + if (rt->forms[i] == form) { + idx = i; + break; + } + } if (idx >= 0 && idx < rt->formCount) { + free(form); rt->formCount--; if (idx < rt->formCount) { rt->forms[idx] = rt->forms[rt->formCount]; } - memset(&rt->forms[rt->formCount], 0, sizeof(BasFormT)); + rt->forms[rt->formCount] = NULL; } } @@ -1361,80 +1590,6 @@ static BasValueT callCommonMethod(BasControlT *ctrl, const char *methodName, Bas } -// ============================================================ -// callListBoxMethod -// ============================================================ -// -// Handles AddItem/RemoveItem/Clear/List for any widget that -// supports item lists (listbox, combobox, dropdown). - -static BasValueT callListBoxMethod(BasControlT *ctrl, const char *methodName, BasValueT *args, int32_t argc) { - if (strcasecmp(methodName, "AddItem") == 0) { - if (argc >= 1 && args[0].type == BAS_TYPE_STRING && args[0].strVal) { - ListBoxItemsT *lb = getListBoxItems(ctrl); - - if (lb && lb->count < MAX_LISTBOX_ITEMS) { - lb->items[lb->count] = strdup(args[0].strVal->data); - lb->count++; - rebuildListBoxItems(ctrl); - } - } - - return zeroValue(); - } - - if (strcasecmp(methodName, "RemoveItem") == 0) { - if (argc >= 1) { - int32_t idx = (int32_t)basValToNumber(args[0]); - ListBoxItemsT *lb = getListBoxItems(ctrl); - - if (lb && idx >= 0 && idx < lb->count) { - free(lb->items[idx]); - - for (int32_t i = idx; i < lb->count - 1; i++) { - lb->items[i] = lb->items[i + 1]; - } - - lb->count--; - rebuildListBoxItems(ctrl); - } - } - - return zeroValue(); - } - - if (strcasecmp(methodName, "Clear") == 0) { - ListBoxItemsT *lb = getListBoxItems(ctrl); - - if (lb) { - for (int32_t i = 0; i < lb->count; i++) { - free(lb->items[i]); - } - - lb->count = 0; - rebuildListBoxItems(ctrl); - } - - return zeroValue(); - } - - if (strcasecmp(methodName, "List") == 0) { - if (argc >= 1) { - int32_t idx = (int32_t)basValToNumber(args[0]); - ListBoxItemsT *lb = getListBoxItems(ctrl); - - if (lb && idx >= 0 && idx < lb->count) { - return basValStringFromC(lb->items[idx]); - } - } - - return basValStringFromC(""); - } - - return zeroValue(); -} - - // ============================================================ // createWidget // ============================================================ @@ -1442,6 +1597,8 @@ static BasValueT callListBoxMethod(BasControlT *ctrl, const char *methodName, Ba // Create a DVX widget by type name using its registered API. // The API's create function signature varies by widget type, so // we call with sensible defaults for each creation pattern. +// For INT_INT types (e.g. PictureBox), hintW/hintH override the +// default createArgs if non-zero (so the bitmap matches the .frm size). static WidgetT *createWidget(const char *wgtTypeName, WidgetT *parent) { const void *api = wgtGetApi(wgtTypeName); @@ -1497,36 +1654,6 @@ static WidgetT *createWidget(const char *wgtTypeName, WidgetT *parent) { } -// ============================================================ -// freeListBoxItems -// ============================================================ - -static void freeListBoxItems(BasControlT *ctrl) { - for (int32_t i = 0; i < sAuxDataCount; i++) { - if (sAuxData[i].ctrl == ctrl) { - ListBoxItemsT *lb = sAuxData[i].items; - - if (lb) { - for (int32_t j = 0; j < lb->count; j++) { - free(lb->items[j]); - } - - free(lb); - } - - sAuxDataCount--; - - if (i < sAuxDataCount) { - sAuxData[i] = sAuxData[sAuxDataCount]; - } - - memset(&sAuxData[sAuxDataCount], 0, sizeof(AuxDataEntryT)); - return; - } - } -} - - // ============================================================ // getCommonProp // ============================================================ @@ -1642,35 +1769,6 @@ static BasValueT getIfaceProp(const WgtIfaceT *iface, WidgetT *w, const char *pr } -// ============================================================ -// getListBoxItems -// ============================================================ - -static ListBoxItemsT *getListBoxItems(BasControlT *ctrl) { - for (int32_t i = 0; i < sAuxDataCount; i++) { - if (sAuxData[i].ctrl == ctrl) { - return sAuxData[i].items; - } - } - - if (sAuxDataCount >= MAX_AUX_DATA) { - return NULL; - } - - ListBoxItemsT *lb = (ListBoxItemsT *)calloc(1, sizeof(ListBoxItemsT)); - - if (!lb) { - return NULL; - } - - sAuxData[sAuxDataCount].ctrl = ctrl; - sAuxData[sAuxDataCount].items = lb; - sAuxDataCount++; - - return lb; -} - - // ============================================================ // onFormClose // ============================================================ @@ -1683,7 +1781,7 @@ static void onFormMenu(WindowT *win, int32_t menuId) { } for (int32_t i = 0; i < sFormRt->formCount; i++) { - BasFormT *form = &sFormRt->forms[i]; + BasFormT *form = sFormRt->forms[i]; if (form->window == win) { for (int32_t j = 0; j < form->menuIdMapCount; j++) { @@ -1712,7 +1810,7 @@ static void onFormClose(WindowT *win) { } for (int32_t i = 0; i < sFormRt->formCount; i++) { - BasFormT *form = &sFormRt->forms[i]; + BasFormT *form = sFormRt->forms[i]; if (form->window == win) { // QueryUnload: if Cancel is set, abort the close @@ -1735,27 +1833,29 @@ static void onFormClose(WindowT *win) { // Free control resources for (int32_t j = 0; j < form->controlCount; j++) { - freeListBoxItems(&form->controls[j]); + free(form->controls[j]); } + arrfree(form->controls); + form->controls = NULL; + form->controlCount = 0; + // Destroy the window if (sFormRt->ctx->modalWindow == win) { sFormRt->ctx->modalWindow = NULL; } dvxDestroyWindow(sFormRt->ctx, win); - form->window = NULL; - form->root = NULL; - form->contentBox = NULL; - // Remove from form list + // Remove from form list and free the form + free(form); sFormRt->formCount--; if (i < sFormRt->formCount) { sFormRt->forms[i] = sFormRt->forms[sFormRt->formCount]; } - memset(&sFormRt->forms[sFormRt->formCount], 0, sizeof(BasFormT)); + sFormRt->forms[sFormRt->formCount] = NULL; // If no forms left, stop the VM if (sFormRt->formCount == 0 && sFormRt->vm) { @@ -1781,7 +1881,7 @@ static void onFormResize(WindowT *win, int32_t newW, int32_t newH) { } for (int32_t i = 0; i < sFormRt->formCount; i++) { - BasFormT *form = &sFormRt->forms[i]; + BasFormT *form = sFormRt->forms[i]; if (form->window == win) { basFormRtFireEvent(sFormRt, form, form->name, "Resize"); @@ -1801,7 +1901,7 @@ static void onFormActivate(WindowT *win) { } for (int32_t i = 0; i < sFormRt->formCount; i++) { - BasFormT *form = &sFormRt->forms[i]; + BasFormT *form = sFormRt->forms[i]; if (form->window == win) { basFormRtFireEvent(sFormRt, form, form->name, "Activate"); @@ -1817,7 +1917,7 @@ static void onFormDeactivate(WindowT *win) { } for (int32_t i = 0; i < sFormRt->formCount; i++) { - BasFormT *form = &sFormRt->forms[i]; + BasFormT *form = sFormRt->forms[i]; if (form->window == win) { basFormRtFireEvent(sFormRt, form, form->name, "Deactivate"); @@ -1923,7 +2023,7 @@ static void onWidgetBlur(WidgetT *w) { // cache and persist to the database if (ctrl->dataSource[0] && ctrl->dataField[0] && ctrl->widget && ctrl->form) { for (int32_t i = 0; i < ctrl->form->controlCount; i++) { - BasControlT *dc = &ctrl->form->controls[i]; + BasControlT *dc = ctrl->form->controls[i]; if (strcasecmp(dc->typeName, "Data") == 0 && strcasecmp(dc->name, ctrl->dataSource) == 0 && @@ -1949,7 +2049,7 @@ static void refreshDetailControls(BasFormT *form, BasControlT *masterCtrl) { } for (int32_t i = 0; i < form->controlCount; i++) { - BasControlT *ctrl = &form->controls[i]; + BasControlT *ctrl = form->controls[i]; if (strcasecmp(ctrl->typeName, "Data") != 0 || !ctrl->widget || !ctrl->iface) { continue; @@ -2001,7 +2101,7 @@ static void updateBoundControls(BasFormT *form, BasControlT *dataCtrl) { } for (int32_t i = 0; i < form->controlCount; i++) { - BasControlT *ctrl = &form->controls[i]; + BasControlT *ctrl = form->controls[i]; if (!ctrl->dataSource[0] || strcasecmp(ctrl->dataSource, dataCtrl->name) != 0) { continue; @@ -2274,64 +2374,6 @@ static void parseFrmLine(const char *line, char *key, char *value) { } -// ============================================================ -// rebuildListBoxItems -// ============================================================ - -static void rebuildListBoxItems(BasControlT *ctrl) { - ListBoxItemsT *lb = getListBoxItems(ctrl); - - if (!lb) { - return; - } - - // Use the widget's setItems API if available - const WgtIfaceT *iface = ctrl->iface; - - if (!iface) { - return; - } - - // Look for a setItems-like method by checking the widget API directly - const void *api = wgtGetApi("listbox"); - - if (api) { - // ListBoxApiT has setItems as the second function pointer - typedef struct { - void *create; - void (*setItems)(WidgetT *, const char **, int32_t); - } SetItemsPatternT; - const SetItemsPatternT *p = (const SetItemsPatternT *)api; - p->setItems(ctrl->widget, (const char **)lb->items, lb->count); - return; - } - - // Fallback: try combobox or dropdown API - api = wgtGetApi("combobox"); - - if (api) { - typedef struct { - void *create; - void (*setItems)(WidgetT *, const char **, int32_t); - } SetItemsPatternT; - const SetItemsPatternT *p = (const SetItemsPatternT *)api; - p->setItems(ctrl->widget, (const char **)lb->items, lb->count); - return; - } - - api = wgtGetApi("dropdown"); - - if (api) { - typedef struct { - void *create; - void (*setItems)(WidgetT *, const char **, int32_t); - } SetItemsPatternT; - const SetItemsPatternT *p = (const SetItemsPatternT *)api; - p->setItems(ctrl->widget, (const char **)lb->items, lb->count); - } -} - - // ============================================================ // resolveTypeName // ============================================================ @@ -2424,13 +2466,13 @@ static bool setCommonProp(BasControlT *ctrl, const char *propName, BasValueT val if (strcasecmp(propName, "BackColor") == 0) { ctrl->widget->bgColor = (uint32_t)(int32_t)basValToNumber(value); - wgtInvalidate(ctrl->widget); + wgtInvalidatePaint(ctrl->widget); return true; } if (strcasecmp(propName, "ForeColor") == 0) { ctrl->widget->fgColor = (uint32_t)(int32_t)basValToNumber(value); - wgtInvalidate(ctrl->widget); + wgtInvalidatePaint(ctrl->widget); return true; } diff --git a/apps/dvxbasic/formrt/formrt.h b/apps/dvxbasic/formrt/formrt.h index bfc5679..125385d 100644 --- a/apps/dvxbasic/formrt/formrt.h +++ b/apps/dvxbasic/formrt/formrt.h @@ -33,8 +33,9 @@ typedef struct BasControlT BasControlT; // ============================================================ typedef struct { - int32_t id; - char name[BAS_MAX_CTRL_NAME]; + int32_t id; + char name[BAS_MAX_CTRL_NAME]; + BasControlT *proxy; // heap-allocated proxy for property access (widget=NULL, menuId stored) } BasMenuIdMapT; // ============================================================ @@ -53,6 +54,7 @@ typedef struct BasControlT { char textBuf[BAS_MAX_TEXT_BUF]; // persistent text for Caption/Text char dataSource[BAS_MAX_CTRL_NAME]; // name of Data control for binding char dataField[BAS_MAX_CTRL_NAME]; // column name for binding + int32_t menuId; // WM menu item ID (>0 for menu items, 0 for controls) } BasControlT; // ============================================================ @@ -65,7 +67,7 @@ typedef struct BasFormT { WidgetT *root; // widget root (from wgtInitWindow) WidgetT *contentBox; // VBox/HBox for user controls AppContextT *ctx; // DVX app context - BasControlT *controls; // stb_ds dynamic array + BasControlT **controls; // stb_ds array of heap-allocated pointers int32_t controlCount; BasVmT *vm; // VM for event dispatch BasModuleT *module; // compiled module (for SUB lookup) @@ -78,13 +80,16 @@ typedef struct BasFormT { bool frmHasResizable; // true if Resizable was explicitly set bool frmCentered; bool frmAutoSize; - bool frmHBox; // true if Layout = "HBox" + char frmLayout[32]; // "VBox", "HBox", or "WrapBox" // Per-form variable storage (allocated at load, freed at unload) BasValueT *formVars; int32_t formVarCount; // Menu ID to name mapping (for event dispatch) BasMenuIdMapT *menuIdMap; int32_t menuIdMapCount; + // Synthetic control entry for the form itself, so that + // FormName.Property works through the same getProp/setProp path. + BasControlT formCtrl; } BasFormT; // ============================================================ @@ -102,7 +107,7 @@ typedef struct { AppContextT *ctx; // DVX app context BasVmT *vm; // shared VM instance BasModuleT *module; // compiled module - BasFormT *forms; // stb_ds dynamic array + BasFormT **forms; // stb_ds array of heap-allocated pointers int32_t formCount; BasFormT *currentForm; // form currently dispatching events BasFrmCacheT *frmCache; // stb_ds array of cached .frm sources diff --git a/apps/dvxbasic/ide/ideDesigner.c b/apps/dvxbasic/ide/ideDesigner.c index e8593e4..c663f87 100644 --- a/apps/dvxbasic/ide/ideDesigner.c +++ b/apps/dvxbasic/ide/ideDesigner.c @@ -115,6 +115,84 @@ WidgetT *dsgnCreateDesignWidget(const char *vbTypeName, WidgetT *parent) { } +// ============================================================ +// dsgnCreateFormWindow +// ============================================================ + +WidgetT *dsgnCreateContentBox(WidgetT *root, const char *layout) { + // wgtInitWindow creates a VBox root. If the requested layout is VBox + // (or empty/missing), reuse root directly to avoid double-nesting. + if (!layout || !layout[0] || strcasecmp(layout, "VBox") == 0) { + return root; + } + + // Look up the layout widget by BASIC name and create it dynamically + const char *wgtName = wgtFindByBasName(layout); + + if (wgtName) { + const WgtIfaceT *iface = wgtGetIface(wgtName); + + if (iface && iface->isContainer && iface->createSig == WGT_CREATE_PARENT) { + const void *api = wgtGetApi(wgtName); + + if (api) { + // All WGT_CREATE_PARENT APIs have create(parent) as the first function pointer + WidgetT *(*createFn)(WidgetT *) = *(WidgetT *(*const *)(WidgetT *))api; + WidgetT *box = createFn(root); + box->weight = 100; + return box; + } + } + } + + // Unknown layout — fall back to root VBox + return root; +} + + +WindowT *dsgnCreateFormWindow(AppContextT *ctx, const char *title, const char *layout, bool resizable, bool centered, bool autoSize, int32_t width, int32_t height, int32_t left, int32_t top, WidgetT **outRoot, WidgetT **outContentBox) { + int32_t defW = (width > 0) ? width : 400; + int32_t defH = (height > 0) ? height : 300; + + WindowT *win = dvxCreateWindowCentered(ctx, title, defW, defH, resizable); + + if (!win) { + return NULL; + } + + win->visible = false; + + WidgetT *root = wgtInitWindow(ctx, win); + + if (!root) { + dvxDestroyWindow(ctx, win); + return NULL; + } + + WidgetT *contentBox = dsgnCreateContentBox(root, layout); + + // Apply sizing + if (autoSize) { + dvxFitWindow(ctx, win); + } else if (width > 0 && height > 0) { + dvxResizeWindow(ctx, win, width, height); + } + + // Apply positioning + if (centered) { + win->x = (ctx->display.width - win->w) / 2; + win->y = (ctx->display.height - win->h) / 2; + } else if (left > 0 || top > 0) { + win->x = left; + win->y = top; + } + + *outRoot = root; + *outContentBox = contentBox; + return win; +} + + // ============================================================ // dsgnBuildPreviewMenuBar // ============================================================ @@ -151,7 +229,15 @@ void dsgnBuildPreviewMenuBar(WindowT *win, const DsgnFormT *form) { } else if (isSubParent && mi->level > 0 && mi->level < 8 && menuStack[mi->level - 1]) { menuStack[mi->level] = wmAddSubMenu(menuStack[mi->level - 1], mi->caption); } else if (mi->level > 0 && mi->level < 8 && menuStack[mi->level - 1]) { - wmAddMenuItem(menuStack[mi->level - 1], mi->caption, -1); + int32_t id = DSGN_MENU_ID_BASE + i; + + if (mi->radioCheck) { + wmAddMenuRadioItem(menuStack[mi->level - 1], mi->caption, id, mi->checked); + } else if (mi->checked) { + wmAddMenuCheckItem(menuStack[mi->level - 1], mi->caption, id, true); + } else { + wmAddMenuItem(menuStack[mi->level - 1], mi->caption, id); + } } } } @@ -180,8 +266,8 @@ void dsgnAutoName(const DsgnStateT *ds, const char *typeName, char *buf, int32_t int32_t count = ds->form ? (int32_t)arrlen(ds->form->controls) : 0; for (int32_t i = 0; i < count; i++) { - if (strncasecmp(ds->form->controls[i].name, prefix, prefixLen) == 0) { - int32_t num = atoi(ds->form->controls[i].name + prefixLen); + if (strncasecmp(ds->form->controls[i]->name, prefix, prefixLen) == 0) { + int32_t num = atoi(ds->form->controls[i]->name + prefixLen); if (num > highest) { highest = num; @@ -209,20 +295,36 @@ void dsgnCreateWidgets(DsgnStateT *ds, WidgetT *contentBox) { // then parent children inside their containers. // Pass 1: create all widgets as top-level children for (int32_t i = 0; i < count; i++) { - DsgnControlT *ctrl = &ds->form->controls[i]; + DsgnControlT *ctrl = ds->form->controls[i]; if (ctrl->widget) { continue; } - // Find the parent widget + // Find the parent widget. For containers with a non-VBox Layout + // property, create a content box inside so children use the + // correct layout direction. WidgetT *parent = contentBox; if (ctrl->parentName[0]) { for (int32_t j = 0; j < count; j++) { - if (j != i && ds->form->controls[j].widget && - strcasecmp(ds->form->controls[j].name, ctrl->parentName) == 0) { - parent = ds->form->controls[j].widget; + if (j != i && ds->form->controls[j]->widget && + strcasecmp(ds->form->controls[j]->name, ctrl->parentName) == 0) { + DsgnControlT *pc = ds->form->controls[j]; + parent = pc->widget; + const char *layout = getPropValue(pc, "Layout"); + + if (layout && layout[0] && strcasecmp(layout, "VBox") != 0) { + // Check if we already created a content box inside + if (!parent->firstChild || !parent->firstChild->userData || + parent->firstChild->userData != (void *)pc) { + WidgetT *box = dsgnCreateContentBox(parent, layout); + box->userData = (void *)pc; + } + + parent = parent->firstChild; + } + break; } } @@ -350,6 +452,9 @@ const char *dsgnDefaultEvent(const char *typeName) { void dsgnFree(DsgnStateT *ds) { if (ds->form) { + for (int32_t i = 0; i < arrlen(ds->form->controls); i++) { + free(ds->form->controls[i]); + } arrfree(ds->form->controls); arrfree(ds->form->menuItems); free(ds->form->code); @@ -407,6 +512,8 @@ bool dsgnLoadFrm(DsgnStateT *ds, const char *source, int32_t sourceLen) { bool inForm = false; bool inMenu = false; int32_t menuNestDepth = 0; + int32_t blockDepth = 0; // Begin/End nesting depth (0 = form level) + bool blockIsContainer[DSGN_MAX_FRM_NESTING]; // whether each block is a container // Parent name stack for nesting (index 0 = form level) char parentStack[8][DSGN_MAX_NAME]; @@ -507,25 +614,29 @@ bool dsgnLoadFrm(DsgnStateT *ds, const char *source, int32_t sourceLen) { curCtrl = NULL; // not a control menuNestDepth++; inMenu = true; + if (blockDepth < DSGN_MAX_FRM_NESTING) { blockIsContainer[blockDepth] = false; } + blockDepth++; } else if (inForm) { - DsgnControlT ctrl; - memset(&ctrl, 0, sizeof(ctrl)); - ctrl.index = -1; - snprintf(ctrl.name, DSGN_MAX_NAME, "%s", ctrlName); - snprintf(ctrl.typeName, DSGN_MAX_NAME, "%s", typeName); + DsgnControlT *cp = (DsgnControlT *)calloc(1, sizeof(DsgnControlT)); + cp->index = -1; + snprintf(cp->name, DSGN_MAX_NAME, "%s", ctrlName); + snprintf(cp->typeName, DSGN_MAX_NAME, "%s", typeName); // Set parent from current nesting if (nestDepth > 0) { - snprintf(ctrl.parentName, DSGN_MAX_NAME, "%s", parentStack[nestDepth - 1]); + snprintf(cp->parentName, DSGN_MAX_NAME, "%s", parentStack[nestDepth - 1]); } - ctrl.width = DEFAULT_CTRL_W; - ctrl.height = DEFAULT_CTRL_H; - arrput(form->controls, ctrl); - curCtrl = &form->controls[arrlen(form->controls) - 1]; + cp->width = DEFAULT_CTRL_W; + cp->height = DEFAULT_CTRL_H; + arrput(form->controls, cp); + curCtrl = form->controls[arrlen(form->controls) - 1]; + bool isCtrl = dsgnIsContainer(typeName); + if (blockDepth < DSGN_MAX_FRM_NESTING) { blockIsContainer[blockDepth] = isCtrl; } + blockDepth++; // If this is a container, push onto parent stack - if (dsgnIsContainer(typeName) && nestDepth < 7) { + if (isCtrl && nestDepth < 7) { snprintf(parentStack[nestDepth], DSGN_MAX_NAME, "%s", ctrlName); nestDepth++; } @@ -535,22 +646,27 @@ bool dsgnLoadFrm(DsgnStateT *ds, const char *source, int32_t sourceLen) { } if (strcasecmp(trimmed, "End") == 0) { - if (inMenu) { - menuNestDepth--; - curMenuItem = NULL; + if (blockDepth > 0) { + blockDepth--; - if (menuNestDepth <= 0) { - menuNestDepth = 0; - inMenu = false; - } - } else if (curCtrl) { - // If we're closing a container, pop the parent stack - if (nestDepth > 0 && strcasecmp(parentStack[nestDepth - 1], curCtrl->name) == 0) { - nestDepth--; - } + if (inMenu) { + menuNestDepth--; + curMenuItem = NULL; - curCtrl = NULL; + if (menuNestDepth <= 0) { + menuNestDepth = 0; + inMenu = false; + } + } else { + // If we're closing a container, pop the parent stack + if (blockDepth < DSGN_MAX_FRM_NESTING && blockIsContainer[blockDepth] && nestDepth > 0) { + nestDepth--; + } + + curCtrl = NULL; + } } else { + // blockDepth == 0: this is the form's closing End inForm = false; // Everything after the form's closing End is code @@ -619,9 +735,10 @@ bool dsgnLoadFrm(DsgnStateT *ds, const char *source, int32_t sourceLen) { val[vi] = '\0'; if (curMenuItem) { - if (strcasecmp(key, "Caption") == 0) { snprintf(curMenuItem->caption, DSGN_MAX_TEXT, "%s", val); } - else if (strcasecmp(key, "Checked") == 0) { curMenuItem->checked = (strcasecmp(val, "True") == 0 || strcasecmp(val, "-1") == 0); } - else if (strcasecmp(key, "Enabled") == 0) { curMenuItem->enabled = (strcasecmp(val, "True") == 0 || strcasecmp(val, "-1") == 0); } + if (strcasecmp(key, "Caption") == 0) { snprintf(curMenuItem->caption, DSGN_MAX_TEXT, "%s", val); } + else if (strcasecmp(key, "Checked") == 0) { curMenuItem->checked = (strcasecmp(val, "True") == 0 || strcasecmp(val, "-1") == 0); } + else if (strcasecmp(key, "RadioCheck") == 0) { curMenuItem->radioCheck = (strcasecmp(val, "True") == 0 || strcasecmp(val, "-1") == 0); } + else if (strcasecmp(key, "Enabled") == 0) { curMenuItem->enabled = (strcasecmp(val, "True") == 0 || strcasecmp(val, "-1") == 0); } } else if (curCtrl) { if (strcasecmp(key, "Left") == 0) { curCtrl->left = atoi(val); } else if (strcasecmp(key, "Top") == 0) { curCtrl->top = atoi(val); } @@ -693,11 +810,12 @@ void dsgnOnKey(DsgnStateT *ds, int32_t key) { // Delete key -- remove the selected control and any children if (key == 0x153 && ds->selectedIdx >= 0 && ds->selectedIdx < count) { - const char *delName = ds->form->controls[ds->selectedIdx].name; + const char *delName = ds->form->controls[ds->selectedIdx]->name; // Remove children first (controls whose parentName matches) for (int32_t i = count - 1; i >= 0; i--) { - if (i != ds->selectedIdx && strcasecmp(ds->form->controls[i].parentName, delName) == 0) { + if (i != ds->selectedIdx && strcasecmp(ds->form->controls[i]->parentName, delName) == 0) { + free(ds->form->controls[i]); arrdel(ds->form->controls, i); if (i < ds->selectedIdx) { @@ -706,6 +824,7 @@ void dsgnOnKey(DsgnStateT *ds, int32_t key) { } } + free(ds->form->controls[ds->selectedIdx]); arrdel(ds->form->controls, ds->selectedIdx); ds->selectedIdx = -1; ds->form->dirty = true; @@ -728,7 +847,7 @@ void dsgnOnMouse(DsgnStateT *ds, int32_t x, int32_t y, bool drag) { if (drag) { if (ds->mode == DSGN_RESIZING && ds->selectedIdx >= 0 && ds->selectedIdx < ctrlCount) { - DsgnControlT *ctrl = &ds->form->controls[ds->selectedIdx]; + DsgnControlT *ctrl = ds->form->controls[ds->selectedIdx]; int32_t dx = x - ds->dragStartX; int32_t dy = y - ds->dragStartY; @@ -755,15 +874,15 @@ void dsgnOnMouse(DsgnStateT *ds, int32_t x, int32_t y, bool drag) { } else if (ds->mode == DSGN_REORDERING && ds->selectedIdx >= 0 && ds->selectedIdx < ctrlCount) { // Determine if we should swap with a neighbor based on drag direction int32_t dy = y - ds->dragStartY; - DsgnControlT *ctrl = &ds->form->controls[ds->selectedIdx]; + DsgnControlT *ctrl = ds->form->controls[ds->selectedIdx]; if (dy > 0 && ctrl->widget) { // Dragging down -- swap with next control if past its midpoint if (ds->selectedIdx < ctrlCount - 1) { - DsgnControlT *next = &ds->form->controls[ds->selectedIdx + 1]; + DsgnControlT *next = ds->form->controls[ds->selectedIdx + 1]; if (next->widget && y > next->widget->y + next->widget->h / 2) { - DsgnControlT tmp = ds->form->controls[ds->selectedIdx]; + DsgnControlT *tmp = ds->form->controls[ds->selectedIdx]; ds->form->controls[ds->selectedIdx] = ds->form->controls[ds->selectedIdx + 1]; ds->form->controls[ds->selectedIdx + 1] = tmp; rebuildWidgets(ds); @@ -775,10 +894,10 @@ void dsgnOnMouse(DsgnStateT *ds, int32_t x, int32_t y, bool drag) { } else if (dy < 0 && ctrl->widget) { // Dragging up -- swap with previous control if past its midpoint if (ds->selectedIdx > 0) { - DsgnControlT *prev = &ds->form->controls[ds->selectedIdx - 1]; + DsgnControlT *prev = ds->form->controls[ds->selectedIdx - 1]; if (prev->widget && y < prev->widget->y + prev->widget->h / 2) { - DsgnControlT tmp = ds->form->controls[ds->selectedIdx]; + DsgnControlT *tmp = ds->form->controls[ds->selectedIdx]; ds->form->controls[ds->selectedIdx] = ds->form->controls[ds->selectedIdx - 1]; ds->form->controls[ds->selectedIdx - 1] = tmp; rebuildWidgets(ds); @@ -803,10 +922,10 @@ void dsgnOnMouse(DsgnStateT *ds, int32_t x, int32_t y, bool drag) { if (ds->activeTool[0] == '\0') { // Check grab handles of selected control first if (ds->selectedIdx >= 0 && ds->selectedIdx < ctrlCount) { - DsgnHandleE handle = hitTestHandles(&ds->form->controls[ds->selectedIdx], x, y); + DsgnHandleE handle = hitTestHandles(ds->form->controls[ds->selectedIdx], x, y); if (handle != HANDLE_NONE) { - DsgnControlT *ctrl = &ds->form->controls[ds->selectedIdx]; + DsgnControlT *ctrl = ds->form->controls[ds->selectedIdx]; ds->mode = DSGN_RESIZING; ds->activeHandle = handle; ds->dragStartX = x; @@ -833,20 +952,19 @@ void dsgnOnMouse(DsgnStateT *ds, int32_t x, int32_t y, bool drag) { // Non-pointer tool: place a new control const char *typeName = ds->activeTool; - DsgnControlT ctrl; - memset(&ctrl, 0, sizeof(ctrl)); - ctrl.index = -1; - dsgnAutoName(ds, typeName, ctrl.name, DSGN_MAX_NAME); - snprintf(ctrl.typeName, DSGN_MAX_NAME, "%s", typeName); - ctrl.width = DEFAULT_CTRL_W; - ctrl.height = DEFAULT_CTRL_H; - setPropValue(&ctrl, "Caption", ctrl.name); + DsgnControlT *cp = (DsgnControlT *)calloc(1, sizeof(DsgnControlT)); + cp->index = -1; + dsgnAutoName(ds, typeName, cp->name, DSGN_MAX_NAME); + snprintf(cp->typeName, DSGN_MAX_NAME, "%s", typeName); + cp->width = DEFAULT_CTRL_W; + cp->height = DEFAULT_CTRL_H; + setPropValue(cp, "Caption", cp->name); // Determine parent: if click is inside a container, nest there WidgetT *parentWidget = ds->form->contentBox; for (int32_t i = ctrlCount - 1; i >= 0; i--) { - DsgnControlT *pc = &ds->form->controls[i]; + DsgnControlT *pc = ds->form->controls[i]; if (pc->widget && dsgnIsContainer(pc->typeName)) { int32_t wx = pc->widget->x; @@ -855,7 +973,7 @@ void dsgnOnMouse(DsgnStateT *ds, int32_t x, int32_t y, bool drag) { int32_t wh = pc->widget->h; if (x >= wx && x < wx + ww && y >= wy && y < wy + wh) { - snprintf(ctrl.parentName, DSGN_MAX_NAME, "%s", pc->name); + snprintf(cp->parentName, DSGN_MAX_NAME, "%s", pc->name); parentWidget = pc->widget; break; } @@ -864,19 +982,19 @@ void dsgnOnMouse(DsgnStateT *ds, int32_t x, int32_t y, bool drag) { // Create the live widget if (parentWidget) { - ctrl.widget = dsgnCreateDesignWidget(typeName, parentWidget); + cp->widget = dsgnCreateDesignWidget(typeName, parentWidget); - if (ctrl.widget) { - ctrl.widget->minW = wgtPixels(ctrl.width); - ctrl.widget->minH = wgtPixels(ctrl.height); + if (cp->widget) { + cp->widget->minW = wgtPixels(cp->width); + cp->widget->minH = wgtPixels(cp->height); } } - arrput(ds->form->controls, ctrl); + arrput(ds->form->controls, cp); ds->selectedIdx = (int32_t)arrlen(ds->form->controls) - 1; - // Set text AFTER arrput so pointers into the array element are stable - DsgnControlT *stable = &ds->form->controls[ds->selectedIdx]; + // Set text AFTER arrput so pointers are stable (heap-allocated, so always stable) + DsgnControlT *stable = ds->form->controls[ds->selectedIdx]; if (stable->widget) { const char *caption = getPropValue(stable, "Caption"); @@ -931,7 +1049,7 @@ void dsgnPaintOverlay(DsgnStateT *ds, int32_t winX, int32_t winY) { return; } - DsgnControlT *ctrl = &ds->form->controls[ds->selectedIdx]; + DsgnControlT *ctrl = ds->form->controls[ds->selectedIdx]; if (!ctrl->widget || !ctrl->widget->visible || ctrl->widget->w <= 0 || ctrl->widget->h <= 0) { return; @@ -980,7 +1098,7 @@ static int32_t saveControls(const DsgnFormT *form, char *buf, int32_t bufSize, i int32_t count = (int32_t)arrlen(form->controls); for (int32_t i = 0; i < count; i++) { - const DsgnControlT *ctrl = &form->controls[i]; + const DsgnControlT *ctrl = form->controls[i]; // Only output controls whose parent matches if (parentName[0] == '\0' && ctrl->parentName[0] != '\0') { continue; } @@ -1160,6 +1278,14 @@ int32_t dsgnSaveFrm(const DsgnStateT *ds, char *buf, int32_t bufSize) { pos += snprintf(buf + pos, bufSize - pos, "Checked = True\n"); } + if (mi->radioCheck) { + for (int32_t p = 0; p < (mi->level + 2) * 4; p++) { + buf[pos++] = ' '; + } + + pos += snprintf(buf + pos, bufSize - pos, "RadioCheck = True\n"); + } + if (!mi->enabled) { for (int32_t p = 0; p < (mi->level + 2) * 4; p++) { buf[pos++] = ' '; @@ -1226,7 +1352,7 @@ const char *dsgnSelectedName(const DsgnStateT *ds) { } if (ds->selectedIdx >= 0 && ds->selectedIdx < (int32_t)arrlen(ds->form->controls)) { - return ds->form->controls[ds->selectedIdx].name; + return ds->form->controls[ds->selectedIdx]->name; } return ds->form->name; @@ -1260,7 +1386,7 @@ static int32_t hitTestControl(const DsgnStateT *ds, int32_t x, int32_t y) { int32_t count = (int32_t)arrlen(ds->form->controls); for (int32_t i = count - 1; i >= 0; i--) { - const DsgnControlT *ctrl = &ds->form->controls[i]; + const DsgnControlT *ctrl = ds->form->controls[i]; if (!ctrl->widget || !ctrl->widget->visible) { continue; @@ -1375,7 +1501,7 @@ static void rebuildWidgets(DsgnStateT *ds) { int32_t count = (int32_t)arrlen(ds->form->controls); for (int32_t i = 0; i < count; i++) { - ds->form->controls[i].widget = NULL; + ds->form->controls[i]->widget = NULL; } // Recreate all widgets in current array order diff --git a/apps/dvxbasic/ide/ideDesigner.h b/apps/dvxbasic/ide/ideDesigner.h index 12160bd..44ab578 100644 --- a/apps/dvxbasic/ide/ideDesigner.h +++ b/apps/dvxbasic/ide/ideDesigner.h @@ -20,11 +20,13 @@ // Limits // ============================================================ -#define DSGN_MAX_NAME 32 +#define DSGN_MAX_NAME 32 #define DSGN_MAX_TEXT 256 -#define DSGN_MAX_PROPS 32 -#define DSGN_GRID_SIZE 8 -#define DSGN_HANDLE_SIZE 6 +#define DSGN_MAX_PROPS 32 +#define DSGN_MAX_FRM_NESTING 16 +#define DSGN_GRID_SIZE 8 +#define DSGN_HANDLE_SIZE 6 +#define DSGN_MENU_ID_BASE 20000 // base ID for designer preview menu items // ============================================================ // Design-time property (stored as key=value strings) @@ -44,6 +46,7 @@ typedef struct { char name[DSGN_MAX_NAME]; // "mnuFile" int32_t level; // 0 = top-level menu, 1 = item, 2+ = submenu bool checked; + bool radioCheck; // true = radio bullet instead of checkmark bool enabled; // default true } DsgnMenuItemT; @@ -83,7 +86,7 @@ typedef struct { bool centered; // true = center on screen, false = use left/top bool autoSize; // true = dvxFitWindow, false = use width/height bool resizable; // true = user can resize at runtime - DsgnControlT *controls; // stb_ds dynamic array + DsgnControlT **controls; // stb_ds array of heap-allocated pointers DsgnMenuItemT *menuItems; // stb_ds dynamic array (NULL if no menus) bool dirty; WidgetT *contentBox; // VBox parent for live widgets @@ -198,6 +201,18 @@ WidgetT *dsgnCreateDesignWidget(const char *vbTypeName, WidgetT *parent); // Used in the form designer to preview the menu layout. void dsgnBuildPreviewMenuBar(WindowT *win, const DsgnFormT *form); +// Create a layout container (VBox/HBox/WrapBox) from a layout name string. +// For VBox, reuses the root directly. For HBox/WrapBox, creates a child. +WidgetT *dsgnCreateContentBox(WidgetT *root, const char *layout); + +// Create and configure a window from form properties. +// Shared by the designer and runtime to ensure consistent behavior. +// Creates the window, widget root, and layout container (VBox/HBox/WrapBox). +// Applies sizing (autoSize or explicit) and positioning (centered or explicit). +// On success, sets *outRoot and *outContentBox and returns the window. +// On failure, returns NULL. +WindowT *dsgnCreateFormWindow(AppContextT *ctx, const char *title, const char *layout, bool resizable, bool centered, bool autoSize, int32_t width, int32_t height, int32_t left, int32_t top, WidgetT **outRoot, WidgetT **outContentBox); + // ============================================================ // Code rename support (implemented in ideMain.c) // ============================================================ diff --git a/apps/dvxbasic/ide/ideMain.c b/apps/dvxbasic/ide/ideMain.c index 6f56926..452ae2c 100644 --- a/apps/dvxbasic/ide/ideMain.c +++ b/apps/dvxbasic/ide/ideMain.c @@ -110,6 +110,9 @@ #define CMD_WIN_WATCH 154 #define CMD_WIN_BREAKPOINTS 155 #define CMD_DEBUG_LAYOUT 156 +#define CMD_OUTPUT_TO_LOG 159 +#define CMD_RECENT_BASE 160 // 160-167 reserved for recent files +#define CMD_RECENT_MAX 8 #define CMD_HELP_CONTENTS 157 #define CMD_HELP_API 158 #define IDE_MAX_IMM 1024 @@ -159,6 +162,11 @@ static int32_t toolbarBottom(void); static void newProject(void); static void onPrjFileDblClick(int32_t fileIdx, bool isForm); static void openProject(void); +static void recentAdd(const char *path); +static void recentLoad(void); +static void recentSave(void); +static void recentRebuildMenu(void); +static void recentOpen(int32_t index); static void closeProject(void); static void saveFile(void); static void onTbSave(WidgetT *w); @@ -169,6 +177,7 @@ static void onClose(WindowT *win); static void onCodeWinClose(WindowT *win); static void onContentFocus(WindowT *win); static void onFormWinClose(WindowT *win); +static void onFormWinMenu(WindowT *win, int32_t menuId); static void onFormWinResize(WindowT *win, int32_t newW, int32_t newH); static void onProjectWinClose(WindowT *win); static WindowT *getLastFocusWin(void); @@ -189,6 +198,7 @@ static void debugUpdateWindows(void); static void onBreakpointHit(void *ctx, int32_t line); static void onGutterClick(WidgetT *w, int32_t lineNum); static void navigateToCodeLine(int32_t fileIdx, int32_t codeLine, const char *procName, bool setDbgLine); +static void navigateToNamedEventSub(const char *ctrlName, const char *eventName); static void debugNavigateToLine(int32_t concatLine); static void buildVmBreakpoints(void); static void showBreakpointWindow(void); @@ -239,9 +249,222 @@ static void onTbStepInto(WidgetT *w); static void onTbStepOver(WidgetT *w); static void onTbStepOut(WidgetT *w); static void onTbRunToCur(WidgetT *w); +static void helpQueryHandler(void *ctx); static void selectDropdowns(const char *objName, const char *evtName); static void updateDropdowns(void); +// ============================================================ +// Keyword-to-topic lookup for context-sensitive help (F1) +// ============================================================ + +typedef struct { + const char *keyword; + const char *topic; +} HelpMapEntryT; + +static const HelpMapEntryT sHelpMap[] = { + // Data types + {"Boolean", "lang.datatypes"}, + {"Double", "lang.datatypes"}, + {"Integer", "lang.datatypes"}, + {"Long", "lang.datatypes"}, + {"Single", "lang.datatypes"}, + {"String", "lang.datatypes"}, + + // Declarations + {"ByRef", "lang.declarations"}, + {"ByVal", "lang.declarations"}, + {"Const", "lang.declarations"}, + {"Declare", "lang.declarations"}, + {"Dim", "lang.declarations"}, + {"Option", "lang.declarations"}, + {"ReDim", "lang.declarations"}, + {"Shared", "lang.declarations"}, + {"Static", "lang.declarations"}, + {"Type", "lang.declarations"}, + + // Operators + {"And", "lang.operators"}, + {"Mod", "lang.operators"}, + {"Not", "lang.operators"}, + {"Or", "lang.operators"}, + {"Xor", "lang.operators"}, + + // Conditionals + {"Case", "lang.conditionals"}, + {"Else", "lang.conditionals"}, + {"ElseIf", "lang.conditionals"}, + {"If", "lang.conditionals"}, + {"Select", "lang.conditionals"}, + {"Then", "lang.conditionals"}, + + // Loops + {"Do", "lang.loops"}, + {"For", "lang.loops"}, + {"Loop", "lang.loops"}, + {"Next", "lang.loops"}, + {"Step", "lang.loops"}, + {"Until", "lang.loops"}, + {"Wend", "lang.loops"}, + {"While", "lang.loops"}, + + // Procedures + {"Call", "lang.procedures"}, + {"Def", "lang.procedures"}, + {"End", "lang.procedures"}, + {"Function", "lang.procedures"}, + {"Sub", "lang.procedures"}, + + // Flow control + {"Exit", "lang.flow"}, + {"GoSub", "lang.flow"}, + {"GoTo", "lang.flow"}, + {"On", "lang.flow"}, + {"Resume", "lang.flow"}, + {"Return", "lang.flow"}, + + // I/O statements + {"Data", "lang.io"}, + {"Input", "lang.io"}, + {"Print", "lang.io"}, + {"Read", "lang.io"}, + {"Rem", "lang.io"}, + {"Write", "lang.io"}, + + // Misc statements + {"DoEvents", "lang.misc"}, + {"Load", "lang.misc"}, + {"Shell", "lang.misc"}, + {"Sleep", "lang.misc"}, + {"Unload", "lang.misc"}, + + // File I/O + {"Close", "lang.fileio"}, + {"Eof", "lang.func.fileio"}, + {"FreeFile", "lang.func.fileio"}, + {"Loc", "lang.func.fileio"}, + {"Lof", "lang.func.fileio"}, + {"Open", "lang.fileio"}, + {"Seek", "lang.func.fileio"}, + + // String functions + {"Asc", "lang.func.string"}, + {"Chr", "lang.func.string"}, + {"Chr$", "lang.func.string"}, + {"Environ", "lang.func.string"}, + {"Environ$", "lang.func.string"}, + {"Format", "lang.func.string"}, + {"Format$", "lang.func.string"}, + {"InStr", "lang.func.string"}, + {"LCase", "lang.func.string"}, + {"LCase$", "lang.func.string"}, + {"LTrim", "lang.func.string"}, + {"LTrim$", "lang.func.string"}, + {"Left", "lang.func.string"}, + {"Left$", "lang.func.string"}, + {"Len", "lang.func.string"}, + {"Mid", "lang.func.string"}, + {"Mid$", "lang.func.string"}, + {"RTrim", "lang.func.string"}, + {"RTrim$", "lang.func.string"}, + {"Right", "lang.func.string"}, + {"Right$", "lang.func.string"}, + {"Spc", "lang.func.string"}, + {"Space", "lang.func.string"}, + {"Space$", "lang.func.string"}, + {"String$", "lang.func.string"}, + {"Tab", "lang.func.string"}, + {"Trim", "lang.func.string"}, + {"Trim$", "lang.func.string"}, + {"UCase", "lang.func.string"}, + {"UCase$", "lang.func.string"}, + + // Math functions + {"Abs", "lang.func.math"}, + {"Atn", "lang.func.math"}, + {"Cos", "lang.func.math"}, + {"Exp", "lang.func.math"}, + {"Fix", "lang.func.math"}, + {"Int", "lang.func.math"}, + {"Log", "lang.func.math"}, + {"Randomize", "lang.func.math"}, + {"Rnd", "lang.func.math"}, + {"Sgn", "lang.func.math"}, + {"Sin", "lang.func.math"}, + {"Sqr", "lang.func.math"}, + {"Tan", "lang.func.math"}, + {"Timer", "lang.func.math"}, + + // Conversion functions + {"CBool", "lang.func.conversion"}, + {"CDbl", "lang.func.conversion"}, + {"CInt", "lang.func.conversion"}, + {"CLng", "lang.func.conversion"}, + {"CSng", "lang.func.conversion"}, + {"CStr", "lang.func.conversion"}, + {"Hex", "lang.func.conversion"}, + {"Hex$", "lang.func.conversion"}, + {"Str", "lang.func.conversion"}, + {"Str$", "lang.func.conversion"}, + {"Val", "lang.func.conversion"}, + + // Misc functions + {"InputBox", "lang.func.misc"}, + {"InputBox$", "lang.func.misc"}, + {"MsgBox", "lang.func.misc"}, + + // SQL functions + {"SQLAffected", "lang.sql"}, + {"SQLClose", "lang.sql"}, + {"SQLEof", "lang.sql"}, + {"SQLError", "lang.sql"}, + {"SQLError$", "lang.sql"}, + {"SQLExec", "lang.sql"}, + {"SQLField", "lang.sql"}, + {"SQLField$", "lang.sql"}, + {"SQLFieldCount", "lang.sql"}, + {"SQLFieldDbl", "lang.sql"}, + {"SQLFieldInt", "lang.sql"}, + {"SQLFreeResult", "lang.sql"}, + {"SQLNext", "lang.sql"}, + {"SQLOpen", "lang.sql"}, + {"SQLQuery", "lang.sql"}, + + // App object + {"App", "lang.app"}, + + // INI functions + {"IniRead", "lang.ini"}, + {"IniWrite", "lang.ini"}, + + // Constants + {"False", "lang.constants"}, + {"True", "lang.constants"}, + {"vbCancel", "lang.constants"}, + {"vbCritical", "lang.constants"}, + {"vbModal", "lang.constants"}, + {"vbOK", "lang.constants"}, + {"vbOKCancel", "lang.constants"}, + {"vbYesNo", "lang.constants"}, + + // Form/control statements + {"Me", "lang.forms"}, + {"Set", "lang.forms"}, +}; + +#define HELP_MAP_COUNT (sizeof(sHelpMap) / sizeof(sHelpMap[0])) + +// Build control help topic from type name. +// Topic IDs in .bhs files follow the pattern ctrl.. +// Generated dynamically so third-party widgets get help automatically. +static void helpBuildCtrlTopic(const char *typeName, char *buf, int32_t bufSize) { + int32_t off = snprintf(buf, bufSize, "ctrl."); + for (int32_t i = 0; typeName[i] && off < bufSize - 1; i++) { + buf[off++] = tolower((unsigned char)typeName[i]); + } + buf[off] = '\0'; +} + // ============================================================ // Module state // ============================================================ @@ -318,6 +541,13 @@ static const char **sObjItems = NULL; // stb_ds dynamic array static const char **sEvtItems = NULL; // stb_ds dynamic array static bool sDropdownNavSuppressed = false; static bool sStopRequested = false; +static bool sOutputToLog = false; + +// Recent files list (stored in dvxbasic.ini [recent] section) +static char sRecentFiles[CMD_RECENT_MAX][DVX_MAX_PATH]; +static int32_t sRecentCount = 0; +static MenuT *sFileMenu = NULL; +static int32_t sFileMenuBase = 0; // item count before recent files // Debug state typedef enum { @@ -562,8 +792,10 @@ int32_t appMain(DxeAppContextT *ctx) { sCtx = ctx; sAc = ctx->shellCtx; - // Set help file for F1 + // Set help file and context-sensitive F1 handler snprintf(ctx->helpFile, sizeof(ctx->helpFile), "%s%c%s", ctx->appDir, DVX_PATH_SEP, "dvxbasic.hlp"); + ctx->onHelpQuery = helpQueryHandler; + ctx->helpQueryCtx = NULL; basStringSystemInit(); prjInit(&sProject); @@ -597,6 +829,10 @@ int32_t appMain(DxeAppContextT *ctx) { wmMenuItemSetChecked(sWin->menuBar, CMD_SAVE_ON_RUN, saveOnRun); } + // Load recent files list and populate menu + recentLoad(); + recentRebuildMenu(); + if (sWin) { dvxFitWindowH(sAc, sWin); } @@ -604,23 +840,6 @@ int32_t appMain(DxeAppContextT *ctx) { sOutputBuf[0] = '\0'; sOutputLen = 0; - // Auto-load project for development/testing - if (prjLoad(&sProject, "C:\\BIN\\APPS\\KPUNCH\\DVXBASIC\\MULTI.DBP")) { - prjLoadAllFiles(&sProject, sAc); - sProjectWin = prjCreateWindow(sAc, &sProject, onPrjFileDblClick, updateProjectMenuState); - - if (sProjectWin) { - sProjectWin->y = toolbarBottom() + 25; - sProjectWin->onClose = onProjectWinClose; - sProjectWin->onMenu = onMenu; - sProjectWin->accelTable = sWin ? sWin->accelTable : NULL; - } - - char title[300]; - snprintf(title, sizeof(title), "DVX BASIC - [%s]", sProject.name); - dvxSetTitle(sAc, sWin, title); - } - updateProjectMenuState(); setStatus("Ready."); return 0; @@ -660,7 +879,7 @@ static WidgetT *loadTbIcon(WidgetT *parent, const char *resName, const char *fal static void buildWindow(void) { // ---- Main toolbar window (top of screen) ---- - sWin = dvxCreateWindow(sAc, "DVX BASIC", 0, 0, sAc->display.width, 200, false); + sWin = dvxCreateWindow(sAc, "DVX BASIC", 0, 0, sAc->display.width, 200, true); if (!sWin) { return; @@ -671,7 +890,8 @@ static void buildWindow(void) { // Menu bar MenuBarT *menuBar = wmAddMenuBar(sWin); - MenuT *fileMenu = wmAddMenu(menuBar, "&File"); + sFileMenu = wmAddMenu(menuBar, "&File"); + MenuT *fileMenu = sFileMenu; wmAddMenuItem(fileMenu, "New &Project...", CMD_PRJ_NEW); wmAddMenuItem(fileMenu, "Open Pro&ject...", CMD_PRJ_OPEN); wmAddMenuItem(fileMenu, "Save Projec&t", CMD_PRJ_SAVE); @@ -686,6 +906,7 @@ static void buildWindow(void) { wmAddMenuItem(fileMenu, "&Remove File", CMD_PRJ_REMOVE); wmAddMenuSeparator(fileMenu); wmAddMenuItem(fileMenu, "E&xit", CMD_EXIT); + sFileMenuBase = sFileMenu->itemCount; MenuT *editMenu = wmAddMenu(menuBar, "&Edit"); wmAddMenuItem(editMenu, "Cu&t\tCtrl+X", CMD_CUT); @@ -711,6 +932,8 @@ static void buildWindow(void) { wmAddMenuItem(runMenu, "Step Ou&t\tCtrl+Shift+F8", CMD_STEP_OUT); wmAddMenuItem(runMenu, "Run to &Cursor\tCtrl+F8", CMD_RUN_TO_CURSOR); wmAddMenuSeparator(runMenu); + wmAddMenuCheckItem(runMenu, "Output Window to &Log", CMD_OUTPUT_TO_LOG, false); + wmAddMenuSeparator(runMenu); wmAddMenuItem(runMenu, "Toggle &Breakpoint\tF9", CMD_TOGGLE_BP); wmAddMenuSeparator(runMenu); wmAddMenuItem(runMenu, "&Clear Output", CMD_CLEAR); @@ -845,6 +1068,10 @@ static void buildWindow(void) { showOutputWindow(); showImmediateWindow(); + + if (sWin) { + dvxRaiseWindow(sAc, sWin); + } } // ============================================================ @@ -1024,9 +1251,17 @@ static void basicColorize(const char *line, int32_t lineLen, uint8_t *colors, vo // ============================================================ static void setOutputText(const char *text) { + if (!sOutWin) { + showOutputWindow(); + } + if (sOutput) { wgtSetText(sOutput, text); } + + if (sOutputToLog && text && text[0]) { + dvxLog("BASIC: %s", text); + } } static void clearOutput(void) { @@ -1519,25 +1754,19 @@ static void compileAndRun(void) { if (sDesigner.form && i == sProject.activeFileIdx) { fileSrc = sDesigner.form->code; } else if (sProject.files[i].buffer) { - // Extract code from the stashed .frm text (after "End\n") - const char *buf = sProject.files[i].buffer; - const char *endTag = strstr(buf, "\nEnd\n"); + // Parse the .frm to extract the code section using + // the same parser as the designer (handles nested containers). + DsgnStateT tmpDs; + memset(&tmpDs, 0, sizeof(tmpDs)); + dsgnLoadFrm(&tmpDs, sProject.files[i].buffer, (int32_t)strlen(sProject.files[i].buffer)); - if (!endTag) { - endTag = strstr(buf, "\nEnd\r\n"); + if (tmpDs.form && tmpDs.form->code) { + fileSrc = tmpDs.form->code; + diskBuf = tmpDs.form->code; + tmpDs.form->code = NULL; } - if (endTag) { - endTag += 5; - - while (*endTag == '\r' || *endTag == '\n') { - endTag++; - } - - if (*endTag) { - fileSrc = endTag; - } - } + dsgnFree(&tmpDs); } // If no code found from memory, fall through to disk read @@ -1568,25 +1797,22 @@ static void compileAndRun(void) { diskBuf[br] = '\0'; fileSrc = diskBuf; - // For .frm from disk, extract code section + // For .frm from disk, parse to extract code section if (sProject.files[i].isForm) { - const char *endTag = strstr(fileSrc, "\nEnd\n"); + DsgnStateT tmpDs; + memset(&tmpDs, 0, sizeof(tmpDs)); + dsgnLoadFrm(&tmpDs, diskBuf, br); - if (!endTag) { - endTag = strstr(fileSrc, "\nEnd\r\n"); - } - - if (endTag) { - endTag += 5; - - while (*endTag == '\r' || *endTag == '\n') { - endTag++; - } - - fileSrc = endTag; + if (tmpDs.form && tmpDs.form->code) { + free(diskBuf); + diskBuf = tmpDs.form->code; + fileSrc = diskBuf; + tmpDs.form->code = NULL; } else { fileSrc = NULL; } + + dsgnFree(&tmpDs); } } } @@ -1598,8 +1824,6 @@ static void compileAndRun(void) { continue; } - int32_t startLine = line; - // Inject BEGINFORM directive for .frm code sections if (sProject.files[i].isForm && sProject.files[i].formName[0]) { int32_t dirLen = snprintf(concatBuf + pos, IDE_MAX_SOURCE - pos, @@ -1608,6 +1832,10 @@ static void compileAndRun(void) { line++; } + // Record startLine AFTER injected directives so the source + // map lines match what the editor shows (not the synthetic lines). + int32_t startLine = line; + int32_t fileLen = (int32_t)strlen(fileSrc); int32_t copyLen = fileLen; @@ -1633,13 +1861,7 @@ static void compileAndRun(void) { line++; } - // Inject ENDFORM directive - if (sProject.files[i].isForm && sProject.files[i].formName[0]) { - int32_t dirLen = snprintf(concatBuf + pos, IDE_MAX_SOURCE - pos, "ENDFORM\n"); - pos += dirLen; - line++; - } - + // Record source map BEFORE injected ENDFORM directive { PrjSourceMapT mapEntry; mapEntry.fileIdx = i; @@ -1649,6 +1871,13 @@ static void compileAndRun(void) { sProject.sourceMapCount = (int32_t)arrlen(sProject.sourceMap); } + // Inject ENDFORM directive + if (sProject.files[i].isForm && sProject.files[i].formName[0]) { + int32_t dirLen = snprintf(concatBuf + pos, IDE_MAX_SOURCE - pos, "ENDFORM\n"); + pos += dirLen; + line++; + } + dvxUpdate(sAc); } @@ -1661,6 +1890,7 @@ static void compileAndRun(void) { if (!src || *src == '\0') { setStatus("No source code to run."); + dvxSetBusy(sAc, false); return; } @@ -1681,23 +1911,64 @@ static void compileAndRun(void) { parser->optionExplicit = prefsGetBool(sPrefs, "editor", "optionExplicit", false); if (!basParse(parser)) { - int32_t n = snprintf(sOutputBuf, IDE_MAX_OUTPUT, "COMPILE ERROR:\n%s\n", parser->error); - sOutputLen = n; - setOutputText(sOutputBuf); + // Translate global error line to local file/line for display + int32_t errFileIdx = -1; + int32_t errLocalLine = parser->errorLine; + const char *errFile = ""; - // Jump to error line -- translate through source map if project - if (parser->errorLine > 0 && sEditor) { - int32_t fileIdx = -1; - int32_t localLine = parser->errorLine; + if (parser->errorLine > 0 && sProject.fileCount > 0) { + prjMapLine(&sProject, parser->errorLine, &errFileIdx, &errLocalLine); - if (sProject.fileCount > 0 && prjMapLine(&sProject, parser->errorLine, &fileIdx, &localLine)) { - // Open the offending file if it's not already active - if (fileIdx != sProject.activeFileIdx) { - onPrjFileDblClick(fileIdx, false); + if (errFileIdx >= 0 && errFileIdx < sProject.fileCount) { + errFile = sProject.files[errFileIdx].path; + } + } + + // Navigate to error location and build a user-friendly error message + char procName[128] = {0}; + int32_t procLine = errLocalLine; + + if (parser->errorLine > 0 && errFileIdx >= 0) { + activateFile(errFileIdx, ViewCodeE); + + int32_t procCount = (int32_t)arrlen(sProcTable); + + for (int32_t i = procCount - 1; i >= 0; i--) { + if (errLocalLine >= sProcTable[i].lineNum) { + snprintf(procName, sizeof(procName), "%s.%s", + sProcTable[i].objName, sProcTable[i].evtName); + procLine = errLocalLine - sProcTable[i].lineNum + 1; + break; } } - wgtTextAreaGoToLine(sEditor, localLine); + navigateToCodeLine(errFileIdx, errLocalLine, procName[0] ? procName : NULL, false); + } + + // Strip the "Line NNN: " prefix from the parser error + const char *msg = parser->error; + if (strncmp(msg, "Line ", 5) == 0) { + while (*msg && *msg != ':') { msg++; } + if (*msg == ':') { msg++; } + while (*msg == ' ') { msg++; } + } + + // Show the error with procedure name and proc-relative line + int32_t n; + if (procName[0]) { + n = snprintf(sOutputBuf, IDE_MAX_OUTPUT, "COMPILE ERROR:\n%s line %d: %s\n", + procName, (int)procLine, msg); + } else { + n = snprintf(sOutputBuf, IDE_MAX_OUTPUT, "COMPILE ERROR:\n%s line %d: %s\n", + errFile, (int)errLocalLine, msg); + } + sOutputLen = n; + setOutputText(sOutputBuf); + + // Ensure output window is visible + showOutputWindow(); + if (sOutWin) { + dvxRaiseWindow(sAc, sOutWin); } setStatus("Compilation failed."); @@ -1900,6 +2171,7 @@ static void runModule(BasModuleT *mod) { sDbgFormRt = formRt; sDbgModule = mod; sDbgState = DBG_RUNNING; + updateProjectMenuState(); // Set breakpoints BEFORE loading forms so breakpoints in form // init code (module-level statements inside BEGINFORM) fire. @@ -1917,12 +2189,12 @@ static void runModule(BasModuleT *mod) { // Auto-show the startup form (or first form if none specified). if (formRt->formCount > 0) { - BasFormT *startupForm = &formRt->forms[0]; + BasFormT *startupForm = formRt->forms[0]; if (sProject.startupForm[0]) { for (int32_t i = 0; i < formRt->formCount; i++) { - if (strcasecmp(formRt->forms[i].name, sProject.startupForm) == 0) { - startupForm = &formRt->forms[i]; + if (strcasecmp(formRt->forms[i]->name, sProject.startupForm) == 0) { + startupForm = formRt->forms[i]; break; } } @@ -2006,6 +2278,9 @@ static void runModule(BasModuleT *mod) { sStopRequested = false; while (sWin && sAc->running && formRt->formCount > 0 && !sStopRequested && !vm->ended) { + totalSteps += vm->stepCount; + vm->stepCount = 0; + if (sDbgState == DBG_PAUSED) { // Paused inside an event handler debugNavigateToLine(sDbgCurrentLine); @@ -2034,16 +2309,22 @@ static void runModule(BasModuleT *mod) { sDbgCurrentLine = -1; sDbgEnabled = false; - // Update output display + basFormRtDestroy(formRt); + basVmDestroy(vm); + + // If the IDE was closed while the program was running, skip + // all UI updates — the windows are already destroyed. + if (!sWin) { + return; + } + + updateProjectMenuState(); setOutputText(sOutputBuf); static char statusBuf[128]; snprintf(statusBuf, sizeof(statusBuf), "Done. %ld instructions executed.", (long)totalSteps); setStatus(statusBuf); - basFormRtDestroy(formRt); - basVmDestroy(vm); - // Restore IDE windows if (hadFormWin && sFormWin) { dvxShowWindow(sAc, sFormWin); } if (hadToolbox && sToolboxWin) { dvxShowWindow(sAc, sToolboxWin); } @@ -2599,6 +2880,184 @@ static void ensureProject(const char *filePath) { } +// ============================================================ +// Recent files +// ============================================================ + +static void recentLoad(void) { + sRecentCount = 0; + + if (!sPrefs) { + return; + } + + for (int32_t i = 0; i < CMD_RECENT_MAX; i++) { + char key[16]; + snprintf(key, sizeof(key), "file%ld", (long)i); + const char *val = prefsGetString(sPrefs, "recent", key, ""); + + if (val[0]) { + snprintf(sRecentFiles[sRecentCount], DVX_MAX_PATH, "%s", val); + sRecentCount++; + } + } +} + + +static void recentSave(void) { + if (!sPrefs) { + return; + } + + for (int32_t i = 0; i < CMD_RECENT_MAX; i++) { + char key[16]; + snprintf(key, sizeof(key), "file%ld", (long)i); + + if (i < sRecentCount) { + prefsSetString(sPrefs, "recent", key, sRecentFiles[i]); + } else { + prefsSetString(sPrefs, "recent", key, ""); + } + } + + prefsSave(sPrefs); +} + + +static void recentAdd(const char *path) { + if (!path || !path[0]) { + return; + } + + // If already in the list, move it to the top + for (int32_t i = 0; i < sRecentCount; i++) { + if (strcasecmp(sRecentFiles[i], path) == 0) { + // Shift entries down to make room at position 0 + char tmp[DVX_MAX_PATH]; + snprintf(tmp, DVX_MAX_PATH, "%s", sRecentFiles[i]); + + for (int32_t j = i; j > 0; j--) { + snprintf(sRecentFiles[j], DVX_MAX_PATH, "%s", sRecentFiles[j - 1]); + } + + snprintf(sRecentFiles[0], DVX_MAX_PATH, "%s", tmp); + recentSave(); + recentRebuildMenu(); + return; + } + } + + // Shift existing entries down + if (sRecentCount < CMD_RECENT_MAX) { + sRecentCount++; + } + + for (int32_t j = sRecentCount - 1; j > 0; j--) { + snprintf(sRecentFiles[j], DVX_MAX_PATH, "%s", sRecentFiles[j - 1]); + } + + snprintf(sRecentFiles[0], DVX_MAX_PATH, "%s", path); + recentSave(); + recentRebuildMenu(); +} + + +static void recentRebuildMenu(void) { + if (!sFileMenu) { + return; + } + + // Truncate menu back to the base items (everything through Exit) + sFileMenu->itemCount = sFileMenuBase; + + if (sRecentCount == 0) { + return; + } + + // Append separator + recent file items after Exit + wmAddMenuSeparator(sFileMenu); + + for (int32_t i = 0; i < sRecentCount; i++) { + // Show just the filename for shorter labels + const char *name = strrchr(sRecentFiles[i], DVX_PATH_SEP); + + if (!name) { + name = strrchr(sRecentFiles[i], '/'); + } + + if (!name) { + name = strrchr(sRecentFiles[i], '\\'); + } + + name = name ? name + 1 : sRecentFiles[i]; + + char label[MAX_MENU_LABEL]; + snprintf(label, sizeof(label), "&%ld %s", (long)(i + 1), name); + wmAddMenuItem(sFileMenu, label, CMD_RECENT_BASE + i); + } +} + + +static void recentOpen(int32_t index) { + if (index < 0 || index >= sRecentCount) { + return; + } + + const char *path = sRecentFiles[index]; + const char *ext = strrchr(path, '.'); + + if (ext && strcasecmp(ext, ".dbp") == 0) { + // Project file + closeProject(); + + if (!prjLoad(&sProject, path)) { + dvxMessageBox(sAc, "Error", "Could not open project file.", MB_OK | MB_ICONERROR); + return; + } + + prjLoadAllFiles(&sProject, sAc); + + if (!sProjectWin) { + sProjectWin = prjCreateWindow(sAc, &sProject, onPrjFileDblClick, updateProjectMenuState); + + if (sProjectWin) { + sProjectWin->y = toolbarBottom() + 25; + sProjectWin->onClose = onProjectWinClose; + sProjectWin->onMenu = onMenu; + sProjectWin->accelTable = sWin ? sWin->accelTable : NULL; + } + } else { + prjRebuildTree(&sProject); + } + + char title[300]; + snprintf(title, sizeof(title), "DVX BASIC - [%s]", sProject.name); + dvxSetTitle(sAc, sWin, title); + setStatus("Project loaded."); + } else { + // Single file + if (!promptAndSave()) { + return; + } + + ensureProject(path); + + if (!sProjectWin) { + sProjectWin = prjCreateWindow(sAc, &sProject, onPrjFileDblClick, updateProjectMenuState); + + if (sProjectWin) { + sProjectWin->y = toolbarBottom() + 25; + sProjectWin->onClose = onProjectWinClose; + dvxRaiseWindow(sAc, sProjectWin); + } + } + } + + updateProjectMenuState(); + recentAdd(path); +} + + static void loadFile(void) { FileFilterT filters[] = { { "BASIC Files (*.bas)", "*.bas" }, @@ -2647,6 +3106,8 @@ static void loadFile(void) { } } } + + recentAdd(path); } // ============================================================ @@ -2853,6 +3314,7 @@ static void openProject(void) { setStatus("Project loaded."); updateProjectMenuState(); + recentAdd(path); } @@ -3325,6 +3787,18 @@ static void onClose(WindowT *win) { return; } + // Stop any running program + sStopRequested = true; + + if (sVm) { + sVm->running = false; + sVm->debugPaused = false; + } + + sDbgState = DBG_IDLE; + sDbgCurrentLine = -1; + sDbgEnabled = false; + // Prevent stale focus tracking during shutdown sLastFocusWin = NULL; @@ -3483,6 +3957,13 @@ static void handleFileCmd(int32_t cmd) { onClose(sWin); } break; + + default: + // Recent files + if (cmd >= CMD_RECENT_BASE && cmd < CMD_RECENT_BASE + CMD_RECENT_MAX) { + recentOpen(cmd - CMD_RECENT_BASE); + } + break; } } @@ -4071,6 +4552,12 @@ static void handleRunCmd(int32_t cmd) { setStatus("Program stopped."); break; + case CMD_OUTPUT_TO_LOG: + if (sWin && sWin->menuBar) { + sOutputToLog = wmMenuItemIsChecked(sWin->menuBar, CMD_OUTPUT_TO_LOG); + } + break; + case CMD_STEP_INTO: case CMD_STEP_OVER: case CMD_STEP_OUT: @@ -4448,6 +4935,130 @@ static void handleWindowCmd(int32_t cmd) { } +// ============================================================ +// helpQueryHandler -- context-sensitive F1 help +// ============================================================ + +static const char *helpLookupKeyword(const char *word) { + for (int32_t i = 0; i < (int32_t)HELP_MAP_COUNT; i++) { + if (strcasecmp(sHelpMap[i].keyword, word) == 0) { + return sHelpMap[i].topic; + } + } + return NULL; +} + + +static void helpSetCtrlTopic(const char *typeName) { + helpBuildCtrlTopic(typeName, sCtx->helpTopic, sizeof(sCtx->helpTopic)); +} + + +static void helpQueryHandler(void *ctx) { + (void)ctx; + + sCtx->helpTopic[0] = '\0'; + + // Determine which window is focused + WindowT *focusWin = NULL; + if (sAc->stack.focusedIdx >= 0 && sAc->stack.focusedIdx < sAc->stack.count) { + focusWin = sAc->stack.windows[sAc->stack.focusedIdx]; + } + + // Code editor: look up the word under the cursor + if (focusWin == sCodeWin && sEditor) { + char word[128]; + if (wgtTextAreaGetWordAtCursor(sEditor, word, sizeof(word)) > 0) { + const char *topic = helpLookupKeyword(word); + if (topic) { + snprintf(sCtx->helpTopic, sizeof(sCtx->helpTopic), "%s", topic); + return; + } + } + // No keyword match -- open language reference + snprintf(sCtx->helpTopic, sizeof(sCtx->helpTopic), "ide.editor"); + return; + } + + // Immediate window: look up the word under the cursor + if (focusWin == sImmWin && sImmediate) { + char word[128]; + if (wgtTextAreaGetWordAtCursor(sImmediate, word, sizeof(word)) > 0) { + const char *topic = helpLookupKeyword(word); + if (topic) { + snprintf(sCtx->helpTopic, sizeof(sCtx->helpTopic), "%s", topic); + return; + } + } + snprintf(sCtx->helpTopic, sizeof(sCtx->helpTopic), "ide.immediate"); + return; + } + + // Form designer: help for the selected control type + if (focusWin == sFormWin && sDesigner.form) { + if (sDesigner.selectedIdx >= 0 && sDesigner.selectedIdx < arrlen(sDesigner.form->controls)) { + helpSetCtrlTopic(sDesigner.form->controls[sDesigner.selectedIdx]->typeName); + } else { + snprintf(sCtx->helpTopic, sizeof(sCtx->helpTopic), "ctrl.form"); + } + return; + } + + // Toolbox: help for the active tool + if (focusWin == sToolboxWin) { + if (sDesigner.activeTool[0]) { + helpSetCtrlTopic(sDesigner.activeTool); + } else { + snprintf(sCtx->helpTopic, sizeof(sCtx->helpTopic), "ide.toolbox"); + } + return; + } + + // Properties panel + if (focusWin == sPropsWin) { + if (sDesigner.form && sDesigner.selectedIdx >= 0 && sDesigner.selectedIdx < arrlen(sDesigner.form->controls)) { + helpSetCtrlTopic(sDesigner.form->controls[sDesigner.selectedIdx]->typeName); + } else { + snprintf(sCtx->helpTopic, sizeof(sCtx->helpTopic), "ctrl.common.props"); + } + return; + } + + // Output window + if (focusWin == sOutWin) { + snprintf(sCtx->helpTopic, sizeof(sCtx->helpTopic), "ide.output"); + return; + } + + // Project window + if (focusWin == sProjectWin) { + snprintf(sCtx->helpTopic, sizeof(sCtx->helpTopic), "ide.project"); + return; + } + + // Debugger windows + if (focusWin == sLocalsWin) { + snprintf(sCtx->helpTopic, sizeof(sCtx->helpTopic), "ide.debug.locals"); + return; + } + if (focusWin == sCallStackWin) { + snprintf(sCtx->helpTopic, sizeof(sCtx->helpTopic), "ide.debug.callstack"); + return; + } + if (focusWin == sWatchWin) { + snprintf(sCtx->helpTopic, sizeof(sCtx->helpTopic), "ide.debug.watch"); + return; + } + if (focusWin == sBreakpointWin) { + snprintf(sCtx->helpTopic, sizeof(sCtx->helpTopic), "ide.debug.breakpoints"); + return; + } + + // Default: IDE overview + snprintf(sCtx->helpTopic, sizeof(sCtx->helpTopic), "ide.overview"); +} + + static void handleProjectCmd(int32_t cmd) { switch (cmd) { case CMD_PRJ_NEW: @@ -4585,7 +5196,7 @@ static bool isCtrlArrayInDesigner(const char *ctrlName) { int32_t count = (int32_t)arrlen(sDesigner.form->controls); for (int32_t i = 0; i < count; i++) { - if (strcasecmp(sDesigner.form->controls[i].name, ctrlName) == 0 && sDesigner.form->controls[i].index >= 0) { + if (strcasecmp(sDesigner.form->controls[i]->name, ctrlName) == 0 && sDesigner.form->controls[i]->index >= 0) { return true; } } @@ -4849,8 +5460,8 @@ static void onObjDropdownChange(WidgetT *w) { if (!isForm && !isMenuItem && sDesigner.form) { for (int32_t i = 0; i < (int32_t)arrlen(sDesigner.form->controls); i++) { - if (strcasecmp(sDesigner.form->controls[i].name, selObj) == 0) { - const char *wgtName = wgtFindByBasName(sDesigner.form->controls[i].typeName); + if (strcasecmp(sDesigner.form->controls[i]->name, selObj) == 0) { + const char *wgtName = wgtFindByBasName(sDesigner.form->controls[i]->typeName); if (wgtName) { iface = wgtGetIface(wgtName); @@ -4987,8 +5598,8 @@ static void onObjDropdownChange(WidgetT *w) { defEvt = dsgnDefaultEvent("Form"); } else if (sDesigner.form) { for (int32_t i = 0; i < (int32_t)arrlen(sDesigner.form->controls); i++) { - if (strcasecmp(sDesigner.form->controls[i].name, selObj) == 0) { - defEvt = dsgnDefaultEvent(sDesigner.form->controls[i].typeName); + if (strcasecmp(sDesigner.form->controls[i]->name, selObj) == 0) { + defEvt = dsgnDefaultEvent(sDesigner.form->controls[i]->typeName); break; } } @@ -5233,7 +5844,7 @@ static void dsgnCopySelected(void) { // Serialize the selected control to FRM text char buf[2048]; int32_t pos = 0; - DsgnControlT *ctrl = &sDesigner.form->controls[sDesigner.selectedIdx]; + DsgnControlT *ctrl = sDesigner.form->controls[sDesigner.selectedIdx]; pos += snprintf(buf + pos, sizeof(buf) - pos, "Begin %s %s\n", ctrl->typeName, ctrl->name); @@ -5370,10 +5981,10 @@ static void dsgnPasteControl(void) { int32_t existCount = (int32_t)arrlen(sDesigner.form->controls); for (int32_t i = 0; i < existCount; i++) { - if (strcasecmp(sDesigner.form->controls[i].name, ctrlName) == 0) { + if (strcasecmp(sDesigner.form->controls[i]->name, ctrlName) == 0) { nameExists = true; - if (sDesigner.form->controls[i].index > highIdx) { - highIdx = sDesigner.form->controls[i].index; + if (sDesigner.form->controls[i]->index > highIdx) { + highIdx = sDesigner.form->controls[i]->index; } } } @@ -5393,8 +6004,8 @@ static void dsgnPasteControl(void) { if (result == ID_YES) { // Convert existing control to index 0 for (int32_t i = 0; i < existCount; i++) { - if (strcasecmp(sDesigner.form->controls[i].name, ctrlName) == 0) { - sDesigner.form->controls[i].index = 0; + if (strcasecmp(sDesigner.form->controls[i]->name, ctrlName) == 0) { + sDesigner.form->controls[i]->index = 0; break; } } @@ -5570,7 +6181,9 @@ static void dsgnPasteControl(void) { } } - arrput(sDesigner.form->controls, ctrl); + DsgnControlT *heapCtrl = malloc(sizeof(DsgnControlT)); + *heapCtrl = ctrl; + arrput(sDesigner.form->controls, heapCtrl); sDesigner.selectedIdx = (int32_t)arrlen(sDesigner.form->controls) - 1; sDesigner.form->dirty = true; @@ -5661,7 +6274,7 @@ static int32_t onFormWinCursorQuery(WindowT *win, int32_t x, int32_t y) { return 0; } - DsgnControlT *ctrl = &sDesigner.form->controls[sDesigner.selectedIdx]; + DsgnControlT *ctrl = sDesigner.form->controls[sDesigner.selectedIdx]; if (!ctrl->widget || !ctrl->widget->visible || ctrl->widget->w <= 0 || ctrl->widget->h <= 0) { return 0; @@ -5742,26 +6355,8 @@ static void selectDropdowns(const char *objName, const char *evtName) { // selected control (or form). Creates the sub skeleton if it doesn't exist. // Code is stored in the .frm file's code section (sDesigner.form->code). -static void navigateToEventSub(void) { - if (!sDesigner.form) { - return; - } - - // Determine control name and default event - const char *ctrlName = NULL; - const char *eventName = NULL; - - if (sDesigner.selectedIdx >= 0 && - sDesigner.selectedIdx < (int32_t)arrlen(sDesigner.form->controls)) { - DsgnControlT *ctrl = &sDesigner.form->controls[sDesigner.selectedIdx]; - ctrlName = ctrl->name; - eventName = dsgnDefaultEvent(ctrl->typeName); - } else { - ctrlName = sDesigner.form->name; - eventName = dsgnDefaultEvent("Form"); - } - - if (!ctrlName || !eventName) { +static void navigateToNamedEventSub(const char *ctrlName, const char *eventName) { + if (!sDesigner.form || !ctrlName || !eventName) { return; } @@ -5817,6 +6412,29 @@ static void navigateToEventSub(void) { } +static void navigateToEventSub(void) { + if (!sDesigner.form) { + return; + } + + // Determine control name and default event + const char *ctrlName = NULL; + const char *eventName = NULL; + + if (sDesigner.selectedIdx >= 0 && + sDesigner.selectedIdx < (int32_t)arrlen(sDesigner.form->controls)) { + DsgnControlT *ctrl = sDesigner.form->controls[sDesigner.selectedIdx]; + ctrlName = ctrl->name; + eventName = dsgnDefaultEvent(ctrl->typeName); + } else { + ctrlName = sDesigner.form->name; + eventName = dsgnDefaultEvent("Form"); + } + + navigateToNamedEventSub(ctrlName, eventName); +} + + static void onFormWinMouse(WindowT *win, int32_t x, int32_t y, int32_t buttons) { (void)win; static int32_t lastButtons = 0; @@ -5887,14 +6505,15 @@ static void onFormWinPaint(WindowT *win, RectT *dirtyArea) { return; } - // Force a full measure + layout + paint cycle. - // widgetOnPaint normally skips relayout if root dimensions haven't - // changed, but we need it to pick up minH changes from handle drag. - if (win->widgetRoot) { + // Force measure + relayout only on structural changes (control + // add/remove/resize). Selection clicks just need repaint + overlay. + if (win->fullRepaint && win->widgetRoot) { widgetCalcMinSizeTree(win->widgetRoot, &sAc->font); - win->widgetRoot->w = 0; // force layout pass to re-run + win->widgetRoot->w = 0; // force layout pass } + // Designer always needs full repaint (handles must be erased/redrawn) + win->fullRepaint = true; widgetOnPaint(win, dirtyArea); // Then draw selection handles on top @@ -5922,7 +6541,7 @@ static void cleanupFormWin(void) { int32_t count = (int32_t)arrlen(sDesigner.form->controls); for (int32_t i = 0; i < count; i++) { - sDesigner.form->controls[i].widget = NULL; + sDesigner.form->controls[i]->widget = NULL; } sDesigner.form->contentBox = NULL; @@ -5962,6 +6581,29 @@ static void onFormWinClose(WindowT *win) { } +// onFormWinMenu -- menu click on the designer form preview. +// Designer menu items use IDs starting at DSGN_MENU_ID_BASE. +// Clicking one navigates to the menu item's Click event code. +// Other IDs (from shared accel table) fall through to onMenu. +static void onFormWinMenu(WindowT *win, int32_t menuId) { + if (sDesigner.form && menuId >= DSGN_MENU_ID_BASE) { + int32_t idx = menuId - DSGN_MENU_ID_BASE; + int32_t menuCount = (int32_t)arrlen(sDesigner.form->menuItems); + + if (idx >= 0 && idx < menuCount) { + const char *name = sDesigner.form->menuItems[idx].name; + + if (name[0]) { + navigateToNamedEventSub(name, "Click"); + return; + } + } + } + + onMenu(win, menuId); +} + + // ============================================================ // stashDesignerState -- save current editor content and set status // ============================================================ @@ -6013,38 +6655,39 @@ static void switchToDesign(void) { dsgnNewForm(&sDesigner, "Form1"); } - // Create the form designer window (same size as runtime) + // Create the form designer window using the shared form window builder const char *formName = sDesigner.form ? sDesigner.form->name : "Form1"; + DsgnFormT *form = sDesigner.form; char title[128]; snprintf(title, sizeof(title), "%s [Design]", formName); - sFormWin = dvxCreateWindowCentered(sAc, title, IDE_DESIGN_W, IDE_DESIGN_H, true); + WidgetT *root; + WidgetT *contentBox; + + sFormWin = dsgnCreateFormWindow(sAc, title, + form ? form->layout : "VBox", + form ? form->resizable : true, + false, + false, + form ? form->width : IDE_DESIGN_W, + form ? form->height : IDE_DESIGN_H, + 0, 0, + &root, &contentBox); if (!sFormWin) { return; } + sFormWin->visible = true; sFormWin->onClose = onFormWinClose; - sFormWin->onMenu = onMenu; + sFormWin->onMenu = onFormWinMenu; sFormWin->accelTable = sWin ? sWin->accelTable : NULL; sDesigner.formWin = sFormWin; // Build preview menu bar from form's menu items dsgnBuildPreviewMenuBar(sFormWin, sDesigner.form); - WidgetT *root = wgtInitWindow(sAc, sFormWin); - WidgetT *contentBox; - - // Reuse root VBox directly for VBox layout (matches runtime). - // Only nest for HBox since wgtInitWindow always creates a VBox. - if (sDesigner.form && strcasecmp(sDesigner.form->layout, "HBox") == 0) { - contentBox = wgtHBox(root); - contentBox->weight = 100; - } else { - contentBox = root; - } - // Override paint and mouse AFTER wgtInitWindow (which sets widgetOnPaint) sFormWin->onPaint = onFormWinPaint; sFormWin->onMouse = onFormWinMouse; @@ -6114,10 +6757,10 @@ static void teardownFormWin(void) { // Toolbar button handlers // ============================================================ -static void onTbOpen(WidgetT *w) { (void)w; loadFile(); } -static void onTbSave(WidgetT *w) { (void)w; saveFile(); } -static void onTbRun(WidgetT *w) { (void)w; compileAndRun(); } -static void onTbStop(WidgetT *w) { (void)w; sStopRequested = true; if (sVm) { sVm->running = false; } setStatus("Program stopped."); } +static void onTbOpen(WidgetT *w) { (void)w; handleProjectCmd(CMD_PRJ_OPEN); } +static void onTbSave(WidgetT *w) { (void)w; handleFileCmd(CMD_SAVE); } +static void onTbRun(WidgetT *w) { (void)w; handleRunCmd(CMD_RUN); } +static void onTbStop(WidgetT *w) { (void)w; handleRunCmd(CMD_STOP); } static void onTbCode(WidgetT *w) { (void)w; handleViewCmd(CMD_VIEW_CODE); } static void onTbDesign(WidgetT *w) { (void)w; handleViewCmd(CMD_VIEW_DESIGN); } static void debugSetBreakTitles(bool paused) { @@ -6126,7 +6769,7 @@ static void debugSetBreakTitles(bool paused) { } for (int32_t i = 0; i < sDbgFormRt->formCount; i++) { - BasFormT *form = &sDbgFormRt->forms[i]; + BasFormT *form = sDbgFormRt->forms[i]; if (!form->window) { continue; @@ -7532,7 +8175,7 @@ static void updateProjectMenuState(void) { bool isIdle = (sDbgState == DBG_IDLE); bool isPaused = (sDbgState == DBG_PAUSED); bool isRunning = (sDbgState == DBG_RUNNING); - bool canRun = isIdle || isPaused; + bool canRun = hasProject && (isIdle || isPaused); bool canStop = isRunning || isPaused; // Project menu @@ -7580,6 +8223,7 @@ static void updateProjectMenuState(void) { // Run menu wmMenuItemSetEnabled(sWin->menuBar, CMD_RUN, canRun); + wmMenuItemSetEnabled(sWin->menuBar, CMD_RUN_NOCMP, canRun && sCachedModule != NULL); wmMenuItemSetEnabled(sWin->menuBar, CMD_DEBUG, canRun); wmMenuItemSetEnabled(sWin->menuBar, CMD_STOP, canStop); wmMenuItemSetEnabled(sWin->menuBar, CMD_STEP_INTO, canRun); @@ -8439,7 +9083,7 @@ static void updateDropdowns(void) { objNames[objNameCount++] = sDesigner.form->name; for (int32_t ci = 0; ci < (int32_t)arrlen(sDesigner.form->controls) && objNameCount < 511; ci++) { - objNames[objNameCount++] = sDesigner.form->controls[ci].name; + objNames[objNameCount++] = sDesigner.form->controls[ci]->name; } for (int32_t mi = 0; mi < (int32_t)arrlen(sDesigner.form->menuItems) && objNameCount < 511; mi++) { @@ -8500,7 +9144,7 @@ static void updateDropdowns(void) { arrput(sObjItems, sDesigner.form->name); for (int32_t i = 0; i < (int32_t)arrlen(sDesigner.form->controls); i++) { - arrput(sObjItems, sDesigner.form->controls[i].name); + arrput(sObjItems, sDesigner.form->controls[i]->name); } // Add menu item names (non-separator, non-top-level-header-only) diff --git a/apps/dvxbasic/ide/ideMenuEditor.c b/apps/dvxbasic/ide/ideMenuEditor.c index 404a861..1733a68 100644 --- a/apps/dvxbasic/ide/ideMenuEditor.c +++ b/apps/dvxbasic/ide/ideMenuEditor.c @@ -47,6 +47,7 @@ typedef struct { WidgetT *captionInput; WidgetT *nameInput; WidgetT *checkedCb; + WidgetT *radioCheckCb; WidgetT *enabledCb; WidgetT *listBox; } MnuEdStateT; @@ -143,8 +144,9 @@ static void applyFields(void) { sMed.nameAutoGen = true; } - mi->checked = wgtCheckboxIsChecked(sMed.checkedCb); - mi->enabled = wgtCheckboxIsChecked(sMed.enabledCb); + mi->checked = wgtCheckboxIsChecked(sMed.checkedCb); + mi->radioCheck = wgtCheckboxIsChecked(sMed.radioCheckCb); + mi->enabled = wgtCheckboxIsChecked(sMed.enabledCb); } @@ -180,8 +182,9 @@ static void loadFields(void) { wgtSetText(sMed.captionInput, ""); wgtSetText(sMed.nameInput, ""); wgtCheckboxSetChecked(sMed.checkedCb, false); + wgtCheckboxSetChecked(sMed.radioCheckCb, false); wgtCheckboxSetChecked(sMed.enabledCb, true); - sMed.nameAutoGen = true; // new blank item — auto-gen eligible + sMed.nameAutoGen = true; // new blank item -- auto-gen eligible return; } @@ -190,6 +193,7 @@ static void loadFields(void) { wgtSetText(sMed.captionInput, mi->caption); wgtSetText(sMed.nameInput, mi->name); wgtCheckboxSetChecked(sMed.checkedCb, mi->checked); + wgtCheckboxSetChecked(sMed.radioCheckCb, mi->radioCheck); wgtCheckboxSetChecked(sMed.enabledCb, mi->enabled); } @@ -661,14 +665,15 @@ bool mnuEditorDialog(AppContextT *ctx, DsgnFormT *form) { // Check row WidgetT *chkRow = wgtHBox(root); chkRow->spacing = wgtPixels(12); - sMed.checkedCb = wgtCheckbox(chkRow, "Checked"); - sMed.enabledCb = wgtCheckbox(chkRow, "Enabled"); + sMed.checkedCb = wgtCheckbox(chkRow, "Checked"); + sMed.radioCheckCb = wgtCheckbox(chkRow, "RadioCheck"); + sMed.enabledCb = wgtCheckbox(chkRow, "Enabled"); wgtCheckboxSetChecked(sMed.enabledCb, true); // Listbox sMed.listBox = wgtListBox(root); - sMed.listBox->weight = 100; - sMed.listBox->onClick = onListClick; + sMed.listBox->weight = 100; + sMed.listBox->onChange = onListClick; // Arrow buttons WidgetT *arrowRow = wgtHBox(root); diff --git a/apps/dvxbasic/ide/ideProperties.c b/apps/dvxbasic/ide/ideProperties.c index b6df0b6..43a3e96 100644 --- a/apps/dvxbasic/ide/ideProperties.c +++ b/apps/dvxbasic/ide/ideProperties.c @@ -82,7 +82,7 @@ static int32_t getTableNames(const char *dbName, char names[][DSGN_MAX_NAME], in static void onPrpClose(WindowT *win); static void onPropDblClick(WidgetT *w); static void onTreeItemClick(WidgetT *w); -static void onTreeReorder(WidgetT *w); +static void onTreeChange(WidgetT *w); // ============================================================ @@ -134,7 +134,7 @@ static int32_t getDataFieldNames(const DsgnStateT *ds, const char *dataSourceNam int32_t ctrlCount = (int32_t)arrlen(ds->form->controls); for (int32_t i = 0; i < ctrlCount; i++) { - DsgnControlT *ctrl = &ds->form->controls[i]; + DsgnControlT *ctrl = ds->form->controls[i]; if (strcasecmp(ctrl->typeName, "Data") != 0 || strcasecmp(ctrl->name, dataSourceName) != 0) { continue; @@ -288,6 +288,7 @@ static int32_t getTableNames(const char *dbName, char names[][DSGN_MAX_NAME], in #define PROP_TYPE_BOOL WGT_IFACE_BOOL #define PROP_TYPE_ENUM WGT_IFACE_ENUM #define PROP_TYPE_READONLY 255 +#define PROP_TYPE_LAYOUT 251 #define PROP_TYPE_DATASOURCE 254 #define PROP_TYPE_DATAFIELD 253 #define PROP_TYPE_RECORDSRC 252 @@ -317,6 +318,7 @@ static uint8_t getPropType(const char *propName, const char *typeName) { if (strcasecmp(propName, "Centered") == 0) { return PROP_TYPE_BOOL; } if (strcasecmp(propName, "Visible") == 0) { return PROP_TYPE_BOOL; } if (strcasecmp(propName, "Enabled") == 0) { return PROP_TYPE_BOOL; } + if (strcasecmp(propName, "Layout") == 0) { return PROP_TYPE_LAYOUT; } if (strcasecmp(propName, "DataSource") == 0) { return PROP_TYPE_DATASOURCE; } if (strcasecmp(propName, "DataField") == 0) { return PROP_TYPE_DATAFIELD; } if (strcasecmp(propName, "RecordSource") == 0) { return PROP_TYPE_RECORDSRC; } @@ -384,7 +386,7 @@ static void cascadeToChildren(DsgnStateT *ds, const char *parentName, bool visib int32_t count = (int32_t)arrlen(ds->form->controls); for (int32_t i = 0; i < count; i++) { - DsgnControlT *child = &ds->form->controls[i]; + DsgnControlT *child = ds->form->controls[i]; if (strcasecmp(child->parentName, parentName) != 0) { continue; @@ -421,40 +423,78 @@ static void onPropDblClick(WidgetT *w) { const char *propName = sCellData[row * 2]; const char *curValue = sCellData[row * 2 + 1]; - // Layout toggles directly -- no input box needed - if (strcasecmp(propName, "Layout") == 0 && sDs->selectedIdx < 0) { - if (strcasecmp(sDs->form->layout, "VBox") == 0) { - snprintf(sDs->form->layout, DSGN_MAX_NAME, "HBox"); - } else { - snprintf(sDs->form->layout, DSGN_MAX_NAME, "VBox"); + // Layout — select from discovered layout containers + if (strcasecmp(propName, "Layout") == 0) { + // Discover available layout types from loaded widget interfaces. + // A layout container is isContainer with WGT_CREATE_PARENT (no extra args). + const char *layoutNames[32]; + int32_t layoutCount = 0; + int32_t ifaceTotal = wgtIfaceCount(); + + for (int32_t i = 0; i < ifaceTotal && layoutCount < 32; i++) { + const WgtIfaceT *iface = wgtIfaceAt(i, NULL); + + if (iface && iface->isContainer && iface->createSig == WGT_CREATE_PARENT && iface->basName) { + layoutNames[layoutCount++] = iface->basName; + } } + if (layoutCount == 0) { + return; + } + + // Determine whose layout we're changing + char *layoutField = NULL; + + if (sDs->selectedIdx < 0) { + layoutField = sDs->form->layout; + } else { + DsgnControlT *ctrl = sDs->form->controls[sDs->selectedIdx]; + + for (int32_t pi = 0; pi < ctrl->propCount; pi++) { + if (strcasecmp(ctrl->props[pi].name, "Layout") == 0) { + layoutField = ctrl->props[pi].value; + break; + } + } + + if (!layoutField) { + return; + } + } + + // Find current selection + int32_t defIdx = 0; + + for (int32_t i = 0; i < layoutCount; i++) { + if (strcasecmp(layoutField, layoutNames[i]) == 0) { + defIdx = i; + break; + } + } + + int32_t chosenIdx = 0; + + if (!dvxChoiceDialog(sPrpCtx, "Layout", "Select layout type:", layoutNames, layoutCount, defIdx, &chosenIdx)) { + return; + } + + snprintf(layoutField, DSGN_MAX_NAME, "%s", layoutNames[chosenIdx]); sDs->form->dirty = true; - // Replace the content box with the new layout type + // Rebuild the form designer to apply the new layout if (sDs->formWin && sDs->formWin->widgetRoot) { WidgetT *root = sDs->formWin->widgetRoot; - // Remove old content box root->firstChild = NULL; root->lastChild = NULL; - // Create new content box - WidgetT *contentBox; + WidgetT *contentBox = dsgnCreateContentBox(root, layoutField); - if (strcasecmp(sDs->form->layout, "HBox") == 0) { - contentBox = wgtHBox(root); - } else { - contentBox = wgtVBox(root); - } - - contentBox->weight = 100; - - // Clear widget pointers and recreate int32_t cc = (int32_t)arrlen(sDs->form->controls); for (int32_t ci = 0; ci < cc; ci++) { - sDs->form->controls[ci].widget = NULL; + sDs->form->controls[ci]->widget = NULL; } dsgnCreateWidgets(sDs, contentBox); @@ -473,7 +513,7 @@ static void onPropDblClick(WidgetT *w) { int32_t ctrlCount = (int32_t)arrlen(sDs->form->controls); if (sDs->selectedIdx >= 0 && sDs->selectedIdx < ctrlCount) { - ctrlTypeName = sDs->form->controls[sDs->selectedIdx].typeName; + ctrlTypeName = sDs->form->controls[sDs->selectedIdx]->typeName; } uint8_t propType = getPropType(propName, ctrlTypeName); @@ -523,8 +563,8 @@ static void onPropDblClick(WidgetT *w) { dataNames[dataCount++] = "(none)"; for (int32_t i = 0; i < formCtrlCount && dataCount < 16; i++) { - if (strcasecmp(sDs->form->controls[i].typeName, "Data") == 0) { - dataNames[dataCount++] = sDs->form->controls[i].name; + if (strcasecmp(sDs->form->controls[i]->typeName, "Data") == 0) { + dataNames[dataCount++] = sDs->form->controls[i]->name; } } @@ -561,7 +601,7 @@ static void onPropDblClick(WidgetT *w) { const char *dataSrc = ""; if (sDs->selectedIdx >= 0 && sDs->selectedIdx < selCount) { - DsgnControlT *selCtrl = &sDs->form->controls[sDs->selectedIdx]; + DsgnControlT *selCtrl = sDs->form->controls[sDs->selectedIdx]; if (strcasecmp(selCtrl->typeName, "Data") == 0) { dataSrc = selCtrl->name; @@ -623,7 +663,7 @@ static void onPropDblClick(WidgetT *w) { const char *dbName = ""; if (sDs->selectedIdx >= 0 && sDs->selectedIdx < selCount) { - DsgnControlT *selCtrl = &sDs->form->controls[sDs->selectedIdx]; + DsgnControlT *selCtrl = sDs->form->controls[sDs->selectedIdx]; for (int32_t i = 0; i < selCtrl->propCount; i++) { if (strcasecmp(selCtrl->props[i].name, "DatabaseName") == 0) { @@ -699,7 +739,7 @@ static void onPropDblClick(WidgetT *w) { int32_t count = (int32_t)arrlen(sDs->form->controls); if (sDs->selectedIdx >= 0 && sDs->selectedIdx < count) { - DsgnControlT *ctrl = &sDs->form->controls[sDs->selectedIdx]; + DsgnControlT *ctrl = sDs->form->controls[sDs->selectedIdx]; if (strcasecmp(propName, "Name") == 0) { char oldName[DSGN_MAX_NAME]; @@ -707,7 +747,7 @@ static void onPropDblClick(WidgetT *w) { // Rename all members of a control array, not just the selected one for (int32_t i = 0; i < count; i++) { - DsgnControlT *c = &sDs->form->controls[i]; + DsgnControlT *c = sDs->form->controls[i]; if (strcasecmp(c->name, oldName) == 0) { snprintf(c->name, DSGN_MAX_NAME, "%.31s", newValue); @@ -722,7 +762,7 @@ static void onPropDblClick(WidgetT *w) { // references on all other controls that pointed to the old name if (strcasecmp(ctrl->typeName, "Data") == 0) { for (int32_t i = 0; i < count; i++) { - DsgnControlT *c = &sDs->form->controls[i]; + DsgnControlT *c = sDs->form->controls[i]; for (int32_t j = 0; j < c->propCount; j++) { if ((strcasecmp(c->props[j].name, "DataSource") == 0 || @@ -1002,7 +1042,7 @@ static void onTreeItemClick(WidgetT *w) { int32_t count = (int32_t)arrlen(sDs->form->controls); for (int32_t i = 0; i < count; i++) { - if (strcmp(sDs->form->controls[i].name, clickedName) == 0) { + if (strcmp(sDs->form->controls[i]->name, clickedName) == 0) { sDs->selectedIdx = i; if (sDs->formWin) { @@ -1022,7 +1062,7 @@ static void onTreeItemClick(WidgetT *w) { // Walk tree items recursively, collecting control names in order. -static void collectTreeOrder(WidgetT *parent, DsgnControlT *srcArr, int32_t srcCount, DsgnControlT **outArr, const char *parentName) { +static void collectTreeOrder(WidgetT *parent, DsgnControlT **srcArr, int32_t srcCount, DsgnControlT ***outArr, const char *parentName) { for (WidgetT *item = parent->firstChild; item; item = item->nextSibling) { const char *label = (const char *)item->userData; @@ -1041,10 +1081,9 @@ static void collectTreeOrder(WidgetT *parent, DsgnControlT *srcArr, int32_t srcC itemName[ni] = '\0'; for (int32_t i = 0; i < srcCount; i++) { - if (strcmp(srcArr[i].name, itemName) == 0) { - DsgnControlT ctrl = srcArr[i]; - snprintf(ctrl.parentName, DSGN_MAX_NAME, "%s", parentName); - arrput(*outArr, ctrl); + if (strcmp(srcArr[i]->name, itemName) == 0) { + snprintf(srcArr[i]->parentName, DSGN_MAX_NAME, "%s", parentName); + arrput(*outArr, srcArr[i]); // Recurse into children (for containers) if (item->firstChild) { @@ -1058,23 +1097,60 @@ static void collectTreeOrder(WidgetT *parent, DsgnControlT *srcArr, int32_t srcC } -static void onTreeReorder(WidgetT *w) { +// Check whether the tree order matches the controls array. +// Returns true if they match (no reorder happened). + +static bool treeOrderMatches(void) { + WidgetT *formItem = sTree->firstChild; + + if (!formItem) { + return true; + } + + DsgnControlT **newArr = NULL; + int32_t count = (int32_t)arrlen(sDs->form->controls); + + collectTreeOrder(formItem, sDs->form->controls, count, &newArr, ""); + + bool match = ((int32_t)arrlen(newArr) == count); + + if (match) { + for (int32_t i = 0; i < count; i++) { + if (strcmp(newArr[i]->name, sDs->form->controls[i]->name) != 0 || + strcmp(newArr[i]->parentName, sDs->form->controls[i]->parentName) != 0) { + match = false; + break; + } + } + } + + arrfree(newArr); + return match; +} + + +static void onTreeChange(WidgetT *w) { (void)w; if (!sDs || !sDs->form || !sTree || sUpdating) { return; } + // If the order hasn't changed, this is just a selection or expand/collapse. + // The onClick handler on individual items handles selection updates. + if (treeOrderMatches()) { + return; + } + + // Actual reorder happened — rebuild the controls array from tree order. int32_t count = (int32_t)arrlen(sDs->form->controls); - DsgnControlT *newArr = NULL; + DsgnControlT **newArr = NULL; WidgetT *formItem = sTree->firstChild; if (!formItem) { return; } - // Collect all controls from the tree in their new order, - // handling nesting (items dragged into containers get parentName updated). collectTreeOrder(formItem, sDs->form->controls, count, &newArr, ""); // If we lost items (dragged above form), revert @@ -1095,7 +1171,7 @@ static void onTreeReorder(WidgetT *w) { int32_t newCount = (int32_t)arrlen(sDs->form->controls); for (int32_t i = 0; i < newCount; i++) { - sDs->form->controls[i].widget = NULL; + sDs->form->controls[i]->widget = NULL; } dsgnCreateWidgets(sDs, sDs->form->contentBox); @@ -1138,7 +1214,7 @@ WindowT *prpCreate(AppContextT *ctx, DsgnStateT *ds) { // Control tree (top pane) sTree = wgtTreeView(splitter); - sTree->onChange = onTreeReorder; + sTree->onChange = onTreeChange; wgtTreeViewSetReorderable(sTree, true); // Property ListView (bottom pane) @@ -1217,7 +1293,7 @@ void prpRebuildTree(DsgnStateT *ds) { WidgetT **treeItems = NULL; for (int32_t i = 0; i < count; i++) { - DsgnControlT *ctrl = &ds->form->controls[i]; + DsgnControlT *ctrl = ds->form->controls[i]; char buf[128]; if (ctrl->index >= 0) { @@ -1234,7 +1310,7 @@ void prpRebuildTree(DsgnStateT *ds) { if (ctrl->parentName[0]) { for (int32_t j = 0; j < i; j++) { - if (strcasecmp(ds->form->controls[j].name, ctrl->parentName) == 0 && treeItems) { + if (strcasecmp(ds->form->controls[j]->name, ctrl->parentName) == 0 && treeItems) { treeParent = treeItems[j]; break; } @@ -1265,13 +1341,62 @@ void prpRebuildTree(DsgnStateT *ds) { // prpRefresh // ============================================================ +// Walk tree items recursively to find the one matching a control name. + +static WidgetT *findTreeItemByName(WidgetT *parent, const char *name) { + for (WidgetT *item = parent->firstChild; item; item = item->nextSibling) { + const char *label = (const char *)item->userData; + + if (label) { + // Labels are "Name (Type)" — match the name portion + int32_t len = 0; + + while (label[len] && label[len] != ' ') { + len++; + } + + if ((int32_t)strlen(name) == len && strncmp(label, name, len) == 0) { + return item; + } + } + + // Recurse into children (containers) + WidgetT *found = findTreeItemByName(item, name); + + if (found) { + return found; + } + } + + return NULL; +} + + void prpRefresh(DsgnStateT *ds) { if (!ds || !ds->form) { return; } - // Don't rebuild the tree here -- just update selection on existing items. - // prpRebuildTree destroys all items which loses TreeView selection state. + // Sync tree selection to match selectedIdx + if (sTree && !sUpdating) { + WidgetT *formItem = sTree->firstChild; + + if (formItem) { + WidgetT *target = NULL; + + if (ds->selectedIdx < 0) { + target = formItem; + } else if (ds->selectedIdx < (int32_t)arrlen(ds->form->controls)) { + target = findTreeItemByName(formItem, ds->form->controls[ds->selectedIdx]->name); + } + + if (target) { + sUpdating = true; + wgtTreeViewSetSelected(sTree, target); + sUpdating = false; + } + } + } // Update property ListView if (!sPropList) { @@ -1283,7 +1408,7 @@ void prpRefresh(DsgnStateT *ds) { int32_t count = (int32_t)arrlen(ds->form->controls); if (ds->selectedIdx >= 0 && ds->selectedIdx < count) { - DsgnControlT *ctrl = &ds->form->controls[ds->selectedIdx]; + DsgnControlT *ctrl = ds->form->controls[ds->selectedIdx]; char buf[32]; addPropRow("Name", ctrl->name); diff --git a/apps/progman/progman.c b/apps/progman/progman.c index 3aa8dc2..4819f38 100644 --- a/apps/progman/progman.c +++ b/apps/progman/progman.c @@ -429,8 +429,12 @@ static void scanAppsDirRecurse(const char *dirPath) { continue; } + // Copy d_name before recursion — readdir may use a shared buffer + char name[MAX_PATH_LEN]; + snprintf(name, sizeof(name), "%s", ent->d_name); + char fullPath[MAX_PATH_LEN]; - snprintf(fullPath, sizeof(fullPath), "%s%c%s", dirPath, DVX_PATH_SEP, ent->d_name); + snprintf(fullPath, sizeof(fullPath), "%s%c%s", dirPath, DVX_PATH_SEP, name); // Check if this is a directory -- recurse into it struct stat st; @@ -440,21 +444,21 @@ static void scanAppsDirRecurse(const char *dirPath) { continue; } - int32_t len = strlen(ent->d_name); + int32_t len = strlen(name); if (len < 5) { continue; } // Check for .app extension (case-insensitive) - const char *ext = ent->d_name + len - 4; + const char *ext = name + len - 4; if (strcasecmp(ext, ".app") != 0) { continue; } // Skip ourselves - if (strcasecmp(ent->d_name, "progman.app") == 0) { + if (strcasecmp(name, "progman.app") == 0) { continue; } @@ -469,7 +473,7 @@ static void scanAppsDirRecurse(const char *dirPath) { nameLen = SHELL_APP_NAME_MAX - 1; } - memcpy(newEntry.name, ent->d_name, nameLen); + memcpy(newEntry.name, name, nameLen); newEntry.name[nameLen] = '\0'; if (newEntry.name[0] >= 'a' && newEntry.name[0] <= 'z') { @@ -480,13 +484,13 @@ static void scanAppsDirRecurse(const char *dirPath) { newEntry.iconData = dvxResLoadIcon(sAc, fullPath, "icon32", &newEntry.iconW, &newEntry.iconH, &newEntry.iconPitch); if (!newEntry.iconData) { - dvxLog("Progman: no icon32 resource in %s", ent->d_name); + dvxLog("Progman: no icon32 resource in %s", name); } dvxResLoadText(fullPath, "name", newEntry.name, SHELL_APP_NAME_MAX); dvxResLoadText(fullPath, "description", newEntry.tooltip, sizeof(newEntry.tooltip)); - dvxLog("Progman: found %s (%s) icon=%s", newEntry.name, ent->d_name, newEntry.iconData ? "yes" : "no"); + dvxLog("Progman: found %s (%s) icon=%s", newEntry.name, name, newEntry.iconData ? "yes" : "no"); arrput(sAppFiles, newEntry); sAppCount = (int32_t)arrlen(sAppFiles); diff --git a/core/Makefile b/core/Makefile index 7dc734c..ec36dac 100644 --- a/core/Makefile +++ b/core/Makefile @@ -23,7 +23,7 @@ TARGETDIR = $(LIBSDIR)/kpunch/libdvx TARGET = $(TARGETDIR)/libdvx.lib # libdvx.lib export prefixes -DVX_EXPORTS = -E _dvx -E _wgt -E _wm -E _prefs -E _rect -E _draw -E _pack -E _text \ +DVX_EXPORTS = -E _dvx -E _wgt -E _wm -E _prefs -E _rect -E _draw -E _pack -E _unpack -E _text \ -E _setClip -E _resetClip -E _stbi_ -E _stbi_write -E _dirtyList \ -E _widget \ -E _sCursor -E _sDbl -E _sDebug -E _sClosed -E _sFocused -E _sKey \ diff --git a/core/dvxApp.c b/core/dvxApp.c index 5083725..14a1da4 100644 --- a/core/dvxApp.c +++ b/core/dvxApp.c @@ -5069,8 +5069,7 @@ bool dvxUpdate(AppContextT *ctx) { for (int32_t i = 0; i < ctx->stack.count; i++) { WindowT *win = ctx->stack.windows[i]; - if (win->needsPaint && win->onPaint) { - win->needsPaint = false; + if (win->fullRepaint && win->onPaint) { dvxInvalidateWindow(ctx, win); } } diff --git a/core/dvxDialog.c b/core/dvxDialog.c index 2df3da0..d41cbe6 100644 --- a/core/dvxDialog.c +++ b/core/dvxDialog.c @@ -512,7 +512,7 @@ static void onMsgBoxPaint(WindowT *win, RectT *dirtyArea) { widgetLayoutChildren(root, &ctx->font); // Paint widgets - wgtPaint(root, &cd, &ctx->blitOps, &ctx->font, &ctx->colors); + wgtPaint(root, &cd, &ctx->blitOps, &ctx->font, &ctx->colors, true); } } diff --git a/core/dvxTypes.h b/core/dvxTypes.h index c58cd93..618c3fb 100644 --- a/core/dvxTypes.h +++ b/core/dvxTypes.h @@ -507,7 +507,7 @@ typedef struct WindowT { bool resizable; bool modal; bool contentDirty; // true when contentBuf has changed since last icon refresh - bool needsPaint; // true until first onPaint call (auto-paint on next frame) + bool fullRepaint; // true = clear + repaint all widgets; false = only dirty ones int32_t maxW; // maximum width (WM_MAX_FROM_SCREEN = use screen width) int32_t maxH; // maximum height (WM_MAX_FROM_SCREEN = use screen height) // Pre-maximize geometry is saved so wmRestore() can put the window diff --git a/core/dvxVideo.c b/core/dvxVideo.c index 5c4483c..1fca331 100644 --- a/core/dvxVideo.c +++ b/core/dvxVideo.c @@ -85,6 +85,32 @@ uint32_t packColor(const DisplayT *d, uint8_t r, uint8_t g, uint8_t b) { } +// ============================================================ +// unpackColor +// ============================================================ + +void unpackColor(const DisplayT *d, uint32_t color, uint8_t *r, uint8_t *g, uint8_t *b) { + if (d->format.bitsPerPixel == 8) { + if (color < 256 && d->palette) { + *r = d->palette[color * 3]; + *g = d->palette[color * 3 + 1]; + *b = d->palette[color * 3 + 2]; + } else { + *r = *g = *b = 0; + } + return; + } + + uint32_t rv = (color >> d->format.redShift) & ((1u << d->format.redBits) - 1); + uint32_t gv = (color >> d->format.greenShift) & ((1u << d->format.greenBits) - 1); + uint32_t bv = (color >> d->format.blueShift) & ((1u << d->format.blueBits) - 1); + + *r = (uint8_t)(rv << (8 - d->format.redBits)); + *g = (uint8_t)(gv << (8 - d->format.greenBits)); + *b = (uint8_t)(bv << (8 - d->format.blueBits)); +} + + // ============================================================ // resetClipRect // ============================================================ diff --git a/core/dvxVideo.h b/core/dvxVideo.h index 5a8ba9c..d4646e9 100644 --- a/core/dvxVideo.h +++ b/core/dvxVideo.h @@ -37,6 +37,9 @@ void videoShutdown(DisplayT *d); // linear scan of the grey ramp and chrome entries). uint32_t packColor(const DisplayT *d, uint8_t r, uint8_t g, uint8_t b); +// Unpack a native pixel value back to 8-bit R, G, B components. +void unpackColor(const DisplayT *d, uint32_t color, uint8_t *r, uint8_t *g, uint8_t *b); + // Set the clip rectangle on the display. All subsequent draw operations // will be clipped to this rectangle. The caller is responsible for // saving and restoring the clip rect around scoped operations. diff --git a/core/dvxWgt.h b/core/dvxWgt.h index f8985d8..d26736a 100644 --- a/core/dvxWgt.h +++ b/core/dvxWgt.h @@ -248,8 +248,15 @@ typedef struct WidgetT { bool enabled; bool readOnly; bool swallowTab; // Tab key goes to widget, not focus nav + bool paintDirty; // needs repaint (set by wgtInvalidatePaint) char accelKey; // lowercase accelerator character, 0 if none + // Content offset: mouse event coordinates are adjusted by this amount + // so callbacks receive content-relative coords (e.g. canvas subtracts + // its border so (0,0) = first drawable pixel, matching VB3 behavior). + int32_t contentOffX; + int32_t contentOffY; + // User data and callbacks. These fire for ALL widget types from the // central event dispatcher, not from individual widget handlers. // Type-specific handlers (e.g. button press animation, listbox @@ -524,7 +531,7 @@ void wgtLayout(WidgetT *root, int32_t availW, int32_t availH, const BitmapFontT // clip rect is set to its bounds before calling its paint function. // Overlays (dropdown popups, tooltips) are painted in a second pass // after the main tree so they render on top of everything. -void wgtPaint(WidgetT *root, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors); +void wgtPaint(WidgetT *root, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors, bool fullRepaint); // ============================================================ // Widget API registry @@ -567,7 +574,14 @@ const void *wgtGetApi(const char *name); #define WGT_SIG_INT_BOOL 5 // void fn(WidgetT *, int32_t, bool) #define WGT_SIG_RET_INT 6 // int32_t fn(const WidgetT *) #define WGT_SIG_RET_BOOL 7 // bool fn(const WidgetT *) -#define WGT_SIG_RET_BOOL_INT 8 // bool fn(const WidgetT *, int32_t) +#define WGT_SIG_RET_BOOL_INT 8 // bool fn(const WidgetT *, int32_t) +#define WGT_SIG_INT3 9 // void fn(WidgetT *, int32_t, int32_t, int32_t) +#define WGT_SIG_INT4 10 // void fn(WidgetT *, int32_t, int32_t, int32_t, int32_t) +#define WGT_SIG_RET_INT_INT_INT 11 // int32_t fn(const WidgetT *, int32_t, int32_t) +#define WGT_SIG_INT_INT_STR 12 // void fn(WidgetT *, int32_t, int32_t, const char *) +#define WGT_SIG_INT5 13 // void fn(WidgetT *, int32_t, int32_t, int32_t, int32_t, int32_t) +#define WGT_SIG_RET_STR_INT 14 // const char *fn(const WidgetT *, int32_t) +#define WGT_SIG_RET_STR_INT_INT 15 // const char *fn(const WidgetT *, int32_t, int32_t) // Property descriptor typedef struct { diff --git a/core/dvxWm.c b/core/dvxWm.c index 3fa2ac3..025e264 100644 --- a/core/dvxWm.c +++ b/core/dvxWm.c @@ -1047,6 +1047,38 @@ void wmAddMenuSeparator(MenuT *menu) { } +// ============================================================ +// wmRemoveMenuItem -- remove a menu item by command ID +// ============================================================ + +bool wmRemoveMenuItem(MenuT *menu, int32_t id) { + if (!menu) { + return false; + } + + for (int32_t i = 0; i < menu->itemCount; i++) { + if (menu->items[i].id == id && !menu->items[i].separator) { + memmove(&menu->items[i], &menu->items[i + 1], (menu->itemCount - i - 1) * sizeof(MenuItemT)); + menu->itemCount--; + return true; + } + } + + return false; +} + + +// ============================================================ +// wmClearMenuItems -- remove all items from a menu +// ============================================================ + +void wmClearMenuItems(MenuT *menu) { + if (menu) { + menu->itemCount = 0; + } +} + + // ============================================================ // wmMenuFindItem -- find menu item by command ID across all menus // ============================================================ @@ -1291,7 +1323,7 @@ WindowT *wmCreateWindow(WindowStackT *stack, DisplayT *d, const char *title, int memset(win->contentBuf, 0xFF, bufSize); } - win->needsPaint = true; + win->fullRepaint = true; stack->windows[stack->count] = win; stack->count++; @@ -1850,6 +1882,7 @@ void wmMaximize(WindowStackT *stack, DirtyListT *dl, const DisplayT *d, WindowT } if (win->onPaint) { + win->fullRepaint = true; RectT fullRect = {0, 0, win->contentW, win->contentH}; win->onPaint(win, &fullRect); win->contentDirty = true; @@ -2367,6 +2400,7 @@ void wmResizeMove(WindowStackT *stack, DirtyListT *dl, const DisplayT *d, int32_ } if (win->onPaint) { + win->fullRepaint = true; RectT fullRect = {0, 0, win->contentW, win->contentH}; win->onPaint(win, &fullRect); win->contentDirty = true; @@ -2420,6 +2454,7 @@ void wmRestore(WindowStackT *stack, DirtyListT *dl, const DisplayT *d, WindowT * } if (win->onPaint) { + win->fullRepaint = true; RectT fullRect = {0, 0, win->contentW, win->contentH}; win->onPaint(win, &fullRect); win->contentDirty = true; diff --git a/core/dvxWm.h b/core/dvxWm.h index ec874c7..d064448 100644 --- a/core/dvxWm.h +++ b/core/dvxWm.h @@ -85,6 +85,12 @@ void wmAddMenuRadioItem(MenuT *menu, const char *label, int32_t id, bool checked // Insert a horizontal separator line. Separators are not interactive. void wmAddMenuSeparator(MenuT *menu); +// Remove a menu item by command ID. Returns true if found and removed. +bool wmRemoveMenuItem(MenuT *menu, int32_t id); + +// Remove all items from a menu (preserves the menu itself on the menu bar). +void wmClearMenuItems(MenuT *menu); + // Query or set the checked state of a menu item by command ID. // Searches all menus in the menu bar. For radio items, setting // checked=true also unchecks other radio items in the same group. diff --git a/core/platform/dvxPlatformDos.c b/core/platform/dvxPlatformDos.c index f8a938b..f5b6e8c 100644 --- a/core/platform/dvxPlatformDos.c +++ b/core/platform/dvxPlatformDos.c @@ -90,17 +90,8 @@ static void sysInfoAppend(const char *fmt, ...); static bool sHasMouseWheel = false; static int32_t sLastWheelDelta = 0; -// Software cursor tracking. Two modes detected at runtime: -// Mickey mode (default): function 0Bh raw deltas accumulated into -// sCurX/sCurY. Works on 86Box and real hardware where function -// 03h coordinates may be wrong in VESA modes. -// Absolute mode: function 03h coordinates used directly. Auto- -// activated when function 0Bh returns zero deltas but function -// 03h position is changing (DOSBox-X seamless mouse). -static bool sAbsMouseMode = false; -static bool sDetecting = true; -static int32_t sPrevAbsX = -1; -static int32_t sPrevAbsY = -1; +// Mouse coordinate range set by functions 07h/08h in platformMouseInit. +// Position read directly from function 03h (absolute coordinates). static int32_t sMouseRangeW = 0; static int32_t sMouseRangeH = 0; static int32_t sCurX = 0; @@ -1564,13 +1555,11 @@ void platformMouseInit(int32_t screenW, int32_t screenH) { sCurX = screenW / 2; sCurY = screenH / 2; - // Function 00h: reset driver, detect mouse hardware + // Function 00h: reset driver memset(&r, 0, sizeof(r)); r.x.ax = 0x0000; __dpmi_int(0x33, &r); - // Set coordinate range for function 03h. Always do this so that - // absolute mode works if we switch to it during detection. // Function 07h: set horizontal range memset(&r, 0, sizeof(r)); r.x.ax = 0x0007; @@ -1592,10 +1581,6 @@ void platformMouseInit(int32_t screenW, int32_t screenH) { r.x.dx = screenH / 2; __dpmi_int(0x33, &r); - // Flush any stale mickey counters so the first poll starts clean. - memset(&r, 0, sizeof(r)); - r.x.ax = 0x000B; - __dpmi_int(0x33, &r); } @@ -1622,12 +1607,8 @@ void platformMouseSetAccel(int32_t threshold) { // platformMousePoll // ============================================================ // -// Reads button state via function 03h and raw mickey deltas via -// function 0Bh. Position is tracked in software (sCurX/sCurY) -// rather than using the driver's coordinates, because many real- -// hardware mouse drivers cannot handle VESA mode coordinate ranges. -// Function 0Bh returns the accumulated mickey motion since the last -// call and is reliable across all drivers. +// Reads button state and cursor position via INT 33h function 03h. +// Coordinate range was set by functions 07h/08h in platformMouseInit. void platformMousePoll(int32_t *mx, int32_t *my, int32_t *buttons) { __dpmi_regs r; @@ -1645,45 +1626,8 @@ void platformMousePoll(int32_t *mx, int32_t *my, int32_t *buttons) { sLastWheelDelta = (int32_t)(int8_t)(r.x.bx >> 8); } - int32_t absX = (int16_t)r.x.cx; - int32_t absY = (int16_t)r.x.dx; - - // Function 0Bh: read mickey motion counters - memset(&r, 0, sizeof(r)); - r.x.ax = 0x000B; - __dpmi_int(0x33, &r); - - int16_t mickeyDx = (int16_t)r.x.cx; - int16_t mickeyDy = (int16_t)r.x.dx; - - // Runtime detection: if mickeys are zero but the driver's own - // position changed, function 0Bh isn't generating deltas. - // Switch to absolute mode (DOSBox-X seamless mouse, etc.). - if (sDetecting) { - if (mickeyDx == 0 && mickeyDy == 0 && - sPrevAbsX >= 0 && (absX != sPrevAbsX || absY != sPrevAbsY)) { - sAbsMouseMode = true; - sDetecting = false; - dvxLog("Mouse: absolute mode (no mickeys detected)"); - } - - // Once we get real mickeys, stop checking - if (mickeyDx != 0 || mickeyDy != 0) { - sDetecting = false; - dvxLog("Mouse: mickey mode (raw deltas detected)"); - } - - sPrevAbsX = absX; - sPrevAbsY = absY; - } - - if (sAbsMouseMode) { - sCurX = absX; - sCurY = absY; - } else { - sCurX += mickeyDx; - sCurY += mickeyDy; - } + sCurX = (int16_t)r.x.cx; + sCurY = (int16_t)r.x.dx; if (sCurX < 0) { sCurX = 0; diff --git a/core/sysdoc.dhs b/core/sysdoc.dhs index 8214f95..d880160 100644 --- a/core/sysdoc.dhs +++ b/core/sysdoc.dhs @@ -1,8 +1,8 @@ -.topic sys.overview -.title DVX System Overview -.toc 0 System Overview +.topic sys.welcome +.title Welcome! +.toc 0 Welcome! .default -.h1 DVX System Overview +.h1 Welcome! DVX (DOS Visual eXecutive) is a graphical user interface environment for DOS, designed for 486-class hardware and above. This help file covers the system architecture, core API, libraries, and widget toolkit. diff --git a/core/widgetEvent.c b/core/widgetEvent.c index 817fdcc..acf1320 100644 --- a/core/widgetEvent.c +++ b/core/widgetEvent.c @@ -281,7 +281,7 @@ static void widgetOnMouseInner(WindowT *win, WidgetT *root, int32_t x, int32_t y if (sDragWidget && !(buttons & MOUSE_LEFT)) { wclsOnDragEnd(sDragWidget, root, x, y); - wgtInvalidatePaint(root); + wgtInvalidatePaint(sDragWidget); sDragWidget = NULL; return; } @@ -311,7 +311,7 @@ static void widgetOnMouseInner(WindowT *win, WidgetT *root, int32_t x, int32_t y if (sDragWidget->wclass && (sDragWidget->wclass->flags & WCLASS_RELAYOUT_ON_SCROLL)) { wgtInvalidate(sDragWidget); } else { - wgtInvalidatePaint(root); + wgtInvalidatePaint(sDragWidget); } return; @@ -353,6 +353,7 @@ static void widgetOnMouseInner(WindowT *win, WidgetT *root, int32_t x, int32_t y } if (!(buttons & MOUSE_LEFT)) { + sPrevMouseButtons = 0; return; } @@ -399,10 +400,10 @@ static void widgetOnMouseInner(WindowT *win, WidgetT *root, int32_t x, int32_t y } } - // Fire mouse event callbacks + // Fire mouse event callbacks (content-relative coordinates) if (hit->enabled) { - int32_t relX = vx - hit->x; - int32_t relY = vy - hit->y; + int32_t relX = vx - hit->x - hit->contentOffX; + int32_t relY = vy - hit->y - hit->contentOffY; // MouseDown: button just pressed if ((buttons & MOUSE_LEFT) && !(sPrevMouseButtons & MOUSE_LEFT)) { @@ -518,8 +519,13 @@ void widgetOnPaint(WindowT *win, RectT *dirtyArea) { cd.clipW = win->contentW; cd.clipH = win->contentH; - // Clear background - rectFill(&cd, &ctx->blitOps, 0, 0, win->contentW, win->contentH, ctx->colors.contentBg); + bool full = win->fullRepaint; + win->fullRepaint = false; + + if (full) { + // Full repaint: clear background, relayout, paint everything + rectFill(&cd, &ctx->blitOps, 0, 0, win->contentW, win->contentH, ctx->colors.contentBg); + } // Apply scroll offset and re-layout at virtual size int32_t scrollX = win->hScroll ? win->hScroll->value : 0; @@ -531,7 +537,10 @@ void widgetOnPaint(WindowT *win, RectT *dirtyArea) { root->y = -scrollY; root->w = layoutW; root->h = layoutH; - widgetLayoutChildren(root, &ctx->font); + + if (full) { + widgetLayoutChildren(root, &ctx->font); + } // Auto-focus first focusable widget if nothing has focus yet if (!sFocusedWidget) { @@ -542,8 +551,8 @@ void widgetOnPaint(WindowT *win, RectT *dirtyArea) { } } - // Paint widget tree (clip rect limits drawing to visible area) - wgtPaint(root, &cd, &ctx->blitOps, &ctx->font, &ctx->colors); + // Paint widget tree (full = all widgets, partial = only dirty ones) + wgtPaint(root, &cd, &ctx->blitOps, &ctx->font, &ctx->colors, full); // Paint overlay popups (dropdown/combobox) widgetPaintOverlays(root, &cd, &ctx->blitOps, &ctx->font, &ctx->colors); diff --git a/core/widgetOps.c b/core/widgetOps.c index b7cc0b1..0841e82 100644 --- a/core/widgetOps.c +++ b/core/widgetOps.c @@ -15,6 +15,8 @@ #include "dvxPlat.h" #include "../widgets/box/box.h" +static bool sFullRepaint = false; + // ============================================================ // debugContainerBorder @@ -75,25 +77,32 @@ void widgetPaintOne(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFo return; } - // Paint this widget via vtable - wclsPaint(w, d, ops, font, colors); + // On partial repaints (fullRepaint=false), only paint dirty widgets. + // The window's fullRepaint flag is stored in sFullRepaint for the + // duration of the paint walk. + bool dirty = w->paintDirty || sFullRepaint; + w->paintDirty = false; + + if (dirty) { + wclsPaint(w, d, ops, font, colors); + } // Widgets that paint their own children return early if (w->wclass && (w->wclass->flags & WCLASS_PAINTS_CHILDREN)) { - if (sDebugLayout) { + if (sDebugLayout && dirty) { debugContainerBorder(w, d, ops); } return; } - // Paint children + // Always recurse into children — a clean parent may have dirty children for (WidgetT *c = w->firstChild; c; c = c->nextSibling) { widgetPaintOne(c, d, ops, font, colors); } // Debug: draw container borders on top of children - if (sDebugLayout && w->firstChild) { + if (sDebugLayout && dirty && w->firstChild) { debugContainerBorder(w, d, ops); } } @@ -355,7 +364,8 @@ void wgtInvalidate(WidgetT *w) { widgetManageScrollbars(w->window, ctx); } - // Dirty the window -- dvxInvalidateWindow calls onPaint automatically + // Full repaint — layout changed, all widgets need redrawing + w->window->fullRepaint = true; dvxInvalidateWindow(ctx, w->window); } @@ -373,6 +383,9 @@ void wgtInvalidatePaint(WidgetT *w) { return; } + // Mark only this widget as needing repaint + w->paintDirty = true; + WidgetT *root = w; while (root->parent) { @@ -385,7 +398,7 @@ void wgtInvalidatePaint(WidgetT *w) { return; } - // Dirty the window -- dvxInvalidateWindow calls onPaint automatically + // Partial repaint — only dirty widgets will be repainted dvxInvalidateWindow(ctx, w->window); } @@ -394,12 +407,14 @@ void wgtInvalidatePaint(WidgetT *w) { // wgtPaint // ============================================================ -void wgtPaint(WidgetT *root, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors) { +void wgtPaint(WidgetT *root, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors, bool fullRepaint) { if (!root) { return; } + sFullRepaint = fullRepaint; widgetPaintOne(root, d, ops, font, colors); + sFullRepaint = false; } diff --git a/docs/dvx_system_reference.html b/docs/dvx_system_reference.html index b1ff82e..b0c7813 100644 --- a/docs/dvx_system_reference.html +++ b/docs/dvx_system_reference.html @@ -2,7 +2,7 @@ -DVX System Overview +Welcome!