1940 lines
53 KiB
C
1940 lines
53 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 "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 IDE_MAX_IMM 1024
|
|
#define IDE_DESIGN_W 400
|
|
#define IDE_DESIGN_H 300
|
|
|
|
// ============================================================
|
|
// Prototypes
|
|
// ============================================================
|
|
|
|
int32_t appMain(DxeAppContextT *ctx);
|
|
static void buildWindow(void);
|
|
static void clearOutput(void);
|
|
static void compileAndRun(void);
|
|
static void loadFile(void);
|
|
static void loadFilePath(const char *path);
|
|
static void saveFile(void);
|
|
static void onTbSave(WidgetT *w);
|
|
static void onClose(WindowT *win);
|
|
static void onContentFocus(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 onFormWinClose(WindowT *win);
|
|
static void setStatus(const char *text);
|
|
static void switchToCode(void);
|
|
static void switchToDesign(void);
|
|
static int32_t onFormWinCursorQuery(WindowT *win, int32_t x, int32_t y);
|
|
static void onFormWinKey(WindowT *win, int32_t key, int32_t mod);
|
|
static void onFormWinMouse(WindowT *win, int32_t x, int32_t y, int32_t buttons);
|
|
static void onFormWinPaint(WindowT *win, RectT *dirtyArea);
|
|
static void onTbRun(WidgetT *w);
|
|
static void showCodeWindow(void);
|
|
static void showOutputWindow(void);
|
|
static void showImmediateWindow(void);
|
|
static void onTbStop(WidgetT *w);
|
|
static void onTbOpen(WidgetT *w);
|
|
static void onTbCode(WidgetT *w);
|
|
static void onTbDesign(WidgetT *w);
|
|
static void updateDropdowns(void);
|
|
|
|
// ============================================================
|
|
// Module state
|
|
// ============================================================
|
|
|
|
static DxeAppContextT *sCtx = NULL;
|
|
static AppContextT *sAc = NULL;
|
|
static 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 *sLastFocusWin = NULL; // last focused non-toolbar window
|
|
|
|
static char sSourceBuf[IDE_MAX_SOURCE];
|
|
static char sOutputBuf[IDE_MAX_OUTPUT];
|
|
static int32_t sOutputLen = 0;
|
|
static char sFilePath[DVX_MAX_PATH];
|
|
|
|
// Procedure table for Object/Event dropdowns
|
|
typedef struct {
|
|
char objName[64];
|
|
char evtName[64];
|
|
int32_t lineNum;
|
|
} IdeProcEntryT;
|
|
|
|
static IdeProcEntryT *sProcTable = NULL; // stb_ds dynamic array
|
|
static const char **sObjItems = NULL; // stb_ds dynamic array
|
|
static const char **sEvtItems = NULL; // stb_ds dynamic array
|
|
|
|
// ============================================================
|
|
// App descriptor
|
|
// ============================================================
|
|
|
|
AppDescriptorT appDescriptor = {
|
|
.name = "DVX BASIC",
|
|
.hasMainLoop = false,
|
|
.multiInstance = false,
|
|
.stackSize = SHELL_STACK_DEFAULT,
|
|
.priority = 0
|
|
};
|
|
|
|
// ============================================================
|
|
// appMain
|
|
// ============================================================
|
|
|
|
int32_t appMain(DxeAppContextT *ctx) {
|
|
sCtx = ctx;
|
|
sAc = ctx->shellCtx;
|
|
|
|
basStringSystemInit();
|
|
buildWindow();
|
|
|
|
// Load persisted settings
|
|
char prefsPath[DVX_MAX_PATH];
|
|
shellConfigPath(sCtx, "dvxbasic.ini", prefsPath, sizeof(prefsPath));
|
|
prefsLoad(prefsPath);
|
|
|
|
if (sToolbar && sWin && sWin->menuBar) {
|
|
bool showTb = prefsGetBool("view", "toolbar", true);
|
|
sToolbar->visible = showTb;
|
|
wmMenuItemSetChecked(sWin->menuBar, CMD_VIEW_TOOLBAR, showTb);
|
|
}
|
|
|
|
if (sStatusBar && sWin && sWin->menuBar) {
|
|
bool showSb = prefsGetBool("view", "statusbar", true);
|
|
sStatusBar->visible = showSb;
|
|
wmMenuItemSetChecked(sWin->menuBar, CMD_VIEW_STATUS, showSb);
|
|
}
|
|
|
|
if (sWin) {
|
|
dvxFitWindowH(sAc, sWin);
|
|
}
|
|
|
|
sFilePath[0] = '\0';
|
|
sSourceBuf[0] = '\0';
|
|
sOutputBuf[0] = '\0';
|
|
sOutputLen = 0;
|
|
|
|
// Auto-load clickme.bas for development/testing
|
|
loadFilePath("C:\\BIN\\APPS\\DVXBASIC\\CLICKME.BAS");
|
|
|
|
setStatus("Ready. Open a .BAS file or type code and press Run.");
|
|
return 0;
|
|
}
|
|
|
|
// ============================================================
|
|
// 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, "&Open...\tCtrl+O", CMD_OPEN);
|
|
wmAddMenuItem(fileMenu, "&Save\tCtrl+S", CMD_SAVE);
|
|
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);
|
|
|
|
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, "&Toolbox", CMD_WIN_TOOLBOX);
|
|
wmAddMenuItem(winMenu, "&Properties", CMD_WIN_PROPS);
|
|
|
|
AccelTableT *accel = dvxCreateAccelTable();
|
|
dvxAddAccel(accel, 'O', ACCEL_CTRL, CMD_OPEN);
|
|
dvxAddAccel(accel, 'S', ACCEL_CTRL, CMD_SAVE);
|
|
dvxAddAccel(accel, KEY_F5, 0, CMD_RUN);
|
|
dvxAddAccel(accel, KEY_F5, ACCEL_CTRL, CMD_RUN_NOCMP);
|
|
dvxAddAccel(accel, KEY_F7, 0, CMD_VIEW_CODE);
|
|
dvxAddAccel(accel, KEY_F7, ACCEL_SHIFT, CMD_VIEW_DESIGN);
|
|
dvxAddAccel(accel, 0x1B, 0, CMD_STOP);
|
|
sWin->accelTable = accel;
|
|
|
|
WidgetT *tbRoot = wgtInitWindow(sAc, sWin);
|
|
sToolbar = wgtToolbar(tbRoot);
|
|
WidgetT *tb = sToolbar;
|
|
|
|
WidgetT *tbOpen = loadTbIcon(tb, "tb_open", "Open");
|
|
tbOpen->onClick = onTbOpen;
|
|
wgtSetTooltip(tbOpen, "Open (Ctrl+O)");
|
|
|
|
WidgetT *tbSave = loadTbIcon(tb, "tb_save", "Save");
|
|
tbSave->onClick = onTbSave;
|
|
wgtSetTooltip(tbSave, "Save (Ctrl+S)");
|
|
|
|
WidgetT *tbRun = loadTbIcon(tb, "tb_run", "Run");
|
|
tbRun->onClick = onTbRun;
|
|
wgtSetTooltip(tbRun, "Run (F5)");
|
|
|
|
WidgetT *tbStop = loadTbIcon(tb, "tb_stop", "Stop");
|
|
tbStop->onClick = onTbStop;
|
|
wgtSetTooltip(tbStop, "Stop (Esc)");
|
|
|
|
WidgetT *tbCode = loadTbIcon(tb, "tb_code", "Code");
|
|
tbCode->onClick = onTbCode;
|
|
wgtSetTooltip(tbCode, "Code View (F7)");
|
|
|
|
WidgetT *tbDesign = loadTbIcon(tb, "tb_design", "Design");
|
|
tbDesign->onClick = onTbDesign;
|
|
wgtSetTooltip(tbDesign, "Design View (Shift+F7)");
|
|
|
|
sStatusBar = wgtStatusBar(tbRoot);
|
|
WidgetT *statusBar = sStatusBar;
|
|
sStatus = wgtLabel(statusBar, "");
|
|
sStatus->weight = 100;
|
|
|
|
// Fit height to content, keeping full screen width
|
|
dvxFitWindowH(sAc, sWin);
|
|
|
|
// Initialize designer (form window created on demand)
|
|
dsgnInit(&sDesigner, sAc);
|
|
|
|
// Create child windows
|
|
showCodeWindow();
|
|
showOutputWindow();
|
|
showImmediateWindow();
|
|
}
|
|
|
|
// ============================================================
|
|
// basicColorize
|
|
// ============================================================
|
|
//
|
|
// Syntax colorizer callback for BASIC source code. Scans a single
|
|
// line and fills the colors array with syntax color indices.
|
|
|
|
static bool isBasicKeyword(const char *word, int32_t wordLen) {
|
|
static const char *keywords[] = {
|
|
"AND", "AS", "CALL", "CASE", "CLOSE", "CONST", "DATA", "DECLARE",
|
|
"DEF", "DEFINT", "DEFLNG", "DEFSNG", "DEFDBL", "DEFSTR",
|
|
"DIM", "DO", "DOEVENTS", "ELSE", "ELSEIF", "END", "ERASE",
|
|
"EXIT", "FOR", "FUNCTION", "GET", "GOSUB", "GOTO", "HIDE",
|
|
"IF", "IMP", "INPUT", "IS", "LET", "LIBRARY", "LINE", "LOAD",
|
|
"LOOP", "MOD", "MSGBOX", "NEXT", "NOT", "ON", "OPEN", "OPTION",
|
|
"OR", "PRINT", "PUT", "RANDOMIZE", "READ", "REDIM", "RESTORE",
|
|
"RESUME", "RETURN", "SEEK", "SELECT", "SHARED", "SHELL", "SHOW",
|
|
"SLEEP", "STATIC", "STEP", "STOP", "SUB", "SWAP", "THEN", "TO",
|
|
"TYPE", "UNLOAD", "UNTIL", "WEND", "WHILE", "WRITE", "XOR",
|
|
NULL
|
|
};
|
|
|
|
char upper[32];
|
|
|
|
if (wordLen <= 0 || wordLen >= 32) {
|
|
return false;
|
|
}
|
|
|
|
for (int32_t i = 0; i < wordLen; i++) {
|
|
upper[i] = (char)toupper((unsigned char)word[i]);
|
|
}
|
|
|
|
upper[wordLen] = '\0';
|
|
|
|
for (int32_t i = 0; keywords[i]; i++) {
|
|
if (strcmp(upper, keywords[i]) == 0) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
|
|
static bool isBasicType(const char *word, int32_t wordLen) {
|
|
static const char *types[] = {
|
|
"BOOLEAN", "BYTE", "DOUBLE", "INTEGER", "LONG", "SINGLE", "STRING",
|
|
"TRUE", "FALSE",
|
|
NULL
|
|
};
|
|
|
|
char upper[32];
|
|
|
|
if (wordLen <= 0 || wordLen >= 32) {
|
|
return false;
|
|
}
|
|
|
|
for (int32_t i = 0; i < wordLen; i++) {
|
|
upper[i] = (char)toupper((unsigned char)word[i]);
|
|
}
|
|
|
|
upper[wordLen] = '\0';
|
|
|
|
for (int32_t i = 0; types[i]; i++) {
|
|
if (strcmp(upper, types[i]) == 0) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
|
|
static void basicColorize(const char *line, int32_t lineLen, uint8_t *colors, void *ctx) {
|
|
(void)ctx;
|
|
int32_t i = 0;
|
|
|
|
while (i < lineLen) {
|
|
char ch = line[i];
|
|
|
|
// Comment: ' or REM
|
|
if (ch == '\'') {
|
|
while (i < lineLen) {
|
|
colors[i++] = 3; // SYNTAX_COMMENT
|
|
}
|
|
return;
|
|
}
|
|
|
|
// String literal
|
|
if (ch == '"') {
|
|
colors[i++] = 2; // SYNTAX_STRING
|
|
|
|
while (i < lineLen && line[i] != '"') {
|
|
colors[i++] = 2;
|
|
}
|
|
|
|
if (i < lineLen) {
|
|
colors[i++] = 2; // closing quote
|
|
}
|
|
|
|
continue;
|
|
}
|
|
|
|
// Number
|
|
if (isdigit((unsigned char)ch) || (ch == '.' && i + 1 < lineLen && isdigit((unsigned char)line[i + 1]))) {
|
|
while (i < lineLen && (isdigit((unsigned char)line[i]) || line[i] == '.')) {
|
|
colors[i++] = 4; // SYNTAX_NUMBER
|
|
}
|
|
|
|
continue;
|
|
}
|
|
|
|
// Identifier or keyword
|
|
if (isalpha((unsigned char)ch) || ch == '_') {
|
|
int32_t start = i;
|
|
|
|
while (i < lineLen && (isalnum((unsigned char)line[i]) || line[i] == '_' || line[i] == '$' || line[i] == '%' || line[i] == '&' || line[i] == '!' || line[i] == '#')) {
|
|
i++;
|
|
}
|
|
|
|
int32_t wordLen = i - start;
|
|
|
|
// Check for REM comment
|
|
if (wordLen == 3 && (line[start] == 'R' || line[start] == 'r') && (line[start + 1] == 'E' || line[start + 1] == 'e') && (line[start + 2] == 'M' || line[start + 2] == 'm')) {
|
|
for (int32_t j = start; j < lineLen; j++) {
|
|
colors[j] = 3; // SYNTAX_COMMENT
|
|
}
|
|
return;
|
|
}
|
|
|
|
uint8_t c = 0; // default
|
|
|
|
if (isBasicKeyword(line + start, wordLen)) {
|
|
c = 1; // SYNTAX_KEYWORD
|
|
} else if (isBasicType(line + start, wordLen)) {
|
|
c = 6; // SYNTAX_TYPE
|
|
}
|
|
|
|
for (int32_t j = start; j < i; j++) {
|
|
colors[j] = c;
|
|
}
|
|
|
|
continue;
|
|
}
|
|
|
|
// Operators
|
|
if (ch == '=' || ch == '<' || ch == '>' || ch == '+' || ch == '-' || ch == '*' || ch == '/' || ch == '\\' || ch == '&') {
|
|
colors[i++] = 5; // SYNTAX_OPERATOR
|
|
continue;
|
|
}
|
|
|
|
// Default (whitespace, parens, etc.)
|
|
colors[i++] = 0;
|
|
}
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// clearOutput
|
|
// ============================================================
|
|
|
|
static void clearOutput(void) {
|
|
sOutputBuf[0] = '\0';
|
|
sOutputLen = 0;
|
|
wgtSetText(sOutput, "");
|
|
}
|
|
|
|
// ============================================================
|
|
// compileAndRun
|
|
// ============================================================
|
|
|
|
static void compileAndRun(void) {
|
|
// Get source from editor
|
|
const char *src = wgtGetText(sEditor);
|
|
|
|
if (!src || *src == '\0') {
|
|
setStatus("No source code to run.");
|
|
return;
|
|
}
|
|
|
|
clearOutput();
|
|
setStatus("Compiling...");
|
|
|
|
// Force a display update so the status is visible
|
|
dvxInvalidateWindow(sAc, sWin);
|
|
|
|
int32_t srcLen = (int32_t)strlen(src);
|
|
|
|
// Compile (heap-allocated -- BasParserT is ~300KB, too large for stack)
|
|
BasParserT *parser = (BasParserT *)malloc(sizeof(BasParserT));
|
|
|
|
if (!parser) {
|
|
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;
|
|
wgtSetText(sOutput, sOutputBuf);
|
|
|
|
// Jump to error line in editor
|
|
if (parser->errorLine > 0 && sEditor) {
|
|
wgtTextAreaGoToLine(sEditor, parser->errorLine);
|
|
}
|
|
|
|
setStatus("Compilation failed.");
|
|
basParserFree(parser);
|
|
free(parser);
|
|
return;
|
|
}
|
|
|
|
BasModuleT *mod = basParserBuildModule(parser);
|
|
basParserFree(parser);
|
|
free(parser);
|
|
|
|
if (!mod) {
|
|
setStatus("Failed to build module.");
|
|
return;
|
|
}
|
|
|
|
// Cache the compiled module for Ctrl+F5
|
|
if (sCachedModule) {
|
|
basModuleFree(sCachedModule);
|
|
}
|
|
|
|
sCachedModule = mod;
|
|
|
|
// Update Object/Event dropdowns
|
|
updateDropdowns();
|
|
|
|
runModule(mod);
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// runCached
|
|
// ============================================================
|
|
|
|
static void runCached(void) {
|
|
if (!sCachedModule) {
|
|
setStatus("No compiled program. Press F5 to compile first.");
|
|
return;
|
|
}
|
|
|
|
clearOutput();
|
|
runModule(sCachedModule);
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// runModule
|
|
// ============================================================
|
|
|
|
static void runModule(BasModuleT *mod) {
|
|
setStatus("Running...");
|
|
|
|
// Create VM
|
|
BasVmT *vm = basVmCreate();
|
|
basVmLoadModule(vm, mod);
|
|
|
|
// Set up implicit main frame
|
|
vm->callStack[0].localCount = mod->globalCount > BAS_VM_MAX_LOCALS ? BAS_VM_MAX_LOCALS : mod->globalCount;
|
|
vm->callDepth = 1;
|
|
|
|
// Set I/O callbacks
|
|
basVmSetPrintCallback(vm, printCallback, NULL);
|
|
basVmSetInputCallback(vm, inputCallback, NULL);
|
|
basVmSetDoEventsCallback(vm, doEventsCallback, NULL);
|
|
|
|
// Create form runtime (bridges UI opcodes to DVX widgets)
|
|
BasFormRtT *formRt = basFormRtCreate(sAc, vm, mod);
|
|
|
|
// Load any .frm files from the same directory as the source
|
|
loadFrmFiles(formRt);
|
|
|
|
sVm = vm;
|
|
|
|
// Run in slices of 10000 steps, yielding to DVX between slices
|
|
basVmSetStepLimit(vm, IDE_STEP_SLICE);
|
|
|
|
int32_t totalSteps = 0;
|
|
BasVmResultE result;
|
|
|
|
for (;;) {
|
|
result = basVmRun(vm);
|
|
totalSteps += vm->stepCount;
|
|
|
|
if (result == BAS_VM_STEP_LIMIT) {
|
|
// Yield to DVX to keep the GUI responsive
|
|
dvxUpdate(sAc);
|
|
|
|
// Stop if IDE window was closed or DVX is shutting down
|
|
if (!sWin || !sAc->running) {
|
|
break;
|
|
}
|
|
|
|
continue;
|
|
}
|
|
|
|
if (result == BAS_VM_HALTED) {
|
|
break;
|
|
}
|
|
|
|
// Runtime error
|
|
int32_t pos = sOutputLen;
|
|
int32_t n = snprintf(sOutputBuf + pos, IDE_MAX_OUTPUT - pos, "\n[Runtime error: %s]\n", basVmGetError(vm));
|
|
sOutputLen += n;
|
|
wgtSetText(sOutput, sOutputBuf);
|
|
break;
|
|
}
|
|
|
|
sVm = NULL;
|
|
|
|
// Update output display
|
|
wgtSetText(sOutput, sOutputBuf);
|
|
|
|
static char statusBuf[128];
|
|
snprintf(statusBuf, sizeof(statusBuf), "Done. %ld instructions executed.", (long)totalSteps);
|
|
setStatus(statusBuf);
|
|
|
|
basFormRtDestroy(formRt);
|
|
basVmDestroy(vm);
|
|
}
|
|
|
|
// ============================================================
|
|
// doEventsCallback
|
|
// ============================================================
|
|
|
|
static bool doEventsCallback(void *ctx) {
|
|
(void)ctx;
|
|
|
|
// Stop if IDE window was closed or DVX is shutting down
|
|
if (!sWin || !sAc->running) {
|
|
return false;
|
|
}
|
|
|
|
dvxUpdate(sAc);
|
|
|
|
return sWin != NULL && sAc->running;
|
|
}
|
|
|
|
// ============================================================
|
|
// evaluateImmediate
|
|
// ============================================================
|
|
//
|
|
// Compile and execute a single line from the Immediate window.
|
|
// If the line doesn't start with PRINT, wrap it in PRINT so
|
|
// expressions produce visible output.
|
|
|
|
static void immPrintCallback(void *ctx, const char *text, bool newline) {
|
|
(void)ctx;
|
|
|
|
if (!sImmediate) {
|
|
return;
|
|
}
|
|
|
|
// Append output to the immediate window
|
|
const char *cur = wgtGetText(sImmediate);
|
|
int32_t curLen = cur ? (int32_t)strlen(cur) : 0;
|
|
int32_t textLen = text ? (int32_t)strlen(text) : 0;
|
|
|
|
if (curLen + textLen + 2 < IDE_MAX_IMM) {
|
|
static char immBuf[IDE_MAX_IMM];
|
|
memcpy(immBuf, cur, curLen);
|
|
memcpy(immBuf + curLen, text, textLen);
|
|
curLen += textLen;
|
|
|
|
if (newline) {
|
|
immBuf[curLen++] = '\n';
|
|
}
|
|
|
|
immBuf[curLen] = '\0';
|
|
wgtSetText(sImmediate, immBuf);
|
|
|
|
// Move cursor to end so user can keep typing
|
|
int32_t lines = 1;
|
|
|
|
for (int32_t i = 0; i < curLen; i++) {
|
|
if (immBuf[i] == '\n') {
|
|
lines++;
|
|
}
|
|
}
|
|
|
|
wgtTextAreaGoToLine(sImmediate, lines);
|
|
}
|
|
}
|
|
|
|
|
|
static void evaluateImmediate(const char *expr) {
|
|
if (!expr || *expr == '\0') {
|
|
return;
|
|
}
|
|
|
|
char wrapped[1024];
|
|
|
|
// If it already starts with a statement keyword, use as-is
|
|
if (strncasecmp(expr, "PRINT", 5) == 0 || strncasecmp(expr, "DIM", 3) == 0 || strncasecmp(expr, "LET", 3) == 0) {
|
|
snprintf(wrapped, sizeof(wrapped), "%s", expr);
|
|
} else {
|
|
snprintf(wrapped, sizeof(wrapped), "PRINT %s", expr);
|
|
}
|
|
|
|
BasParserT *parser = (BasParserT *)malloc(sizeof(BasParserT));
|
|
|
|
if (!parser) {
|
|
return;
|
|
}
|
|
|
|
basParserInit(parser, wrapped, (int32_t)strlen(wrapped));
|
|
|
|
if (!basParse(parser)) {
|
|
// Show error inline
|
|
immPrintCallback(NULL, "Error: ", false);
|
|
immPrintCallback(NULL, parser->error, true);
|
|
basParserFree(parser);
|
|
free(parser);
|
|
return;
|
|
}
|
|
|
|
BasModuleT *mod = basParserBuildModule(parser);
|
|
basParserFree(parser);
|
|
free(parser);
|
|
|
|
if (!mod) {
|
|
return;
|
|
}
|
|
|
|
BasVmT *vm = basVmCreate();
|
|
basVmLoadModule(vm, mod);
|
|
vm->callStack[0].localCount = mod->globalCount > BAS_VM_MAX_LOCALS ? BAS_VM_MAX_LOCALS : mod->globalCount;
|
|
vm->callDepth = 1;
|
|
basVmSetPrintCallback(vm, immPrintCallback, NULL);
|
|
|
|
BasVmResultE result = basVmRun(vm);
|
|
|
|
if (result != BAS_VM_HALTED && result != BAS_VM_OK) {
|
|
immPrintCallback(NULL, "Error: ", false);
|
|
immPrintCallback(NULL, basVmGetError(vm), true);
|
|
}
|
|
|
|
basVmDestroy(vm);
|
|
basModuleFree(mod);
|
|
}
|
|
|
|
// ============================================================
|
|
// inputCallback
|
|
// ============================================================
|
|
|
|
static bool inputCallback(void *ctx, const char *prompt, char *buf, int32_t bufSize) {
|
|
(void)ctx;
|
|
|
|
// Append prompt to output
|
|
if (prompt && sOutputLen < IDE_MAX_OUTPUT - 1) {
|
|
int32_t n = snprintf(sOutputBuf + sOutputLen, IDE_MAX_OUTPUT - sOutputLen, "%s", prompt);
|
|
sOutputLen += n;
|
|
wgtSetText(sOutput, sOutputBuf);
|
|
}
|
|
|
|
return dvxInputBox(sAc, "DVX BASIC", prompt ? prompt : "Enter value:", NULL, buf, bufSize);
|
|
}
|
|
|
|
// ============================================================
|
|
// loadFile
|
|
// ============================================================
|
|
|
|
static void loadFilePath(const char *path) {
|
|
FILE *f = fopen(path, "r");
|
|
|
|
if (!f) {
|
|
return;
|
|
}
|
|
|
|
fseek(f, 0, SEEK_END);
|
|
long size = ftell(f);
|
|
fseek(f, 0, SEEK_SET);
|
|
|
|
if (size >= IDE_MAX_SOURCE - 1) {
|
|
fclose(f);
|
|
return;
|
|
}
|
|
|
|
int32_t bytesRead = (int32_t)fread(sSourceBuf, 1, size, f);
|
|
fclose(f);
|
|
sSourceBuf[bytesRead] = '\0';
|
|
|
|
wgtSetText(sEditor, sSourceBuf);
|
|
snprintf(sFilePath, sizeof(sFilePath), "%s", path);
|
|
|
|
char title[300];
|
|
snprintf(title, sizeof(title), "DVX BASIC - %s", path);
|
|
dvxSetTitle(sAc, sWin, title);
|
|
|
|
if (sFormWin) {
|
|
onFormWinClose(sFormWin);
|
|
}
|
|
|
|
dsgnFree(&sDesigner);
|
|
|
|
updateDropdowns();
|
|
setStatus("File loaded.");
|
|
}
|
|
|
|
|
|
static void loadFile(void) {
|
|
FileFilterT filters[] = {
|
|
{ "BASIC Files (*.bas)", "*.bas" },
|
|
{ "Form Files (*.frm)", "*.frm" },
|
|
{ "All Files (*.*)", "*.*" }
|
|
};
|
|
|
|
char path[DVX_MAX_PATH];
|
|
|
|
if (dvxFileDialog(sAc, "Open BASIC File", FD_OPEN, NULL, filters, 3, path, sizeof(path))) {
|
|
loadFilePath(path);
|
|
}
|
|
}
|
|
|
|
// ============================================================
|
|
// saveFile
|
|
// ============================================================
|
|
|
|
static void saveFile(void) {
|
|
if (sFilePath[0] == '\0') {
|
|
// No file loaded -- use Save As dialog
|
|
FileFilterT filters[] = {
|
|
{ "BASIC Files (*.bas)", "*.bas" },
|
|
{ "All Files (*.*)", "*.*" }
|
|
};
|
|
|
|
if (!dvxFileDialog(sAc, "Save BASIC File", FD_SAVE, NULL, filters, 2, sFilePath, sizeof(sFilePath))) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Save the .bas source
|
|
const char *src = wgtGetText(sEditor);
|
|
|
|
if (src) {
|
|
FILE *f = fopen(sFilePath, "w");
|
|
|
|
if (f) {
|
|
fputs(src, f);
|
|
fclose(f);
|
|
} else {
|
|
dvxMessageBox(sAc, "Error", "Could not write file.", MB_OK | MB_ICONERROR);
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Save the .frm if the designer has form data
|
|
if (sDesigner.form && sDesigner.form->dirty) {
|
|
char frmPath[DVX_MAX_PATH];
|
|
snprintf(frmPath, sizeof(frmPath), "%s", sFilePath);
|
|
char *dot = strrchr(frmPath, '.');
|
|
|
|
if (dot) {
|
|
strcpy(dot, ".frm");
|
|
} else {
|
|
strcat(frmPath, ".frm");
|
|
}
|
|
|
|
char frmBuf[IDE_MAX_SOURCE];
|
|
int32_t frmLen = dsgnSaveFrm(&sDesigner, frmBuf, sizeof(frmBuf));
|
|
|
|
if (frmLen > 0) {
|
|
FILE *f = fopen(frmPath, "w");
|
|
|
|
if (f) {
|
|
fwrite(frmBuf, 1, frmLen, f);
|
|
fclose(f);
|
|
sDesigner.form->dirty = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
setStatus("Saved.");
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// loadFrmFiles
|
|
// ============================================================
|
|
//
|
|
// Try to load a .frm file with the same base name as the loaded
|
|
// .bas source file. For example, if the user loaded "clickme.bas",
|
|
// this looks for "clickme.frm" in the same directory.
|
|
|
|
static void loadFrmFiles(BasFormRtT *rt) {
|
|
if (sFilePath[0] == '\0') {
|
|
return;
|
|
}
|
|
|
|
// Build .frm path from .bas path
|
|
char frmPath[DVX_MAX_PATH];
|
|
snprintf(frmPath, sizeof(frmPath), "%s", sFilePath);
|
|
|
|
// Find the extension and replace with .frm
|
|
char *dot = strrchr(frmPath, '.');
|
|
|
|
if (dot) {
|
|
strcpy(dot, ".frm");
|
|
} else {
|
|
strcat(frmPath, ".frm");
|
|
}
|
|
|
|
// Try to open the .frm file
|
|
FILE *f = fopen(frmPath, "r");
|
|
|
|
if (!f) {
|
|
return;
|
|
}
|
|
|
|
fseek(f, 0, SEEK_END);
|
|
long size = ftell(f);
|
|
fseek(f, 0, SEEK_SET);
|
|
|
|
if (size <= 0 || size >= IDE_MAX_SOURCE) {
|
|
fclose(f);
|
|
return;
|
|
}
|
|
|
|
char *frmBuf = (char *)malloc(size + 1);
|
|
|
|
if (!frmBuf) {
|
|
fclose(f);
|
|
return;
|
|
}
|
|
|
|
int32_t bytesRead = (int32_t)fread(frmBuf, 1, size, f);
|
|
fclose(f);
|
|
frmBuf[bytesRead] = '\0';
|
|
|
|
BasFormT *form = basFormRtLoadFrm(rt, frmBuf, bytesRead);
|
|
|
|
if (form) {
|
|
int32_t pos = sOutputLen;
|
|
int32_t n = snprintf(sOutputBuf + pos, IDE_MAX_OUTPUT - pos, "Loaded form: %s\n", form->name);
|
|
sOutputLen += n;
|
|
}
|
|
|
|
free(frmBuf);
|
|
}
|
|
|
|
|
|
|
|
|
|
// ============================================================
|
|
// 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;
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// onClose
|
|
// ============================================================
|
|
|
|
static void onClose(WindowT *win) {
|
|
// Close all child windows
|
|
if (sCodeWin && sCodeWin != win) {
|
|
dvxDestroyWindow(sAc, sCodeWin);
|
|
}
|
|
|
|
if (sOutWin && sOutWin != win) {
|
|
dvxDestroyWindow(sAc, sOutWin);
|
|
}
|
|
|
|
if (sImmWin && sImmWin != win) {
|
|
dvxDestroyWindow(sAc, sImmWin);
|
|
}
|
|
|
|
if (sFormWin) {
|
|
onFormWinClose(sFormWin);
|
|
}
|
|
|
|
sWin = NULL;
|
|
sCodeWin = NULL;
|
|
sOutWin = NULL;
|
|
sImmWin = NULL;
|
|
sLastFocusWin = NULL;
|
|
sEditor = NULL;
|
|
sOutput = NULL;
|
|
sImmediate = NULL;
|
|
sObjDropdown = NULL;
|
|
sEvtDropdown = NULL;
|
|
sStatus = NULL;
|
|
|
|
if (sCachedModule) {
|
|
basModuleFree(sCachedModule);
|
|
sCachedModule = NULL;
|
|
}
|
|
|
|
dsgnFree(&sDesigner);
|
|
|
|
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_RUN:
|
|
compileAndRun();
|
|
break;
|
|
|
|
case CMD_RUN_NOCMP:
|
|
runCached();
|
|
break;
|
|
|
|
case CMD_STOP:
|
|
if (sVm) {
|
|
sVm->running = false;
|
|
setStatus("Program stopped.");
|
|
}
|
|
break;
|
|
|
|
case CMD_CLEAR:
|
|
clearOutput();
|
|
break;
|
|
|
|
case CMD_VIEW_CODE:
|
|
switchToCode();
|
|
break;
|
|
|
|
case CMD_VIEW_DESIGN:
|
|
switchToDesign();
|
|
break;
|
|
|
|
case CMD_WIN_CODE:
|
|
showCodeWindow();
|
|
break;
|
|
|
|
case CMD_WIN_OUTPUT:
|
|
showOutputWindow();
|
|
break;
|
|
|
|
case CMD_WIN_IMM:
|
|
showImmediateWindow();
|
|
break;
|
|
|
|
case CMD_WIN_TOOLBOX:
|
|
if (!sToolboxWin) {
|
|
sToolboxWin = tbxCreate(sAc, &sDesigner);
|
|
}
|
|
break;
|
|
|
|
case CMD_WIN_PROPS:
|
|
if (!sPropsWin) {
|
|
sPropsWin = prpCreate(sAc, &sDesigner);
|
|
}
|
|
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("view", "toolbar", show);
|
|
prefsSave();
|
|
}
|
|
break;
|
|
|
|
case CMD_VIEW_STATUS:
|
|
if (sStatusBar && sWin->menuBar) {
|
|
bool show = wmMenuItemIsChecked(sWin->menuBar, CMD_VIEW_STATUS);
|
|
sStatusBar->visible = show;
|
|
dvxFitWindowH(sAc, sWin);
|
|
prefsSetBool("view", "statusbar", show);
|
|
prefsSave();
|
|
}
|
|
break;
|
|
|
|
case CMD_EXIT:
|
|
if (sWin) {
|
|
onClose(sWin);
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
// ============================================================
|
|
// onEvtDropdownChange
|
|
// ============================================================
|
|
//
|
|
// Navigate to the selected procedure when the event dropdown changes.
|
|
|
|
static void onEvtDropdownChange(WidgetT *w) {
|
|
(void)w;
|
|
|
|
if (!sObjDropdown || !sEvtDropdown || !sEditor) {
|
|
return;
|
|
}
|
|
|
|
int32_t objIdx = wgtDropdownGetSelected(sObjDropdown);
|
|
int32_t evtIdx = wgtDropdownGetSelected(sEvtDropdown);
|
|
|
|
if (objIdx < 0 || objIdx >= (int32_t)arrlen(sObjItems) || evtIdx < 0) {
|
|
return;
|
|
}
|
|
|
|
const char *selObj = sObjItems[objIdx];
|
|
|
|
// Find the proc matching the selected object + event
|
|
int32_t matchIdx = 0;
|
|
int32_t procCount = (int32_t)arrlen(sProcTable);
|
|
|
|
for (int32_t i = 0; i < procCount; i++) {
|
|
if (strcasecmp(sProcTable[i].objName, selObj) == 0) {
|
|
if (matchIdx == evtIdx) {
|
|
wgtTextAreaGoToLine(sEditor, sProcTable[i].lineNum);
|
|
return;
|
|
}
|
|
|
|
matchIdx++;
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// 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.
|
|
|
|
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];
|
|
|
|
// Build event list for the selected object
|
|
arrsetlen(sEvtItems, 0);
|
|
|
|
int32_t procCount = (int32_t)arrlen(sProcTable);
|
|
|
|
for (int32_t i = 0; i < procCount; i++) {
|
|
if (strcasecmp(sProcTable[i].objName, selObj) == 0) {
|
|
arrput(sEvtItems, sProcTable[i].evtName);
|
|
}
|
|
}
|
|
|
|
int32_t evtCount = (int32_t)arrlen(sEvtItems);
|
|
wgtDropdownSetItems(sEvtDropdown, sEvtItems, evtCount);
|
|
|
|
if (evtCount > 0) {
|
|
wgtDropdownSetSelected(sEvtDropdown, 0);
|
|
}
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// onOpenClick
|
|
// ============================================================
|
|
|
|
// ============================================================
|
|
// printCallback
|
|
// ============================================================
|
|
|
|
static void printCallback(void *ctx, const char *text, bool newline) {
|
|
(void)ctx;
|
|
|
|
if (!text) {
|
|
return;
|
|
}
|
|
|
|
int32_t textLen = (int32_t)strlen(text);
|
|
|
|
// Append to output buffer
|
|
if (sOutputLen + textLen < IDE_MAX_OUTPUT - 2) {
|
|
memcpy(sOutputBuf + sOutputLen, text, textLen);
|
|
sOutputLen += textLen;
|
|
}
|
|
|
|
if (newline && sOutputLen < IDE_MAX_OUTPUT - 2) {
|
|
sOutputBuf[sOutputLen++] = '\n';
|
|
}
|
|
|
|
sOutputBuf[sOutputLen] = '\0';
|
|
|
|
// Update the output textarea immediately so PRINT is visible
|
|
if (sOutput) {
|
|
wgtSetText(sOutput, 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;
|
|
}
|
|
|
|
|
|
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) {
|
|
int32_t prevCount = sDesigner.form ? (int32_t)arrlen(sDesigner.form->controls) : 0;
|
|
bool wasDirty = sDesigner.form ? sDesigner.form->dirty : false;
|
|
bool drag = wasDown;
|
|
dsgnOnMouse(&sDesigner, x, y, drag);
|
|
|
|
// Rebuild tree if controls were added, removed, or reordered
|
|
int32_t newCount = sDesigner.form ? (int32_t)arrlen(sDesigner.form->controls) : 0;
|
|
bool nowDirty = sDesigner.form ? sDesigner.form->dirty : false;
|
|
|
|
if (newCount != prevCount || (nowDirty && !wasDirty)) {
|
|
prpRebuildTree(&sDesigner);
|
|
}
|
|
|
|
prpRefresh(&sDesigner);
|
|
|
|
if (sFormWin) {
|
|
dvxInvalidateWindow(sAc, sFormWin);
|
|
}
|
|
} else if (wasDown) {
|
|
dsgnOnMouse(&sDesigner, x, y, false);
|
|
prpRefresh(&sDesigner);
|
|
|
|
if (sFormWin) {
|
|
dvxInvalidateWindow(sAc, sFormWin);
|
|
}
|
|
}
|
|
|
|
lastButtons = buttons;
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// onFormWinPaint
|
|
// ============================================================
|
|
//
|
|
// Draw selection handles after widgets have painted.
|
|
|
|
static void onFormWinPaint(WindowT *win, RectT *dirtyArea) {
|
|
if (!win) {
|
|
return;
|
|
}
|
|
|
|
// Force a full measure + layout + paint cycle.
|
|
// widgetOnPaint normally skips relayout if root dimensions haven't
|
|
// changed, but we need it to pick up minH changes from handle drag.
|
|
if (win->widgetRoot) {
|
|
widgetCalcMinSizeTree(win->widgetRoot, &sAc->font);
|
|
win->widgetRoot->w = 0; // force layout pass to re-run
|
|
}
|
|
|
|
widgetOnPaint(win, dirtyArea);
|
|
|
|
// Then draw selection handles on top
|
|
int32_t winX = win->contentX;
|
|
int32_t winY = win->contentY;
|
|
|
|
dsgnPaintOverlay(&sDesigner, winX, winY);
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// onFormWinClose
|
|
// ============================================================
|
|
|
|
static void onFormWinClose(WindowT *win) {
|
|
dvxDestroyWindow(sAc, win);
|
|
sFormWin = NULL;
|
|
sDesigner.formWin = NULL;
|
|
|
|
if (sToolboxWin) {
|
|
tbxDestroy(sAc, sToolboxWin);
|
|
sToolboxWin = NULL;
|
|
}
|
|
|
|
if (sPropsWin) {
|
|
prpDestroy(sAc, sPropsWin);
|
|
sPropsWin = NULL;
|
|
}
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// switchToCode
|
|
// ============================================================
|
|
|
|
static void switchToCode(void) {
|
|
if (sFormWin) {
|
|
onFormWinClose(sFormWin);
|
|
}
|
|
|
|
setStatus("Code view.");
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// switchToDesign
|
|
// ============================================================
|
|
|
|
static void switchToDesign(void) {
|
|
// If already open, just bring to front
|
|
if (sFormWin) {
|
|
return;
|
|
}
|
|
|
|
// Load .frm if we don't have a form yet
|
|
if (!sDesigner.form) {
|
|
if (sFilePath[0]) {
|
|
char frmPath[DVX_MAX_PATH];
|
|
snprintf(frmPath, sizeof(frmPath), "%s", sFilePath);
|
|
char *dot = strrchr(frmPath, '.');
|
|
|
|
if (dot) {
|
|
strcpy(dot, ".frm");
|
|
} else {
|
|
strcat(frmPath, ".frm");
|
|
}
|
|
|
|
FILE *f = fopen(frmPath, "r");
|
|
|
|
if (f) {
|
|
fseek(f, 0, SEEK_END);
|
|
long size = ftell(f);
|
|
fseek(f, 0, SEEK_SET);
|
|
|
|
if (size > 0 && size < IDE_MAX_SOURCE) {
|
|
char *buf = (char *)malloc(size + 1);
|
|
|
|
if (buf) {
|
|
int32_t bytesRead = (int32_t)fread(buf, 1, size, f);
|
|
buf[bytesRead] = '\0';
|
|
dsgnLoadFrm(&sDesigner, buf, bytesRead);
|
|
free(buf);
|
|
}
|
|
}
|
|
|
|
fclose(f);
|
|
}
|
|
}
|
|
|
|
if (!sDesigner.form) {
|
|
dsgnNewForm(&sDesigner, "Form1");
|
|
}
|
|
}
|
|
|
|
// Create the form designer window (same size as runtime)
|
|
const char *formName = sDesigner.form ? sDesigner.form->name : "Form1";
|
|
|
|
char title[128];
|
|
snprintf(title, sizeof(title), "%s [Design]", formName);
|
|
|
|
sFormWin = dvxCreateWindowCentered(sAc, title, IDE_DESIGN_W, IDE_DESIGN_H, true);
|
|
|
|
if (!sFormWin) {
|
|
return;
|
|
}
|
|
|
|
sFormWin->onClose = onFormWinClose;
|
|
sDesigner.formWin = sFormWin;
|
|
|
|
WidgetT *root = wgtInitWindow(sAc, sFormWin);
|
|
WidgetT *contentBox;
|
|
|
|
if (sDesigner.form && strcasecmp(sDesigner.form->layout, "HBox") == 0) {
|
|
contentBox = wgtHBox(root);
|
|
} else {
|
|
contentBox = wgtVBox(root);
|
|
}
|
|
|
|
contentBox->weight = 100;
|
|
|
|
// Override paint and mouse AFTER wgtInitWindow (which sets widgetOnPaint)
|
|
sFormWin->onPaint = onFormWinPaint;
|
|
sFormWin->onMouse = onFormWinMouse;
|
|
sFormWin->onKey = onFormWinKey;
|
|
sFormWin->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 (!sPropsWin) {
|
|
sPropsWin = prpCreate(sAc, &sDesigner);
|
|
}
|
|
|
|
setStatus("Design view open.");
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// Toolbar button handlers
|
|
// ============================================================
|
|
|
|
static void onTbOpen(WidgetT *w) { (void)w; loadFile(); }
|
|
static void onTbSave(WidgetT *w) { (void)w; saveFile(); }
|
|
static void onTbRun(WidgetT *w) { (void)w; compileAndRun(); }
|
|
static void onTbStop(WidgetT *w) { (void)w; if (sVm) { sVm->running = false; setStatus("Program stopped."); } }
|
|
static void onTbCode(WidgetT *w) { (void)w; switchToCode(); }
|
|
static void onTbDesign(WidgetT *w) { (void)w; switchToDesign(); }
|
|
|
|
|
|
// ============================================================
|
|
// showCodeWindow
|
|
// ============================================================
|
|
|
|
static void showCodeWindow(void) {
|
|
if (sCodeWin) {
|
|
return; // already open
|
|
}
|
|
|
|
int32_t codeY = sWin ? sWin->y + sWin->h + 2 : 60;
|
|
int32_t codeH = sAc->display.height - codeY - 122;
|
|
|
|
sCodeWin = dvxCreateWindow(sAc, "Code", 0, codeY, sAc->display.width, codeH, true);
|
|
|
|
if (sCodeWin) {
|
|
sCodeWin->onMenu = onMenu;
|
|
sCodeWin->onFocus = onContentFocus;
|
|
sCodeWin->accelTable = sWin ? sWin->accelTable : NULL;
|
|
sLastFocusWin = sCodeWin;
|
|
|
|
WidgetT *codeRoot = wgtInitWindow(sAc, sCodeWin);
|
|
|
|
WidgetT *dropdownRow = wgtHBox(codeRoot);
|
|
dropdownRow->spacing = wgtPixels(4);
|
|
|
|
sObjDropdown = wgtDropdown(dropdownRow);
|
|
sObjDropdown->weight = 100;
|
|
sObjDropdown->onChange = onObjDropdownChange;
|
|
wgtDropdownSetItems(sObjDropdown, NULL, 0);
|
|
|
|
sEvtDropdown = wgtDropdown(dropdownRow);
|
|
sEvtDropdown->weight = 100;
|
|
sEvtDropdown->onChange = onEvtDropdownChange;
|
|
wgtDropdownSetItems(sEvtDropdown, NULL, 0);
|
|
|
|
sEditor = wgtTextArea(codeRoot, IDE_MAX_SOURCE);
|
|
sEditor->weight = 100;
|
|
wgtTextAreaSetColorize(sEditor, basicColorize, NULL);
|
|
wgtTextAreaSetShowLineNumbers(sEditor, true);
|
|
wgtTextAreaSetAutoIndent(sEditor, true);
|
|
|
|
if (sFilePath[0] && sSourceBuf[0]) {
|
|
wgtSetText(sEditor, sSourceBuf);
|
|
}
|
|
|
|
updateDropdowns();
|
|
}
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// showOutputWindow
|
|
// ============================================================
|
|
|
|
static void showOutputWindow(void) {
|
|
if (sOutWin) {
|
|
return;
|
|
}
|
|
|
|
int32_t outH = 120;
|
|
int32_t outY = sAc->display.height - outH;
|
|
|
|
sOutWin = dvxCreateWindow(sAc, "Output", 0, outY, sAc->display.width / 2, outH, true);
|
|
|
|
if (sOutWin) {
|
|
sOutWin->onFocus = onContentFocus;
|
|
sLastFocusWin = sOutWin;
|
|
|
|
WidgetT *outRoot = wgtInitWindow(sAc, sOutWin);
|
|
sOutput = wgtTextArea(outRoot, IDE_MAX_OUTPUT);
|
|
sOutput->weight = 100;
|
|
sOutput->readOnly = true;
|
|
|
|
if (sOutputLen > 0) {
|
|
wgtSetText(sOutput, sOutputBuf);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// showImmediateWindow
|
|
// ============================================================
|
|
|
|
static void showImmediateWindow(void) {
|
|
if (sImmWin) {
|
|
return;
|
|
}
|
|
|
|
int32_t outH = 120;
|
|
int32_t outY = sAc->display.height - outH;
|
|
|
|
sImmWin = dvxCreateWindow(sAc, "Immediate", sAc->display.width / 2, outY, sAc->display.width / 2, outH, true);
|
|
|
|
if (sImmWin) {
|
|
sImmWin->onFocus = onContentFocus;
|
|
sLastFocusWin = sImmWin;
|
|
|
|
WidgetT *immRoot = wgtInitWindow(sAc, sImmWin);
|
|
|
|
if (immRoot) {
|
|
sImmediate = wgtTextArea(immRoot, IDE_MAX_IMM);
|
|
|
|
if (sImmediate) {
|
|
sImmediate->weight = 100;
|
|
sImmediate->readOnly = false;
|
|
sImmediate->onChange = onImmediateChange;
|
|
} else {
|
|
dvxLog("IDE: failed to create immediate TextArea");
|
|
}
|
|
|
|
} else {
|
|
dvxLog("IDE: failed to init immediate window root");
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// setStatus
|
|
// ============================================================
|
|
|
|
static void setStatus(const char *text) {
|
|
if (sStatus) {
|
|
wgtSetText(sStatus, text);
|
|
}
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// updateDropdowns
|
|
// ============================================================
|
|
//
|
|
// Scan the source for SUB/FUNCTION declarations and populate
|
|
// the Object and Event dropdowns. Procedure names are split on
|
|
// '_' into ObjectName and EventName (e.g. "Command1_Click").
|
|
|
|
static void updateDropdowns(void) {
|
|
// Reset dynamic arrays
|
|
arrsetlen(sProcTable, 0);
|
|
arrsetlen(sObjItems, 0);
|
|
arrsetlen(sEvtItems, 0);
|
|
|
|
if (!sEditor || !sObjDropdown || !sEvtDropdown) {
|
|
return;
|
|
}
|
|
|
|
const char *src = wgtGetText(sEditor);
|
|
|
|
if (!src) {
|
|
return;
|
|
}
|
|
|
|
// Scan line by line for SUB / FUNCTION
|
|
const char *pos = src;
|
|
int32_t lineNum = 1;
|
|
|
|
while (*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;
|
|
|
|
// Skip whitespace after keyword
|
|
while (*pos == ' ' || *pos == '\t') {
|
|
pos++;
|
|
}
|
|
|
|
// Extract procedure name
|
|
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';
|
|
|
|
// Split on '_' into object + event
|
|
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);
|
|
}
|
|
|
|
// Advance to end of line
|
|
while (*pos && *pos != '\n') {
|
|
pos++;
|
|
}
|
|
|
|
if (*pos == '\n') {
|
|
pos++;
|
|
}
|
|
|
|
lineNum++;
|
|
}
|
|
|
|
// Build unique object names for the Object dropdown
|
|
int32_t procCount = (int32_t)arrlen(sProcTable);
|
|
|
|
for (int32_t i = 0; i < procCount; i++) {
|
|
bool found = false;
|
|
int32_t objCount = (int32_t)arrlen(sObjItems);
|
|
|
|
for (int32_t j = 0; j < objCount; j++) {
|
|
if (strcasecmp(sObjItems[j], sProcTable[i].objName) == 0) {
|
|
found = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!found) {
|
|
arrput(sObjItems, sProcTable[i].objName);
|
|
}
|
|
}
|
|
|
|
int32_t objCount = (int32_t)arrlen(sObjItems);
|
|
wgtDropdownSetItems(sObjDropdown, sObjItems, objCount);
|
|
|
|
if (objCount > 0) {
|
|
wgtDropdownSetSelected(sObjDropdown, 0);
|
|
onObjDropdownChange(sObjDropdown);
|
|
} else {
|
|
wgtDropdownSetItems(sEvtDropdown, NULL, 0);
|
|
}
|
|
}
|