DVX_GUI/apps/dvxbasic/formrt/formrt.c

2714 lines
84 KiB
C

// formrt.c -- DVX BASIC form runtime implementation
//
// Bridges BASIC programs to the DVX widget system. All widget types
// are discovered dynamically via WgtIfaceT interface descriptors
// 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"
#include "dvxWm.h"
#include "box/box.h"
#include "dataCtrl/dataCtrl.h"
#include "dbGrid/dbGrid.h"
#include "thirdparty/stb_ds_wrap.h"
#include <ctype.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
// ============================================================
// Defines
// ============================================================
#define DEFAULT_FORM_W 400
#define DEFAULT_FORM_H 300
#define MAX_EVENT_NAME_LEN 128
#define MAX_FRM_LINE_LEN 512
// Module-level form runtime pointer for onFormClose callback
static BasFormRtT *sFormRt = NULL;
// ============================================================
// Prototypes
// ============================================================
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 WidgetT *createWidget(const char *wgtTypeName, WidgetT *parent);
static BasValueT getCommonProp(BasControlT *ctrl, const char *propName, bool *handled);
static BasValueT getIfaceProp(const WgtIfaceT *iface, WidgetT *w, const char *propName, bool *handled);
static void onFormActivate(WindowT *win);
static void onFormClose(WindowT *win);
static void onFormDeactivate(WindowT *win);
static void onFormResize(WindowT *win, int32_t newW, int32_t newH);
static void onWidgetBlur(WidgetT *w);
static bool onWidgetValidate(WidgetT *w);
static void onWidgetChange(WidgetT *w);
static void onWidgetClick(WidgetT *w);
static void onWidgetDblClick(WidgetT *w);
static void onWidgetFocus(WidgetT *w);
static void onWidgetKeyPress(WidgetT *w, int32_t keyAscii);
static void onWidgetKeyDown(WidgetT *w, int32_t keyCode, int32_t shift);
static void onWidgetKeyUp(WidgetT *w, int32_t keyCode, int32_t shift);
static void onWidgetMouseDown(WidgetT *w, int32_t button, int32_t x, int32_t y);
static void onWidgetMouseUp(WidgetT *w, int32_t button, int32_t x, int32_t y);
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 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);
static void refreshDetailControls(BasFormT *form, BasControlT *masterCtrl);
static void updateBoundControls(BasFormT *form, BasControlT *dataCtrl);
static BasValueT zeroValue(void);
// ============================================================
// basFormRtBindVm
// ============================================================
void basFormRtBindVm(BasFormRtT *rt) {
BasUiCallbacksT ui;
memset(&ui, 0, sizeof(ui));
ui.getProp = basFormRtGetProp;
ui.setProp = basFormRtSetProp;
ui.callMethod = basFormRtCallMethod;
ui.createCtrl = basFormRtCreateCtrl;
ui.findCtrl = basFormRtFindCtrl;
ui.findCtrlIdx = basFormRtFindCtrlIdx;
ui.loadForm = basFormRtLoadForm;
ui.unloadForm = basFormRtUnloadForm;
ui.showForm = basFormRtShowForm;
ui.hideForm = basFormRtHideForm;
ui.msgBox = basFormRtMsgBox;
ui.inputBox = basFormRtInputBox;
ui.ctx = rt;
basVmSetUiCallbacks(rt->vm, &ui);
}
// ============================================================
// basFormRtCallMethod
// ============================================================
BasValueT basFormRtCallMethod(void *ctx, void *ctrlRef, const char *methodName, BasValueT *args, int32_t argc) {
(void)ctx;
BasControlT *ctrl = (BasControlT *)ctrlRef;
if (!ctrl || !ctrl->widget) {
return zeroValue();
}
// Interface descriptor methods
const WgtIfaceT *iface = ctrl->iface;
if (iface) {
for (int32_t i = 0; i < iface->methodCount; i++) {
if (strcasecmp(iface->methods[i].name, methodName) != 0) {
continue;
}
const WgtMethodDescT *m = &iface->methods[i];
WidgetT *w = ctrl->widget;
switch (m->sig) {
case WGT_SIG_VOID:
((void (*)(WidgetT *))m->fn)(w);
return zeroValue();
case WGT_SIG_INT:
if (argc >= 1) {
((void (*)(WidgetT *, int32_t))m->fn)(w, (int32_t)basValToNumber(args[0]));
}
return zeroValue();
case WGT_SIG_BOOL:
if (argc >= 1) {
((void (*)(WidgetT *, bool))m->fn)(w, basValIsTruthy(args[0]));
}
return zeroValue();
case WGT_SIG_STR:
if (argc >= 1) {
BasStringT *s = basValFormatString(args[0]);
((void (*)(WidgetT *, const char *))m->fn)(w, s->data);
basStringUnref(s);
}
return zeroValue();
case WGT_SIG_INT_INT:
if (argc >= 2) {
((void (*)(WidgetT *, int32_t, int32_t))m->fn)(w, (int32_t)basValToNumber(args[0]), (int32_t)basValToNumber(args[1]));
}
return zeroValue();
case WGT_SIG_INT_BOOL:
if (argc >= 2) {
((void (*)(WidgetT *, int32_t, bool))m->fn)(w, (int32_t)basValToNumber(args[0]), basValIsTruthy(args[1]));
}
return zeroValue();
case WGT_SIG_RET_INT: {
int32_t v = ((int32_t (*)(const WidgetT *))m->fn)(w);
return basValLong(v);
}
case WGT_SIG_RET_BOOL: {
bool v = ((bool (*)(const WidgetT *))m->fn)(w);
return basValBool(v);
}
case WGT_SIG_RET_BOOL_INT: {
if (argc >= 1) {
bool v = ((bool (*)(const WidgetT *, int32_t))m->fn)(w, (int32_t)basValToNumber(args[0]));
return basValBool(v);
}
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();
}
}
}
// Common methods (SetFocus, Refresh)
return callCommonMethod(ctrl, methodName, args, argc);
}
// ============================================================
// basFormRtCreate
// ============================================================
BasFormRtT *basFormRtCreate(AppContextT *ctx, BasVmT *vm, BasModuleT *module) {
BasFormRtT *rt = (BasFormRtT *)calloc(1, sizeof(BasFormRtT));
if (!rt) {
return NULL;
}
rt->ctx = ctx;
rt->vm = vm;
rt->module = module;
sFormRt = rt;
basFormRtBindVm(rt);
return rt;
}
// ============================================================
// basFormRtCreateCtrl
// ============================================================
void *basFormRtCreateCtrl(void *ctx, void *formRef, const char *typeName, const char *ctrlName) {
(void)ctx;
BasFormT *form = (BasFormT *)formRef;
if (!form) {
return NULL;
}
// Resolve VB name to DVX widget type name
const char *wgtTypeName = resolveTypeName(typeName);
if (!wgtTypeName) {
return NULL;
}
// Create the widget
WidgetT *parent = form->contentBox ? form->contentBox : form->root;
if (!parent) {
return NULL;
}
WidgetT *widget = createWidget(wgtTypeName, parent);
if (!widget) {
return NULL;
}
wgtSetName(widget, ctrlName);
// Initialize control entry
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);
arrput(form->controls, ctrl);
form->controlCount = (int32_t)arrlen(form->controls);
// Wire up event callbacks
widget->userData = ctrl;
widget->onClick = onWidgetClick;
widget->onDblClick = onWidgetDblClick;
widget->onChange = onWidgetChange;
widget->onFocus = onWidgetFocus;
widget->onBlur = onWidgetBlur;
widget->onValidate = onWidgetValidate;
return ctrl;
}
// ============================================================
// basFormRtDestroy
// ============================================================
void basFormRtDestroy(BasFormRtT *rt) {
if (!rt) {
return;
}
if (sFormRt == rt) {
sFormRt = NULL;
}
for (int32_t i = 0; i < rt->formCount; i++) {
BasFormT *form = rt->forms[i];
if (form->formVars) {
for (int32_t j = 0; j < form->formVarCount; j++) {
basValRelease(&form->formVars[j]);
}
free(form->formVars);
}
for (int32_t j = 0; j < form->controlCount; 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);
// Free .frm source cache
for (int32_t i = 0; i < rt->frmCacheCount; i++) {
free(rt->frmCache[i].frmSource);
}
arrfree(rt->frmCache);
free(rt);
}
// ============================================================
// basFormRtFindCtrl
// ============================================================
void *basFormRtFindCtrl(void *ctx, void *formRef, const char *ctrlName) {
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];
}
}
// 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;
}
}
}
return NULL;
}
// ============================================================
// basFormRtFindCtrlIdx
// ============================================================
void *basFormRtFindCtrlIdx(void *ctx, void *formRef, const char *ctrlName, int32_t index) {
(void)ctx;
BasFormT *form = (BasFormT *)formRef;
if (!form) {
return NULL;
}
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];
}
}
return NULL;
}
// ============================================================
// basFormRtFireEvent
// ============================================================
bool basFormRtFireEvent(BasFormRtT *rt, BasFormT *form, const char *ctrlName, const char *eventName) {
return basFormRtFireEventArgs(rt, form, ctrlName, eventName, NULL, 0);
}
bool basFormRtFireEventArgs(BasFormRtT *rt, BasFormT *form, const char *ctrlName, const char *eventName, const BasValueT *args, int32_t argCount) {
if (!rt || !form || !rt->vm || !rt->module) {
return false;
}
char handlerName[MAX_EVENT_NAME_LEN];
snprintf(handlerName, sizeof(handlerName), "%s_%s", ctrlName, eventName);
const BasProcEntryT *proc = basModuleFindProc(rt->module, handlerName);
if (!proc) {
return false;
}
if (proc->isFunction) {
return false;
}
// Strict parameter matching: the sub must declare exactly the
// number of parameters the event provides, or zero (no params).
if (proc->paramCount != 0 && proc->paramCount != argCount) {
return false;
}
BasFormT *prevForm = rt->currentForm;
BasValueT *prevVars = rt->vm->currentFormVars;
int32_t prevVarCount = rt->vm->currentFormVarCount;
rt->currentForm = form;
basVmSetCurrentForm(rt->vm, form);
basVmSetCurrentFormVars(rt->vm, form->formVars, form->formVarCount);
bool ok;
if (argCount > 0 && args) {
ok = basVmCallSubWithArgs(rt->vm, proc->codeAddr, args, argCount);
} else {
ok = basVmCallSub(rt->vm, proc->codeAddr);
}
rt->currentForm = prevForm;
basVmSetCurrentForm(rt->vm, prevForm);
basVmSetCurrentFormVars(rt->vm, prevVars, prevVarCount);
return ok;
}
// ============================================================
// basFormRtFireEventWithCancel -- fire an event that has a Cancel
// parameter (first arg, Integer). Returns true if Cancel was set
// to non-zero by the event handler.
static bool basFormRtFireEventWithCancel(BasFormRtT *rt, BasFormT *form, const char *ctrlName, const char *eventName) {
if (!rt || !form || !rt->vm || !rt->module) {
return false;
}
char handlerName[MAX_EVENT_NAME_LEN];
snprintf(handlerName, sizeof(handlerName), "%s_%s", ctrlName, eventName);
const BasProcEntryT *proc = basModuleFindProc(rt->module, handlerName);
if (!proc || proc->isFunction) {
return false;
}
// Must accept 0 or 1 parameter
if (proc->paramCount != 0 && proc->paramCount != 1) {
return false;
}
BasFormT *prevForm = rt->currentForm;
BasValueT *prevVars = rt->vm->currentFormVars;
int32_t prevVarCount = rt->vm->currentFormVarCount;
rt->currentForm = form;
basVmSetCurrentForm(rt->vm, form);
basVmSetCurrentFormVars(rt->vm, form->formVars, form->formVarCount);
bool cancelled = false;
if (proc->paramCount == 1) {
BasValueT args[1];
args[0] = basValLong(0); // Cancel = 0 (don't cancel)
BasValueT outArgs[1];
memset(outArgs, 0, sizeof(outArgs));
if (basVmCallSubWithArgsOut(rt->vm, proc->codeAddr, args, 1, outArgs, 1)) {
cancelled = (basValToNumber(outArgs[0]) != 0);
basValRelease(&outArgs[0]);
}
} else {
basVmCallSub(rt->vm, proc->codeAddr);
}
rt->currentForm = prevForm;
basVmSetCurrentForm(rt->vm, prevForm);
basVmSetCurrentFormVars(rt->vm, prevVars, prevVarCount);
return cancelled;
}
// ============================================================
// basFormRtGetProp
// ============================================================
BasValueT basFormRtGetProp(void *ctx, void *ctrlRef, const char *propName) {
(void)ctx;
BasControlT *ctrl = (BasControlT *)ctrlRef;
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();
}
// Common properties (Name, Left, Top, Width, Height, Visible, Enabled)
bool handled;
BasValueT val = getCommonProp(ctrl, propName, &handled);
if (handled) {
return val;
}
// "Caption" and "Text" map to wgtGetText for all widgets
if (strcasecmp(propName, "Caption") == 0 || strcasecmp(propName, "Text") == 0) {
const char *text = wgtGetText(ctrl->widget);
return basValStringFromC(text ? text : "");
}
// Help topic
if (strcasecmp(propName, "HelpTopic") == 0) {
return basValStringFromC(ctrl->helpTopic);
}
// Data binding properties
if (strcasecmp(propName, "DataSource") == 0) {
return basValStringFromC(ctrl->dataSource);
}
if (strcasecmp(propName, "DataField") == 0) {
return basValStringFromC(ctrl->dataField);
}
// "ListCount" for any widget with item storage
if (strcasecmp(propName, "ListCount") == 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
if (ctrl->iface) {
val = getIfaceProp(ctrl->iface, ctrl->widget, propName, &handled);
if (handled) {
return val;
}
}
return zeroValue();
}
// ============================================================
// basFormRtHideForm
// ============================================================
void basFormRtHideForm(void *ctx, void *formRef) {
BasFormRtT *rt = (BasFormRtT *)ctx;
BasFormT *form = (BasFormT *)formRef;
if (!form || !form->window) {
return;
}
dvxHideWindow(rt->ctx, form->window);
}
// ============================================================
// basFormRtInputBox
// ============================================================
static BasStringT *basFormRtInputBox(void *ctx, const char *prompt, const char *title, const char *defaultText) {
BasFormRtT *rt = (BasFormRtT *)ctx;
char buf[256];
buf[0] = '\0';
if (defaultText && defaultText[0]) {
strncpy(buf, defaultText, sizeof(buf) - 1);
buf[sizeof(buf) - 1] = '\0';
}
if (!dvxInputBox(rt->ctx, title, prompt, defaultText, buf, sizeof(buf))) {
return NULL;
}
return basStringNew(buf, (int32_t)strlen(buf));
}
// ============================================================
// basFormRtLoadForm
// ============================================================
void *basFormRtLoadForm(void *ctx, const char *formName) {
BasFormRtT *rt = (BasFormRtT *)ctx;
// 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];
}
}
// Check the .frm cache for reload after unload
for (int32_t i = 0; i < rt->frmCacheCount; i++) {
if (strcasecmp(rt->frmCache[i].formName, formName) == 0) {
// Re-parse the cached .frm source (creates window, controls, fires Load)
return basFormRtLoadFrm(rt, rt->frmCache[i].frmSource, rt->frmCache[i].frmSourceLen);
}
}
// No cache entry — create a bare form (first-time load without .frm file)
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;
}
BasFormT *form = (BasFormT *)calloc(1, sizeof(BasFormT));
arrput(rt->forms, form);
rt->formCount = (int32_t)arrlen(rt->forms);
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 = 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++) {
if (strcasecmp(rt->module->formVarInfo[j].formName, formName) == 0) {
int32_t vc = rt->module->formVarInfo[j].varCount;
if (vc > 0) {
form->formVars = (BasValueT *)calloc(vc, sizeof(BasValueT));
form->formVarCount = vc;
}
// Execute per-form init block (DIM arrays, UDT init)
int32_t initAddr = rt->module->formVarInfo[j].initCodeAddr;
if (initAddr >= 0 && rt->vm) {
basVmSetCurrentForm(rt->vm, form);
basVmSetCurrentFormVars(rt->vm, form->formVars, form->formVarCount);
basVmCallSub(rt->vm, initAddr);
basVmSetCurrentForm(rt->vm, NULL);
basVmSetCurrentFormVars(rt->vm, NULL, 0);
}
break;
}
}
}
return form;
}
// ============================================================
// basFormRtLoadFrm
// ============================================================
BasFormT *basFormRtLoadFrm(BasFormRtT *rt, const char *source, int32_t sourceLen) {
if (!rt || !source || sourceLen <= 0) {
return NULL;
}
BasFormT *form = NULL;
BasControlT *current = NULL;
WidgetT *parentStack[DSGN_MAX_FRM_NESTING];
int32_t nestDepth = 0;
// Track Begin/End blocks: true = container (Form/Frame), false = control
bool isContainer[DSGN_MAX_FRM_NESTING];
int32_t blockDepth = 0;
// Temporary menu item accumulation
typedef struct {
char caption[256];
char name[BAS_MAX_CTRL_NAME];
int32_t level;
bool checked;
bool radioCheck;
bool enabled;
} TempMenuItemT;
TempMenuItemT *menuItems = NULL; // stb_ds array
TempMenuItemT *curMenuItem = NULL;
int32_t menuNestDepth = 0;
bool inMenu = false;
const char *pos = source;
const char *end = source + sourceLen;
while (pos < end) {
const char *lineStart = pos;
while (pos < end && *pos != '\n' && *pos != '\r') {
pos++;
}
int32_t lineLen = (int32_t)(pos - lineStart);
if (pos < end && *pos == '\r') {
pos++;
}
if (pos < end && *pos == '\n') {
pos++;
}
char line[MAX_FRM_LINE_LEN];
if (lineLen >= MAX_FRM_LINE_LEN) {
lineLen = MAX_FRM_LINE_LEN - 1;
}
memcpy(line, lineStart, lineLen);
line[lineLen] = '\0';
char *trimmed = line;
while (*trimmed == ' ' || *trimmed == '\t') {
trimmed++;
}
if (*trimmed == '\0' || *trimmed == '\'') {
continue;
}
// "VERSION DVX x.xx" (native) or "VERSION x.xx" (VB import)
if (strncasecmp(trimmed, "VERSION ", 8) == 0) {
const char *ver = trimmed + 8;
if (strncasecmp(ver, "DVX ", 4) != 0) {
double vbVer = atof(ver);
if (vbVer > 2.0) {
return NULL; // VB4+ form, not compatible
}
}
continue;
}
// "Begin TypeName CtrlName"
if (strncasecmp(trimmed, "Begin ", 6) == 0) {
char *rest = trimmed + 6;
char typeName[BAS_MAX_CTRL_NAME];
char ctrlName[BAS_MAX_CTRL_NAME];
int32_t ti = 0;
while (*rest && *rest != ' ' && *rest != '\t' && ti < BAS_MAX_CTRL_NAME - 1) {
typeName[ti++] = *rest++;
}
typeName[ti] = '\0';
while (*rest == ' ' || *rest == '\t') {
rest++;
}
int32_t ci = 0;
while (*rest && *rest != ' ' && *rest != '\t' && *rest != '\r' && *rest != '\n' && ci < BAS_MAX_CTRL_NAME - 1) {
ctrlName[ci++] = *rest++;
}
ctrlName[ci] = '\0';
if (strcasecmp(typeName, "Form") == 0) {
form = (BasFormT *)basFormRtLoadForm(rt, ctrlName);
if (!form) {
return NULL;
}
// 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 < DSGN_MAX_FRM_NESTING) {
isContainer[blockDepth++] = true;
}
} else if (strcasecmp(typeName, "Menu") == 0 && form) {
TempMenuItemT mi;
memset(&mi, 0, sizeof(mi));
snprintf(mi.name, BAS_MAX_CTRL_NAME, "%s", ctrlName);
mi.level = menuNestDepth;
mi.enabled = true;
arrput(menuItems, mi);
curMenuItem = &menuItems[arrlen(menuItems) - 1];
current = NULL;
menuNestDepth++;
inMenu = true;
if (blockDepth < DSGN_MAX_FRM_NESTING) {
isContainer[blockDepth++] = false;
}
continue;
} else if (form && nestDepth > 0) {
// Create the content box on first control if not yet done
if (!form->contentBox && form->root) {
form->contentBox = dsgnCreateContentBox(form->root, form->frmLayout);
parentStack[0] = form->contentBox;
}
WidgetT *parent = parentStack[nestDepth - 1];
const char *wgtTypeName = resolveTypeName(typeName);
if (!wgtTypeName) {
continue;
}
WidgetT *widget = createWidget(wgtTypeName, parent);
if (!widget) {
continue;
}
wgtSetName(widget, ctrlName);
{
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);
current = ctrlEntry;
widget->userData = current;
widget->onClick = onWidgetClick;
widget->onDblClick = onWidgetDblClick;
widget->onChange = onWidgetChange;
widget->onFocus = onWidgetFocus;
widget->onBlur = onWidgetBlur;
widget->onValidate = onWidgetValidate;
widget->onKeyPress = onWidgetKeyPress;
widget->onKeyDown = onWidgetKeyDown;
widget->onKeyUp = onWidgetKeyUp;
widget->onMouseDown = onWidgetMouseDown;
widget->onMouseUp = onWidgetMouseUp;
widget->onMouseMove = onWidgetMouseMove;
widget->onScroll = onWidgetScroll;
}
// Track block type for End handling
const WgtIfaceT *ctrlIface = wgtGetIface(wgtTypeName);
bool isCtrlContainer = ctrlIface && ctrlIface->isContainer;
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 < DSGN_MAX_FRM_NESTING) {
isContainer[blockDepth++] = true;
}
} else {
if (blockDepth < DSGN_MAX_FRM_NESTING) {
isContainer[blockDepth++] = false;
}
}
}
continue;
}
// "End"
if (strcasecmp(trimmed, "End") == 0) {
if (inMenu) {
menuNestDepth--;
curMenuItem = NULL;
if (menuNestDepth <= 0) {
menuNestDepth = 0;
inMenu = false;
}
if (blockDepth > 0) {
blockDepth--;
}
continue;
}
if (blockDepth > 0) {
blockDepth--;
// Only decrement parent nesting for containers (Form/Frame)
if (isContainer[blockDepth] && nestDepth > 0) {
nestDepth--;
}
}
current = NULL;
continue;
}
// Property assignment: Key = Value
char key[BAS_MAX_CTRL_NAME];
char value[MAX_FRM_LINE_LEN];
parseFrmLine(trimmed, key, value);
if (key[0] == '\0' || !form) {
continue;
}
if (curMenuItem) {
// Menu item properties
char *text = value;
if (text[0] == '"') {
text++;
int32_t len = (int32_t)strlen(text);
if (len > 0 && text[len - 1] == '"') {
text[len - 1] = '\0';
}
}
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, "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
if (strcasecmp(key, "Index") == 0) {
current->index = atoi(value);
continue;
}
// HelpTopic is stored on BasControlT, not on the widget
if (strcasecmp(key, "HelpTopic") == 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'; }
snprintf(current->helpTopic, sizeof(current->helpTopic), "%s", text);
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] == '"') {
int32_t vlen = (int32_t)strlen(value);
if (vlen >= 2 && value[vlen - 1] == '"') {
value[vlen - 1] = '\0';
}
val = basValStringFromC(value + 1);
} else {
val = basValLong(atoi(value));
}
basFormRtSetProp(rt, current, key, val);
basValRelease(&val);
} else if (nestDepth > 0) {
// Form-level property -- strip quotes from string values
char *text = value;
if (text[0] == '"') {
text++;
int32_t len = (int32_t)strlen(text);
if (len > 0 && text[len - 1] == '"') {
text[len - 1] = '\0';
}
}
if (strcasecmp(key, "Caption") == 0) {
dvxSetTitle(rt->ctx, form->window, text);
} else if (strcasecmp(key, "Width") == 0) {
form->frmWidth = atoi(value);
} else if (strcasecmp(key, "Height") == 0) {
form->frmHeight = atoi(value);
} else if (strcasecmp(key, "Left") == 0) {
form->frmLeft = atoi(value);
} else if (strcasecmp(key, "Top") == 0) {
form->frmTop = atoi(value);
} else if (strcasecmp(key, "Resizable") == 0) {
form->frmResizable = (strcasecmp(text, "True") == 0);
form->frmHasResizable = true;
} else if (strcasecmp(key, "Centered") == 0) {
form->frmCentered = (strcasecmp(text, "True") == 0);
} else if (strcasecmp(key, "AutoSize") == 0) {
form->frmAutoSize = (strcasecmp(text, "True") == 0);
} else if (strcasecmp(key, "Layout") == 0) {
snprintf(form->frmLayout, sizeof(form->frmLayout), "%s", text);
} else if (strcasecmp(key, "HelpTopic") == 0) {
snprintf(form->helpTopic, sizeof(form->helpTopic), "%s", text);
}
}
}
// Apply accumulated form-level properties
if (form) {
// Ensure content box exists even if form has no controls
if (!form->contentBox && form->root) {
form->contentBox = dsgnCreateContentBox(form->root, form->frmLayout);
}
// Build menu bar from accumulated menu items
int32_t menuCount = (int32_t)arrlen(menuItems);
if (menuCount > 0 && form->window) {
MenuBarT *bar = wmAddMenuBar(form->window);
if (bar) {
#define MENU_ID_BASE 10000
MenuT *menuStack[16];
memset(menuStack, 0, sizeof(menuStack));
for (int32_t i = 0; i < menuCount; i++) {
TempMenuItemT *mi = &menuItems[i];
bool isSep = (mi->caption[0] == '-' && (mi->caption[1] == '\0' || mi->caption[1] == '-'));
bool isSubParent = (i + 1 < menuCount && menuItems[i + 1].level > mi->level);
if (mi->level == 0) {
// Top-level menu header
menuStack[0] = wmAddMenu(bar, mi->caption);
} else if (isSep && mi->level > 0 && menuStack[mi->level - 1]) {
// Separator
wmAddMenuSeparator(menuStack[mi->level - 1]);
} else if (isSubParent && mi->level > 0 && menuStack[mi->level - 1]) {
// Submenu parent
menuStack[mi->level] = wmAddSubMenu(menuStack[mi->level - 1], mi->caption);
} else if (mi->level > 0 && menuStack[mi->level - 1]) {
// Regular menu item
int32_t id = 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);
}
if (!mi->enabled) {
wmMenuItemSetEnabled(bar, id, false);
}
// Store ID-to-name mapping for event dispatch
BasMenuIdMapT map;
memset(&map, 0, sizeof(map));
map.id = id;
snprintf(map.name, BAS_MAX_CTRL_NAME, "%s", mi->name);
arrput(form->menuIdMap, map);
form->menuIdMapCount = (int32_t)arrlen(form->menuIdMap);
}
}
form->window->onMenu = onFormMenu;
}
}
arrfree(menuItems);
menuItems = NULL;
// 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;
}
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);
}
if (form->frmCentered) {
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;
}
}
// Cache the .frm source for reload after unload
if (form) {
bool cached = false;
for (int32_t i = 0; i < rt->frmCacheCount; i++) {
if (strcasecmp(rt->frmCache[i].formName, form->name) == 0) {
cached = true;
break;
}
}
if (!cached) {
BasFrmCacheT entry;
memset(&entry, 0, sizeof(entry));
snprintf(entry.formName, BAS_MAX_CTRL_NAME, "%s", form->name);
entry.frmSource = (char *)malloc(sourceLen + 1);
entry.frmSourceLen = sourceLen;
if (entry.frmSource) {
memcpy(entry.frmSource, source, sourceLen);
entry.frmSource[sourceLen] = '\0';
}
arrput(rt->frmCache, entry);
rt->frmCacheCount = (int32_t)arrlen(rt->frmCache);
}
}
// Fire the Load event now that the form and controls are ready
if (form) {
basFormRtFireEvent(rt, form, form->name, "Load");
// Auto-refresh Data controls that have no MasterSource (masters/standalone).
// 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];
if (strcasecmp(dc->typeName, "Data") != 0 || !dc->widget) {
continue;
}
// Skip details — they'll be refreshed by the cascade
const char *ms = NULL;
if (dc->iface) {
for (int32_t p = 0; p < dc->iface->propCount; p++) {
if (strcasecmp(dc->iface->props[p].name, "MasterSource") == 0 && dc->iface->props[p].getFn) {
ms = ((const char *(*)(const WidgetT *))dc->iface->props[p].getFn)(dc->widget);
break;
}
}
}
if (ms && ms[0]) {
continue;
}
wgtDataCtrlRefresh(dc->widget);
updateBoundControls(form, dc);
refreshDetailControls(form, dc);
}
}
return form;
}
// ============================================================
// basFormRtMsgBox
// ============================================================
int32_t basFormRtMsgBox(void *ctx, const char *message, int32_t flags) {
BasFormRtT *rt = (BasFormRtT *)ctx;
return dvxMessageBox(rt->ctx, "DVX BASIC", message, flags);
}
// ============================================================
// basFormRtSetProp
// ============================================================
void basFormRtSetProp(void *ctx, void *ctrlRef, const char *propName, BasValueT value) {
BasFormRtT *rt = (BasFormRtT *)ctx;
BasControlT *ctrl = (BasControlT *)ctrlRef;
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;
}
// Common properties
if (setCommonProp(ctrl, propName, value)) {
return;
}
// "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);
basStringUnref(s);
wgtSetText(ctrl->widget, ctrl->textBuf);
return;
}
// Help topic
if (strcasecmp(propName, "HelpTopic") == 0) {
BasStringT *s = basValFormatString(value);
snprintf(ctrl->helpTopic, BAS_MAX_CTRL_NAME, "%s", s->data);
basStringUnref(s);
return;
}
// Data binding properties (stored on BasControlT, not on the widget)
if (strcasecmp(propName, "DataSource") == 0) {
BasStringT *s = basValFormatString(value);
snprintf(ctrl->dataSource, BAS_MAX_CTRL_NAME, "%s", s->data);
basStringUnref(s);
return;
}
if (strcasecmp(propName, "DataField") == 0) {
BasStringT *s = basValFormatString(value);
snprintf(ctrl->dataField, BAS_MAX_CTRL_NAME, "%s", s->data);
basStringUnref(s);
return;
}
// Interface descriptor properties
if (ctrl->iface) {
if (setIfaceProp(ctrl->iface, ctrl->widget, propName, value)) {
return;
}
}
}
// ============================================================
// basFormRtShowForm
// ============================================================
void basFormRtShowForm(void *ctx, void *formRef, bool modal) {
BasFormRtT *rt = (BasFormRtT *)ctx;
BasFormT *form = (BasFormT *)formRef;
if (!form || !form->window) {
return;
}
dvxShowWindow(rt->ctx, form->window);
dvxRaiseWindow(rt->ctx, form->window);
if (form->frmAutoSize) {
dvxFitWindow(rt->ctx, form->window);
}
if (modal) {
rt->ctx->modalWindow = form->window;
}
}
// ============================================================
// basFormRtUnloadForm
// ============================================================
void basFormRtUnloadForm(void *ctx, void *formRef) {
BasFormRtT *rt = (BasFormRtT *)ctx;
BasFormT *form = (BasFormT *)formRef;
if (!form) {
return;
}
basFormRtFireEvent(rt, form, form->name, "Unload");
// Release per-form variables
if (form->formVars) {
for (int32_t i = 0; i < form->formVarCount; i++) {
basValRelease(&form->formVars[i]);
}
free(form->formVars);
form->formVars = NULL;
form->formVarCount = 0;
}
for (int32_t i = 0; i < form->controlCount; 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;
}
dvxDestroyWindow(rt->ctx, form->window);
form->window = NULL;
form->root = NULL;
form->contentBox = NULL;
}
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];
}
rt->forms[rt->formCount] = NULL;
}
}
// ============================================================
// callCommonMethod
// ============================================================
static BasValueT callCommonMethod(BasControlT *ctrl, const char *methodName, BasValueT *args, int32_t argc) {
(void)args;
(void)argc;
if (strcasecmp(methodName, "SetFocus") == 0) {
wgtSetFocused(ctrl->widget);
return zeroValue();
}
if (strcasecmp(methodName, "Refresh") == 0) {
wgtInvalidatePaint(ctrl->widget);
return zeroValue();
}
return zeroValue();
}
// ============================================================
// createWidget
// ============================================================
//
// 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);
if (!api) {
return NULL;
}
// Determine creation signature from the widget interface descriptor.
const WgtIfaceT *iface = wgtGetIface(wgtTypeName);
uint8_t sig = iface ? iface->createSig : WGT_CREATE_PARENT;
typedef WidgetT *(*CreateParentFnT)(WidgetT *);
typedef WidgetT *(*CreateParentTextFnT)(WidgetT *, const char *);
typedef WidgetT *(*CreateParentIntFnT)(WidgetT *, int32_t);
typedef WidgetT *(*CreateParentIntIntFnT)(WidgetT *, int32_t, int32_t);
typedef WidgetT *(*CreateParentIntIntIntFnT)(WidgetT *, int32_t, int32_t, int32_t);
typedef WidgetT *(*CreateParentIntBoolFnT)(WidgetT *, int32_t, bool);
typedef WidgetT *(*CreateParentBoolFnT)(WidgetT *, bool);
switch (sig) {
case WGT_CREATE_PARENT_TEXT: {
CreateParentTextFnT fn = *(CreateParentTextFnT *)api;
return fn(parent, "");
}
case WGT_CREATE_PARENT_INT: {
CreateParentIntFnT fn = *(CreateParentIntFnT *)api;
return fn(parent, iface->createArgs[0]);
}
case WGT_CREATE_PARENT_INT_INT: {
CreateParentIntIntFnT fn = *(CreateParentIntIntFnT *)api;
return fn(parent, iface->createArgs[0], iface->createArgs[1]);
}
case WGT_CREATE_PARENT_INT_INT_INT: {
CreateParentIntIntIntFnT fn = *(CreateParentIntIntIntFnT *)api;
return fn(parent, iface->createArgs[0], iface->createArgs[1], iface->createArgs[2]);
}
case WGT_CREATE_PARENT_INT_BOOL: {
CreateParentIntBoolFnT fn = *(CreateParentIntBoolFnT *)api;
return fn(parent, iface->createArgs[0], (bool)iface->createArgs[1]);
}
case WGT_CREATE_PARENT_BOOL: {
CreateParentBoolFnT fn = *(CreateParentBoolFnT *)api;
return fn(parent, (bool)iface->createArgs[0]);
}
case WGT_CREATE_PARENT_DATA:
return NULL;
default: {
CreateParentFnT fn = *(CreateParentFnT *)api;
return fn(parent);
}
}
}
// ============================================================
// getCommonProp
// ============================================================
static BasValueT getCommonProp(BasControlT *ctrl, const char *propName, bool *handled) {
*handled = true;
if (strcasecmp(propName, "Name") == 0) {
return basValStringFromC(ctrl->name);
}
if (strcasecmp(propName, "Left") == 0) {
return basValLong(ctrl->widget->x);
}
if (strcasecmp(propName, "Top") == 0) {
return basValLong(ctrl->widget->y);
}
if (strcasecmp(propName, "Width") == 0 || strcasecmp(propName, "MinWidth") == 0) {
return basValLong(ctrl->widget->w);
}
if (strcasecmp(propName, "Height") == 0 || strcasecmp(propName, "MinHeight") == 0) {
return basValLong(ctrl->widget->h);
}
if (strcasecmp(propName, "MaxWidth") == 0) {
return basValLong(ctrl->widget->maxW ? (ctrl->widget->maxW & WGT_SIZE_VAL_MASK) : 0);
}
if (strcasecmp(propName, "MaxHeight") == 0) {
return basValLong(ctrl->widget->maxH ? (ctrl->widget->maxH & WGT_SIZE_VAL_MASK) : 0);
}
if (strcasecmp(propName, "Weight") == 0) {
return basValLong(ctrl->widget->weight);
}
if (strcasecmp(propName, "Visible") == 0) {
return basValBool(ctrl->widget->visible);
}
if (strcasecmp(propName, "Enabled") == 0) {
return basValBool(ctrl->widget->enabled);
}
if (strcasecmp(propName, "TabIndex") == 0) {
return basValLong(0);
}
if (strcasecmp(propName, "BackColor") == 0) {
return basValLong((int32_t)ctrl->widget->bgColor);
}
if (strcasecmp(propName, "ForeColor") == 0) {
return basValLong((int32_t)ctrl->widget->fgColor);
}
*handled = false;
return zeroValue();
}
// ============================================================
// getIfaceProp
// ============================================================
//
// Look up a property in the interface descriptor and call its
// getter, marshaling the return value to BasValueT.
static BasValueT getIfaceProp(const WgtIfaceT *iface, WidgetT *w, const char *propName, bool *handled) {
*handled = false;
for (int32_t i = 0; i < iface->propCount; i++) {
if (strcasecmp(iface->props[i].name, propName) != 0) {
continue;
}
*handled = true;
const WgtPropDescT *p = &iface->props[i];
if (!p->getFn) {
return zeroValue();
}
switch (p->type) {
case WGT_IFACE_STRING: {
const char *s = ((const char *(*)(WidgetT *))p->getFn)(w);
return basValStringFromC(s ? s : "");
}
case WGT_IFACE_INT: {
int32_t v = ((int32_t (*)(const WidgetT *))p->getFn)(w);
return basValLong(v);
}
case WGT_IFACE_BOOL: {
bool v = ((bool (*)(const WidgetT *))p->getFn)(w);
return basValBool(v);
}
case WGT_IFACE_FLOAT: {
float v = ((float (*)(const WidgetT *))p->getFn)(w);
return basValSingle(v);
}
}
return zeroValue();
}
return zeroValue();
}
// ============================================================
// onFormClose
// ============================================================
// onFormMenu -- dispatch menu clicks as ControlName_Click events
// ============================================================
static void onFormMenu(WindowT *win, int32_t menuId) {
if (!sFormRt) {
return;
}
for (int32_t i = 0; i < sFormRt->formCount; i++) {
BasFormT *form = sFormRt->forms[i];
if (form->window == win) {
for (int32_t j = 0; j < form->menuIdMapCount; j++) {
if (form->menuIdMap[j].id == menuId) {
basFormRtFireEvent(sFormRt, form, form->menuIdMap[j].name, "Click");
return;
}
}
return;
}
}
}
// ============================================================
// onFormClose
// ============================================================
//
// Called when the user closes a BASIC form's window. Fires the
// Form_Unload event and stops the VM so the program exits.
static void onFormClose(WindowT *win) {
if (!sFormRt) {
return;
}
for (int32_t i = 0; i < sFormRt->formCount; i++) {
BasFormT *form = sFormRt->forms[i];
if (form->window == win) {
// QueryUnload: if Cancel is set, abort the close
if (basFormRtFireEventWithCancel(sFormRt, form, form->name, "QueryUnload")) {
return;
}
basFormRtFireEvent(sFormRt, form, form->name, "Unload");
// Release per-form variables
if (form->formVars) {
for (int32_t j = 0; j < form->formVarCount; j++) {
basValRelease(&form->formVars[j]);
}
free(form->formVars);
form->formVars = NULL;
form->formVarCount = 0;
}
// Free control resources
for (int32_t j = 0; j < form->controlCount; 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);
// Remove from form list and free the form
free(form);
sFormRt->formCount--;
if (i < sFormRt->formCount) {
sFormRt->forms[i] = sFormRt->forms[sFormRt->formCount];
}
sFormRt->forms[sFormRt->formCount] = NULL;
// If no forms left, stop the VM
if (sFormRt->formCount == 0 && sFormRt->vm) {
sFormRt->vm->running = false;
}
return;
}
}
}
// ============================================================
// onFormResize
// ============================================================
static void onFormResize(WindowT *win, int32_t newW, int32_t newH) {
// Let the widget system re-evaluate scrollbars for the new size
widgetOnResize(win, newW, newH);
if (!sFormRt) {
return;
}
for (int32_t i = 0; i < sFormRt->formCount; i++) {
BasFormT *form = sFormRt->forms[i];
if (form->window == win) {
basFormRtFireEvent(sFormRt, form, form->name, "Resize");
return;
}
}
}
// ============================================================
// onFormActivate / onFormDeactivate
// ============================================================
static void onFormActivate(WindowT *win) {
if (!sFormRt) {
return;
}
for (int32_t i = 0; i < sFormRt->formCount; i++) {
BasFormT *form = sFormRt->forms[i];
if (form->window == win) {
basFormRtFireEvent(sFormRt, form, form->name, "Activate");
return;
}
}
}
static void onFormDeactivate(WindowT *win) {
if (!sFormRt) {
return;
}
for (int32_t i = 0; i < sFormRt->formCount; i++) {
BasFormT *form = sFormRt->forms[i];
if (form->window == win) {
basFormRtFireEvent(sFormRt, form, form->name, "Deactivate");
return;
}
}
}
// ============================================================
// fireCtrlEvent -- fire event, prepending Index for array members
// ============================================================
#define MAX_FIRE_ARGS 8
static void fireCtrlEvent(BasFormRtT *rt, BasControlT *ctrl, const char *eventName, const BasValueT *args, int32_t argCount) {
if (ctrl->index >= 0) {
// Control array element: prepend Index as first argument
BasValueT allArgs[MAX_FIRE_ARGS];
allArgs[0] = basValLong(ctrl->index);
for (int32_t i = 0; i < argCount && i < MAX_FIRE_ARGS - 1; i++) {
allArgs[i + 1] = args[i];
}
basFormRtFireEventArgs(rt, ctrl->form, ctrl->name, eventName, allArgs, argCount + 1);
} else if (argCount > 0 && args) {
basFormRtFireEventArgs(rt, ctrl->form, ctrl->name, eventName, args, argCount);
} else {
basFormRtFireEvent(rt, ctrl->form, ctrl->name, eventName);
}
}
// ============================================================
// Widget event callbacks
// ============================================================
static bool onWidgetValidate(WidgetT *w) {
BasControlT *ctrl = (BasControlT *)w->userData;
if (!ctrl || !ctrl->form || !ctrl->form->vm) {
return true;
}
BasFormRtT *rt = (BasFormRtT *)ctrl->form->vm->ui.ctx;
if (!rt || !rt->module) {
return true;
}
// Look for Data1_Validate(Cancel As Integer) handler
char handlerName[MAX_EVENT_NAME_LEN];
snprintf(handlerName, sizeof(handlerName), "%s_Validate", ctrl->name);
const BasProcEntryT *proc = basModuleFindProc(rt->module, handlerName);
if (!proc || proc->isFunction) {
return true;
}
// Pass Cancel = 0 (False). If the handler sets it to non-zero, cancel.
BasValueT cancelArg = basValLong(0);
BasFormT *prevForm = rt->currentForm;
BasValueT *prevVars = rt->vm->currentFormVars;
int32_t prevVarCount = rt->vm->currentFormVarCount;
rt->currentForm = ctrl->form;
basVmSetCurrentForm(rt->vm, ctrl->form);
basVmSetCurrentFormVars(rt->vm, ctrl->form->formVars, ctrl->form->formVarCount);
BasValueT outCancel = basValLong(0);
if (proc->paramCount == 1) {
basVmCallSubWithArgsOut(rt->vm, proc->codeAddr, &cancelArg, 1, &outCancel, 1);
} else {
basVmCallSub(rt->vm, proc->codeAddr);
}
rt->currentForm = prevForm;
basVmSetCurrentForm(rt->vm, prevForm);
basVmSetCurrentFormVars(rt->vm, prevVars, prevVarCount);
// Non-zero Cancel means abort the write
return !basValIsTruthy(outCancel);
}
static void onWidgetBlur(WidgetT *w) {
BasControlT *ctrl = (BasControlT *)w->userData;
if (!ctrl || !ctrl->form || !ctrl->form->vm) {
return;
}
BasFormRtT *rt = (BasFormRtT *)ctrl->form->vm->ui.ctx;
if (rt) {
fireCtrlEvent(rt, ctrl, "LostFocus", NULL, 0);
}
// Write-back: if this control is data-bound, update the Data control's
// 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];
if (strcasecmp(dc->typeName, "Data") == 0 &&
strcasecmp(dc->name, ctrl->dataSource) == 0 &&
dc->widget) {
const char *text = wgtGetText(ctrl->widget);
if (text) {
wgtDataCtrlSetField(dc->widget, ctrl->dataField, text);
wgtDataCtrlUpdate(dc->widget);
}
break;
}
}
}
}
// refreshDetailControls -- cascade master-detail: refresh detail Data controls
static void refreshDetailControls(BasFormT *form, BasControlT *masterCtrl) {
if (!masterCtrl->widget) {
return;
}
for (int32_t i = 0; i < form->controlCount; i++) {
BasControlT *ctrl = form->controls[i];
if (strcasecmp(ctrl->typeName, "Data") != 0 || !ctrl->widget || !ctrl->iface) {
continue;
}
// Read MasterSource, MasterField, DetailField from the widget's interface
const char *ms = NULL;
const char *mf = NULL;
for (int32_t p = 0; p < ctrl->iface->propCount; p++) {
const WgtPropDescT *pd = &ctrl->iface->props[p];
if (!pd->getFn) {
continue;
}
if (strcasecmp(pd->name, "MasterSource") == 0) {
ms = ((const char *(*)(const WidgetT *))pd->getFn)(ctrl->widget);
} else if (strcasecmp(pd->name, "MasterField") == 0) {
mf = ((const char *(*)(const WidgetT *))pd->getFn)(ctrl->widget);
}
}
if (!ms || !ms[0] || strcasecmp(ms, masterCtrl->name) != 0) {
continue;
}
// Get the master's current value for MasterField
const char *val = "";
if (mf && mf[0]) {
val = wgtDataCtrlGetField(masterCtrl->widget, mf);
}
// Set the filter value and refresh the detail
wgtDataCtrlSetMasterValue(ctrl->widget, val);
wgtDataCtrlRefresh(ctrl->widget);
// Update bound controls for this detail
updateBoundControls(form, ctrl);
}
}
// updateBoundControls -- sync bound controls from Data control's current record
static void updateBoundControls(BasFormT *form, BasControlT *dataCtrl) {
if (!dataCtrl->widget) {
return;
}
for (int32_t i = 0; i < form->controlCount; i++) {
BasControlT *ctrl = form->controls[i];
if (!ctrl->dataSource[0] || strcasecmp(ctrl->dataSource, dataCtrl->name) != 0) {
continue;
}
if (!ctrl->widget) {
continue;
}
// DBGrid: bind to the Data control widget and refresh
if (strcasecmp(ctrl->typeName, "DBGrid") == 0) {
wgtDbGridSetDataWidget(ctrl->widget, dataCtrl->widget);
wgtDbGridRefresh(ctrl->widget);
continue;
}
// Text-based controls: set text from current record field
if (ctrl->dataField[0]) {
const char *val = wgtDataCtrlGetField(dataCtrl->widget, ctrl->dataField);
if (val) {
snprintf(ctrl->textBuf, BAS_MAX_TEXT_BUF, "%s", val);
wgtSetText(ctrl->widget, ctrl->textBuf);
}
}
}
}
static void onWidgetChange(WidgetT *w) {
BasControlT *ctrl = (BasControlT *)w->userData;
if (!ctrl || !ctrl->form || !ctrl->form->vm) {
return;
}
BasFormRtT *rt = (BasFormRtT *)ctrl->form->vm->ui.ctx;
if (rt) {
// Data controls fire "Reposition", update bound controls, and cascade to details
if (strcasecmp(ctrl->typeName, "Data") == 0) {
updateBoundControls(ctrl->form, ctrl);
fireCtrlEvent(rt, ctrl, "Reposition", NULL, 0);
refreshDetailControls(ctrl->form, ctrl);
return;
}
// Timer widgets fire "Timer" event, everything else fires "Change"
const char *evtName = (strcasecmp(ctrl->typeName, "Timer") == 0) ? "Timer" : "Change";
fireCtrlEvent(rt, ctrl, evtName, NULL, 0);
}
}
static void onWidgetClick(WidgetT *w) {
BasControlT *ctrl = (BasControlT *)w->userData;
if (!ctrl || !ctrl->form || !ctrl->form->vm) {
return;
}
BasFormRtT *rt = (BasFormRtT *)ctrl->form->vm->ui.ctx;
if (rt) {
fireCtrlEvent(rt, ctrl, "Click", NULL, 0);
}
}
static void onWidgetDblClick(WidgetT *w) {
BasControlT *ctrl = (BasControlT *)w->userData;
if (!ctrl || !ctrl->form || !ctrl->form->vm) {
return;
}
BasFormRtT *rt = (BasFormRtT *)ctrl->form->vm->ui.ctx;
if (rt) {
fireCtrlEvent(rt, ctrl, "DblClick", NULL, 0);
}
}
static void onWidgetFocus(WidgetT *w) {
BasControlT *ctrl = (BasControlT *)w->userData;
if (!ctrl || !ctrl->form || !ctrl->form->vm) {
return;
}
BasFormRtT *rt = (BasFormRtT *)ctrl->form->vm->ui.ctx;
if (rt) {
fireCtrlEvent(rt, ctrl, "GotFocus", NULL, 0);
}
}
static void onWidgetKeyPress(WidgetT *w, int32_t keyAscii) {
BasControlT *ctrl = (BasControlT *)w->userData;
if (!ctrl || !ctrl->form || !ctrl->form->vm) {
return;
}
BasFormRtT *rt = (BasFormRtT *)ctrl->form->vm->ui.ctx;
if (rt) {
BasValueT args[1];
args[0] = basValLong(keyAscii);
fireCtrlEvent(rt, ctrl, "KeyPress", args, 1);
}
}
static void onWidgetKeyDown(WidgetT *w, int32_t keyCode, int32_t shift) {
BasControlT *ctrl = (BasControlT *)w->userData;
if (!ctrl || !ctrl->form || !ctrl->form->vm) {
return;
}
BasFormRtT *rt = (BasFormRtT *)ctrl->form->vm->ui.ctx;
if (rt) {
BasValueT args[2];
args[0] = basValLong(keyCode);
args[1] = basValLong(shift);
fireCtrlEvent(rt, ctrl, "KeyDown", args, 2);
}
}
static void onWidgetKeyUp(WidgetT *w, int32_t keyCode, int32_t shift) {
BasControlT *ctrl = (BasControlT *)w->userData;
if (!ctrl || !ctrl->form || !ctrl->form->vm) {
return;
}
BasFormRtT *rt = (BasFormRtT *)ctrl->form->vm->ui.ctx;
if (rt) {
BasValueT args[2];
args[0] = basValLong(keyCode);
args[1] = basValLong(shift);
fireCtrlEvent(rt, ctrl, "KeyUp", args, 2);
}
}
static void onWidgetMouseDown(WidgetT *w, int32_t button, int32_t x, int32_t y) {
BasControlT *ctrl = (BasControlT *)w->userData;
if (!ctrl || !ctrl->form || !ctrl->form->vm) {
return;
}
BasFormRtT *rt = (BasFormRtT *)ctrl->form->vm->ui.ctx;
if (rt) {
BasValueT args[3];
args[0] = basValLong(button);
args[1] = basValLong(x);
args[2] = basValLong(y);
fireCtrlEvent(rt, ctrl, "MouseDown", args, 3);
}
}
static void onWidgetMouseUp(WidgetT *w, int32_t button, int32_t x, int32_t y) {
BasControlT *ctrl = (BasControlT *)w->userData;
if (!ctrl || !ctrl->form || !ctrl->form->vm) {
return;
}
BasFormRtT *rt = (BasFormRtT *)ctrl->form->vm->ui.ctx;
if (rt) {
BasValueT args[3];
args[0] = basValLong(button);
args[1] = basValLong(x);
args[2] = basValLong(y);
fireCtrlEvent(rt, ctrl, "MouseUp", args, 3);
}
}
static void onWidgetMouseMove(WidgetT *w, int32_t button, int32_t x, int32_t y) {
BasControlT *ctrl = (BasControlT *)w->userData;
if (!ctrl || !ctrl->form || !ctrl->form->vm) {
return;
}
BasFormRtT *rt = (BasFormRtT *)ctrl->form->vm->ui.ctx;
if (rt) {
BasValueT args[3];
args[0] = basValLong(button);
args[1] = basValLong(x);
args[2] = basValLong(y);
fireCtrlEvent(rt, ctrl, "MouseMove", args, 3);
}
}
static void onWidgetScroll(WidgetT *w, int32_t delta) {
BasControlT *ctrl = (BasControlT *)w->userData;
if (!ctrl || !ctrl->form || !ctrl->form->vm) {
return;
}
BasFormRtT *rt = (BasFormRtT *)ctrl->form->vm->ui.ctx;
if (rt) {
BasValueT args[1];
args[0] = basValLong(delta);
fireCtrlEvent(rt, ctrl, "Scroll", args, 1);
}
}
// ============================================================
// parseFrmLine
// ============================================================
static void parseFrmLine(const char *line, char *key, char *value) {
key[0] = '\0';
value[0] = '\0';
while (*line == ' ' || *line == '\t') {
line++;
}
int32_t ki = 0;
while (*line && *line != '=' && *line != ' ' && *line != '\t' && ki < BAS_MAX_CTRL_NAME - 1) {
key[ki++] = *line++;
}
key[ki] = '\0';
while (*line == ' ' || *line == '\t') {
line++;
}
if (*line == '=') {
line++;
}
while (*line == ' ' || *line == '\t') {
line++;
}
int32_t vi = 0;
while (*line && *line != '\r' && *line != '\n' && vi < MAX_FRM_LINE_LEN - 1) {
value[vi++] = *line++;
}
value[vi] = '\0';
while (vi > 0 && (value[vi - 1] == ' ' || value[vi - 1] == '\t')) {
value[--vi] = '\0';
}
}
// ============================================================
// resolveTypeName
// ============================================================
//
// Resolve a type name (VB-style or DVX widget name) to a DVX
// widget type name. First tries wgtFindByBasName for VB names
// like "CommandButton", then falls back to direct widget name.
static const char *resolveTypeName(const char *typeName) {
// Try VB name first (e.g. "CommandButton" -> "button")
const char *wgtName = wgtFindByBasName(typeName);
if (wgtName) {
return wgtName;
}
// Try as direct widget type name (e.g. "button", "slider")
if (wgtGetApi(typeName)) {
return typeName;
}
return NULL;
}
// ============================================================
// setCommonProp
// ============================================================
static bool setCommonProp(BasControlT *ctrl, const char *propName, BasValueT value) {
if (strcasecmp(propName, "Left") == 0) {
ctrl->widget->x = (int32_t)basValToNumber(value);
wgtInvalidate(ctrl->widget);
return true;
}
if (strcasecmp(propName, "Top") == 0) {
ctrl->widget->y = (int32_t)basValToNumber(value);
wgtInvalidate(ctrl->widget);
return true;
}
if (strcasecmp(propName, "Width") == 0 || strcasecmp(propName, "MinWidth") == 0) {
int32_t w = (int32_t)basValToNumber(value);
ctrl->widget->minW = wgtPixels(w);
wgtInvalidate(ctrl->widget);
return true;
}
if (strcasecmp(propName, "Height") == 0 || strcasecmp(propName, "MinHeight") == 0) {
int32_t h = (int32_t)basValToNumber(value);
ctrl->widget->minH = wgtPixels(h);
wgtInvalidate(ctrl->widget);
return true;
}
if (strcasecmp(propName, "MaxWidth") == 0) {
int32_t mw = (int32_t)basValToNumber(value);
ctrl->widget->maxW = mw > 0 ? wgtPixels(mw) : 0;
wgtInvalidate(ctrl->widget);
return true;
}
if (strcasecmp(propName, "MaxHeight") == 0) {
int32_t mh = (int32_t)basValToNumber(value);
ctrl->widget->maxH = mh > 0 ? wgtPixels(mh) : 0;
wgtInvalidate(ctrl->widget);
return true;
}
if (strcasecmp(propName, "Weight") == 0) {
ctrl->widget->weight = (int32_t)basValToNumber(value);
wgtInvalidate(ctrl->widget);
return true;
}
if (strcasecmp(propName, "Visible") == 0) {
wgtSetVisible(ctrl->widget, basValIsTruthy(value));
return true;
}
if (strcasecmp(propName, "Enabled") == 0) {
wgtSetEnabled(ctrl->widget, basValIsTruthy(value));
return true;
}
if (strcasecmp(propName, "TabIndex") == 0) {
return true;
}
if (strcasecmp(propName, "BackColor") == 0) {
ctrl->widget->bgColor = (uint32_t)(int32_t)basValToNumber(value);
wgtInvalidatePaint(ctrl->widget);
return true;
}
if (strcasecmp(propName, "ForeColor") == 0) {
ctrl->widget->fgColor = (uint32_t)(int32_t)basValToNumber(value);
wgtInvalidatePaint(ctrl->widget);
return true;
}
return false;
}
// ============================================================
// setIfaceProp
// ============================================================
//
// Look up a property in the interface descriptor and call its
// setter, marshaling the BasValueT to the native type.
static bool setIfaceProp(const WgtIfaceT *iface, WidgetT *w, const char *propName, BasValueT value) {
for (int32_t i = 0; i < iface->propCount; i++) {
if (strcasecmp(iface->props[i].name, propName) != 0) {
continue;
}
const WgtPropDescT *p = &iface->props[i];
if (!p->setFn) {
return true; // read-only, but recognized
}
switch (p->type) {
case WGT_IFACE_STRING: {
BasStringT *s = basValFormatString(value);
((void (*)(WidgetT *, const char *))p->setFn)(w, s->data);
basStringUnref(s);
break;
}
case WGT_IFACE_ENUM:
if (p->enumNames && value.type == BAS_TYPE_STRING && value.strVal) {
// Map name to index
int32_t enumVal = 0;
for (int32_t en = 0; p->enumNames[en]; en++) {
if (strcasecmp(p->enumNames[en], value.strVal->data) == 0) {
enumVal = en;
break;
}
}
((void (*)(WidgetT *, int32_t))p->setFn)(w, enumVal);
} else {
((void (*)(WidgetT *, int32_t))p->setFn)(w, (int32_t)basValToNumber(value));
}
break;
case WGT_IFACE_INT:
((void (*)(WidgetT *, int32_t))p->setFn)(w, (int32_t)basValToNumber(value));
break;
case WGT_IFACE_BOOL:
((void (*)(WidgetT *, bool))p->setFn)(w, basValIsTruthy(value));
break;
case WGT_IFACE_FLOAT:
((void (*)(WidgetT *, float))p->setFn)(w, (float)basValToNumber(value));
break;
}
return true;
}
return false;
}
// ============================================================
// zeroValue
// ============================================================
static BasValueT zeroValue(void) {
BasValueT v;
memset(&v, 0, sizeof(v));
return v;
}
// ============================================================
// Common dialog wrappers for DECLARE LIBRARY "commdlg"
// ============================================================
//
// Thin wrappers around dvx dialog functions. They retrieve the
// AppContextT from the running form runtime internally, so BASIC
// programs don't need to manage context pointers. Wrappers are
// needed because: (1) dvx functions require AppContextT* as first
// arg, and (2) functions with output buffers (file paths, text)
// need C-side storage since BASIC strings can't be used as
// writable char pointers.
const char *basFileOpen(const char *title, const char *filter) {
if (!sFormRt) {
return "";
}
FileFilterT filters[2];
filters[0].label = filter;
filters[0].pattern = filter;
filters[1].label = "All Files (*.*)";
filters[1].pattern = "*.*";
static char path[260];
path[0] = '\0';
if (dvxFileDialog(sFormRt->ctx, title, FD_OPEN, NULL, filters, 2, path, sizeof(path))) {
return path;
}
return "";
}
const char *basFileSave(const char *title, const char *filter) {
if (!sFormRt) {
return "";
}
FileFilterT filters[2];
filters[0].label = filter;
filters[0].pattern = filter;
filters[1].label = "All Files (*.*)";
filters[1].pattern = "*.*";
static char path[260];
path[0] = '\0';
if (dvxFileDialog(sFormRt->ctx, title, FD_SAVE, NULL, filters, 2, path, sizeof(path))) {
return path;
}
return "";
}
const char *basInputBox2(const char *title, const char *prompt, const char *defaultText) {
if (!sFormRt) {
return "";
}
static char buf[512];
buf[0] = '\0';
if (dvxInputBox(sFormRt->ctx, title, prompt, defaultText, buf, sizeof(buf))) {
return buf;
}
return "";
}
int32_t basChoiceDialog(const char *title, const char *prompt, const char *items, int32_t defaultIdx) {
if (!sFormRt || !items) {
return -1;
}
// Split pipe-delimited items string into an array
static char itemBuf[1024];
snprintf(itemBuf, sizeof(itemBuf), "%s", items);
const char *ptrs[64];
int32_t count = 0;
char *tok = itemBuf;
while (*tok && count < 64) {
ptrs[count++] = tok;
char *sep = strchr(tok, '|');
if (sep) {
*sep = '\0';
tok = sep + 1;
} else {
break;
}
}
if (count == 0) {
return -1;
}
int32_t chosen = 0;
if (dvxChoiceDialog(sFormRt->ctx, title, prompt, ptrs, count, defaultIdx, &chosen)) {
return chosen;
}
return -1;
}
int32_t basIntInput(const char *title, const char *prompt, int32_t defaultVal, int32_t minVal, int32_t maxVal) {
if (!sFormRt) {
return defaultVal;
}
int32_t result = defaultVal;
dvxIntInputBox(sFormRt->ctx, title, prompt, defaultVal, minVal, maxVal, 1, &result);
return result;
}
int32_t basPromptSave(const char *title) {
if (!sFormRt) {
return 2;
}
return dvxPromptSave(sFormRt->ctx, title);
}