5897 lines
174 KiB
C
5897 lines
174 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 "widgetCheckbox.h"
|
|
#include "widgetImageButton.h"
|
|
#include "widgetLabel.h"
|
|
#include "widgetRadio.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 "ideMenuEditor.h"
|
|
#include "ideToolbox.h"
|
|
#include "ideProperties.h"
|
|
|
|
#include "../compiler/parser.h"
|
|
#include "../formrt/formrt.h"
|
|
#include "../../sql/dvxSql.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 CMD_FIND 141
|
|
#define CMD_REPLACE 142
|
|
#define CMD_FIND_NEXT 143
|
|
#define CMD_MENU_EDITOR 144
|
|
#define CMD_PREFERENCES 145
|
|
#define IDE_MAX_IMM 1024
|
|
#define IDE_DESIGN_W 400
|
|
#define IDE_DESIGN_H 300
|
|
|
|
// Syntax color indices (used by basicColorize / classifyWord)
|
|
#define SYNTAX_DEFAULT 0
|
|
#define SYNTAX_KEYWORD 1
|
|
#define SYNTAX_STRING 2
|
|
#define SYNTAX_COMMENT 3
|
|
#define SYNTAX_NUMBER 4
|
|
#define SYNTAX_TYPE 6
|
|
|
|
// View mode for activateFile
|
|
typedef enum {
|
|
ViewAutoE, // .frm -> design, .bas -> code
|
|
ViewCodeE, // force code view
|
|
ViewDesignE // force design view
|
|
} IdeViewModeE;
|
|
|
|
// ============================================================
|
|
// Prototypes
|
|
// ============================================================
|
|
|
|
static void activateFile(int32_t fileIdx, IdeViewModeE view);
|
|
int32_t appMain(DxeAppContextT *ctx);
|
|
static void buildWindow(void);
|
|
static void clearOutput(void);
|
|
static int cmpStrPtrs(const void *a, const void *b);
|
|
static void compileAndRun(void);
|
|
static void ensureProject(const char *filePath);
|
|
static void freeProcBufs(void);
|
|
static const char *getFullSource(void);
|
|
static void loadFile(void);
|
|
static void loadFormCodeIntoEditor(void);
|
|
static void parseProcs(const char *source);
|
|
static void updateProjectMenuState(void);
|
|
static void saveActiveFile(void);
|
|
static bool saveCurProc(void);
|
|
static void stashCurrentFile(void);
|
|
static void stashFormCode(void);
|
|
static void showProc(int32_t procIdx);
|
|
static int32_t toolbarBottom(void);
|
|
static void newProject(void);
|
|
static void 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 closeFindDialog(void);
|
|
static bool findInProject(const char *needle, bool caseSensitive);
|
|
static void openFindDialog(bool showReplace);
|
|
static void handleEditCmd(int32_t cmd);
|
|
static void handleFileCmd(int32_t cmd);
|
|
static void handleProjectCmd(int32_t cmd);
|
|
static void handleRunCmd(int32_t cmd);
|
|
static void handleViewCmd(int32_t cmd);
|
|
static void handleWindowCmd(int32_t cmd);
|
|
static void onMenu(WindowT *win, int32_t menuId);
|
|
static void basicColorize(const char *line, int32_t lineLen, uint8_t *colors, void *ctx);
|
|
static 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 stashDesignerState(void);
|
|
static void teardownFormWin(void);
|
|
static void updateDirtyIndicators(void);
|
|
static void switchToDesign(void);
|
|
static int32_t onFormWinCursorQuery(WindowT *win, int32_t x, int32_t y);
|
|
static void onFormWinKey(WindowT *win, int32_t key, int32_t mod);
|
|
static void onFormWinMouse(WindowT *win, int32_t x, int32_t y, int32_t buttons);
|
|
static void onFormWinPaint(WindowT *win, RectT *dirtyArea);
|
|
static void onTbRun(WidgetT *w);
|
|
static void showCodeWindow(void);
|
|
static void showOutputWindow(void);
|
|
static void showImmediateWindow(void);
|
|
static void onTbStop(WidgetT *w);
|
|
static void onTbOpen(WidgetT *w);
|
|
static void onTbCode(WidgetT *w);
|
|
static void onTbDesign(WidgetT *w);
|
|
static void 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)
|
|
|
|
// Find/Replace state
|
|
static char sFindText[256] = "";
|
|
static char sReplaceText[256] = "";
|
|
// Find/Replace dialog state (modeless)
|
|
static WindowT *sFindWin = NULL;
|
|
static WidgetT *sFindInput = NULL;
|
|
static WidgetT *sReplInput = NULL;
|
|
static WidgetT *sReplCheck = NULL;
|
|
static WidgetT *sBtnReplace = NULL;
|
|
static WidgetT *sBtnReplAll = NULL;
|
|
static WidgetT *sCaseCheck = NULL;
|
|
static WidgetT *sScopeGroup = NULL; // radio group: 0=Func, 1=Obj, 2=File, 3=Proj
|
|
static WidgetT *sDirGroup = NULL; // radio group: 0=Fwd, 1=Back
|
|
|
|
// Procedure table for Object/Event dropdowns
|
|
typedef struct {
|
|
char objName[64];
|
|
char evtName[64];
|
|
int32_t lineNum;
|
|
} IdeProcEntryT;
|
|
|
|
static IdeProcEntryT *sProcTable = NULL; // stb_ds dynamic array
|
|
static const char **sObjItems = NULL; // stb_ds dynamic array
|
|
static const char **sEvtItems = NULL; // stb_ds dynamic array
|
|
static bool sDropdownNavSuppressed = false;
|
|
static bool sStopRequested = false;
|
|
|
|
// ============================================================
|
|
// App descriptor
|
|
// ============================================================
|
|
|
|
AppDescriptorT appDescriptor = {
|
|
.name = "DVX BASIC",
|
|
.hasMainLoop = false,
|
|
.multiInstance = false,
|
|
.stackSize = SHELL_STACK_DEFAULT,
|
|
.priority = 0
|
|
};
|
|
|
|
// ============================================================
|
|
// activateFile -- central file-switching function
|
|
// ============================================================
|
|
|
|
static void activateFile(int32_t fileIdx, IdeViewModeE view) {
|
|
if (fileIdx < 0 || fileIdx >= sProject.fileCount) {
|
|
return;
|
|
}
|
|
|
|
PrjFileT *target = &sProject.files[fileIdx];
|
|
|
|
// Resolve ViewAutoE
|
|
if (view == ViewAutoE) {
|
|
view = target->isForm ? ViewDesignE : ViewCodeE;
|
|
}
|
|
|
|
// If already active, just ensure the right view is showing
|
|
if (fileIdx == sProject.activeFileIdx) {
|
|
if (view == ViewDesignE) {
|
|
switchToDesign();
|
|
} else if (view == ViewCodeE) {
|
|
if (sCodeWin) {
|
|
dvxRaiseWindow(sAc, sCodeWin);
|
|
} else {
|
|
// Code window was closed — reopen it
|
|
showCodeWindow();
|
|
|
|
if (sCodeWin) {
|
|
dvxRaiseWindow(sAc, sCodeWin);
|
|
}
|
|
|
|
// Reload the file content
|
|
if (sEditor) {
|
|
const char *source = target->buffer;
|
|
|
|
if (source) {
|
|
parseProcs(source);
|
|
updateDropdowns();
|
|
showProc(-1);
|
|
sEditor->onChange = onEditorChange;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
|
|
// PHASE 1: Stash the outgoing file
|
|
stashCurrentFile();
|
|
|
|
// PHASE 2: Load the incoming file
|
|
if (target->isForm) {
|
|
// Load form into designer
|
|
const char *frmSrc = target->buffer;
|
|
char *diskBuf = NULL;
|
|
|
|
if (!frmSrc) {
|
|
// Try disk
|
|
char fullPath[DVX_MAX_PATH];
|
|
prjFullPath(&sProject, fileIdx, fullPath, sizeof(fullPath));
|
|
FILE *f = fopen(fullPath, "r");
|
|
|
|
if (f) {
|
|
fseek(f, 0, SEEK_END);
|
|
long size = ftell(f);
|
|
fseek(f, 0, SEEK_SET);
|
|
|
|
if (size > 0 && size < IDE_MAX_SOURCE) {
|
|
diskBuf = (char *)malloc(size + 1);
|
|
|
|
if (diskBuf) {
|
|
int32_t br = (int32_t)fread(diskBuf, 1, size, f);
|
|
diskBuf[br] = '\0';
|
|
frmSrc = diskBuf;
|
|
}
|
|
}
|
|
|
|
fclose(f);
|
|
}
|
|
}
|
|
|
|
teardownFormWin();
|
|
dsgnFree(&sDesigner);
|
|
|
|
if (frmSrc) {
|
|
dsgnLoadFrm(&sDesigner, frmSrc, (int32_t)strlen(frmSrc));
|
|
free(diskBuf);
|
|
} else {
|
|
// New blank form -- derive name from filename
|
|
char formName[PRJ_MAX_NAME];
|
|
const char *base = strrchr(target->path, '/');
|
|
const char *base2 = strrchr(target->path, '\\');
|
|
|
|
if (base2 > base) {
|
|
base = base2;
|
|
}
|
|
|
|
base = base ? base + 1 : target->path;
|
|
int32_t bl = (int32_t)strlen(base);
|
|
|
|
if (bl >= PRJ_MAX_NAME) {
|
|
bl = PRJ_MAX_NAME - 1;
|
|
}
|
|
|
|
memcpy(formName, base, bl);
|
|
formName[bl] = '\0';
|
|
char *dot = strrchr(formName, '.');
|
|
|
|
if (dot) {
|
|
*dot = '\0';
|
|
}
|
|
|
|
dsgnNewForm(&sDesigner, formName);
|
|
target->modified = true;
|
|
}
|
|
|
|
if (sDesigner.form) {
|
|
snprintf(target->formName, sizeof(target->formName), "%s", sDesigner.form->name);
|
|
}
|
|
|
|
sProject.activeFileIdx = fileIdx;
|
|
|
|
if (view == ViewDesignE) {
|
|
switchToDesign();
|
|
} else {
|
|
loadFormCodeIntoEditor();
|
|
}
|
|
} else {
|
|
// Load .bas into editor
|
|
if (!sCodeWin) {
|
|
showCodeWindow();
|
|
}
|
|
|
|
if (sCodeWin) {
|
|
dvxRaiseWindow(sAc, sCodeWin);
|
|
}
|
|
|
|
const char *source = target->buffer;
|
|
char *diskBuf = NULL;
|
|
|
|
|
|
if (!source) {
|
|
char fullPath[DVX_MAX_PATH];
|
|
prjFullPath(&sProject, fileIdx, fullPath, sizeof(fullPath));
|
|
FILE *f = fopen(fullPath, "r");
|
|
|
|
if (f) {
|
|
fseek(f, 0, SEEK_END);
|
|
long size = ftell(f);
|
|
fseek(f, 0, SEEK_SET);
|
|
|
|
if (size > 0 && size < IDE_MAX_SOURCE) {
|
|
diskBuf = (char *)malloc(size + 1);
|
|
|
|
if (diskBuf) {
|
|
int32_t br = (int32_t)fread(diskBuf, 1, size, f);
|
|
diskBuf[br] = '\0';
|
|
source = diskBuf;
|
|
}
|
|
}
|
|
|
|
fclose(f);
|
|
}
|
|
}
|
|
|
|
if (!source) {
|
|
source = "";
|
|
target->modified = true;
|
|
}
|
|
|
|
parseProcs(source);
|
|
free(diskBuf);
|
|
updateDropdowns();
|
|
showProc(-1);
|
|
|
|
if (sEditor && !sEditor->onChange) {
|
|
sEditor->onChange = onEditorChange;
|
|
}
|
|
|
|
sEditorFileIdx = fileIdx;
|
|
sProject.activeFileIdx = fileIdx;
|
|
}
|
|
|
|
updateDirtyIndicators();
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// appMain
|
|
// ============================================================
|
|
|
|
int32_t appMain(DxeAppContextT *ctx) {
|
|
sCtx = ctx;
|
|
sAc = ctx->shellCtx;
|
|
|
|
basStringSystemInit();
|
|
prjInit(&sProject);
|
|
buildWindow();
|
|
|
|
// Load persisted settings
|
|
shellEnsureConfigDir(sCtx);
|
|
char prefsPath[DVX_MAX_PATH];
|
|
shellConfigPath(sCtx, "dvxbasic.ini", prefsPath, sizeof(prefsPath));
|
|
sPrefs = prefsLoad(prefsPath);
|
|
|
|
if (!sPrefs) {
|
|
sPrefs = prefsCreate();
|
|
prefsSaveAs(sPrefs, prefsPath);
|
|
}
|
|
|
|
if (sToolbar && sWin && sWin->menuBar) {
|
|
bool showTb = prefsGetBool(sPrefs, "view", "toolbar", true);
|
|
sToolbar->visible = showTb;
|
|
wmMenuItemSetChecked(sWin->menuBar, CMD_VIEW_TOOLBAR, showTb);
|
|
}
|
|
|
|
if (sStatusBar && sWin && sWin->menuBar) {
|
|
bool showSb = prefsGetBool(sPrefs, "view", "statusbar", true);
|
|
sStatusBar->visible = showSb;
|
|
wmMenuItemSetChecked(sWin->menuBar, CMD_VIEW_STATUS, showSb);
|
|
}
|
|
|
|
if (sWin && sWin->menuBar) {
|
|
bool saveOnRun = prefsGetBool(sPrefs, "run", "saveOnRun", true);
|
|
wmMenuItemSetChecked(sWin->menuBar, CMD_SAVE_ON_RUN, saveOnRun);
|
|
}
|
|
|
|
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;
|
|
sProjectWin->onMenu = onMenu;
|
|
sProjectWin->accelTable = sWin ? sWin->accelTable : NULL;
|
|
}
|
|
|
|
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);
|
|
wmAddMenuSeparator(editMenu);
|
|
wmAddMenuItem(editMenu, "&Find...\tCtrl+F", CMD_FIND);
|
|
wmAddMenuItem(editMenu, "Find &Next\tF3", CMD_FIND_NEXT);
|
|
wmAddMenuItem(editMenu, "&Replace...\tCtrl+H", CMD_REPLACE);
|
|
|
|
MenuT *runMenu = wmAddMenu(menuBar, "&Run");
|
|
wmAddMenuItem(runMenu, "&Run\tF5", CMD_RUN);
|
|
wmAddMenuItem(runMenu, "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);
|
|
wmAddMenuSeparator(viewMenu);
|
|
wmAddMenuItem(viewMenu, "&Menu Editor...\tCtrl+E", CMD_MENU_EDITOR);
|
|
|
|
MenuT *winMenu = wmAddMenu(menuBar, "&Window");
|
|
wmAddMenuItem(winMenu, "&Code Editor", CMD_WIN_CODE);
|
|
wmAddMenuItem(winMenu, "&Output", CMD_WIN_OUTPUT);
|
|
wmAddMenuItem(winMenu, "&Immediate", CMD_WIN_IMM);
|
|
wmAddMenuSeparator(winMenu);
|
|
wmAddMenuItem(winMenu, "&Project Explorer", CMD_WIN_PROJECT);
|
|
wmAddMenuItem(winMenu, "&Toolbox", CMD_WIN_TOOLBOX);
|
|
wmAddMenuItem(winMenu, "&Properties", CMD_WIN_PROPS);
|
|
|
|
MenuT *toolsMenu = wmAddMenu(menuBar, "&Tools");
|
|
wmAddMenuItem(toolsMenu, "&Preferences...", CMD_PREFERENCES);
|
|
|
|
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);
|
|
dvxAddAccel(accel, 'F', ACCEL_CTRL, CMD_FIND);
|
|
dvxAddAccel(accel, 'H', ACCEL_CTRL, CMD_REPLACE);
|
|
dvxAddAccel(accel, KEY_F3, 0, CMD_FIND_NEXT);
|
|
dvxAddAccel(accel, 'E', ACCEL_CTRL, CMD_MENU_EDITOR);
|
|
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_KEYWORD or SYNTAX_TYPE.
|
|
// Built once on first use, then O(1) per lookup.
|
|
|
|
typedef struct {
|
|
char *key;
|
|
uint8_t value;
|
|
} SyntaxMapEntryT;
|
|
|
|
static SyntaxMapEntryT *sSyntaxMap = NULL;
|
|
|
|
|
|
static void initSyntaxMap(void) {
|
|
if (sSyntaxMap) {
|
|
return;
|
|
}
|
|
|
|
sh_new_arena(sSyntaxMap);
|
|
|
|
static const char *keywords[] = {
|
|
"AND", "AS", "BYVAL", "CALL", "CASE", "CLOSE", "CONST",
|
|
"DATA", "DECLARE", "DEF", "DEFDBL", "DEFINT", "DEFLNG",
|
|
"DEFSNG", "DEFSTR", "DIM", "DO", "DOEVENTS",
|
|
"ELSE", "ELSEIF", "END", "ERASE", "EXIT",
|
|
"FOR", "FUNCTION",
|
|
"GET", "GOSUB", "GOTO",
|
|
"HIDE",
|
|
"IF", "IMP", "INPUT", "IS",
|
|
"LET", "LIBRARY", "LINE", "LOAD", "LOOP",
|
|
"ME", "MOD", "MSGBOX",
|
|
"NEXT", "NOT",
|
|
"ON", "OPEN", "OPTION", "OR",
|
|
"PRINT", "PUT",
|
|
"RANDOMIZE", "READ", "REDIM", "RESTORE", "RESUME", "RETURN",
|
|
"SEEK", "SELECT", "SHARED", "SHELL", "SHOW", "SLEEP",
|
|
"STATIC", "STEP", "STOP", "SUB", "SWAP",
|
|
"THEN", "TO", "TYPE",
|
|
"UNLOAD", "UNTIL",
|
|
"WEND", "WHILE", "WRITE",
|
|
"XOR",
|
|
NULL
|
|
};
|
|
|
|
static const char *types[] = {
|
|
"BOOLEAN", "BYTE", "DOUBLE", "FALSE", "INTEGER",
|
|
"LONG", "SINGLE", "STRING", "TRUE",
|
|
NULL
|
|
};
|
|
|
|
for (int32_t i = 0; keywords[i]; i++) {
|
|
shput(sSyntaxMap, keywords[i], SYNTAX_KEYWORD);
|
|
}
|
|
|
|
for (int32_t i = 0; types[i]; i++) {
|
|
shput(sSyntaxMap, types[i], SYNTAX_TYPE);
|
|
}
|
|
}
|
|
|
|
|
|
// classifyWord -- returns syntax color for an identifier.
|
|
// Converts to uppercase once, then does a single hash lookup.
|
|
|
|
static uint8_t classifyWord(const char *word, int32_t wordLen) {
|
|
char upper[32];
|
|
|
|
if (wordLen <= 0 || wordLen >= 32) {
|
|
return SYNTAX_DEFAULT;
|
|
}
|
|
|
|
for (int32_t i = 0; i < wordLen; i++) {
|
|
upper[i] = (char)toupper((unsigned char)word[i]);
|
|
}
|
|
|
|
upper[wordLen] = '\0';
|
|
|
|
initSyntaxMap();
|
|
|
|
int32_t idx = shgeti(sSyntaxMap, upper);
|
|
|
|
if (idx >= 0) {
|
|
return sSyntaxMap[idx].value;
|
|
}
|
|
|
|
return SYNTAX_DEFAULT;
|
|
}
|
|
|
|
|
|
static void basicColorize(const char *line, int32_t lineLen, uint8_t *colors, void *ctx) {
|
|
(void)ctx;
|
|
int32_t i = 0;
|
|
|
|
while (i < lineLen) {
|
|
char ch = line[i];
|
|
|
|
// Comment: ' or REM
|
|
if (ch == '\'') {
|
|
while (i < lineLen) {
|
|
colors[i++] = SYNTAX_COMMENT;
|
|
}
|
|
return;
|
|
}
|
|
|
|
// String literal
|
|
if (ch == '"') {
|
|
colors[i++] = SYNTAX_STRING;
|
|
|
|
while (i < lineLen && line[i] != '"') {
|
|
colors[i++] = SYNTAX_STRING;
|
|
}
|
|
|
|
if (i < lineLen) {
|
|
colors[i++] = SYNTAX_STRING;
|
|
}
|
|
|
|
continue;
|
|
}
|
|
|
|
// Number
|
|
if (isdigit((unsigned char)ch) || (ch == '.' && i + 1 < lineLen && isdigit((unsigned char)line[i + 1]))) {
|
|
while (i < lineLen && (isdigit((unsigned char)line[i]) || line[i] == '.')) {
|
|
colors[i++] = SYNTAX_NUMBER;
|
|
}
|
|
|
|
continue;
|
|
}
|
|
|
|
// Identifier or keyword
|
|
if (isalpha((unsigned char)ch) || ch == '_') {
|
|
int32_t start = i;
|
|
|
|
while (i < lineLen && (isalnum((unsigned char)line[i]) || line[i] == '_' || line[i] == '$' || line[i] == '%' || line[i] == '&' || line[i] == '!' || line[i] == '#')) {
|
|
i++;
|
|
}
|
|
|
|
int32_t wordLen = i - start;
|
|
|
|
// Check for REM comment
|
|
if (wordLen == 3 && (line[start] == 'R' || line[start] == 'r') && (line[start + 1] == 'E' || line[start + 1] == 'e') && (line[start + 2] == 'M' || line[start + 2] == 'm')) {
|
|
for (int32_t j = start; j < lineLen; j++) {
|
|
colors[j] = SYNTAX_COMMENT;
|
|
}
|
|
return;
|
|
}
|
|
|
|
uint8_t c = classifyWord(line + start, wordLen);
|
|
|
|
for (int32_t j = start; j < i; j++) {
|
|
colors[j] = c;
|
|
}
|
|
|
|
continue;
|
|
}
|
|
|
|
// Operators
|
|
if (ch == '=' || ch == '<' || ch == '>' || ch == '+' || ch == '-' || ch == '*' || ch == '/' || ch == '\\' || ch == '&') {
|
|
colors[i++] = 5; // SYNTAX_OPERATOR
|
|
continue;
|
|
}
|
|
|
|
// Default (whitespace, parens, etc.)
|
|
colors[i++] = 0;
|
|
}
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// clearOutput
|
|
// ============================================================
|
|
|
|
static void setOutputText(const char *text) {
|
|
if (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...");
|
|
dvxSetBusy(sAc, true);
|
|
dvxUpdate(sAc);
|
|
|
|
// Build source: either concatenate project files or use editor contents
|
|
char *concatBuf = NULL;
|
|
const char *src = NULL;
|
|
int32_t srcLen = 0;
|
|
|
|
if (sProject.projectPath[0] != '\0' && sProject.fileCount > 0) {
|
|
// Stash current editor/designer state into project buffers
|
|
stashCurrentFile();
|
|
stashFormCode(); // also stash form code if editor has it
|
|
|
|
// Concatenate all .bas files from buffers (or disk if not yet loaded)
|
|
concatBuf = (char *)malloc(IDE_MAX_SOURCE);
|
|
|
|
if (!concatBuf) {
|
|
setStatus("Out of memory.");
|
|
dvxSetBusy(sAc, false);
|
|
return;
|
|
}
|
|
|
|
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;
|
|
|
|
// Inject BEGINFORM directive for .frm code sections
|
|
if (sProject.files[i].isForm && sProject.files[i].formName[0]) {
|
|
int32_t dirLen = snprintf(concatBuf + pos, IDE_MAX_SOURCE - pos,
|
|
"BEGINFORM \"%s\"\n", sProject.files[i].formName);
|
|
pos += dirLen;
|
|
line++;
|
|
}
|
|
|
|
int32_t fileLen = (int32_t)strlen(fileSrc);
|
|
int32_t copyLen = fileLen;
|
|
|
|
if (pos + copyLen >= IDE_MAX_SOURCE - 64) {
|
|
copyLen = IDE_MAX_SOURCE - 64 - pos;
|
|
}
|
|
|
|
memcpy(concatBuf + pos, fileSrc, copyLen);
|
|
pos += copyLen;
|
|
|
|
// Count lines
|
|
for (int32_t j = 0; j < copyLen; j++) {
|
|
if (fileSrc[j] == '\n') {
|
|
line++;
|
|
}
|
|
}
|
|
|
|
free(diskBuf);
|
|
|
|
// Ensure a trailing newline between files
|
|
if (copyLen > 0 && concatBuf[pos - 1] != '\n' && pos < IDE_MAX_SOURCE - 1) {
|
|
concatBuf[pos++] = '\n';
|
|
line++;
|
|
}
|
|
|
|
// Inject ENDFORM directive
|
|
if (sProject.files[i].isForm && sProject.files[i].formName[0]) {
|
|
int32_t dirLen = snprintf(concatBuf + pos, IDE_MAX_SOURCE - pos, "ENDFORM\n");
|
|
pos += dirLen;
|
|
line++;
|
|
}
|
|
|
|
{
|
|
PrjSourceMapT mapEntry;
|
|
mapEntry.fileIdx = i;
|
|
mapEntry.startLine = startLine;
|
|
mapEntry.lineCount = line - startLine;
|
|
arrput(sProject.sourceMap, mapEntry);
|
|
sProject.sourceMapCount = (int32_t)arrlen(sProject.sourceMap);
|
|
}
|
|
|
|
dvxUpdate(sAc);
|
|
}
|
|
|
|
concatBuf[pos] = '\0';
|
|
src = concatBuf;
|
|
srcLen = pos;
|
|
} else {
|
|
// No project files -- compile the full source
|
|
src = getFullSource();
|
|
|
|
if (!src || *src == '\0') {
|
|
setStatus("No source code to 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.");
|
|
dvxSetBusy(sAc, false);
|
|
return;
|
|
}
|
|
|
|
basParserInit(parser, src, srcLen);
|
|
parser->optionExplicit = prefsGetBool(sPrefs, "editor", "optionExplicit", false);
|
|
|
|
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.");
|
|
dvxSetBusy(sAc, false);
|
|
basParserFree(parser);
|
|
free(parser);
|
|
free(concatBuf);
|
|
return;
|
|
}
|
|
|
|
free(concatBuf);
|
|
|
|
BasModuleT *mod = basParserBuildModule(parser);
|
|
basParserFree(parser);
|
|
free(parser);
|
|
|
|
if (!mod) {
|
|
setStatus("Failed to build module.");
|
|
dvxSetBusy(sAc, false);
|
|
return;
|
|
}
|
|
|
|
dvxSetBusy(sAc, false);
|
|
|
|
// 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...");
|
|
|
|
closeFindDialog();
|
|
|
|
// 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) { dvxHideWindow(sAc, sFormWin); }
|
|
if (sToolboxWin) { dvxHideWindow(sAc, sToolboxWin); }
|
|
if (sPropsWin) { dvxHideWindow(sAc, sPropsWin); }
|
|
if (sCodeWin) { dvxHideWindow(sAc, sCodeWin); }
|
|
if (sProjectWin) { dvxHideWindow(sAc, sProjectWin); }
|
|
|
|
// Create VM
|
|
BasVmT *vm = basVmCreate();
|
|
basVmLoadModule(vm, mod);
|
|
|
|
// Set 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);
|
|
|
|
// Set SQL callbacks
|
|
BasSqlCallbacksT sqlCb;
|
|
memset(&sqlCb, 0, sizeof(sqlCb));
|
|
sqlCb.sqlOpen = dvxSqlOpen;
|
|
sqlCb.sqlClose = dvxSqlClose;
|
|
sqlCb.sqlExec = dvxSqlExec;
|
|
sqlCb.sqlError = dvxSqlError;
|
|
sqlCb.sqlQuery = dvxSqlQuery;
|
|
sqlCb.sqlNext = dvxSqlNext;
|
|
sqlCb.sqlEof = dvxSqlEof;
|
|
sqlCb.sqlFieldCount = dvxSqlFieldCount;
|
|
sqlCb.sqlFieldName = dvxSqlFieldName;
|
|
sqlCb.sqlFieldText = dvxSqlFieldText;
|
|
sqlCb.sqlFieldByName = dvxSqlFieldByName;
|
|
sqlCb.sqlFieldInt = dvxSqlFieldInt;
|
|
sqlCb.sqlFieldDbl = dvxSqlFieldDbl;
|
|
sqlCb.sqlFreeResult = dvxSqlFreeResult;
|
|
sqlCb.sqlAffectedRows = dvxSqlAffectedRows;
|
|
basVmSetSqlCallbacks(vm, &sqlCb);
|
|
|
|
// 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) { dvxShowWindow(sAc, sFormWin); }
|
|
if (hadToolbox && sToolboxWin) { dvxShowWindow(sAc, sToolboxWin); }
|
|
if (hadProps && sPropsWin) { dvxShowWindow(sAc, sPropsWin); }
|
|
if (hadCodeWin && sCodeWin) { dvxShowWindow(sAc, sCodeWin); }
|
|
if (hadPrjWin && sProjectWin) { dvxShowWindow(sAc, sProjectWin); }
|
|
|
|
// Repaint to clear destroyed runtime forms and restore designer
|
|
dvxUpdate(sAc);
|
|
}
|
|
|
|
// ============================================================
|
|
// doEventsCallback
|
|
// ============================================================
|
|
|
|
static bool doEventsCallback(void *ctx) {
|
|
(void)ctx;
|
|
|
|
// Stop if IDE window was closed or DVX is shutting down
|
|
if (!sWin || !sAc->running) {
|
|
return false;
|
|
}
|
|
|
|
dvxUpdate(sAc);
|
|
|
|
return sWin != NULL && sAc->running;
|
|
}
|
|
|
|
// ============================================================
|
|
// evaluateImmediate
|
|
// ============================================================
|
|
//
|
|
// Compile and execute a single line from the Immediate window.
|
|
// If the line doesn't start with PRINT, wrap it in PRINT so
|
|
// expressions produce visible output.
|
|
|
|
static void immPrintCallback(void *ctx, const char *text, bool newline) {
|
|
(void)ctx;
|
|
|
|
if (!sImmediate) {
|
|
return;
|
|
}
|
|
|
|
// Append output to the immediate window
|
|
const char *cur = wgtGetText(sImmediate);
|
|
int32_t curLen = cur ? (int32_t)strlen(cur) : 0;
|
|
int32_t textLen = text ? (int32_t)strlen(text) : 0;
|
|
|
|
if (curLen + textLen + 2 < IDE_MAX_IMM) {
|
|
static char immBuf[IDE_MAX_IMM];
|
|
memcpy(immBuf, cur, curLen);
|
|
memcpy(immBuf + curLen, text, textLen);
|
|
curLen += textLen;
|
|
|
|
if (newline) {
|
|
immBuf[curLen++] = '\n';
|
|
}
|
|
|
|
immBuf[curLen] = '\0';
|
|
wgtSetText(sImmediate, immBuf);
|
|
|
|
// Move cursor to end so user can keep typing
|
|
int32_t lines = 1;
|
|
|
|
for (int32_t i = 0; i < curLen; i++) {
|
|
if (immBuf[i] == '\n') {
|
|
lines++;
|
|
}
|
|
}
|
|
|
|
wgtTextAreaGoToLine(sImmediate, lines);
|
|
}
|
|
}
|
|
|
|
|
|
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);
|
|
}
|
|
|
|
|
|
// Auto-create an implicit project when opening a file without one.
|
|
// Derives project name and directory from the file path. Also adds
|
|
// a matching .frm file if one exists alongside the .bas.
|
|
|
|
static void ensureProject(const char *filePath) {
|
|
if (sProject.projectPath[0] != '\0') {
|
|
return;
|
|
}
|
|
|
|
// Derive directory and base name from path
|
|
char dir[DVX_MAX_PATH];
|
|
char baseName[PRJ_MAX_NAME];
|
|
snprintf(dir, sizeof(dir), "%s", filePath);
|
|
|
|
char *sep = strrchr(dir, '/');
|
|
char *sep2 = strrchr(dir, '\\');
|
|
|
|
if (sep2 > sep) {
|
|
sep = sep2;
|
|
}
|
|
|
|
const char *fileName = filePath;
|
|
|
|
if (sep) {
|
|
fileName = sep + 1;
|
|
*sep = '\0';
|
|
} else {
|
|
dir[0] = '.';
|
|
dir[1] = '\0';
|
|
}
|
|
|
|
// Strip extension for project name
|
|
// Length-clamped memcpy instead of strncpy/snprintf because
|
|
// GCC warns about both when source (DVX_MAX_PATH) exceeds
|
|
// the buffer (PRJ_MAX_NAME), even though truncation is safe.
|
|
int32_t nl = (int32_t)strlen(fileName);
|
|
|
|
if (nl >= PRJ_MAX_NAME) {
|
|
nl = PRJ_MAX_NAME - 1;
|
|
}
|
|
|
|
memcpy(baseName, fileName, nl);
|
|
baseName[nl] = '\0';
|
|
|
|
char *dot = strrchr(baseName, '.');
|
|
|
|
if (dot) {
|
|
*dot = '\0';
|
|
}
|
|
|
|
prjNew(&sProject, baseName, dir, sPrefs);
|
|
|
|
// Determine if this is a .bas or .frm
|
|
const char *ext = strrchr(filePath, '.');
|
|
bool isForm = (ext && strcasecmp(ext, ".frm") == 0);
|
|
|
|
prjAddFile(&sProject, fileName, isForm);
|
|
|
|
// If it's a .bas, check for a matching .frm and add it too
|
|
if (!isForm) {
|
|
char frmPath[DVX_MAX_PATH];
|
|
snprintf(frmPath, sizeof(frmPath), "%s", filePath);
|
|
char *frmDot = strrchr(frmPath, '.');
|
|
|
|
if (frmDot && (frmDot - frmPath) + 4 < DVX_MAX_PATH) {
|
|
snprintf(frmDot, sizeof(frmPath) - (frmDot - frmPath), ".frm");
|
|
} else if ((int32_t)strlen(frmPath) + 4 < DVX_MAX_PATH) {
|
|
snprintf(frmPath + strlen(frmPath), sizeof(frmPath) - strlen(frmPath), ".frm");
|
|
}
|
|
|
|
FILE *frmFile = fopen(frmPath, "r");
|
|
|
|
if (frmFile) {
|
|
fclose(frmFile);
|
|
|
|
// Get just the filename portion
|
|
const char *frmName = strrchr(frmPath, '/');
|
|
const char *frmName2 = strrchr(frmPath, '\\');
|
|
|
|
if (frmName2 > frmName) {
|
|
frmName = frmName2;
|
|
}
|
|
|
|
frmName = frmName ? frmName + 1 : frmPath;
|
|
prjAddFile(&sProject, frmName, true);
|
|
}
|
|
}
|
|
|
|
sProject.dirty = false;
|
|
sProject.activeFileIdx = -1;
|
|
|
|
prjLoadAllFiles(&sProject, sAc);
|
|
|
|
char title[300];
|
|
snprintf(title, sizeof(title), "DVX BASIC - [%s]", sProject.name);
|
|
|
|
if (sWin) {
|
|
dvxSetTitle(sAc, sWin, title);
|
|
}
|
|
|
|
setStatus("Project created.");
|
|
updateProjectMenuState();
|
|
}
|
|
|
|
|
|
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);
|
|
activateFile(sProject.fileCount - 1, ViewAutoE);
|
|
} else {
|
|
// No project -- create one from this file
|
|
if (!promptAndSave()) {
|
|
return;
|
|
}
|
|
|
|
ensureProject(path);
|
|
|
|
if (!sProjectWin) {
|
|
sProjectWin = prjCreateWindow(sAc, &sProject, onPrjFileClick);
|
|
|
|
if (sProjectWin) {
|
|
sProjectWin->y = toolbarBottom() + 25;
|
|
sProjectWin->onClose = onProjectWinClose;
|
|
dvxRaiseWindow(sAc, sProjectWin);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// ============================================================
|
|
// saveFile
|
|
// ============================================================
|
|
|
|
static void saveActiveFile(void) {
|
|
if (sProject.projectPath[0] == '\0') {
|
|
return;
|
|
}
|
|
|
|
int32_t idx = sProject.activeFileIdx;
|
|
|
|
if (idx < 0 || idx >= sProject.fileCount) {
|
|
return;
|
|
}
|
|
|
|
// Ensure buffer is up-to-date with editor/designer state
|
|
stashCurrentFile();
|
|
|
|
PrjFileT *file = &sProject.files[idx];
|
|
char fullPath[DVX_MAX_PATH];
|
|
prjFullPath(&sProject, idx, fullPath, sizeof(fullPath));
|
|
|
|
if (file->buffer) {
|
|
FILE *f = fopen(fullPath, "w");
|
|
|
|
if (f) {
|
|
fputs(file->buffer, f);
|
|
fclose(f);
|
|
file->modified = false;
|
|
|
|
if (file->isForm && sDesigner.form) {
|
|
sDesigner.form->dirty = false;
|
|
}
|
|
} else {
|
|
dvxMessageBox(sAc, "Error", "Could not write file.", MB_OK | MB_ICONERROR);
|
|
return;
|
|
}
|
|
}
|
|
|
|
setStatus("Saved.");
|
|
updateDirtyIndicators();
|
|
}
|
|
|
|
static void saveFile(void) {
|
|
if (sProject.projectPath[0] == '\0' || sProject.activeFileIdx < 0) {
|
|
return;
|
|
}
|
|
|
|
// Save the active project file
|
|
saveActiveFile();
|
|
|
|
// Also save any other dirty forms
|
|
for (int32_t i = 0; i < sProject.fileCount; i++) {
|
|
if (i == sProject.activeFileIdx) {
|
|
continue;
|
|
}
|
|
|
|
if (sProject.files[i].isForm && sProject.files[i].modified && sProject.files[i].buffer) {
|
|
char fullPath[DVX_MAX_PATH];
|
|
prjFullPath(&sProject, i, fullPath, sizeof(fullPath));
|
|
|
|
FILE *f = fopen(fullPath, "w");
|
|
|
|
if (f) {
|
|
fputs(sProject.files[i].buffer, f);
|
|
fclose(f);
|
|
sProject.files[i].modified = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
updateDirtyIndicators();
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// onPrjFileClick -- called when a file is clicked in the project tree
|
|
// ============================================================
|
|
|
|
static void onPrjFileClick(int32_t fileIdx, bool isForm) {
|
|
(void)isForm;
|
|
activateFile(fileIdx, ViewAutoE);
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// newProject
|
|
// ============================================================
|
|
|
|
static void newProject(void) {
|
|
char name[PRJ_MAX_NAME];
|
|
|
|
if (!dvxInputBox(sAc, "New Project", "Project name:", "", name, sizeof(name))) {
|
|
return;
|
|
}
|
|
|
|
if (name[0] == '\0') {
|
|
return;
|
|
}
|
|
|
|
// Ask for directory via save dialog (file = name.dbp)
|
|
FileFilterT filters[] = {
|
|
{ "Project Files (*.dbp)", "*.dbp" }
|
|
};
|
|
|
|
char dbpPath[DVX_MAX_PATH];
|
|
snprintf(dbpPath, sizeof(dbpPath), "%s.dbp", name);
|
|
|
|
if (!dvxFileDialog(sAc, "Save New Project", FD_SAVE, NULL, filters, 1, dbpPath, sizeof(dbpPath))) {
|
|
return;
|
|
}
|
|
|
|
closeProject();
|
|
|
|
// Derive directory from chosen path
|
|
char dir[DVX_MAX_PATH];
|
|
snprintf(dir, sizeof(dir), "%s", dbpPath);
|
|
char *sep = strrchr(dir, '/');
|
|
char *sep2 = strrchr(dir, '\\');
|
|
|
|
if (sep2 > sep) {
|
|
sep = sep2;
|
|
}
|
|
|
|
if (sep) {
|
|
*sep = '\0';
|
|
} else {
|
|
dir[0] = '.';
|
|
dir[1] = '\0';
|
|
}
|
|
|
|
prjNew(&sProject, name, dir, sPrefs);
|
|
snprintf(sProject.projectPath, sizeof(sProject.projectPath), "%s", dbpPath);
|
|
prjSave(&sProject);
|
|
sProject.dirty = false;
|
|
|
|
// Create and show project window
|
|
if (!sProjectWin) {
|
|
sProjectWin = prjCreateWindow(sAc, &sProject, onPrjFileClick);
|
|
|
|
if (sProjectWin) {
|
|
sProjectWin->y = toolbarBottom() + 25;
|
|
sProjectWin->onClose = onProjectWinClose;
|
|
sProjectWin->onMenu = onMenu;
|
|
sProjectWin->accelTable = sWin ? sWin->accelTable : NULL;
|
|
}
|
|
} else {
|
|
prjRebuildTree(&sProject);
|
|
}
|
|
|
|
char title[300];
|
|
snprintf(title, sizeof(title), "DVX BASIC - [%s]", sProject.name);
|
|
dvxSetTitle(sAc, sWin, title);
|
|
|
|
setStatus("New project created.");
|
|
updateProjectMenuState();
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// openProject
|
|
// ============================================================
|
|
|
|
static void openProject(void) {
|
|
FileFilterT filters[] = {
|
|
{ "Project Files (*.dbp)", "*.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;
|
|
sProjectWin->onMenu = onMenu;
|
|
sProjectWin->accelTable = sWin ? sWin->accelTable : NULL;
|
|
}
|
|
} else {
|
|
prjRebuildTree(&sProject);
|
|
}
|
|
|
|
char title[300];
|
|
snprintf(title, sizeof(title), "DVX BASIC - [%s]", sProject.name);
|
|
dvxSetTitle(sAc, sWin, title);
|
|
|
|
setStatus("Project loaded.");
|
|
updateProjectMenuState();
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// closeProject
|
|
// ============================================================
|
|
|
|
static void closeProject(void) {
|
|
if (sProject.projectPath[0] == '\0') {
|
|
return;
|
|
}
|
|
|
|
closeFindDialog();
|
|
|
|
if (sProject.dirty) {
|
|
prjSave(&sProject);
|
|
}
|
|
|
|
// Close designer windows
|
|
if (sFormWin) {
|
|
dvxDestroyWindow(sAc, sFormWin);
|
|
cleanupFormWin();
|
|
}
|
|
|
|
dsgnFree(&sDesigner);
|
|
|
|
// Close code editor
|
|
if (sCodeWin) {
|
|
dvxDestroyWindow(sAc, sCodeWin);
|
|
sCodeWin = NULL;
|
|
sEditor = NULL;
|
|
sObjDropdown = NULL;
|
|
sEvtDropdown = NULL;
|
|
}
|
|
|
|
freeProcBufs();
|
|
|
|
// Close project window
|
|
prjClose(&sProject);
|
|
|
|
if (sProjectWin) {
|
|
prjDestroyWindow(sAc, sProjectWin);
|
|
sProjectWin = NULL;
|
|
}
|
|
|
|
if (sWin) {
|
|
dvxSetTitle(sAc, sWin, "DVX BASIC");
|
|
}
|
|
|
|
if (sStatus) {
|
|
setStatus("Project closed.");
|
|
}
|
|
|
|
updateProjectMenuState();
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// ideRenameInCode -- rename form/control references in all .bas files
|
|
// ============================================================
|
|
//
|
|
// Case-insensitive replacement of OldName followed by '.' or '_' with
|
|
// NewName followed by the same delimiter. This handles:
|
|
// ControlName.Property -> NewName.Property
|
|
// ControlName_Click -> NewName_Click (event handlers)
|
|
// FormName.ControlName.X -> NewFormName.ControlName.X
|
|
// Sub FormName_Load -> Sub NewFormName_Load
|
|
//
|
|
// Word-boundary check on the left: the character before the match must
|
|
// be a non-identifier character (space, tab, newline, '.', '(', start
|
|
// of string) to avoid replacing "Command1" inside "MyCommand1".
|
|
|
|
static bool isIdentChar(char c) {
|
|
return (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') ||
|
|
(c >= '0' && c <= '9') || c == '_';
|
|
}
|
|
|
|
|
|
// Check if position i is inside a string literal or comment.
|
|
// Scans from the start of the line containing i.
|
|
static bool isInStringOrComment(const char *src, int32_t i) {
|
|
// Find start of line
|
|
int32_t lineStart = i;
|
|
|
|
while (lineStart > 0 && src[lineStart - 1] != '\n') {
|
|
lineStart--;
|
|
}
|
|
|
|
bool inString = false;
|
|
|
|
for (int32_t j = lineStart; j < i; j++) {
|
|
if (src[j] == '"') {
|
|
inString = !inString;
|
|
} else if (src[j] == '\'' && !inString) {
|
|
// Rest of line is a comment
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return inString;
|
|
}
|
|
|
|
|
|
static char *renameInBuffer(const char *src, const char *oldName, const char *newName) {
|
|
if (!src || !oldName || !newName || !oldName[0]) {
|
|
return NULL;
|
|
}
|
|
|
|
int32_t oldLen = (int32_t)strlen(oldName);
|
|
int32_t newLen = (int32_t)strlen(newName);
|
|
int32_t srcLen = (int32_t)strlen(src);
|
|
|
|
// First pass: count replacements to compute output size
|
|
int32_t count = 0;
|
|
|
|
for (int32_t i = 0; i <= srcLen - oldLen; i++) {
|
|
if (strncasecmp(&src[i], oldName, oldLen) != 0) {
|
|
continue;
|
|
}
|
|
|
|
char after = src[i + oldLen];
|
|
|
|
if (after != '.' && after != '_') {
|
|
continue;
|
|
}
|
|
|
|
if (i > 0 && isIdentChar(src[i - 1])) {
|
|
continue;
|
|
}
|
|
|
|
if (prefsGetBool(sPrefs, "editor", "renameSkipComments", true) && isInStringOrComment(src, i)) {
|
|
continue;
|
|
}
|
|
|
|
count++;
|
|
}
|
|
|
|
if (count == 0) {
|
|
return NULL;
|
|
}
|
|
|
|
// Allocate output
|
|
int32_t outLen = srcLen + count * (newLen - oldLen);
|
|
char *out = (char *)malloc(outLen + 1);
|
|
|
|
if (!out) {
|
|
return NULL;
|
|
}
|
|
|
|
// Second pass: build output
|
|
int32_t op = 0;
|
|
|
|
for (int32_t i = 0; i < srcLen; ) {
|
|
if (i <= srcLen - oldLen &&
|
|
strncasecmp(&src[i], oldName, oldLen) == 0) {
|
|
char after = src[i + oldLen];
|
|
|
|
if ((after == '.' || after == '_') &&
|
|
(i == 0 || !isIdentChar(src[i - 1])) &&
|
|
(!prefsGetBool(sPrefs, "editor", "renameSkipComments", true) || !isInStringOrComment(src, i))) {
|
|
memcpy(out + op, newName, newLen);
|
|
op += newLen;
|
|
i += oldLen;
|
|
continue;
|
|
}
|
|
}
|
|
|
|
out[op++] = src[i++];
|
|
}
|
|
|
|
out[op] = '\0';
|
|
return out;
|
|
}
|
|
|
|
|
|
void ideRenameInCode(const char *oldName, const char *newName) {
|
|
if (!oldName || !newName || strcasecmp(oldName, newName) == 0) {
|
|
return;
|
|
}
|
|
|
|
// Rename in the per-procedure buffers (form code currently being edited)
|
|
if (sGeneralBuf) {
|
|
char *replaced = renameInBuffer(sGeneralBuf, oldName, newName);
|
|
|
|
if (replaced) {
|
|
free(sGeneralBuf);
|
|
sGeneralBuf = replaced;
|
|
}
|
|
}
|
|
|
|
for (int32_t i = 0; i < (int32_t)arrlen(sProcBufs); i++) {
|
|
if (sProcBufs[i]) {
|
|
char *replaced = renameInBuffer(sProcBufs[i], oldName, newName);
|
|
|
|
if (replaced) {
|
|
free(sProcBufs[i]);
|
|
sProcBufs[i] = replaced;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Update the editor if it's showing a procedure
|
|
if (sEditor && sCurProcIdx >= -1) {
|
|
if (sCurProcIdx == -1 && sGeneralBuf) {
|
|
wgtSetText(sEditor, sGeneralBuf);
|
|
} else if (sCurProcIdx >= 0 && sCurProcIdx < (int32_t)arrlen(sProcBufs)) {
|
|
wgtSetText(sEditor, sProcBufs[sCurProcIdx]);
|
|
}
|
|
}
|
|
|
|
// Update form->code from the renamed buffers (only if editor has this form's code)
|
|
if (sDesigner.form && sEditorFileIdx >= 0 && sEditorFileIdx < sProject.fileCount &&
|
|
sProject.files[sEditorFileIdx].isForm &&
|
|
strcasecmp(sProject.files[sEditorFileIdx].formName, sDesigner.form->name) == 0) {
|
|
free(sDesigner.form->code);
|
|
sDesigner.form->code = strdup(getFullSource());
|
|
sDesigner.form->dirty = true;
|
|
}
|
|
|
|
// Update cached formName if the active file is a form being renamed
|
|
if (sProject.activeFileIdx >= 0 && sProject.activeFileIdx < sProject.fileCount) {
|
|
PrjFileT *cur = &sProject.files[sProject.activeFileIdx];
|
|
|
|
if (cur->isForm && strcasecmp(cur->formName, oldName) == 0) {
|
|
snprintf(cur->formName, sizeof(cur->formName), "%s", newName);
|
|
}
|
|
}
|
|
|
|
// Rename in all project .bas file buffers (and non-active .frm code)
|
|
for (int32_t i = 0; i < sProject.fileCount; i++) {
|
|
// Skip the active file (already handled above)
|
|
if (i == sProject.activeFileIdx) {
|
|
continue;
|
|
}
|
|
|
|
char *buf = sProject.files[i].buffer;
|
|
|
|
if (!buf) {
|
|
// Load from disk
|
|
char fullPath[DVX_MAX_PATH];
|
|
prjFullPath(&sProject, i, fullPath, sizeof(fullPath));
|
|
|
|
FILE *f = fopen(fullPath, "r");
|
|
|
|
if (!f) {
|
|
continue;
|
|
}
|
|
|
|
fseek(f, 0, SEEK_END);
|
|
long size = ftell(f);
|
|
fseek(f, 0, SEEK_SET);
|
|
|
|
if (size <= 0 || size >= IDE_MAX_SOURCE) {
|
|
fclose(f);
|
|
continue;
|
|
}
|
|
|
|
buf = (char *)malloc(size + 1);
|
|
|
|
if (!buf) {
|
|
fclose(f);
|
|
continue;
|
|
}
|
|
|
|
int32_t br = (int32_t)fread(buf, 1, size, f);
|
|
fclose(f);
|
|
buf[br] = '\0';
|
|
sProject.files[i].buffer = buf;
|
|
}
|
|
|
|
char *replaced = renameInBuffer(buf, oldName, newName);
|
|
|
|
if (replaced) {
|
|
free(sProject.files[i].buffer);
|
|
sProject.files[i].buffer = replaced;
|
|
sProject.files[i].modified = true;
|
|
}
|
|
}
|
|
|
|
// Refresh dropdowns to reflect renamed procedures
|
|
updateDropdowns();
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// onCodeWinClose -- user closed the code window via X button
|
|
// ============================================================
|
|
|
|
static void onCodeWinClose(WindowT *win) {
|
|
// Stash code back before the window is destroyed.
|
|
stashFormCode();
|
|
|
|
dvxDestroyWindow(sAc, win);
|
|
sCodeWin = NULL;
|
|
sEditor = NULL;
|
|
sObjDropdown = NULL;
|
|
sEvtDropdown = NULL;
|
|
|
|
if (sLastFocusWin == win) {
|
|
sLastFocusWin = NULL;
|
|
}
|
|
|
|
updateProjectMenuState();
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// onProjectWinClose -- user closed the project window via X button
|
|
// ============================================================
|
|
|
|
static void onProjectWinClose(WindowT *win) {
|
|
prjDestroyWindow(sAc, win);
|
|
sProjectWin = NULL;
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// loadFrmFiles
|
|
// ============================================================
|
|
//
|
|
// Load all .frm files listed in the current project into the
|
|
// form runtime for execution.
|
|
|
|
static void loadFrmFiles(BasFormRtT *rt) {
|
|
for (int32_t i = 0; i < sProject.fileCount; i++) {
|
|
if (!sProject.files[i].isForm) {
|
|
continue;
|
|
}
|
|
|
|
char fullPath[DVX_MAX_PATH];
|
|
prjFullPath(&sProject, i, fullPath, sizeof(fullPath));
|
|
|
|
FILE *f = fopen(fullPath, "r");
|
|
|
|
if (!f) {
|
|
continue;
|
|
}
|
|
|
|
fseek(f, 0, SEEK_END);
|
|
long size = ftell(f);
|
|
fseek(f, 0, SEEK_SET);
|
|
|
|
if (size <= 0 || size >= IDE_MAX_SOURCE) {
|
|
fclose(f);
|
|
continue;
|
|
}
|
|
|
|
char *frmBuf = (char *)malloc(size + 1);
|
|
|
|
if (!frmBuf) {
|
|
fclose(f);
|
|
continue;
|
|
}
|
|
|
|
int32_t bytesRead = (int32_t)fread(frmBuf, 1, size, f);
|
|
fclose(f);
|
|
frmBuf[bytesRead] = '\0';
|
|
|
|
BasFormT *form = basFormRtLoadFrm(rt, frmBuf, bytesRead);
|
|
free(frmBuf);
|
|
|
|
// Cache the form object name in the project file entry
|
|
if (form && form->name[0]) {
|
|
snprintf(sProject.files[i].formName, sizeof(sProject.files[i].formName), "%s", form->name);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// loadFormCodeIntoEditor -- loads form code into proc buffers + editor
|
|
// ============================================================
|
|
|
|
static void loadFormCodeIntoEditor(void) {
|
|
if (!sDesigner.form) {
|
|
return;
|
|
}
|
|
|
|
stashFormCode();
|
|
parseProcs(sDesigner.form->code ? sDesigner.form->code : "");
|
|
sEditorFileIdx = sProject.activeFileIdx;
|
|
|
|
if (!sCodeWin) {
|
|
showCodeWindow();
|
|
}
|
|
|
|
bool saved = sDropdownNavSuppressed;
|
|
sDropdownNavSuppressed = true;
|
|
updateDropdowns();
|
|
sDropdownNavSuppressed = saved;
|
|
|
|
showProc(-1);
|
|
|
|
if (sEditor && !sEditor->onChange) {
|
|
sEditor->onChange = onEditorChange;
|
|
}
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// onContentFocus -- track last focused content window for clipboard
|
|
// ============================================================
|
|
|
|
static void onContentFocus(WindowT *win) {
|
|
sLastFocusWin = win;
|
|
}
|
|
|
|
static WindowT *getLastFocusWin(void) {
|
|
if (sLastFocusWin == sCodeWin ||
|
|
sLastFocusWin == sOutWin ||
|
|
sLastFocusWin == sImmWin) {
|
|
return sLastFocusWin;
|
|
}
|
|
|
|
sLastFocusWin = NULL;
|
|
return NULL;
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// hasUnsavedData -- check if any project files have unsaved changes
|
|
// ============================================================
|
|
|
|
static bool hasUnsavedData(void) {
|
|
// Check the active editor/designer
|
|
if (sDesigner.form && sDesigner.form->dirty) {
|
|
return true;
|
|
}
|
|
|
|
// Check all project files
|
|
for (int32_t i = 0; i < sProject.fileCount; i++) {
|
|
if (sProject.files[i].modified) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return sProject.dirty;
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// promptAndSave -- ask user to save, discard, or cancel
|
|
// ============================================================
|
|
//
|
|
// Returns true if the caller should proceed (user saved or discarded).
|
|
// Returns false if the user cancelled.
|
|
|
|
static bool promptAndSave(void) {
|
|
if (!hasUnsavedData()) {
|
|
return true;
|
|
}
|
|
|
|
int32_t result = dvxPromptSave(sAc, "DVX BASIC");
|
|
|
|
if (result == DVX_SAVE_YES) {
|
|
saveFile();
|
|
return true;
|
|
}
|
|
|
|
return result == DVX_SAVE_NO;
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// onClose
|
|
// ============================================================
|
|
|
|
static void onClose(WindowT *win) {
|
|
if (!promptAndSave()) {
|
|
return;
|
|
}
|
|
|
|
// 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 handleFileCmd(int32_t cmd) {
|
|
switch (cmd) {
|
|
case CMD_OPEN:
|
|
loadFile();
|
|
break;
|
|
|
|
case CMD_SAVE:
|
|
saveFile();
|
|
break;
|
|
|
|
case CMD_SAVE_ALL:
|
|
saveFile();
|
|
|
|
for (int32_t i = 0; i < sProject.fileCount; i++) {
|
|
if (i == sProject.activeFileIdx) {
|
|
continue;
|
|
}
|
|
|
|
if (sProject.files[i].modified && sProject.files[i].buffer) {
|
|
char fullPath[DVX_MAX_PATH];
|
|
prjFullPath(&sProject, i, fullPath, sizeof(fullPath));
|
|
|
|
FILE *f = fopen(fullPath, "w");
|
|
|
|
if (f) {
|
|
fputs(sProject.files[i].buffer, f);
|
|
fclose(f);
|
|
sProject.files[i].modified = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (sProject.projectPath[0] != '\0') {
|
|
prjSave(&sProject);
|
|
sProject.dirty = false;
|
|
}
|
|
|
|
setStatus("All files saved.");
|
|
updateDirtyIndicators();
|
|
break;
|
|
|
|
case CMD_EXIT:
|
|
if (sWin) {
|
|
onClose(sWin);
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// Find/Replace dialog (modeless)
|
|
// ============================================================
|
|
|
|
typedef enum {
|
|
ScopeFuncE,
|
|
ScopeObjE,
|
|
ScopeFileE,
|
|
ScopeProjE
|
|
} FindScopeE;
|
|
|
|
|
|
static FindScopeE getFindScope(void) {
|
|
if (!sScopeGroup) {
|
|
return ScopeProjE;
|
|
}
|
|
|
|
int32_t idx = wgtRadioGetIndex(sScopeGroup);
|
|
|
|
switch (idx) {
|
|
case 0: return ScopeFuncE;
|
|
case 1: return ScopeObjE;
|
|
case 2: return ScopeFileE;
|
|
default: return ScopeProjE;
|
|
}
|
|
}
|
|
|
|
|
|
static bool getFindMatchCase(void) {
|
|
return sCaseCheck && wgtCheckboxIsChecked(sCaseCheck);
|
|
}
|
|
|
|
|
|
static bool getFindForward(void) {
|
|
if (!sDirGroup) {
|
|
return true;
|
|
}
|
|
|
|
return wgtRadioGetIndex(sDirGroup) == 0;
|
|
}
|
|
|
|
|
|
static bool isReplaceEnabled(void) {
|
|
return sReplCheck && wgtCheckboxIsChecked(sReplCheck);
|
|
}
|
|
|
|
|
|
static void onReplCheckChange(WidgetT *w) {
|
|
(void)w;
|
|
bool show = isReplaceEnabled();
|
|
|
|
if (sReplInput) { sReplInput->enabled = show; }
|
|
if (sBtnReplace) { sBtnReplace->enabled = show; }
|
|
if (sBtnReplAll) { sBtnReplAll->enabled = show; }
|
|
|
|
if (sFindWin) {
|
|
dvxInvalidateWindow(sAc, sFindWin);
|
|
}
|
|
}
|
|
|
|
|
|
static void onFindNext(WidgetT *w) {
|
|
(void)w;
|
|
|
|
if (!sFindInput) {
|
|
return;
|
|
}
|
|
|
|
const char *needle = wgtGetText(sFindInput);
|
|
|
|
if (!needle || !needle[0]) {
|
|
return;
|
|
}
|
|
|
|
snprintf(sFindText, sizeof(sFindText), "%s", needle);
|
|
|
|
FindScopeE scope = getFindScope();
|
|
bool caseSens = getFindMatchCase();
|
|
bool forward = getFindForward();
|
|
|
|
if (scope == ScopeFuncE && sEditor) {
|
|
// Search current procedure only
|
|
if (!wgtTextAreaFindNext(sEditor, sFindText, caseSens, forward)) {
|
|
setStatus("Not found.");
|
|
}
|
|
} else if (scope == ScopeObjE || scope == ScopeFileE || scope == ScopeProjE) {
|
|
if (!findInProject(sFindText, caseSens)) {
|
|
setStatus("Not found.");
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
static void onReplace(WidgetT *w) {
|
|
(void)w;
|
|
|
|
if (!sEditor || !sFindInput || !sReplInput || !isReplaceEnabled()) {
|
|
return;
|
|
}
|
|
|
|
const char *needle = wgtGetText(sFindInput);
|
|
const char *repl = wgtGetText(sReplInput);
|
|
|
|
if (!needle || !needle[0]) {
|
|
return;
|
|
}
|
|
|
|
snprintf(sFindText, sizeof(sFindText), "%s", needle);
|
|
snprintf(sReplaceText, sizeof(sReplaceText), "%s", repl ? repl : "");
|
|
|
|
// If text is selected and matches the search, replace it, then find next
|
|
const char *edText = wgtGetText(sEditor);
|
|
|
|
if (edText) {
|
|
// TODO: replace current selection if it matches, then find next
|
|
// For now, just do find next
|
|
onFindNext(w);
|
|
}
|
|
}
|
|
|
|
|
|
static void onReplaceAll(WidgetT *w) {
|
|
(void)w;
|
|
|
|
if (!sFindInput || !sReplInput || !isReplaceEnabled()) {
|
|
return;
|
|
}
|
|
|
|
const char *needle = wgtGetText(sFindInput);
|
|
const char *repl = wgtGetText(sReplInput);
|
|
|
|
if (!needle || !needle[0]) {
|
|
return;
|
|
}
|
|
|
|
snprintf(sFindText, sizeof(sFindText), "%s", needle);
|
|
snprintf(sReplaceText, sizeof(sReplaceText), "%s", repl ? repl : "");
|
|
|
|
bool caseSens = getFindMatchCase();
|
|
FindScopeE scope = getFindScope();
|
|
int32_t totalCount = 0;
|
|
|
|
if (scope == ScopeFuncE && sEditor) {
|
|
totalCount = wgtTextAreaReplaceAll(sEditor, sFindText, sReplaceText, caseSens);
|
|
} else if (scope == ScopeProjE) {
|
|
stashCurrentFile();
|
|
|
|
for (int32_t i = 0; i < sProject.fileCount; i++) {
|
|
activateFile(i, sProject.files[i].isForm ? ViewCodeE : ViewAutoE);
|
|
|
|
if (sEditor) {
|
|
totalCount += wgtTextAreaReplaceAll(sEditor, sFindText, sReplaceText, caseSens);
|
|
}
|
|
}
|
|
} else if (scope == ScopeFileE && sEditor) {
|
|
// Replace in all procs of current file
|
|
int32_t procCount = (int32_t)arrlen(sProcBufs);
|
|
|
|
for (int32_t p = -1; p < procCount; p++) {
|
|
showProc(p);
|
|
|
|
if (sEditor) {
|
|
totalCount += wgtTextAreaReplaceAll(sEditor, sFindText, sReplaceText, caseSens);
|
|
}
|
|
}
|
|
} else if (scope == ScopeObjE && sEditor && sObjDropdown) {
|
|
// Replace in all procs belonging to current object
|
|
int32_t objIdx = wgtDropdownGetSelected(sObjDropdown);
|
|
|
|
if (objIdx >= 0 && objIdx < (int32_t)arrlen(sObjItems)) {
|
|
const char *objName = sObjItems[objIdx];
|
|
int32_t procCount = (int32_t)arrlen(sProcTable);
|
|
|
|
for (int32_t p = 0; p < procCount; p++) {
|
|
if (strcasecmp(sProcTable[p].objName, objName) == 0) {
|
|
showProc(p);
|
|
|
|
if (sEditor) {
|
|
totalCount += wgtTextAreaReplaceAll(sEditor, sFindText, sReplaceText, caseSens);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
char statusBuf[64];
|
|
snprintf(statusBuf, sizeof(statusBuf), "%d replacement(s) made.", (int)totalCount);
|
|
setStatus(statusBuf);
|
|
}
|
|
|
|
|
|
static void onFindClose(WindowT *win) {
|
|
(void)win;
|
|
closeFindDialog();
|
|
}
|
|
|
|
|
|
static void onFindCloseBtn(WidgetT *w) {
|
|
(void)w;
|
|
closeFindDialog();
|
|
}
|
|
|
|
|
|
static void closeFindDialog(void) {
|
|
if (sFindWin) {
|
|
dvxDestroyWindow(sAc, sFindWin);
|
|
sFindWin = NULL;
|
|
sFindInput = NULL;
|
|
sReplInput = NULL;
|
|
sReplCheck = NULL;
|
|
sBtnReplace = NULL;
|
|
sBtnReplAll = NULL;
|
|
sCaseCheck = NULL;
|
|
sScopeGroup = NULL;
|
|
sDirGroup = NULL;
|
|
}
|
|
}
|
|
|
|
|
|
static void openFindDialog(bool showReplace) {
|
|
if (sFindWin) {
|
|
// Already open — just toggle replace mode and raise
|
|
if (sReplCheck) {
|
|
wgtCheckboxSetChecked(sReplCheck, showReplace);
|
|
onReplCheckChange(sReplCheck);
|
|
}
|
|
|
|
dvxRaiseWindow(sAc, sFindWin);
|
|
return;
|
|
}
|
|
|
|
sFindWin = dvxCreateWindowCentered(sAc, "Find / Replace", 320, 210, false);
|
|
|
|
if (!sFindWin) {
|
|
return;
|
|
}
|
|
|
|
sFindWin->onClose = onFindClose;
|
|
sFindWin->onMenu = onMenu;
|
|
sFindWin->accelTable = sWin ? sWin->accelTable : NULL;
|
|
|
|
WidgetT *root = wgtInitWindow(sAc, sFindWin);
|
|
root->spacing = wgtPixels(3);
|
|
|
|
// Find row
|
|
WidgetT *findRow = wgtHBox(root);
|
|
findRow->spacing = wgtPixels(4);
|
|
wgtLabel(findRow, "Find:");
|
|
sFindInput = wgtTextInput(findRow, 256);
|
|
sFindInput->weight = 100;
|
|
wgtSetText(sFindInput, sFindText);
|
|
|
|
// Replace checkbox + input
|
|
WidgetT *replRow = wgtHBox(root);
|
|
replRow->spacing = wgtPixels(4);
|
|
sReplCheck = wgtCheckbox(replRow, "Replace:");
|
|
wgtCheckboxSetChecked(sReplCheck, showReplace);
|
|
sReplCheck->onChange = onReplCheckChange;
|
|
sReplInput = wgtTextInput(replRow, 256);
|
|
sReplInput->weight = 100;
|
|
wgtSetText(sReplInput, sReplaceText);
|
|
|
|
// Options row: scope + direction + case
|
|
WidgetT *optRow = wgtHBox(root);
|
|
optRow->spacing = wgtPixels(8);
|
|
|
|
// Scope
|
|
WidgetT *scopeFrame = wgtFrame(optRow, "Scope");
|
|
WidgetT *scopeBox = wgtVBox(scopeFrame);
|
|
sScopeGroup = wgtRadioGroup(scopeBox);
|
|
wgtRadio(sScopeGroup, "Function");
|
|
wgtRadio(sScopeGroup, "Object");
|
|
wgtRadio(sScopeGroup, "File");
|
|
wgtRadio(sScopeGroup, "Project");
|
|
wgtRadioGroupSetSelected(sScopeGroup, 3); // Project
|
|
|
|
// Direction
|
|
WidgetT *dirFrame = wgtFrame(optRow, "Direction");
|
|
WidgetT *dirBox = wgtVBox(dirFrame);
|
|
sDirGroup = wgtRadioGroup(dirBox);
|
|
wgtRadio(sDirGroup, "Forward");
|
|
wgtRadio(sDirGroup, "Backward");
|
|
wgtRadioGroupSetSelected(sDirGroup, 0); // Forward
|
|
|
|
// Match Case
|
|
WidgetT *caseBox = wgtVBox(optRow);
|
|
sCaseCheck = wgtCheckbox(caseBox, "Match Case");
|
|
|
|
// Buttons
|
|
WidgetT *btnRow = wgtHBox(root);
|
|
btnRow->spacing = wgtPixels(4);
|
|
btnRow->align = AlignEndE;
|
|
|
|
WidgetT *btnFind = wgtButton(btnRow, "Find Next");
|
|
btnFind->onClick = onFindNext;
|
|
|
|
sBtnReplace = wgtButton(btnRow, "Replace");
|
|
sBtnReplace->onClick = onReplace;
|
|
|
|
sBtnReplAll = wgtButton(btnRow, "Replace All");
|
|
sBtnReplAll->onClick = onReplaceAll;
|
|
|
|
WidgetT *btnClose = wgtButton(btnRow, "Close");
|
|
btnClose->onClick = onFindCloseBtn;
|
|
|
|
// Set initial replace enable state
|
|
onReplCheckChange(sReplCheck);
|
|
|
|
dvxFitWindow(sAc, sFindWin);
|
|
}
|
|
|
|
|
|
// findInProject -- search all project files for a text match.
|
|
// Starts from the current editor position in the current file,
|
|
// then continues through subsequent files, wrapping around.
|
|
// Opens the file and selects the match when found.
|
|
|
|
// showProcAndFind -- switch to a procedure, sync the dropdowns, and
|
|
// select the search match in the editor.
|
|
|
|
static bool showProcAndFind(int32_t procIdx, const char *needle, bool caseSensitive) {
|
|
showProc(procIdx);
|
|
|
|
// Sync the Object/Event dropdowns to match
|
|
if (procIdx >= 0 && procIdx < (int32_t)arrlen(sProcTable)) {
|
|
selectDropdowns(sProcTable[procIdx].objName, sProcTable[procIdx].evtName);
|
|
} else if (procIdx == -1 && sObjDropdown) {
|
|
wgtDropdownSetSelected(sObjDropdown, 0); // (General)
|
|
}
|
|
|
|
if (sEditor) {
|
|
return wgtTextAreaFindNext(sEditor, needle, caseSensitive, true);
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
|
|
// Case-insensitive strstr replacement (strcasestr is a GNU extension)
|
|
static const char *findSubstrNoCase(const char *haystack, const char *needle, int32_t needleLen) {
|
|
for (; *haystack; haystack++) {
|
|
if (strncasecmp(haystack, needle, needleLen) == 0) {
|
|
return haystack;
|
|
}
|
|
}
|
|
|
|
return NULL;
|
|
}
|
|
|
|
|
|
static bool findInProject(const char *needle, bool caseSensitive) {
|
|
if (!needle || !needle[0] || sProject.fileCount == 0) {
|
|
return false;
|
|
}
|
|
|
|
int32_t needleLen = (int32_t)strlen(needle);
|
|
|
|
// Stash current editor state so all buffers are up-to-date
|
|
stashCurrentFile();
|
|
|
|
// Start from the active file, searching from after the current selection
|
|
int32_t startFile = sProject.activeFileIdx >= 0 ? sProject.activeFileIdx : 0;
|
|
int32_t startPos = 0;
|
|
|
|
// If the editor is open on the current file, try the current proc
|
|
// first (no wrap — returns false if no more matches ahead).
|
|
if (sEditor && sEditorFileIdx == startFile) {
|
|
if (wgtTextAreaFindNext(sEditor, needle, caseSensitive, true)) {
|
|
return true;
|
|
}
|
|
|
|
// No more matches in current proc — search remaining procs
|
|
int32_t procCount = (int32_t)arrlen(sProcBufs);
|
|
|
|
for (int32_t p = sCurProcIdx + 1; p < procCount; p++) {
|
|
if (!sProcBufs[p]) {
|
|
continue;
|
|
}
|
|
|
|
const char *found = caseSensitive ? strstr(sProcBufs[p], needle) : findSubstrNoCase(sProcBufs[p], needle, needleLen);
|
|
|
|
if (found) {
|
|
showProcAndFind(p, needle, caseSensitive);
|
|
return true;
|
|
}
|
|
}
|
|
|
|
// Search General section if we started in a proc
|
|
if (sCurProcIdx >= 0 && sGeneralBuf) {
|
|
const char *found = caseSensitive ? strstr(sGeneralBuf, needle) : findSubstrNoCase(sGeneralBuf, needle, needleLen);
|
|
|
|
if (found) {
|
|
showProcAndFind(-1, needle, caseSensitive);
|
|
return true;
|
|
}
|
|
}
|
|
|
|
// Search procs before the current one (wrap within file)
|
|
for (int32_t p = 0; p < sCurProcIdx && p < procCount; p++) {
|
|
if (!sProcBufs[p]) {
|
|
continue;
|
|
}
|
|
|
|
const char *found = caseSensitive ? strstr(sProcBufs[p], needle) : findSubstrNoCase(sProcBufs[p], needle, needleLen);
|
|
|
|
if (found) {
|
|
showProcAndFind(p, needle, caseSensitive);
|
|
return true;
|
|
}
|
|
}
|
|
|
|
// Move to next file
|
|
startFile = (startFile + 1) % sProject.fileCount;
|
|
}
|
|
|
|
// Search remaining files, proc by proc.
|
|
// startFile was advanced past the current file if we already searched it.
|
|
int32_t filesToSearch = sProject.fileCount;
|
|
|
|
if (sEditor && sEditorFileIdx >= 0) {
|
|
filesToSearch--; // skip the file we already searched above
|
|
}
|
|
|
|
for (int32_t attempt = 0; attempt < filesToSearch; attempt++) {
|
|
int32_t fileIdx = (startFile + attempt) % sProject.fileCount;
|
|
|
|
// Activate the file to load its proc buffers
|
|
activateFile(fileIdx, sProject.files[fileIdx].isForm ? ViewCodeE : ViewAutoE);
|
|
|
|
// Search General section
|
|
if (sGeneralBuf) {
|
|
const char *found = caseSensitive ? strstr(sGeneralBuf, needle) : findSubstrNoCase(sGeneralBuf, needle, needleLen);
|
|
|
|
if (found) {
|
|
showProcAndFind(-1, needle, caseSensitive);
|
|
return true;
|
|
}
|
|
}
|
|
|
|
// Search each procedure
|
|
int32_t procCount = (int32_t)arrlen(sProcBufs);
|
|
|
|
for (int32_t p = 0; p < procCount; p++) {
|
|
if (!sProcBufs[p]) {
|
|
continue;
|
|
}
|
|
|
|
const char *found = caseSensitive ? strstr(sProcBufs[p], needle) : findSubstrNoCase(sProcBufs[p], needle, needleLen);
|
|
|
|
if (found) {
|
|
showProcAndFind(p, needle, caseSensitive);
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
|
|
static void handleEditCmd(int32_t cmd) {
|
|
switch (cmd) {
|
|
case CMD_CUT:
|
|
case CMD_COPY:
|
|
case CMD_PASTE:
|
|
case CMD_SELECT_ALL: {
|
|
static const int32_t keys[] = { 24, 3, 22, 1 };
|
|
int32_t key = keys[cmd - CMD_CUT];
|
|
WindowT *target = getLastFocusWin();
|
|
|
|
if (target && target->onKey) {
|
|
target->onKey(target, key, ACCEL_CTRL);
|
|
}
|
|
|
|
break;
|
|
}
|
|
|
|
case CMD_DELETE:
|
|
if (sFormWin && sDesigner.selectedIdx >= 0) {
|
|
int32_t prevCount = (int32_t)arrlen(sDesigner.form->controls);
|
|
dsgnOnKey(&sDesigner, KEY_DELETE);
|
|
int32_t newCount = (int32_t)arrlen(sDesigner.form->controls);
|
|
|
|
if (newCount != prevCount) {
|
|
prpRebuildTree(&sDesigner);
|
|
prpRefresh(&sDesigner);
|
|
dvxInvalidateWindow(sAc, sFormWin);
|
|
}
|
|
}
|
|
break;
|
|
|
|
case CMD_FIND:
|
|
openFindDialog(false);
|
|
break;
|
|
|
|
case CMD_FIND_NEXT:
|
|
if (sFindText[0]) {
|
|
onFindNext(NULL);
|
|
} else {
|
|
openFindDialog(false);
|
|
}
|
|
break;
|
|
|
|
case CMD_REPLACE:
|
|
openFindDialog(true);
|
|
break;
|
|
}
|
|
}
|
|
|
|
|
|
static void handleRunCmd(int32_t cmd) {
|
|
switch (cmd) {
|
|
case CMD_RUN:
|
|
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_SAVE_ON_RUN:
|
|
if (sWin && sWin->menuBar) {
|
|
bool save = wmMenuItemIsChecked(sWin->menuBar, CMD_SAVE_ON_RUN);
|
|
prefsSetBool(sPrefs, "run", "saveOnRun", save);
|
|
prefsSave(sPrefs);
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
|
|
static void handleViewCmd(int32_t cmd) {
|
|
switch (cmd) {
|
|
case CMD_VIEW_CODE:
|
|
stashDesignerState();
|
|
break;
|
|
|
|
case CMD_VIEW_DESIGN:
|
|
switchToDesign();
|
|
break;
|
|
|
|
case CMD_VIEW_TOOLBAR:
|
|
if (sToolbar && sWin->menuBar) {
|
|
bool show = wmMenuItemIsChecked(sWin->menuBar, CMD_VIEW_TOOLBAR);
|
|
sToolbar->visible = show;
|
|
dvxFitWindowH(sAc, sWin);
|
|
prefsSetBool(sPrefs, "view", "toolbar", show);
|
|
prefsSave(sPrefs);
|
|
}
|
|
break;
|
|
|
|
case CMD_VIEW_STATUS:
|
|
if (sStatusBar && sWin->menuBar) {
|
|
bool show = wmMenuItemIsChecked(sWin->menuBar, CMD_VIEW_STATUS);
|
|
sStatusBar->visible = show;
|
|
dvxFitWindowH(sAc, sWin);
|
|
prefsSetBool(sPrefs, "view", "statusbar", show);
|
|
prefsSave(sPrefs);
|
|
}
|
|
break;
|
|
|
|
case CMD_MENU_EDITOR:
|
|
if (sDesigner.form) {
|
|
// Snapshot old menu names for rename detection
|
|
char **oldNames = NULL;
|
|
int32_t oldCount = (int32_t)arrlen(sDesigner.form->menuItems);
|
|
|
|
for (int32_t mi = 0; mi < oldCount; mi++) {
|
|
arrput(oldNames, strdup(sDesigner.form->menuItems[mi].name));
|
|
}
|
|
|
|
if (mnuEditorDialog(sAc, sDesigner.form)) {
|
|
sDesigner.form->dirty = true;
|
|
|
|
// Detect renames: match by position (items may have been
|
|
// reordered, but renamed items keep their index)
|
|
int32_t newCount = (int32_t)arrlen(sDesigner.form->menuItems);
|
|
int32_t minCount = oldCount < newCount ? oldCount : newCount;
|
|
|
|
for (int32_t mi = 0; mi < minCount; mi++) {
|
|
if (oldNames[mi][0] && sDesigner.form->menuItems[mi].name[0] &&
|
|
strcasecmp(oldNames[mi], sDesigner.form->menuItems[mi].name) != 0) {
|
|
ideRenameInCode(oldNames[mi], sDesigner.form->menuItems[mi].name);
|
|
}
|
|
}
|
|
|
|
// Rebuild menu bar preview
|
|
if (sFormWin) {
|
|
wmDestroyMenuBar(sFormWin);
|
|
dsgnBuildPreviewMenuBar(sFormWin, sDesigner.form);
|
|
dvxInvalidateWindow(sAc, sFormWin);
|
|
}
|
|
|
|
// Rebuild Object dropdown to reflect added/removed/renamed items
|
|
updateDropdowns();
|
|
}
|
|
|
|
for (int32_t mi = 0; mi < oldCount; mi++) {
|
|
free(oldNames[mi]);
|
|
}
|
|
|
|
arrfree(oldNames);
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// Preferences dialog
|
|
// ============================================================
|
|
|
|
static struct {
|
|
bool done;
|
|
bool accepted;
|
|
WidgetT *renameSkipComments;
|
|
WidgetT *optionExplicit;
|
|
WidgetT *tabWidthInput;
|
|
WidgetT *useSpaces;
|
|
WidgetT *defAuthor;
|
|
WidgetT *defCompany;
|
|
WidgetT *defVersion;
|
|
WidgetT *defCopyright;
|
|
WidgetT *defDescription;
|
|
} sPrefsDlg;
|
|
|
|
|
|
static void onPrefsOk(WidgetT *w) {
|
|
(void)w;
|
|
sPrefsDlg.accepted = true;
|
|
sPrefsDlg.done = true;
|
|
}
|
|
|
|
|
|
static void onPrefsCancel(WidgetT *w) {
|
|
(void)w;
|
|
sPrefsDlg.done = true;
|
|
}
|
|
|
|
|
|
static void showPreferencesDialog(void) {
|
|
memset(&sPrefsDlg, 0, sizeof(sPrefsDlg));
|
|
|
|
WindowT *win = dvxCreateWindowCentered(sAc, "Preferences", 400, 440, false);
|
|
|
|
if (!win) {
|
|
return;
|
|
}
|
|
|
|
win->maxW = win->w;
|
|
win->maxH = win->h;
|
|
|
|
WidgetT *root = wgtInitWindow(sAc, win);
|
|
root->spacing = wgtPixels(4);
|
|
|
|
// ---- Editor section ----
|
|
WidgetT *edFrame = wgtFrame(root, "Editor");
|
|
edFrame->spacing = wgtPixels(2);
|
|
|
|
sPrefsDlg.renameSkipComments = wgtCheckbox(edFrame, "Skip comments/strings when renaming");
|
|
wgtCheckboxSetChecked(sPrefsDlg.renameSkipComments, prefsGetBool(sPrefs, "editor", "renameSkipComments", true));
|
|
|
|
sPrefsDlg.optionExplicit = wgtCheckbox(edFrame, "Require variable declaration (OPTION EXPLICIT)");
|
|
wgtCheckboxSetChecked(sPrefsDlg.optionExplicit, prefsGetBool(sPrefs, "editor", "optionExplicit", false));
|
|
|
|
WidgetT *tabRow = wgtHBox(edFrame);
|
|
tabRow->spacing = wgtPixels(4);
|
|
wgtLabel(tabRow, "Tab width:");
|
|
sPrefsDlg.tabWidthInput = wgtTextInput(tabRow, 4);
|
|
sPrefsDlg.tabWidthInput->maxW = wgtPixels(40);
|
|
|
|
char tabBuf[8];
|
|
snprintf(tabBuf, sizeof(tabBuf), "%d", (int)prefsGetInt(sPrefs, "editor", "tabWidth", 3));
|
|
wgtSetText(sPrefsDlg.tabWidthInput, tabBuf);
|
|
|
|
sPrefsDlg.useSpaces = wgtCheckbox(edFrame, "Insert spaces instead of tabs");
|
|
wgtCheckboxSetChecked(sPrefsDlg.useSpaces, prefsGetBool(sPrefs, "editor", "useSpaces", true));
|
|
|
|
// ---- Project Defaults section ----
|
|
WidgetT *prjFrame = wgtFrame(root, "New Project Defaults");
|
|
prjFrame->spacing = wgtPixels(2);
|
|
|
|
WidgetT *r1 = wgtHBox(prjFrame);
|
|
r1->spacing = wgtPixels(4);
|
|
WidgetT *l1 = wgtLabel(r1, "Author:");
|
|
l1->minW = wgtPixels(80);
|
|
sPrefsDlg.defAuthor = wgtTextInput(r1, 64);
|
|
sPrefsDlg.defAuthor->weight = 100;
|
|
wgtSetText(sPrefsDlg.defAuthor, prefsGetString(sPrefs, "defaults", "author", ""));
|
|
|
|
WidgetT *r2 = wgtHBox(prjFrame);
|
|
r2->spacing = wgtPixels(4);
|
|
WidgetT *l2 = wgtLabel(r2, "Company:");
|
|
l2->minW = wgtPixels(80);
|
|
sPrefsDlg.defCompany = wgtTextInput(r2, 64);
|
|
sPrefsDlg.defCompany->weight = 100;
|
|
wgtSetText(sPrefsDlg.defCompany, prefsGetString(sPrefs, "defaults", "company", ""));
|
|
|
|
WidgetT *r3 = wgtHBox(prjFrame);
|
|
r3->spacing = wgtPixels(4);
|
|
WidgetT *l3 = wgtLabel(r3, "Version:");
|
|
l3->minW = wgtPixels(80);
|
|
sPrefsDlg.defVersion = wgtTextInput(r3, 16);
|
|
sPrefsDlg.defVersion->weight = 100;
|
|
wgtSetText(sPrefsDlg.defVersion, prefsGetString(sPrefs, "defaults", "version", "1.0"));
|
|
|
|
WidgetT *r4 = wgtHBox(prjFrame);
|
|
r4->spacing = wgtPixels(4);
|
|
WidgetT *l4 = wgtLabel(r4, "Copyright:");
|
|
l4->minW = wgtPixels(80);
|
|
sPrefsDlg.defCopyright = wgtTextInput(r4, 64);
|
|
sPrefsDlg.defCopyright->weight = 100;
|
|
wgtSetText(sPrefsDlg.defCopyright, prefsGetString(sPrefs, "defaults", "copyright", ""));
|
|
|
|
wgtLabel(prjFrame, "Description:");
|
|
sPrefsDlg.defDescription = wgtTextArea(prjFrame, 512);
|
|
sPrefsDlg.defDescription->weight = 100;
|
|
sPrefsDlg.defDescription->minH = wgtPixels(48);
|
|
wgtSetText(sPrefsDlg.defDescription, prefsGetString(sPrefs, "defaults", "description", ""));
|
|
|
|
// ---- OK / Cancel ----
|
|
WidgetT *btnRow = wgtHBox(root);
|
|
btnRow->spacing = wgtPixels(8);
|
|
btnRow->align = AlignEndE;
|
|
|
|
WidgetT *btnOk = wgtButton(btnRow, "OK");
|
|
btnOk->minW = wgtPixels(60);
|
|
btnOk->onClick = onPrefsOk;
|
|
|
|
WidgetT *btnCancel = wgtButton(btnRow, "Cancel");
|
|
btnCancel->minW = wgtPixels(60);
|
|
btnCancel->onClick = onPrefsCancel;
|
|
|
|
dvxFitWindow(sAc, win);
|
|
|
|
WindowT *prevModal = sAc->modalWindow;
|
|
sAc->modalWindow = win;
|
|
|
|
while (!sPrefsDlg.done && sAc->running) {
|
|
dvxUpdate(sAc);
|
|
}
|
|
|
|
if (sPrefsDlg.accepted) {
|
|
prefsSetBool(sPrefs, "editor", "renameSkipComments", wgtCheckboxIsChecked(sPrefsDlg.renameSkipComments));
|
|
prefsSetBool(sPrefs, "editor", "optionExplicit", wgtCheckboxIsChecked(sPrefsDlg.optionExplicit));
|
|
prefsSetBool(sPrefs, "editor", "useSpaces", wgtCheckboxIsChecked(sPrefsDlg.useSpaces));
|
|
|
|
const char *tw = wgtGetText(sPrefsDlg.tabWidthInput);
|
|
int32_t tabW = tw ? atoi(tw) : 3;
|
|
|
|
if (tabW < 1) { tabW = 1; }
|
|
if (tabW > 8) { tabW = 8; }
|
|
|
|
prefsSetInt(sPrefs, "editor", "tabWidth", tabW);
|
|
|
|
if (sEditor) {
|
|
wgtTextAreaSetTabWidth(sEditor, tabW);
|
|
wgtTextAreaSetUseTabChar(sEditor, !wgtCheckboxIsChecked(sPrefsDlg.useSpaces));
|
|
}
|
|
|
|
const char *val;
|
|
|
|
val = wgtGetText(sPrefsDlg.defAuthor);
|
|
prefsSetString(sPrefs, "defaults", "author", val ? val : "");
|
|
|
|
val = wgtGetText(sPrefsDlg.defCompany);
|
|
prefsSetString(sPrefs, "defaults", "company", val ? val : "");
|
|
|
|
val = wgtGetText(sPrefsDlg.defVersion);
|
|
prefsSetString(sPrefs, "defaults", "version", val ? val : "1.0");
|
|
|
|
val = wgtGetText(sPrefsDlg.defCopyright);
|
|
prefsSetString(sPrefs, "defaults", "copyright", val ? val : "");
|
|
|
|
val = wgtGetText(sPrefsDlg.defDescription);
|
|
prefsSetString(sPrefs, "defaults", "description", val ? val : "");
|
|
|
|
prefsSave(sPrefs);
|
|
}
|
|
|
|
sAc->modalWindow = prevModal;
|
|
dvxDestroyWindow(sAc, win);
|
|
}
|
|
|
|
|
|
static void handleWindowCmd(int32_t cmd) {
|
|
switch (cmd) {
|
|
case CMD_WIN_CODE:
|
|
showCodeWindow();
|
|
if (sEditor && !sEditor->onChange) {
|
|
sEditor->onChange = onEditorChange;
|
|
}
|
|
break;
|
|
|
|
case CMD_WIN_OUTPUT:
|
|
showOutputWindow();
|
|
break;
|
|
|
|
case CMD_WIN_IMM:
|
|
showImmediateWindow();
|
|
break;
|
|
|
|
case CMD_WIN_TOOLBOX:
|
|
if (!sToolboxWin) {
|
|
sToolboxWin = tbxCreate(sAc, &sDesigner);
|
|
|
|
if (sToolboxWin) {
|
|
sToolboxWin->y = toolbarBottom();
|
|
sToolboxWin->onMenu = onMenu;
|
|
sToolboxWin->accelTable = sWin ? sWin->accelTable : NULL;
|
|
}
|
|
}
|
|
break;
|
|
|
|
case CMD_WIN_PROPS:
|
|
if (!sPropsWin) {
|
|
sPropsWin = prpCreate(sAc, &sDesigner);
|
|
|
|
if (sPropsWin) {
|
|
sPropsWin->y = toolbarBottom();
|
|
sPropsWin->onMenu = onMenu;
|
|
sPropsWin->accelTable = sWin ? sWin->accelTable : NULL;
|
|
}
|
|
}
|
|
break;
|
|
|
|
case CMD_WIN_PROJECT:
|
|
if (!sProjectWin) {
|
|
sProjectWin = prjCreateWindow(sAc, &sProject, onPrjFileClick);
|
|
|
|
if (sProjectWin) {
|
|
sProjectWin->y = toolbarBottom() + 25;
|
|
sProjectWin->onClose = onProjectWinClose;
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
|
|
static void handleProjectCmd(int32_t cmd) {
|
|
switch (cmd) {
|
|
case CMD_PRJ_NEW:
|
|
newProject();
|
|
break;
|
|
|
|
case CMD_PRJ_OPEN:
|
|
openProject();
|
|
break;
|
|
|
|
case CMD_PRJ_SAVE:
|
|
if (sProject.projectPath[0] != '\0') {
|
|
prjSave(&sProject);
|
|
sProject.dirty = false;
|
|
setStatus("Project saved.");
|
|
}
|
|
break;
|
|
|
|
case CMD_PRJ_CLOSE:
|
|
if (promptAndSave()) {
|
|
closeProject();
|
|
}
|
|
break;
|
|
|
|
case CMD_PRJ_PROPS:
|
|
if (sProject.projectPath[0] != '\0') {
|
|
if (prjPropertiesDialog(sAc, &sProject, sCtx->appPath)) {
|
|
char title[300];
|
|
snprintf(title, sizeof(title), "DVX BASIC - [%s]", sProject.name);
|
|
dvxSetTitle(sAc, sWin, title);
|
|
|
|
if (sProjectWin) {
|
|
prjRebuildTree(&sProject);
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
|
|
case CMD_PRJ_REMOVE:
|
|
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;
|
|
}
|
|
}
|
|
|
|
|
|
static void onMenu(WindowT *win, int32_t menuId) {
|
|
(void)win;
|
|
|
|
handleFileCmd(menuId);
|
|
handleEditCmd(menuId);
|
|
handleRunCmd(menuId);
|
|
handleViewCmd(menuId);
|
|
handleWindowCmd(menuId);
|
|
handleProjectCmd(menuId);
|
|
|
|
if (menuId == CMD_PREFERENCES) {
|
|
showPreferencesDialog();
|
|
}
|
|
|
|
if (menuId == CMD_HELP_ABOUT) {
|
|
dvxMessageBox(sAc, "About DVX BASIC",
|
|
"DVX BASIC 1.0\n"
|
|
"Visual BASIC Development Environment\n"
|
|
"for the DVX GUI System\n"
|
|
"\n"
|
|
"Copyright 2026 Scott Duensing",
|
|
MB_OK | MB_ICONINFO);
|
|
}
|
|
}
|
|
|
|
// ============================================================
|
|
// isCtrlArrayInDesigner -- check if a control name is a control
|
|
// array member in the current designer form.
|
|
// ============================================================
|
|
|
|
static bool isCtrlArrayInDesigner(const char *ctrlName) {
|
|
if (!sDesigner.form) {
|
|
return false;
|
|
}
|
|
|
|
int32_t count = (int32_t)arrlen(sDesigner.form->controls);
|
|
|
|
for (int32_t i = 0; i < count; i++) {
|
|
if (strcasecmp(sDesigner.form->controls[i].name, ctrlName) == 0 && sDesigner.form->controls[i].index >= 0) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// getEventExtraParams -- return the extra parameters for a
|
|
// known event type (the part after "Index As Integer").
|
|
// ============================================================
|
|
|
|
static const char *getEventExtraParams(const char *evtName) {
|
|
if (strcasecmp(evtName, "KeyPress") == 0) { return ", KeyAscii As Integer"; }
|
|
if (strcasecmp(evtName, "KeyDown") == 0) { return ", KeyCode As Integer, Shift As Integer"; }
|
|
if (strcasecmp(evtName, "KeyUp") == 0) { return ", KeyCode As Integer, Shift As Integer"; }
|
|
if (strcasecmp(evtName, "MouseDown") == 0) { return ", Button As Integer, X As Integer, Y As Integer"; }
|
|
if (strcasecmp(evtName, "MouseUp") == 0) { return ", Button As Integer, X As Integer, Y As Integer"; }
|
|
if (strcasecmp(evtName, "MouseMove") == 0) { return ", Button As Integer, X As Integer, Y As Integer"; }
|
|
if (strcasecmp(evtName, "Scroll") == 0) { return ", Delta As Integer"; }
|
|
return "";
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// onEvtDropdownChange
|
|
// ============================================================
|
|
//
|
|
// Navigate to the selected procedure when the event dropdown changes.
|
|
|
|
static void onEvtDropdownChange(WidgetT *w) {
|
|
(void)w;
|
|
|
|
if (sDropdownNavSuppressed) {
|
|
return;
|
|
}
|
|
|
|
if (!sObjDropdown || !sEvtDropdown || !sEditor) {
|
|
return;
|
|
}
|
|
|
|
int32_t objIdx = wgtDropdownGetSelected(sObjDropdown);
|
|
int32_t evtIdx = wgtDropdownGetSelected(sEvtDropdown);
|
|
|
|
if (objIdx < 0 || objIdx >= (int32_t)arrlen(sObjItems) ||
|
|
evtIdx < 0 || evtIdx >= (int32_t)arrlen(sEvtItems)) {
|
|
return;
|
|
}
|
|
|
|
const char *selObj = sObjItems[objIdx];
|
|
const char *selEvt = sEvtItems[evtIdx];
|
|
|
|
// (Global) shows the General module-level section
|
|
if (strcasecmp(selEvt, "(Global)") == 0) {
|
|
showProc(-1);
|
|
return;
|
|
}
|
|
|
|
// Strip brackets if present (unimplemented event)
|
|
char evtName[64];
|
|
if (selEvt[0] == '[') {
|
|
snprintf(evtName, sizeof(evtName), "%s", selEvt + 1);
|
|
int32_t len = (int32_t)strlen(evtName);
|
|
|
|
if (len > 0 && evtName[len - 1] == ']') {
|
|
evtName[len - 1] = '\0';
|
|
}
|
|
} else {
|
|
snprintf(evtName, sizeof(evtName), "%s", selEvt);
|
|
}
|
|
|
|
// Search for an existing proc matching object + event
|
|
int32_t procCount = (int32_t)arrlen(sProcTable);
|
|
|
|
for (int32_t i = 0; i < procCount; i++) {
|
|
if (strcasecmp(sProcTable[i].objName, selObj) == 0 &&
|
|
strcasecmp(sProcTable[i].evtName, evtName) == 0) {
|
|
showProc(i);
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Not found -- create a new sub skeleton for editing.
|
|
// Don't mark dirty yet; saveCurProc will discard it if the
|
|
// user doesn't add any code.
|
|
char subName[128];
|
|
snprintf(subName, sizeof(subName), "%s_%s", selObj, evtName);
|
|
|
|
char skeleton[512];
|
|
|
|
if (isCtrlArrayInDesigner(selObj)) {
|
|
snprintf(skeleton, sizeof(skeleton), "Sub %s (Index As Integer%s)\n\nEnd Sub\n", subName, getEventExtraParams(evtName));
|
|
} else {
|
|
snprintf(skeleton, sizeof(skeleton), "Sub %s ()\n\nEnd Sub\n", subName);
|
|
}
|
|
|
|
arrput(sProcBufs, strdup(skeleton));
|
|
showProc((int32_t)arrlen(sProcBufs) - 1);
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// onImmediateChange
|
|
// ============================================================
|
|
//
|
|
// Detect Enter in the Immediate window and evaluate the last line.
|
|
|
|
static void onImmediateChange(WidgetT *w) {
|
|
(void)w;
|
|
|
|
if (!sImmediate) {
|
|
return;
|
|
}
|
|
|
|
const char *text = wgtGetText(sImmediate);
|
|
|
|
if (!text) {
|
|
return;
|
|
}
|
|
|
|
int32_t len = (int32_t)strlen(text);
|
|
|
|
if (len < 2 || text[len - 1] != '\n') {
|
|
return;
|
|
}
|
|
|
|
// Find the start of the line before the trailing newline
|
|
int32_t lineEnd = len - 1;
|
|
int32_t lineStart = lineEnd - 1;
|
|
|
|
while (lineStart > 0 && text[lineStart - 1] != '\n') {
|
|
lineStart--;
|
|
}
|
|
|
|
if (lineStart >= lineEnd) {
|
|
return;
|
|
}
|
|
|
|
// Extract the line
|
|
char expr[512];
|
|
int32_t lineLen = lineEnd - lineStart;
|
|
|
|
if (lineLen >= (int32_t)sizeof(expr)) {
|
|
lineLen = (int32_t)sizeof(expr) - 1;
|
|
}
|
|
|
|
memcpy(expr, text + lineStart, lineLen);
|
|
expr[lineLen] = '\0';
|
|
|
|
evaluateImmediate(expr);
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// onObjDropdownChange
|
|
// ============================================================
|
|
//
|
|
// Update the Event dropdown when the Object selection changes.
|
|
|
|
// Common events available on all controls
|
|
static const char *sCommonEvents[] = {
|
|
"Click", "DblClick", "Change", "GotFocus", "LostFocus",
|
|
"KeyPress", "KeyDown", "KeyUp",
|
|
"MouseDown", "MouseUp", "MouseMove", "Scroll",
|
|
NULL
|
|
};
|
|
|
|
// Form-specific events
|
|
static const char *sFormEvents[] = {
|
|
"Load", "QueryUnload", "Unload", "Resize", "Activate", "Deactivate",
|
|
"KeyPress", "KeyDown", "KeyUp",
|
|
"MouseDown", "MouseUp", "MouseMove",
|
|
NULL
|
|
};
|
|
|
|
// Buffer for event dropdown labels (with [] for unimplemented)
|
|
static char sEvtLabelBufs[64][32];
|
|
|
|
static void onObjDropdownChange(WidgetT *w) {
|
|
(void)w;
|
|
|
|
if (!sObjDropdown || !sEvtDropdown) {
|
|
return;
|
|
}
|
|
|
|
int32_t objIdx = wgtDropdownGetSelected(sObjDropdown);
|
|
|
|
if (objIdx < 0 || objIdx >= (int32_t)arrlen(sObjItems)) {
|
|
return;
|
|
}
|
|
|
|
const char *selObj = sObjItems[objIdx];
|
|
|
|
// Collect which events already have code
|
|
arrsetlen(sEvtItems, 0);
|
|
|
|
int32_t procCount = (int32_t)arrlen(sProcTable);
|
|
const char **existingEvts = NULL; // stb_ds temp array
|
|
|
|
for (int32_t i = 0; i < procCount; i++) {
|
|
if (strcasecmp(sProcTable[i].objName, selObj) == 0) {
|
|
arrput(existingEvts, sProcTable[i].evtName);
|
|
}
|
|
}
|
|
|
|
// Determine which event list to use
|
|
const char **availEvents = sCommonEvents;
|
|
|
|
if (strcasecmp(selObj, "(General)") == 0) {
|
|
// Always include (Global) to access module-level code
|
|
arrput(sEvtItems, "(Global)");
|
|
|
|
for (int32_t i = 0; i < (int32_t)arrlen(existingEvts); i++) {
|
|
arrput(sEvtItems, existingEvts[i]);
|
|
}
|
|
|
|
arrfree(existingEvts);
|
|
|
|
int32_t evtCount = (int32_t)arrlen(sEvtItems);
|
|
|
|
// Sort procs after (Global)
|
|
if (evtCount > 2) {
|
|
qsort(sEvtItems + 1, evtCount - 1, sizeof(const char *), cmpStrPtrs);
|
|
}
|
|
|
|
wgtDropdownSetItems(sEvtDropdown, sEvtItems, evtCount);
|
|
wgtDropdownSetSelected(sEvtDropdown, 0);
|
|
onEvtDropdownChange(sEvtDropdown);
|
|
|
|
return;
|
|
}
|
|
|
|
// Check if this is a form name
|
|
bool isForm = false;
|
|
|
|
if (sDesigner.form && strcasecmp(selObj, sDesigner.form->name) == 0) {
|
|
isForm = true;
|
|
availEvents = sFormEvents;
|
|
}
|
|
|
|
// Check if this is a menu item (only event is Click)
|
|
bool isMenuItem = false;
|
|
|
|
if (!isForm && sDesigner.form) {
|
|
for (int32_t i = 0; i < (int32_t)arrlen(sDesigner.form->menuItems); i++) {
|
|
if (strcasecmp(sDesigner.form->menuItems[i].name, selObj) == 0) {
|
|
isMenuItem = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (isMenuItem) {
|
|
static const char *sMenuEvents[] = { "Click", NULL };
|
|
availEvents = sMenuEvents;
|
|
}
|
|
|
|
// Get widget-specific events from the interface
|
|
const WgtIfaceT *iface = NULL;
|
|
|
|
if (!isForm && !isMenuItem && sDesigner.form) {
|
|
for (int32_t i = 0; i < (int32_t)arrlen(sDesigner.form->controls); i++) {
|
|
if (strcasecmp(sDesigner.form->controls[i].name, selObj) == 0) {
|
|
const char *wgtName = wgtFindByBasName(sDesigner.form->controls[i].typeName);
|
|
|
|
if (wgtName) {
|
|
iface = wgtGetIface(wgtName);
|
|
}
|
|
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Build the event list: standard events + widget-specific events
|
|
int32_t labelIdx = 0;
|
|
|
|
// Add standard events (common or form)
|
|
for (int32_t i = 0; availEvents[i]; i++) {
|
|
bool hasCode = false;
|
|
|
|
for (int32_t j = 0; j < (int32_t)arrlen(existingEvts); j++) {
|
|
if (strcasecmp(existingEvts[j], availEvents[i]) == 0) {
|
|
hasCode = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (labelIdx < 64) {
|
|
if (hasCode) {
|
|
snprintf(sEvtLabelBufs[labelIdx], 32, "%s", availEvents[i]);
|
|
} else {
|
|
snprintf(sEvtLabelBufs[labelIdx], 32, "[%s]", availEvents[i]);
|
|
}
|
|
|
|
arrput(sEvtItems, sEvtLabelBufs[labelIdx]);
|
|
labelIdx++;
|
|
}
|
|
}
|
|
|
|
// Add widget-specific events
|
|
if (iface) {
|
|
for (int32_t i = 0; i < iface->eventCount; i++) {
|
|
const char *evtName = iface->events[i].name;
|
|
|
|
// Skip if already in the standard list
|
|
bool alreadyListed = false;
|
|
|
|
for (int32_t j = 0; availEvents[j]; j++) {
|
|
if (strcasecmp(availEvents[j], evtName) == 0) {
|
|
alreadyListed = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (alreadyListed) {
|
|
continue;
|
|
}
|
|
|
|
bool hasCode = false;
|
|
|
|
for (int32_t j = 0; j < (int32_t)arrlen(existingEvts); j++) {
|
|
if (strcasecmp(existingEvts[j], evtName) == 0) {
|
|
hasCode = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (labelIdx < 64) {
|
|
if (hasCode) {
|
|
snprintf(sEvtLabelBufs[labelIdx], 32, "%s", evtName);
|
|
} else {
|
|
snprintf(sEvtLabelBufs[labelIdx], 32, "[%s]", evtName);
|
|
}
|
|
|
|
arrput(sEvtItems, sEvtLabelBufs[labelIdx]);
|
|
labelIdx++;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Add any existing events not in the standard/widget list (custom subs)
|
|
for (int32_t i = 0; i < (int32_t)arrlen(existingEvts); i++) {
|
|
bool alreadyListed = false;
|
|
int32_t evtCount = (int32_t)arrlen(sEvtItems);
|
|
|
|
for (int32_t j = 0; j < evtCount; j++) {
|
|
const char *label = sEvtItems[j];
|
|
|
|
// Strip brackets for comparison
|
|
if (label[0] == '[') { label++; }
|
|
|
|
if (strncasecmp(label, existingEvts[i], strlen(existingEvts[i])) == 0) {
|
|
alreadyListed = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!alreadyListed && labelIdx < 64) {
|
|
snprintf(sEvtLabelBufs[labelIdx], 32, "%s", existingEvts[i]);
|
|
arrput(sEvtItems, sEvtLabelBufs[labelIdx]);
|
|
labelIdx++;
|
|
}
|
|
}
|
|
|
|
arrfree(existingEvts);
|
|
|
|
int32_t evtCount = (int32_t)arrlen(sEvtItems);
|
|
|
|
// Sort: implemented events first, then unimplemented, alphabetical within each
|
|
if (evtCount > 1) {
|
|
qsort(sEvtItems, evtCount, sizeof(const char *), cmpEvtPtrs);
|
|
}
|
|
|
|
wgtDropdownSetItems(sEvtDropdown, sEvtItems, evtCount);
|
|
|
|
if (evtCount > 0) {
|
|
// Find the first implemented event (no brackets)
|
|
int32_t selectIdx = -1;
|
|
|
|
for (int32_t i = 0; i < evtCount; i++) {
|
|
if (sEvtItems[i][0] != '[') {
|
|
selectIdx = i;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (selectIdx >= 0) {
|
|
// Navigate to the first implemented event
|
|
wgtDropdownSetSelected(sEvtDropdown, selectIdx);
|
|
onEvtDropdownChange(sEvtDropdown);
|
|
} else {
|
|
// No implemented events -- find and select the default event
|
|
// for this widget type, which will create its skeleton
|
|
const char *defEvt = NULL;
|
|
|
|
if (isForm) {
|
|
defEvt = dsgnDefaultEvent("Form");
|
|
} else if (sDesigner.form) {
|
|
for (int32_t i = 0; i < (int32_t)arrlen(sDesigner.form->controls); i++) {
|
|
if (strcasecmp(sDesigner.form->controls[i].name, selObj) == 0) {
|
|
defEvt = dsgnDefaultEvent(sDesigner.form->controls[i].typeName);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!defEvt) {
|
|
defEvt = "Click";
|
|
}
|
|
|
|
// Find the default event in the list (it will be bracketed)
|
|
for (int32_t i = 0; i < evtCount; i++) {
|
|
const char *label = sEvtItems[i];
|
|
|
|
if (label[0] == '[') {
|
|
label++;
|
|
}
|
|
|
|
if (strncasecmp(label, defEvt, strlen(defEvt)) == 0) {
|
|
wgtDropdownSetSelected(sEvtDropdown, i);
|
|
onEvtDropdownChange(sEvtDropdown);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// onOpenClick
|
|
// ============================================================
|
|
|
|
// ============================================================
|
|
// printCallback
|
|
// ============================================================
|
|
|
|
static void printCallback(void *ctx, const char *text, bool newline) {
|
|
(void)ctx;
|
|
|
|
if (!text) {
|
|
return;
|
|
}
|
|
|
|
int32_t textLen = (int32_t)strlen(text);
|
|
|
|
// Append to output buffer
|
|
if (sOutputLen + textLen < IDE_MAX_OUTPUT - 2) {
|
|
memcpy(sOutputBuf + sOutputLen, text, textLen);
|
|
sOutputLen += textLen;
|
|
}
|
|
|
|
if (newline && sOutputLen < IDE_MAX_OUTPUT - 2) {
|
|
sOutputBuf[sOutputLen++] = '\n';
|
|
}
|
|
|
|
sOutputBuf[sOutputLen] = '\0';
|
|
|
|
// Update the output textarea immediately so PRINT is visible
|
|
if (sOutput) {
|
|
setOutputText(sOutputBuf);
|
|
}
|
|
}
|
|
|
|
// ============================================================
|
|
// onFormWinMouse
|
|
// ============================================================
|
|
//
|
|
// Handle mouse events on the form designer window. Coordinates
|
|
// are relative to the window's client area (content box origin).
|
|
|
|
// ============================================================
|
|
// onFormWinKey
|
|
// ============================================================
|
|
|
|
static void dsgnCopySelected(void) {
|
|
if (!sDesigner.form || sDesigner.selectedIdx < 0) {
|
|
return;
|
|
}
|
|
|
|
int32_t count = (int32_t)arrlen(sDesigner.form->controls);
|
|
|
|
if (sDesigner.selectedIdx >= count) {
|
|
return;
|
|
}
|
|
|
|
// Serialize the selected control to FRM text
|
|
char buf[2048];
|
|
int32_t pos = 0;
|
|
DsgnControlT *ctrl = &sDesigner.form->controls[sDesigner.selectedIdx];
|
|
|
|
pos += snprintf(buf + pos, sizeof(buf) - pos, "Begin %s %s\n", ctrl->typeName, ctrl->name);
|
|
|
|
if (ctrl->index >= 0) {
|
|
pos += snprintf(buf + pos, sizeof(buf) - pos, " Index = %d\n", (int)ctrl->index);
|
|
}
|
|
|
|
pos += snprintf(buf + pos, sizeof(buf) - pos, " Caption = \"%s\"\n", wgtGetText(ctrl->widget) ? wgtGetText(ctrl->widget) : "");
|
|
|
|
if (ctrl->width > 0) {
|
|
pos += snprintf(buf + pos, sizeof(buf) - pos, " MinWidth = %d\n", (int)ctrl->width);
|
|
}
|
|
|
|
if (ctrl->height > 0) {
|
|
pos += snprintf(buf + pos, sizeof(buf) - pos, " MinHeight = %d\n", (int)ctrl->height);
|
|
}
|
|
|
|
if (ctrl->maxWidth > 0) {
|
|
pos += snprintf(buf + pos, sizeof(buf) - pos, " MaxWidth = %d\n", (int)ctrl->maxWidth);
|
|
}
|
|
|
|
if (ctrl->maxHeight > 0) {
|
|
pos += snprintf(buf + pos, sizeof(buf) - pos, " MaxHeight = %d\n", (int)ctrl->maxHeight);
|
|
}
|
|
|
|
if (ctrl->weight > 0) {
|
|
pos += snprintf(buf + pos, sizeof(buf) - pos, " Weight = %d\n", (int)ctrl->weight);
|
|
}
|
|
|
|
for (int32_t i = 0; i < ctrl->propCount; i++) {
|
|
if (strcasecmp(ctrl->props[i].name, "Caption") == 0) {
|
|
continue;
|
|
}
|
|
|
|
pos += snprintf(buf + pos, sizeof(buf) - pos, " %s = \"%s\"\n", ctrl->props[i].name, ctrl->props[i].value);
|
|
}
|
|
|
|
// Save interface properties
|
|
if (ctrl->widget) {
|
|
const char *wgtName = wgtFindByBasName(ctrl->typeName);
|
|
const WgtIfaceT *iface = wgtName ? wgtGetIface(wgtName) : NULL;
|
|
|
|
if (iface) {
|
|
for (int32_t i = 0; i < iface->propCount; i++) {
|
|
const WgtPropDescT *p = &iface->props[i];
|
|
|
|
if (!p->getFn) {
|
|
continue;
|
|
}
|
|
|
|
bool already = false;
|
|
|
|
for (int32_t j = 0; j < ctrl->propCount; j++) {
|
|
if (strcasecmp(ctrl->props[j].name, p->name) == 0) {
|
|
already = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (already) {
|
|
continue;
|
|
}
|
|
|
|
if (p->type == WGT_IFACE_ENUM && p->enumNames) {
|
|
int32_t v = ((int32_t (*)(const WidgetT *))p->getFn)(ctrl->widget);
|
|
const char *name = (v >= 0 && p->enumNames[v]) ? p->enumNames[v] : NULL;
|
|
|
|
if (name) {
|
|
pos += snprintf(buf + pos, sizeof(buf) - pos, " %s = %s\n", p->name, name);
|
|
}
|
|
} else if (p->type == WGT_IFACE_INT) {
|
|
int32_t v = ((int32_t (*)(const WidgetT *))p->getFn)(ctrl->widget);
|
|
pos += snprintf(buf + pos, sizeof(buf) - pos, " %s = %d\n", p->name, (int)v);
|
|
} else if (p->type == WGT_IFACE_BOOL) {
|
|
bool v = ((bool (*)(const WidgetT *))p->getFn)(ctrl->widget);
|
|
pos += snprintf(buf + pos, sizeof(buf) - pos, " %s = %s\n", p->name, v ? "True" : "False");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
pos += snprintf(buf + pos, sizeof(buf) - pos, "End\n");
|
|
dvxClipboardCopy(buf, pos);
|
|
}
|
|
|
|
|
|
static void dsgnPasteControl(void) {
|
|
if (!sDesigner.form || !sDesigner.form->contentBox) {
|
|
return;
|
|
}
|
|
|
|
int32_t clipLen = 0;
|
|
const char *clip = dvxClipboardGet(&clipLen);
|
|
|
|
if (!clip || clipLen <= 0) {
|
|
return;
|
|
}
|
|
|
|
// Verify it looks like a control definition
|
|
if (strncasecmp(clip, "Begin ", 6) != 0) {
|
|
return;
|
|
}
|
|
|
|
// Parse type and name from "Begin TypeName CtrlName"
|
|
const char *rest = clip + 6;
|
|
char typeName[DSGN_MAX_NAME];
|
|
char ctrlName[DSGN_MAX_NAME];
|
|
|
|
int32_t ti = 0;
|
|
|
|
while (*rest && *rest != ' ' && *rest != '\t' && ti < DSGN_MAX_NAME - 1) {
|
|
typeName[ti++] = *rest++;
|
|
}
|
|
|
|
typeName[ti] = '\0';
|
|
|
|
while (*rest == ' ' || *rest == '\t') {
|
|
rest++;
|
|
}
|
|
|
|
int32_t ci = 0;
|
|
|
|
while (*rest && *rest != ' ' && *rest != '\t' && *rest != '\r' && *rest != '\n' && ci < DSGN_MAX_NAME - 1) {
|
|
ctrlName[ci++] = *rest++;
|
|
}
|
|
|
|
ctrlName[ci] = '\0';
|
|
|
|
// Check if a control with the same name exists -- create control array
|
|
char newName[DSGN_MAX_NAME];
|
|
int32_t newIndex = -1;
|
|
bool nameExists = false;
|
|
int32_t highIdx = -1;
|
|
int32_t existCount = (int32_t)arrlen(sDesigner.form->controls);
|
|
|
|
for (int32_t i = 0; i < existCount; i++) {
|
|
if (strcasecmp(sDesigner.form->controls[i].name, ctrlName) == 0) {
|
|
nameExists = true;
|
|
if (sDesigner.form->controls[i].index > highIdx) {
|
|
highIdx = sDesigner.form->controls[i].index;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (nameExists) {
|
|
// Already a control array -- just add the next element
|
|
if (highIdx >= 0) {
|
|
snprintf(newName, DSGN_MAX_NAME, "%s", ctrlName);
|
|
newIndex = highIdx + 1;
|
|
} else {
|
|
// Not yet an array -- ask the user
|
|
int32_t result = dvxMessageBox(sAc, "Paste",
|
|
"A control with this name already exists.\n"
|
|
"Create a control array?",
|
|
MB_YESNO | MB_ICONQUESTION);
|
|
|
|
if (result == ID_YES) {
|
|
// Convert existing control to index 0
|
|
for (int32_t i = 0; i < existCount; i++) {
|
|
if (strcasecmp(sDesigner.form->controls[i].name, ctrlName) == 0) {
|
|
sDesigner.form->controls[i].index = 0;
|
|
break;
|
|
}
|
|
}
|
|
|
|
snprintf(newName, DSGN_MAX_NAME, "%s", ctrlName);
|
|
newIndex = 1;
|
|
} else {
|
|
// Rename to a unique name
|
|
dsgnAutoName(&sDesigner, typeName, newName, DSGN_MAX_NAME);
|
|
}
|
|
}
|
|
} else {
|
|
dsgnAutoName(&sDesigner, typeName, newName, DSGN_MAX_NAME);
|
|
}
|
|
|
|
// Create the control
|
|
DsgnControlT ctrl;
|
|
memset(&ctrl, 0, sizeof(ctrl));
|
|
ctrl.index = newIndex;
|
|
snprintf(ctrl.name, DSGN_MAX_NAME, "%s", newName);
|
|
snprintf(ctrl.typeName, DSGN_MAX_NAME, "%s", typeName);
|
|
|
|
// Parse properties from the clipboard text
|
|
const char *line = rest;
|
|
|
|
while (*line) {
|
|
while (*line == '\r' || *line == '\n') {
|
|
line++;
|
|
}
|
|
|
|
if (!*line) {
|
|
break;
|
|
}
|
|
|
|
const char *lineStart = line;
|
|
|
|
while (*line == ' ' || *line == '\t') {
|
|
line++;
|
|
}
|
|
|
|
// "End" terminates
|
|
if (strncasecmp(line, "End", 3) == 0 && (line[3] == '\0' || line[3] == '\r' || line[3] == '\n')) {
|
|
break;
|
|
}
|
|
|
|
// Parse "Key = Value"
|
|
char *eq = strchr(line, '=');
|
|
|
|
if (eq) {
|
|
char key[DSGN_MAX_NAME];
|
|
int32_t klen = 0;
|
|
const char *kp = line;
|
|
|
|
while (kp < eq && *kp != ' ' && *kp != '\t' && klen < DSGN_MAX_NAME - 1) {
|
|
key[klen++] = *kp++;
|
|
}
|
|
|
|
key[klen] = '\0';
|
|
|
|
char *vp = (char *)eq + 1;
|
|
|
|
while (*vp == ' ' || *vp == '\t') {
|
|
vp++;
|
|
}
|
|
|
|
char val[DSGN_MAX_TEXT];
|
|
int32_t vi = 0;
|
|
|
|
if (*vp == '"') {
|
|
vp++;
|
|
|
|
while (*vp && *vp != '"' && vi < DSGN_MAX_TEXT - 1) {
|
|
val[vi++] = *vp++;
|
|
}
|
|
} else {
|
|
while (*vp && *vp != '\r' && *vp != '\n' && vi < DSGN_MAX_TEXT - 1) {
|
|
val[vi++] = *vp++;
|
|
}
|
|
|
|
while (vi > 0 && (val[vi - 1] == ' ' || val[vi - 1] == '\t')) {
|
|
vi--;
|
|
}
|
|
}
|
|
|
|
val[vi] = '\0';
|
|
|
|
if (strcasecmp(key, "MinWidth") == 0 || strcasecmp(key, "Width") == 0) {
|
|
ctrl.width = atoi(val);
|
|
} else if (strcasecmp(key, "MinHeight") == 0 || strcasecmp(key, "Height") == 0) {
|
|
ctrl.height = atoi(val);
|
|
} else if (strcasecmp(key, "MaxWidth") == 0) {
|
|
ctrl.maxWidth = atoi(val);
|
|
} else if (strcasecmp(key, "MaxHeight") == 0) {
|
|
ctrl.maxHeight = atoi(val);
|
|
} else if (strcasecmp(key, "Weight") == 0) {
|
|
ctrl.weight = atoi(val);
|
|
} else if (ctrl.propCount < DSGN_MAX_PROPS) {
|
|
snprintf(ctrl.props[ctrl.propCount].name, DSGN_MAX_NAME, "%s", key);
|
|
snprintf(ctrl.props[ctrl.propCount].value, DSGN_MAX_TEXT, "%s", val);
|
|
ctrl.propCount++;
|
|
}
|
|
}
|
|
|
|
// Advance to next line
|
|
while (*line && *line != '\n') {
|
|
line++;
|
|
}
|
|
}
|
|
|
|
// Create the live widget
|
|
WidgetT *parentWidget = sDesigner.form->contentBox;
|
|
ctrl.widget = dsgnCreateDesignWidget(typeName, parentWidget);
|
|
|
|
if (ctrl.widget) {
|
|
if (ctrl.width > 0) { ctrl.widget->minW = wgtPixels(ctrl.width); }
|
|
if (ctrl.height > 0) { ctrl.widget->minH = wgtPixels(ctrl.height); }
|
|
if (ctrl.maxWidth > 0) { ctrl.widget->maxW = wgtPixels(ctrl.maxWidth); }
|
|
if (ctrl.maxHeight > 0) { ctrl.widget->maxH = wgtPixels(ctrl.maxHeight); }
|
|
ctrl.widget->weight = ctrl.weight;
|
|
wgtSetName(ctrl.widget, ctrl.name);
|
|
|
|
const char *caption = NULL;
|
|
|
|
for (int32_t pi = 0; pi < ctrl.propCount; pi++) {
|
|
if (strcasecmp(ctrl.props[pi].name, "Caption") == 0) {
|
|
caption = ctrl.props[pi].value;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (caption) {
|
|
wgtSetText(ctrl.widget, caption);
|
|
}
|
|
|
|
// Apply interface properties (Alignment, etc.)
|
|
const char *wgtName = wgtFindByBasName(typeName);
|
|
const WgtIfaceT *iface = wgtName ? wgtGetIface(wgtName) : NULL;
|
|
|
|
if (iface) {
|
|
for (int32_t pi = 0; pi < iface->propCount; pi++) {
|
|
const WgtPropDescT *p = &iface->props[pi];
|
|
|
|
if (!p->setFn) {
|
|
continue;
|
|
}
|
|
|
|
const char *val = NULL;
|
|
|
|
for (int32_t j = 0; j < ctrl.propCount; j++) {
|
|
if (strcasecmp(ctrl.props[j].name, p->name) == 0) {
|
|
val = ctrl.props[j].value;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!val) {
|
|
continue;
|
|
}
|
|
|
|
if (p->type == WGT_IFACE_ENUM && p->enumNames) {
|
|
for (int32_t en = 0; p->enumNames[en]; en++) {
|
|
if (strcasecmp(p->enumNames[en], val) == 0) {
|
|
((void (*)(WidgetT *, int32_t))p->setFn)(ctrl.widget, en);
|
|
break;
|
|
}
|
|
}
|
|
} else if (p->type == WGT_IFACE_INT) {
|
|
((void (*)(WidgetT *, int32_t))p->setFn)(ctrl.widget, atoi(val));
|
|
} else if (p->type == WGT_IFACE_BOOL) {
|
|
((void (*)(WidgetT *, bool))p->setFn)(ctrl.widget, strcasecmp(val, "True") == 0);
|
|
} else if (p->type == WGT_IFACE_STRING) {
|
|
((void (*)(WidgetT *, const char *))p->setFn)(ctrl.widget, val);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
arrput(sDesigner.form->controls, ctrl);
|
|
sDesigner.selectedIdx = (int32_t)arrlen(sDesigner.form->controls) - 1;
|
|
sDesigner.form->dirty = true;
|
|
|
|
prpRebuildTree(&sDesigner);
|
|
prpRefresh(&sDesigner);
|
|
|
|
if (sFormWin) {
|
|
dvxInvalidateWindow(sAc, sFormWin);
|
|
}
|
|
}
|
|
|
|
|
|
static void onFormWinKey(WindowT *win, int32_t key, int32_t mod) {
|
|
// Ctrl+C: copy selected control
|
|
if (key == 3 && (mod & ACCEL_CTRL)) {
|
|
dsgnCopySelected();
|
|
return;
|
|
}
|
|
|
|
// Ctrl+X: cut selected control
|
|
if (key == 24 && (mod & ACCEL_CTRL)) {
|
|
dsgnCopySelected();
|
|
|
|
if (sDesigner.selectedIdx >= 0) {
|
|
dsgnOnKey(&sDesigner, KEY_DELETE);
|
|
prpRebuildTree(&sDesigner);
|
|
prpRefresh(&sDesigner);
|
|
|
|
if (sFormWin) {
|
|
dvxInvalidateWindow(sAc, sFormWin);
|
|
}
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
// Ctrl+V: paste control
|
|
if (key == 22 && (mod & ACCEL_CTRL)) {
|
|
dsgnPasteControl();
|
|
return;
|
|
}
|
|
|
|
if (key == KEY_DELETE && sDesigner.selectedIdx >= 0) {
|
|
int32_t prevCount = sDesigner.form ? (int32_t)arrlen(sDesigner.form->controls) : 0;
|
|
dsgnOnKey(&sDesigner, KEY_DELETE);
|
|
int32_t newCount = sDesigner.form ? (int32_t)arrlen(sDesigner.form->controls) : 0;
|
|
|
|
if (newCount != prevCount) {
|
|
prpRebuildTree(&sDesigner);
|
|
prpRefresh(&sDesigner);
|
|
|
|
if (sFormWin) {
|
|
dvxInvalidateWindow(sAc, sFormWin);
|
|
}
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
// Forward unhandled keys to the widget system
|
|
widgetOnKey(win, key, mod);
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// onFormWinCursorQuery
|
|
// ============================================================
|
|
|
|
static int32_t onFormWinCursorQuery(WindowT *win, int32_t x, int32_t y) {
|
|
(void)win;
|
|
|
|
if (!sDesigner.form) {
|
|
return 0;
|
|
}
|
|
|
|
// Crosshair when placing a new control
|
|
if (sDesigner.activeTool[0] != '\0') {
|
|
return CURSOR_CROSSHAIR;
|
|
}
|
|
|
|
if (!sDesigner.form->controls) {
|
|
return 0;
|
|
}
|
|
|
|
int32_t count = (int32_t)arrlen(sDesigner.form->controls);
|
|
|
|
if (sDesigner.selectedIdx < 0 || sDesigner.selectedIdx >= count) {
|
|
return 0;
|
|
}
|
|
|
|
DsgnControlT *ctrl = &sDesigner.form->controls[sDesigner.selectedIdx];
|
|
|
|
if (!ctrl->widget || !ctrl->widget->visible || ctrl->widget->w <= 0 || ctrl->widget->h <= 0) {
|
|
return 0;
|
|
}
|
|
|
|
int32_t cx = ctrl->widget->x;
|
|
int32_t cy = ctrl->widget->y;
|
|
int32_t cw = ctrl->widget->w;
|
|
int32_t ch = ctrl->widget->h;
|
|
int32_t hs = DSGN_HANDLE_SIZE;
|
|
|
|
// SE handle (check first -- overlaps with E and S)
|
|
if (x >= cx + cw - hs/2 && x < cx + cw + hs/2 && y >= cy + ch - hs/2 && y < cy + ch + hs/2) {
|
|
return CURSOR_RESIZE_DIAG_NWSE;
|
|
}
|
|
|
|
// E handle (right edge center)
|
|
if (x >= cx + cw - hs/2 && x < cx + cw + hs/2 && y >= cy + ch/2 - hs/2 && y < cy + ch/2 + hs/2) {
|
|
return CURSOR_RESIZE_H;
|
|
}
|
|
|
|
// S handle (bottom center)
|
|
if (x >= cx + cw/2 - hs/2 && x < cx + cw/2 + hs/2 && y >= cy + ch - hs/2 && y < cy + ch + hs/2) {
|
|
return CURSOR_RESIZE_V;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
|
|
// selectDropdowns -- set the Object and Event dropdowns to match a
|
|
// given control name and event name.
|
|
|
|
// selectDropdowns -- set the Object and Event dropdown selections to
|
|
// match a given control/event without triggering navigation callbacks.
|
|
// The caller is responsible for having already navigated to the proc.
|
|
|
|
static void selectDropdowns(const char *objName, const char *evtName) {
|
|
if (!sObjDropdown || !sEvtDropdown) {
|
|
return;
|
|
}
|
|
|
|
// Select the object
|
|
int32_t objCount = (int32_t)arrlen(sObjItems);
|
|
|
|
for (int32_t i = 0; i < objCount; i++) {
|
|
if (strcasecmp(sObjItems[i], objName) == 0) {
|
|
wgtDropdownSetSelected(sObjDropdown, i);
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Rebuild the event list for this object but suppress navigation
|
|
bool savedSuppress = sDropdownNavSuppressed;
|
|
sDropdownNavSuppressed = true;
|
|
onObjDropdownChange(sObjDropdown);
|
|
sDropdownNavSuppressed = savedSuppress;
|
|
|
|
// Now select the specific event
|
|
int32_t evtCount = (int32_t)arrlen(sEvtItems);
|
|
|
|
for (int32_t i = 0; i < evtCount; i++) {
|
|
const char *label = sEvtItems[i];
|
|
|
|
if (label[0] == '[') {
|
|
label++;
|
|
}
|
|
|
|
if (strncasecmp(label, evtName, strlen(evtName)) == 0) {
|
|
wgtDropdownSetSelected(sEvtDropdown, i);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
// navigateToEventSub -- open code editor at the default event sub for the
|
|
// selected control (or form). Creates the sub skeleton if it doesn't exist.
|
|
// Code is stored in the .frm file's code section (sDesigner.form->code).
|
|
|
|
static void 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);
|
|
|
|
// Load form code into editor (stashes existing code, parses procs,
|
|
// populates dropdowns without triggering navigation)
|
|
loadFormCodeIntoEditor();
|
|
|
|
if (!sEditor) {
|
|
return;
|
|
}
|
|
|
|
// Search for existing procedure
|
|
int32_t procCount = (int32_t)arrlen(sProcTable);
|
|
|
|
for (int32_t i = 0; i < procCount; i++) {
|
|
char fullName[128];
|
|
snprintf(fullName, sizeof(fullName), "%s_%s", sProcTable[i].objName, sProcTable[i].evtName);
|
|
|
|
if (strcasecmp(fullName, subName) == 0) {
|
|
stashDesignerState();
|
|
showProc(i);
|
|
if (sEditor && !sEditor->onChange) {
|
|
sEditor->onChange = onEditorChange;
|
|
}
|
|
selectDropdowns(ctrlName, eventName);
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Not found -- create a new sub skeleton for editing.
|
|
// Don't mark dirty yet; saveCurProc will discard it if the
|
|
// user doesn't add any code.
|
|
char skeleton[512];
|
|
|
|
if (isCtrlArrayInDesigner(ctrlName)) {
|
|
snprintf(skeleton, sizeof(skeleton), "Sub %s (Index As Integer%s)\n\nEnd Sub\n", subName, getEventExtraParams(eventName));
|
|
} else {
|
|
snprintf(skeleton, sizeof(skeleton), "Sub %s ()\n\nEnd Sub\n", subName);
|
|
}
|
|
|
|
arrput(sProcBufs, strdup(skeleton));
|
|
|
|
// Show the new procedure (it's the last one)
|
|
stashDesignerState();
|
|
showProc((int32_t)arrlen(sProcBufs) - 1);
|
|
if (sEditor && !sEditor->onChange) {
|
|
sEditor->onChange = onEditorChange;
|
|
}
|
|
selectDropdowns(ctrlName, eventName);
|
|
}
|
|
|
|
|
|
static void 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();
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// stashDesignerState -- save current editor content and set status
|
|
// ============================================================
|
|
|
|
static void stashDesignerState(void) {
|
|
stashCurrentFile();
|
|
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;
|
|
|
|
// Build preview menu bar from form's menu items
|
|
dsgnBuildPreviewMenuBar(sFormWin, sDesigner.form);
|
|
|
|
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();
|
|
sToolboxWin->onMenu = onMenu;
|
|
sToolboxWin->accelTable = sWin ? sWin->accelTable : NULL;
|
|
}
|
|
}
|
|
|
|
if (!sPropsWin) {
|
|
sPropsWin = prpCreate(sAc, &sDesigner);
|
|
|
|
if (sPropsWin) {
|
|
sPropsWin->y = toolbarBottom();
|
|
sPropsWin->onMenu = onMenu;
|
|
sPropsWin->accelTable = sWin ? sWin->accelTable : NULL;
|
|
}
|
|
}
|
|
|
|
dvxInvalidateWindow(sAc, sFormWin);
|
|
setStatus("Design view open.");
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// teardownFormWin -- destroy the form designer window if it exists
|
|
// ============================================================
|
|
|
|
static void teardownFormWin(void) {
|
|
if (sFormWin) {
|
|
dvxDestroyWindow(sAc, sFormWin);
|
|
cleanupFormWin();
|
|
}
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// Toolbar button handlers
|
|
// ============================================================
|
|
|
|
static void onTbOpen(WidgetT *w) { (void)w; 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; stashDesignerState(); }
|
|
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);
|
|
|
|
wgtLabel(dropdownRow, "Object:");
|
|
|
|
sObjDropdown = wgtDropdown(dropdownRow);
|
|
sObjDropdown->weight = 100;
|
|
sObjDropdown->onChange = onObjDropdownChange;
|
|
wgtDropdownSetItems(sObjDropdown, NULL, 0);
|
|
|
|
wgtLabel(dropdownRow, "Function:");
|
|
|
|
sEvtDropdown = wgtDropdown(dropdownRow);
|
|
sEvtDropdown->weight = 100;
|
|
sEvtDropdown->onChange = onEvtDropdownChange;
|
|
wgtDropdownSetItems(sEvtDropdown, NULL, 0);
|
|
|
|
sEditor = wgtTextArea(codeRoot, IDE_MAX_SOURCE);
|
|
sEditor->weight = 100;
|
|
wgtTextAreaSetColorize(sEditor, basicColorize, NULL);
|
|
wgtTextAreaSetShowLineNumbers(sEditor, true);
|
|
wgtTextAreaSetAutoIndent(sEditor, true);
|
|
wgtTextAreaSetCaptureTabs(sEditor, true);
|
|
wgtTextAreaSetTabWidth(sEditor, prefsGetInt(sPrefs, "editor", "tabWidth", 3));
|
|
wgtTextAreaSetUseTabChar(sEditor, !prefsGetBool(sPrefs, "editor", "useSpaces", true));
|
|
|
|
// onChange is set after initial content is loaded by the caller
|
|
// (navigateToEventSub, onPrjFileClick, etc.) to prevent false dirty marking.
|
|
|
|
updateProjectMenuState();
|
|
}
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// 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');
|
|
bool hasFile = (hasProject && sProject.activeFileIdx >= 0);
|
|
bool hasForm = (hasFile && sProject.files[sProject.activeFileIdx].isForm);
|
|
|
|
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);
|
|
|
|
wmMenuItemSetEnabled(sWin->menuBar, CMD_FIND, hasProject);
|
|
wmMenuItemSetEnabled(sWin->menuBar, CMD_FIND_NEXT, hasProject);
|
|
wmMenuItemSetEnabled(sWin->menuBar, CMD_REPLACE, hasProject);
|
|
|
|
wmMenuItemSetEnabled(sWin->menuBar, CMD_VIEW_CODE, hasFile);
|
|
wmMenuItemSetEnabled(sWin->menuBar, CMD_VIEW_DESIGN, hasForm);
|
|
wmMenuItemSetEnabled(sWin->menuBar, CMD_MENU_EDITOR, hasForm);
|
|
}
|
|
|
|
|
|
static void updateDirtyIndicators(void) {
|
|
// Sync form->dirty to the project file entry so the tree, title
|
|
// bar, and save logic all see a single consistent modified flag.
|
|
if (sDesigner.form && sDesigner.form->dirty) {
|
|
int32_t idx = sProject.activeFileIdx;
|
|
if (idx >= 0 && idx < sProject.fileCount && sProject.files[idx].isForm) {
|
|
sProject.files[idx].modified = true;
|
|
}
|
|
}
|
|
|
|
// Toolbar title: "DVX BASIC - [ProjectName] *"
|
|
if (sWin && sProject.projectPath[0] != '\0') {
|
|
char title[300];
|
|
bool anyDirty = sProject.dirty;
|
|
|
|
if (!anyDirty) {
|
|
for (int32_t i = 0; i < sProject.fileCount; i++) {
|
|
if (sProject.files[i].modified) {
|
|
anyDirty = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
snprintf(title, sizeof(title), "DVX BASIC - [%s]%s",
|
|
sProject.name, anyDirty ? " *" : "");
|
|
dvxSetTitle(sAc, sWin, title);
|
|
}
|
|
|
|
// Code window title -- only shows * when code has been edited
|
|
if (sCodeWin) {
|
|
bool codeDirty = false;
|
|
|
|
const char *codeFile = "";
|
|
|
|
if (sEditorFileIdx >= 0 && sEditorFileIdx < sProject.fileCount) {
|
|
codeDirty = sProject.files[sEditorFileIdx].modified;
|
|
codeFile = sProject.files[sEditorFileIdx].path;
|
|
} else if (sProject.activeFileIdx >= 0) {
|
|
codeDirty = sProject.files[sProject.activeFileIdx].modified;
|
|
codeFile = sProject.files[sProject.activeFileIdx].path;
|
|
}
|
|
|
|
char codeTitle[DVX_MAX_PATH + 16];
|
|
snprintf(codeTitle, sizeof(codeTitle), "Code - %s%s", codeFile, codeDirty ? " *" : "");
|
|
dvxSetTitle(sAc, sCodeWin, codeTitle);
|
|
}
|
|
|
|
// Design window title
|
|
if (sFormWin && sDesigner.form) {
|
|
char title[280];
|
|
snprintf(title, sizeof(title), "%s [Design]%s",
|
|
sDesigner.form->caption[0] ? sDesigner.form->caption : sDesigner.form->name,
|
|
sDesigner.form->dirty ? " *" : "");
|
|
dvxSetTitle(sAc, sFormWin, title);
|
|
}
|
|
|
|
// Project tree: rebuild with "*" on dirty files
|
|
if (sProjectWin) {
|
|
prjRebuildTree(&sProject);
|
|
}
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// updateDropdowns
|
|
// ============================================================
|
|
//
|
|
// Scan the source for SUB/FUNCTION declarations and populate
|
|
// the Object and Event dropdowns. Procedure names are split on
|
|
// '_' into ObjectName and EventName (e.g. "Command1_Click").
|
|
|
|
// freeProcBufs -- release all procedure buffers
|
|
|
|
static void freeProcBufs(void) {
|
|
free(sGeneralBuf);
|
|
sGeneralBuf = NULL;
|
|
|
|
for (int32_t i = 0; i < (int32_t)arrlen(sProcBufs); i++) {
|
|
free(sProcBufs[i]);
|
|
}
|
|
|
|
arrfree(sProcBufs);
|
|
sProcBufs = NULL;
|
|
sCurProcIdx = -2;
|
|
sEditorFileIdx = -1;
|
|
}
|
|
|
|
|
|
// parseProcs -- split source into (General) + per-procedure buffers
|
|
|
|
static void parseProcs(const char *source) {
|
|
freeProcBufs();
|
|
|
|
if (!source) {
|
|
sGeneralBuf = strdup("");
|
|
return;
|
|
}
|
|
|
|
const char *pos = source;
|
|
const char *genEnd = source; // end of (General) section
|
|
|
|
while (*pos) {
|
|
const char *lineStart = pos;
|
|
|
|
// Skip leading whitespace
|
|
const char *trimmed = pos;
|
|
|
|
while (*trimmed == ' ' || *trimmed == '\t') {
|
|
trimmed++;
|
|
}
|
|
|
|
bool isSub = (strncasecmp(trimmed, "SUB ", 4) == 0);
|
|
bool isFunc = (strncasecmp(trimmed, "FUNCTION ", 9) == 0);
|
|
|
|
if (isSub || isFunc) {
|
|
// On first proc, mark end of (General) section
|
|
if (arrlen(sProcBufs) == 0) {
|
|
genEnd = lineStart;
|
|
}
|
|
|
|
// Find End Sub / End Function
|
|
const char *endTag = isSub ? "END SUB" : "END FUNCTION";
|
|
int32_t endTagLen = isSub ? 7 : 12;
|
|
const char *scan = pos;
|
|
|
|
// Advance past the Sub/Function line
|
|
while (*scan && *scan != '\n') { scan++; }
|
|
if (*scan == '\n') { scan++; }
|
|
|
|
// Scan for End Sub/Function
|
|
while (*scan) {
|
|
const char *sl = scan;
|
|
|
|
while (*sl == ' ' || *sl == '\t') { sl++; }
|
|
|
|
if (strncasecmp(sl, endTag, endTagLen) == 0) {
|
|
while (*scan && *scan != '\n') { scan++; }
|
|
if (*scan == '\n') { scan++; }
|
|
break;
|
|
}
|
|
|
|
while (*scan && *scan != '\n') { scan++; }
|
|
if (*scan == '\n') { scan++; }
|
|
}
|
|
|
|
// Extract this procedure
|
|
int32_t procLen = (int32_t)(scan - lineStart);
|
|
char *procBuf = (char *)malloc(procLen + 1);
|
|
|
|
if (procBuf) {
|
|
memcpy(procBuf, lineStart, procLen);
|
|
procBuf[procLen] = '\0';
|
|
}
|
|
|
|
arrput(sProcBufs, procBuf);
|
|
pos = scan;
|
|
continue;
|
|
}
|
|
|
|
// Advance to next line
|
|
while (*pos && *pos != '\n') { pos++; }
|
|
if (*pos == '\n') { pos++; }
|
|
}
|
|
|
|
// If no procs found, the entire source is the (General) section
|
|
if (arrlen(sProcBufs) == 0) {
|
|
genEnd = pos; // pos is at the end of the source
|
|
}
|
|
|
|
// Extract (General) section
|
|
int32_t genLen = (int32_t)(genEnd - source);
|
|
|
|
// Trim trailing blank lines
|
|
while (genLen > 0 && (source[genLen - 1] == '\n' || source[genLen - 1] == '\r' ||
|
|
source[genLen - 1] == ' ' || source[genLen - 1] == '\t')) {
|
|
genLen--;
|
|
}
|
|
|
|
sGeneralBuf = (char *)malloc(genLen + 2);
|
|
|
|
if (sGeneralBuf) {
|
|
memcpy(sGeneralBuf, source, genLen);
|
|
sGeneralBuf[genLen] = '\n';
|
|
sGeneralBuf[genLen + 1] = '\0';
|
|
|
|
if (genLen == 0) {
|
|
sGeneralBuf[0] = '\0';
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
// extractNewProcs -- scan a buffer for Sub/Function declarations that
|
|
// don't belong (e.g. user typed a new Sub in the General section).
|
|
// Extracts them into new sProcBufs entries and removes them from the
|
|
// source buffer. Returns a new buffer (caller frees) or NULL if no
|
|
// extraction was needed.
|
|
|
|
static char *extractNewProcs(const char *buf) {
|
|
if (!buf || !buf[0]) {
|
|
return NULL;
|
|
}
|
|
|
|
// Scan for Sub/Function at the start of a line
|
|
bool found = false;
|
|
const char *pos = buf;
|
|
|
|
while (*pos) {
|
|
const char *trimmed = pos;
|
|
|
|
while (*trimmed == ' ' || *trimmed == '\t') {
|
|
trimmed++;
|
|
}
|
|
|
|
bool isSub = (strncasecmp(trimmed, "SUB ", 4) == 0);
|
|
bool isFunc = (strncasecmp(trimmed, "FUNCTION ", 9) == 0);
|
|
|
|
if (isSub || isFunc) {
|
|
found = true;
|
|
break;
|
|
}
|
|
|
|
while (*pos && *pos != '\n') { pos++; }
|
|
if (*pos == '\n') { pos++; }
|
|
}
|
|
|
|
if (!found) {
|
|
return NULL;
|
|
}
|
|
|
|
// Build the remaining text (before the first proc) and extract procs
|
|
int32_t bufLen = (int32_t)strlen(buf);
|
|
char *remaining = (char *)malloc(bufLen + 1);
|
|
|
|
if (!remaining) {
|
|
return NULL;
|
|
}
|
|
|
|
int32_t remPos = 0;
|
|
pos = buf;
|
|
|
|
while (*pos) {
|
|
const char *lineStart = pos;
|
|
const char *trimmed = pos;
|
|
|
|
while (*trimmed == ' ' || *trimmed == '\t') {
|
|
trimmed++;
|
|
}
|
|
|
|
bool isSub = (strncasecmp(trimmed, "SUB ", 4) == 0);
|
|
bool isFunc = (strncasecmp(trimmed, "FUNCTION ", 9) == 0);
|
|
|
|
if (isSub || isFunc) {
|
|
// Extract procedure name for duplicate check
|
|
const char *np = trimmed + (isSub ? 4 : 9);
|
|
|
|
while (*np == ' ' || *np == '\t') { np++; }
|
|
|
|
char newName[128];
|
|
int32_t nn = 0;
|
|
|
|
while (*np && *np != '(' && *np != ' ' && *np != '\t' && *np != '\n' && nn < 127) {
|
|
newName[nn++] = *np++;
|
|
}
|
|
|
|
newName[nn] = '\0';
|
|
|
|
// Check for duplicate against existing proc buffers
|
|
bool isDuplicate = false;
|
|
|
|
for (int32_t p = 0; p < (int32_t)arrlen(sProcBufs); p++) {
|
|
if (!sProcBufs[p]) { continue; }
|
|
|
|
const char *ep = sProcBufs[p];
|
|
|
|
while (*ep == ' ' || *ep == '\t') { ep++; }
|
|
|
|
if (strncasecmp(ep, "SUB ", 4) == 0) { ep += 4; }
|
|
else if (strncasecmp(ep, "FUNCTION ", 9) == 0) { ep += 9; }
|
|
|
|
while (*ep == ' ' || *ep == '\t') { ep++; }
|
|
|
|
char existName[128];
|
|
int32_t en = 0;
|
|
|
|
while (*ep && *ep != '(' && *ep != ' ' && *ep != '\t' && *ep != '\n' && en < 127) {
|
|
existName[en++] = *ep++;
|
|
}
|
|
|
|
existName[en] = '\0';
|
|
|
|
if (strcasecmp(newName, existName) == 0) {
|
|
isDuplicate = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Find End Sub / End Function
|
|
const char *endTag = isSub ? "END SUB" : "END FUNCTION";
|
|
int32_t endTagLen = isSub ? 7 : 12;
|
|
const char *scan = pos;
|
|
|
|
while (*scan && *scan != '\n') { scan++; }
|
|
if (*scan == '\n') { scan++; }
|
|
|
|
while (*scan) {
|
|
const char *sl = scan;
|
|
|
|
while (*sl == ' ' || *sl == '\t') { sl++; }
|
|
|
|
if (strncasecmp(sl, endTag, endTagLen) == 0) {
|
|
while (*scan && *scan != '\n') { scan++; }
|
|
if (*scan == '\n') { scan++; }
|
|
break;
|
|
}
|
|
|
|
while (*scan && *scan != '\n') { scan++; }
|
|
if (*scan == '\n') { scan++; }
|
|
}
|
|
|
|
if (isDuplicate) {
|
|
// Leave it in the General section -- compiler will report the error
|
|
while (*pos && *pos != '\n') {
|
|
remaining[remPos++] = *pos++;
|
|
}
|
|
|
|
if (*pos == '\n') {
|
|
remaining[remPos++] = *pos++;
|
|
}
|
|
} else {
|
|
// Extract this procedure into a new buffer
|
|
int32_t procLen = (int32_t)(scan - lineStart);
|
|
char *procBuf = (char *)malloc(procLen + 1);
|
|
|
|
if (procBuf) {
|
|
memcpy(procBuf, lineStart, procLen);
|
|
procBuf[procLen] = '\0';
|
|
arrput(sProcBufs, procBuf);
|
|
}
|
|
|
|
pos = scan;
|
|
}
|
|
|
|
continue;
|
|
}
|
|
|
|
// Copy non-proc lines to remaining
|
|
while (*pos && *pos != '\n') {
|
|
remaining[remPos++] = *pos++;
|
|
}
|
|
|
|
if (*pos == '\n') {
|
|
remaining[remPos++] = *pos++;
|
|
}
|
|
}
|
|
|
|
remaining[remPos] = '\0';
|
|
return remaining;
|
|
}
|
|
|
|
|
|
// stashCurrentFile -- stash the currently active file's editor/designer
|
|
// state back into its project buffer. This is caching only -- does not
|
|
// mark modified.
|
|
|
|
static void stashCurrentFile(void) {
|
|
if (sProject.activeFileIdx < 0 || sProject.activeFileIdx >= sProject.fileCount) {
|
|
return;
|
|
}
|
|
|
|
PrjFileT *cur = &sProject.files[sProject.activeFileIdx];
|
|
|
|
if (cur->isForm && sDesigner.form) {
|
|
// Save editor code back to form->code before serializing
|
|
stashFormCode();
|
|
|
|
// Serialize form designer state to .frm text
|
|
char *frmBuf = (char *)malloc(IDE_MAX_SOURCE);
|
|
|
|
if (frmBuf) {
|
|
int32_t frmLen = dsgnSaveFrm(&sDesigner, frmBuf, IDE_MAX_SOURCE);
|
|
|
|
free(cur->buffer);
|
|
|
|
if (frmLen > 0) {
|
|
frmBuf[frmLen] = '\0';
|
|
cur->buffer = frmBuf;
|
|
} else {
|
|
free(frmBuf);
|
|
cur->buffer = NULL;
|
|
}
|
|
}
|
|
} else if (!cur->isForm && sEditorFileIdx == sProject.activeFileIdx) {
|
|
// Stash full source (only if editor has this file's code)
|
|
saveCurProc();
|
|
const char *src = getFullSource();
|
|
free(cur->buffer);
|
|
cur->buffer = src ? strdup(src) : NULL;
|
|
}
|
|
}
|
|
|
|
|
|
// stashFormCode -- if the proc buffers belong to the designer's form,
|
|
// save them back to form->code. Uses sEditorFileIdx to know which
|
|
// file the proc buffers actually belong to.
|
|
|
|
static void stashFormCode(void) {
|
|
if (!sDesigner.form || sEditorFileIdx < 0) {
|
|
return;
|
|
}
|
|
|
|
if (sEditorFileIdx >= sProject.fileCount || !sProject.files[sEditorFileIdx].isForm) {
|
|
return;
|
|
}
|
|
|
|
if (strcasecmp(sProject.files[sEditorFileIdx].formName, sDesigner.form->name) != 0) {
|
|
return;
|
|
}
|
|
|
|
saveCurProc();
|
|
free(sDesigner.form->code);
|
|
sDesigner.form->code = strdup(getFullSource());
|
|
}
|
|
|
|
|
|
// saveCurProc -- save editor contents back to the current buffer.
|
|
// Returns true if the proc list was modified (skeleton discarded or
|
|
// new procs extracted), meaning sProcBufs indices may have shifted.
|
|
|
|
static bool saveCurProc(void) {
|
|
if (!sEditor) {
|
|
return false;
|
|
}
|
|
|
|
const char *edText = wgtGetText(sEditor);
|
|
|
|
if (!edText) {
|
|
return false;
|
|
}
|
|
|
|
if (sCurProcIdx == -1) {
|
|
// General section -- check for embedded proc declarations
|
|
char *cleaned = extractNewProcs(edText);
|
|
|
|
free(sGeneralBuf);
|
|
sGeneralBuf = cleaned ? cleaned : strdup(edText);
|
|
|
|
if (cleaned) {
|
|
wgtSetText(sEditor, sGeneralBuf);
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
} else if (sCurProcIdx >= 0 && sCurProcIdx < (int32_t)arrlen(sProcBufs)) {
|
|
// Get the name of the current proc so we can identify its block
|
|
// regardless of position in the editor.
|
|
char ownName[128] = "";
|
|
|
|
if (sCurProcIdx < (int32_t)arrlen(sProcTable)) {
|
|
snprintf(ownName, sizeof(ownName), "%s_%s",
|
|
sProcTable[sCurProcIdx].objName,
|
|
sProcTable[sCurProcIdx].evtName);
|
|
}
|
|
|
|
// If ownName is empty (General proc without underscore), extract
|
|
// it from the first Sub/Function line of the existing buffer.
|
|
if (ownName[0] == '\0' || strcmp(ownName, "(General)_") == 0) {
|
|
const char *ep = sProcBufs[sCurProcIdx];
|
|
while (*ep == ' ' || *ep == '\t') { ep++; }
|
|
if (strncasecmp(ep, "SUB ", 4) == 0) { ep += 4; }
|
|
else if (strncasecmp(ep, "FUNCTION ", 9) == 0) { ep += 9; }
|
|
while (*ep == ' ' || *ep == '\t') { ep++; }
|
|
int32_t n = 0;
|
|
while (*ep && *ep != '(' && *ep != ' ' && *ep != '\t' && *ep != '\n' && n < 127) {
|
|
ownName[n++] = *ep++;
|
|
}
|
|
ownName[n] = '\0';
|
|
}
|
|
|
|
// Count Sub/Function blocks in the editor text
|
|
int32_t blockCount = 0;
|
|
const char *scan = edText;
|
|
|
|
while (*scan) {
|
|
const char *tl = scan;
|
|
while (*tl == ' ' || *tl == '\t') { tl++; }
|
|
if (strncasecmp(tl, "SUB ", 4) == 0 || strncasecmp(tl, "FUNCTION ", 9) == 0) {
|
|
blockCount++;
|
|
}
|
|
while (*scan && *scan != '\n') { scan++; }
|
|
if (*scan == '\n') { scan++; }
|
|
}
|
|
|
|
if (blockCount <= 1) {
|
|
// Single proc (or none) -- check for empty skeleton
|
|
const char *p = edText;
|
|
while (*p == ' ' || *p == '\t') { p++; }
|
|
bool isSub = (strncasecmp(p, "SUB ", 4) == 0);
|
|
const char *endTag = isSub ? "END SUB" : "END FUNCTION";
|
|
int32_t endTagLen = isSub ? 7 : 12;
|
|
|
|
// Skip declaration line
|
|
while (*p && *p != '\n') { p++; }
|
|
if (*p == '\n') { p++; }
|
|
|
|
bool bodyEmpty = true;
|
|
|
|
while (*p) {
|
|
const char *line = p;
|
|
while (*line == ' ' || *line == '\t') { line++; }
|
|
if (strncasecmp(line, endTag, endTagLen) == 0) { break; }
|
|
while (*p && *p != '\n') {
|
|
if (*p != ' ' && *p != '\t' && *p != '\r') { bodyEmpty = false; }
|
|
p++;
|
|
}
|
|
if (*p == '\n') { p++; }
|
|
}
|
|
|
|
if (bodyEmpty) {
|
|
free(sProcBufs[sCurProcIdx]);
|
|
arrdel(sProcBufs, sCurProcIdx);
|
|
sCurProcIdx = -2;
|
|
return true;
|
|
} else {
|
|
free(sProcBufs[sCurProcIdx]);
|
|
sProcBufs[sCurProcIdx] = strdup(edText);
|
|
return false;
|
|
}
|
|
} else {
|
|
// Multiple proc blocks in the editor. Find the one matching
|
|
// ownName, keep it in this buffer, extract the rest.
|
|
const char *pos = edText;
|
|
const char *ownStart = NULL;
|
|
const char *ownEnd = NULL;
|
|
char *extras = (char *)malloc(strlen(edText) + 1);
|
|
int32_t extPos = 0;
|
|
|
|
if (!extras) {
|
|
free(sProcBufs[sCurProcIdx]);
|
|
sProcBufs[sCurProcIdx] = strdup(edText);
|
|
} else {
|
|
while (*pos) {
|
|
const char *lineStart = pos;
|
|
const char *tl = pos;
|
|
while (*tl == ' ' || *tl == '\t') { tl++; }
|
|
|
|
bool isSub = (strncasecmp(tl, "SUB ", 4) == 0);
|
|
bool isFunc = (strncasecmp(tl, "FUNCTION ", 9) == 0);
|
|
|
|
if (isSub || isFunc) {
|
|
// Extract the proc name
|
|
const char *np = tl + (isSub ? 4 : 9);
|
|
while (*np == ' ' || *np == '\t') { np++; }
|
|
char name[128];
|
|
int32_t nn = 0;
|
|
while (*np && *np != '(' && *np != ' ' && *np != '\t' && *np != '\n' && nn < 127) {
|
|
name[nn++] = *np++;
|
|
}
|
|
name[nn] = '\0';
|
|
|
|
// Find the matching End tag
|
|
const char *endTag = isSub ? "END SUB" : "END FUNCTION";
|
|
int32_t endTagLen = isSub ? 7 : 12;
|
|
const char *s = pos;
|
|
while (*s && *s != '\n') { s++; }
|
|
if (*s == '\n') { s++; }
|
|
|
|
while (*s) {
|
|
const char *sl = s;
|
|
while (*sl == ' ' || *sl == '\t') { sl++; }
|
|
if (strncasecmp(sl, endTag, endTagLen) == 0) {
|
|
while (*s && *s != '\n') { s++; }
|
|
if (*s == '\n') { s++; }
|
|
break;
|
|
}
|
|
while (*s && *s != '\n') { s++; }
|
|
if (*s == '\n') { s++; }
|
|
}
|
|
|
|
if (strcasecmp(name, ownName) == 0) {
|
|
ownStart = lineStart;
|
|
ownEnd = s;
|
|
} else {
|
|
// Copy this block to extras
|
|
int32_t blen = (int32_t)(s - lineStart);
|
|
memcpy(extras + extPos, lineStart, blen);
|
|
extPos += blen;
|
|
}
|
|
|
|
pos = s;
|
|
continue;
|
|
}
|
|
|
|
// Non-proc line (blank lines between blocks etc.) -- skip
|
|
while (*pos && *pos != '\n') { pos++; }
|
|
if (*pos == '\n') { pos++; }
|
|
}
|
|
|
|
extras[extPos] = '\0';
|
|
|
|
// Update this buffer with just the owned proc
|
|
if (ownStart && ownEnd) {
|
|
int32_t keepLen = (int32_t)(ownEnd - ownStart);
|
|
char *kept = (char *)malloc(keepLen + 1);
|
|
if (kept) {
|
|
memcpy(kept, ownStart, keepLen);
|
|
kept[keepLen] = '\0';
|
|
free(sProcBufs[sCurProcIdx]);
|
|
sProcBufs[sCurProcIdx] = kept;
|
|
}
|
|
}
|
|
|
|
// Extract extra procs into their own buffers
|
|
if (extPos > 0) {
|
|
extractNewProcs(extras);
|
|
}
|
|
|
|
free(extras);
|
|
|
|
// Update editor to show only this proc
|
|
wgtSetText(sEditor, sProcBufs[sCurProcIdx]);
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
|
|
// showProc -- display a procedure buffer in the editor
|
|
|
|
static void showProc(int32_t procIdx) {
|
|
if (!sEditor) {
|
|
return;
|
|
}
|
|
|
|
// Save whatever is currently in the editor.
|
|
// If a buffer was deleted (empty skeleton discard), adjust the
|
|
// target index since arrdel shifts everything after it.
|
|
if (sCurProcIdx >= -1) {
|
|
int32_t deletedIdx = sCurProcIdx;
|
|
bool changed = saveCurProc();
|
|
|
|
if (changed && deletedIdx >= 0 && procIdx > deletedIdx) {
|
|
procIdx--;
|
|
}
|
|
}
|
|
|
|
// Suppress onChange while loading -- setting text is not a user edit
|
|
void (*savedOnChange)(WidgetT *) = sEditor->onChange;
|
|
sEditor->onChange = NULL;
|
|
|
|
if (procIdx == -1) {
|
|
wgtSetText(sEditor, sGeneralBuf ? sGeneralBuf : "");
|
|
sCurProcIdx = -1;
|
|
} else if (procIdx >= 0 && procIdx < (int32_t)arrlen(sProcBufs)) {
|
|
wgtSetText(sEditor, sProcBufs[procIdx] ? sProcBufs[procIdx] : "");
|
|
sCurProcIdx = procIdx;
|
|
}
|
|
|
|
sEditor->onChange = savedOnChange;
|
|
}
|
|
|
|
|
|
// getFullSource -- reassemble all buffers into one source string
|
|
// Caller must free the returned buffer.
|
|
|
|
static char *sFullSourceCache = NULL;
|
|
|
|
static const char *getFullSource(void) {
|
|
saveCurProc();
|
|
|
|
free(sFullSourceCache);
|
|
|
|
// Calculate total length
|
|
int32_t totalLen = 0;
|
|
|
|
if (sGeneralBuf && sGeneralBuf[0]) {
|
|
totalLen += (int32_t)strlen(sGeneralBuf);
|
|
totalLen += 2; // blank line separator
|
|
}
|
|
|
|
int32_t procCount = (int32_t)arrlen(sProcBufs);
|
|
|
|
for (int32_t i = 0; i < procCount; i++) {
|
|
if (sProcBufs[i]) {
|
|
totalLen += (int32_t)strlen(sProcBufs[i]);
|
|
totalLen += 2; // newline + blank line between procedures
|
|
}
|
|
}
|
|
|
|
sFullSourceCache = (char *)malloc(totalLen + 1);
|
|
|
|
if (!sFullSourceCache) {
|
|
return "";
|
|
}
|
|
|
|
int32_t pos = 0;
|
|
|
|
if (sGeneralBuf && sGeneralBuf[0]) {
|
|
int32_t len = (int32_t)strlen(sGeneralBuf);
|
|
memcpy(sFullSourceCache + pos, sGeneralBuf, len);
|
|
pos += len;
|
|
|
|
if (pos > 0 && sFullSourceCache[pos - 1] != '\n') {
|
|
sFullSourceCache[pos++] = '\n';
|
|
}
|
|
|
|
sFullSourceCache[pos++] = '\n';
|
|
}
|
|
|
|
for (int32_t i = 0; i < procCount; i++) {
|
|
if (sProcBufs[i]) {
|
|
int32_t len = (int32_t)strlen(sProcBufs[i]);
|
|
memcpy(sFullSourceCache + pos, sProcBufs[i], len);
|
|
pos += len;
|
|
|
|
if (pos > 0 && sFullSourceCache[pos - 1] != '\n') {
|
|
sFullSourceCache[pos++] = '\n';
|
|
}
|
|
|
|
// Blank line between procedures
|
|
if (i < procCount - 1) {
|
|
sFullSourceCache[pos++] = '\n';
|
|
}
|
|
}
|
|
}
|
|
|
|
sFullSourceCache[pos] = '\0';
|
|
return sFullSourceCache;
|
|
}
|
|
|
|
|
|
static void updateDropdowns(void) {
|
|
// Reset dynamic arrays
|
|
arrsetlen(sProcTable, 0);
|
|
arrsetlen(sObjItems, 0);
|
|
arrsetlen(sEvtItems, 0);
|
|
|
|
if (!sObjDropdown || !sEvtDropdown) {
|
|
return;
|
|
}
|
|
|
|
// Scan the reassembled full source
|
|
const char *src = getFullSource();
|
|
|
|
if (!src) {
|
|
return;
|
|
}
|
|
|
|
// Scan line by line for SUB / FUNCTION
|
|
const char *pos = src;
|
|
int32_t lineNum = 1;
|
|
|
|
while (*pos) {
|
|
const char *lineStart = pos;
|
|
|
|
// Skip leading whitespace
|
|
while (*pos == ' ' || *pos == '\t') {
|
|
pos++;
|
|
}
|
|
|
|
// Check for SUB or FUNCTION keyword
|
|
bool isSub = (strncasecmp(pos, "SUB ", 4) == 0);
|
|
bool isFunc = (strncasecmp(pos, "FUNCTION ", 9) == 0);
|
|
|
|
if (isSub || isFunc) {
|
|
pos += isSub ? 4 : 9;
|
|
|
|
while (*pos == ' ' || *pos == '\t') {
|
|
pos++;
|
|
}
|
|
|
|
char procName[64];
|
|
int32_t nameLen = 0;
|
|
|
|
while (*pos && *pos != '(' && *pos != ' ' && *pos != '\t' && *pos != '\n' && *pos != '\r' && nameLen < 63) {
|
|
procName[nameLen++] = *pos++;
|
|
}
|
|
|
|
procName[nameLen] = '\0';
|
|
|
|
// Find End Sub / End Function
|
|
const char *endTag = isSub ? "END SUB" : "END FUNCTION";
|
|
int32_t endTagLen = isSub ? 7 : 12;
|
|
const char *scan = pos;
|
|
|
|
while (*scan) {
|
|
const char *sl = scan;
|
|
|
|
while (*sl == ' ' || *sl == '\t') {
|
|
sl++;
|
|
}
|
|
|
|
if (strncasecmp(sl, endTag, endTagLen) == 0) {
|
|
// Advance past the End line
|
|
while (*scan && *scan != '\n') {
|
|
scan++;
|
|
}
|
|
|
|
if (*scan == '\n') {
|
|
scan++;
|
|
}
|
|
|
|
break;
|
|
}
|
|
|
|
while (*scan && *scan != '\n') {
|
|
scan++;
|
|
}
|
|
|
|
if (*scan == '\n') {
|
|
scan++;
|
|
}
|
|
}
|
|
|
|
IdeProcEntryT entry;
|
|
memset(&entry, 0, sizeof(entry));
|
|
entry.lineNum = lineNum;
|
|
|
|
// Match proc name against known objects: form name,
|
|
// controls, menu items. Try each as a prefix followed
|
|
// by "_". This handles names with multiple underscores
|
|
// correctly (e.g., "cmdOK_Click" matches "cmdOK", not
|
|
// "This_Is_A_Dumb_Name" matching a control named "This").
|
|
bool isEvent = false;
|
|
|
|
if (sDesigner.form) {
|
|
// Collect all known object names
|
|
const char *objNames[512];
|
|
int32_t objNameCount = 0;
|
|
|
|
objNames[objNameCount++] = sDesigner.form->name;
|
|
|
|
for (int32_t ci = 0; ci < (int32_t)arrlen(sDesigner.form->controls) && objNameCount < 511; ci++) {
|
|
objNames[objNameCount++] = sDesigner.form->controls[ci].name;
|
|
}
|
|
|
|
for (int32_t mi = 0; mi < (int32_t)arrlen(sDesigner.form->menuItems) && objNameCount < 511; mi++) {
|
|
objNames[objNameCount++] = sDesigner.form->menuItems[mi].name;
|
|
}
|
|
|
|
// Try each object name as prefix + "_"
|
|
for (int32_t oi = 0; oi < objNameCount; oi++) {
|
|
int32_t nameLen = (int32_t)strlen(objNames[oi]);
|
|
|
|
if (nameLen > 0 && strncasecmp(procName, objNames[oi], nameLen) == 0 && procName[nameLen] == '_') {
|
|
snprintf(entry.objName, sizeof(entry.objName), "%s", objNames[oi]);
|
|
snprintf(entry.evtName, sizeof(entry.evtName), "%s", procName + nameLen + 1);
|
|
isEvent = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!isEvent) {
|
|
snprintf(entry.objName, sizeof(entry.objName), "%s", "(General)");
|
|
snprintf(entry.evtName, sizeof(entry.evtName), "%s", procName);
|
|
}
|
|
|
|
arrput(sProcTable, entry);
|
|
|
|
// Skip to end of this proc (already scanned)
|
|
pos = scan;
|
|
|
|
// Count lines we skipped
|
|
for (const char *c = lineStart; c < scan; c++) {
|
|
if (*c == '\n') {
|
|
lineNum++;
|
|
}
|
|
}
|
|
|
|
continue;
|
|
}
|
|
|
|
// Advance to end of line
|
|
while (*pos && *pos != '\n') {
|
|
pos++;
|
|
}
|
|
|
|
if (*pos == '\n') {
|
|
pos++;
|
|
}
|
|
|
|
lineNum++;
|
|
}
|
|
|
|
// Build object names for the Object dropdown.
|
|
// Always include "(General)" and the form name (if editing a form).
|
|
// Then add control names from the designer, plus any from existing procs.
|
|
arrput(sObjItems, "(General)");
|
|
|
|
if (sDesigner.form) {
|
|
arrput(sObjItems, sDesigner.form->name);
|
|
|
|
for (int32_t i = 0; i < (int32_t)arrlen(sDesigner.form->controls); i++) {
|
|
arrput(sObjItems, sDesigner.form->controls[i].name);
|
|
}
|
|
|
|
// Add menu item names (non-separator, non-top-level-header-only)
|
|
for (int32_t i = 0; i < (int32_t)arrlen(sDesigner.form->menuItems); i++) {
|
|
DsgnMenuItemT *mi = &sDesigner.form->menuItems[i];
|
|
|
|
if (mi->name[0] && mi->caption[0] != '-') {
|
|
arrput(sObjItems, mi->name);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Sort object items alphabetically, keeping (General) first
|
|
int32_t objCount = (int32_t)arrlen(sObjItems);
|
|
|
|
if (objCount > 1) {
|
|
qsort(sObjItems + 1, objCount - 1, sizeof(const char *), cmpStrPtrs);
|
|
}
|
|
|
|
wgtDropdownSetItems(sObjDropdown, sObjItems, objCount);
|
|
|
|
if (objCount > 0) {
|
|
wgtDropdownSetSelected(sObjDropdown, 0);
|
|
onObjDropdownChange(sObjDropdown);
|
|
} else {
|
|
wgtDropdownSetItems(sEvtDropdown, NULL, 0);
|
|
}
|
|
}
|