// ideMain.c -- DVX BASIC Runner application // // A DVX app that loads, compiles, and runs BASIC programs. // PRINT output goes to a scrollable TextArea widget. Compile // errors are displayed with line numbers. // // This is Phase 3 of DVX BASIC: proving the compiler and VM // work on real hardware inside the DVX windowing system. #include "dvxApp.h" #include "dvxCursor.h" #include "dvxPlatform.h" #include "dvxDialog.h" #include "dvxPrefs.h" #include "dvxWidget.h" #include "dvxWidgetPlugin.h" #include "dvxWm.h" #include "shellApp.h" #include "widgetBox.h" #include "widgetImageButton.h" #include "widgetLabel.h" #include "widgetTextInput.h" #include "widgetDropdown.h" #include "widgetButton.h" #include "widgetSplitter.h" #include "widgetStatusBar.h" #include "widgetToolbar.h" #include "ideDesigner.h" #include "ideProject.h" #include "ideToolbox.h" #include "ideProperties.h" #include "../compiler/parser.h" #include "../formrt/formrt.h" #include "../runtime/vm.h" #include "../runtime/values.h" #include "stb_ds_wrap.h" #include #include #include #include #include #include // ============================================================ // Constants // ============================================================ #define IDE_MAX_SOURCE 65536 #define IDE_MAX_OUTPUT 32768 #define IDE_STEP_SLICE 10000 // VM steps per slice before yielding to DVX // Menu command IDs #define CMD_OPEN 100 #define CMD_RUN 101 #define CMD_STOP 102 #define CMD_CLEAR 103 #define CMD_EXIT 104 #define CMD_RUN_NOCMP 105 #define CMD_VIEW_CODE 106 #define CMD_VIEW_DESIGN 107 #define CMD_SAVE 108 #define CMD_WIN_CODE 109 #define CMD_WIN_OUTPUT 110 #define CMD_WIN_IMM 111 #define CMD_WIN_TOOLBOX 112 #define CMD_WIN_PROPS 113 #define CMD_DELETE 114 #define CMD_CUT 115 #define CMD_COPY 116 #define CMD_PASTE 117 #define CMD_SELECT_ALL 118 #define CMD_VIEW_TOOLBAR 119 #define CMD_VIEW_STATUS 120 #define CMD_SAVE_ALL 129 #define CMD_SAVE_ON_RUN 139 #define CMD_PRJ_NEW 130 #define CMD_PRJ_OPEN 131 #define CMD_PRJ_SAVE 132 #define CMD_PRJ_CLOSE 133 #define CMD_PRJ_ADD_MOD 134 #define CMD_PRJ_ADD_FRM 135 #define CMD_PRJ_REMOVE 136 #define CMD_PRJ_PROPS 138 #define CMD_WIN_PROJECT 137 #define IDE_MAX_IMM 1024 #define IDE_DESIGN_W 400 #define IDE_DESIGN_H 300 // ============================================================ // Prototypes // ============================================================ int32_t appMain(DxeAppContextT *ctx); 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); static void onPrjFileClick(int32_t fileIdx, bool isForm); static void openProject(void); static void closeProject(void); static void saveFile(void); static void onTbSave(WidgetT *w); 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); static void onProjectWinClose(WindowT *win); static WindowT *getLastFocusWin(void); 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); static void loadFrmFiles(BasFormRtT *rt); static void onEvtDropdownChange(WidgetT *w); static void onImmediateChange(WidgetT *w); static void onObjDropdownChange(WidgetT *w); static void printCallback(void *ctx, const char *text, bool newline); static bool inputCallback(void *ctx, const char *prompt, char *buf, int32_t bufSize); static bool doEventsCallback(void *ctx); static void runCached(void); static void runModule(BasModuleT *mod); static void setStatus(const char *text); static void switchToCode(void); static void switchToDesign(void); static int32_t onFormWinCursorQuery(WindowT *win, int32_t x, int32_t y); static void onFormWinKey(WindowT *win, int32_t key, int32_t mod); static void onFormWinMouse(WindowT *win, int32_t x, int32_t y, int32_t buttons); static void onFormWinPaint(WindowT *win, RectT *dirtyArea); static void onTbRun(WidgetT *w); static void showCodeWindow(void); static void showOutputWindow(void); static void showImmediateWindow(void); static void onTbStop(WidgetT *w); static void onTbOpen(WidgetT *w); static void onTbCode(WidgetT *w); static void onTbDesign(WidgetT *w); static void updateDropdowns(void); // ============================================================ // Module state // ============================================================ static DxeAppContextT *sCtx = NULL; static AppContextT *sAc = NULL; static PrefsHandleT *sPrefs = NULL; static WindowT *sWin = NULL; // Main toolbar window static WindowT *sCodeWin = NULL; // Code editor window static WindowT *sOutWin = NULL; // Output window static WindowT *sImmWin = NULL; // Immediate window static WidgetT *sEditor = NULL; static WidgetT *sOutput = NULL; static WidgetT *sImmediate = NULL; static WidgetT *sObjDropdown = NULL; static WidgetT *sEvtDropdown = NULL; static WidgetT *sToolbar = NULL; static WidgetT *sStatusBar = NULL; static WidgetT *sStatus = NULL; static BasVmT *sVm = NULL; // VM instance (non-NULL while running) static BasModuleT *sCachedModule = NULL; // Last compiled module (for Ctrl+F5) static DsgnStateT sDesigner; static WindowT *sFormWin = NULL; // Form designer window (separate) static WindowT *sToolboxWin = NULL; static WindowT *sPropsWin = NULL; static WindowT *sProjectWin = NULL; static PrjStateT sProject; static WindowT *sLastFocusWin = NULL; // last focused non-toolbar window static char sSourceBuf[IDE_MAX_SOURCE]; 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]; char evtName[64]; int32_t lineNum; } IdeProcEntryT; static IdeProcEntryT *sProcTable = NULL; // stb_ds dynamic array static const char **sObjItems = NULL; // stb_ds dynamic array static const char **sEvtItems = NULL; // stb_ds dynamic array // ============================================================ // App descriptor // ============================================================ AppDescriptorT appDescriptor = { .name = "DVX BASIC", .hasMainLoop = false, .multiInstance = false, .stackSize = SHELL_STACK_DEFAULT, .priority = 0 }; // ============================================================ // appMain // ============================================================ int32_t appMain(DxeAppContextT *ctx) { sCtx = ctx; sAc = ctx->shellCtx; basStringSystemInit(); prjInit(&sProject); buildWindow(); // Load persisted settings char prefsPath[DVX_MAX_PATH]; shellConfigPath(sCtx, "dvxbasic.ini", prefsPath, sizeof(prefsPath)); sPrefs = prefsLoad(prefsPath); if (sToolbar && sWin && sWin->menuBar) { bool showTb = prefsGetBool(sPrefs, "view", "toolbar", true); sToolbar->visible = showTb; wmMenuItemSetChecked(sWin->menuBar, CMD_VIEW_TOOLBAR, showTb); } if (sStatusBar && sWin && sWin->menuBar) { bool showSb = prefsGetBool(sPrefs, "view", "statusbar", true); sStatusBar->visible = showSb; wmMenuItemSetChecked(sWin->menuBar, CMD_VIEW_STATUS, showSb); } if (sWin && sWin->menuBar) { bool saveOnRun = prefsGetBool(sPrefs, "run", "saveOnRun", true); wmMenuItemSetChecked(sWin->menuBar, CMD_SAVE_ON_RUN, saveOnRun); } if (sWin) { dvxFitWindowH(sAc, sWin); } sFilePath[0] = '\0'; sSourceBuf[0] = '\0'; sOutputBuf[0] = '\0'; sOutputLen = 0; // Auto-load project for development/testing if (prjLoad(&sProject, "C:\\BIN\\APPS\\DVXBASIC\\MULTI.DBP")) { sProjectWin = prjCreateWindow(sAc, &sProject, onPrjFileClick); if (sProjectWin) { sProjectWin->y = toolbarBottom() + 25; sProjectWin->onClose = onProjectWinClose; } char title[300]; snprintf(title, sizeof(title), "DVX BASIC - [%s]", sProject.name); dvxSetTitle(sAc, sWin, title); } setStatus("Ready."); return 0; } // ============================================================ // toolbarBottom -- Y position just below the toolbar window // ============================================================ static int32_t toolbarBottom(void) { return sWin ? sWin->y + sWin->h + 2 : 60; } // ============================================================ // loadTbIcon -- load a toolbar icon from the app's resources // ============================================================ static WidgetT *loadTbIcon(WidgetT *parent, const char *resName, const char *fallbackText) { int32_t iconW = 0; int32_t iconH = 0; int32_t iconPitch = 0; uint8_t *data = dvxResLoadIcon(sAc, sCtx->appPath, resName, &iconW, &iconH, &iconPitch); if (data) { return wgtImageButton(parent, data, iconW, iconH, iconPitch); } // Fallback to text button if icon not found return wgtButton(parent, fallbackText); } // ============================================================ // buildWindow // ============================================================ static void buildWindow(void) { // ---- Main toolbar window (top of screen) ---- sWin = dvxCreateWindow(sAc, "DVX BASIC", 0, 0, sAc->display.width, 200, false); if (!sWin) { return; } sWin->onClose = onClose; sWin->onMenu = onMenu; // Menu bar MenuBarT *menuBar = wmAddMenuBar(sWin); MenuT *fileMenu = wmAddMenu(menuBar, "&File"); wmAddMenuItem(fileMenu, "New &Project...", CMD_PRJ_NEW); wmAddMenuItem(fileMenu, "Open Pro&ject...", CMD_PRJ_OPEN); wmAddMenuItem(fileMenu, "Save Projec&t", CMD_PRJ_SAVE); wmAddMenuItem(fileMenu, "Close Projec&t", CMD_PRJ_CLOSE); wmAddMenuItem(fileMenu, "Project &Properties...", CMD_PRJ_PROPS); wmAddMenuSeparator(fileMenu); wmAddMenuItem(fileMenu, "&Open File...\tCtrl+O", CMD_OPEN); wmAddMenuItem(fileMenu, "&Save File\tCtrl+S", CMD_SAVE); wmAddMenuItem(fileMenu, "Save A&ll", CMD_SAVE_ALL); wmAddMenuSeparator(fileMenu); wmAddMenuItem(fileMenu, "&Remove File", CMD_PRJ_REMOVE); wmAddMenuSeparator(fileMenu); wmAddMenuItem(fileMenu, "E&xit", CMD_EXIT); MenuT *editMenu = wmAddMenu(menuBar, "&Edit"); wmAddMenuItem(editMenu, "Cu&t\tCtrl+X", CMD_CUT); wmAddMenuItem(editMenu, "&Copy\tCtrl+C", CMD_COPY); wmAddMenuItem(editMenu, "&Paste\tCtrl+V", CMD_PASTE); wmAddMenuSeparator(editMenu); wmAddMenuItem(editMenu, "Select &All\tCtrl+A", CMD_SELECT_ALL); wmAddMenuSeparator(editMenu); wmAddMenuItem(editMenu, "&Delete\tDel", CMD_DELETE); MenuT *runMenu = wmAddMenu(menuBar, "&Run"); wmAddMenuItem(runMenu, "&Run\tF5", CMD_RUN); wmAddMenuItem(runMenu, "Run &Without Recompile\tCtrl+F5", CMD_RUN_NOCMP); wmAddMenuItem(runMenu, "&Stop\tEsc", CMD_STOP); wmAddMenuSeparator(runMenu); wmAddMenuItem(runMenu, "&Clear Output", CMD_CLEAR); wmAddMenuSeparator(runMenu); wmAddMenuCheckItem(runMenu, "Save on &Run", CMD_SAVE_ON_RUN, true); MenuT *viewMenu = wmAddMenu(menuBar, "&View"); wmAddMenuItem(viewMenu, "&Code\tF7", CMD_VIEW_CODE); wmAddMenuItem(viewMenu, "&Object\tShift+F7", CMD_VIEW_DESIGN); wmAddMenuSeparator(viewMenu); wmAddMenuCheckItem(viewMenu, "&Toolbar", CMD_VIEW_TOOLBAR, true); wmAddMenuCheckItem(viewMenu, "&Status Bar", CMD_VIEW_STATUS, true); MenuT *winMenu = wmAddMenu(menuBar, "&Window"); wmAddMenuItem(winMenu, "&Code Editor", CMD_WIN_CODE); wmAddMenuItem(winMenu, "&Output", CMD_WIN_OUTPUT); wmAddMenuItem(winMenu, "&Immediate", CMD_WIN_IMM); wmAddMenuSeparator(winMenu); wmAddMenuItem(winMenu, "&Project Explorer", CMD_WIN_PROJECT); wmAddMenuItem(winMenu, "&Toolbox", CMD_WIN_TOOLBOX); wmAddMenuItem(winMenu, "&Properties", CMD_WIN_PROPS); AccelTableT *accel = dvxCreateAccelTable(); dvxAddAccel(accel, 'O', ACCEL_CTRL, CMD_OPEN); dvxAddAccel(accel, 'S', ACCEL_CTRL, CMD_SAVE); dvxAddAccel(accel, KEY_F5, 0, CMD_RUN); dvxAddAccel(accel, KEY_F5, ACCEL_CTRL, CMD_RUN_NOCMP); dvxAddAccel(accel, KEY_F7, 0, CMD_VIEW_CODE); dvxAddAccel(accel, KEY_F7, ACCEL_SHIFT, CMD_VIEW_DESIGN); dvxAddAccel(accel, 0x1B, 0, CMD_STOP); sWin->accelTable = accel; WidgetT *tbRoot = wgtInitWindow(sAc, sWin); sToolbar = wgtToolbar(tbRoot); WidgetT *tb = sToolbar; WidgetT *tbOpen = loadTbIcon(tb, "tb_open", "Open"); tbOpen->onClick = onTbOpen; wgtSetTooltip(tbOpen, "Open (Ctrl+O)"); WidgetT *tbSave = loadTbIcon(tb, "tb_save", "Save"); tbSave->onClick = onTbSave; wgtSetTooltip(tbSave, "Save (Ctrl+S)"); WidgetT *tbRun = loadTbIcon(tb, "tb_run", "Run"); tbRun->onClick = onTbRun; wgtSetTooltip(tbRun, "Run (F5)"); WidgetT *tbStop = loadTbIcon(tb, "tb_stop", "Stop"); tbStop->onClick = onTbStop; wgtSetTooltip(tbStop, "Stop (Esc)"); WidgetT *tbCode = loadTbIcon(tb, "tb_code", "Code"); tbCode->onClick = onTbCode; wgtSetTooltip(tbCode, "Code View (F7)"); WidgetT *tbDesign = loadTbIcon(tb, "tb_design", "Design"); tbDesign->onClick = onTbDesign; wgtSetTooltip(tbDesign, "Design View (Shift+F7)"); sStatusBar = wgtStatusBar(tbRoot); WidgetT *statusBar = sStatusBar; sStatus = wgtLabel(statusBar, ""); sStatus->weight = 100; // Fit height to content, keeping full screen width dvxFitWindowH(sAc, sWin); // Initialize designer (form window created on demand) dsgnInit(&sDesigner, sAc); showOutputWindow(); showImmediateWindow(); } // ============================================================ // basicColorize // ============================================================ // // Syntax colorizer callback for BASIC source code. Scans a single // line and fills the colors array with syntax color indices. static bool isBasicKeyword(const char *word, int32_t wordLen) { static const char *keywords[] = { "AND", "AS", "CALL", "CASE", "CLOSE", "CONST", "DATA", "DECLARE", "DEF", "DEFINT", "DEFLNG", "DEFSNG", "DEFDBL", "DEFSTR", "DIM", "DO", "DOEVENTS", "ELSE", "ELSEIF", "END", "ERASE", "EXIT", "FOR", "FUNCTION", "GET", "GOSUB", "GOTO", "HIDE", "IF", "IMP", "INPUT", "IS", "LET", "LIBRARY", "LINE", "LOAD", "LOOP", "MOD", "MSGBOX", "NEXT", "NOT", "ON", "OPEN", "OPTION", "OR", "PRINT", "PUT", "RANDOMIZE", "READ", "REDIM", "RESTORE", "RESUME", "RETURN", "SEEK", "SELECT", "SHARED", "SHELL", "SHOW", "SLEEP", "STATIC", "STEP", "STOP", "SUB", "SWAP", "THEN", "TO", "TYPE", "UNLOAD", "UNTIL", "WEND", "WHILE", "WRITE", "XOR", NULL }; char upper[32]; if (wordLen <= 0 || wordLen >= 32) { return false; } for (int32_t i = 0; i < wordLen; i++) { upper[i] = (char)toupper((unsigned char)word[i]); } upper[wordLen] = '\0'; for (int32_t i = 0; keywords[i]; i++) { if (strcmp(upper, keywords[i]) == 0) { return true; } } return false; } static bool isBasicType(const char *word, int32_t wordLen) { static const char *types[] = { "BOOLEAN", "BYTE", "DOUBLE", "INTEGER", "LONG", "SINGLE", "STRING", "TRUE", "FALSE", NULL }; char upper[32]; if (wordLen <= 0 || wordLen >= 32) { return false; } for (int32_t i = 0; i < wordLen; i++) { upper[i] = (char)toupper((unsigned char)word[i]); } upper[wordLen] = '\0'; for (int32_t i = 0; types[i]; i++) { if (strcmp(upper, types[i]) == 0) { return true; } } return false; } static void basicColorize(const char *line, int32_t lineLen, uint8_t *colors, void *ctx) { (void)ctx; int32_t i = 0; while (i < lineLen) { char ch = line[i]; // Comment: ' or REM if (ch == '\'') { while (i < lineLen) { colors[i++] = 3; // SYNTAX_COMMENT } return; } // String literal if (ch == '"') { colors[i++] = 2; // SYNTAX_STRING while (i < lineLen && line[i] != '"') { colors[i++] = 2; } if (i < lineLen) { colors[i++] = 2; // closing quote } continue; } // 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 } continue; } // Identifier or keyword if (isalpha((unsigned char)ch) || ch == '_') { int32_t start = i; while (i < lineLen && (isalnum((unsigned char)line[i]) || line[i] == '_' || line[i] == '$' || line[i] == '%' || line[i] == '&' || line[i] == '!' || line[i] == '#')) { i++; } int32_t wordLen = i - start; // 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 } return; } uint8_t c = 0; // default if (isBasicKeyword(line + start, wordLen)) { c = 1; // SYNTAX_KEYWORD } else if (isBasicType(line + start, wordLen)) { c = 6; // SYNTAX_TYPE } for (int32_t j = start; j < i; j++) { colors[j] = c; } continue; } // Operators if (ch == '=' || ch == '<' || ch == '>' || ch == '+' || ch == '-' || ch == '*' || ch == '/' || ch == '\\' || ch == '&') { colors[i++] = 5; // SYNTAX_OPERATOR continue; } // Default (whitespace, parens, etc.) colors[i++] = 0; } } // ============================================================ // clearOutput // ============================================================ static void setOutputText(const char *text) { if (sOutput) { wgtSetText(sOutput, text); } } static void clearOutput(void) { sOutputBuf[0] = '\0'; sOutputLen = 0; setOutputText(""); } // ============================================================ // compileAndRun // ============================================================ static void compileAndRun(void) { // Save all dirty files before compiling if Save on Run is enabled if (sWin && sWin->menuBar && wmMenuItemIsChecked(sWin->menuBar, CMD_SAVE_ON_RUN)) { 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(); setStatus("Compiling..."); // Force a display update so the status is visible dvxInvalidateWindow(sAc, sWin); // Build source: either concatenate project files or use editor contents char *concatBuf = NULL; const char *src = NULL; int32_t srcLen = 0; if (sProject.projectPath[0] != '\0' && sProject.fileCount > 0) { // Stash current editor state if (sProject.activeFileIdx >= 0) { PrjFileT *cur = &sProject.files[sProject.activeFileIdx]; 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) concatBuf = (char *)malloc(IDE_MAX_SOURCE); if (!concatBuf) { setStatus("Out of memory."); return; } int32_t pos = 0; int32_t line = 1; arrfree(sProject.sourceMap); sProject.sourceMap = NULL; sProject.sourceMapCount = 0; for (int32_t i = 0; i < sProject.fileCount; i++) { 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]; prjFullPath(&sProject, i, fullPath, sizeof(fullPath)); FILE *f = fopen(fullPath, "r"); if (!f) { continue; } fseek(f, 0, SEEK_END); long size = ftell(f); fseek(f, 0, SEEK_SET); if (size > 0 && size < IDE_MAX_SOURCE) { diskBuf = (char *)malloc(size + 1); if (diskBuf) { 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; } } } } fclose(f); } if (!fileSrc) { continue; } int32_t startLine = line; int32_t fileLen = (int32_t)strlen(fileSrc); int32_t copyLen = fileLen; if (pos + copyLen >= IDE_MAX_SOURCE - 1) { copyLen = IDE_MAX_SOURCE - 1 - pos; } memcpy(concatBuf + pos, fileSrc, copyLen); pos += copyLen; // Count lines for (int32_t j = 0; j < copyLen; j++) { if (fileSrc[j] == '\n') { line++; } } free(diskBuf); // Ensure a trailing newline between files if (copyLen > 0 && concatBuf[pos - 1] != '\n' && pos < IDE_MAX_SOURCE - 1) { concatBuf[pos++] = '\n'; line++; } { PrjSourceMapT mapEntry; mapEntry.fileIdx = i; mapEntry.startLine = startLine; mapEntry.lineCount = line - startLine; arrput(sProject.sourceMap, mapEntry); sProject.sourceMapCount = (int32_t)arrlen(sProject.sourceMap); } } concatBuf[pos] = '\0'; src = concatBuf; srcLen = pos; } else { // No project files -- compile the full source src = getFullSource(); if (!src || *src == '\0') { setStatus("No source code to run."); return; } srcLen = (int32_t)strlen(src); } // Compile (heap-allocated -- BasParserT is ~300KB, too large for stack) BasParserT *parser = (BasParserT *)malloc(sizeof(BasParserT)); if (!parser) { free(concatBuf); setStatus("Out of memory."); return; } basParserInit(parser, src, srcLen); if (!basParse(parser)) { int32_t n = snprintf(sOutputBuf, IDE_MAX_OUTPUT, "COMPILE ERROR:\n%s\n", parser->error); sOutputLen = n; setOutputText(sOutputBuf); // Jump to error line -- translate through source map if project if (parser->errorLine > 0 && sEditor) { int32_t fileIdx = -1; int32_t localLine = parser->errorLine; if (sProject.fileCount > 0 && prjMapLine(&sProject, parser->errorLine, &fileIdx, &localLine)) { // Open the offending file if it's not already active if (fileIdx != sProject.activeFileIdx) { onPrjFileClick(fileIdx, false); } } wgtTextAreaGoToLine(sEditor, localLine); } setStatus("Compilation failed."); basParserFree(parser); free(parser); free(concatBuf); return; } free(concatBuf); BasModuleT *mod = basParserBuildModule(parser); basParserFree(parser); free(parser); if (!mod) { setStatus("Failed to build module."); return; } // Cache the compiled module for Ctrl+F5 if (sCachedModule) { basModuleFree(sCachedModule); } sCachedModule = mod; // Update Object/Event dropdowns updateDropdowns(); runModule(mod); } // ============================================================ // runCached // ============================================================ static void runCached(void) { if (!sCachedModule) { setStatus("No compiled program. Press F5 to compile first."); return; } clearOutput(); runModule(sCachedModule); } // ============================================================ // runModule // ============================================================ static void runModule(BasModuleT *mod) { setStatus("Running..."); // Create VM BasVmT *vm = basVmCreate(); basVmLoadModule(vm, mod); // Set up implicit main frame vm->callStack[0].localCount = mod->globalCount > BAS_VM_MAX_LOCALS ? BAS_VM_MAX_LOCALS : mod->globalCount; vm->callDepth = 1; // Set I/O callbacks basVmSetPrintCallback(vm, printCallback, NULL); basVmSetInputCallback(vm, inputCallback, NULL); basVmSetDoEventsCallback(vm, doEventsCallback, NULL); // Create form runtime (bridges UI opcodes to DVX widgets) BasFormRtT *formRt = basFormRtCreate(sAc, vm, 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 basVmSetStepLimit(vm, IDE_STEP_SLICE); int32_t totalSteps = 0; BasVmResultE result; for (;;) { result = basVmRun(vm); totalSteps += vm->stepCount; if (result == BAS_VM_STEP_LIMIT) { // Yield to DVX to keep the GUI responsive dvxUpdate(sAc); // Stop if IDE window was closed or DVX is shutting down if (!sWin || !sAc->running) { break; } continue; } if (result == BAS_VM_HALTED) { break; } // Runtime error int32_t pos = sOutputLen; int32_t n = snprintf(sOutputBuf + pos, IDE_MAX_OUTPUT - pos, "\n[Runtime error: %s]\n", basVmGetError(vm)); sOutputLen += n; setOutputText(sOutputBuf); break; } sVm = NULL; // Update output display setOutputText(sOutputBuf); static char statusBuf[128]; snprintf(statusBuf, sizeof(statusBuf), "Done. %ld instructions executed.", (long)totalSteps); setStatus(statusBuf); basFormRtDestroy(formRt); basVmDestroy(vm); } // ============================================================ // doEventsCallback // ============================================================ static bool doEventsCallback(void *ctx) { (void)ctx; // Stop if IDE window was closed or DVX is shutting down if (!sWin || !sAc->running) { return false; } dvxUpdate(sAc); return sWin != NULL && sAc->running; } // ============================================================ // evaluateImmediate // ============================================================ // // Compile and execute a single line from the Immediate window. // If the line doesn't start with PRINT, wrap it in PRINT so // expressions produce visible output. static void immPrintCallback(void *ctx, const char *text, bool newline) { (void)ctx; if (!sImmediate) { return; } // Append output to the immediate window const char *cur = wgtGetText(sImmediate); int32_t curLen = cur ? (int32_t)strlen(cur) : 0; int32_t textLen = text ? (int32_t)strlen(text) : 0; if (curLen + textLen + 2 < IDE_MAX_IMM) { static char immBuf[IDE_MAX_IMM]; memcpy(immBuf, cur, curLen); memcpy(immBuf + curLen, text, textLen); curLen += textLen; if (newline) { immBuf[curLen++] = '\n'; } immBuf[curLen] = '\0'; wgtSetText(sImmediate, immBuf); // Move cursor to end so user can keep typing int32_t lines = 1; for (int32_t i = 0; i < curLen; i++) { if (immBuf[i] == '\n') { lines++; } } wgtTextAreaGoToLine(sImmediate, lines); } } static void evaluateImmediate(const char *expr) { if (!expr || *expr == '\0') { return; } char wrapped[1024]; // If it already starts with a statement keyword, use as-is if (strncasecmp(expr, "PRINT", 5) == 0 || strncasecmp(expr, "DIM", 3) == 0 || strncasecmp(expr, "LET", 3) == 0) { snprintf(wrapped, sizeof(wrapped), "%s", expr); } else { snprintf(wrapped, sizeof(wrapped), "PRINT %s", expr); } BasParserT *parser = (BasParserT *)malloc(sizeof(BasParserT)); if (!parser) { return; } basParserInit(parser, wrapped, (int32_t)strlen(wrapped)); if (!basParse(parser)) { // Show error inline immPrintCallback(NULL, "Error: ", false); immPrintCallback(NULL, parser->error, true); basParserFree(parser); free(parser); return; } BasModuleT *mod = basParserBuildModule(parser); basParserFree(parser); free(parser); if (!mod) { return; } BasVmT *vm = basVmCreate(); basVmLoadModule(vm, mod); vm->callStack[0].localCount = mod->globalCount > BAS_VM_MAX_LOCALS ? BAS_VM_MAX_LOCALS : mod->globalCount; vm->callDepth = 1; basVmSetPrintCallback(vm, immPrintCallback, NULL); BasVmResultE result = basVmRun(vm); if (result != BAS_VM_HALTED && result != BAS_VM_OK) { immPrintCallback(NULL, "Error: ", false); immPrintCallback(NULL, basVmGetError(vm), true); } basVmDestroy(vm); basModuleFree(mod); } // ============================================================ // inputCallback // ============================================================ static bool inputCallback(void *ctx, const char *prompt, char *buf, int32_t bufSize) { (void)ctx; // Append prompt to output if (prompt && sOutputLen < IDE_MAX_OUTPUT - 1) { int32_t n = snprintf(sOutputBuf + sOutputLen, IDE_MAX_OUTPUT - sOutputLen, "%s", prompt); sOutputLen += n; setOutputText(sOutputBuf); } 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; } int32_t bytesRead = (int32_t)fread(sSourceBuf, 1, size, f); fclose(f); sSourceBuf[bytesRead] = '\0'; if (!sCodeWin) { showCodeWindow(); } // Parse into per-procedure buffers and show (General) section parseProcs(sSourceBuf); snprintf(sFilePath, sizeof(sFilePath), "%s", path); if (sFormWin) { dvxDestroyWindow(sAc, sFormWin); cleanupFormWin(); } dsgnFree(&sDesigner); updateDropdowns(); showProc(-1); // show (General) section 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 // a matching .frm file if one exists alongside the .bas. static void ensureProject(const char *filePath) { if (sProject.projectPath[0] != '\0') { return; } // Derive directory and base name from path char dir[DVX_MAX_PATH]; char baseName[PRJ_MAX_NAME]; snprintf(dir, sizeof(dir), "%s", filePath); char *sep = strrchr(dir, '/'); char *sep2 = strrchr(dir, '\\'); if (sep2 > sep) { sep = sep2; } const char *fileName = filePath; if (sep) { fileName = sep + 1; *sep = '\0'; } else { dir[0] = '.'; dir[1] = '\0'; } // Strip extension for project name // 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 nl = (int32_t)strlen(fileName); if (nl >= PRJ_MAX_NAME) { nl = PRJ_MAX_NAME - 1; } memcpy(baseName, fileName, nl); baseName[nl] = '\0'; char *dot = strrchr(baseName, '.'); if (dot) { *dot = '\0'; } prjNew(&sProject, baseName, dir); // Determine if this is a .bas or .frm const char *ext = strrchr(filePath, '.'); bool isForm = (ext && strcasecmp(ext, ".frm") == 0); prjAddFile(&sProject, fileName, isForm); // If it's a .bas, check for a matching .frm and add it too if (!isForm) { char frmPath[DVX_MAX_PATH]; snprintf(frmPath, sizeof(frmPath), "%s", filePath); char *frmDot = strrchr(frmPath, '.'); 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"); if (frmFile) { fclose(frmFile); // Get just the filename portion const char *frmName = strrchr(frmPath, '/'); const char *frmName2 = strrchr(frmPath, '\\'); if (frmName2 > frmName) { frmName = frmName2; } frmName = frmName ? frmName + 1 : frmPath; prjAddFile(&sProject, frmName, true); } } sProject.dirty = false; sProject.activeFileIdx = 0; char title[300]; snprintf(title, sizeof(title), "DVX BASIC - [%s]", sProject.name); if (sWin) { dvxSetTitle(sAc, sWin, title); } } static void loadFile(void) { if (!promptAndSave()) { return; } FileFilterT filters[] = { { "BASIC Files (*.bas)", "*.bas" }, { "Form Files (*.frm)", "*.frm" }, { "All Files (*.*)", "*.*" } }; char path[DVX_MAX_PATH]; if (dvxFileDialog(sAc, "Open BASIC File", FD_OPEN, NULL, filters, 3, path, sizeof(path))) { closeProject(); ensureProject(path); const char *ext = strrchr(path, '.'); bool isForm = (ext && strcasecmp(ext, ".frm") == 0); if (isForm) { onPrjFileClick(0, true); } else { loadFilePath(path); } } } // ============================================================ // saveFile // ============================================================ static void saveActiveFile(void) { if (sProject.projectPath[0] == '\0') { return; } int32_t idx = sProject.activeFileIdx; if (idx < 0 || idx >= sProject.fileCount) { return; } PrjFileT *file = &sProject.files[idx]; char fullPath[DVX_MAX_PATH]; prjFullPath(&sProject, idx, fullPath, sizeof(fullPath)); if (file->isForm && sDesigner.form) { // Save editor code back to form->code before saving if (sCurProcIdx >= -1) { saveCurProc(); free(sDesigner.form->code); sDesigner.form->code = strdup(getFullSource()); } // Save form designer state to .frm file 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); } } else if (!file->isForm) { // Save full source (splice current proc back first) const char *src = getFullSource(); 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; } } } setStatus("Saved."); } static void saveFile(void) { // If no project or no active file, prompt for a path if (sProject.projectPath[0] == '\0' || sProject.activeFileIdx < 0) { if (sFilePath[0] == '\0') { FileFilterT filters[] = { { "BASIC Files (*.bas)", "*.bas" }, { "All Files (*.*)", "*.*" } }; if (!dvxFileDialog(sAc, "Save BASIC File", FD_SAVE, NULL, filters, 2, sFilePath, sizeof(sFilePath))) { return; } ensureProject(sFilePath); } // Save editor to sFilePath directly const char *src = sEditor ? wgtGetText(sEditor) : NULL; if (src) { FILE *f = fopen(sFilePath, "w"); if (f) { fputs(src, f); fclose(f); } } setStatus("Saved."); return; } // Save the active project file saveActiveFile(); // Also save any other dirty forms for (int32_t i = 0; i < sProject.fileCount; i++) { if (i == sProject.activeFileIdx) { continue; } if (sProject.files[i].isForm && 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; } } } } // ============================================================ // onPrjFileClick -- called when a file is clicked in the project tree // ============================================================ static void onPrjFileClick(int32_t fileIdx, bool isForm) { if (fileIdx < 0 || fileIdx >= sProject.fileCount) { return; } if (fileIdx == sProject.activeFileIdx) { return; } // Stash current active file's contents into its buffer if (sProject.activeFileIdx >= 0) { PrjFileT *cur = &sProject.files[sProject.activeFileIdx]; if (cur->isForm && sDesigner.form) { // 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; } cur->modified = true; } } 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; } } 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 (sDesigner.form) { dsgnFree(&sDesigner); } dsgnNewForm(&sDesigner, formName); 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; } if (sDesigner.form) { dsgnFree(&sDesigner); } dsgnLoadFrm(&sDesigner, frmSrc, (int32_t)strlen(frmSrc)); free(diskBuf); sProject.activeFileIdx = fileIdx; switchToDesign(); } else { // Load .bas file from buffer or disk 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; } } sProject.activeFileIdx = fileIdx; } } // ============================================================ // newProject // ============================================================ static void newProject(void) { char name[PRJ_MAX_NAME]; if (!dvxInputBox(sAc, "New Project", "Project name:", "", name, sizeof(name))) { return; } if (name[0] == '\0') { return; } // Ask for directory via save dialog (file = name.dbp) FileFilterT filters[] = { { "Project Files (*.dbp)", "*.dbp" } }; char dbpPath[DVX_MAX_PATH]; snprintf(dbpPath, sizeof(dbpPath), "%s.dbp", name); if (!dvxFileDialog(sAc, "Save New Project", FD_SAVE, NULL, filters, 1, dbpPath, sizeof(dbpPath))) { return; } closeProject(); // Derive directory from chosen path char dir[DVX_MAX_PATH]; snprintf(dir, sizeof(dir), "%s", dbpPath); char *sep = strrchr(dir, '/'); char *sep2 = strrchr(dir, '\\'); if (sep2 > sep) { sep = sep2; } if (sep) { *sep = '\0'; } else { dir[0] = '.'; dir[1] = '\0'; } prjNew(&sProject, name, dir); snprintf(sProject.projectPath, sizeof(sProject.projectPath), "%s", dbpPath); prjSave(&sProject); sProject.dirty = false; // Create and show project window if (!sProjectWin) { sProjectWin = prjCreateWindow(sAc, &sProject, onPrjFileClick); if (sProjectWin) { sProjectWin->y = toolbarBottom() + 25; sProjectWin->onClose = onProjectWinClose; } } else { prjRebuildTree(&sProject); } char title[300]; snprintf(title, sizeof(title), "DVX BASIC - [%s]", sProject.name); dvxSetTitle(sAc, sWin, title); setStatus("New project created."); } // ============================================================ // openProject // ============================================================ static void openProject(void) { FileFilterT filters[] = { { "Project Files (*.dbp)", "*.dbp" }, { "All Files (*.*)", "*.*" } }; char path[DVX_MAX_PATH]; if (!dvxFileDialog(sAc, "Open Project", FD_OPEN, NULL, filters, 2, path, sizeof(path))) { return; } closeProject(); if (!prjLoad(&sProject, path)) { dvxMessageBox(sAc, "Error", "Could not open project file.", MB_OK | MB_ICONERROR); return; } // Create and show project window if (!sProjectWin) { sProjectWin = prjCreateWindow(sAc, &sProject, onPrjFileClick); if (sProjectWin) { sProjectWin->y = toolbarBottom() + 25; sProjectWin->onClose = onProjectWinClose; } } else { prjRebuildTree(&sProject); } // Open the first .bas file in the editor for (int32_t i = 0; i < sProject.fileCount; i++) { if (!sProject.files[i].isForm) { onPrjFileClick(i, false); break; } } char title[300]; snprintf(title, sizeof(title), "DVX BASIC - [%s]", sProject.name); dvxSetTitle(sAc, sWin, title); setStatus("Project loaded."); } // ============================================================ // closeProject // ============================================================ static void closeProject(void) { if (sProject.projectPath[0] == '\0') { return; } if (sProject.dirty) { prjSave(&sProject); } prjClose(&sProject); if (sProjectWin) { prjDestroyWindow(sAc, sProjectWin); sProjectWin = NULL; } if (sWin) { dvxSetTitle(sAc, sWin, "DVX BASIC"); } if (sStatus) { setStatus("Project closed."); } } // ============================================================ // ideRenameInCode -- rename form/control references in all .bas files // ============================================================ // // Case-insensitive replacement of OldName followed by '.' or '_' with // NewName followed by the same delimiter. This handles: // ControlName.Property -> NewName.Property // ControlName_Click -> NewName_Click (event handlers) // FormName.ControlName.X -> NewFormName.ControlName.X // Sub FormName_Load -> Sub NewFormName_Load // // Word-boundary check on the left: the character before the match must // be a non-identifier character (space, tab, newline, '.', '(', start // of string) to avoid replacing "Command1" inside "MyCommand1". static bool isIdentChar(char c) { return (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '_'; } // Check if position i is inside a string literal or comment. // Scans from the start of the line containing i. static bool isInStringOrComment(const char *src, int32_t i) { // Find start of line int32_t lineStart = i; while (lineStart > 0 && src[lineStart - 1] != '\n') { lineStart--; } bool inString = false; for (int32_t j = lineStart; j < i; j++) { if (src[j] == '"') { inString = !inString; } else if (src[j] == '\'' && !inString) { // Rest of line is a comment return true; } } return inString; } static char *renameInBuffer(const char *src, const char *oldName, const char *newName) { if (!src || !oldName || !newName || !oldName[0]) { return NULL; } int32_t oldLen = (int32_t)strlen(oldName); int32_t newLen = (int32_t)strlen(newName); int32_t srcLen = (int32_t)strlen(src); // First pass: count replacements to compute output size int32_t count = 0; for (int32_t i = 0; i <= srcLen - oldLen; i++) { if (strncasecmp(&src[i], oldName, oldLen) != 0) { continue; } char after = src[i + oldLen]; if (after != '.' && after != '_') { continue; } if (i > 0 && isIdentChar(src[i - 1])) { continue; } if (isInStringOrComment(src, i)) { continue; } count++; } if (count == 0) { return NULL; } // Allocate output int32_t outLen = srcLen + count * (newLen - oldLen); char *out = (char *)malloc(outLen + 1); if (!out) { return NULL; } // Second pass: build output int32_t op = 0; for (int32_t i = 0; i < srcLen; ) { if (i <= srcLen - oldLen && strncasecmp(&src[i], oldName, oldLen) == 0) { char after = src[i + oldLen]; if ((after == '.' || after == '_') && (i == 0 || !isIdentChar(src[i - 1])) && !isInStringOrComment(src, i)) { memcpy(out + op, newName, newLen); op += newLen; i += oldLen; continue; } } out[op++] = src[i++]; } out[op] = '\0'; return out; } void ideRenameInCode(const char *oldName, const char *newName) { if (!oldName || !newName || strcasecmp(oldName, newName) == 0) { return; } // Rename in the per-procedure buffers (form code currently being edited) if (sGeneralBuf) { char *replaced = renameInBuffer(sGeneralBuf, 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) { free(sProcBufs[i]); sProcBufs[i] = replaced; } } } // 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]); } } // 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; } char *buf = sProject.files[i].buffer; if (!buf) { // Load from disk char fullPath[DVX_MAX_PATH]; prjFullPath(&sProject, i, fullPath, sizeof(fullPath)); FILE *f = fopen(fullPath, "r"); if (!f) { continue; } fseek(f, 0, SEEK_END); long size = ftell(f); fseek(f, 0, SEEK_SET); if (size <= 0 || size >= IDE_MAX_SOURCE) { fclose(f); continue; } buf = (char *)malloc(size + 1); if (!buf) { fclose(f); continue; } int32_t br = (int32_t)fread(buf, 1, size, f); fclose(f); buf[br] = '\0'; sProject.files[i].buffer = buf; } char *replaced = renameInBuffer(buf, oldName, newName); if (replaced) { free(sProject.files[i].buffer); sProject.files[i].buffer = replaced; 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; } } // ============================================================ // onProjectWinClose -- user closed the project window via X button // ============================================================ static void onProjectWinClose(WindowT *win) { prjDestroyWindow(sAc, win); sProjectWin = NULL; } // ============================================================ // loadFrmFiles // ============================================================ // // 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"); if (!f) { 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; } char *frmBuf = (char *)malloc(size + 1); if (!frmBuf) { fclose(f); return; } int32_t bytesRead = (int32_t)fread(frmBuf, 1, size, f); fclose(f); frmBuf[bytesRead] = '\0'; basFormRtLoadFrm(rt, frmBuf, bytesRead); free(frmBuf); } static void loadFrmFiles(BasFormRtT *rt) { for (int32_t i = 0; i < sProject.fileCount; i++) { if (sProject.files[i].isForm) { char fullPath[DVX_MAX_PATH]; prjFullPath(&sProject, i, fullPath, sizeof(fullPath)); loadFrmFile(rt, fullPath); } } } // ============================================================ // onContentFocus -- track last focused content window for clipboard // ============================================================ static void onContentFocus(WindowT *win) { sLastFocusWin = win; } static WindowT *getLastFocusWin(void) { if (sLastFocusWin == sCodeWin || sLastFocusWin == sOutWin || sLastFocusWin == sImmWin) { return sLastFocusWin; } sLastFocusWin = NULL; return NULL; } // ============================================================ // hasUnsavedData -- check if any project files have unsaved changes // ============================================================ static bool hasUnsavedData(void) { // Check the active editor/designer if (sDesigner.form && sDesigner.form->dirty) { return true; } // Check all project files for (int32_t i = 0; i < sProject.fileCount; i++) { if (sProject.files[i].modified) { return true; } } return sProject.dirty; } // ============================================================ // promptAndSave -- ask user to save, discard, or cancel // ============================================================ // // Returns true if the caller should proceed (user saved or discarded). // Returns false if the user cancelled. static bool promptAndSave(void) { if (!hasUnsavedData()) { return true; } int32_t result = dvxPromptSave(sAc, "DVX BASIC"); if (result == DVX_SAVE_YES) { saveFile(); return true; } return result == DVX_SAVE_NO; } // ============================================================ // onClose // ============================================================ static void onClose(WindowT *win) { if (!promptAndSave()) { return; } // Prevent stale focus tracking during shutdown sLastFocusWin = NULL; // Null widget pointers first so nothing references destroyed widgets sEditor = NULL; sOutput = NULL; sImmediate = NULL; sObjDropdown = NULL; sEvtDropdown = NULL; sStatus = NULL; sToolbar = NULL; sStatusBar = NULL; // Close all child windows // Close all child windows if (sCodeWin && sCodeWin != win) { dvxDestroyWindow(sAc, sCodeWin); } sCodeWin = NULL; if (sOutWin && sOutWin != win) { dvxDestroyWindow(sAc, sOutWin); } sOutWin = NULL; if (sImmWin && sImmWin != win) { dvxDestroyWindow(sAc, sImmWin); } sImmWin = NULL; if (sFormWin) { dvxDestroyWindow(sAc, sFormWin); cleanupFormWin(); } if (sToolboxWin) { tbxDestroy(sAc, sToolboxWin); sToolboxWin = NULL; } if (sPropsWin) { prpDestroy(sAc, sPropsWin); sPropsWin = NULL; } if (sProjectWin) { prjDestroyWindow(sAc, sProjectWin); sProjectWin = NULL; } closeProject(); // Don't destroy win here -- the shell manages it. Destroying // it from inside onClose crashes because the calling code in // dvxApp.c still references the window after the callback returns. sWin = NULL; if (sCachedModule) { basModuleFree(sCachedModule); sCachedModule = NULL; } dsgnFree(&sDesigner); freeProcBufs(); arrfree(sProcTable); arrfree(sObjItems); arrfree(sEvtItems); sProcTable = NULL; sObjItems = NULL; sEvtItems = NULL; dvxDestroyWindow(sAc, win); } // ============================================================ // onMenu // ============================================================ static void onMenu(WindowT *win, int32_t menuId) { (void)win; switch (menuId) { case CMD_OPEN: loadFile(); break; case CMD_SAVE: saveFile(); break; 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; } 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; } } } // Save the project file if (sProject.projectPath[0] != '\0') { prjSave(&sProject); sProject.dirty = false; } setStatus("All files saved."); break; case CMD_RUN: compileAndRun(); break; case CMD_RUN_NOCMP: runCached(); break; case CMD_STOP: if (sVm) { sVm->running = false; setStatus("Program stopped."); } break; case CMD_CLEAR: clearOutput(); break; case CMD_VIEW_CODE: switchToCode(); break; case CMD_VIEW_DESIGN: switchToDesign(); break; case CMD_WIN_CODE: showCodeWindow(); break; case CMD_WIN_OUTPUT: showOutputWindow(); break; case CMD_WIN_IMM: showImmediateWindow(); break; case CMD_WIN_TOOLBOX: if (!sToolboxWin) { sToolboxWin = tbxCreate(sAc, &sDesigner); if (sToolboxWin) { sToolboxWin->y = toolbarBottom(); } } break; case CMD_WIN_PROPS: if (!sPropsWin) { sPropsWin = prpCreate(sAc, &sDesigner); if (sPropsWin) { sPropsWin->y = toolbarBottom(); } } 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]; 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; 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; case CMD_PRJ_NEW: newProject(); break; case CMD_PRJ_OPEN: openProject(); break; case CMD_PRJ_SAVE: if (sProject.projectPath[0] != '\0') { prjSave(&sProject); sProject.dirty = false; setStatus("Project saved."); } break; case CMD_PRJ_CLOSE: if (promptAndSave()) { closeProject(); } break; case CMD_PRJ_PROPS: if (sProject.projectPath[0] != '\0') { if (prjPropertiesDialog(sAc, &sProject, sCtx->appPath)) { char title[300]; snprintf(title, sizeof(title), "DVX BASIC - [%s]", sProject.name); dvxSetTitle(sAc, sWin, title); if (sProjectWin) { prjRebuildTree(&sProject); } } } break; case CMD_PRJ_REMOVE: if (sProject.activeFileIdx >= 0) { PrjFileT *rmFile = &sProject.files[sProject.activeFileIdx]; if (rmFile->modified) { int32_t result = dvxPromptSave(sAc, "DVX BASIC"); if (result == DVX_SAVE_CANCEL) { break; } if (result == DVX_SAVE_YES) { saveActiveFile(); } } prjRemoveFile(&sProject, sProject.activeFileIdx); prjRebuildTree(&sProject); } break; case CMD_WIN_PROJECT: if (!sProjectWin) { sProjectWin = prjCreateWindow(sAc, &sProject, onPrjFileClick); if (sProjectWin) { sProjectWin->y = toolbarBottom() + 25; sProjectWin->onClose = onProjectWinClose; } } break; case CMD_EXIT: if (sWin) { onClose(sWin); } break; } } // ============================================================ // onEvtDropdownChange // ============================================================ // // Navigate to the selected procedure when the event dropdown changes. static void onEvtDropdownChange(WidgetT *w) { (void)w; if (!sObjDropdown || !sEvtDropdown || !sEditor) { return; } int32_t objIdx = wgtDropdownGetSelected(sObjDropdown); int32_t evtIdx = wgtDropdownGetSelected(sEvtDropdown); if (objIdx < 0 || objIdx >= (int32_t)arrlen(sObjItems) || evtIdx < 0 || evtIdx >= (int32_t)arrlen(sEvtItems)) { return; } const char *selObj = sObjItems[objIdx]; const char *selEvt = sEvtItems[evtIdx]; // Strip brackets if present (unimplemented event) char evtName[64]; if (selEvt[0] == '[') { snprintf(evtName, sizeof(evtName), "%s", selEvt + 1); int32_t len = (int32_t)strlen(evtName); if (len > 0 && evtName[len - 1] == ']') { evtName[len - 1] = '\0'; } } else { snprintf(evtName, sizeof(evtName), "%s", selEvt); } // Search for an existing proc matching object + event int32_t procCount = (int32_t)arrlen(sProcTable); for (int32_t i = 0; i < procCount; i++) { if (strcasecmp(sProcTable[i].objName, selObj) == 0 && strcasecmp(sProcTable[i].evtName, evtName) == 0) { showProc(i); return; } } // Not found -- create a new sub skeleton char subName[128]; snprintf(subName, sizeof(subName), "%s_%s", selObj, evtName); char skeleton[256]; snprintf(skeleton, sizeof(skeleton), "Sub %s ()\n\nEnd Sub\n", subName); arrput(sProcBufs, strdup(skeleton)); // Update form code if editing a form if (sDesigner.form) { free(sDesigner.form->code); sDesigner.form->code = strdup(getFullSource()); sDesigner.form->dirty = true; } updateDropdowns(); // Show the new proc (last in the list) procCount = (int32_t)arrlen(sProcTable); for (int32_t i = 0; i < procCount; i++) { if (strcasecmp(sProcTable[i].objName, selObj) == 0 && strcasecmp(sProcTable[i].evtName, evtName) == 0) { showProc(i); return; } } } // ============================================================ // onImmediateChange // ============================================================ // // Detect Enter in the Immediate window and evaluate the last line. static void onImmediateChange(WidgetT *w) { (void)w; if (!sImmediate) { return; } const char *text = wgtGetText(sImmediate); if (!text) { return; } int32_t len = (int32_t)strlen(text); if (len < 2 || text[len - 1] != '\n') { return; } // Find the start of the line before the trailing newline int32_t lineEnd = len - 1; int32_t lineStart = lineEnd - 1; while (lineStart > 0 && text[lineStart - 1] != '\n') { lineStart--; } if (lineStart >= lineEnd) { return; } // Extract the line char expr[512]; int32_t lineLen = lineEnd - lineStart; if (lineLen >= (int32_t)sizeof(expr)) { lineLen = (int32_t)sizeof(expr) - 1; } memcpy(expr, text + lineStart, lineLen); expr[lineLen] = '\0'; evaluateImmediate(expr); } // ============================================================ // onObjDropdownChange // ============================================================ // // Update the Event dropdown when the Object selection changes. // Common events available on all controls static const char *sCommonEvents[] = { "Click", "DblClick", "Change", "GotFocus", "LostFocus", "KeyPress", "KeyDown", "MouseDown", "MouseUp", "MouseMove", "Scroll", NULL }; // Form-specific events static const char *sFormEvents[] = { "Load", "Unload", "Resize", "Activate", "Deactivate", "KeyPress", "KeyDown", "MouseDown", "MouseUp", "MouseMove", NULL }; // Buffer for event dropdown labels (with [] for unimplemented) static char sEvtLabelBufs[64][32]; static void onObjDropdownChange(WidgetT *w) { (void)w; if (!sObjDropdown || !sEvtDropdown) { return; } int32_t objIdx = wgtDropdownGetSelected(sObjDropdown); if (objIdx < 0 || objIdx >= (int32_t)arrlen(sObjItems)) { return; } const char *selObj = sObjItems[objIdx]; // Collect which events already have code arrsetlen(sEvtItems, 0); int32_t procCount = (int32_t)arrlen(sProcTable); const char **existingEvts = NULL; // stb_ds temp array for (int32_t i = 0; i < procCount; i++) { if (strcasecmp(sProcTable[i].objName, selObj) == 0) { arrput(existingEvts, sProcTable[i].evtName); } } // Determine which event list to use const char **availEvents = sCommonEvents; if (strcasecmp(selObj, "(General)") == 0) { // (General) has no standard events -- just show existing procs for (int32_t i = 0; i < (int32_t)arrlen(existingEvts); i++) { arrput(sEvtItems, existingEvts[i]); } arrfree(existingEvts); int32_t evtCount = (int32_t)arrlen(sEvtItems); wgtDropdownSetItems(sEvtDropdown, sEvtItems, evtCount); if (evtCount > 0) { wgtDropdownSetSelected(sEvtDropdown, 0); } return; } // Check if this is a form name bool isForm = false; if (sDesigner.form && strcasecmp(selObj, sDesigner.form->name) == 0) { isForm = true; availEvents = sFormEvents; } // Get widget-specific events from the interface const WgtIfaceT *iface = NULL; if (!isForm && sDesigner.form) { for (int32_t i = 0; i < (int32_t)arrlen(sDesigner.form->controls); i++) { if (strcasecmp(sDesigner.form->controls[i].name, selObj) == 0) { const char *wgtName = wgtFindByBasName(sDesigner.form->controls[i].typeName); if (wgtName) { iface = wgtGetIface(wgtName); } break; } } } // Build the event list: standard events + widget-specific events int32_t labelIdx = 0; // Add standard events (common or form) for (int32_t i = 0; availEvents[i]; i++) { bool hasCode = false; for (int32_t j = 0; j < (int32_t)arrlen(existingEvts); j++) { if (strcasecmp(existingEvts[j], availEvents[i]) == 0) { hasCode = true; break; } } if (labelIdx < 64) { if (hasCode) { snprintf(sEvtLabelBufs[labelIdx], 32, "%s", availEvents[i]); } else { snprintf(sEvtLabelBufs[labelIdx], 32, "[%s]", availEvents[i]); } arrput(sEvtItems, sEvtLabelBufs[labelIdx]); labelIdx++; } } // Add widget-specific events if (iface) { for (int32_t i = 0; i < iface->eventCount; i++) { const char *evtName = iface->events[i].name; // Skip if already in the standard list bool alreadyListed = false; for (int32_t j = 0; availEvents[j]; j++) { if (strcasecmp(availEvents[j], evtName) == 0) { alreadyListed = true; break; } } if (alreadyListed) { continue; } bool hasCode = false; for (int32_t j = 0; j < (int32_t)arrlen(existingEvts); j++) { if (strcasecmp(existingEvts[j], evtName) == 0) { hasCode = true; break; } } if (labelIdx < 64) { if (hasCode) { snprintf(sEvtLabelBufs[labelIdx], 32, "%s", evtName); } else { snprintf(sEvtLabelBufs[labelIdx], 32, "[%s]", evtName); } arrput(sEvtItems, sEvtLabelBufs[labelIdx]); labelIdx++; } } } // Add any existing events not in the standard/widget list (custom subs) for (int32_t i = 0; i < (int32_t)arrlen(existingEvts); i++) { bool alreadyListed = false; int32_t evtCount = (int32_t)arrlen(sEvtItems); for (int32_t j = 0; j < evtCount; j++) { const char *label = sEvtItems[j]; // Strip brackets for comparison if (label[0] == '[') { label++; } if (strncasecmp(label, existingEvts[i], strlen(existingEvts[i])) == 0) { alreadyListed = true; break; } } if (!alreadyListed && labelIdx < 64) { snprintf(sEvtLabelBufs[labelIdx], 32, "%s", existingEvts[i]); arrput(sEvtItems, sEvtLabelBufs[labelIdx]); labelIdx++; } } arrfree(existingEvts); int32_t evtCount = (int32_t)arrlen(sEvtItems); wgtDropdownSetItems(sEvtDropdown, sEvtItems, evtCount); if (evtCount > 0) { wgtDropdownSetSelected(sEvtDropdown, 0); } } // ============================================================ // onOpenClick // ============================================================ // ============================================================ // printCallback // ============================================================ static void printCallback(void *ctx, const char *text, bool newline) { (void)ctx; if (!text) { return; } int32_t textLen = (int32_t)strlen(text); // Append to output buffer if (sOutputLen + textLen < IDE_MAX_OUTPUT - 2) { memcpy(sOutputBuf + sOutputLen, text, textLen); sOutputLen += textLen; } if (newline && sOutputLen < IDE_MAX_OUTPUT - 2) { sOutputBuf[sOutputLen++] = '\n'; } sOutputBuf[sOutputLen] = '\0'; // Update the output textarea immediately so PRINT is visible if (sOutput) { setOutputText(sOutputBuf); } } // ============================================================ // onFormWinMouse // ============================================================ // // Handle mouse events on the form designer window. Coordinates // are relative to the window's client area (content box origin). // ============================================================ // onFormWinKey // ============================================================ static void onFormWinKey(WindowT *win, int32_t key, int32_t mod) { (void)mod; if (key == KEY_DELETE && sDesigner.selectedIdx >= 0) { int32_t prevCount = sDesigner.form ? (int32_t)arrlen(sDesigner.form->controls) : 0; dsgnOnKey(&sDesigner, KEY_DELETE); int32_t newCount = sDesigner.form ? (int32_t)arrlen(sDesigner.form->controls) : 0; if (newCount != prevCount) { prpRebuildTree(&sDesigner); prpRefresh(&sDesigner); if (sFormWin) { dvxInvalidateWindow(sAc, sFormWin); } } return; } // Forward unhandled keys to the widget system widgetOnKey(win, key, mod); } // ============================================================ // onFormWinCursorQuery // ============================================================ static int32_t onFormWinCursorQuery(WindowT *win, int32_t x, int32_t y) { (void)win; if (!sDesigner.form) { return 0; } // Crosshair when placing a new control if (sDesigner.activeTool[0] != '\0') { return CURSOR_CROSSHAIR; } if (!sDesigner.form->controls) { return 0; } int32_t count = (int32_t)arrlen(sDesigner.form->controls); if (sDesigner.selectedIdx < 0 || sDesigner.selectedIdx >= count) { return 0; } DsgnControlT *ctrl = &sDesigner.form->controls[sDesigner.selectedIdx]; if (!ctrl->widget || !ctrl->widget->visible || ctrl->widget->w <= 0 || ctrl->widget->h <= 0) { return 0; } int32_t cx = ctrl->widget->x; int32_t cy = ctrl->widget->y; int32_t cw = ctrl->widget->w; int32_t ch = ctrl->widget->h; int32_t hs = DSGN_HANDLE_SIZE; // SE handle (check first -- overlaps with E and S) if (x >= cx + cw - hs/2 && x < cx + cw + hs/2 && y >= cy + ch - hs/2 && y < cy + ch + hs/2) { return CURSOR_RESIZE_DIAG_NWSE; } // E handle (right edge center) if (x >= cx + cw - hs/2 && x < cx + cw + hs/2 && y >= cy + ch/2 - hs/2 && y < cy + ch/2 + hs/2) { return CURSOR_RESIZE_H; } // S handle (bottom center) if (x >= cx + cw/2 - hs/2 && x < cx + cw/2 + hs/2 && y >= cy + ch - hs/2 && y < cy + ch + hs/2) { return CURSOR_RESIZE_V; } return 0; } // 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; if (!sDesigner.form || !sFormWin) { lastButtons = buttons; return; } if (isDown && !wasDown) { // Detect double-click using the system-wide setting int32_t clicks = multiClickDetect(x, y); 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); } prpRefresh(&sDesigner); if (sFormWin) { dvxInvalidateWindow(sAc, sFormWin); } 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); if (sFormWin) { dvxInvalidateWindow(sAc, sFormWin); } } lastButtons = buttons; } // ============================================================ // onFormWinPaint // ============================================================ // // Draw selection handles after widgets have painted. static void onFormWinPaint(WindowT *win, RectT *dirtyArea) { if (!win) { return; } // Force a full measure + layout + paint cycle. // widgetOnPaint normally skips relayout if root dimensions haven't // changed, but we need it to pick up minH changes from handle drag. if (win->widgetRoot) { widgetCalcMinSizeTree(win->widgetRoot, &sAc->font); win->widgetRoot->w = 0; // force layout pass to re-run } widgetOnPaint(win, dirtyArea); // Then draw selection handles on top int32_t winX = win->contentX; int32_t winY = win->contentY; dsgnPaintOverlay(&sDesigner, winX, winY); } // ============================================================ // onFormWinClose // ============================================================ // cleanupFormWin -- release designer-related state without destroying // the form window itself (the caller handles that). static void cleanupFormWin(void) { sFormWin = NULL; sDesigner.formWin = NULL; if (sToolboxWin) { tbxDestroy(sAc, sToolboxWin); sToolboxWin = NULL; } if (sPropsWin) { prpDestroy(sAc, sPropsWin); sPropsWin = NULL; } } // onFormWinResize -- update form dimensions when the design window is resized static void onFormWinResize(WindowT *win, int32_t newW, int32_t newH) { // Let the widget system handle the layout recalculation widgetOnResize(win, newW, newH); if (sDesigner.form) { sDesigner.form->width = newW; sDesigner.form->height = newH; sDesigner.form->dirty = true; prpRefresh(&sDesigner); } } // onFormWinClose -- shell callback when user clicks X on the form window. static void onFormWinClose(WindowT *win) { dvxDestroyWindow(sAc, win); cleanupFormWin(); } // ============================================================ // switchToCode // ============================================================ static void switchToCode(void) { // 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."); } // ============================================================ // switchToDesign // ============================================================ 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; } // Load .frm if we don't have a form yet if (!sDesigner.form) { if (sFilePath[0]) { char frmPath[DVX_MAX_PATH]; snprintf(frmPath, sizeof(frmPath), "%s", sFilePath); char *dot = strrchr(frmPath, '.'); 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"); if (f) { fseek(f, 0, SEEK_END); long size = ftell(f); fseek(f, 0, SEEK_SET); if (size > 0 && size < IDE_MAX_SOURCE) { char *buf = (char *)malloc(size + 1); if (buf) { int32_t bytesRead = (int32_t)fread(buf, 1, size, f); buf[bytesRead] = '\0'; dsgnLoadFrm(&sDesigner, buf, bytesRead); free(buf); } } fclose(f); } } if (!sDesigner.form) { dsgnNewForm(&sDesigner, "Form1"); } } // Create the form designer window (same size as runtime) const char *formName = sDesigner.form ? sDesigner.form->name : "Form1"; char title[128]; snprintf(title, sizeof(title), "%s [Design]", formName); sFormWin = dvxCreateWindowCentered(sAc, title, IDE_DESIGN_W, IDE_DESIGN_H, true); if (!sFormWin) { return; } sFormWin->onClose = onFormWinClose; sDesigner.formWin = sFormWin; WidgetT *root = wgtInitWindow(sAc, sFormWin); WidgetT *contentBox; if (sDesigner.form && strcasecmp(sDesigner.form->layout, "HBox") == 0) { contentBox = wgtHBox(root); } else { contentBox = wgtVBox(root); } contentBox->weight = 100; // Override paint and mouse AFTER wgtInitWindow (which sets widgetOnPaint) sFormWin->onPaint = onFormWinPaint; sFormWin->onMouse = onFormWinMouse; sFormWin->onKey = onFormWinKey; sFormWin->onResize = onFormWinResize; sFormWin->onCursorQuery = onFormWinCursorQuery; // Create live widgets for each control dsgnCreateWidgets(&sDesigner, contentBox); // Set form caption as window title if (sDesigner.form && sDesigner.form->caption[0]) { char winTitle[280]; snprintf(winTitle, sizeof(winTitle), "%s [Design]", sDesigner.form->caption); dvxSetTitle(sAc, sFormWin, winTitle); } // Size the form window if (sDesigner.form && sDesigner.form->autoSize) { dvxFitWindow(sAc, sFormWin); sDesigner.form->width = sFormWin->w; sDesigner.form->height = sFormWin->h; } else if (sDesigner.form) { dvxResizeWindow(sAc, sFormWin, sDesigner.form->width, sDesigner.form->height); } // Create toolbox and properties windows if (!sToolboxWin) { sToolboxWin = tbxCreate(sAc, &sDesigner); if (sToolboxWin) { sToolboxWin->y = toolbarBottom(); } } if (!sPropsWin) { sPropsWin = prpCreate(sAc, &sDesigner); if (sPropsWin) { sPropsWin->y = toolbarBottom(); } } setStatus("Design view open."); } // ============================================================ // Toolbar button handlers // ============================================================ static void onTbOpen(WidgetT *w) { (void)w; loadFile(); } static void onTbSave(WidgetT *w) { (void)w; saveFile(); } static void onTbRun(WidgetT *w) { (void)w; compileAndRun(); } static void onTbStop(WidgetT *w) { (void)w; if (sVm) { sVm->running = false; setStatus("Program stopped."); } } static void onTbCode(WidgetT *w) { (void)w; switchToCode(); } static void onTbDesign(WidgetT *w) { (void)w; switchToDesign(); } // ============================================================ // showCodeWindow // ============================================================ static void showCodeWindow(void) { if (sCodeWin) { return; // already open } 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; WidgetT *codeRoot = wgtInitWindow(sAc, sCodeWin); WidgetT *dropdownRow = wgtHBox(codeRoot); dropdownRow->spacing = wgtPixels(4); sObjDropdown = wgtDropdown(dropdownRow); sObjDropdown->weight = 100; sObjDropdown->onChange = onObjDropdownChange; wgtDropdownSetItems(sObjDropdown, NULL, 0); sEvtDropdown = wgtDropdown(dropdownRow); sEvtDropdown->weight = 100; sEvtDropdown->onChange = onEvtDropdownChange; wgtDropdownSetItems(sEvtDropdown, NULL, 0); sEditor = wgtTextArea(codeRoot, IDE_MAX_SOURCE); sEditor->weight = 100; wgtTextAreaSetColorize(sEditor, basicColorize, NULL); wgtTextAreaSetShowLineNumbers(sEditor, true); wgtTextAreaSetAutoIndent(sEditor, true); if (sFilePath[0] && sSourceBuf[0]) { wgtSetText(sEditor, sSourceBuf); } updateDropdowns(); } } // ============================================================ // showOutputWindow // ============================================================ static void showOutputWindow(void) { if (sOutWin) { return; } int32_t outH = 120; int32_t outY = sAc->display.height - outH; sOutWin = dvxCreateWindow(sAc, "Output", 0, outY, sAc->display.width / 2, outH, true); if (sOutWin) { sOutWin->onFocus = onContentFocus; sLastFocusWin = sOutWin; WidgetT *outRoot = wgtInitWindow(sAc, sOutWin); sOutput = wgtTextArea(outRoot, IDE_MAX_OUTPUT); sOutput->weight = 100; sOutput->readOnly = true; if (sOutputLen > 0) { setOutputText(sOutputBuf); } } } // ============================================================ // showImmediateWindow // ============================================================ static void showImmediateWindow(void) { if (sImmWin) { return; } int32_t outH = 120; int32_t outY = sAc->display.height - outH; sImmWin = dvxCreateWindow(sAc, "Immediate", sAc->display.width / 2, outY, sAc->display.width / 2, outH, true); if (sImmWin) { sImmWin->onFocus = onContentFocus; sLastFocusWin = sImmWin; WidgetT *immRoot = wgtInitWindow(sAc, sImmWin); if (immRoot) { sImmediate = wgtTextArea(immRoot, IDE_MAX_IMM); if (sImmediate) { sImmediate->weight = 100; sImmediate->readOnly = false; sImmediate->onChange = onImmediateChange; } else { dvxLog("IDE: failed to create immediate TextArea"); } } else { dvxLog("IDE: failed to init immediate window root"); } } } // ============================================================ // setStatus // ============================================================ static void setStatus(const char *text) { if (sStatus) { wgtSetText(sStatus, text); } } // ============================================================ // updateDropdowns // ============================================================ // // Scan the source for SUB/FUNCTION declarations and populate // 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'; } } } // extractNewProcs -- scan a buffer for Sub/Function declarations that // don't belong (e.g. user typed a new Sub in the General section). // Extracts them into new sProcBufs entries and removes them from the // source buffer. Returns a new buffer (caller frees) or NULL if no // extraction was needed. static char *extractNewProcs(const char *buf) { if (!buf || !buf[0]) { return NULL; } // Scan for Sub/Function at the start of a line bool found = false; const char *pos = buf; while (*pos) { 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) { found = true; break; } while (*pos && *pos != '\n') { pos++; } if (*pos == '\n') { pos++; } } if (!found) { return NULL; } // Build the remaining text (before the first proc) and extract procs int32_t bufLen = (int32_t)strlen(buf); char *remaining = (char *)malloc(bufLen + 1); if (!remaining) { return NULL; } int32_t remPos = 0; pos = buf; while (*pos) { const char *lineStart = pos; 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) { // Extract procedure name for duplicate check const char *np = trimmed + (isSub ? 4 : 9); while (*np == ' ' || *np == '\t') { np++; } char newName[128]; int32_t nn = 0; while (*np && *np != '(' && *np != ' ' && *np != '\t' && *np != '\n' && nn < 127) { newName[nn++] = *np++; } newName[nn] = '\0'; // Check for duplicate against existing proc buffers bool isDuplicate = false; for (int32_t p = 0; p < (int32_t)arrlen(sProcBufs); p++) { if (!sProcBufs[p]) { continue; } const char *ep = sProcBufs[p]; while (*ep == ' ' || *ep == '\t') { ep++; } if (strncasecmp(ep, "SUB ", 4) == 0) { ep += 4; } else if (strncasecmp(ep, "FUNCTION ", 9) == 0) { ep += 9; } while (*ep == ' ' || *ep == '\t') { ep++; } char existName[128]; int32_t en = 0; while (*ep && *ep != '(' && *ep != ' ' && *ep != '\t' && *ep != '\n' && en < 127) { existName[en++] = *ep++; } existName[en] = '\0'; if (strcasecmp(newName, existName) == 0) { isDuplicate = true; break; } } // 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 && *scan != '\n') { scan++; } if (*scan == '\n') { scan++; } 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++; } } if (isDuplicate) { // Leave it in the General section -- compiler will report the error while (*pos && *pos != '\n') { remaining[remPos++] = *pos++; } if (*pos == '\n') { remaining[remPos++] = *pos++; } } else { // Extract this procedure into a new buffer 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; } // Copy non-proc lines to remaining while (*pos && *pos != '\n') { remaining[remPos++] = *pos++; } if (*pos == '\n') { remaining[remPos++] = *pos++; } } remaining[remPos] = '\0'; return remaining; } // saveCurProc -- save editor contents back to the current buffer. // If the user typed a new Sub/Function in the General section, // it's automatically extracted into its own procedure buffer. static void saveCurProc(void) { if (!sEditor) { return; } const char *edText = wgtGetText(sEditor); if (!edText) { return; } if (sCurProcIdx == -1) { // General section -- check for embedded proc declarations char *cleaned = extractNewProcs(edText); free(sGeneralBuf); sGeneralBuf = cleaned ? cleaned : strdup(edText); if (cleaned) { // Update editor to show the cleaned General section wgtSetText(sEditor, sGeneralBuf); updateDropdowns(); } } 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 (!sObjDropdown || !sEvtDropdown) { return; } // 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; while (*pos) { const char *lineStart = pos; // Skip leading whitespace while (*pos == ' ' || *pos == '\t') { pos++; } // Check for SUB or FUNCTION keyword bool isSub = (strncasecmp(pos, "SUB ", 4) == 0); bool isFunc = (strncasecmp(pos, "FUNCTION ", 9) == 0); if (isSub || isFunc) { pos += isSub ? 4 : 9; while (*pos == ' ' || *pos == '\t') { pos++; } char procName[64]; int32_t nameLen = 0; while (*pos && *pos != '(' && *pos != ' ' && *pos != '\t' && *pos != '\n' && *pos != '\r' && nameLen < 63) { procName[nameLen++] = *pos++; } procName[nameLen] = '\0'; // 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; char *underscore = strchr(procName, '_'); if (underscore) { int32_t objLen = (int32_t)(underscore - procName); if (objLen > 63) { objLen = 63; } memcpy(entry.objName, procName, objLen); entry.objName[objLen] = '\0'; snprintf(entry.evtName, sizeof(entry.evtName), "%s", underscore + 1); } else { snprintf(entry.objName, sizeof(entry.objName), "%s", "(General)"); snprintf(entry.evtName, sizeof(entry.evtName), "%s", procName); } 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 while (*pos && *pos != '\n') { pos++; } if (*pos == '\n') { pos++; } lineNum++; } // Build object names for the Object dropdown. // Always include "(General)" and the form name (if editing a form). // Then add control names from the designer, plus any from existing procs. arrput(sObjItems, "(General)"); if (sDesigner.form) { arrput(sObjItems, sDesigner.form->name); for (int32_t i = 0; i < (int32_t)arrlen(sDesigner.form->controls); i++) { arrput(sObjItems, sDesigner.form->controls[i].name); } } // Add any objects from existing procs not already in the list int32_t procCount = (int32_t)arrlen(sProcTable); for (int32_t i = 0; i < procCount; i++) { bool found = false; int32_t objCount = (int32_t)arrlen(sObjItems); for (int32_t j = 0; j < objCount; j++) { if (strcasecmp(sObjItems[j], sProcTable[i].objName) == 0) { found = true; break; } } if (!found) { arrput(sObjItems, sProcTable[i].objName); } } int32_t objCount = (int32_t)arrlen(sObjItems); wgtDropdownSetItems(sObjDropdown, sObjItems, objCount); if (objCount > 0) { wgtDropdownSetSelected(sObjDropdown, 0); onObjDropdownChange(sObjDropdown); } else { wgtDropdownSetItems(sEvtDropdown, NULL, 0); } }