// 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 "dvxCur.h" #include "dvxPlat.h" #include "dvxDlg.h" #include "dvxPrefs.h" #include "dvxWgt.h" #include "dvxWgtP.h" #include "dvxWm.h" #include "shellApp.h" #include "box/box.h" #include "checkbox/checkbox.h" #include "imageButton/imgBtn.h" #include "label/label.h" #include "radio/radio.h" #include "textInput/textInpt.h" #include "dropdown/dropdown.h" #include "canvas/canvas.h" #include "listBox/listBox.h" #include "slider/slider.h" #include "tabControl/tabCtrl.h" #include "button/button.h" #include "splitter/splitter.h" #include "statusBar/statBar.h" #include "listView/listView.h" #include "separator/separatr.h" #include "toolbar/toolbar.h" #include "ideDesigner.h" #include "ideProject.h" #include "ideMenuEditor.h" #include "ideToolbox.h" #include "ideProperties.h" #include "../compiler/parser.h" #include "../compiler/strip.h" #include "../runtime/serialize.h" #include "../formrt/formrt.h" #include "../formrt/formcfm.h" #include "dvxRes.h" #include "../../sql/dvxSql.h" #include "../runtime/vm.h" #include "../runtime/values.h" #include "stb_ds_wrap.h" #include #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 CMD_HELP_ABOUT 140 #define CMD_FIND 141 #define CMD_REPLACE 142 #define CMD_FIND_NEXT 143 #define CMD_MENU_EDITOR 144 #define CMD_PREFERENCES 145 #define CMD_STEP_INTO 146 #define CMD_STEP_OVER 147 #define CMD_STEP_OUT 148 #define CMD_TOGGLE_BP 149 #define CMD_RUN_TO_CURSOR 150 #define CMD_WIN_LOCALS 151 #define CMD_DEBUG 152 #define CMD_WIN_CALLSTACK 153 #define CMD_WIN_WATCH 154 #define CMD_WIN_BREAKPOINTS 155 #define CMD_DEBUG_LAYOUT 156 #define CMD_OUTPUT_TO_LOG 159 #define CMD_RECENT_BASE 160 // 160-167 reserved for recent files #define CMD_MAKE_EXE 170 #define CMD_RECENT_MAX 8 #define CMD_HELP_CONTENTS 157 #define CMD_HELP_API 158 #define IDE_MAX_IMM 1024 #define IDE_DESIGN_W 400 #define IDE_DESIGN_H 300 // Syntax color indices (used by basicColorize / classifyWord) #define SYNTAX_DEFAULT 0 #define SYNTAX_KEYWORD 1 #define SYNTAX_STRING 2 #define SYNTAX_COMMENT 3 #define SYNTAX_NUMBER 4 #define SYNTAX_TYPE 6 // View mode for activateFile typedef enum { ViewAutoE, // .frm -> design, .bas -> code ViewCodeE, // force code view ViewDesignE // force design view } IdeViewModeE; // ============================================================ // Prototypes // ============================================================ static void activateFile(int32_t fileIdx, IdeViewModeE view); int32_t appMain(DxeAppContextT *ctx); static void buildWindow(void); static void clearOutput(void); static int cmpStrPtrs(const void *a, const void *b); static bool compileProject(void); static void compileAndRun(void); static void debugStartOrResume(int32_t cmd); static void toggleBreakpoint(void); static void ensureProject(const char *filePath); static void freeProcBufs(void); static const char *getFullSource(void); static void loadFile(void); static void loadFormCodeIntoEditor(void); static void parseProcs(const char *source); static void updateProjectMenuState(void); static void saveActiveFile(void); static bool saveCurProc(void); static void stashCurrentFile(void); static void stashFormCode(void); static void showProc(int32_t procIdx); static int32_t toolbarBottom(void); static void newProject(void); static void onPrjFileDblClick(int32_t fileIdx, bool isForm); static void openProject(void); static void recentAdd(const char *path); static void recentLoad(void); static void recentSave(void); static void recentRebuildMenu(void); static void recentOpen(int32_t index); 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 onFormWinMenu(WindowT *win, int32_t menuId); static void onFormWinResize(WindowT *win, int32_t newW, int32_t newH); static void onProjectWinClose(WindowT *win); static WindowT *getLastFocusWin(void); static void closeFindDialog(void); static bool findInProject(const char *needle, bool caseSensitive); static void openFindDialog(bool showReplace); static void handleEditCmd(int32_t cmd); static void handleFileCmd(int32_t cmd); static void makeExecutable(void); static void handleProjectCmd(int32_t cmd); static void handleRunCmd(int32_t cmd); static void handleViewCmd(int32_t cmd); static void handleWindowCmd(int32_t cmd); static void onMenu(WindowT *win, int32_t menuId); static void basicColorize(const char *line, int32_t lineLen, uint8_t *colors, void *ctx); static uint32_t debugLineDecorator(int32_t lineNum, uint32_t *gutterColor, void *ctx); static void debugSetBreakTitles(bool paused); static void debugUpdateWindows(void); static void onBreakpointHit(void *ctx, int32_t line); static void onGutterClick(WidgetT *w, int32_t lineNum); static void navigateToCodeLine(int32_t fileIdx, int32_t codeLine, const char *procName, bool setDbgLine); static void navigateToNamedEventSub(const char *ctrlName, const char *eventName); static void debugNavigateToLine(int32_t concatLine); static void buildVmBreakpoints(void); static void showBreakpointWindow(void); static void showCallStackWindow(void); static void showLocalsWindow(void); static void showWatchWindow(void); static void updateBreakpointWindow(void); static void toggleBreakpointLine(int32_t line); static void updateCallStackWindow(void); static void updateLocalsWindow(void); static void updateWatchWindow(void); static void evaluateImmediate(const char *expr); static const BasDebugVarT *findDebugVar(const char *name); static void formatValue(const BasValueT *v, char *buf, int32_t bufSize); static bool readDebugVar(const BasDebugVarT *dv, BasValueT *outVal); static BasValueT *getDebugVarSlot(const BasDebugVarT *dv); 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 onEditorChange(WidgetT *w); static void setStatus(const char *text); static void stashDesignerState(void); static void teardownFormWin(void); static void updateDirtyIndicators(void); static void switchToDesign(void); static int32_t onFormWinCursorQuery(WindowT *win, int32_t x, int32_t y); 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 onTbDebug(WidgetT *w); static void onTbStepInto(WidgetT *w); static void onTbStepOver(WidgetT *w); static void onTbStepOut(WidgetT *w); static void onTbRunToCur(WidgetT *w); static void helpQueryHandler(void *ctx); static void selectDropdowns(const char *objName, const char *evtName); static void updateDropdowns(void); // ============================================================ // Keyword-to-topic lookup for context-sensitive help (F1) // ============================================================ typedef struct { const char *keyword; const char *topic; } HelpMapEntryT; static const HelpMapEntryT sHelpMap[] = { // Data types {"Boolean", "lang.datatypes"}, {"Double", "lang.datatypes"}, {"Integer", "lang.datatypes"}, {"Long", "lang.datatypes"}, {"Single", "lang.datatypes"}, {"String", "lang.datatypes"}, // Declarations {"ByRef", "lang.declarations"}, {"ByVal", "lang.declarations"}, {"Const", "lang.declarations"}, {"Declare", "lang.declarations"}, {"Dim", "lang.declarations"}, {"Option", "lang.declarations"}, {"ReDim", "lang.declarations"}, {"Shared", "lang.declarations"}, {"Static", "lang.declarations"}, {"Type", "lang.declarations"}, // Operators {"And", "lang.operators"}, {"Mod", "lang.operators"}, {"Not", "lang.operators"}, {"Or", "lang.operators"}, {"Xor", "lang.operators"}, // Conditionals {"Case", "lang.conditionals"}, {"Else", "lang.conditionals"}, {"ElseIf", "lang.conditionals"}, {"If", "lang.conditionals"}, {"Select", "lang.conditionals"}, {"Then", "lang.conditionals"}, // Loops {"Do", "lang.loops"}, {"For", "lang.loops"}, {"Loop", "lang.loops"}, {"Next", "lang.loops"}, {"Step", "lang.loops"}, {"Until", "lang.loops"}, {"Wend", "lang.loops"}, {"While", "lang.loops"}, // Procedures {"Call", "lang.procedures"}, {"Def", "lang.procedures"}, {"End", "lang.procedures"}, {"Function", "lang.procedures"}, {"Sub", "lang.procedures"}, // Flow control {"Exit", "lang.flow"}, {"GoSub", "lang.flow"}, {"GoTo", "lang.flow"}, {"On", "lang.flow"}, {"Resume", "lang.flow"}, {"Return", "lang.flow"}, // I/O statements {"Data", "lang.io"}, {"Input", "lang.io"}, {"Print", "lang.io"}, {"Read", "lang.io"}, {"Rem", "lang.io"}, {"Write", "lang.io"}, // Misc statements {"DoEvents", "lang.misc"}, {"Load", "lang.misc"}, {"Shell", "lang.misc"}, {"Sleep", "lang.misc"}, {"Unload", "lang.misc"}, // File I/O {"Close", "lang.fileio"}, {"Eof", "lang.func.fileio"}, {"FreeFile", "lang.func.fileio"}, {"Loc", "lang.func.fileio"}, {"Lof", "lang.func.fileio"}, {"Open", "lang.fileio"}, {"Seek", "lang.func.fileio"}, // String functions {"Asc", "lang.func.string"}, {"Chr", "lang.func.string"}, {"Chr$", "lang.func.string"}, {"Environ", "lang.func.string"}, {"Environ$", "lang.func.string"}, {"Format", "lang.func.string"}, {"Format$", "lang.func.string"}, {"InStr", "lang.func.string"}, {"LCase", "lang.func.string"}, {"LCase$", "lang.func.string"}, {"LTrim", "lang.func.string"}, {"LTrim$", "lang.func.string"}, {"Left", "lang.func.string"}, {"Left$", "lang.func.string"}, {"Len", "lang.func.string"}, {"Mid", "lang.func.string"}, {"Mid$", "lang.func.string"}, {"RTrim", "lang.func.string"}, {"RTrim$", "lang.func.string"}, {"Right", "lang.func.string"}, {"Right$", "lang.func.string"}, {"Spc", "lang.func.string"}, {"Space", "lang.func.string"}, {"Space$", "lang.func.string"}, {"String$", "lang.func.string"}, {"Tab", "lang.func.string"}, {"Trim", "lang.func.string"}, {"Trim$", "lang.func.string"}, {"UCase", "lang.func.string"}, {"UCase$", "lang.func.string"}, // Math functions {"Abs", "lang.func.math"}, {"Atn", "lang.func.math"}, {"Cos", "lang.func.math"}, {"Exp", "lang.func.math"}, {"Fix", "lang.func.math"}, {"Int", "lang.func.math"}, {"Log", "lang.func.math"}, {"Randomize", "lang.func.math"}, {"Rnd", "lang.func.math"}, {"Sgn", "lang.func.math"}, {"Sin", "lang.func.math"}, {"Sqr", "lang.func.math"}, {"Tan", "lang.func.math"}, {"Timer", "lang.func.math"}, // Conversion functions {"CBool", "lang.func.conversion"}, {"CDbl", "lang.func.conversion"}, {"CInt", "lang.func.conversion"}, {"CLng", "lang.func.conversion"}, {"CSng", "lang.func.conversion"}, {"CStr", "lang.func.conversion"}, {"Hex", "lang.func.conversion"}, {"Hex$", "lang.func.conversion"}, {"Str", "lang.func.conversion"}, {"Str$", "lang.func.conversion"}, {"Val", "lang.func.conversion"}, // Misc functions {"InputBox", "lang.func.misc"}, {"InputBox$", "lang.func.misc"}, {"MsgBox", "lang.func.misc"}, // SQL functions {"SQLAffected", "lang.sql"}, {"SQLClose", "lang.sql"}, {"SQLEof", "lang.sql"}, {"SQLError", "lang.sql"}, {"SQLError$", "lang.sql"}, {"SQLExec", "lang.sql"}, {"SQLField", "lang.sql"}, {"SQLField$", "lang.sql"}, {"SQLFieldCount", "lang.sql"}, {"SQLFieldDbl", "lang.sql"}, {"SQLFieldInt", "lang.sql"}, {"SQLFreeResult", "lang.sql"}, {"SQLNext", "lang.sql"}, {"SQLOpen", "lang.sql"}, {"SQLQuery", "lang.sql"}, // App object {"App", "lang.app"}, // INI functions {"IniRead", "lang.ini"}, {"IniWrite", "lang.ini"}, // Constants {"False", "lang.constants"}, {"True", "lang.constants"}, {"vbCancel", "lang.constants"}, {"vbCritical", "lang.constants"}, {"vbModal", "lang.constants"}, {"vbOK", "lang.constants"}, {"vbOKCancel", "lang.constants"}, {"vbYesNo", "lang.constants"}, // Form/control statements {"Me", "lang.forms"}, {"Set", "lang.forms"}, }; #define HELP_MAP_COUNT (sizeof(sHelpMap) / sizeof(sHelpMap[0])) // Build control help topic from type name. // Topic IDs in .bhs files follow the pattern ctrl.. // Generated dynamically so third-party widgets get help automatically. static void helpBuildCtrlTopic(const char *typeName, char *buf, int32_t bufSize) { int32_t off = snprintf(buf, bufSize, "ctrl."); for (int32_t i = 0; typeName[i] && off < bufSize - 1; i++) { buf[off++] = tolower((unsigned char)typeName[i]); } buf[off] = '\0'; } // ============================================================ // Module state // ============================================================ static DxeAppContextT *sCtx = NULL; static char sIdeHelpFile[DVX_MAX_PATH]; // IDE help file (restored after program run) 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 WidgetT *sTbRun = NULL; static WidgetT *sTbStop = NULL; static WidgetT *sTbDebug = NULL; static WidgetT *sTbStepInto = NULL; static WidgetT *sTbStepOver = NULL; static WidgetT *sTbStepOut = NULL; static WidgetT *sTbRunToCur = NULL; static WidgetT *sTbCode = NULL; static WidgetT *sTbDesign = 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 sOutputBuf[IDE_MAX_OUTPUT]; static int32_t sOutputLen = 0; // 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) static int32_t sEditorFileIdx = -1; // which project file owns sProcBufs (-1=none) static int32_t sEditorLineCount = 0; // line count for breakpoint adjustment on edit // Find/Replace state static char sFindText[256] = ""; static char sReplaceText[256] = ""; // Find/Replace dialog state (modeless) static WindowT *sFindWin = NULL; static WidgetT *sFindInput = NULL; static WidgetT *sReplInput = NULL; static WidgetT *sReplCheck = NULL; static WidgetT *sBtnReplace = NULL; static WidgetT *sBtnReplAll = NULL; static WidgetT *sCaseCheck = NULL; static WidgetT *sScopeGroup = NULL; // radio group: 0=Func, 1=Obj, 2=File, 3=Proj static WidgetT *sDirGroup = NULL; // radio group: 0=Fwd, 1=Back // 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 static bool sDropdownNavSuppressed = false; static bool sStopRequested = false; static bool sOutputToLog = false; // Recent files list (stored in dvxbasic.ini [recent] section) static char sRecentFiles[CMD_RECENT_MAX][DVX_MAX_PATH]; static int32_t sRecentCount = 0; static MenuT *sFileMenu = NULL; static int32_t sFileMenuBase = 0; // item count before recent files // Debug state typedef enum { DBG_IDLE, // no program loaded DBG_RUNNING, // program executing DBG_PAUSED // stopped at breakpoint/step } IdeDebugStateE; typedef struct { int32_t fileIdx; // project file index int32_t codeLine; // line within file's code section (1-based) int32_t procIdx; // procedure index at time of toggle (-1 = general) char procName[BAS_MAX_PROC_NAME * 2]; // "obj.evt" combined name } IdeBreakpointT; static IdeDebugStateE sDbgState = DBG_IDLE; static IdeBreakpointT *sBreakpoints = NULL; // stb_ds array static int32_t sBreakpointCount = 0; static int32_t *sVmBreakpoints = NULL; // stb_ds array of concat line numbers (built at compile time) static int32_t sDbgCurrentLine = -1; // line where paused (-1 = none) static BasFormRtT *sDbgFormRt = NULL; // form runtime for debug session static BasModuleT *sDbgModule = NULL; // module for debug session static bool sDbgBreakOnStart = false; // break at first statement static bool sDbgEnabled = false; // true = debug mode (breakpoints active) static WindowT *sLocalsWin = NULL; // Locals window static WidgetT *sLocalsList = NULL; // Locals ListView widget static WindowT *sCallStackWin = NULL; // Call stack window static WidgetT *sCallStackList = NULL; // Call stack ListView widget static WindowT *sWatchWin = NULL; // Watch window static WidgetT *sWatchList = NULL; // Watch ListView widget static WidgetT *sWatchInput = NULL; // Watch expression input static char *sWatchExprs[16]; // watch expressions (strdup'd) static WindowT *sBreakpointWin = NULL; // Breakpoints window static WidgetT *sBreakpointList = NULL; // Breakpoints ListView widget static int32_t sWatchExprCount = 0; // ============================================================ // App descriptor // ============================================================ AppDescriptorT appDescriptor = { .name = "DVX BASIC", .hasMainLoop = false, .multiInstance = false, .stackSize = SHELL_STACK_DEFAULT, .priority = 0 }; // ============================================================ // activateFile -- central file-switching function // ============================================================ static void activateFile(int32_t fileIdx, IdeViewModeE view) { if (fileIdx < 0 || fileIdx >= sProject.fileCount) { return; } PrjFileT *target = &sProject.files[fileIdx]; // Resolve ViewAutoE if (view == ViewAutoE) { view = target->isForm ? ViewDesignE : ViewCodeE; } // If already active, just ensure the right view is showing if (fileIdx == sProject.activeFileIdx) { if (view == ViewDesignE) { switchToDesign(); } else if (view == ViewCodeE) { if (sCodeWin) { dvxRaiseWindow(sAc, sCodeWin); } else { // Code window was closed — reopen it showCodeWindow(); if (sCodeWin) { dvxRaiseWindow(sAc, sCodeWin); } // Reload the file content if (sEditor) { const char *source = target->buffer; if (source) { parseProcs(source); updateDropdowns(); showProc(-1); sEditor->onChange = onEditorChange; } } } } return; } // PHASE 1: Stash the outgoing file stashCurrentFile(); // PHASE 2: Load the incoming file if (target->isForm) { // Load form into designer const char *frmSrc = target->buffer; char *diskBuf = NULL; if (!frmSrc) { // Try disk char fullPath[DVX_MAX_PATH]; prjFullPath(&sProject, fileIdx, fullPath, sizeof(fullPath)); FILE *f = fopen(fullPath, "r"); if (f) { fseek(f, 0, SEEK_END); long size = ftell(f); fseek(f, 0, SEEK_SET); if (size > 0 && size < IDE_MAX_SOURCE) { diskBuf = (char *)malloc(size + 1); if (diskBuf) { int32_t br = (int32_t)fread(diskBuf, 1, size, f); diskBuf[br] = '\0'; frmSrc = diskBuf; } } fclose(f); } } teardownFormWin(); dsgnFree(&sDesigner); if (frmSrc) { dsgnLoadFrm(&sDesigner, frmSrc, (int32_t)strlen(frmSrc)); free(diskBuf); } else { // New blank form -- derive name from filename char formName[PRJ_MAX_NAME]; const char *base = strrchr(target->path, '/'); const char *base2 = strrchr(target->path, '\\'); if (base2 > base) { base = base2; } base = base ? base + 1 : target->path; int32_t bl = (int32_t)strlen(base); if (bl >= PRJ_MAX_NAME) { bl = PRJ_MAX_NAME - 1; } memcpy(formName, base, bl); formName[bl] = '\0'; char *dot = strrchr(formName, '.'); if (dot) { *dot = '\0'; } dsgnNewForm(&sDesigner, formName); target->modified = true; } if (sDesigner.form) { snprintf(target->formName, sizeof(target->formName), "%s", sDesigner.form->name); } sProject.activeFileIdx = fileIdx; if (view == ViewDesignE) { switchToDesign(); } else { loadFormCodeIntoEditor(); } } else { // Load .bas into editor if (!sCodeWin) { showCodeWindow(); } if (sCodeWin) { dvxRaiseWindow(sAc, sCodeWin); } const char *source = target->buffer; char *diskBuf = NULL; if (!source) { char fullPath[DVX_MAX_PATH]; prjFullPath(&sProject, fileIdx, fullPath, sizeof(fullPath)); FILE *f = fopen(fullPath, "r"); if (f) { fseek(f, 0, SEEK_END); long size = ftell(f); fseek(f, 0, SEEK_SET); if (size > 0 && size < IDE_MAX_SOURCE) { diskBuf = (char *)malloc(size + 1); if (diskBuf) { int32_t br = (int32_t)fread(diskBuf, 1, size, f); diskBuf[br] = '\0'; source = diskBuf; } } fclose(f); } } if (!source) { source = ""; target->modified = true; } parseProcs(source); free(diskBuf); updateDropdowns(); showProc(-1); if (sEditor && !sEditor->onChange) { sEditor->onChange = onEditorChange; } sEditorFileIdx = fileIdx; sProject.activeFileIdx = fileIdx; } updateProjectMenuState(); updateDirtyIndicators(); } // ============================================================ // appMain // ============================================================ int32_t appMain(DxeAppContextT *ctx) { sCtx = ctx; sAc = ctx->shellCtx; // Set help file and context-sensitive F1 handler snprintf(ctx->helpFile, sizeof(ctx->helpFile), "%s%c%s", ctx->appDir, DVX_PATH_SEP, "dvxbasic.hlp"); snprintf(sIdeHelpFile, sizeof(sIdeHelpFile), "%s", ctx->helpFile); ctx->onHelpQuery = helpQueryHandler; ctx->helpQueryCtx = NULL; basStringSystemInit(); prjInit(&sProject); buildWindow(); // Load persisted settings shellEnsureConfigDir(sCtx); char prefsPath[DVX_MAX_PATH]; shellConfigPath(sCtx, "dvxbasic.ini", prefsPath, sizeof(prefsPath)); sPrefs = prefsLoad(prefsPath); if (!sPrefs) { sPrefs = prefsCreate(); prefsSaveAs(sPrefs, 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); } // Load recent files list and populate menu recentLoad(); recentRebuildMenu(); if (sWin) { dvxFitWindowH(sAc, sWin); } sOutputBuf[0] = '\0'; sOutputLen = 0; updateProjectMenuState(); 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, true); if (!sWin) { return; } sWin->onClose = onClose; sWin->onMenu = onMenu; // Menu bar MenuBarT *menuBar = wmAddMenuBar(sWin); sFileMenu = wmAddMenu(menuBar, "&File"); MenuT *fileMenu = sFileMenu; wmAddMenuItem(fileMenu, "New &Project...", CMD_PRJ_NEW); wmAddMenuItem(fileMenu, "&Open Project...", CMD_PRJ_OPEN); wmAddMenuItem(fileMenu, "Save Projec&t", CMD_PRJ_SAVE); wmAddMenuItem(fileMenu, "&Close Project", CMD_PRJ_CLOSE); wmAddMenuSeparator(fileMenu); wmAddMenuItem(fileMenu, "Project Propert&ies...", CMD_PRJ_PROPS); wmAddMenuSeparator(fileMenu); wmAddMenuItem(fileMenu, "&Add File...\tCtrl+O", CMD_OPEN); wmAddMenuItem(fileMenu, "&Save File\tCtrl+S", CMD_SAVE); wmAddMenuItem(fileMenu, "Save A&ll", CMD_SAVE_ALL); wmAddMenuSeparator(fileMenu); wmAddMenuItem(fileMenu, "&Make Executable...", CMD_MAKE_EXE); wmAddMenuSeparator(fileMenu); wmAddMenuItem(fileMenu, "&Remove File", CMD_PRJ_REMOVE); wmAddMenuSeparator(fileMenu); wmAddMenuItem(fileMenu, "E&xit", CMD_EXIT); sFileMenuBase = sFileMenu->itemCount; 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); wmAddMenuSeparator(editMenu); wmAddMenuItem(editMenu, "&Find...\tCtrl+F", CMD_FIND); wmAddMenuItem(editMenu, "Find &Next\tF3", CMD_FIND_NEXT); wmAddMenuItem(editMenu, "&Replace...\tCtrl+H", CMD_REPLACE); MenuT *runMenu = wmAddMenu(menuBar, "&Run"); wmAddMenuItem(runMenu, "&Run\tF5", CMD_RUN); wmAddMenuItem(runMenu, "&Debug\tShift+F5", CMD_DEBUG); wmAddMenuItem(runMenu, "Run &Without Recompile\tCtrl+F5", CMD_RUN_NOCMP); wmAddMenuItem(runMenu, "&Stop\tEsc", CMD_STOP); wmAddMenuSeparator(runMenu); wmAddMenuItem(runMenu, "Step &Into\tF8", CMD_STEP_INTO); wmAddMenuItem(runMenu, "Step &Over\tShift+F8", CMD_STEP_OVER); wmAddMenuItem(runMenu, "Step Ou&t\tCtrl+Shift+F8", CMD_STEP_OUT); wmAddMenuItem(runMenu, "Run to &Cursor\tCtrl+F8", CMD_RUN_TO_CURSOR); wmAddMenuSeparator(runMenu); wmAddMenuCheckItem(runMenu, "Output Window to &Log", CMD_OUTPUT_TO_LOG, false); wmAddMenuSeparator(runMenu); wmAddMenuItem(runMenu, "Toggle &Breakpoint\tF9", CMD_TOGGLE_BP); wmAddMenuSeparator(runMenu); wmAddMenuItem(runMenu, "Cl&ear Output", CMD_CLEAR); wmAddMenuSeparator(runMenu); wmAddMenuCheckItem(runMenu, "S&ave on Run", CMD_SAVE_ON_RUN, true); MenuT *viewMenu = wmAddMenu(menuBar, "&View"); wmAddMenuItem(viewMenu, "&Code\tF7", CMD_VIEW_CODE); wmAddMenuItem(viewMenu, "&Designer\tShift+F7", CMD_VIEW_DESIGN); wmAddMenuSeparator(viewMenu); wmAddMenuCheckItem(viewMenu, "&Toolbar", CMD_VIEW_TOOLBAR, true); wmAddMenuCheckItem(viewMenu, "&Status Bar", CMD_VIEW_STATUS, true); wmAddMenuSeparator(viewMenu); wmAddMenuItem(viewMenu, "&Menu Editor...\tCtrl+E", CMD_MENU_EDITOR); MenuT *winMenu = wmAddMenu(menuBar, "&Window"); wmAddMenuItem(winMenu, "&Code Editor", CMD_WIN_CODE); wmAddMenuItem(winMenu, "&Output", CMD_WIN_OUTPUT); wmAddMenuItem(winMenu, "&Immediate", CMD_WIN_IMM); wmAddMenuItem(winMenu, "&Locals", CMD_WIN_LOCALS); wmAddMenuItem(winMenu, "Call &Stack", CMD_WIN_CALLSTACK); wmAddMenuItem(winMenu, "&Watch", CMD_WIN_WATCH); wmAddMenuItem(winMenu, "&Breakpoints", CMD_WIN_BREAKPOINTS); wmAddMenuSeparator(winMenu); wmAddMenuItem(winMenu, "&Project Explorer", CMD_WIN_PROJECT); wmAddMenuItem(winMenu, "&Toolbox", CMD_WIN_TOOLBOX); wmAddMenuItem(winMenu, "P&roperties", CMD_WIN_PROPS); MenuT *toolsMenu = wmAddMenu(menuBar, "&Tools"); wmAddMenuItem(toolsMenu, "&Preferences...", CMD_PREFERENCES); wmAddMenuSeparator(toolsMenu); wmAddMenuCheckItem(toolsMenu, "Debug &Layout", CMD_DEBUG_LAYOUT, false); MenuT *helpMenu = wmAddMenu(menuBar, "&Help"); wmAddMenuItem(helpMenu, "&DVX BASIC Help\tF1", CMD_HELP_CONTENTS); wmAddMenuItem(helpMenu, "DVX &API Reference", CMD_HELP_API); wmAddMenuSeparator(helpMenu); wmAddMenuItem(helpMenu, "A&bout DVX BASIC...", CMD_HELP_ABOUT); 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_SHIFT, CMD_DEBUG); 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); dvxAddAccel(accel, 'F', ACCEL_CTRL, CMD_FIND); dvxAddAccel(accel, 'H', ACCEL_CTRL, CMD_REPLACE); dvxAddAccel(accel, KEY_F3, 0, CMD_FIND_NEXT); dvxAddAccel(accel, 'E', ACCEL_CTRL, CMD_MENU_EDITOR); dvxAddAccel(accel, KEY_F8, 0, CMD_STEP_INTO); dvxAddAccel(accel, KEY_F8, ACCEL_SHIFT, CMD_STEP_OVER); dvxAddAccel(accel, KEY_F8, ACCEL_CTRL | ACCEL_SHIFT, CMD_STEP_OUT); dvxAddAccel(accel, KEY_F8, ACCEL_CTRL, CMD_RUN_TO_CURSOR); dvxAddAccel(accel, KEY_F9, 0, CMD_TOGGLE_BP); sWin->accelTable = accel; WidgetT *tbRoot = wgtInitWindow(sAc, sWin); sToolbar = wgtToolbar(tbRoot); WidgetT *tb = sToolbar; // File group 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 *sep1 = wgtVSeparator(tb); sep1->minW = wgtPixels(10); // Run group sTbRun = loadTbIcon(tb, "tb_run", "Run"); sTbRun->onClick = onTbRun; wgtSetTooltip(sTbRun, "Run (F5)"); sTbStop = loadTbIcon(tb, "tb_stop", "Stop"); sTbStop->onClick = onTbStop; wgtSetTooltip(sTbStop, "Stop (Esc)"); WidgetT *sep2 = wgtVSeparator(tb); sep2->minW = wgtPixels(10); // Debug group sTbDebug = loadTbIcon(tb, "tb_debug", "Debug"); sTbDebug->onClick = onTbDebug; wgtSetTooltip(sTbDebug, "Debug (Shift+F5)"); sTbStepInto = loadTbIcon(tb, "tb_stepin", "Into"); sTbStepInto->onClick = onTbStepInto; wgtSetTooltip(sTbStepInto, "Step Into (F8)"); sTbStepOver = loadTbIcon(tb, "tb_stepov", "Over"); sTbStepOver->onClick = onTbStepOver; wgtSetTooltip(sTbStepOver, "Step Over (Shift+F8)"); sTbStepOut = loadTbIcon(tb, "tb_stepou", "Out"); sTbStepOut->onClick = onTbStepOut; wgtSetTooltip(sTbStepOut, "Step Out (Ctrl+Shift+F8)"); sTbRunToCur = loadTbIcon(tb, "tb_runtoc", "Cursor"); sTbRunToCur->onClick = onTbRunToCur; wgtSetTooltip(sTbRunToCur, "Run to Cursor (Ctrl+F8)"); WidgetT *sep3 = wgtVSeparator(tb); sep3->minW = wgtPixels(10); // View group sTbCode = loadTbIcon(tb, "tb_code", "Code"); sTbCode->onClick = onTbCode; wgtSetTooltip(sTbCode, "Code View (F7)"); sTbDesign = loadTbIcon(tb, "tb_design", "Design"); sTbDesign->onClick = onTbDesign; wgtSetTooltip(sTbDesign, "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); sDesigner.projectDir = sProject.projectDir; showOutputWindow(); showImmediateWindow(); if (sWin) { dvxRaiseWindow(sAc, sWin); } } // ============================================================ // basicColorize // ============================================================ // // Syntax colorizer callback for BASIC source code. Scans a single // line and fills the colors array with syntax color indices. // Hash-based keyword/type lookup using stb_ds. // Key = uppercase word, value = SYNTAX_KEYWORD or SYNTAX_TYPE. // Built once on first use, then O(1) per lookup. typedef struct { char *key; uint8_t value; } SyntaxMapEntryT; static SyntaxMapEntryT *sSyntaxMap = NULL; static void initSyntaxMap(void) { if (sSyntaxMap) { return; } sh_new_arena(sSyntaxMap); static const char *keywords[] = { "AND", "AS", "BYVAL", "CALL", "CASE", "CLOSE", "CONST", "DATA", "DECLARE", "DEF", "DEFDBL", "DEFINT", "DEFLNG", "DEFSNG", "DEFSTR", "DIM", "DO", "DOEVENTS", "ELSE", "ELSEIF", "END", "ERASE", "EXIT", "FOR", "FUNCTION", "GET", "GOSUB", "GOTO", "HIDE", "IF", "IMP", "INPUT", "IS", "LET", "LIBRARY", "LINE", "LOAD", "LOOP", "ME", "MOD", "MSGBOX", "NEXT", "NOT", "ON", "OPEN", "OPTION", "OR", "PRINT", "PUT", "RANDOMIZE", "READ", "REDIM", "RESTORE", "RESUME", "RETURN", "SEEK", "SELECT", "SHARED", "SHELL", "SHOW", "SLEEP", "STATIC", "STEP", "STOP", "SUB", "SWAP", "THEN", "TO", "TYPE", "UNLOAD", "UNTIL", "WEND", "WHILE", "WRITE", "XOR", NULL }; static const char *types[] = { "BOOLEAN", "BYTE", "DOUBLE", "FALSE", "INTEGER", "LONG", "SINGLE", "STRING", "TRUE", NULL }; for (int32_t i = 0; keywords[i]; i++) { shput(sSyntaxMap, keywords[i], SYNTAX_KEYWORD); } for (int32_t i = 0; types[i]; i++) { shput(sSyntaxMap, types[i], SYNTAX_TYPE); } } // classifyWord -- returns syntax color for an identifier. // Converts to uppercase once, then does a single hash lookup. static uint8_t classifyWord(const char *word, int32_t wordLen) { char upper[32]; if (wordLen <= 0 || wordLen >= 32) { return SYNTAX_DEFAULT; } for (int32_t i = 0; i < wordLen; i++) { upper[i] = (char)toupper((unsigned char)word[i]); } upper[wordLen] = '\0'; initSyntaxMap(); int32_t idx = shgeti(sSyntaxMap, upper); if (idx >= 0) { return sSyntaxMap[idx].value; } return SYNTAX_DEFAULT; } 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++] = SYNTAX_COMMENT; } return; } // String literal if (ch == '"') { colors[i++] = SYNTAX_STRING; while (i < lineLen && line[i] != '"') { colors[i++] = SYNTAX_STRING; } if (i < lineLen) { colors[i++] = SYNTAX_STRING; } 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++] = 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] = SYNTAX_COMMENT; } return; } uint8_t c = classifyWord(line + start, wordLen); 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 (!sOutWin) { showOutputWindow(); } if (sOutput) { wgtSetText(sOutput, text); } if (sOutputToLog && text && text[0]) { dvxLog("BASIC: %s", text); } } static void clearOutput(void) { sOutputBuf[0] = '\0'; sOutputLen = 0; setOutputText(""); } // ============================================================ // localToConcatLine -- convert editor-local line to concatenated source line // ============================================================ // Convert an editor-local line number to a full-source line number. // The editor shows one procedure at a time (sCurProcIdx), so we need // to add the procedure's starting line offset within the file's code. // For multi-file projects, we also add the file's offset in the // concatenated source. For .frm files, an injected BEGINFORM directive // adds one extra line before the code. static int32_t localToConcatLine(int32_t editorLine) { int32_t fileLine = editorLine; // Add the current procedure's start offset within the file's code. // sCurProcIdx == -1 means (General) section which starts at line 1 // of the file's code (not the concatenated source). if (sCurProcIdx >= 0 && sCurProcIdx < (int32_t)arrlen(sProcTable)) { fileLine = sProcTable[sCurProcIdx].lineNum + editorLine - 1; } // For projects, add the file's offset in the concatenated source. // For .frm files, startLine points to the injected BEGINFORM line, // so the actual code starts at startLine + 1. if (sProject.sourceMapCount > 0 && sProject.activeFileIdx >= 0) { for (int32_t i = 0; i < sProject.sourceMapCount; i++) { if (sProject.sourceMap[i].fileIdx == sProject.activeFileIdx) { int32_t base = sProject.sourceMap[i].startLine; // .frm files have an injected BEGINFORM line before the code if (sProject.activeFileIdx < sProject.fileCount && sProject.files[sProject.activeFileIdx].isForm) { base++; } return base + fileLine - 1; } } } return fileLine; } // ============================================================ // debugLineDecorator -- highlight breakpoints and current debug line // ============================================================ // ============================================================ // toggleBreakpointLine -- toggle breakpoint on a specific line // ============================================================ static void clearAllBreakpoints(void) { arrsetlen(sBreakpoints, 0); sBreakpointCount = 0; arrfree(sVmBreakpoints); sVmBreakpoints = NULL; updateBreakpointWindow(); } static void removeBreakpointsForFile(int32_t fileIdx) { // Remove breakpoints for the given file and adjust indices for // files above the removed one (since file indices shift down). for (int32_t i = sBreakpointCount - 1; i >= 0; i--) { if (sBreakpoints[i].fileIdx == fileIdx) { arrdel(sBreakpoints, i); } else if (sBreakpoints[i].fileIdx > fileIdx) { sBreakpoints[i].fileIdx--; } } sBreakpointCount = (int32_t)arrlen(sBreakpoints); updateBreakpointWindow(); } static void toggleBreakpointLine(int32_t editorLine) { int32_t fileIdx = sProject.activeFileIdx; // Convert editor line to file code line by adding proc offset int32_t codeLine = editorLine; if (sCurProcIdx >= 0 && sCurProcIdx < (int32_t)arrlen(sProcTable)) { codeLine = sProcTable[sCurProcIdx].lineNum + editorLine - 1; } // Check if this breakpoint already exists — remove it for (int32_t i = 0; i < sBreakpointCount; i++) { if (sBreakpoints[i].fileIdx == fileIdx && sBreakpoints[i].codeLine == codeLine) { arrdel(sBreakpoints, i); sBreakpointCount = (int32_t)arrlen(sBreakpoints); if (sEditor) { wgtInvalidatePaint(sEditor); } updateBreakpointWindow(); return; } } // Validate that this line is breakable (not blank, comment, or SUB/FUNCTION decl/end) if (sEditor) { const char *text = wgtGetText(sEditor); if (text) { // Find the start of editorLine (1-based) const char *p = text; int32_t ln = 1; while (*p && ln < editorLine) { if (*p == '\n') { ln++; } p++; } // Skip leading whitespace while (*p == ' ' || *p == '\t') { p++; } // Blank line if (*p == '\0' || *p == '\n' || *p == '\r') { return; } // Comment (single quote or REM) if (*p == '\'') { return; } if (strncasecmp(p, "REM ", 4) == 0 || strncasecmp(p, "REM\n", 4) == 0 || strncasecmp(p, "REM\r", 4) == 0 || strcasecmp(p, "REM") == 0) { return; } // SUB/FUNCTION declaration or END SUB/FUNCTION if (strncasecmp(p, "SUB ", 4) == 0 || strncasecmp(p, "FUNCTION ", 9) == 0) { return; } if (strncasecmp(p, "END SUB", 7) == 0 || strncasecmp(p, "END FUNCTION", 12) == 0) { return; } } } // Add new breakpoint IdeBreakpointT bp; memset(&bp, 0, sizeof(bp)); bp.fileIdx = fileIdx; bp.codeLine = codeLine; bp.procIdx = sCurProcIdx; if (sCurProcIdx >= 0 && sCurProcIdx < (int32_t)arrlen(sProcTable)) { snprintf(bp.procName, sizeof(bp.procName), "%s.%s", sProcTable[sCurProcIdx].objName, sProcTable[sCurProcIdx].evtName); } else { snprintf(bp.procName, sizeof(bp.procName), "(General)"); } arrput(sBreakpoints, bp); sBreakpointCount = (int32_t)arrlen(sBreakpoints); if (sEditor) { wgtInvalidatePaint(sEditor); } updateBreakpointWindow(); } // ============================================================ // onGutterClick -- handle gutter click from TextArea // ============================================================ static void onGutterClick(WidgetT *w, int32_t lineNum) { (void)w; toggleBreakpointLine(lineNum); } static uint32_t debugLineDecorator(int32_t lineNum, uint32_t *gutterColor, void *ctx) { AppContextT *ac = (AppContextT *)ctx; // Convert editor line to file code line int32_t codeLine = lineNum; if (sCurProcIdx >= 0 && sCurProcIdx < (int32_t)arrlen(sProcTable)) { codeLine = sProcTable[sCurProcIdx].lineNum + lineNum - 1; } int32_t fileIdx = sProject.activeFileIdx; // Breakpoint: red gutter dot for (int32_t i = 0; i < sBreakpointCount; i++) { if (sBreakpoints[i].fileIdx == fileIdx && sBreakpoints[i].codeLine == codeLine) { *gutterColor = packColor(&ac->display, 200, 0, 0); break; } } // Current debug line: yellow background (sDbgCurrentLine is editor-local) if (sDbgState == DBG_PAUSED && lineNum == sDbgCurrentLine) { return packColor(&ac->display, 255, 255, 128); } return 0; } // ============================================================ // navigateToCodeLine -- show a specific file/line/proc in the code editor // ============================================================ // // fileIdx: project file index (-1 = current) // codeLine: 1-based line within the file's code section // procName: "Obj.Evt" (dot-separated) to match editor proc table, or NULL for General // setDbgLine: if true, update sDbgCurrentLine for the debug decorator static void navigateToCodeLine(int32_t fileIdx, int32_t codeLine, const char *procName, bool setDbgLine) { // Track whether the file changed so we can force showProc bool fileChanged = (fileIdx >= 0 && fileIdx != sProject.activeFileIdx); // Switch to the correct file if needed if (fileChanged) { activateFile(fileIdx, ViewCodeE); } // Ensure code window exists if (!sCodeWin) { showCodeWindow(); updateDropdowns(); sCurProcIdx = -2; } if (sCodeWin && !sCodeWin->visible) { dvxShowWindow(sAc, sCodeWin); } if (sCodeWin) { dvxRaiseWindow(sAc, sCodeWin); } // Find the target procedure in the editor's proc table int32_t targetProcIdx = -1; int32_t procCount = (int32_t)arrlen(sProcTable); if (procName) { 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, procName) == 0) { targetProcIdx = i; break; } } } // Switch to the target procedure (always force after a file change) if (targetProcIdx != sCurProcIdx || fileChanged) { showProc(targetProcIdx); } // Update dropdowns to match if (targetProcIdx >= 0 && targetProcIdx < procCount) { selectDropdowns(sProcTable[targetProcIdx].objName, sProcTable[targetProcIdx].evtName); } else if (sObjDropdown) { wgtDropdownSetSelected(sObjDropdown, 0); } // Compute editor-local line within the current proc int32_t editorLine = codeLine; if (sCurProcIdx >= 0 && sCurProcIdx < procCount) { editorLine = codeLine - sProcTable[sCurProcIdx].lineNum + 1; } if (setDbgLine) { sDbgCurrentLine = editorLine; } if (sEditor) { wgtTextAreaGoToLine(sEditor, editorLine); wgtInvalidatePaint(sEditor); } } // ============================================================ // debugNavigateToLine -- map concatenated source line to file and navigate // ============================================================ static void debugNavigateToLine(int32_t concatLine) { if (concatLine <= 0) { return; } int32_t fileIdx = -1; int32_t localLine = concatLine; // Map concatenated line to file and file-local line (code section) if (sProject.sourceMapCount > 0) { prjMapLine(&sProject, concatLine, &fileIdx, &localLine); // For .frm files, subtract the injected BEGINFORM line if (fileIdx >= 0 && fileIdx < sProject.fileCount && sProject.files[fileIdx].isForm) { localLine--; } } // Find which procedure we're in using the VM's PC and the compiled // module's proc table, then build a dot-separated name for navigateToCodeLine. const char *procName = NULL; char procBuf[128]; if (sVm && sDbgModule) { const char *compiledName = NULL; int32_t bestAddr = -1; for (int32_t i = 0; i < sDbgModule->procCount; i++) { int32_t addr = sDbgModule->procs[i].codeAddr; if (addr <= sVm->pc && addr > bestAddr) { bestAddr = addr; compiledName = sDbgModule->procs[i].name; } } // Convert compiled name (Obj_Evt) to dot-separated (Obj.Evt) for matching if (compiledName) { int32_t procCount = (int32_t)arrlen(sProcTable); for (int32_t i = 0; i < procCount; i++) { char fullName[128]; if (sProcTable[i].objName[0] && strcmp(sProcTable[i].objName, "(General)") != 0) { snprintf(fullName, sizeof(fullName), "%s_%s", sProcTable[i].objName, sProcTable[i].evtName); } else { snprintf(fullName, sizeof(fullName), "%s", sProcTable[i].evtName); } if (strcasecmp(fullName, compiledName) == 0) { snprintf(procBuf, sizeof(procBuf), "%s.%s", sProcTable[i].objName, sProcTable[i].evtName); procName = procBuf; break; } } } } navigateToCodeLine(fileIdx, localLine, procName, true); } // ============================================================ // buildVmBreakpoints -- convert IDE breakpoints to VM concat line numbers // ============================================================ // // Called after compilation when the source map is fresh. Converts // each (fileIdx, codeLine) pair to a concatenated source line number // that matches OP_LINE values in the compiled bytecode. static void buildVmBreakpoints(void) { arrfree(sVmBreakpoints); sVmBreakpoints = NULL; for (int32_t i = 0; i < sBreakpointCount; i++) { int32_t fileIdx = sBreakpoints[i].fileIdx; int32_t codeLine = sBreakpoints[i].codeLine; // Find this file in the source map for (int32_t m = 0; m < sProject.sourceMapCount; m++) { if (sProject.sourceMap[m].fileIdx == fileIdx) { int32_t base = sProject.sourceMap[m].startLine; // .frm files have an injected BEGINFORM line if (fileIdx >= 0 && fileIdx < sProject.fileCount && sProject.files[fileIdx].isForm) { base++; } int32_t vmLine = base + codeLine - 1; arrput(sVmBreakpoints, vmLine); break; } } } } static int cmpStrPtrs(const void *a, const void *b) { const char *sa = *(const char **)a; const char *sb = *(const char **)b; return strcasecmp(sa, sb); } // Sort event names: implemented (no brackets) first, then unimplemented // ([brackets]), alphabetically within each group. static int cmpEvtPtrs(const void *a, const void *b) { const char *sa = *(const char **)a; const char *sb = *(const char **)b; bool aImpl = (sa[0] != '['); bool bImpl = (sb[0] != '['); if (aImpl != bImpl) { return aImpl ? -1 : 1; } // Skip brackets for alphabetical comparison if (sa[0] == '[') { sa++; } if (sb[0] == '[') { sb++; } return strcasecmp(sa, sb); } // ============================================================ // compileProject -- compile without running, returns true on success // ============================================================ static bool compileProject(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; } } } updateDirtyIndicators(); } clearOutput(); setStatus("Compiling..."); dvxSetBusy(sAc, true); dvxUpdate(sAc); // 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/designer state into project buffers stashCurrentFile(); stashFormCode(); // also stash form code if editor has it // Concatenate all .bas files from buffers (or disk if not yet loaded) concatBuf = (char *)malloc(IDE_MAX_SOURCE); if (!concatBuf) { setStatus("Out of memory."); dvxSetBusy(sAc, false); return false; } int32_t pos = 0; int32_t line = 1; arrfree(sProject.sourceMap); sProject.sourceMap = NULL; sProject.sourceMapCount = 0; // Two passes: .bas modules first (so CONST declarations are // available), then .frm code sections. for (int32_t pass = 0; pass < 2; pass++) for (int32_t i = 0; i < sProject.fileCount; i++) { // Pass 0: modules only. Pass 1: forms only. if (pass == 0 && sProject.files[i].isForm) { continue; } if (pass == 1 && !sProject.files[i].isForm) { continue; } const char *fileSrc = NULL; char *diskBuf = NULL; 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) { // Parse the .frm to extract the code section using // the same parser as the designer (handles nested containers). DsgnStateT tmpDs; memset(&tmpDs, 0, sizeof(tmpDs)); dsgnLoadFrm(&tmpDs, sProject.files[i].buffer, (int32_t)strlen(sProject.files[i].buffer)); if (tmpDs.form && tmpDs.form->code) { fileSrc = tmpDs.form->code; diskBuf = tmpDs.form->code; tmpDs.form->code = NULL; } dsgnFree(&tmpDs); } // 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, parse to extract code section if (sProject.files[i].isForm) { DsgnStateT tmpDs; memset(&tmpDs, 0, sizeof(tmpDs)); dsgnLoadFrm(&tmpDs, diskBuf, br); if (tmpDs.form && tmpDs.form->code) { free(diskBuf); diskBuf = tmpDs.form->code; fileSrc = diskBuf; tmpDs.form->code = NULL; } else { fileSrc = NULL; } dsgnFree(&tmpDs); } } } fclose(f); } if (!fileSrc) { continue; } // Inject BEGINFORM directive for .frm code sections if (sProject.files[i].isForm && sProject.files[i].formName[0]) { int32_t dirLen = snprintf(concatBuf + pos, IDE_MAX_SOURCE - pos, "BEGINFORM \"%s\"\n", sProject.files[i].formName); pos += dirLen; line++; } // Record startLine AFTER injected directives so the source // map lines match what the editor shows (not the synthetic lines). int32_t startLine = line; int32_t fileLen = (int32_t)strlen(fileSrc); int32_t copyLen = fileLen; if (pos + copyLen >= IDE_MAX_SOURCE - 64) { copyLen = IDE_MAX_SOURCE - 64 - 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++; } // Record source map BEFORE injected ENDFORM directive { PrjSourceMapT mapEntry; mapEntry.fileIdx = i; mapEntry.startLine = startLine; mapEntry.lineCount = line - startLine; arrput(sProject.sourceMap, mapEntry); sProject.sourceMapCount = (int32_t)arrlen(sProject.sourceMap); } // Inject ENDFORM directive if (sProject.files[i].isForm && sProject.files[i].formName[0]) { int32_t dirLen = snprintf(concatBuf + pos, IDE_MAX_SOURCE - pos, "ENDFORM\n"); pos += dirLen; line++; } dvxUpdate(sAc); } 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 compile."); dvxSetBusy(sAc, false); return false; } 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."); dvxSetBusy(sAc, false); return false; } basParserInit(parser, src, srcLen); parser->optionExplicit = prefsGetBool(sPrefs, "editor", "optionExplicit", false); if (!basParse(parser)) { // Translate global error line to local file/line for display int32_t errFileIdx = -1; int32_t errLocalLine = parser->errorLine; const char *errFile = ""; if (parser->errorLine > 0 && sProject.fileCount > 0) { prjMapLine(&sProject, parser->errorLine, &errFileIdx, &errLocalLine); if (errFileIdx >= 0 && errFileIdx < sProject.fileCount) { errFile = sProject.files[errFileIdx].path; } } // Navigate to error location and build a user-friendly error message char procName[128] = {0}; int32_t procLine = errLocalLine; if (parser->errorLine > 0 && errFileIdx >= 0) { activateFile(errFileIdx, ViewCodeE); int32_t procCount = (int32_t)arrlen(sProcTable); for (int32_t i = procCount - 1; i >= 0; i--) { if (errLocalLine >= sProcTable[i].lineNum) { snprintf(procName, sizeof(procName), "%s.%s", sProcTable[i].objName, sProcTable[i].evtName); procLine = errLocalLine - sProcTable[i].lineNum + 1; break; } } navigateToCodeLine(errFileIdx, errLocalLine, procName[0] ? procName : NULL, false); } // Strip the "Line NNN: " prefix from the parser error const char *msg = parser->error; if (strncmp(msg, "Line ", 5) == 0) { while (*msg && *msg != ':') { msg++; } if (*msg == ':') { msg++; } while (*msg == ' ') { msg++; } } // Show the error with procedure name and proc-relative line int32_t n; if (procName[0]) { n = snprintf(sOutputBuf, IDE_MAX_OUTPUT, "COMPILE ERROR:\n%s line %d: %s\n", procName, (int)procLine, msg); } else { n = snprintf(sOutputBuf, IDE_MAX_OUTPUT, "COMPILE ERROR:\n%s line %d: %s\n", errFile, (int)errLocalLine, msg); } sOutputLen = n; setOutputText(sOutputBuf); // Ensure output window is visible showOutputWindow(); if (sOutWin) { dvxRaiseWindow(sAc, sOutWin); } setStatus("Compilation failed."); dvxSetBusy(sAc, false); basParserFree(parser); free(parser); free(concatBuf); return false; } free(concatBuf); BasModuleT *mod = basParserBuildModule(parser); basParserFree(parser); free(parser); if (!mod) { setStatus("Failed to build module."); dvxSetBusy(sAc, false); return false; } dvxSetBusy(sAc, false); // Cache the compiled module for Ctrl+F5 if (sCachedModule) { basModuleFree(sCachedModule); } sCachedModule = mod; // Update Object/Event dropdowns updateDropdowns(); return true; } // ============================================================ // compileAndRun // ============================================================ static void compileAndRun(void) { if (compileProject()) { runModule(sCachedModule); } } // ============================================================ // runCached // ============================================================ static void runCached(void) { if (!sCachedModule) { setStatus("No compiled program. Press F5 to compile first."); return; } clearOutput(); runModule(sCachedModule); } // ============================================================ // toggleBreakpoint -- toggle a breakpoint on the current editor line // ============================================================ static void toggleBreakpoint(void) { if (!sEditor) { return; } toggleBreakpointLine(wgtTextAreaGetCursorLine(sEditor)); } // ============================================================ // debugStartOrResume -- handle step/run-to-cursor commands // ============================================================ static void debugStartOrResume(int32_t cmd) { if (sDbgState == DBG_PAUSED && sVm) { // Already paused — apply the appropriate step command and resume sDbgCurrentLine = -1; switch (cmd) { case CMD_STEP_INTO: basVmStepInto(sVm); break; case CMD_STEP_OVER: basVmStepOver(sVm); break; case CMD_STEP_OUT: basVmStepOut(sVm); break; case CMD_RUN_TO_CURSOR: if (sEditor) { basVmRunToCursor(sVm, localToConcatLine(wgtTextAreaGetCursorLine(sEditor))); } break; } sDbgState = DBG_RUNNING; sVm->running = true; debugSetBreakTitles(false); if (sVm) { sVm->debugPaused = false; } if (sEditor) { wgtInvalidatePaint(sEditor); } updateProjectMenuState(); setStatus("Running..."); return; } if (sDbgState == DBG_IDLE) { // Not running — compile and start in debug mode. sDbgEnabled = true; sDbgBreakOnStart = true; compileAndRun(); sDbgBreakOnStart = false; } } // ============================================================ // runModule // ============================================================ static void runModule(BasModuleT *mod) { setStatus("Running..."); closeFindDialog(); // Hide designer windows while the program runs. // Keep the code window visible if debugging (breakpoints or step-into). bool hadFormWin = sFormWin && sFormWin->visible; bool hadToolbox = sToolboxWin && sToolboxWin->visible; bool hadProps = sPropsWin && sPropsWin->visible; bool hadCodeWin = sCodeWin && sCodeWin->visible; bool hadPrjWin = sProjectWin && sProjectWin->visible; if (sFormWin) { dvxHideWindow(sAc, sFormWin); } if (sToolboxWin) { dvxHideWindow(sAc, sToolboxWin); } if (sPropsWin) { dvxHideWindow(sAc, sPropsWin); } if (sCodeWin) { dvxHideWindow(sAc, sCodeWin); } if (sProjectWin) { dvxHideWindow(sAc, sProjectWin); } // Create VM BasVmT *vm = basVmCreate(); basVmLoadModule(vm, mod); // Set App.Path/Config/Data. In the IDE, config and data live under // the project directory so everything stays together during development. // Standalone apps use the DVX root-level CONFIG/ and DATA/ directories // since the app directory (on CD-ROM) is read-only. snprintf(vm->appPath, sizeof(vm->appPath), "%s", sProject.projectDir); snprintf(vm->appConfig, sizeof(vm->appConfig), "%s/CONFIG", sProject.projectDir); snprintf(vm->appData, sizeof(vm->appData), "%s/DATA", sProject.projectDir); platformMkdirRecursive(vm->appConfig); platformMkdirRecursive(vm->appData); // 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); vm->breakpointFn = onBreakpointHit; vm->breakpointCtx = NULL; // Set SQL callbacks BasSqlCallbacksT sqlCb; memset(&sqlCb, 0, sizeof(sqlCb)); sqlCb.sqlOpen = dvxSqlOpen; sqlCb.sqlClose = dvxSqlClose; sqlCb.sqlExec = dvxSqlExec; sqlCb.sqlError = dvxSqlError; sqlCb.sqlQuery = dvxSqlQuery; sqlCb.sqlNext = dvxSqlNext; sqlCb.sqlEof = dvxSqlEof; sqlCb.sqlFieldCount = dvxSqlFieldCount; sqlCb.sqlFieldName = dvxSqlFieldName; sqlCb.sqlFieldText = dvxSqlFieldText; sqlCb.sqlFieldByName = dvxSqlFieldByName; sqlCb.sqlFieldInt = dvxSqlFieldInt; sqlCb.sqlFieldDbl = dvxSqlFieldDbl; sqlCb.sqlFreeResult = dvxSqlFreeResult; sqlCb.sqlAffectedRows = dvxSqlAffectedRows; basVmSetSqlCallbacks(vm, &sqlCb); // Set extern library callbacks (DECLARE LIBRARY support) BasExternCallbacksT extCb; extCb.resolveExtern = basExternResolve; extCb.callExtern = basExternCall; extCb.ctx = NULL; basVmSetExternCallbacks(vm, &extCb); // Create form runtime (bridges UI opcodes to DVX widgets) BasFormRtT *formRt = basFormRtCreate(sAc, vm, mod); sVm = vm; sDbgFormRt = formRt; sDbgModule = mod; sDbgState = DBG_RUNNING; // Set project help file on form runtime for F1 context help if (sProject.helpFile[0]) { snprintf(formRt->helpFile, sizeof(formRt->helpFile), "%s%c%s", sProject.projectDir, DVX_PATH_SEP, sProject.helpFile); } updateProjectMenuState(); // Set breakpoints BEFORE loading forms so breakpoints in form // init code (module-level statements inside BEGINFORM) fire. if (sDbgEnabled) { buildVmBreakpoints(); basVmSetBreakpoints(vm, sVmBreakpoints, (int32_t)arrlen(sVmBreakpoints)); if (sDbgBreakOnStart) { basVmStepInto(vm); } } // Load forms from project files (AFTER breakpoints are set so // init code breakpoints fire), then show the startup form. loadFrmFiles(formRt); basFormRtLoadAllForms(formRt, sProject.startupForm); // Run in slices of 10000 steps, yielding to DVX between slices basVmSetStepLimit(vm, IDE_STEP_SLICE); BasVmResultE result; sStopRequested = false; for (;;) { if (sDbgState == DBG_PAUSED) { // Paused at breakpoint/step — spin on GUI events until user acts dvxUpdate(sAc); if (!sWin || !sAc->running || sStopRequested) { break; } // User may have pressed F5 (continue), F8 (step), or Esc (stop) if (sDbgState == DBG_RUNNING) { vm->running = true; } continue; } result = basVmRun(vm); if (result == BAS_VM_BREAKPOINT) { sDbgState = DBG_PAUSED; sDbgCurrentLine = vm->currentLine; debugNavigateToLine(vm->currentLine); debugUpdateWindows(); setStatus("Paused."); continue; } if (result == BAS_VM_STEP_LIMIT) { dvxUpdate(sAc); if (!sWin || !sAc->running || sStopRequested) { break; } continue; } if (result == BAS_VM_HALTED) { break; } // Runtime error — navigate to error line 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); if (vm->currentLine > 0 && sEditor) { wgtTextAreaGoToLine(sEditor, vm->currentLine); if (sCodeWin && !sCodeWin->visible) { dvxShowWindow(sAc, sCodeWin); } } break; } // VB-style event loop: after module-level code finishes, // keep processing events as long as any form is loaded. // The program ends when all forms are unloaded (closed). if (result == BAS_VM_HALTED && (int32_t)arrlen(formRt->forms) > 0) { setStatus("Running (event loop)..."); sStopRequested = false; while (sWin && sAc->running && (int32_t)arrlen(formRt->forms) > 0 && !sStopRequested && !vm->ended) { if (sDbgState == DBG_PAUSED) { // Paused inside an event handler debugNavigateToLine(sDbgCurrentLine); debugUpdateWindows(); setStatus("Paused."); // Wait for user to resume while (sDbgState == DBG_PAUSED && sWin && sAc->running && !sStopRequested) { dvxUpdate(sAc); } if (sDbgState == DBG_RUNNING) { vm->running = true; setStatus("Running (event loop)..."); } } dvxUpdate(sAc); } } sVm = NULL; sDbgFormRt = NULL; sDbgModule = NULL; sDbgState = DBG_IDLE; sDbgCurrentLine = -1; sDbgEnabled = false; basFormRtDestroy(formRt); basVmDestroy(vm); // If the IDE was closed while the program was running, skip // all UI updates — the windows are already destroyed. if (!sWin) { return; } updateProjectMenuState(); setOutputText(sOutputBuf); setStatus("Done."); // Restore IDE windows if (hadFormWin && sFormWin) { dvxShowWindow(sAc, sFormWin); } if (hadToolbox && sToolboxWin) { dvxShowWindow(sAc, sToolboxWin); } if (hadProps && sPropsWin) { dvxShowWindow(sAc, sPropsWin); } if (hadCodeWin && sCodeWin) { dvxShowWindow(sAc, sCodeWin); } if (hadPrjWin && sProjectWin) { dvxShowWindow(sAc, sProjectWin); } // Repaint to clear destroyed runtime forms and restore designer dvxUpdate(sAc); } // ============================================================ // 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); } } // immTryAssign -- handle "varName = expr" when paused at a breakpoint. // Evaluates the RHS, looks up the variable, and writes the value back // to the running VM. Returns true if handled as an assignment. // immParseScalarFromStr -- parse a string into a typed BasValueT based on the // target slot's current type. Returns true on success. static bool immParseScalarFromStr(const char *rhs, const BasValueT *target, BasValueT *outVal) { memset(outVal, 0, sizeof(*outVal)); switch (target->type) { case BAS_TYPE_INTEGER: outVal->type = BAS_TYPE_INTEGER; outVal->intVal = (int16_t)atoi(rhs); return true; case BAS_TYPE_LONG: outVal->type = BAS_TYPE_LONG; outVal->longVal = (int32_t)atol(rhs); return true; case BAS_TYPE_SINGLE: outVal->type = BAS_TYPE_SINGLE; outVal->sngVal = (float)atof(rhs); return true; case BAS_TYPE_DOUBLE: outVal->type = BAS_TYPE_DOUBLE; outVal->dblVal = atof(rhs); return true; case BAS_TYPE_BOOLEAN: outVal->type = BAS_TYPE_BOOLEAN; if (strcasecmp(rhs, "TRUE") == 0 || strcasecmp(rhs, "-1") == 0) { outVal->intVal = -1; } else { outVal->intVal = (atoi(rhs) != 0) ? -1 : 0; } return true; case BAS_TYPE_STRING: { const char *s = rhs; int32_t sLen = (int32_t)strlen(s); if (sLen >= 2 && s[0] == '"' && s[sLen - 1] == '"') { s++; sLen -= 2; } outVal->type = BAS_TYPE_STRING; outVal->strVal = basStringNew(s, sLen); return true; } default: return false; } } // immResolveLhsSlot -- parse the LHS of an assignment and resolve it to a // pointer into the running VM's live data. Handles: // varName -- scalar variable // varName(i) -- array element // varName.field -- UDT field // varName(i).field -- array element UDT field // Returns NULL if the LHS can't be resolved. *endPtr is set past the LHS. static BasValueT *immResolveLhsSlot(const char *lhs, const char **endPtr) { const char *p = lhs; // Extract variable name const char *nameStart = p; while ((*p >= 'A' && *p <= 'Z') || (*p >= 'a' && *p <= 'z') || (*p >= '0' && *p <= '9') || *p == '_') { p++; } if (p == nameStart) { return NULL; } char varName[128]; int32_t nameLen = (int32_t)(p - nameStart); if (nameLen >= (int32_t)sizeof(varName)) { return NULL; } memcpy(varName, nameStart, nameLen); varName[nameLen] = '\0'; // Look up the base variable const BasDebugVarT *dv = findDebugVar(varName); if (!dv) { return NULL; } BasValueT *slot = getDebugVarSlot(dv); if (!slot) { return NULL; } // Parse optional array subscript: (idx1, idx2, ...) if (*p == '(') { p++; // skip '(' if (slot->type != BAS_TYPE_ARRAY || !slot->arrVal) { return NULL; } int32_t indices[BAS_ARRAY_MAX_DIMS]; int32_t numIndices = 0; while (*p && *p != ')' && numIndices < BAS_ARRAY_MAX_DIMS) { while (*p == ' ') { p++; } indices[numIndices++] = atoi(p); // Skip past the number if (*p == '-') { p++; } while (*p >= '0' && *p <= '9') { p++; } while (*p == ' ') { p++; } if (*p == ',') { p++; } } if (*p == ')') { p++; } int32_t flatIdx = basArrayIndex(slot->arrVal, indices, numIndices); if (flatIdx < 0 || flatIdx >= slot->arrVal->totalElements) { return NULL; } slot = &slot->arrVal->elements[flatIdx]; } // Parse optional UDT field: .fieldName if (*p == '.') { p++; // skip '.' if (slot->type != BAS_TYPE_UDT || !slot->udtVal) { return NULL; } const char *fieldStart = p; while ((*p >= 'A' && *p <= 'Z') || (*p >= 'a' && *p <= 'z') || (*p >= '0' && *p <= '9') || *p == '_') { p++; } char fieldName[128]; int32_t fieldLen = (int32_t)(p - fieldStart); if (fieldLen <= 0 || fieldLen >= (int32_t)sizeof(fieldName)) { return NULL; } memcpy(fieldName, fieldStart, fieldLen); fieldName[fieldLen] = '\0'; // Find field index from debug UDT definitions int32_t fieldIdx = -1; for (int32_t t = 0; t < sDbgModule->debugUdtDefCount; t++) { if (sDbgModule->debugUdtDefs[t].typeId == slot->udtVal->typeId) { for (int32_t f = 0; f < sDbgModule->debugUdtDefs[t].fieldCount; f++) { if (strcasecmp(sDbgModule->debugUdtDefs[t].fields[f].name, fieldName) == 0) { fieldIdx = f; break; } } break; } } if (fieldIdx < 0 || fieldIdx >= slot->udtVal->fieldCount) { return NULL; } slot = &slot->udtVal->fields[fieldIdx]; } if (endPtr) { *endPtr = p; } return slot; } static bool immTryAssign(const char *expr) { if (sDbgState != DBG_PAUSED || !sVm || !sDbgModule) { return false; } // Skip leading whitespace const char *p = expr; while (*p == ' ' || *p == '\t') { p++; } // Skip optional LET keyword if (strncasecmp(p, "LET ", 4) == 0) { p += 4; while (*p == ' ' || *p == '\t') { p++; } } // Resolve the LHS to a live slot in the VM const char *afterLhs = NULL; BasValueT *slot = immResolveLhsSlot(p, &afterLhs); if (!slot || !afterLhs) { return false; } // Build display name from LHS char lhsName[256]; int32_t lhsLen = (int32_t)(afterLhs - p); if (lhsLen >= (int32_t)sizeof(lhsName)) { lhsLen = (int32_t)sizeof(lhsName) - 1; } memcpy(lhsName, p, lhsLen); lhsName[lhsLen] = '\0'; p = afterLhs; // Skip whitespace after LHS while (*p == ' ' || *p == '\t') { p++; } // Must have '=' (but not '==') if (*p != '=' || p[1] == '=') { return false; } p++; // skip '=' while (*p == ' ' || *p == '\t') { p++; } if (*p == '\0') { return false; } // Parse the RHS into a value matching the target slot's type BasValueT newVal; if (!immParseScalarFromStr(p, slot, &newVal)) { immPrintCallback(NULL, "Cannot assign to this variable type", true); return true; } // Write the value directly to the slot basValRelease(slot); *slot = newVal; // transfer ownership — don't release newVal // Show confirmation char confirm[256]; snprintf(confirm, sizeof(confirm), "%s = ", lhsName); immPrintCallback(NULL, confirm, false); formatValue(slot, confirm, sizeof(confirm)); immPrintCallback(NULL, confirm, true); // Update debug windows to reflect the change updateLocalsWindow(); updateWatchWindow(); return true; } static void evaluateImmediate(const char *expr) { if (!expr || *expr == '\0') { return; } // Try assignment first when paused if (immTryAssign(expr)) { 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); // If paused at a breakpoint, copy globals from the running VM // so the immediate window can inspect current variable values if (sDbgState == DBG_PAUSED && sVm && sDbgModule) { for (int32_t g = 0; g < BAS_VM_MAX_GLOBALS && g < sDbgModule->globalCount; g++) { vm->globals[g] = basValCopy(sVm->globals[g]); } } BasVmResultE result = basVmRun(vm); if (result != BAS_VM_HALTED && result != BAS_VM_OK && result != BAS_VM_BREAKPOINT) { 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); } // 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, sPrefs); // 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 = -1; prjLoadAllFiles(&sProject, sAc); char title[300]; snprintf(title, sizeof(title), "DVX BASIC - [%s]", sProject.name); if (sWin) { dvxSetTitle(sAc, sWin, title); } setStatus("Project created."); updateProjectMenuState(); } // ============================================================ // Recent files // ============================================================ static void recentLoad(void) { sRecentCount = 0; if (!sPrefs) { return; } for (int32_t i = 0; i < CMD_RECENT_MAX; i++) { char key[16]; snprintf(key, sizeof(key), "file%ld", (long)i); const char *val = prefsGetString(sPrefs, "recent", key, ""); if (val[0]) { snprintf(sRecentFiles[sRecentCount], DVX_MAX_PATH, "%s", val); sRecentCount++; } } } static void recentSave(void) { if (!sPrefs) { return; } for (int32_t i = 0; i < CMD_RECENT_MAX; i++) { char key[16]; snprintf(key, sizeof(key), "file%ld", (long)i); if (i < sRecentCount) { prefsSetString(sPrefs, "recent", key, sRecentFiles[i]); } else { prefsSetString(sPrefs, "recent", key, ""); } } prefsSave(sPrefs); } static void recentAdd(const char *path) { if (!path || !path[0]) { return; } // If already in the list, move it to the top for (int32_t i = 0; i < sRecentCount; i++) { if (strcasecmp(sRecentFiles[i], path) == 0) { // Shift entries down to make room at position 0 char tmp[DVX_MAX_PATH]; snprintf(tmp, DVX_MAX_PATH, "%s", sRecentFiles[i]); for (int32_t j = i; j > 0; j--) { snprintf(sRecentFiles[j], DVX_MAX_PATH, "%s", sRecentFiles[j - 1]); } snprintf(sRecentFiles[0], DVX_MAX_PATH, "%s", tmp); recentSave(); recentRebuildMenu(); return; } } // Shift existing entries down if (sRecentCount < CMD_RECENT_MAX) { sRecentCount++; } for (int32_t j = sRecentCount - 1; j > 0; j--) { snprintf(sRecentFiles[j], DVX_MAX_PATH, "%s", sRecentFiles[j - 1]); } snprintf(sRecentFiles[0], DVX_MAX_PATH, "%s", path); recentSave(); recentRebuildMenu(); } static void recentRebuildMenu(void) { if (!sFileMenu) { return; } // Truncate menu back to the base items (everything through Exit) sFileMenu->itemCount = sFileMenuBase; if (sRecentCount == 0) { return; } // Append separator + recent file items after Exit wmAddMenuSeparator(sFileMenu); for (int32_t i = 0; i < sRecentCount; i++) { // Show just the filename for shorter labels const char *name = strrchr(sRecentFiles[i], DVX_PATH_SEP); if (!name) { name = strrchr(sRecentFiles[i], '/'); } if (!name) { name = strrchr(sRecentFiles[i], '\\'); } name = name ? name + 1 : sRecentFiles[i]; char label[MAX_MENU_LABEL]; snprintf(label, sizeof(label), "&%ld %s", (long)(i + 1), name); wmAddMenuItem(sFileMenu, label, CMD_RECENT_BASE + i); } } static void recentOpen(int32_t index) { if (index < 0 || index >= sRecentCount) { return; } const char *path = sRecentFiles[index]; const char *ext = strrchr(path, '.'); if (ext && strcasecmp(ext, ".dbp") == 0) { // Project file closeProject(); if (!prjLoad(&sProject, path)) { dvxMessageBox(sAc, "Error", "Could not open project file.", MB_OK | MB_ICONERROR); return; } prjLoadAllFiles(&sProject, sAc); if (!sProjectWin) { sProjectWin = prjCreateWindow(sAc, &sProject, onPrjFileDblClick, updateProjectMenuState); if (sProjectWin) { sProjectWin->y = toolbarBottom() + 25; sProjectWin->onClose = onProjectWinClose; sProjectWin->onMenu = onMenu; sProjectWin->accelTable = sWin ? sWin->accelTable : NULL; } } else { prjRebuildTree(&sProject); } char title[300]; snprintf(title, sizeof(title), "DVX BASIC - [%s]", sProject.name); dvxSetTitle(sAc, sWin, title); setStatus("Project loaded."); } else { // Single file if (!promptAndSave()) { return; } ensureProject(path); if (!sProjectWin) { sProjectWin = prjCreateWindow(sAc, &sProject, onPrjFileDblClick, updateProjectMenuState); if (sProjectWin) { sProjectWin->y = toolbarBottom() + 25; sProjectWin->onClose = onProjectWinClose; dvxRaiseWindow(sAc, sProjectWin); } } } updateProjectMenuState(); recentAdd(path); } static void loadFile(void) { FileFilterT filters[] = { { "BASIC Files (*.bas)" }, { "Form Files (*.frm)" }, { "All Files (*.*)" } }; char path[DVX_MAX_PATH]; if (!dvxFileDialog(sAc, "Add File", FD_OPEN, NULL, filters, 3, path, sizeof(path))) { return; } const char *ext = strrchr(path, '.'); bool isForm = (ext && strcasecmp(ext, ".frm") == 0); if (sProject.projectPath[0] != '\0') { // Add the file to the current project const char *fileName = strrchr(path, '/'); const char *fileName2 = strrchr(path, '\\'); if (fileName2 > fileName) { fileName = fileName2; } fileName = fileName ? fileName + 1 : path; prjAddFile(&sProject, fileName, isForm); prjRebuildTree(&sProject); activateFile(sProject.fileCount - 1, ViewAutoE); } else { // No project -- create one from this file if (!promptAndSave()) { return; } ensureProject(path); if (!sProjectWin) { sProjectWin = prjCreateWindow(sAc, &sProject, onPrjFileDblClick, updateProjectMenuState); if (sProjectWin) { sProjectWin->y = toolbarBottom() + 25; sProjectWin->onClose = onProjectWinClose; dvxRaiseWindow(sAc, sProjectWin); } } } recentAdd(path); } // ============================================================ // saveFile // ============================================================ static void saveActiveFile(void) { if (sProject.projectPath[0] == '\0') { return; } int32_t idx = sProject.activeFileIdx; if (idx < 0 || idx >= sProject.fileCount) { return; } // Ensure buffer is up-to-date with editor/designer state stashCurrentFile(); PrjFileT *file = &sProject.files[idx]; char fullPath[DVX_MAX_PATH]; prjFullPath(&sProject, idx, fullPath, sizeof(fullPath)); if (file->buffer) { FILE *f = fopen(fullPath, "w"); if (f) { fputs(file->buffer, f); fclose(f); file->modified = false; if (file->isForm && sDesigner.form) { sDesigner.form->dirty = false; } } else { dvxMessageBox(sAc, "Error", "Could not write file.", MB_OK | MB_ICONERROR); return; } } setStatus("Saved."); updateDirtyIndicators(); } static void saveFile(void) { if (sProject.projectPath[0] == '\0' || sProject.activeFileIdx < 0) { 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; } } } updateDirtyIndicators(); } // ============================================================ // onPrjFileDblClick -- called when a file is clicked in the project tree // ============================================================ static void onPrjFileDblClick(int32_t fileIdx, bool isForm) { (void)isForm; activateFile(fileIdx, ViewAutoE); } // ============================================================ // 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)" } }; 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, sPrefs); snprintf(sProject.projectPath, sizeof(sProject.projectPath), "%s", dbpPath); prjSave(&sProject); sProject.dirty = false; // Create and show project window if (!sProjectWin) { sProjectWin = prjCreateWindow(sAc, &sProject, onPrjFileDblClick, updateProjectMenuState); if (sProjectWin) { sProjectWin->y = toolbarBottom() + 25; sProjectWin->onClose = onProjectWinClose; sProjectWin->onMenu = onMenu; sProjectWin->accelTable = sWin ? sWin->accelTable : NULL; } } else { prjRebuildTree(&sProject); } char title[300]; snprintf(title, sizeof(title), "DVX BASIC - [%s]", sProject.name); dvxSetTitle(sAc, sWin, title); setStatus("New project created."); updateProjectMenuState(); } // ============================================================ // openProject // ============================================================ static void openProject(void) { FileFilterT filters[] = { { "Project Files (*.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; } prjLoadAllFiles(&sProject, sAc); // Create and show project window if (!sProjectWin) { sProjectWin = prjCreateWindow(sAc, &sProject, onPrjFileDblClick, updateProjectMenuState); if (sProjectWin) { sProjectWin->y = toolbarBottom() + 25; sProjectWin->onClose = onProjectWinClose; sProjectWin->onMenu = onMenu; sProjectWin->accelTable = sWin ? sWin->accelTable : NULL; } } else { prjRebuildTree(&sProject); } char title[300]; snprintf(title, sizeof(title), "DVX BASIC - [%s]", sProject.name); dvxSetTitle(sAc, sWin, title); setStatus("Project loaded."); updateProjectMenuState(); recentAdd(path); } // ============================================================ // closeProject // ============================================================ static void closeProject(void) { if (sProject.projectPath[0] == '\0') { return; } closeFindDialog(); if (sProject.dirty) { prjSave(&sProject); } // Close designer windows if (sFormWin) { dvxDestroyWindow(sAc, sFormWin); cleanupFormWin(); } dsgnFree(&sDesigner); // Close code editor if (sCodeWin) { dvxDestroyWindow(sAc, sCodeWin); sCodeWin = NULL; sEditor = NULL; sObjDropdown = NULL; sEvtDropdown = NULL; } freeProcBufs(); clearAllBreakpoints(); // Close project window prjClose(&sProject); if (sProjectWin) { prjDestroyWindow(sAc, sProjectWin); sProjectWin = NULL; } if (sWin) { dvxSetTitle(sAc, sWin, "DVX BASIC"); } if (sStatus) { setStatus("Project closed."); } updateProjectMenuState(); } // ============================================================ // 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 (prefsGetBool(sPrefs, "editor", "renameSkipComments", true) && 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])) && (!prefsGetBool(sPrefs, "editor", "renameSkipComments", true) || !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 (only if editor has this form's code) if (sDesigner.form && sEditorFileIdx >= 0 && sEditorFileIdx < sProject.fileCount && sProject.files[sEditorFileIdx].isForm && strcasecmp(sProject.files[sEditorFileIdx].formName, sDesigner.form->name) == 0) { free(sDesigner.form->code); sDesigner.form->code = strdup(getFullSource()); sDesigner.form->dirty = true; } // Update cached formName if the active file is a form being renamed if (sProject.activeFileIdx >= 0 && sProject.activeFileIdx < sProject.fileCount) { PrjFileT *cur = &sProject.files[sProject.activeFileIdx]; if (cur->isForm && strcasecmp(cur->formName, oldName) == 0) { snprintf(cur->formName, sizeof(cur->formName), "%s", newName); } } // Rename in all project .bas file buffers (and non-active .frm code) for (int32_t i = 0; i < sProject.fileCount; i++) { // Skip the active file (already handled above) 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 before the window is destroyed. stashFormCode(); dvxDestroyWindow(sAc, win); sCodeWin = NULL; sEditor = NULL; sObjDropdown = NULL; sEvtDropdown = NULL; if (sLastFocusWin == win) { sLastFocusWin = NULL; } updateProjectMenuState(); } // ============================================================ // 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 loadFrmFiles(BasFormRtT *rt) { for (int32_t i = 0; i < sProject.fileCount; i++) { if (!sProject.files[i].isForm) { continue; } char fullPath[DVX_MAX_PATH]; prjFullPath(&sProject, i, fullPath, sizeof(fullPath)); FILE *f = fopen(fullPath, "r"); if (!f) { 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; } char *frmBuf = (char *)malloc(size + 1); if (!frmBuf) { fclose(f); continue; } int32_t bytesRead = (int32_t)fread(frmBuf, 1, size, f); fclose(f); frmBuf[bytesRead] = '\0'; BasFormT *form = basFormRtLoadFrm(rt, frmBuf, bytesRead); free(frmBuf); // Cache the form object name in the project file entry if (form && form->name[0]) { snprintf(sProject.files[i].formName, sizeof(sProject.files[i].formName), "%s", form->name); } } } // ============================================================ // loadFormCodeIntoEditor -- loads form code into proc buffers + editor // ============================================================ static void loadFormCodeIntoEditor(void) { if (!sDesigner.form) { return; } stashFormCode(); parseProcs(sDesigner.form->code ? sDesigner.form->code : ""); sEditorFileIdx = sProject.activeFileIdx; if (!sCodeWin) { showCodeWindow(); } bool saved = sDropdownNavSuppressed; sDropdownNavSuppressed = true; updateDropdowns(); sDropdownNavSuppressed = saved; showProc(-1); if (sEditor && !sEditor->onChange) { sEditor->onChange = onEditorChange; } } // ============================================================ // 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; } // Stop any running program sStopRequested = true; if (sVm) { sVm->running = false; sVm->debugPaused = false; } sDbgState = DBG_IDLE; sDbgCurrentLine = -1; sDbgEnabled = false; // 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 (sLocalsWin && sLocalsWin != win) { dvxDestroyWindow(sAc, sLocalsWin); } sLocalsWin = NULL; sLocalsList = NULL; if (sCallStackWin && sCallStackWin != win) { dvxDestroyWindow(sAc, sCallStackWin); } sCallStackWin = NULL; sCallStackList = NULL; if (sWatchWin && sWatchWin != win) { dvxDestroyWindow(sAc, sWatchWin); } sWatchWin = NULL; sWatchList = NULL; sWatchInput = NULL; if (sBreakpointWin && sBreakpointWin != win) { dvxDestroyWindow(sAc, sBreakpointWin); } sBreakpointWin = NULL; sBreakpointList = 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 // ============================================================ // ============================================================ // makeExecutable -- compile project into standalone .app file // ============================================================ static void makeExecutable(void) { if (sProject.projectPath[0] == '\0') { setStatus("Save the project first."); return; } // Compile (always recompile to ensure latest code) if (!compileProject()) { return; } // Ask for output path FileFilterT filters[] = { { "DVX Applications (*.app)" }, { "All Files (*.*)" } }; char outPath[DVX_MAX_PATH]; outPath[0] = '\0'; if (!dvxFileDialog(sAc, "Make Executable", FD_SAVE, NULL, filters, 2, outPath, sizeof(outPath))) { return; } // Ask debug or release const char *modeItems[] = { "Debug (include error info)" }; int32_t modeChoice = 0; if (!dvxChoiceDialog(sAc, "Build Mode", "Select build mode:", modeItems, 2, 0, &modeChoice)) { return; } bool release = (modeChoice == 1); setStatus(release ? "Building release executable..." : "Building debug executable..."); dvxSetBusy(sAc, true); dvxUpdate(sAc); // Make a copy of the module for potential stripping int32_t modLen = 0; uint8_t *modData = basModuleSerialize(sCachedModule, &modLen); if (!modData) { setStatus("Failed to serialize module."); dvxSetBusy(sAc, false); return; } // For release, deserialize, strip, re-serialize if (release) { BasModuleT *modCopy = basModuleDeserialize(modData, modLen); free(modData); if (!modCopy) { setStatus("Failed to prepare release module."); dvxSetBusy(sAc, false); return; } basStripModule(modCopy); modData = basModuleSerialize(modCopy, &modLen); basModuleFree(modCopy); if (!modData) { setStatus("Failed to serialize stripped module."); dvxSetBusy(sAc, false); return; } } // Serialize debug info from the original (unstripped) module int32_t dbgLen = 0; uint8_t *dbgData = NULL; if (!release) { dbgData = basDebugSerialize(sCachedModule, &dbgLen); } // Extract stub from our own resources DvxResHandleT *selfRes = dvxResOpen(sCtx->appPath); if (!selfRes) { setStatus("Cannot read IDE resources."); free(modData); free(dbgData); dvxSetBusy(sAc, false); return; } uint32_t stubSize = 0; void *stubData = dvxResRead(selfRes, "STUB", &stubSize); dvxResClose(selfRes); if (!stubData || stubSize == 0) { setStatus("Stub not found in IDE resources."); free(modData); free(dbgData); dvxSetBusy(sAc, false); return; } // Write stub to output file FILE *outFile = fopen(outPath, "wb"); if (!outFile) { setStatus("Cannot create output file."); free(stubData); free(modData); free(dbgData); dvxSetBusy(sAc, false); return; } fwrite(stubData, 1, stubSize, outFile); fclose(outFile); free(stubData); // Attach project property resources const char *projName = sProject.name[0] ? sProject.name : "BASIC App"; dvxResAppend(outPath, "name", DVX_RES_TEXT, projName, (uint32_t)strlen(projName) + 1); if (sProject.author[0]) { dvxResAppend(outPath, "author", DVX_RES_TEXT, sProject.author, (uint32_t)strlen(sProject.author) + 1); } if (sProject.company[0]) { dvxResAppend(outPath, "company", DVX_RES_TEXT, sProject.company, (uint32_t)strlen(sProject.company) + 1); } if (sProject.version[0]) { dvxResAppend(outPath, "version", DVX_RES_TEXT, sProject.version, (uint32_t)strlen(sProject.version) + 1); } if (sProject.copyright[0]) { dvxResAppend(outPath, "copyright", DVX_RES_TEXT, sProject.copyright, (uint32_t)strlen(sProject.copyright) + 1); } if (sProject.description[0]) { dvxResAppend(outPath, "description", DVX_RES_TEXT, sProject.description, (uint32_t)strlen(sProject.description) + 1); } // Attach icon: project icon or fallback to IDE's noicon resource if (sProject.iconPath[0]) { char iconFullPath[DVX_MAX_PATH]; snprintf(iconFullPath, sizeof(iconFullPath), "%s%c%s", sProject.projectDir, DVX_PATH_SEP, sProject.iconPath); FILE *iconFile = fopen(iconFullPath, "rb"); if (iconFile) { fseek(iconFile, 0, SEEK_END); long iconSize = ftell(iconFile); fseek(iconFile, 0, SEEK_SET); void *iconData = malloc(iconSize); if (iconData) { if (fread(iconData, 1, iconSize, iconFile) == (size_t)iconSize) { dvxResAppend(outPath, "icon32", DVX_RES_ICON, iconData, (uint32_t)iconSize); } free(iconData); } fclose(iconFile); } } else { // Use stock noicon from IDE resources DvxResHandleT *ideRes = dvxResOpen(sCtx->appPath); if (ideRes) { uint32_t noiconSize = 0; void *noiconData = dvxResRead(ideRes, "noicon", &noiconSize); if (noiconData) { dvxResAppend(outPath, "icon32", DVX_RES_ICON, noiconData, noiconSize); free(noiconData); } dvxResClose(ideRes); } } // Copy help file alongside the output app (if specified in project) if (sProject.helpFile[0]) { // Store just the filename as a text resource so the stub can find it const char *helpBase = sProject.helpFile; const char *sep = strrchr(helpBase, DVX_PATH_SEP); if (sep) { helpBase = sep + 1; } dvxResAppend(outPath, "helpfile", DVX_RES_TEXT, helpBase, (uint32_t)strlen(helpBase) + 1); // Copy the .hlp file to sit next to the output .app char helpSrc[DVX_MAX_PATH]; snprintf(helpSrc, sizeof(helpSrc), "%s%c%s", sProject.projectDir, DVX_PATH_SEP, sProject.helpFile); char outDir[DVX_MAX_PATH]; snprintf(outDir, sizeof(outDir), "%s", outPath); char *lastSep = strrchr(outDir, DVX_PATH_SEP); if (lastSep) { *lastSep = '\0'; } char helpDst[DVX_MAX_PATH]; snprintf(helpDst, sizeof(helpDst), "%s%c%s", outDir, DVX_PATH_SEP, helpBase); FILE *hSrc = fopen(helpSrc, "rb"); if (hSrc) { FILE *hDst = fopen(helpDst, "wb"); if (hDst) { char cpBuf[4096]; size_t n; while ((n = fread(cpBuf, 1, sizeof(cpBuf), hSrc)) > 0) { fwrite(cpBuf, 1, n, hDst); } fclose(hDst); } fclose(hSrc); } } // Attach MODULE resource dvxResAppend(outPath, "MODULE", DVX_RES_BINARY, modData, (uint32_t)modLen); free(modData); // Attach DEBUG resource if (dbgData) { dvxResAppend(outPath, "DEBUG", DVX_RES_BINARY, dbgData, (uint32_t)dbgLen); free(dbgData); } // Compile and attach form resources int32_t formIdx = 0; for (int32_t i = 0; i < sProject.fileCount; i++) { if (!sProject.files[i].isForm) { continue; } char *frmSrc = NULL; if (sProject.files[i].buffer) { frmSrc = strdup(sProject.files[i].buffer); } else { char fp[DVX_MAX_PATH]; prjFullPath(&sProject, i, fp, sizeof(fp)); FILE *ff = fopen(fp, "r"); if (ff) { fseek(ff, 0, SEEK_END); long sz = ftell(ff); fseek(ff, 0, SEEK_SET); frmSrc = (char *)malloc(sz + 1); if (frmSrc) { size_t rd = fread(frmSrc, 1, sz, ff); frmSrc[rd] = '\0'; } fclose(ff); } } if (!frmSrc) { continue; } int32_t frmLen = (int32_t)strlen(frmSrc); char resName[16]; snprintf(resName, sizeof(resName), "FORM%ld", (long)formIdx); dvxResAppend(outPath, resName, DVX_RES_BINARY, frmSrc, (uint32_t)frmLen); free(frmSrc); formIdx++; } dvxSetBusy(sAc, false); char msg[512]; snprintf(msg, sizeof(msg), "Created %s (%s)", outPath, release ? "release" : "debug"); setStatus(msg); } static void handleFileCmd(int32_t cmd) { switch (cmd) { case CMD_OPEN: loadFile(); break; case CMD_SAVE: saveFile(); break; case CMD_SAVE_ALL: saveFile(); 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; } } } if (sProject.projectPath[0] != '\0') { prjSave(&sProject); sProject.dirty = false; } setStatus("All files saved."); updateDirtyIndicators(); break; case CMD_MAKE_EXE: makeExecutable(); break; case CMD_EXIT: if (sWin) { onClose(sWin); } break; default: // Recent files if (cmd >= CMD_RECENT_BASE && cmd < CMD_RECENT_BASE + CMD_RECENT_MAX) { recentOpen(cmd - CMD_RECENT_BASE); } break; } } // ============================================================ // Find/Replace dialog (modeless) // ============================================================ typedef enum { ScopeFuncE, ScopeObjE, ScopeFileE, ScopeProjE } FindScopeE; static FindScopeE getFindScope(void) { if (!sScopeGroup) { return ScopeProjE; } int32_t idx = wgtRadioGetIndex(sScopeGroup); switch (idx) { case 0: return ScopeFuncE; case 1: return ScopeObjE; case 2: return ScopeFileE; default: return ScopeProjE; } } static bool getFindMatchCase(void) { return sCaseCheck && wgtCheckboxIsChecked(sCaseCheck); } static bool getFindForward(void) { if (!sDirGroup) { return true; } return wgtRadioGetIndex(sDirGroup) == 0; } static bool isReplaceEnabled(void) { return sReplCheck && wgtCheckboxIsChecked(sReplCheck); } static void onReplCheckChange(WidgetT *w) { (void)w; bool show = isReplaceEnabled(); if (sReplInput) { sReplInput->enabled = show; } if (sBtnReplace) { sBtnReplace->enabled = show; } if (sBtnReplAll) { sBtnReplAll->enabled = show; } if (sFindWin) { dvxInvalidateWindow(sAc, sFindWin); } } static void onFindNext(WidgetT *w) { (void)w; if (!sFindInput) { return; } const char *needle = wgtGetText(sFindInput); if (!needle || !needle[0]) { return; } snprintf(sFindText, sizeof(sFindText), "%s", needle); FindScopeE scope = getFindScope(); bool caseSens = getFindMatchCase(); bool forward = getFindForward(); if (scope == ScopeFuncE && sEditor) { // Search current procedure only if (!wgtTextAreaFindNext(sEditor, sFindText, caseSens, forward)) { setStatus("Not found."); } } else if (scope == ScopeObjE || scope == ScopeFileE || scope == ScopeProjE) { if (!findInProject(sFindText, caseSens)) { setStatus("Not found."); } } } static void onReplace(WidgetT *w) { (void)w; if (!sEditor || !sFindInput || !sReplInput || !isReplaceEnabled()) { return; } const char *needle = wgtGetText(sFindInput); const char *repl = wgtGetText(sReplInput); if (!needle || !needle[0]) { return; } snprintf(sFindText, sizeof(sFindText), "%s", needle); snprintf(sReplaceText, sizeof(sReplaceText), "%s", repl ? repl : ""); // If text is selected and matches the search, replace it, then find next const char *edText = wgtGetText(sEditor); if (edText) { // TODO: replace current selection if it matches, then find next // For now, just do find next onFindNext(w); } } static void onReplaceAll(WidgetT *w) { (void)w; if (!sFindInput || !sReplInput || !isReplaceEnabled()) { return; } const char *needle = wgtGetText(sFindInput); const char *repl = wgtGetText(sReplInput); if (!needle || !needle[0]) { return; } snprintf(sFindText, sizeof(sFindText), "%s", needle); snprintf(sReplaceText, sizeof(sReplaceText), "%s", repl ? repl : ""); bool caseSens = getFindMatchCase(); FindScopeE scope = getFindScope(); int32_t totalCount = 0; if (scope == ScopeFuncE && sEditor) { totalCount = wgtTextAreaReplaceAll(sEditor, sFindText, sReplaceText, caseSens); } else if (scope == ScopeProjE) { stashCurrentFile(); for (int32_t i = 0; i < sProject.fileCount; i++) { activateFile(i, sProject.files[i].isForm ? ViewCodeE : ViewAutoE); if (sEditor) { totalCount += wgtTextAreaReplaceAll(sEditor, sFindText, sReplaceText, caseSens); } } } else if (scope == ScopeFileE && sEditor) { // Replace in all procs of current file int32_t procCount = (int32_t)arrlen(sProcBufs); for (int32_t p = -1; p < procCount; p++) { showProc(p); if (sEditor) { totalCount += wgtTextAreaReplaceAll(sEditor, sFindText, sReplaceText, caseSens); } } } else if (scope == ScopeObjE && sEditor && sObjDropdown) { // Replace in all procs belonging to current object int32_t objIdx = wgtDropdownGetSelected(sObjDropdown); if (objIdx >= 0 && objIdx < (int32_t)arrlen(sObjItems)) { const char *objName = sObjItems[objIdx]; int32_t procCount = (int32_t)arrlen(sProcTable); for (int32_t p = 0; p < procCount; p++) { if (strcasecmp(sProcTable[p].objName, objName) == 0) { showProc(p); if (sEditor) { totalCount += wgtTextAreaReplaceAll(sEditor, sFindText, sReplaceText, caseSens); } } } } } char statusBuf[64]; snprintf(statusBuf, sizeof(statusBuf), "%d replacement(s) made.", (int)totalCount); setStatus(statusBuf); } static void onFindClose(WindowT *win) { (void)win; closeFindDialog(); } static void onFindCloseBtn(WidgetT *w) { (void)w; closeFindDialog(); } static void closeFindDialog(void) { if (sFindWin) { dvxDestroyWindow(sAc, sFindWin); sFindWin = NULL; sFindInput = NULL; sReplInput = NULL; sReplCheck = NULL; sBtnReplace = NULL; sBtnReplAll = NULL; sCaseCheck = NULL; sScopeGroup = NULL; sDirGroup = NULL; } } static void openFindDialog(bool showReplace) { if (sFindWin) { // Already open — just toggle replace mode and raise if (sReplCheck) { wgtCheckboxSetChecked(sReplCheck, showReplace); onReplCheckChange(sReplCheck); } dvxRaiseWindow(sAc, sFindWin); return; } sFindWin = dvxCreateWindowCentered(sAc, "Find / Replace", 320, 210, false); if (!sFindWin) { return; } sFindWin->onClose = onFindClose; sFindWin->onMenu = onMenu; sFindWin->accelTable = sWin ? sWin->accelTable : NULL; WidgetT *root = wgtInitWindow(sAc, sFindWin); root->spacing = wgtPixels(3); // Find row WidgetT *findRow = wgtHBox(root); findRow->spacing = wgtPixels(4); wgtLabel(findRow, "Find:"); sFindInput = wgtTextInput(findRow, 256); sFindInput->weight = 100; wgtSetText(sFindInput, sFindText); // Replace checkbox + input WidgetT *replRow = wgtHBox(root); replRow->spacing = wgtPixels(4); sReplCheck = wgtCheckbox(replRow, "Replace:"); wgtCheckboxSetChecked(sReplCheck, showReplace); sReplCheck->onChange = onReplCheckChange; sReplInput = wgtTextInput(replRow, 256); sReplInput->weight = 100; wgtSetText(sReplInput, sReplaceText); // Options row: scope + direction + case WidgetT *optRow = wgtHBox(root); optRow->spacing = wgtPixels(8); // Scope WidgetT *scopeFrame = wgtFrame(optRow, "Scope"); WidgetT *scopeBox = wgtVBox(scopeFrame); sScopeGroup = wgtRadioGroup(scopeBox); wgtRadio(sScopeGroup, "Function"); wgtRadio(sScopeGroup, "Object"); wgtRadio(sScopeGroup, "File"); wgtRadio(sScopeGroup, "Project"); wgtRadioGroupSetSelected(sScopeGroup, 3); // Project // Direction WidgetT *dirFrame = wgtFrame(optRow, "Direction"); WidgetT *dirBox = wgtVBox(dirFrame); sDirGroup = wgtRadioGroup(dirBox); wgtRadio(sDirGroup, "Forward"); wgtRadio(sDirGroup, "Backward"); wgtRadioGroupSetSelected(sDirGroup, 0); // Forward // Match Case WidgetT *caseBox = wgtVBox(optRow); sCaseCheck = wgtCheckbox(caseBox, "Match Case"); // Buttons WidgetT *btnRow = wgtHBox(root); btnRow->spacing = wgtPixels(4); btnRow->align = AlignEndE; WidgetT *btnFind = wgtButton(btnRow, "Find Next"); btnFind->onClick = onFindNext; sBtnReplace = wgtButton(btnRow, "Replace"); sBtnReplace->onClick = onReplace; sBtnReplAll = wgtButton(btnRow, "Replace All"); sBtnReplAll->onClick = onReplaceAll; WidgetT *btnClose = wgtButton(btnRow, "Close"); btnClose->onClick = onFindCloseBtn; // Set initial replace enable state onReplCheckChange(sReplCheck); dvxFitWindow(sAc, sFindWin); } // findInProject -- search all project files for a text match. // Starts from the current editor position in the current file, // then continues through subsequent files, wrapping around. // Opens the file and selects the match when found. // showProcAndFind -- switch to a procedure, sync the dropdowns, and // select the search match in the editor. static bool showProcAndFind(int32_t procIdx, const char *needle, bool caseSensitive) { showProc(procIdx); // Sync the Object/Event dropdowns to match if (procIdx >= 0 && procIdx < (int32_t)arrlen(sProcTable)) { selectDropdowns(sProcTable[procIdx].objName, sProcTable[procIdx].evtName); } else if (procIdx == -1 && sObjDropdown) { wgtDropdownSetSelected(sObjDropdown, 0); // (General) } if (sEditor) { return wgtTextAreaFindNext(sEditor, needle, caseSensitive, true); } return false; } // Case-insensitive strstr replacement (strcasestr is a GNU extension) static const char *findSubstrNoCase(const char *haystack, const char *needle, int32_t needleLen) { for (; *haystack; haystack++) { if (strncasecmp(haystack, needle, needleLen) == 0) { return haystack; } } return NULL; } static bool findInProject(const char *needle, bool caseSensitive) { if (!needle || !needle[0] || sProject.fileCount == 0) { return false; } int32_t needleLen = (int32_t)strlen(needle); // Stash current editor state so all buffers are up-to-date stashCurrentFile(); // Start from the active file, searching from after the current selection int32_t startFile = sProject.activeFileIdx >= 0 ? sProject.activeFileIdx : 0; // If the editor is open on the current file, try the current proc // first (no wrap — returns false if no more matches ahead). if (sEditor && sEditorFileIdx == startFile) { if (wgtTextAreaFindNext(sEditor, needle, caseSensitive, true)) { return true; } // No more matches in current proc — search remaining procs int32_t procCount = (int32_t)arrlen(sProcBufs); for (int32_t p = sCurProcIdx + 1; p < procCount; p++) { if (!sProcBufs[p]) { continue; } const char *found = caseSensitive ? strstr(sProcBufs[p], needle) : findSubstrNoCase(sProcBufs[p], needle, needleLen); if (found) { showProcAndFind(p, needle, caseSensitive); return true; } } // Search General section if we started in a proc if (sCurProcIdx >= 0 && sGeneralBuf) { const char *found = caseSensitive ? strstr(sGeneralBuf, needle) : findSubstrNoCase(sGeneralBuf, needle, needleLen); if (found) { showProcAndFind(-1, needle, caseSensitive); return true; } } // Search procs before the current one (wrap within file) for (int32_t p = 0; p < sCurProcIdx && p < procCount; p++) { if (!sProcBufs[p]) { continue; } const char *found = caseSensitive ? strstr(sProcBufs[p], needle) : findSubstrNoCase(sProcBufs[p], needle, needleLen); if (found) { showProcAndFind(p, needle, caseSensitive); return true; } } // Move to next file startFile = (startFile + 1) % sProject.fileCount; } // Search remaining files, proc by proc. // startFile was advanced past the current file if we already searched it. int32_t filesToSearch = sProject.fileCount; if (sEditor && sEditorFileIdx >= 0) { filesToSearch--; // skip the file we already searched above } for (int32_t attempt = 0; attempt < filesToSearch; attempt++) { int32_t fileIdx = (startFile + attempt) % sProject.fileCount; // Activate the file to load its proc buffers activateFile(fileIdx, sProject.files[fileIdx].isForm ? ViewCodeE : ViewAutoE); // Search General section if (sGeneralBuf) { const char *found = caseSensitive ? strstr(sGeneralBuf, needle) : findSubstrNoCase(sGeneralBuf, needle, needleLen); if (found) { showProcAndFind(-1, needle, caseSensitive); return true; } } // Search each procedure int32_t procCount = (int32_t)arrlen(sProcBufs); for (int32_t p = 0; p < procCount; p++) { if (!sProcBufs[p]) { continue; } const char *found = caseSensitive ? strstr(sProcBufs[p], needle) : findSubstrNoCase(sProcBufs[p], needle, needleLen); if (found) { showProcAndFind(p, needle, caseSensitive); return true; } } } return false; } static void handleEditCmd(int32_t cmd) { switch (cmd) { case CMD_CUT: case CMD_COPY: case CMD_PASTE: case CMD_SELECT_ALL: { static const int32_t keys[] = { 24, 3, 22, 1 }; int32_t key = keys[cmd - CMD_CUT]; WindowT *target = getLastFocusWin(); if (target && target->onKey) { target->onKey(target, key, ACCEL_CTRL); } break; } case CMD_DELETE: if (sFormWin && sDesigner.selectedIdx >= 0) { int32_t prevCount = (int32_t)arrlen(sDesigner.form->controls); dsgnOnKey(&sDesigner, KEY_DELETE); int32_t newCount = (int32_t)arrlen(sDesigner.form->controls); if (newCount != prevCount) { prpRebuildTree(&sDesigner); prpRefresh(&sDesigner); dvxInvalidateWindow(sAc, sFormWin); } } break; case CMD_FIND: openFindDialog(false); break; case CMD_FIND_NEXT: if (sFindText[0]) { onFindNext(NULL); } else { openFindDialog(false); } break; case CMD_REPLACE: openFindDialog(true); break; } } static void handleRunCmd(int32_t cmd) { switch (cmd) { case CMD_RUN: if (sDbgState == DBG_PAUSED) { // Resume from breakpoint — clear debug mode so it runs free sDbgCurrentLine = -1; sDbgState = DBG_RUNNING; sDbgEnabled = false; debugSetBreakTitles(false); if (sVm) { sVm->debugPaused = false; sVm->debugBreak = false; sVm->stepOverDepth = -1; sVm->stepOutDepth = -1; sVm->runToCursorLine = -1; basVmSetBreakpoints(sVm, NULL, 0); sVm->running = true; } if (sEditor) { wgtInvalidatePaint(sEditor); } updateProjectMenuState(); setStatus("Running..."); } else { sDbgEnabled = false; compileAndRun(); } break; case CMD_DEBUG: if (sDbgState == DBG_PAUSED) { // Already debugging — resume, run to next breakpoint sDbgCurrentLine = -1; sDbgState = DBG_RUNNING; debugSetBreakTitles(false); if (sVm) { sVm->debugPaused = false; sVm->debugBreak = false; sVm->stepOverDepth = -1; sVm->stepOutDepth = -1; sVm->runToCursorLine = -1; sVm->running = true; } if (sEditor) { wgtInvalidatePaint(sEditor); } updateProjectMenuState(); setStatus("Debugging..."); } else if (sDbgState == DBG_IDLE) { // Start in debug mode with breakpoints sDbgEnabled = true; compileAndRun(); } break; case CMD_RUN_NOCMP: runCached(); break; case CMD_STOP: sStopRequested = true; if (sVm) { sVm->running = false; sVm->debugPaused = false; } sDbgState = DBG_IDLE; sDbgCurrentLine = -1; sDbgEnabled = false; if (sEditor) { wgtInvalidatePaint(sEditor); } updateProjectMenuState(); setStatus("Program stopped."); break; case CMD_OUTPUT_TO_LOG: if (sWin && sWin->menuBar) { sOutputToLog = wmMenuItemIsChecked(sWin->menuBar, CMD_OUTPUT_TO_LOG); } break; case CMD_STEP_INTO: case CMD_STEP_OVER: case CMD_STEP_OUT: case CMD_RUN_TO_CURSOR: debugStartOrResume(cmd); break; case CMD_TOGGLE_BP: toggleBreakpoint(); break; case CMD_CLEAR: clearOutput(); break; case CMD_SAVE_ON_RUN: if (sWin && sWin->menuBar) { bool save = wmMenuItemIsChecked(sWin->menuBar, CMD_SAVE_ON_RUN); prefsSetBool(sPrefs, "run", "saveOnRun", save); prefsSave(sPrefs); } break; } } static void handleViewCmd(int32_t cmd) { switch (cmd) { case CMD_VIEW_CODE: { int32_t selFileIdx = prjGetSelectedFileIdx(); if (selFileIdx >= 0 && selFileIdx != sProject.activeFileIdx) { activateFile(selFileIdx, ViewCodeE); } else if (sProject.activeFileIdx >= 0) { stashDesignerState(); } break; } case CMD_VIEW_DESIGN: { int32_t selFileIdx = prjGetSelectedFileIdx(); if (selFileIdx >= 0 && selFileIdx != sProject.activeFileIdx && selFileIdx < sProject.fileCount && sProject.files[selFileIdx].isForm) { activateFile(selFileIdx, ViewDesignE); } else if (sProject.activeFileIdx >= 0) { switchToDesign(); } break; } case CMD_VIEW_TOOLBAR: if (sToolbar && sWin->menuBar) { bool show = wmMenuItemIsChecked(sWin->menuBar, CMD_VIEW_TOOLBAR); sToolbar->visible = show; dvxFitWindowH(sAc, sWin); prefsSetBool(sPrefs, "view", "toolbar", show); prefsSave(sPrefs); } break; case CMD_VIEW_STATUS: if (sStatusBar && sWin->menuBar) { bool show = wmMenuItemIsChecked(sWin->menuBar, CMD_VIEW_STATUS); sStatusBar->visible = show; dvxFitWindowH(sAc, sWin); prefsSetBool(sPrefs, "view", "statusbar", show); prefsSave(sPrefs); } break; case CMD_MENU_EDITOR: { // Activate selected form if not already active int32_t selMenuIdx = prjGetSelectedFileIdx(); if (selMenuIdx >= 0 && selMenuIdx != sProject.activeFileIdx && selMenuIdx < sProject.fileCount && sProject.files[selMenuIdx].isForm) { activateFile(selMenuIdx, ViewDesignE); } if (sDesigner.form) { // Snapshot old menu names for rename detection char **oldNames = NULL; int32_t oldCount = (int32_t)arrlen(sDesigner.form->menuItems); for (int32_t mi = 0; mi < oldCount; mi++) { arrput(oldNames, strdup(sDesigner.form->menuItems[mi].name)); } if (mnuEditorDialog(sAc, sDesigner.form)) { sDesigner.form->dirty = true; // Detect renames: match by position (items may have been // reordered, but renamed items keep their index) int32_t newCount = (int32_t)arrlen(sDesigner.form->menuItems); int32_t minCount = oldCount < newCount ? oldCount : newCount; for (int32_t mi = 0; mi < minCount; mi++) { if (oldNames[mi][0] && sDesigner.form->menuItems[mi].name[0] && strcasecmp(oldNames[mi], sDesigner.form->menuItems[mi].name) != 0) { ideRenameInCode(oldNames[mi], sDesigner.form->menuItems[mi].name); } } // Rebuild menu bar preview if (sFormWin) { wmDestroyMenuBar(sFormWin); dsgnBuildPreviewMenuBar(sFormWin, sDesigner.form); dvxInvalidateWindow(sAc, sFormWin); } // Rebuild Object dropdown to reflect added/removed/renamed items updateDropdowns(); } for (int32_t mi = 0; mi < oldCount; mi++) { free(oldNames[mi]); } arrfree(oldNames); } break; } } } // ============================================================ // Preferences dialog // ============================================================ // Color entry names for the Colors tab (matches SYNTAX_* indices) static const char *sSyntaxColorNames[] = { "Default Text", // 0 = SYNTAX_DEFAULT "Keywords", // 1 = SYNTAX_KEYWORD "Strings", // 2 = SYNTAX_STRING "Comments", // 3 = SYNTAX_COMMENT "Numbers", // 4 = SYNTAX_NUMBER "Operators", // 5 = SYNTAX_OPERATOR "Types", // 6 = SYNTAX_TYPE }; #define SYNTAX_COLOR_COUNT 7 // Default syntax colors (0x00RRGGBB; 0 = use widget default) static const uint32_t sDefaultSyntaxColors[SYNTAX_COLOR_COUNT] = { 0x00000000, // default -- not used (widget fg) 0x00000080, // keyword -- dark blue 0x00800000, // string -- dark red 0x00008000, // comment -- dark green 0x00800080, // number -- purple 0x00808000, // operator -- dark yellow 0x00008080, // type -- teal }; static struct { bool done; bool accepted; // General tab WidgetT *renameSkipComments; WidgetT *optionExplicit; WidgetT *tabWidthInput; WidgetT *useSpaces; WidgetT *defAuthor; WidgetT *defCompany; WidgetT *defVersion; WidgetT *defCopyright; WidgetT *defDescription; // Colors tab WidgetT *colorList; WidgetT *sliderR; WidgetT *sliderG; WidgetT *sliderB; WidgetT *lblR; WidgetT *lblG; WidgetT *lblB; WidgetT *colorSwatch; uint32_t syntaxColors[SYNTAX_COLOR_COUNT]; } sPrefsDlg; static void onPrefsOk(WidgetT *w) { (void)w; sPrefsDlg.accepted = true; sPrefsDlg.done = true; } static void onPrefsCancel(WidgetT *w) { (void)w; sPrefsDlg.done = true; } static void prefsUpdateSwatch(void) { if (!sPrefsDlg.colorSwatch) { return; } uint8_t r = (uint8_t)wgtSliderGetValue(sPrefsDlg.sliderR); uint8_t g = (uint8_t)wgtSliderGetValue(sPrefsDlg.sliderG); uint8_t b = (uint8_t)wgtSliderGetValue(sPrefsDlg.sliderB); uint32_t color = packColor(&sAc->display, r, g, b); wgtCanvasClear(sPrefsDlg.colorSwatch, color); } static void prefsUpdateColorSliders(void) { int32_t idx = wgtListBoxGetSelected(sPrefsDlg.colorList); if (idx < 0 || idx >= SYNTAX_COLOR_COUNT) { return; } uint32_t c = sPrefsDlg.syntaxColors[idx]; uint8_t r = (c >> 16) & 0xFF; uint8_t g = (c >> 8) & 0xFF; uint8_t b = c & 0xFF; wgtSliderSetValue(sPrefsDlg.sliderR, r); wgtSliderSetValue(sPrefsDlg.sliderG, g); wgtSliderSetValue(sPrefsDlg.sliderB, b); static char rBuf[8]; static char gBuf[8]; static char bBuf[8]; snprintf(rBuf, sizeof(rBuf), "%d", (int)r); snprintf(gBuf, sizeof(gBuf), "%d", (int)g); snprintf(bBuf, sizeof(bBuf), "%d", (int)b); wgtSetText(sPrefsDlg.lblR, rBuf); wgtSetText(sPrefsDlg.lblG, gBuf); wgtSetText(sPrefsDlg.lblB, bBuf); prefsUpdateSwatch(); } static void onColorListChange(WidgetT *w) { (void)w; prefsUpdateColorSliders(); } static void onColorSliderChange(WidgetT *w) { (void)w; int32_t idx = wgtListBoxGetSelected(sPrefsDlg.colorList); if (idx < 0 || idx >= SYNTAX_COLOR_COUNT) { return; } uint8_t r = (uint8_t)wgtSliderGetValue(sPrefsDlg.sliderR); uint8_t g = (uint8_t)wgtSliderGetValue(sPrefsDlg.sliderG); uint8_t b = (uint8_t)wgtSliderGetValue(sPrefsDlg.sliderB); static char rBuf[8]; static char gBuf[8]; static char bBuf[8]; snprintf(rBuf, sizeof(rBuf), "%d", (int)r); snprintf(gBuf, sizeof(gBuf), "%d", (int)g); snprintf(bBuf, sizeof(bBuf), "%d", (int)b); wgtSetText(sPrefsDlg.lblR, rBuf); wgtSetText(sPrefsDlg.lblG, gBuf); wgtSetText(sPrefsDlg.lblB, bBuf); sPrefsDlg.syntaxColors[idx] = ((uint32_t)r << 16) | ((uint32_t)g << 8) | (uint32_t)b; prefsUpdateSwatch(); } static void applySyntaxColors(void) { if (!sEditor) { return; } wgtTextAreaSetSyntaxColors(sEditor, sPrefsDlg.syntaxColors, SYNTAX_COLOR_COUNT); } static void showPreferencesDialog(void) { memset(&sPrefsDlg, 0, sizeof(sPrefsDlg)); WindowT *win = dvxCreateWindowCentered(sAc, "Preferences", 420, 440, false); if (!win) { return; } win->maxW = win->w; win->maxH = win->h; WidgetT *root = wgtInitWindow(sAc, win); root->spacing = wgtPixels(4); // ---- Tab control ---- WidgetT *tabs = wgtTabControl(root); tabs->weight = 100; // ======== General tab ======== WidgetT *generalPage = wgtTabPage(tabs, "General"); generalPage->spacing = wgtPixels(4); // Editor section WidgetT *edFrame = wgtFrame(generalPage, "Editor"); edFrame->spacing = wgtPixels(2); sPrefsDlg.renameSkipComments = wgtCheckbox(edFrame, "Skip comments/strings when renaming"); wgtCheckboxSetChecked(sPrefsDlg.renameSkipComments, prefsGetBool(sPrefs, "editor", "renameSkipComments", true)); sPrefsDlg.optionExplicit = wgtCheckbox(edFrame, "Require variable declaration (OPTION EXPLICIT)"); wgtCheckboxSetChecked(sPrefsDlg.optionExplicit, prefsGetBool(sPrefs, "editor", "optionExplicit", false)); WidgetT *tabRow = wgtHBox(edFrame); tabRow->spacing = wgtPixels(4); wgtLabel(tabRow, "Tab width:"); sPrefsDlg.tabWidthInput = wgtTextInput(tabRow, 4); sPrefsDlg.tabWidthInput->maxW = wgtPixels(40); char tabBuf[8]; snprintf(tabBuf, sizeof(tabBuf), "%d", (int)prefsGetInt(sPrefs, "editor", "tabWidth", 3)); wgtSetText(sPrefsDlg.tabWidthInput, tabBuf); sPrefsDlg.useSpaces = wgtCheckbox(edFrame, "Insert spaces instead of tabs"); wgtCheckboxSetChecked(sPrefsDlg.useSpaces, prefsGetBool(sPrefs, "editor", "useSpaces", true)); // Project Defaults section WidgetT *prjFrame = wgtFrame(generalPage, "New Project Defaults"); prjFrame->spacing = wgtPixels(2); prjFrame->weight = 100; WidgetT *r1 = wgtHBox(prjFrame); r1->spacing = wgtPixels(4); WidgetT *l1 = wgtLabel(r1, "Author:"); l1->minW = wgtPixels(80); sPrefsDlg.defAuthor = wgtTextInput(r1, 64); sPrefsDlg.defAuthor->weight = 100; wgtSetText(sPrefsDlg.defAuthor, prefsGetString(sPrefs, "defaults", "author", "")); WidgetT *r2 = wgtHBox(prjFrame); r2->spacing = wgtPixels(4); WidgetT *l2 = wgtLabel(r2, "Company:"); l2->minW = wgtPixels(80); sPrefsDlg.defCompany = wgtTextInput(r2, 64); sPrefsDlg.defCompany->weight = 100; wgtSetText(sPrefsDlg.defCompany, prefsGetString(sPrefs, "defaults", "company", "")); WidgetT *r3 = wgtHBox(prjFrame); r3->spacing = wgtPixels(4); WidgetT *l3 = wgtLabel(r3, "Version:"); l3->minW = wgtPixels(80); sPrefsDlg.defVersion = wgtTextInput(r3, 16); sPrefsDlg.defVersion->weight = 100; wgtSetText(sPrefsDlg.defVersion, prefsGetString(sPrefs, "defaults", "version", "1.0")); WidgetT *r4 = wgtHBox(prjFrame); r4->spacing = wgtPixels(4); WidgetT *l4 = wgtLabel(r4, "Copyright:"); l4->minW = wgtPixels(80); sPrefsDlg.defCopyright = wgtTextInput(r4, 64); sPrefsDlg.defCopyright->weight = 100; wgtSetText(sPrefsDlg.defCopyright, prefsGetString(sPrefs, "defaults", "copyright", "")); wgtLabel(prjFrame, "Description:"); sPrefsDlg.defDescription = wgtTextArea(prjFrame, 512); sPrefsDlg.defDescription->weight = 100; sPrefsDlg.defDescription->minH = wgtPixels(48); wgtSetText(sPrefsDlg.defDescription, prefsGetString(sPrefs, "defaults", "description", "")); // ======== Colors tab ======== WidgetT *colorsPage = wgtTabPage(tabs, "Colors"); colorsPage->spacing = wgtPixels(4); // Load current colors from prefs (or defaults) for (int32_t i = 0; i < SYNTAX_COLOR_COUNT; i++) { char key[32]; snprintf(key, sizeof(key), "color%d", (int)i); sPrefsDlg.syntaxColors[i] = (uint32_t)prefsGetInt(sPrefs, "syntax", key, (int32_t)sDefaultSyntaxColors[i]); } WidgetT *colorsHBox = wgtHBox(colorsPage); colorsHBox->spacing = wgtPixels(8); colorsHBox->weight = 100; // Left: color list sPrefsDlg.colorList = wgtListBox(colorsHBox); sPrefsDlg.colorList->weight = 100; sPrefsDlg.colorList->onChange = onColorListChange; wgtListBoxSetItems(sPrefsDlg.colorList, sSyntaxColorNames, SYNTAX_COLOR_COUNT); // Right: RGB sliders + value labels + swatch preview WidgetT *sliderBox = wgtVBox(colorsHBox); sliderBox->spacing = wgtPixels(2); sliderBox->weight = 100; wgtLabel(sliderBox, "Red:"); sPrefsDlg.sliderR = wgtSlider(sliderBox, 0, 255); sPrefsDlg.sliderR->onChange = onColorSliderChange; sPrefsDlg.lblR = wgtLabel(sliderBox, "0"); wgtLabelSetAlign(sPrefsDlg.lblR, AlignEndE); wgtLabel(sliderBox, "Green:"); sPrefsDlg.sliderG = wgtSlider(sliderBox, 0, 255); sPrefsDlg.sliderG->onChange = onColorSliderChange; sPrefsDlg.lblG = wgtLabel(sliderBox, "0"); wgtLabelSetAlign(sPrefsDlg.lblG, AlignEndE); wgtLabel(sliderBox, "Blue:"); sPrefsDlg.sliderB = wgtSlider(sliderBox, 0, 255); sPrefsDlg.sliderB->onChange = onColorSliderChange; sPrefsDlg.lblB = wgtLabel(sliderBox, "0"); wgtLabelSetAlign(sPrefsDlg.lblB, AlignEndE); wgtLabel(sliderBox, "Preview:"); sPrefsDlg.colorSwatch = wgtCanvas(sliderBox, 64, 24); // Select first color entry and load sliders wgtListBoxSetSelected(sPrefsDlg.colorList, 1); prefsUpdateColorSliders(); wgtTabControlSetActive(tabs, 0); // ---- OK / Cancel ---- WidgetT *btnRow = wgtHBox(root); btnRow->spacing = wgtPixels(8); btnRow->align = AlignEndE; WidgetT *btnOk = wgtButton(btnRow, "OK"); btnOk->minW = wgtPixels(60); btnOk->onClick = onPrefsOk; WidgetT *btnCancel = wgtButton(btnRow, "Cancel"); btnCancel->minW = wgtPixels(60); btnCancel->onClick = onPrefsCancel; dvxFitWindow(sAc, win); WindowT *prevModal = sAc->modalWindow; sAc->modalWindow = win; while (!sPrefsDlg.done && sAc->running) { dvxUpdate(sAc); } if (sPrefsDlg.accepted) { // General tab prefsSetBool(sPrefs, "editor", "renameSkipComments", wgtCheckboxIsChecked(sPrefsDlg.renameSkipComments)); prefsSetBool(sPrefs, "editor", "optionExplicit", wgtCheckboxIsChecked(sPrefsDlg.optionExplicit)); prefsSetBool(sPrefs, "editor", "useSpaces", wgtCheckboxIsChecked(sPrefsDlg.useSpaces)); const char *tw = wgtGetText(sPrefsDlg.tabWidthInput); int32_t tabW = tw ? atoi(tw) : 3; if (tabW < 1) { tabW = 1; } if (tabW > 8) { tabW = 8; } prefsSetInt(sPrefs, "editor", "tabWidth", tabW); if (sEditor) { wgtTextAreaSetTabWidth(sEditor, tabW); wgtTextAreaSetUseTabChar(sEditor, !wgtCheckboxIsChecked(sPrefsDlg.useSpaces)); } const char *val; val = wgtGetText(sPrefsDlg.defAuthor); prefsSetString(sPrefs, "defaults", "author", val ? val : ""); val = wgtGetText(sPrefsDlg.defCompany); prefsSetString(sPrefs, "defaults", "company", val ? val : ""); val = wgtGetText(sPrefsDlg.defVersion); prefsSetString(sPrefs, "defaults", "version", val ? val : "1.0"); val = wgtGetText(sPrefsDlg.defCopyright); prefsSetString(sPrefs, "defaults", "copyright", val ? val : ""); val = wgtGetText(sPrefsDlg.defDescription); prefsSetString(sPrefs, "defaults", "description", val ? val : ""); // Colors tab for (int32_t i = 0; i < SYNTAX_COLOR_COUNT; i++) { char key[32]; snprintf(key, sizeof(key), "color%d", (int)i); prefsSetInt(sPrefs, "syntax", key, (int32_t)sPrefsDlg.syntaxColors[i]); } applySyntaxColors(); prefsSave(sPrefs); } sAc->modalWindow = prevModal; dvxDestroyWindow(sAc, win); } static void handleWindowCmd(int32_t cmd) { switch (cmd) { case CMD_WIN_CODE: showCodeWindow(); if (sEditor && !sEditor->onChange) { sEditor->onChange = onEditorChange; } break; case CMD_WIN_OUTPUT: showOutputWindow(); break; case CMD_WIN_IMM: showImmediateWindow(); break; case CMD_WIN_LOCALS: showLocalsWindow(); break; case CMD_WIN_CALLSTACK: showCallStackWindow(); break; case CMD_WIN_WATCH: showWatchWindow(); break; case CMD_WIN_BREAKPOINTS: showBreakpointWindow(); break; case CMD_WIN_TOOLBOX: if (!sToolboxWin) { sToolboxWin = tbxCreate(sAc, &sDesigner); if (sToolboxWin) { sToolboxWin->y = toolbarBottom(); sToolboxWin->onMenu = onMenu; sToolboxWin->accelTable = sWin ? sWin->accelTable : NULL; } } break; case CMD_WIN_PROPS: if (!sPropsWin) { sPropsWin = prpCreate(sAc, &sDesigner); if (sPropsWin) { sPropsWin->y = toolbarBottom(); sPropsWin->onMenu = onMenu; sPropsWin->accelTable = sWin ? sWin->accelTable : NULL; } } break; case CMD_WIN_PROJECT: if (!sProjectWin) { sProjectWin = prjCreateWindow(sAc, &sProject, onPrjFileDblClick, updateProjectMenuState); if (sProjectWin) { sProjectWin->y = toolbarBottom() + 25; sProjectWin->onClose = onProjectWinClose; } } break; } } // ============================================================ // helpQueryHandler -- context-sensitive F1 help // ============================================================ static const char *helpLookupKeyword(const char *word) { for (int32_t i = 0; i < (int32_t)HELP_MAP_COUNT; i++) { if (strcasecmp(sHelpMap[i].keyword, word) == 0) { return sHelpMap[i].topic; } } return NULL; } static void helpSetCtrlTopic(const char *typeName) { helpBuildCtrlTopic(typeName, sCtx->helpTopic, sizeof(sCtx->helpTopic)); } static void helpQueryHandler(void *ctx) { (void)ctx; sCtx->helpTopic[0] = '\0'; // Restore IDE help file (may have been swapped for a BASIC program's) snprintf(sCtx->helpFile, sizeof(sCtx->helpFile), "%s", sIdeHelpFile); // Determine which window is focused WindowT *focusWin = NULL; if (sAc->stack.focusedIdx >= 0 && sAc->stack.focusedIdx < sAc->stack.count) { focusWin = sAc->stack.windows[sAc->stack.focusedIdx]; } // Running BASIC program: check if focused window belongs to a form if (sDbgFormRt && sDbgFormRt->helpFile[0] && focusWin) { for (int32_t i = 0; i < (int32_t)arrlen(sDbgFormRt->forms); i++) { BasFormT *form = sDbgFormRt->forms[i]; if (form->window == focusWin) { // Swap to the project's help file snprintf(sCtx->helpFile, sizeof(sCtx->helpFile), "%s", sDbgFormRt->helpFile); // Find the focused widget's HelpTopic WidgetT *focused = wgtGetFocused(); if (focused && focused->userData) { BasControlT *ctrl = (BasControlT *)focused->userData; if (ctrl->helpTopic[0]) { snprintf(sCtx->helpTopic, sizeof(sCtx->helpTopic), "%s", ctrl->helpTopic); return; } } // Fall back to form-level HelpTopic if (form->helpTopic[0]) { snprintf(sCtx->helpTopic, sizeof(sCtx->helpTopic), "%s", form->helpTopic); return; } // No topic set -- open help at default return; } } } // Code editor: look up the word under the cursor if (focusWin == sCodeWin && sEditor) { char word[128]; if (wgtTextAreaGetWordAtCursor(sEditor, word, sizeof(word)) > 0) { const char *topic = helpLookupKeyword(word); if (topic) { snprintf(sCtx->helpTopic, sizeof(sCtx->helpTopic), "%s", topic); return; } } // No keyword match -- open language reference snprintf(sCtx->helpTopic, sizeof(sCtx->helpTopic), "ide.editor"); return; } // Immediate window: look up the word under the cursor if (focusWin == sImmWin && sImmediate) { char word[128]; if (wgtTextAreaGetWordAtCursor(sImmediate, word, sizeof(word)) > 0) { const char *topic = helpLookupKeyword(word); if (topic) { snprintf(sCtx->helpTopic, sizeof(sCtx->helpTopic), "%s", topic); return; } } snprintf(sCtx->helpTopic, sizeof(sCtx->helpTopic), "ide.immediate"); return; } // Form designer: help for the selected control type if (focusWin == sFormWin && sDesigner.form) { if (sDesigner.selectedIdx >= 0 && sDesigner.selectedIdx < arrlen(sDesigner.form->controls)) { helpSetCtrlTopic(sDesigner.form->controls[sDesigner.selectedIdx]->typeName); } else { snprintf(sCtx->helpTopic, sizeof(sCtx->helpTopic), "ctrl.form"); } return; } // Toolbox: help for the active tool if (focusWin == sToolboxWin) { if (sDesigner.activeTool[0]) { helpSetCtrlTopic(sDesigner.activeTool); } else { snprintf(sCtx->helpTopic, sizeof(sCtx->helpTopic), "ide.toolbox"); } return; } // Properties panel if (focusWin == sPropsWin) { if (sDesigner.form && sDesigner.selectedIdx >= 0 && sDesigner.selectedIdx < arrlen(sDesigner.form->controls)) { helpSetCtrlTopic(sDesigner.form->controls[sDesigner.selectedIdx]->typeName); } else { snprintf(sCtx->helpTopic, sizeof(sCtx->helpTopic), "ctrl.common.props"); } return; } // Output window if (focusWin == sOutWin) { snprintf(sCtx->helpTopic, sizeof(sCtx->helpTopic), "ide.output"); return; } // Project window if (focusWin == sProjectWin) { snprintf(sCtx->helpTopic, sizeof(sCtx->helpTopic), "ide.project"); return; } // Debugger windows if (focusWin == sLocalsWin) { snprintf(sCtx->helpTopic, sizeof(sCtx->helpTopic), "ide.debug.locals"); return; } if (focusWin == sCallStackWin) { snprintf(sCtx->helpTopic, sizeof(sCtx->helpTopic), "ide.debug.callstack"); return; } if (focusWin == sWatchWin) { snprintf(sCtx->helpTopic, sizeof(sCtx->helpTopic), "ide.debug.watch"); return; } if (focusWin == sBreakpointWin) { snprintf(sCtx->helpTopic, sizeof(sCtx->helpTopic), "ide.debug.breakpoints"); return; } // Default: IDE overview snprintf(sCtx->helpTopic, sizeof(sCtx->helpTopic), "ide.overview"); } static void handleProjectCmd(int32_t cmd) { switch (cmd) { 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: { int32_t rmIdx = prjGetSelectedFileIdx(); if (rmIdx >= 0 && rmIdx < sProject.fileCount) { PrjFileT *rmFile = &sProject.files[rmIdx]; char rmMsg[DVX_MAX_PATH + 32]; snprintf(rmMsg, sizeof(rmMsg), "Remove %s from the project?", rmFile->path); if (dvxMessageBox(sAc, "Remove File", rmMsg, MB_YESNO | MB_ICONQUESTION) != ID_YES) { break; } if (rmFile->modified) { int32_t result = dvxPromptSave(sAc, "DVX BASIC"); if (result == DVX_SAVE_CANCEL) { break; } if (result == DVX_SAVE_YES) { saveActiveFile(); } } removeBreakpointsForFile(rmIdx); prjRemoveFile(&sProject, rmIdx); if (sProject.activeFileIdx == rmIdx) { sProject.activeFileIdx = -1; } else if (sProject.activeFileIdx > rmIdx) { sProject.activeFileIdx--; } prjRebuildTree(&sProject); updateProjectMenuState(); } break; } } } static void onMenu(WindowT *win, int32_t menuId) { (void)win; handleFileCmd(menuId); handleEditCmd(menuId); handleRunCmd(menuId); handleViewCmd(menuId); handleWindowCmd(menuId); handleProjectCmd(menuId); if (menuId == CMD_PREFERENCES) { showPreferencesDialog(); } if (menuId == CMD_DEBUG_LAYOUT && sWin && sWin->menuBar) { wgtSetDebugLayout(sAc, wmMenuItemIsChecked(sWin->menuBar, CMD_DEBUG_LAYOUT)); } if (menuId == CMD_HELP_CONTENTS) { char hlpPath[DVX_MAX_PATH]; char viewerPath[DVX_MAX_PATH]; snprintf(hlpPath, sizeof(hlpPath), "%s%c%s", sCtx->appDir, DVX_PATH_SEP, "dvxbasic.hlp"); snprintf(viewerPath, sizeof(viewerPath), "APPS%cKPUNCH%cDVXHELP%cDVXHELP.APP", DVX_PATH_SEP, DVX_PATH_SEP, DVX_PATH_SEP); shellLoadAppWithArgs(sAc, viewerPath, hlpPath); } if (menuId == CMD_HELP_API) { char viewerPath[DVX_MAX_PATH]; char sysHlp[DVX_MAX_PATH]; snprintf(viewerPath, sizeof(viewerPath), "APPS%cKPUNCH%cDVXHELP%cDVXHELP.APP", DVX_PATH_SEP, DVX_PATH_SEP, DVX_PATH_SEP); snprintf(sysHlp, sizeof(sysHlp), "APPS%cKPUNCH%cPROGMAN%cDVXHELP.HLP", DVX_PATH_SEP, DVX_PATH_SEP, DVX_PATH_SEP); shellLoadAppWithArgs(sAc, viewerPath, sysHlp); } if (menuId == CMD_HELP_ABOUT) { dvxMessageBox(sAc, "About DVX BASIC", "DVX BASIC 1.0\n" "Visual BASIC Development Environment\n" "for the DVX GUI System\n" "\n" "Copyright 2026 Scott Duensing", MB_OK | MB_ICONINFO); } } // ============================================================ // isCtrlArrayInDesigner -- check if a control name is a control // array member in the current designer form. // ============================================================ static bool isCtrlArrayInDesigner(const char *ctrlName) { if (!sDesigner.form) { return false; } int32_t count = (int32_t)arrlen(sDesigner.form->controls); for (int32_t i = 0; i < count; i++) { if (strcasecmp(sDesigner.form->controls[i]->name, ctrlName) == 0 && sDesigner.form->controls[i]->index >= 0) { return true; } } return false; } // ============================================================ // getEventExtraParams -- return the extra parameters for a // known event type (the part after "Index As Integer"). // ============================================================ static const char *getEventExtraParams(const char *evtName) { if (strcasecmp(evtName, "KeyPress") == 0) { return ", KeyAscii As Integer"; } if (strcasecmp(evtName, "KeyDown") == 0) { return ", KeyCode As Integer, Shift As Integer"; } if (strcasecmp(evtName, "KeyUp") == 0) { return ", KeyCode As Integer, Shift As Integer"; } if (strcasecmp(evtName, "MouseDown") == 0) { return ", Button As Integer, X As Integer, Y As Integer"; } if (strcasecmp(evtName, "MouseUp") == 0) { return ", Button As Integer, X As Integer, Y As Integer"; } if (strcasecmp(evtName, "MouseMove") == 0) { return ", Button As Integer, X As Integer, Y As Integer"; } if (strcasecmp(evtName, "Scroll") == 0) { return ", Delta As Integer"; } return ""; } // ============================================================ // onEvtDropdownChange // ============================================================ // // Navigate to the selected procedure when the event dropdown changes. static void onEvtDropdownChange(WidgetT *w) { (void)w; if (sDropdownNavSuppressed) { return; } 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]; // (Global) shows the General module-level section if (strcasecmp(selEvt, "(Global)") == 0) { showProc(-1); return; } // 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 for editing. // Don't mark dirty yet; saveCurProc will discard it if the // user doesn't add any code. char subName[128]; snprintf(subName, sizeof(subName), "%s_%s", selObj, evtName); char skeleton[512]; if (isCtrlArrayInDesigner(selObj)) { snprintf(skeleton, sizeof(skeleton), "Sub %s (Index As Integer%s)\n\nEnd Sub\n", subName, getEventExtraParams(evtName)); } else { snprintf(skeleton, sizeof(skeleton), "Sub %s ()\n\nEnd Sub\n", subName); } arrput(sProcBufs, strdup(skeleton)); showProc((int32_t)arrlen(sProcBufs) - 1); } // ============================================================ // 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", "KeyUp", "MouseDown", "MouseUp", "MouseMove", "Scroll", NULL }; // Form-specific events static const char *sFormEvents[] = { "Load", "QueryUnload", "Unload", "Resize", "Activate", "Deactivate", "KeyPress", "KeyDown", "KeyUp", "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) { // Always include (Global) to access module-level code arrput(sEvtItems, "(Global)"); for (int32_t i = 0; i < (int32_t)arrlen(existingEvts); i++) { arrput(sEvtItems, existingEvts[i]); } arrfree(existingEvts); int32_t evtCount = (int32_t)arrlen(sEvtItems); // Sort procs after (Global) if (evtCount > 2) { qsort(sEvtItems + 1, evtCount - 1, sizeof(const char *), cmpStrPtrs); } wgtDropdownSetItems(sEvtDropdown, sEvtItems, evtCount); wgtDropdownSetSelected(sEvtDropdown, 0); onEvtDropdownChange(sEvtDropdown); return; } // Check if this is a form name bool isForm = false; if (sDesigner.form && strcasecmp(selObj, sDesigner.form->name) == 0) { isForm = true; availEvents = sFormEvents; } // Check if this is a menu item (only event is Click) bool isMenuItem = false; if (!isForm && sDesigner.form) { for (int32_t i = 0; i < (int32_t)arrlen(sDesigner.form->menuItems); i++) { if (strcasecmp(sDesigner.form->menuItems[i].name, selObj) == 0) { isMenuItem = true; break; } } } if (isMenuItem) { static const char *sMenuEvents[] = { "Click", NULL }; availEvents = sMenuEvents; } // Get widget-specific events from the interface const WgtIfaceT *iface = NULL; if (!isForm && !isMenuItem && 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); // Sort: implemented events first, then unimplemented, alphabetical within each if (evtCount > 1) { qsort(sEvtItems, evtCount, sizeof(const char *), cmpEvtPtrs); } wgtDropdownSetItems(sEvtDropdown, sEvtItems, evtCount); if (evtCount > 0) { // Find the first implemented event (no brackets) int32_t selectIdx = -1; for (int32_t i = 0; i < evtCount; i++) { if (sEvtItems[i][0] != '[') { selectIdx = i; break; } } if (selectIdx >= 0) { // Navigate to the first implemented event wgtDropdownSetSelected(sEvtDropdown, selectIdx); onEvtDropdownChange(sEvtDropdown); } else { // No implemented events -- find and select the default event // for this widget type, which will create its skeleton const char *defEvt = NULL; if (isForm) { defEvt = dsgnDefaultEvent("Form"); } else if (sDesigner.form) { for (int32_t i = 0; i < (int32_t)arrlen(sDesigner.form->controls); i++) { if (strcasecmp(sDesigner.form->controls[i]->name, selObj) == 0) { defEvt = dsgnDefaultEvent(sDesigner.form->controls[i]->typeName); break; } } } if (!defEvt) { defEvt = "Click"; } // Find the default event in the list (it will be bracketed) for (int32_t i = 0; i < evtCount; i++) { const char *label = sEvtItems[i]; if (label[0] == '[') { label++; } if (strncasecmp(label, defEvt, strlen(defEvt)) == 0) { wgtDropdownSetSelected(sEvtDropdown, i); onEvtDropdownChange(sEvtDropdown); break; } } } } } // ============================================================ // 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 dsgnCopySelected(void) { if (!sDesigner.form || sDesigner.selectedIdx < 0) { return; } int32_t count = (int32_t)arrlen(sDesigner.form->controls); if (sDesigner.selectedIdx >= count) { return; } // Serialize the selected control to FRM text char buf[2048]; int32_t pos = 0; DsgnControlT *ctrl = sDesigner.form->controls[sDesigner.selectedIdx]; pos += snprintf(buf + pos, sizeof(buf) - pos, "Begin %s %s\n", ctrl->typeName, ctrl->name); if (ctrl->index >= 0) { pos += snprintf(buf + pos, sizeof(buf) - pos, " Index = %d\n", (int)ctrl->index); } pos += snprintf(buf + pos, sizeof(buf) - pos, " Caption = \"%s\"\n", wgtGetText(ctrl->widget) ? wgtGetText(ctrl->widget) : ""); if (ctrl->width > 0) { pos += snprintf(buf + pos, sizeof(buf) - pos, " MinWidth = %d\n", (int)ctrl->width); } if (ctrl->height > 0) { pos += snprintf(buf + pos, sizeof(buf) - pos, " MinHeight = %d\n", (int)ctrl->height); } if (ctrl->maxWidth > 0) { pos += snprintf(buf + pos, sizeof(buf) - pos, " MaxWidth = %d\n", (int)ctrl->maxWidth); } if (ctrl->maxHeight > 0) { pos += snprintf(buf + pos, sizeof(buf) - pos, " MaxHeight = %d\n", (int)ctrl->maxHeight); } if (ctrl->weight > 0) { pos += snprintf(buf + pos, sizeof(buf) - pos, " Weight = %d\n", (int)ctrl->weight); } for (int32_t i = 0; i < ctrl->propCount; i++) { if (strcasecmp(ctrl->props[i].name, "Caption") == 0) { continue; } pos += snprintf(buf + pos, sizeof(buf) - pos, " %s = \"%s\"\n", ctrl->props[i].name, ctrl->props[i].value); } // Save interface properties if (ctrl->widget) { const char *wgtName = wgtFindByBasName(ctrl->typeName); const WgtIfaceT *iface = wgtName ? wgtGetIface(wgtName) : NULL; if (iface) { for (int32_t i = 0; i < iface->propCount; i++) { const WgtPropDescT *p = &iface->props[i]; if (!p->getFn) { continue; } bool already = false; for (int32_t j = 0; j < ctrl->propCount; j++) { if (strcasecmp(ctrl->props[j].name, p->name) == 0) { already = true; break; } } if (already) { continue; } if (p->type == WGT_IFACE_ENUM && p->enumNames) { int32_t v = ((int32_t (*)(const WidgetT *))p->getFn)(ctrl->widget); const char *name = (v >= 0 && p->enumNames[v]) ? p->enumNames[v] : NULL; if (name) { pos += snprintf(buf + pos, sizeof(buf) - pos, " %s = %s\n", p->name, name); } } else if (p->type == WGT_IFACE_INT) { int32_t v = ((int32_t (*)(const WidgetT *))p->getFn)(ctrl->widget); pos += snprintf(buf + pos, sizeof(buf) - pos, " %s = %d\n", p->name, (int)v); } else if (p->type == WGT_IFACE_BOOL) { bool v = ((bool (*)(const WidgetT *))p->getFn)(ctrl->widget); pos += snprintf(buf + pos, sizeof(buf) - pos, " %s = %s\n", p->name, v ? "True" : "False"); } } } } pos += snprintf(buf + pos, sizeof(buf) - pos, "End\n"); dvxClipboardCopy(buf, pos); } static void dsgnPasteControl(void) { if (!sDesigner.form || !sDesigner.form->contentBox) { return; } int32_t clipLen = 0; const char *clip = dvxClipboardGet(&clipLen); if (!clip || clipLen <= 0) { return; } // Verify it looks like a control definition if (strncasecmp(clip, "Begin ", 6) != 0) { return; } // Parse type and name from "Begin TypeName CtrlName" const char *rest = clip + 6; char typeName[DSGN_MAX_NAME]; char ctrlName[DSGN_MAX_NAME]; int32_t ti = 0; while (*rest && *rest != ' ' && *rest != '\t' && ti < DSGN_MAX_NAME - 1) { typeName[ti++] = *rest++; } typeName[ti] = '\0'; while (*rest == ' ' || *rest == '\t') { rest++; } int32_t ci = 0; while (*rest && *rest != ' ' && *rest != '\t' && *rest != '\r' && *rest != '\n' && ci < DSGN_MAX_NAME - 1) { ctrlName[ci++] = *rest++; } ctrlName[ci] = '\0'; // Check if a control with the same name exists -- create control array char newName[DSGN_MAX_NAME]; int32_t newIndex = -1; bool nameExists = false; int32_t highIdx = -1; int32_t existCount = (int32_t)arrlen(sDesigner.form->controls); for (int32_t i = 0; i < existCount; i++) { if (strcasecmp(sDesigner.form->controls[i]->name, ctrlName) == 0) { nameExists = true; if (sDesigner.form->controls[i]->index > highIdx) { highIdx = sDesigner.form->controls[i]->index; } } } if (nameExists) { // Already a control array -- just add the next element if (highIdx >= 0) { snprintf(newName, DSGN_MAX_NAME, "%s", ctrlName); newIndex = highIdx + 1; } else { // Not yet an array -- ask the user int32_t result = dvxMessageBox(sAc, "Paste", "A control with this name already exists.\n" "Create a control array?", MB_YESNO | MB_ICONQUESTION); if (result == ID_YES) { // Convert existing control to index 0 for (int32_t i = 0; i < existCount; i++) { if (strcasecmp(sDesigner.form->controls[i]->name, ctrlName) == 0) { sDesigner.form->controls[i]->index = 0; break; } } snprintf(newName, DSGN_MAX_NAME, "%s", ctrlName); newIndex = 1; } else { // Rename to a unique name dsgnAutoName(&sDesigner, typeName, newName, DSGN_MAX_NAME); } } } else { dsgnAutoName(&sDesigner, typeName, newName, DSGN_MAX_NAME); } // Create the control DsgnControlT ctrl; memset(&ctrl, 0, sizeof(ctrl)); ctrl.index = newIndex; snprintf(ctrl.name, DSGN_MAX_NAME, "%s", newName); snprintf(ctrl.typeName, DSGN_MAX_NAME, "%s", typeName); // Parse properties from the clipboard text const char *line = rest; while (*line) { while (*line == '\r' || *line == '\n') { line++; } if (!*line) { break; } while (*line == ' ' || *line == '\t') { line++; } // "End" terminates if (strncasecmp(line, "End", 3) == 0 && (line[3] == '\0' || line[3] == '\r' || line[3] == '\n')) { break; } // Parse "Key = Value" char *eq = strchr(line, '='); if (eq) { char key[DSGN_MAX_NAME]; int32_t klen = 0; const char *kp = line; while (kp < eq && *kp != ' ' && *kp != '\t' && klen < DSGN_MAX_NAME - 1) { key[klen++] = *kp++; } key[klen] = '\0'; char *vp = (char *)eq + 1; while (*vp == ' ' || *vp == '\t') { vp++; } char val[DSGN_MAX_TEXT]; int32_t vi = 0; if (*vp == '"') { vp++; while (*vp && *vp != '"' && vi < DSGN_MAX_TEXT - 1) { val[vi++] = *vp++; } } else { while (*vp && *vp != '\r' && *vp != '\n' && vi < DSGN_MAX_TEXT - 1) { val[vi++] = *vp++; } while (vi > 0 && (val[vi - 1] == ' ' || val[vi - 1] == '\t')) { vi--; } } val[vi] = '\0'; if (strcasecmp(key, "MinWidth") == 0 || strcasecmp(key, "Width") == 0) { ctrl.width = atoi(val); } else if (strcasecmp(key, "MinHeight") == 0 || strcasecmp(key, "Height") == 0) { ctrl.height = atoi(val); } else if (strcasecmp(key, "MaxWidth") == 0) { ctrl.maxWidth = atoi(val); } else if (strcasecmp(key, "MaxHeight") == 0) { ctrl.maxHeight = atoi(val); } else if (strcasecmp(key, "Weight") == 0) { ctrl.weight = atoi(val); } else if (ctrl.propCount < DSGN_MAX_PROPS) { snprintf(ctrl.props[ctrl.propCount].name, DSGN_MAX_NAME, "%s", key); snprintf(ctrl.props[ctrl.propCount].value, DSGN_MAX_TEXT, "%s", val); ctrl.propCount++; } } // Advance to next line while (*line && *line != '\n') { line++; } } // Create the live widget WidgetT *parentWidget = sDesigner.form->contentBox; ctrl.widget = dsgnCreateDesignWidget(typeName, parentWidget); if (ctrl.widget) { if (ctrl.width > 0) { ctrl.widget->minW = wgtPixels(ctrl.width); } if (ctrl.height > 0) { ctrl.widget->minH = wgtPixels(ctrl.height); } if (ctrl.maxWidth > 0) { ctrl.widget->maxW = wgtPixels(ctrl.maxWidth); } if (ctrl.maxHeight > 0) { ctrl.widget->maxH = wgtPixels(ctrl.maxHeight); } ctrl.widget->weight = ctrl.weight; wgtSetName(ctrl.widget, ctrl.name); const char *caption = NULL; for (int32_t pi = 0; pi < ctrl.propCount; pi++) { if (strcasecmp(ctrl.props[pi].name, "Caption") == 0) { caption = ctrl.props[pi].value; break; } } if (caption) { wgtSetText(ctrl.widget, caption); } // Apply interface properties (Alignment, etc.) const char *wgtName = wgtFindByBasName(typeName); const WgtIfaceT *iface = wgtName ? wgtGetIface(wgtName) : NULL; if (iface) { for (int32_t pi = 0; pi < iface->propCount; pi++) { const WgtPropDescT *p = &iface->props[pi]; if (!p->setFn) { continue; } const char *val = NULL; for (int32_t j = 0; j < ctrl.propCount; j++) { if (strcasecmp(ctrl.props[j].name, p->name) == 0) { val = ctrl.props[j].value; break; } } if (!val) { continue; } if (p->type == WGT_IFACE_ENUM && p->enumNames) { for (int32_t en = 0; p->enumNames[en]; en++) { if (strcasecmp(p->enumNames[en], val) == 0) { ((void (*)(WidgetT *, int32_t))p->setFn)(ctrl.widget, en); break; } } } else if (p->type == WGT_IFACE_INT) { ((void (*)(WidgetT *, int32_t))p->setFn)(ctrl.widget, atoi(val)); } else if (p->type == WGT_IFACE_BOOL) { ((void (*)(WidgetT *, bool))p->setFn)(ctrl.widget, strcasecmp(val, "True") == 0); } else if (p->type == WGT_IFACE_STRING) { ((void (*)(WidgetT *, const char *))p->setFn)(ctrl.widget, val); } } } } DsgnControlT *heapCtrl = malloc(sizeof(DsgnControlT)); *heapCtrl = ctrl; arrput(sDesigner.form->controls, heapCtrl); sDesigner.selectedIdx = (int32_t)arrlen(sDesigner.form->controls) - 1; sDesigner.form->dirty = true; prpRebuildTree(&sDesigner); prpRefresh(&sDesigner); if (sFormWin) { dvxInvalidateWindow(sAc, sFormWin); } } static void onFormWinKey(WindowT *win, int32_t key, int32_t mod) { // Ctrl+C: copy selected control if (key == 3 && (mod & ACCEL_CTRL)) { dsgnCopySelected(); return; } // Ctrl+X: cut selected control if (key == 24 && (mod & ACCEL_CTRL)) { dsgnCopySelected(); if (sDesigner.selectedIdx >= 0) { dsgnOnKey(&sDesigner, KEY_DELETE); prpRebuildTree(&sDesigner); prpRefresh(&sDesigner); if (sFormWin) { dvxInvalidateWindow(sAc, sFormWin); } } return; } // Ctrl+V: paste control if (key == 22 && (mod & ACCEL_CTRL)) { dsgnPasteControl(); return; } 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; } // selectDropdowns -- set the Object and Event dropdowns to match a // given control name and event name. // selectDropdowns -- set the Object and Event dropdown selections to // match a given control/event without triggering navigation callbacks. // The caller is responsible for having already navigated to the proc. static void selectDropdowns(const char *objName, const char *evtName) { if (!sObjDropdown || !sEvtDropdown) { return; } // Select the object int32_t objCount = (int32_t)arrlen(sObjItems); for (int32_t i = 0; i < objCount; i++) { if (strcasecmp(sObjItems[i], objName) == 0) { wgtDropdownSetSelected(sObjDropdown, i); break; } } // Rebuild the event list for this object but suppress navigation bool savedSuppress = sDropdownNavSuppressed; sDropdownNavSuppressed = true; onObjDropdownChange(sObjDropdown); sDropdownNavSuppressed = savedSuppress; // Now select the specific event int32_t evtCount = (int32_t)arrlen(sEvtItems); for (int32_t i = 0; i < evtCount; i++) { const char *label = sEvtItems[i]; if (label[0] == '[') { label++; } if (strncasecmp(label, evtName, strlen(evtName)) == 0) { wgtDropdownSetSelected(sEvtDropdown, i); break; } } } // 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 navigateToNamedEventSub(const char *ctrlName, const char *eventName) { if (!sDesigner.form || !ctrlName || !eventName) { return; } char subName[128]; snprintf(subName, sizeof(subName), "%s_%s", ctrlName, eventName); // Load form code into editor (stashes existing code, parses procs, // populates dropdowns without triggering navigation) loadFormCodeIntoEditor(); if (!sEditor) { return; } // 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) { stashDesignerState(); showProc(i); if (sEditor && !sEditor->onChange) { sEditor->onChange = onEditorChange; } selectDropdowns(ctrlName, eventName); return; } } // Not found -- create a new sub skeleton for editing. // Don't mark dirty yet; saveCurProc will discard it if the // user doesn't add any code. char skeleton[512]; if (isCtrlArrayInDesigner(ctrlName)) { snprintf(skeleton, sizeof(skeleton), "Sub %s (Index As Integer%s)\n\nEnd Sub\n", subName, getEventExtraParams(eventName)); } else { snprintf(skeleton, sizeof(skeleton), "Sub %s ()\n\nEnd Sub\n", subName); } arrput(sProcBufs, strdup(skeleton)); // Show the new procedure (it's the last one) stashDesignerState(); showProc((int32_t)arrlen(sProcBufs) - 1); if (sEditor && !sEditor->onChange) { sEditor->onChange = onEditorChange; } selectDropdowns(ctrlName, eventName); } 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"); } navigateToNamedEventSub(ctrlName, eventName); } 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; updateDirtyIndicators(); } // ============================================================ // onFormWinPaint // ============================================================ // // Draw selection handles after widgets have painted. static void onFormWinPaint(WindowT *win, RectT *dirtyArea) { if (!win) { return; } // Force measure + relayout only on structural changes (control // add/remove/resize). Selection clicks just need repaint + overlay. if (win->paintNeeded >= PAINT_FULL && win->widgetRoot) { widgetCalcMinSizeTree(win->widgetRoot, &sAc->font); win->widgetRoot->w = 0; // force layout pass } // Designer always needs full repaint (handles must be erased/redrawn) win->paintNeeded = PAINT_FULL; 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; // Null out widget pointers — the widgets were destroyed with the window. // Without this, dsgnCreateWidgets skips controls that have non-NULL // widget pointers, resulting in an empty form on the second open. if (sDesigner.form) { int32_t count = (int32_t)arrlen(sDesigner.form->controls); for (int32_t i = 0; i < count; i++) { sDesigner.form->controls[i]->widget = NULL; } sDesigner.form->contentBox = 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) { // Store the outer window dimensions (win->w/h), not the content // dimensions (newW/newH), since dvxResizeWindow takes outer dims. sDesigner.form->width = win->w; sDesigner.form->height = win->h; 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(); } // onFormWinMenu -- menu click on the designer form preview. // Designer menu items use IDs starting at DSGN_MENU_ID_BASE. // Clicking one navigates to the menu item's Click event code. // Other IDs (from shared accel table) fall through to onMenu. static void onFormWinMenu(WindowT *win, int32_t menuId) { if (sDesigner.form && menuId >= DSGN_MENU_ID_BASE) { int32_t idx = menuId - DSGN_MENU_ID_BASE; int32_t menuCount = (int32_t)arrlen(sDesigner.form->menuItems); if (idx >= 0 && idx < menuCount) { const char *name = sDesigner.form->menuItems[idx].name; if (name[0]) { navigateToNamedEventSub(name, "Click"); return; } } } onMenu(win, menuId); } // ============================================================ // stashDesignerState -- save current editor content and set status // ============================================================ static void stashDesignerState(void) { // If a form is open in the designer, switch to its code view if (sDesigner.form && sProject.activeFileIdx >= 0 && sProject.activeFileIdx < sProject.fileCount && sProject.files[sProject.activeFileIdx].isForm) { loadFormCodeIntoEditor(); if (sCodeWin) { dvxRaiseWindow(sAc, sCodeWin); } setStatus("Code view."); return; } stashCurrentFile(); // Show code window if hidden if (sCodeWin && !sCodeWin->visible) { dvxShowWindow(sAc, sCodeWin); } if (sCodeWin) { dvxRaiseWindow(sAc, sCodeWin); } setStatus("Code view."); } // ============================================================ // switchToDesign // ============================================================ static void switchToDesign(void) { stashFormCode(); // If already open, just bring to front if (sFormWin) { return; } // If no form is loaded, create a blank one if (!sDesigner.form) { dsgnNewForm(&sDesigner, "Form1"); } // Create the form designer window using the shared form window builder const char *formName = sDesigner.form ? sDesigner.form->name : "Form1"; DsgnFormT *form = sDesigner.form; char title[128]; snprintf(title, sizeof(title), "%s [Design]", formName); WidgetT *root; WidgetT *contentBox; sFormWin = dsgnCreateFormWindow(sAc, title, form ? form->layout : "VBox", form ? form->resizable : true, false, false, form ? form->width : IDE_DESIGN_W, form ? form->height : IDE_DESIGN_H, 0, 0, &root, &contentBox); if (!sFormWin) { return; } sFormWin->visible = true; sFormWin->onClose = onFormWinClose; sFormWin->onMenu = onFormWinMenu; sFormWin->accelTable = sWin ? sWin->accelTable : NULL; sDesigner.formWin = sFormWin; // Build preview menu bar from form's menu items dsgnBuildPreviewMenuBar(sFormWin, sDesigner.form); // 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(); sToolboxWin->onMenu = onMenu; sToolboxWin->accelTable = sWin ? sWin->accelTable : NULL; } } if (!sPropsWin) { sPropsWin = prpCreate(sAc, &sDesigner); if (sPropsWin) { sPropsWin->y = toolbarBottom(); sPropsWin->onMenu = onMenu; sPropsWin->accelTable = sWin ? sWin->accelTable : NULL; } } dvxInvalidateWindow(sAc, sFormWin); updateProjectMenuState(); setStatus("Design view open."); } // ============================================================ // teardownFormWin -- destroy the form designer window if it exists // ============================================================ static void teardownFormWin(void) { if (sFormWin) { dvxDestroyWindow(sAc, sFormWin); cleanupFormWin(); } } // ============================================================ // Toolbar button handlers // ============================================================ static void onTbOpen(WidgetT *w) { (void)w; handleProjectCmd(CMD_PRJ_OPEN); } static void onTbSave(WidgetT *w) { (void)w; handleFileCmd(CMD_SAVE); } static void onTbRun(WidgetT *w) { (void)w; handleRunCmd(CMD_RUN); } static void onTbStop(WidgetT *w) { (void)w; handleRunCmd(CMD_STOP); } static void onTbCode(WidgetT *w) { (void)w; handleViewCmd(CMD_VIEW_CODE); } static void onTbDesign(WidgetT *w) { (void)w; handleViewCmd(CMD_VIEW_DESIGN); } static void debugSetBreakTitles(bool paused) { if (!sDbgFormRt) { return; } for (int32_t i = 0; i < (int32_t)arrlen(sDbgFormRt->forms); i++) { BasFormT *form = sDbgFormRt->forms[i]; if (!form->window) { continue; } char *title = form->window->title; const char *tag = " [break]"; int32_t tagLen = (int32_t)strlen(tag); int32_t titleLen = (int32_t)strlen(title); if (paused) { // Add [break] if not already there if (titleLen < tagLen || strcmp(title + titleLen - tagLen, tag) != 0) { if (titleLen + tagLen < MAX_TITLE_LEN) { strcat(title, tag); dvxInvalidateWindow(sAc, form->window); } } } else { // Remove [break] if present if (titleLen >= tagLen && strcmp(title + titleLen - tagLen, tag) == 0) { title[titleLen - tagLen] = '\0'; dvxInvalidateWindow(sAc, form->window); } } } } static void debugUpdateWindows(void) { // Auto-show debug windows if not already open if (!sLocalsWin) { showLocalsWindow(); } else if (!sLocalsWin->visible) { dvxShowWindow(sAc, sLocalsWin); } if (!sCallStackWin) { showCallStackWindow(); } else if (!sCallStackWin->visible) { dvxShowWindow(sAc, sCallStackWin); } updateLocalsWindow(); updateCallStackWindow(); updateWatchWindow(); } static void onBreakpointHit(void *ctx, int32_t line) { (void)ctx; sDbgState = DBG_PAUSED; if (sVm) { sVm->debugPaused = true; } if (line > 0) { sDbgCurrentLine = line; debugNavigateToLine(line); } debugSetBreakTitles(true); debugUpdateWindows(); updateProjectMenuState(); setStatus("Paused."); } static void onTbDebug(WidgetT *w) { (void)w; handleRunCmd(CMD_DEBUG); } static void onTbStepInto(WidgetT *w) { (void)w; handleRunCmd(CMD_STEP_INTO); } static void onTbStepOver(WidgetT *w) { (void)w; handleRunCmd(CMD_STEP_OVER); } static void onTbStepOut(WidgetT *w) { (void)w; handleRunCmd(CMD_STEP_OUT); } static void onTbRunToCur(WidgetT *w) { (void)w; handleRunCmd(CMD_RUN_TO_CURSOR); } // ============================================================ // 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); wgtLabel(dropdownRow, "Object:"); sObjDropdown = wgtDropdown(dropdownRow); sObjDropdown->weight = 100; sObjDropdown->onChange = onObjDropdownChange; wgtDropdownSetItems(sObjDropdown, NULL, 0); wgtLabel(dropdownRow, "Function:"); 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); // Apply saved syntax colors { uint32_t initColors[SYNTAX_COLOR_COUNT]; for (int32_t i = 0; i < SYNTAX_COLOR_COUNT; i++) { char key[32]; snprintf(key, sizeof(key), "color%d", (int)i); initColors[i] = (uint32_t)prefsGetInt(sPrefs, "syntax", key, (int32_t)sDefaultSyntaxColors[i]); } wgtTextAreaSetSyntaxColors(sEditor, initColors, SYNTAX_COLOR_COUNT); } wgtTextAreaSetLineDecorator(sEditor, debugLineDecorator, sAc); wgtTextAreaSetGutterClick(sEditor, onGutterClick); wgtTextAreaSetShowLineNumbers(sEditor, true); wgtTextAreaSetAutoIndent(sEditor, true); wgtTextAreaSetCaptureTabs(sEditor, true); wgtTextAreaSetTabWidth(sEditor, prefsGetInt(sPrefs, "editor", "tabWidth", 3)); wgtTextAreaSetUseTabChar(sEditor, !prefsGetBool(sPrefs, "editor", "useSpaces", true)); // onChange is set after initial content is loaded by the caller // (navigateToEventSub, onPrjFileDblClick, etc.) to prevent false dirty marking. updateProjectMenuState(); updateDirtyIndicators(); } } // ============================================================ // 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; sOutWin->onMenu = onMenu; sOutWin->accelTable = sWin ? sWin->accelTable : NULL; 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; sImmWin->onMenu = onMenu; sImmWin->accelTable = sWin ? sWin->accelTable : NULL; 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"); } } } // ============================================================ // showBreakpointWindow // ============================================================ #define MAX_BP_DISPLAY 64 static char sBpFiles[MAX_BP_DISPLAY][DVX_MAX_PATH]; static char sBpProcs[MAX_BP_DISPLAY][BAS_MAX_PROC_NAME * 2]; static char sBpLines[MAX_BP_DISPLAY][12]; static const char *sBpCells[MAX_BP_DISPLAY * 3]; static void onBreakpointWinClose(WindowT *win) { dvxHideWindow(sAc, win); } static void navigateToBreakpoint(int32_t bpIdx) { if (bpIdx < 0 || bpIdx >= sBreakpointCount) { return; } IdeBreakpointT *bp = &sBreakpoints[bpIdx]; const char *procName = (bp->procIdx >= 0 && bp->procName[0]) ? bp->procName : NULL; navigateToCodeLine(bp->fileIdx, bp->codeLine, procName, false); } static void onBreakpointListDblClick(WidgetT *w) { (void)w; if (!sBreakpointList) { return; } int32_t sel = wgtListViewGetSelected(sBreakpointList); navigateToBreakpoint(sel); } static void onBreakpointListKeyDown(WidgetT *w, int32_t keyCode, int32_t shift) { (void)w; (void)shift; // Delete key if (keyCode != (0x53 | 0x100) && keyCode != 127) { return; } if (!sBreakpointList || sBreakpointCount == 0) { return; } // Remove selected breakpoints in reverse order to preserve indices for (int32_t i = sBreakpointCount - 1; i >= 0; i--) { if (wgtListViewIsItemSelected(sBreakpointList, i)) { arrdel(sBreakpoints, i); } } sBreakpointCount = (int32_t)arrlen(sBreakpoints); updateBreakpointWindow(); // Repaint editor to remove breakpoint dots if (sEditor) { wgtInvalidatePaint(sEditor); } } static void showBreakpointWindow(void) { if (sBreakpointWin) { dvxShowWindow(sAc, sBreakpointWin); dvxRaiseWindow(sAc, sBreakpointWin); updateBreakpointWindow(); return; } int32_t winW = 320; int32_t winH = 180; int32_t winX = sAc->display.width - winW - 10; int32_t winY = sAc->display.height - winH - 10; sBreakpointWin = dvxCreateWindow(sAc, "Breakpoints", winX, winY, winW, winH, true); if (sBreakpointWin) { sBreakpointWin->onClose = onBreakpointWinClose; sBreakpointWin->onMenu = onMenu; sBreakpointWin->accelTable = sWin ? sWin->accelTable : NULL; sBreakpointWin->resizable = true; WidgetT *root = wgtInitWindow(sAc, sBreakpointWin); if (root) { sBreakpointList = wgtListView(root); if (sBreakpointList) { sBreakpointList->weight = 100; sBreakpointList->onKeyDown = onBreakpointListKeyDown; sBreakpointList->onDblClick = onBreakpointListDblClick; wgtListViewSetMultiSelect(sBreakpointList, true); static const ListViewColT cols[] = { { "File", wgtChars(12), ListViewAlignLeftE }, { "Procedure", wgtChars(16), ListViewAlignLeftE }, { "Line", wgtChars(6), ListViewAlignRightE }, }; wgtListViewSetColumns(sBreakpointList, cols, 3); } } } updateBreakpointWindow(); } // ============================================================ // updateBreakpointWindow // ============================================================ static void updateBreakpointWindow(void) { if (!sBreakpointList || !sBreakpointWin || !sBreakpointWin->visible) { return; } if (sBreakpointCount == 0) { wgtListViewSetData(sBreakpointList, NULL, 0); return; } int32_t count = sBreakpointCount; if (count > MAX_BP_DISPLAY) { count = MAX_BP_DISPLAY; } for (int32_t i = 0; i < count; i++) { // File name if (sBreakpoints[i].fileIdx >= 0 && sBreakpoints[i].fileIdx < sProject.fileCount) { snprintf(sBpFiles[i], sizeof(sBpFiles[i]), "%s", sProject.files[sBreakpoints[i].fileIdx].path); } else { snprintf(sBpFiles[i], sizeof(sBpFiles[i]), "?"); } // Procedure name snprintf(sBpProcs[i], sizeof(sBpProcs[i]), "%s", sBreakpoints[i].procName); // Line number snprintf(sBpLines[i], sizeof(sBpLines[i]), "%d", (int)sBreakpoints[i].codeLine); sBpCells[i * 3] = sBpFiles[i]; sBpCells[i * 3 + 1] = sBpProcs[i]; sBpCells[i * 3 + 2] = sBpLines[i]; } wgtListViewSetData(sBreakpointList, sBpCells, count); } // ============================================================ // showLocalsWindow // ============================================================ static void onLocalsClose(WindowT *win) { dvxHideWindow(sAc, win); } static void showLocalsWindow(void) { if (sLocalsWin) { dvxShowWindow(sAc, sLocalsWin); dvxRaiseWindow(sAc, sLocalsWin); return; } int32_t winW = 250; int32_t winH = 200; int32_t winX = sAc->display.width - winW; int32_t winY = toolbarBottom(); sLocalsWin = dvxCreateWindow(sAc, "Locals", winX, winY, winW, winH, true); if (sLocalsWin) { sLocalsWin->onClose = onLocalsClose; sLocalsWin->onMenu = onMenu; sLocalsWin->accelTable = sWin ? sWin->accelTable : NULL; sLocalsWin->resizable = true; WidgetT *root = wgtInitWindow(sAc, sLocalsWin); if (root) { sLocalsList = wgtListView(root); if (sLocalsList) { sLocalsList->weight = 100; static const ListViewColT cols[] = { { "Name", wgtChars(12), ListViewAlignLeftE }, { "Type", wgtChars(8), ListViewAlignLeftE }, { "Value", wgtChars(16), ListViewAlignLeftE }, }; wgtListViewSetColumns(sLocalsList, cols, 3); } } } updateLocalsWindow(); } // ============================================================ // updateLocalsWindow -- refresh locals display from VM state // ============================================================ #define MAX_LOCALS_DISPLAY 64 // Static cell data for the locals ListView static char sLocalsNames[MAX_LOCALS_DISPLAY][BAS_MAX_PROC_NAME]; static char sLocalsTypes[MAX_LOCALS_DISPLAY][16]; static char sLocalsValues[MAX_LOCALS_DISPLAY][64]; static const char *sLocalsCells[MAX_LOCALS_DISPLAY * 3]; static const char *typeNameStr(uint8_t dt) { switch (dt) { case BAS_TYPE_INTEGER: return "Integer"; case BAS_TYPE_LONG: return "Long"; case BAS_TYPE_SINGLE: return "Single"; case BAS_TYPE_DOUBLE: return "Double"; case BAS_TYPE_STRING: return "String"; case BAS_TYPE_BOOLEAN: return "Boolean"; case BAS_TYPE_ARRAY: return "Array"; case BAS_TYPE_UDT: return "UDT"; default: return "?"; } } static void formatValue(const BasValueT *v, char *buf, int32_t bufSize) { switch (v->type) { case BAS_TYPE_INTEGER: snprintf(buf, bufSize, "%d", (int)v->intVal); break; case BAS_TYPE_LONG: snprintf(buf, bufSize, "%ld", (long)v->longVal); break; case BAS_TYPE_SINGLE: snprintf(buf, bufSize, "%.6g", (double)v->sngVal); break; case BAS_TYPE_DOUBLE: snprintf(buf, bufSize, "%.10g", v->dblVal); break; case BAS_TYPE_BOOLEAN: snprintf(buf, bufSize, "%s", v->boolVal ? "True" : "False"); break; case BAS_TYPE_STRING: { if (v->strVal) { snprintf(buf, bufSize, "\"%.*s\"", (int)(bufSize - 3), v->strVal->data); } else { snprintf(buf, bufSize, "\"\""); } break; } case BAS_TYPE_ARRAY: { BasArrayT *arr = v->arrVal; if (!arr) { snprintf(buf, bufSize, "(uninitialized)"); break; } int32_t pos = snprintf(buf, bufSize, "%s(", typeNameStr(arr->elementType)); for (int32_t d = 0; d < arr->dims && pos < bufSize - 10; d++) { if (d > 0) { pos += snprintf(buf + pos, bufSize - pos, ", "); } pos += snprintf(buf + pos, bufSize - pos, "%d To %d", (int)arr->lbound[d], (int)arr->ubound[d]); } snprintf(buf + pos, bufSize - pos, ") [%d]", (int)arr->totalElements); break; } default: snprintf(buf, bufSize, "..."); break; } } static void updateLocalsWindow(void) { if (!sLocalsList || !sLocalsWin || !sLocalsWin->visible) { return; } if (sDbgState != DBG_PAUSED || !sVm || !sDbgModule) { wgtListViewSetData(sLocalsList, NULL, 0); return; } // Find which procedure we're in by matching PC to proc table int32_t curProcIdx = -1; int32_t bestAddr = -1; for (int32_t i = 0; i < sDbgModule->procCount; i++) { int32_t addr = sDbgModule->procs[i].codeAddr; if (addr <= sVm->pc && addr > bestAddr) { bestAddr = addr; curProcIdx = i; } } // Collect matching debug vars int32_t rowCount = 0; if (sDbgModule->debugVars) { for (int32_t i = 0; i < sDbgModule->debugVarCount && rowCount < MAX_LOCALS_DISPLAY; i++) { BasDebugVarT *dv = &sDbgModule->debugVars[i]; // Show locals for current proc, and globals/form vars if (dv->scope == SCOPE_LOCAL && dv->procIndex != curProcIdx) { continue; } // Skip internal mangled names (e.g. "DoCount$Count" for Static vars). // String variable names end with $ (e.g. "name$") — those are fine. // Mangled names have $ in the middle. { const char *dollar = strchr(dv->name, '$'); if (dollar && dollar[1] != '\0') { continue; } } // For form-scope vars, only show if we're in that form's context // and the form name matches the current form. if (dv->scope == SCOPE_FORM) { if (!sVm->currentFormVars) { continue; } // Match against current form name if (dv->formName[0] && sVm->currentForm) { BasFormT *curForm = (BasFormT *)sVm->currentForm; if (strcasecmp(dv->formName, curForm->name) != 0) { continue; } } } snprintf(sLocalsNames[rowCount], BAS_MAX_PROC_NAME, "%s", dv->name); // Read the value first so we can use it for the type column BasValueT val; memset(&val, 0, sizeof(val)); if (dv->scope == SCOPE_LOCAL && sVm->callDepth > 0) { BasCallFrameT *frame = &sVm->callStack[sVm->callDepth - 1]; if (dv->index >= 0 && dv->index < BAS_VM_MAX_LOCALS) { val = frame->locals[dv->index]; } } else if (dv->scope == SCOPE_GLOBAL) { if (dv->index >= 0 && dv->index < BAS_VM_MAX_GLOBALS) { val = sVm->globals[dv->index]; } } else if (dv->scope == SCOPE_FORM && sVm->currentFormVars) { if (dv->index >= 0 && dv->index < sVm->currentFormVarCount) { val = sVm->currentFormVars[dv->index]; } } // Type column — arrays show "Array(type)" with element type if (dv->dataType == BAS_TYPE_ARRAY && val.arrVal) { snprintf(sLocalsTypes[rowCount], 16, "%s()", typeNameStr(val.arrVal->elementType)); } else { snprintf(sLocalsTypes[rowCount], 16, "%s", typeNameStr(dv->dataType)); } formatValue(&val, sLocalsValues[rowCount], 64); sLocalsCells[rowCount * 3 + 0] = sLocalsNames[rowCount]; sLocalsCells[rowCount * 3 + 1] = sLocalsTypes[rowCount]; sLocalsCells[rowCount * 3 + 2] = sLocalsValues[rowCount]; rowCount++; } } wgtListViewSetData(sLocalsList, sLocalsCells, rowCount); } // ============================================================ // showCallStackWindow // ============================================================ static void onCallStackClose(WindowT *win) { dvxHideWindow(sAc, win); } static void showCallStackWindow(void) { if (sCallStackWin) { dvxShowWindow(sAc, sCallStackWin); dvxRaiseWindow(sAc, sCallStackWin); updateCallStackWindow(); return; } int32_t winW = 220; int32_t winH = 180; int32_t winX = sAc->display.width - winW; int32_t winY = toolbarBottom() + 210; sCallStackWin = dvxCreateWindow(sAc, "Call Stack", winX, winY, winW, winH, true); if (sCallStackWin) { sCallStackWin->onClose = onCallStackClose; sCallStackWin->onMenu = onMenu; sCallStackWin->accelTable = sWin ? sWin->accelTable : NULL; sCallStackWin->resizable = true; WidgetT *root = wgtInitWindow(sAc, sCallStackWin); if (root) { sCallStackList = wgtListView(root); if (sCallStackList) { sCallStackList->weight = 100; static const ListViewColT cols[] = { { "Procedure", wgtChars(16), ListViewAlignLeftE }, { "Line", wgtChars(6), ListViewAlignRightE }, }; wgtListViewSetColumns(sCallStackList, cols, 2); } } } updateCallStackWindow(); } // ============================================================ // updateCallStackWindow // ============================================================ #define MAX_CALLSTACK_DISPLAY 32 static char sCallNames[MAX_CALLSTACK_DISPLAY][BAS_MAX_PROC_NAME]; static char sCallLines[MAX_CALLSTACK_DISPLAY][16]; static const char *sCallCells[MAX_CALLSTACK_DISPLAY * 2]; static void updateCallStackWindow(void) { if (!sCallStackList || !sCallStackWin || !sCallStackWin->visible) { return; } if (sDbgState != DBG_PAUSED || !sVm || !sDbgModule) { wgtListViewSetData(sCallStackList, NULL, 0); return; } int32_t rowCount = 0; // Current location first if (sDbgCurrentLine > 0) { // Find proc name for current PC const char *procName = "(module)"; for (int32_t i = 0; i < sDbgModule->procCount; i++) { if (sDbgModule->procs[i].codeAddr <= sVm->pc) { bool best = true; for (int32_t j = 0; j < sDbgModule->procCount; j++) { if (sDbgModule->procs[j].codeAddr > sDbgModule->procs[i].codeAddr && sDbgModule->procs[j].codeAddr <= sVm->pc) { best = false; break; } } if (best) { procName = sDbgModule->procs[i].name; } } } snprintf(sCallNames[rowCount], BAS_MAX_PROC_NAME, "%s", procName); snprintf(sCallLines[rowCount], 16, "%d", (int)sDbgCurrentLine); sCallCells[rowCount * 2 + 0] = sCallNames[rowCount]; sCallCells[rowCount * 2 + 1] = sCallLines[rowCount]; rowCount++; } // Walk call stack (skip frame 0 which is the implicit module frame) for (int32_t d = sVm->callDepth - 2; d >= 0 && rowCount < MAX_CALLSTACK_DISPLAY; d--) { int32_t retPc = sVm->callStack[d + 1].returnPc; const char *name = "(module)"; for (int32_t i = 0; i < sDbgModule->procCount; i++) { if (sDbgModule->procs[i].codeAddr <= retPc) { bool best = true; for (int32_t j = 0; j < sDbgModule->procCount; j++) { if (sDbgModule->procs[j].codeAddr > sDbgModule->procs[i].codeAddr && sDbgModule->procs[j].codeAddr <= retPc) { best = false; break; } } if (best) { name = sDbgModule->procs[i].name; } } } snprintf(sCallNames[rowCount], BAS_MAX_PROC_NAME, "%s", name); sCallLines[rowCount][0] = '\0'; sCallCells[rowCount * 2 + 0] = sCallNames[rowCount]; sCallCells[rowCount * 2 + 1] = sCallLines[rowCount]; rowCount++; } wgtListViewSetData(sCallStackList, sCallCells, rowCount); } // ============================================================ // showWatchWindow // ============================================================ static void onWatchClose(WindowT *win) { dvxHideWindow(sAc, win); } static void watchEditSelected(void) { if (!sWatchList || !sWatchInput) { return; } int32_t sel = wgtListViewGetSelected(sWatchList); if (sel < 0 || sel >= sWatchExprCount) { return; } // Put expression text into the input box wgtSetText(sWatchInput, sWatchExprs[sel]); // Remove from list free(sWatchExprs[sel]); for (int32_t i = sel; i < sWatchExprCount - 1; i++) { sWatchExprs[i] = sWatchExprs[i + 1]; } sWatchExprCount--; updateWatchWindow(); // Focus the input box wgtSetFocused(sWatchInput); } static void onWatchListDblClick(WidgetT *w) { (void)w; watchEditSelected(); } static void onWatchListKeyDown(WidgetT *w, int32_t keyCode, int32_t shift) { (void)w; (void)shift; // Enter — edit selected item if (keyCode == '\r' || keyCode == '\n') { watchEditSelected(); return; } // Delete key (scancode 0x53 with extended flag) if (keyCode != (0x53 | 0x100) && keyCode != 127) { return; } if (!sWatchList) { return; } int32_t sel = wgtListViewGetSelected(sWatchList); if (sel < 0 || sel >= sWatchExprCount) { return; } free(sWatchExprs[sel]); for (int32_t i = sel; i < sWatchExprCount - 1; i++) { sWatchExprs[i] = sWatchExprs[i + 1]; } sWatchExprCount--; updateWatchWindow(); } static void onWatchInputKeyDown(WidgetT *w, int32_t keyCode, int32_t shift) { (void)w; (void)shift; if (keyCode != '\r' && keyCode != '\n') { return; } if (!sWatchInput) { return; } const char *text = wgtGetText(sWatchInput); if (!text || !text[0]) { return; } // Add to watch list if (sWatchExprCount < 16) { sWatchExprs[sWatchExprCount++] = strdup(text); updateWatchWindow(); } // Clear input wgtSetText(sWatchInput, ""); } static void showWatchWindow(void) { if (sWatchWin) { dvxShowWindow(sAc, sWatchWin); dvxRaiseWindow(sAc, sWatchWin); updateWatchWindow(); return; } int32_t winW = 280; int32_t winH = 180; int32_t winX = sAc->display.width - winW - 260; int32_t winY = toolbarBottom(); sWatchWin = dvxCreateWindow(sAc, "Watch", winX, winY, winW, winH, true); if (sWatchWin) { sWatchWin->onClose = onWatchClose; sWatchWin->onMenu = onMenu; sWatchWin->accelTable = sWin ? sWin->accelTable : NULL; sWatchWin->resizable = true; WidgetT *root = wgtInitWindow(sAc, sWatchWin); if (root) { // Expression input at top sWatchInput = wgtTextInput(root, 256); if (sWatchInput) { sWatchInput->onKeyDown = onWatchInputKeyDown; } // Results list below sWatchList = wgtListView(root); if (sWatchList) { sWatchList->weight = 100; sWatchList->onKeyDown = onWatchListKeyDown; sWatchList->onDblClick = onWatchListDblClick; static const ListViewColT cols[] = { { "Expression", wgtChars(14), ListViewAlignLeftE }, { "Value", wgtChars(20), ListViewAlignLeftE }, }; wgtListViewSetColumns(sWatchList, cols, 2); } } } updateWatchWindow(); } // ============================================================ // updateWatchWindow -- evaluate watch expressions // ============================================================ #define MAX_WATCH_DISPLAY 16 static char sWatchExprBuf[MAX_WATCH_DISPLAY][256]; static char sWatchValBuf[MAX_WATCH_DISPLAY][256]; static const char *sWatchCells[MAX_WATCH_DISPLAY * 2]; // readDebugVar -- read a debug variable's value from the paused VM static bool readDebugVar(const BasDebugVarT *dv, BasValueT *outVal) { BasValueT val; memset(&val, 0, sizeof(val)); if (dv->scope == SCOPE_LOCAL && sVm->callDepth > 0) { BasCallFrameT *frame = &sVm->callStack[sVm->callDepth - 1]; if (dv->index >= 0 && dv->index < BAS_VM_MAX_LOCALS) { val = frame->locals[dv->index]; } } else if (dv->scope == SCOPE_GLOBAL) { if (dv->index >= 0 && dv->index < BAS_VM_MAX_GLOBALS) { val = sVm->globals[dv->index]; } } else if (dv->scope == SCOPE_FORM && sVm->currentFormVars) { if (dv->index >= 0 && dv->index < sVm->currentFormVarCount) { val = sVm->currentFormVars[dv->index]; } } *outVal = val; return true; } // getDebugVarSlot -- return a pointer to the actual BasValueT in the running VM static BasValueT *getDebugVarSlot(const BasDebugVarT *dv) { if (!sVm) { return NULL; } if (dv->scope == SCOPE_LOCAL && sVm->callDepth > 0) { BasCallFrameT *frame = &sVm->callStack[sVm->callDepth - 1]; if (dv->index >= 0 && dv->index < BAS_VM_MAX_LOCALS) { return &frame->locals[dv->index]; } } else if (dv->scope == SCOPE_GLOBAL) { if (dv->index >= 0 && dv->index < BAS_VM_MAX_GLOBALS) { return &sVm->globals[dv->index]; } } else if (dv->scope == SCOPE_FORM && sVm->currentFormVars) { if (dv->index >= 0 && dv->index < sVm->currentFormVarCount) { return &sVm->currentFormVars[dv->index]; } } return NULL; } // findDebugVar -- find a debug variable by name, respecting scope static const BasDebugVarT *findDebugVar(const char *name) { if (!sDbgModule || !sDbgModule->debugVars) { return NULL; } // Find current proc index int32_t curProcIdx = -1; int32_t bestAddr = -1; for (int32_t i = 0; i < sDbgModule->procCount; i++) { int32_t addr = sDbgModule->procs[i].codeAddr; if (addr <= sVm->pc && addr > bestAddr) { bestAddr = addr; curProcIdx = i; } } for (int32_t i = 0; i < sDbgModule->debugVarCount; i++) { const BasDebugVarT *dv = &sDbgModule->debugVars[i]; if (strcasecmp(dv->name, name) != 0) { continue; } if (dv->scope == SCOPE_LOCAL && dv->procIndex != curProcIdx) { continue; } if (dv->scope == SCOPE_FORM && !sVm->currentFormVars) { continue; } return dv; } return NULL; } // lookupWatchVar -- evaluate a watch expression // Supports: varName, varName(idx), varName(idx1, idx2), varName.field static bool lookupWatchVar(const char *expr, BasValueT *outVal) { if (!sVm || !sDbgModule) { return false; } char buf[256]; snprintf(buf, sizeof(buf), "%s", expr); // Split on '.' for UDT field access: "varName.fieldName" char *dot = strchr(buf, '.'); char *fieldName = NULL; if (dot && dot > buf && !strchr(buf, '(')) { // Only treat as field access if no subscript before the dot *dot = '\0'; fieldName = dot + 1; } // Split on '(' for array subscript: "varName(idx1, idx2, ...)" char *paren = strchr(buf, '('); int32_t indices[BAS_ARRAY_MAX_DIMS]; int32_t numIndices = 0; if (paren) { char *close = strchr(paren, ')'); if (close) { *close = '\0'; } *paren = '\0'; char *arg = paren + 1; // Parse comma-separated indices while (*arg && numIndices < BAS_ARRAY_MAX_DIMS) { while (*arg == ' ') { arg++; } indices[numIndices++] = atoi(arg); char *comma = strchr(arg, ','); if (comma) { arg = comma + 1; } else { break; } } // Check for ".field" after the closing paren if (close && close[1] == '.') { fieldName = close + 2; } } // Look up the variable const BasDebugVarT *dv = findDebugVar(buf); if (!dv) { return false; } BasValueT val; if (!readDebugVar(dv, &val)) { return false; } // Apply array subscript if (numIndices > 0) { if (val.type != BAS_TYPE_ARRAY || !val.arrVal) { return false; } int32_t flatIdx = basArrayIndex(val.arrVal, indices, numIndices); if (flatIdx < 0 || flatIdx >= val.arrVal->totalElements) { return false; } val = val.arrVal->elements[flatIdx]; } // Apply UDT field access if (fieldName && fieldName[0]) { if (val.type != BAS_TYPE_UDT || !val.udtVal) { return false; } // Find the field by name using debug UDT definitions int32_t fieldIdx = -1; for (int32_t t = 0; t < sDbgModule->debugUdtDefCount; t++) { if (sDbgModule->debugUdtDefs[t].typeId == val.udtVal->typeId) { for (int32_t f = 0; f < sDbgModule->debugUdtDefs[t].fieldCount; f++) { if (strcasecmp(sDbgModule->debugUdtDefs[t].fields[f].name, fieldName) == 0) { fieldIdx = f; break; } } break; } } if (fieldIdx < 0 || fieldIdx >= val.udtVal->fieldCount) { return false; } val = val.udtVal->fields[fieldIdx]; } *outVal = val; return true; } // evalWatchExpr -- compile and evaluate an expression using the paused VM's state. // Used as a fallback when lookupWatchVar can't handle the expression. // Returns the printed result in outBuf. static char sWatchPrintBuf[256]; static int32_t sWatchPrintLen; static void watchPrintCallback(void *ctx, const char *text, bool newline) { (void)ctx; if (text) { int32_t tl = (int32_t)strlen(text); if (sWatchPrintLen + tl < (int32_t)sizeof(sWatchPrintBuf) - 1) { memcpy(sWatchPrintBuf + sWatchPrintLen, text, tl); sWatchPrintLen += tl; } } if (newline && sWatchPrintLen < (int32_t)sizeof(sWatchPrintBuf) - 1) { sWatchPrintBuf[sWatchPrintLen++] = '\n'; } sWatchPrintBuf[sWatchPrintLen] = '\0'; } static bool evalWatchExpr(const char *expr, char *outBuf, int32_t outBufSize) { if (!sVm || !sDbgModule || !sDbgModule->debugVars) { return false; } // Wrap expression: PRINT expr char wrapped[512]; snprintf(wrapped, sizeof(wrapped), "PRINT %s", expr); BasParserT *parser = (BasParserT *)malloc(sizeof(BasParserT)); if (!parser) { return false; } basParserInit(parser, wrapped, (int32_t)strlen(wrapped)); // Pre-populate the symbol table with debug vars from the paused VM. // All variables are added as globals so the expression can reference them. // Find current proc for local variable matching. int32_t curProcIdx = -1; int32_t bestAddr = -1; for (int32_t i = 0; i < sDbgModule->procCount; i++) { int32_t addr = sDbgModule->procs[i].codeAddr; if (addr <= sVm->pc && addr > bestAddr) { bestAddr = addr; curProcIdx = i; } } // Track which debug vars we added and their assigned global indices int32_t varMap[BAS_VM_MAX_GLOBALS]; // maps temp global idx -> debug var idx int32_t varMapCount = 0; for (int32_t i = 0; i < sDbgModule->debugVarCount && varMapCount < BAS_VM_MAX_GLOBALS; i++) { const BasDebugVarT *dv = &sDbgModule->debugVars[i]; // Skip locals from other procs if (dv->scope == SCOPE_LOCAL && dv->procIndex != curProcIdx) { continue; } if (dv->scope == SCOPE_FORM && !sVm->currentFormVars) { continue; } // Skip mangled names const char *dollar = strchr(dv->name, '$'); if (dollar && dollar[1] != '\0') { continue; } // Add to parser's symbol table as a global BasSymbolT *sym = basSymTabAdd(&parser->sym, dv->name, SYM_VARIABLE, dv->dataType); if (sym) { sym->scope = SCOPE_GLOBAL; sym->index = varMapCount; varMap[varMapCount] = i; varMapCount++; } } parser->cg.globalCount = varMapCount; // Parse and compile if (!basParse(parser)) { basParserFree(parser); free(parser); return false; } BasModuleT *mod = basParserBuildModule(parser); basParserFree(parser); free(parser); if (!mod) { return false; } // Create temp VM BasVmT *tvm = basVmCreate(); basVmLoadModule(tvm, mod); tvm->callStack[0].localCount = mod->globalCount > BAS_VM_MAX_LOCALS ? BAS_VM_MAX_LOCALS : mod->globalCount; tvm->callDepth = 1; // Copy values from the paused VM into the temp VM's globals for (int32_t g = 0; g < varMapCount; g++) { const BasDebugVarT *dv = &sDbgModule->debugVars[varMap[g]]; BasValueT val; memset(&val, 0, sizeof(val)); readDebugVar(dv, &val); tvm->globals[g] = basValCopy(val); } // Set up capture callback sWatchPrintBuf[0] = '\0'; sWatchPrintLen = 0; basVmSetPrintCallback(tvm, watchPrintCallback, NULL); // Run basVmRun(tvm); // Strip trailing newline if (sWatchPrintLen > 0 && sWatchPrintBuf[sWatchPrintLen - 1] == '\n') { sWatchPrintBuf[--sWatchPrintLen] = '\0'; } snprintf(outBuf, outBufSize, "%s", sWatchPrintBuf); basVmDestroy(tvm); basModuleFree(mod); return sWatchPrintLen > 0; } static void updateWatchWindow(void) { if (!sWatchList || !sWatchWin || !sWatchWin->visible) { return; } if (sWatchExprCount == 0) { wgtListViewSetData(sWatchList, NULL, 0); return; } for (int32_t i = 0; i < sWatchExprCount; i++) { snprintf(sWatchExprBuf[i], 256, "%s", sWatchExprs[i]); if (sDbgState == DBG_PAUSED && sVm && sDbgModule) { BasValueT val; memset(&val, 0, sizeof(val)); if (lookupWatchVar(sWatchExprs[i], &val)) { // Simple variable/field/subscript lookup succeeded formatValue(&val, sWatchValBuf[i], 256); } else if (evalWatchExpr(sWatchExprs[i], sWatchValBuf[i], 256)) { // Expression compiled and evaluated successfully } else { snprintf(sWatchValBuf[i], 256, ""); } } else { sWatchValBuf[i][0] = '\0'; } sWatchCells[i * 2 + 0] = sWatchExprBuf[i]; sWatchCells[i * 2 + 1] = sWatchValBuf[i]; } wgtListViewSetData(sWatchList, sWatchCells, sWatchExprCount); } // ============================================================ // setStatus // ============================================================ static int32_t countLines(const char *text) { if (!text || !text[0]) { return 1; } int32_t n = 1; for (const char *p = text; *p; p++) { if (*p == '\n') { n++; } } return n; } static void onEditorChange(WidgetT *w) { (void)w; // Adjust breakpoints when lines are added or removed if (sEditor && sBreakpointCount > 0) { const char *text = wgtGetText(sEditor); int32_t newLineCount = countLines(text); int32_t delta = newLineCount - sEditorLineCount; if (delta != 0) { int32_t fileIdx = sProject.activeFileIdx; int32_t cursorLine = wgtTextAreaGetCursorLine(sEditor); // Convert editor cursor line to file code line int32_t editCodeLine = cursorLine; if (sCurProcIdx >= 0 && sCurProcIdx < (int32_t)arrlen(sProcTable)) { editCodeLine = sProcTable[sCurProcIdx].lineNum + cursorLine - 1; } bool changed = false; for (int32_t i = sBreakpointCount - 1; i >= 0; i--) { if (sBreakpoints[i].fileIdx != fileIdx) { continue; } if (sBreakpoints[i].codeLine >= editCodeLine) { sBreakpoints[i].codeLine += delta; // Remove if shifted to invalid line if (sBreakpoints[i].codeLine < 1) { arrdel(sBreakpoints, i); sBreakpointCount = (int32_t)arrlen(sBreakpoints); } changed = true; } } if (changed) { updateBreakpointWindow(); } sEditorLineCount = newLineCount; } } // Mark the active file as modified if (sProject.activeFileIdx >= 0 && sProject.activeFileIdx < sProject.fileCount) { sProject.files[sProject.activeFileIdx].modified = true; // Only mark form dirty when editing the form's code, not a .bas file if (sProject.files[sProject.activeFileIdx].isForm && sDesigner.form) { sDesigner.form->dirty = true; } } updateDirtyIndicators(); } static void setStatus(const char *text) { if (sStatus) { wgtSetText(sStatus, text); } } // ============================================================ // updateDirtyIndicators -- update window titles and project tree // with "*" markers when files have unsaved changes. // ============================================================ static void updateProjectMenuState(void) { if (!sWin || !sWin->menuBar) { return; } bool hasProject = (sProject.projectPath[0] != '\0'); bool hasFile = (hasProject && sProject.activeFileIdx >= 0); bool hasForm = (hasFile && sProject.files[sProject.activeFileIdx].isForm); bool isIdle = (sDbgState == DBG_IDLE); bool isPaused = (sDbgState == DBG_PAUSED); bool isRunning = (sDbgState == DBG_RUNNING); bool canRun = hasProject && (isIdle || isPaused); bool canStop = isRunning || isPaused; // Project menu wmMenuItemSetEnabled(sWin->menuBar, CMD_PRJ_SAVE, hasProject && sProject.dirty); wmMenuItemSetEnabled(sWin->menuBar, CMD_PRJ_CLOSE, hasProject); wmMenuItemSetEnabled(sWin->menuBar, CMD_PRJ_PROPS, hasProject); wmMenuItemSetEnabled(sWin->menuBar, CMD_PRJ_REMOVE, prjGetSelectedFileIdx() >= 0); // Save: only when active file is dirty bool fileDirty = hasFile && sProject.files[sProject.activeFileIdx].modified; bool formDirty = hasFile && sDesigner.form && sDesigner.form->dirty; wmMenuItemSetEnabled(sWin->menuBar, CMD_SAVE, fileDirty || formDirty); // Save All: only when any file is dirty bool anyDirty = false; for (int32_t i = 0; i < sProject.fileCount && !anyDirty; i++) { if (sProject.files[i].modified) { anyDirty = true; } } if (sDesigner.form && sDesigner.form->dirty) { anyDirty = true; } wmMenuItemSetEnabled(sWin->menuBar, CMD_SAVE_ALL, anyDirty); wmMenuItemSetEnabled(sWin->menuBar, CMD_MAKE_EXE, hasProject && isIdle); // Edit menu wmMenuItemSetEnabled(sWin->menuBar, CMD_FIND, hasProject); wmMenuItemSetEnabled(sWin->menuBar, CMD_FIND_NEXT, hasProject); wmMenuItemSetEnabled(sWin->menuBar, CMD_REPLACE, hasProject); // View menu — consider both active file and project tree selection int32_t selIdx = prjGetSelectedFileIdx(); bool selIsFile = (hasProject && selIdx >= 0 && selIdx < sProject.fileCount); bool selIsForm = (selIsFile && sProject.files[selIdx].isForm); bool canCode = hasFile || selIsFile; bool canDesign = hasForm || selIsForm; wmMenuItemSetEnabled(sWin->menuBar, CMD_VIEW_CODE, canCode); wmMenuItemSetEnabled(sWin->menuBar, CMD_VIEW_DESIGN, canDesign); wmMenuItemSetEnabled(sWin->menuBar, CMD_MENU_EDITOR, canDesign); // Run menu wmMenuItemSetEnabled(sWin->menuBar, CMD_RUN, canRun); wmMenuItemSetEnabled(sWin->menuBar, CMD_RUN_NOCMP, canRun && sCachedModule != NULL); wmMenuItemSetEnabled(sWin->menuBar, CMD_DEBUG, canRun); wmMenuItemSetEnabled(sWin->menuBar, CMD_STOP, canStop); wmMenuItemSetEnabled(sWin->menuBar, CMD_STEP_INTO, canRun); wmMenuItemSetEnabled(sWin->menuBar, CMD_STEP_OVER, isPaused); wmMenuItemSetEnabled(sWin->menuBar, CMD_STEP_OUT, isPaused); wmMenuItemSetEnabled(sWin->menuBar, CMD_RUN_TO_CURSOR, isPaused); wmMenuItemSetEnabled(sWin->menuBar, CMD_TOGGLE_BP, hasFile); // Toolbar buttons if (sTbRun) { wgtSetEnabled(sTbRun, canRun); } if (sTbStop) { wgtSetEnabled(sTbStop, canStop); } if (sTbDebug) { wgtSetEnabled(sTbDebug, canRun); } if (sTbStepInto) { wgtSetEnabled(sTbStepInto, canRun); } if (sTbStepOver) { wgtSetEnabled(sTbStepOver, isPaused); } if (sTbStepOut) { wgtSetEnabled(sTbStepOut, isPaused); } if (sTbRunToCur) { wgtSetEnabled(sTbRunToCur, isPaused); } if (sTbCode) { wgtSetEnabled(sTbCode, canCode); } if (sTbDesign) { wgtSetEnabled(sTbDesign, canDesign); } } static void updateDirtyIndicators(void) { // Sync form->dirty to the project file entry so the tree, title // bar, and save logic all see a single consistent modified flag. if (sDesigner.form && sDesigner.form->dirty) { int32_t idx = sProject.activeFileIdx; if (idx >= 0 && idx < sProject.fileCount && sProject.files[idx].isForm) { sProject.files[idx].modified = true; } } // Toolbar title: "DVX BASIC - [ProjectName] *" if (sWin && sProject.projectPath[0] != '\0') { char title[300]; bool anyDirty = sProject.dirty; if (!anyDirty) { for (int32_t i = 0; i < sProject.fileCount; i++) { if (sProject.files[i].modified) { anyDirty = true; break; } } } snprintf(title, sizeof(title), "DVX BASIC - [%s]%s", sProject.name, anyDirty ? " *" : ""); dvxSetTitle(sAc, sWin, title); } // Code window title -- only shows * when code has been edited if (sCodeWin) { bool codeDirty = false; const char *codeFile = ""; if (sEditorFileIdx >= 0 && sEditorFileIdx < sProject.fileCount) { codeDirty = sProject.files[sEditorFileIdx].modified; codeFile = sProject.files[sEditorFileIdx].path; } else if (sProject.activeFileIdx >= 0) { codeDirty = sProject.files[sProject.activeFileIdx].modified; codeFile = sProject.files[sProject.activeFileIdx].path; } char codeTitle[DVX_MAX_PATH + 16]; snprintf(codeTitle, sizeof(codeTitle), "Code - %s%s", codeFile, codeDirty ? " *" : ""); dvxSetTitle(sAc, sCodeWin, codeTitle); } // Design window title if (sFormWin && sDesigner.form) { char title[280]; snprintf(title, sizeof(title), "%s [Design]%s", sDesigner.form->caption[0] ? sDesigner.form->caption : sDesigner.form->name, sDesigner.form->dirty ? " *" : ""); dvxSetTitle(sAc, sFormWin, title); } // Project tree: rebuild with "*" on dirty files if (sProjectWin) { prjRebuildTree(&sProject); } } // ============================================================ // 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; sEditorFileIdx = -1; } // 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++; } } // If no procs found, the entire source is the (General) section if (arrlen(sProcBufs) == 0) { genEnd = pos; // pos is at the end of the source } // 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; } // stashCurrentFile -- stash the currently active file's editor/designer // state back into its project buffer. This is caching only -- does not // mark modified. static void stashCurrentFile(void) { if (sProject.activeFileIdx < 0 || sProject.activeFileIdx >= sProject.fileCount) { return; } PrjFileT *cur = &sProject.files[sProject.activeFileIdx]; if (cur->isForm && sDesigner.form) { // Save editor code back to form->code before serializing stashFormCode(); // Serialize form designer state to .frm text char *frmBuf = (char *)malloc(IDE_MAX_SOURCE); if (frmBuf) { int32_t frmLen = dsgnSaveFrm(&sDesigner, frmBuf, IDE_MAX_SOURCE); free(cur->buffer); if (frmLen > 0) { frmBuf[frmLen] = '\0'; cur->buffer = frmBuf; } else { free(frmBuf); cur->buffer = NULL; } } } else if (!cur->isForm && sEditorFileIdx == sProject.activeFileIdx) { // Stash full source (only if editor has this file's code) saveCurProc(); const char *src = getFullSource(); free(cur->buffer); cur->buffer = src ? strdup(src) : NULL; } } // stashFormCode -- if the proc buffers belong to the designer's form, // save them back to form->code. Uses sEditorFileIdx to know which // file the proc buffers actually belong to. static void stashFormCode(void) { if (!sDesigner.form || sEditorFileIdx < 0) { return; } if (sEditorFileIdx >= sProject.fileCount || !sProject.files[sEditorFileIdx].isForm) { return; } if (strcasecmp(sProject.files[sEditorFileIdx].formName, sDesigner.form->name) != 0) { return; } saveCurProc(); free(sDesigner.form->code); sDesigner.form->code = strdup(getFullSource()); } // saveCurProc -- save editor contents back to the current buffer. // Returns true if the proc list was modified (skeleton discarded or // new procs extracted), meaning sProcBufs indices may have shifted. static bool saveCurProc(void) { if (!sEditor) { return false; } const char *edText = wgtGetText(sEditor); if (!edText) { return false; } if (sCurProcIdx == -1) { // General section -- check for embedded proc declarations char *cleaned = extractNewProcs(edText); free(sGeneralBuf); sGeneralBuf = cleaned ? cleaned : strdup(edText); if (cleaned) { wgtSetText(sEditor, sGeneralBuf); return true; } return false; } else if (sCurProcIdx >= 0 && sCurProcIdx < (int32_t)arrlen(sProcBufs)) { // Get the name of the current proc so we can identify its block // regardless of position in the editor. char ownName[128] = ""; if (sCurProcIdx < (int32_t)arrlen(sProcTable)) { snprintf(ownName, sizeof(ownName), "%s_%s", sProcTable[sCurProcIdx].objName, sProcTable[sCurProcIdx].evtName); } // If ownName is empty (General proc without underscore), extract // it from the first Sub/Function line of the existing buffer. if (ownName[0] == '\0' || strcmp(ownName, "(General)_") == 0) { const char *ep = sProcBufs[sCurProcIdx]; 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++; } int32_t n = 0; while (*ep && *ep != '(' && *ep != ' ' && *ep != '\t' && *ep != '\n' && n < 127) { ownName[n++] = *ep++; } ownName[n] = '\0'; } // Count Sub/Function blocks in the editor text int32_t blockCount = 0; const char *scan = edText; while (*scan) { const char *tl = scan; while (*tl == ' ' || *tl == '\t') { tl++; } if (strncasecmp(tl, "SUB ", 4) == 0 || strncasecmp(tl, "FUNCTION ", 9) == 0) { blockCount++; } while (*scan && *scan != '\n') { scan++; } if (*scan == '\n') { scan++; } } if (blockCount <= 1) { // Single proc (or none) -- check for empty skeleton const char *p = edText; while (*p == ' ' || *p == '\t') { p++; } bool isSub = (strncasecmp(p, "SUB ", 4) == 0); const char *endTag = isSub ? "END SUB" : "END FUNCTION"; int32_t endTagLen = isSub ? 7 : 12; // Skip declaration line while (*p && *p != '\n') { p++; } if (*p == '\n') { p++; } bool bodyEmpty = true; while (*p) { const char *line = p; while (*line == ' ' || *line == '\t') { line++; } if (strncasecmp(line, endTag, endTagLen) == 0) { break; } while (*p && *p != '\n') { if (*p != ' ' && *p != '\t' && *p != '\r') { bodyEmpty = false; } p++; } if (*p == '\n') { p++; } } if (bodyEmpty) { free(sProcBufs[sCurProcIdx]); arrdel(sProcBufs, sCurProcIdx); sCurProcIdx = -2; return true; } else { free(sProcBufs[sCurProcIdx]); sProcBufs[sCurProcIdx] = strdup(edText); return false; } } else { // Multiple proc blocks in the editor. Find the one matching // ownName, keep it in this buffer, extract the rest. const char *pos = edText; const char *ownStart = NULL; const char *ownEnd = NULL; char *extras = (char *)malloc(strlen(edText) + 1); int32_t extPos = 0; if (!extras) { free(sProcBufs[sCurProcIdx]); sProcBufs[sCurProcIdx] = strdup(edText); } else { while (*pos) { const char *lineStart = pos; const char *tl = pos; while (*tl == ' ' || *tl == '\t') { tl++; } bool isSub = (strncasecmp(tl, "SUB ", 4) == 0); bool isFunc = (strncasecmp(tl, "FUNCTION ", 9) == 0); if (isSub || isFunc) { // Extract the proc name const char *np = tl + (isSub ? 4 : 9); while (*np == ' ' || *np == '\t') { np++; } char name[128]; int32_t nn = 0; while (*np && *np != '(' && *np != ' ' && *np != '\t' && *np != '\n' && nn < 127) { name[nn++] = *np++; } name[nn] = '\0'; // Find the matching End tag const char *endTag = isSub ? "END SUB" : "END FUNCTION"; int32_t endTagLen = isSub ? 7 : 12; const char *s = pos; while (*s && *s != '\n') { s++; } if (*s == '\n') { s++; } while (*s) { const char *sl = s; while (*sl == ' ' || *sl == '\t') { sl++; } if (strncasecmp(sl, endTag, endTagLen) == 0) { while (*s && *s != '\n') { s++; } if (*s == '\n') { s++; } break; } while (*s && *s != '\n') { s++; } if (*s == '\n') { s++; } } if (strcasecmp(name, ownName) == 0) { ownStart = lineStart; ownEnd = s; } else { // Copy this block to extras int32_t blen = (int32_t)(s - lineStart); memcpy(extras + extPos, lineStart, blen); extPos += blen; } pos = s; continue; } // Non-proc line (blank lines between blocks etc.) -- skip while (*pos && *pos != '\n') { pos++; } if (*pos == '\n') { pos++; } } extras[extPos] = '\0'; // Update this buffer with just the owned proc if (ownStart && ownEnd) { int32_t keepLen = (int32_t)(ownEnd - ownStart); char *kept = (char *)malloc(keepLen + 1); if (kept) { memcpy(kept, ownStart, keepLen); kept[keepLen] = '\0'; free(sProcBufs[sCurProcIdx]); sProcBufs[sCurProcIdx] = kept; } } // Extract extra procs into their own buffers if (extPos > 0) { extractNewProcs(extras); } free(extras); // Update editor to show only this proc wgtSetText(sEditor, sProcBufs[sCurProcIdx]); return true; } } } return false; } // 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 a buffer was deleted (empty skeleton discard), adjust the // target index since arrdel shifts everything after it. if (sCurProcIdx >= -1) { int32_t deletedIdx = sCurProcIdx; bool changed = saveCurProc(); if (changed && deletedIdx >= 0 && procIdx > deletedIdx) { procIdx--; } } // Suppress onChange while loading -- setting text is not a user edit void (*savedOnChange)(WidgetT *) = sEditor->onChange; sEditor->onChange = NULL; 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; } sEditorLineCount = countLines(wgtGetText(sEditor)); sEditor->onChange = savedOnChange; } // 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 += 2; // newline + blank line between procedures } } 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'; } // Blank line between procedures if (i < procCount - 1) { 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; // Match proc name against known objects: form name, // controls, menu items. Try each as a prefix followed // by "_". This handles names with multiple underscores // correctly (e.g., "cmdOK_Click" matches "cmdOK", not // "This_Is_A_Dumb_Name" matching a control named "This"). bool isEvent = false; if (sDesigner.form) { // Collect all known object names const char *objNames[512]; int32_t objNameCount = 0; objNames[objNameCount++] = sDesigner.form->name; for (int32_t ci = 0; ci < (int32_t)arrlen(sDesigner.form->controls) && objNameCount < 511; ci++) { objNames[objNameCount++] = sDesigner.form->controls[ci]->name; } for (int32_t mi = 0; mi < (int32_t)arrlen(sDesigner.form->menuItems) && objNameCount < 511; mi++) { objNames[objNameCount++] = sDesigner.form->menuItems[mi].name; } // Try each object name as prefix + "_" for (int32_t oi = 0; oi < objNameCount; oi++) { int32_t nameLen = (int32_t)strlen(objNames[oi]); if (nameLen > 0 && strncasecmp(procName, objNames[oi], nameLen) == 0 && procName[nameLen] == '_') { snprintf(entry.objName, sizeof(entry.objName), "%s", objNames[oi]); snprintf(entry.evtName, sizeof(entry.evtName), "%s", procName + nameLen + 1); isEvent = true; break; } } } if (!isEvent) { 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 menu item names (non-separator, non-top-level-header-only) for (int32_t i = 0; i < (int32_t)arrlen(sDesigner.form->menuItems); i++) { DsgnMenuItemT *mi = &sDesigner.form->menuItems[i]; if (mi->name[0] && mi->caption[0] != '-') { arrput(sObjItems, mi->name); } } } // Sort object items alphabetically, keeping (General) first int32_t objCount = (int32_t)arrlen(sObjItems); if (objCount > 1) { qsort(sObjItems + 1, objCount - 1, sizeof(const char *), cmpStrPtrs); } wgtDropdownSetItems(sObjDropdown, sObjItems, objCount); if (objCount > 0) { wgtDropdownSetSelected(sObjDropdown, 0); onObjDropdownChange(sObjDropdown); } else { wgtDropdownSetItems(sEvtDropdown, NULL, 0); } }