From 1fb8e2a387f39809cc58e2557ae21ad0dca982b3 Mon Sep 17 00:00:00 2001 From: Scott Duensing Date: Tue, 31 Mar 2026 21:58:06 -0500 Subject: [PATCH] Working on BASIC code editor. --- apps/dvxbasic/formrt/formrt.c | 7 +- apps/dvxbasic/ide/ideDesigner.c | 48 ++ apps/dvxbasic/ide/ideDesigner.h | 1 + apps/dvxbasic/ide/ideMain.c | 711 ++++++++++++++++++++++++++---- apps/dvxbasic/ide/ideProject.c | 8 +- apps/progman/progman.c | 32 +- core/dvxApp.c | 7 +- core/dvxDialog.c | 10 + core/dvxWm.c | 20 +- core/dvxWm.h | 2 +- taskmgr/shellTaskMgr.c | 2 +- widgets/treeView/widgetTreeView.c | 12 +- 12 files changed, 745 insertions(+), 115 deletions(-) diff --git a/apps/dvxbasic/formrt/formrt.c b/apps/dvxbasic/formrt/formrt.c index 50743fb..c9a5fb0 100644 --- a/apps/dvxbasic/formrt/formrt.c +++ b/apps/dvxbasic/formrt/formrt.c @@ -800,6 +800,11 @@ BasFormT *basFormRtLoadFrm(BasFormRtT *rt, const char *source, int32_t sourceLen } } + // Fire the Load event now that the form and controls are ready + if (form) { + basFormRtFireEvent(rt, form, form->name, "Load"); + } + return form; } @@ -870,8 +875,6 @@ void basFormRtShowForm(void *ctx, void *formRef, bool modal) { if (modal) { rt->ctx->modalWindow = form->window; } - - basFormRtFireEvent(rt, form, form->name, "Load"); } diff --git a/apps/dvxbasic/ide/ideDesigner.c b/apps/dvxbasic/ide/ideDesigner.c index e6710ea..152b370 100644 --- a/apps/dvxbasic/ide/ideDesigner.c +++ b/apps/dvxbasic/ide/ideDesigner.c @@ -273,6 +273,7 @@ const char *dsgnDefaultEvent(const char *typeName) { void dsgnFree(DsgnStateT *ds) { if (ds->form) { arrfree(ds->form->controls); + free(ds->form->code); free(ds->form); ds->form = NULL; } @@ -433,6 +434,28 @@ bool dsgnLoadFrm(DsgnStateT *ds, const char *source, int32_t sourceLen) { curCtrl = NULL; } else { inForm = false; + + // Everything after the form's closing End is code + if (pos < end) { + // Skip leading blank lines + const char *codeStart = pos; + + while (codeStart < end && (*codeStart == '\r' || *codeStart == '\n' || *codeStart == ' ' || *codeStart == '\t')) { + codeStart++; + } + + if (codeStart < end) { + int32_t codeLen = (int32_t)(end - codeStart); + form->code = (char *)malloc(codeLen + 1); + + if (form->code) { + memcpy(form->code, codeStart, codeLen); + form->code[codeLen] = '\0'; + } + } + } + + break; // done parsing } continue; @@ -919,6 +942,31 @@ int32_t dsgnSaveFrm(const DsgnStateT *ds, char *buf, int32_t bufSize) { pos = saveControls(ds->form, buf, bufSize, pos, "", 1); pos += snprintf(buf + pos, bufSize - pos, "End\n"); + + // Append code section if present + if (ds->form->code && ds->form->code[0]) { + int32_t codeLen = (int32_t)strlen(ds->form->code); + int32_t avail = bufSize - pos - 2; // room for \n prefix and \n suffix + + if (avail > 0) { + buf[pos++] = '\n'; + + if (codeLen > avail) { + codeLen = avail; + } + + memcpy(buf + pos, ds->form->code, codeLen); + pos += codeLen; + + // Ensure trailing newline + if (pos > 0 && buf[pos - 1] != '\n' && pos < bufSize - 1) { + buf[pos++] = '\n'; + } + + buf[pos] = '\0'; + } + } + return pos; } diff --git a/apps/dvxbasic/ide/ideDesigner.h b/apps/dvxbasic/ide/ideDesigner.h index c826517..d7e491f 100644 --- a/apps/dvxbasic/ide/ideDesigner.h +++ b/apps/dvxbasic/ide/ideDesigner.h @@ -73,6 +73,7 @@ typedef struct { DsgnControlT *controls; // stb_ds dynamic array bool dirty; WidgetT *contentBox; // VBox parent for live widgets + char *code; // BASIC code section (malloc'd, after End block) } DsgnFormT; // ============================================================ diff --git a/apps/dvxbasic/ide/ideMain.c b/apps/dvxbasic/ide/ideMain.c index a3dc835..c731eb4 100644 --- a/apps/dvxbasic/ide/ideMain.c +++ b/apps/dvxbasic/ide/ideMain.c @@ -99,7 +99,13 @@ static void buildWindow(void); static void clearOutput(void); static void compileAndRun(void); static void ensureProject(const char *filePath); +static void freeProcBufs(void); +static const char *getFullSource(void); static void loadFile(void); +static void parseProcs(const char *source); +static void saveActiveFile(void); +static void saveCurProc(void); +static void showProc(int32_t procIdx); static int32_t toolbarBottom(void); static void loadFilePath(const char *path); static void newProject(void); @@ -112,6 +118,7 @@ static bool hasUnsavedData(void); static bool promptAndSave(void); static void cleanupFormWin(void); static void onClose(WindowT *win); +static void onCodeWinClose(WindowT *win); static void onContentFocus(WindowT *win); static void onFormWinClose(WindowT *win); static void onFormWinResize(WindowT *win, int32_t newW, int32_t newH); @@ -180,6 +187,13 @@ static char sOutputBuf[IDE_MAX_OUTPUT]; static int32_t sOutputLen = 0; static char sFilePath[DVX_MAX_PATH]; +// Procedure view state -- the editor shows one procedure at a time. +// Each procedure is stored in its own malloc'd buffer. The editor +// swaps directly between buffers with no splicing needed. +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) + // Procedure table for Object/Event dropdowns typedef struct { char objName[64]; @@ -589,9 +603,30 @@ static void clearOutput(void) { // ============================================================ static void compileAndRun(void) { - // Save all files before compiling if Save on Run is enabled + // Save all dirty files before compiling if Save on Run is enabled if (sWin && sWin->menuBar && wmMenuItemIsChecked(sWin->menuBar, CMD_SAVE_ON_RUN)) { - saveFile(); + if (sProject.activeFileIdx >= 0) { + saveActiveFile(); + } + + for (int32_t i = 0; i < sProject.fileCount; i++) { + if (i == sProject.activeFileIdx) { + continue; + } + + if (sProject.files[i].modified && sProject.files[i].buffer) { + char fullPath[DVX_MAX_PATH]; + prjFullPath(&sProject, i, fullPath, sizeof(fullPath)); + + FILE *f = fopen(fullPath, "w"); + + if (f) { + fputs(sProject.files[i].buffer, f); + fclose(f); + sProject.files[i].modified = false; + } + } + } } clearOutput(); @@ -606,12 +641,22 @@ static void compileAndRun(void) { int32_t srcLen = 0; if (sProject.projectPath[0] != '\0' && sProject.fileCount > 0) { - // Stash current editor contents into the active file's buffer - if (sProject.activeFileIdx >= 0 && sEditor) { + // Stash current editor state + if (sProject.activeFileIdx >= 0) { PrjFileT *cur = &sProject.files[sProject.activeFileIdx]; - const char *edSrc = wgtGetText(sEditor); - free(cur->buffer); - cur->buffer = edSrc ? strdup(edSrc) : NULL; + + 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) { + saveCurProc(); + free(sDesigner.form->code); + sDesigner.form->code = strdup(getFullSource()); } // Concatenate all .bas files from buffers (or disk if not yet loaded) @@ -629,13 +674,41 @@ static void compileAndRun(void) { sProject.sourceMapCount = 0; for (int32_t i = 0; i < sProject.fileCount; i++) { - if (sProject.files[i].isForm) { - continue; - } - - const char *fileSrc = sProject.files[i].buffer; + const char *fileSrc = NULL; char *diskBuf = NULL; + if (sProject.files[i].isForm) { + // For .frm files, extract just the code section. + // If this is the active form in the designer, use form->code. + if (sDesigner.form && i == sProject.activeFileIdx) { + fileSrc = sDesigner.form->code; + } else if (sProject.files[i].buffer) { + // Extract code from the stashed .frm text (after "End\n") + const char *buf = sProject.files[i].buffer; + const char *endTag = strstr(buf, "\nEnd\n"); + + if (!endTag) { + endTag = strstr(buf, "\nEnd\r\n"); + } + + if (endTag) { + endTag += 5; + + while (*endTag == '\r' || *endTag == '\n') { + endTag++; + } + + if (*endTag) { + fileSrc = endTag; + } + } + } + + // If no code found from memory, fall through to disk read + } else { + fileSrc = sProject.files[i].buffer; + } + if (!fileSrc) { // Not yet loaded into memory -- read from disk char fullPath[DVX_MAX_PATH]; @@ -658,6 +731,27 @@ static void compileAndRun(void) { int32_t br = (int32_t)fread(diskBuf, 1, size, f); diskBuf[br] = '\0'; fileSrc = diskBuf; + + // For .frm from disk, extract code section + if (sProject.files[i].isForm) { + const char *endTag = strstr(fileSrc, "\nEnd\n"); + + if (!endTag) { + endTag = strstr(fileSrc, "\nEnd\r\n"); + } + + if (endTag) { + endTag += 5; + + while (*endTag == '\r' || *endTag == '\n') { + endTag++; + } + + fileSrc = endTag; + } else { + fileSrc = NULL; + } + } } } @@ -708,8 +802,8 @@ static void compileAndRun(void) { src = concatBuf; srcLen = pos; } else { - // No project files -- compile whatever is in the editor - src = wgtGetText(sEditor); + // No project files -- compile the full source + src = getFullSource(); if (!src || *src == '\0') { setStatus("No source code to run."); @@ -823,6 +917,11 @@ 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) + if (formRt->formCount > 0) { + basFormRtShowForm(formRt, &formRt->forms[0], false); + } + sVm = vm; // Run in slices of 10000 steps, yielding to DVX between slices @@ -1037,9 +1136,8 @@ static void loadFilePath(const char *path) { showCodeWindow(); } - if (sEditor) { - wgtSetText(sEditor, sSourceBuf); - } + // Parse into per-procedure buffers and show (General) section + parseProcs(sSourceBuf); snprintf(sFilePath, sizeof(sFilePath), "%s", path); @@ -1050,6 +1148,7 @@ static void loadFilePath(const char *path) { dsgnFree(&sDesigner); updateDropdowns(); + showProc(-1); // show (General) section setStatus("File loaded."); } @@ -1118,10 +1217,10 @@ static void ensureProject(const char *filePath) { snprintf(frmPath, sizeof(frmPath), "%s", filePath); char *frmDot = strrchr(frmPath, '.'); - if (frmDot) { - strcpy(frmDot, ".frm"); - } else { - strcat(frmPath, ".frm"); + if (frmDot && (frmDot - frmPath) + 4 < DVX_MAX_PATH) { + snprintf(frmDot, sizeof(frmPath) - (frmDot - frmPath), ".frm"); + } else if ((int32_t)strlen(frmPath) + 4 < DVX_MAX_PATH) { + snprintf(frmPath + strlen(frmPath), sizeof(frmPath) - strlen(frmPath), ".frm"); } FILE *frmFile = fopen(frmPath, "r"); @@ -1202,6 +1301,13 @@ static void saveActiveFile(void) { 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()); + } + // Save form designer state to .frm file char *frmBuf = (char *)malloc(IDE_MAX_SOURCE); @@ -1221,9 +1327,9 @@ static void saveActiveFile(void) { free(frmBuf); } - } else if (!file->isForm && sEditor) { - // Save code editor contents to .bas file - const char *src = wgtGetText(sEditor); + } else if (!file->isForm) { + // Save full source (splice current proc back first) + const char *src = getFullSource(); if (src) { FILE *f = fopen(fullPath, "w"); @@ -1335,9 +1441,9 @@ static void onPrjFileClick(int32_t fileIdx, bool isForm) { cur->modified = true; } - } else if (!cur->isForm && sEditor) { - // Stash code editor text - const char *src = wgtGetText(sEditor); + } else if (!cur->isForm) { + // Stash full source (splice current proc back first) + const char *src = getFullSource(); free(cur->buffer); cur->buffer = src ? strdup(src) : NULL; cur->modified = true; @@ -1436,9 +1542,9 @@ static void onPrjFileClick(int32_t fileIdx, bool isForm) { } if (target->buffer) { - if (sEditor) { - wgtSetText(sEditor, target->buffer); - } + parseProcs(target->buffer); + updateDropdowns(); + showProc(-1); } else { char fullPath[DVX_MAX_PATH]; prjFullPath(&sProject, fileIdx, fullPath, sizeof(fullPath)); @@ -1448,9 +1554,11 @@ static void onPrjFileClick(int32_t fileIdx, bool isForm) { if (f) { fclose(f); loadFilePath(fullPath); - } else if (sEditor) { - // File doesn't exist yet -- start with empty editor - wgtSetText(sEditor, ""); + } else { + // File doesn't exist yet -- start with empty source + parseProcs(""); + updateDropdowns(); + showProc(-1); target->modified = true; } } @@ -1735,28 +1843,46 @@ void ideRenameInCode(const char *oldName, const char *newName) { return; } - // Rename in the active editor - if (sEditor && sProject.activeFileIdx >= 0 && - !sProject.files[sProject.activeFileIdx].isForm) { - const char *edText = wgtGetText(sEditor); + // Rename in the per-procedure buffers (form code currently being edited) + if (sGeneralBuf) { + char *replaced = renameInBuffer(sGeneralBuf, oldName, newName); - if (edText) { - char *replaced = renameInBuffer(edText, oldName, newName); + if (replaced) { + free(sGeneralBuf); + sGeneralBuf = replaced; + } + } + + for (int32_t i = 0; i < (int32_t)arrlen(sProcBufs); i++) { + if (sProcBufs[i]) { + char *replaced = renameInBuffer(sProcBufs[i], oldName, newName); if (replaced) { - wgtSetText(sEditor, replaced); - free(replaced); + free(sProcBufs[i]); + sProcBufs[i] = replaced; } } } - // Rename in all project .bas file buffers - for (int32_t i = 0; i < sProject.fileCount; i++) { - if (sProject.files[i].isForm) { - continue; + // Update the editor if it's showing a procedure + if (sEditor && sCurProcIdx >= -1) { + if (sCurProcIdx == -1 && sGeneralBuf) { + wgtSetText(sEditor, sGeneralBuf); + } else if (sCurProcIdx >= 0 && sCurProcIdx < (int32_t)arrlen(sProcBufs)) { + wgtSetText(sEditor, sProcBufs[sCurProcIdx]); } + } - // Skip the active file (already handled via editor) + // Update form->code from the renamed buffers + if (sDesigner.form) { + free(sDesigner.form->code); + sDesigner.form->code = strdup(getFullSource()); + sDesigner.form->dirty = true; + } + + // 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) if (i == sProject.activeFileIdx) { continue; } @@ -1804,6 +1930,34 @@ void ideRenameInCode(const char *oldName, const char *newName) { sProject.files[i].modified = true; } } + + // Refresh dropdowns to reflect renamed procedures + updateDropdowns(); +} + + +// ============================================================ +// onCodeWinClose -- user closed the code window via X button +// ============================================================ + +static void onCodeWinClose(WindowT *win) { + // Stash code back to form->code before the window is destroyed + if (sDesigner.form && sCurProcIdx >= -1) { + saveCurProc(); + free(sDesigner.form->code); + sDesigner.form->code = strdup(getFullSource()); + sDesigner.form->dirty = true; + } + + dvxDestroyWindow(sAc, win); + sCodeWin = NULL; + sEditor = NULL; + sObjDropdown = NULL; + sEvtDropdown = NULL; + + if (sLastFocusWin == win) { + sLastFocusWin = NULL; + } } @@ -1821,9 +1975,8 @@ static void onProjectWinClose(WindowT *win) { // loadFrmFiles // ============================================================ // -// Try to load a .frm file with the same base name as the loaded -// .bas source file. For example, if the user loaded "clickme.bas", -// this looks for "clickme.frm" in the same directory. +// 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"); @@ -1852,14 +2005,7 @@ static void loadFrmFile(BasFormRtT *rt, const char *frmPath) { fclose(f); frmBuf[bytesRead] = '\0'; - BasFormT *form = basFormRtLoadFrm(rt, frmBuf, bytesRead); - - if (form) { - int32_t pos = sOutputLen; - int32_t n = snprintf(sOutputBuf + pos, IDE_MAX_OUTPUT - pos, "Loaded form: %s\n", form->name); - sOutputLen += n; - } - + basFormRtLoadFrm(rt, frmBuf, bytesRead); free(frmBuf); } @@ -2016,6 +2162,8 @@ static void onClose(WindowT *win) { dsgnFree(&sDesigner); + freeProcBufs(); + arrfree(sProcTable); arrfree(sObjItems); arrfree(sEvtItems); @@ -2297,7 +2445,7 @@ static void onEvtDropdownChange(WidgetT *w) { for (int32_t i = 0; i < procCount; i++) { if (strcasecmp(sProcTable[i].objName, selObj) == 0) { if (matchIdx == evtIdx) { - wgtTextAreaGoToLine(sEditor, sProcTable[i].lineNum); + showProc(i); return; } @@ -2528,9 +2676,89 @@ static int32_t onFormWinCursorQuery(WindowT *win, int32_t x, int32_t y) { } +// navigateToEventSub -- open code editor at the default event sub for the +// selected control (or form). Creates the sub skeleton if it doesn't exist. +// Code is stored in the .frm file's code section (sDesigner.form->code). + +static void navigateToEventSub(void) { + if (!sDesigner.form) { + return; + } + + // Determine control name and default event + const char *ctrlName = NULL; + const char *eventName = NULL; + + if (sDesigner.selectedIdx >= 0 && + sDesigner.selectedIdx < (int32_t)arrlen(sDesigner.form->controls)) { + DsgnControlT *ctrl = &sDesigner.form->controls[sDesigner.selectedIdx]; + ctrlName = ctrl->name; + eventName = dsgnDefaultEvent(ctrl->typeName); + } else { + ctrlName = sDesigner.form->name; + eventName = dsgnDefaultEvent("Form"); + } + + if (!ctrlName || !eventName) { + return; + } + + char subName[128]; + snprintf(subName, sizeof(subName), "%s_%s", ctrlName, eventName); + + // Parse the form's code into per-procedure buffers + parseProcs(sDesigner.form->code ? sDesigner.form->code : ""); + + // Ensure code window is open + if (!sCodeWin) { + showCodeWindow(); + } + + if (!sEditor) { + return; + } + + updateDropdowns(); + + // Search for existing procedure + int32_t procCount = (int32_t)arrlen(sProcTable); + + for (int32_t i = 0; i < procCount; i++) { + char fullName[128]; + snprintf(fullName, sizeof(fullName), "%s_%s", sProcTable[i].objName, sProcTable[i].evtName); + + if (strcasecmp(fullName, subName) == 0) { + switchToCode(); + showProc(i); + return; + } + } + + // Not found -- create a new sub and add it as a procedure buffer + char skeleton[256]; + snprintf(skeleton, sizeof(skeleton), "Sub %s ()\n\nEnd Sub\n", subName); + + char *newBuf = strdup(skeleton); + arrput(sProcBufs, newBuf); + + sDesigner.form->dirty = true; + + // Save code back to form + free(sDesigner.form->code); + sDesigner.form->code = strdup(getFullSource()); + + updateDropdowns(); + + // Show the new procedure (it's the last one) + switchToCode(); + showProc((int32_t)arrlen(sProcBufs) - 1); +} + + static void onFormWinMouse(WindowT *win, int32_t x, int32_t y, int32_t buttons) { (void)win; static int32_t lastButtons = 0; + bool wasDown = (lastButtons & MOUSE_LEFT) != 0; bool isDown = (buttons & MOUSE_LEFT) != 0; @@ -2539,15 +2767,15 @@ static void onFormWinMouse(WindowT *win, int32_t x, int32_t y, int32_t buttons) return; } - if (isDown) { - int32_t prevCount = sDesigner.form ? (int32_t)arrlen(sDesigner.form->controls) : 0; - bool wasDirty = sDesigner.form ? sDesigner.form->dirty : false; - bool drag = wasDown; - dsgnOnMouse(&sDesigner, x, y, drag); + if (isDown && !wasDown) { + // Detect double-click using the system-wide setting + int32_t clicks = multiClickDetect(x, y); - // Rebuild tree if controls were added, removed, or reordered - int32_t newCount = sDesigner.form ? (int32_t)arrlen(sDesigner.form->controls) : 0; - bool nowDirty = sDesigner.form ? sDesigner.form->dirty : false; + int32_t prevCount = (int32_t)arrlen(sDesigner.form->controls); + bool wasDirty = sDesigner.form->dirty; + dsgnOnMouse(&sDesigner, x, y, false); + int32_t newCount = (int32_t)arrlen(sDesigner.form->controls); + bool nowDirty = sDesigner.form->dirty; if (newCount != prevCount || (nowDirty && !wasDirty)) { prpRebuildTree(&sDesigner); @@ -2558,7 +2786,20 @@ static void onFormWinMouse(WindowT *win, int32_t x, int32_t y, int32_t buttons) if (sFormWin) { dvxInvalidateWindow(sAc, sFormWin); } - } else if (wasDown) { + + if (clicks >= 2 && sDesigner.activeTool[0] == '\0') { + navigateToEventSub(); + } + } else if (isDown && wasDown) { + // Drag + dsgnOnMouse(&sDesigner, x, y, true); + prpRefresh(&sDesigner); + + if (sFormWin) { + dvxInvalidateWindow(sAc, sFormWin); + } + } else if (!isDown && wasDown) { + // Release dsgnOnMouse(&sDesigner, x, y, false); prpRefresh(&sDesigner); @@ -2647,11 +2888,34 @@ static void onFormWinClose(WindowT *win) { // ============================================================ static void switchToCode(void) { - if (sFormWin) { - dvxDestroyWindow(sAc, sFormWin); - cleanupFormWin(); + // Stash form data + 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; + } + + cur->modified = true; + } + } } + // Don't destroy the form window -- allow both code and design + // to be open simultaneously, like VB3. + setStatus("Code view."); } @@ -2661,6 +2925,13 @@ 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()); + } + // If already open, just bring to front if (sFormWin) { return; @@ -2673,10 +2944,10 @@ static void switchToDesign(void) { snprintf(frmPath, sizeof(frmPath), "%s", sFilePath); char *dot = strrchr(frmPath, '.'); - if (dot) { - strcpy(dot, ".frm"); - } else { - strcat(frmPath, ".frm"); + if (dot && (dot - frmPath) + 4 < DVX_MAX_PATH) { + snprintf(dot, sizeof(frmPath) - (dot - frmPath), ".frm"); + } else if ((int32_t)strlen(frmPath) + 4 < DVX_MAX_PATH) { + snprintf(frmPath + strlen(frmPath), sizeof(frmPath) - strlen(frmPath), ".frm"); } FILE *f = fopen(frmPath, "r"); @@ -2800,14 +3071,20 @@ static void showCodeWindow(void) { return; // already open } - int32_t codeY = sWin ? sWin->y + sWin->h + 2 : 60; + int32_t codeY = toolbarBottom(); int32_t codeH = sAc->display.height - codeY - 122; sCodeWin = dvxCreateWindow(sAc, "Code", 0, codeY, sAc->display.width, codeH, true); + // Ensure position is below the toolbar (dvxCreateWindow may adjust) + if (sCodeWin) { + sCodeWin->y = codeY; + } + if (sCodeWin) { sCodeWin->onMenu = onMenu; sCodeWin->onFocus = onContentFocus; + sCodeWin->onClose = onCodeWinClose; sCodeWin->accelTable = sWin ? sWin->accelTable : NULL; sLastFocusWin = sCodeWin; @@ -2928,27 +3205,254 @@ static void setStatus(const char *text) { // the Object and Event dropdowns. Procedure names are split on // '_' into ObjectName and EventName (e.g. "Command1_Click"). +// freeProcBufs -- release all procedure buffers + +static void freeProcBufs(void) { + free(sGeneralBuf); + sGeneralBuf = NULL; + + for (int32_t i = 0; i < (int32_t)arrlen(sProcBufs); i++) { + free(sProcBufs[i]); + } + + arrfree(sProcBufs); + sProcBufs = NULL; + sCurProcIdx = -2; +} + + +// parseProcs -- split source into (General) + per-procedure buffers + +static void parseProcs(const char *source) { + freeProcBufs(); + + if (!source) { + sGeneralBuf = strdup(""); + return; + } + + const char *pos = source; + const char *genEnd = source; // end of (General) section + + while (*pos) { + const char *lineStart = pos; + + // Skip leading whitespace + const char *trimmed = pos; + + while (*trimmed == ' ' || *trimmed == '\t') { + trimmed++; + } + + bool isSub = (strncasecmp(trimmed, "SUB ", 4) == 0); + bool isFunc = (strncasecmp(trimmed, "FUNCTION ", 9) == 0); + + if (isSub || isFunc) { + // On first proc, mark end of (General) section + if (arrlen(sProcBufs) == 0) { + genEnd = lineStart; + } + + // Find End Sub / End Function + const char *endTag = isSub ? "END SUB" : "END FUNCTION"; + int32_t endTagLen = isSub ? 7 : 12; + const char *scan = pos; + + // Advance past the Sub/Function line + while (*scan && *scan != '\n') { scan++; } + if (*scan == '\n') { scan++; } + + // Scan for End Sub/Function + while (*scan) { + const char *sl = scan; + + while (*sl == ' ' || *sl == '\t') { sl++; } + + if (strncasecmp(sl, endTag, endTagLen) == 0) { + while (*scan && *scan != '\n') { scan++; } + if (*scan == '\n') { scan++; } + break; + } + + while (*scan && *scan != '\n') { scan++; } + if (*scan == '\n') { scan++; } + } + + // Extract this procedure + int32_t procLen = (int32_t)(scan - lineStart); + char *procBuf = (char *)malloc(procLen + 1); + + if (procBuf) { + memcpy(procBuf, lineStart, procLen); + procBuf[procLen] = '\0'; + } + + arrput(sProcBufs, procBuf); + pos = scan; + continue; + } + + // Advance to next line + while (*pos && *pos != '\n') { pos++; } + if (*pos == '\n') { pos++; } + } + + // Extract (General) section + int32_t genLen = (int32_t)(genEnd - source); + + // Trim trailing blank lines + while (genLen > 0 && (source[genLen - 1] == '\n' || source[genLen - 1] == '\r' || + source[genLen - 1] == ' ' || source[genLen - 1] == '\t')) { + genLen--; + } + + sGeneralBuf = (char *)malloc(genLen + 2); + + if (sGeneralBuf) { + memcpy(sGeneralBuf, source, genLen); + sGeneralBuf[genLen] = '\n'; + sGeneralBuf[genLen + 1] = '\0'; + + if (genLen == 0) { + sGeneralBuf[0] = '\0'; + } + } +} + + +// saveCurProc -- save editor contents back to the current buffer + +static void saveCurProc(void) { + if (!sEditor) { + return; + } + + const char *edText = wgtGetText(sEditor); + + if (!edText) { + return; + } + + if (sCurProcIdx == -1) { + free(sGeneralBuf); + sGeneralBuf = strdup(edText); + } else if (sCurProcIdx >= 0 && sCurProcIdx < (int32_t)arrlen(sProcBufs)) { + free(sProcBufs[sCurProcIdx]); + sProcBufs[sCurProcIdx] = strdup(edText); + } +} + + +// showProc -- display a procedure buffer in the editor + +static void showProc(int32_t procIdx) { + if (!sEditor) { + return; + } + + // Save whatever is currently in the editor + if (sCurProcIdx >= -1) { + saveCurProc(); + } + + if (procIdx == -1) { + wgtSetText(sEditor, sGeneralBuf ? sGeneralBuf : ""); + sCurProcIdx = -1; + } else if (procIdx >= 0 && procIdx < (int32_t)arrlen(sProcBufs)) { + wgtSetText(sEditor, sProcBufs[procIdx] ? sProcBufs[procIdx] : ""); + sCurProcIdx = procIdx; + } +} + + +// getFullSource -- reassemble all buffers into one source string +// Caller must free the returned buffer. + +static char *sFullSourceCache = NULL; + +static const char *getFullSource(void) { + saveCurProc(); + + free(sFullSourceCache); + + // Calculate total length + int32_t totalLen = 0; + + if (sGeneralBuf && sGeneralBuf[0]) { + totalLen += (int32_t)strlen(sGeneralBuf); + totalLen += 2; // blank line separator + } + + int32_t procCount = (int32_t)arrlen(sProcBufs); + + for (int32_t i = 0; i < procCount; i++) { + if (sProcBufs[i]) { + totalLen += (int32_t)strlen(sProcBufs[i]); + totalLen += 1; // newline separator + } + } + + sFullSourceCache = (char *)malloc(totalLen + 1); + + if (!sFullSourceCache) { + return ""; + } + + int32_t pos = 0; + + if (sGeneralBuf && sGeneralBuf[0]) { + int32_t len = (int32_t)strlen(sGeneralBuf); + memcpy(sFullSourceCache + pos, sGeneralBuf, len); + pos += len; + + if (pos > 0 && sFullSourceCache[pos - 1] != '\n') { + sFullSourceCache[pos++] = '\n'; + } + + sFullSourceCache[pos++] = '\n'; + } + + for (int32_t i = 0; i < procCount; i++) { + if (sProcBufs[i]) { + int32_t len = (int32_t)strlen(sProcBufs[i]); + memcpy(sFullSourceCache + pos, sProcBufs[i], len); + pos += len; + + if (pos > 0 && sFullSourceCache[pos - 1] != '\n') { + sFullSourceCache[pos++] = '\n'; + } + } + } + + sFullSourceCache[pos] = '\0'; + return sFullSourceCache; +} + + static void updateDropdowns(void) { // Reset dynamic arrays arrsetlen(sProcTable, 0); arrsetlen(sObjItems, 0); arrsetlen(sEvtItems, 0); - if (!sEditor || !sObjDropdown || !sEvtDropdown) { + if (!sObjDropdown || !sEvtDropdown) { return; } - const char *src = wgtGetText(sEditor); + // Scan the reassembled full source + const char *src = getFullSource(); if (!src) { return; } // Scan line by line for SUB / FUNCTION - const char *pos = src; - int32_t lineNum = 1; + const char *pos = src; + int32_t lineNum = 1; while (*pos) { + const char *lineStart = pos; + // Skip leading whitespace while (*pos == ' ' || *pos == '\t') { pos++; @@ -2961,12 +3465,10 @@ static void updateDropdowns(void) { if (isSub || isFunc) { pos += isSub ? 4 : 9; - // Skip whitespace after keyword while (*pos == ' ' || *pos == '\t') { pos++; } - // Extract procedure name char procName[64]; int32_t nameLen = 0; @@ -2976,7 +3478,40 @@ static void updateDropdowns(void) { procName[nameLen] = '\0'; - // Split on '_' into object + event + // Find End Sub / End Function + const char *endTag = isSub ? "END SUB" : "END FUNCTION"; + int32_t endTagLen = isSub ? 7 : 12; + const char *scan = pos; + + while (*scan) { + const char *sl = scan; + + while (*sl == ' ' || *sl == '\t') { + sl++; + } + + if (strncasecmp(sl, endTag, endTagLen) == 0) { + // Advance past the End line + while (*scan && *scan != '\n') { + scan++; + } + + if (*scan == '\n') { + scan++; + } + + break; + } + + while (*scan && *scan != '\n') { + scan++; + } + + if (*scan == '\n') { + scan++; + } + } + IdeProcEntryT entry; memset(&entry, 0, sizeof(entry)); entry.lineNum = lineNum; @@ -2999,6 +3534,18 @@ static void updateDropdowns(void) { } arrput(sProcTable, entry); + + // Skip to end of this proc (already scanned) + pos = scan; + + // Count lines we skipped + for (const char *c = lineStart; c < scan; c++) { + if (*c == '\n') { + lineNum++; + } + } + + continue; } // Advance to end of line diff --git a/apps/dvxbasic/ide/ideProject.c b/apps/dvxbasic/ide/ideProject.c index 02f2bed..db015b4 100644 --- a/apps/dvxbasic/ide/ideProject.c +++ b/apps/dvxbasic/ide/ideProject.c @@ -63,7 +63,7 @@ static char **sLabels = NULL; // stb_ds array of strdup'd strings // ============================================================ static void onPrjWinClose(WindowT *win); -static void onTreeItemClick(WidgetT *w); +static void onTreeItemDblClick(WidgetT *w); // ============================================================ // prjInit @@ -362,7 +362,7 @@ static void onPrjWinClose(WindowT *win) { } -static void onTreeItemClick(WidgetT *w) { +static void onTreeItemDblClick(WidgetT *w) { if (!sPrj || !sOnClick) { return; } @@ -473,7 +473,7 @@ void prjRebuildTree(PrjStateT *prj) { arrput(sLabels, label); WidgetT *item = wgtTreeItem(formsNode, label); item->userData = (void *)(intptr_t)i; - item->onClick = onTreeItemClick; + item->onDblClick = onTreeItemDblClick; } } @@ -490,7 +490,7 @@ void prjRebuildTree(PrjStateT *prj) { arrput(sLabels, label); WidgetT *item = wgtTreeItem(modsNode, label); item->userData = (void *)(intptr_t)i; - item->onClick = onTreeItemClick; + item->onDblClick = onTreeItemDblClick; } } diff --git a/apps/progman/progman.c b/apps/progman/progman.c index b2d5c23..cb6fb7f 100644 --- a/apps/progman/progman.c +++ b/apps/progman/progman.c @@ -72,6 +72,7 @@ #define CMD_TILE_H 202 #define CMD_TILE_V 203 #define CMD_MIN_ON_RUN 104 +#define CMD_RESTORE_ALONE 105 #define CMD_ABOUT 300 #define CMD_TASK_MGR 301 #define CMD_SYSINFO 302 @@ -99,7 +100,8 @@ static AppContextT *sAc = NULL; static WindowT *sPmWindow = NULL; static WidgetT *sStatusLabel = NULL; static PrefsHandleT *sPrefs = NULL; -static bool sMinOnRun = false; +static bool sMinOnRun = false; +static bool sRestoreAlone = false; static AppEntryT sAppFiles[MAX_APP_FILES]; static int32_t sAppCount = 0; @@ -164,10 +166,11 @@ static void buildPmWindow(void) { MenuT *fileMenu = wmAddMenu(menuBar, "&File"); wmAddMenuItem(fileMenu, "&Run...", CMD_RUN); wmAddMenuSeparator(fileMenu); - wmAddMenuItem(fileMenu, "E&xit Shell", CMD_EXIT); + wmAddMenuItem(fileMenu, "E&xit DVX", CMD_EXIT); MenuT *optMenu = wmAddMenu(menuBar, "&Options"); wmAddMenuCheckItem(optMenu, "&Minimize on Run", CMD_MIN_ON_RUN, sMinOnRun); + wmAddMenuCheckItem(optMenu, "&Restore when Alone", CMD_RESTORE_ALONE, sRestoreAlone); MenuT *windowMenu = wmAddMenu(menuBar, "&Window"); wmAddMenuItem(windowMenu, "&Cascade", CMD_CASCADE); @@ -176,7 +179,7 @@ static void buildPmWindow(void) { wmAddMenuItem(windowMenu, "Tile &Vertically", CMD_TILE_V); MenuT *helpMenu = wmAddMenu(menuBar, "&Help"); - wmAddMenuItem(helpMenu, "&About DVX Shell...", CMD_ABOUT); + wmAddMenuItem(helpMenu, "&About DVX...", CMD_ABOUT); wmAddMenuItem(helpMenu, "&System Information...", CMD_SYSINFO); wmAddMenuSeparator(helpMenu); wmAddMenuItem(helpMenu, "&Task Manager\tCtrl+Esc", CMD_TASK_MGR); @@ -262,6 +265,13 @@ static void buildPmWindow(void) { // (Task Manager refresh is handled by the shell's shellDesktopUpdate.) static void desktopUpdate(void) { updateStatusText(); + + // Auto-restore if we're the only running app and minimized + if (sRestoreAlone && sPmWindow && sPmWindow->minimized) { + if (shellRunningAppCount() <= 1) { + wmRestoreMinimized(&sAc->stack, &sAc->dirty, &sAc->display, sPmWindow); + } + } } @@ -288,7 +298,7 @@ static void onAppButtonClick(WidgetT *w) { // shellTerminateAllApps() to gracefully tear down all loaded DXEs. static void onPmClose(WindowT *win) { (void)win; - int32_t result = dvxMessageBox(sAc, "Exit Shell", "Are you sure you want to exit DVX Shell?", MB_YESNO | MB_ICONQUESTION); + int32_t result = dvxMessageBox(sAc, "Exit DVX", "Are you sure you want to exit DVX?", MB_YESNO | MB_ICONQUESTION); if (result == ID_YES) { dvxQuit(sAc); @@ -346,6 +356,13 @@ static void onPmMenu(WindowT *win, int32_t menuId) { prefsSave(sPrefs); break; + case CMD_RESTORE_ALONE: + sRestoreAlone = !sRestoreAlone; + shellEnsureConfigDir(sCtx); + prefsSetBool(sPrefs, "options", "restoreAlone", sRestoreAlone); + prefsSave(sPrefs); + break; + case CMD_ABOUT: showAboutDialog(); break; @@ -464,8 +481,8 @@ static void scanAppsDirRecurse(const char *dirPath) { static void showAboutDialog(void) { - dvxMessageBox(sAc, "About DVX Shell", - "DVX Shell 1.0\nA DOS Visual eXecutive desktop shell for DJGPP/DPMI. Using DXE3 dynamic loading for application modules.", + dvxMessageBox(sAc, "About DVX", + "DVX 1.0 - \"DOS Visual eXecutive\" GUI System.\n\n\"We have Windows at home.\"\n\nCopyright 2026 Scott Duensing\nKangaroo Punch Studios\nhttps://kangaroopunch.com", MB_OK | MB_ICONINFO); } @@ -537,7 +554,8 @@ int32_t appMain(DxeAppContextT *ctx) { char prefsPath[DVX_MAX_PATH]; shellConfigPath(sCtx, "progman.ini", prefsPath, sizeof(prefsPath)); sPrefs = prefsLoad(prefsPath); - sMinOnRun = prefsGetBool(sPrefs, "options", "minimizeOnRun", false); + sMinOnRun = prefsGetBool(sPrefs, "options", "minimizeOnRun", false); + sRestoreAlone = prefsGetBool(sPrefs, "options", "restoreAlone", false); scanAppsDir(); buildPmWindow(); diff --git a/core/dvxApp.c b/core/dvxApp.c index 8770c09..57fa8cb 100644 --- a/core/dvxApp.c +++ b/core/dvxApp.c @@ -1773,12 +1773,7 @@ static void handleMouseButton(AppContextT *ctx, int32_t mx, int32_t my, int32_t if (ctx->lastIconClickId == iconWin->id && (now - ctx->lastIconClickTime) < ctx->dblClickTicks) { // Double-click: restore minimized window - // Dirty the entire icon area (may span multiple rows) - int32_t iconY; - int32_t iconH; - wmMinimizedIconRect(&ctx->stack, &ctx->display, &iconY, &iconH); - dirtyListAdd(&ctx->dirty, 0, iconY, ctx->display.width, iconH); - wmRestoreMinimized(&ctx->stack, &ctx->dirty, iconWin); + wmRestoreMinimized(&ctx->stack, &ctx->dirty, &ctx->display, iconWin); ctx->lastIconClickId = -1; } else { // First click -- record for double-click detection diff --git a/core/dvxDialog.c b/core/dvxDialog.c index cf92649..9f8cedd 100644 --- a/core/dvxDialog.c +++ b/core/dvxDialog.c @@ -575,6 +575,12 @@ static void wordWrapDraw(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *fo drawTextN(d, ops, font, x, curY, text, lineLen, fg, bg, true); curY += lineH; text += lineLen; + + // Skip the newline that ended this line (if any) so the + // explicit \n handler at the top doesn't double-advance. + if (*text == '\n') { + text++; + } } } @@ -631,6 +637,10 @@ static int32_t wordWrapHeight(const BitmapFontT *font, const char *text, int32_t lines++; text += lineLen; + + if (*text == '\n') { + text++; + } } if (lines == 0) { diff --git a/core/dvxWm.c b/core/dvxWm.c index aa85162..d4ecd6e 100644 --- a/core/dvxWm.c +++ b/core/dvxWm.c @@ -2403,21 +2403,21 @@ void wmRestore(WindowStackT *stack, DirtyListT *dl, const DisplayT *d, WindowT * // wmRestoreMinimized // ============================================================ // -// Restores a minimized window: clears the minimized flag, raises the window -// to the top of the z-order, and gives it focus. No content buffer -// reallocation is needed because the buffer was preserved while minimized. -// -// The icon strip area is implicitly dirtied by the raise/focus operations -// and by the window area dirty at the end. The compositor repaints the -// entire icon strip on any dirty rect that intersects it, so icon positions -// are always correct after restore even though we don't dirty the icon -// strip explicitly. +// Restores a minimized window: clears the minimized flag, dirties the +// icon strip so the old icon is erased, raises the window to the top +// of the z-order, and gives it focus. -void wmRestoreMinimized(WindowStackT *stack, DirtyListT *dl, WindowT *win) { +void wmRestoreMinimized(WindowStackT *stack, DirtyListT *dl, const DisplayT *d, WindowT *win) { if (!win->minimized) { return; } + // Dirty the icon strip area so the old minimized icon is erased + int32_t iconY; + int32_t iconH; + wmMinimizedIconRect(stack, d, &iconY, &iconH); + dirtyListAdd(dl, 0, iconY, d->width, iconH); + win->minimized = false; for (int32_t i = 0; i < stack->count; i++) { diff --git a/core/dvxWm.h b/core/dvxWm.h index d12fddc..9733818 100644 --- a/core/dvxWm.h +++ b/core/dvxWm.h @@ -200,7 +200,7 @@ void wmMinimizedIconRect(const WindowStackT *stack, const DisplayT *d, int32_t * void wmRestore(WindowStackT *stack, DirtyListT *dl, const DisplayT *d, WindowT *win); // Restore a minimized window -void wmRestoreMinimized(WindowStackT *stack, DirtyListT *dl, WindowT *win); +void wmRestoreMinimized(WindowStackT *stack, DirtyListT *dl, const DisplayT *d, WindowT *win); // Load an icon image for a window (converts to display pixel format) int32_t wmSetIcon(WindowT *win, const char *path, const DisplayT *d); diff --git a/taskmgr/shellTaskMgr.c b/taskmgr/shellTaskMgr.c index 7088680..16c91d1 100644 --- a/taskmgr/shellTaskMgr.c +++ b/taskmgr/shellTaskMgr.c @@ -178,7 +178,7 @@ static void onTmSwitchTo(WidgetT *w) { if (win->appId == app->appId) { if (win->minimized) { - wmRestoreMinimized(&sCtx->stack, &sCtx->dirty, win); + wmRestoreMinimized(&sCtx->stack, &sCtx->dirty, &sCtx->display, win); } wmRaiseWindow(&sCtx->stack, &sCtx->dirty, j); diff --git a/widgets/treeView/widgetTreeView.c b/widgets/treeView/widgetTreeView.c index 983245e..c4b83d3 100644 --- a/widgets/treeView/widgetTreeView.c +++ b/widgets/treeView/widgetTreeView.c @@ -1192,12 +1192,20 @@ void widgetTreeViewOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy) item->onChange(item); } } else { - if (item->onClick) { + int32_t clicks = multiClickDetect(vx, vy); + + if (clicks >= 2 && item->onDblClick) { + item->onDblClick(item); + } else if (item->onClick) { item->onClick(item); } } } else { - if (item->onClick) { + int32_t clicks = multiClickDetect(vx, vy); + + if (clicks >= 2 && item->onDblClick) { + item->onDblClick(item); + } else if (item->onClick) { item->onClick(item); } }