ProgMan now requires double clicks to launch. Enum type added to widgets. Huge amount of BASIC work.

This commit is contained in:
Scott Duensing 2026-04-02 20:06:48 -05:00
parent 17fe1840e3
commit 88746ec2ba
17 changed files with 807 additions and 261 deletions

View file

@ -313,6 +313,7 @@
// Halt
// ============================================================
#define OP_HALT 0xFF
#define OP_END 0xFE // explicit END statement -- terminates program
#define OP_HALT 0xFF // implicit end of module
#endif // DVXBASIC_OPCODES_H

View file

@ -2613,10 +2613,10 @@ static void parseDo(BasParserT *p) {
static void parseEnd(BasParserT *p) {
// END -- by itself = halt
// END -- by itself = terminate program
// END IF / END SUB / END FUNCTION / END SELECT are handled by their parsers
advance(p); // consume END
basEmit8(&p->cg, OP_HALT);
basEmit8(&p->cg, OP_END);
}

View file

@ -491,12 +491,15 @@ void *basFormRtLoadForm(void *ctx, const char *formName) {
}
}
// Forms start hidden; code must call Show to make them visible
WindowT *win = dvxCreateWindowCentered(rt->ctx, formName, DEFAULT_FORM_W, DEFAULT_FORM_H, true);
if (!win) {
return NULL;
}
win->visible = false;
WidgetT *root = wgtInitWindow(rt->ctx, win);
if (!root) {
@ -898,6 +901,7 @@ void basFormRtShowForm(void *ctx, void *formRef, bool modal) {
}
form->window->visible = true;
dvxRaiseWindow(rt->ctx, form->window);
if (form->frmAutoSize) {
dvxFitWindow(rt->ctx, form->window);
@ -1836,6 +1840,24 @@ static bool setIfaceProp(const WgtIfaceT *iface, WidgetT *w, const char *propNam
break;
}
case WGT_IFACE_ENUM:
if (p->enumNames && value.type == BAS_TYPE_STRING && value.strVal) {
// Map name to index
int32_t enumVal = 0;
for (int32_t en = 0; p->enumNames[en]; en++) {
if (strcasecmp(p->enumNames[en], value.strVal->data) == 0) {
enumVal = en;
break;
}
}
((void (*)(WidgetT *, int32_t))p->setFn)(w, enumVal);
} else {
((void (*)(WidgetT *, int32_t))p->setFn)(w, (int32_t)basValToNumber(value));
}
break;
case WGT_IFACE_INT:
((void (*)(WidgetT *, int32_t))p->setFn)(w, (int32_t)basValToNumber(value));
break;

View file

@ -220,6 +220,41 @@ void dsgnCreateWidgets(DsgnStateT *ds, WidgetT *contentBox) {
}
w->weight = ctrl->weight;
// Apply interface properties (Alignment, etc.) from FRM data
const char *wgtName = wgtFindByBasName(ctrl->typeName);
const WgtIfaceT *iface = wgtName ? wgtGetIface(wgtName) : NULL;
if (iface) {
for (int32_t pi = 0; pi < iface->propCount; pi++) {
const WgtPropDescT *p = &iface->props[pi];
if (!p->setFn) {
continue;
}
const char *val = getPropValue(ctrl, p->name);
if (!val) {
continue;
}
if (p->type == WGT_IFACE_ENUM && p->enumNames) {
for (int32_t en = 0; p->enumNames[en]; en++) {
if (strcasecmp(p->enumNames[en], val) == 0) {
((void (*)(WidgetT *, int32_t))p->setFn)(w, en);
break;
}
}
} else if (p->type == WGT_IFACE_INT) {
((void (*)(WidgetT *, int32_t))p->setFn)(w, atoi(val));
} else if (p->type == WGT_IFACE_BOOL) {
((void (*)(WidgetT *, bool))p->setFn)(w, strcasecmp(val, "True") == 0);
} else if (p->type == WGT_IFACE_STRING) {
((void (*)(WidgetT *, const char *))p->setFn)(w, val);
}
}
}
}
}
@ -901,6 +936,58 @@ static int32_t saveControls(const DsgnFormT *form, char *buf, int32_t bufSize, i
pos += snprintf(buf + pos, bufSize - pos, "%s %s = \"%s\"\n", pad, ctrl->props[j].name, ctrl->props[j].value);
}
// Save interface properties (Alignment, etc.) read from the live widget
if (ctrl->widget) {
const char *wgtName = wgtFindByBasName(ctrl->typeName);
const WgtIfaceT *iface = wgtName ? wgtGetIface(wgtName) : NULL;
if (iface) {
for (int32_t j = 0; j < iface->propCount; j++) {
const WgtPropDescT *p = &iface->props[j];
if (!p->getFn) {
continue;
}
// Skip if already saved as a custom prop
bool already = false;
for (int32_t k = 0; k < ctrl->propCount; k++) {
if (strcasecmp(ctrl->props[k].name, p->name) == 0) {
already = true;
break;
}
}
if (already) {
continue;
}
if (p->type == WGT_IFACE_ENUM && p->enumNames) {
int32_t v = ((int32_t (*)(const WidgetT *))p->getFn)(ctrl->widget);
const char *name = NULL;
for (int32_t en = 0; p->enumNames[en]; en++) {
if (en == v) {
name = p->enumNames[en];
break;
}
}
if (name) {
pos += snprintf(buf + pos, bufSize - pos, "%s %s = %s\n", pad, p->name, name);
}
} else if (p->type == WGT_IFACE_INT) {
int32_t v = ((int32_t (*)(const WidgetT *))p->getFn)(ctrl->widget);
pos += snprintf(buf + pos, bufSize - pos, "%s %s = %d\n", pad, p->name, (int)v);
} else if (p->type == WGT_IFACE_BOOL) {
bool v = ((bool (*)(const WidgetT *))p->getFn)(ctrl->widget);
pos += snprintf(buf + pos, bufSize - pos, "%s %s = %s\n", pad, p->name, v ? "True" : "False");
}
}
}
}
// Recursively output children of this container
if (dsgnIsContainer(ctrl->typeName)) {
pos = saveControls(form, buf, bufSize, pos, ctrl->name, indent + 1);

View file

@ -107,7 +107,8 @@ static void loadFile(void);
static void parseProcs(const char *source);
static void updateProjectMenuState(void);
static void saveActiveFile(void);
static void saveCurProc(void);
static bool saveCurProc(void);
static void stashFormCode(void);
static void showProc(int32_t procIdx);
static int32_t toolbarBottom(void);
static void loadFilePath(const char *path);
@ -197,6 +198,7 @@ static int32_t sOutputLen = 0;
static char *sGeneralBuf = NULL; // (General) section: module-level code
static char **sProcBufs = NULL; // stb_ds array: one buffer per procedure
static int32_t sCurProcIdx = -2; // which buffer is in the editor (-1=General, -2=none)
static int32_t sEditorFileIdx = -1; // which project file owns sProcBufs (-1=none)
// Procedure table for Object/Event dropdowns
typedef struct {
@ -209,6 +211,7 @@ static IdeProcEntryT *sProcTable = NULL; // stb_ds dynamic array
static const char **sObjItems = NULL; // stb_ds dynamic array
static const char **sEvtItems = NULL; // stb_ds dynamic array
static bool sDropdownNavSuppressed = false;
static bool sStopRequested = false;
// ============================================================
// App descriptor
@ -265,6 +268,7 @@ int32_t appMain(DxeAppContextT *ctx) {
// Auto-load project for development/testing
if (prjLoad(&sProject, "C:\\BIN\\APPS\\DVXBASIC\\MULTI.DBP")) {
prjLoadAllFiles(&sProject, sAc);
sProjectWin = prjCreateWindow(sAc, &sProject, onPrjFileClick);
if (sProjectWin) {
@ -332,9 +336,10 @@ static void buildWindow(void) {
wmAddMenuItem(fileMenu, "Open Pro&ject...", CMD_PRJ_OPEN);
wmAddMenuItem(fileMenu, "Save Projec&t", CMD_PRJ_SAVE);
wmAddMenuItem(fileMenu, "Close Projec&t", CMD_PRJ_CLOSE);
wmAddMenuSeparator(fileMenu);
wmAddMenuItem(fileMenu, "Project &Properties...", CMD_PRJ_PROPS);
wmAddMenuSeparator(fileMenu);
wmAddMenuItem(fileMenu, "&Open File...\tCtrl+O", CMD_OPEN);
wmAddMenuItem(fileMenu, "&Add File...\tCtrl+O", CMD_OPEN);
wmAddMenuItem(fileMenu, "&Save File\tCtrl+S", CMD_SAVE);
wmAddMenuItem(fileMenu, "Save A&ll", CMD_SAVE_ALL);
wmAddMenuSeparator(fileMenu);
@ -439,54 +444,73 @@ static void buildWindow(void) {
// Syntax colorizer callback for BASIC source code. Scans a single
// line and fills the colors array with syntax color indices.
static bool isBasicKeyword(const char *word, int32_t wordLen) {
// Hash-based keyword/type lookup using stb_ds.
// Key = uppercase word, value = syntax color (1=keyword, 6=type).
// Built once on first use, then O(1) per lookup.
typedef struct {
char *key;
uint8_t value;
} SyntaxMapEntryT;
static SyntaxMapEntryT *sSyntaxMap = NULL;
static void initSyntaxMap(void) {
if (sSyntaxMap) {
return;
}
sh_new_arena(sSyntaxMap);
static const char *keywords[] = {
"AND", "AS", "CALL", "CASE", "CLOSE", "CONST", "DATA", "DECLARE",
"DEF", "DEFINT", "DEFLNG", "DEFSNG", "DEFDBL", "DEFSTR",
"DIM", "DO", "DOEVENTS", "ELSE", "ELSEIF", "END", "ERASE",
"EXIT", "FOR", "FUNCTION", "GET", "GOSUB", "GOTO", "HIDE",
"IF", "IMP", "INPUT", "IS", "LET", "LIBRARY", "LINE", "LOAD",
"LOOP", "MOD", "MSGBOX", "NEXT", "NOT", "ON", "OPEN", "OPTION",
"OR", "PRINT", "PUT", "RANDOMIZE", "READ", "REDIM", "RESTORE",
"RESUME", "RETURN", "SEEK", "SELECT", "SHARED", "SHELL", "SHOW",
"SLEEP", "STATIC", "STEP", "STOP", "SUB", "SWAP", "THEN", "TO",
"TYPE", "UNLOAD", "UNTIL", "WEND", "WHILE", "WRITE", "XOR",
"AND", "AS", "BYVAL", "CALL", "CASE", "CLOSE", "CONST",
"DATA", "DECLARE", "DEF", "DEFDBL", "DEFINT", "DEFLNG",
"DEFSNG", "DEFSTR", "DIM", "DO", "DOEVENTS",
"ELSE", "ELSEIF", "END", "ERASE", "EXIT",
"FOR", "FUNCTION",
"GET", "GOSUB", "GOTO",
"HIDE",
"IF", "IMP", "INPUT", "IS",
"LET", "LIBRARY", "LINE", "LOAD", "LOOP",
"ME", "MOD", "MSGBOX",
"NEXT", "NOT",
"ON", "OPEN", "OPTION", "OR",
"PRINT", "PUT",
"RANDOMIZE", "READ", "REDIM", "RESTORE", "RESUME", "RETURN",
"SEEK", "SELECT", "SHARED", "SHELL", "SHOW", "SLEEP",
"STATIC", "STEP", "STOP", "SUB", "SWAP",
"THEN", "TO", "TYPE",
"UNLOAD", "UNTIL",
"WEND", "WHILE", "WRITE",
"XOR",
NULL
};
char upper[32];
if (wordLen <= 0 || wordLen >= 32) {
return false;
}
for (int32_t i = 0; i < wordLen; i++) {
upper[i] = (char)toupper((unsigned char)word[i]);
}
upper[wordLen] = '\0';
static const char *types[] = {
"BOOLEAN", "BYTE", "DOUBLE", "FALSE", "INTEGER",
"LONG", "SINGLE", "STRING", "TRUE",
NULL
};
for (int32_t i = 0; keywords[i]; i++) {
if (strcmp(upper, keywords[i]) == 0) {
return true;
}
shput(sSyntaxMap, keywords[i], 1);
}
return false;
for (int32_t i = 0; types[i]; i++) {
shput(sSyntaxMap, types[i], 6);
}
}
static bool isBasicType(const char *word, int32_t wordLen) {
static const char *types[] = {
"BOOLEAN", "BYTE", "DOUBLE", "INTEGER", "LONG", "SINGLE", "STRING",
"TRUE", "FALSE",
NULL
};
// classifyWord -- returns syntax color for an identifier.
// Converts to uppercase once, then does a single hash lookup.
static uint8_t classifyWord(const char *word, int32_t wordLen) {
char upper[32];
if (wordLen <= 0 || wordLen >= 32) {
return false;
return 0;
}
for (int32_t i = 0; i < wordLen; i++) {
@ -495,13 +519,15 @@ static bool isBasicType(const char *word, int32_t wordLen) {
upper[wordLen] = '\0';
for (int32_t i = 0; types[i]; i++) {
if (strcmp(upper, types[i]) == 0) {
return true;
}
initSyntaxMap();
int32_t idx = shgeti(sSyntaxMap, upper);
if (idx >= 0) {
return sSyntaxMap[idx].value;
}
return false;
return 0;
}
@ -562,13 +588,7 @@ static void basicColorize(const char *line, int32_t lineLen, uint8_t *colors, vo
return;
}
uint8_t c = 0; // default
if (isBasicKeyword(line + start, wordLen)) {
c = 1; // SYNTAX_KEYWORD
} else if (isBasicType(line + start, wordLen)) {
c = 6; // SYNTAX_TYPE
}
uint8_t c = classifyWord(line + start, wordLen);
for (int32_t j = start; j < i; j++) {
colors[j] = c;
@ -678,23 +698,20 @@ static void compileAndRun(void) {
int32_t srcLen = 0;
if (sProject.projectPath[0] != '\0' && sProject.fileCount > 0) {
// Stash current editor state
if (sProject.activeFileIdx >= 0) {
PrjFileT *cur = &sProject.files[sProject.activeFileIdx];
// Stash current editor state to the file that owns the proc buffers
if (sEditorFileIdx >= 0 && sEditorFileIdx < sProject.fileCount) {
PrjFileT *edFile = &sProject.files[sEditorFileIdx];
if (!cur->isForm) {
const char *fullSrc = getFullSource();
free(cur->buffer);
cur->buffer = fullSrc ? strdup(fullSrc) : NULL;
}
}
// Stash form code if editing a form's code
if (sDesigner.form && sCurProcIdx >= -1) {
if (!edFile->isForm) {
saveCurProc();
free(sDesigner.form->code);
sDesigner.form->code = strdup(getFullSource());
const char *fullSrc = getFullSource();
free(edFile->buffer);
edFile->buffer = fullSrc ? strdup(fullSrc) : NULL;
}
}
// Stash form code if the editor has form code loaded
stashFormCode();
// Concatenate all .bas files from buffers (or disk if not yet loaded)
concatBuf = (char *)malloc(IDE_MAX_SOURCE);
@ -710,7 +727,14 @@ static void compileAndRun(void) {
sProject.sourceMap = NULL;
sProject.sourceMapCount = 0;
// Two passes: .bas modules first (so CONST declarations are
// available), then .frm code sections.
for (int32_t pass = 0; pass < 2; pass++)
for (int32_t i = 0; i < sProject.fileCount; i++) {
// Pass 0: modules only. Pass 1: forms only.
if (pass == 0 && sProject.files[i].isForm) { continue; }
if (pass == 1 && !sProject.files[i].isForm) { continue; }
const char *fileSrc = NULL;
char *diskBuf = NULL;
@ -935,6 +959,19 @@ static void runCached(void) {
static void runModule(BasModuleT *mod) {
setStatus("Running...");
// Hide IDE windows while the program runs
bool hadFormWin = sFormWin && sFormWin->visible;
bool hadToolbox = sToolboxWin && sToolboxWin->visible;
bool hadProps = sPropsWin && sPropsWin->visible;
bool hadCodeWin = sCodeWin && sCodeWin->visible;
bool hadPrjWin = sProjectWin && sProjectWin->visible;
if (sFormWin) { sFormWin->visible = false; dvxInvalidateWindow(sAc, sFormWin); }
if (sToolboxWin) { sToolboxWin->visible = false; dvxInvalidateWindow(sAc, sToolboxWin); }
if (sPropsWin) { sPropsWin->visible = false; dvxInvalidateWindow(sAc, sPropsWin); }
if (sCodeWin) { sCodeWin->visible = false; dvxInvalidateWindow(sAc, sCodeWin); }
if (sProjectWin) { sProjectWin->visible = false; dvxInvalidateWindow(sAc, sProjectWin); }
// Create VM
BasVmT *vm = basVmCreate();
basVmLoadModule(vm, mod);
@ -954,9 +991,21 @@ static void runModule(BasModuleT *mod) {
// Load any .frm files from the same directory as the source
loadFrmFiles(formRt);
// Auto-show the first form (like VB3's startup form)
// Auto-show the startup form (or first form if none specified).
// Other forms remain hidden until code calls Show.
if (formRt->formCount > 0) {
basFormRtShowForm(formRt, &formRt->forms[0], false);
BasFormT *startupForm = &formRt->forms[0];
if (sProject.startupForm[0]) {
for (int32_t i = 0; i < formRt->formCount; i++) {
if (strcasecmp(formRt->forms[i].name, sProject.startupForm) == 0) {
startupForm = &formRt->forms[i];
break;
}
}
}
basFormRtShowForm(formRt, startupForm, false);
}
sVm = vm;
@ -966,6 +1015,7 @@ static void runModule(BasModuleT *mod) {
int32_t totalSteps = 0;
BasVmResultE result;
sStopRequested = false;
for (;;) {
result = basVmRun(vm);
@ -975,8 +1025,8 @@ static void runModule(BasModuleT *mod) {
// Yield to DVX to keep the GUI responsive
dvxUpdate(sAc);
// Stop if IDE window was closed or DVX is shutting down
if (!sWin || !sAc->running) {
// Stop if IDE window was closed, DVX is shutting down, or user hit Stop
if (!sWin || !sAc->running || sStopRequested) {
break;
}
@ -1000,8 +1050,9 @@ static void runModule(BasModuleT *mod) {
// The program ends when all forms are unloaded (closed).
if (result == BAS_VM_HALTED && formRt->formCount > 0) {
setStatus("Running (event loop)...");
sStopRequested = false;
while (sWin && sAc->running && formRt->formCount > 0) {
while (sWin && sAc->running && formRt->formCount > 0 && !sStopRequested && !vm->ended) {
dvxUpdate(sAc);
}
}
@ -1017,6 +1068,16 @@ static void runModule(BasModuleT *mod) {
basFormRtDestroy(formRt);
basVmDestroy(vm);
// Restore IDE windows
if (hadFormWin && sFormWin) { sFormWin->visible = true; dvxInvalidateWindow(sAc, sFormWin); }
if (hadToolbox && sToolboxWin) { sToolboxWin->visible = true; dvxInvalidateWindow(sAc, sToolboxWin); }
if (hadProps && sPropsWin) { sPropsWin->visible = true; dvxInvalidateWindow(sAc, sPropsWin); }
if (hadCodeWin && sCodeWin) { sCodeWin->visible = true; dvxInvalidateWindow(sAc, sCodeWin); }
if (hadPrjWin && sProjectWin) { sProjectWin->visible = true; dvxInvalidateWindow(sAc, sProjectWin); }
// Repaint to clear destroyed runtime forms and restore designer
dvxUpdate(sAc);
}
// ============================================================
@ -1191,6 +1252,9 @@ static void loadFilePath(const char *path) {
showCodeWindow();
}
// Stash form code before overwriting proc buffers
stashFormCode();
// Parse into per-procedure buffers and show (General) section
parseProcs(srcBuf);
free(srcBuf);
@ -1303,6 +1367,8 @@ static void ensureProject(const char *filePath) {
sProject.dirty = false;
sProject.activeFileIdx = 0;
prjLoadAllFiles(&sProject, sAc);
char title[300];
snprintf(title, sizeof(title), "DVX BASIC - [%s]", sProject.name);
@ -1323,7 +1389,7 @@ static void loadFile(void) {
char path[DVX_MAX_PATH];
if (!dvxFileDialog(sAc, "Open BASIC File", FD_OPEN, NULL, filters, 3, path, sizeof(path))) {
if (!dvxFileDialog(sAc, "Add File", FD_OPEN, NULL, filters, 3, path, sizeof(path))) {
return;
}
@ -1358,6 +1424,7 @@ static void loadFile(void) {
onPrjFileClick(0, true);
} else {
loadFilePath(path);
sEditorFileIdx = 0;
}
}
}
@ -1381,15 +1448,14 @@ static void saveActiveFile(void) {
char fullPath[DVX_MAX_PATH];
prjFullPath(&sProject, idx, fullPath, sizeof(fullPath));
if (file->isForm && sDesigner.form) {
// Save editor code back to form->code before saving
if (sCurProcIdx >= -1) {
saveCurProc();
free(sDesigner.form->code);
sDesigner.form->code = strdup(getFullSource());
}
if (file->isForm) {
// Only serialize through the designer if it holds THIS form
bool isDesignerForm = (sDesigner.form &&
strcasecmp(sDesigner.form->name, file->formName) == 0);
if (isDesignerForm) {
stashFormCode();
// Save form designer state to .frm file
char *frmBuf = (char *)malloc(IDE_MAX_SOURCE);
if (frmBuf) {
@ -1408,9 +1474,26 @@ static void saveActiveFile(void) {
free(frmBuf);
}
} else if (!file->isForm) {
// Save full source (splice current proc back first)
const char *src = getFullSource();
} else if (file->buffer) {
// Not the active designer form -- save from stashed buffer
FILE *f = fopen(fullPath, "w");
if (f) {
fputs(file->buffer, f);
fclose(f);
file->modified = false;
}
}
} else {
// Save .bas file -- use editor if it has this file, else use buffer
const char *src = NULL;
if (sEditorFileIdx == idx) {
saveCurProc();
src = getFullSource();
} else {
src = file->buffer;
}
if (src) {
FILE *f = fopen(fullPath, "w");
@ -1472,14 +1555,22 @@ static void onPrjFileClick(int32_t fileIdx, bool isForm) {
}
if (fileIdx == sProject.activeFileIdx) {
// Already active -- but ensure the right view is shown
if (isForm) {
switchToDesign();
}
return;
}
// Stash current active file's contents into its buffer
// Stash current active file's contents into its buffer.
// This is just caching -- do not mark modified.
if (sProject.activeFileIdx >= 0) {
PrjFileT *cur = &sProject.files[sProject.activeFileIdx];
if (cur->isForm && sDesigner.form) {
// Save editor code back to form->code before serializing
stashFormCode();
// Serialize form designer state to .frm text
char *frmBuf = (char *)malloc(IDE_MAX_SOURCE);
@ -1495,15 +1586,13 @@ static void onPrjFileClick(int32_t fileIdx, bool isForm) {
free(frmBuf);
cur->buffer = NULL;
}
cur->modified = true;
}
} else if (!cur->isForm) {
// Stash full source (splice current proc back first)
} else if (!cur->isForm && sEditorFileIdx == sProject.activeFileIdx) {
// Stash full source (only if editor has this file's code)
saveCurProc();
const char *src = getFullSource();
free(cur->buffer);
cur->buffer = src ? strdup(src) : NULL;
cur->modified = true;
}
}
@ -1550,11 +1639,17 @@ static void onPrjFileClick(int32_t fileIdx, bool isForm) {
*dot = '\0';
}
if (sFormWin) {
dvxDestroyWindow(sAc, sFormWin);
cleanupFormWin();
}
if (sDesigner.form) {
dsgnFree(&sDesigner);
}
dsgnNewForm(&sDesigner, formName);
snprintf(target->formName, sizeof(target->formName), "%s", sDesigner.form->name);
target->modified = true;
sProject.activeFileIdx = fileIdx;
switchToDesign();
@ -1583,6 +1678,12 @@ static void onPrjFileClick(int32_t fileIdx, bool isForm) {
frmSrc = diskBuf;
}
// Close the old form designer window before loading a new form
if (sFormWin) {
dvxDestroyWindow(sAc, sFormWin);
cleanupFormWin();
}
if (sDesigner.form) {
dsgnFree(&sDesigner);
}
@ -1590,10 +1691,16 @@ static void onPrjFileClick(int32_t fileIdx, bool isForm) {
dsgnLoadFrm(&sDesigner, frmSrc, (int32_t)strlen(frmSrc));
free(diskBuf);
if (sDesigner.form) {
snprintf(target->formName, sizeof(target->formName), "%s", sDesigner.form->name);
}
sProject.activeFileIdx = fileIdx;
switchToDesign();
} else {
// Load .bas file from buffer or disk
stashFormCode();
if (!sCodeWin) {
showCodeWindow();
}
@ -1624,6 +1731,7 @@ static void onPrjFileClick(int32_t fileIdx, bool isForm) {
sEditor->onChange = onEditorChange;
}
sEditorFileIdx = fileIdx;
sProject.activeFileIdx = fileIdx;
}
}
@ -1724,6 +1832,8 @@ static void openProject(void) {
return;
}
prjLoadAllFiles(&sProject, sAc);
// Create and show project window
if (!sProjectWin) {
sProjectWin = prjCreateWindow(sAc, &sProject, onPrjFileClick);
@ -1766,6 +1876,26 @@ static void closeProject(void) {
prjSave(&sProject);
}
// Close designer windows
if (sFormWin) {
dvxDestroyWindow(sAc, sFormWin);
cleanupFormWin();
}
dsgnFree(&sDesigner);
// Close code editor
if (sCodeWin) {
dvxDestroyWindow(sAc, sCodeWin);
sCodeWin = NULL;
sEditor = NULL;
sObjDropdown = NULL;
sEvtDropdown = NULL;
}
freeProcBufs();
// Close project window
prjClose(&sProject);
if (sProjectWin) {
@ -1938,13 +2068,24 @@ void ideRenameInCode(const char *oldName, const char *newName) {
}
}
// Update form->code from the renamed buffers
if (sDesigner.form) {
// Update form->code from the renamed buffers (only if editor has this form's code)
if (sDesigner.form && sEditorFileIdx >= 0 && sEditorFileIdx < sProject.fileCount &&
sProject.files[sEditorFileIdx].isForm &&
strcasecmp(sProject.files[sEditorFileIdx].formName, sDesigner.form->name) == 0) {
free(sDesigner.form->code);
sDesigner.form->code = strdup(getFullSource());
sDesigner.form->dirty = true;
}
// Update cached formName if the active file is a form being renamed
if (sProject.activeFileIdx >= 0 && sProject.activeFileIdx < sProject.fileCount) {
PrjFileT *cur = &sProject.files[sProject.activeFileIdx];
if (cur->isForm && strcasecmp(cur->formName, oldName) == 0) {
snprintf(cur->formName, sizeof(cur->formName), "%s", newName);
}
}
// Rename in all project .bas file buffers (and non-active .frm code)
for (int32_t i = 0; i < sProject.fileCount; i++) {
// Skip the active file (already handled above)
@ -2006,13 +2147,8 @@ void ideRenameInCode(const char *oldName, const char *newName) {
// ============================================================
static void onCodeWinClose(WindowT *win) {
// Stash code back to form->code before the window is destroyed.
// This is just caching -- do not mark dirty.
if (sDesigner.form && sCurProcIdx >= -1) {
saveCurProc();
free(sDesigner.form->code);
sDesigner.form->code = strdup(getFullSource());
}
// Stash code back before the window is destroyed.
stashFormCode();
dvxDestroyWindow(sAc, win);
sCodeWin = NULL;
@ -2043,11 +2179,19 @@ static void onProjectWinClose(WindowT *win) {
// Load all .frm files listed in the current project into the
// form runtime for execution.
static void loadFrmFile(BasFormRtT *rt, const char *frmPath) {
FILE *f = fopen(frmPath, "r");
static void loadFrmFiles(BasFormRtT *rt) {
for (int32_t i = 0; i < sProject.fileCount; i++) {
if (!sProject.files[i].isForm) {
continue;
}
char fullPath[DVX_MAX_PATH];
prjFullPath(&sProject, i, fullPath, sizeof(fullPath));
FILE *f = fopen(fullPath, "r");
if (!f) {
return;
continue;
}
fseek(f, 0, SEEK_END);
@ -2056,30 +2200,26 @@ static void loadFrmFile(BasFormRtT *rt, const char *frmPath) {
if (size <= 0 || size >= IDE_MAX_SOURCE) {
fclose(f);
return;
continue;
}
char *frmBuf = (char *)malloc(size + 1);
if (!frmBuf) {
fclose(f);
return;
continue;
}
int32_t bytesRead = (int32_t)fread(frmBuf, 1, size, f);
fclose(f);
frmBuf[bytesRead] = '\0';
basFormRtLoadFrm(rt, frmBuf, bytesRead);
BasFormT *form = basFormRtLoadFrm(rt, frmBuf, bytesRead);
free(frmBuf);
}
static void loadFrmFiles(BasFormRtT *rt) {
for (int32_t i = 0; i < sProject.fileCount; i++) {
if (sProject.files[i].isForm) {
char fullPath[DVX_MAX_PATH];
prjFullPath(&sProject, i, fullPath, sizeof(fullPath));
loadFrmFile(rt, fullPath);
// Cache the form object name in the project file entry
if (form && form->name[0]) {
snprintf(sProject.files[i].formName, sizeof(sProject.files[i].formName), "%s", form->name);
}
}
}
@ -2297,10 +2437,11 @@ static void onMenu(WindowT *win, int32_t menuId) {
break;
case CMD_STOP:
sStopRequested = true;
if (sVm) {
sVm->running = false;
setStatus("Program stopped.");
}
setStatus("Program stopped.");
break;
case CMD_CLEAR:
@ -2569,19 +2710,7 @@ static void onEvtDropdownChange(WidgetT *w) {
snprintf(skeleton, sizeof(skeleton), "Sub %s ()\n\nEnd Sub\n", subName);
arrput(sProcBufs, strdup(skeleton));
updateDropdowns();
// Show the new proc (last in the list)
procCount = (int32_t)arrlen(sProcTable);
for (int32_t i = 0; i < procCount; i++) {
if (strcasecmp(sProcTable[i].objName, selObj) == 0 &&
strcasecmp(sProcTable[i].evtName, evtName) == 0) {
showProc(i);
return;
}
}
showProc((int32_t)arrlen(sProcBufs) - 1);
}
@ -3048,9 +3177,10 @@ static void selectDropdowns(const char *objName, const char *evtName) {
}
// Rebuild the event list for this object but suppress navigation
bool savedSuppress = sDropdownNavSuppressed;
sDropdownNavSuppressed = true;
onObjDropdownChange(sObjDropdown);
sDropdownNavSuppressed = false;
sDropdownNavSuppressed = savedSuppress;
// Now select the specific event
int32_t evtCount = (int32_t)arrlen(sEvtItems);
@ -3100,8 +3230,10 @@ static void navigateToEventSub(void) {
char subName[128];
snprintf(subName, sizeof(subName), "%s_%s", ctrlName, eventName);
// Parse the form's code into per-procedure buffers
// Stash any existing editor code, then load this form's code
stashFormCode();
parseProcs(sDesigner.form->code ? sDesigner.form->code : "");
sEditorFileIdx = sProject.activeFileIdx;
// Ensure code window is open
if (!sCodeWin) {
@ -3114,9 +3246,12 @@ static void navigateToEventSub(void) {
// Populate dropdown items without triggering navigation --
// we navigate explicitly below after finding the target proc.
{
bool saved = sDropdownNavSuppressed;
sDropdownNavSuppressed = true;
updateDropdowns();
sDropdownNavSuppressed = false;
sDropdownNavSuppressed = saved;
}
// Search for existing procedure
int32_t procCount = (int32_t)arrlen(sProcTable);
@ -3144,8 +3279,6 @@ static void navigateToEventSub(void) {
arrput(sProcBufs, strdup(skeleton));
updateDropdowns();
// Show the new procedure (it's the last one)
switchToCode();
showProc((int32_t)arrlen(sProcBufs) - 1);
@ -3327,12 +3460,7 @@ static void switchToCode(void) {
// ============================================================
static void switchToDesign(void) {
// Save code back to form before switching
if (sDesigner.form && sCurProcIdx >= -1) {
saveCurProc();
free(sDesigner.form->code);
sDesigner.form->code = strdup(getFullSource());
}
stashFormCode();
// If already open, just bring to front
if (sFormWin) {
@ -3415,6 +3543,7 @@ static void switchToDesign(void) {
}
}
dvxInvalidateWindow(sAc, sFormWin);
setStatus("Design view open.");
}
@ -3426,7 +3555,7 @@ static void switchToDesign(void) {
static void onTbOpen(WidgetT *w) { (void)w; loadFile(); }
static void onTbSave(WidgetT *w) { (void)w; saveFile(); }
static void onTbRun(WidgetT *w) { (void)w; compileAndRun(); }
static void onTbStop(WidgetT *w) { (void)w; if (sVm) { sVm->running = false; setStatus("Program stopped."); } }
static void onTbStop(WidgetT *w) { (void)w; sStopRequested = true; if (sVm) { sVm->running = false; } setStatus("Program stopped."); }
static void onTbCode(WidgetT *w) { (void)w; switchToCode(); }
static void onTbDesign(WidgetT *w) { (void)w; switchToDesign(); }
@ -3685,6 +3814,7 @@ static void freeProcBufs(void) {
arrfree(sProcBufs);
sProcBufs = NULL;
sCurProcIdx = -2;
sEditorFileIdx = -1;
}
@ -3956,19 +4086,42 @@ static char *extractNewProcs(const char *buf) {
}
// saveCurProc -- save editor contents back to the current buffer.
// If the user typed a new Sub/Function in the General section,
// it's automatically extracted into its own procedure buffer.
// stashFormCode -- if the proc buffers belong to the designer's form,
// save them back to form->code. Uses sEditorFileIdx to know which
// file the proc buffers actually belong to.
static void saveCurProc(void) {
if (!sEditor) {
static void stashFormCode(void) {
if (!sDesigner.form || sEditorFileIdx < 0) {
return;
}
if (sEditorFileIdx >= sProject.fileCount || !sProject.files[sEditorFileIdx].isForm) {
return;
}
if (strcasecmp(sProject.files[sEditorFileIdx].formName, sDesigner.form->name) != 0) {
return;
}
saveCurProc();
free(sDesigner.form->code);
sDesigner.form->code = strdup(getFullSource());
}
// saveCurProc -- save editor contents back to the current buffer.
// Returns true if the proc list was modified (skeleton discarded or
// new procs extracted), meaning sProcBufs indices may have shifted.
static bool saveCurProc(void) {
if (!sEditor) {
return false;
}
const char *edText = wgtGetText(sEditor);
if (!edText) {
return;
return false;
}
if (sCurProcIdx == -1) {
@ -3979,10 +4132,11 @@ static void saveCurProc(void) {
sGeneralBuf = cleaned ? cleaned : strdup(edText);
if (cleaned) {
// Update editor to show the cleaned General section
wgtSetText(sEditor, sGeneralBuf);
updateDropdowns();
return true;
}
return false;
} else if (sCurProcIdx >= 0 && sCurProcIdx < (int32_t)arrlen(sProcBufs)) {
// Get the name of the current proc so we can identify its block
// regardless of position in the editor.
@ -4052,10 +4206,11 @@ static void saveCurProc(void) {
free(sProcBufs[sCurProcIdx]);
arrdel(sProcBufs, sCurProcIdx);
sCurProcIdx = -2;
updateDropdowns();
return true;
} else {
free(sProcBufs[sCurProcIdx]);
sProcBufs[sCurProcIdx] = strdup(edText);
return false;
}
} else {
// Multiple proc blocks in the editor. Find the one matching
@ -4150,10 +4305,12 @@ static void saveCurProc(void) {
// Update editor to show only this proc
wgtSetText(sEditor, sProcBufs[sCurProcIdx]);
updateDropdowns();
return true;
}
}
}
return false;
}
@ -4164,9 +4321,16 @@ static void showProc(int32_t procIdx) {
return;
}
// Save whatever is currently in the editor
// Save whatever is currently in the editor.
// If a buffer was deleted (empty skeleton discard), adjust the
// target index since arrdel shifts everything after it.
if (sCurProcIdx >= -1) {
saveCurProc();
int32_t deletedIdx = sCurProcIdx;
bool changed = saveCurProc();
if (changed && deletedIdx >= 0 && procIdx > deletedIdx) {
procIdx--;
}
}
// Suppress onChange while loading -- setting text is not a user edit

View file

@ -28,6 +28,7 @@
#include "dvxWm.h"
#include "widgetBox.h"
#include "widgetButton.h"
#include "widgetDropdown.h"
#include "widgetImage.h"
#include "widgetLabel.h"
#include "widgetTextInput.h"
@ -64,6 +65,7 @@ static char **sLabels = NULL; // stb_ds array of strdup'd strings
static void onPrjWinClose(WindowT *win);
static void onTreeItemDblClick(WidgetT *w);
static bool validateIcon(const char *fullPath, bool showErrors);
// ============================================================
// prjInit
@ -296,6 +298,79 @@ int32_t prjAddFile(PrjStateT *prj, const char *relativePath, bool isForm) {
}
// ============================================================
// prjLoadAllFiles -- read all project files into memory buffers
// and extract form names from .frm files.
// ============================================================
void prjLoadAllFiles(PrjStateT *prj, AppContextT *ctx) {
for (int32_t i = 0; i < prj->fileCount; i++) {
if (prj->files[i].buffer) {
continue; // already loaded
}
char fullPath[DVX_MAX_PATH];
prjFullPath(prj, i, fullPath, sizeof(fullPath));
FILE *f = fopen(fullPath, "r");
if (!f) {
continue;
}
fseek(f, 0, SEEK_END);
long size = ftell(f);
fseek(f, 0, SEEK_SET);
if (size <= 0) {
fclose(f);
continue;
}
char *buf = (char *)malloc(size + 1);
if (!buf) {
fclose(f);
continue;
}
int32_t bytesRead = (int32_t)fread(buf, 1, size, f);
fclose(f);
buf[bytesRead] = '\0';
prj->files[i].buffer = buf;
// Extract form name from .frm files
if (prj->files[i].isForm) {
const char *pos = buf;
while (*pos) {
while (*pos == ' ' || *pos == '\t') { pos++; }
if (strncasecmp(pos, "Begin Form ", 11) == 0) {
const char *np = pos + 11;
while (*np == ' ' || *np == '\t') { np++; }
int32_t n = 0;
while (*np && *np != ' ' && *np != '\t' && *np != '\r' && *np != '\n' && n < PRJ_MAX_NAME - 1) {
prj->files[i].formName[n++] = *np++;
}
prj->files[i].formName[n] = '\0';
break;
}
while (*pos && *pos != '\n') { pos++; }
if (*pos == '\n') { pos++; }
}
}
// Yield between files to keep the UI responsive
if (ctx) {
dvxUpdate(ctx);
}
}
}
// ============================================================
// prjRemoveFile
// ============================================================
@ -460,26 +535,13 @@ void prjRebuildTree(PrjStateT *prj) {
projNode->userData = (void *)(intptr_t)-1;
wgtTreeItemSetExpanded(projNode, true);
// Forms group
// Forms and Modules groups
char *formsLabel = strdup("Forms");
arrput(sLabels, formsLabel);
WidgetT *formsNode = wgtTreeItem(projNode, formsLabel);
formsNode->userData = (void *)(intptr_t)-1;
wgtTreeItemSetExpanded(formsNode, true);
for (int32_t i = 0; i < prj->fileCount; i++) {
if (prj->files[i].isForm) {
char buf[DVX_MAX_PATH + 4];
snprintf(buf, sizeof(buf), "%s%s", prj->files[i].path, prj->files[i].modified ? " *" : "");
char *label = strdup(buf);
arrput(sLabels, label);
WidgetT *item = wgtTreeItem(formsNode, label);
item->userData = (void *)(intptr_t)i;
item->onDblClick = onTreeItemDblClick;
}
}
// Modules group
char *modsLabel = strdup("Modules");
arrput(sLabels, modsLabel);
WidgetT *modsNode = wgtTreeItem(projNode, modsLabel);
@ -487,16 +549,14 @@ void prjRebuildTree(PrjStateT *prj) {
wgtTreeItemSetExpanded(modsNode, true);
for (int32_t i = 0; i < prj->fileCount; i++) {
if (!prj->files[i].isForm) {
char buf[DVX_MAX_PATH + 4];
snprintf(buf, sizeof(buf), "%s%s", prj->files[i].path, prj->files[i].modified ? " *" : "");
char *label = strdup(buf);
arrput(sLabels, label);
WidgetT *item = wgtTreeItem(modsNode, label);
WidgetT *item = wgtTreeItem(prj->files[i].isForm ? formsNode : modsNode, label);
item->userData = (void *)(intptr_t)i;
item->onDblClick = onTreeItemDblClick;
}
}
wgtInvalidate(sTree);
}
@ -521,6 +581,8 @@ static struct {
WidgetT *version;
WidgetT *copyright;
WidgetT *description;
WidgetT *startupForm;
const char **formNames; // stb_ds array of form name strings for startup dropdown
WidgetT *iconPreview;
char iconPath[DVX_MAX_PATH];
const char *appPath;
@ -533,22 +595,10 @@ static void ppdOnOk(WidgetT *w) {
// Validate icon path if set
if (sPpd.iconPath[0] && sPpd.prj) {
const char *iconText = sPpd.iconPath;
char fullPath[DVX_MAX_PATH * 2];
snprintf(fullPath, sizeof(fullPath), "%s/%s", sPpd.prj->projectDir, iconText);
snprintf(fullPath, sizeof(fullPath), "%s/%s", sPpd.prj->projectDir, sPpd.iconPath);
int32_t infoW = 0;
int32_t infoH = 0;
if (!dvxImageInfo(fullPath, &infoW, &infoH)) {
dvxMessageBox(sPpd.ctx, "Invalid Icon", "Could not read image file.", MB_OK | MB_ICONERROR);
return;
}
if (infoW != 32 || infoH != 32) {
char msg[128];
snprintf(msg, sizeof(msg), "Icon must be 32x32 pixels.\nThis image is %dx%d.", (int)infoW, (int)infoH);
dvxMessageBox(sPpd.ctx, "Invalid Icon", msg, MB_OK | MB_ICONWARNING);
if (!validateIcon(fullPath, true)) {
return;
}
}
@ -560,6 +610,33 @@ static void ppdOnCancel(WidgetT *w) { (void)w; sPpd.accepted = false; sPpd.done
static void ppdOnClose(WindowT *win) { (void)win; sPpd.accepted = false; sPpd.done = true; }
// validateIcon -- check that an image file is a valid 32x32 icon.
// Returns true if valid. Shows an error dialog and returns false if not.
static bool validateIcon(const char *fullPath, bool showErrors) {
int32_t infoW = 0;
int32_t infoH = 0;
if (!dvxImageInfo(fullPath, &infoW, &infoH)) {
if (showErrors) {
dvxMessageBox(sPpd.ctx, "Invalid Icon", "Could not read image file.", MB_OK | MB_ICONERROR);
}
return false;
}
if (infoW != 32 || infoH != 32) {
if (showErrors) {
char msg[128];
snprintf(msg, sizeof(msg), "Icon must be 32x32 pixels.\nThis image is %dx%d.", (int)infoW, (int)infoH);
dvxMessageBox(sPpd.ctx, "Invalid Icon", msg, MB_OK | MB_ICONWARNING);
}
return false;
}
return true;
}
static void ppdLoadIconPreview(void) {
if (!sPpd.iconPreview || !sPpd.ctx || !sPpd.prj) {
return;
@ -574,18 +651,7 @@ static void ppdLoadIconPreview(void) {
char fullPath[DVX_MAX_PATH * 2];
snprintf(fullPath, sizeof(fullPath), "%s/%s", sPpd.prj->projectDir, relPath);
// Verify the image is 32x32 before loading
int32_t infoW = 0;
int32_t infoH = 0;
if (!dvxImageInfo(fullPath, &infoW, &infoH)) {
return;
}
if (infoW != 32 || infoH != 32) {
char msg[128];
snprintf(msg, sizeof(msg), "Icon must be 32x32 pixels.\nThis image is %dx%d.", (int)infoW, (int)infoH);
dvxMessageBox(sPpd.ctx, "Invalid Icon", msg, MB_OK | MB_ICONWARNING);
if (!validateIcon(fullPath, true)) {
sPpd.iconPath[0] = '\0';
return;
}
@ -612,19 +678,7 @@ static void ppdOnBrowseIcon(WidgetT *w) {
char path[DVX_MAX_PATH];
if (dvxFileDialog(sPpd.ctx, "Select Icon", FD_OPEN, NULL, filters, 2, path, sizeof(path))) {
// Validate size using the full path before accepting
int32_t infoW = 0;
int32_t infoH = 0;
if (!dvxImageInfo(path, &infoW, &infoH)) {
dvxMessageBox(sPpd.ctx, "Invalid Icon", "Could not read image file.", MB_OK | MB_ICONERROR);
return;
}
if (infoW != 32 || infoH != 32) {
char msg[128];
snprintf(msg, sizeof(msg), "Icon must be 32x32 pixels.\nThis image is %dx%d.", (int)infoW, (int)infoH);
dvxMessageBox(sPpd.ctx, "Invalid Icon", msg, MB_OK | MB_ICONWARNING);
if (!validateIcon(path, true)) {
return;
}
@ -761,6 +815,53 @@ bool prjPropertiesDialog(AppContextT *ctx, PrjStateT *prj, const char *appPath)
sPpd.company = ppdAddRow(root, "Company:", prj->company, PRJ_MAX_STRING);
sPpd.version = ppdAddRow(root, "Version:", prj->version, PRJ_MAX_NAME);
sPpd.copyright = ppdAddRow(root, "Copyright:", prj->copyright, PRJ_MAX_STRING);
// Startup form dropdown
{
WidgetT *sfRow = wgtHBox(root);
sfRow->spacing = wgtPixels(4);
WidgetT *sfLbl = wgtLabel(sfRow, "Startup Form:");
sfLbl->minW = wgtPixels(PPD_LABEL_W);
sPpd.startupForm = wgtDropdown(sfRow);
sPpd.startupForm->weight = 100;
// Populate with form names from the project
sPpd.formNames = NULL;
int32_t selectedIdx = 0;
for (int32_t i = 0; i < prj->fileCount; i++) {
if (!prj->files[i].isForm) {
continue;
}
// Use the cached form object name, fall back to filename
const char *name = prj->files[i].formName;
char fallback[PRJ_MAX_NAME];
if (!name[0]) {
snprintf(fallback, sizeof(fallback), "%s", prj->files[i].path);
char *dot = strrchr(fallback, '.');
if (dot) { *dot = '\0'; }
name = fallback;
}
arrput(sPpd.formNames, strdup(name));
if (strcasecmp(name, prj->startupForm) == 0) {
selectedIdx = (int32_t)arrlen(sPpd.formNames) - 1;
}
}
int32_t formCount = (int32_t)arrlen(sPpd.formNames);
wgtDropdownSetItems(sPpd.startupForm, sPpd.formNames, formCount);
if (formCount > 0) {
wgtDropdownSetSelected(sPpd.startupForm, selectedIdx);
}
}
// Icon row: label + preview + Browse button
{
WidgetT *iconRow = wgtHBox(root);
@ -847,9 +948,25 @@ bool prjPropertiesDialog(AppContextT *ctx, PrjStateT *prj, const char *appPath)
snprintf(prj->iconPath, sizeof(prj->iconPath), "%s", sPpd.iconPath);
// Read startup form from dropdown
if (sPpd.startupForm && sPpd.formNames) {
int32_t sfIdx = wgtDropdownGetSelected(sPpd.startupForm);
if (sfIdx >= 0 && sfIdx < (int32_t)arrlen(sPpd.formNames)) {
snprintf(prj->startupForm, sizeof(prj->startupForm), "%s", sPpd.formNames[sfIdx]);
}
}
prj->dirty = true;
}
// Free form name strings
for (int32_t i = 0; i < (int32_t)arrlen(sPpd.formNames); i++) {
free((char *)sPpd.formNames[i]);
}
arrfree(sPpd.formNames);
sPpd.formNames = NULL;
ctx->modalWindow = prevModal;
dvxDestroyWindow(ctx, win);

View file

@ -26,6 +26,7 @@ typedef struct {
bool isForm; // true = .frm, false = .bas
char *buffer; // in-memory edit buffer (malloc'd, NULL = not loaded)
bool modified; // true = buffer has unsaved changes
char formName[PRJ_MAX_NAME]; // form object name (from "Begin Form <name>")
} PrjFileT;
// ============================================================
@ -73,6 +74,7 @@ bool prjSaveAs(PrjStateT *prj, const char *dbpPath);
void prjNew(PrjStateT *prj, const char *name, const char *directory);
void prjClose(PrjStateT *prj);
int32_t prjAddFile(PrjStateT *prj, const char *relativePath, bool isForm);
void prjLoadAllFiles(PrjStateT *prj, AppContextT *ctx);
void prjRemoveFile(PrjStateT *prj, int32_t idx);
void prjFullPath(const PrjStateT *prj, int32_t fileIdx, char *outPath, int32_t outSize);

View file

@ -102,6 +102,7 @@ static void onPrpClose(WindowT *win) {
#define PROP_TYPE_STRING WGT_IFACE_STRING
#define PROP_TYPE_INT WGT_IFACE_INT
#define PROP_TYPE_BOOL WGT_IFACE_BOOL
#define PROP_TYPE_ENUM WGT_IFACE_ENUM
#define PROP_TYPE_READONLY 255
static uint8_t getPropType(const char *propName, const char *typeName) {
@ -145,6 +146,33 @@ static uint8_t getPropType(const char *propName, const char *typeName) {
}
static const WgtPropDescT *findIfaceProp(const char *typeName, const char *propName) {
if (!typeName || !typeName[0]) {
return NULL;
}
const char *wgtName = wgtFindByBasName(typeName);
if (!wgtName) {
return NULL;
}
const WgtIfaceT *iface = wgtGetIface(wgtName);
if (!iface) {
return NULL;
}
for (int32_t i = 0; i < iface->propCount; i++) {
if (strcasecmp(iface->props[i].name, propName) == 0) {
return &iface->props[i];
}
}
return NULL;
}
// ============================================================
// cascadeToChildren
// ============================================================
@ -260,6 +288,31 @@ static void onPropDblClick(WidgetT *w) {
// Toggle boolean on double-click -- no input box
bool cur = (strcasecmp(curValue, "True") == 0);
snprintf(newValue, sizeof(newValue), "%s", cur ? "False" : "True");
} else if (propType == PROP_TYPE_ENUM) {
// Enum: cycle to next value on double-click
const WgtPropDescT *pd = findIfaceProp(ctrlTypeName, propName);
if (!pd || !pd->enumNames) {
return;
}
// Find current value and advance to next
int32_t enumCount = 0;
int32_t curIdx = 0;
while (pd->enumNames[enumCount]) {
if (strcasecmp(pd->enumNames[enumCount], curValue) == 0) {
curIdx = enumCount;
}
enumCount++;
}
if (enumCount == 0) {
return;
}
int32_t nextIdx = (curIdx + 1) % enumCount;
snprintf(newValue, sizeof(newValue), "%s", pd->enumNames[nextIdx]);
} else if (propType == PROP_TYPE_INT) {
// Spinner dialog for integers
char prompt[128];
@ -387,6 +440,17 @@ static void onPropDblClick(WidgetT *w) {
((void (*)(WidgetT *, const char *))p->setFn)(ctrl->widget, ctrl->props[ctrl->propCount].value);
ctrl->propCount++;
}
} else if (p->type == WGT_IFACE_ENUM && p->enumNames) {
int32_t enumVal = 0;
for (int32_t en = 0; p->enumNames[en]; en++) {
if (strcasecmp(p->enumNames[en], newValue) == 0) {
enumVal = en;
break;
}
}
((void (*)(WidgetT *, int32_t))p->setFn)(ctrl->widget, enumVal);
} else if (p->type == WGT_IFACE_INT) {
((void (*)(WidgetT *, int32_t))p->setFn)(ctrl->widget, atoi(newValue));
} else if (p->type == WGT_IFACE_BOOL) {
@ -885,6 +949,18 @@ void prpRefresh(DsgnStateT *ds) {
if (p->type == WGT_IFACE_STRING && p->getFn) {
const char *s = ((const char *(*)(WidgetT *))p->getFn)(ctrl->widget);
addPropRow(p->name, s ? s : "");
} else if (p->type == WGT_IFACE_ENUM && p->getFn && p->enumNames) {
int32_t v = ((int32_t (*)(const WidgetT *))p->getFn)(ctrl->widget);
const char *name = NULL;
for (int32_t k = 0; p->enumNames[k]; k++) {
if (k == v) {
name = p->enumNames[k];
break;
}
}
addPropRow(p->name, name ? name : "?");
} else if (p->type == WGT_IFACE_INT && p->getFn) {
int32_t v = ((int32_t (*)(const WidgetT *))p->getFn)(ctrl->widget);
snprintf(buf, sizeof(buf), "%d", (int)v);

View file

@ -2534,6 +2534,11 @@ BasVmResultE basVmStep(BasVmT *vm) {
// Halt
// ============================================================
case OP_END:
vm->running = false;
vm->ended = true;
return BAS_VM_HALTED;
case OP_HALT:
vm->running = false;
return BAS_VM_HALTED;
@ -2974,16 +2979,17 @@ static BasVmResultE execArith(BasVmT *vm, uint8_t op) {
BasStringT *sa = a.strVal ? a.strVal : basStringNew("", 0);
BasStringT *sb = b.strVal ? b.strVal : basStringNew("", 0);
int32_t newLen = sa->len + sb->len;
BasStringT *cat = basStringNew("", 0);
BasStringT *cat;
if (newLen > 0) {
basStringUnref(cat);
char *buf = (char *)malloc(newLen + 1);
memcpy(buf, sa->data, sa->len);
memcpy(buf + sa->len, sb->data, sb->len);
buf[newLen] = '\0';
cat = basStringNew(buf, newLen);
free(buf);
} else {
cat = basStringNew("", 0);
}
basValRelease(&a);

View file

@ -248,6 +248,7 @@ typedef struct {
// Execution
int32_t pc; // program counter
bool running;
bool ended; // END statement executed -- program should terminate
bool yielded;
int32_t stepLimit; // max steps per basVmRun (0 = unlimited)
int32_t stepCount; // steps executed in last basVmRun

View file

@ -112,7 +112,7 @@ static int32_t sAppCount = 0;
int32_t appMain(DxeAppContextT *ctx);
static void buildPmWindow(void);
static void desktopUpdate(void);
static void onAppButtonClick(WidgetT *w);
static void onAppButtonDblClick(WidgetT *w);
static void onPmClose(WindowT *win);
static void onPmMenu(WindowT *win, int32_t menuId);
static void scanAppsDir(void);
@ -233,8 +233,7 @@ static void buildPmWindow(void) {
}
btn->userData = &sAppFiles[i];
btn->onDblClick = onAppButtonClick;
btn->onClick = onAppButtonClick;
btn->onDblClick = onAppButtonDblClick;
if (sAppFiles[i].tooltip[0]) {
wgtSetTooltip(btn, sAppFiles[i].tooltip);
@ -277,7 +276,7 @@ static void desktopUpdate(void) {
// Widget click handler for app grid buttons. userData was set to the
// AppEntryT pointer during window construction, giving us the .app path.
static void onAppButtonClick(WidgetT *w) {
static void onAppButtonDblClick(WidgetT *w) {
AppEntryT *entry = (AppEntryT *)w->userData;
if (!entry) {

View file

@ -1546,9 +1546,9 @@ static void drawPopupLevel(AppContextT *ctx, DisplayT *d, const BlitOpsT *ops, c
}
uint32_t bg = ctx->colors.menuBg;
uint32_t fg = ctx->colors.menuFg;
uint32_t fg = item->enabled ? ctx->colors.menuFg : ctx->colors.windowShadow;
if (k == hoverItem) {
if (k == hoverItem && item->enabled) {
bg = ctx->colors.menuHighlightBg;
fg = ctx->colors.menuHighlightFg;
}
@ -1845,9 +1845,15 @@ static void handleMouseButton(AppContextT *ctx, int32_t mx, int32_t my, int32_t
} else {
ctx->lastTitleClickTime = now;
ctx->lastTitleClickId = win->id;
// Don't start a drag on a maximized window --
// dragging clears the maximized flag, which
// prevents the double-click restore from working.
if (!win->maximized) {
wmDragBegin(&ctx->stack, hitIdx, mx, my);
}
}
}
break;
case HIT_CLOSE:
@ -3942,6 +3948,21 @@ void dvxDestroyWindow(AppContextT *ctx, WindowT *win) {
}
// ============================================================
// dvxRaiseWindow
// ============================================================
void dvxRaiseWindow(AppContextT *ctx, WindowT *win) {
for (int32_t i = 0; i < ctx->stack.count; i++) {
if (ctx->stack.windows[i] == win) {
wmRaiseWindow(&ctx->stack, &ctx->dirty, i);
wmSetFocus(&ctx->stack, &ctx->dirty, ctx->stack.count - 1);
return;
}
}
}
// ============================================================
// dvxFitWindow
// ============================================================

View file

@ -203,6 +203,9 @@ WindowT *dvxCreateWindowCentered(AppContextT *ctx, const char *title, int32_t w,
// Destroy a window, free all its resources, and dirty its former region.
void dvxDestroyWindow(AppContextT *ctx, WindowT *win);
// Raise a window to the top of the z-order and give it focus.
void dvxRaiseWindow(AppContextT *ctx, WindowT *win);
// Resize a window to exactly fit its widget tree's computed minimum size
// (plus chrome). Used for dialog boxes and other fixed-layout windows
// where the window should shrink-wrap its content.

View file

@ -552,6 +552,7 @@ const void *wgtGetApi(const char *name);
#define WGT_IFACE_INT 1
#define WGT_IFACE_BOOL 2
#define WGT_IFACE_FLOAT 3
#define WGT_IFACE_ENUM 4 // int32_t with named values
// Method calling conventions (how the form runtime marshals args)
#define WGT_SIG_VOID 0 // void fn(WidgetT *)
@ -570,6 +571,7 @@ typedef struct {
uint8_t type; // WGT_IFACE_*
void *getFn; // getter function pointer (NULL if write-only)
void *setFn; // setter function pointer (NULL if read-only)
const char **enumNames; // WGT_IFACE_ENUM only: NULL-terminated array of value names
} WgtPropDescT;
// Method descriptor

View file

@ -143,6 +143,30 @@ void widgetPaintPopupList(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *f
drawText(d, ops, font, textX, iy, items[idx], ifg, ibg, false);
}
// Draw scroll indicators if the list extends beyond visible area
if (itemCount > visibleItems) {
int32_t cx = popX + popW / 2;
uint32_t arrowC = colors->menuHighlightBg;
// Up triangle (point at top, wide at bottom)
if (scrollPos > 0) {
int32_t ty = popY + 2;
for (int32_t i = 0; i < 3; i++) {
drawHLine(d, ops, cx - i, ty + i, 1 + i * 2, arrowC);
}
}
// Down triangle (wide at top, point at bottom)
if (scrollPos + visibleItems < itemCount) {
int32_t by = popY + popH - 4;
for (int32_t i = 0; i < 3; i++) {
drawHLine(d, ops, cx - i, by - i, 1 + i * 2, arrowC);
}
}
}
}

View file

@ -122,7 +122,7 @@ void widgetDropdownOnKey(WidgetT *w, int32_t key, int32_t mod) {
}
} else {
// Popup is closed
if (key == (0x50 | 0x100) || key == ' ' || key == 0x0D) {
if (key == ' ' || key == 0x0D) {
d->open = true;
d->hoverIdx = d->selectedIdx;
sOpenPopup = w;
@ -134,6 +134,15 @@ void widgetDropdownOnKey(WidgetT *w, int32_t key, int32_t mod) {
if (d->hoverIdx < d->scrollPos) {
d->scrollPos = d->hoverIdx;
}
} else if (key == (0x50 | 0x100)) {
// Down arrow: cycle selection forward (wheel-friendly)
if (d->selectedIdx < d->itemCount - 1) {
d->selectedIdx++;
if (w->onChange) {
w->onChange(w);
}
}
} else if (key == (0x48 | 0x100)) {
if (d->selectedIdx > 0) {
d->selectedIdx--;

View file

@ -147,6 +147,16 @@ void wgtLabelSetAlign(WidgetT *w, WidgetAlignE align) {
}
int32_t wgtLabelGetAlign(const WidgetT *w) {
if (w && w->type == sTypeId) {
LabelDataT *d = (LabelDataT *)w->data;
return (int32_t)d->textAlign;
}
return 0;
}
// ============================================================
// DXE registration
// ============================================================
@ -160,8 +170,10 @@ static const struct {
.setAlign = wgtLabelSetAlign
};
static const char *sAlignNames[] = { "Left", "Center", "Right", NULL };
static const WgtPropDescT sProps[] = {
{ "Alignment", WGT_IFACE_INT, NULL, (void *)wgtLabelSetAlign }
{ "Alignment", WGT_IFACE_ENUM, (void *)wgtLabelGetAlign, (void *)wgtLabelSetAlign, sAlignNames }
};
static const WgtIfaceT sIface = {