DVX_GUI/apps/dvxbasic/ide/ideMain.c

4599 lines
133 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 "dvxCursor.h"
#include "dvxPlatform.h"
#include "dvxDialog.h"
#include "dvxPrefs.h"
#include "dvxWidget.h"
#include "dvxWidgetPlugin.h"
#include "dvxWm.h"
#include "shellApp.h"
#include "widgetBox.h"
#include "widgetImageButton.h"
#include "widgetLabel.h"
#include "widgetTextInput.h"
#include "widgetDropdown.h"
#include "widgetButton.h"
#include "widgetSplitter.h"
#include "widgetStatusBar.h"
#include "widgetToolbar.h"
#include "ideDesigner.h"
#include "ideProject.h"
#include "ideToolbox.h"
#include "ideProperties.h"
#include "../compiler/parser.h"
#include "../formrt/formrt.h"
#include "../runtime/vm.h"
#include "../runtime/values.h"
#include "stb_ds_wrap.h"
#include <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 IDE_MAX_IMM 1024
#define IDE_DESIGN_W 400
#define IDE_DESIGN_H 300
// ============================================================
// Prototypes
// ============================================================
int32_t appMain(DxeAppContextT *ctx);
static void buildWindow(void);
static void clearOutput(void);
static int cmpStrPtrs(const void *a, const void *b);
static void compileAndRun(void);
static void ensureProject(const char *filePath);
static void freeProcBufs(void);
static const char *getFullSource(void);
static void loadFile(void);
static void parseProcs(const char *source);
static void updateProjectMenuState(void);
static void saveActiveFile(void);
static bool saveCurProc(void);
static void stashFormCode(void);
static void showProc(int32_t procIdx);
static int32_t toolbarBottom(void);
static void loadFilePath(const char *path);
static void newProject(void);
static void onPrjFileClick(int32_t fileIdx, bool isForm);
static void openProject(void);
static void closeProject(void);
static void saveFile(void);
static void onTbSave(WidgetT *w);
static bool hasUnsavedData(void);
static bool promptAndSave(void);
static void cleanupFormWin(void);
static void onClose(WindowT *win);
static void onCodeWinClose(WindowT *win);
static void onContentFocus(WindowT *win);
static void onFormWinClose(WindowT *win);
static void onFormWinResize(WindowT *win, int32_t newW, int32_t newH);
static void onProjectWinClose(WindowT *win);
static WindowT *getLastFocusWin(void);
static void onMenu(WindowT *win, int32_t menuId);
static void basicColorize(const char *line, int32_t lineLen, uint8_t *colors, void *ctx);
static void evaluateImmediate(const char *expr);
static void loadFrmFiles(BasFormRtT *rt);
static void onEvtDropdownChange(WidgetT *w);
static void onImmediateChange(WidgetT *w);
static void onObjDropdownChange(WidgetT *w);
static void printCallback(void *ctx, const char *text, bool newline);
static bool inputCallback(void *ctx, const char *prompt, char *buf, int32_t bufSize);
static bool doEventsCallback(void *ctx);
static void runCached(void);
static void runModule(BasModuleT *mod);
static void onEditorChange(WidgetT *w);
static void setStatus(const char *text);
static void switchToCode(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 selectDropdowns(const char *objName, const char *evtName);
static void updateDropdowns(void);
// ============================================================
// Module state
// ============================================================
static DxeAppContextT *sCtx = NULL;
static AppContextT *sAc = NULL;
static PrefsHandleT *sPrefs = NULL;
static WindowT *sWin = NULL; // Main toolbar window
static WindowT *sCodeWin = NULL; // Code editor window
static WindowT *sOutWin = NULL; // Output window
static WindowT *sImmWin = NULL; // Immediate window
static WidgetT *sEditor = NULL;
static WidgetT *sOutput = NULL;
static WidgetT *sImmediate = NULL;
static WidgetT *sObjDropdown = NULL;
static WidgetT *sEvtDropdown = NULL;
static WidgetT *sToolbar = NULL;
static WidgetT *sStatusBar = NULL;
static WidgetT *sStatus = NULL;
static BasVmT *sVm = NULL; // VM instance (non-NULL while running)
static BasModuleT *sCachedModule = NULL; // Last compiled module (for Ctrl+F5)
static DsgnStateT sDesigner;
static WindowT *sFormWin = NULL; // Form designer window (separate)
static WindowT *sToolboxWin = NULL;
static WindowT *sPropsWin = NULL;
static WindowT *sProjectWin = NULL;
static PrjStateT sProject;
static WindowT *sLastFocusWin = NULL; // last focused non-toolbar window
static char 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)
// 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;
// ============================================================
// App descriptor
// ============================================================
AppDescriptorT appDescriptor = {
.name = "DVX BASIC",
.hasMainLoop = false,
.multiInstance = false,
.stackSize = SHELL_STACK_DEFAULT,
.priority = 0
};
// ============================================================
// appMain
// ============================================================
int32_t appMain(DxeAppContextT *ctx) {
sCtx = ctx;
sAc = ctx->shellCtx;
basStringSystemInit();
prjInit(&sProject);
buildWindow();
// Load persisted settings
char prefsPath[DVX_MAX_PATH];
shellConfigPath(sCtx, "dvxbasic.ini", prefsPath, sizeof(prefsPath));
sPrefs = prefsLoad(prefsPath);
if (sToolbar && sWin && sWin->menuBar) {
bool showTb = prefsGetBool(sPrefs, "view", "toolbar", true);
sToolbar->visible = showTb;
wmMenuItemSetChecked(sWin->menuBar, CMD_VIEW_TOOLBAR, showTb);
}
if (sStatusBar && sWin && sWin->menuBar) {
bool showSb = prefsGetBool(sPrefs, "view", "statusbar", true);
sStatusBar->visible = showSb;
wmMenuItemSetChecked(sWin->menuBar, CMD_VIEW_STATUS, showSb);
}
if (sWin && sWin->menuBar) {
bool saveOnRun = prefsGetBool(sPrefs, "run", "saveOnRun", true);
wmMenuItemSetChecked(sWin->menuBar, CMD_SAVE_ON_RUN, saveOnRun);
}
if (sWin) {
dvxFitWindowH(sAc, sWin);
}
sOutputBuf[0] = '\0';
sOutputLen = 0;
// Auto-load project for development/testing
if (prjLoad(&sProject, "C:\\BIN\\APPS\\DVXBASIC\\MULTI.DBP")) {
prjLoadAllFiles(&sProject, sAc);
sProjectWin = prjCreateWindow(sAc, &sProject, onPrjFileClick);
if (sProjectWin) {
sProjectWin->y = toolbarBottom() + 25;
sProjectWin->onClose = onProjectWinClose;
}
char title[300];
snprintf(title, sizeof(title), "DVX BASIC - [%s]", sProject.name);
dvxSetTitle(sAc, sWin, title);
}
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, false);
if (!sWin) {
return;
}
sWin->onClose = onClose;
sWin->onMenu = onMenu;
// Menu bar
MenuBarT *menuBar = wmAddMenuBar(sWin);
MenuT *fileMenu = wmAddMenu(menuBar, "&File");
wmAddMenuItem(fileMenu, "New &Project...", CMD_PRJ_NEW);
wmAddMenuItem(fileMenu, "Open Pro&ject...", CMD_PRJ_OPEN);
wmAddMenuItem(fileMenu, "Save Projec&t", CMD_PRJ_SAVE);
wmAddMenuItem(fileMenu, "Close Projec&t", CMD_PRJ_CLOSE);
wmAddMenuSeparator(fileMenu);
wmAddMenuItem(fileMenu, "Project &Properties...", 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, "&Remove File", CMD_PRJ_REMOVE);
wmAddMenuSeparator(fileMenu);
wmAddMenuItem(fileMenu, "E&xit", CMD_EXIT);
MenuT *editMenu = wmAddMenu(menuBar, "&Edit");
wmAddMenuItem(editMenu, "Cu&t\tCtrl+X", CMD_CUT);
wmAddMenuItem(editMenu, "&Copy\tCtrl+C", CMD_COPY);
wmAddMenuItem(editMenu, "&Paste\tCtrl+V", CMD_PASTE);
wmAddMenuSeparator(editMenu);
wmAddMenuItem(editMenu, "Select &All\tCtrl+A", CMD_SELECT_ALL);
wmAddMenuSeparator(editMenu);
wmAddMenuItem(editMenu, "&Delete\tDel", CMD_DELETE);
MenuT *runMenu = wmAddMenu(menuBar, "&Run");
wmAddMenuItem(runMenu, "&Run\tF5", CMD_RUN);
wmAddMenuItem(runMenu, "Run &Without Recompile\tCtrl+F5", CMD_RUN_NOCMP);
wmAddMenuItem(runMenu, "&Stop\tEsc", CMD_STOP);
wmAddMenuSeparator(runMenu);
wmAddMenuItem(runMenu, "&Clear Output", CMD_CLEAR);
wmAddMenuSeparator(runMenu);
wmAddMenuCheckItem(runMenu, "Save on &Run", CMD_SAVE_ON_RUN, true);
MenuT *viewMenu = wmAddMenu(menuBar, "&View");
wmAddMenuItem(viewMenu, "&Code\tF7", CMD_VIEW_CODE);
wmAddMenuItem(viewMenu, "&Object\tShift+F7", CMD_VIEW_DESIGN);
wmAddMenuSeparator(viewMenu);
wmAddMenuCheckItem(viewMenu, "&Toolbar", CMD_VIEW_TOOLBAR, true);
wmAddMenuCheckItem(viewMenu, "&Status Bar", CMD_VIEW_STATUS, true);
MenuT *winMenu = wmAddMenu(menuBar, "&Window");
wmAddMenuItem(winMenu, "&Code Editor", CMD_WIN_CODE);
wmAddMenuItem(winMenu, "&Output", CMD_WIN_OUTPUT);
wmAddMenuItem(winMenu, "&Immediate", CMD_WIN_IMM);
wmAddMenuSeparator(winMenu);
wmAddMenuItem(winMenu, "&Project Explorer", CMD_WIN_PROJECT);
wmAddMenuItem(winMenu, "&Toolbox", CMD_WIN_TOOLBOX);
wmAddMenuItem(winMenu, "&Properties", CMD_WIN_PROPS);
MenuT *helpMenu = wmAddMenu(menuBar, "&Help");
wmAddMenuItem(helpMenu, "&About 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_CTRL, CMD_RUN_NOCMP);
dvxAddAccel(accel, KEY_F7, 0, CMD_VIEW_CODE);
dvxAddAccel(accel, KEY_F7, ACCEL_SHIFT, CMD_VIEW_DESIGN);
dvxAddAccel(accel, 0x1B, 0, CMD_STOP);
sWin->accelTable = accel;
WidgetT *tbRoot = wgtInitWindow(sAc, sWin);
sToolbar = wgtToolbar(tbRoot);
WidgetT *tb = sToolbar;
WidgetT *tbOpen = loadTbIcon(tb, "tb_open", "Open");
tbOpen->onClick = onTbOpen;
wgtSetTooltip(tbOpen, "Open (Ctrl+O)");
WidgetT *tbSave = loadTbIcon(tb, "tb_save", "Save");
tbSave->onClick = onTbSave;
wgtSetTooltip(tbSave, "Save (Ctrl+S)");
WidgetT *tbRun = loadTbIcon(tb, "tb_run", "Run");
tbRun->onClick = onTbRun;
wgtSetTooltip(tbRun, "Run (F5)");
WidgetT *tbStop = loadTbIcon(tb, "tb_stop", "Stop");
tbStop->onClick = onTbStop;
wgtSetTooltip(tbStop, "Stop (Esc)");
WidgetT *tbCode = loadTbIcon(tb, "tb_code", "Code");
tbCode->onClick = onTbCode;
wgtSetTooltip(tbCode, "Code View (F7)");
WidgetT *tbDesign = loadTbIcon(tb, "tb_design", "Design");
tbDesign->onClick = onTbDesign;
wgtSetTooltip(tbDesign, "Design View (Shift+F7)");
sStatusBar = wgtStatusBar(tbRoot);
WidgetT *statusBar = sStatusBar;
sStatus = wgtLabel(statusBar, "");
sStatus->weight = 100;
// Fit height to content, keeping full screen width
dvxFitWindowH(sAc, sWin);
// Initialize designer (form window created on demand)
dsgnInit(&sDesigner, sAc);
showOutputWindow();
showImmediateWindow();
}
// ============================================================
// basicColorize
// ============================================================
//
// Syntax colorizer callback for BASIC source code. Scans a single
// line and fills the colors array with syntax color indices.
// Hash-based keyword/type lookup using stb_ds.
// Key = uppercase word, value = syntax color (1=keyword, 6=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], 1);
}
for (int32_t i = 0; types[i]; i++) {
shput(sSyntaxMap, types[i], 6);
}
}
// 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 0;
}
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 0;
}
static void basicColorize(const char *line, int32_t lineLen, uint8_t *colors, void *ctx) {
(void)ctx;
int32_t i = 0;
while (i < lineLen) {
char ch = line[i];
// Comment: ' or REM
if (ch == '\'') {
while (i < lineLen) {
colors[i++] = 3; // SYNTAX_COMMENT
}
return;
}
// String literal
if (ch == '"') {
colors[i++] = 2; // SYNTAX_STRING
while (i < lineLen && line[i] != '"') {
colors[i++] = 2;
}
if (i < lineLen) {
colors[i++] = 2; // closing quote
}
continue;
}
// Number
if (isdigit((unsigned char)ch) || (ch == '.' && i + 1 < lineLen && isdigit((unsigned char)line[i + 1]))) {
while (i < lineLen && (isdigit((unsigned char)line[i]) || line[i] == '.')) {
colors[i++] = 4; // SYNTAX_NUMBER
}
continue;
}
// Identifier or keyword
if (isalpha((unsigned char)ch) || ch == '_') {
int32_t start = i;
while (i < lineLen && (isalnum((unsigned char)line[i]) || line[i] == '_' || line[i] == '$' || line[i] == '%' || line[i] == '&' || line[i] == '!' || line[i] == '#')) {
i++;
}
int32_t wordLen = i - start;
// Check for REM comment
if (wordLen == 3 && (line[start] == 'R' || line[start] == 'r') && (line[start + 1] == 'E' || line[start + 1] == 'e') && (line[start + 2] == 'M' || line[start + 2] == 'm')) {
for (int32_t j = start; j < lineLen; j++) {
colors[j] = 3; // SYNTAX_COMMENT
}
return;
}
uint8_t c = 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 (sOutput) {
wgtSetText(sOutput, text);
}
}
static void clearOutput(void) {
sOutputBuf[0] = '\0';
sOutputLen = 0;
setOutputText("");
}
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);
}
// ============================================================
// compileAndRun
// ============================================================
static void compileAndRun(void) {
// Save all dirty files before compiling if Save on Run is enabled
if (sWin && sWin->menuBar && wmMenuItemIsChecked(sWin->menuBar, CMD_SAVE_ON_RUN)) {
if (sProject.activeFileIdx >= 0) {
saveActiveFile();
}
for (int32_t i = 0; i < sProject.fileCount; i++) {
if (i == sProject.activeFileIdx) {
continue;
}
if (sProject.files[i].modified && sProject.files[i].buffer) {
char fullPath[DVX_MAX_PATH];
prjFullPath(&sProject, i, fullPath, sizeof(fullPath));
FILE *f = fopen(fullPath, "w");
if (f) {
fputs(sProject.files[i].buffer, f);
fclose(f);
sProject.files[i].modified = false;
}
}
}
updateDirtyIndicators();
}
clearOutput();
setStatus("Compiling...");
// Force a display update so the status is visible
dvxInvalidateWindow(sAc, sWin);
// Build source: either concatenate project files or use editor contents
char *concatBuf = NULL;
const char *src = NULL;
int32_t srcLen = 0;
if (sProject.projectPath[0] != '\0' && sProject.fileCount > 0) {
// Stash current editor state to the file that owns the proc buffers
if (sEditorFileIdx >= 0 && sEditorFileIdx < sProject.fileCount) {
PrjFileT *edFile = &sProject.files[sEditorFileIdx];
if (!edFile->isForm) {
saveCurProc();
const char *fullSrc = getFullSource();
free(edFile->buffer);
edFile->buffer = fullSrc ? strdup(fullSrc) : NULL;
}
}
// Stash form code if the editor has form code loaded
stashFormCode();
// Concatenate all .bas files from buffers (or disk if not yet loaded)
concatBuf = (char *)malloc(IDE_MAX_SOURCE);
if (!concatBuf) {
setStatus("Out of memory.");
return;
}
int32_t pos = 0;
int32_t line = 1;
arrfree(sProject.sourceMap);
sProject.sourceMap = NULL;
sProject.sourceMapCount = 0;
// 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) {
// Extract code from the stashed .frm text (after "End\n")
const char *buf = sProject.files[i].buffer;
const char *endTag = strstr(buf, "\nEnd\n");
if (!endTag) {
endTag = strstr(buf, "\nEnd\r\n");
}
if (endTag) {
endTag += 5;
while (*endTag == '\r' || *endTag == '\n') {
endTag++;
}
if (*endTag) {
fileSrc = endTag;
}
}
}
// If no code found from memory, fall through to disk read
} else {
fileSrc = sProject.files[i].buffer;
}
if (!fileSrc) {
// Not yet loaded into memory -- read from disk
char fullPath[DVX_MAX_PATH];
prjFullPath(&sProject, i, fullPath, sizeof(fullPath));
FILE *f = fopen(fullPath, "r");
if (!f) {
continue;
}
fseek(f, 0, SEEK_END);
long size = ftell(f);
fseek(f, 0, SEEK_SET);
if (size > 0 && size < IDE_MAX_SOURCE) {
diskBuf = (char *)malloc(size + 1);
if (diskBuf) {
int32_t br = (int32_t)fread(diskBuf, 1, size, f);
diskBuf[br] = '\0';
fileSrc = diskBuf;
// For .frm from disk, extract code section
if (sProject.files[i].isForm) {
const char *endTag = strstr(fileSrc, "\nEnd\n");
if (!endTag) {
endTag = strstr(fileSrc, "\nEnd\r\n");
}
if (endTag) {
endTag += 5;
while (*endTag == '\r' || *endTag == '\n') {
endTag++;
}
fileSrc = endTag;
} else {
fileSrc = NULL;
}
}
}
}
fclose(f);
}
if (!fileSrc) {
continue;
}
int32_t startLine = line;
int32_t fileLen = (int32_t)strlen(fileSrc);
int32_t copyLen = fileLen;
if (pos + copyLen >= IDE_MAX_SOURCE - 1) {
copyLen = IDE_MAX_SOURCE - 1 - pos;
}
memcpy(concatBuf + pos, fileSrc, copyLen);
pos += copyLen;
// Count lines
for (int32_t j = 0; j < copyLen; j++) {
if (fileSrc[j] == '\n') {
line++;
}
}
free(diskBuf);
// Ensure a trailing newline between files
if (copyLen > 0 && concatBuf[pos - 1] != '\n' && pos < IDE_MAX_SOURCE - 1) {
concatBuf[pos++] = '\n';
line++;
}
{
PrjSourceMapT mapEntry;
mapEntry.fileIdx = i;
mapEntry.startLine = startLine;
mapEntry.lineCount = line - startLine;
arrput(sProject.sourceMap, mapEntry);
sProject.sourceMapCount = (int32_t)arrlen(sProject.sourceMap);
}
}
concatBuf[pos] = '\0';
src = concatBuf;
srcLen = pos;
} else {
// No project files -- compile the full source
src = getFullSource();
if (!src || *src == '\0') {
setStatus("No source code to run.");
return;
}
srcLen = (int32_t)strlen(src);
}
// Compile (heap-allocated -- BasParserT is ~300KB, too large for stack)
BasParserT *parser = (BasParserT *)malloc(sizeof(BasParserT));
if (!parser) {
free(concatBuf);
setStatus("Out of memory.");
return;
}
basParserInit(parser, src, srcLen);
if (!basParse(parser)) {
int32_t n = snprintf(sOutputBuf, IDE_MAX_OUTPUT, "COMPILE ERROR:\n%s\n", parser->error);
sOutputLen = n;
setOutputText(sOutputBuf);
// Jump to error line -- translate through source map if project
if (parser->errorLine > 0 && sEditor) {
int32_t fileIdx = -1;
int32_t localLine = parser->errorLine;
if (sProject.fileCount > 0 && prjMapLine(&sProject, parser->errorLine, &fileIdx, &localLine)) {
// Open the offending file if it's not already active
if (fileIdx != sProject.activeFileIdx) {
onPrjFileClick(fileIdx, false);
}
}
wgtTextAreaGoToLine(sEditor, localLine);
}
setStatus("Compilation failed.");
basParserFree(parser);
free(parser);
free(concatBuf);
return;
}
free(concatBuf);
BasModuleT *mod = basParserBuildModule(parser);
basParserFree(parser);
free(parser);
if (!mod) {
setStatus("Failed to build module.");
return;
}
// Cache the compiled module for Ctrl+F5
if (sCachedModule) {
basModuleFree(sCachedModule);
}
sCachedModule = mod;
// Update Object/Event dropdowns
updateDropdowns();
runModule(mod);
}
// ============================================================
// runCached
// ============================================================
static void runCached(void) {
if (!sCachedModule) {
setStatus("No compiled program. Press F5 to compile first.");
return;
}
clearOutput();
runModule(sCachedModule);
}
// ============================================================
// runModule
// ============================================================
static void runModule(BasModuleT *mod) {
setStatus("Running...");
// Hide IDE windows while the program runs
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) { sFormWin->visible = false; dvxInvalidateWindow(sAc, sFormWin); }
if (sToolboxWin) { sToolboxWin->visible = false; dvxInvalidateWindow(sAc, sToolboxWin); }
if (sPropsWin) { sPropsWin->visible = false; dvxInvalidateWindow(sAc, sPropsWin); }
if (sCodeWin) { sCodeWin->visible = false; dvxInvalidateWindow(sAc, sCodeWin); }
if (sProjectWin) { sProjectWin->visible = false; dvxInvalidateWindow(sAc, sProjectWin); }
// Create VM
BasVmT *vm = basVmCreate();
basVmLoadModule(vm, mod);
// Set up implicit main frame
vm->callStack[0].localCount = mod->globalCount > BAS_VM_MAX_LOCALS ? BAS_VM_MAX_LOCALS : mod->globalCount;
vm->callDepth = 1;
// Set I/O callbacks
basVmSetPrintCallback(vm, printCallback, NULL);
basVmSetInputCallback(vm, inputCallback, NULL);
basVmSetDoEventsCallback(vm, doEventsCallback, NULL);
// Create form runtime (bridges UI opcodes to DVX widgets)
BasFormRtT *formRt = basFormRtCreate(sAc, vm, mod);
// Load any .frm files from the same directory as the source
loadFrmFiles(formRt);
// Auto-show the startup form (or first form if none specified).
// Other forms remain hidden until code calls Show.
if (formRt->formCount > 0) {
BasFormT *startupForm = &formRt->forms[0];
if (sProject.startupForm[0]) {
for (int32_t i = 0; i < formRt->formCount; i++) {
if (strcasecmp(formRt->forms[i].name, sProject.startupForm) == 0) {
startupForm = &formRt->forms[i];
break;
}
}
}
basFormRtShowForm(formRt, startupForm, false);
}
sVm = vm;
// Run in slices of 10000 steps, yielding to DVX between slices
basVmSetStepLimit(vm, IDE_STEP_SLICE);
int32_t totalSteps = 0;
BasVmResultE result;
sStopRequested = false;
for (;;) {
result = basVmRun(vm);
totalSteps += vm->stepCount;
if (result == BAS_VM_STEP_LIMIT) {
// Yield to DVX to keep the GUI responsive
dvxUpdate(sAc);
// Stop if IDE window was closed, DVX is shutting down, or user hit Stop
if (!sWin || !sAc->running || sStopRequested) {
break;
}
continue;
}
if (result == BAS_VM_HALTED) {
break;
}
// Runtime error
int32_t pos = sOutputLen;
int32_t n = snprintf(sOutputBuf + pos, IDE_MAX_OUTPUT - pos, "\n[Runtime error: %s]\n", basVmGetError(vm));
sOutputLen += n;
setOutputText(sOutputBuf);
break;
}
// 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 && formRt->formCount > 0) {
setStatus("Running (event loop)...");
sStopRequested = false;
while (sWin && sAc->running && formRt->formCount > 0 && !sStopRequested && !vm->ended) {
dvxUpdate(sAc);
}
}
sVm = NULL;
// Update output display
setOutputText(sOutputBuf);
static char statusBuf[128];
snprintf(statusBuf, sizeof(statusBuf), "Done. %ld instructions executed.", (long)totalSteps);
setStatus(statusBuf);
basFormRtDestroy(formRt);
basVmDestroy(vm);
// Restore IDE windows
if (hadFormWin && sFormWin) { sFormWin->visible = true; dvxInvalidateWindow(sAc, sFormWin); }
if (hadToolbox && sToolboxWin) { sToolboxWin->visible = true; dvxInvalidateWindow(sAc, sToolboxWin); }
if (hadProps && sPropsWin) { sPropsWin->visible = true; dvxInvalidateWindow(sAc, sPropsWin); }
if (hadCodeWin && sCodeWin) { sCodeWin->visible = true; dvxInvalidateWindow(sAc, sCodeWin); }
if (hadPrjWin && sProjectWin) { sProjectWin->visible = true; dvxInvalidateWindow(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);
}
}
static void evaluateImmediate(const char *expr) {
if (!expr || *expr == '\0') {
return;
}
char wrapped[1024];
// If it already starts with a statement keyword, use as-is
if (strncasecmp(expr, "PRINT", 5) == 0 || strncasecmp(expr, "DIM", 3) == 0 || strncasecmp(expr, "LET", 3) == 0) {
snprintf(wrapped, sizeof(wrapped), "%s", expr);
} else {
snprintf(wrapped, sizeof(wrapped), "PRINT %s", expr);
}
BasParserT *parser = (BasParserT *)malloc(sizeof(BasParserT));
if (!parser) {
return;
}
basParserInit(parser, wrapped, (int32_t)strlen(wrapped));
if (!basParse(parser)) {
// Show error inline
immPrintCallback(NULL, "Error: ", false);
immPrintCallback(NULL, parser->error, true);
basParserFree(parser);
free(parser);
return;
}
BasModuleT *mod = basParserBuildModule(parser);
basParserFree(parser);
free(parser);
if (!mod) {
return;
}
BasVmT *vm = basVmCreate();
basVmLoadModule(vm, mod);
vm->callStack[0].localCount = mod->globalCount > BAS_VM_MAX_LOCALS ? BAS_VM_MAX_LOCALS : mod->globalCount;
vm->callDepth = 1;
basVmSetPrintCallback(vm, immPrintCallback, NULL);
BasVmResultE result = basVmRun(vm);
if (result != BAS_VM_HALTED && result != BAS_VM_OK) {
immPrintCallback(NULL, "Error: ", false);
immPrintCallback(NULL, basVmGetError(vm), true);
}
basVmDestroy(vm);
basModuleFree(mod);
}
// ============================================================
// inputCallback
// ============================================================
static bool inputCallback(void *ctx, const char *prompt, char *buf, int32_t bufSize) {
(void)ctx;
// Append prompt to output
if (prompt && sOutputLen < IDE_MAX_OUTPUT - 1) {
int32_t n = snprintf(sOutputBuf + sOutputLen, IDE_MAX_OUTPUT - sOutputLen, "%s", prompt);
sOutputLen += n;
setOutputText(sOutputBuf);
}
return dvxInputBox(sAc, "DVX BASIC", prompt ? prompt : "Enter value:", NULL, buf, bufSize);
}
// ============================================================
// loadFile
// ============================================================
static void loadFilePath(const char *path) {
FILE *f = fopen(path, "r");
if (!f) {
return;
}
fseek(f, 0, SEEK_END);
long size = ftell(f);
fseek(f, 0, SEEK_SET);
if (size >= IDE_MAX_SOURCE - 1) {
fclose(f);
return;
}
char *srcBuf = (char *)malloc(size + 1);
if (!srcBuf) {
fclose(f);
return;
}
int32_t bytesRead = (int32_t)fread(srcBuf, 1, size, f);
fclose(f);
srcBuf[bytesRead] = '\0';
if (!sCodeWin) {
showCodeWindow();
}
// Stash form code before overwriting proc buffers
stashFormCode();
// Parse into per-procedure buffers and show (General) section
parseProcs(srcBuf);
free(srcBuf);
if (sFormWin) {
dvxDestroyWindow(sAc, sFormWin);
cleanupFormWin();
}
dsgnFree(&sDesigner);
updateDropdowns();
showProc(-1); // show (General) section
if (sEditor && !sEditor->onChange) {
sEditor->onChange = onEditorChange;
}
setStatus("File loaded.");
}
// Auto-create an implicit project when opening a file without one.
// Derives project name and directory from the file path. Also adds
// a matching .frm file if one exists alongside the .bas.
static void ensureProject(const char *filePath) {
if (sProject.projectPath[0] != '\0') {
return;
}
// Derive directory and base name from path
char dir[DVX_MAX_PATH];
char baseName[PRJ_MAX_NAME];
snprintf(dir, sizeof(dir), "%s", filePath);
char *sep = strrchr(dir, '/');
char *sep2 = strrchr(dir, '\\');
if (sep2 > sep) {
sep = sep2;
}
const char *fileName = filePath;
if (sep) {
fileName = sep + 1;
*sep = '\0';
} else {
dir[0] = '.';
dir[1] = '\0';
}
// Strip extension for project name
// Length-clamped memcpy instead of strncpy/snprintf because
// GCC warns about both when source (DVX_MAX_PATH) exceeds
// the buffer (PRJ_MAX_NAME), even though truncation is safe.
int32_t nl = (int32_t)strlen(fileName);
if (nl >= PRJ_MAX_NAME) {
nl = PRJ_MAX_NAME - 1;
}
memcpy(baseName, fileName, nl);
baseName[nl] = '\0';
char *dot = strrchr(baseName, '.');
if (dot) {
*dot = '\0';
}
prjNew(&sProject, baseName, dir);
// Determine if this is a .bas or .frm
const char *ext = strrchr(filePath, '.');
bool isForm = (ext && strcasecmp(ext, ".frm") == 0);
prjAddFile(&sProject, fileName, isForm);
// If it's a .bas, check for a matching .frm and add it too
if (!isForm) {
char frmPath[DVX_MAX_PATH];
snprintf(frmPath, sizeof(frmPath), "%s", filePath);
char *frmDot = strrchr(frmPath, '.');
if (frmDot && (frmDot - frmPath) + 4 < DVX_MAX_PATH) {
snprintf(frmDot, sizeof(frmPath) - (frmDot - frmPath), ".frm");
} else if ((int32_t)strlen(frmPath) + 4 < DVX_MAX_PATH) {
snprintf(frmPath + strlen(frmPath), sizeof(frmPath) - strlen(frmPath), ".frm");
}
FILE *frmFile = fopen(frmPath, "r");
if (frmFile) {
fclose(frmFile);
// Get just the filename portion
const char *frmName = strrchr(frmPath, '/');
const char *frmName2 = strrchr(frmPath, '\\');
if (frmName2 > frmName) {
frmName = frmName2;
}
frmName = frmName ? frmName + 1 : frmPath;
prjAddFile(&sProject, frmName, true);
}
}
sProject.dirty = false;
sProject.activeFileIdx = 0;
prjLoadAllFiles(&sProject, sAc);
char title[300];
snprintf(title, sizeof(title), "DVX BASIC - [%s]", sProject.name);
if (sWin) {
dvxSetTitle(sAc, sWin, title);
}
updateProjectMenuState();
}
static void loadFile(void) {
FileFilterT filters[] = {
{ "BASIC Files (*.bas)", "*.bas" },
{ "Form Files (*.frm)", "*.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);
int32_t fileIdx = sProject.fileCount - 1;
onPrjFileClick(fileIdx, isForm);
} else {
// No project -- create one from this file
if (!promptAndSave()) {
return;
}
ensureProject(path);
if (isForm) {
onPrjFileClick(0, true);
} else {
loadFilePath(path);
sEditorFileIdx = 0;
}
}
}
// ============================================================
// saveFile
// ============================================================
static void saveActiveFile(void) {
if (sProject.projectPath[0] == '\0') {
return;
}
int32_t idx = sProject.activeFileIdx;
if (idx < 0 || idx >= sProject.fileCount) {
return;
}
PrjFileT *file = &sProject.files[idx];
char fullPath[DVX_MAX_PATH];
prjFullPath(&sProject, idx, fullPath, sizeof(fullPath));
if (file->isForm) {
// Only serialize through the designer if it holds THIS form
bool isDesignerForm = (sDesigner.form &&
strcasecmp(sDesigner.form->name, file->formName) == 0);
if (isDesignerForm) {
stashFormCode();
char *frmBuf = (char *)malloc(IDE_MAX_SOURCE);
if (frmBuf) {
int32_t frmLen = dsgnSaveFrm(&sDesigner, frmBuf, IDE_MAX_SOURCE);
if (frmLen > 0) {
FILE *f = fopen(fullPath, "w");
if (f) {
fwrite(frmBuf, 1, frmLen, f);
fclose(f);
sDesigner.form->dirty = false;
file->modified = false;
}
}
free(frmBuf);
}
} else if (file->buffer) {
// Not the active designer form -- save from stashed buffer
FILE *f = fopen(fullPath, "w");
if (f) {
fputs(file->buffer, f);
fclose(f);
file->modified = false;
}
}
} else {
// Save .bas file -- use editor if it has this file, else use buffer
const char *src = NULL;
if (sEditorFileIdx == idx) {
saveCurProc();
src = getFullSource();
} else {
src = file->buffer;
}
if (src) {
FILE *f = fopen(fullPath, "w");
if (f) {
fputs(src, f);
fclose(f);
file->modified = false;
} else {
dvxMessageBox(sAc, "Error", "Could not write file.", MB_OK | MB_ICONERROR);
return;
}
}
}
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();
}
// ============================================================
// onPrjFileClick -- called when a file is clicked in the project tree
// ============================================================
static void onPrjFileClick(int32_t fileIdx, bool isForm) {
if (fileIdx < 0 || fileIdx >= sProject.fileCount) {
return;
}
if (fileIdx == sProject.activeFileIdx) {
// Already active -- but ensure the right view is shown
if (isForm) {
switchToDesign();
}
return;
}
// Stash current active file's contents into its buffer.
// This is just caching -- do not mark modified.
if (sProject.activeFileIdx >= 0) {
PrjFileT *cur = &sProject.files[sProject.activeFileIdx];
if (cur->isForm && sDesigner.form) {
// Save editor code back to form->code before serializing
stashFormCode();
// Serialize form designer state to .frm text
char *frmBuf = (char *)malloc(IDE_MAX_SOURCE);
if (frmBuf) {
int32_t frmLen = dsgnSaveFrm(&sDesigner, frmBuf, IDE_MAX_SOURCE);
free(cur->buffer);
if (frmLen > 0) {
frmBuf[frmLen] = '\0';
cur->buffer = frmBuf;
} else {
free(frmBuf);
cur->buffer = NULL;
}
}
} else if (!cur->isForm && sEditorFileIdx == sProject.activeFileIdx) {
// Stash full source (only if editor has this file's code)
saveCurProc();
const char *src = getFullSource();
free(cur->buffer);
cur->buffer = src ? strdup(src) : NULL;
}
}
PrjFileT *target = &sProject.files[fileIdx];
if (isForm) {
// Load form from buffer or disk, or create a new blank form
const char *frmSrc = target->buffer;
char *diskBuf = NULL;
if (!frmSrc) {
char fullPath[DVX_MAX_PATH];
prjFullPath(&sProject, fileIdx, fullPath, sizeof(fullPath));
FILE *f = fopen(fullPath, "r");
if (!f) {
// File doesn't exist yet -- create a new blank form.
// Derive form name from the filename (strip path and extension).
char formName[PRJ_MAX_NAME];
const char *base = strrchr(target->path, '/');
const char *base2 = strrchr(target->path, '\\');
if (base2 > base) {
base = base2;
}
base = base ? base + 1 : target->path;
// Length-clamped memcpy instead of strncpy/snprintf because
// GCC warns about both when source (DVX_MAX_PATH) exceeds
// the buffer (PRJ_MAX_NAME), even though truncation is safe.
int32_t bl = (int32_t)strlen(base);
if (bl >= PRJ_MAX_NAME) {
bl = PRJ_MAX_NAME - 1;
}
memcpy(formName, base, bl);
formName[bl] = '\0';
char *dot = strrchr(formName, '.');
if (dot) {
*dot = '\0';
}
if (sFormWin) {
dvxDestroyWindow(sAc, sFormWin);
cleanupFormWin();
}
if (sDesigner.form) {
dsgnFree(&sDesigner);
}
dsgnNewForm(&sDesigner, formName);
snprintf(target->formName, sizeof(target->formName), "%s", sDesigner.form->name);
target->modified = true;
sProject.activeFileIdx = fileIdx;
switchToDesign();
return;
}
fseek(f, 0, SEEK_END);
long size = ftell(f);
fseek(f, 0, SEEK_SET);
if (size <= 0 || size >= IDE_MAX_SOURCE) {
fclose(f);
return;
}
diskBuf = (char *)malloc(size + 1);
if (!diskBuf) {
fclose(f);
return;
}
int32_t bytesRead = (int32_t)fread(diskBuf, 1, size, f);
fclose(f);
diskBuf[bytesRead] = '\0';
frmSrc = diskBuf;
}
// Close the old form designer window before loading a new form
if (sFormWin) {
dvxDestroyWindow(sAc, sFormWin);
cleanupFormWin();
}
if (sDesigner.form) {
dsgnFree(&sDesigner);
}
dsgnLoadFrm(&sDesigner, frmSrc, (int32_t)strlen(frmSrc));
free(diskBuf);
if (sDesigner.form) {
snprintf(target->formName, sizeof(target->formName), "%s", sDesigner.form->name);
}
sProject.activeFileIdx = fileIdx;
switchToDesign();
} else {
// Load .bas file from buffer or disk
stashFormCode();
if (!sCodeWin) {
showCodeWindow();
}
if (target->buffer) {
parseProcs(target->buffer);
updateDropdowns();
showProc(-1);
} else {
char fullPath[DVX_MAX_PATH];
prjFullPath(&sProject, fileIdx, fullPath, sizeof(fullPath));
FILE *f = fopen(fullPath, "r");
if (f) {
fclose(f);
loadFilePath(fullPath);
} else {
// File doesn't exist yet -- start with empty source
parseProcs("");
updateDropdowns();
showProc(-1);
target->modified = true;
}
}
if (sEditor && !sEditor->onChange) {
sEditor->onChange = onEditorChange;
}
sEditorFileIdx = fileIdx;
sProject.activeFileIdx = fileIdx;
}
}
// ============================================================
// newProject
// ============================================================
static void newProject(void) {
char name[PRJ_MAX_NAME];
if (!dvxInputBox(sAc, "New Project", "Project name:", "", name, sizeof(name))) {
return;
}
if (name[0] == '\0') {
return;
}
// Ask for directory via save dialog (file = name.dbp)
FileFilterT filters[] = {
{ "Project Files (*.dbp)", "*.dbp" }
};
char dbpPath[DVX_MAX_PATH];
snprintf(dbpPath, sizeof(dbpPath), "%s.dbp", name);
if (!dvxFileDialog(sAc, "Save New Project", FD_SAVE, NULL, filters, 1, dbpPath, sizeof(dbpPath))) {
return;
}
closeProject();
// Derive directory from chosen path
char dir[DVX_MAX_PATH];
snprintf(dir, sizeof(dir), "%s", dbpPath);
char *sep = strrchr(dir, '/');
char *sep2 = strrchr(dir, '\\');
if (sep2 > sep) {
sep = sep2;
}
if (sep) {
*sep = '\0';
} else {
dir[0] = '.';
dir[1] = '\0';
}
prjNew(&sProject, name, dir);
snprintf(sProject.projectPath, sizeof(sProject.projectPath), "%s", dbpPath);
prjSave(&sProject);
sProject.dirty = false;
// Create and show project window
if (!sProjectWin) {
sProjectWin = prjCreateWindow(sAc, &sProject, onPrjFileClick);
if (sProjectWin) {
sProjectWin->y = toolbarBottom() + 25;
sProjectWin->onClose = onProjectWinClose;
}
} else {
prjRebuildTree(&sProject);
}
char title[300];
snprintf(title, sizeof(title), "DVX BASIC - [%s]", sProject.name);
dvxSetTitle(sAc, sWin, title);
setStatus("New project created.");
updateProjectMenuState();
}
// ============================================================
// openProject
// ============================================================
static void openProject(void) {
FileFilterT filters[] = {
{ "Project Files (*.dbp)", "*.dbp" },
{ "All Files (*.*)", "*.*" }
};
char path[DVX_MAX_PATH];
if (!dvxFileDialog(sAc, "Open Project", FD_OPEN, NULL, filters, 2, path, sizeof(path))) {
return;
}
closeProject();
if (!prjLoad(&sProject, path)) {
dvxMessageBox(sAc, "Error", "Could not open project file.", MB_OK | MB_ICONERROR);
return;
}
prjLoadAllFiles(&sProject, sAc);
// Create and show project window
if (!sProjectWin) {
sProjectWin = prjCreateWindow(sAc, &sProject, onPrjFileClick);
if (sProjectWin) {
sProjectWin->y = toolbarBottom() + 25;
sProjectWin->onClose = onProjectWinClose;
}
} else {
prjRebuildTree(&sProject);
}
// Open the first .bas file in the editor
for (int32_t i = 0; i < sProject.fileCount; i++) {
if (!sProject.files[i].isForm) {
onPrjFileClick(i, false);
break;
}
}
char title[300];
snprintf(title, sizeof(title), "DVX BASIC - [%s]", sProject.name);
dvxSetTitle(sAc, sWin, title);
setStatus("Project loaded.");
updateProjectMenuState();
}
// ============================================================
// closeProject
// ============================================================
static void closeProject(void) {
if (sProject.projectPath[0] == '\0') {
return;
}
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();
// 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 (isInStringOrComment(src, i)) {
continue;
}
count++;
}
if (count == 0) {
return NULL;
}
// Allocate output
int32_t outLen = srcLen + count * (newLen - oldLen);
char *out = (char *)malloc(outLen + 1);
if (!out) {
return NULL;
}
// Second pass: build output
int32_t op = 0;
for (int32_t i = 0; i < srcLen; ) {
if (i <= srcLen - oldLen &&
strncasecmp(&src[i], oldName, oldLen) == 0) {
char after = src[i + oldLen];
if ((after == '.' || after == '_') &&
(i == 0 || !isIdentChar(src[i - 1])) &&
!isInStringOrComment(src, i)) {
memcpy(out + op, newName, newLen);
op += newLen;
i += oldLen;
continue;
}
}
out[op++] = src[i++];
}
out[op] = '\0';
return out;
}
void ideRenameInCode(const char *oldName, const char *newName) {
if (!oldName || !newName || strcasecmp(oldName, newName) == 0) {
return;
}
// Rename in the per-procedure buffers (form code currently being edited)
if (sGeneralBuf) {
char *replaced = renameInBuffer(sGeneralBuf, oldName, newName);
if (replaced) {
free(sGeneralBuf);
sGeneralBuf = replaced;
}
}
for (int32_t i = 0; i < (int32_t)arrlen(sProcBufs); i++) {
if (sProcBufs[i]) {
char *replaced = renameInBuffer(sProcBufs[i], oldName, newName);
if (replaced) {
free(sProcBufs[i]);
sProcBufs[i] = replaced;
}
}
}
// Update the editor if it's showing a procedure
if (sEditor && sCurProcIdx >= -1) {
if (sCurProcIdx == -1 && sGeneralBuf) {
wgtSetText(sEditor, sGeneralBuf);
} else if (sCurProcIdx >= 0 && sCurProcIdx < (int32_t)arrlen(sProcBufs)) {
wgtSetText(sEditor, sProcBufs[sCurProcIdx]);
}
}
// Update form->code from the renamed buffers (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;
}
}
// ============================================================
// 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);
}
}
}
// ============================================================
// onContentFocus -- track last focused content window for clipboard
// ============================================================
static void onContentFocus(WindowT *win) {
sLastFocusWin = win;
}
static WindowT *getLastFocusWin(void) {
if (sLastFocusWin == sCodeWin ||
sLastFocusWin == sOutWin ||
sLastFocusWin == sImmWin) {
return sLastFocusWin;
}
sLastFocusWin = NULL;
return NULL;
}
// ============================================================
// hasUnsavedData -- check if any project files have unsaved changes
// ============================================================
static bool hasUnsavedData(void) {
// Check the active editor/designer
if (sDesigner.form && sDesigner.form->dirty) {
return true;
}
// Check all project files
for (int32_t i = 0; i < sProject.fileCount; i++) {
if (sProject.files[i].modified) {
return true;
}
}
return sProject.dirty;
}
// ============================================================
// promptAndSave -- ask user to save, discard, or cancel
// ============================================================
//
// Returns true if the caller should proceed (user saved or discarded).
// Returns false if the user cancelled.
static bool promptAndSave(void) {
if (!hasUnsavedData()) {
return true;
}
int32_t result = dvxPromptSave(sAc, "DVX BASIC");
if (result == DVX_SAVE_YES) {
saveFile();
return true;
}
return result == DVX_SAVE_NO;
}
// ============================================================
// onClose
// ============================================================
static void onClose(WindowT *win) {
if (!promptAndSave()) {
return;
}
// Prevent stale focus tracking during shutdown
sLastFocusWin = NULL;
// Null widget pointers first so nothing references destroyed widgets
sEditor = NULL;
sOutput = NULL;
sImmediate = NULL;
sObjDropdown = NULL;
sEvtDropdown = NULL;
sStatus = NULL;
sToolbar = NULL;
sStatusBar = NULL;
// Close all child windows
// Close all child windows
if (sCodeWin && sCodeWin != win) {
dvxDestroyWindow(sAc, sCodeWin);
}
sCodeWin = NULL;
if (sOutWin && sOutWin != win) {
dvxDestroyWindow(sAc, sOutWin);
}
sOutWin = NULL;
if (sImmWin && sImmWin != win) {
dvxDestroyWindow(sAc, sImmWin);
}
sImmWin = NULL;
if (sFormWin) {
dvxDestroyWindow(sAc, sFormWin);
cleanupFormWin();
}
if (sToolboxWin) {
tbxDestroy(sAc, sToolboxWin);
sToolboxWin = NULL;
}
if (sPropsWin) {
prpDestroy(sAc, sPropsWin);
sPropsWin = NULL;
}
if (sProjectWin) {
prjDestroyWindow(sAc, sProjectWin);
sProjectWin = NULL;
}
closeProject();
// Don't destroy win here -- the shell manages it. Destroying
// it from inside onClose crashes because the calling code in
// dvxApp.c still references the window after the callback returns.
sWin = NULL;
if (sCachedModule) {
basModuleFree(sCachedModule);
sCachedModule = NULL;
}
dsgnFree(&sDesigner);
freeProcBufs();
arrfree(sProcTable);
arrfree(sObjItems);
arrfree(sEvtItems);
sProcTable = NULL;
sObjItems = NULL;
sEvtItems = NULL;
dvxDestroyWindow(sAc, win);
}
// ============================================================
// onMenu
// ============================================================
static void onMenu(WindowT *win, int32_t menuId) {
(void)win;
switch (menuId) {
case CMD_OPEN:
loadFile();
break;
case CMD_SAVE:
saveFile();
break;
case CMD_SAVE_ALL:
saveFile();
// Save all non-active files from their buffers
for (int32_t i = 0; i < sProject.fileCount; i++) {
if (i == sProject.activeFileIdx) {
continue;
}
if (sProject.files[i].modified && sProject.files[i].buffer) {
char fullPath[DVX_MAX_PATH];
prjFullPath(&sProject, i, fullPath, sizeof(fullPath));
FILE *f = fopen(fullPath, "w");
if (f) {
fputs(sProject.files[i].buffer, f);
fclose(f);
sProject.files[i].modified = false;
}
}
}
// Save the project file
if (sProject.projectPath[0] != '\0') {
prjSave(&sProject);
sProject.dirty = false;
}
setStatus("All files saved.");
updateDirtyIndicators();
break;
case CMD_RUN:
compileAndRun();
break;
case CMD_RUN_NOCMP:
runCached();
break;
case CMD_STOP:
sStopRequested = true;
if (sVm) {
sVm->running = false;
}
setStatus("Program stopped.");
break;
case CMD_CLEAR:
clearOutput();
break;
case CMD_VIEW_CODE:
switchToCode();
break;
case CMD_VIEW_DESIGN:
switchToDesign();
break;
case CMD_WIN_CODE:
showCodeWindow();
if (sEditor && !sEditor->onChange) {
sEditor->onChange = onEditorChange;
}
break;
case CMD_WIN_OUTPUT:
showOutputWindow();
break;
case CMD_WIN_IMM:
showImmediateWindow();
break;
case CMD_WIN_TOOLBOX:
if (!sToolboxWin) {
sToolboxWin = tbxCreate(sAc, &sDesigner);
if (sToolboxWin) {
sToolboxWin->y = toolbarBottom();
}
}
break;
case CMD_WIN_PROPS:
if (!sPropsWin) {
sPropsWin = prpCreate(sAc, &sDesigner);
if (sPropsWin) {
sPropsWin->y = toolbarBottom();
}
}
break;
case CMD_CUT:
case CMD_COPY:
case CMD_PASTE:
case CMD_SELECT_ALL: {
// Send the corresponding Ctrl+key to the last focused content window
static const int32_t keys[] = { 24, 3, 22, 1 }; // Ctrl+X, C, V, A
int32_t key = keys[menuId - CMD_CUT];
WindowT *target = getLastFocusWin();
if (target && target->onKey) {
target->onKey(target, key, ACCEL_CTRL);
}
break;
}
case CMD_DELETE:
if (sFormWin && sDesigner.selectedIdx >= 0) {
int32_t prevCount = (int32_t)arrlen(sDesigner.form->controls);
dsgnOnKey(&sDesigner, KEY_DELETE);
int32_t newCount = (int32_t)arrlen(sDesigner.form->controls);
if (newCount != prevCount) {
prpRebuildTree(&sDesigner);
prpRefresh(&sDesigner);
dvxInvalidateWindow(sAc, sFormWin);
}
}
break;
case CMD_VIEW_TOOLBAR:
if (sToolbar && sWin->menuBar) {
bool show = wmMenuItemIsChecked(sWin->menuBar, CMD_VIEW_TOOLBAR);
sToolbar->visible = show;
dvxFitWindowH(sAc, sWin);
prefsSetBool(sPrefs, "view", "toolbar", show);
prefsSave(sPrefs);
}
break;
case CMD_VIEW_STATUS:
if (sStatusBar && sWin->menuBar) {
bool show = wmMenuItemIsChecked(sWin->menuBar, CMD_VIEW_STATUS);
sStatusBar->visible = show;
dvxFitWindowH(sAc, sWin);
prefsSetBool(sPrefs, "view", "statusbar", show);
prefsSave(sPrefs);
}
break;
case CMD_SAVE_ON_RUN:
if (sWin && sWin->menuBar) {
bool save = wmMenuItemIsChecked(sWin->menuBar, CMD_SAVE_ON_RUN);
prefsSetBool(sPrefs, "run", "saveOnRun", save);
prefsSave(sPrefs);
}
break;
case CMD_PRJ_NEW:
newProject();
break;
case CMD_PRJ_OPEN:
openProject();
break;
case CMD_PRJ_SAVE:
if (sProject.projectPath[0] != '\0') {
prjSave(&sProject);
sProject.dirty = false;
setStatus("Project saved.");
}
break;
case CMD_PRJ_CLOSE:
if (promptAndSave()) {
closeProject();
}
break;
case CMD_PRJ_PROPS:
if (sProject.projectPath[0] != '\0') {
if (prjPropertiesDialog(sAc, &sProject, sCtx->appPath)) {
char title[300];
snprintf(title, sizeof(title), "DVX BASIC - [%s]", sProject.name);
dvxSetTitle(sAc, sWin, title);
if (sProjectWin) {
prjRebuildTree(&sProject);
}
}
}
break;
case CMD_PRJ_REMOVE:
if (sProject.activeFileIdx >= 0) {
PrjFileT *rmFile = &sProject.files[sProject.activeFileIdx];
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();
}
}
prjRemoveFile(&sProject, sProject.activeFileIdx);
prjRebuildTree(&sProject);
}
break;
case CMD_WIN_PROJECT:
if (!sProjectWin) {
sProjectWin = prjCreateWindow(sAc, &sProject, onPrjFileClick);
if (sProjectWin) {
sProjectWin->y = toolbarBottom() + 25;
sProjectWin->onClose = onProjectWinClose;
}
}
break;
case CMD_EXIT:
if (sWin) {
onClose(sWin);
}
break;
case CMD_HELP_ABOUT:
dvxMessageBox(sAc, "About DVX BASIC",
"DVX BASIC 1.0\n"
"Visual BASIC Development Environment\n"
"for the DVX GUI System\n"
"\n"
"Copyright 2026 Scott Duensing",
MB_OK | MB_ICONINFO);
break;
}
}
// ============================================================
// 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[256];
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",
"MouseDown", "MouseUp", "MouseMove", "Scroll",
NULL
};
// Form-specific events
static const char *sFormEvents[] = {
"Load", "Unload", "Resize", "Activate", "Deactivate",
"KeyPress", "KeyDown",
"MouseDown", "MouseUp", "MouseMove",
NULL
};
// Buffer for event dropdown labels (with [] for unimplemented)
static char sEvtLabelBufs[64][32];
static void onObjDropdownChange(WidgetT *w) {
(void)w;
if (!sObjDropdown || !sEvtDropdown) {
return;
}
int32_t objIdx = wgtDropdownGetSelected(sObjDropdown);
if (objIdx < 0 || objIdx >= (int32_t)arrlen(sObjItems)) {
return;
}
const char *selObj = sObjItems[objIdx];
// Collect which events already have code
arrsetlen(sEvtItems, 0);
int32_t procCount = (int32_t)arrlen(sProcTable);
const char **existingEvts = NULL; // stb_ds temp array
for (int32_t i = 0; i < procCount; i++) {
if (strcasecmp(sProcTable[i].objName, selObj) == 0) {
arrput(existingEvts, sProcTable[i].evtName);
}
}
// Determine which event list to use
const char **availEvents = sCommonEvents;
if (strcasecmp(selObj, "(General)") == 0) {
// 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;
}
// Get widget-specific events from the interface
const WgtIfaceT *iface = NULL;
if (!isForm && sDesigner.form) {
for (int32_t i = 0; i < (int32_t)arrlen(sDesigner.form->controls); i++) {
if (strcasecmp(sDesigner.form->controls[i].name, selObj) == 0) {
const char *wgtName = wgtFindByBasName(sDesigner.form->controls[i].typeName);
if (wgtName) {
iface = wgtGetIface(wgtName);
}
break;
}
}
}
// Build the event list: standard events + widget-specific events
int32_t labelIdx = 0;
// Add standard events (common or form)
for (int32_t i = 0; availEvents[i]; i++) {
bool hasCode = false;
for (int32_t j = 0; j < (int32_t)arrlen(existingEvts); j++) {
if (strcasecmp(existingEvts[j], availEvents[i]) == 0) {
hasCode = true;
break;
}
}
if (labelIdx < 64) {
if (hasCode) {
snprintf(sEvtLabelBufs[labelIdx], 32, "%s", availEvents[i]);
} else {
snprintf(sEvtLabelBufs[labelIdx], 32, "[%s]", availEvents[i]);
}
arrput(sEvtItems, sEvtLabelBufs[labelIdx]);
labelIdx++;
}
}
// Add widget-specific events
if (iface) {
for (int32_t i = 0; i < iface->eventCount; i++) {
const char *evtName = iface->events[i].name;
// Skip if already in the standard list
bool alreadyListed = false;
for (int32_t j = 0; availEvents[j]; j++) {
if (strcasecmp(availEvents[j], evtName) == 0) {
alreadyListed = true;
break;
}
}
if (alreadyListed) {
continue;
}
bool hasCode = false;
for (int32_t j = 0; j < (int32_t)arrlen(existingEvts); j++) {
if (strcasecmp(existingEvts[j], evtName) == 0) {
hasCode = true;
break;
}
}
if (labelIdx < 64) {
if (hasCode) {
snprintf(sEvtLabelBufs[labelIdx], 32, "%s", evtName);
} else {
snprintf(sEvtLabelBufs[labelIdx], 32, "[%s]", evtName);
}
arrput(sEvtItems, sEvtLabelBufs[labelIdx]);
labelIdx++;
}
}
}
// Add any existing events not in the standard/widget list (custom subs)
for (int32_t i = 0; i < (int32_t)arrlen(existingEvts); i++) {
bool alreadyListed = false;
int32_t evtCount = (int32_t)arrlen(sEvtItems);
for (int32_t j = 0; j < evtCount; j++) {
const char *label = sEvtItems[j];
// Strip brackets for comparison
if (label[0] == '[') { label++; }
if (strncasecmp(label, existingEvts[i], strlen(existingEvts[i])) == 0) {
alreadyListed = true;
break;
}
}
if (!alreadyListed && labelIdx < 64) {
snprintf(sEvtLabelBufs[labelIdx], 32, "%s", existingEvts[i]);
arrput(sEvtItems, sEvtLabelBufs[labelIdx]);
labelIdx++;
}
}
arrfree(existingEvts);
int32_t evtCount = (int32_t)arrlen(sEvtItems);
// 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 onFormWinKey(WindowT *win, int32_t key, int32_t mod) {
(void)mod;
if (key == KEY_DELETE && sDesigner.selectedIdx >= 0) {
int32_t prevCount = sDesigner.form ? (int32_t)arrlen(sDesigner.form->controls) : 0;
dsgnOnKey(&sDesigner, KEY_DELETE);
int32_t newCount = sDesigner.form ? (int32_t)arrlen(sDesigner.form->controls) : 0;
if (newCount != prevCount) {
prpRebuildTree(&sDesigner);
prpRefresh(&sDesigner);
if (sFormWin) {
dvxInvalidateWindow(sAc, sFormWin);
}
}
return;
}
// Forward unhandled keys to the widget system
widgetOnKey(win, key, mod);
}
// ============================================================
// onFormWinCursorQuery
// ============================================================
static int32_t onFormWinCursorQuery(WindowT *win, int32_t x, int32_t y) {
(void)win;
if (!sDesigner.form) {
return 0;
}
// Crosshair when placing a new control
if (sDesigner.activeTool[0] != '\0') {
return CURSOR_CROSSHAIR;
}
if (!sDesigner.form->controls) {
return 0;
}
int32_t count = (int32_t)arrlen(sDesigner.form->controls);
if (sDesigner.selectedIdx < 0 || sDesigner.selectedIdx >= count) {
return 0;
}
DsgnControlT *ctrl = &sDesigner.form->controls[sDesigner.selectedIdx];
if (!ctrl->widget || !ctrl->widget->visible || ctrl->widget->w <= 0 || ctrl->widget->h <= 0) {
return 0;
}
int32_t cx = ctrl->widget->x;
int32_t cy = ctrl->widget->y;
int32_t cw = ctrl->widget->w;
int32_t ch = ctrl->widget->h;
int32_t hs = DSGN_HANDLE_SIZE;
// SE handle (check first -- overlaps with E and S)
if (x >= cx + cw - hs/2 && x < cx + cw + hs/2 && y >= cy + ch - hs/2 && y < cy + ch + hs/2) {
return CURSOR_RESIZE_DIAG_NWSE;
}
// E handle (right edge center)
if (x >= cx + cw - hs/2 && x < cx + cw + hs/2 && y >= cy + ch/2 - hs/2 && y < cy + ch/2 + hs/2) {
return CURSOR_RESIZE_H;
}
// S handle (bottom center)
if (x >= cx + cw/2 - hs/2 && x < cx + cw/2 + hs/2 && y >= cy + ch - hs/2 && y < cy + ch + hs/2) {
return CURSOR_RESIZE_V;
}
return 0;
}
// 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 navigateToEventSub(void) {
if (!sDesigner.form) {
return;
}
// Determine control name and default event
const char *ctrlName = NULL;
const char *eventName = NULL;
if (sDesigner.selectedIdx >= 0 &&
sDesigner.selectedIdx < (int32_t)arrlen(sDesigner.form->controls)) {
DsgnControlT *ctrl = &sDesigner.form->controls[sDesigner.selectedIdx];
ctrlName = ctrl->name;
eventName = dsgnDefaultEvent(ctrl->typeName);
} else {
ctrlName = sDesigner.form->name;
eventName = dsgnDefaultEvent("Form");
}
if (!ctrlName || !eventName) {
return;
}
char subName[128];
snprintf(subName, sizeof(subName), "%s_%s", ctrlName, eventName);
// Stash any existing editor code, then load this form's code
stashFormCode();
parseProcs(sDesigner.form->code ? sDesigner.form->code : "");
sEditorFileIdx = sProject.activeFileIdx;
// Ensure code window is open
if (!sCodeWin) {
showCodeWindow();
}
if (!sEditor) {
return;
}
// Populate dropdown items without triggering navigation --
// we navigate explicitly below after finding the target proc.
{
bool saved = sDropdownNavSuppressed;
sDropdownNavSuppressed = true;
updateDropdowns();
sDropdownNavSuppressed = saved;
}
// Search for existing procedure
int32_t procCount = (int32_t)arrlen(sProcTable);
for (int32_t i = 0; i < procCount; i++) {
char fullName[128];
snprintf(fullName, sizeof(fullName), "%s_%s", sProcTable[i].objName, sProcTable[i].evtName);
if (strcasecmp(fullName, subName) == 0) {
switchToCode();
showProc(i);
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[256];
snprintf(skeleton, sizeof(skeleton), "Sub %s ()\n\nEnd Sub\n", subName);
arrput(sProcBufs, strdup(skeleton));
// Show the new procedure (it's the last one)
switchToCode();
showProc((int32_t)arrlen(sProcBufs) - 1);
if (sEditor && !sEditor->onChange) {
sEditor->onChange = onEditorChange;
}
selectDropdowns(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 a full measure + layout + paint cycle.
// widgetOnPaint normally skips relayout if root dimensions haven't
// changed, but we need it to pick up minH changes from handle drag.
if (win->widgetRoot) {
widgetCalcMinSizeTree(win->widgetRoot, &sAc->font);
win->widgetRoot->w = 0; // force layout pass to re-run
}
widgetOnPaint(win, dirtyArea);
// Then draw selection handles on top
int32_t winX = win->contentX;
int32_t winY = win->contentY;
dsgnPaintOverlay(&sDesigner, winX, winY);
}
// ============================================================
// onFormWinClose
// ============================================================
// cleanupFormWin -- release designer-related state without destroying
// the form window itself (the caller handles that).
static void cleanupFormWin(void) {
sFormWin = NULL;
sDesigner.formWin = NULL;
if (sToolboxWin) {
tbxDestroy(sAc, sToolboxWin);
sToolboxWin = NULL;
}
if (sPropsWin) {
prpDestroy(sAc, sPropsWin);
sPropsWin = NULL;
}
}
// onFormWinResize -- update form dimensions when the design window is resized
static void onFormWinResize(WindowT *win, int32_t newW, int32_t newH) {
// Let the widget system handle the layout recalculation
widgetOnResize(win, newW, newH);
if (sDesigner.form) {
sDesigner.form->width = newW;
sDesigner.form->height = newH;
sDesigner.form->dirty = true;
prpRefresh(&sDesigner);
}
}
// onFormWinClose -- shell callback when user clicks X on the form window.
static void onFormWinClose(WindowT *win) {
dvxDestroyWindow(sAc, win);
cleanupFormWin();
}
// ============================================================
// switchToCode
// ============================================================
static void switchToCode(void) {
// Stash form data so the project system has a current copy.
// This does not mark the file as modified -- it's just caching.
if (sDesigner.form && sProject.activeFileIdx >= 0) {
PrjFileT *cur = &sProject.files[sProject.activeFileIdx];
if (cur->isForm) {
char *frmBuf = (char *)malloc(IDE_MAX_SOURCE);
if (frmBuf) {
int32_t frmLen = dsgnSaveFrm(&sDesigner, frmBuf, IDE_MAX_SOURCE);
free(cur->buffer);
if (frmLen > 0) {
frmBuf[frmLen] = '\0';
cur->buffer = frmBuf;
} else {
free(frmBuf);
cur->buffer = NULL;
}
}
}
}
// Don't destroy the form window -- allow both code and design
// to be open simultaneously, like VB3.
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 (same size as runtime)
const char *formName = sDesigner.form ? sDesigner.form->name : "Form1";
char title[128];
snprintf(title, sizeof(title), "%s [Design]", formName);
sFormWin = dvxCreateWindowCentered(sAc, title, IDE_DESIGN_W, IDE_DESIGN_H, true);
if (!sFormWin) {
return;
}
sFormWin->onClose = onFormWinClose;
sFormWin->onMenu = onMenu;
sFormWin->accelTable = sWin ? sWin->accelTable : NULL;
sDesigner.formWin = sFormWin;
WidgetT *root = wgtInitWindow(sAc, sFormWin);
WidgetT *contentBox;
if (sDesigner.form && strcasecmp(sDesigner.form->layout, "HBox") == 0) {
contentBox = wgtHBox(root);
} else {
contentBox = wgtVBox(root);
}
contentBox->weight = 100;
// Override paint and mouse AFTER wgtInitWindow (which sets widgetOnPaint)
sFormWin->onPaint = onFormWinPaint;
sFormWin->onMouse = onFormWinMouse;
sFormWin->onKey = onFormWinKey;
sFormWin->onResize = onFormWinResize;
sFormWin->onCursorQuery = onFormWinCursorQuery;
// Create live widgets for each control
dsgnCreateWidgets(&sDesigner, contentBox);
// Set form caption as window title
if (sDesigner.form && sDesigner.form->caption[0]) {
char winTitle[280];
snprintf(winTitle, sizeof(winTitle), "%s [Design]", sDesigner.form->caption);
dvxSetTitle(sAc, sFormWin, winTitle);
}
// Size the form window
if (sDesigner.form && sDesigner.form->autoSize) {
dvxFitWindow(sAc, sFormWin);
sDesigner.form->width = sFormWin->w;
sDesigner.form->height = sFormWin->h;
} else if (sDesigner.form) {
dvxResizeWindow(sAc, sFormWin, sDesigner.form->width, sDesigner.form->height);
}
// Create toolbox and properties windows
if (!sToolboxWin) {
sToolboxWin = tbxCreate(sAc, &sDesigner);
if (sToolboxWin) {
sToolboxWin->y = toolbarBottom();
}
}
if (!sPropsWin) {
sPropsWin = prpCreate(sAc, &sDesigner);
if (sPropsWin) {
sPropsWin->y = toolbarBottom();
}
}
dvxInvalidateWindow(sAc, sFormWin);
setStatus("Design view open.");
}
// ============================================================
// Toolbar button handlers
// ============================================================
static void onTbOpen(WidgetT *w) { (void)w; loadFile(); }
static void onTbSave(WidgetT *w) { (void)w; saveFile(); }
static void onTbRun(WidgetT *w) { (void)w; compileAndRun(); }
static void onTbStop(WidgetT *w) { (void)w; sStopRequested = true; if (sVm) { sVm->running = false; } setStatus("Program stopped."); }
static void onTbCode(WidgetT *w) { (void)w; switchToCode(); }
static void onTbDesign(WidgetT *w) { (void)w; switchToDesign(); }
// ============================================================
// showCodeWindow
// ============================================================
static void showCodeWindow(void) {
if (sCodeWin) {
return; // already open
}
int32_t codeY = toolbarBottom();
int32_t codeH = sAc->display.height - codeY - 122;
sCodeWin = dvxCreateWindow(sAc, "Code", 0, codeY, sAc->display.width, codeH, true);
// Ensure position is below the toolbar (dvxCreateWindow may adjust)
if (sCodeWin) {
sCodeWin->y = codeY;
}
if (sCodeWin) {
sCodeWin->onMenu = onMenu;
sCodeWin->onFocus = onContentFocus;
sCodeWin->onClose = onCodeWinClose;
sCodeWin->accelTable = sWin ? sWin->accelTable : NULL;
sLastFocusWin = sCodeWin;
WidgetT *codeRoot = wgtInitWindow(sAc, sCodeWin);
WidgetT *dropdownRow = wgtHBox(codeRoot);
dropdownRow->spacing = wgtPixels(4);
sObjDropdown = wgtDropdown(dropdownRow);
sObjDropdown->weight = 100;
sObjDropdown->onChange = onObjDropdownChange;
wgtDropdownSetItems(sObjDropdown, NULL, 0);
sEvtDropdown = wgtDropdown(dropdownRow);
sEvtDropdown->weight = 100;
sEvtDropdown->onChange = onEvtDropdownChange;
wgtDropdownSetItems(sEvtDropdown, NULL, 0);
sEditor = wgtTextArea(codeRoot, IDE_MAX_SOURCE);
sEditor->weight = 100;
wgtTextAreaSetColorize(sEditor, basicColorize, NULL);
wgtTextAreaSetShowLineNumbers(sEditor, true);
wgtTextAreaSetAutoIndent(sEditor, true);
wgtTextAreaSetCaptureTabs(sEditor, true);
wgtTextAreaSetTabWidth(sEditor, 3);
wgtTextAreaSetUseTabChar(sEditor, false);
// onChange is set after initial content is loaded by the caller
// (navigateToEventSub, onPrjFileClick, etc.) to prevent false dirty marking.
}
}
// ============================================================
// 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");
}
}
}
// ============================================================
// setStatus
// ============================================================
static void onEditorChange(WidgetT *w) {
(void)w;
// 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');
wmMenuItemSetEnabled(sWin->menuBar, CMD_PRJ_SAVE, hasProject);
wmMenuItemSetEnabled(sWin->menuBar, CMD_PRJ_CLOSE, hasProject);
wmMenuItemSetEnabled(sWin->menuBar, CMD_PRJ_PROPS, hasProject);
wmMenuItemSetEnabled(sWin->menuBar, CMD_PRJ_REMOVE, hasProject);
wmMenuItemSetEnabled(sWin->menuBar, CMD_SAVE, hasProject);
wmMenuItemSetEnabled(sWin->menuBar, CMD_SAVE_ALL, hasProject);
}
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;
if (sProject.activeFileIdx >= 0) {
codeDirty = sProject.files[sProject.activeFileIdx].modified;
}
dvxSetTitle(sAc, sCodeWin, codeDirty ? "Code *" : "Code");
}
// 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++; }
}
// 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;
}
// 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;
}
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;
char *underscore = strchr(procName, '_');
if (underscore) {
int32_t objLen = (int32_t)(underscore - procName);
if (objLen > 63) {
objLen = 63;
}
memcpy(entry.objName, procName, objLen);
entry.objName[objLen] = '\0';
snprintf(entry.evtName, sizeof(entry.evtName), "%s", underscore + 1);
} else {
snprintf(entry.objName, sizeof(entry.objName), "%s", "(General)");
snprintf(entry.evtName, sizeof(entry.evtName), "%s", procName);
}
arrput(sProcTable, entry);
// Skip to end of this proc (already scanned)
pos = scan;
// Count lines we skipped
for (const char *c = lineStart; c < scan; c++) {
if (*c == '\n') {
lineNum++;
}
}
continue;
}
// Advance to end of line
while (*pos && *pos != '\n') {
pos++;
}
if (*pos == '\n') {
pos++;
}
lineNum++;
}
// Build object names for the Object dropdown.
// Always include "(General)" and the form name (if editing a form).
// Then add control names from the designer, plus any from existing procs.
arrput(sObjItems, "(General)");
if (sDesigner.form) {
arrput(sObjItems, sDesigner.form->name);
for (int32_t i = 0; i < (int32_t)arrlen(sDesigner.form->controls); i++) {
arrput(sObjItems, sDesigner.form->controls[i].name);
}
}
// Add any objects from existing procs not already in the list
int32_t procCount = (int32_t)arrlen(sProcTable);
for (int32_t i = 0; i < procCount; i++) {
bool found = false;
int32_t objCount = (int32_t)arrlen(sObjItems);
for (int32_t j = 0; j < objCount; j++) {
if (strcasecmp(sObjItems[j], sProcTable[i].objName) == 0) {
found = true;
break;
}
}
if (!found) {
arrput(sObjItems, sProcTable[i].objName);
}
}
// 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);
}
}