Major BASIC runtime work.
This commit is contained in:
parent
a8c38267bc
commit
1affec7e8c
62 changed files with 5458 additions and 1333 deletions
8
Makefile
8
Makefile
|
|
@ -107,7 +107,7 @@ SDKDIR = bin/sdk
|
|||
|
||||
deploy-sdk:
|
||||
@echo "Building SDK..."
|
||||
@mkdir -p $(SDKDIR)/include/core $(SDKDIR)/include/shell $(SDKDIR)/include/tasks $(SDKDIR)/include/sql $(SDKDIR)/include/basic
|
||||
@mkdir -p $(SDKDIR)/include/core $(SDKDIR)/include/shell $(SDKDIR)/include/tasks $(SDKDIR)/include/sql $(SDKDIR)/include/basic $(SDKDIR)/samples/basic/basdemo
|
||||
@# Core headers (libdvx public API)
|
||||
@for f in src/libs/kpunch/libdvx/dvxApp.h src/libs/kpunch/libdvx/dvxTypes.h \
|
||||
src/libs/kpunch/libdvx/dvxWgt.h src/libs/kpunch/libdvx/dvxWgtP.h \
|
||||
|
|
@ -137,6 +137,10 @@ deploy-sdk:
|
|||
done
|
||||
@# BASIC include files
|
||||
@cp src/include/basic/*.bas $(SDKDIR)/include/basic/
|
||||
@# BASIC sample: basdemo (project file + form + icon)
|
||||
@cp src/apps/kpunch/basdemo/basdemo.dbp $(SDKDIR)/samples/basic/basdemo/
|
||||
@cp src/apps/kpunch/basdemo/basdemo.frm $(SDKDIR)/samples/basic/basdemo/
|
||||
@cp src/apps/kpunch/basdemo/ICON32.BMP $(SDKDIR)/samples/basic/basdemo/
|
||||
@# README
|
||||
@printf '%s\n' \
|
||||
'DVX SDK' \
|
||||
|
|
@ -154,6 +158,8 @@ deploy-sdk:
|
|||
' sql/ SQLite database wrapper API' \
|
||||
' widget/ Per-widget public API headers' \
|
||||
' basic/ BASIC include files (DECLARE LIBRARY modules)' \
|
||||
' samples/' \
|
||||
' basic/ Example BASIC projects (open in the DVX BASIC IDE)' \
|
||||
'' \
|
||||
'Requirements' \
|
||||
'------------' \
|
||||
|
|
|
|||
|
|
@ -3188,6 +3188,7 @@ End Sub</code></pre>
|
|||
<h2>Type-Specific Methods</h2>
|
||||
<pre> Method Description
|
||||
------ -----------
|
||||
AppendText text$ Append text to the end of the buffer and invalidate the widget.
|
||||
FindNext needle$, caseSensitive, forward Search for text. Returns True if found.
|
||||
GetWordAtCursor() Returns the word under the cursor.
|
||||
GoToLine line% Scroll to and position cursor at the given line.
|
||||
|
|
|
|||
|
|
@ -6977,6 +6977,7 @@ WidgetT *page2 = wgtTabPage(tabs, "Advanced");
|
|||
<h3>API Functions (TextArea-specific)</h3>
|
||||
<pre> Function Description
|
||||
-------- -----------
|
||||
void wgtTextAreaAppendText(w, text) Append text to the end of the buffer and invalidate the widget.
|
||||
void wgtTextAreaSetColorize(w, fn, ctx) Set a syntax colorization callback. The callback receives each line and fills a color index array.
|
||||
void wgtTextAreaGoToLine(w, line) Scroll to and place the cursor on the given line number.
|
||||
void wgtTextAreaSetAutoIndent(w, enable) Enable or disable automatic indentation on newline.
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ C_APPS = progman clock dvxdemo cpanel dvxhelp
|
|||
BASCOMP = ../../../bin/host/bascomp
|
||||
# BASIC apps: each is a .dbp project in its own directory.
|
||||
# BASIC-only notepad, imgview, etc. replace the old C versions.
|
||||
BASIC_APPS = iconed notepad imgview helpedit resedit basicdemo
|
||||
BASIC_APPS = iconed notepad imgview helpedit resedit basdemo
|
||||
|
||||
.PHONY: all clean $(C_APPS) dvxbasic $(BASIC_APPS)
|
||||
|
||||
|
|
@ -115,10 +115,10 @@ resedit: $(BINDIR)/kpunch/resedit/resedit.app
|
|||
$(BINDIR)/kpunch/resedit/resedit.app: resedit/resedit.dbp resedit/resedit.frm resedit/ICON32.BMP $(BASCOMP) | $(BINDIR)/kpunch/resedit dvxbasic
|
||||
$(BASCOMP) resedit/resedit.dbp -o $@ -release
|
||||
|
||||
basicdemo: $(BINDIR)/kpunch/basicdemo/basicdemo.app
|
||||
basdemo: $(BINDIR)/kpunch/basdemo/basdemo.app
|
||||
|
||||
$(BINDIR)/kpunch/basicdemo/basicdemo.app: basicdemo/basicdemo.dbp basicdemo/basicdemo.frm basicdemo/ICON32.BMP $(BASCOMP) | $(BINDIR)/kpunch/basicdemo dvxbasic
|
||||
$(BASCOMP) basicdemo/basicdemo.dbp -o $@ -release
|
||||
$(BINDIR)/kpunch/basdemo/basdemo.app: basdemo/basdemo.dbp basdemo/basdemo.frm basdemo/ICON32.BMP $(BASCOMP) | $(BINDIR)/kpunch/basdemo dvxbasic
|
||||
$(BASCOMP) basdemo/basdemo.dbp -o $@ -release
|
||||
|
||||
$(OBJDIR):
|
||||
mkdir -p $(OBJDIR)
|
||||
|
|
@ -132,7 +132,7 @@ $(BINDIR)/kpunch/iconed: ; mkdir -p $@
|
|||
$(BINDIR)/kpunch/notepad: ; mkdir -p $@
|
||||
$(BINDIR)/kpunch/imgview: ; mkdir -p $@
|
||||
$(BINDIR)/kpunch/resedit: ; mkdir -p $@
|
||||
$(BINDIR)/kpunch/basicdemo: ; mkdir -p $@
|
||||
$(BINDIR)/kpunch/basdemo: ; mkdir -p $@
|
||||
|
||||
# Header dependencies
|
||||
COMMON_H = ../../libs/kpunch/libdvx/dvxApp.h ../../libs/kpunch/libdvx/dvxDlg.h ../../libs/kpunch/libdvx/dvxWgt.h ../../libs/kpunch/libdvx/dvxWm.h ../../libs/kpunch/libdvx/dvxVideo.h ../../libs/kpunch/dvxshell/shellApp.h
|
||||
|
|
@ -153,5 +153,5 @@ clean:
|
|||
rm -f $(BINDIR)/kpunch/imgview/imgview.app
|
||||
rm -f $(BINDIR)/kpunch/dvxhelp/helpedit.app
|
||||
rm -f $(BINDIR)/kpunch/resedit/resedit.app
|
||||
rm -f $(BINDIR)/kpunch/basicdemo/basicdemo.app
|
||||
rm -f $(BINDIR)/kpunch/basdemo/basdemo.app
|
||||
$(MAKE) -C dvxbasic clean
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ the BASIC-based sample apps. Two flavours of app live side by side:
|
|||
* **BASIC apps** -- `.dbp` projects compiled to `.app` files by the
|
||||
host-side `bascomp`. The compiler links the BASIC bytecode into a
|
||||
copy of `basstub.app`, which acts as the runtime host. See
|
||||
`iconed/`, `notepad/`, `imgview/`, `basicdemo/`, `resedit/`,
|
||||
`iconed/`, `notepad/`, `imgview/`, `basdemo/`, `resedit/`,
|
||||
`dvxhelp/helpedit/`.
|
||||
|
||||
The rest of this document covers writing a C app against the SDK.
|
||||
|
|
@ -303,11 +303,11 @@ the compiled system help viewer by default for live preview.
|
|||
GUI wrapper around `dvxres`: open a `.app` / `.wgt` / `.lib`,
|
||||
browse its resources, add, replace, extract, or remove them.
|
||||
|
||||
### BASIC Demo (basicdemo, BASIC)
|
||||
### BASIC Demo (basdemo, BASIC)
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| File | `apps/kpunch/basicdemo/basicdemo.app` |
|
||||
| File | `apps/kpunch/basdemo/basdemo.app` |
|
||||
| Type | BASIC (main-loop via basstub) |
|
||||
| Multi-instance | No |
|
||||
|
||||
|
|
@ -453,9 +453,9 @@ apps/kpunch/
|
|||
imgview.dbp
|
||||
imgview.frm
|
||||
ICON32.BMP
|
||||
basicdemo/
|
||||
basicdemo.dbp
|
||||
basicdemo.frm
|
||||
basdemo/
|
||||
basdemo.dbp
|
||||
basdemo.frm
|
||||
ICON32.BMP
|
||||
resedit/
|
||||
resedit.dbp
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ File0 = ../../../include/basic/commdlg.bas
|
|||
Count = 1
|
||||
|
||||
[Forms]
|
||||
File0 = basicdemo.frm
|
||||
File0 = basdemo.frm
|
||||
Count = 1
|
||||
|
||||
[Settings]
|
||||
|
|
@ -170,6 +170,21 @@ TYPE PointT
|
|||
y AS INTEGER
|
||||
END TYPE
|
||||
|
||||
' Module-level state. Grouped here up front (rather than sprinkled
|
||||
' between SUB definitions) so the compiler sees a simple top-level
|
||||
' run-once block followed by a flat list of SUBs / FUNCTIONs.
|
||||
DIM gfxWin AS LONG
|
||||
DIM dynForm AS LONG
|
||||
DIM dynCount AS INTEGER
|
||||
DIM timerWin AS LONG
|
||||
DIM tickCount AS LONG
|
||||
|
||||
gfxWin = 0
|
||||
dynForm = 0
|
||||
dynCount = 0
|
||||
timerWin = 0
|
||||
tickCount = 0
|
||||
|
||||
|
||||
' ============================================================
|
||||
' OutArea helpers
|
||||
|
|
@ -247,7 +262,7 @@ SUB mnuAbout_Click
|
|||
msg = "DVX BASIC Feature Tour" + CHR$(10) + CHR$(10)
|
||||
msg = msg + "A visual catalog of DVX BASIC language"
|
||||
msg = msg + " and runtime features." + CHR$(10) + CHR$(10)
|
||||
msg = msg + "(c) 2026 DVX Project"
|
||||
msg = msg + "Copyright 2026 Scott Duensing"
|
||||
MsgBox msg, vbOKOnly, "About"
|
||||
END SUB
|
||||
|
||||
|
|
@ -740,17 +755,13 @@ END SUB
|
|||
' Graphics demo (opens a second form with Canvas)
|
||||
' ============================================================
|
||||
|
||||
DIM gfxWin AS LONG
|
||||
gfxWin = 0
|
||||
|
||||
|
||||
SUB mnuGraphics_Click
|
||||
IF gfxWin <> 0 THEN
|
||||
EXIT SUB
|
||||
END IF
|
||||
|
||||
DIM frm AS LONG
|
||||
SET frm = CreateForm("GraphicsForm", 360, 320)
|
||||
SET frm = CreateForm("GraphicsForm", 380, 360)
|
||||
GraphicsForm.Caption = "Graphics Demo"
|
||||
gfxWin = frm
|
||||
|
||||
|
|
@ -844,6 +855,13 @@ SUB GfxClearCanvas
|
|||
END SUB
|
||||
|
||||
|
||||
SUB BasicDemo_Unload
|
||||
' Closing the main form shuts down the whole app, including any
|
||||
' child forms (Graphics, Dynamic, Timer) the user left open.
|
||||
END
|
||||
END SUB
|
||||
|
||||
|
||||
SUB GraphicsForm_Unload
|
||||
gfxWin = 0
|
||||
END SUB
|
||||
|
|
@ -853,10 +871,6 @@ END SUB
|
|||
' Dynamic form demo
|
||||
' ============================================================
|
||||
|
||||
DIM dynForm AS LONG
|
||||
dynForm = 0
|
||||
|
||||
|
||||
SUB mnuDynamic_Click
|
||||
IF dynForm <> 0 THEN
|
||||
EXIT SUB
|
||||
|
|
@ -892,10 +906,6 @@ SUB mnuDynamic_Click
|
|||
END SUB
|
||||
|
||||
|
||||
DIM dynCount AS INTEGER
|
||||
dynCount = 0
|
||||
|
||||
|
||||
SUB DynInc
|
||||
dynCount = dynCount + 1
|
||||
CountLabel.Caption = "Counter: " + STR$(dynCount)
|
||||
|
|
@ -917,10 +927,6 @@ END SUB
|
|||
' Timer demo
|
||||
' ============================================================
|
||||
|
||||
DIM timerWin AS LONG
|
||||
timerWin = 0
|
||||
|
||||
|
||||
SUB mnuTimer_Click
|
||||
IF timerWin <> 0 THEN
|
||||
EXIT SUB
|
||||
|
|
@ -944,10 +950,6 @@ SUB mnuTimer_Click
|
|||
END SUB
|
||||
|
||||
|
||||
DIM tickCount AS LONG
|
||||
tickCount = 0
|
||||
|
||||
|
||||
SUB TickHandler
|
||||
tickCount = tickCount + 1
|
||||
TickLabel.Caption = "Ticks: " + STR$(tickCount) + " Time: " + TIME$
|
||||
|
|
@ -49,7 +49,6 @@
|
|||
#include "shellApp.h"
|
||||
#include "stb_ds_wrap.h"
|
||||
|
||||
#include <dirent.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
|
@ -977,35 +976,33 @@ static void scanThemes(void) {
|
|||
arrsetlen(sThemeEntries, 0);
|
||||
arrsetlen(sThemeLabels, 0);
|
||||
|
||||
DIR *dir = opendir(THEME_DIR);
|
||||
char **names = dvxReadDir(THEME_DIR);
|
||||
|
||||
if (!dir) {
|
||||
if (!names) {
|
||||
return;
|
||||
}
|
||||
|
||||
struct dirent *ent;
|
||||
int32_t n = (int32_t)arrlen(names);
|
||||
|
||||
while ((ent = readdir(dir)) != NULL) {
|
||||
char *dot = strrchr(ent->d_name, '.');
|
||||
|
||||
if (!dot || strcasecmp(dot, THEME_EXT) != 0) {
|
||||
for (int32_t i = 0; i < n; i++) {
|
||||
if (!dvxHasExt(names[i], THEME_EXT)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
FileEntryT entry = {0};
|
||||
int32_t nameLen = (int32_t)(dot - ent->d_name);
|
||||
FileEntryT entry = {0};
|
||||
int32_t nameLen = (int32_t)strlen(names[i]) - (int32_t)strlen(THEME_EXT);
|
||||
|
||||
if (nameLen >= (int32_t)sizeof(entry.name)) {
|
||||
nameLen = (int32_t)sizeof(entry.name) - 1;
|
||||
}
|
||||
|
||||
memcpy(entry.name, ent->d_name, nameLen);
|
||||
memcpy(entry.name, names[i], nameLen);
|
||||
entry.name[nameLen] = '\0';
|
||||
snprintf(entry.path, sizeof(entry.path), "%s" DVX_PATH_SEP "%s", THEME_DIR, entry.name);
|
||||
arrput(sThemeEntries, entry);
|
||||
}
|
||||
|
||||
closedir(dir);
|
||||
dvxReadDirFree(names);
|
||||
|
||||
// Build label array now that sThemeEntries is stable
|
||||
for (int32_t i = 0; i < arrlen(sThemeEntries); i++) {
|
||||
|
|
@ -1018,24 +1015,18 @@ static void scanWallpapers(void) {
|
|||
arrsetlen(sWpaperEntries, 0);
|
||||
arrsetlen(sWpaperLabels, 0);
|
||||
|
||||
DIR *dir = opendir(WPAPER_DIR);
|
||||
char **names = dvxReadDir(WPAPER_DIR);
|
||||
|
||||
if (!dir) {
|
||||
if (!names) {
|
||||
return;
|
||||
}
|
||||
|
||||
struct dirent *ent;
|
||||
int32_t n = (int32_t)arrlen(names);
|
||||
|
||||
while ((ent = readdir(dir)) != NULL) {
|
||||
char *dot = strrchr(ent->d_name, '.');
|
||||
|
||||
if (!dot) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (strcasecmp(dot, ".BMP") != 0 &&
|
||||
strcasecmp(dot, ".JPG") != 0 &&
|
||||
strcasecmp(dot, ".PNG") != 0) {
|
||||
for (int32_t i = 0; i < n; i++) {
|
||||
if (!dvxHasExt(names[i], ".BMP") &&
|
||||
!dvxHasExt(names[i], ".JPG") &&
|
||||
!dvxHasExt(names[i], ".PNG")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
|
@ -1044,19 +1035,19 @@ static void scanWallpapers(void) {
|
|||
// Length-clamped memcpy instead of strncpy/snprintf because GCC
|
||||
// warns about both when d_name (255) exceeds the buffer (64),
|
||||
// even though truncation is intentional and safe.
|
||||
int32_t nl = (int32_t)strlen(ent->d_name);
|
||||
int32_t nl = (int32_t)strlen(names[i]);
|
||||
|
||||
if (nl >= (int32_t)sizeof(entry.name)) {
|
||||
nl = (int32_t)sizeof(entry.name) - 1;
|
||||
}
|
||||
|
||||
memcpy(entry.name, ent->d_name, nl);
|
||||
memcpy(entry.name, names[i], nl);
|
||||
entry.name[nl] = '\0';
|
||||
snprintf(entry.path, sizeof(entry.path), "%s" DVX_PATH_SEP "%s", WPAPER_DIR, entry.name);
|
||||
arrput(sWpaperEntries, entry);
|
||||
}
|
||||
|
||||
closedir(dir);
|
||||
dvxReadDirFree(names);
|
||||
|
||||
// Build label array now that sWpaperEntries is stable
|
||||
for (int32_t i = 0; i < arrlen(sWpaperEntries); i++) {
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ HOSTDIR = ../../../../bin/host
|
|||
DVXRES = $(HOSTDIR)/dvxres
|
||||
|
||||
# Runtime library objects (VM + values + form runtime + serialization)
|
||||
RT_OBJS = $(OBJDIR)/vm.o $(OBJDIR)/values.o $(OBJDIR)/formrt.o $(OBJDIR)/serialize.o
|
||||
RT_OBJS = $(OBJDIR)/vm.o $(OBJDIR)/values.o $(OBJDIR)/formrt.o $(OBJDIR)/frmParser.o $(OBJDIR)/serialize.o
|
||||
RT_TARGETDIR = $(LIBSDIR)/kpunch/basrt
|
||||
RT_TARGET = $(RT_TARGETDIR)/basrt.lib
|
||||
|
||||
|
|
@ -68,6 +68,7 @@ TEST_VM = $(HOSTDIR)/test_vm
|
|||
TEST_LEX = $(HOSTDIR)/test_lex
|
||||
TEST_QUICK = $(HOSTDIR)/test_quick
|
||||
TEST_COMPACT = $(HOSTDIR)/test_compact
|
||||
TEST_SUITE = $(HOSTDIR)/test_suite
|
||||
|
||||
STB_DS_IMPL = ../../../libs/kpunch/libdvx/thirdparty/stb_ds_impl.c
|
||||
PLATFORM_UTIL = ../../../libs/kpunch/libdvx/platform/dvxPlatformUtil.c
|
||||
|
|
@ -76,6 +77,7 @@ TEST_VM_SRCS = test_vm.c runtime/vm.c runtime/values.c runtime/serialize.c
|
|||
TEST_LEX_SRCS = test_lex.c compiler/lexer.c
|
||||
TEST_QUICK_SRCS = test_quick.c compiler/lexer.c compiler/parser.c compiler/codegen.c compiler/symtab.c runtime/vm.c runtime/values.c runtime/serialize.c $(PLATFORM_UTIL) $(STB_DS_IMPL)
|
||||
TEST_COMPACT_SRCS = test_compact.c compiler/lexer.c compiler/parser.c compiler/codegen.c compiler/symtab.c compiler/strip.c compiler/compact.c runtime/vm.c runtime/values.c runtime/serialize.c $(PLATFORM_UTIL) $(STB_DS_IMPL)
|
||||
TEST_SUITE_SRCS = test_suite.c compiler/lexer.c compiler/parser.c compiler/codegen.c compiler/symtab.c runtime/vm.c runtime/values.c runtime/serialize.c $(PLATFORM_UTIL) $(STB_DS_IMPL)
|
||||
|
||||
# Command-line compiler (host tool)
|
||||
BASCOMP_SRCS = stub/bascomp.c basBuild.c compiler/lexer.c compiler/parser.c compiler/codegen.c compiler/symtab.c compiler/strip.c compiler/obfuscate.c compiler/compact.c runtime/vm.c runtime/values.c runtime/serialize.c ../../../libs/kpunch/libdvx/dvxPrefs.c ../../../libs/kpunch/libdvx/dvxResource.c $(PLATFORM_UTIL) $(STB_DS_IMPL)
|
||||
|
|
@ -92,11 +94,15 @@ SYSTEMDIR = ../../../../bin/system
|
|||
|
||||
all: $(RT_TARGET) $(RT_TARGETDIR)/basrt.dep $(STUB_TARGET) $(APP_TARGET) $(BASCOMP_TARGET) $(SYSTEMDIR)/BASCOMP.EXE
|
||||
|
||||
tests: $(TEST_COMPILER) $(TEST_VM) $(TEST_LEX) $(TEST_QUICK) $(TEST_COMPACT)
|
||||
tests: $(TEST_COMPILER) $(TEST_VM) $(TEST_LEX) $(TEST_QUICK) $(TEST_COMPACT) $(TEST_SUITE)
|
||||
$(TEST_SUITE)
|
||||
|
||||
$(TEST_COMPILER): $(TEST_COMPILER_SRCS) | $(HOSTDIR)
|
||||
$(HOSTCC) $(HOSTCFLAGS) -o $@ $(TEST_COMPILER_SRCS) -lm
|
||||
|
||||
$(TEST_SUITE): $(TEST_SUITE_SRCS) | $(HOSTDIR)
|
||||
$(HOSTCC) $(HOSTCFLAGS) -o $@ $(TEST_SUITE_SRCS) -lm
|
||||
|
||||
$(TEST_VM): $(TEST_VM_SRCS) | $(HOSTDIR)
|
||||
$(HOSTCC) $(HOSTCFLAGS) -o $@ $(TEST_VM_SRCS) -lm
|
||||
|
||||
|
|
@ -152,7 +158,10 @@ $(APP_TARGET): $(COMP_OBJS) $(APP_OBJS) $(STUB_TARGET) dvxbasic.res | $(APPDIR)
|
|||
$(OBJDIR)/codegen.o: compiler/codegen.c compiler/codegen.h compiler/symtab.h compiler/opcodes.h runtime/values.h | $(OBJDIR)
|
||||
$(CC) $(CFLAGS) -c -o $@ $<
|
||||
|
||||
$(OBJDIR)/formrt.o: formrt/formrt.c formrt/formrt.h compiler/opcodes.h runtime/vm.h | $(OBJDIR)
|
||||
$(OBJDIR)/formrt.o: formrt/formrt.c formrt/formrt.h formrt/frmParser.h compiler/opcodes.h runtime/vm.h | $(OBJDIR)
|
||||
$(CC) $(CFLAGS) -c -o $@ $<
|
||||
|
||||
$(OBJDIR)/frmParser.o: formrt/frmParser.c formrt/frmParser.h | $(OBJDIR)
|
||||
$(CC) $(CFLAGS) -c -o $@ $<
|
||||
|
||||
$(OBJDIR)/serialize.o: runtime/serialize.c runtime/serialize.h runtime/vm.h | $(OBJDIR)
|
||||
|
|
@ -173,7 +182,7 @@ $(OBJDIR)/basBuild.o: basBuild.c basBuild.h basRes.h | $(OBJDIR)
|
|||
$(OBJDIR)/basstub.o: stub/basstub.c runtime/vm.h runtime/serialize.h formrt/formrt.h | $(OBJDIR)
|
||||
$(CC) $(CFLAGS) -c -o $@ $<
|
||||
|
||||
$(OBJDIR)/ideDesigner.o: ide/ideDesigner.c ide/ideDesigner.h | $(OBJDIR)
|
||||
$(OBJDIR)/ideDesigner.o: ide/ideDesigner.c ide/ideDesigner.h formrt/frmParser.h | $(OBJDIR)
|
||||
$(CC) $(CFLAGS) -c -o $@ $<
|
||||
|
||||
$(OBJDIR)/ideMenuEditor.o: ide/ideMenuEditor.c ide/ideMenuEditor.h ide/ideDesigner.h | $(OBJDIR)
|
||||
|
|
@ -185,7 +194,7 @@ $(OBJDIR)/ideMain.o: ide/ideMain.c ide/ideDesigner.h ide/ideMenuEditor.h ide/ide
|
|||
$(OBJDIR)/ideProject.o: ide/ideProject.c ide/ideProject.h | $(OBJDIR)
|
||||
$(CC) $(CFLAGS) -c -o $@ $<
|
||||
|
||||
$(OBJDIR)/ideProperties.o: ide/ideProperties.c ide/ideProperties.h ide/ideDesigner.h | $(OBJDIR)
|
||||
$(OBJDIR)/ideProperties.o: ide/ideProperties.c ide/ideProperties.h ide/ideDesigner.h formrt/frmParser.h | $(OBJDIR)
|
||||
$(CC) $(CFLAGS) -c -o $@ $<
|
||||
|
||||
$(OBJDIR)/ideToolbox.o: ide/ideToolbox.c ide/ideToolbox.h ide/ideDesigner.h | $(OBJDIR)
|
||||
|
|
|
|||
|
|
@ -46,10 +46,11 @@
|
|||
#define BAS_INI_KEY_HELPFILE "HelpFile"
|
||||
|
||||
// [Settings] / [Modules] / [Forms] sections of a .dbp file
|
||||
#define BAS_INI_SECTION_SETTINGS "Settings"
|
||||
#define BAS_INI_KEY_STARTUPFORM "StartupForm"
|
||||
#define BAS_INI_SECTION_MODULES "Modules"
|
||||
#define BAS_INI_SECTION_FORMS "Forms"
|
||||
#define BAS_INI_SECTION_SETTINGS "Settings"
|
||||
#define BAS_INI_KEY_STARTUPFORM "StartupForm"
|
||||
#define BAS_INI_KEY_OPTIONEXPLICIT "OptionExplicit"
|
||||
#define BAS_INI_SECTION_MODULES "Modules"
|
||||
#define BAS_INI_SECTION_FORMS "Forms"
|
||||
|
||||
// ------------------------------------------------------------
|
||||
// Resource names inside a compiled .app DXE
|
||||
|
|
|
|||
|
|
@ -208,12 +208,40 @@ BasModuleT *basCodeGenBuildModuleWithProcs(BasCodeGenT *cg, void *symtab) {
|
|||
// Count SUB/FUNCTION entries
|
||||
int32_t procCount = 0;
|
||||
|
||||
// Count globals that need runtime type init (currently: STRING).
|
||||
// This list survives stripping, unlike debugVars.
|
||||
int32_t globalInitCount = 0;
|
||||
|
||||
for (int32_t i = 0; i < tab->count; i++) {
|
||||
BasSymbolT *s = tab->symbols[i];
|
||||
|
||||
if ((s->kind == SYM_SUB || s->kind == SYM_FUNCTION) && s->isDefined && !s->isExtern) {
|
||||
procCount++;
|
||||
}
|
||||
|
||||
if (s->scope == SCOPE_GLOBAL && s->kind == SYM_VARIABLE && s->dataType == BAS_TYPE_STRING && !s->isArray) {
|
||||
globalInitCount++;
|
||||
}
|
||||
}
|
||||
|
||||
if (globalInitCount > 0) {
|
||||
mod->globalInits = (BasGlobalInitT *)malloc(globalInitCount * sizeof(BasGlobalInitT));
|
||||
|
||||
if (mod->globalInits) {
|
||||
int32_t gi = 0;
|
||||
|
||||
for (int32_t i = 0; i < tab->count; i++) {
|
||||
BasSymbolT *s = tab->symbols[i];
|
||||
|
||||
if (s->scope == SCOPE_GLOBAL && s->kind == SYM_VARIABLE && s->dataType == BAS_TYPE_STRING && !s->isArray) {
|
||||
mod->globalInits[gi].index = s->index;
|
||||
mod->globalInits[gi].dataType = s->dataType;
|
||||
gi++;
|
||||
}
|
||||
}
|
||||
|
||||
mod->globalInitCount = gi;
|
||||
}
|
||||
}
|
||||
|
||||
if (procCount == 0) {
|
||||
|
|
@ -235,6 +263,8 @@ BasModuleT *basCodeGenBuildModuleWithProcs(BasCodeGenT *cg, void *symtab) {
|
|||
BasProcEntryT *p = &mod->procs[idx++];
|
||||
strncpy(p->name, s->name, BAS_MAX_PROC_NAME - 1);
|
||||
p->name[BAS_MAX_PROC_NAME - 1] = '\0';
|
||||
strncpy(p->formName, s->formName, BAS_MAX_PROC_NAME - 1);
|
||||
p->formName[BAS_MAX_PROC_NAME - 1] = '\0';
|
||||
p->codeAddr = s->codeAddr;
|
||||
p->paramCount = s->paramCount;
|
||||
p->localCount = s->localCount;
|
||||
|
|
|
|||
|
|
@ -169,6 +169,19 @@ int32_t basCompactBytecode(BasModuleT *mod) {
|
|||
break;
|
||||
}
|
||||
|
||||
case OP_FOR_INIT: {
|
||||
// [uint16 varIdx] [uint8 scope] [int16 skipOffset]
|
||||
int16_t oldOff = readI16LE(oldCode + oldPc + 4);
|
||||
|
||||
if (!remapRelI16(newCode, newPc, 4, oldOff,
|
||||
oldPc + 6, newPc + 6,
|
||||
remap, oldCodeLen, newCodeLen, false)) {
|
||||
ok = false;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case OP_ON_ERROR: {
|
||||
int16_t oldOff = readI16LE(oldCode + oldPc + 1);
|
||||
|
||||
|
|
@ -421,7 +434,7 @@ static int32_t opOperandSize(uint8_t op) {
|
|||
case OP_RGB:
|
||||
case OP_GET_RED: case OP_GET_GREEN: case OP_GET_BLUE:
|
||||
case OP_STR_VAL: case OP_STR_STRF: case OP_STR_HEX:
|
||||
case OP_STR_STRING:
|
||||
case OP_STR_STRING: case OP_STR_OCT: case OP_CONV_BOOL:
|
||||
case OP_MATH_TIMER: case OP_DATE_STR: case OP_TIME_STR:
|
||||
case OP_SLEEP: case OP_ENVIRON:
|
||||
case OP_READ_DATA: case OP_RESTORE:
|
||||
|
|
@ -444,6 +457,7 @@ static int32_t opOperandSize(uint8_t op) {
|
|||
return 0;
|
||||
|
||||
case OP_LOAD_ARRAY: case OP_STORE_ARRAY:
|
||||
case OP_PUSH_ARR_ADDR:
|
||||
case OP_PRINT_SPC: case OP_FILE_OPEN:
|
||||
case OP_CALL_METHOD: case OP_SHOW_FORM:
|
||||
case OP_LBOUND: case OP_UBOUND:
|
||||
|
|
@ -466,13 +480,13 @@ static int32_t opOperandSize(uint8_t op) {
|
|||
return 2;
|
||||
|
||||
case OP_STORE_ARRAY_FIELD:
|
||||
case OP_FOR_INIT:
|
||||
return 3;
|
||||
|
||||
case OP_PUSH_INT32: case OP_PUSH_FLT32:
|
||||
case OP_CALL:
|
||||
return 4;
|
||||
|
||||
case OP_FOR_INIT:
|
||||
case OP_FOR_NEXT:
|
||||
return 5;
|
||||
|
||||
|
|
|
|||
|
|
@ -338,10 +338,16 @@ BasTokenTypeE basLexerNext(BasLexerT *lex) {
|
|||
return lex->token.type;
|
||||
}
|
||||
|
||||
// Hex literal (&H...)
|
||||
if (c == '&' && upperChar(peekNext(lex)) == 'H') {
|
||||
lex->token.type = tokenizeHexLiteral(lex);
|
||||
return lex->token.type;
|
||||
// Numeric-base literals: &H hex, &O octal, &B binary. &B is an
|
||||
// extension beyond classic QBASIC; it's convenient for bitmask
|
||||
// work in the widget/graphics code.
|
||||
if (c == '&') {
|
||||
char n = upperChar(peekNext(lex));
|
||||
|
||||
if (n == 'H' || n == 'O' || n == 'B') {
|
||||
lex->token.type = tokenizeHexLiteral(lex);
|
||||
return lex->token.type;
|
||||
}
|
||||
}
|
||||
|
||||
// Identifier or keyword
|
||||
|
|
@ -621,30 +627,56 @@ static void skipWhitespace(BasLexerT *lex) {
|
|||
|
||||
|
||||
static BasTokenTypeE tokenizeHexLiteral(BasLexerT *lex) {
|
||||
advance(lex); // skip &
|
||||
advance(lex); // skip H
|
||||
advance(lex); // skip &
|
||||
char base = upperChar(peek(lex));
|
||||
advance(lex); // skip H/O/B
|
||||
|
||||
int32_t idx = 0;
|
||||
int32_t shift;
|
||||
int32_t maxDigit;
|
||||
|
||||
if (base == 'O') {
|
||||
shift = 3;
|
||||
maxDigit = 7;
|
||||
} else if (base == 'B') {
|
||||
shift = 1;
|
||||
maxDigit = 1;
|
||||
} else {
|
||||
shift = 4;
|
||||
maxDigit = 15;
|
||||
}
|
||||
|
||||
int32_t idx = 0;
|
||||
int32_t value = 0;
|
||||
|
||||
while (!atEnd(lex) && isxdigit((unsigned char)peek(lex))) {
|
||||
char c = advance(lex);
|
||||
for (;;) {
|
||||
if (atEnd(lex)) {
|
||||
break;
|
||||
}
|
||||
|
||||
char c = peek(lex);
|
||||
int32_t digit;
|
||||
|
||||
if (c >= '0' && c <= '9') {
|
||||
digit = c - '0';
|
||||
} else if (shift == 4 && c >= 'A' && c <= 'F') {
|
||||
digit = c - 'A' + 10;
|
||||
} else if (shift == 4 && c >= 'a' && c <= 'f') {
|
||||
digit = c - 'a' + 10;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
|
||||
if (digit > maxDigit) {
|
||||
break;
|
||||
}
|
||||
|
||||
advance(lex);
|
||||
|
||||
if (idx < BAS_MAX_TOKEN_LEN - 1) {
|
||||
lex->token.text[idx++] = c;
|
||||
}
|
||||
|
||||
int32_t digit;
|
||||
|
||||
if (c >= '0' && c <= '9') {
|
||||
digit = c - '0';
|
||||
} else if (c >= 'A' && c <= 'F') {
|
||||
digit = c - 'A' + 10;
|
||||
} else {
|
||||
digit = c - 'a' + 10;
|
||||
}
|
||||
|
||||
value = (value << 4) | digit;
|
||||
value = (value << shift) | digit;
|
||||
}
|
||||
|
||||
lex->token.text[idx] = '\0';
|
||||
|
|
|
|||
|
|
@ -566,6 +566,16 @@ static void rewriteModuleProcs(BasModuleT *mod, const NameMapT *map) {
|
|||
continue;
|
||||
}
|
||||
|
||||
// Remap the owning form name (used at runtime to bind form-scope
|
||||
// variables). The form itself gets renamed by the same pass.
|
||||
if (proc->formName[0]) {
|
||||
const char *mappedForm = nameMapLookup(map, proc->formName);
|
||||
|
||||
if (mappedForm) {
|
||||
snprintf(proc->formName, sizeof(proc->formName), "%s", mappedForm);
|
||||
}
|
||||
}
|
||||
|
||||
// Find last underscore
|
||||
char *underscore = strrchr(proc->name, '_');
|
||||
|
||||
|
|
|
|||
|
|
@ -180,7 +180,7 @@ typedef enum {
|
|||
#define OP_GOSUB_RET 0x54 // pop PC from eval stack, jump (GOSUB return)
|
||||
#define OP_RET 0x55 // return from subroutine
|
||||
#define OP_RET_VAL 0x56 // return from function (value on stack)
|
||||
#define OP_FOR_INIT 0x57 // [uint16 varIdx] [uint8 isLocal] init FOR
|
||||
#define OP_FOR_INIT 0x57 // [uint16 varIdx] [uint8 scope] [int16 skipOffset] init FOR, skip body if range empty
|
||||
#define OP_FOR_NEXT 0x58 // [uint16 varIdx] [uint8 isLocal] [int16 loopTop]
|
||||
#define OP_FOR_POP 0x59 // pop top FOR stack entry (for EXIT FOR)
|
||||
|
||||
|
|
@ -297,6 +297,9 @@ typedef enum {
|
|||
#define OP_STR_STRF 0xB1 // STR$(n) -> string
|
||||
#define OP_STR_HEX 0xB2 // HEX$(n) -> string
|
||||
#define OP_STR_STRING 0xB3 // STRING$(n, char) -> string
|
||||
#define OP_STR_OCT 0xF3 // OCT$(n) -> string
|
||||
#define OP_CONV_BOOL 0xF4 // CBOOL(n) -> -1 (true) or 0 (false)
|
||||
#define OP_PUSH_ARR_ADDR 0xF5 // [uint8 dims] pop dims indices, pop array ref, push REF to element
|
||||
|
||||
// ============================================================
|
||||
// Extended built-ins
|
||||
|
|
|
|||
|
|
@ -69,6 +69,7 @@ static const BuiltinFuncT builtinFuncs[] = {
|
|||
{"LEN", OP_STR_LEN, 1, 1, BAS_TYPE_INTEGER},
|
||||
{"LTRIM$", OP_STR_LTRIM, 1, 1, BAS_TYPE_STRING},
|
||||
{"MID$", OP_STR_MID2, 2, 3, BAS_TYPE_STRING},
|
||||
{"OCT$", OP_STR_OCT, 1, 1, BAS_TYPE_STRING},
|
||||
{"RIGHT$", OP_STR_RIGHT, 2, 2, BAS_TYPE_STRING},
|
||||
{"RTRIM$", OP_STR_RTRIM, 1, 1, BAS_TYPE_STRING},
|
||||
{"SPACE$", OP_STR_SPACE, 1, 1, BAS_TYPE_STRING},
|
||||
|
|
@ -84,6 +85,7 @@ static const BuiltinFuncT builtinFuncs[] = {
|
|||
{"LOF", OP_FILE_LOF, 1, 1, BAS_TYPE_LONG},
|
||||
|
||||
// Conversion functions
|
||||
{"CBOOL", OP_CONV_BOOL, 1, 1, BAS_TYPE_BOOLEAN},
|
||||
{"CDBL", OP_CONV_INT_FLT, 1, 1, BAS_TYPE_DOUBLE},
|
||||
{"CINT", OP_CONV_FLT_INT, 1, 1, BAS_TYPE_INTEGER},
|
||||
{"CLNG", OP_CONV_INT_LONG, 1, 1, BAS_TYPE_LONG},
|
||||
|
|
@ -355,6 +357,11 @@ void basParserInit(BasParserT *p, const char *source, int32_t sourceLen) {
|
|||
}
|
||||
|
||||
|
||||
void basParserSetValidator(BasParserT *p, const BasCtrlValidatorT *v) {
|
||||
p->validator = v;
|
||||
}
|
||||
|
||||
|
||||
static bool check(BasParserT *p, BasTokenTypeE type) {
|
||||
return p->lex.token.type == type;
|
||||
}
|
||||
|
|
@ -501,10 +508,10 @@ static void emitByRefArg(BasParserT *p) {
|
|||
strncpy(name, p->lex.token.text, sizeof(name) - 1);
|
||||
name[sizeof(name) - 1] = '\0';
|
||||
|
||||
// Look up the symbol -- must be a simple variable (not array, not const)
|
||||
// Look up the symbol -- must be a variable (simple or array)
|
||||
BasSymbolT *sym = basSymTabFind(&p->sym, name);
|
||||
|
||||
if (!sym || sym->kind != SYM_VARIABLE || sym->isArray) {
|
||||
if (!sym || sym->kind != SYM_VARIABLE) {
|
||||
parseExpression(p);
|
||||
return;
|
||||
}
|
||||
|
|
@ -517,6 +524,44 @@ static void emitByRefArg(BasParserT *p) {
|
|||
|
||||
advance(p); // consume the identifier
|
||||
|
||||
// Array element as BYREF: `arr(i)` or `arr(i, j)`. Emit LOAD of
|
||||
// the array ref, then the indices, then OP_PUSH_ARR_ADDR which
|
||||
// produces a BAS_TYPE_REF pointing at the element. Writes through
|
||||
// that ref in the callee update the actual array element.
|
||||
if (sym->isArray && check(p, TOK_LPAREN)) {
|
||||
advance(p); // consume (
|
||||
|
||||
// Push the array reference
|
||||
emitLoad(p, sym);
|
||||
|
||||
// Parse indices
|
||||
int32_t dims = 0;
|
||||
parseExpression(p);
|
||||
dims++;
|
||||
|
||||
while (match(p, TOK_COMMA)) {
|
||||
parseExpression(p);
|
||||
dims++;
|
||||
}
|
||||
|
||||
expect(p, TOK_RPAREN);
|
||||
|
||||
// If the next token isn't an argument delimiter, this wasn't
|
||||
// a simple `arr(i)` -- rewind and fall back. (rare; e.g.
|
||||
// `arr(i).field` not supported as BYREF.)
|
||||
if (!check(p, TOK_COMMA) && !check(p, TOK_RPAREN) && !check(p, TOK_NEWLINE) && !check(p, TOK_COLON) && !check(p, TOK_EOF) && !check(p, TOK_ELSE)) {
|
||||
// Too late to rewind cleanly -- we've already emitted code.
|
||||
// Treat this as an error. Callers can restructure as a
|
||||
// simple variable BYREF or BYVAL.
|
||||
error(p, "Complex BYREF array expression not supported; use a temporary variable");
|
||||
return;
|
||||
}
|
||||
|
||||
basEmit8(&p->cg, OP_PUSH_ARR_ADDR);
|
||||
basEmit8(&p->cg, (uint8_t)dims);
|
||||
return;
|
||||
}
|
||||
|
||||
// The token after the identifier must be an argument delimiter
|
||||
// (comma, rparen, newline, colon, EOF, ELSE) for this to be a
|
||||
// bare variable reference. Anything else (operator, dot, paren)
|
||||
|
|
@ -1139,14 +1184,24 @@ static void parseAssignOrCall(BasParserT *p) {
|
|||
memberName[BAS_MAX_TOKEN_LEN - 1] = '\0';
|
||||
advance(p); // consume member name
|
||||
|
||||
// If `name` is a regular variable, the user is dereferencing an
|
||||
// object reference (typically a form or control returned by
|
||||
// CreateForm / CreateControl). Use the variable's value as the
|
||||
// ref directly instead of treating `name` as a literal control
|
||||
// name.
|
||||
bool isVarRef = (sym != NULL && sym->kind == SYM_VARIABLE);
|
||||
|
||||
// Special form methods: Show, Hide
|
||||
if (strcasecmp(memberName, "Show") == 0) {
|
||||
// name.Show [modal]
|
||||
// Push form name, LOAD_FORM, SHOW_FORM
|
||||
uint16_t nameIdx = basAddConstant(&p->cg, name, (int32_t)strlen(name));
|
||||
basEmit8(&p->cg, OP_PUSH_STR);
|
||||
basEmitU16(&p->cg, nameIdx);
|
||||
basEmit8(&p->cg, OP_LOAD_FORM);
|
||||
if (isVarRef) {
|
||||
emitLoad(p, sym);
|
||||
} else {
|
||||
uint16_t nameIdx = basAddConstant(&p->cg, name, (int32_t)strlen(name));
|
||||
basEmit8(&p->cg, OP_PUSH_STR);
|
||||
basEmitU16(&p->cg, nameIdx);
|
||||
basEmit8(&p->cg, OP_LOAD_FORM);
|
||||
}
|
||||
uint8_t modal = 0;
|
||||
if (check(p, TOK_INT_LIT)) {
|
||||
if (p->lex.token.intVal != 0) {
|
||||
|
|
@ -1166,10 +1221,14 @@ static void parseAssignOrCall(BasParserT *p) {
|
|||
}
|
||||
|
||||
if (strcasecmp(memberName, "Hide") == 0) {
|
||||
uint16_t nameIdx = basAddConstant(&p->cg, name, (int32_t)strlen(name));
|
||||
basEmit8(&p->cg, OP_PUSH_STR);
|
||||
basEmitU16(&p->cg, nameIdx);
|
||||
basEmit8(&p->cg, OP_LOAD_FORM);
|
||||
if (isVarRef) {
|
||||
emitLoad(p, sym);
|
||||
} else {
|
||||
uint16_t nameIdx = basAddConstant(&p->cg, name, (int32_t)strlen(name));
|
||||
basEmit8(&p->cg, OP_PUSH_STR);
|
||||
basEmitU16(&p->cg, nameIdx);
|
||||
basEmit8(&p->cg, OP_LOAD_FORM);
|
||||
}
|
||||
basEmit8(&p->cg, OP_HIDE_FORM);
|
||||
return;
|
||||
}
|
||||
|
|
@ -1178,16 +1237,33 @@ static void parseAssignOrCall(BasParserT *p) {
|
|||
// Property assignment: CtrlName.Property = expr
|
||||
advance(p); // consume =
|
||||
|
||||
// Push ctrl ref: push form ref (NULL = current), push ctrl name, FIND_CTRL
|
||||
basEmit8(&p->cg, OP_PUSH_INT16);
|
||||
basEmit16(&p->cg, 0);
|
||||
BasValueT formNull;
|
||||
memset(&formNull, 0, sizeof(formNull));
|
||||
// Use OP_PUSH_STR for ctrl name, then FIND_CTRL
|
||||
uint16_t ctrlNameIdx = basAddConstant(&p->cg, name, (int32_t)strlen(name));
|
||||
basEmit8(&p->cg, OP_PUSH_STR);
|
||||
basEmitU16(&p->cg, ctrlNameIdx);
|
||||
basEmit8(&p->cg, OP_FIND_CTRL);
|
||||
// Compile-time validation: if the host provided a validator
|
||||
// (IDE), check that the property exists on the widget type.
|
||||
// Skip when `name` is a variable reference (dynamic ctrl we
|
||||
// can't statically type) or when the ctrl isn't in the map
|
||||
// (e.g. created via CreateControl at runtime).
|
||||
if (!isVarRef && p->validator && p->validator->lookupCtrlType && p->validator->isPropValid) {
|
||||
const char *wgtType = p->validator->lookupCtrlType(p->validator->ctx, name);
|
||||
|
||||
if (wgtType && !p->validator->isPropValid(p->validator->ctx, wgtType, memberName)) {
|
||||
char buf[BAS_PARSE_ERR_SCRATCH];
|
||||
snprintf(buf, sizeof(buf), "Unknown property '%s.%s' (type '%s' has no such property)", name, memberName, wgtType);
|
||||
error(p, buf);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Push ctrl/form ref
|
||||
if (isVarRef) {
|
||||
emitLoad(p, sym);
|
||||
} else {
|
||||
basEmit8(&p->cg, OP_PUSH_INT16);
|
||||
basEmit16(&p->cg, 0);
|
||||
uint16_t ctrlNameIdx = basAddConstant(&p->cg, name, (int32_t)strlen(name));
|
||||
basEmit8(&p->cg, OP_PUSH_STR);
|
||||
basEmitU16(&p->cg, ctrlNameIdx);
|
||||
basEmit8(&p->cg, OP_FIND_CTRL);
|
||||
}
|
||||
|
||||
// Push property name
|
||||
uint16_t propNameIdx = basAddConstant(&p->cg, memberName, (int32_t)strlen(memberName));
|
||||
|
|
@ -1203,13 +1279,29 @@ static void parseAssignOrCall(BasParserT *p) {
|
|||
}
|
||||
|
||||
// Method call: CtrlName.Method [args]
|
||||
// Push ctrl ref
|
||||
basEmit8(&p->cg, OP_PUSH_INT16);
|
||||
basEmit16(&p->cg, 0);
|
||||
uint16_t ctrlNameIdx = basAddConstant(&p->cg, name, (int32_t)strlen(name));
|
||||
basEmit8(&p->cg, OP_PUSH_STR);
|
||||
basEmitU16(&p->cg, ctrlNameIdx);
|
||||
basEmit8(&p->cg, OP_FIND_CTRL);
|
||||
// Same compile-time validation as above, but for methods.
|
||||
if (!isVarRef && p->validator && p->validator->lookupCtrlType && p->validator->isMethodValid) {
|
||||
const char *wgtType = p->validator->lookupCtrlType(p->validator->ctx, name);
|
||||
|
||||
if (wgtType && !p->validator->isMethodValid(p->validator->ctx, wgtType, memberName)) {
|
||||
char buf[BAS_PARSE_ERR_SCRATCH];
|
||||
snprintf(buf, sizeof(buf), "Unknown method '%s.%s' (type '%s' has no such method)", name, memberName, wgtType);
|
||||
error(p, buf);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Push ctrl/form ref
|
||||
if (isVarRef) {
|
||||
emitLoad(p, sym);
|
||||
} else {
|
||||
basEmit8(&p->cg, OP_PUSH_INT16);
|
||||
basEmit16(&p->cg, 0);
|
||||
uint16_t ctrlNameIdx = basAddConstant(&p->cg, name, (int32_t)strlen(name));
|
||||
basEmit8(&p->cg, OP_PUSH_STR);
|
||||
basEmitU16(&p->cg, ctrlNameIdx);
|
||||
basEmit8(&p->cg, OP_FIND_CTRL);
|
||||
}
|
||||
|
||||
// Push method name
|
||||
uint16_t methodNameIdx = basAddConstant(&p->cg, memberName, (int32_t)strlen(memberName));
|
||||
|
|
@ -1412,13 +1504,52 @@ static void parseAssignOrCall(BasParserT *p) {
|
|||
argc++;
|
||||
}
|
||||
}
|
||||
if (!p->hasError && argc != sym->paramCount) {
|
||||
|
||||
// Determine the minimum acceptable count (ignore trailing optionals).
|
||||
int32_t minArgs = sym->requiredParams;
|
||||
bool hasOptional = false;
|
||||
|
||||
for (int32_t i = 0; i < sym->paramCount && i < BAS_MAX_PARAMS; i++) {
|
||||
if (sym->paramOptional[i]) {
|
||||
hasOptional = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasOptional) {
|
||||
minArgs = sym->paramCount;
|
||||
}
|
||||
|
||||
if (!p->hasError && (argc < minArgs || argc > sym->paramCount)) {
|
||||
char buf[BAS_PARSE_ERR_SCRATCH];
|
||||
snprintf(buf, sizeof(buf), "Sub '%s' expects %d arguments, got %d", sym->name, (int)sym->paramCount, (int)argc);
|
||||
|
||||
if (minArgs == sym->paramCount) {
|
||||
snprintf(buf, sizeof(buf), "Sub '%s' expects %d arguments, got %d", sym->name, (int)sym->paramCount, (int)argc);
|
||||
} else {
|
||||
snprintf(buf, sizeof(buf), "Sub '%s' expects %d to %d arguments, got %d", sym->name, (int)minArgs, (int)sym->paramCount, (int)argc);
|
||||
}
|
||||
|
||||
error(p, buf);
|
||||
return;
|
||||
}
|
||||
|
||||
// Pad missing optional arguments with zero-valued defaults so
|
||||
// the callee's OP_CALL receives a full parameter list.
|
||||
while (argc < sym->paramCount) {
|
||||
uint8_t pType = sym->paramTypes[argc];
|
||||
|
||||
if (pType == BAS_TYPE_STRING) {
|
||||
uint16_t idx = basAddConstant(&p->cg, "", 0);
|
||||
basEmit8(&p->cg, OP_PUSH_STR);
|
||||
basEmitU16(&p->cg, idx);
|
||||
} else {
|
||||
basEmit8(&p->cg, OP_PUSH_INT16);
|
||||
basEmit16(&p->cg, 0);
|
||||
}
|
||||
|
||||
argc++;
|
||||
}
|
||||
|
||||
// External library SUB: emit OP_CALL_EXTERN
|
||||
if (sym->isExtern) {
|
||||
basEmit8(&p->cg, OP_CALL_EXTERN);
|
||||
|
|
@ -2047,9 +2178,13 @@ static void parseDef(BasParserT *p) {
|
|||
paramName[BAS_MAX_TOKEN_LEN - 1] = '\0';
|
||||
advance(p);
|
||||
|
||||
uint8_t pdt = suffixToType(paramName);
|
||||
uint8_t pdt = suffixToType(paramName);
|
||||
int32_t pUdtTypeId = -1;
|
||||
if (match(p, TOK_AS)) {
|
||||
pdt = resolveTypeName(p);
|
||||
if (pdt == BAS_TYPE_UDT) {
|
||||
pUdtTypeId = p->lastUdtTypeId;
|
||||
}
|
||||
}
|
||||
|
||||
BasSymbolT *paramSym = basSymTabAdd(&p->sym, paramName, SYM_VARIABLE, pdt);
|
||||
|
|
@ -2060,6 +2195,7 @@ static void parseDef(BasParserT *p) {
|
|||
paramSym->scope = SCOPE_LOCAL;
|
||||
paramSym->index = basSymTabAllocSlot(&p->sym);
|
||||
paramSym->isDefined = true;
|
||||
paramSym->udtTypeId = pUdtTypeId;
|
||||
|
||||
if (paramCount < BAS_MAX_PARAMS) {
|
||||
paramTypes[paramCount] = pdt;
|
||||
|
|
@ -2283,6 +2419,14 @@ static void parseDim(BasParserT *p) {
|
|||
emitUdtInit(p, udtTypeId);
|
||||
emitStore(p, sym);
|
||||
}
|
||||
} else if (dt == BAS_TYPE_STRING) {
|
||||
// STRING slots must start as an empty string, not numeric 0.
|
||||
// Arithmetic falls through basValToNumber so numeric defaults
|
||||
// stay harmless, but STRING concat checks actual slot type.
|
||||
uint16_t idx = basAddConstant(&p->cg, "", 0);
|
||||
basEmit8(&p->cg, OP_PUSH_STR);
|
||||
basEmitU16(&p->cg, idx);
|
||||
emitStore(p, sym);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -2572,10 +2716,15 @@ static void parseFor(BasParserT *p) {
|
|||
basEmit16(&p->cg, 1);
|
||||
}
|
||||
|
||||
// Emit FOR_INIT -- sets up the for-loop state in the VM
|
||||
// Emit FOR_INIT -- sets up the for-loop state in the VM. The
|
||||
// trailing int16 is a forward offset the VM uses to skip the body
|
||||
// when the loop's range is already empty at entry (e.g. FOR i = 10
|
||||
// TO 5). We patch it after the body is emitted.
|
||||
basEmit8(&p->cg, OP_FOR_INIT);
|
||||
basEmitU16(&p->cg, (uint16_t)loopVar->index);
|
||||
basEmit8(&p->cg, (uint8_t)loopVar->scope);
|
||||
int32_t skipOffsetPos = basCodePos(&p->cg);
|
||||
basEmit16(&p->cg, 0); // placeholder
|
||||
|
||||
int32_t loopBody = basCodePos(&p->cg);
|
||||
|
||||
|
|
@ -2606,6 +2755,12 @@ static void parseFor(BasParserT *p) {
|
|||
int16_t backOffset = (int16_t)(loopBody - (basCodePos(&p->cg) + 2));
|
||||
basEmit16(&p->cg, backOffset);
|
||||
|
||||
// Patch FOR_INIT's forward skip offset to point past FOR_NEXT.
|
||||
int32_t loopEnd = basCodePos(&p->cg);
|
||||
int16_t skipOffset = (int16_t)(loopEnd - (skipOffsetPos + 2));
|
||||
p->cg.code[skipOffsetPos] = (uint8_t)(skipOffset & 0xFF);
|
||||
p->cg.code[skipOffsetPos + 1] = (uint8_t)((skipOffset >> 8) & 0xFF);
|
||||
|
||||
// Patch all EXIT FOR jumps to here
|
||||
exitListPatch(&exitForList, p);
|
||||
exitForList = savedExitFor;
|
||||
|
|
@ -2684,9 +2839,13 @@ static void parseFunction(BasParserT *p) {
|
|||
paramName[BAS_MAX_TOKEN_LEN - 1] = '\0';
|
||||
advance(p);
|
||||
|
||||
uint8_t pdt = suffixToType(paramName);
|
||||
uint8_t pdt = suffixToType(paramName);
|
||||
int32_t pUdtTypeId = -1;
|
||||
if (match(p, TOK_AS)) {
|
||||
pdt = resolveTypeName(p);
|
||||
if (pdt == BAS_TYPE_UDT) {
|
||||
pUdtTypeId = p->lastUdtTypeId;
|
||||
}
|
||||
}
|
||||
|
||||
BasSymbolT *paramSym = basSymTabAdd(&p->sym, paramName, SYM_VARIABLE, pdt);
|
||||
|
|
@ -2697,6 +2856,7 @@ static void parseFunction(BasParserT *p) {
|
|||
paramSym->scope = SCOPE_LOCAL;
|
||||
paramSym->index = basSymTabAllocSlot(&p->sym);
|
||||
paramSym->isDefined = true;
|
||||
paramSym->udtTypeId = pUdtTypeId;
|
||||
|
||||
if (paramCount < BAS_MAX_PARAMS) {
|
||||
paramTypes[paramCount] = pdt;
|
||||
|
|
@ -4172,13 +4332,50 @@ static void parsePrimary(BasParserT *p) {
|
|||
memberName[BAS_MAX_TOKEN_LEN - 1] = '\0';
|
||||
advance(p);
|
||||
|
||||
// Emit: push NULL (current form), push ctrl name, FIND_CTRL
|
||||
basEmit8(&p->cg, OP_PUSH_INT16);
|
||||
basEmit16(&p->cg, 0);
|
||||
uint16_t ctrlNameIdx = basAddConstant(&p->cg, name, (int32_t)strlen(name));
|
||||
basEmit8(&p->cg, OP_PUSH_STR);
|
||||
basEmitU16(&p->cg, ctrlNameIdx);
|
||||
basEmit8(&p->cg, OP_FIND_CTRL);
|
||||
bool isVarRef2 = (sym != NULL && sym->kind == SYM_VARIABLE);
|
||||
|
||||
// Compile-time validation for expression-context reads /
|
||||
// method returns. Peek ahead: if '(' follows, memberName
|
||||
// is a method; otherwise it's a property read. Skip when
|
||||
// the host didn't attach a validator or the ctrl is dynamic.
|
||||
if (!isVarRef2 && p->validator && p->validator->lookupCtrlType) {
|
||||
const char *wgtType = p->validator->lookupCtrlType(p->validator->ctx, name);
|
||||
|
||||
if (wgtType) {
|
||||
bool isMethodCall = check(p, TOK_LPAREN);
|
||||
bool valid = true;
|
||||
|
||||
if (isMethodCall && p->validator->isMethodValid) {
|
||||
valid = p->validator->isMethodValid(p->validator->ctx, wgtType, memberName);
|
||||
} else if (!isMethodCall && p->validator->isPropValid) {
|
||||
valid = p->validator->isPropValid(p->validator->ctx, wgtType, memberName);
|
||||
}
|
||||
|
||||
if (!valid) {
|
||||
char buf[BAS_PARSE_ERR_SCRATCH];
|
||||
snprintf(buf, sizeof(buf), "Unknown %s '%s.%s' (type '%s' has no such %s)",
|
||||
isMethodCall ? "method" : "property",
|
||||
name, memberName, wgtType,
|
||||
isMethodCall ? "method" : "property");
|
||||
error(p, buf);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If `name` is a regular variable holding an object reference
|
||||
// (form/control returned by CreateForm/CreateControl), use its
|
||||
// value directly instead of treating `name` as a literal name.
|
||||
if (isVarRef2) {
|
||||
emitLoad(p, sym);
|
||||
} else {
|
||||
basEmit8(&p->cg, OP_PUSH_INT16);
|
||||
basEmit16(&p->cg, 0);
|
||||
uint16_t ctrlNameIdx = basAddConstant(&p->cg, name, (int32_t)strlen(name));
|
||||
basEmit8(&p->cg, OP_PUSH_STR);
|
||||
basEmitU16(&p->cg, ctrlNameIdx);
|
||||
basEmit8(&p->cg, OP_FIND_CTRL);
|
||||
}
|
||||
|
||||
// If followed by '(', this is a method call with args
|
||||
if (check(p, TOK_LPAREN)) {
|
||||
|
|
@ -5583,9 +5780,13 @@ static void parseSub(BasParserT *p) {
|
|||
paramName[BAS_MAX_TOKEN_LEN - 1] = '\0';
|
||||
advance(p);
|
||||
|
||||
uint8_t pdt = suffixToType(paramName);
|
||||
uint8_t pdt = suffixToType(paramName);
|
||||
int32_t pUdtTypeId = -1;
|
||||
if (match(p, TOK_AS)) {
|
||||
pdt = resolveTypeName(p);
|
||||
if (pdt == BAS_TYPE_UDT) {
|
||||
pUdtTypeId = p->lastUdtTypeId;
|
||||
}
|
||||
}
|
||||
|
||||
BasSymbolT *paramSym = basSymTabAdd(&p->sym, paramName, SYM_VARIABLE, pdt);
|
||||
|
|
@ -5596,6 +5797,7 @@ static void parseSub(BasParserT *p) {
|
|||
paramSym->scope = SCOPE_LOCAL;
|
||||
paramSym->index = basSymTabAllocSlot(&p->sym);
|
||||
paramSym->isDefined = true;
|
||||
paramSym->udtTypeId = pUdtTypeId;
|
||||
|
||||
if (paramCount < BAS_MAX_PARAMS) {
|
||||
paramTypes[paramCount] = pdt;
|
||||
|
|
|
|||
|
|
@ -49,6 +49,27 @@
|
|||
#define BAS_PARSE_ERROR_LEN 1024
|
||||
#define BAS_PARSE_ERR_SCRATCH 512
|
||||
|
||||
// Optional compile-time validator for CtrlName.Member references.
|
||||
// The IDE populates this from the project's .frm files + widget DXE
|
||||
// metadata so typos die at compile time instead of at event-click
|
||||
// time. bascomp leaves it NULL (runs on the host, no widget DXEs)
|
||||
// and falls back to the runtime error net. Dynamically-created
|
||||
// controls aren't in the map, so lookupCtrlType returns NULL and
|
||||
// validation is skipped for those -- no false positives.
|
||||
typedef struct {
|
||||
// Return the widget type name for a control declared in a .frm,
|
||||
// or NULL if the control isn't statically known.
|
||||
const char *(*lookupCtrlType)(void *ctx, const char *ctrlName);
|
||||
// Return true if `methodName` is valid for widget type `wgtType`
|
||||
// (or a common method like Refresh/SetFocus). Called with the
|
||||
// type string returned by lookupCtrlType.
|
||||
bool (*isMethodValid)(void *ctx, const char *wgtType, const char *methodName);
|
||||
// Return true if `propName` is a valid property on wgtType.
|
||||
bool (*isPropValid)(void *ctx, const char *wgtType, const char *propName);
|
||||
void *ctx;
|
||||
} BasCtrlValidatorT;
|
||||
|
||||
|
||||
typedef struct {
|
||||
BasLexerT lex;
|
||||
BasCodeGenT cg;
|
||||
|
|
@ -66,6 +87,8 @@ typedef struct {
|
|||
// Per-form init block tracking
|
||||
int32_t formInitJmpAddr; // code position of JMP to patch (-1 = none)
|
||||
int32_t formInitCodeStart; // code position where init block starts (-1 = none)
|
||||
// Optional compile-time CtrlName.Member validator (IDE-only).
|
||||
const BasCtrlValidatorT *validator;
|
||||
} BasParserT;
|
||||
|
||||
// ============================================================
|
||||
|
|
@ -75,6 +98,11 @@ typedef struct {
|
|||
// Initialize parser with source text.
|
||||
void basParserInit(BasParserT *p, const char *source, int32_t sourceLen);
|
||||
|
||||
// Attach an optional compile-time validator for CtrlName.Member
|
||||
// references. The parser borrows the pointer -- caller owns the
|
||||
// underlying struct and must keep it alive until basParserFree.
|
||||
void basParserSetValidator(BasParserT *p, const BasCtrlValidatorT *v);
|
||||
|
||||
// Parse the entire source and generate p-code.
|
||||
// Returns true on success, false on error (check p->error).
|
||||
bool basParse(BasParserT *p);
|
||||
|
|
|
|||
|
|
@ -83,7 +83,13 @@ BasSymbolT *basSymTabAdd(BasSymTabT *tab, const char *name, BasSymKindE kind, ui
|
|||
sym->dataType = dataType;
|
||||
sym->isDefined = true;
|
||||
|
||||
if (scope == SCOPE_FORM && tab->formScopeName[0]) {
|
||||
// Record owning form for both SCOPE_FORM vars AND SUBs/FUNCTIONs
|
||||
// declared inside BEGINFORM...ENDFORM. SUBs stay at SCOPE_GLOBAL
|
||||
// (callable from anywhere) but carry the owning form so the VM can
|
||||
// bind form-scope vars correctly when the SUB is dispatched as an
|
||||
// event handler for a different form's control.
|
||||
if (tab->inFormScope && tab->formScopeName[0] &&
|
||||
(scope == SCOPE_FORM || kind == SYM_SUB || kind == SYM_FUNCTION)) {
|
||||
strncpy(sym->formName, tab->formScopeName, BAS_MAX_SYMBOL_NAME - 1);
|
||||
sym->formName[BAS_MAX_SYMBOL_NAME - 1] = '\0';
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -87,6 +87,15 @@ typedef struct BasControlT {
|
|||
int32_t menuId; // WM menu item ID (>0 for menu items, 0 for controls)
|
||||
BasEventOverrideT eventOverrides[BAS_MAX_EVENT_OVERRIDES];
|
||||
int32_t eventOverrideCount;
|
||||
// Re-entrancy guard: set while an event handler for this control
|
||||
// is running. runSubLoop pumps events during long-running handlers
|
||||
// (painting, draw loops), and without this guard a lingering
|
||||
// mouse-up or repaint cycle can re-fire the same Click on top of
|
||||
// itself -- an unbounded recursion. firingEventName records which
|
||||
// event is in flight so different-event delivery (e.g. LostFocus
|
||||
// while Click is still running) is allowed through.
|
||||
bool eventFiring;
|
||||
char firingEventName[BAS_MAX_CTRL_NAME];
|
||||
} BasControlT;
|
||||
|
||||
// ============================================================
|
||||
|
|
@ -153,6 +162,10 @@ typedef struct {
|
|||
int32_t frmCacheCount;
|
||||
BasCfmCacheT *cfmCache; // stb_ds array of compiled form binaries
|
||||
int32_t cfmCacheCount;
|
||||
// Set true when a runtime error has halted the program; the event
|
||||
// loop exits at the next pump so the app doesn't stumble forward
|
||||
// with a halted VM.
|
||||
bool terminated;
|
||||
} BasFormRtT;
|
||||
|
||||
// ============================================================
|
||||
|
|
@ -209,7 +222,28 @@ void basFormRtRegisterCfm(BasFormRtT *rt, const char *formName, const uint8_t *d
|
|||
|
||||
// ---- Widget creation ----
|
||||
|
||||
WidgetT *createWidget(const char *wgtTypeName, WidgetT *parent);
|
||||
// Create a widget by resolved (DVX) type name. Returns NULL if the type
|
||||
// is unknown. createWidgetByIface dispatches on the interface
|
||||
// descriptor's createSig; pass allowData=true for an empty pixel-data
|
||||
// widget (runtime path), or false to refuse (design-time path).
|
||||
WidgetT *createWidget(const char *wgtTypeName, WidgetT *parent);
|
||||
WidgetT *createWidgetByIface(const WgtIfaceT *iface, const void *api, WidgetT *parent, bool allowData);
|
||||
|
||||
// Resolve a VB-style name ("CommandButton") to a DVX widget name
|
||||
// ("button"). Falls back to the same name if already canonical. Returns
|
||||
// NULL if the type is unknown.
|
||||
const char *resolveTypeName(const char *typeName);
|
||||
|
||||
// Apply a "Key = Value" string pair to a widget property via the
|
||||
// interface descriptor. Handles ENUM/INT/BOOL/STRING dispatch. Returns
|
||||
// true if the property was set, false if unsupported type / no setter.
|
||||
bool wgtApplyPropFromString(WidgetT *w, const WgtPropDescT *p, const char *val);
|
||||
|
||||
// Read a widget property via the interface getter and format it into
|
||||
// out as a .frm-style value ("True", "False", enum name, or decimal
|
||||
// string). Returns true if the property was read; false if the getter
|
||||
// is NULL, the type is not serializable, or the enum value is unknown.
|
||||
bool wgtPropValueToString(const WidgetT *w, const WgtPropDescT *p, char *out, int32_t outSize);
|
||||
|
||||
// ---- Form window creation ----
|
||||
|
||||
|
|
|
|||
308
src/apps/kpunch/dvxbasic/formrt/frmParser.c
Normal file
308
src/apps/kpunch/dvxbasic/formrt/frmParser.c
Normal file
|
|
@ -0,0 +1,308 @@
|
|||
// The MIT License (MIT)
|
||||
//
|
||||
// Copyright (C) 2026 Scott Duensing
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to
|
||||
// deal in the Software without restriction, including without limitation the
|
||||
// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
||||
// sell copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
||||
// IN THE SOFTWARE.
|
||||
|
||||
// frmParser.c -- callback-based .frm text parser
|
||||
//
|
||||
// See frmParser.h for the public API.
|
||||
|
||||
#include "frmParser.h"
|
||||
#include "dvxPlat.h"
|
||||
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <strings.h>
|
||||
|
||||
#define FRM_MAX_LINE_LEN 512
|
||||
#define FRM_MAX_TOKEN_LEN 64
|
||||
#define FRM_MAX_NESTING 16
|
||||
|
||||
|
||||
// Prototypes (alphabetical)
|
||||
bool frmParse(const char *source, int32_t sourceLen, const FrmParserCbsT *cb);
|
||||
bool frmParseBool(const char *val);
|
||||
void frmParseKeyValue(const char *line, char *key, int32_t keyMax, char *value, int32_t valueMax);
|
||||
void frmStripQuotes(char *val);
|
||||
static bool readToken(const char **p, const char *end, char *buf, int32_t bufMax);
|
||||
|
||||
|
||||
typedef enum {
|
||||
BLK_FORM,
|
||||
BLK_MENU,
|
||||
BLK_CTRL
|
||||
} BlkTypeE;
|
||||
|
||||
|
||||
bool frmParse(const char *source, int32_t sourceLen, const FrmParserCbsT *cb) {
|
||||
if (!source || sourceLen <= 0 || !cb) {
|
||||
return false;
|
||||
}
|
||||
|
||||
BlkTypeE blockStack[FRM_MAX_NESTING];
|
||||
int32_t blockDepth = 0;
|
||||
int32_t menuNestDepth = 0;
|
||||
bool inForm = false;
|
||||
|
||||
const char *pos = source;
|
||||
const char *end = source + sourceLen;
|
||||
|
||||
while (pos < end) {
|
||||
const char *lineStart = pos;
|
||||
|
||||
while (pos < end && *pos != '\n' && *pos != '\r') {
|
||||
pos++;
|
||||
}
|
||||
|
||||
int32_t lineLen = (int32_t)(pos - lineStart);
|
||||
|
||||
if (pos < end && *pos == '\r') {
|
||||
pos++;
|
||||
}
|
||||
|
||||
if (pos < end && *pos == '\n') {
|
||||
pos++;
|
||||
}
|
||||
|
||||
char line[FRM_MAX_LINE_LEN];
|
||||
|
||||
if (lineLen >= FRM_MAX_LINE_LEN) {
|
||||
lineLen = FRM_MAX_LINE_LEN - 1;
|
||||
}
|
||||
|
||||
memcpy(line, lineStart, lineLen);
|
||||
line[lineLen] = '\0';
|
||||
|
||||
const char *trimmed = dvxSkipWs(line);
|
||||
|
||||
if (*trimmed == '\0' || *trimmed == '\'') {
|
||||
continue;
|
||||
}
|
||||
|
||||
// "VERSION DVX x.xx" (native) or "VERSION x.xx" (VB import)
|
||||
if (strncasecmp(trimmed, "VERSION ", 8) == 0) {
|
||||
const char *ver = trimmed + 8;
|
||||
|
||||
if (strncasecmp(ver, "DVX ", 4) != 0) {
|
||||
double vbVer = atof(ver);
|
||||
|
||||
if (vbVer > 2.0) {
|
||||
return false; // VB4+ form, not compatible
|
||||
}
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
// "Begin TypeName CtrlName"
|
||||
if (strncasecmp(trimmed, "Begin ", 6) == 0) {
|
||||
const char *rest = trimmed + 6;
|
||||
char typeName[FRM_MAX_TOKEN_LEN];
|
||||
char ctrlName[FRM_MAX_TOKEN_LEN];
|
||||
|
||||
readToken(&rest, NULL, typeName, FRM_MAX_TOKEN_LEN);
|
||||
rest = dvxSkipWs(rest);
|
||||
readToken(&rest, NULL, ctrlName, FRM_MAX_TOKEN_LEN);
|
||||
|
||||
if (typeName[0] == '\0') {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (strcasecmp(typeName, "Form") == 0) {
|
||||
if (cb->onFormBegin && !cb->onFormBegin(cb->userData, ctrlName)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
inForm = true;
|
||||
|
||||
if (blockDepth < FRM_MAX_NESTING) {
|
||||
blockStack[blockDepth++] = BLK_FORM;
|
||||
}
|
||||
} else if (strcasecmp(typeName, "Menu") == 0 && inForm) {
|
||||
if (cb->onMenuBegin) {
|
||||
cb->onMenuBegin(cb->userData, ctrlName, menuNestDepth);
|
||||
}
|
||||
|
||||
menuNestDepth++;
|
||||
|
||||
if (blockDepth < FRM_MAX_NESTING) {
|
||||
blockStack[blockDepth++] = BLK_MENU;
|
||||
}
|
||||
} else if (inForm) {
|
||||
if (cb->onCtrlBegin) {
|
||||
cb->onCtrlBegin(cb->userData, typeName, ctrlName);
|
||||
}
|
||||
|
||||
if (blockDepth < FRM_MAX_NESTING) {
|
||||
blockStack[blockDepth++] = BLK_CTRL;
|
||||
}
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
// "End"
|
||||
if (strcasecmp(trimmed, "End") == 0) {
|
||||
if (blockDepth == 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
blockDepth--;
|
||||
BlkTypeE blk = blockStack[blockDepth];
|
||||
|
||||
if (blk == BLK_MENU) {
|
||||
if (cb->onMenuEnd) {
|
||||
cb->onMenuEnd(cb->userData);
|
||||
}
|
||||
|
||||
menuNestDepth--;
|
||||
|
||||
if (menuNestDepth < 0) {
|
||||
menuNestDepth = 0;
|
||||
}
|
||||
} else if (blk == BLK_CTRL) {
|
||||
if (cb->onCtrlEnd) {
|
||||
cb->onCtrlEnd(cb->userData);
|
||||
}
|
||||
} else {
|
||||
// Outer Form End -- deliver any trailing text and stop.
|
||||
inForm = false;
|
||||
|
||||
if (cb->onFormEnd) {
|
||||
cb->onFormEnd(cb->userData, pos, (int32_t)(end - pos));
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
// Property assignment: Key = Value
|
||||
char key[FRM_MAX_TOKEN_LEN];
|
||||
char value[FRM_MAX_LINE_LEN];
|
||||
frmParseKeyValue(trimmed, key, FRM_MAX_TOKEN_LEN, value, FRM_MAX_LINE_LEN);
|
||||
|
||||
if (key[0] == '\0' || !inForm) {
|
||||
continue;
|
||||
}
|
||||
|
||||
BlkTypeE curBlock = (blockDepth > 0) ? blockStack[blockDepth - 1] : BLK_FORM;
|
||||
|
||||
if (curBlock == BLK_MENU) {
|
||||
if (cb->onMenuProp) {
|
||||
cb->onMenuProp(cb->userData, key, value);
|
||||
}
|
||||
} else if (curBlock == BLK_CTRL) {
|
||||
if (cb->onCtrlProp) {
|
||||
cb->onCtrlProp(cb->userData, key, value);
|
||||
}
|
||||
} else {
|
||||
if (cb->onFormProp) {
|
||||
cb->onFormProp(cb->userData, key, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
bool frmParseBool(const char *val) {
|
||||
if (!val) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (strcasecmp(val, "True") == 0 || strcasecmp(val, "-1") == 0);
|
||||
}
|
||||
|
||||
|
||||
void frmParseKeyValue(const char *line, char *key, int32_t keyMax, char *value, int32_t valueMax) {
|
||||
key[0] = '\0';
|
||||
value[0] = '\0';
|
||||
|
||||
line = dvxSkipWs(line);
|
||||
|
||||
int32_t ki = 0;
|
||||
|
||||
while (*line && *line != '=' && *line != ' ' && *line != '\t' && ki < keyMax - 1) {
|
||||
key[ki++] = *line++;
|
||||
}
|
||||
|
||||
key[ki] = '\0';
|
||||
|
||||
line = dvxSkipWs(line);
|
||||
|
||||
if (*line == '=') {
|
||||
line++;
|
||||
}
|
||||
|
||||
line = dvxSkipWs(line);
|
||||
|
||||
int32_t vi = 0;
|
||||
|
||||
while (*line && *line != '\r' && *line != '\n' && vi < valueMax - 1) {
|
||||
value[vi++] = *line++;
|
||||
}
|
||||
|
||||
value[vi] = '\0';
|
||||
|
||||
while (vi > 0 && (value[vi - 1] == ' ' || value[vi - 1] == '\t')) {
|
||||
value[--vi] = '\0';
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void frmStripQuotes(char *val) {
|
||||
if (!val || val[0] != '"') {
|
||||
return;
|
||||
}
|
||||
|
||||
int32_t len = (int32_t)strlen(val);
|
||||
|
||||
if (len >= 2 && val[len - 1] == '"') {
|
||||
memmove(val, val + 1, len - 2);
|
||||
val[len - 2] = '\0';
|
||||
} else if (len >= 1) {
|
||||
// Unterminated leading quote -- strip just the leading "
|
||||
memmove(val, val + 1, len - 1);
|
||||
val[len - 1] = '\0';
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Read a whitespace-delimited token starting at *p. Advances *p past
|
||||
// the token. If end is non-NULL, reading stops before end.
|
||||
static bool readToken(const char **p, const char *end, char *buf, int32_t bufMax) {
|
||||
const char *cur = *p;
|
||||
int32_t len = 0;
|
||||
|
||||
while (*cur && *cur != ' ' && *cur != '\t' && *cur != '\r' && *cur != '\n' && len < bufMax - 1) {
|
||||
if (end && cur >= end) {
|
||||
break;
|
||||
}
|
||||
|
||||
buf[len++] = *cur++;
|
||||
}
|
||||
|
||||
buf[len] = '\0';
|
||||
*p = cur;
|
||||
return (len > 0);
|
||||
}
|
||||
88
src/apps/kpunch/dvxbasic/formrt/frmParser.h
Normal file
88
src/apps/kpunch/dvxbasic/formrt/frmParser.h
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
// The MIT License (MIT)
|
||||
//
|
||||
// Copyright (C) 2026 Scott Duensing
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to
|
||||
// deal in the Software without restriction, including without limitation the
|
||||
// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
||||
// sell copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
||||
// IN THE SOFTWARE.
|
||||
|
||||
// frmParser.h -- callback-based .frm text parser
|
||||
//
|
||||
// Shared by the form runtime (formrt.c) and the visual designer
|
||||
// (ideDesigner.c). The parser walks the .frm text, recognises
|
||||
// "Begin Form", "Begin Menu", "Begin <Control>", "End", and
|
||||
// "Key = Value" lines, and calls back into the caller for each
|
||||
// semantic event. The caller owns all data structures -- the
|
||||
// parser keeps only internal block-nesting state.
|
||||
|
||||
#ifndef DVXBASIC_FRMPARSER_H
|
||||
#define DVXBASIC_FRMPARSER_H
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
|
||||
// Callbacks supplied by the consumer. Any field may be NULL.
|
||||
typedef struct FrmParserCbsT {
|
||||
void *userData;
|
||||
|
||||
// "Begin Form <name>". Return false to abort parsing.
|
||||
bool (*onFormBegin)(void *userData, const char *name);
|
||||
|
||||
// Form-level "Key = Value" property (not inside any control or menu).
|
||||
void (*onFormProp)(void *userData, const char *key, const char *value);
|
||||
|
||||
// "Begin Menu <name>" at the given nesting level (0 = top-level).
|
||||
void (*onMenuBegin)(void *userData, const char *name, int32_t level);
|
||||
|
||||
// "End" for a menu item.
|
||||
void (*onMenuEnd)(void *userData);
|
||||
|
||||
// Menu-item "Key = Value" property.
|
||||
void (*onMenuProp)(void *userData, const char *key, const char *value);
|
||||
|
||||
// "Begin <Type> <Name>" for a non-Form, non-Menu control.
|
||||
void (*onCtrlBegin)(void *userData, const char *typeName, const char *name);
|
||||
|
||||
// "End" for a control block.
|
||||
void (*onCtrlEnd)(void *userData);
|
||||
|
||||
// Control-level "Key = Value" property.
|
||||
void (*onCtrlProp)(void *userData, const char *key, const char *value);
|
||||
|
||||
// Outer Form's "End" was reached. trailingSrc points into the
|
||||
// input buffer just past the End, with trailingLen bytes remaining.
|
||||
// The designer uses this to capture the BASIC code section.
|
||||
void (*onFormEnd)(void *userData, const char *trailingSrc, int32_t trailingLen);
|
||||
} FrmParserCbsT;
|
||||
|
||||
// Parse a .frm text. Returns true on success, false if the VERSION
|
||||
// line rejects the input or onFormBegin returns false.
|
||||
bool frmParse(const char *source, int32_t sourceLen, const FrmParserCbsT *cb);
|
||||
|
||||
// Parse a trimmed "Key = Value" line into key and value buffers.
|
||||
// value has trailing whitespace stripped but quotes are preserved.
|
||||
void frmParseKeyValue(const char *line, char *key, int32_t keyMax, char *value, int32_t valueMax);
|
||||
|
||||
// Strip surrounding double quotes from val, in place. No-op if the
|
||||
// string is not quoted.
|
||||
void frmStripQuotes(char *val);
|
||||
|
||||
// Classify a value string as a BASIC boolean. True / -1 -> true;
|
||||
// anything else -> false.
|
||||
bool frmParseBool(const char *val);
|
||||
|
||||
#endif // DVXBASIC_FRMPARSER_H
|
||||
|
|
@ -28,6 +28,7 @@
|
|||
|
||||
#include "ideDesigner.h"
|
||||
#include "../formrt/formrt.h"
|
||||
#include "../formrt/frmParser.h"
|
||||
#include "dvxDraw.h"
|
||||
#include "dvxVideo.h"
|
||||
#include "dvxWm.h"
|
||||
|
|
@ -61,16 +62,39 @@ static const char *FORM_DEFAULT_EVENT = "Load";
|
|||
// ============================================================
|
||||
|
||||
// dsgnCreateDesignWidget is declared in ideDesigner.h (non-static)
|
||||
static void dsgnLoad_onCtrlBegin(void *userData, const char *typeName, const char *name);
|
||||
static void dsgnLoad_onCtrlEnd(void *userData);
|
||||
static void dsgnLoad_onCtrlProp(void *userData, const char *key, const char *value);
|
||||
static bool dsgnLoad_onFormBegin(void *userData, const char *name);
|
||||
static void dsgnLoad_onFormEnd(void *userData, const char *trailingSrc, int32_t trailingLen);
|
||||
static void dsgnLoad_onFormProp(void *userData, const char *key, const char *value);
|
||||
static void dsgnLoad_onMenuBegin(void *userData, const char *name, int32_t level);
|
||||
static void dsgnLoad_onMenuEnd(void *userData);
|
||||
static void dsgnLoad_onMenuProp(void *userData, const char *key, const char *value);
|
||||
static const char *getPropValue(const DsgnControlT *ctrl, const char *name);
|
||||
static int32_t hitTestControl(const DsgnStateT *ds, int32_t x, int32_t y);
|
||||
static DsgnHandleE hitTestHandles(const DsgnControlT *ctrl, int32_t x, int32_t y);
|
||||
static void rebuildWidgets(DsgnStateT *ds);
|
||||
static const char *resolveTypeName(const char *typeName);
|
||||
static int32_t saveControls(const DsgnFormT *form, char *buf, int32_t bufSize, int32_t pos, const char *parentName, int32_t indent);
|
||||
static void setPropValue(DsgnControlT *ctrl, const char *name, const char *value);
|
||||
static void syncWidgetGeom(DsgnControlT *ctrl);
|
||||
|
||||
|
||||
// ============================================================
|
||||
// .frm parsing context (used by dsgnLoad_* callbacks)
|
||||
// ============================================================
|
||||
|
||||
typedef struct {
|
||||
DsgnFormT *form;
|
||||
DsgnControlT *current;
|
||||
int32_t curMenuItemIdx;
|
||||
char parentStack[8][DSGN_MAX_NAME];
|
||||
int32_t nestDepth;
|
||||
bool containerStack[BAS_MAX_FRM_NESTING];
|
||||
int32_t containerDepth;
|
||||
} DsgnFrmLoadCtxT;
|
||||
|
||||
|
||||
void dsgnAutoName(const DsgnStateT *ds, const char *typeName, char *buf, int32_t bufSize) {
|
||||
// Look up the name prefix from the widget interface descriptor.
|
||||
// Falls back to the type name itself if no prefix is registered.
|
||||
|
|
@ -156,8 +180,10 @@ WidgetT *dsgnCreateContentBox(WidgetT *root, const char *layout) {
|
|||
}
|
||||
|
||||
|
||||
// Create a real DVX widget for design-time display. Mirrors the
|
||||
// logic in formrt.c createWidget().
|
||||
// Create a real DVX widget for design-time display. Uses the shared
|
||||
// createWidgetByIface switch from formrt; design-time refuses
|
||||
// WGT_CREATE_PARENT_DATA widgets (Image/ImageButton) since we have no
|
||||
// pixel data.
|
||||
WidgetT *dsgnCreateDesignWidget(const char *vbTypeName, WidgetT *parent) {
|
||||
const char *wgtName = resolveTypeName(vbTypeName);
|
||||
|
||||
|
|
@ -171,50 +197,7 @@ WidgetT *dsgnCreateDesignWidget(const char *vbTypeName, WidgetT *parent) {
|
|||
return NULL;
|
||||
}
|
||||
|
||||
const WgtIfaceT *iface = wgtGetIface(wgtName);
|
||||
uint8_t sig = iface ? iface->createSig : WGT_CREATE_PARENT;
|
||||
|
||||
typedef WidgetT *(*CreateParentFnT)(WidgetT *);
|
||||
typedef WidgetT *(*CreateParentTextFnT)(WidgetT *, const char *);
|
||||
typedef WidgetT *(*CreateParentIntFnT)(WidgetT *, int32_t);
|
||||
typedef WidgetT *(*CreateParentIntIntFnT)(WidgetT *, int32_t, int32_t);
|
||||
typedef WidgetT *(*CreateParentIntIntIntFnT)(WidgetT *, int32_t, int32_t, int32_t);
|
||||
typedef WidgetT *(*CreateParentIntBoolFnT)(WidgetT *, int32_t, bool);
|
||||
typedef WidgetT *(*CreateParentBoolFnT)(WidgetT *, bool);
|
||||
|
||||
switch (sig) {
|
||||
case WGT_CREATE_PARENT_TEXT: {
|
||||
CreateParentTextFnT fn = *(CreateParentTextFnT *)api;
|
||||
return fn(parent, "");
|
||||
}
|
||||
case WGT_CREATE_PARENT_INT: {
|
||||
CreateParentIntFnT fn = *(CreateParentIntFnT *)api;
|
||||
return fn(parent, iface->createArgs[0]);
|
||||
}
|
||||
case WGT_CREATE_PARENT_INT_INT: {
|
||||
CreateParentIntIntFnT fn = *(CreateParentIntIntFnT *)api;
|
||||
return fn(parent, iface->createArgs[0], iface->createArgs[1]);
|
||||
}
|
||||
case WGT_CREATE_PARENT_INT_INT_INT: {
|
||||
CreateParentIntIntIntFnT fn = *(CreateParentIntIntIntFnT *)api;
|
||||
return fn(parent, iface->createArgs[0], iface->createArgs[1], iface->createArgs[2]);
|
||||
}
|
||||
case WGT_CREATE_PARENT_INT_BOOL: {
|
||||
CreateParentIntBoolFnT fn = *(CreateParentIntBoolFnT *)api;
|
||||
return fn(parent, iface->createArgs[0], (bool)iface->createArgs[1]);
|
||||
}
|
||||
case WGT_CREATE_PARENT_BOOL: {
|
||||
CreateParentBoolFnT fn = *(CreateParentBoolFnT *)api;
|
||||
return fn(parent, (bool)iface->createArgs[0]);
|
||||
}
|
||||
case WGT_CREATE_PARENT_DATA:
|
||||
// Image/ImageButton -- cannot auto-create without pixel data
|
||||
return NULL;
|
||||
default: {
|
||||
CreateParentFnT fn = *(CreateParentFnT *)api;
|
||||
return fn(parent);
|
||||
}
|
||||
}
|
||||
return createWidgetByIface(wgtGetIface(wgtName), api, parent, false);
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -320,23 +303,8 @@ void dsgnCreateWidgets(DsgnStateT *ds, WidgetT *contentBox) {
|
|||
|
||||
const char *val = getPropValue(ctrl, p->name);
|
||||
|
||||
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)(w, en);
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else if (p->type == WGT_IFACE_INT) {
|
||||
((void (*)(WidgetT *, int32_t))p->setFn)(w, atoi(val));
|
||||
} else if (p->type == WGT_IFACE_BOOL) {
|
||||
((void (*)(WidgetT *, bool))p->setFn)(w, strcasecmp(val, "True") == 0);
|
||||
} else if (p->type == WGT_IFACE_STRING) {
|
||||
((void (*)(WidgetT *, const char *))p->setFn)(w, val);
|
||||
if (val) {
|
||||
wgtApplyPropFromString(w, p, val);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -427,259 +395,27 @@ bool dsgnLoadFrm(DsgnStateT *ds, const char *source, int32_t sourceLen) {
|
|||
snprintf(form->name, DSGN_MAX_NAME, "Form1");
|
||||
snprintf(form->caption, DSGN_MAX_TEXT, "Form1");
|
||||
|
||||
DsgnControlT *curCtrl = NULL;
|
||||
DsgnMenuItemT *curMenuItem = NULL;
|
||||
bool inForm = false;
|
||||
bool inMenu = false;
|
||||
int32_t menuNestDepth = 0;
|
||||
int32_t blockDepth = 0; // Begin/End nesting depth (0 = form level)
|
||||
bool blockIsContainer[BAS_MAX_FRM_NESTING]; // whether each block is a container
|
||||
DsgnFrmLoadCtxT ctx;
|
||||
memset(&ctx, 0, sizeof(ctx));
|
||||
ctx.form = form;
|
||||
ctx.curMenuItemIdx = -1;
|
||||
|
||||
// Parent name stack for nesting (index 0 = form level)
|
||||
char parentStack[8][DSGN_MAX_NAME];
|
||||
int32_t nestDepth = 0;
|
||||
parentStack[0][0] = '\0';
|
||||
FrmParserCbsT cbs;
|
||||
memset(&cbs, 0, sizeof(cbs));
|
||||
cbs.userData = &ctx;
|
||||
cbs.onFormBegin = dsgnLoad_onFormBegin;
|
||||
cbs.onFormProp = dsgnLoad_onFormProp;
|
||||
cbs.onFormEnd = dsgnLoad_onFormEnd;
|
||||
cbs.onMenuBegin = dsgnLoad_onMenuBegin;
|
||||
cbs.onMenuEnd = dsgnLoad_onMenuEnd;
|
||||
cbs.onMenuProp = dsgnLoad_onMenuProp;
|
||||
cbs.onCtrlBegin = dsgnLoad_onCtrlBegin;
|
||||
cbs.onCtrlEnd = dsgnLoad_onCtrlEnd;
|
||||
cbs.onCtrlProp = dsgnLoad_onCtrlProp;
|
||||
|
||||
const char *pos = source;
|
||||
const char *end = source + sourceLen;
|
||||
|
||||
while (pos < end) {
|
||||
const char *lineStart = pos;
|
||||
|
||||
while (pos < end && *pos != '\n' && *pos != '\r') {
|
||||
pos++;
|
||||
}
|
||||
|
||||
int32_t lineLen = (int32_t)(pos - lineStart);
|
||||
|
||||
if (pos < end && *pos == '\r') { pos++; }
|
||||
if (pos < end && *pos == '\n') { pos++; }
|
||||
|
||||
char line[BAS_MAX_FRM_LINE_LEN];
|
||||
|
||||
if (lineLen >= BAS_MAX_FRM_LINE_LEN) {
|
||||
lineLen = BAS_MAX_FRM_LINE_LEN - 1;
|
||||
}
|
||||
|
||||
memcpy(line, lineStart, lineLen);
|
||||
line[lineLen] = '\0';
|
||||
|
||||
const char *trimmed = dvxSkipWs(line);
|
||||
|
||||
if (*trimmed == '\0' || *trimmed == '\'') {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (strncasecmp(trimmed, "VERSION ", 8) == 0) {
|
||||
// Accept "VERSION DVX x.xx" (native) and "VERSION x.xx" (VB import).
|
||||
// Reject VB forms with version > 1.xx (VB4+/VB6 use features we
|
||||
// don't support like OLE controls and binary properties).
|
||||
const char *ver = trimmed + 8;
|
||||
|
||||
if (strncasecmp(ver, "DVX ", 4) == 0) {
|
||||
// Native DVX BASIC form -- always accepted
|
||||
} else {
|
||||
// VB form -- check version number
|
||||
double vbVer = atof(ver);
|
||||
|
||||
if (vbVer > 2.0) {
|
||||
return false; // VB4+ form, not compatible
|
||||
}
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
// Begin TypeName CtrlName
|
||||
if (strncasecmp(trimmed, "Begin ", 6) == 0) {
|
||||
const char *rest = trimmed + 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';
|
||||
|
||||
rest = dvxSkipWs(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';
|
||||
|
||||
if (strcasecmp(typeName, "Form") == 0) {
|
||||
snprintf(form->name, DSGN_MAX_NAME, "%s", ctrlName);
|
||||
snprintf(form->caption, DSGN_MAX_TEXT, "%s", ctrlName);
|
||||
inForm = true;
|
||||
nestDepth = 0;
|
||||
curCtrl = NULL;
|
||||
} else if (strcasecmp(typeName, "Menu") == 0 && inForm) {
|
||||
DsgnMenuItemT mi;
|
||||
memset(&mi, 0, sizeof(mi));
|
||||
snprintf(mi.name, DSGN_MAX_NAME, "%s", ctrlName);
|
||||
mi.level = menuNestDepth;
|
||||
mi.enabled = true;
|
||||
arrput(form->menuItems, mi);
|
||||
curMenuItem = &form->menuItems[arrlen(form->menuItems) - 1];
|
||||
curCtrl = NULL; // not a control
|
||||
menuNestDepth++;
|
||||
inMenu = true;
|
||||
if (blockDepth < BAS_MAX_FRM_NESTING) { blockIsContainer[blockDepth] = false; }
|
||||
blockDepth++;
|
||||
} else if (inForm) {
|
||||
DsgnControlT *cp = (DsgnControlT *)calloc(1, sizeof(DsgnControlT));
|
||||
cp->index = -1;
|
||||
snprintf(cp->name, DSGN_MAX_NAME, "%s", ctrlName);
|
||||
snprintf(cp->typeName, DSGN_MAX_NAME, "%s", typeName);
|
||||
|
||||
// Set parent from current nesting
|
||||
if (nestDepth > 0) {
|
||||
snprintf(cp->parentName, DSGN_MAX_NAME, "%s", parentStack[nestDepth - 1]);
|
||||
}
|
||||
|
||||
cp->width = DEFAULT_CTRL_W;
|
||||
cp->height = DEFAULT_CTRL_H;
|
||||
arrput(form->controls, cp);
|
||||
curCtrl = form->controls[arrlen(form->controls) - 1];
|
||||
bool isCtrl = dsgnIsContainer(typeName);
|
||||
if (blockDepth < BAS_MAX_FRM_NESTING) { blockIsContainer[blockDepth] = isCtrl; }
|
||||
blockDepth++;
|
||||
|
||||
// If this is a container, push onto parent stack
|
||||
if (isCtrl && nestDepth < 7) {
|
||||
snprintf(parentStack[nestDepth], DSGN_MAX_NAME, "%s", ctrlName);
|
||||
nestDepth++;
|
||||
}
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (strcasecmp(trimmed, "End") == 0) {
|
||||
if (blockDepth > 0) {
|
||||
blockDepth--;
|
||||
|
||||
if (inMenu) {
|
||||
menuNestDepth--;
|
||||
curMenuItem = NULL;
|
||||
|
||||
if (menuNestDepth <= 0) {
|
||||
menuNestDepth = 0;
|
||||
inMenu = false;
|
||||
}
|
||||
} else {
|
||||
// If we're closing a container, pop the parent stack
|
||||
if (blockDepth < BAS_MAX_FRM_NESTING && blockIsContainer[blockDepth] && nestDepth > 0) {
|
||||
nestDepth--;
|
||||
}
|
||||
|
||||
curCtrl = NULL;
|
||||
}
|
||||
} else {
|
||||
// blockDepth == 0: this is the form's closing End
|
||||
inForm = false;
|
||||
|
||||
// Everything after the form's closing End is code
|
||||
if (pos < end) {
|
||||
// Skip leading blank lines
|
||||
const char *codeStart = pos;
|
||||
|
||||
while (codeStart < end && (*codeStart == '\r' || *codeStart == '\n' || *codeStart == ' ' || *codeStart == '\t')) {
|
||||
codeStart++;
|
||||
}
|
||||
|
||||
if (codeStart < end) {
|
||||
int32_t codeLen = (int32_t)(end - codeStart);
|
||||
form->code = (char *)malloc(codeLen + 1);
|
||||
|
||||
if (form->code) {
|
||||
memcpy(form->code, codeStart, codeLen);
|
||||
form->code[codeLen] = '\0';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
break; // done parsing
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
// Property = Value
|
||||
const char *eq = strchr(trimmed, '=');
|
||||
|
||||
if (eq && inForm) {
|
||||
char key[DSGN_MAX_NAME];
|
||||
const char *kend = eq - 1;
|
||||
|
||||
while (kend > trimmed && (*kend == ' ' || *kend == '\t')) { kend--; }
|
||||
|
||||
int32_t klen = (int32_t)(kend - trimmed + 1);
|
||||
|
||||
if (klen >= DSGN_MAX_NAME) { klen = DSGN_MAX_NAME - 1; }
|
||||
|
||||
memcpy(key, trimmed, klen);
|
||||
key[klen] = '\0';
|
||||
|
||||
const char *vstart = dvxSkipWs(eq + 1);
|
||||
|
||||
char val[DSGN_MAX_TEXT];
|
||||
int32_t vi = 0;
|
||||
|
||||
if (*vstart == '"') {
|
||||
vstart++;
|
||||
|
||||
while (*vstart && *vstart != '"' && vi < DSGN_MAX_TEXT - 1) {
|
||||
val[vi++] = *vstart++;
|
||||
}
|
||||
} else {
|
||||
while (*vstart && *vstart != '\r' && *vstart != '\n' && vi < DSGN_MAX_TEXT - 1) {
|
||||
val[vi++] = *vstart++;
|
||||
}
|
||||
|
||||
while (vi > 0 && (val[vi - 1] == ' ' || val[vi - 1] == '\t')) { vi--; }
|
||||
}
|
||||
|
||||
val[vi] = '\0';
|
||||
|
||||
if (curMenuItem) {
|
||||
if (strcasecmp(key, "Caption") == 0) { snprintf(curMenuItem->caption, DSGN_MAX_TEXT, "%s", val); }
|
||||
else if (strcasecmp(key, "Checked") == 0) { curMenuItem->checked = (strcasecmp(val, "True") == 0 || strcasecmp(val, "-1") == 0); }
|
||||
else if (strcasecmp(key, "RadioCheck") == 0) { curMenuItem->radioCheck = (strcasecmp(val, "True") == 0 || strcasecmp(val, "-1") == 0); }
|
||||
else if (strcasecmp(key, "Enabled") == 0) { curMenuItem->enabled = (strcasecmp(val, "True") == 0 || strcasecmp(val, "-1") == 0); }
|
||||
} else if (curCtrl) {
|
||||
if (strcasecmp(key, "Left") == 0) { curCtrl->left = atoi(val); }
|
||||
else if (strcasecmp(key, "Top") == 0) { curCtrl->top = atoi(val); }
|
||||
else if (strcasecmp(key, "MinWidth") == 0 ||
|
||||
strcasecmp(key, "Width") == 0) { curCtrl->width = atoi(val); }
|
||||
else if (strcasecmp(key, "MinHeight") == 0 ||
|
||||
strcasecmp(key, "Height") == 0) { curCtrl->height = atoi(val); }
|
||||
else if (strcasecmp(key, "MaxWidth") == 0) { curCtrl->maxWidth = atoi(val); }
|
||||
else if (strcasecmp(key, "MaxHeight") == 0) { curCtrl->maxHeight = atoi(val); }
|
||||
else if (strcasecmp(key, "Weight") == 0) { curCtrl->weight = atoi(val); }
|
||||
else if (strcasecmp(key, "Index") == 0) { curCtrl->index = atoi(val); }
|
||||
else if (strcasecmp(key, "HelpTopic") == 0) { snprintf(curCtrl->helpTopic, DSGN_MAX_NAME, "%s", val); }
|
||||
else if (strcasecmp(key, "TabIndex") == 0) { /* ignored -- DVX has no tab order */ }
|
||||
else { setPropValue(curCtrl, key, val); }
|
||||
} else {
|
||||
if (strcasecmp(key, "Caption") == 0) { snprintf(form->caption, DSGN_MAX_TEXT, "%s", val); }
|
||||
else if (strcasecmp(key, "Layout") == 0) { strncpy(form->layout, val, DSGN_MAX_NAME - 1); form->layout[DSGN_MAX_NAME - 1] = '\0'; }
|
||||
else if (strcasecmp(key, "AutoSize") == 0) { form->autoSize = (strcasecmp(val, "True") == 0); }
|
||||
else if (strcasecmp(key, "Resizable") == 0) { form->resizable = (strcasecmp(val, "True") == 0); }
|
||||
else if (strcasecmp(key, "Centered") == 0) { form->centered = (strcasecmp(val, "True") == 0); }
|
||||
else if (strcasecmp(key, "Left") == 0) { form->left = atoi(val); }
|
||||
else if (strcasecmp(key, "Top") == 0) { form->top = atoi(val); }
|
||||
else if (strcasecmp(key, "Width") == 0) { form->width = atoi(val); form->autoSize = false; }
|
||||
else if (strcasecmp(key, "Height") == 0) { form->height = atoi(val); form->autoSize = false; }
|
||||
else if (strcasecmp(key, "HelpTopic") == 0) { snprintf(form->helpTopic, DSGN_MAX_NAME, "%s", val); }
|
||||
}
|
||||
}
|
||||
if (!frmParse(source, sourceLen, &cbs)) {
|
||||
free(form);
|
||||
return false;
|
||||
}
|
||||
|
||||
ds->form = form;
|
||||
|
|
@ -688,6 +424,239 @@ bool dsgnLoadFrm(DsgnStateT *ds, const char *source, int32_t sourceLen) {
|
|||
}
|
||||
|
||||
|
||||
static void dsgnLoad_onCtrlBegin(void *userData, const char *typeName, const char *name) {
|
||||
DsgnFrmLoadCtxT *ctx = (DsgnFrmLoadCtxT *)userData;
|
||||
|
||||
if (!ctx->form) {
|
||||
return;
|
||||
}
|
||||
|
||||
DsgnControlT *cp = (DsgnControlT *)calloc(1, sizeof(DsgnControlT));
|
||||
|
||||
if (!cp) {
|
||||
if (ctx->containerDepth < BAS_MAX_FRM_NESTING) {
|
||||
ctx->containerStack[ctx->containerDepth++] = false;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
cp->index = -1;
|
||||
snprintf(cp->name, DSGN_MAX_NAME, "%s", name);
|
||||
snprintf(cp->typeName, DSGN_MAX_NAME, "%s", typeName);
|
||||
|
||||
if (ctx->nestDepth > 0) {
|
||||
snprintf(cp->parentName, DSGN_MAX_NAME, "%s", ctx->parentStack[ctx->nestDepth - 1]);
|
||||
}
|
||||
|
||||
cp->width = DEFAULT_CTRL_W;
|
||||
cp->height = DEFAULT_CTRL_H;
|
||||
arrput(ctx->form->controls, cp);
|
||||
ctx->current = ctx->form->controls[arrlen(ctx->form->controls) - 1];
|
||||
|
||||
bool isCtrl = dsgnIsContainer(typeName);
|
||||
|
||||
if (ctx->containerDepth < BAS_MAX_FRM_NESTING) {
|
||||
ctx->containerStack[ctx->containerDepth++] = isCtrl;
|
||||
}
|
||||
|
||||
if (isCtrl && ctx->nestDepth < 7) {
|
||||
snprintf(ctx->parentStack[ctx->nestDepth], DSGN_MAX_NAME, "%s", name);
|
||||
ctx->nestDepth++;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
static void dsgnLoad_onCtrlEnd(void *userData) {
|
||||
DsgnFrmLoadCtxT *ctx = (DsgnFrmLoadCtxT *)userData;
|
||||
|
||||
if (ctx->containerDepth > 0) {
|
||||
ctx->containerDepth--;
|
||||
|
||||
if (ctx->containerStack[ctx->containerDepth] && ctx->nestDepth > 0) {
|
||||
ctx->nestDepth--;
|
||||
}
|
||||
}
|
||||
|
||||
ctx->current = NULL;
|
||||
}
|
||||
|
||||
|
||||
static void dsgnLoad_onCtrlProp(void *userData, const char *key, const char *value) {
|
||||
DsgnFrmLoadCtxT *ctx = (DsgnFrmLoadCtxT *)userData;
|
||||
|
||||
if (!ctx->current) {
|
||||
return;
|
||||
}
|
||||
|
||||
char val[DSGN_MAX_TEXT];
|
||||
snprintf(val, sizeof(val), "%s", value);
|
||||
frmStripQuotes(val);
|
||||
|
||||
DsgnControlT *cc = ctx->current;
|
||||
|
||||
if (strcasecmp(key, "Left") == 0) {
|
||||
cc->left = atoi(val);
|
||||
} else if (strcasecmp(key, "Top") == 0) {
|
||||
cc->top = atoi(val);
|
||||
} else if (strcasecmp(key, "MinWidth") == 0 || strcasecmp(key, "Width") == 0) {
|
||||
cc->width = atoi(val);
|
||||
} else if (strcasecmp(key, "MinHeight") == 0 || strcasecmp(key, "Height") == 0) {
|
||||
cc->height = atoi(val);
|
||||
} else if (strcasecmp(key, "MaxWidth") == 0) {
|
||||
cc->maxWidth = atoi(val);
|
||||
} else if (strcasecmp(key, "MaxHeight") == 0) {
|
||||
cc->maxHeight = atoi(val);
|
||||
} else if (strcasecmp(key, "Weight") == 0) {
|
||||
cc->weight = atoi(val);
|
||||
} else if (strcasecmp(key, "Index") == 0) {
|
||||
cc->index = atoi(val);
|
||||
} else if (strcasecmp(key, "HelpTopic") == 0) {
|
||||
snprintf(cc->helpTopic, DSGN_MAX_NAME, "%s", val);
|
||||
} else if (strcasecmp(key, "TabIndex") == 0) {
|
||||
// ignored -- DVX has no tab order
|
||||
} else {
|
||||
setPropValue(cc, key, val);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
static bool dsgnLoad_onFormBegin(void *userData, const char *name) {
|
||||
DsgnFrmLoadCtxT *ctx = (DsgnFrmLoadCtxT *)userData;
|
||||
|
||||
if (!ctx->form) {
|
||||
return false;
|
||||
}
|
||||
|
||||
snprintf(ctx->form->name, DSGN_MAX_NAME, "%s", name);
|
||||
snprintf(ctx->form->caption, DSGN_MAX_TEXT, "%s", name);
|
||||
ctx->current = NULL;
|
||||
ctx->nestDepth = 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
static void dsgnLoad_onFormEnd(void *userData, const char *trailingSrc, int32_t trailingLen) {
|
||||
DsgnFrmLoadCtxT *ctx = (DsgnFrmLoadCtxT *)userData;
|
||||
|
||||
if (!ctx->form || trailingLen <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Skip leading whitespace/blank lines
|
||||
const char *codeStart = trailingSrc;
|
||||
const char *codeEnd = trailingSrc + trailingLen;
|
||||
|
||||
while (codeStart < codeEnd && (*codeStart == '\r' || *codeStart == '\n' || *codeStart == ' ' || *codeStart == '\t')) {
|
||||
codeStart++;
|
||||
}
|
||||
|
||||
if (codeStart >= codeEnd) {
|
||||
return;
|
||||
}
|
||||
|
||||
int32_t codeLen = (int32_t)(codeEnd - codeStart);
|
||||
ctx->form->code = (char *)malloc(codeLen + 1);
|
||||
|
||||
if (ctx->form->code) {
|
||||
memcpy(ctx->form->code, codeStart, codeLen);
|
||||
ctx->form->code[codeLen] = '\0';
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
static void dsgnLoad_onFormProp(void *userData, const char *key, const char *value) {
|
||||
DsgnFrmLoadCtxT *ctx = (DsgnFrmLoadCtxT *)userData;
|
||||
|
||||
if (!ctx->form) {
|
||||
return;
|
||||
}
|
||||
|
||||
char val[DSGN_MAX_TEXT];
|
||||
snprintf(val, sizeof(val), "%s", value);
|
||||
frmStripQuotes(val);
|
||||
|
||||
DsgnFormT *ff = ctx->form;
|
||||
|
||||
if (strcasecmp(key, "Caption") == 0) {
|
||||
snprintf(ff->caption, DSGN_MAX_TEXT, "%s", val);
|
||||
} else if (strcasecmp(key, "Layout") == 0) {
|
||||
strncpy(ff->layout, val, DSGN_MAX_NAME - 1);
|
||||
ff->layout[DSGN_MAX_NAME - 1] = '\0';
|
||||
} else if (strcasecmp(key, "AutoSize") == 0) {
|
||||
ff->autoSize = frmParseBool(val);
|
||||
} else if (strcasecmp(key, "Resizable") == 0) {
|
||||
ff->resizable = frmParseBool(val);
|
||||
} else if (strcasecmp(key, "Centered") == 0) {
|
||||
ff->centered = frmParseBool(val);
|
||||
} else if (strcasecmp(key, "Left") == 0) {
|
||||
ff->left = atoi(val);
|
||||
} else if (strcasecmp(key, "Top") == 0) {
|
||||
ff->top = atoi(val);
|
||||
} else if (strcasecmp(key, "Width") == 0) {
|
||||
ff->width = atoi(val);
|
||||
ff->autoSize = false;
|
||||
} else if (strcasecmp(key, "Height") == 0) {
|
||||
ff->height = atoi(val);
|
||||
ff->autoSize = false;
|
||||
} else if (strcasecmp(key, "HelpTopic") == 0) {
|
||||
snprintf(ff->helpTopic, DSGN_MAX_NAME, "%s", val);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
static void dsgnLoad_onMenuBegin(void *userData, const char *name, int32_t level) {
|
||||
DsgnFrmLoadCtxT *ctx = (DsgnFrmLoadCtxT *)userData;
|
||||
|
||||
if (!ctx->form) {
|
||||
return;
|
||||
}
|
||||
|
||||
DsgnMenuItemT mi;
|
||||
memset(&mi, 0, sizeof(mi));
|
||||
snprintf(mi.name, DSGN_MAX_NAME, "%s", name);
|
||||
mi.level = level;
|
||||
mi.enabled = true;
|
||||
arrput(ctx->form->menuItems, mi);
|
||||
ctx->curMenuItemIdx = (int32_t)arrlen(ctx->form->menuItems) - 1;
|
||||
ctx->current = NULL;
|
||||
}
|
||||
|
||||
|
||||
static void dsgnLoad_onMenuEnd(void *userData) {
|
||||
DsgnFrmLoadCtxT *ctx = (DsgnFrmLoadCtxT *)userData;
|
||||
|
||||
ctx->curMenuItemIdx = -1;
|
||||
}
|
||||
|
||||
|
||||
static void dsgnLoad_onMenuProp(void *userData, const char *key, const char *value) {
|
||||
DsgnFrmLoadCtxT *ctx = (DsgnFrmLoadCtxT *)userData;
|
||||
|
||||
if (!ctx->form ||
|
||||
ctx->curMenuItemIdx < 0 ||
|
||||
ctx->curMenuItemIdx >= (int32_t)arrlen(ctx->form->menuItems)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Resolve pointer fresh each write -- arrput on nested menus may
|
||||
// have reallocated the array.
|
||||
DsgnMenuItemT *mip = &ctx->form->menuItems[ctx->curMenuItemIdx];
|
||||
char val[DSGN_MAX_TEXT];
|
||||
snprintf(val, sizeof(val), "%s", value);
|
||||
frmStripQuotes(val);
|
||||
|
||||
if (strcasecmp(key, "Caption") == 0) {
|
||||
snprintf(mip->caption, DSGN_MAX_TEXT, "%s", val);
|
||||
} else if (strcasecmp(key, "Checked") == 0) {
|
||||
mip->checked = frmParseBool(val);
|
||||
} else if (strcasecmp(key, "RadioCheck") == 0) {
|
||||
mip->radioCheck = frmParseBool(val);
|
||||
} else if (strcasecmp(key, "Enabled") == 0) {
|
||||
mip->enabled = frmParseBool(val);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void dsgnNewForm(DsgnStateT *ds, const char *name) {
|
||||
dsgnFree(ds);
|
||||
|
||||
|
|
@ -1218,21 +1187,6 @@ static void rebuildWidgets(DsgnStateT *ds) {
|
|||
}
|
||||
|
||||
|
||||
static const char *resolveTypeName(const char *typeName) {
|
||||
const char *wgtName = wgtFindByBasName(typeName);
|
||||
|
||||
if (wgtName) {
|
||||
return wgtName;
|
||||
}
|
||||
|
||||
if (wgtGetApi(typeName)) {
|
||||
return typeName;
|
||||
}
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
||||
|
||||
// Write controls at a given nesting level with the specified parent name.
|
||||
static int32_t saveControls(const DsgnFormT *form, char *buf, int32_t bufSize, int32_t pos, const char *parentName, int32_t indent) {
|
||||
int32_t count = (int32_t)arrlen(form->controls);
|
||||
|
|
@ -1318,26 +1272,17 @@ static int32_t saveControls(const DsgnFormT *form, char *buf, int32_t bufSize, i
|
|||
continue;
|
||||
}
|
||||
|
||||
if (p->type == WGT_IFACE_ENUM && p->enumNames) {
|
||||
int32_t v = ((int32_t (*)(const WidgetT *))p->getFn)(ctrl->widget);
|
||||
const char *name = NULL;
|
||||
// Skip STRING props here: saveControls only emits
|
||||
// the iface-known scalar types. String values come
|
||||
// through ctrl->props[] (custom props).
|
||||
if (p->type == WGT_IFACE_STRING) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (int32_t en = 0; p->enumNames[en]; en++) {
|
||||
if (en == v) {
|
||||
name = p->enumNames[en];
|
||||
break;
|
||||
}
|
||||
}
|
||||
char valBuf[DSGN_MAX_TEXT];
|
||||
|
||||
if (name) {
|
||||
pos += snprintf(buf + pos, bufSize - pos, "%s %s = %s\n", pad, 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, bufSize - pos, "%s %s = %d\n", pad, p->name, (int)v);
|
||||
} else if (p->type == WGT_IFACE_BOOL) {
|
||||
bool v = ((bool (*)(const WidgetT *))p->getFn)(ctrl->widget);
|
||||
pos += snprintf(buf + pos, bufSize - pos, "%s %s = %s\n", pad, p->name, v ? "True" : "False");
|
||||
if (wgtPropValueToString(ctrl->widget, p, valBuf, sizeof(valBuf))) {
|
||||
pos += snprintf(buf + pos, bufSize - pos, "%s %s = %s\n", pad, p->name, valBuf);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -69,6 +69,7 @@
|
|||
#include "../compiler/strip.h"
|
||||
#include "../runtime/serialize.h"
|
||||
#include "../formrt/formrt.h"
|
||||
#include "../formrt/frmParser.h"
|
||||
#include "dvxRes.h"
|
||||
#include "../../../../libs/kpunch/sql/dvxSql.h"
|
||||
#include "../runtime/vm.h"
|
||||
|
|
@ -1425,6 +1426,254 @@ static void compileAndRun(void) {
|
|||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// Compile-time CtrlName.Member validator
|
||||
// ============================================================
|
||||
//
|
||||
// The IDE (unlike bascomp) has widget DXEs loaded, so wgtGetIface
|
||||
// returns live interface metadata. Combined with a static scan of
|
||||
// the project's .frm files (control name -> widget type), we can
|
||||
// reject typos like GfxCanvas.Boggle or LblStatus.Caphtion at
|
||||
// compile time instead of letting them surface as runtime errors
|
||||
// at event-click time. Dynamically-created controls (via
|
||||
// CreateControl at runtime) aren't in the map; lookupCtrlType
|
||||
// returns NULL for them and validation is skipped.
|
||||
|
||||
typedef struct {
|
||||
char name[BAS_MAX_CTRL_NAME];
|
||||
char wgtType[BAS_MAX_CTRL_NAME]; // "Form" for the form itself, else iface basName
|
||||
} IdeCtrlMapEntryT;
|
||||
|
||||
|
||||
typedef struct {
|
||||
IdeCtrlMapEntryT *entries; // stb_ds dynamic array
|
||||
} IdeValidatorCtxT;
|
||||
|
||||
|
||||
static bool ideValidator_onFormBegin(void *ud, const char *name) {
|
||||
IdeValidatorCtxT *v = (IdeValidatorCtxT *)ud;
|
||||
IdeCtrlMapEntryT e;
|
||||
memset(&e, 0, sizeof(e));
|
||||
snprintf(e.name, BAS_MAX_CTRL_NAME, "%s", name);
|
||||
snprintf(e.wgtType, BAS_MAX_CTRL_NAME, "%s", "Form");
|
||||
arrput(v->entries, e);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
static void ideValidator_onCtrlBegin(void *ud, const char *typeName, const char *name) {
|
||||
IdeValidatorCtxT *v = (IdeValidatorCtxT *)ud;
|
||||
IdeCtrlMapEntryT e;
|
||||
memset(&e, 0, sizeof(e));
|
||||
snprintf(e.name, BAS_MAX_CTRL_NAME, "%s", name);
|
||||
snprintf(e.wgtType, BAS_MAX_CTRL_NAME, "%s", typeName ? typeName : "");
|
||||
arrput(v->entries, e);
|
||||
}
|
||||
|
||||
|
||||
static void ideValidator_onMenuBegin(void *ud, const char *name, int32_t level) {
|
||||
(void)level;
|
||||
IdeValidatorCtxT *v = (IdeValidatorCtxT *)ud;
|
||||
IdeCtrlMapEntryT e;
|
||||
memset(&e, 0, sizeof(e));
|
||||
snprintf(e.name, BAS_MAX_CTRL_NAME, "%s", name);
|
||||
snprintf(e.wgtType, BAS_MAX_CTRL_NAME, "%s", "Menu");
|
||||
arrput(v->entries, e);
|
||||
}
|
||||
|
||||
|
||||
static const char *ideValidator_lookupCtrlType(void *ctx, const char *ctrlName) {
|
||||
IdeValidatorCtxT *v = (IdeValidatorCtxT *)ctx;
|
||||
|
||||
if (!v || !ctrlName) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
for (int32_t i = 0; i < (int32_t)arrlen(v->entries); i++) {
|
||||
if (strcasecmp(v->entries[i].name, ctrlName) == 0) {
|
||||
return v->entries[i].wgtType;
|
||||
}
|
||||
}
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
||||
|
||||
// Methods that exist on every widget via callCommonMethod() in formrt.c.
|
||||
static bool ideValidator_isCommonMethod(const char *methodName) {
|
||||
return strcasecmp(methodName, "SetFocus") == 0 ||
|
||||
strcasecmp(methodName, "Refresh") == 0 ||
|
||||
strcasecmp(methodName, "SetReadOnly") == 0 ||
|
||||
strcasecmp(methodName, "SetEnabled") == 0 ||
|
||||
strcasecmp(methodName, "SetVisible") == 0;
|
||||
}
|
||||
|
||||
|
||||
// Properties that setProp/getProp accept on every non-form, non-menu
|
||||
// widget (common props + widget text/data/help aliases).
|
||||
static bool ideValidator_isCommonProp(const char *propName) {
|
||||
return strcasecmp(propName, "Name") == 0 ||
|
||||
strcasecmp(propName, "Left") == 0 ||
|
||||
strcasecmp(propName, "Top") == 0 ||
|
||||
strcasecmp(propName, "Width") == 0 ||
|
||||
strcasecmp(propName, "Height") == 0 ||
|
||||
strcasecmp(propName, "MinWidth") == 0 ||
|
||||
strcasecmp(propName, "MinHeight") == 0 ||
|
||||
strcasecmp(propName, "MaxWidth") == 0 ||
|
||||
strcasecmp(propName, "MaxHeight") == 0 ||
|
||||
strcasecmp(propName, "Weight") == 0 ||
|
||||
strcasecmp(propName, "Visible") == 0 ||
|
||||
strcasecmp(propName, "Enabled") == 0 ||
|
||||
strcasecmp(propName, "Caption") == 0 ||
|
||||
strcasecmp(propName, "Text") == 0 ||
|
||||
strcasecmp(propName, "HelpTopic") == 0 ||
|
||||
strcasecmp(propName, "DataSource") == 0 ||
|
||||
strcasecmp(propName, "DataField") == 0 ||
|
||||
strcasecmp(propName, "ListCount") == 0;
|
||||
}
|
||||
|
||||
|
||||
static bool ideValidator_isMethodValid(void *ctx, const char *wgtType, const char *methodName) {
|
||||
(void)ctx;
|
||||
|
||||
if (!wgtType || !methodName) {
|
||||
return true; // be permissive on malformed input
|
||||
}
|
||||
|
||||
if (ideValidator_isCommonMethod(methodName)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Form-level methods
|
||||
if (strcasecmp(wgtType, "Form") == 0) {
|
||||
return strcasecmp(methodName, "Show") == 0 ||
|
||||
strcasecmp(methodName, "Hide") == 0;
|
||||
}
|
||||
|
||||
// Menu items have no methods beyond common
|
||||
if (strcasecmp(wgtType, "Menu") == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Widget-specific methods from the live iface
|
||||
const char *wgtName = wgtFindByBasName(wgtType);
|
||||
|
||||
if (!wgtName) {
|
||||
return true; // unknown type -- skip validation
|
||||
}
|
||||
|
||||
const WgtIfaceT *iface = wgtGetIface(wgtName);
|
||||
|
||||
if (!iface || !iface->methods) {
|
||||
return true;
|
||||
}
|
||||
|
||||
for (int32_t i = 0; i < iface->methodCount; i++) {
|
||||
if (strcasecmp(iface->methods[i].name, methodName) == 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
static bool ideValidator_isPropValid(void *ctx, const char *wgtType, const char *propName) {
|
||||
(void)ctx;
|
||||
|
||||
if (!wgtType || !propName) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Form-level properties
|
||||
if (strcasecmp(wgtType, "Form") == 0) {
|
||||
return strcasecmp(propName, "Name") == 0 ||
|
||||
strcasecmp(propName, "Caption") == 0 ||
|
||||
strcasecmp(propName, "Width") == 0 ||
|
||||
strcasecmp(propName, "Height") == 0 ||
|
||||
strcasecmp(propName, "Left") == 0 ||
|
||||
strcasecmp(propName, "Top") == 0 ||
|
||||
strcasecmp(propName, "Visible") == 0 ||
|
||||
strcasecmp(propName, "Resizable") == 0 ||
|
||||
strcasecmp(propName, "AutoSize") == 0 ||
|
||||
strcasecmp(propName, "Centered") == 0 ||
|
||||
strcasecmp(propName, "Layout") == 0;
|
||||
}
|
||||
|
||||
// Menu items
|
||||
if (strcasecmp(wgtType, "Menu") == 0) {
|
||||
return strcasecmp(propName, "Name") == 0 ||
|
||||
strcasecmp(propName, "Checked") == 0 ||
|
||||
strcasecmp(propName, "Enabled") == 0 ||
|
||||
strcasecmp(propName, "Caption") == 0;
|
||||
}
|
||||
|
||||
if (ideValidator_isCommonProp(propName)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Widget-specific props from the live iface
|
||||
const char *wgtName = wgtFindByBasName(wgtType);
|
||||
|
||||
if (!wgtName) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const WgtIfaceT *iface = wgtGetIface(wgtName);
|
||||
|
||||
if (!iface || !iface->props) {
|
||||
return true;
|
||||
}
|
||||
|
||||
for (int32_t i = 0; i < iface->propCount; i++) {
|
||||
if (strcasecmp(iface->props[i].name, propName) == 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
// Walk every .frm in the project and populate a (name -> wgtType) map
|
||||
// the parser can consult. Caller must arrfree(ctx->entries) when done.
|
||||
static void ideBuildCtrlMap(IdeValidatorCtxT *ctx) {
|
||||
ctx->entries = NULL;
|
||||
|
||||
FrmParserCbsT cbs;
|
||||
memset(&cbs, 0, sizeof(cbs));
|
||||
cbs.userData = ctx;
|
||||
cbs.onFormBegin = ideValidator_onFormBegin;
|
||||
cbs.onCtrlBegin = ideValidator_onCtrlBegin;
|
||||
cbs.onMenuBegin = ideValidator_onMenuBegin;
|
||||
|
||||
for (int32_t i = 0; i < sProject.fileCount; i++) {
|
||||
if (!sProject.files[i].isForm) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Use the buffered source if the file is open in an editor,
|
||||
// otherwise load from disk. Mirrors how the designer loads.
|
||||
char *diskBuf = NULL;
|
||||
const char *src = sProject.files[i].buffer;
|
||||
int32_t len = src ? (int32_t)strlen(src) : 0;
|
||||
|
||||
if (!src) {
|
||||
int32_t dlen = 0;
|
||||
diskBuf = platformReadFile(sProject.files[i].path, &dlen);
|
||||
src = diskBuf;
|
||||
len = dlen;
|
||||
}
|
||||
|
||||
if (src && len > 0) {
|
||||
frmParse(src, len, &cbs);
|
||||
}
|
||||
|
||||
free(diskBuf);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
static bool compileProject(void) {
|
||||
// Save all dirty files before compiling if Save on Run is enabled
|
||||
if (sWin && sWin->menuBar && wmMenuItemIsChecked(sWin->menuBar, CMD_SAVE_ON_RUN)) {
|
||||
|
|
@ -1647,7 +1896,22 @@ static bool compileProject(void) {
|
|||
}
|
||||
|
||||
basParserInit(parser, src, srcLen);
|
||||
parser->optionExplicit = prefsGetBool(sPrefs, "editor", "optionExplicit", false);
|
||||
parser->optionExplicit = sProject.optionExplicit;
|
||||
|
||||
// Build a name -> widget-type map from the project's .frm files
|
||||
// and attach a validator so the parser can reject CtrlName.Member
|
||||
// typos at compile time. The validator falls back silently for
|
||||
// dynamically-created controls (not in the map).
|
||||
IdeValidatorCtxT validatorCtx;
|
||||
memset(&validatorCtx, 0, sizeof(validatorCtx));
|
||||
ideBuildCtrlMap(&validatorCtx);
|
||||
|
||||
BasCtrlValidatorT validator;
|
||||
validator.lookupCtrlType = ideValidator_lookupCtrlType;
|
||||
validator.isMethodValid = ideValidator_isMethodValid;
|
||||
validator.isPropValid = ideValidator_isPropValid;
|
||||
validator.ctx = &validatorCtx;
|
||||
basParserSetValidator(parser, &validator);
|
||||
|
||||
if (!basParse(parser)) {
|
||||
// Translate global error line to local file/line for display
|
||||
|
|
@ -1715,10 +1979,12 @@ static bool compileProject(void) {
|
|||
basParserFree(parser);
|
||||
free(parser);
|
||||
free(concatBuf);
|
||||
arrfree(validatorCtx.entries);
|
||||
return false;
|
||||
}
|
||||
|
||||
free(concatBuf);
|
||||
arrfree(validatorCtx.entries);
|
||||
|
||||
BasModuleT *mod = basParserBuildModule(parser);
|
||||
basParserFree(parser);
|
||||
|
|
@ -2062,19 +2328,15 @@ static void dsgnCopySelected(void) {
|
|||
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;
|
||||
// Skip STRING props -- custom props handle those.
|
||||
if (p->type == WGT_IFACE_STRING) {
|
||||
continue;
|
||||
}
|
||||
|
||||
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");
|
||||
char valBuf[DSGN_MAX_TEXT];
|
||||
|
||||
if (wgtPropValueToString(ctrl->widget, p, valBuf, sizeof(valBuf))) {
|
||||
pos += snprintf(buf + pos, sizeof(buf) - pos, " %s = %s\n", p->name, valBuf);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -2305,23 +2567,8 @@ static void dsgnPasteControl(void) {
|
|||
}
|
||||
}
|
||||
|
||||
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);
|
||||
if (val) {
|
||||
wgtApplyPropFromString(ctrl.widget, p, val);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -8077,7 +8324,7 @@ static void showPreferencesDialog(void) {
|
|||
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)");
|
||||
sPrefsDlg.optionExplicit = wgtCheckbox(edFrame, "OPTION EXPLICIT default for new projects");
|
||||
wgtCheckboxSetChecked(sPrefsDlg.optionExplicit, prefsGetBool(sPrefs, "editor", "optionExplicit", false));
|
||||
|
||||
WidgetT *tabRow = wgtHBox(edFrame);
|
||||
|
|
|
|||
|
|
@ -51,6 +51,7 @@
|
|||
#include "dvxWm.h"
|
||||
#include "box/box.h"
|
||||
#include "button/button.h"
|
||||
#include "checkbox/checkbox.h"
|
||||
#include "dropdown/dropdown.h"
|
||||
#include "image/image.h"
|
||||
#include "label/label.h"
|
||||
|
|
@ -98,6 +99,7 @@ static struct {
|
|||
WidgetT *copyright;
|
||||
WidgetT *description;
|
||||
WidgetT *startupForm;
|
||||
WidgetT *optionExplicit;
|
||||
const char **formNames; // stb_ds array of form name strings for startup dropdown
|
||||
WidgetT *helpFileInput;
|
||||
WidgetT *iconPreview;
|
||||
|
|
@ -522,6 +524,8 @@ bool prjLoad(PrjStateT *prj, const char *dbpPath) {
|
|||
val = prefsGetString(h, BAS_INI_SECTION_SETTINGS, BAS_INI_KEY_STARTUPFORM, NULL);
|
||||
if (val) { snprintf(prj->startupForm, sizeof(prj->startupForm), "%s", val); }
|
||||
|
||||
prj->optionExplicit = prefsGetBool(h, BAS_INI_SECTION_SETTINGS, BAS_INI_KEY_OPTIONEXPLICIT, false);
|
||||
|
||||
prefsClose(h);
|
||||
prj->dirty = false;
|
||||
return true;
|
||||
|
|
@ -608,6 +612,7 @@ void prjNew(PrjStateT *prj, const char *name, const char *directory, PrefsHandle
|
|||
snprintf(prj->version, sizeof(prj->version), "%s", prefsGetString(prefs, "defaults", "version", "1.0"));
|
||||
snprintf(prj->copyright, sizeof(prj->copyright), "%s", prefsGetString(prefs, "defaults", "copyright", ""));
|
||||
snprintf(prj->description, sizeof(prj->description), "%s", prefsGetString(prefs, "defaults", "description", ""));
|
||||
prj->optionExplicit = prefsGetBool(prefs, "editor", "optionExplicit", false);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -617,7 +622,7 @@ bool prjPropertiesDialog(AppContextT *ctx, PrjStateT *prj, const char *appPath)
|
|||
return false;
|
||||
}
|
||||
|
||||
WindowT *win = dvxCreateWindowCentered(ctx, "Project Properties", PPD_WIDTH, 380, false);
|
||||
WindowT *win = dvxCreateWindowCentered(ctx, "Project Properties", PPD_WIDTH, 410, false);
|
||||
|
||||
if (!win) {
|
||||
return false;
|
||||
|
|
@ -739,6 +744,10 @@ bool prjPropertiesDialog(AppContextT *ctx, PrjStateT *prj, const char *appPath)
|
|||
hlpBrowse->onClick = ppdOnBrowseHelp;
|
||||
}
|
||||
|
||||
// Compiler options: OPTION EXPLICIT enforces DIM-before-use for this project.
|
||||
sPpd.optionExplicit = wgtCheckbox(root, "Require variable declaration (OPTION EXPLICIT)");
|
||||
wgtCheckboxSetChecked(sPpd.optionExplicit, prj->optionExplicit);
|
||||
|
||||
// Description: label above, textarea below (matches Preferences layout)
|
||||
wgtLabel(root, "Description:");
|
||||
sPpd.description = wgtTextArea(root, PRJ_MAX_DESC);
|
||||
|
|
@ -804,6 +813,7 @@ bool prjPropertiesDialog(AppContextT *ctx, PrjStateT *prj, const char *appPath)
|
|||
}
|
||||
}
|
||||
|
||||
prj->optionExplicit = wgtCheckboxIsChecked(sPpd.optionExplicit);
|
||||
prj->dirty = true;
|
||||
}
|
||||
|
||||
|
|
@ -948,6 +958,7 @@ bool prjSave(const PrjStateT *prj) {
|
|||
|
||||
// [Settings] section
|
||||
prefsSetString(h, BAS_INI_SECTION_SETTINGS, BAS_INI_KEY_STARTUPFORM, prj->startupForm);
|
||||
prefsSetBool(h, BAS_INI_SECTION_SETTINGS, BAS_INI_KEY_OPTIONEXPLICIT, prj->optionExplicit);
|
||||
|
||||
bool ok = prefsSaveAs(h, prj->projectPath);
|
||||
prefsClose(h);
|
||||
|
|
|
|||
|
|
@ -79,6 +79,7 @@ typedef struct {
|
|||
char description[PRJ_MAX_DESC];
|
||||
char iconPath[DVX_MAX_PATH]; // relative path to icon BMP
|
||||
char helpFile[DVX_MAX_PATH]; // relative path to .hlp file
|
||||
bool optionExplicit; // require DIM before use
|
||||
PrjFileT *files; // stb_ds dynamic array
|
||||
int32_t fileCount;
|
||||
PrjSourceMapT *sourceMap; // stb_ds dynamic array
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@
|
|||
// property value to edit it via an InputBox dialog.
|
||||
|
||||
#include "ideProperties.h"
|
||||
#include "../formrt/frmParser.h"
|
||||
#include "dvxDlg.h"
|
||||
#include "dvxWm.h"
|
||||
#include "box/box.h"
|
||||
|
|
@ -616,7 +617,7 @@ static void onPropDblClick(WidgetT *w) {
|
|||
|
||||
if (propType == PROP_TYPE_BOOL) {
|
||||
// Toggle boolean on double-click -- no input box
|
||||
bool cur = (strcasecmp(curValue, "True") == 0);
|
||||
bool cur = frmParseBool(curValue);
|
||||
snprintf(newValue, sizeof(newValue), "%s", cur ? "False" : "True");
|
||||
} else if (propType == PROP_TYPE_ENUM) {
|
||||
// Enum: cycle to next value on double-click
|
||||
|
|
@ -923,7 +924,7 @@ static void onPropDblClick(WidgetT *w) {
|
|||
ctrl->widget->weight = ctrl->weight;
|
||||
}
|
||||
} else if (strcasecmp(propName, "Visible") == 0) {
|
||||
bool val = (strcasecmp(newValue, "True") == 0);
|
||||
bool val = frmParseBool(newValue);
|
||||
|
||||
if (ctrl->widget) {
|
||||
wgtSetVisible(ctrl->widget, val);
|
||||
|
|
@ -934,7 +935,7 @@ static void onPropDblClick(WidgetT *w) {
|
|||
cascadeToChildren(sDs, ctrl->name, val, en);
|
||||
}
|
||||
} else if (strcasecmp(propName, "Enabled") == 0) {
|
||||
bool val = (strcasecmp(newValue, "True") == 0);
|
||||
bool val = frmParseBool(newValue);
|
||||
|
||||
if (ctrl->widget) {
|
||||
wgtSetEnabled(ctrl->widget, val);
|
||||
|
|
@ -965,7 +966,9 @@ static void onPropDblClick(WidgetT *w) {
|
|||
}
|
||||
|
||||
if (p->type == WGT_IFACE_STRING) {
|
||||
// Store in props for persistence, set from there
|
||||
// Strings must outlive this function, so the
|
||||
// ctrl->props[] copy is what we pass to setFn
|
||||
// (not the newValue buffer).
|
||||
bool found = false;
|
||||
|
||||
for (int32_t j = 0; j < ctrl->propCount; j++) {
|
||||
|
|
@ -983,21 +986,8 @@ static void onPropDblClick(WidgetT *w) {
|
|||
((void (*)(WidgetT *, const char *))p->setFn)(ctrl->widget, ctrl->props[ctrl->propCount].value);
|
||||
ctrl->propCount++;
|
||||
}
|
||||
} else if (p->type == WGT_IFACE_ENUM && p->enumNames) {
|
||||
int32_t enumVal = 0;
|
||||
|
||||
for (int32_t en = 0; p->enumNames[en]; en++) {
|
||||
if (strcasecmp(p->enumNames[en], newValue) == 0) {
|
||||
enumVal = en;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
((void (*)(WidgetT *, int32_t))p->setFn)(ctrl->widget, enumVal);
|
||||
} else if (p->type == WGT_IFACE_INT) {
|
||||
((void (*)(WidgetT *, int32_t))p->setFn)(ctrl->widget, atoi(newValue));
|
||||
} else if (p->type == WGT_IFACE_BOOL) {
|
||||
((void (*)(WidgetT *, bool))p->setFn)(ctrl->widget, strcasecmp(newValue, "True") == 0);
|
||||
} else {
|
||||
wgtApplyPropFromString(ctrl->widget, p, newValue);
|
||||
}
|
||||
|
||||
ifaceHandled = true;
|
||||
|
|
@ -1068,7 +1058,7 @@ static void onPropDblClick(WidgetT *w) {
|
|||
dvxSetTitle(sPrpCtx, sDs->formWin, winTitle);
|
||||
}
|
||||
} else if (strcasecmp(propName, "AutoSize") == 0) {
|
||||
sDs->form->autoSize = (strcasecmp(newValue, "True") == 0);
|
||||
sDs->form->autoSize = frmParseBool(newValue);
|
||||
|
||||
if (sDs->form->autoSize && sDs->formWin) {
|
||||
dvxFitWindow(sPrpCtx, sDs->formWin);
|
||||
|
|
@ -1076,14 +1066,14 @@ static void onPropDblClick(WidgetT *w) {
|
|||
sDs->form->height = sDs->formWin->h;
|
||||
}
|
||||
} else if (strcasecmp(propName, "Resizable") == 0) {
|
||||
sDs->form->resizable = (strcasecmp(newValue, "True") == 0);
|
||||
sDs->form->resizable = frmParseBool(newValue);
|
||||
|
||||
if (sDs->formWin) {
|
||||
sDs->formWin->resizable = sDs->form->resizable;
|
||||
dvxInvalidateWindow(sPrpCtx, sDs->formWin);
|
||||
}
|
||||
} else if (strcasecmp(propName, "Centered") == 0) {
|
||||
sDs->form->centered = (strcasecmp(newValue, "True") == 0);
|
||||
sDs->form->centered = frmParseBool(newValue);
|
||||
} else if (strcasecmp(propName, "Left") == 0) {
|
||||
sDs->form->left = atoi(newValue);
|
||||
} else if (strcasecmp(propName, "Top") == 0) {
|
||||
|
|
@ -1473,29 +1463,14 @@ void prpRefresh(DsgnStateT *ds) {
|
|||
continue;
|
||||
}
|
||||
|
||||
// Read the current value from the widget
|
||||
if (p->type == WGT_IFACE_STRING && p->getFn) {
|
||||
const char *s = ((const char *(*)(WidgetT *))p->getFn)(ctrl->widget);
|
||||
addPropRow(p->name, s ? s : "");
|
||||
} else if (p->type == WGT_IFACE_ENUM && p->getFn && p->enumNames) {
|
||||
int32_t v = ((int32_t (*)(const WidgetT *))p->getFn)(ctrl->widget);
|
||||
const char *name = NULL;
|
||||
// Read the current value from the widget. Enum values
|
||||
// that don't map to a name are shown as "?".
|
||||
char valBuf[DSGN_MAX_TEXT];
|
||||
|
||||
for (int32_t k = 0; p->enumNames[k]; k++) {
|
||||
if (k == v) {
|
||||
name = p->enumNames[k];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
addPropRow(p->name, name ? name : "?");
|
||||
} else if (p->type == WGT_IFACE_INT && p->getFn) {
|
||||
int32_t v = ((int32_t (*)(const WidgetT *))p->getFn)(ctrl->widget);
|
||||
snprintf(buf, sizeof(buf), "%d", (int)v);
|
||||
addPropRow(p->name, buf);
|
||||
} else if (p->type == WGT_IFACE_BOOL && p->getFn) {
|
||||
bool v = ((bool (*)(const WidgetT *))p->getFn)(ctrl->widget);
|
||||
addPropRow(p->name, v ? "True" : "False");
|
||||
if (wgtPropValueToString(ctrl->widget, p, valBuf, sizeof(valBuf))) {
|
||||
addPropRow(p->name, valBuf);
|
||||
} else if (p->type == WGT_IFACE_ENUM) {
|
||||
addPropRow(p->name, "?");
|
||||
} else {
|
||||
addPropRow(p->name, "");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -274,6 +274,10 @@ BasModuleT *basModuleDeserialize(const uint8_t *data, int32_t dataLen) {
|
|||
char *name = rStr(&r);
|
||||
snprintf(p->name, BAS_MAX_PROC_NAME, "%s", name);
|
||||
free(name);
|
||||
|
||||
char *formName = rStr(&r);
|
||||
snprintf(p->formName, BAS_MAX_PROC_NAME, "%s", formName);
|
||||
free(formName);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -294,6 +298,19 @@ BasModuleT *basModuleDeserialize(const uint8_t *data, int32_t dataLen) {
|
|||
}
|
||||
}
|
||||
|
||||
// Global-slot runtime type init table (survives strip). STRING
|
||||
// globals need this so first-use string ops work correctly.
|
||||
mod->globalInitCount = rI32(&r);
|
||||
|
||||
if (mod->globalInitCount > 0) {
|
||||
mod->globalInits = (BasGlobalInitT *)calloc(mod->globalInitCount, sizeof(BasGlobalInitT));
|
||||
|
||||
for (int32_t i = 0; i < mod->globalInitCount; i++) {
|
||||
mod->globalInits[i].index = rI32(&r);
|
||||
mod->globalInits[i].dataType = rU8(&r);
|
||||
}
|
||||
}
|
||||
|
||||
return mod;
|
||||
}
|
||||
|
||||
|
|
@ -323,6 +340,7 @@ void basModuleFree(BasModuleT *mod) {
|
|||
|
||||
free(mod->procs);
|
||||
free(mod->formVarInfo);
|
||||
free(mod->globalInits);
|
||||
free(mod->debugVars);
|
||||
|
||||
if (mod->debugUdtDefs) {
|
||||
|
|
@ -411,6 +429,7 @@ uint8_t *basModuleSerialize(const BasModuleT *mod, int32_t *outLen) {
|
|||
bufWriteU8(&b, p->returnType);
|
||||
bufWriteU8(&b, p->isFunction ? 1 : 0);
|
||||
bufWriteStr(&b, p->name);
|
||||
bufWriteStr(&b, p->formName);
|
||||
}
|
||||
|
||||
// Form variable info (runtime-essential for per-form variable allocation)
|
||||
|
|
@ -424,6 +443,14 @@ uint8_t *basModuleSerialize(const BasModuleT *mod, int32_t *outLen) {
|
|||
bufWriteI32(&b, fv->initCodeLen);
|
||||
}
|
||||
|
||||
// Global init table (runtime-essential for STRING default slot type)
|
||||
bufWriteI32(&b, mod->globalInitCount);
|
||||
|
||||
for (int32_t i = 0; i < mod->globalInitCount; i++) {
|
||||
bufWriteI32(&b, mod->globalInits[i].index);
|
||||
bufWriteU8(&b, mod->globalInits[i].dataType);
|
||||
}
|
||||
|
||||
*outLen = b.len;
|
||||
return b.buf;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -68,11 +68,19 @@ void basUdtFree(BasUdtT *udt);
|
|||
BasUdtT *basUdtNew(int32_t typeId, int32_t fieldCount);
|
||||
BasUdtT *basUdtRef(BasUdtT *udt);
|
||||
void basUdtUnref(BasUdtT *udt);
|
||||
BasValueT basValBool(bool v);
|
||||
int32_t basValCompare(BasValueT a, BasValueT b);
|
||||
int32_t basValCompareCI(BasValueT a, BasValueT b);
|
||||
BasValueT basValCopy(BasValueT v);
|
||||
BasValueT basValDouble(double v);
|
||||
BasStringT *basValFormatString(BasValueT v);
|
||||
BasValueT basValInteger(int16_t v);
|
||||
bool basValIsTruthy(BasValueT v);
|
||||
BasValueT basValLong(int32_t v);
|
||||
BasValueT basValObject(void *obj);
|
||||
uint8_t basValPromoteType(uint8_t a, uint8_t b);
|
||||
void basValRelease(BasValueT *v);
|
||||
BasValueT basValSingle(float v);
|
||||
BasValueT basValString(BasStringT *s);
|
||||
BasValueT basValStringFromC(const char *text);
|
||||
BasValueT basValToBool(BasValueT v);
|
||||
|
|
@ -399,8 +407,16 @@ void basUdtUnref(BasUdtT *udt) {
|
|||
|
||||
|
||||
// ============================================================
|
||||
// Value constructors (trivial ones moved to values.h as static inline)
|
||||
// Value constructors / refcount helpers
|
||||
// ============================================================
|
||||
BasValueT basValBool(bool v) {
|
||||
BasValueT val;
|
||||
val.type = BAS_TYPE_BOOLEAN;
|
||||
val.boolVal = v ? -1 : 0;
|
||||
return val;
|
||||
}
|
||||
|
||||
|
||||
int32_t basValCompare(BasValueT a, BasValueT b) {
|
||||
// String comparison
|
||||
if (a.type == BAS_TYPE_STRING && b.type == BAS_TYPE_STRING) {
|
||||
|
|
@ -445,6 +461,27 @@ int32_t basValCompareCI(BasValueT a, BasValueT b) {
|
|||
}
|
||||
|
||||
|
||||
BasValueT basValCopy(BasValueT v) {
|
||||
if (v.type == BAS_TYPE_STRING && v.strVal) {
|
||||
basStringRef(v.strVal);
|
||||
} else if (v.type == BAS_TYPE_ARRAY && v.arrVal) {
|
||||
basArrayRef(v.arrVal);
|
||||
} else if (v.type == BAS_TYPE_UDT && v.udtVal) {
|
||||
basUdtRef(v.udtVal);
|
||||
}
|
||||
|
||||
return v;
|
||||
}
|
||||
|
||||
|
||||
BasValueT basValDouble(double v) {
|
||||
BasValueT val;
|
||||
val.type = BAS_TYPE_DOUBLE;
|
||||
val.dblVal = v;
|
||||
return val;
|
||||
}
|
||||
|
||||
|
||||
BasStringT *basValFormatString(BasValueT v) {
|
||||
char buf[64];
|
||||
|
||||
|
|
@ -478,6 +515,14 @@ BasStringT *basValFormatString(BasValueT v) {
|
|||
}
|
||||
|
||||
|
||||
BasValueT basValInteger(int16_t v) {
|
||||
BasValueT val;
|
||||
val.type = BAS_TYPE_INTEGER;
|
||||
val.intVal = v;
|
||||
return val;
|
||||
}
|
||||
|
||||
|
||||
bool basValIsTruthy(BasValueT v) {
|
||||
switch (v.type) {
|
||||
case BAS_TYPE_INTEGER:
|
||||
|
|
@ -504,6 +549,22 @@ bool basValIsTruthy(BasValueT v) {
|
|||
}
|
||||
|
||||
|
||||
BasValueT basValLong(int32_t v) {
|
||||
BasValueT val;
|
||||
val.type = BAS_TYPE_LONG;
|
||||
val.longVal = v;
|
||||
return val;
|
||||
}
|
||||
|
||||
|
||||
BasValueT basValObject(void *obj) {
|
||||
BasValueT val;
|
||||
val.type = BAS_TYPE_OBJECT;
|
||||
val.objVal = obj;
|
||||
return val;
|
||||
}
|
||||
|
||||
|
||||
uint8_t basValPromoteType(uint8_t a, uint8_t b) {
|
||||
// String stays string (concat, not arithmetic)
|
||||
if (a == BAS_TYPE_STRING || b == BAS_TYPE_STRING) {
|
||||
|
|
@ -529,6 +590,28 @@ uint8_t basValPromoteType(uint8_t a, uint8_t b) {
|
|||
}
|
||||
|
||||
|
||||
void basValRelease(BasValueT *v) {
|
||||
if (v->type == BAS_TYPE_STRING) {
|
||||
basStringUnref(v->strVal);
|
||||
v->strVal = NULL;
|
||||
} else if (v->type == BAS_TYPE_ARRAY) {
|
||||
basArrayUnref(v->arrVal);
|
||||
v->arrVal = NULL;
|
||||
} else if (v->type == BAS_TYPE_UDT) {
|
||||
basUdtUnref(v->udtVal);
|
||||
v->udtVal = NULL;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
BasValueT basValSingle(float v) {
|
||||
BasValueT val;
|
||||
val.type = BAS_TYPE_SINGLE;
|
||||
val.sngVal = v;
|
||||
return val;
|
||||
}
|
||||
|
||||
|
||||
BasValueT basValString(BasStringT *s) {
|
||||
BasValueT val;
|
||||
val.type = BAS_TYPE_STRING;
|
||||
|
|
|
|||
|
|
@ -31,8 +31,6 @@
|
|||
#ifndef DVXBASIC_VALUES_H
|
||||
#define DVXBASIC_VALUES_H
|
||||
|
||||
#include "../compiler/opcodes.h" // BAS_TYPE_*
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
|
|
@ -159,89 +157,22 @@ struct BasValueTag {
|
|||
};
|
||||
};
|
||||
|
||||
// Create values -- trivial constructors inlined so they're fast in vm.c's
|
||||
// hot path (PUSH_INT16, PUSH_TRUE, etc.).
|
||||
static inline BasValueT basValInteger(int16_t v) {
|
||||
BasValueT val;
|
||||
val.type = BAS_TYPE_INTEGER;
|
||||
val.intVal = v;
|
||||
return val;
|
||||
}
|
||||
|
||||
|
||||
static inline BasValueT basValLong(int32_t v) {
|
||||
BasValueT val;
|
||||
val.type = BAS_TYPE_LONG;
|
||||
val.longVal = v;
|
||||
return val;
|
||||
}
|
||||
|
||||
|
||||
static inline BasValueT basValSingle(float v) {
|
||||
BasValueT val;
|
||||
val.type = BAS_TYPE_SINGLE;
|
||||
val.sngVal = v;
|
||||
return val;
|
||||
}
|
||||
|
||||
|
||||
static inline BasValueT basValDouble(double v) {
|
||||
BasValueT val;
|
||||
val.type = BAS_TYPE_DOUBLE;
|
||||
val.dblVal = v;
|
||||
return val;
|
||||
}
|
||||
|
||||
|
||||
static inline BasValueT basValBool(bool v) {
|
||||
BasValueT val;
|
||||
val.type = BAS_TYPE_BOOLEAN;
|
||||
val.boolVal = v ? -1 : 0;
|
||||
return val;
|
||||
}
|
||||
|
||||
|
||||
static inline BasValueT basValObject(void *obj) {
|
||||
BasValueT val;
|
||||
val.type = BAS_TYPE_OBJECT;
|
||||
val.objVal = obj;
|
||||
return val;
|
||||
}
|
||||
|
||||
|
||||
// Create values. Out-of-line so they can be resolved via the DXE
|
||||
// dynamic-symbol table when basrt.lib is loaded.
|
||||
BasValueT basValInteger(int16_t v);
|
||||
BasValueT basValLong(int32_t v);
|
||||
BasValueT basValSingle(float v);
|
||||
BasValueT basValDouble(double v);
|
||||
BasValueT basValBool(bool v);
|
||||
BasValueT basValObject(void *obj);
|
||||
BasValueT basValString(BasStringT *s);
|
||||
BasValueT basValStringFromC(const char *text);
|
||||
|
||||
// Copy a value (increments string/array/udt refcount if applicable).
|
||||
// Inlined so the hot-path case (integer/float/bool) is a single struct
|
||||
// return -- no function call, no branch beyond the type test.
|
||||
static inline BasValueT basValCopy(BasValueT v) {
|
||||
if (v.type == BAS_TYPE_STRING && v.strVal) {
|
||||
basStringRef(v.strVal);
|
||||
} else if (v.type == BAS_TYPE_ARRAY && v.arrVal) {
|
||||
basArrayRef(v.arrVal);
|
||||
} else if (v.type == BAS_TYPE_UDT && v.udtVal) {
|
||||
basUdtRef(v.udtVal);
|
||||
}
|
||||
BasValueT basValCopy(BasValueT v);
|
||||
|
||||
return v;
|
||||
}
|
||||
|
||||
|
||||
// Release a value (decrements refcount if applicable). Integer/float/bool
|
||||
// types are a no-op -- the common case is an immediately-returning branch.
|
||||
static inline void basValRelease(BasValueT *v) {
|
||||
if (v->type == BAS_TYPE_STRING) {
|
||||
basStringUnref(v->strVal);
|
||||
v->strVal = NULL;
|
||||
} else if (v->type == BAS_TYPE_ARRAY) {
|
||||
basArrayUnref(v->arrVal);
|
||||
v->arrVal = NULL;
|
||||
} else if (v->type == BAS_TYPE_UDT) {
|
||||
basUdtUnref(v->udtVal);
|
||||
v->udtVal = NULL;
|
||||
}
|
||||
}
|
||||
// Release a value (decrements refcount if applicable).
|
||||
void basValRelease(BasValueT *v);
|
||||
|
||||
// Convert a value to a specific type. Returns the converted value.
|
||||
// The original is NOT released -- caller manages lifetime.
|
||||
|
|
|
|||
|
|
@ -97,8 +97,9 @@ bool basVmCallSub(BasVmT *vm, int32_t codeAddr) {
|
|||
// Push a call frame that returns to an invalid address (sentinel)
|
||||
// We detect completion when callDepth drops back to savedCallDepth
|
||||
BasCallFrameT *frame = &vm->callStack[vm->callDepth++];
|
||||
frame->returnPc = savedPc;
|
||||
frame->localCount = BAS_VM_MAX_LOCALS;
|
||||
frame->returnPc = savedPc;
|
||||
frame->localCount = BAS_VM_MAX_LOCALS;
|
||||
frame->errorHandler = 0;
|
||||
memset(frame->locals, 0, sizeof(frame->locals));
|
||||
|
||||
// Jump to the SUB
|
||||
|
|
@ -131,8 +132,9 @@ bool basVmCallSubWithArgs(BasVmT *vm, int32_t codeAddr, const BasValueT *args, i
|
|||
bool savedRunning = vm->running;
|
||||
|
||||
BasCallFrameT *frame = &vm->callStack[vm->callDepth++];
|
||||
frame->returnPc = savedPc;
|
||||
frame->localCount = BAS_VM_MAX_LOCALS;
|
||||
frame->returnPc = savedPc;
|
||||
frame->localCount = BAS_VM_MAX_LOCALS;
|
||||
frame->errorHandler = 0;
|
||||
memset(frame->locals, 0, sizeof(frame->locals));
|
||||
|
||||
// Set arguments as locals (parameter 0 = local 0, etc.)
|
||||
|
|
@ -169,8 +171,9 @@ bool basVmCallSubWithArgsOut(BasVmT *vm, int32_t codeAddr, const BasValueT *args
|
|||
bool savedRunning = vm->running;
|
||||
|
||||
BasCallFrameT *frame = &vm->callStack[vm->callDepth++];
|
||||
frame->returnPc = savedPc;
|
||||
frame->localCount = BAS_VM_MAX_LOCALS;
|
||||
frame->returnPc = savedPc;
|
||||
frame->localCount = BAS_VM_MAX_LOCALS;
|
||||
frame->errorHandler = 0;
|
||||
memset(frame->locals, 0, sizeof(frame->locals));
|
||||
|
||||
for (int32_t i = 0; i < argCount && i < BAS_VM_MAX_LOCALS; i++) {
|
||||
|
|
@ -269,6 +272,28 @@ const char *basVmGetError(const BasVmT *vm) {
|
|||
void basVmLoadModule(BasVmT *vm, BasModuleT *module) {
|
||||
vm->module = module;
|
||||
vm->pc = module->entryPoint;
|
||||
|
||||
// Initialize globals whose declared type requires a non-numeric
|
||||
// default. Uninitialized slots are zero (type=INTEGER, value=0),
|
||||
// which breaks STRING operations: `DIM s AS STRING : s = s + "x"`
|
||||
// would do numeric addition instead of concatenation. STATIC
|
||||
// variables also alias global slots, so the same issue applies.
|
||||
// globalInits survives the compiler's strip pass, unlike debugVars.
|
||||
if (module->globalInits) {
|
||||
for (int32_t i = 0; i < module->globalInitCount; i++) {
|
||||
BasGlobalInitT *g = &module->globalInits[i];
|
||||
|
||||
if (g->index < 0 || g->index >= BAS_VM_MAX_GLOBALS) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (g->dataType == BAS_TYPE_STRING) {
|
||||
basValRelease(&vm->globals[g->index]);
|
||||
vm->globals[g->index].type = BAS_TYPE_STRING;
|
||||
vm->globals[g->index].strVal = basStringNew("", 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -325,13 +350,40 @@ BasVmResultE basVmRun(BasVmT *vm) {
|
|||
|
||||
if (result != BAS_VM_OK) {
|
||||
// If an error handler is set and this is a trappable error,
|
||||
// jump to the handler instead of stopping execution
|
||||
if (vm->errorHandler != 0 && !vm->inErrorHandler && result != BAS_VM_HALTED && result != BAS_VM_BAD_OPCODE) {
|
||||
vm->errorPc = savedPc;
|
||||
vm->errorNextPc = vm->pc;
|
||||
vm->inErrorHandler = true;
|
||||
vm->pc = vm->errorHandler;
|
||||
continue;
|
||||
// unwind call frames until we find the SUB whose ON ERROR
|
||||
// GOTO registered a handler, then jump there. Without this
|
||||
// unwind an error raised inside a called SUB would fire the
|
||||
// outer SUB's handler but OP_RET would then pop the inner
|
||||
// frame and resume after the call site (effectively RESUME
|
||||
// NEXT semantics), which is not what ON ERROR promises.
|
||||
if (!vm->inErrorHandler && result != BAS_VM_HALTED && result != BAS_VM_BAD_OPCODE) {
|
||||
int32_t target = 0;
|
||||
|
||||
while (vm->callDepth > 0) {
|
||||
BasCallFrameT *frame = &vm->callStack[vm->callDepth - 1];
|
||||
|
||||
if (frame->errorHandler != 0) {
|
||||
target = frame->errorHandler;
|
||||
break;
|
||||
}
|
||||
|
||||
// No handler on this frame -- discard it and keep
|
||||
// unwinding. frame-local values need releasing.
|
||||
for (int32_t li = 0; li < frame->localCount; li++) {
|
||||
basValRelease(&frame->locals[li]);
|
||||
}
|
||||
|
||||
vm->callDepth--;
|
||||
}
|
||||
|
||||
if (target != 0) {
|
||||
vm->errorPc = savedPc;
|
||||
vm->errorNextPc = vm->pc;
|
||||
vm->inErrorHandler = true;
|
||||
vm->errorHandler = target;
|
||||
vm->pc = target;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
vm->running = false;
|
||||
|
|
@ -840,8 +892,9 @@ BasVmResultE basVmStep(BasVmT *vm) {
|
|||
}
|
||||
|
||||
BasCallFrameT *frame = &vm->callStack[vm->callDepth++];
|
||||
frame->returnPc = vm->pc;
|
||||
frame->localCount = BAS_VM_MAX_LOCALS;
|
||||
frame->returnPc = vm->pc;
|
||||
frame->localCount = BAS_VM_MAX_LOCALS;
|
||||
frame->errorHandler = 0;
|
||||
|
||||
// Zero all local slots
|
||||
memset(frame->locals, 0, sizeof(frame->locals));
|
||||
|
|
@ -853,6 +906,9 @@ BasVmResultE basVmStep(BasVmT *vm) {
|
|||
}
|
||||
}
|
||||
|
||||
// The callee starts with no handler; if it raises an error
|
||||
// the dispatcher will unwind to the nearest frame with one.
|
||||
vm->errorHandler = 0;
|
||||
vm->pc = addr;
|
||||
break;
|
||||
}
|
||||
|
|
@ -870,7 +926,24 @@ BasVmResultE basVmStep(BasVmT *vm) {
|
|||
basValRelease(&frame->locals[i]);
|
||||
}
|
||||
|
||||
bool hadHandler = (frame->errorHandler != 0);
|
||||
frame->errorHandler = 0;
|
||||
vm->pc = frame->returnPc;
|
||||
|
||||
// Restore the active handler to whatever the caller had set
|
||||
BasCallFrameT *caller = currentFrame(vm);
|
||||
vm->errorHandler = caller ? caller->errorHandler : 0;
|
||||
|
||||
// If this SUB owned the active error handler, any handler
|
||||
// body that ran inside it is now done -- clear the flag so
|
||||
// the next error (in this or another SUB) can trap again.
|
||||
// QBASIC documents this: an unresumed handler is cleared
|
||||
// when the procedure that installed it returns.
|
||||
if (hadHandler) {
|
||||
vm->inErrorHandler = false;
|
||||
vm->errorNumber = 0;
|
||||
vm->errorMsg[0] = '\0';
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
|
|
@ -894,8 +967,19 @@ BasVmResultE basVmStep(BasVmT *vm) {
|
|||
basValRelease(&frame->locals[i]);
|
||||
}
|
||||
|
||||
bool hadHandler = (frame->errorHandler != 0);
|
||||
frame->errorHandler = 0;
|
||||
vm->pc = frame->returnPc;
|
||||
|
||||
BasCallFrameT *caller = currentFrame(vm);
|
||||
vm->errorHandler = caller ? caller->errorHandler : 0;
|
||||
|
||||
if (hadHandler) {
|
||||
vm->inErrorHandler = false;
|
||||
vm->errorNumber = 0;
|
||||
vm->errorMsg[0] = '\0';
|
||||
}
|
||||
|
||||
if (!push(vm, retVal)) {
|
||||
basValRelease(&retVal);
|
||||
return BAS_VM_STACK_OVERFLOW;
|
||||
|
|
@ -918,8 +1002,9 @@ BasVmResultE basVmStep(BasVmT *vm) {
|
|||
}
|
||||
|
||||
case OP_FOR_INIT: {
|
||||
uint16_t varIdx = readUint16(vm);
|
||||
uint8_t scopeTag = readUint8(vm);
|
||||
uint16_t varIdx = readUint16(vm);
|
||||
uint8_t scopeTag = readUint8(vm);
|
||||
int16_t skipOffset = readInt16(vm);
|
||||
BasValueT stepVal;
|
||||
BasValueT limitVal;
|
||||
|
||||
|
|
@ -940,6 +1025,43 @@ BasVmResultE basVmStep(BasVmT *vm) {
|
|||
fs->limit = limitVal;
|
||||
fs->step = stepVal;
|
||||
fs->loopTop = vm->pc;
|
||||
|
||||
// Entry check: if the loop's range is already empty (e.g.
|
||||
// FOR i = 10 TO 5 with positive step), skip the body and
|
||||
// pop the FOR state. QBASIC semantics: body executes zero
|
||||
// times when the range is empty.
|
||||
BasValueT *varSlot = NULL;
|
||||
|
||||
if (scopeTag == SCOPE_LOCAL) {
|
||||
BasCallFrameT *frame = currentFrame(vm);
|
||||
|
||||
if (frame && varIdx < (uint16_t)frame->localCount) {
|
||||
varSlot = &frame->locals[varIdx];
|
||||
}
|
||||
} else if (scopeTag == SCOPE_FORM) {
|
||||
if (vm->currentFormVars && varIdx < (uint16_t)vm->currentFormVarCount) {
|
||||
varSlot = &vm->currentFormVars[varIdx];
|
||||
}
|
||||
} else {
|
||||
if (varIdx < BAS_VM_MAX_GLOBALS) {
|
||||
varSlot = &vm->globals[varIdx];
|
||||
}
|
||||
}
|
||||
|
||||
if (varSlot) {
|
||||
double curVal = basValToNumber(*varSlot);
|
||||
double stepNum = basValToNumber(fs->step);
|
||||
double limNum = basValToNumber(fs->limit);
|
||||
bool enter = (stepNum >= 0) ? (curVal <= limNum) : (curVal >= limNum);
|
||||
|
||||
if (!enter) {
|
||||
basValRelease(&fs->limit);
|
||||
basValRelease(&fs->step);
|
||||
vm->forDepth--;
|
||||
vm->pc += skipOffset;
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
|
|
@ -990,7 +1112,11 @@ BasVmResultE basVmStep(BasVmT *vm) {
|
|||
|
||||
// Increment: var = var + step. Preserve the loop variable's
|
||||
// original type so extern calls that pass it as an int32_t
|
||||
// argument don't get an 8-byte double instead.
|
||||
// argument don't get an 8-byte double instead. Exception:
|
||||
// if STEP is fractional, an integer slot would truncate the
|
||||
// increment and spin forever (FOR a = 0 TO 6.3 STEP 0.2 with
|
||||
// `a` stored as INTEGER from the literal 0). Promote to
|
||||
// DOUBLE in that case so the loop can actually progress.
|
||||
double varVal = basValToNumber(*varSlot);
|
||||
double stepVal = basValToNumber(fs->step);
|
||||
double limVal = basValToNumber(fs->limit);
|
||||
|
|
@ -999,7 +1125,11 @@ BasVmResultE basVmStep(BasVmT *vm) {
|
|||
|
||||
basValRelease(varSlot);
|
||||
|
||||
if (varType == BAS_TYPE_INTEGER) {
|
||||
bool stepIsFractional = (stepVal != floor(stepVal));
|
||||
|
||||
if (stepIsFractional) {
|
||||
*varSlot = basValDouble(varVal);
|
||||
} else if (varType == BAS_TYPE_INTEGER) {
|
||||
*varSlot = basValInteger((int16_t)varVal);
|
||||
} else if (varType == BAS_TYPE_LONG) {
|
||||
*varSlot = basValLong((int32_t)varVal);
|
||||
|
|
@ -1639,6 +1769,40 @@ BasVmResultE basVmStep(BasVmT *vm) {
|
|||
break;
|
||||
}
|
||||
|
||||
case OP_STR_OCT: {
|
||||
if (vm->sp < 1) {
|
||||
return BAS_VM_STACK_UNDERFLOW;
|
||||
}
|
||||
|
||||
BasValueT *top = &vm->stack[vm->sp - 1];
|
||||
int32_t n = (int32_t)basValToNumber(*top);
|
||||
char buf[16];
|
||||
snprintf(buf, sizeof(buf), "%o", (unsigned int)n);
|
||||
basValRelease(top);
|
||||
*top = basValStringFromC(buf);
|
||||
break;
|
||||
}
|
||||
|
||||
case OP_CONV_BOOL: {
|
||||
if (vm->sp < 1) {
|
||||
return BAS_VM_STACK_UNDERFLOW;
|
||||
}
|
||||
|
||||
BasValueT *top = &vm->stack[vm->sp - 1];
|
||||
bool truthy;
|
||||
|
||||
if (top->type == BAS_TYPE_STRING) {
|
||||
truthy = (top->strVal && top->strVal->len > 0);
|
||||
} else {
|
||||
truthy = (basValToNumber(*top) != 0.0);
|
||||
}
|
||||
|
||||
basValRelease(top);
|
||||
top->type = BAS_TYPE_BOOLEAN;
|
||||
top->boolVal = truthy ? -1 : 0;
|
||||
break;
|
||||
}
|
||||
|
||||
case OP_STR_STRING: {
|
||||
// STRING$(n, char)
|
||||
BasValueT charVal;
|
||||
|
|
@ -1827,7 +1991,19 @@ BasVmResultE basVmStep(BasVmT *vm) {
|
|||
|
||||
case OP_ON_ERROR: {
|
||||
int16_t handler = readInt16(vm);
|
||||
vm->errorHandler = (handler == 0) ? 0 : vm->pc + handler;
|
||||
int32_t target = (handler == 0) ? 0 : vm->pc + handler;
|
||||
|
||||
// Record the handler on the current call frame. The error
|
||||
// dispatcher walks frames (innermost outward) to find a
|
||||
// handler and unwinds the stack to that frame before
|
||||
// jumping. Module-level code lives in frame 0.
|
||||
BasCallFrameT *frame = currentFrame(vm);
|
||||
|
||||
if (frame) {
|
||||
frame->errorHandler = target;
|
||||
}
|
||||
|
||||
vm->errorHandler = target;
|
||||
break;
|
||||
}
|
||||
|
||||
|
|
@ -2032,6 +2208,58 @@ BasVmResultE basVmStep(BasVmT *vm) {
|
|||
break;
|
||||
}
|
||||
|
||||
case OP_PUSH_ARR_ADDR: {
|
||||
// Pass `arr(i)` as a BYREF parameter: push a BAS_TYPE_REF
|
||||
// pointing into the array's element storage. The array is
|
||||
// ref-counted, and the caller still holds a reference via
|
||||
// the local it came from, so the element memory is stable
|
||||
// for the duration of the call.
|
||||
uint8_t dims = readUint8(vm);
|
||||
int32_t indices[BAS_ARRAY_MAX_DIMS];
|
||||
|
||||
for (int32_t d = dims - 1; d >= 0; d--) {
|
||||
BasValueT idxVal;
|
||||
|
||||
if (!pop(vm, &idxVal)) {
|
||||
return BAS_VM_STACK_UNDERFLOW;
|
||||
}
|
||||
|
||||
indices[d] = (int32_t)basValToNumber(idxVal);
|
||||
basValRelease(&idxVal);
|
||||
}
|
||||
|
||||
BasValueT arrRef;
|
||||
|
||||
if (!pop(vm, &arrRef)) {
|
||||
return BAS_VM_STACK_UNDERFLOW;
|
||||
}
|
||||
|
||||
if (arrRef.type != BAS_TYPE_ARRAY || !arrRef.arrVal) {
|
||||
basValRelease(&arrRef);
|
||||
runtimeError(vm, 13, "Not an array");
|
||||
return BAS_VM_TYPE_MISMATCH;
|
||||
}
|
||||
|
||||
int32_t flatIdx = basArrayIndex(arrRef.arrVal, indices, dims);
|
||||
|
||||
if (flatIdx < 0) {
|
||||
basValRelease(&arrRef);
|
||||
runtimeError(vm, 9, "Subscript out of range");
|
||||
return BAS_VM_SUBSCRIPT_RANGE;
|
||||
}
|
||||
|
||||
BasValueT ref;
|
||||
ref.type = BAS_TYPE_REF;
|
||||
ref.refVal = &arrRef.arrVal->elements[flatIdx];
|
||||
basValRelease(&arrRef);
|
||||
|
||||
if (!push(vm, ref)) {
|
||||
return BAS_VM_STACK_OVERFLOW;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case OP_STORE_ARRAY: {
|
||||
uint8_t dims = readUint8(vm);
|
||||
|
||||
|
|
@ -3623,6 +3851,15 @@ static BasVmResultE execArith(BasVmT *vm, uint8_t op) {
|
|||
}
|
||||
}
|
||||
|
||||
// Capture operand types BEFORE release so we know whether to keep
|
||||
// the result as a float. The parser always emits OP_ADD_INT (the
|
||||
// VM promotes based on operand types), so the opcode alone can't
|
||||
// tell us whether this is an integer or floating-point op.
|
||||
uint8_t aType = a.type;
|
||||
uint8_t bType = b.type;
|
||||
bool hadFloat = (aType == BAS_TYPE_SINGLE || aType == BAS_TYPE_DOUBLE ||
|
||||
bType == BAS_TYPE_SINGLE || bType == BAS_TYPE_DOUBLE);
|
||||
|
||||
double na = basValToNumber(a);
|
||||
double nb = basValToNumber(b);
|
||||
basValRelease(&a);
|
||||
|
|
@ -3682,8 +3919,12 @@ static BasVmResultE execArith(BasVmT *vm, uint8_t op) {
|
|||
break;
|
||||
}
|
||||
|
||||
// Return appropriate type
|
||||
if (op == OP_ADD_INT || op == OP_SUB_INT || op == OP_MUL_INT || op == OP_IDIV_INT || op == OP_MOD_INT) {
|
||||
// Return appropriate type. An op that would normally produce an
|
||||
// integer result (OP_ADD_INT, etc.) still has to yield a float when
|
||||
// either operand was a float -- otherwise 1.5 + 2.25 truncates to 3.
|
||||
bool intOp = (op == OP_ADD_INT || op == OP_SUB_INT || op == OP_MUL_INT || op == OP_IDIV_INT || op == OP_MOD_INT);
|
||||
|
||||
if (intOp && !hadFloat) {
|
||||
if (result >= (double)INT16_MIN && result <= (double)INT16_MAX) {
|
||||
push(vm, basValInteger((int16_t)result));
|
||||
} else if (result >= (double)INT32_MIN && result <= (double)INT32_MAX) {
|
||||
|
|
@ -5333,17 +5574,19 @@ static bool push(BasVmT *vm, BasValueT val) {
|
|||
}
|
||||
|
||||
|
||||
// x86 tolerates unaligned loads at cost of ~1 cycle; cast-through-pointer is
|
||||
// faster than memcpy because the compiler can emit a single 16-bit load.
|
||||
// memcpy with constant size is folded to a single load by the compiler and
|
||||
// is alignment-safe (bytecode operands aren't guaranteed 2-byte aligned).
|
||||
static inline int16_t readInt16(BasVmT *vm) {
|
||||
int16_t val = *(const int16_t *)&vm->module->code[vm->pc];
|
||||
int16_t val;
|
||||
memcpy(&val, &vm->module->code[vm->pc], sizeof(int16_t));
|
||||
vm->pc += sizeof(int16_t);
|
||||
return val;
|
||||
}
|
||||
|
||||
|
||||
static inline uint16_t readUint16(BasVmT *vm) {
|
||||
uint16_t val = *(const uint16_t *)&vm->module->code[vm->pc];
|
||||
uint16_t val;
|
||||
memcpy(&val, &vm->module->code[vm->pc], sizeof(uint16_t));
|
||||
vm->pc += sizeof(uint16_t);
|
||||
return val;
|
||||
}
|
||||
|
|
@ -5364,6 +5607,7 @@ static bool runSubLoop(BasVmT *vm, int32_t savedPc, int32_t savedCallDepth, bool
|
|||
bool hadBreakpoint = false;
|
||||
|
||||
while (vm->running && vm->callDepth > savedCallDepth) {
|
||||
int32_t stepPc = vm->pc; // save for ON ERROR dispatch
|
||||
BasVmResultE result = basVmStep(vm);
|
||||
|
||||
if (result == BAS_VM_HALTED) {
|
||||
|
|
@ -5397,6 +5641,41 @@ static bool runSubLoop(BasVmT *vm, int32_t savedPc, int32_t savedCallDepth, bool
|
|||
}
|
||||
|
||||
if (result != BAS_VM_OK) {
|
||||
// Try ON ERROR GOTO: walk call frames inside the current
|
||||
// sub-call boundary (savedCallDepth) looking for one that
|
||||
// registered a handler. If found, unwind to that frame
|
||||
// and jump to the handler. Mirrors basVmRun's dispatcher
|
||||
// so event handlers (fired via basVmCallSub) behave like
|
||||
// module-level code when it comes to error trapping.
|
||||
if (!vm->inErrorHandler && result != BAS_VM_BAD_OPCODE) {
|
||||
int32_t target = 0;
|
||||
|
||||
while (vm->callDepth > savedCallDepth) {
|
||||
BasCallFrameT *frame = &vm->callStack[vm->callDepth - 1];
|
||||
|
||||
if (frame->errorHandler != 0) {
|
||||
target = frame->errorHandler;
|
||||
break;
|
||||
}
|
||||
|
||||
for (int32_t li = 0; li < frame->localCount; li++) {
|
||||
basValRelease(&frame->locals[li]);
|
||||
}
|
||||
|
||||
vm->callDepth--;
|
||||
}
|
||||
|
||||
if (target != 0) {
|
||||
vm->errorPc = stepPc;
|
||||
vm->errorNextPc = vm->pc;
|
||||
vm->inErrorHandler = true;
|
||||
vm->errorHandler = target;
|
||||
vm->pc = target;
|
||||
stepsSinceYield = 0;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
vm->pc = savedPc;
|
||||
vm->callDepth = savedCallDepth;
|
||||
vm->running = savedRunning;
|
||||
|
|
|
|||
|
|
@ -262,6 +262,7 @@ typedef struct {
|
|||
int32_t returnPc; // instruction to return to
|
||||
int32_t baseSlot; // base index in locals array
|
||||
int32_t localCount; // number of locals in this frame
|
||||
int32_t errorHandler; // ON ERROR GOTO target in this SUB (0 = none)
|
||||
BasValueT locals[BAS_VM_MAX_LOCALS];
|
||||
} BasCallFrameT;
|
||||
|
||||
|
|
@ -293,12 +294,13 @@ typedef struct {
|
|||
#define BAS_MAX_PROC_NAME 64
|
||||
|
||||
typedef struct {
|
||||
char name[BAS_MAX_PROC_NAME]; // SUB/FUNCTION name (case-preserved)
|
||||
int32_t codeAddr; // entry point in code[]
|
||||
int32_t paramCount; // number of parameters
|
||||
int32_t localCount; // number of local variables (for debugger)
|
||||
uint8_t returnType; // BAS_TYPE_* (0 for SUB)
|
||||
bool isFunction; // true = FUNCTION, false = SUB
|
||||
char name[BAS_MAX_PROC_NAME]; // SUB/FUNCTION name (case-preserved)
|
||||
char formName[BAS_MAX_PROC_NAME]; // owning form (for form-scope vars), "" if global
|
||||
int32_t codeAddr; // entry point in code[]
|
||||
int32_t paramCount; // number of parameters
|
||||
int32_t localCount; // number of local variables (for debugger)
|
||||
uint8_t returnType; // BAS_TYPE_* (0 for SUB)
|
||||
bool isFunction; // true = FUNCTION, false = SUB
|
||||
} BasProcEntryT;
|
||||
|
||||
// Debug UDT field definition (preserved for debugger watch)
|
||||
|
|
@ -340,6 +342,16 @@ typedef struct {
|
|||
// Compiled module (output of the compiler)
|
||||
// ============================================================
|
||||
|
||||
// Runtime-required global init entry. STRING and SINGLE/DOUBLE
|
||||
// globals need to start with the correct slot type even when debug
|
||||
// info has been stripped, or operators that switch on slot type
|
||||
// (e.g. STRING concat) break on first use.
|
||||
typedef struct {
|
||||
int32_t index; // global slot index
|
||||
uint8_t dataType; // BAS_TYPE_*
|
||||
} BasGlobalInitT;
|
||||
|
||||
|
||||
typedef struct {
|
||||
uint8_t *code; // p-code bytecode
|
||||
int32_t codeLen;
|
||||
|
|
@ -353,6 +365,8 @@ typedef struct {
|
|||
int32_t procCount;
|
||||
BasFormVarInfoT *formVarInfo; // per-form variable counts
|
||||
int32_t formVarInfoCount;
|
||||
BasGlobalInitT *globalInits; // runtime global type init (survives stripping)
|
||||
int32_t globalInitCount;
|
||||
BasDebugVarT *debugVars; // variable names for debugger
|
||||
int32_t debugVarCount;
|
||||
BasDebugUdtDefT *debugUdtDefs; // UDT type definitions for debugger
|
||||
|
|
|
|||
|
|
@ -183,6 +183,7 @@ int main(int argc, char **argv) {
|
|||
const char *helpFile = prefsGetString(prefs, BAS_INI_SECTION_PROJECT, BAS_INI_KEY_HELPFILE, "");
|
||||
const char *startupForm = prefsGetString(prefs, BAS_INI_SECTION_SETTINGS, BAS_INI_KEY_STARTUPFORM, "");
|
||||
(void)startupForm; // used implicitly by stub's basFormRtLoadAllForms
|
||||
bool optionExplicit = prefsGetBool(prefs, BAS_INI_SECTION_SETTINGS, BAS_INI_KEY_OPTIONEXPLICIT, false);
|
||||
|
||||
// Derive output path
|
||||
char outBuf[DVX_MAX_PATH];
|
||||
|
|
@ -355,6 +356,7 @@ int main(int argc, char **argv) {
|
|||
}
|
||||
|
||||
basParserInit(parser, concatBuf, pos);
|
||||
parser->optionExplicit = optionExplicit;
|
||||
|
||||
if (!basParse(parser)) {
|
||||
fprintf(stderr, "Compile error at line %d: %s\n", (int)parser->errorLine, parser->error);
|
||||
|
|
|
|||
|
|
@ -170,10 +170,15 @@ int32_t appMain(DxeAppContextT *ctx) {
|
|||
basVmSetInputCallback(vm, stubInput, NULL);
|
||||
basVmSetDoEventsCallback(vm, stubDoEvents, NULL);
|
||||
|
||||
// Set app paths
|
||||
snprintf(vm->appPath, DVX_MAX_PATH, "%s", ctx->appDir);
|
||||
snprintf(vm->appConfig, DVX_MAX_PATH, "%s", ctx->appDir);
|
||||
snprintf(vm->appData, DVX_MAX_PATH, "%s", ctx->appDir);
|
||||
// Set app paths. App.Path is the .app's directory (read-only on CD);
|
||||
// App.Config and App.Data are writable subdirectories created on
|
||||
// demand. The IDE does the same split for project debugging, so
|
||||
// behavior matches between compiled apps and in-IDE runs.
|
||||
snprintf(vm->appPath, DVX_MAX_PATH, "%s", ctx->appDir);
|
||||
snprintf(vm->appConfig, DVX_MAX_PATH, "%s" DVX_PATH_SEP "CONFIG", ctx->appDir);
|
||||
snprintf(vm->appData, DVX_MAX_PATH, "%s" DVX_PATH_SEP "DATA", ctx->appDir);
|
||||
platformMkdirRecursive(vm->appConfig);
|
||||
platformMkdirRecursive(vm->appData);
|
||||
|
||||
// Set extern call callbacks (required for DECLARE LIBRARY functions)
|
||||
BasExternCallbacksT extCb;
|
||||
|
|
|
|||
2254
src/apps/kpunch/dvxbasic/test_suite.c
Normal file
2254
src/apps/kpunch/dvxbasic/test_suite.c
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -190,7 +190,7 @@ static void test4(void) {
|
|||
// PUSH 5 (limit); PUSH 1 (step)
|
||||
emit8(OP_PUSH_INT16); emit16(5);
|
||||
emit8(OP_PUSH_INT16); emit16(1);
|
||||
emit8(OP_FOR_INIT); emitU16(0); emit8(1); // isLocal=1
|
||||
emit8(OP_FOR_INIT); emitU16(0); emit8(1); emit16(0); // scope=local, skipOffset patched below
|
||||
|
||||
// Loop body start (record PC for FOR_NEXT offset)
|
||||
int32_t loopBody = sCodeLen;
|
||||
|
|
|
|||
|
|
@ -63,7 +63,6 @@
|
|||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <stdio.h>
|
||||
#include <dirent.h>
|
||||
#include <sys/stat.h>
|
||||
#include "dvxMem.h"
|
||||
#include "stb_ds_wrap.h"
|
||||
|
|
@ -448,9 +447,9 @@ static void scanAppsDirRecurse(const char *dirPath) {
|
|||
// Collect all entries first, close the handle, then process.
|
||||
// DOS has limited file handles; keeping a DIR open while
|
||||
// opening .app files for resource loading causes failures.
|
||||
DIR *dir = opendir(dirPath);
|
||||
char **names = dvxReadDir(dirPath);
|
||||
|
||||
if (!dir) {
|
||||
if (!names) {
|
||||
if (sAppCount == 0) {
|
||||
dvxLog("Progman: %s directory not found", dirPath);
|
||||
}
|
||||
|
|
@ -458,19 +457,6 @@ static void scanAppsDirRecurse(const char *dirPath) {
|
|||
return;
|
||||
}
|
||||
|
||||
char **names = NULL;
|
||||
struct dirent *ent;
|
||||
|
||||
while ((ent = readdir(dir)) != NULL) {
|
||||
if (ent->d_name[0] == '.' && (ent->d_name[1] == '\0' || (ent->d_name[1] == '.' && ent->d_name[2] == '\0'))) {
|
||||
continue;
|
||||
}
|
||||
|
||||
arrput(names, strdup(ent->d_name));
|
||||
}
|
||||
|
||||
closedir(dir);
|
||||
|
||||
int32_t nEntries = (int32_t)arrlen(names);
|
||||
|
||||
for (int32_t i = 0; i < nEntries; i++) {
|
||||
|
|
@ -481,14 +467,10 @@ static void scanAppsDirRecurse(const char *dirPath) {
|
|||
|
||||
if (stat(fullPath, &st) == 0 && S_ISDIR(st.st_mode)) {
|
||||
scanAppsDirRecurse(fullPath);
|
||||
free(names[i]);
|
||||
continue;
|
||||
}
|
||||
|
||||
int32_t len = (int32_t)strlen(names[i]);
|
||||
|
||||
if (len < 5 || strcasecmp(names[i] + len - 4, ".app") != 0 || strcasecmp(names[i], "progman.app") == 0) {
|
||||
free(names[i]);
|
||||
if (!dvxHasExt(names[i], ".app") || strcasecmp(names[i], "progman.app") == 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
|
@ -497,6 +479,7 @@ static void scanAppsDirRecurse(const char *dirPath) {
|
|||
snprintf(newEntry.path, sizeof(newEntry.path), "%s", fullPath);
|
||||
|
||||
// Default name from filename (without .app extension)
|
||||
int32_t len = (int32_t)strlen(names[i]);
|
||||
int32_t nameLen = len - 4;
|
||||
|
||||
if (nameLen >= SHELL_APP_NAME_MAX) {
|
||||
|
|
@ -524,11 +507,10 @@ static void scanAppsDirRecurse(const char *dirPath) {
|
|||
arrput(sAppFiles, newEntry);
|
||||
sAppCount = (int32_t)arrlen(sAppFiles);
|
||||
|
||||
free(names[i]);
|
||||
dvxUpdate(sAc);
|
||||
}
|
||||
|
||||
arrfree(names);
|
||||
dvxReadDirFree(names);
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -270,12 +270,15 @@ SUB mnuAddText_Click
|
|||
|
||||
DIM rName AS STRING
|
||||
rName = basInputBox2("Add Text Resource", "Resource name:", "")
|
||||
IF rName = "" THEN
|
||||
IF basInputCancelled OR rName = "" THEN
|
||||
EXIT SUB
|
||||
END IF
|
||||
|
||||
DIM text AS STRING
|
||||
text = basInputBox2("Add Text Resource", "Text value:", "")
|
||||
IF basInputCancelled THEN
|
||||
EXIT SUB
|
||||
END IF
|
||||
|
||||
IF ResAddText(filePath, rName, text) THEN
|
||||
ReopenAndRefresh
|
||||
|
|
@ -293,7 +296,7 @@ SUB mnuAddFile_Click
|
|||
|
||||
DIM rName AS STRING
|
||||
rName = basInputBox2("Add File Resource", "Resource name:", "")
|
||||
IF rName = "" THEN
|
||||
IF basInputCancelled OR rName = "" THEN
|
||||
EXIT SUB
|
||||
END IF
|
||||
|
||||
|
|
@ -352,6 +355,10 @@ SUB mnuEditText_Click
|
|||
DIM newText AS STRING
|
||||
newText = basInputBox2("Edit Text Resource", "Value for '" + rName + "':", oldText)
|
||||
|
||||
IF basInputCancelled THEN
|
||||
EXIT SUB
|
||||
END IF
|
||||
|
||||
IF ResAddText(filePath, rName, newText) THEN
|
||||
ReopenAndRefresh
|
||||
LblStatus.Caption = "Updated: " + rName
|
||||
|
|
|
|||
|
|
@ -46,6 +46,10 @@ DECLARE LIBRARY "basrt"
|
|||
' Show a modal text input box. Returns entered text, or "" if cancelled.
|
||||
DECLARE FUNCTION basInputBox2(BYVAL title AS STRING, BYVAL prompt AS STRING, BYVAL defaultText AS STRING) AS STRING
|
||||
|
||||
' Returns True if the most recent basInputBox2 call was cancelled,
|
||||
' False if the user clicked OK (even with an empty field).
|
||||
DECLARE FUNCTION basInputCancelled() AS INTEGER
|
||||
|
||||
' Show a choice dialog with a listbox. items$ is pipe-delimited
|
||||
' (e.g. "Red|Green|Blue"). Returns chosen index (0-based), or -1.
|
||||
DECLARE FUNCTION basChoiceDialog(BYVAL title AS STRING, BYVAL prompt AS STRING, BYVAL items AS STRING, BYVAL defaultIdx AS INTEGER) AS INTEGER
|
||||
|
|
|
|||
|
|
@ -1193,6 +1193,14 @@ static void dispatchEvents(AppContextT *ctx) {
|
|||
int32_t menuId = item->id;
|
||||
WindowT *win = findWindowById(ctx, ctx->popup.windowId);
|
||||
closeAllPopups(ctx);
|
||||
// Consume the press so that if the menu handler
|
||||
// re-enters the event loop (CreateForm/Show, modal
|
||||
// dialog) the still-held button isn't re-dispatched
|
||||
// as a fresh MouseDown to whatever widget is under
|
||||
// the (now-closed) menu dropdown. The matching
|
||||
// release is also swallowed via suppressNextMouseUp.
|
||||
ctx->prevMouseButtons |= MOUSE_LEFT;
|
||||
ctx->suppressNextMouseUp = true;
|
||||
|
||||
if (win && win->onMenu) {
|
||||
WIN_CALLBACK(ctx, win, win->onMenu(win, menuId));
|
||||
|
|
@ -1363,9 +1371,14 @@ static void dispatchEvents(AppContextT *ctx) {
|
|||
}
|
||||
}
|
||||
|
||||
// Handle button release on content -- send to focused window
|
||||
// Handle button release on content -- send to focused window.
|
||||
// Skip if a menu click consumed this press (prevents the stray
|
||||
// release from firing as a click on whatever widget sits under
|
||||
// where the menu dropdown was).
|
||||
if (!(buttons & MOUSE_LEFT) && (prevBtn & MOUSE_LEFT)) {
|
||||
if (ctx->stack.focusedIdx >= 0) {
|
||||
if (ctx->suppressNextMouseUp) {
|
||||
ctx->suppressNextMouseUp = false;
|
||||
} else if (ctx->stack.focusedIdx >= 0) {
|
||||
WindowT *win = ctx->stack.windows[ctx->stack.focusedIdx];
|
||||
|
||||
if (win->onMouse) {
|
||||
|
|
|
|||
|
|
@ -82,6 +82,10 @@ typedef struct AppContextT {
|
|||
int32_t prevMouseX;
|
||||
int32_t prevMouseY;
|
||||
int32_t prevMouseButtons;
|
||||
// Set true when a menu/popup click consumed a mouse press; the
|
||||
// matching release is then swallowed so widgets underneath the
|
||||
// (now-closed) menu don't receive a stray click.
|
||||
bool suppressNextMouseUp;
|
||||
// Double-click detection for minimized window icons: timestamps and
|
||||
// window IDs track whether two clicks land on the same icon within
|
||||
// the system double-click interval.
|
||||
|
|
|
|||
|
|
@ -588,6 +588,65 @@ void drawHLine(DisplayT *d, const BlitOpsT *ops, int32_t x, int32_t y, int32_t w
|
|||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// calcCenteredText
|
||||
// ============================================================
|
||||
//
|
||||
// Shared by every widget that centers a text label in its own rect
|
||||
// (buttons, checkbox labels, etc.). Centralising the arithmetic
|
||||
// avoids tiny off-by-one drift from copy-paste variations.
|
||||
|
||||
void calcCenteredText(const BitmapFontT *font, int32_t rectX, int32_t rectY, int32_t rectW, int32_t rectH, const char *text, int32_t *outX, int32_t *outY) {
|
||||
int32_t textW = textWidthAccel(font, text);
|
||||
|
||||
if (outX) {
|
||||
*outX = rectX + (rectW - textW) / 2;
|
||||
}
|
||||
|
||||
if (outY) {
|
||||
*outY = rectY + (rectH - font->charHeight) / 2;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// drawPressableBevel
|
||||
// ============================================================
|
||||
//
|
||||
// Standard 2px button / toggle bevel. Swaps highlight and shadow when
|
||||
// pressed to create the sunken look. face fills the interior (pass 0
|
||||
// to leave the underlying content alone, e.g. for transparent toggle
|
||||
// buttons).
|
||||
|
||||
void drawPressableBevel(DisplayT *d, const BlitOpsT *ops, int32_t x, int32_t y, int32_t w, int32_t h, bool pressed, uint32_t face, const ColorSchemeT *colors) {
|
||||
BevelStyleT bevel;
|
||||
bevel.highlight = pressed ? colors->windowShadow : colors->windowHighlight;
|
||||
bevel.shadow = pressed ? colors->windowHighlight : colors->windowShadow;
|
||||
bevel.face = face;
|
||||
bevel.width = 2;
|
||||
drawBevel(d, ops, x, y, w, h, &bevel);
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// drawWidgetTextAccel
|
||||
// ============================================================
|
||||
//
|
||||
// Every text-bearing widget paints this same enabled/disabled branch.
|
||||
// Centralising it keeps the embossed-disabled appearance consistent
|
||||
// and removes ~5 lines of boilerplate per widget.
|
||||
|
||||
void drawWidgetTextAccel(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, int32_t x, int32_t y, const char *text, uint32_t fg, uint32_t bg, bool opaque, bool enabled, const ColorSchemeT *colors) {
|
||||
if (!enabled) {
|
||||
drawTextAccel(d, ops, font, x + 1, y + 1, text, colors->windowHighlight, 0, false);
|
||||
drawTextAccel(d, ops, font, x, y, text, colors->windowShadow, 0, false);
|
||||
return;
|
||||
}
|
||||
|
||||
drawTextAccel(d, ops, font, x, y, text, fg, bg, opaque);
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// drawInit
|
||||
// ============================================================
|
||||
|
|
|
|||
|
|
@ -116,6 +116,24 @@ void drawTermRow(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, int3
|
|||
// Windows 3.x focus rectangle convention.
|
||||
void drawFocusRect(DisplayT *d, const BlitOpsT *ops, int32_t x, int32_t y, int32_t w, int32_t h, uint32_t color);
|
||||
|
||||
// Compute the (x, y) origin for rendering `text` centered both
|
||||
// horizontally and vertically inside the rect (rectX, rectY, rectW,
|
||||
// rectH). Measures `text` with textWidthAccel (ignores & markers) and
|
||||
// the font's charHeight. Writes results to *outX / *outY.
|
||||
void calcCenteredText(const BitmapFontT *font, int32_t rectX, int32_t rectY, int32_t rectW, int32_t rectH, const char *text, int32_t *outX, int32_t *outY);
|
||||
|
||||
// Draw `text` with &-accelerator markers, automatically choosing
|
||||
// between the enabled and disabled (embossed) rendering paths. Saves
|
||||
// the if/else block every widget paints around drawTextAccel /
|
||||
// drawTextAccelEmbossed.
|
||||
void drawWidgetTextAccel(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, int32_t x, int32_t y, const char *text, uint32_t fg, uint32_t bg, bool opaque, bool enabled, const ColorSchemeT *colors);
|
||||
|
||||
// Draw a pressable 2px bevel. When pressed is true, highlight/shadow
|
||||
// colors swap to produce the "sunken" look (button pressed, toggle
|
||||
// checked). face is the fill color; pass 0 to leave the interior
|
||||
// alone. Used by buttons, checkboxes, toggle bars, etc.
|
||||
void drawPressableBevel(DisplayT *d, const BlitOpsT *ops, int32_t x, int32_t y, int32_t w, int32_t h, bool pressed, uint32_t face, const ColorSchemeT *colors);
|
||||
|
||||
// Horizontal line (1px tall rectangle fill, but with simpler clipping).
|
||||
void drawHLine(DisplayT *d, const BlitOpsT *ops, int32_t x, int32_t y, int32_t w, uint32_t color);
|
||||
|
||||
|
|
|
|||
|
|
@ -142,6 +142,13 @@ void widgetDestroyChildren(WidgetT *w);
|
|||
// Allocation
|
||||
WidgetT *widgetAlloc(WidgetT *parent, int32_t type);
|
||||
|
||||
// Allocate a widget of the given type PLUS a data struct of dataSize
|
||||
// bytes. The data struct must begin with a `const char *text` field
|
||||
// (WCLASS_HAS_TEXT semantics); this field is set to strdup(text) and
|
||||
// the widget's accelKey is parsed from text. Other fields in the data
|
||||
// struct remain zeroed. Returns NULL on allocation failure.
|
||||
WidgetT *widgetAllocWithText(WidgetT *parent, int32_t type, size_t dataSize, const char *text);
|
||||
|
||||
// Focus management
|
||||
WidgetT *widgetFindNextFocusable(WidgetT *root, WidgetT *after);
|
||||
WidgetT *widgetFindPrevFocusable(WidgetT *root, WidgetT *before);
|
||||
|
|
|
|||
|
|
@ -312,6 +312,22 @@ const char *dvxSkipWs(const char *s);
|
|||
// new length. buf may be NULL or empty.
|
||||
int32_t dvxTrimRight(char *buf);
|
||||
|
||||
// Case-insensitive check that `name` ends with `ext` (which should
|
||||
// include the leading dot, e.g. ".app"). Returns false if either
|
||||
// argument is NULL or name is shorter than ext.
|
||||
bool dvxHasExt(const char *name, const char *ext);
|
||||
|
||||
// Read all entries from `dirPath` (except "." and "..") into a
|
||||
// heap-allocated stb_ds array of malloc'd strings. Returns NULL on
|
||||
// open failure. An empty directory returns a valid empty array;
|
||||
// check arrlen() for count. Caller must pass the result to
|
||||
// dvxReadDirFree when done.
|
||||
char **dvxReadDir(const char *dirPath);
|
||||
|
||||
// Free an array returned by dvxReadDir: frees every entry and the
|
||||
// stb_ds array header. Safe to call with NULL.
|
||||
void dvxReadDirFree(char **entries);
|
||||
|
||||
// The platform's native directory separator as a string literal. Use
|
||||
// with string-literal concatenation in format strings or path constants:
|
||||
// snprintf(buf, sz, "%s" DVX_PATH_SEP "%s", dir, name);
|
||||
|
|
|
|||
|
|
@ -2312,12 +2312,15 @@ DXE_EXPORT_TABLE(sDxeExportTable)
|
|||
// --- dvx helpers (lives in dvx.exe, used by all modules) ---
|
||||
DXE_EXPORT(dvxCalloc)
|
||||
DXE_EXPORT(dvxFree)
|
||||
DXE_EXPORT(dvxHasExt)
|
||||
DXE_EXPORT(dvxLog)
|
||||
DXE_EXPORT(dvxMalloc)
|
||||
DXE_EXPORT(dvxMemAppIdPtr)
|
||||
DXE_EXPORT(dvxMemGetAppUsage)
|
||||
DXE_EXPORT(dvxMemResetApp)
|
||||
DXE_EXPORT(dvxMemSnapshotLoad)
|
||||
DXE_EXPORT(dvxReadDir)
|
||||
DXE_EXPORT(dvxReadDirFree)
|
||||
DXE_EXPORT(dvxRealloc)
|
||||
DXE_EXPORT(dvxSkipWs)
|
||||
DXE_EXPORT(dvxStrdup)
|
||||
|
|
|
|||
|
|
@ -29,11 +29,14 @@
|
|||
// can still use them.
|
||||
|
||||
#include "dvxPlat.h"
|
||||
#include "thirdparty/stb_ds_wrap.h"
|
||||
|
||||
#include <ctype.h>
|
||||
#include <dirent.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <strings.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#ifdef __DJGPP__
|
||||
|
|
@ -70,6 +73,67 @@ int32_t dvxTrimRight(char *buf) {
|
|||
}
|
||||
|
||||
|
||||
bool dvxHasExt(const char *name, const char *ext) {
|
||||
if (!name || !ext) {
|
||||
return false;
|
||||
}
|
||||
|
||||
size_t nameLen = strlen(name);
|
||||
size_t extLen = strlen(ext);
|
||||
|
||||
if (nameLen < extLen) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return strcasecmp(name + nameLen - extLen, ext) == 0;
|
||||
}
|
||||
|
||||
|
||||
char **dvxReadDir(const char *dirPath) {
|
||||
if (!dirPath) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
DIR *dir = opendir(dirPath);
|
||||
|
||||
if (!dir) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
char **entries = NULL;
|
||||
struct dirent *ent;
|
||||
|
||||
while ((ent = readdir(dir)) != NULL) {
|
||||
// Skip "." and ".."
|
||||
if (ent->d_name[0] == '.' &&
|
||||
(ent->d_name[1] == '\0' ||
|
||||
(ent->d_name[1] == '.' && ent->d_name[2] == '\0'))) {
|
||||
continue;
|
||||
}
|
||||
|
||||
arrput(entries, strdup(ent->d_name));
|
||||
}
|
||||
|
||||
closedir(dir);
|
||||
return entries;
|
||||
}
|
||||
|
||||
|
||||
void dvxReadDirFree(char **entries) {
|
||||
if (!entries) {
|
||||
return;
|
||||
}
|
||||
|
||||
int32_t n = (int32_t)arrlen(entries);
|
||||
|
||||
for (int32_t i = 0; i < n; i++) {
|
||||
free(entries[i]);
|
||||
}
|
||||
|
||||
arrfree(entries);
|
||||
}
|
||||
|
||||
|
||||
int32_t platformChdir(const char *path) {
|
||||
#ifdef __DJGPP__
|
||||
if (path[0] && path[1] == ':') {
|
||||
|
|
|
|||
|
|
@ -41,9 +41,12 @@
|
|||
// which doesn't map cleanly to an arena pattern.
|
||||
|
||||
#include "dvxWgtP.h"
|
||||
#include "dvxDraw.h"
|
||||
#include "dvxPlat.h"
|
||||
#include "stb_ds_wrap.h"
|
||||
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <time.h>
|
||||
|
||||
// ============================================================
|
||||
|
|
@ -254,6 +257,31 @@ WidgetT *widgetAlloc(WidgetT *parent, int32_t type) {
|
|||
}
|
||||
|
||||
|
||||
WidgetT *widgetAllocWithText(WidgetT *parent, int32_t type, size_t dataSize, const char *text) {
|
||||
WidgetT *w = widgetAlloc(parent, type);
|
||||
|
||||
if (!w) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
void *data = calloc(1, dataSize);
|
||||
|
||||
if (!data) {
|
||||
// Widget itself is already in the tree; leave it rather than
|
||||
// attempting a partial rollback (widget destroy is idempotent
|
||||
// via the parent teardown).
|
||||
dvxLog("Widget: failed to allocate %u-byte data for type %d", (unsigned)dataSize, type);
|
||||
return w;
|
||||
}
|
||||
|
||||
// The data struct must begin with a `const char *text` field.
|
||||
*(const char **)data = text ? strdup(text) : NULL;
|
||||
w->data = data;
|
||||
w->accelKey = accelParse(text);
|
||||
return w;
|
||||
}
|
||||
|
||||
|
||||
int32_t widgetCountVisibleChildren(const WidgetT *w) {
|
||||
int32_t count = 0;
|
||||
|
||||
|
|
|
|||
|
|
@ -38,7 +38,6 @@
|
|||
#include "../tools/hlpcCompile.h"
|
||||
|
||||
#include <ctype.h>
|
||||
#include <dirent.h>
|
||||
#include <dlfcn.h>
|
||||
#include <stdarg.h>
|
||||
#include <stdio.h>
|
||||
|
|
@ -175,49 +174,41 @@ static void collectGlobFiles(char ***outFiles, const char *pattern, const char *
|
|||
dirPart[1] = '\0';
|
||||
}
|
||||
|
||||
DIR *d = opendir(dirPart);
|
||||
char **names = dvxReadDir(dirPart);
|
||||
|
||||
if (!d) {
|
||||
if (!names) {
|
||||
return;
|
||||
}
|
||||
|
||||
char **names = NULL;
|
||||
struct dirent *ent;
|
||||
|
||||
while ((ent = readdir(d)) != NULL) {
|
||||
if (ent->d_name[0] == '.') {
|
||||
continue;
|
||||
}
|
||||
|
||||
arrput(names, strdup(ent->d_name));
|
||||
}
|
||||
|
||||
closedir(d);
|
||||
|
||||
int32_t nEntries = (int32_t)arrlen(names);
|
||||
|
||||
for (int32_t i = 0; i < nEntries; i++) {
|
||||
// Skip hidden files (dvxReadDir already strips "." and "..")
|
||||
if (names[i][0] == '.') {
|
||||
continue;
|
||||
}
|
||||
|
||||
char fullPath[DVX_MAX_PATH];
|
||||
snprintf(fullPath, sizeof(fullPath), "%s" DVX_PATH_SEP "%s", dirPart, names[i]);
|
||||
|
||||
struct stat st;
|
||||
|
||||
if (stat(fullPath, &st) == 0) {
|
||||
if (S_ISDIR(st.st_mode)) {
|
||||
char subPattern[DVX_MAX_PATH];
|
||||
snprintf(subPattern, sizeof(subPattern), "%s" DVX_PATH_SEP "%s", fullPath, globPart);
|
||||
collectGlobFiles(outFiles, subPattern, excludePattern);
|
||||
} else if (platformGlobMatch(globPart, names[i])) {
|
||||
if (!excludePattern || !platformGlobMatch(excludePattern, names[i])) {
|
||||
arrput(*outFiles, strdup(fullPath));
|
||||
}
|
||||
}
|
||||
if (stat(fullPath, &st) != 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
free(names[i]);
|
||||
if (S_ISDIR(st.st_mode)) {
|
||||
char subPattern[DVX_MAX_PATH];
|
||||
snprintf(subPattern, sizeof(subPattern), "%s" DVX_PATH_SEP "%s", fullPath, globPart);
|
||||
collectGlobFiles(outFiles, subPattern, excludePattern);
|
||||
} else if (platformGlobMatch(globPart, names[i])) {
|
||||
if (!excludePattern || !platformGlobMatch(excludePattern, names[i])) {
|
||||
arrput(*outFiles, strdup(fullPath));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
arrfree(names);
|
||||
dvxReadDirFree(names);
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -247,51 +238,38 @@ static int32_t countHcfInputFiles(const char *hcfPath) {
|
|||
|
||||
// Count total progress steps across all .hcf files under a directory.
|
||||
static int32_t countTotalHelpSteps(const char *dirPath) {
|
||||
DIR *dir = opendir(dirPath);
|
||||
char **names = dvxReadDir(dirPath);
|
||||
|
||||
if (!dir) {
|
||||
if (!names) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
char **names = NULL;
|
||||
struct dirent *ent;
|
||||
|
||||
while ((ent = readdir(dir)) != NULL) {
|
||||
if (ent->d_name[0] == '.') {
|
||||
continue;
|
||||
}
|
||||
|
||||
arrput(names, strdup(ent->d_name));
|
||||
}
|
||||
|
||||
closedir(dir);
|
||||
|
||||
int32_t total = 0;
|
||||
int32_t nEntries = (int32_t)arrlen(names);
|
||||
|
||||
for (int32_t i = 0; i < nEntries; i++) {
|
||||
if (names[i][0] == '.') {
|
||||
continue;
|
||||
}
|
||||
|
||||
char fullPath[DVX_MAX_PATH];
|
||||
snprintf(fullPath, sizeof(fullPath), "%s" DVX_PATH_SEP "%s", dirPath, names[i]);
|
||||
|
||||
struct stat st;
|
||||
|
||||
if (stat(fullPath, &st) == 0) {
|
||||
if (S_ISDIR(st.st_mode)) {
|
||||
total += countTotalHelpSteps(fullPath);
|
||||
} else {
|
||||
int32_t nameLen = (int32_t)strlen(names[i]);
|
||||
|
||||
if (nameLen > 4 && strcasecmp(names[i] + nameLen - 4, ".hcf") == 0) {
|
||||
int32_t fileCount = countHcfInputFiles(fullPath);
|
||||
total += hlpcProgressTotal(fileCount);
|
||||
}
|
||||
}
|
||||
if (stat(fullPath, &st) != 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
free(names[i]);
|
||||
if (S_ISDIR(st.st_mode)) {
|
||||
total += countTotalHelpSteps(fullPath);
|
||||
} else if (dvxHasExt(names[i], ".hcf")) {
|
||||
int32_t fileCount = countHcfInputFiles(fullPath);
|
||||
total += hlpcProgressTotal(fileCount);
|
||||
}
|
||||
}
|
||||
|
||||
arrfree(names);
|
||||
dvxReadDirFree(names);
|
||||
return total;
|
||||
}
|
||||
|
||||
|
|
@ -764,49 +742,36 @@ static void processHcf(const char *hcfPath, const char *hcfDir) {
|
|||
|
||||
// Recursively scan a directory for .hcf files and process each one.
|
||||
static void processHcfDir(const char *dirPath) {
|
||||
DIR *dir = opendir(dirPath);
|
||||
char **names = dvxReadDir(dirPath);
|
||||
|
||||
if (!dir) {
|
||||
if (!names) {
|
||||
return;
|
||||
}
|
||||
|
||||
char **names = NULL;
|
||||
struct dirent *ent;
|
||||
|
||||
while ((ent = readdir(dir)) != NULL) {
|
||||
if (ent->d_name[0] == '.') {
|
||||
continue;
|
||||
}
|
||||
|
||||
arrput(names, strdup(ent->d_name));
|
||||
}
|
||||
|
||||
closedir(dir);
|
||||
|
||||
int32_t nEntries = (int32_t)arrlen(names);
|
||||
|
||||
for (int32_t i = 0; i < nEntries; i++) {
|
||||
if (names[i][0] == '.') {
|
||||
continue;
|
||||
}
|
||||
|
||||
char fullPath[DVX_MAX_PATH];
|
||||
snprintf(fullPath, sizeof(fullPath), "%s" DVX_PATH_SEP "%s", dirPath, names[i]);
|
||||
|
||||
struct stat st;
|
||||
|
||||
if (stat(fullPath, &st) == 0) {
|
||||
if (S_ISDIR(st.st_mode)) {
|
||||
processHcfDir(fullPath);
|
||||
} else {
|
||||
int32_t nameLen = (int32_t)strlen(names[i]);
|
||||
|
||||
if (nameLen > 4 && strcasecmp(names[i] + nameLen - 4, ".hcf") == 0) {
|
||||
processHcf(fullPath, dirPath);
|
||||
}
|
||||
}
|
||||
if (stat(fullPath, &st) != 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
free(names[i]);
|
||||
if (S_ISDIR(st.st_mode)) {
|
||||
processHcfDir(fullPath);
|
||||
} else if (dvxHasExt(names[i], ".hcf")) {
|
||||
processHcf(fullPath, dirPath);
|
||||
}
|
||||
}
|
||||
|
||||
arrfree(names);
|
||||
dvxReadDirFree(names);
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -864,35 +829,19 @@ static void scanDir(const char *dirPath, const char *ext, ModuleT **mods) {
|
|||
// Collect all entries first, close the handle, then process.
|
||||
// DOS has limited file handles; keeping a DIR open during
|
||||
// recursion or stat() causes intermittent failures.
|
||||
DIR *dir = opendir(dirPath);
|
||||
char **names = dvxReadDir(dirPath);
|
||||
|
||||
if (!dir) {
|
||||
if (!names) {
|
||||
return;
|
||||
}
|
||||
|
||||
char **names = NULL;
|
||||
struct dirent *ent;
|
||||
|
||||
while ((ent = readdir(dir)) != NULL) {
|
||||
if (ent->d_name[0] == '.' && (ent->d_name[1] == '\0' || (ent->d_name[1] == '.' && ent->d_name[2] == '\0'))) {
|
||||
continue;
|
||||
}
|
||||
|
||||
arrput(names, strdup(ent->d_name));
|
||||
}
|
||||
|
||||
closedir(dir);
|
||||
|
||||
int32_t count = (int32_t)arrlen(names);
|
||||
int32_t extLen = (int32_t)strlen(ext);
|
||||
int32_t count = (int32_t)arrlen(names);
|
||||
|
||||
for (int32_t i = 0; i < count; i++) {
|
||||
char path[DVX_MAX_PATH];
|
||||
snprintf(path, sizeof(path), "%s" DVX_PATH_SEP "%s", dirPath, names[i]);
|
||||
|
||||
int32_t nameLen = (int32_t)strlen(names[i]);
|
||||
|
||||
if (nameLen > extLen && strcasecmp(names[i] + nameLen - extLen, ext) == 0) {
|
||||
if (dvxHasExt(names[i], ext)) {
|
||||
ModuleT mod;
|
||||
memset(&mod, 0, sizeof(mod));
|
||||
snprintf(mod.path, sizeof(mod.path), "%s", path);
|
||||
|
|
@ -905,11 +854,9 @@ static void scanDir(const char *dirPath, const char *ext, ModuleT **mods) {
|
|||
scanDir(path, ext, mods);
|
||||
}
|
||||
}
|
||||
|
||||
free(names[i]);
|
||||
}
|
||||
|
||||
arrfree(names);
|
||||
dvxReadDirFree(names);
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -43,9 +43,10 @@ SYSTEMDIR = ../../bin/system
|
|||
all: $(HOSTDIR)/dvxres $(HOSTDIR)/mkicon $(HOSTDIR)/mktbicon $(HOSTDIR)/mkwgticon $(HOSTDIR)/bmp2raw $(HOSTDIR)/dvxhlpc $(SYSTEMDIR)/SPLASH.RAW $(SYSTEMDIR)/DVXHLPC.EXE $(SYSTEMDIR)/DVXRES.EXE
|
||||
|
||||
PLATFORM_UTIL = ../libs/kpunch/libdvx/platform/dvxPlatformUtil.c
|
||||
STB_DS_IMPL = ../libs/kpunch/libdvx/thirdparty/stb_ds_impl.c
|
||||
|
||||
$(HOSTDIR)/dvxres: dvxres.c ../libs/kpunch/libdvx/dvxResource.c ../libs/kpunch/libdvx/dvxRes.h $(PLATFORM_UTIL) | $(HOSTDIR)
|
||||
$(CC) $(CFLAGS) -o $@ dvxres.c ../libs/kpunch/libdvx/dvxResource.c $(PLATFORM_UTIL)
|
||||
$(HOSTDIR)/dvxres: dvxres.c ../libs/kpunch/libdvx/dvxResource.c ../libs/kpunch/libdvx/dvxRes.h $(PLATFORM_UTIL) $(STB_DS_IMPL) | $(HOSTDIR)
|
||||
$(CC) $(CFLAGS) -o $@ dvxres.c ../libs/kpunch/libdvx/dvxResource.c $(PLATFORM_UTIL) $(STB_DS_IMPL)
|
||||
|
||||
$(HOSTDIR)/mkicon: mkicon.c bmpDraw.c bmpDraw.h | $(HOSTDIR)
|
||||
$(CC) $(CFLAGS) -o $@ mkicon.c bmpDraw.c -lm
|
||||
|
|
@ -59,8 +60,8 @@ $(HOSTDIR)/mkwgticon: mkwgticon.c bmpDraw.c bmpDraw.h | $(HOSTDIR)
|
|||
$(HOSTDIR)/bmp2raw: bmp2raw.c | $(HOSTDIR)
|
||||
$(CC) $(CFLAGS) -o $@ bmp2raw.c
|
||||
|
||||
$(HOSTDIR)/dvxhlpc: dvxhlpc.c ../apps/kpunch/dvxhelp/hlpformat.h $(PLATFORM_UTIL) | $(HOSTDIR)
|
||||
$(CC) $(CFLAGS) -o $@ dvxhlpc.c $(PLATFORM_UTIL)
|
||||
$(HOSTDIR)/dvxhlpc: dvxhlpc.c ../apps/kpunch/dvxhelp/hlpformat.h $(PLATFORM_UTIL) $(STB_DS_IMPL) | $(HOSTDIR)
|
||||
$(CC) $(CFLAGS) -o $@ dvxhlpc.c $(PLATFORM_UTIL) $(STB_DS_IMPL)
|
||||
|
||||
$(HOSTDIR):
|
||||
mkdir -p $(HOSTDIR)
|
||||
|
|
@ -74,8 +75,8 @@ $(BINDIR):
|
|||
$(CONFIGDIR):
|
||||
mkdir -p $(CONFIGDIR)
|
||||
|
||||
$(SYSTEMDIR)/DVXHLPC.EXE: dvxhlpc.c hlpcCompile.h ../apps/kpunch/dvxhelp/hlpformat.h $(PLATFORM_UTIL) | $(SYSTEMDIR)
|
||||
$(DOSCC) $(DOSCFLAGS) -o $(SYSTEMDIR)/dvxhlpc.exe dvxhlpc.c $(PLATFORM_UTIL)
|
||||
$(SYSTEMDIR)/DVXHLPC.EXE: dvxhlpc.c hlpcCompile.h ../apps/kpunch/dvxhelp/hlpformat.h $(PLATFORM_UTIL) $(STB_DS_IMPL) | $(SYSTEMDIR)
|
||||
$(DOSCC) $(DOSCFLAGS) -o $(SYSTEMDIR)/dvxhlpc.exe dvxhlpc.c $(PLATFORM_UTIL) $(STB_DS_IMPL)
|
||||
$(EXE2COFF) $(SYSTEMDIR)/dvxhlpc.exe
|
||||
cat $(CWSDSTUB) $(SYSTEMDIR)/dvxhlpc > $@
|
||||
rm -f $(SYSTEMDIR)/dvxhlpc $(SYSTEMDIR)/dvxhlpc.exe
|
||||
|
|
@ -87,8 +88,8 @@ $(SYSTEMDIR)/DVXHLPC.EXE: dvxhlpc.c hlpcCompile.h ../apps/kpunch/dvxhelp/hlpform
|
|||
../../obj/loader:
|
||||
mkdir -p ../../obj/loader
|
||||
|
||||
$(SYSTEMDIR)/DVXRES.EXE: dvxres.c ../libs/kpunch/libdvx/dvxResource.c ../libs/kpunch/libdvx/dvxRes.h $(PLATFORM_UTIL) | $(SYSTEMDIR)
|
||||
$(DOSCC) $(DOSCFLAGS) -o $(SYSTEMDIR)/dvxres.exe dvxres.c ../libs/kpunch/libdvx/dvxResource.c $(PLATFORM_UTIL)
|
||||
$(SYSTEMDIR)/DVXRES.EXE: dvxres.c ../libs/kpunch/libdvx/dvxResource.c ../libs/kpunch/libdvx/dvxRes.h $(PLATFORM_UTIL) $(STB_DS_IMPL) | $(SYSTEMDIR)
|
||||
$(DOSCC) $(DOSCFLAGS) -o $(SYSTEMDIR)/dvxres.exe dvxres.c ../libs/kpunch/libdvx/dvxResource.c $(PLATFORM_UTIL) $(STB_DS_IMPL)
|
||||
$(EXE2COFF) $(SYSTEMDIR)/dvxres.exe
|
||||
cat $(CWSDSTUB) $(SYSTEMDIR)/dvxres > $@
|
||||
rm -f $(SYSTEMDIR)/dvxres $(SYSTEMDIR)/dvxres.exe
|
||||
|
|
|
|||
|
|
@ -73,15 +73,11 @@ void widgetFramePaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bitmap
|
|||
|
||||
|
||||
WidgetT *wgtFrame(WidgetT *parent, const char *title) {
|
||||
WidgetT *w = widgetAlloc(parent, sFrameTypeId);
|
||||
WidgetT *w = widgetAllocWithText(parent, sFrameTypeId, sizeof(FrameDataT), title);
|
||||
|
||||
if (w) {
|
||||
FrameDataT *fd = calloc(1, sizeof(FrameDataT));
|
||||
fd->title = title ? strdup(title) : NULL;
|
||||
fd->style = FrameInE;
|
||||
fd->color = 0;
|
||||
w->data = fd;
|
||||
w->accelKey = accelParse(title);
|
||||
if (w && w->data) {
|
||||
FrameDataT *fd = (FrameDataT *)w->data;
|
||||
fd->style = FrameInE;
|
||||
}
|
||||
|
||||
return w;
|
||||
|
|
@ -168,11 +164,7 @@ void widgetFramePaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bitmap
|
|||
rectFill(d, ops, titleX - 2, titleY,
|
||||
titleW + 4, font->charHeight, bg);
|
||||
|
||||
if (!w->enabled) {
|
||||
drawTextAccelEmbossed(d, ops, font, titleX, titleY, fd->title, colors);
|
||||
} else {
|
||||
drawTextAccel(d, ops, font, titleX, titleY, fd->title, fg, bg, true);
|
||||
}
|
||||
drawWidgetTextAccel(d, ops, font, titleX, titleY, fd->title, fg, bg, true, w->enabled, colors);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -70,16 +70,7 @@ void widgetButtonPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bitma
|
|||
|
||||
|
||||
WidgetT *wgtButton(WidgetT *parent, const char *text) {
|
||||
WidgetT *w = widgetAlloc(parent, sTypeId);
|
||||
|
||||
if (w) {
|
||||
ButtonDataT *d = calloc(1, sizeof(ButtonDataT));
|
||||
w->data = d;
|
||||
d->text = text ? strdup(text) : NULL;
|
||||
w->accelKey = accelParse(text);
|
||||
}
|
||||
|
||||
return w;
|
||||
return widgetAllocWithText(parent, sTypeId, sizeof(ButtonDataT), text);
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -104,27 +95,18 @@ void widgetButtonPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bitma
|
|||
uint32_t fg = w->fgColor ? w->fgColor : colors->contentFg;
|
||||
uint32_t bgFace = w->bgColor ? w->bgColor : colors->buttonFace;
|
||||
|
||||
BevelStyleT bevel;
|
||||
bevel.highlight = w->pressed ? colors->windowShadow : colors->windowHighlight;
|
||||
bevel.shadow = w->pressed ? colors->windowHighlight : colors->windowShadow;
|
||||
bevel.face = bgFace;
|
||||
bevel.width = 2;
|
||||
drawBevel(d, ops, w->x, w->y, w->w, w->h, &bevel);
|
||||
drawPressableBevel(d, ops, w->x, w->y, w->w, w->h, w->pressed, bgFace, colors);
|
||||
|
||||
int32_t textW = textWidthAccel(font, bd->text);
|
||||
int32_t textX = w->x + (w->w - textW) / 2;
|
||||
int32_t textY = w->y + (w->h - font->charHeight) / 2;
|
||||
int32_t textX;
|
||||
int32_t textY;
|
||||
calcCenteredText(font, w->x, w->y, w->w, w->h, bd->text, &textX, &textY);
|
||||
|
||||
if (w->pressed) {
|
||||
textX += BUTTON_PRESS_OFFSET;
|
||||
textY += BUTTON_PRESS_OFFSET;
|
||||
}
|
||||
|
||||
if (!w->enabled) {
|
||||
drawTextAccelEmbossed(d, ops, font, textX, textY, bd->text, colors);
|
||||
} else {
|
||||
drawTextAccel(d, ops, font, textX, textY, bd->text, fg, bgFace, true);
|
||||
}
|
||||
drawWidgetTextAccel(d, ops, font, textX, textY, bd->text, fg, bgFace, true, w->enabled, colors);
|
||||
|
||||
if (w == sFocusedWidget) {
|
||||
int32_t off = w->pressed ? BUTTON_PRESS_OFFSET : 0;
|
||||
|
|
|
|||
|
|
@ -1045,7 +1045,7 @@ static const WgtMethodDescT sMethods[] = {
|
|||
};
|
||||
|
||||
static const WgtIfaceT sIface = {
|
||||
.basName = "PictureBox",
|
||||
.basName = "Canvas",
|
||||
.props = NULL,
|
||||
.propCount = 0,
|
||||
.methods = sMethods,
|
||||
|
|
|
|||
|
|
@ -67,16 +67,7 @@ void widgetCheckboxPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bit
|
|||
|
||||
|
||||
WidgetT *wgtCheckbox(WidgetT *parent, const char *text) {
|
||||
WidgetT *w = widgetAlloc(parent, sTypeId);
|
||||
|
||||
if (w) {
|
||||
CheckboxDataT *d = calloc(1, sizeof(CheckboxDataT));
|
||||
w->data = d;
|
||||
d->text = text ? strdup(text) : NULL;
|
||||
w->accelKey = accelParse(text);
|
||||
}
|
||||
|
||||
return w;
|
||||
return widgetAllocWithText(parent, sTypeId, sizeof(CheckboxDataT), text);
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -183,11 +174,7 @@ void widgetCheckboxPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bit
|
|||
int32_t labelY = w->y + (w->h - font->charHeight) / 2;
|
||||
int32_t labelW = textWidthAccel(font, cd->text);
|
||||
|
||||
if (!w->enabled) {
|
||||
drawTextAccelEmbossed(d, ops, font, labelX, labelY, cd->text, colors);
|
||||
} else {
|
||||
drawTextAccel(d, ops, font, labelX, labelY, cd->text, fg, bg, false);
|
||||
}
|
||||
drawWidgetTextAccel(d, ops, font, labelX, labelY, cd->text, fg, bg, false, w->enabled, colors);
|
||||
|
||||
if (w == sFocusedWidget) {
|
||||
drawFocusRect(d, ops, labelX - 1, w->y, labelW + 2, w->h, fg);
|
||||
|
|
|
|||
|
|
@ -211,12 +211,7 @@ void widgetImageButtonPaint(WidgetT *w, DisplayT *disp, const BlitOpsT *ops, con
|
|||
uint32_t bgFace = w->bgColor ? w->bgColor : colors->buttonFace;
|
||||
bool pressed = w->pressed && w->enabled;
|
||||
|
||||
BevelStyleT bevel;
|
||||
bevel.highlight = pressed ? colors->windowShadow : colors->windowHighlight;
|
||||
bevel.shadow = pressed ? colors->windowHighlight : colors->windowShadow;
|
||||
bevel.face = bgFace;
|
||||
bevel.width = IMAGEBUTTON_BEVEL_W;
|
||||
drawBevel(disp, ops, w->x, w->y, w->w, w->h, &bevel);
|
||||
drawPressableBevel(disp, ops, w->x, w->y, w->w, w->h, pressed, bgFace, colors);
|
||||
|
||||
if (d->pixelData) {
|
||||
int32_t imgX = w->x + (w->w - d->imgW) / 2;
|
||||
|
|
|
|||
|
|
@ -59,16 +59,7 @@ void widgetLabelPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bitmap
|
|||
|
||||
|
||||
WidgetT *wgtLabel(WidgetT *parent, const char *text) {
|
||||
WidgetT *w = widgetAlloc(parent, sTypeId);
|
||||
|
||||
if (w) {
|
||||
LabelDataT *d = calloc(1, sizeof(LabelDataT));
|
||||
w->data = d;
|
||||
d->text = text ? strdup(text) : NULL;
|
||||
w->accelKey = accelParse(text);
|
||||
}
|
||||
|
||||
return w;
|
||||
return widgetAllocWithText(parent, sTypeId, sizeof(LabelDataT), text);
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -109,15 +100,10 @@ void widgetLabelPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bitmap
|
|||
textX = w->x + w->w - textW;
|
||||
}
|
||||
|
||||
if (!w->enabled) {
|
||||
drawTextAccelEmbossed(d, ops, font, textX, textY, ld->text, colors);
|
||||
return;
|
||||
}
|
||||
|
||||
uint32_t fg = w->fgColor ? w->fgColor : colors->contentFg;
|
||||
uint32_t bg = w->bgColor ? w->bgColor : colors->contentBg;
|
||||
|
||||
drawTextAccel(d, ops, font, textX, textY, ld->text, fg, bg, false);
|
||||
drawWidgetTextAccel(d, ops, font, textX, textY, ld->text, fg, bg, false, w->enabled, colors);
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -95,13 +95,10 @@ static void invalidateOldSelection(WidgetT *group, int32_t oldIdx) {
|
|||
|
||||
|
||||
WidgetT *wgtRadio(WidgetT *parent, const char *text) {
|
||||
WidgetT *w = widgetAlloc(parent, sRadioTypeId);
|
||||
WidgetT *w = widgetAllocWithText(parent, sRadioTypeId, sizeof(RadioDataT), text);
|
||||
|
||||
if (w) {
|
||||
RadioDataT *d = calloc(1, sizeof(RadioDataT));
|
||||
w->data = d;
|
||||
d->text = text ? strdup(text) : NULL;
|
||||
w->accelKey = accelParse(text);
|
||||
if (w && w->data) {
|
||||
RadioDataT *d = (RadioDataT *)w->data;
|
||||
|
||||
// Auto-assign index based on position in parent
|
||||
int32_t idx = 0;
|
||||
|
|
@ -408,11 +405,7 @@ void widgetRadioPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bitmap
|
|||
int32_t labelY = w->y + (w->h - font->charHeight) / 2;
|
||||
int32_t labelW = textWidthAccel(font, rd->text);
|
||||
|
||||
if (!w->enabled) {
|
||||
drawTextAccelEmbossed(d, ops, font, labelX, labelY, rd->text, colors);
|
||||
} else {
|
||||
drawTextAccel(d, ops, font, labelX, labelY, rd->text, fg, bg, false);
|
||||
}
|
||||
drawWidgetTextAccel(d, ops, font, labelX, labelY, rd->text, fg, bg, false, w->enabled, colors);
|
||||
|
||||
if (w == sFocusedWidget) {
|
||||
drawFocusRect(d, ops, labelX - 1, w->y, labelW + 2, w->h, fg);
|
||||
|
|
|
|||
|
|
@ -91,6 +91,7 @@ A multi-line text editing area. This is a DVX extension with no direct VB3 equiv
|
|||
.table
|
||||
Method Description
|
||||
------ -----------
|
||||
AppendText text$ Append text to the end of the buffer and invalidate the widget.
|
||||
FindNext needle$, caseSensitive, forward Search for text. Returns True if found.
|
||||
GetWordAtCursor() Returns the word under the cursor.
|
||||
GoToLine line% Scroll to and position cursor at the given line.
|
||||
|
|
|
|||
|
|
@ -61,6 +61,7 @@ Header: widgets/textInpt.h
|
|||
.table
|
||||
Function Description
|
||||
-------- -----------
|
||||
void wgtTextAreaAppendText(w, text) Append text to the end of the buffer and invalidate the widget.
|
||||
void wgtTextAreaSetColorize(w, fn, ctx) Set a syntax colorization callback. The callback receives each line and fills a color index array.
|
||||
void wgtTextAreaGoToLine(w, line) Scroll to and place the cursor on the given line number.
|
||||
void wgtTextAreaSetAutoIndent(w, enable) Enable or disable automatic indentation on newline.
|
||||
|
|
|
|||
|
|
@ -240,6 +240,7 @@ static int32_t visualColToOff(const char *buf, int32_t len, int32_t lineStart, i
|
|||
WidgetT *wgtMaskedInput(WidgetT *parent, const char *mask);
|
||||
WidgetT *wgtPasswordInput(WidgetT *parent, int32_t maxLen);
|
||||
WidgetT *wgtTextArea(WidgetT *parent, int32_t maxLen);
|
||||
void wgtTextAreaAppendText(WidgetT *w, const char *text);
|
||||
bool wgtTextAreaFindNext(WidgetT *w, const char *needle, bool caseSensitive, bool forward);
|
||||
int32_t wgtTextAreaGetCursorLine(const WidgetT *w);
|
||||
int32_t wgtTextAreaGetWordAtCursor(const WidgetT *w, char *buf, int32_t bufSize);
|
||||
|
|
@ -1518,6 +1519,37 @@ WidgetT *wgtTextArea(WidgetT *parent, int32_t maxLen) {
|
|||
}
|
||||
|
||||
|
||||
void wgtTextAreaAppendText(WidgetT *w, const char *text) {
|
||||
if (!w || w->type != sTextAreaTypeId || !text) {
|
||||
return;
|
||||
}
|
||||
|
||||
TextAreaDataT *ta = (TextAreaDataT *)w->data;
|
||||
|
||||
if (!ta->buf) {
|
||||
return;
|
||||
}
|
||||
|
||||
int32_t addLen = (int32_t)strlen(text);
|
||||
int32_t room = ta->bufSize - 1 - ta->len;
|
||||
|
||||
if (addLen > room) {
|
||||
addLen = room;
|
||||
}
|
||||
|
||||
if (addLen <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
memcpy(ta->buf + ta->len, text, addLen);
|
||||
ta->len += addLen;
|
||||
ta->buf[ta->len] = '\0';
|
||||
ta->cachedLines = -1;
|
||||
ta->cachedMaxLL = -1;
|
||||
wgtInvalidate(w);
|
||||
}
|
||||
|
||||
|
||||
bool wgtTextAreaFindNext(WidgetT *w, const char *needle, bool caseSensitive, bool forward) {
|
||||
if (!w || w->type != sTextAreaTypeId || !needle || !needle[0]) {
|
||||
return false;
|
||||
|
|
@ -3610,6 +3642,7 @@ static const WgtPropDescT sTextAreaProps[] = {
|
|||
};
|
||||
|
||||
static const WgtMethodDescT sTextAreaMethods[] = {
|
||||
{ "AppendText", WGT_SIG_STR, (void *)wgtTextAreaAppendText },
|
||||
{ "FindNext", WGT_SIG_STR_BOOL_BOOL, (void *)wgtTextAreaFindNext },
|
||||
{ "GetWordAtCursor", WGT_SIG_RET_STR, (void *)basGetWordAtCursor },
|
||||
{ "GoToLine", WGT_SIG_INT, (void *)wgtTextAreaGoToLine },
|
||||
|
|
@ -3640,7 +3673,7 @@ static const WgtIfaceT sIfaceTextArea = {
|
|||
.props = sTextAreaProps,
|
||||
.propCount = 1,
|
||||
.methods = sTextAreaMethods,
|
||||
.methodCount = 10,
|
||||
.methodCount = 11,
|
||||
.events = NULL,
|
||||
.eventCount = 0,
|
||||
.createSig = WGT_CREATE_PARENT_INT,
|
||||
|
|
|
|||
|
|
@ -107,6 +107,11 @@ WidgetT *wgtTimer(WidgetT *parent, int32_t intervalMs, bool repeat) {
|
|||
w->visible = false;
|
||||
d->intervalMs = intervalMs;
|
||||
d->repeat = repeat;
|
||||
// Match VB Timer default (Enabled=True on create). Callers can
|
||||
// Stop() if they want it dormant.
|
||||
d->running = true;
|
||||
d->lastFire = clock();
|
||||
timerAddToActiveList(w);
|
||||
}
|
||||
|
||||
return w;
|
||||
|
|
@ -176,6 +181,12 @@ void wgtTimerStop(WidgetT *w) {
|
|||
|
||||
|
||||
void wgtUpdateTimers(void) {
|
||||
static int32_t sUpdateCount = 0;
|
||||
sUpdateCount++;
|
||||
if (sUpdateCount <= 3 || sUpdateCount % 100 == 0) {
|
||||
dvxLog("[T] wgtUpdateTimers call #%d activeCount=%d",
|
||||
(int)sUpdateCount, (int)arrlen(sActiveTimers));
|
||||
}
|
||||
clock_t now = clock();
|
||||
|
||||
// Iterate backwards so arrdel doesn't skip entries
|
||||
|
|
@ -192,6 +203,9 @@ void wgtUpdateTimers(void) {
|
|||
clock_t interval = (clock_t)d->intervalMs * CLOCKS_PER_SEC / 1000;
|
||||
|
||||
if (elapsed >= interval) {
|
||||
dvxLog("[T] timer tick: w=%p onChange=%p intervalMs=%d elapsed=%ld interval=%ld",
|
||||
(void *)w, (void *)w->onChange, (int)d->intervalMs,
|
||||
(long)elapsed, (long)interval);
|
||||
d->lastFire = now;
|
||||
|
||||
if (w->onChange) {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue