DVX_GUI/apps/dvxbasic/ide/ideMain.c

9598 lines
287 KiB
C

// 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 <dlfcn.h>
#include <stdint.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
// ============================================================
// 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.<lowercase(basName)>.
// 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, "<error>");
}
} 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);
}
}