diff --git a/apps/dvxbasic/formrt/formrt.c b/apps/dvxbasic/formrt/formrt.c index a33f8ae..dd07baa 100644 --- a/apps/dvxbasic/formrt/formrt.c +++ b/apps/dvxbasic/formrt/formrt.c @@ -66,7 +66,9 @@ static void freeListBoxItems(BasControlT *ctrl); static BasValueT getCommonProp(BasControlT *ctrl, const char *propName, bool *handled); static BasValueT getIfaceProp(const WgtIfaceT *iface, WidgetT *w, const char *propName, bool *handled); static ListBoxItemsT *getListBoxItems(BasControlT *ctrl); +static void onFormActivate(WindowT *win); static void onFormClose(WindowT *win); +static void onFormDeactivate(WindowT *win); static void onFormResize(WindowT *win, int32_t newW, int32_t newH); static void onWidgetBlur(WidgetT *w); static void onWidgetChange(WidgetT *w); @@ -75,6 +77,7 @@ 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); @@ -392,6 +395,57 @@ bool basFormRtFireEventArgs(BasFormRtT *rt, BasFormT *form, const char *ctrlName } +// ============================================================ +// 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; + rt->currentForm = form; + basVmSetCurrentForm(rt->vm, form); + + 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); + return cancelled; +} + + // ============================================================ // basFormRtGetProp // ============================================================ @@ -515,6 +569,8 @@ void *basFormRtLoadForm(void *ctx, const char *formName) { snprintf(form->name, BAS_MAX_CTRL_NAME, "%s", formName); win->onClose = onFormClose; win->onResize = onFormResize; + win->onFocus = onFormActivate; + win->onBlur = onFormDeactivate; form->window = win; form->root = root; form->contentBox = NULL; // created lazily after Layout property is known @@ -679,6 +735,7 @@ BasFormT *basFormRtLoadFrm(BasFormRtT *rt, const char *source, int32_t sourceLen widget->onBlur = onWidgetBlur; widget->onKeyPress = onWidgetKeyPress; widget->onKeyDown = onWidgetKeyDown; + widget->onKeyUp = onWidgetKeyUp; widget->onMouseDown = onWidgetMouseDown; widget->onMouseUp = onWidgetMouseUp; widget->onMouseMove = onWidgetMouseMove; @@ -1296,9 +1353,6 @@ static ListBoxItemsT *getListBoxItems(BasControlT *ctrl) { // Form_Unload event and stops the VM so the program exits. static void onFormClose(WindowT *win) { - // Find which form owns this window - // The window's userData stores nothing useful, so we search - // by window pointer. We get the form runtime from sFormRt. if (!sFormRt) { return; } @@ -1307,6 +1361,11 @@ static void onFormClose(WindowT *win) { 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"); // Free control resources @@ -1367,6 +1426,42 @@ static void onFormResize(WindowT *win, int32_t newW, int32_t newH) { } +// ============================================================ +// 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; + } + } +} + + // ============================================================ // onWidgetBlur // ============================================================ @@ -1408,7 +1503,9 @@ static void onWidgetChange(WidgetT *w) { } if (rt) { - basFormRtFireEvent(rt, ctrl->form, ctrl->name, "Change"); + // Timer widgets fire "Timer" event, everything else fires "Change" + const char *evtName = (strcasecmp(ctrl->typeName, "Timer") == 0) ? "Timer" : "Change"; + basFormRtFireEvent(rt, ctrl->form, ctrl->name, evtName); } } @@ -1521,6 +1618,28 @@ static void onWidgetKeyDown(WidgetT *w, int32_t keyCode, int32_t shift) { } +// ============================================================ +// onWidgetKeyUp +// ============================================================ + +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); + basFormRtFireEventArgs(rt, ctrl->form, ctrl->name, "KeyUp", args, 2); + } +} + + // ============================================================ // onWidgetMouseDown / onWidgetMouseUp / onWidgetMouseMove // ============================================================ diff --git a/apps/dvxbasic/ide/ideMain.c b/apps/dvxbasic/ide/ideMain.c index caed4e4..f9feed4 100644 --- a/apps/dvxbasic/ide/ideMain.c +++ b/apps/dvxbasic/ide/ideMain.c @@ -91,10 +91,26 @@ #define IDE_DESIGN_W 400 #define IDE_DESIGN_H 300 +// Syntax color indices (used by basicColorize / classifyWord) +#define SYNTAX_DEFAULT 0 +#define SYNTAX_KEYWORD 1 +#define SYNTAX_STRING 2 +#define SYNTAX_COMMENT 3 +#define SYNTAX_NUMBER 4 +#define SYNTAX_TYPE 6 + +// View mode for activateFile +typedef enum { + ViewAutoE, // .frm -> design, .bas -> code + ViewCodeE, // force code view + ViewDesignE // force design view +} IdeViewModeE; + // ============================================================ // Prototypes // ============================================================ +static void activateFile(int32_t fileIdx, IdeViewModeE view); int32_t appMain(DxeAppContextT *ctx); static void buildWindow(void); static void clearOutput(void); @@ -104,14 +120,15 @@ static void ensureProject(const char *filePath); static void freeProcBufs(void); static const char *getFullSource(void); static void loadFile(void); +static void loadFormCodeIntoEditor(void); static void parseProcs(const char *source); static void updateProjectMenuState(void); static void saveActiveFile(void); static bool saveCurProc(void); +static void stashCurrentFile(void); static void stashFormCode(void); static void showProc(int32_t procIdx); static int32_t toolbarBottom(void); -static void loadFilePath(const char *path); static void newProject(void); static void onPrjFileClick(int32_t fileIdx, bool isForm); static void openProject(void); @@ -128,6 +145,12 @@ static void onFormWinClose(WindowT *win); static void onFormWinResize(WindowT *win, int32_t newW, int32_t newH); static void onProjectWinClose(WindowT *win); static WindowT *getLastFocusWin(void); +static void handleEditCmd(int32_t cmd); +static void handleFileCmd(int32_t cmd); +static void handleProjectCmd(int32_t cmd); +static void handleRunCmd(int32_t cmd); +static void handleViewCmd(int32_t cmd); +static void handleWindowCmd(int32_t cmd); static void onMenu(WindowT *win, int32_t menuId); static void basicColorize(const char *line, int32_t lineLen, uint8_t *colors, void *ctx); static void evaluateImmediate(const char *expr); @@ -142,7 +165,8 @@ static void runCached(void); static void runModule(BasModuleT *mod); static void onEditorChange(WidgetT *w); static void setStatus(const char *text); -static void switchToCode(void); +static void stashDesignerState(void); +static void teardownFormWin(void); static void updateDirtyIndicators(void); static void switchToDesign(void); static int32_t onFormWinCursorQuery(WindowT *win, int32_t x, int32_t y); @@ -225,6 +249,163 @@ AppDescriptorT appDescriptor = { .priority = 0 }; +// ============================================================ +// activateFile -- central file-switching function +// ============================================================ + +static void activateFile(int32_t fileIdx, IdeViewModeE view) { + if (fileIdx < 0 || fileIdx >= sProject.fileCount) { + return; + } + + PrjFileT *target = &sProject.files[fileIdx]; + + // Resolve ViewAutoE + if (view == ViewAutoE) { + view = target->isForm ? ViewDesignE : ViewCodeE; + } + + // If already active, just ensure the right view is showing + if (fileIdx == sProject.activeFileIdx) { + if (view == ViewDesignE) { + switchToDesign(); + } + return; + } + + // PHASE 1: Stash the outgoing file + stashCurrentFile(); + + // PHASE 2: Load the incoming file + if (target->isForm) { + // Load form into designer + const char *frmSrc = target->buffer; + char *diskBuf = NULL; + + if (!frmSrc) { + // Try disk + char fullPath[DVX_MAX_PATH]; + prjFullPath(&sProject, fileIdx, fullPath, sizeof(fullPath)); + FILE *f = fopen(fullPath, "r"); + + if (f) { + fseek(f, 0, SEEK_END); + long size = ftell(f); + fseek(f, 0, SEEK_SET); + + if (size > 0 && size < IDE_MAX_SOURCE) { + diskBuf = (char *)malloc(size + 1); + + if (diskBuf) { + int32_t br = (int32_t)fread(diskBuf, 1, size, f); + diskBuf[br] = '\0'; + frmSrc = diskBuf; + } + } + + fclose(f); + } + } + + teardownFormWin(); + dsgnFree(&sDesigner); + + if (frmSrc) { + dsgnLoadFrm(&sDesigner, frmSrc, (int32_t)strlen(frmSrc)); + free(diskBuf); + } else { + // New blank form -- derive name from filename + char formName[PRJ_MAX_NAME]; + const char *base = strrchr(target->path, '/'); + const char *base2 = strrchr(target->path, '\\'); + + if (base2 > base) { + base = base2; + } + + base = base ? base + 1 : target->path; + int32_t bl = (int32_t)strlen(base); + + if (bl >= PRJ_MAX_NAME) { + bl = PRJ_MAX_NAME - 1; + } + + memcpy(formName, base, bl); + formName[bl] = '\0'; + char *dot = strrchr(formName, '.'); + + if (dot) { + *dot = '\0'; + } + + dsgnNewForm(&sDesigner, formName); + target->modified = true; + } + + if (sDesigner.form) { + snprintf(target->formName, sizeof(target->formName), "%s", sDesigner.form->name); + } + + sProject.activeFileIdx = fileIdx; + + if (view == ViewDesignE) { + switchToDesign(); + } else { + loadFormCodeIntoEditor(); + } + } else { + // Load .bas into editor + if (!sCodeWin) { + showCodeWindow(); + } + + const char *source = target->buffer; + char *diskBuf = NULL; + + if (!source) { + char fullPath[DVX_MAX_PATH]; + prjFullPath(&sProject, fileIdx, fullPath, sizeof(fullPath)); + FILE *f = fopen(fullPath, "r"); + + if (f) { + fseek(f, 0, SEEK_END); + long size = ftell(f); + fseek(f, 0, SEEK_SET); + + if (size > 0 && size < IDE_MAX_SOURCE) { + diskBuf = (char *)malloc(size + 1); + + if (diskBuf) { + int32_t br = (int32_t)fread(diskBuf, 1, size, f); + diskBuf[br] = '\0'; + source = diskBuf; + } + } + + fclose(f); + } + } + + if (!source) { + source = ""; + target->modified = true; + } + + parseProcs(source); + free(diskBuf); + updateDropdowns(); + showProc(-1); + + if (sEditor && !sEditor->onChange) { + sEditor->onChange = onEditorChange; + } + + sEditorFileIdx = fileIdx; + sProject.activeFileIdx = fileIdx; + } +} + + // ============================================================ // appMain // ============================================================ @@ -445,7 +626,7 @@ static void buildWindow(void) { // line and fills the colors array with syntax color indices. // Hash-based keyword/type lookup using stb_ds. -// Key = uppercase word, value = syntax color (1=keyword, 6=type). +// Key = uppercase word, value = SYNTAX_KEYWORD or SYNTAX_TYPE. // Built once on first use, then O(1) per lookup. typedef struct { @@ -494,11 +675,11 @@ static void initSyntaxMap(void) { }; for (int32_t i = 0; keywords[i]; i++) { - shput(sSyntaxMap, keywords[i], 1); + shput(sSyntaxMap, keywords[i], SYNTAX_KEYWORD); } for (int32_t i = 0; types[i]; i++) { - shput(sSyntaxMap, types[i], 6); + shput(sSyntaxMap, types[i], SYNTAX_TYPE); } } @@ -510,7 +691,7 @@ static uint8_t classifyWord(const char *word, int32_t wordLen) { char upper[32]; if (wordLen <= 0 || wordLen >= 32) { - return 0; + return SYNTAX_DEFAULT; } for (int32_t i = 0; i < wordLen; i++) { @@ -527,7 +708,7 @@ static uint8_t classifyWord(const char *word, int32_t wordLen) { return sSyntaxMap[idx].value; } - return 0; + return SYNTAX_DEFAULT; } @@ -541,21 +722,21 @@ static void basicColorize(const char *line, int32_t lineLen, uint8_t *colors, vo // Comment: ' or REM if (ch == '\'') { while (i < lineLen) { - colors[i++] = 3; // SYNTAX_COMMENT + colors[i++] = SYNTAX_COMMENT; } return; } // String literal if (ch == '"') { - colors[i++] = 2; // SYNTAX_STRING + colors[i++] = SYNTAX_STRING; while (i < lineLen && line[i] != '"') { - colors[i++] = 2; + colors[i++] = SYNTAX_STRING; } if (i < lineLen) { - colors[i++] = 2; // closing quote + colors[i++] = SYNTAX_STRING; } continue; @@ -564,7 +745,7 @@ static void basicColorize(const char *line, int32_t lineLen, uint8_t *colors, vo // Number if (isdigit((unsigned char)ch) || (ch == '.' && i + 1 < lineLen && isdigit((unsigned char)line[i + 1]))) { while (i < lineLen && (isdigit((unsigned char)line[i]) || line[i] == '.')) { - colors[i++] = 4; // SYNTAX_NUMBER + colors[i++] = SYNTAX_NUMBER; } continue; @@ -583,7 +764,7 @@ static void basicColorize(const char *line, int32_t lineLen, uint8_t *colors, vo // Check for REM comment if (wordLen == 3 && (line[start] == 'R' || line[start] == 'r') && (line[start + 1] == 'E' || line[start + 1] == 'e') && (line[start + 2] == 'M' || line[start + 2] == 'm')) { for (int32_t j = start; j < lineLen; j++) { - colors[j] = 3; // SYNTAX_COMMENT + colors[j] = SYNTAX_COMMENT; } return; } @@ -698,20 +879,9 @@ static void compileAndRun(void) { int32_t srcLen = 0; if (sProject.projectPath[0] != '\0' && sProject.fileCount > 0) { - // Stash current editor state to the file that owns the proc buffers - if (sEditorFileIdx >= 0 && sEditorFileIdx < sProject.fileCount) { - PrjFileT *edFile = &sProject.files[sEditorFileIdx]; - - if (!edFile->isForm) { - saveCurProc(); - const char *fullSrc = getFullSource(); - free(edFile->buffer); - edFile->buffer = fullSrc ? strdup(fullSrc) : NULL; - } - } - - // Stash form code if the editor has form code loaded - stashFormCode(); + // Stash current editor/designer state into project buffers + stashCurrentFile(); + stashFormCode(); // also stash form code if editor has it // Concatenate all .bas files from buffers (or disk if not yet loaded) concatBuf = (char *)malloc(IDE_MAX_SOURCE); @@ -1217,64 +1387,6 @@ static bool inputCallback(void *ctx, const char *prompt, char *buf, int32_t bufS return dvxInputBox(sAc, "DVX BASIC", prompt ? prompt : "Enter value:", NULL, buf, bufSize); } -// ============================================================ -// loadFile -// ============================================================ - -static void loadFilePath(const char *path) { - FILE *f = fopen(path, "r"); - - if (!f) { - return; - } - - fseek(f, 0, SEEK_END); - long size = ftell(f); - fseek(f, 0, SEEK_SET); - - if (size >= IDE_MAX_SOURCE - 1) { - fclose(f); - return; - } - - char *srcBuf = (char *)malloc(size + 1); - - if (!srcBuf) { - fclose(f); - return; - } - - int32_t bytesRead = (int32_t)fread(srcBuf, 1, size, f); - fclose(f); - srcBuf[bytesRead] = '\0'; - - if (!sCodeWin) { - showCodeWindow(); - } - - // Stash form code before overwriting proc buffers - stashFormCode(); - - // Parse into per-procedure buffers and show (General) section - parseProcs(srcBuf); - free(srcBuf); - - if (sFormWin) { - dvxDestroyWindow(sAc, sFormWin); - cleanupFormWin(); - } - - dsgnFree(&sDesigner); - updateDropdowns(); - showProc(-1); // show (General) section - - if (sEditor && !sEditor->onChange) { - sEditor->onChange = onEditorChange; - } - - setStatus("File loaded."); -} - // Auto-create an implicit project when opening a file without one. // Derives project name and directory from the file path. Also adds @@ -1409,9 +1521,7 @@ static void loadFile(void) { prjAddFile(&sProject, fileName, isForm); prjRebuildTree(&sProject); - - int32_t fileIdx = sProject.fileCount - 1; - onPrjFileClick(fileIdx, isForm); + activateFile(sProject.fileCount - 1, ViewAutoE); } else { // No project -- create one from this file if (!promptAndSave()) { @@ -1419,13 +1529,7 @@ static void loadFile(void) { } ensureProject(path); - - if (isForm) { - onPrjFileClick(0, true); - } else { - loadFilePath(path); - sEditorFileIdx = 0; - } + activateFile(0, ViewAutoE); } } @@ -1444,68 +1548,27 @@ static void saveActiveFile(void) { return; } + // Ensure buffer is up-to-date with editor/designer state + stashCurrentFile(); + PrjFileT *file = &sProject.files[idx]; char fullPath[DVX_MAX_PATH]; prjFullPath(&sProject, idx, fullPath, sizeof(fullPath)); - 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 (file->buffer) { + FILE *f = fopen(fullPath, "w"); - if (isDesignerForm) { - stashFormCode(); + if (f) { + fputs(file->buffer, f); + fclose(f); + file->modified = false; - char *frmBuf = (char *)malloc(IDE_MAX_SOURCE); - - if (frmBuf) { - int32_t frmLen = dsgnSaveFrm(&sDesigner, frmBuf, IDE_MAX_SOURCE); - - if (frmLen > 0) { - FILE *f = fopen(fullPath, "w"); - - if (f) { - fwrite(frmBuf, 1, frmLen, f); - fclose(f); - sDesigner.form->dirty = false; - file->modified = false; - } - } - - free(frmBuf); + if (file->isForm && sDesigner.form) { + sDesigner.form->dirty = false; } - } 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"); - - if (f) { - fputs(src, f); - fclose(f); - file->modified = false; - } else { - dvxMessageBox(sAc, "Error", "Could not write file.", MB_OK | MB_ICONERROR); - return; - } + dvxMessageBox(sAc, "Error", "Could not write file.", MB_OK | MB_ICONERROR); + return; } } @@ -1550,190 +1613,8 @@ static void saveFile(void) { // ============================================================ static void onPrjFileClick(int32_t fileIdx, bool isForm) { - if (fileIdx < 0 || fileIdx >= sProject.fileCount) { - return; - } - - 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. - // 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); - - if (frmBuf) { - int32_t frmLen = dsgnSaveFrm(&sDesigner, frmBuf, IDE_MAX_SOURCE); - - free(cur->buffer); - - if (frmLen > 0) { - frmBuf[frmLen] = '\0'; - cur->buffer = frmBuf; - } else { - free(frmBuf); - cur->buffer = NULL; - } - } - } 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; - } - } - - PrjFileT *target = &sProject.files[fileIdx]; - - if (isForm) { - // Load form from buffer or disk, or create a new blank form - const char *frmSrc = target->buffer; - char *diskBuf = NULL; - - if (!frmSrc) { - char fullPath[DVX_MAX_PATH]; - prjFullPath(&sProject, fileIdx, fullPath, sizeof(fullPath)); - - FILE *f = fopen(fullPath, "r"); - - if (!f) { - // File doesn't exist yet -- create a new blank form. - // Derive form name from the filename (strip path and extension). - char formName[PRJ_MAX_NAME]; - const char *base = strrchr(target->path, '/'); - const char *base2 = strrchr(target->path, '\\'); - - if (base2 > base) { - base = base2; - } - - base = base ? base + 1 : target->path; - - // Length-clamped memcpy instead of strncpy/snprintf because - // GCC warns about both when source (DVX_MAX_PATH) exceeds - // the buffer (PRJ_MAX_NAME), even though truncation is safe. - int32_t bl = (int32_t)strlen(base); - - if (bl >= PRJ_MAX_NAME) { - bl = PRJ_MAX_NAME - 1; - } - - memcpy(formName, base, bl); - formName[bl] = '\0'; - char *dot = strrchr(formName, '.'); - - if (dot) { - *dot = '\0'; - } - - if (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(); - return; - } - - fseek(f, 0, SEEK_END); - long size = ftell(f); - fseek(f, 0, SEEK_SET); - - if (size <= 0 || size >= IDE_MAX_SOURCE) { - fclose(f); - return; - } - - diskBuf = (char *)malloc(size + 1); - - if (!diskBuf) { - fclose(f); - return; - } - - int32_t bytesRead = (int32_t)fread(diskBuf, 1, size, f); - fclose(f); - diskBuf[bytesRead] = '\0'; - frmSrc = diskBuf; - } - - // Close the old form designer window before loading a new form - if (sFormWin) { - dvxDestroyWindow(sAc, sFormWin); - cleanupFormWin(); - } - - if (sDesigner.form) { - dsgnFree(&sDesigner); - } - - 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(); - } - - if (target->buffer) { - parseProcs(target->buffer); - updateDropdowns(); - showProc(-1); - } else { - char fullPath[DVX_MAX_PATH]; - prjFullPath(&sProject, fileIdx, fullPath, sizeof(fullPath)); - - FILE *f = fopen(fullPath, "r"); - - if (f) { - fclose(f); - loadFilePath(fullPath); - } else { - // File doesn't exist yet -- start with empty source - parseProcs(""); - updateDropdowns(); - showProc(-1); - target->modified = true; - } - } - - if (sEditor && !sEditor->onChange) { - sEditor->onChange = onEditorChange; - } - - sEditorFileIdx = fileIdx; - sProject.activeFileIdx = fileIdx; - } + (void)isForm; // activateFile determines view from file type + activateFile(fileIdx, ViewAutoE); } @@ -2225,6 +2106,34 @@ static void loadFrmFiles(BasFormRtT *rt) { } +// ============================================================ +// loadFormCodeIntoEditor -- loads form code into proc buffers + editor +// ============================================================ + +static void loadFormCodeIntoEditor(void) { + if (!sDesigner.form) { + return; + } + + stashFormCode(); + parseProcs(sDesigner.form->code ? sDesigner.form->code : ""); + sEditorFileIdx = sProject.activeFileIdx; + + if (!sCodeWin) { + showCodeWindow(); + } + + bool saved = sDropdownNavSuppressed; + sDropdownNavSuppressed = true; + updateDropdowns(); + sDropdownNavSuppressed = saved; + + showProc(-1); + + if (sEditor && !sEditor->onChange) { + sEditor->onChange = onEditorChange; + } +} // ============================================================ @@ -2383,10 +2292,8 @@ static void onClose(WindowT *win) { // onMenu // ============================================================ -static void onMenu(WindowT *win, int32_t menuId) { - (void)win; - - switch (menuId) { +static void handleFileCmd(int32_t cmd) { + switch (cmd) { case CMD_OPEN: loadFile(); break; @@ -2398,7 +2305,6 @@ static void onMenu(WindowT *win, int32_t menuId) { case CMD_SAVE_ALL: saveFile(); - // Save all non-active files from their buffers for (int32_t i = 0; i < sProject.fileCount; i++) { if (i == sProject.activeFileIdx) { continue; @@ -2418,7 +2324,6 @@ static void onMenu(WindowT *win, int32_t menuId) { } } - // Save the project file if (sProject.projectPath[0] != '\0') { prjSave(&sProject); sProject.dirty = false; @@ -2428,6 +2333,51 @@ static void onMenu(WindowT *win, int32_t menuId) { updateDirtyIndicators(); break; + case CMD_EXIT: + if (sWin) { + onClose(sWin); + } + break; + } +} + + +static void handleEditCmd(int32_t cmd) { + switch (cmd) { + case CMD_CUT: + case CMD_COPY: + case CMD_PASTE: + case CMD_SELECT_ALL: { + static const int32_t keys[] = { 24, 3, 22, 1 }; + int32_t key = keys[cmd - CMD_CUT]; + WindowT *target = getLastFocusWin(); + + if (target && target->onKey) { + target->onKey(target, key, ACCEL_CTRL); + } + + break; + } + + case CMD_DELETE: + if (sFormWin && sDesigner.selectedIdx >= 0) { + int32_t prevCount = (int32_t)arrlen(sDesigner.form->controls); + dsgnOnKey(&sDesigner, KEY_DELETE); + int32_t newCount = (int32_t)arrlen(sDesigner.form->controls); + + if (newCount != prevCount) { + prpRebuildTree(&sDesigner); + prpRefresh(&sDesigner); + dvxInvalidateWindow(sAc, sFormWin); + } + } + break; + } +} + + +static void handleRunCmd(int32_t cmd) { + switch (cmd) { case CMD_RUN: compileAndRun(); break; @@ -2448,14 +2398,52 @@ static void onMenu(WindowT *win, int32_t menuId) { clearOutput(); break; + case CMD_SAVE_ON_RUN: + if (sWin && sWin->menuBar) { + bool save = wmMenuItemIsChecked(sWin->menuBar, CMD_SAVE_ON_RUN); + prefsSetBool(sPrefs, "run", "saveOnRun", save); + prefsSave(sPrefs); + } + break; + } +} + + +static void handleViewCmd(int32_t cmd) { + switch (cmd) { case CMD_VIEW_CODE: - switchToCode(); + stashDesignerState(); break; case CMD_VIEW_DESIGN: switchToDesign(); break; + case CMD_VIEW_TOOLBAR: + if (sToolbar && sWin->menuBar) { + bool show = wmMenuItemIsChecked(sWin->menuBar, CMD_VIEW_TOOLBAR); + sToolbar->visible = show; + dvxFitWindowH(sAc, sWin); + prefsSetBool(sPrefs, "view", "toolbar", show); + prefsSave(sPrefs); + } + break; + + case CMD_VIEW_STATUS: + if (sStatusBar && sWin->menuBar) { + bool show = wmMenuItemIsChecked(sWin->menuBar, CMD_VIEW_STATUS); + sStatusBar->visible = show; + dvxFitWindowH(sAc, sWin); + prefsSetBool(sPrefs, "view", "statusbar", show); + prefsSave(sPrefs); + } + break; + } +} + + +static void handleWindowCmd(int32_t cmd) { + switch (cmd) { case CMD_WIN_CODE: showCodeWindow(); if (sEditor && !sEditor->onChange) { @@ -2491,65 +2479,22 @@ static void onMenu(WindowT *win, int32_t menuId) { } break; - case CMD_CUT: - case CMD_COPY: - case CMD_PASTE: - case CMD_SELECT_ALL: { - // Send the corresponding Ctrl+key to the last focused content window - static const int32_t keys[] = { 24, 3, 22, 1 }; // Ctrl+X, C, V, A - int32_t key = keys[menuId - CMD_CUT]; + case CMD_WIN_PROJECT: + if (!sProjectWin) { + sProjectWin = prjCreateWindow(sAc, &sProject, onPrjFileClick); - WindowT *target = getLastFocusWin(); - - if (target && target->onKey) { - target->onKey(target, key, ACCEL_CTRL); - } - - break; - } - - case CMD_DELETE: - if (sFormWin && sDesigner.selectedIdx >= 0) { - int32_t prevCount = (int32_t)arrlen(sDesigner.form->controls); - dsgnOnKey(&sDesigner, KEY_DELETE); - int32_t newCount = (int32_t)arrlen(sDesigner.form->controls); - - if (newCount != prevCount) { - prpRebuildTree(&sDesigner); - prpRefresh(&sDesigner); - dvxInvalidateWindow(sAc, sFormWin); + if (sProjectWin) { + sProjectWin->y = toolbarBottom() + 25; + sProjectWin->onClose = onProjectWinClose; } } break; + } +} - case CMD_VIEW_TOOLBAR: - if (sToolbar && sWin->menuBar) { - bool show = wmMenuItemIsChecked(sWin->menuBar, CMD_VIEW_TOOLBAR); - sToolbar->visible = show; - dvxFitWindowH(sAc, sWin); - prefsSetBool(sPrefs, "view", "toolbar", show); - prefsSave(sPrefs); - } - break; - - case CMD_VIEW_STATUS: - if (sStatusBar && sWin->menuBar) { - bool show = wmMenuItemIsChecked(sWin->menuBar, CMD_VIEW_STATUS); - sStatusBar->visible = show; - dvxFitWindowH(sAc, sWin); - prefsSetBool(sPrefs, "view", "statusbar", show); - prefsSave(sPrefs); - } - break; - - case CMD_SAVE_ON_RUN: - if (sWin && sWin->menuBar) { - bool save = wmMenuItemIsChecked(sWin->menuBar, CMD_SAVE_ON_RUN); - prefsSetBool(sPrefs, "run", "saveOnRun", save); - prefsSave(sPrefs); - } - break; +static void handleProjectCmd(int32_t cmd) { + switch (cmd) { case CMD_PRJ_NEW: newProject(); break; @@ -2612,33 +2557,28 @@ static void onMenu(WindowT *win, int32_t menuId) { prjRebuildTree(&sProject); } break; + } +} - case CMD_WIN_PROJECT: - if (!sProjectWin) { - sProjectWin = prjCreateWindow(sAc, &sProject, onPrjFileClick); - if (sProjectWin) { - sProjectWin->y = toolbarBottom() + 25; - sProjectWin->onClose = onProjectWinClose; - } - } - break; +static void onMenu(WindowT *win, int32_t menuId) { + (void)win; - case CMD_EXIT: - if (sWin) { - onClose(sWin); - } - break; + handleFileCmd(menuId); + handleEditCmd(menuId); + handleRunCmd(menuId); + handleViewCmd(menuId); + handleWindowCmd(menuId); + handleProjectCmd(menuId); - case CMD_HELP_ABOUT: - dvxMessageBox(sAc, "About DVX BASIC", - "DVX BASIC 1.0\n" - "Visual BASIC Development Environment\n" - "for the DVX GUI System\n" - "\n" - "Copyright 2026 Scott Duensing", - MB_OK | MB_ICONINFO); - break; + if (menuId == CMD_HELP_ABOUT) { + dvxMessageBox(sAc, "About DVX BASIC", + "DVX BASIC 1.0\n" + "Visual BASIC Development Environment\n" + "for the DVX GUI System\n" + "\n" + "Copyright 2026 Scott Duensing", + MB_OK | MB_ICONINFO); } } @@ -2775,15 +2715,15 @@ static void onImmediateChange(WidgetT *w) { // Common events available on all controls static const char *sCommonEvents[] = { "Click", "DblClick", "Change", "GotFocus", "LostFocus", - "KeyPress", "KeyDown", + "KeyPress", "KeyDown", "KeyUp", "MouseDown", "MouseUp", "MouseMove", "Scroll", NULL }; // Form-specific events static const char *sFormEvents[] = { - "Load", "Unload", "Resize", "Activate", "Deactivate", - "KeyPress", "KeyDown", + "Load", "QueryUnload", "Unload", "Resize", "Activate", "Deactivate", + "KeyPress", "KeyDown", "KeyUp", "MouseDown", "MouseUp", "MouseMove", NULL }; @@ -3230,29 +3170,14 @@ static void navigateToEventSub(void) { char subName[128]; snprintf(subName, sizeof(subName), "%s_%s", ctrlName, eventName); - // 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) { - showCodeWindow(); - } + // Load form code into editor (stashes existing code, parses procs, + // populates dropdowns without triggering navigation) + loadFormCodeIntoEditor(); if (!sEditor) { return; } - // Populate dropdown items without triggering navigation -- - // we navigate explicitly below after finding the target proc. - { - bool saved = sDropdownNavSuppressed; - sDropdownNavSuppressed = true; - updateDropdowns(); - sDropdownNavSuppressed = saved; - } - // Search for existing procedure int32_t procCount = (int32_t)arrlen(sProcTable); @@ -3261,7 +3186,7 @@ static void navigateToEventSub(void) { snprintf(fullName, sizeof(fullName), "%s_%s", sProcTable[i].objName, sProcTable[i].evtName); if (strcasecmp(fullName, subName) == 0) { - switchToCode(); + stashDesignerState(); showProc(i); if (sEditor && !sEditor->onChange) { sEditor->onChange = onEditorChange; @@ -3280,7 +3205,7 @@ static void navigateToEventSub(void) { arrput(sProcBufs, strdup(skeleton)); // Show the new procedure (it's the last one) - switchToCode(); + stashDesignerState(); showProc((int32_t)arrlen(sProcBufs) - 1); if (sEditor && !sEditor->onChange) { sEditor->onChange = onEditorChange; @@ -3420,37 +3345,11 @@ static void onFormWinClose(WindowT *win) { // ============================================================ -// switchToCode +// stashDesignerState -- save current editor content and set status // ============================================================ -static void switchToCode(void) { - // Stash form data so the project system has a current copy. - // This does not mark the file as modified -- it's just caching. - if (sDesigner.form && sProject.activeFileIdx >= 0) { - PrjFileT *cur = &sProject.files[sProject.activeFileIdx]; - - if (cur->isForm) { - char *frmBuf = (char *)malloc(IDE_MAX_SOURCE); - - if (frmBuf) { - int32_t frmLen = dsgnSaveFrm(&sDesigner, frmBuf, IDE_MAX_SOURCE); - - free(cur->buffer); - - if (frmLen > 0) { - frmBuf[frmLen] = '\0'; - cur->buffer = frmBuf; - } else { - free(frmBuf); - cur->buffer = NULL; - } - } - } - } - - // Don't destroy the form window -- allow both code and design - // to be open simultaneously, like VB3. - +static void stashDesignerState(void) { + stashCurrentFile(); setStatus("Code view."); } @@ -3548,6 +3447,18 @@ static void switchToDesign(void) { } +// ============================================================ +// teardownFormWin -- destroy the form designer window if it exists +// ============================================================ + +static void teardownFormWin(void) { + if (sFormWin) { + dvxDestroyWindow(sAc, sFormWin); + cleanupFormWin(); + } +} + + // ============================================================ // Toolbar button handlers // ============================================================ @@ -3556,7 +3467,7 @@ static void onTbOpen(WidgetT *w) { (void)w; loadFile(); } static void onTbSave(WidgetT *w) { (void)w; saveFile(); } static void onTbRun(WidgetT *w) { (void)w; compileAndRun(); } static void onTbStop(WidgetT *w) { (void)w; sStopRequested = true; if (sVm) { sVm->running = false; } setStatus("Program stopped."); } -static void onTbCode(WidgetT *w) { (void)w; switchToCode(); } +static void onTbCode(WidgetT *w) { (void)w; stashDesignerState(); } static void onTbDesign(WidgetT *w) { (void)w; switchToDesign(); } @@ -4086,6 +3997,47 @@ static char *extractNewProcs(const char *buf) { } +// stashCurrentFile -- stash the currently active file's editor/designer +// state back into its project buffer. This is caching only -- does not +// mark modified. + +static void stashCurrentFile(void) { + if (sProject.activeFileIdx < 0 || sProject.activeFileIdx >= sProject.fileCount) { + return; + } + + 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); + + if (frmBuf) { + int32_t frmLen = dsgnSaveFrm(&sDesigner, frmBuf, IDE_MAX_SOURCE); + + free(cur->buffer); + + if (frmLen > 0) { + frmBuf[frmLen] = '\0'; + cur->buffer = frmBuf; + } else { + free(frmBuf); + cur->buffer = NULL; + } + } + } 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; + } +} + + // 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. diff --git a/apps/dvxbasic/runtime/vm.c b/apps/dvxbasic/runtime/vm.c index b9d014c..76231ce 100644 --- a/apps/dvxbasic/runtime/vm.c +++ b/apps/dvxbasic/runtime/vm.c @@ -70,27 +70,16 @@ bool basVmCallSub(BasVmT *vm, int32_t codeAddr) { vm->pc = codeAddr; vm->running = true; - // Step until the SUB returns (callDepth drops back) - int32_t steps = 0; - + // Run until the SUB returns (callDepth drops back). + // No step limit -- event handlers must run to completion. while (vm->running && vm->callDepth > savedCallDepth) { - if (vm->stepLimit > 0 && steps >= vm->stepLimit) { - // Unwind the call and restore state - vm->callDepth = savedCallDepth; - vm->pc = savedPc; - vm->running = savedRunning; - return false; - } - BasVmResultE result = basVmStep(vm); - steps++; if (result == BAS_VM_HALTED) { break; } if (result != BAS_VM_OK) { - // Runtime error in the event handler vm->pc = savedPc; vm->callDepth = savedCallDepth; vm->running = savedRunning; @@ -98,7 +87,6 @@ bool basVmCallSub(BasVmT *vm, int32_t codeAddr) { } } - // Restore VM state vm->pc = savedPc; vm->running = savedRunning; return true; @@ -139,18 +127,8 @@ bool basVmCallSubWithArgs(BasVmT *vm, int32_t codeAddr, const BasValueT *args, i vm->pc = codeAddr; vm->running = true; - int32_t steps = 0; - while (vm->running && vm->callDepth > savedCallDepth) { - if (vm->stepLimit > 0 && steps >= vm->stepLimit) { - vm->callDepth = savedCallDepth; - vm->pc = savedPc; - vm->running = savedRunning; - return false; - } - BasVmResultE result = basVmStep(vm); - steps++; if (result == BAS_VM_HALTED) { break; @@ -170,6 +148,71 @@ bool basVmCallSubWithArgs(BasVmT *vm, int32_t codeAddr, const BasValueT *args, i } +// ============================================================ +// basVmCallSubWithArgsOut +// ============================================================ + +bool basVmCallSubWithArgsOut(BasVmT *vm, int32_t codeAddr, const BasValueT *args, int32_t argCount, BasValueT *outArgs, int32_t outCount) { + if (!vm || !vm->module) { + return false; + } + + if (codeAddr < 0 || codeAddr >= vm->module->codeLen) { + return false; + } + + if (vm->callDepth >= BAS_VM_CALL_STACK_SIZE - 1) { + return false; + } + + int32_t savedPc = vm->pc; + int32_t savedCallDepth = vm->callDepth; + bool savedRunning = vm->running; + + BasCallFrameT *frame = &vm->callStack[vm->callDepth++]; + frame->returnPc = savedPc; + frame->localCount = BAS_VM_MAX_LOCALS; + memset(frame->locals, 0, sizeof(frame->locals)); + + for (int32_t i = 0; i < argCount && i < BAS_VM_MAX_LOCALS; i++) { + frame->locals[i] = basValCopy(args[i]); + } + + vm->pc = codeAddr; + vm->running = true; + + while (vm->running && vm->callDepth > savedCallDepth) { + BasVmResultE result = basVmStep(vm); + + if (result == BAS_VM_HALTED) { + break; + } + + if (result != BAS_VM_OK) { + vm->pc = savedPc; + vm->callDepth = savedCallDepth; + vm->running = savedRunning; + return false; + } + } + + // Read back modified locals before restoring state. + // The frame at savedCallDepth still has the data even + // though callDepth was decremented by RET. + if (outArgs && outCount > 0) { + BasCallFrameT *doneFrame = &vm->callStack[savedCallDepth]; + + for (int32_t i = 0; i < outCount && i < BAS_VM_MAX_LOCALS; i++) { + outArgs[i] = basValCopy(doneFrame->locals[i]); + } + } + + vm->pc = savedPc; + vm->running = savedRunning; + return true; +} + + // ============================================================ // basVmCreate // ============================================================ diff --git a/apps/dvxbasic/runtime/vm.h b/apps/dvxbasic/runtime/vm.h index 40ad424..94e99a6 100644 --- a/apps/dvxbasic/runtime/vm.h +++ b/apps/dvxbasic/runtime/vm.h @@ -364,4 +364,9 @@ const char *basVmGetError(const BasVmT *vm); bool basVmCallSub(BasVmT *vm, int32_t codeAddr); bool basVmCallSubWithArgs(BasVmT *vm, int32_t codeAddr, const BasValueT *args, int32_t argCount); +// Call a SUB and read back modified argument values. +// outArgs receives copies of the locals after the SUB returns. +// outCount specifies how many args to read back. +bool basVmCallSubWithArgsOut(BasVmT *vm, int32_t codeAddr, const BasValueT *args, int32_t argCount, BasValueT *outArgs, int32_t outCount); + #endif // DVXBASIC_VM_H diff --git a/apps/dvxbasic/test_compiler.c b/apps/dvxbasic/test_compiler.c index e403043..8f90441 100644 --- a/apps/dvxbasic/test_compiler.c +++ b/apps/dvxbasic/test_compiler.c @@ -2273,6 +2273,167 @@ int main(void) { printf("\n"); } + // ============================================================ + // Coverage: Bare sub call (no CALL keyword, no parens) + // ============================================================ + + runProgram("Bare sub call", + "Sub Greet ()\n" + " PRINT \"hello\"\n" + "End Sub\n" + "\n" + "Greet\n" + ); + + // ============================================================ + // Coverage: Bare sub call with forward reference + // ============================================================ + + runProgram("Bare sub call forward ref", + "DoWork\n" + "\n" + "Sub DoWork ()\n" + " PRINT \"worked\"\n" + "End Sub\n" + ); + + // ============================================================ + // Coverage: Unresolved forward reference error + // ============================================================ + + { + printf("=== Unresolved forward reference ===\n"); + + const char *src = "NeverDefined\n"; + int32_t len = (int32_t)strlen(src); + BasParserT parser; + basParserInit(&parser, src, len); + + if (!basParse(&parser)) { + printf("Correctly caught: %s\n", parser.error); + } else { + printf("ERROR: should have failed\n"); + } + + basParserFree(&parser); + printf("\n"); + } + + // ============================================================ + // Coverage: END statement terminates (distinct from HALT) + // ============================================================ + + runProgram("END statement", + "PRINT \"before\"\n" + "END\n" + "PRINT \"after\"\n" + ); + + // ============================================================ + // Coverage: Nested UDT field store + // ============================================================ + + runProgram("Nested UDT store and load", + "TYPE InnerT\n" + " val AS INTEGER\n" + "END TYPE\n" + "\n" + "TYPE OuterT\n" + " child AS InnerT\n" + " name AS STRING\n" + "END TYPE\n" + "\n" + "DIM o AS OuterT\n" + "o.name = \"test\"\n" + "o.child.val = 42\n" + "PRINT o.name\n" + "PRINT o.child.val\n" + ); + + // ============================================================ + // Coverage: Array of UDT field store + // ============================================================ + + runProgram("Array of UDT field store", + "TYPE PointT\n" + " x AS INTEGER\n" + " y AS INTEGER\n" + "END TYPE\n" + "\n" + "DIM pts(5) AS PointT\n" + "pts(1).x = 10\n" + "pts(1).y = 20\n" + "pts(3).x = 30\n" + "pts(3).y = 40\n" + "PRINT pts(1).x; pts(1).y\n" + "PRINT pts(3).x; pts(3).y\n" + ); + + // ============================================================ + // Coverage: VB operator precedence (^ binds tighter than unary -) + // ============================================================ + + runProgram("Exponent precedence", + "PRINT -2 ^ 2\n" // -(2^2) = -4 + "PRINT (-2) ^ 2\n" // (-2)^2 = 4 + "PRINT 3 ^ 2 + 1\n" // 9 + 1 = 10 + ); + + // ============================================================ + // Coverage: Me.Show compiles in a Sub + // ============================================================ + + { + printf("=== Me.Show in Sub ===\n"); + + const char *src = + "Sub Form1_Load ()\n" + " Me.Show\n" + " Me.Caption = \"Hello\"\n" + " Me.Hide\n" + "End Sub\n"; + + int32_t len = (int32_t)strlen(src); + BasParserT parser; + basParserInit(&parser, src, len); + + if (!basParse(&parser)) { + printf("COMPILE ERROR: %s\n", parser.error); + } else { + printf("OK\n"); + } + + basParserFree(&parser); + printf("\n"); + } + + // ============================================================ + // Coverage: OPTION EXPLICIT with valid declaration + // ============================================================ + + runProgram("OPTION EXPLICIT valid", + "OPTION EXPLICIT\n" + "DIM x AS INTEGER\n" + "x = 42\n" + "PRINT x\n" + ); + + // ============================================================ + // Coverage: STATIC variable retains value + // ============================================================ + + runProgram("STATIC in sub", + "Sub Counter ()\n" + " STATIC n AS INTEGER\n" + " n = n + 1\n" + " PRINT n\n" + "End Sub\n" + "\n" + "Counter\n" + "Counter\n" + "Counter\n" + ); + printf("All tests complete.\n"); return 0; } diff --git a/core/dvxApp.c b/core/dvxApp.c index 050f6d7..58279c5 100644 --- a/core/dvxApp.c +++ b/core/dvxApp.c @@ -3005,6 +3005,20 @@ static void pollKeyboard(AppContextT *ctx) { continue; nextKey:; } + + // Key-up events: dispatch to the focused window's onKeyUp callback + PlatformKeyEventT upEvt; + + while (platformKeyUpRead(&upEvt)) { + if (ctx->stack.focusedIdx >= 0) { + WindowT *win = ctx->stack.windows[ctx->stack.focusedIdx]; + int32_t mod = platformKeyboardGetModifiers(); + + if (win->onKeyUp) { + win->onKeyUp(win, upEvt.scancode | 0x100, mod); + } + } + } } @@ -4182,6 +4196,7 @@ int32_t dvxInit(AppContextT *ctx, int32_t requestedW, int32_t requestedH, int32_ memset(ctx, 0, sizeof(*ctx)); platformInit(); + platformKeyUpInit(); // Enumerate available video modes BEFORE setting one. Some VBE // BIOSes return a stale or truncated mode list once a graphics @@ -4784,6 +4799,8 @@ int32_t dvxSetWindowIcon(AppContextT *ctx, WindowT *win, const char *path) { // ============================================================ void dvxShutdown(AppContextT *ctx) { + platformKeyUpShutdown(); + // Destroy all remaining windows while (ctx->stack.count > 0) { wmDestroyWindow(&ctx->stack, ctx->stack.windows[ctx->stack.count - 1]); diff --git a/core/dvxTypes.h b/core/dvxTypes.h index fc10dcc..f2dcf72 100644 --- a/core/dvxTypes.h +++ b/core/dvxTypes.h @@ -560,6 +560,7 @@ typedef struct WindowT { void *userData; void (*onPaint)(struct WindowT *win, RectT *dirtyArea); void (*onKey)(struct WindowT *win, int32_t key, int32_t mod); + void (*onKeyUp)(struct WindowT *win, int32_t scancode, int32_t mod); void (*onMouse)(struct WindowT *win, int32_t x, int32_t y, int32_t buttons); void (*onResize)(struct WindowT *win, int32_t newW, int32_t newH); void (*onClose)(struct WindowT *win); diff --git a/core/dvxWidgetPlugin.h b/core/dvxWidgetPlugin.h index c77dd20..4e434a3 100644 --- a/core/dvxWidgetPlugin.h +++ b/core/dvxWidgetPlugin.h @@ -175,6 +175,7 @@ void widgetLayoutChildren(WidgetT *w, const BitmapFontT *font); void widgetManageScrollbars(WindowT *win, AppContextT *ctx); void widgetOnKey(WindowT *win, int32_t key, int32_t mod); +void widgetOnKeyUp(WindowT *win, int32_t scancode, int32_t mod); void widgetOnMouse(WindowT *win, int32_t x, int32_t y, int32_t buttons); void widgetOnPaint(WindowT *win, RectT *dirtyArea); void widgetOnResize(WindowT *win, int32_t newW, int32_t newH); diff --git a/core/platform/dvxPlatform.h b/core/platform/dvxPlatform.h index 1a50706..3680292 100644 --- a/core/platform/dvxPlatform.h +++ b/core/platform/dvxPlatform.h @@ -177,6 +177,19 @@ int32_t platformKeyboardGetModifiers(void); // them unambiguously. bool platformKeyboardRead(PlatformKeyEventT *evt); +// Non-blocking read of the next key-up event. Returns true if a +// key release was detected. On DOS this requires an INT 9 hook to +// detect break codes (scan code with bit 7 set). On Linux this +// uses SDL_KEYUP events. +bool platformKeyUpRead(PlatformKeyEventT *evt); + +// Install/remove the INT 9 hook for key-up detection. On DOS this +// chains the hardware keyboard interrupt. On Linux this is a no-op +// (SDL provides key-up events natively). Call Init before using +// platformKeyUpRead, and Shutdown before exit. +void platformKeyUpInit(void); +void platformKeyUpShutdown(void); + // Translate an Alt+key scancode to its corresponding ASCII character. // When Alt is held, DOS doesn't provide the ASCII value -- only the // scancode. This function contains a lookup table mapping scancodes diff --git a/core/platform/dvxPlatformDos.c b/core/platform/dvxPlatformDos.c index cb6ab83..d293930 100644 --- a/core/platform/dvxPlatformDos.c +++ b/core/platform/dvxPlatformDos.c @@ -1430,6 +1430,87 @@ bool platformKeyboardRead(PlatformKeyEventT *evt) { } +// ============================================================ +// Key-up detection via INT 9 hook +// ============================================================ +// +// The BIOS keyboard interrupt (INT 16h) only reports key presses. +// To detect key releases we chain INT 9 (the hardware keyboard IRQ) +// and read the scan code directly from port 0x60. Break codes have +// bit 7 set. We queue them in a small ring buffer that +// platformKeyUpRead drains. + +#define KEYUP_BUF_SIZE 16 + +static PlatformKeyEventT sKeyUpBuf[KEYUP_BUF_SIZE]; +static volatile int32_t sKeyUpHead = 0; +static volatile int32_t sKeyUpTail = 0; +static _go32_dpmi_seginfo sOldInt9; +static _go32_dpmi_seginfo sNewInt9; +static bool sKeyUpInstalled = false; + + +// INT 9 handler: reads port 0x60 and queues break codes. +// DJGPP chains to the original handler automatically when using +// _go32_dpmi_chain_protected_mode_interrupt_vector. + +static void int9Handler(void) { + uint8_t scan = inportb(0x60); + + // Break code: bit 7 set and not the 0xE0 prefix + if ((scan & 0x80) && scan != 0xE0) { + int32_t next = (sKeyUpHead + 1) % KEYUP_BUF_SIZE; + + if (next != sKeyUpTail) { + sKeyUpBuf[sKeyUpHead].scancode = scan & 0x7F; + sKeyUpBuf[sKeyUpHead].ascii = 0; + sKeyUpHead = next; + } + } + + // Original handler is called automatically by DJGPP's chain wrapper +} + + +void platformKeyUpInit(void) { + if (sKeyUpInstalled) { + return; + } + + _go32_dpmi_get_protected_mode_interrupt_vector(9, &sOldInt9); + + sNewInt9.pm_offset = (unsigned long)int9Handler; + sNewInt9.pm_selector = _go32_my_cs(); + + // Chain: our handler runs first, then DJGPP automatically + // calls the original handler via an IRET wrapper. + _go32_dpmi_chain_protected_mode_interrupt_vector(9, &sNewInt9); + + sKeyUpInstalled = true; +} + + +void platformKeyUpShutdown(void) { + if (!sKeyUpInstalled) { + return; + } + + _go32_dpmi_set_protected_mode_interrupt_vector(9, &sOldInt9); + sKeyUpInstalled = false; +} + + +bool platformKeyUpRead(PlatformKeyEventT *evt) { + if (sKeyUpTail == sKeyUpHead) { + return false; + } + + *evt = sKeyUpBuf[sKeyUpTail]; + sKeyUpTail = (sKeyUpTail + 1) % KEYUP_BUF_SIZE; + return true; +} + + // ============================================================ // platformLineEnding // ============================================================ @@ -2364,6 +2445,9 @@ DXE_EXPORT_TABLE(sDxeExportTable) DXE_EXPORT(platformInstallCrashHandler) DXE_EXPORT(platformKeyboardGetModifiers) DXE_EXPORT(platformKeyboardRead) + DXE_EXPORT(platformKeyUpInit) + DXE_EXPORT(platformKeyUpRead) + DXE_EXPORT(platformKeyUpShutdown) DXE_EXPORT(platformLineEnding) DXE_EXPORT(platformLogCrashDetail) DXE_EXPORT(platformMkdirRecursive) diff --git a/core/widgetEvent.c b/core/widgetEvent.c index 0eefb56..b8c208d 100644 --- a/core/widgetEvent.c +++ b/core/widgetEvent.c @@ -197,6 +197,39 @@ void widgetOnKey(WindowT *win, int32_t key, int32_t mod) { } +// ============================================================ +// widgetOnKeyUp +// ============================================================ + +void widgetOnKeyUp(WindowT *win, int32_t scancode, int32_t mod) { + WidgetT *root = win->widgetRoot; + + if (!root) { + return; + } + + WidgetT *focus = sFocusedWidget; + + if (!focus || !focus->focused || focus->window != win) { + return; + } + + if (!focus->enabled) { + return; + } + + if (focus->onKeyUp) { + AppContextT *ctx = (AppContextT *)root->userData; + int32_t prevAppId = ctx->currentAppId; + ctx->currentAppId = win->appId; + + focus->onKeyUp(focus, scancode, mod); + + ctx->currentAppId = prevAppId; + } +} + + // ============================================================ // widgetOnMouse // ============================================================ diff --git a/core/widgetOps.c b/core/widgetOps.c index 9346c34..eda8c26 100644 --- a/core/widgetOps.c +++ b/core/widgetOps.c @@ -307,6 +307,7 @@ WidgetT *wgtInitWindow(AppContextT *ctx, WindowT *win) { win->onPaint = widgetOnPaint; win->onMouse = widgetOnMouse; win->onKey = widgetOnKey; + win->onKeyUp = widgetOnKeyUp; win->onResize = widgetOnResize; return root; diff --git a/widgets/timer/widgetTimer.c b/widgets/timer/widgetTimer.c index efa23d1..68abe28 100644 --- a/widgets/timer/widgetTimer.c +++ b/widgets/timer/widgetTimer.c @@ -166,6 +166,15 @@ void wgtTimerStop(WidgetT *w) { } +void wgtTimerSetEnabled(WidgetT *w, bool enabled) { + if (enabled) { + wgtTimerStart(w); + } else { + wgtTimerStop(w); + } +} + + void wgtUpdateTimers(void) { clock_t now = clock(); @@ -220,7 +229,7 @@ static const struct { }; static const WgtPropDescT sProps[] = { - { "Enabled", WGT_IFACE_BOOL, (void *)wgtTimerIsRunning, NULL }, + { "Enabled", WGT_IFACE_BOOL, (void *)wgtTimerIsRunning, (void *)wgtTimerSetEnabled }, { "Interval", WGT_IFACE_INT, NULL, (void *)wgtTimerSetInterval } };